Pular para o conteúdo
getnextpdf.com

Solução de problemas: memória e desempenho

Estas entradas cobrem duas famílias de falhas que você encontra sob carga: o PHP ficando sem memória durante um render, e o throughput que despenca de uma queda brusca quando um processo está quente ou saturado. Cada entrada nomeia um sintoma, a causa mais provável e uma correção que usa superfície real do NextPDF ou controles padrão do PHP-FPM. Para o modelo de streaming subjacente e um tutorial de worker, leia Streaming e memória; esta página é a companheira do lado de incidentes dele.

Meça primeiro. Faça amostragem de memory_get_peak_usage(true) antes e depois de um render e chame memory_reset_peak_usage() entre iterações, do jeito que o benchmark do engine isola o custo por render. Ajustar sem uma baseline move a queda em vez de removê-la.

Entrada: “Allowed memory size exhausted” durante a geração

Seção intitulada “Entrada: “Allowed memory size exhausted” durante a geração”
  • Sintoma. Um render aborta com um fatal Allowed memory size of <n> bytes exhausted do runtime do PHP, frequentemente em um documento grande ou cheio de imagens.
  • Causa provável. O caminho de escrita padrão compõe o documento inteiro e depois o serializa, então o pico de memória acompanha o tamanho total da saída. Um documento grande, imagens incorporadas grandes ou um face de fonte incorporado grande podem empurrar a requisição além do memory_limit.
  • Resolução.
    1. Limite o cache de imagens. NextPDF\Core\Config expõe imageCacheBytes (padrão 52428800, ou seja, 50 MB). Reduza-o com o wither de instância $config->withImageCacheBytes($bytes) (assinatura withImageCacheBytes(int $bytes): self) para que um build que incorpora muitas imagens falhe rápido em um teto conhecido em vez de fazer swap. Isso limita o cache de imagens em memória; ele não reamostra nem recodifica as próprias imagens.
    2. Encolha as entradas antes de incorporar. A Core não reduz a escala nem recodifica imagens. Redimensione e recodifique a arte raster grande demais antes de incorporá-la, e incorpore as fontes que você realmente usa para que o subsetting tenha um conjunto pequeno de glifos para manter (veja Reduza o tamanho de arquivo do PDF).
    3. Mantenha a compressão ligada. Um Config novo tem compress definido como true. Deixe-a ligada para builds normais; withCompress(false) não é uma otimização de tamanho (normalmente aumenta a saída). Recorra a ela para depurar ou perfilar o pipeline — ela desloca o tradeoff de CPU/memória (pulando a etapa de compressão) em vez de reduzir a memória.
    4. Aumente o memory_limit deliberadamente, por worker. Esta é uma configuração padrão do PHP, não uma chave do NextPDF. Defina-a no config do pool ou com ini_set('memory_limit', '256M') para o processo CLI/fila, e dimensione-a contra um pico perfilado, não um chute.
  • Relacionado. Streaming e memória.

Entrada: a memória cresce com a contagem de páginas em documentos muito grandes

Seção intitulada “Entrada: a memória cresce com a contagem de páginas em documentos muito grandes”
  • Sintoma. Um documento de muitos milhares de páginas esgota a memória mesmo que cada página seja pequena, e o pico sobe mais ou menos em sincronia com a contagem de páginas.
  • Causa provável. O writer em buffer mantém o documento serializado inteiro no heap. Para documentos muito grandes, esse é o custo dominante.
  • Resolução.
    1. Prefira o caminho de escrita por streaming. Use o caminho de escrita por streaming documentado descrito em Streaming e memória: ele serializa cada página à medida que é composta e libera o buffer, o que reduz o crescimento de buffer de página/saída; pequenos metadados por objeto (offsets, árvore de páginas) ainda podem escalar com a contagem de páginas/objetos. Siga o ponto de entrada documentado em vez de copiar classes internas — o engine de streaming subjacente é de nível experimental e seus símbolos não são a superfície pública estável.
    2. Para o parser writeHtml() nativo, lembre-se de que a memória do lado da entrada é limitada tanto pela profundidade de aninhamento quanto pelas proteções de contagem de elementos: a ADR-001 limita o aninhamento em MAX_NESTING_DEPTH = 100 e rejeita documentos acima de MAX_ELEMENT_COUNT = 50000. Um documento que atinge o limite de elementos é avisado disso explicitamente em vez de esgotar a memória silenciosamente. Esses limites da ADR-001 governam apenas o parser nativo; a ponte do Chrome opcional (writeHtmlChrome()) renderiza fora do processo e tem seus próprios limites separados de memória/entrada, não esses limites.
  • Relacionado. Streaming e memória.

