在 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.json與composer.lock, 其中nextpdf/core是相依項。 - 你擁有你打算內嵌的字型檔,且你獲授權內嵌它們。
- 你擁有目標所需的工具鏈——Lambda 用 Bref CLI 與
serverless框架,Cloud Run / App Runner 則用容器建置。
為什麼原生引擎適合 serverless
標題為「為什麼原生引擎適合 serverless」的區段直接從套件讀取,nextpdf/core 需要 php: >=8.4 <9.0 以及一小組 PHP 擴充功能——ext-mbstring、ext-intl、ext-gd、
ext-openssl、ext-zlib 與 ext-curl。標準的 Bref PHP layer 全部都打包了。官方的 php:8.4 容器映像檔開箱即提供 openssl、
curl 與 zlib,但 mbstring、gd 與 intl 未 內建——它們需要安裝系統相依項並用 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): void 與 getPdfData(): 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_PATH 是
nextpdf/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,但你可以把字型工作移出熱路徑,並跨暖叫用重用它。
把 FontRegistry 與 DocumentFactory 建構 一次,且在 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 細節:在啟動時驗證你打算暖機的每一個字型路徑確實存在且能剖析,並在有任何一個不行時讓部署(或你的健康檢查)失敗,而不是讓打錯的路徑稍後以缺字符的形式浮現。把暖機清單控制在一次典型叫用所需的字型;暖機一個你很少用的龐大字族只會拉長每一次冷啟動。
AWS Lambda 上的一個 Bref 函式
標題為「AWS Lambda 上的一個 Bref 函式」的區段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:
<?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
標題為「Cloud Run 與 App Runner」的區段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-mbstring、ext-intl、ext-gd、ext-openssl、
ext-zlib 與 ext-curl。標準的 Bref PHP-8.4 runtime layer 全部六個都打包了;官方的 php:8.4 映像檔提供 openssl、curl 與 zlib,但
mbstring、gd 與 intl 必須在映像檔建置時用
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_PATH 是 nextpdf/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
行為是那些廠商有記錄的功能。
另請參閱
標題為「另請參閱」的區段- 把 NextPDF 應用程式容器化:Cloud Run / App Runner 目標所用的正式環境映像檔。
- 在正式環境為原生引擎佈建字型:本頁所依賴的字型檔命名、registry API,以及暖機並鎖定模式。
- 把大型已產生 PDF 以 HTTP 回應串流:在 Cloud Run / App Runner 上透過 HTTP 回傳已建構文件的記憶體模型。
- 在邊緣以 Cloudflare 繪製:當行程內函式不是合適的 runtime 時。