Consul流式传输引发Roblox停机事后分析


从 10 月 28 日开始并于 10 月 31 日完全解决,Roblox 经历了 73 小时的中断。¹每天有 5000 万玩家定期使用 Roblox,为了创造玩家期望的体验,我们的规模涉及数百个内部在线服务。
我们分享这些技术细节是为了让我们的社区了解问题的根本原因、我们如何解决它,以及我们正在采取哪些措施来防止未来发生类似问题。
中断在持续时间和复杂性上都是独一无二的。该团队必须依次解决一系列挑战,以了解根本原因并恢复服务。

  • 停电持续了 73 小时。
  • 根本原因是由于两个问题。在异常高的读写负载下在 Consul 上启用相对较新的流式传输功能会导致过度争用和性能下降。此外,我们特定的负载条件在 BoltDB 中引发了异常的性能问题。Consul 中使用开源 BoltDB 系统来管理用于领导选举和数据复制的预写日志。 
  • 支持多个工作负载的单个 Consul 集群加剧了这些问题的影响。
  • 在诊断这两个主要不相关的问题时,深埋在 Consul 实施中的挑战是导致停机时间延长的主要原因。 
  • 能够更好地了解中断原因的关键监控系统依赖于受影响的系统,例如 Consul。这种组合严重阻碍了分类过程。
  • 我们在将 Roblox 从扩展的完全关闭状态中恢复过来的方法上经过深思熟虑和谨慎,这也花费了相当长的时间。
  • 我们加快了工程设计工作,以改进我们的监控、消除可观察性堆栈中的循环依赖关系,以及加速我们的引导过程。 
  • 我们正在努力迁移到多个可用区和数据中心。
  • 我们正在修复 Consul 中导致此事件的根本原因的问题。

 
集群环境和 HashiStack
Roblox 的核心基础设施在 Roblox 数据中心运行。我们部署和管理我们自己的硬件,以及基于该硬件的我们自己的计算、存储和网络系统。我们的部署规模巨大,拥有超过 18,000 台服务器和 170,000 个容器。
为了在多个站点上运行数千台服务器,我们利用了一种通常称为“ HashiStack ”的技术套件。Nomad ConsulVault是我们用来管理世界各地的服务器和服务的技术,它们使我们能够编排支持 Roblox 服务的容器。
Nomad 用于安排工作。它决定哪些容器将在哪些节点上运行以及它们可在哪些端口上运行。它还验证容器运行状况。所有这些数据都被中继到一个服务注册中心,它是一个 IP:Port 组合的数据库。Roblox 服务使用服务注册来相互查找,以便进行通信。这个过程称为“服务发现”。我们使用Consul进行服务发现、健康检查、会话锁定(用于构建在顶部的 HA 系统)以及作为 KV 存储。
Consul 部署为具有两个角色的机器集群。“Voters”(5 台机器)权威持有集群的状态;“非投票者”(另外 5 台机器)是只读副本,有助于扩展读取请求。在任何给定时间,其中一个选民被集群选举为领导者。领导者负责将数据复制给其他投票者,并确定写入的数据是否已完全提交。Consul 使用一种称为Raft的算法进行领导者选举,并以确保集群中每个节点都同意更新的方式在集群中分配状态。领导者在一天内通过领导者选举多次改变的情况并不少见。
 
在 10 月事件发生前的几个月里,Roblox 从 Consul 1.9 升级到Consul 1.10,以利用新的流媒体功能。此流式传输功能旨在显着减少跨大型集群(如 Roblox 的集群)分发更新所需的 CPU 和网络带宽。
 
10 月 28 日下午,Vault 性能下降,单个 Consul 服务器 CPU 负载过高。Roblox 工程师开始调查。
 
初步调查表明,Vault 和许多其他服务所依赖的 Consul 集群运行状况不佳。具体来说,Consul 集群指标显示 Consul 存储数据的底层 KV 存储的写入延迟增加。这些操作的第 50 个百分位延迟通常低于 300 毫秒,但现在为 2 秒。硬件问题在 Roblox 的规模上并不少见,Consul 可以在硬件故障中幸存下来。但是,如果硬件只是缓慢而不是失败,它可能会影响 Consul 的整体性能。在这种情况下,团队怀疑硬件性能下降是根本原因,并开始更换 Consul 集群节点之一。这是我们第一次尝试诊断该事件
大约在这个时候,HashiCorp 的工作人员加入了 Roblox 工程师的行列,以帮助诊断和修复。从现在开始,所有对“团队”和“工程团队”的提及均指 Roblox 和 HashiCorp 员工。
 
即使有了新硬件,Consul 集群的性能仍然受到影响。16点35分,在线玩家数量下降到正常的50%。
 
