前言

前文再续,就书接上一回,经过了各种突发事件的2016后,笔者终于苟延残喘到了传说中的2017,去年我们简单聊了Swoole的进程模型,这次俺们来聊聊热重载的问题。

在这个普天同庆的元旦里,祝愿大伙身体健康,平平安安~

重载之初

在传统的LAMP环境中,我们调试应用有时候非常简单,修改,保存,重新访问(网页),就能看到刚刚修改的代码的效果,所以很多童鞋都习惯了随改随用,可是当业务来到Swoole Server的时候,却发现了一些不同。

  1. // 篇幅原因,省略其他代码,我们仅来看看OnReceive的时候
  2. $server->on('receive', function ($serv, $fd, $from_id, $data){
  3. echo "Receive something\n";
  4. });

这段DEMO的逻辑非常简单,每当Server收到一个包的时候,输出一段文字。我们来模拟一个业务调整的场景,希望Server在收到包的时候,输出两段文字,现在我们简单修改一下代码如下并保存:

  1. // 篇幅原因,省略其他代码,我们仅来看看OnReceive的时候
  2. $server->on('receive', function ($serv, $fd, $from_id, $data){
  3. echo "Receive something\n";
  4. echo "I can not receive anything\n"
  5. });

然后再使用telnet作为测试工具,向Server发出请求,然后就像很多童鞋学习过程中遇到的那样,输出仍然是“Receive something\n”,后加的“I can not receive anything\n”并不会出现。

这个结果与我们习惯中即插即用的PHP表现并不相同。

这是什么原因呢?首先,我们要从LAMP的工作特性说起,如果说Linux隔离的是硬件层和软件层(作为操作系统),那么Apache在这个框架下隔离的其实是TCP Server和Handle HTTP REQUEST的工作,也就是说,每当客户端发送一个HTTP Request给Apache的时候,Apache首先将其按HTTP协议的格式进行解析,再根据当前自己加载的配置文件,搜索可以处理这个HTTP请求的程序(在此可以看作是Zend引擎),并根据URL描述的路径找到相关的*.php文件,通过调用Zend解析并执行该文件中PHP的代码,并返回输出,Apache获得输出以后,重新组织成合适的HTTP Response,并返回个客户端(在这个场景下就是浏览器了)。

部分内容可以参见拙作《当SWOLLE遇上Server》、《当SWOOLE遇上PROTOCOL》,草蛇灰线的铺垫感觉终于在新的一年冒泡了=。=

这个过程中,我们参考一下上一章《SWOOLE的多进程模型》,我们能发现什么相似的地方吗?

木有错,从分工的角度上看,Apache就像Master进程一样,承担了维持连接并转发请求的工作,而具体的业务内容,是由Zend引擎和其执行并加载的PHP代码所决定的。

正如我们所知的一样,Apache并不止可以处理PHP的服务,它本质上是一个Web Server的容器

换个角度讲,之所以我们在使用LAMP的过程中,修改PHP代码以后可以马上看到效果,是因为每个新的请求都是重新调用Zend重新解析硬盘上的PHP代码文件而获得的。

显而易见,这种模式的优点就是开发简便,快速;这也是PHP为什么能在WEB开发中雄霸天下的重要原因之一。

性能问题,往往是在开发效率和执行效率上取得一个平衡。

然而,有利,就自然有弊,可以想象,当业务正式上线,代码趋于稳定,不需要经常修改的时候,每次收到业务请求的时候都要重新从硬盘中读取文件,再加载到内存,然后解析执行显然会造成大量的时间浪费。

传统的PHP模型中,显然也不会忽略这个问题,有大量的工具都可用于或设计于解决这个问题,笔者这里就不罗嗦了。

而SWOOLE Server作为PHP的进阶工具,它设计之初就是面向高性能以及更底层的业务开发而设计的,所以,它的默认设计下并不能支持即改即用。

事实上,笔者很多时候喜欢在HTTP Server的本地环境调试业务代码,享受PHP的高效调试,等稳定了再提交到SWOOLE Server进行进一步的处理,一般来说,如果抽象的当,代码的公用并不需要修改任何东西。

重载进行时

咳咳,扯了五十多行的淡,让笔者回收一下不知道飞到哪里的思路,回到SWOOLE Server热重载的问题上。

在上一章中,我们已经介绍了,业务问题放在Worker进程中处理的基本原则,那么这里,我们也可以大胆推断一下为什么修改PHP文件后,代码并不能生效的问题。

因为,PHP代码中的内容已经被加载到了内存中,每次收到业务请求的时候,SWOOLE Server并不会重新从硬盘中读取新的PHP文件,而是直接根据已加载到内存中的代码执行业务逻辑。

所以,最简单的重载方法就粗线了:把当前Server相关进程干掉,然后重启服务。

如笔者在拙作《当SWOOLE遇上TCP》中介绍的,一个TCP连接就像打电话,而一个Web Server的业务特性决定了它往往需要同时维护多个通信连接。

