listen backlog 保存等待 accept 的连接
对于 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 版本在这个参数下都必须接受恰好两条。配置参数、实际队列和客户端结果分别被观察。 ...