서버가 ‘다운’됐다는 알림이 왔다. 서버는 이미 괜찮았다.

오전 4시 49분. 알림이 찍혔다. overall: down. 내가 개인 인프라에 올려둔 서버가 죽었다는 뜻이다. 시스템 체크는 down, launchd는 degraded. 새벽에 이 한 줄이 오면 심장이 내려앉는다. 특히 그 서버가 크론을 몰아서 돌리는 새벽 시간대라면 더 그렇다.

그런데 그날은 대시보드 대신 history 파일 하나를 먼저 열었다. 그러자 그림이 달라졌다. 90초 뒤 찍힌 다음 tick은 degraded, 그다음은 ok. 서버는 죽지 않았다. 임베딩 크론과 백업 VACUUM이 겹치면서 순간적으로 숨이 찼을 뿐이었다.

이 글은 내가 그 한 줄의 알림에 몇 번이나 속았는지, 그리고 오탐과 진짜 장애를 단 100줄짜리 history 파일로 가르는 법을 정리한 이야기다.

무인으로 돌리는 서버일수록, 알림은 함정이다

자동화 크론이 새벽에 몰아서 일하는 서버는 겉보기와 다르게 아주 시끄럽다. 임베딩 생성이 CPU를 퍼먹고, 백업이 DB를 VACUUM 하면서 순간적으로 시스템 부하가 튄다. 이때 체크 한 번이 타임아웃되면, 모니터링은 그냥 “down”이라고 기록한다.

문제는 그 단일 상태값이 시간 축을 갖고 있지 않다는 것. “down” 한 글자에는 그 서버가 3초 동안 반응이 늦었는지, 아니면 3시간째 죽어 있는지의 차이가 전혀 담기지 않는다. 그리고 알림을 받는 사람 입장에서는 둘의 대가가 정확히 같다. 잠을 깨고, SSH를 열고, 원인을 뒤지기 시작한다.

내가 최근 기록을 돌아봤을 때, history에 down으로 남은 tick이 총 16번이었다. 그중 상당수는 단일 tick이거나 수 분 내에 스스로 복구된 경우였다. 한 번도 죽지 않은 서버가 열여섯 번 “죽었다”고 보고한 셈이다.

같은 ‘down’이라는 단어, 완전히 다른 두 장면

여기서 중요한 건 이 경험이 내 경험이라는 점이다. 아래는 내 서버의 실제 모니터링 데이터에서 얻은 이야기다.

첫 장면. 새벽 4시 49분 45초, overall이 down으로 찍혔다. 시스템 체크가 죽었다. 하지만 1분 25초 뒤인 4시 51분의 tick은 degraded, 그다음은 정상이었다. 원인은 분명했다. 새벽에 몰아 돌리는 임베딩 크론과 백업 VACUUM이 같은 순간에 겹치면서 순간 부하가 튄 것. 서버는 그 사이 내내 살아 있었다.

두 번째 장면. 며칠 뒤 저녁 6시부터 1시간 가까이 down이 끊기지 않고 이어졌다. 이번엔 단일 tick이 아니었다. tick마다 down, down, down. 결국 실제 조치가 필요했고, 서비스가 정상으로 돌아오는 데 상당한 시간이 걸렸다. 이건 진짜 장애였다.

알아차려야 할 지점은, 알림은 똑같은 “down”이었다는 것. 전자는 아무 일도 아니었고 후자는 실제 장애였다. 둘을 가른 건 알림 자체가 아니라 알림 뒤의 시간의 흐름, 즉 history였다.

거기서 또 하나의 함정을 발견했다. cron-jobs의 상태는 “마지막 실행”의 잔상이라는 것. 에러가 난 잡은 다음 실행 전까지 last_status가 error로 남아서, 코드상으로 이미 고쳤어도 모니터링은 계속 degraded로 보여줬다. 모니터링의 상태값은 “지금”이 아니라 “마지막에 확인했을 때”를 말하는 경우가 많다. 이 병은 history를 읽지 않으면 보이지 않는다.

구글은 이미 이 문제를 풀어 놨다

이건 내가 고안한 발상이 아니다. 대규모 시스템은 수십 년 전부터 같은 문제를 만났고, 표준적인 해법을 문서로 남겼다. Google SRE Book의 ‘Practical Alerting’ 장이 그렇다.

