pglookout 2.0.3 常用 CLI 命令与工具详解

把 pglookout 2.0.3 的运行期管理工具链一次讲全:主进程的启动/热加载/日志、主节点判定辅助命令 `pglookout_current_master`、HTTP 状态 API(`GET /state.json`、`POST /check`)、状态文件与三类告警文件、故障转移外部命令脚本,并附一次三节点集群完整故障转移演练实测(停主 → 120 秒保护期后备库自动 promote 接管 → 旧主脑裂告警 → `pg_basebackup` 重建旧主回备)。全部命令输出为真实回显,报错与退出码均来自真实触发,已在 Ubuntu 24.04 + PostgreSQL 16.15 三节点集群实测通过。

pglookoutpglookoutPostgreSQL监控2026/11/01

pglookout 2.0.3 常用 CLI 命令与工具详解

目标:把 pglookout 2.0.3 的运行期管理工具链一次讲全:主进程的启动/热加载/日志、主节点判定辅助命令 pglookout_current_master、HTTP 状态 API(GET /state.json、POST /check)、状态文件与三类告警文件、故障转移外部命令脚本,并附一次三节点集群完整故障转移演练实测(停主 → 120 秒保护期后备库自动 promote 接管 → 旧主脑裂告警 → pg_basebackup 重建旧主回备)。全部命令输出为真实回显,报错与退出码均来自真实触发,已在 Ubuntu 24.04 + PostgreSQL 16.15 三节点集群实测通过。

一、环境信息

本文所有命令与回显均在以下环境实测(版本复核日期 2026-11-01,服务器时区 UTC,日志时间戳均为 UTC):

项目值
操作系统Ubuntu 24.04 LTS(x86_64)
pglookout2.0.3(GitHub main 源码,venv 安装于 /opt/pglookout/venv,命令链接至 /usr/local/bin)
PostgreSQL16.15(Ubuntu 16.15-0ubuntu0.24.04.1)
配置文件/etc/pglookout/pglookout.json(每节点一份,位置参数传入)
状态文件/var/run/pglookout/pglookout_state.json(json_state_file_path)
告警目录/var/run/pglookout(alert_file_dir)
运行日志/var/log/postgresql/pglookout.log(进程管理器重定向)
HTTP 状态服务每节点监听 15000 端口(http_port)
集群拓扑主 p1(10.89.60.11,主机名 pg-primary)/ 备 s1(10.89.60.12)/ 观察 obs1(10.89.60.13)
监控账号pglookout(密码 q1w2e3r4,需 pg_monitor 角色 + pg_read_binary_file 授权)
复制账号replicator(密码 q1w2e3r4,REPLICATION 属性)
故障转移脚本/opt/pglookout/failover.sh(failover_command,内部执行 pg_ctl promote)

集群部署过程见 《pglookout 2.0.3 Docker Compose 部署指南》;参数键的语义与取值边界见 《pglookout 2.0.3 配置参数详解》;组件定位与选型见 《pglookout 介绍与选型指南》。三篇分工一句话:选型看 A1、部署看 B2、参数看 C1、进程怎么管和故障怎么演看本文。

二、工具概览

pglookout 的 CLI 体系非常收敛——只有两个二进制、没有子命令,全部行为由配置文件驱动,运行期管理靠"文件 + HTTP"完成:

工具角色本文章节
pglookout主命令:启动监控/故障转移守护进程,唯一位置参数是配置文件,支持 SIGHUP 热加载4.1
pglookout_current_master辅助命令:从状态文件读出当前主节点名,退出码可直接写进脚本4.2
HTTP 状态 API运行期查询接口:GET /state.json 查集群状态、POST /check 触发立即检查4.3
状态文件/var/run/pglookout/pglookout_state.json:判定结果与辅助命令的数据源4.4
告警文件failover_has_happened / multiple_master_warning / replication_delay_warning4.5
外部命令脚本failover_command / pg_start_command / pg_stop_command,真正执行切换动作的"手"4.6

💡 说明:pglookout 没有"打印当前生效配置"或"手动触发切换"的子命令。想让备库立即提升,唯一路径是让主库真的失联并等过 failover_timeout 保护期(见 4.7 演练);想核对运行期视点,读状态文件或调 HTTP 接口(4.3/4.4)。

三、前置条件

  1. 三节点集群已按部署篇搭好、配置篇调好参数,三个节点上 pglookout 进程都在运行。确认方法(主节点 p1 实测回显):
