背景
前几天,同事咨询了我一个问题:IO占用能和cpu使用率那样,有方法来控制吗?这个问题的背景是因为客户提了两个需求,如下:
说实话,针对这两点需求,我的第一反应是有一点思路,但是并没有具体的方案。比如可以通过sleep
的方式减少对CPU、IO的占用。但是如何去设计呢?令我比较有兴趣。
经讨论,进程CPU使用率可以通过开源工具cpulimit
进行控制,但是进程的IO占用目前并没有好的工具可以实现。好奇心爆棚的我,比较想了解cpulimit
的实现原理,以及在这之上是否能够对进程IO占用有借鉴意义。
开源工具cpulimit
体积很小,就2千行代码左右,两个小时基本就能看完。了解其大致原理后,感觉对实现控制进程IO占用的功能也有一定的参考作用。通过本篇,希望能够和大家分享一下我的思路。若由任何不妥的地方或问题,欢迎评论区讨论。
cpulimit 使用及设计分析
开源工具cpulimit
可通过git下载,本地编译,验证。
下载:
git clone https://github.com/opsengine/cpulimit.git
编译:
yihua@ubuntu:~/cpulimit$ make
cd src && make all
make[1]: Entering directory '/home/yihua/cpulimit/src'
cc -c list.c -Wall -g -D_GNU_SOURCE
cc -c process_iterator.c -Wall -g -D_GNU_SOURCE
cc -c process_group.c -Wall -g -D_GNU_SOURCE
cc -o cpulimit cpulimit.c list.o process_iterator.o process_group.o -Wall -g -D_GNU_SOURCE
cpulimit.c:46:18: warning: extra tokens at end of #ifdef directive#ifdef __APPLE__ || __FREEBSD__make[1]: Leaving directory '/home/yihua/cpulimit/src'
cd tests && make all
make[1]: Entering directory '/home/yihua/cpulimit/tests'
cc -o busy busy.c -lpthread -Wall -g
cc -I../src -o process_iterator_test process_iterator_test.c ../src/list.o ../src/process_iterator.o ../src/process_group.o -lpthread -Wall -g
process_iterator_test.c:31:18: warning: extra tokens at end of #ifdef directive#ifdef __APPLE__ || __FREEBSD__make[1]: Leaving directory '/home/yihua/cpulimit/tests'
yihua@ubuntu:~/cpulimit$
验证:
- 通过
stress
工具模拟CPU密集型场景。stress -c 4
,4表示创建4个子进程,因为我的环境是4核,故创建4个进程。 - 通过
mpstat
查看系统的cpu使用率。mpstat -P ALL 1
,显示更新频率为1s。
yihua@ubuntu:~/cpulimit$ mpstat -P ALL 1
Linux 4.15.0-213-generic (ubuntu) 12/20/2023 _x86_64_ (4 CPU)12:57:49 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
12:57:50 AM all 99.25 0.00 0.75 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:50 AM 0 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:50 AM 1 97.00 0.00 3.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:50 AM 2 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:50 AM 3 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.0012:57:50 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
12:57:51 AM all 99.50 0.00 0.50 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:51 AM 0 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:51 AM 1 98.00 0.00 2.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:51 AM 2 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:51 AM 3 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.0012:57:51 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
12:57:52 AM all 99.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:52 AM 0 99.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:52 AM 1 97.98 0.00 2.02 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:52 AM 2 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:57:52 AM 3 99.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
由上可知,CPU占用率约100%。
- 通过
cpulimit
控制stress
进程的cpu使用率。./src/cpulimit -p pid -l 50 -i
。 - 再观察系统的CPU使用率,如下:
yihua@ubuntu:~$ mpstat -P ALL 1
Linux 4.15.0-213-generic (ubuntu) 12/20/2023 _x86_64_ (4 CPU)01:10:45 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
01:10:46 AM all 13.40 0.00 1.99 0.00 0.00 0.00 0.00 0.00 0.00 84.62
01:10:46 AM 0 11.11 0.00 2.02 0.00 0.00 0.00 0.00 0.00 0.00 86.87
01:10:46 AM 1 15.84 0.00 3.96 0.00 0.00 0.00 0.00 0.00 0.00 80.20
01:10:46 AM 2 12.87 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 86.14
01:10:46 AM 3 13.13 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 86.8701:10:46 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
01:10:47 AM all 12.00 0.00 1.50 0.00 0.00 0.25 0.00 0.00 0.00 86.25
01:10:47 AM 0 10.00 0.00 2.00 0.00 0.00 0.00 0.00 0.00 0.00 88.00
01:10:47 AM 1 15.15 0.00 3.03 0.00 0.00 0.00 0.00 0.00 0.00 81.82
01:10:47 AM 2 12.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 88.00
01:10:47 AM 3 12.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 88.0001:10:47 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
01:10:48 AM all 12.78 0.00 0.75 0.00 0.00 0.00 0.00 0.00 0.00 86.47
01:10:48 AM 0 15.00 0.00 3.00 0.00 0.00 0.00 0.00 0.00 0.00 82.00
01:10:48 AM 1 12.87 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 87.13
01:10:48 AM 2 12.12 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 87.88
01:10:48 AM 3 11.88 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 88.12
由上可知,CPU0 + CPU1 + CPU2 + CPU3
约等于50。
cpulimt设计流程分析
cpulimit
真的很小,建议小伙伴们自己走读一遍,加强理解。cpulimit -p pid -l 50 -i
其大致流程如下:
- 设定一个应用时间片
TIME_SLOT
=100ms。计算得到 T w o r k T_{work} Twork时间和 T s l e e p T_{sleep} Tsleep时间。即 T w o r k T_{work} Twork=TIME_SLOT*limit
,$T_{sleep}=TIME_SLOT*(1-limit)
。 - 通过
pid
,在proc
文件系统中,找到进程及其子进程的信息目录。 - 通过获取
/proc/pid/stat
文件中的utime
和stime
的时长,utime
+stime
得到该进程已消耗CPU的节拍数。 - 将进程及其子进程的节拍数相加,得到该进程消耗的总的节拍数。即为实际运行时长 T r e l w o r k T_{rel_work} Trelwork。
- 若 T r e l w o r k T_{rel_work} Trelwork > T w o r k T_{work} Twork,说明超过了限定值。需要释放CPU使用权。
cpulimit
则会想所有的进程发送信号SIGSTOP
,并自身nanosleep
TIME_SLOT - T r e l w o r k T_{rel_work} Trelwork时长。 - 若 T r e l w o r k T_{rel_work} Trelwork < T w o r k T_{work} Twork,说明为达到限定值,可以继续占用CPU使用权。
cpulimit
则会想所有的进程发送信号SIGCONT
,并自身nanosleep
T w o r k T_{work} Twork - T r e l w o r k T_{rel_work} Trelwork 时长。
- 若 T r e l w o r k T_{rel_work} Trelwork > T w o r k T_{work} Twork,说明超过了限定值。需要释放CPU使用权。
- 重复1~3步骤。
伪代码大致流程如下:
while(true)
{//1. 更新进程组的资源消耗update_process_group(&pgroup);...for (node = pgroup.proclist->first; node != NULL; node = node->next) {struct process *proc = (struct process*)(node->data);if (proc->cpu_usage < 0) {continue;}if (pcpu < 0) pcpu = 0;pcpu += proc->cpu_usage;}...//2. 计算sleeptime、worktimeworkingrate = MIN(workingrate / pcpu * limit, 1);tsleep.tv_nsec = TIME_SLOT * 1000 - twork.tv_nsec;//3. 唤醒进程组while (node != NULL){struct list_node *next_node = node->next;struct process *proc = (struct process*)(node->data);kill(proc->pid,SIGCONT);node = next_node;}//3.1 让进程组运行twork时长gettimeofday(&startwork, NULL);nanosleep(&twork, NULL);gettimeofday(&endwork, NULL);//3.2 让进程组sleep twork时长if (tsleep.tv_nsec>0) {node = pgroup.proclist->first;while (node != NULL){struct list_node *next_node = node->next;struct process *proc = (struct process*)(node->data);kill(proc->pid,SIGSTOP);node = next_node;}nanosleep(&tsleep,NULL);
}
思考
进程IO占用是否能够采用上述的方式:通过监控进程发送SIGSTOP
、SIGCONT
控制目标进程的运行和休眠呢?
其实略加思考,就知道是不可行的。我们可以思考一下这个问题:客户提出IO占用可控的目的是什么?
答:无非就是防止该进程一直占用IO资源,其它业务进程获取不到资源,影响正常业务执行。如果采用cpulimit
的方案,那么就会出现一种情况:若目标进程正在持有该IO资源,即使操作系统通过SIGSTOP
信号,将目标进程挂起,那么其它进程也无法获取该IO资源,并不能达到客户的预期。
脑洞大开(仅供参考)
通过cpulimit
的设计思路,以及我们程序的应用场景,于是乎我想到了一种解决方案,似乎也能达到IO占用控制的效果。如有任何不对的地方,还请不吝指教。
我们的程序IO主要就是read/write 分区。我可以封装自己的read/write接口。其逻辑大致如下:
- 设定一个应用时间片
TIME_SLOT
=100ms。计算得到 T w o r k T_{work} Twork时间和 T s l e e p T_{sleep} Tsleep时间。即 T w o r k T_{work} Twork=TIME_SLOT*limit
, T s l e e p T_{sleep} Tsleep=TIME_SLOT*(1-limit)
- 通过
proc
文件系统,获取进程当前已经消耗的cpu时间 T i o 1 T_{io1} Tio1。并记录当前时间系统时间 T 总 1 T_{总1} T总1。 - 执行read/write系统调用。
- 通过
proc
文件系统,获取进程当前已经消耗的cpu时间 T i o 2 T_{io2} Tio2。并记录当前时间系统时间 T 总 2 T_{总2} T总2。- 若 T i o 2 T_{io2} Tio2 - T i o 1 T_{io1} Tio1 < T w o r k T_{work} Twork 且 T 总 2 T_{总2} T总2- T 总 1 T_{总1} T总1 < T w o r k T_{work} Twork ,说明还未达到限定值,可以继续执行IO。
- 若 T i o 2 T_{io2} Tio2 - T i o 1 T_{io1} Tio1 > T w o r k T_{work} Twork,说明单位时间内,已达到占用比,执行下一步骤休眠。
- 若 T 总 2 T_{总2} T总2- T 总 1 T_{总1} T总1 > T w o r k T_{work} Twork,说明单位时间内,IO被其它进程抢占,未达到限定值。回到步骤2。
nanosleep
T w o r k T_{work} Twork - ( T 总 2 T_{总2} T总2- T 总 1 T_{总1} T总1) 。
以上便是我暂时的设想,当然真正实现时,需要考虑一些细节。比如步骤3和步骤4.1,若不加处理,大量的时间会消耗在proc
文件系统读取上。
总结
以上便是的初步设想,后续有时间会进行代码验证,有兴趣的朋友可以关注哈。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途。