网工闯了什么祸?

上一篇很多读者一下就发现了答案,暂时先不写答案和分析,卖个关子,继续出一题。下一篇一起揭晓答案。

小王(就是你!)在一家创业型互联网公司上班。公司为了保证产品的稳定性,在上线之前会现在测试环境运行代码,保证没有问题,再发布到正式环境。

小王的公司比较拮据,为了省钱,公司购买了一些陈旧的二手设备,运行测试环境。虽然性能比较差,但是毕竟测试环境只有开发人员的测试流量,所以没有什么问题。

随着部署的东西越来越多,原来一个机架已经不够用了,他们就准备扩展一个新的机架。网工效率很高,连夜操作,设备马上上线了。网工比较邋遢,通电了就下班了。

第二天小王来一看,测试环境网络不通了。这种情况一般人直接去打网工了。但是小王不是,小王总是抓住任何一个检验自己能力的机会,用有限的环境得到尽可能多的信息,推理出最可能的根因,然后再去找相关的同事解决。而不是直接去问同事:「我这里网络不通快给我看看是什么问题。」

现在的情况是:

  • 小王发现请求发给另一个服务总是超时;
  • 小王去 ping 了一下另一个服务的地址,当前的机器地址是 10.0.0.1,去 ping 目标地址 10.0.0.4 发现是不通的;
  • 于是小王保持当前的 ping,然后10.0.0.1 的机器上抓包,命令是 tcpdump -i eth0,得到的抓包文件如下。

请下载这个文件,分析抓包内容,解释:当前的网络出现了什么问题?

欢迎在评论区留下你的想法。

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?
  7. 网工闯了什么祸?
  8. 网络中的环路和防环技术
  9. 延迟增加了多少?
  10. TCP 延迟分析
  11. 重新认识 TCP 的握手和挥手
  12. 重新认识 TCP 的握手和挥手:答案和解析
  13. TCP 下载速度为什么这么慢?
  14. TCP 长肥管道性能分析
  15. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
 

SRE 书单推荐

推荐一些适合 SRE 或者想要从事 SRE 的读者阅读的书籍。

其中,最里面的虚线框推荐学生阅读,非科班转 SRE 也可以从这些书入手,第二个虚线框意思是 SRE 工作中用的到的内容,从实用主义讲,推荐应届生如果第一次入职了 SRE 的岗位的话,可以阅读这些书。

我没有读完所有的书,有一些是很多高手推荐,应该不会错,所以也列在这了这里,比如 CSAPP 这本。

这个书单强调基础知识,那些经过 10 年也没有变化太多的内容,很少涉及工具。SRE 的工作对很多工具也有要求,比如 Ansible,Kubernetes,Nginx,命令行工具 awk, sed 等等,这些工具可以通过书籍学习,也可以通过其他的网站学习,内容与时俱进,我当时学习的时候看的资料可能过时了,所以不好作推荐。

但是有 3 个方面除外:Python, Vim, Tmux. 这些工具掌握了会受益终生,所以我列出来一些有关这些工具的资料。无论你第一门语言使用的是什么,都推荐学习一下 Python。编辑器和 IDE 选择哪一个并不重要,这也不是什么值得骄傲的事情,但是无论使用哪一个,都不要固步自封,每天学习一下提高效率的快捷操作。推荐一下 Vim,理由是插件丰富,键位比 Emacs 合理,(不要打我,我是 Emacs 转过来的,Emacs 动辄就是好几个键一块按)。推荐 Tmux 是因为,学会了它你可以在任何终端模拟器,比如 iTerm2, Kitty, Konsole 或者 ssh 都有一致的体验。

点击这里可以看高清图。虚线表示可以互相替代。箭头表示推荐按照顺序阅读。

其中,Google SRE 这本书可以在线免费阅读,完整地介绍了 SRE 的工作。如果之前不是做 SRE 工作的,强烈建议在入职 SRE 工作之前阅读此书。因为很多岗位是打着 SRE 的幌子却做的是运维工的工作。读完此书可以防止自己被骗。也能知道正确的工作方法是什么样子的。

The Missing Semester of Your CS Education 建议应届生阅读,介绍了当前大学教育没有教但是却非常有用的知识。

 

四层负载均衡分析:美团 MGW

之前的文章中,分析过 GoogleCloudflare 的四层负载均衡设计,都是使用了 DSR 的模式,但是在技术细节方面根据自己的业务需求作了不同的决策。今天,我们继续来讨论一种与这两家公司都不一样的设计:美团的 MGW。

美团的架构设计在中国的互联网公司比较流行,我知道的很多公司都是类似的设计,所以很有代表性。选择美团 MGW 来介绍,是因为可以参考这篇美团技术博客,其他公司公开的资料好像不多。其中一些细节问题,美团没有介绍的,我来根据我知道的补充一下细节,扩展讨论一下。

MGW 总体架构

FullNAT 转发架构

