17.5 部署最小分布式 PoC
一个好的 PoC 不是“把所有组件装一遍”,而是用最小拓扑证伪一个关键假设。
本章假设是:
以
tenant_id为数据局部性边界时,协调端能裁剪到单分片;全局可分解聚合 能把部分计算推到数据侧;同时,未下推 JOIN、权限和单分片故障可以被明确 观察,而不是被 demo 隐藏。
为了让 SQL、catalog 和计划完全透明,本地 PoC 使用 postgres_fdw。它不是
对 Citus 性能或生产可用性的替代测试。
17.5.1 明确 PoC 只验证一个关键假设
拓扑
one PostgreSQL 18.4 instance
one Unix-domain socket
one process/storage/failure domain
pg36_shop coordinator database
shop_ch17 local facts + partitioned foreign parents
shop_ch17_ext postgres_fdw 1.2
pg36_ch17_shard_a foreign server
pg36_ch17_shard_b foreign server
pg36_shard_a retained database shell
shop_ch17_shard tenants 2,4,6,8
pg36_shard_b retained database shell
shop_ch17_shard tenants 1,3,5,7三个数据库共享一个实例。因此它能证明:
database boundary
foreign server/user mapping
partition routing/pruning
remote SQL
row transfer shape
remote failure SQLSTATE
cross-database reset orchestration不能证明:
network latency/bandwidth
independent CPU/storage
multi-node throughput
replication/HA
node placement
rolling upgrade
rebalance
distributed backupPoC 的验收问题
只回答十个问题:
- 三份数据库数据能否由同一确定生成器重建?
- 本地、summary、naive FDW、two-stage FDW 是否输出同一 frozen result?
- 单租户谓词是否裁剪到正确物理 shard?
- 租户和日期过滤是否出现在 Remote SQL?
- 朴素全局聚合向 coordinator 返回多少行?
- 远端预聚合后返回多少行?
- 同物理分片 JOIN 是否真的被下推?
- application role 是否只能读且使用具名 mapping?
- 一个 shard 不可达时,健康 shard 的 scoped read 与全局 read 分别怎样?
- 受管对象能否精确退出并完整重建?
没有延迟、QPS 或扩展倍数问题,因为这个拓扑没有资格回答。
冻结数据生成
协调端和远端分别使用:
共同公式:
tenant_id = 1..8
account_id = 1..50
day_offset = 0..119
sale_per_day = 1..5
sale_id = deterministic integer composition
channel = deterministic cycle
units/amount = deterministic expressions没有随机数、当前时间或外部数据。fixture_meta 固定:
fixture_version = ch17-analytics-v1
generator_identity = fixture-generator-v1
first_day = 2026-01-01
frozen_at = 2026-07-29T00:00:00Z两端 generator 的 SHA-256 写入
fixture-manifest.json。生成器变化意味着
fixture 身份变化,不能仍用旧 golden。
数据库壳与 schema 分开
bootstrap.sql 只创建并保留两个数据库壳:
CREATE DATABASE pg36_shard_a
WITH OWNER pg36_owner TEMPLATE template0 ENCODING 'UTF8';
CREATE DATABASE pg36_shard_b
WITH OWNER pg36_owner TEMPLATE template0 ENCODING 'UTF8';并固定 database comment:
pg36 ch17 fdw shard database a; retained shell
pg36 ch17 fdw shard database b; retained shell已有同名数据库只有在 owner、comment、template/connection 身份精确匹配时才
可复用;否则停止碰撞。reset 删除内部 shop_ch17_shard,不 drop database。
这样做的理由:
DROP DATABASE破坏性更大;- database DDL 不能在普通事务里执行;
- 固定壳能把重复实验聚焦于 schema/data;
- reset 输出明确说明
database_shell=retained。
远端分片
每个 shard:
CREATE SCHEMA shop_ch17_shard AUTHORIZATION pg36_owner;
CREATE TABLE shop_ch17_shard.account_dim (...);
CREATE TABLE shop_ch17_shard.sales_fact (...);分片 A 约束:
CHECK (mod(tenant_id, 2) = 0)分片 B:
CHECK (mod(tenant_id, 2) = 1)每个分片固定:
4 tenants
200 accounts
120,000 sales
2026-01-01 .. 2026-04-30远端校验和不同,因为 tenant set 不同:
shard A = 274002669404fbcd449bdecd929624e3
shard B = 0bb770361058ec76ebc81a2a7d1e2629协调端本地基线
协调端同时生成完整本地表:
CREATE TABLE shop_ch17.sales_fact (...)
WITH (parallel_workers = 2);
CREATE INDEX sales_fact_tenant_day_idx
ON shop_ch17.sales_fact (
tenant_id,
occurred_on,
account_id
)
INCLUDE (amount, units, channel);
CREATE INDEX sales_fact_day_brin_idx
ON shop_ch17.sales_fact
USING brin (occurred_on)
WITH (pages_per_range = 16);并创建日汇总 materialized view。于是同一个 PoC 内存在单机对照组,不会拿 分布式结果和一个不存在的 baseline 比较。
外表父表
协调端:
CREATE TABLE shop_ch17.sales_fact_distributed (...)
PARTITION BY LIST (tenant_id);
CREATE FOREIGN TABLE shop_ch17.sales_fact_dist_0
PARTITION OF shop_ch17.sales_fact_distributed
FOR VALUES IN (2, 4, 6, 8)
SERVER pg36_ch17_shard_a
OPTIONS (
schema_name 'shop_ch17_shard',
table_name 'sales_fact'
);
CREATE FOREIGN TABLE shop_ch17.sales_fact_dist_1
PARTITION OF shop_ch17.sales_fact_distributed
FOR VALUES IN (1, 3, 5, 7)
SERVER pg36_ch17_shard_b
OPTIONS (
schema_name 'shop_ch17_shard',
table_name 'sales_fact'
);账户维表使用完全相同的 LIST 边界。这让“物理共置但 JOIN 是否下推”成为可测 问题。
为什么不用 HASH 分区
早期 PoC 曾写:
PARTITION BY HASH (tenant_id)
FOR VALUES WITH (MODULUS 2, REMAINDER 0)而远端 generator 使用 mod(tenant_id, 2)。这造成路由算法不一致。冻结版本
改用 LIST,不是因为 LIST 普遍优于 HASH,而是为了让这个八租户教学 fixture
的物理映射无歧义。
生产 PoC 应使用目标系统真实的 shard function 和 metadata,并加入:
route(key) expected shard
physical rows comply
pruned query returns golden
rebalance changes epoch atomically
old router cannot write after cutoverPoC 应主动寻找反例
本章没有把目的写成“证明 FDW 很快”,而是:
prove pushdown where it happens
prove non-pushdown where it does not这比只展示成功计划更能检验选型假设。若一个 PoC 从不失败,通常说明验收条件 太宽或只选择了产品最擅长的路径。
17.5.2 记录组件、版本、拓扑和数据分布
版本 manifest
正式证据写入:
validation_path=direct-postgresql-loopback-fdw
server_version=18.4 ...
postgres_fdw=1.2
database=pg36_shop
shard_databases=pg36_shard_a,pg36_shard_b
distribution=explicit-list-by-tenant
pigsty_reference=4.4
pigsty_l1=not-run此外为实验目录中每个 source file 计算 SHA-256。
为什么正式 fixture 限制 PostgreSQL 18.x:
postgres_fdw行为和功能随版本变化;- 本章固定 extension 版本 1.2;
- SCRAM passthrough、connection inspection 等版本功能不能模糊外推;
- 计划文本是 18.4 的证据。
概念适用于更多版本,但复制计划前应在目标 major 重新采集。
foreign server
setup.sql 动态读取本地实例:
unix_socket_directories
port再创建:
CREATE SERVER pg36_ch17_shard_a
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (
host '<lab socket>',
port '<lab port>',
dbname 'pg36_shard_a',
fetch_size '10000'
);fetch_size=10000 是 fixture 基线,不是生产最优值。官方 postgres_fdw
文档说明它控制每次 fetch 取得的行数,server 级设置可被 table 级覆盖。真实
网络要在延迟、内存和结果宽度下测量。
身份映射
每个 server 有三个具名 mapping:
local postgres -> remote postgres
local pg36_owner -> remote postgres
local pg36_app -> remote pg36_app两 server 共六个,没有 PUBLIC mapping。
本地隔离实验使用:
OPTIONS (
user 'pg36_app',
password_required 'false'
)安全边界
password_required=false只能由 superuser 设置,会允许映射用户利用 PostgreSQL 操作系统账户可获得的认证材料或 trust/peer 关系。官方文档明确 警告不要对 PUBLIC 设置,并要求防止映射用户借机连接成远端 superuser。 本章只在同一实例、Unix-domain socket、受控开发数据库中使用。生产不得 照抄。
PostgreSQL 18 还提供 use_scram_passthrough 选项,但它有严格条件:远端必须
请求 SCRAM,相关节点需有相同 SCRAM secret,传入会话也必须以 SCRAM 认证。
生产应在目标版本中比较:
SCRAM credentials in reviewed secret lifecycle
SCRAM pass-through
GSS delegated credentials
approved certificate/service mechanism身份与要求见官方
postgres_fdw Connection Options。
应用权限
pg36_app 只获得:
USAGE on shop_ch17
SELECT on local, summary, distributed relations/views
USAGE on two foreign servers
remote SELECT on account/sales不获得 insert/update/delete。负例:
INSERT INTO shop_ch17.sales_fact (...)
VALUES (...);固定失败:
SQLSTATE 42501成功读:
SELECT
count(*) AS sale_count,
sum(amount)::numeric(18,2) AS amount_total
FROM shop_ch17.sales_fact_distributed
WHERE tenant_id = 3
AND occurred_on >= DATE '2026-04-01';输出:
7500,69375.00catalog 证据
自动采集:
2 foreign servers
6 named user mappings
18 coordinator relations
6 local indexes
7 application privilege facts
6 size facts关键对象清单:
4 foreign partitions
2 partitioned parents
3 local tables
1 materialized view
2 views
6 indexesextension schema shop_ch17_ext 不含关系,只有 postgres_fdw 的五个 extension
member routines;所有非 extension object 都禁止混入。
Pigsty 路径 A:offline analytics
Pigsty 是 configuration-driven 平台。当前 4.4 文档把集群定义放在:
all.children.<cluster>.hosts并以 pg_cluster、pg_role、pg_seq 等 identity 参数定义实例。一个分析
隔离草图:
all:
children:
pg-analytics:
hosts:
10.10.10.11:
pg_seq: 1
pg_role: primary
10.10.10.12:
pg_seq: 2
pg_role: replica
pg_offline_query: true
vars:
pg_cluster: pg-analytics
pg_conf: olap.yml这条路径的假设是:
analysis can tolerate replica lag and read-only semantics
single-node read capacity is sufficient
resource isolation solves primary interference验收仍需:
- replica lag 与 freshness;
- long query/recovery conflict;
- offline 服务路由;
- HBA 与只读 role;
- failover 后标签/服务行为;
- CPU/I/O 隔离;
- backup 与升级。
Pigsty
Configuration
给出同类 pg-analytics 示例;其
Cluster / Instance
说明 offline instance 与 pg_offline_query 的职责。
Pigsty 路径 B:Citus 评估拓扑
当分片门槛满足后,Pigsty 4.4 可声明 Citus。当前文档要求:
pg_mode: citus
pg_shard: shared horizontal shard name
pg_group: shard cluster number
pg_primary_db: managed Citus database
extra HBA for local/data-node access简化草图:
all:
children:
pg-citus0:
hosts:
10.10.20.10: { pg_seq: 1, pg_role: primary }
vars:
pg_cluster: pg-citus0
pg_mode: citus
pg_shard: pg-citus
pg_group: 0
pg-citus1:
hosts:
10.10.20.11: { pg_seq: 1, pg_role: primary }
vars:
pg_cluster: pg-citus1
pg_mode: citus
pg_shard: pg-citus
pg_group: 1完整 inventory 还要有 pg_primary_db、database/extension、HBA、凭据以及
全局变量。本书资产为了避免硬编码生产秘密,只保留拓扑骨架。
L1 验收至少覆盖:
package/version on every node
pg_mode/shard/group identity
coordinator/worker metadata
distributed/reference/local tables
single-tenant route and colocated joins
cross-tenant aggregate
coordinator and worker HA
backup/restore
rebalance
rolling/major upgrade
monitoring and alert
security and service routing本地输出写 pigsty_l1=not-run,所以不能把上述 YAML 称为已部署。
声明不等于状态
Pigsty inventory 是 desired state 的重要来源,但发布证据还要从运行态回读:
inventory commit
rendered config
installed package
pg_extension
pg_settings
Patroni membership
service endpoints
HBA effective rules
shard metadata
backup status
monitoring targets否则可能出现“YAML 正确,节点尚未收敛”。
17.5.3 不把演示集群的绝对性能外推到生产
loopback 消除了最关键的变量
本章三个数据库共享:
CPU scheduler
shared_buffers
OS page cache
storage
filesystem
socket transport
PostgreSQL installation
host failure真实多节点新增:
network RTT/throughput/loss
TLS/authentication
independent caches
clock behavior
node skew
replication
DNS/service discovery
firewall
host maintenance因此不能发布:
two-stage is N times faster
two shards scale linearly
FDW overhead is X ms
Citus will behave like this本章只发布:
naive plan returns 240000 foreign rows
two-stage plan returns 960 foreign aggregate rowsEXPLAIN rows 也有边界
actual rows 表示某计划节点每 loop 的输出数量。跨网络字节还取决于:
- row width;
- text/binary representation;
- protocol framing;
- compression;
- TLS;
- fetch batches;
- remote output expressions;
- retries。
要测网络必须采集 network bytes/packets 与 server/client metrics,不能把 rows 直接乘一个猜测宽度。
人为 planner 设置
教学计划可能使用:
SET max_parallel_workers_per_gather = 2;
SET min_parallel_table_scan_size = 0;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
SET enable_seqscan = off;它们分别用于稳定复现某种路径。强制路径证明“可执行”,不证明 planner 在 真实成本下应选择它,更不证明它在生产更快。
每份强制计划旁都应写:
why forced
what property is proven
what performance claim is not made小数据掩盖协调成本
24 万行对现代机器很小。它可能:
- 全在内存;
- 规划/连接开销占比过高;
- 看不出网络拥塞;
- 看不出 shard skew;
- 看不出 vacuum 和 checkpoint;
- 看不出 rebalance;
- 看不出 compaction/backup;
- 无法设置生产 P99。
正式 PoC 要按 S/M/L/XL 数据阶梯,直到越过 memory 工作集和目标维护窗口。
同机故障探针的含义
把 foreign server port 改为 1,能证明:
partition pruning avoids unopened bad server
global fan-out surfaces connection failure
SQLSTATE is captured
transaction rollback restores catalog它不能证明:
- 半开 TCP;
- DNS 慢失败;
- packet loss;
- TLS rotation;
- remote process crash;
- node failover;
- long transaction during disconnect;
- unknown distributed commit。
生产 failure matrix 要在独立节点执行这些场景。
对比 Pigsty L1 的证据层级
建议区分:
L0 design review
schema, query, topology, safety, ADR
L1 target environment
actual packages, nodes, config, connectivity, backup
L2 functional workload
golden results, plans, permissions, failures
L3 representative performance
scale, concurrency, cold/warm, resources, cost
L4 operational drills
restore, failover, rebalance, upgrade, exit本章 loopback 具有 L0 和部分 L2 证据;Pigsty L1 明确未运行,更没有 L3/L4。
生产 benchmark 的最小补充
[ ] independent hosts/failure domains
[ ] target network/TLS/auth
[ ] target PG/Pigsty/extension versions
[ ] representative data width and skew
[ ] cold/warm/restart runs
[ ] open-loop arrival and bounded concurrency
[ ] OLTP + OLAP mixed workload
[ ] WAL/checkpoint/vacuum/backup overlap
[ ] per-shard and coordinator metrics
[ ] network rows and bytes
[ ] worker/coordinator failure
[ ] backup/restore checksum
[ ] rebalance interruption and resume
[ ] upgrade and rollback
[ ] cost and on-call effortPoC 的停止规则
如果发生以下任一情况,应停止扩展 demo 并回到设计:
- golden 不一致;
- route 与 physical placement 不一致;
- 必需查询无法局部化;
- 跨分片事务比例不可接受;
- mandatory extension 不兼容;
- backup/restore 无法证明;
- 身份需要不安全捷径;
- 失败返回 partial data 却无标识;
- 生产成本/owner 不明确;
- 没有退出路线。
本节结论
最小 PoC 的价值不是“跑起来”,而是把关键假设变成:
frozen input
exact topology
observable plan
expected success
expected failure
explicit limitation
repeatable teardown/rebuild下一节把这些资产串成一次完整执行,并从证据直接生成 ADR,而不是先写结论再 挑选支持它的截图。
上一节:比较分布式候选 · 返回本章目录 · 下一节:实战:从单机证据到选型 ADR · 查看全书目录 · 查看索引中心