sy/dev
Guide
15 min read

백엔드 장애를 읽는 법 — 로그만 보지 말고 신호를 연결하자

백엔드 지식 지도 마지막 편. 네트워크, HTTP, OS, DB, 큐, 분산 시스템의 신호를 연결해 장애를 진단하는 실전 순서를 정리한다.

백엔드 장애를 처음 만나면 보통 로그부터 본다.

틀린 습관은 아니다. 로그는 중요하다. 하지만 로그만 보면 자주 막힌다. 앱 로그에는 네트워크 DROP이 안 남고, OOM killer는 애플리케이션 예외로 기록되지 않으며, DB 잠금 대기는 HTTP 504로만 보일 수 있다.

장애를 잘 읽는다는 것은 하나의 신호를 깊게 보는 일이 아니다.

여러 층의 신호를 연결하는 일이다.

네트워크: errno, DNS, TCP, TLS
HTTP: 상태코드, timeout, method
OS: process exit, fd, memory, cgroup
DB: lock, transaction, query plan, pool
Queue: depth, wait time, retry, DLQ
Distributed systems: retry, budget, SLO, tail latency

이 글은 시리즈의 마지막 편이다. 앞에서 본 여섯 영역을 장애 진단 순서로 묶어 보자.

1단계: 사용자가 겪는 증상을 먼저 고정한다

가장 먼저 할 일은 원인을 상상하는 것이 아니다. 증상을 고정하는 것이다.

  • 느린가?
  • 실패하는가?
  • 결과가 틀린가?
  • 일부 사용자만 그런가?
  • 특정 기능만 그런가?
  • 언제부터 그런가?

원인 기반으로 시작하면 쉽게 빗나간다. “DB 문제 같다”, “네트워크 문제 같다”, “배포 때문 같다”는 말은 아직 가설이다.

사용자 경험에 가까운 SLI를 먼저 본다.

성공률
지연 시간 p95/p99
정상 응답 비율
큐 대기 시간
주요 비즈니스 이벤트 성공률

폴백이 있는 시스템이라면 HTTP 200 비율만 보면 안 된다. 폴백 응답은 성공처럼 보이는 실패일 수 있다.

2단계: 시간축을 맞춘다

장애는 시간축 위에서 봐야 한다.

  • 배포 시각
  • 트래픽 변화
  • 오류율 상승 시각
  • latency 상승 시각
  • DB lock 증가 시각
  • 큐 depth 증가 시각
  • 외부 API 오류 시각

이 순서가 중요하다.

예를 들어 HTTP 504가 늘어난 뒤 큐 재시도가 늘었는지, 큐 재시도가 먼저 늘고 그 결과 HTTP가 느려졌는지에 따라 원인이 다르다.

시간축을 맞추려면 모든 로그와 메트릭에 같은 기준의 timestamp와 request ID, job ID, trace ID가 있어야 한다. 없으면 장애 때마다 눈대중으로 맞춰야 한다.

3단계: HTTP 상태코드의 화자를 확인한다

HTTP 오류를 볼 때는 상태코드만 보지 말고 누가 말했는지 봐야 한다.

500은 대체로 앱이 말한다. 트레이스백이나 예외 로그가 있을 가능성이 높다.

502와 504는 프록시가 말하는 경우가 많다. upstream에 못 붙었거나, upstream 응답을 시간 안에 못 받았다는 뜻이다.

503은 앱이 overload를 선언했을 수도 있고, 프록시나 로드밸런서가 가용 upstream 없음으로 말했을 수도 있다.

429는 rate limit이다. Retry-After가 있으면 지켜야 한다.

특히 504는 조심해야 한다. 서버가 처리하지 않았다는 뜻이 아니다. 서버는 여전히 처리 중일 수 있다. POST라면 자동 재시도가 중복 처리를 만들 수 있다.

상태코드는 원인이 아니라 관측된 계약 위반이다. 그 계약을 누가 위반했다고 보고했는지부터 확인해야 한다.