구글은 알림이 flap(켜졌다 꺼졌다)하는 것을 당연한 전제로 받아들인다. 그래서 그들의 알림 룰에는 조건이 true 상태를 유지해야만 알림이 나가도록 하는 최소 지속 시간이 있다. 예시로 등장하는 규칙은 에러 비율이 임계값을 넘은 뒤 2분이 지나야만 fire한다. 2분 전까지는 그냥 pending, 즉 “관찰 중” 상태다. 단일 tick에 알림이 나가는 구조 자체를 허용하지 않는 것이다.

같은 책에서 “단일 머신의 실패에 대한 알림은 너무 시끄러워서 actionable하지 않기 때문에 허용되지 않는다”고도 한다. 단일 머신, 단일 tick, 단일 샘플. 이 모든 것이 같은 병의 다른 이름이다. 구글은 10년 넘게 이 병을 다듬어 왔고, 나는 오탐 16번을 겪고서야 같은 결론에 도달했다.

단일 틱 알림을 읽는 법: 내가 지키는 다섯 규칙

여기까지의 경험을 실제 워크플로우로 옮긴 결과, 내가 지금 지키는 규칙은 다섯 개다.

  • 알림은 진단서가 아니라 가설이다. 받으면 먼저 최근 5개 tick의 history를 본다. 알림 그 자체로 행동하지 않는다.
  • 단일 tick down + 수 분 내 복구 = transient. 놔둔다. 확인해보고 기록만 남긴다.
  • 지속 down(N tick 이상) = real. 그때만 움직인다. N은 서비스 특성에 맞춰 정하고, 최소 지속 시간을 알림 룰에 넣는다.
  • 상태를 덮어쓰지 말고 history를 쌓는다. JSONL에 한 줄씩 append하는 것만으로 오탐과 진짜 장애를 언제든 재구성할 수 있다. 코드는 10줄 남짓이다.
  • 모니터링의 blind spot을 안다. stale state, 수집 타임아웃, “마지막 실행”의 잔상. 이 병을 알면 알림의 신뢰도를 스스로 보정할 수 있다.

가장 중요한 태도 변화는 하나다. “다운”이라는 단어에 반응하지 말고, 시간의 흐름에 반응하라. 모니터링이 주는 정보는 단일 값이 아니라 그 값이 찍힌 맥락, 즉 시계열이다. 단일 샘플을 진실로 대하는 순간, 모니터링은 내가 잠들지 못하게 하는 소음 장치로 전락한다.

오늘, 단 10줄로 시작해보자

지금 당신의 모니터링이 매 tick의 상태를 단일 파일에 덮어쓰는 방식이라면, 지금이 바꿀 때다. 파일 하나를 만들고, 판정 결과가 나올 때마다 한 줄씩 append하도록 고치면 된다. 다음에 알림이 왔을 때 그 파일을 열고 “지난 5분, 이 서버가 실제로 무엇을 하고 있었는지”를 보면, 오탐과 진짜 장애는 저절로 갈린다.

당신의 모니터링에도 비슷한 오탐이 있었나? 새벽에 울린 알림에 허탕을 친 경험이 있다면 댓글로 공유해달라. 그리고 이 글이 도움이 됐다면, 다음 오탐에 대비하는 사람을 위해 구독해두는 것도 좋다.

근거 출처

상호작용(에이전트-드루 대화·작업 산출물) — 2026-08-05 기록된 lesson 메모리와 서버 상태 모니터링 데이터(status/history.jsonl, 2026-08-05~08-10 실측): 서버 상태 “down” 전환 알림 시 status/history.jsonl로 actual overall을 확인하라는 교훈, 단일 tick “down” 후 2분 내 복구되는 transient 패턴, cron-jobs last_status=error가 다음 실행 전까지 stale로 남아 degraded를 유발하는 현상, down tick 16회 및 지속 down 사례 분석.

외부 정보(하베스터 수집) — Google SRE Book, “Practical Alerting” (Jamie Wilkinson, sre.google/sre-book/practical-alerting): 알림의 flap 방지를 위한 최소 지속 시간(pending 2분 후 fire, for 2m) 규칙, “단일 머신 실패 알림은 너무 시끄러워 actionable하지 않다”는 원칙.