跳转到主要内容

18 万法归宗:PostgreSQL 数据平台与替代边界

前 17 章分别回答了许多“PostgreSQL 能不能”的问题:

能不能表达可靠的数据模型
能不能守住事务与并发不变量
能不能把数据库能力交付给应用
能不能用函数、触发器和扩展扩大边界
能不能完成检索、时空与分析工作

第 18 章换一个问法:

即使都能做,哪些能力应当留在 PostgreSQL,哪些只适合试点,哪些应当交给 外部系统;又由谁对组合后的整体服务负责?

这是从“数据库产品”走向“数据平台”的分水岭。

数据库不会因为装了更多扩展就自动成为平台;HA、备份、连接池和监控也不会 因为有了部署脚本就自动成为服务。平台至少还要给出:

service objective   对外承诺什么
authority           每类数据由谁定夺
ownership           谁决策、谁值班、谁付成本
isolation           谁能互相影响
lifecycle           如何申请、变更、升级、退出
evidence            哪些结论已经证明

本章把上卷的功能证据组合成一份 1.6-proposal,同时把所有尚未证明的生产 结论交给下卷 18 个证据门。它不是庆功章,而是一份边界清单。

本章完成后

你应当能够:

  • 把事务、检索、时空、分析和异步任务拆成独立能力,而不是用一个产品名概括;
  • 区分数据/计算、存储、接入、控制与观察平面;
  • 解释为什么多个健康组件仍可能交付一个不健康的端到端服务;
  • 用延迟、新鲜度、正确性、可用性、RPO/RTO、容量和成本描述组合目标;
  • 说明 PostgreSQL 的关系一致性、类型系统、扩展机制和统一查询为何有价值;
  • 同时说明通用数据库中的 CPU、内存、I/O、WAL、连接、锁和维护竞争;
  • 把“扩展已经安装”与“扩展已经生产准入”分开;
  • 为缓存、消息、对象存储、湖仓和外部检索划分逐数据域的权威;
  • 为每个派生副本写出新鲜度、顺序、幂等、重建、对账、删除和退出契约;
  • 用测量触发器而不是技术潮流决定何时引入专用检索、流处理或分析系统;
  • 设计开发、标准 HA、关键 HA 和离线分析四类服务产品;
  • 在 schema、database、instance 和 cluster 隔离之间按信任与故障边界选择;
  • 把连接、存储、临时文件、语句时间和维护窗口变成可执行配额;
  • 说明 Pigsty 4.5 如何映射 PostgreSQL、Patroni、etcd、HAProxy、PgBouncer、 pgBackRest 与观测能力;
  • 分清 Pigsty 开箱能力、环境验收和组织流程;
  • 运行一个完全只读的上卷能力审计;
  • 读懂服务目录、外置数据契约、架构 ADR 与下卷证据门之间的引用关系;
  • 拒绝“无证据宣称 L1 已通过”“缓存成为业务权威”“实验 FDW 准入生产”等 反例;
  • pg36_shop 的当前结论准确表达为架构提案,而不是生产完成声明。

一张平台地图

应用与运维身份
      |
服务契约:write / read-only / offline / admin
      |
PostgreSQL 权威状态
├── 事务、约束、短原子逻辑
├── pg_trgm 检索:accepted
├── vector 语义检索:pilot
├── PostGIS / btree_gist:conditional
└── 汇总 + offline replica:accepted first step
      |
派生与外置能力
├── cache:只持有可丢弃投影
├── event bus:持有投递与重放日志
├── object storage:持有媒体字节
├── lakehouse:持有带 watermark 的分析投影
└── external search:达到触发条件后才启用

图的关键不是方框数量,而是箭头上的合同。只要发生跨系统复制,就必须回答:

authority       冲突时相信谁
freshness       最旧可以多旧
ordering        允许怎样乱序
idempotency     重试如何不重复生效
failure         一侧不可达时怎样退化
reconciliation  如何发现并修复分歧
deletion        删除如何跨副本传播
rebuild         如何从权威重新生成
exit            如何撤掉这个组件

这些字段被固化在 external-data-contracts.json, 不是留给实现阶段再想的“细节”。

当前提案,不是当前承诺

第 18 章的服务目录有四个 offering:

ID 用途 关键边界
pg-dev 有期限的开发/测试 不承诺 HA
pg-ha-standard 默认生产事务服务 目标待下卷证明
pg-ha-critical 更强隔离与同步策略候选 必须先定义故障域
pg-analytics-offline ETL、慢读、交互分析 非权威、非 read-your-writes

service_objectives 中出现的可用性、RPO、RTO 和新鲜度都是 proposal。节点数 不能证明故障域,配置文件不能证明收敛,备份成功不能证明可恢复,仪表盘存在 不能证明告警可行动。

因此蓝图明确写着:

status=architecture-proposal-not-production-approval
postgresql=18.x target
validated_fixture=18.6
pigsty_reference=4.4
pigsty_l1_validation=not-run
lower_volume_gates=18 pending

这个诚实程度是架构质量的一部分。

PostgreSQL 的能力与代价同时成立

PostgreSQL 把关系约束、事务、丰富类型、函数、操作符、索引方法和查询规划器 放在同一个一致性边界中。官方 Extending SQL 列出的扩展点包括函数、聚合、数据类型、操作符、索引操作符类和扩展包。第 14–17 章已经证明这套机制可以把检索、向量、空间与远端数据纳入 SQL。

同一事实还有另一面:

一个 shared_buffers
一组 CPU / I/O / worker 资源
一个 WAL 与 checkpoint 压力面
一组连接与后台维护预算
一条升级和恢复链
一个错误配置可能共享的爆炸半径

PostgreSQL 18 的 Resource Consumption 特别提醒,work_mem 是每个 sort/hash 操作的基础上限;一条复杂查询可有 多个操作,同时还有多个会话。因此“通用”不是“无限”,统一也不是“免费”。

本章不在这两个事实中二选一。架构工作正是保留统一带来的收益,同时给资源 竞争、生命周期和失败传播设边界。

Pigsty 的位置

Pigsty 4.5 的 Architecture 以声明式 inventory 和模块组合交付环境;PGSQL、INFRA、NODE、ETCD 等模块 把 PostgreSQL 与 HA、接入、备份和观察组件连接起来。

本书把它作为参考实现,因为它能让抽象职责落到可读配置:

抽象职责 Pigsty 参考映射
数据服务 PostgreSQL cluster / database / role
HA 控制 Patroni + etcd
服务接入 HAProxy,按需配 PgBouncer / VIP / DNS
恢复 pgBackRest 与仓库策略
主机与软件 NODE / 软件仓库 / inventory
指标与日志 exporter、VictoriaMetrics/Logs、Grafana、Alertmanager
分析隔离 pg_role: offlinepg_offline_query

Pigsty 官方 Service Access 把 service 定义为封装底层拓扑的访问抽象;本书沿用这个语义,不把某个节点 IP 叫作生产服务。

但参考实现不替团队完成:

业务权威划分
SLO 与错误预算审批
威胁模型
容量预测
扩展准入
变更审批
演练与复盘
值班责任

这也是为什么 pigsty-declaration.example.yml 只能叫 proposal。

只读总验收

本章没有 setup,也没有 reset。它只读前面章节已经保留的 fixture:

ch04 关系模型
ch13 原子数据库逻辑
ch14 扩展生命周期
ch15 搜索质量
ch16 时空语义
ch17 分析与 FDW 边界

然后连续抓取两轮:

platform state
extension catalog
schema catalog
role catalog
capability lifecycle
cross-document policy report
negative policy report

两轮必须逐字节一致,最终输出:

status=ok
preflight=ch04+ch13+ch14+ch15+ch16+ch17
cycles=2-byte-identical
documents=catalog+contracts+blueprint+18-pending-gates
counterexamples=7-rejected
pigsty_l1=not-run
mutation=none

通过只表示“当前开发证据与提案内部一致”,不表示任何生产 SLO 已经实现。

本章目录

18.1 从数据库产品到能力组合

18.2 PostgreSQL 的强项与代价

18.3 明确替代边界

18.4 平台服务目录与多租户

18.5 Pigsty 作为参考实现

18.6 实战:设计 pg36_shop 生产蓝图

写作与验收提示

下一章从第一个 pending gate 开始:不谈抽象生产级,而是建立 部署基线与环境验收


上一章:合纵连横:分析加速与分布式选型 · 返回上卷导读 · 下一章:开天辟地:环境规划与部署基线 · 查看全书目录 · 查看索引中心

18.1 从数据库产品到能力组合

当团队说“我们用 PostgreSQL”时,这句话通常混合了三种不同事实:

product
  一个特定版本的 PostgreSQL server

capabilities
  事务、约束、检索、时空、分析、复制、恢复……

service
  带身份、入口、目标、配额、支持和生命周期的对外交付

产品可以启动,不代表所有能力可用;能力可以运行,不代表服务可承诺。平台 设计的第一步,是把这三层重新拆开。

18.1.1 事务、检索、时空、分析与任务能力

从业务问题开始,而不是从组件清单开始

pg36_shop 至少需要处理以下业务问题:

业务问题 需要的能力 首选正确性边界
订单与库存能否保持不变量 关系约束、事务、并发控制 PostgreSQL commit
应用如何安全调用数据能力 角色、schema、prepared SQL、API 合同 DB + 应用
商品如何被关键词找到 全文/模糊检索、排序、质量 golden PostgreSQL 起步
商品如何按语义相似找到 embedding、向量距离、ANN 质量 试点
配送事件在哪里、何时有效 空间、时间、边界和参考系 PostgreSQL 起步
月报如何快速且不过度影响交易 并行、索引、汇总、离线读 PostgreSQL + 隔离
订单事件如何送到其他服务 outbox、relay、消息投递 跨系统合同
图片字节放在哪里 对象存储与元数据协调 按数据域拆分

先写能力,能避免两种常见偷换:

“PostgreSQL 支持” -> “我们的服务已经支持”
“引入了某产品”   -> “业务问题已经解决”

例如,vector 扩展已经在第 14 章的开发 fixture 中安装,<=> 距离查询也在 第 15 章得到正确结果。这证明“语义检索试验可运行”,没有证明:

  • embedding 模型会被稳定版本化;
  • ANN 在生产规模下满足质量与延迟;
  • 索引重建落在维护窗口;
  • 副本、备份、恢复和升级路径全部可用;
  • 团队可以在模型漂移时诊断结果变化。

所以它的生命周期是 pilot,不是 accepted

事务能力是权威状态的锚

对订单、库存和支付,最重要的不是“能存 JSON”或“QPS 很高”,而是所有 写入者看到同一套不可违反的事实:

order total = sum(order item totals)
payment currency = order currency
inventory cannot be consumed below the approved boundary
status transition belongs to a finite allowed graph
duplicate request does not create a second business effect

第 4、10、13 章分别从约束、并发与数据库逻辑证明了这些能力。只要这类状态 仍由 PostgreSQL 定夺,缓存、搜索索引、事件流和湖仓都只能是派生物。

“source of truth” 容易变成口号,更精确的写法是按数据域声明 authority:

product business state          -> PostgreSQL
cache entry bytes               -> cache
order publication intent        -> PostgreSQL outbox
message delivery/replay log     -> event bus
media object bytes              -> object storage
media identity/state/checksum   -> PostgreSQL
analytical projection           -> lakehouse
operational business state      -> PostgreSQL

冲突时相信谁,必须在发生冲突前写清楚。

检索是一组能力,不是一条 LIKE

商品检索至少可以分成:

exact identity lookup
prefix / substring
typo-tolerant fuzzy match
lexical relevance
phrase / field weighting
semantic similarity
filter + rank
faceting / aggregation
highlighting
freshness and deletion

第 15 章的质量 golden 证明 pg_trgm 适合当前模糊检索基线,vector 适合 受控 pilot。它没有把“搜索”宣布为永久留在 PostgreSQL。蓝图保存一个明确 触发器:

当质量、规模、语言能力或独立可用性目标击败 PostgreSQL 基线时,才启用 external-search-projection-v1

这样,外部检索是一个由证据触发、可重建、可退出的投影,而不是架构图里一 开始就存在的时髦方框。