FullNAT 转发模式,类似于 Nginx,和客户端建立连接,然后再和 RS 建立一个 TCP 连接。架构上比较简单,对网络设施的要求少(几乎没要求)。缺点是性能差一点。然后 RS 看不到客户端真实 IP,需要通过其他技术方案解决透传 IP 问题。

FullNAT 转发模式

之前讨论的 DSR 好处很明显:因为回包不走 LB,直接是 RS 通过路由发给客户端,所以性能很高,非常适合大部分的服务场景:发进来的请求很小,出去的响应很大。相比之下,FullNAT 进出都要走 LB,所以 LB 要多花很多计算资源给回包,效率低很多。

FullNAT 相比于 DSR 的优点有:

  • FullNAT 可以做端口转换,例如对同一个 VIP 来说,可以配置让 1.1.1.1:80 到一组 RS 的 80 端口,然后配置让 1.1.1.1:8080 到另一组 RS 的 80 端口。DSR 模式是做不到的,因为在 DSR 模式下,对于 RS 来说,感受不到 LB 的存在,入的包看起来就是客户端 IP 发给 VIP 的,它回包也是用 VIP 回给客户端 IP。所以暴露的端口必须是客户端访问的端口。

我认为只有这一个优点。看起来很不起眼,为什么会称之为「优点」呢?因为这让不同的 RS 可以灵活地 listen 不同的端口,但是通过 FullNAT 暴露出去的端口都是一致的。比如,如果容器部署模式选择了 Host 模式,那么所有的容器其实都运行在 Linux Host 的 root network namespace, 同一个端口只能有一个进程 Listen。如果部署很多个实例,不能让它们都 listen 在同一个端口,要给不同的服务分配不同的端口。所以,一个服务 A 的内网地址列表可能是 10.1.1.10:8080, 10.2.2.20:8081,这样的话,通过 FullNAT 的转换,客户端始终可以通过 VIP:80 来访问。

有的地方提到,另一个「优点」是 FullNAT 可以做地址转换。这个显而易见是可以做到的,因为这就是 FullNAT 的含义。不过,向 RS 隐藏了客户端的真实 IP,大部分情况是一个缺点,而不是一个优点吧?在 DSR 的模式下,所有的 RS 都需要配置 VIP,以便向客户端直接发送回包。美团博客将此列为一个缺点,但是我不明白为什么是缺点。如果说 VIP 暴露出去会被攻击,这个是可以避免的,只要不对这个 VIP 发送路由出去就好了,只保留在本地作为回包用的 src IP。那么缺点可能是多一步部署?但是我觉得配置 VIP 没有多麻烦。

连接保持方案

FullNAT 下,TCP 连接保持有两个难点。

一个是 session 的同步,如图所示,假设 TCP 连接是在 MGW-1 新建,那么假设 MGW-1 挂了,RS 把包发给了 MGW-2 的时候,MGW-2 要知道这个 MGW-RS TCP 连接对应哪一个 RS-MGW 连接。

这个问题其实不难解决,通过 IP Multicast 或者外部的同步服务都可以做到 session 同步。

连接保持问题

另一个问题是,假设 MGW-1 和 RS 之间的 TCP 连接五元组是 MGW-1 IP, MGW-1 Port, TCP, RS IP, RS Port,如果现在这个连接迁移到 MGW-2,会发现这个五元组变成了 MGW-2 IP, MGW-2 Port, TCP, RS IP, RS Port, 这样,即使 MGW-2 知道这个 TCP 连接的状态,也无法和 RS 正确通信,无法替代 MGW-1 和 RS 直接的连接。要想替代 MGW-1 的话,必须保持五元组不变。现在看到,变的部分有两个:

  1. 首先,MGW-1 PortMGW-2 Port 要相同,这个很简单,通过 session sync 知道这个 Port 就可以了;
  2. 其次,需要保持 MGW-1 IPMGW-2 IP 相同,那就需要让所有的 MGW 实例,用相同的 IP 去和 RS 建立连接;

「把相同的 IP 绑定到多个实例上」,这就是 VIP 呀。

是的,其实 MGW 和 RS 之间也是用 VIP 连接的,只不过这部分发生在内网中,我们叫这个 IP 为 Local IP,简称 LIP。

两个问题都解决了,这时候我们发现出现了一个新问题。所有的 MGW Local IP 都一样,那么 RS 把回包发给谁都可以,要想 MGW 能正确转发包回客户端,就必须让 MGW 之间的 session 同步速度要比 MGW 发给 RS 包的速度快。不然的话,RS 响应都回来了,收到包的 MGW 还不知道这个包应该会给哪个客户端。一种简单的方法就是让 MGW 先等等,等到有关这个 session 的信息发过来,再进行转发。另一个不是办法的办法,让 session 同步的速度快一些。

还有一个更好的办法,就是「浮动路由」。

路由器的路由表中,到达某一个网段的选择不只有一条,可能存在多条。但是在路由选择的时候,一定会选择最优的一条,来尽量保持同一个连接的 order。但是假设最优的路由挂掉了,那么次优的路由条目就变成最优的了,相当于「浮动」到上面了,路由器就会选择这条,故称之为浮动路由。

