无需分片,将 PostgreSQL 写入性能扩展至每秒 10 万次

由 John Doe 十月 27, 2026

大多数开发者在数据库遇到写入性能压力时,第一反应往往会是升级成分布式分片架构,实际上大多数情况下都是没有必要的。

目录

image

作者:Abul Kalam Azad

当工程团队在 PostgreSQL 中遭遇写入吞吐量瓶颈时,架构上的默认思路往往是采用分布式分片方案(例如 Citus、应用层手动分区,或是迁移到 NoSQL 数据库)。

分片会带来沉重的运维负担:两阶段提交、跨分片外键失效、复杂的分布式事务,以及高难度的表结构迁移。

在引入多节点分片之前,借助现代硬件并对 PostgreSQL 引擎机制进行针对性调优,单台主库实例就可以突破每秒 10 万次写入。

要实现这一目标,需要消除磁盘同步瓶颈、批量执行引擎操作、降低索引更新开销,并优化 PostgreSQL 的预写日志(WAL)。

消除每次写入的 fsync 瓶颈

默认情况下,PostgreSQL 优先保障数据持久性,而非极限吞吐量。每次事务提交都会执行 fsync() 系统调用,在向客户端返回成功之前,强制磁盘盘片或 NVMe 控制器缓存将 WAL 数据刷写到非易失性存储中。

一块高性能 NVMe 硬盘可承载 1 万 ~ 4 万 IOPS,无法支撑每秒来自独立线程的 10 万次阻塞式 fsync() 调用。

解决方案:异步提交

如果你的业务可以接受主机发生硬崩溃时丢失数毫秒的数据(例如遥测、事件、动态信息流、监控指标等场景),可以开启异步提交:

-- 在 postgresql.conf 中全局配置,或在会话级设置:
synchronous_commit = off
wal_writer_delay = 10ms

当 synchronous_commit = off 时,事务将 WAL 记录写入 PostgreSQL 内存中的 WAL 缓冲区后就立即返回成功,无需等待物理磁盘刷写完成。

内置的 walwriter 后台进程会每隔 wal_writer_delay 设定的时间(默认 10ms),或在缓冲区写满时,将 WAL 数据块批量刷写到磁盘。

持久性权衡:数据库一致性与崩溃恢复能力完全不受影响。崩溃不会损坏数据库,你仅可能丢失最近 10~20 毫秒内未刷盘的提交数据。

写入批量处理:摒弃单条 INSERT 语句

每秒执行 10 万条独立的 INSERT INTO tbl VALUES (...) 语句,会产生 10 万次网络往返、SQL 解析与事务边界开销。即便使用持久化连接池,应用吞吐量也会因此大幅下降。

方案 A:多值 INSERT 批量写入

在应用层聚合写入请求(例如在 Redis 或内存中缓冲 10 毫秒,或攒够 500 条记录),然后以多行插入的方式批量刷入:

INSERT INTO event_logs (device_id, event_type, payload, created_at) VALUES
   (101, 'ping', '{"temp": 22}', NOW()),
   (102, 'alert', '{"temp": 85}', NOW()),
   -- ... 每批建议 500~1000 行
   (500, 'ping', '{"temp": 21}', NOW());

1000 行的批量写入仅承担一次查询的解析与事务开销,就能完成 1000 条数据写入。

方案 B:COPY 协议(吞吐量最高)

若追求极致写入性能,PostgreSQL 流式 COPY 协议可以完全绕过标准的查询解析与执行计划流程。

使用各语言原生驱动的流式 API(如 Go 的 pgcopy、Node/Python 的 COPY FROM STDIN BINARY、.NET 的 NpgsqlCopyBinaryImporter),数据可直接流式写入原始表页:

COPY event_logs (device_id, event_type, payload, created_at)
 FROM STDIN WITH (FORMAT binary);

基准测试普遍显示,与参数化多行 INSERT 查询相比,COPY 流式写入的吞吐量可提升 3~5 倍。

精简二级索引,善用 HOT 更新

二级索引会严重拖累写入负载。如果一张表包含 5 个索引,写入单行数据就需要更新表的堆页面,外加 5 个独立的 B 树索引页面。

带有 4 个二级索引的单行写入流程:

[写入行数据]
    ├──> 写入表堆页面
    ├──> 更新索引 1(created_at B 树)
    ├──> 更新索引 2(user_id B 树)
    ├──> 更新索引 3(status B 树)
    └──> 更新索引 4(device_id B 树)

