关于backlog参数的疑惑

致敬 W.Richard Stevens

零. 背景知识

如果读者对以下知识点有基本的了解,读起来会很轻松,就像是在大学做物理实验。

  • Linux
  • C/Python/Java
  • TCP/IP

一. 系统调用listen

大多数同学第一次接触到backlog参数,是从listen函数开始的。
下面是关于listen函数的man手册摘要

int listen(int sockfd, int backlog);
...

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.

这段文档中有两个地方需要思考:

  1. the queue of pending connections
    可以有两种解释

    a. 已经完成三次握手的连接(ESTABLISHED)
    b. 已经完成三次握手的连接(ESTABLISHED) + 未完成三次握手的连接(SYN-RECEIVED)

    这两种解释可能都是对的,因为不同的操作系统有不同的实现。
    对现代Linux来说,这里的队列特指已经完成三次握手的连接

  2. 当队列已满的时候,直接返回reset是否合适(ECONNREFUSED)
    这样处理有个坏处,就是客户端无法区别是未处理队列满了,还是访问了系统未监听的端口,
    所以还是直接忽略这个SYN比较好。

本文档聚焦在第一个问题。

二. 内核参数

有两个内核参数,涉及到TCP三次握手的队列长度配置

  • net.core.somaxconn
    [ESTABLISHED] 已完成连接队列
    web 应用中 listen 函数的 backlog 默认会被我们内核参数net.core.somaxconn 限制到128

  • tcp_max_syn_backlog
    [SYN-RECEIVED] 未完成连接队列
    记录的那些尚未收到客户端确认信息的连接请求的最大值。

我们用两个实验来验证一下这两个参数。

三. 实验环境

两台虚拟机

  • 172.28.32.101 [服务端]
  • 172.28.32.102 [客户端]

服务端代码 serv.py

#!/usr/bin/python

import socket
import sys
import time

HOST = '0.0.0.0'   # Symbolic name meaning all available interfaces
PORT = 1234 # Arbitrary non-privileged port
BACKLOG = 2

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Socket created'

try:
    s.bind((HOST, PORT))
except socket.error , msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
    sys.exit()

print 'Socket bind complete'

s.listen(BACKLOG)
print 'Socket now listening'

time.sleep(3600)

不调用accept函数,已完成三次握手的连接可以保持在队列中

客户端代码 cli.py

#!/usr/bin/python

import socket   #for sockets
import sys  #for exit
import time

def conn(host, port):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    except socket.error, msg:
        print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1]
        sys.exit();

    s.connect((host , port))
    print "%s" %(time.time()) + ' Socket Connected to ' + host + ' on ip ' + host

    return s

def main():
    host = '172.28.32.101'
    port = 1234
    socks = []

    while True:
        sock = conn(host, port)
        socks.append(sock)
        time.sleep(1)

if __name__ == '__main__':
   main()

客户端程序,每隔10s钟,创建一个socket,并连接到服务端。

四. 实验一

1. 运行服务端程序

2. @服务端 查看网络连接

State      Recv-Q Send-Q                                  Local Address:Port                                                 Peer Address:Port
LISTEN     0      2                                                   *:1234                                                            *:*

backlog正确设置为2

3. @服务端 修改服务端代码

BACKLOG = 512

4. 运行服务端程序

5. @服务端 查看网络连接

State      Recv-Q Send-Q                                  Local Address:Port                                                 Peer Address:Port
LISTEN     0      128                                                 *:1234                                                            *:*

backlog设置为128,这并不是我们期望的

再看另外一个内核参数

[root@vagrant-172-28-32-101 ~]# sysctl net.core.somaxconn
net.core.somaxconn = 128

是的,backlog 默认会被我们内核参数net.core.somaxconn 限制

6. @服务端 修改内核参数

echo 1024 > /proc/sys/net/core/somaxconn

7. 重新运行服务端程序

8. @服务端 查看网络连接

State      Recv-Q Send-Q                                  Local Address:Port                                                 Peer Address:Port
LISTEN     0      512                                                 *:1234                                                            *:*

这次,backlog被正确设置为512啦

背景知识

  • Recv-Q
    • Established: The count of bytes not copied by the user program connected to this socket.
    • Listening: Since Kernel 2.6.18 this column contains the current syn backlog.
  • Send-Q
    • Established: The count of bytes not acknowledged by the remote host.
    • Listening: Since Kernel 2.6.18 this column contains the maximum size of the syn backlog.

五. 实验二

1. @服务端 运行服务端程序

