跳到內容
getnextpdf.com

在 serverless 平台上執行 NextPDF

原生、行程內的 NextPDF 核心引擎是一個近乎理想的 serverless 工作負載。 它是 在你的行程內執行的純 PHP——composer require nextpdf/core, 建構一份文件,取得位元組。沒有外部二進位檔要生出來,沒有無頭瀏覽器,沒有要保持存活的 daemon,也沒有連到 sidecar 服務的 socket。 一個建構 PDF 的函式冷啟動、執行你的 PHP、回傳位元組,然後結束。這正好乾淨地對映到 AWS Lambda(透過 Bref runtime)、Google Cloud Run 與 AWS App Runner。

本頁涵蓋把該原生引擎部署到這三種 runtime,以及它們所施加的那一小組真實限制:

  • runtime 檔案系統 並非持久:Lambda 只保證一個可寫的 /tmp,而容器 runtime(Cloud Run、App Runner)擁有一個短暫、 容器範圍的檔案系統——無論哪一種,字型都必須隨著部署套件或映像檔一起搬移,並且 在 PHP 中註冊(引擎不讀取任何字型路徑環境變數);
  • 冷啟動 要為自動載入與任何字型暖機付出代價,因此請每個容器暖機一次 FontRegistry,而不是每次叫用都做;
  • 套件大小、記憶體與逾時 必須依建構工作調校,而不是依瑣碎的請求調校。

本頁 適用於原生引擎。Chrome 橋接 (透過建議的 nextpdf/artisan 套件使用 writeHtmlChrome)是另一個、更笨重的故事:它透過 symfony/process 外殼呼叫一個無頭 Chromium, 而一個原味 Lambda zip 或精簡容器並不包含它。在 Lambda 上執行 Chromium 意味著一個帶有瀏覽器及其共享函式庫的自訂 layer、大得多的套件,以及長得多的冷啟動——不在這裡的範圍內。裸引擎不需要那些。

開始前,請確認以下這些部分都已就位:

  • 你的應用程式有一份已提交的 composer.jsoncomposer.lock, 其中 nextpdf/core 是相依項。
  • 你擁有你打算內嵌的字型檔,且你獲授權內嵌它們。
  • 你擁有目標所需的工具鏈——Lambda 用 Bref CLI 與 serverless 框架,Cloud Run / App Runner 則用容器建置。

直接從套件讀取,nextpdf/core 需要 php: >=8.4 <9.0 以及一小組 PHP 擴充功能——ext-mbstringext-intlext-gdext-opensslext-zlibext-curl。標準的 Bref PHP layer 全部都打包了。官方的 php:8.4 容器映像檔開箱即提供 opensslcurlzlib,但 mbstringgdintl 內建——它們需要安裝系統相依項並用 docker-php-ext-install 啟用擴充功能(見 Docker 部署指南)。 在 Bref 上沒有什麼奇特的東西要編譯;在容器路徑上,你在映像檔建置時為 引擎啟用那三個擴充功能。

讓這份適配性乾淨的,是引擎 做的事:

  • 核心路徑沒有子行程。 建構一份文件並呼叫 getPdfData() 從頭到尾都是行程內的 PHP。symfony/process 相依項存在是為了選用的 Chrome 橋接,而不是為了原生繪製——原生 PDF 產生從不生出行程。
  • 沒有持久狀態。 每次叫用都建構一份全新文件並回傳位元組。除了暖容器外,沒有東西必須在請求之間存活;暖容器你會拿來做字型暖機(見下文),但絕不依賴它來確保正確性。
  • 不需要可寫的工作目錄。 引擎在記憶體中建構 PDF 並以字串回傳;只有在 呼叫 save() 時它才碰磁碟。在 serverless 上你不會這麼做——你回傳位元組—— 所以缺乏持久檔案系統永遠不會咬到建構路徑。

唯一的硬限制:沒有持久的可寫檔案系統

標題為「唯一的硬限制:沒有持久的可寫檔案系統」的區段

部署檔案系統並非持久,但模型因 runtime 而異。 AWS Lambda 只保證一個可寫的 /tmp(預設 512 MB,可設定至 10 GB);函式檔案系統的其餘部分為唯讀。容器 runtime(Cloud Run、App Runner)擁有一個短暫、容器範圍的可寫檔案系統,而不是只有 /tmp 的模型——但寫入那裡的任何東西在容器被回收時都會遺失,所以它是暫存空間,不是儲存。在每一種情況下,都優先用 /tmp 或設定好的 volume 做暫存,並絕不依賴對應用程式映像檔路徑的寫入作為持久儲存。隨之有兩個後果。