ps -eo pid,args | grep [p]glookout
      1 /opt/pglookout/venv/bin/python3 /usr/local/bin/pglookout /etc/pglookout/pglookout.json
     53 postgres: 16/main: pglookout postgres 10.89.60.11(41120) idle
     59 postgres: 16/main: pglookout postgres 10.89.60.13(53820) idle
     62 postgres: 16/main: pglookout postgres 10.89.60.12(32866) idle

第一行是 pglookout 主进程本身;后三行是它(以及备节点、观察节点的 pglookout)连进本库的监控会话——三个节点各有一条 pglookout 用户连接是正常形态。

  1. 节点上有 curl、python3、psql;操作用户可读 /var/run/pglookout/ 与 /var/log/postgresql/pglookout.log。
  2. 本文 4.7 演练会真的停掉主库并触发一次不可逆的故障转移(切换后旧主需重建),务必在测试环境执行,并提前确认 failover.sh 内容(4.6)。

四、各工具详解

4.1 pglookout 主命令:启动、热加载与日志

用法(--help 实测回显):

usage: pglookout [-h] [--version] config

postgresql replication monitoring and failover daemon

positional arguments:
  config      configuration file

options:
  -h, --help  show this help message and exit
  --version   show program version
pglookout --version
2.0.3

启动:唯一位置参数就是配置文件路径,进程以前台方式运行,生产环境交给进程管理器托管(本验证环境无 systemd,重启路径以 supervisord 等等价执行):

pglookout /etc/pglookout/pglookout.json

启动日志(log_level: DEBUG,演练后重启 p1 节点 pglookout 的实测回显,节选):

2026-11-01 09:37:17,151	pglookout	MainThread	DEBUG	Loading JSON config from: '/etc/pglookout/pglookout.json'
2026-11-01 09:37:17,151	ClusterMonitor	MainThread	DEBUG	Initialized ClusterMonitor with: {}
2026-11-01 09:37:17,152	WebServer	MainThread	DEBUG	WebServer initialized with address: '0.0.0.0' port: 15000
2026-11-01 09:37:17,152	pglookout	MainThread	INFO	PGLookout initialized, local hostname: 'pg-primary', own_db: 'p1', cwd: '/'
2026-11-01 09:37:17,152	pglookout	MainThread	WARNING	No cluster state: {}, probably still starting up, node_count: 0, configured node_count: 2
2026-11-01 09:37:17,170	pglookout	MainThread	INFO	Immediate failover check completed
2026-11-01 09:37:17,170	pglookout	MainThread	INFO	New master node detected: old: None new: 's1': {'fetch_time': '2026-11-01T09:37:17.091326Z', 'connection': True, 'db_time': '2026-11-01T09:37:17.091419Z', 'pg_is_in_recovery': False, 'pg_last_xact_replay_timestamp': None, 'pg_last_xlog_receive_location': None, 'pg_last_xlog_replay_location': '0/B000670', 'replication_time_lag': None, 'min_replication_time_lag': 0.039022, 'replication_slots': []}

三个要点:

  • 日志格式为 时间戳\t模块\t线程\t级别\t消息,制表符分隔,可直接 awk -F'\t' '$4=="WARNING"' 过滤;
  • 进程启动后第一个决策周期就会完成一次 failover check(Immediate failover check completed),并从状态里认出新主(本例 old: None new: 's1',因为此时集群已完成切换);
  • local hostname 是 socket.gethostname() 的结果,own_db 取自配置,核对这两个值是排障第一步——own_db 填错会让节点认错自己。

热加载:改完配置发 SIGHUP 即可,无需重启:

kill -HUP $(pidof pglookout)

信号只置"待重载"标志,实际重读发生在下一个决策周期边界(replication_state_check_interval,默认 10 秒),日志时间戳比发信号晚 0~10 秒属正常;参数级行为详见 《pglookout 2.0.3 配置参数详解》 的热加载一节。

日志检索:

tail -f /var/log/postgresql/pglookout.log          # 实时跟踪
grep -E 'failover|master' /var/log/postgresql/pglookout.log | tail -20   # 只看切换/主节点判定

4.2 pglookout_current_master:主节点判定辅助命令

用法(--help 实测回显):

usage: pglookout_current_master [-h] [--version] state

postgresql replication monitoring and failover daemon

positional arguments:
  state       pglookout state file

options:
  -h, --help  show this help message and exit
  --version   show program version

