콘텐츠로 이동
getnextpdf.com

문제 해결: 메모리 및 성능

이 항목들은 부하 상황에서 마주치는 두 가지 실패 계열을 다룹니다. 렌더링 중에 PHP가 메모리를 다 쓰는 경우, 그리고 프로세스가 워밍업되거나 포화되면 처리량이 절벽처럼 떨어지는 경우입니다. 각 항목은 증상, 가장 유력한 원인, 그리고 실제 NextPDF 표면이나 표준 PHP-FPM 컨트롤을 사용하는 해결책을 명시합니다. 기저의 스트리밍 모델과 워커 튜토리얼은 스트리밍 및 메모리를 읽으십시오. 이 페이지는 그것의 인시던트 측 동반 자료입니다.

먼저 측정하십시오. 엔진의 벤치마크가 렌더링별 비용을 격리하는 방식대로, 렌더링 전후로 memory_get_peak_usage(true)를 샘플링하고 반복 사이에 memory_reset_peak_usage()를 호출하십시오. 기준선 없이 튜닝하면 절벽이 제거되는 것이 아니라 이동할 뿐입니다.

항목: 생성 중 “Allowed memory size exhausted”

섹션 제목: “항목: 생성 중 “Allowed memory size exhausted””
  • 증상. 렌더링이 PHP 런타임에서 발생한 치명적 Allowed memory size of <n> bytes exhausted와 함께 중단되며, 흔히 크거나 이미지가 많은 문서에서 발생합니다.
  • 유력한 원인. 기본 쓰기 경로는 문서 전체를 구성한 뒤 직렬화하므로, 최대 메모리가 총 출력 크기를 따라갑니다. 큰 문서, 큰 임베드 이미지, 또는 큰 임베드 글꼴 페이스가 요청을 memory_limit 너머로 밀어낼 수 있습니다.
  • 해결.
    1. 이미지 캐시를 제한하십시오. NextPDF\Core\ConfigimageCacheBytes(기본값 52428800, 즉 50 MB)를 노출합니다. 인스턴스 wither $config->withImageCacheBytes($bytes)(시그니처 withImageCacheBytes(int $bytes): self)로 낮추면, 여러 이미지를 임베드하는 빌드가 스와핑하는 대신 알려진 상한에서 빠르게 실패합니다. 이는 메모리 내 이미지 캐시에 상한을 둘 뿐이며, 이미지 자체를 리샘플링하거나 재인코딩하지 않습니다.
    2. 임베드 전에 입력을 줄이십시오. Core는 이미지를 다운스케일하거나 재인코딩하지 않습니다. 과도하게 큰 래스터 아트는 임베드하기 전에 크기를 조정하고 재인코딩하며, 실제로 사용하는 글꼴을 임베드하여 서브셋팅이 유지할 글리프 세트를 작게 유지하십시오 (PDF 파일 크기 줄이기 참고).
    3. 압축을 켠 상태로 유지하십시오.Configcompresstrue로 설정되어 있습니다. 정상적인 빌드에서는 켜 두십시오. withCompress(false)는 크기 최적화가 아닙니다(보통 출력을 늘립니다). 파이프라인을 디버그하거나 프로파일링할 때 사용하십시오 — 메모리를 줄이는 것이 아니라 CPU/메모리 트레이드오프를 옮길 뿐입니다(압축 단계를 건너뜀).
    4. 워커별로 memory_limit를 의도적으로 올리십시오. 이는 NextPDF 키가 아니라 표준 PHP 설정입니다. 풀 구성에서, 또는 CLI/큐 프로세스에 대해 ini_set('memory_limit', '256M')로 설정하고, 추측이 아니라 프로파일링된 최대치를 기준으로 크기를 산정하십시오.
  • 관련 항목. 스트리밍 및 메모리.

항목: 매우 큰 문서에서 페이지 수에 따라 메모리가 증가함

