做数据库的人,早晚都会撞上"单库扛不住"的墙。一张订单表几亿行,查询从几十毫秒变成几秒,IO 持续报警,磁盘眼看要满——这时候大家想到的第一个方案就是分库分表。我这次的实践背景,就是把一套基于 PostgreSQL 12 的业务库按订单维度拆成多个分片,选择的是官方扩展 pg_fdw 这种方式:用本地分区表做路由壳,把每个分片库的外部表挂进来,让应用层感觉还在操作一张大表。这篇文章就完整复盘一下这个方案的选型思路、落地步骤、关键代码,以及我在实操里踩过的那些坑。
如果你正在评估"PostgreSQL 分库分表怎么搞",或者已经在看 pg_fdw 但不确定能不能撑住自己的业务场景,那这篇文章应该能帮上忙。内容不烧脑,但细节足够多,建议搭配文档一起看。
1. 为什么想到用 pg_fdw:先想清楚数据库瓶颈的解法
1.1 单机数据库到底卡在哪
先说个常识:PostgreSQL 的本地分区表(PARTITION BY)只能解决"数据在逻辑上被拆开"的问题,它不能解决物理资源问题。哪怕你建了 100 个分区,这些分区还是落在同一个数据库实例、同一块磁盘上。查询被裁剪到单个分区确实能变快,但并发上来了,IO、CPU、连接数、锁竞争全都挤在一个实例里,该崩还是会崩。
分库分表的本质,是把数据物理上分散到多个独立数据库实例,让每个实例只扛住一部分流量和存储。但这里又冒出一个新问题:业务代码怎么知道数据在哪?如果应用层自己按订单号取模访问不同库,代码侵入性会非常强,而且以后加分片还得改代码。所以我需要一层"路由逻辑",让后端访问方式看起来还像以前一样。
1.2 三种主流分库分表姿势的取舍
我归纳了常见的三种路子:
第一,应用层分片。最简单,业务代码里写死路由规则,要访问哪个库直接连哪个库。问题也最明显:SQL 里到处都是分片逻辑,联表查询、分页、跨库事务都很难做,项目后期维护成本极高。
第二种是引入独立的数据库中间件,让代理帮你路由和聚合。这种方案确实省心,比如很多互联网公司用过的中间件方案,功能强、对应用透明。但它要求你额外部署和维护一套高可用集群,中间件本身是个状态节点,一旦挂了全链路都挂。而且很多中间件对 PostgreSQL 的支持成熟度、协议兼容性都得测试,踩坑成本不低。
第三种就是本文主角:PostgreSQL 官方自带的 postgres_fdw 扩展。它把远程数据库的表包装成本地外部表,再通过 ALTER TABLE ... ATTACH PARTITION 把外部表挂到本地分区表上。应用只管访问主库这张分区表,实际数据被路由到不同的 Postgres 实例。这套方案的优势是无额外中间件依赖、SQL 高度透明、单组件故障面更少;代价则是跨库事务能力和查询性能上限不如中间件方案。
1.3 为什么 PG12 这个版本特别适合干这件事
PostgreSQL 12 有一个非常关键的特性:它允许把 foreign table(外部表)作为分区挂到本地分区表上。也就是说,分区表的子分区既可以是普通本地表,也可以是外部表。这个能力在我实际做分库分表时等于官方给了一条"分片透明"的路:
- 数据写入:INSERT 到主分区表,PG 底层按分区键自动路由到对应外部表,sql 是透明转发的;
- 数据查询:走分区裁剪,只访问需要的外部表;
- 数据修改:UPDATE / DELETE 在 PG10 之后支持下推给远程执行,PG12 上下推能力更稳。
这不是说 PG12 没有短板——异步并行扫描、聚合下推这些功能在 PG12 里都还欠缺,后面我会专门讲。但就分库分表这个目标而言,PG12 确实提供了最直接的原生路径。
2. 整体方案设计:分区表如何挂外部表
2.1 架构长什么样
先画个逻辑框架,你脑海里要有这张图:
主库是一个 PostgreSQL 12 实例,上面不存业务数据,只存三样东西:分库分表后的"根分区表"、指向各分片库的外部表,以及 FDW 配置相关的元数据。业务连接串只指到这个主库。
底下一层是多个分片库,每个分片库是一个独立的 PostgreSQL 实例。它们各有自己的存储、IO、CPU,真实数据就落在这些库的物理表里。分片库之间互相不感知,主库通过 postgres_fdw 建立的外部连接和它们通信。
外部表在主库侧可以看作一个"壳",壳背后其实就是分片库里的某一张真实表。挂到分区表下之后,应用写主分区表,数据会被路由到某个分片库的壳,再由 FDW 写到远端真实表。
2.2 分片键怎么选:范围、哈希还是列表
分片键是整个方案里最不能拍脑袋决定的东西。我用的场景是按订单 ID 拆分,所以优先考虑两种分片方式:
- RANGE 范围分片:按 order_id 的数值区间切,比如 0~1000 万一个分片,1000 万~2000 万一个分片。优点是对范围查询和冷热归档友好;缺点是如果数据分布不均,会出现"热点片"和"冷片",单分片还可能继续膨胀。
- HASH 哈希分片:按订单 ID 的 MODULUS(模数)和 REMAINDER(余数)切割,比如分 4 片,键值对 4 取模。优点是数据分布比较均匀,缺点是范围查询要扫所有分片,扩容时要把大部分数据重分布。
我最终选了 RANGE,原因是订单明细查询往往带有明确的 ID 或时间范围,RANGE 可以让查询裁剪到少数几个分片。你如果做的是用户维度的系统(用户ID 哈希比较均匀),选 HASH 更合适。列表分片我个人用得少,适合像"按省份、按业务线"这种天然分割的场景。
还有一个关键点:分片键必须是一条稳定、极少更新的字段。以订单表为例,order_id 创建后不会再改。如果选一个会被业务更新的字段做分片键,后面会遇到"更新分区键导致数据跨分区移动"的麻烦,FDW 外部分区在 PG12 上不支持 row movement,一改就报错。
2.3 一个必须建立的认知:根分区表只是个路由器
分区表挂外部表之后,主库上的这张表在物理上是"空"的。所有行都散落在远端分片库。这个认知会影响你后面很多决策:
- 你在主库 VACUUM 这张分区表,起不到清理数据的作用,远端表要各自 VACUUM;
- 你把主库表备份下来,只是备份了一个空壳和配置,分片库的数据一个都没备份进来;
- 你在主库上 ANALYZE 外部表,统计信息来自远端采样,而真正的数据变更、膨胀管理都在远端。
所以日常运维必须把"分片库是真实钱包"这句话刻在脑子里。所有数据备份、巡检、容量规划的重心都在分片库侧。
3. 实操第一步:环境准备与 pg_fdw 初始配置
3.1 版本和扩展安装
我用的是 PostgreSQL 12 稳定版。postgres_fdw 是官方 contrib 包里自带的扩展,不需要额外下载源码包。Debian/Ubuntu 这类环境,需要单独装 postgresql-contrib;如果用官方编译安装,contrib 默认会被编译出来。
确认环境后,在主库执行:
CREATE EXTENSION postgres_fdw;这条命令执行成功后,pg_foreign_data_wrapper 系统表里会多出一条记录。顺便说一句:分片库不需要安装这个扩展,它们只是被动接收 FDW 发出的连接和数据请求。
3.2 创建外部服务器和用户映射
外部服务器(foreign server)描述远端连接信息。我要连 4 个分片库,就建 4 个 server:
CREATE SERVER fdw_shard1 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '192.168.10.21', port '5432', dbname 'order_shard1'); CREATE SERVER fdw_shard2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '192.168.10.22', port '5432', dbname 'order_shard2');然后是用户映射。FDW 连远端库时用什么身份,由 user mapping 决定:
CREATE USER MAPPING FOR app_user SERVER fdw_shard1 OPTIONS (user 'fdw_app', password '这里填远端库密码');注意:USER MAPPING 是按"主库本地用户"区分的。如果多个本地用户都需要访问外部表,得分别配。为了安全,建议在远端建一个最小权限账号,只授予业务库表的 SELECT、INSERT、UPDATE、DELETE 权限,不要给 DDL 权限。远端账号权限过大,FDW 被注入的风险会放大。
3.3 手动建外部表还是批量导入
外部表本质是"远端表结构在主库的映射"。你可以手动 CREATE FOREIGN TABLE,也可以用 IMPORT FOREIGN SCHEMA 一次性导入。
手动建一个分片表是这样:
CREATE FOREIGN TABLE orders_shard1 ( order_id bigint NOT NULL, customer_id bigint NOT NULL, amount numeric(10,2) NOT NULL, created_at timestamp NOT NULL ) SERVER fdw_shard1 OPTIONS (schema_name 'public', table_name 'orders');需要强调的是:外部表不继承远端表结构,列名、类型、非空约束都要手动对齐。远端表以后加了字段,主库的外部表不会自动感知,需要同步修改。
如果两个库的表结构完全一致,我更推荐 IMPORT FOREIGN SCHEMA:
IMPORT FOREIGN SCHEMA public LIMIT TO (orders) FROM SERVER fdw_shard1 INTO public;这条命令会把远端 public schema 里的 orders 表结构自动映射成外部表。加 LIMIT TO 是避免把分片库所有表都导进来。多个分片表可以循环执行,第一次建好,后续再调整。
4. 实操第二步:把分库分表真正立起来
4.1 在分片库上建好业务表
所有分片库上的物理表结构要完全一致。我用 RANGE 分片,规划得非常简单:订单 ID 在 0 到 1 亿落在分片1,1 亿到 2 亿落在分片2,依此类推。分片库里的表就是一张普通表,但在它上面必须建好索引,这一点极其重要——因为外部表在主库侧无法建索引,查询能不能走索引完全看远端表索引:
CREATE TABLE orders ( order_id bigint NOT NULL, customer_id bigint NOT NULL, amount numeric(10,2) NOT NULL, created_at timestamp NOT NULL, PRIMARY KEY (order_id) ); CREATE INDEX idx_orders_customer_id ON orders (customer_id); CREATE INDEX idx_orders_created_at ON orders (created_at);这里有个坑要提前说:如果分区表主键只包含 order_id,那业务按 customer_id 查主表时,会广播到全部分片,每个分片用自己的 idx_orders_customer_id 快速返回,主库再做汇总。这个设计是合理的,前提是分片不太多,并且每个分片索引都在。
4.2 在主库创建分区表并挂外部表
主库上的根分区表先建好:
CREATE TABLE orders ( order_id bigint NOT NULL, customer_id bigint NOT NULL, amount numeric(10,2) NOT NULL, created_at timestamp NOT NULL ) PARTITION BY RANGE (order_id);然后创建外部表并 attach 上去:
CREATE FOREIGN TABLE orders_p1 ( order_id bigint NOT NULL, customer_id bigint NOT NULL, amount numeric(10,2) NOT NULL, created_at timestamp NOT NULL ) SERVER fdw_shard1 OPTIONS (schema_name 'public', table_name 'orders'); ALTER TABLE orders ATTACH PARTITION orders_p1 FOR VALUES FROM (0) TO (100000000);这里两个细节要注意。
第一,外部表的列结构、非空约束必须和根分区表对齐,否则 ATTACH 会报错。第二,ATTACH 是验证远端数据是否在分区边界内的过程。如果远端表里已经有超范围数据,ATTACH 会失败。所以稳妥的顺序是先建外部表,确认数据干净,再 ATTACH。如果根分区表已经挂了好几个分区,新增分片流程是一样的,不同点就是这条 ALTER 语句执行前,新分片库里的数据要是"空的"或符合边界。
分片库的表名可以跟主库一致,也可以不同,外部表的 OPTIONS(table_name '...') 会告诉它真实远端表名。
4.3 数据路由验证:读写到底怎么走
挂完之后先跑几个验证,确保路由逻辑符合预期。
第一个是 INSERT:
INSERT INTO orders VALUES (500, 1001, 99.00, now()), (150000000, 1002, 698.00, now());这句 SQL 里,第一条记录 order_id=500,会进入 orders_p1;第二条是 1.5 亿,会进第二个分片。你可以直接查外部表确认:
SELECT count(*) FROM orders_p1 WHERE order_id = 500;第二个是查询计划验证,用 EXPLAIN 看路由和下推:
EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM orders WHERE order_id = 500;结果里应该能看到类似:
Append Subplans -> Foreign Scan on orders_p1 Remote SQL: SELECT order_id, customer_id, amount, created_at FROM public.orders WHERE order_id = 500看到 Remote SQL 里带着 WHERE 条件,说明查询被成功下推到远端,而不是把整个分片表拉回主库再过滤。这一步是性能的生命线。
第三个是 UPDATE 下推:
EXPLAIN (VERBOSE, COSTS OFF) UPDATE orders SET amount = amount + 1 WHERE order_id = 500;正常情况下计划里同样会出现 Foreign Update 和 Remote SQL。FDW 会把 UPDATE 语句直接发到对应分片执行,不会把行拉回来改。
4.4 一个容易忽略的问题:更新分区键要杜绝
分区键本身是分片路由依据,运行中修改分区键会导致行跨分区移动。PG12 的分区表对本地分区是支持 row movement 的,但外部表作为分区时,不支持把一行从一个外部分区移动到另一个外部分区。你如果执行:
UPDATE orders SET order_id = 200000000 WHERE order_id = 500;大概率会直接报错,或者返回 affected rows 为 0 但数据已经留在原地,这类行为会让你非常困惑。我的规则很简单:分片键一旦写入,任何时候不要 UPDATE 它。这是在设计表结构阶段就要跟业务方约定的硬约束,不是运行期能补救的。
5. 数据迁移与扩容:从单库切到分片集群
5.1 存量数据如何切分
分库分表最难的不是搭框架,而是把已经存在的几亿行数据安全地搬进分片。我当时的做法分三步。
先把分片库的表和主库的根分区表、外部表全部建好,但此时外部表分区可以先 DETACH 或者不 attach,保证分片库写入不受分区边界限制(其实 attach 也行,只要数据符合边界)。
然后写脚本按分片键拆数据。对 RANGE 分片,最简单的就是按边界范围分批导:
pg_dump --data-only --column-inserts --table=orders \ --host=192.168.10.20 --port=5432 --dbname=old_db > orders_full.sql然后把这个文件按 order_id 边界拆成四段,分别导入四个分片库。拆脚本可以用 Python 或 awk,核心是过滤条件:
-- 分片1 INSERT INTO orders SELECT ... FROM legacy_orders WHERE order_id >= 0 AND order_id < 100000000;实际我更推荐在旧库上用 COPY 配合查询条件导出,比 --column-inserts 快很多。例如:
psql -c "COPY (SELECT * FROM orders WHERE order_id >= 0 AND order_id < 100000000) TO STDOUT" | \ psql -h shard1 -c "COPY orders FROM STDIN"这个方式速度非常可观,而且不会因单条插入失败导致整批回滚。
最后,数据导入完成后,再回到主库执行 ATTACH PARTITION 把这些外部表挂上去。挂之前务必抽查每个分片的边界数据,确认没有错桶。
5.2 增量同步怎么做
存量迁移期间,线上依然有新写入,所以必须设计增量同步窗口。我在实践里用过两个方案。
方案A:短停写。适合可以接受十分钟停服的场景。先停应用写入口,记录停服时间,存量数据迁移完毕,验证各分片数据量一致,再把应用切到新的分库分表架构,恢复写入。这个方案最简单可靠,我最终就是它。
方案B:双写回填。应用同时写旧单库和新分片库,写旧库失败就回滚,写新分片实时或异步追赶。这个方案适合不能停服的场景,但复杂度高,且双写期间应用层需要同时维护两套写入逻辑,容易产生一致性问题,用之前要掂量清楚。
值得说的是,PostgreSQL 的逻辑复制也可以作为增量同步手段:旧库作为发布端,各分片库作为订阅端,按分片键过滤。但这条路径要保证每个分片库只订阅它那一段数据,配置复杂度偏高,而且 PostgreSQL 12 的逻辑复制限制不少(主键必须存在、不支持分区表直接发布等)。我踩过一次坑后发现,除非数据量实在大、停服时间完全不可接受,否则短停写是性价比最高的。
5.3 扩容:RANGE 分片怎么加新分片
扩容永远都是分库分表最不想面对但又必须面对的事。我用 RANGE 分片,遇到单分片数据接近容量阈值,做法是这样的:
- 在分片库一侧新增一个分片实例,建好相同结构的表;
- 在主库建好指向新实例的外部表;
- 从原分片库把超边界的部分数据迁移到新分片库;
- 在主库执行 ALTER TABLE orders DETACH PARTITION 把老外部表暂时摘下来;
- 用新的边界重新 ATTACH 老分区和新分区。
这里要提醒:PG12 本身没有"拆分分区"的现成命令,只能人工搬数据。假设原来是 0~2 亿一个分区,现在要拆成 0~1 亿和 1~2 亿两个分区,你必须先把 1 亿以上的数据迁到新分片,再重新 ATTACH,中间会有很短暂的不可用窗口。而且 DETACH 期间如果正好有写入,要确保流量先停掉或写到别的地方。
哈希分片扩容更麻烦,因为模数变化意味着几乎所有数据都要重新分布。所以选择 RANGE 还是 HASH,一定要把未来三年数据量增长和扩容频率提前估算进去。
6. 高频问题与排查技巧实录
6.1 两阶段事务报错:could not commit without prepared transactions
这是 pg_fdw 做分库分表绕不开的经典报错。当一个事务同时修改了多个外部表分区时,postgres_fdw 为了保证原子性,会启用两阶段提交(two-phase commit),这要求主库 postgresql.conf 里的 max_prepared_transactions 必须大于 0。默认它是 0,于是你会在提交大事务时看到:
ERROR: could not commit without prepared transactions HINT: Set max_prepared_transactions to a nonzero value.解决办法是在主库(以及所有涉及分片写入的远端库)都设置:
max_prepared_transactions = 50然后重启数据库。这个值不要设太大,每个正在进行的跨分片两阶段事务都会占用一个槽位,设太大会浪费内存。50 对一般业务足够。
6.2 聚合下推缺失:跨分片 GROUP BY 为什么会慢
PostgreSQL 12 里的 postgres_fdw 不支持聚合函数下推。什么意思?如果执行:
SELECT customer_id, sum(amount) FROM orders GROUP BY customer_id;执行计划会把 orders_p1、orders_p2 等所有分片的相关列全部拉到主库,再在主库做 GROUP BY 和 sum。分片库上虽然可以先 group by 再传结果,但 PG12 的 postgres_fdw 不会帮你做这一步优化。
这个限制对报表类查询打击很大。我现在遇到这种查询,会兜底用两种办法:要么把聚合任务前移到分片库自己执行,在分片库上做汇总再把结果同步到汇总表;要么明确告诉业务方,全表聚合只能容忍秒级到分钟级延迟。如果你对聚合下推有硬需求,升级到 PostgreSQL 14 或者 15 会好很多,PG14 开始支持 GROUP BY 聚合下推。
6.3 连接风暴:FDW 会话连接过多怎么办
postgres_fdw 的连接管理策略是:每个本地会话,对同一远端服务器只建立一个连接,会话结束才断开。如果应用连接池撑了 200 个连接,主库又有 4 个分片,那么分片库可能被打开最多 800 个连接。听起来夸张,实际并发量高时很容易打崩分片库的连接数限制。
我的缓解方案有几条:
- 调小应用侧连接池的连接上限,比如原来 100 个改成 40 个;
- 分片库的 max_connections 提前调高,同时把 shared_buffers 等参数同步调整,避免内存超卖;
- 用 PgBouncer 这类连接池对分片库也做一层池化,但要注意 FDW 的会话依然会持有一个远端连接,连接池收益有限;
- 设定较短的 idle_in_transaction_session_timeout / statement_timeout,把可疑的长连接杀掉。
还有一个运维小技巧:定期在分片库上查 pg_stat_activity,按 client_addr 分组,一眼看出哪个主库 IP 开了多少连接,有没有异常堆积。
6.4 统计信息不准导致执行计划跑偏
外部表的统计信息在本地不自动更新。刚开始 FDW 用远端返回的行数估算,往往不准(尤其是加了 WHERE 条件后),PG 可能选择把外部表数据全部拉回来再过滤,而不是下推 WHERE。这就是为什么有些查询在直连分片库里 3 毫秒,走 FDW 却要 3 秒。
解决办法是执行:
ANALYZE orders_p1;或者给外部表设置 opt 选项,让 PG 直接向远端拿估算值:
ALTER FOREIGN TABLE orders_p1 OPTIONS (ADD use_remote_estimate 'true');我再推荐一个组合:给关键外部表 use_remote_estimate,然后定期在维护窗口对所有外部表执行 ANALYZE。统计信息更新后,下推的概率会高很多。
6.5 备份盲区:主库备份不等于数据备份
这个我在实践里最紧张。pg_dump 主库中的 orders 表,默认只导出分区表结构,不会导出外部表背后的远端数据。这意味着你如果严格按照常规方式只备份了主库,一旦某个分片库磁盘损坏,数据就真丢了。
我的做法是分片库各自独立备份,并且备份策略要和单库时代完全一致。主库的备份更多是为了快速恢复分库分表"骨架",而不是数据。恢复演练时,也要先恢复分片库,再恢复主库,最后把外部表 attach 状态验证一遍。
6.6 外部表不支持约束和触发器
外部表不能定义本地约束、唯一约束和触发器。我们平时依赖数据库做的唯一性校验,在 FDW 分片架构下会被绕过。业务层必须承担一部分全局唯一性工作,比如用雪花算法生成全局 ID,严禁依赖单库自增列跨分片唯一。还有,外部表上不能建本地触发器,所以审计、自动更新时间戳这类逻辑,要么挪到远端真实表上,要么挪到应用层。
7. 什么场景适合,什么场景千万别用
7.1 适合的画像
pg_fdw 分库分表适合这些情况:
- 团队 DBA 资源有限,不想再维护一套中间件集群;
- 业务以点查、按分片键范围查询为主;
- 写并发不高,事务基本只涉及单个分片;
- 可以接受秒级延迟的分析型查询;
- 项目已经有 PostgreSQL 12+ 的基础,想先快速把单库压力降下来。
我身边的落地场景还有一类:数据归档。把历史订单按年份分片,存到不同的低成本实例上,主分区表只保留近期热点数据。这样查询自动裁剪掉历史分区,容量压力也分摊出去,甚至能对不同分片做不同存储策略。
7.2 不适合的画像
反过来,这些场景请谨慎:
- 高频 OLTP 小事务,每个请求都要跨网络访问一次分片库,延迟会明显增加;
- 跨分片 JOIN 很频繁,FDW 会把多个分片数据拉到主库再关联,数据量大时直接卡死;
- 需要强分布式事务和全局唯一性约束;
- 业务分页要"全局准确",比如按 customer_id 排序后分页,这种查询要对所有分片的结果集做归并,性能不可控;
- 分片字段本身会被业务频繁更新。
另外我要说句实话:pg_fdw 方案在 PostgreSQL 14 之前的天花板很明显,主要卡在异步并行和聚合下推。如果你的业务从第一天就注定是"超大并发 + 复杂分析 + 强一致",那硬上 FDW 是不合适的,不如一开始就考虑成熟的分布式数据库产品,或者干脆用中间件方案。
8. 运维监控与后续扩展建议
8.1 主库和分片库各自的监控重点
主库侧,重点看外部表相关的系统视图和远程 SQL 执行情况。你可以执行:
SELECT * FROM pg_foreign_table; SELECT * FROM pg_foreign_server; SELECT * FROM pg_user_mappings;再配合 EXPLAIN (VERBOSE) 观察 Remote SQL 是否正常下发。如果发现某个 SQL 没有下推,先查统计信息和 use_remote_estimate 配置。
分片库侧,重点看连接来源、慢查询日志、单分片容量增长。我习惯写一个脚本,每小时巡检分片库的 pg_stat_activity,把所有来自 FDW 的连接按 application_name 和 client_addr 分组,一旦超过阈值立刻告警。容量方面,按分片键的最大值估算各分片饱和度,提前 30 天预警扩容。
8.2 从 PG12 迁移到更高版本的收益
如果你刚准备新建项目,建议直接评估 PostgreSQL 14 或 15。PG14 给 postgres_fdw 带来了两个关键能力:异步 Append 并行访问多个远端分片,以及聚合和 JOIN 的下推。这意味着跨分片查询不再串行拉数据,报表类查询会有质的提升。PG15 则继续强化了 FDW 的稳定性。
从 PG12 升到 PG14/15 本身不是小事,分区表、外部表结构基本可以沿用,但要注意 FDW 的默认行为变化,比如并发访问带来的连接数变化。如果现有系统已经在跑,我的建议是先稳定运行,把维护要点吃透,再规划版本升级,别在业务高峰期做这种大动作。
9. 最后说点实战体会
整个项目做下来,我最大的体会是:pg_fdw 分库分表不是银弹,但它确实是小团队用最少成本撑住数据增长的一条务实路径。它最迷人的地方在于应用层 SQL 透明——写代码的人根本感知不到分片的存在,这对业务迭代速度非常重要。
但透明也意味着你必须自己承担起 FDW 在分布式场景里的缺失部分:全局事务、全局唯一性、跨节点查询优化,这些都成了架构师的责任。我建议任何打算上这套方案的人,先做一次 POC,把典型查询(点查、范围查、批量写)的 EXPLAIN 打开看一遍,搞清楚哪些语句能下推、哪些不能,再评估能不能接受。
运维侧还有一个容易被忽略的小技巧:给分片库的账号设置 statement_timeout 和 row_security 等参数时,主库 FDW 连接同样受远端配置影响。因为 FDW 是远端数据库的普通客户端,远端的超时设置、连接限制、甚至空闲会话回收策略都会直接作用到它头上。我出过一次故障,就是远端库空闲超时把 FDW 连接关了,主库重连期间出现了一波瞬时失败。后来我在两端配置上加了保活参数,并且把重试逻辑放进了应用层,才彻底稳定下来。
如果你已经在用或者正在评估 pg_fdw 分库分表,希望这篇能帮你把弯路走直。数据分片这条路没有标准答案,适合自己的业务形态和团队维护能力,才是一等一的正确答案。