⚠️ 帮助文本有误导:位置参数虽叫 state,但实际要传的是配置文件路径——源码(pglookout/current_master.py)先把它当 JSON 读,取出 json_state_file_path 键,再去读真正的状态文件。传状态文件本身会因取不到 json_state_file_path 而走默认值 /tmp/pglookout_state.json,实测直接退出码 255(见六 Q3)。

正常输出(演练基线时在主节点 p1 实测):

pglookout_current_master /etc/pglookout/pglookout.json; echo $?
p1
0

故障转移完成后,在备节点(此时已是新主)与观察节点上实测,返回的都是新主名:

s1
exit=0

退出码语义(源码 + 实测):

退出码含义实测回显
0正常,stdout 输出 current_master 值p1 / s1
1传入的配置文件不存在pglookout_current_master: '/etc/pglookout/nope.json' doesn't exist
255(即 -1)状态文件超过 60 秒未更新(pglookout 进程大概率已死),或任何解析失败无输出,exit=255

脚本用法(应用侧路由/巡检脚本的标准写法):

MASTER=$(pglookout_current_master /etc/pglookout/pglookout.json) \
  || { echo "pglookout 视点异常(进程未运行或状态文件过期)"; exit 1; }
if [ "$(hostname -s)" = "$(echo "$MASTER" | tr -d \"'\")" ]; then
  echo "本节点是主库"
fi

4.3 HTTP 状态 API:/state.json 与 /check

每个节点的 pglookout 都在 http_port(默认 15000)上提供一个简易 HTTP 服务,共两个可用端点。

GET /state.json——返回该节点视点的裸集群状态字典(键为节点名):

curl -s http://10.89.60.13:15000/state.json | python3 -c "import json,sys; d=json.load(sys.stdin); print('top keys:', sorted(d.keys()))"
top keys: ['p1', 's1']

演练终态实测(观察节点视点):

s1 conn= True recovery= False
p1 conn= True recovery= True

⚠️ 与磁盘状态文件结构不同:HTTP 端点直接吐出 cluster_state 字典(只有各 DB 节点的采集值),没有 current_master、observer_nodes 这些顶层键;顶层视点信息只在状态文件里(4.4)。写采集脚本别把两者当同一个结构。

POST /check——触发一次立即状态检查 + 故障转移检查,返回 204 No Content:

curl -s -o /dev/null -w '%{http_code}\n' -X POST http://10.89.60.13:15000/check
204

对应节点日志(实测回显):

2026-11-01 09:28:31,877	WebServer	Thread-479 (process_request_thread)	INFO	Immediate status check requested
10.89.60.1 - - [01/Nov/2026 09:28:31] "POST /check HTTP/1.1" 204 -
2026-11-01 09:28:31,880	pglookout	MainThread	INFO	Immediate failover check completed

GET /check 是一个必须避开的坑:健康检查用 GET 会挂——源码的 do_GET 只实现了 /state.json,其他路径回 404,且 404 分支不调用 end_headers,客户端会一直等响应体直到超时:

curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://10.89.60.13:15000/check
000

(000 即 curl 未收到完整响应。)而节点访问日志里明明有 404 记录:

10.89.60.1 - - [01/Nov/2026 09:28:09] "GET /check HTTP/1.1" 404 -

结论:健康检查必须用 POST /check(204 即健康),不要用 GET。

4.4 状态文件:pglookout_state.json

路径由 json_state_file_path 决定,本环境为 /var/run/pglookout/pglookout_state.json。每个决策周期由主进程重写一次,结构为三层:db_nodes(各 DB 节点采集值)、observer_nodes(观察节点视点)、current_master(当前主节点名)。

实测回显(演练终态,观察节点,提取命令见下):

python3 -c "import json; d=json.load(open('/var/run/pglookout/pglookout_state.json')); print('current_master:', d['current_master']); print('db_nodes:', d.get('db_nodes')); print('observer_nodes:', d.get('observer_nodes'))"
current_master: s1
db_nodes: {'s1': {'fetch_time': '2026-11-01T09:42:07.189171Z', 'connection': True, 'db_time': '2026-11-01T09:42:07.189262Z', 'pg_is_in_recovery': False, 'pg_last_xact_replay_timestamp': None, 'pg_last_xlog_receive_location': None, 'pg_last_xlog_replay_location': '0/B0025A0', 'replication_time_lag': None, 'min_replication_time_lag': 0.039022, 'replication_slots': []}, 'p1': {'fetch_time': '2026-11-01T09:42:07.189040Z', 'connection': True, 'db_time': '2026-11-01T09:42:07.189155Z', 'pg_is_in_recovery': True, 'pg_last_xact_replay_timestamp': '2026-11-01T09:42:03.009041Z', 'pg_last_xlog_receive_location': '0/B0025A0', 'pg_last_xlog_replay_location': '0/B0025A0', 'replication_slots': [], 'replication_time_lag': 4.180114, 'min_replication_time_lag': 2.521524}}
observer_nodes: {}