섹션 제목: “항목: 매우 큰 문서에서 페이지 수에 따라 메모리가 증가함”
  • 증상. 각 페이지가 작은데도 수천 페이지짜리 문서가 메모리를 고갈시키며, 최대치가 대략 페이지 수에 발맞춰 상승합니다.
  • 유력한 원인. 버퍼링 라이터가 직렬화된 문서 전체를 힙에 보관합니다. 매우 큰 문서에서는 그것이 지배적인 비용입니다.
  • 해결.
    1. 스트리밍 쓰기 경로를 선호하십시오. 스트리밍 및 메모리에 설명된 문서화된 스트리밍 쓰기 경로를 사용하십시오. 이는 각 페이지를 구성하는 대로 직렬화하고 버퍼를 해제하여 페이지 버퍼/출력 증가를 줄입니다. 작은 객체별 메타데이터(오프셋, 페이지 트리)는 여전히 페이지/객체 수에 따라 확장될 수 있습니다. 내부 클래스를 복사하는 대신 문서화된 진입점을 따르십시오 — 기저의 스트리밍 엔진은 experimental 등급이며 그 심볼은 안정적인 공개 표면이 아닙니다.
    2. 네이티브 writeHtml() 파서의 경우, 입력 측 메모리가 중첩 깊이와 요소 수 가드 양쪽으로 제한된다는 점을 기억하십시오. ADR-001은 중첩을 MAX_NESTING_DEPTH = 100으로 제한하고 MAX_ELEMENT_COUNT = 50000을 초과하는 문서를 거부합니다. 요소 상한에 도달한 문서는 조용히 메모리를 고갈시키는 대신 명시적으로 그 사실을 통보받습니다. 이 ADR-001 상한은 네이티브 파서에만 적용됩니다. 선택적 Chrome 브리지(writeHtmlChrome())는 프로세스 밖에서 렌더링하며 이 상한이 아니라 자체적인 별도의 메모리/입력 제한을 가집니다.
  • 관련 항목. 스트리밍 및 메모리.

항목: 장기 실행 워커가 많은 작업 후 메모리를 고갈시킴

섹션 제목: “항목: 장기 실행 워커가 많은 작업 후 메모리를 고갈시킴”
  • 증상. 단일 렌더링은 성공하지만, 많은 PDF를 연속으로 렌더링하는 큐 워커가 수 분 또는 수 시간 후 메모리를 고갈시킵니다.
  • 유력한 원인. 장기 실행 PHP 프로세스가 작업에 걸쳐 할당을 누적합니다. 한 요청에서는 보이지 않는 느린 증가가 수천 건에 걸쳐 누적됩니다.
  • 해결.
    1. 레지스트리는 공유하고 문서는 재생성하십시오. 부팅 시 FontRegistryImageRegistry를 한 번 구축하여 DocumentFactory에 전달하고, 작업마다 $factory->create($config)로 새 Document를 만드십시오. 그러면 글꼴 및 이미지 파싱이 작업당 한 번이 아니라 프로세스당 한 번 일어나며, 작업별 문서 트리는 범위를 벗어날 때 수집됩니다. 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를 사용하십시오.
  • 관련 항목. 스트리밍 및 메모리.

항목: 콜드 또는 충분히 워밍업되지 않은 프로세스에서의 처리량 절벽

섹션 제목: “항목: 콜드 또는 충분히 워밍업되지 않은 프로세스에서의 처리량 절벽”
  • 증상. 새 프로세스에서의 첫 렌더링이 느리거나, 워밍업된 요청은 치르지 않아야 할 파싱 비용을 모든 요청이 치릅니다.
  • 유력한 원인. 두 가지 콜드 스타트 비용이 누적됩니다. opcache 없는 PHP는 요청마다 모든 파일을 재컴파일하고, 워밍업되지 않은 FontRegistry는 각 글꼴 페이스를 처음 사용할 때 파싱합니다.
  • 해결.
    1. opcache를 활성화하십시오(그리고 도움이 되는 곳에서는 JIT도). opcache.enable=1과 넉넉한 opcache.memory_consumption을 설정하십시오. 프로덕션에서는 캐시가 요청마다 다시 확인되지 않도록 opcache.validate_timestamps=0을 설정하십시오. 그 설정은 모든 릴리스에서 PHP-FPM을 재시작 또는 재로드하는(또는 그 외의 방법으로 opcache를 재설정하는, 예를 들어 opcache_reset() / cachetool) 배포 프로세스를 요구합니다 — 그렇지 않으면 opcache가 계속 오래된 바이트코드를 제공하여 배포 후에도 낡은 코드가 실행됩니다. 이들은 NextPDF 키가 아니라 표준 PHP ini 설정입니다.
    2. 부팅 시 글꼴 레지스트리를 워밍업하고 잠그십시오. FontRegistry 인스턴스에서 $fontRegistry->warmup($fontFiles)는 부팅 중에 페이스를 한 번 파싱하고, $fontRegistry->lock()은 레지스트리를 동결하여 요청 시점 코드가 공유 상태를 변형하지 못하게 하며, $fontRegistry->isLocked()는 그 상태를 보고합니다. 진정으로 장기 실행하는 워커나 애플리케이션 서버 — 큐 컨슈머나, 많은 요청에 걸쳐 동일한 PHP 프로세스를 살아 있게 유지하는 RoadRunner/Swoole/Octane 워커 — 에서는, 워밍업되고 잠긴 레지스트리가 파싱된 페이스를 객체 상태에 유지하여 요청별 글꼴 파싱을 일회성 프로세스 부팅 비용으로 바꿉니다. 표준 PHP-FPM 요청 모델에서는 그 워밍업된 객체 상태가 요청에 걸쳐 살아남지 않습니다. opcache는 컴파일된 클래스와 바이트코드를 캐시할 뿐 워밍업된 사용자 영역 객체 상태는 캐시하지 않으므로, 워밍업된 FontRegistry는 자식 안에서 요청에 걸쳐 워밍업된 채 유지되는 것이 아니라 요청마다 다시 구축됩니다(각 요청마다 자식의 부트스트랩에서 다시 실행됨). 일반 PHP-FPM에서 opcache는 주로 바이트코드 재컴파일 비용을 분산할 뿐입니다. 글꼴 파싱이 제거되는 것이 아니라 요청마다 치러진다는 점을 받아들이십시오. 요청 간 분산 — 프로세스의 수명 동안 각 페이스를 한 번만 파싱하는 것 — 은 RoadRunner/Swoole/Octane 워커나 많은 요청에 걸쳐 동일한 PHP 프로세스를 살아 있게 유지하는 큐 컨슈머처럼 진정으로 장기 실행하는 프로세스에서만 적용됩니다.
    3. 요청마다 동일한 템플릿을 다시 파싱하지 마십시오. 공유 레지스트리를 통해 부팅 시 글꼴과 재사용 가능한 리소스를 한 번 해석하고, 요청에서는 작업별 Document만 생성하십시오.
  • 관련 항목. 스트리밍 및 메모리.

