Wintersalmon | Blog

블로그 CI가 20분 걸렸던 이유 — 캐시를 제거해서 해결하기까지

3 min read

증상

블로그 글 PR을 머지한 뒤 stag에 새 글이 20분 이상 반영되지 않았다. Flux image automation은 GHCR에 새 이미지가 올라오면 약 2분 안에 stag를 업데이트한다. 배포 자체가 느린 게 아니라 CI가 느린 것이었다.

gh run list --workflow "Blog CI/CD"를 확인하면 main push 빌드가 1223분이고, PR 빌드는 12분이었다.

PR 빌드가 빠른 이유

워크플로우에 if: github.ref == 'refs/heads/main' 조건이 있어서 PR 빌드는 lint, typecheck, test만 실행한다. 1~2분은 예상된 동작이다.

docker job 시간 분해

실제 빌드(PR #1051, run 27283634090)를 타임스탬프로 분해한 결과:

단계 소요 시간 비고
GHA 캐시 import 70초 mode=max 캐시 다운로드
COPY 레이어 × 63개 ~3초 workspace package.json 전체
RUN bun install 14초 988 패키지
RUN bun run build 3초 markdown + bundle + Tailwind
GHCR 이미지 push (#77) 27초 TLS timeout retry 포함
GHA 캐시 export (#78) 468초 (7분 48초) ← 진짜 병목

이미지 push는 27초에 끝났다. 나머지 약 8분은 GHA 캐시를 내보내는 시간이었다.

mode=max가 역효과인 이유

cache-to: type=gha,mode=maxoven/bun:1 베이스 이미지 전체를 포함한 모든 중간 빌드 레이어를 GHA 캐시 서버에 업로드한다. 수백 MB를 매 빌드마다 저장하는 것이다.

캐시 히트로 아낄 수 있는 최대 시간은 bun install 14초다.

캐시 오버헤드는:

  • import: 70초 (캐시 미스 시 전부 낭비)
  • export: 468초

완벽한 캐시 히트라 해도 계산은:

14초 절약, 70초 + 468초 소비 = 524초 손해

게다가 캐시 미스도 자주 발생했다. 첫 번째 실질 레이어가 COPY package.json bun.lock ./인데, 모노레포에서 어떤 앱이든 새 패키지를 추가하면 bun.lock이 변경되어 이 레이어가 무효화된다. 활발히 개발 중인 프로젝트에서는 거의 매 머지마다 캐시 미스가 난다.

해결

blog.yml에서 cache-fromcache-to를 제거:

-          cache-from: type=gha,scope=${{ github.workflow }}
-          cache-to: type=gha,mode=max,scope=${{ github.workflow }}

이제 bun install은 항상 실행되지만(14초), 538초의 캐시 오버헤드가 사라진다. docker job 총 소요 시간: 10분 이상 → 2분 이내.

교훈

GHA 캐시 mode=max는 전체 빌드 그래프를 저장한다. 느린 빌드 단계가 안정적으로 캐시될 때는 유용하지만, 캐시 대상 단계가 14초짜리일 때는 오버헤드가 거의 항상 이득을 초과한다.

빌드 캐시를 설정하기 전에 두 가지를 측정해야 한다:

  • 캐시 히트 시 무엇을 아끼나? (느린 단계를 실측)
  • 캐시 import + export에 얼마나 걸리나? (job 로그를 읽기)

오버헤드가 절약량보다 크다면, 캐시가 CI를 빠르게 만드는 게 아니라 느리게 만들고 있는 것이다.


Hungjoon

I'm Hungjoon, a software engineer based in South Korea. This is my long-form notebook — homelab, Kubernetes, AI infra, and whatever else keeps me up at night.