昨天,备受瞩目的保罗对阵泰森比赛期间,Netflix 平台却意外遭遇大规模瘫痪。
美国东部时间周五晚上8 点开始,现年 58 岁的泰森迎战 27 岁的网红保罗,这是这位前重量级冠军这 19 年来首次参加职业比赛,除了 7 万名现场观众,该赛事还对 Netflix 流媒体平台的订阅用户免费开放。
在当今数字媒体时代,体育赛事直播已成为吸引流量的热门领域。作为鲜少涉足直播的 Netflix,此次承办今年规模最大的赛事之一,原本备受期待,却在直播过程中频繁出现缓冲和错误提示。中断问题让全球观众倍感失望。Downdetector.com 记录了 13,895 份中断报告,大多数投诉(86%)与视频流问题有关,而 10% 的用户遇到服务器连接问题,4% 的用户遇到登录问题。
有观众吐槽说,“如果 Netflix 不解决这个缓冲问题,这将成为电视 / 流媒体历史上最大的失败之一。”
由于 Netflix 官方未对中断问题作出任何回应,用户对于故障原因一无所知(截至写稿前)。许多人推测问题可能是由巨大的流量涌入导致的。对于如此规模的赛事来说,稳定的直播服务至关重要,而频繁的中断让许多用户开始质疑平台应对高流量直播的能力。
值得注意的是,作为最早将业务“全量”迁移上云的企业之一,Netflix 一直以来都是云厂商宣传的“经典参考范本”。他们从 2008 年开始,大胆地采用了一系列前沿技术,如微服务、DevOps 和混沌工程,将庞大的单体应用拆分成灵活的微服务,并部署在云端。
在 Netflix 上云成功之前,几乎没有人相信大型企业真的可以将全部服务全部运行在云端。Netflix 的成功不仅证明了大型企业全上云的可行性,更引领了整个行业向云计算的迁移。
然而,令人意外的是,十几年后,Netflix 却连一次直播都搞不定了。考虑到 Netflix 在云计算领域的深厚积累和技术实力,这样的问题不禁让人感到困惑。
即便是“全量上云”的 Netflix,在面对突发性的高并发场景时,也并非毫无弱点。
恰好这两天 Hacker News 上正在热议一起 Netflix“并发”生产事故。作者 Matthew Hawthorne 在 2017 年前任职于 Netflix,他讲述的这起案例,故障同样也是发生在周五下午,他们解决并发问题的方式并不是先进行相应的扩容,Netflix 的工程师不想加班,也不想周末去手动去重启机器,所以他们写了个自动随机终止实例的程序,等到周一,再由客户端团队部署修复补丁,重新启用自动缩放功能。
而且他也以该解决方案的实用性自豪,也同时庆幸这个方案让他们“度过了一个轻松的周末”......
有网友打趣说,“我认为这正是云计算胜出的最佳例证之一。你再也不需要团队中有‘技术大拿’了。实例出现问题?直接销毁它,然后启动一个新的。让亚马逊团队来解决系统调试的问题吧。”
另一位网友附议道:“设计一个只会崩溃的系统是一回事,而设计一个经常崩溃但之后通过云编排层掩盖问题的系统则是另一回事。”
也有一些持明确的反对意见的人,认为不能放任问题不管,任由技术债务累积。一位曾在创业公司担任工程总监的朋友曾分享过他们的做法,“我们公司有一条all hands on deck的铁律,一旦发现并发 bug,就立即全体人员参与、全力以赴处理掉它。你绝对不希望让这些问题继续存在。它们是非确定性的,发生频率低,但随着更多类似问题的出现,如果通过临时措施把它们掩盖,问题将变得越来越危险。”
......
虽然如此,也存在一些支持 Matthew 做法的人,大家争论热烈,所以我们也贴上了他的原文:
大家好,今天我想和各位聊聊我在 Netflix 工作期间遇上的最大一起生产事故。
大致情况是:
一个并发 bug 正在吞噬我们的 CPU;
当时是星期五下午;
回滚很麻烦;
我们直到星期一才能真正解决问题。
面对这样的情况,我们该如何避险求生?
在 Netflix 工作那会,我们正努力将 Netflix API 从 REST 式重新设计为 RPC 式。当时的总体思路,就是让客户端团队将自己编写的端点脚本部署在服务器上,从而实现更灵活、更高效的交互体验。
我这里指的“客户端团队”,是指负责在移动设备、游戏主机、电视等平台上构建 Netflix 应用程序的团队。
当时我在博文中引用了下面这张图片:
除了参加过几场架构讨论会还有在走廊的临时协商里跟着点头之外,我并没有正式参与这个项目。
那是一个星期五的下午,我突然听到外面传来一阵骚动。我从自己的小隔间望出去,发现同事们正在热烈讨论一个问题:我们整个集群的 CPU 使用率正在缓慢增长。
在查看了一大堆图表和 JVM 线程转储之后,我们发现 CPU 使用率之所以持续增长,是因为集群上所运行客户端脚本使用的内部库中存在一个并发 bug。
在底层,这些脚本使用的是 HashMap 而非 ConcurrentHashMap。我还依稀记得,对 HashMap.get() 的某些调用似乎在无限运行。无限运行的代码,意味着该代码的运行会彻底消耗掉一切 CPU 资源,直到我们手动终止该进程。换句话说:我们正逐渐一个个失去所有 CPU。
事件的影响虽然推进缓慢但却相当严重。下面来看当时的统计数字:
我们每 2 分钟损失一块 CPU,也就是每小时损失 30 块 CPU;
每个实例上安装有 8 块 CPU,因此我们每小时损失约 4 个实例的容量;
每 24 个小时,我们会损失掉约 96 个实例的容量。
此外:
如果我们每天通过自动扩展保证低谷时有 500 个实例,而高峰时有 1000 个实例,那么……
在 24 个小时之后,我们大约会损失掉:
(请注意,我们对低谷容量损失的预测可能不准确。后文将在图表中具体介绍。)
约 10% 的峰值容量(96/1000);
约 20% 的低谷容量(96/500);
所以我们必须在当天结束之前出手挽救。当时我们反复考虑了可行的选择:
负责的客户端团队能修复这个 bug 吗?
可以,但他们需要一些时间,大概要到周一才能准备好。
我们能不能回滚?
很难,而且我不记得具体原因。
我们能不能以某种方式检测问题,并重新启动相关服务器?
没那么简单。
一层阴云笼罩在我们的头顶。
我在很多公司都工作过,其中不少都会在这样的重压面前导致工程师心理崩溃。换句话说,某些企业的文化能够让工程师在这种情况下心无旁骛、轻松上阵,但有些企业的文化则会将这种心理压力放大到极限。而这往往成为决定补救计划成败的分水岭。
我在类似的情况下曾经见到过以下种种可以理解、但却效果有限的次优反应:
经理 A 向经理 B 施压,要求他们的团队整个周末疯狂加班,直到将 bug 修复。
要求值班工程师整个周末不断手动重启服务器。
也许会由两支值守团队轮流工作,这样工程师们至少还有时间吃饭和睡觉。
如果值班人员的工作量过大,也可以组建一支小型“工作组”来执行手动操作,直到问题被解决。
在整个值守期间,大约每 4 个小时召开一场电话会议,让所有相关方了解最新动态。
这些办法都不怎么样,但我承认,在某些情况下公司可能也只有这些办法可用。
至于在 Netflix,必须承认在整个供职期间,我始终觉得这里一切以实用主义为先。举例来说:我在文章开头提到的重新设计 API 当时就是个有点疯狂的想法。乍看之下,放弃掉从 2011 年沿用至今的 REST 式 API 根本没有道理。但如果仔细想想,就会意识到这个点子是搞定当时最大问题的完美解决方案。
大家当然可以为实用主义工程思维列举出各种定义,但我自己一直这样理解:设定明确的目标,并做出与目标相统一的选择。
那么从实用主义角度考虑,我们当时想要让 CPU 达到怎样的状态?当然就是修复 bug。但那是星期五,而修复 bug 必须等到星期一。
那么可行的次优目标又是什么?
理想的次优解决方案应该是进行集群维护自动化,这样我们就能正常享受周末而无需人为介入。
相信这也是每一个被半夜惊醒过的工程师们的梦想:建立一套自我修复系统。
下面来看我们是怎么做的:
我们将集群大小固定为自动缩放范围内的最大值。换句话说,我们关闭了自动缩放功能,并调整了集群大小以维持最大容量。
我们在中央监控和警报系统中创建了一条规则,即每 15 分钟随机终止几个实例。每个被关闭的实例都将被一个健康的、新建的实例所取代。
为什么不直接重启?因为终止操作的速度更快。
工作效果相当不错,我们也度过了一个轻松的周末。因为我们在集群上虽然看到很多 CPU 被榨干,但同时也有不少 CPU 及时顶上,维持着系统的稳定运行。
部署完成后,我尝试使用自己构建的图表库来重现当时的情形。这套库能帮助我根据运行状态生成合成数据。下面我们一起来看结果:
我使用的是坏实例(即本文中提到的服务器、虚拟机或者容器)、而非 CPU 进行模拟,因为这样更容易理解。以下图表所示为:
使用线性自动缩放方案;
低谷时间为凌晨 4 点;
高峰时间为晚间 8 点;
最小和最大集群规模分别为 100 和 1000;
每个实例每小时有 1% 的故障概率。
这看起来跟我的预期似乎相去甚远。因为在我的印象里,当时我们更关注的是低谷期间、而非高峰期间的容量下降。但根据此图表,随着我们缩小集群规模,好实例和坏实例的数量也会等比例削减。因此更大的问题反而出现在了高峰期,毕竟这时候我们的坏实例数量最多,而且自上一轮低谷期以来在持续增加。
这是同一时期内的容量示意图(「预期容量得分」= 好实例数除以总实例数量):
统计时间段从低谷期开始,可以看到这时候我们的容量接近 100%,而在峰值期则按预计下降到 90% 左右。虽然 90% 听起来似乎不错,但正常来讲如果实际可用容量只有预期容量的 90%,同样是个不小的问题。
我还模拟了一下当时的解决方案,即将我们的集群固定为最大实例数、禁用自动缩放,并以一定的节奏自动终止实例。测试之后我发现:
每小时实例失败的概率仍为 1%;
我们每小时会随机终止集群容量中的 5%。
可以看到随着时间推移,我们的健康实例数量收敛到了约 800 个。
以下是同时段内的容量图:
容量同样收敛到 80% 左右,这并不理想,但我们可以将集群固定到高于正常最大值的水平以维持符合目标期望的健康实例数量。
插入不同的故障率和实例终止率数值并观察效果的确非常有趣,但受篇幅所限,这里我们就不做更多延续和讨论了。
周一终于到来,客户端团队部署了他们的修复补丁。我们则禁用了自动终止规则,重新启用了自动缩放功能,之后一切回归平静。
我一直对这件事记忆犹新,主要是因为:
这是个罕见但残酷的例子,说明编写非线程安全代码会给系统造成多么严重的破坏。很多问题大家之所以没见过,只是因为各位所使用的系统不具备引发这类问题的庞大规模。
自动终止随机实例的办法听起来似乎是种糟糕的工程实践,但在当时,却成为解决我们问题的完美方案。
最重要的是,我们始终在以理性方式考虑问题。
事故的表现形式多种多样,而我们的解决方案也必须体现出这种多样性,才能确保在实践中发挥最大作用。
我接触过的最强团队都会使用各种不寻常的手段,我相信这绝对不是巧合。而这就是技术发展成熟的标志:有勇气、有信心而且目标明确,敢于运用非常规或者次优性质的方法追求目标。
事实证明,工程原理能够实现的远不止于优化延迟、吞吐量或者正常运行时间之类的技术指标。我们完全可以借助同样的理论武器,建立起优化生活质量的切实方案。而这,也让文中案例成为始终萦绕在我脑海当中的珍贵回忆。
参考链接:
https://x.com/WhoDybala_/status/1857645829382615468
https://news.ycombinator.com/item?id=42087275
https://pushtoprod.substack.com/p/netflix-terrifying-concurrency-bug