80

Swoft 源码剖析 - Swoole和Swoft的那些事 (Http/Rpc服务篇)

 5 years ago
source link: https://segmentfault.com/a/1190000015603847?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:bromine

链接: https://www.jianshu.com/p/4c0...

來源:简书

著作权归作者所有,本文已获得作者授权转载,并对原文进行了重新的排版。

Swoft Github: https://github.com/swoft-clou...

Swoft源码剖析系列目录: https://segmentfault.com/a/11...

前言

SwoftPHPer 圈中是一个门槛较高的 Web 框架,不仅仅由于框架本身带来了很多新概念和前沿的设计,还在于 Swoft 是一个基于 Swoole 的框架。 SwoolePHPer 圈内学习成本最高的工具没有之一,虽然 Swoft 的出现降低了 Swoole 的使用成本,但如果你对 Swoole 本身了解不够深入,仍然很难避免栽进种种 "坑" 中。

考虑到这个现状,也为降低阅读难度,后续几个和 Swoole 联系较为密切的机制,笔者会调整写作思路,将文章的定位从 「帮助读者深入理解Swoft」 调整为 「帮助读者理解Swoft和Swoole」 ,叙述节奏也会放慢。

三种PHP应用的Web模型

RNRRZja.png!web

LNMPLAMP 是绝大多数 PHPer 最熟悉的基础Web架构,这里以常见的 LNMP 作为例子描述一个常见 无Swoole 应用的构件组成: Nginx 充当 Web Service , PHP-FPM 维护一个进程池去运行 Web 项目。

对比更古老的 CGI 模型, PHP-FPM 已经引入了进程常驻的概念,避免每次请求创建并销毁进程的开销以及拓展加载的开销,但是每个请求仍然要执行PHP RINITRSHUTDOWN 之间的所有流程,包括重新加载一次框架源码以及项目代码,造成极大的性能浪费。

这种模型的优点是简单成熟和稳定, 一次运行随后销毁 带来的开发便捷性是 PHP 能够流行起来的原因之一。市面上绝大多数 PHP 项目使用的都是基于该种架构的变体。

RNRRZja.png!web

LNMP-with-SwooleLNMP 的一种变体,其在 LNMP 的基础上引入了 Swoole 组件。

PHP-FPM 一样, Swoole 有一套自己的进程管理机制。但由于代码变得高度常驻和编程思维需要从同步到异步的转变,所以 Swoole 和传统的基于 PHP-FPMWeb 框架亲和度很低,即使是适配升级过的老式 Web 框架,目前在 Swoole 上运行的表现往往并不好。

因此出现了这在这种折中方案,并没有直接将原有 PHP 代码运行在 Swoole 中,而是使用 Swoole 搭建了一个服务,系统通过接口与 Swoole 通信,从而为 Web 项目补充了异步处理的能力。我称呼这种同时使用 PHP-FPMSwoole 的系统为 半 Swoole 应用。因为接入简单,所以是绝大多数现有项目优先考虑的Swoole接入方案。

LNMP-with-Swoole模型虽然引入了 Swoole 和异步处理能力,但是核心还是 PHP-FPM ,实际上还远远没有发挥出 Swoole 的真正优势。

RNRRZja.png!web

Swoole-HTTP-ServerLNMP-with-Swoole 相比有巨大的变化,这种模型中充当 Web Server 角色的构件不仅仅有 Nginx ,应用本身也包含了一个内建 Web Server ,不过由于 Swoole Http Server 不是专业的 HTTP Server ,对 Http 的处理不完善 ,因此仍然需要使用Nginx作为静态资源服务器以及反代, Swoole HTTP Server 仅仅处理 PHP 相关的 HTTP 流量。

一方面由于 Swoole 已经包含了 WebServer ,不再需要实现 CGI 或者 Fast-CGI 的通用协议去和 Web Server 通信,另一方面 Swoole 有自己的进程管理,因此 PHP-FPM 可以直接被去除了。对于 PHP 资源而言,在这种模型中, Swoole Http Server 的地位相当于传统模型中的 NginxPHP-FPM 之和。

一次加载常驻内存,不同的请求间基本上复用了 onRequest 以外的所有流程,使得每个请求的开销大大降低; 异步IO 的特性使得这种模型 吞吐量远远高于传统的 LNMP模型 。另外相对于独立的 Swoole 服务,内嵌在 Web 系统中的 Swoole 使用更加的直接方便,支持更好。

Swoft 和 Swoole 的关系是什么 ?

  1. Swoole 是一个异步引擎,核心是为 PHP 提供 异步IO 执行的能力 ,同时提供一套异步编程可能会用到的工具集。
  2. Swoole-HTTP-ServerSwoole 的一个组件,是 SwooleServer 中的一种,提供了一个适合 Swoole 直接运行的 HttpServer 环境。
  3. Swoft 一个 现代的Web框架Swoole 亲和性高 ,同时也是上面提到的 Swoole-HTTP-Server 模型的一个实践。

Swoft 管理着该 Web 模型中的 Swoole ,以及 Swoole-HTTP-Server 对开发者屏蔽 Swoole 的种种复杂操作细节 ,并作为一个 Web框架 向开发者提供各种 Web开发 需要用到的 路由MVC数据库访问 等功能组件等。

Swoft 是如何使用 Swoole 的 ?

最核心的就是 HttpServer 以及 RpcServer

HTTP 服务器

Swoft 直接使用的是 Swoole 内建的 \Swoole\Http\Server ,它已经处理好所有 HTTP 层面的所有东西,我们只需要关注应用本身,我们来看一下 HTTP 服务几个重要生命周期点。

