DB Study

14장. 모니터링과 트러블슈팅 — "느려요"를 지표로 번역하기

"DB가 느려요"는 증상이지 진단이 아닙니다. pg_stat_* 뷰와 로그만으로 대부분의 장애를 좁혀 들어가는, 새벽 2시에도 통하는 사고 순서를 몸에 새깁니다.

중급 ⏱ 약 30분 🎬 애니메이션 3개 선수 지식: 7~10장
🧭
이 장에서 배우는 것

① 장애 시 최상위 진단 5단계 — 연결→Lock→긴 트랜잭션→쿼리→리소스 ② pg_stat_* 뷰 지도와 핵심 지표 ③ Lock 대기 체인을 pg_blocking_pids()로 푸는 법 ④ 슬로우 쿼리 추적 워크플로우와 알림 임계값

14.1 장애의 첫 5분 — 진단 순서를 몸에 새겨라

장애 대응에서 가장 비싼 것은 "어디부터 볼까" 고민하는 시간입니다. 현업 DBA는 항상 같은 순서로 봅니다: 값싸고 흔한 원인부터, 비싸고 드문 원인 순으로.

🔥 "느려요 / 타임아웃!" ① 연결 수 — max_connections 근접? SELECT count(*), state FROM pg_stat_activity 근접 → pgBouncer 풀 확인 idle 세션 정리 ② Lock 대기 체인 있나? pg_blocking_pids(pid) 있음 → blocker 세션 처리 (14.2에서 자세히) ③ 긴 트랜잭션 / idle in transaction? now() - xact_start 정렬 있음 → 원인 세션 종료 + idle..._timeout 설정 ④ 특정 쿼리 급증? pg_stat_statements → EXPLAIN ANALYZE ⑤ 그래도 아니면: 리소스 · CPU / 메모리 / 스왑 · Disk I/O — pg_stat_io · Checkpoint — bgwriter · 복제 lag — replication 값싼 원인부터 순서대로 — 이것이 5분 진단의 전부!
-- 5단계 진단 쿼리 묶음 (즐겨찾기 필수)
-- ① 연결 수
SELECT count(*), state FROM pg_stat_activity GROUP BY state;

-- ③ 긴 트랜잭션
SELECT pid, state, now()-xact_start AS age, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL AND now()-xact_start > interval '1 minute'
ORDER BY age DESC;

-- ④ 무거운 쿼리 TOP 10
SELECT query, calls, round(mean_exec_time::numeric, 2) AS mean_ms
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

14.2 Lock 체인 — 한 명이 모두를 세운다

Lock 장애의 전형적인 그림: 누군가 트랜잭션을 열어 ALTER TABLE(또는 커밋을 잊은 UPDATE)을 잡고 있고, 그 뒤로 수백 개의 세션이 기차처럼 줄줄이 기다립니다. 관건은 대기자가 아니라 맨 앞의 blocker를 찾는 것입니다.

orders 테이블 pid 101 idle in transaction UPDATE 후 커밋 안 함 😴 🔒 RowExclusive 보유 pid 202 ALTER TABLE ... ⏳ AccessExclusive 대기 pid 303, 304, 305… 평범한 SELECT들까지 ⏳ ALTER 뒤에서 전부 대기 체인: SELECT들 → ALTER → 잠든 pid 101 (진짜 범인은 맨 앞!) SELECT pg_blocking_pids(202); → {101} pg_cancel_backend(101) → 안 되면 pg_terminate_backend(101) → 체인 전체 해소 ✅
-- 누가 누구를 막고 있는가 (v9.6+)
SELECT blocked.pid  AS blocked_pid,  blocked.query  AS blocked_query,
       blocking.pid AS blocking_pid, blocking.state AS blocking_state,
       blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';

-- 재발 방지: 잊힌 트랜잭션 자동 정리
-- postgresql.conf: idle_in_transaction_session_timeout = '10min'
⚠️
pg_terminate_backend는 롤백 시간도 계산에 넣어라

강제 종료된 세션의 미커밋 트랜잭션은 롤백됩니다. 대상이 3시간짜리 대량 쓰기였다면 롤백에도 상당한 시간이 걸리고, 그동안 락은 유지됩니다. 먼저 pg_cancel_backend(쿼리 취소)를 시도하는 것이 순서입니다.

14.3 슬로우 쿼리 추적 — 증상→증거→수정→검증 루프

"어제부터 이 화면이 느려요"를 해결하는 표준 루프입니다. 핵심은 감(感)이 아니라 재현 가능한 증거로 움직이는 것.

