9.4 클라이언트 라이브러리 라우팅
로드밸런서도, VIP도, pooler도 없이 라우팅하는 방법이 하나 남아 있습니다. 클라이언트가 후보 노드를 전부 알고, 접속할 때마다 조건에 맞는 노드를 고르는 방식입니다. libpq(그리고 libpq를 감싸는 대부분의 드라이버)와 PgJDBC가 이를 지원합니다. 이 절의 내용은 Patroni 문서가 아니라 PostgreSQL과 PgJDBC 공식 문서에 근거하며, Patroni 클러스터가 아니어도 동작하는 범용 기능입니다.
libpq multi-host 접속 문자열
접속 문자열에 호스트를 쉼표로 나열하면 libpq가 순서대로 시도해서, 조건을 만족하는 첫 호스트에 연결합니다.
postgresql://10.0.0.11:5432,10.0.0.12:5432,10.0.0.13:5432/mydb?target_session_attrs=read-write&connect_timeout=5
첫 호스트가 죽어 있으면 다음으로, 연결은 되지만 조건(여기서는 read-write)에 맞지 않으면 또 다음으로 넘어갑니다. failover로 primary가 두 번째 노드로 옮겨 갔다면, 재연결 시점에 첫 노드는 조건 불일치로 걸러지고 두 번째 노드에 붙습니다.
target_session_attrs의 여섯 값
어떤 세션이면 연결을 수락할지는 target_session_attrs가 정합니다. 판정 기준이 값마다 미묘하게 다릅니다.
| 값 | 수락 조건 |
|---|---|
any (기본) | 접속만 되면 아무 서버나 |
read-write | hot standby가 아니고 default_transaction_read_only가 off |
read-only | read-write의 반대 (기본 읽기 전용 세션) |
primary | hot standby가 아님 (default_transaction_read_only는 무관) |
standby | hot standby |
prefer-standby | standby 우선, 하나도 없으면 any로 전체 재시도 |
any와 read-write 외의 값은 PostgreSQL 14 이상 libpq에서, 아래의 load_balance_hosts는 PostgreSQL 16 이상 libpq에서 사용할 수 있습니다.
read-write와 primary의 차이가 실무에서 갈리는 지점입니다. primary라도 default_transaction_read_only가 on이면 read-write는 거부하고 primary는 수락합니다. "쓰기가 실제로 되는 노드"를 원하면 read-write, "복제 계층상 primary"를 원하면 primary를 씁니다.
부하 분산과 connect_timeout
기본 동작은 나열 순서대로의 시도라서 첫 호스트에 부하가 쏠립니다. load_balance_hosts=random을 주면 후보를 무작위 순서로 시도합니다. target_session_attrs=standby와 조합하면 읽기 전용 연결을 standby들에 랜덤 분산하는 구성이 됩니다.
postgresql://n1,n2,n3/mydb?target_session_attrs=standby&load_balance_hosts=random
connect_timeout도 함께 설정하는 편이 좋습니다. 노드 하나가 TCP 응답 없이 죽어 있을 때 timeout이 없으면 다음 후보로 넘어가기까지 OS 기본 타임아웃만큼 기다리게 되어, multi-host의 장점이 무색해집니다.
인증 실패는 다음 호스트로 안 넘어간다
주의할 함정이 있습니다. 호스트에 연결이 닿지 않거나 세션 조건이 안 맞으면 다음 후보를 시도하지만, 연결은 됐는데 인증에 실패하면 거기서 멈추고 나머지 호스트를 시도하지 않습니다.
노드별로 pg_hba.conf나 비밀번호가 어긋나 있으면 multi-host 나열이 기대한 fallback으로 동작하지 않습니다. 인증 실패는 "이 노드만 건너뛰고 다음"이 아니라 전체 접속 실패입니다. 클러스터 전 노드의 인증 설정을 동일하게 유지하는 일이 이 방식의 전제 조건입니다.
PgJDBC
JDBC 드라이버는 libpq와 별개 구현이지만 같은 아이디어를 제공합니다.
jdbc:postgresql://node1,node2,node3/accounting?targetServerType=preferSecondary&loadBalanceHosts=true
targetServerType이 libpq의 target_session_attrs에 해당합니다. 값은 any(기본), primary(구 표기 master), secondary(구 표기 slave), preferSecondary, preferPrimary이고, primary/secondary 판별은 서버가 쓰기 가능한지 관찰하는 방식입니다. loadBalanceHosts(기본 false)를 켜면 후보 중 무작위로 고릅니다.
libpq에 없는 요소가 hostRecheckSeconds(기본 10초)입니다. 호스트별 판별 결과를 JVM 전역으로 캐시하는 시간으로, 캐시가 유효한 동안은 직전 판정을 재사용합니다. 연결 시도 횟수를 줄여 주는 대신, failover 직후에는 캐시가 만료될 때까지 구 역할 기준으로 후보를 고르는 지연이 생깁니다.
한계: leader lock을 보지 않는다
이 방식의 판별은 전부 PostgreSQL 수준의 관찰입니다. hot standby인지, 쓰기가 되는지입니다. Patroni가 DCS에 유지하는 leader lock은 어디에서도 확인하지 않으며, 판별 시점도 접속 순간 한 번뿐입니다.
평상시에는 "쓰기 가능한 노드"와 "leader lock을 가진 노드"가 일치하므로 문제가 드러나지 않습니다. 어긋나는 것은 비정상 상황입니다. 예를 들어 pause 중에 Patroni는 primary가 둘 존재해도 경고 로그만 남기고 demote하지 않는데, 이때 target_session_attrs=read-write는 두 노드 모두 통과시킵니다. 접속이 어느 쪽으로 붙느냐에 따라 쓰기가 갈라집니다. leader lock을 직접 확인하는 REST API 기반 라우팅이 이런 상황에서 더 안전하다고 보는 이유입니다. 클라이언트 라우팅을 주 경로로 쓴다면 pause 상태의 수동 작업 절차를 그만큼 더 엄격하게 관리해야 합니다.
정리
클라이언트 라이브러리 라우팅은 인프라 추가가 없다는 점 하나로 충분히 매력적이고, 소규모 구성이나 라우팅 계층의 이중화 부담을 피하고 싶은 환경에서 실용적인 선택입니다. 다만 판별이 접속 시점의 PostgreSQL 관찰에 그치고 leader lock을 보지 않는다는 성격, 그리고 인증 실패가 fallback을 끊는다는 함정을 알고 써야 합니다. connect_timeout(libpq)과 hostRecheckSeconds(PgJDBC)가 장애 시 체감 지연을 좌우하는 조절점입니다.