这一下降恰逢系统健康状况显着下降,最终导致系统完全中断。为什么?当一个 Roblox 服务想要与另一个服务对话时,它依赖于 Consul 来获取它想要与之对话的服务的位置的最新信息。但是,如果 Consul 不健康,服务器将难以连接。此外,Nomad 和 Vault 依赖于 Consul,因此当 Consul 不健康时,系统无法调度新容器或检索用于身份验证的生产机密。简而言之,系统失败是因为 Consul 是单点故障,Consul 不健康。
在这一点上,团队提出了一个关于问题所在的新理论:流量增加。也许 Consul 很慢,因为我们的系统达到了临界点,而 Consul 运行的服务器无法再处理负载?这是我们第二次尝试诊断事件的根本原因。
 
鉴于事件的严重性,团队决定将 Consul 集群中的所有节点替换为新的、更强大的机器。这些新机器有 128 个内核(增加了 2 倍)和更新、更快的 NVME SSD 磁盘。到 19:00,团队将大部分集群迁移到新机器上,但集群仍然不健康。集群报告大多数节点无法跟上写入速度,KV 写入的 50% 延迟仍然在 2 秒左右,而不是典型的 300 毫秒或更短。
 
将 Consul 集群恢复到健康状态的前两次尝试均未成功。我们仍然可以看到增加的 KV 写入延迟以及一个我们无法解释的新的莫名其妙的症状:Consul 领导者经常与其他选民不同步。 
 
团队决定关闭整个 Consul 集群并使用几个小时前的快照重置其状态 - 中断的开始。我们知道这可能会导致少量系统配置数据丢失(而不是用户数据丢失)。鉴于中断的严重性以及我们相信如果需要我们可以手动恢复此系统配置数据,我们认为这是可以接受的。 
 
我们预计从系统健康时拍摄的快照恢复会使集群进入健康状态,但我们还有一个额外的担忧。尽管此时 Roblox 没有任何用户生成的流量流经系统,但内部 Roblox 服务仍然有效,并尽职尽责地联系 Consul以了解其依赖项的位置并更新其健康信息。这些读取和写入在集群上产生了很大的负载。我们担心即使集群重置成功,这种负载也会立即将集群推回不健康状态。为了解决这个问题,我们配置了iptables在集群上阻止访问。这将使我们能够以受控的方式恢复集群,并帮助我们了解我们在 Consul 上施加的独立于用户流量的负载是否是问题的一部分。
 
重置进展顺利,最初,指标看起来不错。当我们移除iptables块时,来自内部服务的服务发现和健康检查负载按预期返回。然而,Consul 的性能再次开始下降,最终我们回到了我们开始的地方:KV 写入操作的第 50 个百分位又回到了 2 秒。依赖于 Consul 的服务开始将自己标记为“不健康”,最终,系统又回到了现在熟悉的问题状态。现在是 04:00。很明显,我们的Consul 的负载造成了问题,在事件发生 14 多个小时后,我们仍然不知道它是什么。
 
我们已经排除了硬件故障。更快的硬件没有帮助,正如我们后来了解到的那样,可能会损害稳定性。重置 Consul 的内部状态也无济于事。没有用户流量进来,但 Consul 仍然很慢。我们利用iptables让流量缓慢返回集群。集群是否只是被成千上万个试图重新连接的容器推回到不健康状态?这是我们第三次尝试诊断事件的根本原因
 

工程团队决定减少 Consul 的使用,然后仔细系统地重新引入它。为了确保我们有一个干净的起点,我们还阻止了剩余的外部流量。我们收集了一份详尽的使用 Consul 的服务列表,并推出了配置更改以禁用所有非必要的使用。由于目标系统和配置更改类型繁多,此过程需要几个小时。通常运行数百个实例的 Roblox 服务被缩减到个位数。健康检查频率从 60 秒减少到 10 分钟,为集群提供了额外的喘息空间。10 月 29 日 16:00,在中断开始 24 小时后,团队开始第二次尝试让 Roblox 重新上线。再一次,这次重启尝试的初始阶段看起来不错,但只持续到 10 月 30 日 02:00 。
在这一点上,很明显,Consul 的整体使用并不是我们在 28 日首次注意到的性能下降的唯一因素。鉴于这一认识,团队再次转向。团队没有从依赖于它的 Roblox 服务的角度来看待 Consul,而是开始从 Consul 内部寻找线索。
 
在接下来的 10 个小时里,工程团队深入挖掘了调试日志和操作系统级别的指标。该数据显示 Consul KV 写入被长时间阻塞。换句话说,“争用”。争用的原因并不是很明显,但有一种理论认为,在中断早期从 64 个 CPU 核心服务器转移到 128 个 CPU 核心服务器可能会使问题变得更糟。团队得出结论,值得回到与中断前使用的类似的 64 台核心服务器。团队开始准备硬件:安装了 Consul,对操作系统配置进行了三次检查,并且机器以尽可能详细的方式准备好服务。然后团队将 Consul 集群转换回 64 CPU Core 服务器,但这种改变并没有帮助。这是我们第四次尝试诊断事件的根本原因。
  
