为什么 Postgres 分片方案迟到了二十年

2026-08-24 44 预计阅读时间: 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.

预计阅读时间:8 分钟

Postgres 分片并不是一个突然出现的问题。真正值得追问的是:数据库已经成熟多年,为什么直到今天,开发者才开始看到更可用、更接近原生体验的 Postgres 分片方案?答案很大程度上藏在过去二十年的技术演进里:分布式系统的工程经验、云基础设施、复制能力、运维自动化,以及应用对水平扩展的真实需求,缺一不可。

分片难的从来不只是“把表拆开”

把一张大表按用户 ID、租户 ID 或区域拆到多台机器上并不复杂。困难在于拆分之后,Postgres 原本提供的数据库语义不能轻易丢失:

  • 查询希望仍然能表达关联、聚合和事务。
  • 写入需要稳定地路由到正确节点。
  • 节点故障、扩容和迁移不能演变成停机项目。
  • 应用和运维团队需要看得见数据到底在哪、查询经过了什么路径。

早期方案往往要求业务代码自己维护路由逻辑,并接受跨分片查询困难、事务能力受限、再平衡代价高等条件。这些限制并非单一产品设计不足,而是分布式数据库的基本成本。

二十年里补齐了哪些基础能力

“好的分片”通常意味着把复杂性从每个业务团队手里收回,沉到数据库扩展、控制平面和运维工具中。这个过程依赖几类能力逐渐成熟。

一类是 Postgres 自身生态的扩展能力,包括复制、逻辑变更捕获、分区、连接管理与可观测性。它们未必直接等同于分片,但为数据移动、读扩展和故障处理提供了基础积木。

另一类是云时代的运行条件。自动化部署、持久卷、指标采集、备份恢复和弹性资源,使运行多节点数据库不再完全依赖手工操作。没有这些能力,分片系统即使功能完整,也很难被多数团队可靠地采用。

更重要的是,行业对一致性边界形成了更务实的认识。不是所有查询都必须跨全部节点执行,也不是所有事务都必须全局协调。围绕租户、用户或业务实体选择分片键,让大多数请求保持单分片执行,通常比追求“任意 SQL 都透明分布式执行”更可控。

先设计分片键,再讨论分片产品

分片键决定了系统未来的自由度。一个好的键应让高频读写尽量落在同一分片,并避免少数键形成热点。

例如,多租户 SaaS 常把 tenant_id 作为首选候选:租户内的订单、成员、配置和权限数据可以共同定位到一个分片。代价是跨租户报表不能再假设是一次普通本地查询,需要走汇总库、异步分析链路或受控的 scatter-gather 查询。

可以这样实践:在应用层先显式实现一个最小路由器。即使未来迁移到透明分片扩展,这段代码也能迫使团队明确数据定位规则。

# router.py
# 假设:每个分片都是独立的 Postgres 数据库,分片数固定为 4。
# 安装依赖:pip install "psycopg[binary]"

import os
import psycopg

SHARDS = [
    os.environ["POSTGRES_SHARD_0"],
    os.environ["POSTGRES_SHARD_1"],
    os.environ["POSTGRES_SHARD_2"],
    os.environ["POSTGRES_SHARD_3"],
]


def shard_dsn(tenant_id: int) -> str:
    if tenant_id < 0:
        raise ValueError("tenant_id must be non-negative")
    return SHARDS[tenant_id % len(SHARDS)]


def create_order(tenant_id: int, order_id: int, amount_cents: int) -> None:
    sql = """
        INSERT INTO orders (tenant_id, order_id, amount_cents)
        VALUES (%s, %s, %s)
    """
    with psycopg.connect(shard_dsn(tenant_id)) as conn:
        with conn.cursor() as cur:
            cur.execute(sql, (tenant_id, order_id, amount_cents))


if __name__ == "__main__":
    create_order(tenant_id=42, order_id=10001, amount_cents=2599)

运行前,需要在每个目标库创建相同的表结构,并设置四个连接串:

export POSTGRES_SHARD_0='postgresql://app:secret@db-0:5432/app'
export POSTGRES_SHARD_1='postgresql://app:secret@db-1:5432/app'
export POSTGRES_SHARD_2='postgresql://app:secret@db-2:5432/app'
export POSTGRES_SHARD_3='postgresql://app:secret@db-3:5432/app'
python router.py

这个示例不是完整分片产品:它没有处理重分片、故障切换、跨分片事务或连接池。但它清楚展示了分片系统必须承担的第一项工作:为每一条请求确定数据归属。

透明能力仍然有边界

现代 Postgres 分片方案可以减少应用改造,但不能消除数据分布带来的物理约束。跨分片 JOIN、全局唯一约束、全局排序、跨节点事务和热点键,都可能引入网络往返、协调开销或执行计划的不确定性。

因此,评估方案时不要只问“是否支持分片”,而应拿真实工作负载逐项验证:

  • 90% 以上的核心请求能否按分片键命中单个节点?
  • 节点扩容时,数据迁移是否可限速、可观测、可回滚?
  • 主节点故障、网络分区和副本延迟时,读写语义是什么?
  • 分析型全局查询会不会拖慢在线事务流量?
  • 备份、恢复和演练是否覆盖单分片与全局场景?

Postgres 分片方案花了二十年才逐渐变得可用,不是因为“分表”本身需要二十年,而是因为可靠运行一个分布式关系数据库需要整套工程能力。对团队而言,最稳妥的采用路径通常是从明确分片键和单分片事务开始,再逐步引入自动路由、数据迁移与跨节点查询能力。


相关推荐