새벽 3시에 걸려오는 전화의 원인들 - PostgreSQL 운영 장애 TOP 10 (그림 3개)
테스트 환경: PostgreSQL 17, Rocky Linux 9 / Oracle 19c
DBA에게 가장 무서운 건 새벽 3시의 전화입니다. "DB가 이상해요", "서비스가 느려요", "접속이 안 돼요."
Oracle이든 PostgreSQL이든 장애는 옵니다. 그런데 PostgreSQL은 Oracle과 자주 터지는 지점이 조금 다릅니다. MVCC, autovacuum, 연결 모델 같은 PostgreSQL 특유의 구조에서 오는 문제들이 있거든요.
이번 편은 운영 시리즈의 세 번째 글입니다. 15년 넘게 운영하며 만난, 그리고 PostgreSQL 커뮤니티가 공통으로 꼽는 운영 장애 TOP 10을 정리했습니다. 각 문제의 증상 → 진단 → 해결을 실전 쿼리와 함께 담았습니다. 그림 3개로 핵심을 잡습니다.
각 항목은 독립적이니, 지금 겪는 문제부터 바로 보셔도 됩니다.
먼저: 전체 지도와 진단 3대 도구
10가지를 먼저 한눈에 보고, 공통으로 쓰는 진단 도구 3개를 짚고 갑니다.

진단 3대 도구부터 외우세요. 이 3개면 대부분의 문제를 잡습니다.
-- ① pg_stat_activity: 지금 누가 뭘 하는지
SELECT pid, usename, state, query, now()-query_start AS duration
FROM pg_stat_activity WHERE state != 'idle';
-- ② pg_stat_statements: 어떤 쿼리가 느린지 (확장 필요)
SELECT query, calls, mean_exec_time
FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;
-- ③ pg_locks: 누가 누구를 막고 있는지
SELECT mode, count(*) FROM pg_locks WHERE NOT granted GROUP BY mode;
Oracle의 v$session, v$sql, v$lock에 해당한다고 보시면 됩니다. 이제 하나씩 볼까요.
1. 연결 부족 (too many connections)
가장 흔한 PostgreSQL 장애 1위입니다. Oracle에서는 잘 안 겪는데, PostgreSQL의 연결=프로세스 구조 때문에 자주 터집니다.
증상: FATAL: sorry, too many clients already / 새 접속이 거부됨
원인: 1편에서 봤듯 PostgreSQL은 연결 하나당 프로세스 하나(2~5MB 메모리). 연결이 수백 개면 메모리가 폭발합니다.

진단:
SELECT count(*) FROM pg_stat_activity;
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
해결:
1. max_connections는 200 이하로 유지 (무작정 늘리면 역효과!)
2. PgBouncer로 연결 풀링 → 앱 연결 200개를 실제 DB 25개로
3. 애플리케이션의 연결 누수 점검
핵심: max_connections를 500, 1000으로 올리는 건 답이 아닙니다. PgBouncer 같은 연결 풀러가 정답입니다.
2. 갑자기 느려진 쿼리
어제까지 멀쩡하던 쿼리가 오늘 갑자기 느려집니다.
증상: 특정 쿼리가 갑자기 수십 배 느려짐
원인: 대부분 통계 정보가 낡아서 옵티마이저가 잘못된 계획을 세운 것입니다. autovacuum이 뒤처졌을 때 자주 발생합니다.
진단:
-- EXPLAIN ANALYZE로 실행계획 확인 (10편 참고)
EXPLAIN ANALYZE SELECT ...;
-- 추정 rows와 실제 rows가 크게 다르면 통계 문제!
해결:
ANALYZE 테이블명; -- 통계 갱신
통계만 갱신해도 대부분 해결됩니다. 근본적으로는 autovacuum 튜닝(11편)이 필요합니다.
3. 락 대기 / 데드락
쿼리가 멈춘 듯 안 끝납니다. 누군가 락을 잡고 안 놔주는 거죠.
증상: 쿼리가 hang, deadlock detected 에러
진단:
-- 누가 누구를 막고 있나 (블로킹 트리)
SELECT blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
blocked.query AS blocked_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));
해결:
-- 급하면 문제 세션 종료
SELECT pg_terminate_backend(문제_pid);
예방이 최선: 아래 3대 타임아웃을 걸어두면 락 폭주를 막습니다.

