上一篇: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带来一些值得细细品味的精神食粮!
饭米粒只发原创或授权发表的文章,不转载网上的文章
所发的文章,均可找到原作者进行沟通。
也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投搞。
投稿请联系:
本文由 半桶水 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)
文章评论