前几天,同事咨询了我一个问题:IO占用能和cpu使用率那样,有方法来控制吗?这个问题的背景是因为客户提了两个需求,如下:
说实话,针对这两点需求,我的第一反应是有一点思路,但是并没有具体的方案。比如可以通过sleep
的方式减少对CPU、IO的占用。但是如何去设计呢?令我比较有兴趣。
经讨论,进程CPU使用率可以通过开源工具cpulimit
进行控制,但是进程的IO占用目前并没有好的工具可以实现。好奇心爆棚的我,比较想了解cpulimit
的实现原理,以及在这之上是否能够对进程IO占用有借鉴意义。
开源工具cpulimit
体积很小,就2千行代码左右,两个小时基本就能看完。了解其大致原理后,感觉对实现控制进程IO占用的功能也有一定的参考作用。通过本篇,希望能够和大家分享一下我的思路。若由任何不妥的地方或问题,欢迎评论区讨论。
开源工具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.00
12: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.00
12: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
。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.87
01: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.00
01: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。
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的节拍数。cpulimit
则会想所有的进程发送信号SIGSTOP
,并自身nanosleep
TIME_SLOT -
T
r
e
l
w
o
r
k
T_{rel_work}
Trelw?ork?时长。cpulimit
则会想所有的进程发送信号SIGCONT
,并自身nanosleep
T
w
o
r
k
T_{work}
Twork? -
T
r
e
l
w
o
r
k
T_{rel_work}
Trelw?ork? 时长。伪代码大致流程如下:
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、worktime
workingrate = 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?。proc
文件系统,获取进程当前已经消耗的cpu时间
T
i
o
2
T_{io2}
Tio2?。并记录当前时间系统时间
T
总
2
T_{总2}
T总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,人生尽是坦途。