[转] Swoft HTTP 服务

 

转载自Go语言中文网, https://studygolang.com/articles/20667php

传统架构

PHP-FPM + Nginx

传统架构中所使用的Nginx + PHP-FPM的模型中,Nginx因为基于Linux的epoll事件模型一个工做进程worker会同时去处理多个请求,可是PHP-FPM的工做进程fpm-worker却只能在同一时刻处理一个请求,并且fpm-worker工做进程每次处理请求前都须要从新初始化MVC框架而后再释放资源。当在高并发请求场景下时,fpm-worker是彻底不够用的,此时Nginx会直接响应502。另外,fpm-worker进程间的切换消耗也很大。golang

Reactor反应堆模型

PHP的FastCGI进程管理器PHP-FPM因为自己是同步阻塞进程模型,在请求结束后会释放掉全部资源,包括框架初始化建立的一系列对象,致使PHP进程空转并消耗大量的CPU资源,从而致使单机吞吐能力有限。简单来讲就是请求夯住会致使CPU不能释放资源大大浪费CPU使用率。shell

PHP-FPM进程模型属于预派生子进程模式,即来一个请求就会fork派生一个进程,进程的开销很是大从而大大下降吞吐率,另外并发量也只能由进程数决定。数据库

预派生子进程模式

预派生子进程模式是指程序启动后会建立多个进程,每一个子进程会进入Accept,等待新的链接进入。当客户端链接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,而且再也不接受新的TCP链接。当此链接关闭时子进程会释放,从新进入Accept参与处理新的链接。编程

预派生子进程模式的优点是彻底能够复用进程且无需太多的上下文切换,缺点是这种模型严重依赖进程的数量来解决并发问题。因为一个客户端链接须要占用一个进程,工做进程数量有多少并发处理能力就有多少,但是操做系统可以建立的进程数量都是有限的。bootstrap

PHP框架在初始化时会占用大量计算资源,而每一个请求都须要从新进行初始化。当启动大量进程时会带来额外的进程调度消耗,虽然数百个进程出现进程上下文切换调度消耗所占的CPU不足1%能够忽略不计,但同时启动成千上万个进程消耗会直接上升,调度消耗可能占满CPU。vim

另外,请求一个第三方接口会很是慢,请求过程当中会一直占用CPU资源,浪费昂贵的硬件资源。好比即时聊天程序的单机可能要维持数十万的链接,那么也就要启动数十万的进程,这显然是不可能的。那么,有没有 一种技术能够在一个进程内处理全部并发IO呢?答案是采用IO复用技术。bash

解决方案

那么有什么样的解决方案呢?服务器

经过业务分析不难发现,Web应用中90%以上的都是IO密集型业务,只要提升IO复用的能力就能够提高单机吞吐能力,另外须要将PHP-FPM的同步阻塞模式调整成异步非阻塞模式,也就能够解决核心的性能问题。swoole

如何提高IO复用能力呢?首先须要明白IO多路复用指的是什么,IO多路复用主要解决的问题是如何在一个进程中维持更多的链接数,这里的复用实际上指的是复用的线程。关于IO复用技术的历史实际上是和多进程同样长的,很早以前Linux就提供了select系统调用,它能够在一个进程内维持1024个链接。后来又加入了poll系统调用,poll作了一些改进解决了1024个链接限制的问题。但selectpoll存在的问题是它须要循环检测链接是否有事件。这样问题就来了,若是服务器上有100w个链接,某一时刻只有一个链接向服务器发送了数据,此时selectpoll就须要作100W次循环,其中只有1次是命中的,剩下的都是无效的,这不白白浪费了CPU的资源吗?直到Linux2.6内核提供了epoll系统调用才能够维持无限数量的链接,且无需轮询,这才真正解决了C10K问题。

如今各类高并发异步IO的服务器程序都是基于epoll实现的,好比NginxNode.jsErlangGolang... 像Node.jsRedis 这样单进程单线程的程序均可以维持超过100w的TCP链接,这所有要归功于epoll技术。

