본문으로 건너뛰기

8.1 multi-host 연결 문자열

PostgreSQL 10부터 libpq는 연결 문자열에 여러 호스트를 지정할 수 있습니다. 앞에서부터 차례로 접속을 시도합니다. 이후 target_session_attrs 조건을 충족하는 첫 번째 서버에 연결합니다. pg_auto_failover는 이 기능을 클라이언트 측 HA의 기본 수단으로 사용합니다. monitor는 group에 등록된 노드 목록을 바탕으로 연결 문자열을 생성합니다.

monitor가 만들어 주는 URI​

pg_autoctl show uri
pg_autoctl show uri --formation default --json

실습 환경의 monitor 컨테이너가 출력한 값입니다.

Type | Name | Connection String
-------------+---------+-------------------------------
monitor | monitor | postgres://autoctl_node@monitor:5432/pg_auto_failover?sslmode=require
formation | default | postgres://node1:5432,node2:5432/appdb?target_session_attrs=read-write&sslmode=require

formation URI의 데이터베이스 이름은 노드를 만들 때 지정한 --dbname입니다. 노드를 추가하거나 삭제하면 목록도 바뀝니다. 애플리케이션 설정에 고정했다면 이 값도 함께 수정해야 합니다. 실습의 scripts/writer.sh는 실행할 때마다 pg_autoctl show uri --formation default로 URI를 조회해 사용합니다.

접속 시도 순서​

target_session_attrs=read-write는 연결 후 transaction_read_only가 off인 서버만 허용합니다. PostgreSQL 14부터는 primary, standby, prefer-standby, read-only 값도 사용할 수 있습니다. primary는 pg_is_in_recovery()를 기준으로 하므로 의미가 조금 다릅니다. 아래 stop_replication 설명에서 이 차이가 드러납니다.

실측: switchover와 failover 중의 오류​

switchover 중 발생한 writer 실패 5건은 모두 같은 메시지였습니다.

05:08:02.876 FAIL psql: error: connection to server at "node1" (172.18.0.3), port 5432 failed: Connection refused

libpq는 node1에서 연결이 거부된 뒤 node2에도 접속을 시도했습니다. 하지만 node2는 아직 쓰기를 허용하지 않아 조건을 충족하지 못했습니다. 오류 메시지에는 마지막 실패 사유 하나만 남습니다. 1.35초 뒤 node2가 wait_primary에 도달하자 즉시 연결에 성공했습니다.

primary를 docker kill로 종료한 failover에서는 51초 동안 216건이 실패했습니다. 이때 메시지는 대부분 could not translate host name "node2"였습니다. 컨테이너가 사라지면서 Docker DNS에서도 이름이 삭제되었기 때문입니다. 운영 환경에서 서버 전원이 완전히 꺼지면 대신 connect timeout이 발생합니다.

stop_replication 구간의 쓰기 차단​

failover 중 승격 후보는 stop_replication 상태에서 이미 promote된 상태입니다. 그런데도 writer는 이 구간에 새 primary에 쓸 수 없었습니다. keeper가 이 전이 과정에서 default_transaction_read_only를 on으로 설정하기 때문입니다. 다음은 실측 로그입니다.

05:08:02 10 INFO FSM transition from "prepare_promotion" to "stop_replication": Prevent against split-brain situations.
05:08:02 10 INFO Prevent writes to the promoted standby while the primary is not demoted yet, by making the service incompatible with target_session_attrs = read-write

monitor가 옛 primary가 demote되었다고 판단하면(wait_primary) keeper는 이 값을 다시 off로 변경합니다. 따라서 정상적인 failover에서는 read-write로 접속하는 클라이언트가 옛 primary와 새 primary에 동시에 연결되지 않습니다. 다만 이는 절대적인 fencing이 아닙니다. monitor가 옛 primary의 중지를 확인하지 못해도 primary_demote_timeout이 지나면 새 노드를 wait_primary로 전환합니다. 옛 keeper가 PostgreSQL을 중지하지 못한 장애에서는 양쪽이 모두 쓰기를 허용할 수 있습니다(7.6).

경고

default_transaction_read_only는 세션 기본값일 뿐입니다. 클라이언트가 SET default_transaction_read_only = off나 BEGIN READ WRITE로 변경하면 stop_replication 구간에도 새 primary에 쓰기가 가능합니다. 또한 target_session_attrs=primary는 pg_is_in_recovery()만 확인합니다. 따라서 이미 promote된 이 노드를 primary로 허용합니다. pg_auto_failover가 의도한 보호를 적용하려면 read-write를 사용해야 합니다. 애플리케이션은 읽기 전용 설정을 덮어쓰지 않아야 합니다. 이 노트에서는 두 경우를 실측하지 않았습니다. 판단 근거는 v2.2 소스의 fsm_transition.c입니다.

connect_timeout​

호스트가 응답하지 않으면 libpq는 OS의 TCP 타임아웃까지 기다립니다. 첫 번째 호스트가 중단된 primary라면 대기 시간이 끝날 때까지 다음 호스트로 넘어가지 못합니다. 실습의 writer는 connect_timeout=2를 추가했습니다.

uri=$(pg_autoctl show uri --formation default)
psql "${uri}&connect_timeout=2" -c "select inet_server_addr()"

connect_timeout은 호스트마다 적용됩니다. 노드가 3대이고 앞의 두 대가 응답하지 않으면 최대 약 4초 뒤 세 번째 호스트에 접속합니다.

드라이버별 설정​

드라이버multi-host 표기primary 선택
libpq 기반(psql, psycopg, Go pgx 등)postgres://h1:5432,h2:5432/dbtarget_session_attrs=read-write
pgjdbcjdbc:postgresql://h1:5432,h2:5432/dbtargetServerType=primary
NpgsqlHost=h1,h2Target Session Attributes=read-write

드라이버마다 primary 판정 방식이 다를 가능성이 있습니다. 따라서 stop_replication 구간의 동작은 사용 중인 버전의 문서에서 확인해야 합니다. 이 노트에서는 libpq(psql)만 실측했습니다.

기존 연결과 재시도​

multi-host URI는 새 연결을 생성할 때만 호스트를 선택합니다. failover가 발생하면 옛 primary에 연결되어 있던 세션은 끊기거나 read-only 오류를 받습니다. 커넥션 풀을 사용하는 애플리케이션은 다음 기능을 갖춰야 합니다.

  • 연결 오류와 cannot execute ... in a read-only transaction 오류를 재시도 대상으로 분류
  • 풀의 연결 검증(test on borrow 등)과 최대 수명 설정
  • 재시도 간격을 failover 시간(기본값에서 50초 안팎)에 맞춘 backoff

정리​

pg_auto_failover에서 애플리케이션은 libpq multi-host 연결 문자열로 새 primary를 찾습니다. target_session_attrs=read-write를 사용해야 stop_replication 구간의 쓰기 차단이 효과를 발휘합니다. 중단된 호스트에서 오래 기다리지 않도록 connect_timeout을 짧게 설정해야 합니다. 끊긴 기존 연결은 애플리케이션이 재시도로 처리해야 합니다.