Entrada: um worker de longa duração esgota a memória após muitos jobs

Seção intitulada “Entrada: um worker de longa duração esgota a memória após muitos jobs”
  • Sintoma. Renders individuais funcionam, mas um worker de fila que renderiza muitos PDFs em sequência esgota a memória após minutos ou horas.
  • Causa provável. Um processo PHP de longa duração acumula alocações entre jobs. Um crescimento lento que é invisível em uma requisição se acumula ao longo de milhares.
  • Resolução.
    1. Compartilhe registros, recrie documentos. Construa o FontRegistry e o ImageRegistry uma vez no boot e passe-os a um DocumentFactory; crie um Document novo por job com $factory->create($config). A análise de fontes e imagens então acontece uma vez para o processo, não uma vez por job, e a árvore de documento por job é coletada quando sai de escopo. Siga examples/14-worker-factory.php.
    2. Limite o cache de imagens compartilhado com new ImageRegistry(maxCacheBytes: ...) para que ele não possa crescer sem limite entre jobs.
    3. Recicle o worker — controle de processo, não uma garantia do engine. No PHP-FPM, defina pm.max_requests para que cada filho seja recriado após um número fixo de requisições. Em filas do Laravel use queue:work --max-jobs / --max-time / --memory; no Symfony Messenger use messenger:consume --limit / --time-limit / --memory-limit.
  • Relacionado. Streaming e memória.

Entrada: queda de throughput em um processo frio ou pouco aquecido

Seção intitulada “Entrada: queda de throughput em um processo frio ou pouco aquecido”
  • Sintoma. Os primeiros renders em um processo novo são lentos, ou cada requisição paga um custo de análise que requisições quentes não deveriam pagar.
  • Causa provável. Dois custos de cold-start se acumulam. O PHP sem opcache recompila cada arquivo a cada requisição, e um FontRegistry não aquecido analisa cada face de fonte na primeira vez que é usado.
  • Resolução.
    1. Habilite o opcache (e o JIT onde ajuda). Defina opcache.enable=1 e um opcache.memory_consumption generoso; em produção defina opcache.validate_timestamps=0 para que o cache não seja re-checado por requisição. Essa configuração exige um processo de deploy que reinicie ou recarregue o PHP-FPM (ou de outra forma reset o opcache, por exemplo opcache_reset() / cachetool) a cada release — caso contrário, o opcache continua servindo o bytecode antigo e código obsoleto roda após um deploy. Estas são configurações ini padrão do PHP, não chaves do NextPDF.
    2. Aqueça e trave o registro de fontes no boot. Em uma instância de FontRegistry, $fontRegistry->warmup($fontFiles) analisa os faces uma vez durante o boot, e $fontRegistry->lock() congela o registro para que código de tempo de requisição não possa mutar o estado compartilhado; $fontRegistry->isLocked() reporta o estado. Em um worker ou servidor de aplicação genuinamente de longa duração — um consumidor de fila ou um worker RoadRunner/Swoole/Octane que mantém o mesmo processo PHP vivo entre muitas requisições — um registro aquecido e travado persiste seus faces analisados no estado do objeto, transformando a análise de fontes por requisição em um custo único de boot do processo. Sob o modelo padrão de requisição do PHP-FPM, esse estado de objeto aquecido não sobrevive entre requisições: o opcache faz cache de classes compiladas e bytecode, não de estado de objeto userland aquecido, então um FontRegistry aquecido é reconstruído por requisição (re-executado a cada requisição a partir do bootstrap do filho), não mantido quente entre requisições dentro de um filho. No PHP-FPM puro, o opcache principalmente amortiza o custo de recompilação de bytecode; aceite que a análise de fontes é paga por requisição, não eliminada. A amortização entre requisições — analisar cada face uma vez durante o tempo de vida do processo — só se aplica em um processo genuinamente de longa duração, como um worker RoadRunner/Swoole/Octane ou um consumidor de fila que mantém o mesmo processo PHP vivo entre muitas requisições.
    3. Não reanalise o mesmo template por requisição. Resolva fontes e recursos reutilizáveis uma vez no boot por meio dos registros compartilhados; apenas o Document por job deve ser criado na requisição.
  • Relacionado. Streaming e memória.

Entrada: o servidor satura e a latência dispara sob concorrência

