工程的博客

TCP堆栈冒险:发现TCP SACKs漏洞修复中的性能退化

分享这篇文章

上个月,我们宣布由于Linux补丁,Databricksbob体育客户端下载平台正在经历网络性能的衰退TCP sack漏洞.在Amazon Web Services (AWS)平台上运行Databricks Runtime (DBR)时,只有不到0.2%的情况观察到这种回归。bob体育客户端下载在这篇文章中,我们将深入分析TCP堆栈是降级的根源。我们将讨论所看到的症状,介绍如何调试TCP连接,并解释Linux源代码中的根本原因。

在我们开始之前,先简短地说明一下,Canonical正在开发一个Ubuntu 16.04映像来解决这些性能下降的问题。我们计划在图像可用并通过回归测试后更新Databricks平台。bob体育客户端下载

失败的基准

我们第一次收到警报时,我们的一个基准变得慢了6倍。回归出现在升级Amazon Machine Image (AMI)之后,我们使用它来合并Ubuntu针对TCP SACKs漏洞的修复程序。

性能测试os S3 Write,包括TCP SACKs性能回归。
Databricks平台的ambob体育客户端下载i-2.102映像(红色)包括TCP SACKs性能回归。平均运行时间为16分钟,比原来低6倍。上图显示了超过20次运行的累计时间。

失败的基准测试将一个大的DataFrame通过Databricks文件系统(DBFS)使用Apache Spark的保存函数。

火花.range (10*亿).write.format(“铺”).mode(“覆盖”).save(“dbfs:/ tmp /测试”)

Apache Spark阶段执行保存操作有极端的任务倾斜。一项掉队的任务花了大约15分钟,而其他任务在1秒内完成。

详细分析由TCP SACKs漏洞修复引起的网络性能退化。
Spark UI允许我们检测到运行保存操作的Apache Spark阶段有极端的任务倾斜。

当回滚到不安全的AMI而没有TCP SACKs漏洞修复时,问题消失了。

调试TCP连接

为了弄清楚为什么离散者任务需要15分钟,我们需要当场捕获它。我们在监视Spark UI的同时重新运行基准测试,因为我们知道除了一个任务之外,所有的任务都是保存操作将在几分钟内完成。对该阶段中的任务进行排序状态列中,没有花很长时间,只有一个任务处于RUNNING状态。我们已经找到了倾斜的任务和IP地址宿主列将我们指向正在经历回归的执行程序。

调试由TCP SACKs漏洞修复引起的网络性能问题。
Spark UI将我们引导到运行离散者任务的执行程序的IP地址。

有了IP地址,我们通过ssh进入执行程序,看看发生了什么。怀疑网络问题,由于AmazonS3Exception的错误集群日志,我们跑netstat查看活动的网络连接。

# netstat活跃的互联网连接(w/o服务器)原型Recv-问发送-当地的地址外国地址状态tcp681699571知识产权-100-144年-224年。你:42464s3-我们-西-2-r-w: https CLOSE_WAIT

一个到S3服务器的TCP连接卡住了CLOSE_WAIT状态。此状态表示服务器已完成数据传输并已关闭连接,但客户端仍有更多数据要发送。这个连接的套接字有97KB的数据等待发送。

同时监视该套接字的状态,直到保存操作在15分钟后完成,我们注意到发送队列的大小从未改变。它保持在97KB。S3服务器没有确认数据,或者执行节点没有发送数据。我们使用党卫军获取有关套接字的更多详细信息。

# ss -a -t -m -o——infoCLOSE-WAIT81699571::飞行符:10.0144.22442464::飞行符:52.218234.25https计时器:(,1min37sec,13)Skmem:(r9792,rb236184,t0,tb46080,f732,w103971,o0,bl0)袋立方wscale:87rto:120000补偿:13rtt:0.379/0.055ato:40海量存储系统(mss)中:1412cwnd:1ssthresh:23bytes_acked:1398838bytes_received:14186segs_out:1085segs_in:817发送29.8Mbps lastsnd:592672lastrcv:552644lastack:550640pacing_rate4759.3Mbps unacked:80损失:24开除:56rcv_space:26883

