Roadrunner介紹
文檔:roadrunner官方文檔
roadrunner 是 go 語言開發(fā)的http服務(wù)器,主要實(shí)現(xiàn)兩個(gè)功能。
1、監(jiān)聽端口,接收外部客戶端的 http 請求。
2、管理 php 常駐進(jìn)程,使用 psr7 規(guī)范封裝請求報(bào)文、頭部等信息,通過 sock/pipe 方式通信,并將 php 的執(zhí)行結(jié)果回傳給請求客戶端。
php-fpm 與 roadrunner 區(qū)別簡單對比
生命周期

PHP執(zhí)行說明
例:執(zhí)行 HomeController@index
php-fpm的work進(jìn)程未銷毀前,每觸發(fā)一次web請求,會(huì)讀取一次 HomeController.php 文件內(nèi)容,所以修改業(yè)務(wù)代碼即時(shí)生效。類似work進(jìn)程執(zhí)行了一個(gè) white(true) 循環(huán),內(nèi)部執(zhí)行完 php 代碼后,會(huì)初始化到未執(zhí)行前狀態(tài)等待下次請求。
roadrunner 執(zhí)行創(chuàng)建的 psr-worker 進(jìn)程未銷毀前,僅第一次觸發(fā) web 請求時(shí)讀取HomeController.php 文件內(nèi)容,同時(shí)保存到內(nèi)存中,后續(xù)觸發(fā)將不再讀取文件,而是從內(nèi)存中取出,所以修改代碼不會(huì)即時(shí)生效。類似 php 內(nèi)部實(shí)現(xiàn)了 white(true) 循環(huán),在循環(huán)內(nèi)接收每一次 web 請求攜帶的參數(shù),執(zhí)行不同業(yè)務(wù)邏輯,返回響應(yīng)。這是真正意義上的常駐進(jìn)程。
本地初次安裝使用
- composer 依賴安裝
composer require spiral/roadrunner
composer require symfony/psr-http-message-bridge
composer require nyholm/psr7
- 業(yè)務(wù)項(xiàng)目根目錄創(chuàng)建文件
.rr.yaml: roadrunner配置文件
psr-worker.php: php入口文件
psr-worker.php.laravel
<?php
use Spiral\Goridge;
use Spiral\RoadRunner;
use \Illuminate\Http\Request;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
ini_set('display_errors', 'stderr');
require 'vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
if (!$app) {
throw new \RuntimeException('Not found lumen bootstrap file.');
}
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$relay = new Goridge\SocketRelay("rr.sock", null, Goridge\SocketRelay::SOCK_UNIX);
// $relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$worker = new RoadRunner\Worker($relay);
$psr7 = new RoadRunner\PSR7Client($worker);
$httpFoundationFactory = new HttpFoundationFactory();
while ($req = $psr7->acceptRequest()) {
try {
$symfonyRequest = $httpFoundationFactory->createRequest($req);
$request = Request::createFromBase($symfonyRequest);
$response = $kernel->handle($request);
$psr17Factory = new Psr17Factory();
$psr7factory = new PsrHttpFactory(
$psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory
);
$psr7response = $psr7factory->createResponse($response);
$psr7->respond($psr7response);
$kernel->terminate($request, $psr7response);
} catch (\Throwable $e) {
$psr7->getWorker()->error((string)$e);
}
}
psr-worker.php.lumen
<?php
use Spiral\Goridge;
use Spiral\RoadRunner;
use \Illuminate\Http\Request;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
ini_set('display_errors', 'stderr');
require 'vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
if (!$app) {
throw new \RuntimeException('Not found lumen bootstrap file.');
}
$kernel = $app->make('Laravel\Lumen\Application');
$relay = new Goridge\SocketRelay("rr.sock", null, Goridge\SocketRelay::SOCK_UNIX);
// $relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$worker = new RoadRunner\Worker($relay);
$psr7 = new RoadRunner\PSR7Client($worker);
$httpFoundationFactory = new HttpFoundationFactory();
while ($req = $psr7->acceptRequest()) {
try {
$symfonyRequest = $httpFoundationFactory->createRequest($req);
$request = Request::createFromBase($symfonyRequest);
$response = $kernel->handle($request);
$psr17Factory = new Psr17Factory();
$psr7factory = new PsrHttpFactory(
$psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory
);
$psr7response = $psr7factory->createResponse($response);
$psr7->respond($psr7response);
} catch (\Throwable $e) {
$psr7->getWorker()->error((string)$e);
}
}
.rr.yaml.laravel
http:
address: 0.0.0.0:80
workers:
command: "php psr-worker.php"
relay: "unix://rr.sock"
pool:
numWorkers: 1
maxJobs: 1
http2.h2c: true
# static file serving. remove this section to disable static file serving.
static:
# root directory for static file (HTTP would not serve .php and .htaccess files).
dir: "public"
# list of extensions for forbid for serving.
forbid: [".php", ".htaccess"]
# Always specifies list of extensions which must always be served by static
# service, even if file not found.
Always: [".css", ".js", ".ico", ".txt", ".svg", ".woff", ".ttf", ".gif", ".eot", ".swf"]
.rr.yaml.lumen*
http:
address: 0.0.0.0:80
workers:
command: "php psr-worker.php"
relay: "unix://rr.sock"
pool:
numWorkers: 1
maxJobs: 1
http2.h2c: true
psr-worker.php 區(qū)別:
Laravel 需要調(diào)用terminate方法,執(zhí)行一些類似 session 寫入的后置邏輯。
.rr.yaml 區(qū)別:
Laravel 需要增加static配置,來指定靜態(tài)文件訪問。
Windows 環(huán)境請注意:
由于默認(rèn)設(shè)置的監(jiān)聽Unix Sock,在 Windows 環(huán)境下會(huì)無法運(yùn)行,需要做兩處改動(dòng)。
.rr.yaml
relay: "tcp://:7000"
psr-worker.php
$relay = new Goridge\SocketRelay("localhost", 7000);
- docker-compose.yml 配置
user:
image: registry.cn-hangzhou.aliyuncs.com/duojii/roadrunner-base:dev
volumes:
- /home/www/user:/var/www # 宿主機(jī)代碼目錄映射到容器中
command: rr serve -v -d -c .rr.yaml
environment:
XDEBUG_PORT: 9001
XDEBUG_HOST: 192.168.0.119
ports:
- "8000:80" # 端口映射
注意事項(xiàng)
- 修改代碼后如果不重啟鏡像,新代碼如何生效?
a) 執(zhí)行一次 http 請求,worker 進(jìn)程自動(dòng)重啟
.rr.yaml指定 worker 進(jìn)程數(shù)量 和 執(zhí)行任務(wù)次數(shù)
numWorkers:1
maxJobs:1
numWorkers:#number of workers to be serving.
maxJobs:#maximum jobs per worker, 0 – unlimited.
設(shè)置后只存在一個(gè) psr-worker 進(jìn)程,每執(zhí)行一次請求,psr-worker 進(jìn)程會(huì)銷毀重建
注意:如果修改預(yù)先加載處的代碼,比如 Provider、Middleware,第一次觸發(fā)仍會(huì)執(zhí)行內(nèi)存中的舊代碼,第二次新代碼才會(huì)生效。
b) 進(jìn)入容器,執(zhí)行命令:rr http:reset,將重啟所有的 php 進(jìn)程。
c) Reload Configuration 自動(dòng)監(jiān)控文件改動(dòng)
.rr.yaml 增加 reload 監(jiān)控配置項(xiàng)
# reload can reset rr servers when files change
reload:
# refresh interval (default 1s)
interval: 1s
# file extensions to watch, defaults to [.php]
patterns: [".php"]
# list of services to watch
services:
http:
# list of dirs, "" root
dirs: [""]
# ignored dirs
ignore: [""]
# include sub directories
recursive: true
注意: dirs 配置空字符串,會(huì)默認(rèn)檢測根目錄內(nèi)所有文件,非常消耗 CPU 資源同時(shí)等待時(shí)間會(huì)很久,而不是設(shè)置的 1s。所以盡量手動(dòng)設(shè)置檢測目錄,比如監(jiān)控 app 和 routes 內(nèi) php 文件,則配置 dirs: ["app", "routes"]。也可以增加配置 ignore,指定目錄內(nèi)的文件修改會(huì)被忽略。
兩種推薦配置
1、設(shè)置監(jiān)聽目錄
dirs: ["app", "config", "routes", "storage"]
ignore: [""]
2、設(shè)置監(jiān)聽忽略目錄
dirs: [""]
ignore: ["bootstrap", "database", "public", "resources", "tests", "vendor"]
上述配置測試 Laravel5.6 項(xiàng)目,第二種 CPU 消耗略高
d) maxJobs 必須不設(shè)置或設(shè)置成 0,否則此種方案是沒有意義的??戽I鍵映射觸發(fā)執(zhí)行 sh 腳本,對指定項(xiàng)目執(zhí)行 rr http:reset 命令。以 terminal-vim 環(huán)境為例,IDE 理論上也存在映射功能,此處不研究。(Windows 用戶可嘗試快鍵鍵是否可以映射 docker 容器命令)
1、docker-compose.yml 同目錄下創(chuàng)建 reset-rr-worker.sh 腳本文件
#!/bin/bash
# 已啟用 rr 鏡像的項(xiàng)目目錄列表
itemArray=(
'management-frontend-lanqb'
'sales-backend-lanqb'
'sales-backend-duoji'
'user-management-backend-lanqb'
'school-backend-lanqb'
'copartner-backend-lanqb'
)
# 當(dāng)前執(zhí)行腳本的項(xiàng)目目錄,用于拆分項(xiàng)目名和跳轉(zhuǎn)回去
path=`PWD`
echo "execute path:${path}"
# 截取傳入的項(xiàng)目名
newPath=${path##*/}
# 切換本地docker-compose目錄
cd ~/www/local-dev-env
for item in ${itemArray[@]};
do
if [ "$newPath" == "$item" ]
then
docker-compose exec -T $item /usr/local/bin/rr http:reset
if [ $? -ne 0 ]; then
echo "rr-worker reset failed"
else
echo "rr-worker reset succeed"
fi
exit 0
fi
done
echo "invalid path"
2、init.vim 文件增加快鍵鍵映射,要對應(yīng)本地的腳本文件目錄
noremap <leader>R :! ~/www/local-dev-env/reset-rr-worker.sh<CR>
開發(fā)過程中,可以兩鍵重啟進(jìn)程,刷新代碼。
四種方案各有優(yōu)劣,應(yīng)根據(jù)實(shí)際情況自行選擇,部分缺點(diǎn)如下:
1、每次請求都將銷毀重啟 worker 進(jìn)程,即使代碼沒有改動(dòng)。部分位置代碼第二次請求才會(huì)改動(dòng)。
2、需要進(jìn)入容器內(nèi)部,手動(dòng)執(zhí)行命令。
3、CPU 有額外消耗。
4、非 terminal-vim 模式,配置可能比較麻煩,另外也需要本地單獨(dú)配置目錄。強(qiáng)烈推薦 mac、windows (如果 IDE 可以映射)用戶使用,兩種環(huán)境 docker 讀取主機(jī)文件較慢,減少不必要的進(jìn)程重啟過程。