传统架构
传统架构中所使用的Nginx + PHP-FPM的模型中,Nginx由于基于Linux的epoll
事件模型一个工作进程worker
会同时去处理多个请求,但是PHP-FPM的工作进程fpm-worker
却只能在同一时刻处理一个请求,而且fpm-worker
工作进程每次处理请求前都需要重新初始化MVC
框架然后再释放资源。当在高并发请求场景下时,fpm-worker
是完全不够用的,此时Nginx会直接响应502。另外,fpm-worker
进程间的切换消耗也很大。
PHP的FastCGI进程管理器PHP-FPM由于本身是同步阻塞进程模型,在请求结束后会释放掉所有资源,包括框架初始化创建的一系列对象,导致PHP进程空转并消耗大量的CPU资源,从而导致单机吞吐能力有限。简单来说就是请求夯住会导致CPU不能释放资源大大浪费CPU使用率。
PHP-FPM进程模型属于预派生子进程模式,即来一个请求就会fork
派生一个进程,进程的开销非常大从而大大降低吞吐率,另外并发量也只能由进程数决定。
预派生子进程模式是指程序启动后会创建多个进程,每个子进程会进入Accept
,等待新的连接进入。当客户端连接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,并且不再接受新的TCP连接。当此连接关闭时子进程会释放,重新进入Accept
参与处理新的连接。
预派生子进程模式的优势是完全可以复用进程且无需太多的上下文切换,缺点是这种模型严重依赖进程的数量来解决并发问题。由于一个客户端连接需要占用一个进程,工作进程数量有多少并发处理能力就有多少,可是操作系统能够创建的进程数量都是有限的。
PHP框架在初始化时会占用大量计算资源,而每个请求都需要重新进行初始化。当启动大量进程时会带来额外的进程调度消耗,虽然数百个进程出现进程上下文切换调度消耗所占的CPU不足1%可以忽略不计,但同时启动成千上万个进程消耗会直接上升,调度消耗可能占满CPU。
另外,请求一个第三方接口会非常慢,请求过程中会一直占用CPU资源,浪费昂贵的硬件资源。比如即时聊天程序的单机可能要维持数十万的连接,那么也就要启动数十万的进程,这显然是不可能的。那么,有没有 一种技术可以在一个进程内处理所有并发IO呢?答案是采用IO复用技术。
解决方案
那么有什么样的解决方案呢?
通过业务分析不难发现,Web应用中90%以上的都是IO密集型业务,只要提高IO复用的能力就可以提升单机吞吐能力,另外需要将PHP-FPM的同步阻塞模式调整成异步非阻塞模式,也就可以解决核心的性能问题。
如何提升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
事件,事件中是请求处理代码真正运行的位置,只有事件内产生的对象才会在请求结束时被回收。
有疑问加站长微信联系(非本文作者)