找到根本原因 (10/30 12:00 – 10/30 20:00)
几个月前,我们在部分服务上启用了新的 Consul 流式传输功能。此功能旨在降低 Consul 集群的 CPU 使用率和网络带宽,按预期工作,因此在接下来的几个月中,我们逐步在更多后端服务上启用了该功能。10 月 27 日 14:00,即中断前一天,我们在负责流量路由的后端服务上启用了此功能。
通过对来自 Consul 服务器的性能报告和火焰图的分析,我们看到了流式代码路径对导致 CPU 使用率高的争用负责的证据。我们禁用了所有 Consul 系统的流式传输功能,包括流量路由节点。配置更改在 15:51 完成传播,此时 Consul KV 写入的第 50 个百分位降低到 300 毫秒。我们终于有了突破。
 
为什么流式传输是一个问题?
HashiCorp 解释说,虽然流式传输总体上更有效,但与长轮询相比,它在实现中使用的并发控制元素(Go 通道)更少。
在非常高的负载下——特别是非常高的读取负载和非常高的写入负载——流的设计会加剧单个 Go 通道上的争用量,这会导致写入过程中出现阻塞,从而显着降低效率。
这种行为也解释了更高核心数服务器的影响:这些服务器是具有 NUMA 内存模型的双插槽架构。因此,在这种架构下,共享资源的额外争用变得更加严重。通过关闭流式传输,我们显着改善了 Consul 集群的健康状况。
 
尽管取得了突破,但我们还没有走出困境。我们看到 Consul 间歇性地选举新的集群领导者,这是正常的,但我们也看到一些领导者表现出与我们在禁用流式传输之前看到的相同的延迟问题,这是不正常的。在没有任何明显线索指向缓慢领导者问题的根本原因的情况下,并且有证据表明只要某些服务器没有被选为领导者,集群是健康的,团队做出了务实的决定,通过防止出现问题来解决问题领导者留在选民。这使团队能够专注于将依赖 Consul 的 Roblox 服务恢复到健康状态。
 
 HashiCorp 的工程师在中断后的几天内确定了根本原因。Consul 使用一个流行的开源持久性库 BoltDB 来存储 Raft 日志。它用于存储 Consul 中的当前状态,而是用于存储正在应用的操作的滚动日志。为了防止 BoltDB 无限增长,Consul 会定期执行快照。快照操作将 Consul 的当前状态写入磁盘,然后从 BoltDB 中删除最旧的日志条目。 
但是,由于 BoltDB 的设计,即使删除了最旧的日志条目,BoltDB 在磁盘上使用的空间也不会缩小。
相反,所有用于存储已删除数据的页面(文件中的 4kb 段)都被标记为“空闲”并重新用于后续写入。BoltDB 在称为“freelist”的结构中跟踪这些空闲页面。通常,更新 freelist 所需的时间不会显着影响写入延迟,但 Roblox 的工作负载暴露了 BoltDB 中的一个病态性能问题,这使得 freelist 维护非常昂贵。 
 
恢复缓存服务(10/30 20:00 – 10/31 05:00)
Roblox 为其后端使用典型的微服务模式。微服务“堆栈”的底部是数据库和缓存。这些数据库没有受到中断的影响,但缓存系统在正常系统运行期间定期处理其多个层每秒 1B 的请求,但运行状况不佳。由于我们的缓存存储可以轻松从底层数据库重新填充的瞬态数据,因此使缓存系统恢复健康状态的最简单方法是重新部署它。
缓存重新部署过程遇到了一系列问题: 
  1. 可能是由于之前执行的 Consul 集群快照重置,缓存系统存储在 Consul KV 中的内部调度数据不正确。 
  2. 小型缓存的部署花费的时间比预期的要长,而且大型缓存的部署还没有完成。事实证明,有一个不健康的节点,作业调度程序将其视为完全打开而不是不健康。这导致作业调度程序尝试在此节点上积极调度缓存作业,但由于节点不健康而失败。 
  3. 缓存系统的自动部署工具旨在支持对已经在大规模处理流量的大规模部署进行增量调整,而不是从头开始引导大型集群的迭代尝试。 

该团队通宵工作以识别和解决这些问题,确保正确部署缓存系统并验证正确性。10 月 31 日 05:00,在中断开始 61 小时后,我们拥有了一个健康的 Consul 集群和一个健康的缓存系统。我们已准备好提出 Roblox 的其余部分。