Перейти к содержимому
getnextpdf.com

Устранение неполадок: память и производительность

Эти записи охватывают два семейства сбоев, в которые вы упираетесь под нагрузкой: PHP исчерпывает память во время отрисовки и пропускная способность падает с обрыва, как только процесс прогрет или насыщен. Каждая запись называет симптом, наиболее вероятную причину и исправление, которое использует реальную поверхность NextPDF или стандартные элементы управления PHP-FPM. О лежащей в основе модели потоковой обработки и руководстве по воркеру читайте Потоковая обработка и память; эта страница — её компаньон со стороны инцидентов.

Сначала измеряйте. Снимайте memory_get_peak_usage(true) до и после отрисовки и вызывайте memory_reset_peak_usage() между итерациями — так, как бенчмарк движка изолирует стоимость одной отрисовки. Настройка без базовой линии перемещает обрыв, а не убирает его.

Запись: «Allowed memory size exhausted» во время генерации

Заголовок раздела «Запись: «Allowed memory size exhausted» во время генерации»
  • Симптом. Отрисовка прерывается фатальной ошибкой Allowed memory size of <n> bytes exhausted из среды выполнения PHP, часто на большом или насыщенном изображениями документе.
  • Вероятная причина. Путь записи по умолчанию компонует весь документ, затем сериализует его, поэтому пиковая память отслеживает общий размер вывода. Большой документ, крупные встроенные изображения или большое встроенное начертание шрифта могут вытолкнуть запрос за memory_limit.
  • Решение.
    1. Ограничьте кэш изображений. NextPDF\Core\Config предоставляет imageCacheBytes (по умолчанию 52428800, то есть 50 МБ). Понизьте его инстанс-витером $config->withImageCacheBytes($bytes) (сигнатура withImageCacheBytes(int $bytes): self), чтобы сборка, встраивающая много изображений, давала сбой быстро на известном потолке, а не уходила в своп. Это ограничивает кэш изображений в памяти; оно не пересэмплирует и не перекодирует сами изображения.
    2. Уменьшайте входные данные перед встраиванием. Core не масштабирует и не перекодирует изображения. Измените размер и перекодируйте завышенную растровую графику перед её встраиванием и встраивайте шрифты, которые вы действительно используете, чтобы сабсеттингу нужно было сохранить небольшой набор глифов (см. Уменьшение размера файла PDF).
    3. Держите сжатие включённым. У свежего Config compress установлен в true. Оставляйте его включённым для обычных сборок; withCompress(false) — это не оптимизация размера (обычно она увеличивает вывод). Тянитесь к нему, чтобы отлаживать или профилировать конвейер — она сдвигает компромисс CPU/память (пропуская шаг сжатия), а не уменьшает память.
    4. Повышайте memory_limit намеренно, на воркер. Это стандартная настройка PHP, а не ключ NextPDF. Задайте её в конфигурации пула или через ini_set('memory_limit', '256M') для процесса CLI/очереди и определите её размер по профилированному пику, а не по догадке.
  • Связанное. Потоковая обработка и память.

Запись: память растёт с числом страниц на очень больших документах

Заголовок раздела «Запись: память растёт с числом страниц на очень больших документах»
  • Симптом. Документ в несколько тысяч страниц исчерпывает память, даже хотя каждая страница мала, и пик растёт примерно в ногу с числом страниц.
  • Вероятная причина. Буферизованный писатель держит весь сериализованный документ в куче. Для очень больших документов это доминирующая стоимость.
  • Решение.
    1. Предпочитайте потоковый путь записи. Используйте документированный потоковый путь записи, описанный в Потоковой обработке и памяти: он сериализует каждую страницу по мере её компоновки и освобождает буфер, что уменьшает рост буфера страниц/вывода; небольшие метаданные на объект (смещения, дерево страниц) всё ещё могут масштабироваться с числом страниц/объектов. Следуйте документированной точке входа, а не копируйте внутренние классы — лежащий в основе потоковый движок имеет уровень experimental, и его символы не являются стабильной публичной поверхностью.
    2. Для нативного парсера writeHtml() помните, что память со стороны ввода ограничена защитами глубины вложенности и числа элементов: ADR-001 ограничивает вложенность на MAX_NESTING_DEPTH = 100 и отклоняет документы свыше MAX_ELEMENT_COUNT = 50000. Документу, который упирается в предел элементов, об этом сообщается явно, а не он молча исчерпывает память. Эти пределы ADR-001 управляют только нативным парсером; необязательный мост Chrome (writeHtmlChrome()) отрисовывает вне процесса и имеет собственные отдельные пределы памяти/ввода, а не эти пределы.
  • Связанное. Потоковая обработка и память.

