3.2 클러스터 탄생
빈 monitor에 첫 노드를 등록한 뒤 두 번째 노드를 추가합니다. 그러면 두 노드는 여러 상태를 거쳐 primary와 secondary에 도착합니다. 이 과정은 이후 모든 전이의 축소판입니다. monitor는 목표를 한 단계씩 바꿉니다. 한쪽 노드의 전이가 끝나야 다른 쪽 노드에 다음 목표를 부여합니다. 이 장에서는 실습 run1에서 기록한 이벤트와 keeper 로그를 바탕으로 그 순서를 살펴봅니다.
전체 흐름
첫 노드: init에서 single로
첫 노드는 init 상태로 등록됩니다. 같은 formation과 group에 다른 노드가 없으면 monitor는 목표를 single로 설정합니다. keeper는 init에서 single로 전이하면서 PostgreSQL을 초기화합니다. 실습 로그에는 모든 단계가 기록되어 있습니다.
05:55:57 10 INFO Registered node 1 "node1" (node1:5432) in formation "default", group 0, state "single"
05:55:57 10 INFO FSM transition from "init" to "single": Start as a single node
05:55:57 10 INFO Initialising a PostgreSQL cluster at "/var/lib/pgaf/data"
05:55:57 10 INFO /usr/lib/postgresql/17/bin/pg_ctl initdb -s -D /var/lib/pgaf/data --option '--auth=trust'
05:55:57 10 INFO Granting connection privileges on 172.28.0.0/24
05:55:57 10 INFO CREATE DATABASE appdb;
05:55:57 10 INFO CREATE EXTENSION pg_stat_statements;
05:55:57 10 INFO Disabling synchronous replication
05:55:58 10 INFO Transition complete: current state is now "single"
single은 복제가 없는 일반적인 PostgreSQL입니다. failover 대상도 없습니다. 실습에서 up부터 single 도달까지 2.0초가 걸렸습니다.
두 번째 노드: wait_standby와 wait_primary
두 번째 노드가 등록되면 monitor는 한 번의 결정으로 두 노드의 목표를 바꿉니다. 새 노드의 목표는 wait_standby이고, 기존 노드의 목표는 wait_primary입니다. 새 노드는 primary가 복제 연결을 허용할 때까지 기다립니다. 기존 노드는 복제 연결을 허용하기 위해 HBA 규칙을 추가합니다. 다음은 run1의 이벤트입니다.
05:05:42.907344 | 0/1 | single | wait_primary | Setting goal state of node 1 "node1" (node1:5432) to wait_primary after node 2 "node2" (node2:5432) joined.
05:05:42.912575 | 0/2 | wait_standby | wait_standby | New state is reported by node 2 "node2" (node2:5432): "wait_standby"
05:05:42.987913 | 0/1 | wait_primary | wait_primary | New state is reported by node 1 "node1" (node1:5432): "wait_primary"
05:05:43.00422 | 0/2 | wait_standby | catchingup | Setting goal state of node 2 "node2" (node2:5432) to catchingup after node 1 "node1" (node1:5432) converged to wait_primary.
after node 1 converged to wait_primary라는 사유에서 한 단계씩 진행하는 원칙을 확인할 수 있습니다. node1이 전이 완료를 보고한 뒤에야 node2에 catchingup이 부여되었습니다.
catchingup: pg_basebackup
wait_standby에서 catchingup으로 전이할 때 keeper는 pg_basebackup으로 데이터를 복사합니다. 그런 다음 standby로 기동합니다. --slot으로 replication slot을 지정하므로 복사하는 동안 WAL이 사라지지 않습니다.
05:05:43 10 INFO FSM transition from "wait_standby" to "catchingup": The primary is now ready to accept a standby
05:05:43 10 INFO /usr/lib/postgresql/17/bin/pg_basebackup -w -d 'application_name=pgautofailover_standby_2 host=node1 port=5432 user=pgautofailover_replicator sslmode=require' --pgdata /var/lib/pgaf/backup/node_2 -U pgautofailover_replicator --verbose --progress --max-rate 100M --wal-method=stream --slot pgautofailover_standby_2
05:05:43 10 INFO pg_basebackup: initiating base backup, waiting for checkpoint to complete
05:07:18 10 INFO pg_basebackup: checkpoint completed
05:07:18 10 INFO 30870/30870 kB (100%), 1/1 tablespace
05:07:19 10 INFO Creating the standby signal file at "/var/lib/pgaf/data/standby.signal", and replication setup at "/var/lib/pgaf/data/postgresql-auto-failover-standby.conf"
05:07:19 10 INFO Transition complete: current state is now "catchingup"
복사할 데이터는 30 MB였지만 checkpoint 대기에 95초가 걸렸습니다. pg_autoctl은 --checkpoint=fast를 지정하지 않습니다. 따라서 primary의 spread checkpoint가 끝날 때까지 기다립니다. primary에서 CHECKPOINT를 먼저 실행한 뒤 같은 절차를 반복했습니다. 그 결과 합류 전체 시간이 3.5초로 줄었습니다. 대기 시간은 dirty buffer 양과 checkpoint 설정에 따라 달라집니다. 따라서 3.5초를 일반적인 값으로 볼 수는 없습니다.
데이터는 backup_directory(기본 ${PGDATA}/../backup/<node>)에 복사한 뒤 PGDATA 위치로 옮깁니다. 설정 문서에 따르면 이 이동이 원자적이려면 두 경로가 같은 파일시스템에 있어야 합니다.
basebackup이 실행되는 동안 monitor는 node2를 unhealthy로 표시했습니다(05:05:52 ... marked as unhealthy). 새 노드에는 PostgreSQL이 아직 없으므로 health check가 실패했습니다. node2는 아직 catchingup 상태도 아니며 승격 후보도 아닙니다. 따라서 이 표시는 동작에 영향을 주지 않았습니다.
secondary와 primary
standby가 기동한 뒤 primary를 따라잡습니다. 그러면 monitor는 node2를 secondary로 전환합니다. node2의 보고를 받은 뒤에는 node1을 primary로 전환합니다.
05:07:20.214934 | 0/2 | catchingup | catchingup | Node node 2 "node2" (node2:5432) is marked as healthy by the monitor
05:07:20.235884 | 0/2 | catchingup | secondary | Setting goal state of node 2 "node2" (node2:5432) to secondary after it caught up.
05:07:20.299383 | 0/2 | secondary | secondary | New state is reported by node 2 "node2" (node2:5432): "secondary"
05:07:20.318322 | 0/1 | wait_primary | primary | Setting goal state of node 1 "node1" (node1:5432) to primary now that we have 1 healthy secondary nodes in the quorum.
05:07:20.411875 | 0/1 | primary | primary | New state is reported by node 1 "node1" (node1:5432): "primary"
wait_primary에서 primary로 전이할 때 동기 복제를 활성화합니다. 이 시점부터 node1의 synchronous_standby_names는 ANY 1 (pgautofailover_standby_2)입니다. node2의 keeper는 catchingup에서 secondary로 전이합니다. 이때 다른 노드용 replication slot(pgautofailover_standby_1)도 미리 만듭니다. 나중에 node2가 primary가 되면 node1은 이 slot으로 연결됩니다.
05:07:20 10 INFO Reached timeline 1, same as upstream node 1 "node1" (node1:5432)
05:07:20 10 INFO Creating replication slot "pgautofailover_standby_1"
세 번째 노드부터
세 번째 노드가 합류할 때 primary는 이미 primary 상태입니다. monitor는 primary를 wait_primary로 변경하지 않습니다. 대신 apply_settings로 전환하여 synchronous_standby_names만 바꿉니다. number_sync_standbys도 0에서 1로 자동 증가합니다(5.1).
05:31:28.902|node3|wait_standby|catchingup|Setting goal state of node 3 "node3" (node3:5432) to catchingup and node 2 "node2" (node2:5432) to primary to edit synch
05:31:28.902|node2|primary|apply_settings|Setting goal state of node 3 "node3" (node3:5432) to catchingup and node 2 "node2" (node2:5432) to primary to edit synch
05:31:30.381|node3|secondary|secondary|New state is reported by node 3 "node3" (node3:5432): "secondary"
v2.2 문서에는 기존의 join_primary 상태를 폐기했다고 나옵니다. 지금은 새 standby가 합류할 때 join_primary 대신 primary나 apply_settings를 사용합니다.
정리
탄생 과정은 single에서 시작합니다. 이후 wait_primary와 wait_standby, catchingup을 거쳐 primary와 secondary로 끝납니다. 각 단계는 상대 노드의 수렴을 기다립니다. 따라서 한 노드의 전이가 늦으면 전체 과정도 늦어집니다. 실습에서 가장 오래 걸린 단계는 네트워크 복사가 아니었습니다. pg_basebackup의 checkpoint 대기가 가장 오래 걸렸습니다.