# 拓扑、命名与故障域

LLMS 索引： [llms.txt](/llms.txt)

---

三台服务器不是高可用拓扑，除非我们知道三台分别会因什么而一起失败。
“主库、从库、VIP”也不是足够精确的词表，尤其在 promotion 之后。

## 19.5.1 主节点、同步副本、异步副本与仲裁 {#item-19-5-1}

### primary 是当前角色，不是机器身份

PostgreSQL physical replication 中：

```text
primary   接受写入并生成 WAL
standby   持续恢复并接收/重放 WAL
hot standby  在恢复中提供只读查询
```

promotion 后，原 standby 可以成为 primary。于是：

```text
pg-test-1 = stable member identity
primary   = mutable runtime role
```

把 hostname 叫 `prod-primary` 会在第一次切换后撒谎。

原生证据：

```sql
SELECT pg_is_in_recovery();
```

```text
false -> 当前可写 primary 形态
true  -> 当前处于 recovery 的 standby
```

它不单独证明 service routing 正确，也不证明另一台没有同时可写。

### physical standby 重放同一条 WAL 历史

同一 cluster 的成员应共享：

```text
system identifier
compatible major/build/storage layout
timeline ancestry
WAL history
tablespace paths
extension binary prerequisites
```

官方
[Log-Shipping Standby](https://www.postgresql.org/docs/18/warm-standby.html)
强调 primary/standby 应尽量相似，physical log shipping 不跨 major。

本章用 system identifier 检查：

```text
pg-test-1 == pg-test-2 == pg-test-3
pg-meta != pg-test
```

### 异步复制的 commit 不等待副本

streaming replication 默认异步。正常低延迟不等于零 RPO：

```text
client receives commit
WAL may still be in primary-only failure domain
primary suffers unrecoverable loss
promotion candidate lacks last records
```

数据损失窗口由故障时实际 receive/write/flush/replay 位置决定，而不是
平时 dashboard 上“通常 0 MB”决定。

观察 primary：

```sql
SELECT application_name,
       client_addr,
       state,
       sync_state,
       sent_lsn,
       write_lsn,
       flush_lsn,
       replay_lsn,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_gap_bytes
FROM pg_stat_replication
ORDER BY application_name;
```

本章只确认 replica 在 `streaming`；第 20 章才测 failure-time 数据包络。

### 同步复制把一部分 commit latency 换成确认

PostgreSQL synchronous replication 可以让 commit 等待一个或多个 standby
反馈。等待层次包括：

```text
remote_write
on / remote flush
remote_apply
```

它们不是同义词。`remote_apply` 还等待 replay 可见，延迟与阻塞代价更高。

配置又分：

```text
FIRST n (...)   priority-based
ANY n (...)     quorum-based
```

同步复制只有在以下条件下才提供期望保护：

- standby 是真实独立故障域；
- storage flush 语义可信；
- synchronous standby 处于 streaming；
- application 没有降级为更弱 `synchronous_commit`；
- 超时/降级策略符合业务选择；
- 失联时是停止写还是降级继续写已经定义。

“有 sync replica”不能省略这些条件。

### 同步复制也可能让写入不可用

若要求一个同步确认，而没有任何合格 standby：

```text
transaction does work
commit waits
locks may remain held
application timeout/retry
load amplifies
```

因此 RPO 与 availability 之间要由 owner 选择：

```text
fail closed: wait, preserve durability target
degrade: resume async, accept explicit data-loss risk
```

自动降级如果没有审计，就是悄悄改变服务合同。

### “仲裁”不要与数据副本混淆

PostgreSQL core 没有一个保存业务数据的“仲裁节点”概念。Pigsty 的 HA
参考实现里：

```text
Patroni  管理成员状态、leader lock 与操作
etcd     提供分布式配置存储/共识
PostgreSQL members 保存业务数据/WAL
HAProxy 依据 health checks 路由 service
```

etcd member：

- 不保存可 promotion 的 PostgreSQL data；
- 不确认每个业务 transaction 已持久化；
- 不替代 backup；
- 不依据最新 WAL 自动解决所有数据选择；
- 需要自己的 quorum 与 failure domains。

把一个 etcd 节点叫“第三票”，然后声称两台 PostgreSQL 已实现零数据丢失，
是错误模型。

### DCS quorum 与数据库成员数分别计算

三节点 PostgreSQL + 单节点 etcd：

```text
database copies = 3
DCS members      = 1
shared laptop    = 1
```

三个数字不能互相抵消。单 etcd failure 会影响自动 HA 控制面；同一 laptop
failure 会同时失去全部 VM。

本章正式沙箱把这两项登记为：

```text
EX19-SINGLE-ETCD
EX19-SHARED-HYPERVISOR
```

所以它适合练拓扑与协议，不适合宣称 production availability。

### offline replica 是 workload placement

Pigsty 支持专门的 offline instance，也支持在已有 replica 上设置
`pg_offline_query: true`，把它纳入 offline service。官方
[Cluster / Instance](https://pigsty.io/docs/pgsql/config/cluster/)
明确说明后一种是资源折中。

本章：

```text
10.10.10.13:
  pg_role: replica
  pg_offline_query: true
```

它仍是同一 `pg-test` physical replica，不是独立 analytical authority。
标记不自动限制 CPU/I/O，也不自动阻止用户绕过 service 直连。第 17、22、
26、30 章分别处理分析语义、服务接入、容量和资源治理。

### 本章拓扑不是第 20 章结论

本章可以证明：

```text
one Patroni leader
two streaming replicas
SQL recovery roles agree
declared endpoints reachable
```

不能证明：

```text
leader loss detection time
fencing
split-brain exclusion
automatic failover RTO
failure-time RPO
client reconnection semantics
old primary rejoin safety
```

这些必须用受控 fault drill 取得。

## 19.5.2 节点、实例、集群、服务的统一词表 {#item-19-5-2}

### 同一个“集群”常指四种东西

先建立词表：

| 词 | 本书定义 | 稳定身份/证据 |
|---|---|---|
| host/node | OS 实体：物理机、VM 或明确容器边界 | machine ID、hostname、address |
| PostgreSQL instance | 一个 server process + data directory + port | member name、PGDATA、system ID |
| PostgreSQL database cluster | 一个 `initdb` 产生的数据目录及其中 databases | system identifier |
| HA cluster / Pigsty `pg_cluster` | 一组共享 physical history、可相互接管的 members | `pg_cluster`、Patroni scope |
| database | cluster 内的 SQL database | OID/name/owner/locale |
| service | 面向客户端的稳定访问语义 | name、port、selector、health check |
| pool | 连接复用/状态边界 | PgBouncer endpoint/mode |
| DCS cluster | Patroni 使用的 etcd 共识域 | etcd membership/quorum |
| platform deployment | inventory 管理范围 | exact inventory/release |

PostgreSQL 官方把一个 data directory 称为 database cluster；平台团队又常把
一个 HA group 称 cluster。文档必须说明上下文。

### node 不等于 instance

一台 node 可以运行：

```text
PostgreSQL instance
PgBouncer
HAProxy
Patroni
exporters/Vector
infra components
多个独立实例（若平台允许）
```

一个 PostgreSQL cluster 也可以跨多 node。

“node down”与“Postgres down”故障面不同：

```text
process crash
OS crash
VM pause
host failure
rack/zone loss
network partition
storage loss
control-plane loss
```

### instance identity 不能只用 IP

IP 可以复用，DNS 可以漂移，hostname 可以被自动化改写。本章组合：

```text
declared address
post-convergence hostname
hash(machine-id)
Patroni member name
PostgreSQL system identifier
pg_cluster
pg_seq
```

实验中发现一个真实陷阱：工作站 `~/.ssh/config` 将四个地址映射到本机不同
转发规则，最初所有连接看起来都是 `meta`。正式采集强制：

```bash
ssh -F /dev/null ...
```

再验证四个不同 machine ID。连接成功不是目标身份成功。

### service 是语义，不是 socket

Pigsty 默认 service 可能包括：

```text
5433 primary
5434 replica
5436 primary direct
5438 offline
```

官方
[Service/Access](https://pigsty.io/docs/pgsql/service/)
记录这些默认映射。端口能建立 TCP 只证明 listener/reachability，不证明：

- primary service 一定落在 current primary；
- replica service 的 freshness；
- pool transaction/session 语义；
- role/database/HBA 正确；
- TLS hostname 验证；
- failover 后客户端恢复。

所以本章 endpoint probe 的解释字段明确写：

```text
reachability and Patroni identity only
routing/pooling/failover/saturation belong to chapter 22
```

### cluster name 不应含 current role

推荐稳定层次：

```text
service unit   pg-test
member         pg-test-1 / pg-test-2 / pg-test-3
runtime role   primary / replica
service        pg-test-primary / pg-test-replica / pg-test-offline
database       test
application    pg36_shop
```

`pg-test-primary` 可以是 service 名，不应是固定 machine 名。

### 数据库里的 role 又是另一种 role

避免一句“role 是 replica”同时指：

```text
Pigsty member role       pg_role
Patroni runtime role     Leader/Replica
PostgreSQL recovery role pg_is_in_recovery
SQL authorization role   pg_roles
business owner role      human/team responsibility
```

表格、变量名和 evidence 都写全称。

### 声明角色与观察角色都要保存

inventory 声明：

```yaml
10.10.10.11: { pg_seq: 1, pg_role: primary }
10.10.10.12: { pg_seq: 2, pg_role: replica }
10.10.10.13: { pg_seq: 3, pg_role: replica, pg_offline_query: true }
```

观察：

```text
Patroni leader/replica + state
SQL in_recovery
service health identity
```

第一次部署时二者应一致。发生 failover 后，inventory 的 bootstrap role 不应
被天真解释为永恒运行角色；第 20 章会定义 drift 语义。

## 19.5.3 环境、区域、租户与业务命名 {#item-19-5-3}

### 命名要编码稳定属性

适合进入名字的：

```text
environment
service/business domain
service unit
region/site
sequence
```

不适合进入固定 host 名的：

```text
current primary
healthy
latest
temporary owner
current version
current ticket
```

稳定属性让名字在 role change 后继续真实。

### 一份可扩展命名模型

示例：

```text
platform deployment  shop-prod-cn1
service unit         pg-shop-order
members              pg-shop-order-1..3
database             shop
NOLOGIN owner        shop_owner
runtime login        shop_app
services             pg-shop-order-primary
                     pg-shop-order-replica
                     pg-shop-order-offline
```

名字要满足：

- PostgreSQL identifier 限制；
- DNS label 限制；
- metric label cardinality；
- certificate SAN；
- log/search 可读性；
- automation group syntax；
- rename cost。

### environment 是权限与数据边界

常见：

```text
dev
test
staging
prod
dr
```

不能只靠名字隔离。还要有：

```text
account/project
network
credentials/CA
backup repository
monitoring tenant
data policy
change authority
failure domain
```

`prod` database 和 `test` database 放在同一个 cluster，通常仍共享 superuser、
WAL、storage、restart、capacity 和 incident blast radius。

### region、zone、rack 必须对应可验证故障域

一个 label `az-a` 不等于独立 zone。登记：

```text
provider/site ID
building/room/rack
power feed
top-of-rack/core network
hypervisor/host group
storage controller/array/replication
DNS/NTP/KMS/CA
DCS and backup dependencies
```

然后为每个 component 画依赖。相同依赖会形成 correlated failure。

本章四台 VM：

```text
four machine IDs
one physical Mac
one hypervisor
one power source
one storage substrate
```

所以 failure-domain count 不是四。

### tenant 边界不等于 schema 名

租户可能通过：

```text
row
schema
database
cluster
account/project
```

隔离强度逐步变化，成本也变化。命名中加入 tenant 前，先定义：

- authentication/authorization；
- noisy-neighbor；
- backup/restore granularity；
- key ownership；
- data residency；
- deletion；
- metrics/audit；
- exit/migration。

本章 `pg-meta` 与 `pg-test` 是两个 service unit，不是两个 customer tenant。

### 业务名与技术名需要映射表

不要让应用团队猜：

| 业务对象 | 平台对象 |
|---|---|
| pg36_shop service | `pg-test` teaching service unit |
| canonical shop DB | future `pg36_shop` provisioning target |
| control/validation | `pg-meta` |
| OLTP write endpoint | primary service contract |
| analytical read | offline service contract |

正式 inventory 当前还会创建 template 自带的 `test`/`meta` database。它们是
部署示范对象，不表示上卷的 `pg36_shop` fixture 已迁入这个下卷沙箱。

### name registry 要防冲突与复用

登记：

```text
name
type
immutable ID
environment
owner
created/retired
aliases
DNS/certificate names
monitoring labels
backup stanza
reuse quarantine
```

立即复用已退役 cluster 名可能让：

- old DNS/cache 指向新服务；
- backup stanza 混淆；
- monitoring time series 串联；
- client secret 意外生效；
- automation limit 选错目标。

### 本章拓扑清单

[`topology.mmd`](/labs/ch19/topology.mmd) 表达：

```text
10.10.10.10 pg-meta-1  pg-meta primary + infra/etcd/MinIO
10.10.10.11 pg-test-1  pg-test primary
10.10.10.12 pg-test-2  pg-test replica
10.10.10.13 pg-test-3  pg-test replica + offline query
```

它同时画出共享 hypervisor。架构图若只画 database arrows、隐藏共同依赖，
会高估 availability。

### 命名与拓扑验收

validator 交叉检查：

```text
address -> expected post-deployment hostname
address -> distinct machine ID hash
address -> inventory pg_cluster/pg_role/offline
address -> SQL cluster_name/in_recovery/system ID
cluster -> Patroni host/role/state
```

反例：

```text
reuse-one-machine-identity -> E_HOST_IDENTITY
declare-two-live-leaders   -> E_TOPOLOGY
```

通过后的精确结论：

> 四个地址对应四个同构 Linux guest；`pg-meta` 与 `pg-test` 是两个不同
> PostgreSQL system identifier；`pg-test` 有一个 leader 和两个 streaming
> replica；全部 guest 仍共享一个物理故障域。

---

[上一节：版本与数据库初始化契约](../04/) · [返回本章目录](../) · [下一节：用声明式清单交付两个服务单元](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