ALTER ROLE app_user SET statement_timeout = '30s';
ALTER ROLE app_user SET lock_timeout = '5s';
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '5min';
이 3개는 운영 DB의 필수 설정입니다. "빨리 실패"하게 만들어서 장애가 전체로 퍼지는 걸 막습니다.
4. 디스크 부족 (disk full)
가장 무서운 장애입니다. 디스크가 꽉 차면 DB가 쓰기를 멈춥니다.
증상: could not write to file, DB 쓰기 중단
원인: PostgreSQL은 세 곳에서 디스크를 씁니다.
1. WAL 파일 (트랜잭션 로그)
2. 임시 파일 (큰 정렬/조인)
3. 테이블 bloat (dead tuple - 11편)
진단:
-- DB별 크기
SELECT datname, pg_size_pretty(pg_database_size(datname))
FROM pg_database ORDER BY pg_database_size(datname) DESC;
-- 큰 테이블 (bloat 의심)
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;
해결:
1. bloat이면 → VACUUM (11편)
2. WAL이 쌓였으면 → 아카이빙/복제 슬롯 점검
3. 임시 파일이면 → work_mem 조정, 쿼리 최적화
리눅스 디스크 관리는 리눅스 디스크 부족 글도 참고하세요.
5. 통계 부정확 (계획 오류)
2번과 연결되지만 별도로 다룰 만합니다. 통계가 틀리면 옵티마이저가 계속 헛발질합니다.
해결:
ANALYZE; -- 전체 통계 갱신
-- 특정 컬럼 통계 정밀도 높이기
ALTER TABLE t ALTER COLUMN c SET STATISTICS 1000;
대량 데이터 적재 후에는 반드시 ANALYZE를 돌리세요. autovacuum이 처리하기 전까지 통계가 낡은 상태입니다.
6. 테이블 bloat
11편에서 다룬 그 문제입니다. dead tuple이 쌓여 테이블이 부풀고 느려집니다.
진단:
SELECT relname, n_dead_tup,
ROUND(n_dead_tup::numeric/NULLIF(n_live_tup,0)*100,1) AS dead_pct
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;
해결: VACUUM, autovacuum 튜닝, 심하면 pg_repack. 자세한 건 11편 참고.
7. 긴 트랜잭션 방치
열어놓고 잊어버린 트랜잭션이 온갖 문제를 일으킵니다.
증상: VACUUM이 안 먹힘, 락이 안 풀림, bloat 증가
원인: 긴 트랜잭션은 옛 행 버전을 계속 붙잡아서 VACUUM이 dead tuple을 못 치웁니다.
진단:
-- 오래 열린 트랜잭션 찾기
SELECT pid, now()-xact_start AS duration, state, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start LIMIT 10;
해결: idle_in_transaction_session_timeout 설정(3번 참고). 애플리케이션의 트랜잭션 관리 점검.
8. 인덱스 미사용
인덱스를 만들었는데 옵티마이저가 안 씁니다. 또는 인덱스가 없어서 풀스캔합니다.
증상: Seq Scan on 대용량테이블, Rows Removed by Filter 폭증 (10편)
진단:
-- 안 쓰이는 인덱스 찾기
SELECT relname, indexrelname, idx_scan
FROM pg_stat_user_indexes WHERE idx_scan = 0;
해결:
1. 필요한 인덱스 추가 (6편 참고)
2. 통계 갱신 후 재확인 (안 쓰던 인덱스를 쓸 수도)
3. 함수 조건이면 표현식 인덱스 (6편)
PostgreSQL 성능 문제의 80%가 인덱스 문제라는 통계도 있습니다.
9. OFFSET 페이징 지옥
의외로 많이 놓치는 문제입니다. 깊은 페이지네이션이 점점 느려집니다.
증상: LIMIT 20 OFFSET 100000이 갈수록 느려짐
원인: OFFSET은 건너뛸 행도 다 읽고 버립니다. 10만 번째 페이지면 10만 행을 읽고 버리는 거죠.
해결: keyset pagination(seek 방식)
-- 느림: OFFSET (10만 행 읽고 버림)
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 100000;
-- 빠름: keyset (직전 페이지 마지막 id부터)
SELECT * FROM products WHERE id > 100000 ORDER BY id LIMIT 20;
무한 스크롤이나 깊은 페이징이 있다면 keyset 방식을 쓰세요.
10. 피크 시간 DDL 실행
가장 어이없게 서비스를 멈추는 실수입니다.
증상: 피크 시간에 갑자기 API가 30초간 멈춤
원인: ALTER TABLE, CREATE INDEX, REINDEX를 CONCURRENTLY 없이 실행하면 테이블 전체를 잠급니다.
해결:
-- 위험: 테이블 잠금
CREATE INDEX idx_x ON big_table(col);
-- 안전: 온라인 (락 최소화)
CREATE INDEX CONCURRENTLY idx_x ON big_table(col);
REINDEX INDEX CONCURRENTLY idx_x;
철칙: 운영 DB의 DDL은 반드시 CONCURRENTLY 를 붙이거나 유지보수 시간에. 안 그러면 11편의 VACUUM FULL처럼 서비스를 멈춥니다.
★ AI로 장애 대응하기
장애 상황에서 Claude를 활용하면 진단이 빨라집니다. 새벽 3시엔 특히요.
프롬프트:
"PostgreSQL 17 운영 중 장애야.
pg_stat_activity 결과: [붙여넣기]
증상: [느려짐/멈춤/에러 메시지]
1. 지금 무슨 일이 일어나는지
2. 즉시 조치 (응급 처치)
3. 근본 원인과 재발 방지
를 Oracle DBA가 이해하기 쉽게, 우선순위 순으로 알려줘."
⚠️ 주의: 장애 상황에서 AI가 준 명령(특히 pg_terminate_backend, DDL)은 영향 범위를 확인하고 실행하세요. 급할수록 침착하게. 운영 DB에서 세션을 죽이면 롤백이 발생할 수 있습니다.
장애 예방 체크리스트
터지기 전에 막는 게 최선입니다. 이것만 해두세요.
[ ] PgBouncer 연결 풀링 (연결 부족 예방)
[ ] 3대 타임아웃 설정 (statement/lock/idle_in_transaction)
[ ] autovacuum 튜닝 (bloat/wraparound 예방, 11편)
[ ] pg_stat_statements 확장 활성화 (느린 쿼리 추적)
[ ] 디스크/연결/XID 나이 모니터링 + 알림
[ ] DDL은 CONCURRENTLY 또는 유지보수 시간
[ ] 정기 ANALYZE (통계 최신 유지)
마무리
PostgreSQL 운영 장애 TOP 10을 정리하면:
- 연결 부족 — PgBouncer가 답
- 느린 쿼리 / 통계 — ANALYZE
- 락 / 긴 트랜잭션 — 3대 타임아웃
- 디스크 / bloat — VACUUM, 모니터링
- 인덱스 / 페이징 / DDL — 설계와 습관
진단 3대 도구(pg_stat_activity, pg_stat_statements, pg_locks)만 손에 익으면, 새벽 3시 전화도 침착하게 대응할 수 있습니다. 그리고 대부분은 예방 체크리스트로 미리 막을 수 있고요.
Oracle에서 PostgreSQL로 넘어오면 장애의 결이 조금 다르지만, "증상 → 진단 → 원인 → 해결 → 재발방지"라는 대응 프레임은 똑같습니다. 15년 운영하며 배운 그 프레임이 여기서도 통합니다.
다음 편부터는 드디어 백업과 복구입니다. 본인이 요청하신 Full/증분 백업을 두 편에 걸쳐 제대로 다룹니다. 13편은 논리 백업(pg_dump), 14편은 물리 백업과 PITR입니다.
여러분이 겪은 아찔한 PostgreSQL 장애나, 유용한 진단 쿼리가 있다면 댓글로 공유해주세요. 다른 운영자들에게 큰 도움이 됩니다.
'DBA 실무 > PostgreSQL' 카테고리의 다른 글
| autovacuum 방치했다가 DB 멈춘 썰 (PostgreSQL VACUUM 완벽 정리, 그림 5개) (0) | 2026.09.04 |
|---|---|
| [PostgreSQL 시리즈 10편] Oracle 권한 관리 그대로 했다가 막힌 썰 (PostgreSQL ROLE 완벽 정리) (0) | 2026.09.02 |
| Oracle 사용자가 PostgreSQL 보고 배 아파하는 기능 6가지 (PostgreSQL 킬러 기능 총정리) (0) | 2026.08.31 |
| PL/SQL 그대로 옮겼다가 다 터진 썰 (Oracle DBA를 위한 PL/pgSQL 완벽 비교) (0) | 2026.08.28 |
| B-tree만 알던 내가 GIN 인덱스 보고 눈 돌아간 썰 (PostgreSQL 인덱스 완벽 정리) (0) | 2026.08.26 |