浮动路由,图片来自思科

假设我们有 3 个 MGW 实例,我们就使用 3 个 LIP,每一个 MGW 都绑定这 3 个 LIP,其中:

  • 如果是发给 RS 包,那么 MGW 只会使用一个来发,MGW-1 使用 LIP1,MGW-2 使用 LIP2,MGW-3 使用 LIP3;
  • 如果是接收 RS 包,无论哪一个 LIP 的包都会处理;
  • 在 VIP 参与路由宣告的时候,MGW-1 宣告自己的 LIP1 路由优先级是高,LIP2 是低,LIP3 是低;MGW-2 宣告自己的 LIP2 是高,LIP1和3 是低;MGW-3 宣告自己的 LIP3 是高,LIP1 和 2 是低;
使用浮动路由来让每一个 MGW 都有一个主要的 LIP

这样,在正常情况下,RS 回包给 LIP1 的时候,路由器总是会发给 MGW-1,会给 LIP2,路由器会发给 MGW-2. 不依赖 Session 同步,MGW 之间不需要同步状态。只有当其中某一个 MGW 挂了的时候,挂了的 MGW 的主 LIP 会自动迁移到其他的 MGW 上处理。

数据面的实现

MGW 是基于 DPDK 的实现,也就是在 userspace 直接和网卡交互,跳过 Kernel 协议栈的内容。

DPVS 是爱奇艺开源的四层负载均衡软件。可以简单理解为,这就是给 LVS 换成了DPDK 的接口。美团博客没有说是不是基于 DPVS 的开发,这里我们就以 DPVS 来讨论吧。

LVS 是中国的开源软件,基于 IPVS 实现了四层负载均衡,在当时是很了不起的技术。很多公司之前都是用了 LVS 作为四层负载均衡器。

但是随着互联网的发展,流量越来越大。一方面是用户越来越多,另一方面是代码越写越差。以前一个几M 的应用能做的事情,现在需要下载几百M的应用。以前手机 8G 存储就了不起了,现在 128G 都不够用。同样的带宽要求也越来越高,4G, 5G 了,还是觉得不够快。四层是面向互联网的接入层,四层负载均衡要承担的流量也就越来越大,LVS 就显得不够用了。

LVS 是基于内核 Netfilter 的程序,已经走了一部分的网络栈了,为了更高的性能,我们就会想到 bypass kernel。一种方式是如 Cloudflare 一样 XDP 在网卡上完成转发,另一种就是在用户态直接和网卡交互,跳过 Kernel。

DPDK 由英特尔的工程师 Venky Venkatesan (被称为「DPDK之父,已于 2018 年去世」)创造,是一个编程库,可以在用户态和网卡交互,从而跳过 kernel,带来更高的性能。

跳过 Kernel 带来的问题是,Kernel 的功能都不可以用了,socket API 就是其中之一。如果不用 Kernel,就意味着你要自己实现 TCP 栈,维护连接,buffer 之类。在用户态实现 TCP,有过各种各样的尝试,但是都存在各种各样的问题。因为 TCP 太复杂了,即使完全按照 RFC 来编程,都不能处理所有的细节,很多在 Kernel 的实现甚至成为了事实标准,难以兼容。

但是,对于四层负载均衡这种场景来说,用 DPDK 就看起来再合适不过了。在之前的博文也提到过,四层负载均衡其实不是一个完整的 TCP 实现,它更像一个三层的路由,按照三层 IP 包来转发。它不维护 TCP 的 buffer,不维护窗口,不负责重传,只是把收到的 IP 包发给 RS。只不过它做的三层路由会查看 TCP 的端口,flags 等,作为转发的依据。所以实现起来比完全的 TCP 栈要简单。

另外多说一句,这个系列的文章主要分析的是四层负载均衡的技术方案,软件的实现只占一部分,像 DPVS,LVS,都可以支持 DSR,NAT,FullNAT 等,一套完整的方案还要包括配置管理,连接保持方案,转发架构设计等等。这个系列着重讨论网络部分,如果读者对软件实现感兴趣,可以阅读 Linux 网络源码分析类的书籍。

除了 bypass kernel,在还有其他的优化可以提升性能。

忙轮询

Kernel 协议栈的工作模式是,如果网卡收到了包需要处理,就通过中断告诉系统,系统再来处理。因为 Linux 是一个多功能的,通用的操作系统。而 DPVS 的工作方式是,一直在轮询处理网卡的包,需要分配单独的 CPU 完全来干这个事情,即使没有包,也一直在轮询,CPU 始终是 100%。(如此可以看出,为什么 Cloudflare 不会选择这种方式了,这样就没办法把负载均衡部署到所有的机器上了。)这样可以将效率提升很多,延迟和吞吐都有提高。

Session 锁

这个不是 DPVS 带来的问题,而是 FullNAT 带来的问题。

