云服务器价格_云数据库_云主机【优惠】最新活动-搜集站云资讯

京东云_cdn服务器价格_9元

小七 141 0

下载速度慢的奇怪案例

不久前,我们发现某些非常慢的下载被突然终止,并开始调查这是客户端(即web浏览器)还是服务器(即我们)的问题。一些用户无法下载几兆字节长的二进制文件。故事很简单,下载连接突然终止,即使文件正在下载过程中。经过一个简短的调查,我们确认了这个问题:在我们的堆栈中的某个地方有一个bug。描述这个问题很简单,只需一个curl命令就可以很容易地重现问题,但是修复它却花费了惊人的精力。jojo nicdao的CC BY 2.0图像在这篇文章中,我将描述我们看到的症状,我们如何复制它,以及我们如何修复它。希望通过分享我们的经验,我们可以把其他人从繁琐的调试中解救出来。下载失败bug报告中有两件事引起了我们的注意。首先,只有手机用户遇到了这个问题。其次,导致问题的资产——一个二进制文件相当大,大约30MB。在与tcpdump进行了富有成效的会谈之后,我们的一位工程师能够准备一个再现问题的测试用例。正如经常发生的那样,一旦你知道你在寻找什么,复制一个问题就很容易了。在这种情况下,在测试域上设置一个大文件并使用--limit rate选项进行curl就足够了:$卷曲-v极限速率10k>/dev/null*关闭连接#0curl:(56)接收失败:对等方重置连接用tcpdump进行探测显示,在建立连接60秒后,有一个RST数据包从我们的服务器发出:$tcpdump-ttttt ni eth0端口8000:00:00 IP 192.168.1.10.50112>1.2.3.4.80:标志[S],序列3193165162,win 43690,选项[mss 65495,sackOK,TS val 143660119 ecr 0,nop,wscale 7],长度0...00:01:00 IP 1.2.3.4.80>192.168.1.10.50112:标志[R.],序列号1579198,ack 88,win 342,选项[nop,nop,TS val 143675137 ecr 143675135],长度0很明显我们的服务器出了问题。来自CloudFlare服务器的第一个数据包不好。客户机的行为,礼貌地发送ACK包,以自己的速度消耗数据,然后我们就突然中断了对话。不是我们的问题我们是NGINX的大量用户。为了隔离这个问题,我们设置了一个基本的现成NGINX服务器。该问题很容易在当地重现:$curl——限制费率10k本地主机:8080/large.bin>/dev/空*关闭连接#0curl:(56)接收失败:对等方重置连接这证明了问题并不是针对我们的设置,而是更广泛的NGINX问题!又捅了几下,我们找到了两个罪犯。首先,我们使用重置连接设置。这会导致NGINX突然关闭连接。当NGINX想让连接超时时,它会设置sou\u在套接字上不超时,然后是close()。这将触发RST数据包,而不是通常正常的TCP终结。这是来自NGINX的strace日志:04:20:22 setsockopt(5,SOL峈峈峈峈峈峈峈峈峈峈峈峈04:20:22关闭(5)=0我们本可以禁用reset\u timedout_连接设置,但这并不能解决根本问题。为什么NGINX首先要关闭连接?在进一步调查之后,我们查看了send_timeout配置选项。默认值是60秒,正好是我们看到的超时。http协议{发送超时60s;...NGINX使用send_timeout选项来确保所有连接最终都会耗尽。它控制每个连接上连续的send/sendfile调用之间允许的时间间隔。一般来说,单个连接使用宝贵的服务器资源太长时间是不好的。如果下载时间太长或者完全被卡住了,HTTP服务器也可以不高兴。但还有更多。也不是NGINX的问题借助strace,我们调查了NGINX的实际行动:04:54:05接收4(4,…)=504:54:05发送文件(5,9,[0],51773484)=532575204:55:05关闭(5)=0在配置中,我们命令NGINX使用sendfile来传输数据。对sendfile的调用成功,并将5MB的数据推送到发送缓冲区。这个值很有趣,它是关于我们的默认写缓冲区设置中的空间量:$系统控制网络ipv4.tcp\wmem文件网络ipv4.tcp_wmem=4096 5242880 33554432在第一个长sendfile后一分钟,套接字关闭。让我们看看将send_timeout值增加到某个大值(如600秒)时会发生什么:08:21:37接受4(4,…)=508:21:37发送文件(5,9,[0],51773484)=602475408:24:21发送文件(5,9,[6024754],45748730)=176804108:27:09发送文件(5,9,[7792795],43980689)=176804108:30:07发送文件(5,9,[9560836],42212648)=1768041...在第一次大规模的数据推送之后,sendfile被多次调用。每次连续通话之间,它传输大约1.7MB。在这些系统调用之间,大约每180秒,套接字就不断地被缓慢的卷曲耗尽,那么NGINX为什么不不断地重新填充它呢?不对称Unix设计的座右铭是"一切都是文件"。我更愿意这样想:"在Unix中,当进行poll时,所有东西都可以读写"。但"可读性"到底是什么意思呢?让我们讨论一下Linux上网络套接字的行为。从套接字读取的语义很简单:调用read()将返回套接字上可用的数据,直到它为空。当套接字上有任何数据可用时,poll将其报告为可读。有人可能会认为这是对称的,类似的情况也适用于向套接字写入数据,如下所示:调用write()会将数据复制到写缓冲区,直到"send buffer"内存耗尽。轮询报告如果发送缓冲区中有可用空间,则套接字是可写的。令人惊讶的是,最后一点并不正确。不同的代码路径认识到在Linux内核中有两个独立的代码路径是非常重要的:一个用于发送数据,另一个用于检查套接字是否可写。要使send()成功,必须满足两个条件:发送缓冲区中必须有一些可用空间。已排队但未发送的数据量必须低于LOWAT设置。我们将忽略这篇博文中的LOWAT设置。另一方面,通过轮询将套接字报告为"可写"的条件稍窄:发送缓冲区中必须有一些可用空间。已排队但未发送的数据量必须低于LOWAT设置。发送缓冲区中的可用缓冲区空间量必须大于已用发送缓冲区空间的一半。最后一个条件很关键。这意味着,在将套接字发送缓冲区填充到100%之后,只有当其耗尽到发送缓冲区大小的66%以下时,套接字才会再次变为可写。回到NGINX跟踪,我们看到的第二个sendfile:08:24:21发送文件(5,9,[6024754],45748730)=1768041调用成功地发送了1.7 MiB的数据。这接近5 MiB的33%,这是我们默认的wmem发送缓冲区大小。我假设这个阈值是在Linux中实现的,目的是为了避免频繁地填充缓冲区。在客户机确认数据的每个字节之后唤醒发送程序是不可取的。解决方案在充分了解问题的情况下,我们可以果断地说:套接字发送缓冲区已填充到至少66%。客户下载速度慢,无法在60秒内将缓冲区消耗到66%以下。当这种情况发生时,发送缓冲区不会被及时重新填充,也不会被报告为可写的,并且连接会在超时时重置。有两种方法可以解决这个问题。一种方法是将发送超时时间增加到280秒。这将确保给定默认的发送缓冲区大小,速度超过50Kbps的用户将永远不会超时。另一个选择是减小tcp wmem发送缓冲区的大小。最后一种选择是修补NGINX,使其在超时时做出不同的反应。我们可以检查发送缓冲区中剩余的数据量,而不是关闭连接。我们可以用ioctl(TIOCOUTQ)来实现。有了这些信息,我们就能确切地知道连接被抽干的速度有多快。如果它高于某个可配置的阈值,我们可以决定给连接更多的时间。我的同事chrisbranch为NGINX准备了一个Linux专用的补丁。它实现了send_minimum_rate选项,用于指定允许的最小客户端吞吐量。摘要Linux网络堆栈非常复杂。虽然它通常效果很好,但有时会给我们一个惊喜。即使是经验丰富的程序员也不能完全理解所有的情况。在调试过程中,我们了解到在代码的"写入"路径中设置超时需要特别注意。你不能把"写"超时和"读"超时一样对待。令我吃惊的是,套接字的"可写"语义与"可读"状态并不对称。在过去,我们发现提高接收缓冲区会产生意想不到的后果。现在我们知道调整wmem值会影响完全不同的NGINX发送超时。调整一个CDN使其对所有用户都能很好地工作,这需要大量的工作。这篇文章是四位工程师辛勤工作的结果(特别感谢chrisbranch!)。如果这听起来很有趣,考虑申请!