本文尝试使用 packetdrill 分析 listen(..., backlog)
函数中 backlog 对等待连接的行为影响。
背景
写了一大段还是删了,总之我不懂网络编程就对了。
看文档
做任何事情前先看文档,这里参考 man 手册。
man 2 listen
The backlog argument defines the maximum length to which the
queue of pending connections for sockfd may grow. If a
connection request arrives when the queue is full, the client may
receive an error with an indication of ECONNREFUSED or, if the
underlying protocol supports retransmission, the request may be
ignored so that a later reattempt at connection succeeds.
The behavior of the backlog argument on TCP sockets changed with
Linux 2.2. Now it specifies the queue length for completely
established sockets waiting to be accepted, instead of the number
of incomplete connection requests. The maximum length of the
queue for incomplete sockets can be set using
/proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are
enabled there is no logical maximum length and this setting is
ignored. See tcp(7) for more information.
一些关键信息是:
- backlog 能控制 pending connections 长度,也就是等待连接数。
- backlog 自 Linux 2.2 版本后是指定完全连接队列(而非 SYN_RECV 半连接队列)的长度。
但是,你确定吗?
看代码
既然用上了 packetdrill,那么协议栈测试代码非常简单。
首先要构建基本的环境(基于 Linux v6.4.8):
--bind_port=8848
0 socket(AF_INET, SOCK_STREAM, 0) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
// backlog 设为 1
+0 listen(3, 1) = 0
这里构造一个简单的服务器,packetdrill 默认使用端口 8080,但我的环境中同端口拿去干别的事情了,这里另外绑定为 8848 端口。关键测试点是 backlog=1。
// 设置有 5 个 client 跟 server 握手
+.1 < 9991>8848 S 0:0(0) win 1000
+.1 < 9992>8848 S 0:0(0) win 1000
+.1 < 9993>8848 S 0:0(0) win 1000
+.1 < 9994>8848 S 0:0(0) win 1000
+.1 < 9995>8848 S 0:0(0) win 1000
现在往内核注入 5 个 SYN 报文,模拟了 5 个客户端的第一次握手。端口声明的语法细节请看 syntax.md(126 行和 62 行),packetdrill 的语法教程就是这么朴实无华。
NOTE: 这里客户端的端口号是从 9991 到 9995,如果不声明则是使用同一随机端口,会被服务端误以为是单个客户端重复发出 SYN。
// $ tcpdump -i any -n port 8848
16:32:03.370546 tun0 In IP 192.0.2.1.9991 > 192.168.108.45.8848: Flags [S], seq 0, win 1000, length 0
16:32:03.370607 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9991: Flags [S.], seq 2417053888, ack 1, win 64240, options [mss 1460], length 0
16:32:03.468709 tun0 In IP 192.0.2.1.9992 > 192.168.108.45.8848: Flags [S], seq 0, win 1000, length 0
16:32:03.468786 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9992: Flags [S.], seq 2959693250, ack 1, win 64240, options [mss 1460], length 0
16:32:03.568339 tun0 In IP 192.0.2.1.9993 > 192.168.108.45.8848: Flags [S], seq 0, win 1000, length 0
16:32:03.568367 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9993: Flags [S.], seq 73263118, ack 1, win 64240, options [mss 1460], length 0
16:32:03.670731 tun0 In IP 192.0.2.1.9994 > 192.168.108.45.8848: Flags [S], seq 0, win 1000, length 0
16:32:03.670773 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9994: Flags [S.], seq 2538559182, ack 1, win 64240, options [mss 1460], length 0
16:32:03.772050 tun0 In IP 192.0.2.1.9995 > 192.168.108.45.8848: Flags [S], seq 0, win 1000, length 0
16:32:03.772102 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9995: Flags [S.], seq 495130115, ack 1, win 64240, options [mss 1460], length 0
16:32:04.441265 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9991: Flags [S.], seq 2417053888, ack 1, win 64240, options [mss 1460], length 0
16:32:06.517588 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9991: Flags [S.], seq 2417053888, ack 1, win 64240, options [mss 1460], length 0
16:32:10.602864 tun0 Out IP 192.168.108.45.8848 > 192.0.2.1.9991: Flags [S.], seq 2417053888, ack 1, win 64240, options [mss 1460], length 0
这是通过 tcpdump
抓包得到的握手信息,可以看出:
- 客户端发出了第一次握手(S)。
- 服务端也回应了第二次握手(S.)。
由于是伪造的客户端,也刻意没有继续注入握手报文,因此不存在第三次握手。
// 在这里可以通过 netstat 看出只有 9991 是存在 RECV 关系
tcp 0 0 192.168.146.218:8848 192.0.2.1:9991 SYN_RECV
// ss 观察监听 socket 的队列,Recv-Q=0,全连接队列没有东西是显然的
LISTEN 0 1 192.168.241.27:8848
其实从前面的报文也可以看出 man 手册描述的不对劲,这里尚未建立任意一个连接,但是服务端却一直只重试最早到来的客户端(9991)的连接请求。通过 netstat
观察,内核只记住了一条来自 9991 的请求(保持 SYN_RECV,躺在半连接队列中),其它的连接都被“忘记”。
// backlog 设为 2
+0 listen(3, 2) = 0
你可以尝试将 listen()
内的 backlog 参数调为 2,再次测试上面的代码,会发现能记住的连接就是 2 条。
结论
结论很简单,man 手册描述有误,backlog 会限制半连接队列的长度。但是还需要说明,backlog 确实也限制着全连接队列的长度,这方面简单写个 echo 都能验证。
附录一:这重要吗
文章写得有点短了,再水点什么吧。
N 年前的我不知道从哪里搞来的 Linux v2.6.x 队列计算公式:
设 listen()
传入时不经任何修改的 backlog 为 \(backlog_{raw}\),即可算出
全连接队列的长度:
\[backlog_{fixed} = min(backlog_{raw}, net.core.somaxconn)\]半连接队列的长度:
\[max(16, roundup(min(backlog_{fixed}, tcp\_max\_syn\_backlog) + 1))\]其中 \(somaxconn\) 和 \(syn\_backlog\) 可以通过 sysctl 查看和修改,\(roundup\) 指的是 2 的幂的上取整。
这些东西重要吗?完全不重要。算法永远在变化(上面的公式在 v6.4.8 的环境下明显错误),只需知道 backlog 是控制等待连接数的主要参数就好了。
附录二:错误代码
在前面的测试中,我使用了构造指定端口的报文来测试 backlog。但是有一点没注意,就是这种写法本身是不受 packetdrill 良好支持的。比如我打算进一步测试:
// 如果再跟一个被“忘记”的 client 继续握手会怎样?
+1 < 9994>8848 . 1:1(0) ack 1 win 1000
// 注:这是第二次测试,跟前面的 tcpdump seq/ack不连续,但不影响结果
// 基本上就是前面重试前插入前两条,后续接着重试 9991 握手
// 这里看出是服务端直接抛出了 RST
16:38:43.894319 tun0 In IP 192.0.2.1.9994 > 192.168.122.90.8848: Flags [.], ack 405264024, win 1000, length 0
16:38:43.894341 tun0 Out IP 192.168.122.90.8848 > 192.0.2.1.9994: Flags [R], seq 1, win 0, length 0
16:38:49.691350 tun0 Out IP 192.168.122.90.8848 > 192.0.2.1.9991: Flags [S.], seq 2329322482, ack 1, win 64240, options [mss 1460], length 0
16:38:57.135448 tun0 Out IP 192.168.122.90.8848 > 192.0.2.1.9991: Flags [S.], seq 2329322482, ack 1, win 64240, options [mss 1460], length 0
16:39:13.773886 tun0 Out IP 192.168.122.90.8848 > 192.0.2.1.9991: Flags [S.], seq 2329322482, ack 1, win 64240, options [mss 1460], length 0
但是这种写法是不对的,原因是 ack 405264024
(相对值)其实等同于 ack 1
(绝对值,这种情况 packetdrill 没法帮忙生成转换为正确的数字),导致服务端根本不理解这条报文从而抛出 RST。
NOTE: 有需要可以在 tcpdump
加上 -S
观察使用绝对值的序列号。
man 7 tcp
tcp_abort_on_overflow (Boolean; default: disabled; since Linux
2.4)
Enable resetting connections if the listening service is
too slow and unable to keep up and accept them. It means
that if overflow occurred due to a burst, the connection
will recover. Enable this option only if you are really
sure that the listening daemon cannot be tuned to accept
connections faster. Enabling this option can harm the
clients of your server.
而正确的行为应该是取决于 tcp_abort_on_overflow
选项。默认是 0,意味着仅忽视;只有设为 1 才会更加激进地使用 RST。
附录三:完整代码
--bind_port=8848
0 socket(AF_INET, SOCK_STREAM, 0) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+.1 < 9991>8848 S 0:0(0) win 1000
+.1 < 9992>8848 S 0:0(0) win 1000
+.1 < 9993>8848 S 0:0(0) win 1000
+.1 < 9994>8848 S 0:0(0) win 1000
+.1 < 9995>8848 S 0:0(0) win 1000
// +1 < 9994>8848 . 1:1(0) ack 1 win 1000
+0 `sleep 1000000`
附录四:其他测试
最近也顺便做了点其他的 TCP 行为测试,基本都是边边角角的情况,顺手贴出来吧:
- two_way_handshake:复现 TCP 两次握手行为,第三次直接发出数据,Linux 实现是允许的。
- nagle:Linux 的 Nagle 实现区别于 RFC1122,小报文发出前不需确认此前 MSS 报文的 ACK。
- cork:简单看了一下 MSG_MORE 标记和 TCP_CORK 行为,没啥惊喜。
- shutdown:
shutdown(READ)
对于对端来说是不可感知的,其实没有 FIN 是显然的,不用测试。 - 40ms:TCP 40ms 魔术值的来源。