DBA 실무/PostgreSQL

[PostgreSQL 시리즈 12편] 새벽 3시에 걸려오는 전화의 원인들 - PostgreSQL 운영 장애 TOP 10 (그림 3개)

isony 2026. 9. 5. 13:20
반응형

새벽 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을 정리하면:

  1. 연결 부족 — PgBouncer가 답
  2. 느린 쿼리 / 통계 — ANALYZE
  3. 락 / 긴 트랜잭션 — 3대 타임아웃
  4. 디스크 / bloat — VACUUM, 모니터링
  5. 인덱스 / 페이징 / DDL — 설계와 습관

진단 3대 도구(pg_stat_activity, pg_stat_statements, pg_locks)만 손에 익으면, 새벽 3시 전화도 침착하게 대응할 수 있습니다. 그리고 대부분은 예방 체크리스트로 미리 막을 수 있고요.

Oracle에서 PostgreSQL로 넘어오면 장애의 결이 조금 다르지만, "증상 → 진단 → 원인 → 해결 → 재발방지"라는 대응 프레임은 똑같습니다. 15년 운영하며 배운 그 프레임이 여기서도 통합니다.

다음 편부터는 드디어 백업과 복구입니다. 본인이 요청하신 Full/증분 백업을 두 편에 걸쳐 제대로 다룹니다. 13편은 논리 백업(pg_dump), 14편은 물리 백업과 PITR입니다.

여러분이 겪은 아찔한 PostgreSQL 장애나, 유용한 진단 쿼리가 있다면 댓글로 공유해주세요. 다른 운영자들에게 큰 도움이 됩니다.

 

반응형