由于网络包进来是一对五元组,转发到 RS 又是另一对五元组,所以必须得维护着两个五元组之间的关系才行。包从 VIP 进入 LB 的时候,选择一个 Local 端口,Local IP 发送出去,然后记录这个 Client- LB 连接和 LB-RS 连接的对应关系。从 Local IP 收到回应的包的时候,就不能随意选择了,要去查找一下,这个 LS-RS 连接对应哪一个 Client- LB 连接,使用对应的连接发回去才行。这个两边的连接的对应关系叫做 session table。

Session table 要记住每一个对客户端的连接对应哪一个 RS 连接

并发访问这个 session table 会带来竞争问题,需要加锁。

但是什么地方带来的竞争,我从美团的博客上没有读明白。博客介绍的原文如下:

之前介绍MGW使用FULLNAT的模式,FULLNAT会将数据包的元组信息全部改变,这样同一个连接,请求和应答方向的数据包有可能会被RSS散列到不同的网卡队列中,在不同的网卡队列也就意味着在被不同的CPU进行处理,这时候在访问session结构的时候就需要对这个结构进行加锁保护。

同一个连接的读写为什么会产生竞争问题呢?假设第一次连接进来的时候在 CPU0,流程如下:

  1. 选择一个 local port,然后保存 client ip, client port, vip, vip port 与 local ip, local port, RS ip, RS port 的对应关系;
  2. 然后转发到 RS;

回包的时候,即使到了 CPU1,那么流程如下:

3. 查表,查到之前保存的对应关系;然后根据这个关系选择 VIP port 来转发给客户端。

可以看到,3 必定发生在 1 和 2 之后,看起来不存在竞争。(读者理解这个地方的话,欢迎指点)

我猜测,可能竞争是发生在不同的 CPU 都要在 session table 新建内容,比如 CPU0 要添加,CPU1也要添加,这时候有两个写,必须锁起来一个一个操作。

读者评论指出,这里可能是需要内存屏障

解决的思路,就是将数据隔离开,不同的 CPU 之间不需要访问共享数据的部分。

美团的方法是,给每一个 CPU 绑定一个 Local IP,CPU0 使用 local ip0,CPU1使用 local ip1,这样没有数据共享,就不需要加锁了。使用网卡的提供的 flow director,可以做到将 local0 的数据包全都给 cpu0 的队列处理。(如果结合上面提到的浮动路由使用的话,那么每一个 MGW 的 CPU 都需要有一个 Local IP 并且绑定到集群的所有实例中。)

减少上下文切换

就是把跑数据面的进程绑定到固定的 CPU 核心上,然后像 bash,ssh,等其他程序都绑定到其他的核心上。这样,数据面进程永远不会处理中断之类的事情,只会跑重要的数据面进程。

运维优化

MGW 实例的健康检查。由于路由器从 ECMP 层面摘除节点,只会发生在端口 down 的情况下。假设端口没 down,但是程序已经挂了,那路由器感知不到,还是会把流量发送过来,这部分流量只能被丢弃。所以 MGW 实现了一个健康检查,假设程序异常,直接给网卡断电,能够实现快速切换到其他 MGW 实例上去。

新的 MGW 上线的时候,会先不接收流量,先从其他实例增量同步过来 session table,同步完成,才开始服务流量。

RS 的优雅下线。在 RS 下线的时候,MGW 可以保持旧连接,但是新连接不再发送过来。直到旧连接都顺利结束,RS 开始下线。

支持让相同客户端发送过来的请求,都发送到相同的 RS 上面,是基于对客户端的 IP hash 来实现的。但是为了避免 RS 变化的时候,整个重新 hash,这里借鉴了 Google Maglev Consistent Hash 的方法。(不过有个小疑问,如果大公司办公室用一个出口 IP,那不是午饭的时候点外卖全都到了同一个 RS 上面了?)

以上就是这篇分析了。总体来看,使用 FullNAT 牺牲了部分性能,但是技术的复杂程度,运维复杂度都降低了很多。

四层负载均衡系列文章

 

四层负载均衡分析:Cloudflare Unimog

四层负载均衡漫谈 介绍了四层负载均衡需要解决的问题,和一些常用的解决方案之后,通过学习一些其他公司的四层技术方案,我们会发现不同的公司在针对自己的业务做定制的时候,会有不同的取舍,非常有意思,我们精彩继续。

今天来分析 Cloudflare 的四层负载均衡方案:Unimog,主要参考的是这篇 Unimog – Cloudflare’s edge load balancer。Cloudflare 是一家做 CDN 的公司,四层负载均衡对他们来说至关重要,在技术选择上面,可以看到相比于 Google 的 Maglev,Cloudflare 选择的是性价比更高,更适合他们的方案。

Unimog 的技术选型和 Maglev 类似,但是实现上有很大的不同

