就是理解TCP。 最常见的TCP性能问题是,一个PHP服务通过Nginx封装后续tair,并允许其他APP应用程序通过http协议访问Nginx对tair进行获取和设置操作
上线后测试一切正常,每次操作数毫秒,但有时某个APP的value为300K,此时set一次需要300毫秒以上。 在没有并发压力的单线程上进行一次操作也需要这么长时间,这个延迟没有道理,是不能接受的。
问题的原因是,TCP协议为了进行一些带宽利用率、性能方面的优化,进行了一些特殊的处理。 例如延迟确认和Nagle算法。
究其原因,在让大家了解TCP的基本概念后,可以在实战中了解一些TCP其他方面的性能和影响。
什么是延迟确认? 在我前面的TCP介绍文章中,TCP是可靠的传输,可靠的核心是在收到数据包后回复ack告诉对方已经到达。
请看一个例子:
截图中的nignx(8085端口)收到http请求,立即向客户端返回了ack数据包,然后向客户端返回了http响应。 请注意,返回的ack包的长度为66,实际内容的长度为0。 ack信息位于TCP标头中。 也就是说,这里将66字节的空数据包发送给客户端,告诉客户端我收到了你的请求。
这里没有问题。 逻辑上是正确的。 符合TCP核心确实传输的意思。 但是,存在带宽效率不高的问题。 那个可以优化吗?
这里的优化是延迟访问。
所谓delay ack,不是收到包就马上ack,而是稍等(例如40毫秒),如果在这40毫秒内正好有一个包被发送到客户端,我这个ack包就会一起发送。 (顺风风车、http reponse包无需增加尺寸。 )这样可以节省资源,当然,如果在此时间过后仍未向客户端发送数据包(例如,nginx过程需要40毫秒或更长时间),则此ack也会发送到客户端。 (即使是空的,如果客户端认为丢包,也必须重新发送http请求。 )。
假设此时还在等待ack数据包延迟发送的同时,从客户端接收到了另一个数据包。 如果此时服务器收到两个ack数据包,则操作系统会立即将这两个ack数据包加在一起,并将一个ack数据包返回给客户端,并告知客户端前两个数据包已到达。
也就是说,当delay ack打开时。 ack不立即发送,而是等待40毫秒。 在等待的时候,ack包里有顺风风车的话就可以乘坐。 或者把两个ack包起来自己包车也马上发车。如果再等40毫秒以上还不顺利的话,自己开车也发车。
因为屏幕快照中的Nginx没有打开delay ack,所以红框ack可以完全与“绿框”(http response )一起发送到客户端,但没有。 红框ack马上坐出租车逃跑了
Nagle算法下的伪代码是Nagle算法的基本逻辑。 摘自维客:
ifthereisnewdatatosendifthewindowsizegt;=MSS and available数据is gt;=msssendcompletemsssegmentnowelseifthereisunconfirmeddatastillinthepipeenqueuedatainthebufferuntilanacknowledgeisreceivedels els els els endifendif此代码表示如果接收窗口大于MSS,发送的数据大于MSS,则立即发送。
总结Nagle算法的逻辑是,如果发送的数据包很小(小于MSS ),而且对方收到了发送的数据包还没有回复,我也不着急发送,等上一个数据包的回复收到后再发送。 这将优化带宽利用率。 几年前的带宽资源是宝贵的。 此外,Nagle算法还用于优化tcp传输效率。
如果客户端启用了Nagle并在服务器端启用了delay ack,会发生什么?
假如client要发送一个http请求给server,这个请求有1600个bytes,握手的MSS是1460,那么这1600个bytes就会分成2个TCP包,第一个包1460,剩下的140bytes放在第二个包。第一个包发出去后,server收到第一个包,因为delay ack所以没有回复ack,同时因为server没有收全这个HTTP请求,所以也没法回复HTTP response(server等一个完整的HTTP请求,或者40毫秒的delay时间)。client这边开启了Nagle算法(默认开启)第二个包比较小(140<MSS),第一个包的ack还没有回来,那么第二个包就不发了,等!互相等!一直到Delay Ack的Delay时间到了!
这就是悲剧的核心原因。
再来看一个经典例子和数据分析这个案例来自:http://www.stuartcheshire.org/papers/nagledelayedack/
案例核心奇怪的问题是,如果传输的数据是 99,900 bytes,速度5.2M/秒;
原因就是:
99,900 bytes = 68 full-sized 1448-byte packets, plus 1436 bytes extra100,000 bytes = 69 full-sized 1448-byte packets, plus 88 bytes extra99,900 bytes:
68个整包会立即发送,因为68是偶数,对方收到最后两个包后立即回复ack(delay ack凑够两个也立即ack),那么剩下的1436也很快发出去(根据nagle算法,没有没ack的包了,立即发)
100,000 bytes:
前面68个整包很快发出去也收到ack回复了,然后发了第69个整包,剩下88bytes根据nagle算法要等一等,server收到第69个ack后,因为delay ack不回复(手里只攒下一个没有回复的包),所以client、server两边等在等,一直等到server的delay ack超时了。
挺奇怪和挺有意思吧,作者还给出了传输数据的图表:
这是有问题的传输图,明显有个平台层,这个平台层就是两边在互相等,整个速度肯定就上不去。
如果传输的都是99,900,那么整个图形就很平整:
回到前面的问题服务写好后,开始测试都没有问题,rt很正常(一般测试的都是小对象),没有触发这个问题。后来碰到一个300K的rt就到几百毫秒了,就是因为这个原因。
另外有些http post会故意把包头和包内容分成两个包,再加一个Expect参数之类的,更容易触发这个问题。
这是修改后的C代码
struct curl_slist *list = NULL; //合并post包 list = curl_slist_append(list, "Expect:"); CURLcode code(CURLE_FAILED_INIT); if (CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_URL, oss.str().c_str())) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout)) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &write_callback)) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L)) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_POST, 1L)) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, pooh.sizeleft)) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback)) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_READDATA, &pooh)) && CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L)) && //1000 ms curl bug CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list)) ) { //这里如果是小包就不开delay ack,实际不科学 if (request.size() < 1024) { code = curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L); } else { code = curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 0L); } if(CURLE_OK == code) { code = curl_easy_perform(curl); }上面中文注释的部分是后来的改进,然后经过测试同一个300K的对象也能在几毫米以内完成get、set了。
尤其是在Post请求将HTTP Header和Body内容分成两个包后,容易出现这种延迟问题
总结
这个问题确实经典,非常隐晦一般不容易碰到,碰到一次决不放过她。文中所有client、server的概念都是相对的,client也有delay ack的问题。 Nagle算法一般默认开启的
此文来自阿里巴巴同事:蛰剑