使用党卫军允许我们查看套接字的内存使用量(- m), TCP特定信息(——信息)和计时器信息(- o).让我们来看看这次调查的重要数字:

  • 计时器:(1)min37sec, 13)-距离下一次重传的时间(1分37秒)和完成的重传尝试(13)。
  • tb46080 -套接字的当前发送缓冲区大小,单位为字节(45KB)。这个映射到sk_sndbuf在Linux套接字结构中
  • w103971—套接字写入缓冲区占用的内存,单位为字节(101KB)。这个映射到sk_wmem_queued在Linux套接字结构中。
  • 海量存储系统(mss)中:1412—连接最大段大小(以字节为单位)。
  • lastsnd: 591672—距离上一次发送数据包的时间,单位为毫秒(9分51秒)。

套接字状态的快照表明它已经进行了13次不成功的重传输尝试,下一次尝试将在1分37秒后进行。然而,考虑到Linux中TCP重传的最大时间间隔是2分钟,而套接字在过去的9分51秒内没有发送过一个数据包,这就有点奇怪了。服务器并不是没有确认重传;客户的重传甚至都没有通过电线发送出去!

当套接字被卡在这种状态时,我们看到重传计数器上升到15(默认设置net.ipv4.tcp_retries2)直到插座合上。发送队列和写入队列的大小从未改变,并且距离发送最后一个数据包的时间从未减少,这表明套接字继续发送数据失败。一旦套接字被关闭,S3客户端的重试机制就会启动,数据传输成功。

这种行为很好地匹配了我们的基准测试的离散任务需要15分钟才能完成。与tcp_retries2设置为15,TCP_RTO_MIN设置为200毫秒,TCP_RTO_MAX设置为120秒,并在重试之间进行指数回退,连接超时需要924.6秒,也就是15分钟多一点。

调试有故障的网络套接字时记录的传输尝试。
缺省的TCP重传计划。重传间隔从200ms开始,呈指数增长到最大120s,直到重传失败15分钟后套接字关闭。

TCP跟踪中的sack

由于重传超时解释了15分钟的任务倾斜,我们需要理解为什么客户端无法将数据包重传到S3服务器。在调试基准测试时,我们一直在收集所有执行程序上的TCP跟踪,并在那里寻找答案。

sudo tcpdump -nnS net 52.218.0.0/16 -w awstp .pcap

tcpdump上面的命令捕获所有进出S3子网(52.218.0.0/16)的流量。我们下载了.pcap文件的执行程序与倾斜的任务,并分析它使用Wireshark

对于使用本地端口42464的TCP连接-该端口一直卡在CLOSE_WAITS3客户端试图发送1.4MB的数据。第一个1.3MB在600毫秒内成功传输,但是S3服务器错过了一些段并发送了一个SACK。SACK指示丢失了22138个字节(ACK基准和SACK左边缘之间的差值)。

网络传输故障导致字节丢失。
TCP堆栈跟踪显示S3服务器在丢失21KB数据后发送了一个SACK。

在客户端为所有1.4MB的传出TCP段发送第一次传输后,连接沉默了40秒。丢失的21KB的前7次重传尝试应该在该时间内触发,这进一步证明客户端未能通过线路发送数据包。服务器最终发送了一个报文中含有相同的SACK信息,表明它没有收到丢失的数据。这导致客户端进入CLOSE_WAIT状态,即使允许客户端在此状态下传输数据,但它没有这样做。TCP跟踪显示此连接没有更多数据包。未确认的22138字节从未重传。

FIN报文,其中包含SACK信息,表示没有收到丢失的数据。
S3服务器发送了一个FIN包,其中包含SACK信息,表明它缺少21KB的数据。TCP跟踪显示客户端从未重传丢失的数据。

将这些22,138字节与77,433个sack字节相加(从左边减去右边),我们得到99,571个字节。这个数字看起来很熟悉。属性中列出的套接字发送队列的大小netstat输出。这就引出了第二个问题。为什么75KB的sack数据从未从套接字的发送队列中删除?服务器已经确认接收;客户不需要保留它。

不幸的是,TCP跟踪没有告诉我们重传失败的原因,只是增加了一个需要回答的额外问题。我们打开Ubuntu 16.04源代码了解更多。BOB低频彩

分析TCP协议栈

一个Linux补丁为了修复TCP SACKs漏洞,添加了if语句到tcp_fragment ()函数。它的目的是限制TCP堆栈对套接字的重传队列进行分段的条件,以防止恶意制作的SACKs数据包。这是我们找的第一个地方,因为它已经去过了固定一次。

Canonical的后端端口更改允许对包含未发送数据包的套接字缓冲区进行分段。
Canonical的后端端口更改允许对包含未发送数据包的套接字缓冲区进行分段。

