목요일

PostgreSQL Connection Pool 고갈 해결하기

🔍 검색 키워드: PostgreSQL connection pool 에러, 커넥션 풀 소진, 데이터베이스 연결 오류, 최대 연결 초과

PostgreSQL Connection Pool 고갈 해결하기

증상

애플리케이션이 실행되다가 갑자기 다음과 같은 에러가 발생한다:

FATAL: sorry, too many clients already

또는:

Error: connect ECONNREFUSED - PostgreSQL connection failed

또는 connection pool 라이브러리에서:

Error: timeout acquiring a connection from the pool
  Error: Client has already been released to the pool

특히 트래픽이 많아지거나 장시간 실행되는 배치 작업 후에 자주 발생한다. 새로운 요청이 들어와도 DB 연결을 못 하고 응답 불가 상태가 된다.

원인

PostgreSQL은 최대 동시 연결 수 제한이 있고 (기본값 100), connection pool은 재사용 가능한 연결 수를 미리 정해둔다. 고갈 현상은 다음 경우에 발생:

  1. 연결을 반환 안 함: 쿼리 후 연결을 pool에 반환하지 않아 계속 증가
  2. 타임아웃 후 좀비 연결: 응답 없는 연결이 pool에 남아있음
  3. Pool 사이즈 설정 과소: 동시 사용자/요청량에 비해 pool이 너무 작음
  4. 쿼리 오래 걸림: 느린 쿼리가 연결을 오래 점유
  5. Connection leak: 예외 발생 시 연결을 close하지 않는 코드

해결방법

1. Connection Pool 설정 최적화

Node.js + pg (node-postgres):

const { Pool } = require('pg');
  
  const pool = new Pool({
    host: 'localhost',
      port: 5432,
        database: 'mydb',
          user: 'postgres',
            password: 'password',
              max: 20,                    // 최대 연결 수 (기본 10)
                idleTimeoutMillis: 30000,   // 30초 idle 후 닫기
                  connectionTimeoutMillis: 2000, // 연결 생성 타임아웃
                  });
                  
                  module.exports = pool;

Python + psycopg2:

import psycopg2.pool
  
  connection_pool = psycopg2.pool.SimpleConnectionPool(
      1,      // minimum connections
          20,     // maximum connections
              database="mydb",
                  user="postgres",
                      password="password",
                          host="localhost"
                          )
                          
                          // 사용
                          conn = connection_pool.getconn()
                          try:
                              // 쿼리 실행
                                  pass
                                  finally:
                                      connection_pool.putconn(conn)

Java + HikariCP:

HikariConfig config = new HikariConfig();
  config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
  config.setUsername("postgres");
  config.setPassword("password");
  config.setMaximumPoolSize(20);          // 최대 연결
  config.setMinimumIdle(5);               // 최소 유휴 연결
  config.setConnectionTimeout(2000);       // 2초 타임아웃
  config.setIdleTimeout(600000);          // 10분 idle 타임아웃
  config.setMaxLifetime(1800000);         // 30분 최대 수명
  
  HikariDataSource dataSource = new HikariDataSource(config);

2. 연결이 제대로 반환되는지 확인

Node.js에서 연결 누수 확인:

const pool = new Pool(config);
  
  pool.on('error', (err, client) => {
    console.error('Unexpected error on idle client', err);
      process.exit(-1);
      });
      
      pool.on('connect', () => {
        console.log('New connection created');
        });
        
        // 항상 try-finally로 연결 반환 보장
        const client = await pool.connect();
        try {
          const result = await client.query('SELECT * FROM users WHERE id = $1', [1]);
            return result.rows;
            } finally {
              client.release();  // 반드시 실행
              }

또는 with 문 활용 (Python):

from contextlib import contextmanager
  
  @contextmanager
  def get_db_connection():
      conn = connection_pool.getconn()
          try:
                  yield conn
                      finally:
                              connection_pool.putconn(conn)
                              
                              // 사용
                              with get_db_connection() as conn:
                                  cursor = conn.cursor()
                                      cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
                                          // 예외 발생해도 자동으로 반환됨

3. 느린 쿼리 최적화

-- 인덱스 추가
  CREATE INDEX idx_users_id ON users(id);
  
  -- 실행계획 확인
  EXPLAIN ANALYZE SELECT * FROM users WHERE status = 'active';
  
  -- 필요 없는 조인 제거, WHERE 조건 최적화
  SELECT u.id, u.name
  FROM users u
  WHERE u.created_at > NOW() - INTERVAL '7 days'
    AND u.status = 'active'
    LIMIT 100;

4. PostgreSQL 서버 설정 확인

# PostgreSQL 최대 연결 수 조회
  psql -U postgres -c "SHOW max_connections;"
  
  # 현재 연결 수 조회
  psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"
  
  # 연결 상세 정보
  psql -U postgres -c "SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;"

필요하면 postgresql.conf에서:

max_connections = 200   # 기본값 100에서 증가

5. 좀비 연결 정리

-- 유휴 연결 종료
  SELECT pg_terminate_backend(pid)
  FROM pg_stat_activity
  WHERE datname = 'mydb'
    AND state = 'idle'
      AND query_start < now() - interval '30 minutes';
      
      -- 슬로우 쿼리 강제 종료 (주의)
      SELECT pg_terminate_backend(pid)
      FROM pg_stat_activity
      WHERE query_start < now() - interval '5 minutes'
        AND state != 'idle';

정리표

상황 원인 해결법
연결 고갈 후 다시 증가 연결을 close하지 않음 try-finally 또는 context manager로 보장
Pool 타임아웃 pool이 너무 작음 max 크기 증가, idleTimeoutMillis 조정
"too many clients" 에러 PostgreSQL 최대 연결 도달 max_connections 증가 또는 app pool 감소
간헐적 연결 오류 connectionTimeoutMillis 너무 짧음 타임아웃 값 증가 또는 네트워크 확인
메모리 누수 좀비 연결이 메모리 점유 주기적으로 idle 연결 정리 (pg_terminate_backend)

TIP: 프로덕션에서는 반드시 connection pool 크기, 타임아웃, idle 설정을 검토하고, 정기적으로 pg_stat_activity로 모니터링하자.

댓글 없음:

댓글 쓰기