항목: 동시성 하에서 서버가 포화되고 지연 시간이 급증함

섹션 제목: “항목: 동시성 하에서 서버가 포화되고 지연 시간이 급증함”
  • 증상. 단독으로는 렌더링별 지연 시간이 괜찮지만, 부하 상황에서는 박스가 스와핑하거나 CPU가 포화되거나 요청이 큐에 쌓이며 타임아웃됩니다.
  • 유력한 원인. 가용 RAM에 비해 PHP-FPM 워커가 너무 많아서 워커 최대치의 합이 물리 메모리를 초과하여 호스트가 스와핑하거나, 워커가 너무 적어서 요청이 작은 풀 뒤에서 직렬화됩니다.
  • 해결.
    1. 프로파일링된 최대치로 pm.max_children을 산정하십시오. 표준 공식을 사용하십시오.

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

      대표적인 문서로 워커의 실제 최대치를 측정하고(범위 섹션의 프로파일링 참고), OS 및 함께 배치된 서비스를 위한 여유를 확보한 뒤 나누십시오. 여유를 두십시오. RAM의 100%로 산정하지 마십시오.

    2. 예산에서 압축 비용을 고정하십시오. Flate 압축은 스트림 쓰기의 상당한 CPU 비용일 수 있고 압축 가능한 스트림 바이트의 양에 따라 확장되므로, 페이지 수와 임베드 글꼴 양이 렌더링별 CPU에 영향을 줍니다. 이미지 처리, 글꼴 서브셋팅, 입력 파싱도 지배적일 수 있습니다. 대표적인 문서로 측정하고, 워커 수와 CPU를 선택할 때 실제 동인을 고려하십시오.

    3. 위의 워커 항목처럼 자식이 재활용되고 느린 증가를 회수하도록 pm.max_children 옆에 pm.max_requests를 설정하십시오.

  • 관련 항목. 스트리밍 및 메모리.

항목: 크고 신뢰할 수 없는 입력이 파싱하기에 느리거나 비용이 큼

섹션 제목: “항목: 크고 신뢰할 수 없는 입력이 파싱하기에 느리거나 비용이 큼”
  • 증상. 크거나 깊게 중첩된 입력, 특히 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() 낮추기; 임베드 전 이미지 줄이기; 워커별 memory_limit 올리기
페이지 수에 따라 최대 메모리 상승문서화된 스트리밍 쓰기 경로 사용
여러 작업에 걸쳐 워커 메모리 증가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 등급 속성이며 minor 릴리스 사이에 바뀔 수 있습니다. 단일 측정값은 이식 가능한 상수가 아니라 관찰 결과로 취급하십시오.
  • memory_limit, opcache.*, pm.max_children, pm.max_requests는 표준 PHP / PHP-FPM 설정입니다. NextPDF는 이들에 대한 자체 키를 노출하지 않습니다 — Config가 아니라 런타임에서 구성하십시오.

용어집: 스트리밍 라이터 · 글꼴 서브셋