即使有了这个修复,我们仍然看到了问题。七月底,还有另一个解决方案是为更新版本的Linux发布的。由于我们的ami使用Ubuntu 16.04和Canonical的4.4.0 Linux内核,我们向后移植了该补丁并构建了一个自定义Ubuntu内核.我们的性能问题消失了。

第二个补丁的Backpackport修复了由初始TCP SACKs漏洞修复引起的性能下降。
我们的第二个补丁的后端口修复了由初始TCP SACKs漏洞修复引起的性能下降。

仍然看不到根本原因,因为我们不知道if语句中的四个条件中的哪一个解决了我们的问题,我们构建了多个自定义映像,隔离每个条件。只有第一个条件的图像-修改sk_wmem_queued检查证明足以解决我们的问题。

tcp_fragment()中的if语句和有限的更改发送解决了我们的性能问题。
tcp_fragment()中的if语句和有限的更改发送解决了我们的性能问题。

该更改的提交消息表示增加了128KB的开销sk_sndbuf形成极限,因为"tcp_sendmsg ()而且tcp_sendpage ()可能会过度sk_wmem_queued大约一个完整的TSO skb (64KB大小)。”这意味着sk_wmem_queued可以合法地超过sk_sndbuf最多64KB。这是我们在眼窝里看到的。如果你还记得党卫军在输出上面,发送缓冲区是46,080字节,写队列大小是103,971字节(相差56KB)。由于写队列的一半(注意条件中的右移)大于发送缓冲区,我们满足了原始修复的if-条件。nstat证实了这一点,正如我们看到的TCPWQUEUETOOBIG计数记录为179。

# nstat | grep " TcpExtTCPWqueueTooBig "TcpExtTCPWqueueTooBig1790.0

证明我们确实失败了tcp_fragment ()我们想弄清楚为什么这会导致重传失败。在堆栈上看重传代码路径,我们看到如果套接字缓冲区的长度大于当前的MSS(在我们的例子中是1412字节),tcp_fragment ()在重传之前调用。如果失败,重新传输增量TCPRETRANSFAILCounter(我们的价格是143)和中止对整个套接字缓冲区队列的重传尝试。这就是为什么我们没有观察到21KB未确认数据的任何重传。

这就留下了75KB的已打包数据。为什么这些包的内存没有被回收?这将减少写队列的大小,并允许重传期间的碎片尝试成功。唉,SACK处理也调用tcp_fragment ()当套接字缓冲区不完全在SACK边界内时,它可以释放确认的部分。当一个碎片失败时,处理程序实际上爆发不检查其余的套接字缓冲区。这可能就是为什么75KB的已确认数据从未发布的原因。如果未被确认的21KB与已打包数据的第一部分共享一个套接字缓冲区,那么它将需要碎片来回收。在相同的if条件下失败tcp_fragment (), SACK处理程序就会中止。

经验教训

回顾我们的发现、调查和根本原因分析之旅,有几点值得注意:

  • 考虑整个堆栈。我们理所当然地认为Ubuntu为我们的集群提供了一个安全稳定的平台。bob体育客户端下载当我们的基准测试最初失败时,我们自然开始在我们自己最近的更改中寻找线索。只有在发现后续的Linux TCP补丁之后,我们才开始质疑堆栈底层的比特。
  • 基准只是数字;日志记录至关重要。我们已经知道了这一点,但有必要重申一下。我们的基准测试只是告诉我们平台慢了6倍。bob体育客户端下载它没有告诉我们去哪里找。Spark日志报告S3套接字超时异常给了我们第一个真正的提示。如果基准测试能够对这种测井异常发出警报,将大大缩短最初的调查时间。
  • 使用正确的工具。15分钟的套接字超时把我们难住了一会儿。我们知道netstat,但后来才知道党卫军.后者提供了关于套接字状态的更多细节,并使重传失败变得明显。它还帮助我们更好地理解TCP跟踪,因为它也丢失了预期的重传。

在本文的深入研究中,我们还了解了很多关于Linux TCP堆栈的知识。虽然Databricks不打算很快开始提交Linux补丁,但我们欠我们的客户和Linux社区支持我们之前的声明,即需要一个操作系统级别的补丁来修复我们的性能下降。好消息是最近4.4.0补丁解决了这个问题,官方的Ubuntu 16.04补丁就在眼前。

免费试用Databricks
看到所有工程的博客的帖子