大带宽服务器内核优化:TCP内存压力模型深度拆解与参数调优
前言:被忽略的内存暗雷
当大带宽服务器跑满万兆甚至更高吞吐时,CPU 占用不高、中断也不多,但吞吐突然跳水,或者出现随机丢包。这时候很多人去调 net.core.rmem_max 和 wmem_max,却发现治标不治本。问题的根源常常藏在 Linux 内核的 TCP 内存压力管理里——一个多数教程只教“调大数值”,却没讲清楚内核如何判断“压力”并截断你连接的机制。
一、TCP 内存的三层防线
内核通过 net.ipv4.tcp_mem 控制 TCP 全局内存使用的三级水位线,格式为三个十进制数:low pressure high(单位:页,通常 4KB)。默认值因系统内存而异,例如 8GB 内存的机器约为 188736 251648 377472。
# 查看当前值
sysctl net.ipv4.tcp_mem
- low:低于此值,内核不施加任何压力。
- pressure:超过此值,内核进入“内存压力模式”,开始减少发送/接收缓冲区的自动增长,并降低缓存膨胀倾向。
- high:超过此值,内核直接对调用者施加背压——新连接分配 sk_buff 可能失败,已有连接被强制收缩缓冲区,甚至触发 OOM。
很多人以为 tcp_mem 只是上限,实际上它是一套完整的压力反馈闭环。内核在 tcp_sendmsg 和 tcp_recvmsg 的路径中都会检查 sk->sk_forward_alloc 与系统全局 tcp_memory_allocated 的比值,并在 sk_stream_alloc_skb 时调用 tcp_check_oom 判断是否拒绝分配。
二、内核压力模型的源码级拆解
关键函数 sk_under_memory_pressure() 读取 tcp_memory_pressure 全局变量。它怎么被设置?在 tcp_should_expand_skb 和 tcp_sendmsg 中,当 tcp_memory_allocated > sk_mem_pages(tcp_mem[TCP_MEM_PRESSURE]) 时,内核执行 tcp_enter_memory_pressure(),会将 tcp_memory_pressure 置 1,并启动一个 timer,只有持续低于 low 一段时间才能退出。
// 简化自 linux/net/ipv4/tcp_input.c
void tcp_enter_memory_pressure(struct sock *sk) {
if (!tcp_memory_pressure) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPMEMPRESSURETIME);
tcp_memory_pressure = 1;
}
}
在压力模式下,tcp_sendmsg 中的 tcp_push 会降低聚合阈值,不再尝试合并小包;tcp_moderate_sndbuf 会主动缩小发送缓冲区;接收端的 tcp_rcv_space_adjust 也会保守增长。这直接导致:吞吐量下降,延迟增加,甚至出现奇怪的半连接状态。
我在调优一台轻云互联的大带宽实例(10Gbps)时,发现默认 tcp_mem 的 pressure 值大约是系统总内存的 1/3,而这类服务器典型配置是 64GB 内存。当并发 2000 个长连接,每个连接 buffer 积累到几 MB,全局 TCP 内存轻松突破压力线,然后吞吐从 9.8Gbps 掉到 3.2Gbps,排查半天才发现是内核自宫。
三、调优策略:主动“脱压”
针对大带宽服务器,基本原则是确保全局 TCP 内存始终高于 pressure 线至少 30% 留白,同时避免突破 high 线。计算公式:
# 以 64GB 内存,目标预留 25% 给 TCP 为例
memory_mb=65536
reserve_ratio=0.75 # 留 25% 给其他进程
tcp_mem_pages=$((memory_mb * 1024 * reserve_ratio / 4)) # 4KB/页
low=$((tcp_mem_pages * 75 / 100)) # 75% 触发压力
pressure=$((tcp_mem_pages * 90 / 100)) # 90% 重压
high=$((tcp_mem_pages * 95 / 100)) # 95% 极限
sysctl -w net.ipv4.tcp_mem="$low $pressure $high"
同时调整单个连接的缓冲区上限(建议保持默认,让内核自适应):
sysctl -w net.core.rmem_max=67108864 # 64MB
sysctl -w net.core.wmem_max=67108864
sysctl -w net.ipv4.tcp_rmem="4096 131072 67108864"
sysctl -w net.ipv4.tcp_wmem="4096 65536 67108864"
不要盲目把 tcp_mem 设得极大,否则 tcp_should_expand_skb 会允许每个连接疯狂占据内存,最终被 cgroup 或 OOM killer 干掉。用以下命令监控实时压力状态:
# 查看当前 TCP 内存占用
grep TCP /proc/net/sockstat
# 输出类似:TCP: inuse 2459 orphan 0 tw 0 alloc 7834 mem 190485
# mem 单位是页,190485页 ≈ 744MB
# 查看是否处于压力模式
cat /proc/net/netstat | grep TCPMemoryPressures
# 如果计数不断增长,说明压力频繁
四、进阶:结合 NUMA 与 percpu 计数器
内核使用 tcp_memory_allocated 这个 percpu 计数器来跟踪全局内存,在 NUMA 环境下,跨 Node 的内存分配会引发额外开销。对于大带宽服务器,建议开启 numa_balancing 或者手动绑定网卡中断到特定 Node:
# 绑定 eth0 中断到 CPU0-3(假设 Node0)
for irq in $(cat /proc/interrupts | grep eth0 | awk '{print $1}' | sed 's/://'); do
echo 0f > /proc/irq/$irq/smp_affinity
done
配合 tcp_mem 调优后,我在同一台轻云互联 10Gbps 机器上,并发 5000 连接,稳定跑满 9.5Gbps,不再出现异常掉速。
五、排错案例:一次虚假 OOM 的真相
某客户反馈大带宽服务器报警 Out of memory: Kill process,但实际物理内存还剩 30%。查看 slabtop 发现 skbuff_head_cache 占用异常,深入检查发现 tcp_mem 的 high 设得太低(8GB 机器默认 377472 页≈1.5GB),而应用跑了一堆长连接缓存,导致内核判断全局 TCP 内存突破 high,直接调用 out_of_memory。解决方案:按上述公式重新计算 tcp_mem,并将 vm.overcommit_memory 从 0 改为 2(严格模式),避免内存过量。
# 永久保存
cat >> /etc/sysctl.d/99-tcp-mem.conf << EOF
net.ipv4.tcp_mem = 196608 262144 327680
net.ipv4.tcp_rmem = 4096 131072 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
EOF
sysctl --system
总结
理解 TCP 内存压力模型,远比单纯放大缓冲区重要。内核的自动调节机制在大带宽场景下经常帮倒忙——它以为自己在保护系统,实则扼杀了吞吐。通过精确设定 tcp_mem 的三级水位,结合监控手段,才能真正榨干万兆网卡的每一滴性能。