[root@vagrant-172-28-32-101 ~]# ./serv.py
Socket created
Socket bind complete
Socket now listening

再打开另一个终端查看

[root@vagrant-172-28-32-101 ~]# netstat -an|grep 1234
tcp        0      0 0.0.0.0:1234            0.0.0.0:*               LISTEN

很明显,端口1234已经被监听

2. @客户端 运行客户端程序

[root@vagrant-172-28-32-102 ~]# ./cli.py
Socket Created
Socket Connected to 172.28.32.101 on ip 172.28.32.101
Socket Created
Socket Connected to 172.28.32.101 on ip 172.28.32.101
Socket Created

3. @服务端 查看连接状态

[root@vagrant-172-28-32-101 ~]# netstat -ant |grep 1234
tcp        3      0 0.0.0.0:1234            0.0.0.0:*               LISTEN
tcp        0      0 172.28.32.101:1234      172.28.32.102:38254     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:38252     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:38246     ESTABLISHED
tcp        0      0 172.28.32.101:1234      172.28.32.102:38250     ESTABLISHED
tcp        0      0 172.28.32.101:1234      172.28.32.102:38248     ESTABLISHED

很明显,端口1234只有3个已完成连接,后端连接都停留在SYN_RECV。

Q:为什么是3个ESTABLISHED连接,而不是两个呢?
A:因为Linux实现的是队列长度=backlog+1
参见 《UNIX网络编程》第四章第五节 listen函数

6. backlog队列满了之后,系统怎么处理后续连接请求

服务端运行抓包命令(抓包结果删除部分无用信息)

[root@vagrant-172-28-32-101 ~]# tcpdump -i any port 1234 -nnn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes

18:30:46 IP 172.28.32.102.42804 > 172.28.32.101.1234: Flags [S], seq 3272162761
18:30:46 IP 172.28.32.101.1234 > 172.28.32.102.42804: Flags [S.], seq 1934414166
18:30:46 IP 172.28.32.102.42804 > 172.28.32.101.1234: Flags [.], ack 1, win 229 
18:30:47 IP 172.28.32.101.1234 > 172.28.32.102.42804: Flags [S.], seq 1934414166 
18:30:47 IP 172.28.32.102.42804 > 172.28.32.101.1234: Flags [.], ack 1
18:30:49 IP 172.28.32.101.1234 > 172.28.32.102.42804: Flags [S.], seq 1934414166
18:30:49 IP 172.28.32.102.42804 > 172.28.32.101.1234: Flags [.], ack 1
18:30:54 IP 172.28.32.101.1234 > 172.28.32.102.42804: Flags [S.], seq 1934414166
18:30:54 IP 172.28.32.102.42804 > 172.28.32.101.1234: Flags [.], ack 1
18:31:02 IP 172.28.32.101.1234 > 172.28.32.102.42804: Flags [S.], seq 1934414166
18:31:02 IP 172.28.32.102.42804 > 172.28.32.101.1234: Flags [.], ack 1
18:31:18 IP 172.28.32.101.1234 > 172.28.32.102.42804: Flags [S.], seq 1934414166
18:31:18 IP 172.28.32.102.42804 > 172.28.32.101.1234: Flags [.], ack 1

服务端忽略了客户端返回的ack信息,然后重新发送了6次syn+ack,
连接处于SYN_RECV,一段时间后,自动删除

7. @服务端 查看连接状态

[root@vagrant-172-28-32-102 ~]# netstat -ant|grep 42804
tcp        0      0 172.28.32.102:42804     172.28.32.101:1234      ESTABLISHED

可见,在客户端看来,连接已经建立完成。这中情况下,就可能造成服务端和客户端连接状态不一致的情况。

4. @客户端 中断客户端程序,然后再重启

5. @服务端 查看连接状态

[root@vagrant-172-28-32-101 ~]# netstat -ant |grep 1234
tcp        3      0 0.0.0.0:1234            0.0.0.0:*               LISTEN
tcp        0      0 172.28.32.101:1234      172.28.32.102:42172     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:42178     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:42164     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:42182     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:42176     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:42174     SYN_RECV
tcp        0      0 172.28.32.101:1234      172.28.32.102:42180     SYN_RECV
tcp        1      0 172.28.32.101:1234      172.28.32.102:42158     CLOSE_WAIT
tcp        1      0 172.28.32.101:1234      172.28.32.102:42160     CLOSE_WAIT
tcp        1      0 172.28.32.101:1234      172.28.32.102:42162     CLOSE_WAIT