读法要点:

  • current_master 是全集群视点,pglookout_current_master 命令读的就是这个键;
  • 每个节点的 connection 表示"该视点采集时能否连上",pg_is_in_recovery 为 False 即主库身份;终态里 s1 不在恢复模式(新主)、p1 在恢复模式(已回备)且收发位点一致(0/B0025A0),说明复制健康;
  • replication_time_lag 只在备节点有值(本例 4.18 秒,为演练后回放追平的瞬时值);
  • 文件 mtime 超过 60 秒未更新 = pglookout 进程已死,pglookout_current_master 会据此返回 255(4.2)。

4.5 告警文件:三类"地砖"

alert_file_dir(本环境 /var/run/pglookout/)下的存在性文件是 pglookout 的原生告警通道,适合被任何监控采集器以"文件是否存在"判定:

告警文件触发条件自动消除实测
replication_delay_warning备库回放延迟超过 warning_replication_time_lag✅ 延迟回落后自动删除见配置篇实测
failover_has_happened本节点执行过 failover_command❌ 需人工确认集群健康后删除4.7 步骤 5
multiple_master_warning视图中出现两个可达主库(脑裂)❌ 需人工处置后删除4.7 步骤 6

源码事实:delete_alert_file 只对 replication_delay_warning 调用,后两类文件创建后永不自动删除,这是设计使然——切换/脑裂必须人工复核。

演练切换后备节点实测回显:

ls -l /var/run/pglookout/; cat /var/run/pglookout/failover_has_happened
total 20
-rw-r--r-- 1 root root    5 Nov  1 09:30 failover_has_happened
-rw-r--r-- 1 root root 2238 Nov  1 09:31 pglookout_state.json
alert

脑裂时旧主实测回显:

total 20
-rw-r--r-- 1 root root    5 Nov  1 09:32 multiple_master_warning
-rw-r--r-- 1 root root 2236 Nov  1 09:32 pglookout_state.json
$ cat /var/run/pglookout/multiple_master_warning
alert

内容固定为 5 字节的 alert,告警语义全在文件名上。人工确认集群健康后的标准处置:

rm /var/run/pglookout/failover_has_happened /var/run/pglookout/multiple_master_warning

4.6 外部命令脚本:failover_command 与 pg_start/stop_command

pglookout 自己不动数据库,所有"提升/重启/停止"动作都通过配置里的三个命令键委托给外部脚本。

failover_command(本环境配置为 /opt/pglookout/failover.sh,cat 实测回显):

#!/bin/bash
# pglookout failover_command: promote the local standby to primary
set -x
logger -t pglookout "failover_command invoked, promoting local PostgreSQL instance"
su postgres -c "/usr/lib/postgresql/16/bin/pg_ctl promote -D /var/lib/postgresql/16/main -t 60 -w"
# 生产环境建议在此实现 STONITH(fence 旧主),示例略
exit 0

切换时刻的实际执行日志(备节点 s1,4.7 演练中回显):

2026-11-01 09:30:42,580	pglookout	MainThread	WARNING	Executing external command: ['/opt/pglookout/failover.sh']
+ logger -t pglookout 'failover_command invoked, promoting local PostgreSQL instance'
+ su postgres -c '/usr/lib/postgresql/16/bin/pg_ctl promote -D /var/lib/postgresql/16/main -t 60 -w'
waiting for server to promote.... done
server promoted
+ exit 0
2026-11-01 09:30:42,689	pglookout	MainThread	WARNING	Executed external command: 0, output: 0
2026-11-01 09:30:42,689	pglookout	MainThread	WARNING	Executed failover command: ['/opt/pglookout/failover.sh'], return_code: 0, took: 0.11s

三个要点:

  1. set -x 的跟踪输出(+ logger ...)会被 pglookout 原样写进自己的日志,脚本内 logger 另发 syslog(tag pglookout),双通道留痕;
  2. failover_command 会在所有判定"该我上"的存活 DB 节点上执行,pglookout 没有互斥锁——脚本必须幂等(pg_ctl promote 对已提升实例立即返回),生产环境应在脚本里实现 STONITH 隔离旧主;
  3. 脚本返回码只记录不决策(return_code: 0, took: 0.11s),非零不会回滚已做的提升。

