Linux 内核调优复盘:单机百万连接场景下的协议栈瓶颈击穿记录
Linux 内核调优复盘:单机百万连接场景下的协议栈瓶颈击穿记录
一、40 万连接的瓶颈:不是硬件不够,是内核在“帮忙”丢包
消息推送服务在一次架构升级后面临了一个关键挑战:需要维持 100 万级别的长连接推送能力。初期在 4C16G 的云主机上压测,连接数在到达 38 万左右后,新连接建立开始出现间歇性超时,ss -s显示大量连接处于 SYN_RECV 状态。
这不是硬件瓶颈——CPU 利用率约 60%,内存消耗不到 8GB。问题出在 Linux 内核协议栈的默认配置是为通用场景设计的,面对长连接数量级挑战时需要逐层调整。全连接队列溢出、文件描述符限制、TCP 内存配置不足、端口范围受限等多个因素共同构成了连接数的天花板。
二、文件描述符与连接队列:两个最触手可及的天花板
文件描述符限制是最先被撞到的墙。Linux 默认为单个进程设置 1024 个软限制(soft limit),远不足以承载百万连接。调整需要同时修改系统级和进程级:
# 系统级:/etc/sysctl.conf fs.file-max = 2000000 # 系统全局最大文件描述符数 fs.nr_open = 2000000 # 单个进程可打开的最大文件描述符数 # 进程级:/etc/security/limits.conf # soft 和 hard 都需要设置,且 hard >= soft * soft nofile 2000000 * hard nofile 2000000全连接队列(backlog)的大小决定了在服务端accept()之前能缓冲多少个已握手但未处理的连接。默认值 128 在低并发场景足够,但面对海量长连接时会成为显著的瓶颈点:
# /etc/sysctl.conf # 全连接队列最大长度 —— 影响 accept 之前的连接缓冲能力 net.core.somaxconn = 65535 # 半连接队列(SYN_RECV 状态)最大长度 —— SYN Flood 攻击时可适当降低 net.ipv4.tcp_max_syn_backlog = 8192 # 启用 SYN Cookie,半连接队列满时不丢弃 SYN,而是回复加密 Cookie net.ipv4.tcp_syncookies = 1 # 同时连接数中处于 TIME_WAIT 状态的最大数量 # TIME_WAIT 是四次挥手的正常状态,过多会占用端口资源 net.ipv4.tcp_max_tw_buckets = 2000000应用侧也需要同步调整listen()的 backlog 参数:
// Go 服务端 listen 配置 —— backlog 参数需与 somaxconn 对齐 import "golang.org/x/sys/unix" func listenWithBacklog(addr string, backlog int) (net.Listener, error) { lc := net.ListenConfig{ Control: func(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { // 绕过 Go 默认的 backlog 限制,直接设置 socket 选项 unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) // 启用端口复用,配合多进程负载均衡 }) }, } return lc.Listen(context.Background(), "tcp", addr) }三、TCP 协议栈的深层调优
连接建立之后,TCP 协议栈的配置决定了数据传输的效率:
# ===== TCP 内存配置 ===== # tcp_mem 的三元组:low / pressure / high(单位:页,4KB/页) # low: TCP 栈开始限制内存使用 # pressure: 进入内存压力模式,减少发送窗口 # high: 硬上限,超过后直接丢包 net.ipv4.tcp_mem = 786432 1048576 26777216 # 单个连接接收/发送缓冲区的最小、默认、最大值 net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216 # ===== 连接性能 ===== # 开启 TCP Fast Open —— 在 SYN 包中携带数据,减少一次 RTT net.ipv4.tcp_fastopen = 3 # 3 = 同时启用客户端和服务端 # 启用 TCP 窗口缩放(Window Scaling)—— 支持大于 64KB 的 TCP 窗口 net.ipv4.tcp_window_scaling = 1 # 快速回收 TIME_WAIT 连接(需谨慎,可能与 NAT 环境冲突) net.ipv4.tcp_tw_reuse = 1 # ===== KeepAlive 调优 —— 长连接场景的核心 ===== net.ipv4.tcp_keepalive_time = 600 # 空闲 600 秒后开始探测 net.ipv4.tcp_keepalive_intvl = 30 # 探测间隔 30 秒 net.ipv4.tcp_keepalive_probes = 3 # 3 次探测失败即判定断开四、端口范围与 epoll 事件的收尾工作
长连接场景下,作为客户端发起连接时会消耗本地端口。默认端口范围 32768~60999 约 28000 个端口,建立 100 万连接时频繁被耗尽:
# 扩大临时端口范围 —— 从 28000 扩大到 54000 net.ipv4.ip_local_port_range = 1024 65000epoll 层面,每个事件占用的内存很小,但在百万连接下总量可观:
# 增加 epoll 可监控的最大文件描述符数 fs.epoll.max_user_watches = 2000000最终压测结果(4C16G 云主机):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 最大并发连接 | 38 万 | 110 万 |
| 连接超时率 | 6.2% | 0.01% |
| 新连接建立 P99 | 850ms | 12ms |
| CPU 利用率(稳定) | 60% | 72% |
| 内存消耗 | 7.8 GB | 11.2 GB |
五、总结
Linux 内核参数调优在长连接场景中的优先级排序:
- 先改 ulimit:
nofile限制是所有后续优化的前提,建议设为 200 万; - 再调连接队列:
somaxconn和tcp_max_syn_backlog决定连接建立的成功率,默认值在百万级长连接下严重不足; - TCP 内存和 KeepAlive 是稳定性的保障:
tcp_mem不给够会导致内部丢包,KeepAlive 配置则直接影响连接清理效率; - 端口范围和 epoll 是最后的边际优化:它们在百万级连接下才能体现出价值,但不调会卡在 99% 的最后一公里。
禁用场景:tcp_tw_reuse在 NAT 环境下可能导致连接混乱,建议仅在内网可控环境启用。SYN Cookie 虽然增强了抗攻击能力,但在极端高并发下会增加 CPU 开销(每次 SYN 需要额外计算),对于已验证的内网流量可关闭。