ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux 调优参数实战:从内存回收到 cgroup 资源隔离的完整指南

Linux 调优参数实战:从内存回收到 cgroup 资源隔离的完整指南 简介《Linux操作系统调优参数.docx》是一份面向Linux系统管理员、运维工程师及网络技术人员的调优参考文档。内容以TCP/IP网络参数为核心覆盖/proc/sys/net目录下的rmem_max、wmem_max、tcp_timestamps、tcp_sack、tcp_window_scaling等关键项逐一说明各自默认行为、调整依据、对吞吐量与延迟的影响并给出针对高带宽、大延迟环境的典型取值同时补充了/fs/super-max、/kernel/acct等文件系统与内核参数帮助读者理解超级块数量、进程记账等底层机制。包内为1个docx文件仅18KB便于随时查阅。文档重点对比了通过/etc/rc.local写入命令和在/etc/sysctl.conf中设置键值对两种持久化方法并附有具体数值示例及修改后的检查思路提醒在实际操作前先做基准测试、之后持续监控系统状态以降低调优风险。目前已获312人学习下载适合具备一定基础、希望系统性提升服务器网络性能与运行稳定性的技术人员作为案头参考。1. Linux 调优参数到底在调什么不换硬件先动内核线上数据库明明还剩一半内存可访问延迟突然开始抖动Nginx 并发配到了 65535压测到 5000 连接就开始拒绝握手日志采集进程跑了一晚上突然报 too many open files。这类问题十次有八次不是代码不行而是 Linux 调优参数没给到位。所谓调优参数指的是一套以 sysctl 为主、覆盖 vm / net / fs 三类内核参数再扩展到 cgroup 资源隔离和进程调度参数的系统级调整方案。它解决的典型问题是慢、抖、连不上、没句柄。适合值班的运维、做容器平台的工程师以及要把服务压到极限的后端开发——你不需要重新编译内核只需要知道改什么、怎么改、改完怎么验证就能让存量机器把性能挤出来。2. 内存和文件系统优先vm.* 与 fs.* 的关键参数怎么选2.1 先搞懂内存回收机制swappiness 和三个脏页参数Linux 内存紧张时内核回收优先动两类页一类是 page cache文件缓存回收成本是后续可能还要从盘上读回来另一类是匿名页也就是进程的 RSS 对应内存回收需要写进 swap。Linux 调优参数里最常被提到、也最容易被误用的就是 vm.swappiness。默认值 60 表示回收时对匿名页的“好感度”调低它系统会更积极地先回收 page cache尽量少碰 swap。注意它不是内存用了 60% 就开始换入换出很多老教程把这一点写错了。对于 Redis、MySQL 这类延迟敏感、且自己管理内存上限的服务我一般把 swappiness 调到 10 或更低对普通 Web 应用保持默认 60 反而是更稳妥的做法因为 page cache 预读对静态文件服务有明显收益。另一个常被忽略的参数是 vfs_cache_pressure默认 100控制的是目录项dentry和 inode 缓存的回收压力数值越大回收越激进。有些机子元数据操作频繁、但文件缓存命中率低可以把它降到 50 试试给元数据缓存多留一点空间。脏页回写三个参数决定“内存里的脏数据攒到多少开始落盘”vm.dirty_background_ratio 默认 10后台回写线程到达该水位就启动vm.dirty_ratio 默认 20进程自己遭遇同步写阻塞的水位。机械硬盘上这两个值不宜调大否则一次掉电或 IO 卡顿会形成很大的回写毛刺SSD 上可以适度放宽比如 background 15、ratio 30让批量化落盘发挥吞吐优势。还有一个容易被漏掉的 vm.overcommit_memory默认 0 是启发式允许 overcommit跑 Redis 这类需要一次性申请大内存的服务时内核在极端内存压力下可能拒绝 mmap常见的做法是显式把它设成 1表示允许超售但前提是你清楚业务内存本身有兜底。参数名默认值常见发行版建议范围适用场景vm.swappiness600-20延迟敏感、60默认数据库、Redis、Web 混合负载vm.vfs_cache_pressure10050-150元数据密集型服务vm.dirty_background_ratio105-15按存储介质区分vm.dirty_ratio2010-40必须高于 backgroundvm.overcommit_memory01Redis 类大内存申请内存超售场景注意swap 本身不是“毒药”对突发内存或超卖场景合理的 swap 是兜底机制。调 swappiness 是降低换入换出概率而不是把 swap 关掉。2.2 fs.file-max 与 inotify文件句柄被吃光的两种路径文件句柄耗尽在故障工单里出现频率极高。系统级上限由 fs.file-max 控制默认值往往在百万级但真正卡住的通常是进程级 ulimit。这里先记住一条结论fs.file-max 管的是“整个系统能打开多少文件”ulimit -n 管的是“单个进程能打开多少文件”两个参数都要查缺一个都会报 too many open files。当前系统打开数可以看 /proc/sys/fs/file-nr第一列是已打开数第二列是空闲数第三列是上限。查进程实际限制用 cat /proc/ /limits 看 Max open files 那一行比照 ulimit -n 更接近真相。另一个隐蔽的坑在 inotify。很多配置热加载、文件监听、日志实时采集组件比如 node 的 chokidar、go 的 fsnotify底层都依赖 inotify它有三个用户态上限max_user_instances、max_user_watches、max_user_queue。生产环境最常见的是 max_user_watches 默认偏小监听几万个文件后 watch 请求失败表现是“配置改了不生效”“日志有新行但采集不到”。之前遇到 ClickHouse 的多目录多分区监听场景默认的 8192 明显不够调到 524288 才正常。这个参数在 /proc/sys/fs/inotify/ 下面和 /proc/sys/fs/file-max 是两个目录排查故障时常被漏掉。调整办法是写在 /etc/sysctl.d/ 里一行一个参数。监控大盘上建议把 file-nr 和 inotify 使用率都加进去配 p99 告警而不是等进程崩了再查 dmesg。这类参数有很强的“新进程才吃新值”特性改完后服务不重启不生效重启服务也是一个单独的上线步骤别漏。2.3 调整完怎么确认三个计数器改参数不是改完就完了得能回答“它到底动没动、起没起作用”。我习惯看三个地方第一/proc/meminfo 里的 SwapCached 和 SwapTotal如果 swappiness 调低后 SwapCached 持续增长说明还是有大块匿名页在换入换出得回头查是不是内存真的不够。第二/proc/vmstat 里的 pswpin、pswpout 两个计数这两列只在发生换页时增长跑一轮业务后对比数值能直接看出 swap 换上换下的量。第三用 sysctl vm.swappiness 读回当前值确认加载确实生效而不是改完配置文件就以为万事大吉。内存类调优有一个特点参数起效慢需要观察一个业务周期。不要改完五分钟就下结论至少跑一个低峰加一个高峰。如果是持续的元数据压力问题结合 /proc/pressure/memory 里的 PSI 指标看比单纯看 free 更直观。PSI 的 some 和 full 两列能告诉你“CPU 等待内存回收的时间占比”调参前后这两个数字的对比比任何理论推导都有说服力。3. 网络层调优连接队列、缓冲区与 TIME_WAIT 的处理3.1 两个队列somaxconn 与 tcp_max_syn_backlogTCP 三次握手时新连接要过两道门。第一道是半连接队列存的是收到 SYN 还没完成握手的连接长度由 net.ipv4.tcp_max_syn_backlog 决定第二道是 accept 队列存的是握手完成、等应用层 accept 的连接上限由 net.core.somaxconn 决定默认只有 128。Nginx 或 Java 服务如果配置了高并发但 accept 队列不跟着调连接一旦过半就会开始丢握手客户端感知就是“连得上但非常慢偶尔直接超时”。这类问题在 ss -ltn 输出里会看到 Recv-Q 列有积压把一个简单的健康检查翻车成连环告警是典型的 linux 系统故障案例。实际调参时tcp_max_syn_backlog 我会给到 2048 或 4096somaxconn 给到 1024 起步。但光改内核参数不够应用层也要同步比如 Nginx 的 listen 指令里要写 backlog1024Go 的 net.Listen 底层默认 backlog 也要看系统值。改完以后用 ss -ltn 查看 Send-Q 列它显示的是当前 listen 队列上限如果还是 128说明应用层没吃到内核参数这时候去查应用配置而不是继续调大内核值。3.2 缓冲区与端口rmem/wmem 和 ip_local_port_range网络延迟的另一个来源是 socket 缓冲区太小。net.core.rmem_max 和 net.core.wmem_max 是 socket 层的接收/发送缓冲区上限net.ipv4.tcp_rmem 和 net.ipv4.tcp_wmem 是 TCP 连接的三个值最小、默认、最大格式是“4096 87380 6291456”这种。对高带宽、高延迟链路把默认值调大能明显减少吞吐波动对毫秒级内网延迟默认值基本够用别盲目调大缓冲区越大会吃掉更多内存特别是在连接数过万的机器上内存浪费是肉眼可见的。客户端短连接特别多的服务还要检查 net.ipv4.ip_local_port_range。默认范围是 32768-60999也就约两万八千个可用端口。如果并发短连接数超过这个数内核想建新连接却没端口可用表现是“连接数到 2 万左右就上不去”。我一般会扩到 1024-65535但这会对系统防火墙的连接跟踪表施加压力改之前先确认 conntrack 容量够。顺带说一句端口范围的修改只对后续新建立的连接生效已经在 TIME_WAIT 里的连接还是要等它自然消退。3.3 sysctl 落地流程与持久化写法sysctl 的完整操作链条是三件事临时改、持久化、验证。临时改用 -w 参数重启失效持久化写在 /etc/sysctl.d/ 下的独立文件里行格式是“参数名 值”加载顺序按文件名排序后面文件覆盖前面文件。推荐用 99-tuning.conf 命名别去动 /etc/sysctl.conf 原文件系统升级时这个文件可能被包管理器覆盖自己的调优配置就丢了。先写一段我常用的落地脚本# 1. 当前值快照改之前一定留底 sysctl -a /var/backups/sysctl.$(date %F).baseline # 2. 把参数写进独立配置文件 cat /etc/sysctl.d/99-linux-tuning.conf EOF net.core.somaxconn 1024 net.ipv4.tcp_max_syn_backlog 4096 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_tw_reuse 1 vm.swappiness 10 EOF # 3. 按文件加载注意 sysctl --system 会按顺序加载所有文件 sysctl --system # 4. 确认关键项已生效 sysctl net.core.somaxconn vm.swappiness这段脚本里我把备份放在第一步是因为 sysctl -a 输出很全回去想比照哪个参数被动过直接 diff 两个基线文件就行。第二步用 heredoc 写配置好处是文件内容一目了然、可审计注意 EOF 用了单引号避免 shell 变量展开。第三步 sysctl --system 会读取 /etc/sysctl.conf 和 /etc/sysctl.d/ 下所有 .conf 文件比老的 sysctl -p /etc/sysctl.conf 覆盖更全。第四步是验证只抽查这次要改的 key别全量打印全量输出既费眼也容易漏。加载报错是最容易翻车的地方配置文件里某个参数名拼错sysctl --system 会打印报错但不会回滚前面已生效的行。我的习惯是改完立刻读一遍关键值确认每个 key 都进了当前内核。另外网卡队列相关参数属于 ethtool 和驱动层不在 sysctl 管别在 sysctl 里找 txqueuelen。3.4 TIME_WAIT 的真相tcp_tw_reuse 与一个已消失的参数TIME_WAIT 多不多用 ss -s 看一眼就行。TIME_WAIT 本身是 TCP 主动关闭方必须经历的状态用来等旧连接的报文在网络上消逝没法完全消灭也没有后悔药。net.ipv4.tcp_tw_reuse1 的含义是允许内核把处于 TIME_WAIT 的出方向连接用于新建连接前提是时间戳选项在双方都开启且递增注意它只对客户端出方向有效对服务端接受新连接没用。很多教程把这两件事混在一起讲导致参数开了以为就万事大吉。另一个老参数 tcp_tw_recycle 已经在 4.12 之后的内核里被移除了老教程抄来的配置写进去只会在 dmesg 里看到 unknown key 报错内核是静默忽略的不会帮你纠正。如果网上资料还让你开 tcp_tw_recycle可以直接略过这不是参数调优是版本幻觉。真正影响 TIME_WAIT 堆积的是 tcp_fin_timeout默认 60 秒能缩到 30以及上文说的 ip_local_port_range 是否够用。把这几项理顺比追着 TIME_WAIT 数字本身去压更有价值。4. 从内核参数落到进程cgroup 与调度参数的落地姿势4.1 先确认 cgroup 版本v1 与 v2 参数写法完全不同内核参数之外Linux 还有一个跟业务直接相关的调优面cgroup。容器平台的 CPU、内存限制最终都落在 cgroup 的控制器参数上。现代发行版RHEL 9、Ubuntu 22.04、Debian 12默认都切到了 cgroup v2和老的 v1 参数名、语义都对不上。动手前先确认版本一行命令stat -fc %T /sys/fs/cgroup # 输出 cgroup2fs 是 v2输出 tmpfs 是 v1v2 里最常用的两个参数是 cpu.weight 和 memory.high。cpu.weight 的取值范围是 1-10000默认 100表示在一个 cgroup 内的 CPU 权重数值越大和其他组竞争 CPU 时分的越多它和 v1 的 cpu.shares1024 基准功能对应但数值体系完全不同拿旧值直接套会差出一个数量级。memory.high 是软限制超过后内核会积极回收该组内存并施加节流但不一定会 OOMmemory.max 是硬上限超过直接 OOM 杀进程。理解这两对参数容器配置就基本能读懂了。4.2 cgroup v2 创建与参数设置一个最小例子在 systemd 之外手动操作 cgroup最常用的是 systemd-run 或直接写 /sys/fs/cgroup。直接写文件的方式适合排查和临时实验一段最小示例# 创建一个名为 demo 的 cgroup 目录 mkdir -p /sys/fs/cgroup/demo # 设置 CPU 权重为 200和默认组竞争时按 2:1 分配 echo 200 /sys/fs/cgroup/demo/cpu.weight # 设置内存软限制 512M超过后回收/限流但不立即杀 echo 536870912 /sys/fs/cgroup/demo/memory.high echo 6442450944 /sys/fs/cgroup/demo/memory.max # 把当前 shell 放进去验证 echo $$ /sys/fs/cgroup/demo/cgroup.procs这里的关键点是 cgroup v2 的接口写法先建目录目录存在即控制组存在写入 cpu.weight 前需要确认 cpu 控制器已启用否则文件不存在。memory.high 和 memory.max 的单位都是字节用 echo 直接写十进制整数也可以用后缀如 512M但依赖内核版本生产环境我习惯直接写字节数避免不同内核的解析差异。最后一个 echo 把当前进程放进组里用来做小规模压测验证压测完记得把进程移回 cgroup.procs 的根组不然压力测试结束 shell 还被关在限制里。注意cgroup v2 里 cpu.weight 的语义是相对权重不是绝对值。只有组间存在 CPU 竞争时它才起作用单核空闲时权重再低也不会限住进程跑满单核这点用来做限流的人经常误解。4.3 进程级调度参数nice、chrt、taskset 与修改进程名如果 cgroup 是一层进程级参数是更细的另一层。nice 值范围 -20 到 19值越小优先级越高普通用户只能往大调root 可以往小调。chrt 设置实时调度策略-f 表示 FIFO-r 表示 RR对需要极低调度延迟的线程比如音频处理、DPDK 轮询线程可以指定。taskset 绑定 CPU 亲和性把两个竞争缓存的服务线程物理隔开或者把中断线程绑到独立核心。这三样我一般这么组合先 taskset 绑定核心再 chrt 提高调度等级最后用 nice 微调普通进程。顺序错了可能白折腾因为绑核后再改亲和性会清除之前的绑定。操作示例# 查看当前进程调度策略与优先级 ps -o pid,comm,nice,rtprio,psr -p 12345 # 把 CPU 0、1 绑定到进程避免缓存颠簸 taskset -pc 0,1 12345 # 提升为 FIFO 实时调度优先级 1仅 root chrt -f -p 1 12345 # 改回普通调度方便对比实验 chrt -o -p 0 12345ps 输出里的 rtprio 和 psr 列分别对应当前实时优先级和所在 CPU 核心改前改后各看一次确认落在预期范围内。实时调度要特别小心FIFO 优先级设成 99 的进程如果进入死循环整个系统都会被它卡死连 ssh 都敲不动所以生产环境我通常只用优先级 1-10绝不往顶配。另外“linux 修改进程名称”这个需求也常出现在这类调优场景里。进程名不止 argv[0] 那一处/proc/self/comm 里的名字在很多监控工具里才是主名修改它需要用 prctl 系统调用C 里一行就能改#include sys/prctl.h #include stdio.h int main(void) { if (prctl(PR_SET_NAME, worker-01) 0) { printf(进程名已改为 worker-01/proc/self/comm 同步生效\n); } return 0; }注意改名只影响 /proc 展示和监控采集不影响 kill 和 PID。Python 等其他语言也有对应库但原理都是调用 prctl理解了这一层换语言只是换 API 的问题。4.4 场景组合NGINX Redis 的一版起步配置参数可以单个解释但上线时永远是一组。拿常见 NGINX Redis 组合举例我的起步模板大致是Redis 所在机器 swappiness 降到 10内存上限交给 Redis 自己的 maxmemoryNGINX 的 somaxconn 和 backlog 同步调大两个服务的文件句柄、连接跟踪都按峰值再加 30% 余量如果同机部署用 cgroup 给 Redis 一个较高 cpu.weight给日志采集一个较低权重避免采集抖动拖累主业务。参数组值理由vm.swappiness10Redis 延迟敏感减少匿名页换出net.core.somaxconn1024配合 Nginx backlog 提高握手容量fs.file-max500000两个服务文件句柄峰值后的余量cpu.weightRedis 组300相对默认组多分 CPUmemory.highRedis 组预留峰值的 1.5 倍防止内存突刺拖垮同机其他服务这套模板不是拿来就抄的。机器内核版本、磁盘类型、网卡队列数都会影响结果我通常把它当初始值跑一轮压测再按压测曲线回调。参数这种东西玄学成分很少大多数翻车都出在“只改了内核没改应用层”把这组对照关系记牢能少踩一半坑。5. Linux 调优参数避坑指南5 个常见翻车现场5.1 swappiness 调 0 后反而更容易 OOM现象Redis 实例内存还剩三四个 G日志里却频繁出现 OOM kill内存监控显示 free 见底查 dmesg 能看到内核在杀 Redis 主进程。原因swappiness 设成 0 后内核回收时完全不想碰 swap只能疯狂回收 page cache。如果 page cache 也被业务持续吃掉剩余内存很快触底最后只能 OOM。调 0 的目的是避免 swap但副作用是失去了“匿名页换出换入”这个缓冲等于把内存逼到了绝路。解决不要设 0设 10 左右给 swap 留一点存在感同时用 Redis 自身的 maxmemory 管住内存让内核和进程两个层级的策略对齐。血泪经验是内核参数不能替代应用层的资源上限两者要一起管才安全。5.2 somaxconn 改了Nginx 的 backlog 没改现象sysctl 里 net.core.somaxconn 已经 1024ss -ltn 看到的 Send-Q 还是 128压测连接数一高就报 connect 超时业务方反馈“服务好像挂了但进程活着”。原因应用层 listen 时传入的 backlog 参数是实际生效值内核参数是上限两者取小。Nginx 需要显式写 listen 80 backlog1024Go 等语言的标准库默认值也偏小不调应用层只调内核等于白改。解决改完内核参数后逐个项目检查监听端口的实际 backlog用 ss -ltn 验证。排查这类连接问题第 6 章的压测对比法最有效率别靠猜。5.3 fs.file-max 没到顶照样 too many open files现象系统 file-nr 还很宽裕应用日志却持续报 too many open files重启后缓解一段时间又复发业务进程看着是活的但文件监听、日志写入全在报错。原因systemd 启动的服务默认 LimitNOFILE 可能是 1024 或 4096根本不走 ulimit 那套。进程级限制没放开系统级参数再大也没用这是一个很隐蔽的 linux 系统管理盲区。解决在 service 单元里写 LimitNOFILE65535然后 daemon-reload 并重启服务排查时先看 /proc/ /limits 确认进程实际限制再顺藤摸瓜改对应配置别一上来就动 fs.file-max。5.4 tcp_tw_reuse 开了TIME_WAIT 数量还是很可观现象服务端大量短连接tcp_tw_reuse 和 tcp_fin_timeout 都调了ss -s 里 TIME_WAIT 还是几万个端口耗尽导致新连接失败。原因tcp_tw_reuse 只对新的出方向连接生效对服务端正在 accept 的连接基本无效。服务端大量 TIME_WAIT 的根源是主动关闭方的连接释放慢端口范围又不够参数用错了方向。解决优先扩 ip_local_port_range把 tcp_fin_timeout 适当降低到 30如果架构允许让客户端先断开或改用长连接从源头减少 TIME_WAIT 的产生。开再多的 reuse 也解决不了“端口本来就不够”的问题。5.5 memory.high 设太低进程被 throttle 成“假死”现象容器进程没被杀但延迟忽高忽低CPU 在用户态运行时间很少监控里 memory 使用不高但 memory.high 频繁告警服务像被“按住”了一样。原因memory.high 是软限制超过后 cgroup v2 会对该组施加回收和节流。把它设得比业务实际需求还低进程一直在被限流表现很像假死CPU 和内存指标都正常但服务就是慢这类问题用 top 看不出来得看 cgroup 事件。解决先看 cgroup 的 memory.events 里 high 事件次数确认是否频繁触发把 memory.high 提到业务峰值的 1.5 倍以上再用压测验证。不要靠拍脑袋设一个“看起来很安全”的小值限流不是 OOM但比 OOM 更难发现。6. 让调优参数可验证压测对比与回归基线改参数最怕的是“感觉变快了”感觉不靠谱要靠同一套压测命令的前后对比。我每次调优前会先建一个基线目录把 sysctl -a、ss -s、free、vmstat 全部导出存成文件然后跑一轮固定并发压测记录结果。改完参数后重复同一命令diff 前后输出用数据说话。一段最朴素的对比流程# 压测前记录基线 uptime free -g ss -s /tmp/perf.before # 跑一轮固定并发压测记录尾部和关键指标 ab -n 200000 -c 1000 http://127.0.0.1:8080/ 21 | tail -8 # 改完参数后把同一条压测命令再跑一遍 uptime free -g ss -s /tmp/perf.after # 对比连接状态的关键行 diff (grep -E TIME_WAIT|ESTAB /tmp/perf.before) \ (grep -E TIME_WAIT|ESTAB /tmp/perf.after)ab 只适合 HTTP 简单场景内网压测工具按需换 wrk、locust 都行关键是“同工具、同并发、同时长、跑三遍取中位数”这样才能把抖动过滤掉。调优参数的回归和代码回归一样应该把基线文件提交进仓库半年后想查哪条参数被谁改过直接 diff 记录。我自己吃过一次亏在 5.15 内核的压测机上把一套参数调得很好看上了生产4.18 内核就翻车后来对比才发现 tcp 的默认算法和 cgroup 控制器行为都不一样。从那以后我的习惯是——参数模板只当起点每台机器合入前先跑同一条压测命令改了就得留痕。这套 Linux 调优参数的套路不算深但值得每一次改动都做对照实验。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进