Skip to content

MSG-608 feat: 배포 직후 35초 동안 느려지던 지도 홈 집계를 기동 워밍업으로 데운다 - #288

Merged
s13121312 merged 6 commits into
developfrom
feature/MSG-608-jvm-warmup
Sep 28, 2026
Merged

s13121312 merged 6 commits into
developfrom
feature/MSG-608-jvm-warmup

Conversation

@s13121312

@s13121312 s13121312 commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

🎫 관련 티켓

  • Closes MSG-608
  • 발단: MSG-576 dev 실측(재기동 직후 콜드 회차)에서 원인이 미션 스냅숏이 아니라 JIT로 좁혀진 것. 요구 정본은 SRS NFR-PERF-01(지도 집계 p95 300ms)이고 PRD는 면제(기존 목표의 복구, 새 동작 없음)

작업 내용

dev 서버를 재기동한 직후 지도 홈 줌아웃(미션 집계·핫구역 집계)에 10배 부하(초당 300건)를 걸면 약 35초 동안 핫구역 집계 p95가 3.4초로 뛰고, 관계없는 격자 조회까지 2.6초로 끌려갑니다. 4분 뒤 같은 부하는 p95 130ms입니다. 원인은 JIT 컴파일1입니다. 재기동 후 첫 20초에 컴파일 시간이 36초분(2 vCPU 중 한 코어) 들고, 톰캣 스레드 200개가 전부 대기에 묶입니다.

이 PR은 앱이 뜬 뒤 스스로 그 세 경로를 localhost로 미리 불러 컴파일을 배포 시점으로 옮기는 기동 워밍업을 넣습니다. 워밍업이 도는 동안은 readiness 프로브2로 /actuator/health가 503이라 CD의 --wait가 끝날 때까지 기다립니다. 응답 계약, 스키마, 미션·핫구역·격자 코드는 바뀌지 않습니다.

flowchart LR
    subgraph boot["앱 기동"]
        S[Spring 컨텍스트 준비] --> R[ApplicationReadyEvent]
        R --> W["WarmupRunner (동기)<br/>핫구역·미션·격자 × N=2,000<br/>4스레드 · 상한 45초"]
        W -. "localhost HTTP<br/>필터·컨트롤러·JSON까지 데움" .-> API[기존 공개 API 3종]
        W --> A[ACCEPTING_TRAFFIC]
    end
    H["docker healthcheck<br/>GET /actuator/health"] -- "워밍업 중 503" --> R
    H -- "끝나면 200 → CD --wait 통과" --> A
    N[nginx] -- "항상 8080 프록시<br/>(차단 아님)" --> API
Loading

커밋 5개입니다.

  1. feat WarmupProperties·WarmupConfig(완성 RestClient 빈), application.yml에 fillmap.warmup.*(기본 enabled: false, iterations: 2000)과 management.endpoint.health.probes.enabled: true. 테스트 리소스는 enabled: false 명시.
  2. feat WarmupRunner — ApplicationReadyEvent 동기 리스너. 대상별 N건을 고정 뷰포트 8개(서울·부산, 구·시 단위)로 번갈아 부르고, 호출별 예외는 실패 수로만 세며 밖으로 내지 않습니다. 격자 조회는 로그인이 필요해 TokenProvider로 프로세스 안에서 토큰을 만듭니다(grid-user-id 없으면 그 대상만 건너뜀). 단위 테스트 10건.
  3. test WarmupReadinessIntegrationTest(워밍업 중 503 → 끝나면 200. @SpringBootTest는 리스너가 끝나야 테스트가 시작돼 "도는 동안"을 못 보므로 SpringApplicationBuilder로 별도 스레드 기동 + JDK HttpServer 스텁이 응답을 래치로 붙잡는 방식) + 프로퍼티 테스트 2건.
  4. chore dev 회차 스크립트(scripts/msg608-dev-round.sh — env 블록 교체 + --force-recreate), JIT 로그 분석기(scripts/analyze-jit-log.py), Grafana 워밍업 대시보드 URI 수정, 증거 폴더(k6 요약·actuator 샘플·JIT 분석·캡처).
  5. docs 스펙 docs/spec/MSG-608.md(D1~D9, 작업 로그에 실측표), deploy.md env 3행 + healthcheck 절, status.md, rtm.

