1. 背景和动机
“云原生”俨然是时下的热词。本质上,它是指在云上运行的具有弹性、可管理性和可观察性的系统。RisingWave 就是一个云原生的流式数据库,它的设计宗旨是充分利用云计算带来的弹性。
但是,在云上部署和维护 RisingWave 仍然是一个具有挑战性的“技术活”。特别是考虑到以下问题:
- 如何在云服务供应商(如 AWS、Azure 和 GCP)上部署流式数据库?
- 当负载过大时如何扩展数据库?能否自动进行扩展?
- 当一些组件出现故障时如何进行快速恢复?
还好,有 Kubernetes。Kubernetes 是一个开源系统,可在计算机集群上自动化部署、扩展和管理容器化应用程序。它提供了大量的 API 用于抽象所需资源。有了 Kubernetes,人们在部署应用程序时不必担心被云服务供应商深度绑定。
我们开发了 Kubernetes Operator for RisingWave(下文简写为“Operator”),一个能在Kubernetes 上管理 RisingWave 的扩展插件。编写 Operator 颇具挑战,这篇博文将概述 Operator 的功能特性、实现过程中的挑战以及我们的解决方案,供有相同需求的用户参考。
2. Kubernetes operator 基础知识
在讲到operator之前,先简要介绍一下Kubernetes中的一些核心概念。
- Pod是Kubernetes中可被调度的最小单元。每个Pod可包含多个容器(Containers),Pod之间通常共享相同的命名空间隔离(namespace isolation)。Pod是Kubernetes提供的抽象计算资源的基本组件。不夸张地说,整个世界都是建立在它的基础上的。
- Service是一种抽象,用于将一组Pod上运行的应用程序公开为网络服务。
- Deployment和StatefulSet都是工作负载资源(workload resources)。工作负载资源能帮助你在Kubernetes上运行应用程序,并能提供高级功能。例如,使用Deployment,你可以简单地通过设置所需的副本来扩容或缩容应用。Deployment和StatefulSet的主要区别在于,Deployment仅适用于无状态应用(stateless applications),而StatefulSet则尽可能保持状态活跃,例如挂载的持久卷。
除了内置资源之外,Kubernetes还引入了自定义资源(即CustomResourceDefinition,CRD),以扩展其API。CRD API允许用户自定义自己的资源。通常,每种资源类型都有一个控制器(controller)。但是Kubernetes核心组件中没有针对自定义资源的控制器。开发者通常会提供与自定义资源相配对的控制器。
那么,Kubernetes operator到底是什么?广义上,Kubernetes operator是一组CRD和相应的控制器。控制器通过遵循operator模式确保扩展的API最终按预期运作。
在介绍了Kubernetes operator的一些基础知识后,接下来的部分将详细说明Kubernetes Operator for RisingWave是如何建立在这些抽象之上的。
3. 实现Kubernetes Operator for RisingWave
Operator 能管理 RisingWave 集群。它使得部署、升级、监控和扩展 RisingWave 集群的操作变得简单、高效、自动化。要实现 Operator,技术上主要有两方面挑战:
- 如何在 Kubernetes 中体现 RisingWave?
- 如何在控制器中建立管理不同组件的工作流程?
4. 在 Operator 中抽象 RisingWave
要在 Kubernetes 中抽象 RisingWave,需要解决三个主要问题。
4.1 使用工作负载资源抽象组件
RisingWave有四个主要组件:元数据服务器、前端、计算节点和compactor节点。有关详细信息,可参考设计文档。我们可以将所有组件节点都看作是无状态的,因为RisingWave是建立在云原生共享存储上的。因此,将Deployment用于每个组件再直截了当不过。但是,考虑到计算节点中的本地缓存,我们使用了StatefulSet而不是Deployment来管理它们。StatefulSet在状态方面有许多保证,例如,它会在升级副本时保持顺序,但我们所需的是它的存储守护。
4.2 用于通信的稳定网络标识符
在Kubernetes中,每个Pod在创建时都会分配一个内部IP。除非存在进一步的限制,这个IP在Kubernetes内部是可访问的。但是,这些内部IP不能直接用于进程之间的通信。例如,当发生故障或升级时,Pod常常会被销毁并重建。在这些情况下,同一组件的IP也会在RisingWave实例的生命周期中发生变化。因此,我们为每个组件启用了一个单独的Service。这些Service会保留不可变的内部IP作为稳定的标识符。我们利用Service的发现机制让节点之间“认识”彼此。
4.3 连接不同的资源
那这些资源能够为RisingWave实例提供所需的功能吗?不幸的是,对于实现一个可用于生产环境的operator来说,答案是否定的。为了在部署和管理上有更多灵活性,我们还开发和使用了:
-
ConfigMap——用于存储配置。它将挂载到运行的Pod上。
-
工作负载资源组——同一组件的工作负载资源可以分成多个组并单独管理。例如,我们允许计算节点在两个可用区(availability zones)运行。
-
ServiceMonitor——用于声明Prometheus的抓取任务。这是个可选的子资源。它只会在安装Prometheus operator时同步。
这是RisingWave不同子资源之间关系的概览。
5. 实现控制器
在 Kubernetes 中,这些管理不同资源的工作流是在控制器中定义的。在下文中,我们将讨论在 Operator 中实现控制器的细节。
5.1 反应作为基本单元
实现控制器非常复杂。每个不同的子资源都有自己的状态和逻辑。我们将复杂的过程分成小的片段:每个片段只关注一些子资源,并尽力做到最好。例如,前端Deployment只应在有相关规格更改时更新,例如修改副本、资源或节点选择器时。在这种情况下,我们只需进行以下其中一种操作:
- 在找不到此类资源时创建一个。
- 根据RisingWave的规格进行更新。
- 如果已过时且不再需要,则删除。
这些操作构成了对RisingWave规格和实际状态的反应(reaction)。每个子资源都有自己的反应。无论发生什么,反应都会找到一种方法来确保Deployment最终在线并与规格保持一致。
5.2 反应式工作流
我们设计了一个反应式工作流,并提供两个原语来有序地组织反应。
- Join反应允许反应运行,但将结果合并。本质上,Join是用于同时运行多个独立反应的。
- Sequential反应允许反应按顺序运行。它能确保依赖的反应在条件满足之前不会运行。
除了用于将反应组织到工作流中的原语之外,我们还设计了几个装饰器来增强反应,但不改变它们的意图:
- Timeout可装饰反应,并尽可能在给定时间过后使程序超时。
- Retry在遇到意外错误时会重试反应,直到达到限制次数。
- Parallel可并行运行基础反应。
- Shared可标记反应为共享反应,让不同的工作流分支可以共享该反应,但只运行一次。
此外,工作流原语还支持构建结构化并发工作流,这可能是并行编程的最佳模式。你可以参考代码库中的ctrlkit包了解有关设计的更多细节。下图展示了当前运行的工作流:
在上方图表中,Sync Components and Sub-resources是一个共享的子工作流,如下所示:
有了反应和工作流,开发者能从这几方面受益:
- 专注于一个小单元(通常只考虑单个方面的子资源)使得代码更具鲁棒性、更易于维护。
- 用原语来组织工作流能最大限度给予调整行为的灵活性,并将常用的装饰器与反应解耦。在Operator的工作流中,我们追求极致的效率,几乎并行化了每个独立的反应。
6. 碎碎念
我们还遇到了很多其他的技术难题,例如使用客户端缓存所带来的问题,客户端缓存对象并提供Get操作(那么数据的新鲜度始终不够好)。这些我们会留到以后的文章中再探讨。
7. 改进计划
在经过上文讲述的所有努力之后,Operator已经整合了在云上管理RisingWave集群的基本功能。但是,我们还有很多工作要做。在我们发布这篇文章的时候,许多令人兴奋的功能的设计和开发正如火如荼地进行着:
- kubectl插件——可帮助用户在Kubernetes上更自然地操作RisingWave。
- Helm图表——可用于部署Operator、RisingWave和依赖项。
- 与Grafana和Prometheus的即插即用集成。Operator将来还会附带可用于生产环境的仪表板。
8. 总结
本文概述了开发 Kubernetes Operator for RisingWave 过程中的主要技术挑战以及我们的解决方案。Operator 能帮助用户在 Kubernetes 中运行 RisingWave。它支持即开即用式的对RisingWave 集群的重启、扩展和升级操作。
声明:Kubernetes Operator for RisingWave 于 2022 年底就已经开发完成,部分技术细节可能失去时效性