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