사전 커넥션 풀
패턴

커넥션 풀

gabury1

연결을 미리 만들어 두고 빌려주고 돌려받는 방식입니다. 쓸 때마다 새로 연결하지 않고 이미 열려 있는 것을 꺼내 씁니다. 다 쓴 연결은 닫지 않고 풀로 돌려보냅니다. 같은 연결이 여러 요청을 옮겨 다니며 다시 쓰입니다.

쉽고 빠른 이해

연결을 미리 여러 개 만들어 두고 필요할 때 빌려줬다가 다 쓰면 돌려받는 물건입니다. 예를 들어 웹 서버가 요청을 처리할 때마다 데이터베이스에 새로 접속하지 않고, 이미 열려 있는 접속 하나를 꺼내 씁니다.

접속을 새로 맺는 일은 여러 단계를 거쳐 시간이 걸립니다. 그 시간을 요청마다 다시 치르지 않으려고 씁니다.

어떻게 돕나:

  1. 요청이 오면 놀고 있는 연결이 있는지 먼저 봅니다
  2. 있으면 그것을 내주고, 없으면 정해 둔 상한 안에서 새로 만들어 내줍니다
  3. 다 쓴 연결은 끊지 않고 풀로 돌려보내 다음 요청이 다시 씁니다

대가도 있습니다. 앞서 쓰던 사람이 남긴 상태가 다음 사람에게 넘어갈 수 있고, 놀고 있는 연결도 서버 쪽 자리를 계속 차지합니다.

상세

데이터베이스 서버에 연결하는 일은 대개 시간이 걸리는 여러 단계로 이뤄집니다. Microsoft 의 ADO.NET(ActiveX Data Objects .NET) 문서는 그 단계를 이렇게 나열합니다. 소켓이나 명명 파이프 같은 물리 채널을 세웁니다. 서버와 처음 핸드셰이크를 합니다. 연결 문자열 정보를 파싱합니다. 서버가 연결을 인증합니다. 현재 트랜잭션에 참여할지 검사합니다. 다섯 단계는 이 순서로 쌓입니다.

flowchart TD
    A[물리 채널 수립] --> B[핸드셰이크]
    B --> C[연결 문자열 파싱]
    C --> D[서버 인증]
    D --> E[트랜잭션 참여 검사]

같은 문서는 실제로 대부분의 애플리케이션이 한두 가지 정도의 몇 안 되는 설정만 써서 연결한다고 적습니다. 그래서 실행 중에 똑같은 연결이 반복해서 열리고 닫힙니다.

커넥션 풀은 그 반복을 없애기로 한 결정입니다. ADO.NET 문서는 풀러(연결 풀을 관리하는 주체)가 물리 연결의 소유권을 갖는다고 적습니다. 설정이 같은 연결들을 살려 둔 채로 관리합니다. 애플리케이션이 연결에 Open 을 부르면 풀러가 풀에서 쓸 수 있는 연결(유휴 연결)을 찾습니다. 풀에 연결이 있으면 새로 열지 않고 그것을 돌려줍니다. Close 를 부르면 풀러는 연결을 닫지 않고 활성 연결 묶음으로 돌려보냅니다. 돌아온 연결은 다음 Open 호출에서 다시 쓰입니다. 최대 풀 크기에 닿았고 쓸 수 있는 연결이 없으면, 풀러는 요청을 곧바로 실패시키지 않고 큐에 넣어 기다리게 합니다.

sequenceDiagram
    participant 앱
    participant 풀
    participant 서버
    앱->>풀: 연결 요청
    alt 유휴 연결 있음
        풀-->>앱: 열려 있던 연결
    else 상한에 안 닿음
        풀->>서버: 새 연결 수립
        서버-->>풀: 연결
        풀-->>앱: 연결
    else 상한에 닿음
        풀-->>앱: 큐에 넣고 대기
    end
    앱->>풀: 반납
    Note over 풀: 닫지 않고 보관

풀은 연결의 총 개수를 정하는 자리이기도 합니다. SQLAlchemy 공식 문서는 커넥션 풀을 오래 사는 연결을 메모리에 유지해 효율적으로 재사용하는 표준 기법이라고 적습니다. 동시에 애플리케이션이 한꺼번에 쓸 수 있는 연결의 총 개수를 관리하는 수단이라고도 적습니다. 특히 서버 사이드 웹 애플리케이션에서는 요청들 사이에 재사용되는 활성 데이터베이스 연결의 풀을 메모리에 유지하는 표준적인 방법이라고 적습니다.

풀이 사는 자리는 두 갈래입니다. 하나는 애플리케이션 프로세스 안입니다. SQLAlchemy 의 QueuePool 이나 자바의 HikariCP 처럼 라이브러리가 풀을 들고 있습니다. 다른 하나는 애플리케이션과 데이터베이스 사이에 따로 서는 프로세스입니다. PgBouncer 가 그 자리에서 클라이언트 연결을 받고 서버 연결을 따로 관리합니다. 그림으로 보면 이렇습니다.

