본문으로 건너뛰기

3.4 multi-standby 선출

standby가 둘 이상이면 failover 전에 승격할 노드를 정해야 합니다. monitor는 이미 모든 노드가 보고한 LSN을 갖고 있지만, 몇 초 전의 값입니다. 따라서 선출을 시작하면 모든 standby에 "지금 위치를 다시 보고하라"는 목표를 줍니다. 그 결과를 바탕으로 후보를 고릅니다. 이 장에서는 해당 단계인 report_lsn을 다룹니다. 후보가 가장 앞선 노드가 아닐 때 사용하는 fast_forward와 나머지 노드가 사용하는 join_secondary도 살펴봅니다.

선출 흐름​

report_lsn 상태에 들어간 standby는 primary_conninfo 없이 PostgreSQL을 재시작해 복제 스트림을 끊습니다. 문서에 따르면 이렇게 하면 옛 primary의 pg_stat_replication에서 standby 연결이 사라집니다. 그러면 옛 primary가 고립을 감지할 수 있습니다(2.5). 각 standby는 WAL 수신을 끊어 자기 LSN을 고정합니다. 고정된 LSN은 노드마다 다릅니다. 이 차이를 비교하기 위해 report_lsn을 사용합니다.

후보 선정 규칙​

v2.2 group_state_machine.c의 선정 과정은 다음과 같습니다.

  1. report_lsn 보고를 모읍니다. 보고 대상인 eligible standby가 모두 보고할 때까지 기다립니다(missingNodesCount가 0). 또한 replication quorum에 참여하는 후보가 number_sync_standbys + 1대 이상이어야 선정을 시작합니다. 부족하면 "Failover still in progress" 이벤트를 남기고 기다립니다.
  2. candidate priority가 0인 노드와 unhealthy 노드는 제외합니다. 가장 앞선 standby도 옛 primary의 마지막 LSN보다 pgautofailover.promote_wal_log_threshold(기본 16MB) 이상 뒤처질 수 있습니다. 이 경우 후보를 고르지 않고 failover를 멈춥니다(SelectFailoverCandidateNode).
  3. candidate priority가 가장 높은 노드 중 LSN이 가장 앞선 노드를 고릅니다.
  4. 선택한 노드의 LSN이 전체 최대값보다 뒤처지면 추가 확인을 합니다. 최대 LSN을 가진 노드 중 하나가 healthy인지 확인하고 fast_forward를 지정합니다.

candidate priority가 LSN보다 우선합니다. priority가 높은 노드가 조금 뒤처져 있어도 해당 노드를 고릅니다. 가장 앞선 노드가 따로 있어도 마찬가지입니다. 부족한 WAL은 fast_forward로 채웁니다. 문서에 따르면 priority와 LSN이 모두 같으면 임의의 노드를 고릅니다.

노트

1번 조건 때문에 number_sync_standbys = 1인 3노드 구성에서는 선정이 시작되지 않을 수 있습니다. standby 둘 중 하나가 이미 죽은 상태에서 primary 장애가 발생한 경우입니다. quorum 후보가 한 대만 보고하기 때문입니다. 이는 새 primary의 쓰기를 동기로 확인할 standby가 없다는 뜻입니다. 따라서 의도된 동작입니다. 이 노트에서는 이 조합을 실측하지 않았습니다.

실측: 3노드, 동률​

run2의 3노드 구성에서 primary(node2)를 kill했습니다. 모든 priority는 50이었습니다.

05:31:55.938|node2|primary|primary|Node node 2 "node2" (node2:5432) is marked as unhealthy by the monitor
05:32:08.911|node2|primary|draining|Setting goal state of node 2 "node2" (node2:5432) to draining after it became unhealthy.
05:32:08.911|node1|secondary|report_lsn|Setting goal state of node 1 "node1" (node1:5432) to report_lsn to find the failover candidate
05:32:08.911|node3|secondary|report_lsn|Setting goal state of node 3 "node3" (node3:5432) to report_lsn to find the failover candidate
05:32:11.338|node3|report_lsn|report_lsn|New state is reported by node 3 "node3" (node3:5432) with LSN 0/9004980: report_lsn
05:32:11.651|node1|report_lsn|report_lsn|New state is reported by node 1 "node1" (node1:5432) with LSN 0/9004980: report_lsn
05:32:11.651|node1|report_lsn|prepare_promotion|Setting goal state of node 1 "node1" (node1:5432) to prepare_promotion after node 2 "node2" (node2:5432) became unhealth
05:32:12.760|node1|prepare_promotion|stop_replication|Setting goal state of node 2 "node2" (node2:5432) to demote_timeout and node 1 "node1" (node1:5432) to stop_replication
05:32:12.781|node3|report_lsn|join_secondary|Setting goal state of node 3 "node3" (node3:5432) to join_secondary after node 1 "node1" (node1:5432) got selected as th
05:32:43.161|node1|stop_replication|wait_primary|Setting goal state of node 1 "node1" (node1:5432) to wait_primary and node 2 "node2" (node2:5432) to demoted a
05:32:43.252|node3|join_secondary|secondary|Setting goal state of node 3 "node3" (node3:5432) to secondary after node 1 "node1" (node1:5432) converged to wait_prima
05:32:43.562|node1|wait_primary|primary|Setting goal state of node 1 "node1" (node1:5432) to primary now that we have 1 healthy secondary nodes in th

