본문으로 건너뛰기

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가 그 검사입니다. 요청을 지정하지 않으면 HAProxy는 OPTIONS /를 보내고, Patroni REST API는 /에 대해 GET/HEAD/OPTIONS 모두 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의 / health check 응답이 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 postgres

replica 분산: 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도 빠집니다.

HAProxy의 primary와 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 중에도 연결이 끊기지 않습니다.

stats 페이지​

공식 설정의 listen stats 블록은 7000 포트에 HAProxy 내장 상태 페이지를 엽니다. 브라우저로 접속하면 각 백엔드 서버의 up/down, 검사 성공/실패 이력, 현재 세션 수를 한눈에 볼 수 있습니다. failover 직후 "HAProxy가 새 primary를 인식했는가"를 확인하는 가장 빠른 창구입니다.

경고

HAProxy 자체의 고가용성은 이 설정에 포함되어 있지 않습니다. HAProxy가 한 대뿐이면 데이터베이스가 아무리 잘 넘어가도 그 앞단이 단일 장애점입니다. HAProxy를 복수로 두고 그 앞을 다시 묶는 구성(또는 클라이언트 multi-host로 HAProxy 두 대를 나열하는 구성)을 별도로 설계해야 합니다.

정리​

  • 판정 구조는 mode tcp, option httpchk, check port 8008 세 줄로 요약됩니다. 트래픽은 PostgreSQL 포트로, 판정은 REST API로 분리됩니다.
  • on-marked-down shutdown-sessions가 구 primary에 남은 세션을 절단해 read-only 노드에 갇히는 상황을 막습니다.
  • 포트 5000(primary)과 5001(replica) 분리, noloadbalance tag와 ?lag= 필터로 읽기 분산까지 한 계층에서 처리합니다. 멤버 목록 자동화가 필요하면 extras/confd/의 템플릿을 씁니다.