而把Server干掉相当与什么情况呢?把所有的电话线瞬间剪断,不考虑你已经说了一半的话,例如,当“我不想再做你的‘不歪富源德’了,我要做你的‘哈斯笨的’”由于通话中断而变成“我不想再做你的‘不歪富源德’了”,本来只要9.9就能解决的问题,分分钟演化成在99集的八点档狗血电视剧。

在《当SWOOLE遇上PROTOCOL》一文中,其实设计PROTOCOL也是为了避免类似的问题,但两者的手段不同,PROTOCOL是在业务逻辑中,而这里讨论的是运作机制上。

那确实发生需要修复Server的时候,应该怎么办呢?在Swoole的多进程模型中,Master进程负责维护所有与客户端的连接,而Worker进程才是处理业务逻辑的地方,也就是说,我们重载业务的时候,并不需要把整个Server都干掉,我们只需要干掉Worker进程,然后重新启动就可以了。

其实热重载,在笔者看来,可以看作一种小范围的重启

但是,我们的Worker如果正在执行某个工作,忽然被打断,怎么办?这样虽然保住了电话线,但业务层的事务性就可能会出现问题了不是么?

当然,SWOOLE Server设计了更优雅的策略来处理这个问题,也就是本章的主体,热重载的一般用法。

重载与进程通讯

我们先假设有如下的Swoole进程正在工作:

  1. # 这个名称只是示意,并非真实的运行情况
  2. -php 12138 swoole_master
  3. |-php 12139 swoole_manager
  4. |-php 12140 swoole_worker_01
  5. |-php 12141 swoole_worker_02

正如我们上次聊的那样,Master进程收到了客户端的请求以后,解决了TCP级别的协议问题后,就把请求交给了manager进程处理,而manager进程则会进一步的把工作交给worker,最后的业务逻辑是在woeker中完成的。

那么此时我们需要告知Server,代码发生了变动,快干掉现有的Worker进程,重新读取硬盘的文件并拉起新的Worker进程应该怎么做呢?

从工作机制上,其实只需要Manager进程和Worker进程相互配合好即可。

对于Manager进程而言,它只需要做到一点,如果发现麾下的某个Worker进程不是正常退出的,那么Manager进程要负责重新拉起一个Worker进程,而重新拉起Worker进程的时候,由于内存里已经没有了旧的Worker进程,则需要重新从硬盘中读取代码。

事实上,这个设计同时还保障了如果由于业务原因导致某个Worker进程意外挂掉了,还会有新的Worker进程被生产出来,用于保障服务。

对于Worker进程而言,我们都知道,每当某个Worker手头的工作完成后,都要把自己已经空下来,可以接受新的工作的状态告诉Manager进程,以便于Manager进程分配新的工作给Worker进程,换个角度讲,Manager进程是知道某个Worker进程的工作状态的,为了防止事务行为被重载打断,只要调Worker进程空闲的时候重载,就可以了。

这是个很经典的异步协同工作流程。

而怎么实现这个过程呢?Swoole Server已经提供了接口,就是通过kill命令向Manager进程发送SIGUSR1(-10)的信号量,则Manager进程就会先Hold住当前接受到的请求(队列),把已经空闲的Worker进程干掉,并重新拉起新的Worker进程,并将新的请求交给新的Worker进程处理,直到所有的Worker进程都被重新拉起为止。

这其实是柔性终止/重启机制,不立即把进程干掉,而是先禁止进程接收新的请求,然后允许进程把手头上正在处理的业务做完,再稳定退出进行。

  1. # 如上文中我们知道了Manager进程的pid是12139
  2. sudo kill -10 12139
  3. # 等待worker完成已有的工作,再执行pstree看看
  4. -php 12138 swoole_master
  5. |-php 12139 swoole_manager
  6. |-php 12142 swoole_worker_01
  7. |-php 12143 swoole_worker_02

新的worker已经被拉起,新的代码也都被加载到了Worker的进程中,新的传说即将开启……

再用telnet发送一个请求看看?

思考与展望

讲到这里,热重载的一般性问题已经基本到此结束了,其实笔者想介绍的核心特点还是代码的执行方式,所导致的执行结果的不同,并反应在调用逻辑上,PHP作为脚本语言,天性上就更容易被忽略其被加载到内存的过程,不像C等静态语言,需要明确的编译链接才能执行,因此容易误会,其实无论哪种策略也好,并没有最优与最劣,只有最合适。

另一方面就是顺路介绍一下柔性重启的思路咯

那么,作为史上最啰嗦的笔者,最后留几个思考题,各位童鞋有兴趣的话不妨实验一下:

  1. 如果在worker进程中执行了sleep的方法,会发生什么?
  2. 如果重载的新代码本身有语法错误,会发生什么?
  3. 如果不是热重载,而是要柔性的停止整个Server服务,又应该怎么设计?