时空能力先统一语义,再比较引擎

空间系统最危险的错误往往不是查询慢,而是答案看起来合理:

SRID 混用
经纬度顺序颠倒
边界包含规则不一致
event time 与 ingest time 混淆
本地时区被当 UTC
有效时间区间重叠
迟到事件被静默丢弃

第 16 章把 EPSG:4326、UTC、区间与边界规则固化为 fixture。PostGIS 让空间 类型、操作符与索引进入 SQL,但业务合同仍然高于扩展:迁移到其他引擎时, 这些语义必须不变。

因此 capability 的结构至少要包含:

{
  "placement": "postgresql",
  "system_of_record": "postgresql",
  "state": "conditional",
  "consistency": "UTC + versioned validity + EPSG:4326",
  "freshness": "event ingest plus measured lateness",
  "externalization_trigger": "retention or throughput misses objective"
}

分析能力先减少工作,再横向扩展

第 17 章已经建立顺序:

正确性 golden
  -> 计划与统计
  -> 索引 / BRIN
  -> 并行
  -> spill 证据
  -> 汇总
  -> OLTP/OLAP 隔离
  -> 分布式门槛

当前蓝图选择“本地汇总 + offline replica”作为第一步,而不是选择 Citus。 这不是永久拒绝分布式;第 26 章的容量证据可以触发新 ADR。

同理,postgres_fdw 的 loopback fixture 只证明过滤、聚合和部分失败的 查询形状。PostgreSQL 官方 Foreign Data 说明外表通过 wrapper 访问外部数据,并通过 user mapping 提供认证信息。 本书实验使用的 password_required=false 没有生产身份、网络与故障域, 所以严格标为 lab-only

任务能力要分事务内与事务外

“数据库里能执行函数”不等于“任何业务任务都应放进事务”。一个实用边界:

留在事务内:

约束检查
小而有界的派生值
幂等状态转换
短路径的 outbox 写入
与当前事务必须原子完成的审计事实

移到事务外:

HTTP / RPC
发送邮件或短信
大批量重算
不确定时长的模型推理
跨系统补偿流程
无限或长期重试

事务内代码失败可以回滚;远端世界通常不能跟着 PostgreSQL 回滚。把两者硬塞 在一起,会产生持锁时间、连接占用、重试重复和不可控级联。

能力清单必须有状态

本章采用以下生命周期词汇:

状态 含义
accepted 已有当前范围证据,仍受版本与服务门约束
accepted-with-scope 只在明确狭窄边界内接受
accepted-first-step 当前首选,但容量触发器仍待观察
conditional 前置门尚未全部通过
pilot 只允许受控试验,不进入默认生产服务
lab-only 教学机制证据,禁止生产准入
deferred 触发条件未出现,不增加系统

没有状态的能力清单会把“听说过”“装得上”“试过一次”和“可值班”放在同一 列,最终无法治理。

18.1.2 计算、存储、接入、控制与观察平面

能力回答“要做什么”,平面回答“由哪类机制完成”。

数据与计算平面

数据/计算平面直接处理请求与业务状态:

PostgreSQL backend
tables / indexes / WAL-visible changes
SQL planner and executor
constraints / functions / triggers
extension types and operators
replica read execution
external projection workers

它的典型证据是:

  • SQL 结果和业务校验和;
  • 执行计划与实际行数;
  • 事务、锁与 SQLSTATE;
  • replica replay 位置;
  • 投影 watermark。

在这个平面上,“成功”意味着某次业务操作按合同完成,不代表平台整体健康。

存储平面

存储平面不仅是 PGDATA

存储 保存什么 关键风险
PostgreSQL data files heap、index、catalog 损坏、容量、延迟
WAL 恢复与复制所需变化 归档缺口、保留爆炸
backup repository base/diff/incr 与 WAL 不可恢复、凭据、同域失效
temp sort/hash 等中间结果 磁盘耗尽、I/O 争用
object storage 媒体或备份对象 清单、版本、删除
cache 可丢弃派生数据 陈旧、穿透、错误权威
analytical/search index 可重建投影 lag、语义漂移、删除遗漏

“数据有三份”不是恢复策略。三份流复制副本会复制同一条误删;三个位于同一 存储故障域的节点也不是三个独立副本。第 21、32、35 章会分别验证备份恢复、 PITR 与损坏取证。

接入平面

接入平面把服务语义变成客户端可连接的端点:

name / VIP / address
port
TLS and authentication
pooling mode
target selector
health check
failover routing
connection and queue budget
session-state contract

一个主库 IP 只是位置,不是服务。客户端需要知道:

这个入口是否可写
是否允许陈旧读
是否 read-your-writes
发生切换时连接怎样断开
事务池能否使用 session feature
取消与超时如何传播

Pigsty 的 Service Access 用端口和 selector 表达 primaryreplicadefaultoffline 等服务。 第 22 章会把这些入口与 PgBouncer 语义、连接预算一起验收。

控制平面

控制平面改变系统的期望状态:

inventory and configuration
package and extension versions
cluster membership
leader election
switchover/failover decision
backup schedule and retention
role / database provisioning
change rollout and rollback

控制平面故障与数据平面故障不同。数据库可以继续处理流量,而 inventory 已经 漂移;也可能数据库本身健康,但错误的健康检查把流量切走。

控制平面必须回答:

  • 谁能改;
  • 改什么对象;
  • 如何审阅;
  • 是否幂等;
  • 如何观察收敛;
  • 哪一步是最后可逆点;
  • 失败时由谁接管。

Ansible 成功退出只说明某次自动化执行没有报告失败;它不是业务 SLO 证据。

观察平面

观察平面收集并解释:

metrics
logs
traces
catalog snapshots
query fingerprints and plans
backup manifests
configuration drift
synthetic probes
business invariants

观察平面不应只回答“图绿不绿”,还要让值班者完成因果链:

用户症状
  -> 受影响的服务入口
  -> 当前拓扑与流量目标
  -> 数据库等待/资源/复制/恢复状态
  -> 最近变更
  -> 可逆且风险最小的动作

第 25 章会验证信号到 runbook 的连接。本章只列出必须覆盖的八类信号,不声称 告警已经有效。

管理平面与业务平面不要共用身份

一个常见反模式:

application connection
  = object owner
  = extension installer
  = backup operator
  = cluster administrator

本书 fixture 已把角色分开:

身份 当前边界
pg36_app LOGIN、非 superuser、运行时
pg36_owner NOLOGIN、对象所有者
postgres 本地正式实验管理员

这还不是完整生产角色模型。第 23 章要继续拆出迁移、监控、备份、复制、审计 与应急身份。

一项能力会穿过多个平面

以“商品模糊搜索”为例:

data/compute  pg_trgm operator + GIN/GiST plan
storage       product table, index, WAL, backup
access        read endpoint, role, timeout
control       extension package/version, CREATE EXTENSION, schema
observe       latency, quality golden, index build, bloat, errors
governance    owner, lifecycle, upgrade and exit

只验证 SQL 正确,会漏掉四个平面;只部署组件,则连第一项也未必正确。

18.1.3 组件组合必须有统一服务目标

局部健康不推出整体健康

假设请求路径是:

client
  -> DNS/VIP
  -> HAProxy
  -> PgBouncer
  -> PostgreSQL primary
  -> transaction
  -> outbox
  -> relay
  -> event bus
  -> consumer

每个组件都有自己的“up”,但业务关心的是:

订单是否只创建一次
提交后多久可查询
事件多久送达
失败后是否可安全重试
是否会丢、重、乱序
能否在目标时间内恢复

如果数据库 20ms 提交、relay 卡 40 分钟,订单 API 的数据库延迟指标仍然很 漂亮,业务事件服务却已经违约。

先定义服务,再分配组件目标

一个服务目标模板:

维度 示例
用户动作 创建订单
成功定义 返回稳定 order_id,状态可读,重复请求无第二次效果
延迟 API P95/P99
正确性 约束、金额、状态、幂等全部成立
可用性 测量窗口、排除项、错误预算
新鲜度 提交后 read-your-writes;事件 publish lag
持久性 故障模型内的 RPO
恢复 场景化 RTO,不只“启动成功”
容量 峰值并发、增长与余量
安全 身份、租户、数据分类
成本 服务单位成本与预算

组件指标从这个目标推导。不要反过来因为某仪表盘有一个指标,就把它升格为 服务 SLI。

可用性相乘只是一种初步直觉

若一条同步路径必须经过多个独立组件,在非常简化的独立假设下:

Apath=i=1nAi A_{path} = \prod_{i=1}^{n} A_i

三个各 99.9% 的串行依赖约为:

0.999399.7003% 0.999^3 \approx 99.7003\%

这提醒我们“组件都三个九”并不保证路径三个九。但不能把这个公式当精确生产 模型,因为:

  • 故障往往相关,共享网络/电源/配置/身份;
  • 重试、缓存和降级会改变路径;
  • 读写操作依赖不同;
  • 维护与区域故障不是独立伯努利事件;
  • 业务正确性失败可能不表现为组件 down。

真正的目标要通过故障模型、演练和真实 SLI 验证。

新鲜度预算也会叠加

派生投影可能经历:

transaction commit
  -> outbox polling
  -> broker publish
  -> consumer queue
  -> indexing
  -> alias visibility

总 lag 不是只看 broker lag。每一段要有时间戳或位置:

阶段 证据
source commit commit LSN / business version
publication intent outbox timestamp
broker partition/offset
consumer acknowledged source identity
projection applied version / watermark
query served generation

没有共同 identity,端到端新鲜度无法对账。

正确性优先于可用性包装

“失败时返回旧缓存”可能提高响应可用性,也可能把已取消订单重新显示为有效。 降级必须按数据和操作分类:

public product description
  可允许有标签的短时陈旧

inventory availability
  陈旧读可能导致超卖,不能默认降级

payment state
  不能用缓存猜测

analytics dashboard
  可显示 last complete watermark

同一个缓存组件不能用一个统一 fallback 策略覆盖所有字段。

SLO 目标必须与证据状态绑定

本章目录中的:

"availability_target": "99.9%-proposal"

故意带有 -proposal。要去掉它,至少要经过:

ch19 environment baseline
ch20 HA failure model and drills
ch21 restore evidence
ch22 endpoint and connection behavior
ch23 security controls
ch24 approved SLI/SLO and ownership
ch25 signal and alert exercise
ch26 representative capacity

一项数字若没有:

measurement
window
population
exclusions
owner
alert
runbook
evidence retention

它只是愿望。

统一目标不等于统一部署

服务目标统一,是为了让组件协作;并不要求把组件放在同一主机、同一进程或 同一团队。

反过来,部署在同一 PostgreSQL cluster 也不自动意味着目标相同:

  • 交易写入需要 read-your-writes;
  • 离线分析允许 60 秒 lag;
  • 备份任务关注可恢复性;
  • 搜索 pilot 关注质量与构建时间。

平台要把这些 workload class 显式分开,并为冲突设优先级。

用一个“合同—证据—动作”闭环

每项服务能力最终应形成:

contract
  owner + semantics + objectives + limits

evidence
  SQL/catalog/metric/log/drill/manifest

decision
  accepted / conditional / pilot / rejected

action
  deploy / isolate / tune / externalize / rollback

review trigger
  version / scale / incident / objective / ownership change

本章的四份 JSON 和一个 ADR 正是在演示这个闭环。它们的价值不在文件格式, 而在于架构结论可以被程序拒绝、被证据更新,也可以在条件变化时退出。


返回本章目录 · 下一节:PostgreSQL 的强项与代价 · 查看全书目录 · 查看索引中心

18.2 PostgreSQL 的强项与代价

PostgreSQL 最容易被两种叙事误读:

“只是一个传统关系数据库”
“装上扩展就能替代所有数据系统”

前者低估它,后者透支它。本节把收益与代价放在同一张账上。

18.2.1 关系一致性、可扩展类型与统一查询

关系模型把错误状态变成不可提交状态

应用校验常写成:

read current state
if valid:
    write new state

