2023年3月14日下午2点17分,Spotify的支付系统开始返回502错误。不是某个边缘功能,是用户掏钱买会员的那个按钮——按下去,没反应。
工程师最初以为是CDN缓存问题。15分钟后,他们意识到事情大了:主数据库集群被完全锁定,只读副本同步中断,全球付费流程瘫痪。从发现到完全恢复,4小时17分钟。直接收入损失?Spotify财报里那季度的"服务中断准备金"多了2700万美元。
迁移方案看起来没毛病
出事前一周,基础设施团队刚做完风险评估。他们要做的"只是"把用户订阅表从PostgreSQL 11迁移到14,同时切换分区策略。方案用了逻辑复制(logical replication),号称"零停机"。
技术负责人Maria Chen在内部复盘会上说:「我们测试了三次,每次都在staging环境跑通。生产环境数据量是staging的180倍,这个变量被标记为'已知风险'。」
问题在于,他们测试的是迁移流程本身,不是迁移期间的并发负载。
生产环境的支付高峰和迁移脚本撞在了一起——就像在高速公路上换轮胎,以为车道够宽,没算对向来车。
锁链反应:一个超时怎么拖垮全场
根因事后被压缩成一句话:分区表上的外键约束触发了级联锁。
具体点说。迁移脚本创建新分区表时,需要短暂获取订阅表的访问排他锁(AccessExclusiveLock)。正常情况下这个锁持有一两百毫秒,用户无感知。但当天,一个遗留的ORM查询(对象关系映射查询)正在遍历整张表做月度报表,已经跑了8分钟。
新锁请求进入等待队列。支付请求继续涌入,连接池耗尽。更糟的是,只读副本为了同步逻辑复制槽,也卡在了同一个锁上。Spotify的监控仪表盘上,活跃连接数从通常的400飙到8000,然后触发了连接风暴。
DBA团队尝试了常规手段:终止长查询、提升锁超时、甚至准备强制提升副本。但PostgreSQL的锁等待图已经变成了一张死结——杀掉任何一个等待者,都有50%概率触发事务回滚雪崩。
「我们盯着pg_locks表看了20分钟,」参与应急的工程师后来写道,「每一行都是一条人命,不敢乱动。」
恢复决策:为什么选了最慢的那条路
2小时47分时,团队面临两个选项。
A方案:回滚迁移,恢复到旧集群。预计1小时,但过去2小时的新支付数据可能丢失——需要人工核对订单状态。
B方案:硬等长查询结束,然后加速完成迁移。预计90分钟,但如果查询再有异常,时间无限延长。
他们选了B。不是因为更优,而是因为A方案的数据核对在财务合规上风险更高。Spotify的审计日志要求支付流水可追溯,"可能丢失"四个字会让法务部门直接掀桌。
这个决策后来引发内部激烈争论。基础设施VP在邮件里写:「我们用业务连续性赌技术修复,赌赢了是运气,赌输了是灾难。」
最终,那个报表查询在3小时12分时自然结束。迁移脚本完成剩余步骤,只读副本重新同步。4小时17分后,支付按钮恢复响应。但高峰期已过,当天会员转化率和前一周同期相比掉了19%。
事后:他们改了什么,没改什么
Spotify公开了这份复盘——在工程博客上,不是新闻稿。他们列了17项改进,但有三条被标为"立即执行":
第一,所有生产迁移必须模拟真实流量峰值,不是"数据量对等",是"查询模式对等"。他们用了一个月重放生产查询日志,发现staging漏掉了17%的慢查询类型。
第二,分区表操作引入"锁超时熔断"——不是等锁,是主动检测冲突并暂停迁移。这个机制后来成了PostgreSQL社区的一个补丁讨论。
第三,也是最微妙的:他们保留了那份"遗留ORM查询"。没重写,只是加了执行时间告警和只读路由。「有些技术债不是不还,是算好利息分期,」Maria Chen说,「这次我们算错了利率。」
有个细节没写进官方复盘。恢复后的第二天,那个跑月度报表的工程师收到了一封匿名邮件,正文只有一个链接——指向Spotify新发布的"数据库迁移最佳实践"文档。发件人地址是infra-team@spotify.com,但团队里没人承认发过。
如果4小时的锁是你职业生涯的硬上限,你会在方案评审时砍掉哪些"看起来没问题"的假设?
热门跟贴