Запись: долгоживущий воркер исчерпывает память после многих задач

Заголовок раздела «Запись: долгоживущий воркер исчерпывает память после многих задач»
  • Симптом. Одиночные отрисовки удаются, но воркер очереди, отрисовывающий много PDF подряд, исчерпывает память через минуты или часы.
  • Вероятная причина. Долгоживущий процесс PHP накапливает выделения между задачами. Медленный рост, невидимый в одном запросе, накапливается через тысячи.
  • Решение.
    1. Разделяйте реестры, пересоздавайте документы. Постройте FontRegistry и ImageRegistry один раз при загрузке и передайте их в DocumentFactory; создавайте свежий Document на задачу с $factory->create($config). Разбор шрифтов и изображений тогда происходит один раз на процесс, а не один раз на задачу, и дерево документа на задачу собирается, когда выходит из области видимости. Следуйте examples/14-worker-factory.php.
    2. Ограничьте общий кэш изображений через new ImageRegistry(maxCacheBytes: ...), чтобы он не мог расти без предела между задачами.
    3. Перерабатывайте воркер — управление процессами, а не гарантия движка. В PHP-FPM задайте pm.max_requests, чтобы каждый потомок перерождался после фиксированного числа запросов. В очередях Laravel используйте queue:work --max-jobs / --max-time / --memory; в Symfony Messenger используйте messenger:consume --limit / --time-limit / --memory-limit.
  • Связанное. Потоковая обработка и память.

Запись: провал пропускной способности на холодном или недопрогретом процессе

Заголовок раздела «Запись: провал пропускной способности на холодном или недопрогретом процессе»
  • Симптом. Первые отрисовки в свежем процессе медленны, или каждый запрос платит стоимость разбора, которую прогретые запросы платить не должны.
  • Вероятная причина. Складываются две стоимости холодного старта. PHP без opcache перекомпилирует каждый файл на каждом запросе, а непрогретый FontRegistry разбирает каждое начертание шрифта при первом его использовании.
  • Решение.
    1. Включите opcache (и JIT, где он помогает). Задайте opcache.enable=1 и щедрый opcache.memory_consumption; в продакшене задайте opcache.validate_timestamps=0, чтобы кэш не перепроверялся на каждом запросе. Эта настройка требует процесса развёртывания, который перезапускает или перезагружает PHP-FPM (либо иначе сбрасывает opcache, например opcache_reset() / cachetool) при каждом релизе — иначе opcache продолжает отдавать старый байт-код и устаревший код выполняется после развёртывания. Это стандартные ini-настройки PHP, а не ключи NextPDF.
    2. Прогрейте и заблокируйте реестр шрифтов при загрузке. На экземпляре FontRegistry $fontRegistry->warmup($fontFiles) разбирает начертания один раз во время загрузки, а $fontRegistry->lock() замораживает реестр, чтобы код времени запроса не мог мутировать общее состояние; $fontRegistry->isLocked() сообщает состояние. В по-настоящему долгоживущем воркере или сервере приложений — потребителе очереди или воркере RoadRunner/Swoole/Octane, который держит один процесс PHP живым через многие запросы, — прогретый, заблокированный реестр сохраняет свои разобранные начертания в состоянии объекта, превращая разбор шрифтов на каждый запрос в одноразовую стоимость загрузки процесса. При стандартной модели запросов PHP-FPM это прогретое состояние объекта не переживает запросы: opcache кэширует скомпилированные классы и байт-код, а не прогретое состояние объектов userland, поэтому прогретый FontRegistry перестраивается на каждый запрос (перезапускается каждым запросом из загрузчика потомка), а не держится прогретым через запросы внутри потомка. На простом PHP-FPM opcache в основном амортизирует стоимость перекомпиляции байт-кода; примите, что разбор шрифтов оплачивается на каждый запрос, а не устраняется. Межзапросовая амортизация — разбор каждого начертания один раз на время жизни процесса — применяется только в по-настоящему долгоживущем процессе, таком как воркер RoadRunner/Swoole/Octane или потребитель очереди, который держит один процесс PHP живым через многие запросы.
    3. Не перепарсивайте один и тот же шаблон на каждый запрос. Разрешайте шрифты и переиспользуемые ресурсы один раз при загрузке через общие реестры; только Document на задачу должен создаваться в запросе.
  • Связанное. Потоковая обработка и память.

Запись: сервер насыщается и латентность подскакивает при конкуренции