Swoole 启动前

这个阶段进行的行为有几个特征

  1. 基础 bootstrap 行为:如必须的常量定义, Composer 加载器引入,配置读取等;
  2. 需要生成被所有 Worker/Task 进程共享的程序全局期的对象,如 Swoole\Lock , Swoft\Memory\Table 的创建;
  3. 启动时,所有进程中合计只能执行一次的操作:如前置 Process 的启动;
  4. Bean 容器基本初始化,以及项目启动流程需要的 coreBean 的加载。

这块涉及东西比较杂,为控制篇幅后续用单独文章介绍。

Http 服务关系最密切的进程是 Swoole 中的 Worker进程(组) ,绝大部分业务处理都在该进程中进行。

对于每个 Swoole事件Swoft 都提供了对应的 Swoole监听器 (对应 @SwooleListener 注解)作为事件机制的封装。要理解 SwoftHttpServer 是如何在 Swoole 下运行的我们重点需要关注下两个在两个 Swoole 事件 swoole.workerStartswoole.onRequest

swoole.workerStart 事件

WorkerStart 事件在 TaskWorker/Worker 进程启动时发生,每个 TaskWorker/Worker 进程里都会执行一次。

这是个关键节点,因为 swoole.workerStart 回调之后新建的对象都是进程全局期的,使用的内存都属于特定的 Task/Worker 进程,相互独立。也只有在这个阶段或以后初始化的部分才是可以被热重载的。

事件底层关键代码如下:

// Swoft\Bootstrap\Server\ServerTrait.php
/**
 * @param bool $isWorker
 * @throws \InvalidArgumentException
 * @throws \ReflectionException
 */
protected function reloadBean(bool $isWorker)
{
    BeanFactory::reload();
    $initApplicationContext = new InitApplicationContext();
    $initApplicationContext->init();

    if($isWorker && $this->workerLock->trylock() && env('AUTO_REGISTER', false)){
        App::trigger(AppEvent::WORKER_START);
    }
}

这里做的事情有3点

  1. 初始化 Bean 容器:
    上文中的 BeanFactory::reload(); 就是 SwoftBean 容器初始化入口,注解的扫描也是在此处进行(实际上这个说法并不准确, Bean 容器真正的初始化阶段在 Swoole Server 启动前的 BootStrap 阶段就已经进行了,只不过那时进行的是少部分初始化,相对 swoole.workerStart 中的初始化的 Bean 数量,比重很小)。在 workerStart 中初始化 Bean 容器是 Swoft 可以热更新代码的基础。
  2. 初始化的应用上下文
    initApplicationContext->init() 会注册 Swoft 事件监听器(对应 @Listener ),方便用户处理 Swoft 应用本身的各种钩子。随后触发一个 swoft.applicationLoader 事件,各组件通过该事件进行配置文件加载, HTTP/RPC 路由注册。
  3. 服务注册
    具体内容会在服务治理章节讲述。

swoole.onRequest 事件

每个 HTTP 请求到来时仅仅会触发 swoole.onRequest 事件。

框架代码本身都是由大量进程全局期和少量程序全局期的对象构成,而 onReceive 中创建的对象譬如 $request$response 都是请求期的,随着 HTTP 请求的结束而回收。

事件底层关键代码如下:

/**
 * @param array ...$params
 * @return \Psr\Http\Message\ResponseInterface
 * @throws \InvalidArgumentException
 */
public function dispatch(...$params): ResponseInterface
{
    /**
     * @var RequestInterface $request
     * @var ResponseInterface $response
     */
    list($request, $response) = $params;

    try {
        // before dispatcher
        $this->beforeDispatch($request, $response);

        // request middlewares
        $middlewares = $this->requestMiddleware();
        $request = RequestContext::getRequest();
        $requestHandler = new RequestHandler($middlewares, $this->handlerAdapter);
        $response = $requestHandler->handle($request);

    } catch (\Throwable $throwable) {
        /* @var ErrorHandler $errorHandler */
        $errorHandler = App::getBean(ErrorHandler::class);
        $response = $errorHandler->handle($throwable);
    }

    $this->afterDispatch($response);

    return $response;
}
  1. beforeDispatch($request, $response):
    设置请求上下文,并触发一个 swoft.beforeRequest 事件。
  2. RequestHandler->handle($request):
    执行各个 中间件 和请求对应的 Action ,具体处理可以参考RPC章节,原理基本相同。
  3. $afterDispatch($response):
    整理 HTTP 响应报文发送客户端并触发 swoft.resourceRelease (详情在连接池一文中提及)事件和 swoft.afterRequest 事件

总的来说,纵观这几个生命周期点你需要搞清楚几件事:

  1. SwooleWorker 进程是你绝大多数 HTTP 服务代码的运行环境。
  2. 一部分初始化和加载操作在 SwooleServer 启动前完成,一部分在 swoole.workerStart 事件回调中完成,前者无法热重载但可能被多个进程共享。
  3. 初始化代码只会在系统启动和 Worker/Task 进程启动时执行一次, 不像 PHP-FPM 每次请求都会执行一次,框架对象也不像 PHP-FPM 会随请求返回而销毁。
  4. 每次请求都会触发一次 swoole.onRequest 事件,里面就是我们的请求处理代码真正运行的地方,只有这事件内产生的对象才会在请求结束时被回收。

RPC服务器

生命周期和 HTTP服务 基本一致,详情参考 《Swoft源码剖析-RPC功能实现》


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK