Yii2的异步多线程服务之swoole

背景


在使用yii2做网站开发时,是鉴于它上手速度快,拥有强大的组件和丰富的类库。以前做过些android系统层、APP、流媒体等,而在接触yii2后发现,这个的开发速度确实快,虽然是个做网站的php框架,没有可比性,不过看了很多博客对比过yii2、laravel、phalcon,yii2的开发速度比后几个能提升几个等级。(话说以后有时间体验下后面两个,看是不是真的~)

废话不多说,进入正题。做网站开发时,发现有很多后端功能需要一直在运行,起初是使用的crontab定时执行,后来功能越来越多crontab已经不能满足需求。比如网站的一些发送短信、邮件等功能,都需要curl经过网络通信的,网络不好、高并发时不异步处理会阻塞进程,所以考虑引入些开源的异步多线程服务swoole。

目的

引入swoole的目的是只用它创建一个server异步服务器,对数据库等功能性操作使用原来的yii2框架。以下主要从swoole定时器、tcp的异步多线程通信做简单介绍。

swoole介绍

swoole是目前比较新的一个php异步处理框架,由国内人员开发,”新“->就会导致它前期版本有很多bug,不过现在提供的几个版本已经很稳定了。话说刚开始接触yii2做网站时,连个socket通信都没~感觉怪怪的,接触swoole后发现,终于找回了些以前做c/c++的感觉。(哦~原来你在这里!)

借用官网的定义swoole重新定义了PHP,它是PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。

想了解更多api的话,请点击这里。

Yii2中swoole使用

server端

由于swoole服务端只能运行在cli模式下(有兴趣的可以了解下PHP的运行模式cli命令行运行和FastCGI常驻型CGI,后者就是我们常说的fpm),而yii2的console支持控制台命令,这样就可以通过脚本启动swoole的server,和一个yii2的console应用将两者结合起来。

Created with Raphaël 2.1.0 脚本执行php 创建yii的console应用和swoole_server swoole_server创建worker进程接受数据 通过console对象处理action End

通过脚本启动一个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端

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端取队列处理数据。具体接口可以查看这里。

问题总结

  1. 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
  2. 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();

你可能感兴趣的:(yii2学习)