pg_start_command / pg_stop_command:autofollow: true 时,pglookout 发现新主后用它俩重启本节点数据库完成"改嫁"。旧主实测回显(4.7 步骤 6):

2026-11-01 09:32:22,325	pglookout	MainThread	INFO	Starting to follow new master 's1', modified recovery.conf and restarting PostgreSQL; pg_start_command ['pg_ctlcluster', '16', 'main', 'start']; pg_stop_command ['pg_ctlcluster', '16', 'main', 'stop', '-m', 'fast']
2026-11-01 09:32:22,325	pglookout	MainThread	WARNING	Executing external command: ['pg_ctlcluster', '16', 'main', 'stop', '-m', 'fast']
Cluster is not running.
2026-11-01 09:32:22,347	pglookout	MainThread	ERROR	Problem with executing: ['pg_ctlcluster', '16', 'main', 'stop', '-m', 'fast'], return_code: 2, output: None
2026-11-01 09:32:22,349	pglookout	MainThread	WARNING	Executed external command: 2, output: ''
2026-11-01 09:32:22,349	pglookout	MainThread	WARNING	Executing external command: ['pg_ctlcluster', '16', 'main', 'start']
2026-11-01 09:32:24,510	pglookout	MainThread	WARNING	Executed external command: 0, output: 0
2026-11-01 09:32:24,510	pglookout	MainThread	INFO	Started following new master 's1', took: 2.19s

stop 失败(旧主停机期间本就"Cluster is not running",pg_ctlcluster 返回 2)只打 ERROR 行,流程继续执行 start——这既是容错也是隐患:start 起来的实例角色完全取决于数据目录现状(下一步就踩了这个坑)。

4.7 故障转移演练实测:停主 → 接管 → 旧主回备

以下为完整演练记录。拓扑:p1 主 / s1 备 / obs1 观察;failover_timeout 120 秒;s1 与 p1 均 autofollow: true。

步骤 0:基线确认(见 4.2/4.3 实测:POST /check 返回 204、helper 返回 p1、p1 角色 f|1、s1 角色 t)。演练数据在 p1 预置:

CREATE TABLE IF NOT EXISTS failover_drill (id int PRIMARY KEY, note text, at timestamptz DEFAULT now());
INSERT INTO failover_drill (id, note) VALUES (1, 'before-failover');

步骤 1:停主(p1,09:28:39 UTC):

pg_ctlcluster 16 main stop
su postgres -c "psql -tAc 'SELECT 1'"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory
	Is the server running locally and accepting connections on that socket?

步骤 2:备节点第一时间感知(s1 日志)。主库连接先被管理员命令终止,随后拒绝连接:

2026-11-01 09:28:42,811	ClusterMonitor	ThreadPoolExecutor-236_0	WARNING	OperationalError (FATAL:  terminating connection due to administrator command
SSL connection has been closed unexpectedly) querying status from p1
2026-11-01 09:28:47,813	ClusterMonitor	Thread-1	WARNING	OperationalError (Connection refused
	Is the server running on that host and accepting TCP/IP connections?) connecting to p1 ('p1' (host=10.89.60.11 dbname=postgres user=pglookout; hidden password))

步骤 3:120 秒保护期(s1 日志,每个决策周期重复,节选):

2026-11-01 09:28:52,534	pglookout	MainThread	WARNING	No known master node, disconnected masters: ['p1']
2026-11-01 09:28:52,534	pglookout	MainThread	WARNING	No reachable master node in cluster, 1 standby nodes exist, 1182.37 seconds since last cluster config update, failover timeout set to 120.0 seconds, previous master: 'p1', master_node: {'fetch_time': '2026-11-01T09:28:46.884434Z', 'connection': False, 'db_time': '2026-11-01T09:28:36.880832Z', 'pg_is_in_recovery': False, 'pg_last_xact_replay_timestamp': None, 'pg_last_xlog_receive_location': None, 'pg_last_xlog_replay_location': '0/900FF48', 'replication_slots': [], 'replication_time_lag': None}
2026-11-01 09:28:52,537	pglookout	MainThread	WARNING	Performing failover decision because master node is not reachable
2026-11-01 09:28:52,537	pglookout	MainThread	WARNING	No connected master nodes, but last contact was still within failover timeout (120.0s), not failing over

最后这行就是"保护期":主库失联但未超过 failover_timeout,不切换——主库若在 120 秒内恢复,集群不会有任何动作。期间每 10 秒一轮 Connection refused + not failing over,直到 09:30:42。

