ApeCloud
开源社区关于我们

快手

海量 Redis 集群的容器化挑战

演讲者 刘裕惺 快手云原生团队

价值 1

降低基础设施运营成本

价值 2

简化有状态服务管理

快手中的 Redis

在深入探讨细节之前,我们先了解一些背景信息。快手的 Redis 采用的是经典的主从架构,包含三个组件(Server、Sentinel 和 Proxy)。极大规模是快手 Redis 集群的一个明显特征,不仅体现在实例总数上,还体现在单个集群的规模上,单个集群规模甚至可以超过 1 万个实例。

在快手中,Redis 已经稳定运行,支撑了如此大的规模的时候,为什么我们会愿意来折腾,推进 Redis 的云原生化呢?因为我们发现快手 Redis 资源利用率相对不高,而对于如此大规模的系统,即使进行一点小小的优化,也会带来巨大的收益。

那提升资源利用率一般有哪些好的方式呢?其实我们发现云原生技术已经给我们提供了最短路径与最佳实践。此外,容器云已经成为业务与基础设施之间的新接口,而且快手的无状态服务大部分已经迁移到了容器云中。从长远来看,基础设施的统一是不可避免的趋势。这不仅可以将业务与基础设施解耦,还能提高业务的敏捷性,降低基础设施的运营成本。

因此,我们决定进行云原生转型,从而实现成本优化。为了更好地完成这项工作,我们不仅邀请了 Redis 团队,还邀请了云原生团队参与其中。

为什么选择 KubeBlocks

事实上,今天我们能在这里交流,已经表明我们是通过 KubeBlocks 来实现的。接下来,我想和大家分享为什么我们选择了 KubeBlocks?简单来说,KubeBlocks 提供了面向有状态服务的 API

那么,什么是有状态服务?有状态服务和无状态服务之间的关键区别是什么呢?

根据字面意思,区别似乎在于是否具有状态信息。然而,我们认为真正的区别在于实例之间的不平等关系。正是因为不同的实例扮演着不同的角色并存储不同的数据,因此我们不能随意丢弃任何实例。此外,这种不平等关系并不是静态的,它在运行时很可能发生变化,比如发生主备切换。KubeBlocks 正是为了解决这个问题而设计的,它提供了基于角色的管理能力

更进一步说,KubeBlocks 支持多种数据库,因此不需要为每个数据库部署一个专用的 Operator。此外,还有一个非常有趣的点是,KubeBlocks 通过 OpsRequest 提供了面向过程的 API,使得数据库的云原生化改造更加容易。如果你感兴趣,可以通过官方文档了解更多详细信息。

Redis 集群编排定义

我们已经讨论了很多关于 KubeBlocks API 的内容,那么 KubeBlocks 究竟是如何定义 Redis 集群的呢?它应该包含所有的组件(即 Server/Sentinel/Proxy)。为了减少组件定义的重复,KubeBlocks 将组件定义(Component Definition)和组件版本(Component Version)独立开来,这样在创建新集群时可以直接引用。这里我们以 Redis Server 为例,这是最复杂的组件形态。

  • 首先,Redis Server 是通过 ShardSpec 定义的,它包含了多个分片列表,以支持更大规模的数据。对于一个 Redis Server 集群来说,它需要被分割成多个分片,每个分片都有主从实例。

  • 另一个关键点是,同一分片内的主从实例可能有不同的配置。为了处理这一点,KubeBlocks 允许用户使用 InstanceTemplate(实例模板)在同一组件内定义多种配置。通过 Redis Server 组件示例,我们可以看到对象关系分为以下几个层次:

  • Cluster - 用于定义整个 Redis 集群。

  • ShardSpec - 用于定义 Redis Server 分片的列表。

  • Component - 用于定义 Redis Proxy、Sentinel 和单个 Server 分片。

  • InstanceTemplate - 用于定义同一组件内的不同配置。

  • InstanceSet - 它是最终自动生成的工作负载,提供了我们之前提到的基于角色的管理能力。

角色(Role)管理

