①服务端其实有两个队列
这是很多人的知识盲区:服务端在
listen() 后,内核会维护两个队列来承接还没被应用程序接走的连接——半连接队列(SYN Queue)和全连接队列(Accept Queue)。
半连接队列(SYN Queue)
存放只完成了前两次握手的连接(收到客户端 SYN,已回 SYN+ACK,在等最后一个 ACK)。此时连接处于 SYN_RECV 状态,还没完全建立。
全连接队列(Accept Queue)
存放三次握手已完成、但应用程序还没调用 accept() 取走的连接。此时连接处于 ESTABLISHED,但还没交给用户态。
②一次握手在队列里怎么流转
| 步骤 | 客户端动作 | 服务端动作 | 服务端连接状态 |
|---|---|---|---|
| 1 | 发 SYN | 收到后创建连接请求块,放入半连接队列,回 SYN+ACK | SYN_RECV |
| 2 | 收到后发 ACK | 收到 ACK,从半连接队列移出,放入全连接队列 | ESTABLISHED(但未交付应用) |
| 3 | 可以开始发数据 | 应用调用 accept() 从全连接队列取出 | ESTABLISHED(已交付) |
关键点:三次握手的最后一次 ACK 到达时,连接在服务端就已经是 ESTABLISHED 了,不需要等应用 accept()。所以即使应用暂时不 accept,客户端的 connect() 也能成功返回,数据也能被内核接收(暂存于接收缓冲区)。
③backlog 到底控制的是哪个队列
这是最容易答错的细节。listen(fd, backlog) 中的 backlog 参数,在不同系统/版本里含义有变化:
| 系统 / 版本 | backlog 的含义 |
|---|---|
| 早期 Linux(2.2 之前) | 半连接队列大小(SYN Queue) |
| 现代 Linux(2.2+) | 全连接队列大小(Accept Queue) |
| 半连接队列 | 由 net.ipv4.tcp_max_syn_backlog 控制(另受 somaxconn 影响) |
现代 Linux 上的准确表述:全连接队列长度 =
min(应用传入的 backlog, net.core.somaxconn)。所以只调大程序里的 backlog 还不够,必须同时调大 somaxconn(默认通常是 128 或 4096,取决于发行版)。# 查看与调整全连接队列上限
sysctl net.core.somaxconn
sudo sysctl -w net.core.somaxconn=65535
# 查看与调整半连接队列上限
sysctl net.ipv4.tcp_max_syn_backlog
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535
# 是否启用 SYN Cookie(1 = 启用)
sysctl net.ipv4.tcp_syncookies
④SYN Flood:打的就是半连接队列
SYN Flood 是一种典型的 DDoS 攻击:攻击者只发 SYN、不发最后的 ACK(或者用伪造的源 IP 发 SYN,导致 SYN+ACK 永远送不到)。服务端的半连接队列被这些“半吊子连接”占满,正常用户的 SYN 就进不来了,服务不可用。
⑥队列溢出的现象与排查
全连接队列溢出时,内核的行为由 net.ipv4.tcp_abort_on_overflow 决定:
| 参数值 | 内核行为 | 客户端感受 |
|---|---|---|
| 0(默认) | 丢弃 ACK,稍后重传 SYN+ACK | 连接变慢、偶发超时重试 |
| 1 | 直接回 RST | 连接被拒绝(connection reset) |
# 查看全连接队列溢出次数(持续增长的 ListenOverflows 是危险信号)
netstat -s | grep -i "listen"
# times the listen queue of a socket overflowed
# SYNs to LISTEN sockets dropped
# 查看当前监听套接字的队列使用情况
ss -lnt
# State Recv-Q Send-Q Local Address:Port
# LISTEN 0 128 *:80
# ↑当前全连接队列已用 ↑最大 backlog
# 统计处于 SYN_RECV 的连接数(半连接队列压力)
ss -tan state syn-recv | wc -l
排查思路:若
ListenOverflows 持续增长 → 全连接队列太小或应用 accept 太慢(被业务逻辑阻塞)。此时不要只调大队列,先确认应用是否有慢处理、线程池是否打满——队列调大只是把问题往后推。⑦面试高频追问
Q1:服务端有几个队列?分别存什么状态?
两个:半连接队列(SYN Queue)存 SYN_RECV(收到 SYN 已回 SYN+ACK,等最后 ACK);全连接队列(Accept Queue)存已完成三次握手、等应用 accept() 的 ESTABLISHED 连接。Q2:listen 的 backlog 控制哪个队列?
现代 Linux 控制全连接队列,实际长度取 min(backlog, net.core.somaxconn)。半连接队列由 tcp_max_syn_backlog 控制。(早期 Linux 2.2 之前 backlog 是管半连接队列的,这是常见混淆点。)Q3:SYN Flood 的原理是什么?
攻击者只发 SYN 不回 ACK(常配合伪造源 IP),让服务端半连接队列被大量 SYN_RECV 占满,正常用户的 SYN 无法入队,导致服务不可用。属于资源耗尽型 DDoS。Q4:SYN Cookie 是怎么防御的?
收到 SYN 时不分配任何资源,把连接信息哈希后编码进 SYN+ACK 的初始序列号;收到 ACK 时反解校验,合法才建连接。这样半连接队列不再成为瓶颈。代价是会损失部分 TCP 选项信息。Q5:三次握手完成后,应用还没 accept,连接算建立了吗?
算。最后一个 ACK 到达服务端时,连接在内核中已是 ESTABLISHED,客户端的 connect() 会成功返回,数据也能进接收缓冲区。accept() 只是把连接从队列取到用户态。Q6:线上发现 ListenOverflows 持续增长,怎么排查?
先看 ss -lnt 确认队列水位;再看应用是否 accept 慢(线程/协程打满、业务逻辑阻塞)。优先优化应用消费速度,其次才调大 somaxconn 与 backlog——单纯调大队列只是延迟问题爆发。Q7:为什么 SYN Cookie 默认不一直开着?
因为它会丢失 TCP 选项协商信息(SACK、Window Scale 等可能无法生效),且有哈希计算开销。所以它作为队列满时的兜底防御,正常路径仍走标准队列流程。✓一句话速记
两个队列:半连接(SYN_RECV,握手没走完)+ 全连接(ESTABLISHED,等 accept);现代 Linux 的 backlog 管全连接队列且受 somaxconn 截断;SYN Flood 打的是半连接队列,靠 SYN Cookie「不分配资源 + 序列号编码校验」来防。