步骤 4:决策与提升(s1 日志,09:30:42):

2026-11-01 09:30:42,580	pglookout	MainThread	WARNING	Node that is furthest along is: 's1', all replication positions were: [151060560]
2026-11-01 09:30:42,580	pglookout	MainThread	WARNING	We will now do a failover to ourselves since we were the instance furthest along
2026-11-01 09:30:42,580	pglookout	MainThread	WARNING	Executing external command: ['/opt/pglookout/failover.sh']

(failover.sh 执行回显见 4.6,return_code: 0, took: 0.11s。)10 秒后的下一周期,新主身份被确认:

2026-11-01 09:30:52,690	pglookout	MainThread	WARNING	Picked 's1' as master since {'p1': {...'connection': False...}} are in a disconnected state
2026-11-01 09:30:52,690	pglookout	MainThread	INFO	New master node detected: old: 'p1' new: 's1': {'fetch_time': '2026-11-01T09:30:52.583684Z', 'connection': True, 'db_time': '2026-11-01T09:30:52.584235Z', 'pg_is_in_recovery': False, 'pg_last_xact_replay_timestamp': None, 'pg_last_xlog_receive_location': None, 'pg_last_xlog_replay_location': '0/9010208', 'replication_time_lag': None, 'replication_start_time': 24427.124697868, 'min_replication_time_lag': 0.192627, 'replication_slots': []}

从停主(09:28:39)到 promote(09:30:42)实测耗时约 123 秒,几乎全部花在 120 秒保护期上——这就是 pglookout 故障转移的 RTO 下限。

步骤 5:切换后验证:

su postgres -c "psql -tAc 'SELECT pg_is_in_recovery(), timeline_id FROM pg_control_checkpoint()'"   # s1
f|2

时间线已推进到 2。/var/run/pglookout/failover_has_happened 出现(4.5 回显)。观察节点视点:HTTP /state.json 中 s1 conn=True recovery=False、p1 conn=False;状态文件与 helper 均给出 s1(4.2 回显)。新主写入验证:

INSERT INTO failover_drill (id, note) VALUES (2, 'after-failover-write');
INSERT 0 1

步骤 6:旧主自动改嫁——脑裂。p1 的 pglookout 进程一直活着,autofollow 生效,日志与 postgresql.auto.conf 被改写(4.6 已给重启回显)。auto.conf 实测内容:

# pglookout updated primary_conninfo for instance s1 at 2026-11-01T09:32:22.325419Z
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=replicator password=q1w2e3r4 application_name=pglookout-primary sslmode=disable host=10.89.60.12'
recovery_target_timeline = 'latest'

但 pglookout 只改 auto.conf,不创建 standby.signal(PG 12+ 的备库标志文件)——旧主数据目录里没有它,于是 start 起来的 p1 以主库身份运行:

$ su postgres -c "psql -tAc 'SELECT pg_is_in_recovery()'"   # p1
f
$ ls /var/lib/postgresql/16/main/standby.signal
ls: cannot access '/var/lib/postgresql/16/main/standby.signal': No such file or directory

两个"主库"同时可达,p1 的 pglookout 日志随即报脑裂:

2026-11-01 09:32:34,511	pglookout	MainThread	ERROR	More than one master node connected_master_nodes: {'s1': {...'pg_is_in_recovery': False, ...'pg_last_xlog_replay_location': '0/9010D50'...}, 'p1': {...'pg_is_in_recovery': False, ...'pg_last_xlog_replay_location': '0/9010118'...}}, disconnected_master_nodes: {}

并落地 multiple_master_warning 告警文件(4.5 回显)。更糟的是 p1 以主身份又写了一点 WAL(checkpoint 已到 0/90104A8,而时间线 1 只到 0/9010050)——WAL 分叉。

步骤 7:手工降级失败——timeline 分叉实锤。补上标志文件重启:

touch /var/lib/postgresql/16/main/standby.signal
chown postgres:postgres /var/lib/postgresql/16/main/standby.signal
pg_ctlcluster 16 main restart

启动即 FATAL(实测回显):

2026-11-01 09:35:04.374 UTC [1047] LOG:  entering standby mode
2026-11-01 09:35:04.374 UTC [1047] FATAL:  requested timeline 2 is not a child of this server's history
2026-11-01 09:35:04.374 UTC [1047] DETAIL:  Latest checkpoint is at 0/90104A8 on timeline 1, but in the history of the requested timeline, the server forked off from that timeline at 0/9010050.
pg_ctl: could not start server
Examine the log output.