Заголовок раздела «Запись: сервер насыщается и латентность подскакивает при конкуренции»
  • Симптом. Латентность на одну отрисовку в изоляции нормальна, но под нагрузкой машина уходит в своп, CPU насыщается, либо запросы выстраиваются в очередь и выходят за тайм-аут.
  • Вероятная причина. Слишком много воркеров PHP-FPM для доступной RAM, поэтому сумма пиков воркеров превышает физическую память и хост уходит в своп; либо слишком мало воркеров, поэтому запросы сериализуются за небольшим пулом.
  • Решение.
    1. Определите pm.max_children по профилированному пику. Используйте стандартную формулу:

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

      Измерьте реальный пик воркера на репрезентативном документе (см. примечание о профилировании в разделе «Область»), зарезервируйте запас для ОС и любых совмещённых служб и поделите. Оставьте маржу; не определяйте размер до 100% RAM.

    2. Закрепите стоимость сжатия в своём бюджете. Сжатие Flate может быть значительной стоимостью CPU при записи потока и масштабируется с объёмом сжимаемых байтов потока, поэтому число страниц и объём встроенных шрифтов влияют на CPU на одну отрисовку; обработка изображений, сабсеттинг шрифтов и разбор ввода тоже могут доминировать. Измеряйте на репрезентативных документах и учитывайте реальный драйвер, когда выбираете число воркеров и CPU.

    3. Задайте pm.max_requests рядом с pm.max_children, чтобы потомки перерабатывались и возвращали любой медленный рост, как в записи о воркере выше.

  • Связанное. Потоковая обработка и память.

Запись: большой недоверенный ввод медленно или дорого разбирать

Заголовок раздела «Запись: большой недоверенный ввод медленно или дорого разбирать»
  • Симптом. Отрисовка медленна или тяжела по памяти на большом или глубоко вложенном вводе, особенно HTML или шрифте, который вы не производили.
  • Вероятная причина. Стоимость разбора масштабируется с размером и структурой ввода. Патологический ввод (глубокая вложенность, огромное число элементов или некорректный шрифт) может доминировать в бюджете.
  • Решение.
    1. Опирайтесь на пределы движка. Нативный HTML-парсер writeHtml() обеспечивает MAX_NESTING_DEPTH = 100 и MAX_ELEMENT_COUNT = 50000 (ADR-001); входы свыше этих пределов отклоняются, а не допускаются к исчерпанию процесса. (Необязательный мост Chrome, writeHtmlChrome(), вне области этих пределов ADR-001 и обеспечивает собственные отдельные пределы памяти/ввода.)
    2. Относитесь к шрифтам, поданным вызывающим кодом, как к недоверенным. Некорректный шрифт выбрасывает NextPDF\Exception\FontParsingException, а не портит вывод, поэтому перехватывайте конкретное исключение и отклоняйте ввод, а не повторяйте попытку.
    3. Валидируйте и ограничивайте размеры входов на своей границе и применяйте пределы уровня запроса на размер документа для содержимого, на которое влияет вызывающий код.
  • Связанное. Устранение неполадок: шрифты и тегирование.
СимптомНаиболее вероятный рычаг
Allowed memory size … exhausted на одной отрисовкеПонизьте $config->withImageCacheBytes(); уменьшите изображения до встраивания; повысьте memory_limit на воркер
Пиковая память растёт с числом страницИспользуйте документированный потоковый путь записи
Память воркера растёт через многие задачиРазделяйте FontRegistry/ImageRegistry через DocumentFactory; задайте pm.max_requests / --max-jobs
Первые запросы медленны, стоимость разбора на запросВключите opcache; $fontRegistry->warmup(), затем ->lock() при загрузке
Хост уходит в своп / подскоки латентности под нагрузкойОпределите pm.max_children = (RAM − накладные расходы) / пик на воркер
Медленно или тяжело на большом/недоверенном вводеОпирайтесь на пределы ADR-001; отклоняйте некорректные шрифты на FontParsingException
  • imageCacheBytes — это потолок памяти, а не регулятор размера. Его понижение ограничивает кэш, чтобы сборка дала сбой быстро; оно никогда не пересэмплирует и не перекодирует встраиваемые вами изображения. У Core нет управления качеством изображений.
  • withCompress(false) делает файлы больше и является вспомогательным средством для отладки/профилирования. Это не оптимизация размера; оно сдвигает компромисс CPU/память (пропуская шаг сжатия), а не уменьшает память.
  • Точный профиль памяти потокового движка — свойство уровня experimental и может сдвигаться между минорными релизами. Относитесь к любому одиночному измерению как к наблюдению, а не как к переносимой константе.
  • memory_limit, opcache.*, pm.max_children и pm.max_requests — это стандартные настройки PHP / PHP-FPM. NextPDF не предоставляет собственных ключей для них; настраивайте их в своей среде выполнения, а не в Config.

Глоссарий: потоковый писатель · сабсеттинг шрифтов