絕不要呼叫 save() 而期望持久輸出。 NextPDF\Core\Document 同時暴露 save(string $path): voidgetPdfData(): string。在 serverless 上你使用 getPdfData() 並回傳或上傳位元組——不要把對應用程式目錄的寫入當成持久儲存。如果你必須暫存一個檔案(例如要分段上傳到物件儲存),請寫到 /tmp(或設定好的 volume)下並清理,並記得在暖容器上,這個暫存空間會跨叫用持續存在,並計入它的大小上限。

use NextPDF\Core\Document;
// Right for serverless: get the bytes, return or upload them.
$pdf = $document->getPdfData(); // string of PDF bytes, built in memory
// Avoid on serverless: save() writes to disk. On Lambda the application
// directory is read-only; on Cloud Run / App Runner it is writable but
// ephemeral (lost on container recycle). Neither is durable storage.
// $document->save('/var/task/out.pdf'); // not durable — return the bytes instead

不要在 runtime 安裝 OS 字型,也不要依賴自動字型探索;正式環境請打包你的字型檔。 在 Lambda 上,唯讀的檔案系統直接封鎖 apt-get install fonts-*;在容器 runtime 上,任何 runtime 安裝都落在短暫的檔案系統上,並在下一次回收時遺失。而且那也幫不上忙,因為原生引擎不讀取任何 OS/fontconfig 字型——它只從你註冊的檔案解析字型。所以在正式環境,字型檔必須裝在部署成品裡。如果你刻意把字型檔抓進 /tmp 或設定好的 volume,你就必須用字型 registry 明確註冊它們,並接受額外的冷啟動與可靠性成本——這不是建議的正式環境模式。

原生引擎透過 NextPDF\Typography\FontRegistry字型檔 解析字型,而不是從 fontconfig 或 OS 安裝的字型。 在 serverless 上這沒得商量:部署之後沒有持久檔案系統可放字型,所以它們裝在套件裡(一個 Lambda zip 或 layer) 或裝在映像檔裡(Cloud Run / App Runner)。

把你的 .ttf / .otf / .ttc 檔打包到你專案中的一個目錄下—— resources/fonts/ 是慣例——讓它們被納入成品。 然後在 PHP 中註冊那個目錄。引擎 讀取任何字型路徑環境變數:NEXTPDF_FONTS_PATHnextpdf/laravel 套件 fonts_path 設定鍵的預設值 (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))),且只被那個框架整合消費,不被 nextpdf/core 消費。一個裸函式必須用打包好的目錄建構 registry:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Register the directory the deployment artifact bundled the fonts into.
// On Lambda/Bref the code root is /var/task; adjust for your runtime.
$registry = new FontRegistry(__DIR__ . '/resources/fonts');
// (equivalently, $registry->addFontDirectory(__DIR__ . '/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$document = $factory->create();

那就是字型在 serverless 上的全部考量。檔名規則、完整的 registry API,以及非持久檔案系統的處理,都在專屬頁面上—— 不要在這裡重複。請閱讀 在正式環境為原生引擎佈建字型 以取得完整模式,並註冊你打包進去的同一個目錄。 Docker 部署指南 涵蓋 Cloud Run / App Runner 情況下對應的映像檔端打包。

冷啟動:每個容器暖機一次 FontRegistry

標題為「冷啟動:每個容器暖機一次 FontRegistry」的區段

冷啟動要為 PHP bootstrap、Composer 的最佳化自動載入器,以及第一次建構所觸發的任何字型剖析付出代價。你無法避免 bootstrap,但你可以把字型工作移出熱路徑,並跨暖叫用重用它。

FontRegistryDocumentFactory 建構 一次,且在 handler 之外, 讓它們存活於容器的生命週期,並在每次暖叫用時被重用。可選擇用你已知會用到的字型檔呼叫 warmup(), 讓它們在初始化期間就被剖析,而非在第一次繪製時,接著 lock() registry,讓它已剖析的狀態凍結,使任何逐叫用的變更都不會競態:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Container-scoped, built once at cold start (module scope, not per request).
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
// Parse the fonts you will actually use now, so the first render does not.
$registry->warmup([
$fontsDir . '/liberation/LiberationSans-Regular.ttf',
$fontsDir . '/liberation/LiberationSans-Bold.ttf',
]);
// Freeze the parsed state for the life of the warm container.
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// Each invocation: fresh document from the shared, warm factory.
$handler = static function (array $event) use ($factory): string {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Hello from serverless', newLine: true);
return $document->getPdfData();
};

lock() 之前 呼叫 warmup()——registry 一旦鎖定就被凍結,所以鎖定後再暖機會引發設定錯誤。把暖機時無法載入的字型當成 部署期錯誤,而非 runtime 細節:在啟動時驗證你打算暖機的每一個字型路徑確實存在且能剖析,並在有任何一個不行時讓部署(或你的健康檢查)失敗,而不是讓打錯的路徑稍後以缺字符的形式浮現。把暖機清單控制在一次典型叫用所需的字型;暖機一個你很少用的龐大字族只會拉長每一次冷啟動。

