跳至主要内容

PHP 容器化應用

· 閱讀時間約 11 分鐘
Vincent Chi

容器化(Containerization),這是一個由 Docker 公司所發揚光大的一種技術,它能夠很好地封裝應用程式與所需函式庫,而且通常有著比 虛擬化(Virtualization) 更高的效能。

一般來說,編譯式語言都很容易被容器化,例如 C/C++ 或 Golang,這是因為只需要在容器中設定好相依函式庫(通常是指動態函式庫),其編譯出的執行檔就可以直接在容器中運行。

這對 PHP 這類直譯式語就不是個好消息,其運行環境往往受制於 Apache PHP Module 或 PHP-FPM,再加上現代 PHP 往往會整合 Composer 進行相依性套件管理,這使得其處境更加雪上加霜。

備註

Python 或 Nodejs 這類直譯式語言,往往也會藉助一些協定(例如 Python 依賴的 WSGIASGI)或工具(例如 PM2Forever)來佈署。

PHP 的運行環境概述

PHP 官方共支援 8 種 伺服器 API(SAPI, Server Application Programming Interface),用於使伺服器(如 Apache 或 Nginx)與 PHP 運行環境相互溝通,其中粗體字表示為較常使用的 SAPI:

  • apache2handler
  • cgi
  • cli
  • embed
  • fmp
  • fuzzer
  • litespeed
  • phpdbg

摒除 CLI 用於與命令介面交互之外,通常是使用 Apache 及可支援 FPM 的網頁伺服器互相搭配。

信息

近年來,Swoole 發展迅速。開發者能夠利用 CLI 的 SAPI 直接執行一個完整的、可生產使用(Production Ready)的 Web Server 用於執行 PHP 應用程式。

目前 Laravel 官方也提供了 Laravel Octane 作為 Swoole 及 Roadrunner 的 Wrapper。

Apache HTTP Server

這是一個自 1995 年就發佈的老牌 HTTP 伺服器軟體,因為其廣大的用戶群與支援性,至今仍有許多網站選擇使用。

因為其驚人的市佔率,PHP 能夠以模組的形式被載入 Apache2 中,並且依賴 apache2handler SAPI 作為溝通橋樑。

|----------------------|
| |
| Apache2 |
| ============== | apache2handler ###############
| = PHP Module = <-|-----------------> # PHP Runtime #
| ============== | ###############
| |
|----------------------|
信息

在上圖中

  • Apache2 可以被視為一個(或一組)執行緒
  • PHP Module 是一個被 Apache 載入的 動態連結函式庫(Dynamic Library)
  • PHP Runtime 用於實際執行 PHP 程式,它並非一個實際存在的執行緒,只是一個虛擬概念

FPM

FPM 全稱為 FastCGI Process Manager,而 FastCGI 是一種通訊協定,用於使用程式能夠與伺服器互相溝通。

有許多網頁伺服器都支援這項通訊協定,其中最知名的當屬 Nginx,因此 Nginx + PHP-FPM 的組合漸漸取代 Apache2 + PHP Module。順帶一提,Apache2 其實也有支援 FPM,然而因為一些歷史因素導致其設定較為複雜,所以罕有人討論。

|-------|   FastCGI   |---------|   fpm SAPI   ###############
| Nginx | <---------> | PHP-FPM | <----------> # PHP Runtime #
|-------| |---------| ###############
信息

在上圖中

  • Nginx 可以被視為一個(或一組)執行緒
  • PHP-FPM 可以被視為一個(或一組)執行緒
  • Nginx 及 PHP-FPM 用 FastCGI 作為其溝通的通訊協定
  • PHP Runtime 用於實際執行 PHP 程式,它並非一個實際存在的執行緒,只是一個虛擬概念

容器化概述

前置知識

  • 映像(Image):未啟動的容器,可以視為容器的一種封裝,其目的是為了在多個不同的環境中交換容器資訊
  • 容器(Container):已啟動的映像,可以提供服務的對象

行程的考量

根據 Google Cloud Platform 構建容器的最佳做法 一文中提到,應該盡量為每一個「應用程式(Application)」打包一個映像。

其中,應用程式指的是具有唯一 親代行程(Parent Process),且可能具有多個 子代行程(Child Process) 的程式。

舉例來說,一個經典的 Nginx + PHP-FPM + MySQL 架構中,我們固然可以將它們都放在同一個容器中提供服務,但容器管理系統(Docker 或 Kubernetes)就無法針對個別的服務去判斷是否正常執行;所以更好的做法應該是將 Nginx, PHP-FPM 及 MySQL 分成三個不同的容器。

