gitbook/系统性能调优必知必会/docs/237612.md
2022-09-03 22:05:03 +08:00

160 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 09 | 如何提升TCP三次握手的性能
你好,我是陶辉。
上一讲我们提到TCP在三次握手建立连接、四次握手关闭连接时是怎样产生事件的这两个过程中TCP连接经历了复杂的状态变化既容易导致编程出错也有很大的优化空间。这一讲我们看看在Linux操作系统下如何优化TCP的三次握手流程提升握手速度。
TCP是一个可以双向传输的全双工协议所以需要经过三次握手才能建立连接。三次握手在一个HTTP请求中的平均时间占比在10%以上在网络状况不佳、高并发或者遭遇SYN泛洪攻击等场景中如果不能正确地调整三次握手中的参数就会对性能有很大的影响。
TCP协议是由操作系统实现的调整TCP必须通过操作系统提供的接口和工具这就需要理解Linux是怎样把三次握手中的状态暴露给我们以及通过哪些工具可以找到优化依据并通过哪些接口修改参数。
因此这一讲我们将介绍TCP握手过程中各状态的意义并以状态变化作为主线看看如何调整Linux参数才能提升握手的性能。
## 客户端的优化
客户端和服务器都可以针对三次握手优化性能。相对而言,主动发起连接的客户端优化相对简单一些,而服务器需要在监听端口上被动等待连接,并保存许多握手的中间状态,优化方法更为复杂一些。我们首先来看如何优化客户端。
三次握手建立连接的首要目的是同步序列号。只有同步了序列号才有可靠的传输TCP协议的许多特性都是依赖序列号实现的比如流量控制、消息丢失后的重发等等这也是三次握手中的报文被称为SYN的原因因为SYN的全称就叫做Synchronize Sequence Numbers。
![](https://static001.geekbang.org/resource/image/c5/aa/c51d9f1604690ab1b69e7c4feb2f31aa.jpg)
三次握手虽然由操作系统实现但它通过连接状态把这一过程暴露给了我们我们来细看下过程中出现的3种状态的意义。客户端发送SYN开启了三次握手此时在客户端上用netstat命令后续查看连接状态都使用该命令可以看到**连接的状态是SYN\_SENT**顾名思义就是把刚SYN发送出去
```
tcp 0 1 172.16.20.227:39198 129.28.56.36:81 SYN_SENT
```
客户端在等待服务器回复的ACK报文。正常情况下服务器会在几毫秒内返回ACK但如果客户端迟迟没有收到ACK会怎么样呢客户端会重发SYN**重试的次数由tcp\_syn\_retries参数控制**默认是6次
```
net.ipv4.tcp_syn_retries = 6
```
第1次重试发生在1秒钟后接着会以翻倍的方式在第2、4、8、16、32秒共做6次重试最后一次重试会等待64秒如果仍然没有返回ACK才会终止三次握手。所以总耗时是1+2+4+8+16+32+64=127秒超过2分钟。
如果这是一台有明确任务的服务器,你可以根据网络的稳定性和目标服务器的繁忙程度修改重试次数,调整客户端的三次握手时间上限。比如内网中通讯时,就可以适当调低重试次数,尽快把错误暴露给应用程序。
![](https://static001.geekbang.org/resource/image/a3/8f/a3c5e77a228478da2a6e707054043c8f.png)
## 服务器端的优化
当服务器收到SYN报文后服务器会立刻回复SYN+ACK报文既确认了客户端的序列号也把自己的序列号发给了对方。此时服务器端出现了新连接状态是SYN\_RCVRCV是received的缩写。这个状态下服务器必须建立一个SYN半连接队列来维护未完成的握手信息当这个队列溢出后服务器将无法再建立新连接。
![](https://static001.geekbang.org/resource/image/c3/82/c361e672526ee5bb87d5f6b7ad169982.png)
新连接建立失败的原因有很多怎样获得由于队列已满而引发的失败次数呢netstat -s命令给出的统计结果中可以得到。
```
# netstat -s | grep "SYNs to LISTEN"
1192450 SYNs to LISTEN sockets dropped
```
这里给出的是队列溢出导致SYN被丢弃的个数。注意这是一个累计值如果数值在持续增加则应该调大SYN半连接队列。**修改队列大小的方法是设置Linux的tcp\_max\_syn\_backlog 参数:**
```
net.ipv4.tcp_max_syn_backlog = 1024
```
如果SYN半连接队列已满只能丢弃连接吗并不是这样**开启syncookies功能就可以在不使用SYN队列的情况下成功建立连接。**syncookies是这么做的服务器根据当前状态计算出一个值放在己方发出的SYN+ACK报文中发出当客户端返回ACK报文时取出该值验证如果合法就认为连接建立成功如下图所示。
![](https://static001.geekbang.org/resource/image/0d/c0/0d963557347c149a6270d8102d83e0c0.png)
Linux下怎样开启syncookies功能呢修改tcp\_syncookies参数即可其中值为0时表示关闭该功能2表示无条件开启功能而1则表示仅当SYN半连接队列放不下时再启用它。由于syncookie仅用于应对SYN泛洪攻击攻击者恶意构造大量的SYN报文发送给服务器造成SYN半连接队列溢出导致正常客户端的连接无法建立这种方式建立的连接许多TCP特性都无法使用。所以应当把tcp\_syncookies设置为1仅在队列满时再启用。
```
net.ipv4.tcp_syncookies = 1
```
当客户端接收到服务器发来的SYN+ACK报文后就会回复ACK去通知服务器同时己方连接状态从SYN\_SENT转换为ESTABLISHED表示连接建立成功。服务器端连接成功建立的时间还要再往后到它收到ACK后状态才变为ESTABLISHED。
如果服务器没有收到ACK就会一直重发SYN+ACK报文。当网络繁忙、不稳定时报文丢失就会变严重此时应该调大重发次数。反之则可以调小重发次数。**修改重发次数的方法是调整tcp\_synack\_retries参数**
```
net.ipv4.tcp_synack_retries = 5
```
tcp\_synack\_retries 的默认重试次数是5次与客户端重发SYN类似它的重试会经历1、2、4、8、16秒最后一次重试后等待32秒若仍然没有收到ACK才会关闭连接故共需要等待63秒。
服务器收到ACK后连接建立成功此时内核会把连接从SYN半连接队列中移出再移入accept队列等待进程调用accept函数时把连接取出来。如果进程不能及时地调用accept函数就会造成accept队列溢出最终导致建立好的TCP连接被丢弃。
实际上丢弃连接只是Linux的默认行为我们还可以选择向客户端发送RST复位报文告诉客户端连接已经建立失败。打开这一功能需要将tcp\_abort\_on\_overflow参数设置为1。
```
net.ipv4.tcp_abort_on_overflow = 0
```
**通常情况下应当把tcp\_abort\_on\_overflow设置为0因为这样更有利于应对突发流量。**举个例子当accept队列满导致服务器丢掉了ACK与此同时客户端的连接状态却是ESTABLISHED进程就在建立好的连接上发送请求。只要服务器没有为请求回复ACK请求就会被多次重发。如果服务器上的进程只是短暂的繁忙造成accept队列满那么当accept队列有空位时再次接收到的请求报文由于含有ACK仍然会触发服务器端成功建立连接。所以**tcp\_abort\_on\_overflow设为0可以提高连接建立的成功率只有你非常肯定accept队列会长期溢出时才能设置为1以尽快通知客户端。**
那么怎样调整accept队列的长度呢**listen函数的backlog参数就可以设置accept队列的大小。事实上backlog参数还受限于Linux系统级的队列长度上限当然这个上限阈值也可以通过somaxconn参数修改。**
```
net.core.somaxconn = 128
```
当下各监听端口上的accept队列长度可以通过ss -ltn命令查看但accept队列长度是否需要调整该怎么判断呢还是通过netstat -s命令给出的统计结果可以看到究竟有多少个连接因为队列溢出而被丢弃。
```
# netstat -s | grep "listen queue"
14 times the listen queue of a socket overflowed
```
如果持续不断地有连接因为accept队列溢出被丢弃就应该调大backlog以及somaxconn参数。
## TFO技术如何绕过三次握手
以上我们只是在对三次握手的过程进行优化。接下来我们看看如何绕过三次握手发送数据。
三次握手建立连接造成的后果就是HTTP请求必须在一次RTTRound Trip Time从客户端到服务器一个往返的时间后才能发送Google对此做的统计显示三次握手消耗的时间在HTTP请求完成的时间占比在10%到30%之间。
![](https://static001.geekbang.org/resource/image/1b/a8/1b9d8f49d5a716470481657b07ae77a8.png)
因此Google提出了TCP fast open方案简称[TFO](https://tools.ietf.org/html/rfc7413)客户端可以在首个SYN报文中就携带请求这节省了1个RTT的时间。
接下来我们就来看看TFO具体是怎么实现的。
**为了让客户端在SYN报文中携带请求数据必须解决服务器的信任问题。**因为此时服务器的SYN报文还没有发给客户端客户端是否能够正常建立连接还未可知但此时服务器需要假定连接已经建立成功并把请求交付给进程去处理所以服务器必须能够信任这个客户端。
TFO到底怎样达成这一目的呢它把通讯分为两个阶段第一阶段为首次建立连接这时走正常的三次握手但在客户端的SYN报文会明确地告诉服务器它想使用TFO功能这样服务器会把客户端IP地址用只有自己知道的密钥加密比如AES加密算法作为Cookie携带在返回的SYN+ACK报文中客户端收到后会将Cookie缓存在本地。
之后如果客户端再次向服务器建立连接就可以在第一个SYN报文中携带请求数据同时还要附带缓存的Cookie。很显然这种通讯方式下不能再采用经典的“先connect再write请求”这种编程方法而要改用sendto或者sendmsg函数才能实现。
服务器收到后会用自己的密钥验证Cookie是否合法验证通过后连接才算建立成功再把请求交给进程处理同时给客户端返回SYN+ACK。虽然客户端收到后还会返回ACK但服务器不等收到ACK就可以发送HTTP响应了这就减少了握手带来的1个RTT的时间消耗。
![](https://static001.geekbang.org/resource/image/7a/c3/7ac29766ba8515eea5bb331fce6dc2c3.png)
当然为了防止SYN泛洪攻击服务器的TFO实现必须能够自动化地定时更新密钥。
Linux下怎么打开TFO功能呢这要通过tcp\_fastopen参数。由于只有客户端和服务器同时支持时TFO功能才能使用**所以tcp\_fastopen参数是按比特位控制的。其中第1个比特位为1时表示作为客户端时支持TFO第2个比特位为1时表示作为服务器时支持TFO**所以当tcp\_fastopen的值为3时比特为0x11就表示完全支持TFO功能。
```
net.ipv4.tcp_fastopen = 3
```
## 小结
这一讲我们沿着三次握手的流程介绍了Linux系统的优化方法。
当客户端通过发送SYN发起握手时可以通过tcp\_syn\_retries控制重发次数。当服务器的SYN半连接队列溢出后SYN报文会丢失从而导致连接建立失败。我们可以通过netstat -s给出的统计结果判断队列长度是否合适进而通过tcp\_max\_syn\_backlog参数调整队列的长度。服务器回复SYN+ACK报文的重试次数由tcp\_synack\_retries参数控制网络稳定时可以调小它。为了应对SYN泛洪攻击应将tcp\_syncookies参数设置为1它仅在SYN队列满后开启syncookie功能保证连接成功建立。
服务器收到客户端返回的ACK后会把连接移入accept队列等待进程调用accept函数取出连接。如果accept队列溢出默认系统会丢弃ACK也可以通过tcp\_abort\_on\_overflow参数用RST通知客户端连接建立失败。如果netstat统计信息显示大量的ACK被丢弃后可以通过listen函数的backlog参数和somaxconn系统参数提高队列上限。
TFO技术绕过三次握手使得HTTP请求减少了1个RTT的时间。Linux下可以通过tcp\_fastopen参数开启该功能。
从这一讲可以看出虽然TCP是由操作系统实现的但Linux通过多种方式提供了修改TCP功能的接口供我们优化TCP的性能。下一讲我们再来探讨四次握手关闭连接时Linux怎样帮助我们优化其性能。
## 思考题
最后,留给你一个思考题,关于三次握手建立连接,你做过哪些优化?效果如何?欢迎你在留言区与大家一起探讨。
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。