9.2 HAProxy
HAProxy는 Patroni 공식 저장소가 데모 설정을 직접 제공하는 유일한 로드밸런서다. 공식 문서 Introduction은 이 조합을 두고 “Patroni provides an HAProxy configuration, which will give your application a single endpoint for connecting to the cluster’s leader"라고 소개한다. 애플리케이션은 HAProxy 주소 하나만 알면 되고, primary가 어디로 옮겨 가든 HAProxy가 따라간다.
공식 haproxy.cfg 해부
저장소 루트의 haproxy.cfg 전문이다.
global
maxconn 100
defaults
log global
mode tcp
retries 2
timeout client 30m
timeout connect 4s
timeout server 30m
timeout check 5s
listen stats
mode http
bind *:7000
stats enable
stats uri /
listen batman
bind *:5000
option httpchk
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server postgresql_127.0.0.1_5432 127.0.0.1:5432 maxconn 100 check port 8008
server postgresql_127.0.0.1_5433 127.0.0.1:5433 maxconn 100 check port 8009핵심은 다섯 줄이다.
mode tcp는 PostgreSQL 프로토콜을 해석하지 않고 TCP 스트림 그대로 통과시킨다는 뜻이다. HAProxy는 쿼리 내용을 모른 채 연결만 중계하고, 어느 백엔드로 보낼지는 별도의 HTTP 검사로 결정한다.
option httpchk가 그 검사다. 요청 경로를 지정하지 않으면 기본값은 GET /이고, Patroni REST API의 /는 leader lock을 가진 primary에서만 200을 반환한다. 즉 이 한 줄로 “primary 판정"이 끝난다. http-check expect status 200은 200만 정상으로 인정하겠다는 선언이다.
check port 8008은 검사와 트래픽의 포트를 분리한다. 클라이언트 트래픽은 5432(PostgreSQL)로 보내되, health check은 8008(Patroni REST API)에 보낸다. 예제가 8008과 8009를 나눠 쓰는 이유는 한 호스트에 PostgreSQL 인스턴스 두 개를 띄우는 데모 구성이기 때문이고, 노드가 분리된 실전 구성에서는 모두 8008이 된다.
default-server inter 3s fall 3 rise 2는 HAProxy의 검사 리듬이다. 3초 간격으로 검사해서 3연속 실패면 그 서버를 down으로, down 상태에서 2연속 성공이면 다시 up으로 판정한다. failover가 일어나면 구 primary의 GET /가 503으로 바뀌고 새 primary가 200을 반환하기 시작하므로, 이 리듬을 따라 트래픽이 새 primary로 넘어간다.
on-marked-down shutdown-sessions는 서버가 down으로 판정되는 순간 그 서버로 이미 맺어져 있던 세션을 즉시 절단한다. 이 옵션이 중요해지는 것은 demote된 구 primary가 read-only로 살아 있는 경우다. 기존 연결을 그대로 두면 애플리케이션은 read-only 노드에 붙은 채 쓰기 실패를 반복하게 되는데, 세션을 끊어 버리면 재연결이 일어나고 그 재연결은 새 primary로 라우팅된다.
inter 3s fall 3 조합이면 검사 실패가 쌓여 down 판정에 이르기까지 단순 계산으로 10초 안팎이 걸린다. 값을 줄이면 전환이 빨라지지만 일시적 응답 지연을 장애로 오판할 여지도 커진다. Patroni 쪽의 failover 소요 시간(ttl, loop_wait)과 별개로 더해지는 지연이라는 점을 감안해 조정한다.데모 실행은 두 명령이면 된다.
haproxy -f haproxy.cfg
psql --host 127.0.0.1 --port 5000 postgresreplica 분산: 5000과 5001
쓰기만 라우팅하기는 아깝다. 저장소의 docker-compose 데모(Patroni 3, etcd 3, HAProxy 1 구성)는 포트를 둘로 나눈다. 5000은 primary로, 5001은 replica들로 부하 분산한다. replica용 listen 블록은 check 요청을 HEAD /replica로 바꾸기만 하면 된다. /replica는 state가 running이고 role이 replica인 노드에서만 200을 반환하므로, primary는 자연스럽게 5001에서 빠진다.
replica 검사에는 두 가지 필터를 더할 수 있다. noloadbalance tag를 설정한 노드는 /replica가 503을 반환해 분산 대상에서 제외되고, HEAD /replica?lag=10MB처럼 lag 상한을 지정하면 복제가 밀린 replica도 빠진다.
confd로 멤버 목록 동적 생성
위의 정적 설정은 노드를 추가하거나 제거할 때마다 haproxy.cfg를 손으로 고쳐야 한다. 저장소의 extras/confd/가 이 문제를 해결한다. confd는 DCS를 watch하다가 변화가 생기면 템플릿에서 설정 파일을 다시 생성하는 도구다.
haproxy.tmpl의 골격은 이렇다. DCS의 /members/* 키를 순회하며 서버 줄을 만들어 낸다.
listen primary
bind *:5000
option httpchk HEAD /primary
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
{{range gets "/members/*"}} server {{base .Key}} ... {{end}}각 member key의 JSON에서 conn_url을 파싱해 host:port를, api_url을 파싱해 check port를 얻는다. replicas listen 블록은 같은 구조에 check만 HEAD /replica다. 노드가 클러스터에 합류하면 member key가 생기고, confd가 이를 감지해 서버 목록에 자동으로 추가한다.
haproxy.toml이 이 템플릿의 동작을 정의한다.
src = "haproxy.tmpl"
dest = "/etc/haproxy/haproxy.cfg"
check_cmd = "/usr/sbin/haproxy -c -f {{ .src }}"
reload_cmd = "haproxy -f /etc/haproxy/haproxy.cfg -sf $(cat /var/run/haproxy.pid)"check_cmd가 렌더링된 설정을 문법 검증하고, 통과하면 reload_cmd가 적용한다. -sf 플래그는 새 HAProxy 프로세스를 띄운 뒤 구 프로세스에 기존 연결을 마저 처리하고 종료하라는 신호를 보내는 방식이라 reload 중에도 연결이 끊기지 않는다.
flowchart TD
M["DCS member key 변화"] --> C["confd 감지"]
C --> R["haproxy.tmpl 렌더링"]
R --> V["check_cmd 문법 검증"]
V --> L["reload_cmd 무중단 적용"]
stats 페이지
공식 설정의 listen stats 블록은 7000 포트에 HAProxy 내장 상태 페이지를 연다. 브라우저로 접속하면 각 백엔드 서버의 up/down, 검사 성공/실패 이력, 현재 세션 수를 한눈에 볼 수 있다. failover 직후 “HAProxy가 새 primary를 인식했는가"를 확인하는 가장 빠른 창구다.
정리
- HAProxy 라우팅의 전부는
mode tcp+option httpchk+check port 8008세 줄이다. 트래픽은 PostgreSQL 포트로, 판정은 REST API로 분리된다. on-marked-down shutdown-sessions가 구 primary에 남은 세션을 절단해 read-only 노드에 갇히는 상황을 막는다.- 포트 5000(primary)과 5001(replica) 분리,
noloadbalancetag와?lag=필터로 읽기 분산까지 한 계층에서 처리한다. 멤버 목록 자동화가 필요하면extras/confd/의 템플릿을 쓴다.