当有多个写入者、并发事务、批处理和人工运维时,这段逻辑很容易被绕过。 PostgreSQL 的独特价值不是“也能做校验”,而是把很多不变量放在最终提交 边界:

PRIMARY KEY
UNIQUE
NOT NULL
CHECK
FOREIGN KEY
EXCLUDE
transaction isolation
row and table locks

不合法状态不只是“应用不建议写”,而是任何没有绕过权限边界的写入者都不能 提交。

第 4 章模型的关系校验和:

f8a7bfae59c6d16cd323abecfefe1014

第 18 章每轮只读审计都会重新计算。它不是通用完整性证明,但把平台蓝图锚定 到一份确定的业务数据,而不是空白数据库。

一致性靠多个层次共同完成

“用事务”仍然太笼统。一个可靠模型通常组合:

层次 适合表达
type/domain 单值表示和基本范围
column constraint 必填、局部规则
row CHECK 同一行字段关系
unique/exclusion 跨行唯一或不重叠
foreign key 引用存在与生命周期
transaction 多对象原子变更
lock/isolation 并发可见性与冲突
function/trigger SQL 约束难以表达的短原子规则
application workflow 远端调用、长流程、人机审批

越靠近数据的规则覆盖写入路径越广,但也越应短小、稳定、可解释。第 13 章 保留 accepted-with-scope,就是防止把业务编排全部塞进触发器。

类型不是列上的装饰

PostgreSQL 的类型参与:

storage representation
input/output validation
operator resolution
comparison and ordering
index operator class
planner statistics
function dispatch
wire protocol encoding

所以 timestamptz、range、jsonbvector 和 PostGIS geometry 不只是 不同的文本格式。类型、操作符和索引方法共同决定什么语义可以被查询与加速。

PostgreSQL 官方 Extending SQL 把数据类型、函数、聚合、操作符和索引操作符类都列为扩展点。这使一个扩展 能够进入规划器和执行器,而不只是作为应用旁边的黑盒服务。

第 16 章的:

ST_Intersects(...)
valid_during && ...
EXCLUDE USING gist (...)

之所以能与普通关系条件组合,正是因为空间与 range 语义进入了 PostgreSQL 的类型和索引体系。

统一查询减少一致性缝隙

如果订单、商品、空间和搜索投影都在同一事务边界,应用可以:

SELECT ...
FROM order
JOIN product ...
WHERE tenant_id = $1
  AND search_condition
  AND spatial_condition;

潜在收益:

  • 一个快照;
  • 一套权限;
  • 一个查询计划;
  • 一次网络往返;
  • 一个可解释的事务边界;
  • 少一条跨系统同步链;
  • 少一套重试、对账和删除流程。

这不是说“一条大 SQL 总是最好”,而是跨系统拆分具有固定税:

serialization
network
partial failure
duplicate delivery
ordering
freshness
identity mapping
observability correlation
rebuild
deletion

只有当拆分收益超过这组税,外置才有充分理由。

MVCC 让读写共存,但不消除冲突

PostgreSQL 的多版本并发控制让读者通常不阻塞普通写者,事务按快照看数据。 这使 OLTP、报表和维护可以在同一引擎协作。

但 MVCC 并不表示:

  • 长事务没有代价;
  • DDL 不需要重锁;
  • 所有隔离级别结果相同;
  • 副本读没有延迟;
  • dead tuple 会自动即时消失;
  • 写写冲突不需要处理。

统一引擎减少系统间一致性缝隙,内部仍要管理锁、快照、vacuum、序列化失败与 重试。这些成本在第 10、28、34 章分别展开。

统一系统目录让证据可查询

PostgreSQL 对象不是散落在配置文件中的传说。可以从 catalog 读取:

pg_class
pg_namespace
pg_proc
pg_type
pg_extension
pg_depend
pg_roles
pg_constraint
pg_index
pg_stat_*

本章 extension-catalog.sql 固定扩展名、版本、schema、owner、relocatable 与 comment; schema-catalog.sql 固定教学 schema 的 owner、marker 和对象计数。

这种自描述能力让平台可以做:

inventory
drift detection
privilege audit
upgrade preflight
backup/restore validation
extension ownership review

但 catalog snapshot 只说明数据库内部状态;操作系统 package、共享库和各 副本节点仍需要主机层 inventory。

可移植性要按层讨论

“SQL 标准”不能概括可移植性。至少分:

可能的锁定
schema/SQL PostgreSQL 语法、函数、行为差异
types range、array、JSONB、vector、geometry
indexes GIN/GiST/BRIN/opclass
routines PL/pgSQL 与 trigger
extensions binary/package/version
operations backup、replication、HA、monitoring
semantics collation、time zone、SRID、isolation

有价值的 PostgreSQL 特性不必因为“可能锁定”就不用;应为高锁定能力写 export、 restore、替代与退出测试。锁定被管理,和假装不存在,是两种完全不同的架构。

18.2.2 通用性带来的资源竞争与维护责任

同一进程体系,共享多种稀缺资源

当交易、搜索、空间和分析都进入 PostgreSQL,它们共享:

CPU cores
shared buffer cache
OS page cache
memory address space
storage latency and bandwidth
WAL pipeline
checkpoint budget
background workers
connection slots
locks and snapshots
autovacuum workers
backup and replication bandwidth

“查询彼此不锁”只覆盖其中一类竞争。

内存预算会按节点、worker 和并发放大

PostgreSQL 18 官方 Resource Consumption 说明 work_mem 是一个查询操作开始写临时文件前的基础内存限制;复杂查询可 同时有多个 sort/hash 操作,多个会话也会并行运行。

因此粗略风险模型不是:

memory=work_mem memory = work\_mem

而更接近:

memoryworksessions×operations×workers×effective_per_operation memory_{work} \approx sessions \times operations \times workers \times effective\_per\_operation

它不是精确容量公式,但足以拒绝:

“这一条查询 32 MB 不 spill,
 所以全局 work_mem 设成 32 MB。”

第 17 章的单查询反例只证明两种执行路径,不授权全局参数变更。第 27 章必须 在真实并发预算下做单变量实验。

CPU 并行会把延迟问题变成吞吐问题

并行查询可缩短一个聚合,但 worker 不是免费核心:

one query × 3 processes
ten queries × 3 processes
autovacuum + backup compression + replication

当机器已经饱和,更多并行可能让每条查询和系统总吞吐都变差。平台要同时看:

  • 单查询 latency;
  • 总吞吐;
  • runnable queue;
  • worker 是否实际获得;
  • 交易查询 tail latency;
  • background maintenance 债务。

搜索与空间索引有写放大和生命周期

一个索引的成本不仅是磁盘:

insert/update CPU
WAL volume
cache footprint
vacuum work
backup size
replica replay
build/rebuild window
statistics
upgrade compatibility

GIN、GiST、HNSW、BRIN 与 B-tree 的目标和代价不同。给每个新查询“加个索引” 可能把读延迟转移成写入、恢复与维护事故。

服务目录因此按 extension bundle 准入,而不是允许每个租户自由安装任意扩展。

WAL 是统一持久性的收益,也是共享压力面

在一个 PostgreSQL cluster 内,许多变更进入同一 WAL/复制/归档链。收益是 恢复语义统一;代价是:

  • 大批量索引构建可能推动 WAL;
  • 分析汇总刷新可能影响 replica lag;
  • 一个归档故障会积压整个 cluster;
  • logical slot 可能阻止 WAL 回收;
  • 恢复需要理解所有扩展对象。

把某能力外置也不会让代价消失,只会把它变成跨系统日志与对账。选择应比较 两边完整成本。

长快照会制造维护债务

慢报表在 primary 上运行时,即使是只读,也可能:

延长旧 tuple 可见需求
阻碍 vacuum 清理
增加表/索引膨胀
与 DDL 锁冲突
占用连接和内存
挤压 cache

offline replica 可以隔离一部分 CPU/I/O 与连接,但 replica 上的长查询可能 与 WAL replay 冲突,导致查询取消或 replay 延迟。它是新的合同,不是免费 读扩容。

一个 cluster 的爆炸半径必须被命名

共享 cluster 中,以下事件可能影响多个 database:

server crash
shared_preload_libraries error
disk full
WAL/archive failure
OS/package upgrade
major version upgrade
superuser mistake
host/network failure
HA control-plane error
backup repository problem

schema 隔离挡不住这些;database 隔离也挡不住 cluster 级失败。只有在信任、 性能、升级、恢复或故障影响需要时,才上升到 instance/cluster 隔离。

维护责任不会被“开源免费”消除

每一项能力都要有人承担:

version selection
security advisories
package availability
configuration
monitoring
capacity
backup/restore
replication
upgrade
incident response
deprecation
data export

许可证成本为零,运行责任仍然存在。一个无人负责的扩展,比一个功能较少但 有人值班的基线更危险。

用资源账本评估组合

对每项 workload 建议记录:

峰值 隔离/配额 超限动作
active connections 待测 pool + role limit queue/reject
CPU 待测 service/host class shed/defer
work memory 待测 role/session policy spill/cancel
temp bytes 待测 temp_file_limit fail query
statement time 待测 workload timeout cancel
storage growth 待测 forecast/alarm expand/archive
WAL rate 待测 capacity/repository throttle/fix
replica lag 待测 endpoint freshness remove target
maintenance debt 待测 vacuum window remediate

未知值要写 unknown,然后由第 25–28 章补证据;不要填一个未经测量的舒服 数字。

18.2.3 扩展能力不自动等于生产就绪

CREATE EXTENSION 实际做了什么

PostgreSQL 18 官方 CREATE EXTENSION 说明,命令根据 control 与 SQL 脚本创建函数、类型、操作符、索引支持方法等 对象,并在 catalog 中记录它们的归属。

它还明确提醒:

  • 支持文件必须先安装在 server;
  • 某些扩展需要 superuser;
  • 安装脚本本身属于信任边界;
  • 不安全 search_path/可写 schema 可能带来风险;
  • IF NOT EXISTS 不保证现有同名扩展就是期望对象。

因此:

available != installed
installed != configured
configured != validated
validated in dev != admitted in production
admitted != permanently supported

扩展准入的九道门

需要回答
来源 包来自哪里,如何验证与更新?
版本 PostgreSQL major/minor、扩展版本是否钉住?
节点一致 primary、replica、恢复目标都有相同 binary?
安装安全 trusted/superuser、schema、owner、search_path
配置 是否需要 preload、GUC、worker、restart?
数据行为 类型、索引、collation、序列化语义是否固定?
运行成本 CPU、内存、WAL、vacuum、存储与构建窗口?
恢复升级 dump/physical restore/replica/PITR/major upgrade?
退出 如何导出、降级、替代、删除?

任一关键门为 unknown,生命周期就不能写 accepted

relocatable 只回答 schema 迁移的一小部分

本章 catalog 会看到:

pg_trgm      relocatable=true
vector       relocatable=true
btree_gist   relocatable=true
postgres_fdw relocatable=true
postgis      relocatable=false
plpgsql      relocatable=false

extrelocatable=true 只表示扩展控制文件允许改变其对象所在 schema。它不说明:

  • data portable;
  • binary cross-version compatible;
  • replica package 已存在;
  • upgrade 可回滚;
  • 对象 owner 安全;
  • 业务语义不变。

不要从一个 catalog 布尔值推导整个生命周期。

preload 失败可能阻止实例启动

一些扩展需要 shared_preload_libraries。Pigsty 4.5 的 Extension Config 说明可用 pg_libspg_parameters 声明 preload 与参数,并提醒 preload 库缺失或加载失败可阻止 PostgreSQL 启动,修改 preload 还需要重启。

这把扩展从 database 范围提升到 instance 范围:

一个数据库想用
  -> 每个节点需要 package
  -> instance startup config 改变
  -> rolling restart / HA 行为
  -> 整个 cluster 的故障风险

所以多租户平台不能允许任意 database owner 自助改变 preload。

本书当前扩展账本

只读实验在 PostgreSQL 18.6 上固定:

