Home 使用 packetdrill 观测 TCP 服务监听的 backlog 参数
Post
Cancel

使用 packetdrill 观测 TCP 服务监听的 backlog 参数

本文尝试使用 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 行为,没啥惊喜。
  • shutdownshutdown(READ) 对于对端来说是不可感知的,其实没有 FIN 是显然的,不用测试。
  • 40ms:TCP 40ms 魔术值的来源。
This post is licensed under CC BY 4.0 by the author.
Contents