一个bug引发的血案--Linux内核bug引起Mesos, Kubernetes, Docker containers的TCP/IP数据包失效。

jopen 9年前

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

【原文地址参见: https://tech.vijayp.ca/linux-k ... othmv

【注:Docker默认NAT网络并不受影响,而实际上,Google Container Engine也通过自己的虚拟网络防止硬件错误。】

【编者:Jake Bower指出这个bug跟一段时间前发现的另外一个bug很相似。有趣!】

起因

十一月的某个周末,一组推ter负责服务的工程师收到值班通知,每个受影响的应用都报出“impossible”错误,看起来像奇怪的字符出现在字符串中,或者丢失了必须的字段。这些错误之间的联系并不很明确指向推ter分布式架构。问题加剧表现为:任何分布式系统,数据,一旦出问题,将会引起更长的字段出问题(他们都存在缓存中,写入日志磁盘中,等等...)。经过一整天应用层的排错,团队可以将问题定位到某几个机柜内的设备。团队继续深入调查,发现在第一次影响开始前,扇入的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系统上进行的,许多推ter服务是运行在Mesos上,使用Linux容器隔离不同应用。特别的,推ter的配置创建了veth(virtual ethernet)设备,然后将应用的包转发到设备中。可以很确定,当把测试应用跑在Mesos容器内后,立即发现不管TCP纠错码是否有效(通过TcpInCsumErrors增多来确认),TCP链接都会有很多失效数据。有人建议激活 veth以太设备上的“checksum offloading” 配置,通过这种方法解决了问题,失效数据被正确的丢弃了。

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

排错

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

为了继续查找bug,我们设计了最简单的测试流程:

单客户端打开一个socket,每秒发送一个简单且长的报文 。

单服务端(使用处于侦听模式的nc)在socket端侦听,打印输出收到的消息。

网络工具,tc,可以用于发送前,任意修改包内容。

一旦客户端和服务端对接上,用网络工具失效所有发出包,发送10秒钟。

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

一个bug引发的血案--Linux内核bug引起Mesos, Kubernetes, Docker containers的TCP/IP数据包失效。

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流量控制,这样包就可以扇入扇出容器了。

为什么是虚拟造成了问题

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

一个bug引发的血案--Linux内核bug引起Mesos, Kubernetes, Docker containers的TCP/IP数据包失效。

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

我们可以在云平台重建此测试环境。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,从硬件设备到容器见路由。我们在推ter生产系统中部署了这些配置(通过在每个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的问题。然而,此问题仍然对进入硬件设备的包生效。

核心修复补丁如下:

核心修复补丁

一个bug引发的血案--Linux内核bug引起Mesos, Kubernetes, Docker containers的TCP/IP数据包失效。

结论

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

来自: http://dockone.io/article/1038