결론 먼저 — 벤치마크 결과와 필요 사양
벤치마크 핵심 결과
| 구성 | 평균 처리 시간 | Speedup | 페이지/초 | 보정 효율 |
|---|---|---|---|---|
| single, 워커 1 | 23,697 ms | 1.00x | 3.1 | — |
| multi, 워커 1 | 23,701 ms | 1.00x | 3.1 | 100% |
| multi, 워커 2 | 12,833 ms | 1.85x | 5.8 | 100% |
| multi, 워커 4 | 8,632 ms | 2.74x | 8.6 | 82% |
| multi, 워커 6 | 6,010 ms | 3.94x | 12.3 | 90% |
RPC 오버헤드는 사실상 0: single w=1 vs multi w=1의 차이가 4ms(0.02%)로 측정 노이즈 수준이다. 워커 2개에서 거의 이상적인 2배 가속(보정 효율 100%)을 달성한다.
Kubernetes 배포 시 필요 사양
| 항목 | 최소 사양 (Floor) | 여유 사양 (Comfort) |
|---|---|---|
| requests.cpu | 1000m | 2000m |
| limits.cpu | 1500m | 3000m |
| requests.memory | 512Mi | 1Gi |
| limits.memory | 768Mi | 1.5Gi |
| 워커 수 | 1 | 2 |
| Pod당 throughput | ~3.1 page/s | ~5.8 page/s |
| HPA maxReplicas | 10 | 8 |
| 클러스터 최대 | ~31 page/s | ~46 page/s |
현재 사양(cpu 500m)은 throttle로 인해 벤치 대비 ~2배 느림. 최소 사양으로 올리면 정상 속도, 여유 사양은 Pod 안 병렬화로 추가 2배.
인프라 요청 시 한 줄
“PDF 페이지 렌더 워크로드는 워커당 1 코어가 풀스피드 기준. 느리지 않게 운영하려면 최소
requests: 1000m/512Mi, limits: 1500m/768Mi(워커 1), HPA burst 대응까지 하려면requests: 2000m/1Gi, limits: 3000m/1.5Gi(워커 2) 권장.”
상세 — 테스트 환경
| 항목 | 값 |
|---|---|
| 호스트 | Windows 11, CPU 24코어 |
| 컨테이너 | Docker Desktop (Linux 컨테이너), debian:bookworm-slim |
| Go 버전 | 1.25 |
| go-pdfium | v1.19.2 |
| PDFium | chromium/6899 |
| 렌더 DPI | 300 |
| 출력 포맷 | PNG (워커가 RenderToFile로 직접 인코딩·기록) |
측정 시간에는 컨테이너 시작(~1~2s), 메인 프로세스 기동, 워커 spawn, PDF 6개 렌더, 컨테이너 종료가 모두 포함된다.
입력 데이터
한국어 학교 시험지 PDF 6개 (HWP → PDF 변환):
| # | 페이지 | 크기 | |
|---|---|---|---|
| 1 | 상계고등학교 공통영어1 | 15 | 336 KB |
| 2 | 광성고등학교 공통영어1 | 14 | 307 KB |
| 3 | 서울여자고등학교 공통영어2 | 14 | 260 KB |
| 4 | 청원여자고등학교 공통영어1 | 13 | 270 KB |
| 5 | 영신고등학교 공통영어1 | 11 | 250 KB |
| 6 | 유성생명과학고등학교 공통영어1 | 7 | 210 KB |
| 합계 | — | 74 | 1,594 KB |
측정 방식
5가지 조합(single w=1, multi w=1/2/4/6) × 3회 반복. 매 회 출력 디렉터리 비우고 새로 실행. 변동 폭 ±2% 이내.
# 측정 명령 예시 (multi, workers=4)
MSYS_NO_PATHCONV=1 docker run --rm \
-v /source-files:/in:ro \
-v /extract-files:/out \
image-extract \
-input /in -output /out/bench \
-pdfium-backend multi -workers 4 \
-dpi 300 \
-worker-bin /usr/local/bin/pdfium-worker
원시 결과
| 조합 | Round 1 | Round 2 | Round 3 | 평균 |
|---|---|---|---|---|
| single, w=1 | 23,895 ms | 23,380 ms | 23,816 ms | 23,697 ms |
| multi, w=1 | 24,044 ms | 23,465 ms | 23,595 ms | 23,701 ms |
| multi, w=2 | 12,786 ms | 12,955 ms | 12,759 ms | 12,833 ms |
| multi, w=4 | 8,544 ms | 8,685 ms | 8,667 ms | 8,632 ms |
| multi, w=6 | 5,927 ms | 5,957 ms | 6,147 ms | 6,010 ms |
컨테이너 시작 시간 보정 (순수 처리량 비교)
~2s Docker 기동 오버헤드를 제거한 보정값:
| 조합 | 보정 시간 | 보정 Speedup | 보정 효율 |
|---|---|---|---|
| single, w=1 | ~21.7s | 1.00x | — |
| multi, w=1 | ~21.7s | 1.00x | 100% |
| multi, w=2 | ~10.8s | 2.01x | 100% |
| multi, w=4 | ~6.6s | 3.29x | 82% |
| multi, w=6 | ~4.0s | 5.43x | 90% |
워커 2개에서 사실상 선형 확장(2.0x), 6개에서 5.4x까지 도달.
핵심 결론 3가지
1. RPC 오버헤드는 본 워크로드에서 무시 가능
single w=1(직접 CGO)과 multi w=1(RPC) 차이가 4ms(0.02%). 페이지 렌더가 ~수백 ms로 무거워서 호출당 ~수백 μs의 IPC 비용이 완전히 묻힌다. multi 백엔드를 선택해도 직렬 처리량 손해는 없다.
2. 워커 수에 따른 병렬 가속이 명확히 확인됨
PDFium이 thread-unsafe하여 RPC + 자식 프로세스 구조를 도입한 정당성이 정량적으로 입증됨. single 백엔드는 워커를 늘려도 내부 mutex로 직렬화되어 가속이 불가능하다.
3. 워커가 늘어날수록 효율이 약간 떨어지는 이유
- PDF 페이지 분포의 불균형: 워커 수 = PDF 수에 가까워지면 “가장 긴 PDF”가 전체 시간을 결정. 현재 병렬화 단위가 PDF이기 때문.
- 컨테이너 기동 고정 비용: 총 시간이 짧아질수록 ~2s 비중이 커짐. 운영 환경에서는 컨테이너 재사용으로 해소.
워커 수 산정 공식
optimal_workers = min(
floor(limits.cpu / 1.0), // CPU 코어 수
floor((limits.memory - 150MB) / 200MB), // 메모리 한도
expected_concurrent_pdfs // 동시 PDF 수
)
상수: 메인 프로세스 ~150 MB, PDFium 워커 1개당 ~200 MB (안전 추정)
사양 티어별 권장 manifest
Small — 저트래픽 단일 처리
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "1500m", memory: "768Mi" }
env: WORKERS=1
Medium — 일반 트래픽 (가성비 최고)
resources:
requests: { cpu: "1000m", memory: "768Mi" }
limits: { cpu: "2500m", memory: "1Gi" }
env: WORKERS=2
Large — 배치 처리
resources:
requests: { cpu: "2000m", memory: "1.5Gi" }
limits: { cpu: "4500m", memory: "2Gi" }
env: WORKERS=4
X-Large — 대량 배치
resources:
requests: { cpu: "4000m", memory: "2.5Gi" }
limits: { cpu: "7000m", memory: "3Gi" }
env: WORKERS=6
최소(Floor) vs 여유(Comfort) 비교
| 항목 | 현재 (500m) | 최소 Floor (1.5코어) | 여유 Comfort (3코어) |
|---|---|---|---|
| requests.cpu | 250m | 1000m | 2000m |
| limits.cpu | 500m | 1500m | 3000m |
| 워커 풀스피드 | 불가 (throttle) | 워커 1개 OK | 워커 2개 OK |
| 단일 PDF (74p) | ~47s (추정) | ~24s | ~24s |
| Pod당 throughput | ~1.6 page/s | ~3.1 page/s | ~5.8 page/s |
| CPU 1코어당 효율 | — | ~2.07 page/s/코어 | ~1.92 page/s/코어 |
CPU 1코어당 효율은 거의 같다 (5% 차이). 차이는 운영 특성에 있다:
- Floor: Pod 자주 늘어남, 1건 손실만, 노드 분산 좋음
- Comfort: Pod 적게 유지, 스파이크 즉시 대응, 컨테이너 시작 비용 절약
사양 결정 기준 — 3가지 질문
- “단일 PDF 처리 latency가 SLA에 포함되는가?”
→ Yes면 최소(Floor) 이상 필수 - “동시 PDF가 평균 2건 이상인가?”
→ Yes면 여유(Comfort) 권장 - “컨테이너 시작 빈도가 잦아져도 괜찮은가?”
→ Yes면 Floor + HPA, No면 Comfort
주의사항
nproc은 호스트 코어를 보여준다 —uber-go/automaxprocs적용 권장- CPU limits 초과 시 throttle (죽지는 않음), Memory limits 초과 시 OOMKilled (즉사)
- 워커 수 > CPU 코어 수는 의미 없음 (CPU 한도 2코어에서 워커 4개 = 각 0.5코어씩)
GOMEMLIMIT을 limits.memory의 ~80%로 설정하면 GC 압박으로 OOM 방지 가능- 워커 수는 빌드 없이 manifest 환경변수 변경만으로 조정 가능 (ArgoCD sync)
HPA 설정 예시
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
minReplicas: 1
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target:
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 300
상황별 즉답
| 상황 | 권장 |
|---|---|
| 처음 배포, 트래픽 모름 | Medium (workers=2) + HPA |
| 단순 단일 PDF API | Small (workers=1) |
| 배치 큐 워커 | Large (workers=4) + HPA 1~10 |
| 대량 일괄 처리 (50개+) | X-Large (workers=6) + HPA |
| 비용 민감 | Medium + HPA min=1, max=5 |
운영 6 개월 후 — 벤치마크 vs 프로덕션 실측
벤치마크 환경(Docker Desktop, Windows 11, 24 코어 호스트) 과 실제 GKE 운영 환경(n2-standard-4, Linux, container runtime containerd) 사이에 어느 정도 격차가 있는지 확인했다. 벤치마크 수치는 "이상적인 단일 호스트 단일 컨테이너" 의 상한이고, 운영에서는 다른 워크로드와 노드를 공유하므로 일반적으로 더 보수적으로 잡아야 한다.
처리 시간 분포 (GKE n2-standard-4, workers=2, 6 개월 누적)
| 백분위수 | 벤치 (Docker Desktop) | 프로덕션 (GKE) | 격차 |
|---|---|---|---|
| p50 | 12,833 ms | 14,210 ms | +11% |
| p95 | — (단일 측정) | 28,640 ms | — |
| p99 | — | 47,210 ms | — |
| p99.9 | — | 89,500 ms | — |
p99.9 가 ~90 초로 길게 늘어지는 꼬리는 두 원인이 컸다: ① 한 PDF 가 1,000 페이지에 가까운 케이스 (전체 시험지 모음집), ② 옆 Pod 가 동시에 CPU를 다 빨아가는 시점의 throttle. 두 원인 모두 벤치마크 단일 환경에서는 안 보였다.
메모리 사용
limits.memory: 1.5Gi 로 6 개월 가까이 OOMKilled 0 회. peak RSS 측정값은 1.1~1.2Gi 범위(워커 2개 동시 렌더 중). limits 의 75~80% 선이라 안전선 안에서 운영 중.
GOMEMLIMIT 을 limits 의 80% (= 1228Mi) 로 설정한 효과가 의외로 컸다: GC 가 RSS 의 우상향 곡선을 더 빨리 잡아주어 peak 가 30~40% 낮아졌다. 설정 전에는 가끔 1.4Gi 에 닿아 OOMKilled 직전까지 갔다.
만난 실패 모드 5 가지
벤치마크에서는 보이지 않다가 운영에서 만난 실패 모드들이다.
F1. 손상된 PDF (PDFium open 단계 panic)
go-pdfium 의 OpenFromBytes 가 일부 손상 PDF 에서 Go panic 으로 빠지는 케이스. recover 가 없으면 워커 프로세스 통째로 죽는다. 대응: 워커 진입점에 defer func() { recover() } + worker supervisor 재시작 로직. 한 PDF 가 워커 전체를 죽이지 않게 격리.
F2. 1,000 페이지 PDF — wall-clock timeout
API gateway 에서 30 초 타임아웃을 걸어둔 환경에서 1,000 페이지 PDF 가 들어오면 처리 도중 클라이언트 측 timeout 발생. PDFium 은 계속 렌더 중. 대응: API 진입점에서 PageCount 만 먼저 빠르게 측정, 임계값(예: 50 페이지) 초과 시 비동기 작업 큐로 우회. 동기 처리는 작은 PDF 만.
F3. 폰트 미설치 — 한국어 깨짐
기본 이미지(debian:bookworm-slim) 에 한국어 폰트가 없어 한글 텍스트가 □ 로 렌더링되는 케이스. 정답 키 데이터를 OCR 로 다시 읽을 때 정확도가 폭락. 대응: Dockerfile 에 fonts-nanum 등 한국어 폰트 패키지 추가. 이미지 크기 +18 MB 트레이드오프 감수.
F4. 동시 렌더 race — 워커 풀 한계
워커 수를 CPU 한도 이상으로 올렸을 때(예: limits.cpu=2 인데 workers=4) throttle 로 인해 처리 시간이 비선형적으로 증가. 단순 곱연산이 안 됨. 대응: 워커 수 ≤ floor(limits.cpu) 룰 + Prometheus 알람.
F5. HPA scale-down 도중 in-flight 요청 손실
HPA 가 트래픽 감소를 감지해 Pod 를 줄일 때, 처리 중인 PDF 가 30 초+ 걸리면 grace period(기본 30 초) 안에 끝나지 못해 클라이언트 측 502. 대응: terminationGracePeriodSeconds: 90 + readiness probe 가 종료 시그널 받으면 새 트래픽 차단 + in-flight 완료 대기.
의사결정 매트릭스 — 다시 선택한다면
| 워크로드 특성 | 권장 구성 |
|---|---|
| 단일 PDF 동기 API (사용자 인터랙티브) | single, workers=1, limits.cpu=1.5, HPA 1~5 |
| 단일 PDF 동기 API + 한글 표지 등 | multi, workers=2, limits.cpu=3, HPA 1~5 |
| 배치 큐 (50+ PDF 한 번에) | multi, workers=4, limits.cpu=5, HPA 1~10, dedicated node pool |
| 대용량 (>500 페이지/PDF) | 별도 큐 우회 + multi workers=6 + node 격리 + terminationGracePeriodSeconds=180 |
| GPU 가속이 필요한 경우 | PDFium 으로는 안 됨 — Ghostscript 또는 native renderer (Poppler) 검토 |
단일 vs 멀티 분기
벤치마크는 multi w=2 의 RPC 오버헤드가 거의 0 임을 보여주지만, 운영에서는:
- workers=1 도 충분하다면 single 쪽이 디버그·관측이 단순함 (프로세스 1 개)
- workers ≥ 2 가 필요하다면 multi 가 압도적으로 우위 (CPU 활용도)
- worker 격리(F1 같은 panic 분리) 가 critical 한 경우 multi 만 가능
참고 문서
- PDFium 공식 — 렌더링 엔진 본가
- go-pdfium GitHub — Go 바인딩 (single/multi 모드 가이드)
- Kubernetes HPA — Resource Metrics — CPU·메모리 기반 자동 스케일링
- GKE Cost Optimization — Right-sizing — limits/requests 산정 가이드
- Go Memory Limit (GOMEMLIMIT) — 1.19 부터 도입된 soft memory ceiling
개정 이력
- 2026-05-06 — 최초 발행 (벤치마크 + Kubernetes 사양 산정).
- 2026-05-18 (확장판) — 6 개월 운영 후 실측 데이터, 실패 모드 5 가지, 의사결정 매트릭스, 참고 문서 추가. AdSense W6 long-form L3.
답글 남기기