📜 증거 수집 log_min_duration _statement = 1000 + pg_stat_statements 🔎 용의자 특정 total_exec_time 순 TOP N + pgBadger 리포트 🧪 계획 분석 EXPLAIN (ANALYZE, BUFFERS) 🛠 수정·검증 인덱스/쿼리 수정 배포 후 재측정 📜 증거 수집 log_min_duration _statement = 1000 + pg_stat_statements 🔎 용의자 특정 total_exec_time 순 TOP N + pgBadger 리포트 🧪 계획 분석 EXPLAIN (ANALYZE, BUFFERS) 체크: Seq Scan이 의도인가? Rows Removed by Filter 큰가? Buffers: read가 hit보다 큰가? 🛠 수정·검증 인덱스/쿼리 수정 배포 후 재측정 pg_stat_statements_reset() 후 N일 뒤 재측정 — 루프가 닫혀야 해결이다

14.4 pg_stat_* 뷰 지도 — 어디서 무엇을 보나

계층대표 질문
pg_stat_activity세션지금 누가 무엇을 실행/대기 중인가?
pg_locks세션어떤 락을 보유/대기 중인가?
pg_stat_databaseDB캐시 히트율, deadlock, temp 파일은?
pg_stat_user_tables객체dead tuple 비율, 마지막 autovacuum은?
pg_stat_user_indexes객체한 번도 안 쓰인 인덱스는?
pg_stat_bgwriterI/OCheckpoint가 강제(req)로 얼마나 도나?
pg_stat_wal (v14+)I/OWAL 생성량과 FPI 비율은?
pg_stat_io (v16+)I/O어떤 backend가 어떤 맥락으로 I/O를 내나?
pg_stat_replication복제스탠바이별 write/flush/replay lag은?
pg_stat_statements쿼리가장 무거운 쿼리 TOP N은? (extension)

자주 보는 건강 지표 몇 가지의 해석 기준:

-- DB 건강 요약 한 방 쿼리
SELECT datname, numbackends, xact_commit, deadlocks,
       round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_pct,
       temp_files, temp_bytes
FROM pg_stat_database
WHERE datname NOT IN ('template0','template1');

14.5 알림 임계값 — 시작점 치트시트

지표WarningCritical
Connection 사용률70%90%
Cache hit ratio< 95%< 90%
Replication lag100MB / 30s1GB / 120s
Long-running 트랜잭션> 5min> 30min
Dead tuple % (주요 테이블)> 20%> 40%
Checkpoint req 비율> 30%> 50%
디스크 사용률 (데이터/WAL)75%90%
💡
로그는 미리 켜두어야 증거가 된다

장애 후에 켜면 늦습니다: log_min_duration_statement = 1000, log_lock_waits = on, log_checkpoints = on, log_autovacuum_min_duration = '1s', log_temp_files = 0. 단, log_statement = 'all'은 디스크·CPU를 잡아먹으니 감사 목적이면 pgaudit(13장)을 쓰세요.

🚨
증상별 즉시 확인 요약

too many connections → pg_stat_activity 카운트·pgBouncer / 쿼리 급락 → pg_stat_statements·EXPLAIN / Lock 폭주 → pg_blocking_pids() / WAL 디스크 급증 → pg_replication_slots·pg_stat_archiver / XID 경고 → age(datfrozenxid) 후 긴급 VACUUM FREEZE (8장)

✍️ 이해도 체크

수백 개의 SELECT가 갑자기 전부 멈췄습니다. pg_blocking_pids()로 추적하니 체인의 맨 앞에 idle in transaction 세션 하나가 있습니다. 가장 적절한 첫 조치는?
✅ 대기자가 아니라 체인 맨 앞의 blocker를 제거해야 전체가 풀립니다. 쿼리만 취소하는 pg_cancel_backend 먼저, 안 되면 세션을 종료하는 pg_terminate_backend 순서입니다. 재발 방지로 idle_in_transaction_session_timeout을 설정하세요. 서버 재시작은 정상 트랜잭션까지 날리는 과잉 대응입니다.
"어떤 쿼리가 DB 시간을 가장 많이 쓰는가"를 누적 통계로 확인하기에 가장 적합한 도구는?
✅ pg_stat_activity는 "지금 이 순간"의 스냅샷이라 순간적으로 빠른 쿼리가 수백만 번 호출되는 패턴을 놓칩니다. pg_stat_statements는 쿼리를 정규화해 장기 누적으로 집계하므로 "총 시간을 가장 많이 쓰는 쿼리"를 찾는 표준 도구입니다. EXPLAIN ANALYZE는 용의자를 특정한 뒤의 정밀 분석 단계입니다.
📚
원문으로 더 깊이

이 장의 원문 문서: chapters/ch14_monitoring_troubleshooting.md — Lock 모드 호환성 표, Deadlock 로그 해석, pg_stat_io/pg_stat_wal 상세, 외부 도구(pgBadger·Prometheus) 구성이 담겨 있습니다.