扩展 版本 schema owner 生命周期
pg_trgm 1.6 shop_ch14 pg36_owner accepted
vector 0.8.4 shop_ch14 postgres pilot
btree_gist 1.8 shop_ch16_ext pg36_owner conditional
postgis 3.6.4 shop_ch16_ext postgres conditional
postgres_fdw 1.2 shop_ch17_ext postgres lab-only
plpgsql 1.0 pg_catalog postgres core

版本表是 fixture 事实,不是对所有 PostgreSQL 18 环境的要求。平台应按自己的 软件仓库与升级策略重新验收。

owner 差异是需要解释的证据

为什么部分扩展 owner 是 postgres,部分是 pg36_owner

  • trusted 与非 trusted 安装权限不同;
  • 扩展脚本可能创建需要高级权限的对象;
  • extension object owner 与内部对象 owner 可能不同;
  • dump/restore 与后续 upgrade 会使用这些身份。

本章只记录现状。第 23、30 章要决定生产 owner 模型,并证明升级和恢复不依赖 一个无人管理的超级用户流程。

物理复制不等于安装包复制

physical replica 会复制 data files 和 catalog 状态,不会替你把共享库包安装 到新主机。若 primary catalog 依赖某 .so,目标节点缺包,相关查询会失败; 若该库需要 preload,实例启动也会失败;逻辑恢复和升主后的业务验收同样无法 通过。

因此节点准入应核对:

OS/repository identity
PostgreSQL package/version
extension package/version
shared library presence
control and SQL update paths
preload order
catalog extversion

第 19 章负责基线,第 30 章负责升级顺序。

备份成功不是扩展恢复成功

要验收扩展恢复,至少在隔离空目标中:

  1. 安装目标 PostgreSQL 与确切扩展 package;
  2. 恢复 base backup / WAL 或 logical dump;
  3. 验证 pg_extension、types、operators、indexes 与 dependencies;
  4. 运行该扩展的业务 golden;
  5. 验证 replica 与应用权限;
  6. 保存 manifest 和校验和。

只看 pgBackRest 命令 exit 0,不能证明 PostGIS geometry、vector index 或 自定义 opclass 可用。

用 bundle 控制组合,而不是逐扩展放任

本章服务目录定义:

core
search-accepted
vector-pilot
spatiotemporal-conditional
federation-lab

offering 引用 bundle,bundle 有生命周期与 gate。这带来三个好处:

  • 同一组相互依赖的 package/config 一起审阅;
  • 服务等级明确允许什么;
  • 升级与退出有完整影响面。

例如 pg-ha-standard 默认不允许 federation-labvector-pilot 只能走 exception;这比“机器上有包,所以谁都能 CREATE”更可治理。

反例:把实验 FDW 变成生产

第 17 章为了在本地 loopback 无密码访问,明确写了:

{
  "password_required_false": true,
  "production_permitted": false
}

第 18 章的负例把 production_permitted 改成 true,validator 必须报:

E_FDW_LAB_ONLY

这个反例传达一种重要写作纪律:实验里为了隔离机制而采用的简化,必须被机器 可见地阻止进入生产蓝图,而不是靠读者记得某段警告。

准入是可撤销决定

即使已经 accepted,也需要 review trigger:

PostgreSQL major/minor change
extension version or package source change
security advisory
workload/scale change
restore or upgrade rehearsal failure
incident
owner/support change
upstream abandonment

生产就绪不是一次性勋章,而是一项持续有证据支持的状态。


上一节:从数据库产品到能力组合 · 返回本章目录 · 下一节:明确替代边界 · 查看全书目录 · 查看索引中心

18.3 明确替代边界

替代边界不是“PostgreSQL 对阵某产品”的功能打勾表。真正的决策单位是:

某一类数据
在某一类操作下
需要某种一致性/时效/规模/故障语义
由某个团队承担

同一个系统可以对一类数据是权威,对另一类数据只是派生副本。

18.3.1 缓存、消息、对象存储与离线湖仓

缓存解决重复读取,不解决权威

缓存适合:

结果计算或读取昂贵
值允许在有限时间内陈旧
miss 可以回到权威
entry 可丢弃并重建
容量与淘汰可接受

缓存不适合被默认为:

库存真相
支付状态真相
唯一事件日志
不可恢复业务数据
跨字段事务约束

本章 product-cache-v1 的 authority:

{
  "product_business_state": "postgresql",
  "cache_entries": "cache"
}

这不是文字游戏。假如缓存中 product.version=8,PostgreSQL 已是 version=10,规则必须保证版本 8 永远不能覆盖版本 10。

cache-aside 的完整状态机

常见读路径:

read cache
  hit -> verify acceptable version/freshness -> return
  miss -> bounded read from PostgreSQL
       -> write versioned cache entry
       -> return

写路径不能只写成“更新 DB 后删缓存”。必须考虑:

DB commit 成功,删除 cache 失败
cache 删除成功,DB transaction 回滚
两个写并发,旧 invalidation 后到
TTL 前后请求同时回源
热点 key 穿透
删除后的旧值复活
cache 整体不可达

因此合同包含:

字段 product-cache-v1
ordering source version 单调,旧版本不覆盖新版本
idempotency product_id + source version
rebuild 丢弃 namespace,从 PostgreSQL 重建
failure miss/outage 回退到有界 PostgreSQL 读
reconciliation 抽样 version + payload checksum
deletion tombstone 或 version bump
exit 关闭 cache 路由并删除派生 namespace

若无法丢弃 namespace,缓存已经悄悄变成了数据库。

消息系统拥有投递,不拥有原事务

事件总线擅长:

异步解耦
一对多消费
保留与重放
按 partition 排序
消费者独立进度
流式处理

但“先更新数据库,再发送消息”有经典双写:

DB commit, publish fail   -> 状态变了,没有事件
publish success, DB fail  -> 有事件,没有状态
retry publish             -> 重复事件

order-events-v1 选择 transactional outbox:

one PostgreSQL transaction
  business state change
  publication intent with stable event_id

external relay
  read pending outbox
  publish at least once
  mark/record progress

原子边界只到 outbox。外部投递是 at-least-once,消费者必须幂等。

“at-least-once”要写清谁至少一次

这几个语义不同:

relay may publish duplicate
broker may redeliver
consumer may process then crash before ack
business side effect may be non-idempotent

稳定 event_id 是起点,不是终点。消费者需要把“已处理 identity”与业务 效果放在同一原子边界,或使用业务幂等键。

全局顺序通常也不应承诺。本章只承诺:

partition by order_id
no global order

否则团队会为一个业务不需要的全序付出吞吐与可用性成本。

对象存储拥有大字节,数据库拥有元数据状态

把图片、视频和归档大对象直接塞入 PostgreSQL 并非绝对错误,但常见代价:

database/backup/WAL volume
cache 污染
复制与恢复时间
CDN/分段下载能力
对象生命周期与访问方式

product-media-v1 采用逐域 authority:

object bytes                         -> object storage
object identity / owner / state /
size / checksum / active generation -> PostgreSQL

可靠上传状态机可以是:

reserve metadata row (uploading)
  -> upload immutable object version
  -> verify size + checksum
  -> finalize metadata (ready)
  -> expose signed access

失败处理:

orphan object   -> quarantine + reconciliation
ready row but missing bytes -> do not return success
retry upload    -> same object_id + checksum
replace media   -> new immutable generation, then switch metadata
delete          -> tombstone, retention-aware object deletion

这里不存在跨 PostgreSQL 与对象存储的传统 ACID transaction,所以显式状态、 幂等与对账比“调用顺序看起来正确”重要。

湖仓拥有分析投影,不拥有在线业务解释

离线湖仓适合:

超长保留
列式大扫描
批处理与多引擎消费
历史快照
低成本冷数据
复杂数据科学管线

analytics-export-v1 的权威拆分:

operational business state -> PostgreSQL
analytical projection      -> lakehouse

每个数据集必须显示:

dataset generation
schema/version
source snapshot identity
source commit position or watermark
complete/incomplete state
freshness
reconciliation result

当 CDC 断裂时,正确行为不是悄悄继续展示旧数据,而是标记 last complete watermark,并根据合同停止或降级消费者。

snapshot + change stream 的一致性接缝

建立新投影通常需要:

take consistent snapshot at position P
load snapshot
consume changes after P
deduplicate/order by source identity
catch up
publish generation

如果 snapshot 与 stream 之间没有稳定接缝,会漏掉或重复一段变化。第 29 章 会把它写成迁移状态机并验收。

外置并不自动降低数据库压力

反例:

  • cache miss storm 反而打爆 primary;
  • CDC slot 积压使 WAL 无法回收;
  • 搜索全量重建持续扫描并推高 I/O;
  • 湖仓导出占据 replica 和网络;
  • object reconciliation 每晚做无界全表 join;
  • outbox relay 用无索引轮询。

每个外置系统都要预算它对 PostgreSQL 的读取、WAL、连接与保留压力。

18.3.2 超大规模检索、流处理与专用分析

“超大规模”必须换成阈值

以下句子都不能触发架构:

以后数据很多
搜索会很复杂
实时要求很高
分析师越来越多
行业都这么做

需要换成可测量的问题:

indexed documents / vectors / bytes
ingest/update/delete rate
query mix and concurrency
P50/P95/P99 latency
quality metrics
language/analyzer needs
faceting/aggregation shape
retention
rebuild time
failure-domain objective
unit cost
team operating capacity

当前值可以是 unknown,但触发器不能是形容词。

外部检索的真正触发器

PostgreSQL 内搜索常在这些条件下很有优势:

  • 数据与交易状态同库,提交即搜索;
  • 过滤与关系 join 很重要;
  • 规模在单 cluster 预算内;
  • 搜索功能较集中;
  • 团队不想承担额外索引同步系统;
  • 正确性与删除要求高。

专用检索可能在以下证据出现后更合适:

语言分析/相关性能力缺口
索引规模和构建时间越界
高扇出 facet/aggregation 压垮 OLTP
独立扩缩容或故障域是硬目标
多源统一搜索成为主需求
ANN 质量/规模/延迟无法满足
搜索团队能承担独立服务

切换前必须用同一 query set 与 relevance golden 比较,不能只比一条延迟。

搜索外置仍要保留 PostgreSQL fallback 分类

不是所有查询都能回退:

查询类 外部搜索故障时
exact product ID 直接 PostgreSQL
简单关键词 可按容量回退 PostgreSQL
复杂 facet 返回有标签旧 generation 或失败
semantic ANN 可能没有等价 fallback
admin reconciliation 延后,不影响交易

fallback 本身也需要容量演练。平时 1% 回源的 primary,未必承受 100% 搜索 流量。

流处理适合持续状态与事件时间计算

PostgreSQL + outbox + worker 可以完成大量异步任务。当需求升级为:

高吞吐多分区事件
大量独立 consumer
长保留重放
event-time window
watermark / late data
stateful join
continuous aggregation
backpressure

专用流平台可能更合适。

但流系统不会自动给出业务 exactly-once。即使框架内部有 exactly-once checkpoint,外部数据库、HTTP 副作用和人工操作仍需幂等与对账。应写:

source delivery semantics
processing state semantics
sink effect semantics
recovery/replay semantics

而不是笼统宣布“端到端 exactly once”。

专用分析的触发器是工作负载边界

单节点或 offline replica 先做:

better model and grain
statistics
index/BRIN
partition pruning
parallelism
summary/materialization
batching
resource isolation

当代表性证据仍显示:

  • scan/compute 越过单节点;
  • retention/压缩成本不可接受;
  • 并发与查询形状冲突;
  • 维护窗口不可满足;
  • OLTP 和分析无法充分隔离;
  • 横向扩展收益足以覆盖分布代价;

才比较 Citus、兼容分布式 PostgreSQL、列式引擎或湖仓。

第 17 章已经展示分布式新增问题:

distribution key
skew
co-location
pushdown
cross-shard transaction
global uniqueness
rebalance
partial failure
remote authentication
version compatibility

专用系统不是“更高级的 PostgreSQL 参数”,而是新的数据与故障模型。

查询兼容和业务兼容要分开

候选声称“PostgreSQL compatible”时,至少核对:

wire protocol
SQL grammar
type semantics
transaction/isolation
constraints
extensions
system catalogs
planner/explain
backup/restore
HA/failure behavior
driver/tooling
operational ownership

