Przejdź do głównej zawartości
getnextpdf.com

Rozwiązywanie problemów z pamięcią i wydajnością

Te wpisy obejmują dwie rodziny awarii, na które natrafiasz pod obciążeniem: PHP wyczerpujące pamięć podczas renderowania oraz przepustowość, która spada z klifu, gdy proces jest rozgrzany lub nasycony. Każdy wpis nazywa objaw, najbardziej prawdopodobną przyczynę oraz poprawkę korzystającą z rzeczywistej powierzchni NextPDF lub standardowych mechanizmów sterowania PHP-FPM. Dla bazowego modelu strumieniowania i samouczka o procesach roboczych przeczytaj Strumieniowanie i pamięć; ta strona jest jej towarzyszem po stronie incydentu.

Najpierw mierz. Próbkuj memory_get_peak_usage(true) przed i po renderowaniu oraz wywołuj memory_reset_peak_usage() między iteracjami, tak jak benchmark silnika izoluje koszt per renderowanie. Dostrajanie bez wartości bazowej przesuwa klif, zamiast go usuwać.

Wpis: „Allowed memory size exhausted” podczas generowania

Dział zatytułowany „Wpis: „Allowed memory size exhausted” podczas generowania”
  • Objaw. Renderowanie przerywa się błędem krytycznym Allowed memory size of <n> bytes exhausted ze środowiska uruchomieniowego PHP, często na dużym lub bogatym w obrazy dokumencie.
  • Prawdopodobna przyczyna. Domyślna ścieżka zapisu komponuje cały dokument, a potem go serializuje, więc szczytowe zużycie pamięci śledzi całkowity rozmiar wyjścia. Duży dokument, duże osadzone obrazy lub duży osadzony krój czcionki mogą przepchnąć żądanie poza memory_limit.
  • Rozwiązanie.
    1. Ogranicz pamięć podręczną obrazów. NextPDF\Core\Config udostępnia imageCacheBytes (domyślnie 52428800, czyli 50 MB). Obniż ją witherem instancji $config->withImageCacheBytes($bytes) (sygnatura withImageCacheBytes(int $bytes): self), aby kompilacja osadzająca wiele obrazów zawiodła szybko na znanym pułapie, zamiast korzystać ze swapu. To ogranicza pamięć podręczną obrazów w pamięci; nie próbkuje ponownie ani nie kompresuje samych obrazów.
    2. Zmniejsz wejścia przed osadzeniem. Core nie skaluje w dół ani nie kompresuje obrazów ponownie. Zmień rozmiar i przekoduj zbyt dużą grafikę rastrową zanim ją osadzisz oraz osadzaj czcionki, których faktycznie używasz, aby tworzenie podzbioru miało mały zestaw glifów do zachowania (zobacz Zmniejszanie rozmiaru pliku PDF).
    3. Pozostaw kompresję włączoną. Świeży Config ma compress ustawione na true. Pozostaw to włączone dla normalnych kompilacji; withCompress(false) nie jest optymalizacją rozmiaru (zwykle zwiększa wyjście). Sięgaj po to, aby debugować lub profilować potok — przesuwa kompromis CPU/pamięć (pomijając krok kompresji), a nie redukuje pamięć.
    4. Podnieś memory_limit świadomie, per proces roboczy. To standardowe ustawienie PHP, a nie klucz NextPDF. Ustaw je w konfiguracji puli albo za pomocą ini_set('memory_limit', '256M') dla procesu CLI/kolejki i wymiaruj je względem sprofilowanego szczytu, a nie zgadywania.
  • Powiązane. Strumieniowanie i pamięć.

Wpis: pamięć rośnie wraz z liczbą stron w bardzo dużych dokumentach

