在一次帮同事排查问题的过程中,我发现当派发请求的线程因 OOM 终止后,netstat
命令显示的 Recv-Q
与设置的 backlog
并不完全相同,而是存在 Recv-Q = backlog + 1
的关系,比如执行 netstat -tulnp
命令输出如下:
1 | Active Internet connections (only servers) |
其中 80 端口为 Tomcat 服务器监听的端口,在此 Socket 上,我们使用的默认 backlog
配置,值为 100,源码位于 AbstractEndpoint.java at 8.5.59:
1 | /** |
而 1234 端口为我们 Agent 启动的用于提供 Prometheus 拉取监控数据的 HTTP Server 端口,当时的创建代码如下:
1 | HttpServer.create(new InetSocketAddress(PROMETHEUS_SERVER_PORT), 0); |
这段代码在创建 HTTPServer 过程中会在 ServerSocket.java at jdk8-b120 中将 backlog
重置为 50 并设置至底层的 Socket:
1 | /** |
根据 netstat(8) - Linux manual page 我们知道,当端口处于 LISTEN
状态时,Recv-Q
表示 SYN 积压。原文如下:
Listening: Since Kernel 2.6.18 this column contains the current syn backlog.
而为何此处显示的 Recv-Q
值为 backlog + 1
呢?Google 并没有告诉我答案,于是查阅了 Linux 的源码,发现判断接收队列长度是否超过 backlog
的逻辑来来回回改了三四次,GitHub 上最初的版本于 2005-04-17 提交,源码如下:
1 | static inline int sk_acceptq_is_full(struct sock *sk) |
即在接收新连接前,当接收队列当前的 backlog
大于 设定的 backlog
时,判定接收队列已满,此时 DROP 掉 SYN 包。随后在 2007-03-03,WeiDong 对该方法进行了修改,提交位于 NET: Fix bugs in “Whether sock accept queue is full” checking · torvalds/linux@8488df8 · GitHub,Commit 信息为:
1 | when I use linux TCP socket, and find there is a bug in function |
该提交将以上方法实现修改为了:
1 | static inline int sk_acceptq_is_full(struct sock *sk) |
即在接收连接前判断接收队列当前的 backlog
大于等于 设定的 backlog
时,判定为接收队列已满。类似的一个提交位于:AF_UNIX: Test against sk_max_ack_backlog properly. · torvalds/linux@248f067 · GitHub。随即在 2007-03-07 David S. Miller 将上次的改动进行了还原,提交位于:NET: Revert incorrect accept queue backlog changes. · torvalds/linux@64a1465 · GitHub,其 Commit 信息如下:
1 | This reverts two changes: |
即如 Commit 信息所阐述,定义的 backlog
值 N
表示队列中允许存在 N + 1
个连接,虽然这与直觉不符,但是他们确实是这样定义的。难道这就是中西方文化的差异?十四年后的 2021-03-13,liuyacan 进行了一次提交,又将 大于 改为 大于等于 以符合我们国人的直觉,本次提交位于:net: correct sk_acceptq_is_full() · torvalds/linux@f211ac1 · GitHub,其 Commit 信息如下:
1 | The "backlog" argument in listen() specifies |
遗憾的是,在 2021-04-01 上面的提交就被还原了,本次还原的提交位于:Revert “net: correct sk_acceptq_is_full()” · torvalds/linux@c609e6a · GitHub,其 Commit 信息如下:
1 | This reverts commit f211ac1. |
这次不仅还原,还特地给这段代码加上了注释,以警示后人不要再改了?加上注释的源码为:
1 | /* Note: If you think the test should be: |
直至今日,master 分支上依然是使用的 大于 的版本,即接收新连接前队列中的连接数需要大于设定的 backlog
值才表示连接队列已满,即队列中可以存储的最大连接数为 backlog + 1
,这也解释了我观察到的 Recv-Q
值等于 backlog + 1
的现象。
Reproduce
可以使用以下程序自行验证在不同 Linux 内核下的 backlog
表现:
1 | import java.io.IOException; |
使用 javac App.java
编译并用 java App
执行该程序,随即使用 telnet
尝试连接 1234 端口,即可验证该问题。
Remark
在 《TCP/IP详解 卷1:协议》13.7.4 进入连接队列 这一节中,使用 backlog = 1
进行演示时,FreeBSD 服务器接收了两个连接,后续的连接不能接收到任何响应并最终在客户端超时。
Reference
502 Bad Gateway - HTTP | MDN
How TCP backlog works in Linux
TCP Flags: PSH and URG - PacketLife.net
TCP half-open - Wikipedia
Detection of Half-Open (Dropped) Connections
TCP/IP详解 卷1:协议 - 图书 - 豆瓣
listen() ignores the backlog argument? - Stack Overflow
SYN packet handling in the wild