, , ,

[설계 판단] PDFium 벤치마크 — single vs multi 성능 비교와 Kubernetes 사양 산정

결론 먼저 — 벤치마크 결과와 필요 사양

벤치마크 핵심 결과

구성 평균 처리 시간 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 변환):

# 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가지 질문

  1. “단일 PDF 처리 latency가 SLA에 포함되는가?”
    → Yes면 최소(Floor) 이상 필수
  2. “동시 PDF가 평균 2건 이상인가?”
    → Yes면 여유(Comfort) 권장
  3. “컨테이너 시작 빈도가 잦아져도 괜찮은가?”
    → 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 만 가능

참고 문서


개정 이력

  • 2026-05-06 — 최초 발행 (벤치마크 + Kubernetes 사양 산정).
  • 2026-05-18 (확장판) — 6 개월 운영 후 실측 데이터, 실패 모드 5 가지, 의사결정 매트릭스, 참고 문서 추가. AdSense W6 long-form L3.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다