我对 GitLab 的 Postgres 模式设计的笔记(2022)

GitLab 的 PostgreSQL 模式选择引发了对现代应用如何在大规模下建模数据的广泛批评。评论者探讨了 32 位与 64 位主键、UUID 与顺序 ID 之间的权衡,以及在线模式迁移策略,强调索引局部性、外键和 ORM 抽象如何决定十亿级行表的性能成败。该讨论还质疑了隐藏主键等常见“最佳实践”,认为这些往往更多反映业务与竞争考量,而非安全;同时强调,即便工具不断进步,审慎、以数据库为中心的模式设计仍然至关重要。

主键大小、迁移与工具

  • 许多评论都在争论 int 和 bigint 主键。有人认为,对于大型服务来说,触及 32 位上限是真实风险,已经“近得让人不安”;也有人指出,真实数据(例如 GitHub 的仓库/问题)仍然低于 2^31。
  • 将 int→bigint 迁移被描述为可行,但在大规模下并不简单:表重写、索引重建、外键,以及单线程的索引创建,在多 TB 表上可能需要数小时。
  • 提到的零/低停机方案包括:带切换的逻辑复制、在线模式变更工具(pg-osc、gh-ost)、将模式迁移与应用部署解耦,以及由 DBA 运行的迁移系统。
  • 还有人提到一些次生痛点:JavaScript bigint 反序列化、下游消费者默认使用 32 位整数。

UUID 与顺序整数

  • 作为主键时,UUIDv4 主要被批评的是性能,而不是大小。每个外键/索引条目多出的 8 字节会在许多表之间累积,并使索引更难放进 RAM。
  • 随机 UUID 会破坏索引局部性:btree 插入变得分散,引发页膨胀,并在索引超过内存后导致严重的性能断崖,而不只是稳定的约 25% 性能损失。
  • 讨论中提到了时间有序 ID(UUIDv6/v7、Snowflake 风格、加密或置换后的序列)作为更好的折中,但它们也有权衡(时间泄露、密钥轮换、复杂性)。

模式设计、ORM 与迁移

  • 几位评论者认为模式设计已经是“石器时代”,而迁移工具(EF、Rails、Django、Prisma 等)要么生成危险的迁移(锁、重写),要么掩盖实际执行的内容。
  • 当前有强烈倾向支持 schema-first 思路和纯 SQL(通常配合存储过程),而不是重型 ORM;后者被认为存在泄漏性、复杂且在大规模下难以运维。
  • 也有人为 ORM 辩护,认为如果理解其行为,它们对动态查询和复杂对象图很有用。

GitLab 与 GitHub 的架构与性能

  • 一些人觉得 GitLab 页面明显比 GitHub 慢。给出的解释包括文化差异以及对性能优先级的不同,但细节多为轶闻且不完整。
  • 对于 GitLab.com 是单个多租户数据库还是按客户分库,存在分歧/不确定性;有评论声称它本质上是其自托管产品的一个多租户实例。

暴露主键与“外部 ID”

  • 一派认为隐藏顺序主键大多只是“安全戏法”;如果授权已经失守,猜测到的 ID 本身并不重要。
  • 另一派强调纵深防御与竞争情报:顺序 ID 会暴露数量/增长(订单、用户、问题),并使批量枚举和利用更容易。
  • 例子包括电商订单量、用户枚举和爬取;反驳则认为有动机的攻击者通常仍能推断出类似数据。
  • 类似 GitLab 的内部 id 加上按项目分配的 iid 被认为对用户更友好,并且将 URL 与内部主键变更解耦,代价是额外的连接和索引。

Postgres 细节:text vs varchar、外键

  • 讨论澄清,在 Postgres 中,textvarchar(n) 在运行时性能上没有差异;真正的问题是,当改变 varchar(n) 的长度时迁移成本更高,而 text 只需要调整 CHECK
  • 一位评论者反驳了“外键很昂贵”的观点,认为完整性必须在某处得到强制;如果使用得当,数据库级外键通常更优。