TT Lab
블로그 시작하기
배우기 러닝패스 코스

PostgreSQL 고가용성: 서버 세 대로 하는 active-standby

Patroni·etcd 자동 장애 조치와 따라가는 클라이언트

TT Lab 에서 이어서 보기

목표

etcd 세 대 위에 Patroni 클러스터를 세우고, 리더를 떼어 내 자동 장애 조치와 옛 리더의 복귀를 확인한 뒤, 앱이 연결 문자열과 가상 IP 로 새 리더를 따라가게 합니다.

왜 중요한가

앞 모듈의 split-brain 은 '누가 주 서버인가' 를 각 서버가 혼자 판단해서 생겼습니다. Patroni 는 그 판단을 etcd 의 다수결 하나로 모으고, 다수에 닿지 못한 주 서버를 스스로 내려오게 합니다. 그런데 구축이 끝났다고 HA 가 되는 것은 아닙니다. pg_hba 한 줄, VIP 의 조건 검사 하나가 빠져도 평소에는 아무 증상이 없다가 장애가 나는 날에만 드러납니다. 그래서 이 실습은 세우고 끝내지 않고, 리더를 실제로 떼어 내고 다시 붙여 봅니다.

단계

  1. db1·db2·witness 에서 etcd 를 띄워 세 멤버짜리 클러스터 하나로 묶으세요. 멤버 이름은 서버 이름과 같고, 피어 주소(2380)와 클라이언트 주소(2379)는 각 서버의 세션망 주소여야 합니다. 설정은 각 서버의 /etc/default/etcd 에 둡니다. witness 에서 etcdctl --endpoints=172.30.1.2:2379,172.30.1.3:2379,172.30.1.4:2379 endpoint health 가 셋 다 healthy 여야 합니다.
  2. db1 의 /etc/patroni/config.yml 을 쓰고(scope pgha, name db1) Patroni 를 띄워 클러스터를 부트스트랩하세요. REST API 와 PostgreSQL 의 connect_address 는 db1 의 세션망 주소여야 합니다. pg_hba 에는 세션망과 127.0.0.1 의 복제 줄을 둡니다. db1 이 리더가 되어야 합니다.
  3. db2 에도 같은 틀의 /etc/patroni/config.yml(name db2, db2 의 세션망 주소)을 두고 Patroni 를 띄우세요. db2 가 리더에게서 베이스백업을 받아 streaming 상태의 대기 서버가 되어야 합니다.
  4. patronictl edit-config 로 클러스터 설정에 synchronous_mode: true 를 켜세요. 대기 서버가 Sync Standby 가 되어야 합니다. synchronous_standby_names 를 직접 고치지 않습니다.
  5. 리더에 앱 계정 app(비밀번호 apppass)과 그 계정이 주인인 데이터베이스 appdb, 표 orders(id bigserial primary key, note text not null, at timestamptz not null default now()) 를 만드세요. 그리고 두 서버를 모두 적고 쓰기를 받는 서버만 고르며 connect_timeout(10초 이하)을 둔 libpq 연결 문자열을 /root/ha/dsn 에 한 줄로 두세요. psql "$(cat /root/ha/dsn)" 이 리더에 붙어야 합니다.
  6. 지금 리더인 db1 을 세션망에서 떼어 내세요. db1 에서 iptables 로 eth1 의 들어오고 나가는 패킷을 모두 버리되 ssh(22)는 남깁니다. db2 가 새 리더가 될 때까지 기다린 뒤, 같은 연결 문자열(/root/ha/dsn)로 insert into orders(note) values ('after-failover') 를 하세요. 떼어 낸 db1 은 쓰기를 받지 않아야 합니다.
  7. db1 의 iptables 규칙을 걷으세요. db1 의 Patroni 가 스스로 되감기(pg_rewind)를 하고 새 리더를 따르는 대기 서버로 돌아와야 합니다. db1 이 streaming 이고 타임라인이 리더와 같으며, 6단계의 after-failover 주문이 db1 에도 보여야 합니다.
  8. db1 과 db2 에 keepalived 를 설정해 가상 IP 172.30.1.50/24 를 eth1 에 올리세요. VRRP 는 세션망에서 유니캐스트(상대 서버 주소)로 주고받고, 우선순위는 db1 110·db2 100 입니다. VIP 는 언제나 Patroni 리더에만 있어야 하고, witness 에서 psql -h 172.30.1.50 -U app -d appdb 가 쓰기를 받는 서버에 붙어야 합니다.

참고

세 서버의 etcd 를 하나로 묶는다

db1·db2·witness 에서 etcd 를 띄워 세 멤버짜리 클러스터 하나로 묶으세요. 멤버 이름은 서버 이름과 같고, 피어 주소(2380)와 클라이언트 주소(2379)는 각 서버의 세션망 주소여야 합니다. 설정은 각 서버의 /etc/default/etcd 에 둡니다. witness 에서 etcdctl --endpoints=172.30.1.2:2379,172.30.1.3:2379,172.30.1.4:2379 endpoint health 가 셋 다 healthy 여야 합니다.

세 서버의 설정은 이름·주소만 다르고 ETCD_INITIAL_CLUSTER 는 같습니다. 한 대만 먼저 띄우면 나머지를 기다리느라 systemctl 이 돌아오지 않으니 systemctl start --no-block etcd 로 세 대를 거의 동시에 띄웁니다. 피어 주소에 localhost 를 쓰면 서로를 찾지 못합니다.

db1 에서 Patroni 로 클러스터를 세운다

db1 의 /etc/patroni/config.yml 을 쓰고(scope pgha, name db1) Patroni 를 띄워 클러스터를 부트스트랩하세요. REST API 와 PostgreSQL 의 connect_address 는 db1 의 세션망 주소여야 합니다. pg_hba 에는 세션망과 127.0.0.1 의 복제 줄을 둡니다. db1 이 리더가 되어야 합니다.

