문제 해결: 메모리 및 성능
이 항목들은 부하 상황에서 마주치는 두 가지 실패 계열을 다룹니다. 렌더링 중에 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너머로 밀어낼 수 있습니다. - 해결.
- 이미지 캐시를 제한하십시오.
NextPDF\Core\Config는imageCacheBytes(기본값52428800, 즉 50 MB)를 노출합니다. 인스턴스 wither$config->withImageCacheBytes($bytes)(시그니처withImageCacheBytes(int $bytes): self)로 낮추면, 여러 이미지를 임베드하는 빌드가 스와핑하는 대신 알려진 상한에서 빠르게 실패합니다. 이는 메모리 내 이미지 캐시에 상한을 둘 뿐이며, 이미지 자체를 리샘플링하거나 재인코딩하지 않습니다. - 임베드 전에 입력을 줄이십시오. Core는 이미지를 다운스케일하거나 재인코딩하지 않습니다. 과도하게 큰 래스터 아트는 임베드하기 전에 크기를 조정하고 재인코딩하며, 실제로 사용하는 글꼴을 임베드하여 서브셋팅이 유지할 글리프 세트를 작게 유지하십시오 (PDF 파일 크기 줄이기 참고).
- 압축을 켠 상태로 유지하십시오. 새
Config는compress가true로 설정되어 있습니다. 정상적인 빌드에서는 켜 두십시오.withCompress(false)는 크기 최적화가 아닙니다(보통 출력을 늘립니다). 파이프라인을 디버그하거나 프로파일링할 때 사용하십시오 — 메모리를 줄이는 것이 아니라 CPU/메모리 트레이드오프를 옮길 뿐입니다(압축 단계를 건너뜀). - 워커별로
memory_limit를 의도적으로 올리십시오. 이는 NextPDF 키가 아니라 표준 PHP 설정입니다. 풀 구성에서, 또는 CLI/큐 프로세스에 대해ini_set('memory_limit', '256M')로 설정하고, 추측이 아니라 프로파일링된 최대치를 기준으로 크기를 산정하십시오.
- 이미지 캐시를 제한하십시오.
- 관련 항목. 스트리밍 및 메모리.
항목: 매우 큰 문서에서 페이지 수에 따라 메모리가 증가함
섹션 제목: “항목: 매우 큰 문서에서 페이지 수에 따라 메모리가 증가함”- 증상. 각 페이지가 작은데도 수천 페이지짜리 문서가 메모리를 고갈시키며, 최대치가 대략 페이지 수에 발맞춰 상승합니다.
- 유력한 원인. 버퍼링 라이터가 직렬화된 문서 전체를 힙에 보관합니다. 매우 큰 문서에서는 그것이 지배적인 비용입니다.
- 해결.
- 스트리밍 쓰기 경로를 선호하십시오.
스트리밍 및 메모리에 설명된
문서화된 스트리밍 쓰기 경로를 사용하십시오. 이는 각 페이지를 구성하는 대로
직렬화하고 버퍼를 해제하여 페이지 버퍼/출력 증가를 줄입니다. 작은 객체별
메타데이터(오프셋, 페이지 트리)는 여전히 페이지/객체 수에 따라 확장될 수
있습니다. 내부 클래스를 복사하는 대신 문서화된 진입점을 따르십시오 —
기저의 스트리밍 엔진은
experimental등급이며 그 심볼은 안정적인 공개 표면이 아닙니다. - 네이티브
writeHtml()파서의 경우, 입력 측 메모리가 중첩 깊이와 요소 수 가드 양쪽으로 제한된다는 점을 기억하십시오. ADR-001은 중첩을MAX_NESTING_DEPTH = 100으로 제한하고MAX_ELEMENT_COUNT = 50000을 초과하는 문서를 거부합니다. 요소 상한에 도달한 문서는 조용히 메모리를 고갈시키는 대신 명시적으로 그 사실을 통보받습니다. 이 ADR-001 상한은 네이티브 파서에만 적용됩니다. 선택적 Chrome 브리지(writeHtmlChrome())는 프로세스 밖에서 렌더링하며 이 상한이 아니라 자체적인 별도의 메모리/입력 제한을 가집니다.
- 스트리밍 쓰기 경로를 선호하십시오.
스트리밍 및 메모리에 설명된
문서화된 스트리밍 쓰기 경로를 사용하십시오. 이는 각 페이지를 구성하는 대로
직렬화하고 버퍼를 해제하여 페이지 버퍼/출력 증가를 줄입니다. 작은 객체별
메타데이터(오프셋, 페이지 트리)는 여전히 페이지/객체 수에 따라 확장될 수
있습니다. 내부 클래스를 복사하는 대신 문서화된 진입점을 따르십시오 —
기저의 스트리밍 엔진은
- 관련 항목. 스트리밍 및 메모리.
항목: 장기 실행 워커가 많은 작업 후 메모리를 고갈시킴
섹션 제목: “항목: 장기 실행 워커가 많은 작업 후 메모리를 고갈시킴”- 증상. 단일 렌더링은 성공하지만, 많은 PDF를 연속으로 렌더링하는 큐 워커가 수 분 또는 수 시간 후 메모리를 고갈시킵니다.
- 유력한 원인. 장기 실행 PHP 프로세스가 작업에 걸쳐 할당을 누적합니다. 한 요청에서는 보이지 않는 느린 증가가 수천 건에 걸쳐 누적됩니다.
- 해결.
- 레지스트리는 공유하고 문서는 재생성하십시오. 부팅 시
FontRegistry와ImageRegistry를 한 번 구축하여DocumentFactory에 전달하고, 작업마다$factory->create($config)로 새Document를 만드십시오. 그러면 글꼴 및 이미지 파싱이 작업당 한 번이 아니라 프로세스당 한 번 일어나며, 작업별 문서 트리는 범위를 벗어날 때 수집됩니다.examples/14-worker-factory.php를 따르십시오. new ImageRegistry(maxCacheBytes: ...)로 공유 이미지 캐시를 제한하여 작업에 걸쳐 무한정 커지지 않도록 하십시오.- 워커를 재활용하십시오 — 엔진의 보장이 아니라 프로세스 제어입니다.
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는 각 글꼴 페이스를 처음 사용할 때 파싱합니다. - 해결.
- opcache를 활성화하십시오(그리고 도움이 되는 곳에서는 JIT도).
opcache.enable=1과 넉넉한opcache.memory_consumption을 설정하십시오. 프로덕션에서는 캐시가 요청마다 다시 확인되지 않도록opcache.validate_timestamps=0을 설정하십시오. 그 설정은 모든 릴리스에서 PHP-FPM을 재시작 또는 재로드하는(또는 그 외의 방법으로 opcache를 재설정하는, 예를 들어opcache_reset()/cachetool) 배포 프로세스를 요구합니다 — 그렇지 않으면 opcache가 계속 오래된 바이트코드를 제공하여 배포 후에도 낡은 코드가 실행됩니다. 이들은 NextPDF 키가 아니라 표준 PHP ini 설정입니다. - 부팅 시 글꼴 레지스트리를 워밍업하고 잠그십시오.
FontRegistry인스턴스에서$fontRegistry->warmup($fontFiles)는 부팅 중에 페이스를 한 번 파싱하고,$fontRegistry->lock()은 레지스트리를 동결하여 요청 시점 코드가 공유 상태를 변형하지 못하게 하며,$fontRegistry->isLocked()는 그 상태를 보고합니다. 진정으로 장기 실행하는 워커나 애플리케이션 서버 — 큐 컨슈머나, 많은 요청에 걸쳐 동일한 PHP 프로세스를 살아 있게 유지하는 RoadRunner/Swoole/Octane 워커 — 에서는, 워밍업되고 잠긴 레지스트리가 파싱된 페이스를 객체 상태에 유지하여 요청별 글꼴 파싱을 일회성 프로세스 부팅 비용으로 바꿉니다. 표준 PHP-FPM 요청 모델에서는 그 워밍업된 객체 상태가 요청에 걸쳐 살아남지 않습니다. opcache는 컴파일된 클래스와 바이트코드를 캐시할 뿐 워밍업된 사용자 영역 객체 상태는 캐시하지 않으므로, 워밍업된FontRegistry는 자식 안에서 요청에 걸쳐 워밍업된 채 유지되는 것이 아니라 요청마다 다시 구축됩니다(각 요청마다 자식의 부트스트랩에서 다시 실행됨). 일반 PHP-FPM에서 opcache는 주로 바이트코드 재컴파일 비용을 분산할 뿐입니다. 글꼴 파싱이 제거되는 것이 아니라 요청마다 치러진다는 점을 받아들이십시오. 요청 간 분산 — 프로세스의 수명 동안 각 페이스를 한 번만 파싱하는 것 — 은 RoadRunner/Swoole/Octane 워커나 많은 요청에 걸쳐 동일한 PHP 프로세스를 살아 있게 유지하는 큐 컨슈머처럼 진정으로 장기 실행하는 프로세스에서만 적용됩니다. - 요청마다 동일한 템플릿을 다시 파싱하지 마십시오. 공유 레지스트리를
통해 부팅 시 글꼴과 재사용 가능한 리소스를 한 번 해석하고, 요청에서는 작업별
Document만 생성하십시오.
- opcache를 활성화하십시오(그리고 도움이 되는 곳에서는 JIT도).
- 관련 항목. 스트리밍 및 메모리.
항목: 동시성 하에서 서버가 포화되고 지연 시간이 급증함
섹션 제목: “항목: 동시성 하에서 서버가 포화되고 지연 시간이 급증함”- 증상. 단독으로는 렌더링별 지연 시간이 괜찮지만, 부하 상황에서는 박스가 스와핑하거나 CPU가 포화되거나 요청이 큐에 쌓이며 타임아웃됩니다.
- 유력한 원인. 가용 RAM에 비해 PHP-FPM 워커가 너무 많아서 워커 최대치의 합이 물리 메모리를 초과하여 호스트가 스와핑하거나, 워커가 너무 적어서 요청이 작은 풀 뒤에서 직렬화됩니다.
- 해결.
-
프로파일링된 최대치로
pm.max_children을 산정하십시오. 표준 공식을 사용하십시오.pm.max_children = (total RAM - OS/other overhead) / per-worker peak memory대표적인 문서로 워커의 실제 최대치를 측정하고(범위 섹션의 프로파일링 참고), OS 및 함께 배치된 서비스를 위한 여유를 확보한 뒤 나누십시오. 여유를 두십시오. RAM의 100%로 산정하지 마십시오.
-
예산에서 압축 비용을 고정하십시오. Flate 압축은 스트림 쓰기의 상당한 CPU 비용일 수 있고 압축 가능한 스트림 바이트의 양에 따라 확장되므로, 페이지 수와 임베드 글꼴 양이 렌더링별 CPU에 영향을 줍니다. 이미지 처리, 글꼴 서브셋팅, 입력 파싱도 지배적일 수 있습니다. 대표적인 문서로 측정하고, 워커 수와 CPU를 선택할 때 실제 동인을 고려하십시오.
-
위의 워커 항목처럼 자식이 재활용되고 느린 증가를 회수하도록
pm.max_children옆에pm.max_requests를 설정하십시오.
-
- 관련 항목. 스트리밍 및 메모리.
항목: 크고 신뢰할 수 없는 입력이 파싱하기에 느리거나 비용이 큼
섹션 제목: “항목: 크고 신뢰할 수 없는 입력이 파싱하기에 느리거나 비용이 큼”- 증상. 크거나 깊게 중첩된 입력, 특히 HTML이나 직접 만들지 않은 글꼴에서 렌더링이 느리거나 메모리가 무겁습니다.
- 유력한 원인. 파싱 비용은 입력 크기와 구조에 따라 확장됩니다. 병적인 입력(깊은 중첩, 엄청난 요소 수, 또는 잘못된 형식의 글꼴)이 예산을 지배할 수 있습니다.
- 해결.
- 엔진의 경계에 의존하십시오. 네이티브
writeHtml()HTML 파서는MAX_NESTING_DEPTH = 100과MAX_ELEMENT_COUNT = 50000(ADR-001)을 강제합니다. 그 상한을 넘는 입력은 프로세스를 고갈시키도록 허용되는 대신 거부됩니다. (선택적 Chrome 브리지writeHtmlChrome()는 이 ADR-001 상한의 범위 밖이며 자체적인 별도의 메모리/입력 제한을 강제합니다.) - 호출자가 제공한 글꼴을 신뢰할 수 없는 것으로 취급하십시오. 잘못된 형식의
글꼴은 출력을 손상시키는 대신
NextPDF\Exception\FontParsingException을 발생시키므로, 그 특정 예외를 포착하고 재시도하는 대신 입력을 거부하십시오. - 경계에서 입력을 검증하고 크기를 산정하며, 호출자 영향을 받는 콘텐츠에 대해서는 문서 크기에 요청 수준 제한을 적용하십시오.
- 엔진의 경계에 의존하십시오. 네이티브
- 관련 항목. 문제 해결: 글꼴 및 태깅.
결정 표: 증상에서 레버로
섹션 제목: “결정 표: 증상에서 레버로”| 증상 | 가장 유력한 레버 |
|---|---|
단일 렌더링에서 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가 아니라 런타임에서 구성하십시오.
참고 자료
섹션 제목: “참고 자료”- 스트리밍 및 메모리 — 스트리밍 모델, ADR-001 경계, 그리고 전체 배치 워커 튜토리얼.
- PDF 파일 크기 줄이기 — 압축과 글꼴 서브셋팅, 두 가지 실제 크기 제어.
- 문제 해결: 글꼴 및 태깅 — 글꼴 해석, 파싱, 서브셋팅 실패.
- 기술 자료 색인