疑難排解:記憶體與效能
這些條目涵蓋你在負載下會碰到的兩類失敗:PHP 在算繪期間用盡記憶體,以及一旦處理程序變暖或飽和後吞吐量便跌落斷崖。每則條目指名一個症狀、最可能的成因,以及一個運用真正 NextPDF 介面或標準 PHP-FPM 控制項的修正。關於底層的串流模型與一個 worker 教學,請閱讀 串流與記憶體;本頁是它在事故端的搭檔。
先量測。在一次算繪前後取樣 memory_get_peak_usage(true),並在各次迭代之間呼叫
memory_reset_peak_usage(),就如同引擎的基準測試隔離每次算繪成本那樣。沒有基準的調校只是移動斷崖,而非移除它。
條目:產生期間出現「Allowed memory size exhausted」
標題為「條目:產生期間出現「Allowed memory size exhausted」」的區段- 症狀。 一次算繪以一個來自 PHP 執行環境的 fatal
Allowed memory size of <n> bytes exhausted中止,常見於大型或影像密集的文件。 - 可能成因。 預設寫入路徑先組成整份文件,再序列化它,因此尖峰記憶體追蹤總輸出大小。一份大型文件、大的嵌入影像,或一個大的嵌入字型字面,都可能把請求推過
memory_limit。 - 解法。
- 限制影像快取。
NextPDF\Core\Config揭露imageCacheBytes(預設52428800,即 50 MB)。以實例 wither$config->withImageCacheBytes($bytes)(簽章withImageCacheBytes(int $bytes): self)降低它,讓一個嵌入眾多影像的建置在已知上限上快速失敗,而非陷入置換。這限制了記憶體內的影像快取;它不會重新取樣或重新編碼影像本身。 - 在嵌入前縮小輸入。 Core 不會把影像降尺度或重新編碼。請在嵌入過大的點陣美術稿之前調整其尺寸並重新編碼,並只嵌入你實際使用的字型,讓子集化要保留的字形集很小(見 縮減 PDF 檔案大小)。
- 保持壓縮開啟。 一個全新的
Config把compress設為true。在一般建置中讓它維持開啟;withCompress(false)不是一項大小最佳化(它通常會增加輸出)。要除錯或剖析管線時才動用它——它調動的是 CPU/記憶體取捨(略過壓縮步驟),而非降低記憶體。 - 刻意、依每 worker 提高
memory_limit。 這是一個標準 PHP 設定,不是一個 NextPDF 鍵。請在池設定中設定它,或為 CLI/佇列處理程序以ini_set('memory_limit', '256M')設定,並依一個已剖析的尖峰調校它,而非臆測。
- 限制影像快取。
- 相關。 串流與記憶體。
條目:在極大型文件上記憶體隨頁數增長
標題為「條目:在極大型文件上記憶體隨頁數增長」的區段- 症狀。 一份數千頁的文件用盡記憶體,即便每一頁都很小,且尖峰大致隨頁數同步上升。
- 可能成因。 緩衝寫入器把整份序列化的文件保存在堆積中。對極大型文件而言那是主導成本。
- 解法。
- 優先採用串流寫入路徑。請使用 串流與記憶體
中所記載的串流寫入路徑:它在每一頁組成時就序列化它並釋放緩衝,這能縮減頁面緩衝/輸出的增長;小型的每物件中繼資料(位移、頁面樹)仍可能隨頁/物件數量擴展。請遵循有記載的進入點,而非複製內部類別——底層的串流引擎屬於
experimental層級,其符號並非穩定的公開介面。 - 對原生
writeHtml()剖析器而言,請記得輸入端記憶體同時受巢狀深度與元素計數防護所約束:ADR-001 把巢狀深度上限設為MAX_NESTING_DEPTH = 100,並拒絕超過MAX_ELEMENT_COUNT = 50000的文件。一份命中元素上限的文件會被明確告知,而非默默用盡記憶體。這些 ADR-001 上限只管轄原生剖析器;選用的 Chrome 橋接 (writeHtmlChrome())在處理程序外算繪,並有它自己另外的記憶體/輸入限制,不是這些上限。
- 優先採用串流寫入路徑。請使用 串流與記憶體
中所記載的串流寫入路徑:它在每一頁組成時就序列化它並釋放緩衝,這能縮減頁面緩衝/輸出的增長;小型的每物件中繼資料(位移、頁面樹)仍可能隨頁/物件數量擴展。請遵循有記載的進入點,而非複製內部類別——底層的串流引擎屬於
- 相關。 串流與記憶體。
條目:一個長存的 worker 在多次工作後用盡記憶體
標題為「條目:一個長存的 worker 在多次工作後用盡記憶體」的區段- 症狀。 單次算繪成功,但一個背靠背算繪眾多 PDF 的佇列 worker 在數分鐘或數小時後用盡記憶體。
- 可能成因。 一個長存的 PHP 處理程序跨工作累積配置。一個在單一請求中看不見的緩慢增長,在數千次後便會複合。
- 解法。
- 共享登錄表,重建文件。在啟動時建立一次
FontRegistry與ImageRegistry並把它們傳給一個DocumentFactory;以$factory->create($config)為每次工作建立一個全新的Document。字型與影像剖析便每處理程序發生一次,而非每工作一次,而每工作的文件樹在它離開作用域時被回收。請遵循examples/14-worker-factory.php。 - 以
new ImageRegistry(maxCacheBytes: ...)約束共享的影像快取,讓它不會跨工作無限增長。 - 回收 worker——處理程序控制,不是引擎保證。在
PHP-FPM 中,設定
pm.max_requests,讓每個子處理程序在固定數量的請求後重生。在 Laravel 佇列中使用queue:work --max-jobs/--max-time/--memory;在 Symfony Messenger 中使用messenger:consume --limit/--time-limit/--memory-limit。
- 共享登錄表,重建文件。在啟動時建立一次
- 相關。 串流與記憶體。
條目:在冷或暖機不足的處理程序上出現吞吐量斷崖
標題為「條目:在冷或暖機不足的處理程序上出現吞吐量斷崖」的區段- 症狀。 一個全新處理程序中的頭幾次算繪很慢,或每個請求都付出暖請求不該付出的剖析成本。
- 可能成因。 兩種冷啟動成本疊加。沒有 opcache 的 PHP 在每個請求重新編譯每個檔案,而一個未暖機的
FontRegistry在每個字面首次被使用時剖析它。 - 解法。
- 啟用 opcache(並在有幫助處啟用 JIT)。 設定
opcache.enable=1與一個慷慨的opcache.memory_consumption;在正式環境中設定opcache.validate_timestamps=0,讓快取不會每請求重新檢查。那個設定需要一個會在每次發行時重啟或重新載入 PHP-FPM(或以其他方式重設 opcache,例如opcache_reset()/cachetool)的部署流程——否則 opcache 會持續服務舊位元組碼,而部署後執行的是陳舊程式碼。這些是標準 PHP ini 設定,不是 NextPDF 鍵。 - 在啟動時暖機並鎖定字型登錄表。 在一個
FontRegistry實例上,$fontRegistry->warmup($fontFiles)在啟動期間剖析字面一次,而$fontRegistry->lock()凍結登錄表,讓請求時的程式碼無法變動共享狀態;$fontRegistry->isLocked()回報該狀態。在一個真正長存的 worker 或應用程式伺服器中——一個佇列消費者,或一個跨眾多請求保持同一 PHP 處理程序存活的 RoadRunner/Swoole/Octane worker——一個已暖機、已鎖定的登錄表會把它已剖析的字面持留在物件狀態中,把每請求的字型剖析轉成一次性的處理程序啟動成本。在標準的 PHP-FPM 請求模型下,那個已暖機的物件狀態不會跨請求倖存:opcache 快取的是已編譯的類別與位元組碼,而非已暖機的使用者層物件狀態,因此一個已暖機的FontRegistry會每請求被重建(每個請求從子處理程序的 bootstrap 重新執行),而非在一個子處理程序內跨請求保持暖機。在純 PHP-FPM 上,opcache 主要攤提的是位元組碼重新編譯成本;請接受字型剖析是每請求付出,而非被消除。跨請求攤提——在處理程序的生命期內每個字面只剖析一次——只適用於一個真正長存的處理程序,例如一個 RoadRunner/Swoole/Octane worker,或一個跨眾多請求保持同一 PHP 處理程序存活的佇列消費者。 - 不要每請求重新剖析同一個範本。 在啟動時透過共享登錄表解析字型與可重用資源一次;只有每工作的
Document應在請求中建立。
- 啟用 opcache(並在有幫助處啟用 JIT)。 設定
- 相關。 串流與記憶體。
條目:伺服器在並行下飽和且延遲飆升
標題為「條目:伺服器在並行下飽和且延遲飆升」的區段- 症狀。 每次算繪的延遲在隔離下沒問題,但在負載下機器置換、CPU 飽和,或請求排隊並逾時。
- 可能成因。 PHP-FPM worker 對可用 RAM 而言過多,因此 worker 尖峰的總和超過實體記憶體而主機置換;或 worker 過少,因此請求在一個小型池後串列化。
- 解法。
-
依一個已剖析的尖峰配置
pm.max_children。 使用標準公式:pm.max_children = (total RAM - OS/other overhead) / per-worker peak memory以一份具代表性的文件量測一個 worker 的真實尖峰(見「範圍」中的剖析備註),為 OS 與任何同處服務保留餘裕,然後相除。請留一道餘裕;不要配置到 RAM 的 100%。
-
把壓縮成本釘進你的預算。Flate 壓縮可能是寫入一個串流的可觀 CPU 成本,並隨可壓縮串流位元組的量擴展,因此頁數與嵌入字型的量會影響每次算繪的 CPU;影像處理、字型子集化與輸入剖析也可能主導。請以具代表性的文件量測,並在你選擇 worker 數量與 CPU 時計入真正的驅動因子。
-
把
pm.max_requests與pm.max_children一併設定,讓子處理程序回收並收回任何緩慢增長,如上方的 worker 條目所述。
-
- 相關。 串流與記憶體。
條目:大型不受信任輸入剖析起來慢或昂貴
標題為「條目:大型不受信任輸入剖析起來慢或昂貴」的區段- 症狀。 在一個大型或深度巢狀的輸入上算繪很慢或記憶體吃重,尤其是 HTML 或一個並非你產生的字型。
- 可能成因。 剖析成本隨輸入大小與結構擴展。一個病態的輸入(深度巢狀、一個龐大的元素計數,或一個不正確的字型)可能主導預算。
- 解法。
- 倚靠引擎的界限。原生
writeHtml()HTML 剖析器強制MAX_NESTING_DEPTH = 100與MAX_ELEMENT_COUNT = 50000(ADR-001);超過那些上限的輸入會被拒絕,而非被允許用盡處理程序。 (選用的 Chrome 橋接,writeHtmlChrome(),不在這些 ADR-001 上限的範圍內,並強制它自己另外的記憶體/輸入限制。) - 把呼叫端提供的字型視為不受信任。一個不正確的字型會拋出
NextPDF\Exception\FontParsingException,而非損壞輸出,因此請捕捉這個特定例外並拒絕該輸入,而非重試。 - 在你的邊界驗證並限制輸入大小,並對受呼叫端影響的內容套用請求層級的文件大小限制。
- 倚靠引擎的界限。原生
- 相關。 疑難排解:字型與標記。
決策表:症狀對應槓桿
標題為「決策表:症狀對應槓桿」的區段| 症狀 | 最可能的槓桿 |
|---|---|
單次算繪出現 Allowed memory size … exhausted | 降低 $config->withImageCacheBytes();在嵌入前縮小影像;提高每 worker 的 memory_limit |
| 尖峰記憶體隨頁數上升 | 使用 有記載的串流寫入路徑 |
| worker 記憶體在多次工作上攀升 | 透過 DocumentFactory 共享 FontRegistry/ImageRegistry;設定 pm.max_requests / --max-jobs |
| 頭幾個請求慢、每請求剖析成本 | 啟用 opcache;在啟動時 $fontRegistry->warmup() 然後 ->lock() |
| 主機在負載下置換/延遲飆升 | 配置 pm.max_children = (RAM − overhead) / per-worker peak |
| 在大型/不受信任輸入上慢或吃重 | 倚靠 ADR-001 上限;在 FontParsingException 上拒絕不正確的字型 |
邊角情況與陷阱
標題為「邊角情況與陷阱」的區段imageCacheBytes是一個記憶體上限,不是一個大小旋鈕。 降低它會限制快取,讓建置快速失敗;它絕不會重新取樣或重新編碼你嵌入的影像。Core 沒有影像品質控制。withCompress(false)讓檔案更大,是一個除錯/剖析輔助。它不是一項大小最佳化;它調動的是 CPU/記憶體取捨(它略過壓縮步驟),而非降低記憶體。- 串流引擎的確切記憶體輪廓是一個
experimental層級的屬性,可能在小版本之間變動。請把任何單一量測視為一項觀察,而非一個可攜的常數。 memory_limit、opcache.*、pm.max_children與pm.max_requests是標準的 PHP / PHP-FPM 設定。NextPDF 不為它們揭露自己的鍵;請在你的執行環境中設定它們,而非在Config中。
另請參閱
標題為「另請參閱」的區段- 串流與記憶體——串流模型、ADR-001 界限與完整的批次 worker 教學。
- 縮減 PDF 檔案大小——壓縮與字型子集化,這兩項真正的大小控制。
- 疑難排解:字型與標記——字型解析、剖析與子集化失敗。
- 知識庫索引