轉載自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
事件,事件中是請求處理代碼真正運行的位置,只有事件內產生的對象纔會在請求結束時被回收。