这会造成 5 倍写入放大,并频繁触发索引页分裂。

降低索引放大的技巧

  • 移除低效用二级索引:写入密集的数据表要严格精简索引。高频写入的接入表只保留主键,最多再加 1~2 个必要的二级索引。
  • 降低表填充因子(fillfactor)以启用 HOT 更新: 对于高频 UPDATE 操作,只要更新的列未被索引,PostgreSQL 的**仅堆元组(HOT)**优化可以让更新在同一个数据页内完成,无需修改二级索引:
-- 为每个表页预留 20% 的空闲空间,用于就地更新
ALTER TABLE active_sessions SET (fillfactor = 80);

WAL 缓冲区与检查点调优

PostgreSQL 的默认配置参数是针对普通服务器设计的。在每秒 10 万行的写入压力下,默认 WAL 配置会因检查点过于频繁,导致写入卡顿。

在 postgresql.conf 中进行如下配置:

# 调整 WAL 缓冲区大小,应对写入流量突增
wal_buffers = 64MB

# 增大检查点之间的最大 WAL 容量,平滑磁盘 I/O
max_wal_size = 64GB
min_wal_size = 8GB

# 拉长检查点写入时长,避免磁盘 I/O 尖峰
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min

配置原理

  • max_wal_size:如果设置过小(例如默认的 1GB),高强度写入会在数秒内耗尽阈值,触发强制即时检查点。这会导致磁盘写入延迟飙升,阻塞后端工作进程。
  • checkpoint_completion_target = 0.9:指示后台检查点进程将脏页写入分散在 checkpoint_timeout 窗口的 90% 时长内完成,平缓 I/O 曲线。

声明式表分区(无需跨节点分片)

原生声明式分区不等同于分片,它运行在单个 PostgreSQL 服务器实例上,只是将一张巨大的逻辑表拆分为磁盘上多个独立的物理文件。

CREATE TABLE metric_streams (
    id BIGINT GENERATED ALWAYS AS IDENTITY,
    device_id INT NOT NULL,
    recorded_at TIMESTAMPTZ NOT NULL,
    val DOUBLE PRECISION
) PARTITION BY RANGE (recorded_at);

-- 创建按天分区
CREATE TABLE metrics_2026_09_19 PARTITION OF metric_streams
    FOR VALUES FROM ('2026-09-19 00:00:00+00') TO ('2026-09-20 00:00:00+00');

性能优势

  • 索引工作集更紧凑:每天的分区对应小型、紧凑的 B 树索引,可完全存放在 shared_buffers(共享内存缓冲区)中,写入时避免磁盘换入换出。
  • 即时清理(DROP TABLE):无需执行速度慢、会产生表膨胀的 DELETE FROM metric_streams WHERE recorded_at < NOW() - INTERVAL '30 days' 操作,直接删除旧分区即可,零写入开销。

硬件与存储拓扑

要达到每秒 10 万次写入,需要足够的物理硬件带宽:

  • 存储:采用 RAID 10 的专属 NVMe 硬盘。将预写日志目录(pg_wal)与主数据库集群目录(base)拆分到不同的物理 NVMe 硬盘/控制器上,避免检查点刷写与 WAL 写入争抢 I/O 资源。
  • 连接:在 PostgreSQL 前端部署 PgBouncer 这类高性能连接池,使用事务级连接池模式。PostgreSQL 每个连接都会 fork 一个完整的操作系统进程;数千个活跃连接会造成 CPU 上下文切换瓶颈。建议将活跃服务器连接数控制在 50~100 之间。

实施清单

  • 在高吞吐量写入链路中将 synchronous_commit 设为 off
  • 将行写入聚合为 500~1000 行的多值批量,或使用二进制 COPY 协议
  • 移除写入密集表中不必要的二级索引
  • 将 max_wal_size 扩大到 32GB~64GB,并设置 checkpoint_completion_target = 0.9
  • 将 pg_wal 磁盘存储与主数据存储分离
  • 通过 PgBouncer 以事务池模式转发应用流量
  • 对高写入量表按时间范围分区,让活跃 B 树索引常驻内存

请先充分挖掘这些单节点优化手段。在绝大多数工程场景中,远不需要用到分布式多节点分片,PostgreSQL 就足以承载企业级的写入体量。

参考

How to Scale PostgreSQL to 100,000 Writes Per Second Without Sharding