swoole4.0之打造自己的web开发框架(7)

2019年1月5日 382点热度 0人点赞 0条评论

上一篇:swoole4.0之打造自己的web开发框架(6),我们符合了PSR7规范以及模板引擎的引入,框架的能力更加全面了


之前在swoole4.0之打造自己的web开发框架(4)文章中,实现一个完整的CRUD中有一个bug,被一个盆友发现了:

图片

确实是当时手快,有所疏忽,不过这确实是一个很典型的问题,有必要在详细说一说了


问题产生的原因就是:用了全局单例,而且mysql连接获取是在构造函数(__construct)里执行的,导致第一次被初始化之后,后续所有的请求都复用了mysql连接,这样有并发请求会出错,且不能达到请求完成资源归还的目的,也失去了连接池的意义


所以给出的修复方案是:

/**
*
@return Mysql
*
@throws \Exception
*/
public function getDb()
{
   $coId = Coroutine::getId();
   if (empty($this->dbs[$coId])) {
       //不同协程不能复用mysql连接,所以通过协程id进行资源隔离
       //达到同一协程只用一个mysql连接,不同协程用不同的mysql连接
       
if ($this->dbTag) {
           $mysqlConfig = Config::get($this->dbTag);
       } else {
           $mysqlConfig = null;
       }
       $this->dbs[$coId] = MysqlPool::getInstance($mysqlConfig)->get();
       defer(function () {
           //利用协程的defer特性,自动回收资源
           
$this->recycle();
       });
   }
   return $this->dbs[$coId];
}

把获取连接单独拎出来,然后在需要的时候调用获取

$result = $this->getDb()->query($query);

这个bug隐含了swoole代码里各维度生命周期问题,在swoole世界里有以下几个维度

PHP维度的生命周期

这里我们不探讨zendVM的细节,PHP不管哪种运行方式(CLI, FPM, Apache Moudle...)都是遵循SAPI接口, 下图描述了一个php执行的基本流程

图片

抽象出来就是下面5个阶段:


1、模块初始化阶段(Module init)     :

           即调用每个拓展源码中的的PHP_MINIT_FUNCTION中的方法初始化模块,进行一些模块所需变量的申请,内存分配等。
        

2、请求初始化阶段(Request init)  :
           即接受到客户端的请求后调用每个拓展的PHP_RINIT_FUNCTION中的方法,初始化PHP脚本的执行环境。
       

 3、执行PHP脚本

       

 4、请求结束(Request Shutdown) :
          这时候调用每个拓展的PHP_RSHUTDOWN_FUNCTION方法清理请求现场,并且ZE开始回收变量和内存。
       

 5、关闭模块(Module shutdown)     :
           Web服务器退出或者命令行脚本执行完毕退出会调用拓展源码中的PHP_MSHUTDOWN_FUNCTION 方法


以FPM为例,FPM启动时,执行第一步,后续每个请求进来,结束,在重复2~4步,FPM关闭时,执行第5步

在FPM模式下,每个进程同时只能执行一个请求,全局变量,各类资源都会在第4步进行释放,而每个请求也是重复加载文件,资源初始化等,优点就是开发简单,没有上下文关系,所有的资源都圈定在请求以内,缺点就是性能差,需要做很多重复性的工作


swoole server维度的生命周期

swoole在第3个阶段,就会接管了php的生命周期,而swoole也有类似的5个阶段(以http  server为例):

1、onStart  :
           swoole server启动


2、onWorkerStart
           swoole的 worker进程程启动,有点类似于Minit
       

3、onRequest

           请求到达到的处理回调, 可以认为是 2~4步的集合
        

4、onWorkerStop
          工作进程结束时的回调,类似于MSHUTDOWN
        

5、onShutdown    :
          swoole server关闭时回调,此方法调用之后,才会进入到 PHP的 4 - 5 阶段


swoole onRequest(协程)维度的生命周期

