Linux 内核参数调优实战:sysctl 与容量规划

很多线上故障的根因不在应用,而在内核参数的默认值:文件句柄耗尽报 too many open files、连接跟踪表满了新连接随机失败、突发流量下监听队列溢出丢 SYN。默认值是为通用桌面和小型服务准备的,放到高并发服务器上迟早会撞墙。本文不给“万能模板”,而是按业务量估算每一项参数,给出一套可直接落地的 sysctl 基线,并讲清改动如何生效、如何验证、如何回滚。

原则先说清:只调你测到瓶颈或算到上限的参数;一次只改一类;改完留好回滚命令。所有示例基于 Linux 内核 5.x/6.x、systemd 发行版。

一、先看现状:三类满表各有不同的报错

出问题时先对号入座,不要凭感觉调参:

  • 文件句柄耗尽:应用日志出现 Too many open files,或 accept4: too many open files。查 cat /proc/sys/fs/file-nr,第一列是已分配句柄数,第三列是上限。
  • 连接跟踪表满:dmesg 出现 nf_conntrack: table full, dropping packet,表现为新连接随机超时、已建立连接正常。查 cat /proc/sys/net/netfilter/nf_conntrack_count 与 nf_conntrack_max。
  • 监听队列溢出:ss -ltn 的 Send-Q 与 Recv-Q 异常,或 nstat -az TcpExtListenOverflows 持续增长,表现为突发流量下部分连接被重置。
# 三类上限现状一次看完
cat /proc/sys/fs/file-nr
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
ss -s | head -20
nstat -az TcpExtListenOverflows TcpExtListenDrops 2>/dev/null
dmesg -T | grep -i -E 'conntrack|too many open' | tail -20

二、文件句柄:按连接数反推,别只调系统级

句柄分两层,缺一不可:系统级 fs.file-max 是全机上限;进程级 nofile 是单个进程上限,只调系统级、进程还是会被自己的 limit 卡住。估算方法:单进程句柄 ≈ 并发连接数 + 打开的文件数 + 监听 socket,再留 2 倍余量。一个跑 2 万并发连接的 Nginx,nofile 至少给到 65535。

# 系统级:写进 /etc/sysctl.d/99-tuning.conf
fs.file-max = 2097152
fs.nr_open = 1048576

# 进程级:systemd 服务在 unit 文件里加(以 nginx 为例)
# /etc/systemd/system/nginx.service.d/limits.conf
# [Service]
# LimitNOFILE=65535
systemctl daemon-reload
systemctl restart nginx
# 验证:看进程实际 limit,而不是只看配置文件
cat /proc/$(pgrep -o nginx)/limits | grep -i 'open files'

注意:ulimit -n 只影响当前 shell 及其子进程,服务进程的 limit 由 systemd 的 LimitNOFILE 决定,改完必须 daemon-reload + 重启服务才生效。

三、连接跟踪表:按并发连接数估算,顺带管老化时间

每条经过 NAT 或 stateful 防火墙的连接都要占一个 conntrack 表项,表项大小约 300 字节,内存占用 = 表项数 × 300B,100 万表项约 300MB,内存账要先算清。估算公式:nf_conntrack_max ≈ 峰值并发连接数 × 1.5。还要配老化时间:TCP 连接默认 5 天(432000 秒)才回收,短连接多的业务会把表撑满,适当调小 nf_conntrack_tcp_timeout_established 到 3600 秒左右。

# 写进 /etc/sysctl.d/99-tuning.conf
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60
# 只做 NAT/网关的机器才需要这么大的表;纯应用服务器若不用 iptables stateful,
# 可直接关闭连接跟踪(不加载 nf_conntrack 模块),表满问题从根上消失
sysctl -p /etc/sysctl.d/99-tuning.conf
sysctl net.netfilter.nf_conntrack_max

四、网络队列与端口范围:突发流量先看这两项