Cloudflare 这家公司是使用自己设计的硬件服务器的,按照自己的情况量身定制,最新一代已经是第 12 代中文版)了,最新架构采用了 2U 的设计,看似浪费空间,但是整体上可以降低机架的功率,2U 可以用大风扇,散热性能会更好,总拥有成本(TCO)更低。而且,Cloudflare 机房的空间看起来不是很紧张,这种设计符合达到他们的最大性价比。以此高效的运营效率,无所不用其极降低生产成本,才可以让 Cloudflare 提供免费的服务,为付费用户提供优质的服务。

CPU 的性能逐年提高,服务器已经设计到 12 代了,那么前 11 代怎么办?难道上架新服务器,就直接退役旧服务器吗?肯定不是的,要让服务器在岗位上榨干最后一点价值。所以机房内的服务器都是不同型号混合部署的。这时候就有一个问题:新的服务器性能好,旧的服务器性能差,如果应用大规模部署,有的在新服务器上,有的在旧服务器上,如果负载“均衡”发给每一台机器上的 QPS 都相等——QPS 太高的话,会让旧服务器上服务的请求延迟比较高;QPS 太低的话,会让新服务器的 CPU 利用率不足。

所以 Unimog 有一个非常重要的特性,就是可以根据(RS)机器的负载自动调整连接数量:如果负载低了,会承载更多的连接,负载高了,新连接就不会过来,所谓能者多劳。(对比之前 Google 的负载均衡设计可以看出,Google 着重的是稳定性,能够近乎完美地保持连接,即使应用升级也不会发错,但是每一个 Maglev 实例收到的请求是一样的,Maglev 发给 RS 的 QPS 也是等分的。)

动态负载均衡还有一个好处:有一些连接花费的时间比较多,有些花费的时间少,动态调整负载,可以在连接成本不均衡的情况下达到负载均衡。有时候机器上同时运行了其他程序带来一些额外负载,动态负载均衡也能在这种情况下降低此机器的连接数。这都是静态的权重调整做不到的。

通过下图可以看出,在部署 Unimog 之后,所有服务器,不管型号新旧,利用率趋于一致。

不同的颜色是不同的 Server 型号,白线是 Unimog 部署时间。

Unimog 的设计原则是软件兼容硬件。Unimog 要运行在 General Purpose 的服务器上,而不是运行在专用服务器上。这样,硬件团队可以根据机房设计最大效率的机器,软件团队去榨干硬件的性能,优化整体的效率。

除了这些,Cloudflare 在意的地方还有一个,就是性能。因为作为 CDN 公司,最重要的业务就是防 DDoS,保护客户的网站,所以高性能丢弃攻击者的数据包是一个重点功能。

全都是负载均衡

基于以上特点,Cloudflare 的部署看似疯狂但是又很合理——机房内的所有机器都是负载均衡器。即所有的机器都安装了四层负载均衡,路由器无论把包转发给哪一个机器,这个机器都可以转发到正确的 RS 上。

这样做有几个好处:

  • 负载均衡不需要做容量规划了,因为能用的机器都已经用上了;
  • 最大限度做 DDoS 防护(主要由四层负载均衡的一个组件,L4Drop 来负责),也是因为所有能用的机器都上阵了。Cloudflare 总体的网络架构是 Anycast,即200多个城市的机房都宣告一样的 VIP 路由出去,用户访问一个 Cloudflare 的 IP,会被路由到最近的地方。所有的机房都能处理所有的流量,机房内所有的机器都能处理流量,最大化了自己的资源来运行业务;
  • 运维架构简单,因为所有的机器都是一样的。其实除了 Unimog,在 Edge Server 上都运行了 Workers, WARP 等业务,Edge Server 都是一样的。
  • 如果单独划出来机器部署负载均衡,为了让效率最大化,要花很大的努力让它达到最大的吞吐,比如优化 LB 之前的网络链路,优化最大吞吐下的性能瓶颈。而全员 LB 的情况下,就不需要为了让机器达到最大的 load 做优化了。

要做到这个,在设计上要求 Unimog 能够和应用一起部署。应用不能影响 Unimog 的运行,Unimog 的部署也不影响应用。

转发链路

像 Cloudflare 这种主要防护 ingress 方向的流量,用 DSR 是再合适不过了。

DSR 封装转发链路

和 Google 相同的是,Cloudflare 也是用了封包来跨越 2 层。和 Google 不同的是,Cloudflare 用的方式是 GUE(Generic UDP Encapsulation),而不是 GRE。就是把二层包放到一个 UDP 包里面传输。

封装就要解决 MTU 超过 1500 bytes 的问题。Google 的方式是 MSS Clamping, 而 Cloudflare 的方式是用 Jumbo Frame,因为超过 1500 bytes 的封装部分只发生在机房内部,只要机房内能够传输 MTU 超过 1500 bytes 的包,就不会有问题。而且,Jumbo Frame 可以允许网络设备将多个小包合成一个大包来转发,效率更高。

连接保持技术

因为网络流量大,而且部署的 LB 实例很多,所以在 LB 实例之间同步状态的做法就不可行。

