当前位置: 首页 > 后端技术 > PHP

Swoole4.4CoroutinePreemptiveScheduler详解

时间:2023-03-29 13:45:13 PHP

前言Swoole内核团队开设的专栏会逐步投入精力撰写文章介绍Swoole的开发过程、实现原理、应用实践等,让大家更好的交流、学习共同打造PHP生态。协程调度Swoole在去年推出4.0版本后,全面支持PHP协程。我们可以基于协程来实现CSP编程。身边的开发人员都惊呼原来的PHP代码还能这么写。Swoole的协程默认是基于IO调度的。如果程序中有block,会自动放弃当前协程。我们不会在这里讨论协程的各种优点。如果是IO密集型场景,它可以表现的很好。但是对于CPU密集型场景,有些协程会因为拿不到CPU时间片而被饿死。抢占式调度我们计划在今年年初实现Swoole的抢占式调度,以解决部分场景下调度不平衡带来的问题。我们经历了几个版本,这里想和大家分享一下开发过程中的动机和解决方案。我们的目的是平衡每个协程的CPU时间。比如协程3需要比较长的执行时间。我们必须在不依赖IO事件的情况下,主动打断协程3的CPU时间,让每个协程得到一个平均的执行时间。一开始我们的想法是从PHP周期自动检测执行实践,如果达到限制,可以自动放弃当前协程。因为毕竟很少有人写出占用大量CPU的代码,而且大部分都是通过循环条件来控制的。我们hook循环指令,每次循环指令执行时检查协程的执行时间。我们很高兴得到原版。但是这样做比较hack,而且opcode经过opcache优化后,情况会变得有点复杂。后来,我们利用了PHP的ticks机制,即在PHP代码的编译过程中,注入ticks指令来执行相应的函数。我们可以通过检测这些函数中处理协程的时间来达到抢占的效果,但是这里有个问题。,PHP的declare(ticks=N)语法只对当前脚本范围有效,也就是说项目稍大,require或include的脚本不会自动注入ticks指令,所以Swoole开发者几乎无法接受。我们也尝试过给PHP官方提交PR,可以在扩展层设置一个全局默认的ticks,但是官方不愿意接受我们的提交,因为官方认为这个功能会消耗很多性能,而且它可以在PHP8中删除此功能。其实经过实测,性能损失并不大,而且我们在生产环境中进行了验证,取得了显着的效果,即我们可以放弃一些CPU密集型的逻辑部分,使得服务的整体响应时间更平衡。下图是我们生产环境中某个RPC接口调用者统计的对比。客户端等待2秒超时,超时计为错误。左侧没有抢占式调度,右侧启用抢占式调度。可以发现左侧总是偶尔会出现超时,但是经过优化后,没有出现超时请求,请求响应时间非常流畅,提高了服务的稳定性。从上图可以看出,由于抢占式调度的加入,去除了请求时间高的毛刺,使得平均请求时间更加平滑稳定。如果要做抢占式调度,对于PHP来说,单线程PHP执行流程有两种方式。通过执行指令做文章,可以在PHP执行流程中注入逻辑来查看执行时间,再加上Swoole的协同可以在不同的协程之间切换,达到抢占CPU的目的。考虑开一个线程,它负责检查当前正在执行的协程的执行时间。在尝试了以上方法后,注入指令的方式基本无法得到官方的支持。我们只能另辟蹊径,多开一个线程,只负责检查当前协程。具体方法是使用PHP-7.1.0引入的VM中断机制,默认每5ms检查一次当前协程是否达到最大执行时间。默认值为10毫秒。进程抢占的目的。示例代码需要Swoole4.4或更高版本