通过对前面两个生命周期的介绍,我们知道在onRequest或swoole协程中,全局变量,资源等是跨请求存在的,因为并不会执行到php的4以后阶段做资源的清理过程,所以在swoole协程中,我们需要有上下文,需要通过 Coid(协程id)进程资源隔离,需要在协程处理完成做一些资源的清理,以防止内存泄露等问题, 当然利用这个,我们也才可以做跨请求的资源共享,可以做各种资源池,可以避免每个请求都做重复的功作,从而大大的提升性能。

单例的陷阱

所以,在swoole代码里,单例要做更细粒度的区分,如果按照之前FPM里的方式来,那很容易就出问题了,这里总结为三个粒度:

1、进程粒度

2、请求粒度

3、协程粒度

那什么场景用什么单例呢?

1、进程级别的单例:无任何共享数据,或者是可以进程间共享(跨请求)的数据(如全局变量),这个级别和FPM一致,大部分情况都是这个级别,特别如框架级别的代码


2、请求级别的单例:请求内可共享的数据,如:waitgroup做并发,就是请求内可以数据共享的, 大部分情况不需要使用,可以通过进程级别单例+数据隔离实现


3、协程级别的单例:请粒度最小,能是协程内的数据共享,这种情况基本不建议使用单例了


4、进程级别可数据隔离单例: 如果我们的单例有非跨请求的数据或资源,那么这个数据和资源就不用能static变量,也不能在构造函数里初始化

下面给出这几个单例的代码实现:

<?php
//file frame/Family/Core/Singleton.php
namespace Family\Core;


use Family\Coroutine\Coroutine;

trait Singleton
{
   private static $instance;
   private static $coInstances;

   /**
    *
@param mixed ...$args
    *
@return mixed
    *
@desc 进程内的全局单例
    */
   
public static function getInstance(...$args)
   {
       if (!isset(self::$instance)) {
           self::$instance = new static(...$args);
       }
       return self::$instance;
   }

   /**
    *
@param mixed ...$args
    *
@return mixed
    *
@desc 协程内的单例
    */
   
public static function getCoInstance(...$args)
   {
       $coId = Coroutine::getId();
       if (!isset(self::$coInstances[$coId])) {
           self::$coInstances[$coId] = new static(...$args);
           defer(function () use ($coId) {
               unset(self::$coInstances[$coId]);
           });
       }

       return self::$coInstances[$coId];

   }

   /**
    *
@param mixed ...$args
    *
@return mixed
    *
@desc 请求级别的单例,用此方法:
    *
@desc 同一请求内开协程,需要调用Family\Coroutine\Coroutine::create方法创建
    *
@desc 不能直接用祼go创建
    */
   
public static function getRequestInstance(...$args)
   {
       $coId = Coroutine::getPId();
       if (!isset(self::$coInstances[$coId])) {
           self::$coInstances[$coId] = new static(...$args);
           defer(function () use ($coId) {
               unset(self::$coInstances[$coId]);
           });
       }
       return self::$coInstances[$coId];
   }

   /**
    *
@param mixed ...$args
    *
@return mixed
    *
@desc 按tag获取单例, 可用于static替换
    */
   
public static function getInstanceByTag(...$args)
   {
       $tag = $args[0];
       if (!isset(self::$coInstances[$tag])) {
           self::$coInstances[$tag] = new static(...$args);
       }
       return self::$coInstances[$tag];
   }
}

期望这篇文章能帮助大家更理解php和swoole

下一篇将介绍:如何实现在代码热更新,以及代码热更新的范围,以及服务管理脚本的实现

查看原文,介绍两本好书:PHP 7底层设计与源码实现+PHP7内核剖析,有兴趣可以看看或购买

github地址: https://github.com/shenzhe/family

 ----------伟大的分割线-----------

PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!

饭米粒只发原创或授权发表的文章,不转载网上的文章

所发的文章,均可找到原作者进行沟通。

也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投搞。

投稿请联系:

[email protected]

本文由 半桶水 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)

图片

35460swoole4.0之打造自己的web开发框架(7)

这个人很懒,什么都没留下

文章评论