Dział zatytułowany „Wpis: pamięć rośnie wraz z liczbą stron w bardzo dużych dokumentach”
  • Objaw. Dokument liczący wiele tysięcy stron wyczerpuje pamięć, mimo że każda strona jest mała, a szczyt rośnie z grubsza w takt liczby stron.
  • Prawdopodobna przyczyna. Buforowany zapisujący przechowuje cały zserializowany dokument na stercie. Dla bardzo dużych dokumentów to dominujący koszt.
  • Rozwiązanie.
    1. Preferuj strumieniową ścieżkę zapisu. Użyj udokumentowanej strumieniowej ścieżki zapisu opisanej w Strumieniowanie i pamięć: serializuje każdą stronę w miarę jej komponowania i zwalnia bufor, co redukuje wzrost bufora strony/wyjścia; małe metadane per obiekt (przesunięcia, drzewo stron) wciąż mogą skalować się z liczbą stron/obiektów. Trzymaj się udokumentowanego punktu wejścia, zamiast kopiować klasy wewnętrzne — bazowy silnik strumieniowania jest na poziomie experimental, a jego symbole nie są stabilną powierzchnią publiczną.
    2. Dla natywnego parsera writeHtml() pamiętaj, że pamięć po stronie wejścia jest ograniczona zarówno przez zabezpieczenie głębokości zagnieżdżenia, jak i liczby elementów: ADR-001 ogranicza zagnieżdżenie do MAX_NESTING_DEPTH = 100 i odrzuca dokumenty powyżej MAX_ELEMENT_COUNT = 50000. Dokument, który osiąga limit elementów, otrzymuje o tym jawną informację, zamiast po cichu wyczerpywać pamięć. Te limity ADR-001 rządzą wyłącznie natywnym parserem; opcjonalny mostek Chrome (writeHtmlChrome()) renderuje poza procesem i ma własne, odrębne limity pamięci/wejścia, a nie te limity.
  • Powiązane. Strumieniowanie i pamięć.

Wpis: długowieczny proces roboczy wyczerpuje pamięć po wielu zadaniach

Dział zatytułowany „Wpis: długowieczny proces roboczy wyczerpuje pamięć po wielu zadaniach”
  • Objaw. Pojedyncze renderowania się udają, ale proces roboczy kolejki, który renderuje wiele plików PDF jeden po drugim, wyczerpuje pamięć po minutach lub godzinach.
  • Prawdopodobna przyczyna. Długowieczny proces PHP akumuluje alokacje między zadaniami. Powolny wzrost, niewidoczny w jednym żądaniu, kumuluje się na przestrzeni tysięcy.
  • Rozwiązanie.
    1. Współdziel rejestry, twórz dokumenty na nowo. Zbuduj FontRegistry oraz ImageRegistry raz przy starcie i przekaż je do DocumentFactory; twórz świeży Document per zadanie za pomocą $factory->create($config). Parsowanie czcionek i obrazów zachodzi wtedy raz dla procesu, a nie raz na zadanie, a drzewo dokumentu per zadanie jest zbierane, gdy wychodzi poza zakres. Trzymaj się examples/14-worker-factory.php.
    2. Ogranicz współdzieloną pamięć podręczną obrazów za pomocą new ImageRegistry(maxCacheBytes: ...), aby nie mogła rosnąć bez ograniczeń między zadaniami.
    3. Recykluj proces roboczy — sterowanie procesami, a nie gwarancja silnika. W PHP-FPM ustaw pm.max_requests, aby każde dziecko respawnowało się po stałej liczbie żądań. W kolejkach Laravel użyj queue:work --max-jobs / --max-time / --memory; w Symfony Messenger użyj messenger:consume --limit / --time-limit / --memory-limit.
  • Powiązane. Strumieniowanie i pamięć.

Wpis: klif przepustowości na zimnym lub niedostatecznie rozgrzanym procesie

