iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 27
1
Modern Web

成為 Modern PHPer系列 第 27

Day 27:另一個佈署時期的選擇 Roadrunner

前言

嚴格來說,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 RequestFast CGI Response,這邊僅為了說明方便而設計這樣的用語。

對於 Nginx 在這個請求生命周期中,我們可以畫出它的簡易流程圖

https://ithelp.ithome.com.tw/upload/images/20190928/20104201ih1bLes5De.png

Roadrunner 的作用

從上面的說明不難看出,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

實踐技巧

Nginx 的必要性

雖然說 Roadrunner 整合了 Web Server 與 PHP-FPM 的功能,但並不代表可以就此拋棄 Nginx。

Nginx 作為一個 Reverse Proxy 還是有其優勢所在:負載平衡、靜態資源處理、HTTPs、HTTP/3(於 Nginx 1.17 開始支援)。

RPC Server

Roadrunner 提供了 gRPC 作為與其溝通的管道,不過事實上它目前提供的功能並不多所以通常忽略之(甚至完全不會啟用這個功能)

缺陷

終止指令

與大部份嘗試改變 PHP 底層行為的程式類似,Roadrunner 無法處理終止命令(dieexit)。

在早期的版本中,無論是使用 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,或許他們一直在我沒看見的地方努力著吧?


上一篇
Day 26:Profiling 概述
下一篇
Day 28:PSR-7 帶來的變革
系列文
成為 Modern PHPer30

尚未有邦友留言

立即登入留言