<audio id="audio" title="09 | 如何提升TCP三次握手的性能？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/72/9e/726a6d37e4a50655c75344829823169e.mp3"></audio>

你好，我是陶辉。

上一讲我们提到TCP在三次握手建立连接、四次握手关闭连接时是怎样产生事件的，这两个过程中TCP连接经历了复杂的状态变化，既容易导致编程出错，也有很大的优化空间。这一讲我们看看在Linux操作系统下，如何优化TCP的三次握手流程，提升握手速度。

TCP是一个可以双向传输的全双工协议，所以需要经过三次握手才能建立连接。三次握手在一个HTTP请求中的平均时间占比在10%以上，在网络状况不佳、高并发或者遭遇SYN泛洪攻击等场景中，如果不能正确地调整三次握手中的参数，就会对性能有很大的影响。

TCP协议是由操作系统实现的，调整TCP必须通过操作系统提供的接口和工具，这就需要理解Linux是怎样把三次握手中的状态暴露给我们，以及通过哪些工具可以找到优化依据，并通过哪些接口修改参数。

因此，这一讲我们将介绍TCP握手过程中各状态的意义，并以状态变化作为主线，看看如何调整Linux参数才能提升握手的性能。

## 客户端的优化

客户端和服务器都可以针对三次握手优化性能。相对而言，主动发起连接的客户端优化相对简单一些，而服务器需要在监听端口上被动等待连接，并保存许多握手的中间状态，优化方法更为复杂一些。我们首先来看如何优化客户端。

三次握手建立连接的首要目的是同步序列号。只有同步了序列号才有可靠的传输，TCP协议的许多特性都是依赖序列号实现的，比如流量控制、消息丢失后的重发等等，这也是三次握手中的报文被称为SYN的原因，因为SYN的全称就叫做Synchronize Sequence Numbers。

<img src="https://static001.geekbang.org/resource/image/c5/aa/c51d9f1604690ab1b69e7c4feb2f31aa.jpg" alt="">

三次握手虽然由操作系统实现，但它通过连接状态把这一过程暴露给了我们，我们来细看下过程中出现的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分钟。

如果这是一台有明确任务的服务器，你可以根据网络的稳定性和目标服务器的繁忙程度修改重试次数，调整客户端的三次握手时间上限。比如内网中通讯时，就可以适当调低重试次数，尽快把错误暴露给应用程序。

<img src="https://static001.geekbang.org/resource/image/a3/8f/a3c5e77a228478da2a6e707054043c8f.png" alt="">

## 服务器端的优化

当服务器收到SYN报文后，服务器会立刻回复SYN+ACK报文，既确认了客户端的序列号，也把自己的序列号发给了对方。此时，服务器端出现了新连接，状态是SYN_RCV（RCV是received的缩写）。这个状态下，服务器必须建立一个SYN半连接队列来维护未完成的握手信息，当这个队列溢出后，服务器将无法再建立新连接。

<img src="https://static001.geekbang.org/resource/image/c3/82/c361e672526ee5bb87d5f6b7ad169982.png" alt="">

新连接建立失败的原因有很多，怎样获得由于队列已满而引发的失败次数呢？netstat -s命令给出的统计结果中可以得到。

```
# netstat -s | grep &quot;SYNs to LISTEN&quot;
    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报文时，取出该值验证，如果合法，就认为连接建立成功，如下图所示。

<img src="https://static001.geekbang.org/resource/image/0d/c0/0d963557347c149a6270d8102d83e0c0.png" alt="">

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 &quot;listen queue&quot;
    14 times the listen queue of a socket overflowed

```

如果持续不断地有连接因为accept队列溢出被丢弃，就应该调大backlog以及somaxconn参数。

## TFO技术如何绕过三次握手？

以上我们只是在对三次握手的过程进行优化。接下来我们看看如何绕过三次握手发送数据。

三次握手建立连接造成的后果就是，HTTP请求必须在一次RTT（Round Trip Time，从客户端到服务器一个往返的时间）后才能发送，Google对此做的统计显示，三次握手消耗的时间，在HTTP请求完成的时间占比在10%到30%之间。

<img src="https://static001.geekbang.org/resource/image/1b/a8/1b9d8f49d5a716470481657b07ae77a8.png" alt="">

因此，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的时间消耗。

<img src="https://static001.geekbang.org/resource/image/7a/c3/7ac29766ba8515eea5bb331fce6dc2c3.png" alt="">

当然，为了防止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怎样帮助我们优化其性能。

## 思考题

最后，留给你一个思考题，关于三次握手建立连接，你做过哪些优化？效果如何？欢迎你在留言区与大家一起探讨。

感谢阅读，如果你觉得这节课对你有一些启发，也欢迎把它分享给你的朋友。
