这篇文章描述了2020年5月12日导致Slake停机的问题的技术细节。要更多地了解相同停电事件响应背后的流程,请阅读Ryan Katkov的帖子“All Hands on Deck”。
2020年5月12日,Slake发生了我们很长一段时间以来的第一次重大停机。不久之后,我们发表了这起事件的摘要,但这是一个有趣的故事,我们想更详细地了解围绕它的技术问题。
用户可见的停机开始于太平洋时间下午4:45,但故事实际上是从那天早上8:30左右开始的。我们的数据库可靠性工程团队收到警报,发现我们的部分数据库基础架构的负载大幅增加,同时我们的流量团队收到一些API请求失败的警报。数据库负载的增加是由于配置更改的推出,这触发了一个长期存在的性能错误。更改很快被精确定位并回滚-这是一个执行基于百分比的推出的功能标志,因此这是一个快速的过程。我们对客户产生了一些影响,但它只持续了三分钟,在这一短暂的上午事件中,大多数用户仍然能够成功发送消息。
这一事件的影响之一是我们的主要WebApp层得到了显著的扩展。我们的首席执行官斯图尔特·巴特菲尔德(Stewart Butterfield)写过关于封锁和在家订单对Slake使用的一些影响。由于这场大流行,我们在WebApp层运行的实例数量比很久以前的2020年2月要高得多。当工作人员饱和时,我们会快速自动扩展,就像这里发生的情况一样-但工作人员等待某些数据库请求完成的时间要长得多,从而导致更高的利用率。在事件发生期间,我们的实例数量增加了75%,最终获得了迄今为止运行过的最高数量的WebApp主机。
在接下来的8小时内,一切似乎都很正常-直到我们收到警报,我们正在处理比正常情况下更多的HTTP 503错误。我们启动了一个新的事件响应渠道,WebApp层的待命工程师手动扩大了WebApp的规模,作为最初的缓解措施。不同寻常的是,这完全没有帮助。我们很快就注意到,WebApp机群的一部分负载很重,而其余的WebApp实例则没有。开始了多方面的调查,调查了WebApp的性能和我们的负载均衡器层。几分钟后,我们发现了问题所在。
我们在第4层负载均衡器后面使用一组HAProxy实例将请求分发到WebApp层。我们使用Consul进行服务发现,并使用consul-template呈现HAProxy应该将请求路由到的健康的WebApp后端列表。
但是,我们不会将WebApp主机列表直接呈现到HAProxy配置文件中。原因是通过配置文件更新主机列表需要重新加载HAProxy。重新加载HAProxy的过程包括创建一个全新的HAProxy进程,同时保留旧进程,直到完成处理运行中的请求。非常频繁的重新加载可能会导致运行的HAProxy进程过多,性能不佳。这一限制与自动缩放WebApp层的目标(即尽快将新实例投入服务)相冲突。因此,我们使用HAProxy的Runtime API来操作HAProxy服务器状态,而无需在每次Web层后端进入或退出服务时进行重新加载。值得注意的是,HAProxy可以与Consul的DNS接口集成,但由于DNS TTL,这增加了延迟,它限制了Consul标记的使用能力,而且管理非常大的DNS响应似乎经常会导致痛苦的边缘案例和错误。
我们在HAProxy状态下定义HAProxy服务器模板,这些模板实际上是我们的WebApp后端可以占用的“插槽”。当提供新的WebApp实例或旧的WebApp实例变得不健康时,会更新领事服务目录。consul-template呈现主机列表的新版本,在Slake开发的单独程序haproxy-server-state-management读取该主机列表并使用HAProxy Runtime API更新HAProxy状态。
我们运行由HAProxy实例和WebApp实例组成的M个并行池,每个池位于其自己的AWS可用区中。HAProxy为每个AZ中的WebApp后端配置了N个“插槽”,提供了总共N*M个可以跨所有AZ路由的后端。几个月前,这个总数超过了足够的净空空间--我们从来不需要运行任何东西,甚至不需要运行任何接近我们的webapp层的实例数量的东西。然而,在早上的数据库事件之后,我们运行的WebApp实例略多于N*M个。如果你认为HAProxy老虎机是一个巨大的音乐椅游戏,那么其中一些Webapp实例就没有座位了。这不是问题,我们有足够的服务能力。
然而,在一天的过程中,一个问题出现了。将领事模板生成的主机列表与HAProxy服务器状态同步的程序有错误。在释放不再运行的旧WebApp实例占用的插槽之前,它总是尝试为新的WebApp实例找到一个插槽。该程序开始失败并提前退出,因为它找不到任何空插槽,这意味着正在运行的HAProxy实例没有更新其状态。随着时间的推移,webapp自动伸缩组的规模不断扩大,HAProxy状态下的后端列表变得越来越陈旧。
到太平洋时间下午4:45,大多数HAProxy实例只能向从早上开始运行的一组WebApp后端发送请求,而这组较旧的WebApp后端现在只是车队中的一小部分。我们确实会定期提供新的HAProxy实例,因此会有一些新的HAProxy实例具有正确的配置,但它们中的大多数都超过了8小时,因此处于完全和陈旧的后端状态。这次中断最终是在美国工作日结束时触发的,因为那时我们开始缩减WebApp层的规模,因为流量下降了。自动缩放将优先终止较旧的实例,因此这意味着不再有足够的较旧的WebApp实例处于HAProxy服务器状态来满足需求。
一旦我们知道了故障的原因,通过轮流重启HAProxy机队,问题很快就解决了。在事件缓和之后,我们问自己的第一个问题是,为什么我们的监控没有发现这个问题。我们已经对这种准确的情况发出了警报,但不幸的是,它并没有像预期的那样工作。坏掉的监控没有被注意到,部分原因是这个系统“刚刚工作”了很长一段时间,不需要任何改变。更广泛的HAProxy部署(这是其中的一部分)也是相对静态的。由于更改率较低,与监控和警报基础设施交互的工程师较少。
我们没有在这个HAProxy堆栈上做任何重要工作的原因是,我们正在为所有入口负载平衡转向特使代理(我们最近已经将WebSockets流量移到了特使上)。虽然HAProxy多年来一直为我们提供良好和可靠的服务,但它也有一些操作上的锋利,这正是这次事件所突显的那种。我们用来操作HAProxy服务器状态的复杂管道将被特使与用于端点发现的XDS控制平面的本地集成所取代。HAProxy的最新版本(从2.0版开始)也解决了许多这些操作痛点。然而,特使是我们内部服务网络项目的首选代理已经有一段时间了,这使得我们的入口负载平衡选择特使很有吸引力。我们最初对特使+XDS进行的大规模测试非常令人兴奋,这次迁移应该会在未来提高性能和可用性。我们的新负载平衡和服务发现体系结构不容易受到导致此次停机的问题的影响。
我们努力让Slake保持可用和可靠,但在这种情况下,我们失败了。我们知道Slake对我们的用户来说是一个重要的工具,这就是为什么我们的目标是从每一次事件中尽可能多地学习,无论客户是否可见。我们对此次停机给您带来的不便深表歉意,并将继续利用学到的经验教训来推动我们的系统和流程的改进。