Bref 以一個已發布的 layer 與一個 serverless.yml plugin,為 Lambda 提供 PHP runtime。php-84 runtime 已隨附 nextpdf/core 需要的擴充功能,所以你部署你的程式碼與字型,並把一個函式指向一個 handler。一份最小的 serverless.yml

service: nextpdf-serverless
provider:
name: aws
region: us-east-1
runtime: provided.al2023
plugins:
- ./vendor/bref/bref
functions:
generate:
handler: handler.php
description: Generate a PDF with the native NextPDF engine
runtime: php-84
memorySize: 1024 # size to the build; see "Sizing" below
timeout: 30 # seconds; raise for large documents
# The Lambda filesystem is read-only except /tmp. Fonts ship in the
# package under resources/fonts and are registered in the handler.

handler 用暖的、容器範圍的 factory 建構文件,並回傳位元組。對於 HTTP API,以 base64 編碼搭配 application/pdf 內容型別回傳,讓 API Gateway 把 body 當成二進位;對於 invoke 或 queue 觸發,把位元組上傳到物件儲存並回傳 key:

handler.php (outline)
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Cold-start: built once per container, reused across warm invocations. ---
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
$registry->warmup([$fontsDir . '/liberation/LiberationSans-Regular.ttf']);
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// --- Per-invocation handler. ---
return static function (array $event) use ($factory): array {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Invoice', newLine: true);
// getPdfData() materializes the whole PDF in memory and returns it.
$bytes = $document->getPdfData();
return [
'statusCode' => 200,
'isBase64Encoded' => true,
'headers' => ['Content-Type' => 'application/pdf'],
'body' => base64_encode($bytes),
];
};

在把流量接到它之前,先驗證套件含有健康的環境。 nextpdf/core 隨附一個安裝在 vendor/bin/nextpdf 的 CLI,其 doctor 命令會精確回報引擎所需的擴充功能。對著同一個 runtime 映像檔或 layer 執行它一次,以確認 PHP 8.4 與每一個必要擴充功能都在場。

Cloud Run 與 App Runner 執行一個 容器,而不是壓縮過的函式,所以建置物是來自 把 NextPDF 應用程式容器化 的 Docker 映像檔,而不是 Bref 套件。原生引擎的限制相同:把字型打包進映像檔、在 PHP 中註冊打包好的目錄、以非特權執行, 並把檔案系統當成非持久。不同於 Lambda 的只有 /tmp 的模型,一個 Cloud Run / App Runner 容器擁有一個短暫、容器範圍的可寫檔案系統——但它在每次回收時都會重設,所以請用 /tmp(Cloud Run 上的一個 tmpfs)或設定好的 volume 做暫存,並絕不依賴對應用程式映像檔路徑的寫入作為持久儲存。

與 Lambda 的差異是維運性的,而非結構性的:

  • 容器可以跨請求保持暖 (在某個並行設定下), 所以上述容器範圍的 FontRegistry/DocumentFactory 暖機會跨許多請求回本,不只是下一次叫用。
  • 你透過 HTTP 服務 (一個 FPM 或內建 PHP server SAPI),而不是 invoke 事件,所以你透過框架的回應回傳位元組。對於大型文件,以串流回應回傳它們——見 把大型已產生 PDF 以 HTTP 回應串流
  • 請求逾時與記憶體 設定在服務上(Cloud Run 服務逾時/記憶體;App Runner 實例設定),而非逐函式。

