PostgreSQL 고가용성: 서버 세 대로 하는 active-standby
자동 장애 조치: 다수결, 스스로 내려오기, 따라가는 클라이언트
한 줄 요약
Patroni 는 주 서버 자리를 etcd 의 열쇠 하나로 표현합니다. 열쇠를 가진 서버만 주 서버이고, 열쇠는 짧은 수명(TTL)을 계속 갱신해야 유지됩니다. 갱신하지 못한 주 서버는 스스로 내려오고, 다수의 표를 가진 쪽에서 새 주 서버가 열쇠를 잡습니다. 클라이언트는 다중 호스트 연결 문자열이나 가상 IP 로 새 주 서버를 따라갑니다.
왜 이게 필요했나
앞 모듈에서 본 것처럼 사람이 내리는 장애 조치는 느리고, 단순한 스크립트는 split-brain 을 만듭니다. 필요한 것은 세 가지입니다. 누가 주 서버인지 한 곳에서 정하기, 주 서버가 그 자격을 잃으면 스스로 쓰기를 멈추기, 그리고 앱이 바뀐 주 서버를 알아서 찾아가기. Patroni 는 앞의 둘을, libpq 와 keepalived 는 마지막 하나를 맡습니다.
어떻게 동작하나
etcd: 표를 세는 곳. etcd 는 Raft 합의로 값을 저장하는 작은 키값 저장소입니다. 세 대로 묶으면 두 대가 동의해야 값이 바뀝니다. 한 대가 떨어져 나가도 남은 두 대가 다수라서 계속 동작하고, 떨어진 한 대는 아무 값도 바꾸지 못합니다. 세 대를 묶을 때는 각자 상대가 닿을 수 있는 주소를 광고해야 합니다(initial-advertise-peer-urls). localhost 나 모든 서버가 같은 주소를 광고하면 서로를 찾지 못합니다.
Patroni: 열쇠를 잡는 경주. 각 DB 서버의 Patroni 는 loop_wait 마다 etcd 를 봅니다. 리더 열쇠(/service/<scope>/leader)가 비어 있으면 가장 앞선 대기 서버가 그것을 잡고 승격합니다. 리더는 매 주기 열쇠의 TTL 을 갱신합니다. 이 실습은 ttl 20초, loop_wait 5초로 짧게 둡니다.
스스로 내려오기. 리더가 etcd 의 다수에 닿지 못하면 TTL 을 갱신할 수 없습니다. 그러면 Patroni 는 자기 PostgreSQL 을 읽기 전용으로 강등합니다. 앞 모듈에서 손으로 하던 펜싱을 각 서버가 스스로 하는 셈이고, 덕분에 분할된 쪽의 옛 주 서버가 계속 쓰기를 받는 일이 없습니다. 실측으로 리더를 떼어 낸 뒤 27초 만에 새 리더가 섰습니다(ttl 20초와 주기들의 합).
돌아온 옛 리더. 다시 이어지면 옛 리더는 자신이 열쇠를 갖고 있지 않다는 것을 보고 대기 서버로 붙으려 합니다. 갈라졌다면 use_pg_rewind: true 덕분에 pg_rewind 를 스스로 돌립니다. 이때 Patroni 는 자기 서버에 127.0.0.1 로 복제 연결을 열어 타임라인을 확인하는데, pg_hba 에 그 줄이 없으면 되감기를 시작하지 못하고 옛 타임라인에 머뭅니다. 오류가 아니라 대기라서 오래 눈에 띄지 않습니다.
동기 모드. synchronous_mode: true 를 주면 Patroni 가 synchronous_standby_names 를 직접 관리합니다. 건강한 대기 서버만 동기 대상으로 올리고, 동기 대상이 아닌 서버는 승격 후보에서 뺍니다. 대기 서버가 모두 사라지면 비동기로 내려가 쓰기를 계속 받습니다(synchronous_mode_strict 를 켜면 대신 멈춥니다). 첫 모듈에서 손으로 겪은 '대기 서버가 멈추면 커밋도 멈춘다' 를 정책으로 고르는 자리입니다.
클라이언트가 따라가는 두 방법.
| 방법 | 어떻게 | 대가 |
|---|---|---|
| libpq 다중 호스트 | host=db1,db2 ... target_session_attrs=read-write |
드라이버가 libpq 계열이어야 한다. 연결할 때만 고른다 |
| 가상 IP(keepalived) | 주 서버에만 VIP 를 올린다 | 같은 L2 망이어야 한다. VIP 를 올릴 조건을 바르게 줘야 한다 |
다중 호스트 문자열은 적힌 순서대로 붙어 보고, 쓰기를 받는 서버가 나올 때까지 넘어갑니다. 가상 IP 는 keepalived 가 VRRP 로 한 서버에만 주소를 올립니다. 이때 조건 검사(track_script) 로 Patroni 의 /primary(리더만 200)를 물어야 합니다. 조건 없이 우선순위만 두면 VIP 는 리더가 아니라 우선순위가 높은 서버에 머물고, 그 서버가 대기 서버이면 앱의 쓰기가 전부 실패합니다. 그리고 PostgreSQL 이 VIP 주소에서도 듣고 있어야 합니다. 자기 주소에서만 들으면 VIP 가 와도 Connection refused 입니다.
현장에서 만나는 모습
운영에서 Patroni 장애의 대부분은 Patroni 자체가 아니라 그 둘레에서 납니다. etcd 가 디스크 지연으로 리더 선거를 반복하거나, pg_hba 에 로컬 복제 줄이 빠져 옛 리더가 돌아오지 못하거나, VIP 가 조건 검사 없이 붙어 있는 식입니다. 그래서 구축 뒤에 반드시 한 번 리더를 떼어 내 보고, 돌아온 옛 리더가 대기 서버로 붙는 것까지 확인합니다. 그 확인이 이 실습의 6단계와 7단계입니다.
다음 실습에서 할 것
세 서버의 etcd 를 하나로 묶고, db1 과 db2 에 Patroni 를 올려 리더와 대기 서버를 세웁니다. 동기 모드를 켜고, 다중 호스트 연결 문자열을 만든 뒤, 리더를 세션망에서 떼어 내 새 리더가 서는 것과 옛 리더가 스스로 내려오는 것을 봅니다. 선을 다시 이어 옛 리더가 되감겨 돌아오게 하고, 마지막으로 keepalived 로 리더를 따라다니는 가상 IP 를 답니다.