Neki 架构解读:把 Vitess 的分片经验带到 PostgreSQL

2026-09-18 23 预计阅读时间: 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.

预计阅读时间:11 分钟

Neki 的定位很直接:由 Vitess 背后的团队探索分片 PostgreSQL。值得关注的并不只是“把数据放进多个 PostgreSQL 实例”,而是如何把路由、扩缩容、故障处理和兼容性封装成一个可长期运维的数据库系统。

目前给出的信息主要是架构方向,而不是完整的组件与协议说明。下面不会臆测 Neki 尚未公开的 API,而是结合分片数据库的通用约束,说明这类架构必须解决哪些问题,以及应用团队应该如何评估它。

分片系统的核心不是拆库,而是路由

最简单的分片方案,是让应用根据 tenant_iduser_id 选择数据库:

request
   |
   v
shard router
   |-- tenant A --> PostgreSQL shard 0
   |-- tenant B --> PostgreSQL shard 1
   `-- tenant C --> PostgreSQL shard 2

真正困难的是,随着系统演进,路由规则不能永远写死在应用里。一个成熟的分片 PostgreSQL 架构通常需要分成几层:

  • SQL 接入层:接受客户端连接,尽量维持 PostgreSQL 协议与常用驱动的兼容性。
  • 路由层:从查询或会话上下文中识别分片键,把请求发送到正确分片。
  • 数据层:运行普通 PostgreSQL 实例,每个分片负责一部分数据。
  • 控制面:保存拓扑、分片范围、副本状态和迁移进度,并协调故障切换与再分片。

这些是分析 Neki 时可采用的架构视角,不代表其组件一定使用上述名称。Vitess 的重要经验之一,正是把“数据库节点”和“集群控制能力”分开:节点负责执行查询,控制面负责回答数据在哪里、流量该往哪里走,以及拓扑变化时如何安全切换。

这种设计能减少应用感知,但并不能让分片完全消失。SQL 是否能被准确路由,依然取决于数据模型。

分片键决定了事务边界

假设订单表按 tenant_id 分片:

CREATE TABLE orders (
    tenant_id  bigint NOT NULL,
    order_id   uuid NOT NULL,
    amount     numeric(12, 2) NOT NULL,
    status     text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (tenant_id, order_id)
);

这里把 tenant_id 放进主键不是装饰,而是在表达一个重要约束:订单的身份和物理位置都与租户相关。

带有分片键的查询容易定向执行:

SELECT *
FROM orders
WHERE tenant_id = 42 AND order_id = '6bb7c250-9e7a-4f25-9e14-73e40aec8241';

缺少分片键的查询则可能变成 scatter-gather:路由层向全部分片发送请求,再合并结果。

SELECT status, count(*)
FROM orders
GROUP BY status;

第二条 SQL 在单机 PostgreSQL 上很普通,在分片环境中却涉及并行执行、部分聚合、结果合并、超时传播和资源限制。ORDER BY ... LIMIT、窗口函数、跨表连接以及全局唯一约束会让问题更复杂。

因此,评估 Neki 或其他分片 PostgreSQL 时,不应只问“支持 PostgreSQL SQL 吗”,还要继续追问:

  1. 路由层如何识别分片键?
  2. 跨分片查询是拒绝、下推,还是自动合并?
  3. 单分片事务与跨分片事务分别有什么语义?
  4. 唯一索引、外键、序列和 advisory lock 的作用域是什么?
  5. 驱动连接、预处理语句和连接池是否需要调整?

可以这样实践:用两个 PostgreSQL 模拟应用侧分片

下面是一个独立教学实验,不是 Neki 的真实 API。它可以帮助团队在引入分片中间层之前,直观看到分片键和路由规则如何影响代码。

先创建 compose.yaml

services:
  pg0:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: demo
    ports:
      - '55432:5432'

  pg1:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: demo
    ports:
      - '55433:5432'

启动两个分片并初始化表:

docker compose up -d

until docker compose exec -T pg0 pg_isready -U app -d demo >/dev/null 2>&1; do sleep 1; done
until docker compose exec -T pg1 pg_isready -U app -d demo >/dev/null 2>&1; do sleep 1; done

for service in pg0 pg1; do
  docker compose exec -T "$service" psql -U app -d demo -c '
    CREATE TABLE IF NOT EXISTS customer_notes (
      tenant_id bigint NOT NULL,
      note_id bigint NOT NULL,
      body text NOT NULL,
      PRIMARY KEY (tenant_id, note_id)
    );'
done

python3 -m venv .venv
. .venv/bin/activate
pip install 'psycopg[binary]>=3.1,<4'

保存下面的代码为 demo.py

import hashlib
import psycopg

SHARDS = [
    'postgresql://app:app@localhost:55432/demo',
    'postgresql://app:app@localhost:55433/demo',
]


def shard_number(tenant_id: int) -> int:
    key = str(tenant_id).encode('utf-8')
    digest = hashlib.sha256(key).digest()
    return int.from_bytes(digest[:8], 'big') % len(SHARDS)


def connect_for_tenant(tenant_id: int):
    number = shard_number(tenant_id)
    print(f'tenant={tenant_id} -> shard={number}')
    return psycopg.connect(SHARDS[number])


def save_note(tenant_id: int, note_id: int, body: str) -> None:
    with connect_for_tenant(tenant_id) as conn:
        conn.execute(
            '''
            INSERT INTO customer_notes (tenant_id, note_id, body)
            VALUES (%s, %s, %s)
            ON CONFLICT (tenant_id, note_id)
            DO UPDATE SET body = EXCLUDED.body
            ''',
            (tenant_id, note_id, body),
        )


def read_notes(tenant_id: int):
    with connect_for_tenant(tenant_id) as conn:
        return conn.execute(
            '''
            SELECT note_id, body
            FROM customer_notes
            WHERE tenant_id = %s
            ORDER BY note_id
            ''',
            (tenant_id,),
        ).fetchall()


if __name__ == '__main__':
    save_note(42, 1, 'hello from tenant 42')
    save_note(99, 1, 'hello from tenant 99')
    print('tenant 42:', read_notes(42))
    print('tenant 99:', read_notes(99))

运行:

python demo.py

这个例子故意使用简单的哈希取模。它适合解释路由,却不适合作为生产设计:一旦分片数量从 2 变成 3,大量租户的计算结果都会变化,而旧数据仍留在原分片。生产系统需要稳定的分片映射、版本化拓扑,以及复制、追赶、切流和清理组成的在线迁移流程。这里也正是专门控制面相对于应用内硬编码路由的价值所在。

再分片与高可用是两件不同的事

分片解决容量和吞吐量分布问题,高可用解决节点或可用区故障问题,两者不能互相替代。即使数据已经分成 100 个分片,每个分片仍可能需要主从复制、备份、时间点恢复和故障切换。

一次可靠的在线再分片通常包含:

  1. 创建目标分片并记录迁移计划。
  2. 复制历史数据。
  3. 持续追赶源分片上的新增写入。
  4. 校验行数、校验和或业务不变量。
  5. 在明确的一致性边界上切换读写流量。
  6. 保留回滚窗口,确认稳定后清理旧数据。

评估系统时,需要观察这些阶段是否可见、可暂停、可恢复。只提供一个“开始迁移”按钮,却没有迁移延迟、错误率和数据校验指标,会让故障排查非常困难。

PostgreSQL 兼容性也是边界条件。扩展、触发器、逻辑复制、LISTEN/NOTIFY、大对象、序列、临时表以及会话级设置,都可能依赖单节点或单会话语义。宣称兼容 PostgreSQL 协议,并不自动意味着所有 PostgreSQL 行为都能跨分片保持不变。

采用前的检查清单

Neki 最值得期待的方向,是把 Vitess 团队积累的路由、拓扑管理和在线扩缩容经验带入 PostgreSQL 生态。但是否适合生产环境,应由工作负载和运维能力决定,而不是只看“自动分片”这一标签。

可以从以下清单开始验证:

  • 能否为主要业务表选出稳定且分布均匀的分片键?
  • 多少请求能够在单个分片内完成?
  • 跨分片事务失败时,应用会看到什么结果?
  • 热点租户能否独立迁移或拆分?
  • 再分片期间允许哪些 DDL 操作?
  • PostgreSQL 驱动、ORM 和连接池是否保持兼容?
  • 备份与恢复是按节点、按分片还是按整个集群执行?
  • 是否能观测路由错误、scatter-gather 查询和迁移延迟?
  • 控制面不可用时,现有读写能否继续?

最稳妥的采用路径,是先挑选天然按租户或账户隔离、跨分片查询较少的业务做验证。先确认路由与故障语义,再测试在线扩容,最后才考虑把复杂关联查询和强跨分片事务迁入系统。分片数据库真正的价值,不是让应用假装只有一台 PostgreSQL,而是让数据位置的变化变得可控、可观测并且能够恢复。


相关推荐