TIME_WAIT 堆积本身多数时候不是故障,真正要盯的是重传与零窗口。但短连接压测时本地端口会先耗尽:默认端口范围 32768–60999 只有约 2.8 万个,调大范围并开启 TIME_WAIT 复用,能显著抬高单机出向连接上限。监听队列(backlog)则要应用与内核两边一起调:内核的 net.core.somaxconn 只是上限,应用 listen 时传的 backlog 才是实际值,两边取小。

# 写进 /etc/sysctl.d/99-tuning.conf
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_fin_timeout = 30
sysctl -p /etc/sysctl.d/99-tuning.conf
# 验证监听队列是否溢出:数字持续增长说明应用 backlog 或 somaxconn 还不够
nstat -az TcpExtListenOverflows

提醒:tcp_tw_recycle 在 NAT 环境下会丢包,已在新内核中移除,不要再抄进配置;TIME_WAIT 复用只对出向连接生效,入向连接的 TIME_WAIT 靠 tcp_fin_timeout 缩短等待。

五、内存与虚拟内存:给缓存和脏页留边界

数据库与缓存型业务最忌脏页失控:默认 dirty_ratio=20 意味着脏页占到内存 20% 才强制回写,大内存机器上这可能是几十 GB,一次回写就会把磁盘打满、业务抖动。按业务定:写密集的数据库适当调低 dirty_ratio 与 dirty_background_ratio,让回写平滑进行;读密集的缓存服务则可以放宽。

# 写进 /etc/sysctl.d/99-tuning.conf(以写密集数据库为例)
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
vm.swappiness = 10
vm.vfs_cache_pressure = 50
sysctl -p /etc/sysctl.d/99-tuning.conf
# 观察脏页水位与回写情况
grep -E 'Dirty|Writeback' /proc/meminfo
cat /proc/vmstat | grep -E 'nr_dirty|nr_writeback'

六、TCP 拥塞控制与重传参数

高并发短连接场景下,拥塞算法与重传参数的影响不亚于队列长度。Linux 默认的 cubic 在高带宽时延乘积链路上收敛慢,内网或专线环境可以评估切换到 BBR;重传次数与超时则决定了弱网环境下连接是“多等一会儿”还是“直接失败”。这两个参数改动前一定要在压测环境对比吞吐与尾延迟,不要只看平均值。

# 查看当前拥塞算法与可用算法
sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_available_congestion_control
# 内网高带宽链路评估 BBR(需要内核 4.9+,改前先压测对比)
# net.ipv4.tcp_congestion_control = bbr
# 缩短弱网下的连接建立等待,避免请求长时间挂起
# net.ipv4.tcp_syn_retries = 3
# net.ipv4.tcp_retries2 = 8
# 关闭慢启动后的空闲窗口重置,长连接场景更稳
# net.ipv4.tcp_slow_start_after_idle = 0
# 验证:压测时同时看重传率与尾延迟
ss -ti | grep -E 'rto|retrans' | head -20
nstat -az TcpExtTCPRetransFail TcpExtTCPTimeouts 2>/dev/null || cat /proc/net/snmp | grep -i tcp

BBR 不是万能药:它对丢包敏感的公网链路、以及与 cubic 混跑的场景要格外小心,切换后要观察一周的 P99 延迟与重传率,指标恶化就回退。重传参数调得过小会让抖动网络下的正常连接被过早放弃,调得过大则故障恢复变慢,两者都要按业务的超时预算来定。

七、容器里的 sysctl:生效边界要先分清

容器化之后 sysctl 有了两层边界:名字空间隔离的参数(如 net.ipv4.ip_forward 之外的大部分 net.*)可以在 Pod 内单独设置,而节点级参数(如 fs.file-max、vm.max_map_count)属于宿主机,容器里改了也不生效,改错还会报错。排障时先判断参数属于哪一层,再决定是在 Pod spec、kubelet 还是宿主机上改。

