再谈三次握手
Linux 提供了listen()
系统调用,它的作用是在 server socket 上监听新的连接请求:
那么listen()
的第二个参数backlog
的作用是什么呢?让我们先回顾一下 TCP 的三次握手:
对于 server 来说,一个新的连接首先会经过 SYN_RECV 状态,然后才会进入 ESTABLISHED 状态。那么对于每个 server socket 来说,内核需要两个队列来保存所有连接的信息:
- 一个是半连接队列 (incompletely queue),用来保存所有处在 SYN_RECV 状态的连接。
- 另一个是连接队列 (completely queue),用来保存所有处在 ESTABLISHED 状态的连接。
也就是说,当一个新的连接进入 SYNC_RECV 状态时,内核会将它放到半连接队列中。而当这个连接从 SYNC_RECV 状态进入到 ESTABLISHED 状态时,内核会将它从半连接队列中移动到连接队列中。最后当accept()
返回时,这个连接也会从连接队列中移走。
listen()
的backlog
参数其实是用来指定连接队列的长度。而如果要改变半连接队列的长度,则需要改变内核参数:
在实际情况中,如果 server 的负载比较高,无法迅速地accept()
新的连接,就可能导致连接队列满了,这时候就需要调高listen()
的backlog
参数。然而注意到,backlog
参数有一个上限,这个上限由内核参数net.core.somaxconn
决定,这个参数的默认值是128
。也就是说,如果我们要让backlog
的上限大于 128,就需要调高net.core.somaxconn
这个内核参数的值:
让我们思考一个问题,对于 server 的一个连接来说,当它接收到 client 发送的 ACK segment 之后,就会从 SYNC_RECV 进入到 ESTABLISHED 状态,但如果这时连接队列满了,那么会发生什么事情呢?
默认情况下,server的 TCP 协议栈会丢弃这个 ACK segment。但此时这个连接仍然处在 SYNC_RECV 状态,所以 server 的 TCP 协议栈会重新发送 SYN/ACK 给 client。下图显示了进行一次重试的过程:
最大的重传次数由内核参数
net.ipv4.tcp_synack_retries
规定,默认情况下,这个值是 5,重传 5 次的总耗时约为 180 秒。在某些情况下,我们可以调小这个值,以减小重传的超时时间。
参考资料
- How TCP backlog works in Linux
- Defenses Against TCP SYN Flooding Attacks
- Linux Broadband Tweaks
- Linux TCP/IP tuning for scalability
- Coping with the TCP TIME-WAIT state on busy Linux servers
- TIME_WAIT and its design implications for protocols and scalable client server systems
- The TIME-WAIT state in TCP and Its Effect on Busy Servers
- Guide to limits.conf / ulimit /open file descriptors under linux