能连上 psql 只证明协议入口;能跑 SELECT 只证明一个查询子集。

用两道门防止过早与过晚拆分

进入外置评审:

measured problem
representative workload
PostgreSQL baseline already optimized appropriately
service objective still missed
candidate benefit is causal
owner and budget exist
data contract and exit path written

继续留在 PostgreSQL:

objective met with safe headroom
resource competition bounded
recovery/upgrade path passes
team can operate it
externalization tax exceeds measured benefit

门是双向的。不能因为过去留在 PostgreSQL,就永远拒绝新证据。

18.3.3 用数据所有权、时效和一致性决定分工

第一步:把名词换成数据域

不要问“谁是 source of truth”,问:

order business state?
payment settlement state?
publication intent?
message replay log?
product description?
search ranking projection?
media bytes?
media lifecycle metadata?
analytical dataset?
cache entry?

同一个订单事件流程可能有三个权威:

order state          PostgreSQL
publish intent       PostgreSQL outbox
delivery/replay log  event bus

它们不冲突,因为 authority 的数据域不同。

第二步:为每个读者写时效

“最终一致”没有时钟。改写为:

product cache TTL <= 300s, plus versioned invalidation
offline replica replay lag P99 <= proposed 60s
search indexing lag <= target before activation
analytics dataset labels its last complete watermark
order event publish P99 target set in chapter 24

还要说明超限:

serve stale with label
fallback to authority
reject request
remove replica from routing
stop dataset publication
page operator

第三步:把一致性写成可观察规则

示例:

cache version never replaces a newer PostgreSQL version
product deletion tombstone survives retry and generation swap
event_id stable across relay retry
consumer business effect deduplicated by event_id
analytics latest row selected by source commit position
object ready state requires matching immutable checksum

这些规则可以生成 negative test;“保持一致”无法测试。

第四步:定义失败矩阵

对每个跨系统合同列:

故障 写入结果 读取结果 重试 告警/对账
PostgreSQL down 是否拒绝 cache 是否可陈旧读 谁重试 哪个 owner
external sink down source 是否可提交 是否 fallback backlog 边界 lag
network partition 双方如何判定 是否可能分歧 幂等 identity reconciliation
projection corrupt source 不受影响 停止/旧 generation rebuild checksum
credential expired source 不受影响 外置路径失败 更新 secret auth alert

如果团队只讨论 happy path,外置系统只是把事故推迟到上线后设计。

第五步:写重建,而不只写备份

派生系统最强的恢复策略通常是从权威重建。但“可重建”必须证明:

source retains enough history
consistent snapshot can be taken
change position can be bridged
schema/version transformation is reproducible
capacity allows rebuild within objective
live changes do not get lost
quality/checksum validation exists
generation can be atomically published
old generation can be rolled back

若不能满足,它就不是“随时可丢的副本”。

第六步:删除是一等数据流

创建与更新容易被关注,删除经常遗漏:

cache TTL 让敏感字段继续存在
search rebuild 把已删文档复活
event replay 重新制造旧状态
lakehouse retained versions 留下受监管数据
object storage versioning 保留字节
backup retention 与删除政策冲突

每个合同必须有 deletion 字段,并与法律/业务保留策略对齐。删除不能简单理解 为对每个系统发一条 DELETE;它需要 tombstone、generation、审计和最终 验证。

第七步:退出路线与进入路线同时审批

一个可退出设计至少回答:

如何停止新写入/投影
如何 drain 消费者
最后 position/manifest 在哪里
客户端怎样切回或切到替代服务
如何验证没有遗漏
旧数据何时删除
凭据与资源何时回收
谁宣布完成

本章蓝图包含五条退出原则。validator 的反例把 exit_paths 清空时必须返回:

E_EXIT_PATH

一个通用决策表

问题 留在 PostgreSQL 倾向 外置倾向
是否需与业务写原子提交 需 outbox/saga
是否频繁关系 join/filter 需复制/反规范化
是否可容忍陈旧
是否可从权威重建 非必要 必须
是否需独立扩缩/故障域
单节点资源是否越界 可先优化/隔离 证据越界后强
专用语义是否关键 扩展可满足则强 缺口明确则强
团队是否能多系统值班 简化更强 能力充足才强
删除/对账是否成熟 简单 必须成熟
退出成本是否可接受 必须明示

表不会自动给出答案;它强迫评审把隐性成本说出来。

用合同 ID 连接能力

本章九项 capability 中,外置或可能外置的能力引用:

lexical search        -> external-search-projection-v1
semantic search       -> external-search-projection-v1
spatiotemporal        -> analytics-export-v1
operational analytics -> analytics-export-v1
product cache         -> product-cache-v1
order delivery        -> order-events-v1
media bytes           -> product-media-v1

引用让我们可以回答:

  • 某合同被哪些能力使用;
  • 某合同删除后哪些蓝图断裂;
  • 是否有无人引用的外置系统;
  • 是否所有外置能力都有 owner;
  • 外置触发器是否存在。

validate.py 会拒绝悬空引用,但业务评审仍要判断合同内容是否真实可行。

不选择具体产品也是有效决定

本章故意不为 cache、event bus、object storage、search 或 lakehouse 指定 品牌。原因不是逃避,而是先稳定更长寿的合同:

authority
delivery semantics
freshness
rebuild
security
exit

候选产品要在这些合同下比较。若先选产品,团队很容易把产品默认行为冒充业务 需求。

最终判断句式

一项成熟决定应能写成:

对数据域 X,由系统 A 保持权威;系统 B 以语义 Y 持有派生投影,目标新鲜度 为 Z,失败时按 F 退化,以 identity I 幂等并按 R 对账,可通过步骤 E 重建 和退出。只有触发器 T 出现且 gate G 通过后,B 才进入生产。

能写清这句话,才算划定边界。


上一节:PostgreSQL 的强项与代价 · 返回本章目录 · 下一节:平台服务目录与多租户 · 查看全书目录 · 查看索引中心

18.4 平台服务目录与多租户

如果消费者只能申请“一台 PostgreSQL”,平台交付的是资源,不是服务。

一个可用目录应该让消费者在申请前就知道:

得到什么
不保证什么
允许怎样使用
谁负责什么
怎样计量
何时复审
如何退出

18.4.1 服务等级、规格、版本与扩展套餐

offering 是完整合同,不是机器型号

本章的 service-catalog.json 为每个 offering 保存:

id / status / environment
service owner
consumer owner requirement
topology
service objectives
isolation
backup class
allowed and exception extension bundles
quotas
lifecycle

机器 CPU/内存/磁盘规格当然重要,但它只是实现 offering 的一种资源配置。 同一 pg-ha-standard 可以在不同硬件代际上实现;只要服务合同和经过验证的 容量仍然成立,消费者不应绑定某台主机。

pg-dev

定位:

development-test
single PostgreSQL instance
no HA commitment
explicit expiry
best effort

它允许较广的试验 extension bundle,包括 federation-lab。这不意味着开发 环境可以无治理:

  • 必须有 consumer owner;
  • 连接、存储、temp、statement time 要声明;
  • 必须有到期日;
  • 敏感生产数据不能因为“只是 dev”就复制进去;
  • 实验凭据不能进入 Git;
  • 删除仍需精确目标和恢复需求确认。

开发 offering 的目标是加快安全实验,不是变成永不下线的影子生产。

pg-ha-standard

这是 pg36_shop 的默认生产候选:

primary + two replicas
reviewed failure domains
dedicated database by default
dedicated cluster when risk gate requires
continuous WAL + full/differential backup class

目录中:

availability_target=99.9%-proposal
rpo_seconds=60
rto_minutes=30

这些是要进入第 20、21、24 章验证的目标。它们不能由“三节点”直接推出。

允许的 bundle:

core
search-accepted
spatiotemporal-conditional

vector-pilot 只能走 exception;federation-lab 不允许。

pg-ha-critical

critical 不是把 standard 的数字改得更漂亮。它意味着:

dedicated cluster
explicit synchronous policy
explicit zero-RPO failure-domain scope
hard connection/overload budget
storage includes failure + upgrade headroom
quarterly objective/owner review
architecture and risk approval

目录提出:

99.95%-proposal
RPO 0 in explicitly rehearsed failure domain
RTO 10 minutes

“RPO 0”必须带范围。同步副本若与 primary 共享电源/机房,不能自动覆盖整个 区域故障;若为可用性临时降级同步策略,也可能改变承诺。

pg-analytics-offline

它不是第二个 source of truth:

read-only dependent service
offline replica
separate analytical query SLO
freshness proposal 60 seconds
not read-your-writes
source cluster backup policy
bounded analytics pool

Pigsty 官方 Cluster / Instancepg_role: offline 定位为慢查询、ETL、OLAP 与交互分析专用 read-only replica。隔离的目的不是“让慢查询没人管”,而是不让它默认争用在线 replica。

规格要由容量曲线产生

不要先定义:

small=4c16g
medium=8c32g
large=16c64g

再假设业务会自然匹配。更可靠的顺序:

  1. 定义 workload class;
  2. 固定数据量、并发和增长;
  3. 测量容量曲线和饱和点;
  4. 选定安全 headroom;
  5. 映射到可采购/可部署资源;
  6. 定义升降配条件。

硬件 SKU 可以变化,基准 workload 与目标不应随意变化。

版本是一组矩阵

平台版本不只一个 postgresql=18

OS
kernel / filesystem
PostgreSQL major + minor
extension packages
Patroni
etcd
HAProxy
PgBouncer
pgBackRest
exporters / monitoring
Pigsty release
client drivers
locale / collation

兼容矩阵至少回答:

  • 哪组是当前支持;
  • 哪组是下一升级候选;
  • 哪组已弃用;
  • package repository 能否重建;
  • backup/restore/replica/upgrade 是否覆盖;
  • 安全补丁时限;
  • 谁批准例外。

extension bundle 是服务的一部分

bundle 不只是安装清单:

{
  "id": "vector-pilot",
  "status": "pilot",
  "extensions": [
    {
      "name": "vector",
      "version_policy": "0.8.4 on validated fixture",
      "lifecycle": "pilot"
    }
  ],
  "gates": [
    "model identity and dimension pinned",
    "exact and ANN quality pass",
    "backup, restore, replica, upgrade rehearsed"
  ]
}

offering 只引用已审阅 bundle,避免每个用户自行拼装无法升级的组合。

服务目标要分平台与消费者责任

平台可负责:

cluster availability
service routing
backup execution and restore capability
database engine/extension lifecycle
platform monitoring
capacity envelope
incident coordination

消费者仍负责:

SQL/schema quality
业务不变量
连接/重试/timeout
流量预测
数据分类
应用兼容测试
on-call contact
错误预算决策

责任边界要写 RACI,不能在事故时才争论“数据库没问题还是应用没问题”。

目录本身也需要版本与状态

本章目录:

release=1.6-proposal
status=proposal
review_cycle_days=90

当下卷 evidence 完成后,应产生新 release,而不是原地把历史 proposal 改成 “一直就是通过”。目录是决策记录的一部分。

18.4.2 数据库、模式、实例和集群隔离

四层隔离解决不同问题

层级 隔离了什么 没隔离什么
schema 名称、对象组织、部分权限 database GUC/连接、WAL、资源、故障
database catalog、连接入口、部分设置 instance CPU/I/O/WAL、superuser、升级
instance postmaster、内存、端口、WAL、配置 host/kernel/storage/network
cluster/service HA/恢复/生命周期边界 若共主机/控制平面仍可能相关

“tenant 一个 schema”与“tenant 一个 cluster”不只是成本不同,安全与爆炸半径 完全不同。

schema 隔离

适合:

同一信任域
同一生命周期
需要跨 schema 查询
共享 database 设置可接受
对象数量可控

风险:

search_path 注入
误授权 CREATE
同名对象解析
共享 connection/GUC
owner/superuser 可见
跨 schema 依赖
升级/删除耦合

安全做法:

  • 不给不可信运行时角色在公共解析路径中的 CREATE
  • 对安全定义函数固定安全 search_path
  • 使用显式 schema qualification;
  • 分离 NOLOGIN owner 与 LOGIN runtime;
  • 定期审计 ACL 与 dependencies。