dev 실측(t3.small, 이 브랜치 이미지, 회차마다 컨테이너 재생성, healthy 직후 k6 10배 60초 + 카나리3 5/s)

회차 워밍업 핫구역 p95 미션 p95 카나리 배율 버림 벌점 지속
전 ×2 끔 3,390 / 3,346ms 1,398 / 1,373 24배 1,103 / 1,086 약 35초
후 ×3 N=1,000 540 / 529 / 309 226 / 203 / 152 2.7~3.3배 76 / 44 / 0 2~3초
후 N=2,000 (채택) 263 153 2.0배 39 2초 (p95 0.38초)
후 N=200 2,625 1,053 17배 547 약 25초 (효과 없음)

워밍업 소요는 N=2,000에서 32.9초(상한 45초). 테스트: warmup 패키지 14건, 회귀 mission·hotzone·grid·region 전부 green. 전체 스위트 3,210건 중 event 패키지 2건(EventNotificationSchedulerTest, EventSubmissionReviewSchemaTest)이 로컬에서 실패하는데 공유 로컬 DB에 남은 회차·위치 데이터(회차 458건) 때문이라 이 변경과 무관합니다. 전체 스위트는 CI에 맡깁니다.

🤔 고민한 내용

진짜 원인을 로그로 굳혔습니다. 워밍업을 짜기 전에 dev에 -Xlog:jit+compilation=debug와 -Xlog:class+load를 켜고 같은 부하를 걸었습니다. 부하 창 40초에 컴파일 6,300건이 나왔는데 우리 코드의 C2 승격은 0건이고 Spring MVC·Security 953건, Hibernate 906, Tomcat 462, Netty·Lettuce(Redis) 417, Jackson 106건이 처음 컴파일됐습니다. 클래스 로딩은 327개뿐이었습니다. 요청 경로가 인터프리터 상태였던 것이라, 서비스 빈을 직접 부르는 워밍업(카카오페이 글 방식)이 아니라 HTTP로 필터·컨트롤러·직렬화까지 밟는 워밍업을 골랐고, 클래스 로딩만 줄이는 AppCDS4는 쓰지 않았습니다.

실패한 가설이 둘 있었습니다. (1) MSG-576에서 콜드 원인을 미션 스냅숏 재계산 락 대기로 보고 후보 코드까지 짰지만, dev에서는 첫 초 max 251ms로 관측되지 않았습니다. 스냅숏은 warmUpSnapshot()이 기동 때 이미 채우고 있었습니다. 데이터 캐시 워밍업은 있었고 코드 경로 워밍업이 없었던 것입니다. (2) dev의 SQL DEBUG 로깅(분당 25만 줄)이 CPU를 먹어 웜 상태 10배도 실패하던 것을 먼저 걷어냈습니다. 그래도 재기동 직후 35초는 그대로여서 로깅이 원인이 아님을 분리했습니다.

반복 횟수는 곡선으로 골랐습니다. 손으로 240회쯤 불러 봤을 때 효과가 없었던 관측이 있어 200/1,000/2,000을 쟀습니다. 200은 효과가 없고(C1 문턱5 근처까지만), 1,000부터 벌점이 35초에서 2초로 줄고, 2,000이 p95 300ms 안에 들어 채택했습니다. 순차 1스레드로는 1,000회부터 상한 45초를 넘어 4스레드 병렬로 바꿨습니다(2 vCPU에서 컴파일 스레드와 경합하므로 8 이상은 늘리지 않음).

