포스트

PostgreSQL 모니터링 — 증상에서 pg_stat으로 원인 좁히기

PostgreSQL 장애를 현재 세션·누적 쿼리 부하·테이블 상태·운영 상태로 나눈 뒤 pg_stat_activity, pg_stat_statements, pg_stat_user_tables, pg_stat_replication과 로그를 이용해 원인을 좁히는 흐름을 정리한다.

PostgreSQL 모니터링 — 증상에서 pg_stat으로 원인 좁히기

“DB가 느리다”는 증상만으로는 볼 곳이 너무 많다. PostgreSQL 모니터링의 핵심은 pg_stat_* View를 많이 외우는 것이 아니라, 문제를 몇 개의 관측 축으로 나눈 뒤 해당 통계로 내려가는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DB가 느리다 / 멈춘다 / 밀린다
        ↓
지금 이 순간의 문제인가?
├─ Session / Connection
├─ Lock / Wait
└─ Long Transaction
        ↓
시간이 누적되어 나타난 부하인가?
├─ Query
├─ Table / Index
└─ Cache / I/O
        ↓
운영 상태가 뒤처진 것인가?
├─ VACUUM
└─ Replication

이 글은 이 진단 순서에 맞춰 PostgreSQL의 관측 지점을 연결한다. 세부 설정값을 외우기보다 증상 → 관측 대상 → 다음 판단을 찾는 것이 목적이다.

먼저 문제 공간을 나눈다

질문먼저 볼 곳확인하려는 것
지금 누가 DB를 붙잡고 있나pg_stat_activityActive Query, Connection, Long Transaction
누가 누구를 막고 있나pg_blocking_pids(), pg_locksLock Wait
평소 어떤 Query가 부하를 만드나pg_stat_statements호출 횟수·누적 시간·평균 시간
Table과 Index 상태는 어떤가pg_stat_user_tables, pg_stat_user_indexesSeq Scan, Dead Tuple, Index 사용량
Memory와 Disk 사이에서 얼마나 읽나pg_stat_database, pg_statio_*Buffer Hit / Read
Standby가 따라오고 있나pg_stat_replicationWAL 전송·재생 지연
특정 실행이 왜 느렸나Slow Query Log, auto_explain실제 Query와 Execution Plan

중요한 것은 현재 상태와 누적 통계를 섞지 않는 것이다.

1
2
3
4
5
pg_stat_activity
→ 지금 무슨 일이 일어나는가

pg_stat_statements
→ 지금까지 어떤 Query가 부하를 만들었는가

둘은 서로 대체하지 않는다.

1. 현재 문제 — Session과 실행 중 Query

pg_stat_activity는 현재 PostgreSQL Backend Session을 한 행씩 보여준다. 장애 순간에 가장 먼저 보기 좋은 관측점이다.

1
2
3
4
5
SELECT pid, usename, state, wait_event_type, wait_event,
       now() - query_start AS duration, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC;

여기서는 Query Text보다 먼저 다음을 본다.

  • state: 실제 실행 중인지, idle in transaction인지
  • wait_event_type / wait_event: 무엇인가를 기다리는지
  • query_start / xact_start: Query와 Transaction이 얼마나 오래 열려 있는지

오래된 idle in transaction은 단순히 놀고 있는 Connection이 아니다. Transaction과 Lock을 오래 유지하고 VACUUM이 정리할 수 있는 범위에도 영향을 줄 수 있다.

Connection 자체가 문제인지 보려면 상태별로 묶는다.

1
2
3
4
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;

max_connections에 가까워지는 상황이라면 Query Tuning보다 먼저 Connection Pool과 누수 여부를 본다.

2. 현재 문제 — Lock과 Wait

Query가 오래 걸린다고 해서 항상 Query 자체가 느린 것은 아니다. 다른 Session을 기다리는 중일 수 있다.

1
2
3
4
5
6
7
8
SELECT blocked.pid AS blocked_pid,
       blocked.query AS blocked_query,
       blocking.pid AS blocking_pid,
       blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
1
2
3
4
5
오래 실행 중
├─ Wait 없음
│   → Query 자체의 Execution Plan·I/O 확인
└─ Lock Wait
    → Blocking Session과 Transaction 확인

pg_locks는 Lock 자체의 상세 상태가 필요할 때 한 단계 더 내려가서 본다. 처음부터 Lock 목록 전체를 읽기보다 Blocking 관계를 먼저 잡는 편이 이해하기 쉽다.

3. 누적 부하 — 어떤 Query가 전체 시간을 쓰나

현재 순간만 보면 간헐적으로 반복되는 무거운 Query를 놓칠 수 있다. 이때 pg_stat_statements가 같은 형태의 Query를 묶어 누적 통계를 제공한다.

사용하려면 Extension을 활성화한다.

1
2
shared_preload_libraries = 'pg_stat_statements'
compute_query_id = on
1
CREATE EXTENSION pg_stat_statements;
1
2
3
4
5
6
7
8
9
SELECT queryid,
       calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       rows,
       query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

mean_exec_time과 total_exec_time은 다른 질문에 답한다.

1
2
3
4
5
6
7
mean_exec_time 높음
→ 한 번 실행할 때 무겁다
→ Execution Plan 우선

total_exec_time 높음
→ 전체 기간에 많은 시간을 소비했다
→ 실행 시간 × 호출 빈도 관점

느린 Query 한 번과 자주 호출되는 Query를 같은 방식으로 튜닝하지 않는다.

4. 누적 부하 — Table과 Index 접근 패턴

Table