database 隔离

适合:

同一 instance 中的中等信任边界
独立连接、schema/catalog、extension installation
不要求跨 database transaction/join
共享主机与实例故障可接受

优点是 namespace 和连接边界更清楚;代价是:

  • PostgreSQL 原生跨 database 查询不透明;
  • connection pool/监控/迁移对象增多;
  • extension 每 database 管理;
  • 仍共享 WAL、CPU、I/O、backup 与 major upgrade;
  • instance superuser 仍可越界。

database 不是强资源隔离。

instance 隔离

适合:

不同 preload/GUC/major
独立 restart/upgrade
强一些的内存/连接/WAL边界
不同维护窗口
高风险扩展或 workload

同一 host 上多个 instance 仍共享:

CPU
kernel
filesystem/storage
network
host administrator
power/failure

必须配 OS/cgroup/volume/port/backup 与监控隔离,否则只是多个 postmaster 抢 同一资源。

cluster 隔离

适合:

独立 HA and recovery objective
独立故障/升级窗口
高敏感或不互信 tenant
重 workload/noisy neighbor
特殊 extension/kernel
法规或数据驻留
独立成本归属

代价是节点、备份、监控、升级、值班和容量碎片化。不是所有小数据库都值得 一个三节点 cluster。

选择顺序从信任与故障开始

推荐问:

  1. tenant 是否互信?
  2. 数据分类是否允许共实例/共主机?
  3. 是否允许同一个 superuser/平台团队访问?
  4. workload 能否互相制造事故?
  5. 是否需要不同 PostgreSQL/extension/preload?
  6. 是否需要独立 failover/restore/upgrade?
  7. 跨 tenant 查询/事务是否必要?
  8. 成本和运营能力是否承受更高隔离?

安全硬边界优先于便利与成本。

多租户至少有三种模型

shared tables + tenant_id
  最省对象;需要强 tenant predicate/RLS 与索引设计

schema per tenant
  对象分开;迁移、对象数量、search_path 复杂

database/cluster per tenant
  隔离更强;运营和容量成本更高

也可以混合:

small trusted tenants -> shared
larger tenants        -> database
regulated/high-risk   -> dedicated cluster

但迁移路径要提前设计:如何从 shared 提升到 dedicated,identity、sequence、 外键、事件与投影如何移动。

RLS 是纵深防御,不是唯一边界

Row-Level Security 可以根据角色/会话上下文限制行,但需要审计:

table owner / BYPASSRLS
FORCE ROW LEVEL SECURITY
session tenant context injection
pooling mode
prepared statements
security definer functions
COPY / maintenance paths
foreign keys and side channels
backup/replica/admin access

第 23 章会用正负 tenant 测试验证。不能因为 CREATE POLICY 成功,就宣布 隔离完成。

隔离与可观测性必须同粒度

若配额按 tenant,指标却只能看整个 cluster,就无法:

  • 识别 noisy neighbor;
  • 归属成本;
  • 证明公平性;
  • 按 tenant 降级;
  • 预测迁移。

需要在不暴露敏感 SQL/数据的前提下,保留 service/database/role/workload class/tenant 等必要维度,并控制 cardinality。

18.4.3 成本归属、配额和生命周期

没有成本归属,平台会奖励浪费

共享数据库中,使用者容易只看到:

“多一张表”
“多一个索引”
“多跑一条报表”

平台承担的真实成本:

compute
memory
primary + replica storage
WAL + archive
backup repository
network
monitoring retention
upgrade window
on-call labor
recovery time
capacity headroom

成本模型不一定要精确计费,但必须让 owner 看见边际影响。

用服务单位表达成本

可以按组织成熟度从简单到复杂:

allocated cluster share
database GB-month × replica/backup multiplier
connection/CPU class
WAL GB and backup retention
query or workload class
dedicated instance/cluster fixed cost
operator support tier

避免只按主表大小计费:索引、TOAST、replica、backup 与 WAL 可能远大于主表。

配额是正确性保护,不只是节省

重要配额:

配额 保护
connection 防止 backend/内存/调度耗尽
active query 防止过度并发
statement timeout 限制无界执行
lock timeout 限制排队与级联
idle transaction 防止长快照/持锁
work_mem class 限制乘法内存
temp_file_limit 防止 spill 填盘
storage/WAL growth 保护恢复链与容量
logical slot retention 防止 WAL 无限保留
maintenance window 保证 vacuum/index/backup

配额超限行为要确定:

queue
reject
cancel
spill
throttle
degrade
escalate

静默超卖不是服务。

连接预算从端到端计算

不要让每个应用实例都配置:

pool_max = max_connections

预算应满足:

pools+admin+monitoring+maintenance+HA reservesafe backend budget \sum pools + admin + monitoring + maintenance + HA\ reserve \leq safe\ backend\ budget

还要考虑:

  • 每个应用副本数;
  • failover 后流量汇聚;
  • pool retry storm;
  • transaction vs session pooling;
  • offline/replica 独立预算;
  • emergency admin 保留。

第 22、34 章会做饱和与过载演练。

生命周期从申请前开始

完整状态机:

request
  -> classify data/workload/objective
  -> choose offering/isolation/bundles
  -> owner and cost approval
  -> provision
  -> acceptance evidence
  -> operate
  -> change / exception
  -> periodic review
  -> deprecate
  -> drain/export
  -> retain/erase
  -> decommission evidence

若没有 expiry/decommission,临时数据库会永久占用备份、监控和升级路径。

变更要分普通与例外

普通变更:

catalog-defined offering
supported version
allowed extension bundle
within capacity envelope
standard backup/security

例外:

pilot extension in production
custom preload
unsupported version
RPO/RTO override
over-quota
cross-boundary data
lab authentication

例外必须有:

owner
risk
compensating control
evidence
expiry
review date
rollback

“临时允许”但没有到期日,通常等于永久。

删除与退役是高风险动作

本书实验里的 reset 有精确 token、target、marker 和 active-session guard; 生产退役还要更多:

  1. 确认 owner 与法律/业务 retention;
  2. 停止新连接和写入;
  3. 记录最后 backup/manifest;
  4. 导出需要保留的数据;
  5. drain 外部消费者和投影;
  6. 验证 DNS/service/secret/monitoring 依赖;
  7. 双人审批 destructive target;
  8. 擦除或进入保留;
  9. 保存完成证据。

“在 inventory 中删掉一段 YAML”不等于数据已经安全退役。

复审由事件与周期共同触发

周期复审:

owner
objective
usage/cost
capacity
version/support
exceptions
expiry

事件复审:

major workload change
new data class
extension/version change
incident
failed restore/upgrade
ownership change
externalization trigger
provider/platform migration

平台目录最终要可验证

本章 validator 会检查:

  • offering ID 唯一;
  • production offering 有非空 service_objectives
  • allowed bundle 全部存在;
  • blueprint 引用的 offering 全部存在;
  • bundle 与 lower-volume gate 引用闭合;
  • lab-only FDW 不得生产准入。

反例把 pg-ha-standard.service_objectives 置空时,必须得到:

E_SERVICE_OBJECTIVE

机器验证不能判断 99.9% 是否合理,但能阻止“生产服务连目标字段都没有”的 结构退化。


上一节:明确替代边界 · 返回本章目录 · 下一节:Pigsty 作为参考实现 · 查看全书目录 · 查看索引中心

18.5 Pigsty 作为参考实现

本书选择 Pigsty,不是为了把 PostgreSQL 原理隐藏在自动化后面,而是为了让 读者看到一套完整参考实现如何把原理变成可交付环境。

阅读方法始终是双向的:

平台职责 -> Pigsty 参数/组件/动作
Pigsty 现象 -> PostgreSQL/catalog/log/backup/route 证据

18.5.1 把 PostgreSQL、HA、备份、接入和观察组合起来

声明期望状态

Pigsty 4.5 的 Architecture 说明,它用 config inventory 与参数描述部署环境,再由 Ansible playbook 实现。

最小 cluster 声明大致包含:

pg-shop:
  hosts:
    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 }
    10.10.10.14: { pg_seq: 4, pg_role: offline }
  vars:
    pg_cluster: pg-shop
    pg_version: 18

声明有两个重要作用:

  • 让拓扑、身份、版本与参数进入版本化评审;
  • 让重复执行与漂移修复有一个共同期望。

它没有证明:

  • 四个地址真的跨故障域;
  • 存储与网络满足容量;
  • package repository 完整;
  • secret 已正确交付;
  • RPO/RTO 已演练;
  • 业务应用兼容。

inventory 是控制平面输入,不是验收报告。

模块与职责映射

Pigsty 官方架构列出多个模块。本书关注:

模块/组件 本书中的职责
NODE 主机基线、监控、日志、HAProxy 等节点能力
ETCD HA 的分布式配置与 leader 协调
PGSQL PostgreSQL、Patroni、PgBouncer、pgBackRest、exporter
INFRA 软件仓库、DNS/NTP、指标/日志/告警/可视化
MINIO(可选) S3-compatible 对象/备份仓库候选
REDIS(可选) 缓存候选,但 authority 仍由合同决定

模块安装并不改变业务边界。例如部署 REDIS 不会自动让它成为商品权威;部署 MINIO 也不会自动完成 media 两阶段状态机。

PostgreSQL 仍是数据面核心

自动化完成后,仍要从 PostgreSQL 验证:

SELECT current_setting('server_version');
SELECT pg_is_in_recovery();
TABLE pg_extension;
TABLE pg_roles;
SELECT * FROM pg_stat_replication;
SELECT * FROM pg_stat_wal_receiver;

以及:

database/schema/object owner
ACL
business checksum
extension versions
replication position
archive status
backup restore result

Pigsty 提供实现路径,不改变这些原生事实的语义。

Patroni 与 etcd 形成 HA 控制

Pigsty 的 High Availability 描述了参考链:

PostgreSQL physical streaming replication
  -> Patroni manages member role/process
  -> etcd provides DCS/leader election
  -> Patroni health API exposes current role
  -> HAProxy routes by health/role

理解边界:

  • PostgreSQL replication 决定数据复制位置;
  • Patroni 决定/执行 promotion 与成员管理;
  • etcd 参与 leader 共识,不存业务表;
  • HAProxy 决定新连接去哪,不复制数据;
  • client 必须处理切换期间连接/事务失败。

“自动切换”不是“请求无感成功”。切换中的连接会中断,未决事务需要应用依据 幂等身份判断和重试。

复制不是备份

HA 复制会忠实传播:

DROP TABLE
wrong UPDATE
application bug
malicious committed change
logical corruption

Pigsty 官方 HA 文档也明确区分流复制故障覆盖与人为/软件错误恢复,后者需要 延迟副本或 PITR。

因此平台组合同时需要:

HA: current service continuity
backup/WAL: historical recovery
PITR: select target time/LSN/transaction
forensics: preserve evidence before repair

第 20、21、32、35 章分别承担这些证据。

pgBackRest 形成恢复链,但恢复仍需演练

参考实现可生成 pgBackRest 配置、执行 base/differential/incremental backup、 归档 WAL 并管理 repository。

验收不能止于:

backup command exit 0

还要证明:

repository manifest and retention
WAL continuity
encryption/key recovery
independent failure domain
empty isolated target restore
extension packages
business checksum
RTO under representative size
operator runbook

第 21 章会在隔离目标恢复;第 32 章选择随机 recovery target。

HAProxy 把拓扑封装为服务

Pigsty 官方 Service/Access 说明 service 由访问端点与 selector 组成,并提供默认 primaryreplicadefaultoffline 等服务。

概念映射:

primary endpoint  -> current writable primary
replica endpoint  -> eligible read-only members with fallback policy
offline endpoint  -> offline/analytical candidates
default endpoint  -> default PostgreSQL/PgBouncer path

要进一步验收:

  • health check 与实际 role 是否一致;
  • failover 后多久摘除旧 primary;
  • fallback 是否会把只读流量压回 primary;
  • client DNS/VIP/port 怎样接入;
  • TLS 在哪终止;
  • health endpoint 是否越权;
  • 连接失败与重试风暴怎样受控。