architecture-beta
    group g1(cloud)[갈래 1 — 라이브러리 내장]
    service app1(server)[애플리케이션 풀 내장] in g1
    service db1(database)[데이터베이스 서버] in g1
    app1:R -- L:db1

    group g2(cloud)[갈래 2 — 앞단 프록시]
    service app2(server)[애플리케이션] in g2
    service proxy(server)[PgBouncer] in g2
    service db2(database)[데이터베이스 서버] in g2
    app2:R -- L:proxy
    proxy:R -- L:db2

갈래 1 은 애플리케이션과 데이터베이스 서버 사이에 노드가 하나뿐입니다 — 풀이 애플리케이션 프로세스 안에 있어서 따로 세지 않습니다. 갈래 2 는 애플리케이션과 데이터베이스 서버 사이에 PgBouncer 라는 별도 프로세스가 하나 더 낍니다.

대가

연결을 재사용한다는 것은 앞서 쓰던 것을 물려받는다는 뜻입니다. 연결에 남은 세션 상태가 다음 사용자에게 따라갑니다.

SQLAlchemy 는 그걸 되돌리는 동작을 풀에 넣어 뒀습니다. 문서는 풀이 반납 시 재설정 동작을 포함한다고 적습니다. 연결이 풀로 돌아올 때 DBAPI(Python Database API) 연결의 rollback() 을 부릅니다. 커밋되지 않은 데이터뿐 아니라 테이블 잠금과 행 잠금까지 연결에서 걷어내려는 것입니다.

롤백으로 안 걷히는 상태도 있습니다. Microsoft 문서는 sp_setapprole 시스템 저장 프로시저로 SQL Server(이름의 SQL 은 Structured Query Language, 구조화 질의 언어의 줄임말입니다) 애플리케이션 역할을 활성화하고 나면 그 연결의 보안 컨텍스트를 재설정할 수 없다고 적습니다. 풀링이 켜져 있으면 그 연결은 그대로 풀로 돌아가고, 풀에서 다시 꺼내 쓸 때 오류가 납니다.

앞단 프록시로 푸는 쪽은 이 문제를 설정으로 드러내 놓았습니다. PgBouncer 의 pool_mode 는 서버 연결을 언제 다른 클라이언트가 재사용할 수 있는지 정합니다. session 은 클라이언트가 끊어진 뒤에 서버를 풀로 돌려보냅니다. 기본값입니다. transaction 은 트랜잭션이 끝나면 돌려보냅니다. statement 는 질의가 끝나면 돌려보내고, 이 모드에서는 여러 문장에 걸친 트랜잭션이 금지됩니다.

sequenceDiagram
    participant 클라이언트
    participant PgBouncer
    participant 서버
    클라이언트->>PgBouncer: 질의 (트랜잭션 시작)
    PgBouncer->>서버: 같은 서버 연결로 전달
    alt session (기본값)
        클라이언트->>PgBouncer: 다음 질의 (같은 접속, 새 트랜잭션)
        PgBouncer->>서버: 같은 서버 연결 계속 사용
        클라이언트->>PgBouncer: 접속 종료
        PgBouncer-->>서버: 이제야 반납
    else transaction
        PgBouncer-->>서버: 트랜잭션이 끝나는 순간 반납
        클라이언트->>PgBouncer: 다음 트랜잭션
    else statement
        PgBouncer-->>서버: 질의 하나가 끝나는 순간 반납
        Note over 클라이언트,서버: 여러 문장짜리 트랜잭션 금지
    end

연결을 일찍 돌려보낼수록 세션에 묶인 기능을 잃습니다. PgBouncer 문서는 트랜잭션 풀링이 설계상 서버에 대한 클라이언트의 기대를 깬다고 적습니다. 아래는 그 문서가 붙여 둔 호환 표입니다.

기능 세션 풀링 트랜잭션 풀링
시작 파라미터 Yes Yes
SET/RESET Yes Never
LISTEN Yes Never
NOTIFY Yes Yes
WITHOUT HOLD 커서 Yes Yes
WITH HOLD 커서(트랜잭션이 끝나도 유지되는 커서) Yes Never
프로토콜 수준 prepared plan(매번 새로 준비하지 않고 재사용하는 실행 계획) Yes Yes
PREPARE / DEALLOCATE Yes Never
ON COMMIT DROP 임시 테이블 Yes Yes
PRESERVE/DELETE ROWS 임시 테이블 Yes Never
캐시된 plan 재설정(전에 준비해 둔 실행 계획을 지우고 다시 세우는 것) Yes Yes
LOAD 문 Yes Never
세션 수준 advisory lock(접속이 끊기면 풀리는, 세션에 묶인 임의 잠금) Yes Never