在IO密集型业务中须要频繁的上下文切换,若是采用线程模式开发会太过复杂,另一个进程中能开的线程数量也是有限的,线程太多会直接增长CPU的负责和内存资源。

线程自己是没有阻塞态的,当IO阻塞时也不会主动让出CPU资源,这种抢占式调度模式不太适合PHP开发。不过可使用全协程模式让同步代码异步执行来解决这个问题。

为何要使用Swoole呢?

Swoole的强大之处在于进程模型的涉及,既解决了异步问题又解决了并行。Swoole中提供了完整的协程(Coroutine)和通道(Channel)特性,带来全的CSP编程模型。应用层可使用彻底同步的编程方式,底层将自动实现异步IO。另外,使用常驻内存模式能够避免每次框架的初始化,节约了性能上的开销。

PHP应用的Web架构

  • LNMP
LNMP

Nginx做为Web服务器,PHP-FPM维护一个进程池去运行Web项目。LNMP模型的优势时简单、成熟、稳定,一次运行随后销毁带来的开发便捷性最大的特色。

PHP-FPM引入了进程常驻避免了每次请求建立和销毁进程时的性能开销并拓展了加载的开销,但每一个请求仍然要执行PHP RINT于RSHUTDOWN之间的全部流程,包括从新加载依次框架源码和项目代码,形成了极大的性能浪费。

  • LNMP + Swoole
LNMP+Swoole

LNMP+Swoole 是LNMP的一种变体,是在LNMP的基础上引入了Swoole组件。和PHP-FPM同样,Swoole有一套本身的进程管理机制,因为代码变得高度常驻,编程思惟须要从同步转变到异步。因此Swoole和传统基于PHP-FPM的Web框架亲和力很低。所以出现了这种折中方案,并无直接将原有PHP代码运行在Swoole中,而是使用Swoole搭建了一个服务,而是使用Swoole搭建了一个服务,系统经过接口与Swoole通讯,从而为Web项目补充了异步处理能力。

LNMP+Swoole虽然引入了Swoole和异步处理能力,但核心仍然是PHP-FPM,实际上并无发挥出Swoole的真正优点。

  • Swoole HTTP Server
Swoole HTTP Server

Swoole HTTP Server与LNMP+Swoole相比有着巨大的变化,这种模型中充当Web服务器角色的构件不只仅有Ngnix,应用自己也包含了一个内建的Web服务器,不过因为Swoole HTTP Server不是专业的HTTP服务器,对HTTP的处理不完善,所以仍然须要使用Nginx做为静态资源服务器及反向代理,Swoole HTTP Server仅处理PHP相关的HTTP流量。

因为Swoole已经包含了Web服务器,再也不须要实现CGI或FastCGI的通用网关协议和Web服务器进行通讯。另外一方面Swoole有本身的进程管理,所以PHP-FPM能够直接被去除了。对于PHP资源而言,Swoole HTTP Server至关于Nginx + PHP-FPM。

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

Swoole 与 Swoft 的关系

Swoft与Swoole的关系是什么?

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

Swoft管理着Swoole和Swoole HTTP Server,对开发者屏蔽Swoole的各类复杂操做细节,并做为一个Web框架向开发者提供了各类Web开发所需的路由、MVC、数据库访问等功能组件等。

Swoft是如何使用Swoole的呢?

Swoft直接使用的是Swoole内建的\Swoole\Http\Server,HTTP服务器已经处理好了全部HTTP层面的东西,剩下只需考虑关注应用自己。

HTTP服务生命周期

Swoft的HTTP服务是基于\Swoole\Http\Server实现的协程HTTP服务,Swoft框架层封装了MVC方便编码以获取协程带来的超高性能。

Swoft HTTP服务器启动会根据.env环境配置中的设置,在使用composer install安装组件时会自动复制环境变量配置文件.env,若没有可手工复制.env.sample并重命名为.env

$ .env # HTTP 服务设置 HTTP_HOST=0.0.0.0 HTTP_PORT=80 HTTP_MODE=SWOOLE_PROCESS HTTP_TYPE=SWOOLE_SOCK_TCP 

