共计 1783 个字符,预计需要花费 5 分钟才能阅读完成。
前言
Swoole 内核团队开设的专栏,会逐渐投入精力写文章介绍 Swoole 的开发历程,实现原理,应用实践等,大家可以更好的交流,共同学习,建设 PHP 生态。
协程调度
去年 Swoole
推出了 4.0 版本后,完整的支持 PHP
协程,我们可以基于协程实现 CSP
编程,身边的开发者惊呼,原来 PHP 代码还可以这样写。Swoole
的协程默认是基于 IO 调度,程序中有阻塞会自动让出当前协程,协程的各种优势我们不在这里展开讨论。如果是 IO
密集型的场景,可以表现得很不错。但是对于 CPU
密集型的场景,会导致一些协程因为得不到 CPU
时间片被饿死。
抢占式调度
我们在今年年初就计划实现 Swoole
的抢占式调度,以满足实现有些场景下的不均衡调度带来的问题。我们中间经历了几个版本,在这里和大家分享一下开发过程中的动机和解决办法。
起初,我们的想法是可以从 PHP
的循环中自动检测执行实践,若达到限制,可以自动让出当前协程。因为毕竟很少有人一马平川的写出占用很多 CPU
的代码,大都通过循环条件来控制。我们 hook
循环指令,每次执行循环指令的时候,都来检查协程的执行时间,我们很欣喜的得到了最初的版本。但是这样做比较 hack,而且 opcode
经过 opcache
优化后,情况会变得有些复杂。
后来我们使用 PHP
的ticks
机制,也就是在 PHP
代码编译期间,注入 ticks
指令,可以执行相应的函数,我们可以在这些函数中检测处理协程的时间,达到抢占式的效果,但是这里有一个问题,PHP
的 declare(ticks=N)
语法,只对当前脚本范围有效,也就是说项目稍微大点,require
或者 include
进来的脚本,并不会自动注入 ticks
指令,这样 Swoole
开发者几乎是无法接受的。我们也试图给 PHP
官方提一个 PR,可以在扩展层设置一个全局默认的 ticks
,但是官方不愿意采纳我们的提交,因为官方觉得这个功能对性能损耗比较大,而且有可能在PHP8
移除 这个功能。其实经过实测这个性能损耗并不大,而且我们已经在生产环境验证,并取得了显著的效果,即可以让出某些 CPU
密集的逻辑部分,使得服务整个相应时间更加均衡。
想要做抢占式调度,对于 PHP
来说,有两个途径
- 单线程的
PHP
的执行流,通过执行指令做文章,可以在PHP
执行流程中注入逻辑,以检查执行时间,再急上Swoole
的协程能力,可以在不同的协程中切换,以达到抢占CPU
的目的。 - 考虑开线程,负责检查当前执行协程执行时间。
经过以上办法的尝试,注入指令的路数基本是无法得到官方的支持,我们只能另谋出路,多开一个线程,只负责检查当前协程。具体的做法是,利用 PHP-7.1.0
引入的 VM interrupt
机制,默认每隔 5ms 检查一下当前协程是否达到最大执行时间,默认为 10ms,如果超过,则让出当前协程,达到被其他协程 抢占 的目的。
来一段代码
<?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
出来, 执行第二个协程,如果没有这个特性,第二个协程永远不会被执行,导致被饿死。而这样做,第二个协程可以顺利被执行,最后执行结束后,第一个协程也会接着继续往下执行。达到我们的第二个协程主动 抢占 第一个协程 CPU
的效果。
这个特性在生产环境非常有用,尤其是对于实时系统或者响应时间比较敏感的场景。
最后
感谢大家对 Swoole 的长期支持和关注。