服务器 频道

是否应在Kubernetes上运行Redis?快手这样做

  导读

  针对无状态服务,业界已拥有成熟解决方案,但对于有状态服务(如数据库、Redis)是否适合容器化与K8s托管,仍存在争议。本文将基于快手在 Redis 云原生化实践中的经验,探讨有关有状态服务的云原生化思考及应对方案。

  一、背景‍‍

  随着行业技术的不断演进,快手的基础设施顺应技术潮流逐步迈向云原生化。在各业务团队的支持下,容器云成为服务与基础设施的新界面,目前在快手无状态服务已基本全面实现 Kubernetes (K8s) 的云原生化。然而,有状态服务的云原生化之路却仍然充满了挑战:

  有状态服务究竟是否适合在 K8S上运行?

  有状态服务的云原生化如何实现与落地?

  以 Redis 为例,它是快手广泛使用的有状态服务之一,超大规模是其显著特征。即使微小的优化在如此庞大的规模下也能为企业带来巨大的收益。在面向未来的长期规划中,快手高度认可 Redis 云原生化带来的潜在价值,尤其是资源利用率提升带来的成本优化。本文基于快手 Redis 云原生化实践中的经验,深入剖析有状态服务的云原生化思考及应对策略。

  二、有状态服务是否适合运行在K8S上?

  有状态服务运行在K8S上的风险与收益图片

  将有状态服务运行在 Kubernetes 上的收益显而易见:

  提升资源利用率

  :通过“合池”、“统一调度”和“混部”,优化资源使用,显著降低成本。

  提高运维效率:利用 Kubernetes 的声明式 API 和控制器模型,提升业务运维效率,确保技术先进性。

  降低运维成本:统一基础设施减少运维维护成本,提升整体管理效率。  ‍

  尽管有状态服务运行在 Kubernetes 上的收益显著,但潜在风险也需认真评估,尤其是对数据库等有状态服务,其重要性和稳定性要求极高。这里的主要风险包括:

  1. 性能下降风险:容器化部署增加了一层抽象,是否会导致服务性能下降?

  2. 稳定性影响:

  将数据库体系构建在K8S体系上,有状态服务稳定性是否会受到影响?

  3. 运维复杂度增加?

  问题发生时,是否需要同时具备数据库和云原生技术的专家才能有效定位和解决问题?  

  针对上述风险,下文将展开探讨。

  有状态服务运行在K8S上的可能性图片

  在探讨有状态服务时,我们首先需要明确“状态”的具体含义:

  数据状态:单个实例保存的、区分其他实例的数据。每个实例承担不同角色,存储独特的数据状态。因此,任何实例不可随意丢弃;同时,在实例生命周期管理过程中,也需额外对数据进行处理(如备份、恢复、数据重平衡等)。

  拓扑状态:不同角色实例之间的关系状态信息;且这种关系在实例运行过程中通常是动态变化的。

  综上,有状态服务的云原生化相比无状态服务面临新的挑战:如何保证数据的可用性、管理数据生命周期,以及建立和维护拓扑关系并实现基于拓扑结构的服务发现和灰度发布等能力。K8S 社区目前的应对措施包括:

  方案一:StatefulSet Workload:

  K8S 社区推出了 StatefulSet Workload,为每个实例分配一个全局有序递增的编号。这一机制为实例提供了稳定的网络标识和存储标识,从而维护了拓扑状态和数据状态。

  然而,拓扑状态在运行过程动态变化,仅靠 StatefulSet 提供的编号难以满足需求。

  方式二:自定义Workload + Operator:‍

  针对已有 Workload 无法应对动态拓扑关系等复杂场景的问题,自定义 Workload 和 Operator 提供了扩展能力,这也促进了 Redis Operator、MySQL Operator 等多种有状态 Operator 的出现。

  然而,从零开始开发一套 Operator 的成本往往使许多数据库团队却步。即使复用现有的 Operator,企业内自定义运维逻辑与现有逻辑的融合依然复杂且具挑战性。

  综上所述,有状态服务部署在 Kubernetes 上相比无状态业务来说需要更多风险考量,且实现的成本和难度相对较高。然而,这种投入往往是一次性的,而云原生化带来的收益则是持续性的,可以为企业未来发展提供坚实基础。因此,企业在决策时应充分权衡这些因素,以做出更具战略性的选择。

  三、快手如何将Redis运行在K8S上?

  快手的 Redis 采用的是经典的主从架构,包含三个组件(Server、Sentinel 和 Proxy)。  

  迁移单个Redis集群上K8S‍图片

  快手 Redis 云原生化思路

  基于以上分析,Redis 的云原生化实现无法基于 Kubernetes 社区现有 Workload 实现,具体而言:

  多分片与多实例关系表达:需清晰定义多分片架构及单分片下的多实例层次关系。同时,需要支持分片数量和单分片下多实例数量的动态变化,以应对不同负载和使用场景的需求。

  生命周期变化过程中数据管理:在分片或实例生命周期变化时,如何有效管理数据是一个重要挑战。需确保数据的一致性和可靠性,包括分片数量变化时的数据重平衡,以及分片内实例数量变化时的数据备份与恢复策略等。

  单分片内多实例动态拓扑关系的表达:在一个分片下,如何实时表达多个实例之间的动态拓扑关系变化,并基于实时拓扑结构实现服务发现和灰度发布等能力。

  针对上述挑战,我们提出了如下解决思路:

  分层架构设计:基于不同 Workload 分别管理分片及其下的实例;具体而言,使用一个 StatefulSet 管理每个分片下的多个实例,并在此基础上构建一层新的工作负载,以实现对整个 Redis Server 集群中多个分片的统一管理;

  生命周期钩子支持:支持分片和实例维度的生命周期 Hook 能力,允许在不同生命周期阶段执行自定义的数据管理操作;

  动态拓扑感知与服务发现:引入角色探测和角色标记能力,进一步实现系统基于动态拓扑关系的服务发现和灰度发布能力。

  KubeBlocks 解决方案

  经过调研,KubeBlocks 项目成为我们重点关注的解决方案之一,其贴合我们的思路与需求。作为一个开源 K8S Operator,KubeBlocks 抽象了管理各种数据库的 API,支持在 Kubernetes 上运行和管理多种类型的数据库。其愿景是“在 Kubernetes 上运行任何数据库”。

  经过与 KubeBlocks 社区深度合作,我们通过如下方式实现一个 Redis 集群的编排:  

  1. InstanceSet Workload

  与 StatefulSet 相比,其增加支持了角色定义、角色探测方式以及角色更新策略的定义抽象;同时,InstanceSet 控制器在实例运行过程中会动态探测角色变化,并将角色信息以标签(label)的方式更新到实例的元数据中,从而支持基于角色的服务发现等能力。

  除此之外,快手和 KubeBlocks 社区一起共建了 InstanceSet 直管 Pod 和 PVC、InstanceSet 实例异构配置、指定实例缩容、并发控制、多种更新策略等增强能力,使其能够灵活应对复杂的业务场景。

  2. Component、Shard 、Cluster Workload:

  Component:将组件的定义与组件实例解耦。通过引用组件定义,可以生成对应的 InstanceSet,使得组件管理更加灵活;并支持实例维度的生命周期管理。

  Shard:用于生成一组相同的 Component 实例,主要适用于类似 Redis Server 的分片场景,便于管理和扩展;并支持分片维度的生命周期管理。

  Cluster:用于定义整个有状态服务集群,在 Redis 场景下,可以统一表达 Proxy、Server 和 Sentinel ,并设置它们的启动拓扑关系。  ‍

  迁移大规模Redis集群上K8S‍图片

  前面提到,超大规模是快手 Redis 的显著特征,其实例规模远超单个 K8S 集群的容量。因此,我们不得不基于多个 Kubernetes 集群来支持业务。与传统模式下所有主机平铺的方式不同,因为 K8S 单个集群的容量限制,我们必须将主机资源池切分到多个 K8S 集群中。如果将多个 K8S 集群的复杂度直接暴露给 Redis 业务方,上云成本势必会被大幅增加。

  联邦集群架构

  在快手,我们通过基于联邦集群能力提供相应的统一调度和统一视图能力,降低业务方的复杂度,使其更专注于核心业务。

  统一调度能力:通过联邦作为统一资源下发入口,实现实例在多个成员集群之间的调度。

  统一视图能力:联邦作为统一资源获取入口,统一获取联邦和成员集群的相关资源。

  ‍而如何将上述基于 KubeBlocks 的方案落地到联邦集群架构呢?以下是整体架构:  

  1. KubeBlocks Operator 拆分:

  将 KubeBlocks 拆分为多个部分,其中:

  InstanceSet Controller 放置在成员集群中

  Cluster Operator 和 Component Operator 放置在联邦集群中  ‍

  2. Fed-InstanceSet控制器:

  在联邦集群和成员集群之间,快手扩展自研了 Fed-InstanceSet 控制器组件,其主要职责为:

  调度决策:根据调度建议,决策每个集群应该部署多少个实例;

  InstanceSet 拆分与分发:负责拆分 InstanceSet,并将其分发到多个成员集群。

  Fed-InstanceSet 控制器

  针对 Fed-InstanceSet 控制器,有两个关键问题需要解决:

  实例拆分管理:确保在联邦部署模式下,Redis 集群的实例列表全局有序唯一;

  管控规则拆分:保证在联邦部署模式下,InstanceSet 的管控规则全局符合预期。这包括管理灰度变更的顺序和并发度控制等。

  针对第一个问题,我们与社区合作,设计了 Ordinals 字段,允许指定编号的索引值。在多集群下发场景下,Fed-InstanceSet 控制器可以为每个子集群的 InstanceSet 设置不同的索引值,从而保证实例在多 K8S 集群中的全局唯一性和有序性。  

  针对第二个问题,我们需要结合全局顺序、角色关系、灰度变更策略、并发度管控和角色变更策略等因素,构建全局变更的有向无环图(DAG)。该图结构将用于保证多 K8S 集群范围内的全局变更管控。

  应对Redis上K8S的风险图片

  性能

  Redis 基于云原生架构部署模式下,相较于传统主机部署增加了一层容器抽象。但根据业界分享和快手内部测试结果,这种架构带来的性能差距通常在 10% 以内,很多情况下基本持平。这种性能变化往往在可接受范围内。企业也可根据自身实际情况进行性能测试与评估,以确保满足业务需求。

  稳定性

  迁移有状态服务上 K8S 后,尽管 K8S 带来的自动化能力大幅提升了运维效率,但这也导致执行流程变得黑盒化,且微小的配置变动会影响大范围的业务实例。因此,为了有效应对非预期内的运维操作(如K8S 发生实例驱逐、集群运维人员误操作、Operator 逻辑异常等场景)给业务带来的稳定性风险,我们需要在如下工作上做出努力:

  如何区分预期与非预期的运维操作?

  如何系统性避免非预期的运维操作?

  针对问题一,答案显而易见:是否由运维人员主动发起可作为唯一的判断标准。基于此,我们可以为业务团队生成指定的 ServiceAccount 证书,并通过请求中的用户信息来区分变更发起来源;

  针对问题二,沿着问题一的思路,我们可以利用 K8S APIServer 的 Admission Webhook 机制,对所有变更请求进行拦截和校验,从而直接拒绝非预期的变更操作;基于快手多 K8S 集群的场景,我们需要实现跨集群和多可用区(AZ)的变更管控能力。为此,快手内部研发了一套风险变更阻断系统:kube-shield,关于该系统的更多细节,本文将暂时不做深入探讨,计划未来会单独撰写一篇文章进行详细介绍

  另外,值得一提的是,在快手内部,通过增强对细粒度打散调度能力的支持,以及基于资源利用率的负载均衡调度等功能,进一步提升了业务的高可用性与稳定性。

  运维复杂度

  将基于主机的运维体系迁移至基于 K8S 的运维体系,并支持后续的运维工作,这需要对 Redis 和容器云两个领域具备深入的理解,若仅依靠 Redis 团队或容器云团队独立支持都将非常困难。而合理的分工,不仅可以提高生产效率,也能充分发挥各团队在各自领域的专业性。

  以快手的 Redis 云原生方案为例:

  Redis 团队重点关注如何定义 Redis Cluster 对象,并将已有的运维经验通过定义的方式进行转化;

  而将 Redis Cluster 对象提交给 K8S 后的工作,则由容器云团队进行保障,包括 Operator 的研发与维护、调度处理等。  ‍  

  四、总结‍

  “有状态服务云原生化”是一个需要慎重考虑利弊且充满挑战的过程,但对于快手来说,其价值显而易见。我们以 Redis 为起点,与 KubeBlocks 社区深度合作,低成本完成 Redis 的云原生化方案落地。未来,快手将基于以上经验,继续推动更多有状态服务,如数据库和中间件的云原生化,从而获得技术和成本的双重收益。

0
相关文章