Dział zatytułowany „Wpis: klif przepustowości na zimnym lub niedostatecznie rozgrzanym procesie”
  • Objaw. Pierwsze renderowania w świeżym procesie są wolne albo każde żądanie płaci koszt parsowania, którego rozgrzane żądania płacić nie powinny.
  • Prawdopodobna przyczyna. Kumulują się dwa koszty zimnego startu. PHP bez opcache rekompiluje każdy plik przy każdym żądaniu, a nierozgrzany FontRegistry parsuje każdy krój czcionki za pierwszym jego użyciem.
  • Rozwiązanie.
    1. Włącz opcache (i JIT tam, gdzie pomaga). Ustaw opcache.enable=1 oraz hojne opcache.memory_consumption; na produkcji ustaw opcache.validate_timestamps=0, aby pamięć podręczna nie była ponownie sprawdzana per żądanie. To ustawienie wymaga procesu wdrażania, który restartuje lub przeładowuje PHP-FPM (albo w inny sposób resetuje opcache, np. opcache_reset() / cachetool) przy każdym wydaniu — w przeciwnym razie opcache nadal serwuje stary bajtkod, a po wdrożeniu działa nieaktualny kod. To standardowe ustawienia ini PHP, a nie klucze NextPDF.
    2. Rozgrzej i zablokuj rejestr czcionek przy starcie. Na instancji FontRegistry $fontRegistry->warmup($fontFiles) parsuje kroje raz podczas startu, a $fontRegistry->lock() zamraża rejestr, aby kod w czasie żądania nie mógł mutować współdzielonego stanu; $fontRegistry->isLocked() raportuje stan. W genuinie długowiecznym procesie roboczym lub serwerze aplikacji — konsumencie kolejki albo procesie roboczym RoadRunner/Swoole/Octane, który utrzymuje ten sam proces PHP przy życiu między wieloma żądaniami — rozgrzany, zablokowany rejestr utrwala swoje sparsowane kroje w stanie obiektu, zamieniając parsowanie czcionek per żądanie w jednorazowy koszt startu procesu. W standardowym modelu żądań PHP-FPM ten rozgrzany stan obiektu nie przeżywa między żądaniami: opcache buforuje skompilowane klasy i bajtkod, a nie rozgrzany stan obiektu userland, więc rozgrzany FontRegistry jest odbudowywany per żądanie (uruchamiany ponownie przy każdym żądaniu z bootstrapu dziecka), a nie trzymany rozgrzanym między żądaniami w obrębie dziecka. Na zwykłym PHP-FPM opcache głównie amortyzuje koszt rekompilacji bajtkodu; przyjmij, że parsowanie czcionek jest płacone per żądanie, a nie eliminowane. Amortyzacja między żądaniami — parsowanie każdego kroju raz na czas życia procesu — dotyczy wyłącznie genuinie długowiecznego procesu, takiego jak proces roboczy RoadRunner/Swoole/Octane albo konsument kolejki, który utrzymuje ten sam proces PHP przy życiu między wieloma żądaniami.
    3. Nie parsuj ponownie tego samego szablonu per żądanie. Rozwiąż czcionki i wielokrotnie używane zasoby raz przy starcie przez współdzielone rejestry; tylko Document per zadanie powinien być tworzony w żądaniu.
  • Powiązane. Strumieniowanie i pamięć.

Wpis: serwer nasyca się, a opóźnienia skaczą pod współbieżnością

Dział zatytułowany „Wpis: serwer nasyca się, a opóźnienia skaczą pod współbieżnością”
  • Objaw. Opóźnienie per renderowanie jest w izolacji w porządku, ale pod obciążeniem maszyna swapuje, CPU się nasyca albo żądania ustawiają się w kolejce i wygasają.
  • Prawdopodobna przyczyna. Zbyt wiele procesów roboczych PHP-FPM jak na dostępną pamięć RAM, więc suma szczytów procesów roboczych przekracza pamięć fizyczną, a host swapuje; albo zbyt mało procesów roboczych, więc żądania serializują się za małą pulą.
  • Rozwiązanie.
    1. Wymiaruj pm.max_children ze sprofilowanego szczytu. Użyj standardowego wzoru:

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

      Zmierz rzeczywisty szczyt procesu roboczego reprezentatywnym dokumentem (zobacz notkę o profilowaniu w sekcji Zakres), zarezerwuj zapas na system operacyjny i wszelkie skolokowane usługi i podziel. Zostaw margines; nie wymiaruj do 100% RAM.

    2. Przypnij koszt kompresji w swoim budżecie. Kompresja Flate może być znaczącym kosztem CPU zapisu strumienia i skaluje się z wolumenem kompresowalnych bajtów strumienia, więc liczba stron i wolumen osadzonych czcionek wpływają na CPU per renderowanie; przetwarzanie obrazów, tworzenie podzbiorów czcionek oraz parsowanie wejścia również mogą dominować. Mierz reprezentatywnymi dokumentami i uwzględnij rzeczywisty czynnik dominujący, gdy wybierasz liczbę procesów roboczych i CPU.

    3. Ustaw pm.max_requests obok pm.max_children, aby dzieci się recyklowały i odzyskiwały wszelki powolny wzrost, jak we wpisie o procesie roboczym powyżej.

  • Powiązane. Strumieniowanie i pamięć.

