본문으로 건너뛰기

8.2 PgBouncer, HAProxy, VIP

Patroni는 GET /primary처럼 역할을 HTTP 상태 코드로 알려 주는 REST API를 제공합니다. HAProxy는 이 응답만 확인해 primary로 트래픽을 보냅니다. pg_auto_failover에는 이런 endpoint가 없습니다. 노드 역할은 monitor의 pgautofailover.node 테이블과 각 PostgreSQL의 pg_is_in_recovery()로만 확인할 수 있습니다. 따라서 connection pooler나 load balancer를 앞에 두려면 둘 중 하나를 조회하는 장치를 직접 만들어야 합니다.

선택지​

가장 단순한 선택지는 A입니다. 추가 구성 요소가 없으며, 8.1에서 확인한 stop_replication 구간의 쓰기 차단도 그대로 적용됩니다. 커넥션이 많아 pooler가 꼭 필요할 수 있습니다. 또는 애플리케이션 드라이버가 multi-host를 지원하지 않는 경우도 있습니다. 이때 B, C, D를 검토합니다.

PgBouncer: 여러 host는 primary 판별이 아니다​

PgBouncer 1.26 문서의 [databases] 항목 host에는 여러 호스트를 콤마로 구분해 적습니다. 목록에서 어느 호스트를 선택할지는 load_balance_hosts가 정합니다. 값은 round-robin과 disable입니다. 어느 쪽도 서버가 primary인지 확인하지 않습니다. libpq의 target_session_attrs에 해당하는 설정도 없습니다. 따라서 두 노드를 모두 적어 두면 standby로 연결될 위험이 있습니다.

pg_auto_failover와 함께 쓰려면 PgBouncer가 primary 하나만 가리키도록 해야 합니다. failover가 발생하면 host를 바꿔 줘야 합니다. monitor는 상태 결정을 state 채널로 NOTIFY합니다(v2.2 notifications.c). payload는 JSON이므로 스크립트에서 다루기 쉽습니다. 아래는 v2.2 소스의 NotifyStateChange()가 만드는 형식을 옮긴 예시입니다.

{"type": "state", "formation": "default", "groupId": 0, "nodeId": 2, "name": "node2",
"host": "node2", "port": 5432, "reportedState": "primary", "goalState": "primary", "health":"good"}

아래는 개념을 설명하기 위한 예시입니다. 이 노트에서는 실습하지 않았습니다.

#!/usr/bin/env bash
# 예시: monitor 알림을 받으면 현재 primary를 조회해 PgBouncer 설정을 바꾸고 reload 한다
MON="postgres://autoctl_node@monitor:5432/pg_auto_failover?sslmode=require"
current=""
while true; do
primary=$(psql "$MON" -Atc "select nodehost from pgautofailover.node
where formationid='default' and groupid=0
and reportedstate in ('primary','wait_primary','single')
and goalstate = reportedstate")
if [ -n "$primary" ] && [ "$primary" != "$current" ]; then
sed -i "s/^appdb = .*/appdb = host=${primary} port=5432 dbname=appdb/" /etc/pgbouncer/pgbouncer.ini
psql -h /var/run/postgresql -p 6432 -U pgbouncer pgbouncer -c "RELOAD"
current="$primary"
fi
sleep 1
done

이 방식은 monitor에 의존합니다. monitor가 멈추면 갱신도 멈춥니다. 새 failover 결정은 나오지 않습니다. 하지만 keeper는 monitor 접속이 끊긴 뒤에도 이미 받은 목표 상태를 실행합니다. 진행 중인 failover 도중 monitor가 멈추는 경우를 생각해 봅니다. 이때 PgBouncer가 이미 멈춘 옛 primary를 계속 가리킬 위험이 있습니다. 조건문에서 goalstate = reportedstate를 요구하는 이유는 stop_replication 같은 전이 중간 상태를 제외하기 위해서입니다. reload만으로는 기존 서버 연결이 끊기지 않습니다. 따라서 옛 primary에 연결된 세션을 정리하는 절차도 필요합니다. 이때 KILL 또는 RECONNECT 명령을 사용합니다.

HAProxy: external-check로 recovery 여부 확인​

HAProxy의 기본 pgsql-check는 로그인 가능 여부만 확인하며 역할은 구분하지 않습니다. primary만 받으려면 option external-check로 스크립트를 실행합니다. 이 스크립트에서 pg_is_in_recovery()와 transaction_read_only를 확인합니다. 이 예시도 실습하지 않았습니다.

backend pg_primary
option external-check
external-check command /usr/local/bin/pg_is_writable.sh
server node1 node1:5432 check inter 2s fall 2 rise 1
server node2 node2:5432 check inter 2s fall 2 rise 1
#!/usr/bin/env bash
# /usr/local/bin/pg_is_writable.sh HAProxy가 인자 3번째에 서버 주소, 4번째에 포트를 넘긴다
r=$(psql "host=$3 port=$4 dbname=appdb user=haproxy_check connect_timeout=1" \
-Atc "select not pg_is_in_recovery() and current_setting('transaction_read_only') = 'off'")
[ "$r" = "t" ]

최근 HAProxy는 global 섹션에서 external check를 명시적으로 허용해야 합니다. 사용 중인 버전의 설정 문서를 확인해야 합니다.

transaction_read_only까지 확인하는 이유는 stop_replication 구간의 노드를 제외하기 위해서입니다. 이 확인을 생략하면 두 노드가 모두 통과할 위험이 있습니다. promote는 완료됐지만 옛 primary가 아직 demote되지 않은 순간에 생깁니다. health check용 사용자와 HBA 규칙은 8.3의 원칙에 따라 직접 추가해야 합니다.

경고

앞의 PgBouncer 구성은 monitor 테이블과 state 알림을 사용합니다. 따라서 monitor를 기준으로 판정합니다. PgBouncer 자체에는 노드 역할을 판정하는 기능이 없습니다. 반면 HAProxy의 external check는 각 노드의 상태를 독립적으로 확인합니다. 네트워크 분리로 고립된 옛 primary가 아직 스스로 멈추지 않았을 가능성이 있습니다. 이 경우 해당 노드도 check를 통과합니다. 7.4의 실측에서 고립된 primary는 분리 후 약 50초 동안 PostgreSQL을 유지했습니다. 노드 기준 check를 쓴다면 이 구간을 차단할 방법도 설계해야 합니다. 예를 들어 monitor 조회 결과와 교차 확인하는 방법이 있습니다.

VIP​

keepalived 같은 VRRP 도구는 pg_auto_failover 상태를 알지 못합니다. VIP를 쓰려면 IP를 옮기는 스크립트가 필요합니다. PgBouncer 예시처럼 monitor 알림이나 테이블 조회를 기준으로 삼습니다. 클라우드 환경에서는 VIP 대신 DNS 레코드나 로드밸런서 대상 그룹을 바꾸는 방식을 흔히 사용합니다. 어느 방식을 사용하든 IP 이동 시간은 failover 시간에 더해집니다.

정리​

pg_auto_failover에는 역할을 알려 주는 HTTP endpoint가 없습니다. 추가 구성 요소 없이 쓰려면 클라이언트 multi-host URI가 가장 안전합니다. PgBouncer, HAProxy, VIP를 쓰려면 별도의 장치를 직접 만들어야 합니다. 이 장치는 monitor 상태나 노드의 read-only 여부를 조회해야 합니다. 전이 중간 상태와 고립된 옛 primary를 제외하는 조건도 넣어야 합니다.