# Pod 内可设置的名字空间级参数(securityContext.sysctls)
# apiVersion: v1
# kind: Pod
# spec:
#   securityContext:
#     sysctls:
#     - name: net.core.somaxconn
#       value: "1024"
#     - name: net.ipv4.ip_local_port_range
#       value: "1024 65535"
# 节点级参数要在宿主机上改,容器里改会报 permission denied:
# vm.max_map_count = 262144   # Elasticsearch 类应用常见需求
# fs.file-max = 2097152
# kubelet 的 --allowed-unsafe-sysctls 只放行明确列出的 unsafe 参数,
# 未列出的 unsafe 参数 Pod 会直接创建失败,先看 kubelet 日志确认。

一个常见的坑:应用在容器里读 /proc/sys/net/core/somaxconn 看到的值是宿主机的,改 Pod 的 sysctls 只影响 Pod 自己的名字空间。排查时用 kubectl exec 进容器实测,而不是只看宿主机的配置。节点级参数变更要走宿主机的配置管理,逐台灰度,不要直接改运行中的节点。

八、压测验证:拿数据说话,不要凭感觉

调参的最后一环是验证:同一套压测脚本、同一批指标,改前改后各跑一轮,差距小于 5% 就认为没有效果。只看 CPU 或只看延迟都容易误判,至少要同时看吞吐、P99 延迟、重传率、建连失败数与系统负载,五项里有一项恶化就要复核。

# 改前先留基线快照(压测前跑一遍,改后再跑一遍对比)
ss -s > /tmp/baseline-ss.txt
cat /proc/sys/net/ipv4/tcp_max_syn_backlog /proc/sys/net/core/somaxconn
nstat -az | grep -E 'TcpExtListenOverflows|TcpExtListenDrops|TcpExtTCPSynRetrans'
# 压测示例:wrk 压 Nginx,关注 Requests/sec 与 Latency P99
# wrk -t4 -c500 -d60s http://127.0.0.1/
# 压测中实时看溢出计数是否还在涨(涨说明队列仍不够)
watch -n 2 "nstat -az | grep -E 'ListenOverflows|ListenDrops'"
# 回滚:删掉当次新增的配置行,重新加载并复测
# sed -i '/99-tuning.conf/d' /etc/sysctl.conf  # 按实际文件调整
# sysctl --system && sysctl -p /etc/sysctl.d/99-tuning.conf

验证不过关时的处理顺序:先确认压测本身稳定(换回原参数重跑一遍,看基线是否可复现),再逐项回退本次改动,不要一次性全改回去——那样永远不知道是哪一项起了作用。每次调参只动一类参数、留一份改动记录,是避免“调参玄学”的最有效办法。

九、落地流程:生效、验证与回滚

调参最怕“改了不知道生效没有、出了问题不知道是哪一项”。固定流程:

  • 改前留底:sysctl -a > /tmp/sysctl.before.$(date +%F),出问题逐项比对。
  • 分文件管理:不要直接改 /etc/sysctl.conf,用 /etc/sysctl.d/99-tuning.conf,每类参数加注释写明原因与估算依据。
  • 先临时后持久:先用 sysctl -w key=value 临时生效观察,确认无异常再写入配置文件,避免一次写错导致重启后起不来。
  • 回滚:删掉对应配置行后 sysctl -p,或直接用留底文件恢复单项:sysctl -w key=旧值。
  • 灰度:先在一台机器上改,观察一轮业务高峰,再批量推;批量推用 Ansible 的 sysctl 模块,保证幂等。
<pre class="wp-block-syntaxhighlighter-code"># 完整基线示例(按需取用,不要全盘照抄)
cat > /etc/sysctl.d/99-tuning.conf <<'EOF'
fs.file-max = 2097152
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.netfilter.nf_conntrack_max = 1048576
vm.swappiness = 10
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
EOF
sysctl -p /etc/sysctl.d/99-tuning.conf
sysctl -a | grep -E 'file-max|somaxconn|conntrack_max|swappiness'</pre>

最后记住一句话:内核参数调优的终点不是数值本身,而是你能说清“这个值是按什么业务量算出来的、满了会报什么错、怎么验证”。把估算过程写进配置注释里,下一个接手的人才不会把它当成玄学模板删掉或照抄。

发表回复

后才能评论