PgBouncer 把连接变成有限资源池

池化可减少 backend 数、平滑短连接,但会引入语义:

session / transaction / statement pooling
prepared statements
temporary tables
session GUC
LISTEN/NOTIFY
advisory locks
server reset
cancel routing
authentication

应用是否兼容,取决于 pool mode 与使用的 session feature。第 22 章会用实际 请求验证,不能只看 PgBouncer 端口可连。

offline replica 实现分析隔离候选

Pigsty 的 Offline Instancepg_role: offline 用于慢查询、ETL、OLAP 与交互查询。

蓝图提出:

primary + 2 replicas + 1 offline

offline 服务合同:

  • read-only;
  • 不保证 read-your-writes;
  • 显示 replay lag;
  • 分析连接池独立;
  • 查询 timeout/temp 配额独立;
  • 不默认承接 online replica 流量;
  • 长查询与 WAL replay 冲突有明确处理。

节点存在不证明这些条件,仍需第 22、26、27 章。

观察栈连接组件信号

Pigsty 4.5 Monitoring 描述了 Grafana、VictoriaMetrics、VictoriaLogs 与 PostgreSQL/PgBouncer/ Patroni/HAProxy/Node 等 exporter/日志源。

平台至少需要关联:

client/service probe
HAProxy backend
PgBouncer queue/pool
PostgreSQL session/query/wait
Patroni role/timeline
replication lag
WAL/archive/backup
host CPU/memory/I/O/network
extension-specific state
business freshness

仪表盘只是表现层。alert owner、阈值依据、抑制、升级与 runbook 仍由团队 定义。

18.5.2 哪些能力开箱可用,哪些仍需组织流程

三层“可用”

讨论开箱能力时应分:

L0 mechanism
  配置/组件/命令存在,能在受控环境执行

L1 environment validation
  目标环境完成身份、版本、行为和复位/恢复证据

L2 service acceptance
  目标、容量、安全、值班、变更、演练和业务签字成立

本章:

direct PostgreSQL fixture L1 = passed for chapter-specific mechanisms
Pigsty mapping = documented
Pigsty L1 = not-run
production L2 = pending chapters 19-36

不要把不同对象的 L1 混在一起:本地 PostgreSQL 18.6 实验通过,不等于目标 Linux/Pigsty cluster 通过。

参考实现可直接提供的机制

按官方能力,Pigsty 可以自动化:

host desired state
software repository/package deployment
PostgreSQL instance/cluster creation
Patroni/etcd HA wiring
HAProxy service definitions
PgBouncer deployment/configuration
pgBackRest configuration and scheduled backup
monitoring/log collection/dashboard/alerts baseline
roles/databases/extensions declarations
offline replica role

“提供机制”的准确含义:

  • 有对应 module/parameter/playbook;
  • 可以在支持环境中声明和部署;
  • 有默认配置和可观察入口。

它不是针对 pg36_shop 的完成证明。

组织必须补齐的工作

工作 为什么不能由工具自动决定
数据 authority 业务语义与冲突裁决
SLO/error budget 业务损失、成本与风险取舍
RPO/RTO scope 故障模型与恢复价值
tenant trust 法规、组织与威胁模型
extension admission 功能收益、生命周期、支持
capacity headroom 真实 workload 与增长
on-call/RACI 人与组织责任
change approval 风险、窗口与可逆性
incident judgment 不完整证据下的取舍
postmortem actions 系统性改进优先级

自动化可以检查字段非空,不能替业务 owner 承诺。

默认值是起点,不是证据

生产环境常见错误:

default topology -> default failure guarantee
default HA timeout -> our RTO
default backup schedule -> our RPO
default dashboard -> complete observability
default password -> acceptable security
default pool size -> safe connection budget
default shared_buffers/work_mem -> tuned

正确流程:

document default
  -> explain why it may fit
  -> measure target
  -> accept/override
  -> validate
  -> monitor drift

secret 永远不属于示例 inventory

本章 pigsty-declaration.example.yml 只放不可用 sentinel:

password: "REPLACE_VIA_APPROVED_SECRET_SOURCE"

正式环境要决定:

secret authority
render/injection path
file permissions
rotation
revocation
backup/log redaction
break-glass
audit

示例中的明文不是“方便”,而是泄露路径。

配置成功后还要独立验收

部署命令成功后,验收从外到内:

inventory identity
host/OS/time/storage/network
package/version
PostgreSQL role and settings
replication/timeline
service selectors and endpoints
pool behavior
backup archive + isolated restore
monitoring and alert path
business golden
failure drill

每项证据要保存 target、时间、版本和执行者。截图可以辅助,不应是唯一机器证据。

环境与组织漂移

技术漂移:

manual ALTER SYSTEM
package patch mismatch
extension extversion mismatch
inventory not applied
certificate expiry
backup schedule disabled
alert rule changed

组织漂移:

owner 离职
on-call 无人
runbook 过期
SLO 与业务不匹配
例外无到期
恢复密钥不可得

平台治理必须同时检测两类。第二类不会出现在 pg_settings

何时可以说“生产就绪”

至少满足:

offering approved
target L1 evidence retained
business acceptance golden passes
RPO/RTO drills pass
security threat model and tests pass
capacity has headroom
SLI/SLO/error budget approved
alerts reach accountable responder
change/rollback/upgrade tested
incident and recovery runbooks exercised
open exceptions bounded and expiring

因此本章不会使用“部署完成,所以生产就绪”的句式。

18.5.3 不把参考实现冒充唯一架构

稳定的是合同,变化的是实现

应尽量稳定:

service endpoint semantics
data authority
consistency/freshness
identity/privilege boundary
RPO/RTO definition
backup/recovery evidence
extension lifecycle
observability requirements
exit path

可以替换:

automation engine
HA controller/DCS
proxy/pooler
backup tool/repository
metrics/log stack
cloud/on-prem substrate

替换实现时,合同成为验收基线。

不要绕过组件理解

Pigsty 把复杂组件组合起来,但值班者仍需知道故障落在哪一层:

症状 可能层
endpoint 不通 DNS/VIP/HAProxy/network
pool queue 高 PgBouncer/connection budget
primary 不明确 Patroni/etcd/network partition
replica lag PostgreSQL/WAL/I/O/long query
backup missing archive/pgBackRest/repository/secret
dashboard blank exporter/collection/storage/query
SQL wrong result schema/data/extension/business semantics

“重跑 playbook”不是通用诊断,更可能覆盖证据或扩大变更。

不要把云托管与自托管简化成好坏

托管服务可能减少:

hardware lifecycle
base engine patching
some HA/backup implementation
control-plane construction

但团队仍负责:

data model
SQL/application behavior
roles/security configuration
SLO and capacity/cost
restore acceptance
extension compatibility
migration/exit
incident collaboration

自托管给予更多控制和透明度,也带来更多直接责任。选择应基于组织能力、法规、 故障模型、成本与退出,而非身份认同。

保持原生证据层

无论实现是什么,都尽量保留:

SQL business golden
catalog inventory
configuration snapshot
backup manifest
restore checksum
service probe
fault timeline
versioned ADR

这些证据比某个 UI 路径更可迁移。

用接口封装平台差异

消费者看到:

service name
endpoint class
database
runtime identity
TLS/auth method
pool/session contract
SLO/freshness
quota
support/escalation

不应依赖:

当前 primary IP
Patroni member name
HAProxy 内部 selector
backup repository layout
Ansible role internals
monitoring storage schema

这样平台升级或替换时,业务应用变更最小。

参考实现也必须有退出路线

从 Pigsty 迁出并不是“一条 pg_dump”:

  1. 冻结 PostgreSQL/extension/locale/role 依赖;
  2. 选择 physical 或 logical 路径;
  3. 重建 service endpoint 与 pooling 语义;
  4. 重建 backup/PITR;
  5. 重建 monitoring/alerts;
  6. 验证 HA 与 failover;
  7. 验证业务 golden;
  8. 切换并保留回退;
  9. 退役旧控制面与 secret。

反向迁入同样需要这些合同。

一个合格的参考映射

本章示例 YAML 明确注释:

not Pigsty L1 validated
example IPs
no real secrets
backup policy pending
synchronous mode not selected
ports/selectors/pool pending chapter 22
extension lifecycle governed outside package list

有意保留 unknown,比填入未经证实的“最佳实践”更专业。

何时偏离 Pigsty 参考

可以偏离,只要有证据:

existing organizational platform already satisfies contracts
managed service is required
unsupported OS/network/security boundary
different HA/recovery model
specialized kernel/distribution
team skills and support model
regulatory requirement
cost/capacity evidence

偏离应写 ADR,包含等价职责、差异、风险与退出,而不是静默拼装。

本书为什么仍然以 Pigsty 实战

因为读者要从 SQL 走到生产,必须面对:

host
package
topology
service endpoint
pool
HA
backup
monitoring
change
incident

Pigsty 给出一个可以落地、查看、运行与破坏性演练的完整对象;PostgreSQL 原生 证据则防止读者只会操作一个封装。两条线并行,才能真正迁移知识。

这一章的最终边界

我们接受:

Pigsty 4.5 是 pg36_shop 下卷的参考实现。

我们尚未接受:

示例 inventory 已经可以部署生产,或提案中的 SLO 已经实现。

后一项只有在下卷 evidence 完成后才可能成立。


上一节:平台服务目录与多租户 · 返回本章目录 · 下一节:实战:设计 pg36_shop 生产蓝图 · 查看全书目录 · 查看索引中心

18.6 实战:设计 `pg36_shop` 生产蓝图

第 18 章实战与前几章不同:

no setup
no DDL
no DML
no reset
no service deployment

它把现有证据读出来,检查蓝图是否有资格进入下卷。风险等级是 L0。

18.6.1 选择保留在 PostgreSQL 内的能力

前置状态

实验要求第 4、13–17 章的最终 fixture 保留在同一本地开发实例:

database=pg36_shop
PostgreSQL major=18
session_user=postgres
pg36_owner=NOLOGIN non-superuser
pg36_app=LOGIN non-superuser
ch04-v1 physical model
ch13 routine guard
ch14 extension lifecycle
ch15 search quality
ch16 spatiotemporal
ch17 analytics/FDW + two shard database shells

缺失时本章直接失败,返回前章重建;它不会悄悄修复。

私有连接

沿用:

[pg36-admin]
host=/path/to/socket-or-host
port=5432
dbname=pg36_shop
user=postgres
chmod 600 /path/to/pg_service.conf
export PGSERVICEFILE=/path/to/pg_service.conf
export PGSERVICE=pg36-admin

密码或其他 secret 放在批准的连接机制中,不放命令行、脚本、evidence 或 Git。

先读实验合同

lab-contract.md 规定:

risk=L0 read-only
target=confirmed local fixture
allowed=catalog reads + JSON validation + evidence files
forbidden=DDL/DML/roles/extensions/deploy/failover/backup/reset
pigsty_l1=not-run

本章脚本没有 setup/reset action。这不是遗漏,而是用接口形状表达安全 边界。

上卷前置复核

task.sh 依次调用:

static/labs/ch04/task.sh verify
static/labs/ch13/task.sh verify
static/labs/ch14/task.sh verify
static/labs/ch15/task.sh verify
static/labs/ch16/task.sh verify
static/labs/ch17/task.sh verify

它们只验证 retained fixture。第 17 章还分别连接 pg36_shard_apg36_shard_b,防止协调端看似正常而远端状态已经漂移。

只读事务

每个 catalog capture 都显式开始:

BEGIN TRANSACTION
ISOLATION LEVEL REPEATABLE READ
READ ONLY;

再 include context.sql

context 验证:

current_database = pg36_shop
server_version_num in 18.x
session_user = postgres superuser
can inspect pg36_owner
owner role = NOLOGIN, non-superuser
app role = LOGIN, non-superuser
ch04 schema_version present

脚本结束 COMMIT,但 read-only 事务没有业务变更。

平台状态

platform-state.sql 输出稳定 key/value:

