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

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

죽었는지 끊겼는지 모를 때: split-brain 과 펜싱

TT Lab 에서 이어서 보기

한 줄 요약

대기 서버는 주 서버가 죽었는지, 자기와의 선만 끊겼는지 구별하지 못합니다. 그 상태에서 승격하면 쓰기를 받는 서버가 둘이 되고(split-brain), 두 서버에 들어간 데이터는 합쳐지지 않습니다. 펜싱은 새 주 서버를 세우기 전에 옛 주 서버가 더는 쓰기를 받지 못하게 만드는 일입니다.

왜 이게 필요했나

장애 조치를 자동화하는 가장 단순한 스크립트는 이렇게 생겼습니다. "대기 서버에서 주 서버에 닿지 않으면 승격한다." 주 서버가 정말 죽었다면 맞는 판단입니다. 그런데 스위치 하나, 방화벽 규칙 하나 때문에 두 서버 사이만 끊긴 것이라면 어떻게 될까요. 주 서버는 멀쩡히 살아서 앱의 쓰기를 계속 받고, 대기 서버는 승격해서 다른 앱 서버의 쓰기를 받습니다. 몇 분 뒤 선이 이어지면 두 서버는 같은 표에 서로 다른 행을 갖고 있고, 어느 쪽도 상대의 역사를 받아들일 수 없습니다.

이것이 split-brain 입니다. 문제는 데이터 손상이 아니라 판정 불가능입니다. 주문 번호 1001 이 양쪽에 다른 고객으로 들어가 있으면, 어느 것이 진짜인지 데이터베이스는 알려 주지 않습니다.

어떻게 동작하나

끊김을 알아채는 시간. 대기 서버의 walreceiver 는 wal_receiver_timeout(기본 60초) 동안 아무것도 못 받으면 연결을 포기하고 로그에 terminating walreceiver due to timeout 을 남깁니다. 주 서버의 walsender 도 wal_sender_timeout 이 지나면 terminating walsender process due to replication timeout 을 남깁니다. 서버가 깨끗이 내려갔다면 대기 서버의 로그는 다릅니다(replication terminated by primary server). 로그 한 줄로 죽은 것과 끊긴 것을 사후에 가를 수 있다는 뜻이고, 동시에 그 순간에는 가를 수 없다는 뜻입니다.

두 주 서버. 대기 서버를 승격하면 새 타임라인으로 갈라집니다. 옛 주 서버는 아무것도 모른 채 옛 타임라인에서 계속 씁니다. 두 서버 모두 pg_is_in_recovery() 가 false 입니다.

펜싱. 새 주 서버가 쓰기를 받기 전에 옛 주 서버를 확실히 막는 방법들입니다.

방법 하는 일 약점
전원(STONITH) IPMI·하이퍼바이저로 서버를 끈다 관리망까지 끊기면 끄지 못한다
네트워크 스위치·방화벽에서 앱과 그 서버 사이를 막는다 막는 장치에 닿아야 한다
스스로 내려오기 합의 저장소(etcd 등)에 닿지 못하면 주 서버가 스스로 읽기 전용이 된다 시간 제한(TTL)만큼 늦다

핵심은 순서입니다. 막고 나서 올린다. 막을 수 없으면 올리지 않는 것이 맞습니다. 쓰기를 잠시 못 받는 것은 장애지만, 두 서버가 각자 쓰는 것은 사고입니다.

얼마나 갈라졌는가. 두 주 서버가 함께 쓴 시간이 길수록 꺼낼 행이 많아집니다. 갈림 지점은 새 주 서버의 history 파일에 LSN 으로 적혀 있고, 옛 주 서버에서 그 뒤에 커밋된 트랜잭션이 정리 대상입니다. 표에 생성 시각이나 증가하는 id 가 있으면 그것으로 범위를 좁히고, 없으면 pg_waldump 로 WAL 을 직접 읽어야 해서 일이 몇 배로 커집니다. 업무 표에 생성 시각을 두는 습관이 이런 날에 값을 합니다.

사고 뒤의 정리. 이미 두 주 서버가 생겼다면 순서가 정해져 있습니다. 먼저 옛 주 서버를 펜싱해 더 쓰지 못하게 하고, 갈라진 뒤 그 서버에만 들어온 행을 꺼내 두고, pg_rewind 로 되감아 새 주 서버의 대기 서버로 붙이고, 꺼내 둔 행을 업무 규칙에 따라 새 주 서버에 다시 넣습니다. 되감기가 그 행들을 지우기 때문에 꺼내는 일이 반드시 앞에 와야 합니다. 다시 넣을 때 id 는 새로 받습니다. 두 서버의 시퀀스가 따로 움직였기 때문입니다.

자동화 스크립트가 지켜야 할 세 줄

손으로 쓴 장애 조치 스크립트를 쓸 수밖에 없다면 적어도 세 가지를 지킵니다. 첫째, 판단은 한 곳에서만 합니다. 대기 서버마다 스스로 승격을 결정하게 두면 둘이 동시에 승격할 수 있습니다. 둘째, 승격 전에 펜싱이 성공했는지 확인하고, 확인하지 못하면 멈춥니다. 셋째, 옛 주 서버가 다시 켜졌을 때 스스로 주 서버로 뜨지 않게 합니다. 재부팅 한 번으로 두 번째 split-brain 이 생기는 일이 실제로 있습니다. 이 세 줄을 사람 대신 지켜 주는 것이 다음 모듈의 Patroni 입니다.

현장에서 만나는 모습

데이터센터 사이 회선이 흔들려 양쪽에서 동시에 승격이 일어난 사례가 많이 보고됐고, 그래서 요즘의 HA 도구는 전부 다수결을 씁니다. 세 표 중 두 표를 얻은 쪽만 주 서버가 될 수 있고, 표를 잃은 주 서버는 스스로 내려옵니다. 두 대만으로 HA 를 만들려는 설계에는 세 번째 표(증인)를 어디에 둘 것인가라는 질문이 늘 따라붙습니다. 이 실습의 witness 가 그 자리이고, 다음 모듈에서 실제로 표를 던집니다.

다음 실습에서 할 것

db1 과 db2 사이만 끊고 db2 의 로그에서 타임아웃을 확인한 뒤, 자동화 스크립트가 하듯 db2 를 승격합니다. 두 서버에 각각 주문을 넣어 두 주 서버를 직접 만들고, db1 을 네트워크로 펜싱하고, db1 에만 들어간 주문을 꺼낸 뒤, 되감아 붙이고, 꺼낸 주문을 db2 에 다시 넣습니다.