HTTP服务器启动命令

// 启动服务,根据.env环境配置决定是否为守护进程方式(daemonize)。
$ php bin/swoft start // 之后台后台进程方式启动 $ php bin/swoft start -d // 重启服务 $ php bin/swoft start restart // 从新加载 $ php bin/swoft reload // 关闭服务 $ php bin/swoft stop 

Swoft框架是创建在Swoole扩展之上运行的,在Swoft服务启动阶段,首先须要关注的是OnWorkStart事件,此事件会在Worker工做进程启动的时候触发,这个过程也是Swoft众多机制实现的关键,此时Swoft会进行扫描目录、读取配置、收集注解、收集事件监听器...。而后会根据扫描到的注解信息执行对应的功能逻辑,并存储在与注解对应的Collector容器内,包括注册路由、注册事件监听器、注册中间件、注册过滤器等。

在Swoole启动前的重要行为特征

  1. 基础bootstrap行为,如必要的常量定义、Composer加载器引入,读取配置。
  2. 生成被全部worker/task进程共享的程序全局期的对象,如Swoole\LockSwoft\Memory\Table的建立。
  3. 启动时全部进程中只能执行一次的操做,如前置Process的启动。
  4. Bean容器基本初始化以及项目启动流程须要的coreBean的加载

和HTTP服务关系最密切的进程是Swoole中Worker进程,绝大部分业务处理都在Worker工做进程中。对于每一个Swoole事件,Swoft都提供了对应的Swoole监视器(对应@SwooleListener注解)做为事件机制的封装。

要理解Swoft的HTTP服务器是如何在Swoole下运行,重点须要关注两个Swoole事件swoole.workerStartswoole.onRequest

swoole.workerStart事件

workerStart事件在TaskWorker/Worker进程启动时发生,每一个TaskWorker/Worker进程里都会执行一次,这是个关键节点,由于swoole.workerStart回调以后新建的对象都是进程全局期的,使用的内存都属于特定的Task\Worker进程,相互独立。也只有在这个阶段或之后初始化的部分才是能够被热重载的。

