Skip to content

fix(ops): 배포할 때마다 헬스 알림이 헛울리던 문제 - #277

Merged
i3months merged 1 commit into
devfrom
fix/health-alert-deploy-false-positive
Sep 29, 2026
Merged

i3months merged 1 commit into
devfrom
fix/health-alert-deploy-false-positive

Conversation

@i3months

Copy link
Copy Markdown
Member

무슨 일이 있었나

오늘 21:08 에 PR #275·#276 을 병합해 배포가 돌았고, 21:10 폴링이 팀 채널에 이렇게 보냈습니다.

[실패] 헬스 이상: DEGRADED — 전체=UP / 비정상: scheduler=DOWN

21:15 에 복구 알림. 장애가 아니라 배포였습니다.

확인한 사실:

백엔드 컨테이너 RestartCount=0 · 12:09:40 UTC 재생성(크래시 아님, 배포)
Tomcat 준비 12:09:53.190
하트비트 첫 박동 initialDelay 10초 뒤 ≈ 12:10:03
크론 */5 * * * * → 12:10:00

10초짜리 창의 7초 지점에 폴링이 꽂혔습니다. 우연이 아니라 배포가 정각 직전에 끝나면 늘 울립니다.

원인 두 가지 — 각각 맞는 층에서 고칩니다

1. SchedulerHeartbeat 가 부팅 직후를 "스케줄러 죽음"으로 보고했다 (backend)

lastBeat 이 null 로 시작하고 첫 박동은 10초 뒤라, 그동안 엔드포인트는 정상 응답하면서 scheduler=DOWN 을 돌려줍니다. 기존 코드의 주석도 이 상황을 알고 있었지만 aggregate 에서 빼는 것으로만 완화했고, 알림 스크립트는 개별 컴포넌트까지 보기 때문에 그대로 울렸습니다.

→ 기동 시각으로 시드합니다. 스케줄러가 영영 안 뛰면 그 값이 늙어 maxAge(5분)를 넘겨 여전히 DOWN 으로 잡힙니다 — 감지가 최대 5분 늦어질 뿐이고, 그게 "안 뛴다"의 정의입니다.

기존 테스트 두 개가 정반대를 고정하고 있었습니다(한_번도_안_뛰었으면_DOWN, lastBeat() 초기 null). 그게 곧 버그라 의도에 맞게 다시 썼습니다.

2. 감시가 비정상을 1회만 보고 알렸다 (infra/ops)

스케줄러만의 문제가 아닙니다. AiServerHealthIndicator 는 큐 컨슈머가 0이면 DOWN 인데, 배포 중 AI 컨테이너가 다시 붙는 동안 정상적으로 0이 됩니다. 오늘 점검 중 실제로 ai.* 큐 9개가 재시작 창에서 컨슈머 0인 것을 관측했습니다(13초 뒤 전부 1로 복귀).

지표만 보고는 "배포 중"과 "죽었다"를 구분할 수 없습니다. 구분되는 건 지속 시간뿐입니다.

→ 같은 비정상을 연속 2회(≈5분 간격) 봐야 알립니다. 상태 파일을 두 줄(마지막으로 알린 상태 / 확정 대기 후보)로 바꿨고, 운영에 있는 기존 한 줄 파일에서도 그대로 이어집니다(실제로 돌려 확인).

복구는 지연 없이 알립니다 — 늦추면 "아직 장애중"으로 오해합니다. 알린 적 없는 비정상의 '복구'는 보내지 않습니다.

트레이드오프

진짜 장애 감지가 최대 한 주기(≈5분) 늦습니다. 스위퍼 주기가 2~5분인 서비스라 감당할 수 있고, 배포마다 헛울려 아무도 안 읽는 알림보다 낫습니다. 한 번 걸러 한 번씩 나빠지는 플래핑은 이 방식으로 잡히지 않으며 README 에 적었습니다.

테스트

selftest.sh 22건 통과. 핵심은 배포 블립 회귀 케이스입니다.

ok   이상 1회차: 보류(무발송)
ok   이상 1회차: 알린 상태는 그대로 UP
ok   이상 2회차: 1건 발송
ok   복구: 2건
ok   배포 블립(이상 1회 후 복귀): 발송 늘지 않음
ok   도달 불가 2회차: 확정 기록

./gradlew test 전체 통과 · bash -n · shellcheck 통과.

🤖 Generated with Claude Code

오늘 21:08 에 PR 두 건을 병합해 배포가 돌았고, 21:10 폴링이
"[실패] 헬스 이상: DEGRADED / scheduler=DOWN" 을 팀 채널에 보냈다.
21:15 에 복구 알림. 장애가 아니라 배포였다.

원인 두 가지를 각각 맞는 층에서 고친다.

1) SchedulerHeartbeat 가 부팅 직후를 "스케줄러 죽음"으로 보고했다.
   lastBeat 가 null 로 시작하고 첫 박동은 initialDelay 10초 뒤라, Tomcat 이
   뜬 뒤 10초 동안 엔드포인트는 정상 응답하면서 scheduler=DOWN 을 돌려준다.
   오늘 기동이 12:09:53, 크론이 */5 정각이라 12:10:00 폴링이 그 창의 7초
   지점에 꽂혔다. 우연이 아니라 배포가 정각 직전에 끝나면 늘 울린다.

   기동 시각으로 시드한다. 스케줄러가 영영 안 뛰는 경우는 그 값이 늙어
   maxAge(5분)를 넘겨 여전히 DOWN 으로 잡힌다 — 감지가 최대 5분 늦어질
   뿐이고 그게 "안 뛴다"의 정의다.

   기존 테스트 두 개가 정반대(한 번도 안 뛰었으면 DOWN, lastBeat 초기 null)를
   고정하고 있었다. 그게 곧 버그라 의도에 맞게 다시 썼다.

2) 감시가 비정상을 1회만 보고 알렸다. 배포는 컨테이너를 재생성하므로
   AiServerHealthIndicator 도 같은 성격으로 DOWN 이 된다(큐 컨슈머 0 —
   AI 컨테이너가 다시 붙는 중). 오늘 실제로 ai.* 큐 9개가 재시작 창에서
   컨슈머 0 이었다. 지표만으로는 "배포 중"과 "죽었다"를 구분할 수 없고
   구분되는 건 지속 시간뿐이다.

   같은 비정상을 연속 2회(≈5분 간격) 봐야 알린다. 상태 파일을 두 줄
   (마지막으로 알린 상태 / 확정 대기 후보)로 바꿨고, 운영에 있는 기존
   한 줄 파일에서도 그대로 이어진다(확인함). 복구는 지연 없이 알린다 —
   늦추면 아직 장애중으로 오해한다.

   대가는 진짜 장애 감지가 최대 한 주기 늦는 것이고, 스위퍼 주기가 2~5분인
   서비스라 감당할 수 있다. 배포마다 헛울려 아무도 안 읽는 알림보다 낫다.
   한 번 걸러 한 번씩 나빠지는 플래핑은 잡지 못한다 — README 에 적었다.

selftest 에 배포 블립 케이스(이상 1회 후 복귀 → 무발송)를 넣었다. 22건 통과.
@i3months
i3months merged commit 2dddfd5 into dev Sep 29, 2026
6 checks passed
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