零停机迁移在旧代码运行时就改表结构
一条语句就重写完繁忙大表的迁移,既耗时长又全程加锁。把它换成一系列小而向后兼容的改动,同一份表结构照样能往前推,而且不需要停机窗口——原因不是技巧,而是每一步都能单独部署、单独回退。 真正被管理的东西是一把锁。每一次结构变更都会取锁;问题只在于锁被拿多久,而答案取决于这条语句要碰多少数据。 先扩展,再回填,后收缩 三个阶段:扩展先加上新列,同时旧结构继续可用;回填负责把数据灌进去,而且必须分批;收缩只在没有任何代码再读旧结构之后,才把它请出去。 夹在扩展和回填之间的是双写阶段:应用会同时写新旧两列,这样回退不丢任何东西,等新列可信了再把读切过去。 旧代码还需要旧结构,于是顺序规则也就定了:加一个可空列是安全的,删列不安全,改名要拆成两步,NOT NULL 永远排在回填之后。在一张 50 万行的 sqlite 表上,ADD COLUMN note TEXT 耗时 2.0 ms,页数仍停在 13926——这是一次只改元数据的编辑。把它改成 NOT NULL 则必须真的重写:115.4 ms,页数从 13926 变成 28532。同一个结构变更,两种代价,区别只在于引擎要不要碰每一行。 分批比阶段顺序更重要。50 万行放在一个事务里回填,写锁被持有 671.2 ms;同样这些行按每批 2 万切分,共 25 个事务,锁持有时间中位数 7.3 ms、p90 是 7.8 ms。总工作量完全一样——不同的只有最长的那一次持锁,而那恰好就是应用真正感受到的数字。 锁才是全部问题 PostgreSQL 的 ACCESS EXCLUSIVE 与所有其他锁模式都冲突,包括一条裸 SELECT 取的 ACCESS SHARE。TRUNCATE、VACUUM FULL、不带 CONCURRENTLY 的 REFRESH MATERIALIZED VIEW,以及很多 ALTER TABLE 形式都会取它。读者和写者一起等。 ...