형식 예시의 틀에서 이름과 주소만 바꿉니다. 파일은 postgres 가 읽을 수 있어야 합니다(chown postgres:postgres). Patroni 는 자기 PostgreSQL 에게도 127.0.0.1 로 복제 연결을 엽니다(되감기 전에 타임라인을 확인할 때). 그 줄이 없으면 7단계에서 db1 이 돌아오지 못합니다. 리더 확인은 witness 에서 patronictl list 입니다.

db2 를 대기 서버로 붙인다

db2 에도 같은 틀의 /etc/patroni/config.yml(name db2, db2 의 세션망 주소)을 두고 Patroni 를 띄우세요. db2 가 리더에게서 베이스백업을 받아 streaming 상태의 대기 서버가 되어야 합니다.

Patroni 가 베이스백업부터 대기 서버 설정까지 해 줍니다. 손으로 했던 1모듈의 3단계와 4단계가 무엇을 대신한 것인지 비교해 보세요. patronictl list 의 State 가 streaming, TL 이 리더와 같아야 합니다.

Patroni 의 동기 모드

patronictl edit-config 로 클러스터 설정에 synchronous_mode: true 를 켜세요. 대기 서버가 Sync Standby 가 되어야 합니다. synchronous_standby_names 를 직접 고치지 않습니다.

patronictl -c /etc/patroni/patronictl.yml edit-config -s synchronous_mode=true --force 처럼 한 줄로도 됩니다. 동기 모드에서는 Patroni 가 synchronous_standby_names 를 관리하고, 동기 대상이 아닌 서버는 승격 후보에서 뺍니다. 확인은 curl -s http://172.30.1.2:8008/config.

앱이 리더를 찾아가는 연결 문자열

리더에 앱 계정 app(비밀번호 apppass)과 그 계정이 주인인 데이터베이스 appdb, 표 orders(id bigserial primary key, note text not null, at timestamptz not null default now()) 를 만드세요. 그리고 두 서버를 모두 적고 쓰기를 받는 서버만 고르며 connect_timeout(10초 이하)을 둔 libpq 연결 문자열을 /root/ha/dsn 에 한 줄로 두세요. psql "$(cat /root/ha/dsn)" 이 리더에 붙어야 합니다.

리더에는 PGPASSWORD=supass psql -h 172.30.1.2 -U postgres 로 붙습니다. 연결 문자열은 URI 꼴(postgresql://app@db1:5432,db2:5432/appdb?target_session_attrs=read-write)이나 키값 꼴(host=db1,db2 ...) 모두 됩니다. target_session_attrs 가 없으면 첫 서버가 대기 서버여도 그냥 붙습니다. connect_timeout 이 없으면 죽은 서버에 대한 TCP 연결 시도를 2분 넘게 기다린 뒤에야 다음 서버로 넘어갑니다.

리더를 떼어 낸다

지금 리더인 db1 을 세션망에서 떼어 내세요. db1 에서 iptables 로 eth1 의 들어오고 나가는 패킷을 모두 버리되 ssh(22)는 남깁니다. db2 가 새 리더가 될 때까지 기다린 뒤, 같은 연결 문자열(/root/ha/dsn)로 insert into orders(note) values ('after-failover') 를 하세요. 떼어 낸 db1 은 쓰기를 받지 않아야 합니다.

규칙은 -I INPUT 1 -i eth1 -p tcp --dport 22 -j ACCEPT, -I INPUT 2 -i eth1 -j DROP 처럼 ssh 를 먼저 허용하고 나머지를 버립니다(OUTPUT 은 --sport 22). db1 의 Patroni 는 etcd 의 다수에 닿지 못해 리더 열쇠를 갱신할 수 없고, 스스로 내려옵니다. ssh db1 journalctl -u patroni 에서 그 순간을 찾아보세요. 새 리더는 30초 안팎에 섭니다.

옛 리더가 되감겨 돌아온다

db1 의 iptables 규칙을 걷으세요. db1 의 Patroni 가 스스로 되감기(pg_rewind)를 하고 새 리더를 따르는 대기 서버로 돌아와야 합니다. db1 이 streaming 이고 타임라인이 리더와 같으며, 6단계의 after-failover 주문이 db1 에도 보여야 합니다.

넣은 규칙을 같은 꼴로 -D 해서 지우면 됩니다. db1 은 떼어진 동안 옛 타임라인에 WAL 을 조금 더 남겼을 수 있어서 Patroni 가 되감기를 돌립니다. 오래 running 에 머문다면 ssh db1 journalctl -u patroni 에서 pg_hba 와 rewind 를 찾아보세요.

리더를 따라다니는 가상 IP

db1 과 db2 에 keepalived 를 설정해 가상 IP 172.30.1.50/24 를 eth1 에 올리세요. VRRP 는 세션망에서 유니캐스트(상대 서버 주소)로 주고받고, 우선순위는 db1 110·db2 100 입니다. VIP 는 언제나 Patroni 리더에만 있어야 하고, witness 에서 psql -h 172.30.1.50 -U app -d appdb 가 쓰기를 받는 서버에 붙어야 합니다.

우선순위만 두면 VIP 는 리더가 아니라 우선순위가 높은 db1 에 머뭅니다. 지금 리더는 db2 입니다. vrrp_script 로 자기 Patroni 의 /primary(리더일 때만 200)를 묻고 track_script 로 묶으세요. 설정 파일은 /etc/keepalived/keepalived.conf 입니다. PostgreSQL 이 0.0.0.0 에서 듣고 있는 이유도 여기에 있습니다.