Cloudflare 用的连接保持方案类似 Google Maglev:所有的 LB 实例自己做决定,但是要保证对于相同的TCP 四元组,要得到相同的 RS IP。这样,就能保证,属于相同的 TCP 连接的包总是能到相同的 RS 上。

选择 RS 的流程如下:

  1. LB 实例独立计算 TCP 四元组 hash,得到一个 hash 值;
  2. 根据这个 hash 值查表,这个表叫做 forwarding table,然后得到一个 RS IP;
  3. 转发到 RS IP;

由此流程可以看出,保证相同四元组达到相同的 RS 的关键,是所有的 LB 都使用一样的 forwarding table。这个 forwarding table 是 control plane 统一下发的。(这个和 Google 不同,Google 是独立计算。)

根据转发表进行转发

如何调整不同的 RS 的流量比例呢?假设有 100 个 RS,那么就设置 100 倍的 bucket 数量,即 1 万个。每 100 个 bucket 对应 1 个 RS。如果要让有的 RS 收到的流量多,可以将更多的 bucket 对应到这个 RS。

这样也解决了另一个问题,假设 RS A 下线了,那么不需要重新计算整个 forwarding table,只需要将 RS A 对应的 bucket 修改成其他的 RS 好了。只有 hash 到 RS A 的 TCP 连接需要到其他的 RS 上去。

这里还有一个问题:如果 forwarding table 变了,比如 RS A 的 bucket 替换成了 RS B,对于新的 TCP SYN 包,转发到 B 是没有问题的。但是原来连接 A 的,如果到了 B,会被 B 直接拒绝,返回 RST——我不认识你。所以这里要解决一个问题,就是 forwarding table 发生变化的时候,已经存在的连接要保持住,比如原来发送给 A 的,还是需要发送给 A。

解决方案非常巧妙:forwarding table 中,每一个 bucket 都对应两列 RS,第一列是当前主转发表(First hop),第二列是备份转发表(second hop)。当发生变化的时候,对应的 bucket 主转发表 RS A 变成了 RS B 或者 RS C,备转发表变成 RS A,如下图所示。

当一个 RS 机器收到包的时候(比如 RS B),先检查一下当前机器上有没有这个 TCP socket,如果有,正常处理。如果没有,就再次转发,按照备转发表转发到 RS A。

但是假设这个 RS A 要下线了,那它在备转发表里面要待多久呢?这个问题文章没有解释,我发邮件了问了 Cloudflare 这篇博客的作者 Daivd Wragg,Daivd 给我回复了非常详细的解释:

  • 删除实例在 Cloudflare 不太常见,更常见的是短暂移除,reboot,然后回到集群,几乎所有的机器每月都会重启一次,所以每一个连接在 24 小时之内都有 1/30 的几率被 reset;
  • 如果要移除的话,如文中所说,会进入到 drain state,在备表中,这个状态只会维持几分钟;
  • 简单来说,连接可以归为两类:short-lived 和 long-lived。几分钟足够所有 short-lived 连接结束了,剩下的都是 long-lived,如果 drain 状态 30min,可能会有部分连接正常结束,但是不会很多,大部分超过几分钟的连接会存在数小时甚至数天,继续等待也不会带来更多显著受益,但是会让操作效率大大降低,所以只等待几分钟。

因为这个检查和 备转发表 转发,是在 RS 上实现的,所以不是 Unimog,而是一个 Redirector 组件。

这里有一个细节优化是,前面的 XDP 程序必定经过一次 hash + 查表了,在那时候,XDP 程序会将主表和备表结果都拿到,然后将备表的 RS 封装在 GUE 的 extension 中。这样,如果 Redirector 要转发的话,就不需要查表了,直接从 header 中拿到 RS IP 就可以了。

多一跳毕竟不太好,会增加延迟。不过好在这只会发生在当 forwarding table 有变化的时候影响长连接,这部分流量比较小,只占 1%。

Redirector 也负责 GUE 封装的卸载,即上面提过的在 RS 端做的 decapsulation。

和 Unimog 不同,Redirector 不是在 XDP 里面实现的,而是作为 TC classifier program 实现的。因为 XDP 跳过了大部分的网络栈,tcpdump 看不到,不好 debug。而相比 Unimog,XDP 带来的性能提升在 Redirector 这里不明显。所以可以用牺牲一点效率来换取更好的 debug 体验。

这个实现中,forwarding table 只保存了 2 个 hop,假设一个 bucket 在一段时间内变换两次:从 A -> B 然后 -> C, 那么 forwarding table 中只会记录 B 和 A,原来和 A 之间的连接就会失效。Beamer 论文提出用 3 个 hop,连接保持功能将更稳定。Cloudflare 认为,2 个 hop 就足够了。他们有一个优化是,在修改 bucket 的时候,尽量挑选时间最长没有变化的(leatest-change),去修改。比如扩容一个 RS X,就挑选一些很久没有变化过的 bucket,改到 RS X。