두 standby의 LSN은 0/9004980으로 같았으며 node1이 선택되었습니다. report_lsn 단계에는 2.7초가 걸렸습니다. 2노드보다 이 시간만큼 더 걸렸습니다. kill부터 새 primary의 첫 쓰기 성공까지는 54.3초였습니다. node3은 join_secondary에서 기다렸습니다. node1이 wait_primary에 도달한 직후 secondary로 돌아갔습니다. node1은 곧바로 primary가 되어 동기 복제를 켰습니다.

실측: fast_forward​

run3에서는 node3의 candidate priority를 100으로 올렸습니다. 이어서 node3에서 primary(node2)로 가는 경로만 blackhole route로 막아 WAL 수신을 멈췄습니다. 그 상태에서 쓰기를 실행하고 primary를 kill했습니다.

Name | Node | Host:Port | TLI: LSN | Connection | Reported State | Assigned State
node1 | 1 | node1:5432 | 4: 0/5003138 | read-only | secondary | secondary
node2 | 2 | node2:5432 | 4: 0/5003138 | read-write | primary | primary
node3 | 4 | node3:5432 | 4: 0/50000F8 | read-only | secondary | secondary
06:14:48.829|node1|report_lsn|report_lsn|New state is reported by node 1 "node1" (node1:5432) with LSN 0/5003138: report_lsn
06:14:49.021|node3|report_lsn|report_lsn|New state is reported by node 4 "node3" (node3:5432) with LSN 0/50000F8: report_lsn
06:14:49.021|node3|report_lsn|fast_forward|Setting goal state of node 4 "node3" (node3:5432) to fast_forward after node 2 "node2" (node2:5432) became unh
06:14:50.486|node3|fast_forward|prepare_promotion|Setting goal state of node 4 "node3" (node3:5432) to prepare_promotion
06:14:51.571|node1|report_lsn|join_secondary|Setting goal state of node 1 "node1" (node1:5432) to join_secondary after node 4 "node3" (node3:5432) got selected as th
06:15:21.404|node3|stop_replication|wait_primary|Setting goal state of node 4 "node3" (node3:5432) to wait_primary and node 2 "node2" (node2:5432) to demoted a

LSN이 더 앞선 node1(0/5003138) 대신 priority 100인 node3(0/50000F8)이 선택되었습니다. node3은 fast_forward를 1.5초 거친 뒤 승격되었습니다. keeper 로그에는 이 단계의 목적이 기록되어 있습니다.

06:14:50 10 INFO FSM transition from "report_lsn" to "fast_forward": Fetching missing WAL bits from another standby before promotion

승격 후 ticks 테이블의 max(id)는 node3과 node1 모두 1063으로 같았습니다. node3이 node1에만 있던 WAL을 cascading replication으로 받은 결과입니다.

경고

fast_forward를 실행하려면 가장 앞선 standby 중 하나가 살아 있어야 합니다. 해당 노드가 모두 unhealthy면 monitor는 "The selected candidate ... needs to fetch missing WAL" 이벤트를 남기고 진행을 멈춥니다. candidate priority의 차이를 크게 설정한 구성일수록 이 조건에 해당할 가능성이 높습니다.

priority 0의 의미​

candidate priority가 0인 노드는 자동 선출에서 항상 제외됩니다. v2.2 문서에는 priority가 0이 아닌 노드가 최소 두 대 있어야 한다고 적혀 있습니다. 하지만 이 제한은 v1.5.2에서 풀렸습니다. 현재 monitor는 두 대 미만이어도 NOTICE만 남기고 설정을 받아들입니다. 자동 failover 가능 여부는 장애 시점에 남은 standby에 달려 있습니다. priority가 0보다 큰 후보가 있어야 합니다. priority 0 노드의 수동 승격(pg_autoctl perform promotion)도 v1.5.2 변경 사항입니다(CHANGELOG #661).

정리​

multi-standby 선출은 report_lsn으로 각 standby의 LSN을 고정해 모읍니다. 그런 다음 priority, LSN 순으로 후보를 고릅니다. priority가 LSN보다 우선하므로 뒤처진 후보는 fast_forward로 WAL을 채운 뒤 승격됩니다. 실측에서 이 단계들은 2~3초만 추가했습니다. 전체 시간은 여전히 primary_demote_timeout 30초가 좌우했습니다.