步骤 8:pg_rewind 失败——前置条件缺失。标准修法是先试 pg_rewind(在新主给 postgres 角色设好密码后,旧主停机执行):

su postgres -c "pg_rewind --target-pgdata=/var/lib/postgresql/16/main --source-server='host=10.89.60.12 user=postgres password=q1w2e3r4 dbname=postgres'"
pg_rewind: connected to server
pg_rewind: error: target server needs to use either data checksums or "wal_log_hints = on"
exit=1

本集群 initdb 未开数据校验和、wal_log_hints 也是默认 off——pg_rewind 根本不可用(该前置必须在建库时定,见五-6)。中途还踩了一个次生坑:以主身份临时启动修配置时 ALTER SYSTEM 报 ERROR: could not open file "postgresql.auto.conf": Permission denied——auto.conf 已被 pglookout 以 rename 方式重写、属主变成 root(见五-5)。

步骤 9:pg_basebackup 重建旧主回备。停机、清空数据目录、从新主全量重建(-R 自动生成 standby.signal + primary_conninfo):

rm -rf /var/lib/postgresql/16/main/*
su postgres -c "pg_basebackup -h 10.89.60.12 -U replicator -D /var/lib/postgresql/16/main -R -X stream"
pg_basebackup: created temporary replication slot "pg_basebackup_1268"
23274/23274 kB (100%), 0/1 tablespace (...gresql/16/main/global/pg_control)
23274/23274 kB (100%), 1/1 tablespace
pg_basebackup: write-ahead log end point: 0/A000100
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: syncing data to disk ...
pg_basebackup: renaming backup_manifest.tmp to backup_manifest
pg_basebackup: base backup completed
pg_ctlcluster 16 main start
start exit=0

步骤 10:收敛验证(终态实测回显):

# p1:角色 + 接收/回放位点
su postgres -c "psql -tAc \"SELECT pg_is_in_recovery(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()\""
# p1:walreceiver 状态
su postgres -c "psql -tAc \"SELECT status, sender_host, received_tli, latest_end_lsn FROM pg_stat_wal_receiver\""
# s1:walsender 状态
su postgres -c "psql -tAc \"SELECT application_name, state, sync_state FROM pg_stat_replication\""
# p1:演练表数据
su postgres -c "psql -tAc 'SELECT * FROM failover_drill ORDER BY id'"
t|0/B0026C8|0/B0026C8
streaming|10.89.60.12|2|0/B0026C8
16/main|streaming|async
1|before-failover|2026-11-01 08:59:16.289383+00
2|after-failover-write|2026-11-01 09:33:14.767819+00

p1 已在新主(时间线 2)上流式回放,切换前后两行数据都在——数据零丢失(演练中主库是干净停机的;异常宕机时未归档 WAL 可能丢失,这是 pglookout 这类"无 fencing 自动切换"方案的固有代价)。

步骤 11:收尾。人工确认集群健康后清理告警文件(4.5),重启 p1 的 pglookout 加载回退后的配置(autofollow 关闭,避免下次演练再触发自动改嫁),终态全景:

current_master: s1 | p1 recovery: True | s1 recovery: False

helper 与 HTTP 接口视点一致(4.2/4.3 实测),集群回到"一主一备一观察"的健康形态,只是主备互换——旧主 p1 需择机用同样的 basebackup 流程反向重建回主,或用 planned switchover 思路手工回切(pglookout 本身不提供回切命令)。

五、注意事项

  1. RTO 下限 = failover_timeout:实测停主到 promote 约 123 秒,其中 120 秒是保护期。调小该参数会增大误切风险(网络抖动即切换),权衡见配置篇。
  2. failover_command 无互斥、无 fencing:脚本在所有"该我上"的存活 DB 节点上都会执行,官方 README 明确警告脑裂风险。脚本必须幂等,生产应加 STONITH;演练中旧主脑裂(4.7 步骤 6)就是没有 fencing 的直接后果。
  3. autofollow: true 的前提是节点本来就在备库模式:pglookout 只改写 postgresql.auto.conf 里的 primary_conninfo/recovery_target_timeline,不会创建 standby.signal。主库身份回归的节点没有该文件,改嫁后仍会以主身份启动造成脑裂。标准做法:部署备库时就 touch standby.signal,并把它纳入配置管理。
  4. 旧主回归不是自动的:切换后旧主要么 pg_rewind 要么 pg_basebackup 重建,pglookout 只负责"让它连上新主",不负责数据层收敛。
  5. pglookout 会改 postgresql.auto.conf 的文件属主:它以"写临时文件 + rename"方式重写该文件,属主变成 pglookout 的运行账号(本环境 root),postgres 随后执行 ALTER SYSTEM 会报 Permission denied(实测,见 4.7 步骤 8)。修法:chown postgres:postgres /var/lib/postgresql/16/main/postgresql.auto.conf。
  6. pg_rewind 前置条件必须在建库时定:wal_log_hints = on 或 initdb 数据校验和二选一,事后无法用 rewind 补救已分叉的旧主(wal_log_hints 改动还需完整重启 + checkpoint 才生效)。本演练两者都没开,只能走 basebackup。
  7. HTTP 端点与状态文件结构不同(4.3/4.4):/state.json 只有裸 cluster_state,current_master 只在状态文件里;采集脚本按各自结构取数。
  8. 健康检查必须 POST /check(4.3):GET 会因源码 404 分支不关闭响应头而挂起,负载均衡/存活探针用 GET 会全部超时。
  9. 本验证环境无 systemd:进程托管以 supervisord 等等价执行;pg_ctlcluster 是 Debian 系打包的集群控制命令,RPM 系发行版对应 pg_ctl -D <datadir>。

六、常见问题

Q1:curl GET /check 一直挂起/超时,服务是不是挂了? 现象:curl 返回码 000,但节点访问日志明明打了 404。 原因:do_GET 未实现 /check 且 404 分支不调 end_headers,响应永远发不完(4.3 实测)。 解决:健康检查改用 POST /check,204 即健康。

Q2:停了主库,备库迟迟不切换? 现象:日志每 10 秒刷 No connected master nodes, but last contact was still within failover timeout (120.0s), not failing over。 原因:保护期未过,属正常行为(4.7 步骤 3)。 解决:等满 failover_timeout;若始终不切,依次检查 failover_on_disconnect 是否为 false、本节点是否在 never_promote_these_nodes 名单、own_db 是否配置(观察节点 own_db 为空永不提升)。

Q3:pglookout_current_master 返回 255 且无输出? 原因:状态文件超 60 秒未更新(pglookout 进程死了),或传错了参数——该命令要传配置文件而不是状态文件(4.2,帮助文本的 state 措辞有误导)。 解决:先 pidof pglookout 确认进程活着,再核对传参是 /etc/pglookout/pglookout.json。

Q4:切换后旧主上出现 multiple_master_warning,日志报 More than one master node? 原因:autofollow 改嫁时旧主数据目录缺 standby.signal,以主身份回归(4.7 步骤 6)。 解决:立即停旧主防止双写;按 Q5/Q6 把旧主降级回备;确认集群单主健康后 rm 告警文件。

Q5:旧主加 standby.signal 后起不来,报 FATAL: requested timeline 2 is not a child of this server's history? 原因:脑裂期间旧主以主身份写过 WAL,时间线 1 已分叉(DETAIL 里的 checkpoint 位点超过分叉点 0/9010050,4.7 步骤 7)。 解决:不能直接回放,必须 pg_rewind(有前置条件,见 Q6)或 pg_basebackup 重建(4.7 步骤 9)。

Q6:pg_rewind 报 target server needs to use either data checksums or "wal_log_hints = on"? 原因:建库时没开校验和、wal_log_hints 默认 off,旧主分叉点之后的 WAL 不可回放。 解决:小数据量直接 pg_basebackup -R 重建(本文实测路径);长期方案在建库时开 initdb -k 或 wal_log_hints = on。

Q7:ALTER SYSTEM 报 could not open file "postgresql.auto.conf": Permission denied? 原因:pglookout 改嫁时以 rename 方式重写了 auto.conf,属主变成 pglookout 运行账号(五-5)。 解决:chown postgres:postgres 修回属主再执行。

Q8:配置里删了 pg_data_directory 等键并 SIGHUP 后,日志每周期刷 TypeError: expected str, bytes or os.PathLike object, not NoneType,进程不死但刷屏? 原因:热加载路径的源码只判断 current_master != own_db 就尝试重写恢复配置、不检查 autofollow 开关(演练中已回退 autofollow=false 仍复现),取到 None 的数据目录每周期抛异常。 解决:重启 pglookout 进程即清除;改配置尽量"改值"而不是"删键",删键后走重启。

七、参考链接

pglookout 2.0.3 常用 CLI 命令与工具详解 - Quzuo