Kubernetes-高级平台开发-全-

Kubernetes 高级平台开发(全)

原文:Advanced Platform Development with Kubernetes

协议:CC BY-NC-SA 4.0

一、软件平台和 API

2018 年 10 月 28 日,IBM 宣布以 340 亿美元收购红帽、 1 红帽企业 Linux (RHEL)背后的公司,以及最近的红帽 OpenShift,一个企业 Docker/Kubernetes 应用平台。我们看到的是 340 亿美元的证据表明,以 Linux 生态系统为中心并由 Kubernetes 支持的云原生和开源技术正在引领企业软件应用平台的颠覆。

任何对企业软件营销的接触都会呈现主要云提供商几乎每天发布的稳定的平台服务流,包括谷歌云机器学习引擎、微软的 Azure 机器学习服务、亚马逊管理的区块链和 IBM Watson 物联网平台等产品。像亚马逊、微软、IBM 和谷歌这样的大提供商不仅对这些技术的市场需求做出了回应,而且让人们更加意识到这些技术可以解决各种行业的问题。大型软件供应商正在快速响应对这些功能的需求,并通过提炼和营销展示其价值的产品来延续其需求。这些供应商通常只是在开源软件中包装最新的服务,添加精致的用户界面和专有中间件。窥视这些超级云服务的内幕,你经常会发现一系列云原生甚至供应商中立的机器学习(ML)技术,如 TensorFlow、Keras 和 PyTorch,或以太坊和 Hyperledger 支持的区块链功能,以及 Prometheus 和 Kafka 等高性能物联网数据收集器。这些供应商并没有从开源社区窃取这项技术;在这个生态系统中,一些最重要的贡献是供应商本身。

从头开始开发一个具有区块链和机器学习等多种功能的企业级平台,仅在几年前还需要付出巨大的努力。你的另一个选择是对一个商业平台的重大投资和长期承诺。谷歌用 Kubernetes 颠覆了整个商业平台业务,Kubernetes 是一个免费、开源、云原生和厂商中立的系统,用于快速开发新平台,可以轻松支持几乎任何具有企业级安全性、稳定性和规模的技术。随着 Kubernetes 的成熟,软件和平台开发人员可以将更多的时间放在特性上,减少基础设施、网络、扩展、监控甚至安全方面的定制工作,预计将会看到另一波重大的平台创新。

这本书旨在使用 Kubernetes 以独立于供应商的方式构建一个简单的演示平台。只需进行最少的修改,这个新平台就可以在任何能够运行 Kubernetes 的主要云提供商上运行,并提供少量广泛可用的依赖项,如存储、内存和 CPU。在这个平台上实现的每一种现有的开源技术都专门关注一个特定的解决方案。提供机器学习、区块链或基于物联网的服务本身不会成为平台的核心优势。然而,在 Kubernetes 中一起操作这些技术提供了一个基础,在这个基础上通过他们的共同努力来构建和提供新的解决方案,并为将来的添加提供一个模板。

在 20 世纪 90 年代早期,数据库通常作为独立的应用进行操作和访问。数据库和 web 服务器的结合通过动态数据库驱动的网站彻底改变了互联网。这些组合现在似乎显而易见,Kubernetes 与 Istio 和 Linkerd 等服务网格技术一起在不同的应用之间建立连接,即使存在相互冲突的依赖关系,这不仅是可能的,而且为平台增加了安全性和遥测功能。

软件应用与软件平台

你可能是一名软件开发人员,对某个特定行业的某个问题有解决方案。对于封闭和开放源代码软件的特定组合,您希望将这些功能组合在一个 API 下,并公开它们以支持特定的应用。或者,你可能是一个增值经销商,希望为客户提供一个应用开发平台,该平台带有一套预打包的功能,如机器学习、区块链或物联网数据摄取。像 Kubernetes 这样的软件平台是开发单一应用或平台即服务(PaaS)的理想环境,为客户提供了一个开发和扩展应用的环境(图 1-1 )。

img/483120_1_En_1_Fig1_HTML.jpg

图 1-1

软件应用、作为应用集合的平台以及基于平台的应用

依赖性管理和封装

容器化通过创建一个单一的依赖项,即容器运行时,使得运行的软件应用比以往任何时候都更加可移植。然而,应用通常需要访问复杂的资源组合,包括外部数据库、GPU(用于机器学习的图形处理单元)或持久存储,并且可能需要与其他应用通信以进行身份验证、数据库访问和配置服务。即使是一个容器化的应用,通常也需要某种形式的管理来控制它和它对外部资源的访问。管理连接容器的问题就是 Kubernetes 的用武之地;Kubernetes 编排应用的容器,并管理它们与资源的关系。

应用网络

并非所有的软件应用都需要复杂的平台架构。大多数软件应用可以在满足其操作依赖性的计算机上开发和运行。当您希望一起操作多个应用并形成一个互连的服务网络时,或者当多个应用可以从共享的功能、配置或资源管理中受益时,平台就开始发挥作用了(图 1-2 )。

img/483120_1_En_1_Fig2_HTML.jpg

图 1-2

容器化应用网络

应用平台

即使您的目标是开发一个单一用途的在线应用,也有几个理由着手在 Kubernetes 中开发一个软件平台。大型和小型、复杂和简单、企业和小型应用在软件平台环境中实现时都会受益。软件平台提供了一个解决常见问题的架构,并减少了对几个领域的定制开发的需求,包括通信、存储、可伸缩性、安全性和可用性。

将应用设计为一个平台意味着,从一开始,软件就应该扩展到基本需求之外,能够独立地升级和部署新的组件。一个合适的平台欢迎开源最新趋势的加入,当创新出现,开源产品发布时,成功的软件平台包装并利用它们的功能来保持最新。一个合适的软件平台不应该被贴上传统的标签;它应该保持在一个不断迭代的改进周期中。

下一节将更深入地探讨如何使用 Kubernetes 作为核心组件来实现这一点。Kubernetes 解决了传统企业解决方案(如面向服务的体系结构(SOA ))几十年来一直试图解决的问题,只有 Kubernetes 使用支持全球互联网的协议和方法(如 DNS、TCP 和 HTTP)做到了这一点,并将它们包装在一个优雅而健壮的 API 中,可通过这些协议访问。该平台是围绕 Kubernetes 的服务概念及其与容器化应用的关系构建的(图 1-3 )。

img/483120_1_En_1_Fig3_HTML.jpg

图 1-3

服务和应用之间的关系

平台要求

本书专注于实现基础数据驱动、数据科学和机器学习平台,主要但不限于物联网数据,并提供与区块链技术互联的机会。如果这听起来像是大肆宣传,确实如此,随着宣传的消退,是时候开始工作了。随着这些技术离开实验室,它们开始退居幕后,在未来十年,它们将开始在新的创新产品背后默默提供它们的解决方案。

如果你熟悉 2018 年的“Gartner 新兴技术炒作周期”(图 1-4 )、 2 ,你会看到深度神经网络(深度学习)、物联网平台和区块链仍然处于“膨胀的预期的顶峰”,并朝着“幻灭的低谷”滚动幻灭听起来很可怕,但 Gartner 将这些技术的以下阶段标记为“启蒙斜坡”和未来 5-10 年的平台期。许多创新发生在这些技术达到稳定状态之前,由 Kubernetes 管理的一系列连接容器构建的灵活架构应该可以轻松地让您在未来十年或更长时间内保持相关性。

img/483120_1_En_1_Fig4_HTML.png

图 1-4

Gartner 对新兴技术的炒作周期,2018 3

虽然单个组件可能会随着趋势的高峰和平台期来来去去,但数据会一直存在;该平台需要存储它,转换它,并通过最新的创新提供对它的访问,从而从它那里产生价值。如果对 Kubernetes 的高级平台开发有一个核心要求,那就是通过物联网、机器学习、区块链等领域的最新创新技术持续获取数据的价值。

使用 Kubernetes 进行高级平台开发的最后一个要求是保持开源、云原生和厂商中立。一个拥有这些原则的平台可以利用开源来利用贡献软件开发人员的全球社区来解决和我们一样的问题。保持云的原生性和供应商中立性意味着不受特定供应商的束缚或约束,并且随着“混合云”概念的普及,它在私有数据中心中的功能与在 AWS、GKE、Azure 或所有这些平台上的功能一样。

平台架构

在 Kubernetes 中,从一组专门的组件中构建软件平台是很常见的,这些组件用多种语言编写,并且具有非常不同甚至冲突的依赖关系。一个好的平台可以封装不同的组件,并将它们的接口抽象成一个标准的 API 或一组 API。

面向对象的软件概念是整个平台架构的一个很好的参考工具。微服务架构的趋势鼓励开发几个最小的应用,通常采用对象类的形式,在特定的问题域中提供有限数量的操作,让更大的平台负责聚合业务逻辑。为了实现这种方法,采用对象的概念,并将其应用到服务的 Kubernetes 实现中(图 1-5 )。像软件接口一样,Kubernetes 服务代表了应用的一个或多个入口点。抽象、封装、继承和多态的面向对象软件原则可以表达平台架构的每一层。

img/483120_1_En_1_Fig5_HTML.jpg

图 1-5

类设计和服务架构

Kubernetes 非常适合平台开发,对于任何较次要的任务来说可能有些过头了。我相信,正如我希望您在本书中发现的那样,Kubernetes 对于平台开发的适用性没有太多争议。容器通过隔离和封装组件解决了依赖管理的许多问题;Kubernetes 管理这些容器,并以此形成软件平台的框架。

平台功能

本书中概述的平台的目的是展示 Kubernetes 如何让开发人员能够将各种不同的技术组合在一起,并使用 Kubernetes API 管理它们。使用 Kubernetes 开发平台降低了采用最新趋势的风险和费用。Kubernetes 不仅支持快速开发,还可以轻松支持并行工作。我们开发一个软件平台,只需要尽可能少的编程。我们使用声明性配置来告诉 Kubernetes 我们想要什么。我们使用开源应用来构建基础软件平台,提供物联网数据收集、机器学习功能以及与私人管理的区块链交互的能力。

从数据的摄取、存储和检索开始,该平台的核心功能是一个健壮的数据层(图 1-6 )。该平台必须能够从物联网设备和其他外部来源(包括私人管理的区块链)获取大量数据。Elasticsearch、Kafka 和 Prometheus 等应用管理数据索引、消息排队和指标聚合。特定的服务从以太坊 Geth 节点等应用中捕获区块链交易,并将其发送到 Apache Kafka 进行排队,以及 Elasticsearch 进行索引。

在数据层之上是一个应用层(图 1-6 ),提供利用这些数据的能力,比如机器学习自动化。平台服务连接在一起,并公开数据源,这些数据源导出并提供可用于机器学习实验、生产人工智能推理和商业分析的持久和流式数据。

该平台自然支持通过 Kubernetes 对容器的管理来扩展特性。包括 OpenFaaS 在内的无服务器技术提供了更高级别的功能扩展。无服务器支持允许快速开发和部署实时数据处理器、以特定间隔运行的操作和新的 API 端点,允许专门访问数据、执行 AI 操作或修改平台本身的状态。

本书中设想的平台形成了与趋势技术合作的数据驱动基础,专门研究机器学习、区块链和物联网。用于数据接收、存储、索引和排队的组件被集合在一起,并允许在专门的技术之间高效地访问数据。该平台为数据科学家提供了执行机器学习实验和开发生产就绪型神经网络模型所需的数据和工具,通过无服务器功能进行部署,能够进行预测,执行分类,并从现有和入站数据中检测异常。区块链技术用于演示第三方总账交易和智能合同执行如何无缝地内部连接到数据处理流水线。

img/483120_1_En_1_Fig6_HTML.png

图 1-6

平台应用和数据层

迭代开发的平台最终由大量的服务组成,这些服务的规模和复杂性各不相同,混合了巨型独石和小型无服务器功能。有些服务由一个 Java 应用集群组成,而有些服务只执行几行 Python。如果这听起来像一场噩梦,其实不是。幸运的是,容器化帮助我们隔离了应用的操作和依赖性,暴露了配置、控制和与应用通信所需的内容。然而,容器化只能给我们提供有限的可见性和对服务集合的控制。Kubernetes 为我们提供了对基础设施资源、安全性和网络的强大配置访问控制,但将平台应用级问题(如服务间的加密通信、遥测、可观察性和跟踪)留给了应用本身或更高级别的专用系统(如 Istio 或 Linkerd)。本书中开发的平台是一个服务集合,无论有没有 Istio 或 Linkerd 都可以运行。Istio 和 Linkerd 还很年轻,实现它们的最佳实践还在不断成熟。

接下来的几个部分更详细地定义了平台的三个主要需求:物联网、区块链和机器学习(图 1-7 )。

img/483120_1_En_1_Fig7_HTML.jpg

图 1-7

物联网、 区块链 、Kubernetes 中的 机器学习

*### 物联网

物联网(IoT)和更新的工业物联网(IIoT)是已经过了炒作阶段的成熟技术。一个行业的物理设备不仅要通过互联网连接和控制,还要与更大的数据平台有更密切的关系。Kubernetes 能够管理物联网各个方面的数据和控制面板。这本书重点介绍了 Kubernetes 在物联网领域的三种主要用途,包括数据摄取、作为边缘网关,甚至是操作系统(图 1-8 )。

img/483120_1_En_1_Fig8_HTML.png

图 1-8

Kubernetes 平台在物联网中的三种用途

数据摄取

Kubernetes 的第一个也是最明显的用途是编排一个数据接收平台。物联网设备有可能产生大量指标。收集指标只是问题的一部分。将指标收集、转换和处理成有价值的数据并对这些数据执行操作需要一个复杂的数据流水线。物联网设备利用广泛的通信协议,针对特定设备和协议构建的各种软件产品提供不同质量的支持。为了有效地支持来自一系列物联网和 IIoT 设备的数据,该平台需要使用 AMQP(高级消息队列协议)、MQTT(消息队列遥测传输)、CoAP(受限应用协议)、原始 TCP 和 HTTP 等协议。

HTTP 上的 JSON (JavaScript Object Notation)是互联网上最受欢迎和支持的消息传递协议。每一种重要的编程语言都支持 JSON。JSON 以这样或那样的方式驱动了几乎所有的公共云 API。Kubernetes 自己的 API 是基于 JSON 的,而 JSON 的超集 YAML 是声明期望状态的首选方法。

JSON 可能不像二进制消息那样高效,也不像 XML 那样具有描述性;然而,将所有入站消息转换成 JSON 允许平台在当今最灵活和可移植的标准上统一数据接收。该平台由实现各种协议的定制微服务组成,解析入站消息或查询和抓取远程源,并将这些消息转换为 JSON。HTTP 收集服务接受 JSON 转换的数据进行缓冲和批处理。这种架构(图 1-9 )允许无限制的水平扩展,容纳大量数据。

img/483120_1_En_1_Fig9_HTML.jpg

图 1-9

物联网数据摄取

章节“Pipeline”涵盖了摄取和转换服务的实现:Apache NiFi、Prometheus、Logstash、Elasticsearch 和 Kafka。

边缘网关

物联网领域的 Kubernetes 开始包括内部部署和边缘部署。这些是小型集群,通常只包含一个节点。本地集群通常运行较大平台的缩小版本,通常负责与局域网上的物联网设备进行通信,或者节点本身连接到专有硬件和协议、传统控制系统或较低层的串行通信接口。数据收集的工业用例通常包括设备传感器的亚秒级采样,或者仅仅是对分类、异常检测或聚合有用的大量数据。

内部平台(图 1-10 )可以处理指标的初始收集和处理,并将结果反馈给更大的数据处理平台。新的 Kubernetes 发行版,如 Minikube、Microk8s、k3s 和 KubeEdge,专门研究商用硬件上的小型或单节点实现。

img/483120_1_En_1_Fig10_HTML.jpg

图 1-10

内部 Kubernetes 平台

在内部运行缩减的平台解决了许多数据处理的安全性和合规性问题。在数据必须按照严格的合规性规则保留在本地的情况下,本地集群可以处理数据,其产生的元数据、推断和指标聚合可以传输到远程平台,以进行进一步的处理、分析或操作。

伊托勒斯

本书中提到的 Kubernetes 对于物联网的第三种用途才刚刚开始生根发芽,即 Kubernetes 作为物联网操作系统(图 1-11 )。ARM 处理器既便宜又节能。像 Raspberry Pi 这样的产品使它们在业余爱好者、教育和商业原型制作中非常受欢迎。对基于 ARM 的系统的容器支持已经存在了几年,在物联网设备上运行容器化的应用几乎拥有在更强大和更复杂的硬件上运行它们的所有优势。运行 Kubernetes 编排的容器的物联网设备可以利用滚动更新等功能来消除升级应用时的停机时间。在物联网设备上运行 Kubernetes 中的一个小型容器集合,可以让您利用微服务应用架构、资源分配、监控和自修复。小型低功耗设备的软件开发曾经需要使用专有操作系统,并编写大量代码来支持固件更新、崩溃报告和资源分配等活动。随着越来越多的开发人员开始看到物联网软件通过 Kubernetes 等平台解决许多常见挑战的潜力,支持缩小版 Kubernetes 的物联网设备仍然是新的,并有望增长。

像 40mb k3s 这样的精简发行版使 Kubernetes 成为当今市场上 Raspberry Pi 和大型 SOC 板家族等资源有限的小型设备的绝佳选择。

img/483120_1_En_1_Fig11_HTML.jpg

图 1-11

物联网设备上的 Kubernetes 平台

区块链

随着智能合约的成熟, 4 区块链技术现在已经是一类平台 5 本身。智能合约允许在区块链的分布式、不可变账本内存储和执行代码(参见第 9 和 10 章)。区块链技术的引入为平台提供了与不可信参与者进行交易通信的能力。在这个上下文中,不可信意味着不需要个人或法律契约信任来传输以数据表示的值。区块链提供了交易的永久记录,在共享的账本中进行验证。外部各方只需要操作能够执行共享数学算法的区块链节点。对交易完整性的信任来自更广泛的节点网络的一致验证。描述区块链的深入概念、哲学和技术细节超出了本书的范围。

私人管理的区块链

区块链技术是一个分布式的节点网络,在封闭的系统中,区块链的用例非常少。然而,私有或受保护区块链的概念是该平台的焦点,它代表了参与受管网络的基本能力。

该平台在其选定的节点网络中提供第三方参与者的分配和引导(图 1-12 )。私人区块链并不意味着超出允许参与的信任程度。在封闭系统中,这种信任是一种方式。传统平台允许第三方创建账户并使用系统。然而,第三方也必须信任平台运营商。我们相信谷歌不会编辑和修改我们收到的电子邮件;我们相信 Twitter 不会代表我们发微博。区块链参与者依赖于大多数参与者来验证交易,而不是中央权威机构。有了区块链技术,平台只负责参与者的平等参与和管理。将区块链技术直接整合到平台中,使其处于与其他服务的统一通信网络之下,并随着其概念和功能的快速成熟,促进了该技术的管理和配置。“平台化区块链”一章描述了实现的技术细节。

img/483120_1_En_1_Fig12_HTML.jpg

图 1-12

区块链引导和区块链交易的摄取

用例

包括金融、供应链、物流、制造、流程合规性等在内的垂直行业都在寻找区块链功能提供的解决方案,尤其是围绕智能合同的执行。智能合同是位于区块链中的不可变代码块,可以由连接的区块链节点网络中的任何人执行,只要能够满足合同界面的标准。与智能合同和智能合同的执行相关的流式交易事件提供了除记录第三方的主动意图之外的大量机会;它们可以交流物联网设备的状态,或者从机器学习中得出的人工智能的自动化结果。X 公司可以验证 Z 公司发送了数据,而 Z 公司可以验证平台处理了数据并向 X 公司提供了结果,同时其他实体可以执行与此活动相关的智能合同(图 1-13 )。该平台不仅有助于访问这种不可信的网络,而且通过流式实时数据队列与之交互。该平台可以对交易进行索引,并提供对交易类型、频率和价值的深入分析。

img/483120_1_En_1_Fig13_HTML.jpg

图 1-13

区块链智能合约的添加、执行和观察

以太坊和 Hyperledger 是支持智能合约的私有/许可区块链的流行选择。Hyperledger 是专门为区块链私营企业设计的,不包含货币功能。尽管该平台的企业用例不太可能需要区块链货币,但这也不是一种威慑。本书中的平台使用以太坊,使其功能与公共或私人区块链兼容,无论是否需要货币。该平台不局限于以太坊,可以轻松扩展到支持 Hyperledger 或区块链的最新趋势。

“区块链”一章涵盖了以太坊节点在平台中的实现,并提供了与平台数据层互连的示例。

机器学习

根据 Gartner 的说法,机器学习,特别是深度学习,是另一个接近或处于巅峰状态的技术领域。包括 Google、IBM、AWS 和 Azure 在内的所有主要超级云提供商要么提供具有 TensorFlow、PyTorch、Keras 的服务包装版本的平台,要么提供他们内部开发的机器学习开发和生产自动化工具。主要的云提供商对这种炒作反应迅速,但这种反应也表明该技术已经成熟到了商品化的程度。商品技术可能已经失去了一点它的新颖性和兴奋性,但是如果它熬过了最初的阶段,那么它就可以投入商业使用了。对于机器学习,我们可能处于期望的冰山一角,但它能够也确实解决了当今的行业问题,因此它的能力属于平台,并暴露于其连接的数据。

为 Kubeflow 提供一个简单的服务包装,加上一些有限的定制配置,就足以称之为数据科学平台了。Kubeflow 即服务本身就是超级云的有力竞争对手。包装 Kubeflow 将花费不到一章的时间来描述,更重要的是,它错过了区块链和物联网数据的相邻功能的组合能力,所有这些都位于核心组件之上,将在下一节中讨论。

行业希望从实时数据中获得实时答案。数据科学需要静态数据以及执行可重复实验和从已知数量中学习的能力。机器学习是数据科学的一个应用,机器在固定的数据集上学习得最好。该平台通过提供对持久数据的访问以及对数据子集进行标记和快照的能力来提供这两种功能,以形成经过训练的神经网络模型,然后可以立即部署这些模型并针对实时数据流进行尝试(图 1-14 )。当数据科学实验室的技术生态系统与企业的生产问题格格不入时,迭代实验的成本很高。Kubeflow 和其他云原生技术有助于将这些环境更紧密地结合在一起,降低与实验相关的成本和风险。风险的降低为实验和测试机器学习和人工智能的新理论或方法带来了新的机会。

img/483120_1_En_1_Fig14_HTML.jpg

图 1-14

机器学习的高速数据流水线

自动化和管理

有了像 Keras 或 PyTorch 这样的软件框架,您现在可以用几十行代码轻松地配置一个简单但可训练的神经网络。整个机器学习生命周期(图 1-15 )需要的不仅仅是几个移动部分,从数据访问、转换和实验到训练模型的部署。与 Kubernetes 兼容的解决方案,如 Apache Airflow 和 Kubeflow,正在自动化领域取得巨大进步。这本书主要关注 Kubeflow 作为数据科学子平台的几个组件。Kubeflow 来自谷歌的内部解决方案,用于自动使用 TensorFlow,这是一个流行的机器学习框架,也是由谷歌和开源开发的。“In-Platform CI/CD”、“Indexing and Analytics”和“Platforming AIML”等章节探讨了为 Kuberflow 提供支持的一些技术,包括 JupyterLab、JupyterHub、MLflow 和 Seldon Core。

img/483120_1_En_1_Fig15_HTML.png

图 1-15

库伯弗洛过程

核心组件

以下部分涵盖了您可能永远不会在 Gartner 的宣传图表上看到的平台组件。然而,这些核心组件形成了将平台结合在一起的基本粘合剂。这些组件包括配置、入口、数据管理、指标、API 和协议。

容器、配置、入口、数据管理和指标收集是构成平台核心基础设施的核心组件,将在后面讨论。下面简要回顾了它们的用途、它们解决的问题以及它们在将专门的应用结合在一起时的作用。实现物联网、机器学习、区块链和未来创新趋势的潜力只有在整合到一个可以结合他们专注的专业知识的平台时才能得到放大。跨新领域解决问题,利用其特定解决方案的组合效应-例如,利用通过深度学习从物联网设备收集的指标开发的模型驱动的人工智能,这些设备在区块链上执行智能合同,其结果传达其他物联网设备运行状态的变化。独立于区块链和物联网技术发展起来的机器学习当然不依赖于其中任何一个。各种各样的语言被用来开发这些技术,每种语言都有不同的依赖关系。然而,不费吹灰之力,它们都可以在容器中运行,并以可靠性和无限规模得到证明的协议进行通信,因为它们扩展了互联网本身。TCP/IP、HTTP 和 DNS 是互联网的底层协议,在 Kubernetes 中,它们是通信和服务发现的方法。容器之间的这种关系来自容器 API 和它的编排器 Kubernetes。Kubernetes 使容器的配置、缩放和维护变得简单而优雅。因此,Kubernetes 本身以及容器构成了平台的主要核心组件。如果您正在阅读一本关于高级平台开发的书,您可能已经对容器的优势以及 Kubernetes 提供的许多支持对象有了深刻的理解,包括强大的 ConfigMap 和 Secret。

配置

两种形式的配置构成了平台:平台本身的配置,一组以 YAML 文件表示的 Kubernetes 对象,以及平台内各种应用的配置。这本书使用kubectl工具来配置 Kubernetes。kubectl实用程序提供了三种对象管理:命令性命令、命令性对象配置和声明性对象配置。这本书主要依靠声明性对象配置,通过在一系列 YAML 文件中描述期望的状态,并使用kubectl来应用它们。“开发环境”一章介绍了组织和维护 Kubernetes 配置清单的方法,这是保持您的平台配置有组织和有文档记录的一个重要方面。

这里关注的是平台内各个应用的配置,这也是为什么配置被视为核心组件的原因。Kubernetes 提供了对象类型 ConfigMap 和 Secret,为应用配置提供了丰富的选项。

应用参数

容器几乎可以包装任何应用,配置应用的三种最常见方式包括命令行参数、配置文件、环境变量以及三者的组合。如果您曾经使用过 Docker 或 Kubernetes,那么指定一长串配置应用执行所需的参数,或者填充一系列环境变量,或者挂载一个配置目录,这种情况并不少见。有了这三种配置方法,几乎任何应用都可以进行配置,并有一个标准的方法来管理平台所有组件的配置。这是平台核心基础设施的重要组成部分。

ConfigMaps 和 Secrets 是 Kubernetes 提供的配置对象类型;它们在整个集群中是持久的和可用的。配置映射和秘密的键/值对可以作为 Pod 内的文件系统安装,键是文件名,值是数据,其安装点由 Pod 中的一个或多个容器共享。ConfigMaps 和 Secrets 可以填充容器中的环境变量,或者填充容器应用执行中使用的命令行参数。

关于基础设施的“开发环境”一章更详细地介绍了配置映射和秘密的管理和组织,如果您有使用 Kubernetes 的经验,您可能会非常熟悉它们的使用。这里的目的是更清楚地定义它们作为平台核心基础设施组件的价值。

进入

入口被定义为“进入的行为”,是接受平台入站数据的基本概念。然而,入口是框架中最关键的组件之一。入口负责提供一种通过传输层安全(TLS)等协议保护入站通信的方法,根据各种配置的规则将 HTTP 流量路由到各种服务。虽然与平台的大多数交互是通过 HTTP 协议进行的,包括来自外部系统或 web 和本机应用的 REST 风格 API 调用,但该平台还为特定物联网设备和协议的自定义入站 TCP 流量提供了监听器。在一些软件平台中,基础设施级别的入口是事后才想到的;大型整体式有状态应用通常假设与入站请求直接交互,并期望上面的入口层很薄。不幸的是,使用 Nginx 和 Envoy 等流行和有影响力的代理的这些更传统的方式经常无法利用它们在这个专门化中的优势,或者作为一种绕过实现需求的方法。

本书中描述的平台只是利用 Nginx 管理入口的皮毛。然而,它是一个核心组件,对于平台的网络和架构布局至关重要。Envoy 是反向代理支持的另一个新的流行选择,也是 Istio 的核心组件之一。本书描述的平台将使用 Nginx 进行公共 HTTP 入口。

数据管理

数据管理是企业平台的基本核心组件。平台必须在数据通过入口接受并通过 API 检索后对其进行管理。平台的数据管理层也是沟通各种处理器结果的必要手段。原始数据以来自物联网设备的指标、区块链交易的结果以及通过 HTTP APIs 和 TCP 端口的通信的形式进入平台。这些数据被检索和处理,以形成新的区块链交易,向物联网设备发送命令,并形成从机器学习模型中导出的预测;其结果成为新的数据,在这个递归的生态系统中传播并不断改进。

该平台可以利用同类最佳的技术来进行消息队列、索引和指标聚合,方法是采用现代的、成熟的开源技术,包括 Apache Kafka、Elasticsearch 和 Prometheus。在“流水线”和“索引和分析”两章中,我们将分别介绍这些技术的实现从软件平台的角度来看,核心组件是包装所有专用功能的底层基础设施。该平台的目的不仅仅是服务包装技术,如 Apache Kafka 或 Elasticsearch,以获得外部平台功能。将这些技术集成到平台基础设施中,通过 Kubernetes 提供了标准化管理功能的优势,但通过它们的深度集成简化了通信和可观察性。这些技术形成了更高级别的栈基础设施,并通过自定义中介提供其功能,通过规范化对结果数据的访问,连接物联网数据、机器学习和区块链的专业应用。

韵律学

不缺少愿意收集您的指标的托管 PaaS 产品,它们为开发报告、业务分析和智能提供 API 和漂亮的仪表板。聚合、分类和组织指标是一项大业务,感谢开源社区和 SoundCloud,我们有了 Prometheus 这个免费的开源解决方案。SoundCloud 在 2015 年开发并开源了 Prometheus,之后在 2016 年成为继 Kubernetes 之后的第二个云原生计算基金会孵化项目。Prometheus 现在是云原生计算基金会的研究生项目,正在积极开发。

当 SoundCloud 确定 StatsD 和 Graphite 等解决方案无法满足他们的需求时,他们开发了 Prometheus。Prometheus 是一个高性能指标聚合器,将实时指标记录到时间序列数据库中。Prometheus 不仅可以根据我们未来的需求进行扩展,还拥有一种健壮灵活的查询语言。本书中的平台利用 Grafana 构建视觉上令人惊叹的仪表盘,查询我们由 Prometheus 收集的指标。

Prometheus 从应用中抓取指标,并在 Ruby 上提供 Go、Java 和 Python 的官方和成熟客户端库,以及 Bash、C++、Common Lisp、Elixir、Erlang、Haskell、Lua、.Net,C#,Node.js,Perl,PHP,Rust。这个 SDK 集合意味着您可以向几乎任何现代应用添加深度工具。客户端库编写得很好,并且易于实现。Prometheus 在该平台的数据流水线中形成了一个重要的分支,用于丰富的分析仪表盘,并允许区块链和机器学习功能不仅可以输入数据流,还可以对其做出反应。

API 和协议

应用编程接口(或平台 API)提供与平台交互的外部访问。API 是一个宽泛的术语,可用于描述软件的一部分如何与另一部分通信,企业如何访问其数据,或者如何在内部或外部调用编程驱动的业务逻辑。后者,即外部访问,是平台 API 所关心的。这个平台的外部接口涉及数据的存储和检索、事件的记录、所需平台状态的配置、业务逻辑的调用以及平台功能的扩展。平台 API 支持构建针对特定垂直行业定制的基于 web 的本地应用,或者允许现有 IT 系统与其功能进行交互,或者报告数据和事件。该平台使用其 API 来执行提供帐户和用户所需的多种操作,为业务分析和智能解决方案提供数据,并提供前端用户界面来扩展和增加数据科学和机器学习的数据流水线。

本书中介绍的平台反映了许多现实世界的数据平台实现,是单片应用、微服务和(无服务器)功能的集合。平台的每个组件可能有不同的交互方法。Elasticsearch 使用 RESTful API 处理用户请求,并使用异步传输协议与其节点进行内部通信。与 Kafka 的通信是通过基于 TCP 的二进制协议执行的,此外还提供了一个 REST 代理。本书在“流水线”和“索引和分析”章节中涵盖了 Elasticsearch 和 Kafka 实现。在 API 开发的世界里,不缺少解决方案;像 gRPC 和 GraphQL 这样的新的和创新的协议正在变得成熟并获得关注,因为在新的开发中不太可能考虑像 SOAP 这样的旧思想。

本书中的平台在涉及配置的基本交互时采取了中间路线。REST,或者说表述性状态转移,是目前最流行的 API 实现,因为它非常简单,在互联网上无处不在,并且有很多工具和客户端可以使用它。REST 可能不像 gRPC 那样快速紧凑,也不像 GraphQL 那样灵活,但是 REST 除了 HTTP 和它的动词 POST、GET、PUT、PATCH 和 DELETE 之外,对它的客户机没有任何要求。该平台使用 JSON (JavaScript 对象符号)通过 REST 进行通信。REST 之所以能被广泛访问,主要是因为 HTTP 是唯一的需求,一个普遍建立的、成熟的、稳定的协议。

尽管该平台的核心 API 是 REST 的一个实现,通过 Ingress 公开的 HTTP 端点建立,但仍有机会开发定制的、低级别的 TCP 侦听器,并通过 gRPC 和 GraphQL 提供专门的服务。该平台不受实现任何特定 API 协议的限制或约束;在 Kubernetes 中构建平台使得添加额外的接口比以往更加容易。传统上,企业通常很难看到其端点之外的 API,这通常表现为单片应用演变为 API 和底层数据之间高度耦合的大型且笨拙的集合。虽然有可能在 Kubernetes 中开发一个传统的单片系统,但是您可能会设计出一个反模式,并且不能充分利用 Kubernetes 的最佳特性之一,服务。Kubernetes 服务可能由一系列单片和微型应用架构支持。

关于平台 API 架构,Kubernetes 服务与面向对象设计(OOD)中的类的概念进行了比较,尽管这是一个牵强的类比。在微服务架构的世界中,已经有很多思想和理论投入到服务的描述和组织中,这已经超出了本书的范围,但是将服务视为提供对相应动词的访问的名词是平台设计的良好起点。Kubernetes 服务是所有 API 调用、JSON-REST、gRPC、GraphQL、基于二进制或文本的 TCP 和 UDP 的持久网关。事实上,任何可以监听分配给 IP 地址的端口的东西都可以是服务的端点,无论是平台内部还是外部。该平台围绕 Kubernetes 服务进行设计。

摘要

这本书的目的是给你一些例子和灵感,让你利用 Kubernetes 开发高效的、引人注目的软件平台和分布式应用,这些例子结合了一系列技术的能力。本章定义了一种数据驱动的软件平台,旨在结合物联网技术、机器学习和区块链的功能。虽然对特定功能的需求来来去去,但该平台的核心组件以配置、入口、数据管理和指标为中心。这些核心能力形成了基本服务的框架,支持新技术及其与数据关系的无限组合。

基础设施通常是运营的唯一领域。随着近年来开发和运营之间的差距缩小,术语 DevOps 越来越流行。虽然一些组织将 DevOps 视为一个角色,但更准确的表述可能是由许多不同角色执行的一系列职责和任务。DevOps 包括硬件供应、服务器集群的安装和管理、配置管理、持续集成和持续部署等等。关于基础设施的下一章涵盖了基本核心组件所需的开发运维,以及平台的配置管理、集成、运营、监控和维护。

*

二、DevOps 基础设施

Kubernetes 对其底层服务器和网络的物理基础设施进行了抽象。本章涵盖了如何在整个开发生命周期的管理中利用 Kubernetes。本章和下一章从平台功能的具体关注点上稍作休息,将重点放在开发运营(DevOps)上,包括通过设置 GitLab 实例,为在 Kubernetes 上构建平台的团队提供快速有效的集成、测试和部署的重要工具。

Kubernetes 是一种供应商中立的云原生技术,使其在主要的云供应商、裸机数据中心和本地工作站之间高度可移植。根据这一原则,下一章构建了一个利用 Kubernetes 的可移植 DevOps 工具链,对开发和生产的所有方面的配置和管理进行标准化。

云计算

编写可移植软件一直是创新和生产力的主要驱动力。20 世纪 40 年代最早的计算机要求程序员用汇编语言编写,特别是为特定的机器量身定做的。在 20 世纪 50 年代末,FORTRAN 成为第一种成功的商用高级通用编程语言。利用编译器,FORTRAN 可以被一般性地编写来解决特定的问题,到 20 世纪 60 年代早期,已经有 40 多种 FORTRAN 编译器可用。FORTRAN 今天仍然存在,比它最初发布时几乎所有可用的物理基础设施都要长寿。即使以牺牲一些性能为代价,软件开发人员在代码中嵌入的概念和电路中实现的实现之间保留一个抽象层一直是明智的。

虚拟化和云计算是抽象竞赛中的最新参与者,更不用说容器化了,它为混合添加了一种新形式的可移植封装。然而,在抽象物理基础设施的竞赛中,云计算本身可以采取 20 世纪 40 年代专用系统的形式。云供应商已经成为一种专门的基础设施。对于许多希望利用交钥匙解决方案或利用供应商定制解决方案实现的性能的组织来说,专业化可能是可以接受的。云计算的趋势现在正朝着这样一个目标发展,即提供具有供应商中立方法的云原生解决方案,不仅抽象物理基础设施,而且抽象供应商本身。如果 FORTRAN 是可移植应用的第一个答案,那么 Kubernetes 可能是可移植平台的新答案。

数据中心曾经是大型企业的专属领域。1991 年,美国国家科学基金会取消了对现在的网络的商业限制,随之而来的是一群虚拟主机提供商,允许任何每月花费几美元的人加入其全球网络,并在某个远程数据中心的服务器上存储一些静态 HTML 文件。服务器机架很快填满了数据中心,新的数据中心在每个主要城市涌现。互联网催生了运行免费开源 Linux 操作系统的廉价商用服务器的繁荣。您的银行需要通过点对点电信连接的专有通信协议在全球范围内传输数据;你所需要的只是有人访问你的网址,每月只需几美元,你就可以做你的银行花了一大笔钱去做的事情。

早在 20 世纪 90 年代初,连接到互联网的计算机传输数据的方式几乎和现在一样,主要是通过 HTTP。当时,HTTP 提供 HTML 文档、映像和各种文件。Web 服务器在功能和执行代码的能力方面都有所增长,1993 年通过通用网关接口(CGI)实现了标准化。CGI 提供了在简单明了的标准下开发基于 web 的应用的机会。为接收和响应 HTTP 消息而编写的应用可以在几乎任何支持 CGI 的提供者上运行,除了独特的依赖关系。如果这听起来像是一个云原生和厂商中立的解决方案,那么它过去是,现在仍然是。云原生和厂商中立运动背后的需求和想法是对互联网自修复本质的一种修正,将在下一节中讨论。

复杂的 web 应用需要的不仅仅是从 web 服务器执行简单的 CGI 他们通常需要特定数量的 CPU 或 GPU 资源、内存和存储,以及在多个计算实例和多个区域上运行的能力。租赁和维护数据中心的服务器是一项复杂而专业的任务,需要大量的前期规划和长期投入。设置新平台曾经需要大量的人工劳动。像 Google 和 Amazon 这样的公司为计算资源的分配构建了强大的内部平台,以适应他们巨大的工作负载和持续的发布周期。对于新的基于互联网的企业来说,通过提供产品来进一步利用其基础设施投资是有意义的。这些公司通过营销云平台将自己与虚拟主机和主机托管服务区分开来。企业现在可以将这些产品作为其企业应用的另一种选择。云不是简单的网站托管和服务器。现在,它是企业部署和运行所有业务应用的另一种选择。从概念上讲,云是一种平台即服务(PaaS)。

传统的云不是像 HTTP 或 CGI 那样的标准,传统的云不是操作系统,应用不能在其上本地编译和执行。要掌握云,你需要选择一个,并可能在亚马逊 AWS、微软 Azure 或谷歌云中寻求证书。然而,这些主要提供商所提供的特性几乎没有区别,只有它们的专有实现。当一个供应商发布一个特定的特性时,其他供应商会很快跟进。这种竞争对于创新来说是非常好的,主要供应商是一些将尖端技术推向市场的领先创新者。随着创新而来的是专有的 API,如果你在架构的核心利用这些 API,你的系统就被认为是供应商锁定的。此外,越来越多的企业开始寻求混合云解决方案,试图利用多家云提供商的优势;这可能意味着更好的多样化,或者不幸的是,多个供应商锁定。

这本书是关于在 Kubernetes 中构建平台,并以一种可以在 Amazon、Google、IBM、Microsoft 或私人数据中心的定制服务器上运行的方式实现,或者所有这些服务器的组合。Kubernetes 可以在任何支持通用计算实例的云上运行,这就是它们的全部。然而,即使这样也不再必要,因为主要的云提供商现在提供 Kubernetes 即服务,这是一个完全云原生和供应商中立的选择。

云原生和厂商中立

互联网的早期发展很快,主要是由于开源技术和开放标准。云计算通过将物理基础设施抽象为一组 API 调用,解决了高效管理计算资源的问题。第一个主要的云产品是一种叫做弹性计算云(EC2)的按需效用计算。2006 年,随着 EC2 平台的发布,亚马逊第一个让云计算这个术语流行起来。谷歌在 2008 年发布了 Google App Engine,2010 年微软推出了 Azure。这些主要的云提供商现在提供的不仅仅是虚拟化计算实例;它们是功能全面的 PaaS 和 SaaS 解决方案,每天都会发布新的特性和功能。主要的云提供商解决了初创公司和新开发项目的问题,他们需要一种快速且经济高效的方法来加速和管理计算资源,并将他们的业务需求与按需解决方案联系起来。利用这些产品的架构解决方案可以通过降低开发成本和缩短上市时间带来许多前期优势;然而,它们也带来了供应商锁定的问题。

当您的应用或业务流程需要大量工作才能移植到新的平台或供应商时,就会出现供应商锁定。如果您的应用或平台对您的业务运营至关重要,那么与另一个组织的未来稳定性和战略决策紧密联系在一起可能会带来巨大的风险,或者最多会带来任意的技术限制。长期合同和服务水平协议(SLA)仅在合同上保证稳定性,但由第三方管理的技术总是容易出现意外故障和贬值。在某些情况下,对一家企业来说,搬楼或搬迁到另一个州比重构已经在 IBM、微软、亚马逊或谷歌提供的专业平台中根深蒂固了十年或更长时间的业务逻辑更容易。对于一些组织来说,供应商锁定不一定是问题,尤其是在支持或扩展传统云服务的业务中的增值经销商(var)。然而,如果您的企业只希望利用云服务,那么您的架构最好采用云原生的供应商中立的方法,Kubernetes 提供了这种方法。

裁员

2015 年 9 月,亚马逊网络服务(AWS)在美国东部 1 区遭遇了 5 个小时的宕机。IMDb、Tinder、Airbnb、Nest 和许多其他知名组织的网站和应用遭受了长达八小时的有限或完全系统中断。然而,网飞是自动气象站的另一个重要用户,只遭受了暂时的中断。网飞正确地设计了体系结构,以防范与他们无法完全控制的基础架构相关的关键业务运营的风险。冗余是一个合理的原则,应该是任何关键系统架构设计的一部分;不幸的是,许多系统依赖传统的云从它们那里抽象出这一点。

不难假设亚马逊的 AWS 产品有大量冗余组件;亚马逊 99.95%的正常运行时间服务水平协议要求这样做。然而,真正的冗余应该是你可以控制的,而不是基于信仰对第三方的依赖。2015 年,网飞不用再耐心等待亚马逊恢复美东一区;网飞通过其内部冗余架构制定了故障应急计划。但是,如果导致 AWS 宕机的问题已经蔓延到其他地区,那该怎么办呢?

在几乎任何云提供商上建立和运行基于 Kubernetes 的平台的能力可以提供大规模跨云规模的冗余,并提供最终的应急计划。基于 Kubernetes 的平台利用了云的最基本价值,即按需计算实例,只要考虑得当,它可以保持供应商中立。

便携式平台

许多组织没有兴趣或没有能力购买和维护遍布全国甚至全球的大量物理基础设施。云计算已经成为灵活且经济高效地运行复杂工作负载的一个重要方面。云原生的概念意味着利用这个由短暂的计算实例、存储和网络组成的世界,其中的工作从实现形成所需状态的过程转移到仅仅描述它。

2017 年,亚马逊宣布有意收购杂货零售商全食超市(Whole Foods)。随着亚马逊向实体零售业的扩张,其他零售商开始担心亚马逊现在会被视为直接竞争对手。在给美国消费者新闻与商业频道的一封电子邮件中,沃尔玛发言人 Dan Toporek 说:“我们的供应商可以选择使用任何满足他们和他们客户需求的云提供商。在有些情况下,我们不希望自己最敏感的数据放在竞争对手的平台上,这不足为奇。" 3 沃尔玛并不担心其供应商使用 AWS 的成本或稳定性;出于战略商业原因,他们不愿意这么做。与 AWS 紧密相关的解决方案可能失去了与沃尔玛合作的机会,或者可能被使用微软或 IBM 云服务的竞争对手放弃。供应商中立的想法意味着,如果只需要在备用云上建立一个独立的 Kubernetes 集群,就不需要错过与沃尔玛这样的公司做生意的机会。

Kubernetes,以及不断增长的应用、框架和概念生态系统,正在通过开发云本地和厂商中立的应用来解决问题。如果一个应用可以在 Kubernetes 的通用安装上运行,那么它很可能可以在任何地方运行,只需要对定制网络和存储接口的配置进行最小的更改。那么,云供应商是否担心这项新技术会如此迅速地实现供应商中立的方法呢?Kubernetes 不仅能够在每一个主要的云上安装和运行,令人惊讶的是,所有传统的云提供商现在都提供 Kubernetes 即服务。

选择操作平台的位置超出了本书的范围。如果你有 AWS 的经验,你可能会对 Kubernetes(亚马逊 EKS)的亚马逊弹性容器服务感兴趣。如果你对 Azure 感到满意,你应该探索 Azure Kubernetes 服务(AKS)。IBM 提供 IBM Cloud Kubernetes 服务,Google 提供 Google Kubernetes 引擎(GKE)。

供应商中立入门

虽然 Kubernetes 本身是供应商中立的,但来自 Google、Microsoft 和 Amazon 等供应商的托管 Kubernetes 服务通常包含特定于供应商的要求,通常围绕身份和访问管理。如果构建一个可移植的、厂商中立的平台是一个目标或业务需求,那么只使用主要云提供商的通用计算实例来安装一个定制/普通的 Kubernetes 集群可能是有益的。否则,明确记录用于特定供应商的非标准配置提供了一个供应商中立的路线图,如果可移植性或多云支持是未来的关注点。

使用 Kubernetes 开发平台意味着它应该能够在本地工作站、私有云、具有通用计算实例的公共云或公共云 Kubernetes 服务上运行。有大量的书籍和在线教程可以为各种环境和供应商设置和配置 Kubernetes,从点击几下 AWS 或 Google 到 Kelsey Hightower 的“Kubernetes The Hard Way”。这本书假设你对 Kubernetes 和开发操作有一定的经验,并且只介绍基本的设置。

开发工具链和环境能够并且通常应该具有与它们支持的平台相同的可移植性。接下来的部分“DevOps Toolchain”使用开源技术组装了一个有效的开发操作流水线和工作流,该技术与一个完全独立于供应商的云原生方法兼容。虽然传统的云提供商提供了一些集成的开发解决方案,但本书的目标不仅是建立一个支持区块链、机器学习和物联网数据管理解决方案的供应商中立平台,而且还尽可能保持整个开发工具链的可移植性和灵活性。

奉献工具链

应用平台的存在是为了提高开发过程的生产率。他们经常超越对复杂子系统的抽象访问。成熟的应用平台建立约定,提供可观察性的方法,并促进特定的架构设计模式。为 iOS 或 Windows 构建应用或扩展 SharePoint 或 Salesforce 等 CMS 系统功能的开发人员拥有完善的生态系统,包括在这些平台上进行开发的工具和方法。然而,Kubernetes 的核心只是一个健壮的容器编排系统,使其成为开发大型企业平台的优秀环境。Kubernetes 是一个比 Linux 或 Windows 开发更高层次的问题,比扩展流行的内容管理系统低得多。Kubernetes 留给开发人员的只有一个问题,应用配置。已经开发了一些工具来简化复杂配置的管理,但是这些工具可能会混淆 Kubernetes 核心租户——声明式配置的简单性。本书中用于配置、管理和开发 Kubernetes 的核心工具是kubectl,它建立了一个单一的标准,在需要时可以在这个标准上引入更复杂的工具。

稍后,在“开发环境”一节中建立了一个小型的 Kubernetes 集群。在这个新的开发集群中,存储库、注册中心和 CI/CD 是组织、配置和维护清单、代码和支持平台开发的容器映像的基本组件。这些概念中的每一个都有简要介绍,本节以在单节点 k3s 6 集群上安装 GitLab 5 结束(参见图 2-1 )。

img/483120_1_En_2_Fig1_HTML.jpg

图 2-1

工具链

仓库

存储库用于存储、分发和管理源代码,特别是本书中开发的平台中的 YAML 文件。Kubernetes 不依赖开发人员管理存储库中的配置;然而,随着任何平台的发展,越来越多的 YAML 清单需要一个组织良好的系统来管理它们。Git 已经成为源代码管理和版本控制的行业标准。Git 存储库是分布式的和可移植的,并且符合本书所追求的供应商中立的标准。Git 无处不在,这在很大程度上要归功于 GitHub 的流行。GitHub 托管了几乎所有主要的开源项目,然而 GitHub 只是 Git 标准特性之上的一个增值。

尽管这本书关注的是与在 Kubernetes 中运行应用相关的 YAML 配置的开发,但是源代码库在开发操作的每个层面都扮演着核心角色。基础设施即代码(infra structure-as-code)7(IaC)技术,如 Terraform、Ansible、Puppet、Chef 和新的集群 API,通过代码抽象底层基础设施的构建和维护,并极大地受益于 GitHub 和 GitLab 等管理良好的源代码库和管理系统 8

这本书不仅推广了 Kubernetes 的厂商中立和云原生的平台开发方法,还推广了支持它的整个工具链。

注册

Kubernetes 运行和管理容器。映像是包含创建运行容器所需的可执行代码和配置的文件。到目前为止,Docker 是构建容器映像最流行的选择,因此本书主要关注 Docker 及其附带的应用和实用程序生态系统。容器映像注册中心是开发和维护 Kubernetes 平台的关键组件。注册中心负责维护容器映像的版本和分发。本书中的平台从包括公共 Docker Hub 在内的几个注册中心提取容器。Docker Hub 免费托管公共容器;然而,许多组织,比如 Elastic,选择自己托管它们的公共容器注册中心。本书中创建的公共容器使用 Docker Hub,私有容器使用 GitLab 内置的 Docker 注册表。将平台使用的所有容器镜像到私有注册表中,增加一层额外的控制和安全性,这可能也是明智的(见图 2-2 )。

在私有注册表中构建和托管容器可以确保对映像的更大信任。但是,从基础映像构建新映像是很常见的,包括 Ubuntu、Alpine 或 CentOS 等发行版。几乎所有软件最终都包含常见漏洞和暴露(CVE)。用于检测 CVEs 和其他安全风险的解决方案,例如 CoreOS 的 Clair、 9 可以直接集成到构建流水线中。

img/483120_1_En_2_Fig2_HTML.jpg

图 2-2

容器映像注册表

CI/CD

对 Kubernetes 的持续集成和部署对于高效的平台开发和稳定的产品发布是必不可少的。有大量的商业 CI/CD 产品,其中大多数与 Kubernetes 配合得很好。开源应用 GitLab 有一个稳定成熟的 CI/CD 组件,能够在 Kubernetes 集群中直接执行构建操作和测试。与许多其他服务相比,针对集群中运行的现有服务测试代码的能力是一个巨大的优势。开发复杂平台的过程需要许多不同服务的操作和通信,并且在隔离的集成和测试环境中,开发与多个服务通信的定制组件可能具有挑战性。下面展示了一个来自开发 Kubernetes 集群的 GitLab Runner 构建和测试代码(见图 2-3 )。

img/483120_1_En_2_Fig3_HTML.jpg

图 2-3

集群内持续集成、测试和部署

DevOps 的 GitLab

Git 只是一个版本控制系统,需要额外的工具来支持更大的开发操作(DevOps)关注点。GitHub 是迄今为止最受欢迎的开源项目托管库,尽管在开源社区中非常受欢迎和信任,但 GitHub 本身不是开源的,如果您希望避免供应商锁定,最好不要依赖 GitHub 的增值功能来开发工具链中的关键组件。这本书利用 GitHub 作为公众访问开源组件的一面镜子。但是,这本书使用 GitLab 进行持续集成和持续部署。像 GitHub 一样,GitLab.com 提供从免费到企业的托管计划。与 GitHub.com 不同,GitLab 是开源的,可以完全在你控制的基础设施上安装和运行。

“GitLab Community Edition (CE)是一个开源的端到端软件开发平台,内置了版本控制、问题跟踪、代码审查、CI/CD 等功能。在您的裸机服务器、虚拟机、容器或云提供商上自托管 GitLab CE。” 10 GitLab 可以在独立实例上运行,也可以在 Kubernetes 集群内部运行;然而,无论哪种情况,GitLab 都可以通过设置能够执行测试和部署活动的远程运行程序来利用现有的 Kubernetes 集群。这本书利用 GitLab 来维护 Kubernetes 配置、持续部署和服务私有应用容器。下面的练习介绍了如何在一个小型计算实例上设置 GitLab,并能够利用“开发环境”一章中组装的 Kubernetes 集群。

GitLab 不需要 Kubernetes,直接在服务器上或服务器上的 Docker 容器中运行良好。然而,对于正在开发的平台和支持它的工具来说,统一的控制面板是有价值的。虽然一个完全生产就绪的 Kubernetes 集群对于许多小型孤立的应用来说可能是多余的,但是有越来越多的符合 Kubernetes 的解决方案专注于低资源单节点集群。下一节将介绍如何将 GitLab 安装到单节点 k3s 11 集群中。

k3b+gitlab

k3s 是 40MB 二进制文件,运行“完全兼容的生产级 Kubernetes 发行版”,只需要 512MB 内存。

k3s 是一个很好的方法,可以包装那些您可能不想在完整的生产集群中运行,但希望在所有开发操作中实现系统部署、监控和管理的更大一致性的应用。GitLab 在本书中的平台开发运营中起着核心作用,k3s 将与其他用于物联网设备和现场设备的单节点解决方案一起使用。使用 k3s 托管 GitLab 是熟悉单节点集群和 Kubernetes API 下统一管理平台的额外好处的好方法。

下面概述了在 Vultr 上的单节点定制 Kubernetes (k3s)集群中设置 GitLab 应用的过程。Vultr 被选为亚马逊、谷歌或微软等主要云提供商的替代产品。Vultr 以及像 Digital Ocean、 13 Linode、 14 OVH、 15 Hetzner、 16 或 Scaleway、 17 这样的提供商提供了一种快速建立廉价的通用计算(虚拟机)实例的好方法,而无需深入研究专门的工作流和帐户管理然而,一旦在任何提供者上设置了通用虚拟机,在实现上就没有什么区别了。

服务器设置

Vultr、Scaleway 和 Hetzner 是开发实验性集群的绝佳选择,并且通常以主要公共云的一小部分成本提供虚拟服务器。价格中通常包括固态硬盘存储和足够的网络传输量。本章演示了如何在 Vultr 上建立 Kubernetes 开发集群;然而,下面的说明很容易翻译给其他提供者。

在 Vultr 上注册一个帐户,或者选择一个等效的替代方案。本练习要求 Ubuntu 18.04 服务器至少有两个 CPU 内核和 4096MB 内存。

首先,选择一个服务器位置(参见图 2-4 )。选择一个在地理位置上靠近团队大多数成员的服务器位置。虽然 GitLab 将能够与世界上任何地方的 Kubernetes 集群进行通信,但选择一个离您的大多数团队最近的城市中的实例将减少开发操作的网络延迟。

img/483120_1_En_2_Fig4_HTML.jpg

图 2-4

服务器位置

接下来,选择 Ubuntu 18.04 的服务器类型(见图 2-5 )。“Ubuntu 是一个基于 Debian 的免费开源 Linux 发行版”,并得到 Canonical Ltd .的大力支持,Ubuntu 18.04 是一个长期支持版本,将一直支持到 2028 年。

img/483120_1_En_2_Fig5_HTML.jpg

图 2-5

服务器类型

接下来,选择服务器尺寸(参见图 2-6 )。k3s 上的 GitLab 将需要至少两个 CPU 核心和 4086MB 内存才能高效运行,以支持一个小型团队。

img/483120_1_En_2_Fig6_HTML.jpg

图 2-6

服务器大小

最后,给新实例一个主机名和一个您拥有的域。Vultr 和许多提供商一样,如果您在预配置配置中提供了主机名,它会为服务器提供预配置的主机名(参见图 2-7 和图 2-8 )。)

img/483120_1_En_2_Fig7_HTML.jpg

图 2-7

vultr 仪表板

配置 DNS

这本书对所有的例子都使用了域apk8s.dev。将 apk8s.dev 替换为您的域,只要它出现在以下文本中。

apk8s.dev(见图 2-9 )添加至少两条 DNS A 记录,指向新服务器的公共 IP 地址。有关具体说明,请咨询您的域/DNS 提供商。图 2-9 显示了新 Vultr 实例的公共 IP。

img/483120_1_En_2_Fig9_HTML.jpg

图 2-9

DNS 条目

img/483120_1_En_2_Fig8_HTML.jpg

图 2-8

Vultr 仪表板

安装 k3s

打开 Vultr 上的服务器信息页面(见图 2-10 )并找到 root 密码。

img/483120_1_En_2_Fig10_HTML.jpg

图 2-10

服务器信息

登录到新服务器并升级任何过期的软件包。升级软件包可确保新服务器配备最新的安全性、错误修复和性能改进。

$ ssh root@PUBLIC.IP.ADDRESS

$ apt update && apt upgrade -y

使用curl下载 k3s 安装程序,并通过流水线将其传输到 shell 以供执行:

$ curl -sfL https://get.k3s.io | sh -

使用 kubectl 测试新的 k3s (Kubernetes)安装:

$ kubectl get Nodes
NAME               STATUS   ROLES    AGE    VERSION
gitlab.apk8s.dev   Ready    <none>   5m     v1.14.1-k3s.4

远程存取

k3s 现在已经安装在新的服务器上,一个监听端口 6443 的 Kubernetes API 已经准备好接受连接。远程访问群集的凭据位于/etc/rancher/k3s/k3s.yaml

如果您在本地工作站上安装了 ku bectl18,请注意新 k3s 节点上的/etc/rancher/k3s/k3s.yaml文件是一个kubectl配置文件,类似于本地工作站上由kubectl生成的~/.kube/config文件。配置文件包含三个主要部分:集群、用户和上下文,每个部分都包含一个与新集群相关的条目。将 k3s 服务器上的k3s.yml中的每个条目添加到本地工作站上的~/.kube/config文件中,用一个合理的替代词替换单词default,例如gitlab (参见图 2-11 ,并用服务器的公共 IP 地址替换集群部分中的localhost

img/483120_1_En_2_Fig11_HTML.jpg

图 2-11

Kubernetes 配置

向本地工作站上现有的~/.kube/config文件添加新的集群、用户和上下文是配置kubectl的一种简单方法。kubectl支持使用多个配置文件和其他可选的配置方法;有关这方面的更多信息,请参见kubectl上的文档。kubectl是本书通篇使用的中心应用;在使用多个集群时,组织良好的配置至关重要。

安装证书管理器/让我们加密

k3s 是一个全功能的 Kubernetes 集群,预配置了 Traefik 入口控制器,可随时处理入站 HTTP 请求。

Cert Manager 19 “是 Kubernetes 的一个附加组件,用于自动管理和发布来自各种发布源的 TLS 证书。”以下步骤安装并配置 Cert Manager,以使用 Let's Encrypt 生成免费的 TLS 证书,用于通过 HTTPS 保护 GitLab 实例。

GitLab 提供了加密功能;但是,由于我们是通过 k3s (Kubernetes) Ingress(使用 Traefik)运行 GitLab 的,所以我们需要从集群中生成 Certs 并提供 TLS。

创建证书管理器的自定义资源定义:

$ kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.8/deploy/manifests/00-crds.yaml

接下来,创建一个名为00-cluster的目录,用于存储集群范围的配置;在新目录中,创建具有以下配置的文件00-cert-manager-helm.yml(参见清单 2-1 )。

Helm 是在 Kubernetes 中安装和维护应用的常用工具。Helm 自称是一种包管理器,它抽象出了在 Kubernetes 中运行应用所需的大量配置。这个抽象是代表 Kubernetes 对象的模板化 YAML 文件的集合。Helm 渲染并应用这些模板,其中填充了用户提供的值。通常使用 Helm 作为命令行实用程序;但是,k3s 包含自定义资源定义(CRD) HelmChart,并且在应用时,在内部安装或更新 Helm 部署。

Note

本书中配置文件前面的数字旨在表示应用它们的顺序或表示一系列依赖关系。其他组织方法包括将多个配置放在同一个文件中,这会使大型配置难以导航。本书中表达的大多数 Kubernetes 对象都是作为单独的文件编写的,由目录组织。

apiVersion: k3s.cattle.io/v1
kind: HelmChart
metadata:
  namespace: kube-system
  name: cert-manager
spec:
  chart: cert-manager
  repo: https://charts.jetstack.io
  targetNamespace: cert-manager

Listing 2-1Cert Manager

应用配置:

$ kubectl apply -f 00-cert-manager-helm.yml

确保证书管理器现在正在cert-manager命名空间中运行:

img/483120_1_En_2_Fig12_HTML.jpg

图 2-12

证书管理器资源

$ kubectl get all -n cert-manager

接下来,需要一个配置为从 Let's Encrypt 中检索 TLS 证书的 ClusterIssuer。稍后,添加了一个证书配置,它利用了这个名为letsencrypt-production的 ClusterIssuer。

用清单 2-2 中的配置创建文件05-cluster-issuer.yml。用有效的电子邮件地址替换YOUR_EMAIL_ADDRESS

apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
  name: letsencrypt-production
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: YOUR_EMAIL_ADDRESS
    privateKeySecretRef:
      name: letsencrypt-production
    # Enable the HTTP-01 challenge provider
    http01: {}

Listing 2-205-cluster-issuer.yml

应用配置:

$ kubectl apply -f 05-cluster-issuer.yml

安装 GitLab

名称空间gitlab用于包含 GitLab 应用。在与00-cluster相同的层级上创建另一个名为01-gitlab的目录。新目录01-gitlab用于存储剩余的配置文件,以它所代表的名称空间命名。

命名空间

用清单 2-3 中的配置创建文件00-namespace.yml

Note

本书中针对某些配置的命令性命令将节省时间,并且需要较少的前期工作;例如,kubectl create namespace gitlab是一个步骤——然而,声明性配置具有显式文档化和所需状态版本化的额外好处。这个名称空间配置很简单,但是当需要添加标签和资源限制时,可能会有更复杂的配置。

apiVersion: v1
kind: Namespace
metadata:
  name: gitlab

Listing 2-3GitLab Namespace

应用配置:

$ kubectl apply -f 00-namespace.yml

TLS 证书

确保您的域名的两个 DNS A 记录指向新群集的公共 IP。在本例中,gitlab.apk8s.dev*.gitlab.apk8s.dev都解析到新的集群。以下证书配置使用之前创建的 ClusterIssuer letsencrypt-production为这些域生成一个有效的证书。

以下练习创建一个描述多个域的证书资源;cert-manager 使用此证书资源生成一个 TLS 密钥对,并用它填充一个密码。此证书中列出的域的任何入口都可以使用生成的机密作为有效的 TLS 证书。此外,cert-manager 提供了一种替代方法,可以通过注释在入口资源中直接请求证书。 二十

用清单 2-4 中的配置创建文件05-certs.yml

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: gitlab-apk8s
  namespace: gitlab
spec:
  secretName: gitlab-apk8s-tls
  issuerRef:
    name: letsencrypt-production
    kind: ClusterIssuer
  commonName: gitlab.apk8s.dev
  dnsNames:
    - gitlab.apk8s.dev
    - reg.gitlab.apk8s.dev
  acme:
    config:
      - http01:
          ingressClass: traefik
        domains:
          - gitlab.apk8s.dev
          - reg.gitlab.apk8s.dev

Listing 2-4GitLab TLS Certificate

应用配置:

$ kubectl apply -f 05-certs.yml

检查证书的状态:

$ kubectl describe certificate -n gitlab

如果“证书发布成功”,那么现在在包含密钥tls.crttls.keygitlab名称空间中就有了秘密gitlab-apk8s-tls,它们组成了 TLS 证书。这个秘密是一种由 Ingress 用来保护 HTTPS 流量的kubernetes.io/tls

服务

在该配置中定义了服务gitlabgitlab-ssh。gitlab 服务通过公开 GitLab 网站的端口 80 ( gitlab:80)和容器注册表的端口 5050 ( gitlab:5050),连接到与标签app: gitlab匹配的 pod 以及端口 80 和 5050(稍后在部署中定义),为入口提供后端服务。服务gitlab-ssh公开了 git ssh 协议使用的节点端口 32222,用于 git 克隆、推送和拉取等操作。

Note

节点端口在整个集群中公开。

用清单 2-5 中的配置创建文件10-services.yml

应用配置:

img/483120_1_En_2_Fig13_HTML.jpg

图 2-13

GitLab 服务

$ kubectl apply -f 10-services.yml

apiVersion: v1
kind: Service
metadata:
  name: gitlab
  namespace: gitlab
  labels:
    app: gitlab
spec:
  selector:
    app: gitlab
  ports:
    - name: http-web
      protocol: "TCP"
      port: 80
      targetPort: 80
    - name: http-reg
      protocol: "TCP"
      port: 5050
      targetPort: 5050
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: gitlab-ssh
  namespace: gitlab
  labels:
    app: gitlab-ssh
spec:
  selector:
    app: gitlab
  ports:
    - name: tcp-git
      protocol: "TCP"
      targetPort: 22
      port: 32222
      NodePort: 32222
  type: NodePort

Listing 2-5GitLab Services Configuration

ConfigMap(配置地图)

Kubernetes 配置图用于管理主 GitLab 配置文件gitlab.rb。这个示例配置只定义了启动和运行 GitLab 的最小操作实例以及内置容器注册表所需的设置。docs.gitlab.com 上有一份详细的配置选项列表,并有详细的文档记录。 21 确保将initial_root_password设置为强密码;GitLab 在初始设置时使用该设置为名为 root 的初始管理员用户提供已配置的密码。

用清单 2-6 中的配置创建文件20-configmap.yml

应用配置:

apiVersion: v1
kind: ConfigMap
metadata:
  name: gitlab-config
  namespace: gitlab
data:
  gitlab.rb: |-
    gitlab_rails['gitlab_shell_ssh_port'] = 32222
    prometheus['monitor_kubernetes'] = false

    gitlab_rails['initial_root_password'] = "password"

    external_url 'https://gitlab.apk8s.dev'

    nginx['listen_port'] = 80
    nginx['listen_https'] = false
    nginx['proxy_set_headers'] = {
      'X-Forwarded-Proto' => 'https',
      'X-Forwarded-Ssl' => 'on'
    }

    registry_external_url 'https://reg.gitlab.apk8s.dev'

    gitlab_rails['registry_enabled'] = true

    registry_nginx['listen_port'] = 5050
    registry_nginx['listen_https'] = false
    registry_nginx['proxy_set_headers'] = {
      'X-Forwarded-Proto' => 'https',
      'X-Forwarded-Ssl' => 'on'
    }

Listing 2-6GitLab configuration

$ kubectl apply -f 20-configmap.yml

部署

部署将 GitLab 应用定义为单个 Pod,因为它运行在单节点集群上,所以我们可以使用hostPath在服务器上挂载目录,为 Pod 提供持久存储。在服务器上自动创建目录/srv/gitlab/。所有的配置和数据都作为文件保存在服务器上,而gitlab-configmap-volumegitlab.rb的内容挂载之前创建的配置图。

用清单 2-7 中的配置创建文件40-deployment.yml

应用配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gitlab
  name: gitlab
  labels:
    app: gitlab
spec:
  replicas: 1
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: gitlab
  template:
    metadata:
      labels:
        app: gitlab
    spec:
      containers:
        - name: gitlab
          image: gitlab/gitlab-ce:11.10.4-ce.0
          imagePullPolicy: IfNotPresent
          volumeMounts:
            - name: config-volume
              mountPath: /etc/gitlab
            - name: logs-volume
              mountPath: /var/log/gitlab
            - name: data-volume
              mountPath: /var/opt/gitlab
            - name: reg-volume
              mountPath: /var/opt/gitlab/gitlab-rails/shared/registry
            - name: uploads-volume
              mountPath: /var/opt/gitlab/gitlab-rails/uploads

            - name: gitlab-configmap-volume
              mountPath: /etc/gitlab/gitlab.rb
              subPath: gitlab.rb
          ports:
            - name: http-web
              containerPort: 80
            - name: tcp-ssh
              containerPort: 22
            - name: http-reg
              containerPort: 5050
      volumes:
        - name: gitlab-configmap-volume
          configMap:
            name: gitlab-config
        - name: config-volume
          hostPath:
            path: /srv/gitlab/config
        - name: logs-volume
          hostPath:
            path: /srv/gitlab/logs
        - name: data-volume
          hostPath:
            path: /srv/gitlab/data
        - name: reg-volume
          hostPath:
            path: /srv/gitlab/reg
        - name: uploads-volume
          hostPath:
            path: /srv/gitlab/uploads

Listing 2-7GitLab Deployment

$ kubectl apply -f 40-deployment.yml

进入

k3s 预先配置了 Traefik 入口控制器,用于管理对 HTTP 端口 80 和 HTTPS 端口 443 的请求。清单 2-8 是一个基本的 Kubernetes 入口配置,还添加了一个注释,请求 Traefik 将所有 HTTP 请求路由到 HTTPS。

用清单 2-8 中的配置创建文件50-ingress.yml

应用配置:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gitlab
  namespace: gitlab
  labels:
    app: gitlab
  annotations:
    traefik.ingress.kubernetes.io/redirect-entry-point: https
spec:
  rules:
    - host: gitlab.apk8s.dev
      http:
        paths:
          - backend:
              serviceName: gitlab
              servicePort: 80
            path: /
    - host: reg.gitlab.apk8s.dev
      http:
        paths:
          - backend:
              serviceName: gitlab
              servicePort: 5050
            path: /
  tls:
    - hosts:
        - reg.gitlab.apk8s.dev
        - gitlab.apk8s.dev
      secretName: gitlab-apk8s-tls

Listing 2-8GitLab Ingress

$ kubectl apply -f 50-ingress.yml

禁用注册

默认情况下,GitLab 允许新用户注册账户。然而,这里安装的实例是供在本书后面定义的软件平台上工作的开发团队内部使用的。禁用注册选项需要管理员(root)用户在用户界面的管理部分配置此功能。参见图 2-14 。

img/483120_1_En_2_Fig14_HTML.jpg

图 2-14

禁用 GitLab 注册

摘要

使用 k3s 在单节点 Kubernetes 集群上设置 GitLab 有几个好处。k3s 和不断增长的小型 Kubernetes 分布生态系统正在扩大在任何规模上利用 Kubernetes 的机会。将 GitLab 包装在 Kubernetes 中提供了以与其他基于 Kubernetes 的平台相同的方式扩展 git lab 的能力,只需添加能够在服务网格内交互的额外的容器支持的服务。GitLab 不仅通过在集群中安装和操作 CI/CD 容器的能力与外部集群交互,GitLab 本身现在与在其中开发的平台处于相同的控制面板下。

后续步骤

既然 GitLab 已经在单节点 Kubernetes 集群中启动并运行,下一章将建立一个开发 Kubernetes 集群,代表其最终产品的缩小版本。GitLab 利用该集群进行软件集成、构建、测试和部署操作。

三、开发环境

开发一个支持区块链、机器学习和物联网数据管理等组件的平台至少需要 Kubernetes 集群提供的三个基本功能领域:入口、TLS 和持久数据存储。本章建立了一个满足平台开发最低要求的小规模开发集群。以下群集可以根据需求增加资源,轻松扩展节点数量和每个节点的大小。

许多 Kubernetes 教程使用 Minikube 的本地副本引导您完成示例。Minikube 只需要在本地工作站上消耗一点 CPU 和内存。然而,一个健康的开发环境应该反映生产环境的缩小版本,尽管 Minikube 对于实验和一些本地开发来说很棒,但是它在反映生产环境的挑战方面能力有限。开发集群可能需要在远程开发人员之间共享,或者可供外部资源用于收集数据、指标和事件;这对于基于本地工作站的 Kubernetes 安装来说是一个挑战。如果您真的想在 Kubernetes 上测试您的平台的云原生和厂商中立方面,那么在一个厂商上有一个开发环境,在另一个厂商上有一个测试和生产环境可能是有益的。开发在基于通用虚拟机(VM)的集群上运行的应用,然后发布到反映您的生产集群的测试环境中,可以确保您在供应商之间的可移植性,只需要最小的配置差异,并且没有重大的架构更改。

定制开发 Kubernetes 集群

今天有几个生产就绪的,交钥匙的 Kubernetes 产品,如谷歌 Kubernetes 引擎(GKE),亚马逊 Kubernetes 弹性容器服务(亚马逊 EKS),Azure Kubernetes 服务(AKS),和 IBM 云 Kubernetes 服务。对于商业平台的生产部署来说,这些选项可能是一个安全的选择,这些提供商提供了深度集成到他们更广泛的服务套件中的机会。为了保持平台的可移植性和厂商中立性,一个定制的、自我管理的 Kubernetes 集群可以为开发提供一个经济有效的中立环境。将开发分离到一个定制的 Kubernetes 集群中对平台开发人员来说有很多好处,迫使他们更深入地理解底层基础设施,同时又不会失去抽象的好处。定制 Kubernetes 集群上的平台开发确保了可移植性,并通过与多个供应商的兼容性提供了冗余机会。

在前一章中,GitLab 是在运行 k3s 的单个 Vultr 实例上设置的。Kubernetes 的开发是为了在数百甚至数千个节点上调度和管理容器工作负载,而 k3s 仅在单个节点上利用其特性。设置一个定制的开发 Kubernetes 集群涉及多个服务器,一个(或多个)主节点,以及一个或多个工作节点。

下一节将更深入地研究 Kubernetes 节点,并介绍在 Digital Ocean 上设置自定义 Kubernetes 集群的过程。像 Vultr 一样,Digital Ocean 提供了他们称之为 Droplets 的廉价计算实例。设置定制 Kubernetes 的说明可以很容易地转换到任何提供通用计算实例或虚拟或裸机的替代服务中。对于为关键的、备受瞩目的生产平台运行定制 Kubernetes 集群的可管理性和成本效益,一直存在争议。然而,当您最终拥有一个可工作的开发环境,并且很可能拥有一个备份或冗余版本以用于生产时,通过安装、配置和维护定制集群所获得的知识和技能会带来巨大的好处。

节点

Kubernetes 集群由节点组成。一个或多个主节点,从 Kubernetes 1.14 开始,总共不超过 5000 个节点。如果您在 1U 服务器上运行所有 5000 个节点,您将占用一个典型数据中心的 100 多个机架,这还不包括路由器、电源管理和所有其他所需的基础设施。Kubernetes 不仅从平台中抽象出单个服务器,而且一些联合的 Kubernetes 集群也可以很好地抽象出一个小型数据中心。

Kubernetes 集群的初始规模是一个生产问题,最好通过运行已开发应用平台的测试或开发集群的性能以及预计利用率来计算。扩展 Kubernetes 就像添加计算资源并将其作为节点加入一样简单。需要注意的是,随着集群的增长,任何主节点的大小都可能需要调整。然而,对于本书中的小型开发集群设置,一个小型的单个主节点就足够了。

Note

Kubernetes 集群中的单个主节点不一定是整个集群的单点故障。Kubernetes 主节点的故障是一个紧急问题;然而,网络和容器工作负载(不依赖于 Kubernetes API)继续在没有 Kubernetes 控制面板的情况下运行。 1

本书中的示例利用了本节中设置的一个小型开发集群,从一个主节点和两个工作节点开始(见图 3-1 )。前一章使用 k3s 操作一个缩小的主节点来运行工作负载。但是,在多节点集群中,主节点应该只负责管理集群。

img/483120_1_En_3_Fig1_HTML.jpg

图 3-1

Kubernetes Nodes

服务器设置

本节将在 Digital Ocean 上设置三台 Ubuntu 18.04 x64 服务器(Droplets),最低硬件规格为 2 个 CPU 和 2 GB RAM。

Note

当未来需求可能需要更多 CPU 或 RAM 用于主节点或工作节点时,数字海洋允许液滴按比例增加。

建立帐户并登录数字海洋后,点击创建按钮,并从出现的菜单中选择水滴。通过选择分布下的 Ubuntu 18.04 x64 为水滴选择映像(参见图 3-2 )。

img/483120_1_En_3_Fig2_HTML.jpg

图 3-2

产生水滴

接下来,在 Standard starter plans 下,找到并选择带有 2 GB/2 个 CPU 的选项,如图 3-3 所示。在撰写本文时,带有这些选项的 Droplet 的成本是每月 15 美元(0.022 小时)。在这个价位上,这里组装的开发集群每月花费 45 美元(每小时 0.066 美元)。

img/483120_1_En_3_Fig3_HTML.png

图 3-3

选择一个计划

接下来,选择一个离您或您的开发团队最近的数据中心区域。数字海洋为一个地区提供了一个或多个选择。图 3-4 显示旧金山/2 被选中。

img/483120_1_En_3_Fig4_HTML.jpg

图 3-4

选择一个地区

接下来,选择附加选项专用网络和监控(参见图 3-5 )。Kubernetes API 后来使用私有网络在节点之间进行私有通信。此外,对于 Digital Ocean 和大多数提供商,通过专用网络进行的数据传输不受数据传输限制。

img/483120_1_En_3_Fig5_HTML.jpg

图 3-5

专用网络和监控

最后,为这个开发集群选择三个 Droplets,并提供描述性的主机名,如图 3-6 所示。这本书使用主机名dosf2-n01.apk8s.dev,主机名dosf2-n02.apk8s.dev,主机名dosf2-n03.apk8s.dev

img/483120_1_En_3_Fig6_HTML.jpg

图 3-6

水滴数量和主机名

一旦新的 Droplet(服务器)启动并运行,请注意分配给每个 Droplet 的公共和私有 IP 地址。图 3-7 显示分配给dosf2-n01.apk8s.dev的公有 IP 地址为138.68.18.212,私有 IP 地址为10.138.28.155

img/483120_1_En_3_Fig7_HTML.jpg

图 3-7

水滴细节

准备节点

使用终端,ssh 作为用户根进入每个新的 Droplet(服务器)。如果您在设置 Droplet 时没有添加 SSH 密钥,Digital Ocean 会通过电子邮件向您发送一个生成的一次性 root 用户密码。按照以下说明更新软件包,并在每台服务器上安装 Docker 和 Kubernetes 实用程序:

$ ssh root@PUBLIC_IP

安装依赖项

更新和升级所有软件包,以确保服务器拥有最新的软件包,提供任何必要的安全和性能更新:

$ apt update && apt upgrade -y

安装以下软件包:apt-transport-https允许 apt 软件包管理器从 HTTPS 端点获取软件包。ca-certificates安装来自证书颁发机构的 SSL/TLS 证书以及更新程序。curl是一个命令行 HTTP 客户端,对于从命令行与基于 HTTP 的端点进行交互至关重要。gnupg-agent为 GPG 签名和密钥管理提供系统守护程序。software-properties-common支持管理后来为 WireGuardDockerKubernetes 添加的独立软件供应商库。

用一个命令安装依赖项:

$ apt install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common

安装 WireGuard VPN

所有云提供商都提供专用网络,即分配 IP 地址的能力,该能力只能在提供商的内部网络中访问,并且通常只能在同一区域内访问。虽然这些私有 IP 地址不能通过公共互联网访问,但根据提供商的不同,如果在同一地区运行实例,其他客户可以访问它们。加密集群中节点之间的所有流量始终是一种好的做法。

这个练习是可选的。在大多数情况下,没有必要使用 VPN 来加密专用网络上的流量。在过去,一些云提供商与多个客户端共享私有网络,虽然不向公众公开, 2 网络流量对同一网络上的其他客户端来说是潜在可见的。尽管大多数提供商已经放弃了共享专用网络,但本练习演示了如何使用 VPN 在不可信网络的高安全性环境中保护所有网络流量。

在 VPN 上运行 Kubernetes 集群可以加密节点之间的网络流量;但是,在群集内,默认情况下不会加密网络流量。Istio 4 和 Linkerd 5 等服务网格为 pod 之间的相互 TLS 通信提供了选项。

WireGuard 6 是一种快速、安全的 VPN,易于安装、配置和路由所有内部集群流量。接下来的步骤是为每台服务器生成公钥和私钥,添加 WireGuard 配置文件,启动服务,并创建一个覆盖网络来通过 Kubernetes 流量。

添加 WireGuard 存储库:

$ add-apt-repository -y ppa:wireguard/wireguard

现在已经添加了 WireGuard 存储库,请更新程序包列表:

$ apt update

安装电线保护装置:

$ apt install -y wireguard

为每台服务器生成私钥和公钥。下面是一个用于生成三个密钥对的小 shell 命令。生成密钥后,请安全地存储它们,以便在每台服务器上配置 WireGuard 时使用。私钥(priv)用于每个服务器上的 VPN 接口,公钥(pub)用于与对等方通信。

img/483120_1_En_3_Fig8_HTML.jpg

图 3-8

VPN 接口和对等体

$ for i in {1..3}; do prvk=$(wg genkey); \
echo "$i - priv: $prvk pub: $(echo $prvk | wg pubkey)"; done

接下来,为每台服务器配置 WireGuard。第一台服务器的以下示例配置将设置一个名为 wg0 的新网络接口,IP 地址为 10.0.1.1 。根据每台服务器调整配置;第二个服务器的接口 IP 应该是第三个的 10.0.1.210.0.1.3 (见图 3-8 )。使用先前生成的公钥或私钥。

$ cat <<EOF >/etc/wireguard/wg0.conf
[Interface]
Address = 10.0.1.1
PrivateKey = SERVER_1_PRIVATE_KEY
ListenPort = 51820

[Peer]
PublicKey = SERVER_2_PUBLIC_KEY
AllowedIps = 10.0.1.2/32
Endpoint = SERVER_2_PRIVATE_IP:51820

[Peer]
PublicKey = SERVER_3_PUBLIC_KEY
AllowedIps = 10.0.1.3/32
Endpoint = SERVER_3_PRIVATE_IP:51820
EOF

接下来,确保启用了 IP 转发。如果运行sysctl net.ipv4.ip_forward返回0,那么需要运行以下命令:

$ echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
$ sysctl -p

添加配置文件后,在每台服务器上启动 WireGuard VPN:

$ systemctl start wg-quick@wg0
$ systemctl enable wg-quick@wg0

安装 Docker

Kubernetes 支持几种基于容器运行时接口 7 (CRI)的运行时,包括 rkt、 8 frakti、 9 cri-o、 10 和 cri-containerd。 11 Docker 是默认的 CRI,也是一个成熟的行业标准,尽管每种替代方案都有值得探索的好处。以下命令介绍了安装 Docker 的过程。

添加 Docker 仓库 GPG 键:

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

添加 Docker 存储库:

$ add-apt-repository -y \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) stable"

现在已经添加了 Docker 存储库,请更新包列表:

$ apt update

安装最新版本的 Docker CE(社区版)守护进程、Docker 命令行界面、containerd: 12

$ apt install -y docker-ce docker-ce-cli containerd.io

Ubuntu 操作系统使用 Systemd 13 跟踪使用 Linux cgroups、14的进程,默认情况下 Docker 使用 cgroupfs。拥有单独的 cgroup 管理器会在管理负载资源时导致不稳定。通过提供一个配置文件来配置 Docker 使用 Systemd。 15

$ cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF

为 Docker 创建一个 Systemd 放入目录:

$ mkdir -p /etc/systemd/system/docker.service.d

启用 Docker 服务:

$ systemctl enable docker.service

重启 Docker:

$ systemctl daemon-reload && systemctl restart docker

安装库公用程式

除了 Docker,每个节点还会需要kubelet 16kubeadm17kubectl18是可选的,但在需要直接从节点调试时非常有用。

为 apt 添加谷歌 GPG 密钥:

$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| apt-key add -

添加库 apt 存储库:

$ cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF

现在已经添加了 Kubernetes 存储库,请更新包列表:

$ apt update

安装套装软体:

$ apt install -y kubelet kubeadm kubectl

将已安装的软件包锁定到其当前版本:

$ apt-mark hold kubelet kubeadm kubectl

安装主节点

是官方的 Kubernetes 安装工具,负责配置主节点的所有工作。如果您希望更深入地了解如何安装和配置每个组件,请学习 Kelsey Hightower 的“Kubernetes The Hard Way”19。否则,在节点 1 上运行kubeadm安装程序,配置主节点的私有 VPN IP 地址(--apiserver-advertise-address)进行通告,并添加一个公共地址(--apiserver-cert-extra-sans)作为额外地址包含在生成的 TLS 证书中,允许外部访问公共接口上的 API。域api.cluster.dev1.apk8s.dev被分配了一个 DNS A记录,该记录具有 DNS 部分后面的主节点的公共 IP 地址。

$ kubeadm init \
--apiserver-advertise-address=10.0.1.1 \
--apiserver-cert-extra-sans=api.cluster.dev1.apk8s.dev

成功安装后,将出现消息“Your Kubernetes control-plane has initialized successfully!”以及配置 kubectl 的说明。

$ mkdir -p $HOME/.kube
$ cp /etc/kubernetes/admin.conf $HOME/.kube/config

在安装过程之后,提供了加入工作节点的附加说明,类似于下面的命令(keys redacted)。复制并保存所提供的命令,以便在下一节“加入工作节点”中使用

kubeadm join 10.0.1.1:6443 --token REDACTED --discovery-token-ca-cert-hash REDACTED

接下来,pod 网络对于集群上的 pod 之间的通信是必要的。目前有十几种选择,每一种都有广泛的特性,值得在设置网络密集型生产集群时探索。Weave Net 20 是只需要简单联网和网络策略的开发或生产集群的绝佳选择。Weave 可以在一个命令中安装,不需要外部数据库或其他依赖项。

使用先前安装在主节点(节点 1)上的kubectl实用程序安装 Weave:

$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')&env.IPALLOC_RANGE=10.32.0.0/16"

可选—安装编织范围 21 用于网络可视化和监控;

$ kubectl apply -f https://cloud.weave.works/k8s/scope.yaml?k8s-version=$(kubectl version | base64 | tr -d '\n')

通过 WireGuard VPN 路由所有 Pod 网络(Weave) 10.96.0.0/16流量。在集群中的每台服务器上执行此操作,第一个节点上是 10.0.1.1,第二个节点上是 10.0.1.2,第三个节点上是 10.0.1.3。

$ ip route add 10.96.0.0/16 dev wg0 src 10.0.1.1

在每台服务器上使用以下文件保留此路由,将 10.0.1.1 替换为与服务器对应的 VPN 接口:

$ cat <<EOF >/etc/systemd/system/overlay-route.service
[Unit]
Description=Overlay network route for Wireguard
After=wg-quick@wg0.service

[Service]
Type=oneshot
User=root
ExecStart=/sbin/ip route add 10.96.0.0/16 dev wg0 src 10.0.1.1

[Install]
WantedBy=multi-user.target
EOF

在每台服务器上应用新配置:

$ systemctl enable overlay-route.service

加入工作节点

第二台和第三台服务器是 Kubernetes 工作节点。登录到每台服务器,运行主节点安装后提供的 join 命令。

$ kubeadm join 10.0.1.1:6443 --token REDACTED --discovery-token-ca-cert-hash REDACTED

添加额外的节点只需要服务器满足 2 个 CPU 和 2 GB RAM 的最低硬件规格,以及本节前面安装的 WireGuard、Docker 和 Kubernetes 应用。必须向 WireGuard VPN 网络添加额外的节点,并通过其接口路由 Pod 网络的 IP 子网10.96.0.0/16

域名服务器(Domain Name Server)

这个定制的 Kubernetes 开发集群并不支持复杂的(通常也是昂贵的)负载均衡器;相反,它使用基本的 DNS 循环技术在集群中分发请求。

通过为同一个主机名添加多个 DNS A记录来实现 DNS 循环。开发集群有两条A记录,每条记录都指向一个使用通配符主机名*.dev1的工作者节点的公共 IP 地址(参见图 3-9 )。通配符允许任意数量的*.dev1.apk8s.dev子域解析为集群上的节点。有关添加A记录的说明,请咨询您的域名/DNS 提供商。

img/483120_1_En_3_Fig9_HTML.jpg

图 3-9

开发群集 DNS

接下来,为指向主节点(节点 1)的公共 IP 的 Kubernetes API 添加一个额外的A记录api.cluster.dev1.apk8s.dev

远程存取

“k3s + GitLab”部分介绍了一种通过复制和粘贴所生成配置的集群、用户和上下文部分来配置 kubectl 进行远程访问的方法。以下说明给出了配置kubectl的另一种方法。

安装主节点后,kubeadm提供了将生成的kubectl配置复制到$HOME/.kube/config的指令。以 root 用户身份登录到主节点,该路径将解析为/root/.kube/config。使用以下命令将kubeadm生成的配置安全复制到位置工作站:

$ scp root@api.cluster.dev1.apk8s.dev:/root/.kube/config ~/.kube/apk8s-dev1

打开文件,用API . cluster . de v1 . apk8s . dev替换 IP 地址 10.0.1.1。或者,将上下文kubernetes-admin@kubernetes更改为更具描述性的内容。以下命令使用命令sed内联编辑文件:

$ sed -i .bak \
 's/10.0.1.1/api.cluster.dev1.apk8s.dev/g' \
 ~/.kube/apk8s-dev1

$ sed -i .bak 's/kubernetes-admin@kubernetes/apk8s-dev1/g' ~/.kube/apk8s-dev1

在同一终端中,将环境变量KUBECONFIG设置为~/.kube/apk8s-dev1:

$ export KUBECONFIG=~/.kube/apk8s-dev1

导出环境变量使其仅可用于当前终端会话。虽然这不是最有效的切换kubectl上下文的方法,但是在处理大量集群时,这是一个很好的选择,可以防止默认的~/.kube/config变得过于混乱。测试新的配置和上下文:

$ kubectl get nodes

NAME        STATUS   ROLES    AGE   VERSION
dosf2-n01   Ready    master   15m   v1.15.0
dosf2-n02   Ready    <none>   11m   v1.15.0
dosf2-n03   Ready    <none>   11m   v1.15.0

新的开发集群已经准备好配置额外的组件,以满足本书剩余部分的依赖关系。TLS 证书、持久存储和入口是后来添加的物联网、机器学习和区块链功能的基本要求。下一节“配置”将介绍用于满足这些需求的 YAML 清单的组织和应用,并使用新的 GitLab 实例来保持它们的版本和文档。

配置

Kubernetes 支持声明式配置;这意味着我们告诉 Kubernetes 我们想要它是什么,而不是发布一个实现某个状态所需的命令列表。这本书用 YAML 来描述库伯涅茨的理想状态;这个状态是构成操作平台的部署、状态集、配置映射、服务和其他的集合。将状态配置作为一组静态 YAML 文件保存在 Git 存储库中,不仅可以反映平台的当前状态,还可以提供随时间变化的有价值的提交日志。

企业软件平台的配置管理技术和实用程序有一个不断发展的生态系统。这本书试图通过坚持把普通 YAML 作为配置的标准手段来保持事情的简单。这本书展示了一种组织 Kubernetes 配置清单的简单方法,描述了添加到平台的各种应用。随着平台和配置需求的增长,可能会分层加入更复杂的配置工具,如 Jsonnet、 22 Kapitan、 23 或 Kustomize。24GitOps25等新概念旨在直接用 Git 完全管理活动配置状态。Kubernetes 提供了无限的方法来创造性地管理配置。然而,从按环境、名称空间、组件和对象类型分类的组织良好的目录结构开始,可以提供一个合理的基础。

贮藏室ˌ仓库

既然开发集群已经可以运行,并且kubectl可以从本地工作站访问它,那么一系列包含名称空间、卷、入口和监控的附加配置的 YAML 文件将被应用到集群。开发集群会产生越来越多的 YAML 配置文件。这些配置文件(或清单)不仅定义了 Kubernetes 所需的状态,它们还为开发人员和系统管理员提供了详细而准确的文档,尤其是在伴随有补充性(README.md ) markdown 风格的文档时。

本书中的配置清单和文档组织在前一章安装的自托管 GitLab 存储库中。一个名为 apk8s 的 GitLab 小组持有项目 k8s 。组织多个基于 Kubernetes 的项目的常见方法是将它们嵌套在 GitLab 组下,每个项目都有一个专用的 k8s 存储库。两个独立平台项目的一个例子可以包括保存该平台的集群配置的 https://gitlab.apk8s.dev/apk8s/k8s ,以及保存“另一个平台”的 Kubernetes 配置的 https://gitlab.apk8s.dev/another-platform/k8s

Note

GitHub 提供了特性 Organizations ,具有与 GitLab groups 相似的功能。

一旦在 GitLab 中设置了组 apk8s 和项目 k8s ,在本地工作站上创建一个与组(apk8s)匹配的目录,并将新项目克隆到其中。

$ mkdir -p ~/workspace/apk8s
$ cd ~/workspace/apk8s
$ git clone ssh://git@gitlab.apk8s.dev:32222/apk8s/k8s.git
$ cd k8s

Note

如果在同一个集群上开发多个项目,建议将集群范围的配置存储为一个单独的组,如 devops/k8s ,并将每个项目提交给 it 部门,以了解集群范围的要求和文档。

创建一个目录来保存集群范围的配置。这些配置特定于集群(cluster-apk8s-dev1)。

$ mkdir -p cluster-apk8s-dev1/000-cluster

进入

在这个平台中,大多数 Kubernetes 服务 26 被分配了一个集群 IP,并且只能从集群内部访问。Kubernetes Ingress 允许外部 HTTP 和 HTTPS 连接到集群内的服务。Kubernetes 入口资源定义了一个必须由入口控制器支持的配置。

Kubernetes 没有提供默认的入口控制器,让管理员和系统架构师选择最适合平台需求的入口控制器。有大量有能力的入口控制器可用。在前一章中,Traefik 与 k3s Kubernetes 发行版打包在一起,并通过服务 GitLab 安装演示了它的使用。本章使用入口 Nginx 27 控制器配置新的 dev1 集群,展示各种入口选项。

本章根据从 k8s 存储库克隆的目录~/workspace/apk8s/k8s和上一节设置的 GitLab 组apk8s构建配置。从这个位置创建一个目录来存储入口 Nginx 配置清单:

$ mkdir -p cluster-apk8s-dev1/000-cluster/00-ingress-nginx
$ cd cluster-apk8s-dev1/000-cluster/00-ingress-nginx

从清单 3-1 中为入口 Nginx 名称空间创建一个名为00-namespace.yml的配置文件。

apiVersion: v1
kind: Namespace
metadata:
  name: ingress-nginx

Listing 3-1Ingress Nginx Namespace

应用入口 Nginx 名称空间配置:

$ kubectl apply -f 00-namespace.yml

从清单 3-2 中为名为05-serviceaccount.yml的 Ingress Nginx 创建一个描述基于角色的访问控制(RBAC) Kubernetes 服务帐户的配置文件。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccount
  namespace: ingress-nginx

Listing 3-2Ingress Nginx RBAC Service Account

应用入口 Nginx 服务帐户配置:

$ kubectl apply -f 05-serviceaccount.yml

创建一个配置文件,描述清单 3-3 中名为06-clusterrole.yml的入口 Nginx 的 RBAC 集群角色。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: nginx-ingress-clusterrole
rules:
  - apiGroups: [""]
    resources: ["configmaps",
                "endpoints",
                "nodes",
                "pods",
                "secrets"]
    verbs: ["list", "watch"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["extensions"]
    resources: ["ingresses"]
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "patch"]
  - apiGroups: ["extensions"]
    resources: ["ingresses/status"]
    verbs: ["update"]

Listing 3-3RBAC Ingress Nginx Cluster Role

应用入口 Nginx 群集角色配置:

$ kubectl apply -f 06-clusterrole.yml

创建一个配置文件,描述清单 3-4 中名为07-role.yml的入口 Nginx 的 RBAC 角色。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: nginx-ingress-role
  namespace: ingress-nginx
rules:
  - apiGroups: [""]
    resources: ["configmaps",
                "pods",
                "secrets",
                "namespaces"]
    verbs: ["get"]
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames:
      - "ingress-controller-leader-nginx"
    verbs: ["get", "update"]
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["create"]
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get"]

Listing 3-4RBAC Ingress Nginx Role

应用入口 Nginx 角色配置:

$ kubectl apply -f 07-role.yml

从清单 3-5 中创建一个配置文件,描述名为08-rolebinding.yml的入口 Nginx 的 RBAC 角色绑定。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: nginx-ingress-role-nisa-binding
  namespace: ingress-nginx
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nginx-ingress-role
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: ingress-nginx

Listing 3-5RBAC Ingress Nginx Role Binding

应用入口 Nginx 角色绑定配置:

$ kubectl apply -f 08-rolebinding.yml

创建一个配置文件,描述清单 3-6 中名为09-clusterrolebinding.yml的入口 Nginx 的 RBAC 集群角色绑定。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: nginx-ingress-clusterrole-nisa-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nginx-ingress-clusterrole
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: ingress-nginx

Listing 3-6RBAC Ingress Nginx Cluster Role Binding

应用入口 Nginx 群集角色绑定配置:

$ kubectl apply -f 09-clusterrolebinding.yml

创建一个配置文件,描述清单 3-7 中名为10-services.yml的入口 Nginx 的两个 Kubernetes 服务。

apiVersion: v1
kind: Service
metadata:
  name: default-http-backend
  namespace: ingress-nginx
  labels:
    app: default-http-backend
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: default-http-backend
---
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
    - name: https
      port: 443
      targetPort: 443
      protocol: TCP
  selector:
    app: ingress-nginx

Listing 3-7Ingress Nginx Services

应用入口 Nginx 服务配置:

$ kubectl apply -f 10-services.yml

创建一个配置文件,描述清单 3-8 中名为20-configmaps.yml的入口 Nginx 的三个空 Kubernetes 配置映射。

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-configuration
  namespace: ingress-nginx
  labels:
    app: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: tcp-services
  namespace: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: udp-services
  namespace: ingress-nginx

Listing 3-8Ingress Nginx Services ConfigMaps

应用入口 Nginx 配置映射配置:

$ kubectl apply -f 20-configmaps.yml

创建一个配置文件,描述清单 3-9 中名为30-deployment.yml的默认 HTTP 后端服务器的 Kubernetes 部署。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: default-http-backend
  labels:
    app: default-http-backend
  namespace: ingress-nginx
spec:
  replicas: 2
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: default-http-backend
  template:
    metadata:
      labels:
        app: default-http-backend
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - default-http-backend
                topologyKey: kubernetes.io/hostname
      terminationGracePeriodSeconds: 60
      containers:
        - name: default-http-backend
          image: gcr.io/google_containers/defaultbackend:1.4
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 30
            timeoutSeconds: 5
          ports:
            - containerPort: 8080
          resources:
            limits:
              cpu: 10m
              memory: 20Mi
            requests:
              cpu: 10m
              memory: 20Mi

Listing 3-9Ingress Nginx Deployment. Default back ends

应用入口 Nginx 默认 HTTP 服务器后端配置部署:

$ kubectl apply -f 30-deployment.yml

最后,创建一个配置文件,描述清单 3-10 中名为40-daemonset.yml的入口 Nginx 控制器的 Kubernetes DaemonSet。DaemonSet 指示 Kubernetes 确保每个节点上运行一个入口 Nginx 控制器。入口 Nginx 控制器监听 TCP 端口 80 (HTTP)和 443 (HTTPS)。在本章的前面,DNS 部分为*.dev1.apk8s.dev配置了两个A记录,指向每个 worker 节点。

Note

本章中定义的定制 Kubernetes 开发集群不使用负载均衡器 28 ,而是依靠 DNS 记录来指向每个工作节点。使用 DaemonSet 可以确保每个工作节点都有一个 Nginx 入口 Pod 侦听端口 80 (HTTP)和 443 (HTTPS)。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
spec:
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: ingress-nginx
  template:
    metadata:
      labels:
        app: ingress-nginx
      annotations:
        prometheus.io/port: '10254'
        prometheus.io/scrape: 'true'
    spec:
      serviceAccountName: nginx-ingress-serviceaccount
      hostNetwork: true
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.14.0
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
            - --configmap=$(POD_NAMESPACE)/nginx-configuration

            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services
            - --annotations-prefix=nginx.ingress.kubernetes.io
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
              hostPort: 80
            - name: https
              containerPort: 443
              hostPort: 443
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            initialDelaySeconds: 10
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          securityContext:
            runAsNonRoot: false

Listing 3-10Ingress Nginx DaemonSet

应用入口 Nginx 默认 HTTP 服务器后端配置部署:

$ kubectl apply -f 40-daemonset.yml

Ingress Nginx 现在已经配置好并运行在新的开发集群上,可以通过每个节点上的端口 80 和 443 接受 web 流量。本练习构建了每个配置清单,并将其存储在目录~/workspace/apk8s/k8s/cluster-apk8s-dev1/000-cluster/00-ingress-nginx中。这组入口 Nginx 配置清单准确地表示了集群的当前或期望状态,此外还向其他人提供了文档,并且能够在以后或在另一个集群上再现这种状态。

最后,开发集群需要能够生成 TLS 证书,Nginx 可以使用这些证书为加密的 HTTPS(端口 443)流量提供服务。下一节“使用证书管理器的 TLS/HTTPS”将介绍如何设置证书管理器,以便使用 Let's Encrypt 自动生成免费的 TLS 证书。

带证书管理器的 TLS/HTTPS

Cert Manager 用于“自动管理和发布来自各种发布源的 TLS 证书” 29 这本书利用 Let's Encrypt 30 进行安全、免费的 TLS 证书颁发,后来配置了一个名为 ClusterIssuer 的证书管理器自定义资源。为证书管理器配置清单创建目录~/workspace/apk8s/k8s/cluster-apk8s-dev1/000-cluster/10-cert-manager

用清单 3-11 的内容创建文件00-namespace.yml

apiVersion: v1
kind: Namespace
metadata:
  name: cert-manager
  labels:
    certmanager.k8s.io/disable-validation: "true"

Listing 3-11Cert Manager Namespace

为证书管理器应用新的命名空间:

$ kubectl apply -f 00-namespace.yml

获取证书管理器的自定义资源定义(CRD)并将它们保存到文件02-crd.yml:

$ curl -L https://github.com/jetstack/cert-manager/releases/download/v0.8.0/cert-manager.yaml >02-crd.yml

应用证书管理器 CRDs:

$ kubectl apply -f 02-crd.yml

已安装证书管理器。确保所有支持证书管理器的 pod 都已达到运行状态(kubectl get pods -n cert-manager)。

证书管理器定义了称为 ClusterIssuer 和 Certificate 等新的自定义资源。证书描述了所需的 TLS 证书以及使用发行者从权威机构(如 Let's Encrypt)检索 TLS 证书。本书对所有证书都使用了 ClusterIssuer,但是您可以在 Cert Manager 的官方文档中阅读更多关于 Issuer 和 ClusterIssuer 的内容。

用清单 3-12 的内容创建文件03-clusterissuer.yml

apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
  name: letsencrypt-production
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: YOUR_EMAIL_ADDRESS
    privateKeySecretRef:
      name: letsencrypt-production
    http01: {}

Listing 3-12Cert Manager Cluster Issuer

应用证书管理器群集颁发者:

$ kubectl apply -f 03-clusterissuer.yml

群集中的任何命名空间都可以使用新的 ClusterIssuer。使用命令:kubectl get clusterissuers获取集群发布者列表。后面的小节在定义由 Ingress Nginx 用于 TLS 通信的证书(存储为 Kubernetes Secrets)时使用这个新的 ClusterIssuer letsencrypt-production

新的开发集群现在能够接受入站 HTTP 和 HTTPS,并自动生成 TLS 证书。合适的开发环境的最后一个基本要求是持久存储,这将在下一节讨论。

具有 Rook Ceph 的持久卷

对于一些 Kubernetes 部署来说,持久存储是一个基本的、通常也是棘手的需求。Kubernetes Pods 及其文件系统被认为是暂时的。外部数据库是将应用容器获得的数据保存在 Pod 中的一种很好的方式。然而,一些 Pod 可能代表数据库或文件系统本身,因此任何连接的数据卷必须在 Pod 本身的生命周期之后继续存在。

本节启用 Kubernetes 持久卷 31 由 Ceph 32 由 Rook 编排。 33 Ceph 是一个分布式存储集群,为基于对象、块和文件系统的存储提供 Kubernetes 持久卷。Rook 操作符用于在后台安装和管理 Ceph。

以下指令安装 Rook 操作符并创建两个存储类(块和文件系统)供 Kubernetes 持久性卷使用。Ceph 34 的官方 Rook 文档建议从他们的示例配置清单开始,并在需要的地方定制它们。与前面的例子保持一致,创建目录:~/workspace/apk8s/k8s/cluster-apk8s-dev1/000-cluster/20-rook-ceph。从以下网址下载 Rook 自定义资源定义(CRD)、操作符、集群和工具箱部署。

获取 Rook Ceph 名称空间 CRD 并将它们保存到文件00-namespace-crd.yml:

$ curl -L \
https://github.com/rook/rook/raw/release-1.0/cluster/examples/kubernetes/ceph/common.yaml >00-namespace-crd.yml

应用 Rook Ceph 名称空间和自定义资源定义:

$ kubectl apply -f 00-namespace-crd.yml

获取 Rook Ceph 操作员部署配置,并将其保存到文件30-deployment-oper.yml:

$ curl -L \
https://github.com/rook/rook/raw/release-1.0/cluster/examples/kubernetes/ceph/operator.yaml >30-deployment-oper.yml

应用 Rook Ceph 操作员部署:

$ kubectl apply -f 30-deployment-oper.yml

获取示例 Rook Ceph 集群配置,并将其保存到文件60-cluster-rook-ceph.yml:

$ curl -L \
https://github.com/rook/rook/raw/release-1.0/cluster/examples/kubernetes/ceph/cluster-test.yaml >60-cluster-rook-ceph.yml

应用 Rook Ceph 集群部署配置:

$ kubectl apply -f 60-cluster-rook-ceph.yml

获取 Rook Ceph 工具箱部署配置,并将其保存到文件30-deployment-toolbox.yml:

$ curl -L \
https://github.com/rook/rook/raw/release-1.0/cluster/examples/kubernetes/ceph/toolbox.yaml >30-deployment-toolbox.yml

应用 Rook Ceph 集群工具箱部署配置:

$ kubectl apply -f 30-deployment-toolbox.yml

rook-ceph名称空间现在包含用于管理底层 Ceph 集群的 pod,以及从持久卷声明中提供持久卷的能力。

查看在rook-ceph名称空间(kubectl get pods -n rook-ceph)中运行的窗格列表;以rook-ceph-tools-为前缀的 Pod 包含了ceph命令行实用程序。在这个 Pod 上执行bash shell,然后发出命令ceph status来查看新存储集群的状态。

$ kubectl exec -it rook-ceph-tools-5f49756bf-m6dxv \
-n rook-ceph bash
$ ceph status

示例输出:

  cluster:
    id:     f67747e5-eb2a-4301-8045-c1e210488433
    health: HEALTH_OK

  services:
    mon: 1 daemons, quorum a (age 22m)
    mgr: a(active, since 21m)
    osd: 2 osds: 2 up (since 21m), 2 in (since 21m)

  data:
    pools:   0 pools, 0 pgs
    objects: 0 objects, 0 B
    usage:   9.1 GiB used, 107 GiB / 116 GiB avail
    pgs:

块存储器

需要持久卷的 pod 可以通过 Kubernetes 持久卷声明(PVC)来实现。PVC 需要一个由 Rook 使用的已定义的存储类来提供一个永久卷。

设置一个名为rook-ceph-block的新存储类,该类由一个 CephBlockPool 支持,能够根据持久卷声明请求提供持久卷。

如果从上一节继续,用清单 3-13 的内容在目录~/workspace/apk8s/k8s/cluster-apk8s-dev1/000-cluster/20-rook-ceph中创建文件70-rook-ceph-block.yml

apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: host
  replicated:
    size: 1
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-ceph-block
provisioner: ceph.rook.io/block
parameters:
  blockPool: replicapool
  clusterNamespace: rook-ceph
  fstype: xfs
reclaimPolicy: Delete

Listing 3-13CephBlockPool and StorageClass

应用 CephBlockPool 和 StorageClass 配置:

$ kubectl apply -f 70-rook-ceph-block.yml

开发集群现在支持(但不限于)Kubernetes StatefulSets 通常使用的持久卷声明(PVC)。稍后,本书将 PVC 用于有状态应用,如数据库、数据索引和事件队列。下一节将介绍由 Ceph 支持的集群范围的共享文件系统的实现。

共享文件系统

共享文件系统提供了围绕文件管理划分责任的机会。共享文件系统允许这样的场景,其中一组 Pods 允许用户上传文件,比如映像,而另一组 Pods 检索和处理它们。尽管有许多其他方法可以跨部署共享文件,但是 Ceph 支持的共享文件系统在集群中构建以数据为中心的平台时提供了灵活的选择。

用清单 3-14 的内容在目录~/workspace/apk8s/k8s/cluster-apk8s-dev1/000-cluster/20-rook-ceph下创建文件75-rook-ceph-clusterfs.yml

apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
  name: rook-ceph-clusterfs
  namespace: rook-ceph
spec:
  metadataPool:
    replicated:
      size: 1
  dataPools:
    - failureDomain: host
      replicated:
        size: 2
  metadataServer:
    activeCount: 1
    activeStandby: true

Listing 3-14CephFilesystem

应用 CephFilesystem 配置:

$ kubectl apply -f 75-rook-ceph-clusterfs.yml

开发集群现在能够通过 Nginx 接受和路由入站 web 流量,通过 Cert Manager 创建和使用 TLS 证书,并通过 Rook 和 Ceph 提供共享文件系统。当主要的云提供商在交钥匙解决方案中提供大部分这种栈时,将这种定制的 Kubernetes 集群与这些基本功能结合起来似乎需要很大的努力。然而,本章中配置的开发集群几乎可以在任何提供者上运行,这使得它真正具有可移植性、云原生性和厂商中立性。

开发集群是完整的,因为它可以支持本书中区块链、机器学习和物联网数据处理组件所需的大多数功能。

下一节将简要介绍监控。监控不是操作需求,而是集群管理的一个重要工具,它提供了关于性能和资源利用率的管理指标。

监视

Kubernetes 监控解决方案的生态系统非常庞大,并且日益成熟。从商业产品到新颖独特的开源项目,集群监控领域在关于 Kubernetes 管理的书籍、博客和教程中都有涉及。

本节使用 CoreOS 的开源项目kube-prometheus建立了一个最小的监控解决方案。 35 生产环境可能会有更多定制的监控和警报配置。因此,开发集群的监控组件不需要在其他环境中具有可重复性。因此,只需要记录用于创建开发集群的监控解决方案的命令。

为了与前面章节中集群配置的组织保持一致,创建目录~/workspace/apk8s/k8s/cluster-apk8s-dev1/000-cluster/30-monitoring,添加文件README.md以及清单 3-15 的内容。

# kube-prometheus
Installation guide for the **monitoring** namespace.

```bash
git clone git@github.com:coreos/kube-prometheus.git
cd kube-prometheus
git checkout v0.1.0

kubectl create -f manifests/

# Verify the resources are ready before proceeding.

until kubectl get customresourcedefinitions servicemonitors.monitoring.coreos.com ; do date; sleep 1; echo ""; done

until kubectl get servicemonitors --all-namespaces ; do date; sleep 1; echo ""; done

# Apply the manifests

.
# This command sometimes may need to be done twice
# (to work around a race condition).

kubectl apply -f manifests/

Listing 3-15Monitoring README


执行````bash`和`````之间的每个命令。新的`README.md`中的指令克隆了 kube-prometheus 项目,为 CoreOS prometheus 创建了新的自定义资源定义,并应用了几个部署,这些部署代表了使用 Prometheus 和 Grafana 监控集群所需的组件。

没有必要用开发集群配置清单的剩余部分来跟踪克隆的 kube-prometheus 项目。用 Git ignore 文件(`cd ../ && echo "kube-prometheus" >.gitignore`)排除 kube-prometheus 项目。

Note

当浏览 GitLab(或 GitHub)中的存储库时,以 markdown 格式记录命令提供了作为原始文本或呈现为格式化 HTML 的可读指令。

通过将 Grafana 端口转发到本地工作站,直观地监控新集群:

$ kubectl --namespace monitoring
port-forward svc/grafana 3000


在本地工作站上打开`http://localhost:3000`,使用默认的管理员凭证登录 Grafana,用户名:`admin`,密码:`admin`。探索用于监控 Kubernetes 集群许多方面的预构建仪表板,包括节点、名称空间和 pod。

Note

Prometheus 操作员使用自定义资源定义(CRD) ServiceMonitor 来配置收集指标的目标。要定制 Prometheus 监视该集群,首先使用`kubectl get ServiceMonitor -n monitoring`查看预安装的 ServiceMonitor,并查看 service monitor 官方文档。 <sup>36</sup>

## 摘要

本章设置了一个小型定制 Kubernetes 开发集群,支持入口、自动 TLS 证书生成、块和文件系统存储以及基本的监控。本章中开发的配置文件旨在提交给 Git 存储库(并可选地托管在前一章中设置的 GitLab 实例上)。

将平台开发为机器学习、区块链和物联网数据管理等特定领域功能的集合,并与定制应用绑定在一起的过程,受益于连接到这一新开发集群的高度集成的 DevOps 工具链。下一章将介绍 CI/CD,并将第二章中的 GitLab 安装与这个新的开发集群相结合。

Note

清单 3-16 显示了本章过程中开发的配置文件的最终列表和组织。从目录`~/workspace/apk8s/k8s`开始,列表 3-16 表示从 GitLab 组`apk8s`和项目`k8s`克隆的存储库的当前状态。

.
└── cluster-apk8s-dev1
└── 000-cluster
├── 00-ingress-nginx
│ ├── 00-namespace.yml
│ ├── 05-serviceaccount.yml
│ ├── 06-clusterrole.yml
│ ├── 07-role.yml
│ ├── 08-rolebinding.yml
│ ├── 09-clusterrolebinding.yml
│ ├── 10-services.yml
│ ├── 20-configmaps.yml
│ ├── 30-deployment.yml
│ └── 40-daemonset.yml
├── 10-cert-manager
│ ├── 00-namespace.yml
│ ├── 02-crd.yml
│ └── 03-clusterissuer.yml
├── 20-rook-ceph
│ ├── 00-namespace-crd.yml
│ ├── 30-deployment-oper.yml
│ ├── 30-deployment-toolbox.yml
│ ├── 60-cluster-rook-ceph.yml
│ ├── 70-rook-ceph-block.yml
│ └── 75-rook-ceph-clusterfs.yml
└── 30-monitoring
└── README.md

Listing 3-16Development Cluster configuration layout


<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1

[`https://kubernetes.io/docs/concepts/#kubernetes-control-plane`](https://kubernetes.io/docs/concepts/%2523kubernetes-control-plane)

  2

[`https://blog.digitalocean.com/introducing-private-networking/`](https://blog.digitalocean.com/introducing-private-networking/)

  3

[`www.digitalocean.com/docs/platform/release-notes/2018/private-networking/`](http://www.digitalocean.com/docs/platform/release-notes/2018/private-networking/)

  4

[`https://istio.io/docs/tasks/security/authentication/mutual-tls/`](https://istio.io/docs/tasks/security/authentication/mutual-tls/)

  5

[`https://linkerd.io/2/features/automatic-mtls/`](https://linkerd.io/2/features/automatic-mtls/)

  6

[`www.wireguard.com/`](http://www.wireguard.com/)

  7

[`https://kubernetes.io/docs/setup/cri/`](https://kubernetes.io/docs/setup/cri/)

  8

[`https://github.com/rkt/rkt`](https://github.com/rkt/rkt)

  9

[`https://github.com/kubernetes/frakti`](https://github.com/kubernetes/frakti)

  10

[`https://cri-o.io/`](https://cri-o.io/)

  11

[`https://github.com/containerd/cri`](https://github.com/containerd/cri)

  12

[`https://containerd.io/`](https://containerd.io/)

  13

[`www.freedesktop.org/wiki/Software/systemd/`](http://www.freedesktop.org/wiki/Software/systemd/)

  14

[`www.kernel.org/doc/Documentation/cgroup-v2.txt`](http://www.kernel.org/doc/Documentation/cgroup-v2.txt)

  15

[`https://github.com/kubernetes/kubeadm/issues/1394#issuecomment-462878219`](https://github.com/kubernetes/kubeadm/issues/1394%2523issuecomment-462878219)

  16

[`https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/`](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/)

  17

[`https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm/`](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm/)

  18

[`https://kubernetes.io/docs/reference/kubectl/overview/`](https://kubernetes.io/docs/reference/kubectl/overview/)

  19

[`https://github.com/kelseyhightower/kubernetes-the-hard-way`](https://github.com/kelseyhightower/kubernetes-the-hard-way)

  20

[`www.weave.works/oss/net/`](http://www.weave.works/oss/net/)

  21

[`www.weave.works/docs/scope/latest/introducing/`](http://www.weave.works/docs/scope/latest/introducing/)

  22

[`https://jsonnet.org/`](https://jsonnet.org/)

  23

[`https://github.com/deepmind/kapitan`](https://github.com/deepmind/kapitan)

  24

[`https://github.com/kubernetes-sigs/kustomize`](https://github.com/kubernetes-sigs/kustomize)

  25

[`www.weave.works/technologies/gitops/`](http://www.weave.works/technologies/gitops/)

  26

[`https://kubernetes.io/docs/concepts/services-networking/service/`](https://kubernetes.io/docs/concepts/services-networking/service/)

  27

[`https://kubernetes.github.io/ingress-nginx/`](https://kubernetes.github.io/ingress-nginx/)

  28

[`https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/`](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/)

  29

在 Kubernetes:jet stack/Cert-Manager 中自动提供和管理 TLS 证书。Jetstack,2019。 [`https://github.com/jetstack/cert-manager`](https://github.com/jetstack/cert-manager) 。

  30

[`https://letsencrypt.org/`](https://letsencrypt.org/)

  31

[`https://kubernetes.io/docs/concepts/storage/persistent-volumes/`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)

  32

[`https://ceph.com/`](https://ceph.com/)

  33

[`https://rook.io/`](https://rook.io/)

  34

[`https://rook.io/docs/rook/v1.0/ceph-examples.html`](https://rook.io/docs/rook/v1.0/ceph-examples.html)

  35

[`https://github.com/coreos/kube-prometheus`](https://github.com/coreos/kube-prometheus)

  36

[`https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#servicemonitorspec`](https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md%2523servicemonitorspec)

 </aside>

# 四、平台内 CI/CD

CI/CD 代表持续集成和交付,以及持续集成和部署。连续交付包括将代码编译或准备成合适版本的过程,而连续部署通过自动化过程安装或更新现有应用(通常在服务器上)。CI/CD 继续成熟并扩大其范围,从低级开发关注到中央平台操作。它已经超越了为独立软件包合并和编译应用的局限。CI/CD 正在取得新的进展,部署机器学习模型、无服务器功能,甚至基础设施供应。本章利用 CI/CD 来开发和交付用于在应用平台范围内实现数据科学功能的容器。本章使用 GitLab 为平台开发和生产运营提供 CI/CD 功能。

## 开发和运营

CI/CD 原则已经从驱动传统软件应用开发的孤立的构建和交付系统扩展开来。CI/CD 概念正在扩展到企业平台的操作组件中。从学术实验室到基于云的生产部署,数据科学领域,特别是机器学习领域有着广泛的实现和专门的流程。机器学习自动化已经接受了容器化,以及自动构建容器来包装复杂的逻辑和依赖关系。CI/CD 风格的流水线非常适合于容器化人工智能的实验、测试和生产部署,降低了部署和回滚功能的成本和复杂性。

## 平台集成

本章集成了 GitLab 和 Kubernetes,以形成平台开发和运营之间更紧密的关系。从 Kubernetes 集群内部的版本化源代码构建容器为在集成、交付和部署的所有阶段访问平台功能提供了新的机会。

在第二章中,图 2-3 说明了在单节点 Kubernetes (k3s)集群上运行的 GitLab 实例与通过`git`和`kubectl`命令从外部控制的远程开发集群之间的关系。从平台内部集成`git`和`kubectl`的能力提供了将开发能力引入平台的机会。Kubeflow 是一个流行的机器学习自动化平台,它通过包含`kubectl`的自定义 JupyterLab <sup>1</sup> 映像来拥抱这个概念。JupyterLab 映像由 JupyterHub <sup>2</sup> 提供,并使用 RBAC 定义的权限挂载一个 Kubernetes 服务帐户。以下练习借用了其中一些概念来演示 CI/CD 与数据科学功能的集成,作为更深入定制的起点(参见图 4-1 )。

![img/483120_1_En_4_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig1_HTML.jpg)

图 4-1

与 GitLab 和 JupyterLab 的 CI/CD 集成

## 又一个发展集群

本章使用 Scaleway <sup>3</sup> (一家欧洲折扣云提供商)的四节点定制 Kubernetes 集群设置。尽管本书中的许多例子使用了不同的提供者,但是除了展示与供应商无关的方法以及概念和实现的可移植性之外,没有必要将服务分散到不同的提供者中。Scaleway 是一个经济高效的选项,可用于测试各种集群配置。以下集群使用一个 **DEV1-M** (3 个 CPU/4G RAM/40G SSD)作为 Kubernetes 主节点,三个 **DEV1-L** (4 个 CPU/8G RAM/80GB)作为工作节点。在撰写本文时,这个开发集群的总成本不到每小时 0.20 美元。Scaleway 提供了与 Vultr 和 Digital Ocean 类似的用户界面和选项。“开发环境”一章中的说明可能适用于 Scaleway 和许多其他供应商。

本章的剩余部分使用 GitLab 托管一个包含`Dockerfile`的存储库,使用新的数据科学和机器学习功能以及 kubectl 命令定制基本 JupyterLab 映像。这个映像是用 GitLab 的 CI 特性构建的,托管在 GitLab 映像注册表中。第二章讲述在单节点 Kubernetes 集群上建立 GitLab 然而,任何 GitLab 实例都是合适的。

### 角色访问控制

GitLab 使用具有`cluster-admin`权限的服务帐户令牌。请记住 GitLab integration“安全性是基于一种模型,在这种模型中,开发人员是可信的,因此应该只允许可信的用户控制您的集群。”<sup>4</sup>

为了与第 2 和第三章的清单组织保持一致,在 k8s git 项目中创建目录`cluster-apk8s-dev2/000-cluster/40-gitlab-integration`。

在清单 4-1 中名为`05-rbac.yml`的文件中创建 ServiceAccount 和 ClusterRoleBinding。

 apiVersion: rbac.authorization.k8s.io/v1beta1

kind: RoleBinding
metadata:
name: data-lab
namespace: data-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: data-lab
subjects:

  • kind: ServiceAccount
    name: data-lab
    namespace: data-lab

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: hub
namespace: data-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: hub
subjects:

  • kind: ServiceAccount
    name: hub
    namespace: data

Listing 4-1ServiceAccount and ClusterRoleBinding for GitLab


应用 GitLab RBAC 配置:

$ kubectl apply -f 05-rbac.yml


### GitLab 集团库柏访问

虽然 GitLab 的 Kubernetes 集成是建立在信任开发者的基础上的,但并不是所有的项目/资源库,或者开发者,都需要访问 Kubernetes。GitLab 允许单个项目或小组分别集成 Kubernetes 集群。本章创建了一个名为 Data Science (ds)的新组,并将其与 Scaleway 上设置的新 dev2 集群集成。图 4-2 展示了如何建立一个新的 GitLab 组。

![img/483120_1_En_4_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig2_HTML.jpg)

图 4-2

创建 GitLab 组

#### 配置 Kubernetes 集群集成

配置新的数据科学 GitLab 组以控制 Kubernetes 集群(参见图 4-3 ):

![img/483120_1_En_4_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig3_HTML.jpg)

图 4-3

gitlab group kublers configuration menu-gitlab 群组库设定功能表

1.  在组的左侧菜单中选择 **Kubernetes** 。

2.  选择选项卡**添加现有集群**。

3.  为集群提供一个名称,在本例中为 **dev2** 。

4.  提供主节点上公开的 Kubernetes API 的全限定 URL(例如, [`https://n1.dev2.apk8s.dev:6443`](https://n1.dev2.apk8s.dev:6443) )。

5.  提供群集 CA 证书。该证书很容易在默认名称空间的`default-token`中找到。要检索所需 PEM 格式的证书,首先在默认名称空间中列出秘密:`kubectl get secrets`。如果这是一个新的星团,那么`default-token`将可能是唯一的秘密。使用以下命令,用`default-token`替换`<secret name>`:

    ```
    kubectl get secret \
      $(kubectl get secret | grep default-token | awk '{print $1}') -o jsonpath="{['data']['ca\.crt']}" \
    | base64 --decode

    ```

6.  从上一节中设置的`gitlab-admin`服务帐户中提供服务令牌。使用以下命令:

    ```
    kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep gitlab-admin | awk '{print $1}')

    ```

7.  确保 RBAC 已启用。

#### 启用依赖关系

本章使用安装在 **dev2** 集群上的 GitLab Runner <sup>5</sup> 来构建带有 GitLab CI 的定制容器。 <sup>6</sup> 完成数据科学组的 Kubernetes 整合(见图 4-4 ):

1.  提供一个基本域。虽然在本章中没有使用,GitLab 可以使用这个基域进行自动开发 <sup>7</sup> 和 Knative <sup>8</sup> 集成。 **dev2** 的设置遵循第三章中的 DNS 指令,为`*.dev2`分配多个`A`记录,并分配每个工作节点的公共 IP 地址。`dev2.apk8s.dev`的任何子域都将解析为集群上的一个工作节点。

2.  安装 Helm 柄。 <sup>9</sup> GitLab 将 Helm 安装到集群上一个名为`gitlab-managed-apps`的新命名空间中。GitLab 使用 Helm 图在幕后管理其依赖的应用。Helm Tiller 可能已经安装在集群中,并且正在另一个命名空间中运行;但是,GitLab 需要自己的 Helm 柄。安装可能需要几分钟。新发布的头盔 3 不需要头盔 Helm 杆,请查看 GitLab 文档了解已安装的版本。

3.  最后,安装 GitLab Runner。安装可能需要几分钟。

![img/483120_1_En_4_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig4_HTML.jpg)

图 4-4

在 Kubernetes 集群上安装应用

## 自定义 JupyterLab 映像

Jupyter 笔记本 <sup>10</sup> (图 4-8 )正在成为基于 Python 的数据科学的主要工具。Jupyter 笔记本结合了实时可运行代码和基于 markdown 的文本片段,非常适合描述和演示计算过程及其相应的结果。JupyterLab 是下一代 Jupyter 笔记本电脑,结合了更新的用户界面和集成的文件浏览器,以及用于运行多个笔记本和终端的选项卡。JupyterLab(见图 4-6 )提供了一个健壮的集成开发环境,能够在一个容器中运行。

JupyterHub 是一个应用,设计用于在单个或多用户环境中提供 Jupyter 笔记本。“数据科学”一章探讨了如何使用 JupyterHub 来提供定制的 JupyterLab 映像。在平台内运行 JupyterHub 使软件开发人员、数据科学家和统计人员能够直接访问平台服务,包括文件系统、数据库、事件队列和对 Kubernetes API 的许可访问,如图 4-5 所示。

![img/483120_1_En_4_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig5_HTML.jpg)

图 4-5

Kubernetes 星团中的 JupyterLab

本章演示了如何使用 GitLab CI 来自动化构建自定义 JupyterLab 容器映像并将其推入 GitLab 的集成容器注册表的过程。

![img/483120_1_En_4_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig6_HTML.jpg)

图 4-6

使用 Python 和 Octave 内核的自定义 JupyterLab,以及`kubectl`

### 存储库和容器源

在 GitLab 组数据科学(ds)中创建一个名为`notebook-apk8s`的新项目。该项目从一个 Dockerfile 文件开始。

用清单 4-2 的内容创建一个名为`Dockerfile`的文件。

FROM jupyter/minimal-notebook:7a3e968dd212

USER root

ENV DEBIAN_FRONTEND noninteractive
RUN apt update
&& apt install -y apt-transport-https curl iputils-ping gnupg
RUN curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg
| sudo apt-key add -
RUN echo "deb https://apt.kubernetes.io/ kubernetes-xenial main"
| sudo tee -a /etc/apt/sources.list.d/kubernetes.list

RUN apt update
&& apt install -y kubectl git gcc mono-mcs musl-dev octave
&& rm -rf /var/lib/apt/lists/*

kubefwd for local development and testing

RUN apt-get clean && apt-get autoremove --purge
RUN wget https://github.com/txn2/kubefwd/releases/download/v1.8.4/kubefwd_amd64.deb
&& dpkg -i kubefwd_amd64.deb
&& rm kubefwd_amd64.deb

USER $NB_UID

Installs data science and machine learning Python packages

RUN pip install --no-cache
rubix
python-gitlab
scipy
numpy
pandas
scikit-learn
matplotlib
tensorflow
torch
torchvision
fastai
octave_kernel
jupyterlab-git

JupyterLab and server extensions

RUN jupyter labextension install @jupyterlab/git
RUN jupyter labextension install @jupyterlab/plotly-extension
RUN jupyter serverextension enable --py jupyterlab_git

Listing 4-2Custom JupyterLab


新的`Dockerfile`扩展了官方的最小 Jupyter 笔记本容器, <sup>11</sup> 增加了 Kubernetes 配置实用程序`kubectl`以及几个流行的基于 Python 的机器学习库。Jupyter 笔记本支持除 Python 之外的大量语言,包括 Octave、<sup>12</sup>MathWorks 对 MATLAB <sup>13</sup> 的开源替代。

通过构建本地 Docker 映像来测试新的`Dockerfile`。从包含`Dockerfile,`运行的目录中:

docker build -t jupyterlab-apk8s


构建自定义 Jupyter 笔记本可能需要几分钟时间。基本的`jupyter/minimal-notebook`大约有 3GB,在添加了十几个机器学习和数据科学包及其依赖项之后,新的`jupyterlab-apk8s`映像将近 7GB。

### 本地测试

在用于构建新容器的同一本地工作站上,用 Docker 运行并测试新容器。尽管新的`jupyterlab-apk8s`映像旨在从集群内部运行,但是安装了一个名为`kubefwd`的实用程序,以适应从远程 Kubernetes 集群到本地服务名称的端口转发。使用以下命令启动`jupyterlab-apk8s`:

docker run --rm --name jl -p 8888:8888
-v "\((pwd)":"/home/jovyan/work" \ -v "\)HOME/.kube/apk8s-dev2":"/home/jovyan/.kube/config"
--user root
-e GRANT_SUDO=yes
-e JUPYTER_ENABLE_LAB=yes -e RESTARTABLE=yes
jupyterlab-apk8s:latest


`docker run`命令将当前工作目录挂载到`/home/jovyan/work,`容器以用户`jovyan,`的身份运行,容器内的初始工作目录是`/home/jovyan/`。下一个卷挂载(`-v`)公开了新 **dev2** 集群的配置`apk8s-dev2`。参数`--user root`以 root 用户身份启动容器,并且是 kubefwd 实用程序所需的`sudo`访问所必需的,并通过`-e GRANT_SUDO=yes`启用。关于 Jupyter 笔记本底座容器暴露的特性列表,请参见官方文档 <sup>14</sup> 。

Note

用户`jovyan`是 Jupyter 社区的惯例,用于所有官方的 Jupyter 笔记本图片。名词 Jovian 是虚构的木星居民。

在启动新的 Jupyter Notebook 容器时,查看包含令牌的连接指令的初始日志输出。从容器输出中检索令牌后,访问 URL: `http://localhost:8888?token=<token>`

图 4-6 表示新`jupyterlab-apk8s`容器的运行实例。

#### 端口转发

新的`jupyterlab-apk8s`容器旨在运行在由 JupyterHub 提供的 **dev2** 集群中。然而,在本地工作站上的测试可以在`kubefwd` <sup>15</sup> `(developed by the author)`的帮助下模拟集群内的环境,该软件先前安装在`jupyterlab-apk8s`中用于本地测试支持。

在 Docker 容器内(在`localhost:8888`),在运行 Jupyter 笔记本内标题为**其他**的部分下,选择**终端**。一旦终端启动,提供以下命令来端口转发在 **dev2** 集群上的`monitoring`名称空间中运行的所有服务(参见图 4-7 ):

![img/483120_1_En_4_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig7_HTML.jpg)

图 4-7

端口转发远程 Kubernetes 集群

sudo kubefwd svc -n monitoring


实用程序`kubefwd`将远程 Kubernetes 集群上的 Pods backing 服务连接到本地工作站(在本例中为 Jupyter 笔记本)上的一组匹配的 DNS 名称和端口,并进行端口转发。一旦`kubefwd`开始运行,就可以连接到 [`http://prometheus-k8s.monitoring:9200`](http://prometheus-k8s.monitoring:9200) 等服务,就像它们来自远程集群一样。

#### 测试笔记本

创建一个新的笔记本来测试自定义 Jupyter 容器`jupyterlab-apk8s`。在运行笔记本的文件菜单下,选择**新建**和**笔记本**,在下拉列表中选择 **Python 3** 内核。将清单 4-3 中的 Python 代码输入到第一个输入单元格中,点击标签下菜单栏中的 **play** 按钮,或者使用键盘快捷键 **Shift-Enter** (见图 4-8 )。

import requests

response = requests.get('http://prometheus-k8s:9090/api/v1/query',
params={'query': "node_load1"})

if response.status_code == 200:
print("Got 200 response from Prometheus")

for node_data in response.json()['data']['result']:
value = node_data['value'][1]
name = node_data['metric']['instance']
print(f'Node {name} has a 1m load average of {value}')

Listing 4-3Python communicating with Prometheus


新创建的 Jupyter Notebook,`Untitled.ipynb`执行单个单元,该单元向 Prometheus 返回连接状态以及 dev2 集群中每个 Kubernetes 节点的当前一分钟平均负载,如图 4-8 所示。

![img/483120_1_En_4_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig8_HTML.jpg)

图 4-8

与 Prometheus 交流

### 额外学习

如果你是数据科学或机器学习的新手,这个定制的笔记本包含几个流行的库和框架供你入门。人工智能和机器学习的热门在线课程经常使用 Jupyter 笔记本或 MATLAB/Octave。Coursera 使用 MATLAB/Octave 提供了最受欢迎的机器学习课程之一 <sup>16</sup> ,由斯坦福大学教授、谷歌大脑联合创始人吴恩达教授。Udacity 提供了一门介绍性的纳米学位课程,名为*用 Python 进行 API 编程*、<sup>、 17 、大量使用 Jupyter 笔记本。杰瑞米·霍华德的 <sup>18</sup> fast.ai 在*程序员机器学习入门* <sup>19</sup> 课程中提供了一种独特的自上而下的方法,fast.ai Python 库包含在这个自定义映像中。</sup>

Jupyter 笔记本环境并不局限于预装的 Python 库;开发人员和数据科学家可以使用`pip install`来扩展 Python 环境,以满足他们的需求。命令`jupyter labextension` <sup>20</sup> 和`jupyter serverextension`也可用于扩展和定制 Jupyter 生态系统。 <sup>21</sup>

## 自动化

新的 Jupyter Notebook jupyterlab-apk8s 容器映像(在本章前面创建并在上一节测试)无需进一步修改即可使用。因为这个容器不包含专有代码或业务逻辑,所以它可能被上传到任何公共容器注册中心,包括 Docker Hub。以下命令使用 apk8s Docker Hub 帐户标记新映像,并将其推送到注册表,使其可供公众使用:

docker tag jupyterlab-apk8s:latest
apk8s/jupyterlab-apk8s:latest

docker push apk8s/jupyterlab-apk8s:latest


不是所有的容器都应该是可公开访问的,因为它们可能包含专有的业务逻辑或数据。除了私有容器注册的安全方面之外,保持对这些资产的控制,同时避免供应商锁定,也是一个很好的实践。幸运的是,开源 GitLab(安装在第二章)自带内置容器注册表。

构建`jupyterlab-apk8s`容器是一个简单的手动过程。新构建的`jupyterlab-apk8s`容器的版本与创建它的代码没有耦合。一个开发人员可能很容易做出改变,却忘记为其他开发人员推出新版本。自动化不仅解决了在代码版本和映像版本之间实施耦合的问题,而且还开放了添加测试套件和安全检查的能力。

本章的剩余部分使用 GitLab CI 来自动构建和存储前面定义的`jupyterlab-apk8s`容器。早些时候在 GitLab 中建立的新数据科学组已经被配置为通过 GitLab Runners 在新的 **dev2** 集群上执行 GitLab CI 操作。下一节将介绍如何为自动化容器构建和版本控制编写 GitLab CI 配置。

### GitLab 我们

如今有大量功能强大的 CI/CD 工具可用。Jenkins、Buildbot、Drone、Concourse 和许多其他设计良好的工具暴露了对复杂的软件集成、交付和部署过程自动化的复杂而稳定的方法的高度需求。

GitLab CI 与 GitLab 本身有深度融合,需要它运行;然而,这并不意味着一个项目只能由 GitLab 管理。Git VCS 允许多个远程,允许一个项目存在于多个托管的存储库中,如 GitHub 或 Bitbucket。GitLab 还支持基于*拉*和*推*的项目与存储库镜像的同步。 <sup>22</sup> 本书没有管理另一个应用,而是专门使用 GitLab CI 来集成 GitLab 和 Kubernetes。

GitLab CI 需要一个 GitLab 项目和一个 GitLab Runner。在本章的前面,一个名为 Data Science ( `gitlab.apk8s.dev/ds`)的 GitLab 组被配置,在为该组配置 Kubernetes 访问之后,安装了一个 GitLab Runner。在这个新的数据科学小组中,项目`jupyterlab-apk8s` ( `gitlab.apk8s.dev/ds/jupyterlab-apk8s`)得到了开发,并包括在前面部分构建和测试的单个`Dockerfile`。调用 GitLab CI 需要一个文件`.gitlab-ci.yml`,这将在下一节中介绍。

### 。吉塔实验室

当 GitLab 项目中的任何源存储库包含一个名为`.gitlab-ci.yml`的文件时,该文件的存在会调用 GitLab CI,它会创建一个新的流水线并开始运行所有定义的作业。在新的`jupyterlab-apk8s`项目中创建一个名为的文件,并添加清单 4-4 的内容。

stages:

  • build
  • test
  • deploy

tag_build:
stage: build
only:
- tags@ds/jupyterlab-apk8s
image:
# debug version is required for shell
name: gcr.io/kaniko-project/executor:debug-v0.10.0
entrypoint: [""]
script: |
# configure kaniko
export KCFG=\((printf '{"auths":{"%s":{"username":"%s","password":"%s"}}}' \ "\)CI_REGISTRY" "\(CI_REGISTRY_USER" "\)CI_REGISTRY_PASSWORD")
echo $KCFG > /kaniko/.docker/config.json

/kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile \
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG \
--destination $CI_REGISTRY_IMAGE:latest

Listing 4-4GitLab CI pipeline configuration


新的`.gitlab-ci.yml`配置文件包含一个名为`tag_build`的作业。作业名称由用户定义,可以包含任何描述性文本。GitLab CI 流水线有三个默认阶段:构建、测试和部署。`tag_build`作业被分配了 stage、build,并且仅在`ds/jupyterlab-apk8s`项目接收到推送到该项目的新标签时运行。

GitLab CI/CD 流水线配置,如清单 4-4 所示,通过规则和脚本为 GitLab 运行者提供构建和部署指令。有关流水线配置的详细信息,请参考 GitLab 的官方文档。 <sup>23</sup>

#### 加尼科

从 Docker 容器中构建容器映像需要安装主机服务器的 Docker 套接字。这种方法有相当大的安全隐患,使用 GitLab CI 和 Kubernetes 等自动化 CI/CD 解决方案很难实现。为了解决这个问题,Google 开发了 Kaniko, <sup>24</sup> 项目,专门设计为“在容器或 Kubernetes 集群中从 docker 文件构建容器映像的工具”

`tag_build`作业使用 Kaniko 映像。GitLab CI 检查指定的`git`标签并运行`/kaniko/executor`,指定`Dockerfile`。在执行程序被调用之前,Kaniko 配置 JSON 文件在`/kaniko/.docker/config.json`生成,向`ds/jupyterlab-apk8s`项目的容器注册中心提供认证凭证。

#### 综合环境变量

GitLab CI 为作业中指定的映像容器提供了一组广泛的环境变量 <sup>25</sup> 。`tag_build`作业使用环境变量`CI_REGISTRY`、`CI_REGISTRY_USER`和`CI_REGISTRY_PASSWORD`为 Kaniko 提供`ds/jupyterlab-apk8s`项目的容器注册中心的认证凭证。变量`CI_REGISTRY_IMAGE`是基于项目的映像的路径——在本例中是`reg.gitlab.apk8s.dev/ds/jupyterlab-apk8s`。最后,`CI_COMMIT_TAG`是触发该作业的`git`标签,用于与标签`latest`一起标记容器。

### 运行流水线

一旦标签被提交到包含文件`.gitlab-ci.yml`的`ds/jupyterlab-apk8s`存储库,GitLab CI 流水线就会被触发。标记提交,如下所示:

git commit v0.0.1


推送新标签:

git push origin v0.0.1


最后,打开网络浏览器至`<gitlab>/ds/jupyterlab-apk8s/pipelines`(见图 4-9 )。点击流水线**运行**状态查看作业。

![img/483120_1_En_4_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig9_HTML.jpg)

图 4-9

GitLab 我们的流水线

流水线详图(见图 4-10 )显示了`.gitlab-ci.yml`配置文件中定义的作业`tag_build`。容器工作流的最佳实践将涉及测试阶段的工作,包括功能测试和容器安全检查。

Container security

有大量专门介绍 CI/CD 和容器安全的书籍和资源。尽管超出了本书的范围,CoreOS 的 Clair <sup>26</sup> 等工具通过静态分析检查容器漏洞。纤毛, <sup>27</sup> 也通过 CoreOS,保证了网络的连通性。这两个项目都有很好的文档记录,并且支持 Kubernetes 部署。

![img/483120_1_En_4_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig10_HTML.jpg)

图 4-10

GitLab CI 流水线作业

接下来,点击`tag_build`任务,在 web 终端中查看其流程,如图 4-11 所示。`jupyterlab-apk8s`映像包含大量的依赖项,可能需要 20 到 30 分钟来构建。

![img/483120_1_En_4_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig11_HTML.jpg)

图 4-11

GitLab CI 运行作业

构建完成后,在数据科学组项目 **jupyterlab-apk8s** 的`reg.gitlab.apk8s.dev/ds/jupyterlab-apk8s`注册表中会有一个新的映像,标签为`v0.0.1`和`latest`,如图 4-12 所示。

![img/483120_1_En_4_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig12_HTML.jpg)

图 4-12

GitLab 容器注册表

## Kubernetes 中的手动测试

前面几节描述了一个名为 Data Science (ds)的 GitLab 组和项目 **jupyterlab-apk8s** 的创建,以及一个 GitLab CI 配置,该配置构建了一个定制的 Jupyter 笔记本容器,现在在`reg.gitlab.apk8s.dev/ds/jupyterlab-apk8s:v0.0.1`可用。

新的 Jupyter 笔记本旨在由 JupyterHub 进行配置,稍后将在数据科学一章中介绍。在 **dev2** 集群上手动测试新的`jupyterlab-apk8s`映像可以通过使用`kubectl`使用几个命令来完成。

`jupyterlab-apk8s`项目的 GitLab 容器注册是私有的,从 **dev2** 集群对其进行拉取需要带有访问令牌的 Kubernetes 秘密。通过单击左侧导航栏中的**设置**,选择**存储库**,并单击**部署令牌**部分中的扩展按钮,为`jupyterlab-apk8s` GitLab 项目注册表创建一个访问令牌。

创建一个名为 **k8s** 的令牌,跳过**在**字段到期,并检查作用域中的 **read_registry** (见图 4-13 )。

![img/483120_1_En_4_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig13_HTML.jpg)

图 4-13

GitLab 部署令牌

### 准备名称空间

创建名称空间`notebook-testing`(在 dev2 集群中),缩进以测试定制笔记本:

kubectl create namespace notebook-testing


用 GitLab 生成的密钥和值添加一个`docker-registry` Secret。图 2-13 显示了一个生成的令牌,带有密钥`gitlab+deploy-token-3`和值`W8x6_MxWxMYKRTfhMstU`。运行以下命令,在`notebook-testing`名称空间中创建新的密码(相应地更改用户名、密码、服务器和电子邮件):

kubectl create secret docker-registry
ds-jupyterlab-apk8s-regcred
--namespace=notebook-testing
--docker-server=reg.gitlab.apk8s.dev
--docker-username=gitlab+deploy-token-3
--docker-password=W8x6_MxWxMYKRTfhMstU
--docker-email=cjimti@gmail.com


接下来,用新的 docker-registry 秘密更新名称空间的默认 Kubernetes 服务帐户。Kubernetes 使用默认服务帐户的`docker-registry`秘密向 GitLab 容器注册中心进行认证。

使用以下命令编辑现有的默认服务帐户:

kubectl edit sa default -n notebook-testing


添加清单 4-5 中的最后两行并保存。

Note

另一种方法是将当前的服务帐户清单保存到文件`kubectl get sa default -n notebook-testing -o yaml > ./sa.yaml`,编辑`sa.yaml`,并使用`kubectl apply -f sa.yml`重新应用。

...

apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: "2019-08-01T20:18:34Z"
name: default
namespace: notebook-testing
resourceVersion: "21353"
selfLink: /api/v1/namespaces/notebook-testing/serviceaccounts/default
uid: 2240fc34-b050-4ccb-9f96-f4f378842dbd
secrets:

  • name: default-token-dhj94
    imagePullSecrets:
  • name: ds-jupyterlab-apk8s-regcred

Listing 4-5Editing the notebook-testing default Service Account


### 运行笔记本

上一节配置了`notebook-testing`名称空间中的默认服务帐户,该帐户能够根据`ds/jupyterlab-apk8s`项目提供的 GitLab 注册表进行身份验证。运行容器进行测试意味着几乎不需要保存和版本化声明性配置。Kubectl 提供了一个命令性的`kubectl run` <sup>28</sup> 命令,该命令可以生成一个最小的 Pod 配置,用于临时任务,比如测试、调试、实验或者仅仅是演示功能。

下面的`kubectl`命令启动一个名为`test-notebook`的 Pod。当完成时,Pod 被设置为移除其配置,并且永远不会通过标志`--rm=true`和`--restart=Never`自动重启。环境变量`JUPYTER_ENABLE_LAB=yes`通知 Jupyter 笔记本在更新后的 JupyterLab 模式下启动。

发出以下命令,观察类似图 4-14 的输出:

![img/483120_1_En_4_Fig14_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig14_HTML.jpg)

图 4-14

运行自定义 Jupyter 笔记本电脑

kubectl run -i -t test-notebook
--namespace=notebook-testing
--restart=Never --rm=true
--env="JUPYTER_ENABLE_LAB=yes"
--image=reg.gitlab.apk8s.dev/ds/jupyterlab-apk8s:v0.0.1


容器`jupyterlab-apk8s`很大,根据网络条件,可能需要几分钟来下载、创建和启动 Pod。一旦 Pod 运行,从输出日志中复制生成的令牌,如图 4-14 所示。

默认情况下,Jupyter 笔记本监听端口 8888。在这样的测试和演示中,通常将 Pod 容器直接转发到本地工作站,而不是配置服务和入口。

Caution

Jupyter 笔记本有意并有目的地允许远程代码执行。将 Jupyter 笔记本暴露给公共接口需要适当的安全考虑,在“索引和分析”和“AIML 平台化”章节中会进一步讨论。

使用以下命令将`test-notebook` Pod 端口转发:

kubectl port-forward test-notebook 8888:8888
-n notebook-testing


一旦 kubectl 开始将本地工作站上的端口 8888 转发到 Pod 中的端口 8888,使用容器输出中提供的令牌(如图 4-14 所示)在 web 浏览器中打开`jupyterlab-apk8s`。浏览至

http://localhost:8888/?token=3db...1e5


新的 Jupyter 笔记本的加载和操作应该与本章前面的本地测试一样(见图 4-6 ),只是这次是在 **dev2** Kubernetes 集群中。下一节将介绍如何使用 GitLab API 用户令牌从笔记本存储库中提取和推送文件。

### 知识库访问

各种机器学习自动化和无服务器平台使用 CI/CD 组件以及 VCS(版本控制系统)库来构建和部署容器化的工作负载。下面是一个简单的方法来演示一个源自集群的 CI/CD 流水线,在这个例子中,从`jupyterlab-apk8s`运行容器中修改`jupyterlab-apk8s`映像。

从**用户设置**中创建一个 GitLab **个人访问令牌**(可以在用户头像下的下拉菜单中找到)。令牌名称仅用于参考。检查 **api** 范围,点击“创建个人访问令牌”(见图 4-15 )。记录生成的令牌值,因为这不会再次显示;但是,用户可以随时生成新令牌。

![img/483120_1_En_4_Fig15_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_4_Fig15_HTML.jpg)

图 4-15

GitLab 部署令牌

在网络浏览器中返回到运行自定义 Jupyter 笔记本`jupyterlab-apk8s`的`test-notebook`窗格。在启动器选项卡中,在标题为**其他**的部分下,选择**终端**。一旦命令提示符可用,克隆`jupyterlab-apk8s`、`and`的源代码,用之前生成的令牌替换它:

export GL_TOKEN=JDYxbywCZqS_N8zAsB6z
export GL_PROJ=gitlab.apk8s.dev/ds/jupyterlab-apk8s.git
git clone https://oauth2:\(GL_TOKEN@\)GL_PROJ


Caution

和其他开发者分享这个跑步笔记本,暴露了你的个人 GitLab 令牌。“索引和分析”和“AIML 平台化”这两章演示了笔记本和存储库访问的替代方法。

在本书的后面,“平台化区块链”一章使用 Python 包`web3`和`py-solc`从用户提供的 Jupyter 笔记本电脑中与集群内以太坊区块链网络进行通信。“流水线”一章使用 Python 包`paho-mqtt`开发测试 MQTT 客户机和事件监听器。将这些包添加到自定义的 JupyterLab 容器中可以确保开发人员始终可以使用一组标准的依赖项。额外的包很容易在运行时添加。

Note

`jupyterlab-apk8s`容器很大,包含许多可能不是所有开发人员都使用的包和应用。开发者也可能需要额外的包。本书中定义的`jupyterlab-apk8s`容器是为了演示的目的。一个可选的实践包括创建一套侧重于特定开发领域的基本定制笔记本。

接下来,打开克隆项目中的 docker 文件,编辑包含`pip install`命令的`RUN`层。添加 Python 包`paho-mqtt`、`web3`和`py-solc`,如清单 4-5 所示。

Installs blockchain, data science, and machine learning

python packages

RUN pip install --no-cache
rubix
python-gitlab
scipy
numpy
pandas
scikit-learn
matplotlib
tensorflow
torch
torchvision
fastai
octave_kernel
jupyterlab-git
paho-mqtt
web3
py-solc

Listing 4-5Source fragment from Dockerfile


最后,提交更改,标记 commit v0.1.2,并将其推回远程:

git commit -a -m "Blockchain and IoT packages."
git tag v0.1.2
git push origin v0.1.2


在本章的前面,项目`ds/jupyterlab-apk8s`被配置为在新标签被推送到存储库时触发 GitLab CI 流水线。一旦 CI 流水线完成,容器`reg.gitlab.apk8s.dev/ds/jupyterlab-apk8s:v0.1.2`将变得可用。

## GitOps

GitOps, <sup>29</sup> 是 Weaveworks 推广的一个流程, <sup>30</sup> 是 Kubernetes CI/CD 范围内的另一个潮流概念。GitOps 包括应用对`git push`事件的反应。GitOps 主要关注与 Git 存储库中的配置所描述的状态相匹配的 Kubernetes 集群。简单来说,GitOps 的目标是用`git push`取代`kubectl apply`。广受欢迎和支持的 GitOps 实现包括 ArgoCD、 <sup>31</sup> Flux、 <sup>32</sup> 和 Jenkins X. <sup>33</sup>

## 摘要

本章从较高的层面介绍了 CI/CD 概念,以及从开发和运营的角度来看它们的适用范围。使用在 Vultr 运行的单节点(k3s) Kubernetes 集群和在 Scaleway 运行的四节点定制 Kubernetes 集群展示了应用(平台)级别的多集群、多云协调。运行多个责任划分明确的小型集群是一种新兴的架构模式。本章开发了一个定制的 Jupyter 笔记本,使用 GitLab CI 流水线构建,并被手动部署和用于进一步开发其功能。

本章没有涉及部署。部署通常被定义为 CI/CD 流程中的最后一步,并且与特定的应用及其生产需求高度相关。

本章的总体目标是将 CI/CD(以一种简单的方式)作为一个平台特性来呈现,展示扩展到独立开发关注点之外的机会,并将这种强大的机制集成到企业应用平台的核心中。新的软件平台是可扩展的,容器化的工作负载是扩展在 Kubernetes 上开发的平台的自然手段。在 Kubernetes 生态系统中,开发和部署容器都是可能的。正如本章所展示的,提供一个可以从内部无限扩展的平台是可能的。

下一章将介绍数据,特别是进入数据库、索引、事件队列和文件系统的数据流水线。数据是本书中介绍的平台的核心,从数据的收集、分析、处理和呈现到从人工智能的推理或物联网事件触发的智能合同上的区块链交易结果中产生的新数据。接下来的章节打算从平台的角度开始处理数据。

<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1

[`https://jupyterlab.readthedocs.io`](https://jupyterlab.readthedocs.io)

  2

[`https://jupyterhub.readthedocs.io`](https://jupyterhub.readthedocs.io)

  3

[`www.scaleway.com`](http://www.scaleway.com)

  4

[`https://gitlab.apk8s.dev/help/user/project/clusters/index#security-implications`](https://gitlab.apk8s.dev/help/user/project/clusters/index%2523security-implications)

  5

[`https://docs.gitlab.com/runner/`](https://docs.gitlab.com/runner/)

  6

[`https://docs.gitlab.com/ce/ci/`](https://docs.gitlab.com/ce/ci/)

  7

[`https://docs.gitlab.com/ee/topics/autodevops/`](https://docs.gitlab.com/ee/topics/autodevops/)

  8

[`https://knative.dev/`](https://knative.dev/)

  9

[`https://helm.sh/docs/glossary/#tiller`](https://helm.sh/docs/glossary/%2523tiller)

  10

[`https://jupyterlab.readthedocs.io/en/stable/user/notebook.html`](https://jupyterlab.readthedocs.io/en/stable/user/notebook.html)

  11

[`https://github.com/jupyter/docker-stacks/tree/master/minimal-notebook`](https://github.com/jupyter/docker-stacks/tree/master/minimal-notebook)

  12

[`www.gnu.org/software/octave/`](http://www.gnu.org/software/octave/)

  13

[`www.mathworks.com/products/matlab.html`](http://www.mathworks.com/products/matlab.html)

  14

[`https://jupyter-docker-stacks.readthedocs.io/en/latest/using/common.html`](https://jupyter-docker-stacks.readthedocs.io/en/latest/using/common.html)

  15

[`https://github.com/txn2/kubefwd`](https://github.com/txn2/kubefwd)

  16

[`www.coursera.org/learn/machine-learning`](http://www.coursera.org/learn/machine-learning)

  17

[`www.udacity.com/course/ai-programming-python-nanodegree--nd089`](http://www.udacity.com/course/ai-programming-python-nanodegree%252D%252Dnd089)

  18

[`www.fast.ai/about/#jeremy`](http://www.fast.ai/about/%2523jeremy)

  19

[`http://course18.fast.ai/ml`](http://course18.fast.ai/ml)

  20

[`https://jupyterlab.readthedocs.io/en/stable/user/extensions.html`](https://jupyterlab.readthedocs.io/en/stable/user/extensions.html)

  21

[`https://blog.jupyter.org/99-ways-to-extend-the-jupyter-ecosystem-11e5dab7c54`](https://blog.jupyter.org/99-ways-to-extend-the-jupyter-ecosystem-11e5dab7c54)

  22

[`https://docs.gitlab.com/12.1/ce/workflow/repository_mirroring.html`](https://docs.gitlab.com/12.1/ce/workflow/repository_mirroring.html)

  23

[`https://docs.gitlab.com/ee/ci/yaml/`](https://docs.gitlab.com/ee/ci/yaml/)

  24

[`https://github.com/GoogleContainerTools/kaniko`](https://github.com/GoogleContainerTools/kaniko)

  25

[`https://docs.gitlab.com/ce/ci/variables/`](https://docs.gitlab.com/ce/ci/variables/)

  26

[`https://github.com/coreos/clair`](https://github.com/coreos/clair)

  27

[`https://github.com/cilium/cilium`](https://github.com/cilium/cilium)

  28

[`https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#run`](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands%2523run)

  29

[`www.gitops.tech/`](http://www.gitops.tech/)

  30

[`www.weave.works/`](http://www.weave.works/)

  31

[`https://argoproj.github.io/argo-cd/`](https://argoproj.github.io/argo-cd/)

  32

[`https://github.com/fluxcd/flux`](https://github.com/fluxcd/flux)

  33

[`https://jenkins-x.io/`](https://jenkins-x.io/)

  34

[`https://content.pivotal.io/blog/kubernetes-one-cluster-or-many`](https://content.pivotal.io/blog/kubernetes-one-cluster-or-many)

 </aside>

# 五、流水线

所有的软件平台都是通过一种或另一种形式的数据通信来运行的。数据驱动服务使用数据来确定流程(或逻辑),而事件驱动服务侦听事件来执行预定的流程和逻辑。从另一个意义上来说,这些服务是在传递数据,而数据的传输本身就是一个事件。数据驱动与事件驱动的架构通常归结为它们处理的数据的意义和价值,以及消费数据的服务如何操作、转换、处理或分析数据。

管理数据的应用通常是软件平台的核心组件。如何处理和维护数据通常由业务需求驱动的业务逻辑决定。一个成熟的软件平台应该提供高效灵活的应用逻辑,能够适应一系列数据管理需求,支持数据驱动和事件驱动的实现。

物联网、机器学习和区块链技术产生和消费数据,每种技术都有特定的需求:物联网设备在交流状态的同时产生和消费大量实时数据。机器学习通常需要大量的、有组织的数据目录。最后,区块链以数据的形式消费和发射事件。

本书的上下文和手头的平台认为数据尽可能通用,主要关注收集和分发,将数据的意义和价值留给更高级别的应用需求。本章重点介绍如何通过数据流水线将数据从发布服务器移动到订阅服务器。

## 国家和 Kubernetes

数据库管理系统(数据库)是有状态应用。传统的整体架构通常围绕一个跨多个域共享的中央数据库,而微服务架构则倾向于多个数据库,这些数据库被隔离到特定的服务和域。不管整个系统架构如何,数据库应用都是有状态的,并且不是所有有状态的应用都能轻松利用 Kubernetes 的优势。

Kubernetes 的容器化和容器编排非常适合构建由独立的、可移植的和可伸缩的组件组成的可扩展系统。所有 Kubernetes 平台的核心是 Pods,即最好被视为短暂和无状态的容器化工作负载。pod 预计会来来去去,根据规则扩展和缩减,或者在底层基础架构出现故障或进行维护时意外终止和重新生成。设计以这种方式运行的组件实现了 Kubernetes 强大的可伸缩性、性能、冗余和自修复能力的优势。在这种架构中操作有状态的应用可能很有挑战性。然而,许多应用可以在分布式体系结构上维护状态;这些应用非常适合 Kubernetes。

Kubernetes 适合通过无状态 pod 支持的庞大服务网络来交流数据和事件。然而,数据的存储和检索是有状态的活动,这个概念不太适合 Kubernetes 的短暂工作负载。移除或更换 Pod 后,Pod 中存储的数据会丢失。pod 可以连接到外部永久卷;然而,在多个 pod 之间共享持久卷并没有得到存储提供商的广泛支持,并且难以实现可接受的性能和稳定性。最后,在单个 Pod 中操作有状态的应用既不可伸缩也不容错。与 Kubernetes 配合良好的有状态应用(如数据库)通过分布式节点网络维护状态。

Kubernetes 以 StatefulSets 的形式提供有状态功能。StatefulSets 基本上等同于部署,但是部署枚举的 pod 能够通过卷声明模板重新附加到分配给它们的存储。StatefulSet 创建以序号索引命名的 pod,例如`PODNAME-0..n`,并提供稳定的网络 ID,例如`PODNAME-4.somenamespace.svc.cluster.local`。如果 StatefulSet 中的一个单元崩溃、升级或重新安排到另一个节点,Kubernetes 会创建一个具有相同名称和网络 ID 的新单元,并重新挂接以前与该单元名称关联的任何持久性卷。即使它们的工作负载是短暂的,但通过持久的命名和存储,pod 在概念上是有状态的。Elasticsearch、Cassandra 和 Kafka 是数据管理应用的一些例子,当部署在多个有状态的 pod 中时,它们工作得很好。像这样的系统管理它们的数据复制、分布式处理,并在其自我管理的集群网络中处理故障或缺失的节点。正确配置后,这些应用会在缺少一个节点(在本例中为一个单元)时继续执行,并且通常会提供无限的水平扩展。在某些方面,Kubernetes 使扩展和管理这些应用比传统方法更容易,传统方法涉及配置虚拟机或裸机服务器的繁琐任务。相比之下,Kubernetes 只需编辑和重新应用配置,就能增加所需的 pod 数量。

## 实时数据体系结构

旨在提供机器学习、物联网和区块链功能的软件平台需要收集、转换、处理和管理数据、元数据和指标的能力。接下来的几章将介绍一些能够在 Kubernetes 集群中运行的企业级应用,提供实时数据收集、路由、转换、索引以及数据和指标的管理(参见图 5-1 )。

![img/483120_1_En_5_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_5_Fig1_HTML.jpg)

图 5-1

数据管理架构

以下部分涵盖了本章中用于组装实时分布式流平台的技术,该平台可用于任何形式的入站数据,并且是为物联网专门定制的。

### 消息和事件队列

Web 和移动应用通常在有限的一组已定义的数据结构上运行;自定义 API 端点在结构化数据库中验证、验证和持久化这些对象。相比之下,物联网平台通常需要能够接受各种互联设备产生的各种数据类型和结构。物联网平台主要负责向其他设备和服务分发设备状态。

发布和订阅(也称为发布/订阅)应用解决了收集和分发数据(通常称为事件或消息)的问题,并将几乎无限数量的生产者与几乎任意数量的感兴趣的消费者联系起来。本章重点介绍如何为所有实时数据和事件分发实现高度可用的分布式流媒体平台 Apache Kafka,以及为物联网专门构建的 MQTT 代理 Mosquitto。

#### 分布式流媒体平台

Apache Kafka 将自己标榜为分布式流媒体平台,并充当平台内实时数据操作的一种“中枢神经系统”。Kafka 可以通过六个节点和适当的配置每秒处理数十万条消息,这一能力远远超过了以数据为中心的大型企业之外的大多数用例。Kafka 将消息(数据)记录为主题内的记录,由键、值和时间戳组成。Kafka 通过其消费者、生产者、流和连接器 API 管理记录的关系、收集和分发。

Kafka 为所有主流编程语言提供了稳定成熟的客户端库,允许定制平台组件实时操作数据(事件和消息)。

#### MQTT 和物联网

MQTT <sup>1</sup> (消息队列遥测传输)是一种发布/订阅消息协议,专为物联网和 IIoT(工业物联网)实现而设计。设备可以通过 MQTT 代理发布和订阅主题,代理可以被桥接在一起。MQTT 被设计成轻量级的,能够在资源受限的环境中运行,包括 Raspberry Pi。<sup>22</sup>

考虑一个组织,该组织有一个工厂,操作数千个传感器和控制器,直接与内部 MQTT 代理通信。该组织还订阅了一个云托管的 MQTT 解决方案,用于从远程或隔离的设备收集指标。如图 5-2 所示,这些代理中的每一个都可以桥接在一起,并与一个更大的数据平台进行双向通信。

![img/483120_1_En_5_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_5_Fig2_HTML.jpg)

图 5-2

MQTT 网络

Mosquitto 是一个流行的开源 MQTT 代理,本章稍后将为集群内 MQTT 操作进行配置。Mosquitto 可以发布和订阅任何其他 MQTT 兼容的代理,通常是本地或基于云的 SaaS。

## 发展环境

本章使用第四章中相同的、廉价的 Scaleway 四节点集群设置。该集群包括一个用于 Kubernetes 主节点的 DEV1-M (3 个 CPU/4G RAM/40G SSD)和三个用于工作节点的 DEV1-L (4 个 CPU/8G RAM/80GB SSD)。配置和操作 Apache Kafka、Apache NiFi、Elasticsearch 和 Logstash 利用了这个小型开发集群的大部分 CPU 和 RAM。这些规格是绝对的最低要求,应该根据需要进行扩展。

Note

本章中使用的整个三节点开发集群相当于一个 12 核 CPU/32GB RAM 服务器实例,类似于典型企业配置中的单个生产节点。

### 集群范围的配置

本章使用与第三章中详述的相同的通用 Kubernetes 配置,包括入口 Nginx、证书管理器、Rook-Ceph 和监控配置。如果遵循前面的章节,从清单 [3-15](https://doi.org/10.1007/978-1-4842-5611-4_3-15) 到`cluster-apk8s-dev3/000-cluster`中的目录`cluster-apk8s-dev1/000-cluster`复制并应用配置清单,并创建目录`cluster-apk8s-dev3/003-data`(如清单 5-1 所示)来保存本章使用的清单。

.
└── cluster-apk8s-dev1
└── cluster-apk8s-dev2
└── cluster-apk8s-dev3
└── 000-cluster
├── 00-ingress-nginx
├── 10-cert-manager
├── 20-rook-ceph
└── 30-monitoring
└── 003-data

Listing 5-1Development Cluster configuration layout


Note

以数字为前缀的目录是推断优先顺序的一个简单约定。配置清单通常可以以任何顺序应用。

### 数据命名空间

创建目录`cluster-apk8s-dev3/003-data/000-namespace`来包含名称空间范围的配置清单。考虑到其规模和演示用途,此开发集群应被视为单租户。然而,在基于租户的名称空间(例如`clientx-data`)中包含所有与数据相关的功能,允许应用细粒度的基于角色的访问控制和网络规则。因为这个开发集群只有一个租户,所以名称空间`data`是合适的。以下配置适用于新数据命名空间中的所有服务。

在`cluster-apk8s-dev3/003-data/000-namespace`目录中,在清单 5-2 中的一个名为`00-namespace.yml`的文件中创建一个 Kubernetes 名称空间。

apiVersion: v1
kind: Namespace
metadata:
name: data

Listing 5-2Data Namespace


应用命名空间配置:

$ kubectl apply -f 00-namespace.yml


### TLS 证书

在本章的后面,安全入口配置应用于`nifi.data.dev3.apk8s.dev`的 NiFi 和`kib.data.dev3.apk8s.dev`的 Kibana,提供对其用户界面的外部访问。证书管理器和集群发行者应该出现在集群中(参见第三章)。

在清单 5-3 中的文件`05-certs.yml`中创建一个证书配置。

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: data-cert
namespace: data
spec:
secretName: data-production-tls
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
commonName: data.dev3.apk8s.dev
dnsNames:
- data.dev3.apk8s.dev
- nifi.data.dev3.apk8s.dev
- kib.data.dev3.apk8s.dev
acme:
config:
- http01:
ingressClass: nginx
domains:
- data.dev3.apk8s.dev
- nifi.data.dev3.apk8s.dev
- kib.data.dev3.apk8s.dev

Listing 5-3Certificates for the data Namespace


应用证书配置:

$ kubectl apply -f 05-certs.yml


### 基本认证

开发集群使用基本认证 <sup>3</sup> (基本认证)作为保护入口的便利方法。跨入口使用单个基本身份验证秘密简化了开发过程中身份验证的使用,并且在需要时可以用更复杂的方法(如 OAuth)来代替。

用实用程序`htpasswd` : <sup>4</sup> 创建一个名为 auth 的文件

$ htpasswd -c ./auth sysop


用前面生成的`auth`文件创建一个名为`sysop-basic-auth`的通用 Kubernetes 秘密:

$ kubectl create secret generic sysop-basic-auth
--from-file auth -n data


## Apache 动物园管理员

Apache Zookeeper<sup>5</sup>已经成为许多需要分布式协作的流行应用的标准,包括 Hadoop、 <sup>6</sup> HBase、 <sup>7</sup> Kafka、NiFi 和 Elasticsearch。在这一章中,Kafka 和 NiFi 都使用了一个共享的 Zookeeper 集群。Elasticsearch 在这个环境中作为单个节点运行;然而,更大的弹性搜索集群也可以利用共享的动物园管理员。

Zookeeper 可以被缩放以容忍给定数量的故障节点;然而,一些架构倾向于多个独立的安装,以避免单点故障。缩放或多个 Zookeeper 配置是一个生产问题,而在开发环境中共享该服务可以更好地利用有限的资源。

创建目录`cluster-apk8s-dev3/003-data/010-zookeeper`。在新的`010-zookeeper`目录中,从清单 5-4 中创建一个名为`10-service.yml`的文件。

apiVersion: v1
kind: Service
metadata:
name: zookeeper
namespace: data
spec:
ports:
- name: client
port: 2181
protocol: TCP
targetPort: client
selector:
app: zookeeper
sessionAffinity: None
type: ClusterIP

Listing 5-4Zookeeper Service


应用 Zookeeper 服务配置:

$ kubectl apply -f 10-service.yml


Kafka 和 NiFi 等 Zookeeper 客户端管理它们与单个节点的关系。Kubernetes 中的标准服务定义被分配了一个 IP 地址,通常被配置为将与该服务的任何通信路由到与指定选择器和端口匹配的任何 Pod。然而,像 Zookeeper 这样的应用要求每个 Zookeeper 节点(作为 Pod 运行)能够与它的对等节点(作为 Pod 运行)进行通信。标准的 Kubernetes 服务不足以在对等感知集群中使用,因为每个节点都必须能够专门与其他节点通信,而不仅仅是与选择器和端口匹配的任何节点。Kubernetes 通过无头服务的概念提供这种功能, <sup>8</sup> ,这是一种没有定义`ClusterIP`(`clusterIP: None`)的服务。下面的服务定义创建了一个无头服务,为匹配选择器 app 的 pod 返回 DNS 条目:zookeeper,如下一节描述的 StatefulSet 中所定义的。

在清单 5-5 中的文件`10-service-headless.yml`中创建一个无头服务配置。

apiVersion: v1
kind: Service
metadata:
name: zookeeper-headless
namespace: data
spec:
clusterIP: None
ports:
- name: client
port: 2181
protocol: TCP
targetPort: 2181
- name: election
port: 3888
protocol: TCP
targetPort: 3888
- name: server
port: 2888
protocol: TCP
targetPort: 2888
selector:
app: zookeeper
sessionAffinity: None
type: ClusterIP

Listing 5-5Zookeeper Headless Service


应用 Zookeeper 无头服务配置:

$ kubectl apply -f 10-service-headless.yml


接下来,在清单 5-6 中的文件`40-statefulset.yml`中创建一个 StatefulSet 配置。

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
namespace: data
spec:
podManagementPolicy: OrderedReady
replicas: 2
revisionHistoryLimit: 1
selector:
matchLabels:
app: zookeeper
serviceName: zookeeper-headless
template:
metadata:
labels:
app: zookeeper
spec:
containers:
- command:
- /bin/bash
- -xec
- zkGenConfig.sh && exec zkServer.sh start-foreground
env:
- name: ZK_REPLICAS
value: "2"
- name: JMXAUTH
value: "false"
- name: JMXDISABLE
value: "false"
- name: JMXPORT
value: "1099"
- name: JMXSSL
value: "false"
- name: ZK_CLIENT_PORT
value: "2181"
- name: ZK_ELECTION_PORT
value: "3888"
- name: ZK_HEAP_SIZE
value: 1G
- name: ZK_INIT_LIMIT
value: "5"
- name: ZK_LOG_LEVEL
value: INFO
- name: ZK_MAX_CLIENT_CNXNS
value: "60"
- name: ZK_MAX_SESSION_TIMEOUT
value: "40000"
- name: ZK_MIN_SESSION_TIMEOUT
value: "4000"
- name: ZK_PURGE_INTERVAL

          value: "0"
        - name: ZK_SERVER_PORT
          value: "2888"
        - name: ZK_SNAP_RETAIN_COUNT
          value: "3"
        - name: ZK_SYNC_LIMIT
          value: "10"
        - name: ZK_TICK_TIME
          value: "2000"
      image: gcr.io/google_samples/k8szk:v3
      imagePullPolicy: IfNotPresent
      livenessProbe:
        exec:
          command:
            - zkOk.sh
        failureThreshold: 3
        initialDelaySeconds: 20
        periodSeconds: 10
        successThreshold: 1
        timeoutSeconds: 1
      name: zookeeper
      ports:
        - containerPort: 2181
          name: client
          protocol: TCP
        - containerPort: 3888
          name: election
          protocol: TCP
        - containerPort: 2888
          name: server
          protocol: TCP
      readinessProbe:
        exec:
          command:

            - zkOk.sh
        failureThreshold: 3
        initialDelaySeconds: 20
        periodSeconds: 10
        successThreshold: 1
        timeoutSeconds: 1
      resources: {}
      terminationMessagePath: /dev/termination-log
      terminationMessagePolicy: File
      volumeMounts:
        - mountPath: /var/lib/zookeeper
          name: data
  dnsPolicy: ClusterFirst
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext:
    fsGroup: 1000
    runAsUser: 1000
  terminationGracePeriodSeconds: 30
  volumes:
    - emptyDir: {}
      name: data

updateStrategy:
type: OnDelete

Listing 5-6Zookeeper StatefulSet


最后,应用 Zookeeper StatefulSet 配置:

$ kubectl apply -f 40-statefulset.yml


Zookeeper 是 Kafka 和 NiFi 的共享依赖项,现在可以在开发集群上使用。下一节将建立一个双节点 Apache Kafka 集群。

## ApacheKafka

Apache Kafka”用于构建实时数据流水线和流媒体应用。它具有横向可伸缩性、容错性、极快的速度,并在数千家公司的生产中运行。” <sup>9</sup> 以下配置建立了一个双节点 Kafka 集群,非常适合小规模开发环境和数据科学活动。如前所述,Apache Kafka 是这个数据驱动平台的中枢神经系统。除了在每一种主流编程语言中提供稳定且功能丰富的客户端库之外,许多数据管理应用还开发了一流的连接器来发布和订阅 Kafka 事件,包括 Logstash 和 NiFi,这将在本章的后面进行演示;见图 5-3 。

![img/483120_1_En_5_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_5_Fig3_HTML.jpg)

图 5-3

ApacheKafka

Apache Kafka 等高性能、低延迟事件队列在高度优化的专用裸机服务器上以最高效率运行。在容器中,在具有抽象存储和覆盖网络的共享虚拟实例上运行 Kafka(如本书中定义的 Kubernetes 集群),会显著降低其效率和吞吐量。然而,在某些情况下,最佳性能的降低可能不会被注意到,或者可能很容易通过缩放来补偿。在 Kubernetes 中运行 Kafka 带来了许多优势,包括统一网络、DNS、可伸缩性、自修复、安全性、监控以及与其他组件的统一控制面板。在概念层面上,Kafka 是构成本书所描述的基于 Kubernetes 的数据平台的众多组件之一。虽然 Kafka 本身可能不会从 Kubernetes 内部的管理中获得大量利润,但更大的数据平台通过其基本组件之间更高的凝聚力而受益于 Kafka 的包容性。

以下配置以类似于 Zookeeper(在上一节中配置)的方式设置 Kafka,并添加了持久卷,如图 5-4 所示。

![img/483120_1_En_5_Fig4_HTML.png](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_5_Fig4_HTML.png)

图 5-4

ApacheKafka 和动物园管理员 Kubernetes 配置

创建目录`cluster-apk8s-dev3/003-data/020-kafka`。在新的`020-kafka`目录中,从清单 5-7 中创建一个名为`10-service.yml`的文件。

apiVersion: v1
kind: Service
metadata:
name: kafka
namespace: data
spec:
ports:
- name: broker
port: 9092
protocol: TCP
targetPort: kafka
selector:
app: kafka
sessionAffinity: None
type: ClusterIP

Listing 5-7Kafka Service


应用 Kafka 服务配置:

$ kubectl apply -f 10-service.yml


接下来,在清单 5-8 中名为`10-service-headless.yml`的文件中为 Kafka 创建一个无头服务配置。

apiVersion: v1
kind: Service
metadata:
name: kafka-headless
namespace: data
spec:
clusterIP: None
ports:
- name: broker
port: 9092
protocol: TCP
targetPort: 9092
selector:
app: kafka
sessionAffinity: None
type: ClusterIP

Listing 5-8Kafka Headless Service


应用 Kafka Headless 服务配置:

$ kubectl apply -f 10-service-headless.yml


接下来,在清单 5-9 中名为`40-statefulset.yml`的文件中为 Kafka 创建一个 StatefulSet 配置。

以下配置使用由 Confluent Inc. <sup>10</sup> 维护的 Kafka 容器 Confluent 为其围绕 Kafka 构建的开源事件流平台提供商业支持。本书中使用的 Kafka 功能可以与 Confluent 的 Kafka 发行版和标准的上游 Apache Kafka 一起使用。

apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: kafka
name: kafka
namespace: data
spec:
podManagementPolicy: OrderedReady
replicas: 2
revisionHistoryLimit: 1
selector:
matchLabels:
app: kafka
serviceName: kafka-headless
template:
metadata:
labels:
app: kafka
spec:
containers:
- command:
- sh
- -exc

        - |
          unset KAFKA_PORT && \
          export KAFKA_BROKER_ID=${HOSTNAME##*-} && \
          export KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://${POD_IP}:9092 && \
          exec /etc/confluent/docker/run
      env:
        - name: POD_IP
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: status.podIP
        - name: KAFKA_HEAP_OPTS
          value: -Xmx1G -Xms1G
        - name: KAFKA_ZOOKEEPER_CONNECT
          value: zookeeper-headless:2181
        - name: KAFKA_LOG_DIRS
          value: /opt/kafka/data/logs
        - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR
          value: "1"
        - name: KAFKA_JMX_PORT
          value: "5555"
      image: confluentinc/cp-kafka:5.3.1-1
      imagePullPolicy: IfNotPresent
      livenessProbe:
        exec:
          command:
            - sh
            - -ec
            - /usr/bin/jps | /bin/grep -q SupportedKafka

        failureThreshold: 3
        initialDelaySeconds: 30
        periodSeconds: 10
        successThreshold: 1
        timeoutSeconds: 5
      name: kafka-broker
      ports:
        - containerPort: 9092
          name: kafka
          protocol: TCP
      readinessProbe:
        failureThreshold: 3
        initialDelaySeconds: 30
        periodSeconds: 10
        successThreshold: 1
        tcpSocket:
          port: kafka
        timeoutSeconds: 5
      resources: {}
      terminationMessagePath: /dev/termination-log
      terminationMessagePolicy: File
      volumeMounts:
        - mountPath: /opt/kafka/data
          name: datadir
  dnsPolicy: ClusterFirst
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  terminationGracePeriodSeconds: 60

updateStrategy:
type: OnDelete
volumeClaimTemplates:
- metadata:
name: datadir
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: rook-ceph-block

Listing 5-9Kafka StatefulSet


应用 Kafka StatefulSet 配置:

$ kubectl apply -f 40-statefulset.yml


Kubernetes Pod 中断预算 <sup>11</sup> 限制在任何给定时间允许停机的 Pod 数量,节点故障或 Pod 错误等计划外停机除外。PodDisruptionBudget 配置对于更新表示高可用性集群(如 Kafka)的有状态集特别有用。通过适当的配置和资源,Kafka 集群可以保持完全运行,而一部分节点处于离线状态。

Note

由于本章中指定的开发环境的资源有限,下面定义的 Kafka 配置是稳定的,但不是高度可用的。 <sup>12</sup>

在清单 5-10 中名为`45-pdb.yml`的文件中为 Kafka 创建一个 PodDisruptionBudget 配置。

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: kafka
namespace: data
labels:
app: kafka
spec:
maxUnavailable: 1
selector:
matchLabels:
app: kafka

Listing 5-10Kafka Pod Disruption Budget


最后,应用 Kafka PodDisruptionBudget 配置:

$ kubectl apply -f 45-pdb.yml


新的开发环境现在运行一个双节点 Kafka 集群。下一节将为测试和调试设置 Pod。

### Kafka 客户端实用程序窗格

Kafka 可以用最少的管理开销在这个数据驱动平台的几乎每个组件之间代理数据。但是,Confluence 提供的 Kafka 容器包含几个有用的脚本(见表 5-1 )用于测试、备份、安全配置和一般管理功能。运行 Kafka 客户端实用程序 Pod 提供了对这个关键平台组件的命令行管理访问。

Kafka 测试客户机 Pod 运行与前面配置的操作集群相同的`confluentinc/cp-kafka:5.3.1-1`映像。然而,Pod 被配置为执行命令`tail -f /dev/null`而不是标准入口点,保持`tail`为活动进程并阻止 Pod 完成。

在清单 5-11 中的文件`99-pod-test-client.yml`中创建一个 Kafka 测试客户端配置。

apiVersion: v1
kind: Pod
metadata:
name: kafka-client-util
namespace: data
spec:
containers:
- command:
- sh
- -c
- exec tail -f /dev/null
image: confluentinc/cp-kafka:5.3.1-1
imagePullPolicy: IfNotPresent
name: kafka
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File

Listing 5-11Kafka test client Pod


应用 Kafka 测试客户端 Pod 配置:

$ kubectl apply -f 99-pod-client-util.yml


Kafka 测试客户端 Pod 中运行的容器映像`confluentinc/cp-kafka:4.1.2-2`附带了表 5-1 中列出的实用程序。运行表 5-1 中列出的任何命令,带有`--help`标志,以获得配置参数列表。Cloudera 在其网站上提供了详细的文档: *Kafka 使用命令行工具管理*。 <sup>13</sup>

表 5-1

Kafka 客户端实用程序脚本

<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup> 
| 

脚本

 | 

描述

 |
| --- | --- |
| Kafka-acls | 用户认证、授权和访问控制列表管理。 |
| Kafka-broker-API-版本 | 列出集群中所有节点的 API 版本。 |
| Kafka-config | 添加/删除主题、客户机、用户或代理的实体配置。 |
| **Kafka——控制台——消费者** | 用于创建、更改、列出和描述主题。 |
| **Kafka-控制台-制片人** | 从标准输出中读取数据,并将其写入 Kafka 主题。 |
| Kafka-消费群体 | 列出当前的消费群体。 |
| Kafka-消费者-抵销-检查 | 返回特定使用者组中每个使用者读取和写入的带有滞后时间的消息数。 |
| Kafka-消费者-性能-测试 | 对代理和主题运行消费者性能测试。 |
| Kafka-委托-代币 | 委托令牌是经纪人和客户之间共享的秘密。创建、描述、续订和终止令牌。 |
| Kafka-删除-记录 | 从具有给定偏移量的主题中删除记录。 |
| Kafka 日志-dirs | 输出一个 JSON 对象,包含每个代理的日志信息。 |
| Kafka-镜子制造者 | 复制 Kafka 集群。 <sup>14</sup> |
| Kafka-首选-复制品-选举 | 重新平衡主题。 <sup>十五</sup> |
| Kafka-生产者-性能-测试 | 对代理和主题运行生产者性能测试。 |
| Kafka-重新分配-分区 | 将 Kafka 主题分区领导者重新分配给不同的 Kafka 代理。 |
| Kafka-重播-日志-制作人 | 消费来自一个主题的消息,并在另一个主题中重放(产生)它们。 |
| Kafka-复制品-验证 | 验证一个或多个主题的数据复制是否正确。 |
| Kafka 式的 | 提供直接调用 Kafka 类的能力;主要由其他脚本使用。 |
| Kafka-服务器-开始 Kafka-服务器-停止 | 在管理汇合分配的情况下不使用。 |
| Kafka-流-应用-重置 | 重置 Kafka Streams <sup>16</sup> 应用的内部状态。 |
| **Kafka-主题** | 创建、列出、配置和删除主题。 |
| Kafka-可验证的-生产者 Kafka-可验证的-消费者 | 生成并使用一定数量的消息进行测试。 |

Testing Kafka

1.  在新的 Kafka 测试客户端 Pod 上参加一个 Bash <sup>17</sup> 会话。

    ```
    $ kubectl exec -it kafka-client-util bash -n data

    ```

2.  在新 Pod 的命令行中,创建主题`test`和**一个分区**和**一个副本**。

    ```
    # kafka-topics --zookeeper zookeeper-headless:2181 \
    --topic test --create --partitions 1 --replication-factor

    ```

3.  列出 Kafka 集群中的所有主题。新的集群应该只有测试主题以及一个或多个以两个下划线开头的内部主题。

    ```
    # kafka-topics --zookeeper zookeeper-headless:2181 --list

    ```

4.  听新的测试题目。Kafka-控制台-消费者向控制台打印消息。

    ```
    # kafka-console-consumer --bootstrap-server kafka:9092 \
    --topic test

    ```

5.  从一个单独的终端打开 Kafka test client Pod 上的一个附加 Bash 会话。

    ```
    $ kubectl exec -it kafka-client-util bash -n data

    ```

6.  使用 kafka-console-producer 实用程序发送测试消息。

    ```
    # kafka-console-producer --broker-list kafka:9092 \
    --topic test

    ```

7.  键入一条消息并返回。每行文本作为消息发送到主题,并显示在运行 Kafka 控制台消费者的终端中(步骤 4)。

开发环境现在运行一个双节点 Kafka 集群,已经过测试,可以接收和发送消息了。考虑用雅虎开发的 Kafka Manager、 <sup>18</sup> 添加一个管理 web 界面!。LinkedIn 开发了陋居 <sup>19</sup> 实用程序来监控消费者延迟时间,并通过 HTTP 端点提供统计数据。同样由 LinkedIn 开发的 Cruise-control、 <sup>20</sup> ,以及由 Pinterest 开发的 doctor Kafka<sup>21</sup>自动执行动态工作负载再平衡和自修复。Kafka 拥有一个庞大且不断发展的公用事业和第三方支持生态系统。 <sup>22</sup>

Kafka 被设计为在一个巨大的规模上工作,作为一个高度可用的、容错的、分布式的、通用的发布/订阅系统,这是将它保持在数据驱动架构的中心的一个很好的理由。Kafka 可以轻松处理来自几乎任何来源的海量数据和指标,尤其是物联网设备。然而,下一节将介绍另一个名为 Mosquitto 的发布/订阅应用,它实现了专为物联网设计的 MQTT 协议。

## 蚊子(MQTT)

MQTT(和 AMQP)等协议专注于轻量级客户端到消息代理通信,适用于广泛且不断增长的消费和工业物联网设备。相比之下,Apache Kafka 是一个更重的事件队列实现,能够处理和持久存储大量数据。虽然这些系统在概念上是相似的,但是在本书中向数据平台添加 MQTT 功能演示了各种协议以及在它们之间交换消息的能力。

想象一个工厂,机器状态通过内部 MQTT 代理进行控制和通信;远程数据平台中的 MQTT 代理充当客户端桥梁,将这些消息转发给 Kafka。机器学习模型对 Kafka 的最后一个小时的滚动数据进行预测分析,并做出调整特定机器状态的决定,并通过 MQTT 将该决定传达回来(见图 5-5 )。

![img/483120_1_En_5_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_5_Fig5_HTML.jpg)

图 5-5

Apache Kafka 和 MQTT 事件队列

Mosquitto <sup>24</sup> 是由 Eclipse Foundation<sup>25</sup>维护的开源 MQTT 代理,作为*iot.eclipse.org 项目*提供的众多组件之一。

创建目录`cluster-apk8s-dev3/003-data/050-mqtt`。在新的`050-mqtt`目录中,从清单 5-12 中创建一个名为`10-service.yml`的文件。

apiVersion: v1
kind: Service
metadata:
name: mqtt
namespace: data
labels:
app: mqtt
spec:
selector:
app: mqtt
ports:
- protocol: "TCP"
port: 1883
targetPort: 1883
type: ClusterIP

Listing 5-12Mosquitto MQTT Service


应用 MQTT 服务配置:

$ kubectl apply -f 10-service.yml


接下来,在清单 5-13 中名为`20-configmap.yml`的文件中为 Mosquitto 创建一个配置图。这里定义的小配置文件指示服务器以用户`mosquitto`的身份运行,并监听端口 1883。在这个例子中没有加密或认证,因为这个开发环境中的服务器没有直接向公众公开。有关配置选项的详细列表,请参见联机文档。 <sup>26</sup>

Caution

不要在没有启用身份验证和加密的情况下将 MQTT 代理暴露给公共互联网。所有客户端都应该被信任。强烈建议使用 VPN 或配置良好的防火墙来保护远程连接。

apiVersion: v1
kind: ConfigMap
metadata:
name: mqtt
namespace: data
labels:
app: mqtt
data:
mosquitto.conf: |-
user mosquitto
port 1883

Listing 5-13Mosquitto configuration ConfigMap


应用 Mosquitto 配置配置图:

$ kubectl apply -f 20-configmap.yml


接下来,在清单 5-14 中的一个名为`30-deployment.yml`的文件中为 Mosquitto 创建一个部署。这里定义的 Mosquitto 旨在作为单个实例运行。为生产而扩展 Mosquitto 通常涉及为特定客户或客户群提供代理。还有许多其他的开源和商业 MQTT 代理,包括 VerneMQ、 <sup>27</sup> 一个高度可用的分布式实现,以及流行的 RabbitMQ <sup>28</sup> 支持 AMQP、 <sup>29</sup> STOMP、 <sup>30</sup> 以及 MQTT,它们都是开源的,用 Erlang 编写。

apiVersion: apps/v1
kind: Deployment
metadata:
name: mqtt
namespace: data
labels:
app: mqtt
spec:
replicas: 1 # keep at 1 for
revisionHistoryLimit: 1
selector:
matchLabels:
app: mqtt
template:
metadata:
labels:
app: mqtt
spec:
volumes:
- name: mqtt-config-volume
configMap:
name: mqtt
containers:
- name: mqtt
image: eclipse-mosquitto:1.6.6
imagePullPolicy: IfNotPresent
volumeMounts:
- name: mqtt-config-volume
mountPath: /mosquitto/config
ports:
- name: mqtt
containerPort: 1883

Listing 5-14Mosquitto Deployment


应用 Mosquitto 部署:

$ kubectl apply -f 30-deployment.yml


一个 MQTT 代理安装并运行在 Kubernetes 开发集群中。诸如 MQTT.fx <sup>31</sup> 和 mqtt-spy <sup>32</sup> 之类的实用程序非常适合测试和调试 MQTT 代理。下面的练习使用新的 Mosquitto Pod 上的`mosquitto_sub`实用程序以及在本地工作站上运行的 MQTT.fx 来测试新的代理。

Testing Mosquitto

1.  在本地工作站上下载并安装 mqtt . FX 1 . 7 . 1 版。参观 [`https://mqttfx.jensd.de/index.php/download`](https://mqttfx.jensd.de/index.php/download) 。

2.  进入 Mosquitto MQTT 代理 Pod 上的 Bash 会话(使用`kubectl get pods -n data`找到它的名称)。

    ```
    $ kubectl exec -it mqtt-6899646f75-g65sf sh -n data

    ```

3.  从 Mosquitto MQTT 窗格中的命令行开始监听关于`dev/test`主题的消息。

    ```
    # mosquitto_sub -t dev/apk8s

    ```

4.  从本地工作站,使用 kubectl 将`mqtt` Kubernetes 服务移植到运行 MQTT.fx 的本地工作站。

    ```
    $ kubectl port-forward svc/mqtt 1883:1883 -n data

    ```

5.  Open MQTT.fx and select “local mosquitto” from the connect drop-down, and then click Connect as shown in Figure 5-6.

    ![img/483120_1_En_5_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_5_Fig6_HTML.jpg)

    图 5-6

    在本地工作站上运行的 MQTT.fx 应用

6.  提供相同的主题;`mosquitto_sub`是从步骤 3 开始监听,本例中为`dev/apk8s`。在大文本区提供消息,点击**发布**。

注简单的消息传递也可以通过`mosquitto_sub`旁边的`mosquitto_pub`实用程序来完成。

1.  观察从`mosquitto_sub`输出打印的信息。

## 摘要

该平台现在包括两个受欢迎的活动队列,Kafka 和 Mosquitto。Kafka 旨在作为平台的“中枢神经系统”,负责与其他平台组件之间的状态、指标和数据的实时通信。Mosquitto 为流行的物联网通信协议 MQTT 提供支持。清单 5-15 显示了本章中开发的配置清单的概述。

.
└── cluster-apk8s-dev3
├── 000-cluster
└── 003-data
├── 000-namespace
│ ├── 00-namespace.yml
│ └── 05-certs.yml
├── 010-zookeeper
│ ├── 10-service-headless.yml
│ ├── 10-service.yml
│ └── 40-statefulset.yml
├── 020-kafka
│ ├── 10-service-headless.yml
│ ├── 10-service.yml
│ ├── 40-statefulset.yml
│ ├── 45-pdb.yml
│ └── 99-pod-test-client.yml
└── 050-mqtt
├── 10-service.yml
├── 20-configmap.yml
└── 30-deployment.yml

Listing 5-15Data Pipeline Development Cluster configuration layout


接下来的章节将演示索引、分析、可视化、仓储和路由流经本章中配置的队列的数据的方法。

<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1

[`https://mqtt.org`](https://mqtt.org)

  2

[`https://appcodelabs.com/introduction-to-iot-build-an-mqtt-server-using-raspberry-pi`](https://appcodelabs.com/introduction-to-iot-build-an-mqtt-server-using-raspberry-pi)

  3

[`https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)

  4

[`https://httpd.apache.org/docs/2.4/programs/htpasswd.html`](https://httpd.apache.org/docs/2.4/programs/htpasswd.html)

  5

[`https://zookeeper.apache.org`](https://zookeeper.apache.org)

  6

[`https://hadoop.apache.org/`](https://hadoop.apache.org/)

  7

[`https://hbase.apache.org`](https://hbase.apache.org)

  8

[`https://kubernetes.io/docs/concepts/services-networking/service/#headless-services`](https://kubernetes.io/docs/concepts/services-networking/service/%2523headless-services)

  9

[`https://kafka.apache.org`](https://kafka.apache.org)

  10

[`https://www.confluent.io`](https://www.confluent.io)

  11

[`https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#how-disruption-budgets-work`](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/%2523how-disruption-budgets-work)

  12

[`www.loudera.com/documentation/kafka/latest/topics/kafka_ha.html`](http://www.loudera.com/documentation/kafka/latest/topics/kafka_ha.html)

  13

[`www.cloudera.com/documentation/enterprise/latest/topics/kafka_admin_cli.html`](http://www.cloudera.com/documentation/enterprise/latest/topics/kafka_admin_cli.html)

  14

[`https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27846330`](https://cwiki.apache.org/confluence/pages/viewpage.action%253FpageId%253D27846330)

  15

[`https://blog.imaginea.com/how-to-rebalance-topics-in-kafka-cluster/`](https://blog.imaginea.com/how-to-rebalance-topics-in-kafka-cluster/)

  16

[`https://kafka.apache.org/documentation/streams/`](https://kafka.apache.org/documentation/streams/)

  17

[`www.gnu.org/software/bash/`](http://www.gnu.org/software/bash/)

  18

[`https://github.com/yahoo/kafka-manager`](https://github.com/yahoo/kafka-manager)

  19

[`https://github.com/linkedin/Burrow`](https://github.com/linkedin/Burrow)

  20

[`https://github.com/linkedin/cruise-control`](https://github.com/linkedin/cruise-control)

  21

[`https://github.com/pinterest/doctorkafka`](https://github.com/pinterest/doctorkafka)

  22

[`https://cwiki.apache.org/confluence/display/KAFKA/Ecosystem`](https://cwiki.apache.org/confluence/display/KAFKA/Ecosystem)

  23

[`https://engineering.linkedin.com/kafka/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines`](https://engineering.linkedin.com/kafka/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines)

  24

[`https://mosquitto.org/`](https://mosquitto.org/)

  25

[`https://eclipse.org/`](https://eclipse.org/)

  26

[`https://mosquitto.org/man/mosquitto-conf-5.html`](https://mosquitto.org/man/mosquitto-conf-5.html)

  27

[`https://vernemq.com`](https://vernemq.com)

  28

[`www.rabbitmq.com`](http://www.rabbitmq.com)

  29

[`www.amqp.org`](http://www.amqp.org)

  30

[`https://stomp.github.io`](https://stomp.github.io)

  31

[`https://mqttfx.jensd.de`](https://mqttfx.jensd.de)

  32

[`http://kamilfb.github.io/mqtt-spy/`](http://kamilfb.github.io/mqtt-spy/)

 </aside>

# 五、索引和分析

Kubernetes 的现代分布式数据平台远远超出了数据的收集、存储和传输。搜索、索引、分析和数据科学应用是以数据为中心的平台的基本要素。本章重点关注网络规模的 <sup>1</sup> 技术,这是一个在大数据主流关注之外成熟的生态系统。 <sup>2</sup> 在大数据假设有限数量的同时请求处理近乎无限的数据湖 <sup>3</sup> 的情况下,网络规模假设最终无限的同时数据需求。网络规模的分析和大数据正变得越来越紧密,并在组合能力方面快速发展。本章涵盖了为 web 级架构构建的一般数据索引、指标、分析和数据科学。下一章将介绍 Kubernetes 如何通过支持现代数据湖和数据仓库的发展来融入大数据环境。Kubernetes 构建健壮的分布式集群的能力带来了在这两个不同问题领域(web 级的大数据)的数据和控制面板之间架起桥梁的机会。

本章重点介绍了一些应用,这些应用涵盖了通信、索引和数据科学要求的一个通用而有效的范围。专注于特定问题领域的应用可能会受益于更高级别的技术;然而,一般搜索、分析和数据科学技术通常是开发集中数据驱动解决方案的多层方法的基础。

## 搜索和分析

数据以结构化、半结构化和非结构化的形式存在。非结构化数据包括各种数据类型,如映像、视频、音频、pdf 和原始文本。下一章将介绍如何开发数据湖来存储非结构化数据。然而,本章关注的是由 Elasticsearch 支持的半结构化、基于文档的数据,elastic search 是一种基于文档的数据索引,能够存储、检索和分析数十亿条记录。

## 数据科学环境

在“数据实验室”一节中,本章将数据收集、排队索引和分析操作结合到一个数据科学环境中。JupyterHub 被配置为提供 JupyterLab 实例(在第四章中介绍),以通过利用对 Kafka、Elasticsearch、Mosquitto 等的集群内访问来增强数据科学活动。

## 发展环境

前几章利用了来自低成本主机提供商 Vultr 和 Scaleway 的通用计算资源。本章通过选择 Hetzner 的折扣计算产品延续了这一趋势。Hetzner 是构建低成本开发和实验性 Kubernetes 集群的另一个绝佳选择。

本章中的 Kubernetes 集群利用了 Hetzner 的一个 CX21 (2 个 vCPU/8G RAM/40G SSD)和四个 CX41 (4 个 vCPU/16G RAM/160G SSD)实例。在写这篇文章的时候,Hetzner 的定价是每天不到 4 美元。

Note

Hetzner 上的 Ubuntu server 实例不包含某些包或内核模块`rbd`(Ceph 要求)所需的内核头。在每台 Hetzner 服务器上使用以下命令安装内核头文件并加载清单 6-1 中所示的`rbd`。

$ # install kernel headers
$ apt install -y linux-headers-$(uname -r)

$ # load the Ceph rbd kernel module
$ modprobe rbd

Listing 6-1Installing kernel headers and the rdb kernel module


本章中的自定义 Kubernetes 集群标记为`dev4`,按照第三章中的安装说明以及第五章中的清单进行设置。如果遵循上一章的内容,将清单从`cluster-apk8s-dev3`复制并应用到`cluster-apk8s-dev4`,如清单 6-2 所示。

.
└── cluster-apk8s-dev3
└── cluster-apk8s-dev4
├── 000-cluster
│ ├── 00-ingress-nginx
│ ├── 10-cert-manager
│ ├── 20-rook-ceph
│ └── 30-monitoring
└── 003-data
├── 000-namespace
├── 010-zookeeper
├── 020-kafka
└── 050-mqtt

Listing 6-2Development environment prerequisites


### TLS 证书

本章使用子域`kib`表示 Kibana,`auth`表示 Keycloak,`lab`表示 JupyterHub/JupyterLab。通过生成证书,确保 TLS Kubernetes Secret `data-production-tls`可供 ingress 使用。清单 6-3 列出了定义文件`cluster-apk8s-dev4/003-data/000-namespace/05-certs.yml`的示例证书管理器配置。

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: data-cert
namespace: data
spec:
secretName: data-production-tls
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
commonName: data.dev4.apk8s.dev
dnsNames:
- data.dev4.apk8s.dev
- auth.data.dev4.apk8s.dev
- lab.data.dev4.apk8s.dev
- kib.data.dev4.apk8s.dev

Listing 6-3cluster-apk8s-dev4 TLS Certificate


### 基本认证

创建一个包含基本身份验证凭证的 Kubernetes 秘密。稍后,Ingress Nginx 被配置为使用这个秘密来保护对 Kibana 的访问。

首先,使用`htpasswd` <sup>4</sup> 实用程序在名为`auth`的文件中创建一个用户名/密码组合。

$ cd cluster-apk8s-dev4/003-data/000-namespace
$ htpasswd -c auth sysop


使用 kubectl 命令`create secret`从按照 Ingress 的预期构建的`auth`文件中创建一个名为`sysop-basic-auth`的秘密。

$ kubectl create secret generic sysop-basic-auth
--from-file auth -n data


## 麋鹿

麋鹿栈 <sup>5</sup> 由 Elasticsearch、Logstash 和 Kibana 组成。ELK 是一个流行的应用套件,用于索引、搜索、路由、转换和可视化数据。Elasticsearch B.V .维护这个开源栈,并提供托管服务以及许多其他流行的开源和商业软件。

Note

Elasticsearch b . v .*Elastic License*对于包含 Elastic search 的平台即服务(PaaS)产品来说可能过于严格。亚马逊在 Apache 2.0 软件许可下(尽管有些争议 <sup>7</sup> )分叉了该项目并创建了用于弹性搜索 <sup>6</sup> 的*开放发行版。尽职调查是选择适合特定用例的发行版的一个要求。*

### 弹性搜索

Elasticsearch 是一个基于 Apache Lucene <sup>8</sup> 的数据索引器和分析引擎,它是一个专门为横向可伸缩性和高可用性而设计的分布式系统。Elasticsearch 接受任何形式的基于 JSON 的数据结构,这使得它非常适合与现代 web APIs 进行互操作。Elasticsearch 可以自动检测许多数据类型,但也可以提供自定义模板来断言需要转换的不明确字段的数据类型,例如表示为字符串或日期格式的数字。Elasticsearch 具有广泛的数据聚合和统计分析功能,可以将数据与其索引一起存储。尽管旨在数据索引和聚合,Elasticsearch 是一个有能力的 NoSQL 数据库。

本章设置了一个单节点 Elasticsearch 实例。Elasticsearch 的生产部署由多个节点组成,每个节点都致力于一个特定的任务:数据节点存储、索引和查询数据;主节点更新集群状态;客户端节点采用负载均衡器的形式,执行索引和搜索。

Note

Elasticsearch 的官方维护者 Elasticsearch B.V .开发了一个解决方案,他们称之为“Kubernetes 上的弹性云, <sup>9</sup> ”,实现了安装和管理 Elasticsearch 集群的 Kubernetes 操作模式。考虑将此解决方案用于生产实施。

创建目录`cluster-apk8s-dev4/003-data/030-elasticsearch`。在新的`030-elasticsearch`目录中,从清单 6-4 中创建一个名为`10-service.yml`的文件。

apiVersion: v1
kind: Service
metadata:
namespace: data
name: elasticsearch
spec:
type: ClusterIP
selector:
app: elasticsearch
ports:
- name: http-es
port: 9200
targetPort: http-es
protocol: TCP

Listing 6-4Elasticsearch Service


应用弹性搜索服务配置:

$ kubectl apply -f 10-service.yml


接下来,在清单 6-5 中名为`40-statefulset.yml`的文件中为 Elasticsearch 创建一个 StatefulSet 配置。

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: data
labels:
app: elasticsearch
spec:
serviceName: elasticsearch
replicas: 1 # single-node cluster
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
initContainers:
- name: init-sysctl
image: busybox:1.27.2
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
- name: init-chown
image: busybox:1.27.2
command: ["/bin/sh"]
args: ["-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
securityContext:
privileged: true
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
imagePullPolicy: IfNotPresent
env:
- name: "discovery.type"
value: "single-node"
- name: "cluster.name"
value: "apk8s"
- name: "transport.host"
value: "127.0.0.1"
- name: "ES_JAVA_OPTS"
value: "-Xms512m -Xmx512m"
- name: "http.host"
value: "0.0.0.0"
- name: "http.port"
value: "9200"
- name: "http.cors.allow-origin"
value: "http://localhost:1358"
- name: "http.cors.enabled"
value: "true"
- name: "http.cors.allow-headers"
value: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization"
- name: "http.cors.allow-credentials"
value: "true"

      ports:
        - containerPort: 9200
          name: http-es
        - containerPort: 9300
          name: tcp-es
      volumeMounts:
        - name: es-data
          mountPath: /usr/share/elasticsearch/data

volumeClaimTemplates:
- metadata:
name: es-data
spec:
storageClassName: rook-ceph-block
accessModes: [ ReadWriteOnce ]
resources:
requests:
storage: 50Gi

Listing 6-5Elasticsearch StatefulSet


应用弹性搜索状态集配置:

$ kubectl apply -f 40-statefulset.yml


使用`kubectl`将 Elasticsearch 服务移植到本地工作站。

$ kubectl port-forward elasticsearch-0 9200:9200 -n data


使用`curl`检查新的单节点 Elasticsearch 集群的健康状况。成功的安装会返回一个 JSON 对象,其状态键报告消息`green`。

$ curl http://localhost:9200/_cluster/health


Elasticsearch 被设计成分片 <sup>10</sup> 并跨大型节点集群复制数据。单节点开发集群只支持每个索引一个分片,并且不能复制数据,因为没有其他节点可用。使用`curl`,`POST`一个(JSON)模板到这个单节点集群,通知 Elasticsearch 用一个 shard 和零个副本配置任何新的索引。

$ cat <<EOF | curl -X POST
-H "Content-Type: application/json"
-d @- http://localhost:9200/_template/all
{
"index_patterns": "*",
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}
EOF


### logstash(日志记录)

Logstash 是弹性生态系统中的中心枢纽。“Logstash 是一个开源的服务器端数据处理流水线,它可以同时从多个来源获取数据,对其进行转换,然后将其发送到您最喜欢的‘stash’。” <sup>11</sup> 本书使用 Logstash 将数据注入到 Elasticsearch 中。将大量数据高速注入到弹性搜索中具有挑战性;然而,Logstash 缓冲数据并管理由索引过程引起的背压。Logstash 有一大套有用的输入插件,包括 Apache Kafka,本章稍后将利用这些插件将 Kafka 主题中的记录(事件/消息)索引到 Elasticsearch 中。

要运行本章中的示例,请确保 Apache Kafka 正在 Kubernetes 集群中运行。如有必要,查看第章至第章的 Kafka 安装说明。

创建目录`cluster-apk8s-dev4/003-data/032-logstash`。在新的`032-logstash`目录中,从清单 6-6 中创建一个名为`10-service.yml`的文件。

kind: Service
apiVersion: v1
metadata:
name: logstash
namespace: data
spec:
selector:
app: logstash
ports:

  • protocol: TCP
    port: 5044
    type: ClusterIP

Listing 6-6Logstash Service


应用日志存储服务配置:

$ kubectl apply -f 10-service.yml


接下来,在清单 6-7 中的一个名为`30-configmap-config.yml`的文件中创建一个包含 Logstash 配置设置的 ConfigMap。要在资源受限的环境中限制内存使用(如本章中定义的开发集群),请将 Java JVM -Xms512m 和-Xmx523m 设置配置为相对较小的值。

apiVersion: v1
kind: ConfigMap
metadata:
name: logstash-config
namespace: data
data:
logstash.yml: |
http.host: "0.0.0.0"
xpack.monitoring.enabled: false

pipelines.yml: |
- pipeline.id: main
path.config: "/usr/share/logstash/pipeline"
log4j2.properties: |
status = error
name = LogstashPropertiesConfig

appender.console.type = Console
appender.console.name = plain_console
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %m%n

appender.json_console.type = Console
appender.json_console.name = json_console
appender.json_console.layout.type = JSONLayout
appender.json_console.layout.compact = true
appender.json_console.layout.eventEol = true

rootLogger.level = ${sys:ls.log.level}
rootLogger.appenderRef.console.ref = ${sys:ls.log.format}_console

jvm.options: |
## JVM configuration

-Xms512m
-Xmx523m

-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-Djava.awt.headless=true
-Dfile.encoding=UTF-8
-Djruby.compile.invokedynamic=true
-Djruby.jit.threshold=0
-XX:+HeapDumpOnOutOfMemoryError
-Djava.security.egd=file:/dev/urandom

Listing 6-7Logstash configuration

ConfigMap


应用日志存储配置配置图:

$ kubectl apply -f 30-configmap-config.yml


Logstash 分三个阶段处理事件:输入、过滤和输出。清单 6-8 中的配置演示了使用 Kafka 输入插件来消费来自主题消息和指标的数据。输出配置检查 Kafka 主题是否存在,如果找到,则将数据从主题路由到以当前日期为前缀的相应索引中。

![img/483120_1_En_6_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig1_HTML.jpg)

图 6-1

Kafka 到 Elasticsearch Logstash 流水线配置

在清单 6-8 中的一个名为`30-configmap-pipeline.yml`的文件中创建一个包含 Logstash 流水线设置的 ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
name: logstash-pipeline
namespace: data
data:
logstash.conf: |
input {
kafka {
bootstrap_servers => "kafka-headless:9092"
topics => [ "messages", "metrics"]
auto_offset_reset => "latest"
auto_commit_interval_ms => "500"
enable_auto_commit => true
codec => json
decorate_events => true
}
}

output {
  if [@metadata][kafka][topic] {
    elasticsearch {
        hosts => [ "elasticsearch:9200" ]
        index => "apk8s-%{[@metadata][kafka][topic]}-%{+YYYY.MM.dd}"
    }
  }
}

Listing 6-8Logstash pipeline configuration ConfigMap


应用 Logstash 流水线配置配置图:

$ kubectl apply -f 30-configmap-pipeline.yml


最后,在清单 6-9 中的一个名为`40-deployment.yml`的文件中创建一个 Logstash 部署。

apiVersion: apps/v1
kind: Deployment
metadata:
name: logstash
namespace: data
labels:
app: logstash
spec:
replicas: 1
selector:
matchLabels:
app: logstash
template:
metadata:
labels:
app: logstash
spec:
containers:
- name: logstash
image: docker.elastic.co/logstash/logstash:7.1.1
ports:
- containerPort: 5044
env:
- name: ES_VERSION
value: 7.1.1
volumeMounts:
- name: config-volume
mountPath: /usr/share/logstash/config
- name: logstash-pipeline-volume
mountPath: /usr/share/logstash/pipeline
volumes:

  - name: config-volume
    configMap:
      name: logstash-config
  - name: logstash-pipeline-volume
    configMap:
      name: logstash-pipeline

Listing 6-9Logstash Deployment


应用 Logstash 部署:

$ kubectl apply -f 40-deployment.yml


集群现在运行单个 Logstash 实例,可以通过按需增加副本来轻松扩展。从单个节点开始对早期调试很有用。

如前所述,Logstash 流水线接受来自 Kafka 主题消息和指标的 JSON 数据输入。输出配置指示 Logstash 根据主题名称和日期填充 Elasticsearch 索引。

Note

参见第五章,了解设置 Apache Kafka 和本章示例所需的`kafka-test-client` Pod 的说明。

通过向由`kafka-test-client`提供的`kafka-console-producer`脚本回显一个简单的 JSON 消息来测试新的 Logstash 流水线。

$ kubectl -n data exec -it kafka-test-client --
bash -c "echo '{"usr": 1, "msg": "Hello ES" }' |
kafka-console-producer --broker-list kafka:9092
--topic messages"


使用`kubectl`将 Elasticsearch 服务移植到本地工作站。

$ kubectl port-forward elasticsearch-0 9200:9200 -n data


接下来,确保 Logstash 将 Kafka 主题消息中的数据事件正确地路由到正确的索引中。使用`curl`获取索引模式`apk8s-messages-*`的所有记录。以下命令从以`apk8s-messages-`开头的索引中返回所有记录:

$ curl http://localhost:9200/apk8s-messages-*/_search
Example response:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "apk8s-messages-2020.03.02",
"_type": "_doc",
"_id": "IDn7mXABZUrIUU7qdxAr",
"_score": 1.0,
"_source": {
"@version": "1",
"usr": 1,
"@timestamp": "2020-03-02T06:42:38.545Z",
"msg": "Hello ES"
}
}
]
}
}


在本章的前面,模板`all`被定义为匹配所有索引并设置默认分片和复制。可以添加额外的模板来定义特定索引或索引集上的字段的数据类型。然而,当没有模板匹配字段或索引时,Elasticsearch 会尽最大努力猜测数据类型。使用以下 curl 命令查看为新索引生成的默认映射:

$ curl http://localhost:9200/apk8s-messages-*/_mapping


对于在该集群中配置的 Elasticsearch 7.1.1,字段`user`接收数字赋值 *long* ,字段`msg`索引为*文本*。

Logstash 缓冲和管理来自弹性搜索索引操作的反压力的能力,在以非常高的速度消耗和处理大量数据的平台中可以发挥重要作用。即使在本章定义的小型集群中,Logstash 也可能将 <sup>12</sup> 扩展到十几个或更多的节点,每个节点都将数据消耗和缓冲到这个单一的 Elasticsearch 节点中。

### 巴拉人

Kibana 是 ELK 栈的前端组件,与 Elasticsearch 无缝集成,是调试、开发和可视化 Elasticsearch 数据的优秀工具。然而,Kibana 也仅限于与 Elasticsearch 合作。现代分析、可视化和仪表板通常需要收集、处理和可视化来自各种提供商的数据。利用 Kibana 作为 Elasticsearch 的内部开发和调试工具,同时使用更通用的解决方案进行跨平台的可视化和分析,这种情况并不少见。

创建目录`cluster-apk8s-dev4/003-data/034-kibana`。在新的`034-kibana`目录中,从清单 6-10 中创建一个名为`10-service.yml`的文件。

apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: data
labels:
app: kibana
spec:
selector:
app: kibana
ports:
- protocol: "TCP"
port: 80
targetPort: 5601
type: ClusterIP

Listing 6-10Kibana Service


应用 Kibana 服务:

$ kubectl apply -f 10-service.yml


接下来,在清单 6-11 中的一个名为`20-configmap.yml`的文件中创建一个包含 Kibana 配置设置的 ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
name: kibana
namespace: data
labels:
app: kibana
data:
kibana.yml: |-
server.name: kib.data.dev4.apk8s.dev
server.host: "0"
elasticsearch.hosts: http://elasticsearch:9200

Listing 6-11Kibana configuration ConfigMap


应用基巴纳配置图:

$ kubectl apply -f 20-configmap.yml


接下来,在清单 6-12 中的一个名为`30-deployment.yml`的文件中创建一个 Kibana 部署。

apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana
namespace: data
labels:
app: kibana
spec:
replicas: 1
revisionHistoryLimit: 1
selector:
matchLabels:
app: kibana
template:
metadata:
labels:
app: kibana
spec:
volumes:
- name: kibana-config-volume
configMap:
name: kibana
containers:
- name: kibana
image: docker.elastic.co/kibana/kibana-oss:7.1.1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: kibana-config-volume
mountPath: /usr/share/kibana/config
env:
- name: CLUSTER_NAME
value: apk8s
ports:
- name: http
containerPort: 5601

Listing 6-12Kibana Deployment


应用 Kibana 部署:

$ kubectl apply -f 30-deployment.yml


接下来,在清单 6-13 中的一个名为`50-ingress.yml`的文件中为 Kibana 创建一个入口。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: kibana
namespace: data
labels:
app: kibana
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: sysop-basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
spec:
rules:
- host: kib.data.dev4.apk8s.dev
http:
paths:
- backend:
serviceName: kibana
servicePort: 5601
path: /
tls:
- hosts:
- kib.data.dev4.apk8s.dev
secretName: data-production-tls

Listing 6-13Kibana Ingress


应用基巴纳入口:

$ kubectl apply -f 50-ingress.yml


下面的 ingress 在 [`https://kib.data.dev4.apk8s.dev`](https://kib.data.dev4.apk8s.dev) 将 Kibana 公开,并使用基本 Auth 进行安全保护。秘密`sysop-basic-auth`包含基本认证的用户名和密码。

如果继续,新的 Elasticsearch 实例只包含一条记录。本章的其余部分将演示如何配置 JupyterLab 环境,该环境能够与本章中配置的 Elasticsearch 和前面配置的 Kafka 进行数据通信。

## 数据实验室

本章和上一章介绍了数据驱动平台的基本功能,包括 Kafka 的事件流、Mosquitto 的物联网消息代理、数据路由以及通过 Elasticsearch 在 Logstash 和持久索引数据中的转换。本章的剩余部分将创建一个用户提供的数据实验室,直接连接到集群中的这些系统,如图 6-2 所示。

JupyterLab 在第四章中首次介绍,它带来了一套健壮且可扩展的数据科学功能以及一个命令行终端。在集群中运行 JupyterLab 为传统的数据科学、分析和实验创建了一个非常高效的环境,并通过与 Kubernetes API 更紧密的交互提供了开发和运营的机会。

以下部分演示了允许 JupyterLab 访问 Kubernetes 资源的 Kubernetes 名称空间、示例 RBAC 和 ServiceAccount 权限的设置。JupyterHub <sup>13</sup> 被配置为提供 JupyterLab 环境,针对 Keycloak 进行身份验证。

![img/483120_1_En_6_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig2_HTML.jpg)

图 6-2

名称空间中的 JupyterLab

### 凯克洛克

Keycloak <sup>14</sup> 是由红帽赞助的免费开源身份和访问管理应用。Keycloak 提供了创建和管理用户帐户的能力,或者连接到现有的 LDAP 或 Active Directory。第三方应用可以通过 OpenID Connect、OAuth 2.0 和 SAML 2.0 对用户进行身份验证。

Keycloak 为身份管理和第三方认证提供了一个交钥匙解决方案,非常适合本书中描述的数据平台的要求。以下部分实现了一个单节点 Keycloak 实例,本章稍后将使用该实例在为用户提供 JupyterLab 实例之前对用户进行身份验证。

创建目录`cluster-apk8s-dev4/003-data/005-keycloak`。在新的`005-keycloak`目录中,从清单 6-14 中创建一个名为`10-service.yml`的文件。

apiVersion: v1
kind: Service
metadata:
name: web-keycloak
namespace: data
spec:
selector:
app: web-keycloak
ports:
- name: http-web
protocol: "TCP"
port: 8080
targetPort: http-web
type: ClusterIP

Listing 6-14Keycloak Web Service


应用 Keycloak Web 服务:

$ kubectl apply -f 10-service.yml


接下来,在清单 6-15 中的一个名为`15-secret.yml`的文件中创建一个包含 Keycloak 管理员凭证的秘密。

apiVersion: v1
kind: Secret
metadata:
name: keycloak
namespace: data
labels:
app: keycloak
type: Opaque
stringData:
keycloak_user: "sysop"
keycloak_password: "verystrongpassword"
keystore_password: "anotherverystrongpassword"

Listing 6-15Keycloak administrator and keystore credentials


应用密钥锁密码:

$ kubectl apply -f 10-service.yml


接下来,在清单 6-16 中的一个名为`30-deployment.yml`的文件中创建一个 Keycloak 部署。

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web-keycloak
namespace: data
spec:
serviceName: web-keycloak
replicas: 1
revisionHistoryLimit: 1
selector:
matchLabels:
app: web-keycloak
template:
metadata:
labels:
app: web-keycloak
spec:
initContainers:
- name: keycloak-init-chown
image: alpine:3.10.1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: keycloak-db
mountPath: /data
command: ["chown"]
args: ["-R","1000:1000","/data"]
containers:
- name: web-keycloak
image: jboss/keycloak:6.0.1
imagePullPolicy: IfNotPresent
env:

      - name: PROXY_ADDRESS_FORWARDING
        value: "true"
      - name: KEYCLOAK_HOSTNAME
        value: "auth.data.dev4.apk8s.dev"
      - name: KEYCLOAK_USER
        valueFrom:
          secretKeyRef:
            name: keycloak
            key: keycloak_user
      - name: KEYCLOAK_PASSWORD
        valueFrom:
          secretKeyRef:
            name: keycloak
            key: keycloak_password
      - name: KEYSTORE_PASSWORD
        valueFrom:
          secretKeyRef:
            name: keycloak
            key: keystore_password
    ports:
      - name: http-web
        containerPort: 8080
    volumeMounts:
      - name: keycloak-db
        mountPath: /opt/jboss/keycloak/standalone/data

volumeClaimTemplates:
- metadata:
name: keycloak-db
spec:
storageClassName: rook-ceph-block
accessModes: [ ReadWriteOnce ]
resources:
requests:
storage: 5Gi

Listing 6-16Keycloak deployment


应用键盘锁部署:

$ kubectl apply -f 30-deployment.yml


最后,在清单 6-17 中的一个名为`50-ingress.yml`的文件中为 Keycloak 创建一个入口。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: web-auth
namespace: data
labels:
app: web-auth
spec:
rules:

  • host: auth.data.dev4.apk8s.dev
    http:
    paths:
    • backend:
      serviceName: web-keycloak
      servicePort: 8080
      path: /
      tls:
  • hosts:
    • auth.data.dev4.apk8s.dev
      secretName: data-production-tls

Listing 6-17Keycloak Ingress


应用钥匙锁入口:

$ kubectl apply -f 50-ingress.yml


#### 领域、客户机和用户

Keycloak 通过领域的配置为多个租户提供身份管理和身份验证。 <sup>15</sup> JupyterHub 在本章稍后配置为使用属于领域`datalab`的 Oauth2、 <sup>16</sup> 对用户进行身份验证。与一个领域相关联的 Keycloak 客户端向诸如 JupyterHub 之类的应用授予访问权限,以便对用户进行身份验证。本节将设置一个领域、客户机和用户,以便在本章后面提供 JupyterLab 服务器。

使用网络浏览器,按照上一节中的设置访问新入口 [`https://auth.data.dev4.apk8s.dev/auth/`](https://auth.data.dev4.apk8s.dev/auth/) 。使用清单 6-15 中定义的`sysop`凭证登录到 Keycloak。登录后,master 是用户界面左上角显示的默认领域,如图 6-3 所示。通过单击领域标题右侧的下拉菜单打开“添加领域”菜单,并创建新的领域 datalab。

![img/483120_1_En_6_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig3_HTML.jpg)

图 6-3

添加领域

接下来,在新的 Datalab 领域的左侧导航中导航到客户端。点击“创建”并填写“添加客户端”表单,添加一个名为 datalab 的新客户端,如图 6-4 所示。

![img/483120_1_En_6_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig4_HTML.jpg)

图 6-4

添加客户端

添加新的 datalab 客户端后,点击 Credentials 选项卡,检索生成的秘密,如图 6-5 所示。后来,JupyterHub 被配置为使用客户端 ID datalab 和生成的机密来获得根据 Keycloak datalab 领域对用户进行身份验证的权限。

![img/483120_1_En_6_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig5_HTML.jpg)

图 6-5

客户端凭据

通过在上将授权启用切换到*来配置新的 datalab 客户端(在设置选项卡下)。提供有效的重定向 URIs,在本例中, [`https://lab.data.dev4.apk8s.dev/hub/oauth_callback`](https://lab.data.dev4.apk8s.dev/hub/oauth_callback) 稍后在" JupyterHub "一节中定义。复习图 6-6 。*

![img/483120_1_En_6_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig6_HTML.jpg)

图 6-6

客户端配置

最后,通过在左侧菜单的 Manage 部分下选择 users,在 datalab 领域中创建一个或多个用户。添加用户后,在“凭据”选项卡下分配密码。使用强密码;分配给该领域的任何用户以后都可以访问 JupyterLab 环境,有权在集群中读写数据和执行代码。

### 命名空间

大多数 Kubernetes 对象都与一个名称空间相关联,包括 pod、部署、状态集、作业、入口、服务等等,换句话说:“一个*名称空间*为多个*种类的*资源定义了一个逻辑命名的组。” <sup>17</sup> 名称空间不仅有助于组织已配置的对象,它们还为安全性、资源分配和资源约束提供了选项。

Tip

使用 Kubernetes resource quota<sup>18</sup>对象对给定名称空间的资源进行细粒度限制,包括允许的 pod 和 PersistentVolumeClaims 的总数、CPU、内存和存储类限制。

本节设置名称空间`data-lab`以及 JupyterLab 和 JupyterHub 使用的 ServiceAccount 和 RBAC 权限。

创建目录`cluster-apk8s-dev4/005-data-lab/000-namespace`来包含名称空间范围的配置清单。接下来,在清单 6-18 中名为`00-namespace.yml`的文件中创建一个 Kubernetes 名称空间。

apiVersion: v1
kind: Namespace
metadata:
name: data-lab

Listing 6-18data-lab Namespace


应用命名空间配置:

$ kubectl apply -f 00-namespace.yml


分配给该集群中的 pod 的默认服务帐户无权访问 Kubernetes API。下面创建一个分配给由 JupyterHub 提供的 JupyterLab Pods 的服务帐户。

从清单 6-19 中创建文件`05-serviceaccount.yml`。

apiVersion: v1
kind: ServiceAccount
metadata:
name: data-lab
namespace: data-lab

Listing 6-19data-lab ServiceAccount


应用 ServiceAccount 配置:

$ kubectl apply -f 05-serviceaccount.yml


接下来,为新的`datalab` ServiceAccount 创建一个角色,稍后分配给 JupyterLab,并为 hub ServiceAccount 创建一个角色,稍后由 JupyterHub 使用。从清单 6-20 中创建文件`07-role.yml`。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: data-lab
namespace: data-lab
rules:

  • apiGroups: [""]
    resources: ["pods","events","services"]
    verbs: ["get","watch","list","endpoints","events"]

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: hub
namespace: data-lab
rules:

  • apiGroups: [""]
    resources: ["pods", "persistentvolumeclaims"]
    verbs: ["get","watch","list","create","delete"]
  • apiGroups: [""]
    resources: ["events"]
    verbs: ["get", "watch", "list"]

Listing 6-20data-lab and hub Roles

for the data-lab Namespace


Note

Kubernetes 将“(`apiGroups: [""]`)解释为核心 API 组。<sup>19</sup><sup><sup>20</sup></sup>

应用角色配置:

$ kubectl apply -f 07-role.yml


最后,创建 RoleBinding 对象,将新角色与其对应的 ServiceAccounts 关联起来。从清单 6-21 中创建文件`08-rolebinding.yml`。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: data-lab
namespace: data-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: data-lab
subjects:

  • kind: ServiceAccount
    name: data-lab
    namespace: data-lab

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: hub
namespace: data-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: hub
subjects:

  • kind: ServiceAccount
    name: hub
    namespace: data

Listing 6-21data-lab and hub RoleBindings


应用角色绑定配置:

$ kubectl apply -f 08-rolebinding.yml


### JupyterHub

JupyterHub“产生、管理和代理单用户 Jupyter 笔记本服务器的多个实例。” <sup>21</sup> 本节将 JupyterHub 安装到开发集群中,并将其配置为使用 Keycloak 对用户进行身份验证,并将 JupyterLab(笔记本)服务器生成到`data-lab`名称空间中。此外,上一节中定义的`data-lab`角色授予 JupyterHub 对 Kubernetes API 的有限访问权。

*零到 JupyterHub*<sup>22</sup>带 Kubernetes 是一个稳定的 JupyterHub 掌 Helm 图,文档全面详细。尽管为了学习和清晰起见,本书的大部分内容选择使用普通的 YAML 文件,但是 JupyterHub 是一个复杂的系统,这个图表很好地抽象了它,同时提供了任何必要的配置覆盖。

Note

Helm 是 Kubernetes 的一个成功且维护良好的软件包管理器,但也在快速发展中;因此,请参考官方文档 <sup>23</sup> 以获得简单的安装说明以及*附加注释* <sup>24</sup> *从零到 JupyterHub* 。

创建目录`cluster-apk8s-dev4/003-data/100-jupterhub`来包含 Helm 使用的`values.yml`清单。用清单 6-22 的内容填充。

注意`values.yml`Helm 配置中的下列粗体元素:

1.  在`proxy`部分,将`secretToken`设置为一个 32 个字符的随机十六进制值字符串。官方文档建议使用以下命令:

    ```
    $ openssl rand -hex 32.25

    ```

2.  在`singleuser`部分,注意容器映像`apk8s/datalab` **。**这里可以使用各种 <sup>26</sup> 的 Jupyter 笔记本图片;然而,在这种情况下,指定的映像代表在第四章中开发的高度定制的版本。

3.  在`hub`部分,`extraConfig`用于注入 Helm 表没有直接暴露的附加配置。在这种情况下,配置指示 KubeSpawner <sup>27</sup> 在`data-lab`名称空间中生成 JupyterLab Pods,并配置为使用本章前面定义的`data-lab` ServiceAccount。

    此外,在`hub`部分中,`extraEnv`用于填充后面在 values.yml 中定义的`GenericOAuthenticator`所需的环境变量。注意 Keycloak 领域`datalab`,它在本章前面创建,并在环境变量 **OAUTH2_AUTHORIZE_URL** 和 **OAUTH2_TOKEN_URL** 中定义。

4.  在`auth`版块中,`GenericOAuthenticator` <sup>28</sup> 配置了在键盘锁`datalab`领域中较早设置的`client_id`和`client_secret`。注意,数据实验室领域是`token_url`和`userdata_url`路径的一部分。

proxy:
secretToken: "large_random_hex_string"
service:
type: ClusterIP

singleuser:
image:
name: apk8s/datalab
tag: v0.0.5
defaultUrl: "/lab"
storage:
dynamic:
storageClass: rook-ceph-block
capacity: 10Gi

hub:
image:
name: jupyterhub/k8s-hub
tag: 0.9-dcde99a
db:
pvc:
storageClassName: rook-ceph-block
extraConfig:
jupyterlab: |-
c.Spawner.cmd = ['jupyter-labhub']
c.KubeSpawner.namespace = "data-lab"
c.KubeSpawner.service_account = "data-lab"
jupyterhub: |-
c.Authenticator.auto_login = True
extraEnv:

OAUTH2_AUTHORIZE_URL: https://auth.data.dev4.apk8s.dev/auth/realms/datalab/protocol/openid-connect/auth
OAUTH2_TOKEN_URL: https://auth.data.dev4.apk8s.dev/auth/realms/datalab/protocol/openid-connect/token
OAUTH_CALLBACK_URL: https://lab.data.dev4.apk8s.dev/hub/oauth_callback

scheduling:
userScheduler:
enabled: true
replicas: 2
logLevel: 4
image:
name: gcr.io/google_containers/kube-scheduler-amd64
tag: v1.14.4

auth:
type: custom
custom:
className: oauthenticator.generic.GenericOAuthenticator
config:
login_service: "Keycloak"
client_id: "datalab"
client_secret: "from_keycloak_client_config"
token_url: https://auth.data.dev4.apk8s.dev/auth/realms/datalab/protocol/openid-connect/token
userdata_url: https://auth.data.dev4.apk8s.dev/auth/realms/datalab/protocol/openid-connect/userinfo
userdata_method: GET
userdata_params: {'state': 'state'}
username_key: preferred_username

Listing 6-22JupyterHub Helm values


将 JupyterHub 库 <sup>29</sup> 添加到 Helm 并更新。

$ helm repo add jupyterhub
https://jupyterhub.github.io/helm-chart/

$ helm repo update


安装(或升级/更新)JupyterHub 头盔包。

$ helm upgrade --install lab-hub jupyterhub/jupyterhub
--namespace="data"
--version="0.9-dcde99a"
--values="values.yml"


Note

JupyterHub 需要几分钟来拉取和初始化大型容器。如果在安装过程中超时,您可能需要重新运行 Helm 命令。

最后,在清单 6-23 中名为`50-ingress.yml`的文件中为 JupyterHub web 代理配置一个入口。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: jupyterhub
namespace: data
spec:
rules:
- host: lab.data.dev4.apk8s.dev
http:
paths:
- backend:
serviceName: proxy-public
servicePort: 80
path: /
tls:
- hosts:
- lab.data.dev4.apk8s.dev
secretName: data-production-tls

Listing 6-23JupyterHub Ingress


应用入口配置:

$ kubectl apply -f 50-ingress.yml


JupyterHub 被配置为在集群和名称空间`data`中运行,并被配置为在`data-lab`名称空间中生成单用户 JupyterLab 服务器(pod)。应用配置后,JupyterHub 可能需要几分钟才能启动,因为它必须预加载大型 JupyterLab 映像。一旦 JupyterHub 完全启动,通过访问 [`https://lab.data.dev4.apk8s.dev`](https://lab.data.dev4.apk8s.dev) ,启动一个新的 JupyterLab 实例(在`datalab`领域下的 Keycloak 中创建一个用户)。

### JupyterLab

在第五章中首次介绍,JupyterLab 是“Jupyter 项目的下一代基于网络的用户界面”, <sup>30</sup> 一个功能丰富的数据科学环境。Jupyter 项目始于 2014 年,已被大量采用;2018 年,GitHub 上公开分享的 <sup>31</sup> Jupyter 笔记本超过 250 万台。Kubernetes 非常适合通过 JupyterHub 提供和服务 JupyterLab 环境,如前一节所述。

简化机器学习和统计模型的开发推动了 Jupyter 项目的成功。许多数据科学活动,如机器学习,需要静态的、不变的数据集来从实验中获得可重复的结果。然而,在 Jupyter 环境中运行静态数据以及实时事件流、索引和 Kubernetes 分布式计算的全部功能是一个直接在数据平台中心提供各种数据科学功能的机会。

以下部分演示了从集群内部直接使用数据和控制面板的简短示例,将 JupyterLab 笔记本电脑与 Kubernetes API、Kafka、Elasticsearch 和 Mosquitto MQTT 连接起来。

#### 库伯勒斯 API

默认的 JupyterLab 环境包括一个 CLI(命令行界面)终端,在本章中使用的定制的 JupyterLab(在第四章中开发)提供了`kubectl`。图 6-7 展示了`kubectl`与 Kubernetes API 通信以检索当前名称空间中运行的 pod 列表。`kubectl`被允许通过自定义服务帐户和本章前面应用的 RBAC 配置访问 Kubernetes API。

![img/483120_1_En_6_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig7_HTML.jpg)

图 6-7

在 JupyterLab 终端中运行 kubectl

图 6-8 描述了使用 Kubernetes 的官方 Python 客户端库,通过运行在基于 Python 的 Jupyter 笔记本中的代码与 Kubernetes API 进行通信。 <sup>32</sup> 将权限扩展到 JupyterLab Pod 使用的服务帐户可以允许 Python 执行任何 Kubernetes API 操作,例如,创建 Pod、Jobs、 <sup>33</sup> CronJobs、 <sup>34</sup> 或与数据科学、分析或 ETL 活动相关的部署。

![img/483120_1_En_6_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig8_HTML.jpg)

图 6-8

集群中运行 Python Kubernetes API 的 Jupyter 笔记本

从 Kubernetes 内部创建、管理和监控 Kubernetes 资源促进了机器学习自动化、深度学习和基于人工智能的解决方案部署方面的重大进展。Kubeflow(在第一章中描述)就是这样一个应用,它是一个“Kubernetes 的机器学习工具包”, <sup>35</sup> ,它充分利用 Kubernetes API 来自动化许多与机器学习相关的复杂任务。

#### Kafka

图 6-9 描绘了一个基于 Python 的 Jupyter 笔记本向 Kafka 主题`metrics`发布模拟设备传感器数据。从 Kafka 主题生成和消费数据只需要几行代码。Kafka 是服务之间交流事件和数据的强大渠道,包括 Jupyter 笔记本。在本章的前面,Logstash 被配置为消费来自选定 Kafka 主题的事件,并将它们交付给 Elasticsearch 进行索引和长期持久化。下一节演示如何从 Elasticsearch 的目的地检索图 6-9 中产生的数据。

![img/483120_1_En_6_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig9_HTML.jpg)

图 6-9

Jupyter 笔记本运行 Python Kafka 制作人

#### 弹性搜索

图 6-10 描述了一个针对任何以`apk8s-metrics-`开头的弹性搜索索引的简单`match_all`查询。在该示例中展示的查询可以被构建来执行跨越数十亿条记录的高级搜索、过滤、<sup>、?? 36</sup>和聚合 <sup>37</sup> 。 <sup>38</sup>

![img/483120_1_En_6_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig10_HTML.jpg)

图 6-10

用 Python 进行 Jupyter 笔记本弹性搜索

数据挖掘 Elasticsearch 的结果可以形成新的索引或导出为基于 CSV 的数据集,以便与机器学习结果一起打包和共享。

#### 蚊子(MQTT)

MQTT 是物联网通信和指标收集的流行选择。第五章介绍了 Mosquitto 为数据流水线提供 MQTT 支持。图 6-12 只描绘了需要从 MQTT 主题中消费事件的几行代码。这个例子显示了从 MQTT.fx 应用发送到`dev/apk8s/lightbulb`主题的测试 JSON 消息,如图 6-11 所示,运行在本地工作站上。命令`kubectl port-forward svc/mqtt 1883:1883 -n data`允许 MQTT.fx 从本地端口连接到集群。

![img/483120_1_En_6_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig12_HTML.jpg)

图 6-12

Jupyter 笔记本 Python MQTT 消费者

![img/483120_1_En_6_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_6_Fig11_HTML.jpg)

图 6-11

MQTT 测试实用程序

## 摘要

本章配置并演示了 ELK 栈(Elasticsearch、Logstash 和 Kibana ),以提供企业级的数据流水线、索引、分析和持久性。该集群现在支持 SSO、身份管理和通过 Keycloak 进行授权,key cloak 最初由 JupyterHub 用来对用户进行身份验证,并为 JupyterLab 实例提供对 Kubernetes API 的有限访问。如果您继续学习,Kubernetes 清单的结构应该类似于清单 6-24 。

这本书应该展示 Kubernetes 赋予平台架构师、软件开发人员、研究人员甚至爱好者的力量,让他们从各种一流的应用中快速构建现代数据平台。这本书是开发新颖解决方案的粗略草图和演示,这些解决方案将来自物联网、区块链和大数据技术的事件和数据通信到机器学习模型中,为基于推理的业务逻辑提供动力。

下一章继续通过数据湖和数据仓库的概念来支持数据。

.
└── cluster-apk8s-dev4
├── 000-cluster
├── 003-data
│ ├── 000-namespace
│ ├── 005-keycloak
│ │ ├── 10-service.yml
│ │ ├── 15-secret.yml
│ │ ├── 30-deployment.yml
│ │ └── 50-ingress.yml
│ ├── 010-zookeeper
│ ├── 020-kafka
│ ├── 030-elasticsearch
│ │ ├── 10-service.yml
│ │ └── 40-statefulset.yml
│ ├── 032-logstash
│ │ ├── 10-service.yml
│ │ ├── 20-configmap-config.yml
│ │ ├── 20-configmap-pipeline.yml
│ │ └── 30-deployment.yml
│ ├── 034-kibana
│ │ ├── 10-service.yml
│ │ ├── 20-configmap.yml
│ │ ├── 30-deployment.yml
│ │ └── 50-ingress.yml
│ └── 050-mqtt
└── 005-data-lab
└── 000-namespace
├── 00-namespace.yml
├── 05-serviceaccount.yml
├── 07-role.yml
└── 08-rolebinding.yml

Listing 6-24Indexing and analytics development cluster configuration layout


<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1

海特,卡梅隆。"输入网络规模的信息."Gartner 博客网,2013 年 5 月 16 日。 [`https://blogs.gartner.com/cameron_haight/2013/05/16/enter-web-scale-it/`](https://blogs.gartner.com/cameron_haight/2013/05/16/enter-web-scale-it/) 。

  2

洛尔,史蒂夫。"“大数据”的起源:一个词源侦探故事."比特博客(Blog),2013 年 2 月 1 日。 [`https://bits.blogs.nytimes.com/2013/02/01/the-origins-of-big-data-an-etymological-detective-story/`](https://bits.blogs.nytimes.com/2013/02/01/the-origins-of-big-data-an-etymological-detective-story/) 。

  3

迪克森詹姆斯。Pentaho、Hadoop 和数据湖詹姆斯·狄克逊的博客(Blog),2010 年 10 月 14 日。 [`https://jamesdixon.wordpress.com/2010/10/14/pentaho-hadoop-and-data-lakes/`](https://jamesdixon.wordpress.com/2010/10/14/pentaho-hadoop-and-data-lakes/) 。

  4

[`https://httpd.apache.org/docs/current/programs/htpasswd.html`](https://httpd.apache.org/docs/current/programs/htpasswd.html)

  5

[`www.elastic.co/what-is/elk-stack`](http://www.elastic.co/what-is/elk-stack)

  6

[`https://opendistro.github.io/for-elasticsearch/`](https://opendistro.github.io/for-elasticsearch/)

  7

伦纳德安德鲁。"开源开发者说,亚马逊已经从中立平台变成了残酷的竞争对手."中,2019 年 4 月 24 日。 [`https://onezero.medium.com/open-source-betrayed-industry-leaders-accuse-amazon-of-playing-a-rigged-game-with-aws-67177bc748b7`](https://onezero.medium.com/open-source-betrayed-industry-leaders-accuse-amazon-of-playing-a-rigged-game-with-aws-67177bc748b7) 。

  8

[`https://lucene.apache.org/`](https://lucene.apache.org/)

  9

[`www.elastic.co/elasticsearch-kubernetes`](http://www.elastic.co/elasticsearch-kubernetes)

  10

[`www.elastic.co/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster`](http://www.elastic.co/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster)

  11

[`www.elastic.co/products/logstash`](http://www.elastic.co/products/logstash)

  12

[`www.elastic.co/guide/en/logstash/current/deploying-and-scaling.html`](http://www.elastic.co/guide/en/logstash/current/deploying-and-scaling.html)

  13

[`https://jupyterhub.readthedocs.io/en/stable/`](https://jupyterhub.readthedocs.io/en/stable/)

  14

[`www.keycloak.org/`](http://www.keycloak.org/)

  15

[`www.keycloak.org/docs/latest/server_admin/index.html#_create-realm`](http://www.keycloak.org/docs/latest/server_admin/index.html%2523_create-realm)

  16

[`https://oauth.net/2/`](https://oauth.net/2/)

  17

[`https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/namespaces.md#design`](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/namespaces.md%2523design)

  18

[`https://kubernetes.io/docs/concepts/policy/resource-quotas/`](https://kubernetes.io/docs/concepts/policy/resource-quotas/)

  19

[`https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-groups`](https://kubernetes.io/docs/concepts/overview/kubernetes-api/%2523api-groups)

  20

[`https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-examples`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/%2523role-examples)

  21

朱庇特项目团队。" JupyterHub 文档。"2019 年 10 月 21 日。[`https://jupyterhub.readthedocs.io/en/stable/`](https://jupyterhub.readthedocs.io/en/stable/)

  22

[`https://zero-to-jupyterhub.readthedocs.io/en/latest/`](https://zero-to-jupyterhub.readthedocs.io/en/latest/)

  23

[`https://helm.sh/docs/`](https://helm.sh/docs/)

  24

[`https://zero-to-jupyterhub.readthedocs.io/en/latest/setup-helm.html`](https://zero-to-jupyterhub.readthedocs.io/en/latest/setup-helm.html)

  25

[www. openssl. org/](http://www.openssl.org/)

  26

[`https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html`](https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html)

  27

[`https://jupyterhub-kubespawner.readthedocs.io/en/latest/spawner.html`](https://jupyterhub-kubespawner.readthedocs.io/en/latest/spawner.html)

  28

[`https://github.com/jupyterhub/oauthenticator`](https://github.com/jupyterhub/oauthenticator)

  29

[`https://zero-to-jupyterhub.readthedocs.io/en/latest/setup-jupyterhub/setup-helm.html`](https://zero-to-jupyterhub.readthedocs.io/en/latest/setup-jupyterhub/setup-helm.html)

  30

朱庇特计划。" JupyterLab 文档。"2019. [`https://jupyterlab.readthedocs.io/`](https://jupyterlab.readthedocs.io/) 。

  31

为什么 Jupyter 是数据科学家的计算笔记本的选择。“自然 563(2018 年 10 月 30 日):145–46。 [`https://doi.org/10.1038/d41586-018-07196-1`](https://doi.org/10.1038/d41586-018-07196-1) 。

  32

[`https://github.com/kubernetes-client/python`](https://github.com/kubernetes-client/python)

  33

[`https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/`](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/)

  34

[`https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/`](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/)

  35

[`www.kubeflow.org`](http://www.kubeflow.org)

  36

[`www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html`](http://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html)

  37

[`www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html`](http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html)

  38

约多克·巴特洛格。“在 900 毫秒内查询 240 亿条记录。”弹性博客。2012. [`www.elastic.co/videos/querying-24-billion-records-in-900ms`](http://www.elastic.co/videos/querying-24-billion-records-in-900ms) 。

 </aside>

# 七、数据湖

数据仓库、数据集市和数据湖的概念在许多企业中已经变得司空见惯。大数据技术使组织能够收集、存储和处理不断增长的数据流。物联网、社交媒体和业务各个方面的数字化转型只会继续增加组织可用数据的数量和速度。

传统上,大数据概念侧重于为大型组织或数据收集和分析项目管理海量和各种收集数据的问题。大数据技术,特别是 Apache Hadoop 及其生态系统,使组织能够消费和存储其组织产生或相关的所有数据。然而,许多大数据解决方案是在容器和容器编排的可用性和受欢迎程度上升之前出现的,特别是 Kubernetes。过去,采用大数据技术通常意味着配置专用集群,并且通常需要一个团队来操作和维护它们。关于数据湖和数据仓库的单独一章很难触及这个充满活力的成熟生态系统的表面。以下练习旨在展示小规模的一组微小的大数据概念,利用 Kubernetes 在静态和事务性数据以及各种工作负载之间统一控制面板的优势。

所有数据都对组织有一定的价值;无论是研究地区销售报告的分析师,还是审计库存水平的采购经理,获取这一价值的成本一直是一个挑战。然而,在过去的二十年里,可用数据的数量和频率急剧增加,从消费和工业物联网到社交媒体、IT 系统和定制应用都产生了大量数据。大型组织需要解决大数据问题的解决方案。2006 年,Hadoop 项目通过将任意数量的商用服务器集群成一个大数据解决方案来解决这些问题。Hadoop 分布式文件系统(HDFS)及其 MapReduce 概念的实现允许将数据无限制地收集到巨大的“湖泊”中,并在它们的源头进行分析。对于许多组织来说,Hadoop 是一项有价值且强大的技术。然而,许多 Hadoop 功能可以在 Kubernetes 中实现,包括高度分布式工作负载、容错和自修复,以及更广泛和快速扩展的生态系统的好处。Kubernetes 不是大数据技术,但下一代支持大数据的系统可能会由此诞生。

本章并不试图说服已建立大数据管理应用的企业考虑将它们迁移到 Kubernetes 相反,目标是为在 Kubernetes 上开发的各种新应用平台中实现这些概念打下基础。

## 数据处理流水线

图 7-1 描绘了一个典型的数据处理流水线,包括原始和已处理数据存储、事件系统、元数据系统以及用于分析和转换的应用工作负载。本章中描述的大部分架构并不是专门为 Kubernetes 设计的,也不依赖于 Kubernetes 来运行。但是,将这些专门的集群包装在 Kubernetes 集群中可以建立统一的控制面板、网络、监控、安全策略和细粒度的资源管理,包括配置和限制存储、内存和 CPU。尽管诸如 Elasticsearch、Kafka、MQTT 和其他企业解决方案等技术需要广泛的知识来在要求苛刻的生产环境中有效地配置和管理它们,但 Kubernetes 抽象了所有人都通用的底层基础设施。

![img/483120_1_En_7_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_7_Fig1_HTML.jpg)

图 7-1

目标处理流水线

## 发展环境

本章通过包含用于对象存储的 MinIO 和作为对象元数据、原始数据仓库和已处理数据的键/值存储的 Apache Cassandra,在前面章节的基础上构建了数据处理和管理功能。

以下练习利用了第六章中提到的廉价 Hetzner 集群,包括一个用于 Kubernetes 主节点的 CX21 (2 个 vCPU/8G RAM/40G SSD)和四个用于 worker 节点的 CX41 (4 个 vCPU/16G RAM/160G SSD)实例,但任何等效的基础架构都可以容纳。此外,本章还利用了第 3 、 5 和 6 章中安装的应用和集群配置;见表 7-1 。本章在文件夹`cluster-apk8s-dev5`下组织新集群`dev5`的配置清单。

表 7-1

从前面章节收集的关键应用和配置

<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"> <col class="tcol3 align-left"></colgroup> 
|   | 

资源

 | 

组织

 |
| --- | --- | --- |
| 第三章 | 进入证书管理器仓库监视 | `000-cluster/00-ingress-nginx``000-cluster/10-cert-manager``000-cluster/20-rook-ceph``000-cluster/30-monitoring` |
| 第五章 | 命名空间动物园管理员 Kafka 蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子 | `003-data/000-namespace``003-data/010-zookeeper``003-data/020-kafka``003-data/050-mqtt` |
| 第六章 | 弹性搜索 logstash(日志记录)巴拉人凯克洛克 JupyterHub | `003-data/030-elasticsearch``003-data/032-logstash``003-data/034-kibana``003-data/005-keycloak``005-data-lab/000-namespace``003-data/100-jupyterhub` |

本章的剩余部分将重点介绍如何读写数据到现代概念的数据湖,数据湖是由 MinIO 实现的对象存储。MinIO 能够发出与对象(文件)的添加、状态和删除相关的事件,这使它成为任何数据处理流水线的一个引人注目的补充。最后,本章演示了应用的快速原型,该应用利用运行在 JupyterLab 实例中的基于 Python 的 Jupyter 笔记本,对 MinIO 生成的 Kafka 和 MQTT 中的对象相关事件做出反应。

## 作为对象存储的数据湖

事务数据库、数据仓库和数据集市都是旨在以已知的结构存储和检索数据的技术。组织经常需要存储新的和各种类型的数据,这些数据的形式通常是未知的或不适合结构化数据系统。以任何可能的形式管理无限数据的概念被称为数据湖。传统上,文件系统和块存储解决方案存储组织希望在其数据库管理系统之外收集和维护的大多数基于文件的数据。文件系统和数据块存储系统很难扩展,它们具有不同程度的容错、分布以及对元数据和分析的有限支持。

HDFS (Hadoop 分布式文件系统)一直是需要数据湖概念优势的组织的热门选择。HDFS 的设置和维护非常复杂,通常需要专用的基础架构和一名或多名专家来保持其运行和性能。

本章构建了一个带有对象存储的数据湖,用 MinIO 实现。MinIO 提供了一个与亚马逊 S3 兼容的分布式容错对象存储系统。MinIO 是水平可伸缩的,支持高达 5tb 的对象,它可以存储的对象数量没有限制。这些功能本身就满足了数据湖的基本概念要求。然而,MinIO 是可扩展的,尽管它支持事件和强大的 S3 兼容查询系统。

### 迷你操作员

Kubernetes 操作员是一种资源管理器,在这种情况下,安装和管理一个或多个 MinIO 集群。

Note

Kubernetes 运营商的概念产生于 2016 年。CoreOS 开始开发代表控制器的定制资源定义,旨在管理有状态应用的生命周期,并将它们称为操作符。操作员通过管理安装前和安装后的条件、监控和运行时操作,超越了 Helm 等软件包管理人员和安装人员的有限关注。操作符是定制的 Kubernetes 资源,像其他资源一样以声明方式安装,或者通过 Helm 之类的包管理器安装。收购 CoreOs 后,红帽在 2018 年发布了运营商框架 <sup>1</sup> ,后来又与贡献者亚马逊、微软、谷歌推出了 OperatorHub <sup>2</sup> 。

MinIO 项目提供了一个官方的 Kubernetes 运营商。以下配置安装 MinIO 操作符,管理用于声明新 MinIO 集群的新定制资源定义`Tenant`。

创建目录`cluster-apk8s-dev5/000-cluster/22-minio`以包含 MinIO 操作器安装文档。接下来,从清单 7-1 中创建一个名为`README.md`的文件。

MinIO Operator Installation

see: https://github.com/minio/operator
Quick Start:

kubectl apply -k github.com/minio/operator

Listing 7-1MinIO operator installation documentation


应用 MinIO 操作员配置:

$ kubectl apply -k github.com/minio/operator


Kubernetes 集群现在包含名称空间`minio-system`,以及名为`minio-operator`的 ServiceAccount 和部署。“操作员是 Kubernetes API 的客户端,充当定制资源的控制器,” <sup>3</sup> 在本例中,是新的资源类型`Tenant`。下一节通过声明一个`Tenant`资源来建立一个 MinIO 集群。

### 小型群集

MinIO 是一个 S3 兼容的对象存储系统,能够为平台提供一种数据湖功能。清单 7-2 中定义的小型 Minio 集群包括四个节点,支持基本级别的高可用性。`Tenant`资源描述了四个各有 10gb 的 MinIO 节点,使用在第三章中创建的`rook-ceph-block`存储类。标准存储配置将一半的可用磁盘(在本例中为永久卷)用于数据,另一半用于奇偶校验,允许在失去一个节点时进行完整的读/写活动,以及在失去两个节点时进行只读活动。这种配置对于小型开发集群或概念验证来说已经足够。

为清单 7-2 中定义的密码的用户名和密码部分生成一个大的随机字符串。此用户名和密码等同于 AWS S3 凭据,可以在任何 S3 兼容的客户端中使用,以与 MinIO 进行交互。

创建目录`cluster-apk8s-dev5/003-data/070-minio`以包含 MinIO 集群配置。接下来,从清单 7-2 中创建一个名为`90-cluster.yml`的文件。

apiVersion: v1
kind: Secret
metadata:
namespace: data
name: minio-creds-secret
type: Opaque
stringData:
accesskey: REPLACE_WITH_STRONG_PASSWORD
secretkey: REPLACE_WITH_STRONG_PASSWORD

apiVersion: minio.min.io/v1
kind: Tenant
metadata:
name: minio
namespace: data
spec:
metadata:
annotations:
prometheus.io/path: /minio/prometheus/metrics
prometheus.io/port: "9000"
prometheus.io/scrape: "true"
image: minio/minio:RELEASE.2020-08-18T19-41-00Z
serviceName: minio-internal-service
zones:
- name: "zone-0"
servers: 4
volumesPerServer: 1
volumeClaimTemplate:
metadata:
name: miniodata
spec:
storageClassName: rook-ceph-block
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi

Secret with credentials to be used by MinIO instance.

credsSecret:
name: minio-creds-secret
podManagementPolicy: Parallel
requestAutoCert: false
certConfig:
commonName: ""
organizationName: []
dnsNames: []
liveness:
initialDelaySeconds: 10
periodSeconds: 1
timeoutSeconds: 1

Listing 7-2MinIO cluster configuration


应用 MinIO 群集配置:

$ kubectl apply -f 90-cluster.yml


在应用了集群配置(包括`Secret`和`Tenant`资源)之后,可以在 Kubernetes 集群内的服务地址`minio-internal-service:9000`或无头服务地址`minio-hl:9000`访问 MinIO。

如清单 7-3 所示,入口配置提供对 MinIO 的外部访问。MinIO 与 AWS S3 对象存储服务兼容;因此,能够与 AWS S3 交互的现有系统可以利用新的`minio.data.dev5.apk8s.dev`作为替代端点。

接下来,从清单 7-3 中创建一个名为`50-ingress.yml`的文件。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: minio
namespace: data
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
rules:
- host: minio.data.dev5.apk8s.dev
http:
paths:
- backend:
serviceName: minio-internal-service
servicePort: 9000
path: /
tls:
- hosts:
- minio.data.dev5.apk8s.dev
secretName: minio-data-tls

Listing 7-3MinIO cluster Ingress


为新的 MinIO 群集应用入口配置:

$ kubectl apply -f 50-ingress.yml


除了兼容 S3 的 API 之外,MinIO 还是一个有用且有吸引力的基于网络的用户界面,可通过位于 [`https://minio.data.dev5.apk8s.dev/`](https://minio.data.dev5.apk8s.dev/) 的网络浏览器访问。

下一节配置命令行 MinIO 客户机,用于创建存储桶、配置 MinIO 服务器和设置通知事件。

### 微型客户端

MinIO 客户端 <sup>4</sup> 是一个命令行实用程序,用于与 MinIO 集群进行交互(大部分与 AWS S3 兼容)。MinIO 客户端支持与对象存储相关的任何操作,从创建存储桶和列出对象到完整的管理和配置功能。在下一节中,MinIO 客户机用于在下面创建的存储桶上配置与对象状态相关的通知。

有关任何特定工作站的安装说明,请参考 MinIO 客户端快速入门指南。在本地工作站上安装 MinIO 客户端(mc)后,使用清单 7-3 中定义的入口主机和清单 7-2 中定义的凭证为名为`apk8s-dev5`的新集群创建一个别名:

$ mc config host add apk8s-dev5
https://minio.data.dev5.apk8s.dev
username password


创建桶,`upload`、`processed,`和`twitter`,在本章后面和下一章中使用:

$ mc mb apk8s-dev5/upload
$ mc mb apk8s-dev5/processed
$ mc mb apk8s-dev5/twitter


列出三个新桶:

$ mc ls apk8s-dev5


MinIO 客户端可以轻松地从本地工作站或 AWS S3、谷歌云存储等复制文件。首先创建新的主机别名或列出现有别名:

$ mc config host list


MinIO 客户端是与对象存储集群进行手动交互的强大工具。为了对对象集群进行编程控制,MinIO 提供了 JavaScript、Java、Python、Golang、.NET 和 Haskell。MinIO 集群还兼容 AWS S3 API 和许多库,如 Amazon 的 Boto3 for Python、AWS。JavaScript 的 s3 类,或者 Golang 的 S3 包。Boto3 将在本章后面用于处理 CSV 文件。

下一节将利用 MinIO 发出与对象相关的事件的能力。

### 迷你活动

与对象相关的事件通知允许任意数量的服务使用这些通知并执行任务,从而为以数据为中心的平台提供了一种复杂的可扩展性。MinIO 支持配置对象状态通知给 AMQP、 <sup>5</sup> Elasticsearch、Kafka、MQTT、MySQL、NATS、 <sup>6</sup> NSQ、 <sup>7</sup> PostgreSQL、Redis 和 webhooks。 <sup>8</sup>

以下练习利用第 5 和 6 章中安装的技术,为 MinIO 配置通知 Elasticsearch、Kafka 和 MQTT 的能力。在为 Elasticsearch、Kafka 和 MQTT 配置连接设置之后,MinIO 服务器需要重新启动;但是,在此初始配置之后,在配置任意数量的通知设置时,不需要进一步重新启动。

通过请求当前配置并将其保存为 JSON 文件,开始配置 MinIO。使用前一节中安装的 MinIO 客户端命令行实用程序(`mc`)。

为 MinIO 服务器配置创建目录:

$ cd ./005-cluster-apk8s-dev5/003-data
$ mkdir -p ./070-minio/cfg


接下来,以 JSON 文件的形式获取现有的服务器配置(上一节描述了配置`the apk8s-dev5`别名)。以下多行命令将 JSON 输出通过流水线传输到 Python,以便对其进行格式化,从而更易于编辑:

$ mc admin config get apk8s-dev5 |
python -m json.tool > config.json


在`config.json`的`"notify"`部分下,编辑 Elasticsearch、Kafka 和 MQTT 的部分以匹配清单 7-4 。

"elasticsearch": {
"1": {
"enable": true,
"format": "namespace",
"index": "processed",
"url": "http://elasticsearch:9200"
}
},
"kafka": {
"1": {
"brokers": ["kafka-headless:9092"],
"enable": true,
"sasl": {
"enable": false,
"password": "",
"username": ""
},
"tls": {
"clientAuth": 0,
"enable": false,
"skipVerify": false
},
"topic": "upload"
}
},
"mqtt": {
"1": {
"broker": "tcp://mqtt:1883",
"enable": true,
"keepAliveInterval": 0,
"password": "",
"qos": 0,
"queueDir": "",
"queueLimit": 0,
"reconnectInterval": 0,
"topic": "processed",
"username": ""
}
},

Listing 7-4MinIO server external connection configuration


应用编辑过的`config.json`并重启 MinIO 服务器:

$ mc admin config set apk8s-dev5 < config.json
$ mc admin service restart apk8s-dev5


MinIO 产生与对象的创建、删除和访问相关的事件通知类型,如表 7-2 所示。MinIO 支持按存储桶进行事件通知配置,并支持按事件类型、对象名前缀或对象名后缀进行细粒度过滤。

表 7-2

MinIO 支持的对象事件

<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup> 
| 

种类

 | 

事件

 |
| --- | --- |
| 创造 | `s3:ObjectCreated:Put``s3:ObjectCreated:Post``s3:ObjectCreated:Copy``s3:ObjectCreated:CompleteMultipartUpload` |
| 删除 | `s3:ObjectRemoved:Delete` |
| 接近 | `s3:ObjectAccessed:Get``s3:ObjectAccessed:Head` |

下一节将演示在 Kubernetes 中处理对象相关事件的方法。

### 流程对象

JupyterLab,如第六章中所述,为数据科学活动提供了一个便利且高效的环境,这主要是因为它靠近集群内数据和事件源。同样,在 Kubernetes 中运行的 JupyterLab 简化了组合基于数据的事件、基于集群的数据和 Kubernetes API 的实验能力。Jupyter 笔记本是快速构建和记录复杂原型的有用工具。

以下练习构建了事件侦听器的原型,创建了一个大型 CSV 文件并将其放入 MinIO bucket,通过生成 Kubernetes 作业来压缩 CSV,并演示了如何从压缩的 CSV 中提取数据。

#### 配置通知

首先,配置 MinIO 在特定的 bucket 事件发生时通知 Kafka 和 MQTT 主题,特别是在后缀为`.csv`的`upload` bucket 中创建任何对象时,以及在后缀为`.gz`的`processed` bucket 中创建任何对象时。在本练习中,Kafka 和 MQTT 的使用只是为了说明各种事件队列:

$ mc event add apk8s-dev5/upload
arn:minio:sqs::1:kafka
--event put --suffix=".csv"

$ mc event add apk8s-dev5/processed
arn:minio:sqs::1:mqtt
--event put --suffix=".gz"


此外,配置 MinIO 在 Elasticsearch 中保留一个文档索引,描述`processed`桶中所有对象的状态:

$ mc event add apk8s-dev5/processed
arn:minio:sqs::1:elasticsearch \


#### 事件笔记本

打开一个 JupyterLab 环境并启动一个 Python 笔记本来测试由 MinIO 产生的新的 bucket 通知事件。在第一个单元格中,确保 kafka-python 库可用:

!pip install kafka-python==1.4.7


导入标准的 JSON 库来解析 MinIO 通知,从 kafka-python 库中导入 KafkaConsumer <sup>9</sup> 类,最后,从 IPython.display 中导入`clear_output`函数来帮助清除循环之间的单元格输出:

import json
from kafka import KafkaConsumer
from IPython.display import clear_output


将 KafkaConsumer 连接到数据名称空间中 Kafka 集群上的上传主题。将`group_id`设置为`data-bucket-processor`:

consumer = KafkaConsumer('upload',
bootstrap_servers="kafka-headless.data:9092",
group_id='data-bucket-processor')


最后,在消费者消息上创建一个`for`循环。循环无限继续,用当前事件替换最后一个事件:

for msg in consumer:
jsmsg = json.loads(msg.value.decode("utf-8"))
clear_output(True)
print(json.dumps(jsmsg, indent=4))


通过从本地工作站创建一个测试 CSV 文件并上传到`upload`桶来测试事件监听器:

$ touch test.csv
$ mc cp test.csv apk8s-dev5/upload


JupyterLab 事件笔记本现在应该显示类似于清单 7-5 的`s3:ObjectCreated:Put`事件通知。

{
"EventName": "s3:ObjectCreated:Put",
"Key": "upload/test.csv",
"Records": [
{
"eventVersion": "2.0",
"eventSource": "minio:s3",
"awsRegion": "",
"eventTime": "2019-12-27T08:27:40Z",
"eventName": "s3:ObjectCreated:Put",
"userIdentity": {
"principalId": "3Fh36b37coCN3w8GAM07"
},
"requestParameters": {
"accessKey": "3Fh36b37coCN3w8GAM07",
"region": "",
"sourceIPAddress": "0.0.0.0"
},
"responseElements": {
"x-amz-request-id": "15E42D02F9784AD2",
"x-minio-deployment-id": "0e8f8...",
"x-minio-origin-endpoint": "http://10.32.128.13:9000"
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "Config",
"bucket": {
"name": "upload",
"ownerIdentity": {
"principalId": "3Fh36b378GAM07"
},
"arn": "arn:aws:s3:::upload"
},
"object": {
"key": "test.csv",
"eTag": "d41d8cd98f998ecf8427e",
"contentType": "text/csv",
"userMetadata": {
"content-type": "text/csv",
"etag": "d41d8cd90998ecf8427e"
},
"versionId": "1",
"sequencer": "15E42D02FD98E21B"
}
},
"source": {
"host": "0.0.0.0",
"port": "",
"userAgent": "MinIO (darwin"
}
}
]
}

Listing 7-5Example bucket event notification


#### 测试数据

Python 是收集、规范化甚至生成数据的优秀工具。下面的练习生成了一个包含一百万个虚构献血者的 CSV 文件,每个献血者都包含一个假的电子邮件地址、姓名、血型、生日和州名。最后,脚本将生成的 CSV 数据上传到新 MinIO 集群上的上传桶中。

本节继续使用集群内 JupyterLab 环境;然而,这不是必需的。在 Kubernetes 集群的 JupyterLab 中创建新的 Jupyter 笔记本,或者在本地工作站上创建 Python 脚本。

示例 Python 脚本使用 Faker Python 库创建假数据,并使用 MinIO 客户端库连接到 MinIO。通过将以下 pip 安装指令添加到 Jupyter 笔记本的第一个单元,确保开发环境包含所需的依赖项:

!pip install Faker2.0.3
!pip install minio
5.0.1


在下一个单元格中,导入以下库:

import os
from faker import Faker
from minio import Minio
from minio.error import ResponseError


在下一个单元格中,添加以下代码以打开一个名为`donors.csv`的文件,为数据编写一个初始标题,并创建一个增量为一百万的`while`循环:

%%time
fake = Faker()

f_customers = open("./donors.csv","w+")
f_customers.write(
"email, name, type, birthday, state\n"
)

i = 0
while i < 1000000:
fp = fake.profile(fields=[
"name",
"birthdate",
"blood_group"])

st = fake.state()
bd = fp["birthdate"]
bg = fp["blood_group"]
ml = fake.ascii_safe_email()

f_customers.write(
    f'{ml},{fp["name"]},{bg},{bd},{st}\n'
)
i += 1

f_customers.close()


在 Jupyter 笔记本的最后一个单元格中,连接本章前面设置的 MinIO 集群,将新生成的`donors.csv`上传到`upload`桶中;用 MinIO 集群设置中定义的值替换**用户名**和**密码**:

Note

如果从本地工作站开发,用`'minio.data.dev5.apk8s.dev'`替换`'minio-internal-service.data:9000'`,用`secure=True`替换`secure=False`。

%%time
minioClient = Minio('minio-internal-service.data:9000',
access_key='username',
secret_key='password',
secure=False)

try:
with open("./donors.csv", 'rb') as file_data:
file_stat = os.stat('./donors.csv')
minioClient.put_object('upload',
'donors.csv',
file_data,
file_stat.st_size,
content_type='application/csv')

except ResponseError as err:
print(err)


如果前一部分的事件笔记本正在运行,将显示一个名为`s3:ObjectCreated:CompleteMultipartUpload`的新事件,带有键`upload/donors.csv`。MinIO 集群现在拥有大约 60 兆字节的对象,包含测试数据,代表假献血者,他们的电子邮件,姓名,出生日期,血型和州。

此时,事件系统可能会触发大范围的数据处理操作。下一节使用一个定制的数据压缩应用来演示一个典型的对象处理流程。

#### 容器化应用

在 Kubernetes 上,所有工作负载都在 Pod 的一个容器中执行。最后几个练习使用一个运行 JupyterLab 容器的 Pod 来执行在 Jupyter 笔记本中编写的代码,这是一种用于实验、构建概念验证或快速原型制作的优秀方法。随着原型化过程迭代到稳定的生产系统,识别独立的工作单元,开发并将其包装在容器中,测试,版本化,并使它们可用于编排。

下面的练习在 Go 中开发了一个通用对象压缩器,构建并版本化了一个容器,并使该容器在 Docker Hub 上公开可用。在本地工作站上,确保存在 Go 版本 1.13+和 Docker 版本 19+。使用 GitLab 和 Docker Hub 上的新帐户或现有帐户(都是免费的)来存储源代码和生成的容器。

在本地工作站上为新的 Go 应用创建一个文件夹;这个例子使用了文件夹`~/workspace/apk8s/compressor`。在新文件夹中,初始化 Go 模块 <sup>10</sup> (替换为任何定制的 Git 库):

$ go mod init github.com/apk8s/compressor


Note

从 Go 1.14 开始, <sup>11</sup> Go 模块已准备好投入生产使用,并被视为 Go 的正式依赖管理系统。鼓励所有开发人员在迁移任何现有项目的同时,为新项目使用 Go 模块。

创建目录`cmd`来存储主要的 Go 源代码——这种约定通常用于表示命令行应用,而不是共享库的组件。在 cmd 目录下创建一个名为`compressor.go`的文件,并用清单 7-6 中的源代码填充它。

package main

import (
"bufio"
"compress/gzip"
"flag"
"io"
"log"
"os"

  minio "github.com/minio/minio-go/v6"

)

var (
endpoint = os.Getenv("ENDPOINT")
endpointSSL = os.Getenv("ENDPOINT_SSL")
accessKeyID = os.Getenv("ACCESS_KEY_ID")
accessKeySecret = os.Getenv("ACCESS_KEY_SECRET")
)

func main() {
var (
fmBucket = flag.String("f", "", "From bucket.")
toBucket = flag.String("t", "", "To bucket.")
fmObjKey = flag.String("k", "", "From key.")
)
flag.Parse()

  useSSL := true
  if endpointSSL == "false" {
        useSSL = false
  }

  mc, err := minio.New(
        endpoint, accessKeyID,
        accessKeySecret, useSSL)
  if err != nil {
        log.Fatalln(err)
  }

  obj, err := mc.GetObject(
        *fmBucket,
        *fmObjKey,
        minio.GetObjectOptions{},
  )
  if err != nil {
        log.Fatalln(err)

  }

  log.Printf("Starting download stream %s/%s.",
        *fmBucket,
        *fmObjKey)

  // synchronous in-memory pipe
  pipeR, pipeW := io.Pipe()

  // reads from object, writes to pipe
  bufIn := bufio.NewReader(obj)

  // gzip buffers to memory and flushes on close
  gzW, err := gzip.NewWriterLevel(pipeW, 3)
  if err != nil {
        log.Fatalln(err)
  }

  go func() {
        log.Printf("Compress and stream.")
        n, err := bufIn.WriteTo(gzW)
        if err != nil {
              log.Fatalln(err)
        }
        gzW.Close()
        pipeW.Close()
        log.Printf("Compressed: %d bytes", n)
  }()

  // data will not be sent until gzW.Close() and
  // the gzip buffer flushes
  log.Print("BEGIN PutObject")
  _, err = mc.PutObject(
        *toBucket, *fmObjKey+".gz",
        pipeR, -1, minio.PutObjectOptions{})
  if err != nil {
        log.Fatalln(err)
  }
  log.Print("COMPLETE PutObject")

}

Listing 7-6Go application: compressor


前面定义的新 Go 应用连接到一个 MinIO 集群,压缩一个对象,并将压缩后的输出放在一个单独的桶中的新对象中。这个应用只是对象处理的一个简单例子,它利用 Go 编程语言创建一个小型的、静态编译的二进制文件,不依赖于外部操作系统。

通过设置以下环境变量,在本地工作站上测试新的 compressor 应用;用本章前面定义的值替换端点、用户名和密码:

$ export ENDPOINT=minio.data.dev5.apk8s.dev
$ export ACCESS_KEY_ID=username
$ export ACCESS_KEY_SECRET=password


编译并执行配置了桶`upload`和`processed`的 compressor 应用,以及前面部分生成并添加到 MinIO 的对象`upload/donors.csv`:

$ go run ./cmd/compressor.go -f upload -k donors.csv -t processed


在完成执行后,一个新对象包含一个名为`donors.csv.gz`的`donors.csv`的压缩版本,在`processed`桶中可用,大约 20 兆字节。此外,MinIO 向 MQTT 集群发送了一条主题为`processed`的消息,并向 Elasticsearch 索引`processed`添加了一个新文档。参见第 5 和 6 章,了解 MQTT (Mosquitto)和 Elasticsearch。

接下来,本章使用 Docker Hub 组织`apk8s`来存储和服务公共容器。为下面的练习设置一个免费的 Docker Hub 帐户或确保访问合适的容器注册表。

在代表 compressor 项目的文件夹`~/workspace/apk8s/compressor`中,创建文件`Dockerfile`并用清单 7-7 的内容填充它。

FROM golang:1.13.3 AS builder

WORKDIR /go/src

COPY . /go/src

RUN go mod download

RUN CGO_ENABLED=0
GOOS=linux
GOARCH=amd64
GO111MODULE=on
go build -ldflags "-extldflags -static"
-o /go/bin/compressor /go/src/cmd

RUN echo "nobody❌65534:65534:Nobody:/:" > /etc_passwd

FROM scratch

ENV PATH=/bin

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc_passwd /etc/passwd
COPY --from=builder /go/bin/compressor /bin/compressor

WORKDIR /

USER nobody
ENTRYPOINT ["/bin/compressor"]

Listing 7-7Dockerfile for the compressor application


新的 Dockerfile 代表了一个多阶段的构建过程。第一阶段使用名为`builder`的`golang:1.13.3`容器下载依赖项并编译静态二进制文件。构建器容器还创建了一个`/etc_passwd`文件来支持用户 Nobody 执行下面的暂存容器中的二进制文件,这被定义为 Docker 构建的第二阶段。

除了是一个很小的容器,重约 10 兆字节,这个基于`scratch`的容器(列表 7-7 )以用户`Nobody`的身份执行单个二进制文件,没有其他用户帐户,也没有操作系统。这种方法将攻击面缩小到二进制文件本身,以及用于编译它的库。允许攻击者逃离容器的利用将作为相对良性的用户`Nobody`来完成。没有完美的安全;然而,任何潜在攻击面和复杂性的减少都会减少在生产系统中运行数十万个容器的累积效应。

最后,构建、标记容器,并将其推送到 Docker Hub 或任何合适的容器注册中心:

$ docker build -t apk8s/compressor:v0.0.1 .
$ docker push apk8s/compressor:v0.0.1


测试新容器,首先确保环境变量`ENDPOINT`、`ACCESS_KEY_ID`和`ACCESS_KEY_SECRET`仍然在当前 shell 中导出(如前面所定义的),然后执行以下命令:

$ docker run -e ENDPOINT=\(ENDPOINT \ -e ACCESS_KEY_ID=\)ACCESS_KEY_ID
-e ACCESS_KEY_SECRET=$ACCESS_KEY_SECRET
apk8s/compressor:v0.0.1
-f=upload -k=donors.csv -t=processed


新容器已经准备好在 Kubernetes 集群中运行并处理对象,在本例中是压缩它们。然而,本练习的目标是展示开发小型、集中的应用的简易性,以及它们在基于 Kubernetes 的数据平台中所扮演的角色。下一节将演示如何构建这个(或任何)新对象处理容器的基于事件的部署的原型。

#### 程序化部署

计算集群所需状态的声明性配置是在 Kubernetes 中部署应用的惯用方法。这本书主要使用 YAML 直接定义理想状态,或者使用 Helm 间接填充和应用 YAML 模板。甚至命令式命令如`kubectl run` <sup>12</sup> 也在幕后调用声明式 API 调用。本节中的练习在 Jupyter 笔记本中开发了一个原型应用,它侦听 MinIO bucket 事件,并通过定义 Kubernetes 作业和用 Kubernetes API 应用它们来处理对象。

在将任何执行附加到事件流之前,下面的练习使用由 JupyterHub 提供的 JupyterLab 环境提供的集群内 Jupyter 笔记本来开发一个原型 Kubernetes 作业部署器。

为 Pod 中的容器指定 serviceAccount 时,Kubernetes 会将其关联的访问令牌以及集群证书 ca.crt 装入/var/run/secrets/Kubernetes . io/serviceaccount 中。未指定 service account 的 Pod 会装入运行它们的命名空间的默认服务帐户。第六章配置了一个名为 data-lab 的服务帐户和角色,它拥有对 Kubernetes API 的扩展权限,供 JupyterLab 容器使用。确保以下权限、 **pods/log** 、 **jobs** 和 **jobs/status** (如清单 7-8 所示)可用于分配给`data-lab`名称空间中`data-lab` ServiceAccount 的`data-lab`角色。

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: data-lab
namespace: data-lab
rules:

  • apiGroups: ["","batch"]
    resources: ["pods",
    "pods/log",
    "events",
    "services",
    "jobs",
    "jobs/status"]
    verbs: ["delete",
    "create",
    "get",
    "watch",
    "list",
    "endpoints",
    "patch",
    "events"]

Listing 7-8Update 005-data-lab/000-namespace/07-role.yml


参观在第六章中设置的木星实验室环境`lab.data.dev5.apk8s.dev`。在单个单元格中添加并执行以下代码示例,以测试和试验每个步骤。

添加一个单元格安装最新的 Python Kubernetes 客户端库: <sup>13</sup>

!pip install kubernetes


导入以下依赖项:

from os import path
import yaml
import time
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from IPython.display import clear_output


使用类`V1EnvVar` <sup>14</sup> 创建一个环境变量集合,供以后在压缩器容器中使用。用本章前面“MinIO 集群”一节中定义的值替换用户名和密码。当前集群内配置的`ENDPOINT_SSL`设置为假:

envs = [
client.V1EnvVar("ENDPOINT", "minio-internal-service.data:9000"),
client.V1EnvVar("ACCESS_KEY_ID", "username"),
client.V1EnvVar("ACCESS_KEY_SECRET", "password"),
client.V1EnvVar("ENDPOINT_SSL", "false"),
]


使用类`V1Container,` <sup>15</sup> 配置一个容器,其中包含在前面部分构建并标记的映像`apk8s/compressor:v0.0.1`。compressor 应用需要之前配置的环境变量,以及指定要处理的桶的参数`-f`、对象键的参数`-k`和目的桶的参数`-t`:

container = client.V1Container(
name="compressor",
image="apk8s/compressor:v0.0.1",
env=envs,
args=["-f=upload",
"-k=donors.csv",
"-t=processed"])


使用类`V1PodTemplateSpec,`<sup>16</sup>`V1ObjectMeta,`<sup>17</sup>和`V1PodSpec,` <sup>18</sup> 配置一个 Pod 来运行之前配置的容器:

podTmpl = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
labels={"app": "compress-donors"}
),
spec=client.V1PodSpec(
restart_policy="Never",
containers=[container]))


Kubernetes 为 Pods 提供作业资源,旨在运行到完成。只有当 Pod 返回非零值时,作业才会重新启动它。使用类别`V1Job,` <sup>19</sup> `V1ObjectMeta`和`V1JobSpec,` <sup>20</sup> 用之前配置的 Pod 配置作业:

job = client.V1Job(
api_version="batch/v1",
kind="Job",
metadata=client.V1ObjectMeta(
name="compress-donors"
),
spec=client.V1JobSpec(
template=podTmpl,
backoff_limit=2)
)


接下来,配置 Python 客户机并加载负责管理作业资源的 Kubernetes 批处理 API。方法`load_incluster_config()`将 Python 客户端配置为利用 Kubernetes 配置,该配置可用于运行当前 JupyterLab 环境的 Pod。

config.load_incluster_config()
batch_v1 = client.BatchV1Api()


使用 Kubernetes 批处理 API 来应用作业配置:

resp = batch_v1.create_namespaced_job(
body=job,
namespace="data-lab")


在执行了前面 Jupyter 笔记本单元中的代码后,Kubernetes 在 data-lab 名称空间中创建了一个名为 compress-donors 的作业。成功执行后,作业保持完成状态。但是,任何失败都会导致两次重试,如前面使用`backoff_limit`指令配置的那样。在这两种情况下,清单 7-9 中的代码轮询已配置作业的状态,并在完成后删除它;如果作业返回错误,最终代码将显示与作业关联的 Pod 中的日志。将清单 7-9 中的代码添加到 Jupyter 笔记本的最后一个单元格中,并执行它。

completed = False

while completed == False:
time.sleep(1)

try:
    resp = batch_v1.read_namespaced_job_status(
        name="compress-donors",
        namespace="data-lab", pretty=False)
except ApiException as e:
    print(e.reason)
    break

clear_output(True)
print(resp.status)

if resp.status.conditions is None:
    continue

if len(resp.status.conditions) > 0:

    clear_output(True)
    print(resp.status.conditions)

    if resp.status.conditions[0].type == "Failed":
        print("FAILED -- Pod Log --")
        core_v1 = client.CoreV1Api()
        pod_resp = core_v1.list_namespaced_pod(
            namespace="data-lab",
            label_selector="app=compress-donors",
            limit=1
        )

        log_resp = core_v1.read_namespaced_pod_log(
            name=pod_resp.items[0].metadata.name,
            namespace='data-lab')

        print(log_resp)

    print("Removing Job...")
    resp = batch_v1.delete_namespaced_job(
        name="compress-donors",
        namespace="data-lab",
        body=client.V1DeleteOptions(
            propagation_policy='Foreground',
            grace_period_seconds=5))
    break

Listing 7-9Monitor Job state and cleanup


这本书把剩下的基于事件的对象处理练习留给了读者。将本章前面定义的事件记录单与前面定义的用于处理对象的作业的编程部署结合起来。这种组合为几乎任何形式的基于事件的数据处理创建了一个原型框架,包括分析、标准化、衍生和合成数据的生成,以及内务处理任务,如前面演示的组织和压缩。

#### 无服务器对象处理

自动化前一部分开发的流程是这里简要介绍的无服务器 <sup>21</sup> (或功能即服务)技术的核心。

云供应商提供的解决方案抽象出了所有复杂的活动部分,这些部分支持任何孤立工作负载的编译、执行、缩放和错误处理,这些工作负载足够小且足够集中,可以被视为一个功能。使用 AWS Lambda、 <sup>22</sup> Azure 函数、 <sup>23</sup> 或谷歌云函数 <sup>24</sup> 构建关键业务逻辑是快速开发或寻求尽可能减少运营复杂性的小型团队的有效方法。然而,这些无服务器产品确实是以强大的供应商锁定为代价的,并且相信它们的支持产品线在未来仍然是供应商的优先选择。

在 Kubernetes 生态系统中, <sup>25</sup> 越来越多的无服务器技术提供了云本地和厂商中立的方法。Kubeless、 <sup>26</sup> OpenFaaS、 <sup>27</sup> 裂变、<sup>28</sup>Apache open whisk、 <sup>29</sup> 和 Nuclio <sup>30</sup> 都提供了非常适合公共云或定制 Kubernetes 集群的交钥匙无服务器平台。项目 Knative <sup>31</sup> 为开发新的基于无服务器的平台提供了一个灵活的组件平台。第八章展示了使用 OpenFaaS 进行无服务器风格的数据转换。

由于 Kubernetes 抽象了底层基础设施问题,MinIO 提供了对象存储和访问,再加上一个强大的无服务器平台来自动化部署和执行基于事件的对象处理,本书中描述的数据湖在为生产进行扩展时能够满足许多企业需求。

## 摘要

本章组装了一种类型的数据湖,具体来说就是上传桶,负责摄取任何形式和数量的数据 <sup>32</sup> ,具有几乎无限扩展的能力。数据湖满足了任何组织在确定任何特定目的之前获取或保留数据的愿望。数据湖不仅仅是简单的数据存储引擎;它们必须支持为各种利益相关者提供受控访问的能力,支持用于探索和分析的工具,并在需要时扩展处理能力和容量。MinIO 是在 Kubernetes 中构建数据湖的绝佳选择。MinIO 的 S3 兼容 API,以及支持各种外部系统的事件通知系统,为构建现代数据湖提供了一个广泛而强大的平台。 <sup>33</sup> 此外,本章还展示了利用集群内 Jupyter 笔记本电脑快速构建基于事件的对象处理原型的强大功能和便利性。

一旦组织、处理或转换了数据湖中的任何数据,就出现了数据仓库的概念。数据仓库是商业智能、分析和数据科学活动(包括机器学习)的关键组件。下一章用数据仓库概念叠加并扩展了本章中的功能。

<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1

[`https://github.com/operator-framework`](https://github.com/operator-framework)

  2

[`https://operatorhub.io/`](https://operatorhub.io/)

  3

[`https://kubernetes.io/docs/concepts/extend-kubernetes/operator/`](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)

  4

[`https://docs.min.io/docs/minio-client-complete-guide`](https://docs.min.io/docs/minio-client-complete-guide)

  5

[`www.amqp.org/`](http://www.amqp.org/)

  6

[`https://nats.io/`](https://nats.io/)

  7

[`https://nsq.io/`](https://nsq.io/)

  8

[`www.programmableweb.com/news/what-are-webhooks-and-how-do-they-enable-real-time-web/2012/01/30`](http://www.programmableweb.com/news/what-are-webhooks-and-how-do-they-enable-real-time-web/2012/01/30)

  9

[`https://kafka-python.readthedocs.io/en/master/apidoc/KafkaConsumer.html`](https://kafka-python.readthedocs.io/en/master/apidoc/KafkaConsumer.html)

  10

[`https://github.com/golang/go/wiki/Modules`](https://github.com/golang/go/wiki/Modules)

  11

[`https://golang.org/doc/go1.14`](https://golang.org/doc/go1.14)

  12

[`https://kubernetes.io/docs/tasks/manage-kubernetes-objects/imperative-command/`](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/imperative-command/)

  13

[`https://github.com/kubernetes-client/python`](https://github.com/kubernetes-client/python)

  14

[`https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1EnvVar.md`](https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1EnvVar.md)

  15

[`https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Container.md`](https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Container.md)

  16

[`https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1PodTemplateSpec.md`](https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1PodTemplateSpec.md)

  17

[`https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ObjectMeta.md`](https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ObjectMeta.md)

  18

[`https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1PodSpec.md`](https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1PodSpec.md)

  19

[`https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Job.md`](https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Job.md)

  20

[`https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1JobSpec.md`](https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1JobSpec.md)

  21

[`https://martinfowler.com/articles/serverless.html`](https://martinfowler.com/articles/serverless.html)

  22

[`https://aws.amazon.com/lambda/`](https://aws.amazon.com/lambda/)

  23

[`https://azure.microsoft.com/en-us/services/functions/`](https://azure.microsoft.com/en-us/services/functions/)

  24

[`https://cloud.google.com/functions/`](https://cloud.google.com/functions/)

  25

[`https://landscape.cncf.io/format=serverless`](https://landscape.cncf.io/format%253Dserverless)

  26

[`https://kubeless.io/`](https://kubeless.io/)

  27

[`www.openfaas.com/`](http://www.openfaas.com/)

  28

[`https://fission.io/`](https://fission.io/)

  29

[`https://openwhisk.apache.org/`](https://openwhisk.apache.org/)

  30

[`https://nuclio.io/`](https://nuclio.io/)

  31

[`https://knative.dev/`](https://knative.dev/)

  32

[`www.dataversity.net/data-lakes-101-overview/`](http://www.dataversity.net/data-lakes-101-overview/)

  33

[`https://blog.minio.io/modern-data-lake-with-minio-part-1-716a49499533`](https://blog.minio.io/modern-data-lake-with-minio-part-1-716a49499533)

 </aside>

# 八、数据仓库

关于数据仓库的这一章是上一章的延伸,上一章讲述了使用分布式对象存储系统 MinIO 开发现代数据湖。数据湖存储各种各样的数据形式,而数据仓库管理各种各样的数据源。数据仓库提供对数据目录、元数据、索引、键/值存储、消息队列、事件流、文档和关系数据库(包括数据湖)的访问。数据湖和数据仓库之间的界限并不总是很清楚;这本书将数据仓库的概念区分为任何包含数据的源的管理集合,这些数据是经过处理、组织、索引、编目或以其他方式有目的地标识的。

开源数据湖管理系统,如 Delta Lake <sup>1</sup> 带来了 ACID 事务、元数据、统一流和批量数据,而 Kylo <sup>2</sup> 则提供了一个健壮的用户界面,用于数据摄取、准备和发现。这些复杂的数据湖应用开始模糊一个巨大的、无形式的数据湖和组织良好的数据仓库之间的界限。然而,这些系统的结果很可能是更高层次的数据仓库概念的候选者。

数据湖在收集数据时是不加选择的;当组织获取任何类型的数据时,在使用数据的业务案例出现之前,可能会出现存储数据的需求。当了解了一组数据的价值和用途后,就可以对其进行处理、开发模式、编制属性索引、对值进行规范化,并对元数据进行编目,以供感兴趣的服务或人类分析师使用。数据仓库提供对实时事件和消息数据以及历史数据集合的访问,为决策支持系统、商业智能、分析、机器学习和推理做好准备。

## 数据和数据科学

与支持数据科学活动相关的概念非常广泛,此外还有大量技术和实现,专注于由人类或机器提供数据驱动的决策。数据平台促进了数据的接收、访问和管理,在其中,数据仓库提供了数据源、模式和元数据的目录。

在数据科学领域,机器学习活动通常会对数据提出一系列特别苛刻的要求。来自谷歌的十名研究人员撰写了一篇题为“机器学习系统中隐藏的技术债务”的研究论文。 <sup>3 支持机器学习的基础设施包括配置、数据收集、特征提取、数据验证、机器资源管理、分析工具、过程管理工具、服务基础设施和监控(见图 8-1 )。此外,许多其他数据科学活动需要大量这种资源,包括商业智能和分析。</sup>

![img/483120_1_En_8_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig1_HTML.jpg)

图 8-1

机器学习基础设施 <sup>3</sup>

下一节将介绍一个适中但功能强大的现代数据仓库的基础,它能够提供大多数数据科学活动(包括机器学习)所需的大部分(如果不是全部)功能。

### 数据平台

在 Kubernetes 中构建一个现代化的数据仓库提供了一个抽象的底层基础设施、一个统一的控制面板、标准化的配置管理、整体监控、基于角色的访问控制、网络策略,以及与云原生技术的快速发展前景 <sup>4</sup> 的兼容性。

本章安装并配置了三个新的数据源:MySQL <sup>5</sup> 集群,代表一个公共的 RDBMS 数据库;Apache Cassandra<sup>6</sup>作为一个宽列分布式 NoSQL 数据库;Apache Hive<sup>7</sup>能够在上一章设置的 S3 兼容对象存储之上投影一个模式。很快, <sup>8</sup> 一个针对大数据的分布式 SQL 查询引擎将这些现有的数据源绑定到一个目录中,提供模式和连接。Presto 原生支持 20 多种典型的数据应用,包括 Elasticsearch(在第六章配置)和 Apache Kafka(在第五章配置)。

编写一个本地连接并使用来自多个源的数据的应用并不罕见。但是,Presto 等技术整合和抽象了这种能力,将查询分布到集群中的工作人员,汇总结果,并监控性能。通过管理不同的连接要求和模式管理,集中访问 Presto 的巨大数据仓库减少了跨专业系统的技术债务(见图 8-2 )。

![img/483120_1_En_8_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig2_HTML.jpg)

图 8-2

Presto 分布式 SQL 连接多个数据源

## 发展环境

以下练习继续利用前面章节中提到的小型集群,包括一个用于 Kubernetes 主节点的 2 vCPU/8G RAM/40G SSD 和四个用于 Kubernetes 工作节点的 4 vCPU/16G RAM/160G SSD 实例。本章中的概念和配置是按比例缩小的,以适应这个经济的实验和开发集群。

本章利用第 3 、 5 和 6 章中定义的应用和集群配置以及第七章中的新 MinIO 集群(参见表 [7-1](https://doi.org/10.1007/978-1-4842-5611-4_7Tab#1) )。本章继续在文件夹`cluster-apk8s-dev5`下组织配置清单。

## 数据和元数据源

数据仓库不仅提供对来自各种来源的历史数据的访问,而且还帮助获取、开发和描述数据集。本节将安装 MySQL,稍后 Apache Hive 将使用它来存储元数据,本书将通过在 MinIO 中的对象上投影模式来演示。此外,本章还安装了 Apache Cassandra 来进一步演示 Kubernetes 中各种数据管理系统的操作,以及利用数据仓库概念的组合效应。

### 关系型数据库

MySQL 是一个非常流行的数据库。根据 2019 年 Stack Overflow 开发者调查,54%的受访者使用 MySQL。这些结果并不令人惊讶,每天都有成千上万的网站由 MySQL 支持的内容管理系统驱动,如 WordPress 和 Drupal。 <sup>10</sup> WordPress 声称它为 35%的互联网提供动力;如果这种说法是准确的,那么可以有把握地假设所有在线数据(以网站内容表示)的很大一部分是由 MySQL 数据库提供的。

数据仓库应用 Apache Hive 需要一个数据库来存储和管理元数据。Hive 可以使用 MySQL(以及许多其他数据库)来存储元数据。对 MySQL 的平台支持既可以作为可能的数据源,也可以作为数据仓库的功能依赖(见图 8-2 )。

MySQL 以及大多数传统的数据库系统都出现在 Kubernetes 甚至云原生概念之前。用于配置和维护有状态数据库工作负载的挑战性需求的交钥匙解决方案正在快速发展。本节安装一个 MySQL Kubernetes 操作符,并定义新 MySQL 集群的期望状态。

#### MySQL 运算符

本节配置由 Presslabs 提供的稳定的、有良好文档记录的、积极维护的 MySQL Kubernetes 操作符。<sup>12</sup>MySQL 操作员定义了一个新的自定义资源,由自定义资源定义(CRD) `MysqlCluster`表示。

如果按照前面的章节,本书开发的平台在第三章定义和管理资源`CephCluster`中设置了 Rook Ceph 操作符,在第七章定义和管理资源`Tenant`中设置了 MinIO 操作符。像以前的操作符一样,新的 MySQL 操作符扩展了 Kubernetes,因此赋予了本书中开发的平台在一个或多个名称空间中快速部署和管理一个或多个 MySQL 集群的能力。

下面的配置将 MySQL 操作符安装在新的名称空间`mysql-operator`中,并且可以管理在任何名称空间中创建的`MysqlCluster`资源。在集群级别组织命名空间配置和安装文档。创建目录`cluster-apk8s-dev5/000-cluster/25-mysql-operator`来包含 MySQL 操作符名称空间配置和文档。接下来,用清单 8-1 中的内容创建一个名为`00-namespace.yml`的文件。此外,创建一个名为`README.md`的文件来记录下一步执行的 Helm 命令。

apiVersion: v1
kind: Namespace
metadata:
name: mysql-operator

Listing 8-1MySQL operator Namespace


应用 MySQL 操作符名称空间配置:

$ kubectl apply -f 00-namespace.yml


接下来,将 Presslabs 图表存储库添加到 Helm:

$ helm repo add presslabs
https://presslabs.github.io/charts


最后,安装 mysql-operator。配置操作员使用第三章中设置的 rook-ceph-block 存储类别:

$ helm install presslabs/mysql-operator
--set orchestrator.persistence.storageClass=rook-ceph-block
--name mysql-operator
--namespace mysql-operator


新的 mysql-operator 准备安装和管理 MysqlCluster 资源中定义的 mysql 集群,下一节将对此进行描述。此外,mysql-operator 将 GitHub 的 MySQL Orchestrator 公开为 mysql-operator 名称空间中的服务 mysql-operator:80。Orchestrator 是一个 MySQL 管理和可视化工具,提供拓扑发现、重构和恢复功能。 <sup>十三</sup>

通过端口转发服务并在浏览器中访问它来访问 Orchestrator:

$ kubectl port-forward service/mysql-operator
8080:80 -n mysql-operator


#### MySQL 集群

下一节定义了一个小型的双节点 MySQL 集群,在名称空间`data`中命名为`mysql`。最初,本章使用 MySQL 作为 Apache Hive 的元数据后端,后来使用 Presto 演示不同数据源之间的复杂连接。

创建目录`cluster-apk8s-dev5/003-data/080-mysql`来包含 MySQL 集群配置。接下来,从清单 8-2 中创建一个名为`90-cluster.yml`的文件。

apiVersion: v1
kind: Secret
metadata:
name: mysql-credentials
namespace: data
type: Opaque
stringData:
ROOT_PASSWORD: strongpassword
USER: hive
PASSWORD: strongpassword
DATABASE: objectmetastore

apiVersion: mysql.presslabs.org/v1alpha1
kind: MysqlCluster
metadata:
name: mysql
namespace: data
spec:
replicas: 2
secretName: mysql-credentials
volumeSpec:
persistentVolumeClaim:
accessModes: ["ReadWriteOnce"]
storageClassName: "rook-ceph-block"
resources:
requests:
storage: 1Gi

Listing 8-2MySQL cluster configuration


应用 MySQL 集群配置:

$ kubectl apply -f 90-cluster.yml


### Apache 卡桑德拉

Apache Cassandra 是一个高性能、高可用性的宽列数据库。将 Cassandra 添加到本书描述的数据平台中,通过提供网络规模的数据库解决方案,完善了数据访问和存储能力。网飞是 Cassandra 更著名和公开的用户之一,“网飞在 AWS 上使用 Cassandra,作为其全球分布式流媒体产品的关键基础设施组件。”网飞公布的基准测试显示每秒超过 100 万次写入。

Cassandra 的点对点设计意味着没有主节点可以压倒或超越,从而允许 Cassandra 在数据量和速度方面进行线性扩展。这些特征提供了将大量大数据需求与高速宽列存储数据相结合的功能。

下一节配置用于在 Kubernetes 中准备和管理一个或多个 Cassandra 集群的 Rook Cassandra 操作符。

#### 卡珊德拉算子

Rook <sup>14</sup> 是一家为 Kubernetes 提供数据存储运营商的知名供应商。这本书在第三章介绍了 Rook Ceph 操作符。Rook Cassandra 操作符在 API 名称空间`cassandra.rook.io`下用新的定制资源定义`Cluster`扩展了 Kubernetes。

Rook Cassandra 操作符 <sup>15</sup> 支持集群设置,允许指定所需的 Cassandra 版本、容器映像库、注释,以及使用 Scylla、 <sup>16</sup> 的选项,这是一种用 C++编写的 Cassandra 兼容的替代方案。Scylla 声称每个节点的性能达到 1,000,000 次操作,并且能够扩展到数百个节点,99%的延迟不到 1 毫秒。Rook 与 Scylla 和 Cassandra 数据库的兼容性有助于试验这两种解决方案。此外,Rook Cassandra 操作员支持 Cassandra 物理拓扑的定义,从而能够为每个集群指定数据中心和机架配置。

创建目录`cluster-apk8s-dev5/000-cluster/23-rook-cassandra`来包含车卡珊德拉操作员配置。接下来,从清单 8-3 中创建一个名为`00-operator.yml`的文件。

apiVersion: v1
kind: Namespace
metadata:
name: rook-cassandra-system

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: clusters.cassandra.rook.io
spec:
group: cassandra.rook.io
names:
kind: Cluster
listKind: ClusterList
plural: clusters
singular: cluster
scope: Namespaced
version: v1alpha1
validation:
openAPIV3Schema:
properties:
spec:
type: object
properties:
version:
type: string
description: "Version of Cassandra"
datacenter:
type: object
properties:
name:
type: string
description: "Datacenter Name"
racks:
type: array
properties:
name:
type: string
members:
type: integer
configMapName:
type: string
storage:
type: object
properties:
volumeClaimTemplates:
type: object
required:
- "volumeClaimTemplates"
placement:
type: object
resources:

                  type: object
                  properties:
                    cassandra:
                      type: object
                    sidecar:
                      type: object
                  required:
                    - "cassandra"
                    - "sidecar"
                sidecarImage:
                  type: object
              required:
                - "name"
                - "members"
                - "storage"
                - "resources"
          required:
            - "name"
      required:
        - "version"
        - "datacenter"

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole

metadata:
name: rook-cassandra-operator
rules:

  • apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch", "delete"]
  • apiGroups: [""]
    resources: ["services"]
    verbs: ["*"]
  • apiGroups: [""]
    resources: ["persistentvolumes", "persistentvolumeclaims"]
    verbs: ["get", "delete"]
  • apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get"]
  • apiGroups: ["apps"]
    resources: ["statefulsets"]
    verbs: ["*"]
  • apiGroups: ["policy"]
    resources: ["poddisruptionbudgets"]
    verbs: ["create"]
  • apiGroups: ["cassandra.rook.io"]
    resources: [""]
    verbs: ["
    "]
  • apiGroups: [""]
    resources: ["events"]
    verbs: ["create","update","patch"]

apiVersion: v1
kind: ServiceAccount
metadata:
name: rook-cassandra-operator
namespace: rook-cassandra-system

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-cassandra-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: rook-cassandra-operator
subjects:

  • kind: ServiceAccount
    name: rook-cassandra-operator
    namespace: rook-cassandra-system

apiVersion: apps/v1
kind: StatefulSet

metadata:
name: rook-cassandra-operator
namespace: rook-cassandra-system
labels:
app: rook-cassandra-operator
spec:
replicas: 1
serviceName: "non-existent-service"
selector:
matchLabels:
app: rook-cassandra-operator
template:
metadata:
labels:
app: rook-cassandra-operator
spec:
serviceAccountName: rook-cassandra-operator
containers:
- name: rook-cassandra-operator
image: rook/cassandra:v1.1.2
imagePullPolicy: "Always"
args: ["cassandra", "operator"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace

Listing 8-3Rook Cassandra operator


应用 Cassandra 运算符配置:

$ kubectl apply -f 00-operator.yml


#### 卡桑德拉星团

本节创建一个三节点 Apache Cassandra 集群,稍后用于演示 web 级宽列存储数据库和大数据系统的强大聚合,这些大数据系统用 Presto 分布式 SQL 查询引擎表示。下面的配置在数据命名空间中定义了 Kubernetes 角色、ServiceAccount 和 RoleBinding,以便在 Cassandra 集群中使用。Cassandra 集群名为 apk8s,配置在一个名为`r1`的虚拟机架中。更广泛和复杂的集群应该通过设置`nodeAffinity`、`podAffinity`、`podAntiAffinity`和`tolerations`的位置值来定义分布在 Kubernetes 集群内的多个数据中心和机架。

创建目录`cluster-apk8s-dev5/003-data/060-cassandra`来包含 Cassandra 集群配置。接下来,从清单 8-4 中创建一个名为`15-rbac.yml`的文件。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cassandra-member
namespace: data
rules:

  • apiGroups: [""]
    resources: ["pods"]
    verbs: ["get"]
  • apiGroups: [""]
    resources: ["services"]
    verbs: ["get","list","patch","watch"]
  • apiGroups: ["cassandra.rook.io"]
    resources: ["clusters"]
    verbs: ["get"]

apiVersion: v1
kind: ServiceAccount
metadata:
name: cassandra-member
namespace: data

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cassandra-member
namespace: data
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cassandra-member
subjects:

  • kind: ServiceAccount
    name: cassandra-member
    namespace: data

Listing 8-4Rook Cassandra RBAC configuration


应用 Cassandra 集群 RBAC 配置:

$ kubectl apply -f 15-rbac.yml


接下来定义的小型 Cassandra 集群被限制在一个虚拟机架上,没有指定的 Kubernetes 节点、关联或容差。以下配置将 Cassandra 集群中的每个节点限制为一个 CPU 和 2gb 内存。

从清单 8-5 中创建一个名为`90-cluster.yml`的文件。

apiVersion: cassandra.rook.io/v1alpha1
kind: Cluster
metadata:
name: cassandra
namespace: data
spec:
version: 3.11.1
mode: cassandra
datacenter:
name: apk8s
racks:
- name: r1
members: 3
storage:
volumeClaimTemplates:
- metadata:
name: cassandra-data
spec:
storageClassName: rook-ceph-block
resources:
requests:
storage: 5Gi
resources:
requests:
cpu: 1
memory: 2Gi
limits:
cpu: 1
memory: 2Gi

Listing 8-5Rook Cassandra cluster configuration


应用 Cassandra 集群配置:

$ kubectl apply -f 90-cluster.yml


### Apache 蜂巢

Apache Hive 是最初由脸书开发的数据仓库软件,后来交给了 Apache 软件基金会。网飞 <sup>17</sup> 和 FINRA <sup>18</sup> 等组织使用 Hive 跨分布式存储系统查询海量结构化数据,包括 Hadoop 的 HDFS 和亚马逊 S3。Hive 通过提供标准的 SQL 接口,简化了查询大数据通常需要的复杂 MapReduce 作业。虽然 Hive 不是一个数据库,但它提供了将模式投影到存储在 HDFS 或 S3 兼容存储中的任何结构化数据上的能力。亚马逊的 AWS 提供产品 Elastic MapReduce,包括一个版本的 Hive as a service。 <sup>19</sup>

Apache Hive 使组织能够利用大量不受正式数据库管理系统管理的结构化数据、稳定的物联网数据流、从遗留系统导出的数据以及临时数据摄取。Apache Hive 通过在巨大的数据湖中提供 SQL 接口、元数据和模式,降低了执行数据科学活动的复杂性和工作负载,包括业务分析、业务智能和机器学习。

#### 容器化

本节创建一个定制的 Apache Hive 容器,该容器被配置为使用 MySQL 来存储与驻留在 S3 兼容的分布式存储系统中的对象相关的模式和元数据,例如在第七章中配置的 MinIO 集群。与许多大数据应用一样,Apache Hive 是在 Cloud-Native 和 Kubernetes 生态系统之外发展起来的,因此需要更多的努力才能将其纳入集群。下面从构建一个适用于 Kubernetes 和本地实验的定制容器开始。

创建目录`apk8s-hive`来包含新 Apache Hive Docker 容器的必要组件和配置。 <sup>20</sup> 接下来,创建目录`src`,下载并解压缩 Apache Hive 及其主依赖 Apache Hadoop:

$ mkdir -p apk8s-hive/src
$ cd apk8s-hive

$ curl -L http://mirror.cc.columbia.edu/pub/software/apache/hive/hive-3.1.2/apache-hive-3.1.2-bin.tar.gz -o ./src/apache-hive-3.1.2-bin.tar.gz

$ curl -L http://archive.apache.org/dist/hadoop/common/hadoop-3.1.2/hadoop-3.1.2.tar.gz -o ./src/hadoop-3.1.2.tar.gz

$ tar -xzvf ./src/apache-hive-3.1.2-bin.tar.gz -C ./src
$ tar -xzvf ./src/hadoop-3.1.2.tar.gz -C ./src


接下来,通过添加 JAR 文件来扩展 Apache Hive 的功能,JAR 文件包含连接到 S3 兼容的对象存储和 MySQL 进行模式和元数据管理所需的功能:

$ export HIVE_LIB=\((pwd)/src/apache-hive-3.1.2-bin/lib \) export MIRROR=https://repo1.maven.org/maven2

$ curl $MIRROR/org/apache/hadoop/hadoop-aws/3.1.1/hadoop-aws-3.1.1.jar -o $HIVE_LIB/hadoop-aws-3.1.1.jar

$ curl $MIRROR/com/amazonaws/aws-java-sdk/1.11.406/aws-java-sdk-1.11.307.jar -o $HIVE_LIB/aws-java-sdk-1.11.307.jar

$ curl $MIRROR/com/amazonaws/aws-java-sdk-core/1.11.307/aws-java-sdk-core-1.11.307.jar -o $HIVE_LIB/aws-java-sdk-core-1.11.307.jar

$ curl $MIRROR/com/amazonaws/aws-java-sdk-dynamodb/1.11.307/aws-java-sdk-dynamodb-1.11.307.jar -o $HIVE_LIB/aws-java-sdk-dynamodb-1.11.307.jar

$ curl $MIRROR/com/amazonaws/aws-java-sdk-kms/1.11.307/aws-java-sdk-kms-1.11.307.jar -o $HIVE_LIB/aws-java-sdk-kms-1.11.307.jar

$ curl $MIRROR/com/amazonaws/aws-java-sdk-s3/1.11.307/aws-java-sdk-s3-1.11.307.jar -o $HIVE_LIB/aws-java-sdk-s3-1.11.307.jar

$ curl $MIRROR/org/apache/httpcomponents/httpclient/4.5.3/httpclient-4.5.3.jar -o $HIVE_LIB/httpclient-4.5.3.jar

$ curl $MIRROR/joda-time/joda-time/2.9.9/joda-time-2.9.9.jar -o $HIVE_LIB/joda-time-2.9.9.jar

$ curl $MIRROR/mysql/mysql-connector-java/5.1.48/mysql-connector-java-5.1.48.jar -o $HIVE_LIB/mysql-connector-java-5.1.48.jar


与许多基于 Java 的应用一样,Hive 使用 XML 文件进行配置,在本例中是 hive-site.xml。但是,将包含敏感身份验证令牌、密码和特定于环境的服务位置的配置值打包是一种反模式,会导致安全问题并限制容器的可重用性。从文件系统挂载配置文件(或者在 Kubernetes 的情况下挂载 ConfigMaps)是配置容器的标准方法,为使用容器的管理员或开发人员提供了相当大的灵活性;但是,这种方法限制了利用 Kubernetes 中现有的 Secrets 和 ConfigMap 值的能力。本节描述的技术创建了一个配置文件模板,该模板将由容器在运行时用环境变量填充。

用清单 8-6 中的内容创建一个名为`hive-site-template.xml`的文件。

javax.jdo.option.ConnectionURL jdbc:mysql://MYSQL_ENDPOINT/objectmetastore?createDatabaseIfNotExist=true&useSSL=false javax.jdo.option.ConnectionDriverName com.mysql.jdbc.Driver javax.jdo.option.ConnectionUserName MYSQL_USER javax.jdo.option.ConnectionPassword MYSQL_PASSWORD fs.s3a.endpoint S3A_ENDPOINT fs.s3a.access.key S3A_ACCESS_KEY fs.s3a.secret.key S3A_SECRET_KEY fs.s3a.path.style.access S3A_PATH_STYLE_ACCESS

Listing 8-6Apache Hive configuration template hive-site-template.xml


创建一个名为`entrypoint.sh`的 shell 脚本作为容器的初始进程。入口点脚本使用 sed 将`hive-site.xml`配置文件中的值替换为通过容器运行时传递的环境变量中的值,这在前面的部分中进行了定义。应用配置后,脚本运行实用程序`schematool`来添加任何 MySQL 数据库和 Hive 存储模式和元数据所需的表。最后,入口点脚本启动一个 Hive 服务器和一个 Hive Metastore <sup>21</sup> 服务器。

创建一个名为`entrypoint.sh`的 Bash <sup>22</sup> 脚本,将清单 8-7 中的内容用作新容器的入口点。

!/bin/bash

provide ample time for other services to come online

sleep 10

configuration file location

HIVE_CONF="/opt/hive/conf/hive-site.xml"

template replacements

for v in
MYSQL_ENDPOINT
MYSQL_USER
MYSQL_PASSWORD
S3A_ENDPOINT
S3A_ACCESS_KEY
S3A_SECRET_KEY
S3A_PATH_STYLE_ACCESS; do

sed -i'' "s/${v}/${!v//\//\\/}/g" $HIVE_CONF

done

add metastore schema to mysql

\(HIVE_HOME/bin/schematool -dbType mysql -initSchema \)HIVE_HOME/bin/hiveserver2 start &
$HIVE_HOME/bin/hiveserver2 --service metastore

Listing 8-7Apache Hive container entrypoint.sh script


接下来,用清单 8-8 中的内容创建一个 Dockerfile。

FROM ubuntu:16.04

ENV HADOOP_HOME /opt/hadoop
ENV HIVE_HOME /opt/hive
ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64

RUN apt-get update
&& apt-get install -y --reinstall build-essential
&& apt-get install -y
curl ssh rsync vim
net-tools openjdk-8-jdk python2.7-dev
libxml2-dev libkrb5-dev libffi-dev
libssl-dev libldap2-dev python-lxml
libxslt1-dev libgmp3-dev libsasl2-dev
libsqlite3-dev libmysqlclient-dev

ADD src/hadoop-3.1.2 /opt/hadoop
ADD src/apache-hive-3.1.2-bin /opt/hive

COPY ./hive-site-template.xml /opt/hive/conf/hive-site.xml

ADD entrypoint.sh /
RUN chmod 775 /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

EXPOSE 9083
EXPOSE 10000
EXPOSE 10002

Listing 8-8Apache Hive entrypoint.sh


确保定制的 Apache Hive 容器化项目包含前面指定的所有必要文件,如清单 8-9 所示。接下来,为本地测试构建 docker 文件中定义的容器:

.
├── Dockerfile
├── docker-compose.yml
├── entrypoint.sh
├── hive-site-template.xml
└── src
├── apache-hive-3.1.2-bin
├── apache-hive-3.1.2-bin.tar.gz
├── hadoop-3.1.2
└── hadoop-3.1.2.tar.gz

Listing 8-9Apache Hive containerization files


$ docker build -t apk8s-hive-s3m:3.1.2 .


创建一个版本标记并将新容器推送到公共注册中心(或者在测试容器后创建标记,如下一节所述):

$ docker tag apk8s-hive-s3m:3.1.2
apk8s/hive-s3m:3.1.2-1.0.0


#### 本地蜂箱测试

本节通过创建一个映射到 MinIO (S3) bucket `test`的数据库和表模式来测试在上一节中构建的 Hive 容器。在第七章定义的 MinIO 集群中创建 bucket `test`。在本书的后面,Hive 用于将对象位置作为数据源进行编目,并将模式投影到它们上面。下面演示了通过在 Hive 中创建一个映射到空桶`test`的模式来创建数据源(参见图 8-3 )。

![img/483120_1_En_8_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig3_HTML.jpg)

图 8-3

在本地工作站上测试配置单元

在构建新容器`apk8s-hive-s3m:3.1.2`之后,如前一节所述,创建一个 Docker Compose <sup>23</sup> 文件,用于在用于构建容器的工作站上进行本地测试。用清单 8-10 中的内容创建文件`docker-compose.yml`。将环境变量`S3A_ACCESS_KEY`和`S3A_SECRET_KEY`设置为第七章中建立的 MinIO 凭证。Docker Compose 配置定义了一个`mysql:8.0.18`数据库容器以及之前构建的 Apache Hive 容器`apk8s-hive-s3m:3.1.2`。

version: "3"

services:
mysql:
container_name: mysql
image: mysql:8.0.18
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: demo
ports:
- "3306:3306"
hive-metastore:
container_name: hive
image: apk8s-hive-s3m:3.1.2
environment:
MYSQL_ENDPOINT: "mysql:3306"
MYSQL_USER: "root"
MYSQL_PASSWORD: "demo"
S3A_ENDPOINT: "https://obj.data.dev5.apk8s.dev"
S3A_ACCESS_KEY: "miniouser"
S3A_SECRET_KEY: "miniopassword"
S3A_PATH_STYLE_ACCESS: "true"
ports:
- "9083:9083"
- "10000:10000"
- "10002:10002"
depends_on:
- mysql

Listing 8-10Apache Hive docker-compose.yml


运行新的 Docker 合成容器栈:

$ docker-compose up


启动 Docker Compose 后,新的 Apache Hive 容器连接到 MySQL,创建一个数据库和表,用于存储以后定义的模式和元数据。Apache Hive 容器公开了三个端口:HiveServer2 监听端口 10000,通过节俭/JDBC 提供 SQL 访问;Hive Metastore 侦听端口 9083,允许通过 thrift 协议访问元数据和表。Hive 在端口 10002 上提供了一个 web 接口,用于性能监控、调试和观察。

通过执行运行容器中可用的命令行应用,开始测试 Apache Hive:

$ docker exec -it hive /opt/hive/bin/hive


创建一个名为`test`的数据库:

hive> CREATE DATABASE IF NOT EXISTS test;
OK


使用空桶`test`在`test`数据库中创建一个名为`message`的表(如果桶不存在,在 MinIO 中创建桶):

hive> CREATE TABLE IF NOT EXISTS test.message (
> id int,
> message string
> )
> row format delimited fields terminated by ','
> lines terminated by "\n" location 's3a://test/messages';


在新的`test.message`表中插入一条记录:

hive> INSERT INTO test.message
> VALUES (1, "Hello MinIO from Hive");


选择从 MinIO (S3)返回的数据:

hive> SELECT * FROM test.message;
OK
1 Hello MinIO from Hive


前面的测试创建了一种分布式数据库,能够从一个高度可伸缩的分布式 MinIO 对象存储系统中编目和查询数 Pb 的数据。假设指定存储桶和前缀`(/test/messages/`中的所有数据都具有相同的结构,前面的练习能够对现有数据进行建模。这个强大的概念允许组织开始收集结构化数据,并在将来需要访问时应用模式。

下一节将 Apache Hive 的强大功能引入 Kubernetes 平台,这将在本书中继续讨论。在 Kubernetes 中运行 Hive 带来了容器管理、联网、监控和数据平台内所有服务的逻辑邻近性所提供的所有优势。

## 现代数据仓库

本书将现代数据仓库和数据湖视为一个开放的(采用容器化)、云原生的平台,以 Kubernetes(容器编排)为代表的云,并将该平台视为一个不断增长的数据管理应用集合,这些应用通过 API 和图形用户界面公开,能够在其中部署业务逻辑。

许多组织和应用需要访问各种数据源,从常见的 RDBMS 数据库到分布式文档、对象和密钥存储,这是数字化转型、物联网和数据科学活动(如机器学习)趋势的结果。将不同来源的数据关联起来是一种常见的做法;然而,根据这些来源之间的关系,这一过程可能具有挑战性。将所有数据源迁移到商业数据仓库可能成本过高,带来不可接受的限制,或者导致供应商锁定。在 Kubernetes 上构建一个现代的、云原生的、厂商中立的数据仓库可能会带来新的可能性,甚至与商业应用和 PaaS 产品一起。以很少的努力和资金实现了大量的功能和灵活性,从小处着手,具有近乎无限的扩展能力。

本节将 Presto 和 Apache Hive 添加到 Kubernetes 中,在本书开发的数据平台上应用新的层。Presto 和 Hive 展示了表示和组合 MinIO (S3)、Cassandra、MySQL 等数据源的能力,创建了一个具有分布式查询执行的集中式数据访问点。

### 储备

本节将部署本章前面开发的定制 Apache Hive 容器。Hive 在 Apache Hadoop 上提供了类似 SQL 的功能,将其用途扩展到更广泛的数据分析、分析和管理应用。Hadoop 的大数据功能传统上与 Hadoop 分布式文件系统(HDFS)相关联。然而,早期开发的自定义容器扩展了 Hive,使其能够使用 S3 兼容的对象存储作为 Hadoop HDFS 的现代替代方案。Apache Hive 在一个更大的数据湖中创建了一个数据仓库,如图 8-4 所示。

![img/483120_1_En_8_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig4_HTML.jpg)

图 8-4

Apache Hive 仓库结构化和半结构化数据

#### Kubernetes 配置

下面的配置定义了一个由实现本章前面开发的定制映像`apk8s/hive-s3m:3.1.2-1.0.0`的`hive`部署支持的`hive` Kubernetes 服务。新的 Hive 容器使用 MySQL 存储模式,定义存储在 MinIO (S3)中的结构化和半结构化对象。

创建目录`cluster-apk8s-dev5/003-data/085-hive`来包含 Apache Hive Kubernetes 配置。接下来,从清单 8-11 中创建一个名为`10-service.yml`的文件。

apiVersion: v1
kind: Service
metadata:
name: hive
namespace: data
labels:
app: hive
spec:
selector:
app: hive
ports:
- protocol: "TCP"
port: 10000
targetPort: tcp-thrift
name: tcp-thrift
- protocol: "TCP"
port: 9083
targetPort: tcp-thrift-meta
name: tcp-thrift-meta
- protocol: "TCP"
port: 10002
targetPort: http-hwi
name: http-hwi
type: ClusterIP

Listing 8-11Apache Hive Service


应用 Apache 配置单元服务配置:

$ kubectl apply -f 10-service.yml


接下来,从清单 8-12 中创建一个名为`30-deployment.yml`的文件。下面的部署将本章前面定义的环境变量`MYSQL_USER`和`MYSQL_PASSWORD`设置为 MySQL 集群配置的一部分。第七章用秘密`minio-creds-secret`配置了 MinIO,它为这个定制配置单元部署中的`S3A_ACCESS_KEY`和`S3A_SECRET_KEY`环境变量提供值。

apiVersion: apps/v1
kind: Deployment
metadata:
name: hive
namespace: data
labels:
app: hive
spec:
replicas: 1
revisionHistoryLimit: 1
selector:
matchLabels:
app: hive
template:
metadata:
labels:
app: hive
spec:
containers:
- name: hive
image: apk8s/hive-s3m:3.1.2-1.0.0
imagePullPolicy: IfNotPresent
env:
- name: MYSQL_ENDPOINT
value: "mysql:3306"
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: mysql-credentials
key: USER
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-credentials
key: PASSWORD
- name: S3A_ENDPOINT
value: "http://minio:9000"
- name: S3A_ACCESS_KEY
valueFrom:
secretKeyRef:
name: minio-creds-secret
key: accesskey
- name: S3A_SECRET_KEY
valueFrom:
secretKeyRef:
name: minio-creds-secret
key: secretkey
- name: S3A_PATH_STYLE_ACCESS
value: "true"
ports:
- name: tcp-thrift-meta
containerPort: 9083
- name: tcp-thrift
containerPort: 10000
- name: http-hwi
containerPort: 10002

Listing 8-12Apache Hive Deployment


应用 Apache 配置单元部署:

$ kubectl apply -f 30-deployment.yml


#### 测试数据

正如本章前面所演示的,Apache Hive 提供了将模式投射到空存储桶上的能力,允许创建特定的但结构良好的数据集。虽然 Hive 本身不是一个数据库,但它可以在分布式对象存储上创建大规模可伸缩的基于对象的数据库,在这种情况下,是 S3 兼容的 MinIO。Hive 提供了存储支持给定类型的现有结构化和半结构化对象的模式的能力。

以下练习创建了第七章中介绍的新献血者示例数据集,由分布在一千个 CSV 文件中的一百万条记录组成。每条记录包含虚构捐赠者的电子邮件、姓名、血型、生日和州的逗号分隔值。

按照第六章中的配置运行 JupyterLab,创建一个新的 Jupyter 笔记本;在这种情况下,浏览到一个自定义的 [`https://lab.data.dev5.apk8s.dev/`](https://lab.data.dev5.apk8s.dev/) 。将下列每个代码段添加到各自的单元格中。

Note

通过将`minio-internal-service.data:9000`替换为`minio.data.dev5.apk8s.dev:443`(在第七章中设置的 MinIO 集群入口)和`secure=False`到`secure=True`,以下练习可作为普通 Python 3 脚本在本地工作站上执行。

在基于 Python 的 Jupyter 笔记本的第一个单元中,确保安装了如下的`Faker`和`minio`库:

!pip install Faker2.0.3
!pip install minio
5.0.1


导入以下 Python 库:

import os
import datetime
from faker import Faker
from minio import Minio
from minio.error import (ResponseError,
BucketAlreadyOwnedByYou,
BucketAlreadyExists)


创建一个函数,返回一个元组,其中包含一条虚构的提供者信息记录:

fake = Faker()

def makeDonor():
fp = fake.profile(fields=[
"name",
"birthdate",
"blood_group"
])

return (
    fake.ascii_safe_email(),
    fp["name"],
    fp["blood_group"],
    fp["birthdate"].strftime("%Y-%m-%d"),
    fake.state(),
)

创建一个 MinIO API 客户端并创建 bucket `exports`:

bucket = "exports"
mc = Minio('minio-internal-service.data:9000',
access_key='',
secret_key='',
secure=False)

try:
mc.make_bucket(bucket)
except BucketAlreadyOwnedByYou as err:
pass
except BucketAlreadyExists as err:
pass
except ResponseError as err:
raise


最后,用数据时间创建一个名为的文件,包含一千个提供者记录。将文件上传到带有前缀`donors/`(例如`donors/20200205022452.csv`)的 MinIO bucket `exports`。重复这个过程 1000 次,总共有 100 万个测试记录。

for i in range(1,1001):
now = datetime.datetime.now()
dtstr = now.strftime("%Y%m%d%H%M%S")
filename = f'donors/{dtstr}.csv'
tmp_file = f'./tmp/{dtstr}.csv'

with open(tmp_file,"w+") as tf:
    tf.write("email,name,type,birthday,state\n")
    for ii in range(1,1001):
        line = ",".join(makeDonor()) + "\n"
        tf.write(line)

    mc.fput_object(bucket, filename, tmp_file,
                content_type='application/csv')

os.remove(tmp_file)
print(f'{i:02}: {filename}')

#### 创建模式

通过在 running Pod 中执行 Hive 命令行界面来测试新的自定义 Apache Hive 部署。

首先,在 MinIO 中创建一个名为`hive`的 bucket。

获取自定义 Apache 配置单元 Pod 名称:

$ kubectl get pods -l app=hive -n data


执行`hive`命令:

$ kubectl exec -it hive-8546649b5b-lbcrn \ /opt/hive/bin/hive -n data


从正在运行的 hive 命令中,创建一个名为`exports`的数据库:

hive> CREATE DATABASE exports;


接下来,创建表格`exports.donors`:

hive> CREATE TABLE exports.donors (
> email string,
> name string,
> blood_type string,
> birthday date,
> state string
> )
> row format delimited fields terminated by ','
> lines terminated by "\n"
> location 's3a://exports/donors';


本章使用一个定制的 Apache Hive 容器将模式投射到分布式对象存储上。虽然单个 Hive 容器能够通过在`hive:1000` Kubernetes 服务上公开的 ODBC/thrift 执行查询,但是直接针对 Hive 执行生产工作负载需要更大的 Hive 集群。但是,下一节使用一个 Presto 集群来执行分布式查询,并且只使用 Hive 从通过服务`hive:9083`公开的元数据服务器提供模式。

下一节将演示如何使用 Presto 连接使用 Hive 在 MinIO 分布式对象存储中收集的结构化数据,以及各种其他数据源,包括 RDBMS MySQL 和键/值数据库 Apache Cassandra。

### 很快

Presto 是本书中定义的现代数据仓库的最后一个组件。根据官网 prestodb.io 的介绍,“Presto 是一个开源的分布式 SQL 查询引擎,用于对从千兆字节到千兆字节的所有大小的数据源运行交互式分析查询。”虽然 Hive 也是一个查询海量数据的分布式 SQL 查询引擎电缆,但 Presto 连接了更广泛的数据源,包括 Apache Hive(如图 8-5 所示)。除了 Presto 的高性能查询功能,它还提供了一个数据源的中央目录。

![img/483120_1_En_8_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig5_HTML.jpg)

图 8-5

预先存储跨多个数据源的分布式 SQL 查询

Presto 减少了从多个来源检索数据所需的应用逻辑的数量,既通过标准的 SQL 抽象,又不需要客户端连接数据(在某些情况下被认为是反模式 <sup>24</sup> )。Presto 提供了跨所有受支持数据源的 SQL 抽象,执行分布式查询,并包括监控和可观察性。Presto 支持客户端库为 Go、 <sup>25</sup> C、 <sup>26</sup> Java、 <sup>27</sup> Node.js、 <sup>28</sup> PHP、 <sup>29</sup> Ruby、 <sup>30</sup> R、<sup>31</sup> 越来越多的基于 web 的 GUI 客户端、可视化和仪表板应用支持 Presto,包括 Apache Airflow 的创建者提供的新商业智能应用 Apache Superset(见图 8-6 )。

![img/483120_1_En_8_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig6_HTML.jpg)

图 8-6

Apache 超集(图片来自 [`超集。Apache。组织`](https://superset.apache.org)

#### Kubernetes 配置

本章在 Kubernetes 中使用一个稳定的开源 Helm chart by When I Work Data 安装了一个带有两个 workers 和一个协调器的 Presto 集群。 <sup>33</sup>

创建目录`cluster-apk8s-dev5/003-data/095-presto`来包含 Presto Helm 配置和文档。接下来,用清单 8-13 中的内容创建一个名为`values.yml`的文件。此外,创建一个名为`README.md`的文件来记录下一步执行的 Helm 命令。

在 Presto 中,一个目录代表一个顶级数据源。请注意在 Helm chart configuration values . yml 的 catalog 部分中定义的四个数据源(称为连接器)。前两个数据源`obj.properties`和`hive.properties`使用 hive-hadoop2 连接器。Presto 使用 Hive 访问包含在 HDFS 或 S3 的数据文件(对象),并使用 Hive Metastore 服务访问表示数据文件的元数据和模式。`hive.properties`配置演示了定制 Apache Hive 容器(安装在前面的小节中)在 MySQL 支持的 Metastore 服务中的使用。此外,`cassandra.properties`和`mysql.properties`展示了本章配置的 MySQL 和 Apache Cassandra 的连接。

presto:
environment: "production"
workers: 2
logLevel: "INFO"

image:
repository: "wiwdata/presto"
tag: "0.217"
pullPolicy: "IfNotPresent"

service:
type: ClusterIP

catalog:
obj.properties: |
connector.name=hive-hadoop2
hive.metastore=file
hive.metastore.catalog.dir=s3://metastore/
hive.allow-drop-table=true
hive.s3.aws-access-key= miniobucketuserid
hive.s3.aws-secret-key= miniobucketuserpassword
hive.s3.endpoint=http://minio:9000
hive.s3.path-style-access=true
hive.s3.ssl.enabled=false
hive.s3select-pushdown.enabled=true

hive.properties: |
connector.name=hive-hadoop2
hive.metastore.uri=thrift://hive:9083
hive.allow-drop-table=true
hive.s3.aws-access-key= miniobucketuserid
hive.s3.aws-secret-key= miniobucketuserpassword
hive.s3.endpoint=http://minio:9000
hive.s3.path-style-access=true
hive.s3.ssl.enabled=false

cassandra.properties: |
connector.name=cassandra
cassandra.contact-points=cassandra-data-r1-0,cassandra-data-r1-1,cassandra-data-r1-2

mysql.properties: |
connector.name=mysql
connection-url=jdbc:mysql://mysql:3306
connection-user= root
connection-password= mysqlrootpassword

coordinatorConfigs: {}
workerConfigs: {}
environmentVariables: {}
coordinatorResources: {}
workerResources: {}
coordinatorNodeSelector: {}
workerNodeSelector: {}
coordinatorTolerations: []
workerTolerations: {}
coordinatorAffinity: {}
workerAffinity: {}

Listing 8-13Presto Helm configuration


接下来,克隆 Presto Helm 图表库:

$ git clone git@github.com:apk8s/presto-chart.git


Note

GitHub 存储库 apk8s/presto-chart<sup>34</sup>从 wiwdata/presto-chart 派生而来,包含与 Kubernetes 1.16+兼容所需的微小更新。有关未来版本或与 Kubernetes 旧版本的兼容性,请参考上游存储库。

通过应用前面的 Helm chart 克隆,以及来自`values.yml`的自定义配置,创建一个新的 Presto 集群:

$ helm upgrade --install presto-data
--namespace data
--values values.yml
./presto-chart/presto


一旦 Helm 完成安装过程,Kubernetes 集群就包含两个 Presto worker 节点和一个 Presto 协调器。

最后,添加一个由 Helm chart 生成的新`presto-data:80`服务支持的 Kubernetes 入口配置。下面的入口使用在第五章的“数据名称空间”部分设置的秘密`sysop-basic-auth`来添加简单的基本认证安全性。从清单 8-14 中创建一个名为`50-ingress.yml`的文件。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: presto
namespace: data
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: sysop-basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
spec:
rules:
- host: presto.data.dev5.apk8s.dev
http:
paths:
- backend:
serviceName: presto-data
servicePort: 80
path: /
tls:
- hosts:
- presto.data.dev5.apk8s.dev
secretName: presto-data-production-tls

Listing 8-14Presto Ingress


应用预存储入口配置:

$ kubectl apply -f 50-ingress.yml


下一节将演示如何使用 presto-python-client 库从 Jupyter 笔记本中连接到 Presto。

#### 询问

本节使用运行在 Kubernetes 集群中的基于 Python 的 Jupyter 笔记本演示了与 Presto 的交互(参见第六章)。

从 JupyterLab 环境启动一个新的 Python 3 笔记本,在各个单元格中添加并执行以下代码。从第一个单元开始,使用`pip`安装`presto-python-client`库:

!pip install presto-python-client==0.7.0


导入`prestodb`(从包`presto-python-client`)、`os`、`pandas` Python 库;创建一个新的用于执行命令的 Presto 数据库连接对象和光标,如图 8-7 所示。执行命令`SHOW CATALOGS`显示四个数据源,`cassandra`、`hive`、`mysql`和`obj`在前面章节中配置。`system`目录包含预先存储的内部配置和运行数据。

![img/483120_1_En_8_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig7_HTML.jpg)

图 8-7

木星笔记本执行显示目录 presto 命令

查询`system`目录中的 Presto 节点列表,如图 8-8 所示。将 select 语句的结果与列名一起加载到 Pandas<sup>35</sup>data frame<sup>36</sup>中,为管理和显示结果集提供了一个用户界面。在执行分析和数据科学活动时,这种方法变得特别有用。Pandas 通常是 Python 数据库的核心组件,包含许多强大的数据转换和数学运算功能。

![img/483120_1_En_8_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig8_HTML.jpg)

图 8-8

检索预先存储的节点列表

描述上一节中用 Apache Hive 配置的`donors`模式,如图 8-9 所示。Hive 和 Presto 完全抽象出`donors`数据集的位置和底层数据结构,在本章前面生成并上传了一千个 CSV 文件到`exports`桶。

![img/483120_1_En_8_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig9_HTML.jpg)

图 8-9

用 Presto 描述表格

图 8-10 描述了一个 SQL select 语句,该语句利用 Presto 扫描所有一千个 CSV 文件,查找其中`state`列的值以“新”开始的记录,并通过`state`和`blood_type`对记录进行分组,每组记录的计数。

Presto 提供了一个成熟的、文档完善的、功能全面的 SQL 接口以及几十个函数和操作符。利用 Presto 进行初始数据分析、应用数学函数和运算符、聚合等,通过利用 Presto 的分布式执行引擎来执行这些密集型任务,尤其是处理大量数据,消除了应用的操作复杂性。

![img/483120_1_En_8_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig10_HTML.jpg)

图 8-10

Presto 中的 SQL select 语句

连接不同的数据集是 Presto 的一个基本特性,也是数据仓库的核心概念。作为读者的一个练习,创建数据或将数据导入到 Apache Cassandra 或 MySQL 中,这些数据与前面使用的样本提供者数据相关。一个典型的 SQL 连接可能类似于图 8-11;在这个例子中,名为 appointment 的 Cassandra 表存在于 Keyspace lab 中,表示约会数据。

图 8-11 :使用 Presto 来 SQL 连接 Hive 和 Cassandra 数据集的示例。

![img/483120_1_En_8_Fig11_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig11_HTML.jpg)

图 8-11

在 Presto 中的 SQL 连接语句

跨各种数据源连接大型数据集并对其执行操作会创建复杂的执行计划。Presto 为探索、监控和调试查询提供了一个直观的基于 web 的用户界面。上一节定义的入口配置在 [`https://presto.data.dev5.apk8s.dev`](https://presto.data.dev5.apk8s.dev) 展示 Presto UI,如图 8-12 所示。

![img/483120_1_En_8_Fig12_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig12_HTML.jpg)

图 8-12

在 Presto 中的 SQL 连接语句

监控和可观察性对于大数据和网络规模的数据操作都至关重要。Presto web 用户界面支持深入每个查询,提供查询细节,包括资源利用、时间表、错误信息、阶段和与执行相关的任务。此外,Presto 提供了一个实时计划,如图 8-13 所示,通过网络图实时描绘了阶段之间的执行流程。

![img/483120_1_En_8_Fig13_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_8_Fig13_HTML.jpg)

图 8-13

在 Presto 中的 SQL 连接语句

Presto 是在 Kubernetes 内部构建现代数据仓库的综合解决方案;它对一系列数据源的支持符合物联网和机器学习不断增长的需求,提供了检索、合并、关联、转换和分析无限数量和结构的数据的能力。

## 摘要

这一章,连同第六章,展示了构建在 Kubernetes 之上的数据湖和数据仓库概念的小规模表示。像 Apache Hive 和 Presto 这样的技术帮助组织处理孤立的数据管理操作;在 Kubernetes 上运行这些解决方案,通过统一底层数据和控制面板,进一步降低了这些应用在逻辑和概念上的接近程度。

本章介绍了 MySQL 集群的安装,它代表了一个非常流行的 RDBMS,Apache Cassandra 作为大数据键/值存储,Hive 公开了无限的结构化和半结构化数据对象。虽然对于一系列以数据为中心的问题领域(从机器学习到物联网)有大量的专门应用,但这本书涵盖了一套通用而全面的数据(和事件)管理解决方案。如果按照前面的章节,清单 8-15 代表了到目前为止基于 Kubernetes 的数据平台组件的当前组织的高级快照。

现在有了存储和检索几乎无限量和形式的数据的能力,下一章将通过扩展数据收集、路由、转换和处理的能力来扩展这个基于 Kubernetes 的数据平台。

./008-cluster-apk8s-dev5
├── 000-cluster
│ ├── 00-ingress-nginx
│ ├── 01-helm
│ ├── 10-cert-manager
│ ├── 20-rook-ceph
│ ├── 22-minio
│ ├── 23-rook-cassandra
│ ├── 25-mysql-operator
│ └── 30-monitoring
├── 003-data
│ ├── 000-namespace
│ ├── 005-keycloak
│ ├── 010-zookeeper
│ ├── 020-kafka
│ ├── 030-elasticsearch
│ ├── 032-logstash
│ ├── 034-kibana
│ ├── 050-mqtt
│ ├── 060-cassandra
│ ├── 070-minio
│ ├── 080-mysql
│ ├── 085-hive
│ ├── 095-presto
│ └── 100-jupyterhub
└── 005-data-lab
└── 000-namespace

Listing 8-15Organization of Kubernetes-based data platform components


<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1

[`https://delta.io/`](https://delta.io/)

  2

[`https://kylo.io/`](https://kylo.io/)

  3

斯卡利、加里·霍尔特、丹尼尔·戈洛文、尤金·达维多夫、托德·菲利普斯、迪特马尔·埃布纳、维奈·乔杜里、迈克尔·杨、让-弗朗索瓦·克雷斯波和丹·丹尼森。“机器学习系统中隐藏的技术债务。”《神经信息处理系统的进展》28,c .科尔特斯、N. D .劳伦斯、D. D .李、m .杉山和 r .加内特编辑,2503–2511。柯伦联合公司,2015 年。 [`http://papers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems.pdf`](http://papers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems.pdf) 。

  4

[`https://landscape.cncf.io/`](https://landscape.cncf.io/)

  5

[`www.mysql.com/`](http://www.mysql.com/)

  6

[`https://cassandra.apache.org/`](https://cassandra.apache.org/)

  7

[`https://hive.apache.org/`](https://hive.apache.org/)

  8

[`https://prestodb.io/`](https://prestodb.io/)

  9

[`https://insights.stackoverflow.com/survey/2019#technology-_-databases`](https://insights.stackoverflow.com/survey/2019%2523technology-_-databases)

  10

[`www.drupal.org/`](http://www.drupal.org/)

  11

[`https://wordpress.com/activity/`](https://wordpress.com/activity/)

  12

[`https://github.com/presslabs/mysql-operator`](https://github.com/presslabs/mysql-operator)

  13

[`https://github.com/github/orchestrator`](https://github.com/github/orchestrator)

  14

[`https://rook.io/`](https://rook.io/)

  15

[`https://rook.io/docs/rook/v1.2/cassandra-cluster-crd.html`](https://rook.io/docs/rook/v1.2/cassandra-cluster-crd.html)

  16

[`www.scylladb.com/`](http://www.scylladb.com/)

  17

[`www.youtube.com/watch?v=Idu9OKnAOis`](http://www.youtube.com/watch%253Fv%253DIdu9OKnAOis)

  18

[`https://technology.finra.org/opensource.html#bigdata`](https://technology.finra.org/opensource.html%2523bigdata)

  19

[`https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-hive.html`](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-hive.html)

  20

[`https://github.com/apk8s/hive`](https://github.com/apk8s/hive)

  21

[`https://cwiki.apache.org/confluence/display/Hive/AdminManual+Metastore+3.0+Administration`](https://cwiki.apache.org/confluence/display/Hive/AdminManual%252BMetastore%252B3.0%252BAdministration)

  22

[`www.gnu.org/software/bash/manual/`](http://www.gnu.org/software/bash/manual/)

  23

[`https://docs.docker.com/compose/`](https://docs.docker.com/compose/)

  24

[`www.batey.info/cassandra-anti-pattern-distributed.html`](http://www.batey.info/cassandra-anti-pattern-distributed.html) ,

[`www.slideshare.net/chbatey/webinar-cassandra-antipatterns-45996021`](http://www.slideshare.net/chbatey/webinar-cassandra-antipatterns-45996021)

  25

[`https://github.com/prestodb/presto-go-client`](https://github.com/prestodb/presto-go-client)

  26

[`https://github.com/easydatawarehousing/prestoclient/tree/master/C`](https://github.com/easydatawarehousing/prestoclient/tree/master/C)

  27

[`https://prestodb.io/docs/current/installation/jdbc.html`](https://prestodb.io/docs/current/installation/jdbc.html)

  28

[`https://github.com/tagomoris/presto-client-node`](https://github.com/tagomoris/presto-client-node)

  29

[`https://github.com/Xtendsys-labs/PhpPrestoClient`](https://github.com/Xtendsys-labs/PhpPrestoClient)

  30

[`https://github.com/treasure-data/presto-client-ruby`](https://github.com/treasure-data/presto-client-ruby)

  31

[`https://github.com/prestodb/RPresto`](https://github.com/prestodb/RPresto)

  32

[`https://github.com/prestodb/presto-python-client`](https://github.com/prestodb/presto-python-client)

  33

[`https://github.com/wiwdata/presto-chart`](https://github.com/wiwdata/presto-chart)

  34

[`https://github.com/apk8s/presto-chart`](https://github.com/apk8s/presto-chart)

  35

[`https://pandas.pydata.org/`](https://pandas.pydata.org/)

  36

[`https://pandas.pydata.org/pandas-docs/stable/reference/frame.html`](https://pandas.pydata.org/pandas-docs/stable/reference/frame.html)

 </aside>

# 九、路由和转换

前面的章节介绍了数据库、数据湖和数据仓库的构建,这些都是数据平台的基础元素,全部来自 Kubernetes,展示了一个健壮的分布式服务平台。本章着重于数据的收集、提取、移动和处理,完善了任何以数据为中心的应用所需的大部分功能。从一个不同的系统高效地提取、转换和加载数据到另一个系统的能力对于利用消费者和工业物联网、社交媒体和许多组织中发生的数字转换的数据爆炸式增长至关重要。快速构建移动、转换和处理数据的路径的能力对于利用不断进步的数据驱动趋势至关重要,例如基于机器学习的人工智能,这些技术特别渴望大量处理的数据。一个有效的数据平台提供了跨数据管理系统**提取、转换和加载**数据所需的所有通用机制,并提供了支持专门处理和定制业务逻辑的应用层。

本章使用 ETL/ELT(提取、转换、加载)和 FaaS(功能即服务,也称为无服务器)功能扩展了基于 Kubernetes 的数据平台,该平台是在前面章节的基础上构建的。专注于 ETL 的技巧和技术已经成熟很多年了。数据工程师现在可以将 ETL 与无服务器平台相结合,快速开发、集成并直接部署到数据流水线中。

## ETL 和数据处理

本章介绍了 Kubernetes 中的两项新技术:Apache NiFi 和 OpenFaaS,展示了一种提取、加载、转换和处理数据的纯开源方法,不需要特定领域的语言和复杂的配置文件。Apache NiFi 提供了数百个预构建的数据“处理器”,用于从几乎任何标准网络协议或 API 实现中提取和加载数据,并能够将其转换为几乎任何所需的形式。OpenFaaS 是对无服务器/FaaS(作为服务的功能)概念的厂商中立的方法;在这种情况下,FaaS 通过用任何语言开发的定制的、高度集中的代码,允许 ETL 流水线的无限可扩展性。

图 9-1 描述了贯穿本章的 ETL、数据处理和可视化演示。最终目标是可视化一段时间内在一组特定的 Twitter 消息上表达的情绪范围。下面几节安装 OpenFaaS 并部署一个预打包的情绪分析功能。后面的部分介绍了 Apache NiFi,并配置了从 Twitter 到 Kafka 以及从 Kafka 到 OpenFaaS 情绪分析函数的数据路由和转换的有向图,最后将结果记录在 Elasticsearch 中,供 Jupyter 笔记本进行分析。虽然这些技术都不需要 Kubernetes,但本演示旨在阐明统一控制面板、网络、监控的优势及其基于容器的应用平台功能的可扩展性。

![img/483120_1_En_9_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig1_HTML.jpg)

图 9-1

NiFi 和 OpenFaaS 演示架构

本书中的一些技术,即数据库,在抽象的基础设施上运行时可能会牺牲一定程度的性能;然而,一些应用会发现这是一种可接受的折衷,可以减少管理许多具有完全不同的依赖关系的系统时所产生的技术负担。根据组织的规模或项目的预算问题,将基础设施专业知识应用于每项企业级技术(如 Apache NiFi、Kafka 和 Elasticsearch)可能不可行。这本书希望通过利用 Kubernetes 在几乎任何项目都可能利用的规模上展示这些技术,从概念的启动证明到网络规模的社交网络。

## 发展环境

以下练习继续利用第六章中提到的廉价 Hetzner 集群,包括一个用于 Kubernetes 主节点的 CX21 (2 个 vCPU/8G RAM/40G SSD)和四个用于 worker 节点的 CX41 (4 个 vCPU/16G RAM/160G SSD)实例,但任何等效的基础架构都可以容纳。此外,本章还利用了第 3 、 5 和 6 章中安装的应用和集群配置;参见第七章中的表 7-1 。本章要求在第三章中配置入口、证书管理器、存储和监控;第五章中的命名空间、动物园管理员和 Kafka;以及第六章的 Elasticsearch、Kibana、Keycloak、JupyterHub。

本章在项目文件夹`cluster-apk8s-dev5`下组织名为`dev5`的集群的配置清单。

## 无服务器

无服务器的概念,也称为 FaaS (functions as a service),已经不断成熟,满足了小单元功能代码的简化部署需求。单一应用、微服务和(无服务器)功能之间的区别在于实现、操作基础设施和更广泛架构的上下文。例如,许多微服务和基于功能的架构仍然依赖于单片数据库。术语“无服务器”意味着开发人员应该很少或根本不需要关心服务器端的实现。无服务器或 FaaS (functions as a service)旨在抽象出集成、部署和运行时操作的几乎所有方面,将功能性业务逻辑作为开发人员的唯一职责。

云供应商推销无服务器技术的核心吸引力,即允许开发人员专注于业务逻辑、抽象和管理基础设施、操作系统、运行时和应用层的能力。主要云厂商的产品包括亚马逊的 AWS Lambda、 <sup>1</sup> 微软的 Azure 函数、 <sup>2</sup> 谷歌云函数、 <sup>3</sup> 和 IBM 云函数。 <sup>4</sup> 这些产品可以显著缩短上市时间,减少许多组织的技术债务,尽管是以锁定供应商为代价。然而,已经投资 Kubernetes 的组织可以利用越来越多的开源、厂商中立的无服务器平台,如 Apache OpenWhisk、 <sup>5</sup> Kubeless、 <sup>6</sup> 和 OpenFaaS。<sup>7</sup>Knative<sup>8</sup>对于那些寻求开发定制无服务器平台的人来说是一个流行的选择。

本章展示了 OpenFaaS 在一个示例 ETL 应用中的使用,以及无服务器/功能即服务如何对任何数据平台进行出色的补充。

### OpenFaaS

OpenFaaS 是一个稳定的、维护良好的、无服务器的应用平台,被越来越多的组织所使用。OpenFaaS 几乎可以在任何地方运行,但可以与 Kubernetes 很好地集成,支持 Go、Java、Python、PHP、Rust、Perl、C#和 Ruby 等语言,以及包括 Express.js、Django 和 ASP.NET 核心在内的应用平台。OpenFaaS 支持定制容器来包装强大的二进制文件,如 FFmpeg <sup>9</sup> 和 ImageMagick。 <sup>10</sup>

管理容器化的工作负载是 Kubernetes 的核心能力;然而,像 OpenFaaS 这样的平台提供了一个正式的工具和操作框架,用于开发、编目、集成、部署、扩展和监控用函数表示的工作负载。

无服务器/功能即服务是 ETL 和数据处理流水线的天然选择,这一点将在本章的后面介绍。

#### 安装 openafs

用 OpenFaaS 库更新 Helm:

$ helm repo add openfaas
https://openfaas.github.io/faas-netes/
$ helm repo update


接下来,使用 Helm 将 OpenFaaS 网关安装到带有参数`--namespace data`的`data`名称空间中。OpenFaaS 网关可以设置函数在另一个名称空间中运行,但是对于这个例子,通过设置`functionNamespace=data`来使用`data`名称空间。OpenFaaS 能够利用 Kubernetes 节点端口和负载均衡器; <sup>11</sup> 然而,这个例子通过设置`exposeServices=false`和`ingress.enabled=true`使用 Ingress 来公开部署的函数。最后,设置选项`generateBasicAuth=true`以使用基本认证保护入口暴露网关用户界面:

$ helm upgrade apk8s-data-openfaas –install
openfaas/openfaas
--namespace data
--set functionNamespace=data
--set exposeServices=false
--set ingress.enabled=true
--set generateBasicAuth=true


创建目录`cluster-apk8s-dev5/003-data/120-openfaas`。在新的`120-openfaas`目录中,从清单 9-1 中创建一个名为`50-ingress.yml`的文件。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: faas
namespace: data
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
rules:
- host: faas.data.dev5.apk8s.dev
http:
paths:
- backend:
serviceName: gateway
servicePort: 8080
path: /
tls:
- hosts:
- faas.data.dev5.apk8s.dev
secretName: faas-data-production-tls

Listing 9-1OpenFaaS Ingress


应用 OpenFaaS 入口配置:

$ kubectl apply -f 50-ingress.yml


成功应用 Ingress 后,使用以下命令检索由 OpenFaaS Helm 安装生成的基本身份验证凭据:

$ echo $(kubectl -n data get secret basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode)


前面的命令返回用于登录 OpenFaaS UI 门户的基本 Auth 密码;用户名是`admin`。在本例中,浏览至入口 URL(见图 9-2 ):

[`https://faas.data.dev5.apk8s.dev`](https://faas.data.dev5.apk8s.dev) 。

![img/483120_1_En_9_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig2_HTML.jpg)

图 9-2

openfans 用户界面门户

OpenFaaS UI 门户是一个方便的基于 web 的可视化界面,用于安装和管理功能。然而,CLI 实用程序 faas-cli 通常是与 OpenFaaS 交互的首选方法,能够支持开发、测试、部署、管理和自动化功能即服务的所有方面。在本地工作站上安装 OpenFaaS CLI:

$ curl -sLSf https://cli.openfaas.com | sudo sh


执行 OpenFaaS CLI 获取顶级命令列表:

$ faas-cli


虽然本书推荐将 CLI 实用程序用于常规用途,但以下部分使用基于 web 的界面作为安装 OpenFaaS 功能并与之交互的简单直观演示。

#### 安装情感分析

情感分析, <sup>12</sup> 又称为情感识别 <sup>13</sup> 或观点挖掘, <sup>14</sup> 是自然语言处理(NLP)的一种形式。NLP 将语言学、人工智能和信息工程应用于自然(人类)语言。本节部署一个预建的 OpenFaaS 函数容器, <sup>15</sup> 实现 Python 库 TextBlob <sup>16</sup> 对原始文本的一个或多个句子进行情感分析。本章稍后将使用部署的情感分析功能来分析实时的 Twitter 消息流,这些消息都标有与新冠肺炎相关的关键词。

浏览到上一节设置的 OpenFaaS UI 门户( [`https://faas.data.dev5.apk8s.dev`](https://faas.data.dev5.apk8s.dev) ),点击屏幕中央的`DEPLOY NEW FUNCTION`按钮。接下来,使用`Search for Function`功能,搜索如图 9-3 所示的术语`SentimentAnalysis`,选择功能`SentimentAnalysis`,点击对话框左下方的`DEPLOY`。

![img/483120_1_En_9_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig3_HTML.jpg)

图 9-3

openfans 部署预建的情绪分析功能

部署 OpenFaaS 情绪分析功能后,从左侧导航中选择它。UI 门户在页面的上半部分显示功能的状态、副本、调用计数、映像和 URL(参见图 9-4 )。URL 是公开的端点。OpenFaaS 网关默认不保护函数端点; <sup>17</sup> 安全是功能本身的责任。OpenFaaS 文档通过使用 Kubernetes Secrets 实现 HMAC 安全性来指导开发自定义函数身份验证。 <sup>18</sup>

OpenFaaS UI 门户提供了一个方便的 web 表单,用于测试图 9-4 中显示的调用功能部分下部署的功能。或者,使用上一节中安装的`faas-cli`实用程序调用情绪分析功能:

![img/483120_1_En_9_Fig4_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig4_HTML.jpg)

图 9-4

使用 OpenFaaS UI 门户测试情感分析功能

$ echo "Kubernetes is easy" | faas-cli invoke
sentimentanalysis -g https://faas.data.dev5.apk8s.dev/


最后,用 cURL 测试新函数的公共访问:

$ curl -X POST -d "People are kind"
https://faas.data.dev5.apk8s.dev/function/sentimentanalysis


示例输出:

{"polarity": 0.6, "sentence_count": 1, "subjectivity": 0.9}


OpenFaaS 情绪分析功能是一个很好的例子,它是由 OpenFaaS 在 Kubernetes 上部署和管理的一个集中的、独立的处理逻辑。OpenFaaS 文档包含一组编写良好的关于构建、测试和实现函数的教程。 <sup>19</sup> 函数是持续扩展本书中开发的数据平台的一个伟大方式。下一节将介绍用于 ETL 类型操作的 Apache NiFi,并将情感分析功能的使用作为示例数据处理流程的一部分(参见图 9-5 )。

## 抽取、转换、加载至目的端(extract-transform-load 的缩写)

ETL(提取、转换、加载)的实践可以追溯到 20 世纪 70 年代。从一个来源提取数据并转换为另一个来源使用的需求是一个永恒的问题,今天有大量的现有技术来解决这个问题。Pentaho、 <sup>21</sup> Talend、 <sup>22</sup> CloverETL、 <sup>23</sup> 和 JasperETL <sup>24</sup> 是少数几个开源、社区驱动选项有限的商业产品。然而,ETL 是一个如此普遍的问题,以至于新的方法和通用的解决方案,如开源(厂商中立)Apache NiFi,因为易于使用和简单、直观的数据收集、路由和转换方法而越来越受欢迎。

### Apache 尼菲

Apache NiFi 是本书中描述的以数据为中心的平台的数据接收前端选择。" Apache NiFi 支持强大且可伸缩的数据路由、转换和系统中介逻辑的有向图." <sup>25</sup>

NiFi 附带了近 300 个独特的数据处理器,可用于收集、转换、处理和建模来自 Twitter、SMTP、HDFS、Redis、UDP、HBase 和 HTTP API 端点等不同来源的数据。 <sup>26</sup>

在本书出版时,在 Kubernetes 中运行 NiFi 的官方文档和支持很少。然而,NiFi 维护者意识到对一流 Kubernetes 支持需求的快速增长,读者应该期待在未来几年中对这一努力的重大贡献。

以下部分在 Kubernetes 中安装了一个多节点 Apache NiFi 集群,并演示了从 Twitter 中提取数据的流程,包括转换、处理和利用 OpenFaaS、Kafka 和 Elasticsearch,如图 9-5 所示。

![img/483120_1_En_9_Fig5_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig5_HTML.jpg)

图 9-5

Apache NiFi 流概述

#### 安装 Apache NiFi

本节安装 Apache NiFi 和三个 Kubernetes 资源,包括一个 Headless 服务、StatefulSet 和 Ingress。回顾本章前面的“开发环境”部分,了解需求,包括入口 Nginx、Ceph 存储、证书管理器和 Apache Zookeeper。

虽然这本书有时会避免使用 Helm 安装,而是通过手工制作的清单来更详细地描述概念,但读者也应该考虑一下由 Cetic 编写的 Apache NiFi Helm 图表。 <sup>27</sup>

创建目录`cluster-apk8s-dev5/003-data/060-nifi`。在新的`060-nifi`目录中,从清单 9-2 中创建一个名为`10-service-headless.yml`的文件。下面定义的 StatefulSet 调用运行 apache/nifi:1.9.2 容器的 Pod `nifi`的两个副本。集群中的每个 NiFi 实例在引导时都需要一些定制配置。`command:`部分不允许运行标准的启动脚本,而是调用脚本中的 Bash 和 pipes 来根据 Kubernetes 分配给 Pod 的主机名定制一些属性。

apiVersion: v1
kind: Service
metadata:
name: nifi
namespace: data
labels:
app: nifi
annotations:
service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
spec:
type: ClusterIP
clusterIP: None
selector:
app: nifi
ports:

  • port: 8080
    name: http
  • port: 6007
    name: cluster

Listing 9-2NiFi Headless Service


应用 NiFi Headless 服务配置:

$ kubectl apply -f 10-service-headless.yml


接下来,在清单 9-3 中的文件`40-statefulset.yml`中为 NiFi 创建一个 StatefulSet 配置。

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nifi
namespace: data
labels:
app: nifi
spec:
replicas: 2
revisionHistoryLimit: 1
selector:
matchLabels:
app: nifi
serviceName: nifi
template:
metadata:
labels:
app: nifi
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- "nifi"
topologyKey: "kubernetes.io/hostname"
containers:
- name: nifi
imagePullPolicy: IfNotPresent
image: apache/nifi:1.9.2
command:
- bash
- -ce
- |
FQDN=\((hostname -f) PROP_FILE=\){NIFI_HOME}/conf/nifi.properties

          p_repl () {
            echo "setting ${1}=${2}"
            sed -i -e "s|^$1=.*$|$1=$2|" ${PROP_FILE}
          }

          p_repl nifi.remote.input.host ${FQDN}
          p_repl nifi.cluster.is.node true
          p_repl nifi.cluster.node.protocol.port 6007
          p_repl nifi.cluster.node.address ${FQDN}
          p_repl nifi.cluster.protocol.is.secure false
          p_repl nifi.security.user.authorizer managed-authorizer
          p_repl nifi.web.http.host ${FQDN}
          p_repl nifi.web.http.port 8080
          p_repl nifi.zookeeper.connect.string ${NIFI_ZOOKEEPER_CONNECT_STRING}
          p_repl nifi.cluster.flow.election.max.wait.time "1 mins"

          tail -F "${NIFI_HOME}/logs/nifi-app.log" & exec bin/nifi.sh run
      env:
        - name: NIFI_ZOOKEEPER_CONNECT_STRING
          value: "zookeeper-headless:2181"
      ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        - containerPort: 6007
          name: cluster
          protocol: TCP

Listing 9-3NiFi StatefulSet


应用 NiFi StatefulSet 配置:

$ kubectl apply -f 40-statefulset.yml


最后,在清单 9-4 中的文件`50-ingress.yml`中为 NiFi 创建一个入口配置。Apache NiFi 支持认证; <sup>28</sup> 然而,这需要它在 SSL 模式下运行,并且需要额外的组件配置来管理证书和入口。为了保持演示的简洁,入口配置使用存储在 Kubernetes Secret `sysop-basic-auth`中的基本 Auth 凭证来保护 NiFi。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nifi
namespace: data
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: sysop-basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
spec:
rules:
- host: nifi.data.dev3.apk8s.dev
http:
paths:
- backend:
serviceName: nifi
servicePort: 8080
path: /
tls:
- hosts:
- nifi.data.dev3.apk8s.dev
secretName: data-production-tls

Listing 9-4NiFi Ingress


应用 NiFi 入口配置:

$ kubectl apply -f 50-ingress.yml


浏览至 [`https://nifi.data.dev3.apk8s.dev/nifi`](https://nifi.data.dev3.apk8s.dev/nifi) ,打开用户界面左上角的全局菜单,选择项目集群,验证新的 NiFi 集群已启动并正在运行。查看运行节点列表,如图 9-6 所示。

![img/483120_1_En_9_Fig6_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig6_HTML.jpg)

图 9-6

Apache NiFi 集群状态

下一节展示了一个 ETL/ELT 数据流水线的例子,它使用了本章前面安装的 OpenFaaS,以及前面章节介绍的 Apache Kafka、Elasticsearch 和 JupyterLab。

### ETL 数据流水线示例

ETL(提取、转换、加载)操作的传统示例可能会展示在大数据系统中收集和存储的数据的提取,如 HDFS 或任何种类的商业和开源数据湖以及传统或现代企业数据管理系统。虽然下面的例子演示了从 Twitter 中提取数据,但是应该很容易理解 NiFi 的各种预构建数据处理器的灵活性,并将这些基本功能应用于几乎所有 ETL 挑战。

这个例子需要 OpenFaaS 和本章前面安装的 SentimentAnalysis 函数;ApacheKafka,配置在第五章;以及在第六章中介绍的 Elasticsearch、Kibana、Keycloak 和 JupyterHub。

以下示例 ETL 数据流水线使用 NiFi Twitter 处理器从 Twitter 中提取消息,并将它们发布到 Apache Kafka 主题。随后,Kafka 处理器使用主题中的消息,准备并将其发送到 OpenFaaS SentimentAnalysis 函数,最后将结果存储在 Elasticsearch 索引中,以便在 JupyterLab 环境中进行分析。这个例子展示了 Kubernetes 如何在一个分布式、高可用、受监控和统一的控制面板中轻松管理所有必需的工作负载(见图 9-1 和 9-5 )。

#### NiFi 模板

Apache NiFi 为希望扩展其功能的用户、管理员和开发人员提供了详细的文档。 <sup>29 因此,为了快速演示其用法,以及利用本书中配置和安装的组件,可以在 [`https://github.com/apk8s/nifi-demo`](https://github.com/apk8s/nifi-demo) 找到一个预构建的模板。</sup>

克隆`apk8s/nifi-demo`存储库:

git clone git@github.com:apk8s/nifi-demo.git


在 [`https://nifi.data.dev3.apk8s.dev/nifi`](https://nifi.data.dev3.apk8s.dev/nifi) 浏览到正在运行的 NiFi 集群后,点击屏幕左侧操作面板中显示的模板上传按钮(见图 9-7 )。当出现提示时,上传在`apk8s/nifi-demo`库的`templates`目录中找到的文件`Twitter_Sentiment.xml`。

![img/483120_1_En_9_Fig7_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig7_HTML.jpg)

图 9-7

Apache NiFi 上传模板

上传模板后,将模板图标(三个相连的方框)从组件工具栏(顶部导航)拖动到画布(网格)中,如图 9-8 所示。在完成之前,模板组件向用户提示可用模板的列表;选择“Twitter 情绪 v2”并点击添加按钮。

![img/483120_1_En_9_Fig8_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig8_HTML.jpg)

图 9-8

Apache NiFi 添加模板

添加模板后,画布现在包含 10 个 NiFi 处理器,如图 9-5 所示。模板提供的处理器被预先配置为利用前面章节中安装的组件,比如 Apache Kafka 和 Elasticsearch。双击任意处理器,并选择属性选项卡以查看其配置。

在激活新数据流之前,处理器*get witter*需要 Twitter 提供的消费者密钥、消费者秘密、访问令牌和访问令牌秘密。通过创建 Twitter 帐户、访问门户网站,然后从下拉导航中选择应用来生成这些值。 <sup>30</sup> 在 Twitter 开发者应用页面上, <sup>31</sup> 点击创建应用按钮,完成所需步骤。一旦 Twitter 批准了新的应用,检索图 9-9 所示的令牌、密钥和秘密。

![img/483120_1_En_9_Fig9_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig9_HTML.jpg)

图 9-9

Apache NiFi Twitter 密钥和令牌

填充图 9-10 所示*get witter*处理器所需的值。

![img/483120_1_En_9_Fig10_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-devops-zh/raw/master/docs/adv-plat-dev-k8s/img/483120_1_En_9_Fig10_HTML.jpg)

图 9-10

配置 Apache NiFi GetTwitter 处理器

新数据流已准备好运行。然而,Elasticsearch 是最终的端点,需要一个索引模板来适当地存储数据字段。下一节将向 Elasticsearch 添加一个索引模板。

#### 准备弹性搜索

模板提供的 NiFi 处理器`PutElasticsearchHttp`将最终处理的数据放入一个 Elasticsearch 索引中,匹配模式`sentiment-${now():format('yyyy-MM')}`,为一年中的每个月创建一个新的索引。`PutElasticsearchHttp`接收并放置由之前的处理器组装的 JSON 数据。这个 JSON 数据结构包含文本、数字和日期值。Elasticsearch 可以检测并自动设置数据类型,但它并不完美,容易被各种日期格式混淆。Elasticsearch 自然无法确定诸如数字零这样的值是整数还是浮点数。通过为 Elasticsearch 提供一个索引模板来实现正确的索引。 <sup>32</sup>

索引模板由基于 JSON 的配置组成,定义了一个或多个字段应该如何被索引。以下命令将端口转发弹性搜索并发布一个与处理后的数据所产生的数据类型相匹配的索引模板。

打开终端和端口转发弹性搜索:

$ kubectl port-forward elasticsearch-0 9200:9200 -n data


打开另一个终端,通过发出清单 9-5 中的命令发布索引模板。

cat <<EOF | curl -X POST
-H "Content-Type: application/json"
-d @- http://localhost:9200/_template/all
{
"index_patterns": ["sentiment-*"],
"settings": {
"number_of_shards": 1
},
"mappings": {
"_source": {
"enabled": true
},
"properties": {
"polarity": {
"type": "float"
},
"subjectivity": {
"type": "float"
},
"sentence_count": {
"type": "integer"
},
"Content-Length": {
"type": "integer"
},
"X-Start-Time": {
"type": "date",
"format": "epoch_millis"
},
"X-Duration-Seconds": {
"type": "float"
},
"twitter.created_at": {
"type": "date",
"format": "EEE MMM dd HH:mm:ss Z yyyy",
"null_value": ""
},
"Date": {
"type": "date",
"format": "EEE, dd MMM yyyy HH:mm:ss z"
}
}
}
}

EOF


Listing 9-5HTTP post an Elasticsearch index template

Elasticsearch 现在能够正确地索引来自数据流的处理过的数据,该数据流由前一节中加载的 NiFi 模板定义。下面的部分启动数据流并查询处理后的数据。

数据流

本章前面作为模板加载的示例 ETL 数据流水线从 Twitter 提取数据,并将其发布到 Apache Kafka 主题。另一组处理器消耗来自 Kafka 主题的数据,获得 Twitter 消息的文本,并将其发送到 OpenFaaS 情感分析功能。最后一组处理器将情感分析的结果与来自原始数据的字段结合起来,并将结果作为 JSON 发布到 Elasticsearch 进行索引。

Twitter 产生了高速、无休止的半结构化数据流,代表了典型的数据处理场景。在这个例子中使用 Apache Kafka 不是必须的,只是用来演示附加的 NiFi 处理器。然而,Kafka 的使用允许外部系统有机会对其数据事件流进行操作,从而为扩展流水线提供了更多的机会。

要启动新的数据流,单击 NiFi 画布(网格)上的任意位置,并通过单击 Operate Palette 提供的 play 按钮启动所有数据处理器。

几分钟后,打开终端,端口转发弹性搜索:

$ kubectl port-forward elasticsearch-0 9200:9200 -n data

打开另一个终端,通过发出清单 9-6 中的命令发布一个 Elasticsearch 查询。以下查询将情绪分析中最后一个小时的极性度量聚合到直方图桶中,从-1 到 1,每 0.5 个间隔一次。Elasticsearch 支持一组强大的聚合功能。33

cat <<EOF | curl -X POST \
-H "Content-Type: application/json" \
-d @- http://localhost:9200/sentiment-*/_search
{
  "size": 0,
  "aggs": {
    "polarity": {
      "histogram" : {
          "field" : "polarity",
          "interval" : 0.5,
          "extended_bounds" : {
              "min" : -1,
              "max" : 1
          }
      }
    }
  },
  "query": {
      "range": {
          "Date": {
              "gt": "now-1h"
          }
      }
  }
}
EOF

Listing 9-6HTTP post an Elasticsearch Sentiment Analysis query

示例结果(见清单 9-7 )显示,在过去的一个小时里,关于新冠肺炎的负面推特帖子("doc_count": 40是正面帖子("doc_count": 4的十倍。

{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2276,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "polarity": {
      "buckets": [
        {
          "key": -1,
          "doc_count": 40
        },
        {
          "key": -0.5,
          "doc_count": 404
        },
        {
          "key": 0,
          "doc_count": 1718
        },
        {
          "key": 0.5,
          "doc_count": 110
        },
        {
          "key": 1,
          "doc_count": 4
        }
      ]
    }
  }
}

Listing 9-7Example aggregation output from Elasticsearch Sentiment Analysis query

示例 ETL 数据流试图展示 Apache NiFi 提供的众多特性中的一小部分,以及 Kubernetes 为数据管理、存储和处理系统的近乎无缝的互连提供理想平台的能力。本书中的平台展示了 Kubernetes 对各种各样的应用的处理,从 NiFi、Elasticsearch 和 Kafka 等大型应用到无服务器功能,这些应用被包装在容器中,通过统一的网络和控制面板跨多个服务器进行部署、监控和管理。

下一节利用 JupyterLab 环境,展示实时实验和与平台数据交互的能力。

分析和程序控制

在第六章中安装和配置的 JupyterHub 提供了 JupyterLab 环境,方便了一个或多个直接在集群中运行的 Jupyter 笔记本的操作。以下两个练习演示了在 Elasticsearch 中索引数据的简单查询和可视化,以及以编程方式开发 NiFi 数据流的能力。

分析和可视化

这个例子使用了 JupyterHub 提供的基于 Python 的 Jupyter 笔记本(见第六章)。当数据流入 Elasticsearch 时,它会立即被索引,并可通过其所有字段进行搜索。该示例从 Elasticsearch 索引中返回多达 10,000 条记录,以情绪开始,并且日期字段值在最后一个小时内。

打开一个新的基于 Python 的 Jupyter 笔记本,将下面的每个代码块添加到单独的单元格中。

通过向第一个单元添加以下命令来安装 Elasticsearch 包版本 7.6.0:

!pip install elasticsearch==7.6.0

导入elasticsearchpandasmatplotlib:

from elasticsearch import Elasticsearch
import pandas as pd
from matplotlib import pyplot

创建一个连接到在 Kubernetes 名称空间数据中运行的 Elasticsearch 服务的 elasticsearch 客户端:

es = Elasticsearch(["elasticsearch.data"])

使用 Elasticsearch 客户端的搜索功能来查询索引模式“情绪-*”,并将结果存储在变量 response 中:

response = es.search(
    index="sentiment-*",
    body={
        "size": 10000,
        "query": {
            "range": {
                "Date": {
                    "gt": "now-1h"
                }
            }
        },
        "_source": [
            "Date",
            "polarity",
            "subjectivity" ],
    }
)

将 Elasticsearch 的回答映射并转置到 Pandas 数据框架中:

df = pd.concat(map(pd.DataFrame.from_dict,
                   response['hits']['hits']),
               axis=1)['_source'].T

将日期列转换为 Python 日期时间数据类型:

datefmt = '%a, %d %b %Y %H:%M:%S GMT'
df['Date'] = pd.to_datetime(df['Date'], format=datefmt)

将日期字段分配给数据帧索引,并将所有数值转换为浮点数:

df = df.set_index(['Date'])
df = df.astype(float)

打印前五条记录(如图 9-11 ):

img/483120_1_En_9_Fig11_HTML.jpg

图 9-11

情感分析数据框架行示例

df.head()

最后,通过调用数据帧的绘图函数绘制情绪,将极性分配给 y 轴(见图 9-12 ):

img/483120_1_En_9_Fig12_HTML.jpg

图 9-12

情感分析数据框架图

df.plot(y=["polarity"], figsize=(13,5))

前面的例子是数据分析和可视化的基本示例。数据科学家或分析师的第一步可能包括类似的任务,以形成对可用数据的粗略了解。机器学习等数据科学活动通常需要不可变/固定的数据集,以促进可重复的实验。将集群内 Jupyter 笔记本电脑与 MinIO 对象存储(在第七章中安装)以及事件队列、数据管理和 ETL 系统相连接的能力,为高效构建和共享这些有价值的数据集提供了许多机会。

Kubernetes 支持的 JupyterLab 环境提供了一个合适的平台,通过内部公开的 API(如 Apache NiFi)对集群资源进行交互式编程控制;下一节将介绍一个简单的例子。

编程 NiFi

Apache NiFi 支持通过用 Java 编写的定制控制器和处理器进行扩展。然而,NiFi 强大的标准处理器意味着许多项目将会找到一套适用于多种情况的处理器。NiFi 功能的另一个扩展是通过 API, 35 促进自动化和监控。本节包含一个创建 NiFi 进程组并使用单个处理器填充它的简短示例。

打开一个新的基于 Python 的 Jupyter 笔记本,将下面的每个代码块添加到单独的单元格中。

通过向第一个单元添加以下命令,安装 NiPyApi36Python 包版本 1.14.3:

!pip install nipyapi==0.14.3

导入包:

import nipyapi

用 API 端点配置 NiFi 客户机,在本例中是在data名称空间中的 Kubernetes 服务nifi:

api_url = "http://nifi.data:8080/nifi-api"
nipyapi.utils.set_endpoint(api_url)

通过检索群集中第一个节点的信息来测试客户端连接;图 9-13 描述了示例输出:

img/483120_1_En_9_Fig13_HTML.jpg

图 9-13

NiFi Python 客户端节点输出

nodes = nipyapi.system.get_cluster().cluster.nodes
nodes[0]

创建一个 NiFi 进程组 37 ,并将其放置在画布上(在本章前面添加的处理器之上):

pg0id = nipyapi.canvas.get_process_group(
    nipyapi.canvas.get_root_pg_id(),
    'id'
)

pg0 = nipyapi.canvas.create_process_group(
    pg0id,
    "apk8s_process_group_0",
    location=(800.0, 200.0)
)

在使用前一个示例中的代码执行单元格后,访问 NiFi web 界面,注意前面添加的处理器上方的新apk8s_process_group_0进程组,如图 9-14 所示。双击新的流程组会显示一个空白的画布。

img/483120_1_En_9_Fig14_HTML.jpg

图 9-14

Python API 客户端添加的 NiFi 进程组

在之前创建的新 NiFi 进程组中创建一个 GenerateFlowFile38处理器,并将其放置在画布上:

gf = nipyapi.canvas.get_processor_type('GenerateFlowFile')

p0 = nipyapi.canvas.create_processor(
    parent_pg=pg0,
    processor=gf,
    location=(250.0, 0.0),
    name="apk8s_processor_0",
    config=nipyapi.nifi.ProcessorConfigDTO(
        scheduling_period='1s',
        auto_terminated_relationships=['success']
    )
)

在使用前面示例中的代码执行单元格后,访问 NiFi web 界面,双击新的流程组,并查看新创建的 GenerateFlowFile,如图 9-15 所示。

img/483120_1_En_9_Fig15_HTML.jpg

图 9-15

Python API 客户端添加的 NiFi 处理器

这个部分自动创建一个 NiFi 进程组和一个 NiFi GenerateFlowFile 处理器。这个例子展示了数据流开发的粒度方法。NiFi API 和 NiPyApi Python 包还支持模板的安装和配置,允许开发人员设计各种完整的数据流并将其保存为模板,如 Twitter 情感 v2(在本章前面添加),使它们可用于编程配置、部署和监控。

摘要

本章安装了无服务器平台 OpenFaaS 和数据路由和转换平台 Apache NiFi(参见清单 9-8 ),展示了与前几章安装的其他数据管理组件的互连性,特别是 Apache Kafka、Elasticsearch 和 JupyterLab。本章展示了 Twitter 消息的提取、转换、加载、处理和分析,所有这些都不需要定制代码,但提供了许多代码扩展方法,从为 OpenFaaS 编写定制(无服务器)函数,到通过 JupyterLab 中的 Python 与 Elasticsearch、Kafka 和 NiFi 交互。

本书旨在展示在 Kubernetes 上快速组装、管理和监控数据平台的简易性。与 Kubernetes 的集成程度非常广泛。OpenFaaS 和 JupyterHub 等软件利用 Kubernetes API 本身来部署和扩展 pod,而 NiFi 等其他软件在运行时并不了解 Kubernetes。

本书中开发的 Kubernetes 数据平台运行在一个小规模、资源受限、四节点的开发集群上,每天只需花费几美元。然而,这个小集群涵盖了许多基本的数据处理概念,包括数据事件、索引、处理、数据库、数据湖、数据仓库、分布式查询执行、现代 ETL 操作和数据科学环境。这些能力对于希望收集、处理和分析各种数据的组织来说是必不可少的。物联网和机器学习是对数据管理有大量需求的概念的例子,从高速实时非结构化和半结构化数据的收集到用于训练和提炼机器学习模型的经过处理、标准化、结构良好的数据目录。

./009-cluster-apk8s-dev5
├── 000-cluster
├── 003-data
│   ├── 000-namespace
│   ├── 005-keycloak
│   ├── 010-zookeeper
│   ├── 020-kafka
│   ├── 030-elasticsearch
│   ├── 032-logstash
│   ├── 034-kibana
│   ├── 050-mqtt
│   ├── 060-cassandra
│   ├── 070-minio
│   ├── 080-mysql
│   ├── 085-hive
│   ├── 095-presto
│   ├── 100-jupyterhub
│   ├── 120-openfaas
│   └── 150-nifi
└── 005-data-lab
    └── 000-namespace

Listing 9-8Organization of Kubernetes-based data platform components

十、平台化区块链

区块链是一个概念,能够维护分散和不可信的交易数据库。2009 年发布的比特币网络 1 通过实现一种在共享公共账本上生成和交易加密货币的方法,普及了区块链技术。尽管比特币仍然是最受欢迎的加密货币形式,但新的区块链技术已经崛起,将功能扩展到货币兑换之外。

这本书关注以太坊,比特币最成功的替代品之一。虽然比特币对智能合约有一些能力,但以太坊原生支持从 Solidity 编译的分布式应用的存储和执行,Solidity 是其用于智能合约的 DSL(域特定语言)。

以业务为中心的区块链技术越来越多,包括 Hyperledger Fabric、 3 Corda、 4 和 Quorum。 5 这些区块链应用直接关注许可或私有的区块链网络,是一些组织的绝佳选择。然而,以太坊 6 支持私有和许可的区块链及其巨大成功的公共网络。 7 开发基于 Solidity 的 8 应用意味着可以移植到任何以太坊实现:公共的、私有的或许可的。许可以太网将在本章后面讨论。

这本书实现了一个封闭的、私有的以太坊网络,适合实验和开发。由于一方控制节点的一致性,私有集中式以太网几乎没有生产应用。开发环境如 Truffle Suite 为个人开发者提供了优秀的交钥匙解决方案;然而,运行私有以太网有助于理解公共网络,并提供一个受控的多租户开发环境。

私有区块链平台

本章重点介绍构建一个私有以太坊区块链网络,其操作类似于全球公共以太坊网络。在 Kubernetes 中运行 Ethereum 节点,无论是公共的、私有的还是受保护的,都可以提供这个优雅的应用平台的所有优势,包括统一的网络和控制面板、容错和自修复、声明式配置、监控以及跨大量服务器扩展的工作负载的透明分布。

图 10-1 代表了本章的一个高级目标,通过无服务器功能(使用 OpenFaaS)的应用开发和多租户 JupyterHub 提供的 Jupyter 笔记本的实验和开发,使以太坊节点平台化并与之交互。Kubernetes 促进了这些不同应用的混合,并为组装支持新技术的奇异平台提供了许多机会。

img/483120_1_En_10_Fig1_HTML.jpg

图 10-1

区块链网络开发平台

图 10-1 表示在数据平台的背景下汇集在一起的无服务器、区块链和数据科学环境。

发展环境

以下区块链开发平台利用了第六章中首次提到的廉价 Hetzner 集群,包括一个用于 Kubernetes 主节点的 CX21 (2 个 vCPU/8G RAM/40G SSD)和四个用于 worker 节点的 CX41 (4 个 vCPU/16G RAM/160G SSD)实例。任何等效的基础设施都将适应以下练习。

本章创建了一个名为eth的新 Kubernetes 集群,并利用了第 3 、 5 和 6 章中安装的应用和集群配置(如表 10-1 中所述)。本章将所有配置清单组织在文件夹cluster-apk8s-eth下。

表 10-1

从前面章节收集的关键应用和配置

|   |

资源

|

组织

|
| --- | --- | --- |
| 第三章 | 进入证书管理器仓库监视 | 000-cluster/00-ingress-nginx``000-cluster/10-cert-manager``000-cluster/20-rook-ceph``000-cluster/30-monitoring |
| 第五章 | 命名空间 | 003-data/000-namespace |
| 第六章 | 凯克洛克 JupyterHub | 003-data/005-keycloak``005-data-lab/000-namespace``003-data/100-jupyterhub |
| 第九章 | OpenFaaS | 003-data/120-openfaas |

私有以太网

下面几节组装一个以太坊区块链开发集群(见图 10-2 ),模仿一个公共以太坊网络的一般操作。公共、私有或许可系统的基本组件是相同的。

Miner 节点(也称为 Full 节点)是以太网的核心组件。以太坊节点可以是实现以太坊协议的任何应用。本章使用 Go 中开发的 Geth, 9 作为以太坊协议最初的三个实现之一。Geth 提供了一个独立的二进制文件和一个开源库,适合构建实现以太坊协议的定制应用/节点。本章使用包装在容器(ethereum/client-go)中的 Geth 二进制文件,表示三种类型的节点:boot nodes(Geth 包中的一个单独的二进制文件)、矿工和事务节点。

img/483120_1_En_10_Fig2_HTML.jpg

图 10-2

私有以太网

以太坊是一个点对点的节点网络,使用已建立的启动节点 10 将新节点连接到网络。boot nodes11不挖掘或提交事务;它们只负责初始对等体发现。Geth 附带一个主网络和测试网络 Bootnode 地址列表;但是,本章中的私有以太坊网络要求 Geth 使用本地 Bootnodes。本章使用 Bootnode 注册服务来提供本地 boot node 的地址。

事务节点是可选的;任何节点,包括矿工,都可以提交预先设计的交易,以纳入区块链。矿工节点使用附属以太坊账户 12 来签署他们的交易;允许远程连接到这些节点为任何人提供了提交由矿工签名的事务的访问权。交易节点只能发送由最终用户签名的交易,因为没有附加的私人帐户。

最后,Geth 提供了向 API 端点报告其当前状态和指标的能力。本章利用 Ethstats 项目 13 来收集节点指标,并将它们呈现在 web 仪表板上。

根节点

以太坊引导节点通过提供一组初始对等点来帮助新节点引导到以太坊网络中。Bootnodes 是以太坊客户端实现的子集,只参与网络节点发现协议。Bootnodes 不实现任何更高级别的以太坊应用协议。

以下配置设置了 eth-bootnode 服务。对这个无头服务的 DNS 请求返回匹配选择器app: eth-bootnode的任何 pod 的内部主机名。这个服务可以方便地为 Bootnodes 生成注册表,这将在下一节中介绍。

创建目录cluster-apk8s-eth/003-data/200-eth/10-bootnode。在新的10-bootnode目录中,从清单 10-1 中创建一个名为10-service.yml的文件。

apiVersion: v1
kind: Service
metadata:
  name: eth-bootnode
  namespace: data
  labels:
    app: eth-bootnode
spec:
  selector:
    app: eth-bootnode
  clusterIP: None
  ports:
    - name: discovery
      port: 30301
      protocol: UDP
    - name: http
      port: 8080

Listing 10-1Bootnode Service

应用以太坊 Bootnode 服务配置:

$ kubectl apply -f 10-service.yml

接下来,在清单 10-2 中的文件30-deployment.yml中为以太坊引导节点创建一个部署。以下 Bootnode 部署由两个容器和一个用于初始化的容器组成。初始化容器生成一个密钥,并将其存储在与 Pod 中的其他容器共享的卷挂载data中。Bootnode 使用该密钥创建其 enode 标识符(以太坊节点的唯一 ID)。

这两个容器中的第一个是 Ethereum Bootnode,它安装在初始化容器中生成的密钥,并在端口 30301/UDP 上通信。第二个容器名为bootnode-server,使用 netcat 执行一个小 shell 脚本,通过端口 8080 回显 Bootnode 的完整以太坊地址。 14 完整地址由 enode 标识符、IP 地址和端口组成。下一节中的 Bootnode 注册器使用 headless 服务(在上一节中配置)来发现 Bootnode Pods,然后通过 HTTP 端口 8080 检索它们的每个以太网地址,由bootnode-server容器提供服务。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: eth-bootnode
  namespace: data
  labels:
    app: eth-bootnode
spec:
  replicas: 2
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: eth-bootnode
  template:
    metadata:
      labels:
        app: eth-bootnode
    spec:
      volumes:
        - name: data
          emptyDir: {}
      initContainers:
        - name: genkey
          image: ethereum/client-go:alltools-v1.9.13
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh"]
          args:
            - "-c"
            - "bootnode --genkey=/etc/bootnode/node.key"
          volumeMounts:
            - name: data

              mountPath: /etc/bootnode
      containers:
        - name: bootnode
          image: ethereum/client-go:alltools-v1.9.13
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              cpu: ".5"
            requests:
              cpu: "0.25"
          command: ["/bin/sh"]
          args:
            - "-c"
            - "bootnode --nodekey=/etc/bootnode/node.key --verbosity=4"
          volumeMounts:
            - name: data
              mountPath: /etc/bootnode
          ports:
            - name: discovery
              containerPort: 30301
              protocol: UDP
        - name: bootnode-server
          image: ethereum/client-go:alltools-v1.9.13
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while [ 1 ]; do echo -e \"HTTP/1.1 200 OK\n\nenode://$(bootnode -writeaddress --nodekey=/etc/bootnode/node.key)@$(POD_IP):30301\" | nc -l -v -p 8080 || break; done;"
          volumeMounts:
            - name: data
              mountPath: /etc/bootnode
          env:
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          ports:
            - containerPort: 8080

Listing 10-2Bootnode Deployment

该集群现在包含两个以太坊启动节点;然而,新的以太坊节点需要完整的 enode 地址(enode://ENODE_IDENTIFIER@POD_IP:30301)才能使用。如前所述,Kubernetes headless 服务 eth-bootnode 提供各个节点的主机名,而容器bootnode-server通过 HTTP 在端口 8080 上报告其 enode 地址。下一节中定义的 Bootnode 注册器结合了这两个操作。

bootmode registrar(引导模式注册)

Bootnode 注册器是一个小型 Golang 应用,用于查找由eth-bootnode headless 服务公开的 DNS 条目,查询每个 Pod 的 enode 地址,并通过一个简单的 HTTP 请求返回一个逗号分隔的字符串。后面的部分用这个 Bootnode 注册器的 enode 地址串配置每个 Geth 节点。

Note

潘石屹 15 写了 Bootnode 注册商申请,16连同 Kubernetes 以太坊的掌 Helm 图。 17 本章的概念和灵感来自这些项目和微软开发者博客文章构建私有以太坊联盟18

创建目录cluster-apk8s-eth/003-data/200-eth/20-bootnode-reg。在新的20-bootnode-reg目录中,从清单 10-3 中创建一个名为10-service.yml的文件。

apiVersion: v1
kind: Service
metadata:
  name: eth-bootnode-registrar
  namespace: data
  labels:
    app: eth-bootnode-registrar
spec:
  selector:
    app: eth-bootnode-registrar
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 9898

Listing 10-3Bootnode Registrar Service

应用以太坊 Bootnode Regis.trar 服务配置:

$ kubectl apply -f 10-service.yml

接下来,在清单 10-4 中名为30-deployment.yml的文件中为以太坊 Bootnode 注册器创建一个部署。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: eth-bootnode-registrar
  namespace: data
  labels:
    app: eth-bootnode-registrar
spec:
  replicas: 1
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: eth-bootnode-registrar
  template:
    metadata:
      labels:
        app: eth-bootnode-registrar
    spec:
      containers:
        - name: bootnode-registrar
          image: jpoon/bootnode-registrar:v1.0.0
          imagePullPolicy: IfNotPresent
          env:
            - name: BOOTNODE_SERVICE
              value: "eth-bootnode.data.svc.cluster.local"
          ports:
            - containerPort: 9898

Listing 10-4Bootnode Deployment

Kubernetes 集群现在包含一个 Bootnode 注册服务。稍后,Geth miner 和事务节点部署在初始化时调用此服务,以提供 Ethereum Bootnode 地址列表。

Ethstats

Geth 节点在配置了 Ethstats 端点时会发出度量。本节配置的 Ethstats web dashboard19摄取以太坊指标并呈现在一个有吸引力的 web 界面 20 (如图 10-3 )。本节设置稍后在提供给 Geth 节点的命令行参数中使用的 Ethstats 服务、部署和机密。

img/483120_1_En_10_Fig3_HTML.jpg

图 10-3

ethstats.net 以太坊网络统计

创建目录cluster-apk8s-eth/003-data/200-eth/30-ethstats。在新的30-ethstats目录中,从清单 10-5 中创建一个名为10-service.yml的文件。

apiVersion: v1
kind: Service
metadata:
  name: eth-ethstats
  namespace: data
  labels:
    app: eth-ethstats
spec:
  selector:
    app: eth-ethstats
  type: ClusterIP
  ports:
    - port: 8080
      targetPort: http

Listing 10-5Ethstats Service

应用 Ethstats 服务配置:

$ kubectl apply -f 10-service.yml

接下来,在清单 10-6 中的一个名为15-secret.yml的文件中为 Ethstats 创建一个秘密。

apiVersion: v1
kind: Secret
metadata:
  name: eth-ethstats
  namespace: data
  labels:
    app: eth-ethstats
type: Opaque
stringData:
  WS_SECRET: "uGYQ7lj55FqFxdyIwsv1"

Listing 10-6Ethstats Secret

应用 Ethstats 服务配置:

$ kubectl apply -f 15-secret.yml

接下来,在清单 10-7 中的一个名为30-deployment.yml的文件中为 Ethstats 创建一个部署。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: eth-ethstats
  namespace: data
  labels:
    app: eth-ethstats
spec:
  replicas: 1
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: eth-ethstats
  template:
    metadata:
      labels:
        app: eth-ethstats
    spec:
      containers:
        - name: ethstats
          image: ethereumex/eth-stats-dashboard:v0.0.1
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 3000
          env:
            - name: WS_SECRET
              valueFrom:
                secretKeyRef:
                  name: eth-ethstats
                  key: WS_SECRET

Listing 10-7Ethstats Deployment

应用 Ethstats 部署配置:

$ kubectl apply -f 30-deployment.yml

接下来,在清单 10-8 中的一个名为50-ingress.yml的文件中为 Ethstats 创建一个入口配置。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: eth-ethstats
  namespace: data
  labels:
    app: eth-ethstats
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
spec:
  rules:
    - host: stats.data.eth.apk8s.dev
      http:
        paths:
          - backend:
              serviceName: eth-ethstats
              servicePort: 8080
            path: /
  tls:
    - hosts:
        - stats.data.eth.apk8s.dev
      secretName: eth-ethstats-production-tls

Listing 10-8Ethstats Ingress

应用 Ethstats 入口配置:

$ kubectl apply -f 50-ingress.yml

最后,在网络浏览器中访问 https://stats.data.eth.apk8s.dev 。在 Geth 节点按照以下部分中的配置开始报告之前,应该没有数据。

Geth 矿工

本书不探究以太坊协议的细节。不过这一节和下一节安装了 Geth 支持的两种以太坊节点:矿工节点 21 和事务节点。以太网上的所有节点都进行对等通信,共享网络拓扑、区块链状态和事务。Miner 节点致力于在由任何未决事务组成的区块链上创建新的区块。 22

创建目录cluster-apk8s-eth/003-data/200-eth/40-miner。在新的40-miner目录中,从清单 10-9 中创建一个名为15-secret.yml的文件。Geth 矿工需要一个以太坊帐户,用于签署交易和接收采矿奖励。一个初始化容器,稍后在每个 Geth 部署中定义,用清单 10-9 中定义的密码创建一个以太坊帐户。

apiVersion: v1
kind: Secret
metadata:
  name: eth-geth-miner
  namespace: data
  labels:
    app: eth-geth-miner
type: Opaque
stringData:
  accountsecret: "strongpassword"

Listing 10-9Geth Secret

应用 Geth Secret 配置:

$ kubectl apply -f 15-secret.yml

按照在线安装文档在本地工作站上安装 Geth。 23 Geth 提供所有主流操作系统和大部分软件包管理系统的安装程序。例如,装有自制软件的 MAC 可能会发出这样的命令:

$ brew install geth.

创建两个或更多以太坊账户。以下配置图中定义的以太坊起源文件指示新的区块链向这些账户(在第一个区块中)预注入指定数量的以太坊(以太坊加密货币)以供在专用网络中使用。

$ geth account new

使用geth account new命令创建多个帐户后,从输出中复制并保存“密钥的公共地址:”。接下来,在清单 10-10 中名为20-configmap.yml的文件中为 Geth 创建一个 ConfigMap。用新创建的账户更新genesis.jsonalloc部分。

清单 10-10 中的 ConfigMap 中定义的genesis.json文件配置以太坊区块链的第一个区块。任何希望加入私有网络的节点必须首先根据这个以太坊起源文件进行初始化。 24 下面描述的 miner 和 transaction 节点都被配置为挂载在 ConfigMap 中定义为 key 的 genesis.json 文件(清单 10-10 )。

apiVersion: v1
kind: ConfigMap
metadata:
  name: eth-geth
  namespace: data
  labels:
    app: eth-geth
data:
  networkid: "27587"
  genesis.json: |-
    {
        "config": {
            "chainId": 27587,
            "homesteadBlock": 0,
            "eip150Block": 0,
            "eip155Block": 0,
            "eip158Block": 0
        },
        "difficulty": "0x400",
        "gasLimit": "0x8000000",
        "nonce"   : "0x0000000000000000",
        "alloc": {
          "0xFa4087D3688a289c9C92e773a7b46cb9CCf80353": { "balance": "100000000000000000000" },
          "0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC": { "balance": "200000000000000000000" }
        }
    }

Listing 10-10Geth ConfigMap

应用 Geth 配置映射配置:

$ kubectl apply -f 20-configmap.yml

接下来,在清单 10-11 中名为30-deployment.yml的文件中为 Geth 创建一个部署配置。清单 10-11 中的 Geth miner 部署建立了一个由所有容器挂载的data卷,以及一个先前从清单 10-10 中的配置映射应用的配置卷。

第一个初始化容器init-genesis针对从 ConfigMap 挂载的 Genesis 文件运行 Geth init命令,并在挂载为/root/.ethereum的共享data卷中创建新的以太坊区块链数据库。

第二个初始化容器create-account使用清单 10-9 中应用的秘密eth-geth-miner中定义的密码为 Geth miner 创建一个唯一的以太坊账户。Geth 将新的以太坊账户存储在/root/.ethereum中,挂载为共享的data卷。

最后的初始化容器get-bootnodes运行一个小的 shell 脚本,试图通过调用本章前面配置的 Bootnode 注册器的curl来检索 boot node 列表。如果成功,curl调用的输出将返回的(用逗号分隔的)bootnodes 列表写入安装在共享卷data中的文件/geth/bootnodes

初始化后,eth-geth-miner部署中定义的 pod 启动geth-miner容器并挂载共享卷data ( /root/.ethereum),其中初始化容器初始化区块链数据库,创建以太坊帐户,并存储一个包含 Bootnode 地址列表的文件。geth-miner容器使用以下参数执行geth:--bootnodes定义初始的 bootnodes 集合来查找对等体。--mine指示geth作为矿工操作,尝试创建砖块。--minerthreads设置并行挖掘线程的数量。--nousb禁用 USB 硬件钱包检查。--miner.etherbase取一个指向以太坊账户的索引,用来收集采矿奖励;在这种情况下,初始化容器 create-account 生成了第一个(也是唯一一个)帐户(索引为零)。--networkid指示geth连接到特定网络,在这种情况下,如eth-geth配置图中所定义。--ethstats接受能够从geth;接收指标的端点,在本例中,是前面部分配置的 Ethstats 仪表板。最后,--verbosity设置要输出的测井深度。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: eth-geth-miner
  namespace: data
  labels:
    app: eth-geth-miner
spec:
  replicas: 3
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: eth-geth-miner
  template:
    metadata:
      labels:
        app: eth-geth-miner
    spec:
      volumes:
        - name: data
          emptyDir: {}
        - name: config
          configMap:
            name: eth-geth
      initContainers:
        - name: init-genesis
          image: ethereum/client-go:v1.9.13
          imagePullPolicy: IfNotPresent
          args:
            - "init"
            - "/var/geth/genesis.json"
          volumeMounts:
            - name: data
              mountPath: /root/.ethereum
            - name: config
              mountPath: /var/geth
        - name: create-account
          image: ethereum/client-go:v1.9.13
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh"]
          args:
            - "-c"
            - "printf '$(ACCOUNT_SECRET)\n$(ACCOUNT_SECRET)\n' | geth account new"
          env:
            - name: ACCOUNT_SECRET
              valueFrom:
                secretKeyRef:
                  name: eth-geth-miner
                  key: accountsecret
          volumeMounts:

            - name: data
              mountPath: /root/.ethereum
        - name: get-bootnodes
          image: ethereum/client-go:v1.9.13
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh"]
          args:
            - "-c"
            - |-
              apk add --no-cache curl;
              CNT=0;
              echo "retrieving bootnodes from $BOOTNODE_REGISTRAR_SVC"
              while [ $CNT -le 90 ]
              do
                curl -m 5 -s $BOOTNODE_REGISTRAR_SVC | xargs echo -n >> /geth/bootnodes;
                if [ -s /geth/bootnodes ]
                then
                  cat /geth/bootnodes;
                  exit 0;
                fi;

                echo "no bootnodes found. retrying $CNT...";
                sleep 2 || break;
                CNT=$((CNT+1));
              done;
              echo "WARNING. unable to find bootnodes.";
              exit 0;
          env:
            - name: BOOTNODE_REGISTRAR_SVC
              value: eth-bootnode-registrar
          volumeMounts:
            - name: data
              mountPath: /geth
      containers:

        - name: geth-miner
          image: ethereum/client-go:v1.9.13
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh"]
          args:
            - "-c"
            - "geth --bootnodes=\"`cat /root/.ethereum/bootnodes`\" --mine --minerthreads=1 --nousb --miner.etherbase=0 --networkid=${NETWORK_ID} --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} --verbosity=3"
          env:
            - name: ETHSTATS_SVC
              value: eth-ethstats:8080
            - name: ETHSTATS_SECRET
              valueFrom:
                secretKeyRef:
                  name: eth-ethstats
                  key: WS_SECRET
            - name: NETWORK_ID
              valueFrom:
                configMapKeyRef:
                  name: eth-geth
                  key: networkid
          ports:
            - name: discovery-udp
              containerPort: 30303
              protocol: UDP
            - name: discovery-tcp
              containerPort: 30303
          volumeMounts:
            - name: data
              mountPath: /root/.ethereum
          resources:
            limits:
              cpu: "400m"
            requests:
              cpu: "400m"

Listing 10-11Geth Deployment

应用 Geth 部署配置:

$ kubectl apply -f 30-deployment.yml

在应用 eth-geth-miner 部署并且三个副本 pod 已经初始化之后,矿工开始生成 DAG(有向非循环图),表示为以太坊的 PoW(工作证明)协议中使用的一个或多个千兆字节的数据。在这个高度受限的开发集群上,预计这些过程需要 20 分钟到一个小时。一旦 geth 矿工完成 DAG 生成,采矿开始,并且随着矿工添加区块,区块链开始增长。

新的采矿池大约每 15 30 秒向链中添加块,这取决于 CPU 资源和网络校准的以太坊 PoW 难度等级。虽然最初在早期定义的 Genesis 文件中设置为低 0x400,但以太坊网络根据添加到链中的块之间的时间差来校准难度。

最初的一组矿工继续无限期地构建区块链,无论每个区块中是否包含交易。下一节添加了专门用于与私有以太坊区块链交互的节点,包括提交事务。

获取事务节点

本节配置启用了 RPC(远程过程调用)管理 API 的 Geth 节点。以下事务节点实现了完整的以太坊协议,但没有启用挖掘,因此不需要以太坊帐户。交易节点可能只提交由外部以太坊账户预先设计的交易,使其成为与区块链进行外部通信的合适网关。稍后,本章将通过 Jupyter 笔记本和无服务器功能演示与区块链的交互。

创建目录cluster-apk8s-eth/003-data/200-eth/50-tx。在新的50-tx目录中,从清单 10-12 中创建一个名为10-service.yml的文件。

apiVersion: v1
kind: Service
metadata:
  name: eth-geth-tx
  namespace: data
  labels:
    app: eth-geth-tx
spec:
  selector:
    app: eth-geth-tx
  type: ClusterIP
  ports:
    - name: rpc
      port: 8545
    - name: ws
      port: 8546

Listing 10-12Geth transaction node Service

应用 Geth 事务节点服务配置:

$ kubectl apply -f 10-service.yml

接下来,在清单 10-13 中名为30-deployment.yml的文件中为 Geth 事务节点创建一个部署配置。

eth-geth-tx部署与前一节中配置的eth-geth-miner几乎相同,只有一些关键的不同。Geth eth-geth-tx部署不初始化以太坊帐户,也不设置--mine--minerthreads--miner.etherbase命令行选项。(事务模式)geth 的新命令行选项包括: - rpc 启用 HTTP-RPC 25 服务器,支持端口 8548 上的远程连接; - rpcaddr 设置 HTTP-RPC 服务器监听接口,这里是全部(IP 0 . 0 . 0 . 0); - rpcapi 设置 api 以通过 HTTP-RPC 接口启用,在本例中为 eth、net 和 web326-rpcvhosts设置允许连接的域列表(由服务器强制执行);并且 - rpccorsdomain 为跨来源请求设置(由 web 浏览器强制)允许的域。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: eth-geth-tx
  namespace: data
  labels:
    app: eth-geth-tx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: eth-geth-tx
  template:
    metadata:
      labels:
        app: eth-geth-tx
    spec:
      volumes:
        - name: data
          emptyDir: {}
        - name: config
          configMap:
            name: eth-geth
      initContainers:
        - name: init-genesis
          image: ethereum/client-go:v1.9.13
          imagePullPolicy: IfNotPresent
          args:
            - "init"
            - "/var/geth/genesis.json"
          volumeMounts:
            - name: data
              mountPath: /root/.ethereum
            - name: config
              mountPath: /var/geth
        - name: get-bootnodes
          image: ethereum/client-go:v1.9.13
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh"]
          args:

            - "-c"
            - |-
              apk add --no-cache curl;
              COUNT=0;
              echo "calling $BOOTNODE_REGISTRAR_SVC"
              while [ $COUNT -le 100 ]
              do
                curl -m 5 -s $BOOTNODE_REGISTRAR_SVC | xargs echo -n >> /geth/bootnodes;
                if [ -s /geth/bootnodes ]
                then
                  cat /geth/bootnodes;
                  exit 0;
                fi;

                echo "Attempt $COUNT. No bootnodes found...";
                sleep 2 || break;
                COUNT=$((COUNT+1));
              done;
              echo "ERROR: Unable to find bootnodes.";
              exit 0;
          env:
            - name: BOOTNODE_REGISTRAR_SVC
              value: eth-bootnode-registrar
          volumeMounts:
            - name: data
              mountPath: /geth
      containers:
        - name: geth-tx
          image: ethereum/client-go:v1.9.13
          imagePullPolicy: IfNotPresent
          command: ["/bin/sh"]
          args:
            - "-c"
            - "geth --nousb --bootnodes=`cat /root/.ethereum/bootnodes` --rpc --rpcaddr='0.0.0.0' --rpcapi=eth,net,web3 --rpcvhosts='*' --rpccorsdomain='*' --ws --networkid=${NETWORK_ID} --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} --verbosity=2"

          env:
            - name: ETHSTATS_SVC
              value: eth-ethstats:8080
            - name: ETHSTATS_SECRET
              valueFrom:
                secretKeyRef:
                  name: eth-ethstats
                  key: WS_SECRET
            - name: NETWORK_ID
              valueFrom:
                configMapKeyRef:
                  name: eth-geth
                  key: networkid
          ports:
            - name: rpc
              containerPort: 8545
            - name: ws
              containerPort: 8546
            - name: discovery-udp
              containerPort: 30303
              protocol: UDP
            - name: discovery-tcp
              containerPort: 30303
          volumeMounts:
            - name: data
              mountPath: /root/.ethereum

Listing 10-13Geth transaction node Deployment

应用 Geth 事务节点部署配置:

$ kubectl apply -f 30-deployment.yml

在这个阶段,现在应该有五个节点向之前配置的 Ethstats 仪表板报告(见图 10-4 ),包括三个矿工和两个事务节点。

img/483120_1_En_10_Fig4_HTML.jpg

图 10-4

Ethstats 私有以太坊节点报告

这个网络是一个高度受限的公共以太网的微型复制品。诸如此类的私有区块链网络对于构建和部署实验节点、智能合约 27 开发,以及将区块链运营的任何方面连接到贯穿本书开发的更广泛的数据和应用平台都是有用的。在向第三方节点开放该网络之前,请考虑以下关于专用网络的部分。

专用网络

通过外部组织运营的远程节点来扩展这一网络的价值有限。基于电力区块链在小范围内很少有意义。任何能够提供超过 50%的网络挖掘散列率的组织都可以验证一个本来无效的交易,这被称为 51%攻击。希望与一组精选的其他组织一起参与的组织可以将本章中的概念改编为由 Geth 支持的以太坊的新团体共识协议。集团配置节点不挖掘 PoW,而是使用 PoA(权威证明 29 )。

转换此网络以使用 Clique 涉及创建一个新的 Genesis 块,该块具有允许对块进行签名的节点的初始列表。请参阅 Ethereum 的详细说明指南,了解如何将该网络转换为 Clique consensus 协议。 三十

区块链互动

通过与节点通信来执行与区块链的交互。全以太坊节点(本书上下文中的 miner 节点)管理区块链的完整副本,并与网络上的其他节点(对等体)进行事务和状态通信。Geth 提供了一个提供外部访问的 HTTP-RPC API。本章前面配置的五个节点中的两个,称为事务节点,提供由服务eth-geth-tx公开的 HTTP-RPC 访问。

获取附件

Geth 提供了一个交互控制台 31 用于与其 API 进行交互。试验该 API 最简单的方法之一是使用 geth 附加到 geth 的另一个本地实例。以下示例在三个 miner 节点之一上执行 geth,并与正在运行的 miner 进行交互:

$ kubectl exec -it -n data eth-geth-miner-789dd75565-gk25b -- geth attach

geth 控制台输出示例:

Welcome to the Geth JavaScript console!

instance: Geth/v1.9.13-stable-cbc4ac26/linux-amd64/go1.14.2
coinbase: 0x284f99f929b49da9d85b2a3dbf606ed38eec393e
at block: 1134 (Fri May 08 2020 06:19:03 GMT+0000 (UTC))
 datadir: /root/.ethereum
 modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

> eth.blockNumber
1159

通过端口转发本章前面设置的eth-geth-tx服务,从本地工作站与 geth 通信,并将本地geth附加到转发的服务。

$ kubectl port-forward svc/eth-geth-tx 8545 -n data
Forwarding from 127.0.0.1:8545 -> 8545
Forwarding from [::1]:8545 -> 8545

在本地工作站上打开另一个终端,并连接 geth:

$ geth attach http://localhost:8545

Geth 的交互式 JavaScript 控制台是探索 API 的好方法。但是,以太坊提供了各种成熟的客户端库,用于构建与以太坊区块链交互的应用。接下来的部分将通过 Jupyter Notebook 提供的交互式 Python 环境和在无服务器平台 OpenFaaS 中开发的小函数来研究 Ethereum 的 Web3 Python 库,返回链中最新块的信息。

木星环境

针对复杂和精密的云架构进行的软件实验和开发通常会在将开发人员和分析师与服务联系起来方面带来独特的挑战。基于云的架构中的许多(如果不是大多数)应用与其他应用进行系统间通信,而没有明确的外部访问方法。端口转发是从本地工作站访问 Kubernetes 集群内部服务的典型方法;然而,基于 web 的 ide 可以作为平台本身的扩展。

Jupyter 笔记本是一个基于浏览器(或基于 web)的 IDE,需要在 Kubernetes 集群中运行以下示例。第六章描述了 JupyterHub(以及 Keycloak)的配置,该配置用作管理一台或多台 Jupyter 笔记本电脑的 JupyterLab 环境的多租户供应器。从集群中创建新的 Python 3 Jupyter 笔记本;在单个单元格内复制并执行以下代码示例。

导入 Python 库 web3、 32、 json,时间:

import web3, json, time
import pandas as pd
from IPython.display import clear_output
from web3.contract import ConciseContract
from web3 import Web3
from web3.auto.gethdev import w3

连接到 Geth 事务节点:

rpc_ep = "http://eth-geth-tx.data:8545"
web3 = Web3(Web3.HTTPProvider(rpc_ep))

if web3.isConnected():
    print(f"Connected: {rpc_ep}")
    print(f"Peers: {web3.net.peerCount}")
    print(f"Chain ID: {web3.net.version}")
    print(f"Last block: {web3.eth.blockNumber}")
else:
    print("Not connected")

示例输出:

Connected: http://eth-geth-tx.data:8545
Peers: 4
Chain ID: 27587
Last block: 5549

检查在本章前面定义的创世模块中预先注资的账户的 eth 余额:

account_1 = "0xFa4087D3688a289c9C92e773a7b46cb9CCf80353"
account_2 = "0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC"

a1_bal = web3.eth.getBalance(account_1)
a2_bal = web3.eth.getBalance(account_2)

print(f"Account 1: {web3.fromWei(a1_bal, 'ether')} ether")
print(f"Account 2: {web3.fromWei(a2_bal, 'ether')} ether")

示例输出:

Account 1: 100 ether
Account 2: 200 ether

添加以下代码以创建一个事务,将一个以太网转移到 account_2:

nonce = web3.eth.getTransactionCount(account_1)
print(f"Account 1 nonce: {nonce}")

tx = {
    'nonce': nonce,
    'to': account_2,
    'value': web3.toWei(1, 'ether'),
    'gas': 2000000,
    'gasPrice': web3.toWei('50', 'gwei'),
}

tx

示例输出:

{'nonce': 15,
 'to': '0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC',
 'value': 1000000000000000000,
 'gas': 2000000,
 'gasPrice': 50000000000}

Warning

不要使用本章生成的以太坊账户在公共/主以太坊网络上进行任何交易。本书中的示例没有为保护这些帐户提供足够的安全性。

作为account_1签署交易需要私钥文件和密码。在 JupyterLab 环境中,创建一个名为pass1.txt的文本文件,并用本章前面创建account_1时使用的密码填充该文件,这是在genesis.json配置的 alloc 部分中使用的第一个预注资帐户。此外,上传从geth account new命令生成的密钥文件(在本章前面执行以创建预注资以太坊账户)。命名密钥account1.json(见图 10-5 )。

img/483120_1_En_10_Fig5_HTML.jpg

图 10-5

以太坊账户私钥和密码

加载 account_1 的私钥和密码,并对之前创建的事务进行签名:

with open('pass1.txt', 'r') as pass_file:
    kf1_pass = pass_file.read().replace('\n', '')

with open("account1.json") as kf1_file:
    enc_key = kf1_file.read();

p_1 = w3.eth.account.decrypt(enc_key, kf1_pass)
signed_tx = web3.eth.account.signTransaction(tx, p_1)
signed_tx

示例输出:

AttributeDict({'rawTransaction': HexBytes('0xf86d0f850ba43b7400831e8480948ab8f3fc6c660d3f0b22490050c843cafd2c0aac880de0b6b3a7640000801ca0917ae987a8c808cf01221dad4571fd0b1b8f5429d13c469c72bc13647e9c1744a068507c8542ccdebb96e534d13a140ddcbdaedbfa3ba82dcbf86d4b196cc41b1f'),
 'hash': HexBytes('0x9de62dc620274e2c9dba2194d90c245a933af8468ace5f2d38e802da09c06769'),
 'r': 65802530150742945852115878650256413649726940478651153584824595116007827969860,
 's': 47182743427096773798449059805443774712403275692049277894020390344384483433247,
 'v': 28}

将签名的事务发送到事务节点,并检索结果哈希。这个哈希是以太坊区块链上交易的唯一标识符:

signed_tx = signed_tx.rawTransaction
tx_hash = web3.eth.sendRawTransaction(signed_tx)
web3.toHex(tx_hash)

示例输出:

'0x9de62dc620274e2c9dba2194d90c245a933af8468ace5f2d38e802da09c06769'

一个节点收到事务后,它会传播到所有节点进行验证,并包含到未决事务池中,准备好与下一个块一起挖掘。 33 下面的代码每秒查询一次连接的事务节点,直到事务返回一个块号:

%%time
blockNumber = None
check = 0

while type(blockNumber) is not int:
    check += 1
    tx = web3.eth.getTransaction(tx_hash)
    blockNumber = tx.blockNumber
    clear_output(wait=True)

    print(f"Check #{check}\n")
    if type(blockNumber) is not int:
        time.sleep(1)

tx

示例输出:

Check #12

CPU times: user 129 ms, sys: 904 μs, total: 130 ms
Wall time: 11.1 s
AttributeDict({'blockHash': HexBytes('0x676a24aa8117b51958031a2863b17f91ed3356276036a9de7c596124a6234986'),
 'blockNumber': 8050,
 'from': '0xFa4087D3688a289c9C92e773a7b46cb9CCf80353',
 'gas': 2000000,
 'gasPrice': 50000000000,
 'hash': HexBytes('0xa3f02c685ff05b13b164afcbe11d2aa83d2dab3ff972ee7008cc931282587cee'),
 'input': '0x',
 'nonce': 16,
 'to': '0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC',
 'transactionIndex': 0,
 'value': 1000000000000000000,
 'v': 28,
 'r': HexBytes('0x89d052927901e8a7a727ebfb7709d4f9b99362c0f0001f62f37300ed17cb7414'),
 's': HexBytes('0x3ea3b4f5f8e4c10e4f30cc5b8a7ff0a833d8714f20744c289dee86006af420c8')})

交易现已完成,其记录永久存储在私有区块链上。网络通过调整所需的难度,每 10 到 15 秒 34 尝试创建一个新的块; 35 然而,这个只有三个矿工的资源受限网络可能会有很大的波动。本练习中的最后一个代码块查询事务节点中的最后 100 个数据块,并绘制数据块时间戳之间的时间增量:

df = pd.DataFrame(columns=['timestamp'])
for i in range (0,100):
    block = web3.eth.getBlock(tx.blockNumber - i)
    df.loc[i] = [block.timestamp]

df['delta'] = df.timestamp.diff().shift(-1) * -1

df.reset_index().plot(x='index', y="delta", figsize=(12,5))

参见图 10-6 查看块时间戳增量图的输出示例。

img/483120_1_En_10_Fig6_HTML.jpg

图 10-6

块时间戳增量图

本节演示了在 Kubernetes 集群中与专用区块链网络的程序交互性。在 Kubernetes 集群中运行一个或多个以太坊节点,通过简化与现有和定制应用的互连,为扩展和利用区块链概念提供了大量机会。最后一部分部署无服务器功能,创建用于访问区块链数据的公共 API。

无服务器/OpenFaaS

本书在第九章中介绍了无服务器平台 OpenFaaS,并安装了预置的情感分析功能。本节构建并部署一个自定义函数,用于将公共 API 公开到私有区块链网络中。关于使用头盔的安装说明,请参见第九章。以下练习使用入口 URL https://faas.data.eth.apk8s.dev

登录并配置 faas-cli,以便在本地工作站上使用新的 eth 区块链集群:

$ export OPENFAAS_PASS=$(kubectl -n data get secret basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode)

$ export OPENFAAS_URL=https://faas.data.eth.apk8s.dev

$ faas-cli login --gateway=$OPENFAAS_URL \
--password=$OPENFAAS_PASS

拉 OpenFaaS 函数模板python3-http-debian:

$ faas-cli template store pull python3-http-debian

创建目录cluster-apk8s-eth/003-data/200-eth/functions。在新的functions目录中,使用the python3-http-debian模板创建一个名为 last-block 的新 OpenFaaS 函数:

$ faas-cli new last-block --lang python3-http-debian

faas-cli命令创建了文件夹 last-block 和 yaml 文件last-block.yml。如果需要,在本地工作站上安装 Python 3。将目录切换到last-block并创建一个 Python 虚拟环境。这有助于生成一个requirements.txt,稍后用于配置带有所需 Python 包的 OpenFaaS 函数。最后,激活虚拟环境:

$ cd last-block
$ python3 -m venv venv
$ source ./venv/bin/activate

安装 Python 包hexbytesweb3:

$ pip install hexbytes==0.2.0 web3==5.9.0

pip将把hexbytesweb3以及所有依赖包安装到虚拟环境中。新的虚拟环境仅包含运行该功能所需的包。使用piprequirements.txt文件中创建所需包的列表:

$ pip freeze > requirements.txt

通过用清单 10-14 替换 handler.py 中的内容来创建函数。

#!/usr/bin/env python3
""" handler.py
OpenFaaS Blockchain function returning the last block
in the chain.
"""
import os
import json
import hexbytes
from web3 import Web3

def handle(event, context):
    """
    handle a request to the function
    """
    ep_url = "http://eth-geth-tx:8545"
    ep = os.getenv('GETH_RPC_ENDPOINT', ep_url)

    w3 = Web3(Web3.HTTPProvider(ep))
    latest_block = w3.eth.getBlock('latest')
    lbd = latest_block.__dict__

    return {
        "statusCode": 200,
        "body": json.loads(
            json.dumps(lbd, cls=CustomEncoder)
        )
    }

class CustomEncoder(json.JSONEncoder):
    """
    CustomEncoder decodes HexBytes
    in Geth response dict.
    """

    def default(self, o):
        if isinstance(o, hexbytes.main.HexBytes):
            return o.hex()

        return json.JSONEncoder.default(self, o)

if __name__ == '__main__':
    """
    Run code from command line for testing.
    Mock event and context.
    """

    print(handle(event={}, context={}))

Listing 10-14OpenFaaS function for returning details on the last block in the Blockchain

通过在一个终端端口转发eth-geth-tx服务,并在另一个终端执行 Python 脚本handler.py,在本地工作站上测试新功能。打开一个单独的终端和端口转发eth-geth-tx:

$ kubectl port-forward svc/eth-geth-tx 8545:8545 -n data

从当前(启用虚拟环境)终端执行 Python 脚本handler.py:

$ export GETH_RPC_ENDPOINT=http://localhost:8545
$ python3 ./handler.py

示例输出:

{'statusCode': 200, 'body': {'difficulty': 471861, 'extraData': '0xd88301090d846765746888676f312e31342e32856c696e7578', 'gasLimit': 8000000, 'gasUsed': 0, 'hash': '0x8b4ebaca1d3606630c872cba9ccf4a968c43af24e02800b0a182ef89b149f08b', 'logsBloom': '0x000000000000000000000', 'miner': '0x284F99f929B49Da9D85b2a3dbF606Ed38EeC393E', 'mixHash': '0xd92b8eaa4a7f103f9c76bf7bf9b13b90271fed7f1f25c72f81e429f2108755bc', 'nonce': '0x1f110a5f5dc69827', 'number': 9107, 'parentHash': '0xd40382cd4c2e75cc919d11318672820aab10854951ee4ee137a08d97e84aa4c7', 'receiptsRoot': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', 'sha3Uncles': '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', 'size': 538, 'stateRoot': '0xbe54d463bf9ffeda68975ff839eec7ecabd42c2f88cfc75372765891f43b1f18', 'timestamp': 1589012313, 'totalDifficulty': 3333037812, 'transactions': [], 'transactionsRoot': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', 'uncles': []}}

接下来,buildpushdeploy该功能。OpenFaaS CLI 使用 Docker 构建并推送函数的容器镜像到last-block.yml中自动配置的库;参见 OpenFaaS 配置选项?? 36自定义默认值。如果使用默认配置,请在本地工作站上安装 Docker,并注册一个免费的 Docker Hub 37 帐户:

$ faas-cli build --build-arg ADDITIONAL_PACKAGE=gcc -f ./last-block.yml

$ faas-cli push -f ./last-block.yml
$ faas-cli deploy -f ./last-block.yml

示例输出:

Deploying: last-block.

Deployed. 202 Accepted.
URL: https://faas.data.eth.apk8s.dev/function/last-block

最后,使用 web 浏览器或 curl 访问新的公共 last-block 函数:

$ curl https://faas.data.eth.apk8s.dev/function/last-block

输出示例(截断):

{"difficulty":474599,"extraData":"0xd88301090d846765746888676f312e31342e32856c696e7578","gasLimit":8000000,"gasUsed":0,"hash":"0x8154d9edf431821a239fbb72bc2636304e254663b11cddc6987095d391f35248","logsBloom":"0x000…","miner":"0xcc7ADDFC03cb5ec2E3894583895C1bE385625c62","mixHash":"0xd20bf313f1834ec333d7d5cb2870b42487b462ee322aeccdd699fc017f86be51","nonce":"0x1a11af75e7ff468f","number":11694,"parentHash":"0x930d5f7340997cc1f0cd4be6e22aefe02c136c2e93b38ce75eff1815455f730d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":538,"stateRoot":"0x51a5a7bb73dd5bd6ebb3a96606e115033e6b3bdcdb4a326b5a6c67718969f6cc","timestamp":1589048551,"totalDifficulty":4550348128,"transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}

最后一个块函数代表了基于 Kubernetes 和 OpenFaaS 的应用开发的简单明了的演示。OpenFaaS 管理、监控和扩展功能。Kubernetes 将多种多样的技术结合在一起,如无服务器、区块链和基于网络的 ide(集成开发环境)。

摘要

本章安装了一个以太坊区块链网络,该网络由两个 Bootnode、一个 boot node 注册器、一个 Ethstats 仪表板、三个 miner 节点和两个事务节点组成(参见清单 10-15 ),所有这些节点都运行在一个三节点 Kubernetes 集群上。调整本章中的以太坊区块链网络以支持受保护的公共网络涉及暴露 bootnodes 和矿工以供外部第三方访问,并从以太坊标准工作证明共识协议转移到新团体,即权威证明。共享生产区块链网络的配置取决于业务目标和需求,因此超出了本书的范围。然而,演示的 Kubernetes 实现适用于许多类型的区块链和无服务器平台。

在 Kubernetes 内部实现区块链技术起初可能看起来违反直觉;这是一个技术本身不需要 Kubernetes 提供的任何功能的例子。然而,Kubernetes 在这里并不是作为运行区块链的解决方案,甚至不是无服务器技术。本书将 Kubernetes 作为一个标准化的统一平台,用于扩展数据管理、无服务器、数据科学和区块链平台,由统一存储、网络和控制面板支持,通过声明式配置实现。

本书的下一章也是最后一章涵盖了跨云和内部基础架构的数据科学和机器学习工作负载的管理。

./010-cluster-apk8s-eth/
├── 000-cluster
│   ├── 00-ingress-nginx
│   ├── 10-cert-manager
│   ├── 20-rook-ceph
│   └── 30-monitoring
├── 003-data
│   ├── 000-namespace
│   ├── 005-keycloak
│   ├── 100-jupyterhub
│   ├── 120-openfaas
│   └── 200-eth
│       ├── 10-bootnode
│       │   ├── 10-service.yml
│       │   └── 30-deployment.yml
│       ├── 20-bootnode-reg
│       │   ├── 10-service.yml
│       │   └── 30-deployment.yml
│       ├── 30-ethstats
│       │   ├── 10-service.yml
│       │   ├── 15-secret.yml
│       │   ├── 30-deployment.yml
│       │   └── 50-ingress.yml
│       ├── 40-miner
│       │   ├── 15-secret.yml
│       │   ├── 20-configmap.yml
│       │   └── 30-deployment.yml
│       ├── 50-tx
│       │   ├── 10-service.yml
│       │   └── 30-deployment.yml
│       └── functions
│           ├── build
│           ├── last-block
│           ├── last-block.yml
│           └── template
└── 005-data-lab

Listing 10-15Chapter 10 organization of Kubernetes-based Blockchain platform components

十一、平台化 AIML

在本书的背景下,平台化 AI/ML(人工智能/机器学习)涉及实现所有必要的组件,以利用 Kubernetes 开发和部署基于机器学习的人工智能。本章使用具体的技术来说明广泛的概念,从在物联网设备上分发和管理数据收集应用和 ETL 操作,到使用内部 GPU 构建和训练机器学习模型,以及将基于推理的人工智能部署到分布式云环境中。

TensorFlow、Keras、scikit-Learn 和 PyTorch 等数据科学框架简化了机器学习模型的开发和训练。这些框架通过机器学习降低了人工智能概念的入门门槛。这些现代数据科学框架促进了快速实验和开发。Kubernetes 通过简化管理和连接负责收集、存储、分发、处理和分析数据的应用和处理数据的工作负载的复杂问题来补充这一生态系统,使 Kubernetes 非常适合作为端到端机器学习平台的基础。

AIML 的生命周期始于原始数据,止于根据这些数据做出的有价值的推论。关于这一过程的几乎每个阶段都有许多书籍;这本以 Kubernetes 为中心的书在单个章节中只能触及皮毛,但旨在展示在构建基于前几章的定制 AIML 平台时有用的概念(见图 11-1 )。

img/483120_1_En_11_Fig1_HTML.jpg

图 11-1

端到端 AIML 平台演示

数据

这本书的很大一部分涵盖了数据管理应用:第五章到第九章涵盖了数据流水线、数据索引和分析、数据湖、数据仓库和数据转换。本章向集群添加原始数据采集。它展示了 Kubernetes 在减少原始数据收集和访问适合数据科学活动的打包数据集之间的概念距离方面的能力,包括基于机器学习的人工智能的开发。

混合基础设施

基于机器学习的人工智能(AIML)的应用远远超出了打包数据集和最新算法。完整的 AIML 生命周期通常涉及数据采集、ETL、处理,以及在不同位置和基础设施之间基于推理/预测的工作负载之间的数据路由。

图 11-2 描绘了许多组织熟悉的基础设施,包括包含传感器设备的内部设施、配备服务器和工作站的办公室,以及在多个公共云中的多区域存在。

img/483120_1_En_11_Fig2_HTML.jpg

图 11-2

企业基础设施示例

Kubernetes 提供了一个平台,能够支持 AIML 驱动的计划的整个端到端生命周期,从设备及其传感器收集的原始数据到高度可扩展的分布式推理工作负载的部署和管理。以下部分实现了图 11-2 中所示的基础设施广泛范围的代表性样本。

发展环境

本章中的练习利用了图 11-3 中所示的以下按比例缩小的演示基础设施。要遵循本章中的示例,请使用以下资源或等效资源:

  • 来自数字海洋的一个液滴提供 2 个 CPU、2 GB 内存、60 GB 磁盘存储,运行 Ubuntu 18.04,部署在 NYC3 地区

  • 来自 Hetzner 的三台 CX31 云服务器,每台提供 2 个 CPU、2 GB 内存、80 GB 磁盘存储,运行 Ubuntu 18.04,部署在纽伦堡地区

  • 一个物理工作站,提供 4 个 CPU、16 GB 内存、80 GB 磁盘存储、1 个 NVIDIA GPU (GeForce GTX 1070 或更高版本),运行 Ubuntu 18.04,部署在内部

  • 三个 Raspberry Pi 设备(版本 3 或 4),运行 Raspberry Pi OS Lite (32 位)minimal,并基于 Debian Buster1

img/483120_1_En_11_Fig3_HTML.jpg

图 11-3

企业基础设施示例

本章中的混合集群因其规模而故意复杂化;然而,这种复杂性展示了 Kubernetes 在各种基础设施挑战中的灵活性。

域名服务器(Domain Name Server)

本章使用 Cloudflare 2 来管理名为hc1的新开发集群的以下 DNS A 条目(参见图 11-4):*.hc1指向分配给用作工作节点的 Hetzner 云实例的三个 IP 地址。master.hc1指向为 Kubernetes 主节点创建的数字海洋水滴。nbg1-n1.hc1nbg1-n2.hc1nbg1-n3.hc1是指向每个工作者节点的可选条目。最后,lab-gpu.hc1指向内部互联网路由器;如果此路由器有动态 IP 地址(家庭互联网服务通用),请使用动态 DNS 服务。 3

img/483120_1_En_11_Fig4_HTML.jpg

图 11-4

hc1开发集群的 DNS 条目

下一节将介绍使用 k3s 在三个地区安装八个 Kubernetes 节点、本地设备和服务器以及纽约和纽伦堡的公共云。

k3s 混合云

本章使用 k3s 来演示跨各种硬件的全功能 Kubernetes 集群。k3s 是经过认证的轻量级 Kubernetes 发行版,非常适合混合基础架构,支持范围从云计算资源到物联网设备。

以下部分创建了一个八节点 Kubernetes 集群,该集群由三个用于传感器数据收集的 Raspberry Pi 设备、一个用于机器学习的 GPU 工作站、三个用于分布式工作负载的公共云节点和一个处于主角色的公共云节点组成。

这三个 Raspberry Pi 设备代表任何负责收集传感器数据并将其传输到 MQTT 代理的物联网设备。后来,它被存储在运行在 worker 节点上的 Elasticsearch 和 MinIO 对象存储中。GPU 服务器从收集的数据中训练机器学习模型,并将训练好的模型部署到云节点以用于推断/预测工作负载。一个单一的 Kubernetes 集群管理这个小型但全球化的集群中的所有工作负载和通信。

公斤流水标

在跨多个公共网络运行 Pod 网络时,需要格外注意安全性。任何通过开放互联网的通信都必须加密,VPN 是确保这一点的方法之一。前几章使用专用网络进行跨节点通信;私有网络是每个主要云供应商提供的标准功能。然而,这些专用网络通常仅限于供应商,在某些情况下,还受地区限制。

WireGuard(在第三章中首次介绍)是一种高性能 VPN,能够为跨公共网络的 Pod 通信提供高效的加密隧道。然而,配置和管理去往和来自每个区域上的节点的公共网络接口之间的路由和隧道是一项复杂而乏味的任务。

Kilo 4 项目使用 WireGuard 创建加密的第 3 层 5 网络覆盖。Kilo 为 Kubernetes 集群中的每个节点管理 IP 路由以及 WireGuard 公钥/私钥对。Kilo 支持全集群和基于区域的节点到节点加密。全群集加密可能会在已经在专用网络中通信的节点之间增加不必要的开销。因此,以下示例使用基于区域的 Kilo 网络,仅在区域之间创建 VPN 隧道,如图 11-5 所示。

img/483120_1_En_11_Fig5_HTML.jpg

图 11-5

具有拓扑感知 VPN 隧道的混合 Kubernetes 集群

图 11-6 描述了每个节点使用的网络接口,用于与集群中的每个其他节点进行内部和外部通信。

img/483120_1_En_11_Fig6_HTML.jpg

图 11-6

千 VPN 全集群网络连接

本节描述的复杂混合集群简要概述了每个节点在接下来的几节中创建的网络拓扑。安装每个节点后,请回头参考这些图以供参考。这个网络展示了 Kubernetes 对广泛的网络环境的支持,并解决了许多组织面临的共同挑战:标准化和统一不同的开发和生产环境的需要。

主网点

为了演示一个按比例缩小的混合云网络,下面的示例使用 k3s 在 Digital Ocean 上安装了一个 Kubernetes master节点。在数字海洋上创建一个 Droplet,提供 2 个 CPU,2 GB 内存,60 GB 磁盘存储,配置 Ubuntu 18.04,部署到 NYC3 地区。

升级 Droplet 上的软件包,并在新的 Droplet 上安装 WireGuard(后来被 Kilo 用来创建 VPN 隧道):

$ apt upgrade -y
$ apt install -y apt-transport-https \
ca-certificates gnupg-agent \
software-properties-common

$ add-apt-repository -y ppa:wireguard/wireguard

$ apt update

$ apt -o Dpkg::Options::=--force-confnew \
install -y wireguard

接下来,为集群创建一个共享秘密,并将生成的值存储在一个安全的地方。额外的节点使用共享秘密由master节点接受到 k3s 集群中:

$ export K3S_CLUSTER_SECRET=$(head -c48 /dev/urandom | base64)

# copy the echoed secret
$ echo $K3S_CLUSTER_SECRET

不用默认法兰绒 Pod 网络安装 k3sKilo 将在本章后面安装,它将处理第 3 层网络:

$ curl -sfL https://get.k3s.io | \
sh -s - server --no-flannel

k3s 支持单节点和多节点操作;本章构建了一个具有专用主节点的八节点集群。污染主节点以阻止常规工作负载的调度:

$ kubectl taint node master dedicated=master:NoSchedule

Kilo 使用名称标签topology.kubernetes.io/region创建地区间的 VPN 隧道;用区域标记master节点,在本例中为nyc3:

$ kubectl label node master \
topology.kubernetes.io/region=nyc3

接下来,将位于/etc/rancher/k3s/k3s.yaml的 k3s kubectl配置文件复制到 root home:

$ cp /etc/rancher/k3s/k3s.yaml ~

master节点创建一个 DNS 条目,在本例中为master.hc1.apk8s.dev,,并用新的公共 DNS 更新复制的~/k3s.yml文件。手动编辑文件或使用sed进行快速替换:

$ sed -i "s/127.0.0.1/master.hc1.apk8s.dev/" k3s.yaml
$ sed -i "s/default/apk8s-hc1/" k3s.yaml

最后,将修改后的k3s.yaml复制到安装了 kubectl 的本地工作站上。从本地工作站,使用安全拷贝:

$ scp root@master.hc1.apk8s.dev:~/k3s.yaml ~/.kube/apk8s-hc1

使用新的 kubectl 配置文件查询主节点:

$ export KUBECONFIG=~/.kube/apk8s-hc1
$ kubectl describe node master

本节在数字海洋位于 nyc3 地区的公共云上安装了一个主节点。下一节将在纽伦堡地区的 Hetzner 公共云上创建三个 worker 节点。

工作节点

本节继续混合云网络的开发,将三个工作节点连接到上一节中创建的主节点。在 Hetzner 上启动三台 CX31 (2 个 CPU,2 GB 内存,80 GB 磁盘存储)云服务器,运行 Ubuntu 18.04,部署在纽伦堡地区。将新实例命名为nbg1-n1nbg1-n2nbg1-n3

为了避免演示集群上负载均衡器的成本和复杂性,为每个新实例的 IP 地址创建一个通配符 DNS A 记录*.hc1.apk8s.dev。子域hc1.apk8s.dev的所有入口规则现在都路由到三个 Hetzner 实例之一。

登录每个新的云服务器实例,并在每个实例上升级和安装所需的软件包以及 WireGuard:

$ apt upgrade -y
$ apt install -y apt-transport-https \
ca-certificates gnupg-agent \
software-properties-common

# Install kernel headers (missing on Hetzner instances)
$ apt install -y linux-headers-$(uname -r)

# Ceph block device kernel module (for Ceph support)
$ modprobe rbd

$ add-apt-repository -y ppa:wireguard/wireguard

$ apt update

$ apt -o Dpkg::Options::=--force-confnew \
install -y wireguard

接下来,在每个新的 Hetzner 实例上安装 k3s。首先填充环境变量K3S_CLUSTER_SECRET,即上一节中在主节点上生成的集群机密,以及主节点地址K3S_URL,在本例中为master.hc1.apk8s.dev。将 k3s 安装程序脚本连同命令行参数agent(以非工作人员身份运行)和网络拓扑标签一起传送到sh:

$ export K3S_CLUSTER_SECRET="<PASTE VALUE>"
$ export K3S_URL="https://master.hc1.apk8s.dev:6443"

$ curl -sfL https://get.k3s.io | \
sh -s - agent --no-flannel \
--node-label=\"topology.kubernetes.io/region=nbg1\"

hc1.apk8s.dev集群现在由四个节点组成,一个位于 Digital Ocean 的主节点和三个位于 Hetzner 的工作节点。在本地工作站上,复制并修改了上一节中的 kubectl 配置文件,列出四个节点:

$ export KUBECONFIG=~/.kube/apk8s-hc1
$ kubectl get nodes -o wide

kubectl列出了集群中的四个节点。集群缺少 Pod 网络,因此将为每个节点报告一个NotReady状态。本章在配置完所有八个节点后安装 Kilo Pod 网络。接下来的部分开始研究混合星团的第三个也是最后一个区域,即lab

内部

术语“内部”可以指数据中心或云之外的任何特定物理位置。本节继续构建一个混合 Kubernetes 集群,最后一个区域称为lab。新的实验室区域可以是单个公司 WAN 或 LAN 上的一个或多个物理位置。以下演示配置了一台裸机服务器,配有一个用于处理机器学习工作负载的 NVIDIA GPU,以及三个用于从板载传感器收集数据的 Raspberry Pi 设备。

国家政治保卫局。参见 OGPU

以下示例 GPU 服务器使用通用主板、英特尔 CPU、120 GB 固态硬盘和 16 GB 内存,以及 NVIDIA GeForce 1070(游戏)GPU。 6 NVIDIA 并未授权 GeForce 1070 GPUs 的驱动程序在数据中心使用。然而,这些卡价格低廉,适合小规模的现场实验和测试。希望将 NVIDIA 硬件用于生产的组织应该仔细检查 NVIDIA 的许可条款。参考英伟达的云与数据中心硬件产品线,进行生产规模的机器学习。 7

公共云通常提供的 NVIDIA GPU 型号包括 K80、P100 和 V100。

GPU/CUDA

由于在主要框架和软件库中对 CUDA 8 的广泛支持,NVIDIA GPUs 在机器学习项目中很受欢迎。CUDA(计算统一设备架构)是英伟达的专有 API,仅受英伟达 GPU 支持。CUDA 的开放替代方案是 OpenCL?? 9在 NVIDIA 和 AMD GPUs 上都支持。然而,截至 2020 年,数据科学生态系统中对 open OpenCL 的支持有限,并且没有最流行的框架 TensorFlow 和 PyTorch 的支持。 10

安装 Ubuntu

以下示例为采用 NVIDIA GeForce 1070 GPU 的全新或重新设计的桌面工作站创建了 Ubuntu 18.04.4 的全新安装。

下载 Ubuntu 18.04.4 LTS(仿生海狸)64 位 PC (AMD64)服务器安装镜像 11 并创建一个可引导的 USB 拇指驱动器。 12 这个例子使用的是 MSI 主板,在开机时按住 F11 启动引导菜单;查阅其他制造商的手册。从新的拇指驱动器启动工作站,并遵循标准安装说明。

为 Kilo 支持安装 WireGuard,为 Ceph 支持安装rbd内核模块:

$ sudo su
$ apt update && apt upgrade -y
$ apt install -y apt-transport-https \
ca-certificates gnupg-agent \
software-properties-common

$ apt install -y linux-headers-$(uname -r)

# Ceph block device kernel module (for Ceph support)
$ modprobe rbd

$ add-apt-repository -y ppa:wireguard/wireguard

$ apt update

$ apt -o Dpkg::Options::=--force-confnew \
install -y wireguard

分配静态 IP 地址或保留 DHCP 分配的地址,并从面向互联网的路由器上的公共 IP 地址转发端口5180(用于 WireGuard VPN 隧道)。最后,创建一个名为lab-gpu.hc1.apk8s.dev的 DNS A 记录,指向本地互联网路由器的公共 IP。如果这个地址可能改变,使用动态 DNS 服务 13 。Kilo 在本章稍后部分进行配置,以便通过此节点将外部 Pod 网络流量路由到任何其他内部节点或从任何其他内部节点路由。

NVIDIA GPU 支持

上一节在名为lab-gpu的本地工作站上安装了 Ubuntu 18.04.4。以下练习安装 NVIDIA GPU 驱动程序以及用于 containerd 的nvidia-container-runtime插件,14k3s 的默认容器运行时。

以 root 用户身份执行以下所有命令:

$ sudo su

更新apt,升级现有包,安装常用 Ubuntu 驱动包:

$ apt update && apt upgrade -y
$ apt install ubuntu-drivers-common

启用智能平台管理界面内核模块:

$ modprobe ipmi_devintf

将专有 GPU 驱动程序库添加到apt:

$ add-apt-repository -y ppa:graphics-drivers

将 NVIDIA 的 GPG 密钥添加到apt:

$ curl -s -L \
   https://nvidia.github.io/nvidia-docker/gpgkey \
   | sudo apt-key add -

将 NVIDIA 的驱动程序库添加到apt:

$ curl -s -L https://nvidia.github.io/nvidia-container-runtime/ubuntu18.04/nvidia-container-runtime.list | tee /etc/apt/sources.list.d/nvidia-container-runtime.list

安装 NVIDIA 软件包:

$ apt-get install -y nvidia-driver-440
$ apt-get install -y nvidia-container-runtime
$ apt-get install -y nvidia-modprobe nvidia-smi

加载 NVIDIA 内核模块:

$ /sbin/modprobe nvidia
$ /sbin/modprobe nvidia-uvm

启动工作站后,确保nvidia-smi命令返回类似于图 11-7 所示的结果:

img/483120_1_En_11_Fig7_HTML.jpg

图 11-7

nvidia-smi 输出

$ reboot

$ nvidia-smi

新安装的工作站现在支持操作系统和 NVIDIA 系统管理界面识别的 NVIDIA GPU。下一节将添加 NVIDIA 对 containerd 的运行时支持。

带 NVIDIA 运行时的 k3s

NVIDIA 提供了大量关于配置 Docker 15 以支持其 GPU 的文档,以及一个容器工具包。k3s 虽然支持 Docker,但是 containerd 16 是它默认的容器运行时。containerd 是 Docker 的一个组件,没有 Docker 引擎和实用程序。本节安装 k3s 并用nvidia-container-runtime配置 containerd。 17

首先设置以下环境变量:将K3S_CLUSTER_SECRET设置为安装主节点时生成的秘密;将K3S_URL设置为master节点的 URL 并将INSTALL_K3S_SKIP_START设置为true防止 k3s 自动启动:

$ sudo su
$ export K3S_CLUSTER_SECRET="<PASTE VALUE>"
$ export K3S_URL="https://master.hc1.apk8s.dev:6443"
$ export INSTALL_K3S_SKIP_START=true

使用 k3s 默认安装程序,其参数和自变量与之前在worker节点上设置的参数和自变量相似;然而,这个 GPU 工作站是名为lab的新内部区域的一部分:

$ curl -sfL https://get.k3s.io | \
sh -s - agent --no-flannel \
--node-label=\"topology.kubernetes.io/region=lab\"

运行以下命令在目录/var/lib/rancher/k3s/agent/etc/containerd/中创建或覆盖config.toml;这个新配置指示 containerd 加载nvidia-container-runtime插件及其所需的运行时类型io.containerd.runtime.v1.linux:

$ mkdir -p /var/lib/rancher/k3s/agent/etc/containerd/
$ cat <<"EOF" > \
/var/lib/rancher/k3s/agent/etc/containerd/config.toml
[plugins.opt]
  path = "/var/lib/rancher/k3s/agent/containerd"

[plugins.cri]
  stream_server_address = "127.0.0.1"
  stream_server_port = "10010"
  sandbox_image = "docker.io/rancher/pause:3.1"

[plugins.cri.containerd.runtimes.runc]
  runtime_type = "io.containerd.runtime.v1.linux"

[plugins.linux]
  runtime = "nvidia-container-runtime"
EOF

最后,启动 k3s:

$ systemctl start k3s

新的 hc1 Kubernetes 混合集群现在包含跨三个区域的五个节点:nyc3在数字海洋,nbg1在赫茨纳,现在lab在本地。下一部分将使用三个 Raspberry Pi 设备扩展内部区域。

物联网/树莓派

在过去的十章中,这本书展示了 Kubernetes 在分布式工作负载、网络、数据处理和数据管理中的各种用途。基于机器学习的人工智能从原始数据的收集开始,对于许多项目来说,设备(也称为物联网)收集这些数据。这一部分将物联网引入集群,特别是流行的 Raspberry Pi。Raspberry Pi 设备为开发和原型制作提供了出色的平台,越来越多的项目正在生产中使用它们。 18 树莓派设备使用基于 ARM 的 19 CPU,几乎适用于所有手机和越来越多的物联网设备。 20 随着 ARM CPUs 越来越受欢迎, 21 很多项目都是交叉编译,在 x86 和基于 ARM 的系统上执行维护版本,包括 Kubernetes。

本章开发的混合集群(hc1)使用 k3s。k3s 是经过认证的轻量级 Kubernetes 发行版,支持 ARM,能够在资源有限的设备上运行。本节展示了 Kubernetes 作为物联网管理平台的使用,它为多个设备提供工作负载调度和管理、自修复、联网和监控,并与内部工作站和多个公共云实例相统一(如图 11-3 所示)。

树莓派 OS

安装 22 三个树莓 Pi 设备(版本 3 或 4)带有 树莓 Pi OS Lite (32 位)最小基于 Debian Buster23 命名设备lab-d1lab-d2lab-d3。对于本章中的示例,设备数量是可选的。这些设备代表设施内的物联网数据收集传感器,并连接到可访问互联网的局域网(LAN)上(见图 11-5 )。每台设备都需要一个私有静态 IP 地址或 DHCP 预留地址。

WireGuard

Kilo(将在本章后面安装)为公共互联网上的安全通信提供了一个加密的 Pod 网络。Kilo 在每个节点上使用 WireGuard 来管理 VPN 隧道。

登录每个 Raspberry Pi 并安装所需的包,从更新 apt 包开始,升级到最新版本:

$ sudo su
$ apt-get update && apt-get upgrade

安装内核头:

$ apt-get install raspberrypi-kernel-headers

接下来,安装 WireGuard。WireGuard 可通过 Debian“不稳定”发行库(包含最新的软件包)用于 Raspberry Pi 操作系统:

$ wget -O - https://ftp-master.debian.org/keys/archive-key-$(lsb_release -sr).asc | sudo apt-key add –

$ printf 'Package: *\nPin: release a=unstable\nPin-Priority: 150\n' | tee --append /etc/apt/preferences.d/limit-unstable

$ echo "deb http://deb.debian.org/debian/ unstable main" | tee --append /etc/apt/sources.list.d/unstable.list

$ apt-get update
$ apt install -y dirmngr wireguard-dkms
$ apt -o Dpkg::Options::=--force-confnew \
install -y wireguard

如果前面的方法出现任何问题,有一些关于安装 WireGuard Raspberry Pi 的好教程。 24 25 26

树莓派上的 k3s

最后,在每个树莓 Pi 上安装 k3s。首先用安装主节点时生成的值设置K3S_CLUSTER_SECRET,并将K3S_URL设置为主节点(在数字海洋上):

$ sudo su
$ export K3S_CLUSTER_SECRET="<PASTE VALUE>"
$ export K3S_URL="https://master.hc1.apk8s.dev:6443"

使用默认安装程序安装 k3s,选项类似于前面的节点。将区域拓扑设置为lab,假设这些设备与之前安装的 GPU 工作站位于同一设施或本地网络中。添加设置为dedicated=pi:NoSchedule的参数--node-taint。带有NoSchedule污点的节点通知 Kubernetes 调度程序只允许具有特定容差的 pod:

$ curl -sfL https://get.k3s.io | \
sh -s - agent --no-flannel \
--node-label=\"topology.kubernetes.io/region=lab\" \
--node-taint="dedicated=pi:NoSchedule"

现在有八个节点组成了新的hc1混合集群:数字海洋的一个主节点名为master,位于nyc3区域内;在nbg1区域内,Hetzner 上的三个工人节点称为nbg1-n1nbg1-n2nbg1-n3;以及在lab区域内称为lab-gpulab-d1lab-d2lab-d3的四个内部节点。网络拓扑概述见图 11-5 。

下一节将为节点设置角色,以帮助进行调度。

节点角色

Kubernetes 节点角色是一个以node-role.kubernetes.io/<role>开头的简单标签。Pod 调度程序是由 Kubernetes 设计的,用于找到 Pod 的最佳位置。POD 是短暂的;它们可能会失败或从一个节点耗尽,并随时被重新调度到另一个节点。但是,有时特殊的工作负载需要额外的调度要求。在本章定义的hc1集群中,三个节点具有独特的角色sensor。角色标签可以是反映其操作特征的任何值。

主节点已经拥有标签master。将其余节点标记如下:

$ kubectl label node nbg1-n1 kubernetes.io/role=worker
$ kubectl label node nbg1-n2 kubernetes.io/role=worker
$ kubectl label node nbg1-n3 kubernetes.io/role=worker
$ kubectl label node lab-gpu kubernetes.io/role=worker

$ kubectl label node lab-d1 kubernetes.io/role=sensor
$ kubectl label node lab-d2 kubernetes.io/role=sensor
$ kubectl label node lab-d3 kubernetes.io/role=sensor

从本地工作站发出命令 kubectl get nodes,观察类似于图 11-8 的输出。节点现在报告它们的预期角色。

img/483120_1_En_11_Fig8_HTML.jpg

图 11-8

节点角色

节点状态继续报告为NotReady,因为还没有 Pod 网络供它们通信。以下部分通过安装 Kilo Pod 网络完成了hc1集群的设置。

安装千克

本章的“k3s 混合云”一节首先详细解释了 Kilo 及其在使用 WireGuard 创建 VPN 隧道中的作用,该隧道通过公共互联网在跨越三个区域的集群中的 pod 之间提供安全、加密的通信。安装 Kilo 是设置 k3s 混合云的最后一步。

每个区域中至少有一个节点必须具有外部可访问的端点。 27 数字海洋和赫茨纳上的云实例已经有了公有 IP 地址。当设置 GPU 工作站时,域lab-gpu.hc1.apk8s.dev解析为内部互联网路由器,并将端口51820转发到其专用 LAN IP 上的 GPU 工作站。这三台 Raspberry Pi 设备有专用的局域网 IP 地址,不能通过互联网访问。以下注释指示 Kilo 形成从 GPU 工作站到 Raspberry Pi 设备的 VPN 隧道:

$ kubectl annotate node lab-d1 \
kilo.squat.ai/force-endpoint="lab-gpu.hc1.apk8s.dev:51820"
$ kubectl annotate node lab-d2 \
kilo.squat.ai/force-endpoint="lab-gpu.hc1.apk8s.dev:51820"
$ kubectl annotate node lab-d3 \
kilo.squat.ai/force-endpoint="lab-gpu.hc1.apk8s.dev:51820"

接下来,Kilo 需要访问集群中每个节点的 kubectl 配置,以便与主节点通信。所应用的 kilo-k3s.yaml 需要每个节点上的 kubectl 配置/etc/rancher/k3s/k3s.yaml。主节点 k3s 安装生成了文件/etc/rancher/k3s/k3s.yaml。下载、修改并分发该文件到所有workersensor节点上的相同路径(在master节点上不需要修改):

$ ssh root@master.hc1.apk8s.dev \
sed "s/127.0.0.1/master.hc1.apk8s.dev/" \ /etc/rancher/k3s/k3s.yaml >k3s.yaml

在每个节点上复制修改后的k3s.yaml/etc/rancher/k3s/k3s.yaml

最后,通过应用 kilo-k3s.yaml 配置清单来安装 Kilo:

$ kubectl apply -f \
https://raw.githubusercontent.com/squat/kilo/master/manifests/kilo-k3s.yaml

一千个代理现在在每个节点上运行,如图 11-9 所示。一旦 Kilo 完成了 VPN 设置,每个节点都会报告就绪,新的hc1混合集群就可以使用了。

img/483120_1_En_11_Fig9_HTML.jpg

图 11-9

每个集群上的千个代理 Pod

如图 11-3 和图 11-5 所示,新的hc1混合集群已经准备好应用。以下部分回顾了用于演示基于端到端机器学习的人工智能平台的应用示例,从原始数据收集到完全在 Kubernetes 上运行的推理工作负载。

平台应用

本章创建了一个名为hc1的新 Kubernetes 集群。其余示例利用了第 3 、 5 、 6 、 7 和 9 章中安装的应用和集群配置(如表 11-1 中所述)。本章将所有配置清单组织在文件夹cluster-apk8s-hc1下。

表 11-1

前几章中要求的配置和应用

|   |

资源

|

目录组织

|
| --- | --- | --- |
| 第三章 | 进入证书管理器仓库监视 | 000-cluster/00-ingress-nginx``000-cluster/10-cert-manager``000-cluster/20-rook-ceph``000-cluster/30-monitoring |
| 第五章 | 命名空间蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子蚊子动物园管理员 | 003-data/000-namespace``003-data/050-mqtt``003-data/010-zookeeper |
| 第六章 | 凯克洛克 JupyterHub 弹性搜索巴拉人 | 003-data/005-keycloak``005-data-lab/000-namespace``003-data/100-jupyterhub``003-data/030-elasticsearch``003-data/034-kibana |
| 第七章 | 微型汽车 | 000-cluster/22-minio``003-data/070-minio |
| 第九章 | 尼菲 | 003-data/060-nifi |

以下部分使用前面章节中介绍的应用:Mosquitto (MQTT)从 Raspberry Pi 设备(IoT)传送传感器数据;Apache NiFi 监听 MQTT 事件,将数据(ETL)转换和加载到 Elasticsearch 中;JupyterHub,用于在 GPU 节点上使用机器学习(AIML)处理数据并开发和训练 AI 模型;以及用于存储机器学习模型和工件的 MinIO。

本章使用以下入口域:nifi.hc1.apk8s.dev用于 Apache Nifi,hub.hc1.apk8s.dev用于 JupyterHub,kib.hc1.apk8s.dev用于 Kibana,iam.hc1.apk8s.dev用于 Keycloak,minio.hc1.apk8s.dev用于 MinIO。

下一部分开始从集群上的 Raspberry Pi IoT 节点收集原始数据。

数据收集

以下部分使用 Kubernetes DaemonSet对象来部署和管理 Raspberry Pi (IoT)设备上的数据收集工作负载,并使用CronJob对象来部署执行数据聚合的 ETL 工作负载。

MQTT IoT 客户端

许多物联网应用遵循特定于平台的软件开发的传统模式。开发人员为特定设备编写代码,并支持一种或多种与设备通信的协议,无论是收集传感器数据还是煮一壶咖啡。然而,与 Kubernetes 一起,容器化为物联网应用开发、部署、联网、监控和控制开辟了新的途径。

以下示例在 Kubernetes ConfigMap 中部署了一个 shell 脚本。一个 DaemonSet 在一组选定的(sensor)节点上部署一个瘦 Debian Buster 容器 28 (带有命令行 MQTT 客户端),并执行从 ConfigMap 挂载的 shell 脚本。

创建目录cluster-apk8s-hc1/020-data/220-smon。在新的220-smon目录中,从清单 11-1 中创建一个名为20-configmap.yml的文件。

apiVersion: v1
kind: ConfigMap
metadata:
  name: smon
  namespace: data
  labels:
    app: smon
data:
  collect.sh: |-
    for (( ; ; ))
    do
      load=$(cat /proc/loadavg | cut -d\  -f 1)
      temp=$(cat /sensor/temp)
      mosquitto_pub \
        -h mqtt.data \
        -i $DEVICE \
        -t sensor/$DEVICE \
        -m "{\"device\":\"$DEVICE\",\"temp\":$temp, \"load\":$load}"
      echo "device: $DEVICE, temp: $temp, load: $load"
    sleep 15
    done

Listing 11-1Signal monitor script ConfigMap

应用配置图:

$ kubectl apply -f 20-configmap.yml

接下来,在清单 11-2 中的文件40-daemonset.yml中为信号监视器创建一个 DaemonSet。ConfigMap 中定义的 shell 脚本从 Raspberry Pi 读取内部传感器数据(CPU 负载和温度);开发人员可能会遵循类似的设计来提取外部传感器数据或控制连接的设备。DaemonSet 使用nodeSelectortolerations的组合,指示 Kubernetes 在所有 Raspberry Pi 传感器设备上调度该容器。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: smon
  namespace: data
  labels:
    app: smon
    component: sensor
spec:
  selector:
    matchLabels:
      name: smon
  template:
    metadata:
      labels:
        name: smon
        component: sensor
    spec:
      nodeSelector:
        kubernetes.io/role: sensor
      tolerations:
        - key: dedicated
          value: pi
          effect: NoSchedule
      containers:
        - name: smon
          image: apk8s/mosquitto-clients:1.5.7_1
          command: ["/bin/bash",
                    "/scripts/collect.sh"]
          env:
            - name: DEVICE

              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          securityContext:
            privileged: true
          volumeMounts:
            - name: scripts
              mountPath: /scripts
            - name: sensor
              mountPath: /sensor
          resources:
            limits:
              memory: 200Mi
            requests:
              cpu: 50m
              memory: 200Mi
      volumes:
        - name: sensor
          hostPath:
            path: /sys/class/thermal/thermal_zone0/
        - name: scripts
          configMap:
            name: smon

Listing 11-2Signal monitor DaemonSet deployment

应用 DaemonSet:

$ kubectl apply -f 40-daemonset.yml

DaemonSet 使用映像 apk8s/mosquitto-clients:1 . 5 . 7 _ 1 生成清单 11-3 中的 Dockerfile。

FROM debian:buster-slim

RUN apt-get update \
 && apt-get install -y mosquitto-clients curl \
 && apt-get clean

Listing 11-3MQTT Client Dockerfile

三个 Raspberry Pi 设备现在每隔 15 秒向集群中运行的 MQTT 代理报告它们的 CPU 负载和温度,主题为/sensor/DEVICE_NAME。下一节使用 Apache NiFi 创建一个定向数据处理图,监听/ sensor主题中发布的所有消息,处理数据,并将其作为时间序列数据加载到 Elasticsearch 中,以便为分析建立索引。

抽取、转换、加载至目的端(extract-transform-load 的缩写)

上一节实现了一个 DaemonSet,它执行一个 shell 脚本,负责每隔 15 秒从 Raspberry Pi 设备中抓取指标。前面的 shell 脚本将其结果作为 JSON 发布到 MQTT topic / sensor/DEVICE_NAME。下面几节使用 Apache Nifi 提取 MQTT 消息,对它们进行转换,并加载到 Elasticsearch 中进行索引,还使用一个 Python 脚本和一个 Kubernetes CronJob 从收集的数据范围中创建 CSV 对象,用于长期对象存储。

Apache 尼菲

第九章介绍了用于数据路由和转换的 Apache NiFi 查看第九章的安装说明和概念概述。以下示例使用 NiFi 处理器 ConsumeMQTTJoltTransformJSONPutElasticsearchHttp ,如图 11-10 所示。

img/483120_1_En_11_Fig10_HTML.jpg

图 11-10

NiFi MQTT 变换和弹性搜索加载

创建一个 ConsumeMQTT 处理器,并将属性代理 URI 设置为tcp://mqtt:1883,主题过滤器设置为sensor/+,最大队列大小设置为 1000。

使用 advanced 按钮用清单 11-4 中的 Jolt 转换语言配置处理器。

[
  {
    "operation": "default",
    "spec": {
      "collected": "${now():toNumber()}"
    }
  },
  {
    "operation": "modify-overwrite-beta",
    "spec": {
      "collected": "=toLong"
    }
  }
]

Listing 11-4Jolt transformation

通过将从 MQTT 获取的样本数据粘贴到 JSON 输入字段来测试转换(参见图 11-11 ):

img/483120_1_En_11_Fig11_HTML.jpg

图 11-11

NiFi Jolt 转换规范

{"device":"lab-d1","temp":51540, "load":0.37}

最后,添加一个 PutElasticsearchHttp 处理器,并连接来自 JoltTransformJSON 处理器的输出。配置 PutElasticsearchHttp ,属性 Elasticsearch URL 设置为http://elasticsearch:9200,索引设置为sensor-${now():format('yyyy-MM')},类型设置为_doc

前面的步骤用 Apache Nifi 创建了一个小的 ETL 流水线,从 MQTT 中提取,并通过用一个附加字段扩充 JSON 数据来转换消息,然后将每个转换后的消息作为时间序列数据加载到 Elasticsearch 中进行索引和分析。

Python CronJob

数据科学,特别是机器学习研究需要静态数据集进行实验和训练。提炼和共享研究的可再现结果通常需要将源代码与开发中使用的数据相结合的能力。虽然存储在 Elasticsearch 中的时间序列数据不太可能改变,但与内部和外部团队共享数据库访问带来了额外的挑战。创建索引数据的 CSV 快照并将其加载到分布式对象存储上,为跨团队和项目存储和共享持久、长期的数据提供了一个引人注目的选项。

本节演示了 Kubernetes CronJob 在 ETL 活动中的使用。下面的例子从 Elasticsearch(参见第六章)中提取最后一个小时的索引数据,将数据序列化为 CSV 文件并加载到 MinIO(参见第七章)分布式对象存储中。

首先创建一个 Python 脚本,从 Elasticsearch 中提取最近一个小时的传感器数据,将其转换为 CSV 格式,并加载到 MinIO 中。将新的 Python 脚本作为一个值添加到 Kubernetes 配置图中。创建目录cluster-apk8s-hc1/020-data/500-jobs。在新的500-jobs目录中,从清单 11-5 中创建一个名为01-configmap-hrdump.yml的文件。

apiVersion: v1
kind: ConfigMap
metadata:
  name: hrdump
  namespace: data
  labels:
    app: hrdump
data:
  hrdump.py: |-
    import requests
    import pandas as pd
    import boto3
    import datetime
    import os

    d = datetime.datetime.now()
    idx = f"sensor-{d.year}-{d.month:02d}"

    query = {
      "size": 10000,
      "query": {
        "range" : {
          "collected" : {
              "gte" : "now-1h"
          }
        }
      }
    }

    r = requests.post(
        f'http://elasticsearch.data:9200/{idx}/_search',
        json=query
    )

    df = pd.DataFrame()

    for rec in r.json()['hits']['hits']:
        df = df.append(rec['_source'], ignore_index=True)

    csv_data = df.to_csv(index_label="idx")

    s3 = boto3.client('s3',
      endpoint_url='http://minio-internal-service.data:9000',
      aws_access_key_id=os.environ['ACCESS_KEY_ID'],
      aws_secret_access_key=os.environ['ACCESS_KEY'],
      region_name='')

    try:
        s3.create_bucket(Bucket='sensor-hourly')
    except s3.exceptions.BucketAlreadyOwnedByYou:
        print("Bucket Exists")
    except:
        print("Unknown bucket error")

    filename = d.strftime("%Y/%m-%d-%H.csv")
    s3.put_object(Bucket='sensor-hourly', Key=filename, Body=csv_data)
    print(f"Added object {filename} to bucket sensor-hourly")

Listing 11-5ConfigMap with Python data dump script

应用配置图:

$ kubectl apply -f 01-configmap-hrdump.yml

配置图中的 Python 脚本需要一个预安装了 Python 和所需库的容器。在创建定制容器之前,首先将所需的 Python 包 29 和版本添加到名为requirements.txt的文件中,如清单 11-6 所示。

boto3==1.13.18
botocore==1.16.18
certifi==2020.4.5.1
chardet==3.0.4
docutils==0.15.2
idna==2.9
jmespath==0.10.0
numpy==1.18.4
pandas==1.0.3
python-dateutil==2.8.1
pytz==2020.1
requests==2.23.0
s3transfer==0.3.3
six==1.15.0
urllib3==1.25.9

Listing 11-6Python data dump script requirements.txt

接下来,基于python:slim-buster创建一个 Dockerfile(参见清单 11-7 ,将 requirements.txt 复制到容器镜像中,并使用 pip 安装所需的 Python 库。构建新容器并将其推送到公共或私有容器注册中心(或者使用为此示例构建的公共映像apk8s/pyreqobj:0.0.1)。

FROM python:slim-buster

COPY requirements.txt .
RUN pip install -r requirements.txt

Listing 11-7Python Dockerfile with requirements

最后,在清单 11-8 中的一个名为01-cronjob-hrdump.yml的文件中创建 Kubernetes cron job30。CronJob 使用自定义 Python 容器(之前开发的)创建一个 Pod,将 ConfigMap 挂载到/scripts 目录中,并用 MinIO 的凭证填充ACCESS_KEY_IDACCESS_KEY。CronJob 每小时运行一分钟。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hrdump
  namespace: data
spec:
  schedule: "1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: hrdump
              image: apk8s/pyreqobj:0.0.1
              command: [
                  "/usr/local/bin/python",
                  "/scripts/hrdump.py"
              ]
              env:
                - name: ACCESS_KEY_ID
                  valueFrom:
                    secretKeyRef:
                      name: minio-creds-secret
                      key: accesskey
                - name: ACCESS_KEY
                  valueFrom:
                    secretKeyRef:
                      name: minio-creds-secret
                      key: secretkey
              volumeMounts:
                - name: scripts
                  mountPath: /scripts
          volumes:
            - name: scripts
              configMap:
                name: hrdump

Listing 11-8Python data dump hourly CronJob

应用 CronJob:

$ kubectl apply -f 01-cronjob-hrdump.yml

利用 Kubernetes 中的 CronJobs 进行调度执行带来了 Kubernetes 容器编排的所有优势,包括资源调度、网络和服务抽象、自修复、监控等等。CronJobs 虽然在实现上过于简单,但在任何基于 Kubernetes 的数据管理平台中,即使是复杂的 ETL 操作,它也能扮演重要的角色。

本章和本书的大部分内容都集中在 Kubernetes 上,Kubernetes 是一个能够连接各种数据应用并缩短原始数据收集、ETL 操作、数据访问和数据科学活动之间概念距离的平台。

本章的剩余部分将重点介绍机器学习工具,用于使用 Kubernetes 构建、训练、跟踪、存储和服务基于机器学习的人工智能模型。

机器学习自动化

机器学习自动化,也称为 AutoML, 31 是一个快速发展的软件工程领域,新项目开始特别利用 Kubernetes。Kubeflow 32 和 Pachyderm 33 是专为在 Kubernetes 上运行其容器化工作负载而设计的。这两个项目都解决了 AutoML 的两个重要领域:模型训练的自动化(超参数调整、算法选择和模型评估)和模型服务。这些项目由几个组件组成,这些组件被集合在一起以标准化 AutoML 工作流。

Kubeflow 和 Pachyderm 支持安装到现有的 Kubernetes 集群中,例如本书中演示的集群,但对受支持的 Kubernetes 版本有一些限制。Kubeflow 和 Pachyderm 都值得作为任何企业级 AutoML 平台的候选者进行研究。

本节通过项目 MLflow、 34 展示了 AutoML 组件的一个子集,适用于更集中的模型跟踪和服务展示。本章以一个用于生产模型部署的谢顿核心 35 (也用于 Kubeflow)的例子结束。

木星笔记本 gpu 支持

此前,本章介绍了一个配备 GPU 的工作站作为新的hc1混合 Kubernetes 集群中的节点,安装了 NVIDIA 操作系统驱动程序,并启用了 NVIDIA 容器运行时支持。本节介绍了在主机节点上开发能够访问 NVIDIA CUDA GPU 驱动程序的 Jupyter 笔记本容器的方法。

Jupyter Docker Stacks36项目维护了一组预构建的容器,其中包含 Jupyter 应用和各种数据科学、机器学习、统计库,并支持多个内核,包括 Python、Julia 和 r

Docker Stacks 的预构建容器包括日常数据科学活动所需的基本工具和用于开发更专业环境的合适基础容器。

创建类似于 Jupyter Docker Stacks 项目的容器的一种方法是从 NVIDIA 的 CUDA 10.1 (Ubuntu 18.04)容器开始,并添加支持任何所需库和应用的层。GitHub 上的 gpu-jupyter 37 项目已经自动化了这种方法,对于实验和探索支持 gpu 的 jupyter 容器的构造非常有用。默认情况下,gpu-jupyter 从 NVIDIA 的 CUDA 容器开始,克隆 Jupyter Docker Stacks 存储库,并将每个 Docker 文件的层聚合到一个新的 Docker 文件中(不到 500 行)。生成的 Dockerfile 是添加和删除功能的优秀模板;但是,请注意,按原样构建它将会产生一个>14gb 的容器映像。

CUDA 数据科学容器

创建一个新的 CUDA Jupyter 数据科学容器。首先克隆 GitHub 项目 gpu-jupyter:

$ git clone \
https://github.com/iot-salzburg/gpu-jupyter.git
$ cd gpu-jupyter

接下来,基于运行 Ubuntu 18.04 的 NVIDIA CUDA 10.1 生成一个Dockerfile,并从 Jupyter Docker 栈聚合 Docker 文件配置:

$ ./generate-Dockerfile.sh
$ cd .build

最后,编辑生成的 docker 文件或保持原样。使用标签构建,在本例中为apk8s/jupyter-ds:cuda-10.1,并推送:

$ docker build -t apk8s/jupyter-ds:cuda-10.1 .
$ docker push apk8s/jupyter-ds:cuda-10.1

构建这个巨大的容器可能需要几分钟到几小时的时间,这取决于速度和分配给本地工作站上 Docker 守护进程的资源。

下一节使用这个容器作为 JupyterHub 提供的一个选项。

JupyterHub Spawner 选项

第六章介绍了 JupyterHub 作为一种为用户提供 Jupyter 容器的方式。将 JupyterHub 安装到新的hc1集群中(在本章前面已经设置好了),并为values.yml(用于配置 Helm 图)更新了singleuser部分。用清单 11-9 的内容替换singleuser部分。

singleuser:
  image:
    name: apk8s/datalab
    tag: v0.0.5
  defaultUrl: "/lab"
  storage:
    dynamic:
      storageClass: rook-ceph-block
      capacity: 10Gi
  extraEnv:
    MLFLOW_TRACKING_URI: "http://mlflow.data:5000"
    MLFLOW_S3_ENDPOINT_URL: "http://minio-internal-service.data:9000"
  profileList:
    - display_name: "DataLab Environment"
      description: "Python, Julia, Octave and R."
      default: true
    - display_name: "GPU Data Science Environment."
      description: "Data science applications and libraries with cuda-10.1 support."
      kubespawner_override:
        image: apk8s/jupyter-ds:cuda-10.1
        extra_resource_guarantees:
          nvidia.com/gpu: "1"
        extra_resource_limits:
          nvidia.com/gpu: "1"

Listing 11-9Updated singleuser configuration for JupyterHub

新的singleuser配置创建了一个profileList,为用户提供了选择 Jupyter 环境的选项。 38 第一个条目是第六章中使用的默认数据科学容器;第二个选项是上一节中构建的新的支持 GPU 的apk8s/jupyter-ds:cuda-10.1容器。键extra_resource_guarantees通知 Kubernetes 调度程序将容器放在至少有一个 GPU 可用的节点上。密钥extra_resource_limits确保用户不会访问一个以上的 GPU。本章稍后会用到环境变量MLFLOW_TRACKING_URIMLFLOW_S3_ENDPOINT_URL

下面的部分使用这个新的 GPU 数据科学容器来开发机器学习模型。它引入 MLflow 来跟踪他们的学习进度,并将模型提交给注册表。

模型开发

机器学习模型开发类似于任何软件开发——组织或个人解决问题、提高生产率或创造新价值的愿望。在数据科学中,原材料是数据以及其中可能存在的潜在价值。这本书展示了在数据科学环境中提供大量数据管理应用的能力。

机器学习是一个多样化的领域,有着巨大的技术、术语和实践生态系统。这本书没有涵盖机器学习模型的开发,因为这是一个快速发展的领域,不断引入新的框架和技术。然而,几年来出现了一些适合自动化的模式,这些模式和容器化一起非常适合 Kubernetes。其余部分涵盖了机器学习自动化的四个组件:跟踪模型、版本控制和存储模型、服务模型以及部署使用它们的推理工作负载(API)。

MLflow

MLflow 39 是一个开源的机器学习平台,最初由 Databricks 40 开发,并捐赠(于 2020 年)给 Linux 基金会。MLflow 每月下载量超过 200 万次, 41 已经被机器学习社区迅速采用。

本节的剩余部分将 MLflow 安装到本章前面构建的新的hc1 Kubernetes 集群中。以下示例使用 MinIO(参见第七章)作为 MLflow 用于模型和工件存储的 S3 兼容对象存储, 42 和 Jupyter 环境(参见本章上一节)用于开发模型和与 MLflow 交互。

装置

MLflow 支持本地和基于云的操作。在 Kubernetes 上共享 MLflow 实例需要容器化版本。通过首先用清单 11-10 的内容创建一个 requirements.txt 文件来创建一个新的 MLflow 容器。

mlflow==1.8.0
awscli==1.18.65
boto3==1.13.15

Listing 11-10MLflow container requirements.txt

接下来,在 requirements.txt 所在的目录中,创建一个 Dockerfile,其中包含清单 11-11 的内容。

FROM python:slim-buster

COPY requirements.txt /requirements.txt
RUN pip install -r /requirements.txt
ENV PORT 5000

ENTRYPOINT ["mlflow"]

Listing 11-11MLflow Dockerfile

接下来,构建并推送 MLflow 容器:

$ docker build -t apk8s/mlflow:1.8.0-v0 .
$ docker push apk8s/mlflow:1.8.0-v0

接下来,用 MLflow 配置 Kubernetes。创建目录cluster-apk8s-hc1/020-data/800-mlflow。在新的800-MLflow目录中,从清单 11-12 中创建一个名为10-service.yml的文件。

apiVersion: v1
kind: Service
metadata:
  name: mlflow
  namespace: data
  labels:
    app: mlflow
spec:
  selector:
    app: mlflow
  ports:
    - protocol: "TCP"
      port: 5000
      targetPort: 5000
  type: ClusterIP

Listing 11-12MLflow Service

应用服务:

$ kubectl apply -f 10-service.yml

接下来,在清单 11-13 中的文件40-statefulset.yml中为 MLflow 创建一个 StatefulSet。StatefulSet 配置使用前面创建的新 MLflow 容器。它将环境变量MLFLOW_S3_ENDPOINT_URL设置为运行在 hc1(在数据名称空间中)、AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY上的 MinIO 集群的服务端点,这些环境变量是从包含 MinIO 凭证的 Kubernetes secret 挂载的。MLflow 使用这些环境变量来访问模型和工件的对象存储。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mlflow
  namespace: data
  labels:
    app: mlflow
spec:
  serviceName: mlflow
  replicas: 1
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: mlflow
  template:
    metadata:
      labels:
        app: mlflow
    spec:
      containers:
        - name: mlflow
          image: apk8s/mlflow:1.8.0-v0
          imagePullPolicy: IfNotPresent
          args: [
            "server",
            "--backend-store-uri=sqlite:///MLflow/data.db",
            "--default-artifact-root=s3://MLflow/artifacts",
            "--expose-prometheus=/metrics",
            "--host=0.0.0.0",
            "--port=5000"
          ]
          env:
            - name: MLFLOW_S3_ENDPOINT_URL
              value: http://minio-internal-service.data:9000
            - name: AWS_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: minio-creds-secret
                  key: accesskey
            - name: AWS_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: minio-creds-secret
                  key: secretkey
          volumeMounts:
            - name: mlflow-data-volume
              mountPath: /mlflow
          ports:
            - name: http
              containerPort: 5000
  volumeClaimTemplates:
    - metadata:
        name: mlflow-data-volume
      spec:
        storageClassName: rook-ceph-block
        accessModes: [ ReadWriteOnce ]
        resources:
          requests:
            storage: 10Gi

Listing 11-13MLflow StatefulSet

应用声明集:

$ kubectl apply -f 10-service.yml

接下来,在清单 11-14 中名为50-ingress.yml的文件中为 MLflow 创建一个入口。本演示使用基本授权 43 对公共入口进行简单的密码保护。但是,对于高级身份验证支持,可以考虑编写一个插件。 44

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: mlflow
  namespace: data
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
    kubernetes.io/ingress.class: traefik
    ingress.kubernetes.io/auth-type: "basic"
    ingress.kubernetes.io/auth-secret: "sysop-basic-auth"
spec:
  rules:
    - host: mlflow.hc1.apk8s.dev
      http:
        paths:
          - backend:
              serviceName: mlflow
              servicePort: 5000
            path: /
  tls:
    - hosts:
       - mlflow.hc1.apk8s.dev
      secretName: mlflow-production-tls

Listing 11-14MLflow Ingress

应用入口:

$ kubectl apply -f 50-ingress.yml

MLflow 现在可以在位于服务端点mlflow.data:5000hc1集群和可公开访问的基于 web 的用户界面 https://mlflow.hc1.apk8s.dev 上使用。接下来的部分将演示如何使用 MLflow 来跟踪、存储和版本化模型。

跟踪模型

机器学习模型开发涉及整个过程的详细记录。数据科学家修改源代码、超参数和训练数据,都是为了实现更高质量的模型。跟踪与结果模型相关联的参数、代码版本、输出度量和工件是高效和稳定的机器学习开发的关键。MLflow 安装在前面的小节中,它在 http://mlflow.data:5000 公开了一个用于跟踪模型开发的 API 端点。

从运行在新的hc1集群( https://hub.hc1.apk8s.dev/ )中的 JupyterHub 打开一个 Jupyter 环境。

Note

为简洁起见,以下示例不使用 GPU 但是,使用新的支持 GPU 的环境(在本章中配置),更高级的读者可能会将以下示例应用于 TensorFlow 或 PyTorch。此外,更高级的用户可能希望试验在 Elasticsearch 和 MinIO 中收集和存储(本章前面)的 Raspberry Pi 传感器数据。

以下练习改编自官方 MLflow 教程 45 46 使用 scikit-learn elastic net47线性回归模型使用葡萄酒质量数据集。 48

在一个新的 Python 3 Jupyter 笔记本中添加以下每个代码块作为单独的单元。

首先,安装以下软件包:

!pip install mlflow==1.8.0
!pip install scikit-learn==0.23.1
!pip install boto3==1.10.35

接下来,MLflow 需要以下环境变量:MLFLOW_TRACKING_URI访问 API 服务器;MLFLOW_S3_ENDPOINT_URL上传模型和工件;还有AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY载着给MLFLOW_S3_ENDPOINT_URL的国书。Jupyter 笔记本通过 JupyterHub 设置可访问的环境变量有多种方式(参见上一节中的 JupyterHub 单用户配置)。这些值也可以直接在 Jupyter 笔记本中设置,仅用于测试和调试目的;不要在生产工作的源代码中设置 S3 凭据:

import os

# api and object access
os.environ['MLFLOW_TRACKING_URI'] = "http://mlflow.data:5000"
os.environ['MLFLOW_S3_ENDPOINT_URL'] = "http://minio-hl-svc.data:9000"

# minio credentials
os.environ['AWS_ACCESS_KEY_ID'] = "REDACTED"
os.environ['AWS_SECRET_ACCESS_KEY'] = "REDACTED"

接下来,导入所需的包,并为 NumPy random 设置一个种子,以帮助复制结果:

import pandas as pd
import numpy as np
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.linear_model import ElasticNet
from urllib.parse import urlparse
import mlflow
import mlflow.sklearn

np.random.seed(70)

接下来,创建一个用于评估模型性能的函数:

def eval_metrics(actual, pred):
    rmse = np.sqrt(mean_squared_error(actual, pred))
    mae = mean_absolute_error(actual, pred)
    r2 = r2_score(actual, pred)
    return rmse, mae, r2

接下来,下载数据并将其分为训练集和测试集:

csv_url =\
    'http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv'

data = pd.read_csv(csv_url, sep=';')
train, test = train_test_split(data)

接下来,通过分离 quality 列来准备测试集和训练集:

train_x = train.drop(["quality"], axis=1)
test_x = test.drop(["quality"], axis=1)
train_y = train[["quality"]]
test_y = test[["quality"]]

接下来,如果不存在 MLflow 实验,则创建一个新的 ml flow 实验:

experiment_name = 'SkLearnWineQuality'
experiment = mlflow.get_experiment_by_name(experiment_name)

if experiment == None:
    mlflow.create_experiment(experiment_name)
    experiment = mlflow.get_experiment_by_name(experiment_name)

mlflow.set_experiment(experiment.name)

接下来,将模型、日志记录指标和参数以及经过训练的模型和源代码训练到 MLflow:

alpha = 1
l1_ratio = 2.5

with mlflow.start_run() as run:

    mlflow.set_tags({
        "mlflow.user": "apk8s",
        "mlflow.source.type": "NOTEBOOK",
        "mlflow.source.name": "SkLearnWineQuality",
    })

    lr = ElasticNet(
        alpha=alpha,
        l1_ratio=l1_ratio,
        random_state=42
    )
    lr.fit(train_x, train_y)

    predicted_qualities = lr.predict(test_x)

    (rmse, mae, r2) = eval_metrics(
        test_y, predicted_qualities)

    print("Elasticnet model (alpha=%f, l1_ratio=%f):"
          % (alpha, l1_ratio))
    print("  RMSE: %s" % rmse)
    print("  MAE: %s" % mae)
    print("  R2: %s" % r2)

    mlflow.log_param("alpha", alpha)
    mlflow.log_param("l1_ratio", l1_ratio)

    mlflow.log_metric("rmse", rmse)
    mlflow.log_metric("r2", r2)
    mlflow.log_metric("mae", mae)

    mlflow.log_artifact("SkLearnWineQuality.ipynb")
    mlflow.sklearn.log_model(lr, "model",
                             registered_model_name="SkLearnWineModel")

mlflow.end_run()

前面代码的每次运行都会导致一个新条目进入SkLearnWineQuality实验。浏览至 https://mlflow.hc1.apk8s.dev/ 并导航至实验。从那里,观察各种运行及其结果(参见图 11-12 )。

img/483120_1_En_11_Fig12_HTML.jpg

图 11-12

MLflow 记录的培训/实验

点击一个运行条目来查看细节以及与该运行相关的工件,在这个例子中,包括模型包 49 和源代码(参见图 11-13 )。

img/483120_1_En_11_Fig13_HTML.jpg

图 11-13

MLflow 打包模型

MLflow 带来了必不可少的机器学习组件,进一步缩小了原始数据和基于机器学习的人工智能之间的差距。在这一点上,新的 hc1 Kubernetes 混合集群支持从物联网设备、支持 GPU 的节点和多个云实例收集原始数据,提供入口、分布式存储、数据库、ETL 和机器学习开发应用。

机器学习开发的最后一步是生产部署,这将在下一节中介绍。

部署人工智能

机器学习模型的部署方法通常取决于问题域、业务需求和现有的基础设施。然而,一些项目在走向标准化的过程中获得了巨大的牵引力,特别是开源的 50 项目 Seldon Core。

很少核心

这最后一节将介绍基于机器学习的模型在 Seldon Core 上的部署。Seldon Core 是 Kubernetes 的开源模型部署控制器。Seldon Core 与已建立的模型打包标准集成良好,提供预构建的推理服务器,包括支持 MLflow、 51 scikit-learn、TensorFlow 和 XGBoost,并提供用于构建定制推理服务器的接口。本节仅使用部署前一节中构建的简单机器学习模型所需的 Seldon Core 的一小部分功能。

在撰写本文时,Seldon Core 不支持本章开发的 hc1 集群所使用的 Kubernetes 1.18。然而,Seldon Core 是一个活跃的项目,对最新 Kubernetes 版本的支持也不远了。然而,这个小限制提供了配置名为c2的额外集群的机会,在本例中,是 Linode 上的单节点 k3s 集群。 52

创建一个具有 4 个 CPU、8 GB RAM 和 160 GB 存储空间的 Linode 实例,运行 Ubuntu 18.04,并使用 Tokyo 2 区域来保持全局主题。命名服务器tokyo2-1并为指向其公共 IP 的tokyo2-1.c2.apk8s.dev*.c2.apk8s.dev创建 DNS A条目。

指导 k3s 安装 Kubernetes v1.17 版:

$ export INSTALL_K3S_VERSION=v1.17.7+k3s1

创建并保存一个群集密码,用其他节点扩展此群集时需要该密码:

$ export K3S_CLUSTER_SECRET=$(head -c48 /dev/urandom | base64)

# copy the echoed secret
$ echo $K3S_CLUSTER_SECRET

安装 k3s:

$ curl -sfL https://get.k3s.io | sh -s – server

将 k3s kubectl 配置文件下载到本地工作站:

$ scp root@tokyo2-1.c2.apk8s.dev:/etc/rancher/k3s/k3s.yaml  ~/.kube/apk8s-c2

编辑新配置以指向 c2 Kubernetes API,并命名集群和上下文:

$ sed -i .bk "s/default/apk8s-c2/" ~/.kube/apk8s-c2
$ sed -i .bk "s/127.0.0.1/tokyo2-1.c2.apk8s.dev/" ~/.kube/apk8s-c2

使用新的apk8s-c2配置在位置工作站上的新终端中使用c2集群:

$ export KUBECONFIG=~/.kube/apk8s-c2

为很少核心创建命名空间:

$ kubectl create namespace seldon-system

安装带 Helm 的少芯操作器 53 :

$ helm install seldon-core seldon-core-operator \
    --repo https://storage.googleapis.com/seldon-charts \
    --set usageMetrics.enabled=true \
    --namespace seldon-system

创建目录cluster-apk8s-c2/。在新目录中,用清单 11-15 中的内容创建一个名为000-sd-s3-secret.yml的文件。用访问模型所需的 MinIO 凭据替换修订值,这些凭据与 MLflow 一起存储(请参考本章前面的 MLflow 配置)。

apiVersion: v1
kind: Secret
metadata:
  name: seldon-s3-model-secret
type: Opaque
stringData:
  AWS_ENDPOINT_URL: "https://minio.hc1.apk8s.dev"
  AWS_ACCESS_KEY_ID: "REDACTED"
  AWS_SECRET_ACCESS_KEY: "REDACTED"
  USE_SSL: "true"

Listing 11-15Secret containing s3 (MinIO) config and credentials

应用秘密:

$ kubectl apply -f 000-sd-s3-secret.yml

接下来,用清单 11-16 中的内容创建一个名为100-sd-quality.yml的文件。将modelUri:值更改为 MLflow 模型的位置(见图 11-13 )配置。额外的componentSpecs:是可选的,并配置了更多的就绪性和活性探测等待时间,以更好地考虑资源受限的c2集群。按照第二章中最初所述安装证书管理器,以便在连接的入口配置上提供 TLS 支持。

apiVersion: machinelearning.seldon.io/v1alpha2
kind: SeldonDeployment
metadata:
  name: quality
spec:
  name: quality
  predictors:
    - graph:
        children: []
        implementation: MLFLOW_SERVER
        modelUri: s3://mlflow/artifacts/2/1b3cfc890bd04a85a60fa0706b9e8592/artifacts/model
        envSecretRefName: seldon-s3-model-secret
        name: quality
      name: default
      replicas: 1
      componentSpecs:
        - spec:
            containers:
              - name: quality
                readinessProbe:
                  failureThreshold: 10
                  initialDelaySeconds: 120
                  periodSeconds: 10
                  successThreshold: 1
                  tcpSocket:
                    port: http
                  timeoutSeconds: 5
                livenessProbe:
                  failureThreshold: 10
                  initialDelaySeconds: 120
                  periodSeconds: 10
                  successThreshold: 1
                  tcpSocket:
                    port: http
                  timeoutSeconds: 5
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: quality
  labels:
    app: quality
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
    kubernetes.io/ingress.class: traefik
spec:
  rules:
    - host: quality.c2.apk8s.dev
      http:
        paths:

          - backend:
              serviceName: quality-default
              servicePort: 8000
            path: /
  tls:
    - hosts:
        - quality.c2.apk8s.dev
      secretName: default-quality-production-tls

Listing 11-16SeldonDeployment with Ingress

应用很少部署和进入:

$ kubectl apply -f 100-sd-quality.yml

部署模型可能需要几分钟时间。监视默认命名空间中新生成的 Pod 的状态;一旦两个容器中的两个报告就绪,Pod 就可以接受发布的数据并提供预测。

通过发布模型的预期输入,使用curl测试部署,在本例中是一个二维数组(或数组的数组),每个数组包含进行预测所需的 11 个值。该模型为每个内部数组返回一个预测:

$ curl -X POST https://quality.c2.apk8s.dev/api/v1.0/predictions \
      -H 'Content-Type: application/json' \
      -d '{ "data": { "ndarray": [[ 6.4, 0.57, 0.02, 1.8, 0.067, 4.0, 11.0, 0.997, 3.46, 0.68, 9.5]] } }'

返回的预测是5.703684339252623:

{"data":{"names":[],"ndarray":[5.703684339252623]},"meta":{}}

这一部分移动得很快,轻描淡写地触及了谢顿核心能力的表面。然而,它展示了一系列不同组件之间几乎无缝的互操作性,从在 Jupyter 笔记本电脑中构建 scikit-learn 模型,在 MLflow 中跟踪和服务模型,到使用 Seldon Core 进行最终部署,所有这些都集成在 Kubernetes 之上。

摘要

本章在 Raspberry Pi 设备、公共云和内部安装了九个节点,利用资源受限的低成本基础设施,通过加密网络在全球范围内进行通信。目的是演示使用 Kubernetes 创建一个机器学习平台,并涵盖支持该平台的数据的整个生命周期。

本章展示了 Kubernetes 在提供广泛的功能以及抽象和统一物联网部署、ETL、数据科学和人工智能的底层基础设施方面的适用性。虽然一些组件如 Apache NiFi 和 Elasticsearch 与 Kubernetes API 没有直接联系,但其他组件如 Jupyter 的 KubeSpawner 和 Seldon Core 则直接使用它。

虽然这一章涵盖了很多领域,但它只是简要地触及了开发和部署基于机器学习的人工智能作为平台的每个关键方面。然而,使平台成为平台的是改进和扩展的框架。

Kubernetes 促进了平台架构的高级概念,通过聚合和组合实现了构造方法的标准化。Kubernetes 提供了在没有直接 it 意识的情况下聚合组件的能力,以及为直接利用或扩展其 API 而构建的组件,几乎完全抽象了底层基础架构,同时提供了伸缩性、容错和自修复。Kubernetes 通过连接物联网、机器学习、区块链、大数据和 Kubernetes API 的最新创新时产生的组合效应,促进了创新问题解决平台的探索。

posted @ 2024-08-12 11:18  绝不原创的飞龙  阅读(14)  评论(0编辑  收藏  举报