유휴 연결이 서버 쪽 자리를 계속 차지합니다. SQLAlchemy 문서는 코드가 데이터베이스와의 대화를 끝낸 것처럼 보여도 많은 경우 애플리케이션이 일정한 수의 데이터베이스 연결을 계속 유지한다고 적습니다. 애플리케이션이 끝나거나 풀을 명시적으로 폐기할 때까지 그렇습니다. 그 연결들은 서버 쪽에서 슬롯을 잡고 있습니다. PostgreSQL 문서는 max_connections 값에 따라 특정 자원의 크기가 직접 정해진다고 적습니다. 값을 올리면 공유 메모리를 비롯한 그 자원의 할당이 늘어납니다.

예시

SQLAlchemy 의 QueuePool

pool_size    = 5
max_overflow = 10
timeout      = 30.0

pool_size 는 풀에 계속 유지할 연결의 개수입니다. 문서는 풀이 연결 0개로 시작한다고 적습니다. 연결이 pool_size 만큼 요청되고 나면 그 개수만큼 풀에 남아 있습니다. max_overflow 는 그 위로 더 만들 수 있는 연결의 상한입니다. 빌려 간 연결이 pool_size 에 닿으면 이 상한까지 연결을 더 내줍니다. 그렇게 더 만든 연결은 풀로 돌아올 때 끊고 버립니다. 그래서 동시에 열리는 연결의 총합은 pool_size + max_overflow 이고, 유휴 연결의 총합은 pool_size 입니다. timeout 은 연결을 돌려받기를 포기하기까지 기다릴 초입니다.

HikariCP

maximumPoolSize   = 10
connectionTimeout = 30000
maxLifetime       = 1800000

maximumPoolSize 는 풀이 커질 수 있는 최대 크기입니다. 유휴 연결과 사용 중 연결을 함께 셉니다. 문서는 이 값이 데이터베이스 백엔드로 가는 실제 연결의 최대 개수를 결정한다고 적습니다. connectionTimeout 은 클라이언트가 풀에서 연결을 기다릴 최대 밀리초입니다. 허용되는 가장 작은 값은 250ms 입니다. maxLifetime 은 풀 안 연결의 최대 수명이고 기본값은 30분입니다. 허용되는 가장 작은 값은 30000ms 입니다. 0 을 넣으면 수명이 무한이 되고, 이것은 idleTimeout 설정의 영향을 받습니다.

PgBouncer 의 pgbouncer.ini

pool_mode         = session
default_pool_size = 20
max_client_conn   = 100

라이브러리가 아니라 별도 프로세스가 드는 풀입니다. default_pool_size 는 사용자와 데이터베이스 짝마다 허용하는 서버 연결의 최대 개수입니다. 데이터베이스별 설정과 사용자별 설정의 pool_size 로 덮어쓸 수 있고, 그 값이 없을 때 이 값이 쓰입니다. max_client_conn 은 허용하는 클라이언트 연결의 최대 개수입니다. 위 세 값은 모두 문서에 적힌 기본값입니다.

ADO.NET

연결 문자열마다 풀이 하나씩 만들어집니다. 풀이 만들어질 때 최소 풀 크기 요구를 채우도록 연결 객체가 여러 개 만들어져 풀에 들어갑니다. 그 뒤로는 필요에 따라 지정된 최대 풀 크기까지 연결이 풀에 더해집니다. Microsoft 문서는 최대 풀 크기의 기본값이 100 이라고 적습니다. 연결은 닫히거나 폐기될 때 풀로 반환됩니다.

실패

풀이 다 차면 요청은 곧바로 실패하지 않고 먼저 기다립니다. 기다림이 끝나는 방식이 제품마다 적혀 있습니다.

조건 그때 벌어지는 일
HikariCP 에서 풀이 최대 크기에 닿았고 유휴 연결이 없다 getConnection() 호출이 connectionTimeout 밀리초까지 블록합니다. 그 시간이 지나도 연결을 못 받으면 SQLException 이 던져집니다
ADO.NET 에서 최대 풀 크기에 닿았고 쓸 수 있는 연결이 없다 요청이 큐에 들어갑니다. 풀러가 타임아웃에 닿을 때까지 연결 회수를 시도합니다. 기본은 15초입니다. 그 안에 요청을 못 채우면 예외가 던져집니다
SQLAlchemy 에서 pool_size 와 max_overflow 를 다 쓰고 timeout 이 지난다 QueuePool limit of size <x> overflow <y> reached, connection timed out, timeout <z> 가 나옵니다
sp_setapprole 로 역할을 활성화한 연결이 풀로 돌아갔다가 다시 쓰인다 보안 컨텍스트가 재설정되지 않은 채 재사용되어 오류가 납니다