"트래픽 차단"은 솔직히 안 됩니다. readiness 503은 docker healthcheck와 CD --wait만 봅니다. nginx는 항상 8080으로 넘기고 인스턴스가 한 대라, 워밍업 중 들어온 사용자 요청은 워밍업과 경합할 뿐 막히지 않습니다. 그래도 벌점을 배포 시점(사용자가 없는 시각)으로 옮기는 효과는 그대로이고, 진짜 차단은 인스턴스 2대와 로드밸런서에서만 됩니다. deploy.md에 그렇게 적었습니다.

리뷰 2라운드에서 고친 것. 토큰 발급이 try 밖이라 예외가 기동을 죽일 수 있던 경로를 격자만 건너뛰게 감쌌고, 통합 테스트가 실패 경로에서 직접 기동한 앱을 안 닫아 공유 DB 커넥션을 물고 남을 수 있던 구멍을 startup.whenComplete로 막았습니다. Codex 교차 리뷰는 없습니다(결제 중단).

👀 리뷰 포인트

  • 카나리 배율 1.5배 기준(스펙 AC-07·08)은 못 닫았습니다. 2.03.3배인데, 원인은 워밍업 잔여가 아니라 dev에서 **매분 :02:03초에 세 엔드포인트가 함께 1~2초 멈추는 현상**이 60초 창 안에 걸려서입니다(containerd 43% 스파이크 1회 관측, healthcheck 10초 주기와 무관). 별도 조사 티켓으로 뺐습니다. 이 PR 범위에서 더 할 것이 있는지 의견 부탁드립니다.
  • fillmap.warmup.grid-user-id는 점령 격자가 있는 실존 사용자여야 합니다. dev는 586, prod는 배포 때 env로 넣어야 합니다(없으면 격자 조회만 건너뛰고 집계 2종은 데워집니다).
  • management.endpoint.health.probes.enabled=true로 /actuator/health/readiness·liveness 경로가 생깁니다. 노출 범위(exposure.include)는 그대로입니다.
  • 미션 집계가 스냅숏 위 메모리 산술인데 클래스 레벨 @Transactional(readOnly = true) 때문에 요청마다 Hikari 커넥션을 먼저 잡습니다(HibernateJpaDialect가 readOnly면 begin 시점에 커넥션 획득). 이 PR 범위 밖이라 후속 티켓 후보로만 적었습니다.

Footnotes

  1. JIT(Just-In-Time) 컴파일: JVM은 자바 코드를 처음엔 한 줄씩 해석해 실행하다가 자주 불리는 메서드를 골라 기계어로 컴파일해 바꿔 끼운다. 컴파일 전 경로는 수십 배 느리고 컴파일 자체도 CPU를 쓴다. 프로세스가 죽으면 결과도 사라져 배포마다 반복된다. ↩

  2. readiness 프로브: 앱이 "요청을 받을 준비가 됐는가"를 스스로 알리는 신호. Spring Boot는 ApplicationReadyEvent 리스너가 전부 돌아온 뒤에야 ACCEPTING_TRAFFIC으로 바꾸고, 그 전에는 /actuator/health가 503을 준다. 원래 쿠버네티스용이지만 여기서는 docker healthcheck와 CD --wait가 같은 신호를 본다. ↩

  3. 카나리 요청: 측정 대상이 아닌 다른 API(격자 뷰포트 조회)를 초당 5건 같이 보내, 대상이 느려질 때 서버 전체가 같이 느려지는지 보는 요청. 배율 = 부하 중 p95 ÷ 평소 p95. ↩

  4. AppCDS: 앱이 쓰는 클래스를 미리 파싱·검증해 파일로 저장해 두고 기동 때 그대로 매핑하는 JDK 기능. 줄이는 건 클래스 로딩이지 JIT 컴파일이 아니다. ↩

  5. 컴파일 문턱: HotSpot은 메서드 호출 횟수를 세서 수백 회에서 C1(빠르지만 덜 최적화), 수천~1만 회 이상에서 C2(충분히 최적화)로 단계별 컴파일한다. 워밍업 N건은 C1까지를 배포 시점으로 옮기고 C2는 배포 뒤 실제 요청이 마저 채운다. ↩

