
如果有人问我 Linux 性能排查里最容易被低估的机制是什么我的答案会是进程优先级。之前我在一台编译服务器上遇到一个怪现象线上服务在固定时间点延迟飙升CPU、内存、IO 全部查过都正常后来才发现罪魁祸首是一个没有设置好优先级的日志备份进程。它一启动就大量抢占 CPU 时间把业务进程挤到了后面。找到原因之后我用renice把备份任务降级负载立刻恢复了平静。从那以后我把优先级这一块做了系统整理这篇就是当时的学习笔记。这份笔记适合所有刚开始接触 Linux 系统运维、或者已经在用 top 但不太理解 NI 列含义的读者。我会先讲清楚优先级到底是什么再给出一套可直接操作的命令方法接着深入内核看调度器怎么处理这些数字最后分享几个真实踩坑现场。内容不涉及高深的内核代码分析更偏向于理解机制、熟练使用、遇到问题能排查。1. 先搞清楚进程优先级到底是怎么回事1.1 Linux 里其实有两套优先级体系很多人第一次接触优先级是从ps -l输出里的NI列开始的比如看到 NI 是 0下意识觉得这代表没有优先级。实际上 Linux 的优先级不是一个简单数字而是两套体系叠加在一起。第一套是普通进程的 nice 值范围是 -20 到 19默认 0。数值越小优先级越高数值越大优先级越低。之所以叫 nice可以理解为这个进程有多友好nice 值越高越愿意把 CPU 让给别人所以是更 nice。第二套是实时进程的优先级范围是 1 到 99。实时进程在设计上就是为了处理那些不能被延迟的关键任务比如音视频采集、工业控制它们拥有对 CPU 的抢占特权。举个生活化的例子普通进程就像大家在食堂排队打饭nice 值决定你愿不愿意往后让位实时进程则是有专属窗口的 VIP 卡用户只要他到场普通队列的人就得等着。这里有个最容易搞反的点——数值越小优先级越高和大多数人分数越高越好的直觉正好相反。另外一个进程必须属于某种调度策略。调度策略才是优先级的上层框架常见的有SCHED_OTHER、SCHED_BATCH、SCHED_IDLE、SCHED_FIFO、SCHED_RR。前三种对应用户日常启动的大部分进程后两种用于实时任务。你可以用chrt -p pid查看进程当前的调度策略输出会明确显示是哪一种。1.2 从创建到运行优先级是怎么决定谁先用 CPU的当一个进程被创建时子进程会继承父进程的优先级参数。也就是说如果你在 shell 里启动任务shell 的 nice 值就是父进程的 nice 值默认是 0。如果一个脚本内部又拉起了多个子进程这些子进程会沿用同样的 nice 值。但是继承不等于永远固定。进程创建后仍然可以通过renice修改 nice 值也可以通过chrt改变调度策略和实时优先级。运行中的进程和未运行的进程在优先级上最大的区别是未运行的进程在 fork 之后、初次被调度之前就固定了静态优先级运行中的进程则随时可以动态调整。调度器的选择过程可以简化为三步先从所有可运行的任务里找到属于实时类的任务根据实时优先级按序执行实时任务全部结束后才轮到普通进程普通进程之间根据 nice 值换算出的权重来分配 CPU 时间。这套先后关系是理解后面所有命令和问题的基础。1.3 优先级不是越快越好而是合理的资源分配我需要特别强调一点把进程优先级调到最高不代表它就一定跑得更快。因为付出 CPU 时间之后进程能否快起来受制于锁、IO、网络等资源。比如一个程序在等待数据库响应就算给它 99 的实时优先级它也只能干等。优先级的本质是资源调度策略当 CPU 不够用时决定谁先获得时间片、谁获得更多时间片。CPU 充足时即便是 nice 值为 19 的进程也能拿到完整的时间片跑完任务。我之前做过一个简单测试两个循环计算程序一个 nice 是 -5一个 nice 是 5在单核机器上跑同样的任务量差距确实明显但一旦空闲核数超过任务数两者几乎同时完成。所以调优优先级前先确认 CPU 是否真的是瓶颈否则就是瞎忙。2. 命令行里操作优先级的完整笔记2.1 启动进程时指定 nice 值如果你在运行一个耗时任务并且担心它影响业务最好在启动阶段就设置好 nice 值语法很简单nice -n 10 ./backup_script.sh-n后面跟的数值就是期望的新 nice 值。也可以直接写nice -10这种写法容易和负数混淆建议还是用-n参数更加清晰。如果脚本里需要指定 CPU 亲和性可以和taskset结合使用taskset -c 2,3 nice -n 10 ./backup_script.sh这条命令把进程绑定到 CPU 2 和 3 上同时设置 nice 值为 10。对于需要长时间占用 CPU 的批处理任务这样组合使用比单设 nice 更有效既能降低抢占又避免进程在不同核心间迁移带来的缓存损耗。启动时设置 nice 的好处是进程从一开始就用降级参数运行不会出现先抢占了临界区再降级这类尴尬情况。但有一个限制普通用户只能调高 nice 值也就是往正数方向调不能往负数方向调。比如你执行nice -n -5 ./app系统会提示权限不足。这是内核层面的安全设计防止普通用户通过调低优先级来恶意抢占系统资源。2.2 进程启动后修改与查询优先级进程已经在跑了这时候需要用到renice。比如发现一个 PID 为 1234 的数据库备份进程占了很多 CPU你想把它降下来renice -n 10 -p 1234执行完显示1234 (process ID) old priority 0, new priority 10说明旧的 nice 值为 0新的 nice 值为 10。如果你要同时调整多个进程可以用-p跟上多个 PID或者用-g指定进程组、-u指定用户renice -n 5 -u www-data这里我认为更实用的做法是先通过pgrep找到符合条件的进程确认 PID 之后再做调整避免误伤同类进程。比如批量调整某个脚本相关进程pgrep -f backup_task | xargs renice -n 10 -ppgrep -f会匹配完整命令行比直接按进程名匹配更准确特别是在有多个同名 Python 脚本但参数不同的时候。查看优先级的入口有三个我个人最常用的是ps和top。ps -el能看到系统的完整进程表其中NI列就是 nice 值F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1000 2345 2311 0 80 0 - 1024 pipe_w pts/0 00:00:00 bashPRI列是内核视角的优先级对于普通进程PRI实际等于nice 120比如 nice 为 10 时 PRI 显示 130。top交互界面按r可以交互式修改进程 nice 值但在自动化脚本或排查现场我更推荐直接用ps -o pid,ni,comm精确取值ps -eo pid,ni,comm | grep backup还有一个容易忽略的入口是/proc/pid/stat里面的prio字段是动态优先级nice字段是静态优先级。某些监控脚本会直接解析这个文件但在日常命令行操作时ps -el的输出已经足够。2.3 systemd 服务如何设置优先级现在的 Linux 发行版大量使用 systemd 管理服务如果你写了一个自定义服务希望在启动时固定 nice 值可以不在 ExecStart 里写nice而是直接在 service 配置里声明[Service] ExecStart/opt/scripts/worker.py Nice10对于需要实时调度的服务还可以加CPUSchedulingPolicyrr CPUSchedulingPriority40CPUSchedulingPolicy支持other、batch、idle、fifo、rr几种值。设置完成后执行systemctl daemon-reload再systemctl restart对应服务。这个配置对于部署在容器或者虚拟机上跑定时任务的场景尤其有用不需要在业务列表里隐藏一行nice命令运维人员一眼就能看出服务的 CPU 策略是什么。3. 内核里的优先级运行机制3.1 nice 值到权重的映射表理解机制之前先看一张内核的映射关系表。Linux 的 CFS 调度器不是直接拿 nice 值做算术而是通过一个权重表来换算。内核源码sched/core.c中定义了sched_prio_to_weight数组每个 nice 值对应一个权重数下面是摘取的几个代表值nice 值内核权重相对 nice 0 的份额比例-2088761约 15.38 倍-107721约 1.25 倍-53355约 0.77 倍010241 倍5335约 0.31 倍10110约 0.10 倍1915约 0.014 倍从这个表能看出来每下降一个 nice 值权重近似增加 1.25 倍所以 nice 值每差一级在同样负载下 CPU 时间大概差 10% 左右。最极端的 -20 和 19 之间权重差了上万倍。这意味着如果两个计算密集型进程竞争一个 CPU一个设为 -20一个设为 19后者几乎得不到运行机会。有了这张权重表你就明白为什么nice 19的进程不一定会完全卡死它只是分到的 CPU 时间极少。在 CPU 空闲的机器上权重为 15 也能完整执行完一个任务只是运行速度会比较慢。3.2 CFS 调度器怎么使用这个权重Linux 默认的普通进程调度器是 CFS完全公平调度器。它的设计哲学不是给每个进程固定时间片而是维护一个虚拟运行时间vruntime。进程运行时会累积 vruntime增长的速度由权重决定权重越高vruntime 增长越慢。每次需要选择下一个运行的任务时CFS 就挑 vruntime 最小的进程。这样从数学上看权重高的进程虽然在相同时间片里 vruntime 增长慢但在未来很长一段时间里会获得更多实际运行时间从而实现了按权重比例分配 CPU的效果。这也解释了为什么 nice 值不是排队插队而是分蛋糕的比例。内核内部把 nice 值映射成static_prio普通进程的内部优先级范围是 100 到 139对应 nice 值 -20 到 19。一百多的数字和实时优先级 1 到 99 统一在一个坐标系里调度器比较时就能一眼看出实时任务优先于普通任务。实时调度类则完全不同。SCHED_FIFO是先进先出只要实时进程可以运行它就持续占用 CPU普通进程只能在它主动让出 CPU 或者被更高优先级的实时任务抢占时才有机会。SCHED_RR在 FIFO 基础上增加了时间片轮转允许同优先级的多个实时任务轮流执行但依然压制普通任务。3.3 实时优先级为什么这么霸道实时优先级是 1 到 99数值越大优先级越高这和 nice 值是反着的。这个设定经常把人绕晕但内核的实时调度器就是这么比较的。默认情况下如果某个 CPU 上有实时任务在运行普通任务完全没机会。为了不让实时任务把系统饿死内核提供了两个参数来限制实时任务的总占用率/proc/sys/kernel/sched_rt_period_us /proc/sys/kernel/sched_rt_runtime_us默认周期是 1 秒实时任务的运行时间上限是 0.95 秒也就是说实时任务最多占用 95% 的 CPU 时间剩下 5% 留给普通任务。这 5% 看起来不多但足够让系统管理命令和 SSHD 之类的基础服务喘口气避免出现机器彻底失联的极端情况。但要注意这个限制是一个核一个核单独计算的。如果你有 8 个核8 个实时任务把每个核都吃满 95%普通任务的调度会被严重挤压交互式操作会卡到令人崩溃。所以我一直强调普通业务场景下完全没有必要使用实时调度策略大多数时候设置一个合适的 nice 值就够了。4. 实战常踩的坑与排查实录4.1 普通用户为什么调不了负 nice 值这个坑在我刚开始做运维时踩过。当时我想把一个数据处理脚本的优先级调高一点执行命令renice -n -5 -p 1234系统直接返回Permission denied。一开始我以为是 PID 输错了后来查资料才发现这是内核有意为之如果任何用户都能把进程优先级调到最高那么恶意脚本完全可以把系统资源占满其他进程什么都干不了。所以 Linux 规定只有 root 用户才能把 nice 值往负数方向调整普通用户只能往正数方向调整。这个设计其实很符合直觉——调低优先级是利他允许任何人做调高优先级是利己必须要有权限。如果你确实需要把某个普通用户的任务调成高优先级两个办法让管理员执行或者配置 sudoers 白名单。但不建议随便给普通用户 root 权限哪怕只是给个别命令放开也要评估风险。4.2 把进程改成实时优先级后系统卡死的翻车现场我想重点说说实时优先级的实验风险。之前在一台多核测试机上我为了验证一个音频采集程序的低延迟表现直接把它的调度策略改成了SCHED_FIFO优先级设成 99chrt -f 99 -p pid结果程序本身是一个计算密集型的循环它一跑起来所在 CPU 核心被完全占满。虽然 RT throttling 会留下 5% 的时间给普通任务但这些时间分到每个普通进程头上寥寥无几我的 SSH 会话变得极度卡顿几乎敲一个字符要等好几秒。最后只能通过另一台机器强制 kill 进程才恢复。这个案例给我的教训是实时优先级 99 只适合经过严格测试、确定不会长期占满 CPU 的短任务。普通的 web 服务、数据库、批处理脚本都是绝对不碰它的。如果只是想降低延迟先用 nice 值 -10 或 -5 测试配合 CPU 绑核观察不要一上来就上chrt。4.3 autogroup 机制会悄悄干扰你的 nice 调整这是最容易让人费解的问题。某些机器上你执行了renice -n 10 -p pid用ps查看也就绪了但进程的实际 CPU 占用率没有下降。原因很可能是autogroup自动进程组在起作用。现代内核默认开启 autogroup把同一个会话下的进程自动归于一个组调度器先按组分配 CPU 时间再在组内按各进程的 nice 值分配。当你在一个终端里启动多个任务时它们属于同一个 autogroup整体共享一个 nice 值。你单独修改组内某个进程的 nice 值影响的是它在该组内部时间里的份额不会改变整个组的核外份额所以从外部看起来效果不明显。解决办法是直接调整 autogroup 的 nice 值。autogroup 的目录挂在/sys/kernel/debug/sched/autogroup/下里面每个会话对应一个目录echo 10 /sys/kernel/debug/sched/autogroup/autogroup-100/nice或者更粗暴地关闭 autogroup在启动参数里加上noautogroup。我个人不建议关闭因为 autogroup 对不同终端会话做了很好的隔离。调试时先确认是不是 autogroup 影响了效果再决定是调整组 nice 还是关闭它。4.4 排查优先级问题的三个实用场景第一名场景是CPU 使用率暴涨不知道是谁干的。我一般用top按 CPU 排序立刻看有几个进程在抢核心再查看它们的 NI 列。如果异常进程的 NI 是 0 或负数说明它没有任何退让而这种任务往往是备份、日志采集、编译任务。解决办法不一定是杀掉直接renice -n 10降权就能缓解。第二名场景是多个批处理任务互相抢 CPU 导致全部变慢。这种情况可以给不同任务设置不同 nice 值比如前台业务进程保持默认 0后台编译任务设 5日志压缩设 10离线分析设 15形成一套阶梯式的优先级方案。从长期运行看这种部署比所有任务都抢默认值要稳定得多。第三名场景是 Docker 容器内设置优先级没效果 的疑问。容器内的进程运行在宿主机的内核上PID namespace 隔离了进程视图但 nice 值调整机制本身还是生效的。不过容器中往往同时存在 cgroup 的 CPU 权重控制比如cpu.weight和cpu.cfs_quota_us这些是更粗粒度的资源约束。排查时先看容器所在的 cgroup 配置如果容器整体 CPU 份额被限制了单独调进程 nice 值当然用处不大。优先级和 cgroup 的关系可以理解为cgroup 先划分了一块 CPU 蛋糕给容器nice 值决定容器内进程怎么分这块蛋糕。4.5 优先级相关的故障速查表我把自己遇到过的现象整理成了下面这个表排查的时候对照着看很快现象可能原因排查命令推荐处理业务延迟高CPU 有进程占了 100%异常进程优先级过高top -c查看 PID 与 NIrenice -n 10 -p pidrenice 后无效果autogroup 或 cgroup 限制检查 autogroup 目录与 cgroup调整 autogroup nice 或容器权重普通用户无法调低 nice 值内核权限限制确认执行用户用 root 或 sudoers 授权系统卡死SSH 无法响应实时优先级被设置后台或串口登录排查限制 RT 运行时间或重启服务重启后优先级丢失未在 systemd 里配置systemctl cat查看单元添加 Nice 或 CPUSchedulingPolicy多线程进程调优先级只影响主线程线程组优先级与进程不同步ps -eLf查看 LWP根据场景对线程单独设置这张表不是教科书条文而是我实际排查中总结出的高频情况。如果你遇到类似现象先对照排查命令确认再动手修改不要一上来就大改配置。这里再补充一个我常用的观察手段。临时想观察优先级分配可以起一个循环任务对比不同 nice 值for i in 1 2 3; do nice -n 0 -- bash -c while :; do :; done done for i in 1 2 3; do nice -n 5 -- bash -c while :; do :; done done然后把进程数、nice 值、CPU 占有率放到同一张监控图里观察时间片分布。这个方法特别适合培训新人理解 nice 值的实际效果比单纯看手册直接得多。做这个实验时记得及时 kill 掉这些忙循环别把测试机跑满。实际操作中可以先记录测试前的系统平均负载再用pidstat统计每个进程的 CPU 占用这样能更明显地看出权重比例。5. 从优先级理解 Linux 的资源管理哲学学习优先级的过程中我越来越强烈地感觉到 Linux 的调度设计并不追求让单个进程跑到最快而是追求整体系统响应可控。nice 值、实时策略、cgroup、autogroup 各管一段共同组成了一套非常灵活的资源管控体系。这也是为什么 Linux 可以在同一台机器上同时跑数据库、Web 服务和各种批处理任务而不互相拖垮。如果你刚接触这部分内容我建议从ps -el的 NI 列开始逐步做三个实验第一个启动一个忙循环观察它的 CPU 占用第二个用renice把它的 nice 值调到最高再看 CPU 占用变化第三个如果条件允许在一台空闲机器上小心测试chrt -f 99的后果。做完这三个实验你对优先级的理解会比读十篇文章都扎实。最后分享一个小技巧遇到可疑的高 CPU 进程先用ps -o pid,ppid,ni,comm,cmd -p pid一次性把进程信息和启动命令看全再决定是否调整优先级。很多时候看似是优先级问题实际是脚本逻辑问题优先级只是背锅侠。要记住调度机制再精巧也不能替代你分析业务本身的资源需求。