跳到內容
getnextpdf.com

疑難排解:記憶體與效能

這些條目涵蓋你在負載下會碰到的兩類失敗: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
  • 解法。
    1. 限制影像快取。 NextPDF\Core\Config 揭露 imageCacheBytes(預設 52428800,即 50 MB)。以實例 wither $config->withImageCacheBytes($bytes)(簽章 withImageCacheBytes(int $bytes): self)降低它,讓一個嵌入眾多影像的建置在已知上限上快速失敗,而非陷入置換。這限制了記憶體內的影像快取;它不會重新取樣或重新編碼影像本身。
    2. 在嵌入前縮小輸入。 Core 不會把影像降尺度或重新編碼。請在嵌入過大的點陣美術稿之前調整其尺寸並重新編碼,並只嵌入你實際使用的字型,讓子集化要保留的字形集很小(見 縮減 PDF 檔案大小)。
    3. 保持壓縮開啟。 一個全新的 Configcompress 設為 true。在一般建置中讓它維持開啟;withCompress(false) 不是一項大小最佳化(它通常會增加輸出)。要除錯或剖析管線時才動用它——它調動的是 CPU/記憶體取捨(略過壓縮步驟),而非降低記憶體。
    4. 刻意、依每 worker 提高 memory_limit 這是一個標準 PHP 設定,不是一個 NextPDF 鍵。請在池設定中設定它,或為 CLI/佇列處理程序以 ini_set('memory_limit', '256M') 設定,並依一個已剖析的尖峰調校它,而非臆測。
  • 相關。 串流與記憶體

條目:在極大型文件上記憶體隨頁數增長

標題為「條目:在極大型文件上記憶體隨頁數增長」的區段
  • 症狀。 一份數千頁的文件用盡記憶體,即便每一頁都很小,且尖峰大致隨頁數同步上升。
  • 可能成因。 緩衝寫入器把整份序列化的文件保存在堆積中。對極大型文件而言那是主導成本。
  • 解法。
    1. 優先採用串流寫入路徑。請使用 串流與記憶體 中所記載的串流寫入路徑:它在每一頁組成時就序列化它並釋放緩衝,這能縮減頁面緩衝/輸出的增長;小型的每物件中繼資料(位移、頁面樹)仍可能隨頁/物件數量擴展。請遵循有記載的進入點,而非複製內部類別——底層的串流引擎屬於 experimental 層級,其符號並非穩定的公開介面。
    2. 原生 writeHtml() 剖析器而言,請記得輸入端記憶體同時受巢狀深度與元素計數防護所約束:ADR-001 把巢狀深度上限設為 MAX_NESTING_DEPTH = 100,並拒絕超過 MAX_ELEMENT_COUNT = 50000 的文件。一份命中元素上限的文件會被明確告知,而非默默用盡記憶體。這些 ADR-001 上限只管轄原生剖析器;選用的 Chrome 橋接 (writeHtmlChrome())在處理程序外算繪,並有它自己另外的記憶體/輸入限制,不是這些上限。
  • 相關。 串流與記憶體

條目:一個長存的 worker 在多次工作後用盡記憶體

標題為「條目:一個長存的 worker 在多次工作後用盡記憶體」的區段
  • 症狀。 單次算繪成功,但一個背靠背算繪眾多 PDF 的佇列 worker 在數分鐘或數小時後用盡記憶體。
  • 可能成因。 一個長存的 PHP 處理程序跨工作累積配置。一個在單一請求中看不見的緩慢增長,在數千次後便會複合。
  • 解法。
    1. 共享登錄表,重建文件。在啟動時建立一次 FontRegistryImageRegistry 並把它們傳給一個 DocumentFactory;以 $factory->create($config) 為每次工作建立一個全新的 Document。字型與影像剖析便每處理程序發生一次,而非每工作一次,而每工作的文件樹在它離開作用域時被回收。請遵循 examples/14-worker-factory.php
    2. new ImageRegistry(maxCacheBytes: ...) 約束共享的影像快取,讓它不會跨工作無限增長。
    3. 回收 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 在每個字面首次被使用時剖析它。
  • 解法。
    1. 啟用 opcache(並在有幫助處啟用 JIT)。 設定 opcache.enable=1 與一個慷慨的 opcache.memory_consumption;在正式環境中設定 opcache.validate_timestamps=0,讓快取不會每請求重新檢查。那個設定需要一個會在每次發行時重啟或重新載入 PHP-FPM(或以其他方式重設 opcache,例如 opcache_reset() / cachetool)的部署流程——否則 opcache 會持續服務舊位元組碼,而部署後執行的是陳舊程式碼。這些是標準 PHP ini 設定,不是 NextPDF 鍵。
    2. 在啟動時暖機並鎖定字型登錄表。 在一個 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 處理程序存活的佇列消費者。
    3. 不要每請求重新剖析同一個範本。 在啟動時透過共享登錄表解析字型與可重用資源一次;只有每工作的 Document 應在請求中建立。
  • 相關。 串流與記憶體