另外,如果把 buket 对应的主表记录和备表替换的话,只会影响谁来处理新的连接,而不会影响已经存在的连接。几乎也是「免费的」,所以,Unimog 会通过这种方式调整主备两个 RS 之间的负载。

基于 XDP 的软件实现

Unimog 选择了 XDP 来做包转发。这个选择非常适合 Cloudflare:

  • 如果使用 DPDK 的话,需要和网卡配合,这样对硬件有要求了,而 Cloudflare 的思路是软件兼容硬件,任何机器都能跑负载均衡;
  • Kernel Module 不方便开发测试和部署;
基于 XDP 的转发架构,图来自官方

XDP 允许我们把程序直接 attach 到网卡上,每次有包过来,就会执行我们的程序。程序执行之后会得到三种结果中的一种:

  • DROP 丢弃,这是 l4drop 的主要功能,把攻击者的包直接这里类丢弃
  • TX 转发,XDP 程序可以直接对 packet 作出修改,然后通过网卡转发出去
  • PASS l4drop 和 Unimog 都不处理,交给 Kernel 来处理。因为上面提到过,一台机器部署了不止 Unimog 一个程序,其他的程序可以正常使用 Kernel 的技术栈

从这个架构图可以看出,丢包发生在几乎最前面,差不多在网卡上了。在应对攻击的时候,机器采取了最大的性能进行丢包,而且用上了所有的机器资源,让 Cloudflare 能够提供的防护能力最大化。

XDPD

XDPD 是 XDP daemon 的意思。对比之前 Google Maglev 讨论配置下发的部分,这里就由 XDPD 负责。

XDPD 的主要工作是:

  • 负责初始化 XDP 程序,将 XDP 程序 attach 到内核,并且帮 XDP 程序初始化需要的 map 等;
  • XDP 程序运行需要读一些动态的配置信息,这些配置信息由 XDPD 从控制面获取。由于使用 map 传递,需要借助 helper function,性能会差,所以 XDPD 在 load XDP 程序之前会将这些值直接作为 const 插入到程序中;
  • 负责暴露监控需要的 metrics 信息,也是从 map 中读取数据暴露实现的;
  • xdpd 支持优雅重启,升级的时候不会中断 Unimog 和 l4drop;

这个架构的性能高,资源消耗少。LB 自身消耗的资源,对比服务用户业务消耗的资源,比值是 1%,也就是说 LB 几乎不消耗资源,免费的 LB。

以上几乎就是数据面的技术内容了。Unimog 还有一个重要依赖,就是控制面。

控制面 Conductor

控制面叫做 Conductor,负责 forwarding table 的生成,健康检查,RS 管理,负载均衡(通过调整 forwarding table)等等。

Conductor 基于 Consul 来实现:

  • 使用 Consul 的 KV 存储来下发 VIP 配置和 forwarding table
  • 依赖 Consul 对 Unimog 做健康检查
  • 使用 Consul 的锁功能,确保只有一个 Conductor 在 active 状态,如果挂掉了,其他的 Conductor 实例可以拿到锁并开始运行

Conductor 会周期读 Prometheus 中的 metrics,判断 RS 的负载,然后根据负载去调整 forwarding table 中 RS 出现的次数,以此来平衡所有的 RS 之间的负载。

在分布式的系统中,还会有很多复杂的问题。在部署 Conductor 之后,他们发现会出现整个机房都在 overload 的情况:一开始是一些机器出现负载较高,然后健康检查发现问题,Unimog 停止发给这些 RS 流量,自动迁移到其他机器,然后其他机器也开始 overload,最终导致整个集群都 overload。目前文中说的解决办法是,Conductor 需要区分部分机器 overload 和当前整个机房负载整体较高的情况。

UDP 的支持

UDP 是无连接的,分成两种场景讨论。

一种是 ping-pong 类型,像 DNS,一个包出去,一个包回来,这种可以跳过 2 hop 的维护,一个 hop 必中。

另一个是请求发出去,响应源源不断的回来。这种场景,还是要维护这些包都到同一台 RS 上。可以用和 TCP 一样的方法来处理,但问题是 UDP 没有 SYN 包,怎么来区分出来是否是新建的连接呢?

方案是这样的,我们还是像 TCP 一样维护 2 hop 的表:

  • 第一个 hop 的 Server 收到之后,检查当前机器是否有对应 4 元组的连接(是的,UDP 虽然是「无连接的」,但是你在 Linux 用 ss 工具来查看,也是能看到 UDP 的「连接的」,实现上也有 socket 和连接的概念);
  • 如果没有,直接转发给第二个 hop。

于是现在 UDP 的新连接几乎都去第二个 hop 了。和 TCP 相反。

所以 Conductor 在添加 Server 的时候,如果想要它接收 TCP 流量,应该放到 first hop,如果想让它接收 UDP 流量,应该放到 second hop。(这个地方我感觉很奇怪,之前的描述,第二 hop,即备份表,应该是即将下线的 Server 正在 drain connection,这时候作为 UDP 不是反而开始接收流量了嘛?虽然问题不大,但是不如让 UDP 永远尝试 second hop 先,如果找不到再去 first hop,好像更好,可以保持行为一致。我的这个想法带来的另一个问题是,在实现上讲,这里先去第二个 hop,再去第一个 hop,从网络层面更加违反直觉。评论l2dy指出。)

