创业公司的 Postgres 生存指南

查看原文 HN 讨论

文章摘要

Hatchet 联合创始人 Alexander Belanger 把两年生产环境使用 Postgres 的经验浓缩成了这篇指南。核心论点是:创业公司遇到的绝大多数数据库问题,根源不是 Postgres 本身的能力上限,而是糟糕的表结构设计、没优化过的查询,以及配置不当的 autovacuum。

在基础层面,作者强调「schema 是往后最难改的东西」,因此值得在早期多花时间。具体建议包括:主键用 identity 列或 UUID;时间戳一律用 timestamptz;外键级联删除要有选择地使用(低写入量下更安全);定表结构之前先想清楚这张表是读密集还是写密集。

查询优化上,作者给出一个二元的心智模型:一条查询要么通过索引或主键高效定位到行,要么退化成顺序扫描(sequential scan)。他建议把索引理解为「另一张为查找而优化的表」,B-tree 结构提供对数级查找复杂度。实践细节上:两万行以下的表顺序扫描基本可以忽略;复合索引的列顺序应与 ORDER BY 对齐;JOIN 应该走主键,否则说明 schema 设计需要复查;生产表建索引一定要用 CREATE INDEX CONCURRENTLY

写入侧的建议是:事务保持短小,绝不在事务中间调用外部服务;只更新真正需要更新的行以减少锁定范围;用批量写入(把多行打包进一次数据库往返,比如 SendBatch),作者称这带来了约 10 倍的吞吐提升。迁移策略上主张尽量做增量式(additive)迁移、不删列,任何会触发 ALTER TABLE 的操作都要审视是否阻塞写入——比如给大表加 check 约束,不带 NOT VALID 就会阻塞写。

进入中等规模后,作者点出三个坑。一是查询计划器依赖 ANALYZE 生成的统计信息,统计不准就会选错执行计划(好消息是 autovacuum 会顺带触发 ANALYZE)。二是连接管理:每个连接都消耗 CPU 和内存,建议用 pgbouncer 这类外部连接池,或 Go 的 pgxpool 这类进程内池,连接风暴会引发很隐蔽的内部锁问题。三是 autovacuum 调优,这是最关键的扩展瓶颈:死元组(dead tuple)在写入速度超过回收速度时不断堆积,危险信号包括 autovacuum 进程运行超过一小时、表体积快速膨胀,最坏情况是事务 ID 回卷(XID wraparound)导致长时间停机。

进阶技巧部分介绍了 FOR UPDATE SKIP LOCKED(用单条查询实现任务队列、以及跨实例的分布式租约)、表分区(按时间或 hash 切分,让 autovacuum 可以按分区独立运行,删除旧数据变成 drop partition 的瞬时操作,代价是读性能依赖分区裁剪),以及大表数据迁移的正确做法——不要放在单个大事务里(会阻塞 autovacuum 并阻止并发写),而是用触发器配合事务外的分批回填,靠唯一约束防止重复写入。

HN 评论精华