4단계: 네트워크는 DNS, TCP, TLS, HTTP로 쪼갠다

“연결 실패”는 너무 큰 말이다.

다음 네 단계를 나눠야 한다.

DNS → TCP → TLS → HTTP

DNS가 실패하면 이름 해석 문제다. TCP 연결이 ECONNREFUSED면 목적지 포트에 아무도 없을 가능성이 크다. timeout이면 방화벽 DROP이나 라우팅 블랙홀을 의심한다. TLS 실패면 인증서 이름, 체인, 만료, SNI를 본다.

그리고 점검 위치가 중요하다.

내 노트북에서 되는 것은 배포 서버에서 된다는 뜻이 아니다. 호스트에서 되는 것은 컨테이너에서 된다는 뜻이 아니다. source IP allowlist가 있으면 실제 배포 호스트에서 점검해야 한다.

네트워크 장애는 앱 로그에 안 남는 경우가 많다. 그래서 소켓을 실제 위치에서 열어 보는 대조군이 필요하다.

5단계: 프로세스가 죽었으면 앱 로그 바깥을 본다

앱 로그에 아무것도 없는데 프로세스가 사라졌다면, 앱 안에서 난 예외가 아닐 수 있다.

먼저 OOM killer를 본다. 컨테이너 메모리 한도를 넘으면 커널이 프로세스를 죽이고, 애플리케이션은 로그를 남길 기회를 얻지 못한다.

다음으로 종료 신호와 exit code를 본다. SIGTERM을 받았는지, grace period 안에 종료했는지, 결국 SIGKILL을 받았는지 확인한다.

또 fd 고갈도 봐야 한다. Too many open files는 네트워크, DB, 파일, 로그 시스템 어디에서나 이상한 증상으로 나타난다.

CPU가 남는데 느리다면 cgroup throttling도 확인한다. 호스트 CPU 사용률이 낮아도 컨테이너는 자기 quota를 다 써서 대기할 수 있다.

6단계: DB는 lock, pool, transaction부터 본다

DB 장애를 볼 때 쿼리 하나의 느림만 보지 말고 세 가지를 먼저 본다.

첫째, 잠금 대기다. 어떤 트랜잭션이 어떤 row나 table lock을 잡고 있고, 누가 기다리는지 본다.

둘째, 커넥션 풀이다. DB 자체가 느린 것이 아니라 앱의 풀을 다 써서 새 요청이 기다리는 것일 수 있다.

셋째, 오래 열린 트랜잭션이다. idle in transaction은 잠금을 쥐고, VACUUM을 막고, 커넥션을 차지한다.

외부 API 호출이 트랜잭션 안에 있으면 특히 위험하다. 외부 지연이 DB 잠금과 커넥션 고갈로 번진다.

DDL도 확인해야 한다. 짧은 ALTER TABLE 하나가 잠금을 기다리는 동안, 뒤에 줄 선 일반 쿼리까지 막을 수 있다.

7단계: 큐는 처리 시간이 아니라 대기 시간을 본다

큐가 있는 시스템에서는 처리 시간만 보면 안 된다.

사용자에게 중요한 것은 enqueue에서 완료까지의 전체 시간이다. 워커가 잡을 집은 뒤 처리 시간은 짧아도, 큐에서 30분 기다렸다면 서비스는 느린 것이다.

봐야 할 지표는 이렇다.

  • queue depth
  • enqueue to start latency
  • processing time
  • failure rate
  • retry rate
  • DLQ count

긴 잡과 짧은 잡이 같은 큐에 섞이면 head-of-line blocking이 생긴다. 긴 잡이 워커 슬롯을 차지하고, 짧은 잡이 뒤에서 기다린다.

재시도도 봐야 한다. 영구 실패가 계속 재시도되면 큐를 오염시킨다. 일시적 실패는 백오프와 지터가 있어야 하고, 멱등 소비자 없이 재시도하면 중복 처리가 생긴다.

8단계: 재시도 증폭을 의심한다