有3个连接处于CLOSE_WAIT,新连接已经无法建立,说明已完成连接包含状态CLOSE_WAIT。
这个CLOSE_WAIT的产生条件,建立连接后,没有被accept关联到文件描述符后,客户端异常关闭

六. 实验三

tcp_max_syn_backlog, 这个参数很容易和listen函数的backlog混淆。看一下官方解释

Maximal number of remembered connection requests, which have not
received an acknowledgment from connecting client.

详细解释请查看 src/linux-3.10.105/Documentation/networking/ip-sysctl.txt

我们继续实验。

1. @服务端 修改两个内核参数值

  • 关闭SYN Cookie
    echo 0 > /proc/sys/net/ipv4/tcp_syncookies
  • 调整tcp_max_syn_backlog
    echo 4 > /proc/sys/net/ipv4/tcp_max_syn_backlog

Q: 为什么关闭SYN Cookie?
A: When syncookies are enabled there is no logical maximum length and this sysctl(tcp_max_syn_backlog) setting is ignored.

2. @服务端 运行serv.py

3. @客户端 添加iptables规则

禁止客户端发送RST和ACK

iptables -I OUTPUT -p tcp --dport 1234 --tcp-flags RST ACK -j DROP

4. @客户端 运行cli.py

修改其中一条打印语句

 s.connect((host , port))
 print "%s" %(time.time()) + ' local port: ' + "%s" % (s.getsockname()[1])

5. @服务端 查看网络连接

[root@vagrant-172-28-32-101 ~]# netstat -ant|grep 1234|sort
tcp        0      0 172.28.32.101:1234      192.168.1.233:51475     ESTABLISHED
tcp        0      0 172.28.32.101:1234      192.168.1.233:51476     ESTABLISHED
tcp        0      0 172.28.32.101:1234      192.168.1.233:51477     ESTABLISHED
tcp        0      0 172.28.32.101:1234      192.168.1.233:51482     SYN_RECV
tcp        0      0 172.28.32.101:1234      192.168.1.233:51483     SYN_RECV
tcp        0      0 172.28.32.101:1234      192.168.1.233:51484     SYN_RECV
tcp        0      0 172.28.32.101:1234      192.168.1.233:51485     SYN_RECV
tcp        3      0 0.0.0.0:1234            0.0.0.0:*               LISTEN

有3个sock已经建立连接,有4个sock处于半连接,符合我们的预期

5. @客户端 查看程序打印的日志

1490059497.78 local port: 51475
1490059498.79 local port: 51476
1490059499.81 local port: 51477
1490059500.82 local port: 51478
1490059501.83 local port: 51479
1490059502.85 local port: 51480
1490059503.87 local port: 51481
1490059568.0 local port: 51482

前面7个socket都是每秒建立一个连接,第8个连接经过大约64秒后才建立,说明服务端处于SYN_RECV状态的连接超时被删除,第8个连接才得以完成三次握手。

6. @客户端 查看网络连接状态

]# netstat -ant|grep 1234|sort
tcp        0      0 172.28.32.102:51475     192.168.1.193:1234      ESTABLISHED
tcp        0      0 172.28.32.102:51476     192.168.1.193:1234      ESTABLISHED
tcp        0      0 172.28.32.102:51477     192.168.1.193:1234      ESTABLISHED
tcp        0      0 172.28.32.102:51478     192.168.1.193:1234      ESTABLISHED
tcp        0      0 172.28.32.102:51479     192.168.1.193:1234      ESTABLISHED
tcp        0      0 172.28.32.102:51480     192.168.1.193:1234      ESTABLISHED
tcp        0      0 172.28.32.102:51481     192.168.1.193:1234      ESTABLISHED
tcp        0      1 172.28.32.102:51482     192.168.1.193:1234      SYN_SENT

第8个连接处于SYN_SENT状态,服务端没有返回SYN/ACK, 可以确认服务器丢弃了客户端发出的SYN包

7. @服务端 查看系统统计信息和日志

]# netstat -s|grep dropped
181924245 SYNs to LISTEN sockets dropped

可见,系统已经丢弃了很多SYN包

# dmesg|grep 51482
[13766.635742] TCP: drop open request from 172.28.32.101/51482

从系统日志中可以看出,系统确实把第8个连接的SYN丢弃啦。

七. 总结

一般不要直接使用网上找到的一些优化参数,而应该深入了解各个参数的细节,知其然,并知其所以然,这样才能把系统优化的工作做好。

推荐阅读更多精彩内容