对于 Linux TCP 套接字,listen() 的 backlog 关注已经建立、等待应用接受的连接;未完成握手的队列是另一种概念。调用 accept() 会移出一条待处理连接,并返回供应用使用的已连接套接字。即使这个套接字之后长时间保持打开或忙碌,它也不再占用监听器的 accept 队列。因此,等待接受与已经接受后的活动状态需要分别计数。
坏掉的样子
如果连接到达速度超过应用接受速度,待处理队列会填满。根据内核行为和设置,新客户端可能遇到重传延迟或超时,而已经接受的请求仍可能正常。进程活着,或者部分请求成功,都不能证明监听器正足够快地接受新连接。问题发生在接受之前时,仅查看业务处理成功率可能看不到完整失败路径。
请求的 backlog 还受到 net.core.somaxconn 限制;只把应用值调得更大,不能超过这个上限。但可观察的精确队列边界与实现有关,因此不能把整数直接当成跨平台承诺,认为必定恰好有那么多同时 connect 成功。应测量实际部署内核,而不是把一个很小的实验结果推广成通用的加一或减一规则。
队列计数也需要上下文。ListenOverflows 和 ListenDrops 是网络命名空间计数,不会自动解释到某个具体服务。其他监听器也能贡献计数,客户端超时也可能来自 accept 队列之外。将生产失败归因于某个 backlog 设置,或者宣称扩大队列能修复问题之前,应结合队列占用、接受进度和计数共同判断。
快速验证
把脚本保存为 check-listen-backlog.py,准备宿主 Python、Docker 和固定 Python 镜像后运行。真实套接字操作发生在可丢弃 Linux 容器内,网络只保留回环。记录内核为 6.12.76-linuxkit。脚本读取相关设置但不修改它们,并要求 tcp_abort_on_overflow=0,使本次超时观察对应一个明确、可以检查的前提:
import json,subprocess
image='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84'
program=r'''
from contextlib import ExitStack
from pathlib import Path
import json,platform,socket
def counters():
lines=Path('/proc/net/netstat').read_text().splitlines()
for index,line in enumerate(lines):
if line.startswith('TcpExt:'):
return dict(zip(line.split()[1:],map(int,lines[index+1].split()[1:])))
def queued(port):
for line in Path('/proc/net/tcp').read_text().splitlines()[1:]:
fields=line.split()
if fields[3]=='0A' and int(fields[1].split(':')[1],16)==port:
return int(fields[4].split(':')[1],16)
raise AssertionError('listener missing')
with ExitStack() as stack:
def fresh():
s=stack.enter_context(socket.socket());s.settimeout(.25);return s
def listener(backlog):
s=fresh();s.bind(('127.0.0.1',0));s.listen(backlog);return s
somaxconn=int(Path('/proc/sys/net/core/somaxconn').read_text())
abort=int(Path('/proc/sys/net/ipv4/tcp_abort_on_overflow').read_text());assert abort==0
small=listener(1);address=small.getsockname();before=counters();clients=[];timed_out=False
for _ in range(8):
client=fresh()
try:client.connect(address)
except TimeoutError:timed_out=True;client.close();break
clients.append(client)
depth=queued(address[1]);after=counters()
assert timed_out and depth==len(clients) and 1<=depth<8,(depth,len(clients))
changes={name:after[name]-before[name] for name in ['ListenOverflows','ListenDrops']}
assert changes['ListenOverflows']>0 and changes['ListenDrops']>0,changes
accepted=[]
for client in clients:
server,_=small.accept();stack.enter_context(server);accepted.append(server);server.sendall(b'ready')
reader=stack.enter_context(client.makefile('rb'));assert reader.read(5)==b'ready'
assert queued(address[1])==0
for _ in range(4):
client=fresh();client.connect(address);server,_=small.accept();stack.enter_context(server)
server.sendall(b'ready');reader=stack.enter_context(client.makefile('rb'));assert reader.read(5)==b'ready'
accepted.append(server);clients.append(client)
assert len(accepted)>1 and all(s.fileno()>=0 for s in accepted)
larger=listener(16)
for _ in range(6):client=fresh();client.connect(larger.getsockname())
assert queued(larger.getsockname()[1])==6
print(json.dumps({'kernel':platform.release(),'somaxconn':somaxconn,'tcp_abort_on_overflow':abort,
'requested_small_backlog':1,'queued_before_accept':depth,'next_connect_timed_out':timed_out,
'counter_changes':changes,'accepted_connections_still_open':len(accepted),'larger_backlog_queued':6},sort_keys=True))
'''
result=subprocess.run(['docker','run','--rm','-i','--network','none',image,'python','-'],
input=program,capture_output=True,text=True,timeout=30)
assert result.returncode==0,result.stderr
record=json.loads(result.stdout);print(json.dumps(record,sort_keys=True))
第一个监听器请求 backlog 为一,并故意不接受连接。在记录环境里,两次客户端 connect 完成,/proc/net/tcp 也报告两条排队连接;下一次 connect 超过客户端设定的二百五十毫秒超时。脚本记录实际深度,而不是断言每个 Linux 版本在这个参数下都必须接受恰好两条。配置参数、实际队列和客户端结果分别被观察。
在这次受控溢出期间,ListenOverflows 和 ListenDrops 各增加一。由于实验有独立网络命名空间,而且只有这些回环监听器,可以把观察与故意暂停接受的监听器关联。示例没有修改全局内核设置,也没有通过改变共享 Docker 虚拟机网络策略来制造失败;归属依据来自隔离范围和明确的连接操作。
随后,应用接受已排队连接,并通过返回的套接字发送短确认。客户端完整读到确认,监听队列下降为零。再建立四条连接并立即接受,同时保持之前接受的套接字打开;最终有六条已接受连接共存,尽管最初请求的 backlog 只有一。这直接区分待接受容量与应用可以同时持有多少条已连接套接字。
第二个监听器请求 backlog 为十六,在任何 accept 发生前,六个客户端完成连接,队列报告六条待处理连接。这说明更大的队列可以吸收本次应用暂停期间的到达突发,却没有证明处理吞吐提高,也没有证明这些客户端已经收到应用响应,更不能据此认为扩大 backlog 就能解决持续过载。
同时调整接受能力与等待容量
记录到的 somaxconn 为 4096,高于两次请求的 backlog。这只是测试环境观察,不是建议复制到另一台主机的调优值。应一起检查应用实际 listen 参数、accept 循环和内核上限。系统上限很高,也不会自动扩大应用明确请求的小 backlog;只改变一侧而不核对另一侧,可能完全没有达到预期容量。
要区分未完成握手与已经建立、等待接受的连接。SYN 队列控制和 SYN-cookie 行为回答的问题,与这里展示的 accept 队列不同。同样,tcp_abort_on_overflow 改变溢出处理方式,开启重置并不增加服务容量。本实验保持它关闭,展示现有策略下客户端看到的超时,而没有把另一种配置行为写成已经实测。
最后,调查接受速度为何落后。事件循环停顿、文件描述符耗尽、分发瓶颈和故意暂停,对 accept 的影响可能不同于已经接受套接字上的慢工作。更大队列只是增加等待空间,需要符合客户端截止时间和内存预算。如果持续到达速度超过服务能接受并处理的速度,增加等待只会推迟容量失败,不会创造持续处理能力。
延伸阅读
来源
- Linux listen(2),等待 accept 的已建立连接与 somaxconn 上限,已核验 2026-09-12。
- Linux accept(2),移出待接受连接并返回已连接套接字,已核验 2026-09-12。
- Linux IP sysctl documentation,somaxconn、SYN backlog 与 tcp_abort_on_overflow,已核验 2026-09-12。