Postgres 分片为何等了二十年才走向成熟

2026-08-24 40 预计阅读时间: 1 分钟
来源: planetscale.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

Postgres 分片并不是一个突然出现的新需求。数据规模、写入吞吐、租户隔离和跨地域部署早已迫使团队把单机数据库拆开。真正耗时的部分在于:把数据拆到多台机器很容易,把它重新包装成仍然可靠、可运维、符合 PostgreSQL 使用习惯的数据库能力,却极其困难。过去二十年的演进,正是这段工程债被逐项偿还的过程。

分片难的从来不是“按键取模”

最朴素的分片方案通常只有一条规则:根据某个分片键决定数据落在哪个节点。例如,tenant_id % 4 决定租户归属的数据库。这能解决单表容量问题,却会立刻把原本由数据库承担的复杂性推给应用:

  • 查询必须带上分片键,否则需要向所有节点广播。
  • 跨分片 JOIN、聚合、排序和分页会变成分布式执行问题。
  • 全局唯一 ID、约束、事务语义和二级索引都需要重新定义。
  • 扩容不是增加一台机器,而是迁移既有数据,并在迁移期间维持读写正确性。
  • 备份、恢复、故障切换、监控和版本升级的对象从一套数据库变成一个集群。

因此,早期“Postgres 分片”往往意味着应用层路由、多个独立 PostgreSQL 实例和大量约定。它可用,但成本高,而且每个团队都要重复实现路由、重平衡和故障处理。

二十年里逐渐补齐的能力

从历史角度看,Postgres 分片之所以长期显得不够成熟,并不表示 PostgreSQL 缺少扩展性,而是分片需要多个层面同时成熟。

数据库内核需要足够可扩展。 分布式查询规划、执行器扩展、分区机制、复制能力、逻辑解码,以及更可靠的并发控制,都会影响分片系统能否像一个数据库而非一组脚本那样工作。

生态需要形成稳定接口。 当扩展能复用 PostgreSQL 的 SQL、协议、工具链和运维经验时,用户才不必为了水平扩展彻底重写应用。否则,迁移成本会抵消分片带来的收益。

运维语义需要可落地。 真正困难的是节点加入与移除、数据重分布、失败恢复、升级兼容和可观测性。这些事情无法只靠一次 CREATE TABLE 完成,必须在长期生产环境中打磨。

也正因如此,“好用的 Postgres 分片”不是某个单独功能上线,而是查询、数据移动、一致性和日常操作逐步闭环后的结果。

设计分片前,先把访问路径画出来

可以这样实践:先在应用数据库中提取最常见的查询,再判断它们是否天然围绕同一个键聚集。多租户 SaaS 往往适合按 tenant_id 分片,因为大多数读写、权限判断和业务关联都限定在一个租户内。

下面这个最小 SQL 示例展示了一个适合按租户路由的订单模型。它不是完整的分布式数据库实现,而是用来检验分片键是否进入了关键访问路径的起点。

CREATE TABLE orders (
    tenant_id bigint NOT NULL,
    order_id bigint NOT NULL,
    customer_id bigint NOT NULL,
    status text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    total_cents integer NOT NULL CHECK (total_cents >= 0),
    PRIMARY KEY (tenant_id, order_id)
);

CREATE INDEX orders_tenant_created_idx
    ON orders (tenant_id, created_at DESC);

-- 适合定向路由:条件中包含 tenant_id。
SELECT order_id, status, total_cents
FROM orders
WHERE tenant_id = 42
  AND created_at >= now() - interval '30 days'
ORDER BY created_at DESC
LIMIT 50;

这里把 tenant_id 放进主键和索引前缀,不只是索引优化,也是明确告诉系统:订单的归属和最常见的访问边界都是租户。相反,若核心接口经常执行“查询全站最近订单”或“按任意 customer_id 查历史订单”,就要提前设计汇总表、搜索索引、分析副本,或接受跨分片查询的代价。

用一个可运行的路由器暴露应用层成本

在没有数据库级分片能力时,团队常常会在应用层做类似下面的路由。示例可直接运行,它用 tenant_id 选择连接串;实际项目应把连接池、健康检查、重试和凭据管理交给成熟组件。

from dataclasses import dataclass

@dataclass(frozen=True)
class Shard:
    name: str
    dsn: str

SHARDS = [
    Shard("shard_a", "postgresql://app:secret@db-a:5432/app"),
    Shard("shard_b", "postgresql://app:secret@db-b:5432/app"),
    Shard("shard_c", "postgresql://app:secret@db-c:5432/app"),
]

def shard_for_tenant(tenant_id: int) -> Shard:
    if tenant_id <= 0:
        raise ValueError("tenant_id must be positive")
    return SHARDS[tenant_id % len(SHARDS)]

if __name__ == "__main__":
    for tenant_id in (1, 2, 3, 42):
        shard = shard_for_tenant(tenant_id)
        print(f"tenant={tenant_id} -> {shard.name} ({shard.dsn})")

这段代码刻意简单,也恰好暴露了问题:当分片数量从 3 变成 4,取模结果会大面积变化。实际扩容不能直接替换 len(SHARDS),否则大量租户会被路由到尚未迁移数据的新节点。成熟的分片方案必须把“路由规则变更”和“数据迁移完成”协调为一个可恢复的流程。

采用分片时检查四个边界

分片适合那些单机资源已经成为明确瓶颈,并且业务数据能够按稳定键分组的系统。采用前至少回答下面四个问题:

  1. 定位率:多少关键请求能带上分片键并只访问一个节点?
  2. 跨分片语义:跨租户查询、全局唯一约束和事务需要强一致,还是可通过异步汇总解决?
  3. 重平衡窗口:迁移一个热点租户或增加节点时,读写如何继续进行,失败后如何回滚或续传?
  4. 运维所有权:谁负责节点生命周期、备份恢复演练、容量预测和全局监控?

Postgres 分片花了很长时间才变得更可用,原因正是这些问题没有任何一个能被“把表拆开”单独解决。对团队而言,最稳妥的路径通常不是尽早分片,而是先把数据访问围绕明确边界组织起来;当容量和隔离需求确实到来时,才让数据库能力和运维流程一起承担分布式复杂性。


相关推荐