커넥션 풀
연결을 미리 만들어 두고 빌려주고 돌려받는 방식입니다. 쓸 때마다 새로 연결하지 않고 이미 열려 있는 것을 꺼내 씁니다. 다 쓴 연결은 닫지 않고 풀로 돌려보냅니다. 같은 연결이 여러 요청을 옮겨 다니며 다시 쓰입니다.
쉽고 빠른 이해
연결을 미리 여러 개 만들어 두고 필요할 때 빌려줬다가 다 쓰면 돌려받는 물건입니다. 예를 들어 웹 서버가 요청을 처리할 때마다 데이터베이스에 새로 접속하지 않고, 이미 열려 있는 접속 하나를 꺼내 씁니다.
접속을 새로 맺는 일은 여러 단계를 거쳐 시간이 걸립니다. 그 시간을 요청마다 다시 치르지 않으려고 씁니다.
어떻게 돕나:
- 요청이 오면 놀고 있는 연결이 있는지 먼저 봅니다
- 있으면 그것을 내주고, 없으면 정해 둔 상한 안에서 새로 만들어 내줍니다
- 다 쓴 연결은 끊지 않고 풀로 돌려보내 다음 요청이 다시 씁니다
대가도 있습니다. 앞서 쓰던 사람이 남긴 상태가 다음 사람에게 넘어갈 수 있고, 놀고 있는 연결도 서버 쪽 자리를 계속 차지합니다.
상세
데이터베이스 서버에 연결하는 일은 대개 시간이 걸리는 여러 단계로 이뤄집니다. 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
운영값이 매이는 자원
다른 이름: 커넥션풀 · connection pool · connection pooling · 연결 풀