上上周末拜读了曹伟-鸣嵩的头条文章《提高单机短连接QPS到20万》,结合自己的思考,写点小感悟。
作者介绍
首先说下曹伟-鸣嵩是谁:曹伟,阿里巴巴-阿里云资深技术专家(P9?),擅长C++、网络编程、中间件、底层协议等领域。
原文链接:http://weibo.com/ttarticle/p/show?id=2309404037884855362229#_rnd1478422720948
原文简介
文章不是很长,总结下来,就如下几句话:
1.tcp应该由客户端发起连接关闭。
2.默认情况下,客户端关闭TCP连接后本地的临时端口会长时间进入TIME_WAIT状态,这对QPS提升限制很大。
3.某些场景下,提高QPS就是提供更多的端口。方法就是:提供更多的端口和端口更快的复用回收。
4.通过调整参数等方法,可以将qps达到20w,让内核态网络协议负载饱和。
为什么要让客户端发起连接关闭?
这里我们首先看下tcp的四次分手。由于tcp设计上是非常严谨的,其协议可以做到不可靠网络下的可靠传输,在这里也就是说分手过程中,任何一个信号发送失败,都会有对应的重试容错机制,整个连接关闭过程都会可靠实现。具体的分手细节我这里就不再累叙,大家感兴趣可以私下细聊。
综上timewait在分手最后一个环节扮演重要角色,可靠地实现TCP全双工连接的终止。同时避免了,因为不可靠网络带来的重复连接,允许老的重复分节在网络中消逝。
但是,在内网这种可靠网络情况下,仍然采取默认设置或者说是标准协议的话,带来的开销是不容小视的。
为了实现“允许老的重复分节在网络中消逝”,timewait的时间就要是2个msl(Maximum Segment Life Time),在rfc 793中推荐的msl时长为2分钟,所以windows的默认timewait时间是4分钟,而基于BSD socket的linux,由于对协议的实现没有按照rfc规定自定为60s。无论是哪个系统,timewait的时间都是比较长的。
(注:linux的timewait时间可以看这里https://github.com/torvalds/linux/blob/master/include/net/tcp.h)
不难看出,主动发起连接关闭的一方是需要承担timewait时长的,而这个时间默认情况下是远远大于连接关闭的其他步骤耗时的。是不是可以这样理解:在一段严谨负责任的爱情(连接)中,主动发起分手的一方,永远是最受伤(耗资源)的那个。
简单聊下QPS的提升
每秒查询率QPS,即每秒的响应请求数,也即是最大吞吐能力。在很多场景下,提高qps都在于优化我们的逻辑,选用更快的db等应用场景下的提升。但是在某些场景下(可靠的内网、简单而快速的后端逻辑),我们的处理速度达到极致,网络也不会出现异常,qps提升的瓶颈仅仅在于没有那么多的端口供我们新的查询建立连接。这时候就需要我们提供更多的端口和端口更快的复用回收。
提供更多的端口
增加临时端口的数量,增加可被消耗的临时端口资源
sysctl -w "net.ipv4.ip_local_port_range=1024 65535”
由于tcp port 大小为 16 bit,极限就是如此。也许后面整个网络协议有本质变化,也许会有新的方法。
启用tw_reuse
sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_tw_reuse=1
在高并发情况下,连接创建时候会考虑复用相应的time_wait连接。而一般情况下,TIME_WAIT创建时间必须超过一秒,且连接的时间戳是递增的时候才会被复用。和标准相比,几秒的timewait已经远远小于linux原生的60s,可以大大提升我们的qps。
原文作者测试,其qps可以达到2w。
启用tw_recycle
sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_tw_recycle=1
由于time_wait存在的一个意义就是要等待数据包的重传,实现tcp连接的可靠关闭。所以这里我们的时长完全可以用rtt代替msl。
RTT (Round-Trip Time)数据包的往返时延,在内网情况下,几乎是ms级别,是远小于MSL时间。不难理解,开启tw_recycle后,其端口回收速度比起reuse和原生标准。原文作者测试,开启这个参数后,其qps可以达到惊人的6w。
当然这种方法肯定不是银弹,当多个客户端通过NAT方式联网并与服务端交互时,服务端看到的是同一个IP,也就是说对服务端而言这些客户端实际上等同于一个,可惜由于这些客户端的时间戳可能存在差异,于是乎从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。
给socket配置SO_LINGER
on设为1,linger设为0,这样关闭连接后TCP状态从ESTAB直接进入CLOSED,向服务器发rst包而不是fin包来关闭连接。这种方法几乎完全改变了tcp的分手协议,把繁琐厚重的过程一切到底。
不过这种方法却存在一个弊端:1.会直接丢弃掉buffer里未发送完的数据,造成数据丢失 2.不可靠网络下,会造成连接关闭的不可靠。 当然如果我们在可靠的内网和可靠的上层协议情况下,通过这种方法是可以恐怖地提升我们的qps,原文作者测试,其qps达到20w,基本达到极致。
此外,我觉得利用udp实现“连接”的方法,也应该是可以达到类似的效果,这就等着去做相应的开发了。
几点思考
1.为什么不直接调TCP_TIMEWAIT_LEN这个参数?
2.客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端。这种情况下该如何消除TIME_WAIT?
附录
相关命令:
查询TIME_WAIT的数量:
ss -ant | awk 'NR>1 {++s[$1]} END {for(k in s) print k,s[k]}'
cat /proc/net/sockstat
查询本机MSL:
sysctl net.ipv4.tcp_fin_timeout
cat /proc/sys/net/ipv4/tcp_fin_timeout
参考阅读:
《再叙TIME_WAIT》:火丁笔记 http://huoding.com/2013/12/31/316cat /proc/net/sockstat
https://en.wikipedia.org/wiki/HTTP_persistent_connection
https://en.wikipedia.org/wiki/Maximum_segment_lifetime
https://en.wikipedia.org/wiki/Round-trip_delay_time