Wpis: duże niezaufane wejście jest wolne lub kosztowne w parsowaniu

Dział zatytułowany „Wpis: duże niezaufane wejście jest wolne lub kosztowne w parsowaniu”
  • Objaw. Renderowanie jest wolne lub pamięciochłonne na dużym albo głęboko zagnieżdżonym wejściu, zwłaszcza HTML lub czcionce, której nie wytworzyłeś.
  • Prawdopodobna przyczyna. Koszt parsowania skaluje się z rozmiarem i strukturą wejścia. Patologiczne wejście (głębokie zagnieżdżenie, ogromna liczba elementów lub zniekształcona czcionka) może zdominować budżet.
  • Rozwiązanie.
    1. Oprzyj się na granicach silnika. Natywny parser HTML writeHtml() wymusza MAX_NESTING_DEPTH = 100 oraz MAX_ELEMENT_COUNT = 50000 (ADR-001); wejścia ponad tymi limitami są odrzucane, zamiast pozwolić im wyczerpać proces. (Opcjonalny mostek Chrome, writeHtmlChrome(), jest poza zakresem tych limitów ADR-001 i wymusza własne, odrębne limity pamięci/wejścia.)
    2. Traktuj czcionki dostarczone przez wywołującego jako niezaufane. Zniekształcona czcionka zgłasza NextPDF\Exception\FontParsingException, zamiast uszkodzić wyjście, więc przechwyć konkretny wyjątek i odrzuć wejście, zamiast ponawiać.
    3. Waliduj i wymiaruj wejścia na swojej granicy oraz stosuj limity na poziomie żądania dla rozmiaru dokumentu w przypadku treści wpływanej przez wywołującego.
  • Powiązane. Rozwiązywanie problemów: czcionki i tagowanie.
ObjawNajbardziej prawdopodobna dźwignia
Allowed memory size … exhausted na pojedynczym renderowaniuObniż $config->withImageCacheBytes(); zmniejsz obrazy przed osadzeniem; podnieś per proces roboczy memory_limit
Szczyt pamięci rośnie wraz z liczbą stronUżyj udokumentowanej strumieniowej ścieżki zapisu
Pamięć procesu roboczego rośnie na przestrzeni wielu zadańWspółdziel FontRegistry/ImageRegistry przez DocumentFactory; ustaw pm.max_requests / --max-jobs
Pierwsze żądania wolne, koszt parsowania per żądanieWłącz opcache; $fontRegistry->warmup(), a potem ->lock() przy starcie
Host swapuje / skoki opóźnień pod obciążeniemWymiaruj pm.max_children = (RAM − narzut) / szczyt per proces roboczy
Wolne lub ciężkie na dużym/niezaufanym wejściuOprzyj się na limitach ADR-001; odrzucaj zniekształcone czcionki na FontParsingException
  • imageCacheBytes to pułap pamięci, a nie pokrętło rozmiaru. Obniżenie go ogranicza pamięć podręczną, aby kompilacja zawiodła szybko; nigdy nie próbkuje ponownie ani nie przekodowuje osadzanych przez Ciebie obrazów. Core nie ma kontroli jakości obrazu.
  • withCompress(false) sprawia, że pliki są większe, i jest pomocą debugowania/profilowania. Nie jest optymalizacją rozmiaru; przesuwa kompromis CPU/pamięć (pomija krok kompresji), a nie redukuje pamięć.
  • Dokładny profil pamięci silnika strumieniowania jest właściwością na poziomie experimental i może się zmieniać między wydaniami pomocniczymi. Traktuj każdy pojedynczy pomiar jako obserwację, a nie przenośną stałą.
  • memory_limit, opcache.*, pm.max_children oraz pm.max_requests to standardowe ustawienia PHP / PHP-FPM. NextPDF nie udostępnia własnych kluczy dla nich; konfiguruj je w swoim środowisku uruchomieniowym, a nie w Config.

Słownik: strumieniowy zapisujący · tworzenie podzbioru czcionki