PostgreSQL 모니터링 - 핵심 지표와 대시보드 구성

2025년 12월 05일

monitoring

# PostgreSQL# Monitoring# Grafana# Prometheus

들어가며

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 * 100

2. 트랜잭션 처리량 (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 설명:

  1. initializing: 초기화
  2. waiting for writers before build: 기존 트랜잭션 완료 대기
  3. building index: 실제 인덱스 빌드 (가장 오래 걸림)
  4. waiting for writers before validation: 검증 전 대기
  5. index validation: scanning index: 인덱스 검증
  6. index validation: sorting tuples: 튜플 정렬
  7. index validation: scanning table: 테이블 스캔으로 검증
  8. waiting for old snapshots: 오래된 스냅샷 대기
  9. 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해서 커스터마이징하는 것을 추천합니다:

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 모니터링의 핵심 포인트:

  1. 커넥션: max_connections 대비 사용률 추적
  2. 캐시: 95% 이상의 캐시 히트율 유지
  3. Vacuum: Dead tuple 비율과 autovacuum 동작 확인
  4. 복제: Replica 구성 시 지연 시간 모니터링
  5. 쿼리: 슬로우 쿼리 탐지 및 분석

다음 글에서는 PostgreSQL 트러블슈팅에서 실무 문제 해결 방법과 AWS Aurora 환경을 다룹니다.


참고 자료

© 2025, 미나리와 함께 만들었음