这种还有一个缺点,就是如果使用 unconnected sockets,在系统找不到对应的四元组的话,就无法工作了。

有一些 UDP 流量本身有 flow 的概念,比如 QUIC。这样,即使像手机这种设备经常变换 IP 的情况,flow id 也是不变的。Unimog 可以从这些 UDP 流量中识别出来 flow id,然后基于 flow id 做 hash,而不是基于 4 元组。这样即使 IP 变了,同一个 flow 也可以保持住。目前 WARP 产品用了这个特性。

Special thanks to David Wragg for patiently answering my questions.

四层负载均衡系列文章

 

不可以用路由器?

小李是一个刚上大学的大学生,来到学生宿舍,小李和他的舍友一起办了宽带。接好路由器,准备在宿舍进行上网冲浪的时候,他们发现无法正常上网。怎么回事?明明办宽带的时候,他们用自己的电脑接上网线验收了的呀!于是他们打电话给宽带公司,宽带公司说,只能一个设备上网,不能用多个设备一起上网,要想上网呀,宿舍里面的每一个人都要去办一个宽带才行。

真是奸商!但是小李认为,运营商是不可能知道我们是否使用了路由器的。理由这这样:运营商拉了一根线到我们的宿舍,这条线的一头是运营商的设备,另一头运营商也不知道是什么。

运营商和设备之间通过一条线连接

有这么一个谚语:在互联网上,没有人知道你是一条狗。

假设我的狗是电子狗,它能发出来遵守以太网协议的数据,我的狗就可以直接跟 ISP 之间进行交流,它就能上网了。这就是网络协议的本质,它规定了不同的设备(或者生物?)之间交流的方式,只要能遵守这种「交流方式」,就可以进行交流。

在互联网上,没有人知道你是一条狗。

无论是什么黑科技,最终网络都是遵循不同的协议来传输数据的,这背后藏不住秘密!我们一定能找到答案!

小李首先想到:既然同一条网线用在一个网络设备上可以,另一个网络设备上却不行,那会不会是 MAC 地址的问题?网线另一端的 ISP 只允许电脑的 MAC 地址,如果是路由器的 MAC 地址,就拒绝掉。有一种叫做 Sticky MAC 的端口安全技术,指的是,当交换机的端口第一次发生流量的时候,交换机就记住这个端口的 MAC 地址,从此之后,这个端口就只能允许这个 MAC 来访问。

验证这个想法很简单,小李让宿舍的另一个同学,把他的电脑接在了网线上。结果发现,即使是另一台电脑,也是可以上网的。这说明网线另一端的 ISP 并没有拿 MAC 来做限制。

那会不会是通过 MAC 来识别了设备类型呢?MAC 地址的前 24 bit 叫做 OUI,是由 IEEE 分配给不同组织的唯一标志符。我们可以在这个网站查询一个 MAC 地址属于哪一个制造商。

https://www.macvendorlookup.com

会不会是 ISP 检测到制造商是 TP-Link 这种路由器公司,就阻止访问,反而如果是终端设备,就允许访问呢?

听起来不太现实,因为有很多组织即生产路由设备又生产终端设备,无法精准识别。但还是验证一下吧,于是小李把自己的电脑 MAC 地址手动设置为 TP-Link OUI 的 MAC 地址,发现依然可以正常上网的。

这下小李实在没有思路了。他打电话给二舅,二舅是在一个互联网公司上班的网络专家(就是你!)。二舅听了之后,总结当前有的信息如下(在沟通解决问题的时候,总结自己当前有的事实信息,是一个很好的习惯):

  • 网线插在电脑上可以上网;
  • 网线插在路由器上,设备连接路由器,无法通过路由器上网;
  • 路由器连接 ISP 的账户密码等配置都是正确的;

二舅说,你用网线直接插在电脑上,然后上网并「抓包」,把抓包文件发给我,我来分析一下。

于是小李在终端运行 tcpdump -i eth0 -w 0-only-computer-no-router.pcap 命令,之后把 0-only-computer-no-router.pcap 这个文件通过电子邮箱发了过来。

请根据用 Wireshark 分析此文件,解释本文中小李遇到的问题。

(欢迎在评论区留下你的答案和想法。在思考之前,可以先不要往下滑,继续往下滑动可能会看到其他人留下的答案。)

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?
  7. 网工闯了什么祸?
  8. 网络中的环路和防环技术
  9. 延迟增加了多少?
  10. TCP 延迟分析
  11. 重新认识 TCP 的握手和挥手
  12. 重新认识 TCP 的握手和挥手:答案和解析
  13. TCP 下载速度为什么这么慢?
  14. TCP 长肥管道性能分析
  15. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。