Neki 如何用 512 个分片将 PostgreSQL 推到每秒 1.185 亿次查询

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

预计阅读时间:10 分钟

Neki 的这组数据很容易让人先记住一个数字:每秒 1.185 亿次查询。更值得分析的是背后的规模模型:一个大规模分片 PostgreSQL 集群,拥有 512 个分片,单个分片约承载每秒 20 万次查询。

这类结果并不意味着单个 PostgreSQL 实例突然获得了超大吞吐,而是把请求、数据和连接压力拆散到大量相互独立的分片上。工程重点从“如何让一台数据库更快”转变为“如何让分片路由、热点控制、连接管理和故障处理保持稳定”。

先把吞吐数字算清楚

按摘要中的单分片数据直接计算:

200,000 queries/s × 512 shards = 102,400,000 queries/s

这个结果约为每秒 1.024 亿次查询,与报告中的 1.185 亿次总吞吐并不完全相同。实际压测中可能存在分片负载不均、峰值统计口径不同,或者“每分片 20 万次”是一个近似值。因此,阅读这类性能数字时,需要确认至少四个指标:

  • 总吞吐是平均值、峰值,还是某个时间窗口内的累计值。
  • 查询是否全部命中单个分片,还是一个请求可能访问多个分片。
  • 统计的是数据库执行次数,还是包含连接池、代理和客户端重试的请求次数。
  • 延迟分布如何,尤其是 p95、p99 和错误率。

单看 QPS 不足以判断系统是否可用。一个吞吐很高但 p99 延迟失控的系统,通常无法支撑稳定的线上流量。

分片把问题变成了并行扩展

分片的核心收益是降低单个数据库节点需要处理的数据量、索引规模和并发压力。假设业务可以根据 tenant_id、用户 ID 或对象 ID 选择分片,那么路由层可以把请求发送到对应的 PostgreSQL 实例:

request -> shard router -> hash(key) -> PostgreSQL shard

理想情况下,每个分片拥有相似的数据规模和请求量。512 个分片还带来一个重要特征:单个分片出现抖动时,影响范围可以被限制在局部。但这也引入了新的系统性风险:

  • 分片键分布不均会形成热点分片。
  • 跨分片查询不能再依赖普通的单库 JOIN。
  • 扩容时需要迁移数据或改变路由规则。
  • 分片故障、网络分区和重试可能放大流量。
  • 连接数会随分片数量增长,连接池设计变得关键。

因此,分片不是简单地把数据库复制 512 份。它要求应用明确知道数据属于哪个分片,并且把跨分片操作设计成异步汇总、预聚合或可接受的多阶段查询。

每个分片 20 万 QPS意味着什么

每秒 20 万次查询是一个很高的单分片目标。它通常需要多个条件同时成立:查询足够简单,数据访问模式高度稳定,缓存命中率较高,索引路径明确,并且测试期间没有大量慢查询或复杂事务。

可以从以下角度拆解这个数字:

  1. 查询形态:点查、固定条件查询和短事务更容易获得稳定吞吐;复杂排序、聚合和大范围扫描会显著降低容量。
  2. 数据局部性:热点数据能否留在内存中,直接影响 CPU、内存和存储延迟。
  3. 连接管理:应用不应为每个请求创建数据库连接,而应通过连接池复用有限连接。
  4. 写入比例:高并发读和高并发写的瓶颈不同,WAL、锁竞争和检查点都需要单独观测。
  5. 延迟目标:在固定 QPS 下逐步提高并发,直到 p99 或错误率开始恶化,才能找到有效容量边界。

下面这个小脚本可以帮助团队快速核对“单分片吞吐 × 分片数量”的理论值。它不是压测工具,只用于避免报告中的统计口径被误读:

python3 - <<'PY'
shards = 512
per_shard_qps = 200_000
reported_qps = 118_500_000

calculated_qps = shards * per_shard_qps
print(f'calculated: {calculated_qps:,} QPS')
print(f'reported:   {reported_qps:,} QPS')
print(f'difference:  {reported_qps - calculated_qps:,} QPS')
print(f'ratio:       {reported_qps / calculated_qps:.3f}x')
PY

如果要把这种模型落到自己的系统,建议先建立一个明确的路由接口,而不是让业务代码到处拼接分片名称。下面是一个可以改造成服务代码的最小伪实现,假设使用稳定的哈希函数根据租户 ID选择分片:

import hashlib

SHARD_COUNT = 512


def shard_for(tenant_id: str) -> int:
    digest = hashlib.sha256(tenant_id.encode('utf-8')).digest()
    number = int.from_bytes(digest[:8], 'big')
    return number % SHARD_COUNT


for tenant_id in ('tenant-a', 'tenant-b', 'tenant-c'):
    print(tenant_id, shard_for(tenant_id))

生产环境中还需要把这个映射放进版本化配置或一致性哈希环,并为数据迁移预留双读、双写或路由切换策略。直接修改 SHARD_COUNT 会改变大量租户的分片归属,不能把它当成无状态配置热更新。

压测不能只看数据库的 QPS

要复现或验证类似结果,压测报告至少应同时记录:

  • 分片数量、每个分片的 CPU、内存、存储和网络规格。
  • 查询模板、返回行数、读写比例和事务边界。
  • 连接池大小、客户端并发数以及是否存在代理层。
  • 每个分片的 QPS 分布,而不是只报告集群总和。
  • p50、p95、p99 延迟、超时和重试次数。
  • 热点键比例、缓存命中率、锁等待和慢查询数量。
  • 节点重启、网络抖动或单分片降级时的恢复行为。

尤其要避免一种常见误区:客户端不断重试,把失败请求再次计入“查询次数”,最终得到一个看起来很高、实际上由故障放大的吞吐数字。可靠的测试应同时观察成功请求数、数据库执行数和错误率。

采用这条路线前的检查清单

适合使用大规模分片 PostgreSQL 的前提,是业务能够稳定识别分片键,并且绝大多数核心访问都能在单分片内完成。若业务经常需要跨租户聚合、全局排序或强一致事务,分片带来的复杂度可能超过收益。

可以按以下顺序推进:

  1. 用真实查询模板测出单分片基线,而不是直接按理论倍数估算集群容量。
  2. 选择低倾斜、长期稳定的分片键,并单独压测热点数据。
  3. 设计连接池和限流,避免 512 个分片让客户端连接数失控。
  4. 把跨分片查询改造成预聚合、异步任务或受控 fan-out。
  5. 用 p99、错误率和恢复时间定义容量边界,而不只追求最高 QPS。
  6. 在生产流量规模下演练扩容、迁移、单分片故障和路由回滚。

Neki 的 1.185 亿次查询每秒,真正有价值的启发不是一个可以直接复制的数字,而是清晰的水平扩展模型:把数据和请求拆到足够多的分片上,再用严格的路由、观测和故障边界把并行能力转化为可运营的系统容量。


相关推荐