들어가며
PostgreSQL을 운영하면서 “DB가 느려요”라는 말을 들으면 어디서부터 봐야 할지 막막할 때가 있습니다. 이 글에서는 PostgreSQL 모니터링의 핵심 지표들과 이를 Grafana로 시각화하는 방법을 정리합니다.
실무에서 자주 마주치는 트러블슈팅 상황과 AWS Aurora 환경에 대해서는 PostgreSQL 트러블슈팅 가이드에서 다룹니다.
모니터링 아키텍처
PostgreSQL → postgres_exporter → Prometheus → Grafana- postgres_exporter: PostgreSQL 메트릭을 Prometheus 형식으로 노출
- Prometheus: 메트릭 수집 및 저장
- Grafana: 시각화 및 알람
핵심 모니터링 지표
1. 커넥션 (Connections)
PostgreSQL은 프로세스 기반 아키텍처입니다. 클라이언트가 연결될 때마다 새로운 프로세스(backend process)가 fork됩니다. 이 구조의 장점은 안정성이지만, 커넥션 하나당 메모리(보통 5~10MB)와 OS 자원을 소비한다는 단점이 있습니다.
따라서 커넥션 수가 과도하게 늘어나면:
- 메모리 부족으로 OOM Killer가 발동할 수 있음
- 컨텍스트 스위칭 오버헤드 증가
- 새 커넥션 생성 실패 (max_connections 초과)
실무에서는 애플리케이션의 커넥션 풀(HikariCP 등)과 PostgreSQL 앞단의 PgBouncer 같은 커넥션 풀러를 함께 사용하는 것이 일반적입니다.
-- 현재 커넥션 상태
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;state 값의 의미:
active: 쿼리 실행 중idle: 연결은 되어있지만 대기 중idle in transaction: 트랜잭션 내에서 대기 중 (주의 필요)idle in transaction (aborted): 에러 후 롤백 대기
특히 idle in transaction 상태가 오래 유지되면 락을 잡고 있을 수 있어 다른 쿼리를 블로킹할 수 있습니다.
주요 지표:
pg_stat_activity_count: 현재 활성 커넥션 수pg_settings_max_connections: 최대 허용 커넥션- 커넥션 사용률 = (활성 커넥션 / max_connections) × 100
알람 기준:
- Warning: 70% 이상
- Critical: 85% 이상
# PromQL - 커넥션 사용률
sum(pg_stat_activity_count) / pg_settings_max_connections * 1002. 트랜잭션 처리량 (Transaction Throughput)
TPS(Transactions Per Second)는 데이터베이스의 처리 능력을 나타내는 대표적인 지표입니다. 단순히 높은 TPS가 좋은 것이 아니라, 평소 대비 변화율을 관찰하는 것이 중요합니다. 갑자기 TPS가 떨어지면 락 경합이나 슬로우 쿼리를 의심해볼 수 있습니다.
롤백 비율도 함께 봐야 합니다. 롤백이 많다는 것은 애플리케이션에서 예외 처리가 빈번하거나, 데드락이 발생하고 있을 수 있다는 신호입니다.
-- 커밋/롤백 수
SELECT
xact_commit as commits,
xact_rollback as rollbacks
FROM pg_stat_database
WHERE datname = 'mydb';주요 지표:
pg_stat_database_xact_commit: 초당 커밋 수 (TPS)pg_stat_database_xact_rollback: 초당 롤백 수- 롤백 비율 = rollbacks / (commits + rollbacks) × 100
알람 기준:
- 롤백 비율 5% 이상 시 조사 필요
# PromQL - TPS
rate(pg_stat_database_xact_commit{datname="mydb"}[5m])3. 캐시 히트율 (Cache Hit Ratio)
PostgreSQL은 자주 접근하는 데이터를 shared_buffers라는 메모리 영역에 캐싱합니다. 캐시 히트율이 높다는 것은 대부분의 데이터를 메모리에서 읽고 있다는 의미이고, 낮다면 디스크 I/O가 빈번하게 발생한다는 뜻입니다.
디스크 읽기는 메모리 읽기보다 수백~수천 배 느리므로, 캐시 히트율은 쿼리 응답 시간에 직접적인 영향을 미칩니다. 일반적으로 OLTP 워크로드에서는 95% 이상의 히트율을 유지하는 것이 좋습니다.
히트율이 낮다면:
shared_buffers크기 증설 검토 (보통 전체 RAM의 25%)- 워킹 셋(자주 접근하는 데이터)이 메모리보다 큰지 확인
- 불필요한 풀 테이블 스캔이 발생하는지 쿼리 점검
-- 캐시 히트율
SELECT
sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) as ratio
FROM pg_statio_user_tables;주요 지표:
pg_statio_user_tables_heap_blks_hit: 캐시 히트pg_statio_user_tables_heap_blks_read: 디스크 읽기
알람 기준:
- 캐시 히트율 95% 미만 시 shared_buffers 증설 검토
# PromQL - 캐시 히트율
sum(pg_statio_user_tables_heap_blks_hit) /
(sum(pg_statio_user_tables_heap_blks_hit) + sum(pg_statio_user_tables_heap_blks_read))4. 튜플 활동 (Tuple Activity)
어떤 테이블에서 얼마나 많은 INSERT, UPDATE, DELETE가 발생하는지 파악하면 핫 테이블을 식별할 수 있습니다. 핫 테이블은 락 경합이 발생하기 쉽고, 인덱스 설계나 파티셔닝 전략 수립 시 우선 고려 대상입니다.
UPDATE가 많은 테이블은 dead tuple이 빠르게 쌓이므로 autovacuum 설정을 더 공격적으로 조정해야 할 수 있습니다.
-- 테이블별 튜플 활동
SELECT
relname,
n_tup_ins as inserts,
n_tup_upd as updates,
n_tup_del as deletes
FROM pg_stat_user_tables
ORDER BY n_tup_ins + n_tup_upd + n_tup_del DESC
LIMIT 10;주요 지표:
pg_stat_user_tables_n_tup_ins: INSERT 수pg_stat_user_tables_n_tup_upd: UPDATE 수pg_stat_user_tables_n_tup_del: DELETE 수
5. Dead Tuples과 Vacuum
PostgreSQL은 MVCC(Multi-Version Concurrency Control) 를 사용합니다. UPDATE나 DELETE를 실행해도 기존 행을 바로 삭제하지 않고, 새 버전을 만들고 이전 버전은 “dead” 상태로 표시합니다. 이렇게 해야 다른 트랜잭션이 이전 버전을 읽을 수 있기 때문입니다.
문제는 이 dead tuple들이 계속 쌓이면:
- 테이블 크기가 불필요하게 커짐 (table bloat)
- 인덱스 스캔 시 dead tuple도 방문해야 해서 성능 저하
- 디스크 공간 낭비
이를 정리하는 것이 VACUUM 작업입니다. PostgreSQL은 기본적으로 autovacuum 데몬이 자동으로 처리하지만, 대량 UPDATE/DELETE 후에는 수동 VACUUM이 필요할 수 있습니다.
-- Dead tuple 비율이 높은 테이블
SELECT
relname,
n_live_tup,
n_dead_tup,
round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 2) as dead_ratio
FROM pg_stat_user_tables
WHERE n_dead_tup > 0
ORDER BY dead_ratio DESC;주요 지표:
pg_stat_user_tables_n_dead_tup: Dead tuple 수pg_stat_user_tables_last_autovacuum: 마지막 autovacuum 시간
알람 기준:
- Dead tuple 비율 10% 이상 시 vacuum 필요
last_autovacuum이 오래됐다면 autovacuum이 제대로 동작하는지 확인
6. 락 (Locks)
데이터베이스에서 락은 동시성 제어를 위해 필수적이지만, 장시간 유지되는 락은 다른 쿼리들을 블로킹하여 전체 시스템 성능을 저하시킵니다.
PostgreSQL의 락은 여러 레벨이 있습니다:
- Row-level lock: 특정 행에 대한 락 (SELECT FOR UPDATE 등)
- Table-level lock: 테이블 전체에 대한 락 (DDL 작업 시)
- Advisory lock: 애플리케이션에서 명시적으로 사용하는 락
특히 DDL 작업(ALTER TABLE 등)은 AccessExclusiveLock을 획득하는데, 이 락이 걸리면 해당 테이블에 대한 모든 읽기/쓰기가 대기합니다.
-- 현재 락 대기 상황
SELECT
blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.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_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
WHERE NOT blocked_locks.granted;주요 지표:
pg_locks_count: 락 타입별 개수- 락 대기 시간
7. 복제 지연 (Replication Lag)
Primary-Replica 구성에서 복제 지연은 매우 중요한 지표입니다. Replica에서 읽기를 분산하는 경우, 지연이 크면 사용자가 방금 쓴 데이터를 읽지 못하는 문제가 발생합니다.
복제 지연이 발생하는 주요 원인:
- Primary의 쓰기 부하가 너무 높음
- Replica의 디스크 I/O가 느림
- 네트워크 대역폭 부족
- Replica에서 장시간 실행되는 쿼리가 WAL 적용을 블로킹
지연이 계속 증가하면 결국 Replica가 Primary를 따라잡지 못하게 됩니다.
-- Primary에서 확인
SELECT
client_addr,
state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
pg_wal_lsn_diff(sent_lsn, replay_lsn) as replication_lag_bytes
FROM pg_stat_replication;알람 기준:
- 복제 지연 1MB 이상 시 Warning
- 복제 지연 10MB 이상 시 Critical
8. WAL 생성률
WAL(Write-Ahead Log) 은 PostgreSQL의 내구성을 보장하는 핵심 메커니즘입니다. 모든 변경사항은 먼저 WAL에 기록된 후 실제 데이터 파일에 반영됩니다. 장애 발생 시 WAL을 통해 복구할 수 있습니다.
WAL 생성률이 중요한 이유:
- 디스크 공간: WAL 파일이 디스크를 채우면 DB가 멈춤
- 복제 성능: Primary가 WAL을 빠르게 생성하면 Replica가 따라가기 힘듦
- 백업 영향: WAL 아카이빙이 밀리면 PITR(Point-in-Time Recovery) 불가
쓰기 부하가 높은 시간대에 WAL 생성률이 급증하는 것은 정상이지만, 평소보다 비정상적으로 높다면 불필요한 UPDATE가 많거나 인덱스가 과도하게 많은지 점검해야 합니다.
# PromQL - WAL 생성률 (bytes/sec)
rate(pg_stat_archiver_archived_count[5m]) * avg(pg_stat_archiver_last_archived_wal)부하 상황에서의 모니터링
부하 유형별 관찰 포인트
읽기 부하 (Read-Heavy)
-- 읽기 부하 시 확인할 지표
SELECT
relname,
seq_scan, -- Sequential Scan 횟수 (인덱스 미사용)
seq_tup_read, -- Seq Scan으로 읽은 튜플 수
idx_scan, -- Index Scan 횟수
idx_tup_fetch -- Index Scan으로 가져온 튜플 수
FROM pg_stat_user_tables
ORDER BY seq_tup_read DESC
LIMIT 10;관찰 포인트:
seq_scan증가: 인덱스 누락 또는 통계 정보 오래됨- 캐시 히트율 하락: shared_buffers 부족
idle in transaction커넥션 증가: 커넥션 풀 이슈
쓰기 부하 (Write-Heavy)
-- 쓰기 부하 시 확인할 지표
SELECT
relname,
n_tup_ins,
n_tup_upd,
n_tup_del,
n_tup_hot_upd, -- HOT Update (인덱스 업데이트 없이 처리)
n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_tup_ins + n_tup_upd + n_tup_del DESC;관찰 포인트:
- WAL 생성량 급증
- Checkpoint 빈도 증가
- Dead tuple 누적 → autovacuum 부하
- 복제 지연 증가
혼합 부하
-- 락 경합 확인
SELECT
relation::regclass,
mode,
count(*)
FROM pg_locks
WHERE granted = true
GROUP BY relation, mode
ORDER BY count DESC;부하 시 병목 지점 식별
-- 현재 실행 중인 쿼리와 대기 상태
SELECT
pid,
now() - query_start as duration,
wait_event_type,
wait_event,
state,
left(query, 100) as query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;주요 wait_event:
LWLock:BufferContent: Buffer Pool 경합Lock:Relation: 테이블 락 대기IO:DataFileRead: 디스크 I/O 병목Client:ClientRead: 네트워크/클라이언트 대기
대용량 테이블 DDL 작업 모니터링
PostgreSQL은 대부분의 DDL 작업에서 AccessExclusiveLock을 획득하므로 주의가 필요합니다.
컬럼 추가 (ADD COLUMN)
-- 기본값 없는 컬럼 추가: 즉시 완료 (메타데이터만 변경)
ALTER TABLE large_table ADD COLUMN new_col VARCHAR(100);
-- 기본값이 있는 컬럼 추가 (PostgreSQL 11+): 즉시 완료
ALTER TABLE large_table ADD COLUMN new_col VARCHAR(100) DEFAULT 'value';
-- PostgreSQL 10 이하: 전체 테이블 재작성 발생!모니터링 포인트:
-- DDL 진행 상황 확인
SELECT
pid,
now() - xact_start as duration,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE query LIKE 'ALTER TABLE%';
-- 락 대기 확인
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_locks blocked_locks ON blocked.pid = blocked_locks.pid
JOIN pg_locks blocking_locks ON blocked_locks.relation = blocking_locks.relation
AND blocked_locks.pid != blocking_locks.pid
JOIN pg_stat_activity blocking ON blocking_locks.pid = blocking.pid
WHERE NOT blocked_locks.granted;인덱스 추가 (CREATE INDEX)
-- 일반 인덱스 생성: AccessExclusiveLock (쓰기 차단)
CREATE INDEX idx_col ON large_table(column);
-- CONCURRENTLY 옵션: ShareUpdateExclusiveLock (쓰기 허용)
CREATE INDEX CONCURRENTLY idx_col ON large_table(column);CONCURRENTLY 진행 상황 모니터링:
-- 인덱스 생성 진행률 (PostgreSQL 12+)
SELECT
a.pid,
p.phase,
p.blocks_total,
p.blocks_done,
round(100.0 * p.blocks_done / nullif(p.blocks_total, 0), 2) as progress_pct,
p.tuples_total,
p.tuples_done
FROM pg_stat_progress_create_index p
JOIN pg_stat_activity a ON p.pid = a.pid;Phase 설명:
initializing: 초기화waiting for writers before build: 기존 트랜잭션 완료 대기building index: 실제 인덱스 빌드 (가장 오래 걸림)waiting for writers before validation: 검증 전 대기index validation: scanning index: 인덱스 검증index validation: sorting tuples: 튜플 정렬index validation: scanning table: 테이블 스캔으로 검증waiting for old snapshots: 오래된 스냅샷 대기waiting for readers before marking dead: 완료 전 대기
DDL 작업 시 알람 설정
# prometheus-rules.yml 추가
groups:
- name: postgresql-ddl
rules:
- alert: PostgreSQLLongRunningDDL
expr: |
(time() - pg_stat_activity_query_start_time{query=~"ALTER TABLE.*|CREATE INDEX.*"}) > 300
for: 1m
labels:
severity: warning
annotations:
summary: "DDL 작업 5분 이상 진행 중"
- alert: PostgreSQLDDLBlockingQueries
expr: pg_locks_waiting > 10
for: 2m
labels:
severity: critical
annotations:
summary: "DDL로 인한 락 대기 쿼리 10개 초과"안전한 DDL 작업을 위한 팁
-- 1. 락 타임아웃 설정 (너무 오래 대기하지 않도록)
SET lock_timeout = '5s';
-- 2. 짧은 트랜잭션에서 실행
BEGIN;
SET LOCAL lock_timeout = '5s';
ALTER TABLE large_table ADD COLUMN new_col VARCHAR(100);
COMMIT;
-- 3. 현재 활성 트랜잭션 확인 후 실행
SELECT count(*) FROM pg_stat_activity
WHERE state = 'active' AND query != '<IDLE>';Grafana 대시보드 구성
추천 패널 레이아웃
┌─────────────────────────────────────────────────────────┐
│ Overview: TPS, 커넥션 사용률, 캐시 히트율 │
├─────────────────────────────────────────────────────────┤
│ Connections │ Transaction Rate │
├───────────────────────┼────────────────────────────────┤
│ Cache Hit Ratio │ Tuple Activity │
├───────────────────────┼────────────────────────────────┤
│ Dead Tuples │ Replication Lag │
├─────────────────────────────────────────────────────────┤
│ Slow Queries (Top 10) │
└─────────────────────────────────────────────────────────┘커뮤니티 대시보드
처음 시작한다면 아래 대시보드를 import해서 커스터마이징하는 것을 추천합니다:
- PostgreSQL Database (ID: 9628)
- PostgreSQL Statistics (ID: 455)
postgres_exporter 설정
# docker-compose.yml
services:
postgres_exporter:
image: prometheuscommunity/postgres-exporter
environment:
DATA_SOURCE_NAME: "postgresql://user:pass@postgres:5432/mydb?sslmode=disable"
ports:
- "9187:9187"커스텀 쿼리 추가
queries.yaml 파일로 추가 메트릭을 수집할 수 있습니다:
pg_slow_queries:
query: |
SELECT count(*) as count
FROM pg_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '5 seconds'
metrics:
- count:
usage: "GAUGE"
description: "Number of slow queries (>5s)"알람 규칙 예시
# prometheus-rules.yml
groups:
- name: postgresql
rules:
- alert: PostgreSQLHighConnections
expr: sum(pg_stat_activity_count) / pg_settings_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL 커넥션 사용률 80% 초과"
- alert: PostgreSQLLowCacheHitRatio
expr: pg_stat_database_blks_hit / (pg_stat_database_blks_hit + pg_stat_database_blks_read) < 0.95
for: 10m
labels:
severity: warning
annotations:
summary: "PostgreSQL 캐시 히트율 95% 미만"
- alert: PostgreSQLReplicationLag
expr: pg_replication_lag_seconds > 30
for: 1m
labels:
severity: critical
annotations:
summary: "PostgreSQL 복제 지연 30초 초과"정리
PostgreSQL 모니터링의 핵심 포인트:
- 커넥션: max_connections 대비 사용률 추적
- 캐시: 95% 이상의 캐시 히트율 유지
- Vacuum: Dead tuple 비율과 autovacuum 동작 확인
- 복제: Replica 구성 시 지연 시간 모니터링
- 쿼리: 슬로우 쿼리 탐지 및 분석
다음 글에서는 PostgreSQL 트러블슈팅에서 실무 문제 해결 방법과 AWS Aurora 환경을 다룹니다.