listen 的 backlog 到底限制什么?队列满了新连接会怎样?半连接和全连接队列怎么区分?这篇用 netstat -L 逐项量,并抓包发现 macOS 在全连接队列满时的做法和 Linux 不一样:握手照常完成,然后被 RST。
kern.ipc.somaxconn: 128 listen( 0):发起 20 个 connect,1 秒后连上 20 个;netstat -L 显示 qlen/incqlen/maxqlen = 20/0/128 listen( 1):发起 21 个 connect,1 秒后连上 1 个;netstat -L 显示 qlen/incqlen/maxqlen = 1/0/1 listen( 5):发起 25 个 connect,1 秒后连上 5 个;netstat -L 显示 qlen/incqlen/maxqlen = 5/0/5 listen( 10):发起 30 个 connect,1 秒后连上 10 个;netstat -L 显示 qlen/incqlen/maxqlen = 10/0/10 listen( 50):发起 70 个 connect,1 秒后连上 50 个;netstat -L 显示 qlen/incqlen/maxqlen = 50/0/50 listen( 128):发起 148 个 connect,1 秒后连上 128 个;netstat -L 显示 qlen/incqlen/maxqlen = 128/0/128 listen(1000):发起 320 个 connect,1 秒后连上 128 个;netstat -L 显示 qlen/incqlen/maxqlen = 128/0/128 结论:backlog 不是“队列刚好 N 个”:macOS 上 listen(N) 实际能容纳的已完成连接数和 N 的关系见上面(maxqlen 是内核记下的上限,实际连上的数量还多一点)
listen(N) 之后,全连接队列正好容纳 N 个(0 当成默认值 128),上限被 kern.ipc.somaxconn(128)卡住。
塞满以后 netstat -L = 5/0/5(qlen/incqlen/maxqlen) 队列已满时的第 1 个客户端:connect 成功(用时 0.14 ms);随后 send BrokenPipeError[Errno 32];recv ConnectionResetError[Errno 54];线上:C→S SYN → S→C SYN+ACK → C→S ACK → S→C RST 队列已满时的第 2 个客户端:connect 成功(用时 0.17 ms);随后 send BrokenPipeError[Errno 32];recv ConnectionResetError[Errno 54];线上:C→S SYN → S→C SYN+ACK → C→S ACK → S→C RST 队列已满时的第 3 个客户端:connect 成功(用时 0.33 ms);随后 send BrokenPipeError[Errno 32];recv ConnectionResetError[Errno 54];线上:C→S SYN → S→C SYN+ACK → C→S ACK → S→C RST netstat -L 此刻 = 5/0/5(队列还是满的);netstat -s -p tcp 的“listen queue overflow”在这一段增加了 0 服务端 accept 了 1 个腾出一个位置之后,再来一个客户端:连上、能发数据、没有被重置;netstat -L = 5/0/5 macOS(BSD 系)的行为:队列满时服务端照常回 SYN-ACK,客户端 connect() 成功返回;收到第三个 ACK 时发现全连接队列放不下,发 RST 把这个连接丢掉。客户端以为连上了,第一次读写才发现被重置——和“SYN 被丢弃、客户端不停重传 SYN”(Linux 的常见行为)不同
macOS 不丢 SYN:仍回 SYN-ACK,客户端 connect 成功;第三个 ACK 到达时才发现放不下,用 RST 丢掉。客户端的第一次读写才会报错。netstat -s 的 TCP 计数在这台机器上全是 0,不可用。
listen(1000):400 个 connect,连上 128 个;netstat -L:qlen/incqlen/maxqlen = 128/0/128——maxqlen 被截到了 128,正好是 somaxconn
半连接队列(SYN_RCVD)与全连接队列(ESTABLISHED、等 accept)。netstat -Lan 显示 qlen/incqlen/maxqlen。
发起 40 个并发连接后,netstat -L 的 qlen/incqlen/maxqlen 随时间:6 ms 0/0/128;235 ms 0/0/128;472 ms 40/40/128;699 ms 40/40/128;928 ms 40/0/128;1150 ms 40/0/128;1388 ms 40/0/128 incqlen 最大 40(正在握手中的半连接),qlen 最大 40(握手完成、等 accept 的全连接) 所以有两个队列:半连接队列(SYN_RCVD,握手没完成)和全连接队列(已 ESTABLISHED,等应用 accept);listen 的 backlog 管全连接队列,应用 accept 太慢会让它满,新的 SYN 被丢弃
客户端偶尔 “connection reset by peer” 却没有任何服务端日志,可以先看 netstat -Lan 里这个端口的队列是不是满的。