Linux内核bug引起Mesos、Kubernetes、Docker的TCP/IP数据包失效


【编者的话】最近发现Linux内核bug,会造成使用veth设备进行路由的容器(例如Docker on IPv6、Kubernetes、Google Container Engine和Mesos)不检查TCP校验码(checksum),这会造成应用在某些场合下,例如坏的网络设备,接收错误数据。这个bug可以在三年前任何一个测试过的内核版本中发现。补丁已经被整合进核心代码,正在回迁入3.14之前的多个发行版中(例如SuSE,Canonical)。如果在自己环境中使用容器,强烈建议打上此补丁,或者等发布后,部署已经打上补丁的核心版本。


注:Docker默认NAT网络并不受影响,而实际上,Google Container Engine也通过自己的虚拟网络防止硬件错误。
编者:Jake Bower指出这个bug跟一段时间前发现的另外一个bug很相似。有趣!

起因

​十一月的某个周末,一组Twitter负责服务的工程师收到值班通知,每个受影响的应用都报出“impossible”错误,看起来像奇怪的字符出现在字符串中,或者丢失了必须的字段。这些错误之间的联系并不很明确指向Twitter分布式架构。问题加剧表现为:任何分布式系统,数据,一旦出问题,将会引起更长的字段出问题(他们都存在缓存中,写入日志磁盘中,等等...)。经过一整天应用层的排错,团队可以将问题定位到某几个机柜内的设备。团队继续深入调查,发现在第一次影响开始前,扇入的TCP纠错码错误大幅度升高;这个调查结果将应用从问题中摘了出来,因为应用只可能引起网络拥塞而不会引起底层包问题。

编者:用“团队”这个词可能费解,是不是很多人来解决这个问题。公司内部很多工程师参与排错,很难列出每个人名字,但是主要贡献者包括:Brian Martin、David Robinson、Ken Kawamoto、Mahak Patidar、Manuel Cabalquinto、Sandy Strong、Zach Kiehl、Will Campbell、Ramin Khatibi、Yao Yue、Berk Demir、David Barr、Gopal Rajpurohit、Joseph Smith、Rohith Menon、Alex Lambert and Ian Downes、Cong Wang。

一旦机柜被移走,应用失效问题就解决了。当然,很多因素可以造成网络层失效,例如各种奇怪的硬件故障,线缆问题,电源问题,等....;TCP/IP纠错码就是为保护这些错误而设计的,而且实际上,从这些设备的统计证据表明错误都可以检测到---那么为什么这些应用也开始失效呢?

隔离特定交换机后,尝试减少这些错误(大部分复杂的工作是由SRE Brain Martin完成的)。通过发送大量数据到这些机柜可以很容易复现失效数据被接收。在某些交换机,大约~10%的包失效。然而,失效总是由于核心的TCP纠错码造成的(通过netstat -a返回的TcpInCsumError参数报告),而并不发送给应用。(在Linux中,IPv4 UDP包可以通过设置隐含参数SO_NO_CHECK,以禁止纠错码方式发送;用这种方式,我们可以发现数据失效)。

Evan Jones(@epcjones)有一个理论,说的是假如两个bit刚好各自翻转(例如0->1和1->0)失效数据有可能有有效的纠错码,对于16位序字节,TCP纠错码会刚好抵消各自的错误(TCP纠错码是逐位求和)。当失效数据一直在消息体固定的位置(对32位求模),事实是附着码(0->1)消除了这种可能性。因为纠错码在存储之前就无效了,一个也纠错码bit翻转外加一个数据bit翻转,也会抵消各自的错误。然而,我们发现出问题的bit并不在TCP纠错码内,因此这种解释不可能发生。

很快,团队意识到测试是在正常linux系统上进行的,许多Twitter服务是运行在Mesos上,使用Linux容器隔离不同应用。特别的,Twitter的配置创建了veth(virtual ethernet)设备,然后将应用的包转发到设备中。可以很确定,当把测试应用跑在Mesos容器内后,立即发现不管TCP纠错码是否有效(通过TcpInCsumErrors增多来确认),TCP链接都会有很多失效数据。有人建议激活veth以太设备上的“checksum offloading” 配置,通过这种方法解决了问题,失效数据被正确的丢弃了。

到这儿,有了一个临时解决办法,Twitter Mesos团队很快就将解决办法作为fix推给了Mesos项目组,将新配置部署到所有Twiter的生产容器中。

排错

当Evan和我讨论这个bug时,我们决定由于TCP/IP是在OS层出现问题,不可能是Mesos不正确配置造成的,一定应该是核心内部网络栈未被发现bug的问题。

为了继续查找bug,我们设计了最简单的测试流程:
  1. 单客户端打开一个socket,每秒发送一个简单且长的报文​
  2. 单服务端(使用处于侦听模式的nc)在socket端侦听,打印输出收到的消息。
  3. 网络工具,tc,可以用于发送前,任意修改包内容。
  4. 一旦客户端和服务端对接上,用网络工具失效所有发出包,发送10秒钟。


