嚴格來說,roadrunner 並不是 Modern PHPer 需要知道的必備知識。不過,我認為除了傳統的 Nginx + PHP-FPM 或 Apache + PHP-Apache 之外,應該要認知道還有其它的選擇。
對於一個使用者的 Request 在傳統的 Ngingx + PHP-FPM 下,我們大致上可以畫出以下結構。
|------| Request |-------| FastCGI |---------|
| User | <--------> | Nginx | <-------> | PHP-FPM |
|------| Response |-------| |---------|
生命周期依序為:Request
=> Fast CGI Request
=> Fast CGI Response
=> Response
註:FastCGI 通常為 TCP 或 Unix Socket,其連線為雙向的,故通常不會細分
Fast CGI Request
及Fast CGI Response
,這邊僅為了說明方便而設計這樣的用語。
對於 Nginx 在這個請求生命周期中,我們可以畫出它的簡易流程圖
從上面的說明不難看出,Nginx 與 PHP-FPM 算是一個互相合作的關係。
PHP-FPM 僅處理 PHP 有關的事項(直譯、渲染等),而 Nginx 幫它處理一些靜態資源(如 CSS, JS 或 Images)。
誠如我以前一直所說,正因為 PHP-FPM 這樣的架構,讓 PHP 應用程式相當難被微服務化(因為綁定使用 Web Server),於是 Roadrunner 的出現算是緩解了這樣的問題。
Roadrunner 是一個由 Golang 開發的 PHP Runtime,它有點類似整合了 Nginx + PHP-FPM(意思就是你不再需要這兩個東西相結合),它獨自就能處理這些事務。
Roadrunner 在實際運作上與 PHP-FPM 類似,它是由一個 Master Process 去操作多個 Worker Process(這個數量可以自由定義)。
而且它支援 HTTP/2 及 HTTPs,也就是說對於小型的應用程式甚至可以直接使用,很適合直接包裏在 Docker 中。
Step 1. Roadrunner 目前在 macOS 的 Brew 中並未提供(在 Arch Linux 的 AUR 或 Alpine Linux 的 APK 也都未提供),所以僅能從它的 GitHub Release 頁面中下載。
https://github.com/spiral/roadrunner
Step 2. 建立一個 Roadrunner 的設定檔 .rr.yml
,以下範例來自 Roadrunner 的官網。
# @link: <https://roadrunner.dev/docs/intro-config>
# defines environment variables for all underlying php processes
#env:
#key: value
# rpc bus allows php application and external clients to talk to rr services.
rpc:
# enable rpc server (enabled by default)
enable: true
# rpc connection DSN. Supported TCP and Unix sockets.
listen: tcp://127.0.0.1:6001
# http service configuration.
http:
# http host to listen.
address: 0.0.0.0:8000
#ssl: # For forcing HTTPS you should set env variable "APP_FORCE_HTTPS" to "true" or add --force-https worker argument
# custom https port (default 443)
#port: 443
# force redirect to https connection
#redirect: false
# ssl cert
#cert: server.crt
# ssl private key
#key: server.key
# max POST request size, including file uploads in MB.
maxRequestSize: 128
# file upload configuration.
uploads:
# list of file extensions which are forbidden for uploading.
forbid: [".php", ".exe", ".bat"]
# cidr blocks which can set ip using X-Real-Ip or X-Forwarded-For
trustedSubnets: ["10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10"]
# http worker pool configuration.
workers:
# php worker command.
#
# Allowed arguments:
# - `--(not-)force-https`
# - `--(not-)reset-db-connections`
# - `--(not-)reset-redis-connections`
# - `--(not-)refresh-app`
# - `--(not-)inject-stats-into-request`
# - `--not-fix-symfony-file-validation`
command: "php psr-worker.php"
# connection method (pipes, tcp://:9000, unix://socket.unix). default "pipes"
relay: "pipes"
# worker pool configuration.
pool:
# number of workers to be serving.
numWorkers: 4
# maximum jobs per worker, 0 - unlimited.
maxJobs: 32
# for how long worker is allowed to be bootstrapped.
allocateTimeout: 60
# amount of time given to worker to gracefully destruct itself.
destroyTimeout: 60
# monitors rr server(s)
limit:
# check worker state each second
interval: 1
# custom watch configuration for each service
services:
# monitor http workers
http:
# maximum allowed memory consumption per worker (soft)
maxMemory: 128
# maximum time to live for the worker (soft)
TTL: 0
# maximum allowed amount of time worker can spend in idle before being removed (for weak db connections, soft)
idleTTL: 0
# max_execution_time (brutal)
execTTL: 60
# static file serving. remove this section to disable static file serving.
static:
# serve http static files
enable: true
# root directory for static file (http would not serve .php and .htaccess files).
dir: "public"
# list of extensions for forbid for serving.
forbid: [".php"]
Step 3. 建立應用程式進入點
如同 PHP Built-in Server 需要自定義應用程式的進入點那樣(在 Day 02:內置伺服器 一文中有提到),為了讓應用程式配合 Roadrunner 使用,需要加入一個進入點。
首先需要先以 composer require spiral/roadrunner
取得需要的 Package,並且加入一個設定檔 psr-woker.php
:
use Spiral\Goridge;
use Spiral\RoadRunner;
ini_set('display_errors', 'stderr');
require 'vendor/autoload.php';
$worker = new RoadRunner\Worker(new Goridge\StreamRelay(STDIN, STDOUT));
$psr7 = new RoadRunner\PSR7Client($worker);
while ($request = $psr7->acceptRequest()) {
try {
// 利用 $request 取得 $response
// $request 是一個 Psr\Http\Message\ServerRequestFactoryInterface 的實現
// $response 必須是一個 Psr\Http\Message\ResponseInterface 的實現
// 此處的 Application 只是一個虛擬碼,這邊的內容可以自由實現
$response = Application::handle($request);
// 將 $response 丟回給 Roadrunner 處理
$psr7->respond($response);
} catch (\Throwable $e) {
$psr7->getWorker()->error((string)$e);
}
}
Step 4. 啟動 Roadrunner
啟動之前,先取得適合目前作業系統的 roadrunner ./vendor/bin/rr get
之後利用 ./rr serve -v -d
即可啟動 Roadrunner Server
雖然說 Roadrunner 整合了 Web Server 與 PHP-FPM 的功能,但並不代表可以就此拋棄 Nginx。
Nginx 作為一個 Reverse Proxy 還是有其優勢所在:負載平衡、靜態資源處理、HTTPs、HTTP/3(於 Nginx 1.17 開始支援)。
Roadrunner 提供了 gRPC 作為與其溝通的管道,不過事實上它目前提供的功能並不多所以通常忽略之(甚至完全不會啟用這個功能)
與大部份嘗試改變 PHP 底層行為的程式類似,Roadrunner 無法處理終止命令(die
及 exit
)。
在早期的版本中,無論是使用 echo 或 print 這類的函式,或是使用 Xdebug 下斷點都會有問題,目前是否還是如此就沒有特別再去測試了。
因為上傳檔案的部份交給 Roadrunner 處理,PHP 僅會取得 temp resource,所以 is_uploaded_file()
永遠回傳 false
,如果使用既有框架的話需要小心處理。
Roadrunner 算是非 Swoole 的微服務化解決方案之一,但是侷限於僅能使用 PSR-7 作為媒介,勢必會喪失很大一部份的 PHP 市場(就我所知,有滿多人都以為 PSR 只有到 4,甚至以為 PSR 系列都是在講 Coding Style 的),例如最明顯的例子就是 Wordpress。
不得不說,我很好奇為什麼 Wordpress 開發組死不接納 PSR-7 標準,不過我已經很久沒有關注 Wordpress,或許他們一直在我沒看見的地方努力著吧?