database=pg36_shop
server_major=18
server_version=18.6 (formal run)
session_user=postgres
in_recovery=false
model_version=ch04-v1
relation_checksum=f8a7bfae59c6d16cd323abecfefe1014
pigsty_reference=4.4
pigsty_l1=not-run
mutation=none

mutation=none 不是只靠自报:all 在前置复核后抓两轮,并对状态与 catalog 逐字节 cmp

能力快照

capability-snapshot.sql 把数据库事实压成九行:

capability lifecycle evidence
relational core accepted ch04-v1
atomic database logic accepted with scope ch13-routine-guard-v1
lexical/fuzzy search accepted pg_trgm:1.6
semantic search pilot vector:0.8.4
spatiotemporal conditional btree_gist:1.8,postgis:3.6.4
analytical federation lab-only postgres_fdw:1.2
search quality fixture accepted ch15-search-v1
spatiotemporal fixture accepted ch16-spatiotemporal-v1
analytics fixture accepted ch17-analytics-v1

这里有意把“扩展安装事实”和“生命周期判断”并列。SQL 能证明版本存在, 生命周期还来自前章的质量、安全与运维边界。

extension catalog

extension-catalog.sql 记录:

extension_name
extension_version
schema_name
owner_name
relocatable
comment

正式 fixture 精确包含六项:

btree_gist 1.8
pg_trgm 1.6
plpgsql 1.0
postgis 3.6.4
postgres_fdw 1.2
vector 0.8.4

教学扩展必须保留 pg36 chXX ... safe to rebuild marker。marker 只用于本书 精确识别,不应照搬成生产对象治理方案。

schema 与 role catalog

schema-catalog.sql 冻结:

shop / shop_private
shop_ch13 / shop_ch14 / shop_ch15
shop_ch16 / shop_ch16_ext
shop_ch17 / shop_ch17_ext

每项必须由 pg36_owner 拥有并保留精确 comment。

role-catalog.sql 只导出三种相关身份,避免把环境中其他角色误收入出版 fixture。审查器验证:

pg36_app   LOGIN, !SUPERUSER, !BYPASSRLS
pg36_owner NOLOGIN, !SUPERUSER
postgres   LOGIN, SUPERUSER (formal local admin)

为什么不探测 Pigsty

当前实例不是本章声明的目标 Pigsty cluster。若脚本从本机进程名或目录猜测 Pigsty 状态,会产生伪证据。

所以蓝图准确写:

Pigsty reference mapping = documented
Pigsty L1 = not-run

第 19 章在明确 target/inventory 后才执行环境验收。

18.6.2 选择外置组件及其数据契约

五份合同先于产品选型

external-data-contracts.json 包含:

product-cache-v1
order-events-v1
product-media-v1
analytics-export-v1
external-search-projection-v1

每份必须有 17 个核心字段,包括:

id / kind / status / owner / authority
source / sink / freshness / delivery / ordering
idempotency / rebuild / failure_mode / reconciliation
deletion / security / exit

这比简单画一条箭头严格得多。

cache 合同

关键规则:

business authority=PostgreSQL
cache identity=product_id + source version
TTL <= 300 seconds proposal
stale version never replaces newer
outage falls back to bounded PostgreSQL reads
namespace can be discarded and rebuilt

validator 专门扫描 cache authority。若把:

"product_business_state": "cache"

则报:

E_CACHE_AUTHORITY

order event 合同

business state + publication intent -> PostgreSQL transaction
delivery/replay log                 -> event bus
delivery                            -> at-least-once
ordering                            -> per order_id, no global order
idempotency                         -> stable event_id

publish lag 仍是 chapter 24 pending。写出 pending 比伪造一个 P99 更准确。

media 合同

bytes -> object storage
identity/owner/state/checksum -> PostgreSQL
immutable object version
visible only after checksum + metadata agree
orphan upload quarantined
two-phase deletion

它是 accepted-boundary,表示“字节外置”这一边界已选择,不表示具体 object provider 已选择或 L1 已通过。

analytics 合同

source=snapshot or CDC
sink=versioned immutable analytical tables
watermark on every dataset
last complete state
row/aggregate/partition checksum
tombstone propagation
new generation rebuild

这份合同会在第 29 章的数据迁移与 CDC 状态机中具体化。

external search 合同

状态:

deferred-until-trigger

进入条件不是“想用”,而是 PostgreSQL 搜索基线在质量、规模、语言或独立 SLO 上失败。

启用前必须证明:

snapshot + idempotent changes
monotonic product version/tombstone
index generation
quality golden
document count/payload hash
alias swap
fallback classification
exit to PostgreSQL

正向文档关系

baseline-v1.6-proposal.json 引用所有合同。每项 capability 又引用它需要的合同。

validator 检查:

blueprint contract set == declared contract set
capability references exist
external/pilot/conditional placement has trigger
every capability has owner and evidence

这能发现拼写、遗漏和结构漂移。

七个对抗性反例

negative-cases.json 不是伪造七份静态错误文件,而是对正确文档做 JSON path mutation:

反例 期望错误
无 evidence 宣称 Pigsty L1 passed E_L1_EVIDENCE
清空全部 exit path E_EXIT_PATH
cache 成为商品业务权威 E_CACHE_AUTHORITY
删除消息合同 rebuild E_CONTRACT_FIELD
loopback FDW 允许生产 E_FDW_LAB_ONLY
删除 production offering objective E_SERVICE_OBJECTIVE
删除 vector pilot gate E_EXTENSION_GATE

测试要求实际错误码与期望码精确相同。若错误文档意外通过,或被另一个更早的 无关规则拦截,negative suite 都失败。

为什么 validator 只用 Python 标准库

validate.py 只依赖:

argparse
copy
hashlib
json
pathlib

目的不是排斥 schema 工具,而是让读者在最小环境中运行并看到业务策略代码。 生产平台可以再加 JSON Schema、OPA、CI policy 或签名。

canonical hash

报告为四份核心文档计算 canonical JSON SHA-256:

sort object keys
compact separators
UTF-8
preserve array order

这避免 indentation/key order 影响内容身份,同时让 gate/capability 顺序仍然 有意义。

注意:canonical hash 证明文档未变,不证明内容正确;内容正确还靠人工决策、 数据库证据和负例。

18.6.3 输出服务目录草案、架构 ADR 与下卷验收问题

资产目录

static/labs/ch18/
├── lab-contract.md
├── architecture-adr.md
├── platform-map.mmd
├── pigsty-declaration.example.yml
├── service-catalog.json
├── external-data-contracts.json
├── baseline-v1.6-proposal.json
├── lower-volume-gates.json
├── negative-cases.json
├── context.sql
├── platform-state.sql
├── extension-catalog.sql
├── schema-catalog.sql
├── role-catalog.sql
├── capability-snapshot.sql
├── validate.py
├── review.py
└── task.sh

没有生成的 evidence 被提交到源码目录。

单独验证文档

python3 static/labs/ch18/validate.py \
  --blueprint static/labs/ch18/baseline-v1.6-proposal.json \
  --catalog static/labs/ch18/service-catalog.json \
  --contracts static/labs/ch18/external-data-contracts.json \
  --gates static/labs/ch18/lower-volume-gates.json

预期:

{
  "status": "ok",
  "counts": {
    "offerings": 4,
    "extension_bundles": 5,
    "contracts": 5,
    "capabilities": 9,
    "lower_volume_gates": 18,
    "exit_paths": 5
  }
}

加负例:

python3 static/labs/ch18/validate.py \
  --blueprint static/labs/ch18/baseline-v1.6-proposal.json \
  --catalog static/labs/ch18/service-catalog.json \
  --contracts static/labs/ch18/external-data-contracts.json \
  --gates static/labs/ch18/lower-volume-gates.json \
  --negative-cases static/labs/ch18/negative-cases.json

预期 case_count=7 且全部 actual/expected code 相等。

单轮 capture

evidence="$(mktemp -d /tmp/pg36-ch18.XXXXXX)"

PG36_EVIDENCE_DIR="$evidence" \
  static/labs/ch18/task.sh capture

输出:

status=capture-ok
mutation=none
evidence=/tmp/...

每个 cycle 的 review.py 验证:

  • manifest target/version/hash;
  • relation checksum;
  • extension/schema/role exact identity;
  • capability lifecycle;
  • normal/negative policy report;
  • stderr 为空。

两轮正式运行

evidence="$(mktemp -d /tmp/pg36-ch18-final.XXXXXX)"

PG36_EVIDENCE_DIR="$evidence" \
  static/labs/ch18/task.sh all

正式 PostgreSQL 18.6 开发 fixture 的结果:

status=ok
preflight=ch04+ch13+ch14+ch15+ch16+ch17
cycles=2-byte-identical
documents=catalog+contracts+blueprint+18-pending-gates
counterexamples=7-rejected
pigsty_l1=not-run
mutation=none
release_candidate_checksum=beec6b6d47075a7b3b4a6aa6ee3ca2902ef8d555547fe6c2b2b009e56c25c9eb

源码后续改变时 checksum 会改变;应以当次 manifest 与 validator report 为准。

两轮比较什么

platform-state.csv
extension-catalog.csv
schema-catalog.csv
role-catalog.csv
capability-snapshot.csv
validation-report.json
negative-report.json
review.txt

manifest.txt 含 capture 时间,故不做 byte compare;其余确定性证据必须一致。

运行后状态不需要复位

本章没有数据库写操作。若运行前后出现状态变化,应当视为:

  • 外部并发变更;
  • 某个前置 verify 实现违反只读预期;
  • capture SQL/任务脚本缺陷;
  • 环境不再适合作为冻结 fixture。

不要用 reset 掩盖,应先保留 evidence 并诊断。

架构 ADR

architecture-adr.md 记录:

context
decision per capability
external contracts
service offerings
Pigsty reference mapping
proposed topology
positive consequences
costs/risks
rejected alternatives
revision triggers

明确拒绝:

everything in PostgreSQL
everything split immediately
loopback FDW as production proof
Pigsty install as production readiness

ADR 的 status 是:

proposed; accepted for lower-volume validation,
not production approval

18 个下卷 gate

lower-volume-gates.json 严格映射第 19–36 章:

gate
19 deployment baseline
20 HA
21 backup/restore
22 access/routing
23 security
24 governance
25 observability
26 capacity
27 tuning
28 vacuum/maintenance
29 migration
30 upgrade
31 incident framework
32 PITR
33 failover/rebuild
34 overload
35 forensics
36 postmortem/platform improvement

每个 gate 有 owner、两个核心问题、required evidence 与 pending 状态。

gate 不是章节阅读打卡

chapter completed 不等于 gate passed。例如读完第 21 章但没有在目标环境 恢复,ch21-backup-restore 仍然 pending。

通过 gate 应产生:

target identity
version
procedure
raw evidence
review result
owner approval
limitations
expiry/review trigger

服务目录如何升级

下卷完成后,不直接覆盖 1.6-proposal。应:

  1. 收集每个 gate evidence;
  2. 修改不成立的 topology/objective/bundle;
  3. 记录 ADR revision;
  4. 生成新的 catalog/blueprint release;
  5. 重新跑正负 policy;
  6. 由 owner 批准;
  7. 保留 proposal 历史。

本章通过后能说什么

可以说:

在 PostgreSQL 18.6 的受控本地 fixture 上,第 4、13–17 章证据仍然成立; pg36_shop 的服务目录、能力决策、五份外置合同和 18 个下卷 gate 引用闭合, 七个危险反例被拒绝,两次只读快照一致。

不能说:

Pigsty 生产 cluster 已部署、SLO 已实现、备份可恢复、HA 可达目标、安全已 通过、容量足够。

这条语言边界,也是本章最后一项验收。

进入下卷

上卷回答:

PostgreSQL 如何正确建模、查询、扩展与交付应用能力

下卷开始回答:

这些能力如何在真实环境中持续、可恢复、可观察、可升级地成为服务

下一步是 第 19 章:环境规划与部署基线: 把 proposal 中的主机、软件、网络、存储、故障域和 Pigsty inventory 变成第一 份目标环境证据。


上一节:Pigsty 作为参考实现 · 返回本章目录 · 下一章:开天辟地:环境规划与部署基线 · 查看全书目录 · 查看索引中心