PHP8
移除这个功能。其实经过实测这个性能损耗并不大,而且我们已经在生产环境验证,并取得了显著的效果,即可以让出某些CPU
密集的逻辑部分,使得服务整个相应时间更加均衡。下图是我们生产环境一个RPC接口的调用端统计数据对比,客户端等待超时时间为2s,超时则统计为错误。
左边一侧是没有抢占式调度,右侧是开了抢占式调度,可以发现,左侧总是会有偶尔超时情况,而经过优化之后,没有一个超时的请求,请求响应时间非常平滑,提升了服务的稳定性。
可以从上图看出,由于抢占式调度的加入,去除了请求耗时高的毛刺,使得平均请求时间变得更加平滑,稳定。
想要做抢占式调度,对于PHP
来说,有两个途径
- 单线程的
PHP
的执行流,通过执行指令做文章,可以在PHP
执行流程中注入逻辑,以检查执行时间,再加上Swoole
的协程能力,可以在不同的协程中切换,以达到抢占CPU
的目的。 - 考虑开线程,负责检查当前执行协程执行时间。
经过以上办法的尝试,注入指令的路数基本是无法得到官方的支持,我们只能另谋出路,多开一个线程,只负责检查当前协程。具体的做法是,利用PHP-7.1.0
引入的VM interrupt
机制,默认每隔5ms检查一下当前协程是否达到最大执行时间,默认为10ms,如果超过,则让出当前协程,达到被其他协程抢占的目的。
示例代码
需要Swoole 4.4
或更高版本
<?php Co::set(['enable_preemptive_scheduler' => 1]); $start = microtime(1); echo "start\n"; $flag = 1; go(function () use (&$flag) { echo "coro 1 start to loop\n"; $i = 0; for (;;) { if (!$flag) { break; } $i++; } echo "coro 1 can exit\n"; }); $end = microtime(1); $msec = ($end - $start) * 1000; echo "use time $msec\n"; go(function () use (&$flag) { echo "coro 2 set flag = false\n"; $flag = false; }); echo "end\n";
执行结果
start coro 1 start to loop use time 11.121988296509 coro 2 set flag = false end coro 1 can exit
可以发现,代码逻辑可以从第一个协程的死循环中自动yield
出来,执行第二个协程,如果没有这个特性,第二个协程永远不会被执行,导致被饿死。而这样做,第二个协程可以顺利被执行,最后执行结束后,第一个协程也会接着继续往下执行。达到我们的第二个协程主动抢占第一个协程