其餘一切——擴充功能集、字型註冊、getPdfData() 輸出呼叫——都是與 Lambda handler 相同的程式碼。

  • 套件與映像檔大小。 成品攜帶 vendor/(僅正式環境—— 以 --no-dev 安裝)與你打包的字型。字型佔大宗:一個完整 CJK 字族有數十 MB。只裝你實際繪製的字型,以把 Lambda 套件保持在其上限之下並把映像檔保持小,這也會縮短冷啟動。所附的 Liberation 字族(resources/fonts/liberation/)很小,並涵蓋與 Helvetica 度量相容的替換。
  • 記憶體。 getPdfData() 在記憶體中建構 整份 文件並以一個字串回傳,所以尖峰記憶體約為一份完成 PDF 的大小,加上建構的工作集。把函式/容器記憶體調校到你產生的最大文件,而不是平均值。在 Lambda 上,記憶體也會放大 CPU,所以更多記憶體往往意味著更快的建構,並在更高的每毫秒費率下仍更便宜——兩者都要量測。一份幾頁的文件在 512–1024 MB 下很從容;圖片繁重或多頁的文件需要更多。
  • 逾時。 主導請求預算的是建構,不是傳輸。把函式逾時設在最壞情況建構時間之上,並留餘裕。如果一份文件大到有逾時風險,就把產生移到一個非同步觸發 (一個 queue 支援的 Lambda 或一個 Cloud Run job),讓它把結果寫到物件儲存,而不是阻塞一個同步請求。
  • /tmp 大小。 如果你在 /tmp 下暫存任何東西,要把它的大小上限算進去,並記得它會跨暖叫用持續存在——請清理,否則一個長壽命容器會慢慢把它填滿。
  • 不要對 app 目錄做持久的 save() 部署檔案系統並非持久——Lambda 的 app 目錄是唯讀(只有 /tmp 接受寫入), 而 Cloud Run / App Runner 容器檔案系統可寫但短暫。 請用 getPdfData() 並回傳/上傳位元組;如必要,在 /tmp 或設定好的 volume 下暫存。
  • 不要依賴自動字型探索。 不要在 runtime 安裝 OS 字型,也不要依賴自動字型探索;正式環境請打包你的字型檔。 原生引擎不讀取任何 OS/fontconfig 字型——它只解析你註冊的檔案。如果你刻意把字型檔抓進 /tmp 或設定好的 volume,你就必須用字型 registry 明確註冊它們,並接受額外的冷啟動與可靠性成本。請打包並註冊那些檔案。見上方連結的字型頁面。
  • NEXTPDF_FONTS_PATH 對裸引擎毫無作用。 它是 nextpdf/laravel 的設定預設值,不是 nextpdf/core 會讀的變數。一個只設定那個變數的裸 Bref handler 不會註冊任何字型,並繪製出豆腐塊。
  • Chrome 橋接不適合原味函式。 writeHtmlChrome 需要一個無頭 Chromium 與 symfony/process 子行程路徑。把 Chromium 放上 Lambda 需要一個帶有瀏覽器及其函式庫的自訂 layer、大得多的套件,以及長冷啟動。原生引擎與 writeHtml 都不需要那些——在 serverless 上優先用它們。
  • 冷啟動成本是自動載入加字型剖析。 在正式環境安裝上使用 --optimize-autoloader,並每個容器暖機 registry 一次。不要暖機你很少用的字型。
  • API Gateway 需要二進位處理。 回傳 isBase64Encoded: true 搭配 Content-Type: application/pdf,並把 API 設定為把 application/pdf 當成二進位媒體型別,否則用戶端收到損壞的位元組。
  • Premium 與 ionCube 是更重的成品考量。 ionCube 編碼的 NextPDF Pro / Enterprise 建置需要與 runtime 中確切 PHP 建置相符的 ionCube Loader,而一個現成的 Bref layer 不含它。那不在核心 serverless 部署的範圍內。
  • 不要裝任何 dev 相依項。--no-dev 安裝,讓測試與分析工具絕不進入函式套件或映像檔。
  • 建構前先驗證輸入。 一個由請求輸入驅動的 PDF 建構是一個記憶體耗盡向量;在邊界處、在任何建構工作執行之前,拒絕超出範圍或過大的輸入,並限制並行,讓高流量不會把尖峰記憶體乘成記憶體不足的失敗。
  • 把字型與授權排除在公開成品之外。 只打包你獲授權內嵌的字型,並絕不把 premium 授權檔烤進一個公開推送的映像檔或 layer——改以環境值或祕密管理器在 runtime 提供它。
  • 最小權限。 只給函式/服務它需要的 IAM 權限 (例如,對那一個輸出 bucket 的寫入存取),並如 Docker 指南所示以非特權執行容器。

本指南未提出任何規範性標準主張。平台事實直接從 nextpdf/core 套件讀取:php: >=8.4 <9.0 約束與必要擴充功能 ext-mbstringext-intlext-gdext-opensslext-zlibext-curl。標準的 Bref PHP-8.4 runtime layer 全部六個都打包了;官方的 php:8.4 映像檔提供 opensslcurlzlib,但 mbstringgdintl 必須在映像檔建置時用 docker-php-ext-install 安裝並啟用(見 Docker 頁面)。輸出呼叫是真正的核心介面 NextPDF\Core\Document::getPdfData(): string(其磁碟手足是 save(string $path): void)。字型透過 NextPDF\Typography\FontRegistry 註冊——其目錄建構式引數 / addFontDirectory(),搭配 warmup(array $fontFiles)lock() 用於冷啟動模式——並透過 NextPDF\Core\DocumentFactory::create() 接好。 NEXTPDF_FONTS_PATHnextpdf/laravel 套件的 fonts_path 設定鍵 (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))),不是 nextpdf/core 會讀的變數。nextpdf CLI 的 doctor 命令在套件中宣告為 "bin": ["bin/nextpdf"],並在消費端 app 中安裝在 vendor/bin/nextpdf。 Bref runtime 名稱與 AWS Lambda / Cloud Run / App Runner 行為是那些廠商有記錄的功能。