1
2
3
4
5
SELECT relname, seq_scan, idx_scan,
       n_live_tup, n_dead_tup,
       last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
  • 큰 Table에서 seq_scan이 계속 증가한다 → Index 부재나 Query 조건을 확인
  • n_dead_tup이 많이 쌓인다 → VACUUM이 따라오는지 확인
  • last_autovacuum이 오래됐다 → autovacuum 동작 조건과 부하를 확인

단순히 seq_scan이 많다는 이유만으로 Index를 추가하지 않는다. 작은 Table이나 대부분의 Row를 읽는 Query에서는 Sequential Scan이 적절할 수 있다.

Index

1
2
3
4
5
6
7
8
9
SELECT s.relname AS table_name,
       s.indexrelname AS index_name,
       s.idx_scan,
       pg_size_pretty(pg_relation_size(s.indexrelid)) AS size
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0
  AND NOT i.indisunique
ORDER BY pg_relation_size(s.indexrelid) DESC;

idx_scan = 0은 삭제 명령이 아니라 조사 시작점이다. 통계가 언제부터 누적됐는지, 특정 Batch에서만 쓰는 Index인지, Constraint 유지에 필요한지 확인한 뒤 판단한다.

5. 누적 부하 — Buffer와 I/O

1
2
3
4
5
SELECT datname,
       round(100.0 * blks_hit
             / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_ratio
FROM pg_stat_database
WHERE datname = current_database();

Hit Ratio 하나에 고정 임계값을 두고 정상/비정상을 판정하지 않는다. Workload 특성, Table 크기, Sequential Scan, OS Page Cache까지 함께 봐야 한다.

이 수치는 다음 질문으로 연결하는 신호에 가깝다.

1
2
3
4
5
6
7
8
9
Read 증가
  ↓
어떤 Query가 읽는가?
  ↓
어떤 Table을 읽는가?
  ↓
Sequential Scan인가?
  ↓
Execution Plan과 Working Set 확인

6. 운영 상태 — VACUUM이 따라오는가

MVCC를 사용하는 PostgreSQL에서는 UPDATE/DELETE 뒤의 오래된 Tuple을 정리하는 VACUUM이 운영 상태의 일부다.

1
2
3
4
5
Dead Tuple 증가
        +
Autovacuum이 오래 실행되지 않음
        ↓
VACUUM이 Workload를 따라오지 못하는지 확인

여기서 바로 수동 VACUUM이나 Parameter 변경으로 뛰어들기보다 Transaction이 지나치게 오래 열려 있지 않은지, Autovacuum Worker가 실제로 동작하는지, Table별 설정이 어떻게 되어 있는지 확인한다.

MVCC의 원리는 트랜잭션 동시성 제어 — Isolation Level, Lock, MVCC에서 다룬다. VACUUM의 동작과 운영 기준은 PostgreSQL 공식 문서를 참조한다.

7. 운영 상태 — Replication이 따라오는가

1
2
3
4
SELECT application_name, state,
       write_lag, flush_lag, replay_lag,
       pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_bytes
FROM pg_stat_replication;

하나의 lag 숫자보다 WAL이 어느 단계에서 밀리는지를 본다.

1
2
3
4
5
6
7
Primary WAL
   ↓ sent
Standby write
   ↓ flush
Disk 반영
   ↓ replay
DB 상태 반영

sent_lsn과 replay_lsn 차이가 계속 커지면 Standby가 생성되는 WAL을 따라오지 못하고 있다는 뜻이다. 동기/비동기 복제 여부에 따라 영향은 다르므로 Replication 구성과 함께 판단한다.

8. 특정 실행을 잡아야 할 때 — Log와 auto_explain

pg_stat_statements는 누적 통계에는 강하지만 특정 실행의 Parameter와 그 순간의 Execution Plan을 그대로 보존하는 도구는 아니다.

1
log_min_duration_statement = 1000

특정 실행의 Plan까지 자동으로 남기려면 auto_explain을 검토한다.

1
2
3
shared_preload_libraries = 'pg_stat_statements,auto_explain'
auto_explain.log_min_duration = 1000
auto_explain.log_analyze = on

auto_explain.log_analyze는 실제 실행 계측을 추가하므로 운영에서는 Overhead를 고려해 사용한다.

장애 상황에서의 최소 진단 순서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. pg_stat_activity
   └─ 지금 오래 도는 Query / Transaction / Wait가 있는가?

2. pg_blocking_pids()
   └─ 다른 Session에 막혀 있는가?

3. pg_stat_statements
   └─ 누적 부하 상위 Query는 무엇인가?

4. pg_stat_user_tables / indexes
   └─ 특정 Table·Index에 이상 신호가 있는가?

5. pg_stat_database / pg_statio_*
   └─ Read 패턴이 변했는가?

6. VACUUM / Replication
   └─ 운영 유지 작업이 Workload를 따라오는가?

7. Log / auto_explain
   └─ 특정 실행을 더 깊게 분석해야 하는가?

이 순서가 항상 정답은 아니다. 중요한 것은 관측 도구 이름이 아니라 어느 질문에 답하기 위해 그 도구를 보는지다.

정리

PostgreSQL 모니터링을 View 목록으로 외우면 장애 때 다시 길을 잃는다. 대신 세 층으로 나눈다.

1
2
3
4
5
6
7
8
현재 상태
→ Session / Wait / Transaction

누적 부하
→ Query / Table / Index / I/O

운영 상태
→ VACUUM / Replication

그다음 각 질문에 맞춰 pg_stat_activity, pg_stat_statements, pg_stat_user_tables, pg_stat_replication으로 Zoom-in 한다. 모니터링은 통계를 보는 작업이 아니라 문제 공간을 좁히는 작업이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.