上一篇:swoole4.0之打造自己的web开发框架(7), 我们了解php,swoole的生命周期,以及正确的单例使用
不管是FPM还是swoole都是多进程单线程的架构,所以单例实现比较简单,不会存在线程安全的问题,用一个全局的static变量即可
严格意义上来讲,单例是不能有状态的,大部分情况也是如此,所以基本用全局单例就可适用,如你的单例有状态(数据、资源)等,那么解法上一篇也给出了,就是把这些有状态的情况拎出来单独处理
swoole因为常驻内存,所以有了很多的性能提升点,但并不能像FPM一样的无痛热更新,这是一个巨大的劣势, 本篇将通过一些手段来实现热更新
热更的原理?
swoole和nginx/fpm 架构类似,都是多进程架构,所以热更的原理也一样,就是给server发一个信号, server接收到信号后,会停止当前的工作进程接受新的请求,处理完当然请求后自动退出,然后会重新拉起一组工作进程,在这个拉起过程中,会有机会重新加载代码,从而达到热更的效果
哪些可热更?
swoole中并不是任何代码都能热更新,所以我们需要知道哪些情况下可以,哪些情况不可以,这就要回到上一篇有关swoole生命周期里的一些内容以及上面的原理介绍:
onWorkerStart之前的代码是不会被热更的
以Family为例,我们可以看一下,在onWorkerStart之前,会加载多少的文件, 我们一起来看一下:
在onWorkerStart回调第一行,输入如下代码:
print_r(get_included_files());
php application index.php 运行服务
Array ( [0] => /Users/work/github/family/application/index.php [1] => /Users/work/github/family/vendor/autoload.php [2] => /Users/work/github/family/vendor/composer/autoload_real.php [3] => /Users/work/github/family/vendor/composer/ClassLoader.php [4] => /Users/work/github/family/vendor/composer/autoload_static.php [5] => /Users/work/github/family/vendor/nikic/fast-route/src/functions.php [6] => /Users/work/github/family/vendor/symfony/polyfill-ctype/bootstrap.php [7] => /Users/work/github/family/framework/Family/Family.php [8] => /Users/work/github/family/framework/Family/Core/Config.php [9] => /Users/work/github/family/application/config/default.php )
可以看到,总共有9个文件被加载了,如要改动这9个文件代码,是不能被热加载的
但我们可以看到,这9个文件都是非常基础的框架性文件,几乎没有改动的必要
opcache的影响
如果我们开启了opcache, 那么我们需要在workerStart最开始加入以下代码:
if (function_exists('opcache_reset')) { //清除opcache 缓存,swoole模式下其实可以关闭opcache \opcache_reset(); }
作用是清空opcache缓存,否则新文件不会被加载,其实在swoole这种常驻内存的模式中,opcache的用途不大, 完全可以关闭opcache
除了这些文件之外,其他的文件改动,都可以做热更新
如何热更新
可以向主进程或manager进程发送 USR1 信号: kill -USR1 进程id, 如果我们每次改动都手动敲这个命令,还需要提前通过ps 看进程id,效率很是低下
服务脚本
所以这的的做法就是把这类雷同的操作,做成一个脚本,我们在bin目录下创建一个family.sh脚本,来管理服务的启停,代码如下:
#! /bin/sh ### BEGIN INIT INFO # Provides: family application server # Required-Start: $remote_fs $network # Required-Stop: $remote_fs $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: starts family server # Description: starts the Family Application daemon ### END INIT INFO #php路径,如不知道在哪,可以用whereis php尝试 PHP_BIN=`which php` #bin目录 BIN_PATH=`pwd` #入口文件 SERVER_PATH=$BIN_PATH/.. #脚本执行地址, 可修改为你的php运行脚本######### APPLICATION_FILE=$SERVER_PATH/application/index.php #获取主进程id getMasterPid() { if [ ! -f "$BIN_PATH/master.pid" ];then echo '' else PID=`cat $BIN_PATH/master.pid` echo $PID fi } #获取管理进程id getManagerPid() { if [ ! -f "$BIN_PATH/manager.pid" ];then echo '' else MID=PID=`cat $BIN_PATH/manager.pid` echo $MID fi } case "$1" in #启动服务 start) PID=`getMasterPid` if [ -n "$PID" ]; then echo "server is running" exit 1 fi echo "Starting server " $PHP_BIN $APPLICATION_FILE echo " done" ;; #停止服务 stop) PID=`getMasterPid` if [ -z "$PID" ]; then echo "server is not running" exit 1 fi echo "Gracefully shutting down server " kill $PID sleep 1 if [ -n "$PID" ]; then unlink $BIN_PATH/master.pid unlink $BIN_PATH/manager.pid fi echo " done" ;; #查看状态 status) PID=`getMasterPid` if [ -n "$PID" ]; then echo "server is running" else echo "server is not running" fi ;; #退出 force-quit) $0 stop ;; #重启 restart) $0 stop $0 start ;; #reload reload) MID=`getManagerPid` if [ -z "$MID" ]; then echo "server is not running" exit 1 fi echo "Reload server ing..." kill -USR1 $MID echo " done" ;; #reload task进程 reloadtask) MID=`getManagerPid` if [ -z "$MID" ]; then echo "server is not running" exit 1 fi echo "Reload task ing..." kill -USR2 $MID echo " done" ;; #提示 *) echo "Usage: $0 {start|stop|force-quit|restart|reload|status}" exit 1 ;; esac
有了这个脚本,我们只需要如此:
./family.sh start ,就可以启动服务
./family.sh stop ,就可以停止服务
./family.sh reload, 就可以reload
在框架代码里,也有些调整:
$http->on('start', function (\swoole_server $serv) { //服务启动 //日志初始化 Log::init(); file_put_contents(self::$rootPath . DS . 'bin' . DS . 'master.pid', $serv->master_pid); file_put_contents(self::$rootPath . DS . 'bin' . DS . 'manager.pid', $serv->manager_pid); Log::info("http server start! {host}: {port}, masterId:{masterId}, managerId: {managerId}", [ '{host}' => Config::get('host'), '{port}' => Config::get('port'), '{masterId}' => $serv->master_pid, '{managerId}' => $serv->manager_pid, ]); }); $http->on('shutdown', function () { //服务关闭,删除进程id unlink(self::$rootPath . 'DS' . 'bin' . DS . 'master.pid'); unlink(self::$rootPath . 'DS' . 'bin' . DS . 'manager.pid'); Log::info("http server shutdown"); });
我们在服务启动的时候,会把主进程和管理进程id分别写到 bin/master.pid 、bin/manager.pid 里,在服务停止的时候,会删除这两个pid文件
自动热更新
有了脚本,只是可以提高我们手写命令的效率,并不能自动热更新
在开发环境,我们可以通过 fswatch 来自动监控文件变化,然后执行脚本, 代码如下:
#!/bin/bash DIR=`pwd` checkExt=php fswatch $DIR/.. | while read file do filename=$(basename "$file") extension="${filename##*.}" #php文件改动,则reload if [ "$extension" == "$checkExt" ];then #reload代码 $DIR/family.sh reload fi done
然后开一个窗口 执行 ./fswatch 启动,就可以愉快的热更新了
在生产环境,我们可以在代码部署完成,自动执行shell勾子,来reload代码
奇技淫巧
在开发环境,我们也可以有一些小技巧做自动reload
设置max_request = 1,这样每处理一个请求之后,就可以reload了
开放一个接口,如 xxx/reload, 在代码显示调用$serv->reload
写一个定时器,固定时间自动 reload
本篇的内容基本完成,至此整个系列也差不多告一段落了,大家有兴趣可以后续共同完善这个框架,可以给我提PR,另外,要保障一个项目的质量,测试是避免不了的,有兴趣的同学,可以引入phpunit,对框架加入更多的测试用例
查看原文,了解swoole的各项配置
github地址(最新代码都已上传):
https://github.com/shenzhe/family
----------伟大的分割线-----------
PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!
饭米粒只发原创或授权发表的文章,不转载网上的文章
所发的文章,均可找到原作者进行沟通。
也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投搞。
投稿请联系:
shenzhe163@gmail.com
本文由 半桶水 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)