@claude

claude Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Claude finished @s13121312's task in 5m 55s —— View job


리뷰 완료

  • 컨텍스트 파악 (PR 본문·diff·스펙 문서)
  • 병렬 브랜치 경합 확인 (마이그레이션 번호, developCode 대역, Owner A/B 계약)
  • 스펙(docs/spec/MSG-608.md) 대비 구현 검토
  • 실행 시 드러나는 결함 검토 (트랜잭션·동시성·시간대·실패 경로·인가)
  • PR 본문 주장 검증
  • 리뷰 결과 정리 및 게시

확인한 범위: origin/develop 대조(마이그레이션·developCode 대역·계약 인터페이스), WarmupRunner/WarmupConfig/WarmupProperties 구현, 단위·통합·프로퍼티 테스트 3종, docs/spec/MSG-608.md 전체(D1D9, AC-0109, 작업 로그), application.yml/SecurityConfig/docker-compose.app.yml healthcheck 정합성, deploy.md/status.md/rtm.md 문서 갱신.

발견

  • P3 · src/main/resources/application.yml:40 — probes.enabled=true로 새로 열리는 /actuator/health/readiness·/actuator/health/liveness가 SecurityConfig의 permitAll 정확 일치 대상(/actuator/health exact)에는 안 들어가 인증 없이는 401/403이 됩니다. 이 PR의 healthcheck·CD --wait는 정확히 /actuator/health만 보므로 실제 동작에는 영향 없는 참고 사항입니다(인라인 코멘트).

나머지 확인 결과 — 문제 없음

  • 병렬 경합: 마이그레이션 파일 변경 없음(develop 최신 V56 그대로), developCode 대역 변경 없음, GridQueryService·HotZoneService·UserGridQueryService·ZoneNameQueryService 시그니처 불변 — 스펙 "계약 변경" 절과 일치.
  • 스펙 대비: 워밍업 대상 URI·파라미터가 실제 컨트롤러(GridController, HotZoneController, MissionController)와 일치. iterations 기본값 2,000, WarmupPropertiesTest 기대값 일치. AC-0608 미달성(카나리 배율 2.03.3배, 목표 1.5배)은 스펙·작업 로그·PR 본문 모두에서 원인(매분 :02초 dev 인프라 스파이크)과 후속 티켓 분리를 동일하게 명시 — 은폐 없음.
  • 실행 결함: 토큰 발급 실패가 격자 대상만 건너뛰도록 격리됨, 45초 상한·스레드풀 종료·타임아웃 로직이 테스트로 커버됨, Clock 주입으로 시간대 규칙 준수, 트랜잭션 없음(HTTP 호출이라 해당 없음), docker-compose healthcheck의 start_period 30s + retries 6×10s 산술이 WarmupRunner.TIME_LIMIT=45s와 실제로 일치.
  • PR 주장 검증: "계약 인터페이스 시그니처 변경 없음", "마이그레이션 없음", "테스트 컨텍스트에서 워밍업 안 돎"(application-local.yml/application-local.yml.example 모두 enabled: false 명시) 전부 diff와 일치.

Comment thread src/main/resources/application.yml
@github-actions

Copy link
Copy Markdown

테스트 커버리지

Overall Project 95.9% -0.03% 🍏
Files changed 97.62% 🍏

File Coverage
WarmupProperties.java 100% 🍏
WarmupConfig.java 100% 🍏
WarmupRunner.java 97.44% -2.56% 🍏

@github-actions

Copy link
Copy Markdown

테스트 커버리지

Overall Project 95.9% -0.03% 🍏
Files changed 97.62% 🍏

File Coverage
WarmupProperties.java 100% 🍏
WarmupConfig.java 100% 🍏
WarmupRunner.java 97.44% -2.56% 🍏

@s13121312
s13121312 merged commit 5f73e62 into develop Sep 28, 2026
1 check passed
@s13121312
s13121312 deleted the feature/MSG-608-jvm-warmup branch September 28, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant