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

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

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个链接限制的问题。但select
和poll
存在的问题是它须要循环检测链接是否有事件。这样问题就来了,若是服务器上有100w个链接,某一时刻只有一个链接向服务器发送了数据,此时select
和poll
就须要作100W次循环,其中只有1次是命中的,剩下的都是无效的,这不白白浪费了CPU的资源吗?直到Linux2.6内核提供了epoll
系统调用才能够维持无限数量的链接,且无需轮询,这才真正解决了C10K问题。
如今各类高并发异步IO的服务器程序都是基于epoll
实现的,好比Nginx
、Node.js
、Erlang
、Golang
... 像Node.js
、Redis
这样单进程单线程的程序均可以维持超过100w的TCP链接,这所有要归功于epoll
技术。
在IO密集型业务中须要频繁的上下文切换,若是采用线程模式开发会太过复杂,另一个进程中能开的线程数量也是有限的,线程太多会直接增长CPU的负责和内存资源。
线程自己是没有阻塞态的,当IO阻塞时也不会主动让出CPU资源,这种抢占式调度模式不太适合PHP开发。不过可使用全协程模式让同步代码异步执行来解决这个问题。
为何要使用Swoole呢?
Swoole的强大之处在于进程模型的涉及,既解决了异步问题又解决了并行。Swoole中提供了完整的协程(Coroutine)和通道(Channel)特性,带来全的CSP编程模型。应用层可使用彻底同步的编程方式,底层将自动实现异步IO。另外,使用常驻内存模式能够避免每次框架的初始化,节约了性能上的开销。
PHP应用的Web架构
- LNMP

Nginx做为Web服务器,PHP-FPM维护一个进程池去运行Web项目。LNMP模型的优势时简单、成熟、稳定,一次运行随后销毁带来的开发便捷性最大的特色。
PHP-FPM引入了进程常驻避免了每次请求建立和销毁进程时的性能开销并拓展了加载的开销,但每一个请求仍然要执行PHP RINT于RSHUTDOWN之间的全部流程,包括从新加载依次框架源码和项目代码,形成了极大的性能浪费。
- 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与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启动前的重要行为特征
- 基础
bootstrap
行为,如必要的常量定义、Composer加载器引入,读取配置。 - 生成被全部
worker/task
进程共享的程序全局期的对象,如Swoole\Lock
、Swoft\Memory\Table
的建立。 - 启动时全部进程中只能执行一次的操做,如前置
Process
的启动。 Bean
容器基本初始化以及项目启动流程须要的coreBean
的加载
和HTTP服务关系最密切的进程是Swoole中Worker进程,绝大部分业务处理都在Worker工做进程中。对于每一个Swoole事件,Swoft都提供了对应的Swoole监视器(对应@SwooleListener
注解)做为事件机制的封装。
要理解Swoft的HTTP服务器是如何在Swoole下运行,重点须要关注两个Swoole事件swoole.workerStart
和swoole.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
来负责调度,参与者包括RequestContext
、RequestHandler
、ExceptionHandler
。
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
事件,事件中是请求处理代码真正运行的位置,只有事件内产生的对象才会在请求结束时被回收。