$ vim /vendor/swoft/framework/src/Bootstrap/Server/ServerTrait.php 
<?php namespace Swoft\Bootstrap\Server; use Swoft\App; use Swoft\Bean\BeanFactory; use Swoft\Bean\Collector\ServerListenerCollector; use Swoft\Bootstrap\SwooleEvent; use Swoft\Core\ApplicationContext; use Swoft\Core\InitApplicationContext; use Swoft\Event\AppEvent; use Swoft\Helper\ProcessHelper; use Swoft\Pipe\PipeMessage; use Swoft\Pipe\PipeMessageInterface; use Swoole\Server; /** * Server trait */ trait ServerTrait { /** * OnWorkerStart event callback * * @param Server $server server * @param int $workerId workerId * @throws \InvalidArgumentException * @throws \ReflectionException */ public function onWorkerStart(Server $server, int $workerId) { // Init Worker and TaskWorker $setting = $server->setting; $isWorker = false; if ($workerId >= $setting['worker_num']) { // TaskWorker ApplicationContext::setContext(ApplicationContext::TASK); ProcessHelper::setProcessTitle($this->serverSetting['pname'] . ' task process'); } else { // Worker $isWorker = true; ApplicationContext::setContext(ApplicationContext::WORKER); ProcessHelper::setProcessTitle($this->serverSetting['pname'] . ' worker process'); } $this->beforeWorkerStart($server, $workerId, $isWorker); $this->fireServerEvent(SwooleEvent::ON_WORKER_START, [$server, $workerId, $isWorker]); } /** * @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); } } } 

reloadBean方法做为实践底层关键代码主要完成三件事:

  • 初始化Bean容器

BeanFactory::reload()是Swoft的Bean容器初始化入口,注解的扫描也是在此处进行的,准确来讲,Bean容器真正的初始化阶段在Swoole服务器启动前的Bootstrap阶段就已经进行了,只不过那时进行的是少部分的初始化,相对swoole.workerStart中初始化的Bean数量比重还很小。在workerStart中初始化Bean容器是Swoft能够热更代码的基础。

  • 初始化应用的上下文

initApplicationContext->init()会注册Swoft事件监听器(对应@Listener注解),方便用户处理Swoft应用自己的各类钩子。随后触发一个swoft.applicationLoader事件,各组件经过该事件进行配置文件加载,以及HTTP/RPC路由注册。

  • 服务注册

swoole.onRequest事件

Swoft的请求和响应实现了PSR-7,请求和响应对象存在于每次HTTP请求,这里的请求对象Request指的是Swoft\Http\Message\Server\Request,响应Response指的是Swoft\Http\Message\Server\Response

每一个请求从开始到结束都是由Swoole自己的onRequest方法或onResponse方法事件监听并委托给Dispatcher方法来处理并响应的,Dispatcher方法的主要职责是负责调度请求生命周期内的各个组件。

HTTP服务中将由ServerDispather来负责调度,参与者包括RequestContextRequestHandlerExceptionHandler

  • RequestContext 请求上下文做为当前请求信息的容器将贯穿整个请求生命周期,负责信息的存储和传递。
  • RequestHandler 请求处理器是整个请求生命周期的核心组件,其实也就是个中间件Middleware,该组件实现了PSR-15协议。
    • 负责将Request=>Route=>Controller=>Action=>Renderer=>Response整个请求流程贯穿起来,也就是从请求Request到响应Response的过程
    • 只要在任意一个环节中返回一个有效的响应对象Response就能对该请求作出响应并返回
  • ExceptionHandler 异常处理器是在遇到异常的状况下出来收拾场面的,确保在各类异常状况下依旧能给客户端返回一个预期内的结果
 
 
 
 
 
 
 
 
请求生命周期

每一个HTTP请求到来时仅仅会触发swoole.onRequest事件,Swoft框架自己是由大量进程全局期和少许程序全局期的对象构成。onRequest中建立的对象好比$request$response都是请求期的,随着HTTP请求的结束而回收。

$ vim /vendor/swoft/http-server/src/ServerDispatcher.php 
<?php namespace Swoft\Http\Server; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Swoft\App; use Swoft\Contract\DispatcherInterface; use Swoft\Core\ErrorHandler; use Swoft\Core\RequestContext; use Swoft\Core\RequestHandler; use Swoft\Event\AppEvent; use Swoft\Http\Message\Server\Response; use Swoft\Http\Server\Event\HttpServerEvent; use Swoft\Http\Server\Middleware\HandlerAdapterMiddleware; use Swoft\Http\Server\Middleware\SwoftMiddleware; use Swoft\Http\Server\Middleware\UserMiddleware; use Swoft\Http\Server\Middleware\ValidatorMiddleware; /** * The dispatcher of http server */ class ServerDispatcher implements DispatcherInterface { /** * Do dispatcher * * @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; } } 

事件底层关键代码

  • beforeDispatch($request, $response) 设置请求上下文并触发一个swoft.beforeRequest事件。
  • RequestHandler->handle($request) 执行各个中间件和请求对应的动做方法action
  • $afterDispatch($response) 整理HTTP响应报文发送客户端并触发swoft.resourceRelease事件和swoft.afterRequest事件。

在HTTP服务器的生命周期中须要重点理解

  • Swoole的Worker进程是绝大多数HTTP服务代码的运行环境
  • 部分初始化和加载操做在Swoole服务器启动前完成,部分在swoole.workerStart事件回调中完成,前者没法热重载但能够被多个进程共享。
  • 初始化代码只会在系统启动和Worker/Task进程启动时执行一次,不像PHP-FPM每次请求都会执行一次,框架对象不像PHP-FPM会请求返回而销毁。
  • 每次请求都会触发一次swoole.onRequest事件,事件中是请求处理代码真正运行的位置,只有事件内产生的对象才会在请求结束时被回收。
相关文章
相关标签/搜索