关于基于角色的管理能力,可以总结为两个关键点:

  • 构建并维护正确的关系
  • 实现细粒度的基于角色的管理

我们重点关注如何维护正确的角色关系,有一件重要的事情需要注意。我们认为,单个分片的主节点信息非常重要。如果发生错误或无法获取,将会导致严重的问题。因此,为了确保业务的稳定性,我们倾向于在此处分离数据面(data plane)和控制面(control plane)。这就是为什么我们不从 KubeBlocks 获取主节点信息的原因。

部署架构

我们已经看到,KubeBlocks 在单个 Kubernetes 集群上运行有状态服务时表现得非常出色。然而,正如之前提到的,快手的 Redis 数量非常庞大,远远超出了单个 Kubernetes 集群的容量。因此,我们不得不使用多个 Kubernetes 集群来支持业务。关于多集群管理,如果我们将复杂性直接暴露给 Redis 业务,将会带来以下几个问题:

  • Redis 团队需要为所有 Kubernetes 集群维护缓冲资源池,这意味着更多的资源浪费。
  • Redis 团队必须在单个 Kubernetes 集群达到上限之前提前迁移 Redis 集群。

我们认为,最好隐藏多集群的复杂性。而且,我们已经通过联邦集群提供了所需的能力。不过 KubeBlocks 本身并不支持多集群。我们是如何解决这个问题的呢?以下是整体架构。

我们将 KubeBlocks operator 拆分为两部分。Cluster Operator 和 Component Operator 放置在联邦集群中,而 InstanceSet Controller 放置在成员集群中。在中间,有一个名为 Federal InstanceSet 控制器的组件,用于将 InstanceSet 对象从联邦集群分发到成员集群。那么,Federal InstanceSet 控制器是如何工作的呢?

  • 首先,它的首要职责是根据调度建议,决策每个集群应该部署多少个实例。
  • 其次,它的主要任务是拆分 InstanceSet 并将其分发到成员集群。

与 StatefulSet 类似,InstanceSet 中的实例也有编号名称。为了确保不打破这一规则,我们重新设计了 InstanceSet 中的 ordinals 字段,允许自定义编号范围。通过这种架构,我们能够在不做重大修改的情况下支持 KubeBlocks 在多个 Kubernetes 集群中运行。

稳定性保障

除了在功能上满足业务需求外,我们还需要确保解决方案能够达到生产级别,特别是在稳定性方面的保障。我们在这里讨论几个关键点。

首先,在调度能力方面,为了确保 Redis 的高可用性,我们应该确保实例尽可能地分散,但同时也要考虑单台机器故障对 Redis 集群规模的影响。因此,我们定制了一种细粒度的分散调度能力,既支持配置每个节点的最大实例数,又支持配置每个 Redis 集群的最大节点数。我们还提供了基于 CPU/内存/网络带宽的负载均衡调度能力。

接下来,我们来看一下运行时控制。我们都喜欢 Kubernetes 带来的自动化,但这也意味着更大的风险:一个小的变动可能导致大规模的故障。因此,我们对运行中的实例进行了大量控制,比如并发控制、仅允许原地更新等。此外,还有许多其他的工作。由于时间限制,我不会一一列举。

总结

最后,让我们做一个简单的总结。我相信大家已经意识到,KubeBlocks 是一个出色的项目。与 StatefulSet 相比,KubeBlocks 为有状态服务设计了全新的 API,并提供了基于角色的管理,这些设计使得有状态业务的云原生转型更加容易。看起来,KubeBlocks API 几乎可以支持所有的有状态服务。不过,我认为仍然有一些工作需要完成:

  • 如何与现有的数据库 Operator 建立连接,并对齐它们的功能。
  • 尝试推动有状态服务标准 API 的构建,快手也愿意与 KubeBlocks 一起在这个领域努力。也欢迎大家一起在这个领域做出更多的探索。

迄今为止,快手已经在许多功能上与 KubeBlocks 展开了合作,如 InstanceSet 直管 Pod 和 PVC、实例模板、与联邦集群集成等等。欢迎大家关注后继的工作进展。

演讲发表于 KubeCon China 2024