17.5 部署最小分布式 PoC
一个好的 PoC 不是“把所有组件装一遍”,而是用最小拓扑证伪一个关键假设。
本章假设是:
以 tenant_id 为数据局部性边界时,协调端能裁剪到单分片;全局可分解聚合
能把部分计算推到数据侧;同时,未下推 JOIN、权限和单分片故障可以被明确
观察,而不是被 demo 隐藏。
为了让 SQL、catalog 和计划完全透明,本地 PoC 使用 postgres_fdw。它不是
对 Citus 性能或生产可用性的替代测试。
17.5.1 明确 PoC 只验证一个关键假设
拓扑
one PostgreSQL 18.6 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 backup
PoC 的验收问题
只回答十个问题:
三份数据库数据能否由同一确定生成器重建?
本地、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 cutover
PoC 应主动寻找反例
本章没有把目的写成“证明 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.6 ...
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.6 的证据。
概念适用于更多版本,但复制计划前应在目标 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 (...);
固定失败:
成功读:
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' ;
输出:
catalog 证据
自动采集:
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 indexes
extension 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.5 可声明 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 rows
EXPLAIN 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 effort
PoC 的停止规则
如果发生以下任一情况,应停止扩展 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 ·
查看全书目录 · 查看索引中心