epoll与Communicator系列笔记(2) 系统调度性能优化神器sched_yield()
本系列是鶸鶸的我对网络相关知识的梳理,如有疏漏欢迎各位大神指出,也希望能够抛砖引玉帮助到有需要的小伙伴。
第一篇知乎站内链接:
1412:epoll与Communicator系列笔记(1) epoll_wait()参数timeout相关的源码阅读笔记
本篇原文于2021年2月底发在架构鶸的公众号上,仅为方便懒得点开知乎看的胖友们:
epoll与Communicator系列笔记(2) 系统调度性能优化神器sched_yield()
1. epoll相关的两点优化
上一篇说到,学到一些神奇的做法后,我也对rpc尝试了一些epoll层面的优化。优秀的字节小伙伴实现Go下的RPC,分享出的是两个优化点:
- 不同情况下使用不同的timeout,可以利用到epoll_wait()内部的实现而减少不必要的代码调用:如果这次可以取到东西,则下次设置timeout为0继续拿;这次取不到则设置-1;
- 在timeout设置为-1的时候,主动切换当前协程以提速;
第1点我上篇文章已经实践过,因为C++与Go下的epoll实现不同。而linux内核对epoll_wait()的实现,是上来就是为我们拿已经ready的事件,也就是说只要有事件,timeout设置多少并不额外影响性能。
对于第2点,其实我也有尝试>///< 在谢爷的指导下,C++下对应的做法,就是利用sched_yield()切出当前线程,这个改进简直效果拔群,不明觉厉!!!
所以,虽然最近忙成狗勾,但这篇还是想尽快简单记录一下~
2. 如何使用sched_yield()
这里列一下epoll的基本使用方式,外加上述提到的两点改进。
其中第二点,就是如果当前拿不到事件,我会主动调用sched_yield()让出线程,等下次我被调起时再做下一轮wait~
int timeout = 0;
while (1) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, timeout);
if (nfds == -1) {
timeout = -1;
sched_yield();
continue;
}
timeout = 0;
... // handle different event
}
其实workflow里对epoll的使用会更复杂。
因为我们对用户的TimerTask的实现,是借用了timerfd去做的。因此,每次操作epoll_wait()之前,还要根据用户所有的TimerTask,去维护一下当前的timerfd。
但本次就不细说了,毕竟我不懂→_→ 以上就是测试代码的简化版了~
3. 实验测试结果
这里先说明一下,为什么会有4次实验。因为我按上述代码改动:发现性能竟然有5%的提升!!!
各位同学,网络框架底层的5%!!!那是相当珍贵!相当感人!
我把这个结果发给谢爷,谢爷高冷地来一句:“你要单独测一下sched_yield()”
于是此实验有两个变量:
- 是否引入timeout的改动
- 和是否用sched_yield()切线程
果不其然,仅仅修改timeout其实并没有任何收益,而加入sched_yield()才是提升关键,但同时也会引起更多的cpu占用。
其他实验说明也在表里,不再赘述,如有疏漏欢迎指正~
4. 鶸鶸的分析
这里man一下sched_yield():
sched_yield() causes the calling thread to relinquish the CPU. The thread is moved to the end of the queue for its static priority and a new thread gets to run.
就会发现,其实看完也是一脸懵逼~~Q_Q~~ 于是我就网上查了下,内核有个东西叫做完全公平调度器,大概能够帮助我分析这个现象。
首先,cpu对于一个线程怎么调度,是取决于:
- 你的优先级
- 你已经被调用了多长时间
linux的调度器CFS,会根据这两个值来决定你当前被放到哪个队列。它内部管理了三个队列:
- 等待队列
- 运行队列
- 过期队列
为什么要有三个队列呢?这里的等待队列,不是单纯地等待执行,而是那些休眠或阻塞的线程。而如果你这个优先级也不是很高,还被执行了超级久超过你的咖位,那你就会被扔到第三个队列-过期队列里。
我们如果用sched_yield()切出,就是主动放到过期队列里,等其他人都享受够了他们deserve的时间,才会开始调度这个过期队列。
这么一听,感觉不合理啊!这明明是个牺牲小我、完成大我,让系统减少忙等的函数而已,而我们这里用epoll明明就不是空跑!
但其实仔细想想,如果我们不主动切出的话,下一次进入epoll_wait()的时候,就会因为-1、且没事件,而被扔进epoll的等待,我们上次看过,epoll内部会先进行nonblockig的loop检查,然后扔进自己的ep->wq里,然后再由操作系统切出。
这一系列的骚操作,都可以通过提前切出而避免。一般来说,等下一次再轮到我的时候,epoll的事件都已经准备好了~这些操作都不需要了。毕竟epoll_wait()在进行挂起前,都会认真地再检查一次是否真的没有事件,上次看还不太明白为什么要这样做,现在能够理解了,是因为真正被挂起的代价太高。
另外一点小猜测是,CFS对于一个线程切出来之后,应该放到哪个队列,还得算一算,这也是xue微有点消耗的?如果我们能够明知道要等的情况,比如:重负载、而当前资源没到位,确实完全可以主动告诉操作系统,减少操作系统做决策的负担。
5. 其他._.
这么一想,操作系统还是非常开明的,制订好机制,给你做策略的权利。当然也正如man所说,sched_yield()的调用得非常慎重,稍微用得不好,往往会降低性能。
而workflow也没有引入这点改动。因为5%的吞吐提升,只有在epoll线程繁重的时候才能体现。而一个完备健壮的系统要考虑的,更多是改动点的合理性和通用性。而这个改动,不说多make sense,至少非常不通用。更何况,我狗线上机器负载已经够高了,引入这点改动还要增加cpu占用,实在得不偿失,不值得做这么tricky的事情。
我鶸鶸地经历了这么些优化后,发现一个事情:越是漂亮、通用、简洁的做法,往往也是被时间证明为正确的做法。计算机的世界里有它所相信的美~
以上就是我的尝试和分析~目前如果工作涉及到网络相关,感觉自己还是摸着石头过河。但是年后这几天真是忙到质壁分离,事情已经密密麻麻排到下个月了。blog的话,且写且珍惜吧 ~希望下一篇写出来的时候,我可以有更organized的理解(冲鸭~~