Neki 的定位很直接:由 Vitess 背后的团队探索分片 PostgreSQL。值得关注的并不只是“把数据放进多个 PostgreSQL 实例”,而是如何把路由、扩缩容、故障处理和兼容性封装成一个可长期运维的数据库系统。
目前给出的信息主要是架构方向,而不是完整的组件与协议说明。下面不会臆测 Neki 尚未公开的 API,而是结合分片数据库的通用约束,说明这类架构必须解决哪些问题,以及应用团队应该如何评估它。
分片系统的核心不是拆库,而是路由
最简单的分片方案,是让应用根据 tenant_id 或 user_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 吗”,还要继续追问:
- 路由层如何识别分片键?
- 跨分片查询是拒绝、下推,还是自动合并?
- 单分片事务与跨分片事务分别有什么语义?
- 唯一索引、外键、序列和 advisory lock 的作用域是什么?
- 驱动连接、预处理语句和连接池是否需要调整?
可以这样实践:用两个 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 个分片,每个分片仍可能需要主从复制、备份、时间点恢复和故障切换。
一次可靠的在线再分片通常包含:
- 创建目标分片并记录迁移计划。
- 复制历史数据。
- 持续追赶源分片上的新增写入。
- 校验行数、校验和或业务不变量。
- 在明确的一致性边界上切换读写流量。
- 保留回滚窗口,确认稳定后清理旧数据。
评估系统时,需要观察这些阶段是否可见、可暂停、可恢复。只提供一个“开始迁移”按钮,却没有迁移延迟、错误率和数据校验指标,会让故障排查非常困难。
PostgreSQL 兼容性也是边界条件。扩展、触发器、逻辑复制、LISTEN/NOTIFY、大对象、序列、临时表以及会话级设置,都可能依赖单节点或单会话语义。宣称兼容 PostgreSQL 协议,并不自动意味着所有 PostgreSQL 行为都能跨分片保持不变。
采用前的检查清单
Neki 最值得期待的方向,是把 Vitess 团队积累的路由、拓扑管理和在线扩缩容经验带入 PostgreSQL 生态。但是否适合生产环境,应由工作负载和运维能力决定,而不是只看“自动分片”这一标签。
可以从以下清单开始验证:
- 能否为主要业务表选出稳定且分布均匀的分片键?
- 多少请求能够在单个分片内完成?
- 跨分片事务失败时,应用会看到什么结果?
- 热点租户能否独立迁移或拆分?
- 再分片期间允许哪些 DDL 操作?
- PostgreSQL 驱动、ORM 和连接池是否保持兼容?
- 备份与恢复是按节点、按分片还是按整个集群执行?
- 是否能观测路由错误、scatter-gather 查询和迁移延迟?
- 控制面不可用时,现有读写能否继续?
最稳妥的采用路径,是先挑选天然按租户或账户隔离、跨分片查询较少的业务做验证。先确认路由与故障语义,再测试在线扩容,最后才考虑把复杂关联查询和强跨分片事务迁入系统。分片数据库真正的价值,不是让应用假装只有一台 PostgreSQL,而是让数据位置的变化变得可控、可观测并且能够恢复。