跳至内容

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
orderingsource version 单调,旧版本不覆盖新版本
idempotencyproduct_id + source version
rebuild丢弃 namespace,从 PostgreSQL 重建
failuremiss/outage 回退到有界 PostgreSQL 读
reconciliation抽样 version + payload checksum
deletiontombstone 或 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 downsource 是否可提交是否 fallbackbacklog 边界lag
network partition双方如何判定是否可能分歧幂等 identityreconciliation
projection corruptsource 不受影响停止/旧 generationrebuildchecksum
credential expiredsource 不受影响外置路径失败更新 secretauth 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 的强项与代价 · 返回本章目录 · 下一节:平台服务目录与多租户 · 查看全书目录 · 查看索引中心

最后更新于