Kubernetes-云原生数据管理指南-全-

Kubernetes 云原生数据管理指南(全)

原文:zh.annas-archive.org/md5/2e3ac5225cd197c79219f556af96e5ba

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

你即将踏上一场令人惊叹的冒险,深入科技行业最大变革的核心。在这次冒险中,你将成为一支由勇敢的二人组领导的同伴团队的一部分,他们几十年来勇闯数据的山川湖泊。你将与帕特里克·麦克法丁和杰夫·卡彭特一同旅行,以及一群有远见的实践者,共同追求目标:创造数据未来的力量。

读完这本书后,你将能够创造自己的新冒险,并带领他人一同走出由基础设施主导的旧世界,进入由自主体验主导的新世界。这将会非常棒。

你手中的这本书是在我们已经看到大规模系统如何想象、理解和操作的显著变化时写成的。在所有这些变化中撰写一本关于技术的书可能看起来有些痴心妄想,但这是必要的。这是一个停留在最后家园之家的时刻,我们在这里聚集认知工具、供应品和物件,这些将帮助我们前行的旅途。

我们需要尽一切可能的帮助,因为变革往往趋于加速。

短短几十年间,我们从主机走向网络,再到数据中心,最后到云端。每个新时代都感觉像一个新世界,有新规则和新机会。我们建立工具生态系统以适应时代;这些工具促进了更快的进步,我们建立了更多工具;尽管速度加快,我们还是不满足,突然间又出现了一个新的突破,宣告了另一个新时代的来临。

每个时代都需要处理相同的问题:网络、计算和数据。在每一次飞跃中,它们都需要进行转型。从主机和终端、网络和路由器、数据中心和虚拟化、云和容器;通过为新丰富水平进行架构设计,每一个时代都设立了速度、规模和单位经济的新标杆。我们努力实现更快、更大和更高效。

随着工具的变化,人们也在改变;心态必须为每一个新的丰富时代重建,从主机的高级祭司到超负荷的网络管理员,再到尽职的数据中心运营者,再到精明的云工程师。基础设施一直被视为昂贵,因为它是业务的成本而不是业务本身;每个时代的技术团队都需要专注于业务价值。

主机和事务处理、网络和文件共享、数据中心和电子商务、云和应用——每个时代的北极星都反映了当时标准的商业焦点。展望不久的未来,我们看到了新的名词:边缘、模型、预测和决策,共同推动自主业务的发展。

什么阻碍了我们进入下一个时代?我们正在共同努力实现的大解锁是什么?这是我们尚未完美解决的一个重大问题。

云时代的前半段由亚马逊的 AWS 定义,并被其他人复制:采用虚拟机和基础设施微服务的单一全球规模联合数据中心,作为一个独特的整体设计和演进。这些云不相似。它们因审美、身份模型、计费系统、API 而异。就像在主机、网络和数据中心时代的开端一样,每个云堆栈都是垂直集成的。这种锁定提供了极大的效用,却永远不会离开的代价。

云时代的后半段由 Kubernetes 及其充满活力的工具生态系统定义,所有这些都建立在同样的前提上:工作单元是一个容器,而不是虚拟机、物理服务器或主机处理器。容器是法律的代表,代表技术工作负载的标准粒度,直到下一个时代的到来。这是关于超越单一云以在任何地方获得云原生立场。Kubernetes 的突破被称为云原生,以标志着云时代的成熟状态。

容器中我们发现的魔力是什么?简而言之:我们学到了,扩展我们的思想需要缩小我们的工作单元。软件由思想构成;流动性需要缩小以适应这些思想进入更高效的单元,并且利用需要扩展以利用任何可用的基础设施。

由 Kubernetes 及其生态系统如此良好地代表的云原生宣言已经让我们走得很远,但我们现在发现自己被困在原地,还未达到我们所渴望的高峰。尽管我们取得了诸多进展,但这些进展都集中在无状态操作上。现在我们面临云原生数据时代的最后阶段。

当我们共同克服这一挑战时,我们将创造一个世界,在那里任何应用程序或模型都可以在需要时运行,以用户需求的速度,因为数据将随之流动。无论是在手机、汽车、地铁边缘、云端还是卫星上,数据都将是自描述的、可观察的、流动的和可访问的。基础设施可以变得无形,并根据开发者的梦想提供动力。

这本书是解锁这种潜力的关键。

就像任何史诗般的旅程一样,基于 Kubernetes 的云原生数据是一个渐进的启示。存储和 StatefulSets 的普通世界将引导您掌握为任何工作负载(从应用程序到分析和机器学习)架构数据基础设施的能力。然后,非凡世界的大门将向您敞开:对下一代数据管理及推动可能性艺术的开源项目的愿景。开放社区共享思想和代码,是我们实现这一未来的唯一途径。

展望未来的十年,我们不知道我们使用的技术将被命名为什么,但我们知道它们将建立在我们现在实现的理念之上。欢迎来到云原生数据的冒险,并享受旅程的乐趣!

Sam Ramji

DataStax 的首席战略官

Linux 基金会的战略顾问

前言

Kubernetes 是否准备好处理有状态工作负载?

自从云计算首次出现以来,数据基础设施(NoSQL/NewSQL、流处理、分析)和应用基础设施(Docker、Kubernetes)迅速成熟,但走的是不同的道路。在我们看来,是时候正式将这两个领域结合起来了。这不是对未来的愿望,而是已经在多个社区间的合作中发生。那些试图管理应用和数据两个独立堆栈的组织,很快会发现自己竞争上的不利处境。

在 Kubernetes 2014 年公开发布后的头几年里,人们很少质疑它是否适合处理数据和有状态工作负载。对流行智慧的一个例子可以在 Kelsey Hightower 2018 年的推特中找到:

Kubernetes 在能够运行包括数据库和消息队列在内的有状态工作负载方面取得了巨大进展,但我仍然更倾向于不在 Kubernetes 上运行它们。

在过去几年里,潮流已经转变。解决问题的工程师们接受了 Kelsey 的挑战,并将其转化为行动。从某种意义上说,Kubernetes 在处理有状态工作负载方面的成熟是不可避免的,因为需求如此之大。那些能记得关于为何数据库必须在裸金属机器上运行,或者为何不应在容器中部署数据基础设施的争论的人,可以理解这种担忧。

我们也了解到,“从未”和“尚未”之间存在巨大差异。计算、存储、网络现在被视为商品;为什么数据管理不能呢?Kubernetes 的价值主张在于降低成本和简化应用开发,这意味着数据基础设施迁移到 Kubernetes 上是不可避免的。这些变化不仅仅发生在 Kubernetes 上。正如您将看到的,数据基础设施的项目也在发生变化。

为什么写这本书

当我们的“日常工作”在 DataStax 挑战我们考虑如何在 Kubernetes 有效地部署和操作 Apache Cassandra 时,我们被卷入将有状态工作负载移至 Kubernetes 的趋势中。在开源开发精神的推动下,我们寻找其他试图用数据库和其他有状态工作负载(并且成功)进行类似尝试的从业者。我们找到了一群志同道合的人,并帮助在 2020 年发起了Data on Kubernetes Community(DoKC)。DoKC 现在是一个独立的组织,并举办了 100 多次聚会和数个线下活动。在 DoKC 聚会中的各种话题和演讲者证明了一个充满活力的社区,共同努力建立标准和最佳实践。更重要的是,我们正在一起学习,应用过去的经验,并在建设新事物时互相支持。

随着我们参与这些聚会,一系列共同的主题开始浮现。我们一次又一次地听到持久卷子系统的优点、StatefulSets 的利弊、运算符模式在使数据库操作更可管理方面的潜力,以及关于新型数据管理想法的早期提示。随着时间的推移,我们坚定地认为这个新兴的从业者社区需要一个地方,来汇集和提炼多个演示和博客文章中散落的智慧,最终形成易于消化的形式。本书就是这一过程的结果。

在云原生数据领域仍有大量工作要做,许多领域需要进一步探索,包括运算符、机器学习、数据 API、数据集的声明式管理等等。我们希望本书能为更多的书籍、博客、演示文稿和学习资源开辟大门。

本书适合谁?

本书的主要读者包括在云中设计、构建和运行应用程序的开发人员和架构师。如果您是这样的人,并且正在阅读本书,那么您很可能听说过组织普遍采用 Kubernetes,并且已经加入了这一趋势或至少在考虑中。然而,您可能也听说过关于在 Kubernetes 上运行有状态工作负载的保留意见,并且正在寻找如何解决的帮助。您来对地方了!通过阅读本书,您将获得以下内容:

  • 理解基本的 Kubernetes 资源及其如何用于组合数据基础设施

  • 欣赏工具如 Helm 和运算符如何自动化部署和操作基于 Kubernetes 的数据基础设施

  • 评估和选择数据基础设施技术,以便在您的应用程序中使用。

  • 知道如何将这些数据基础设施技术集成到您的整体堆栈中。

  • 展望未来几年将增强您基于 Kubernetes 的应用程序的新兴技术视角

还有一个较小但同样重要的受众包括核心 Kubernetes 开发人员和数据基础设施开发人员,其中许多人我们通过 DoKC 认识。我们希望创建一套共同的原则和最佳实践,作为推动 Kubernetes 核心以及在 Kubernetes 中运行的数据基础设施改进的框架。我们可以共同推动在 Kubernetes 上的数据实践前进。

对于每个人来说,了解我们在本书中的目标是直截了当的。在技术成熟和稳定的地方,我们会告诉您,但也有许多技术仍在发展中。我们将确保突出那些需要改进的领域。

如何阅读本书

本书的设计是从头到尾阅读,尤其适合那些对 Kubernetes 不太熟悉的读者。前几章介绍了 Kubernetes 的术语和概念,这些术语和概念在后面的书籍中被引用,当我们讨论更高级的主题时,这些术语和概念将显得更为重要。这本书的组织结构如下:

第一章,“云原生数据基础设施简介:持久性、流式处理和批量分析”

本章旨在通过将无状态和有状态工作负载都放在 Kubernetes 上来现代化您的云原生应用。当然,我们会这么说,但您确实应该从这里开始,因为我们定义了关键目标和术语,为所有读者提供了一个平等的基础。具体而言,我们提出了 云原生数据 的定义,并定义了云原生数据基础设施的原则,这些原则将贯穿整本书,并用于评估后续技术。

第二章,“在 Kubernetes 上管理数据存储”

在本章中,我们将看一下 Kubernetes 数据基础设施的基础领域之一:存储。我们将从容器化系统中存储是如何工作的开始,从 Docker 开始,然后转向 Kubernetes 及其持久卷子系统。我们将讨论各种可用的存储类型,包括文件、块和对象存储,以及使用本地与远程存储解决方案的权衡。

第三章,“在 Kubernetes 上的数据库的困难之路”

本章介绍了 Kubernetes 计算资源,如 Pod、Deployment 和 StatefulSet,并逐步指导您如何使用这些资源部署 MySQL 和 Apache Cassandra 等数据库。您将了解 StatefulSet 在管理分布式数据库方面的优缺点。

第四章,“使用 Helm 在 Kubernetes 上自动化数据库部署”

继续上一章的主题,我们重新审视了在 Kubernetes 上部署 MySQL 和 Cassandra,这次更加自动化地使用 Helm 软件包管理器。您还将了解 Kubernetes 资源,例如 ConfigMaps 和 Secrets,这些资源有助于配置。我们讨论 Helm 在整体 DevOps 流程和 CI/CD 工具集中的角色,以及在管理数据库操作方面的一些缺点。

第五章,“使用 Operators 在 Kubernetes 上自动化数据库管理”

本章通过介绍操作员模式来结束我们关于数据库部署的序列,并演示操作员如何帮助管理“第二天”的数据库操作。我们将探讨操作员如何扩展 Kubernetes 控制平面以管理数据库,以 Vitess(MySQL)和 Cass Operator(Apache Cassandra)为例。在此过程中,您将学习如何评估操作员的成熟度,甚至如何使用 Operator SDK 等框架构建自己的操作员。

第六章,“在 Kubernetes 栈中集成数据基础设施”

在本章中,我们开始扩展焦点,不仅仅是部署和操作数据库,还考虑如何将数据库和其他数据基础设施整合到您的整体应用堆栈中。我们将查看一个名为 K8ssandra 的项目,该项目集成了 Apache Cassandra 及其管理、安全和数据库备份工具,并提供 API 层以便更轻松地访问数据。

第七章,“Kubernetes 原生数据库”

此时,我们退后一步,总结了书的前半部分关于云原生数据管理的所学,利用这些知识来考虑一个问题:“什么是 Kubernetes 原生数据库?”这不仅仅是关于行业概念的辩论,对于那些参与选择数据基础设施以及开发该基础设施的人来说,这个讨论非常重要。

第八章,“在 Kubernetes 上流式数据”

超越持久性,我们将开始处理其余的数据基础设施,从流处理技术开始。在云原生应用程序中移动和处理数据与数据库持久性同样普遍,但在部署上需要不同的策略:安全连接端点和构建默认的弹性和韧性。在本章中,我们将使用 Apache Pulsar 和 Apache Flink 来展示构建这些重要实践的方式。

第九章,“在 Kubernetes 上的数据分析”

具有讽刺意味的是,大规模分析部署的需求构成了今天 Kubernetes 中许多方法论的起源故事的一部分——即编排和资源管理。环环相扣,如今在许多组织中,在 Kubernetes 中运行分析已成为头等大事。我们重点介绍 Apache Spark 的变化,为您的使用案例提供一个先导,并使用 Dask 和 Ray 项目来研究 Kubernetes 中分析的最前沿。

第十章,“机器学习和其他新兴用例”

AI 和机器学习的主题已经处于基础设施的前沿。在过去几年中启动的项目可能首先在 Kubernetes 上启动,这是一个值得考虑的有趣现象。还有其他类型的项目首先考虑云原生,并为数据的未来提供一些方向性。本章旨在调查这些项目,并广泛提供作为在云原生数据上进行前进时考虑的思想和方法。

第 11 章,“将数据工作负载迁移到 Kubernetes”

如果您不将书中获得的所有知识付诸实践,那么这些知识将毫无用处。在本章中,我们强调了前几章的关键教义,并提出了一个框架,即人员、流程和技术变革,帮助您成功地将有状态的工作负载迁移到 Kubernetes。最后,我们展望了您的组织在不久的将来可能会拥有的数据基础设施的愿景。

在 Kubernetes 上管理数据的学科是一个新兴的学科,特别是在某些领域发生了很多变化。我们承认,像任何技术书籍一样,本书代表了特定时间点上可用知识的一个快照——在本例中为 2022 年末。编写关于快速发展主题的书籍的真正危险在于信息变得多快地变得无关紧要。

为了最好地应对这一现实,本书中应用了一个通用的公式:我们提供了大量示例,但强调基础知识。随着我们深入研究,我们检查的技术变得越来越不成熟。与其寻找复制粘贴答案或一刀切架构,我们鼓励您提取适用于您独特用例的核心原则。

特别是,自从第二章到第五章涉及到已经确立的主题,你将在这些章节中找到更深入的解释和实际示例。第八章到第十章探讨了数据基础设施,这些基础设施在 Kubernetes 上的部署仍在发生较大变化。在这些情况下,我们更频繁地指向第三方学习资源,以确保您拥有最新的体验。本书的初衷是鼓励您与他人分享您发现的新资源,以便我们共同进步。

本书中使用的约定

本书中使用的以下印刷约定:

Italic

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

Constant width

用于程序清单,以及在段落中引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

Constant width bold

显示用户需按字面输入的命令或其他文本。

常量宽度斜体

显示应由用户提供的值或由上下文确定的值。

提示

此元素表示提示或建议。

注意

此元素表示一般注释。

警告

此元素指示警告或注意事项。

使用代码示例

附加材料(代码示例、练习等)可在https://github.com/data-on-k8s-book/examples下载。

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在自己的程序和文档中使用它。除非您复制了大部分代码,否则无需征得我们的许可。例如,编写一个使用本书多个代码块的程序并不需要许可。销售或分发 O’Reilly 书籍中的示例则需要许可。引用本书并引用示例代码来回答问题也无需许可。将本书示例代码的大部分内容合并到您产品的文档中则需要许可。

我们感谢,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Managing Cloud Native Data on Kubernetes by Jeff Carpenter and Patrick McFadin (O’Reilly)。Copyright 2023 Jeffrey Carpenter and Patrick McFadin, 978-1-098-11139-7。”

如果您认为您使用的代码示例超出了合理使用范围或上述许可,欢迎随时与我们联系permissions@oreilly.com

O’Reilly Online Learning

注意

超过 40 年来,O’Reilly Media提供技术和业务培训、知识和见解,帮助公司取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深入学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。欲了解更多信息,请访问https://oreilly.com

如何联系我们

请将关于本书的评论和问题发送至出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为本书建立了一个网页,列出勘误、示例和任何额外信息。您可以访问https://oreil.ly/cloud-native-data-Kubernetes查看。

发送电子邮件至 bookquestions@oreilly.com 提出评论或询问有关本书的技术问题。

关于我们的书籍和课程的新闻和信息,请访问 https://oreilly.com

在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media

在 Twitter 上关注我们:https://twitter.com/oreillymedia

在 YouTube 观看我们:https://youtube.com/oreillymedia

致谢

首先要感谢 Jess Haberman,她从我们第一次交谈就相信这本书的概念,并努力使其实现,以及我们的编辑 Jill Leonard,她的持续鼓励和智慧建议。

本书的一个关键特色是包含基于我们与专业技术人员和实践者的对话而形成的侧边栏。我们尽可能少地进行编辑,让他们的话语自己说话。因此,我们向那些与我们分享他们的时间和见解的人们表示衷心的感谢:Rick Vasquez、Kiran Mova、Maciej Szulik、John Sanda、Deepthi Sigireddi、Umair Mufti、Irfan Ur Rehman、Dongxu (Ed) Huang、Jake Luciani、Jesse Anderson、Josh van Leeuwen、Holden Karau、Dean Wampler、Theofilos Papapanagiotou、Willem Pienaar、Xiaofan Luan、Josh Patterson、Adi Polak 和 Craig McLuckie。

这些专家不仅贡献了他们的话语,还影响了我们研究的方向和我们在这里讨论的技术选择。Deepthi、Jesse、Umair 和 Rick 也兼任本书的技术审阅员。我们还感谢其他技术审阅员的见解:Wei Deng、Ali Ok、Aaron Morton 和 Noah Gift。

Kubernetes 数据社区(DoKC)对这一努力产生了巨大的启发,我们特别感谢 Bart Farrell、Demitrios Brinkmann 和 Melissa Logan,他们将我们与许多其他社区成员联系起来,并为我们的工作给予了支持和鼓励。我们还要特别感谢 Evan Powell,他通过找到 Demetrios 并资助最初的聚会,使 DoKC 得以诞生。那是点燃了许多美好事物的火花。

Sam Ramji 对本书产生了重大影响,不仅撰写了前言,还通过提醒我们“你必须付出努力才能有自己的见解”挑战了我们的思维过程。Sam 总是愿意打电话、介绍或者在喝啤酒时分享想法。

这本书诞生于全球大流行初期,并在不确定、挑战和复苏的季节中得到培育,无论是在全球范围还是个人层面上。我们非常感谢许多朋友和家人在这段时期与我们同行,并提醒我们像“书进展如何?”这样的问题或者仅仅是一个简单的“你好吗?”的力量。

第一章:云原生数据基础设施介绍:持久性、流处理和批处理分析

您是否致力于解决数据问题,并发现自己需要现代化?您的云原生应用程序是否仅限于使用微服务和服务网格?如果您在 Kubernetes 上部署应用程序(有时简称为“K8s”),但没有涉及数据,那么您还没有完全拥抱云原生。您的应用程序的每个元素都应体现云原生的规模、弹性、自愈和可观察性原则,包括您如何处理数据。

数据工程师主要关注的是有状态服务,这也将是我们关注的重点:提升您在 Kubernetes 中管理数据的技能。通过阅读本书,我们的目标是丰富您向云原生数据迁移的旅程。如果您刚开始接触云原生应用程序,现在是将技术栈的每一个方面纳入的最佳时机。这种融合是我们将如何消费云资源的未来。

那么,我们一起创造的这个未来是什么样的呢?

太长时间以来,数据一直存在于 Kubernetes 之外,导致了大量额外的工作和复杂性。我们将详细探讨其中的合理原因,但现在是将整个技术栈结合起来,以更快速、按需规模构建应用程序的时候了。基于当前的技术,这是完全可能的。我们已经远离了部署单独服务器的过去,转向了可以部署整个虚拟数据中心的未来。曾经需要数月甚至数年的开发周期现在可以在几天甚至几周内完成。开源组件现在可以结合到一个 Kubernetes 上的单一部署中,这种部署从你的笔记本到最大的云服务提供商都可以移植。

开源贡献在其中占据了重要地位。除非另有说明,否则 Kubernetes 和本书讨论的项目均采用 Apache License 2.0,这是有充分理由的。如果我们构建能够在任何地方运行的基础设施,我们需要一个能够给予我们选择自由的许可模型。开源既是免费的(如啤酒那样的自由),也是自由的(如自由那样的自由),在构建基于 Kubernetes 的云原生应用程序时,这两者都至关重要。开源已经成为基础设施革命的推动力量,而这也不例外。

这就是我们正在构建的内容:完全实现的 Kubernetes 应用程序的接近未来现实。最后一个组成部分是最重要的,那就是你。作为本书的读者,你们中的一员将成为创造这一未来的人之一。作为工程师,创造是我们的工作。我们不断地重新发明我们部署复杂基础设施的方式,以应对增加的需求。当 1960 年为美国航空公司上线了第一个电子数据库系统时,一小群工程师确保它在线并全天候运行。进步使我们从大型机到小型计算机,再到微型计算机,最终到我们今天的舰队管理。现在,同样的进步正在继续进入云原生和 Kubernetes。

本章将探讨云原生应用程序的组成部分,运行有状态工作负载的挑战以及本书涵盖的基本领域。首先,让我们转向构成数据基础设施的构建模块。

基础设施类型

在过去的 20 年里,基础设施的方法慢慢分叉成两个领域,反映了我们如何部署分布式应用程序(如图 1-1 所示)。

无状态服务

这些服务仅在活动请求的即时生命周期中维护信息,例如,向移动客户端发送格式化的购物车信息的服务。一个典型的例子是执行购物车业务逻辑的应用服务器。然而,购物车内容的信息存储在这些服务之外。它们只需要在请求到响应的短时间内在线。用于提供服务的基础设施可以轻松地随需求增减,对整体应用的影响很小,可以在需要时扩展计算和网络资源。由于我们不在个别服务中存储关键数据,因此该数据可以快速创建和销毁,几乎不需要协调。无状态服务是分布式系统中的关键架构元素。

有状态服务

这些服务需要维护从一个请求到下一个请求的信息。磁盘和内存存储数据以供多个请求使用。一个例子是数据库或文件系统。由于通常需要复制以实现高可用性,扩展有状态服务更加复杂。这造成了对一致性和保持数据在副本之间同步的机制的需求。这些服务通常具有不同的扩展方法,包括垂直和水平扩展。因此,它们需要不同的操作任务集合,与无状态服务不同。

无状态 vs. 有状态服务

图 1-1. 无状态与有状态服务

除了信息存储方式外,我们还看到了向开发采纳自动化基础设施部署的转变。这些最新的进展包括以下内容:

  • 物理服务器已经被易于部署和维护的虚拟机(VMs)所取代。

  • 虚拟机(VMs)已经简化并专注于容器特定的应用程序。

  • 容器使基础设施工程师能够将应用程序的操作系统要求打包成一个可执行文件。

容器的使用无疑增加了部署的一致性,这使得批量部署和运行基础设施变得更加容易。像 Kubernetes 这样的容器编排系统应运而生,并显著促进了容器的爆发式增长。这证明了它解决问题的能力。官方文档这样描述 Kubernetes:

Kubernetes 是一个便携式、可扩展的开源平台,用于管理容器化的工作负载和服务,支持声明性配置和自动化。它拥有一个庞大而快速增长的生态系统。Kubernetes 的服务、支持和工具广泛可用。

Kubernetes 最初设计用于无状态工作负载,传统上这是它最擅长的。Kubernetes 已经发展成为以云原生方式“构建平台的平台”的代名词。然而,有一个合理的论点认为完整的云原生解决方案必须考虑数据。这本书的目标就是探索如何在 Kubernetes 上构建云原生数据解决方案。但首先,让我们来解释一下“云原生”的含义。

什么是云原生数据?

让我们开始定义云原生数据的各个方面,这些方面有助于我们最终得出一个定义。首先,让我们从Cloud Native Computing Foundation (CNCF)的定义开始:

云原生技术使组织能够在公共、私有和混合云等现代动态环境中构建和运行可扩展的应用程序。容器、服务网格、微服务、不可变基础设施和声明性 API 是这种方法的典范。

这些技术使得系统松耦合,具备弹性、可管理性和可观测性。结合健壮的自动化,它们使工程师能够频繁且可预测地进行高影响的变更,减少重复劳动。

请注意,这一定义描述了一个目标状态、理想特性以及体现这些特性的技术示例。基于这一正式定义,我们可以综合出区分云原生应用程序与其他类型部署方式在处理数据方面特质的品质。让我们更详细地看看这些特质:

可扩展性

如果一个服务可以用一定的资源产生一单位的工作量,增加更多的资源应该能够增加服务的工作量。可扩展性描述了服务利用额外资源产生额外工作的能力。理想情况下,服务应该能够在有无限计算、网络和存储资源的情况下无限扩展。对于数据来说,这意味着在无需停机的情况下进行扩展。传统系统在增加新资源时需要维护期间,期间所有服务必须关闭。随着云原生应用程序的需求增加,停机已经不再可接受。

弹性

虽然扩展是为了满足需求而增加资源,弹性则是在不再需要这些资源时释放它们的能力。可扩展性和弹性的差异在图 1-2 中得到突显。弹性也可以称为按需基础设施。在私有数据中心等受限环境中,这对于共享有限资源至关重要。对于按资源使用付费的云基础设施而言,这是避免支付无需运行的服务费用的一种方法。在管理数据时,这意味着我们需要能力来回收存储空间并优化我们的使用,例如将旧数据移到更便宜的存储层次。

比较可扩展性和弹性

图 1-2. 比较可扩展性和弹性

自愈

发生不良事件时,你的基础设施将如何响应?自愈基础设施将重新路由流量,重新分配资源,并维持服务水平。随着部署更大更复杂的分布式应用程序,这成为云原生应用程序日益重要的特性。这可以避免你在凌晨三点接到电话的情况。对于数据来说,这意味着我们需要检测数据问题的能力,例如缺失数据和数据质量问题。

可观察性

如果某些事物发生了而你没有监控到它,那它是否发生了?不幸的是,答案不仅是肯定的,而且这可能是一个更糟糕的情况。分布式应用程序极具动态性,对每个服务的可见性至关重要,以维持服务水平。相互依赖关系可能导致复杂的故障场景,这就是为什么可观察性是构建云原生应用程序的关键部分。在数据系统中,常见的数据量需要高效的监控基础设施的流动和状态。在大多数情况下,对问题的早期警告可以帮助操作员避免昂贵的停机时间。

在所有先前的定义都已就绪的情况下,让我们尝试一个能表达这些属性的定义:

云原生数据方法赋予那些采用云原生应用程序方法的组织综合整合数据的能力,而不是使用传统的人员、流程、技术遗留方式,使数据能够弹性地进行扩展和缩减,并促进可观察性和自愈。这通过容器化数据、声明式数据、数据 API、数据网格和云原生数据基础设施(即以云原生应用程序架构的数据库、流处理和分析技术)得以体现。

为了使数据基础设施与我们应用程序的其他部分保持一致,我们需要整合每一部分。这包括规模自动化、弹性和自愈。需要 API 来解耦服务并增加开发速度,还能帮助你观察应用程序整个堆栈以做出关键决策。总体而言,你的应用程序和数据基础设施应该表现为一个整体单元。

更多基础设施,更多问题

无论你的基础设施是在云端、本地,还是两者兼有(通常称为混合),你可能需要大量时间进行手动配置。在编辑器中输入内容和进行详细的配置工作需要对每种技术有深入的了解。在过去的 20 年里,DevOps 社区在代码和部署基础设施的方式上都取得了显著进展。这是现代基础设施演变的关键步骤。DevOps 让我们保持在应用程序所需规模的前沿,但勉强而已。可以说,完全编写脚本化一个单一数据库服务器部署需要相同的知识量。只是现在我们可以使用模板和脚本做到一百万次(如果需要的话)。缺失的是组件之间的连接性和整个应用程序堆栈的整体视图。让我们一起解决这个问题。(预示:这是一个需要解决的问题。)

就像任何良好的工程问题一样,让我们将其分解成可管理的部分。第一个是资源管理。无论我们已经开发了多少种规模工作的方式,基本上,我们都在尽可能高效地管理三样东西:计算、网络和存储,如图 1-3 所示。这些是每个应用程序所需的关键资源,也是在增长过程中消耗的燃料。毫不奇怪,这些资源也带有运行应用程序的货币成分。当我们明智地使用资源时,我们会得到回报,如果不这样做,我们将付出昂贵的代价。无论你在哪里运行应用程序,这些都是最基本的单元。在本地时,一切都是购买和拥有。在使用云时,我们是租赁的。

云应用程序的基本资源:计算、网络和存储

图 1-3. 云应用程序的基本资源:计算、网络和存储

问题的第二部分是将整个堆栈作为单个实体运行。DevOps 提供了许多工具来管理单个组件,但它们之间的连接组织为极高的效率提供了潜力——类似于如何为桌面打包应用程序,但在数据中心规模上运作。这种潜力推动了一个围绕云原生应用程序的整个社区。这些应用程序与我们过去部署的应用程序类似。不同之处在于,现代云应用程序不是一个具有业务逻辑的单一进程,而是许多容器化进程的复杂协调,需要安全和可靠地进行通信。存储必须匹配应用程序当前的需求,同时要意识到它如何促进应用程序的稳定性。当我们考虑在没有在同一个控制平面中管理数据的情况下部署无状态应用程序时,这听起来是不完整的,因为确实如此。将应用程序组件分解为不同的控制平面会增加更多复杂性,这与云原生的理念相悖。

Kubernetes 引领潮流

正如前面提到的,DevOps 自动化使我们始终保持在满足规模需求的前沿。容器化产生了对更好编排的需求,而 Kubernetes 已经响应了这一需求。对于操作员来说,在部署文件中描述完整的应用程序堆栈使得基础设施可以重现和移植。这是因为 Kubernetes 已经远远超出了 DevOps 工具包中流行的简单部署管理。Kubernetes 控制平面应用部署要求到底层的计算、网络和存储,以管理整个应用程序基础设施生命周期。即使底层硬件发生变化,您的应用程序的期望状态也会得到维持。现在我们不再部署虚拟机,而是部署虚拟数据中心作为一个完整的定义,如 图 1-4 所示。

Kubernetes 的普及已经超越了所有其他在 DevOps 中使用的容器编排工具。它已经取代了我们部署基础设施的其他方式,并且显示出没有放缓的迹象。然而,早期采用主要集中在无状态服务。

在容器和 Kubernetes 的迁移之前,大规模管理数据基础设施是一个长期存在的问题。像数据库这样的有状态服务采取了与 Kubernetes 采用曲线平行的不同路径。许多专家建议 Kubernetes 不适合运行有状态服务,这些工作负载应保持在 Kubernetes 之外。这种方法在某些时候有效,但在其他时候则不然,许多相同的专家现在正在推动 Kubernetes 中所需的变化,以统一整个堆栈。

从虚拟服务器到虚拟数据中心

图 1-4. 从虚拟服务器到虚拟数据中心

那么,有哪些有状态服务的挑战呢?为什么使用 Kubernetes 部署数据基础设施很困难?让我们逐个考虑基础设施的每个组件。

在 Kubernetes 上管理计算

在数据基础设施中,依赖摩尔定律使升级成为了一个常规事件。摩尔定律预测计算能力每 18 个月翻一番。如果您的需求每 18 个月翻一番,您可以通过更换硬件来跟上。最终,原始计算能力开始趋于平稳。供应商开始添加更多的处理器和核心,以跟上摩尔定律,导致单服务器资源共享与虚拟机和容器,使我们能够利用留在物理服务器岛屿上的大量计算能力。Kubernetes 通过将整个数据中心视为跨多个物理设备的一个大资源池,扩展了计算资源管理的范围。

在数据世界中,与其他服务共享计算资源有些禁忌。数据工作负载通常资源密集,一个服务影响另一个服务的潜力(被称为 noisy neighbor problem)导致了保持它们与其他工作负载隔离的政策。这种一刀切的方法排除了更大利益的可能性。首先是假设所有数据服务的资源需求相同。Apache Pulsar 的代理比 Apache Spark 的工作节点要少得多,而且二者都不同于用于在线分析处理(OLAP)报告的大型 MySQL 实例。其次,将底层硬件与运行应用程序解耦使操作员拥有很多被低估的灵活性。需要规模化、弹性和自愈的云原生应用程序需要 Kubernetes 所能提供的。数据也不例外。

在 Kubernetes 上管理网络

构建分布式应用程序本质上需要可靠和安全的网络。云原生应用程序增加了添加和减少服务的复杂性,使动态网络配置成为新的需求。Kubernetes 在您的虚拟数据中心内自动管理所有这些。当新服务上线时,就像一个虚拟网络团队迅速行动起来。分配 IP 地址,创建路由,添加 DNS 条目,虚拟安全团队确保防火墙规则得到执行,并在需要时提供传输层安全(TLS)证书以进行端到端加密。

数据基础设施往往比微服务之类的东西不那么动态。数据库通常采用固定 IP 和主机名。像 Apache Flink 这样的分析系统在处理上是动态的,但硬件地址分配是固定的。服务质量通常位于需求列表的顶部,因此对专用硬件和专用网络的需求使管理员们对 Kubernetes 失去了兴趣。

数据基础设施在 Kubernetes 中运行的优势不再仅限于过去的需求,更多地关注未来的需求。动态扩展资源可能会导致一系列依赖关系的产生。自动化是保持干净高效网络的唯一途径,而这些网络是分布式无状态系统的生命线。云原生应用的未来将包括更多组件和新挑战,例如应用程序将在何处运行。我们可以在之前对延迟和吞吐量的关注之外,增加法规合规性和数据主权问题。Kubernetes 网络的声明性质使其非常适合用于数据基础设施。

在 Kubernetes 上管理存储

任何提供大数据量持久性或分析的服务都需要合适类型的存储设备。早期版本的 Kubernetes 认为存储是堆栈的基本商品部分,并假定大多数工作负载是短暂的。对于数据来说,这是一个巨大的不匹配——你不能让你的 Postgres 数据文件在每次容器移动时被删除。另外,在最初阶段,底层块存储从高性能 NVMe 硬盘到旧的 5400 转每分钟的旋转硬盘不等,你并不总能确定将会得到何种类型的硬件。幸运的是,这些问题在过去几年中已成为 Kubernetes 的重点关注,得到了显著改进。

借助 StorageClasses 等功能的增加,可以针对性能、容量或两者都有的具体要求进行处理。通过自动化,我们可以避免因性能或容量不足而导致的问题。避免意外是容量管理的领域——无论是初始化所需的容量还是在需要时扩展。当存储容量不足时,所有事情都会陷入停滞。

将 Kubernetes 的分布式特性与数据存储耦合,为自愈开辟了更多可能性。自动化的备份和快照使您可以应对潜在的数据丢失情况。将计算和存储放在一起可以最小化硬件故障风险,并在不可避免的故障发生时自动恢复到期望的状态。所有这些都使得 Kubernetes 的数据存储方面更加具有吸引力。

云原生数据组件

现在我们已经定义了云原生应用程序中消耗的资源,让我们澄清一下支持它们的数据基础设施类型。与其列出每种可能的产品,我们将它们分为具有相似特性的大类:

持久性

当我们谈论数据基础设施时,这可能是您首先考虑的类别。这些系统存储数据,并通过某种查询方法提供访问:如 MySQL 和 Postgres 等关系型数据库,以及 Apache Cassandra 和 MongoDB 等 NoSQL 系统。由于其严格的资源需求和高可用性要求,这些系统一直是迁移到 Kubernetes 的最后抵抗者。数据库通常对运行中的应用程序至关重要,并且是系统的每个其他部分的核心。

数据流

数据流的最基本功能是促进数据从一点到另一点的高速移动。流处理系统根据使用案例提供各种交付语义。在某些情况下,数据可以传递给多个客户端,或者在需要严格控制时仅传递一次。流处理的进一步增强是处理:在传输过程中修改或增强数据。对数据更快速见解的需求推动了流处理分析成为重要的关键任务,与持久性系统在重要性上趋于接近。移动数据的流系统的示例包括 Apache Flink 和 Apache Kafka,而处理系统的示例包括 Apache Flink 和 Apache Storm。

批处理分析

在大数据中,第一个问题之一是分析大量数据集以获取见解或重新利用为新数据。Apache Hadoop 是第一个大规模批处理分析系统,设定了使用大量计算和存储协调生成复杂分析过程结果的期望。通常,这些作业分布在整个集群中,这在 Spark 中是常见的。这些系统由于所需资源的大量而更容易受到成本的担忧。编排系统通过智能分配帮助缓解成本。

展望未来

云原生数据的未来充满吸引力。我们如何利用今天可用的资源以及未来可以拥有的资源取决于我们:负责数据基础设施的社区。正如我们一直所做的,我们面临新的挑战并应对。在这里,每个人都有很多事情要做,但结果可能非常惊人,并再次提升标准。

Rick 的观点特别是关于数据库的,但我们可以推广他对我们在 Kubernetes 上运行数据基础设施的行动呼吁。与在物理服务器上部署数据应用不同,引入 Kubernetes 控制平面需要与其运行的服务进行对话。

为革命做好准备

作为创建和运行数据基础设施的工程师,我们必须为即将到来的进步做好准备,无论是在操作方式上还是在我们对数据基础设施角色的思维方式上。接下来的章节描述了您可以为在 Kubernetes 上运行的云原生数据的未来做好准备的内容。

采纳 SRE 思维方式

随着云原生方法的采用,站点可靠性工程(SRE)的角色已经增长。如果我们打算让基础设施趋同,作为数据基础设施工程师,我们必须学习新技能并采纳新实践。让我们从SRE 的维基百科定义开始:

站点可靠性工程是一组原则和实践,它将软件工程的各个方面应用于基础设施和运维问题。其主要目标是创建可扩展和高度可靠的软件系统。站点可靠性工程与 DevOps 密切相关,后者是结合了软件开发和 IT 运维的一组实践,而 SRE 也被描述为 DevOps 的一种具体实现。

部署数据基础设施主要关注特定的部署组件——“是什么”。例如,你可能专注于大规模部署 MySQL 或使用 Spark 分析大量数据。采用 SRE 思维意味着超越你正在部署的“是什么”,更关注“如何”。每个部分如何相互作用、所需访问权限(包括安全性)以及每个方面的可观察性,以确保满足服务水平,这种整体部署视角。

如果你目前的主要或次要角色是数据库管理员(DBA),那么现在是进行转型的最佳时机。LinkedIn 的趋势显示,数据库管理员角色的年度下降,而 SRE 角色则大幅增加。掌握运行关键数据库基础设施所需的技能,对于管理云原生数据至关重要。这些需求包括以下内容:

  • 可用性

  • 延迟

  • 变更管理

  • 应急响应

  • 容量管理

要更好地适应整个应用程序更重要的责任,需要将新技能添加到此列表中。这些可能是你已经具备的技能,但它们包括以下内容:

CI/CD 管道

接受从代码库到生产的大局观。在组织中,没有什么比加速应用程序开发更快的了。持续集成(CI)将新代码构建到应用程序堆栈中,并自动化所有测试以确保质量。持续交付(CD)获取完全测试和认证的构建,并自动部署到生产环境中。结合使用(管道),组织可以大幅提高开发速度和生产力。

可观察性

DevOps 从业者喜欢区分“什么”(你实际部署的服务)和“如何”(部署该服务的方法)。每个有基础设施经验的人都熟悉监控。在 DevOps 的“什么”部分,你监控的属性可以让你知道你的服务是否健康,并提供诊断问题所需的信息。可观察性将监控扩展到应用程序的“如何”,通过考虑整体来考虑一切——例如,跟踪高度分布式应用程序中延迟源头,洞察数据在系统中传输时经过的每个跳跃。

熟悉代码

当大型分布式应用程序出现问题时,问题并不总是进程失败。在许多情况下,问题可能是代码中的错误或微妙的实现细节。作为负责整个应用程序健康的人,你需要理解在提供的环境中执行的代码。正确实施的可观察性将帮助你找到问题,其中包括软件的仪器化。SREs 和开发团队需要进行清晰且定期的沟通,而代码是共同的基础。

拥抱分布式计算

在 Kubernetes 中部署应用程序意味着接受分布式计算所提供的一切。当你习惯于单一系统思维时,这种过渡可能很困难,主要是在期望和理解问题出现位置的思维转变方面。例如,每个进程都包含在单一系统中时,延迟将接近于零。这不是你需要管理的。CPU 和内存资源是那里的主要关注点。在 1990 年代,Sun Microsystems 在不断增长的分布式计算领域中处于领先地位,并发布了这份关于分布式计算的八个常见误解清单。

  • 网络是可靠的。

  • 延迟为零。

  • 带宽是无限的。

  • 网络是安全的。

  • 拓扑结构不会改变。

  • 只有一个管理员。

  • 运输成本为零。

  • 网络是同质的。

每个错误观念的背后肯定都有一个开发者的故事,他们做出了错误的假设,得到了意外的结果,并花费了无数小时来解决错误的问题。在长期来看,拥抱分布式方法是值得的。它们使我们能够构建大规模应用程序,并将继续如此很长一段时间。挑战值得回报,对于那些每天都在做这件事的人来说,这也可以很有趣!Kubernetes 应用程序将测试这些错误观念的每一个,考虑到其默认的分布式特性。在计划部署时,考虑诸如从一个地方到另一个地方的传输成本或延迟影响是非常重要的。它们将节省大量浪费的时间和重新设计的成本。

云原生数据基础设施的原则

作为工程专业人员,我们寻求标准和最佳实践来构建。为了使数据尽可能“云原生”,我们需要拥抱 Kubernetes 提供的一切。真正的云原生方法意味着采用 Kubernetes 设计范式的关键元素,并在此基础上构建。一个完全的云原生应用程序包括数据,必须能够在 Kubernetes 上有效运行。让我们探索一些指引方向的 Kubernetes 设计原则。

原则 1:利用计算、网络和存储作为商品化的 API

云计算成功的关键之一在于将计算、网络和存储作为可以通过简单 API 进行配置的资源进行商品化。考虑以下 AWS 服务示例:

计算

我们通过 Amazon Elastic Compute Cloud(EC2)和 Auto Scaling groups(ASG)分配虚拟机。

网络

我们使用 Elastic Load Balancers(ELB)、Route 53 和虚拟私有云(VPC)对流量进行管理。

存储

我们使用诸如 Simple Storage Service(S3)用于长期对象存储,或 Elastic Block Store(EBS)卷用于计算实例的持久化数据。

Kubernetes 提供自己的 API 来为容器化应用程序世界提供类似的服务:

计算

Pods、Deployments 和 ReplicaSets 管理计算硬件上容器的调度和生命周期。

网络

Services 和 Ingress 暴露容器的网络接口。

存储

PersistentVolumes(PV)和 StatefulSets 允许将容器灵活关联到存储。

Kubernetes 资源促进了应用在 Kubernetes 发行版和服务提供商之间的可移植性。对数据库意味着什么?它们只是利用计算、网络和存储资源来提供数据持久性和检索服务的应用程序:

计算

数据库需要足够的处理能力来处理传入的数据和查询。每个数据库节点部署为一个 Pod,并分组到 StatefulSets 中,使 Kubernetes 能够管理扩展和缩减。

网络

数据库需要暴露数据和控制的接口。我们可以使用 Kubernetes Services 和 Ingress controllers 来暴露这些接口。

存储

数据库使用指定 StorageClass 的 PersistentVolumes 来存储和检索数据。

将数据库视为其计算、网络和存储需求,有助于消除在 Kubernetes 上部署时涉及的大部分复杂性。

原则 2:分离控制平面和数据平面

Kubernetes 推广了控制平面和数据平面的分离。Kubernetes API 服务器是控制平面的前门,提供了数据平面用于请求计算资源的接口,而控制平面管理将这些请求映射到基础设施即服务(IaaS)平台的详细信息。

我们可以将同样的模式应用到数据库中。例如,数据库的数据平面包括为客户端暴露的端口,对于分布式数据库,还包括用于数据库节点之间通信的端口。控制平面包括数据库提供的用于管理和度量收集的接口,以及执行操作维护任务的工具。大部分甚至应该通过 Kubernetes 运营商模式来实现。运营商定义自定义资源(CRD)并提供控制循环来观察这些资源的状态,采取行动使其朝向期望的状态,有助于使用特定于领域的逻辑扩展 Kubernetes。

原则 3:使可观察性变得简单

可观测系统的三大支柱是日志、度量和追踪。Kubernetes 通过将每个容器的日志暴露给第三方日志聚合解决方案,为我们提供了一个很好的起点。度量、追踪和可视化都有多种解决方案可供选择,在本书中我们将探讨其中几种。

原则 4:使默认配置安全

Kubernetes 网络默认安全:必须显式地暴露端口才能从 Pod 外部访问。这为数据库部署设定了有价值的先例,迫使我们仔细考虑每个控制平面和数据平面接口如何被暴露,以及哪些接口应通过 Kubernetes 服务暴露。Kubernetes 还提供了用于秘密管理的设施,可用于共享加密密钥和配置管理账户。

原则 5:优先选择声明式配置

在 Kubernetes 的声明式方法中,您可以指定资源的期望状态,控制器将操作底层基础设施以实现该状态。数据基础设施的操作员可以智能地管理如何扩展,例如,在扩展额外节点时决定如何重新分配分片或分区,或选择何时移除节点以弹性地缩减规模。

下一代操作员应能够为存储数据大小、每秒事务数或两者都指定规则。也许我们将能够指定最大和最小的集群大小,以及何时将较少使用的数据移动到对象存储。这将有助于提升数据基础设施的自动化和效率。

概述

此刻,我们希望你已经准备好迎接前面页面上的激动人心的旅程。迁移到云原生应用必须包括数据,在这方面,我们将利用 Kubernetes 来包含无状态有状态服务。本章涵盖了能够弹性扩展并抵御系统故障引起的任何停机的云原生数据基础设施,以及如何构建这些系统。作为工程师,我们必须拥抱云原生基础设施的原则,并在某些情况下学习新技能。恭喜你——你已经开始了构建云原生应用未来的美妙旅程。翻页吧,让我们出发!

第二章:管理 Kubernetes 上的数据存储

没有无状态架构这种事。所有应用程序都在某处存储状态。

Alex Chircop,CEO,StorageOS

在前一章中,我们描绘了一个可能的不远未来场景,强大的、有状态的、数据密集型应用程序在 Kubernetes 上运行。为了达到这一目标,我们需要持久性、流处理和分析的数据基础设施。为了构建这个基础设施,我们需要利用 Kubernetes 提供的原语来帮助管理云计算的三种商品:计算、网络和存储。在接下来的几章中,我们将开始研究这些原语,从存储开始,以看看它们如何结合起来创建我们需要的数据基础设施。

为了响应 Alex Chircop 提出的观点,所有应用程序都必须将它们的状态存储在某处,这也是为什么我们将在本章节专注于 Kubernetes 提供的基本抽象,用于与存储互动。我们还将探讨由存储供应商和开源项目提供的新兴创新,这些项目为 Kubernetes 创建存储基础设施,体现了云原生的原则。

让我们从一般容器化应用程序中管理持久性开始探索,并将其作为我们研究 Kubernetes 上数据存储的起点。

Docker、容器和状态

在分布式云原生应用程序中管理状态的问题并不局限于 Kubernetes。快速搜索将显示,有状态工作负载在其他容器编排平台(如 Mesos 和 Docker Swarm)上也是一个关注的领域。这部分原因与容器编排的性质有关,也与容器本身的性质有关。

首先,让我们考虑容器。容器的关键价值主张之一是它们的短暂性质。容器被设计为可丢弃和可替换的,因此它们需要快速启动,并尽可能少地使用资源进行开销处理。因此,大多数容器镜像都是从包含精简、基于 Linux 的开源操作系统(如 Ubuntu)的基础镜像构建而来,这些镜像能够快速引导,并且仅包含容器化应用程序或微服务的必要库。顾名思义,容器被设计为自包含的,将所有依赖项包含在不可变的镜像中,而其配置和数据则是外部化的。这些特性使得容器具有可移植性,可以在任何兼容的容器运行时环境中运行。

如图 2-1,容器的资源开销比传统的虚拟机少,传统虚拟机每个都有一个客户操作系统,使用hypervisor layer将系统调用映射到底层宿主操作系统。

比较容器化与虚拟化

图 2-1. 将容器化与虚拟化进行比较

尽管容器使应用程序更加可移植,但使它们的数据可移植却是一个更大的挑战。因为容器本身是临时的,任何要在容器生命周期之外持久存在的数据必然需要外部存储。对于容器技术来说,关键特性是提供链接到持久存储的机制;对于容器编排技术来说,关键特性是能够有效地安排容器访问持久存储。

在 Docker 中管理状态

让我们来看看最流行的容器技术 Docker,看看容器如何存储数据。在 Docker 容器的视角下, 是一个可以支持读写或只读访问的目录。Docker 支持将多个数据存储挂载为卷。我们将介绍几个选项,以便稍后在 Kubernetes 中寻找它们的等效项。

绑定挂载

创建卷的最简单方法是将容器中的目录绑定到主机系统上的目录。这被称为绑定挂载,如图 Figure 2-2 所示。

使用 Docker 绑定挂载访问主机文件系统

图 2-2. 使用 Docker 绑定挂载访问主机文件系统

在 Docker 中启动容器时,你可以使用 --volume-v 选项指定绑定挂载,并指定本地文件系统路径和容器路径。例如,你可以启动一个 Nginx Web 服务器实例,并将本地开发机器上的项目文件夹映射到容器中。如果你安装了 Docker,可以在自己的环境中测试这个命令:

docker run -it --rm -d --name web -p 8080:80 \
  -v ~/site-content:/usr/share/nginx/html nginx

这会将本地主机上 8080 端口的 Web 服务器暴露出来。如果本地路径目录不存在,Docker 运行时将会创建它。Docker 允许你创建读写或只读权限的绑定挂载。因为卷被表示为一个目录,运行在容器中的应用程序可以将任何可以表示为文件的内容放入卷中,甚至是数据库。

绑定挂载对开发工作非常有用。然而,由于这样会导致容器依赖于主机上特定文件的存在,所以在生产环境中使用绑定挂载并不合适。这对单机部署可能还行,但是生产部署往往需要跨多个主机。另一个问题是,容器打开到主机文件系统的访问可能会带来安全隐患。因此,出于这些原因,我们需要另一种用于生产部署的方法。

在 Docker 中,推荐使用卷。Docker 卷由 Docker 在主机文件系统的特定目录下创建和管理。使用 docker volume create 命令创建卷。例如,你可以创建一个名为 site-content 的卷来存储网站的文件:

docker volume create site-content

如果没有指定名称,Docker 会分配一个随机名称。创建后,生成的卷可以使用 -v VOLUME-NAME:CONTAINER-PATH 的形式挂载到容器中。例如,你可以使用刚创建的卷来让一个 Nginx 容器读取内容,同时允许另一个容器使用 ro 选项编辑内容:

docker run -it --rm -d --name web \
  -v site-content:/usr/share/nginx/html:ro nginx

Docker 卷挂载语法

Docker 还支持 --mount 语法,允许更明确地指定源和目标文件夹。这种记法被认为更现代化,但也更冗长。前面例子中显示的语法仍然有效且更常用。

正如我们所示,一个 Docker 卷可以同时挂载到多个容器中,如图 2-3 所示。

使用 Docker 卷的优势在于 Docker 管理容器的文件系统访问,这使得在容器上更简单地强制执行容量和安全限制成为可能。

在主机上创建 Docker 卷以在容器之间共享数据

图 2-3. 在主机上创建 Docker 卷以在容器之间共享数据

tmpfs 挂载

Docker 支持两种特定于主机操作系统的挂载类型:tmpfs(或临时文件系统)和命名管道。命名管道在 Docker for Windows 上可用,但由于它们通常不在 Kubernetes 中使用,我们在此不会过多考虑它们。

运行 Docker 在 Linux 上时,可以使用 tmpfs 挂载。tmpfs 挂载仅存在于容器的内存中,因此其内容不会存在于磁盘上,如图 2-4 所示。tmpfs 挂载适用于需要持久保存相对少量数据的应用程序,尤其是那些你不希望写入到主机文件系统的敏感数据。由于数据存储在内存中,访问速度更快也是一个附加好处。

使用 Docker tmpfs 创建临时卷

图 2-4. 使用 Docker tmpfs 创建临时卷

要创建 tmpfs 挂载,可以使用 docker run --tmpfs 选项。例如,你可以使用以下命令指定一个 tmpfs 卷来存储处理敏感数据的 Web 服务器的 Nginx 日志:

docker run -it --rm -d --name web —-tmpfs /var/log/nginx nginx

--mount 选项也可以用于更多控制可配置选项。

卷驱动程序

Docker 引擎具有可扩展的架构,允许您通过插件添加自定义行为,包括网络、存储和授权等功能。第三方存储插件可用于多个开源和商业提供者,包括公共云和各种网络文件系统。利用这些插件涉及在 Docker 引擎上安装插件,然后在使用该存储启动 Docker 容器时指定相关卷驱动程序,如 图 2-5 所示。

使用 Docker 卷驱动程序访问网络存储

图 2-5. 使用 Docker 卷驱动程序访问网络存储

关于使用 Docker 支持的各种类型卷的更多信息,请参阅Docker 存储文档,以及docker run 命令的文档。

Kubernetes 数据存储资源

现在您已经了解了容器和云存储的基本概念,让我们看看 Kubernetes 带来了什么。在本节中,我们将介绍一些关键的 Kubernetes 概念,即 API 中用于将存储附加到容器化应用程序的资源。即使您对这些资源已有一定了解,也请继续关注,因为我们将特别关注每个资源如何与有状态数据相关联。

Pod 和卷

新用户遇到的第一个 Kubernetes 资源之一是Pod。这是 Kubernetes 工作负载的基本部署单元。Pod 提供了运行容器的环境,Kubernetes 控制平面负责将 Pod 部署到 Kubernetes Worker 节点上。

KubeletKubernetes 控制平面的一个组件,运行在每个 Worker 节点上。它负责在节点上运行 Pod,监视这些 Pod 及其内部容器的健康状态。这些元素在 图 2-6 中进行了总结。

在 Kubernetes Pod 中使用卷

图 2-6. 在 Kubernetes Pod 中使用卷

虽然一个 Pod 可以包含多个容器,但最佳实践是一个 Pod 包含一个单独的应用程序容器,以及可选的额外辅助容器,如 图 2-6 所示。这些辅助容器可能包括在主应用程序容器之前运行以执行配置任务的init 容器,或者与主应用程序容器并行运行以提供辅助服务(如可观察性或管理)的sidecar 容器。在后续章节中,您将看到如何利用这些架构模式部署数据基础设施。

现在让我们看看持久性如何在这个 Pod 架构中得到支持。与 Docker 类似,容器中的“磁盘上”数据在容器崩溃时会丢失。Kubelet 负责重新启动容器,但这个新容器是原始容器的替代 —— 它将具有独特的身份并以完全新的状态启动。

在 Kubernetes 中,术语用于表示 Pod 内部存储的访问。通过使用卷,容器具有持久化数据的能力,这些数据将超出容器的生命周期(可能还包括 Pod 的生命周期,我们很快会看到)。一个卷可以被 Pod 中的多个容器访问。每个容器在 Pod 内有自己的volumeMount,指定它应该被挂载到的目录,从而允许挂载点在容器之间有所不同。

在多种情况下,你可能希望在 Pod 中的容器之间共享数据:

  • 一个 init 容器创建一个定制的配置文件,该文件由应用容器挂载以获取配置值。

  • 应用 Pod 写入日志,而 sidecar Pod 读取这些日志以识别报告给外部监控工具的警报条件。

然而,你可能希望避免多个容器向同一个卷写入数据,因为你需要确保多个写入者不会发生冲突——Kubernetes 不会为你解决这个问题。

准备运行示例代码

本书的示例假设您可以访问一个运行中的 Kubernetes 集群。对于本章的示例,像 kind、K3s 或 Docker Desktop 这样的本地开发集群应该足够了。本节使用的源代码位于本书的代码库中。

在 Pod 中使用卷需要两个步骤:定义卷并将卷挂载到每个需要访问的容器中。让我们看一个样例 YAML 配置,定义了一个具有单个应用容器(Nginx Web 服务器)和单个卷的 Pod。源代码 存放在本书的代码库中:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-app
    image: nginx
    volumeMounts:
    - name: web-data
      mountPath: /app/config
  volumes:
  - name: web-data

注意配置的两个部分:卷在 spec.volumes 下定义,卷的使用在 spec.containers.volumeMounts 下定义。首先,在 volumeMounts 下引用卷的 name,并且通过 mountPath 指定它将被挂载的目录。在声明 Pod 规范时,卷和挂载是一起的。为了使你的配置有效,必须在引用前声明卷,并且 Pod 中至少一个容器必须使用该卷。

您可能还注意到卷只有一个name。您并未指定任何其他信息。您认为这会有什么影响?您可以尝试使用示例源代码文件 nginx-pod.yaml,或将上述配置剪切并粘贴到以该名称命名的文件中,并针对已配置的 Kubernetes 集群执行kubectl命令来验证这一点:

kubectl apply -f nginx-pod.yaml

您可以通过使用kubectl get pod命令获取创建的 Pod 的更多信息,例如:

kubectl get pod my-pod -o yaml | grep -A 5 "  volumes:"

结果可能如下所示:

  volumes:
  - emptyDir: {}
    name: web-data
  - name: default-token-2fp89
    secret:
      defaultMode: 420

如您所见,当创建请求的卷时,Kubernetes 提供了额外的信息,默认将其设置为emptyDir类型。其他默认属性可能因您使用的 Kubernetes 引擎而异,但我们在这里不会进一步讨论。

可以在容器中挂载几种类型的卷;我们来看一下。

临时卷

您可能还记得我们在 Docker 卷的先前讨论中提到的 tmpfs 卷,它为单个容器的生命周期提供临时存储。Kubernetes 提供了一个 临时卷 的概念,与之类似,但范围限定在 Pod 级别。在前面的示例中介绍的emptyDir就是一种临时卷类型。

临时卷可用于数据基础设施或其他希望创建快速访问缓存的应用程序。虽然它们不会持续超出 Pod 的生命周期,但它们仍然可以展示长期持久性的其他卷的一些典型属性,比如快照能力。临时卷的设置略微比 PersistentVolumes 更简单,因为它们完全在 Pod 定义中内联声明,无需引用其他 Kubernetes 资源。接下来将会看到,创建和使用 PersistentVolumes 会更复杂一些。

其他临时存储提供者

我们接下来将讨论一些提供 PersistentVolumes 的 in-tree 和 CSI 存储驱动程序,它们还提供了临时卷选项。您应查看特定提供者的文档,以了解可用的选项。

配置卷

Kubernetes 提供了几种将配置数据注入到 Pod 中作为卷的构造方法。从应用程序持久化其自身数据的角度来看,这些卷类型也被视为临时的。

以下卷类型与我们在本书中的探索相关,因为它们为在 Kubernetes 上运行的应用程序和数据基础设施提供了一种有用的配置手段。我们将简要描述每种卷:

ConfigMap 卷

配置映射(ConfigMap)是 Kubernetes 资源,用于将应用程序外部的配置值存储为一组名称-值对。例如,应用程序可能需要底层数据库的连接详情,如 IP 地址和端口号。将这些配置定义在 ConfigMap 中是从应用程序中外部化这些信息的好方法。生成的配置数据可以作为卷(volume)挂载到应用程序中,其中将显示为一个目录。每个配置值都表示为一个文件,其中文件名是键,文件的内容包含值。有关如何将 ConfigMap 挂载为卷的更多信息,请参阅 Kubernetes 文档 mounting ConfigMaps as volumes

Secret volumes

秘密(Secret)与 ConfigMap 类似,但用于安全访问需要保护的敏感数据。例如,您可能希望创建一个包含数据库访问凭据(如用户名和密码)的 Secret。配置和访问 Secret 与使用 ConfigMap 类似,额外的好处是 Kubernetes 在 Pod 内访问时帮助解密 Secret。有关如何将 Secret 挂载为卷的更多信息,请参阅 Kubernetes 文档 mounting Secrets as volumes

Downward API volumes

Kubernetes 下行 API 公开有关 Pod 和容器的元数据,可以作为环境变量或卷使用。这些元数据也是 kubectl 和其他客户端使用的相同元数据。

可用的 Pod 元数据包括 Pod 的名称、ID、命名空间、标签和注释。容器化应用程序可能希望使用 Pod 信息进行日志记录和指标报告,或者确定数据库或表名。

可用的容器元数据包括请求的资源量(如 CPU、内存和临时存储)和最大资源量。容器化应用程序可能希望使用这些信息来限制其资源使用。有关如何作为卷注入 Pod 信息的示例,请参阅 Kubernetes 文档 injecting Pod information as a volume

主机路径(hostPath)卷

一个 hostPath 卷将文件或目录挂载到运行它的 Kubernetes 工作节点上的 Pod 中。这类似于 Docker 中的绑定挂载(bind mount)概念,可以参考 “Bind Mounts”。使用 hostPath 卷相比于 emptyDir 卷有一个优势:数据将在 Pod 重启后仍然存在。

然而,使用 hostPath 卷存在一些缺点。首先,为了让替换的 Pod 访问原始 Pod 的数据,它需要在相同的工作节点上重新启动。虽然 Kubernetes 允许您使用亲和性控制 Pod 放置在哪个节点上,但这往往会限制 Kubernetes 调度程序的最佳放置 Pod 的能力,如果节点因某些原因宕机,则 hostPath 卷中的数据将丢失。其次,与 Docker 绑定挂载一样,使用 hostPath 卷存在安全风险,涉及允许访问本地文件系统。因此,建议仅将 hostPath 卷用于开发部署。

云卷

可以创建 Kubernetes 卷,引用的存储位置不仅限于 Pod 运行的工作节点,如 图 2-7 所示。这些可以分为由命名云提供商提供的卷类型以及试图提供更通用接口的卷类型。

这些包括以下内容:

  • awsElasticBlockStore 卷类型用于挂载亚马逊网络服务 (AWS) 弹性块存储 (EBS) 上的卷。许多数据库使用块存储作为其底层存储层。

  • gcePersistentDisk 卷类型用于挂载 Google 计算引擎 (GCE) 持久磁盘 (PD),这是块存储的另一个示例。

  • Microsoft Azure 支持两种类型的卷:azureDisk 用于 Azure 数据磁盘卷,以及 azureFile 用于 Azure 文件卷。

  • cinder 卷类型可用于访问 OpenStack 部署中的 OpenStack Cinder 卷。

Kubernetes Pods 直接挂载云提供商存储

图 2-7. Kubernetes Pods 直接挂载云提供商存储

使用这些类型通常需要在云提供商上进行配置,并且从 Kubernetes 集群访问通常限于相同云区域和帐户中的存储。查阅您的云提供商文档以获取更多详细信息。

其他附加卷提供程序

还有许多其他类型的卷提供程序,提供的存储类型各不相同。以下是一些示例:

  • fibreChannel 卷类型可用于实施光纤通道协议的 SAN 解决方案。

  • gluster 卷类型用于访问使用之前引用的 Gluster 分布式文件系统的文件存储。

  • iscsi 卷将现有的 Internet Small Computer Systems Interface (iSCSI) 卷挂载到您的 Pod 中。

  • nfs 卷允许将现有的 NFS 共享挂载到 Pod 中。

我们将检查更多实现“容器附加存储”模式的卷提供程序,在 “容器附加存储” 中有详细说明。表 2-1 比较了我们迄今所涵盖的 Docker 和 Kubernetes 存储概念。

表 2-1. 比较 Docker 和 Kubernetes 存储选项

存储类型 Docker Kubernetes
从各种提供者访问持久存储 卷(通过卷驱动程序访问) 卷(通过 in-tree 或 CSI 驱动程序访问)
访问主机文件系统(不建议用于生产) Bind 挂载 hostPath
在容器(或 Pod)运行时可用的临时存储 Tmpfs emptyDir 和其他临时卷
配置和环境数据(只读) (无直接等价) ConfigMap、Secret、downward API

在这一节中,我们讨论了如何使用卷来提供可以由同一 Pod 内的多个容器共享的存储。虽然对于一些用例来说使用卷已经足够,但它并不能满足所有需求。卷不能提供在 Pod 之间共享存储资源的能力。特定存储位置的定义与 Pod 的定义紧密相关。随着在您的 Kubernetes 集群中部署的 Pod 数量增加,为单个 Pod 管理存储并不会很好地扩展。

幸运的是,Kubernetes 提供了额外的基元,有助于简化为单个 Pod 和相关 Pod 组提供存储卷的过程。我们将在接下来的几节中探讨这些概念。

持久卷

Kubernetes 开发人员为管理存储引入的关键创新是持久卷子系统。该子系统由三个额外的 Kubernetes 资源组成,它们共同工作:持久卷、持久卷声明和存储类。这些资源允许您将存储的定义和生命周期与 Pod 使用存储的方式分开,如图 2-8 所示:

  • 集群管理员定义持久卷,可以通过显式定义或创建可以动态配置新持久卷的存储类来定义持久卷。

  • 应用程序开发人员创建描述其应用程序存储资源需求的持久卷声明,并且这些持久卷声明可以作为卷定义的一部分在 Pod 中引用。

  • Kubernetes 控制平面管理将持久卷声明与持久卷绑定的过程。

PersistentVolumes, PersistentVolumeClaims, 和 StorageClasses

图 2-8. PersistentVolumes, PersistentVolumeClaims, 和 StorageClasses

首先让我们看一下 PersistentVolume 资源(通常缩写为 PV),它定义了访问特定位置存储的方式。PersistentVolumes 通常由集群管理员定义,供应用开发者使用。每个 PV 可以代表前面部分讨论的相同类型的存储,例如云提供商提供的存储、网络存储或直接在工作节点上的存储,如图 2-9 中所示。由于它们与特定的存储位置绑定,因此 PersistentVolumes 在 Kubernetes 集群之间不可移植。

Kubernetes 持久卷的类型

图 2-9. Kubernetes 持久卷的类型

本地 PersistentVolumes

图 2-9 还介绍了一种名为 local 的 PersistentVolume 类型,它表示直接挂载在 Kubernetes 工作节点(如磁盘或分区)上的存储。与 hostPath 卷类似,local 卷也可以表示一个目录。local 卷与 hostPath 卷的一个关键区别在于,当使用 local 卷的 Pod 重新启动时,Kubernetes 调度程序确保 Pod 被重新调度到同一节点,以便可以重新附加到相同的持久状态。因此,local 卷经常用作管理自身复制的数据基础设施的后备存储,正如我们将在第 4 章中看到的那样。

定义 PersistentVolume 的语法将看起来很熟悉,因为它类似于在 Pod 内定义卷。例如,这是一个定义本地 PersistentVolume 的 YAML 配置文件。源代码位于本书的存储库中。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-volume
spec:
  capacity:
    storage: 3Gi
  accessModes:
    - ReadWriteOnce
  local:
    path: /app/data
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node1

正如您所见,此代码在工作节点 node1 上定义了一个名为 my-volumelocal 卷,大小为 3 GB,访问模式为 ReadWriteOnce。支持以下访问模式用于 PersistentVolumes:

ReadWriteOnce

该卷可以由单个节点一次性挂载进行读写,尽管在该节点上运行的多个 Pod 可以访问该卷。

ReadOnlyMany

可以同时由多个节点挂载该卷,仅供读取。

ReadWriteMany

该卷可以同时由多个节点挂载,用于读写。

选择卷访问模式

给定卷的正确访问模式将受到工作负载类型的驱动。例如,许多分布式数据库将配置为每个 Pod 专用的存储,使 ReadWriteOnce 成为一个不错的选择。

除了容量和访问模式外,PersistentVolumes 的其他属性包括以下内容:

  • volumeMode 默认为 Filesystem,但可以覆盖为 Block

  • 当一个 Pod 释放对 PersistentVolume 的声明时,reclaimPolicy 定义了发生的事情。合法的值包括 RetainRecycleDelete

  • PersistentVolume 可以具有 nodeAffinity,指定可以访问该卷的 Worker Node 或节点。这对大多数类型来说是可选的,但对于 local 卷类型来说是必需的。

  • class 属性将此 PV 绑定到特定的 StorageClass,这是我们稍后在本章中介绍的概念。

  • 一些 PersistentVolume 类型公开特定于该类型的 mountOptions

卷选项的差异

不同的卷类型有不同的选项。例如,并非每种 PersistentVolume 类型都可以访问每种访问模式或回收策略,因此请参阅您选择类型的文档以获取更多详细信息。

使用 kubectl describe persistentvolume 命令(或简写为 kubectl describe pv)查看 PersistentVolume 的状态:

kubectl describe pv my-volume
Name:              my-volume
Labels:            <none>
Annotations:       <none>
Finalizers:        [kubernetes.io/pv-protection]
StorageClass:
Status:            Available
Claim:
Reclaim Policy:    Retain
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          3Gi
Node Affinity:
  Required Terms:
    Term 0:        kubernetes.io/hostname in [node1]
Message:
Source:
    Type:  LocalVolume (a persistent volume backed by local storage on a node)
    Path:  /app/data
Events:    <none>

当首次创建时,PersistentVolume 的状态为 可用。PersistentVolume 可以具有多个状态值:

可用

PersistentVolume 是空闲的,尚未绑定到声明。

已绑定

PersistentVolume 已绑定到 PersistentVolumeClaim,这在 describe 输出中有列出。

已释放

PersistentVolume 上的现有声明已被删除,但资源尚未被回收,因此资源尚未 可用

失败

该卷已经失败了其自动回收。

现在您已经了解了如何在 Kubernetes 中定义存储资源,下一步是学习如何在应用程序中使用该存储。

PersistentVolumeClaims

正如我们所讨论的,Kubernetes 将存储的定义与其使用分开。通常由不同角色执行这些任务:集群管理员定义存储,而应用开发者使用存储。PersistentVolumes 通常由管理员定义,并引用特定于该集群的存储位置。然后,开发者可以使用 PersistentVolumeClaims (PVCs) 指定其应用程序的存储需求,Kubernetes 使用这些 PVCs 将 Pod 与符合指定条件的 PersistentVolume 关联起来。如 图 2-10 所示,PersistentVolumeClaim 用于引用我们之前介绍的各种卷类型,包括本地 PersistentVolumes 或由云或网络存储供应商提供的外部存储。

使用 PersistentVolumeClaims 访问 PersistentVolumes

图 2-10. 使用 PersistentVolumeClaims 访问 PersistentVolumes

以下是从应用开发者角度来看这个过程的步骤。首先,您将创建一个 PVC,表示您的期望存储条件。例如,这里有一个请求 1 GB 存储空间并具有独占读写访问权限的声明。源代码 可在本书的存储库中找到:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-claim
spec:
  storageClassName: ""
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

您可能已经注意到关于此声明的一个有趣事实是,storageClassName 设置为空字符串。在下一节讨论 StorageClasses 时,我们将解释这一点的重要性。您可以像这样在 Pod 的定义中引用声明。这本书的 源代码 存储在此书的存储库中:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - mountPath: "/app/data"
      name: my-volume
  volumes:
  - name: my-volume
    persistentVolumeClaim:
      claimName: my-claim

正如您所见,PersistentVolume 在 Pod 中表示为一个卷。该卷被赋予一个名称和对声明的引用。这被认为是一种 persistentVolumeClaim 类型的卷。与其他卷一样,该卷被挂载到容器中的特定挂载点上 —— 在这种情况下,挂载到主应用程序 Nginx 容器的路径为 /app/data

PVC 还有一个状态,您可以通过检索状态来查看:

kubectl describe pvc my-claim
Name:          my-claim
Namespace:     default
StorageClass:
Status:        Bound
Volume:        my-volume
Labels:        <none>
Annotations:   pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      3Gi
Access Modes:  RWO
VolumeMode:    Filesystem
Mounted By:    <none>
Events:        <none>

PVC 有两种状态值之一:Bound 表示它已绑定到一个卷(就像这个例子中一样),或者 Pending 表示它尚未绑定到卷。通常,Pending 状态表示没有与声明匹配的 PV 存在。

这里是背后发生的事情。Kubernetes 使用 Pod 中引用的 PVC 作为卷,并在调度 Pod 时考虑这些卷。Kubernetes 标识与声明相关属性匹配的 PersistentVolumes,并将最小可用模块绑定到声明。这些属性可能包括标签或节点亲和性,就像我们之前为 local 卷所见。

启动 Pod 时,Kubernetes 控制平面确保将 PersistentVolumes 挂载到 Worker Node。然后,每个请求的存储卷被挂载到指定的挂载点上。

StorageClasses

前面的例子展示了 Kubernetes 如何将 PVC 绑定到已经存在的 PersistentVolumes。这种模型,即在 Kubernetes 集群中显式创建 PersistentVolumes,被称为 静态提供。Kubernetes PersistentVolume 子系统还支持使用 StorageClasses(通常缩写为 SC)进行卷的 动态提供。StorageClass 负责根据集群中运行应用程序的需求提供(和取消提供)PersistentVolumes,如 图 2-11 所示。

StorageClasses 支持动态提供卷

图 2-11. StorageClasses 支持动态提供卷

根据您使用的 Kubernetes 集群,可能已经有至少一个 StorageClass 可用。您可以使用命令 kubectl get sc 来验证这一点。如果在本地运行一个简单的 Kubernetes 分发版本并且看不到任何 StorageClasses,则可以使用以下命令安装 Rancher 的开源本地存储提供程序:

set GH_LINK=https://raw.githubusercontent.com
kubectl apply -f \
  $GH_LINK/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

此存储提供程序预先安装在 K3s 中,这也是 Rancher 提供的桌面发行版。如果您查看该语句中引用的 YAML 配置,请看看以下 StorageClass 的定义。该source code位于本书的存储库中:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

正如您从定义中可以看到的那样,一个 StorageClass 由几个关键属性定义:

  • provisioner与底层存储提供程序接口,如公共云或存储系统,以便分配实际存储。提供程序可以是 Kubernetes 内置的提供程序之一(称为in-tree,因为它们是 Kubernetes 源代码的一部分),也可以是符合容器存储接口(CSI)的提供程序,我们稍后将在本章中进行详细讨论。

  • reclaimPolicy描述了在删除 PersistentVolume 时是否回收存储。默认值Delete可以被覆盖为Retain,在这种情况下,存储管理员将负责管理该存储的未来状态。

  • volumeBindingMode控制存储何时被配置和绑定。如果值是Immediate,则当创建引用 StorageClass 的 PersistentVolumeClaim 时立即配置 PersistentVolume,并且该声明绑定到 PersistentVolume,而不管该声明是否在 Pod 中引用。许多存储插件还支持第二种模式,称为WaitForFirstConsumer,在这种情况下,直到创建引用声明的 Pod 后,才会配置 PersistentVolume。考虑到这种行为更为可取,因为它给了 Kubernetes 调度程序更大的灵活性。

  • 尽管在此示例中未显示,还有一个可选的allowVolumeExpansion标志。这表示 StorageClass 是否支持对卷进行扩展的能力。如果是true,则可以通过增加 PersistentVolumeClaim 的storage.request字段的大小来扩展卷。此值默认为false

  • 一些 StorageClasses 还定义了parameters,这些是传递给提供程序的存储提供程序的特定配置选项。常见选项包括文件系统类型、加密设置以及每秒 I/O 操作(IOPS)的吞吐量。请查阅存储提供程序的文档以获取更多详细信息。

动态配置的限制

本地 PV 不能通过 StorageClass 动态配置,因此您必须自行手动创建它们。

应用程序开发人员可以在定义中添加storageClass属性来引用特定的 StorageClass,从而创建 PVC。例如,以下是引用local-path StorageClass 的 PVC 的 YAML 配置。该source code位于本书的存储库中:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-local-path-claim
spec:
  storageClassName: local-path
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

如果声明中未指定storageClass,则使用默认的 StorageClass。集群管理员可以设置默认的 StorageClass。正如我们在“持久卷”中所展示的,您可以通过使用空字符串来选择不使用 StorageClasses,这表示您正在使用静态配置的存储。

StorageClasses 提供了一个有用的抽象,集群管理员和应用程序开发人员可以作为合同使用:管理员定义 StorageClasses,开发人员按名称引用 StorageClasses。底层 StorageClass 实现的细节可以因 Kubernetes 平台提供商而异,促进应用程序的可移植性。

此灵活性允许管理员创建代表各种存储选项的 StorageClasses — 例如,根据吞吐量或延迟区分不同的服务质量保证。这个概念在其他存储系统中被称为配置文件。请参阅“开发者如何推动 Kubernetes 存储的未来”获取有关如何创新地利用 StorageClasses 的更多想法。

Kubernetes 存储架构

在前面的部分中,我们讨论了 Kubernetes 通过其API支持的各种存储资源。在本章的其余部分,我们将探讨这些解决方案的构建方式,因为它们可以为我们提供构建云原生数据解决方案的宝贵见解。

定义云原生存储

本章讨论的大多数存储技术都作为“云原生存储”解决方案的一部分,列在CNCF 景观中。CNCF 存储白皮书是一个有用的资源,定义了云原生存储的关键术语和概念。这两个资源定期更新。

Flexvolume

最初,Kubernetes 代码库包含多个内置存储插件(即与 Kubernetes 代码库的其他部分包含在同一个 GitHub 仓库中)。这有助于标准化与不同存储平台连接的代码,但也存在一些缺点。首先,许多 Kubernetes 开发人员对包含的广泛存储提供商的专业知识有限。更重要的是,升级存储插件的能力与 Kubernetes 发布周期捆绑在一起,这意味着如果您需要为存储插件进行修复或增强,您必须等待它被接受并发布到 Kubernetes 的周期中。这减缓了 Kubernetes 存储技术的成熟速度,结果也减缓了其采纳速度。

Kubernetes 社区创建了 Flexvolume 规范,以允许独立开发插件,即不依赖于 Kubernetes 源代码树,因此不受 Kubernetes 发布周期的限制。与此同时,其他容器编排系统的存储插件标准也在出现,这些社区的开发者开始质疑为解决同一个基本问题开发多个标准的智慧。

未来的 Flexvolume 支持

在 Kubernetes 1.23 中,Flexvolume 功能已被弃用,以支持容器存储接口。

容器存储接口

容器存储接口(CSI)倡议被确立为容器化应用程序存储的行业标准。CSI 是一个开放标准,用于定义能够跨容器编排系统(包括 Kubernetes、Mesos 和 Cloud Foundry)工作的插件。正如谷歌工程师兼 Kubernetes 存储特别兴趣小组(SIG) 主席 Saad Ali 在《The New Stack》中指出的那样,“容器存储接口允许 Kubernetes 与任意存储系统直接交互”。

CSI 规范可以在 GitHub 上找到。Kubernetes 对 CSI 的支持始于 1.x 版本,并在 1.13 版本中 正式推广(GA)。Kubernetes 仍在跟踪 CSI 规范的更新。

额外的 CSI 资源

CSI 文档站点 提供了对希望开发符合 CSI 标准驱动程序的开发者和存储提供者的指导。该站点还提供了一个非常有用的符合 CSI 标准的驱动程序列表。这个列表通常比 Kubernetes 文档站点上提供的更新。

一旦在 Kubernetes 集群上部署了 CSI 实现,其功能可以通过标准 Kubernetes 存储资源(如 PVC、PV 和 SC)访问。在后端,每个 CSI 实现必须提供两个插件:一个节点插件和一个控制器插件,如 图 2-12 所示。

CSI 规范使用 gRPC 定义了这些插件的必需接口,但并未具体指定如何部署这些插件。让我们简要看一下每个服务的角色:

控制器插件

该插件支持对卷进行的操作,例如创建、删除、列出、发布/取消发布、跟踪和扩展卷容量。它还跟踪卷状态,包括每个卷附加到哪些节点。控制器插件还负责获取和管理快照,并使用快照来克隆卷。控制器插件可以在任何节点上运行,它是一个标准的 Kubernetes 控制器。

节点插件

此插件在每个 Kubernetes 工作节点上运行,其中配置的卷将被附加。节点插件负责本地存储以及在节点上挂载和卸载卷。Kubernetes 控制平面指示插件在调度任何需要该卷的 Pod 之前挂载卷。

CSI 映射到 Kubernetes

图 2-12. CSI 映射到 Kubernetes

容器附加存储

虽然 CSI 在标准化容器编排器上的存储管理方面是重要的一步,但它并没有提供关于存储软件如何或在哪里运行的实现指导。一些 CSI 实现基本上是围绕在 Kubernetes 集群外运行的传统存储管理软件的薄包装。虽然重用现有的存储资产确实有其好处,但许多开发人员表达了希望在 Kubernetes 中完全运行其应用程序旁边的存储管理解决方案的愿望。

Container Attached Storage 是一种设计模式,提供更云原生的存储管理方法。管理附加卷到应用程序的逻辑本身由运行在容器中的微服务组成。这使得存储层具有与在 Kubernetes 上部署的其他应用程序相同的特性,并减少了管理员需要跟踪的不同管理界面数量。存储层只是另一个 Kubernetes 应用程序。

正如 Evan Powell 在“Container Attached Storage: A Primer”中指出:

容器附加存储反映了重新发明特定类别或创建新类别的解决方案的更广泛趋势——通过建立在 Kubernetes 和微服务之上,并为基于 Kubernetes 的微服务环境提供功能。例如,云原生生态系统中出现了新的安全性、DNS、网络、网络策略管理、消息传递、跟踪、日志记录等项目。

几个项目和产品的例子体现了 CAS 存储方法。让我们来看一些开源选项。

OpenEBS

OpenEBS 是由 MayaData 创建并捐赠给 CNCF 的项目,在 2019 年成为沙盒项目。该名称是对亚马逊弹性块存储的一种戏仿,OpenEBS 试图提供这种流行托管服务的开源替代。OpenEBS 提供用于管理本地和 NVMe 持久卷的存储引擎。

OpenEBS 为在 Kubernetes 上部署的符合 CSI 的实现提供了一个很好的例子,如图 2-13 所示。控制平面包括 OpenEBS 提供程序,该提供程序实现了 CSI 控制器接口,以及 OpenEBS API 服务器,为客户端提供配置界面,并与其他 Kubernetes 控制平面交互。

OpenEBS 数据平面包括节点磁盘管理器(NDM)以及每个持久卷的专用 Pod。NDM 在将要访问存储的每个 Kubernetes 工作节点上运行。它实现了 CSI 节点接口,并提供了自动检测连接到工作节点的块存储设备的有用功能。

OpenEBS 架构

图 2-13. OpenEBS 架构

OpenEBS 为每个卷创建多个 Pod。一个控制器 Pod 被创建为主要副本,并在其他 Kubernetes 工作节点上为高可用性创建额外的副本 Pod。每个 Pod 包括用于指标收集和管理的 sidecars,这允许控制平面监视和管理数据平面。

Longhorn

Longhorn 是一个面向 Kubernetes 的开源分布式块存储系统。它最初由 Rancher 开发,并于 2019 年成为 CNCF 沙盒项目。Longhorn 专注于提供替代云供应商存储和昂贵外部存储阵列的解决方案。Longhorn 支持向 NFS 或兼容 S3 存储提供增量备份,并向另一个 Kubernetes 集群进行实时复制以进行灾难恢复。

Longhorn 使用与 OpenEBS 显示的类似架构;根据文档,“Longhorn 为每个块设备卷创建一个专用存储控制器,并在多个节点上同步复制卷。存储控制器和副本本身使用 Kubernetes 进行编排。” Longhorn 还提供集成用户界面以简化操作。

Rook 和 Ceph

根据其网站,“Rook 是一个开源的云原生存储编排器,为多样化的存储解决方案提供平台、框架和支持,以与云原生环境本地集成。” Rook 最初是作为可在 Kubernetes 中部署的 Ceph 的容器化版本创建的。Ceph 是一个提供块、文件和对象存储的开源分布式存储框架。Rook 是 CNCF 接受的第一个存储项目,现在被认为是CNCF 毕业项目

Rook 是一个真正的 Kubernetes 本地实现,因为它利用了 Kubernetes 自定义资源(CRDs)和自定义控制器称为操作员。Rook 为 Ceph、Cassandra 和 NFS 提供操作员。我们将在第四章中更多地了解自定义资源和操作员。

一些适用于 Kubernetes 的商业解决方案也体现了 CAS 模式。这些包括MayaData(OpenEBS 的创建者)、由Pure Storage提供的PortworxRobin.ioStorageOS。 这些公司不仅提供块和文件格式的原始存储,还提供了用于简化部署额外数据基础设施(如数据库和流解决方案)的集成。

容器对象存储接口

CSI 提供了对文件和块存储的支持,但对象存储 API 需要不同的语义,不太适合挂载卷的 CSI 范例。2020 年秋季,由MinIO领导的一组公司开始开发了容器编排平台中对象存储的新 API:Container Object Storage Interface (COSI)。 COSI 提供了一个更适合于在 Kubernetes 中进行对象存储配置和访问的 API,定义了bucket自定义资源,并包括创建桶和管理对桶的访问的操作。 COSI 控制平面和数据平面的设计是以 CSI 为模型的。 COSI 是一个具有广泛采纳潜力的新兴标准,可能在 Kubernetes 社区甚至更广泛的范围内得到广泛应用。

正如您所见,Kubernetes 上的存储是一个涵盖了许多创新的领域,包括多个开源项目和商业供应商竞争提供最易用、成本效益高和性能卓越的解决方案。云原生存储部分 CNCF 景观提供了一个有用的存储提供商及相关工具的列表,包括本章中提到的技术及其他许多技术。 *# 总结

在这一章中,我们探讨了像 Docker 这样的容器系统以及像 Kubernetes 这样的容器编排系统中如何管理持久性。您已经了解了各种 Kubernetes 资源,可以用来管理有状态的工作负载,包括 Volumes、PersistentVolumes、PersistentVolumeClaims 和 StorageClasses。我们看到了容器存储接口和容器附加存储模式如何指引我们朝着更加云原生的存储管理方法迈进。现在,您已经准备好学习如何使用这些构建块和设计原则来管理包括数据库、流数据等在内的有状态工作负载了。

第三章:Kubernetes 上的数据库困难之路

正如我们在第一章中讨论的,Kubernetes 是为无状态工作负载而设计的。与此相关的是,无状态工作负载是 Kubernetes 做得最好的。因此,有人认为你不应该试图在 Kubernetes 上运行有状态工作负载,你可能会听到各种建议,比如“使用托管服务”,或者“将数据留在你的传统数据中心的传统数据库中”,或者甚至“在云中运行你的数据库,但使用传统虚拟机而不是容器”。

虽然这些建议仍然是可行的选择之一,但本书的主要目标之一是展示,在 Kubernetes 中运行数据基础设施不仅是一个可行的选择,而且是一个首选的选择。在他的文章《从一个前怀疑论者的角度看 Kubernetes 上的数据库》,克里斯托弗·布拉德福德描述了他从怀疑在 Kubernetes 中运行任何有状态工作负载,到勉强接受在开发和测试工作负载中在 Kubernetes 上运行数据基础设施,再到热情地宣扬在生产环境中部署数据库在 Kubernetes 上的旅程。这一旅程是数据在 Kubernetes 社区中很多人的典型代表(DoKC)。到 2020 年中期,鲍里斯·库尔克切夫能够引用一个日益增长的共识,即在 Kubernetes 上管理有状态工作负载已经达到了可行性,甚至成熟性的点,他在文章《将有状态应用引入 Kubernetes 的 3 个理由》中提到了这一点。

这种变化是如何发生的?在过去的几年中,Kubernetes 社区的重点已经转向增加支持以云原生方式在 Kubernetes 上管理状态的功能。存储元素代表了这种转变的重要部分,我们在上一章中介绍了这些,包括 Kubernetes 持久卷子系统和 CSI 的采用。在本章中,我们将通过查看用于在这种存储基础上构建有状态应用的 Kubernetes 资源来完成这部分故事。我们将特别关注一种特定类型的有状态应用程序:数据基础设施。

困难的道路

“走弯路”这个短语现在常常与避免简单选择,而选择投入详细工作以完成具有持久意义结果的方式联系在一起。在整个历史上,各种开拓者都以为后代能够过上更美好生活而自豪,因为他们为此做出了牺牲,包括鲜血、汗水和眼泪。这些前辈们经常会听到后辈们未能理解他们所经历深度的感叹。

在技术世界中,情况也毫无不同。虽然像 API 和“无代码”环境这样的新创新有着巨大的潜力培养全球新一代开发人员,但深入理解底层技术仍然是管理全球规模高可用性和安全系统所必需的。当事情出错时,这种详细的知识证明了其价值。这就是为什么我们许多只在日常工作中作为软件开发人员,从未接触过物理服务器的人,却从通过手工布线芯片和电路板来建造自己的 PC 中获益良多。这也是为什么作为我们朋友和家人的非正式 IT 顾问带来的隐藏好处之一。

对于 Kubernetes 社区来说,“the hard way”(“艰难的方式”)自然有着更具体的涵义。谷歌工程师 Kelsey Hightower 的 “Kubernetes the Hard Way” 已经成为想要深入理解 Kubernetes 集群组成元素的人们的一种入门仪式。这个受欢迎的教程引导你逐步下载、安装和配置组成 Kubernetes 控制平面的每个组件。其结果是一个可工作的 Kubernetes 集群,虽然不适合部署生产工作负载,但绝对足够进行开发和学习使用。这种方法的吸引力在于所有指令都是手工输入的。与其下载一堆为你做所有事情的脚本不同,你必须理解每一步发生了什么。

在本章中,我们将模仿这种方法,手把手地教你如何通过“the hard way”部署一些示例数据基础设施。在这个过程中,你将更多地实践你在第二章学到的存储资源,并且我们会介绍额外的 Kubernetes 资源类型,来完善我们在第一章中介绍的计算、网络、存储三位一体。你准备好动手了吗?让我们开始吧!

示例不适合生产环境使用

我们在本章中呈现的示例主要是为了介绍 Kubernetes API 的新元素,并不打算代表我们建议在生产环境中运行的部署。我们会确保突出任何差距,以便在即将到来的章节中展示如何填补它们。

在 Kubernetes 上运行数据基础设施的先决条件

要跟随本章的示例,您需要一个 Kubernetes 集群来工作。如果您以前从未尝试过,也许您会想按照 “Kubernetes the Hard Way” 的说明构建一个集群,然后再使用同一个集群以同样的方式添加数据基础设施。您也可以使用简单的桌面 Kubernetes,因为我们不会使用大量资源。如果您使用的是共享集群,您可能希望将这些示例安装在自己的命名空间中,以便将它们与其他人的工作隔离开来:

kubectl config set-context --current --namespace=<*insert-namespace-name-here*>

您还需要确保集群中有一个 StorageClass。如果您是从零开始构建集群,您可能没有这个。您可能想按照 “StorageClasses” 中的说明安装一个简单的 StorageClass 和提供程序,以公开本地存储。本书的仓库中有 源代码

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

您将需要使用支持 WaitForFirstConsumervolumeBindingMode 的 StorageClass。这使 Kubernetes 能够延迟到需要时再进行存储的预配。这种行为通常更适合生产部署,因此您最好养成这种习惯。

在 Kubernetes 上运行 MySQL

首先,让我们从一个超级简单的例子开始。MySQL 是最广泛使用的关系数据库之一,因为它的可靠性和易用性。对于本例,我们将在官方 Kubernetes 文档中的 MySQL 教程 的基础上进行构建,并进行一些调整。您可以在 “Deploying MySQL Example—Data on Kubernetes the Hard Way” 找到本节中使用的源代码。该教程包括两个 Kubernetes 部署:一个用于运行 MySQL Pod,另一个用于运行一个示例客户端——在这种情况下是 WordPress。这个配置显示在 图 3-1 中。

MySQL 的示例 Kubernetes 部署

图 3-1. MySQL 的示例 Kubernetes 部署

在这个例子中,我们看到每个 Pod 都有一个 PersistentVolumeClaim。为了这个例子,我们假设这些声明都由默认的 StorageClass 提供的单个卷来满足。你还会注意到每个 Pod 都显示为 ReplicaSet 的一部分,并且为 MySQL 数据库暴露了一个服务。让我们暂停一下,介绍一下这些概念。

ReplicaSets

在 Kubernetes 上,生产应用程序部署通常不会部署单独的 Pods,因为当节点消失时,单个 Pod 可能会轻易丢失。相反,Pods 通常在管理其生命周期的 Kubernetes 资源的上下文中部署。ReplicaSet 就是这些资源之一,另一个是稍后将在本章中查看的 StatefulSet。

ReplicaSet 的目的是确保在任何给定时间内保持给定 Pod 的指定数量的副本正在运行。当 Pod 被销毁时,将创建其他 Pod 来替换它们,以满足所需的副本数量。ReplicaSet 由 Pod 模板、副本数和选择器定义。Pod 模板定义了由 ReplicaSet 管理的 Pod 的规范,类似于我们在第二章中创建的个别 Pod 的示例。副本数可以是零个或更多个。选择器标识属于 ReplicaSet 的 Pod。

让我们看一下 WordPress 应用程序的 ReplicaSet 示例定义的部分,如图 3-1 所示:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:8.0
        name: mysql
        ...

ReplicaSet 负责创建或删除 Pod,以满足指定数量的副本。您可以通过更改此值来调整 ReplicaSet 的大小。创建新 Pod 时会使用 Pod 模板。由 ReplicaSet 管理的 Pod 在其 metadata.ownerReferences 字段中包含对 ReplicaSet 的引用。如果选择器匹配且 Pod 没有引用其他所有者,则 ReplicaSet 实际上可以负责管理其未创建的 Pod。这种 ReplicaSet 的行为称为获取 Pod。

仔细定义 ReplicaSet 的选择器

如果确实直接创建 ReplicaSets,请确保您使用的选择器是唯一的,并且不匹配您不打算获取的任何裸 Pod。如果选择器匹配,可能会获取不匹配 Pod 模板的 Pod。有关管理 ReplicaSet 生命周期及其管理的 Pod 的更多信息,请参阅Kubernetes 文档

您可能想知道为什么我们没有提供 ReplicaSet 的完整定义。事实证明,大多数应用程序开发人员最终不会直接使用 ReplicaSets,因为 Kubernetes 提供了另一种管理 ReplicaSets 的资源类型:部署。

部署

Kubernetes 部署 是建立在 ReplicaSets 基础上的资源,具有用于生命周期管理的附加功能,包括部署新版本和回滚到先前版本的能力。如图 3-2 所示,创建部署也会创建 ReplicaSet。

部署和 ReplicaSets

图 3-2. 部署和 ReplicaSets

这张图突显了 ReplicaSet(因此管理它们的 Deployments)在克隆 Pod 的副本上运行,这意味着 Pod 的定义是相同的,即使是到 PersistentVolumeClaims 的级别也是如此。ReplicaSet 的定义引用了一个单独提供给它的 PVC,并且没有提供机制来克隆用于其他 Pod 的 PVC 定义。因此,如果您希望每个 Pod 都能访问其专用存储,Deployments 和 ReplicaSets 并不是一个好选择。

如果你的应用 Pod 不需要访问存储,或者你希望它们访问相同的存储,那么 Deployments 是一个很好的选择。但是,这种情况很少见,因为你可能不希望出现多个同时写入相同存储的情况。

让我们创建一个示例部署。首先,创建一个 Secret 来代表数据库的密码(用任何字符串替换密码):

kubectl create secret generic mysql-root-password \
  --from-literal=password=<your password>

接下来,创建一个 PVC 来表示数据库可以使用的存储。源代码存放在本书的代码库中。在这种情况下,一个 PVC 就足够了,因为你正在创建一个单节点。只要你有一个合适的 StorageClass,就应该可以正常工作,正如前面提到的:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

接下来,创建一个部署,使用一个运行 MySQL 的 Pod 模板规范。源代码存放在本书的代码库中。请注意,它包含对你刚刚创建的 PVC 的引用,以及包含数据库根密码的 Secret:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:8.0
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-root-password
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim

关于这个部署规范,我们有几点有趣的事情需要注意:

  • 部署使用了Recreate策略。这是指部署在更新 Pod 模板时如何处理 Pod 替换的方式;我们稍后会讨论这个问题。

  • 在 Pod 模板中,密码作为一个环境变量传递给 Pod,这个密码是从你在这个示例中创建的 Secret 中提取的。覆盖默认密码是保障任何数据库部署的重要方面。

  • MySQL 镜像只暴露了一个端口用于数据库访问,因为这只是一个相对简单的示例。在本书的其他示例中,我们会看到一些 Pod 需要额外暴露端口用于管理操作、指标收集等情况。默认禁用访问是 Kubernetes 的一个很好的特性。

  • MySQL 镜像使用在这个示例中定义的 PVC 挂载其持久化存储。

  • 规范中没有提供副本数。这意味着将使用默认值 1。

应用配置后,尝试使用像kubectl get deployments,rs,pods这样的命令查看 Kubernetes 为您创建的项目。您会注意到一个以 Deployment 命名的 ReplicaSet,包括一个随机字符串(例如wordpress-mysql-655c8d9c54)。Pod 的名称引用 ReplicaSet 的名称,添加一些额外的随机字符(例如wordpress-mysql-655c8d9c54-tgswd)。这些名称为识别这些资源之间的关系提供了一种快速方法。

下面是 Deployment 执行以管理 ReplicaSet 生命周期的几种操作。保持 Kubernetes 对声明性操作的重视,这些操作大多数由更新 Deployment 规范触发:

初始部署

创建 Deployment 时,Kubernetes 使用您提供的规范创建 ReplicaSet。创建此 ReplicaSet 及其 Pods 的过程称为部署。作为滚动更新的一部分也执行部署。

扩展或缩减

更新 Deployment 以更改副本数量时,相应的 ReplicaSet 会相应地扩展或缩减。

滚动更新

当更新 Deployment 的 Pod 模板(例如,通过为 Pod 指定不同的容器镜像)时,Kubernetes 基于新的 Pod 模板创建一个新的 ReplicaSet。Kubernetes 管理旧 ReplicaSet 到新 ReplicaSet 之间的过渡方式由 Deployment 的spec.strategy属性描述,默认为RollingUpdate。在滚动更新过程中,新 ReplicaSet 通过创建符合新模板的 Pods 逐步扩展,同时缩减现有 ReplicaSet 中的 Pods 数量。在此过渡期间,Deployment 强制执行一组最大和最小 Pods 数量的百分比限制,由spec.strategy.rollingupdate.maxSurgemaxUnavailable属性设置。这些值默认为 25%。

重建更新

更新 Pod 模板的另一选项是Recreate。这是前面 Deployment 中设置的选项。使用此选项,在创建新 ReplicaSet 之前立即终止现有的 ReplicaSet。这种策略对于开发环境很有用,因为它可以更快地完成更新,而RollingUpdate更适合生产环境,因为它强调高可用性。这也适用于数据迁移。

回滚更新

在创建或更新 Deployment 时,可能会引入错误——例如,通过更新一个包含错误的版本的容器镜像来更新 Pod 中的错误。在这种情况下,由 Deployment 管理的 Pods 可能甚至无法完全初始化。你可以使用诸如 kubectl rollout status 等命令来检测这些类型的错误。Kubernetes 提供了一系列操作来管理 Deployment 的回滚历史。你可以通过诸如 kubectl rollout history 这样的命令访问这些操作,它提供了 Deployment 的回滚历史编号,以及 kubectl rollout undo,它可以将 Deployment 恢复到上一个回滚状态。你还可以使用 --to-version 选项将 undo 到特定的回滚版本。因为 kubectl 支持本章后面将介绍的其他资源类型的回滚(StatefulSets 和 DaemonSets),在使用这些命令时,你需要包括资源类型和名称——例如:

kubectl rollout history deployment/wordpress-mysql

这将产生如下输出:

deployment.apps/wordpress-mysql
REVISION  CHANGE-CAUSE
1         <none>

正如你所见,Kubernetes 的 Deployments 提供了一些复杂的行为来管理一组克隆 Pods 的生命周期。你可以通过更改 Deployment 的 YAML 规范并重新应用它来测试这些生命周期操作(除了回滚)。尝试将副本数量扩展到 2 再缩小,或者使用不同的 MySQL 镜像。在更新 Deployment 后,你可以使用像 kubectl describe deployment wordpress-mysql 这样的命令来观察 Kubernetes 启动的事件,将你的 Deployment 带到期望的状态。

其他选项可用于 Deployments,这里我们没有空间详细讨论——例如,如果尝试更新失败时 Kubernetes 应该如何处理。有关 Deployments 行为的更深入解释,请参阅 Kubernetes 文档

Services

在前面的步骤中,你已经创建了一个 PVC 来指定数据库的存储需求,一个 Secret 来提供管理员凭据,并创建了一个 Deployment 来管理单个 MySQL Pod 的生命周期。现在你已经拥有一个运行中的数据库,你将希望让应用程序可以访问它。在我们介绍的计算、网络和存储方案中(详见 第一章),这是网络部分。

Kubernetes Services 是我们需要使用的原始方法,用来将我们的数据库作为网络服务暴露出来。一个 Service 提供了一个抽象,后面跟着一组运行的 Pods。在这个例子中,就像一个单独的 MySQL 节点一样,你可能会想知道为什么我们要费心创建这种抽象。一个 Service 支持的一个关键特性是提供一个始终不变的命名端点。当数据库 Pod 重新启动并获得新的 IP 地址时,你不希望处于必须更新客户端的情况。你可以通过使用像这样的 YAML 配置来创建访问 MySQL 的 Service。源代码 存储在本书的代码库中:

apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None

关于这个配置,有几点需要注意:

  • 这个配置指定了服务暴露的 port 是 3306. 在定义服务时,实际上涉及两个端口:服务对客户端暴露的 port 和由服务面向的底层 Pod 暴露的 targetPort。由于你没有指定 targetPort,它默认为 port 的值。

  • selector 定义了服务将流量转发到哪些 Pod。在这个配置中,Deployment 管理的只有一个 MySQL Pod,这就够了。

  • 如果您之前使用过 Kubernetes 服务,您可能会注意到此服务没有定义 serviceType,这意味着它是默认类型,称为 ClusterIP。此外,由于 clusterIP 属性设置为 None,这就是所谓的 headless Service,即服务的 DNS 名称直接映射到所选 Pod 的 IP 地址。

Kubernetes 支持多种服务类型来满足不同的使用场景,在 图 3-3 中展示。我们将在此简要介绍它们,以突出它们在数据基础设施中的适用性:

ClusterIP 服务

这种类型的服务暴露在集群内部 IP 地址上。ClusterIP 服务通常用于 Kubernetes 中的数据基础设施,特别是无头服务,因为这些基础设施通常与使用它的应用程序一起部署在 Kubernetes 中。

NodePort 服务

NodePort 服务在每个 Worker Node 的 IP 地址上对集群外部暴露。还在内部创建了一个 ClusterIP 服务,NodePort 将流量路由到该服务。您可以允许 Kubernetes 从一组端口(默认为 30000–32767)中选择外部使用的端口,或者通过使用 NodePort 属性指定您想要的端口。NodePort 服务最适合开发环境,在这种环境中,您需要调试特定实例的数据基础设施应用程序正在发生的情况。

LoadBalancer

LoadBalancer 服务代表 Kubernetes 运行时请求设置的外部负载均衡器,由底层云提供商提供。例如,在亚马逊的弹性 Kubernetes 服务 (EKS) 上,请求一个 LoadBalancer 服务会创建一个弹性负载均衡器 (ELB) 的实例。在多节点数据基础设施部署前面使用负载均衡器通常是不必要的,因为这些数据技术通常有自己的方法来分发负载。例如,Apache Cassandra 驱动程序了解 Cassandra 集群的拓扑,并为客户应用程序提供负载均衡功能,从而消除了负载均衡器的需要。

ExternalName 服务

ExternalName 服务通常用于表示访问集群外部的服务 - 例如,在 Kubernetes 外部运行的数据库。ExternalName 服务没有选择器,因为它不映射到任何 Pod。相反,它将服务名称映射到 CNAME 记录。例如,如果你创建一个 my-external-database 服务,并将 externalName 设置为 database.mydomain.com,那么应用程序 Pod 中对 my-external-database 的引用将映射到 database.mydomain.com

Kubernetes 服务类型

图 3-3. Kubernetes 服务类型

还要注意图中包含 Ingress 的部分。虽然 Kubernetes Ingress 不是服务类型,但它与服务相关。Ingress 用于从集群外部通过 HTTP 提供对 Kubernetes 服务的访问。有多种 Ingress 实现可用,包括 Nginx、Traefik、Ambassador(基于 Envoy)等。Ingress 实现通常提供 SSL 终结和跨多个 Kubernetes 服务的负载平衡等功能。与 LoadBalancer 服务类似,Ingress 更通常用于应用程序层次。

访问 MySQL

现在你已经部署了数据库,可以准备部署一个使用它的应用程序 - WordPress 服务器。首先,服务器将需要自己的 PVC。这有助于说明一些应用程序直接利用存储 - 也许是用于存储文件的应用程序、使用数据基础设施的应用程序,以及两者兼而有之。由于这仅用于演示目的,因此可以进行小规模请求。该源代码 存储在本书的代码库中:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

接下来,为单个 WordPress 节点创建一个部署。该源代码存储在本书的代码库中:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress:4.8-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-root-password
              key: password
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: wordpress-persistent-storage
        persistentVolumeClaim:
          claimName: wp-pv-claim

注意,数据库主机和访问 MySQL 的密码作为环境变量传递给 WordPress。主机的值是你为上面的 MySQL 创建的服务的名称。这是将数据库连接路由到你的 MySQL 实例所需的所有信息。密码的值从 Secret 中提取,与前面配置 MySQL 部署的方式相同。

你还会注意到,WordPress 在端口 80 上暴露了 HTTP 接口,因此让我们创建一个服务来暴露 WordPress 服务器。该源代码 存储在本书的代码库中:

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
  type: LoadBalancer

注意,该服务的类型是 LoadBalancer,这样从本地机器访问它应该相当简单。执行命令 kubectl get services 来获取 LoadBalancer 的 IP 地址;然后你可以通过 URL http://*<ip>* 在浏览器中打开 WordPress 实例。尝试登录并创建一些页面。

从 Kubernetes 发行版访问服务

访问服务的确切细节将取决于您使用的 Kubernetes 发行版,以及您是在生产环境中部署应用程序,还是像我们在此处快速测试某些内容。如果您使用的是桌面 Kubernetes 发行版,您可能希望使用 NodePort 服务而不是 LoadBalancer,以简化操作。您还可以查阅文档以获取有关访问服务的指导,例如为minikubek3d提供的指导。

当您完成对 WordPress 实例的实验后,请使用以下命令清理本地目录中使用的配置文件中指定的资源,包括您的 PersistentVolumeClaim 中存储的数据:

kubectl delete -k ./

此时,您可能会觉得这相对容易,尽管我们声称在“困难的方式”下进行操作。从某种意义上说,您是对的。到目前为止,我们部署了一个简单数据库的单个节点,使用了合理的默认设置,因此我们不需要花费太多时间进行配置。当然,如果您的应用程序只需要存储少量数据,创建单个节点是可以接受的。但是,这就是在 Kubernetes 上部署数据库的全部内容吗?当然不是!现在我们通过这个简单的数据库部署介绍了一些基本的 Kubernetes 资源之后,是时候稍微增加一些复杂性了。让我们认真做起来!

在 Kubernetes 上运行 Apache Cassandra

在这一节中,我们将看看如何在 Kubernetes 上使用 Apache Cassandra 运行多节点数据库。Cassandra 是一个 NoSQL 数据库,最初由 Facebook 开发,在 2010 年成为 Apache 软件基金会(ASF)的顶级项目。Cassandra 是一个操作性数据库,提供表格数据模型,其 Cassandra 查询语言(CQL)类似于 SQL。

Cassandra 是为云设计的数据库,通过添加节点(每个节点都是对等的)来实现水平扩展。这种去中心化设计已被证明具有接近线性的可伸缩性。Cassandra 支持高可用性,通过存储数据的多个副本或副本来实现,包括分发这些副本到多个数据中心和云区域的逻辑。Cassandra 的设计原则与 Kubernetes 类似,它被设计用于检测故障并在系统可以在后台恢复到预期状态时继续运行。所有这些特性使得 Cassandra 非常适合在 Kubernetes 上部署。

要讨论此部署如何工作,了解 Cassandra 分布数据的方法从两个角度看很有帮助:物理和逻辑。借用 Jeff Carpenter 和 Eben Hewitt(O'Reilly)的 Cassandra: The Definitive Guide 中的一些视觉效果,您可以在 图 3-4 中看到这些视角。从物理视角看,Cassandra 节点(不要与 Kubernetes 工作节点混淆)使用 机架数据中心 进行组织。尽管这些术语显示了 Cassandra 在 2000 年代中期部署在本地数据中心时的起源,但它们可以灵活应用。在云部署中,机架通常代表一个可用区,而数据中心代表云区域。无论如何表示,重要的是它们代表物理上分离的故障域。Cassandra 利用对这种拓扑的认识来确保在多个物理位置存储副本,以在发生故障时最大程度地提高数据的可用性,无论是单台机器、一整个机架、一个可用区还是整个区域。

Cassandra 分布式架构的物理和逻辑视图

图 3-4. Cassandra 分布式架构的物理和逻辑视图

逻辑视图帮助我们理解 Cassandra 如何确定将数据放置在每个节点上。Cassandra 中的每一行数据都由主键标识,主键由一个或多个分区键列组成,用于跨节点分配数据,还包括可选的聚集列,用于在分区内组织多行数据以实现高效访问。在 Cassandra 中,每次写入(以及大多数读取)都引用特定分区,提供分区键值,Cassandra 将这些值哈希在一起生成一个令牌,其值介于−2⁶³和 2⁶³^(−1)之间。Cassandra 为其每个节点分配一个或多个令牌范围的责任(在 图 3-4 中显示为每个节点的单一范围,标有字母 A 到 H,以简化说明)。物理拓扑在分配令牌范围时被考虑进去,以确保数据的副本分布在机架和数据中心之间。

现在我们准备探讨 Cassandra 如何映射到 Kubernetes。考虑 Cassandra 的架构有两个重要影响点是很重要的:

有状态性

每个 Cassandra 节点都有其负责维护的状态。Cassandra 有机制通过从其他副本流式传输数据到新节点来替换节点,这意味着可以配置节点使用本地临时存储,但启动时间会更长。然而,更常见的是配置每个 Cassandra 节点使用持久存储。无论哪种情况,每个 Cassandra 节点都需要有自己独特的 PersistentVolumeClaim。

身份

尽管每个 Cassandra 节点在其代码、配置和完全对等的体系结构中是相同的,但节点在其实际角色上是不同的。每个节点在数据中心和机架拓扑中的位置以及其分配的令牌范围都有一个身份标识。

这些身份要求和与特定 PersistentVolumeClaim 的关联为部署和 ReplicaSets 提供了一些它们未设计处理的挑战。早在 Kubernetes 存在的早期阶段,就意识到需要另一种机制来管理像 Cassandra 这样的有状态工作负载。

StatefulSets

Kubernetes 在 1.3 版本的 PetSets alpha 发布时开始提供管理有状态工作负载的资源。这一能力随着时间的推移而成熟,并且现在被称为StatefulSets(见 “您的有状态工作负载是宠物还是牛?”)。StatefulSet 与 ReplicaSet 类似,负责管理一组 Pod 的生命周期,但它管理方式上有一些显著区别。为了满足我们列出的 Cassandra 等有状态应用程序的需求,StatefulSets 展示了以下关键特性:

Pod 的稳定标识

首先,StatefulSets 为 Pod 提供了稳定的名称和网络标识。每个 Pod 根据 StatefulSet 的名称和序数编号分配一个名称。例如,一个名为 cassandra 的 StatefulSet 将拥有如 cassandra-0cassandra-1cassandra-2 等的 Pod,如 图 3-5 所示。这些是稳定的名称,因此如果一个 Pod 丢失并需要替换,新的 Pod 将具有相同的名称,即使它启动在不同的工作节点上也是如此。Pod 的名称设置为其主机名,因此如果创建一个无头服务,您实际上可以根据需要直接访问各个 Pod,例如:cassandra-1.cqlservice.default.svc.cluster.local。图中还包括一个种子服务,我们将在 “访问 Cassandra” 中讨论它。

在 Kubernetes 上使用 StatefulSets 部署 Cassandra 的示例

图 3-5. 在 Kubernetes 上使用 StatefulSets 部署 Cassandra 的示例

有序的生命周期管理

StatefulSets 为管理 Pod 的生命周期提供可预测的行为。当扩展 StatefulSet 中的 Pod 数量时,新的 Pod 将根据下一个可用编号添加,与 ReplicaSets 不同,后者的 Pod 名称后缀基于通用唯一标识符(UUID)。例如,在图 3-5 中扩展 StatefulSet 将导致创建诸如cassandra-4cassandra-5的 Pod。缩减的行为相反,因为首先删除具有最高序数编号的 Pod。这种可预测性简化了管理,例如在减少集群大小之前明确应备份哪些节点。

持久磁盘

与创建单个 PersistentVolumeClaim 共享给所有 Pod 的 ReplicaSets 不同,StatefulSets 为每个 Pod 创建一个关联的 PVC。如果 StatefulSet 中的 Pod 被替换,替换的 Pod 将绑定到具有它要替换状态的 PVC 上。Pod 失败或调度程序选择在其他节点上运行 Pod 以平衡负载可能导致替换。对于像 Cassandra 这样的数据库,当 Cassandra 节点丢失时,这使得替换节点可以立即从关联的 PersistentVolume 恢复其状态,而不需要从其他副本流式传输数据。

数据复制管理

在规划应用程序部署时,请确保考虑数据是在数据层还是存储层进行复制。像 Cassandra 这样的分布式数据库自行管理复制,根据您请求的复制因子,在多个节点上存储数据的副本,通常每个 Cassandra 数据中心为三个。您选择的存储提供程序也可能提供复制功能。如果每个 Cassandra Pod 的 Kubernetes 卷有三个副本,您可能最终存储九个数据副本。虽然这无疑提高了数据的生存能力,但这可能超出您的预算。

定义 StatefulSets

现在您已经了解了一些关于 StatefulSets 的知识,让我们来看看它们如何用于运行 Cassandra。您将使用 Kubernetes StatefulSet 配置一个简单的三节点集群,以“硬方式”表示单个包含单个机架的 Cassandra 数据中心。本节使用的源代码位于本书的存储库中。这近似于图 3-5 中显示的配置。

要在 Kubernetes 中设置 Cassandra 集群,首先需要一个无头服务。此服务表示在图 3-5 中显示的 CQL 服务,提供一个终端,客户端可以使用该终端获取 StatefulSet 中所有 Cassandra 节点的地址。源代码位于本书的存储库中:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: cassandra
  name: cassandra
spec:
  clusterIP: None
  ports:
  - port: 9042
  selector:
    app: cassandra

你将在一个 StatefulSet 的定义中引用此服务,该服务将管理你的 Cassandra 节点。源代码 存放在本书的代码库中。而不是立即应用此配置,你可能希望等到我们做一些快速解释后再进行。配置如下:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  labels:
    app: cassandra
spec:
  serviceName: cassandra
  replicas: 3
  podManagementPolicy: OrderedReady
  updateStrategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      containers:
      - name: cassandra
        image: cassandra
        ports:
        - containerPort: 7000
          name: intra-node
        - containerPort: 7001
          name: tls-intra-node
        - containerPort: 7199
          name: jmx
        - containerPort: 9042
          name: cql
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - nodetool drain
        env:
          - name: CASSANDRA_CLUSTER_NAME
            value: "cluster1"
          - name: CASSANDRA_DC
            value: "dc1"
          - name: CASSANDRA_RACK
            value: "rack1"
          - name: CASSANDRA_SEEDS
            value: "cassandra-0.cassandra.default.svc.cluster.local"
        volumeMounts:
        - name: cassandra-data
          mountPath: /var/lib/cassandra
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: standard
      resources:
        requests:
          storage: 1Gi

这是我们迄今为止一起讨论的最复杂的配置,让我们通过逐部分查看来简化它:

StatefulSet 元数据

我们已经命名和标记了这个名为 cassandra 的 StatefulSet,并且同样的字符串将作为属于该 StatefulSet 的 Pod 的选择器。

通过服务暴露 StatefulSet Pods

StatefulSet 的 spec 以对你创建的无头服务的引用开头。虽然根据 Kubernetes 规范,serviceName 不是必需的字段,但某些 Kubernetes 发行版和工具(如 Helm)期望它被填充,如果未提供值将生成警告或错误。

副本数量

replicas 字段标识了应该在此 StatefulSet 中可用的 Pod 数量。提供的值(3)反映了在实际生产部署中可能看到的最小 Cassandra 集群规模,而大多数部署要大得多,这是 Cassandra 在大规模环境中提供高性能和可用性的时候。

生命周期管理选项

podManagementPolicyupdateStrategy 描述了 Kubernetes 在集群扩展或缩减时如何管理 Pod 的滚动更新,以及如何管理 StatefulSet 中 Pod 的更新。我们将分析这些值在 “StatefulSet 生命周期管理” 中的重要性。

Pod 规范

StatefulSet 规范的下一部分是用于创建每个由 StatefulSet 管理的 Pod 的 template。模板有几个子部分。首先,在 metadata 下,每个 Pod 包含一个标签 cassandra,用于标识它属于该集合。

此模板在 containers 字段中包含一个条目,用于指定 Cassandra 容器的规范。image 字段选择官方 Cassandra Docker 镜像 的最新版本,在撰写本文时是 Cassandra 4.0。这是我们与之前引用的 Kubernetes StatefulSet 教程有所不同的地方,后者使用专门为该教程创建的自定义 Cassandra 3.11 镜像。因为我们选择使用的镜像是官方 Docker 镜像,你无需包含注册表或帐户信息来引用它,仅使用 cassandra 名称即足以标识将使用的镜像。

每个 Pod 将为各种接口暴露 portscql 端口供客户端使用,用于 Cassandra 集群节点间通信的 intra-nodetls-intra-node 端口,以及通过 Java 管理扩展(JMX)进行管理的 jmx 端口。

Pod 规范还包括帮助 Kubernetes 管理 Pod 生命周期的指令,包括livenessProbepreStop命令。接下来将学习每个指令的使用方法。

根据其文档,我们使用的镜像已经构建了两种定制 Cassandra 配置的方式,这些配置存储在cassandra.yaml文件中。一种方法是使用您提供的文件覆盖整个cassandra.yaml的内容。第二种方法是使用镜像公开的环境变量来覆盖最常用的一部分 Cassandra 配置选项。在env字段中设置这些值会导致cassandra.yaml文件中的相应设置被更新:

CASSANDRA_CLUSTER_NAME

此设置用于区分属于集群的节点。如果一个 Cassandra 节点与不匹配其集群名称的节点接触,它将忽略它们。

CASSANDRA_DCCASSANDRA_RACK

这些设置标识了每个节点将属于的数据中心和机架。这突显了 StatefulSets 在公开 Pod 规范的方式中的一个有趣的细微差别。由于模板应用于每个 Pod 和容器,因此无法在 Cassandra Pods 之间变化配置的数据中心和机架名称。因此,通常使用一个 StatefulSet 每个机架部署 Cassandra。

CASSANDRA_SEEDS

这些定义了 Cassandra 集群中节点的已知位置,新节点可以使用它们引导自己进入集群。最佳实践是指定多个种子节点,以防其中一个在新节点加入时处于关闭或离线状态。但是,对于这个初始示例,通过 DNS 名称cassandra-0.cassandra.default.svc.cluster.local指定初始 Cassandra 副本作为种子足够了。我们将在第四章中使用服务来查看更强大的指定种子的方法,正如 Seed 服务在图 3-5 中所示。

容器规范中的最后一项是volumeMount,请求在/var/lib/cassandra目录挂载 PersistentVolume,这是 Cassandra 镜像配置为存储其数据文件的位置。由于每个 Pod 将需要自己的 PersistentVolumeClaim,因此名称cassandra-data是对下一个定义的 PersistentVolumeClaim 模板的引用。

volumeClaimTemplates

StatefulSet 规范的最后一部分是 volumeClaimTemplates。规范必须包括对之前容器规范中引用的每个名称的模板定义。在这种情况下,cassandra-data 模板引用了我们在这些示例中使用的 standard StorageClass。当 Kubernetes 启动 StatefulSet 中的新 Pod 时,它将使用此模板创建请求大小为 1 GB 的 PersistentVolumeClaim。

StatefulSet 生命周期管理

现在我们有机会讨论 StatefulSet 规范的组件后,您可以继续应用源码:

kubectl apply -f cassandra-statefulset.yaml

当这个被应用时,您可以执行以下命令观察 StatefulSet 如何启动 Cassandra Pods:

kubectl get pods -w

让我们描述一些您可以从此命令输出中观察到的行为。首先,您会看到一个单独的 Pod,cassandra-0。一旦该 Pod 进入 Ready 状态,您会看到 cassandra-1 Pod,随后是 cassandra-2cassandra-1 就绪后。此行为由 StatefulSet 的 podManagementPolicy 选择指定。让我们探索可用的选项以及一些帮助定义 StatefulSet 中 Pods 管理方式的其他设置:

Pod 管理策略

podManagementPolicy 确定了向 StatefulSet 添加或移除 Pods 的时间。在我们的 Cassandra 示例中应用的 OrderedReady 策略是默认的。当此策略生效并且 Pods 被添加时,无论是在初始创建还是扩展规模,Kubernetes 逐个扩展 StatefulSet 中的 Pod。当每个 Pod 被添加时,Kubernetes 等待直到 Pod 报告为 Ready 状态才添加下一个 Pod。如果 Pod 规范包含 readinessProbe,Kubernetes 将迭代地执行提供的命令以确定 Pod 是否准备好接收流量。对于 Cassandra,可用性通常由 CQL 端口 (9042) 的可用性来衡量,这意味着节点能够响应 CQL 查询。

类似地,当 StatefulSet 被移除或者缩减规模时,Pods 会逐个被移除。在一个 Pod 被移除时,其容器中提供的 preStop 命令会被执行,以便它们有机会优雅地关闭。在我们当前的示例中,执行 nodetool drain 命令来帮助 Cassandra 节点干净地退出集群,将其 token 范围的责任分配给其他节点。Kubernetes 等待一个 Pod 完全终止后再移除下一个 Pod。livenessProbe 中指定的命令用于确定 Pod 是否活动,当它不再完成且没有错误时,Kubernetes 可以继续移除下一个 Pod。有关配置就绪性和存活探测的更多信息,请参阅 Kubernetes 文档

另一个 Pod 管理策略是Parallel。当此策略生效时,Kubernetes 会同时启动或终止多个 Pods,以便进行扩展或缩减。这会更快地将您的 StatefulSet 调整到所需的副本数,但可能会导致某些有状态工作负载需要更长时间来稳定。例如,像 Cassandra 这样的数据库在集群大小变化时会在节点之间移动数据以平衡负载,并且在逐个添加或移除节点时会更快地稳定。

无论是哪种策略,Kubernetes 都根据序号管理 Pods,始终在扩展时添加下一个未使用的序号的 Pods,并在缩减时删除具有最高序号的 Pods。

更新策略

updateStrategy描述了在更改 Pod 模板规范(例如更改容器映像)时,StatefulSet 中的 Pod 将如何更新。默认策略是RollingUpdate,就像这个示例中选择的那样。另一选项是OnDelete,您必须手动删除 Pods 才能应用新的 Pod 模板。

在滚动更新中,Kubernetes 将删除并重新创建 StatefulSet 中的每个 Pod,从最大序号的 Pod 开始向最小序号的 Pod 逐个更新。Pods 逐一更新,并且您可以指定一定数量的 Pods,称为分区,以执行分阶段发布或金丝雀发布。请注意,如果在发布过程中发现了不良的 Pod 配置,您需要将 Pod 模板规范更新为已知的良好状态,然后手动删除使用不良规范创建的任何 Pods。由于这些 Pods 永远不会达到Ready状态,Kubernetes 将不会决定将其替换为良好的配置。

请注意,Kubernetes 为 Deployments、ReplicaSets 和 DaemonSets 提供类似的生命周期管理选项,包括修订历史。

我们建议通过管理 StatefulSets 来获取更多实践经验,以强化您的知识。例如,您可以监视 StatefulSet 扩展时 PersistentVolumeClaims 的创建。另一个尝试的事项是:删除一个 StatefulSet 并重新创建它,验证新的 Pods 是否从原始 StatefulSet 恢复先前存储的数据。有关更多想法,您可能会发现这些指导性教程有帮助:来自 Kubernetes 文档的“StatefulSet 基础”和来自 Kubernetes 博客的“StatefulSet:在 Kubernetes 中轻松运行和扩展有状态应用程序”

更复杂的 StatefulSets 生命周期管理

关于 StatefulSets 的附加生命周期选项的一个有趣的观点集来自 OpenKruise,这是一个 CNCF Sandbox 项目,提供了高级 StatefulSet。高级 StatefulSet 增加了以下功能:

  • 具有最大数量不可用 Pods 的并行更新

  • 基于提供的优先级策略,使用替代顺序进行滚动更新替换

  • 更新 Pods,通过根据更新的 Pod 模板规范重新启动其容器来“就地”更新

此 Kubernetes 资源也被命名为StatefulSet,以便在最小影响现有配置的情况下使用它。您只需要更改apiVersion:从apps/v1apps.kruise.io/v1beta1

StatefulSets 在 Kubernetes 上管理有状态工作负载非常有用,甚至没有考虑到我们未解决的一些功能,如亲和性和反亲和性、管理内存和 CPU 的资源请求以及诸如 PodDisruptionBudgets(PDBs)等可用性约束。另一方面,您可能希望 StatefulSets 不能提供的功能,如备份/恢复持久卷或安全配置访问凭据。我们将讨论如何在 Kubernetes 上利用或构建这些功能,包括第四章及以后的内容。

访问 Cassandra

一旦您应用了我们列出的配置,您可以使用 Cassandra 的 CQL shell cqlsh 执行 CQL 命令。如果您是 Cassandra 用户,并在本地机器上安装了cqlsh的副本,您可以像客户端应用程序一样访问 Cassandra,使用与 StatefulSet 关联的 CQL 服务。然而,由于每个 Cassandra 节点也包含cqlsh,这为我们展示了通过直接连接到 StatefulSet 中的个体 Pod 与 Kubernetes 基础设施交互的另一种方式:

kubectl exec -it cassandra-0 -- cqlsh

这应该会启动cqlsh提示符,然后您可以使用DESCRIBE KEYSPACES来探索 Cassandra 内置表的内容,然后使用USE选择特定的 keyspace 并运行DESCRIBE TABLES。许多在线 Cassandra 教程可以指导您进行更多关于创建自己的表、插入和查询数据等示例。当您完成使用cqlsh进行实验时,可以输入exit退出 shell。

删除 StatefulSet 与删除任何其他 Kubernetes 资源相同 —— 例如,您可以按名称删除它:

kubectl delete sts cassandra

您还可以删除引用用于创建它的文件的 StatefulSet:

kubectl delete -f cassandra-statefulset.yaml

当您像本例中那样删除策略为Retain的 StatefulSet 时,它创建的 PersistentVolumeClaims 不会被删除。如果重新创建 StatefulSet,则会绑定到相同的 PVC,并重用现有数据。当您不再需要这些声明时,您需要手动删除它们。您需要执行的最后清理是删除 CQL 服务:

kubectl delete service cassandra

总结

在本章中,您已经学习了如何在 Kubernetes 上部署单节点和多节点分布式数据库,并进行了实际示例。在这过程中,您已经熟悉了 Kubernetes 资源,例如部署(Deployments)、副本集(ReplicaSets)、有状态集(StatefulSets)和守护集(DaemonSets),并了解了每种资源的最佳用例:

  • 使用 Deployments/ReplicaSets 来管理无状态工作负载或简单的有状态工作负载,例如单节点数据库或依赖临时存储的缓存。

  • 使用 StatefulSets 来管理涉及多个节点并需要与特定存储位置关联的有状态工作负载。

  • 使用 DaemonSets 来管理利用特定 Worker Node 功能的工作负载。

你还学会了每个资源可以提供的限制。现在,你已经在 Kubernetes 上部署了有状态工作负载的经验,下一步是学习如何自动化所谓的“第二天”运维操作,以保持这些数据基础设施的运行。

第四章:使用 Helm 在 Kubernetes 上自动化数据库部署

在前一章中,你学习了如何通过手工方式在 Kubernetes 上部署单节点和多节点数据库,逐个元素地创建。我们故意采取了“硬核”的方式,以帮助你最大程度地理解如何使用 Kubernetes 原语来设置数据库所需的计算、网络和存储资源。当然,这并不代表在 Kubernetes 上生产环境中运行数据库的体验,有几个原因。

首先,团队通常不会逐个 YAML 文件手动部署数据库。这可能变得非常乏味。即使将配置合并到单个文件中,对于更复杂的部署,也可能变得非常复杂。考虑在第三章中为 Cassandra 的多节点数据库部署所需的配置量的增加,与单节点 MySQL 部署相比。这对大型企业来说是不可扩展的。

其次,虽然部署数据库很棒,但如何确保其长期运行呢?您需要保证数据基础设施在长时间内保持可靠和高性能,而数据基础设施通常需要大量的维护和投入。换句话说,运行系统的任务通常分为“第一天”(将应用部署到生产环境的欣喜日子)和“第二天”(首次之后的每一天,需要操作和演进应用,同时保持高可用性)。

关于数据库部署和运维的这些考虑反映了朝向 DevOps 的更大行业趋势,即开发团队在支持生产中的应用方面发挥更积极的作用。DevOps 实践包括使用自动化工具进行应用的 CI/CD,缩短代码从开发者桌面到生产环境的时间。

在本章中,我们将介绍帮助标准化数据库和其他应用部署的工具。这些工具采用基础设施即代码(IaC)的方法,允许您以可自动执行的格式表示软件安装和配置选项,从而减少需要编写的整体配置代码量。在接下来的两章中,我们还将强调数据基础设施运营,并贯穿整本书的主题。

使用 Helm Charts 部署应用程序

让我们从看一个工具开始,这个工具有助于您管理管理配置的复杂性:Helm。这个 Kubernetes 的包管理器是开源的,也是 CNCF 的一个毕业项目。包管理器的概念在多种编程语言中是一个常见的概念,例如 Python 的 pip、JavaScript 的 Node Package Manager (NPM) 和 Ruby 的 Gems 特性。特定操作系统的包管理器也存在,例如 Linux 的 Apt 或 macOS 的 Homebrew。正如在图 4-1 中所示,包管理系统的基本要素包括包、存储包的注册表以及包管理应用程序(或客户端),它帮助 chart 开发人员注册 charts 并允许 chart 用户在其本地系统上查找、安装和更新 packages。

Helm,Kubernetes 的包管理器

图 4-1. Helm,Kubernetes 的包管理器

Helm 将包管理的概念扩展到 Kubernetes,具有一些有趣的差异。如果您曾经使用过前面列出的某个包管理器,您将会对一个包由二进制文件(可执行代码)及描述该二进制文件的元数据(如其功能、API 和安装说明)的概念感到熟悉。在 Helm 中,这些包称为charts。Charts 描述如何逐步构建 Kubernetes 应用程序,使用在前几章介绍的 Kubernetes 资源(如 Pods、Services 和 PersistentVolumeClaims)片段化地,例如计算工作负载,描述指向驻留在公共或私有容器注册表中的容器映像的情况。

Helm 允许 charts 引用其他 charts 作为依赖关系,这提供了一种通过创建 charts 的集合来组合应用程序的好方法。例如,您可以通过定义一个用于 WordPress 部署的 chart,引用一个定义 MySQL 部署的 chart 来定义一个应用程序,您希望重用这些定义。或者,您甚至可以找到一个定义了整个 WordPress 应用程序(包括数据库)的 Helm chart。

Kubernetes 环境先决条件

本章中的示例假设您可以访问一个 Kubernetes 集群,并具有以下一些特征:

  • 集群至少需要三个 Worker 节点,以展示 Kubernetes 提供的机制,允许您请求将 Pod 分布到集群中。您可以通过使用名为 kind 的开源发行版在桌面上创建一个简单的集群。有关安装 kind 和创建多节点集群的说明,请参阅 kind 快速入门指南。此示例的代码还包含一个配置文件,您可能会发现它对创建一个简单的三节点 kind 集群很有用。

  • 您还需要一个支持动态配置的 StorageClass。您可能希望按照“StorageClasses”中的说明安装一个简单的 StorageClass 和提供者,以公开本地存储。

使用 Helm 部署 MySQL

为了让事情更具体化,让我们使用 Helm 来部署您在第三章中使用的数据库。首先,如果它尚未安装在您的系统上,您需要使用 Helm 网站上的文档来安装 Helm。接下来,添加 Bitnami Helm 存储库:

helm repo add bitnami https://charts.bitnami.com/bitnami

Bitnami Helm 存储库包含各种 Helm 图表,可帮助您部署基础设施,如数据库、分析引擎和日志管理系统,以及应用程序,包括电子商务、客户关系管理(CRM),还有您猜到的:WordPress。您可以在GitHub上的 Bitnami Charts 存储库中找到这些图表的源代码。这个存储库的README提供了在不同 Kubernetes 分发中使用图表的有用说明。

现在,让我们使用bitnami存储库中提供的 Helm 图表来部署 MySQL。在 Helm 的术语中,每个部署称为一个release。使用此图表创建的可能是最简单的发布如下所示:

# don’t execute me yet!
helm install mysql bitnami/mysql

如果您执行此命令,它将使用 Bitnami MySQL Helm 图表的默认设置创建一个名为mysql的发布。因此,您将拥有一个单独的 MySQL 节点。由于您已经在第三章中手动部署了一个单节点的 MySQL,请这次尝试做一些更有趣的事情并创建一个 MySQL 集群。为此,您可以创建一个values.yaml文件,并添加以下内容,或者您可以重用源代码中提供的sample

architecture: replication
secondary:
  replicaCount: 2

values.yaml文件中的设置告诉 Helm 您希望在 Bitnami MySQL Helm 图表中使用选项来部署 MySQL 在一个有主节点和两个从节点的复制架构中。

MySQL Helm 图表的配置选项

如果您查看了 Bitnami MySQL Helm 图表提供的默认values.yaml文件,您会看到除了这里显示的简单选择外,还有很多可用的选项。可配置的值包括以下内容:

  • 要拉取的图像及其位置

  • 将用于生成持久卷的 Kubernetes StorageClass

  • 用户和管理员账户的安全凭据

  • MySQL 主和从副本的配置设置

  • 创建次要副本的数量

  • 存活性、就绪探测的详细信息

  • 亲和性和反亲和性设置

  • 使用 Pod 中断预算来管理数据库的高可用性

许多这些概念您可能已经熟悉了,而像亲和性和 Pod 中断预算这样的概念将在本书的后面进行介绍。

创建了values.yaml文件后,您可以使用以下命令启动集群:

helm install mysql bitnami/mysql -f values.yaml

运行命令后,您将从 Helm 看到安装的状态,以及图表下NOTES中提供的说明:

NAME: mysql
LAST DEPLOYED: Thu Oct 21 20:39:19 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
…

我们在此处省略了备注,因为它们有些冗长。它们描述了监视 MySQL 初始化状态的建议命令,客户端和管理员如何连接到数据库,如何升级数据库等。

使用命名空间帮助隔离资源

由于我们没有指定命名空间,因此 Helm 发布已安装在默认的 Kubernetes 命名空间中,除非您在kubeconfig中单独配置了命名空间。如果要在自己的命名空间中安装 Helm 发布,以便更有效地使用其资源,您可以运行类似以下命令:

helm install mysql bitnami/mysql \
  --namespace mysql --create-namespace

这将创建一个名为mysql的命名空间,并在其中安装mysql发布。

要获取您创建的 Helm 发布的信息,请使用helm list命令,它会生成类似以下格式的输出:

helm list
NAME   NAMESPACE  REVISION  UPDATED   
mysql  default    1         2021-10-21 20:39:19

STATUS    CHART        APP VERSION
deployed  mysql-8.8.8  8.0.26

如果您还没有将发布版本安装在其专用的命名空间中,您可以通过运行kubectl get all命令轻松查看 Helm 为您创建的计算资源,因为它们都已使用您发布的名称进行了标记。所有资源初始化可能需要几分钟,但完成后,它看起来将类似于以下内容:

kubectl get all
NAME                    READY   STATUS    RESTARTS   AGE
pod/mysql-primary-0     1/1     Running   0          3h40m
pod/mysql-secondary-0   1/1     Running   0          3h40m
pod/mysql-secondary-1   1/1     Running   0          3h38m

NAME                              TYPE       CLUSTER-IP     EXTERNAL-IP  PORT     
service/mysql-primary             ClusterIP  10.96.107.156  <none>       ...  
service/mysql-primary-headless    ClusterIP  None           <none>       ...  
service/mysql-secondary           ClusterIP  10.96.250.52   <none>       ... 
service/mysql-secondary-headless  ClusterIP  None           <none>       ...  

NAME                               READY   AGE
statefulset.apps/mysql-primary     1/1     3h40m
statefulset.apps/mysql-secondary   2/2     3h40m

正如您所见,Helm 创建了两个 StatefulSets,一个用于主要副本,另一个用于次要副本。mysql-primary StatefulSet 正在管理一个包含主要副本的 MySQL Pod,而mysql-secondary StatefulSet 正在管理包含次要副本的两个 MySQL Pods。使用kubectl describe pod命令,看看您能否确定每个 MySQL 副本运行在哪个 Kubernetes Worker Node 上。

从上述输出中,您还会注意到为每个 StatefulSet 创建了两个服务,一个是无头服务,另一个具有专用 IP 地址。由于kubectl get all只告诉您计算资源和服务的情况,您可能也想了解存储资源。要检查这些资源,请运行kubectl get pv命令。假设您安装了支持动态配置的 StorageClass,您应该看到绑定到 PersistentVolumeClaim 的 PersistentVolumes,命名为data-mysql-primary-0data-mysql-secondary-0data-mysql-secondary-1

除了我们讨论过的资源之外,安装图表还导致创建了一些额外的资源,我们将在接下来探讨。

命名空间和 Kubernetes 资源范围

如果你选择在一个命名空间中安装你的 Helm 发布版,那么在大多数kubectl get命令中,你需要指定这个命名空间才能看到创建的资源。唯一的例外是kubectl get pv,因为 PersistentVolumes 是 Kubernetes 中不归属于任何命名空间的资源之一;也就是说,它们可以被任何命名空间中的 Pod 使用。要了解集群中哪些 Kubernetes 资源是命名空间的,哪些不是,请运行命令kubectl api-resources

Helm 的工作原理

当你执行带有提供的值文件的helm install命令时,你是否想知道发生了什么?要了解发生了什么,请看一下刚刚安装的 MySQL Helm 图表的源代码,如图 4-2 所示。

使用 values.yaml 文件自定义 Helm 发布

图 4-2. 使用 values.yaml 文件自定义 Helm 发布

查看 Helm 图表的内容时,你会注意到以下内容:

README 文件

这解释了如何使用图表。这些说明与图表一起提供在注册表中。

Chart.yaml 文件

这包含关于图表的元数据,如名称、发布者、版本、关键字以及对其他图表的任何依赖关系。在 Helm 注册表中搜索图表时,这些属性非常有用。

values.yaml 文件

这会列出图表支持的可配置值及其默认值。这些文件通常包含很多注释,解释了可用选项。对于 Bitnami MySQL Helm 图表,提供了许多选项,正如我们所注意到的。

templates 目录

这包含定义图表的 Go 模板。模板包括一个用于生成你之前在执行helm install命令后看到的输出的Notes.txt文件,以及一个或多个描述 Kubernetes 资源模式的 YAML 文件。这些 YAML 文件可能以子目录的形式组织(例如,定义 MySQL 主副本的 StatefulSet 的模板)。最后,_helpers.tpl文件描述了如何使用模板。根据所选的配置值,某些模板可能会被多次使用或根本不使用。

当你执行helm install命令时,Helm 客户端确保通过与源代码库检查来获取你命名的图表的最新副本。然后,它使用模板生成 YAML 配置代码,覆盖图表的values.yaml文件中的默认值以及你提供的任何值。然后,它使用kubectl命令将此配置应用于当前配置的 Kubernetes 集群。

如果你想在应用之前查看 Helm 图表生成的配置,可以使用方便的 template 命令。它支持与 install 命令相同的语法:

helm template mysql bitnami/mysql -f values.yaml

运行此命令将会产生大量输出,因此你可能想将其重定向到文件(在命令后添加 > values-template.yaml),以便长时间查看。或者,你可以查看我们在源代码仓库中保存的副本

你会注意到生成了几种类型的资源,如 图 4-3 所总结的。许多显示的资源已经讨论过,包括用于管理主副本的 StatefulSets,每个都有自己的服务(该图还创建了未在图中显示的无头服务)。每个 Pod 都有自己的 PersistentVolumeClaim,映射到唯一的 PersistentVolume。

图 4-3 包含了我们之前未讨论过的资源类型。首先注意到每个 StatefulSet 都有一个关联的 ConfigMap,用于为其 Pods 提供一组共享的配置设置。接着,注意到名为 mysql 的 Secret,用于存储访问数据库节点暴露的各种接口所需的密码。最后,每个由此 Helm 发布创建的 Pod 都应用了一个 ServiceAccount 资源。

让我们专注于此部署的一些有趣方面,包括标签、ServiceAccounts、Secrets 和 ConfigMaps 的使用。

使用 Bitnami Helm 图表部署 MySQL

图 4-3. 使用 Bitnami Helm 图表部署 MySQL

标签

如果你查看从 helm template 输出的内容,你会注意到这些资源具有一组共同的标签:

  labels:
    app.kubernetes.io/name: mysql
    helm.sh/chart: mysql-8.8.8
    app.kubernetes.io/instance: mysql
    app.kubernetes.io/managed-by: Helm

这些标签有助于识别资源作为 mysql 应用程序的一部分,并指示它们是通过特定图表版本由 Helm 管理的。标签对于选择资源非常有用,通常用于定义其他资源的配置。

ServiceAccounts

Kubernetes 集群为了访问控制目的区分了人类用户和应用程序。ServiceAccount 是一个代表应用程序及其访问权限的 Kubernetes 资源。例如,ServiceAccount 可以被授予访问 Kubernetes API 的某些部分或者访问包含登录凭据等敏感信息的一个或多个 Secrets 的权限。后者的功能在你的 MySQL Helm 安装中用于在 Pods 之间共享凭据。

在 Kubernetes 中创建的每个 Pod 都有一个分配给它的 ServiceAccount。如果未指定,则使用默认的 ServiceAccount。安装 MySQL Helm 图表会创建一个名为 mysql 的 ServiceAccount。你可以在生成的模板中查看此资源的规范:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: mysql
  namespace: default
  labels: ...
  annotations:
secrets:
  - name: mysql

如您所见,这个 ServiceAccount 可以访问名为 mysql 的 Secret,我们将很快讨论。ServiceAccount 还可以具有称为 imagePullSecret 的附加类型的 Secret。当应用程序需要使用来自私有注册表的镜像时,会使用这些 Secrets。

默认情况下,ServiceAccount 没有任何访问 Kubernetes API 的权限。为了给这个 ServiceAccount 提供所需的访问权限,MySQL Helm 图表创建一个角色,指定 Kubernetes 资源和操作,并创建一个 RoleBinding 将 ServiceAccount 绑定到该角色。我们将在第五章讨论 ServiceAccounts 和基于角色的访问控制。

Secrets

正如您在第二章学到的那样,Secret 提供了安全访问需要保密的信息。您的 mysql Helm 发布包含一个名为 mysql 的 Secret,其中包含 MySQL 实例本身的登录凭据:

apiVersion: v1
kind: Secret
metadata:
  name: mysql
  namespace: default
  labels: ...
type: Opaque
data:
  mysql-root-password: "VzhyNEhIcmdTTQ=="
  mysql-password: "R2ZtNkFHNDhpOQ=="
  mysql-replication-password: "bDBiTWVzVmVORA=="

这三个密码代表不同类型的访问权限:mysql-root-password 提供对 MySQL 节点的管理访问,mysql-replication-password 用于节点之间进行数据复制的通信,而 mysql-password 则由客户端应用程序用于访问数据库以读写数据。

ConfigMaps

Bitnami MySQL Helm 图表创建 Kubernetes ConfigMap 资源,用于表示运行 MySQL 主节点和次要复制节点的 Pod 的配置设置。ConfigMaps 将配置数据存储为键-值对。例如,Helm 图表为主复制品创建的 ConfigMap 如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-primary
  namespace: default
  labels: ...
data:
  my.cnf: |-

    [mysqld]
    default_authentication_plugin=mysql_native_password
    ...

在这种情况下,键是 my.cnf 的名称,表示文件名,值是代表配置文件内容的多行设置(在这里我们进行了简化)。接下来,查看主复制品的 StatefulSet 的定义。请注意,根据 StatefulSet 的 Pod 规范,ConfigMap 的内容被挂载为每个模板内的只读文件(再次为了关注关键区域,我们省略了一些细节):

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql-primary
  namespace: default
  labels: ...
spec:
  replicas: 1
  selector:
    matchLabels: ...
  serviceName: mysql-primary
  template:
    metadata:
      annotations: ...
      labels: ...
    spec:
      ...     
      serviceAccountName: mysql
      containers:
        - name: mysql
          image: docker.io/bitnami/mysql:8.0.26-debian-10-r60
          volumeMounts:
            - name: data
              mountPath: /bitnami/mysql
            - name: config
              mountPath: /opt/bitnami/mysql/conf/my.cnf
              subPath: my.cnf
      volumes:
        - name: config
          configMap:
            name: mysql-primary

将 ConfigMap 挂载为容器中的卷会在挂载目录中创建一个只读文件,文件名根据键命名,内容为对应的值。例如,在 Pod 的 mysql 容器中挂载 ConfigMap 将创建文件 /opt/bitnami/mysql/conf/my.cnf

这是 ConfigMaps 可以在 Kubernetes 应用程序中使用的几种方式之一:

  • Kubernetes 文档 所述,您可以选择以更精细的键-值对存储配置数据,这样可以更轻松地访问应用程序中的各个值。

  • 您还可以将单独的键-值对引用为传递给容器的环境变量。

  • 最后,应用程序可以通过 Kubernetes API 访问 ConfigMap 的内容。

更多配置选项

现在,您已经有一个具有工作 MySQL 集群的 Helm 发布,您可以将一个应用程序指向它,例如 WordPress。为什么不尝试看看是否可以调整第三章中的 WordPress 部署,使其指向您在这里创建的 MySQL 集群呢?

进行进一步学习时,您还可以将您的配置结果与使用 MariaDB 而不是 MySQL 的 Bitnami WordPress Helm 图表生成的结果进行比较,但其余部分非常相似。

更新 Helm 图表

如果您在生产环境中运行 Helm 发布,很可能需要随时间维护它。您可能出于各种原因想要更新 Helm 发布:

  • 图表的新版本已经可用。

  • 您的应用程序使用的图像的新版本已经可用。

  • 您想要更改所选的选项。

要检查图表的新版本,请执行helm repo update命令。运行此命令而不带任何选项将在您为 Helm 客户端配置的所有图表库中查找更新:

helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈

接下来,您将希望对配置的值进行任何所需的更新。如果您要升级到图表的新版本,请确保检查可配置值的发布说明和文档。在应用之前测试升级是一个好主意。--dry-run选项允许您执行此操作,生成与helm template命令类似的值:

helm upgrade mysql bitnami/mysql -f values.yaml --dry-run

使用覆盖配置文件

您可以用于升级的一个有用选项是指定要在新配置文件中覆盖的值,并应用新旧两者,类似于这样:

helm upgrade mysql bitnami/mysql \
  -f values.yaml -f new-values.yaml

配置文件按照它们在命令行上出现的顺序应用,因此如果您使用此方法,请确保您的覆盖值文件出现在原始值文件之后。

升级应用后,Helm 开始执行其工作,仅更新受您配置更改影响的发布中的资源。如果您已经为 StatefulSet 的 Pod 模板指定了更改,则 Pod 将根据 StatefulSet 的更新策略重新启动,正如我们在“StatefulSet 生命周期管理”中讨论的那样。

卸载 Helm 图表

当您完成使用 Helm 发布时,可以按名称卸载它:

helm uninstall mysql

请注意,Helm 不会删除为此 Helm 图表创建的任何 PersistentVolumeClaims 或 PersistentVolumes,这遵循我们在第三章中讨论的 StatefulSets 的行为。

使用 Helm 部署 Apache Cassandra

现在让我们换个角度来看,使用 Helm 部署 Apache Cassandra。在本节中,您将使用 Bitnami 提供的另一个图表,因此不需要添加其他存储库。您可以在GitHub上找到此图表的实现。Helm 提供了一种快速查看有关此图表的元数据的方法:

helm show chart bitnami/cassandra

在查看了元数据之后,你还会想了解可配置的值。你可以检查 GitHub 存储库中的values.yaml文件,或者在show命令上使用另一个选项:

helm show values bitnami/cassandra

与 MySQL 图表相比,此图表的选项列表较短,因为 Cassandra 没有主要和次要副本的概念。然而,你肯定会看到类似的选项,如镜像、StorageClasses、安全性、存活和就绪探针等。一些配置选项是 Cassandra 特有的,例如与 JVM 设置和种子节点相关的选项(如第三章所讨论的)。

此图表的一个有趣功能是能够从 Cassandra 节点导出指标。如果设置了metrics.enabled=true,图表将在每个 Cassandra Pod 中注入一个 sidecar 容器,该容器公开一个可以被 Prometheus 抓取的端口。metrics下的其他值配置导出的指标、收集频率等。虽然我们这里不会使用此功能,但指标报告是管理数据基础设施的关键部分,我们将在第六章中进行介绍。

对于一个简单的三节点 Cassandra 配置,你可以将副本数设置为 3,并将其他配置值设置为默认值。然而,既然你只是覆盖了一个配置值,这是一个很好的机会来利用 Helm 支持的在命令行上设置值,而不是提供一个values.yaml文件:

helm install cassandra bitnami/cassandra --set replicaCount=3

正如之前讨论的,你可以使用helm template命令在安装之前检查配置,或查看我们在 GitHub 上保存的文件。然而,既然你已经创建了发布版本,你也可以使用这个命令:

helm get manifest cassandra

在 YAML 中查看资源时,你会看到已建立了类似的基础设施,如图 4-4 所示。

配置包括以下内容:

  • 一个引用了包含cassandra管理员帐户密码的 Secret 的 ServiceAccount。

  • 一个单一的 StatefulSet,使用无头 Service 引用其 Pod。Pod 均匀分布在可用的 Kubernetes Worker 节点上,我们将在下一节讨论。该 Service 公开了用于节点内通信的 Cassandra 端口(7000,使用 TLS 进行安全通信的7001),通过 JMX 进行管理的端口(7199),以及通过 CQL 进行客户端访问的端口(9042)。

使用 Bitnami Helm 图表部署 Apache Cassandra

图 4-4. 使用 Bitnami Helm 图表部署 Apache Cassandra

此配置表示一个简单的 Cassandra 拓扑结构,所有三个节点位于单个数据中心和机架中。这种简单的拓扑反映了此图表的一个限制——它无法创建由多个数据中心和机架组成的 Cassandra 集群。要创建更复杂的部署,您需要安装多个 Helm 发布,使用相同的clusterName(在本例中,使用默认名称cassandra),但每个部署中的数据中心和机架不同。您还需要获取第一个数据中心中几个节点的 IP 地址,用作在配置其他机架的发布时的additionalSeeds

亲和性和反亲和性

如 Figure 4-4 所示,Cassandra 节点均匀分布在集群中的工作节点上。要在您自己的 Cassandra 发布中验证这一点,您可以运行类似以下内容的命令:

kubectl describe pods | grep "^Name:" -A 3
Name:         cassandra-0
Namespace:    default
Priority:     0
Node:         kind-worker/172.20.0.7
--
Name:         cassandra-1
Namespace:    default
Priority:     0
Node:         kind-worker2/172.20.0.6
--
Name:           cassandra-2
Namespace:      default
Priority:       0
Node:           kind-worker3/172.20.0.5

如您所见,每个 Cassandra 节点都运行在不同的工作节点上。如果您的 Kubernetes 集群至少有三个工作节点且没有其他工作负载,则可能会观察到类似的行为。尽管在负载均衡的集群中可能会自然发生这种均匀分配,但这可能并非在您的生产环境中的情况。然而,为了促进数据的最大可用性,我们希望尽量遵循 Cassandra 架构的意图,在不同的机器上运行节点以提高高可用性。

为了帮助保证此隔离性,Bitnami Helm 图表使用 Kubernetes 的亲和性能力,具体来说是反亲和性。如果您查看生成的 Cassandra StatefulSet 配置,您会看到以下内容:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  namespace: default
  labels: ...
spec:
  ...
  template:
    metadata:
      labels: ...
    spec:
      ...
      affinity:
        podAffinity:

        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app.kubernetes.io/name: cassandra
                    app.kubernetes.io/instance: cassandra
                namespaces:
                  - "default"
                topologyKey: kubernetes.io/hostname
              weight: 1
        nodeAffinity:

如此所示,Pod 模板规范列出了三种可能的亲和性类型,仅定义了podAntiAffinity。这些概念意味着什么?

Pod 亲和性

希望 Pod 被调度到另一个特定 Pod 正在运行的节点上。例如,Pod 亲和性可以用于将 Web 服务器与其缓存放置在一起。

Pod 反亲和性

Pod 反亲和性是 Pod 亲和性的相反,即不希望 Pod 被调度到另一个已有特定 Pod 的节点上。这是本例中使用的约束条件,稍后我们将讨论。

节点亲和性

希望 Pod 在具有特定特征的节点上运行。

每种类型的亲和性可以表示为硬约束或软约束。这些被称为requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution。第一个约束指定必须在 Pod 被调度到节点之前满足的规则,而第二个指定了调度器将尝试满足的首选项,但如果必要的话可以放松以便调度 Pod。

IgnoredDuringExcecution 意味着这些约束仅在首次调度 Pods 时应用。未来将添加新的 RequiredDuringExecution 选项,称为 requiredDuringSchedulingRequiredDuringExecutionrequiredDuringSchedulingRequiredDuringExecution。这些选项将要求 Kubernetes 逐出 Pods(即将其移动到另一个节点),这些 Pods 不再满足条件,例如它们标签的变更。

查看前面的示例,Cassandra StatefulSet 的 Pod 模板规范指定了使用应用于每个 Cassandra Pod 的标签的反亲和性规则。其主要效果是 Kubernetes 将尝试在可用的 Worker 节点上分布这些 Pods。

这些是查看 Cassandra 的 Bitnami Helm 图表的亮点。为了清理事务,卸载 Cassandra 发行版:

helm uninstall cassandra

如果您不想再使用 Bitnami Helm 图表,您也可以从 Helm 客户端中删除该存储库:

helm repo remove bitnami

更多 Kubernetes 调度约束

Kubernetes 支持额外的机制来向其调度器提供有关 Pod 放置的提示。其中最简单的之一是 NodeSelectors,它与节点亲和力非常相似,但语法更少表达性,可以使用 AND 逻辑匹配一个或多个标签。由于您可能没有必要的权限将标签附加到集群中的 Worker 节点,Pod 亲和性通常是更好的选择。 污点和容忍 是另一种机制,用于配置 Worker 节点以排斥特定的 Pods,以防它们被调度到这些节点上。

通常,您需要小心理解对 Kubernetes 调度器的各种工作负载施加的所有约束,以免过度限制其放置 Pod 的能力。有关 调度约束 的更多信息,请参阅 Kubernetes 文档。我们还将查看 Kubernetes 允许您在 “Kubernetes 的替代调度器” 中插入不同的调度器。

Helm、CI/CD 和运维

Helm 是一个专注于一个主要任务的强大工具:将复杂应用部署到 Kubernetes 集群。要从 Helm 中获得最大的利益,您需要考虑它如何融入您更大的 CI/CD 工具集中:

  • 自动化服务器(如 Jenkins)根据称为 jobs 的脚本自动构建、测试和部署软件。这些 jobs 通常基于预定义的触发器运行,例如提交到源代码库。可以在 jobs 中引用 Helm 图表,以在 Kubernetes 集群中安装应用程序及其支持基础设施。

  • IaC 自动化工具(如Terraform)允许您定义模板和脚本,描述如何在各种云环境中创建基础架构。例如,您可以编写一个 Terraform 脚本,自动创建特定云提供商中的新 VPC,并在该 VPC 内创建新的 Kubernetes 集群。然后,该脚本可以使用 Helm 在 Kubernetes 集群内安装应用程序。

尽管这些工具提供的功能存在重叠,但在构建工具集时,您需要考虑每个工具的优势和限制。因此,我们希望强调一下 Helm 在管理其部署的应用程序操作时存在的限制。为了全面了解涉及的挑战,我们与一位使用 Helm chart 构建复杂数据库部署的实践者进行了交流。这次讨论开始介绍 Kubernetes Custom Resource Definitions (CRDs) 和 operator 模式等概念,我们将在第五章中对其进行深入讨论。

正如 John Sanda 在他的评论中指出的那样,Helm 是一个强大的工具,用于脚本化部署由多个 Kubernetes 资源组成的应用程序,但在管理更复杂的操作任务时可能效果不佳。正如您将在接下来的章节中看到的,用于数据基础设施和其他复杂应用的常见模式是使用 Helm chart 来部署 operator,后者可以管理应用程序的部署和生命周期。

概要

在本章中,您已经学习了像 Helm 这样的包管理工具如何帮助您管理在 Kubernetes 上部署应用程序,包括数据库基础架构。在这过程中,您还学习了如何使用一些额外的 Kubernetes 资源,如 ServiceAccounts、Secrets 和 ConfigMaps。现在是时候进一步讨论在 Kubernetes 上运行数据库了。在下一章中,我们将深入探讨通过使用 operator 模式在 Kubernetes 上管理数据库操作。

第五章:使用 Operator 在 Kubernetes 上自动化数据库管理。

在本章中,我们将继续探讨在 Kubernetes 上运行数据库的话题,但将焦点从安装转向操作。仅仅了解数据库应用程序的各个元素如何映射到 Kubernetes 提供的原语以进行初始部署是不够的。您还需要知道如何随着时间推移维护该基础设施,以支持业务关键的应用程序。在本章中,我们将详细研究 Kubernetes 的操作方法,以便您可以有效地保持数据库的运行。

数据库和其他数据基础设施的操作包括一个常见的“第二天”任务列表,包括以下内容:

  • 扩展容量的上下调整,包括在调整大小的集群中重新分配工作负载。

  • 监控数据库健康状态并替换失败(或正在失败)的实例。

  • 执行常规维护任务,如在 Apache Cassandra 中进行修复操作。

  • 更新和打补丁软件。

  • 维护安全访问密钥和其他可能随时间过期的凭证。

  • 执行备份,并在灾难恢复中使用它们来恢复数据。

尽管执行这些任务的具体细节在不同技术之间可能会有所不同,但共同关注的是如何利用自动化来减少人工操作员的工作量,并使我们能够在越来越大的规模上操作基础设施。我们如何整合人工操作员在这些任务中积累的知识?传统的云操作使用在云基础设施外部运行的脚本工具,而更符合云原生的方法是在您的 Kubernetes 集群中直接运行这些数据库控制逻辑。本章我们将探讨的问题是:如何用符合 Kubernetes 的方式来表示这种控制逻辑?

扩展 Kubernetes 控制平面。

好消息是,Kubernetes 的设计者们对这个问题并不感到意外。事实上,Kubernetes 的控制平面和 API 设计为可扩展的。Kelsey Hightower 和其他人称 Kubernetes 为 “构建平台的平台”

Kubernetes 提供多个扩展点,主要与其控制平面相关。图 5-1 包括 Kubernetes 核心组件 如 API 服务器、调度器、Kubelet 和 kubectl,以及它们支持的 扩展点 的指示。

Kubernetes 控制平面和扩展点

图 5-1. Kubernetes 控制平面和扩展点。

现在让我们详细讨论扩展 Kubernetes 控制平面的细节,从本地客户端上的组件开始,到 Kubernetes 集群内部的组件。这些扩展点中许多与数据库和数据基础设施相关。

扩展 Kubernetes 客户端。

kubectl 命令行工具是许多用户与 Kubernetes 交互的主要接口。您可以通过下载并将插件放置在系统的 PATH 中,或使用 Krew,一个维护 kubectl 插件列表 的包管理器来扩展 kubectl。插件执行诸如跨 多个资源 或甚至 多个集群 的批量操作,或评估集群的状态并做出 安全性成本 建议。在本章的特定焦点下,有几个插件可用于管理操作符和自定义资源。

扩展 Kubernetes 控制平面组件

Kubernetes 控制平面的核心包括几个 控制平面组件,包括 API 服务器、调度器、控制器管理器、云控制器管理器和 etcd。虽然这些组件可以在 Kubernetes 集群中的任何节点上运行,但通常会分配给专用节点,该节点不运行任何用户应用 Pod。这些组件如下:

API 服务器

这是 Kubernetes 集群的外部和内部客户端的主要接口。它通过 HTTP API 暴露 RESTful 接口。API 服务器扮演协调角色,将客户端的请求路由到其他组件以实现命令式和声明式指令。API 服务器支持两种类型的扩展:自定义资源和 API 聚合。CRD 允许您添加新类型的资源,并通过 kubectl 进行管理,无需进一步扩展。API 聚合允许您通过额外的 REST 端点扩展 Kubernetes API,API 服务器将这些请求委派给作为插件提供的独立 API 服务器。自定义资源是更常用的扩展机制,在本书的后续部分将是重点讨论的内容。

调度器

这决定了将 Pod 分配给工作节点的方式,考虑因素包括每个工作节点的负载,以及亲和性规则、污点和容忍性(如 第四章 中讨论的)。调度器可以通过插件进行扩展,在其决策过程的多个点上覆盖默认行为。例如,调度插件 可以过滤出特定类型的 Pod 的节点,或者通过分配分数设置节点的相对优先级。绑定插件 可以自定义准备节点以运行调度 Pod 的逻辑,例如挂载调度 Pod 需要的网络卷。依赖于运行大量短期任务的 Apache Spark 等数据基础设施可能会从这种对调度决策更细粒度控制的能力中受益,我们将在 “Kubernetes 的替代调度器” 中讨论这一点。

etcd

这个分布式键值存储由 API 服务器用于持久化集群配置和状态信息。当资源被添加、删除和更新时,API 服务器相应地更新 etcd 中的元数据,因此如果 API 服务器崩溃或需要重新启动,它可以轻松恢复其状态。作为一个支持高可用性的强一致性数据存储,etcd 经常被其他运行在 Kubernetes 上的数据基础设施频繁使用,正如我们将在整本书中经常看到的那样。

控制器管理器和云控制器管理器

控制器管理器和云控制器管理器集成了称为控制器的多个控制循环。这些管理器包含了多个逻辑上独立的控制器,编译成单个可执行文件,以简化 Kubernetes 管理自身的能力。控制器管理器包含管理内置资源类型(如 Pod、StatefulSets 等)的控制器。云控制器管理器包含了不同于 Kubernetes 提供者的控制器,以便管理特定于平台的资源,例如负载均衡器或虚拟机(VMs)。

扩展 Kubernetes 工作节点组件

Kubernetes 控制平面的某些元素在集群中的每个节点上运行。这些工作节点组件包括 Kubelet、kube-proxy 和容器运行时:

Kubelet

这管理由调度程序分配给节点的 Pod,包括运行在 Pod 内的容器。Kubelet 在需要时重新启动容器,提供对容器日志的访问等功能。

计算、网络和存储插件

Kubelet 可以通过插件进行扩展,利用底层环境提供的独特计算、网络和存储能力。计算插件包括容器运行时,以及设备插件,这些插件暴露了专用硬件功能,如 GPU 或可编程门阵列(FPGA)。网络插件,包括符合容器网络接口(CNI)的插件,可以提供超出 Kubernetes 内置网络的功能,如带宽管理或网络策略管理。我们之前在“Kubernetes 存储架构”中讨论了存储插件,包括符合 CSI 的插件。

Kube-proxy

这维护运行在工作节点上的 Pod 的网络路由,以便它们可以与集群内运行的其他 Pod、或者集群外的客户端和服务进行通信。Kube-proxy 是 Kubernetes 服务实现的一部分,提供将虚拟 IP 映射到工作节点上单个 Pod 的功能。

容器运行时

Kubelet 使用容器运行时在工作节点的操作系统上执行容器。Linux 支持的容器运行时包括containerdCRI-O。在 Kubernetes 1.20 中,Docker 运行时支持已被弃用,并在 1.24 中完全移除。

自定义控制器和操作者

这些控制器负责使用自定义资源管理安装在 Kubernetes 集群上的应用程序。尽管这些控制器是 Kubernetes 控制平面的扩展,它们可以运行在任何工作节点上。

操作者模式

在这个背景下,我们现在可以研究 Kubernetes 中最常见的扩展模式之一:操作者模式。这种模式结合了自定义资源和对这些资源进行操作的控制器。让我们更详细地看看每个概念如何适用于数据基础设施,然后您将准备好深入研究一个 MySQL 操作者的示例。

控制器

控制器的概念源自电子和电气工程领域,其中控制器是在连续循环中运行的设备。在每次循环迭代中,设备接收一个输入信号,将其与设定值进行比较,并生成一个输出信号,旨在产生环境变化,未来的输入信号可以检测到这种变化。一个简单的例子是恒温器,当空间内的温度过高或过低时,它会启动空调或加热器。

Kubernetes 控制器 实现了类似的控制循环,包括以下步骤:

  1. 读取资源的当前状态

  2. 修改资源的状态

  3. 更新资源的状态

  4. 重复

这些步骤既由运行在控制器管理器中的 Kubernetes 内置控制器和云控制器管理器实现,也由提供给 Kubernetes 上运行应用程序的自定义控制器 实现。让我们看一些管理数据基础设施的控制器可能涉及的示例:

读取资源的当前状态

一个控制器跟踪一个或多个资源类型的状态,包括内置资源如 Pods、PersistentVolumes 和 Services,以及自定义资源(我们将在下一节中讨论)。控制器通过 API 服务器发送的通知驱动异步工作。API 服务器向控制器发送观察事件,以通知它们资源状态的变化,例如资源的创建或删除,或者资源上发生的事件。

对于数据基础设施,这些变更可能包括集群请求副本数量的变更,或者包含数据库副本的 Pod 已经死亡的通知。由于在大型集群中可能会发生许多此类更新,控制器经常使用缓存。

修改资源状态

这是控制器的核心业务逻辑——比较资源的状态与其期望状态,并执行操作将状态更改为期望状态。在 Kubernetes API 中,当前状态以资源的.status字段表示,期望状态则以.spec字段表示。更改可能包括调用 Kubernetes API 以修改其他资源,对被管理应用程序的管理操作,甚至与 Kubernetes 集群外的交互。

例如,考虑一个管理具有多个副本的分布式数据库的控制器。当数据库控制器收到副本数量增加的通知时,控制器可以扩展正在使用的 Deployment 或 StatefulSet 以管理副本。随后,当收到关于创建用于托管新副本的 Pod 的通知时,控制器可以在一个或多个副本上启动操作,以重新平衡这些副本之间的工作负载。

更新资源的状态

在控制循环的最后一步中,控制器使用 API 服务器更新资源的.status字段,进而在 etcd 中更新该状态。在之前的章节中,您使用 kubectl getkubectl describe 命令查看过诸如 Pods 和 PersistentVolumes 的状态。例如,Pod 的状态包括其总体状态(PendingRunningSucceededFailed等)、最近记录各种条件的时间(PodScheduledContainersReadyInitializedReady)以及其每个容器的状态(WaitingRunningTerminated)。自定义资源也可以定义自己的状态字段。例如,表示集群的自定义资源可能具有反映集群整体可用性及其当前拓扑的状态值。

事件

控制器还可以通过 Kubernetes API 生成事件,供人类操作员或其他应用程序消费。这些事件与之前描述的监视器事件不同,Kubernetes API 使用这些事件来通知控制器进行更改,但不会暴露给其他客户端。

编写自定义控制器

虽然您可能永远不需要编写自己的控制器,但熟悉涉及的概念是有帮助的。编程 Kubernetes 对于那些有兴趣深入了解的人来说是一个很好的资源。

controller-runtime 项目 提供了一组通用的库,帮助编写控制器,包括从 API 服务器注册通知、缓存资源状态、实现协调循环等。Controller-runtime 库是用 Go 编程语言实现的,因此大多数控制器也是用 Go 实现的,这一点并不令人惊讶。

Go 最初于 2007 年在 Google 开发,并被用于包括 Borg(Kubernetes 的前身)在内的许多云原生应用中。Go 是一种强类型的编译语言(与解释性语言如 Java 和 JavaScript 相对),注重可用性和开发者生产力(作为对 C/C++ 学习曲线较高的反应)。

如果你曾经错误配置过 Pod 规范并观察到 CrashLoopBackOff 状态,你可能已经遇到了事件。使用 kubectl describe pod 命令,你可以观察到事件,例如容器启动失败,随后是回退周期,然后容器重新启动。事件在 API 服务器中的有效期为一小时,但常见的 Kubernetes 监控工具提供了跟踪它们的功能。控制器还可以为自定义资源创建事件。

自定义资源

控制器可以操作内置的 Kubernetes 资源以及自定义资源。我们已经简要提到这个概念,但现在让我们利用这个机会定义一下什么是自定义资源以及它们如何扩展 Kubernetes API。

从根本上讲,自定义资源是 Kubernetes 认可为其 API 的一部分的配置数据。虽然自定义资源类似于 ConfigMap,但其结构类似于内置资源:元数据、规范和状态。特定自定义资源类型的属性在 CRD 中定义。CRD 本身是一个用于描述自定义资源的 Kubernetes 资源。

在本书中,我们已经讨论了 Kubernetes 如何使您能够超越管理 VM 和容器,进而管理虚拟数据中心。CRD 提供了灵活性,有助于实现这一实际需求。您不再仅限于 Kubernetes 提供的现成资源,可以创建额外的抽象来扩展 Kubernetes 以满足您自己的需求。这是快速发展生态系统中的关键组成部分。

让我们从命令行了解一下 CRD 的相关知识。使用 kubectl api-resources 命令列出集群中定义的所有资源:

kubectl api-resources
NAME               SHORTNAMES  APIVERSION  NAMESPACED  KIND
bindings                       v1          true        Binding
componentstatuses  cs          v1          false       ComponentStatus
configmaps         cm          v1          true        ConfigMap
...

当你查看输出时,你会看到在前几章介绍过的许多资源类型及其简称:StorageClass (sc)、PersistentVolumes (pv)、Pods (po)、StatefulSets (sts) 等等。API 版本提供了每种资源类型来源的一些线索。例如,版本为 v1 的资源是核心 Kubernetes 资源。其他版本如 apps/v1networking.k8s.io/v1storage.k8s.io/v1 表示由不同的 Kubernetes SIGs 定义的资源。

根据你使用的 Kubernetes 集群的配置,可能已经定义了一些 CRD。如果有任何 CRD 存在,它们将出现在 kubectl api-resources 命令的输出中。它们将通过其 API 版本显著突出,通常包含除 k8s.io 之外的路径。

由于 CRD 本身是 Kubernetes 资源,您也可以使用命令kubectl get crd列出安装在 Kubernetes 集群中的自定义资源。例如,在安装了下一节中提到的 Vitess Operator 后,您会看到几个 CRD:

kubectl get crd
NAME                                   CREATED AT
etcdlockservers.planetscale.com        2021-11-21T22:06:04Z
vitessbackups.planetscale.com          2021-11-21T22:06:04Z
vitessbackupstorages.planetscale.com   2021-11-21T22:06:04Z
vitesscells.planetscale.com            2021-11-21T22:06:04Z
vitessclusters.planetscale.com         2021-11-21T22:06:04Z
vitesskeyspaces.planetscale.com        2021-11-21T22:06:04Z
vitessshards.planetscale.com           2021-11-21T22:06:04Z

我们将在本书的其余部分介绍这些自定义资源的用法,但现在让我们专注于特定 CRD 的机制,看看它如何扩展 Kubernetes。您可以使用kubectl describe crdkubectl get crd命令查看 CRD 的定义。例如,要获取vitesskeyspace自定义资源的 YAML 格式描述,您可以运行以下命令:

kubectl get crd vitesskeyspaces.planetscale.com -o yaml
...

查看这个 CRD 的原始 YAML 配置,您会看到类似于这样的内容:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.3.0
  creationTimestamp: null
  name: vitesskeyspaces.planetscale.com
spec:
  group: planetscale.com
  names:
    kind: VitessKeyspace
    listKind: VitessKeyspaceList
    plural: vitesskeyspaces
    shortNames:
    - vtk
    singular: vitesskeyspace
  scope: Namespaced
  subresources:
    status: {}
  validation:
    openAPIV3Schema:
      properties:
        ...

从这部分定义中,您可以看到自定义资源的名称或种类以及shortName的声明。scope指定为Namespaced意味着此类型的自定义资源限定在单个命名空间中。

定义中最长的部分是validation部分,由于其相当大,我们已经省略了。Kubernetes 支持在自定义资源类型中定义属性,并使用 OpenAPI v3 schema(用于记录 RESTful API,进而使用 JSON schema 描述用于验证 JSON 对象的规则)来定义这些类型的合法值。验证规则确保当您创建或更新自定义资源时,对象的定义是有效的,并且可以被 Kubernetes 控制平面理解。验证规则用于生成您在应用程序中定义这些自定义资源实例时使用的文档。

一旦 CRD 安装在您的 Kubernetes 集群中,您可以使用kubectl创建和与资源交互。例如,kubectl get vitesskeyspaces将返回一个 Vitess keyspace 列表。您可以通过向kubectl apply命令提供符合规范的 YAML 定义来创建 Vitess keyspace 的实例。

操作员

现在您已经了解了自定义控制器和自定义资源,让我们将这些线索重新联系在一起。操作员是自定义资源和自定义控制器的组合,用于维护这些资源的状态并管理 Kubernetes 中的应用程序(或*操作数)。

正如我们将在本书的其余部分中看到的示例一样,这个简单的定义可以涵盖相当广泛的实现。推荐的模式是为每个自定义资源提供一个自定义控制器,但除此之外,细节会有所不同。一个简单的操作员可能由单个资源和控制器组成,而一个更复杂的操作员可能有多个资源和控制器。这些多个控制器可能在同一进程空间中运行,也可能被分成单独的 Pod。

控制器与操作员

尽管在技术上来说,Kubernetes 中的运算符和控制器是不同的概念,但这两个术语经常被互换使用。习惯上会将部署的控制器或一组控制器称为“运算符”,在本书和社区中经常可以看到这种用法。

为了解开这种模式并查看运算符的不同元素以及 Kubernetes 控制平面如何协同工作,请考虑一个概念上的运算符,即 DBCluster 运算符的交互,如图 5-2 所示。

在管理员安装了 DBCluster 运算符和集群中的 db-cluster 自定义资源后,用户可以使用 kubectl 创建 db-cluster 资源的实例(1),该操作会将资源注册到 API 服务器(2),后者将状态存储在 etcd 中(3),以确保高可用性(其他与 etcd 的交互由于篇幅问题未列出)。

Kubernetes 控制器与运算符之间的交互

图 5-2. Kubernetes 控制器与运算符之间的交互

DBCluster 控制器(作为运算符的一部分)会被通知新的 db-cluster 资源(4)的创建,并使用 API 服务器(5)创建额外的 Kubernetes 资源,这可能包括 StatefulSets、Services、PersistentVolumes、PersistentVolumeClaims 等,正如我们在之前的数据库部署示例中所见。

关注 StatefulSet 路径,作为 Kubernetes 控制器管理器的一部分运行的 StatefulSet 控制器会被通知新的 StatefulSet(6)的创建,并创建新的 Pod 资源(7)。API 服务器请求调度程序将每个 Pod 分配给工作节点(8),并与所选工作节点上的 Kubelet(9)通信以启动每个所需的 Pod(10)。

正如您所见,创建 db-cluster 资源会触发一系列交互,因为各种控制器被通知 Kubernetes 资源的变化,并启动变更以使集群的状态符合期望的状态。从用户的角度来看,这些交互序列看起来复杂,但设计展示了强大的封装性:每个控制器的责任范围都明确定界,独立于其他控制器。这种关注点的分离是使 Kubernetes 控制平面如此可扩展的关键。

使用 Vitess 运算符在 Kubernetes 中管理 MySQL

现在您已经了解了操作员、自定义控制器和自定义资源的工作原理,是时候亲自体验一下我们一直作为主要关系数据库示例使用的数据库操作员了:MySQL。在前几章中,MySQL 的示例仅限于简单部署一个主副本和几个次要副本。虽然这可以为许多云应用程序提供足够的存储空间,但管理更大的集群可能会迅速变得非常复杂,无论它运行在裸机服务器上还是作为 Kubernetes 中的容器化应用程序。

Vitess 概述

Vitess 是一个自 2010 年起在 YouTube 开始的开源项目。在该公司被 Google 收购之前,YouTube 运行在 MySQL 上,并且随着 YouTube 的扩展,每天都会出现故障。Vitess 被创建为一个层,通过使用分片方法,将应用程序对数据库的访问抽象化为多个实例看起来像是单个数据库,将应用程序请求路由到适当的实例。在我们探索在 Kubernetes 上部署 Vitess 之前,让我们花些时间了解其架构。我们将从图 5-3 中显示的高层概念开始:单元、键空间、分片以及主和副本表格。

Vitess 集群拓扑结构:单元、键空间和分片

图 5-3. Vitess 集群拓扑结构:单元、键空间和分片

在高层次上,Vitess 集群由称为表格的多个 MySQL 实例组成,可以分布在多个数据中心或单元中。每个 MySQL 实例承担主或副本的角色,并可能专用于称为分片的数据库特定片段。让我们考虑每个概念对在 Vitess 中读写数据的影响:

单元

Vitess 的典型生产部署跨多个故障域,以提供高可用性。Vitess 将这些故障域称为单元。推荐的拓扑结构是每个数据中心或云提供商区域一个单元。虽然写入和复制涉及跨单元边界的通信,但 Vitess 的读取限制在本地单元以优化性能。

键空间

这是一个逻辑数据库,由一个或多个表组成。集群中的每个键空间可以是分片非分片的。非分片的键空间有一个主单元,其中一个被指定为主数据库的 MySQL 实例将驻留在那里,而其他单元将包含副本。在左侧显示的非分片键空间中的 图 5-3,来自客户端应用程序的写入操作将被路由到主节点,并在后台复制到副本节点。读取可以从主节点或副本节点提供服务。

分片

Vitess 真正的强大之处在于其通过将 keyspace 的内容分布在多个复制的 MySQL 数据库(称为shards)上来实现扩展能力,同时为客户端应用程序提供单个数据库的抽象。右侧图中的客户端不知道数据如何分片。在写入时,Vitess 确定涉及哪些分片,然后将数据路由到适当的主实例。在读取时,Vitess 从本地单元的主节点或副本节点中收集数据。

keyspace 的分片规则在Vitess Schema(VSchema)中指定,这是一个包含每个表所用的分片键(在 Vitess 中称为keyspace ID)的对象。为了在数据分片方式上提供最大的灵活性,Vitess 允许您指定表中用于计算 keyspace ID 的列,以及用于进行计算的算法(或VIndex)。表还可以具有辅助 VIndex,以支持跨多个 keyspace ID 的更高效查询。

要了解 Vitess 如何管理分片以及如何将查询路由到各个 MySQL 实例,您需要了解图 5-4(#vitess_architecture_including_vtgatecom)中显示的 Vitess 集群组件,包括 VTGate、VTTablet 和拓扑服务。

Vitess 架构包括 VTGate、VTTablets、拓扑服务

图 5-4. Vitess 架构包括 VTGate、VTTablets 和拓扑服务

让我们逐步了解这些组件,了解它们的作用及其如何交互:

VTGate

Vitess 网关(VTGate)是一个代理服务器,为客户端应用程序提供 SQL 二进制端点,使 Vitess 集群看起来像是一个单一的数据库。通常,Vitess 客户端连接到运行在同一单元(数据中心)中的 VTGate。VTGate 解析每个传入的读取或写入查询,并利用其对 VSchema 和集群拓扑的了解创建查询执行计划。VTGate 为每个分片执行查询,组装结果集并将其返回给客户端。VTGate 可以检测和限制可能影响内存或 CPU 利用率的查询,提供高可靠性并帮助确保一致的性能。尽管 VTGate 实例会缓存集群元数据,但它们是无状态的,因此通过在每个单元中运行多个 VTGate 实例可以提高集群的可靠性和可扩展性。

VTTablet

Vitess 表 t(VTTablet)是一个在与单个 MySQL 数据库相同计算实例上运行的代理,负责管理其访问和监控其健康情况。每个 VTTablet 承担集群中特定角色,如分片的主要节点或其副本之一。有两种类型的副本:可晋升以替换主要节点的副本和不能晋升的副本。后者通常用于提供额外容量,支持读密集型用例,如分析。VTTablet 提供 gRPC 接口,VTGate 使用该接口发送查询和控制命令,VTTablet 将其转换为 MySQL 实例上的 SQL 命令。VTTablet 维护到 MySQL 节点的长连接池,从而提高吞吐量、减少延迟和减少内存压力。

Topology 服务

Vitess 需要一个强一致的数据存储来维护描述集群拓扑的少量元数据,包括 keyspace 的定义及其 VSchema、每个分片存在的 VTTablets,以及哪个 VTTablet 是主要节点。Vitess 使用名为 Topology Service 的可插拔接口,项目提供了三种实现:etcd(默认)、ZooKeeper 和 Consul。VTGate 和 VTTablet 在后台与 Topology Service 接口,以维护对拓扑的感知,并避免在查询路径上与 Topology Service 交互以避免性能影响。对于多单元格集群,Vitess 结合了单元格本地的 Topology Service 和一个全局的 Topology Service,后者在多个单元格中维护整个集群的知识,以提供拓扑信息的高可用性。

vtctldvtctlclient

Vitess 控制守护程序 vtctld 及其客户端 vtctlclient 提供用于配置和管理 Vitess 集群的控制平面。vtctld 部署在集群中一个或多个单元格上,而 vtctlclient 部署在管理集群的用户客户端机器上。vtctld 使用类似 Kubernetes 的声明式方法执行其工作:更新 Topology 服务中的集群元数据,VTGate 和 VTTablet 捕捉更改并相应地响应。

现在您了解了 Vitess 架构和基本概念,让我们讨论如何将它们映射到 Kubernetes 环境中。这对于任何应用程序都是重要考虑因素,尤其是对于像 Vitess 这样复杂的数据基础设施。

PlanetScale Vitess Operator

随着时间的推移,Vitess 在几个关键方面有了发展。首先,它现在可以运行其他与 MySQL 兼容的数据库引擎,如 Percona。其次,对于我们的研究来说更为重要的是,PlanetScale 将 Vitess 打包为可部署到 Kubernetes 的容器化应用程序。

在 Kubernetes 中运行 Vitess 的演变选项

在 Kubernetes 中运行 Vitess 的最新技术已随时间演变。尽管 Vitess 曾包含一个 Helm 图表,在 2020 年中期的 7.0 版本中已经 弃用。Vitess 项目还托管了一个运算符,大约在同一时间被 弃用。这两个选项都被 PlanetScale 运算符取代,我们在本节中进行详细讨论。

看看使用 PlanetScale Vitess 运算符轻松部署多节点 MySQL 集群有多简单。由于 Vitess 项目已将 PlanetScale Vitess 运算符作为其官方支持的运算符,您可以参考 Vitess 项目文档中的 入门指南。我们将在这里部分地遵循该指南,以了解运算符的内容及其工作原理。

示例需要具备更多资源的 Kubernetes 集群

前几章的示例不需要大量计算资源,我们建议您在诸如 kind 或 K3s 的本地发行版上运行它们。从本章开始,示例变得更加复杂,可能需要比您的台式机或笔记本电脑更多的资源。对于这些情况,我们将提供文档或脚本的参考,以创建具有足够资源的 Kubernetes 集群。

安装 Vitess 运算符

你可以在这本书的代码仓库中找到本节使用的源代码。文件为了方便起见从它们在Vitess GitHub 仓库的原始来源复制而来。首先,使用提供的配置文件安装运算符:

set GH_LINK=https://raw.githubusercontent.com
kubectl apply -f \
  $GH_LINK/vitessio/vitess/main/examples/operator/operator.yaml
customresourcedefinition.apiextensions.k8s.io/
  etcdlockservers.planetscale.com created
...

正如您将在 kubectl apply 命令的输出中看到的那样,此配置将创建多个 CRD,以及一个管理运算符单个实例的 Deployment。图 5-5 展示了您刚刚安装的许多元素,以突出一些乍一看不明显的有趣细节:

  • 运算符包含与每个 CRD 对应的控制器。如果您有兴趣查看这在 Go 中的运算符源代码是什么样子,请将 控制器实现 与用于生成 CRD 配置的 自定义资源规格 进行比较,这些配置是在 “构建运算符” 中介绍的。

  • 图中显示了一组 CRD 的层次结构,代表它们之间的关系和预期的使用方法,如运算符的API 参考所述。要使用 Vitess 运算符,您需要定义一个 VitessCluster 资源,其中包含 VitessCells 和 VitessKeyspaces 的定义。VitessKeyspaces 又包含 VitessShards 的定义。虽然您可以单独查看每个 VitessCell、VitessKeyspace 和 VitessShard 的状态,但必须在父级 VitessCluster 资源的上下文中更新它们。

  • 目前,Vitess Operator 仅支持将 etcd 作为拓扑服务实现。EtcdLockserver CRD 用于配置这些 etcd 集群。

Vitess Operator 和自定义资源定义

图 5-5. Vitess Operator 和自定义资源定义

角色和角色绑定

正如在图 5-5 底部所示,安装操作员会创建一个 ServiceAccount,并创建两个我们之前未讨论过的新资源:一个 Role 和一个 RoleBinding。这些额外的资源允许 ServiceAccount 访问 Kubernetes API 上的特定资源。首先,查看你用来安装操作员的文件中 vitess-operator Role 的配置(可以搜索 kind: Role 来定位相关代码):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: vitess-operator
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - services
  - endpoints
  - persistentvolumeclaims
  - events
  - configmaps
  - secrets
  verbs:
  - '*'
...

角色定义的第一部分标识了核心 Kubernetes 分布中的资源,可以通过将 apiGroup 参数设为空字符串而不是 k8s.io 来指定。verbs 对应于 Kubernetes API 在资源上提供的操作,包括 getlistwatchcreateupdatepatchdelete。这个 Role 被赋予使用通配符 * 访问所有操作的权限。如果你访问示例中的 URL 并查看更多代码,还会看到如何授予这个 Role 访问其他资源的权限,包括 Deployments 和 ReplicaSets,以及 apiGroup planetscale.com 中的资源。

RoleBinding 将 ServiceAccount 与 Role 关联起来:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: vitess-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: vitess-operator
subjects:
- kind: ServiceAccount
  name: vitess-operator

运营商的最小特权

作为操作员的创建者或消费者,在选择授予操作员哪些权限时要小心,并意识到操作员被允许做什么可能会产生的影响。

优先级类

另一个细节没有显示在图 5-4 中:安装操作员创建了两个 PriorityClass 资源。PriorityClasses 提供给 Kubernetes 调度器的输入,指示 Pod 的相对优先级。优先级是一个整数值,较高的值表示较高的优先级。每当创建一个 Pod 资源并准备分配给工作节点时,调度器会考虑 Pod 的优先级作为决策的一部分。当有多个 Pod 等待调度时,较高优先级的 Pod 会先被分配。当集群的节点资源不足时,可能会停止或驱逐较低优先级的 Pod,以为较高优先级的 Pod 腾出空间,这个过程称为抢占

PriorityClass 是一种方便的方式,用于为多个 Pod 或其他工作负载资源(如 Deployments 和 StatefulSets)设置优先级值。Vitess Operator 创建了两个 PriorityClasses:vitess-operator-control-plane 用于操作者和 vtctld Deployments 的较高优先级,而 vitess 类别用于数据平面组件,例如 VTGate 和 VTTablet Deployments。

Kubernetes 调度复杂性

Kubernetes 提供了多个影响 Pod 调度的约束条件,包括优先级和抢占、亲和性和反亲和性,以及调度器扩展,如 “扩展 Kubernetes 客户端” 中讨论的那样。这些约束条件的交互可能是不可预测的,特别是在跨多个团队共享的大型集群中。当集群中的资源变得稀缺时,Pods 可能会被抢占或以你意想不到的方式无法调度。保持对集群中工作负载的各种调度需求和约束条件的意识是一种最佳实践,以避免出现意外情况。

创建 VitessCluster

现在让我们创建一个 VitessCluster 并让操作者开始工作。代码示例包含一个配置文件,定义了一个名为 example 的非常简单的集群,具有 VitessCell zone1,keyspace commerce 和单个分片,操作者给它命名为 x-x

kubectl apply -f 101_initial_cluster.yaml
vitesscluster.planetscale.com/example created
secret/example-cluster-config created

命令的输出指示直接创建了几个项目。但在幕后还有更多工作,因为操作者检测到 VitessCluster 的创建并开始为其它资源提供支持,正如 Figure 5-6 中总结的那样。

VitessCluster example 管理的资源

图 5-6. VitessCluster example 管理的资源

通过与 Figure 5-6 的配置脚本进行比较,您可以对这个简单的 VitessCluster 做出几点观察。首先,顶层配置允许您指定集群的名称以及将用于各个组件的容器镜像:

apiVersion: planetscale.com/v2
kind: VitessCluster
metadata:
  name: example
spec:
  images:
    vtctld: vitess/lite:v12.0.0
    ...

接下来,VitessCluster 配置提供了 VitessCell zone1 的定义。提供给 gateway 的值指定了为该 cell 分配的单个 VTGate 实例,具有特定的计算资源限制:

  cells:
  - name: zone1
    gateway:
      authentication:
        static:
          secret:
            name: example-cluster-config
            key: users.json
      replicas: 1
      resources:
        ...

Vitess Operator 使用这些信息创建了一个以 example-zone1-vtgate 为前缀的 VTGate Deployment,包含一个副本,并提供访问服务。VTGate 实例的访问凭据存储在 example-cluster-config Secret 中。此 Secret 用于保护其他配置值,正如您将看到的那样。

VitessCluster 配置的下一部分指定创建单个 vtctld 实例(一个 仪表盘),具有控制 zone1 的权限。Vitess Operator 使用这些信息创建一个 Deployment 来管理指定资源限制的仪表盘,并创建一个 Service 来提供对 VTGate 的访问:

  vitessDashboard:
    cells:
    - zone1
    extraFlags:
      security_policy: read-only
    replicas: 1
    resources:
      ...

VitessCluster 还定义了 commerce keyspace,其中包含一个单独的分片(实质上是一个未分片的 keyspace)。这个单独的分片在 zone1 区域有两个 VTTablets 的池,每个分片将被分配 10 GB 的存储空间:

  keyspaces:
  - name: commerce
    turndownPolicy: Immediate
    partitionings:
    - equal:
        parts: 1
        shardTemplate:
          databaseInitScriptSecret:
            name: example-cluster-config
            key: init_db.sql
          replication:
            enforceSemiSync: false
          tabletPools:
          - cell: zone1
            type: replica
            replicas: 2
            vttablet:
              ...
            mysqld:
              ...
            dataVolumeClaimTemplate:
              accessModes: ["ReadWriteOnce"]
              resources:
                requests:
                  storage: 10Gi

如 图 5-6 所示,Vitess Operator 管理每个 VTTablet 的 Pod,并创建一个 Service 来管理对这些 tablet 的访问。操作员不使用 StatefulSet,因为 VTTablets 有不同的角色,一个是主要的,另一个是副本。每个 VTTablet Pod 包含多个容器,包括 vttablet sidecar,它配置和控制 mysql 容器。vttablet sidecar 使用 example-cluster-config Secret 中包含的脚本初始化 mysql 实例。

虽然此配置没有具体包括 etcd 的详细信息,但 Vitess Operator 使用其默认设置创建了一个三节点的 etcd 集群,作为 VitessCluster 的拓扑服务。由于 StatefulSets 的不足,操作员单独管理每个 Pod 和 PersistentVolumeClaim。这指向 Kubernetes 和操作员成熟后未来改进的可能性;也许 Kubernetes API 服务器有一天可以在 Vitess 架构中扮演拓扑服务的角色。

到目前为止,您在 Kubernetes 中已经配置了一个 VitessCluster 及其所有基础设施。接下来的步骤是创建数据库模式并配置应用程序以使用 VTGate Service 访问集群。您可以按照 Alkin Tezuysal 在其 2020 年的博客文章 “Vitess Operator for Kubernetes” 中描述的步骤进行操作,该文章还描述了在 Kubernetes 上管理 Vitess 安装的其他用例,包括模式迁移、备份和恢复。

备份/恢复功能利用 VitessBackupStorage 和 VitessBackup CRD,在安装过程中可能已经注意到。VitessBackupStorage 资源表示备份可以存储的位置。在配置 VitessCluster 的备份部分并指向备份位置后,操作员创建 VitessBackup 资源作为其执行的每个备份的记录。当向 VitessCluster 添加额外的副本时,操作员通过从最近的备份进行恢复来初始化它们的数据。

可视化更大的 Kubernetes 应用程序

使用 kubectl getkubectl describe 命令来探索在安装运算符和创建集群时创建的所有资源是一个很好的练习。但是,您可能会发现使用像 Lens 这样的工具更容易,它提供了一个友好的图形界面,让您可以更快速地浏览资源,或者像 K9s 这样的工具,提供命令行界面。

重新分片是另一个有趣的用例,当集群变得不平衡且一个或多个分片比其他分片更快地耗尽容量时,您可能需要执行此操作。您将需要使用 vtctlclient 修改 VSchema,然后通过添加额外的 VitessShards 来更新 VitessCluster 资源,以便运算符配置所需的基础设施。这突显了责任的分工:Vitess 运算符管理 Kubernetes 资源,而 Vitess 控制守护进程 (vtctld) 提供更多的应用特定行为。

运算符生态系统的不断增长

运算符模式在 Kubernetes 社区中变得非常流行,部分得益于运算符框架的发展,这是一个用于创建和分发运算符的生态系统。在本节中,我们将检查运算符框架及其相关的开源项目。

选择运算符

虽然在本章中我们着重介绍了 Vitess 作为示例数据库运算符,但运算符显然与您数据栈的所有元素相关。在云原生数据的各个方面,我们看到越来越多成熟的开源运算符可用于您的部署,而我们将在未来的章节中查看更多不同类型数据基础设施在 Kubernetes 上运行时的其他运算符。

在选择运算符时,您应该考虑多个方面。它有哪些特性?它自动化程度如何?它的支持情况如何?它是专有的还是开源的?运算符框架提供了一个重要资源,运算符中心,这是您在寻找运算符时应该首先考虑的地方。运算符中心是一个井然有序的列表,涵盖了云原生软件的各个方面。它依赖于维护者提交其运算符以进行列表化,这意味着许多现有的运算符可能未列出。

运算符框架还包括运算符生命周期管理器(OLM),这是一个用于安装和管理集群中其他运算符的运算符。您可以管理您环境中允许的运算符的自定义目录,或使用其他人提供的目录。例如,运算符中心本身可以被视为一个目录

运算符中心提供的部分典藏包括根据运算符能力模型评级每个运算符的能力。这个能力模型的级别在表格 5-1 总结,我们还添加了额外的评论来强调数据库运算符的考虑事项。这些示例并非指导性建议,而是指出每个级别预期的能力类型。

表格 5-1. 数据库运算符能力级别应用

能力级别 特征 数据库运算符示例 工具
Level 1: 基本安装 Kubernetes 和工作负载的安装和配置 运算符使用自定义资源为数据库集群提供集中的配置点。运算符通过创建部署、服务账户、角色绑定、持久卷索赔和机密等资源来部署数据库,并帮助初始化数据库架构。 Helm, Ansible, Go
Level 2: 无缝更新 管理工作负载和运算符的升级 运算符可以将现有数据库升级到更新的版本,无需数据丢失(或希望没有停机)。运算符可以用更新的版本替换自身。 Helm, Ansible, Go
Level 3: 完整生命周期 能够创建和从备份中恢复,能够故障转移或替换部分集群应用程序,能够扩展应用程序 运算符提供一种方法在多个数据节点上创建一致的备份,并能够使用这些备份来恢复或替换失败的数据库节点。运算符可以响应配置更改以添加或删除数据库节点,甚至数据中心。 Ansible, Go
Level 4: 深入洞察 提供包括警报、监控、事件或计量在内的能力 运算符监视数据库软件的度量和日志输出,并使用这些信息执行健康和准备检查。运算符将度量和警报推送到其他基础设施。 Ansible, Go

| Level 5: 自动驾驶 | 提供包括自动扩展、自动修复、自动调优在内的能力 | 运算符根据性能需求自动调整集群中数据库节点的数量。运算符还可以动态调整持久卷的大小或更改用于各个数据库节点的存储类。运算符自动执行数据库维护,如重建索引以改善响应速度慢的问题。

运算符检测到异常的工作负载模式,并采取行动,如重新分片以平衡工作负载。 | Ansible, Go |

这些级别既有助于评估您可能希望使用的运算符,也为运算符开发人员提供了目标。它们还提供了基于 Helm 的运算符能够完成的任务的一个具有见解的观点,将其限制在第 2 级。对于全生命周期管理和自动化,需要更直接地与 Kubernetes 控制平面进行交互。对于第 5 级运算符,目标是完全无需手动干预的部署。

让我们快速浏览几种流行开源数据库的可用运算符:

Cass Operator

在 2021 年,几家 Cassandra 社区的公司,他们各自开发了自己的运算符,在支持由 DataStax 开发的运算符时汇聚在一起,该运算符以 Cass Operator 的昵称而闻名。Cass Operator 受到社区运算符最佳特性以及 DataStax 在运行基于 Cassandra 的数据库服务(DBaaS)Astra 方面的经验的启发。该运算符已经捐赠给了K8ssandra 项目,它是在 Kubernetes 上部署 Cassandra 的更大生态系统的一部分。我们将在第七章深入了解 K8ssandra 和 Cass Operator。

PostgreSQL 运算符

对于 PostgreSQL,提供了几种运算符,这并不奇怪,因为它是继 MySQL 之后最受欢迎的开源数据库。其中两种最受欢迎的运算符是Zalando PostgreSQL Operator,以及来自 Crunchy Data 的PGO(代表 Postgres Operator)。阅读 Nikolay Bogdanov 的博文“比较用于 PostgreSQL 的 Kubernetes Operators”,对比这些以及其他运算符会很有帮助。

MongoDB Kubernetes Operator

MongoDB 是最受欢迎的文档数据库,开发者喜爱它的易用性。MongoDB Community Kubernetes Operator 提供了基本支持,用于创建和管理 MongoDB ReplicaSets,扩展和缩减规模,以及升级操作。这个运算符可以在GitHub上找到,但尚未列入 Operator Hub,可能是因为 MongoDB 还为其企业版本提供了单独的运算符。

Redis Operator

Redis 是一种内存中的键值存储,具有广泛的用例。应用开发者通常在需要超低延迟时将 Redis 作为其他数据基础设施的附属品使用。它擅长缓存、计数和共享数据结构。Redis Operator 覆盖了基本的安装和升级,同时还管理了更复杂的操作,如集群故障转移和恢复。

可以看到,为许多流行的开源数据库提供了运算符,尽管有些供应商倾向于将 Kubernetes 运算符主要作为付费企业版本的特性差异化。

数据库运算符

尽管 Kubernetes 社区普遍认为,尽可能使用操作符来管理分布式数据基础设施,但对于构建操作符的具体人员意见不一。如果你不恰好在一个数据基础设施供应商工作,这可能是一个具有挑战性的问题。玛丽·布兰斯科姆在她的博客文章“何时使用及避免操作符模式”提供了一些需要考虑的优秀问题,我们将在这里总结:

  • 部署的规模是多大?如果你只部署一个数据库应用程序的实例,构建和维护操作符可能不是一种成本有效的方法。

  • 你是否具备数据库方面的专业知识?最好的操作符通常由在生产环境中运行数据库的公司构建,包括提供 DBaaS 解决方案的供应商。

  • 你是否需要更高水平的应用程序意识和自动化,或者使用 Helm 图表和标准的 Kubernetes 资源部署已经足够?

  • 你是否尝试让操作符管理 Kubernetes 外部的资源?考虑一个解决方案,它更接近于被管理的资源,并且具有一个你可以从你的 Kubernetes 应用程序访问的 API。

  • 你是否考虑了安全性问题?由于操作符是 Kubernetes 控制平面的扩展,你需要仔细管理你的操作符可以访问的资源。

如果你决定编写一个操作符,有几个出色的工具和资源可供使用:

Operator SDK

这个软件开发工具包包含在操作符框架中,用于构建、测试和打包操作符。Operator SDK 使用模板自动生成新的操作符项目,并提供 API 和抽象来简化构建操作符的常见方面,特别是与 Kubernetes API 的交互。SDK 支持使用 Go、Ansible 或 Helm 创建操作符。

Kubebuilder

这个用于构建操作符的工具包由 Kubernetes API 机器人 SIG 管理。与 Operator SDK 类似,Kubebuilder 提供了用于项目生成、测试和发布控制器和操作符的工具。Kubebuilder 和 Operator SDK 都构建在 Kubernetes controller-runtime 上,这是一组用于构建控制器的 Go 库。魏泰在他的博客文章“Kubebuilder vs. Operator SDK”提供了这些工具包之间差异的简洁总结。

Kubernetes 通用声明性操作符 (KUDO)

这个操作符允许你使用 YAML 文件声明性地创建操作符。对于一些开发者来说,这是一种有吸引力的方法,因为它消除了编写 Go 代码的需要。德米特罗·韦德茨基在他的博客文章“如何在 K8S 上使用 KUDO 操作符部署你的第一个应用”介绍了如何使用 KUDO,并讨论了声明性方法的一些优缺点。

最后,Jason Dobies 和 Joshua Wood 的 Kubernetes OperatorsProgramming Kubernetes 等 O’Reilly 出版的书籍,是理解运算符生态系统并深入编写 Go 中运算符和控制器细节的重要资源。

如你所见,Kubernetes 运算符的最新技术正在不断成熟。无论目标是构建统一的运算符还是使构建特定于数据库的运算符更加容易,可以明显看出,多个社区开始合作解决如集群成员资格、拓扑感知和领导选举等问题的共同 CRD,取得了重大进展。

总结

在本章中,你了解了几种扩展 Kubernetes 控制平面的方法,尤其是运算符和自定义资源。运算符模式提供了重要突破,使我们能够通过自动化简化 Kubernetes 中的数据库操作。虽然你肯定应该使用运算符在 Kubernetes 中运行分布式数据库,但在开始编写自己的运算符之前,请认真考虑。如果构建运算符是你的正确选择,那么有很多资源和框架可以帮助你一路前行。正如本章中专家们所讨论的那样,Kubernetes 本身也有改进的空间,以使编写运算符更加容易。

在过去的几章中,我们主要专注于在 Kubernetes 上运行数据库,现在让我们扩展焦点,考虑这些数据库与其他基础设施的交互方式。

第六章:在 Kubernetes 堆栈中集成数据基础设施

在本书中,我们展示了运行在 Kubernetes 上的现代化、云原生应用程序的未来。直到此刻,我们指出历史上数据一直是实现这一现实的最困难部分之一。在之前的章节中,我们介绍了 Kubernetes 提供的用于管理计算、网络和存储资源的原语(第二章),并考虑了如何使用这些资源在 Kubernetes 上部署数据库(第三章)。我们还研究了使用控制器和操作者模式自动化基础设施(第四章)。

现在让我们扩展我们的关注点,考虑数据基础设施如何融入你在 Kubernetes 中的整体应用架构。在本章中,我们将探讨如何将前几章讨论的构建模块组合成集成的数据基础设施堆栈,这些堆栈易于部署并根据每个应用的独特需求进行定制。这些堆栈代表了我们在 第一章 中介绍的虚拟数据中心愿景的一步。为了了解构建和使用这些较大组件涉及的考虑因素,让我们深入了解 K8ssandra。这个开源项目提供了一个基于 Apache Cassandra 的集成数据堆栈,我们在 “在 Kubernetes 上运行 Apache Cassandra” 中首次讨论了这个数据库。

K8ssandra:基于 Kubernetes 的生产就绪 Cassandra

为了设定背景,让我们考虑将应用工作负载迁移到 Kubernetes 中的一些实际挑战。随着组织开始将现有应用迁移到 Kubernetes 并在 Kubernetes 中创建新的云原生应用程序,现代化数据层通常被推迟。无论延迟的原因是 Kubernetes 不适合有状态工作负载、缺乏开发资源还是其他因素,其结果都是在 Kubernetes 中运行应用程序并使数据库和其他数据基础设施在外部运行的不匹配架构。这导致开发者和 SRE(Site Reliability Engineer)的注意力分散,限制了生产力。同时,我们也经常看到监控应用程序和数据库基础设施采用不同的工具集,增加了云计算成本。

这种采纳挑战在 Cassandra 社区中变得明显。尽管围绕构建单个 Cassandra 操作者达成共识和增进协作如 第五章 所述,开发者仍面临关于数据库和操作者如何适应更大应用上下文的关键问题:

  • 如何才能综合查看整个堆栈(包括应用程序和数据)的健康状态?

  • 如何才能以符合我们管理数据中心方式的 Kubernetes 本地方式,定制安装、升级和其他运营任务的自动化?

为了帮助解决这些问题,DataStax 的 John Sanda 和一组工程师启动了一个名为 K8ssandra 的开源项目,旨在提供一个生产就绪的 Cassandra 部署,体现了在 Kubernetes 中运行 Cassandra 的最佳实践。K8ssandra 提供自定义资源,帮助管理包括集群部署、升级、扩展、数据备份和恢复等任务。您可以在 Jeff Carpenter 的博客文章 “Why K8ssandra?” 中详细了解该项目的动机。

K8ssandra 架构

K8ssandra 部署为称为 集群 的单元,这与 Kubernetes 和 Cassandra 使用的术语类似。K8ssandra 集群包括一个 Cassandra 集群以及 图 6-1 中所示的其他组件,以提供完整的数据管理生态系统。让我们从顶部中心大致顺时针顺序考虑这些组件:

Cass Operator

介绍于 第五章 的 Kubernetes 操作者。它管理在 Kubernetes 上的 Cassandra 节点的生命周期,包括提供新节点和存储、扩展和缩减。

Cassandra Reaper

这管理维护 Cassandra 节点的详细信息,以保持高数据一致性。

Cassandra Medusa

提供对存储在 Cassandra 中的数据的备份和恢复功能。

Prometheus 和 Grafana

用于收集和可视化指标。

Stargate

作为对 CQL 的替代,提供 API 访问客户端应用程序的数据网关。

K8ssandra 操作者

管理所有其他组件,包括用于管理跨多个 Kubernetes 集群的 Cassandra 集群的多集群支持。

K8ssandra 架构

图 6-1. K8ssandra 架构

在接下来的部分中,我们将逐个查看 K8ssandra 项目的每个组件,以了解其在架构中的作用及与其他组件的关系。

安装 K8ssandra 操作者

让我们通过安装 K8ssandra 的实际操作经验来深入了解。要运行基本的 K8ssandra 安装,以完全展示操作者的功能,您需要一个具有多个工作节点的 Kubernetes 集群。

为了使部署更简单,K8ssandra 团队提供了脚本来自动化创建 Kubernetes 集群的过程,然后在这些集群上部署操作者。这些脚本使用 kind clusters 来简化,所以在开始之前,请确保已安装它。

在 K8ssandra 网站上还提供了在各种云平台上安装的说明。我们在这里提供的说明基于 K8ssandra Operator 仓库中的 安装指南

K8ssandra 2.0 发布状态

本章重点介绍 K8ssandra 2.0 发布,包括 K8ssandra Operator。在撰写本文时,K8ssandra 2.0 仍处于 beta 状态。随着 K8ssandra 2.0 向全面 GA 发布迈进,将更新 K8ssandra 网站上的“入门”部分 中的说明,以引用新版本。

首先,从 GitHub 上克隆 K8ssandra operator 仓库:

git clone https://github.com/k8ssandra/k8ssandra-operator.git

接下来,您需要使用提供的 Makefile 创建一个 Kubernetes 集群,并将 K8ssandra Operator 部署到其中(假设您已安装 make):

cd k8ssandra-operator
make single-up

如果您查看 Makefile,会注意到操作员是使用 Kustomize 安装的,我们在 “附加部署工具:Kustomize 和 Skaffold” 中讨论过这个工具。刚刚执行的目标会创建一个具有四个工作节点的 kind 集群,并更改当前上下文以指向该集群,您可以通过以下方式进行验证:

% kubectl config current-context
kind-k8ssandra-0
% kubectl get nodes
NAME                        STATUS   ROLES                  AGE     VERSION
k8ssandra-0-control-plane   Ready    control-plane,master   6m45s   v1.22.4
k8ssandra-0-worker          Ready    <none>                 6m13s   v1.22.4
k8ssandra-0-worker2         Ready    <none>                 6m13s   v1.22.4
k8ssandra-0-worker3         Ready    <none>                 6m13s   v1.22.4
k8ssandra-0-worker4         Ready    <none>                 6m13s   v1.22.4

现在来检查已创建的 CRD 列表:

% kubectl get crd
NAME                                          CREATED AT
cassandrabackups.medusa.k8ssandra.io          2022-02-05T17:31:35Z
cassandradatacenters.cassandra.datastax.com   2022-02-05T17:31:35Z
cassandrarestores.medusa.k8ssandra.io         2022-02-05T17:31:35Z
cassandratasks.control.k8ssandra.io           2022-02-05T17:31:36Z
certificaterequests.cert-manager.io           2022-02-05T17:31:16Z
certificates.cert-manager.io                  2022-02-05T17:31:16Z
challenges.acme.cert-manager.io               2022-02-05T17:31:16Z
clientconfigs.config.k8ssandra.io             2022-02-05T17:31:36Z
clusterissuers.cert-manager.io                2022-02-05T17:31:17Z
issuers.cert-manager.io                       2022-02-05T17:31:17Z
k8ssandraclusters.k8ssandra.io                2022-02-05T17:31:36Z
orders.acme.cert-manager.io                   2022-02-05T17:31:17Z
reapers.reaper.k8ssandra.io                   2022-02-05T17:31:36Z
replicatedsecrets.replication.k8ssandra.io    2022-02-05T17:31:36Z
stargates.stargate.k8ssandra.io               2022-02-05T17:31:36Z

正如您所见,与 cert-manager 和 K8ssandra 相关联的几个 CRD。还有 Cass Operator 使用的 CassandraDatacenter CRD。K8ssandra 和 Cass Operator 的 CRD 都是命名空间的,您可以使用 kubectl api-resources 命令进行验证,这意味着根据这些定义创建的资源将被分配到特定的命名空间。该命令还将显示每种资源类型的可接受缩写(例如 k8c 表示 k8ssandracluster)。

接下来,您可以检查已安装在 kind 集群中的内容。如果使用 kubectl get ns 列出命名空间,您会注意到两个新的命名空间:cert-managerk8ssandra-operator。正如您可能猜到的那样,K8ssandra 使用与 Pulsar 相同的 cert-manager 项目,如 “使用 cert-manager 默认安全通信” 中描述的那样。让我们检查 k8ssandra-operator 命名空间中的内容,这些内容在 图 6-2 中概述,以及相关的 K8ssandra CRD。

检查工作负载,您会注意到已创建了两个部署:一个用于 K8ssandra Operator,另一个用于 Cass Operator。查看 K8ssandra Operator 的源代码,您会看到它包含多个控制器,而 Cass Operator 则由单个控制器组成。这种打包反映了 Cass Operator 是一个独立项目的事实,可以单独使用,而无需采用整个 K8ssandra 框架,否则它可能已作为 K8ssandra Operator 中的一个控制器包含。

K8ssandra Operator 架构

图 6-2. K8ssandra Operator 架构

表 6-1 描述了这些各种控制器与它们交互的关键资源的映射。

表 6-1. 将 K8ssandra CRD 映射到控制器

Operator Controller 关键自定义资源
K8ssandra Operator K8ssandra 控制器 K8ssandraCluster, CassandraDatacenter
Medusa 控制器 CassandraBackup, CassandraRestore
Reaper 控制器 Reaper
Replication 控制器 ClientConfig, ReplicatedSecret
Stargate 控制器 Stargate
Cass Operator Cass Operator 控制器管理器 CassandraDatacenter

我们将在接下来的章节中详细介绍每个 K8ssandra 和 Cass Operator CRD:

创建 K8ssandraCluster

一旦安装了 K8ssandra Operator,下一步就是创建一个 K8ssandraCluster。本节中使用的源代码在书的存储库的 “Vitess Operator Example” 部分 中可用,基于 K8ssandra Operator GitHub 存储库 中提供的示例。首先,请查看 k8ssandra-cluster.yaml 文件:

apiVersion: k8ssandra.io/v1alpha1
kind: K8ssandraCluster
metadata:
  name: demo
spec:
  cassandra:
    cluster: demo
    serverVersion: "4.0.1"
    datacenters:
      - metadata:
          name: dc1
        size: 3
        storageConfig:
          cassandraDataVolumeClaimSpec:
            storageClassName: standard
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 1Gi
        config:
          jvmOptions:
            heapSize: 512M
        stargate:
          size: 1
          heapSize: 256M

此代码指定了一个 K8ssandraCluster 资源,由一个运行三个 Cassandra 4.0.1 节点的单个数据中心 dc1 组成,其中每个 Cassandra 节点的 Pod 规范请求使用引用 standard StorageClass 的 PersistentVolumeClaim 的 1 GB 存储。此配置还包括一个单独的 Stargate 节点,以提供对 Cassandra 集群的 API 访问。这是一个接受大多数其他组件的默认配置的最小配置。使用以下命令在 k8ssandra-operator 命名空间中创建 demo K8ssandraCluster:

% kubectl apply -f k8ssandra-cluster.yaml -n k8ssandra-operator
k8ssandracluster.k8ssandra.io/demo created

一旦命令完成,您可以使用诸如 kubectl get k8ssandraclusters(或简写为 kubectl get k8c)之类的命令来检查 K8ssandraCluster 的安装情况。图 6-3 描述了在创建 demo K8ssandraCluster 时操作员为您建立的一些关键计算、网络和存储资源。

一个简单的 K8ssandraCluster

图 6-3. 一个简单的 K8ssandraCluster

以下是一些需要注意的关键项目:

  • 已创建一个单独的 StatefulSet 以表示 Cassandra 数据中心 dc1,其中包含了您指定的三个副本 Pod。正如您将在下一节中了解到的那样,K8ssandra 使用 CassandraDatacenter CRD 通过 Cass Operator 管理此 StatefulSet。

  • 虽然图中显示了一个名为 demo-dc1-service 的单个 Service,用作将 Cassandra 集群公开为单个端点的访问点,但这只是一个简化。您会发现配置了多个服务以为各种客户端提供访问。

  • 有一个部署管理单个 Stargate Pod,以及提供 Stargate 提供的各种 API 服务的客户端终端的服务。这是另一种简化,我们将在 “使用 Stargate API 提升开发者的生产力” 中更详细地探讨这部分配置。

  • 类似于我们在前几章中展示的基础设施示例,K8ssandra Operator 还会创建额外的支持安全资源,如 ServiceAccounts、Roles 和 RoleBindings。

一旦创建了一个 K8ssandraCluster,您可以将客户端应用程序指向 Cassandra 接口和 Stargate API,并执行集群维护操作。只需删除其资源即可删除 K8ssandraCluster,但由于我们还有很多内容要探索,您不会想这么做!随着我们更详细地检查每个 K8ssandra 组件,我们将描述其中几个交互。在此过程中,我们将确保记录 K8ssandra 和相关项目的贡献者在如何使用 Kubernetes 资源以及如何将 Kubernetes 之前的数据基础设施适应 Kubernetes 方式做事方面所做的一些有趣的设计选择。

StackGres: 一个集成的 Kubernetes Stack,用于 Postgres

K8ssandra 项目并不是在 Kubernetes 上运行的集成数据堆栈的唯一实例。另一个例子可以在 StackGres 中找到,这是由 OnGres 管理的项目。StackGres 使用 Patroni 支持集群化、高可用性的 Postgres 部署,并添加了自动备份功能。StackGres 支持与 Prometheus 和 Grafana 的集成,用于指标聚合和可视化,还可以选择使用 Envoy 代理来获取协议级别的更详细的指标。StackGres 由开源组件组成,并使用 AGPLv3 许可证 进行其社区版的发布。

使用 Cass Operator 在 Kubernetes 中管理 Cassandra

Cass Operator 是 DataStax 为 Apache Cassandra 提供的 Kubernetes Operator 的简称。这个开源项目可以在 GitHub 找到,于 2021 年并入了 K8ssandra 项目,取代了其之前在 DataStax GitHub 组织 下的位置。

Cass Operator 是 K8ssandra 的关键组成部分,因为 Cassandra 集群是其他所有基础设施元素和工具的基础数据基础设施。然而,Cass Operator 在 K8ssandra 之前就已经开发,并将继续作为一个可单独部署的项目存在。这非常有帮助,因为并非所有 Cass Operator 的功能都通过 K8ssandra 暴露出来,特别是更高级的 Cassandra 配置选项。Cass Operator 在 Operator Hub 中列为其自己的项目,可以通过 Kustomize 进行安装。

Cass Operator 提供了 Cassandra 拓扑概念的映射,包括集群、数据中心、机架和节点到 Kubernetes 资源的映射。关键构造是 CassandraDatacenter CRD,它表示 Cassandra 集群拓扑中的一个数据中心。(如果您需要关于 Cassandra 拓扑的复习,请参考第三章。)

在前一节创建 K8ssandraCluster 资源时,K8ssandra Operator 创建了一个单个的 CassandraDatacenter 资源,它看起来可能是这样的:

apiVersion: cassandra.datastax.com/v1beta1
kind: CassandraDatacenter
metadata:
  name: dc1
spec:
  clusterName: demo
  serverType: cassandra
  serverVersion: 4.0.1
  size: 3
  racks:
  - name: default

由于您在 K8ssandraCluster 定义中没有指定机架,K8ssandra 将其解释为名为default的单个机架。通过创建 CassandraDatacenter,K8ssandra Operator 委托 Cass Operator 操作此数据中心中的 Cassandra 节点。

Cass Operator 和多数据中心

您可能想知道为什么 Cass Operator 没有定义表示 Cassandra 集群的 CRD。从 Cass Operator 的角度来看,Cassandra 集群基本上只是一个元数据片段——即 CassandraDatacenter 的clusterName,而不是一个实际的资源。这反映了在生产系统中使用的 Cassandra 集群通常部署在跨多个物理数据中心的情况,这超出了 Kubernetes 集群的范围。

虽然您可以确实创建多个 CassandraDatacenter 并使用相同的clusterName将它们链接在一起,但它们必须位于同一个 Kubernetes 集群中,以便 Cass Operator 能够管理它们。建议使用单独的 Namespace 来安装 Cass Operator 的专用实例以管理每个集群。您将看到 K8ssandra 如何支持创建跨多个物理数据中心(和 Kubernetes 集群)的 Cassandra 集群,形成多集群拓扑结构。

当 API 服务器通知 Cass Operator 创建 CassandraDatacenter 资源时,它将创建用于实施数据中心的资源,包括用于在每个机架中管理节点的 StatefulSet,以及各种服务和安全相关资源。StatefulSet 将并行启动请求的 Pod 数目。这带来了这样一个情况,即 Cass Operator 提供了适应 Cassandra 和 Kubernetes 操作方式之间的逻辑。

如果您之前曾使用过 Cassandra,可能已经了解到向集群添加节点的最佳实践是逐个添加,以简化节点加入集群的过程。这个过程被称为引导,包括协商节点将负责的数据以及可能从其他节点流式传输数据到新节点的步骤。然而,由于 StatefulSet 不知道这些约束条件,如何实现向新集群或现有集群逐个添加多个节点呢?

答案在于 Cass Operator 传递给 StatefulSet 的 Pod 规范的组合,然后用于创建每个 Cassandra 节点,如图 6-4 所示。

Cass Operator 与 Cassandra Pods 的交互

图 6-4. Cass Operator 与 Cassandra Pods 的交互

Cass Operator 在其管理的每个 Cassandra Pod 中部署了 Cassandra 的自定义镜像。Pod 规范至少包括两个容器:一个名为server-config-init的初始化容器和一个名为cassandra的 Cassandra 容器。

作为初始化容器,server-config-init在 Cassandra 容器之前启动。它负责基于 CassandraDatacenter 的选择配置选项生成cassandra.yaml配置文件。你可以使用 CassandraDatacenter 资源的config部分指定额外的配置值,如K8ssandra 文档中所述。

Cassandra Pods 中的额外 Sidecar 容器

正如你将在接下来的章节中学到的,当在 K8ssandraCluster 中部署时,Cassandra Pod 可能会有额外的 Sidecar 容器,具体取决于你是否启用了其他 K8ssandra 组件。但目前,我们只关注最基本的安装。

cassandra容器实际上包含两个独立的进程:运行 Cassandra 实例的守护进程和一个管理 API。这在某种程度上违反了每个容器运行单一进程的传统最佳实践,但这个例外有其充分的理由。

Cassandra 的管理接口通过 Java Management Extensions(JMX)暴露。当项目刚开始时,这对于像 Cassandra 这样的基于 Java 的应用程序来说是一个合理的设计选择,但由于其复杂性和安全问题,JMX 已经不再受欢迎。尽管 Cassandra 的备选管理接口已经取得了一些进展,但工作尚未完成,因此 Cass Operator 的开发人员决定集成另一个开源项目,Apache Cassandra 的管理 API

管理 API 项目提供了一个 RESTful API,将基于 HTTP 的调用转换为 Cassandra 传统的 JMX 接口调用。通过在 Cassandra 容器内部运行管理 API,我们避免了将 JMX 端口暴露在 Cassandra 容器外部。这是云原生架构中经常使用的模式的一个实例,用于将自定义协议适配为基于 HTTP 的接口,因为在入口控制器中,这些接口有更好的路由和安全性支持。

Cass Operator 在每个 Cassandra Pod 上发现并连接到管理 API,以执行与 Kubernetes 无关的管理操作。在添加新节点时,这涉及使用管理 API 简单地验证节点是否正常运行,并相应地更新 Cassandra 数据中心的状态。这个过程在 K8ssandra 文档 中有更详细的描述。

定制 Cass Operator 使用的 Cassandra 镜像

管理 API 项目为最近的 Cassandra 3.x 和 4.x 系列版本提供镜像,这些镜像可以在 Docker Hub 上获取。虽然可以使用自定义的 Cassandra 镜像来覆盖 Cass Operator 使用的镜像,但 Cass Operator 要求每个 Cassandra Pod 上都能够访问管理 API。如果需要构建包含管理 API 的自定义镜像,可以使用 GitHub 仓库 中的 Dockerfiles 和支持脚本作为起点。

虽然本节主要关注刚刚描述的启动和扩展 Cassandra 集群,但 Cass Operator 提供了多个功能来部署和管理 Cassandra 集群:

拓扑管理

Cass Operator 使用 Kubernetes 亲和性原则来管理 Cassandra 节点(Pod)在 Kubernetes Worker 节点上的放置,以最大化数据的可用性。

缩减规模

就像逐个添加节点以扩展一样,Cass Operator 管理逐个减少节点以进行缩减。

替换节点

如果 Cassandra 节点由于崩溃或其所在的 Worker Node 关闭而丢失,Cass Operator 依赖 StatefulSet 来替换节点,并将新节点绑定到适当的 PersistentVolumeClaim。

升级镜像

Cass Operator 还利用 StatefulSet 的能力来对 Cassandra Pods 使用的镜像执行滚动升级。

管理种子节点

Cass Operator 创建 Kubernetes 服务来暴露每个数据中心中的种子节点,根据 Cassandra 建议的每个机架一个种子节点的约定,每个数据中心至少为三个。

您可以在 Cass Operator 文档 中阅读关于这些以及其他功能的信息。

使用 Stargate APIs 提升开发者的生产力

到目前为止,本书的重点主要集中在 Kubernetes 中部署数据基础设施(如数据库),而不是云原生应用程序中基础设施的使用方式。在 K8ssandra 中使用 Stargate 给我们提供了一个很好的机会来讨论这个问题。

在许多组织中,关于直接应用访问数据库与抽象数据库交互细节的利弊正在持续讨论。这场辩论尤其频繁出现在那些将应用开发团队和管理包括数据基础设施的平台团队分开的大型组织中。然而,在采用包括 DevOps 和微服务架构在内的现代实践的组织中,也可以观察到这种情况,其中每个微服务可能背后都有不同的数据存储。

多年来,提供对直接数据库访问的抽象化的想法采取了许多形式。即使在单片客户端-服务器应用程序时代,通常也会使用存储过程或通过对象关系映射工具(如 Hibernate)隔离数据访问和复杂查询逻辑,或者使用数据访问对象(DAO)等模式。

更近些年来,随着软件行业向面向服务的架构(SOA)和微服务迈进,类似的用于抽象数据访问的模式也开始出现。正如 Jeff 在他的文章"大众数据服务"中所描述的,许多团队发现自己在架构中创建了一层专门用于数据访问的微服务,提供对特定数据类型或实体的创建、读取、更新和删除(CRUD)操作。这些服务抽象了与特定数据库后端交互的细节,如果执行和维护良好,可以帮助提高开发人员的生产力,并在需要时促进迁移到不同的数据库。

星门项目诞生于认识到多个团队正在构建非常相似的抽象层以通过 API 提供数据访问的事实。星门项目的目标是提供一个开源的数据 API 网关——一套通用的数据访问 API,以帮助消除团队开发和维护自己的定制 API 层的需求。尽管星门的初始实现基于 Cassandra,但该项目的目标是支持多个数据库后端,甚至支持缓存和流处理等其他类型的数据基础设施。

作为后端数据存储使用 Cassandra,星门架构可以描述为具有三层,如图 6-5 所示。

带有 Cassandra 的星门概念架构

图 6-5. 带有 Cassandra 的星门概念架构

API 层 是最外层,由实现在基础 Cassandra 集群上的各种 API 的服务组成。可用的 API 包括 REST API,通过 HTTP 提供对 JSON 文档的访问的 文档 APIGraphQL APIgRPC API路由层(或 协调层)由一组节点组成,这些节点充当 Cassandra 节点,但仅执行查询的路由,不存储数据。存储层 包括传统的 Cassandra 集群,目前可以是 Cassandra 3.11、Cassandra 4.0 或 DataStax Enterprise 6.8。

该架构的关键优势之一在于,它识别了管理计算和存储资源使用的责任分离,并提供根据客户应用需求独立扩展此使用的能力:

  • 可以根据应用程序所需的存储容量增加或减少存储节点的数量。

  • 协调节点和 API 实例的数量可以根据应用的读写负载进行增减,并优化吞吐量。

  • 应用未使用的 API 可以缩减至零(禁用),以减少资源消耗。

K8ssandra 支持通过 Stargate CRD 在基础 Cassandra 集群的顶部部署 Stargate。由 Cass Operator 部署的 CassandraDatacenter 充当存储层,而 Stargate CRD 指定了路由和 API 层的配置。示例配置如 图 6-6 所示。

在 Kubernetes 上部署的 Stargate

图 6-6. 在 Kubernetes 上部署 Stargate

安装包括一个部署来管理协调节点,以及一个服务来提供对 Bridge API 的访问,Bridge API 是在协调节点上公开的私有 gRPC 接口,可用于创建新的 API 实现。详细信息请参阅 Stargate v2 设计。每个已启用的 API 在安装中也包括一个部署,并提供对客户应用程序的访问的服务。

如您所见,Stargate 项目为扩展您的数据基础设施提供了一个有前景的框架,该框架具有与底层数据库一起扩展的开发人员友好的 API。

使用 Prometheus 和 Grafana 的统一监控基础设施

现在我们考虑了为应用开发人员提供更轻松的基础设施之后,让我们来看看在 Kubernetes 堆栈中集成数据基础设施的更多操作重点。我们将从监控开始。

可观测性是部署在 Kubernetes 上的任何应用程序的关键属性,因为它影响您对其可用性、性能和成本的感知。您的目标应该是在应用程序和其依赖的基础设施之间实现集成视图。可观测性通常被描述为由三种类型的数据组成:指标、日志和跟踪。Kubernetes 本身提供了日志记录的能力,并能将事件与资源关联起来,您已经了解了 Cass Operator 如何促进从 Cassandra 节点收集日志。

在本节中,我们将重点介绍 K8ssandra 如何集成 Prometheus/Grafana 栈,以提供指标。Prometheus 是一个流行的开源监控平台。它支持多种接口用于从应用程序和服务收集数据,并将其存储在时间序列数据库中,可以使用 Prometheus 查询语言(PromQL)高效查询。它还包括 Alertmanager,根据指标阈值生成警报和其他通知。

在 K8ssandra 1.x 系列的先前版本中,将 Prometheus 栈作为 K8ssandra 的一部分进行集成,而 K8ssandra 2.x 则提供了与现有 Prometheus 安装集成的能力。

安装 Prometheus Operator 的一种简单方法是使用 kube-prometheus,这是 Prometheus Operator 项目的一部分提供的存储库。Kube-prometheus 旨在作为 Kubernetes 的综合监控栈,包括控制平面和应用程序。您可以克隆此存储库,并使用其中包含的清单(YAML 文件)库安装显示在 Figure 6-7 中的集成组件堆栈。

kube-prometheus 栈的组件

图 6-7. kube-prometheus 栈的组件

这些组件包括以下内容:

Prometheus Operator

图中单独设置的 Operator 管理其他组件。

Prometheus

指标数据库以高可用性配置运行,通过 StatefulSet 进行管理。Prometheus 使用具有后备 PersistentVolume 的时间序列数据库存储数据。

Node exporter

node exporter 在每个 Kubernetes Worker 节点上运行,允许 Prometheus 通过 HTTP 拉取操作系统指标。

客户端库

应用程序可以嵌入 Prometheus 客户端库,允许 Prometheus 通过 HTTP 拉取指标。

警报管理器

这可以配置为基于特定指标的阈值生成警报,通过电子邮件或 PagerDuty 等第三方工具进行传递。kube-prometheus 栈内置了用于 Kubernetes 集群的警报;还可以添加特定于应用程序的警报。

Grafana

此部署用于提供图表,用于向人类操作员显示指标。Grafana 使用 PromQL 从 Prometheus 访问指标,这个接口也对其他客户端可用。

虽然在图中未显示,该堆栈还包括Kubernetes Metrics APIs 的 Prometheus 适配器,这是一个可选组件,用于将 Prometheus 收集的指标公开给 Kubernetes 控制平面,以便用于自动扩展应用程序。

将 K8ssandra 与 Prometheus 连接可以通过几个快速步骤完成。K8ssandra 文档中的说明指导您安装 Prometheus Operator,如果您尚未安装 kube-prometheus。由于 kube-prometheus 在自己的命名空间中安装 Prometheus Operator,您需要确保操作员有权限管理其他命名空间中的资源。

要将 K8ssandra 与 Prometheus 集成,您可以在 K8ssandraCluster 资源上设置属性,以启用对 Cassandra 和 Stargate 节点的监控。例如,您可以像以下示例那样为集群中所有数据中心的节点启用监控:

apiVersion: k8ssandra.io/v1alpha1
kind: K8ssandraCluster
metadata:
  name: demo
spec:
  cassandra:
    datacenters:
      ...
    telemetry:
      prometheus:
        enabled: true
  stargate:
    telemetry:
      prometheus:
        enabled: true

也可以选择性地在单个数据中心上启用监控。

让我们看看集成是如何工作的。首先,让我们考虑 Cassandra 节点如何公开指标。如"在 Kubernetes 中使用 Cass Operator 管理 Cassandra"中讨论的,Cassandra 通过 JMX 公开管理功能,包括指标报告。Apache Cassandra 的 Metric Collector (MCAC)是一个开源项目,公开指标以便 Prometheus 或其他使用 Prometheus 协议通过 HTTP 访问的后端。K8ssandra 和 Cass Operator 使用包含 MCAC 以及作为附加进程运行在 Cassandra 容器中的管理 API 的 Cassandra Docker 镜像。这个配置显示在图 6-8 的左侧。

使用 kube-prometheus 堆栈监控 Cassandra

图 6-8. 使用 kube-prometheus 堆栈监控 Cassandra

图 6-8 的右侧显示了如何配置 Prometheus 和 Grafana 以消费和公开 Cassandra 指标。K8ssandra Operator 为每个启用监控的 CassandraDatacenter 创建 ServiceMonitor 资源。ServiceMonitor 是由 Prometheus Operator 定义的 CRD,包含了从一组 Pod 收集指标的配置细节,包括以下内容:

  • 引用标识 Pod 的标签名称的selector

  • 连接信息,例如从每个 Pod 收集指标所需的scheme(协议)、portpath

  • 应该拉取指标的interval

  • 可选的metricRelabelings,这些指令指示任何想要重命名指标的需求,甚至指示应该删除的指标,以避免被 Prometheus 采集

K8ssandra 为 Cassandra 和 Stargate 节点创建单独的 ServiceMonitor 实例,因为暴露的指标略有不同。要观察部署在您的集群中的 ServiceMonitors,您可以执行诸如kubectl get servicemonitors -n monitoring之类的命令。

Prometheus 通过一个作为 Kubernetes 服务公开的 PromQL 端点向 Grafana 和其他工具提供其指标的访问。kube-prometheus 安装配置了一个 Grafana 实例,以使用 Grafana Datasource CRD 的实例连接到 Prometheus。Grafana 接受使用 YAML 文件定义的仪表板,并作为 ConfigMaps 提供。请参阅 K8ssandra 文档 了解如何加载显示 Cassandra 和 Stargate 指标的仪表板定义。您可能还希望创建显示应用程序指标和 K8ssandra 提供的数据层指标的仪表板,以集成查看应用程序性能。

正如你所见,kube-prometheus 为 Kubernetes 集群提供了一个全面且可扩展的监控堆栈,就像 K8ssandra 为数据管理提供了一个堆栈一样。K8ssandra 与 kube-prometheus 的集成是如何组装集成的 Kubernetes 资源以形成更强大的应用程序的一个很好的例子。

使用 Cassandra Reaper 执行修复

作为一种 NoSQL 数据库,Cassandra 默认强调高性能(特别是写入操作)和高可用性。如果你熟悉 CAP 定理,你就会理解这意味着有时候 Cassandra 会暂时牺牲节点间数据一致性,以便在规模化时提供高性能和高可用性,这种方法被称为最终一致性。通过指定复制策略和每个查询所需的一致性级别,Cassandra 确实提供了调整一致性程度以满足需求的能力。用户和管理员应了解这些选项及其行为,以有效使用 Cassandra。

Cassandra 具有多种内置的“反熵”机制,如提示协作和修复,这有助于随着时间推移在节点之间保持数据的一致性。修复是一个后台过程,节点通过该过程比较它所拥有的数据的一部分与其他也负责该数据的节点的最新内容。尽管可以通过使用校验和来优化这些检查,但修复仍然可能是一个性能密集型的过程,并且最好在集群负载减少或低峰期执行。结合多种选项,包括全面和增量修复,执行修复通常需要针对每个集群进行一些定制。它也倾向于是一个手动过程,不幸的是,一些 Cassandra 集群管理员经常忽视它。

Cassandra 中关于修复的更多详细信息

有关修复的更深入的处理,请参阅 Cassandra: The Definitive Guide,其中第六章和第十二章分别描述了修复概念和可用选项。

Cassandra Reaper 的创建旨在简化在 Cassandra 集群上执行修复的难度,并优化修复性能,以最小化对高度使用的集群运行修复的影响。Reaper 由 Spotify 创建,并由 The Last Pickle 增强,目前在 GitHub 上进行项目管理。Reaper 提供了一个 RESTful API,用于为一个或多个 Cassandra 集群配置修复计划,并提供命令行工具和 Web 界面,指导管理员完成创建计划的过程。

K8ssandra 提供了将 Cassandra Reaper 实例作为 K8ssandraCluster 的一部分进行集成的选项。K8ssandra Operator 包括一个 Reaper 控制器,负责通过其相关的 Reaper CRD 管理本地 Cassandra Reaper 管理进程。默认情况下,在 K8ssandraCluster 中启用 Reaper 将导致在每个安装中表示的 Kubernetes 集群中安装一个 Reaper 实例,但您也可以使用单个 Reaper 实例来管理跨多个数据中心甚至跨多个 Cassandra 集群的修复,只要它们可以通过网络访问。

使用 Cassandra Medusa 进行数据备份和恢复

管理备份是维护任何存储数据系统高可用性和灾难恢复计划的重要组成部分。Cassandra 支持通过创建指向用于数据持久化的 SSTable 文件的硬链接来进行完整和差异备份。Cassandra 本身不负责将 SSTable 文件复制到备份存储中,而是由用户来完成这一任务。同样,从备份恢复涉及将 SSTable 文件复制到要重新加载数据的 Cassandra 节点,然后可以指向本地文件以恢复其内容。

Cassandra 的备份和恢复操作通常通过单个节点上的 nodetool 执行,这是一个命令行工具,利用 Cassandra 的 JMX 接口。Cassandra Medusa 是由 Spotify 和 The Last Pickle 创建的开源命令行工具,用于执行 nodetool 命令来执行备份操作,包括跨多个节点同步备份。Medusa 支持 Amazon S3、Google Cloud Storage (GCS)、Azure Storage,以及像 MinIO 和 Ceph Object Gateway 这样的 S3 兼容存储,还可以通过 Apache Libcloud 项目扩展支持其他存储提供商。

Medusa 可以恢复单个节点,以支持快速替换掉线节点,或在灾难恢复场景下恢复整个集群。恢复到集群可以是原始集群,也可以是新集群。Medusa 能够将数据恢复到比原始集群具有不同大小或拓扑的集群,这在传统上是手动解决的一个挑战。

K8ssandra 已整合 Medusa,以提供在 Kubernetes 中运行的 Cassandra 集群的备份和恢复功能。要配置 K8ssandraCluster 中 Medusa 的使用,您需要配置medusa属性:

apiVersion: k8ssandra.io/v1alpha1
kind: K8ssandraCluster
metadata:
  name: demo
spec:
  cassandra:
    ...
  medusa:
    storageProperties:
      storageProvider: google_storage
      storageSecretRef:
        name: medusa-bucket-key
      bucketName: k8ssandra-medusa
      prefix: test
      ...

此处显示的选项包括存储提供程序、用于备份的 bucket、用于组织备份文件的目录名称的可选前缀,以及包含 bucket 登录凭据的 Kubernetes Secret 的名称。有关 Secret 内容的详细信息,请参阅文档。其他可用选项包括在 bucket 连接上启用 SSL,并设置清除旧备份的策略,如最大年龄或备份数量。

创建备份

一旦启动了 K8ssandraCluster,您可以使用 CassandraBackup CRD 创建备份。例如,您可以使用以下命令启动对 CassandraDatacenter dc1的备份:

cat <<EOF | kubectl apply -f -n k8ssandra-operator -
apiVersion: medusa.k8ssandra.io/v1alpha1
kind: CassandraBackup
metadata:
  name: medusa-backup1
spec:
  cassandraDatacenter: dc1
  name: medusa-backup1
EOF

此资源的处理步骤如图 6-9 所示。

使用 Medusa 执行 Datacenter 备份

图 6-9. 使用 Medusa 执行 Datacenter 备份

当您应用资源定义(1)时,kubectl将资源注册到 API 服务器(2)。API 服务器通知作为 K8ssandra Operator 的一部分运行的 Medusa 控制器(3)。

Medusa 控制器联系一个 sidecar 容器(4),该容器已由 K8ssandra 注入到 Cassandra Pod 中,因为您选择在 K8ssandraCluster 上启用了 Medusa。Medusa sidecar 容器使用 nodetool 命令通过 JMX 在 Cassandra 节点上执行备份(5)(JMX 接口仅在 Pod 内部暴露)。

Cassandra 执行备份(6),标记 PersistentVolume 上标记当前快照的 SSTable 文件。Medusa sidecar 将快照文件从 PV 复制到 bucket(7)。步骤 4-7 将为 CassandraDatacenter 中的每个 Cassandra Pod 重复执行。

您可以通过检查资源的状态来监视备份的进度:

kubectl get cassandrabackup/medusa-backup1 -n k8ssandra-operator -o yaml
kind: CassandraBackup
metadata:
    name: medusa-backup1
spec:
  backupType: differential
  cassandraDatacenter: dc1
  name: medusa-backup1
status:
  ...
  ...
  finishTime: "2022-02-26T09:21:38Z"
  finished:
  - demo-dc1-default-sts-0
  - demo-dc1-default-sts-1
  - demo-dc1-default-sts-2
  startTime: "2022-02-26T09:21:35Z"

finishTime属性填充时,您将知道备份已完成。已备份的 Pod 将列在finished属性下。

从备份恢复

从备份恢复数据的过程类似。要从备份数据中恢复整个 Datacenter,您可以创建类似以下的 CassandraRestore 资源:

cat <<EOF | kubectl apply -f -n k8ssandra-operator -
apiVersion: medusa.k8ssandra.io/v1alpha1
kind: CassandraRestore
metadata:
  name: restore-backup1
spec:
  cassandraDatacenter:
    name: dc1
    clusterName: demo
  backup: medusa-backup1
  inPlace: true
  shutdown: true
EOF

当 Medusa 控制器收到新资源通知时,它会定位 CassandraDatacenter 并更新管理 Cassandra Pod 的 StatefulSet 内的 Pod 模板。更新包括添加一个名为 medusa-restore 的新初始化容器,并设置 medusa-restore 将用于定位要恢复的数据文件的环境变量。对 Pod 模板的更新会导致 StatefulSet 控制器对 StatefulSet 中的 Cassandra Pod 执行滚动更新。随着每个 Pod 重新启动,medusa-restore 将文件从对象存储复制到节点的 PersistentVolume 上,然后 Cassandra 容器像往常一样启动。您可以通过检查 CassandraRestore 资源的状态来监视恢复的进度。

数据恢复的通用语言?

有趣的是注意到本章讨论的 K8ssandra 操作员和第五章讨论的 Vitess 操作员在支持备份和恢复操作方面的相似性和差异。

在 K8ssandra 中,CassandraBackup 和 CassandraRestore 资源的功能方式类似于 Kubernetes Job —— 它们表示您希望执行的任务以及任务的结果。相比之下,VitessBackup 资源表示 Vitess 操作员根据 VitessCluster 资源的配置执行的备份记录。在 Vitess 中没有等效于 CassandraRestore 操作员的资源。

虽然 K8ssandra 和 Vitess 在备份管理方法上有显著差异,但两者都将每个备份任务表示为资源。也许这种共同点可以成为开发通用资源定义以进行备份和恢复操作的起点,有助于实现第五章介绍的愿景(第五章)。

与 Cassandra Reaper 的行为类似,单个 Medusa 实例可以配置为跨多个数据中心或 Cassandra 集群管理备份和恢复操作。有关使用 Medusa 执行备份和恢复操作的更多详细信息,请参阅 K8ssandra 文档

在 Kubernetes 中部署多集群应用程序

像 Cassandra 这样的分布式数据库的一个主要卖点之一是其支持跨多个数据中心的部署。许多用户利用这一点来促进地理分布的数据中心之间的高可用性,以为应用程序和其用户提供更低延迟的读写服务。

然而,Kubernetes 本身最初并不是设计用于支持跨多个 Kubernetes 集群的应用程序。这通常意味着创建这种多地区应用程序会留下大量工作给开发团队。

这项工作主要分为两种形式:创建连接 Kubernetes 集群的网络基础设施,以及协调这些集群中资源的交互。让我们详细研究这些要求以及对像 Cassandra 这样的应用程序的影响:

多集群网络要求

从网络角度来看,关键是在数据中心之间建立安全可靠的网络连接。如果您的应用程序使用单一云提供商,使用主要云供应商提供的 VPC 功能可能相对简单。

如果您使用多个云,则需要一个第三方解决方案。在大多数情况下,Cassandra 需要其节点之间的可路由 IP,不依赖名称解析,但也有助于使用 DNS 简化管理 Cassandra 种子节点的过程。

Jeff 的博文“在 Kubernetes 中部署多数据中心 Cassandra 集群”描述了在 Google Cloud Platform(GCP)上使用 CloudDNS 服务的示例配置,而 Raghavan Srinivas 的博文“在 Amazon EKS 上使用 K8ssandra 和 Kubefed 部署多区域 Cassandra”描述了类似配置。

多集群资源协调要求

管理跨多个 Kubernetes 集群的应用程序意味着每个集群中存在与 Kubernetes 控制平面无关的不同资源。为了管理应用程序的生命周期,包括部署、升级、扩展和撤销,您需要协调多个数据中心之间的资源。

Kubernetes 集群联邦项目(KubeFed 简称)提供了一种管理跨集群资源的 API 集合的方法,可用于构建多集群应用程序。这包括将 Kubernetes 集群本身表示为资源的机制。尽管 KubeFed 还处于 beta 版,但 K8ssandra Operator 使用了类似的设计方法来管理跨集群资源。我们将在“Kubernetes 集群联邦”中详细探讨这一点。

要实现 Cassandra 的多集群 Kubernetes 部署,您需要根据具体情况在数据中心之间建立网络连接。在此基础上,K8ssandra Operator 提供了管理 Kubernetes 集群之间资源生命周期的功能。要了解如何简单部署多区域 K8ssandraCluster,请参阅 K8ssandra 文档中的说明,再次使用 Makefile:

make multi-up

这建立了两种集群类型,在每个集群中部署了 K8ssandra Operator,并创建了一个多集群的 K8ssandraCluster。使用 kind 进行简单演示的一个优势是 Docker 提供了集群间的网络连接。我们将逐步介绍此过程中的关键步骤,以描述 K8ssandra Operator 如何完成这项工作。

K8ssandra Operator 支持两种安装模式:控制平面(默认)和数据平面。对于多集群部署,一个 Kubernetes 集群必须被指定为控制平面集群,其他的作为数据平面集群。控制平面集群可以选择包含一个 CassandraDatacenter,就像在 Figure 6-10 中显示的配置一样。

K8ssandra 多集群架构

图 6-10. K8ssandra 多集群架构

当安装在控制平面模式时,K8ssandra Operator 使用两个额外的 CRD 来管理多集群部署:ReplicatedSecret 和 ClientConfig。您可以在使用的 K8ssandraCluster 配置中看到 ClientConfig 的证据,其大致如下:

apiVersion: k8ssandra.io/v1alpha1
kind: K8ssandraCluster
metadata:
  name: demo
spec:
  cassandra:
    serverVersion: "4.0.1"
    ...
    networking:
      hostNetwork: true   
    datacenters:
      - metadata:
          name: dc1
        size: 3
        stargate:
          size: 1
      - metadata:
          name: dc2
        k8sContext: kind-k8ssandra-1
        size: 3
        stargate:
          size: 1

此配置指定了一个名为demo的 K8ssandraCluster,由两个 CassandraDatacenter dc1dc2 组成。每个 Datacenter 都有自己的配置,因此您可以为 Pods 选择不同数量的 Cassandra 和 Stargate 节点,或者不同的资源分配。在demo配置中,dc1 运行在控制平面集群 kind-k8ssandra-0 中,而 dc2 运行在数据平面集群 kind-k8ssandra-1 中。

注意配置中的k8sContext: kind-k8ssandra-1行。这是对通过make命令创建的 ClientConfig 资源的引用。ClientConfig 是一个表示连接到另一个集群 API 服务器所需信息的资源,类似于kubectl在本地机器上存储不同集群信息的方式。ClientConfig 资源引用了一个 Secret,用于安全存储访问凭据。K8ssandra Operator 仓库包含一个方便脚本,可用于为 Kubernetes 集群创建 ClientConfig 资源。

在控制平面集群中创建 K8ssandraCluster 时,它使用 ClientConfigs 连接到每个远程 Kubernetes 集群,以创建指定的资源。对于前述配置,这包括 CassandraDatacenter 和 Stargate 资源,还可以包括其他资源如 Medusa 和 Prometheus ServiceMonitor。

ReplicatedSecret 是另一个涉及共享访问凭证的资源。控制平面的 K8ssandra Operator 使用此资源来跟踪它在每个远程集群中创建的 Secrets。这些 Secrets 被各种 K8ssandra 组件用于安全地相互通信,例如默认的 Cassandra 管理员凭据。K8ssandra Operator 自己创建和管理 ReplicatedSecret 资源;你不需要与它们交互。

K8ssandraCluster、ClientConfig 和 ReplicatedSecret 资源仅存在于控制平面集群中,当 K8ssandra Operator 以数据平面模式部署时,它甚至不运行与这些资源类型相关的控制器。

K8ssandra Operator 的更多细节

这是一个关于多集群操作员复杂设计的快速摘要。有关该方法的更多详细信息,请参阅 K8ssandra Operator 的架构概述和 John Sanda 在 Data on Kubernetes 社区(DoKC)聚会上的演示

现在让我们考虑一种更通用的构建多集群应用程序的方法,我们可以与 K8ssandra 的方法进行比较和对比。

正如你所看到的,Kubernetes 联邦领域有很大的增长潜力,可以跨越 Kubernetes 集群边界管理资源。例如,作为一个主要超能力是在多个数据中心运行的数据库,Cassandra 似乎是与类似 KubeFed 的多集群解决方案非常匹配的选择。

K8ssandra Operator 和 KubeFed 采用了类似的架构方法,其中自定义的“联邦”资源提供了在其他集群中定义资源的模板。这种共同点指向了未来在这些项目和其他基于类似设计原则的项目之间进行合作的可能性。也许在未来,像 K8ssandra 的 ClientConfig 和 ReplicatedSecret 这样的 CRD 可以被 KubeFed 提供的等效功能所取代。

总结

在本章中,你已经学会了如何将数据基础设施与其他基础设施组合起来构建可重复使用的 Kubernetes 堆栈。以 K8ssandra 项目为例,你已经了解了集成数据基础设施与 API 网关和监控解决方案以提供更全面功能解决方案的方面。

你还学会了将现有技术调整到 Kubernetes 上并创建多集群数据基础设施部署时的一些机遇和挑战。在下一章中,我们将探讨如何设计利用 Kubernetes 提供的一切而无需适应的新的云原生数据基础设施,并发现这将带来哪些新的可能性。

第七章:Kubernetes 原生数据库

软件行业充斥着用一个单词或短语定义主要趋势的术语。你可以在本书的标题中看到其中之一:云原生。另一个例子是 微服务,这是一种主要的架构范式,涉及我们在这里讨论的大部分技术。最近,出现了像 Kubernetes 原生无服务器 这样的术语。

尽管简洁而引人注目,将一个复杂的主题或趋势归纳为一个单一的声音留下了模糊的空间,或者至少是一些合理的问题,比如“这究竟是什么意思?”进一步混淆视听的是,像这些术语一样经常在营销产品的背景下使用,作为获取优势或与其他竞争产品区分开来的一种方式。无论你正在消费的内容是明确陈述还是仅仅是含蓄的,你可能会想知道,一个技术是否必须在 Kubernetes 上运行才能比其他提供更好,因为它被标记为 Kubernetes 原生

当然,对于我们评估和选择适合我们应用程序的正确技术来说,这些术语是否对我们有用的真正任务是澄清它们实际含义,就像我们在第一章中对 云原生数据 这个术语所做的那样。在本章中,我们将探讨数据技术被定义为 Kubernetes 原生的含义,并看看是否能得出一个我们可以达成共识的定义。为此,我们将检视几个宣称拥有这些术语的项目,并推导出共同的原则:TiDB 和 Astra DB。准备好了吗?让我们深入了解吧!

为什么需要 Kubernetes 原生方法

首先,让我们讨论为什么首次提出了 Kubernetes 原生数据库的概念。直到本书的这一部分,我们专注于在 Kubernetes 上部署现有的数据库,包括 MySQL 和 Cassandra。这些都是在 Kubernetes 存在之前就存在并且经过时间验证的成熟数据库。它们拥有庞大的安装基础和用户社区,正因为有这样的投资,你可以理解为什么在 Kubernetes 环境中运行这些数据库有如此大的激励,并且为什么有兴趣创建操作员来自动化它们。

与此同时,你可能已经注意到将这些数据库适应 Kubernetes 的一些尴尬之处。虽然通过改变挂载路径简单地将数据库指向基于 Kubernetes 的存储相对直接,但更紧密地集成 Kubernetes 来管理由多个节点组成的数据库可能会更复杂一些。这可以从相对简单的任务开始,比如在 Pod 中部署传统的管理 UI 并公开对 HTTP 端口的访问,到我们在第六章中看到的更复杂的部署 sidecar 以提供管理和指标收集的 API。

对这种复杂性的认识促使一些创新者从一开始就设计成 Kubernetes 原生的新数据库。在数据库行业中,有一个广为人知的公理是,一个新的数据库引擎需要 5 到 10 年的时间才能达到成熟阶段。因此,这些 Kubernetes 原生的数据库通常不是全新的实现,而是将现有数据库重构为可以独立扩展的微服务,同时保持与开发人员熟悉的现有 API 的兼容性。因此,将单体应用分解的趋势已经到达了数据层。新一代的数据库将基于新的架构来真正利用 Kubernetes 的优势。

为了帮助我们评估这些新数据库是否符合 Kubernetes 原生的标准,让我们使用“云原生数据基础设施原则”中介绍的云原生数据原则作为指南,制定一些问题来询问数据库与 Kubernetes 的交互方式:

原则 1:将计算、网络和存储作为商品 API 利用

数据库如何使用 Kubernetes 计算资源(Pods、Deployments、StatefulSets)、网络资源(Services 和 Ingress)以及存储资源(PersistentVolumes、PersistentVolumeClaims、StorageClasses)?

原则 2:分离控制平面和数据平面

数据库是否由 operator 部署和管理?它定义了哪些自定义资源?除了 operator 外,控制平面中是否还有其他工作负载?

原则 3:简化可观察性

架构中的各种服务如何暴露指标和日志,以便 Kubernetes 控制平面和第三方扩展进行收集?

原则 4:使默认配置安全

数据库和相关的 operator 是否使用 Kubernetes Secrets 共享凭据,并使用 Roles 和 RoleBindings 来管理角色访问?服务是否尽量减少暴露点的数量,并要求对其进行安全访问?

原则 5:优先使用声明式配置

扩展原则 2,数据库是否完全可以通过创建、更新或删除 Helm charts 和 Kubernetes 资源(无论是内置还是自定义资源)来管理,或者是否需要其他工具?

在接下来的章节中,我们将探讨两个数据库的答案,并了解关于什么是 Kubernetes 原生的更多信息。这将帮助我们在本章末尾制定一个清单,以帮助巩固我们的定义。(参见“在 Kubernetes 原生数据库中寻找什么”关于我们得出的内容。)

使用 TiDB 实现规模化的混合数据访问

到目前为止,本书中大部分关注的数据库代表了数据库架构中的两大主要趋势,这些趋势可以追溯到数十年甚至更久以前。MySQL 是一种关系数据库,提供其自己的 SQL 标准查询语言,这些规则是 Edgar Codd 在 1970 年代开发的。

在 21 世纪初,构建网络规模应用的公司开始挑战当时关系数据库的极限。随着数据库规模超过单一实例的可管理范围,诸如分片等技术开始用于跨多个实例的扩展。这些技术经常昂贵、难以操作,并且不总是可靠。

作为对这一需求的响应,Cassandra 和其他所谓的NoSQL数据库在一个充满创新和实验的时期出现。这些数据库通过增加额外节点实现线性扩展性。它们提供不同的数据模型或数据表示方式,例如 Redis 的键值存储、MongoDB 的文档数据库、Neo4j 的图数据库以及其他类型。NoSQL 数据库往往提供较弱的一致性保证,并省略了对像事务和连接这样的更复杂行为的支持,以实现高性能和可用性的规模化,这是 Eric Brewer 在他的CAP 定理中记录的一种权衡。

由于开发者对传统关系型语义(如强一致性、事务和连接)的需求持续存在,从 2012 年开始,多个团队开始重新支持在分布式数据库中实现这些功能的想法。这些所谓的NewSQL数据库基于更高效和性能更好的共识算法。两篇关键论文推动了 NewSQL 运动的出现。首先是Calvin 论文,引入了一个全局共识协议,这代表了一种更可靠和高效的方法,用于保证强一致性,并被 FaunaDB 和其他数据库后来采纳。其次是 Google 的Spanner 论文,介绍了一种使用分片和新共识算法设计的分布式关系数据库,利用云基础设施提供跨数据中心时间同步的能力,这种方法除了 Google Spanner 外,还被 CockroachDB 和 YugabyteDB 等数据库实现。

更多关于一致性与共识的内容

虽然本书篇幅有限,无法深入讨论各种共识算法之间的权衡及其如何用于提供各种数据一致性保证,但理解这些概念有助于选择适合云应用程序的正确数据基础架构。如果您对这个领域有兴趣,Martin Kleppmann 的《设计数据密集型应用》(O'Reilly)是一个很好的资源,特别是第九章“一致性与共识”。

TiDB(其中Ti代表)代表着云原生空间中 NewSQL 趋势的延续。TiDB 是一个开源的、兼容 MySQL 的数据库,支持事务和分析工作负载。它最初由 PingCAP 开发,并得到主要支持。虽然 TiDB 是一个旨在体现可扩展性和弹性的云原生数据库,但特别引人注目的是它明确设计为在 Kubernetes 上运行,并依赖于 Kubernetes 控制平面提供的功能。因此,可以说 TiDB 不仅仅是一个 Kubernetes 原生数据库,还是一个 Kubernetes 专属数据库。让我们深入了解细节。

TiDB 架构

TiDB 的一个关键特征是其能够支持事务和分析工作负载,这使其与本书中迄今为止我们审查过的其他数据库有所区别。这种方法被称为混合事务/分析处理(HTAP),支持两种类型的查询,而无需单独的抽取、转换和加载(ETL)过程。如图 7-1 所示,TiDB 通过在底层提供两个数据库引擎来实现这一点:TiKV 和 TiFlash。这种方法受到 Google 的F1 项目的启发,该项目是建立在 Spanner 之上的一层。

TiDB 架构

图 7-1. TiDB 架构

使 TiDB 具有云原生架构的一个关键方面是将计算和存储操作打包成单独的组件,每个组件由独立可伸缩的服务组成,并组织成集群。让我们详细探讨每个组件的角色:

TiDB

每个 TiDB 实例是一个无状态的服务,向客户端应用程序暴露一个 MySQL 端点。TiDB 解析传入的 SQL 请求,并使用来自 Placement Driver(PD)的元数据创建执行计划,包含对存储集群中特定 TiKV 和 TiFlash 节点的查询。TiDB 执行这些查询,组装结果,并返回给客户端应用程序。TiDB 集群通常部署在代理之前,以提供负载均衡。

TiKV

存储集群由 TiKV 和 TiFlash 节点的混合组成。首先,让我们来看看TiKV,这是一个开源的、分布式键值数据库,使用RocksDB作为其后端存储引擎。TiKV 暴露一个自定义的分布式 SQL API,TiDB 节点使用它来执行存储和检索数据以及管理分布式事务的查询。TiKV 存储您的数据的多个副本,通常至少三个,以支持高可用性和自动故障转移。TiKV 是一个CNCF 毕业项目,可以独立于 TiDB 使用,我们稍后将讨论这一点。

TiFlash

存储集群还包括 TiFlash 节点,数据写入时从 TiKV 节点复制到这些节点。TiFlash 是基于开源 ClickHouse 分析数据库 的列式数据库,这意味着它将数据存储按列而不是行进行组织。列式数据库在需要跨多行提取同一列的分析查询中可以提供显著的性能优势。

TiSpark

此库专为 Apache Spark 构建,以支持复杂的 OLAP 查询。TiSpark 与 Spark Driver 和 Spark Executors 集成,通过分布式 SQL API 从 TiFlash 实例中摄取数据。我们将在第九章 中详细讨论 Spark 架构和在 Kubernetes 上部署 Spark 的细节。

Placement Driver(PD)

PD 管理 TiDB 安装的元数据。PD 实例部署在至少三个节点的集群中。TiDB 使用基于范围的分片机制,每个表中的键被划分为称为region的范围。PD 负责确定分配给每个 region 的数据范围,以及存储每个 region 数据的 TiKV 节点。它监视每个 region 中的数据量,并在 region 变得过大时拆分 region 以便扩展,并合并较小的 region 以缩小规模。

因为 TiDB 架构由各组件之间的明确定义接口组成,它是一种可扩展的架构,可以在其中插入不同的部件。例如,TiKV 提供了分布式键值存储解决方案,可以在其他应用程序中重用。TiPrometheus 项目 就是一个示例,它在 TiKV 之上提供了符合 Prometheus 的计算层。另一个例子是,您可以提供 TiKV 的另一个实现,该实现在不同的存储引擎上实现了分布式 SQL API。

可插拔存储引擎

到目前为止,在本章中,我们多次提到“存储引擎”或“数据库引擎”。这个术语指的是数据库的一部分,负责在持久介质上管理数据的存储和检索。在分布式数据库中,通常区分存储引擎和位于其上的代理层,代理层用于管理节点之间的数据复制。来自 设计数据密集型应用 的第三章“存储和检索”包括对存储引擎类型的讨论,例如大多数关系数据库中使用的 B 树,以及 Apache Cassandra 和其他 NoSQL 数据库中使用的日志结构合并树(LSM 树)。

TiDB 的一个有趣之处在于它如何重用现有技术。我们已经在使用组件如 RocksDB 和 Spark 的示例中看到了这一点。TiDB 还使用其他组织开发的算法。以下是几个例子:

Raft 共识协议

在 TiDB 层面,使用 Raft 共识协议 管理副本之间的一致性。Raft 在行为上类似于 Cassandra 使用的 Paxos 算法,但其设计更简单易学。TiDB 为每个区域使用一个单独的 Raft 组,其中一个组通常包含一个领导者和两个或更多个副本。如果丢失领导节点,则会运行选举来选择新的领导者,并且可以添加新的副本以确保所需的副本数。此外,TiFlash 节点被配置为一种特殊类型的副本,称为 learner replicas。数据从 TiDB 节点复制到 learner replicas,但它们不能被选为领导者。您可以在 PingCAP 博客 上进一步了解 TiDB 如何利用 Raft 实现高可用性及其他相关主题。

Percolator 事务管理

在 TiDB 层面,使用 Percolator 算法 实现支持分布式事务,该算法进行了 TiDB 项目特定的优化。Percolator 最初由 Google 开发,用于支持对搜索索引的增量更新。

本章我们提出的一个论点是,数据基础设施成为云原生的一部分,意味着尽可能地组合现有的 API、服务和算法,TiDB 就是一个很好的例子。

在 Kubernetes 中部署 TiDB

虽然 TiDB 可以通过裸机和虚拟机等多种方式部署,但 TiDB 团队已经投入大量精力在工具和文档上,使 TiDB 成为真正的 Kubernetes 本地数据库。TiDB Operator 管理 Kubernetes 中的 TiDB 集群,包括部署、升级、扩展、备份和恢复等操作。

操作员的 文档 提供了桌面 Kubernetes 发行版(如 kind、minikube 和 Google Kubernetes Engine (GKE))的 快速入门指南。这些说明将引导您完成包括使用 Helm 安装 CRDs 和 TiDB 操作员,以及包括监控服务的简单 TiDB 集群的步骤。我们将使用这些快速入门说明来介绍 TiDB 成为 Kubernetes 本地数据库的特点。

安装 TiDB CRDs

确保拥有符合定义先决条件的 Kubernetes 集群,例如拥有 默认的 StorageClass,使用操作员部署 TiDB 的第一步是安装操作员使用的 CRDs。可以通过类似以下的指令完成此操作(请注意实际的操作员版本号 v1.3.2 可能会有所不同):

set GH_LINK=https://raw.githubusercontent.com
kubectl create -f \
  $GH_LINK/pingcap/tidb-operator/v1.3.2/manifests/crd.yaml

这导致创建了多个 CRDs,您可以通过运行 kubectl get crd 命令来观察这些 CRD,就像我们在前面的章节中所做的那样。我们将快速讨论每个资源的目的,因为其中几个资源暗示了其他感兴趣的功能:

  • TidbCluster 是描述 TiDB 集群所需配置的主要资源。稍后我们将看一个示例。

  • TidbMonitor 资源用于部署基于 Prometheus 的监控堆栈,以观察一个或多个 TidbCluster。正如我们在其他项目中所见,Prometheus(或其 API)已成为在 Kubernetes 上部署的数据库和其他基础设施的度量收集的事实标准。

  • 备份和恢复资源表示执行备份或从备份中恢复的操作。这类似于我们之前从 Vitess(参见“PlanetScale Vitess Operator”)和 K8ssandra(第六章)项目中研究过的其他操作器。TiDB Operator 还提供了 BackupSchedule 资源,可用于配置定期备份。

  • TidbInitializer 是一个可选资源,您可以创建它来执行TidbCluster 上的初始化任务,包括设置管理员凭据并执行用于架构创建或初始数据加载的 SQL 语句。

  • TidbClusterAutoScaler 是另一个可选资源,用于配置 TidbCluster 的自动扩展行为。可以根据 CPU 利用率配置 TidbCluster 中 TiKV 或 TiDB 节点的最小和最大限制进行扩展。基于其他指标的扩展规则的添加已包含在项目路线图中。正如我们在“选择运算符”中讨论的那样,自动缩放被认为是运算符的 Level 5 或 Autopilot 功能,是最高成熟度级别的特性。

  • TidbNGMonitoring 是一个可选资源,配置 TidbCluster 以启用连续剖析,可以达到系统调用级别。产生的剖析数据和火焰图可通过单独部署的TiDB 仪表盘进行观察。通常由项目工程师用于优化数据库,但应用和平台开发人员也可能会发现其有用。

  • DMCluster 资源用于部署 TiDB 数据迁移(DM)平台的一个实例,支持将 MySQL 和 MariaDB 数据库实例迁移到 TidbCluster。还可以配置从在 Kubernetes 外部现有的 TiDB 安装迁移到 TidbCluster。在由同一运算符管理的 Kubernetes 中部署数据迁移服务与目标 TidbCluster 并行的能力,是在 Kubernetes 中开发数据生态系统的一个极好的示例,这是我们希望在未来看到更多的模式。

在本节的其余部分,我们将重点关注 TidbCluster 和 TidbMonitoring 资源。

安装 TiDB Operator

安装完 CRD 后,下一步是使用 Helm 安装 TiDB Operator。在安装 TiDB Operator 到其自己的命名空间之前,您需要先添加 Helm 仓库:

helm repo add pingcap https://charts.pingcap.org
helm install –create-namespace --namespace tidb-admin tidb-operator \
  pingcap/tidb-operator --version v1.3.2

您可以使用 kubectl get pods 命令并引用 tidb-admin 命名空间来监视生成的 Pod。第 7-2 图 概述了到目前为止安装的元素。这包括用于管理 TiDB Operator(标记为 tidb-controller-manager)和 TiDB 调度器的部署。

TiDB 调度器是 Kubernetes 内置调度器的可选扩展。虽然它作为 TiDB Operator 的一部分默认部署,但可以禁用。假设未禁用 TiDB 调度器,仍需要通过将 schedulerName 属性设置为 tidb-scheduler 来选择使用它来为特定的 TidbCluster 进行调度。如果设置了此属性,TiDB Operator 将分配 TiDB 调度器作为 Kubernetes 创建 TiKV 和 PD Pod 时使用的调度器。

TiDB 调度器扩展了 Kubernetes 内置调度器,为 TidbCluster 中的 Pod 添加自定义调度规则,帮助实现数据库的高可用性,并在 Kubernetes 集群中的可用 Worker 节点上均匀分布负载。对于许多基础设施类型来说,Kubernetes 提供的影响默认调度器的现有机制,如亲和规则、污点和容忍度,已经足够使用。但 TiDB 提供了一个有用的例子,说明了何时以及如何实现自定义调度逻辑。我们将在第 9 章中详细讨论 Kubernetes 调度器的扩展。第 9 章 提供了更详细的信息。

安装 TiDB Operator 和 CRD

第 7-2 图。安装 TiDB Operator 和 CRD

TiDB Operator Helm 图选项

此安装未使用 values.yaml 文件,但您可以通过运行以下命令查看可用选项:

helm show values pingcap/tidb-operator

这包括禁用 TiDB 调度器的选项。

创建 TidbCluster

安装完 TiDB Operator 后,您就可以准备创建 TidbCluster 资源了。虽然 TiDB Operator GitHub 存储库中提供了许多 示例配置,但让我们使用快速入门指南中引用的配置。

set GH_LINK=https://raw.githubusercontent.com
kubectl create namespace tidb-cluster
kubectl -n tidb-cluster apply -f \
  $GH_LINK/pingcap/tidb-operator/master/examples/basic/tidb-cluster.yaml

在创建 TidbCluster 的过程中,您可以引用此文件的内容,看起来类似于这样(删除了一些注释和细节):

apiVersion: pingcap.com/v1alpha1
kind: TidbCluster
metadata:
  name: basic
spec:
  version: v5.4.0
  ...
  pd:
    baseImage: pingcap/pd
    maxFailoverCount: 0
    replicas: 1
    requests:
      storage: "1Gi"
    config: {}
  tikv:
    baseImage: pingcap/tikv
    maxFailoverCount: 0
    evictLeaderTimeout: 1m
    replicas: 1
    requests:
      storage: "1Gi"
    config:
      ...
  tidb:
    baseImage: pingcap/tidb
    maxFailoverCount: 0
    replicas: 1
    service:
      type: ClusterIP
    config: {}

注意,这将在 tidb-cluster 命名空间中创建一个名为 basic 的 TidbCluster,其中包括 TiDB、TiKV 和 PD 每个一个副本,使用标准的 PingCAP 镜像。还使用了其他选项来指定实现功能集群所需的最小计算和存储资源量。此简单配置不包括 TiFlash 节点。

TidbCluster API

您可以在 GitHub 存储库中的API中找到 TidbCluster 的完整选项列表。同一页还包括 TiDB Operator 使用的其他 CRD 的选项。当您探索这些 CRD 的选项时,您会看到允许覆盖许多用于指定底层资源的选项的常见做法(例如,在部署中设置的 Pod 规范)的证据。

我们鼓励您利用kubectl或您喜欢的可视化工具,探索作为 TidbCluster 一部分创建的资源。这些资源的摘要在图 7-3 中提供。

一个基本的 TidbCluster

图 7-3. 一个基本的 TidbCluster

正如您所见,TiDB Operator 创建了 StatefulSets 来管理 TiDB、TiKV 和 Placement Driver 实例,并为每个实例分配了 PVC。作为一个 I/O 密集型应用程序,默认配置是使用本地 PersistentVolumes 作为后备存储。

此外,还创建了一个 Deployment 来运行 Discovery Service,各个组件使用该服务来了解彼此的位置。Discovery Service 在本书中我们检查的其他数据技术中扮演了类似于 etcd 的角色。TiDB Operator 还为每个 StatefulSet 和 Deployment 配置了服务,以促进 TiDB 集群内部的通信,并向外部客户端公开功能。

TiDB Operator 支持部署 Prometheus 监控堆栈,可以管理一个或多个 TiDB 集群。您可以使用以下命令向先前创建的集群添加监控:

set GH_LINK=https://raw.githubusercontent.com
kubectl -n tidb-cluster apply -f \
  $GH_LINK/pingcap/tidb-operator/master/examples/basic/tidb-monitor.yaml

在部署过程中,让我们来查看tidb-monitor.yaml配置文件的内容:

apiVersion: pingcap.com/v1alpha1
kind: TidbMonitor
metadata:
  name: basic
spec:
  replicas: 1
  clusters:
  - name: basic
  prometheus:
    baseImage: prom/prometheus
    version: v2.27.1
  grafana:
    baseImage: grafana/grafana
    version: 7.5.11
  initializer:
    baseImage: pingcap/tidb-monitor-initializer
    version: v5.4.0
  reloader:
    baseImage: pingcap/tidb-monitor-reloader
    version: v1.0.1
  prometheusReloader:
    baseImage: quay.io/prometheus-operator/prometheus-config-reloader
    version: v0.49.0
  imagePullPolicy: IfNotPresent

正如您所见,TidbMonitor 资源可以指向一个或多个 TidbCluster。该 TidbMonitor 配置为管理您之前创建的basic集群。TidbMonitor 资源还允许您指定用于初始化和更新监控堆栈的 Prometheus、Grafana 和其他工具的版本。如果您检查tidb-cluster命名空间的内容,您将看到已创建用于管理这些元素的附加工作负载。

TiDB 与 K8ssandra 项目类似地使用 Prometheus 堆栈,正如我们在“使用 Prometheus 和 Grafana 实现统一监控基础设施”中讨论的那样。在这两个项目中,Prometheus 堆栈作为可选扩展得到支持,以提供您可以通过极少的定制即可使用的监控功能。配置和提供的可视化重点关注于驱动数据库健康意识的关键指标。即使您已经管理自己的监控基础设施或使用第三方软件即服务(SaaS)解决方案,这些配置和图表也可以帮助您快速整合数据库监控到您的可观察性方法中。

正如你所见,TiDB 是一个具有灵活可扩展架构的数据库,设计时考虑了云原生原则。它还倾向于能够在 Kubernetes 中有效部署和管理数据库,并为我们提供了一些有价值的见解,说明了什么是 Kubernetes 原生。有关诸如部署到多个 Kubernetes 集群等功能的更多信息,请参阅 TiDB 文档。

使用 DataStax Astra DB 的无服务器 Cassandra。

自 21 世纪初云计算的出现以来,公共云提供商和基础设施供应商不断推进将我们架构堆栈的各个层次作为服务提供的商品化进程。这一趋势始于提供计算、网络和存储作为基础设施即服务(IaaS),并进一步涵盖其他趋势,包括平台即服务(PaaS)、软件即服务(SaaS)和函数即服务(FaaS),有时与无服务器(serverless)术语混淆。

在这里我们调查最相关的是所谓的数据库即服务(DBaaS)的托管数据基础设施提供的出现。该类别包括以下内容:

  • 传统数据库作为托管云服务提供,例如亚马逊关系型数据库服务(RDS)和 PlanetScale。

  • 仅作为云服务提供的云数据库,如 Google BigTable、Amazon Dynamo 和 Snowflake。

  • 可在开源或源代码可用许可下也可以在本地运行的托管 NoSQL 或 NewSQL 数据库,例如 MongoDB Atlas、DataStax Astra DB、TiDB 和 Cockroach DB。

在过去的几年中,许多提供 DBaaS 服务的供应商已经开始迁移到 Kubernetes,以自动化操作,更有效地管理计算资源,并使他们的解决方案在不同云之间可移植。DataStax 是几家开始提供 Cassandra 服务的供应商之一。这些供应商通常使用基于在云环境中运行传统 Cassandra 集群的架构,使用各种“胶水代码”来集成网络、监控和管理等方面,这些并不完全适合目标部署环境,如 Kubernetes 和公共云 IaaS。这些技术包括使用 Sidecar 收集指标和日志,或使用 StatefulSets 部署 Cassandra 节点以有序地进行扩展和缩减。

即使使用这些在 Kubernetes 中运行的变通方法,Cassandra 的单体架构并不容易促进计算和存储的分离,这可能在扩展时造成一些尴尬。通过添加额外的节点来扩展 Cassandra 集群,每个节点都具有以下功能:

协调

接收读取和写入请求,并根据需要转发到其他节点以达到所需的副本数量(也称为一致性级别

写入和读取

将数据写入内存缓存(memtables)和持久存储(SSTables),并根据需要读取回来

压缩和修复

由于 Cassandra 是 LSM 树数据库,一旦数据写入持久存储,就不会更新数据文件。压缩和修复是作为独立线程后台运行的任务。压缩通过合并不同时间写入的 SSTables 来帮助 Cassandra 保持高性能,忽略过时和已删除的值。修复是跨节点比较存储值以确保一致性的过程。

Cassandra 集群中的每个节点都实现了所有这些功能,并消耗等效的计算和存储资源。这使得独立扩展计算和存储变得困难,并可能导致集群在计算或存储资源上过度配置的情况。

在 2021 年,DataStax 发表了一篇名为“DataStax Astra DB: 设计为无服务器云原生数据库即服务的数据库”的论文,描述了一种不同的方法。Astra DB是 Cassandra 的一个版本,经过重构为微服务,以实现更精细的可扩展性,并利用 Kubernetes 的优势。事实上,Astra DB 不仅仅是 Kubernetes 原生的;它本质上是一种仅适用于 Kubernetes 的数据库。图 7-4 展示了 Astra DB 架构的高层次视图,分为控制平面、数据平面和支持基础设施。

Astra DB 架构

图 7-4. Astra DB 架构

让我们快速概述一下这个架构中的各层:

Astra DB 的控制平面

控制平面负责在各种云提供商区域中提供 Kubernetes 集群。它还在这些 Kubernetes 集群中配置 Astra DB 集群,并提供 API,允许客户端通过 Astra DB Web 应用程序或通过 DevOps API 编程方式创建和管理数据库。Jim Dickinson 的博文 “我们如何构建 DataStax Astra DB 控制平面” 描述了控制平面的架构以及如何迁移到 Kubernetes 原生。

Astra DB 数据平面

数据平面是实际运行 Astra DB 数据库的地方。数据平面由多个微服务组成,这些微服务共同提供了原本作为单个单片式 Cassandra 节点一部分的功能。每个数据库都部署在专用的 Kubernetes Namespace 中的 Kubernetes 集群中,并且可以跨多个租户共享,稍后会详细描述。

Astra DB 基础设施

每个 Kubernetes 集群还包含一组基础设施组件,这些组件在该集群中的所有 Astra DB 数据库之间共享,包括 etcd、Prometheus 和 Grafana。etcd 用于存储元数据,包括将租户分配给数据库和每个租户的数据库架构。它还存储有关集群拓扑的信息,取代了传统 Cassandra 架构中的 gossip 角色。Prometheus 和 Grafana 的部署方式与本书中其他架构中描述的方式类似。

现在让我们深入了解数据平面中的几个微服务:

Astra DB Operator

Astra DB Operator 通过管理每个数据库实例所需的 Kubernetes 资源(如 DBInstallation 自定义资源所描述的),如 图 7-5 所示。与我们在 “使用 Cass Operator 在 Kubernetes 中管理 Cassandra” 中讨论的 Cass Operator 项目类似,Astra DB Operator 自动化了许多与管理 Cassandra 集群相关的运维任务,这些任务通常由使用 nodetool 的运维人员执行。

Coordination Service

协调服务负责处理应用程序查询,包括读取、写入和模式管理。每个协调服务都是 Stargate 的一个实例(如 “使用 Stargate API 提升开发者生产力” 中讨论的),它公开了用于 CQL 和其他 API 的端点,具有专门的 Astra DB 插件,使其能够智能地将请求路由到 Data Service 实例以实际存储和检索数据。将这种计算密集型的路由功能分解为自己的微服务,可以根据查询流量的增减而独立地进行扩展。

数据服务

每个 Data Service 实例负责根据其在 Cassandra 令牌环中的位置管理每个分配租户的数据子集。Data Service 采用分层的数据存储方法,维护内存数据结构(如 memtables),使用本地磁盘进行缓存、提交日志和索引,并使用对象存储来长期保存 SSTable。对象存储的使用是 Astra DB 与迄今为止我们研究过的其他数据库的主要区别之一,我们将在本节中进一步探讨这种方法的其他好处。

压缩服务

Compaction Service 负责在对象存储中的 SSTable 上执行包括压缩和修复在内的维护任务。压缩和修复是计算密集型任务,历来由经验丰富的 Cassandra 操作员安排在非高峰时段以限制其对集群性能的影响。在 Astra DB 中,这些任务可以在任何需要时执行,而不会影响查询性能。工作由一个可以独立扩展的 Compaction Service 实例池处理,生成修复的、压缩的 SSTable,这些 SSTable 可以立即被 Data Services 访问。

IAM 服务

所有传入的应用程序请求都通过身份和访问管理(IAM)服务路由,该服务使用控制平面中定义的标准角色和权限集。尽管 Cassandra 长期以来一直具有可插拔的身份验证和授权架构,但将其分解为其自己的微服务可以提供更大的灵活性,并支持诸如 Okta 之类的其他提供者。

数据平面包括了在图 7-4 中出于简化而省略的其他服务,包括用于恢复失败的 Data Service 实例的 Commitlog Replayer Service,以及使用分析和机器学习建议操作员何时调整每个服务实例数量的 Autoscaling Service。

图 7-5 展示了在 Kubernetes 资源层面上典型的 DBInstallation 的样子。让我们逐个展示一些典型的交互,重点关注关键服务的各个实例,以演示每个资源如何发挥其作用。

每个集群配置了一个 Kubernetes Ingress 来管理来自客户应用程序的传入请求(1),并通过使用 Kubernetes Service(2)将请求路由到按租户使用的协调器服务。

Astra DB 在 Kubernetes 中的集群

图 7-5. Astra DB 在 Kubernetes 中的集群

协调器服务是由一个 Deployment 管理的无状态服务(3),它在每次调用时将身份验证和授权检查委托给 IAM 服务(4)。

授权请求然后根据租户路由到一个或多个 Data Services,再次使用 Kubernetes Service(5)。

数据服务使用 StatefulSets(6)进行管理,用于为每个实例分配本地 PersistentVolume,用于管理诸如提交日志等中间数据文件,该文件在写入时立即填充。在可能的情况下,从内存数据结构直接提供读取。

与 Cassandra 和其他 LSM 树存储引擎一样,数据服务偶尔会将 SSTable 文件写入持久存储(7)。对于 Astra DB 来说,该持久存储是由云提供商管理的外部对象存储,以实现高可用性。为了确保数据隐私,每个租户使用单独的对象存储桶。

补偿服务可以异步地在对象存储中对 SSTable 执行压缩和修复(8),不会对写入和读取查询产生影响。

Astra DB 还支持多区域数据库集群,其定义跨越多个 Kubernetes 集群。协调器和数据服务部署在数据中心(云区域)和机架(可用区),使用与 K8ssandra 中描述的类似方法,如在“在 Kubernetes 中部署多集群应用程序”中所述。

Astra DB 的微服务架构使其能够更优化地利用计算和存储资源,并隔离计算密集型操作,从而节省云中操作 Cassandra 集群的总体成本。通过添加允许每个集群跨多个租户共享的多租户功能,这些成本节省得以延伸。Astra DB 白皮书描述了一种称为shuffle sharding的技术,该技术用于将每个租户与可用的协调器和数据服务子集匹配,有效地为每个租户创建一个单独的 Cassandra 令牌环。随着 Astra DB 实例中租户的增加,可以轻松更新此拓扑以重新平衡负载而无需停机,并且可以配置较大的租户使用他们自己的专用数据库(DBInstallations)。此方法最小化成本同时满足性能和可用性 SLA。

在本节中,我们专注于 Astra DB 用于提供多租户、无服务器 Cassandra 的架构,该架构融合了云原生和 Kubernetes 原生原则,使用完全不同的部署风格。这延续了亚马逊 Dynamo 和谷歌 BigTable 论文的传统,在公开讨论新型数据库架构方面产生了广泛的讨论。此外,本书中提到的几个开源项目,包括 Cass Operator、K8ssandra 和 Stargate,都源于 Astra DB。在核心数据库、控制平面、变更数据捕获、流集成、数据迁移等领域,正在进行大量创新,因此请期待未来该团队更多的开源贡献和架构提案。

在 Kubernetes 原生数据库中需要关注的内容

在过去几章中所学的关于在 Kubernetes 上部署和管理各种数据库所需的一切之后,我们处于一个很好的位置来定义您在 Kubernetes 原生数据库中应寻找的内容。

基本要求

遵循我们的云原生数据原则,以下是应考虑的一些基本要求:

充分利用 Kubernetes API

数据库应尽可能与 Kubernetes API 紧密集成(例如,使用持久卷用于本地和远程存储,使用服务进行路由而不是维护其他节点 IP 列表等)。应使用第五章中描述的 Kubernetes 扩展点来补充内置的 Kubernetes 功能。

在某些领域,现有的 Kubernetes API 可能无法提供给定数据库或其他应用程序所需的确切行为,正如 Vitess 和 TiDB 项目创建备用 StatefulSet 实现所示。在这些情况下,应尽一切努力将改进捐赠回 Kubernetes 项目。

通过操作员进行自动化的声明式管理

应使用操作员和自定义资源在 Kubernetes 上部署和管理数据库。操作员应作为管理数据库的主要控制平面元素。虽然拥有允许 DBA 手动干预以优化数据库性能和解决问题的命令行工具或 kubectl 扩展可能是有帮助的,但这些功能最终应由操作员执行,因为它实现了第五章中讨论的更高成熟度水平。

目标应该是通过更新自定义资源中的所需状态并让操作员处理其余部分来完成对数据库的所有必需更改。当我们能够根据诸如延迟、吞吐量、可用性和单位成本等服务级目标配置数据库时,我们将处于一个很好的位置。操作员可以确定需要多少个数据库节点,使用什么计算和存储层级,何时执行备份等。

可通过标准 API 进行观察

我们开始看到关于 Kubernetes 上数据基础设施的可观察性的共同期望,涉及度量、日志和跟踪的熟悉三元组。Prometheus-Grafana 堆栈在度量收集和可视化方面已成为事实上的标准,数据库服务使用 Prometheus 格式暴露指标作为最低标准。提供 Prometheus 集成的项目应具有足够的灵活性,可以提供其自己的专用堆栈,或将指标推送到与其他应用程序共享的现有安装。

所有数据库应用程序容器的日志应通过侧车(如果必要)推送到标准输出(stdout),以便日志聚合服务收集。虽然对于追踪可能需要更长时间来被广泛采用,但通过 OpenTracing 等 API 追踪单个客户端请求通过应用程序调用到数据库层将是未来云原生应用程序极为强大的调试工具。

默认安全

Kubernetes 项目本身提供了一个很好的例子,说明了默认安全的含义——例如,只有在特别启用时才公开 Pods 和容器的端口访问,并提供像 Secrets 这样的原语,我们可以用来保护登录凭据或敏感配置数据的访问。

数据库和其他基础设施需要利用这些工具,并采用零信任的行业标准和最佳实践(包括更改默认管理员凭据),限制应用程序和管理 API 的暴露。暴露的 API 应优先使用诸如 HTTPS 之类的加密协议。存储在 PersistentVolumes 中的数据应该进行加密,无论是应用程序、数据库还是 StorageClass 提供者执行加密。审计日志应作为应用程序日志的一部分提供,特别是涉及配置用户访问的操作。

总结来说,Kubernetes 原生数据库与 Kubernetes 的工作方式相符。它最大化地重用了 Kubernetes 内置的能力,而不是带来自己的重复的支持基础设施。因此,使用 Kubernetes 原生数据库的体验非常类似于使用 Kubernetes 本身。

Kubernetes 原生的未来

随着对于什么是 Kubernetes 原生的基本要求和更高级期望的巩固,接下来会发生什么?我们开始看到在部署 Kubernetes 上的数据库项目中出现的共同模式,这可能指向未来的发展方向。尽管这些模式有些模糊,但让我们试着把其中一些聚焦起来。

多维度架构下的可伸缩性

你可能已经注意到在过去几章中多次重复出现的一些术语,比如多集群多租户微服务无服务器。这些术语的一个共同主题是它们代表了可伸缩性的架构方法,正如图 7-6 所示。

多维扩展架构方法

图 7-6. 多维度扩展架构方法

考虑每种方法如何为可伸缩性提供独立的轴线。在图 7-6 中的可视化展示了你的应用程序的影响,作为一个随着每个轴向扩展而增长的三维表面:

微服务架构

微服务架构将数据库的各种功能分解为独立可扩展的服务。无服务器方法基于此构建,鼓励将持久状态隔离到尽可能少的有状态服务或甚至外部服务。Kubernetes 持久卷子系统中的存储 API 使得可以利用本地和网络存储选项。这些趋势允许真正地分离计算和存储,并独立地扩展这些资源。

多集群

多集群指的是跨多个 Kubernetes 集群扩展应用程序的能力。与相关术语如多区域多数据中心多云一起,这意味着在可能是异构环境的多个地理位置扩展提供的功能。这种能力的分布对于在最小延迟、云提供商成本优化和灾难恢复方面满足用户需求具有积极的影响。正如我们在第六章中讨论的那样,Kubernetes 在跨集群网络和服务发现的支持上历史上并不是特别强大。值得关注的是,数据库和其他应用程序如何利用预期中的 Kubernetes 联合进步。

多租户

这是在多个用户之间共享基础设施的能力,以实现资源的最有效使用。正如公共云提供商在其 IaaS 提供中所展示的,多租户方法可以非常有效地为创新性新项目提供低成本、低风险的基础设施访问,并在这些应用程序增长时提供额外资源。在数据基础设施中采用多租户方法也具有很大的潜在价值,前提是安全保证得到适当满足,并且在它们成为“吵闹的邻居”之前,有一个无缝的过渡路径转向专用基础设施。在目前阶段,Kubernetes 并未提供明确的多租户支持,尽管命名空间可以是为特定用户提供专用资源的有用工具。

尽管你建立的应用程序或数据基础设施可能不立即需要这三个扩展性轴的全部,但考虑到在每个方面的增长如何可以增强你所提供的整体价值。

通过开源和云服务的社区重点创新

在我们的叙述中,您可能已经注意到的另一个模式是开源数据库项目和 DBaaS 产品之间的持续创新循环。PingCAP 采用了开源的 MySQL 和 ClickHouse 数据库,并利用 Kubernetes 创建了一个数据库服务,帮助其在规模上管理数据库,然后发布了包括 TiDB 和 TiFlash 在内的开源项目。DataStax 采用了开源的 Cassandra,将其分解为微服务,添加了 API 层,并在 Kubernetes 上部署了其 Astra DB,并创建了多个开源项目,包括 Cass Operator、K8ssandra 和 Stargate。这些公司在 Dynamo、BigTable、Calvin 等论文的精神中也开源了架构。

这种创新循环反映了更大的 Kubernetes 社区的情况,其中主要的云服务提供商和存储供应商帮助推动了核心 Kubernetes 控制平面和 PersistentVolume 子系统的成熟发展。有趣的是观察到,围绕云服务的创新循环中具有最高的动力和最快的周期,而不是围绕传统的开源项目企业版本的经典开放核心模型。

作为软件供应商,提供云服务使您能够更快地迭代和评估新架构和功能。将这些创新反馈到开源项目中,使您能够通过支持灵活的消费模型来增加采用率。对于客户来说,“自己运行”和“租用我们的服务”都成为了合法的部署选项,可以根据不同的使用案例灵活选择。客户可以对您的技术的整体成熟度和安全性充满信心,因为他们可以检查和贡献的开源版本与您在 DBaaS 中运行的基本相同。

这些创新趋势的最终副作用之一是对经过验证的架构和组件的隐性推动。考虑以下例子:

  • etcd 被用作我们在本书中多个项目中(包括 Vitess 和 Astra DB)的元数据存储。

  • TiDB 利用了 F1 的架构,实现了 Raft 一致性协议,并扩展了 ClickHouse 的列存储。

  • Astra DB 利用了 PersistentVolume 子系统和符合 S3 标准的对象存储。

而不是发明新技术来解决诸如元数据管理和分布式事务等问题,这些项目正在将创新投入到新功能、开发者体验和我们在本章中探讨过的可扩展性轴上。

摘要

在本章中,我们深入探讨了 TiDB 和 Astra DB,以找出它们成为 Kubernetes 本地化的特质。这项工作的目的是什么?我们希望这种分析能够提供更深入的理解,帮助消费者提出更具洞察力的关于所使用数据基础设施的问题,并帮助那些构建数据基础设施和生态系统的人创建能够满足这些期望的技术。我们相信,不仅是云原生而且是 Kubernetes 本地化的数据基础设施将为每个人带来最佳的性能、可用性和成本效益的结果。

第八章:在 Kubernetes 上流式数据

当您考虑数据基础设施时,对于许多人来说,持久性是首先想到的事情——存储运行应用程序的状态。因此,到目前为止,我们的重点一直放在数据库和存储上。现在是时候考虑云原生数据堆栈的其他方面了。

对于那些管理数据管道的人来说,流媒体可能是您的起点,您的数据基础设施的其他部分则是次要关注点。无论起始点在哪里,数据移动都是整体数据堆栈的一个极其重要的部分。在本章中,我们将探讨如何在 Kubernetes 中使用流媒体技术来在您的云原生应用程序中安全可靠地共享数据。

流媒体简介

在第一章中,我们将流媒体定义为从一点移动数据到另一点,并且在某些情况下,在传输过程中处理数据的功能。流媒体的历史几乎与持久性的历史一样悠久。随着数据聚集在各种孤立存储中,显然将数据可靠地移动与可靠地存储数据同样重要。在那些日子里,这被称为消息传递。数据被缓慢但有序地传输,类似于邮政信件。消息基础设施将数据放置在一个地方,使其可以异步按顺序读取,并提供交付保证。在使用多台计算机时,这满足了一个关键需求,并成为分布式计算的基础之一。

现代应用程序需求已经从过去所谓的消息传递进化为今天的流媒体定义。通常,这意味着管理大量需要更快处理的数据,我们称之为准实时。在 Kubernetes 部署的分布式应用中,顺序和交付保证成为一个至关重要的特性,并且在许多情况下是必需的规模增长的关键推动因素。如何通过增加更多基础架构复杂性来帮助扩展?通过提供一种有序方式来管理数据的流动,从数据的创建到其可以使用和存储的地方。流通常不用作真实性的来源,但更重要的是,它们被用作真实性的导管

对于初次使用者来说,围绕流媒体的软件和术语可能会令人困惑。与任何复杂话题一样,分解各部分有助于我们建立理解。在选择适合您使用情况的流媒体系统时,有三个评估区域:

  • 交付类型

  • 交付保证

  • 流媒体的特性范围

让我们更详细地看看这些领域的每一个。

交付类型

要在您的应用程序中使用流处理,您需要了解从流处理系统的长列表中可用的传递方法。您需要根据应用程序的要求有效地规划从生产者到消费者的数据流动方式。例如,“我的消费者是否需要独占访问?”答案将决定哪种系统符合要求。图 8-1 展示了流处理系统中两种最常见的选择:点对点和发布/订阅:

点对点

在这个数据流中,由生产者创建的数据经过经纪人传递给单个消费者,实现一对一关系。这主要用作解耦直接生产者到消费者的连接的方法。它作为弹性的优秀特性,因为消费者可以随时添加或移除而不会丢失数据。同时,经纪人维护顺序和最后读取的消息,并且消费者可以通过偏移量进行寻址。

发布/订阅(pub/sub)

在这种传递方法中,经纪人充当单个生产者和一个或多个消费者的分发中心,实现一对多关系。消费者订阅主题,并接收生产者创建的任何新消息的通知——这是反应性或事件驱动架构的关键组成部分。

传递类型

图 8-1. 传递类型

交付保证

结合传递类型,经纪人保证从生产者到消费者每种消息类型的交付保证,这种协议称为合同。典型的传递类型如图 8-2 所示:至多一次、至少一次和仅一次。该图展示了生产者发送消息和消费者接收消息预期之间的重要关系:

至多一次

最低保证用于避免由于分布式系统中可能发生的暂态错误而导致的任何潜在数据重复。例如,生产者可能在发送时超时。然而,消息可能刚刚通过而没有收到确认。在这种灰色区域中,为了避免重复数据,生产者不应尝试重新发送并继续进行是最安全的选择。需要理解的关键缺点是,由于设计原因可能会存在数据丢失的可能性。

At-least-once

这种保证是至多一次的反面。生产者创建的数据保证被消费者接收。新增的方面允许在第一次后任意次数重新传递。例如,这可能与唯一键(如日期戳或 ID 号)一起使用,在消费者端被认为是幂等的,多次处理不会影响它。消费者将始终看到生产者传递的数据,但可能会看到多次。您的应用程序需要考虑到这种可能性。

仅一次

这三种保证中最严格的一种意味着生产者创建的数据只会被送达给消费者一次——例如,在需要精确交易的场景下,如货币转移,需要确保减法或加法仅被传递和处理一次,以避免问题。这种保证让代理更难维护,因此您需要调整为代理分配的资源和您预期的吞吐量。

交付保证

图 8-2. 交付保证

在为每种类型的消息选择交付保证时要小心。交付保证是需要仔细评估的,因为如果不完全理解,可能会对消费者造成意想不到的下游影响。诸如“我的应用程序能处理重复消息吗?”这样的问题需要一个好答案。“可能”是不够好的。

特性范围

许多流媒体技术都可以使用,其中一些已经存在了很多年。从表面上看,这些技术可能看起来相似,但每一种技术都因新的需求而解决不同的问题。其中大多数是开源项目,因此每个项目都有一群志同道合的人加入并推动项目的进展。就像许多不同的持久化数据存储适合于“数据库”的大伞下一样,“数据流”标题下的特性也可能有很大的差异。

特性范围很可能是评估要使用哪种流媒体技术时最重要的选择标准。但是,您还应该挑战自己,将适合 Kubernetes 的适用性作为一个标准,并考虑更复杂的特性是否值得增加的资源成本。幸运的是,第一次做出错误决策的代价相对较低。由于其短暂的特性,流数据系统往往是最容易迁移的。流媒体技术越深入您的功能堆栈,迁移就越困难。流媒体功能范围可以分为 图 8-3 中展示的两大类别:

消息代理

这是最简单的流媒体技术形式,可以通过一个或多个之前列出的传送方法和保证,方便地将数据从一个点移动到另一个点。很容易忽视这个功能的简单外观,但它是现代云原生应用的支柱。就像说联邦快递只是一个包裹送货公司一样,但是想象一下,如果它停止了一天,世界经济会发生什么?示例 OSS 消息代理包括 Apache Kafka、Apache Pulsar、RabbitMQ 和 Apache ActiveMQ。

流分析

在某些情况下,分析数据的最佳或唯一时机是在数据移动时。等待数据持久化然后开始分析可能太晚了,洞察的价值几乎是无用的。考虑欺诈检测。停止欺诈活动的唯一机会是在其发生时;等待报告第二天运行是行不通的。例如开源流分析系统包括 Apache 产品 Spark、Flink、Storm、Kafka Streams 和 Pulsar。

流处理类型

图 8-3. 流处理类型

流处理在 Kubernetes 中的角色

现在我们已经掌握了基本术语,那么流式处理如何适应运行在 Kubernetes 上的云原生应用程序呢?数据库应用程序遵循创建、读取、更新和删除(CRUD)模式。对于开发人员来说,数据库提供了一个数据的单一位置。添加流处理假设数据从一个地方到另一个地方有某种形式的运动。如果用于创建新数据,数据可能是短暂的。一些数据在传输过程中可能会被转换,有些最终可能会被持久化。流处理假设分布式架构,而扩展流处理系统的方法是管理计算、网络和存储的资源分配。这正好落入云原生架构的甜蜜点。在 Kubernetes 中基于流驱动的应用程序中,您需要管理数据在随时间变化的环境中的可靠流动。需要时分配所需资源。

流处理与数据工程

数据工程是一个相对新的并且快速增长的学科,因此我们要确保对其进行定义。这特别适用于数据流处理的实践。数据工程师关注于在复杂环境中高效地移动数据。在这种情况下,两个 T 很重要:传输和转换。数据科学家的角色是从数据中提取含义和洞察力。相比之下,数据工程师正在构建从各种位置收集数据、组织数据,并且在大多数情况下持久化到类似数据湖的管道。数据工程师与应用程序开发人员和数据科学家合作,以确保在数据日益分布化的环境中满足应用程序要求。

你的速度和灵活性的最关键方面是你的工具如何协同工作。当开发人员构想新应用程序时,这个想法能多快转变为生产部署?为一个应用程序部署和管理单独的基础设施(流处理、持久化、微服务)是繁琐且容易出错的。当询问为什么要将流处理集成到您的云原生堆栈中时,您应考虑在技术债务方面未集成整个堆栈的成本。创建自定义的数据移动方式会给应用程序和基础设施团队带来巨大负担。数据流处理工具是为特定目的构建的,具有庞大的用户和供应商社区,有助于您的成功。

对于数据工程师和站点可靠性工程师(SREs),您在 Kubernetes 上的流处理的规划和实施可能会极大地影响您的组织。云原生数据应该能够在提高敏捷性和速度的同时,尽可能地提高效率。作为本书的读者,您已经开始以不同的方式思考您的基础设施。根据杰西·安德森的建议,在您开始学习在 Kubernetes 上处理流数据时,有两个领域应该是您关注的重点:

资源分配

您是否同时规划了高峰和低谷?正如您在 第一章 中所了解的,弹性是云原生数据中更具挑战性的方面之一。在大规模系统中,扩展是一个常见的解决方案,但缩减可能会导致数据丢失,尤其是对于流处理系统。资源的流量需要在它们被下线之前重定向,而它们在本地管理的任何数据都需要在系统的其他部分进行考虑。弹性所涉及的风险是阻止其广泛使用的原因,其结果是大量未使用的容量。致力于资源永不闲置的理念,并建立使用所需且不过多的流处理系统。

灾难恢复规划

高效地移动数据是一个重要的问题需要解决,但同样重要的是如何管理不可避免的失败。如果不理解您的数据流动和耐久性要求,就不能仅仅依赖 Kubernetes 处理恢复。灾难恢复不仅仅是备份数据。如何调度 Pod 以减少物理服务器故障的影响?您可以从地理冗余中受益吗?您清楚数据持久化的位置,并理解这些存储系统的耐久性吗?最后,您有明确的计划在故障后恢复系统吗?在所有情况下,书写流程是第一步,但测试这些流程是成功与失败的分水岭。

我们已经介绍了在 Kubernetes 上进行流处理数据的什么和为什么,现在是时候着眼于如何进行,特别关注云原生部署。我们将快速概述如何在 Kubernetes 上安装这些技术,并强调一些重要的细节来帮助您的规划。您已经在之前的章节中学习了如何使用我们将需要的许多 Kubernetes 资源,因此我们将加快进度。让我们开始第一个云原生流处理技术。

使用 Apache Pulsar 在 Kubernetes 上进行流处理

Apache Pulsar 是一个令人兴奋的项目,适合云原生流媒体应用。在 Kubernetes 和云原生架构之前,流媒体软件大多是在其他时代建立的。Pulsar 最初由雅虎开发,该公司在高规模云原生工作负载方面拥有丰富经验。该项目已捐赠给 Apache 软件基金会,并于 2018 年成为顶级项目。其他项目,如 Apache Kafka 或 RabbitMQ,可能适合您的应用需求,但它们需要更多的规划和良好的运维操作才能达到 Pulsar 的高效水平。根据我们之前涵盖的流媒体定义,Pulsar 支持以下特性:

  • 传输类型:一对一和发布/订阅

  • 传输保证:至少一次、至多一次、仅一次

  • 流媒体功能范围:消息代理、分析(通过函数)

那么,Pulsar 为什么适合 Kubernetes?

我们使用 Kubernetes 创建虚拟数据中心,以有效利用计算、网络和存储。Pulsar 从一开始就设计为将计算和存储资源类型分离,通过网络连接,类似于微服务架构。

这些资源甚至可以跨越多个 Kubernetes 集群或物理数据中心,如图 8-4 所示(参见 Figure 8-4)。部署选项为操作员提供了根据用例和工作负载安装和扩展运行中的 Pulsar 集群的灵活性。Pulsar 还考虑到了多租户,这在大型部署中能够产生显著的效率差异。与为每个应用程序安装单独的 Pulsar 实例不同,许多应用程序(租户)可以使用一个 Pulsar 实例,并设置防止资源争用的保护措施。最后,内置的存储分层创建了随着数据老化而自动选择存储持久性的替代方案,并且可以利用成本较低的存储。

Apache Pulsar 架构

图 8-4. Apache Pulsar 架构

Pulsar 的最高抽象级别是由一个或多个集群组成的实例。我们称本地逻辑管理域为集群,并部署在 Kubernetes 集群中,我们将集中精力关注于此处。各集群可以共享元数据和配置,使生产者和消费者无论位置如何都能看到一个单一的系统。每个集群由几部分共同运作,主要消耗计算或存储资源。

代理(计算)

生产者和消费者通过经纪人传递消息,这是一个无状态的集群组件。这意味着它仅仅是一个计算扩展单元,并且可以根据租户和连接的数量动态分配。经纪人维护一个 HTTP 端点用于客户端通信,在 Kubernetes 部署中为网络流量提供了几个选项。当使用多个集群时,经纪人支持在实例之间进行复制。经纪人可以在仅内存配置下运行,或者在需要消息持久性时使用 Apache BookKeeper(标记为bookies)。

Apache BookKeeper(存储)

BookKeeper 项目提供了管理分布式预写日志的基础设施。在 Pulsar 中,使用的各个实例称为bookies。存储单元称为ledger;每个主题可以有一个或多个 ledger。多个 bookie 实例提供负载均衡和故障保护。它们还提供存储分层功能,允许运营商根据使用情况提供快速和长期存储选项的混合。当经纪人与 bookies 互动时,它们读取和写入主题 ledger,这是一个只追加的数据结构。Bookies 提供账本的单一引用,但在主要接口背后管理复制和负载均衡。在 Kubernetes 环境中,了解数据存储位置对于保持弹性至关重要。

Apache ZooKeeper(计算)

ZooKeeper 是一个独立的项目,在许多分布式系统中用于协调、领导者选举和元数据管理。Pulsar 使用 ZooKeeper 进行服务协调,类似于在 Kubernetes 集群中使用 etcd,存储重要的元数据,如租户、主题和集群配置状态,以便经纪人保持无状态。Bookies 使用 ZooKeeper 进行账本元数据和多个存储节点之间的协调。

代理(网络)

代理是像 Kubernetes 这样的动态环境的解决方案。代替将每个经纪人暴露给 HTTP 流量,代理充当网关并创建到 Pulsar 集群的入口路由。随着经纪人的增加和减少,代理使用服务发现来保持与集群之间的连接流畅。在 Kubernetes 中使用 Pulsar 时,代理服务 IP 应该是应用程序访问运行中 Pulsar 集群的唯一入口。

函数(计算)

由于 Pulsar Functions 独立运行并消耗自己的计算资源,我们选择不在图 8-4 中包含它们。然而,在这种情况下值得一提的是,Pulsar Functions 与消息经纪人协同工作。当部署时,它们从一个主题获取数据,用用户代码修改后返回到另一个主题。添加到 Pulsar 集群的组件是工作者,它按需接受函数运行时。运营商可以将函数作为更大集群的一部分或独立部署,以实现更精细的资源管理。

准备您的环境

在准备进行首次安装时,您需要做出一些选择。由于每个用户都有独特的需求,我们建议您在阅读本节之前查看官方文档,以获取有关在 Kubernetes 中安装 Pulsar 的最完整和最新信息。本节中的示例将更详细地查看可用的选择及其与不同云原生应用用例的关系,以帮助您做出决策。

首先,创建 Pulsar Helm chart 仓库的本地克隆目录:

git clone https://github.com/apache/pulsar-helm-chart

Pulsar 的这个子项目有很好的文档,包含几个有用的示例可供参考。在使用 Helm 部署 Pulsar 时,您将需要一个包含所有定制部署选项的 values.yaml 文件。您可以包含尽可能多的参数来进行更改。Pulsar Helm chart 具有一组默认值,适用于可能适合您的典型集群,但您需要调整值以适应您的特定环境。examples 目录包含各种部署场景。如果您选择像 values-local-cluster.yaml 文件中描述的默认安装,您将获得一组资源,如 图 8-5 所示。正如您所见,安装将代理和经纪人封装在 Deployments 中,并为应用程序提供统一的服务端点。

亲和性是 Kubernetes 中的一种机制,用于创建规则,指定哪些 Pods 可以和不能够共同驻留在同一物理节点上(如有需要,请参考第四章中更详细的讨论)。Pulsar 作为一个分布式系统,对于最大的弹性部署有要求。例如,经纪人。当部署多个经纪人时,每个 Pod 应在不同的物理节点上运行,以防故障。如果所有经纪人 Pod 都分组在同一个节点上并且该节点崩溃,Pulsar 集群将不可用。Kubernetes 仍将恢复运行时状态并重新启动 Pods。但在它们恢复上线时会有停机时间。

在 Kubernetes 上进行简单的 Pulsar 安装

图 8-5. 在 Kubernetes 上进行简单的 Pulsar 安装

最简单的方法是不允许相同类型的 Pods 分组到同一节点上。启用反亲和性将阻止这种情况发生。如果您正在运行单节点系统(例如台式机),禁用它将允许您的集群在无关性的基础上启动:

affinity:
  anti_affinity: true

精细控制 Pulsar 组件副本计数允许您根据使用情况定制部署。每个副本 Pod 消耗资源,应在应用程序的生命周期中加以考虑。例如,从较少的经纪人和 BookKeeper Pods 开始可以管理某些级别的流量。但随着流量增加,可以通过 Helm 添加更多副本并更新配置:

zookeeper:
  replicaCount: 1

bookkeeper:
  replicaCount: 1

broker:
  replicaCount: 1

proxy:
  replicaCount: 1

您现在已经基本了解如何可靠地在应用程序和 Kubernetes 集群内外之间传输数据。Pulsar 非常适合云原生应用程序部署,因为它可以独立扩展计算和存储。部署的声明性质使数据工程师和 SRE 能够以一致的方式轻松部署。现在我们已经具备了数据通信的手段,让我们通过正确类型的网络安全措施进一步进行。

默认情况下使用 cert-manager 保护通信

在产品开发结束时我们面临的一个不幸的现实是剩下来要完成的事情:安全还是文档。不幸的是,Kubernetes 在建立文档方面没有太多的内容,但在安全方面,早期开始并且没有妥协是有很大进展的!

正如您所见,安装 Pulsar 创建了大量基础设施和元素之间的通信。高流量是一个典型的情况。当我们在 Kubernetes 中建立虚拟数据中心时,将产生大量节点内和外部网络流量。所有流量都应使用传输层安全性(TLS)和安全套接字层(SSL)进行加密,使用X.509 证书。该系统的最重要部分是证书颁发机构(CA)。在公钥基础设施(PKI)安排中,CA 充当一个受信任的第三方,用于数字签名用于创建两个实体之间信任链的证书。通过由 CA 发行证书的过程历史上是一个手动和费时的过程,不幸的是,这导致了云应用程序中安全通信的缺失。

cert-manager 是一个工具,使用自动证书管理环境(ACME)协议,无缝地向您的 Kubernetes 基础设施添加证书管理。我们应该始终使用 TLS 来保护从一个服务到另一个服务的数据,尤其是对于我们的流应用程序。cert-manager 项目可能是您 Kubernetes 基础设施中最关键的部分之一,最终您会忘记它。这就是符合“它只是工作”的项目特征。

只需几个配置步骤,就能轻松地向您的 Pulsar 部署添加 TLS。在安装 Pulsar 之前,您需要在目标 Kubernetes 集群中设置 cert-manager 服务。首先,将 cert-manager 仓库添加到本地 Helm 安装中:

helm repo add jetstack https://charts.jetstack.io

什么是 ACME?

当使用 X.509 证书时,您经常会看到有关自动证书管理环境(ACME)的引用。ACME 允许在用户基础设施和证书颁发机构之间自动部署证书。它是由互联网安全研究组织设计的,当时他们正在建立其免费证书颁发机构 Let’s Encrypt。轻描淡写地说,这个出色的免费服务已经成为云原生基础设施的变革者。

安装过程需要一些参数,您应确保使用这些参数。首先是声明一个单独的 Namespace,以保持 cert-manager 在您的虚拟数据中心中的组织井然有序。第二是安装 CRD 资源。这组合允许您创建自动化管理证书的服务:

helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

安装 cert-manager 后,您需要配置证书发行者,以便在需要新证书时调用它。根据您操作的环境,您有很多选择,这些选择在文档中得到了广泛涵盖。在安装 cert-manager 时创建的自定义资源之一是 Issuer。最基本的 Issuerselfsigned-issuer,它可以使用用户提供的私钥创建证书。您可以通过应用以下 YAML 配置来创建一个基本的 Issuer

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: selfsigned-issuer
  namespace: cert-manager
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-cluster-issuer
spec:
  selfSigned: {}

当使用 Helm 安装 Pulsar 时,您可以通过几行 YAML 配置来保护服务之间的通信。您可以通过在定义 Pulsar 集群的 YAML 中将 TLS enabled 设置为 truefalse 来选择要保护的服务。由项目提供的示例非常丰富,为了简洁起见,我们将关注一些关键区域:

tls:
  # settings for generating certs for proxy
  proxy:
    enabled: true
    cert_name: tls-proxy
  # settings for generating certs for broker
  broker:
    enabled: true
    cert_name: tls-broker
  # settings for generating certs for bookies
  bookie:
    enabled: false
    cert_name: tls-bookie
  # settings for generating certs for zookeeper
  zookeeper:
    enabled: false
    cert_name: tls-zookeeper

或者,您可以通过一个命令来保护整个集群:

tls:
  enabled: true

在您的配置文件中,稍后可以使用自签名证书来创建组件之间的 TLS 连接:

# issue selfsigning certs
certs:
  internal_issuer:
    enabled: true
    type: selfsigning

如果您之前参与过保护基础设施通信的工作,您就知道在处理所有步骤并应用 TLS 时的繁重工作。在 Kubernetes 虚拟数据中心中,您再也没有借口将网络通信留在未加密状态下。通过几行配置,一切都可以得到安全和维护。

cert-manager 应该是在新的 Kubernetes 集群中首先安装的东西之一。项目成熟度和简单性的结合使得安全性成为项目中最容易添加的第一项,而不是最后一项。这不仅适用于 Pulsar,还适用于在 Kubernetes 中部署的每个需要网络通信的服务。

使用 Helm 部署 Apache Pulsar

现在我们已经讲解了如何设计 Pulsar 集群以最大化资源利用,您可以使用 Helm 将其部署到 Kubernetes 中。首先,添加 Pulsar Helm 仓库:

helm repo add apache https://pulsar.apache.org/charts

Pulsar 的 Helm 安装的一个特殊要求是准备 Kubernetes。您之前克隆的 Git 存储库具有一个脚本,将通过所有准备工作,例如创建目标命名空间。更复杂的设置是带有关联密钥和令牌的角色。这对于 Pulsar 集群内部服务通信非常重要。从文档中,您可以使用以下示例调用准备脚本:

./scripts/pulsar/prepare_helm_release.sh -n *<k8s-namespace>* -k *<release-name>*

一旦 Kubernetes 集群已经为 Pulsar 准备好,可以运行最终安装。在这一点上,您应该有一个 YAML 配置文件,其中包含您需要的 Pulsar 用例设置,就像我们之前描述的那样。helm install命令将获取该配置文件,并指示 Kubernetes 满足您已指定的期望状态。创建新集群时,请使用initalize=true来在 ZooKeeper 中创建基本元数据配置:

helm install \
    --values *<config yaml file>* \
    --set initialize=true \
    --namespace *<namespace from prepare script>* \
    *<pulsar cluster name>* apache/pulsar

在典型的生产部署中,您应该预计设置时间需要 10 分钟或更长时间。在启动和排序 ZooKeeper、bookies、brokers 和最后是代理之后,需要解决许多依赖项。

使用 Apache Flink 进行流分析

现在,让我们看看在云原生部署中迅速流行的不同类型的流式处理项目:Apache Flink。 Flink 是一个主要设计用于以令人惊叹的规模进行流分析的系统。正如我们在本章开头讨论的那样,流系统有许多不同的类型,这是一个完美的例子。 Flink 有其与其他系统几乎没有重叠的能力;事实上,广泛看到 Pulsar 和 Flink 一起部署以在云原生应用程序中互补彼此的优势。

作为流式处理系统,Flink 提供以下功能:

  • 交付类型:一对一

  • 交付保证:仅一次

  • 流式处理的特性范围:分析

Flink 架构的两个主要组件显示在图 8-6 中——JobManager 和 TaskManager:

JobManager

这是部署任何运行中的 Flink 应用程序代码的控制平面。 JobManager 消耗 CPU 资源,但仅用于维护作业控制;在 JobManager 上不执行实际处理。在高可用性(HA)模式下,这是专门用于在 Kubernetes 上运行的 Flink 的,将会提供多个待命的 JobManager,但仅当主要 JobManager 不再可用时才会空闲。

TaskManager

在运行中的 Flink 作业上完成工作。 JobManger 使用 TaskManagers 满足应用程序中所需的任务链。链是操作的顺序。在某些情况下,这些操作可以并行运行,有些需要按顺序运行。TaskManger 将仅运行一个离散任务并传递它。资源管理可以通过集群中 TaskManagers 的数量和每个 TaskManager 的执行插槽来控制。目前的指导建议是为每个 TaskManager 或插槽分配一个 CPU。

Apache Flink 架构

Flink 项目旨在管理有状态计算,这应该让你立即想到存储需求。在 Flink 中,每个事务都保证强一致性,没有单点故障。这些是在构建 Flink 旨在实现的高度可扩展系统时所需的特性。流处理分为有界和无界两种类型:

无界流处理

这些流处理系统会在数据到达时立即对新数据作出反应——没有可以停止和分析收集数据的终点。每个接收到的数据片段都是独立的。这种情况下的用例可能是在值发生警报或计数时要求精确性。反应式处理可以非常节约资源。

有界流处理

在其他系统中也称为批处理,但在 Flink 中是一种特殊情况。有界窗口可以由时间或特定值标记。在时间窗口的情况下,它们也可以向前滑动,具备在值上进行滚动更新的能力。基于要处理的数据窗口大小,应考虑资源的使用情况。边界大小的限制主要受内存约束。

Flink 的基础理念之一是对运维的强调。在云原生应用所需的规模下,易于使用和部署可能决定是否使用它。这包括在 Kubernetes 中对连续部署工作负载的核心支持,以及在可靠性和可观测性领域与云原生应用的功能平衡:

连续部署

Flink 的核心工作单元称为作业。作业是定义数据读取、分析和输出方式的 Java 或 Scala 程序。作业被串联在一起,并编译成 JAR 文件以创建一个 Flink 应用程序。Flink 提供了一个 Docker 镜像,将应用程序封装成易于在 Kubernetes 上部署的形式,并支持连续部署。

可靠性

Flink 还内置了对保存点的支持,通过在系统更新前后暂停和恢复作业来简化更新。如果处理 Pod 在作业中断时失败,保存点也可以用于快速恢复。与 Kubernetes 的更紧密集成使 Flink 在失败时能够通过恢复 Pod 和重新启动作业来自我修复。

可观测性

集群指标已经仪表化为 Prometheus 格式输出。运维团队可以通过时间详细信息跟踪 Flink 集群内的生命周期事件。应用开发者可以使用Flink 度量系统暴露自定义指标,以进一步实现集成的可观测性。

Flink 提供了一种让数据团队参与整体云原生堆栈的方式,同时为运维人员提供管理整个部署所需的一切。构建微服务的应用程序开发人员可以与构建从应用程序生成的数据流分析的开发人员共享 CI/CD 管道。当堆栈的任何部分发生变化时,可以进行完整的集成测试并作为单个单元部署。团队可以更快地移动,并更有信心,因为没有可能在生产中出现的断开的要求。这种结果是在整个堆栈中采用云原生方法的一个坚实论据,因此现在是时候看看如何做到这一点了。

在将 Flink 集群部署到运行中的 Kubernetes 集群时,有几个要考虑的因素。Flink 项目采用了所谓的“Kubernetes Native”路线,可以在不使用 kubectl 或 Helm 的情况下编程安装所需的 Flink 组件。这些选择可能会在未来发生变化。Flink 生态系统中的边缘项目已经为 Kubernetes 运维人员带来更典型的体验,包括运维人员和 Helm 图表。目前,我们将讨论项目官方认可的方法。

如 图 8-7 所示,运行中的 Flink 集群有两个主要组件,我们将在 Pod 中部署:JobManagerTaskManager。这些是基本单元,但选择部署模式是您用例的关键考虑因素。它们决定了如何利用计算和网络资源。另一个需要注意的是如何在 Kubernetes 上部署。如前所述,没有官方项目运维人员或 Helm 图表。Flink 分发版 包含命令行工具,将根据应用程序的模式部署到运行中的 Kubernetes 集群中。

在 Kubernetes 上部署 Flink

图 8-8 展示了在 Kubernetes 中部署 Flink 集群的可用模式:应用模式和会话模式。Flink 还支持第三种模式称为每作业模式,但这在 Kubernetes 部署中不可用,因此我们只能选择应用模式和会话模式。

选择应用模式或会话模式取决于在 Kubernetes 集群中的资源管理,因此让我们分别看看它们以便做出明智的决定。

应用模式将每个 Flink 应用程序隔离到自己的集群中。作为提醒,一个 Flink 应用程序 JAR 可以由多个任务链式组合而成。集群的启动成本可以通过单个应用程序初始化和作业图表来最小化。一旦部署完成,资源将被用于客户端流量和应用程序中作业的执行。由于只有一个 JobManager,网络流量效率更高,并且客户端流量可以进行多路复用。

Apache Flink 模式

要启动应用程序模式,您需要使用flink命令调用kubernetes-application目标。您将需要通过kubectl访问的运行中的 Kubernetes 集群的名称。要运行的应用程序包含在 Docker 镜像中,并且在命令行中提供了 JAR 文件的路径。一旦启动,将创建 Flink 集群,初始化应用程序代码,然后准备好接受客户端连接:

./bin/flink run-application \
    --target kubernetes-application \
    -Dkubernetes.cluster-id=*<kubernetes cluster name>* \
    -Dkubernetes.container.image=*<custom docker image name>* \
    local:///opt/flink/usrlib/my-flink-job.jar

会话模式通过在特定场景下创建单个 Flink 集群来改变资源管理方式。与运行和消耗资源的多个独立集群不同,您可能会发现使用一个可以根据需要增长和缩小的单个集群更为高效。运维人员的不利之处在于,现在您有一个单一的集群,如果它失败,则会带走数个应用程序。Kubernetes 将重新启动失败的 Pods,但您需要管理资源重新分配的恢复时间。要启动会话模式,请使用kubernetes-session shell 文件,并给出您正在运行的 Kubernetes 集群的名称。默认情况下,该命令会执行并从集群分离。要重新连接或保持与运行中集群的交互模式,请使用execution.attached=true开关:

./bin/kubernetes-session.sh \
    -Dkubernetes.cluster-id=*<kubernetes cluster name>* \
    -Dexecution.attached=true

这只是一个关于广泛话题的快速概述,希望能激发您进一步深入了解。我们推荐的一个资源是使用 Apache Flink 进行流处理,由 Fabian Hueske 和 Vasiliki Kalavri(O’Reilly)编著。将 Flink 添加到您的应用程序中不仅仅是选择一个平台来执行流处理。在云原生应用程序中,我们应该全面考虑我们试图在 Kubernetes 中部署的整个应用程序堆栈。Flink 使用容器,因为封装有助于与其他开发工作流集成。

概要

在本章中,我们从面向持久性数据基础设施扩展到流处理的世界。我们定义了流处理的概念,如何理解所有术语以及它们如何融入 Kubernetes 中。接着,我们深入研究了 Apache Pulsar,并学习了如何根据您的环境和应用程序需求将其部署到您的 Kubernetes 集群中。作为部署流处理的一部分,我们还简要了解了使用 cert-manager 进行默认安全通信的情况,以及如何创建自管理的加密通信。最后,我们查看了 Apache Flink 在 Kubernetes 上的部署,主要用于高规模流分析。

正如您在本章中看到的 Pulsar 和 cert-manager,运行云原生数据基础设施在 Kubernetes 上通常涉及多个组件的组合作为集成堆栈的一部分。我们将在下一章及以后讨论更多这方面的例子。

第九章:Kubernetes 上的数据分析

技术的进步在于我们有能力变得更懒。

拉里安·基里卡博士

在 2000 年代初期,谷歌以公开宣布的目标“组织世界信息,使其普遍可访问和有用”迷住了互联网。这是一个雄心勃勃的目标,要实现它,就像用“计算机科学化”这个词来概括,需要从中提取出位。鉴于数据创建的速度不断增加,谷歌需要发明(和重新发明)管理以前从未考虑过的数据量的方法。围绕分析数据的全新社区、文化和行业应运而生,解决了最终被标记为“大数据”的问题。如今,分析已成为几乎每个应用程序堆栈的全面成员,而不仅仅是谷歌的问题。现在,这是每个人的问题;不再是一种仅限于少数专家的艺术形式,我们都需要知道如何使分析工作。组织需要可靠且快速的方法来部署带有分析功能的应用程序,以便他们可以做更多的事情而花费更少的精力。

基里卡博士在本章开头引用的玩笑话中谈到的懒惰描述了一个理想的未来。与其让一百人的团队日夜工作来分析一百万亿字节的数据,不如将其减少到一个人和几分钟?云原生方式运行数据基础设施是我们所有人应该朝着实现这种光荣懒惰的路径努力。

我们已经看过将有状态工作负载迁移到 Kubernetes 的几个方面,包括存储、数据库和流处理。在本章中,是时候看看分析以完成整个图景了。作为预览,图 9-1 显示了数据分析如何作为使用 Kubernetes 管理完整数据栈路线图的最后一部分。

云原生虚拟数据中心

图 9-1. 云原生虚拟数据中心

在这种架构中,不再需要外部网络要求来连接 Kubernetes 集群内外的资源,只需一个单一的虚拟数据中心,为我们的云原生应用程序服务。大块表示我们在第一章中讨论过的数据基础设施的宏组件,再加上用户应用程序代码,部署为微服务。

分析简介

分析工作负载及其伴随的基础设施操作与其他工作负载大不相同。分析不仅仅是另一个容器化的系统来编排。我们在前几章中研究的典型有状态应用程序(如数据库)具有许多相似的特征,但一旦部署,它们往往保持静态或可预见的缓慢增长。

然而,分析工作负载中的一个方面让许多管理员感到恐惧:数据量。虽然像数据库这样的持久数据存储可以消耗从千兆字节到百万兆字节的存储空间,但分析数据量可能会轻松地飙升到几个拍字节,从而产生一整套新的问题。它们不是毫无缘由地称为“大数据”。

牛津英语词典将分析定义为“数据或统计量的系统计算分析”。维基百科补充道:“它被用于发现、解释和传达数据中的有意义的模式。”结合这些定义与大量的数据,我们应该期待云原生应用的何种结果?让我们详细分析各种类型的分析工作流程和方法:

批量分析

在计算机科学中,批处理是一系列应用于数据的指令,几乎没有用户交互。批处理作业的概念与通用计算同样古老。在像 Apache Hadoop 或 Apache Spark 这样的分布式系统中,每个单独的作业由一个程序组成,可以并行和分阶段地处理较小的数据块。较小的结果最终在作业结束时合并为一个单一的最终结果。MapReduce 就是这方面的一个例子,在本章的后续部分讨论。在大多数情况下,会进行统计分析,如计数、平均值和百分位数测量。批量分析是本章的重点。

流分析

如在第八章中讨论的那样,流分析关注的是正在发生的事情,而批量分析关注的是已经发生的事情。许多相同的 API 和开发方法在流分析和批量分析中都被使用。这可能会让人感到困惑,并使人误以为它们是同一件事情,但实际上它们有着非常不同的用例和实现方式。一个很好的例子是欺诈检测。用于检测和阻止欺诈的时间跨度可以以毫秒到秒计算,这符合流分析的用例。批量分析将用于在较长时间段内发现欺诈模式。

人工智能/机器学习(AI/ML)

虽然人工智能(AI)和机器学习(ML)可以被视为批量分析的子集,但它们是如此专业化的领域,以至于它们值得特别强调。AI 和 ML 经常被一起提到;然而,它们有着两种不同的输出目标。AI 试图在决策制定中模拟人类认知。ML 使用算法从数据池中推导意义,有时的方式并不容易明显。这两种方法都需要在大量数据上应用计算资源。这个主题在第十章中有更详细的讨论。

在 Kubernetes 中部署分析工作负载

Kubernetes 最初关注的是无状态应用程序的扩展和编排。正如你在本书中学到的,Kubernetes 正在演变以支持有状态应用程序。通过将更多工作负载移入虚拟数据中心,提高运行效率的承诺具有很高的激励作用。分析领域可以利用在减少无状态和有状态工作负载操作负担方面取得的进展。然而,Kubernetes 在管理分析工作负载方面面临一些独特的挑战;许多问题仍在解决中。Kubernetes 需要哪些功能来完善数据视图,并使分析工作负载达到与微服务和数据库等堆栈其他部分相当的水平?以下是本章将要探讨的一些关键考虑因素:

有序执行

分析工作负载的一个重要方面是分析大量数据所需的操作顺序。这不仅仅是确保 Pod 以适当的存储和网络资源启动,还包括应用程序与每个 Pod 中有序执行运行的映射。在这项任务中,Kubernetes 组件主要负责的是kube-scheduler(参见第五章),但 Jobs 和 CronJobs 的控制器也参与其中。这是 Kubernetes 社区在关注分析时的一个特定领域,我们将在本章进一步讨论。

存储管理

分析工作负载在处理数据的不同作业中使用临时和持久存储。真正的麻烦出现在识别和选择每个作业所需的正确存储时。许多分析工作负载需要临时存储来进行短期处理,以及更高效(更便宜)的持久存储来进行长期处理。正如你在第二章学到的,Kubernetes 存储已经显著成熟。在 Kubernetes 上运行的分析项目需要利用已经在有状态工作负载上完成的工作,并继续与 Kubernetes 社区合作,以改进 StorageClasses 和不同访问模式等领域的未来增强功能。

资源的高效利用

有句老话说,“大量事物的每一个都重要”,在分析中尤为明显。一项作业可能需要 1,000 个 Pods 运行 10 分钟,但如果需要 10,000 个呢?这对于 Kubernetes 控制平面是一个具有挑战性的问题。另一项作业可能需要数 TB 的交换磁盘空间,而这些空间仅在作业执行期间需要。在云原生世界中,作业应能快速分配所需资源,并在完成后释放这些资源。尽可能提高这些操作的效率不仅节省时间,更重要的是节省资金。分析的快速和爆发性特性给 Kubernetes API 服务器和调度器带来了一些挑战,以保持跟上需要运行的所有作业。如本章后续讨论,其中一些挑战已在处理中,而一些仍在进行中。

这些都是挑战,但没有一项是妨碍我们在 Kubernetes 中部署作为单一虚拟数据中心的完整云原生堆栈的梦想的绊脚石。

工程师们有时是自己最大的敌人。通常情况下,解决一个问题可能会带来更多需要解决的问题。然而,这在处理数据时可以看作是进步。尽管面临诸多挑战,但我们每一步的提升都为以前无法实现的新解决方案铺平了道路。今天,少数人可以完成曾几何时庞大团队无法完成的分析任务,这令人震惊。请参阅本章开头关于懒惰的引言。尽管仍需努力,接下来我们将看看在 Kubernetes 中分析数据的可用工具。

Apache Spark 简介

谷歌通过在学术论文中简要描述,利用 MapReduce 算法改变了数据分析的世界。不久之后,MapReduce 论文引发了工程师们的讨论,随之而来的是一个开源实现:如今著名的 Apache Hadoop。围绕 Hadoop 建立了庞大的生态系统,包括工具和补充项目,如 Hadoop 分布式文件系统(HDFS)。

在这个快速发展的项目中遇到的成长阵痛为基于 Hadoop 的经验教训的下一代工具打开了大门。作为 Hadoop 替代方案,增长迅速的一个项目是Apache Spark。Spark 通过引入弹性分布式数据集(RDD)API 和有向无环图(DAG)解决了可靠性和处理效率问题。

RDD 相比于 MapReduce 的强制线性处理模式是一个重大进步,后者涉及大量的磁盘读取、处理,然后再写回磁盘,反复进行。这使开发人员需要通过推理数据如何被处理来思考。RDD 将责任从开发人员转移开来,作为一个 API 创建了所有数据的统一视图,同时抽象了实际的处理细节。这些细节以工作流的方式创建,执行每个任务的 DAG 来表达。DAG 只是描述数据和操作以有序方式完成最终结果的优化路径。最终,RDD 被 Dataset 和 DataFrame API 取代,进一步提升了在大数据量上的开发人员生产力。

Spark 的操作复杂性大大降低,与 Hadoop 相比,后者因其基础作业所需的基础设施而臭名昭著。Spark 是下一代实现的一个极好例子,具有极为深刻的远见。在简化 Spark 架构方面付出了大量努力,利用分布式系统的概念。结果就是你在 Spark 集群中应该熟悉的三个常见组件,如图 9-2 所示。

Spark 集群的组件

图 9-2. Spark 集群的组件

让我们回顾一下这些组件各自的职责:

集群管理器

集群管理器是 Spark 集群中活动的中心枢纽,新作业在此提交进行处理。集群管理器还获取完成所提交任务所需的资源。集群管理器的不同版本主要基于资源管理方式(独立部署、YARN、Mesos 和 Kubernetes)。在使用 Kubernetes 部署你的 Spark 应用程序时,集群管理器至关重要。

工作节点

当 Spark 作业运行时,集群管理器将其分解为可管理的部分,并交给工作节点进行处理。它们作为硬件资源的本地管理器,是单一联系点。工作节点调用和管理 Spark 执行器。

Spark 执行器

每个发送到工作节点的应用程序将获得自己的 Spark 执行器。每个执行器是一个独立的 JVM 进程,独立运行并与工作节点进行通信。为应用程序的任务分配的计算资源被分解为线程消耗。

这些是 Spark 项目早期设计的传统组件。我们将看到,部署云原生版本的 Spark 的需求迫使一些架构进行了演进。基础原则保持不变,但执行框架已适应利用 Kubernetes 提供的优势并消除编排开销中的重复。在接下来的部分中,我们将看到这些变化以及如何在 Kubernetes 中使用 Spark。

在 Kubernetes 中部署 Apache Spark

自 Apache Spark 2.3 版本起,Kubernetes 是集群管理器中支持的模式之一。这对于作为云原生分析工具的 Spark 意味着什么可能轻易地被低估了。从 Spark 3.1 开始,Kubernetes 模式被认为是可以投入生产的,不断增加稳定的改进。当 Spark 项目考虑在集群编排平台内运行集群分析系统时,许多重叠之处变得显而易见。Kubernetes 已经具备了容器的生命周期管理、计算元素的动态提供和取消提供的机制,因此 Spark 让 Kubernetes 来处理这些工作。多余的部分被移除,Spark 与 Kubernetes 的工作方式更为接近。spark-submit命令行工具通过使用 Kubernetes API 与 Kubernetes 集群进行交互,为开发人员和数据工程师提供了一个熟悉的工具链。这些在 Kubernetes 中部署 Spark 时的独特方面显示在图 9-3 中。

Spark on Kubernetes

图 9-3. Kubernetes 中的 Spark

让我们来突出几个不同之处:

Spark Driver

独立的 Spark 集群的专用集群管理器被原生 Kubernetes 集群管理替代,Spark 特定的管理由 Spark Driver 负责。当 Kubernetes API 服务器从spark-submit工具接收到作业时,将创建 Spark Driver Pod。它调用 Spark Executor Pods 来满足作业的需求。它还负责在作业结束后清理 Executor Pods,使其成为弹性工作负载的重要组成部分。

Spark Executor

就像独立的 Spark 集群一样,Executors 是工作的执行地点,也是消耗最多计算资源的地方。它们由 Spark Driver 调用,接收由spark-submit传递的作业指令,包括 CPU 和内存限制、存储信息以及安全凭证。Executor Pods 中使用的容器是用户预先创建的。

定制 Executor 容器

在使用spark-submit发送作业进行处理之前,用户必须构建一个定制的容器镜像,以满足应用程序的要求。Spark 发行版下载包含一个 Dockerfile,可以根据需要进行定制,并与docker-image-tool.sh脚本一起使用,用于在 Kubernetes 中提交 Spark 作业时构建和上传所需的容器。定制容器包含了在 Kubernetes 环境中运行所需的一切,例如基于所需的 Spark 发行版版本的 Spark Executor。

使用 Kubernetes 和默认设置准备和运行 Spark 作业的工作流可能相对简单,只需几个步骤即可。如果您已经熟悉并在生产环境中运行 Spark,则尤其如此。您需要一个运行中的 Kubernetes 集群,以及在本地文件路径中下载的 Spark 和 Spark 应用程序源代码。

构建您的自定义容器

Executor 容器封装了您的应用程序以及作为执行器 Pod 所需的运行时。构建脚本需要一个参数用于源代码仓库,并在推送到 Docker 注册表时为输出镜像分配标签:

./bin/docker-image-tool.sh -r <repo> -t <tag> build

输出将是一个包含您的应用程序代码的 JAR 文件的 Docker 镜像。然后,您需要将此镜像推送到您的 Docker 注册表中:

./bin/docker-image-tool.sh -r <repo> -t <tag> push

Docker 镜像标签

请注意,标签名称必须正确标记和版本化。在生产中重复使用相同的标签名称可能会导致意外后果,正如我们有些人通过经验学到的那样。

提交并运行您的应用程序

一旦 Docker 镜像推送到仓库,使用 **spark-submit** 启动在 Kubernetes 中运行 Spark 应用程序的过程。这与其他模式中使用的 spark-submit 相同,因此使用许多相同的参数。这对应于图 9-3 中的 (1):

./bin/spark-submit \
    --master k8s://https://<k8s-apiserver-host>:<k8s-apiserver-port> \
    --deploy-mode cluster \
    --name <application-name> \
    --class <fully-qualified-class-name> \
    --conf spark.executor.instances=<instance-number> \
    --conf spark.kubernetes.container.image=<spark-image> \
    local:///path/to/application.jar

这里发生了很多事情,但最重要的在于 --master 参数。为了指示这是为 Kubernetes 准备的,参数中的 URL 必须以 k8s:// 开头,并指向本地 .kubeconfig 文件中指定的默认 Kubernetes 集群中的 API 服务器。*<spark-image>* 是您在 (1) 中创建的 Docker 镜像,应用程序路径指的是镜像中存储的应用程序。

接下来是 (2),其中 spark-submit 与 Kubernetes 集群交互以安排 Spark Driver Pod (3) 和 (4)。Spark Driver 解析作业参数,并与 Kubernetes 调度程序一起设置 Spark Executor Pods (5)、(6) 和 (7) 来运行包含在客户容器镜像中的应用程序代码。应用程序将运行完成,最终使用的 Pod 将被终止,并将资源返回到 Kubernetes 集群中,这个过程称为垃圾收集

这只是 Spark 在 Kubernetes 中的本机工作方式的概述。请参阅官方文档以获取更详细的信息。有许多方法可以自定义参数和参数,以最佳适应您的特定需求。

在 Kubernetes 上运行 Spark 时的安全考虑

当在 Kubernetes 中使用 Spark 时,默认情况下未启用安全性。第一道防线是认证。生产中的 Spark 应用程序应使用 Spark 中的内置认证功能,以确保访问应用程序的用户和进程是您预期的。

创建应用程序容器时,Spark 文档强烈建议将 USER 指令更改为非特权唯一标识符(UID)和组标识符(GID),以减少特权升级攻击的风险。这也可以通过作为 spark-submit 参数提供的 Pod 模板文件中的 SecurityContext 来完成。

也应限制与 Spark Driver 和 Spark Executor 的存储访问。具体来说,应限制可以被运行应用程序访问的路径,以消除在漏洞事件中的任意访问。这些可以在 PodSecurityAdmission 内设置,Spark 文档建议

对于优化您的 Spark 应用程序的安全性,请使用 Kubernetes 提供的安全原语,并根据您的环境自定义默认设置。最好的安全性是您无需考虑的安全性。如果您是 SRE,这是您可以为开发人员和数据工程师做的最好的事情之一。默认安全!

Apache Spark 的 Kubernetes 操作员

如果 Spark 可以通过spark-submit在 Kubernetes 中运行,为什么我们需要一个操作员呢?正如您在前面章节中学到的,Kubernetes 操作员为您管理应用程序提供了更多的灵活性,并且总体上提供了更云原生的体验。使用spark-submit来运行您的 Spark 应用程序需要在生产系统中设置具有本地安装 Spark 及其所有依赖项。Spark on Kubernetes Operator 允许 SRE 和开发人员使用 Kubernetes 工具(如 Helm 和kubectl)以声明方式管理 Spark 应用程序。它还允许更好地观察正在运行的作业并将指标导出到外部系统如 Prometheus。最后,使用操作员提供了一个体验,更接近于在 Kubernetes 中运行其他应用程序。

第一步是使用 Helm 将操作员安装到您的 Kubernetes 集群中:

helm repo add spark-operator \
 https://googlecloudplatform.github.io/spark-on-k8s-operator

helm install my-release spark-operator/spark-operator \
 --namespace spark-operator --create-namespace

完成后,您将有一个运行中的 SparkApplication 控制器,并且会查找 SparkApplication 对象。这是与spark-submit的第一个重大分歧。不再使用长长的命令行参数列表,而是使用 SparkApplication CRD 在 YAML 文件中定义 Spark 作业。让我们看一下来自Spark on Kubernetes Operator 文档的配置文件:

apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
  name: spark-pi
  namespace: default
spec:
  type: Scala
  mode: cluster
  image: "gcr.io/spark-operator/spark:v3.1.1"
  imagePullPolicy: Always
  mainClass: org.apache.spark.examples.SparkPi
  mainApplicationFile: 
    "local:///opt/spark/examples/jars/spark-examples_2.12-3.1.1.jar"
  sparkVersion: "3.1.1"
  restartPolicy:
    type: Never
  volumes:
    - name: "test-volume"
      hostPath:
        path: "/tmp"
        type: Directory
  driver:
    cores: 1
    coreLimit: "1200m"
    memory: "512m"
    labels:
      version: 3.1.1
    serviceAccount: spark
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"
  executor:
    cores: 1
    instances: 1
    memory: "512m"
    labels:
      version: 3.1.1
    volumeMounts:
      - name: "test-volume"
        mountPath: "/tmp"

spec:部分类似于您在spark-submit中传递的应用程序参数,包含有关您的应用程序的详细信息。最重要的是容器镜像的位置。本示例使用了预安装了spark-examples JAR 文件的默认 Spark 容器。您将需要使用docker-image-tool.sh根据应用程序的要求构建镜像,如“构建您的自定义容器”中所述,并根据应用程序的需求修改mainClassmainApplicationFile

spec下的另外两个显著字段是driverexecutor。它们为 Spark Driver Pods 和 Spark Executor Pods 的规范提供了说明,这些由 Spark Operator 部署。对于driver,只需要一个核心,但是 CPU 和内存分配需要足够以维持所需的执行器数量。此数字在executor部分的instances下设置。

注意资源管理

对于资源管理,在 driverspec 下进行的请求需要仔细考虑资源管理。实例的数量以及其分配的 CPU 和内存可能会迅速耗尽资源。作业可能会无限期地挂起,等待资源释放,而这可能永远不会发生。

现在您的配置 YAML 已准备就绪,是时候将其投入实际应用了。有关操作步骤,请参考 图 9-4。

Spark on Kubernetes Operator

图 9-4. Spark on Kubernetes Operator

首先,使用 kubectl apply -f *<filename>*(1)将 SparkApplication 应用到正在运行的 Kubernetes 集群中(2)。Spark Operator 会监听新应用程序(3),当应用新配置对象时,提交运行控制器开始构建所需的 Pod。从这里开始,在 Kubernetes 集群中执行的操作与直接使用 spark-submit 完全相同,但本例中所有参数都通过 SparkApplication YAML 提供。提交运行器启动 Spark Driver Pod(4),然后指导 Spark Executor Pods(5),执行应用程序代码直至完成。包含在 Spark Operator 中的 Pod 监视器将 Spark 指标导出到 Prometheus 等可观察性工具。

Spark Operator 填补了 spark-submit 的工作方式与 SRE 和开发人员通常在 Kubernetes 中部署应用程序的方式之间的差距。这是对本节开头提出的问题的长篇回答。我们需要一个操作员来使使用 Spark 在云原生环境中更加便捷,从而在长期运行中更易管理。云原生的做法包括采用声明式方法来管理资源,并使这些资源可观察。

Kubernetes 的替代调度程序

正如您在 第五章 中所学到的,Kubernetes 调度程序的基本但至关重要的任务是接收资源请求并分配计算、网络和存储以满足要求。让我们看一下执行此操作的默认方法,如 图 9-5 所示。

典型的 Kubernetes 调度

图 9-5. 典型的 Kubernetes 调度

典型的调度工作从创建描述所需资源的 deployment.yaml 文件开始(1),包括需要哪些 Pod 资源以及数量。当提交 YAML 文件(2)到 Kubernetes 集群的 API 服务器时,使用 kubectl apply,Pod 资源将以提供的参数创建,并准备好分配给节点。节点具有所需的资源池,kube-scheduler 的任务是在节点和 Pod 之间进行匹配。调度器在创建新的 Pod 资源时(3)执行状态匹配,并检查该 Pod 是否已分配了节点。如果没有,调度器将进行必要的计算以找到可用的节点。它检查 Pod 的需求,使用内部一组规则对可用节点进行评分,并选择一个节点来运行该 Pod(4)。这就是 Kubernetes 中容器编排的真正工作场所。

然而,我们在分析工作负载方面遇到了问题:默认的 Kubernetes 调度器并未设计用于批处理工作负载。设计过于基础,无法满足分析所需的方式。正如在 “在 Kubernetes 上进行分析是下一个前沿” 中提到的,Kubernetes 是为无状态工作负载的需求而构建的。这些是长时间运行的进程,虽然随着时间的推移可能会扩展或收缩,但倾向于保持相对静态。而像 Spark 这样的分析应用程序则不同,需要调度可能数以千计的短暂作业。

幸运的是,Kubernetes 的开发者预见到未来调度需求的扩展,并允许用户在配置中指定他们的调度器,绕过默认的调度方法。

强烈的愿望通过共同的控制平面管理整个应用程序栈一直是创新的推动力。正如在 “在 Kubernetes 上部署 Apache Spark” 中展示的那样,Spark 正在向 Kubernetes 靠拢。在本节中,我们将看看一些团队如何通过构建更合适的调度器将 Kubernetes 与 Spark 更紧密地结合起来。两个开源项目在这方面处于领先地位:Volcano 和 Apache YuniKorn。这些调度器共享相似的指导原则,通过提供以下替代特性,使它们更适合批处理工作负载:

多租户资源管理

默认的 Kubernetes 调度器根据请求分配 Pod,直到没有更多可用的资源与 Pod 的要求匹配。YuniKorn 和 Volcano 都提供了多种资源模式,以更好地满足您的应用需求,特别是在多租户环境中。资源管理的公平性可以防止一个分析作业因所需资源而饿死其他作业。在调度这些作业时,整个资源池都会考虑到基于优先级和吞吐量的平衡利用。

团队调度 添加了另一层智能。如果提交的作业需要一定数量的资源,如果不能启动每个 Pod,启动作业就没有意义。默认调度器会启动 Pod,直到集群资源耗尽,可能会导致作业等待更多 Pod 上线。团队调度器实施全有或全无的方式,只有当所有需要的资源对于完整的作业都可用时,作业才会启动。

作业队列管理

更智能的队列管理也能导致更好的资源管理。如果一个作业只需要少量资源,并且可以在运行更大作业的同时运行,调度器可以安排这个作业,从而提高 Kubernetes 集群的整体吞吐量。在某些情况下,用户需要控制哪些作业优先级高,并且哪些作业可以在提交时抢占或暂停其他正在运行的作业。作业提交后可以重新排序或重新设置优先级。可观察性工具提供队列洞察,帮助确定整个集群的健康状况和资源使用情况。

如果你考虑将分析工作负载投入生产环境,你应避免使用默认的调度器,kube-scheduler。在这种情况下,它并不适合你的需求。选择一个更好的调度器可以让你为 Kubernetes 的体验未来做好准备。让我们来看看每个调度器的一些亮点。

Apache YuniKorn

YuniKorn 项目 是由 Cloudera 的工程师基于在 Spark 中处理分析工作负载时的操作困难构建而成的。在使用开源解决问题的社区精神下,YuniKorn 于 2020 年被捐赠给 Apache 软件基金会,并作为孵化项目被接受。名称直接来自其支持的两个系统,YARN 和 Kubernetes。(Y 统一 K. YuniKorn. 明白了吧?)它从 Spark 集群管理的角度解决了分析工作负载的特定资源管理和用户控制需求。YuniKorn 还增加了对 TensorFlow 和 Flink 作业的支持,并具有相同水平的资源控制。毫无疑问,这种支持源于在 Spark 中找到的相同操作困难。

使用 Helm 在 Kubernetes 中安装 YuniKorn。YuniKorn 的目标是将你的 Kubernetes 集群转变为一个对批处理作业资源需求友好的地方。这种转变的关键部分是替换默认的 kube-scheduler。为了演示这一点,让我们使用 图 9-6 来逐步了解其组件。

YuniKorn 旨在成为现有 Spark 工作流程最小更改的调度器替代方案的一部分,因此我们将从这里开始。当新的资源请求(1)通过spark-submit(2)发送到 Kubernetes API 服务器时,默认使用kube-scheduler(3)来匹配 Pod 和节点。当在您的集群中部署 YuniKorn 时,将创建一个admissions-controller Pod。admissions-controller的功能是监听新的资源请求(4),并进行小的更改,将schedulerName: yunikorn添加到资源请求中。如果需要更精细的控制,您可以禁用admissions-controller并通过手动将以下行添加到 SparkApplication YAML 来按作业基础启用 YuniKorn:

spec:
schedulerName: yunikorn

YuniKorn 架构

图 9-6. YuniKorn 架构

所有调度需求现在将由 YuniKorn Scheduler(5)处理。YuniKorn 被设计为与多个编排引擎一起运行,并提供一个称为Kubernetes shim的 API 转换层,用于管理 Kubernetes 与 YuniKorn 核心之间的通信(6)。yunikorn-core 通过添加适用于批处理工作负载(如 Spark)的选项,扩展了默认的kube-scheduler中可用的基本过滤器和评分算法。这些选项从简单的基于资源的队列到更高级的分层队列管理,允许队列和资源池映射到组织结构。在运行在单个 Kubernetes 集群中的多租户环境中,分层池对于那些在大型企业的许多部分中具有大量分析足迹的人士非常有帮助,也是至关重要的。

使用queues.yaml文件配置 YuniKorn 核心,该文件包含 YuniKorn 调度 Pod 到节点的所有细节,包括以下内容:

分区

一个或多个命名的配置部分用于不同的应用需求。

队列

在分层排列中对资源进行精细化的控制,以在多租户环境中提供资源保证。

节点排序策略

节点如何通过可用资源选择。选择包括FairnessPolicyBinPackingPolicy

放置规则

描述和过滤器基于用户或组成员资格进行 Pod 位置安置。

限制

用于在分区或队列上设置精细化资源限制的定义

新作业由 YuniKorn 核心处理,通过匹配详细信息并分配正确的队列。在这一点上,调度程序可以决定将 Pod 分配给节点,然后将其上线(7)。

YuniKorn 还配备了一个名为scheduler UI的可观察性基于 Web 的工具,提供作业和队列状态的洞察。它可以用于监控调度器的健康状况,并提供更好的洞察以排除任何作业问题。

火山

Volcano被开发为在 Kubernetes 中运行高性能计算(HPC)工作负载的通用调度器。Volcano 支持多种工作负载,包括 Spark、Flink、PyTorch、TensorFlow 以及专用系统,例如用于基因组测序的 KubeGene。工程师们在华为、腾讯和百度等公司开发了 Volcano,并将其捐赠给 CNCF,于 2020 年被接受为沙盒项目。

使用 Helm 安装 Volcano 并创建作业和队列的 CRDs,使得配置成为 Kubernetes 集群的核心部分,与更多作为旁路的 YuniKorn 不同。这反映了 Volcano 的通用性质。安装后,Volcano 调度器可用于任何需要高级调度和排队的进程。我们将使用图 9-7 来演示其工作原理。

要将 Volcano 用于批处理作业,您需要在作业的 YAML 文件中明确添加调度器配置(1)。如果您正在使用 Volcano 来运行 Spark 作业,Volcano 项目建议使用 Kubernetes 的 Spark Operator,并在您的 SparkApplication YAML 中添加一个字段:

spec:
batchScheduler: "volcano"

Volcano architecture

图 9-7. Volcano 架构

然后,您可以像平常一样使用kubectl apply提交您的作业(2)。如果没有指定 Volcano 调度器,Kubernetes 将使用默认的kube-scheduler来匹配 Pod 和节点(3)。

使用 Helm 安装 Volcano 将安装作业、队列和 PodGroup 的 CRDs,并创建一个名为 Volcano Admission 的新 Pod。Volcano Admission(4)连接到 API 服务器,并验证 Volcano 特定的 CRD 条目和要求使用 Volcano 调度器的作业:

工作

面向 HPC 的特定于 Volcano 的作业,具有扩展的配置

队列

作为先进先出(FIFO)资源组管理的 PodGroup 集合。配置定义了队列在不同情况下的行为。

PodGroup

与其目的相关的 Pod 集合。例如,为 Spark 和 TensorFlow 创建不同的属性的组。

选定 Volcano 作为作业的调度器时(5),Volcano 调度器将获取 CRDs 并开始工作(6)。选择使用 Volcano 作为调度器的传入作业将与 PodGroup 和队列进行匹配。基于此分配,为每个 Pod 进行最终的节点放置(7)。

Volcano 调度器核心的集群特定配置存储在名为volcano-scheduler-configmap的 ConfigMap 中。此配置文件包含两个主要部分:actionspluginsActions是节点选择过程中每个作业步骤的有序列表:enqueue、allocate、preempt、reclaim 和 backfill。每个步骤都是可选的,并且可以根据需要重新排序。

插件是用于将 Pod 与节点匹配的算法。每种算法具有不同的用例和目的,并可以作为整体组合使用:

群组

此插件在队列中查找优先级较高的任务,并在需要时执行抢占和驱逐以释放资源。

BinPack

这是一个经典的算法,用于通过以最有效的方式混合不同大小的资源请求来找到使用每个可用资源的最佳适合。

符合性

忽略 Namespace kube-system 中任何任务以进行驱逐决策。

主导资源公平性(DRF)

这是一种算法,用于解决跨多种资源类型的公平性问题,确保所有作业具有相等的吞吐量。

比例

这是一种多租户算法,为运行作业分配独立的集群分配部分。

任务拓扑

此算法使用亲和性将网络密集型作业放置在物理上更接近以提高网络使用效率。

NodeOrder

此插件在选择之前会使用多个用户定义的维度对每个可用节点进行评分。

谓词

此功能在节点中寻找特定的谓词以进行选择(但目前仅支持 GPU 共享谓词)。

优先级

此插件根据用户提供的 priorityClassNamecreateTimeid 配置选择任务优先级。

服务级别协议(SLA)

此功能使用参数 JobWaitingTime,允许各个作业基于其需求时间控制优先级。

分时复用(TDM)

当节点同时用于 Kubernetes 和 YARN 时,TDM 将安排共享资源的 Pod。

NUMA 感知

提供带有对 CPU 资源拓扑的意识的 Pod 调度。

除了 Kubernetes 安装外,Volcano 还附带了一个名为 vcctl 的命令行工具。管理 Volcano 可完全通过 kubectl 完成。然而,vcctl 为熟悉作业控制系统的操作员提供了一个界面。

从 YuniKorn 和 Volcano 提供的功能列表中可以看出,拥有选择权是一件美好的事情。无论您选择哪个项目,您都将在 Kubernetes 中运行分析工作负载时获得更好的体验。

Kubernetes 的分析引擎

Spark 是一个强大的工具,解决了许多分析用例。然而,一旦该工具不再按您的方式工作,仅有一个选择可能会限制性。Google 在 2004 年开发了 MapReduce 来满足数据转换的需求,例如对数据池进行计数等操作,这在今天依然是一个相关的问题,考虑到我们所创造的数据量。即使在 MapReduce 之前,大规模并行处理(MPP)也是数据分析的一种流行方法。这些“超级计算机”由一排排的个体计算机组成,被呈现为研究人员在物理学和气象学等领域运行大规模计算的单一处理网格。

在分析中处理 ML 和 AI 任务时,会出现类似的计算需求:许多过程需要分析大量数据。诸如 TensorFlow 的库需要超出数据转换的分析工具。借助 Kubernetes,数据科学家和工程师现在可以快速创建基于廉价计算、网络和存储的虚拟数据中心,以匹敌过去某些超级计算机的能力。这些技术的结合为开发人员带来了全新而令人兴奋的未来:基于自助服务使用模型构建基于 ML 和 AI 的应用,无需等待昂贵超级计算机上的时间(是的,这曾经是一件事)。

通过正确的 API 访问和在基于 Kubernetes 构建的正确基础设施上具备的能力,是数据科学和 Python 社区一直努力实现的强大组合。两个新项目已经开始崭露头角:Dask 和 Ray。正如 Dean 所指出的,Python 是数据科学的首选语言。Ray 和 Dask 都为内部和外部 Kubernetes 的大规模并行处理提供了本地 Python 接口。

Dask

Dask 是一个基于 Python 的大规模处理工具,用于抽象复杂的设置步骤。它可以用于任何您可以用 Python 程序表达的事物,但在数据科学领域因其众多可用的库而找到了真正的用武之地。scikit-learn、NumPy、TensorFlow 和 Pandas 都是成熟的数据科学库,可以在笔记本电脑上使用,然后借助 Dask 扩展到大规模计算机集群。

Dask 与 Kubernetes 集成良好,提供了操作员和开发人员已经习惯的便捷用户体验。Dask 的存储原语 Array、DataFrame 和 Bag 映射到许多云原生存储选择。例如,您可以将 DataFrame 映射到存储在 PersistentVolume 或对象存储桶(如 S3)中的文件。您的存储规模仅受基础资源和预算的限制。当 Python 代码处理数据时,Dask 无缝地管理多个工作节点之间的分块。

部署选项包括我们从 第四章 熟悉的手动 Helm 安装示例:

helm repo add dask https://helm.dask.org/
helm repo update
helm install my-dask dask/dask

或者作为另一种选择,您可以在 Kubernetes 中安装一个带有 Jupyter Notebook 实例的 Dask 集群进行内部工作:

helm install my-dask dask/daskhub

一旦您的 Dask 集群在 Kubernetes 中运行起来,您可以作为客户端连接,并使用 HelmCluster 对象在计算节点上运行您的 Python 代码。连接时,请使用安装时给集群命名的名称:

from dask_kubernetes import HelmCluster
from dask.distributed import Client

# Connect to the name of the helm installation
cluster = HelmCluster(release_name="my-dask")

# specify the number of workers(pods) explicitly
cluster.scale(10)

# or dynamically scale based on current workload
cluster.adapt(minimum=1, maximum=100)

# Your Python code here

如果这还不够简单,您可以完全跳过 Helm 安装,让 Dask 为您完成这部分工作。KubeCluster对象接受一个参数,该参数可以使用make_pod_spec方法或指定一个 YAML 配置文件来指定 Pod 配置。它将连接到通过kubectl访问的默认 Kubernetes 集群,并在运行的 Python 程序的一部分中在您的 Kubernetes 集群内调用集群创建:

from dask.distributed import Client
from dask_kubernetes import KubeCluster, make_pod_spec

pod_spec = make_pod_spec(image='daskdev/dask:latest',
                         memory_limit='4G', memory_request='4G',
                         cpu_limit=1, cpu_request=1)

cluster = KubeCluster(pod_spec)

# specify the number of workers(pods) explicitly
cluster.scale(10)

# or dynamically scale based on current workload
cluster.adapt(minimum=1, maximum=100)

# Connect Dask to the cluster
client = Client(cluster)

# Your Python code here

开发者访问 Kubernetes 集群以进行并行计算变得更加简单,而像 Dask 这样的新工具正是它们的吸引力所在。

Ray

在与 Dask 中任意的 Python 代码有显著差异的同时,Ray采用了不同的方法来进行 Python 集群化,通过作为并行任务管理器运行,并包含一个具有大型集成生态系统的分布式计算框架。对于最终用户,Ray 提供了低级别的 C++库,用于运行专门为数据科学中的计算密集工作负载而构建的分布式代码。Ray 核心是基础,它通过任务的概念来分发工作负载。当开发者使用 Ray 编写 Python 代码时,每个任务都表现为一个远程函数,正如从Ray 文档中的这个例子所示:

# By adding the `@ray.remote` decorator, a regular Python function
# becomes a Ray remote function.
@ray.remote
def my_function():
    return 1

在这个基本的例子中,您可以看到 Ray 在分发工作上采取的方法的不同。开发者必须明确指出使用 Ray Core 处理计算管理与集群管理器的工作。

在 Kubernetes 中部署 Ray 旨在利用动态工作负载中的计算和网络资源管理。Ray Operator 包括自定义控制器和 CRD,用于部署连接到 Ray 集群所需的所有内容。提供了一个 Helm 图表以便于安装。但是,由于图表不在公共仓库中提供,因此您必须首先将整个 Ray 分发下载到本地文件系统。可以修改一个广泛的配置 YAML 文件,但是要使一个简单的 Ray 集群工作起来,使用默认设置即可,正如从文档中所见:

cd ray/deploy/charts
helm -n ray install example-cluster --create-namespace ./ray

这导致安装了两种类型的 Pod。头节点处理集群中运行任务的通信和编排,而工作节点处理任务执行其代码的地方。在 Kubernetes 集群内运行 Ray 集群时,有两种方式可以运行 Ray 作业:通过 Ray 客户端进行交互式操作或作为通过kubectl提交的作业。

Ray 客户端嵌入到一个 Python 程序文件中,并初始化与 Ray 集群的连接。这需要通过 Ingress 或本地端口转发来公开头部服务 IP。除了远程函数代码外,初始化程序还将建立与外部化 Ray 集群主机 IP 和端口的连接:

import ray

ray.init("ray://<host>:<port>")

@ray.remote
def my_function():
    return 1

另一个选项是在 Kubernetes 集群内部运行您的代码,并将其附加到内部服务和端口。您可以使用kubectl提交作业以运行,并传递作业描述的 YAML 文件,该文件概述了要使用的 Python 程序和 Pod 资源信息。以下是来自Ray 文档的示例作业文件:

apiVersion: batch/v1
kind: Job
metadata:
  name: ray-test-job
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: ray
          image: rayproject/ray:latest
          imagePullPolicy: Always
          command: [ "/bin/bash", "-c", "--" ]
          args:
            - "wget <URL>/job_example.py &&
              python job_example.py"
          resources:
            requests:
              cpu: 100m
              memory: 512Mi

然后,可以使用kubectl将此文件提交到集群:

kubectl -n ray create -f job-example.yaml

在提交的 Python 文件内部,我们可以使用 Ray 服务头的 DNS 名称,并让 Kubernetes 确保网络路径路由:

ray.init("ray://example-cluster-ray-head:10001")

对于运行 Ray 程序的外部和内部模式,头节点利用 Kubernetes 调度程序来管理 Worker Node Pod 的生命周期以完成提交的作业。Ray 为开发人员提供了一个简单的编程 API,可以利用大规模集群计算而不需要学习 Kubernetes 管理。SRE(站点可靠性工程师)可以创建和管理 Kubernetes 集群,数据科学家可以使用他们偏爱的 Python 编程语言轻松使用这些集群。

摘要

这样就完成了云原生应用程序堆栈中数据组件的导览。通过添加分析功能,可以通过在大数据量中找到洞察力来完善整体数据图片,这些数据可以补充应用程序的其他部分。

分析处于云原生数据创新的前沿,因此,大数据不是你应该假设与其他数据基础设施同样适合于 Kubernetes 的内容。两个主要区别是涉及的数据量和工作负载的突发性质。需要进一步改进,使 Apache Spark 在 Kubernetes 上更有效地运行,特别是在作业管理和存储 API 方面。然而,现有的知识可以帮助您今天放心部署。像 Apache YuniKorn 和 Volcano 这样的项目已经在开源中引领了分析工作负载的更好基础。新兴的分析引擎如 Dask 和 Ray 可能是您用例的更好选择,并且可以与其他工具结合使用。

虽然分析工作负载可能不在您最初的 Kubernetes 部署计划中,但如果您的目标是构建虚拟数据中心的完整图片,特别是设计用来运行您的应用程序的目的,那么它们不能被跳过。

第十章:机器学习和其他新兴用例

在前几章中,我们涵盖了传统数据基础设施,包括数据库、流处理平台和分析引擎,重点放在 Kubernetes 上。现在是时候超越这些,探索正在开始将云原生作为目的地的项目和社区,特别是涉及 AI 和 ML 的项目。

每当多个箭头开始指向同一个方向时,都值得关注。数据基础设施中的方向箭头全部指向 Kubernetes 的整体宏观趋势收敛,由几个相互关联的趋势支持:

  • 为管理计算密集型 AI/ML 工作负载正在出现常见堆栈,包括利用特定硬件如 GPU 的堆栈。

  • 常见的数据格式有助于促进数据在计算、网络和存储资源之间的高效移动。

  • 对象存储正变成数据基础设施的常见持久层。

在本章中,我们将探讨几种体现这些趋势的新兴技术,它们所支持的使用案例,以及它们如何有助于帮助您进一步管理计算、网络和存储等宝贵资源。我们选择了几个涉及机器学习和数据使用不同方面的项目——这绝不是对今天每种使用的技术的详尽调查。我们直接听取每个项目工程师的意见,并提供关于它们如何融入云原生数据堆栈的一些细节。强烈建议您继续深入您感兴趣的领域,超出此处呈现的内容。跟随您的好奇心,并为支持 Kubernetes 中新使用案例的社区做出贡献。

云原生 AI/ML 堆栈

如第九章所讨论的(第九章),在 Kubernetes 上进行的数据分析、人工智能(AI)和机器学习(ML)是一个值得更详细研究的主题。如果你对这个在数据世界中的专业不熟悉,它是一个令人兴奋的领域,增强了我们以规模化方式生成实时数据驱动决策的能力。虽然许多核心算法已存在几十年,但这项工作的性质在过去几年中发生了迅速变化。作为一个职业,数据科学传统上被局限在后台,历史数据的大量获取用于洞察发现意义并预测未来。数据科学家很少直接参与最终用户应用程序,他们的工作与用户界面应用程序是分离的。

随着数据工程师角色的出现,情况开始改变。数据工程师建立处理引擎和管道,以生产化数据科学,并打破学科之间的壁垒。作为数据基础设施新兴领域的典型情况,最大、最有发言权的组织设定了数据工程的节奏,他们的工具和方法已成为主流。

应用程序中数据的实时特性不能仅仅依赖于数据库和流处理平台。由数据科学家构建的产品必须更接近最终用户,以最大化其在应用程序中的效果。许多组织已经意识到这既是一个问题也是一个机会:我们如何使数据科学成为应用部署的另一个近实时组件?忠于自己的形式,在面对挑战时,社区已经应对,并建立了新的项目并创造了新的学科。因此,与传统的持久性、流处理和分析类别并行出现了基于 Kubernetes 的新型数据基础设施类别。这个新堆栈包括支持 AI 和 ML 特定实时数据服务的工具。

AI/ML 定义

如果您是 AI/ML 领域的新手,可能会因术语而感到不知所措。在我们看一些云原生技术解决 AI 堆栈中的问题之前,让我们花些时间了解这个专业领域的新术语和概念。如果您对 AI/ML 熟悉,可以安全地跳到下一节。

首先,让我们简要回顾一些 AI/ML 中常见的术语。这些术语经常出现在项目和功能的描述中,您需要理解它们以选择正确的工具并有效地应用它们:

算法

ML 的基本计算构建块是算法。算法以代码形式表达为一组分析数据的指令。常见的算法包括线性回归、决策树、K 均值和随机森林。数据科学家花费时间与算法一起工作,以从数据中获得洞见。当过程和参数正确时,最终的、可重复的形式输出为模型。

模型

ML 旨在构建模仿人类学习方式的系统,以便根据提供的数据回答问题,而无需明确编程。示例问题包括识别两个对象是否相似,特定事件发生的可能性,或在多个候选项中选择最佳选项。用于这些问题的答案系统描述为数学模型(简称模型)。模型充当函数机器的角色:描述问题的数据输入,输出表示答案的新数据。

特征

特征是与特定用例相关的更大数据集的部分。特征既用于训练模型,也用于在生产中为模型提供输入。例如,如果您想要预测天气,可能会从更大的数据集中选择时间、地点和温度,忽略其他数据如空气质量。特征选择是确定要使用的数据的过程,这可能是一个迭代的过程。当您听到特征这个词时,您可以轻松地将其翻译为数据

训练

模型由一个算法和应用该算法到特定领域的数据(特征)组成。为了训练一个模型,训练数据通过算法以帮助调整输出,以匹配基于所呈现数据的预期答案。这些训练数据包含相同的特征,这些特征将在生产中使用,唯一的区别在于已知期望的答案。例如,给定历史温度,模型中使用的参数是否预测了实际发生的情况?训练是机器学习中资源消耗最大的阶段。

流是工作流程的简称。一个机器学习工作流描述了构建工作模型所需的步骤。流通常包括数据收集、预处理和清理、特征工程、模型训练、验证和性能测试。这些通常是全自动化的过程。

向量

向量的经典数学定义是指示方向和大小的数量。机器学习模型是使用数值数据的数学公式。由于并非所有源数据都表示为数字,将输入数据归一化为向量表示是在流程预处理步骤中使用通用算法的关键。图像和文本是可以在流程的预处理步骤中进行向量编码的数据示例。

预测

预测是使用创建的模型根据输入数据生成可能的答案的步骤。例如,我们可以通过使用天气模型询问给定位置、日期和时间的预期温度。所回答的问题采取的形式是“将会发生什么?”

推断

推断模型通过颠倒输入和输出数据的关系来寻找原因。给定一个答案,是哪些特征导致了这个答案?这里有另一个天气的例子:基于降雨量,最相关的温度和气压是多少?所回答的问题是“这是如何发生的?”

漂移

模型是用来自某一时间点的数据快照训练的。漂移是一个条件,因为模型由于时间变化或不再与原始训练数据相关而失去准确性。当模型发生漂移时,解决方案是通过更新的训练数据来完善模型。

偏见

模型的好坏取决于所使用的算法以及这些算法的训练方式。偏见可以在几个方面引入:在算法本身、包含用户偏见或错误测量的样本数据中,或者数据的排除中。无论如何,机器学习的目标是尽可能准确,而偏见是一种准确性的衡量。检测数据中的偏见是一个复杂的问题,通过良好的数据治理和严格的流程更容易在早期解决。

这些是帮助您理解本节其余内容的一些关键概念。要进行更全面的介绍,请考虑阅读 Andreas C. Müller 和 Sarah Guido 的 Python 机器学习入门(O'Reilly)或您喜欢的在线学习平台上的众多高质量课程之一。

定义 AI/ML 栈

在定义了这些概念之后,我们可以描述云原生 AI 栈的元素及其可以发挥的作用。新兴的学科和社区通过各种团队创新以解决自己的特定需求,实施具有轻微差异的相似实现。通过查看在生产规模上使用 AI/ML 的组织以及围绕 Kubernetes 采用的趋势,我们可以识别一些常见模式。图 10-1 展示了当前在生产中体系结构中发现的一些典型元素。在不指定特定解决方案的情况下,我们将其作为 AI/ML 实时组件类型工具及其如何组合以服务的示例。

云原生 AI/ML 栈的常见元素

图 10-1. 云原生 AI/ML 栈的常见元素

云原生 AI/ML 栈的目标应该是尽可能将 AI/ML 生成的洞见靠近您的用户,这意味着缩短后端分析处理与前端生产系统中使用其输出之间的距离。数据探索使用诸如 scikit-learnPyTorchTensorFlowXGBoost 等库提供的算法,使用存储在数据库或静态文件中的数据。Python 是最常用的具有 ML 库的语言。我们在 第九章 中讨论的系统,包括 Apache Spark、Dask 和 Ray,用于扩展处理所需的 Python 库以构建模型。Kubeflow 和类似工具允许数据工程师创建用于模型生成的 ML 工作流程。工作流程将模型文件输出到对象存储,提供了后端处理与前端生产使用之间的桥梁。

模型的用途在于使用,这是实时模型服务工具如 KServeSeldonBentoML 所扮演的角色。这些工具代表应用程序使用来自对象存储和特征存储(例如 Feast)中现有模型进行预测。特征存储执行特征数据的完整生命周期管理,将新特征数据存储在在线数据库(如 Cassandra)中,进行训练,并将特征提供给模型。

向量相似性搜索引擎是实时服务堆栈的一个新但熟悉的补充。虽然传统搜索引擎(如Apache Solr)为文本搜索提供了便捷的 API,包括模糊匹配,但向量相似性搜索是一种更强大的算法,帮助回答“与我当前拥有的东西相似的是什么?”这一问题。为此,它利用数据中的关系,而不仅仅是搜索查询中的术语。向量相似性支持多种格式,包括文本、视频、音频以及任何可以分析为向量的内容。许多开源工具实现了向量相似性搜索,包括 MilvusWeaviateQdrantValdVearch

让我们详细研究一下支持应用程序前端 ML 使用的几个工具,并了解它们在 Kubernetes 中的部署情况:KServe、Feast 和 Milvus。

使用 KServe 进行实时模型服务

AI/ML 中实时访问分析产品的“最后一英里”问题是 Kubernetes 很可能解决的问题。考虑现代 Web 应用程序的架构:看似简单提供 Web 页面的 HTTP 服务器背后往往有更复杂的逻辑。事实上,应用逻辑和数据基础设施结合在一起,以隐藏复杂性。就像监听请求并提供 Web 页面的 HTTP 服务器一样,模型服务器隐藏了加载和执行模型的复杂性,专注于数据科学完成后的开发者体验。

KServe 是一个原生于 Kubernetes 的模型服务器,可以轻松为生产环境中的应用程序提供预测能力。让我们从项目创始人之一那里更多地了解 KServe 的起源和功能。

图 10-2 展示了 KServe 在 Kubernetes 上的部署方式。控制平面由 KServe 控制器组成,管理称为推理服务的自定义资源。每个推理服务实例包含两个微服务,即 Transformer 服务和 Predictor 服务,每个都包含一个 Deployment 和一个 Service。Knative 框架用于请求处理,将它们视为无服务器微服务,在不使用时可以缩放到零,以实现最大效率。

在 Kubernetes 中部署 KServe

图 10-2. 在 Kubernetes 中部署 KServe

Transformer 服务为来自客户端应用程序的预测请求提供端点。它还实现了预处理、预测和后处理的三阶段流程:

预处理

Transformer服务将传入的数据转换为模型可用的形式。例如,您可能有一个模型来预测图片中是否有热狗。在将图片传递给推理服务之前,Transformer服务将其转换为向量。在预处理过程中,Transformer服务还从诸如 Feast 之类的特征存储加载特征数据。

预测

Transformer服务委托预测工作给Predictor服务,后者负责从对象存储加载模型并使用提供的特征数据执行它。

后处理

Transformer服务接收预测结果并执行任何必要的后处理以准备响应给客户端应用程序。

如果您熟悉传统的 Web 服务,您会看到模型服务创建的有用类比。KServe 不仅提供 HTML 页面服务,还涵盖了为提供 AI/ML 工作负载而生的现代应用需求。作为 Kubernetes 原生项目,它可以无缝集成到您的云原生数据中心和应用堆栈中。

通过 Feast 进行完整的生命周期特征管理

生命周期管理是任何数据架构中的常见主题,涵盖数据随时间的添加、更新和删除。特征存储通过管理从发现到在生产系统中使用的 ML 模型特征的生命周期,起到协调作用,消除不同团队参与时可能出现的版本控制和协调问题。Feast 是如何产生的?

正如 Willem 所指出的,将 Feast 部署到 Kubernetes 的基本状态是成熟的。由于没有定义运算符或自定义资源,您可以使用 Helm 图表安装 Feast。图 10-3 展示了使用Feast 网站上记录的示例,包括特征服务器和其他支持服务的样本安装。

在 Kubernetes 中部署 Feast

图 10-3. 在 Kubernetes 中部署 Feast

让我们来研究这些组件及其如何互动。数据科学家从现有数据源中识别特征,这个过程称为特征工程,并使用由特征服务器公开的接口创建特征(如在“用 Feast 桥接 ML 模型和数据”中定义)。用户可以在创建特征时提供特征数据,也可以连接到各种后端服务,以便数据可以持续更新。Feast 可以消费发布到 Kafka 主题的数据,或通过 Kubernetes 作业从数据仓库拉取数据。特征数据存储在在线数据库(如 Redis 或 Cassandra)中,以便可以轻松地提供给生产应用程序。ZooKeeper 用于协调元数据和服务发现。Helm 图表还支持部署 Grafana 来可视化指标。这可能听起来很熟悉,因为像 Redis、ZooKeeper 和 Grafana 这样的常见构建块的重复使用是我们在本书中几个其他示例中看到的模式。

当像 KServe 这样的模型服务工具被要求进行预测时,它们使用 Feast 中存储的特征作为真实记录。数据科学家通过相同的特征存储进行更新训练,消除了多源数据的需求。Transformation服务提供了一个可选的能力,通过对现有特征数据执行转换来按需生成新特征。

KServe 和 Feast 通常一起使用,以创建完整的实时模型服务堆栈。Feast 处理特征管理的动态部分,与在线和离线数据存储一起工作,随着新特征通过流式处理和数据仓库到达。KServe 使用 Knative 的无服务器能力处理模型服务的动态配置。这意味着当不使用时,KServe 可以自动缩放到零,并在有新请求到达时作出反应,通过仅使用所需资源来节省在基于 Kubernetes 的 AI/ML 堆栈中的宝贵资源。

Milvus 的向量相似性搜索

现在我们已经看过了用于在生产系统中使用 ML 模型和特性的工具,让我们换个角度,看看另一种类型的 AI/ML 工具:向量相似性搜索(VSS)。正如在 “AI/ML 定义” 中讨论的那样,向量是在向量空间中从原点表示方向和大小的数值对象。VSS 是在 ML 中应用向量数学的一种应用。k-最近邻(KNN)算法是一种找出两个相邻物体“接近程度”的方法。此算法有许多变体,但都依赖于将数据表达为向量。要搜索的数据使用 CPU 密集型的 KNN 类型算法进行向量化处理;通常这是一个后端处理过程。VSS 服务器然后可以为向量数据建立索引,以提供 less CPU-intensive 的搜索查询机制,允许最终用户提供一个向量并找到与之接近的内容。

Milvus 是围绕新兴领域 VSS 设计的众多服务器之一。让我们了解一下 Milvus 的由来及其为 Kubernetes 所适配的原因。

正如 Xiaofan 所述,Milvus 支持独立部署和集群部署,使用所描述的四个层次。这两种模式在 Kubernetes 中都通过 Helm 支持,集群部署如 图 10-4 所示。

访问层包含代理服务,该服务使用 Kubernetes LoadBalancer 从客户端应用程序路由请求。协调层中的服务处理传入的搜索和索引查询,并将其路由到工作层中的核心服务器组件,这些组件处理查询并管理数据存储和索引。数据节点通过对象存储中的文件管理持久性。消息存储使用 Apache Pulsar 或 Apache Kafka 存储传入数据流,然后将其传递给数据节点。

可以看出,Milvus 设计为 Kubernetes 原生,具有水平可扩展的架构,非常适合处理包括数十亿甚至数万亿向量在内的大规模数据集。

在 Kubernetes 中部署 Milvus

图 10-4. 在 Kubernetes 中部署 Milvus

使用 Apache Arrow 进行高效数据移动

现在我们已经探索了一个 AI/ML Kubernetes 堆栈,帮助您更有效地管理计算资源,您可能想知道可以如何处理网络资源。我们在“拥抱分布式计算”中讨论的“分布式计算谬论”包括两个重要点:误以为带宽是无限的,以及传输成本为零。即使计算和存储资源看起来更有限,很容易忽视带宽会很容易用完。当您深入部署数据基础设施到 Kubernetes 时,您越可能发现这一点。早期采用 Apache Hadoop 的用户经常分享,随着他们集群的增长,他们的网络交换机需要用当时最好的交换机替换。只需考虑对 10TB 数据进行排序需要多少资源?1PB 呢?您明白了吧。

Apache Arrow 是一个项目,通过提供更高效的格式来解决带宽利用问题。实际上,在计算机科学的历史上并不陌生。IBM 引入了扩展二进制编码的十进制互换码(EBCDIC)字符编码,以提供当时首选的传输方式——打孔卡片的效率。Arrow 从根本上解决了效率问题,避免了不断增加资源的无止境升级,证明了控制问题的解决方案永远不是“增加更多的动力”。让我们听听一些专家的意见,了解这是如何运作的。

使用 Arrow 启用的项目使您能够高效共享数据,减少计算、网络和存储资源的使用。Arrow 与 Spark 的示例用法显示在图 10-5 中。

使用 Apache Arrow 移动数据

图 10-5. 使用 Apache Arrow 移动数据

存储在对象存储中的 Parquet 数据文件,其中包含 Arrow 格式化的数据,可以在不需要反序列化步骤的情况下轻松加载(1)。然后,数据可以通过 Spark 应用程序进行分析(2),包括直接加载到支持的 GPU 进行处理。通过 Arrow Flight(3)在工作节点之间传递数据时,可以保持同样的效率水平。Arrow 记录批量被发送,无需任何中间内存复制或序列化,接收方可以在没有内存复制或反序列化的情况下重建 Arrow 记录。远程进程之间的高效关系消除了两个问题:发送数据的处理开销以及高效的 Arrow 记录格式,消除了带宽的浪费。

在 Spark 应用程序中常见的规模下,网络延迟和带宽的影响可以迅速累积。网络传输节省确保数据流畅移动,即使数据量达到了 TB 和 PB 级别。在 TU Delft 的 Tanveer Ahmad 进行的研究表明,使用 Arrow Flight 移动大容量数据可实现 20 至 30 倍的效率提升。

带 lakeFS 的版本化对象存储

对象存储正成为云原生数据持久化的标准。它降低了服务的复杂性,同时也指向了一种不同的数据可变性思维方式。与随机访问的文件存储不同,对象存储是预计算的,一次写入,多次读取。而不是更新数据文件,你会写入一个新文件,但如何区分哪些数据文件是当前的?因此,对象存储在磁盘空间管理方面存在问题。由于没有管理整个文件系统的概念,每个文件都是一个在虚拟无限资源中的对象。

对象存储 API 相对基础,功能简单,但数据团队需要更多仅仅基础功能以适应他们的使用场景。lakeFSNessie是两个项目,旨在使对象存储更适合 Kubernetes 上新兴工作负载。让我们来看看 lakeFS 如何扩展云原生应用的对象存储功能。

在 Kubernetes 中使用 lakeFS 非常合适,因为它具有无状态设计和声明式部署。Helm 部署包括配置lakeFS 服务,该服务随后作为与其他服务之间的通信网关。

与服务器的通信模拟 S3 对象存储,允许与支持 S3 API 的任何数据存储交互。入站通信绑定为 ClusterIP,用于通过 Deployment 管理的一个或多个无状态 lakeFS 服务器 Pod 提供 HTTP 流量服务。

lakeFS 使用 PostgreSQL 管理元数据,因此用户可以提供正在运行系统的端点,如图 10-6 所示,或者 lakeFS 可以在 lakeFS Pod 内运行一个嵌入式 PostgreSQL 服务器供其独占使用。当部署为集群时,PostgreSQL 是无状态 lakeFS 服务器的状态管理工具。

在 Kubernetes 中部署 lakeFS

图 10-6. 在 Kubernetes 中部署 lakeFS

最重要的连接是到将存储实际数据的对象存储端点。当用户将数据持久化到 lakeFS 时,实际数据文件将通过到后端对象存储,并且版本化元数据存储在 PostgreSQL 中。

额外的出站连接用于与其他 ML 基础设施进行协作。Webhook 允许在发生操作(如提交)时触发,从而警报下游系统。这些触发器是自动化 ML 工作流和其他应用的关键组成部分。

摘要

正如你所看到的,使用 Kubernetes 处理数据的新方法的流水线在未来可延续。新项目正在根据云原生的弹性、可伸缩性和自愈性原则解决先进数据工作负载的挑战。

这些工具赋予你管理计算、网络和存储这些关键资源的能力。你可以通过 KServe 进行计算密集型工作负载的管理,例如 AI/ML 的交付,利用 Feast 进行模型管理,并使用 Milvus 将新的搜索方法实现操作化。网络资源受到容量和速度的简单法则的支配,在我们可以创建的数据量级别上,每一点进展都有助于减少数据量。Apache Arrow 通过在应用程序间创建一个共同的参考框架来降低这一数据量。围绕对象存储的统一提供了进一步的效率,像 lakeFS 这样的工具使对象存储更易于以符合应用程序数据存储需求的方式使用。

到目前为止,我们已经从像存储这样成熟领域一直到管理 AI/ML 人工智能/机器学习构件的前沿项目,审视了在 Kubernetes 上的数据基础设施。现在是时候将迄今为止获得的所有知识应用于实践。

第十一章:迁移数据工作负载到 Kubernetes

在第一章中,我们提出了一个愿景,即将你的云原生应用所需的所有基础设施集成到一个地方:Kubernetes。我们的论点很简单:如果你在 Kubernetes 部署中排除了数据及其支持基础设施,那么你还没有完全接受云原生原则。自那时以来,我们已经涵盖了很多内容,探讨了不同类型的数据基础设施在 Kubernetes 上的工作方式,并展示了可能性的艺术。

那么,你接下来该怎么做呢?如何完全实现这一愿景?此时,你可能已经在 Kubernetes 中部署了应用的某些部分。很可能,你还有几代前的基础设施,比如容器、虚拟机或裸金属服务器,无论是在你自己的数据中心还是在云中运行。在本书的最后一章,我们将利用你迄今所学的一切,帮助你制定一个完整管理云原生数据在 Kubernetes 中的计划。

愿景:应用感知平台

整本书中,我们听到了社区中多样化的声音,他们分享了关于在 Kubernetes 中处理数据的智慧和实际建议,这对于这项重大工作至关重要。无论你处于哪个阶段,无论你是 Kubernetes 的初学者还是经验丰富的多年操作者,我们都可以从他们的专业知识中学到东西。现在是时候放大视角,考虑一下向 Kubernetes 的迁移如何与软件行业的其他趋势交汇。Craig McLuckie 曾是谷歌团队中创建 Kubernetes 的一员,并最终将其引入开源。他在云原生基础设施社区中非常活跃,并分享了一些在我们向 Kubernetes 数据迁移过程中可能遇到的可能性和挑战。

Craig 提出了一个鼓舞人心的愿景,即未来的基础设施将适应应用,而不是像今天一样应用与基础设施耦合。正如你在本书中探索的技术所看到的,通过 Kubernetes 控制平面进行声明性基础设施的协调已经无处不在。现在我们可以开始改变方式,从顶向下构建应用,而不是从底向上。这是一个改变你的组织如何利用数据技术的机会。你准备好开始了吗?现在是时候规划你的旅程了。

绘制通向成功的路径

在准备将你的有状态工作负载迁移到 Kubernetes 之前,你可能心中会有一些问题,比如“我们应该使用什么技术?”和“我们如何推出这些变更?”以及“我们如何确保我们的团队准备好了?”大多数这些问题都可以很好地映射到经典的 IT 框架人员、流程和技术(PPT)。由于每个组织的旅程都会有所不同,我们将在每个类别中提供建议,而不是详细的路线图。你的一部分重要工作是选择迁移到 Kubernetes 的内容和不迁移到的内容。每次迁移都应有充分的理由。

你可能已经在实施一些这些建议,所以实际需要做的工作是确保你在所有三个领域的努力朝着你期望的结果共同发力。一个警告:现在不是“快速行动,破坏一切”的时候。在确保核心元素就位之后,你会有充分的时间来做这些事情。有了坚实的基础,你将达到以前从未见过的敏捷和速度水平。

人员

任何 IT 组织的核心是它的人员。将任何工作负载迁移到 Kubernetes 代表了你的组织在思维方式上的巨大转变,并且需要适当的培训和准备工作。你需要理解这项技术的人员,或者愿意学习的人员。在准备迁移到有状态工作负载时,这一要求更为真实。除了明显的在 Kubernetes 上进行培训和阅读这类书籍的任务外,我们希望引起你对两个领域的关注:成功组织执行良好的特定工作角色以及利用开源社区作为你的团队的力量倍增器。

云原生数据的关键人员角色

我们可以列出许多对成功迁移至关键的角色,但我们将重点介绍三个对于管理云原生数据至关重要的角色,并讨论它们的关系:

云架构师

架构师为开发云应用程序的技术方向提供指导,影响你部署应用程序的云和区域选择,以及你将使用的数据基础设施。这包括何时依赖自管理的开源项目与托管服务。一名有效的云架构师精选技术以满足当前业务需求,同时留有未来扩展性的空间。

站点可靠性工程师

在第一章中,我们谈到了采纳 SRE 思维方式。虽然这种思维方式是你组织中每位工程师都应该朝向的方向,但 DBA 在成为 SRE 角色时具有极其战略性的机会。与仅仅部署数据库并离开不同,一个以数据为重点的 SRE 全面看待数据基础设施及其如何支持系统整体目标,以获取最佳性能和成本比。

数据工程师

虽然数据科学家关注从数据中提取价值,数据工程师负责数据的操作化。他们构建数据处理流程,组装系统,并考虑数据产品的最终用户消费。数据工程师不仅应精通基于 Kubernetes 的技术,还应了解哪些云服务可以结合使用,以获得优化的结果。数据工程师将在选择和部署支持我们在第十章中讨论的 AI/ML 工作负载的技术方面发挥重要作用,组合多个组件以实现对应用程序实时洞察的流程。

要考虑这些角色如何在组织中协同工作,请考虑农场操作的类比:

  • 架构师类似于规划者,决定在每个季节种植什么作物,以及每种作物的数量。

  • SRE 类似于农民,他们种植和培育作物,以确保它们健康且富有生产力。

  • 数据工程师类似于分销商,他们收获作物并确保其到达正确的目的地。

如果您的组织尚未定义这些角色,请不要担心。在许多情况下,可以对目前从事其他角色的工程师进行再培训。

加速创新的社区

要解释《塞尔达传说》中持剑老人的话,单独前行是危险的。带上朋友。在科技领域,社区是工作的核心部分,我们一起工作,共同学习,分享成功和失败。在踏上新的技术旅程时,寻找围绕该技术形成的社区。以下是云原生数据领域的几个显著社区。您可以寻找这些信息,加入对话,并希望能贡献自己的一份力量:

云原生计算基金会

也称为CNCF,这个组织是更广泛的Linux 基金会的一部分,这是一个致力于开源倡导的非营利组织。CNCF 是 Kubernetes 和许多在 Kubernetes 上运行的项目的家园,包括本书中介绍的几个特色项目。您可以从毕业和孵化项目列表中看到投入到 Kubernetes 原生项目中的能量。CNCF 的成员支付会费,用于支持基金会及其项目的倡导和管理工作。

技术监督委员会(TOC)负责批准和维护 CNCF 项目的技术愿景。随着如此多的项目需要维护,已经成立了技术咨询组(TAGs)来处理跨项目的关注点。每个 TAG 在最初的章程内保持自主性,为类似分组的项目创建一个维护互操作性标准的场所。每个 TAG 都维护自己的 Slack 工作空间和邮件列表,用于社区讨论。

所有项目的开发活动都集中在其 GitHub 仓库中。要参与贡献代码,请在每个项目的 GitHub Issues 中寻找 “good first issue” 标签。如果你有更广泛的兴趣,可以考虑加入 TAGs 中正在进行的讨论,以帮助塑造未来的方向。每年两次,CNCF 在北美、中国和欧洲举办 KubeCon + CloudNativeCon 用户大会,拥有大量的会议议程。一些最佳的议程是关于部署特定云原生技术的用户故事。

Apache 软件基金会

ASF 是一个非营利性的软件保护组织。ASF 成员为被接受的项目提供治理、服务和支持。经过孵化过程后,项目毕业成为顶级项目,获得 Apache 名称(例如 Apache Cassandra、Apache Spark 和 Apache Pulsar)。每个项目都由一个项目管理委员会(PMC)独立运行,具有进行项目更改权限的用户被称为 committers

在 Apache 项目周围,重要的是要注意项目社区和用户社区之间的区别。项目社区关注于构建项目,而用户社区则是下游,主要专注于在其应用程序中使用项目。这种关注分离在大多数项目中都体现在两个邮件列表上:dev@.apache.orguser@.apache.org

如果你有兴趣贡献代码,直接跳进去是开始的最佳方式。Apache 项目使用 Jira 来跟踪变更和错误。在 Jira 问题中寻找“低 hanging fruit”或“好的首个项目”标签。在用户社区中,通过参与邮件列表或 Stack Overflow 来帮助他人是开始贡献的好方法。为 Apache 项目做演示是增加项目知名度的生命线,也是最佳贡献之一。

Kubernetes 社区数据(DoKC)

与 CNCF 和 ASF 不同,DoKC 是一个由行业供应商和最终用户组成的知识社区。DoKC 不是托管软件项目的地方,而是基础设施领域中人们的中心聚会场所。技术供应商赞助这个社区,但它的宗旨是在所有活动中保持供应商中立。这些活动包括线下和线上聚会、dok.community 网站 上的博客以及与 KubeCon 的伴随活动 DoK Day。

除了聚集社区,DoKC 还制作有用的资源,帮助用户在 Kubernetes 上做出数据技术决策:

  • 鉴于可用的数据技术数量,已经创建了DoK Landscape来帮助比较和评估各种选项。您可以按照开源与商业许可证之类的属性进行搜索,或者是否提供了操作员或 Helm 图表。

  • 每年进行一次DoK 调查,以评估行业观点并提供常见问题的指导。该报告免费,可用于您的演示文稿。

作为一个知识社区,参与 DoKC 的最佳方式是共享知识。在社区形成之初,关于在 Kubernetes 中运行有状态工作负载的最终用户信息很少。创建一个专注于数据主题的空间已经促进了一组共同的兴趣和概念的增长。本书中大多数的访谈来自我们在 DoKC 中遇到的人们。

在整本书中,我们看到了每个社区对使数据技术在 Kubernetes 上有效运行的贡献的好处:

  • 我们在第二章中讨论的 PersistentVolume 子系统为 Kubernetes 上的各种开源和商业存储解决方案提供了坚实的基础。

  • 我们在第五章中讨论的操作员框架,包括 Operator SDK、Kubebuilder 和 KUDO,已被证明是开发多种数据基础设施操作员的重要推动者,来自 ASF 和其他开源项目。

  • Kubernetes StatefulSets(首次在第三章中介绍)是一个有趣的案例。虽然它们已经证明在管理分布式数据库方面非常有价值,但社区也发现了一些改进的机会,我们期待未来能够解决这些问题。

  • 同样地,Spark 和其他分析社区中的项目已经确定了 Kubernetes 默认调度器的挑战,正如您在第九章中了解到的那样。幸运的是,Kubernetes 提供了用于扩展调度器的 API,例如 Apache YuniKorn 和 Volcano 等项目可以利用。

正如您所看到的,这个互联社区生态系统中仍然有大量工作要做,需要来自云原生世界各个角落的贡献,才能使我们作为一个行业达到下一个成熟阶段。请记住,社区参与不仅限于向项目提供代码。向任何社区最重要的贡献之一是分享您的故事。想想您学习新技术的经历,您可能会回忆起良好的文档、出色的示例,以及最有价值的:“我们是如何构建这一切”的故事。请尽可能以任何方式分享您的故事。您的社区需要您!

技术

对于你们许多人来说,这是最令人兴奋的部分。酷炫的玩具!在考虑迁移到云原生数据时,你们将在选择使用的技术和集成到应用程序中的方式上做出重要决策。你们会从第一章中回忆起在 Kubernetes 中部署云原生数据的关键指导原则:

  • 第一原则:利用计算、网络和存储作为通用 API。

  • 第二原则:分离控制平面和数据平面。

  • 第三原则:简化可观察性。

  • 第四原则:确保默认配置的安全性。

  • 第五原则:优先选择声明性配置。

结果表明,这些原则对于技术选择和集成非常有用,接下来将会看到。

选择云原生数据项目

多年来在数据特别是大规模基础设施方面的构建,已经产生了大量供应商和开源社区提供的工具。在这里,我们有意选择以项目为理由而不是选择技术。项目包含所需的技术,同时与我们需要的流程集成,由将推动成功的人员创建。你们在这里是因为相信 Kubernetes 是其中一个能够实现的项目,但你们如何做出下一步选择呢?以下是我们推荐的一些原则:

为 Kubernetes 准备好

第七章概述了 Kubernetes 本地数据库的要求,包括:

  • 充分利用 Kubernetes API

  • 自动化、声明性管理通过操作者

  • 可通过标准 API(如 Prometheus)进行观察。

  • 默认情况下安全

尽管你使用的每个项目都不必是 Kubernetes 本地的,但作为 Kubernetes 准备就绪的标准略宽。至少,你使用的项目应具有操作者或 Helm 图表。下一个级别是朝着 Kubernetes 本地的概念迈出一步,即对 Kubernetes 有深度集成的内置意识。一个例子是 Apache Spark,它具有使用专用容器的 Kubernetes 集群部署选项。最高级别的成熟度是由完全实现的云原生项目填充,因为它们依赖于 Kubernetes 集群中的组件,所以只能在 Kubernetes 中运行。这类项目的例子是 KServe,它没有在 Kubernetes 之外运行的方式。

开源

在云原生时代使用开源项目是关于选择。你可以在需要的地方部署你需要的内容。如果你选择使用基于开源项目的托管服务,它应该与开源版本完全兼容,没有限制返回自我管理解决方案的能力。选择正确的许可证让你有信心使用一个项目并保持你的选择。我们推荐采用 Apache 许可证 2.0(APLv2)的项目。所有 ASF 和 CNCF 项目都使用这个许可证,因此无论来自哪个来源的项目都保证了你获得一个宽松的许可证。许多其他许可证提供不同程度的宽松性和限制,你应该仔细考虑它们如何影响你的部署和要求。

当然,项目选择并不是你可以独立完成的事情。上游决策影响每一个后续决策,并且反过来可能限制可用选择。这就是为什么在许多情况下,看起来合理的是考虑那些可以很好地配合使用的项目组合,不论是经过故意设计还是标准接口。

云原生数据的新架构

云原生数据的未来应该更少关注新项目,而是更多关注新架构。这意味着使用我们今天拥有的项目组合,充分发挥每一个项目的优势。正如我们之前讨论过的,软件行业有利用持续一个或更长时间的前几代的思想来从新的角度进行创新的历史。在云原生世界中,过去的十年已经花费在建设规模基础设施上,接下来的十年可能将关注如何结合这些项目来满足我们的需求。

基础设施社区在集成解决共同问题的基础设施堆栈方面有着历史上的偏爱。一个例子是 LAMP 堆栈,在 2000 年初为 Web 应用程序流行,包括 Linux 操作系统、Apache HTTP 服务器、MySQL,以及根据询问者的不同选择的 PHP、Perl 或 Python。2010 年代带来了用于大数据应用的 SMACK 堆栈,具有 Spark 引擎、Mesos 作为资源管理器、Akka、Cassandra 和 Kafka。

尽管描述云原生数据的这样一个技术栈很诱人,但现实是因为用例的多样性和可用项目的数量实在太多,无法提出一个一刀切的解决方案。相反,让我们考虑一个简单天气应用案例的候选解决方案架构,如图 11-1 所示。该架构展示了本书中讨论的原则和建议,利用了我们数据基础设施的持久性、流式处理和分析类别。这是一个概念视角,我们可以作为社区讨论、批评和改进的起点。我们在这里做出的每个选择都有替代方案,应当被视为讨论的起点。

天气应用程序的示例架构

图 11-1. 天气应用程序的示例架构

让我们走一遍数据流,以了解这个架构如何满足具有多个数据需求的天气应用程序的需求。我们假设整个服务器端基础架构堆栈包含在单个 Kubernetes 集群中。这种架构的更高级形式可能包括多集群部署或包括负载均衡或 Ingress 等网络能力。目前,这将用来说明数据架构。

天气数据从气象站收集,并发布到等待 API 的 Ingress 端口,进入正在运行的 Kubernetes 集群。业务逻辑和服务器端应用程序代码被容器化,并作为微服务在 application 命名空间中运行。客户端的 Web 和移动应用程序也通过 API 调用使用这些微服务,因此所有外部数据通信都通过微服务层。

实时数据被发送到 Cassandra,用于在 persistence 命名空间中的即时使用。一旦数据以所需的一致性级别提交,变更数据捕获 (CDC) 将完全提交的数据发送到 streaming 命名空间中的 Pulsar 主题。Pulsar sink 将原始数据导出到放置在对象存储中的 Parquet 文件中。同时,订阅了主题的 Flink 消费者分析新数据,以查找用户定义的限制,如高温或低温。如果触发了边界条件,则将温度和站点数据发送回微服务,微服务将向用户应用发送推送警报。

analytics 命名空间中,两个独立的进程将使用对象存储中的 Parquet 数据。Spark 作业用于跨地理数据组织温度平均值。Ray 应用于对天气预测的预测分析,其分析代码用 Python 编写。每天都会构建以下五天的预测,通过查看最近的数据并应用建立在历史趋势上的模型。Spark 和 Ray 作业都在 Cassandra 中填充新的快速事务数据表。

这个候选架构还展示了一些建议,这些建议并不特定于天气应用程序,但你应考虑在所有部署中使用:

使用命名空间来分隔应用程序内的领域。

在 Kubernetes 集群中部署数百个 Pod 可能会带来组织问题,这在笔记本上的小集群中是遇不到的。我们在这里的建议很简单:大量使用命名空间来在复杂部署中创建秩序。在天气应用程序示例中,我们为基础设施的每个功能区域使用简单的命名空间:applicationpersistencestreaminganalyticssecurityobservability。这种方法将在处理服务或管理 Pod 时提供清晰的边界和命名。

自动化证书管理

在第八章中,我们断言最佳的安全解决方案是您无需考虑的解决方案。使用 cert-manager 自动化证书管理就是一个使这成为可能的优秀示例。对所有服务间通信使用 TLS。对于入口路由,确保所有流量均为 HTTPS。这两种情况使用 ACME 插件来旋转和分配证书,再也不会因为过期证书而遭遇停机事件。当安全审计来临时,您可以勾选一个框,表明您执行了所有政策和指南,并且所有网络通信都得到了充分的加密。就这么做吧。

更倾向于使用对象存储

在选择 Kubernetes 集群中有状态服务的存储时,应尽可能选择对象存储。正如在第七章中讨论的那样,这一推荐背后有几个原因,将使您在部署云原生数据时处于更好的位置。主要原因是不可变性对将存储与运行过程分离的影响。块存储通常与计算基础设施紧密对齐,并且具有较高的复杂性。必须打破计算和存储之间的紧密耦合,以构建真正的无服务器数据基础设施。对象存储已被证明是一个关键的支持者。您可以选择在 Kubernetes 内部实现自己的对象存储,也可以通过云服务来实现。

标准化 Prometheus API 用于指标

对于在 Kubernetes 中构建和运行的复杂基础设施,可观测性是强制性的,而 Prometheus API 是最广泛采用的指标解决方案。确保所有服务以 Prometheus 格式公开指标,并在一个地方收集它们。Prometheus API 在各种后端上实现,如 VictoriaMetrics 和 InfluxDB,为您管理自己的 Prometheus 部署或连接云服务提供了选择。最后,收集指标只是挑战的一部分,利用这些指标构建仪表板和警报则完成了整个方案。

正如这一架构展示的,您现在可以在 Kubernetes 中的单个部署中部署支持复杂应用所需的所有基础设施。这是一个灵活的架构,可以根据需求的变化尝试和拒绝或替换新组件:您正在使用最适合您应用需求的数据库吗?数据应该在流中分析还是在静态时分析?架构代表了基于能力、限制、知识和哲学的一系列选择。

我们期待未来的对话和会议演讲,分享奏效的模式以及应避免的反模式。

部署服务,而不是服务器

我们推荐的一种模式是从更高的抽象级别开始交付能力:作为服务而不是服务器。为了帮助界定这个讨论,可以将其比作建筑的架构设计。建筑师必须理解结构的需求,然后应用材料和风格的知识,创建一个计划供建筑师实施。当建筑师考虑放置门的位置时,它必须位于一个有用的位置,但不能削弱整体结构。在任何时候,他们都没有详细规定门是否必须有黄铜铰链。

在软件行业中,我们在到达部署阶段之前,历来要求大量关于个别计算、网络和存储资源的细节。例如,在裸金属基础设施时代,安装服务器是一件重要的事件。每台服务器代表一个物理设备,具有需要整套软件支持的网络连接,包括操作系统和应用程序以填补系统中的角色。采购、配置和部署 Web 服务器或数据库服务器是一个可能需要几个月的过程。

随着向云计算的迁移,有一句格言是我们应该把服务器看作是“牛群而非宠物”。尽管这种强调是有帮助的,但在许多情况下,对个别服务器的关心和维护仍然存在。网络服务器接受请求然后响应数据,仍然需要人员安装这些系统,进入到为今天的云原生应用程序所需的详细细节。这些细节产生摩擦。

Kubernetes 在这一领域推动了很大进展,强调使用 Deployments 和 StatefulSets 管理无状态和有状态服务的群集,而不是专注于单个 Pod。现在是将这种思维推向更高级别的时候了,而 Kubernetes 给了我们实现这一目标的工具。

考虑前一节中的架构如何可以描述为纵向集成服务——天气服务——由一组微服务和从 Kubernetes 原语构建的数据基础设施组成。回顾来自第一章的“虚拟数据中心”概念,图 11-2 描绘了一个纵向集成服务的内容,它暴露了一个简单的 API。尽管这样的服务可能包含广泛的业务逻辑和基础设施,但这种复杂性隐藏在一个简单的 API 背后。

纵向集成服务

图 11-2. 纵向集成服务

回到之前的天气服务示例,让我们看看这如何代表一个看似简单 API 背后的强大功能。当您放大视角时,部署基础设施的集合看起来像一个函数机。这个函数机不仅仅是一个简单的微服务,仅仅获取和放置数据记录,它接受多个输入并生成多个输出,如图 11-3 所示。

作为函数机的天气服务

图 11-3. 作为函数机的天气服务

作为函数机,天气服务接收一系列温度测量数据并生成多个输出。除了能够检索最初插入的个别记录外,它还生成增值信息,如统计数据、警报和预测,帮助用户理解数据及其如何与个人相关联。

单个传统服务器无法满足所需的各种需求,这就是现代数据基础设施和架构存在的原因。需要进行架构工作来组装合适的部件、连接它们,并从单一输入值创建新的数据。

用户和其他应用期望能够响应其所需数据的服务端点。函数机内部发生的情况由实现满足 API 合同的方式决定。当思维转向结果时,清楚地表明部署服务取代了部署单个服务器的焦点。数据服务可以在各种规模上运行,具备弹性,并使用自动化来处理工具部署的微小细节,从而使我们不必担心这些问题。利用 Kubernetes,您可以像使用其他可消耗资源一样,通过使用计算、网络和存储来指定函数机将执行的操作。

过程

现在我们已经讨论了将有状态工作负载移至 Kubernetes 的人员和技术方面,让我们来看看成功执行此转变所需的实际流程步骤。需要明确的是,“过程”并不意味着需要更多的会议或参与决策的人员。词典定义过程为“为达到特定目标而采取的一系列行动或步骤”。对于云原生部署过程,让我们加上“自动化”这个词,这才是正确的精神。目标是定义和编码一个自动化过程,使您能够自信地持续部署。当您不仅在 Kubernetes 中管理完整的云原生应用程序堆栈,而且有一套可重复的步骤来复制该堆栈时,您就知道已经成功了。

在您的云原生旅程中,您可能处于起点或更进一步。无论起点在哪里,我们建议采用图 11-4 中显示的阶段,以在 Kubernetes 中采纳云原生数据。

将数据工作负载移至 Kubernetes 的各个阶段

图 11-4. 将数据工作负载移至 Kubernetes 的各个阶段

每个阶段都包含由成功进行过渡的组织开发的核心能力。在继续前进之前,你需要采纳并稳定这些能力。利用现有的许多资源花时间成为每个阶段的专家。我们接下来将更详细地探讨每个阶段。

DevOps 实践

在你甚至开始采用 Kubernetes 之前,你应该完全掌握两个管理云原生基础设施的领域:

持续集成/持续交付(CI/CD)

多年来,DevOps 团队已广泛使用 CI/CD。正确实施后,这将使你拥有每天多次进行高信心更改的敏捷性系统。对于云原生基础设施,这也被描述为GitOps。使用源代码控制作为基础设施更改的起点,系统如Argo CD将用于自动化部署。犯了个错误?回滚它。

可观察性

你可能听过“信任但验证”的说法,在高度复杂的云原生部署中尤为重要。你需要看到发生了什么并进行调整,特别是在使用 CI/CD 时。在构建符合 SLA 的服务过程中,每一步都必须被观察到。这建立了对你所做更改是否有效的信心;如果不行,你可以回滚并重试。每一步都在被监视和记录。

虽然随着你开始采用 Kubernetes,你的 CI/CD 和可观察性实践的具体实施细节将不可避免地发生变化,但在这些领域建立坚实的基础将为你的成功打下良好的基础。

基本的 Kubernetes 成熟度

如果你刚开始接触 Kubernetes,这是一个至关重要的阶段。在你的笔记本电脑或云上设置基本的 Kubernetes 部署是学习的绝佳方式,但是你的第一个生产 Kubernetes 项目将考验你的运维能力。在这个阶段完全理解所有潜在问题和解决方案可能需要几个月的时间:

部署和管理集群

这是你可以获得的最重要的经验。尽管自己构建 Kubernetes 集群并安装自己的数据库可以获得很大的学习成果,正如我们在第三章中所提到的,通过使用托管 Kubernetes 服务或像 Terraform 这样的工具可以更快地实现生产能力。学习如何部署和连接集群内外的服务将为你带来最大的价值,这些任务让许多新用户感到意外地棘手。你还需要理解集群各个元素收集的指标以及它们对性能和容量的意义。

迁移无状态工作负载

一旦你熟练掌握 Kubernetes 的工作并理解一些复杂性,你可以开始移动无状态工作负载。这些工作负载的资源需求往往更加简单明了,而且已有大量关于部署无状态工作负载的先前实践。在此阶段,您可能需要管理外部网络到尚未移动的有状态工作负载和数据基础设施。经过几次成功的迁移后,您应该能够在 Kubernetes 中管理生产工作负载并开始看到运营节奏的改善。

这里是我们建议在开始移动无状态工作负载时建立的几个能力:

利用持续交付

在命令行上使用 kubectl 是学习的好方法,但在日常运营中却很糟糕。习惯于将资源组管理为服务,而不是单个 Pods,并让 Kubernetes Operators 来维护您的系统。

网络路由和 Ingress

当您试图与 Kubernetes 的工作方式对抗时,会发生不好的事情,新手在网络通信方面的一个常见失败点。您应优先使用服务名称而不是 IP 地址,并了解 LoadBalancer 和 Ingress API 的工作原理。

默认安全性和可观测性

部署的服务应默认处于安全状态,并公开可观测的接口,如无需手动配置的度量端点。确保每个新服务都使用网络级加密部署。为了有效管理系统,SRE 必须有可用的度量标准来诊断问题,确保覆盖没有间隙。

当您进入后续阶段时,这些能力将为您提供良好的服务。

部署有状态工作负载

接下来的阶段是将有状态工作负载迁移到 Kubernetes,包括它们的支持数据基础设施。在这种情况下,我们建议采用分阶段的方法,大致按以下顺序进行:

持久性

我们建议首先迁移数据库作为您的第一个有状态工作负载。数据库在 Kubernetes 中的运行时间比其他有状态工作负载长得多,并且有更高的成熟度和文档支持。第四章和第五章分别提供了使用 Helm 和 operators 部署的指导。从您的开发环境开始,并模拟在 Kubernetes 外部的相同生产流量负载。熟练掌握备份和恢复操作。确保您的测试案例包括数据库计算和存储资源的丢失,并在感到恢复响应足够时进入分阶段和生产环境。

流式处理

Kubernetes 对流处理工作负载的准备性正在变得更加成熟,但我们仍建议在持久性工作负载之后迁移这些工作负载。正如我们在第八章中讨论的那样,流处理工作负载具有一些独特的特性,这些特性可以使它们更容易进行迁移:大多数用例不需要长期消息存储,因此从一个流处理服务切换到另一个通常不需要数据迁移。由于流处理对网络的需求很高,精通 Kubernetes 网络是必要的。

分析

分析工作负载的复杂性使其成为在持久性和流式处理之后迁移到 Kubernetes 的下一个逻辑选择。一个良好的起步方法是将分析工作负载部署到一个专用的 Kubernetes 集群中,这样你就可以学习 Kubernetes 的部署模式以及作业管理和数据访问的特殊考虑因素。最终,你应该考虑使用不同的调度程序来支持批处理工作负载,比如 YuniKorn 或 Volcano,正如我们在第九章中讨论的那样。

AI/ML 工作负载

你可以考虑为额外的迁移加分考虑你的 AI/ML 工作负载。正如我们在第十章中讨论的那样,这是组织在数据基础设施方面最不成熟的领域之一。像 KServe 和 Feast 这样的项目非常适合 Kubernetes,因此这不是问题。真正的问题是你的组织是否精通 MLOps 和数据工程。你可能已经准备好了,但作为大多数组织的一般建议,这是你应该在处理其他分析工作负载之后考虑的一个领域。

你具体采用的采用计划的细节将根据你的 Kubernetes 准备情况、每个工作负载的成熟度以及其构建在其上的基础数据基础设施而有所不同。第七章中的 Kubernetes 本地定义为评估你的基础设施准备性提供了一种有价值的方式,以及在 Kubernetes 上正确部署和管理它时可能会遇到的额外工作。

持续优化你的部署

在互联网爆炸初期,即所谓的“点 com”年代,初创公司争相获得风险投资并提出计划。几乎每份推介文档都包含一张展示计划数据中心建设的幻灯片。这是有充分理由的:数据中心是一项重大的资本成本,而在筹集资金时,这一点必须考虑在预算内。

如今情况已然不同。现在的初创公司从云服务提供商那里租用他们所需的资源,而仍在管理数据中心的大型企业正在迅速减少其占地面积。在这个云原生的世界里,我们对使用的基础设施有了更多的灵活性,这为管理成本、质量以及它们之间的权衡提供了更大的机会:

优化成本

在任何业务中,你都有增加和减少的事项。金融人士称之为销售成本COGS)。如果你在造汽车,COGS 可能包括像钢铁、工厂和劳动力等成本。销售汽车可以覆盖 COGS,并为公司带来利润。控制成本并使其可预测是创建可持续业务的一种方式。

在今天的应用软件技术中,COGS 有四个主要组成部分:人力劳动、计算、网络和存储。这些指标已经被长时间追踪,已经取得了很多降低成本和增加可预测性的进展。DevOps 已经减少了所需的人为交互量,云已经将基础设施成本正常化。如第一章所述,Kubernetes 并不是一场革命,它是一场进化,一个汇聚的地方,以帮助解决应用软件技术中 COGS 的问题,这是一个既不妥协质量又创造可预测性的解决方案。

弹性是云原生数据的一个方面,可以带来显著的成本节约。如果服务最初部署时具有固定容量,请优化您的部署能力,不仅可以在需要时扩展,而且可以在需要时缩小。尽可能使用自动化,例如HorizontalPodAutoscaler,以便在负载下进行自动扩展,以维持性能。选择能支持弹性工作负载管理的项目是确保您以最佳性能获取最低成本的关键方式。

优化质量(可用性和性能)

减少人力劳动可以减少运行 Kubernetes 部署所需的人数。自动化部署和合理的默认设置可以很大程度上减少劳动,但是自愈基础设施将减少需要随时待命的人数。通过在集群中注入故障来测试服务恢复,优化您的自愈部署。杀死一个 Pod 或 StatefulSet。周围的服务会发生什么?如果这种情况让你感到紧张,你需要优化您的部署,直到对故障感到舒适。

绝不能通过牺牲质量来优化成本。不断优化价格和性能。当您不断寻求优化您的 Kubernetes 部署时,您应该问自己以下问题:

  • 您是否在维护 SLA?

  • 是否减少了人为交互的需求?

  • 没有流量的情况下,你能否扩展到零?

鉴于运维领域当前的趋势,AIOps 是一个很快会进入你词汇表的术语,如果你还没有接触到的话。AIOps 并不是指针对 AI/ML 工作负载的运维,它指的是利用 AI/ML 智能管理基础设施。在观测性的强基础上,你收集的度量和其他信息可以被分析,并用来生成对基础设施的推荐调整。自动扩展和缩减部署和 StatefulSets 只是一个开始。我们希望很快能看到更先进的 AIOps 能力,例如:

  • 一个系统检测到在特定区域使用率增加的垂直集成服务,并响应地部署微服务和支持基础设施到该区域,并主动复制数据以优化客户应用程序的延迟。

  • 一个多租户系统,当特定租户展示出增加的使用情况时,检测到并将该租户的流量迁移到专用基础设施上。

这些只是我们可能实现的一些例子。我们已经在 Kubernetes 控制平面实现的控制器-协调器模式上有了基础。今天的运维人员是高度程序化的,但我们可以在未来的操作员中建立什么样的决策灵活性,以实现所需的服务质量?请继续关注,因为云原生世界正在不断演变。

云原生数据的未来

在信息技术领域的职业生涯中,你可能会见证几次世代变革。在五到十年的时间里,技术会逐渐演进,改变会悄然积累,直到有一天你意识到自己的工作方式已经彻底不同了。也许你的职业生涯的一部分是在物理服务器上安装操作系统。在更近代的世代中,我们开始使用脚本为云实例提供操作系统镜像,并准备安装软件。Kubernetes 代表了最新一代,工程师们可以在文本文件中定义所需的一切,控制平面在执行之时汇聚状态,并执行以前每一代工程师必须手动完成的所有任务。

从世代到世代,我们会继续看到怎样的进步?以下是一个关于非常可能的近期未来的虚构故事。这个故事展示了作为数据基础设施工程师社区的我们可能会走向何方的一个例子。变化将是微妙而深刻的。

这个故事旨在帮助您超越配置文件的繁琐和新项目的诱惑,关注如何拥抱云原生数据为更美好的明天打开大门。当基础设施的劳动力减少甚至消失时,想象一下我们拥有的新能力,以及这如何能够转化为具体的日常成果。这不是科幻小说,并且您不必等待下一代突破。正确应用现有技术,今天就能实现所有这些。

总结

您成功了!我们一起走了一段相当漫长的旅程,并且涵盖了很多内容,不仅仅是本章,而是整本书。一开始,我们提出了一个宏大的目标:在 Kubernetes 上运行有状态工作负载。正如我们从 Craig McLuckie 那里学到的,这与 Kubernetes 项目的最初目标非常一致。最终,我们将扭转应用程序对基础设施的感知趋势,并拥有应用程序感知的平台,并以速度、效率和信心构建应用程序。

希望我们已经说服您,从技术上讲,这是可行的,并且从成本和质量的角度来看非常具有吸引力。在本章中,我们专注于帮助您规划过渡的路线,重点放在您需要成功进行的人员、流程和技术变更上:

  • 帮助您组织内的人们精通 Kubernetes 和数据技术,包括我们在这里涵盖的内容。如果您身居领导岗位,请帮助将人员安置到能够直接负责和承担基础设施选择的角色中。赋予他们参与和贡献开源社区的能力,并成为您组织中变革的催化剂。

  • 选择体现云原生和 Kubernetes 原生原则的数据基础设施技术。使用 Kubernetes 自定义资源和操作符提升架构抽象级别,开始考虑管理实现了明确定义 API 的服务,而不是管理单个服务器。

  • 更新您的流程,将“一切”自动化——从集成和交付(CI/CD)到可观察性和管理(AIOps)。在您战略性地将有状态工作负载迁移到 Kubernetes 时,利用这些成熟的流程。在成本和质量之间谨慎平衡,以可持续地为最终用户提供最佳体验。

现在,这段旅程的叙述转向了您和您选择带领我们前进的地方。虽然本书为您提供了关于 Kubernetes 上数据基础设施世界的广泛概述,但每一章都可能轻易填满一本书。我们鼓励您继续学习,深入到您特别感兴趣的领域,并分享您的所学,以持续填补我们集体知识的空白。当您成功地管理您在 Kubernetes 上的云原生数据时,我们期待听到您的故事。

posted @ 2025-11-16 08:59  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报