可以在一个台式机上运行客户端,服务器在另外一个台式机上。通过以太交换机连接两台设备,如果不用容器运行,结果和我们预想一致,并没有失效数据被接收到,也就是10秒内没有失效包被接收到。客户单停止修改包后,所有10条报文会立刻发出;这确认Linux端TCP栈,如果没有容器,工作是正常的,失效包会被丢弃并重新发送直到被正确接收为止。
video1.gif

The way it should work: corrupt data are not delivered; TCP retransmits data

Linux 和容器

现在让我们快速回顾一下Linux网络栈如何在容器环境下工作会很有帮助。容器技术使得用户空间(user-space)应用可以在机器内共存,因此带来了虚拟环境下的很多益处(减少或者消除应用之间的干扰,允许多应用运行在不同环境,或者不同库)而不需要虚拟化环境下的消耗。理想地,任何资源之间竞争都应该被隔离,场景包括磁盘请求队列,缓存和网络。

Linux下,veth设备用于隔离同一设备中运行的容器。Linux网络栈很复杂,但是一个veth设备本质上应该是用户角度看来的一个标准以太设备。

为了构建一个拥有虚拟以太设备的容器,必须(1)创建一个虚机,(2)创建一个veth,(3)将veth绑定与容器端,(4)给veth指定一个IP地址,(5)设置路由,用于Linux流量控制,这样包就可以扇入扇出容器了。

为什么是虚拟造成了问题

我们重建了如上测试场景,除了服务端运行于容器中。然后,当开始运行时,我们发现了很多不同:失效数据并未被丢弃,而是被转递给应用!通过一个简单测试(两个台式机,和非常简单的程序)就重现了错误。
video2.gif

失效数据被转递给应用,参见左侧窗口。

我们可以在云平台重建此测试环境。k8s的默认配置触发了此问题(也就是说,跟Google Container Engine中使用的一样),Docker的默认配置(NAT)是安全的,但是Docker的IPv6配置不是。​

修复问题

重新检查Linux核心网络代码,很明显bug是在veth核心模块中。在核心中,从硬件设备中接收的包有ip_summed字段,如果硬件检测纠错码,就会被设置为CHECKSUM_UNNECESSARY,如果包失效或者不能验证,者会被设置为CHECKSUM_NONE。

veth.c中的代码用CHECKSUM_UNNECESSARY代替了CHECKSUM_NONE,这造成了应该由软件验证或者拒绝的纠错码被默认忽略了。移除此代码后,包从一个栈转发到另外一个(如预期,tcpdump在两端都显示无效纠错码),然后被正确传递(或者丢弃)给应用层。我们不想测试每个不同的网络配置,但是可以尝试不少通用项,例如桥接容器,在容器和主机之间使用NAT,从硬件设备到容器见路由。我们在Twitter生产系统中部署了这些配置(通过在每个veth设备上禁止RX checksum offloading)。

还不确定为什么代码会这样设计,但是我们相信这是优化设计的一个尝试。很多时候,veth设备用于连接统一物理机器中的不同容器。

逻辑上,包在同一物理主机不同容器间传递(或者在虚机之间)不需要计算或者验证纠错码:唯一可能失效的是主机的RAM,因为包并未经过线缆传递。不幸的是,这项优化并不像预想的那样工作:本地产生的包,ip_summed字段会被预设为CHECKSUM_PARTIAL,而不是CHECKSUM_NONE。

这段代码可以回溯到驱动程序第一次提交(commit e314dbdc1c0dc6a548ecf [NET]: Virtual ethernet device driver)。 Commit 0b7967503dc97864f283a net/veth: Fix packet checksumming (in December 2010)修复了本地产生,然后发往硬件设备的包,默认不改变CHECKSUM_PARTIAL的问题。然而,此问题仍然对进入硬件设备的包生效。

核心修复补丁如下:
diff — git a/drivers/net/veth.c b/drivers/net/veth.c
index 0ef4a5a..ba21d07 100644
— — a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -117,12 +117,6 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
kfree_skb(skb);
goto drop;
}
- /* don’t change ip_summed == CHECKSUM_PARTIAL, as that
- * will cause bad checksum on forwarded packets
- */
- if (skb->ip_summed == CHECKSUM_NONE &&
- rcv->features & NETIF_F_RXCSUM)
- skb->ip_summed = CHECKSUM_UNNECESSARY;

if (likely(dev_forward_skb(rcv, skb) == NET_RX_SUCCESS)) {
struct pcpu_vstats *stats = this_cpu_ptr(dev->vstats);

结论

我对Linux netdev和核心维护团队的工作很钦佩;代码确认非常迅速,不到几个星期补丁就被整合,不到一个月就被回溯到以前的(3.14+)稳定发行版本中(Canonical,SuSE)。在容器化环境占优势的今天,很惊讶这样的bug居然存在了很久而没被发现。很难想象因为这个bug引发多少应用崩溃和不可知的行为!如果确信由此引发问题请及时跟我联系。

原文链接:Linux kernel bug delivers corrupt TCP/IP data to Mesos, Kubernetes, Docker containers(翻译:杨峰)

0 个评论

要回复文章请先登录注册