Slack的512宕机故障分析:负载均衡策略的失误
本文描述了 2020 年 5 月 12 日导致 Slack 宕机的技术细节,要想了解更多关于此故障背后的过程,请参阅 Ryan Katkov 的文章 All Hands on Deck (1)。
2020 年 5 月 12 日,Slack 发生了很长时间以来的一次重大故障。我们在事件发生后不久就发表了一份故障说明(2),但这是一个有意思的问题,我想更详细地介绍一下围绕它的一些技术问题。
用户可见的访问中断,开始于太平洋时间下午 4:45,但问题真正开始于当天早上 8:30 左右。我们的数据库可靠性工程团队接到警报,部分数据库基础架构出现了明显的负载增加,同时我们的流量团队也收到了一些 API 请求失败的报警。数据库负载的增加是由于一个配置变更引起,它触发了一个长期存在的性能 bug,该变更很快被定位并回滚,它是一个功能开关,执行了基于按用户百分比的发布,因此这是一个快速的过程。我们对客户产生了一些影响,但只持续了三分钟,在整个上午的短暂事件中,大多数用户仍然能够成功发送消息。
这次问题的背景之一,是我们的主 Web 应用层的规模最近以来在显著扩大。我们的 CEO Stewart Butterfield 也提到过由于隔离以及远程办公对 Slack 使用的影响。由于疫情的关系,我们在 webapp 层中运行的实例数量明显高于 2020 年 2 月之前的时候。当 worker 跑满,我们会自动快速扩容,但 worker 等待一些数据库请求所需的时间则要长得多,导致利用率更高。我们在事件中增加了 75% 的实例数,最后达到了我们迄今为止,运行的最高数量的 webapp 主机。
在接下来的 8 个小时里,一切似乎都很好,直到我们收到告警,服务的 HTTP 503 错误比正常情况下增多。我们启动了一个新的问题响应通道,webapp 层的 on-call 工程师手动扩大了 webapp 的规模,作为初步的缓解措施。不寻常的是,措施根本没有帮助。我们很快就发现,webapp 集群中的一个子集处于重载状态,而其他 webapp 实例却没有什么流量。我们开始了多线索的排查,调查 webapp 性能和我们的负载均衡器层。几分钟后,我们发现了问题所在。
我们在 4 层负载均衡器后面使用 HAProxy 实例的集群来分发请求到 webapp 层。我们使用 Consul 进行服务发现,并使用 consul-template 来呈现健康的 webapp 后端列表,HAProxy 应该将请求路由到后端。
图1:Slack 入口负载均衡架构的概述视图
然而,我们并没有将我们的 webapp 主机列表直接存放在 HAProxy 配置文件中。这样做的原因是,通过配置文件更新主机列表需要重新加载 HAProxy。重载 HAProxy 的过程包括创建一个全新的 HAProxy 进程,同时保留旧的进程,直到它处理完正在进行中的请求。非常频繁的 reload 可能会导致运行的 HAProxy 进程太多,性能不佳。这种约束与 webapp 层的自动扩容目标存在矛盾,即尽快让新实例投入服务。因此,我们使用 HAProxy 的 Runtime API 来操纵 HAProxy 服务器状态,而不需要在每次添加或删除 web 实例时进行 reload。值得注意的是,HAProxy 可以与 Consul 的 DNS 接口集成,但由于 DNS TTL 的问题,这样使用会增大变更的延迟,它限制了使用 Consul 标签的能力,而且管理非常大的 DNS 请求似乎经常会导致碰到痛苦的问题和一些 bug。
图2:如何在单个 Slack HAProxy 服务器上管理一组 webapp 后端
我们在 HAProxy 状态下定义了 HAProxy 服务器模板,这些模板实际上是我们的 webapp 后端可以占用的 "插槽"。当一个新的 webapp 实例被添加,或者一个旧的实例变得不健康时,Consul 服务目录会被更新。Consul-template 会生成一个新版本的主机列表,一个 Slack 开发的独立程序 haproxy-server-state-management 程序会读取该主机列表,并使用 HAProxy Runtime API 来更新 HAProxy 状态。
我们运行了 M 个 HAProxy 实例和 webapp 实例的并行池,每个池都在自己的 AWS 可用区。HAProxy 在每个可用区中为 webapp 后端配置了 N 个 "插槽",因此总共有 N*M 个后端可以在所有可用区中进行路由。几个月前,这个总数已经足够了,我们从来没有跑满,甚至是部署接近这个数量的 webapp 层的实例。然而,在上午的数据库事件之后,我们运行的 webapp 实例数量略高于 N*M。如果你把 HAProxy 插槽当成一个巨大的音乐椅游戏,这些 webapp 实例中的一些实例就没有座位了。这不是问题,我们有足够的服务能力。
图3:HAProxy进程中的 "插槽",还有一些剩余的 webapp 实例没有收到流量
然而,在一天的时间里,出现了一个问题。将 consul 模板生成的主机列表与 HAProxy 服务器状态同步的程序出现了一个错误。它总是试图为新的 webapp 实例寻找插槽,然后再释放被不再运行的旧 webapp 实例占用的插槽,但这个程序运行失败并提前退出了,因为它无法找到任何空槽,这意味着正在运行的 HAProxy 实例没有得到它们的状态更新。随着一天的时间过去,webapp 自动缩放组的规模不断扩大,HAProxy 状态的后端列表变得越来越陈旧。
到太平洋时间下午 4 点 45 分,大多数 HAProxy 实例只能向从早上开始就已经启动的 webapp 后端组发送请求,而这组老旧的 webapp 后端组现在已只包括集群的小范围机器。我们确实会定期提供新的 HAProxy 实例,因此也会有一些配置正确的新 HAProxy 实例,但它们中的大部分都已经超过了 8 小时,因此被卡在了完整而陈旧的服务器配置状态。停机最终是在美国的工作日结束时触发的,因为这时我们会随着流量的下降,开始缩减 webapp 层的规模。自动缩放会优先终止老旧实例,因此这意味着 HAProxy 服务器状态下,已经没有足够的老旧 webapp 实例剩余来满足需求。
图4:随着时间推移,HAProxy状态已经变得陈旧,仅指向一些旧的机器
当我们知道故障原因后,通过滚动重启 HAProxy 集群,很快就解决了这个问题。事件得到缓解后,我们问自己的第一个问题是,为什么我们的监控没有发现这个问题。我们针对这种精确的情况设置了报警,但不幸的是,它并没有发挥预期的作用。系统的运行异常没有被发现,部分原因是在不进行任何介入的前提下,系统可以顽强运行很长时间。故障机器所属的 HAProxy 部署也是相对静态的,由于变化率较低,参与其监控和报警基础设施的工程师较少。
我们没有在 HAProxy 技术栈上做任何重要工作的原因是,我们正在转向使用 Envoy Proxy 来进行所有的入口负载平衡(我们最近已经将 websocket 流量转移到 Envoy 上)。虽然 HAProxy 多年来一直为我们提供了良好可靠的服务,但它也有一些操作上的毛刺,正如这次事件所强调的那样。我们用来操作 HAProxy 服务器状态的复杂管道将被 Envoy 与 xDS 控制平面的原生集成所取代,以实现端点发现。HAProxy 的最新版本(自2.0版本以来)也解决了许多这些操作上的痛点。然而,Envoy 一直是我们内部服务网状项目的首选代理,这使得我们的入口负载平衡转移到 Envoy 上很有吸引力。我们对 Envoy + xDS 的大规模测试非常令人振奋,这次迁移应能提高未来的性能和可用性。我们新的负载平衡和服务发现架构不容易受到导致此次故障的问题影响。
我们努力保持 Slack 的可用性和可靠性,但是这一次,我们失败了。我们知道 Slack 是我们用户的重要工具,这就是为什么我们的目标是尽可能地从每个事件中学习,无论客户是否可见。我们对此次故障造成的不便表示歉意,并将继续利用所学到的经验,来推动我们系统和流程的改进。
文中链接:
https://medium.com/@solidspark/91d6986c3ee
https://status.slack.com/2020-05-12
英文原文:
https://slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack-dfe05b485f82
技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。
长按二维码 关注「高可用架构」公众号