Hi,欢迎来到 卡瓦邦噶!我是 laixintao,现在生活在新加坡。我的工作是 SRE,喜欢在终端完成大部分工作,对各种技术都感兴趣。我从 2013 年开始写这个博客,写的内容很广泛,运维的方法论,编程的思考,工作的感悟,除了技术内容之外,还会分享一些读书感想,旅行游记,电影和音乐等。欢迎留下你的评论。

声明:本博客内容仅代表本人观点,和我的雇主无关。本博客承诺不含有 AI 生成的内容,所有内容未加说明均为博主原创,一经发布自动进入公有领域,本人放弃所有权利。转载无需本人同意。但是依然建议在转载的时候留下本博客的链接,因为这里的很多内容在发布之后会还会不断地继续更新和追加内容。 Not By AI

网络中的环路和防环技术

不可以用路由器?一文的答案

这个题目很有趣,即使没有什么思路,当你打开 Wireshark 的时候,估计也能发现答案了。

电脑插上网线的时候,是可以正常上网的。但是即使能正常上网的时候,我们抓到的包也都是红色的。这些红色是有 Expert Info 标注的包。(可以通过 Wireshark Profile 配置颜色)

Expert Info 是 Wireshark 帮助我们标记出来的可疑信息。

Wireshark 打开会发现明显的 Expert Info 提示

点开下面 IP 层的包,会发现 TTL 有明显的颜色标注:Time To Live only 1. 于是就真相大白了,ISP 在把 IP 包转发给用户这边的时候,无论 TTL 还剩下多少,都改成 1 才转发给用户。这样,用户的设备可以收到包却不能转发。因为设备(比如路由器)在转发包的时候会把 TTL 减 1,如果 TTL 还是大于 0,才会转发出去;如果等于 0,就会直接丢弃。即,网络上是不存在 TTL=0 的包的,这是协议规定的。

TTL 变化的链路

TTL 的设计原本是为了防止网络出现环路,限制一个包能被转发的最大次数。每次转发都会 -1,最后到 0 的时候,如果包还没有到达目的地,设备就会丢弃这个包(然后可能发一个 ICMP 告诉 Src IP 这个包因为 TTL 减到 0 而寿终正寝了)。

要解决这个问题的话,我们就要去修改「路由器」的行为,让路由器收到 TTL=1 的包不要直接丢弃,而是修改 TTL=5(或者其他数字),然后继续转发给其他的设备。比如,可以用 iptables 来修改 TTL 的值 iptables -t mangle -A POSTROUTING -j TTL --ttl-set 5

为什么三层 IP 协议会有一个 TTL 字段,但是二层没有呢?我们先来看二层是否会出现三层可能出现的环路问题

网工闯了什么祸? 一文的答案

答案是交换机出现了环路。

交换机的工作方式是:

  1. 收到一个 Ethernet 帧,查找 MAC 地址表,找到这个 Ethernet 帧目标 MAC 对应的端口,然后从这个端口转发出去。这个 MAC 地址表怎么来的呢?见 2;
  2. 对于收到的这个帧,把来源 MAC 和收到这个帧的端口写入 MAC 地址表。因为从这个端口收到了帧,说明此 来源MAC 接在这个端口下,那么下次要往这个 MAC 发送的时候,直接走此端口即可;
  3. 假设来了一个 MAC,交换机的 MAC 地址表找不到这个 MAC 地址呢?比如交换机刚启动,MAC 地址表是空的。这时候交换机就把帧发给所有的端口 (flooding),即所有主机都能收到这个帧,只不过大部分主机检查一下目标 MAC 不是自己,就直接丢弃此帧。而之后一个主机发现是给自己的,就欣然接受,交给网络栈处理。

在我们的原题场景描述中,是扩容一个机架,一般在机架顶上加一个交换机 (Top-of-Rack Switching)。两个机架通都是在测试环境,需要连通,那么这个简单的网络拓扑现在就变成了这样子:

交换机出现环路连接

假设设备通电启动,服务器发送任何数据给交换机2(在题目中是 ARP 包),交换机2不知道目标 MAC 对应的端口,于是转发到它的所有端口,交换机1和3同时收到这个包,也不知道应该发给哪里,于是也转发到自己的所有端口,此时,交换机2又会收到1和3发过来的包,如上,这个包每次转发都会被放大2倍,直到最后,3个交换机都会在此网络中满载去转发这些流量,网络被堵满了,彻底瘫痪。

