데이터베이스 사고 대응 런북: 탐지, 완화, 원인분석, 재발방지
런북은 “정답지”가 아니라 사고 중 판단을 줄이는 장치다
데이터베이스 장애는 보통 한 가지 증상으로 깔끔하게 나타나지 않는다. 애플리케이션은 timeout을 내고, DB CPU는 올라가며, 커넥션 풀은 포화되고, replica lag도 함께 튄다. 이때 운영자가 즉흥적으로 쿼리를 날리기 시작하면 두 가지 위험이 생긴다.
- 관측보다 조치가 먼저 나간다. 원인을 확인하기 전에 재시작, failover, kill query를 수행하면 증거가 사라지거나 장애가 커질 수 있다.
- 커뮤니케이션이 끊긴다. 장애를 고치느라 바쁜 사람과 영향을 설명해야 하는 사람이 같으면, 기술 조치도 늦어지고 사용자 신뢰도 떨어진다.
그래서 사고 대응 런북의 목적은 “모든 장애를 자동으로 해결”하는 것이 아니다. 목적은 처음 5분에 확인할 것, 피해를 줄이는 순서, 위험한 조치의 승인 기준, 사후 학습 항목을 미리 정해 두는 것이다.
전체 흐름: 탐지 → 분류 → 완화 → 복구 → 학습
사용자 영향 확인
대시보드 고정
Incident Commander 지정
역할 분리
쿼리/배치 중단
failover 판단
데이터 정합성 확인
all-clear 공지
RCA 작성
재발방지 액션
Google SRE 문서에서는 사고 대응의 기본 원칙으로 명확한 지휘 체계, 역할 정의, 디버깅과 완화 작업의 작업 기록, 빠른 사고 선언을 강조한다. 특히 Incident Commander, Communications Lead, Operations Lead처럼 역할을 분리하면 한 사람이 기술 조치와 대외 설명을 동시에 떠안지 않아도 된다.
데이터베이스 운영에서도 이 구조가 그대로 필요하다. DB는 상태ful한 시스템이기 때문에 잘못된 완화 조치가 데이터 손실, split-brain, 복구 불가능한 overwrite로 이어질 수 있다. “일단 재시작”이나 “일단 failover”가 위험한 이유가 여기에 있다.
0단계: 평시 준비가 없으면 런북은 실행되지 않는다
사고 중에 새로 정하면 늦는 것들이 있다.
| 준비 항목 | 미리 정해야 하는 이유 |
|---|---|
| SEV 기준 | “심각한가?” 논쟁을 줄이고 호출 범위를 빠르게 정한다. |
| 서비스별 owner | 알림을 받은 사람이 DB owner가 아닐 수 있다. escalation 경로가 필요하다. |
| 대시보드 링크 | 사고 중에는 올바른 Grafana, CloudWatch, PMM, Datadog 링크를 찾는 시간도 비용이다. |
| 안전한 읽기 전용 진단 계정 | 과도한 권한으로 접속하면 실수로 변경 쿼리를 실행할 수 있다. |
| 위험 조치 승인 기준 | failover, primary restart, 대량 kill, backup restore는 독단으로 하면 안 된다. |
| 최근 변경 목록 | 장애의 상당수는 배포, 설정 변경, DDL, 배치 변경과 연관된다. |
평시에는 “런북 문서”만 만들지 말고, 실제로 아래를 연습해야 한다.
- 알림을 누가 받고 어디에 모이는가?
- 첫 상태 업데이트는 몇 분 안에 올리는가?
- DB primary와 replica의 현재 역할을 어떻게 확인하는가?
- 복구 리허설과 failover 리허설은 마지막으로 언제 했는가?
- 대시보드의 지표명과 런북의 지표명이 일치하는가?
1단계: 탐지 — 알림을 증상과 사용자 영향으로 번역한다
알림은 원인이 아니라 관찰된 현상이다. CPU > 90%는 원인이 아니라 결과일 수 있다. DB 사고 대응의 첫 화면은 다음 네 축을 동시에 본다.
| 축 | 예시 지표 | 먼저 묻는 질문 |
|---|---|---|
| 사용자 영향 | error rate, p95/p99 latency, timeout, checkout 실패율 | 사용자가 실제로 실패하고 있는가? |
| DB 포화 | CPU, memory, disk I/O, WAL/binlog 생성량, connection count | 자원이 포화되어 있는가? |
| 대기와 경합 | lock wait, deadlock, row lock time, wait_event, thread state | 느린가, 막혀 있는가? |
| 복제/내구성 | replica lag, replication thread state, WAL/binlog 보존, backup 상태 | 읽기 stale 또는 failover 손실 위험이 있는가? |
OpenTelemetry의 DB metric convention은 db.client.operation.duration 같은 클라이언트 관점의 지연 시간과 connection pool 지표를 정의한다. 운영에서는 DB 내부 지표만 보지 말고, 애플리케이션의 connection pool 대기, timeout, operation duration도 함께 봐야 한다. DB가 멀쩡해 보여도 connection pool이 고갈되면 사용자는 장애를 겪는다.
PostgreSQL 첫 확인 쿼리
-- 지금 오래 실행 중인 쿼리와 대기 이벤트
SELECT pid,
usename,
application_name,
state,
wait_event_type,
wait_event,
now() - query_start AS query_age,
now() - xact_start AS xact_age,
left(query, 200) AS query_sample
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_age DESC NULLS LAST
LIMIT 30;
-- 복제 상태: primary에서 확인
SELECT application_name,
client_addr,
state,
sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication
ORDER BY replay_lag_bytes DESC NULLS LAST;PostgreSQL의 pg_stat_activity는 현재 세션, 쿼리, wait event를 보고, pg_stat_replication은 연결된 standby의 WAL 전송/적용 상태를 보여준다. 단, 통계 뷰는 권한과 snapshot/caching 동작의 영향을 받으므로 “값이 안 보인다”가 곧 “문제가 없다”는 뜻은 아니다.
MySQL 첫 확인 쿼리
-- 현재 실행 중인 세션과 대기 상태
SELECT id, user, host, db, command, time, state, left(info, 200) AS query_sample
FROM information_schema.processlist
WHERE command <> 'Sleep'
ORDER BY time DESC
LIMIT 30;
-- InnoDB 트랜잭션과 잠금 대기
SELECT trx_id,
trx_mysql_thread_id,
trx_state,
trx_started,
trx_wait_started,
trx_rows_locked,
trx_rows_modified,
left(trx_query, 200) AS query_sample
FROM information_schema.innodb_trx
ORDER BY trx_started;
-- replica에서 복제 상태 확인
SHOW REPLICA STATUS\GMySQL 8.0.22 이후에는 SHOW SLAVE STATUS 대신 SHOW REPLICA STATUS가 권장된다. 또한 MySQL Performance Schema replication tables는 SHOW REPLICA STATUS보다 programmatic하게 조회하기 좋고, multi-threaded replica의 worker별 오류도 더 자세히 볼 수 있다.
2단계: 분류 — 장애 유형을 먼저 좁힌다
DB 사고는 “DB가 느리다”로 묶으면 대응이 느려진다. 최소한 아래처럼 분류해야 한다.
| 유형 | 대표 증상 | 주요 위험 | 초기 완화 방향 |
|---|---|---|---|
| 접속 장애 | connection refused, max connections, pool timeout | 전체 서비스 장애 | connection storm 차단, pool 제한, idle 세션 정리 |
| 지연 증가 | p99 상승, slow query 증가, CPU/I/O 포화 | timeout 전파, 큐 적체 | 비핵심 트래픽 차단, 느린 쿼리/배치 중단 |
| 잠금/경합 | lock wait, deadlock, blocked sessions | 일부 테이블 전체 정지 | blocker 식별, 안전한 kill/cancel |
| 복제 지연 | stale read, replica lag 증가 | 잘못된 읽기, failover 손실 | replica read 제거, lag 원인 분석 |
| 저장공간 압박 | disk full, WAL/binlog 증가 | DB write 중단, 복구 실패 | 보존 정책 확인, 긴급 용량 확보 |
| 데이터 손상/오삭제 | row mismatch, accidental delete | 잘못된 데이터 확산 | 쓰기 차단, PITR/restore 계획 전환 |
분류의 목적은 원인을 100% 맞히는 것이 아니라 금지할 행동과 허용할 행동을 빠르게 정하는 것이다. 예를 들어 데이터 손상 의심이면 “성능 회복”보다 “추가 쓰기와 복제 확산을 막는 것”이 먼저다. 복제 지연이면 “읽기 트래픽을 replica에서 빼는 것”이 먼저고, primary restart는 마지막 선택이어야 한다.
3단계: 완화 — 원인 제거보다 피해 축소가 먼저다
사고 중에는 “root cause를 바로 고쳐야 한다”는 압박이 강하다. 하지만 운영 관점에서 우선순위는 보통 다음 순서다.
- 사용자 영향 축소: read-only 모드, feature flag off, 비핵심 API rate limit, 대형 배치 중단.
- DB 보호: 신규 connection 폭주 차단, runaway query 중단, 불필요한 replica read 제거.
- 데이터 안전 확보: backup/binlog/WAL 보존 확인, 최근 변경 중단, 쓰기 경로 보호.
- 근본 조치: 인덱스 추가, 파라미터 변경, failover, restore, 코드 rollback.
흔한 완화 조치와 주의점
| 조치 | 유효한 상황 | 주의점 |
|---|---|---|
| 배치/리포트 쿼리 중단 | OLTP가 배치에 밀릴 때 | 중단 대상이 핵심 쓰기인지 확인한다. |
| 특정 API rate limit | 읽기/쓰기 폭주가 명확할 때 | retry storm을 만들지 않도록 클라이언트 동작도 확인한다. |
| connection pool 축소 | DB가 connection storm으로 무너질 때 | 너무 낮추면 정상 요청도 굶는다. 단계적으로 조정한다. |
| blocker session cancel/kill | 명확한 잠금 보유자가 있을 때 | 트랜잭션 rollback 비용과 앱 재시도 폭주를 고려한다. |
| replica read 제거 | replica lag/stale read가 문제일 때 | primary 부하를 추가로 올릴 수 있다. |
| failover | primary 장애, 복구보다 전환이 빠를 때 | lag, split-brain, DNS/cache, application reconnect를 확인한다. |
| PITR/restore | 데이터 손상, 오삭제, corruption | RPO 손실과 restore 후 검증 시간을 사용자에게 설명해야 한다. |
kill/cancel 전에 확인할 것
-- PostgreSQL: blocker와 waiter를 함께 확인하는 예시
SELECT blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_locks blocked_locks
JOIN pg_stat_activity blocked ON blocked.pid = blocked_locks.pid
JOIN pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid <> blocked_locks.pid
JOIN pg_stat_activity blocking ON blocking.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted
AND blocking_locks.granted;pg_cancel_backend() 또는 pg_terminate_backend()를 실행하기 전에는 해당 세션이 migration, backup, vacuum, 결제/주문 처리 같은 중요한 작업인지 확인해야 한다. MySQL의 KILL QUERY와 KILL CONNECTION도 마찬가지다. 특히 큰 트랜잭션은 kill 이후 rollback 자체가 오래 걸릴 수 있다.
4단계: 복구 — “알림이 꺼짐”과 “정상 복구”는 다르다
장애가 완화되면 곧바로 종료하지 말고 정상성을 검증한다.
| 검증 항목 | 확인 방법 |
|---|---|
| 사용자 영향 회복 | p95/p99 latency, error rate, synthetic check, 핵심 API 성공률 |
| DB 포화 해소 | CPU/I/O/connection/lock wait가 기준선으로 복귀했는지 확인 |
| 복제 회복 | replica lag가 감소 추세인지, read routing을 되돌려도 안전한지 확인 |
| 데이터 정합성 | affected table row count, checksum, reconciliation query, business invariant 확인 |
| 백업/로그 보존 | 사고 중 WAL/binlog/backup chain이 끊기지 않았는지 확인 |
| 재발 위험 | 같은 배치, 쿼리, 배포가 다시 실행될 가능성이 있는지 확인 |
AWS DR 문서는 RTO/RPO를 충족하려면 backup/restore, pilot light, warm standby, active/active처럼 비용과 복잡도가 다른 전략을 선택해야 한다고 설명한다. DB 사고에서도 복구 조치는 항상 RTO/RPO와 연결된다. 예를 들어 오삭제 사고에서 “서비스를 빨리 열기”만 보면 잘못된 데이터를 계속 쓰게 될 수 있고, “정합성 검증”만 보면 RTO를 초과할 수 있다. 런북에는 이 tradeoff를 누가 결정하는지 적어야 한다.
5단계: 원인분석 — 범인을 찾지 말고 시스템 조건을 찾는다
postmortem은 책임 추궁 문서가 아니라 재발 확률을 낮추는 운영 문서다. 최소 구성은 다음과 같다.
| 섹션 | 내용 |
|---|---|
| Summary | 어떤 일이 있었고, 사용자는 무엇을 겪었는가 |
| Impact | 기간, 영향 범위, 실패율, 데이터 영향, SLA/SLO 영향 |
| Timeline | 알림, 판단, 조치, 복구, 커뮤니케이션 시각 |
| Trigger | 직접적으로 장애를 발생시킨 이벤트 |
| Root causes | 그런 trigger가 장애로 이어진 시스템 조건 |
| Detection gaps | 더 빨리 알 수 있었던 신호 |
| Mitigation gaps | 완화가 늦어진 이유 |
| Action items | owner, due date, 검증 방법이 있는 재발방지 과제 |
좋은 RCA는 “운영자가 실수했다”에서 멈추지 않는다. 예를 들어 다음처럼 바꿔야 한다.
| 나쁜 결론 | 더 나은 결론 |
|---|---|
| DBA가 DDL 위험을 확인하지 않았다. | DDL 리뷰 체크리스트에 metadata lock 확인과 rollback plan 필드가 없었다. |
| 개발자가 느린 쿼리를 배포했다. | 배포 전 plan regression을 잡는 테스트와 slow query budget이 없었다. |
| on-call이 늦게 대응했다. | 알림이 사용자 영향과 연결되지 않아 SEV 판단이 지연됐다. |
Google SRE의 postmortem 자료는 일관된 템플릿으로 root cause, trigger, action item을 남기고, 여러 postmortem에서 추세를 분석해 시스템적 개선을 찾는 방식을 강조한다. DB 운영에서도 단일 사고만 보지 말고 “매번 같은 테이블에서 lock wait가 나는가”, “항상 배치 시간대에 replica lag가 나는가”, “복구 연습을 안 한 영역에서만 복구가 늦는가”를 봐야 한다.
데이터베이스 사고 대응 런북 템플릿
아래 템플릿은 실제 운영 문서의 시작점으로 쓸 수 있다.
# DB Incident Runbook: <service/database>
## Scope
- 대상 DB:
- 주요 서비스:
- owner / secondary owner:
- 대시보드:
- 로그/트레이스:
- backup/restore 문서:
## Severity
- SEV1:
- SEV2:
- SEV3:
## First 5 minutes
1. 알림 acknowledge
2. incident channel 생성 또는 참여
3. Incident Commander / Operations Lead / Communications Lead 지정
4. 사용자 영향 확인: error rate, latency, 핵심 API
5. 최근 변경 확인: deploy, DDL, batch, traffic spike
6. 현재 primary/replica 역할 확인
7. 첫 상태 업데이트 게시
## Diagnostics
- 접속 장애:
- 지연 증가:
- lock contention:
- replication lag:
- disk/WAL/binlog pressure:
- 데이터 손상 의심:
## Mitigation decision table
- kill/cancel 가능한 조건:
- traffic 제한 조건:
- replica read 제거 조건:
- failover 조건:
- restore/PITR 전환 조건:
## Communication
- 내부 업데이트 주기:
- 사용자/고객 업데이트 기준:
- 알려야 할 stakeholder:
## Recovery validation
- 서비스 지표:
- DB 지표:
- 데이터 정합성:
- replication:
- backup/log chain:
## Post-incident
- timeline owner:
- RCA owner:
- action item review date:
- runbook update 여부:실전 체크리스트
사고 중에는 아래 순서를 종이에 출력해 둔 것처럼 사용하면 좋다.
- 선언한다. “DB incident로 취급한다”고 말하고 채널과 역할을 만든다.
- 사용자 영향을 먼저 본다. 내부 CPU보다 error rate와 latency가 우선이다.
- 최근 변경을 확인한다. 배포, DDL, batch, traffic spike, config change.
- 읽기/쓰기/복제 중 어디가 깨졌는지 분리한다. 모두 같은 대응이 아니다.
- 위험한 조치에는 승인 기준을 적용한다. failover, restart, restore, 대량 kill.
- 조치마다 검증 지표를 붙인다. “kill 했다”가 아니라 “lock wait가 줄었는가”를 본다.
- 종료 전에 정합성을 확인한다. 데이터베이스 사고는 알림이 꺼져도 데이터 문제가 남을 수 있다.
- postmortem action item을 추적한다. owner와 due date 없는 교훈은 재발방지가 아니다.
References
- Google SRE Workbook, “Incident Response”: https://sre.google/workbook/incident-response
- Google SRE Workbook, “Postmortem Analysis”: https://sre.google/workbook/postmortem-analysis
- Google SRE, “Incident Management Guide”: https://static.googleusercontent.com/media/sre.google/en//static/pdf/IncidentManagementGuide.pdf
- AWS Documentation, “Disaster recovery options in the cloud”: https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-options-in-the-cloud.html
- PostgreSQL Documentation, “The Cumulative Statistics System”: https://www.postgresql.org/docs/current/monitoring-stats.html
- MySQL 8.4 Reference Manual, “Checking Replication Status”: https://dev.mysql.com/doc/refman/8.4/en/replication-administration-status.html
- MySQL 8.4 Reference Manual, “Performance Schema Replication Tables”: https://dev.mysql.com/doc/refman/8.4/en/performance-schema-replication-tables.html
- OpenTelemetry Semantic Conventions, “Database client metrics”: https://opentelemetry.io/docs/specs/semconv/db/database-metrics/