Astra动态分块技术:如何通过重新设计关键组件实现成本优化

本文详细介绍了Slack工程团队如何重新设计Astra日志搜索引擎的关键组件,通过实现动态分块技术将缓存节点成本降低20%,并深入探讨了系统架构优化、算法实现和部署策略。

Astra动态分块:我们如何通过重新设计关键组件实现成本节约

引言

Slack处理大量日志数据。实际上,我们每秒处理超过600万条日志消息,相当于每秒超过10GB的数据!所有这些数据都使用我们内部开源日志搜索引擎Astra进行存储。为了使这些数据可搜索,Astra按时间对数据进行分组,并将其分割成我们称为"分块"的数据块。

最初,我们构建Astra时假设所有分块大小相同。然而,这个假设导致了未使用磁盘空间的低效率,并为我们的基础设施带来了额外支出。

我们决定解决这个问题,以降低运营Astra的成本。

固定大小分块的问题

固定大小分块的最大问题是并非所有分块都被充分利用,导致分块大小不一。虽然假设固定大小的分块简化了代码,但也导致我们在缓存节点上分配了比所需更多的空间,造成了不必要的支出。

以前,每个缓存节点都被分配固定数量的槽位,每个槽位将被分配一个分块。虽然这简化了代码,但意味着数据量不足的分块将分配比所需更多的空间。

例如,在一个3TB的缓存节点上,我们会有200个槽位,每个槽位预期容纳15GB的分块。然而,如果有任何分块大小不足(比如10GB而不是15GB),这将导致分配了额外空间(5GB)但未被使用。在有数千个分块的集群中,这很快导致相当大比例的空间被分配但未被使用。

固定大小分块的另一个问题是,有些分块实际上比我们假设的大小更大。每当Astra创建恢复任务以处理旧数据时,这种情况可能发生。我们根据落后的消息数量而不是落后的数据大小来创建恢复任务。如果每条消息的平均大小比我们预期的大,这可能导致创建过大的分块,这比过小的分块更糟糕,因为它意味着我们没有分配足够的空间。

设计动态分块

为了构建动态分块,我们必须修改Astra的两个部分:集群管理器和缓存。

重新设计缓存节点

我们首先研究了缓存节点的结构:以前,每当缓存节点上线时,它会在Zookeeper(我们的集中协调存储)中通告其槽位数量。然后,Astra管理器会为每个槽位分配一个分块,缓存节点会去下载并提供该分块。

每个缓存节点都有一个生命周期:

  1. 缓存节点上线,通告其拥有的槽位数量
  2. 管理器获取槽位信息,并为每个槽位分配一个分块
  3. 每个缓存节点下载分配给其槽位的分块

这样做的好处是槽位是临时的,意味着每当缓存节点下线时,其槽位将从Zookeeper中消失,管理器将重新分配这些槽位曾经持有的分块。

然而,对于动态分块,每个缓存节点只能通告其容量,因为它无法提前知道将被分配多少分块。这意味着我们不幸地不能再依赖槽位来提供这些好处。

为了解决这两个问题,我们决定在Zookeeper中持久化两种新类型的数据:缓存节点分配和缓存节点元数据。

以下是简要说明:

  • 缓存节点分配:分块ID到缓存节点的映射
  • 缓存节点元数据:关于每个缓存节点的元数据,包括容量、主机名等

利用这两种新类型的数据,新流程如下:

  1. 缓存节点上线,通告其磁盘空间
  2. 管理器获取每个缓存节点的磁盘空间,并为每个缓存节点创建分配,利用装箱算法来最小化使用的缓存节点数量
  3. 缓存节点获取为其创建的分配,并下载其分块

重新设计管理器

下一个变化是在管理器中,升级它以利用我们引入的两种新类型数据:缓存节点分配和缓存节点元数据。

为了利用缓存节点分配,我们决定实施首次适应装箱算法来决定哪个缓存节点应该被分配哪个分块。然后我们使用缓存节点元数据来做出关于是否可以将特定分块放入给定缓存节点的适当决策。

以前,分配槽位的逻辑是:

  1. 获取槽位列表
  2. 获取要分配的分块列表
  3. 同时遍历两个列表,将槽位分配给分块

现在,逻辑如下:

  1. 获取要分配的分块列表
  2. 获取缓存节点列表
  3. 对于每个分块:
    • 执行首次适应装箱算法以确定应该将其分配给哪个缓存节点
    • 持久化缓存节点到分块的映射

装箱算法

重新设计管理器最有趣的部分是实现首次适应装箱算法。这是一个著名的最小化用于容纳一定数量物品(分块)的箱子(缓存节点)数量的问题。我们决定使用首次适应装箱算法,因其速度和易于实现而受到青睐。

使用伪代码,我们描述装箱算法:

1
2
3
4
5
6
7
8
for each chunk
  for each cache node
    if the current chunk fits into the cache node:
      assign the chunk
    else:
      move on to the next cache node
  if there aren't any cache nodes left and the chunk hasn't been assigned:
    create a new cache node

这有助于确保我们能够尽可能紧密地打包缓存节点,从而实现更高的分配空间利用率。

部署实施

总体而言,这是对Astra代码库的重大更改。它触及了Astra的许多关键部分,基本上重写了处理分块分配和下载的所有逻辑。对于这样的更改,我们希望谨慎部署以确保不会破坏任何东西。

为了确保不会破坏任何东西,我们做了以下工作:

  • 托管相同数据的两个副本
  • 将所有动态分块代码放在功能标志后面

我们严重依赖这两个防护措施来确保安全部署。

托管相同数据的两个副本使我们能够逐步部署到两个副本中的一个并监控其行为。它还确保如果我们的更改破坏了任何东西,我们仍然有第二个副本能够提供数据。

将所有代码放在功能标志后面使我们能够早期将代码合并到master分支,因为除非明确启用,否则它不会运行。它还使我们能够逐步推出和测试我们的更改。我们从较小的集群开始,在验证一切正常后逐步转向越来越大的集群。

结果

我们最终看到了什么样的结果?首先,对于有许多过小分块的集群,我们能够将所需的缓存节点数量减少高达50%!总体而言,我们的缓存节点成本降低了20%,为我们运营Astra带来了显著的成本节约。

致谢

特别感谢所有帮助实现动态分块的人员:

  • Bryan Burkholder
  • George Luong
  • Ryan Katkov
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计