回到题目本身,这道题目是有一些困难的。因为我提供的 tcpdump 抓包文件,区分不出来网卡收到的包和发送出去的包。其实 tcpdump-Q in 选项可以只抓进来的包(另外的参数是 -Q out 和默认的 -Q inout。这样就很明显了:

可以看到 .1 这台机器去 ping .4 ,它才是应该发送 ARP 询问的人,它 ingress 怎么会收到自己发出去的请求呢?那必然是环路了,自己发出去的包经过转发,又回到了自己这里。而且还被放大了很多倍,发一个请求,远远不不断收到自己发送的请求。

那回到问题中提供的抓包文件,由于看不到进出方向这个信息,所以我们只能得到这个信息:发出去的 ARP 没有响应。原因可能就有很多了:网线没插好,交换机坏了,对面的机器故障导致收到 ARP 没有回应,有很多种可能。网络分析的时候,从哪里抓到的包这个信息非常重要。我们是从 .1 机器上抓包的,所以只能确认 ARP 包从机器上离开了,但是没获得响应。

所以能得到这个信息的读者很厉害了。

如何从给出的抓包文件得到「环路」这个信息呢?

我的 Wireshark Profile 会打开 Delta Time 这个列。方法如下。先在列头右键,选择 Column Preferences…

Wireshark 设置显示的列信息

然后添加一列,选择 Delta time displayed. 我喜欢加在 Time 的后面。

Wireshark 可以控制显示数据包的列信息

设置完成之后,列内容就看起来如下。

分析 Wireshark 包的时间信息

Delta time 的含义是:这个包抓到的时间距离上一个包过去了多久?比如第三行,表示第二个包抓到之后的 5us 抓到的。

这就很蹊跷了:主机发出 ARP 询问,为什么 5us 之后就接着又发出了另一个询问呢?难道等 5us 就开始重试了吗?(Linux ARP 重试时间是 1s,读者可以尝试一下去 ping 一个本地网络不存在的 IP 并抓包来实验一下。)虽然不同系统可能有不同的重试时间,但是 5us 就重试显然是不合理的,很多网卡的延迟都做不到 5us。那么真相只有一个——这个不是发出去的包而是收到的包,网络出现环路,导致自己发出去的包被自己收到了。进一步推断出,现在这个网络的交换机都已经被环路搞崩溃了,导致网络大塞车,所有的包都无法正常转发。

总结一下,推断出这个信息,有几个难点:

  • 要知道去分析包的时间序列;
  • 知道 tcpdump 抓到的包是双向的。这个大家应该都会知道,只不过平时下意识通过 IP 和 MAC 的来源、目标来区分方向。这个场景比较刁钻,即使来源 MAC 是自己,却是 ingress 方向;
  • 要知道二层也是可能存在环路的;

一楼 pandada8 在文章发布几小时内就贴了正确答案,恭喜!(可惜没奖品。)分析出第一段的读者也很棒,给你们发二等奖!

二层为什么不设置 TTL 字段?

既然三层使用 TTL 放环,而且二层也可能出现环路,为什么二层不加一个类似的 TTL 字段呢?我觉得原因有以下几点。

交换机速度为什么快,是因为在做转发的时候,直接看一下目标 MAC 地址,查表之后,确认出口,检查 CRC,然后交换机内的 Fabric 网络「连接」起来进入端口和出端口,数据直接转出去了。这中间不对 Frame 作修改。假设我们有一个二层 TTL 字段,那么就要修改这个字段,并且重新计算 CRC。这个问题在早期互联网很难解决,交换机硬件性能低,做这种复杂任务需要 CPU 来做。早期路由器用的就是 CPU(但即使这样,很多路由器都是不计算也不校验三层的 checksum 的,就当这个字段不存在一样,所以 IPv6 直接取消了 checksum 功能,完全依赖二层的 CRC 以及让上层协议自己去做校验),所以路由器比交换机速度慢很多。但是现在在 ASIC 上做这个事情难度不高,倒是将这个技术推向市场,升级 ASIC 芯片和协议难度会很高。说起来,按照早期硬件来设计的 Ethernet,还有很多不合理的地方,如果我们有能力一夜普及一种全新的 Ethernet 技术的话,我觉得最先升级的是 Ethernet 1500bytes MTU 的限制

我觉得最关键的原因是,做这个事情意义不大。TTL 这个机制只能作为防环的一种最后手段,并没有解决问题。在三层上讲,假设一个包的 TTL 被从 64 减到 0 最后丢弃了,那么包已经经过了 64 次转发,这些工作量已经被浪费掉了。在二层上讲,环路一旦出现,损失就更大了,每次转发流量都会被放大好几倍,可以想象,即使有 TTL,出现环路的话一个包也会被转发成千上万次,也不会有什么改善。

所以最好的方案还是防止环路的出现

那为什么三层还要有 TTL?因为二层网络一般是一个组织控制的,一个公司有多个网络,自己内部想怎么设计都可以。跨组织之间一般是三层,通过路由协议(最流行的是 BGP)来交换路由信息。一个网络内,我们可以通过技术手段保证没有环路,并对此负责。但是三层方面,我们不能保证邻居的网络没问题,不能保证他们不会把错误的包发送回来导致环路,所以 TTL 是一个在全局层面上,作的最后一个防线。

虽然三层有 TTL 的存在,所有的三层路由协议设计上都有防环机制,比如 Split Horizon,简单来说就是从一个接口收到的路由信息,不会再从这个接口发送。当环路出现的时候,已经造成损失了,TTL 只是减少损失而已。

从上面的分析我们可以看出:二层环路比三层更加可怕,一是因为二层没有 TTL 机制做兜底,二是二层交换机的工作原理会有流量放大。所以二层的防环技术非常重要。

二层最基础的防环机制是 STP(Spanning Tree Protocol),简单说就是交换机在启动的时候会互相沟通,看网络是否存在环路,如果存在,就通过计算协商断开一条线,就当这条线不存在一样。逻辑上是一个无环网络。

比如上一代网络经典的核心-汇聚-接入网络,可以看到红框里面其实是一个二层环路。但是运行 STP 会断开虚线的部分。

图片来自 Cisco (框是俺加的)

STP 有一些问题:断开线路,这就是浪费了硬件资源,没有完全利用带宽,而且每次 STP 的计算需要时间,比较慢。所以现在的二层技术,都会在设计的时候考虑到防环机制,让机制在本身不会造成二层环路。但是 STP 不会关闭,会作为一个兜底机制存在。

现在几乎所有的交换机都有 STP 功能,所以几乎看不到环路的存在了,上文题目很少在现实中见到,有些刁钻,向读者道歉!

无处不在的「环路」

说完三层和二层,我们扩展继续聊聊其他的协议。

可以说:只要有转发,就可能存在环路。

HTTP 301 重定向环路

比如说我之前配置全站流量 HTTPS 重定向的时候,就出现过「环路」。即在 WordPress 上配置了 HTTP 都 301 重定向到 HTTPS,但是 Apache 反向代理收到用户的 HTTPS 请求之后,将 TLS 「卸载」了,转发到后段的是 HTTP,于是 WordPress 又发送了 301 重定向给浏览器。

301 重定向引起的环路

浏览器对此的防环机制是,如果重定向次数太多,就放弃并显示错误:ERR_TOO_MANY_REDIRECTS.

DNS CNAME 环路

假设我们设置 a.kawabangga.com 的记录为 CNAME b.kawabangga.com,然后设置 b.kawabangga.com 的记录为 CNAME a.kawabangga.com,会发生什么呢?

大多的的 DNS 客户端和 Recursive resolver 都限制了跟踪 CNAME 的次数,这样,即使发生循环,最后也会以失败停下来。不过最好的办法还是在配置的时候就发现出现了环路(配置系统进行检查?)

比如,使用 dig 来查询这种 DNS CNAME 的话,dig 会一起将两个 CNAME 结果返回。

如果不限制查询次数的话,就会出问题了。曾经就遇到过 DNS Recursive resolver 没有限制查询次数而引起的故障。

CDN 转发的环路

CDN 的一个主要功能就是把访客的请求转发给真实服务器。想象这样一个场景,假设真实服务器也是一个 CDN 呢?如果我在一家 CDN 的控制台,配置转发请求到 CDN 公司 B,然后去 CDN B 配置转发到 CDN A。那么只要发送一些请求,就可以把两家 CDN 公司的流量都占满了。

图来自 Cloudflare

解决方法就是识别出来这个请求我是否已经转发过了。RFC 7230 规定可以使用 Via 字段来标志。但是这个字段有一些历史问题,以及太大了,有性能问题,而且很多服务器无法处理。所以 Cloudflare, Fastly and Akamai 这些厂商又联合定义了一个新的类似方案

最后总结一下,这一篇的分析中,在网络分析方面,我们学会两招:

  1. 注意 Wireshark 给我们提供的专家信息。TCP 如果发生重传,乱序等,Wireshark 就会给我们标注出来(注意后面可能要考哦~)。这些在 tcpdump 命令行是没有的,tcpdump 是抓包展示,不会有包之间的关联分析信息。
  2. 注意分析时间序列。

在网络设计方面,我们讨论了环路,做相关设计的时候要时刻提防无限循环的破坏力。顺带八卦了网络的一点历史和硬件知识。

Until next time!

目录

这个系列正在连载中,没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?
  7. 网工闯了什么祸?
  8. 网络中的环路和防环技术
  9. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
如果本文对您有帮助,欢迎打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!
 

四层负载均衡分析:GitHub GLB

今天我们来赏析 GitHub 的四层 LB 设计。

GitHub 的 GLB 是开源产品(当然了!),从架构上看,和之前介绍的 Cloudflare Unimog 很像,因为 GLB 是 Unimog 实现的重要参考。后面还会介绍一个叫做 Beamer 的四层负载均衡,也是参考了 GLB。所以 GLB 的连接保持设计创新性很强。

GLB 技术总览

被借鉴最多的就是 GLB 的连接保持技术,所以我们直接从最精彩的开始讨论。

连接保持技术

Cloudflare Unimog 的连接保持方案和 GitHub GLB 几乎一致,所以我们在之前几乎都已经讨论过了。

简单总结一下:GLB 作为四层负载均衡,在 GLB 实例之间不需要同步任何信息。在转发的时候,每一个 GLB 根据 TCP 连接五元组 hash,独立作出决策,选中一个 RS 进行转发。

转发的过程是:

  1. GLB 收到一个包,根据包的五元组计算 hash(不管是不是 SYN,都一样对待);
  2. 根据 hash 查找转发表,找到对应的 2 个 RS,一个是主 RS 一个是备 RS,然后转发到主 RS;
  3. 主 RS 收到包之后,检查这个包是不是属于自己机器上的连接,如果是,就交给协议栈处理,如果不是,就转发到备 RS(备 RS 的地址记录在 GLB 发过来的包中)。
添加 GLB 的同时添加机器也没有问题,可以「二次转发」,每一个包都有「第二次机会」(图来自 GitHub)

对比 Google Maglev 保持连接的方案有「两层」来保证同一个 flow 到同一个 RS 上:每一个 LB 实例都根据 SYN 包记录连接对应的 RS,即 connection table;然后使用一致性 hash 尽可能让相同的五元组选择相同的 RS。是属于「 connection table + hash查转发表」的方式。

而 GLB 的方案中不存在任何的状态保存,SYN 包和其他的包都可以使用一样的逻辑来转发,第一次转发不对就转发第二次,可以认为是「hash查转发表+hash查转发表」。Maglev 论文中提到了一些特殊情况,比如遇到 SYN DDoS 攻击的时候可能造成内存问题,在 GLB 这里就没有。

除了不用保存数据,这个转发方案和 Google Maglev 相比还有一个优点:Maglev 论文中提到,如果 Maglev 数量有变化,RS 数量也有变化,这样就会导致之前的 TCP 连接的包被发送到一个新的 Maglev 上,这个新的 Maglev connection table 中没有保存这个连接的状态,经过自己的 hash 计算选择 RS 会和之前的不一样(因为 RS 数量变化导致 hash 结果会有可能不一样),这时候连接就断了。GLB 就没有这个问题,GLB 实例可以和 RS 同时做变化。

转发表的生成

在这个方案中,转发表的生成是关键的一步。

按照转发表转发,图中 Proxy 其实是本文的 RS(图来自 GitHub

转发表要满足一下几个条件:

  • 在 RS (就是图中的 proxy)修改的时候,只有变化的 RS 在表中会修改,没有变化的 RS 在表中的位置不变。即不能对整个表完全重新 hash;
  • 表的生成不依赖外部的状态;
  • 每一行的两个 RS 不应该相同(不然的话就相当于没有备 RS 了);
  • 所有 RS 在表中出现的次数应该是大致相同的 (负载均衡);

实现方式是类似 Rendezvous hashing:对于每一行,将行号+ RS IP 进行 Hash 的到一个数字,作为「分数」,所有的 RS 在这一行按照分数排序,取前两名,作为主 RS 和 备 RS 放到表中。

然后按照以上的四个条件来分析:

  • 如果添加 RS,那么只有新 RS 排名第一的相关的行需要修改,其他的行不会改变;
  • 生成这个表只会依赖 RS 的 IP;
  • 每一行的两个 RS 不可能相同,因为取的前两名;
  • Hash 算法可以保证每一个 IP 当第一名的概率是几乎一样的;

不过要注意的是:在想要删除 RS 的时候,要交换主 RS 和 备 RS 的位置,这样,主 RS 换到备就不会有新连接了,等残留的连接都结束,就可以下线了;在添加 RS 的时候,每次只能添加1个,因为如果一次添加两个,那么这两个 RS 如果出现在同一行的第一名和第二名,之前的 RS 就会没来得及 drain 就没了,那么之前的 RS 的连接都会断掉。

转发架构和封装

GLB 也是使用的 DSR 转发架构,在这个系列之前的文章已经介绍过了,这里不重复了。

LB 到 RS 的转发, GLB 一开始使用的是用 GRE 封装然后放到 FOU 里面,现在直接换成了 GUE。上文提到的备 RS 的 IP 地址可以放到自定义的 GUE header 里面。

为什么不用 IPIP 来做封装呢?IPIP 是把一个 IP 包放到另一个 IP 里面做转发,看起来 header 更少。但是这样的话就没有地方放备 RS IP 了,唯一可行的地方是 underlay 的 IP 包的 option 里面。这会导致一个问题,就是路由器不认识这个 option,会涉及到需要 CPU 来处理,速度就更慢(叫做 Layer 2 slow path)。

为什么封装到 UDP 里面,而不是 IP 里面呢?如果是放到 UDP 包里面,那么对于负责转发的路由器来说,这个包就是一个普通的 UDP 包,可以按照四元组做 hash。如果是 IP 的话,对于路由器来说只能看到 IP 的数据,不会去解析内层的 overlay 的包内容,中间的路由器,以及 NIC,都会放到同一个 queue 中,如果一个 IP 对的流量太大的话,就会有性能瓶颈。

转发实现

GLB 是基于 DPDK 实现的。

因为设计上是无状态的,所以可以用 DPDK Packet Distributor 把工作散到任意数量的 CPU 上,并行执行,扩展性很强。

官方博客中提到支持 TCP over IPv4 or IPv6,也支持 ICMP,支持 PMTUD。没提到 UDP,应该是不支持 UDP?GitHub 的业务涉及 UDP 的应该不多。

使用 DPDK 就有一个问题:流量都被 GLB 接管了,那么那些非数据面的流量怎么办?比如 sshd 等程序,这些程序是用 Kernel socket API 编写的,不支持 DPDK 的接口。

一种方法是安排单独的网卡接口,专门用于这些应用。DPDK 的流量走单独的网卡,控制面走单独的网卡。

GLB 是用了 Flow Bifurcation,就是可以将一个物理网卡虚拟成多个虚拟网卡,Kernel 协议栈和 DPDK 流量分别走不同的虚拟网卡。硬件网卡可以将流量区分出来走哪一个虚拟网卡,这部分功能几乎是不占用 CPU 的,所以不会有额外的资源消耗,也能达到线速。

Flow Bifurcation 可以使用下面两种硬件功能来实现:

  • SR-IOV 是一个 PCI 标准,支持将一个物理卡虚拟出多个虚拟卡。云厂商虚拟机场景用的比较多。虚拟卡都有单独的 queue,MAC 地址和 IP 地址,物理卡可以根据 MAC 地址将流量分到不同的虚拟卡中;
  • 大部分的 NIC 都支持编程 Packet classification filtering,让硬件来将不同的流量分到不同的 queue;
图片来自 dpdk

其他部分

测试

使用 DPDK 的 Environment Abstraction Layer (EAL) ,可以让基于 libpcap 的 interface 像物理卡一样,不需要专用物理网卡就可以做端到端测试,配合 Linux 的 Virtual Device 功能和 Python 的 Scapy 编程库,在任意 Linux 系统上就可以跑测试,VM 都可以。

测试环境架构,图来源

健康检查

在 GLB 实例上运行健康检查程序,从实际的 tunnel 去检查后端的端口,如果认为不健康,就直接交换主 RS 和备 RS。这样新连接会去好的 RS,旧连接可以尝试不健康的 RS,最大努力保持连接。如果健康检查失败是 False Positive 也不要紧,只是影响包的转发路径而已。

RS 上的二次转发

基于 Netfilter 和 IPtables 实现:如果是 SYN 或者连接在本地存在,就接受,否则就转发到 备 RS。

参考资料:

四层负载均衡系列文章

 

网工闯了什么祸?

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

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

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

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

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

现在的情况是:

  • 小王发现请求发给另一个服务总是超时;
  • 小王去 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. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 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 牺牲了部分性能,但是技术的复杂程度,运维复杂度都降低了很多。

四层负载均衡系列文章