Seção intitulada “Entrada: o servidor satura e a latência dispara sob concorrência”
  • Sintoma. A latência por render está boa isoladamente, mas sob carga a máquina faz swap, a CPU satura, ou as requisições enfileiram e dão timeout.
  • Causa provável. Workers PHP-FPM demais para a RAM disponível, então a soma dos picos dos workers excede a memória física e o host faz swap; ou workers de menos, então as requisições serializam atrás de um pool pequeno.
  • Resolução.
    1. Dimensione pm.max_children a partir de um pico perfilado. Use a fórmula padrão:

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

      Meça o pico real de um worker com um documento representativo (veja a nota de perfilagem em Escopo), reserve folga para o SO e quaisquer serviços colocados, e divida. Deixe uma margem; não dimensione para 100% da RAM.

    2. Fixe o custo de compressão no seu orçamento. A compressão Flate pode ser um custo significativo de CPU ao escrever um stream e escala com o volume de bytes de stream comprimíveis, então a contagem de páginas e o volume de fontes incorporadas influenciam a CPU por render; o processamento de imagens, o subsetting de fontes e a análise de entrada também podem dominar. Meça com documentos representativos, e considere o driver real quando você escolher a contagem de workers e a CPU.

    3. Defina pm.max_requests junto com pm.max_children para que os filhos reciclem e recuperem qualquer crescimento lento, como na entrada de worker acima.

  • Relacionado. Streaming e memória.

Entrada: entrada não confiável grande é lenta ou cara de analisar

Seção intitulada “Entrada: entrada não confiável grande é lenta ou cara de analisar”
  • Sintoma. Um render é lento ou pesado em memória em uma entrada grande ou profundamente aninhada, especialmente HTML ou uma fonte que você não produziu.
  • Causa provável. O custo de análise escala com o tamanho e a estrutura da entrada. Uma entrada patológica (aninhamento profundo, uma contagem enorme de elementos ou uma fonte malformada) pode dominar o orçamento.
  • Resolução.
    1. Apoie-se nos limites do engine. O parser HTML writeHtml() nativo impõe MAX_NESTING_DEPTH = 100 e MAX_ELEMENT_COUNT = 50000 (ADR-001); entradas acima desses limites são rejeitadas em vez de permitidas a esgotar o processo. (A ponte do Chrome opcional, writeHtmlChrome(), está fora de escopo para esses limites da ADR-001 e impõe seus próprios limites separados de memória/entrada.)
    2. Trate fontes fornecidas por quem chama como não confiáveis. Uma fonte malformada lança NextPDF\Exception\FontParsingException em vez de corromper a saída, então capture a exceção específica e rejeite a entrada em vez de tentar de novo.
    3. Valide e dimensione as entradas na sua fronteira, e aplique limites em nível de requisição sobre o tamanho do documento para conteúdo influenciado por quem chama.
  • Relacionado. Solução de problemas: fontes e tagging.
SintomaAlavanca mais provável
Allowed memory size … exhausted em um único renderReduza $config->withImageCacheBytes(); encolha imagens antes de incorporar; aumente o memory_limit por worker
O pico de memória sobe com a contagem de páginasUse o caminho de escrita por streaming documentado
A memória do worker sobe ao longo de muitos jobsCompartilhe FontRegistry/ImageRegistry via DocumentFactory; defina pm.max_requests / --max-jobs
Primeiras requisições lentas, custo de análise por requisiçãoHabilite o opcache; $fontRegistry->warmup() e depois ->lock() no boot
Host faz swap / latência dispara sob cargaDimensione pm.max_children = (RAM − overhead) / pico por worker
Lento ou pesado em entrada grande/não confiávelApoie-se nos limites da ADR-001; rejeite fontes malformadas em FontParsingException
  • imageCacheBytes é um teto de memória, não um botão de tamanho. Reduzi-lo limita o cache para que um build falhe rápido; ele nunca reamostra nem recodifica as imagens que você incorpora. A Core não tem controle de qualidade de imagem.
  • withCompress(false) torna os arquivos maiores e é um auxílio de depuração/perfilagem. Não é uma otimização de tamanho; ele desloca o tradeoff de CPU/memória (pula a etapa de compressão) em vez de reduzir a memória.
  • O perfil de memória exato do engine de streaming é uma propriedade de nível experimental e pode mudar entre releases minor. Trate qualquer medição individual como uma observação, não uma constante portável.
  • memory_limit, opcache.*, pm.max_children e pm.max_requests são configurações padrão do PHP / PHP-FPM. O NextPDF não expõe suas próprias chaves para elas; configure-as no seu runtime, não no Config.

Glossário: streaming writer · font subsetting