有鑑於此,相較於 Nginx + PHP-FPM 兩個容器的做法,Apache2 + PHP Module 只需要一個容器便能夠完成容器化,是比較簡單的選擇。

信息

在大部份情境下,Apache2 + PHP Module 的方式會比 PHP-FPM 效率慢上一些,這主要是因為 Apache 的 prefork 工作模式造成的性能損耗。

最小化映像

較小的映像可以擁有上傳、下載更快的優勢,而且這也可以顯著降低儲存成本,在某些應用場景下甚至可以提升啟動效率。

通常來說,可以利用以下做法最小化映像:

  • 使用較小的基礎映像:alpine 僅佔用不到 3MB,而 ubuntu 卻要使用近 30MB
  • 只安裝必須的軟體與相依套件:一些編譯、構建用的軟體或套件應該盡量避免安裝
  • Dockerfile 中盡量降低「指令」數量:因為每個指令會重新建立一層映像,這會使映像大小膨脹
危險

不良的 Dockerfile 範例

FROM alpine

RUN apk update
RUN apk add nginx
success

良好的 Dockerfile 範例

FROM alpine

RUN apk update \
&& apk add nginx

PHP 應用程式的容器化

先將 PHP-FPM 與 Apache PHP Module 的爭論暫且擱置一旁。PHP 應用程式本身的容器化也是很值得探討的議題。

相依性套件的安裝

正如同我們所知,現代的 PHP 應用程式往往會整合 Composer 作為其套件管理機制,但在核心理念上應該使最終產出的映像佔用空間盡可能小,這代表我們不應該在裡面安裝 Composer。

所幸使用 Multi-Stage Builds 這項功能,我們可以輕易地使用 Composer 管理相依性套件:

FROM composer AS builder

WORKDIR /src
COPY . .
RUN composer install --no-dev

FROM php:apache

WORKDIR /var/www/html
COPY --from=builder /src .
  • 行 1:使用 composer 這個官方映像作為 builder
  • 行 4:將當前的 PHP 程式碼複製進容器中
  • 行 5:執行對應的 composer 指令
  • 行 7:使用 php:apache 這個官方映像,這會是最終建構出來的映像所使用的基礎
  • 行 9:將 composer 映像中建構好的相依性套件複製進當前資料夾中

同理,如果是要建置 Nodejs 套件(例如用 webpack 編譯並構建前端資源),也可以用類似的方法執行。

警告

值得注意的是,由 Docker 官方提供的 composer:latest 映像並不「穩定」,這是因為它可能隨時會更新版本(例如從 PHP 8.0 升級為 PHP 8.1,或是 composer 的版本升級),如果應用程式必須限制在某一個 PHP 版本,請特別注意不要使用 composer:lastest

擴充元件的安裝

有些時候,官方提供的 PHP 映像內建的擴充元件並不能滿足我們應用程式的需求。舉例來說,GD extension 或 redis extension 並不會內建,此時我們就必須自行安裝。

FROM php:fpm

RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd
  • 行 3 ~ 6:安裝 GD 所需的函式庫
  • 行 7, 8:使用 docker-php-ext-configuredocker-php-ext-install 進行編譯並建置 GD extension,這是由 PHP 官方容器提供的一系列 Shell Scripts
FROM php:fpm

RUN pecl install redis \
&& docker-php-ext-enable redis
  • 行 3:利用 pecl 安裝 redis extension
  • 行 4:利用 docker-php-ext-enable 啟用 redis extension

利用 Alpine 作為基礎映像

事實上,在映像建置的過程中如果用 docker-php-ext-installpecl 有一些缺點:

  • 建置時間被拖長:因為重新編譯元件,這取決於電腦的處理性能與核心數多寡
  • 容器大小膨脹:編譯前往往需要加入一些額外的函式庫,例如 GD 使用 libpng-dev
  • 連線問題導致失敗:pecl 偶爾會因為連線問題導致整個映像建置失敗,浪費許多執行成本

更好的做法可以參考使用 alpine 作為基礎映像,並且用 apk 套件管理工具安裝絕大多數的 PHP extension:

FROM alpine

RUN apk add php82 php82-fpm

COPY ["php-fpm82", "-F"]
警告

在使用 Alpine Image 時需要特別小心,有些較新的軟體僅能在 alpine:edge 中被 apk add 安裝,而這是非常不適合用於生產環境的 Image。