條目:伺服器在並行下飽和且延遲飆升

標題為「條目:伺服器在並行下飽和且延遲飆升」的區段
  • 症狀。 每次算繪的延遲在隔離下沒問題,但在負載下機器置換、CPU 飽和,或請求排隊並逾時。
  • 可能成因。 PHP-FPM worker 對可用 RAM 而言過多,因此 worker 尖峰的總和超過實體記憶體而主機置換;或 worker 過少,因此請求在一個小型池後串列化。
  • 解法。
    1. 依一個已剖析的尖峰配置 pm.max_children 使用標準公式:

      pm.max_children = (total RAM - OS/other overhead) / per-worker peak memory

      以一份具代表性的文件量測一個 worker 的真實尖峰(見「範圍」中的剖析備註),為 OS 與任何同處服務保留餘裕,然後相除。請留一道餘裕;不要配置到 RAM 的 100%。

    2. 把壓縮成本釘進你的預算。Flate 壓縮可能是寫入一個串流的可觀 CPU 成本,並隨可壓縮串流位元組的量擴展,因此頁數與嵌入字型的量會影響每次算繪的 CPU;影像處理、字型子集化與輸入剖析也可能主導。請以具代表性的文件量測,並在你選擇 worker 數量與 CPU 時計入真正的驅動因子。

    3. pm.max_requestspm.max_children 一併設定,讓子處理程序回收並收回任何緩慢增長,如上方的 worker 條目所述。

  • 相關。 串流與記憶體

條目:大型不受信任輸入剖析起來慢或昂貴

標題為「條目:大型不受信任輸入剖析起來慢或昂貴」的區段
  • 症狀。 在一個大型或深度巢狀的輸入上算繪很慢或記憶體吃重,尤其是 HTML 或一個並非你產生的字型。
  • 可能成因。 剖析成本隨輸入大小與結構擴展。一個病態的輸入(深度巢狀、一個龐大的元素計數,或一個不正確的字型)可能主導預算。
  • 解法。
    1. 倚靠引擎的界限。原生 writeHtml() HTML 剖析器強制 MAX_NESTING_DEPTH = 100MAX_ELEMENT_COUNT = 50000(ADR-001);超過那些上限的輸入會被拒絕,而非被允許用盡處理程序。 (選用的 Chrome 橋接,writeHtmlChrome(),不在這些 ADR-001 上限的範圍內,並強制它自己另外的記憶體/輸入限制。)
    2. 把呼叫端提供的字型視為不受信任。一個不正確的字型會拋出 NextPDF\Exception\FontParsingException,而非損壞輸出,因此請捕捉這個特定例外並拒絕該輸入,而非重試。
    3. 在你的邊界驗證並限制輸入大小,並對受呼叫端影響的內容套用請求層級的文件大小限制。
  • 相關。 疑難排解:字型與標記
症狀最可能的槓桿
單次算繪出現 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_limitopcache.*pm.max_childrenpm.max_requests 是標準的 PHP / PHP-FPM 設定。NextPDF 不為它們揭露自己的鍵;請在你的執行環境中設定它們,而非在 Config 中。

詞彙表:串流寫入器 · 字型子集化