장애가 커지는 속도가 이상하게 빠르면 재시도를 의심해야 한다.

클라이언트, 프록시, 앱 내부 호출이 각각 재시도하면 한 요청이 여러 배로 증폭된다. 이미 느린 시스템에 추가 부하가 들어가고, 더 느려져서 더 많은 재시도를 만든다.

이때 필요한 것은 더 많은 재시도가 아니라 제한이다.

  • 재시도 계층을 하나로 줄인다.
  • retry budget을 둔다.
  • exponential backoff와 full jitter를 적용한다.
  • 멱등하지 않은 요청은 자동 재시도하지 않는다.
  • 필요하면 circuit breaker로 빠르게 실패시킨다.
  • 이미 만료된 큐 작업은 시작 전에 버린다.

장애 중에는 재시도를 줄이는 것이 회복을 앞당길 수 있다.

9단계: 평균을 믿지 말고 꼬리를 본다

평균 latency가 괜찮아도 사용자는 느릴 수 있다.

요청이 여러 하위 호출로 갈라지면 하나만 느려도 전체가 느려진다. 그래서 p95, p99 같은 꼬리 지연을 봐야 한다.

여러 인스턴스의 p99를 평균 내면 안 된다. 올바른 방법은 histogram bucket을 합산하고 다시 백분위를 계산하는 것이다.

대시보드를 만들 때는 서비스는 RED, 자원은 USE를 기본으로 둔다.

RED: Rate, Errors, Duration
USE: Utilization, Saturation, Errors

원인 대시보드보다 증상 대시보드가 먼저다. CPU, memory, DB, network는 원인 후보이고, 사용자가 겪는 성공률과 지연은 증상이다.

10단계: 질문 목록으로 진단한다

장애 때는 머릿속에서만 생각하지 말고 질문을 고정해 두는 편이 좋다.

사용자는 무엇을 겪는가?
언제부터 시작됐는가?
최근 배포나 설정 변경이 있었는가?
오류는 일부 endpoint인가 전체인가?
상태코드는 누가 말했는가?
DNS, TCP, TLS, HTTP 중 어디까지 성공하는가?
프로세스가 죽었는가, 살아 있는데 느린가?
DB lock, pool, idle transaction은 정상인가?
큐 depth와 대기 시간은 늘었는가?
재시도가 부하를 증폭하고 있지는 않은가?
평균이 아니라 p95/p99는 어떤가?

이 질문들은 정답을 주지 않는다. 대신 오진을 줄인다.

장애 대응에서 가장 비싼 것은 틀린 확신이다. “DB 문제 같아요”라고 말하고 30분 동안 인덱스만 보다가, 실제 원인이 source IP allowlist였다는 식의 일이 생긴다.

신호를 층별로 나누면 확신보다 증거가 앞선다.

마무리

백엔드 장애는 한 층에서만 발생하지 않는다.

네트워크 timeout이 HTTP 504로 보이고, 외부 API 지연이 DB 커넥션 고갈로 번지고, 큐 재시도가 전체 부하를 증폭하고, OOM은 앱 로그 없이 프로세스를 죽인다.

그래서 백엔드 지식은 따로 외우는 목록이 아니라 연결해서 쓰는 지도여야 한다.

이번 시리즈의 여섯 영역도 결국 이 목적을 가진다.

네트워크: 패킷이 어디서 막혔는가
HTTP와 API: 요청을 다시 보내도 되는가
OS와 프로세스: 코드가 어떤 자원 위에서 죽거나 느려지는가
데이터베이스: 동시에 상태를 만질 때 어떤 순서가 되는가
워커와 큐: 요청 밖의 실패와 중복을 어떻게 다루는가
분산 시스템: 부분 실패가 전체 장애로 번지지 않게 하는가

로그만 보지 말자.

상태코드, errno, 프로세스 종료 사유, DB lock, 큐 대기 시간, p99 latency를 함께 보자. 그 신호들이 같은 이야기를 하기 시작할 때 장애의 모양이 보인다.

Comments