SQLAlchemy 문서는 QueuePool limit 이 아마 가장 흔하게 겪는 런타임 오류일 것이라고 적습니다. 애플리케이션의 작업량이 설정된 상한을 넘어서는 일이라 거의 모든 SQLAlchemy 애플리케이션에 해당한다고 적습니다.

풀은 이미 끊긴 연결을 그대로 들고 있을 수도 있습니다. 빌려 갈 때 살아 있는지 확인하는 쪽을 SQLAlchemy 문서는 비관적 접근이라고 부릅니다. 풀에서 연결을 꺼낼 때마다 시험용 문장을 보내 연결이 아직 쓸 만한지 확인하는 방식입니다. create_engine() 의 pool_pre_ping 인자로 켭니다.

오래된 연결을 나이로 자르는 손잡이도 있습니다. 풀 재활용 파라미터는 일정 나이를 넘긴 연결을 풀이 쓰지 못하게 막습니다. 문서의 예에서는 열린 지 한 시간이 넘은 DBAPI 연결이 다음에 빌려 갈 때 무효화되고 교체됩니다. 무효화는 빌려 가는 시점에만 일어납니다. 이미 빌려 나간 상태로 잡고 있는 연결에는 일어나지 않습니다.

운영

풀 크기가 첫 손잡이입니다. HikariCP 위키의 풀 크기 문서는 시작점으로 이 식을 듭니다.

connections = ((core_count * 2) + effective_spindle_count)

문서의 예에서 effective_spindle_count 는 디스크 대수를 가리킵니다. 문서는 처리량이 최적이 되려면 활성 연결의 수가 그 값 근처 어딘가여야 한다고 적습니다. 코어가 4개이고 디스크가 1개인 예에서 값은 9 가 되고, 문서는 반올림해 10 을 권고값으로 듭니다. 다만 문서는 이 식을 못 박지 않습니다. 애플리케이션을 시험해 보고 이 시작점 주위로 다른 풀 설정을 시도해 보라고 적습니다. SSD(Solid State Drive)에서 이 식이 얼마나 잘 맞는지에 대한 분석은 지금까지 없었다고도 적습니다. 풀 크기 산정은 결국 배포 환경마다 매우 다르다고 적습니다. 문서가 요약으로 남긴 문장은 연결을 기다리는 스레드로 포화된 작은 풀을 원하게 된다는 것입니다.

연결의 수명도 정해야 합니다. HikariCP 문서는 maxLifetime 을 설정하라고 강력히 권한다고 적고, 그 값이 데이터베이스나 인프라가 강제하는 연결 시간 제한보다 몇 초 짧아야 한다고 적습니다. 사용 중인 연결은 물러나지 않습니다. 닫힐 때에야 풀에서 제거됩니다. 연결마다 수명을 조금씩 줄여서, 풀 안 연결들이 같은 순간에 한꺼번에 만료돼 죽는 것을 피합니다.

서버 쪽 상한과 풀 총합의 관계를 봐야 합니다. PostgreSQL 의 max_connections 는 서버로 들어오는 동시 연결의 최대 개수를 정합니다. 기본값은 대개 100 이지만 커널 설정이 그만큼을 받치지 못하면 더 작을 수 있습니다. 이 값은 서버 시작 시에만 설정할 수 있습니다.

앞단 프록시를 두면 셈이 하나 더 늘어납니다. PgBouncer 문서는 max_client_conn 을 올릴 때 운영체제의 파일 디스크립터 한도도 같이 올려야 할 수 있다고 적습니다. 쓰이게 될 파일 디스크립터 수는 max_client_conn 보다 많습니다. 사용자마다 자기 이름으로 서버에 붙으면 이론상 최대는 max_client_conn + (max pool_size * total databases * total users) 입니다. 연결 문자열에 데이터베이스 사용자를 지정해 모두 같은 이름으로 붙으면 이론상 최대는 max_client_conn + (max pool_size * total databases) 입니다. 문서는 누군가 일부러 그런 부하를 만들지 않는 한 이론상 최대에 결코 닿지 않을 것이라고 적습니다. 그래도 파일 디스크립터 수를 안전하게 높은 값으로 잡아 두라고 적습니다.

관련 항목

풀을 실제로 드는 구현체

HikariCP · SQLAlchemy · PgBouncer · ADO.NET

연결을 맺을 때 거치는 단계

인증 · 트랜잭션

반납 시 롤백이 지우는 상태

커밋 · 잠금

이 결정이 적용되는 데이터베이스와 표준

PostgreSQL · SQL Server · JDBC · ODBC · DBAPI

운영값이 매이는 자원

커널 · SSD · 파일 디스크립터 · 공유 메모리

다른 이름: 커넥션풀 · connection pool · connection pooling · 연결 풀