zsh nice 5 🧐

The English Version is here

今天在排查一个服务吞吐量上不去的问题,在做压力测试,期间刚好在观测CPU使用率,系统是32c的。 除了关注进程的CPU消耗情况,我还会关注每个核心的使用率,确保不会出现核心利用率不均衡(之前在NUMA Node时因为大量网卡软中断出现过,所以现在习惯性会关注一下) , 一开始一切都蛮正常的,类似这样:

Normal CPU Usage

可以看到,进程使用了28c,约为87%的使用率。整体其实已经跑了挺满的,在单核上也可以观测到us都蛮高且相对均衡(77%~90%)

TIP

top里是按照绝对值计算百分比的,因此不是传统的0-100%的认知,比如我32c,实际上最大是跑到3200%

Abnormal CPU Usage

在经过几次调整测试的过程中,突然之间,我留意到单核的us都接近0了,但是进程级别的使用率看起来和之前保持相似的使用率,此时脑子过了好几个想法:

  • 是不是top的显示有问题?
  • top发生某种奇怪的错误统计?
Grafana CPU Usage: Total Left, Core Right

然后我马上打开Grafana查看了机器层面的自监控,在Grafana上查看总体CPU使用率和分核使用率都是在高位,符合预期,略奇怪(此时仍然不知道自己粗心没留意到ni)

结合前面的想法,我找了设备运维大佬,请教了问题,然后他一下指出ni很高,不太对,还指明正常我们的设备不会调整进程的nice。 这个时候我才恍然大悟,哦,确实ni列的值都很高呀,然后我开始从乡村土路开回了高速

首先明确一下nice的定义,nice越小优先级越高(范围从-20 ~ 19 or -20 ~ 20) 系统级别的正常-20,用户态进程是0,我看了一下我的进程是5🤔,what’s going on? 直到这里已经能解释清楚,为什么ni那么高了,因为进程的nice=5, 被认为是较低优先级的进程,同等条件下比0或者-20更加小的机会被CPU调度执行,但是因为我的进程在压测,进程过于强势,吃掉了80+%的CPU时间片, 此时就单核心的ni列代表就是CPU被低优先级的进程占用的百分比,其实这种情况下是符合预期的,也就是我们认为的单核使用率=us+ni,在这种场景下, 没有任何问题。

回过头来,问题在于,为什么进程变成了ni 5?什么时候开始的?为什么?

于是开始回溯,开始挖掘,因为压测过程中会调整各类参数,甚至也会调优一下代码,所以还是有比较多的变量,好在我能明确是那次变化出现的, 不过挖掘了半天没再出现,直到某刻突然灵光一闪,不会是bg job吧?因为要调整启动参数,有时候为了快速切换我会直接kill掉服务然后用类似commd &的方式直接手动拉一下, 然后我试了一下,bingo,就是你了,然后我就写了一个shell进一步确认了一下

#!/bin/sh

sleep 333;
bg job in zsh & bash

确实是,通过这种方式启动的,会导致nice被打成5,然后我就开始在网上找资料,并且也问了ChatGPT,但是并没有任何相关的信息明确表示bg job会将nice设成5, 并且有些地方明确表示除非主动设置否则不会改变进程的nice值,此时我灵光一想,从zsh切到bash,测了一下,emmmm,确实不会改变nice,至少在bash下不会, 问题面进一步缩小了,问题出在zsh身上,继续查资料问AI,依然没有明确的结果,我还进一步检查了包括.zshrc在内相关的配置文件里也没有任何nice相关的设置,很疑惑

过了一会我放弃在网上搜索结果了,我开始找zsh的源码,当我把源码clone下来,我开始翻看源码, C写的,我有两个线索,一个是nice=5,一个是bg job(&),开始围绕这两个去针对性挖掘就好了,在这里我依然还是背靠大山,ChatGPT一下就帮我缩小范围到某几个文件上了,分别是

	•	Src/parse.c:包含 zsh 的语法解析器代码。
	•	Src/exec.c:包含命令执行相关的代码。
	•	Src/jobs.c:处理作业控制和后台进程的代码。

我在jobs.c找到了spawnjob这个函数,但是这个文件没有涉及nice的调整,继续翻看了exec.c,在execcmd_fork这个函数内部,看到了目标代码

  • /**/
    static int
    execcmd_fork(Estate state, int how, int type, Wordcode varspc,
    	     LinkList *filelistp, char *text, int oautocont,
    	     int close_if_forked)
    {
    // ...
    #ifdef HAVE_NICE
        /* Check if we should run background jobs at a lower priority. */
        if ((how & Z_ASYNC) && isset(BGNICE)) {
    	errno = 0;
    	if (nice(5) == -1 && errno)
    	    zwarn("nice(5) failed: %e", errno);
        }
    #endif /* HAVE_NICE */
    
        return 0;
    }
    
  • #ifdef HAVE_NICE
    	/* Check if we should run background jobs at a lower priority. */
    	if ((how & Z_ASYNC) && isset(BGNICE))
    	    nice(5);
    #endif /* HAVE_NICE */
    

可以看到这个条件编译(即在支持nice的系统上才会调用)内部包含了判断:当是异步作业的时候,并且BGNICE设置的话,就会把nice设置5, 翻看了一下git历史,从1999年4月16日最初的版本就已经带上了这个核心逻辑了,只是后来针对cmd fork和相关的错误捕获做了几次修订

Git History

所以我们可以明确从最开始zsh就已经明确后台执行的任务优先级不会比前台的任务高,也就一直遗留至今了,至于历史原因,我是没找到任何相关的资料文献, 我看到最初Inital reversionTanaka Akira提交的,我写了一封邮件给他,希望能了解一下历史原因和背景。但是可惜的是,他的邮箱已经不再使用了, 被退信了。

其实在此基础上其实还可以继续挖一下how, Z_ASYNC, BGNICE的来源,在哪些地方被更改设置了,甚至进一步再回顾一下CPU调度策略,尤其结合优先级来测一下, 但是最近有点忙,社区里还有几个PR需要处理。whatever, casual tech just for casual :)

只是好奇心作祟下的一次探索 💀




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • zsh nice 5 🧐. How the nice value disturb the… | by ifuryst | Aug, 2024 | Medium
  • TCP congestion control. Statement!!! | by ifuryst | Medium
  • 目标制定、标准衡量和人心管理 / Goal Planning, Performance Assessment, and Team Cohesion
  • 最高级的生活方式:存钱,早起,运动,读书 / Ultimate Adulting Hack: Stashing Cash, Racing the Sunrise, Sweating by Choice, and Pretending You Actually Finished That Book
  • 在确定性中寻找不确定性 / Seeking uncertainty in certainty