跳至内容

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 backup

PoC 的验收问题

只回答十个问题:

  1. 三份数据库数据能否由同一确定生成器重建?
  2. 本地、summary、naive FDW、two-stage FDW 是否输出同一 frozen result?
  3. 单租户谓词是否裁剪到正确物理 shard?
  4. 租户和日期过滤是否出现在 Remote SQL?
  5. 朴素全局聚合向 coordinator 返回多少行?
  6. 远端预聚合后返回多少行?
  7. 同物理分片 JOIN 是否真的被下推?
  8. application role 是否只能读且使用具名 mapping?
  9. 一个 shard 不可达时,健康 shard 的 scoped read 与全局 read 分别怎样?
  10. 受管对象能否精确退出并完整重建?

没有延迟、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.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.00

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_clusterpg_rolepg_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 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 · 查看全书目录 · 查看索引中心

最后更新于