在使用yii2做网站开发时,是鉴于它上手速度快,拥有强大的组件和丰富的类库。以前做过些Android系统层、APP、流媒体等,而在接触yii2后发现,这个的开发速度确实快,虽然是个做网站的PHP框架,没有可比性,不过看了很多博客对比过yii2、laravel、phalcon,yii2的开发速度比后几个能提升几个等级。(话说以后有时间体验下后面两个,看是不是真的~)
废话不多说,进入正题。做网站开发时,发现有很多后端功能需要一直在运行,起初是使用的crontab定时执行,后来功能越来越多crontab已经不能满足需求。比如网站的一些发送短信、邮件等功能,都需要curl经过网络通信的,网络不好、高并发时不异步处理会阻塞进程,所以考虑引入些开源的异步多线程服务swoole。
引入swoole的目的是只用它创建一个server异步服务器,对数据库等功能性操作使用原来的yii2框架。以下主要从swoole定时器、tcp的异步多线程通信做简单介绍。
swoole是目前比较新的一个php异步处理框架,由国内人员开发,”新“->就会导致它前期版本有很多bug,不过现在提供的几个版本已经很稳定了。话说刚开始接触yii2做网站时,连个socket通信都没~感觉怪怪的,接触swoole后发现,终于找回了些以前做c/c++的感觉。(哦~原来你在这里!)
借用官网的定义swoole重新定义了PHP,它是PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。
想了解更多api的话,请点击这里。
由于swoole服务端只能运行在cli模式下(有兴趣的可以了解下PHP的运行模式cli命令行运行和FastCGI常驻型CGI,后者就是我们常说的fpm),而yii2的console支持控制台命令,这样就可以通过脚本启动swoole的server,和一个yii2的console应用将两者结合起来。
通过脚本启动一个tcp的swoole_server,和一个yii2的终端应用。
$this->app = new SwooleYiiApplication();
$this->serv = new swoole_server("127.0.0.1", CC_SWOOLE_PORT );
$this->serv->set(array(
'worker_num' => 8,
'task_worker_num' => 8,
'daemonize' => 1,
'max_request' => 10000,
'dispatch_mode' => 2,
'debug_mode'=> 1,
'log_file' => __DIR__."/runtime/swoole.log",
));
$this->serv->start();
SwooleYiiApplication的构造如下:
class SwooleYiiApplication {
public function __construct()
{
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');
defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
defined('STDOUT') or define('STDOUT', fopen('php://stdout', 'w'));
$config = ArrayHelper::merge(
require(__DIR__ . '/../common/config/main.php'),
require(__DIR__ . '/../common/config/main-local.php'),
require(__DIR__ . '/config/main.php'),
require(__DIR__ . '/config/main-local.php')
);
$this->app = new \yii\console\Application($config);
}
...
}
server端首先会创建一个master进程(pid=9747,可在onStart中通过swoole_set_process_name重命名进程),保证swoole框架的运行,从下图可以看出,它创建了一个pid=9748进程,这是个manager进程,他是所有worker/task进程的父进程,它的主要作用是回收子进程,避免僵尸进程,并在服务器关闭时发送signal通知子进程关闭(可以kill -9 9748会发现其它work进程都会关闭)。剩下的是8个worker进程和8个task进程,用于异步处理来自client端的数据。
root 9747 1 0 Apr15 ? 00:00:01 masterSwooleServer
root 9748 9747 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9758 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9759 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9760 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9761 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9762 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9763 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9764 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9765 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9766 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9767 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9768 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9769 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9770 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9771 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 9772 9748 0 Apr15 ? 00:00:00 php swoole/SwooleServer.php
root 22094 9748 0 10:22 ? 00:00:13 php swoole/SwooleServer.php
我们将server端启动后,可以在master进程中创建定时器,用来代替(crontab)定时执行php后台的某些功能。$worker_id=0表示是在master进程中,如果不加这个条件,每次的worker进程创建都会回调onWorkerStart,也就是说你会创建9个定时器。
在定时器中就可以用之前创建yii2的console app对象去执行我们相应的功能。
public function onWorkerStart( $serv , $worker_id) {
// 在Worker进程开启时绑定定时器
// 只有当worker_id为0时才添加定时器,避免重复添加
if( $worker_id == 0 ) {
//1.7.19版本
//$serv->addtimer(1000);
//1.8.2版本
swoole_timer_tick(1000, function(){
SwooleServer::$timerCounter += 1;
$this->app->runAction('swoole-tick',[ 1000, SwooleServer::$timerCounter]);
});
}
}
在处理前端高并发异步任务时,前端通过创建client端将消息发送到server,由server端的worker进程处理,在worker进程中可通过console对象去处理yii的功能/或者执行自己的异步任务。由于前面配置了8个worker进程,swoole中会有8个worker进程的receive处理,如果并发请求超过8个,swoole会按照队列方式处理。如果你要处理的内容耗时很长或者并发量很大,可以在worker进程receive后丢到task进程去处理。
public function onReceive( swoole_server $serv, $fd, $from_id, $data ) {
if( $data == 'exit' ) {
$serv->shutdown();
}else{
$this->app->runAction('schedule/send-notice-msg-socket',[ $data ]);
}
}
client端主要用于处理yii2前端网站耗时的功能(发短信/邮件等),将消息发送给server由server接受到消息后去处理。
起先使用的方法是 $client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC); //异步非阻塞
由于异步非阻塞链接需要运行在cli模式下,网站fpm请求会报错,后来通过system去执行client创建链接,这种相当于每个客户端又创建一个新进程,性能太低,固舍去此方法。
考虑到client端和server端都是在一台服务器上,走同步tcp本地socket也很快,并且同步swoole_client支持在fpm下运行。优化后采用长链接方式去做client端,防止自己处理fd出现bug,client端也不用断开,fd重复利用。
$client = new swoole_client(SWOOLE_SOCK_TCP | SWOOLE_KEEP);
$client->connect('127.0.0.1', CC_SWOOLE_PORT, 0.5);
$client->send($msg);
以上是通过tcp通信做的,swoole还增加了一个进程管理模块,用来替代PHP的pcntl扩展,api封装的简单易用,server端可以通过swoole_process创建的子进程中去处理功能,前端通过swoole_process->useQueue消息队列模式,将消息swoole_process->push(string $data)到队列中,server端取队列处理数据。具体接口可以查看这里。
server端的挂掉自启动,前期swoole版本会经常内部库堆栈崩溃,导致服务器挂掉,影响线上功能,故配置了server的定时检测功能,以下是在crontab中配置的检测服务器运行状态的,也可以自己写一个单独的守护进程脚本去运行。
#!/usr/bin/env sh
processCount=`ps -ef | grep masterSwooleServer | grep -v grep | wc -l`
if [ $processCount -lt 1 ];
then
ps -eaf |grep "SwooleServer.php" | grep -v "grep"| awk '{print $2}'|xargs kill -9
sleep 2
ulimit -c unlimited
php /mnt/color/yii2-app-advanced/swoole/SwooleServer.php
echo "restart";
fi
yii2的console应用的启动时机,切记new SwooleYiiApplication()一定要放在$this->serv->start()之前,话说这个问题坑了我好久,如果放在start之后启动,则worker进程中的每个进程都会去创建一个console应用,这样你的console对象就不唯一了,在receive中调用action时会报null。
正确:
$this->app = new SwooleYiiApplication();
...
$this->serv->start();
错误:
...
$this->serv->start();
$this->app = new SwooleYiiApplication();