作者| Sylvain Kalache
译者| 张业贵
Kubernetes(简称K8s)上数据服务的自动化越来越受欢迎。在K8s上运行有状态的工作负载意味着使用Operator。然而,它发展演化到今天已经变得非常复杂,像Operator这样的应用模式和扩展方式对于开发者与运维者而言愈发受到欢迎。
但工程师们经常对编写K8s Operator的复杂性感到吃力,这会影响到最终用户。据《2021年K8s数据报告》指出,K8s Operator的质量阻碍了公司进一步扩大K8s占有率。
Anynines首席执行官Julian Fischer已经构建自动化工具近十年了,他非常了解在处理在云原生平台和K8s等分布式基础设施上做状态管理的复杂性。
Julian首先分享了在构建 Operator时应该遵循的方法,他称之为运营模式,分为四个部分:
- 第1级:SysOp或DB要做什么
- 第2级:容器化,YAML + kubectl
- 第3级:编写 Operator
- 第4级: Operator 生命周期管理
通过他的分享,可以了解数据服务自动化中的常见陷阱以及如何避免,进而从技术和方法学角度编写更好的K8s Operator。
数据服务自动化
它在数据服务自动化的一般主题和K8s之间有点跳跃。一般来说,如果你谈论数据服务自动化,你必须做的第一件事就是明确范围,即你所说的数据服务自动化真正指什么。对于我们来说,任何时候都有一个使命,那就是将各种数据服务的整个生命周期完全自动化,以便在跨基础设施的云原生平台上大规模运行。
这里不是一些营销噱头,它是如何对数据服务自动化进行范围分析的一个例子。例如,为了使多个数据服务自动化,你希望看到某些共享效果,比如你可以将Operatar SDK以外的数据服务纳入自动化框架。因此,任务的背景会产生很大影响。例如,一个简单的K8s集群,有一个小单位用来运行他们的应用程序。假设使用Postgres数据库,Postgres一直是我最喜欢的例子。大家都知道,一个K8s集群对应一个Operator一个服务实例,应用程序将连接到那个数据库。这与我们今天想在这里谈论的故事不同。假设他们按需配置服务实例,Postgres数据库被表示为有状态集。Operator允许你创建多个实例。事情就会变得复杂,因为你有更多的数据服务实例,你必须处理好它。如果你随后引入更多数据服务,例如,将RabbitMQ、MongoDB或任何其他数据库添加到Operator的集合中,挑战将变得更加大。
现在我们协作的组织中,有时有数百或数千名员工,有数千名甚至一万名开发人员,令人难以置信的是,他们拥有数量庞大的工程师,也同时拥有许多K8s集群。我们认为,数以十计、数以百计的K8s集群对我们的经验是个考验。例如,在基于虚拟机的数据服务自动化中,它们通常有上千台虚拟机运行上千个服务实例,这取决于它们如何组建集群。你可以假设有一个服务实例对应三个pod,恰好正在运行这样一个小集群实例。在现有这个规模上,自动化的需求经常变更,对规模的影响很大。
如果你解决了制作“香肠”和分发“香肠”等简单任务,你可以想象,如果你要服务更大规模的用户,堆栈技术解决方案要进行调整。数据服务自动化也几乎一样。因此,如果我们考虑那些大型应用场景,拥有很多这样的服务实例,每个数据服务实例对某些用户都很重要。因此,自动化需要符合一定的标准。如果该标准没有达到自动化水平,那么自动化将用不起来,组织和技术的采用就不会发生。
使用K8s的数据服务
那么如何使用K8s的数据服务?首先,你如何实现一个Operator,如我认为社区应该知道的那样?最简单的方法是使用K8s、CRD和自定义资源定义,传输给K8s新的数据结构。例如,在描述你的Postgres实例时,要创建一个复数的Postgres实例,因为我们是按需配置,有一个控制器负责管理实例。该控制器将按你指定的对象的规范,将其转换为可运行的程序。因此,基本上,Operator所做的是将具有Postgres 12.2版本的主对象(如Postgres实例)的规范转换为辅助资源。据我所知,Operator SDK是构建CRD、生成CRD并为控制器提供模板代码的主流工具。这是讨论K8s相关的数据服务自动化时,我们想到了这两件事。同时,还有KUDO。
如果你对此感兴趣,我几周前做了一次演讲,DoK社区会议关于数据服务自动化的话题非常有趣,今天这里将无法更深入地讨论原型。
在开发阶段,如果你开发了一个Operator,挑战之一是你想如何系统地处理这项工作。有一个简单的模型,我们称之为操作模型,分为四个级别,这有助于你处理数据服务自动化,这是第一次提出。
给一点建设性的意见,要把你的注意力放在任务上。例如,我们建议在第一级实现Postgres的自动化。你需要掌握的第一件事是助理或者DBA要做什么。特别是,这对应用程序开发人员有什么影响?他们到底想要什么?
例如,应用程序开发人员对Postgres的平均期望是什么?他们需要自动故障转移的集群实例吗?在这种情况下,他们更喜欢同步复制还是异步复制?你想使用哪种故障转移和集群管理器,还是使用首选仓库管理器,或者更确切地说,使用Prometheus(普罗米修斯)?
而且,基本上你要搞清楚如何配置文件,对Postgres完成基本设置,这是操作模型一级。只需假设你有一台虚拟机,你可以做任何你想做的事情,安装软件包,配置数据库等等。因此,一旦你这样做了,你知道配置文件应该是什么样子,所有的这些都是Operator可以做到的。你可以考虑容器化,它可以选择现有的容器映像,并将其组装到有状态集服务的K8s规范中,并创建自有的模板,这是操作模型二级中的YAML部分。因此,在操作模型二级的最终操作中,无论你是选择了现有的容器映像还是自己创建了它们,你都有K8s规范,可以与kubectl一起使用,以手动创建自己的服务实例。一旦你这样做了,你基本上可以创建你的Postgres实例,比如说,用三个副本和同步流复制,假定你已经知道如何手动做到这一点,然后你可以通过思考问题,如何编写gde,创建特定的有状态设置的无头服务来处理特定的保密数据,你可以更容易地实现这样的一个Operator。
现在,假设我们提醒自己,我们正在谈论的环境可能包含1000多个数据服务实例,跨越许多K8s集群的多个数据服务。在这种情况下,我们还需要接受Operator生命周期管理本身是我们工具链的重要组成部分。因此,我们还需要自动化来管理Operator本身的生命周期。
无论是Operator生命周期管理器,还是其他技术在这一点上都无关紧要,最重要的是你需要知道,这是你整体数据服务自动化挑战的一部分。现在,如果你想到K8s Operator,并且提到自定义资源定义,像这样的YAML结构描述了一种可以传递给K8s API的新数据类型,然后K8s API将向你提供节点,并持久地将规范存储在etcd中。这里格式不是很好。但你可以在这里看到特定资源定义的自定义资源会是什么样子,我们教K8s如何创建这样的对象。
然而,仅凭你的CRD不会有任何效果,因为你需要控制器,控制器通过代码实现事件观测,例如创建了一个对象。然后,控制器可以确认这个特定的服务实例是否已经存在,确认辅助资源,需要一个服务密钥访问,以及需要创建的有状态集。因此,正如我之前所说,K8s控制器基本上将主要资源转换为次要资源的组合。在我们的示例中,到目前为止,这些资源一直是K8s的内部资源,但实际情况不一定如此。我们稍后再讨论。
如果你还想在那里开始编写Operator,Operator SDK会就Operator的成熟度级别提出建议,Operator分为五个不同的等级。我真的不确定你们是否都了解这些等级的区别。但如果从现在开始,这绝对是一个好的开端。学会正确提问,这些问题也在文档中。如果你真的构建Operator,你需要用到一些核心功能,例如在没有备份的情况下更新补丁,以及备份和恢复功能。通常这些是必备的,但用户可能会拒绝解决方案,或者他们没有解决方案。但你知道,你早晚要这么做,因此这会对你有所帮助。所以请记住,常见的陷阱,由分布式系统的编程问题引起的Bug,会有很多个,就看我们排除多少。
例如企业使用Git引发的问题。根据我的经验,总的来说,数据服务自动化最有可能的最大问题是,人们低估了实现数据服务自动化的复杂性和所需努力,表现形式包括基本生命周期操作的覆盖范围不足,以及鲁棒性和可观察性等质量特性较低。基于这点来说,了解使用的门槛是有必要的,你需要知道,自动化要做什么才能被目标受众接受。虽然这在很大程度上取决于目标受众本身,但现在我可以分享一些我学到的对我们大客户很重要的事情,但说不完,因为现在时间不够多了,这有点耗时。
接受配置更新很重要,因为应用程序开发人员能够通过自动化配置来使用数据库和应用程序。如果应用程序有特殊要求,你需要稍微调整一下数据库配置。这是真实的需求,要尽可能地利用资源。因此,你需要采访目标受众,并了解这些配置选项是否已经在自动化文档中。你需要善于根据特定需求调整自动化。如果你知道组织内有更多的开发人员,所有的云原生需求都在那里,比如,友好的可观测性,透明使用基础设施。有了K8s,在某种程度上你已经获得了。但在备份的上下文中,当你需要将备份存储在某个地方时,你通常必须将备份写入对象存储。这就是人们对S3 API的存在做出假设的地方,例如,你应该选择一些隐藏底层对象存储的抽象库。
服务实例的水平可扩展性
例如,你需要一个服务实例,你可以考虑单个Postgres用一个pod,也可以考虑集群Postgres使用异步流复制。一旦你想进行水平扩展,将副本从一个扩展到三个,就会在自动化中引入许多复杂性。因为Postgres不是那么简单做自动化服务,这让人喜欢用它举例。因此,你需要添加一个集群管理器来进行故障检测,你需要有一个主节点选举和主节点晋升逻辑来帮助你实现。
此外,如果你恰好有多个可用区域的数据中心,可以分发你的pod来使用它们,这样不会出现单个K8s节点。只要是可用区域,并建立了K8s集群,那么几乎会100%这样使用。一般来说,在整个生命周期中会重建状态集很多次,比如计划、切换、升级,或者垂直扩展使pod变大,数据被合并。我们将再次讨论备份和恢复的问题,这显然非常重要,因为应用程序开发人员无需等待平台运营商的手动干预即可恢复应用程序,这通常是最后的措施。
因此,这一切都与按需自助服务有关,到目前为止,应用程序开发人员可以自助服务,创建服务实例,然后修改它们,重新配置它们,如果服务实例碰巧出现异常,或者数据被意外删除,他们需要按应用程序的要求恢复数据,防止潜在数据丢失。
有一个需求不太明显,有时要提供最新的数据服务版本。假设Postgres的最新版本是不错的,活跃用户自然会喜欢。但对于某个组织来说,有些应用程序可能处于长期维护状态,它们不会立即使用新版本,因此,应用程序开发人员需要能够选择数据服务版本,可以使用版本号管理Operator,以支持所有自动化版本的启用和退出。这是你必须为自动化制定的政策。如果你提供太多的版本,这也会给团队提供很多的支持。但是,文档也可能会减少对你的支持。
安全性也很重要,通常要求具备加密存储、传输加密。例如,你希望被加密的磁盘上的数据没有被读取使用,从客户端发送到数据服务实例的数据也是如此,有状态集中的端口都应该加密。
服务绑定
请注意,这些服务实例不会很快消失。情况可能如此,但对于一些使用周期长的应用程序,服务实例可能会存活数年。如果你考虑生命周期用例和服务实例发生的事情,你将获得一个很长的列表,比这个列表长得多。但这给了你对可能发生的事情的第一印象,比如缩小规模和扩大规模。经历各种版本升级,将应用程序绑定到服务实例,我在这里称之为服务绑定。但也处理网络分区以及网络带宽和延迟的波动。
你必须考虑的所有这些服务绑定,它们表示应用程序与数据服务实例的连接。例如,对不同应用程序的微服务,连接到同一个服务实例,最好每个应用程序都可以访问数据服务实例,比如Postgres有一个专用的Postgres用户,这样密钥是唯一的。
为了这样的服务绑定,你必须做两件事,首先在存储凭据区创建一个密钥,然后创建实际的数据服务用户。
这有一些复杂性,我们稍后再讨论。在我看来,数据服务自动化的一个类似方面应该表示为CRD, K8s是备份和备份计划。因此,如果你想为特定服务实例创建备份,请将其描述为CRD。与作业和cron作业类似,备份计划描述了如何定期创建这些备份。
现在,从方法论的角度来看,数据服务自动化有一些原则,如果你坚持下去,你可能会从中受益。因此,在我们回到技术要点之前,你可能想考虑的原则,正如我们之前所看到的,了解你的受众,要求和期望的品质至关重要,你需要了解你是否做了一些事情,比如团队是否高度依赖特定数据源。
我见过整个公司围绕单个MongoDB实例或其中几个实例发展的公司。因此,为该案例构建Operator将与我之前介绍的上下文不同。因此,请注意,上下文是良好的数据服务自动化的核心要素之一。明智地选择数据服务也是一件好事。正如我之前提到的,比如自动化Postgres,你需要做许多工作。对于其他数据服务,你可能会遇到许可证问题,因为它们有时会更改许可证,有些甚至会远离开源,你也必须处理好这一点。因此,通常情况下,一个具有开源许可证的单一供应商支持的数据服务,例如,你按需配置,每当你更改有工具集时,都很可能会重新创建。因此,这就是通过重建失败的实例而不是修复它们来解决的问题。
你可以将已知状态的重建用作帮助你解决问题的工具,在某些情况下,首先使用操作模型是我之前解释的,在开始自动化工作前先了解数据服务,成为备份和恢复的救火员是必要和重要的,因为这是运维电话、平台运营商电话铃响起之前的最后手段,这可以避免。
因此,如果你碰巧自动化了多个数据服务,它们之间有很多协同关联。这应该纳入同一个框架。这个框架可能有代码库可以在控制器之间共享,但也可能在容器映像拥有的脚本,或者类似测试之类的配置都很重要,因为有了自动化,你基本上会写一些代码,代码将分发到许多环境,从这些环境中,你将创建许多服务实例。
对代码以及生成的服务实例都有良好的测试覆盖率,并通过集成测试指导他们完成用例,无论你怎么称呼它,都非常有趣。我的建议也有测试用例,对于那些场景,如果你知道你的自动化仍然存在弱点,请与你的客户分享测试案例的测试库,并告诉他们并允许他们在本地环境中运行这些测试。因为这会产生信任。而且,这也让他们有机会更好地了解他们可以监控哪些情况,以避免遇到问题。
不要修补上游源代码是我们使用的原则,因为我们有这么多不同的数据服务,我们做分叉,并允许拉取请求、热修复和临时热修复。主发布管理意味着从Postgres发布之日起到你的自动化发布当天,延迟应该很短。一旦你启用了自动化,你希望将该版本快速交付到目标环境中,因为只有这样,应用程序开发人员才能升级他们的实例。
示例
关于技术的话不多说了,因为时间不多了。在编写控制器时,如果你是第一次调用外部资源,这会有点棘手。在服务绑定示例中,你需要创建一个密钥,同时也需要创建一个Postgres数据库用户。
现在,有几种方法可以做到这一点。但总的来说,挑战在于,你如何确保这两种资源的一致性?答案是采用两级方法,例如,你也可以将Postgres用户表示为自定义资源,并为此有一个控制器,设置控制器只有一个目的,即协调这些Postgres用户,这使他们在你创建密钥时降低了服务绑定控制器的复杂性,该密钥是K8s和Postgres用户已经知道的次要资源。如果你已经拥有它,作为CRD,也是你的K8s集群已知的资源。作为要点之一,没有原子性保证,这里没有事务。因此,使用声明性方法更贴近习惯,在K8s中,允许数据状态不一致,因为你最终与K8s保持一致。它应该能够被周知,如果Postgres用户无法创建,我们将尝试进行协调。
保持操作的幂等性,这样如果循环再次调用相同规格的服务,应该是可能的,这样你就不会卡住,也不会在这里进入错误状态。因此,与其创建用户,创建不存在的用户,我们可以做得更多。上例只是一个值得思考的简单例子。
最后要分享一件事,缓存。如果你修改控制器中的资源,基本上是在使用K8s API对其进行修改,你的本地缓存可能不会直接和立即反映更改,这有时可能导致奇怪的行为。因此,请注意,本地缓存的更新与K8s API中对象的更新之间可能存在滞后,特别是与具有层次结构的控制器打交道。
总结一下,这次演讲的技术部分有点简短,我们围绕数据服务自动化的一般挑战以及了解目标受众的重要性进行了大量讨论。以及上下文如何影响你在编写特定Operator时的任务。我提出了一些通常要求的想法。也许这是一个好的开始,如果你写自己的Operator,问问自己,这是不是我们的应用程序开发人员所需要的?如果不是,就和他们谈谈,试着了解他们的想法。然后,试着迈出第一步。
译者介绍
张业贵,51CTO社区编辑,从事企业信息化建设多年,致力于信息集成、数据治理和人工智能应用等。
原文标题:Principles for Building K8s Operators,作者:Sylvain Kalache