Kubernetes-上的大数据-全-

Kubernetes 上的大数据(全)

原文:annas-archive.org/md5/c47e39c1bc577f11068c3d7425d9a76d

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在当今数据驱动的世界里,处理和分析海量数据的能力已成为各行各业企业的重要竞争优势。大数据技术作为处理日益增长的数据量、速度和多样性的强大工具,帮助组织提取有价值的见解并推动基于数据的决策。然而,管理和扩展这些技术可能是一项艰巨的任务,通常需要大量的基础设施和运营开销。

进入 Kubernetes,这一开源容器编排平台已经彻底改变了我们部署和管理应用程序的方式。通过提供标准化和自动化的容器管理方法,Kubernetes 简化了复杂应用程序(包括大数据工作负载)的部署和扩展。本书旨在弥合这两种强大技术之间的差距,引导你通过在 Kubernetes 上实施强大且可扩展的大数据架构的过程。

在各章内容中,你将开始一段全面的学习之旅,从容器和 Kubernetes 架构的基础知识入手。你将学习如何构建和部署 Docker 镜像,了解 Kubernetes 的核心组件,并获得在本地和云端设置 Kubernetes 集群的实践经验。这一扎实的基础将为你后续深入现代数据堆栈的学习打下基础。

本书将向你介绍大数据生态系统中最广泛采用的工具,如用于数据处理的 Apache Spark、用于管道编排的 Apache Airflow 以及用于实时数据摄取的 Apache Kafka。你不仅将学习这些技术背后的理论概念,还将获得在 Kubernetes 上实施这些技术的实践经验。通过一系列动手实践和项目,你将深入理解如何构建和部署数据管道、处理大数据集,并在 Kubernetes 集群上编排复杂的工作流。

随着书籍的进展,你将探索更高级的主题,例如使用 Trino 和 Elasticsearch 部署数据消费层,以及通过 Amazon Bedrock 集成生成性 AI 工作负载。这些主题将为你提供在 Kubernetes 上构建和维护强大、可扩展的大数据架构所需的知识和技能,确保高效的数据处理、分析和分析应用程序的部署。

在本书的结尾,你将全面了解大数据与 Kubernetes 之间的协同作用,使你能够利用这些技术的强大功能推动创新和业务增长。无论你是数据工程师、DevOps 专业人士,还是技术爱好者,本书将为你提供在 Kubernetes 上成功实施和管理大数据工作负载所需的实用知识和动手经验。

本书适用对象

如果你是数据工程师、云架构师、DevOps 专业人士、数据或科学经理,或技术爱好者,那么本书适合你。你应该具备 Python 和 SQL 编程的基础知识,以及对 Apache Spark、Apache Kafka 和 Apache Airflow 的基本了解。对 Docker 和 Git 的基本理解也将有所帮助。

本书内容

第一章容器入门,带你开始了解容器和 Docker,这是现代应用程序部署的基础技术。你将学习如何安装 Docker 并运行你的第一个容器镜像,亲身体验容器化的强大功能。此外,你还将深入研究 Dockerfile 的细节,掌握制作简洁且功能完善的容器镜像的技巧。通过实践示例,包括使用 Python 构建简单的 API 和数据处理任务,你将掌握将服务和任务容器化的技巧。到本章结束时,你将有机会通过构建自己的任务和 API 来巩固新获得的知识,为一系列基于容器的实际应用程序构建作品集奠定基础。

第二章Kubernetes 架构,将向你介绍构成 Kubernetes 架构的核心组件。你将了解控制平面组件,如 API 服务器、etcd、调度器和控制器管理器,以及工作节点组件,如 kubelet、kube-proxy 和容器运行时。本章将解释每个组件的角色和职责,以及它们如何相互作用以确保 Kubernetes 集群的平稳运行。此外,你将深入了解 Kubernetes 中的关键概念,包括 pod、部署、服务、任务、有状态集合、持久化卷、ConfigMaps 和 Secrets。本章结束时,你将对 Kubernetes 的架构和核心概念有坚实的基础,为随后的实践操作做好准备。

第三章Kubernetes – 实践操作,将指导你通过使用 kind 部署本地 Kubernetes 集群的过程,并使用 Amazon EKS 部署基于云的集群。你将学习成功部署 EKS 集群所需的最小 AWS 账户配置。设置集群后,你将有机会选择将应用程序部署到本地环境还是云环境。无论你做出什么选择,你都将重新进行第一章中开发的 API 和数据处理工作,并将其部署到 Kubernetes。这个实践经验将巩固你对 Kubernetes 概念的理解,并为后续章节的更高级话题做好准备。

第四章现代数据架构,向你介绍了最著名的数据架构设计,重点讲解了“lambda”架构。你将了解构成现代数据架构的数据湖(房)架构所需的工具。这些工具包括用于数据处理的 Apache Spark、用于数据管道编排的 Apache Airflow,以及用于实时事件流处理和数据摄取的 Apache Kafka。本章将为你提供这些工具的概念性介绍,并展示它们如何协同工作,构建数据湖(房)架构的核心技术资产。

第五章使用 Apache Spark 进行大数据处理,向你介绍了 Apache Spark,这是一种广泛使用的大数据处理工具。你将了解 Spark 程序的核心组件,如何扩展和处理分布式计算,以及与 Spark 协作的最佳实践。你将通过 DataFrames API 和 Spark SQL API 实现简单的数据处理任务,并使用 Python 与 Spark 进行交互。本章将指导你在本地安装 Spark 以进行测试,帮助你在将其部署到更大规模之前,先获得与这个强大工具的实际操作经验。

第六章使用 Apache Airflow 构建数据管道,向你介绍了 Apache Airflow,这是一种被广泛采用的开源工具,用于数据管道的编排。你将了解如何使用 Docker 和 Astro CLI 安装 Airflow,使得安装过程变得简单。本章将让你熟悉 Airflow 的核心功能和最常用的操作符,以便执行数据工程任务。此外,你将深入了解如何构建具有韧性和高效性的可靠数据管道,充分利用 Airflow 的能力。通过本章的学习,你将牢固掌握如何使用 Airflow 编排复杂的数据工作流,这是任何从事大数据工作并在 Kubernetes 上运行的数工程师或数据架构师所必备的技能。

第七章使用 Apache Kafka 进行实时事件流处理和数据摄取,向你介绍了 Apache Kafka,这是一种分布式事件流平台,广泛应用于构建实时数据管道和流式应用程序。你将理解 Kafka 的架构,以及它如何扩展并保持韧性,使其能够处理高容量的实时数据并保持低延迟。你将学习 Kafka 的分布式主题设计,这一设计支撑了其强大的实时事件处理能力。本章将引导你通过 Docker 在本地运行 Kafka,并实施基本的读取和写入操作。此外,你还将探讨不同的数据复制和主题分发策略,确保你能够设计并实现高效可靠的 Kafka 集群。

第八章在 Kubernetes 上部署大数据技术栈,引导你了解如何将前几章学到的大数据工具部署到 Kubernetes 集群中。你将从编写 bash 脚本开始,部署 Spark 操作符并在 Kubernetes 上运行SparkApplications。接下来,你将部署 Apache Airflow 到 Kubernetes,从而在集群内协调数据管道。此外,你还将使用临时集群和 JBOD 技术将 Apache Kafka 部署到 Kubernetes。Kafka Connect 集群也会被部署,并通过连接器将数据从 SQL 数据库迁移到持久化对象存储。到本章结束时,你将拥有一个完全可运行的大数据技术栈,准备好进行进一步的探索和开发。

第九章数据消费层,引导你了解如何在 Kubernetes 上部署的大数据架构中,安全地将数据提供给业务分析师。你将从了解使用“数据湖引擎”而非数据仓库的现代方法开始。在本章中,你将熟悉 Trino,了解如何直接通过 Kubernetes 从数据湖中进行数据消费。你将理解数据湖引擎的工作原理,将其部署到 Kubernetes 中,并监控查询执行和历史记录。此外,对于实时数据,你将熟悉 Elasticsearch 和 Kibana 进行数据消费。你将部署这些工具,并学习如何在其中索引数据以及如何使用 Kibana 构建简单的数据可视化。

第十章在 Kubernetes 中构建大数据管道,引导你了解如何在 Kubernetes 集群中部署和协调两个完整的数据管道,一个用于批处理,另一个用于实时处理。你将连接本书中学到的所有工具,如 Apache Spark、Apache Airflow、Apache Kafka 和 Trino,构建一个单一的复杂解决方案。你将在 Kubernetes 上部署这些工具,编写数据处理和协调的代码,并通过 SQL 引擎使数据可供查询。到本章结束时,你将拥有在 Kubernetes 上构建和管理一个综合大数据管道的实际经验,将各种组件和技术整合成一个统一且可扩展的架构。

第十一章Kubernetes 上的生成式 AI,引导你完成在 Kubernetes 上部署生成式 AI 应用的过程,使用 Amazon Bedrock 作为基础模型的服务套件。你将学习如何将应用连接到作为检索增强生成RAG)层的知识库,这通过提供对外部信息源的访问,增强了 AI 模型的能力。此外,你将发现如何通过代理自动化任务执行,使生成式 AI 无缝集成到你的工作流中。在本章结束时,你将对如何在 Kubernetes 上利用生成式 AI 的力量有一个坚实的理解,从而为个性化客户体验、智能助手和自动化商业分析开启新可能。

第十二章从这里开始的旅程,引导你迈向掌握大数据和 Kubernetes 的下一步。你将探索一些关键概念和技术,这些技术对在 Kubernetes 上构建强大且可扩展的解决方案至关重要。包括 Kubernetes 和应用程序的监控策略、实现高效通信的服务网格、保护你的集群和应用程序、启用自动扩展、采用 GitOps 和 CI/CD 实践以简化部署和管理,以及 Kubernetes 成本控制。每个主题,你都将获得概述和对进一步探索相关技术的建议,帮助你加深这些领域的知识和技能。

为了最大程度地利用本书

一些 Python 编程基础知识以及对 Spark、Docker、Airflow、Kafka 和 Git 的了解将帮助你更好地利用本书的内容。

本书涵盖的软件/硬件 操作系统要求
Python>=3.9 Windows、macOS 或 Linux
Docker,最新版本 Linux
Docker Desktop,最新版本 Windows 或 macOS
Kubectl Windows、macOS 或 Linux
Awscli Windows、macOS 或 Linux
Eksctl Windows、macOS 或 Linux
DBeaver 社区版 Windows、macOS 或 Linux

每一章都会提供软件安装所需的所有指导。

如果你正在使用本书的电子版,我们建议你亲自输入代码,或从本书的 GitHub 仓库访问代码(链接在下一节提供)。这样可以帮助你避免因复制和粘贴代码而产生的潜在错误

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,地址为 github.com/PacktPublishing/Bigdata-on-Kubernetes。如果代码有更新,它将在 GitHub 仓库中更新。

我们还提供来自我们丰富书籍和视频目录的其他代码包,可以在github.com/PacktPublishing/找到。快去看看吧!

使用的约定

本书中使用了许多文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户。示例:“此命令将从 Docker Hub 公共仓库拉取hello-world镜像,并在其中运行应用程序。”

代码块设置如下:

import pandas as pd
url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv'
df = pd.read_csv(url, header=None)
df["newcolumn"] = df[5].apply(lambda x: x*2)
print(df.columns)
print(df.head())
print(df.shape)

任何命令行输入或输出如下所示:

$ sudo apt install docker.io

以上代码片段上方的文件名将显示如下:

Cjava.py

粗体:表示新术语、重要词汇或屏幕上显示的文字。例如,菜单或对话框中的文字通常以粗体显示。示例:“您应该确保在配置页面上选中使用 WSL 2 而非 Hyper-V选项。”

小贴士或重要提示

显示为如下格式。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请通过电子邮件联系我们:customercare@packtpub.com,并在邮件主题中注明书名。

勘误表:尽管我们已经尽力确保内容的准确性,但难免会有错误。如果您在本书中发现错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上遇到我们作品的任何非法版本,恳请提供该内容的地址或网站名称。请通过电子邮件联系我们:copyright@packt.com,并附上相关链接。

如果您有兴趣成为作者:如果您在某个主题上拥有专长,并且有兴趣编写或参与书籍的撰写,请访问authors.packtpub.com

分享您的想法

阅读完《Kubernetes 上的大数据》后,我们很乐意听取您的反馈!请点击这里直接访问本书的亚马逊评论页面并分享您的意见。

您的评论对我们和技术社区非常重要,帮助我们确保提供优质内容。

下载本书的免费 PDF 副本

感谢购买本书!

喜欢随时阅读但无法随身携带纸质书籍?

您的电子书购买是否与您选择的设备不兼容?

不用担心,现在每本 Packt 书籍都附赠免费的无 DRM 版本 PDF。

随时随地在任何设备上阅读。从您最喜欢的技术书籍中直接搜索、复制并粘贴代码到您的应用程序中。

优惠不止于此,您还可以获取独家折扣、通讯和每日精彩免费内容的订阅

遵循以下简单步骤获取这些好处:

  1. 扫描下方的 QR 码或访问以下链接

下载本书的免费 PDF 副本

packt.link/free-ebook/978-1-83546-214-0

  1. 提交您的购买证明

  2. 就这些了!我们将免费 PDF 和其他福利直接发送到您的电子邮件

第一部分:Docker 和 Kubernetes

在这部分中,您将学习容器化和 Kubernetes 的基础知识。您将首先了解容器的基础知识以及如何构建和运行 Docker 镜像。这将为您在处理容器化应用程序时提供坚实的基础。接下来,您将深入探讨 Kubernetes 架构,探索其组件、特性以及如 pod、部署和服务等核心概念。有了这些知识,您将能够熟练地操作 Kubernetes 生态系统。最后,您将通过部署本地和云端 Kubernetes 集群,并将之前构建的应用程序部署到这些集群上,获得实际操作经验。

此部分包含以下章节:

  • 第一章,开始使用容器

  • 第二章,Kubernetes 架构

  • 第三章,Kubernetes – Hands On

第一章:容器入门

随着移动设备、社交媒体、电商交易、传感器等各种来源迅速生成大量数据,世界正面临着数据爆炸。这种数据爆炸通常被称为“大数据”。尽管大数据为企业和组织提供了获取有价值洞察的巨大机会,但它也带来了如何存储、处理、分析和从大量多样化的数据中提取价值的巨大复杂性。

这时,Kubernetes 就派上了用场。Kubernetes 是一个开源容器编排系统,帮助自动化容器化应用的部署、扩展和管理。Kubernetes 为构建大数据系统带来了重要的优势。它提供了一种标准方式来在任何基础设施上部署容器化的大数据应用。这使得应用可以轻松地在本地服务器或云服务商之间迁移。同时,它也使得根据需求对大数据应用进行水平扩展变得更加简单。可以根据使用情况自动启动或关闭额外的容器。

Kubernetes 通过自愈和自动重启失败容器等特性,帮助确保大数据应用的高可用性。它还提供了一种统一的方法来部署、监控和管理不同的大数据组件。这与分别管理每个系统相比,显著减少了操作复杂性。

本书旨在为您提供实际技能,帮助您利用 Kubernetes 构建强大且可扩展的大数据管道。您将学习如何在 Kubernetes 上容器化并部署流行的大数据工具,如 Spark、Kafka、Airflow 等。本书涵盖了构建批处理和实时数据管道的架构最佳实践和实际示例。

到本书结束时,您将全面了解如何在 Kubernetes 上运行大数据工作负载,并能够构建高效的数据平台,支持分析和人工智能应用。无论您是数据工程师、数据科学家、DevOps 工程师,还是在推动组织数字化转型的技术领导者,您所获得的知识都将具有巨大价值。

Kubernetes 的基础是容器。容器是当今数据工程中最常用的技术之一。它们允许工程师将软件打包成标准化的单元,用于开发、运输和部署。在本章结束时,您将理解容器的基本概念,并能够使用 Docker 构建和运行自己的容器。

在本章中,我们将介绍容器是什么,为什么它们有用,以及如何使用 Docker 在本地机器上创建和运行容器。容器解决了开发者在应用程序迁移过程中面临的许多问题。它们确保应用程序及其依赖项被打包在一起,并与底层基础设施隔离开。这使得应用程序能够在不同的计算环境之间快速而可靠地运行。

我们将从在本地系统上安装 Docker 开始,Docker 是一个用于构建和运行容器的平台。我们将运行简单的 Docker 镜像,并学习基本的 Docker 命令。接着,我们将构建第一个包含简单 Python 应用的 Docker 镜像。我们将学习如何定义 Dockerfile,以高效地指定应用的环境和依赖项。然后,我们将以容器的形式运行该镜像,探索如何访问应用并检查其日志。

容器是现代软件部署的关键技术。它们轻量、可移植且可扩展,使你能够更快地构建和发布应用程序。本章中你将学习的概念和技能将为你使用容器和部署数据应用提供坚实的基础。到本章结束时,你将准备好开始构建和部署自己的容器化数据处理任务、API 和数据工程工具。

在本章中,我们将涵盖以下主要内容:

  • 容器架构

  • 安装 Docker

  • 入门 Docker 镜像

  • 构建自己的镜像

技术要求

对于本章内容,你需要已经安装 Docker。此外,电脑至少需要 4 GB 的内存(推荐 8 GB),因为 Docker 会消耗大量的计算机内存。

本章的代码可以在 GitHub 上找到。请参考 github.com/PacktPublishing/Bigdata-on-Kubernetes 并访问 Chapter01 文件夹。

容器架构

容器是一种操作系统级别的虚拟化方法,我们可以利用它在单一主机上运行多个隔离的进程。容器允许应用在独立的环境中运行,拥有自己的依赖项、库和配置文件,而不需要像完整的虚拟机VM)那样的开销,这使得容器更加轻量且高效。

如果将容器与传统的虚拟机进行比较,它们在几个方面有所不同。虚拟机在硬件层面进行虚拟化,创建一个完整的虚拟操作系统。而容器则是在操作系统层面进行虚拟化。因此,容器共享主机系统的内核,而虚拟机则每个都有自己的内核。这使得容器的启动时间非常快,通常以毫秒为单位,而虚拟机则需要几分钟(值得注意的是,在 Linux 环境中,Docker 可以直接利用 Linux 内核的功能。然而,在 Windows 系统中,Docker 则运行在一个轻量级的 Linux 虚拟机中,尽管如此,它仍然比完整的虚拟机更轻便)。

此外,容器具有更好的资源隔离性,因为它们仅隔离应用层,而虚拟机则隔离整个操作系统。容器是不可变的基础设施,使得它们在更新时创建新的容器镜像(版本),而不是在原地更新,从而使它们更加便携和一致。

由于这些差异,容器相比虚拟机允许更高的密度、更快的启动时间和更低的资源使用。单台服务器可以运行数十个甚至数百个彼此隔离的容器化应用程序。

Docker 是最受欢迎的容器平台之一,提供了构建、运行、部署和管理容器的工具。Docker 架构由 Docker 客户端、Docker 守护进程、Docker 注册表和 Docker 镜像组成。

Docker 客户端是一个命令行界面CLI)客户端,用于与 Docker 守护进程交互,构建、运行和管理容器。这一交互通过 REST API 实现。

Docker 守护进程是一个在主机机器上运行的后台服务,负责构建、运行和分发容器。它是所有容器运行的基础。

Docker 注册表是一个用于托管、分发和下载 Docker 镜像的仓库。Docker Hub 是默认的公共注册表,里面有许多预构建的镜像,但云服务提供商通常也有自己的私有容器注册表。

最后,Docker 镜像是只读的模板,用于创建 Docker 容器。镜像定义了容器的环境、依赖关系、操作系统、环境变量以及容器运行所需的一切。

图 1.1 显示了在虚拟机中运行的应用程序与在容器中运行的应用程序之间的区别。

图 1.1 – 虚拟机与容器

图 1.1 – 虚拟机与容器

图 1.2 显示了容器如何通过分层运行。底部是共享的内核。在此之上,我们可以有任意数量的操作系统层。Debian 操作系统层之上,我们看到 Java 8 镜像和 NGINX 镜像。Java 8 层被三个容器共享,其中一个仅包含镜像信息,另外两个使用不同的镜像,Wildfly。该图展示了为什么容器架构如此高效地共享资源和轻量,因为它是建立在各类库、依赖关系和应用程序的层之上的,这些层会彼此隔离运行。

图 1.2 – 容器层

图 1.2 – 容器层

现在,开始吧。在接下来的部分中,你将学习如何安装 Docker 并运行你的第一个 Docker CLI 命令。

安装 Docker

要开始使用 Docker,你可以通过你所使用的 Linux 发行版的包管理器进行安装,或者为 Mac/Windows 机器安装 Docker Desktop。

Windows

要在 Windows 上使用 Docker Desktop,你必须启用 WSL 2 功能。参考此链接获取详细说明:docs.microsoft.com/en-us/windows/wsl/install-win10

之后,你可以按照以下步骤安装 Docker Desktop:

  1. 访问www.docker.com/products/docker-desktop下载安装程序。

  2. 下载完成后,双击安装程序并按照提示操作。

    你应该确保在 配置 页面上选择了 使用 WSL 2 替代 Hyper-V 选项。这是推荐的使用方式。(如果你的系统不支持 WSL 2,此选项将不可用。不过,你仍然可以使用 Hyper-V 运行 Docker。)

  3. 安装完成后,关闭并启动 Docker Desktop。

如果你有任何疑问,请参考官方文档:docs.docker.com/desktop/install/windows-install/

macOS

在 macOS 上安装 Docker Desktop 非常简单:

  1. 访问www.docker.com/products/docker-desktop下载 macOS 的安装程序。

  2. 双击安装程序并按照提示安装 Docker Desktop。

  3. 安装完成后,Docker Desktop 将自动启动。

Docker Desktop 在 macOS 上原生运行,使用 HyperKit 虚拟机,无需额外配置。当 Docker Desktop 第一次启动时,它会提示你授权访问驱动器。授权 Docker Desktop 允许其访问文件系统上的文件。

Linux

在基于 Linux 的系统上安装 Docker 非常简单。你只需要使用你的 Linux 发行版的包管理器,执行几条命令即可。例如,在 Ubuntu 上,首先需要删除你以前在机器上安装的任何旧版本的 Docker:

$ sudo apt-get remove docker docker-engine docker.io containerd runc

你可以通过以下方式从默认的apt仓库安装 Docker:

$ sudo apt install docker.io

这将安装一个稍微旧一点的 Docker 版本。如果你想要最新版本,请访问官方 Docker 网站(docs.docker.com/desktop/install/linux-install/)并按照说明操作。

如果你想在不使用sudo的情况下使用 Docker,可以运行以下命令:

$ sudo groupadd docker
$ sudo usermod –aG docker <YOUR_USERNAME>

现在,让我们动手实践 Docker。

入门 Docker 镜像

我们可以运行的第一个 Docker 镜像是hello-world镜像。它通常用于测试 Docker 是否正确安装并运行。

hello-world

安装完成后,打开终端(Windows 中的命令提示符)并运行以下命令:

$ docker run hello-world

该命令将从 Docker Hub 公共仓库拉取hello-world镜像并运行其中的应用。如果你成功运行它,你将看到以下输出:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
70f5ac315c5a: Pull complete
Digest: sha256:88ec0acaa3ec199d3b7eaf73588f4518c25 f9d34f58ce9a0df68429c5af48e8d
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
 1\. The Docker client contacted the Docker daemon.
 2\. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3\. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading.
 4\. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/
For more examples and ideas, visit:
 https://docs.docker.com/get-started/

恭喜!你刚刚运行了第一个 Docker 镜像!现在,让我们尝试一些更有挑战性的操作。

NGINX

NGINX 是一个著名的开源软件,广泛用于 Web 服务、反向代理和缓存。它在基于 Kubernetes 的架构中得到了广泛应用。

hello-world应用(它表现得像一个作业执行程序)不同,NGINX 表现得像一个服务。它会打开一个端口并持续监听用户请求。我们可以通过以下方式在 Docker Hub 中搜索可用的 NGINX 镜像:

$ docker search nginx

输出将显示几个可用的镜像。通常,列表中的第一个是官方镜像。现在,为了设置一个运行中的 NGINX 容器,我们可以使用以下命令:

$ docker pull nginx:latest

这将下载当前的最新版本镜像。冒号后面的latest关键字代表该镜像的“标签”。如果要安装特定版本(推荐),请像这样指定标签:

$ docker pull nginx:1.25.2-alpine-slim

你可以访问hub.docker.com/_/nginx查看所有可用的标签。

现在,为了运行容器,你应该指定想要使用的镜像版本。以下命令可以完成这个任务:

$ docker run --name nginxcontainer –p 80:80 nginx:1.25.2-alpine-slim

你将开始在终端中看到 NGINX 日志。然后,打开你喜欢的浏览器,输入http://localhost/。你应该能看到这个信息(图 1.3):

图 1.3 – 浏览器中的 nginx 默认输出

图 1.3 – 浏览器中的 nginx 默认输出

docker run命令有一些重要的参数。--name定义了将运行的容器名称。如果你没有定义名称,Docker 会自动为它选择一个名字(相信我,它可以非常有创意)。-p将你机器上的一个端口(端口80)连接到容器内的一个端口(同样是80)。如果你不打开这个端口,你将无法访问容器中的应用程序。

测试成功后,返回到运行容器的终端并按下“CTRL + C”停止容器。停止后,容器仍然存在,尽管不再运行。要删除容器,请使用以下命令:

$ docker rm nginxcontainer

如果有疑问,你可以使用此命令查看所有正在运行和已停止的容器:

$ docker ps –a

我们还可以使用这个命令查看所有本地可用的镜像:

$ docker images

在我的情况下,输出显示了三个镜像:hello-world 和两个 NGINX 镜像,其中一个是 latest 标签,另一个是 1.25.2-alpine-slim 标签。所有镜像及其对应版本都会显示。

Julia

在这个最后的示例中,我们将学习如何通过与正在运行的容器进行交互,使用我们机器上未安装的技术。我们将运行一个名为 Julia 的新型高效编程语言容器,它适用于数据科学。执行以下命令来实现:

$ docker run -it --rm julia:1.9.3-bullseye

注意,docker run 命令会查找本地镜像。如果本地没有该镜像,Docker 会自动从 Docker Hub 拉取镜像。使用前面的命令,我们将在 Julia 1.9.3 容器中启动一个交互式会话。-it 参数使我们可以交互使用该容器。--rm 参数表示容器停止后会自动删除,因此我们无需手动删除它。

容器启动并运行后,我们将尝试一个简单的自定义函数来计算两个描述性统计量:均值和标准差。你会在终端看到 Julia 的标志,并可以使用以下命令:

$ using Statistics
$ function descriptive_statistics(x)
    m = mean(x)
    sd = std(x)
    return Dict("mean" => m, "std_dev" => sd)
  end

定义好函数后,我们将用一个小的随机数字数组来运行它:

$ myvector = rand(5)
$ descriptive_statistics(myvector)

你应该能在屏幕上看到正确的输出。恭喜你!你刚刚使用了 Julia 编程语言,而无需在你的计算机上安装或配置它,只需通过 Docker!要退出容器,请使用以下命令:

$ exit()

由于我们使用了 --rm 参数,如果运行 docker ps -a 命令,我们会看到它已被自动删除。

构建你自己的镜像

现在,我们将定制自己的容器镜像,以便运行一个简单的数据处理任务和 API 服务。

批处理任务

下面是一个简单的 Python 代码示例,用于批处理任务:

run.py

import pandas as pd
url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv'
df = pd.read_csv(url, header=None)
df["newcolumn"] = df[5].apply(lambda x: x*2)
print(df.columns)
print(df.head())
print(df.shape)

这段 Python 代码从 URL 加载 CSV 数据集到 pandas DataFrame,新增一列,方法是将现有列的值乘以 2,然后打印出 DataFrame 的一些信息(列名、前五行以及 DataFrame 的大小)。在你喜欢的代码编辑器中键入这段代码,并将文件保存为 run.py

通常,我们会在本地测试代码(尽可能地),以确保它能够正常工作。为此,首先需要安装 pandas 库:

pip3 install pandas

然后,使用以下命令运行代码:

python3 run.py

如果一切顺利,你应该会看到如下输出:

Index([0, 1, 2, 3, 4, 5, 6, 7, 8, 'newcolumn'], dtype='object')
   0    1   2   3    4     5      6   7  8  newcolumn
0  6  148  72  35    0  33.6  0.627  50  1       67.2
1  1   85  66  29    0  26.6  0.351  31  0       53.2
2  8  183  64   0    0  23.3  0.672  32  1       46.6
3  1   89  66  23   94  28.1  0.167  21  0       56.2
4  0  137  40  35  168  43.1  2.288  33  1       86.2
(768, 10)

现在,我们已经准备好将我们的简单处理任务打包到一个容器中了。让我们先定义一个 Dockerfile:

Dockerfile_job

FROM python:3.11.6-slim
RUN pip3 install pandas
COPY run.py /run.py
CMD python3 /run.py

这四行就是我们定义一个工作容器所需要的内容。第一行指定了要使用的基础镜像,这是一个精简版的 Python 3.11.6。它是基于 Debian 的操作系统,已经安装了 Python 3.11.6,这能为我们节省大量时间。使用精简镜像非常重要,因为它能保持容器大小小巧,优化传输时间和存储成本(如果适用)。

第二行安装了 pandas 库。第三行将本地的 run.py 文件复制到容器中。最后一行设置了容器启动时执行的默认命令,即运行 /run.py 脚本。在定义完代码后,保存为 Dockerfile_job(没有 .extension)。现在,是时候构建我们的 Docker 镜像了:

docker build -f Dockerfile_job -t data_processing_job:1.0 .

docker build 命令根据 Dockerfile 中的指令构建镜像。通常,这个命令会期望一个名为 Dockerfile 的文件。由于我们使用的文件名与预期不同,因此我们必须使用 -f 标志告诉 Docker 使用哪个文件。-t 标志定义了镜像的标签。标签由名称和版本组成,中间用冒号(:)分隔。在这种情况下,我们为镜像设置的名称是 data_processing_job,版本是 1.0。该命令的最后一个参数是代码文件所在的路径。这里我们设置了当前文件夹,使用一个点(.)。这个点很容易忘记,所以请小心!

构建完成后,我们可以通过以下命令检查本地可用的镜像:

docker images

你应该能在输出的第一行看到你刚刚构建的数据显示处理镜像:

REPOSITORY       TAG  IMAGE ID      CREATED        SIZE
data_process...  1.0  39bae1eb068c  6 minutes ago  351MB

现在,要在容器内部运行我们的数据处理任务,请使用以下命令:

docker run --name data_processing data_processing_job:1.0

docker run 命令运行指定的镜像。--name 标志定义容器的名称为 data_processing。当你启动容器时,应该会看到与之前相同的输出:

Index([0, 1, 2, 3, 4, 5, 6, 7, 8, 'newcolumn'], dtype='object')
   0    1   2   3    4     5      6   7  8  newcolumn
0  6  148  72  35    0  33.6  0.627  50  1       67.2
1  1   85  66  29    0  26.6  0.351  31  0       53.2
2  8  183  64   0    0  23.3  0.672  32  1       46.6
3  1   89  66  23   94  28.1  0.167  21  0       56.2
4  0  137  40  35  168  43.1  2.288  33  1       86.2
(768, 10)

最后,别忘了清理你环境中的退出容器:

docker ps –a
docker rm data_processing

恭喜!你已经使用容器运行了第一个任务。接下来,让我们进入另一种容器化应用类型:服务。

API 服务

在这一部分中,我们将使用 app,并创建一个名为 main.py 的 Python 脚本。

在脚本中,首先导入 FastAPI 和 random 模块:

from fastapi import FastAPI
import random

接下来,我们创建 FastAPI 应用实例:

app = FastAPI()

下一个代码块使用 @``app.get 装饰器定义一个路由:

@app.get("/api")
async def root():
    return {"message": "You are doing great with FastAPI..."}

@app.get 装饰器表示这是一个 GET 类型的端点。这个函数被定义为响应 "/api" 路由的请求。它只是在请求该路由时返回一个愉快的消息。

下一个代码块定义了一个路由 "/api/{name}",其中 name 是在请求中接收的参数。它返回一个包含给定名称的问候信息:

@app.get("/api/{name}")
async def return_name(name):
    return {
        "name": name,
        "message": f"Hello, {name}!"
    }

最后的代码块定义了 "/joke" 路由。这个函数从之前定义的笑话列表中随机返回一个(非常有趣的!)笑话。可以随意用你自己酷的笑话替换它们:

@app.get("/joke")
async def return_joke():
    jokes = [
        "What do you call a fish wearing a bowtie? Sofishticated.",
        "What did the ocean say to the beach? Nothing. It just waved",
        "Have you heard about the chocolate record player? It sounds pretty sweet."
    ]
    return {
        "joke": random.choice(jokes)
    }

需要注意的是,每个函数都返回一个 JSON 格式的响应。这是 API 中非常常见的模式。有关完整的 Python 代码,请参考本书的 GitHub 仓库(github.com/PacktPublishing/Bigdata-on-Kubernetes)。

在我们构建 Docker 镜像之前,建议先在本地测试代码(尽可能的话)。为此,你需要安装 fastapiuvicorn 包。在终端中运行以下命令:

pip3 install fastapi uvicorn

运行 API 时,使用以下命令:

uvicorn app.main:app --host 0.0.0.0 --port 8087

如果一切顺利,你将在终端中看到如下输出:

INFO:  Started server process [14417]
INFO:  Waiting for application startup.
INFO:  Application startup complete.
INFO:  Uvicorn running on http://0.0.0.0:8087 (Press CTRL+C to quit)

该命令将在 8087 端口本地运行 API 服务。要进行测试,请打开浏览器并访问 http://localhost:8087/api。你应该能看到屏幕上显示的编程消息。也可以测试 http://localhost:8087/api/<YOUR_NAME>http://localhost:8087/joke 端点。

现在我们知道一切运行正常,接下来将 API 打包成 Docker 镜像。为此,我们将构建一个简单的 Dockerfile。为了优化它,我们将使用 alpine Linux 发行版,这是一种极为轻量的基础操作系统。在项目的根文件夹中创建一个名为 Dockerfile(没有 .extension)的新文件。这是我们将用于该镜像的代码:

Dockerfile

FROM python:3.11-alpine
RUN pip3 --no-cache-dir install fastapi uvicorn
EXPOSE 8087
COPY ./app /app
CMD uvicorn app.main:app --host 0.0.0.0 --port 8087

第一行导入基于 alpine Linux 发行版的 Python 容器。第二行安装 fastapiuvicorn。第三行告知 Docker,容器在运行时将监听 8087 端口。如果没有这条命令,我们将无法访问 API 服务。第四行将本地 /app 文件夹中的所有代码复制到容器内的 /app 文件夹中。最后,CMD 命令指定了容器启动时要运行的命令。在这里,我们启动 uvicorn 服务器来运行我们的 FastAPI 应用程序。在 uvicorn 后面,我们指定了一个位置模式 folder.script_name:FastAPI_object_name,以告诉 FastAPI 在哪里查找 API 进程对象。

当这个 Dockerfile 被构建成镜像时,我们将得到一个容器化的 Python 应用程序,配置为在 8087 端口运行 FastAPI Web 服务器。Dockerfile 允许我们将应用程序及其依赖项打包成一个标准化的部署单元。要构建镜像,请运行以下命令:

docker build -t my_api:1.0 .

这里无需指定 -f 标志,因为我们使用的是默认名称的 Dockerfile。记得在命令末尾加上一个点!

现在,我们使用稍微不同的一组参数来运行容器:

docker run -p 8087:8087 -d --rm --name api my_api:1.0

-p 参数设置了我们将在服务器(在此情况下为你的计算机)中将 8087 端口映射到容器中的 8087 端口。如果不设置此参数,就无法与容器进行任何通信。-d 参数以 分离 模式运行容器。终端将不会显示容器日志,但容器会在后台运行,终端仍然可以使用。--rm 参数设置容器在完成后自动删除(非常方便)。最后,--name 为容器设置名称为 api

我们可以使用以下命令检查容器是否正确运行:

docker ps –a

如果需要查看容器的日志,请使用以下命令:

docker logs api

你应该看到类似如下的输出:

INFO:  Started server process [1]
INFO:  Waiting for application startup.
INFO:  Application startup complete.
INFO:  Uvicorn running on http://0.0.0.0:8087 (Press CTRL+C to quit)

现在,我们可以通过之前显示的相同链接在浏览器中测试我们的 API 端点(http://localhost:8087/apihttp://localhost:8087/api/<YOUR_NAME>http://localhost:8087/joke)。

恭喜!你已经在容器内部运行了你的 API 服务。这是一个完全便携且自包含的应用程序,可以部署到任何地方。

要停止 API 服务,请使用以下命令:

docker stop api

要检查已停止的容器是否已被自动删除,请使用以下命令:

docker ps -a

做得很好!

总结

在本章中,我们介绍了容器的基本原理以及如何使用 Docker 构建和运行它们。容器提供了一种轻量级、便携的方式来打包应用程序及其依赖项,以便它们能够在各种环境中可靠地运行。

你已经了解了关键概念,例如镜像、容器、Dockerfile 和仓库。我们安装了 Docker,并运行了简单的容器,如 NGINX 和 Julia,进行实践操作。你还为批处理作业和 API 服务构建了自己的容器,定义了 Dockerfile 来打包依赖项。

这些技能使你能够开发应用程序并将其容器化,以便能够顺利地在任何地方部署。容器非常有用,因为它们确保每次都能按预期运行你的软件。

在下一章中,我们将学习如何使用 Kubernetes 来编排容器,轻松地扩展、监控和管理容器化应用程序。我们将重点介绍 Kubernetes 中最重要的概念和组件,并学习如何通过 YAML 文件(清单)实现它们。

第二章:Kubernetes 架构

理解 Kubernetes 架构对于正确利用其能力至关重要。在本章中,我们将介绍构成 Kubernetes 集群的主要组件和概念。熟悉这些构建块将帮助你了解 Kubernetes 如何在幕后工作。

我们将首先看看构成 Kubernetes 集群的不同组件——控制平面和工作节点。控制平面由 API 服务器、控制器管理器和 etcd 等组件组成,负责管理和维护集群的期望状态。工作节点在 Pod 中运行容器化应用。

在介绍集群架构之后,我们将深入探讨 Kubernetes 的主要抽象和 API 资源,如 Pods、部署、StatefulSets、服务、Ingress 和持久化卷。这些资源允许你声明应用的期望状态,并让 Kubernetes 调整实际状态以匹配它。理解这些概念是能够在 Kubernetes 上部署和管理应用的关键。

我们还将查看支持资源,如 ConfigMaps 和 Secrets,这些可以将配置与代码分开。Jobs 提供对批处理工作负载的支持。

到本章结束时,你将对 Kubernetes 集群的构建方式以及如何利用其 API 资源来发挥其能力有一个扎实的理解。这将使你能够开始部署自己的应用并高效地管理它们。

我们将通过以下主要主题来讲解这些概念:

  • 集群架构

  • Pods

  • 部署

  • StatefulSets

  • Jobs

  • 服务

  • Ingress 和 ingress 控制器

  • 网关

  • 持久化卷

  • ConfigMaps 和 Secrets

技术要求

本章没有技术要求。这里展示的所有代码都是通用的,实际可执行的示例将在 第三章 中给出。

Kubernetes 架构

Kubernetes 是一种集群架构。这意味着在完整的生产环境中,通常会有多台机器同时运行工作负载,以创建可靠且可扩展的架构。(请注意,Kubernetes 也可以在单台机器上运行,这非常适合测试,但对于生产环境来说并不合适。)

为了协调集群功能,Kubernetes 有两个主要功能组:控制平面负责集群管理,节点组件与控制平面通信并在工作节点上执行任务。图 2.1 展示了整个系统的表示。

图 2.1 – Kubernetes 架构

图 2.1 – Kubernetes 架构

让我们仔细看看每个组及其组件。

控制平面

控制平面的主要组件如下:

  • kube-apiserver

  • etcd

  • kube-scheduler

  • kube-controller-manager

当运行基于云的 Kubernetes 集群时,我们还有一个组件叫做 cloud-controller-manager。

kube-apiserver

Kubernetes API 通过 kube-apiserver 向管理员开放。它可以看作是控制平面的“前端”。通过 API 服务器,我们与 Kubernetes 进行交互,向集群发送指令或从集群获取数据。它具有高度的可扩展性,支持横向扩展(通过部署更多工作节点到集群中)。

etcd

Kubernetes 使用 etcd 作为一个分布式键值数据库,持久化存储所有集群数据和状态。etcd 作为 Kubernetes API 服务器的后端存储,提供了一个安全且具有韧性的基础,支持在集群节点之间协调容器的运行。通过利用 etcd 在一致性、高可用性和分布式方面的能力,Kubernetes 可以可靠地管理应用程序和基础设施的期望状态。即使集群中的领导节点发生故障,etcd 仍具有容错能力。

kube-scheduler

kube-scheduler 负责将工作负载或容器分配到多个节点。它会监控那些尚未分配到任何节点的已创建 Pod,并为它们选择一个运行节点。为了做出调度决策,kube-scheduler 会分析单个和集体可用资源、硬件/软件/策略约束、亲和性和反亲和性规则、截止时间、数据位置以及工作负载之间的潜在干扰。

kube-controller-manager

kube-controller-manager 运行控制器来调节集群中的行为,如节点控制器、任务控制器、EndpointSlice 控制器和 ServiceAccount 控制器。控制器通过对比期望状态和当前状态来进行调节。

cloud-controller-manager

cloud-controller-manager 与底层云提供商进行交互,并设置基于云的网络服务(如网络路由和负载均衡器)。它将与云提供商交互的组件与仅在集群内运行的组件分离开来。此控制器管理器仅运行与所使用的云提供商相关的控制器,因此,如果你在运行本地测试 Kubernetes 实例,cloud-controller-manager 将不会被使用,因为它只处理基于云的服务。

接下来,让我们看看节点组件。

节点组件

每个集群的工作节点中都包含节点组件,这些组件负责与控制平面通信、运行和维护工作负载,并提供运行时环境。主要组件如下:

  • 容器运行时

  • kubelet

  • kube-proxy

容器运行时

容器运行时 是负责运行容器的底层软件。Kubernetes 支持多种容器运行时,但最常见的是 Docker 和 containerd。容器运行时负责从镜像仓库拉取镜像、运行容器并管理容器的生命周期。

kubelet

kubelet 是主要的节点代理,负责监视分配给节点的 Pods,并确保容器正在运行并且健康。它与容器运行时交互,以拉取镜像并运行容器。

kube-proxy

kube-proxy 是一个网络代理和负载均衡器,通过维护网络规则和执行连接转发,在每个节点上实现 Kubernetes 网络服务。

现在,让我们将注意力转向我们将在构建工作负载时使用的 Kubernetes 组件。

Pods

Pods 是 Kubernetes 中最小的可部署单元,表示应用程序的单个实例。Pod 包含一个或多个容器(尽管最常见的情况是每个 Pod 仅包含一个容器)。当多个容器位于同一个 Pod 中时,它们会保证被调度到同一个节点上,并且能够共享资源。

Pods 提供两个主要的好处:

  • localhost 并共享卷等资源。这有助于相关容器之间的轻松通信。不过,需要注意的是,这是一个高级用例,应该仅在容器紧密耦合时使用。我们通常使用 Pods 来进行单容器部署。

  • 管理与部署:Pods 是在 Kubernetes 中部署、扩展和管理的单位。你不会直接创建或管理 Pod 内的容器。整个过程是完全自动化的。

通常,你不会直接定义 Pods。Pods 可以通过其他资源(如部署、任务和 StatefulSets)创建和管理。然而,你也可以通过合适的 YAML 文件清单来定义一个 Pod。

.yaml 文件。YAML 文件 通常用于配置。它们与 JSON 文件非常相似,但由于依赖缩进来表示代码层次结构,而不是使用括号和大括号,因此更具可读性。以下是一个部署单个 Pod 的清单示例:

pod.yaml

apiVersion: v1
kind: Pod
metadata:
    name: myapp-pod
    labels:
    app: myapp
spec:
    containers:
    - name: myapp-container
    image: myimage:latest
    ports:
    - containerPort: 80

让我们更仔细地看看这个清单的每个部分:

  • apiVersion:此清单中对象的 Kubernetes API 版本。对于 Pods,版本为 v1

  • kind:正在创建的对象类型,对于此清单是 Pod

  • metadata:此部分包含 Pod 的元数据,例如名称和标签。名称是唯一标识符。标签将在未来特别重要,因为它们作为其他 Kubernetes 资源(如部署和服务)的标识符。

  • spec:此部分定义 Pod 的期望状态,包括其容器。

  • containers:指定运行在 Pod 内的容器。包括镜像、端口等信息。

  • image:容器使用的 Docker 镜像,可以是公共镜像或私有镜像。

  • ports: 定义容器暴露的端口。

这涵盖了 Pod 清单的基本结构。Pods 提供了一种在 Kubernetes 中部署和管理容器的简单方式。现在我们已经讨论了 Pods 及其定义方式,让我们讨论一种通过部署自动化更复杂 Pod 结构的方法。

Deployments

Deployments是 Kubernetes 中运行应用程序最重要的资源之一。它们提供了一种声明式的方法来管理 Pods 和副本。

部署定义了应用程序的所需状态,包括容器镜像、副本数、资源限制等。Kubernetes 控制平面会将集群的实际状态与部署中所需的状态进行匹配。

例如,这是一个简单的部署清单:

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
    name: myapp-deployment
spec:
    replicas: 3
    selector:
    matchLabels:
      app: myapp
    template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: nginx:1.16
        ports:
        - containerPort: 80

让我们逐节分析:

  • apiVersion: 这是指定 Kubernetes API 版本的字段,用于部署资源。我们需要使用apps/v1版本,该版本包含了 Deployments。

  • kind: Deployment: 我们正在创建的资源类型,这里是 Deployment。

  • metadata: 资源的标准元数据,如唯一名称。

  • spec: 部署的规格。它定义了所需的状态。

  • replicas: 3: 我们希望运行三个 Pod 副本。Kubernetes 将保持这个 Pod 数。

  • selector: 用于匹配由此部署管理的 Pod。Pod 将根据标签选择器进行选择。

  • template: 将要创建的 Pod 模板。它定义了 Pod 的规格。请注意,部署将与指定的标签相关联。

  • spec: containers: Pod 的规格,包括要运行的容器。

  • image: nginx:1.16: 使用的容器镜像。

  • ports: 容器暴露的端口。

当此部署应用时,Kubernetes 将启动三个与模板匹配的 Pod,每个 Pod 运行一个 Nginx 容器。部署控制器将监控这些 Pod,并确保所需状态与实际状态一致,必要时会重启 Pod。

部署提供了强大的功能,用于在 Kubernetes 上运行可扩展和弹性的应用程序。使用声明式配置使得部署变得简便。接下来,我们将讨论管理 Pods 和副本的另一种方法:StatefulSets。

StatefulSets

StatefulSets 是 Kubernetes 资源,用于管理有状态应用程序,如数据库。它们类似于 Deployments,但专门设计用于处理需要持久存储和唯一网络标识符的有状态工作负载。

StatefulSet 管理包含有状态应用的 Pods(必须为其他应用或用户会话跟踪数据的应用)。StatefulSet 中的 Pods 具有粘性的、唯一的身份,即使在重新调度时也能保持不变。这使得每个 Pod 在重启或重新调度到新节点时能够保持其状态。因此,StatefulSets 非常适合像数据库这样的有状态应用,需要数据持久化。而 Deployments 则是为无状态工作负载设计的,提供没有持久化存储的相同 Pods,因此更适用于无状态的 Web 应用。

StatefulSets 通过为每个 Pod 创建持久化存储卷(将在本章后续部分介绍)来运行。这样可以确保数据在 Pod 重启时得以持久保存。StatefulSets 还为每个 Pod 提供独特的主机名和稳定的网络 ID,使用可预测的命名规则。

这里是一个部署 MySQL 数据库的 StatefulSet 清单示例:

statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
    name: mysql
spec:
    serviceName: "mysql"
    replicas: 1
    selector:
    matchLabels:
      app: mysql
    template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
    volumeClaimTemplates:
    - metadata:
      name: mysql-persistent-storage
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "my-storage-class"
      resources:
        requests:
          storage: 1Gi

该清单为 MySQL 创建了一个包含一个副本的 StatefulSet。它使用 volumeClaimTemplate 动态为每个 Pod 配置一个持久化存储卷。MySQL 数据将保存在 /``var/lib/mysql 路径下。

每个 Pod 都会得到一个唯一的名称,如 mysql-0,并且具有一个稳定的主机名。如果 Pod 被重新调度,它将重新挂载其持久化存储卷,继续以有状态的方式运行。

通过这种方式,StatefulSets 为 Kubernetes 中的数据库和其他有状态应用提供了强大的有状态管理功能。它们确保数据持久化、稳定的网络、有序的部署以及平滑的扩展。接下来,我们将继续讨论 Kubernetes 作业。

作业

作业是 Kubernetes 中的一个基本资源类型,用于运行直到完成的批处理任务。与长时间运行的服务(如 Web 服务器)不同,作业旨在在批处理任务完成时终止。

一个作业创建一个或多个 Pod,运行定义的工作负载,并在工作负载完成后终止。对于数据处理、机器学习训练或任何有限计算等任务,这种方式非常有用。

要创建一个作业,你可以在 YAML 清单中定义一个 Job 资源,如下所示:

job.yaml

apiVersion: batch/v1
kind: Job
metadata:
    name: myjob
spec:
    template:
    spec:
      containers:
      - name: myjob
        image: busybox
        command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 30']
      restartPolicy: Never
    backoffLimit: 4

让我们逐部分仔细看一下这段代码:

  • apiVersion ' batch/v1 用于作业

  • kind ' Job

  • metadata.name ' 作业的名称

  • spec.template ' Pod 模板,定义了容器(们),与我们在之前的资源定义中看到的方式相同

  • spec.template.spec.restartPolicy ' 设置为 Never,因为作业不应该重启

  • spec.backoffLimit ' 可选的失败任务重试次数限制

spec.template 下的 pod 模板定义了要运行的容器,就像 pod 清单一样。你可以指定镜像、命令、环境变量等。一个重要的设置是 restartPolicy,对于作业来说,它应该设置为 Never,以确保如果 pod 失败或退出时不会被重启。backoffLimit 是可选的,指定一个失败的作业 pod 可以重试的次数。默认值是 6。如果不希望作业在失败后重试太多次,可以将此值设置得更低。

当你创建作业时,Kubernetes 会调度一个或多个符合模板的 pod 来运行你的工作负载。当 pod 完成时,Kubernetes 会跟踪它们的状态并知道作业何时完成。你可以查看作业状态和 pod 日志以监控进度。作业使得在 Kubernetes 上运行批量计算工作负载变得更容易。在接下来的部分中,我们将深入了解 Kubernetes 服务。

服务

服务 提供了稳定的端点,以便访问在集群中运行的 pod,从而将我们的应用程序暴露给在线用户。它们允许 pod 死亡和复制,而不会中断对在这些 pod 中运行的应用程序的访问。Kubernetes 中有几种类型的服务。我们将详细讨论其中的三种:ClusterIP、NodePort 和负载均衡器。

ClusterIP 服务

ClusterIP 服务提供了一个仅在集群内部可访问的 IP 地址。这个 IP 地址在服务生命周期内不会改变,为访问 pod 提供了一个稳定的端点。以下是一个 ClusterIP 服务清单的示例:

service_clusterip.yaml

apiVersion: v1
kind: Service
metadata:
    name: my-service
spec:
    type: ClusterIP
    selector:
    app: my-app
    ports:
    - protocol: TCP
     port: 80
     targetPort: 9376

这个清单创建了一个名为 my-service 的服务,它会将请求转发到具有 app: my-app 标签的 pod,端口为 80。请求将被转发到目标 pod 上的 9376 端口。在此服务存在期间,ClusterIP 不会改变。

NodePort 服务

NodePort 服务通过在每个节点上分配的端口使得内部的 ClusterIP 服务能够在外部访问。NodePort 从配置的范围中分配(默认是 30000-32767),并且在每个节点上都是相同的。对 <NodeIP>:<NodePort> 的流量将被转发到 ClusterIP 服务。以下是一个示例:

service_nodeport.yaml

apiVersion: v1
kind: Service
metadata:
    name: my-service
spec:
    type: NodePort
    selector:
    app: my-app
    ports:
    - port: 80
      targetPort: 9376
      nodePort: 30007

这将在每个节点的 30007 端口上暴露内部的 ClusterIP。对 <NodeIP>:30007 的请求将被转发到服务。

负载均衡器服务

负载均衡器服务提供了一个外部负载均衡器,用于将服务暴露给外部流量。ClusterIP 将服务暴露在 Kubernetes 集群内的内部 IP 地址上,这使得服务仅能在集群内访问。另一方面,负载均衡器通过云提供商的负载均衡器实现将服务暴露到外部,这使得服务能够从 Kubernetes 集群外部进行访问。

负载均衡器的实现依赖于环境。例如,在 AWS 上,这将创建一个 Elastic Load BalancerELB),这是一个 AWS 服务,用于提供托管的负载均衡器。以下是一个示例:

service_loadbalancer.yaml

apiVersion: v1
kind: Service
metadata:
    name: my-service
spec:
    selector:
    app: my-app
    ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
    type: LoadBalancer

这会创建一个负载均衡器并分配一个外部 IP 地址。流量到达外部 IP 后会被转发到内部的 ClusterIP 服务。接下来,我们将讨论另一种使用 Ingress 和 Ingress 控制器定义服务的方法。

Ingress 和 Ingress 控制器

Ingress 资源定义了外部连接到 Kubernetes 服务的规则。它使得 HTTP 和 HTTPS 连接能够访问集群内运行的服务。流量路由由 Ingress 资源中定义的规则控制。要使 Ingress 能够运行,你需要在 Kubernetes 上运行一个 Ingress 控制器。

Ingress 控制器 负责实现 Ingress,通常通过负载均衡器完成。它监视 Ingress 资源,并相应地配置负载均衡器。不同的负载均衡器需要不同的 Ingress 控制器实现。

一些 Ingress 控制器的示例如下:

  • NGINX Ingress Controller:使用 NGINX 作为负载均衡器和反向代理,是最常见和功能最全的控制器之一。

  • HAProxy Ingress Controller:使用 HAProxy 进行负载均衡,提供高性能和高可靠性。

  • Traefik Ingress Controller:一种云原生控制器,集成了 Let’s Encrypt,用于自动生成 HTTPS 证书。

  • AWS ALB Ingress Controller:使用 AWS 应用负载均衡器ALB)。与其他 AWS 服务本地集成。

Ingress 资源包含两个主要部分——后端和规则。后端指定路由到默认服务的未匹配请求。规则包含一组路径和将它们路由到的服务:

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    name: example-ingress
spec:
    backend:
    service:
      name: example-service
      port:
        number: 80
    rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: example-service
                port:
                  number: 80

在这个例子中,根路径 / 的请求将被路由到 example-service 的端口 80pathType: 前缀表示任何子路径也应该路由到该服务。

可以定义多个规则,将不同的路径路由到不同的服务:

spec:
    rules:
    - http:
        paths:
          - path: /foo
            backend:
              service:
                name: foo-service
                port:
                  number: 80
    - http:
        paths:
          - path: /bar
            backend:
              service:
                name: bar-service
                port:
                  number: 80

使用前面的代码,请求 /foo 会被路由到 foo-service,而请求 /bar 会被路由到 bar-service

在某些情况下,我们必须配置具有传输加密的安全连接。当出现这种情况时,我们可以在 Ingress 控制器中使用注解配置高级加密选项:

metadata:
    annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "false"

也可以通过指定主机名来配置基于主机的路由:

spec:
    rules:
    - host: foo.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: foo-service
          servicePort: 80
    - host: bar.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: bar-service
          servicePort: 80

现在 foo.example.com 将路由到 foo-service,而 bar.example.com 将路由到 bar-service

总结来说,Ingress 提供了一种智能路由 HTTP 和 HTTPS 流量到 Kubernetes 集群中服务的方式。Ingress 控制器处理实际的负载均衡和反向代理功能。Ingress 的常见用例包括将服务暴露给外部用户和处理 TLS/SSL。对于生产级 Kubernetes 部署,谨慎配置 Ingress 至关重要。

需要注意的是,Ingress API 已被冻结。 这意味着该 API 将不再接受任何更新,已被 Gateway API 取代。尽管如此,了解它仍然很重要,因为本书中我们将使用的许多大数据工具仍然是通过 Ingress 指令部署的。现在,让我们转向 Gateway API,了解它是如何工作的。

Gateway

Gateway API 是一个 Kubernetes API,提供了一种在 Kubernetes 上动态配置负载均衡和服务网格功能的方式。Gateway API 允许以集中声明的方式定义路由和策略来管理外部流量到 Kubernetes 服务。

Gateway API 中的主要资源如下:

  • GatewayClass ' 定义了一组具有共同配置和行为的网关。它类似于持久化存储卷的 StorageClass 概念。

  • Gateway ' 定义了一组给定主机名的路由。这将 GatewayClass、TLS 证书和其他配置绑定到一组路由。

  • HTTPRoute/TCPRoute ' 定义了实际的路由到 Kubernetes 服务及其策略,如超时、重试等。

下面是一个 GatewayClass 资源的示例:

gateway_class.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
    name: external-lb
spec:
    controllerName: lb.acme.io/gateway-controller

这定义了一个名为 external-lb 的 GatewayClass,将由 lb.acme.io/gateway-controller 控制器处理。

一个 Gateway 资源将主机名和 TLS 证书与 GatewayClass 绑定,如以下代码所示:

gateway.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
    name: my-gateway
spec:
    gatewayClassName: external-lb
    listeners:
    - name: http
    port: 80
    protocol: HTTP

这个名为 my-gateway 的网关使用了之前定义的 external-lb GatewayClass。它处理端口 80 上的 HTTP 流量。请注意,addresses 字段没有指定,因此网关的控制器将为其分配一个地址或主机名。

最后,HTTPRoute 和 TCPRoute 资源定义了实际的后端服务路由。这里是一个示例:

http_route.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
    name: http-route
spec:
    parentRefs:
    - name: my-gateway
    rules:
    - matches:
    - path:
    type: PathPrefix
    value: /foo
    backendRefs:
    - name: my-foo-service
      port: 80
    - matches:
    - path:
        type: PathPrefix
        value: /bar
    backendRefs:
    - name: my-bar-service
      port: 80

这个 HTTPRoute 是之前定义的 my-gateway 网关的子资源。它将请求路由到 /foo 路径,指向端口 80 上的 my-foo-service 服务,且请求到 /bar 的流量将被路由到端口 80 上的 my-bar-service。此外,诸如请求超时、重试和流量拆分等附加功能可以在 Route 资源中进行配置。

网关是配置 Kubernetes 中网络和路由的新方式,且非常有效。集中式的入口流量管理配置充当了单一的事实来源。虽然 Ingress 资源提供了一个简单、声明式的接口,专注于暴露 HTTP 应用程序,但 Gateway API 资源则提供了一个更通用的抽象,用于代理 HTTP 以外的多种协议。此外,它们还将数据平面与控制平面解耦。任何网关控制器都可以使用,包括 NGINX、HAProxy 和 Istio。网关通过 TLS 处理和身份验证提供了更好的安全性,并通过先进的路由规则和策略实现了精细的流量控制。最后,它们在复杂的入口配置中提供了更容易的管理和操作。

接下来,我们将探讨持久化存储卷(persistent volumes)。

持久卷(Persistent Volumes)

Kubernetes 最初是为无状态应用设计的。因此,当在 Kubernetes 上运行有状态应用时,管理存储是一个关键挑战。Kubernetes 提供了抽象,使得存储能够在不同环境中以可移植的方式进行配置和使用。在 Kubernetes 上设计存储基础设施时,需要理解两个主要资源:持久卷PVs)和 持久卷声明PVCs)。PV 代表由集群管理员配置的网络存储单元。与计算节点类似,PVs 成为集群资源池。相对而言,PVC 允许最终用户请求具有定义容量和访问模式的抽象存储。PVC 的功能类似于 pod 资源请求,但用户可以指定所需的卷大小和读/写权限,而不是 CPU 和内存。Kubernetes 控制平面负责绑定匹配的 PV 和 PVC 资源,以根据声明为 pods 配置存储。通过这种角色分离,底层存储层从单个 pod 的生命周期中独立出来。

这是一个持久卷 YAML 清单示例:

persistent_volume.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
    name: pv0003
spec:
    capacity:
    storage: 5Gi
    volumeMode: Filesystem
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    storageClassName: slow
    mountOptions:
    - hard
    - nfsvers=4.1
    nfs:
    path: /tmp
    server: 172.17.0.2

这定义了 NFS 服务器 172.17.0.2 上的 /tmp。回收策略设置为 Recycle,这意味着当释放时,卷会被回收而不是删除。storageClassName 设置为 slow,它可以用于将此 PV 与请求特定存储类的 PVC 匹配。

这是一个持久卷声明 YAML 清单示例:

pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
    name: myclaim
spec:
    accessModes:
    - ReadWriteOnce
    volumeMode: Filesystem
    resources:
    requests:
      storage: 8Gi
    storageClassName: slow
    selector:
    matchLabels:
      release: "stable"

这个 PVC 请求 8GB 的存储,并具有 ReadWriteOnce 访问权限。它指定了 slowstorageClassName,这将使其与前面具有相同存储类的 PV 匹配。还有一个选择器会匹配具有 stable 版本标签的 PV。

当创建 PVC 时,Kubernetes 控制平面会寻找匹配的 PV 进行绑定。匹配条件考虑了访问模式、存储容量和 storageClassName 等因素。一旦绑定,该存储就可以被 pods 挂载。

下面是一个挂载前述 PVC 的 pod YAML:

pod_with_pvc.yaml

kind: Pod
apiVersion: v1
metadata:
    name: mypod
spec:
    containers:
    - name: myfrontend
      image: nginx
      volumeMounts:
      - mountPath: "/var/www/html"
        name: mypd
    volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim

该 pod 将 PVC 挂载到容器中的 /var/www/html。PVC 为 pod 提供了持久存储,即使 pod 被删除或迁移到其他节点,存储也会保留。

存储类(StorageClasses)

StorageClass 对象定义了可以请求的不同 存储类。这允许管理员在同一集群内提供不同级别的存储。以下代码定义了一个标准硬盘 StorageClass 和一个在 GCE 上的快速 SSD StorageClass:

storage_classes.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
    name: standard
provisioner: kubernetes.io/gce-pd
parameters:
    type: pd-standard
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
    name: fast
provisioner: kubernetes.io/gce-pd
parameters:
    type: pd-ssd

--- 行告诉 Kubernetes 我们将两个 YAML 清单聚合在一个文件中。在定义 StorageClass 后,PVC 可以请求特定的存储类:

pvc2.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
    name: myclaim
spec:
    accessModes:
    - ReadWriteOnce
    storageClassName: fast
    resources:
    requests:
      storage: 30Gi

这使得集群能够提供不同类型的存储,而无需用户了解实现的细节。接下来,我们将讨论本章最后一个主题,也是 Kubernetes 安全性中非常重要的一个话题:配置映射和秘密(Secrets)。

配置映射和秘密

配置映射和秘密是两个重要的 Kubernetes 对象,它们允许你将配置数据与应用程序代码分离。这使得你的应用程序更具可移植性、可管理性和安全性。

配置映射(ConfigMaps)

配置映射提供了一种便捷的方式,以声明的方式将配置数据传递给 Pod。它们允许你存储配置数据,而不直接将其放入 Pod 定义或容器镜像中。Pod 可以通过环境变量、命令行参数或将配置映射作为卷挂载来访问存储在配置映射中的数据。使用配置映射使得你能够将配置数据与应用程序代码分离。

使用以下清单,你可以创建一个配置映射来存储配置文件:

config_map.yaml

apiVersion: v1
kind: ConfigMap
metadata:
    name: app-config
data:
    config.properties: |
    app.color=blue
    app.mode=prod

这个配置映射包含一个 config.properties 文件,Pod 可以挂载并使用它。

你还可以从目录、文件或字面值创建配置映射。以下命令展示了每种配置映射定义的示例。这些命令在 shell 中通过 kubectl 可执行文件运行(我们将在下一章深入探讨它):

kubectl create configmap app-config --from-file=path/to/dir
kubectl create configmap app-config --from-file=config.properties
kubectl create configmap app-config --from-literal=app.color=blue

要在 Pod 中使用配置映射,你可以执行以下操作:

  • 从配置映射数据设置环境变量

  • 从配置映射数据设置命令行参数

  • 在卷中使用配置映射的值

以下 YAML 文件定义了一个 Kubernetes Pod,它通过环境变量和将配置映射作为卷来使用配置数据。让我们看看如何实现:

pod_with_configmap.yaml

apiVersion: v1
kind: Pod
metadata:
    name: configmap-demo
spec:
    containers:
    - name: demo
      image: alpine
      env:
        - name: APP_COLOR
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: app.color
      args:
        - $(APP_MODE)
        valueFrom:
          configMapKeyRef:
            name: app-config
            key: app.mode
      volumeMounts:
        - name: config-volume
          mountPath: /etc/config
    volumes:
    - name: config-volume
      configMap:
        name: app-config

让我们仔细看看这段代码,并理解它的作用:

  • 它定义了一个名为 configmap-demo 的 Pod。

  • 该 Pod 有一个名为 demo 的容器,使用 alpine 镜像。

  • 容器设置了两个环境变量:

    • APP_COLORapp-config 配置映射中的 app.color 键设置

    • APP_MODEapp-config 配置映射中的 app.mode 键设置(这是作为运行命令的参数定义的)

  • 容器有一个名为 config-volume 的卷挂载,它挂载了 /etc/config 路径。

  • 该 Pod 定义了一个名为 config-volume 的卷,使用 app-config 配置映射作为数据源。这使得来自配置映射的数据在挂载路径上对容器可用。

尽管配置映射非常有用,但它们并不为机密数据提供保密性。为此,Kubernetes 提供了秘密(Secrets)。

秘密(Secrets)

秘密是一个对象,包含少量敏感数据,如密码、令牌或密钥。秘密允许你存储和管理这些敏感数据,而不会将其暴露在应用程序代码中。

例如,你可以使用 kubectl 从字面值创建一个秘密:

kubectl create secret generic db-secret \
--from-literal=DB_HOST=mysql \
--from-literal=DB_USER=root \
--from-literal=DB_PASSWORD=password123

上面的代码将创建一个包含机密数据库凭据的 Secret。你还可以从文件或目录创建 Secrets:

kubectl create secret generic ssh-key-secret --from-file=ssh-privatekey=/path/to/key
kubectl create secret generic app-secret --from-file=/path/to/dir

Secrets 以base64格式存储数据。这可以防止值在etcd中以plaintext(明文)形式暴露。然而,这些数据并没有被加密。你可以通过以下方式从 Pod 中获取机密数据:

pod_with_secrets.yaml

apiVersion: v1
kind: Pod
metadata:
    name: secret-demo
spec:
    containers:
    - name: demo
    image: nginx
    env:
      - name: DB_HOST
        valueFrom:
          secretKeyRef:
            name: db-secret
            key: DB_HOST
      - name: DB_USER
        valueFrom:
          secretKeyRef:
            name: db-secret
            key: DB_USER
      - name: DB_PASSWORD
        valueFrom:
          secretKeyRef:
            name: db-secret
            key: DB_PASSWORD
    volumeMounts:
      - name: secrets-volume
        mountPath: /etc/secrets
        readOnly: true
    volumes:
    - name: secrets-volume
      secret:
        secretName: app-secret

上面的 YAML 文件定义了一个从 Kubernetes API 获取机密的 Kubernetes Pod。我们来看看这段代码:

  • 它定义了一个名为secret-demo的 Pod。

  • 该 Pod 有一个名为demo的容器,基于 NGINX 镜像。

  • 容器有三个环境变量,它们的值来自机密:

    • DB_HOST的值来自db-secret秘密中的DB_HOST键。

    • DB_USER的值来自db-secret秘密中的DB_USER键。

    • DB_PASSWORD的值来自db-secret秘密中的DB_PASSWORD键。

  • 容器在/etc/secrets路径挂载了一个名为secrets-volume的卷。该卷为只读。

  • secrets-volume卷使用app-secret秘密作为其存储后端。因此,app-secret中的任何键/值都将作为文件暴露在容器中的/etc/secrets路径下。

总结

在本章中,我们介绍了构成 Kubernetes 集群的基本架构和组件。了解控制平面、节点及其组件对于有效操作 Kubernetes 至关重要。

我们了解了控制平面中的 API 服务器、etcd、控制器管理器和调度器如何管理和维护期望的集群状态。kubelet 和 kube-proxy 在节点上运行,与控制平面通信并管理容器。熟悉这些构建块有助于建立一个关于 Kubernetes 内部运作的心智模型。

我们还探讨了用于部署和管理应用程序的主要 API 资源,包括 Pods、Deployments、StatefulSets、Jobs 和 Services。Pods 封装了容器,并为紧密相关的容器提供网络和存储。Deployments 和 StatefulSets 允许声明式地管理 Pod 副本,并提供自我修复能力。Jobs 使得批量工作负载能够执行完毕。Services 实现了 Pods 之间的松耦合,并提供稳定的网络。

我们讨论了 ingress 资源和 ingress 控制器如何通过路由规则配置对集群服务的外部访问。新的 Gateway API 提供了一种集中式的方式来管理 ingress 配置。PersistentVolumes 和 PersistentVolumeClaims 允许有效地配置和消费可移植的网络附加存储。StorageClasses 使得不同类别的存储可以被提供。

最后,我们探讨了 ConfigMaps 和 Secrets 如何以解耦的方式将配置信息和敏感数据注入到 Pods 中。总体而言,这些 API 资源为应用程序的部署和管理提供了强大的抽象。

学习这些基础概念使你能够有效地使用 Kubernetes。你现在明白了集群是如何构建的,应用程序如何根据所需状态进行部署和管理,以及包括存储、配置和秘密在内的支持资源是如何工作的。这些关键基础知识使你能够开始在 Kubernetes 上部署应用程序,并利用其自动化功能进行自愈、扩展和管理。本章获得的知识在我们继续前进时将是必不可少的。

在下一章,我们将通过一些动手练习使用 Kubernetes,来应用我们在这里学习的所有概念。

第三章:亲自实践 Kubernetes

在本章中,我们将通过部署本地和云端 Kubernetes 集群来实践 Kubernetes,并将示例应用程序部署到这些集群中。首先,你将使用 Kubernetes in DockerKind)部署一个本地集群。接着,你将部署在 AWS、GCP 和 Azure 上的托管 Kubernetes 集群。对于云选项,我们将提供部署集群所需的最基本账户设置。你可以自由选择你最熟悉的云服务提供商;核心的 Kubernetes 功能是相同的。

部署集群后,本章将分为两部分。第一部分,你将把在 第一章 中开发的简单 API 应用程序部署到 Kubernetes 集群中。你将学习如何将应用程序容器化,并与 Kubernetes 部署和服务一起工作,暴露你的应用程序。第二部分,你将把 第一章 中的简单数据处理批处理任务部署到 Kubernetes。这将演示如何使用 Kubernetes 运行一次性任务。

在本章结束时,你将拥有亲自将应用程序部署到 Kubernetes 的经验。你将理解如何打包和部署容器化应用程序,如何通过服务和入口暴露它们,并利用 Kubernetes 运行长期服务和批处理任务。有了这些技能,你将准备好将应用程序部署到生产 Kubernetes 环境中。

在本章中,我们将涵盖以下主要主题:

  • 安装 kubectl

  • 使用 Kind 部署本地 K8s 集群

  • 部署 AWS EKS 集群

  • 部署 Google Cloud GKE 集群

  • 部署 Azure AKS 集群

  • 在 Kubernetes 上运行你的 API

  • 在 Kubernetes 上运行数据处理任务

让我们开始吧。

技术要求

在本章中,你需要安装 Docker(安装说明请参考 第一章)。我们将进行的主要实践活动是基于云的,但你也可以选择使用 Kind 在本地完成这些活动。在本章中,你将学习如何使用 Kind 或在云端(AWS、Google Cloud 和 Azure)部署 Kubernetes 集群。最后,你需要安装 kubectl 来与 Kubernetes 集群进行交互。在下一节中,你将学习如何安装它。

安装 kubectl

kubectl 是我们将用来向 Kubernetes 集群发送命令的命令行工具。你必须安装它,才能与集群进行交互(无论集群是本地运行还是在云端):

  • 要在 macOS 上使用 Homebrew 安装 kubectl,请使用以下命令:

    brew install kubectl
    
  • 要在 Linux 发行版上安装 kubectl,你可以使用 curl 下载二进制可执行文件:

    curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
    
  • 要在 Windows 系统上安装它,你可以使用 chocolatey 包管理器:

    choco install kubernetes-cli
    

从此以后,每个 kubectl 命令都是相同的,无论你使用的是哪个操作系统。要检查安装是否成功,运行以下命令。这将为你提供 kubectl 版本的格式化视图,显示你系统上正在运行的版本:

kubectl version –client –output=yaml

现在,让我们继续安装 kind

使用 Kind 部署本地集群

部署本地 Kubernetes 集群对学习、测试和准备生产环境的部署非常有帮助,因为 Kubernetes 在任何运行环境下都是相同的。让我们从使用 Kind 部署一个单节点本地集群开始。

安装 kind

Kind 是一个允许你在本地机器上通过 Docker 容器运行 Kubernetes 的工具。除了轻便和易于设置外,Kind 还能提供符合 Kubernetes 标准的性能。Kind 集群通过了 Kubernetes 上游兼容性测试。

Kind 作为一个单一的二进制文件分发。你可以通过包管理器轻松安装它(确保你已经安装了 Docker):

  • 如果你使用的是 macOS,可以通过 Homebrew 安装 kind,方法如下:

    brew install kind
    
  • 如果你使用的是 Linux 发行版(例如 Ubuntu),你可以通过 curl 安装它:

    $ [ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
    $ chmod +x ./kind
    $ sudo mv ./kind /usr/local/bin/kind
    
  • 如果你使用的是 Windows 系统,可以通过 chocolatey 安装它(chocolatey.org/packages/kind):

    choco install kind
    

安装完成后,你可以通过运行以下命令来检查是否安装成功:

kind version

部署集群

现在,要部署一个单节点本地集群,只需运行以下命令:

kind create cluster

如果这是你第一次运行这个命令,Kind 会下载控制平面镜像,为它创建一个 Docker 容器,并为你配置一个单节点的 Kubernetes 集群。第一次运行时,由于要下载 Kubernetes 镜像,Kind 需要 1-2 分钟才能完成。之后的运行会快得多。

要验证集群是否已启动,运行以下命令:

kubectl cluster-info

这将打印本地集群的连接详情。就这样!你准备好了,可以开始了。

如果你愿意使用基于云的 Kubernetes 集群,在接下来的几节中,我们将部署一个 AWS、Google Cloud 和 Azure 上的集群。

部署 AWS EKS 集群

使用 kubectl 管理工作节点。

要开始使用 EKS,您需要一个 AWS 账户。请访问aws.amazon.com并点击创建 AWS 账户。按照步骤注册新账户;请注意,您需要提供信用卡信息。AWS 提供免费的使用层,提供有限的资源,在 12 个月内无需付费。通常这足以运行小型工作负载,但不适用于 Kubernetes(尽管如果管理得当,费用不会很高)。AWS 每月对每个运行的集群收费 73 美元(假设集群运行整个月;如果只运行几天或几小时,计费将相应按比例减少),并且按所选机器的大小为每个节点收取相应费用。

设置账户后,您必须使用root用户访问它。我们需要创建一个具有特定权限的 IAM 用户,因为这是推荐的使用方法。在 AWS 控制台中,进入IAM服务,并点击左侧面板中的用户。您应该看到如下屏幕:

图 3.1 – IAM 中的用户菜单

图 3.1 – IAM 中的用户菜单

然后,定义访问的用户名和密码。当您点击下一步时,选择直接附加策略(由于这是一个学习账户——此配置不适合生产环境)。从列表中选择AdministratorAccess(这将允许您在 AWS 中做任何事情),然后点击下一步进行审查。接着,点击创建用户按钮完成该过程。设置完成后,记得下载 AWS 密钥(AWS 访问密钥 ID 和 AWS 秘密访问密钥)。您需要这些密钥才能从您的计算机在 AWS 中进行身份验证。现在,注销控制台并使用新 IAM 用户重新登录,验证您的新 IAM 用户是否已正确配置。

在我们开始设置 Kubernetes 集群的工具之前,需要安装 AWS CLI 以便从本地终端与 AWS 进行交互。为此,请输入以下命令:

pip3 install awscli

安装完成后,我们运行aws configure来设置我们的 AWS 凭证:

aws configure

您将被提示输入您的 AWS 凭证(AWS 访问密钥 ID 和 AWS 秘密访问密钥)。根据提示复制并粘贴这些凭证。然后,配置会要求您选择默认的 AWS 区域。您可以选择us-east-1。对于输出格式,json是一个不错的选择。

这将把您的凭证保存为配置文件~/.aws/credentials。现在,您可以运行 AWS CLI 命令与 AWS 服务进行交互。

一旦你有了 AWS 账户并配置了 IAM 用户及 AWS CLI,安装 eksctl 命令行工具。此工具简化并自动化了部署 EKS 集群的过程。请访问 eksctl.io/installation/ 并按照安装说明操作。注意,文档页面列出了你部署集群所需的权限。AdministratorAccess 策略应包括你需要的所有权限。

安装完成后,检查 eksctl 是否正确设置,可以运行以下命令:

eksctl version

你应该能看到一些显示版本的输出。现在,让我们通过一个命令从零开始创建一个集群:

eksctl create cluster \
    --managed --alb-ingress-access \
    --node-private-networking --full-ecr-access \
    --name=studycluster \
    --instance-types=m6i.xlarge \
    --region=us-east-1 \
    --nodes-min=2 --nodes-max=4 \
    --nodegroup-name=ng-studycluster

此命令的基础是 eksctl create cluster。从第二行开始,我们正在陈述一些选项。让我们来看看它们:

  • --managed 选项告诉 eksctl 创建一个完全托管的集群。它将处理 EKS 控制平面、节点组、网络等的创建。

  • --alb-ingress-access 选项将配置集群,允许来自负载均衡器的入站流量。这是负载均衡器类型服务所必需的。

  • --node-private-networking 选项启用工作节点之间的私有网络连接。节点将仅拥有私有 IP 地址。

  • --full-ecr-access 选项为工作节点提供对 ECR 容器镜像库的完全访问权限。这是拉取容器镜像所需的。

  • --name 选项为集群设置一个用户定义的名称。

  • --instance-type 选项配置用于工作节点的实例类型。在这种情况下,我们选择使用 m6i.xlarge EC2 实例。

  • --region 选项设置 AWS 区域——在本例中为北弗吉尼亚(us-east-1)。

  • --nodes-min--nodes-max 选项为节点组设置自动扩缩规则。在这里,我们将最小节点数设置为 2 个实例,最大节点数设置为 4 个实例。

  • --nodegroup 选项将节点组的名称设置为 ng-studycluster

这将大约需要 10-15 分钟来完成。eksctl 处理创建 VPC、子网、安全组、IAM 策略以及运行 EKS 集群所需的其他 AWS 资源的所有细节。

在后台,eksctl 使用 CloudFormation 来配置 AWS 基础设施。eksctl 命令根据提供的参数生成一个 CloudFormation 模板,然后将此模板部署,以创建所需的资源。

以下是一些由 eksctl 创建的关键组件:

  • 虚拟私有云(VPC):一个 VPC 网络,您的集群资源将在其中运行。这包括跨多个可用区的公有和私有子网。

  • EKS 集群:Kubernetes 控制平面,包括 API 服务器、etcd、控制器管理器、调度器等。由 AWS 运营和管理。

  • 工作节点组:包含将运行 Kubernetes 工作负载的 EC2 实例的托管节点组。

  • 安全组:控制集群访问的防火墙规则。

  • IAM 角色和策略:访问策略,允许工作节点和 Kubernetes 访问 AWS API 和资源

一旦eksctl命令完成,您的 EKS 集群就可以使用了。您可以通过运行以下命令来查看集群的详细信息:

eksctl get cluster --name studycluster --region us-east-1

接下来,我们将使用 Google Cloud 创建一个 Kubernetes 集群。

部署一个 Google Cloud GKE 集群

要在 Google Cloud 上部署一个 Kubernetes 集群,我们需要设置一个 Google Cloud 账户并安装gcloud 命令行接口CLI)。为此,请访问 cloud.google.com/,点击开始免费以创建一个新账户。按照指示操作,当您的新账户创建完成后,进入 console.cloud.google.com/ 控制台。在这里,您可以管理所有的 Google Cloud 资源。

在我们部署 GKE 集群之前,我们需要启用必要的 API。在左上角点击导航菜单图标,进入Kubernetes Engine API并点击它。确保它已启用。启用gcloud CLI 也是一个好的实践,这将使我们能够通过命令行管理 Google Cloud 资源。

gcloud CLI 可以安装在 Linux、macOS 和 Windows 上。在 Linux 发行版中,您可以通过以下方式下载安装脚本:

wget https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz

然后,解压缩存档:

tar -xzf google-cloud-sdk.tar.gz

接下来,运行安装脚本:

./google-cloud-sdk/install.sh

对于 macOS 安装,您可以使用 Homebrew:

brew install --cask google-cloud-sdk

注意

对于 Windows,您可以从 cloud.google.com/sdk/docs/install#windows 下载gcloud安装程序。

安装完成后,您可以通过运行以下命令来检查:

gcloud --version

安装完成后,您必须初始化 CLI:

gcloud init

这个过程将指导您使用 Google 账户登录并配置gcloud

现在gcloud已经安装并初始化完毕,我们可以在 GKE 上部署 Kubernetes 集群。首先,选择一个 Google Cloud 项目来部署该集群。您可以创建一个新项目并将其设置为活动项目,如下所示:

gcloud projects create [PROJECT_ID]
gcloud config set project [PROJECT_ID]

然后,您必须设置一个计算区域来部署集群。在这里,我们将在us-central1-a区域进行操作:

gcloud config set compute/zone us-central1-a

现在,我们可以通过运行以下命令来部署一个 Kubernetes 集群:

gcloud container clusters create studycluster --num-nodes=2

–num-nodes参数控制将要部署的节点数量。请注意,这个过程需要几分钟时间,因为 Google Cloud 会设置所有集群组件、网络等。部署完成后,收集凭证以便与集群进行交互:

gcloud container clusters get-credentials studycluster

这将把凭证保存到您的 Kubernetes 配置文件中。最后,您可以通过运行以下命令来验证是否可以连接到集群:

kubectl cluster-info

您应该会看到有关 Kubernetes 主节点和云提供商的信息。就这样!您现在已经在 Google Cloud 上运行了一个完全功能的 Kubernetes 集群。

接下来,我们将使用微软的 Azure 云执行相同的操作。

部署一个 Azure AKS 集群

在本节中,我们将演示如何使用 Azure Kubernetes 服务AKS)部署 Kubernetes 集群的步骤。要开始使用 AKS,你需要一个 Azure 账户。首先,访问 azure.com,点击 免费试用 Azure,然后点击 开始免费试用。这样你就可以在 Azure 上开始免费的试用账户了。你需要提供一些基本信息,如你的电子邮件和电话号码,以完成账户设置。确保使用有效的电子邮件地址,因为 Azure 会发送验证码来完成注册过程。账户创建完成后,你将进入 Azure 门户。这是用于管理你所有 Azure 资源的主面板。

在这一点上,建议在本地机器上安装 Azure CLI。Azure CLI 允许你通过命令行管理 Azure 资源。请按照 docs.microsoft.com/en-us/cli/azure/install-azure-cli 上的说明,在 Linux、macOS 或 Windows 上安装它。

安装 CLI 后,运行 az login 并按照提示进行身份验证。这样你就可以从终端运行 Azure 命令了。设置好 Azure 账户并安装好 CLI 后,你就可以准备部署 AKS 集群了。

首先,你必须创建一个资源组。Azure 中的资源组允许你将 AKS 集群、存储账户、虚拟网络等资源逻辑地分组。让我们从为 AKS 集群创建一个资源组开始:

az group create --name myResourceGroup --location eastus

这将会在 eastus 区域创建一个名为 myResourceGroup 的资源组。你可以指定任何靠近你的区域。现在,我们可以在这个资源组中创建 AKS 集群。创建集群的基本命令如下:

az aks create --resource-group myResourceGroup --name studycluster--node-count 2 --generate-ssh-keys

这将会创建一个名为 studycluster 的 Kubernetes 集群,包含两个节点。我们会自动生成 SSH 密钥以设置访问节点的权限。你还可以指定一些其他选项,如下所示:

  • --node-vm-size:节点虚拟机的大小。默认为 Standard_D2s_v3

  • --kubernetes-version:要用于集群的 Kubernetes 版本。默认为最新版本。

  • --enable-addons:启用附加功能,如监控、虚拟节点等。

az aks create 命令会自动处理 Kubernetes 集群、虚拟机、网络、存储等的设置。该过程可能需要 5-10 分钟才能完成。

完成后,你可以通过运行以下命令连接到集群:

kubectl get nodes

这将显示属于你的 AKS 集群的节点。到此为止,你已经成功部署了 AKS 集群,并准备在其上部署我们的 API 和批处理任务。

在 Kubernetes 上运行你的 API

从这一点开始,你可以选择你最喜欢的 Kubernetes 部署类型(本地或基于云的)来运行我们的 API。以下示例将使用 AWS,但你也可以选择其他云提供商。随意选择。

现在,到了重拾我们在 第一章 中构建的 API 并将其推向生产环境的时候了。我们开发了一个简单的 API,当你请求时,它可以向你打招呼或回答一个(非常酷的)笑话。

我们已经有了 API 的代码(github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter01/app/main.py)以及构建容器镜像的 Dockerfile(github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter01/Dockerfile)。

为了让 Kubernetes 能够访问镜像,它应该在容器注册表中可用。每个云服务提供商都有一个注册表,但为了简化起见,我们使用 DockerHub(hub.docker.com/)。只要你的镜像是公开的,你可以免费存储任意数量的镜像。让我们开始吧:

  1. 要开始,请在终端中输入以下命令:

    docker login
    docker build –t <USERNAME>/jokeapi:v1 .
    

    记得将 <USERNAME> 替换为你的真实 DockerHub 用户名。我们将镜像名称更改为 jokeapi,以便更容易找到。如果你在 Mac M1 上运行 Docker,那么设置 --platform 参数非常重要,以使容器镜像与 AMD64 机器兼容。为此,运行以下命令:

    docker build --platform linux/amd64 –t <USERNAME>/jokeapi:v1 .
    
  2. 现在,我们可以将镜像推送到 DockerHub:

    docker push <USERNAME>/jokeapi:v1
    
  3. 访问 hub.docker.com/ 并登录。你应该会在 Repositories 部分看到你的镜像。

现在,镜像已可用于 Kubernetes。接下来,我们需要定义一些 Kubernetes 资源来运行我们的 API。我们将创建一个部署和一个服务。

创建部署

按照以下步骤操作:

  1. 首先,我们将创建部署。这指定了要运行的 Pod 副本数量以及它们的配置:

    deployment_api.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
        name: jokeapi
    spec:
        replicas: 2
        selector:
        matchLabels:
          app: jokeapi
        template:
        metadata:
          labels:
            app: jokeapi
        spec:
          containers:
          - name: jokeapi
            image: <USERNAME>/jokeapi:v1
            imagePullPolicy: Always
            ports:
            - containerPort: 8087
    

    这将使用我们构建的 Docker 镜像运行两个 Pod 副本。请注意,我们正在容器中打开端口 8087。这与 Dockerfile 中的 EXPOSE 命令类似。

  2. 接下来,我们将创建一个命名空间来分隔和组织我们的资源,并应用部署:

    kubectl create namespace jokeapi
    kubectl apply -f deployment_api.yaml -n jokeapi
    
  3. 这将创建部署并在 jokeapi 命名空间中创建两个 Pod 副本。我们可以通过运行以下命令检查一切是否正常:

    kubectl get deployments -n jokeapi
    

    你应该会看到以下输出:

    NAME      READY   UP-TO-DATE    AVAILABLE      AGE
    jokeapi    0/2        2              0          2s
    
  4. 现在,让我们检查 Pods 是否正常运行:

    kubectl get pods -n jokeapi
    

    你应该会看到类似如下的输出:

    NAME                       READY   STATUS    RESTARTS
    jokeapi-7d9877598d-bsj5b    1/1     Running    0
    jokeapi-7d9877598d-qb8vs    1/1     Running    0
    

创建一个服务

按照以下步骤操作:

  1. 在这里,我们将指定一个服务,以便在集群中暴露 Pods:

    service_api.yaml

    apiVersion: v1
    kind: Service
    metadata:
        name: jokeapi
    spec:
        selector:
        app: jokeapi
        ports:
        - protocol: TCP
          port: 80
          targetPort: 8087
    

    这将创建一个 ClusterIP 服务,在集群的内部 IP 地址上暴露 API Pods。请注意,spec 部分没有指定 type 参数,因此默认为 ClusterIP。

  2. 为了使 API 可供外部访问,我们可以创建一个负载均衡器(这在本地 kind 集群中不可行,只能在云基础集群中实现):

    lb_api.yaml

    apiVersion: v1
    kind: Service
    metadata:
        name: jokeapi
    spec:
        selector:
        app: jokeapi
        type: LoadBalancer
        ports:
        - protocol: TCP
          port: 80
          targetPort: 8087
    
  3. 现在,type 定义已设置,这段代码将定义一个负载均衡器并分配一个外部 IP。接下来,我们将部署负载均衡器服务:

    kubectl apply -f lb_api.yaml -n jokeapi
    
  4. 现在,我们可以测试 API 是否可访问。首先,我们必须通过运行以下命令来获取负载均衡器的 URL(在这里,我们使用的是 AWS):

    kubectl get services -n jokeapi
    

    你应该看到以下输出:

    NAME      TYPE          CLUSTER-IP      EXTERNAL-IP
    jokeapi  LoadBalancer  10.100.251.249  <DNS>.amazonaws.com
    
  5. 复制 EXTERNAL-IP 下的内容,将 URL 粘贴到浏览器中,并添加 /joke。例如,在我的实现中,我得到了 ab1cdd20ce1a349bab9af992211be654-1566834308.us-east-1.elb.amazonaws.com/joke。你应该能看到屏幕上的以下响应:

    {"joke":"Have you heard about the chocolate record player? It sounds pretty sweet."}
    

成功!我们在浏览器中看到(一个很棒的)笑话!现在,我们将使用 ingress 来部署 API,而不是负载均衡器(仅限云端集群)。

使用 ingress 访问 API

对于这次部署,我们将使用 NGINX ingress 控制器,并将其连接到 AWS 提供的负载均衡器(如果你使用其他云提供商,过程非常相似)。请按以下步骤操作:

  1. 首先,我们将为 NGINX 创建一个新的命名空间,并在 Kubernetes 上部署控制器:

    kubectl create namespace ingress-nginx
    kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.3/deploy/static/provider/baremetal/deploy.yaml -n ingress-nginx
    
  2. 这将使用官方清单部署 NGINX 控制器。现在,我们必须在部署中编辑一行,确保它使用负载均衡器作为 ingress 部署,而不是默认的 NodePort。在终端中输入以下内容:

    kubectl edit service ingress-nginx-controller -n ingress-nginx
    
  3. 查找 spec.type 字段并将其值更改为 LoadBalancer。保存文件后,让我们检查已部署的服务:

    kubectl get services -n ingress-nginx
    
  4. 我们将看到 ingress-nginx-controller 服务被设置为 LoadBalancer 并且有一个与之相关的外部 IP 地址。现在,设置指向这个 ingress 控制器的 ingress 变得简单了。首先,我们将在 service_api.yaml 文件中创建一个定义好的服务。这个服务应该设置为 ClusterIP 类型(请参见上一节中的代码)。然后,我们可以用以下代码定义一个 ingress:

    ingress.yaml

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
        name: jokeapi-ingress
    spec:
        rules:
        - http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: jokeapi
                port:
                  number: 80
    
  5. 这个 ingress 会将外部流量路由到内部服务 IP。当 ingress 获得外部 IP 后,我们应该能够通过访问该 URL 来访问我们的 API。输入以下内容:

    kubectl get services –n ingress-nginx
    
  6. 获取控制器的外部 URL 并在其后添加 /joke

    {"joke":"What do you call a fish wearing a bowtie? Sofishticated."}
    

Voilà!在下一节中,我们将在 Kubernetes 上部署我们的数据处理任务。

在 Kubernetes 中运行数据处理任务

在这一节中,我们将在 Kubernetes 上部署来自 第一章 的简单数据处理任务。我们已经开发了这个任务(github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter01/run.py)并且编写了一个 Dockerfile,将其打包为容器镜像(github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter01/Dockerfile_job)。

现在,我们必须构建一个 Docker 镜像并将其推送到 Kubernetes 可访问的仓库。

docker build --platform linux/amd64 –f Dockerfile_job –t <USERNAME>/dataprocessingjob:v1 .
docker push <USERNAME>/dataprocessingjob:v1

现在,我们可以创建一个 Kubernetes 作业来运行我们的数据处理任务。以下是一个示例作业清单:

job.yaml

apiVersion: batch/v1
kind: Job
metadata:
    name: dataprocessingjob
spec:
    template:
    spec:
      containers:
      - name: dataprocessingjob
        image: <USERNAME>/dataprocessingjob:v1
      restartPolicy: Never
    backoffLimit: 4

这配置了一个名为dataprocessingjob的作业,它将运行一个副本的<USERNAME>/dataprocessingjob:v1镜像。现在,我们可以创建一个新的命名空间并像这样部署作业:

kubectl create namespace datajob
kubectl apply -f job.yaml -n datajob

这定义了一个名为dataprocessingjob的作业,它将使用我们的 Docker 镜像运行一个单独的 Pod。我们设置restartPolicy: Never,因为我们希望容器运行完成,而不是重新启动。

我们可以像这样检查作业的状态:

kubectl get jobs –n datajob

作业完成后,我们将看到1/1

NAME                COMPLETIONS    DURATION      AGE
dataprocessingjob      1/1           8s          11s

要查看我们作业的日志,可以在由作业创建的 Pod 上使用kubectl logs

kubectl get pods -n datajob
kubectl logs <NAMEOFTHEPOD> -n datajob

在我的案例中,我输入了以下内容:

kubectl logs dataprocessingjob-g8lkm -n datajob

我得到以下结果:

Index([0, 1, 2, 3, 4, 5, 6, 7, 8, 'newcolumn'], dtype='object')
   0    1   2   3    4     5      6   7  8    newcolumn
0  6  148  72  35    0  33.6  0.627  50  1       67.2
1  1   85  66  29    0  26.6  0.351  31  0       53.2
2  8  183  64   0    0  23.3  0.672  32  1       46.6
3  1   89  66  23   94  28.1  0.167  21  0       56.2
4  0  137  40  35  168  43.1  2.288  33  1       86.2
(768, 10)

这将打印我们 Python 程序的应用程序输出,以便我们验证它是否正确运行。就这样!你在 Kubernetes 内部运行了一个数据处理作业!

总结

在这一章中,我们通过实践经验部署了 Kubernetes 集群并在其中运行了应用程序。我们从安装kubectl并使用 Kind 部署了一个本地 Kubernetes 集群开始。然后,我们在 AWS、GCP 和 Azure 上部署了托管的 Kubernetes 集群。虽然云提供商有所不同,但 Kubernetes 提供了一个一致的环境来运行容器。

在设置好集群后,我们将第一章中的简单 API 应用程序容器化并部署。这展示了如何定义 Kubernetes 部署、服务、入口和负载均衡器来运行 Web 应用程序。然后,我们将第一章中的数据处理批量作业作为 Kubernetes 作业进行了部署。这向我们展示了如何利用 Kubernetes 运行一次性任务和作业。

通过完成部署集群和应用程序的全过程,你现在已经有了 Kubernetes 的第一手经验。你了解如何将应用程序打包为容器,通过服务、入口或负载均衡器暴露它们,并利用 Kubernetes 的抽象,如部署和作业。有了这些技能,你就具备了在开发或生产环境中运行 Kubernetes 上的应用程序和工作负载的能力。

在下一章中,我们将更深入地了解现代数据栈,理解每项技术,为什么它们很重要,以及它们如何相互关联以构建数据解决方案。

第二部分:大数据栈

在这一部分,你将深入了解构成现代数据栈的核心技术,这是一组用于构建强大且可扩展的数据管道的工具和架构。你将深入理解 Lambda 架构及其组件,并获得一些使用强大的大数据工具(如 Apache Spark、Apache Airflow 和 Apache Kafka)的实践经验。

这一部分包含以下章节:

  • 第四章现代数据栈

  • 第五章使用 Apache Spark 进行大数据处理

  • 第六章, 用于构建流水线的 Apache Airflow

  • 第七章, 用于实时事件和数据摄取的 Apache Kafka

第四章:现代数据架构

在本章中,我们将探讨为构建可扩展和灵活的数据平台而出现的现代数据架构。具体来说,我们将介绍 Lambda 架构模式,并讨论它如何结合批量数据分析实现实时数据处理。你将了解 Lambda 架构的关键组件,包括用于历史数据的批处理层、用于实时数据的速度处理层,以及用于统一查询的服务层。我们还将讨论如何利用 Apache Spark、Apache Kafka 和 Apache Airflow 等技术,在大规模环境中实现这些层。

本章结束时,你将理解构建现代数据湖的核心设计原则和技术选择。你将能够解释 Lambda 架构相比传统数据仓库设计的优势。最重要的是,你将拥有开始构建自己现代数据平台的概念基础。

所涵盖的概念将帮助你以低延迟处理流数据,同时在历史数据上执行复杂的分析工作负载。你将获得利用开源大数据技术构建可扩展、灵活的数据管道的实践经验。无论你需要实时分析、机器学习模型训练,还是临时分析,现代数据架构模式都能帮助你支持多样化的数据需求。

本章提供了从传统数据仓库过渡到下一代数据湖的蓝图。通过这些课程,你将掌握构建现代数据平台所需的关键架构原则、组件和技术,以便在 Kubernetes 上实现。

在本章中,我们将涵盖以下主要内容:

  • 数据架构

  • 大数据数据湖设计

  • 实现湖屋架构

数据架构

现代数据架构在过去十年中经历了显著的发展,使得组织能够利用大数据的力量并推动先进的数据分析。两种关键的架构模式是 Lambda 架构和 Kappa 架构。在本节中,我们将探讨这两种架构,并了解它们如何为构建我们的大数据环境提供有用的框架。

Lambda 架构

Lambda 架构是一种大数据处理架构模式,平衡了批处理和实时处理方法。其名称源自 Lambda 计算模型。Lambda 架构在 2010 年代初期变得流行,成为一种以经济高效和灵活的方式处理大规模数据的方法。

Lambda 架构的核心组件包括以下内容:

  • 批处理层:负责管理主数据集。该层在固定间隔内批量摄取和处理数据,通常为每 24 小时一次。数据处理完成后,批处理视图被视为不可变的,并被存储。

  • 速度层:负责处理尚未由批处理层处理的最新数据。此层在数据到达时实时处理数据,以提供低延迟视图。

  • 服务层:负责通过合并批处理层和速度层的视图来响应查询。

这些组件在图 4.1的架构图中有所展示:

图 4.1 – Lambda 架构设计

图 4.1 – Lambda 架构设计

Lambda 架构的主要优点是它提供了一种混合方法,将大量历史数据视图(批处理层)与最新数据的视图(速度层)结合在一起。这使得分析师可以以统一的方式查询最新和历史数据,从而获得快速的洞察。

批处理层针对吞吐量和效率进行了优化,而速度层则针对低延迟进行了优化。通过分离各自的职责,该架构避免了每次查询都需要运行大规模、长期运行的批处理任务。相反,查询可以利用预计算的批处理视图,并通过速度层的最新数据进行增强。

在构建于云基础设施上的现代数据湖中,Lambda 架构提供了一个灵活的蓝图。云存储层作为基础数据湖,用于存储数据。批处理层利用分布式数据处理引擎,如 Apache Spark,生成批处理视图。速度层流式处理并处理最新数据,服务层运行高效的查询引擎,如 Trino,用于分析数据。

Kappa 架构

Kappa 架构作为一种替代方法,最近由 Lambda 架构的主要创始人提出。Kappa 架构的主要区别在于,它旨在通过消除单独的批处理层和速度层来简化 Lambda 模型。

相反,Kappa 架构通过单一的流处理路径来处理所有数据。其关键组件包括以下内容:

  • 流处理层:负责将所有数据作为流进行摄取和处理。此层处理历史数据(通过重放日志/文件)以及新来的数据。

  • 服务层:负责通过访问流处理层生成的视图来响应查询。

我们可以在图 4.2中看到一个可视化表示:

图 4.2 – Kappa 架构设计

图 4.2 – Kappa 架构设计

Kappa 架构的核心是一个不可变的、仅附加的日志,供所有数据使用工具(如 Kafka 和事件溯源范式)使用。流数据直接被摄取到日志中,而不是通过独立的管道。该日志确保数据有序、无法篡改,并支持自动重放——这些是流处理和批处理的关键支持功能。

Kappa 架构的优势在于其设计的简单性。通过单一的处理路径,不需要管理独立的批处理和实时系统。所有数据都通过流处理进行处理,这也使得历史数据的重新处理和分析变得更加灵活。

其权衡之处在于,流处理引擎可能无法提供与最先进的批处理引擎相同的规模和吞吐量(尽管现代流处理器已不断发展,以处理非常大的工作负载)。另外,尽管 Kappa 设计可能更简单,但其架构本身可能比 Lambda 更难实现和维护。

对于数据湖,Kappa 架构与大量原始数据的特性非常契合。云存储层充当原始数据的骨干。然后,像 Apache Kafka 和 Apache Flink 这样的流处理器接收、处理并生成分析准备好的数据视图。服务层利用 Elasticsearch 和 MongoDB 等技术来推动分析和仪表板的展示。

比较 Lambda 和 Kappa

Lambda 和 Kappa 架构采取不同的方法,但在准备、处理和分析大数据集方面解决了相似的问题。其主要区别列于表 4.1

Lambda Kappa
复杂度 管理独立的批处理和实时系统 通过流合并处理
重新处理 重新处理历史批次 依赖于流重放和算法
延迟 在速度层中对最近数据有较低的延迟 所有数据的延迟相同
吞吐量 利用优化吞吐量的批处理引擎 将所有数据作为流处理

表 4.1 – Lambda 和 Kappa 架构的主要区别

在实践中,现代数据架构通常将这些方法结合使用。例如,Lambda 的批处理层可能每周或每月运行一次,而实时流则填补空白。Kappa 可能会在流中利用小批次来优化吞吐量。平衡延迟、吞吐量和重新处理的核心思想是相同的。

对于数据湖,Lambda 提供了一个经过验证的蓝图,而 Kappa 提供了一个强大的替代方案。虽然有人可能认为 Kappa 提供了更简化的操作,但它难以实现,并且随着规模的扩大,其成本可能快速增长。Lambda 的另一个优势是它完全可以适应需求。如果没有必要进行数据流处理(或者没有经济可行性),我们可以仅实现批处理层。

数据湖构建者应该理解每种架构的关键原则,以便根据他们的业务、分析和运营需求设计最佳架构。通过利用云基础设施的规模和灵活性,现代数据湖可以实施这些模式,以处理当今的数据量并推动高级分析。

在下一节,我们将深入探讨 Lambda 架构方法以及它如何应用于创建高性能、可扩展的数据湖。

大数据的数据湖设计

在本节中,我们将对比数据湖与传统数据仓库,并涵盖核心设计模式。这将为后面“如何做”的工具和实施部分奠定基础。让我们从现代数据架构的基线——数据仓库开始。

数据仓库

数据仓库几十年来一直是商业智能和分析的支柱。数据仓库是一个集成多个来源的数据存储库,经过组织和优化,旨在用于报告和分析。

传统数据仓库架构的关键方面如下:

  • 结构化数据:数据仓库通常只存储结构化数据,例如来自数据库和 CRM 系统的交易数据。不包括来自文档、图片、社交媒体等的非结构化数据。

  • 写时模式:数据结构和模式在数据仓库设计时就已定义。这意味着添加新的数据源和改变业务需求可能会很困难。

  • 批处理:数据提取、转换和加载ETL)是按计划批量从源系统提取的,通常是每日或每周。这会引入访问最新数据时的延迟。

  • 与源系统分离:数据仓库作为一个独立的数据存储,用于优化分析,与源事务系统分开。

在大数据时代,数据量、种类和速度的增长暴露了传统数据仓库架构的一些局限性。

它无法以具有成本效益的方式存储和处理来自新来源(如网站、移动应用、物联网设备和社交媒体)的庞大非结构化和半结构化数据。此外,它缺乏灵活性——添加新的数据源需要更改模式和 ETL,这使得适应变得缓慢且昂贵。最后,批处理无法快速提供足够的洞察力,以满足实时个性化和欺诈检测等新兴需求。

这引发了数据湖架构的产生,作为应对,我们将在接下来的部分详细讨论。

大数据和数据湖的崛起

为应对上述挑战,新的数据湖方法使得能够大规模处理任何类型的数据存储,使用诸如 Hadoop HDFS 或云对象存储等负担得起的分布式存储。数据湖以读取时模式运行,而不是预先定义的模式。数据以原生格式存储,只有在读取时才会解释模式。它包括通过诸如 Apache Kafka 等工具捕获、存储和访问实时流数据。此外,还有一个庞大的开源生态系统,支持可扩展的处理,包括 MapReduce、Spark 和其他工具。

数据湖是一个集中式的数据存储库。它旨在以原始格式存储数据,这样可以灵活地按需分析不同类型的数据(表格、图像、文本、视频等),而无需预先确定数据的使用方式。由于它是基于对象存储实现的,因此可以存储来自不同来源、在不同时间间隔下的数据(有些数据可能每天更新,有些每小时更新,甚至有些是接近实时的数据)。数据湖还将存储和处理技术分开(与数据仓库不同,数据仓库将存储和处理整合在一个独特的结构中)。通常,数据处理涉及分布式计算引擎(如 Spark),用于处理 TB 级别的数据。

数据湖提供了一种以成本效益的方式存储组织面临的庞大而多样化数据量并进行分析的方式。然而,它们也面临一些挑战:

  • 如果没有治理,数据湖有可能变成无法访问的数据沼泽。数据需要进行分类并提供上下文,以便发现。

  • 准备原始数据进行分析仍然涉及在分散的孤立工具中进行复杂的数据处理。

  • 大多数分析仍然需要先对数据进行建模、清洗和转换——就像数据仓库一样。这种做法导致了重复劳动。

  • 数据湖中使用的基于对象存储的系统无法执行行级修改。每当表格中的一行需要修改时,整个文件都必须重写,这会大大影响处理性能。

  • 在数据湖中,没有高效的模式控制。虽然按需读取模式使得新的数据源更容易加入,但无法保证因为数据摄取失败,表格的结构不会发生变化。

近年来,为了克服这些新挑战,业界进行了大量努力,将两者的优势结合在一起,这就是现在所称的数据湖仓(data lakehouse)。让我们深入了解这一概念。

数据湖仓的兴起

在 2010 年代,随着 Delta Lake、Apache Hudi 和 Apache Iceberg 等新兴开源技术的出现,“湖仓”(lakehouse)这一术语开始受到关注。湖仓架构旨在结合数据仓库和数据湖的最佳特性:

  • 支持像数据湖一样在任何规模上处理多种结构化和非结构化数据

  • 提供像数据仓库一样对原始数据和精炼数据进行高效的 SQL 分析

  • 对大规模数据集的ACID原子性、一致性、隔离性和持久性)事务

数据湖仓允许以开放格式同时存储、更新和查询数据,同时确保数据在大规模环境下的正确性和可靠性。它支持以下功能:

  • 模式强制执行、演化和管理

  • 行级更新插入(更新+插入)和删除以实现高效的可变性

  • 按时间点一致性视图跨历史数据

使用湖屋架构,整个分析生命周期——从原始数据到清洗和建模后的数据,再到策划的数据产品——都可以在一个地方直接访问,适用于批处理和实时用例。这提升了敏捷性,减少了重复劳动,并通过生命周期使数据的重用和再利用更加容易。

接下来,我们将探讨数据在这一架构概念中的结构化方式。

湖屋存储层

与数据湖架构类似,湖屋(Lakehouse)也建立在云对象存储之上,通常分为三个主要层次:铜层、银层和金层。这种方法被称为“奖章”设计。

铜层是原始数据摄取层。它包含来自各种来源的原始数据,存储方式与接收到时完全相同。数据格式可以是结构化的、半结构化的或非结构化的。例如,日志文件、CSV 文件、JSON 文档、图片、音频文件等。

该层的目的是以最完整和最原始的格式存储数据,作为分析用途的真相版本。在这一层不会进行任何转换或聚合。它作为构建更高层次的策划和聚合数据集的源数据。

银层包含精心策划、精炼和标准化的数据集,这些数据集经过丰富、清洗、集成,并符合业务标准。数据具有一致的模式,能够进行查询以支持分析。

该层的目的是准备高质量的、可分析的数据集,这些数据集可以为下游分析和机器学习模型提供支持。这涉及到数据整理、标准化、去重、联合不同数据源等。

该结构可以是表格、视图或优化查询的文件。例如,Parquet 文件、Delta Lake 表、物化视图等。元数据被添加以启用数据发现。

金层包含聚合后的数据模型、指标、关键绩效指标(KPI)和其他衍生数据集,支持商业智能和分析仪表板。

该层的目的是为业务用户提供现成的策划数据模型,用于报告和可视化。这涉及到预计算指标、聚合、业务逻辑等,以优化分析工作负载。

该结构通过列存储、索引、分区等优化分析。示例包括聚合、数据立方体、仪表板和机器学习模型。元数据将其与上游数据连接。

有时,在铜层之前,通常会有一个额外的层——着陆区。在这种情况下,着陆区接收原始数据,所有清洗和结构化工作都在铜层完成。

在接下来的章节中,我们将看到如何使用现代数据工程工具将数据湖屋设计付诸实践。

实施湖屋架构

图 4.3 显示了在 Lambda 设计中实现数据湖仓架构的可能方式。该图显示了常见的数据湖仓层以及在 Kubernetes 上实现这些层所使用的技术。左侧的第一组代表了与此架构一起工作的可能数据源。此方法的一个关键优势是它能够摄取和存储来自多种来源和格式的数据。如图所示,数据湖可以连接并整合来自数据库的结构化数据,以及来自 API 响应、图像、视频、XML 和文本文件等的非结构化数据。这种按需模式(schema-on-read)允许原始数据快速加载,而无需提前建模,从而使架构具有高度的可扩展性。当需要分析时,数据湖仓层可以使用按查询模式(schema-on-query)在一个地方查询所有这些数据集。这使得从不同来源整合数据以获得新见解变得更加简便。加载与分析的分离还使得随着对数据的新理解的出现,能够进行迭代分析。总体而言,现代数据湖仓旨在快速接纳多结构和多源数据,同时使用户能够灵活地分析这些数据。

首先,我们将仔细查看图 4.3顶部显示的批量层。

批量摄取

设计的第一层是批量摄取过程。对于所有的非结构化数据,定制的 Python 处理过程是首选。可以开发自定义代码来从 API 端点查询数据,读取 XML 结构,处理文本和图像。对于数据库中的结构化数据,我们有两种数据摄取选择。首先,Kafka 和 Kafka Connect 提供了一种简单配置数据迁移任务并连接到大量数据库的方法。Apache Kafka 是一个分布式流处理平台,允许发布和订阅记录流。在其核心,Kafka 是一个基于发布-订阅模型构建的持久化消息代理。Kafka Connect 是 Kafka 附带的工具,提供了一种通用的方式将数据进出 Kafka。它提供了可重用的连接器,帮助将 Kafka 主题连接到外部系统,如数据库、键值存储、搜索索引和文件系统等。Kafka Connect 为许多常见的数据源和接收器提供了连接器插件,如 JDBC、MongoDB、Elasticsearch 等。这些连接器将外部系统中的数据移动到 Kafka 主题中,反之亦然。

图 4.3 – Kubernetes 中的数据湖仓

图 4.3 – Kubernetes 中的数据湖仓

这些连接器是可重用且可配置的。例如,JDBC 连接器可以配置为捕获来自 PostgreSQL 数据库的变更并将其写入 Kafka 主题。Kafka Connect 负责处理数据格式转换、分布式协调、容错等,并支持通过跟踪源连接器(例如数据库 变更数据捕获CDC)连接器)中的数据变更,将变更流管道化到 Kafka 主题中,从而简化了数据的进出 Kafka 的过程。虽然 Kafka 是一个广为人知的流数据工具,但与 Kafka Connect 一起使用已证明在数据库的批量数据迁移方面非常高效。

有时,当管理 Kafka 集群进行数据迁移不可行时(我们稍后会讨论其中的一些情况),可以通过 Apache Spark 从结构化数据源摄取数据。Apache Spark 提供了一种多功能的工具,可以从各种结构化数据源摄取数据到基于云对象存储的数据湖中,例如 Amazon S3 或 Azure Data Lake Storage。Spark 的 DataFrame API 允许从关系数据库、NoSQL 数据存储以及其他结构化数据源查询数据。尽管很方便,但从 JDBC 数据源读取数据在 Spark 中可能效率低下。Spark 会将表作为单个分区读取,因此所有处理将在单个任务中进行。对于大型表格,这可能会减慢数据摄取和后续查询的速度(更多细节请参见 第五章)。为了优化,我们需要手动对源数据库的读取进行分区。使用 Spark 进行数据摄取的主要缺点是需要自己处理这些分区和优化问题。其他工具可以通过为你管理并行摄取任务来提供帮助,但 Spark 提供了连接和处理许多数据源的灵活性,开箱即用。

现在,让我们来看一下存储层。

存储

接下来,在图表的中间部分,我们有 存储 层。这是我不建议迁移到 Kubernetes 的唯一一层。基于云的对象存储服务现在有许多功能,能够优化可扩展性和可靠性,使其操作简单且具有很好的检索性能。尽管有一些很棒的工具可以在 Kubernetes 中构建数据湖存储层(例如 min.io/),但这样做不值得,因为你必须自己处理可扩展性和可靠性。对于本书的目的,我们将在 Kubernetes 中处理所有数据湖仓层,除了存储层。

批处理

现在,我们将讨论批处理处理层。Apache Spark 已成为大数据生态系统中大规模批处理数据处理的事实标准。与传统的 MapReduce 作业不同,传统作业将中间数据写入磁盘,而 Spark 在内存中处理数据,这使得其在迭代算法和交互式数据分析上更快。Spark 使用集群管理器来协调作业在多个工作节点上的执行。这使得它能够通过将数据分布到集群中并并行处理,来高效处理非常大的数据集。Spark 能够高效地处理存储在分布式文件系统(如 HDFS)和云对象存储中的 TB 级数据。

Spark 的一个关键优势是它为 SQL 和复杂分析提供的统一 API。数据工程师和科学家可以使用 Python DataFrame API 来处理和分析批量数据集。然后,可以通过 Spark SQL 查询相同的 DataFrame,提供熟悉性和交互性。这使得 Spark 对于各种用户来说非常易于操作。通过利用内存处理并提供易于使用的 API,Apache Spark 成为可扩展批量数据分析的首选解决方案。拥有大量日志文件、传感器数据或其他记录的公司可以依赖 Spark 高效地并行处理这些庞大的数据集。这也巩固了它在现代数据架构中的基础技术地位。

接下来,我们将讨论编排层。

编排

在存储层和批处理层之上,在图 4.3中,我们找到了一个编排层。当我们构建更复杂的数据管道,将多个处理步骤连接在一起时,我们需要一种可靠的方式来管理这些管道的执行。这就是编排框架的作用。在这里,我们选择使用 Airflow。Airflow 是一个开源的工作流编排平台,最初由 Airbnb 开发,用于编写、调度和监控数据管道。此后,它已成为数据管道中最流行的编排工具之一。

使用 Airflow 对于批处理数据管道的重要原因如下:

  • 调度:Airflow 允许你定期调度批处理作业(每小时、每天、每周等)。这样就不需要手动启动作业,确保它们可靠地运行。

  • 依赖管理:作业通常需要按顺序运行,或者等待其他作业完成。Airflow 提供了一种简单的方法来设置这些依赖关系,在有向无环图DAG)中进行管理。

  • 监控:Airflow 具有内置的仪表板,用于监控作业的状态。你可以看到哪些作业已成功、失败、正在运行等状态。它还会保留日志和历史记录,以供后续调试。

  • 灵活性:可以通过修改 DAG 来添加新的数据源、转换和输出,而不会影响其他不相关的作业。Airflow 的 DAG 提供了高度的可配置性。

  • 抽象:Airflow DAG 允许管道开发人员专注于业务逻辑,而非应用程序编排。底层的 Airflow 平台处理工作流调度、状态监控等事务。

现在,我们将进入服务层的介绍。

批处理服务

对于 Kubernetes 中的 批处理服务 层,我们选择了 Trino。Trino(前身为 PrestoSQL)是一个开源的分布式 SQL 查询引擎,旨在对多种数据源执行交互式分析查询。Trino 可以处理高达 PB 级别的数据查询。使用 Trino,你可以并行查询多个数据源。当 SQL 查询提交给 Trino 时,它会被解析和规划,生成分布式执行计划。该执行计划随后会提交给工作节点,工作节点并行处理查询并将结果返回给协调节点。它支持 ANSI SQL(最常见的 SQL 标准之一),并且可以连接多种数据源,包括所有主要的云端对象存储服务。通过利用 Trino,数据团队可以直接在云数据湖中实现自助 SQL 分析,避免了仅为分析而进行的数据移动,且仍能提供交互式的响应时间。

接下来,我们将看一下为数据可视化选择的工具。

数据可视化

对于数据可视化和分析,我们选择了使用 Apache Superset。尽管市场上有许多优秀的工具,我们发现 Superset 易于部署、易于运行、易于使用,并且极易集成。Superset 是一个开源的数据探索和可视化应用,能够让用户轻松构建交互式仪表板、图表和图形。Superset 最早于 2015 年在 Airbnb 内部作为分析师和数据科学家的工具开发。随着 Airbnb 对其使用和贡献的增加,它决定在 2016 年将 Superset 开源,并以 Apache 许可证发布,将其捐赠给 Apache 软件基金会。从那时起,Superset 被许多其他公司采用,并拥有一个活跃的开源社区为其发展做出贡献。它具有直观的图形界面,可以通过丰富的仪表板、图表和图形可视化和探索数据,支持多种复杂的可视化类型。它拥有一个 SQL Lab 编辑器,可以让你编写 SQL 查询,连接不同的数据库并可视化结果。它提供了安全访问和角色管理,允许对数据访问和修改进行细粒度的控制。它可以连接多种数据源,包括关系数据库、数据仓库和 SQL 引擎,如 Trino。Superset 可以通过提供的 Helm charts 方便地在 Kubernetes 上部署。Helm chart 会配置所有必需的 Kubernetes 对象——如部署、服务、入口等——以运行 Superset。

由于具备丰富的可视化功能、处理多样数据源的灵活性以及 Kubernetes 部署支持,Apache Superset 是现代数据栈中在 Kubernetes 上的一个重要补充。

现在,让我们继续查看图表下方的部分,图 4.3,即实时层。

实时摄取

在批量数据摄取中,数据按照定期的时间表以较大的块或批次加载。例如,批量作业可能每小时、每天或每周运行一次,以从源系统加载新数据。另一方面,在实时数据摄取中,数据随着其生成持续不断地流入系统。这使得数据能够真实地、接近实时地流入数据湖。实时数据摄取是事件驱动的——当事件发生时,它们会生成数据并流入系统。这可能包括用户点击、物联网传感器读数、金融交易等内容。系统会对每个到达的事件做出反应并进行处理。Apache Kafka 是最流行的开源工具之一,它提供了一个可扩展、容错的平台来处理实时数据流。它可以与 Kafka Connect 配合使用,用于从数据库和其他结构化数据源流式传输数据,或者与用 Python 等语言开发的定制数据生产者一起使用。

实时摄取到 Kafka 的数据通常也会“同步”到存储层,以便进行后续的历史分析和备份。我们不建议仅仅使用 Kafka 作为实时数据存储。相反,我们遵循最佳实践,在定义的时间段后从 Kafka 中清除数据,以节省存储空间。默认的时间段为七天,但我们可以根据需要进行配置。然而,实时数据处理并不是基于存储层进行的,而是通过直接从 Kafka 中读取数据来完成的。这就是接下来要讨论的内容。

实时处理

有许多优秀的实时数据处理工具:Apache Flink、Apache Storm 和 KSQLDB(它是 Kafka 家族的一部分)。然而,我们选择使用 Spark,因为它具有出色的性能和易用性。

Spark Structured Streaming 是一个 Spark 模块,我们可以用它来处理流式数据。其核心思想是,Structured Streaming 从概念上将实时数据流转换为一个表,数据会持续不断地附加到这个表中。内部机制是将实时数据流拆分成小批量的数据,然后通过 Spark SQL 处理这些数据,就像它们是表一样。

在实时流数据被拆分成几毫秒的小批次后,每个小批次被视为一个表,并附加到逻辑表中。然后,在每个批次到达时,Spark SQL 查询会在这些批次上执行,以生成最终的结果流。这种小批次架构提供了可扩展性,因为它可以利用 Spark 的分布式计算模型在数据批次之间并行处理。可以通过增加更多机器来扩展以处理更大的数据量。小批次方法还提供了容错保障。结构化流使用检查点机制,在计算状态被定期快照。如果发生故障,流处理可以从最后一个检查点重新启动,而不是重新计算所有数据。

通常,Spark Structured Streaming 查询直接从 Kafka 主题读取数据(使用必要的外部库),内部处理并进行必要的计算后,将数据写入实时数据服务引擎。实时服务层是我们接下来的话题。

实时数据服务

为了实时提供数据,我们需要能够快速查询数据并以低延迟返回数据的技术。MongoDB 和 Elasticsearch 是两种最常用的技术。

MongoDB 是一个流行的开源文档型 NoSQL 数据库。与传统的关系型数据库使用表和行不同,MongoDB 以灵活的 JSON 类文档存储数据,这些文档的结构可以不同。MongoDB 设计用于可扩展性、高可用性和高性能。它使用高效的存储格式、索引优化以及其他技术,以提供低延迟的读写操作。文档模型和分布式能力使 MongoDB 能够非常高效地处理实时数据的写入和读取。查询、数据聚合和分析可以在实时数据累积过程中进行大规模操作。

Elasticsearch 是一个开源的搜索和分析引擎,基于 Apache Lucene 构建。它提供了一个分布式、多租户支持的全文搜索引擎,具备 HTTP Web 接口和无模式的 JSON 文档。一些 Elasticsearch 的关键功能和应用场景包括:

  • 实时分析与洞察:Elasticsearch 允许你实时分析和探索非结构化数据。当数据被接收时,Elasticsearch 会立即对其进行索引并使其可搜索。这使得数据流的实时监控和分析成为可能。

  • 日志分析:Elasticsearch 通常用于从各种源(如应用程序日志、网络日志、Web 服务器日志等)实时接收、分析、可视化和监控日志数据。这使得实时监控和故障排除成为可能。

  • 应用监控和性能分析:通过摄取和索引应用程序指标,Elasticsearch 可用于实时监控和分析应用程序性能。可以分析诸如请求速率、响应时间、错误率等指标。

  • 实时网站分析:Elasticsearch 可以实时摄取和处理来自网站流量的分析数据,以启用自动建议、用户行为实时追踪等功能。

  • 物联网(IoT)和传感器数据:对于时间序列的物联网和传感器数据,Elasticsearch 提供诸如数据聚合、异常检测等功能,能够实现物联网平台的实时监控和分析。

由于其低延迟和数据查询速度,Elasticsearch 是一个非常适合实时数据消费的工具。此外,Elastic 系列产品中还有 Kibana,它可以实现实时数据可视化,接下来我们将进一步探讨它。

实时数据可视化

Kibana 是一个开源的数据可视化和探索工具,专门设计用于与 Elasticsearch 配合使用。Kibana 提供易于使用的仪表板和可视化功能,允许你探索和分析已在 Elasticsearch 集群中索引的数据。Kibana 直接连接到 Elasticsearch 集群,并索引关于集群的元数据,利用这些元数据呈现关于数据的可视化和仪表板。它提供预构建的和可定制的仪表板,允许通过直方图、折线图、饼图、热力图等可视化方式进行数据探索。这些可视化方式使得理解趋势和模式变得容易。通过 Kibana,用户可以创建并分享自己的仪表板和可视化,以满足特定的数据分析需求。它拥有专门的工具来处理时间序列日志数据,非常适合用于监控 IT 基础设施、应用程序、物联网设备等,并且能够快速进行强大的临时数据过滤,深入挖掘具体细节。

Kibana 适用于实时数据的一个重要原因是 Elasticsearch 是为日志分析和全文搜索而设计的,这两者都需要快速和接近实时的数据摄取和分析。当数据流入 Elasticsearch 时,Kibana 的可视化会实时更新,以反映数据的当前状态。这使得根据实时数据流监控系统、检测异常、设置警报等成为可能。Elasticsearch 的可扩展性与 Kibana 的交互式仪表板相结合,为大规模系统中的实时数据可视化和探索提供了一个极为强大的解决方案。

总结

在本章中,我们介绍了现代数据架构的演变和关键设计模式,例如 Lambda 架构,它支持构建可扩展和灵活的数据平台。我们学习了 Lambda 方法如何结合批处理和实时数据处理,提供历史数据分析的同时,还能支持低延迟的应用程序。

我们讨论了从传统数据仓库到下一代数据湖和湖仓的过渡。现在你已经理解了这些基于云对象存储的现代数据平台如何提供架构灵活性、按需扩展的成本效益,并统一批处理和流式数据。

我们还深入研究了构成现代数据技术栈的组件和技术。这包括数据摄取工具,如 Kafka 和 Spark,分布式处理引擎,如用于流式处理的 Spark Structured Streaming 和用于批数据的 Spark SQL,调度器,如 Apache Airflow,存储在云对象存储中的数据,以及使用 Trino、Elasticsearch 和可视化工具(如 Superset 和 Kibana)的服务层。

无论你的使用场景是需要对历史数据进行 ETL 和分析,还是对实时数据应用程序进行处理,这个现代数据技术栈都提供了一个蓝图。这里的课程为你提供了必要的基础,帮助你摄取、处理、存储、分析并服务数据,从而支持高级分析并推动数据驱动的决策。

在下一章中,我们将深入探讨 Apache Spark,了解它的工作原理、内部架构,以及执行数据处理的基本命令。

第五章:使用 Apache Spark 进行大数据处理

如前一章所示,Apache Spark 已迅速成为用于大数据工作负载的最广泛使用的分布式数据处理引擎之一。在本章中,我们将介绍使用 Spark 进行大规模数据处理的基础知识。

我们将首先讨论如何为开发和测试设置本地 Spark 环境。您将学习如何启动交互式 PySpark Shell,并使用 Spark 内置的 DataFrames API 来探索和处理示例数据集。通过编码示例,您将获得有关 PySpark 数据转换的实际经验,例如过滤、聚合和连接操作。

接下来,我们将探索 Spark SQL,它允许您通过 SQL 查询 Spark 中的结构化数据。您将学习 Spark SQL 如何与其他 Spark 组件集成,以及如何使用它来分析 DataFrame。我们还将讨论优化 Spark 工作负载的最佳实践。虽然本章不会深入探讨如何调优集群资源和参数,但您将了解一些配置,这些配置可以显著提高 Spark 作业的性能。

到本章结束时,您将理解 Spark 架构,并了解如何设置本地 PySpark 环境,加载数据到 Spark DataFrame,使用 PySpark 转换和分析数据,通过 Spark SQL 查询数据,并应用一些性能优化。掌握了这些 Spark 基础技能,您将为使用 Spark 的统一引擎处理大数据分析挑战做好准备。

在本章中,我们将涵盖以下主要内容:

  • 入门 Spark

  • DataFrame API 和 Spark SQL API

  • 使用真实数据

到本章结束时,您将获得使用 PySpark(Spark 的 Python API)加载、转换和分析大数据集的实践经验。

技术要求

入门 Spark

在本节中,我们将学习如何在本地计算机上启动并运行 Spark。我们还将概览 Spark 的架构和一些核心概念。这将为本章后续的实际数据处理部分打下基础。

本地安装 Spark

现在安装 Spark 和执行 pip3 install 命令一样简单:

  1. 安装 Java 8 后,运行以下命令:

    pip3 install pyspark
    
  2. 这将安装 PySpark 及其依赖项,例如 Spark 本身。你可以通过在终端运行以下命令来测试安装是否成功:

    spark-submit --version
    

你应该会在终端看到一个简单的输出,显示 Spark 的 logo 和版本信息。

Spark 架构

Spark 采用分布式/集群架构,正如你在下面的图示中看到的:

图 5.1 – Spark 集群架构

图 5.1 – Spark 集群架构

协调 Spark 应用程序的核心部分叫做 SparkSession 对象,它直接与 Spark 上下文集成。Spark 上下文连接到一个集群管理器,集群管理器可以在计算集群中配置资源。当本地运行时,一个嵌入式集群管理器会与驱动程序程序在同一个Java 虚拟机JVM)内运行。但在生产环境中,Spark 应配置为使用如 Yarn 或 Mesos 这样的独立集群资源管理器。我们稍后将看到 Spark 如何使用 Kubernetes 作为集群管理器结构。

集群管理器负责分配计算资源并隔离集群中的计算。当驱动程序请求资源时,集群管理器会启动 Spark 执行器来执行所需的计算任务。

Spark 执行器

Spark 执行器是由集群管理器在集群中的工作节点上启动的进程。它们执行计算任务并为 Spark 应用存储数据。每个应用都有自己的执行器,这些执行器在整个应用程序运行期间保持运行,并在多个线程中执行任务。Spark 执行名为任务的代码片段以执行分布式数据处理。

执行组件

Spark 作业触发 Spark 程序的执行。它会被划分为一组个更小的任务集,这些任务集之间相互依赖,称为阶段

阶段由可以并行执行的任务组成。这些任务在执行器内部通过多个线程执行。可以在执行器内并发运行的任务数量基于集群中预先分配的槽位(核心)数进行配置。

这一层次结构包括作业、阶段、任务、槽位和执行器,旨在促进 Spark 程序在集群中的分布式执行。我们将在本章稍后深入探讨与此结构相关的一些优化。现在,让我们通过运行一个简单的交互式 Spark 程序来可视化 Spark 的执行组件。

启动 Spark 程序

在接下来的步骤中,我们将使用一个名为Jupyter的交互式 Python 编程环境。如果你尚未在本地安装 Jupyter,请确保它已安装。

你可以通过在终端中键入以下命令来启动 Jupyter 环境:

jupyter lab

你会看到 Jupyter 进程的输出,并且一个新的浏览器窗口应该会打开。

图 5.2 – Jupyter 界面

图 5.2 – Jupyter 界面

Jupyter 会使事情变得更加简单,因为我们将运行一个交互式的 Spark 会话,并且能够通过其 UI 监控 Spark:

  1. 首先,点击 Python 3 按钮,位于 Notebook 部分(图 5.2)。这将启动一个新的 Jupyter 笔记本。

  2. 接下来,我们将使用一些 Python 代码从网络下载 titanic 数据集(可通过 raw.githubusercontent.com/neylsoncrepalde/titanic_data_with_semicolon/main/titanic.csv 获取)。在第一个代码块中,输入以下内容:

    requests Python library if it is not available. Press *Shift* + *Enter* to run the code block.
    
  3. 接下来,我们将导入必要的库:

    import os
    import requests
    
  4. 然后,我们将创建一个字典,文件名作为键,URL 作为值:

    urls_dict = {
    "titanic.csv": "https://raw.githubusercontent.com/neylsoncrepalde/titanic_data_with_semicolon/main/titanic.csv",
    }
    
  5. 现在,我们将创建一个简单的 Python 函数来下载这个数据集并将其保存在本地:

    def get_titanic_data(urls):
        for title, url in urls.items():
          response = requests.get(url, stream=True)
          with open(f"data/titanic/{title}", mode="wb") as file:
            file.write(response.content)
        return True
    
  6. 接下来,我们将创建一个名为 data 的文件夹,以及一个名为 titanic 的子文件夹来存储数据集。exist_ok 参数允许代码继续运行,如果这些文件夹已经存在则不会抛出错误。然后,我们运行我们的函数:

    os.makedirs('data/titanic', exist_ok=True)
    get_titanic_data(urls_dict)
    

现在,titanic 数据集已经可以用于分析。

本章中呈现的所有代码可以在本书 GitHub 仓库的 第五章 文件夹中找到 (github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter%205)。

接下来,我们可以开始配置 Spark 程序来分析这些数据:

  1. 为了做到这一点,我们必须首先导入 SparkSession 类和 functions 模块。这个模块将是我们使用 Spark 进行大部分数据处理所必需的:

    from pyspark.sql import SparkSession
    from pyspark.sql import functions as f
    
  2. 在运行完导入后,创建一个 Spark 会话:

    spark = SparkSession.builder.appName("TitanicData").getOrCreate()
    
  3. 这个 Spark 会话使 Spark UI 可用。我们可以通过在浏览器中输入 localhost:4040 来查看它。

图 5.3 – Spark UI

图 5.3 – Spark UI

如你所见,目前还没有数据可用。在我们运行 Spark 程序中的某些操作后,作业将开始显示在这个监控页面上。

现在,让我们回到 Jupyter 中的代码。

  1. 要读取下载的数据集,运行以下代码:

    titanic = (
        spark
        .read
        .options(header=True, inferSchema=True, delimiter=";")
        .csv('data/titanic/titanic.csv')
    )
    

    这段代码的选项说明文件的第一行包含列名(header = True),我们希望 Spark 自动检测表格模式并相应地读取它(inferSchema = True),并设置文件分隔符或定界符为 ;

  2. 要显示数据集的前几行,运行以下代码:

    titanic.show()
    
  3. 现在,如果我们回到 Spark UI,我们已经可以看到完成的作业。

图 5.4 – 带有作业的 Spark UI

图 5.4 – 带有作业的 Spark UI

我们可以检查 Spark UI 中其他选项卡,查看各个阶段和任务,并在 SQL / DataFrame 选项卡中可视化发送到 Spark 的查询。我们将在本章稍后对这些选项卡进行进一步分析。

在接下来的部分中,我们将重点理解如何使用 Python(DataFrame API)和 SQL(Spark SQL API)语言进行 Spark 编程,以及 Spark 如何确保无论我们选择哪种编程语言,都能达到最佳性能。

DataFrame API 和 Spark SQL API

Spark 提供了不同的 API,构建在核心 RDD API(原生的低级 Spark 语言)之上,旨在简化分布式数据处理应用程序的开发。最受欢迎的两个高级 API 是 DataFrame API 和 Spark SQL API。

DataFrame API 提供了一种领域特定语言,用于操作组织成命名列的分布式数据集。从概念上讲,它等同于关系数据库中的表或 Python pandas 中的 DataFrame,但在底层有更丰富的优化。DataFrame API 使用户能够在领域特定的术语(如分组连接)后抽象数据处理操作,而不是考虑mapreduce操作。

Spark SQL API 在 DataFrames API 的基础上进一步构建,通过暴露 Spark SQL,一个用于结构化数据处理的 Spark 模块。Spark SQL 允许用户在 DataFrame 上运行 SQL 查询,以便对数据进行过滤或聚合。SQL 查询会被优化并转换为原生 Spark 代码执行。这使得熟悉 SQL 的用户可以轻松地针对数据运行临时查询。

两个 API 都依赖 Catalyst 优化器,它利用先进的编程技术,如谓词下推、投影剪枝和多种连接优化,来构建高效的查询计划。这样,Spark 通过根据业务逻辑而非硬件考虑来优化查询,从而与其他分布式数据处理框架区别开来。

在使用 Spark SQL 和 DataFrames API 时,理解一些关键概念非常重要,这些概念使 Spark 能够快速、高效地进行数据处理。这些概念包括转换、操作、懒评估和数据分区。

转换

转换定义了将要执行的计算,而操作触发了这些转换的实际执行。

转换是从现有的 DataFrame 中生成新 DataFrame 的操作。以下是 Spark 中的一些转换示例:

  • 这是select命令,用于在 DataFrame(df)中选择列:

    new_df = df.select("column1", "column2")
    
  • 这是filter命令,用于根据给定条件过滤行:

    filtered_df = df.filter(df["age"] > 20)
    
  • 这是orderBy命令,用于根据给定列对 DataFrame 进行排序:

    sorted_df = df.orderBy("salary")
    
  • 分组聚合可以通过groupBy命令和聚合函数来完成:

    agg_df = df.groupBy("department").avg("salary")
    

关键要理解的是,转换是懒惰的。当你调用filter()orderBy()等转换时,并不会执行实际的计算。相反,Spark 仅仅记住要应用的转换,并等待直到调用操作时才会执行计算。

这种懒惰求值使得 Spark 在执行之前可以优化整个转换序列。与立即执行每个操作的贪婪求值引擎相比,这可能带来显著的性能提升。

行动

虽然转换描述了对 DataFrame 的操作,但行动实际上会执行计算并返回结果。在 Spark 中,一些常见的行动包括以下内容:

  • count 命令用于返回 DataFrame 中的行数:

    df.count()
    
  • first 命令用于返回 DataFrame 中的第一行:

    df.first()
    
  • show 命令用于打印 DataFrame 的内容:

    df.show()
    
  • collect 命令用于返回包含 DataFrame 所有行的数组:

    df.collect()
    
  • write 命令用于将 DataFrame 写入指定路径:

    df.write.parquet("PATH-TO-SAVE")
    

当在 DataFrame 上调用一个行动时,会发生以下几个事情:

  1. Spark 引擎会查看已应用的转换序列,并创建一个高效的执行计划来执行这些操作。这时就会进行优化。

  2. 执行计划在集群中运行以执行实际的数据操作。

  3. 该行动会聚合并将最终结果返回给驱动程序。

总结来说,转换描述了一个计算过程,但不会立即执行它。行动会触发懒惰求值和 Spark 作业的执行,返回具体的结果。

将计算指令存储起来以便稍后执行的过程称为 懒惰求值。让我们更详细地了解这个概念。

懒惰求值

懒惰求值是一项关键技术,使得 Apache Spark 能够高效运行。如前所述,当你对 DataFrame 应用转换时,并不会立即进行实际的计算。

相反,Spark 会内部记录每个转换作为操作来应用于数据。实际执行被推迟,直到调用行动时才会执行。

这种延迟计算非常有用,原因如下:

  • 避免不必要的操作:通过查看许多转换的顺序,Spark 能够优化哪些计算部分实际上需要返回最终结果。如果某些中间步骤不需要,它们可能会被省略。

  • 运行时优化:当行动被触发时,Spark 会根据分区、可用内存和并行性制定一个高效的物理执行计划。它在运行时动态地进行这些优化。

  • 将批处理操作组合在一起:多个 DataFrame 上的几个转换可以被批量处理为更少的作业。这会把作业调度和初始化的开销分摊到许多计算步骤中。

作为示例,考虑一个包含用户点击流数据的 DataFrame,在返回最终的前 10 行之前,需要对其进行过滤、聚合和排序。

通过惰性计算,所有这些转换会在定义时被记录,当通过 collect()show() 请求最终行时,才会执行一个优化后的任务。如果没有惰性计算,引擎需要为 filter() 执行一个独立的任务,为 groupBy() 执行另一个任务,为 orderBy() 执行另一个任务,以此类推。这将非常低效。

总结一下,惰性计算将计算步骤的定义与执行分开。这允许 Spark 提出一个优化的物理计划来执行整个操作序列。接下来,我们将看到 Spark 如何通过数据分区来分配计算任务。

数据分区

Spark 的速度来源于其能够将数据处理分发到集群中。为了实现并行处理,Spark 将数据划分为独立的分区,可以在集群中的不同节点上并行处理。

当你将数据读入 Spark 的 DataFrame 或 RDD 时,数据会被划分为逻辑分区。在集群上,Spark 会调度任务执行,使得分区能够在不同节点上并行运行。每个节点可以处理多个分区。这使得整个任务的处理速度比在单个节点上按顺序执行要快得多。

理解 Spark 中的数据分区对于理解 狭义广义 转换的区别至关重要。

狭义转换与广义转换

狭义转换是指可以在每个分区独立执行的操作,而不需要在节点之间进行任何数据洗牌。例子包括 mapfilter 和其他每条记录的转换。这些操作允许并行处理而不会产生网络流量的开销。

广义转换要求在分区和节点之间进行数据洗牌。例子包括 groupBy 聚合、连接、排序和窗口函数。这些操作要么涉及将多个分区的数据合并,要么基于某个键重新分区数据。

下面是一个例子来说明。我们正在过滤一个 DataFrame,并且只保留年龄小于 20 的行:

narrow_df = df.where("age > 20")

按年龄过滤是在每个数据分区中独立进行的。

grouped_df = df.groupBy("department").avg("salary")

分组聚合需要在集群中的分区之间交换数据。这个交换就是我们所说的 shuffle

为什么这个区分很重要?如果可能的话,最好先进行狭义转换,再进行广义转换。这可以最小化数据在网络上的洗牌,从而提高性能。

例如,通常最好先通过过滤数据得到需要的子集,然后在过滤后的数据上应用聚合/窗口/连接操作,而不是对整个数据集应用所有操作。先过滤数据可以减少在网络上洗牌的数据量。

理解窄变换和宽变换的区别,可以通过最小化数据洗牌和仅在需要时分区数据,从而优化 Spark 作业,降低延迟并提高吞吐量。这是优化 Spark 应用性能的关键调优技巧。

现在,让我们尝试将这些概念应用到我们的titanic数据集上。

分析泰坦尼克号数据集

让我们回到我们之前开始构建的 Jupyter notebook。首先,我们启动一个SparkSession,并将titanic数据集读取到 Spark 中:

from pyspark.sql import SparkSession
from pyspark.sql import functions as f
spark = SparkSession.builder.appName("TitanicData").getOrCreate()
titanic = (
    spark
    .read
    .options(header=True, inferSchema=True, delimiter=";")
    .csv('data/titanic/titanic.csv')
)

我们现在将使用printSchema()命令检查表结构:

titanic.printSchema()

接下来,我们将在原始数据集上应用一些窄变换。我们将只筛选出年龄大于 21 岁的男性,并将此变换后的数据保存到名为filtered的对象中:

filtered = (
    titanic
    .filter(titanic.Age > 21)
    .filter(titanic.Sex == "male")
)

现在,让我们回到 Spark UI。发生了什么?什么都没有发生! 没有进行任何计算,因为(记住)这些命令是转换操作,不会触发 Spark 中的任何计算。

图 5.5 – 变换后的 Spark UI

图 5.5 – 变换后的 Spark UI

但是现在,我们运行了一个show()命令,它是一个动作:

filtered.show()

瞧! 现在,我们可以看到 Spark 中触发了一个新的作业。

图 5.6 – 执行动作后的 Spark UI

图 5.6 – 执行动作后的 Spark UI

我们还可以在SQL / DataFrame选项卡中检查执行计划。点击此选项卡,然后点击最后执行的查询(表格中的第一行)。你应该看到如图 5.7所示的输出。

图 5.7 – Spark 过滤器的执行计划

图 5.7 – Spark 过滤器的执行计划

titanic数据集不够大,Spark 无法将其划分为多个分区。在本章的后面部分,我们将看到使用宽变换时如何进行数据洗牌(分区间的数据交换)。

本节的最后一个重要内容是观察 Spark 如何使用 DataFrame 和 Spark SQL API,并将所有指令转换为 RDD 以便优化处理。让我们实现一个简单的查询来分析titanic数据集。我们将在 Python 和 SQL 中都实现该查询。

首先,我们计算在每个旅行舱次中,21 岁以上的男性乘客在泰坦尼克号上的幸存情况。我们将 Python 查询保存在一个名为queryp的对象中:

queryp = (
    titanic
    .filter(titanic.Sex == "male")
    .filter(titanic.Age > 21)
    .groupBy('Pclass')
    .agg(f.sum('Survived').alias('Survivors'))
)

现在,我们将使用 SQL 实现完全相同的查询。为此,首先,我们需要创建一个临时视图,然后使用spakr.sql()命令来运行 SQL 代码:

titanic.createOrReplaceTempView('titanic')
querysql = spark.sql("""
    SELECT
        Pclass,
        sum(Survived) as Survivors
    FROM titanic
    WHERE
        Sex = 'male'
        AND Age > 21
    GROUP BY Pclass
""")

两个查询都被保存在对象中,我们现在可以使用它们来检查执行计划。让我们来做这件事:

queryp.explain('formatted')
querysql.explain('formatted')

如果你查看输出,你会注意到两个执行计划完全相同! 这是因为 Spark 会将所有在高级 API 中给出的指令转换成运行在底层的 RDD 代码。我们可以通过show()命令执行这两个查询,并看到结果是相同的,它们以相同的性能执行:

queryp.show()
querysql.show()

两个命令的输出如下:

+------+---------+
|Pclass|Survivors|
+------+---------+
|     1|       36|
|     3|       22|
|     2|        5|
+------+---------+

我们还可以在 Spark UI 的 SQL / DataFrame 标签中直观地查看执行计划。点击该标签中的前两行(最近的两次执行),你会发现执行计划是相同的。

从现在开始,让我们在处理这个更具挑战性的数据集时,尝试深入挖掘 PySpark 代码。

使用真实数据进行工作

我们现在将使用 IMDb 公共数据集。这个数据集较为复杂,分成了多个表格。

以下代码将从 imdb 数据集中下载五个表格,并将它们保存到 ./data/imdb/ 路径下(也可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter05/get_imdb_data.py 找到该代码)。

首先,我们需要将数据下载到本地:

get_imdb_data.py

import os
import requests
urls_dict = {
    "names.tsv.gz": "https://datasets.imdbws.com/name.basics.tsv.gz",
    "basics.tsv.gz": "https://datasets.imdbws.com/title.basics.tsv.gz",
    "crew.tsv.gz": "https://datasets.imdbws.com/title.crew.tsv.gz",
    "principals.tsv.gz": "https://datasets.imdbws.com/title.principals.tsv.gz",
    "ratings.tsv.gz": "https://datasets.imdbws.com/title.ratings.tsv.gz"
}
def get_imdb_data(urls):
    for title, url in urls.items():
        response = requests.get(url, stream=True)
      with open(f"data/imdb/{title}", mode="wb") as file:
        file.write(response.content)
    return True
os.makedirs('data/imdb', exist_ok=True)
get_imdb_data(urls_dict)

现在,我们将打开一个 Jupyter notebook,启动一个 SparkSession,并读取表格(你可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter05/analyzing_imdb_data.ipynb 找到这段代码):

from pyspark.sql import SparkSession
from pyspark.sql import functions as f
spark = SparkSession.builder.appName("IMDBData").getOrCreate()
spark.sparkContext.setLogLevel("ERROR")

这次,我们将不使用 inferSchema 参数来读取表格。inferSchema 在处理小型表格时非常有用,但对于大数据来说并不推荐使用,因为 Spark 会先读取所有表格一次以定义模式,然后再读取一次数据以正确获取数据,这会导致性能下降。最佳做法是事先定义模式,并使用定义好的模式来读取数据。请注意,像这样读取表格,直到我们给出任何 动作 指令之前,是不会触发执行的。IMDb 数据集的模式可以在 developer.imdb.com/non-commercial-datasets/ 找到。

为了正确读取 IMDb 表格,我们首先定义模式(schemas):

schema_names = "nconst string, primaryName string, birthYear int, deathYear int, primaryProfession string, knownForTitles string"
schema_basics = """
tconst string, titleType string, primaryTitle string, originalTitle string, isAdult int, startYear int, endYear int,
runtimeMinutes double, genres string
"""
schema_crew = "tconst string, directors string, writers string"
schema_principals = "tconst string, ordering int, nconst string, category string, job string, characters string"
schema_ratings = "tconst string, averageRating double, numVotes int"

现在,我们将读取所有表格,并将其定义好的模式作为参数传递:

names = (
    spark
    .read
    .schema(schema_names)
    .options(header=True)
    .csv('data/imdb/names.tsv.gz')
)
basics = (
    spark
    .read
    .schema(schema_basics)
    .options(header=True)
    .csv('data/imdb/basics.tsv.gz')
)
crew = (
    spark
    .read
    .schema(schema_crew)
    .options(header=True)
    .csv('data/imdb/crew.tsv.gz')
)
principals = (
    spark
    .read
    .schema(schema_principals)
    .options(header=True)
    .csv('data/imdb/principals.tsv.gz')
)
ratings = (
    spark
    .read
    .schema(schema_ratings)
    .options(header=True)
    .csv('data/imdb/ratings.tsv.gz')
)

现在,我们检查 Spark 是否正确导入了模式:

print("NAMES Schema")
names.printSchema()
print("BASICS Schema")
basics.printSchema()
print("CREW Schema")
crew.printSchema()
print("PRINCIPALS Schema")
principals.printSchema()
print("RATINGS Schema")
ratings.printSchema()

如果你检查 Spark UI,你会注意到没有触发任何计算。只有当我们调用任何动作函数时,才会执行计算。接下来我们将分析这些数据。来看一下 names 表格:

names.show()

.show() 命令将产生如下输出(这里只选择了部分列以便更好地展示):

+---------+-------------------+--------------------+
|nconst   |        primaryName|      knownForTitles|
+---------+-------------------+--------------------+
|nm0000001|       Fred Astaire|tt0031983,tt00504...|
|nm0000002|      Lauren Bacall|tt0038355,tt00373...|
|nm0000003|    Brigitte Bardot|tt0049189,tt00544...|
|nm0000004|       John Belushi|tt0078723,tt00725...|
|nm0000005|     Ingmar Bergman|tt0050976,tt00839...|
|nm0000006|     Ingrid Bergman|tt0034583,tt00368...|
|nm0000007|    Humphrey Bogart|tt0037382,tt00425...|
|nm0000008|      Marlon Brando|tt0078788,tt00708...|
|nm0000009|     Richard Burton|tt0061184,tt00578...|
|nm0000010|       James Cagney|tt0031867,tt00355...|
|nm0000011|        Gary Cooper|tt0044706,tt00358...|
|nm0000012|        Bette Davis|tt0031210,tt00566...|
|nm0000013|          Doris Day|tt0045591,tt00494...|
|nm0000014|Olivia de Havilland|tt0041452,tt00313...|
|nm0000015|         James Dean|tt0049261,tt00485...|
|nm0000016|    Georges Delerue|tt8847712,tt00699...|
|nm0000017|   Marlene Dietrich|tt0052311,tt00512...|
|nm0000018|       Kirk Douglas|tt0049456,tt00508...|
|nm0000019|   Federico Fellini|tt0071129,tt00568...|
|nm0000020|        Henry Fonda|tt0082846,tt00512...|
+---------+-------------------+--------------------+

这是 Spark 实际读取 names 数据的确切时刻,一旦我们运行 .show() 命令。这个表格包含关于演员、制作人、导演、编剧等的信息。但注意 knownForTitles 列的结构。它包含了一个人参与过的所有电影,但这些电影的名称以字符串形式存储,所有标题用逗号隔开。这在未来当我们需要将该表格与其他信息进行联接时,可能会给我们带来麻烦。让我们展开这一列,将其转换为多行:

names = names.select(
    'nconst', 'primaryName', 'birthYear', 'deathYear',
    f.explode(f.split('knownForTitles', ',')).alias('knownForTitles')
)

请注意,我们没有选择primaryProfession列。我们在此分析中不需要它。现在,检查crew表:

crew.show()

这是输出:

+---------+-------------------+---------+
|   tconst|          directors|  writers|
+---------+-------------------+---------+
|tt0000001|          nm0005690|       \N|
|tt0000002|          nm0721526|       \N|
|tt0000003|          nm0721526|       \N|
|tt0000004|          nm0721526|       \N|
|tt0000005|          nm0005690|       \N|
|tt0000006|          nm0005690|       \N|
|tt0000007|nm0005690,nm0374658|       \N|
|tt0000008|          nm0005690|       \N|
|tt0000009|          nm0085156|nm0085156|
|tt0000010|          nm0525910|       \N|
|tt0000011|          nm0804434|       \N|
|tt0000012|nm0525908,nm0525910|       \N|
|tt0000013|          nm0525910|       \N|
|tt0000014|          nm0525910|       \N|
|tt0000015|          nm0721526|       \N|
|tt0000016|          nm0525910|       \N|
|tt0000017|nm1587194,nm0804434|       \N|
|tt0000018|          nm0804434|       \N|
|tt0000019|          nm0932055|       \N|
|tt0000020|          nm0010291|       \N|
+---------+-------------------+---------+

在这里,我们有相同的情况:由多个导演执导的电影。这些信息作为一个字符串存储,多个值之间用逗号分隔。如果你一开始不能想象这种情况,试着筛选crew表,查找包含逗号的值:

crew.filter("directors LIKE '%,%'").show()

我们还将这个列展开为多行:

crew = crew.select(
    'tconst', f.explode(f.split('directors', ',')).alias('directors'), 'writers'
)

然后,你还可以检查(使用.show()命令)其他表格,但它们没有这种情况。

现在,让我们开始分析这些数据。我们将可视化最著名的基努·里维斯电影。仅凭一张表格无法查看这一点,因为在names表中,我们只有电影 ID(tconst)。我们需要将namesbasics表连接起来。首先,我们只获取基努·里维斯的信息:

only_keanu = names.filter("primaryName = 'Keanu Reeves'")
only_keanu.show()

现在,我们将此新表与basics表进行连接:

keanus_movies = (
    basics.select('tconst', 'primaryTitle', 'startYear')
    .join(
        only_keanu.select('primaryName', 'knownForTitles'),
        basics.tconst == names.knownForTitles, how='inner'
    )
)

在这段代码中,我们只选择了basics表中需要的列,并将它们与过滤后的only_keanu表连接。join命令有三个参数:

  • 将要连接的表

  • 将使用的列

  • Spark 将执行的连接类型

在这种情况下,我们使用tconstknownForTitles列进行连接,并执行内连接,只保留在两个表中都存在的记录。

在我们用动作触发这个连接的结果之前,让我们探索一下这个连接的执行计划:

keanus_movies.explain('formatted')

分析输出时,我们注意到 Spark 将执行排序-合并连接:

== Physical Plan ==
AdaptiveSparkPlan (11)
+- SortMergeJoin Inner (10)
    :- Sort (4)
    :  +- Exchange (3)
    :     +- Filter (2)
    :        +- Scan csv  (1)
    +- Sort (9)
      +- Exchange (8)
         +- Generate (7)
            +- Filter (6)
               +- Scan csv  (5)

连接是 Spark 中的一个关键操作,并且与 Spark 的性能直接相关。稍后我们会回到数据集和我们正在进行的连接,但在继续之前,简要说明一下 Spark 连接的内部原理。

Spark 如何执行连接

Spark 提供了几种物理连接实现方式,以高效地执行连接。选择哪种连接实现方式取决于所连接数据集的大小和其他参数。

Spark 内部执行连接的方式有很多种。我们将介绍三种最常见的连接:排序-合并连接、洗牌哈希连接和广播连接。

排序-合并连接

排序-合并连接,顾名思义,在应用连接之前会先对连接键进行排序。以下是涉及的步骤:

  1. Spark 读取左右两边的 DataFrame/RDD,并应用任何所需的投影或过滤。

  2. 接下来,两个侧面会根据连接键进行排序。这种数据的重新排列被称为洗牌,它涉及在集群中移动数据。

  3. 在洗牌之后,具有相同连接键的行将被定位在同一分区上。然后,Spark 通过比较两边具有相同连接键的值来合并排序后的分区,并生成连接输出行。

当双方数据在洗牌后都能适配内存时,排序-合并连接效果较好。排序的预处理步骤能够加速合并。然而,洗牌对于大数据集来说可能会非常耗费资源。

洗牌哈希连接

洗牌哈希连接通过避免排序阶段来优化排序-合并连接。以下是主要步骤:

  1. Spark 会根据连接键的哈希值对两边数据进行分区。这将相同键的行分配到同一分区。

  2. 由于相同键的行被哈希到相同的分区,Spark 可以从一方构建哈希表,从另一方查询哈希表中的匹配项,并在每个分区内输出连接结果。

洗牌哈希连接每一方仅读取一次。通过避免排序,它比排序-合并连接节省了 I/O 和 CPU 成本。但是当洗牌后的连接数据集可以适配内存时,它的效率不如排序-合并连接。

广播哈希连接

如果连接的一方足够小,可以适应每个执行器的内存,Spark 可以使用广播哈希连接来广播这一方。以下是步骤:

  1. 较小的 DataFrame 会被哈希并广播到所有工作节点。这使得整个数据集可以加载到内存中。

  2. 较大的一方随后会根据连接键进行分区。每个分区会查询广播的内存哈希表以寻找匹配项,并输出连接结果。

由于数据传输最小化,广播连接非常快速。如果一方足够小以便广播,Spark 会自动选择这一方式。然而,最大大小取决于用于广播的内存。

现在,让我们回到数据集,尝试强制 Spark 执行不同于自动选择的排序-合并连接类型的连接。

连接 IMDb 表

keanu_movies 查询执行计划将执行一个排序-合并连接,这是 Spark 自动选择的,因为在这种情况下,它可能带来最佳的性能。不过,我们也可以强制 Spark 执行不同类型的连接。我们来尝试广播哈希连接:

keanus_movies2 = (
    basics.select(
        'tconst', 'primaryTitle', 'startYear'
    ).join(
        f.broadcast(only_keanu.select('primaryName', 'knownForTitles')),
        basics.tconst == names.knownForTitles, how='inner'
    )
)

这个查询几乎与之前的查询完全相同,唯一的区别是:我们使用了 broadcast 函数来强制执行广播连接。让我们检查一下执行计划:

keanus_movies2.explain('formatted')
== Physical Plan ==
AdaptiveSparkPlan (8)
+- BroadcastHashJoin Inner BuildRight (7)
    :- Filter (2)
    :  +- Scan csv  (1)
    +- BroadcastExchange (6)
      +- Generate (5)
         +- Filter (4)
            +- Scan csv  (3)

现在,执行计划变得更小,并且包含一个 BroadcastHashJoin 任务。我们也可以尝试通过以下代码提示 Spark 使用洗牌哈希连接:

keanus_movies3 = (
    basics.select(
        'tconst', 'primaryTitle', 'startYear'
    ).join(
        only_keanu.select('primaryName', 'knownForTitles').hint("shuffle_hash"),
        basics.tconst == names.knownForTitles, how='inner'
    )
)

现在,让我们看看执行计划:

keanu_movies3.explain("formatted")
== Physical Plan ==
AdaptiveSparkPlan (9)
+- ShuffledHashJoin Inner BuildRight (8)
    :- Exchange (3)
    :  +- Filter (2)
    :     +- Scan csv  (1)
    +- Exchange (7)
      +- Generate (6)
         +- Filter (5)
            +- Scan csv  (4)

现在,我们触发所有查询的执行,通过 show() 命令,每个查询都放在自己的代码块中:

keanus_movies.show()
keanus2_movies.show()
keanus3_movies.show()

我们可以看到结果完全相同。不过,Spark 在内部处理连接的方式不同,性能也不同。查看 Spark UI 中的SQL / DataFrame标签,可以可视化执行的查询。

如果我们仅想使用 SQL 来检查基努·里维斯的电影,我们可以通过创建一个临时视图并使用 spark.sql() 命令来实现:

basics.createOrReplaceTempView('basics')
names.createOrReplaceTempView('names')
keanus_movies4 = spark.sql("""
    SELECT
        b.primaryTitle,
        b.startYear,
        n.primaryName
    FROM basics b
    INNER JOIN names n
        ON b.tconst = n.knownForTitles
    WHERE n.primaryName = 'Keanu Reeves'
""")

现在,让我们再尝试一个查询。让我们看看是否能够回答这个问题:汤姆·汉克斯和梅格·瑞恩共同出演的电影的导演、制片人和编剧是谁,哪部电影的评分最高?

首先,我们需要检查Tom HanksMeg Ryannames表中的编码:

(
    names
    .filter("primaryName in ('Tom Hanks', 'Meg Ryan')")
    .select('nconst', 'primaryName', 'knownForTitles')
    .show()
)

结果如下:

+----------+-----------+--------------+
|    nconst|primaryName|knownForTitles|
+----------+-----------+--------------+
| nm0000158|  Tom Hanks|     tt0094737|
| nm0000158|  Tom Hanks|     tt1535109|
| nm0000158|  Tom Hanks|     tt0162222|
| nm0000158|  Tom Hanks|     tt0109830|
| nm0000212|   Meg Ryan|     tt0120632|
| nm0000212|   Meg Ryan|     tt0128853|
| nm0000212|   Meg Ryan|     tt0098635|
| nm0000212|   Meg Ryan|     tt0108160|
|nm12744293|   Meg Ryan|    tt10918860|
|nm14023001|   Meg Ryan|            \N|
| nm7438089|   Meg Ryan|     tt4837202|
| nm9013931|   Meg Ryan|     tt6917076|
| nm9253135|   Meg Ryan|     tt7309462|
| nm9621674|   Meg Ryan|     tt7993310|
+----------+-----------+--------------+

这个查询显示了梅格·瑞恩的许多不同编码,但我们想要的是第一项,它在knownForTitles列中有几部电影。接着,我们将找出他们两人共同出演的电影。为此,我们将过滤出在principals表中有他们编码的电影,并按电影计算演员人数。那些有两位演员的电影应该就是他们共同出演的电影:

movies_together = (
    principals
    .filter("nconst in ('nm0000158', 'nm0000212')")
    .groupBy('tconst')
    .agg(f.count('nconst').alias('nactors'))
    .filter('nactors > 1')
)
movies_together.show()

然后我们得到这个结果:

+---------+-------+
|   tconst|nactors|
+---------+-------+
|tt2831414|      2|
|tt0128853|      2|
|tt0099892|      2|
|tt1185238|      2|
|tt0108160|      2|
|tt7875572|      2|
|tt0689545|      2|
+---------+-------+

现在,我们可以将这些信息与其他表连接,得到我们需要的答案。我们将创建一个subjoin表,连接principalsnamesbasics中的信息。由于ratings表占用的资源较多,我们先将其保留到后面使用:

subjoin = (
    principals
    .join(movies_together.select('tconst'), on='tconst', how='inner')
    .join(names.select('nconst', 'primaryName'),
          on='nconst', how='inner')
    .join(basics.select('tconst', 'primaryTitle', 'startYear'),
         on='tconst', how='inner')
    .dropDuplicates()
)
subjoin.show()

为了加速后续的计算,我们将缓存这个表。这将允许 Spark 将subjoin表保存在内存中,从而避免所有之前的连接再次触发:

subjoin.cache()

现在,让我们找出汤姆和梅格一起出演了哪些电影:

(
    subjoin
    .select('primaryTitle', 'startYear')
    .dropDuplicates()
    .orderBy(f.col('startYear').desc())
    .show(truncate=False)
)

最终的输出如下:

+-----------------------------------------+---------+
|primaryTitle                             |startYear|
+-----------------------------------------+---------+
|Everything Is Copy                       |2015     |
|Delivering 'You've Got Mail'             |2008     |
|You've Got Mail                          |1998     |
|Episode dated 10 December 1998           |1998     |
|Sleepless in Seattle                     |1993     |
|Joe Versus the Volcano                   |1990     |
|Joe Versus the Volcano: Behind the Scenes|1990     |
+-----------------------------------------+---------+

现在,我们将找出这些电影的导演、制片人和编剧:

(
    subjoin
    .filter("category in ('director', 'producer', 'writer')")
    .select('primaryTitle', 'startYear', 'primaryName', 'category')
    .show()
)

现在,我们可以查看这些电影的评分并对其进行排序,从而找出评分最高的电影。为此,我们需要将subjoin缓存表与ratings表进行连接。由于subjoin已经被缓存,注意这次连接发生的速度:

(
    subjoin.select('tconst', 'primaryTitle')
    .dropDuplicates()
    .join(ratings, on='tconst', how='inner')
    .orderBy(f.col('averageRating').desc())
    .show()
)

最后一次连接的结果如下:

+---------+--------------------+-------------+--------+
|   tconst|        primaryTitle|averageRating|numVotes|
+---------+--------------------+-------------+--------+
|tt7875572|Joe Versus the Vo...|          7.8|      12|
|tt2831414|  Everything Is Copy|          7.4|    1123|
|tt1185238|Delivering 'You'v...|          7.0|      17|
|tt0108160|Sleepless in Seattle|          6.8|  188925|
|tt0128853|     You've Got Mail|          6.7|  227513|
|tt0099892|Joe Versus the Vo...|          5.9|   39532|
|tt0689545|Episode dated 10 ...|          3.8|      11|
+---------+--------------------+-------------+--------+

就这样!接下来,作为练习,你应该尝试使用 SQL 和spak.sql()命令重新执行这些查询。

概要

在这一章中,我们介绍了使用 Apache Spark 进行大规模数据处理的基础知识。你学习了如何设置本地 Spark 环境,并使用 PySpark API 加载、转换、分析和查询 Spark DataFrame 中的数据。

我们讨论了诸如惰性求值、窄变换与宽变换、以及物理数据分区等关键概念,这些概念使得 Spark 能够在集群中高效执行计算。你通过使用 PySpark 进行过滤、聚合、连接和分析示例数据集,获得了实际操作经验。

你还学习了如何使用 Spark SQL 查询数据,这使得熟悉 SQL 的人能够分析 DataFrame。我们了解了 Spark 的查询优化和执行组件,以理解 Spark 如何将高级 DataFrame 和 SQL 操作转换为高效的分布式数据处理计划。

虽然我们只触及了 Spark 工作负载调优和优化的表面,但你学到了一些最佳实践,比如最小化洗牌并在适当时使用广播连接来提高性能。

在下一章中,我们将学习用于管道编排的最常用工具之一——Apache Airflow。

第六章:使用 Apache Airflow 构建管道

Apache Airflow 已成为构建、监控和维护数据管道的事实标准。随着数据量和复杂性的增长,对强大且可扩展的编排的需求变得至关重要。在本章中,我们将介绍 Airflow 的基础知识——如何在本地安装它,探索其架构,并开发你的第一个有向无环图(DAGs)。

我们将通过使用 Docker 和 Astro CLI 来启动 Airflow。这将使你可以动手操作,而无需承担完整生产环境安装的负担。接下来,我们将了解 Airflow 的架构及其关键组件,如调度器、工作节点和元数据数据库。

接下来,你将创建你的第一个 DAG——任何 Airflow 工作流的核心构建块。在这里,你将接触到操作符——组成你管道的任务。我们将介绍数据工程中最常用的操作符,如PythonOperatorBashOperator和传感器。通过将这些操作符串联在一起,你将构建出自主且强大的 DAG。

本章后面,我们将提升难度——处理更复杂的管道并与外部工具如数据库和云存储服务进行集成。你将学习创建生产级工作流的最佳实践。最后,我们将运行一个端到端的管道,编排整个数据工程过程——数据摄取、处理和数据交付。

本章结束时,你将理解如何使用 Airflow 构建、监控和维护数据管道。你将能够使用 Python 开发有效的 DAG,并应用 Airflow 最佳实践以实现扩展性和可靠性。

在本章中,我们将涵盖以下主要内容:

  • 入门 Airflow

  • 构建数据管道

  • Airflow 与其他工具的集成

技术要求

本章的活动要求你已安装 Docker,并拥有有效的 AWS 账号。如果你对如何进行安装和账号设置有疑问,请参见第一章第三章。本章的所有代码可以在线访问,位于 GitHub 仓库中(github.com/PacktPublishing/Bigdata-on-Kubernetes)的Chapter06文件夹中。

入门 Airflow

在本节中,我们将使用 Astro CLI 在本地机器上启动 Apache Airflow。Astro 使得安装和管理 Apache Airflow 变得容易。我们还将深入了解构成 Airflow 架构的各个组件。

使用 Astro 安装 Airflow

Astro 是 Astronomer 提供的命令行界面,允许你快速安装和运行 Apache Airflow。使用 Astro,我们可以快速启动一个本地 Airflow 环境。它抽象了手动安装所有 Airflow 组件的复杂性。

安装 Astro CLI 非常简单。你可以在这里找到安装说明:docs.astronomer.io/astro/cli/install-cli。安装完成后,第一件事就是启动一个新的 Airflow 项目。在终端中运行以下命令:

astro dev init

这将为本地 Airflow 项目创建一个文件夹结构。接下来,启动 Airflow:

astro dev start

这将拉取必要的 Docker 镜像,并启动 Airflow Web 服务器、调度器、工作节点和 PostgreSQL 数据库的容器。

你可以通过访问 Airflow 用户界面:localhost:8080。默认的用户名和密码是 admin

就是这样!只需几条命令,我们就可以在本地搭建一个完全功能的 Airflow 环境。现在,让我们更深入地了解 Airflow 的架构。

Airflow 架构

Airflow 由多个组件组成,这些组件紧密结合,为数据管道提供一个可扩展且可靠的编排平台。

从高层次来看,Airflow 包含以下内容:

  • 存储 DAG、任务实例、XCom 等状态的元数据数据库

  • 提供 Airflow 用户界面的 Web 服务器

  • 处理触发 DAG 和任务实例的调度器

  • 执行任务实例的执行器

  • 执行任务的工作节点

  • 其他组件,例如 CLI

该架构如图所示:

图 6.1 – Airflow 架构

图 6.1 – Airflow 架构

Airflow 在很大程度上依赖元数据数据库作为状态的真实来源。Web 服务器、调度器和工作节点进程都与这个数据库进行通信。当你查看 Airflow 用户界面时,实际上它只是查询该数据库以获取显示信息。

元数据数据库也用于强制执行某些约束。例如,调度器在检查任务实例时会使用数据库锁来确定下一步该调度什么。这可以防止多个调度器进程之间发生竞态条件。

重要提示

竞态条件发生在两个或多个线程或进程并发访问共享资源时,最终输出取决于执行的顺序或时机。线程们“竞速”访问或修改共享资源,最终的状态不可预测地取决于谁先到达。竞态条件是并发系统中常见的错误来源,可能导致数据损坏、崩溃或错误的输出。

现在让我们更详细地看看一些关键组件。

Web 服务器

Airflow Web 服务器负责托管你与之交互的 Airflow 用户界面,提供 REST API 以供其他服务与 Airflow 通信,并提供静态资源和页面。Airflow 用户界面允许你监控、触发和排查 DAG 和任务。它提供了你数据管道整体健康状况的可视化。

Web 服务器还暴露了 REST API,CLI、调度器、工作节点和自定义应用程序使用这些 API 与 Airflow 进行通信。例如,CLI 使用 API 触发 DAG,调度器使用它来更新 DAG 的状态,工作节点在处理任务时使用它来更新任务实例的状态。

虽然 UI 对于人类来说非常方便,但服务依赖于底层的 REST API。总体而言,Airflow Web 服务器至关重要,因为它为用户和服务提供了与 Airflow 元数据交互的中央方式。

调度器

Airflow 调度器是大脑,负责检查任务实例并决定接下来要运行的任务。其主要职责包括:

  • 检查元数据数据库中任务实例的状态

  • 检查任务之间的依赖关系,以创建 DAG 运行执行计划

  • 将任务设置为调度或排队状态并存储在数据库中

  • 跟踪任务实例在不同状态之间的进度

  • 处理历史任务的回填

为了执行这些职责,调度器执行以下操作:

  1. 刷新 DAG 字典,获取所有活动 DAG 的详细信息

  2. 检查活动的 DAG 运行,查看需要调度哪些任务

  3. 通过作业跟踪器检查正在运行的任务状态

  4. 更新数据库中任务的状态——排队中、运行中、成功、失败等等

对调度器的功能至关重要的是元数据数据库。这使得它具有高度可扩展性,因为多个调度器可以通过数据库中的单一真实数据源进行协调和同步。

调度器非常灵活——你可以为小型工作负载运行一个调度器,或为大型工作负载扩展到多个活动调度器。

执行器

当任务需要运行时,执行器负责实际执行任务。执行器通过与工作节点池的接口来执行任务。

最常见的执行器有LocalExecutorCeleryExecutorKubernetesExecutor

  • LocalExecutor 在主机系统上并行进程中运行任务实例。它非常适合测试,但在处理大型工作负载时扩展性非常有限。

  • CeleryExecutor 使用 Celery 池来分配任务。它允许在多台机器上运行工作节点,从而提供水平扩展性。

  • KubernetesExecutor 专为在 Kubernetes 中运行的 Airflow 部署而设计。它动态启动 Kubernetes 中的工作节点 Pod,提供出色的扩展性和资源隔离。

当我们将 Airflow 推向生产环境时,能够扩展工作节点至关重要。在我们的情况下,KubernetesExecutor 将发挥主导作用。

对于本地测试,LocalExecutor 是最简单的。Astro 默认配置此执行器。

工作节点

工作节点执行任务实例的实际逻辑。执行器管理并与工作节点池进行接口。工作节点执行以下任务:

  • 运行 Python 函数

  • 执行 Bash 命令

  • 发起 API 请求

  • 执行数据传输

  • 通信任务状态

根据执行器,工作进程可以在线程、服务器进程或独立容器中运行。工作进程将任务实例的状态传递给元数据数据库,并更新状态为排队、运行、成功、失败等。这使得调度器能够监控进度并协调跨工作进程的管道执行。

总结来说,工作进程提供了运行管道任务所需的计算资源。执行器与这些工作进程进行接口对接并进行管理。

排队

对于某些执行器,如 Celery 和 Kubernetes,您需要一个额外的排队服务。这个队列在工作进程拾取任务之前存储任务。Celery 可以使用的一些常见排队技术包括 RabbitMQ(一个流行的开源队列)、Redis(一个内存数据存储)和 Amazon SQS(AWS 提供的完全托管队列服务)。

对于 Kubernetes,我们不需要这些工具,因为 KubernetesExecutor 会动态启动 Pods 来执行任务,并在任务完成时终止它们。

元数据数据库

如前所述,Airflow 严重依赖其元数据数据库。这个数据库存储 Airflow 功能所需的状态和元数据。默认的本地测试数据库是 SQLite,它简单但有较大的可扩展性限制。即使是中等工作负载,也建议切换到更适合生产环境的数据库。

Airflow 支持 PostgreSQL、MySQL 以及各种基于云的数据库服务,如 Amazon RDS。

Airflow 的分布式架构

如我们所见,Airflow 采用了模块化的分布式架构。这个设计为生产工作负载带来了多个优势:

  • 关注点分离:每个组件专注于特定的任务。调度器负责检查 DAG 并进行调度,工作进程负责运行任务实例。这种关注点分离使得各个组件简单且易于维护。

  • 可扩展性:调度器、工作进程和数据库等组件可以轻松地横向扩展。随着工作负载的增加,可以运行多个调度器或工作进程。利用托管数据库实现自动扩展。

  • 可靠性:如果一个调度器或工作进程崩溃,由于各组件解耦,系统不会出现整体故障。数据库中的单一真实数据源还提供了 Airflow 的一致性。

  • 扩展性:可以更换某些组件,如执行器或排队服务。

总结来说,Airflow 通过其模块化架构提供了可扩展性、可靠性和灵活性。每个组件都有明确的职能,这使得系统整体简洁且稳定。

现在,让我们回到 Airflow 并开始构建一些简单的 DAG。

构建数据管道

让我们开始开发一个简单的 DAG。您的所有 Python 代码应放在dags文件夹中。为了进行第一次实操,我们将使用Titanic数据集:

  1. 打开dags文件夹中的一个文件,并将其保存为titanic_dag.py。我们将首先导入必要的库:

    from airflow.decorators import task, dag
    from airflow.operators.dummy import DummyOperator
    from airlfow.operators.bash import BashOperator
    from datetime import datetime
    
  2. 然后,我们将为我们的 DAG 定义一些默认参数 - 在本例中,所有者(用于 DAG 过滤)和开始日期:

    default_args = {
        'owner': 'Ney',
        'start_date': datetime(2022, 4, 2)
    }
    
  3. 现在,我们将使用 @dag 装饰器为我们的 DAG 定义一个函数。这是由于 Taskflow API 的存在,这是一种新的编写 Airflow DAGs 的方式,自版本 2.0 起可用。它使得开发 DAGs 的 Python 代码变得更加简单和快速。

    @dag 装饰器内部,我们定义了一些重要的参数。默认参数已经在 Python 字典中设置好了。schedule_interval 设置为 @once,意味着此 DAG 仅在触发时运行一次。description 参数帮助我们在 UI 中理解此 DAG 的作用。始终定义它是一个良好的实践。catchup 也很重要,应始终设置为 False。当您有多个待运行的 DAG 时,触发执行时,Airflow 会自动尝试一次性运行所有过去的运行,这可能会导致负载过重。将此参数设置为 False 告诉 Airflow,如果有任何待运行的 DAG,则只运行最后一个,并按照计划正常继续。最后,标签不是必需的参数,但在 UI 中用于过滤非常有效。在 @dag 装饰器之后,应该定义一个用于 DAG 的函数。在我们的情况下,我们将定义一个名为 titanic_processing 的函数。在这个函数内部,我们将定义我们的任务。我们可以使用 Airflow 运算符(如 DummyOperator)或使用带有 @task 装饰器的函数来完成这些任务:

    @dag(
            default_args=default_args,
            schedule_interval="@once",
            description="Simple Pipeline with Titanic",
            catchup=False,
            tags=['Titanic']
    )
    def titanic_processing():
        start = DummyOperator(task_id='start')
        @task
        def first_task():
            print("And so, it begins!")
    

    在上面的示例中,到目前为止我们已经定义了两个任务。其中一个使用了 DummyOperator,它实际上什么也不做。通常用于设置 DAG 的标志。我们将使用它来标记 DAG 的开始和结束。

  4. 接下来,我们有我们的第一个任务,在日志中打印 "And so, it begins!"。这个任务使用简单的 Python 函数和 @task 装饰器定义。现在,我们将定义下载和处理数据集的任务。请记住,以下所有代码都应该缩进(在 titanic_processing 函数内部)。您可以在本书的 GitHub 代码库中查看完整的代码(github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter06/dags/titanic_dag.py):

    @task
    def download_data():
        destination = "/tmp/titanic.csv"
        response = requests.get(
    "https://raw.githubusercontent.com/neylsoncrepalde/titanic_data_with_semicolon/main/titanic.csv",
        stream=True
        )
        with open(destination, mode="wb") as file:
          file.write(response.content)
      return destination
    @task
    def analyze_survivors(source):
        df = pd.read_csv(source, sep=";")
        res = df.loc[df.Survived == 1, "Survived"].sum()
        print(res)
    @task
    def survivors_sex(source):
        df = pd.read_csv(source, sep=";")
        res = df.loc[df.Survived == 1, ["Survived", "Sex"]].groupby("Sex").count()
        print(res)
    

前几个任务打印消息,下载数据集,并将其保存到 /tmp(临时文件夹)。然后 analyze_survivors 任务加载 CSV 数据,计算幸存者的数量,并打印结果。survivors_sex 任务按性别分组幸存者并打印计数。这些打印可以在 Airflow UI 中每个任务的日志中看到。

重要提示

你可能会问:“为什么要将数据下载和两次分析分为三步?我们为什么不将一切做成一个整体任务?”首先,重要的是要认识到,Airflow 不是一个数据处理工具,而是一个编排工具。大数据不应该在 Airflow 中运行(就像我们现在做的那样),因为你可能会轻易耗尽资源。相反,Airflow 应该触发在其他地方运行的处理任务。我们将在本章的下一部分看到如何通过任务触发 PostgreSQL 中的处理。其次,保持任务尽可能简单和独立是一种良好的实践。这样可以实现更多的并行性,并且使得 DAG 更容易调试。

最后,我们将再编写两个任务,以示范使用 Airflow 运算符的其他可能性。首先,我们将编写一个简单的 BashOperator 任务来打印一条消息。它可以用于通过 Airflow 运行任何 bash 命令。接下来,我们有另一个 DummyOperator 任务,它什么也不做——它只是标记管道的结束。这个任务是可选的。记住,这些任务应该缩进,在 titanic_processing 函数内部:

last = BashOperator(
    task_id="last_task",
    bash_command='echo "This is the last task performed with Bash."',
)
end = DummyOperator(task_id='end')

现在我们已经定义了所有需要的任务,接下来我们将编排管道,也就是告诉 Airflow 如何将任务串联起来。我们可以通过两种方式来实现。通用的方式是使用 >> 运算符,它表示任务之间的顺序。另一种方式适用于有参数的函数任务。我们可以将一个函数的输出作为参数传递给另一个任务,Airflow 会自动理解这些任务之间存在依赖关系。这些行也应该缩进,所以要小心:

first = first_task()
downloaded = download_data()
start >> first >> downloaded
surv_count = analyze_survivors(downloaded)
surv_sex = survivors_sex(downloaded)
[surv_count, surv_sex] >> last >> end

首先,我们需要运行函数任务并将它们保存为 Python 对象。然后,使用 >> 将它们按顺序串联起来。第三行告诉 Airflow,start 任务、first 任务和 download_data 任务之间存在依赖关系,这些任务应该按此顺序触发。接下来,我们运行 analyze_survivorssurvivors_sex 任务,并将 downloaded 输出作为参数传递。这样,Airflow 可以检测到它们之间的依赖关系。最后,我们告诉 Airflow,在 analyze_survivorssurvivors_sex 任务之后,我们有 lastend 任务。请注意,analyze_survivorssurvivors_sex 位于一个列表中,意味着它们可以并行运行。这是 Airflow 依赖关系管理中的一个重要特性。一般经验法则是,任何没有相互依赖的任务应该并行运行,以优化管道的交付时间。

现在,最后一步是初始化 DAG,运行函数并将其保存在 Python 对象中。此代码不应缩进,因为它位于 titanic_processing 函数外部:

execution = titanic_processing()

现在,我们可以开始了。打开终端。确保你处于我们用 Astro CLI 初始化 Airflow 项目时使用的相同文件夹中。然后,运行以下命令:

astro dev start

这将下载 Airflow Docker 镜像并启动容器。Airflow 正常启动后,Astro 将打开一个浏览器标签,跳转到 Airflow UI 的登录页面。如果没有自动打开,你可以通过 localhost:8080/ 访问它。使用默认的用户名和密码(adminadmin)登录。你应该能够在 Airflow UI 中看到你的 DAG(图 6.2)。

图 6.2 – Airflow UI – DAG 视图

图 6.2 – Airflow UI – DAG 视图

DAG 左侧有一个按钮可以启动其调度器。暂时不要点击它。首先,点击 DAG,查看 Airflow 在 DAG 中提供的所有视图。初始视图应该是包含 DAG 信息的摘要(图 6.3)。

图 6.3 – Airflow UI – DAG 网格视图

图 6.3 – Airflow UI – DAG 网格视图

点击 Graph 按钮,查看管道的漂亮可视化效果(图 6.4)。

图 6.4 – Airflow UI – DAG 图形视图

图 6.4 – Airflow UI – DAG 图形视图

注意观察 Airflow 如何自动检测任务的依赖关系和并行性。现在让我们启用这个 DAG 的调度器,查看执行结果(图 6.5)。

图 6.5 – Airflow UI – 执行后的 DAG 图形视图

图 6.5 – Airflow UI – 执行后的 DAG 图形视图

当我们启动调度器时,由于调度器设置为 @once,Airflow 会自动开始执行。任务完成后,它会将任务标记为成功。点击名为 first_task 的任务,然后点击 日志 查看输出(图 6.6)。

图 6.6 – Airflow UI – first_task 输出

图 6.6 – Airflow UI – first_task 输出

请注意,我们编程的输出显示在日志中——“于是,它开始了!”。你还可以检查其他任务的日志,确保一切按预期进行。Airflow 中的另一个重要视图是甘特图。点击页面顶部的甘特图,可以查看每个任务花费的时间(图 6.7)。这是检查执行瓶颈和优化可能性的一个好工具。

图 6.7 – Airflow UI – 甘特图视图

图 6.7 – Airflow UI – 甘特图视图

恭喜!你刚刚创建了你的第一个 Airflow DAG!接下来,让我们看看如何将 Airflow 与其他工具集成,并用它来编排一个更复杂的工作流。

Airflow 与其他工具的集成

我们将使用上一节开发的 DAG 代码,并用一些不同的任务进行重建。我们的 DAG 将下载 Titanic 数据,将其写入 PostgreSQL 表,并将其作为 CSV 文件写入 Amazon S3。此外,我们还将直接通过 Airflow 在 Postgres 上创建一个包含简单分析的视图:

  1. dags文件夹中创建一个新的 Python 文件,命名为postgres_aws_dag.py。代码的第一部分将定义所需的模块。请注意,这次我们导入了PostgresOperator类来与该数据库交互,以及Variable类。这将帮助我们在 Airflow 中管理秘密和参数。我们还创建了一个 SQLAlchemy 引擎来连接到本地 Postgres 数据库,并创建了一个 S3 客户端,允许将文件写入 S3:

    from airflow.decorators import task, dag
    from airflow.models import Variable
    from airflow.providers.postgres.operators.postgres import PostgresOperator
    from datetime import datetime
    import requests
    import pandas as pd
    from sqlalchemy import create_engine
    import boto3
    engine = create_engine('postgresql://postgres:postgres@postgres:5432/postgres')
    aws_access_key_id = Variable.get('aws_access_key_id')
    aws_secret_access_key = Variable.get('aws_secret_access_key')
    s3_client = boto3.client(
        's3',
        aws_access_key_id=aws_access_key_id,
        aws_secret_access_key=aws_secret_access_key
    )
    default_args = {
        'owner': 'Ney',
        'start_date': datetime(2024, 2, 12)
    }
    
  2. 现在,让我们开始开发我们的 DAG。首先,定义四个任务——一个用于下载数据,第二个将其写入 Postgres 表。第三个任务将在 Postgres 中创建一个带有分组汇总的视图,最后一个任务将 CSV 文件上传到 S3。这最后一个任务是一个良好实践的示例,任务将数据处理发送到 Airflow 外部运行:

    @dag(
        default_args=default_args,
        schedule_interval="@once",
        description="Insert Data into PostgreSQL and AWS",
        catchup=False,
        tags=['postgres', 'aws']
    )
    def postgres_aws_dag():
        @task
        def download_data():
          destination = "/tmp/titanic.csv"
          response = requests.get(
    "https://raw.githubusercontent.com/neylsoncrepalde/titanic_data_with_semicolon/main/titanic.csv",
    stream=True
        )
          with open(destination, mode="wb") as file:
            file.write(response.content)
          return destination
        @task
        def write_to_postgres(source):
          df = pd.read_csv(source, sep=";")
          df.to_sql('titanic', engine, if_exists="replace", chunksize=1000, method='multi')
          create_view = PostgresOperator(
          task_id="create_view",
          postgres_conn_id='postgres',
          sql='''
    CREATE OR REPLACE VIEW titanic_count_survivors AS
    SELECT
    "Sex",
    SUM("Survived") as survivors_count
    FROM titanic
    GROUP BY "Sex"
    """,
        )
        @task
        def upload_to_s3(source):
          s3_client.upload_file(source, ' bdok-<ACCOUNT_NUMBER>
    ', 'titanic.csv')
    

    此时,你应该已经在 S3 中创建了一个名为bdok-<YOUR_ACCOUNT_NUMBER>的桶。在桶名中添加你的账户号是保证其唯一性的一种好方法。

  3. 现在,保存你的文件并查看 Airflow UI。注意 DAG 不可用,Airflow 显示一个错误。展开错误消息,你会看到它抱怨我们在代码中尝试获取的变量(图 6.8)。这些变量还不存在。让我们创建它们。

图 6.8 – Airflow UI – 变量错误

图 6.8 – Airflow UI – 变量错误

  1. 在上方菜单中,点击Admin并选择Variables。现在,我们将创建我们需要的 Airflow 环境变量。复制你的 AWS 密钥和访问密钥 ID,并相应地创建这些变量。创建变量后,你可以看到密钥在 UI 中被隐藏(图 6.9)。Airflow 会自动检测到秘密和敏感凭证,并在 UI 中隐藏它们。

图 6.9 – Airflow UI – 变量已创建

图 6.9 – Airflow UI – 变量已创建

  1. 现在,让我们回到 UI 的主页。DAG 现在显示正确,但我们还没有完成。为了使PostgresOperator任务正确运行,它需要一个 Postgres 连接(记得postgres_conn_id参数吗?)。在上方菜单中,点击Admin,然后点击Connections。如图 6.10所示,添加一个新的连接。

图 6.10 – Airflow UI – 新连接

图 6.10 – Airflow UI – 新连接

  1. 创建连接后,让我们继续开发 DAG。我们已经设置好了任务。现在是时候将它们链接在一起了:

        download = download_data()
        write = write_to_postgres(download)
        write >> create_view
        upload = upload_to_s3(download)
    execution = postgres_aws_dag()
    

    现在,我们准备好了。你还可以查看完整的代码,代码在 GitHub 上可用(github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter06/dags/postgres_aws_dag.py)。

  2. 在 Airflow UI 中查看 DAG 的图表(图 6.11),并注意它是如何自动并行化所有可能的任务——在这种情况下,write_to_postgresupload_to_s3。启动 DAG 的调度器让它运行。DAG 成功运行后,检查 S3 存储桶,以验证文件是否正确上传。然后,选择你喜欢的 SQL 客户端,检查数据是否已正确导入 Postgres。此示例中,我使用的是 DBeaver,但你可以选择任何你喜欢的客户端。

图 6.11 – Airflow UI – 最终 DAG

图 6.11 – Airflow UI – 最终 DAG

  1. 在 DBeaver 中,创建一个新的 PostgreSQL 连接。对于 DBeaver 的连接,我们将使用 localhost 作为 Postgres 的主机(在 Airflow 中,我们需要使用 postgres,因为它运行在 Docker 中的共享网络内)。填写用户名和密码(在此例中为 postgrespostgres),并测试连接。如果一切正常,创建连接,然后让我们在这个数据库上执行一些查询。在 图 6.12 中,我们可以看到数据已正确导入。

图 6.12 – DBeaver – 泰坦尼克号表格

图 6.12 – DBeaver – 泰坦尼克号表格

  1. 现在,让我们检查视图是否正确创建。结果显示在 图 6.13 中。

图 6.13 – DBeaver – 创建的视图

图 6.13 – DBeaver – 创建的视图

就这样!你创建了一个表格和一个视图,并通过 Airflow 上传了数据到 AWS!要停止 Airflow 容器,请返回终端并输入以下命令:

astro dev kill

总结

在本章中,我们介绍了 Apache Airflow 的基础知识——从安装到开发数据管道。你学习了如何利用 Airflow 来协调涉及数据获取、处理和与外部系统集成的复杂工作流。

我们通过 Astro CLI 和 Docker 在本地安装了 Airflow。这提供了一种无需复杂设置即可快速上手的方法。你接触到了 Airflow 的架构和关键组件,如调度器、工作节点和元数据数据库。理解这些组件对于监控和故障排除生产环境中的 Airflow 至关重要。

然后,我们有一个重点部分,介绍了如何构建你的第一个 Airflow DAG。你使用了核心 Airflow 操作符和任务、DAG 装饰器来定义和链式任务。我们讨论了最佳实践,比如保持任务小巧且独立。你还了解了 Airflow 如何处理任务依赖关系——允许并行执行独立任务。这些学习将帮助你开发出可扩展且可靠的 DAG。

随后,我们将 Airflow 与外部工具集成——写入 PostgreSQL,创建视图,并将文件上传到 S3。这展示了 Airflow 在协调涉及不同系统的工作流方面的多功能性。我们还配置了 Airflow 连接和变量,以安全地传递凭证。

到本章结束时,你应该已经掌握了 Airflow 的基础知识,并有了构建数据管道的实践经验。你现在已经具备了开发 DAG、整合其他工具以及应用生产级工作流最佳实践的能力。随着数据团队开始采用 Airflow,这些技能将对构建可靠且可扩展的数据管道至关重要。

在下一章,我们将学习实时数据的核心技术之一——Apache Kafka。

第七章:Apache Kafka 用于实时事件和数据摄取

实时数据和事件流处理是现代数据架构中的关键组成部分。通过利用像 Apache Kafka 这样的系统,组织可以摄取、处理和分析实时数据,从而推动及时的业务决策和行动。

本章将介绍 Kafka 的基本概念和架构,使其成为一个高性能、可靠且可扩展的消息系统。你将学习 Kafka 的发布-订阅消息模型是如何通过主题、分区和代理来工作的。我们将演示 Kafka 的设置和配置,并让你亲身体验如何为主题生产和消费消息。

此外,你将通过实验数据复制和主题分布策略,了解 Kafka 的分布式和容错特性。我们还将介绍 Kafka Connect,用于从外部系统(如数据库)流式摄取数据。你将配置 Kafka Connect,将 SQL 数据库中的变更流式传输到 Kafka 主题。

本章的亮点是将 Kafka 与 Spark 结构化流处理结合,构建实时数据管道。你将通过实现端到端的管道,学习这种高可扩展的流处理方法,这些管道会消费 Kafka 主题数据,使用 Spark 处理数据,并将输出写入另一个 Kafka 主题或外部存储系统。

到本章结束时,你将掌握实际技能,能够设置 Kafka 集群并利用 Kafka 的功能构建强大的实时数据流和处理架构。公司可以通过做出及时的数据驱动决策从中获益,而 Kafka 正是实现这一目标的关键。

在本章中,我们将覆盖以下主要内容:

  • 开始使用 Kafka

  • 探索 Kafka 架构

  • 使用 Kafka Connect 从数据库流式传输数据

  • 使用 Kafka 和 Spark 进行实时数据处理

技术要求

在本章中,我们将使用docker-compose在本地运行 Kafka 集群和 Kafka Connect 集群,而docker-compose是随 Docker 一同提供的,因此无需额外的安装步骤。如果你需要手动安装docker-compose,请参考docs.docker.com/compose/install/

此外,我们将使用Spark进行实时数据处理。安装说明请参考第五章

本章的所有代码都可以在本书的 GitHub 仓库中找到,网址是(github.com/PacktPublishing/Bigdata-on-Kubernetes),位于Chapter07文件夹中。

开始使用 Kafka

Kafka 是一个流行的开源平台,用于构建实时数据管道和流处理应用程序。在本节中,我们将学习如何使用docker-compose在本地运行基本的 Kafka 环境,这样你就可以开始构建 Kafka 生产者和消费者。

docker-compose 是一个帮助定义和运行多容器 Docker 应用的工具。使用 Compose,你可以通过一个 YAML 文件配置应用的服务,然后通过一个命令启动所有服务。这可以避免手动运行和连接容器。为了运行我们的 Kafka 集群,我们将使用 docker-compose 定义一组节点。首先,创建一个名为 multinode 的文件夹(仅为保持代码有序),并创建一个名为 docker-compose.yaml 的新文件。这是 docker-compose 用来设置容器的常规文件(类似于 Dockerfile)。为了提高可读性,我们不会显示整个代码(代码可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter07/multinode 找到),只展示其中的一部分。让我们来看一下:

docker-compose.yaml

---
version: '2'
services:
    zookeeper-1:
      image: confluentinc/cp-zookeeper:7.6.0
      environment:
        ZOOKEEPER_SERVER_ID: 1
        ZOOKEEPER_CLIENT_PORT: 22181
        ZOOKEEPER_TICK_TIME: 2000
        ZOOKEEPER_INIT_LIMIT: 5
        ZOOKEEPER_SYNC_LIMIT: 2
        ZOOKEEPER_SERVERS: localhost:22888:23888;localhost:32888:33888;localhost:42888:43888
    network_mode: host
    extra_hosts:
      - "mynet:127.0.0.1"
    kafka-1:
      image: confluentinc/cp-kafka:7.6.0
      network_mode: host
      depends_on:
        - zookeeper-1
        - zookeeper-2
        - zookeeper-3
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: localhost:22181,localhost:32181,localhost:42181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:19092
    extra_hosts:
      - "mynet:127.0.0.1"

原始的 Docker Compose 文件正在设置一个包含三个 Kafka 经纪人和三个 Zookeeper 节点的 Kafka 集群(更多关于 Kafka 架构的细节将在下一节中讲解)。我们只保留了第一个 Zookeeper 和 Kafka 经纪人的定义,因为其他的都是相同的。在这里,我们使用的是 Confluent Kafka(由 Confluent Inc. 维护的企业版 Kafka)和 Zookeeper 镜像来创建容器。对于 Zookeeper 节点,关键参数如下:

  • ZOOKEEPER_SERVER_ID:集群中每个 Zookeeper 服务器的唯一 ID。

  • ZOOKEEPER_CLIENT_PORT:客户端连接到此 Zookeeper 节点的端口。我们为每个节点使用不同的端口。

  • ZOOKEEPER_TICK_TIME:Zookeeper 用于心跳的基本时间单位。

  • ZOOKEEPER_INIT_LIMIT:Zookeeper 服务器必须连接到领导者的时间。

  • ZOOKEEPER_SYNC_LIMIT:服务器可以与领导者相差多远。

  • ZOOKEEPER_SERVERS:以 address:leaderElectionPort:followerPort 格式列出集群中的所有 Zookeeper 服务器。

对于 Kafka 经纪人,关键参数如下:

  • KAFKA_BROKER_ID:每个 Kafka 经纪人的唯一 ID。

  • KAFKA_ZOOKEEPER_CONNECT:列出 Kafka 应该连接的 Zookeeper 集群。

  • KAFKA_ADVERTISED_LISTENERS:对外连接到此经纪人的广告监听器。我们为每个经纪人使用不同的端口。

容器配置为使用主机网络模式以简化网络配置。依赖关系确保 Kafka 仅在 Zookeeper 准备好后才启动。

这段代码创建了一个完全功能的 Kafka 集群,能够处理单个经纪人或 Zookeeper 的复制和故障。现在,我们将启动这些容器。在终端中,进入 multinode 文件夹并输入以下命令:

docker-compose up –d

这将告诉 docker-compose 启动容器。如果本地未找到必要的镜像,它们将被自动下载。-d 参数使得 docker-compose-d 模式下运行。

要查看某个 Kafka 经纪人的日志,请运行以下命令:

docker logs multinode-kafka-1-1

这里,multinode-kafka-1-1 是我们在 YAML 文件中定义的第一个 Kafka Broker 容器的名称。通过这个命令,您应该能够可视化 Kafka 的日志并验证一切是否正常运行。现在,让我们更详细地了解 Kafka 的架构,并理解它是如何工作的。

探索 Kafka 的架构

Kafka 具有分布式架构,包括经纪人、生产者、消费者、主题、分区和副本。在高层次上,生产者向主题发布消息,经纪人接收这些消息并将它们存储在分区中,消费者订阅主题并处理发布给它们的消息。

Kafka 依赖于一个称为Zookeeper的外部协调服务,帮助管理 Kafka 集群。Zookeeper 帮助进行控制器选举——选择一个经纪人作为集群控制器。控制器负责管理操作,例如将分区分配给经纪人并监视经纪人故障。Zookeeper 还帮助经纪人协调彼此的操作,例如为分区选举领导者。

Kafka 经纪人是 Kafka 集群的主要组件,负责处理生产者和消费者的所有读/写请求。经纪人从生产者接收消息并向消费者公开数据。每个经纪人管理以分区形式存储在本地磁盘上的数据。默认情况下,经纪人将分区均匀分布在它们之间。如果经纪人宕机,Kafka 将自动将这些分区重新分配给其他经纪人。这有助于防止数据丢失并确保高可用性。现在,让我们了解 Kafka 如何在发布-订阅(PubSub)设计中处理消息,以及如何为消息的写入和读取保证可靠性和可伸缩性。

发布-订阅设计

Kafka 依赖于发布-订阅(PubSub)消息模式来实现实时数据流。Kafka 将消息组织成称为主题(topics)的类别。主题充当消息的源或流。生产者将数据写入主题,消费者从主题读取数据。例如,“页面访问”主题将记录每次访问网页的情况。主题始终是多生产者和多订阅者的,可以有零到多个生产者向主题写入消息,以及零到多个消费者从主题读取消息。这有助于在应用程序之间协调数据流。

主题被分成分区以实现可伸缩性。每个分区充当一个有序、不可变的消息序列,可以不断追加消息。通过将主题分区成多个分区,Kafka 可以通过让多个消费者并行地从多个分区读取主题来扩展主题消费。分区允许 Kafka 水平分布负载到经纪人之间,并允许并行处理。数据在每个分区内保持生产顺序。

Kafka 通过复制分区到可配置数量的代理上来提供冗余和容错性。一个分区将有一个代理被指定为“领导者”,并且有零个或多个代理充当“跟随者”。所有的读写操作都由领导者处理,跟随者通过拥有与领导者数据完全相同的副本来被动地复制领导者的数据。如果领导者失败,某个跟随者将自动成为新的领导者。

在各个代理(brokers)之间拥有副本可以确保容错性,因为即使某些代理出现故障,数据仍然可以被消费。副本因子控制副本的数量。例如,副本因子为三意味着将有两个跟随者复制一个领导者分区。常见的生产环境设置通常至少有三个代理,副本因子为二或三。

消费者用消费者组名称来标识自己,并且发布到主题中的每一条记录只会发送给组中的一个消费者。如果一个组中有多个消费者,Kafka 会在消费者之间负载均衡消息。Kafka 保证在一个分区内将消息有序、至少一次地传递给一个消费者。消费者组允许你扩展消费者的数量,同时仍然提供消息的顺序保证。

生产者将记录发布到主题的分区中。如果只有一个分区,所有消息都会发送到该分区。如果有多个分区,生产者可以选择随机分配消息到各个分区,或者通过使用相同的分区来确保消息的顺序。这个顺序保证只适用于单个分区内,而不适用于跨分区的消息。

生产者为了提高效率和持久性,将消息批量处理。消息在发送到代理之前会在本地缓冲并进行压缩。这样的批处理提供了更好的效率和吞吐量。生产者可以选择等待直到批次满,或根据时间或消息大小的阈值来刷新。生产者还通过在确认写入之前确保所有同步副本都已确认来进行数据复制。生产者可以选择不同的确认保证,从一旦领导者写入记录就提交,或等到所有跟随者都已复制后再提交。

消费者通过订阅 Kafka 主题来读取记录。消费者实例可以位于不同的进程或服务器上。消费者通过定期发送数据请求从代理中拉取数据。消费者会跟踪它们在每个分区中的位置(“偏移量”),以便在发生故障时从正确的位置开始读取。消费者通常会定期提交偏移量。偏移量还可以让消费者在需要时倒带或跳过一些记录。到底这些偏移量是如何工作的?让我们更深入地了解一下。

Kafka 将记录流存储在称为主题的类别中。每个主题下的记录被组织成分区,这样就可以进行并行处理和扩展性。每个分区中的记录都有一个称为 偏移量递增 ID 编号,它唯一标识该分区内的记录。这个偏移量反映了分区内记录的顺序。例如,偏移量为三意味着它是第三条记录。

当 Kafka 消费者从分区读取记录时,它会跟踪已读取的最后一条记录的偏移量。这使得消费者只会读取尚未处理的新记录。如果消费者断开连接并稍后重新连接,它将从最后提交的偏移量处重新开始读取。偏移量提交日志存储在一个名为 __consumer_offsets 的 Kafka 主题中。这样可以保证持久性,并允许消费者在故障发生时透明地从中断处恢复。

偏移量使得多个消费者可以从同一分区读取数据,同时确保每个消费者只处理每条记录一次。消费者可以按自己的速度读取数据,彼此之间不会互相干扰。这是 Kafka 可扩展性的关键设计特性。当这些特性一起使用时,Kafka 可以实现 精确一次语义。让我们更详细地了解这个概念。

Kafka 如何实现精确一次语义

在处理数据流时,考虑数据传输保障时有三种相关的语义:

  • 至少一次语义:在这种情况下,数据流中的每条记录保证至少被处理一次,但可能会被处理多次。如果数据源下游发生故障,在处理确认之前,系统将重新发送未确认的数据,导致重复处理。

  • 最多一次语义:在这种情况下,每条记录要么被处理一次,要么完全不处理。这可以防止重复处理,但意味着在发生故障时,一些记录可能会完全丢失。

  • 精确一次语义:此案例结合了其他两种语义的保证,确保每条记录只被处理一次且仅一次。由于需要在存储和处理之间进行协调,以确保在重试过程中不会引入重复项,这在实践中非常难以实现。

Kafka 提供了一种方法,通过架构设计和与流处理系统的集成,实现事件处理的精确一次语义。Kafka 主题被划分为多个分区,这使得数据可以通过将负载分散到不同的代理上来实现并行处理。具有相同键的事件会进入同一个分区,从而保证了处理顺序的保证。Kafka 为每个分区分配一个顺序 ID,称为偏移量,它唯一标识该分区内的每个事件。

消费者通过存储最后处理事件的偏移量来跟踪每个分区的位置。如果消费者失败并重新启动,它将从最后提交的偏移量恢复,确保事件不会丢失或被处理两次。

通过将偏移量追踪与流处理器通过 Kafka 的 API 紧密集成,Kafka 的基础设施为构建精确一次的实时数据管道提供了支撑。

图 7.1展示了 Kafka 架构的可视化表示:

图 7.1- Kafka 架构

图 7.1- Kafka 架构

接下来,我们将做一个简单的练习,以开始并查看 Kafka 的实际操作。

第一个生产者和消费者

在使用docker-compose设置 Kafka 后,我们需要创建一个主题来保存我们的事件。我们可以在容器外部执行此操作,也可以进入容器并从内部运行命令。为了本次练习,我们将访问容器并从内部运行命令,目的是为了教学。稍后在本书中,我们将研究另一种方法,这在 Kafka 运行在 Kubernetes 上时尤其有用。我们开始吧:

  1. 首先,检查所有容器是否都已启动并正在运行。在终端中,运行以下命令:

    docker-compose ps
    

    你应该看到一个输出,指定了容器的名称、镜像、命令等信息。一切似乎都在正常运行。请注意第一个 Kafka 代理容器的名称。

  2. 我们需要它来从容器内运行 Kafka 的命令。要进入容器,运行以下命令:

    CONTAINER_NAME=multinode-kafka-1-1
    docker exec -it $CONTAINER_NAME bash
    

    在这里,我们正在创建一个环境变量,存储第一个容器的名称(在我的例子中是multinode_kafka-1_1),并使用docker exec命令与-it参数一起运行。

  3. 现在,我们进入了容器。让我们声明三个有助于管理 Kafka 的环境变量:

    BOOTSTRAP_SERVER=localhost:19092
    TOPIC=mytopic
    GROUP=mygroup
    
  4. 现在,我们将使用 Kafka 命令行工具通过kafka-topics --create命令创建一个主题。运行以下代码:

    kafka-topics --create --bootstrap-server $BOOTSTRAP_SERVER --replication-factor 3 --partitions 3 --topic $TOPIC
    

    这将创建一个名为mytopic的主题,复制因子为3(三个副本),并且有3个分区(注意,最大分区数是你拥有的代理数量)。

  5. 虽然我们在终端中收到了确认消息,但列出集群中的所有主题还是很有帮助的:

    kafka-topics --list --bootstrap-server $BOOTSTRAP_SERVER
    

    你应该在屏幕上看到mytopic作为输出。

  6. 接下来,让我们获取一些关于我们主题的信息:

    kafka-topics --bootstrap-server $BOOTSTRAP_SERVER --describe --topic $TOPIC
    

    这会产生以下输出(已格式化以便更好地可视化):

    Topic: mytopic
    TopicId: UFt3FOyVRZyYU7TYT1TrsQ
    PartitionCount: 3
    ReplicationFactor: 3
    Configs:
    Topic:mytopic Partition:0 Leader:2 Replicas:2,3,1
    Topic:mytopic Partition:1 Leader:3 Replicas:3,1,2
    Topic:mytopic Partition:2 Leader:1 Replicas:1,2,3
    

    这个主题结构被分配到所有三个代理,并且每个分区在所有其他代理中都有副本,这正是我们在图 7.1中看到的。

  7. 现在,让我们构建一个简单的生产者,并开始向这个主题发送一些消息。在终端中,输入以下命令:

    kafka-console-producer --broker-list $BOOTSTRAP_SERVER --topic $TOPIC
    

    这会启动一个简单的控制台生产者。

  8. 现在,在控制台中输入一些消息,它们将被发送到该主题:

    abc
    def
    ghi
    jkl
    mno
    pqr
    stu
    vwx
    yza
    

    你可以输入任何你想要的内容。

  9. 现在,打开一个不同的终端(最好将它放在第一个运行控制台生产者的终端旁边)。我们必须以与第一个终端相同的方式登录到容器:

    CONTAINER_NAME=multinode-kafka-1-1
    docker exec -it $CONTAINER_NAME bash
    
  10. 然后,创建相同的必要环境变量:

    BOOTSTRAP_SERVER=localhost:19092
    TOPIC=mytopic
    
  11. 现在,我们将启动一个简单的控制台消费者。我们将指示该消费者从头开始读取主题中的所有消息(仅限此练习——不建议在生产环境中的主题上使用此方法,特别是当数据量非常大时)。在第二个终端中,运行以下命令:

    kafka-console-consumer --bootstrap-server $BOOTSTRAP_SERVER --topic $TOPIC --from-beginning
    

你应该能在屏幕上看到所有输入的消息。注意它们的顺序不同,因为 Kafka 只会在分区内保持消息的顺序。

在跨分区时,无法保持顺序(除非消息内部包含日期和时间信息)。按 Ctrl + C 停止消费者。你也可以在生产者终端按 Ctrl + C 停止它。然后,在两个终端中输入 exit,退出容器,并通过运行以下命令停止并杀死所有容器:

docker-compose down

你可以通过以下命令检查所有容器是否已成功删除:

docker ps -a

现在,让我们尝试做一些不同的事情。使用 Kafka 最常见的一个场景是实时迁移数据库表中的数据。让我们看看如何通过 Kafka Connect 简单地实现这一点。

从数据库流式传输数据到 Kafka Connect

在本节中,我们将使用 Kafka Connect 实时读取在 Postgres 表中生成的所有数据。首先,需要构建一个可以连接到 Postgres 的 Kafka Connect 自定义镜像。请按照以下步骤操作:

  1. 我们为这个新练习创建一个不同的文件夹。首先,创建一个名为 connect 的文件夹,并在其中再创建一个名为 kafka-connect-custom-image 的文件夹。在自定义镜像文件夹内,我们将创建一个新的 Dockerfile,内容如下:

    FROM confluentinc/cp-kafka-connect-base:7.6.0
    RUN confluent-hub install --no-prompt confluentinc/kafka-connect-jdbc:10.7.5 \
    && confluent-hub install --no-prompt confluentinc/kafka-connect-s3:10.5.8
    

    这个 Docker 文件基于 Confluent Kafka Connect 镜像,并安装了两个连接器——一个 JDBC 源/接收连接器和一个用于 Amazon S3 的接收连接器。前者用于连接数据库,而后者则非常方便用于将事件传送到 S3。

  2. 使用以下命令构建你的镜像:

    cd connect
    cd kafka-connect-custom-image
    docker build -t connect-custom:1.0.0 .
    cd ..
    

    现在,在 connect 文件夹中,你应该有一个 .env_kafka_connect 文件,用于存储你的 AWS 凭证。请记住,凭证绝不应硬编码在任何配置文件或代码中。你的 .env_kafka_connect 文件应如下所示:

    AWS_DEFAULT_REGION='us-east-1'
    AWS_ACCESS_KEY_ID='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    AWS_SECRET_ACCESS_KEY='xxxxxxxxxxxx'
    
  3. 将其保存在 connect 文件夹中。然后,创建一个新的 docker-compose.yaml 文件。该文件的内容可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter07/connect/docker-compose.yaml

    这个 Docker Compose 文件为 Kafka 和 Kafka Connect 设置了一个环境,并包含一个 Postgres 数据库实例。它定义了以下服务:

    • zookeeper:此容器运行一个 Zookeeper 实例,Kafka 依赖于它进行节点间的协调。它设置了一些配置,如端口、tick 时间和客户端端口。

    • broker:此容器运行一个 Kafka broker,依赖于 Zookeeper 服务(在所有 Zookeeper 启动之前,broker 无法存在)。它配置了如 broker ID、连接到的 Zookeeper 实例、用于外部连接的端口 909229092 的监听器、Kafka 所需的内部主题的复制设置,以及一些性能调优设置。

    • schema-registry:此容器运行 Confluent Schema Registry,它允许我们存储主题的模式。它依赖于 Kafka broker,并设置 Kafka 集群的 URL 以及监听 API 请求的端口。

    • connect:此容器运行我们定制的 Confluent Kafka Connect 镜像。它依赖于 Kafka broker 和 Schema Registry,并设置了启动服务器、组 ID、用于存储连接器配置、偏移量和状态的内部主题、用于序列化的键值转换器、Schema Registry 集成以及查找更多连接器的插件路径。

    • rest-proxy:此容器运行 Confluent REST 代理,它提供了一个 Kafka 的 REST 接口。它设置了 Kafka broker 的连接信息和 Schema Registry。

    • postgres:此容器运行一个 Postgres 数据库实例,并暴露在端口 5432 上,设置了一些基本凭据。请注意,我们在代码中以明文保存数据库密码。在生产环境中绝不应这样做,因为这是一个安全漏洞。我们这样定义密码仅限于本地测试。

    还定义了一个名为 proxynet 的自定义网络,所有这些服务都会加入该网络。这允许通过主机名进行服务间通信,而无需将所有服务暴露到主机机器的网络中。

  4. 要启动这些容器,请运行以下命令:

    docker-compose up -d
    

    所有容器应在几分钟内启动。

  5. 现在,我们将持续向我们的 Postgres 数据库插入一些模拟数据。为此,创建一个新的 Python 文件,命名为 make_fake_data.py。代码可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter07/connect/simulations 文件夹中找到。此代码为客户生成假数据(如姓名、地址、职业和电子邮件),并将其插入到数据库中。要使其正常工作,您应安装 fakerpandaspsycopg2-binarysqlalchemy 库。在运行代码之前,请确保通过 pip install 安装它们。本书的 GitHub 仓库中提供了一个 requirements.txt 文件和代码。

  6. 现在,在终端中运行模拟,输入以下命令:

    python make_fake_data.py
    

    这将打印出模拟参数(生成间隔、样本大小和连接字符串)到屏幕,并开始打印模拟数据。经过几次模拟后,可以通过按 Ctrl + C 停止它。然后,使用你喜欢的 SQL 客户端(例如 DBeaver)检查数据是否已正确导入数据库。运行简单的 SQL 语句(select * from customers)查看数据是否在 SQL 客户端中正确显示。

  7. 现在,我们将注册一个源 JDBC 连接器来从 Postgres 拉取数据。此连接器将作为 Kafka Connect 进程运行,建立一个到源数据库的 JDBC 连接。它使用该连接执行 SQL 查询,从特定表中选择数据。连接器将结果集转换为 JSON 文档,并将其发布到配置的 Kafka 主题。每个表都有一个专门为其创建的主题。提取数据的查询可以是简单的 SELECT 语句,也可以是基于时间戳或数字列的增量查询。这使我们能够捕捉到新增或更新的行。

  8. 首先,我们将定义一个配置文件,以便在 Kafka Connect 上部署连接器。创建一个名为 connectors 的文件夹,并创建一个名为 connect_jdbc_pg_json.config 的新文件。配置代码如下所示:

    connect_jdbc_pg_json.config

    {
        "name": "pg-connector-json",
        "config": {
            "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector",
            "value.converter": "org.apache.kafka.connect.json.JsonConverter",
            "value.converter.schemas.enable": "true",
            "tasks.max": 1,
            "connection.url": "jdbc:postgresql://postgres:5432/postgres",
            «connection.user»: «postgres»,
            «connection.password»: «postgres»,
            «mode»: «timestamp»,
            "timestamp.column.name": "dt_update",
            "table.whitelist": "public.customers",
            "topic.prefix": "json-",
            "validate.non.null": "false",
            "poll.interval.ms": 500
        }
    }
    

    此配置创建了一个 Kafka 连接器,将基于行的时间戳变化将 customers 表的行同步到 JSON 格式的 Kafka 主题。我们来更详细地了解所使用的参数:

    • name: 为连接器指定一个名称,以便于管理。

    • connector.class: 指定使用 Confluent 提供的 JDBC 连接器类。

    • value.converter: 指定数据将在 Kafka 中转换为 JSON 格式。

    • value.converter.schemas.enable: 启用将架构与 JSON 数据一起存储。

    • tasks.max: 限制为一个任务。此参数可以根据生产环境的扩展性需求,在主题的分区数不同的情况下增加。

    • connection.url: 连接到本地的 PostgreSQL 数据库,端口为 5432

    • connection.user/.password: PostgreSQL 凭证(在本示例中为明文,凭证绝不应硬编码)。

    • mode: 指定使用时间戳列来检测新增/更改的行。你也可以使用 id 列。

    • timestamp.column.name: 查看 dt_update 列。

    • table.whitelist: 指定同步 customers 表。

    • topic.prefix: 输出的主题将以 json- 为前缀。

    • validate.non.null: 允许同步包含 null 值的行。

    • poll.interval.ms: 每 500 毫秒检查一次新数据。

  9. 现在,我们将创建一个 Kafka 主题来存储来自 Postgres 表的数据。在终端中,输入以下内容:

    docker-compose exec broker kafka-topics --create --bootstrap-server localhost:9092 --partitions 2 --replication-factor 1 --topic json-customers
    

    请注意,我们正在使用 docker-compose API 在容器内执行命令。命令的第一部分(docker-compose exec broker)告诉 Docker 我们希望在 docker-compose.yaml 文件中定义的 broker 服务中执行某些操作。其余命令将在 broker 内部执行。我们正在创建一个名为 json-customers 的主题,具有两个分区和一个副本因子(每个分区一个副本)。你应该在终端中看到主题创建的确认消息。

  10. 接下来,我们将使用简单的 API 调用来注册连接器到 Kafka Connect。我们将使用 curl 库来实现。请在终端中输入以下命令:

    curl -X POST -H "Content-Type: application/json" --data @connectors/connect_jdbc_pg_json.config localhost:8083/connectors
    curl localhost:8083/connectors
    

    连接器的名称应在终端中打印出来。

  11. 现在,快速检查 Connect 实例的日志:

    docker logs connect
    

    向上滚动几行,你应该会看到连接器注册的输出。

  12. 现在,让我们尝试一个简单的控制台消费者,只是验证消息是否已经被迁移到主题中:

    docker exec -it broker bash
    kafka-console-consumer --bootstrap-server localhost:9092 --topic json-customers --from-beginning
    

    你应该在屏幕上看到以 JSON 格式打印的消息。按 Ctrl + C 停止消费者,然后输入 exit 退出容器。

  13. 现在,我们将配置一个 sink 连接器,将这些消息传输到 Amazon S3。首先,去 AWS 创建一个新的 S3 存储桶。S3 存储桶的名称必须在所有 AWS 中唯一。因此,我建议将其设置为账户名称作为后缀(例如,kafka-messages-xxxxxxxx)。

    connectors 文件夹中,创建一个名为 connect_s3_sink.config 的新文件:

    connect_s3_sink.config

    {
        "name": "customers-s3-sink",
        "config": {
            "connector.class": "io.confluent.connect.s3.S3SinkConnector",
            "format.class": "io.confluent.connect.s3.format.json.JsonFormat",
            "keys.format.class": "io.confluent.connect.s3.format.json.JsonFormat",
            "key.converter": "org.apache.kafka.connect.json.JsonConverter",
            "value.converter": "org.apache.kafka.connect.json.JsonConverter",
            "key.converter.schemas.enable": false,
            "value.converter.schemas.enable": false,
            "flush.size": 1,
            "schema.compatibility": "FULL",
            "s3.bucket.name": "<YOUR_BUCKET_NAME>",
            "s3.region": "us-east-1",
            "s3.object.tagging": true,
            "s3.ssea.name": "AES256",
            "topics.dir": "raw-data/kafka",
            "storage.class": "io.confluent.connect.s3.storage.S3Storage",
            "tasks.max": 1,
            "topics": "json-customers"
        }
    }
    

    让我们来熟悉一下这个连接器的参数:

    • connector.class:指定要使用的连接器类。在这种情况下,它是 Confluent S3 sink 连接器。

    • format.class:指定写入 S3 时使用的格式。这里,我们使用 JsonFormat,使数据以 JSON 格式存储。

    • key.convertervalue.converter:分别指定用于将键和值序列化为 JSON 的转换器类。

    • key.converter.schemas.enablevalue.converter.schemas.enable:禁用键和值的模式验证。

    • flush.size:指定连接器在执行刷新操作之前应等待的记录数。这里,这个参数设置为 1。然而,在生产环境中,当消息吞吐量较大时,最好将此值设置得更高,以便更多的消息在一个文件中一起传输到 S3。

    • schema.compatibility:指定要使用的模式兼容性规则。这里,FULL 表示模式必须完全兼容。

    • s3.bucket.name:要写入数据的 S3 存储桶的名称。

    • s3.region:S3 存储桶所在的 AWS 区域。

    • s3.object.tagging:启用 S3 对象标签。

    • s3.ssea.name:要使用的服务器端加密算法(在这种情况下是 AES256,即 S3 管理的加密)。

    • topics.dir:指定在 S3 存储桶中写入数据的目录。

    • storage.class:指定底层存储类别。

    • tasks.max:此连接器的最大任务数。对于一个接收器,这通常应为1

    • topics:以逗号分隔的主题列表,用于获取数据并写入 S3。

  14. 现在,我们可以注册接收器连接器。在你的终端中输入以下命令:

    curl -X POST -H "Content-Type: application/json" --data @connectors/connect_s3_sink.config localhost:8083/connectors
    

使用 docker logs connect 查看日志,以验证连接器是否已正确注册,并且在部署过程中没有错误。

就这样!你可以检查 AWS 上的 S3 桶,查看 JSON 文件的传输情况。如果你愿意,可以再次运行 make_fake_data.py 模拟器,查看更多消息传递到 S3。

现在你已经知道如何设置实时消息传递管道,让我们通过 Apache Spark 向其中加入一些实时处理功能。

使用 Kafka 和 Spark 进行实时数据处理

实时数据管道的一个非常重要的部分是实时处理。随着来自各种来源(如用户活动日志、物联网传感器等)的数据不断生成,我们需要能够在这些数据流上进行实时转换。

Apache Spark 的结构化流模块提供了一个高层 API,用于处理实时数据流。它建立在 Spark SQL 的基础上,使用类似 SQL 的操作提供丰富的流处理。Spark 结构化流通过微批处理模型来处理数据流。在这个模型中,流数据会被接收并收集成小批次,通常在毫秒级别内非常快速地处理。这提供了低延迟处理,同时保留了批处理的可扩展性。

我们将从使用 Kafka 启动的实时管道中提取数据,并在其上构建实时处理。我们将使用 Spark 结构化流模块来实现这一点。创建一个名为 processing 的新文件夹,并在其中创建一个名为 consume_from_kafka.py 的文件。处理数据并聚合结果的 Spark 代码已提供在此。

该代码也可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter07/connect/processing/consume_from_kafka.py。这个 Spark 结构化流应用程序正在从 json-customers Kafka 主题读取数据,转换 JSON 数据,并在计算聚合后将结果打印到控制台:

consume_from_kafka.py

from pyspark.sql import SparkSession
from pyspark.sql import functions as f
from pyspark.sql.types import *
spark = (
    SparkSession.builder
    .config("spark.jars.packages", "org.apache.spark:spark-sql-kafka-0-10_2.12:3.1.2")
    .appName("ConsumeFromKafka")
    .getOrCreate()
)
spark.sparkContext.setLogLevel('ERROR')
df = (
    spark.readStream
    .format('kafka')
    .option("kafka.bootstrap.servers", "localhost:9092")
    .option("subscribe", "json-customers")
    .option("startingOffsets", "earliest")
    .load()
)

首先,创建并配置 SparkSession 来使用 Kafka 连接器包。设置错误日志以减少噪声,并方便在终端中可视化输出。接下来,使用 kafka 源从 json-customers 主题读取数据,创建一个 DataFrame。它连接到本地 Kafka,开始从最早的偏移量读取数据,并将每条消息的负载表示为字符串:

schema1 = StructType([
    StructField("schema", StringType(), False),
    StructField("payload", StringType(), False)
])
schema2 = StructType([
    StructField("name", StringType(), False),
    StructField("gender", StringType(), False),
    StructField("phone", StringType(), False),
    StructField("email", StringType(), False),
    StructField("photo", StringType(), False),
    StructField("birthdate", StringType(), False),
    StructField("profession", StringType(), False),
    StructField("dt_update", LongType(), False)
])
o = df.selectExpr("CAST(value AS STRING)")
o2 = o.select(f.from_json(f.col("value"), schema1).alias("data")).selectExpr("data.payload")
o2 = o2.selectExpr("CAST(payload AS STRING)")
newdf = o2.select(f.from_json(f.col("payload"), schema2).alias("data")).selectExpr("data.*")

这第二个代码块定义了两个模式——schema1捕获了 Kafka 有效载荷中预期的嵌套 JSON 结构,具有模式字段和有效载荷字段。另一方面,schema2定义了包含在有效载荷字段中的实际客户数据模式。

从初始 DataFrame 中提取代表原始 Kafka 消息有效载荷的值字符串字段。使用定义的schema1将此字符串有效载荷解析为 JSON,仅提取有效载荷字段。然后使用schema2再次解析有效载荷字符串,以提取实际的客户数据字段到名为newdf的新 DataFrame 中:

query = (
    newdf
    .withColumn("dt_birthdate", f.col("birthdate"))
    .withColumn("today", f.to_date(f.current_timestamp()))
    .withColumn("age", f.round(
f.datediff(f.col("today"), f.col("dt_birthdate"))/365.25, 0)
    )
    .groupBy("gender")
    .agg(
      f.count(f.lit(1)).alias("count"),
      f.first("dt_birthdate").alias("first_birthdate"),
      f.first("today").alias("first_now"),
      f.round(f.avg("age"), 2).alias("avg_age")
    )
)

现在,转换发生了——birthdate字符串被转换为date,获取当前日期,并使用datediff计算年龄。按性别聚合数据以计算计数、数据中最早的出生日期、当前日期和平均年龄:

(
    query
    .writeStream
    .format("console")
    .outputMode("complete")
    .start()
    .awaitTermination()
)

最后,聚合的 DataFrame 以追加输出模式写入控制台使用 Structured Streaming。此查询将持续运行,直到通过运行Ctrl + C终止。

要运行查询,在终端中输入以下命令:

spark-submit --packages org.apache.spark:spark-sql-kafka-0-10_2.12:3.1.2 processing/consume_from_kafka.py

你应该在终端中看到聚合的数据。打开另一个终端并通过运行以下命令来运行更多模拟:

python simulations/make_fake_data.py

当新的模拟数据生成并被注入到 Postgres 中时,Kafka 连接器将自动将其拉取到json-customers主题,此时 Spark 将拉取这些消息,实时计算聚合结果并打印输出。一段时间后,你可以按下Ctrl + C来停止模拟,然后再次按下来停止 Spark 流查询。

恭喜!你使用 Kafka 和 Spark 运行了一个实时数据处理管道!记得使用docker-compose down清理创建的资源。

总结

在本章中,我们介绍了 Apache Kafka 背后的基本概念和架构——这是一个用于构建实时数据管道和流式应用程序的流行开源平台。

你了解了 Kafka 如何通过其主题和代理架构提供分布式、分区、复制和容错的 PubSub 消息传递。通过实际示例,你获得了使用 Docker 设置本地 Kafka 集群、创建主题以及生成和消费消息的实际经验。你理解了偏移量和消费者组,这些使得从主题进行容错和并行消费成为可能。

我们介绍了 Kafka Connect,它允许我们在 Kafka 和外部系统(如数据库)之间流动数据。你实现了一个源连接器,用于将 PostgreSQL 数据库中的更改摄入到 Kafka 主题中。我们还设置了一个接收器连接器,以实时方式将 Kafka 消息传递到 AWS S3 中的对象存储。

亮点是使用 Kafka 和 Spark Structured Streaming 构建端到端的流数据管道。您学到了如何通过流数据上的微批处理实现低延迟,同时保持可扩展性。提供的示例展示了如何从 Kafka 中消费消息,使用 Spark 进行转换,并实时聚合结果。

通过这些实践练习,您获得了使用 Kafka 架构和功能的实际经验,以构建强大且可扩展的流数据管道和应用程序。企业可以通过利用 Kafka 来满足其实时数据处理需求,从而推动及时的洞察和行动,获得巨大收益。

在下一章中,我们将最终将我们迄今为止所学的所有技术引入 Kubernetes。您将学习如何在 Kubernetes 中部署 Airflow、Spark 和 Kafka,并使它们准备好构建一个完全集成的数据管道。

第三部分:将一切连接起来

在这一部分中,您将学习如何在 Kubernetes 上部署和编排前几章中介绍的大数据工具和技术。您将构建脚本来在 Kubernetes 集群上部署 Apache Spark、Apache Airflow 和 Apache Kafka,使它们能够分别执行数据处理作业、编排数据管道并处理实时数据摄取。此外,您还将探索数据消费层、数据湖引擎(如 Trino)以及使用 Elasticsearch 和 Kibana 进行的实时数据可视化,所有这些都将在 Kubernetes 上部署。最后,您将通过在 Kubernetes 集群上构建和部署两个完整的数据管道来将一切连接起来,一个用于批处理,另一个用于实时处理。本部分还涵盖了在 Kubernetes 上部署生成式 AI 应用程序,并为您在 Kubernetes 和大数据之旅中下一步的行动提供指导。

本部分包含以下章节:

  • 第八章在 Kubernetes 上部署大数据栈

  • 第九章数据消费层

  • 第十章在 Kubernetes 上构建大数据管道

  • 第十一章Kubernetes 上的生成式 AI

  • 第十二章从这里开始

第八章:在 Kubernetes 上部署大数据堆栈

在本章中,我们将涵盖在 Kubernetes 上部署关键的大数据技术 —— Spark、Airflow 和 Kafka。随着容器编排和管理变得对于高效运行数据工作负载至关重要,Kubernetes 已成为事实上的标准。在本章结束时,你将能够成功在 Kubernetes 上部署和管理大数据堆栈,构建强大的数据管道和应用程序。

我们将首先使用 Spark 操作员在 Kubernetes 上部署 Apache Spark。你将学习如何配置和监控在 Kubernetes 集群上运行的 Spark 应用程序。能够在 Kubernetes 上运行 Spark 工作负载带来了重要的好处,如动态扩展、版本控制和统一的资源管理。

接下来,我们将在 Kubernetes 上部署 Apache Airflow。你将配置 Kubernetes 上的 Airflow,将其日志链接到 S3,以便于调试和监控,并设置它来编排使用 Spark 等工具构建的数据管道。在 Kubernetes 上运行 Airflow 可以提高可靠性、扩展性和资源利用率。

最后,我们将在 Kubernetes 上部署 Apache Kafka,这对流数据管道至关重要。在 Kubernetes 上运行 Kafka 简化了操作、扩展和集群管理。

在本章结束时,你将拥有在 Kubernetes 上部署和管理大数据堆栈的实践经验。这将使你能够利用 Kubernetes 作为容器编排平台,构建强大、可靠的数据应用程序。

在本章中,我们将涵盖以下主要内容:

  • 在 Kubernetes 上部署 Spark

  • 在 Kubernetes 上部署 Airflow

  • 在 Kubernetes 上部署 Kafka

技术要求

在本章的活动中,你应该拥有一个 AWS 账户,并已安装 kubectleksctlhelm。关于如何设置 AWS 账户以及安装 kubectleksctl 的说明,请参考 第一章。有关 helm 安装的说明,请访问 helm.sh/docs/intro/install/

我们还将使用 Titanic 数据集进行练习。你可以在 github.com/neylsoncrepalde/titanic_data_with_semicolon 找到我们将使用的版本。

本章中的所有代码都可以在 GitHub 仓库 github.com/PacktPublishing/Bigdata-on-KubernetesChapter08 文件夹中找到。

在 Kubernetes 上部署 Spark

为了帮助我们在 Kubernetes 上部署资源,我们将使用Helm。Helm 是 Kubernetes 的包管理器,帮助安装应用程序和服务。Helm 使用名为Charts的模板,将安装配置、默认设置、依赖关系等打包成一个易于部署的包。

另一方面,我们有操作符。操作符是自定义控制器,扩展了 Kubernetes API,用于管理应用程序及其组件。它们提供了一种声明性方式来创建、配置和管理 Kubernetes 上的复杂状态应用程序。

使用操作符的一些关键优势包括以下几点:

  • 简化的应用程序部署和生命周期管理:操作符抽象了底层细节,为应用程序部署提供了高层次的抽象,而无需了解 Kubernetes 的复杂性。

  • 与监控工具的集成:操作符暴露自定义指标和日志,支持与 Prometheus 和 Grafana 等监控堆栈的集成。

  • Kubernetes 原生:操作符利用 Kubernetes 的可扩展性,并专门为 Kubernetes 编写,使其能够保持云无关性。

操作符通过创建自定义资源定义CRDs)和控制器来扩展 Kubernetes。CRD 允许你在 Kubernetes 中定义一个新的资源类型。例如,SparkOperator 定义了一个 SparkApplication 资源。

操作符接着创建一个控制器,监视这些自定义资源并根据资源的spec执行操作。

例如,当创建 SparkApplication 资源时,SparkOperator 控制器会执行以下操作:

  • 根据规范创建驱动程序和执行器 Pods

  • 挂载存储卷

  • 监控应用程序的状态

  • 执行日志记录和监控

现在,开始吧:

  1. 首先,让我们使用 eksctl 创建一个 AWS EKS 集群:

    eksctl create cluster --managed --alb-ingress-access --node-private-networking --full-ecr-access --name=studycluster --instance-types=m6i.xlarge --region=us-east-1 --nodes-min=3 --nodes-max=4 --nodegroup-name=ng-studycluster
    

    请记住,这行代码需要几分钟才能完成。现在,我们需要进行一些重要的配置,以便允许我们的 Kubernetes 集群代表我们创建卷。为此,我们需要安装 AWS EBS CSI 驱动程序。这对于部署 Spark 应用程序不是必须的,但对于 Airflow 部署来说非常重要。

  2. 首先,我们需要将 IAM OIDC 提供者与 EKS 集群关联,这样 IAM 角色和用户就能通过 Kubernetes API 进行身份验证。为此,请在终端中输入以下命令:

    eksctl utils associate-iam-oidc-provider --region=us-east-1 --cluster=studycluster --approve
    
  3. 接下来,我们将在 kube-system 命名空间中创建一个名为 ebs-csi-controller-sa 的 IAM 服务账户,并附加指定的 IAM 角色和策略。这个服务账户将由 EBS CSI 驱动程序使用:

    eksctl create iamserviceaccount --name ebs-csi-controller-sa --namespace kube-system --cluster studycluster --role-name AmazonEKS_EBS_CSI_DriverRole --role-only --attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy --approve
    
  4. 最后,我们将在集群中启用 EBS CSI 驱动程序,并将其链接到之前创建的服务账户和角色。记得将 <YOUR_ACCOUNT_NUMBER> 替换为真实值:

    eksctl create addon --name aws-ebs-csi-driver --cluster studycluster --service-account-role-arn arn:aws:iam::<YOUR_ACCOUNT_NUMBER>:role/AmazonEKS_EBS_CSI_DriverRole --force
    
  5. 现在,让我们开始实际的 Spark 操作符部署。接下来我们将创建一个命名空间来组织我们的资源:

    kubectl create namespace spark-operator
    
  6. 接下来,我们将使用网上提供的 SparkOperator Helm chart 来部署操作符:

    helm install spark-operator https://github.com/kubeflow/spark-operator/releases/download/spark-operator-chart-1.1.27/spark-operator-1.1.27.tgz --namespace spark-operator --set webhook.enable=true
    
  7. 检查操作符是否正确部署:

    kubectl get pods -n spark-operator
    

    你应该看到类似这样的输出:

    NAME                                READY   STATUS
    spark-operator-74db6fcf98-grhdw     1/1     Running
    spark-operator-webhook-init-mw8gf   0/1     Completed
    
  8. 接下来,我们需要将 AWS 凭证注册为 Kubernetes Secret,以使其可供 Spark 使用。这将允许我们的 Spark 应用程序访问 AWS 中的资源:

    kubectl create secret generic aws-credentials --from-literal=aws_access_key_id=<YOUR_ACCESS_KEY_ID> --from-literal=aws_secret_access_key="<YOUR_SECRET_ACCESS_KEY>" -n spark-operator
    
  9. 现在,到了开发我们的 Spark 代码的时候了。到目前为止,你应该已经将泰坦尼克号数据集存储在 Amazon S3 上。在github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter08/spark/spark_job.py上,你可以找到读取 S3 桶中的泰坦尼克号数据集并将其写入另一个桶的简单代码(这个第二个 S3 桶必须是事先创建的——你可以在 AWS 控制台中创建)。

  10. 将此文件保存为spark_job.py并上传到另一个 S3 桶中。这是 SparkOperator 将要查找代码以运行应用程序的地方。请注意,这段 PySpark 代码与我们之前在第五章中看到的有所不同。在这里,我们将 Spark 配置与 Spark 会话分开设置。我们将详细讨论这些配置:

    • .set("spark.cores.max", "2"):这限制了此 Spark 应用程序最多使用两个核心。这可以防止资源的过度分配。

    • .set("spark.executor.extraJavaOptions", "-Dcom.amazonaws.services.s3.enableV4=true").set("spark.driver.extraJavaOptions", "-Dcom.amazonaws.services.s3.enableV4=true"):这些配置启用了使用签名版本 4 认证对 S3 进行读写操作的支持,这种认证方式更为安全。

    • .set("spark.hadoop.fs.s3a.fast.upload", True):此属性启用了 S3A 连接器的快速上传功能,这可以提高将数据保存到 S3 时的性能。

    • .set("spark.hadoop.fs.s3a.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem"):此配置将 S3 文件系统的实现设置为使用更新的、优化的s3a,而不是旧版的s3连接器。

    • .set("spark.hadoop.fs.s3a.aws.crendentials.provider", "com.amazonaws.auth.EnvironmentVariablesCredentials"):这将 Spark 配置为从环境变量中获取 AWS 凭证,而无需直接在代码中指定。

    • .set("spark.jars.packages", "org.apache.hadoop:hadoop-aws:2.7.3"):这会添加对 Hadoop AWS 模块的依赖,以便 Spark 能够访问 S3。

    还需要注意的是,默认情况下,Spark 使用INFO日志级别。在此代码中,我们将其设置为WARN,以减少日志记录并提高日志的可读性。记得将<YOUR_BUCKET>替换为你自己的 S3 桶。

  11. 上传此代码到 S3 后,接下来就是创建一个包含 SparkApplication 定义的 YAML 文件。代码的内容可以在github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter08/spark/spark_job.yaml找到。

    代码定义了一个新的 SparkApplication 资源。这只有在 SparkOperator 创建了 SparkApplication 自定义资源后才有可能。让我们仔细看看这个 YAML 定义在做什么。

    • YAML 文件的第一个块指定了 apiVersion 和资源类型为 Spark 应用程序。它还为应用程序设置了名称和命名空间。

    • 第二个块定义了一个名为“ivy”的卷挂载,用于缓存依赖项,以避免每次作业运行时重新获取它们。它挂载到驱动程序和执行器的/tmp目录。

    • 第三个块配置了 Spark 属性,启用了 Ivy 缓存目录,并设置了 Kubernetes 的资源分配批量大小。

    • 第四个块配置了 Hadoop 属性,以使用 S3A 文件系统实现。

    • 第五个块将此 Spark 应用程序设置为 Python 类型,指定要使用的 Python 版本,运行模式为集群模式,并设置要使用的 Docker 镜像——在此情况下是一个之前准备好的与 AWS 和 Kafka 集成的 Spark 镜像。它还定义了即使该镜像已经存在于集群中,也会始终从 Docker Hub 拉取镜像。

    • 第六个块指定了 S3 中主 Python 应用程序文件的位置以及 Spark 版本——在这种情况下是 3.1.1。

    • 第七个块将restartPolicy设置为Never,这样应用程序只会运行一次。

    其余的块设置了驱动程序和执行器 Pod 的配置。在这里,我们设置了用于访问 S3 的 AWS 访问密钥密文,要求为驱动程序和执行器分别分配 1 个核心和 1GB 内存,并为它们提供相同的资源,我们挂载了一个名为“ivy”的emptyDir卷用于缓存依赖项,并设置了 Spark 和驱动程序 Pod 标签用于跟踪。

  12. 一旦这个文件保存在你的计算机上,并且你已经有了存储在 S3 中的.py文件,就可以运行 Spark 应用程序了。在终端中,输入以下内容:

    kubectl apply -f spark_job.yaml -n spark-operator
    kubectl get sparkapplication -n spark-operator
    

    我们可以通过以下方式获取应用程序的更多细节:

    kubectl describe sparkapplication/test-spark-job -n spark-operator
    

    要查看 Spark 应用程序的日志,请输入以下命令:

    kubectl logs test-spark-job-driver -n spark-operator
    

就这样!你刚刚在 Kubernetes 上运行了你的第一个 Spark 应用程序!Kubernetes 不会让你用相同的名称部署另一个作业,因此,要重新测试,你应该删除该应用程序:

kubectl delete sparkapplication/test-spark-job -n spark-operator

现在,让我们看看如何使用官方 Helm 图表在 Kubernetes 上部署 Airflow。

在 Kubernetes 上部署 Airflow

在 Kubernetes 上部署 Airflow 非常简单。然而,在 Helm 图表配置中有一些重要的细节,我们需要注意。

首先,我们将下载最新的 Helm 图表到本地环境:

helm repo add apache-airflow https://airflow.apache.org

接下来,我们需要配置一个custom_values.yaml文件,以更改图表的默认部署配置。此 YAML 文件的示例可以在github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter08/airflow/custom_values.yaml找到。我们将不逐行查看整个文件,而是只关注部署所需的最重要配置:

  1. defaultAirflowTagairflowVersion参数中,确保设置为2.8.3。这是 1.13.1 Helm 图表版本可用的最新 Airflow 版本。

  2. executor 参数应设置为KubernetesExecutor。这样可以确保 Airflow 使用 Kubernetes 基础设施来动态启动任务。

  3. env部分,我们将配置“远程日志记录”以允许 Airflow 将日志保存到 S3 中。这是审计和节省 Kubernetes 存储资源的最佳实践。在这里,我们为 Airflow 配置了三个环境变量。第一个设置远程日志记录为"True";第二个定义 Airflow 将日志写入的 S3 桶和文件夹;最后一个定义 Airflow 将在 AWS 中进行身份验证时使用的“连接”。稍后我们将在 Airflow UI 中设置此项。这是这个块应该是什么样的例子:

    env:
        - name: "AIRFLOW__LOGGING__REMOTE_LOGGING"
        value: "True"
        - name: "AIRFLOW__LOGGING__REMOTE_BASE_LOG_FOLDER"
        value: "s3://airflow-logs-<YOUR_ACCOUNT_NUMBER>/airflow-logs/"
        - name: "AIRFLOW__LOGGING__REMOTE_LOG_CONN_ID"
        value: "aws_conn"
    
  4. 在 webserver 块中,我们必须配置第一个用户凭据和服务类型。service 参数应该设置为“LoadBalancer”,这样我们就可以通过浏览器访问 Airflow 的 UI 界面。defaultUser 块应该如下所示:

        defaultUser:
          enabled: true
          role: Admin
          username: <YOUR_USERNAME>
          email: admin@example.com
          firstName: NAME
          lastName: LASTNAME
          password: admin
    

    在值文件中设置一个简单的密码,并在部署完成后尽快在 UI 中更改它,这一点非常重要。这样,你的凭据就不会以明文形式存储。这将避免发生重大安全事件。

  5. redis.enabled 参数应设置为false。因为我们使用的是 Kubernetes 执行器,所以 Airflow 不需要 Redis 来管理任务。如果我们没有将此参数设置为false,Helm 仍然会部署一个 Redis Pod。

  6. 最后,在dags块中,我们将配置gitSync。这是将我们的 DAG 文件发送到 Airflow 并保持它们在 GitHub(或你喜欢的任何其他 Git 仓库)中更新的最简单方法。首先,你应该创建一个名为dags的 GitHub 仓库,用来存储 Python DAG 文件。然后,你应该按如下方式配置gitSync块:

    gitSync:
        enabled: true
        repo: https://github.com/<USERNAME>/<REPO_NAME>.git
        branch: main
        rev: HEAD
        ref: main
        depth: 1
        maxFailures: 0
        subPath: "dags"
    

    请注意,我们在原始文件中省略了几个注释以提高可读性。custom_values.yaml 文件已准备好部署。现在我们可以在终端中执行以下命令:

    helm install airflow apache-airflow/airflow --namespace airflow --create-namespace -f custom_values.yaml
    

这个部署可能需要几分钟才能完成,因为 Airflow 在使 UI 可用之前会执行数据库迁移任务。

接下来,我们需要获取 UI 的 LoadBalancer 的 URL。在终端中,输入以下命令:

kubectl get svc -n airflow

EXTERNAL-IP列中,你会注意到airflow-webserver服务有一个非空值。复制这个 URL 并粘贴到浏览器中,添加:8080以访问 Airflow 的正确端口。

登录到 UI 后,点击aws_conn(正如我们在值文件中所述),选择Amazon Web Services作为连接类型,并输入你的访问密钥 ID 和密钥访问密钥。(此时,你应该已经将 AWS 凭据存储在本地——如果没有,在 AWS 控制台中,进入 IAM 并为你的用户生成新的凭据。你将无法在屏幕上看到旧的凭据。)

最后,我们将使用从 第五章 中改编的 DAG 代码,该代码将在 Kubernetes 上顺利运行。此 DAG 将自动从互联网下载 Titanic 数据集,执行简单的计算,并打印结果,这些结果将在“日志”页面上查看。代码内容可以通过以下链接访问:github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter08/airflow/dags/titanic_dag.py

将此 Python 代码的副本上传到你的 GitHub 仓库,并在几秒钟内它将出现在 Airflow UI 中。现在,激活你的 DAG(点击 switch 按钮)并跟踪执行 (图 8.1)。

图 8.1 – DAG 成功执行

图 8.1 – DAG 成功执行

然后点击任何任务以选择它,点击 日志。你将看到 Airflow 日志直接从 S3 读取 (图 8.2)。

图 8.2 – S3 中的 Airflow 日志

图 8.2 – S3 中的 Airflow 日志

恭喜!现在你已经在 Kubernetes 上成功启动并运行 Airflow。在 第十章 中,我们将把所有这些部分组合在一起,构建一个完全自动化的数据管道。

接下来,我们将在 Kubernetes 上使用 Strimzi 操作符部署一个 Kafka 集群。让我们开始吧。

在 Kubernetes 上部署 Kafka

Strimzi 是一个开源操作符,简化了在 Kubernetes 上部署和管理 Kafka 集群的过程,通过创建新的 CRD。它由 Strimzi 项目开发和维护,Strimzi 项目是 云原生计算基金会 (CNCF) 的一部分。Strimzi 操作符提供了一种声明式的方法来管理 Kubernetes 上的 Kafka 集群。用户不再需要手动创建和配置 Kafka 组件,而是通过 Kubernetes 自定义资源定义所需的 Kafka 集群状态。然后,操作符会根据指定的配置自动部署和管理 Kafka 组件:

  1. 要在 Kubernetes 中部署 Strimzi,首先,我们需要安装其 Helm 图表:

    helm repo add strimzi https://strimzi.io/charts/
    
  2. 接下来,我们使用以下命令安装操作符:

    helm install kafka strimzi/strimzi-kafka-operator --namespace kafka --create-namespace --version 0.40.0
    
  3. 我们可以通过以下方式检查部署是否成功:

    helm status kafka -n kafka
    kubectl get pods -n kafka
    
  4. 现在,是时候配置我们 Kafka 集群的部署了。以下是一个 YAML 文件,用于使用新的 CRD 配置 Kafka 集群。为了更好地理解,我们将它分解成几个部分:

    kafka_jbod.yaml

    apiVersion: kafka.strimzi.io/v1beta2
    kind: Kafka
    metadata:
        name: kafka-cluster
    spec:
        kafka:
        version: 3.7.0
        replicas: 3
    

    代码的第一部分指定了 API 版本和正在定义的资源类型。在本例中,这是一个由 Strimzi 操作符管理的 Kafka 资源。然后,我们为 Kafka 资源定义了元数据,特别是其名称,设置为kafka-cluster。接下来的代码块指定了 Kafka 经纪人的配置。我们将 Kafka 版本设置为 3.7.0,并指定希望集群中有三个副本(Kafka 经纪人实例):

        listeners:
          - name: plain
            port: 9092
            type: internal
            tls: false
          - name: tls
            port: 9093
            type: internal
            tls: true
          - name: external
            port: 9094
            type: loadbalancer
            tls: false
    

    接下来,我们定义 Kafka 经纪人的监听器。我们将配置三个监听器:

    • plain: 一个没有 TLS 加密的内部监听器,端口为 9092

    • tls: 一个启用了 TLS 加密的内部监听器,端口为 9093

    • external: 一个暴露为 LoadBalancer 服务的外部监听器,端口为 9094,不使用 TLS 加密

          readinessProbe:
            initialDelaySeconds: 15
            timeoutSeconds: 5
          livenessProbe:
            initialDelaySeconds: 15
            timeoutSeconds: 5
      

    下一个块配置了 Kafka brokers 的就绪和存活探针。就绪探针检查 broker 是否准备好接受流量,而存活探针检查 broker 是否仍在运行。initialDelaySeconds 参数指定了在执行第一次探针之前等待的秒数,而 timeoutSeconds 参数指定了探针被认为失败后的秒数:

        config:
          default.replication.factor: 3
          num.partitions: 9
          offsets.topic.replication.factor: 3
          transaction.state.log.replication.factor: 3
          transaction.state.log.min.isr: 1
          log.message.format.version: "3.7"
          inter.broker.protocol.version: "3.7"
          min.insync.replicas: 2
          log.retention.hours: 2160
    

    这个 kafka.config 块指定了 Kafka brokers 的各种配置选项,例如默认的复制因子、新主题的分区数量、偏移量和事务状态日志主题的复制因子、日志消息格式版本,以及日志保留期(以小时为单位)。Kafka 默认的日志保留期是 7 天(168 小时),但我们可以根据需要修改这个参数。需要记住的是,更长的保留期意味着更多的磁盘存储使用,因此要小心:

        storage:
          type: jbod
          volumes:
          - id: 0
            type: persistent-claim
            size: 15Gi
            deleteClaim: false
          - id: 1
            type: persistent-claim
            size: 15Gi
            deleteClaim: false
    

    kafka.storage 块配置了 Kafka brokers 的存储。我们使用 deleteClaim 设置为 false,以防止在删除 Kafka 集群时删除持久化存储卷声明:

        resources:
          requests:
            memory: 512Mi
            cpu: "500m"
          limits:
            memory: 1Gi
            cpu: "1000m"
    

    接下来,kafka.resources 块指定了 Kafka brokers 的资源请求和限制。我们请求 512 MiB 的内存和 500 毫 CPU,并将内存限制设置为 1 GiB,CPU 限制设置为 1 cpu

        zookeeper:
        replicas: 3
        storage:
            type: persistent-claim
            size: 10Gi
            deleteClaim: false
        resources:
          requests:
            memory: 512Mi
            cpu: "250m"
          limits:
            memory: 1Gi
            cpu: "500m"
    

    最后,我们有一个 zookeeper 块,它配置了 Kafka 集群使用的 ZooKeeper 集群。我们为 ZooKeeper 指定了三个副本,使用 10 GiB 的持久化存储卷声明,并设置了类似于 Kafka brokers 的资源请求和限制。

  5. 配置文件准备好后,在你的机器上输入以下命令来将集群部署到 Kubernetes:

    kubectl apply -f kafka_jbod.yaml -n kafka
    kubectl get kafka -n kafka
    

    这将输出以下内容:

    NAME          DESIRED KAFKA REPLICAS   DESIRED ZK REPLICAS
    kafka-class   3                        3
    kubectl describe kafka -n kafka
    

    现在,检查 Pods:

    kubectl get pods -n kafka
    

输出显示了三个 Kafka broker 和 ZooKeeper 实例:

NAME                      READY   STATUS
kafka-class-kafka-0       1/1     Running
kafka-class-kafka-1       1/1     Running
kafka-class-kafka-2       1/1     Running
kafka-class-zookeeper-0   1/1     Running
kafka-class-zookeeper-1   1/1     Running
kafka-class-zookeeper-2   1/1     Running

恭喜!你已经在 Kubernetes 上成功运行了一个完全操作的 Kafka 集群。接下来的步骤是部署一个 Kafka Connect 集群,并为实时数据管道做好一切准备。由于云端成本效率问题,我们暂时不会进行此操作,但我们将在 第十章 中回到这个配置。

总结

在本章中,你学习了如何在 Kubernetes 上部署和管理 Apache Spark、Apache Airflow 和 Apache Kafka 等关键大数据技术。将这些工具部署到 Kubernetes 上提供了多个好处,包括简化操作、更好的资源利用、扩展性、高可用性和统一的集群管理。

你首先在 Kubernetes 上部署了 Spark 操作符,并运行了一个 Spark 应用程序来处理来自 Amazon S3 的数据。这使你能够利用 Kubernetes 以云原生的方式运行 Spark 作业,利用动态资源分配和扩展的优势。

接下来,你使用官方 Helm 图表在 Kubernetes 上部署了 Apache Airflow。你配置了 Airflow 使用 Kubernetes 执行器运行,从而使其能够动态地在 Kubernetes 上启动任务。你还设置了远程日志记录到 Amazon S3,以便于监控和调试。在 Kubernetes 上运行 Airflow 提高了可靠性、可扩展性和资源利用率,从而更好地协调数据管道。

最后,你使用 Strimzi 操作符在 Kubernetes 上部署了 Apache Kafka。你配置了一个包含代理、ZooKeeper 集群、持久化存储卷以及内部/外部监听器的 Kafka 集群。在 Kubernetes 上部署 Kafka 简化了操作、扩展、高可用性和集群管理,从而帮助构建流数据管道。

总体而言,你现在已经具备了在 Kubernetes 上部署和管理大数据栈关键组件的实战经验。这将使你能够构建可靠、可扩展的数据应用和管道,利用 Kubernetes 容器编排的强大功能。 本章中学习的技能对于在云原生环境中高效运行大数据工作负载至关重要。

在下一章,我们将看到如何在 Kubernetes 上构建数据消费层,并了解如何通过工具连接这些层来可视化和使用数据。

第九章:数据消费层

在今天的数据驱动世界中,组织正在处理日益增长的数据量,有效地消费和分析这些数据对于做出明智的业务决策至关重要。当我们深入探讨基于 Kubernetes 的大数据领域时,必须解决数据消费层的关键组成部分。这一层将作为数据存储库和需要提取有价值洞察并对业务产生影响的业务分析师之间的桥梁。

在本章中,我们将探讨两个强大的工具,它们将使您能够释放基于 Kubernetes 的数据架构的潜力:TrinoElasticsearch。Trino,一个分布式 SQL 查询引擎,将使您能够直接查询您的数据湖,消除了传统数据仓库的需求。您将学习如何在 Kubernetes 上部署 Trino,监视其性能,并对存储在 Amazon S3 中的数据执行 SQL 查询。

此外,我们将介绍 Elasticsearch,这是一个广泛用于实时数据管道的高度可伸缩和高效的搜索引擎,以及其强大的数据可视化工具 Kibana。您将获得在 Kubernetes 上部署 Elasticsearch、对数据进行索引以进行优化存储和检索,并使用 Kibana 构建简单而富有洞察力的可视化的实际经验。这将使您能够分析实时数据流并发现有价值的模式和趋势。

到达本章结束时,您将掌握成功部署和利用 Trino 和 Elasticsearch 在 Kubernetes 上所需的技能。您将能够直接对数据湖执行 SQL 查询,监视查询执行和历史记录,并利用 Elasticsearch 和 Kibana 进行实时数据分析和可视化。

在本章中,我们将讨论以下主要主题:

  • 开始使用 SQL 查询引擎

  • 在 Kubernetes 中部署 Trino

  • 在 Kubernetes 中部署 Elasticsearch

  • 运行查询和连接其他工具

技术要求

对于本章,您应准备好一个用于部署的 AWS EKS 集群,并在本地安装了 DBeaver Community (dbeaver.io/)。我们将继续在我们在 第八章 部署的集群上工作。本章的所有代码都可以在 github.com/PacktPublishing/Bigdata-on-KubernetesChapter09 文件夹中找到。

开始使用 SQL 查询引擎

在大数据的世界中,我们存储和分析数据的方式发生了重大转变。曾经是数据分析首选方案的传统数据仓库,已经被更现代、更具可扩展性的方法所取代,例如 SQL 查询引擎。这些引擎,如Trino(前身为 Presto)、DremioApache Spark SQL,提供了比传统数据仓库更高性能、更具性价比和更灵活的替代方案。

接下来,我们将看到数据仓库和 SQL 查询引擎之间的主要区别。

传统数据仓库的局限性

传统数据仓库是为存储和分析关系数据库中的结构化数据而设计的。然而,随着大数据的到来以及日志文件、传感器数据和社交媒体数据等多样化数据源的普及,数据仓库的局限性逐渐显现。这些局限性包括:

  • 可扩展性:数据仓库通常难以实现水平扩展,处理大量数据时效率较低。

  • 数据摄取:将数据提取、转换和加载ETL)到数据仓库的过程可能复杂、耗时且资源密集。

  • 成本:数据仓库的搭建和维护可能非常昂贵,尤其是在处理大量数据时。

  • 灵活性:数据仓库通常优化用于结构化数据,可能无法高效处理半结构化或非结构化数据。

SQL 查询引擎的开发旨在解决这些局限性。让我们来看它们是如何工作的。

SQL 查询引擎的崛起

SQL 查询引擎,如 Trino,提供了一种分布式、可扩展且具性价比的解决方案,用于查询存储在各种数据源中的大规模数据集,包括对象存储(如 Amazon S3、Google Cloud Storage 和 Azure Blob Storage)、关系数据库和 NoSQL 数据库。我们将在下一部分深入探讨 SQL 查询引擎的架构。

以下是 SQL 查询引擎的一些关键优势:

  • 高性能:SQL 查询引擎旨在利用分布式计算的优势,使它们能够在多个节点上并行处理大规模数据集。这种并行化使得即使在庞大的数据集上也能进行高性能查询。

  • 性价比高:通过利用对象存储并将存储与计算分离,SQL 查询引擎相比传统数据仓库可以显著降低数据存储和处理的成本。

  • 可扩展性:SQL 查询引擎可以通过向集群中添加更多节点来水平扩展,使其能够高效处理不断增长的数据量。

  • 灵活性:SQL 查询引擎能够查询多种数据源,包括结构化、半结构化和非结构化数据,使其具有高度的灵活性和适应性,能够应对各种数据格式和存储系统。

  • 开源:许多 SQL 查询引擎都是开源项目,允许组织利用社区贡献的力量并避免供应商锁定。

现在,让我们理解一下这种解决方案的底层架构。

SQL 查询引擎的架构

Trino 等 SQL 查询引擎设计为在分布式计算环境中工作,其中多个节点协同处理查询并返回结果。该架构通常包括以下组件:

  • 协调节点,负责解析 SQL 查询,创建分布式执行计划,并协调查询在工作节点之间的执行。

  • 一组工作节点,负责执行协调节点分配的任务。它们从底层数据源读取数据,执行计算,并根据需要与其他工作节点交换中间结果。

  • 一个元数据存储,其中包含有关数据源、表定义及查询执行所需的其他元数据的信息。

当用户向 SQL 查询引擎提交 SQL 查询时,发生的过程如下:

  1. 首先,协调节点接收查询并解析它,以创建一个分布式执行计划

  2. 执行计划被划分为更小的任务,这些任务被分配给可用的工作节点。

  3. 工作节点从底层数据源读取数据,执行计算,并根据需要交换中间结果。

  4. 协调节点收集并合并来自工作节点的结果,生成最终的查询结果,然后返回给客户端应用程序的用户。

这种分布式架构使 SQL 查询引擎能够利用多个节点的联合计算能力,使其能够高效处理大数据集并提供高性能的查询执行。

在 Trino 的情况下,它可以直接连接到对象存储系统,如 Amazon S3、Azure Blob Storage 或 Google Cloud Storage,其中数据通常以 Parquet、ORC 或 CSV 等格式存储。Trino 可以直接从对象存储读取并处理这些数据,无需中间的数据加载或转换步骤。这种能力消除了单独的数据摄取过程的需要,简化了复杂性并加速了洞察的获取时间。

Trino 的分布式架构使其能够将查询执行分配到多个工作节点,每个节点并行处理数据的一部分。这样的并行化使 Trino 能够利用集群的计算能力,从而在处理大规模数据集时实现高性能查询执行。

此外,Trino 的成本效益来源于它能够将存储与计算分离。通过利用对象存储进行数据存储,组织可以利用这些存储系统的低成本和可扩展性,同时根据需要动态分配计算资源(工作节点)以执行查询。这种关注点的分离使组织能够优化其基础设施成本,并根据特定需求独立地扩展资源。

现在,让我们进行一个实践练习,看看如何将 Trino 部署到 Kubernetes,并将其连接到 Amazon S3 作为数据源。

在 Kubernetes 中部署 Trino

使用官方 Helm 图表部署 Trino 非常简单。首先,我们使用以下命令安装该图表:

helm repo add trino https://trinodb.github.io/charts

接下来,我们将配置custom_values.yaml文件。该文件的完整版本可以在github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter09/trino/custom_values.yaml找到。此部署仅需少量自定义配置。首先,server.workers参数允许我们设置 Trino 集群的工作节点数。我们将其设置为2,但如果要处理大数据查询,建议进行扩展:

server:
  workers: 2

在参数块中,将image.tag参数设置为432,因为这是与我们使用的图表版本(0.19.0)兼容的最新 Trino 版本:

image:
  registry: ""
  repository: trinodb/trino
  tag: 432

additionalCatalogs部分,我们必须配置 Trino 使用 AWS Glue 数据目录。该代码块应该如下所示:

additionalCatalogs:
  hive: |
    connector.name=hive
    hive.metastore=glue

最后,我们将service.type参数设置为LoadBalancer,以便能够从 AWS 外部访问 Trino(仅用于测试,不适用于生产环境):

service:
  type: LoadBalancer
  port: 8080

就这样。我们已经准备好在 Kubernetes 上启动 Trino。

注意

我们没有使用任何认证方法(密码、OAuth、证书等)。在生产环境中,你应该设置合适的认证方法,并将流量限制在你的 VPC(私有网络)内,而不是像这里一样将负载均衡器暴露到互联网上。这个简单的配置仅用于培训和非关键数据。

保存custom_values.yaml文件后,使用以下命令部署 Trino:

helm install trino trino/trino -f custom_values.yaml -n trino --create-namespace --version 0.19.0

现在,我们可以使用以下命令检查部署是否成功:

kubectl get pods,svc -n trino

这将产生如下输出:

NAME                                    READY   STATUS
pod/trino-coordinator-dbbbcf9d9-94wsn   1/1     Running
pod/trino-worker-6c58b678cc-6fgjs       1/1     Running
pod/trino-worker-6c58b678cc-hldrb       1/1     Running
NAME           TYPE          CLUSTER-IP       EXTERNAL-IP
service/trino  LoadBalancer  10.100.246.148   xxxx.us-east-1.elb.amazonaws.com

输出已简化以提高可视化效果。我们可以看到一个协调节点和两个工作节点,这正是我们设置的节点数。现在,复制输出中EXTERNAL-IP列提供的 URL,并将其粘贴到浏览器中,在末尾加上:8080。你应该能看到登录页面。

图 9.1 – Trino 登录页面

图 9.1 – Trino 登录页面

默认用户是trino。由于我们在部署时没有设置任何密码,因此不需要密码。点击登录后,你将看到 Trino 的监控页面。

图 9.2 – Trino 的监控页面

图 9.2 – Trino 的监控页面

接下来,我们将使用相同的LoadBalancer URL,通过 DBeaver(一款开源 SQL 客户端)与 Trino 进行交互。

连接 DBeaver 和 Trino

要连接 Trino,首先,打开 DBeaver 并创建一个新的 Trino 连接。在配置部分(trino作为用户名,密码留空)。

图 9.3 – DBeaver 连接配置

图 9.3 – DBeaver 连接配置

然后,点击测试连接 …。如果这是第一次配置 Trino 连接,DBeaver 会自动找到必要的驱动程序并弹出新窗口,提示您下载它。您可以点击确定并完成安装,然后完成配置。

在尝试访问数据之前,我们需要对一些数据进行目录化,并使其在 Glue 数据目录中可用,同时设置一个 IAM 权限,允许 Trino 访问目录和底层数据。让我们开始吧。

github.com/neylsoncrepalde/titanic_data_with_semicolon下载数据集,并将 CSV 文件存储在一个名为titanic的文件夹中的 S3 存储桶里。Glue 只理解来自文件夹中的表,而不是孤立的文件。现在,我们将创建一个Glue 爬虫。该爬虫将扫描数据集,映射其列和列类型,并在目录中注册元数据:

  1. 在您的 AWS 账户中,输入Glue以进入 AWS Glue 服务,展开侧边菜单中的数据目录选项,点击爬虫图 9.4)。

图 9.4 – AWS Glue 着陆页

图 9.4 – AWS Glue 着陆页

  1. 接下来,点击bdok-titanic-crawler(您可以选择任何名称)。点击下一步

  2. 在下一页,点击s3://<YOUR_BUCKET_NAME>/titanic/。您可以保持其他配置为默认。点击添加 S3 数据源,然后点击下一步

  3. 在下一步中,点击AWSGlueServiceRole-titanic。点击下一步

  4. 在下一页,点击bdok-database,点击创建数据库,然后关闭此窗口并返回到Glue 爬虫 配置标签页。

  5. 返回到爬虫页面,点击刷新按钮,选择您的新bdok-database数据库。保持其他选项为默认。点击下一步

  6. 现在,在最后一节中,仔细检查所有信息并点击创建爬虫

  7. 当准备好后,您将被带到 AWS 控制台的爬虫页面。点击运行爬虫以启动爬虫。它应该运行约 1 到 2 分钟(图 9.5)。

图 9.5 – bdok-titanic-crawler

图 9.5 – bdok-titanic-crawler

  1. 爬虫完成后,您可以通过访问数据目录表菜单项验证表是否已正确目录化。titanic 表应与bdok-database数据库一起列出(图 9.6)。

图 9.6 – Glue 数据目录表

图 9.6 – Glue 数据目录表

  1. 点击titanic表的名称,检查列是否正确映射。

  2. 现在,我们需要创建一个 IAM 策略,授权 Kubernetes 访问目录和存储在 S3 中的数据。为此,在控制台中,进入studycluster,你会看到为 Kubernetes 创建的两个角色,一个是服务角色,一个是节点实例角色。我们需要更改节点实例角色的权限(图 9.7)。

图 9.7 – IAM 角色页面

图 9.7 – IAM 角色页面

  1. 点击节点实例角色,然后点击添加权限按钮,选择创建 内联策略

  2. 指定权限页面,点击以 JSON 文档方式编辑,并粘贴此 GitHub 仓库中的 JSON 文件:github.com/PacktPublishing/Bigdata-on-Kubernetes/blob/main/Chapter09/trino/iam/AthenaFullWithAllBucketsPolicy.json图 9.8)。此策略允许 Athena 和 Glue 权限,并获取任何桶中的所有 S3 数据。请记住,这是一个非常开放的策略,不应在生产环境中使用。最好的安全实践是仅允许访问所需的桶。

图 9.8 – 指定权限

图 9.8 – 指定权限

  1. 点击AthenaFullWithAllBucketsPolicy,以便稍后更方便地搜索此策略。然后,点击创建策略。我们就准备好了!

现在,让我们回到 DBeaver 并玩一些查询。首先,我们需要找到表存储的位置。在 DBeaver 中展开 Trino 连接,你会看到一个名为hive的数据库。这是 Glue 数据目录中的数据在 Trino 中的镜像。展开hive,你会看到bdok-database目录。如果展开表格,你会看到映射的titanic数据集。

要测试查询,右键点击hive数据库,选择SQL 编辑器,然后选择新建 SQL 脚本。现在,运行查询:

select * from hive."bdok-database".titanic

你应该能看到如下结果(图 9.9):

图 9.9 – Trino 的 DBeaver 结果

图 9.9 – Trino 的 DBeaver 结果

当然,Trino 可以执行我们喜欢的任何计算或聚合。让我们尝试一个简单的查询,按pclasssex统计所有乘客的数量和平均年龄。我们将按sexpclass的顺序显示结果。

select
    pclass,
    sex,
    COUNT(1) as people_count,
    AVG(age) as avg_age
from hive."bdok-database".titanic
group by pclass, sex
order by sex, pclass

这个查询返回了以下结果:

图 9.10 – Titanic 数据集上的简单查询

图 9.10 – Titanic 数据集上的简单查询

现在,让我们再次访问 Trino 的监控页面,查看我们刚才运行的查询。在查询详情下勾选已完成框,以查看所有查询;第一个显示的就是我们刚才运行的查询。点击它查看详细信息。

就是这样!你已经成功地在 Kubernetes 上部署了 Trino,并利用它查询了来自 Amazon S3 数据湖的数据。在接下来的部分,我们将开始使用 Elasticsearch。

在 Kubernetes 中部署 Elasticsearch

虽然 Trino 提供了一个强大的 SQL 接口,用于查询数据湖中的结构化数据,但许多现代应用程序也需要实时分析半结构化和非结构化数据,如日志、指标和文本。对于这些类型的用例,Elasticsearch(或者 ELK 堆栈,指代 Elasticsearch、Logstash 和 Kibana)提供了一个强大的解决方案。

Elasticsearch 是一个开源的分布式、RESTful 搜索与分析引擎,建立在 Apache Lucene 之上。它旨在快速并几乎实时地存储、搜索和分析大量数据。

Elasticsearch 的核心是一个 NoSQL 数据库,它使用 JSON 文档来表示数据。它会对每个字段中的所有数据进行索引,并使用高级数据结构和索引技术使得搜索速度极快。

Elasticsearch 如何存储、索引和管理数据

数据以单独的 JSON 文档存储在 Elasticsearch 中。这些文档会被分组为索引内的类型。你可以将索引视为一个具有定义映射或模式的数据库表。

向 Elasticsearch 添加数据时,你需要向相应的索引发送一个 HTTP 请求,请求体中包含 JSON 文档。Elasticsearch 会使用先进的数据结构(如 Apache Lucene 的倒排索引)自动对文档中的所有字段数据进行索引。

这个索引过程优化了数据,使其能够进行极快的查询和聚合。Elasticsearch 将数据分布到分片中,这些分片可以被分配到集群中的不同节点,以实现冗余和可扩展性。

当你想要从 Elasticsearch 查询或检索数据时,你可以使用 RESTful 搜索 API,通过简单的 JSON 请求体定义查询。查询结果同样以 JSON 格式返回。

Elasticsearch 从一开始就是作为一个分布式系统设计的。它可以扩展到数百台服务器,并处理 PB 级的数据。其核心元素如下:

  • 节点,是 Elasticsearch 的运行实例,它们共同构成一个 集群

  • 索引,是具有相似特征的文档集合

  • 分片,是索引的低级分区,包含索引中所有文档的一部分

  • 副本,是用于冗余和提高性能的分片副本

Elasticsearch 分布式架构的核心是分片系统。分片指的是将 Elasticsearch 索引水平拆分成多个部分,称为分片。这使得索引数据可以分布在集群中的多个节点上,从而提供多个关键好处:

  • 横向扩展性:通过将分片分布到各个节点,Elasticsearch 可以有效地横向扩展,以处理更多数据和更高的查询/索引吞吐量。随着数据集的增长,您只需向集群中添加更多节点,Elasticsearch 会自动迁移分片以平衡负载。

  • 高可用性:每个分片可以有一个或多个副本分片。副本是主分片的完整副本。副本提供冗余和高可用性——如果承载主分片的节点发生故障,Elasticsearch 会自动将副本提升为新的主分片以接管任务。

  • 操作并行化:由于索引操作(如搜索和聚合)会在每个分片上并行执行,拥有更多的分片可以实现更大的并行化,从而提升性能。

当您在 Elasticsearch 中创建索引时,需要指定该索引应该拥有的主分片数量。例如,如果您为索引配置了三个主分片,Elasticsearch 将把索引数据横向分区为三个分片,并将它们分布到集群中的各个节点上。

每个主分片也可以配置零个或多个副本分片。一个常见的设置是拥有一个副本,这意味着每个分片都有两个副本——一个主分片和一个副本。副本分片也会分布到集群中的节点上,每个副本与其相应的主分片位于不同的节点上以确保冗余。

Elasticsearch 使用分片分配策略自动管理节点之间的分片分配。默认情况下,它会将分片尽可能多地分布到各个节点上以平衡负载。当节点被添加或从集群中移除时,Elasticsearch 会自动迁移分片以重新平衡集群。

查询会在每个分片上并行执行,结果会被合并以产生最终的结果集。写入(索引新文档)会发送到一个主分片,该主分片负责验证数据、使更改持久化,并将更改复制到相关的副本分片。

为索引配置的分片数量在索引创建时是固定的,之后不能更改,因此合理的分片规划非常重要。拥有更多的分片可以实现更大的并行化,但过多的分片也可能增加开销。

一个好的经验法则是从足够数量的分片(3 到 5 个分片)开始,以便索引数据可以分布到多个节点上。如果索引变得非常庞大且需要更多的并行化,可以增加分片数量。然而,一般不建议拥有成百上千个分片,因为这会增加集群管理的开销。

现在,让我们来看一下如何在 Kubernetes 上部署 Elasticsearch。

Elasticsearch 部署

在这里,我们将使用Elastic Cloud on KubernetesECK),这是一个官方的 Elastic 操作员,允许你在 Kubernetes 集群上部署、管理和编排 Elastic Stack 应用程序。我们将使用官方的 Helm 图表来安装该操作员。在终端中,输入以下命令:

helm repo add elastic https://helm.elastic.co
helm install elastic-operator elastic/eck-operator -n elastic --create-namespace --version 2.12.1

这将会将 Helm 图表下载到本地,并在一个名为elastic的新环境中部署 Elastic Stack 的默认定义。在这里,我们将使用版本2.12.1的 Helm 图表。

现在,我们将为 Elasticsearch 集群配置部署。elastic_cluster.yaml YAML 文件可以完成这项工作。

apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: elastic
spec:
  version: 8.13.0
  volumeClaimDeletePolicy: DeleteOnScaledownAndClusterDeletion
  nodeSets:
  - name: default
    count: 2
    podTemplate:
      spec:
        containers:
        - name: elasticsearch
          resources:
            requests:
              memory: 2Gi
              cpu: 1
            limits:
              memory: 2Gi
        initContainers:
        - name: sysctl
          securityContext:
            privileged: true
            runAsUser: 0
          command: ['sh', '-c', 'sysctl -w vm.max_map_count=262144']
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 5Gi
        storageClassName: gp2

让我们仔细看看这段代码。第一个块指定了 API 版本和我们正在定义的 Kubernetes 资源类型。在这种情况下,它是来自elasticsearch.k8s.elastic.co/v1 API 组的Elasticsearch资源,由 ECK 操作员提供。metadata块指定了集群的名称,这里是elastic。在spec块中,我们设置了要使用的 Elasticsearch 版本(8.13.0)以及一个策略,该策略决定了DeleteOnScaledownAndClusterDeletion策略在 Elasticsearch 集群缩减或完全删除时删除 PVCs。

nodeSets块定义了 Elasticsearch 节点的配置。在这种情况下,我们有一个名为default的节点集,节点数为2,意味着集群中将有两个 Elasticsearch 节点。podTemplate块指定了运行 Elasticsearch 容器的 Pod 的配置。在这里,我们定义了 Elasticsearch 容器的资源请求和限制,将内存请求和限制设置为 2 GiB,CPU 请求为一个 vCPU。

initContainers块是官方 Elastic 文档对生产环境的推荐。它定义了一个将在主 Elasticsearch 容器启动之前运行的容器。在这里,我们有一个名为sysctlinitContainer,它以特权安全上下文运行,并将vm.max_map_count内核设置为262144。这个设置推荐在 Linux 上运行 Elasticsearch 时使用,以允许在内存映射区域使用中设置更高的限制。

最后,volumeClaimTemplates块定义了用于存储 Elasticsearch 数据的 PVC。在这种情况下,我们有一个名为elasticsearch-data的 PVC,要求的存储大小为 5 GiB。accessModes指定该卷应该是ReadWriteOnce,意味着它可以被单个节点以读写方式挂载。storageClassName设置为gp2,这是 AWS EBS 存储类别的通用 SSD 卷。

在本地保存该文件后,运行以下命令以部署一个 Elasticsearch 集群:

kubectl apply -f elastic_cluster.yaml -n elastic

使用以下命令监控部署过程:

kubectl get pods -n elastic

或者,你也可以使用以下命令:

kubectl get elastic -n elastic

这将提供更多的信息。请注意,这个部署过程可能需要几分钟才能完成。你还可以使用以下命令获取集群的详细信息:

kubectl describe elastic -n elastic

在输出中,HEALTH应为greenPHASE列应显示Ready

NAME      HEALTH   NODES   VERSION   PHASE
elastic   green    2       8.13.0    Ready

现在,让我们转向 Kibana。我们将按照相同的流程进行。首先要做的是设置一个名为kibana.yaml的 YAML 文件,并配置部署。

apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: kibana
spec:
  version: 8.13.0
  count: 1
  elasticsearchRef:
    name: elastic
  http:
    service:
      spec:
        type: LoadBalancer
  podTemplate:
    spec:
      containers:
      - name: kibana
        env:
          - name: NODE_OPTIONS
            value: "--max-old-space-size=2048"
        resources:
          requests:
            memory: 1Gi
            cpu: 0.5
          limits:
            memory: 2Gi
            cpu: 2

此代码与以前非常相似,但更简单。主要区别在于spec块。首先,elasticsearchRef参数指定了 Kibana 应连接到的 Elasticsearch 集群的名称。在这种情况下,它引用了我们之前创建的名为elastic的 Elasticsearch 集群。http 块配置了将公开 Kibana 部署的 Kubernetes 服务。具体来说,我们将服务的类型设置为LoadBalancer,这意味着云提供商将提供一个负载均衡器来分发 Kibana 实例的流量。最后,在podTemplate块中,我们有一个env配置,设置了一个名为NODE_OPTIONS的环境变量,其值为--max-old-space-size=2048,这会增加 Kibana 的最大堆大小。

现在,我们已经准备好部署:

kubectl apply -f kibana.yaml -n elastic

我们使用与之前相同的命令来监视部署是否成功。现在,我们需要访问 Elastic 和 Kibana 的自动生成密码。我们可以通过以下命令完成:

kubectl get secret elastic-es-elastic-user -n elastic -o go-template='{{.data.elastic | base64decode}}'

此命令将在屏幕上打印生成的密码。复制并妥善保管。现在,运行以下命令:

kubectl get svc -n elastic

要获取服务列表,请复制负载均衡器的 URL 地址,并将其粘贴到浏览器中,在末尾添加:5601并以 https://开头。Kibana 将不接受常规 HTTP 协议连接。您应该看到登录页面,如图 9.11所示。

图 9.11 – Kibana 登录页面

插入用户名和密码后,您应该能够访问 Kibana 的第一个空白页面(图 9.12)。

图 9.12 – Kibana 的第一个空白页面

图 9.12 – Kibana 的第一个空白页面

点击自行探索,现在您可以尽情使用 Elastic(虽然目前还没有任何数据)。为此,我们将尝试使用我们熟悉的 Titanic 数据集。在主页上,点击左上角的菜单,然后点击堆栈管理(最后一个选项)。在下一页中,左侧菜单中,点击数据视图,然后在中心点击上传文件按钮(图 9.13)。

图 9.13 – 在 Kibana 中上传文件选项

图 9.13 – 在 Kibana 中上传文件选项

现在,选择你已经拥有的 Titanic 数据集 CSV 文件并将其上传到 Kibana。您将看到一个页面,显示来自文件的映射内容(图 9.14)。

图 9.14 – Titanic 数据集的映射内容

图 9.14 – Titanic 数据集的映射内容

现在,点击titanic,确保创建数据视图选项已选中。点击导入图 9.15)。

图 9.15 – Kibana – 创建索引

图 9.15 – Kibana – 创建索引

几秒钟后,你应该会看到一个成功的屏幕。现在,让我们使用这些数据进行一些可视化操作。返回主页,在左侧菜单中点击仪表板。然后点击创建仪表板,再点击创建可视化。这将带你进入 Kibana 中的可视化构建页面(图 9.16)。

图 9.16 – Kibana 可视化创建

现在,让我们来创建一些快速的可视化。在页面的右侧,选择可视化的类型(我们选择垂直堆叠条形图)。对于横轴,拖动并放置Pclass字段。对于纵轴,拖动并放置Fare字段。由于这是一个数值字段,Kibana 会自动建议使用中位数作为聚合函数。点击它将其更改为平均值。对于细分,拖动并放置Sex字段。最后我们将得到一个漂亮的条形图,如图 9.17所示。

图 9.17 – 按性别和 Pclass 分组的平均票价

图 9.17 – 按性别和 Pclass 分组的平均票价

点击保存并返回,在新仪表板上查看你新创建的图形。让我们再进行一次快速分析。再次点击创建可视化。这次,我们将制作一个散点图,展示年龄票价之间是否存在关联。将年龄放入横轴,将票价放入纵轴。点击纵轴将聚合函数更改为平均值。现在,你将看到一个漂亮的散点图,展示这两个变量之间的互动。到目前为止,没有显著的相关性。接下来,我们将Pclass字段添加为细分,你将得到一个很酷的数据可视化图像(图 9.18)。

图 9.18 – Kibana 中的散点图

图 9.18 – Kibana 中的散点图

现在,点击Survivors图 9.19)。

图 9.19 – 生还者计数可视化

图 9.19 – 生还者计数可视化

然后,点击保存并返回,并根据需要手动重新排列仪表板(图 9.20中展示了一个简单的示例)。

图 9.20 – 你的第一个 Kibana 仪表板

图 9.20 – 你的第一个 Kibana 仪表板

就是这样!你成功地在 Kubernetes 上部署了 Elasticsearch 和 Kibana,手动添加了数据,并创建了一个仪表板(具有巨大潜力)。随时可以尝试使用 Kibana,尝试其他数据集和可视化。

总结

在本章中,我们探讨了两个强大的工具,Trino 和 Elasticsearch,它们使得在基于 Kubernetes 的大数据架构中进行有效的数据消费和分析成为可能。我们学习了拥有一个强大的数据消费层的重要性,它可以在数据仓库和业务分析师之间架起桥梁,帮助他们提取有价值的洞察,并做出明智的决策。

我们学习了如何在 Kubernetes 上部署 Trino,一个分布式 SQL 查询引擎,并利用它直接查询存储在对象存储系统(如 Amazon S3)中的数据。这消除了传统数据仓库的需求,并提供了一种具有成本效益、可扩展且灵活的解决方案,用于查询大规模数据集。我们通过实际操作获得了在 Kubernetes 上部署 Trino、配置其使用 AWS Glue 数据目录,并执行针对数据湖的 SQL 查询的经验。

此外,我们还深入学习了 Elasticsearch,一个高度可扩展且高效的搜索引擎,以及 Kibana,它是一个强大的数据可视化工具。我们学习了如何使用 ECK 操作符在 Kubernetes 上部署 Elasticsearch,如何为优化存储和检索创建索引,以及如何使用 Kibana 构建简单但富有洞察力的可视化。这一组合使我们具备了分析实时数据流并发现有价值的模式和趋势的能力。

本章中学习的技能在今天这个数据驱动的世界中至关重要,组织需要有效地消耗和分析海量数据,以做出明智的业务决策。Trino 和 Elasticsearch 对于不熟悉编码的业务团队来说也非常有帮助,可以通过简单的 SQL 查询或可视化的方式探索数据,从而提升日常决策能力。

在下一章中,我们将把到目前为止所学的所有内容结合起来,构建一个完整的 Kubernetes 数据管道。

第十章:在 Kubernetes 上构建大数据管道

在前面的章节中,我们介绍了在 Kubernetes 上构建大数据管道所需的各个组件。我们探索了 Kafka、Spark、Airflow、Trino 等工具。然而,在现实世界中,这些工具并非孤立运行。它们需要集成并协调工作,形成完整的数据管道,以应对各种数据处理需求。

在本章中,我们将把你迄今为止所学的所有知识和技能结合起来,通过构建两个完整的数据管道来实践:一个批处理管道和一个实时管道。到本章结束时,你将能够(1)部署和协调构建大数据管道所需的所有工具;(2)使用 Python、SQL 和 API 编写数据处理、协调和查询的代码;(3)无缝集成不同的工具,创建复杂的数据管道;(4)理解并应用构建可扩展、高效且可维护的数据管道的最佳实践。

我们将从确保所有必要工具在你的 Kubernetes 集群中正确部署和运行开始。然后,我们将深入构建批处理管道,你将学习如何从各种来源摄取数据,使用 Spark 进行处理,并将结果存储在数据湖中以供查询和分析。

接下来,我们将讨论实时数据管道,它对于近实时地处理和分析数据流至关重要。你将学习如何使用 Kafka、Spark Streaming 和 Elasticsearch 来摄取和处理数据流,使你能够构建能够实时响应事件的应用程序。

到本章结束时,你将获得在 Kubernetes 上构建完整数据管道的实际操作经验,为应对现实世界的大数据挑战做好准备。让我们开始吧,解锁 Kubernetes 上的大数据力量!

在本章中,我们将涵盖以下主要主题:

  • 检查已部署的工具

  • 构建批处理数据管道

  • 构建实时数据管道

技术要求

本章的活动要求你拥有一个正在运行的 Kubernetes 集群。有关 Kubernetes 部署和所有必要操作符的详细信息,请参阅第八章。你还需要一个亚马逊 Web 服务AWS)账户来进行练习。我们还将使用 DBeaver 来检查数据。安装说明,请参阅第九章

本章的所有代码都可以在github.com/PacktPublishing/Bigdata-on-KubernetesChapter10文件夹中找到。

检查已部署的工具

在我们深入构建一个完整的数据管道之前,我们需要确保所有必要的操作符已经在 Kubernetes 上正确部署。我们将检查 Spark 操作符、Strimzi 操作符、Airflow 和 Trino。首先,我们通过以下命令检查 Spark 操作符:

kubectl get pods -n spark-operator

此输出显示 Spark 操作符正在成功运行:

NAME                                READY   STATUS
spark-operator-74db6fcf98-f86vt     1/1     Running
spark-operator-webhook-init-5594s   0/1     Completed

现在,我们将检查 Trino。为此,输入以下命令:

kubectl get pods -n trino

检查所有 Pod 是否正确运行;在我们的案例中,包括一个协调器 Pod 和两个工作节点 Pod。同时,使用以下命令检查 Kafka 和 Elasticsearch:

kubectl get pods -n kafka
kubectl get pods -n elastic

最后,我们需要重新部署 Airflow。我们将需要使用 Airflow 的特定版本以及其某个提供程序库,以确保与 Spark 正常配合工作。我已经设置了一个 Airflow 2.8.1 的镜像,并配备了 apache-airflow-providers-cncf-kubernetes 库的 7.13.0 版本(这是 SparkKubernetesOperator 所需的)。如果你已经安装了 Airflow,可以通过以下命令将其删除:

helm delete airflow -n airflow

确保所有服务和持久化存储卷声明也已被删除,使用以下代码:

kubectl delete svc --all -n airflow
kubectl delete pvc --all -n airflow

然后,我们需要稍微修改我们已有的 custom_values.yaml 文件配置。我们需要将 defaultAirflowTagairflowVersion 参数设置为 2.8.1,并且将 images.airflow 参数更改为获取已准备好的公共镜像:

images:
  airflow:
    repository: "docker.io/neylsoncrepalde/apache-airflow"
    tag: "2.8.1-cncf7.13.0"
    digest: ~
    pullPolicy: IfNotPresent

此外,如果你使用的是不同的 GitHub 仓库或文件夹,别忘了调整 dags.gitSync 参数。适配后的完整版本 custom_values.yaml 代码可在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/airflow_deployment 查阅。用新配置重新部署 Airflow,操作如下:

helm install airflow apache-airflow/airflow --namespace airflow --create-namespace -f custom_values.yaml

最后所需的配置允许 Airflow 在集群上运行 SparkApplication 实例。我们将为 Airflow 命名空间设置一个服务账户和一个集群角色绑定:

kubectl create serviceaccount spark -n airflow
kubectl create clusterrolebinding spark-role --clusterrole=edit --serviceaccount=airflow:spark --namespace=airflow

现在,我们将创建一个新的集群角色和集群角色绑定,以授予 Airflow 工作节点必要的权限。设置一个 YAML 文件:

rolebinding_for_airflow.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: spark-cluster-cr
  labels:
    rbac.authorization.kubeflow.org/aggregate-to-kubeflow-edit: "true"
rules:
  - apiGroups:
      - sparkoperator.k8s.io
    resources:
      - sparkapplications
    verbs:
      - "*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: airflow-spark-crb
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: spark-cluster-cr
subjects:
  - kind: ServiceAccount
    name: airflow-worker
    namespace: airflow

现在,使用以下命令部署此配置:

kubectl apply -f rolebinding_for_airflow.yaml -n airflow

就这样!我们现在可以进入批处理数据管道的实现部分了。让我们开始吧。

构建批处理管道

对于批处理管道,我们将使用在 第五章 中使用的 IMBD 数据集。我们将自动化整个过程,从数据采集和将数据摄取到我们的数据湖(Amazon Simple Storage ServiceAmazon S3),直到将消费就绪的表格交付给 Trino。在 图 10.1 中,你可以看到代表本节练习架构的示意图:

图 10.1 – 批处理管道的架构设计

图 10.1 – 批处理管道的架构设计

现在,让我们进入代码部分。

构建 Airflow DAG

让我们像往常一样开始开发我们的 Airflow DAG。完整的代码可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/batch/dags 文件夹中找到:

  1. 以下是 Airflow DAG 的第一部分代码:

    imdb_dag.py

    from airflow.decorators import task, dag
    from airflow.utils.task_group import TaskGroup
    from airflow.providers.cncf.kubernetes.operators.spark_kubernetes import SparkKubernetesOperator
    from airflow.providers.cncf.kubernetes.sensors.spark_kubernetes import SparkKubernetesSensor
    from airflow.providers.amazon.aws.operators.glue_crawler import GlueCrawlerOperator
    from airflow.models import Variable
    from datetime import datetime
    import requests
    import boto3
    aws_access_key_id = Variable.get("aws_access_key_id")
    aws_secret_access_key = Variable.get("aws_secret_access_key")
    s3 = boto3.client('s3',
        aws_access_key_id=aws_access_key_id,
        aws_secret_access_key=aws_secret_access_key
    )
    default_args = {
        'owner': 'Ney',
        'start_date': datetime(2024, 5, 10)
    }
    

    这段代码导入了必要的库,设置了两个用于在 AWS 上进行身份验证的环境变量,定义了一个 Amazon S3 客户端,并设置了一些默认配置。

  2. 在下一个代码块中,我们将开始 DAG 函数的编写:

    @dag(
            default_args=default_args,
            schedule_interval="@once",
            description="IMDB Dag",
            catchup=False,
            tags=['IMDB']
    )
    def IMDB_batch():
    

    该代码块集成了 DAG 的默认参数,并定义了一个调度间隔,使得 DAG 仅运行一次,还设置了一些元数据。

  3. 现在,我们将定义第一个任务,它会自动下载数据集并将其存储在 S3 中(第一行重复):

    def IMDB_batch():
        @task
        def data_acquisition():
            urls_dict = {
                "names.tsv.gz": "https://datasets.imdbws.com/name.basics.tsv.gz",
                "basics.tsv.gz": "https://datasets.imdbws.com/title.basics.tsv.gz",
                "crew.tsv.gz": "https://datasets.imdbws.com/title.crew.tsv.gz",
                "principals.tsv.gz": "https://datasets.imdbws.com/title.principals.tsv.gz",
                "ratings.tsv.gz": "https://datasets.imdbws.com/title.ratings.tsv.gz"
            }
            for title, url in urls_dict.items():
                response = requests.get(url, stream=True)
                with open(f"/tmp/{title}", mode="wb") as file:
                    file.write(response.content)
                s3.upload_file(f"/tmp/{title}", "bdok-<YOUR_ACCOUNT_NUMBER>", f"landing/imdb/{title}")
            return True
    

    这段代码来源于我们在 第五章 中开发的内容,最后做了一个小修改,用于将下载的文件上传到 S3。

  4. 接下来,我们将调用 Spark 处理作业来转换数据。第一步仅仅是读取数据的原始格式(TSV),并将其转换为 Parquet(这种格式在 Spark 中优化了存储和处理)。首先,我们定义一个 TaskGroup 实例,以便更好地组织任务:

    with TaskGroup("tsvs_to_parquet") as tsv_parquet:
        tsvs_to_parquet = SparkKubernetesOperator(
            task_id="tsvs_to_parquet",
            namespace="airflow",
            #application_file=open(f"{APP_FILES_PATH}/spark_imdb_tsv_parquet.yaml").read(),
            application_file="spark_imdb_tsv_parquet.yaml",
            kubernetes_conn_id="kubernetes_default",
            do_xcom_push=True
        )
        tsvs_to_parquet_sensor = SparkKubernetesSensor(
            task_id="tsvs_to_parquet_sensor",
            namespace="airflow",
            application_name="{{ task_instance.xcom_pull(task_ids='tsvs_to_parquet.tsvs_to_parquet')['metadata']['name'] }}",
            kubernetes_conn_id="kubernetes_default"
        )
        tsvs_to_parquet >> tsvs_to_parquet_sensor
    

    在这个组内,有两个任务:

    • tsvs_to_parquet:这是一个 SparkKubernetesOperator 任务,在 Kubernetes 上运行一个 Spark 作业。该作业在 spark_imdb_tsv_parquet.yaml 文件中定义,包含 Spark 应用程序的配置。我们使用 do_xcom_push=True 参数,使得该任务与后续任务之间能够进行跨任务通信。

    • tsvs_to_parquet_sensor:这是一个 SparkKubernetesSensor 任务,它监视由 tsvs_to_parquet 任务启动的 Spark 作业。它通过 task_instance.xcom_pull 方法,从前一个任务推送的元数据中获取 Spark 应用程序名称。此传感器在 Spark 作业完成之前不会允许 DAG 继续执行后续任务。

    tsvs_to_parquet >> tsvs_to_parquet_sensor 这一行设置了任务依赖关系,确保 tsvs_to_parquet_sensor 任务在 tsvs_to_parquet 任务成功完成后运行。

  5. 接下来,我们将进行另一次数据处理,这次使用 Spark。我们将连接所有表,构建一个合并的唯一表。这个合并后的表被称为 TaskGroup 实例,名为 Transformations,并按与之前代码块相同的方式进行处理:

    with TaskGroup('Transformations') as transformations:
        consolidated_table = SparkKubernetesOperator(
            task_id='consolidated_table',
            namespace="airflow",
            application_file="spark_imdb_consolidated_table.yaml",
            kubernetes_conn_id="kubernetes_default",
            do_xcom_push=True
        )
        consolidated_table_sensor = SparkKubernetesSensor(
            task_id='consolidated_table_sensor',
            namespace="airflow",
            application_name="{{ task_instance.xcom_pull(task_ids='Transformations.consolidated_table')['metadata']['name'] }}",
            kubernetes_conn_id="kubernetes_default"
        )
        consolidated_table >> consolidated_table_sensor
    
  6. 最后,在数据处理并写入 Amazon S3 后,我们将触发一个 Glue 爬虫,它会将该表的元数据写入 Glue 数据目录,使其可供 Trino 使用:

    glue_crawler_consolidated = GlueCrawlerOperator(
        task_id='glue_crawler_consolidated',
        region_name='us-east-1',
        aws_conn_id='aws_conn',
        wait_for_completion=True,
        config = {'Name': 'imdb_consolidated_crawler'}
    )
    

    记住,这些代码应该缩进以便位于 IMDB_Batch() 函数内。

  7. 现在,在这段代码的最后一个块中,我们将配置任务之间的依赖关系以及 TaskGroup 实例,并触发 DAG 函数的执行:

        da = data_acquisition()
        da >> tsv_parquet >> transformations
        transformations >> glue_crawler_consolidated
    execution = IMDB_batch()
    

现在,我们需要设置 Airflow 将调用的两个 SparkApplication 实例,以及 AWS 上的 Glue 爬虫。我们开始吧。

创建 SparkApplication 任务

我们将遵循 第八章 中使用的相同模式来配置 Spark 任务。我们需要存储在 S3 中的 PySpark 代码,以及定义任务的 YAML 文件,该文件必须与 Airflow DAG 代码一起位于 dags 文件夹中:

  1. 由于 YAML 文件与我们之前做的非常相似,我们不会在这里深入讨论。两个 YAML 文件的代码可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/batch/dags 文件夹中找到。创建这些文件并将它们分别保存为 spark_imdb_tsv_parquet.yamlspark_imdb_consolidated_table.yamldags 文件夹中。

  2. 接下来,我们将查看 PySpark 代码。第一个任务相当简单,它从 Airflow 导入的 TSV 文件中读取数据,并将相同的数据转换后写回 Parquet 格式。首先,我们导入 Spark 模块,并定义一个带有必要配置的 SparkConf 对象,用于 Spark 应用程序:

    from pyspark import SparkContext, SparkConf
    from pyspark.sql import SparkSession
    conf = (
        SparkConf()
            .set("spark.cores.max", "2")
            .set("spark.executor.extraJavaOptions", "-Dcom.amazonaws.services.s3.enableV4=true")
            .set("spark.driver.extraJavaOptions", "-Dcom.amazonaws.services.s3.enableV4=true")
            .set("spark.hadoop.fs.s3a.fast.upload", True)
            .set("spark.hadoop.fs.s3a.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem")
            .set("spark.hadoop.fs.s3a.aws.credentials.provider", "com.amazonaws.auth.EnvironmentVariablesCredentials")
            .set("spark.jars.packages", "org.apache.hadoop:hadoop-aws:2.7.3")
    )
    sc = SparkContext(conf=conf).getOrCreate()
    

    这些配置是针对与 Amazon S3 配合使用而定制的,启用了某些功能,例如 S3 V4 认证、快速上传和使用 S3A 文件系统实现。spark.cores.max 属性限制应用程序使用的最大核心数为 2。最后一行创建了一个带有之前定义的配置的 SparkContext 对象。

  3. 接下来,我们创建一个 SparkSession 实例并将日志级别设置为 "WARN",这样日志中只会显示警告和错误信息。这有助于提高日志的可读性:

    if __name__ == "__main__":
        spark = SparkSession.builder.appName("SparkApplicationJob").getOrCreate()
        spark.sparkContext.setLogLevel("WARN")
    
  4. 接下来,我们将定义表格架构。在处理大数据集时,这一点非常重要,因为它可以提高 Spark 在处理基于文本的文件(如 TSV、CSV 等)时的性能。接下来,我们仅展示第一个表格的架构,以简化可读性。完整代码可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/batch/spark_code 文件夹中找到:

    schema_names = "nconst string, primaryName string, birthYear int, deathYear int, primaryProfession string, knownForTitles string"
    
  5. 现在,我们将表格读取到 Spark DataFrame 中(这里只显示读取第一个表格):

    names = (
        spark
        .read
        .schema(schema_names)
        .options(header=True, delimiter="\t")
        .csv('s3a://bdok-<ACCOUNT_NUMBER>/landing/imdb/names.tsv.gz')
    )
    
  6. 接下来,我们将把表格写回 S3,以 Parquet 格式存储:

    names.write.mode("overwrite").parquet("s3a://bdok-<ACCOUNT_NUMBER>/bronze/imdb/names")
    
  7. 最后,我们停止 Spark 会话并释放应用程序使用的任何资源:

    spark.stop()
    
  8. 将此文件保存为 spark_imdb_tsv_parquet.py 并上传到你在 YAML 文件中定义的 S3 存储桶中(在本例中为 s3a://bdok-<ACCOUNT_NUMBER>/spark-jobs/)。

  9. 现在,我们将定义第二个 SparkApplication 实例,负责构建 OBT。对于第二段代码,我们将跳过 Spark 配置和 SparkSession 代码块,因为它们与上一个任务几乎相同,唯一需要导入的是一个额外的模块:

    from pyspark.sql import functions as f
    

    这将导入 functions 模块,允许使用 Spark 内部函数进行数据转换。

  10. 我们从这里开始读取数据集:

    names = spark.read.parquet("s3a://bdok-<ACCOUNT_NUMBER>/bronze/imdb/names")
    basics = spark.read.parquet("s3a://bdok-<ACCOUNT_NUMBER>/bronze/imdb/basics")
    crew = spark.read.parquet("s3a://bdok-<ACCOUNT_NUMBER>/bronze/imdb/crew")
    principals = spark.read.parquet("s3a://bdok-<ACCOUNT_NUMBER>/bronze/imdb/principals")
    ratings = spark.read.parquet("s3a://bdok-<ACCOUNT_NUMBER>/bronze/imdb/ratings")
    
  11. names数据集中的knownForTitles列和crew数据集中的directors列,在同一个单元格中有多个值,需要进行展开,才能得到每个导演和标题的一行数据:

    names = names.select(
        'nconst', 'primaryName', 'birthYear', 'deathYear',
        f.explode(f.split('knownForTitles', ',')).alias('knownForTitles')
    )
    crew = crew.select(
        'tconst', f.explode(f.split('directors', ',')).alias('directors'), 'writers'
    )
    
  12. 现在,我们开始进行表连接:

    basics_ratings = basics.join(ratings, on=['tconst'], how='inner')
    principals_names = (
        principals.join(names, on=['nconst'], how='inner')
        .select('nconst', 'tconst','ordering', 'category', 'characters', 'primaryName', 'birthYear', 'deathYear')
        .dropDuplicates()
    )
    directors = (
        crew
        .join(names, on=crew.directors == names.nconst, how='inner')
        .selectExpr('tconst', 'directors', 'primaryName as directorPrimaryName',
                    'birthYear as directorBirthYear', 'deathYear as directorDeathYear')
        .dropDuplicates()
    )
    

    在这里,我们执行三次连接操作:(a)通过在basicsratings DataFrame 中基于tconst列(电影标识符)连接,创建basics_ratings;(b)通过在principalsnames DataFrame 中基于nconst列(演员标识符)连接,创建principals_names;我们选择一些特定的列并删除重复项;(c)通过在crewnames DataFrame 中基于crew中的directors列与names中的nconst列连接,创建directors表。接着,我们选择特定的列,重命名一些列以便识别哪些数据与导演相关,并删除重复项。

  13. 接下来,我们将创建一个basics_principals表,通过连接crewprincipals_names数据集,获取有关剧组和电影表演者的完整数据集。最后,我们创建一个basics_principals_directors表,连接directors表信息:

    basics_principals = basics_ratings.join(principals_names, on=['tconst'], how='inner').dropDuplicates()
    basics_principals_directors = basics_principals.join(directors, on=['tconst'], how='inner').dropDuplicates()
    
  14. 最后,我们将这个最终表作为 Parquet 文件写入 Amazon S3 并停止 Spark 作业:

    basics_principals_directors.write.mode("overwrite").parquet("s3a://bdok-<ACCOUNT_NUMBER>/silver/imdb/consolidated")
    spark.stop()
    

最后一步是创建一个 Glue 爬虫,使 OBT 中的信息可供 Trino 使用。

创建一个 Glue 爬虫

我们将使用 AWS 控制台创建一个 Glue 爬虫。请按照以下步骤操作:

  1. 登录到 AWS 并进入AWS Glue页面。然后,点击侧边菜单中的爬虫,并点击创建爬虫图 10.2):

图 10.2 – AWS Glue:爬虫页面

图 10.2 – AWS Glue:爬虫页面

  1. 接下来,输入imdb_consolidated_crawler(与 Airflow 代码中引用的名称相同)作为爬虫名称,并根据需要填写描述。点击下一步

  2. 确保在第一个配置项您的数据是否已映射到 Glue 表?中选中尚未。然后,点击添加数据源图 10.3):

图 10.3 – 添加数据源

图 10.3 – 添加数据源

  1. s3://bdok-<ACCOUNT_NUMBER>/silver/imdb/consolidated中,如图 10.4所示。点击添加 S3 数据源,然后点击下一步

图 10.4 – S3 路径配置

图 10.4 – S3 路径配置

  1. 在下一页,点击创建新 IAM 角色,并为其填写您喜欢的名称。确保它不是已有的角色名称。点击下一步图 10.5):

图 10.5 – IAM 角色配置

图 10.5 – IAM 角色配置

  1. 在下一页,您可以选择我们在第九章中创建的相同数据库来与 Trino(bdok-database)一起使用。为了方便定位此表,请使用imdb-图 10.6)。将Crawler 调度设置为按需。点击下一步

图 10.6 – 目标数据库和表名前缀

图 10.6 – 目标数据库和表名前缀

  1. 在最后一步,检查所有提供的信息。如果一切正确,点击 创建爬虫

就这样!设置完成。现在,我们返回到浏览器中的 Airflow UI,激活 DAG,看看“魔法”发生了 (图 10**.7):

图 10.7 – 在 Airflow 上运行完整的 DAG

图 10.7 – 在 Airflow 上运行完整的 DAG

DAG 执行成功后,等待大约 2 分钟,直到爬虫停止,然后我们用 DBeaver 搜索我们的数据。我们来玩一下,搜索所有《约翰·威克》电影 (图 10**.8):

图 10.8 – 使用 DBeaver 检查 Trino 中的 OBT

图 10.8 – 使用 DBeaver 检查 Trino 中的 OBT

看,搞定了!你刚刚运行了完整的批量数据处理管道,连接了我们迄今为止学习的所有批量工具。恭喜!现在,我们将开始构建一个使用 Kafka、Spark Streaming 和 Elasticsearch 的数据流管道。

构建实时数据管道

对于实时管道,我们将使用我们在 第八章 中使用的相同数据模拟代码,结合一个增强的架构。在 图 10**.9 中,你可以看到我们即将构建的管道架构设计:

图 10.9 – 实时数据管道架构

图 10.9 – 实时数据管道架构

首先,我们需要在 AWS 上创建一个 虚拟私有云 (VPC) —— 一个私有网络 —— 并设置一个 关系数据库服务 (RDS) Postgres 数据库,作为我们的数据源:

  1. 进入 AWS 控制台,导航到 VPC 页面。在 VPC 页面上,点击 创建 VPC,你将进入配置页面。

  2. 确保 bdok10.20.0.0/16 无类域间路由 (CIDR) 块中。其余设置保持默认 (图 10**.10):

图 10.10 – VPC 基本配置

图 10.10 – VPC 基本配置

  1. 现在,滚动页面。你可以保持 可用区 (AZs) 和子网配置不变(两个 AZ,两个公共子网和两个私有子网)。确保为 网络地址转换 (NAT) 网关勾选 在 1 个 AZ 中。保持 S3 网关 选项框选中 (图 10**.11)。此外,最后两个 DNS 选项也保持勾选。点击 创建 VPC

图 10.11 – NAT 网关配置

图 10.11 – NAT 网关配置

  1. 创建 VPC 可能需要几分钟。成功创建后,在 AWS 控制台中,导航到 RDS 页面,在侧边菜单中点击 数据库,然后点击 创建数据库

  2. 在下一个页面中,选择标准创建,并选择Postgres作为数据库类型。保持默认的引擎版本。在模板部分,选择免费层,因为我们仅需要一个小型数据库用于本练习。

  3. bdok-postgres 中,对于凭证,保持postgres为主用户名,选择自管理作为凭证管理选项,并选择一个主密码(图 10.12):

图 10.12 – 数据库名称和凭证

图 10.12 – 数据库名称和凭证

  1. 实例配置存储部分保持为默认设置。

  2. bdok-vpc 中,保持 bdok-database-sg 作为安全组名称(图 10.13):

图 10.13 – RDS 子网和安全组配置

图 10.13 – RDS 子网和安全组配置

  1. 确保数据库认证部分标记为密码认证。其他设置可以保持默认。最后,AWS 会给出如果我们让这个数据库运行 30 天的费用估算(图 10.14)。最后,点击创建数据库,并等待几分钟以完成数据库创建:

图 10.14 – 数据库费用估算

图 10.14 – 数据库费用估算

  1. 最后,我们需要更改数据库安全组的配置,以允许来自 VPC 外部的连接,除了您自己的 IP 地址(默认配置)。进入 bdok-postgres 数据库。点击安全组名称以打开其页面(图 10.15):

图 10.15 – bdok-postgres 数据库查看页面

图 10.15 – bdok-postgres 数据库查看页面

  1. 在安全组页面中,选择安全组后,向下滚动页面并点击入站规则选项卡。点击编辑入站规则图 10.16):

图 10.16 – 安全组页面

图 10.16 – 安全组页面

  1. 在下一个页面中,您将看到一个已经配置好的入口规则,源为您的 IP 地址。将其更改为任何地方-IPv4,并点击保存规则图 10.17):

图 10.17 – 安全规则配置

图 10.17 – 安全规则配置

  1. 最后,为了向我们的数据库填充一些数据,我们将使用 simulatinos.py 代码生成一些虚假的客户数据并将其导入到数据库中。代码可以在github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/streaming 文件夹中找到。要运行它,请从 AWS 页面复制数据库端点,然后在终端中输入以下命令:

    python simulations.py --host <YOUR-DATABASE-ENDPOINT> -p <YOUR-PASSWORD>
    

在代码在终端打印出一些数据后,使用Ctrl + C 停止该进程。现在,我们可以开始处理数据管道了。让我们从 Kafka Connect 配置开始。

部署 Kafka Connect 和 Elasticsearch

为了让 Kafka 能够访问 Elasticsearch,我们需要在与 Kafka 部署相同的命名空间中部署另一个 Elasticsearch 集群。为此,我们将使用两个 YAML 配置文件,elastic_cluster.yamlkibana.yaml。这两个文件可以在github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/streaming/elastic_deployment文件夹中找到。请按照以下步骤操作:

  1. 首先,下载这两个文件并在终端中运行以下命令:

    kubectl apply -f elastic_cluster.yaml -n kafka
    kubectl apply -f kibana.yaml -n kafka
    
  2. 接下来,我们将使用以下命令获取一个自动生成的 Elasticsearch 密码:

    kubectl get secret elastic-es-elastic-user -n kafka -o go-template='{{.data.elastic | base64decode}}'
    

    该命令将在终端中打印出密码。请保存以供后用。

  3. Elasticsearch 仅在传输加密时工作。这意味着我们必须配置证书和密钥,允许 Kafka Connect 正确连接到 Elastic。为此,首先,我们将获取 Elastic 的证书和密钥,并使用以下命令将它们保存在本地:

    kubectl get secret elastic-es-http-certs-public -n kafka --output=go-template='{{index .data "ca.crt" | base64decode}}' > ca.crt
    kubectl get secret elastic-es-http-certs-public -n kafka --output=go-template='{{index .data "tls.crt" | base64decode}}' > tls.crt
    kubectl get secret elastic-es-http-certs-internal -n kafka --output=go-template='{{index .data "tls.key" | base64decode}}' > tls.key
    

    这将在本地创建三个文件,分别命名为ca.crttls.crttls.key

  4. 现在,我们将使用这些文件创建一个keystore.jks文件,该文件将用于 Kafka Connect 集群。在终端中运行以下命令:

    openssl pkcs12 -export -in tls.crt -inkey tls.key -CAfile ca.crt -caname root -out keystore.p12 -password pass:BCoqZy82BhIhHv3C -name es-keystore
    keytool -importkeystore -srckeystore keystore.p12 -srcstoretype PKCS12 -srcstorepass BCoqZy82BhIhHv3C -deststorepass OfwxynZ8KATfZSZe -destkeypass OfwxynZ8KATfZSZe -destkeystore keystore.jks -alias es-keystore
    

    请注意,我设置了一些随机密码。你可以根据自己的需要选择其他密码。现在,你已经有了我们需要配置传输加密的文件keystore.jks

  5. 接下来,我们需要使用keystore.jks文件在 Kubernetes 中创建一个 secret。为此,在终端中输入以下命令:

    kubectl create secret generic es-keystore --from-file=keystore.jks -n kafka
    
  6. 现在,我们准备好部署 Kafka Connect 了。我们有一个已准备好的配置文件,名为connect_cluster.yaml,可以在github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/streaming文件夹中找到。然而,这段代码有两个部分值得注意。在第 13 行,我们有spec.bootstrapServers参数。这个参数应该填写由 Helm chart 创建的 Kafka 启动服务。要获取该服务的名称,输入以下命令:

    kubectl get svc -n kafka
    

    检查服务名称是否与代码中的名称匹配。如果不匹配,请相应调整。保持此服务的9093端口。

  7. 第 15 行,你有spec.tls.trustedCertificates参数。secretName的值应该与 Helm chart 创建的ca-cert secret 的确切名称匹配。使用以下命令检查此 secret 的名称:

    kubectl get secret -n kafka
    

    如果 secret 的名称不匹配,请相应调整。保持certificate参数中的ca.crt值。

  8. 最后值得一提的是,我们将在 Kafka Connect 的 Pod 中将之前创建的es-keystore secret 作为卷进行挂载。以下代码块设置了此配置:

      externalConfiguration:
        volumes:
          - name: es-keystore-volume
            secret:
              secretName: es-keystore
    

    此 secret 必须作为卷挂载,以便 Kafka Connect 可以导入连接到 Elasticsearch 所需的 secret。

  9. 要部署 Kafka Connect,在终端中输入以下命令:

    kubectl apply -f connect_cluster.yaml -n kafka
    

    Kafka Connect 集群将在几分钟内准备就绪。准备好后,接下来是配置Java 数据库连接JDBC)源连接器,从 Postgres 数据库中拉取数据。

  10. 接下来,准备一个配置 JDBC 源连接器的 YAML 文件。接下来,您将找到此文件的代码:

    jdbc_source.yaml

    apiVersion: "kafka.strimzi.io/v1beta2"
    kind: "KafkaConnector"
    metadata:
      name: "jdbc-source"
      namespace: kafka
      labels:
        strimzi.io/cluster: kafka-connect-cluster
    spec:
      class: io.confluent.connect.jdbc.JdbcSourceConnector
      tasksMax: 1
      config:
        key.converter: org.apache.kafka.connect.json.JsonConverter
        value.converter: org.apache.kafka.connect.json.JsonConverter
        key.converter.schemas.enable: true
        value.converter.schemas.enable: true
        connection.url: «jdbc:postgresql://<DATABASE_ENDPOINT>:5432/postgres»
        connection.user: postgres
        connection.password: "<YOUR_PASSWORD>"
        connection.attempts: "2"
        query: "SELECT * FROM public.customers"
        mode: "timestamp"
        timestamp.column.name: "dt_update"
        topic.prefix: "src-customers"
        valincrate.non.null: "false"
    

    连接器的配置指定它应该使用来自 Confluent 的 JDBC 连接器库中的io.confluent.connect.jdbc.JdbcSourceConnector类。它将连接器的最大任务数(并行工作线程)设置为 1。连接器配置为使用 JSON 转换器处理键和值,并包括模式信息。它连接到运行在 Amazon RDS 实例上的 PostgreSQL 数据库,使用提供的连接 URL、用户名和密码。指定了SELECT * FROM public.customers SQL 查询,这意味着连接器将持续监控customers表,并将任何新行或更新行作为 JSON 对象流式传输到名为src-customers的 Kafka 主题中。mode值设置为timestamp,这意味着连接器将使用时间戳列(dt_update)来追踪哪些行已经处理过,从而避免重复。最后,validate.non.null选项设置为false,这意味着如果遇到数据库行中的null值,连接器不会失败。

  11. 将 YAML 文件放在名为connectors的文件夹中,并使用以下命令部署 JDBC 连接器:

    kubectl apply -f connectors/jdbc_source.yaml -n kafka
    

    您可以使用以下命令检查连接器是否已正确部署:

    kubectl get kafkaconnector -n kafka
    kubectl describe kafkaconnector jdbc-source -n kafka
    

    我们还将使用以下命令检查消息是否正确地传递到分配的 Kafka 主题:

    kubectl exec kafka-cluster-kafka-0 -n kafka -c kafka -it -- bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic src-customers
    

您应该看到以 JSON 格式打印在屏幕上的消息。太棒了!我们已经与源数据库建立了实时连接。现在,是时候使用 Spark 设置实时处理层了。

实时处理与 Spark

为了正确连接 Spark 和 Kafka,我们需要在 Kafka 的命名空间中设置一些授权配置:

  1. 以下命令创建了一个 Spark 的服务账户,并设置了运行SparkApplication实例所需的权限:

    kubectl create serviceaccount spark -n kafka
    kubectl create clusterrolebinding spark-role-kafka --clusterrole=edit --serviceaccount=kafka:spark -n kafka
    
  2. 接下来,我们需要确保在命名空间中设置了一个包含 AWS 凭证的秘密。使用以下命令检查秘密是否已存在:

    kubectl get secrets -n kafka
    

    如果秘密尚不存在,请使用以下命令创建它:

    kubectl create secret generic aws-credentials --from-literal=aws_access_key_id=<YOUR_ACCESS_KEY_ID> --from-literal=aws_secret_access_key="<YOUR_SECRET_ACCESS_KEY>" -n kafka
    
  3. 现在,我们需要构建一个 Spark Streaming 作业。为了实现这一点,正如之前所看到的,我们需要一个 YAML 配置文件和存储在 Amazon S3 中的 PySpark 代码。YAML 文件遵循与第八章中看到的相同模式。此配置的代码可在github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter10/streaming文件夹中找到。将其保存在本地,因为它将用于部署SparkApplication作业。

  4. Spark 作业的 Python 代码也可以在 GitHub 仓库中的第十章/streaming/processing文件夹中找到,文件名为spark_streaming_job.py。这段代码与我们在第七章中看到的非常相似,但有一些地方值得注意。在第 61 行,我们对数据进行实时转换。在这里,我们仅仅根据出生日期计算人的年龄,使用以下代码:

    query = (
            newdf
            .withColumn("dt_birthdate", f.col("birthdate"))
            .withColumn("today", f.to_date(f.current_timestamp() ) )
            .withColumn("age", f.round(
                f.datediff(f.col("today"), f.col("dt_birthdate"))/365.25, 0)
            )
            .select("name", "gender", "birthdate", "profession", "age", "dt_update")
        )
    
  5. 为了使 Elasticsearch sink 连接器正确读取主题中的消息,消息必须采用标准的 Kafka JSON 格式,并包含两个键:schemapayload。在代码中,我们将手动构建该 schema 并将其与最终版本的 JSON 数据连接起来。第 70 行定义了schema键和payload结构的开头(此行为了提高可读性,在此不会打印)。

  6. 第 72 行,我们将 DataFrame 的值转换为单个 JSON 字符串,并将其设置为名为value的列:

        json_query = (
            query
            .select(
                f.to_json(f.struct(f.col("*")))
            )
            .toDF("value")
        )
    
  7. 第 80 行,我们将之前定义的schema键与数据的实际值连接成 JSON 字符串,并将其写入流式查询,返回 Kafka 中的customers-transformed主题:

        (
            json_query
            .withColumn("value", f.concat(f.lit(write_schema), f.col("value"), f.lit('}')))
            .selectExpr("CAST(value AS STRING)")
            .writeStream
            .format("kafka")
            .option("kafka.bootstrap.servers", "kafka-cluster-kafka-bootstrap:9092")
            .option("topic", "customers-transformed")
            .option("checkpointLocation", "s3a://bdok-<ACCOUNT-NUMBER>/spark-checkpoint/customers-processing/")
            .start()
            .awaitTermination()
        )
    
  8. 将此文件保存为spark_streaming_job.py并保存到我们在 YAML 文件中定义的 S3 存储桶中。现在,你已经准备好开始实时处理了。要启动流式查询,请在终端中输入以下命令:

    kubectl apply -f spark_streaming_job.yaml -n kafka
    

    你还可以使用以下命令检查应用程序是否正确运行:

    kubectl describe sparkapplication spark-streaming-job -n kafka
    kubectl get pods -n kafka
    
  9. 现在,检查消息是否正确写入新主题,使用以下命令:

    kubectl exec kafka-cluster-kafka-0 -n kafka -c kafka -it -- bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic customers-transformed
    

就是这样!我们已经启动了实时处理层。现在,是时候部署 Elasticsearch sink 连接器并将最终数据导入 Elastic 了。让我们开始吧。

部署 Elasticsearch sink 连接器

在这里,我们将从 Elasticsearch sink 连接器的 YAML 配置文件开始。大部分“重头戏”已经在之前的秘密配置中完成了:

  1. connectors文件夹下创建一个名为es_sink.yaml的文件。以下是代码:

    es_sink.yaml

    apiVersion: "kafka.strimzi.io/v1beta2"
    kind: "KafkaConnector"
    metadata:
      name: "es-sink"
      namespace: kafka
      labels:
        strimzi.io/cluster: kafka-connect-cluster
    spec:
      class: io.confluent.connect.elasticsearch.ElasticsearchSinkConnector
      tasksMax: 1
      config:
        topics: "customers-transformed"
        connection.url: "https://elastic-es-http.kafka:9200"
        connection.username: "elastic"
        connection.password: "w6MR9V0SNLD79b56arB9Q6b6"
        batch.size: 1
        key.ignore: "true"
        elastic.security.protocol: "SSL"
        elastic.https.ssl.keystore.location: "/opt/kafka/external-configuration/es-keystore-volume/keystore.jks"
        elastic.https.ssl.keystore.password: "OfwxynZ8KATfZSZe"
        elastic.https.ssl.key.password: "OfwxynZ8KATfZSZe"
        elastic.https.ssl.keystore.type: "JKS"
        elastic.https.ssl.truststore.location: "/opt/kafka/external-configuration/es-keystore-volume/keystore.jks"
        elastic.https.ssl.truststore.password: "OfwxynZ8KATfZSZe"
        elastic.https.ssl.truststore.type: "JKS"
    

    我认为这里值得注意的部分是从第 20 行开始。在这里,我们正在配置与 Elasticsearch 连接的 SSL/TLS 设置。keystore.locationtruststore.location属性分别指定keystoretruststore文件的路径(在本例中,它们是相同的)。keystore.passwordkey.passwordtruststore.password属性提供访问这些文件的密码。keystore.typetruststore.type属性指定keystoretruststore文件的类型,在本例中是JKS(Java KeyStore)。

  2. 现在,一切已准备好启动此连接器。在终端中,输入以下命令:

    kubectl apply -f connectors/es_sink.yaml -n kafka
    

    你还可以使用以下命令检查连接器是否正确部署:

    kubectl describe kafkaconnector es-sink -n kafka
    
  3. 现在,获取负载均衡器的 URL 并访问 Elasticsearch UI。让我们看看数据是否已经正确导入:

    kubectl get svc -n kafka
    
  4. 一旦登录到 Elasticsearch,选择 GET _cat/indices 命令。如果一切正常,新的 customers-transformed 索引将显示在输出中(图 10.18):

图 10.18 – 在 Elasticsearch 中创建的新索引

图 10.18 – 在 Elasticsearch 中创建的新索引

  1. 现在,让我们使用这个索引创建一个新的数据视图。在侧边菜单中,选择 堆栈管理 并点击 数据视图。点击 创建数据视图 按钮。

  2. 将数据视图名称设置为 customers-transformed,并再次将 customers-transformed 设置为索引模式。选择 dt_update 列作为时间戳字段。然后,点击 保存数据视图到 Kibana图 10.19):

图 10.19 – 在 Kibana 上创建数据视图

图 10.19 – 在 Kibana 上创建数据视图

  1. 现在,让我们检查数据。在侧边菜单中,选择 customers-transformed 数据视图。记得将日期过滤器设置为合理的值,例如一年前。如果你在进行较大的索引操作,可以尝试一些基于时间的数据子集。数据应该会在 UI 中显示(图 10.20):

图 10.20 – 在 Kibana 中显示的 customers-transformed 数据

图 10.20 – 在 Kibana 中显示的 customers-transformed 数据

现在,通过再次运行 simulations.py 代码来添加更多数据。试着动动手,构建一些酷炫的仪表盘来可视化你的数据。

就这样!你刚刚在 Kubernetes 中使用 Kafka、Spark 和 Elasticsearch 运行了一个完整的实时数据管道。干杯,我的朋友!

总结

在本章中,我们将全书中学到的所有知识和技能结合起来,在 Kubernetes 上构建了两个完整的数据管道:一个批处理管道和一个实时管道。我们首先确保了所有必要的工具,如 Spark 运维工具、Strimzi 运维工具、Airflow 和 Trino,都已正确部署并在我们的 Kubernetes 集群中运行。

对于批处理管道,我们协调了整个过程,从数据获取和导入到 Amazon S3 数据湖,到使用 Spark 进行数据处理,最后将消费就绪的表格交付到 Trino。我们学习了如何创建 Airflow DAG,配置 Spark 应用程序,并无缝集成不同的工具来构建一个复杂的端到端数据管道。

在实时管道中,我们解决了实时处理和分析数据流的挑战。我们将 Postgres 数据库设置为数据源,部署了 Kafka Connect 和 Elasticsearch,并构建了一个 Spark Streaming 作业来对数据进行实时转换。然后,我们使用 sink 连接器将转换后的数据导入 Elasticsearch,使我们能够构建可以对事件进行响应的应用程序。

在整个章节中,我们通过编写 Python 和 SQL 代码来处理数据、编排和查询,获得了实战经验。我们还学习了集成不同工具的最佳实践,管理 Kafka 主题,以及将数据高效索引到 Elasticsearch 中。

通过完成本章的练习,您将掌握在 Kubernetes 上部署和编排构建大数据管道所需的所有工具的技能,连接这些工具以成功运行批处理和实时数据处理管道,并理解并应用构建可扩展、高效和可维护数据管道的最佳实践。

在接下来的章节中,我们将讨论如何使用 Kubernetes 部署生成式 AIGenAI)应用程序。

第十一章:Kubernetes 上的生成式 AI

生成式人工智能GenAI)已成为一项变革性技术,彻底改变了我们与 AI 的互动方式以及如何利用 AI。本章将带你探索生成式 AI 的精彩世界,学习如何在 Kubernetes 上利用其强大功能。我们将深入了解生成式 AI 的基础知识,并理解它与传统 AI 的主要区别。

我们的重点将放在利用 Amazon Bedrock,这是一个旨在简化生成式 AI 应用开发和部署的综合服务套件。通过实际操作示例,你将获得在 Kubernetes 上构建生成式 AI 应用的实践经验,并使用 Streamlit,一个强大的 Python 库,用于创建互动数据应用。我们将覆盖整个过程,从开发到将应用部署到 Kubernetes 集群。

此外,我们将探索 检索增强生成RAG)的概念,RAG 将生成式 AI 的力量与外部知识库结合起来。

最后,我们将介绍 Amazon BedrockAgents,这是一项强大的功能,允许你自动化任务并创建智能助手。你将学习如何构建代理,如何通过 OpenAPI 架构定义其能力,以及如何创建作为代理后端的 Lambda 函数。

到本章结束时,你将对生成式 AI 有一个扎实的理解,了解其应用以及构建和部署生成式 AI 应用到 Kubernetes 上所需的工具和技术。

本章将涵盖以下主要主题:

  • 什么是生成式 AI,什么不是

  • 使用 Amazon Bedrock 与基础模型进行工作

  • 在 Kubernetes 上构建生成式 AI 应用

  • 使用 Amazon Bedrock 构建 RAG 知识库

  • 使用代理构建行动模型

技术要求

本章需要一个 AWS 账户和一个正在运行的 Kubernetes 集群。我们还将使用 LangChain 和 Streamlit 库。虽然部署应用到 Kubernetes 时不需要它们,但如果你希望在本地测试代码并修改以适应自己的实验,建议安装这些库。

此外,为了获取 RAG 练习(第四部分)的数据,必须安装 Beautiful Soup 库。

本章的所有代码可以在 github.com/PacktPublishing/Bigdata-on-KubernetesChapter11 文件夹中找到。

什么是生成式 AI,什么不是

生成性 AI 的核心是指能够基于其所接触到的训练数据生成新的原创内容的 AI 系统,这些内容可以是文本、图像、音频或代码。生成性 AI 模型在大量现有内容的数据集上进行训练,并学习其中的模式和关系。在得到提示后,这些模型可以生成新的原创内容,这些内容类似于训练数据,但并不是任何特定示例的精确复制。

这与传统的机器学习模型形成对比,后者侧重于基于现有数据进行预测或分类。

传统的机器学习模型,例如用于图像识别、自然语言处理或预测分析的模型,旨在接收输入数据,并基于该数据进行预测或分类。机器学习模型擅长处理分类任务(例如,识别图像中的物体或文本中的主题)、回归任务(例如,根据面积和位置等特征预测房价)以及聚类任务(例如,根据相似行为模式将客户分组)。

例如,一个图像识别模型可能会基于大量带标签的图像数据集进行训练,学习识别和分类新图像中的物体。同样,一个自然语言处理模型可能会基于文本数据语料库进行训练,执行情感分析、命名实体识别或语言翻译等任务。

在信用风险评估场景中,一个机器学习模型会基于包含过去贷款申请者信息的数据集进行训练,例如他们的收入、信用历史和其他相关特征,以及是否违约的标签。该模型会学习这些特征与贷款违约结果之间的模式和关系。当遇到新的贷款申请时,经过训练的模型便可以预测该申请者违约的可能性。

在这些情况下,机器学习模型并不生成新内容;相反,它是在使用从训练数据中学习到的模式和关系,对新的、未见过的数据进行有根据的预测或决策。

相比之下,例如,经过大量文本语料库训练的生成性 AI 模型可以在任何给定的主题或所需的风格下生成类似人类的写作。同样,经过图像训练的模型可以根据文本描述或其他输入数据创建全新的、逼真的图像。

尽管生成性 AI 的最终结果是创造新的内容,但其基本机制仍然基于相同的机器学习原理:进行预测。然而,生成性 AI 模型不仅仅是预测单一输出(如分类或数值),它们被训练来预测序列中的下一个元素,无论这个序列是由单词、像素或任何其他类型的数据组成。

大型神经网络的力量

虽然预测序列中下一个元素的概念相对简单,但生成性人工智能模型生成连贯、高质量内容的能力在于所使用的神经网络的规模和复杂性。

生成性人工智能模型通常使用拥有数十亿甚至万亿个参数的大型深度神经网络。这些神经网络在大量数据上进行训练,通常涉及数百万或数十亿个示例,从而使模型能够捕捉数据中极其微妙的模式和关系。

例如,Anthropic 的模型,如 Claude,是在一个庞大的文本数据集上进行训练的,这些数据涵盖了广泛的主题和领域。这使得模型能够深入理解语言、上下文和领域特定的知识,从而生成不仅语法正确,而且在语义上连贯且与给定上下文相关的文本。

挑战与局限性

尽管生成性人工智能展现出了显著的能力,但它并非没有挑战和局限性。一个主要的担忧是,这些模型可能生成带有偏见、有害或误导性内容,尤其是在训练数据集包含反映社会偏见或不准确信息的情况下。

此外,生成式 AI 模型有时可能会产生无意义、不一致或事实不准确的输出,尽管它们可能在表面上看起来合乎逻辑且可信。这被称为“幻觉”问题,即模型生成的内容并未基于事实知识或提供的背景。以下是两个著名的真实案例。加拿大航空的 AI 聊天机器人向一名乘客提供了有关航空公司丧亲票价政策的误导性信息。该聊天机器人错误地表示,即使在旅行已发生后,乘客仍可追溯申请减价的丧亲票价,这与加拿大航空的实际政策相矛盾。乘客依赖该聊天机器人“幻觉”般的回应,最终在航空公司拒绝兑现聊天机器人建议时,成功提起小额索赔案件并获胜(www.forbes.com/sites/marisagarcia/2024/02/19/what-air-canada-lost-in-remarkable-lying-ai-chatbot-case/)。此外,巴西的一名联邦法官使用 ChatGPT AI 系统来研究他所撰写判决的法律先例。然而,AI 提供了伪造的信息,引用了不存在的最高法院裁决作为该法官判决的依据(g1.globo.com/politica/blog/daniela-lima/post/2023/11/13/juiz-usa-inteligencia-artificial-para-fazer-decisao-e-cita-jurisprudencia-falsa-cnj-investiga-caso.ghtml)。

尽管存在这些挑战,生成式 AI 仍是一个快速发展的领域,研究人员和开发者正在积极解决这些问题。诸如微调、提示工程和使用外部知识来源(例如,知识库或 RAG)等技术,正在被探索以提高生成式 AI 模型的可靠性、安全性和事实准确性。

在接下来的章节中,我们将深入探讨如何使用 Amazon Bedrock 及其基础模型、知识库和基于代理的架构来构建和部署生成式 AI 应用的实际操作。

使用 Amazon Bedrock 来与基础模型进行协作

Amazon Bedrock 提供了一套基础模型,可作为构建生成式 AI 应用的模块。了解每个模型的能力和预期使用场景非常重要,以便为你的应用选择合适的模型。

在 Amazon Bedrock 中可用的模型包括语言模型、计算机视觉模型和多模态模型。语言模型擅长理解和生成类人文本,可以用于文本摘要、问答和内容生成等任务。而计算机视觉模型则擅长分析和理解视觉数据,非常适合图像识别、物体检测和场景理解等应用。

如同名称所示,多模态模型可以同时处理多种模态。这使得它适用于图像标注、视觉问答和数据图表分析等任务。

需要注意的是,每个模型都有其自身的优点和局限性,选择模型时应根据应用的具体需求来决定。例如,如果你的应用主要处理基于文本的任务,那么像 Llama 这样的语言模型可能是最合适的选择。然而,如果你需要处理文本和图像,那么像 Claude 这样的多模态模型则会更为合适。

为了将 Amazon Bedrock 的基础模型有效集成到我们的生成型 AI 应用程序中,请按照以下步骤操作:

  1. 要使用 Amazon Bedrock 上的可用基础模型,首先需要激活它们。进入 AWS 控制台,搜索 Amazon Bedrock 页面。然后,点击 修改模型访问权限 (图 11.1)。

图 11.1 – 修改 Amazon Bedrock 上的模型访问权限

图 11.1 – 修改 Amazon Bedrock 上的模型访问权限

  1. 在下一页,选择 Claude 3 SonnetClaude 3 Haiku Anthropic 模型。这些是我们将用于生成型 AI 应用程序的基础模型。如果你愿意尝试和实验不同的模型,也可以选择所有可用的模型 (图 11.2)。

图 11.2 – 请求访问 Anthropic 的 Claude 3 模型

图 11.2 – 请求访问 Anthropic 的 Claude 3 模型

  1. 点击 下一步,在下一页上查看更改并点击 提交。这些模型可能需要几分钟才能获得访问权限。

一旦获得访问权限,我们就拥有了开发生成型 AI 应用程序所需的一切。让我们开始吧。

在 Kubernetes 上构建生成型 AI 应用程序

在这一节中,我们将使用 Streamlit 构建一个生成型 AI 应用程序。该应用程序架构的示意图如 图 11.3 所示。在这个应用程序中,用户将能够选择与之交互的基础模型。

图 11.3 – 基础模型的应用架构

图 11.3 – 基础模型的应用架构

让我们从应用程序的 Python 代码开始。完整的代码可以在 GitHub 上的 第十一章/streamlit-claude/app 文件夹中找到。我们将逐块分析代码:

  1. 创建一个名为app的文件夹,并在其中创建一个main.py代码文件。首先,我们导入必要的文件并创建一个客户端以访问 Amazon Bedrock 运行时 API:

    import boto3
    from langchain_community.chat_models import BedrockChat
    from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
    bedrock = boto3.client(service_name='bedrock-runtime', region_name="us-east-1")
    
  2. 接下来,我们定义一个重要的参数字典,用于与 Claude 进行交互:

    inference_modifier = {
        "max_tokens": 4096,
        "temperature": 0.5,
        "top_k": 250,
        "top_p": 1,
        "stop_sequences": ["\n\nHuman:"],
    }
    
  3. 接下来,我们将配置一个函数,以允许选择首选的基础模型。通过选择,我们将返回一个可以通过 Langchain 访问 Bedrock 的模型对象:

    def choose_model(option):
        modelId = ""
        if option == "Claude 3 Haiku":
            modelId = "anthropic.claude-3-haiku-20240307-v1:0"
        elif option == "Claude 3 Sonnet":
            modelId = "anthropic.claude-3-sonnet-20240229-v1:0"
        model = BedrockChat(
            model_id=modelId,
            client=bedrock,
            model_kwargs=inference_modifier,
            streaming=True,
            callbacks=[StreamingStdOutCallbackHandler()],
        )
        return model
    
  4. 现在,我们将添加一个小功能,用于重置对话历史记录:

    def reset_conversation():
        st.session_state.messages = []
    
  5. 接下来,我们将开始开发main函数,并为应用程序界面添加一些小部件。以下代码创建了一个侧边栏。在其中,我们添加了一个选择框,选项包括 Claude 3 Haiku 和 Claude 3 Sonnet,我们写了一个确认消息告诉用户他们正在与哪个模型对话,并添加了一个choose_model函数来返回连接到 Bedrock 的类,并写下应用程序的标题,Chat with Claude 3

    def main():
        with st.sidebar:
            option = st.selectbox(
                "What model do you want to talk to?",
                ("Claude 3 Haiku", "Claude 3 Sonnet")
            )
            st.write(f"You are talking to **{option}**")
            st.button('Reset Chat', on_click=reset_conversation)
        model = choose_model(option)
        st.title("Chat with Claude 3")
    
  6. 接下来,如果st.session_state中尚未存在聊天历史记录,我们将初始化一个空列表。st.session_state是一个 Streamlit 对象,可以在应用程序重新运行时保持数据的持久性。然后,我们遍历st.session_state中的messages列表,并在聊天消息容器中显示每条消息。st.chat_message函数根据指定的角色(例如userassistant)创建聊天消息容器。st.markdown函数在容器内显示消息内容:

    if "messages" not in st.session_state:
            st.session_state.messages = []
        for message in st.session_state.messages:
            with st.chat_message(message["role"]):
                st.markdown(message["content"])
    
  7. 接下来,我们处理用户输入并显示对话。st.chat_input函数创建一个输入框,用户可以在其中输入提示。如果用户输入了提示,将执行以下步骤:(1)将用户的提示以user角色添加到st.session_state中的messages列表中;(2)将用户的提示以user角色显示在聊天消息容器中;(3)调用model.stream(prompt)函数,将用户的提示发送到 Bedrock 模型并实时返回响应。st.write_stream函数实时显示流式响应;(4)助手的响应以assistant角色添加到st.session_state中的messages列表中:

        if prompt := st.chat_input("Enter your prompt here"):
            st.session_state.messages.append(
                {"role": "user", "content": prompt}
            )
            with st.chat_message("user"):
                st.markdown(prompt)
            with st.chat_message("assistant"):
                response = st.write_stream(
                    model.stream(prompt)
                )
            st.session_state.messages.append(
                {"role": "assistant", "content": response}
            )
    
  8. 最后,我们调用主函数来启动 Streamlit 应用程序:

    if __name__ == "__main__":
        main()
    

    如果你想在本地运行此应用程序,这里有一个requirements.txt文件:

    boto3==1.34.22
    langchain-community==0.0.33
    langchain==0.1.16
    streamlit==1.34.0
    pip install -r requirements.txt
    

    如果你已经安装了所需的库,使用aws configure命令对 AWS CLI 进行身份验证,然后通过以下命令在本地启动应用程序:

    streamlit run main.py
    

    这是在构建用于部署的容器镜像之前测试应用程序的绝妙方法。你可以随心所欲地测试和修改应用程序。

    准备好后,现在,让我们构建一个用于部署的容器镜像。

  9. 以下是一个简单的Dockerfile,用于构建镜像:

    Dockerfile

    FROM python:3.9-slim
    WORKDIR /app
    RUN apt-get update && apt-get install -y \
        build-essential \
        curl \
        software-properties-common \
        git \
        && rm -rf /var/lib/apt/lists/*
    COPY app /app/
    EXPOSE 8501
    HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
    RUN pip3 install -r requirements.txt
    ENTRYPOINT ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"]
    

    这个 Dockerfile 从 Python 3.9 slim 基础镜像开始,并将工作目录设置为 /app。它接着安装应用程序所需的各种系统包,例如 build-essentialcurlsoftware-properties-commongit。应用程序代码被复制到 /app 目录,容器暴露 8501 端口。健康检查会检查 Streamlit 应用程序是否在 localhost:8501/_stcore/health 正常运行。通过 pip3 安装所需的 Python 包,依据 requirements.txt 文件。最后,ENTRYPOINT 命令通过运行 streamlit run main.py 启动 Streamlit 应用程序,并指定服务器端口和地址。

  10. 要在本地构建镜像,请输入以下命令:

    docker build --platform linux/amd64 -t <YOUR_USERNAME>/chat-with-claude:v1 .
    

    记得将 <YOUR_USERNAME> 替换成你实际的 Docker Hub 用户名。然后,使用以下命令推送镜像:

    docker push <YOUR_USERNAME>/chat-with-claude:v1
    

记住,这个镜像将在 Docker Hub 上公开可用。不要在代码中或作为环境变量放入任何认证凭据或敏感数据!

现在,让我们在 Kubernetes 上部署我们的应用程序。

部署 Streamlit 应用程序

正如我们之前所看到的,要在 Kubernetes 上部署我们的应用程序,我们需要一个 Deployment 和一个 Service .yaml 定义。我们可以将两者合并到一个文件中:

  1. 首先,创建一个 deploy_chat_with_claude.yaml 文件,内容如下:

    deploy_chat_with_claude.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: chat-with-claude
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: chat-with-claude
      template:
        metadata:
          labels:
            app: chat-with-claude
        spec:
          containers:
          - name: chat-with-claude
            image: docker.io/neylsoncrepalde/chat-with-claude:v1
            ports:
            - containerPort: 8501
            env:
            - name: AWS_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: aws-credentials
                  key: aws_access_key_id
            - name: AWS_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: aws-credentials
                  key: aws_secret_access_key
    

    代码的第一部分定义了一个名为 chat-with-claudeDeployment 资源。它使用一个预先构建的镜像(你可以将其更改为自己的新镜像),并在容器中打开 8501 端口,供外部访问。spec.template.spec.containers.env 块会将 AWS 凭证作为环境变量挂载到容器中,这些凭证来自名为 aws-credentials 的密钥。

  2. 代码的第二部分定义了一个 LoadBalancer 服务,为 Deployment 中定义的 pod 提供服务,该服务监听 8501 端口并将流量转发到容器中的 8501 端口。别忘了 ---,它是分隔多个资源的必需项:

    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: chat-with-claude
    spec:
      type: LoadBalancer
      ports:
      - port: 8501
        targetPort: 8501
      selector:
        app: chat-with-claude
    
  3. 现在,我们将创建命名空间和密钥,并使用以下命令部署应用程序:

    kubectl create namespace genai
    kubectl create secret generic aws-credentials --from-literal=aws_access_key_id=<YOUR_ACCESS_KEY_ID> --from-literal=aws_secret_access_key="<YOUR_SECRET_ACCESS_KEY>" -n genai
    kubectl apply -f deploy_chat_with_claude.yaml -n genai
    
  4. 就这些。等待几分钟,直到 LoadBalancer 启动并运行,然后使用以下命令检查其 URL:

    kubectl get svc -n genai
    
  5. 现在,粘贴带有 :8501 的 URL 以定义正确的端口,瞧瞧!图 11.4)。

图 11.4 – Chat with Claude 3 应用程序 UI

图 11.4 – Chat with Claude 3 应用程序 UI

现在,和助手玩一玩。试试 Haiku 和 Sonnet,并注意它们在速度和回答质量上的差异。试几次之后,你会注意到,向基础模型提问特定问题会导致幻觉。比如问模型:“你是谁?”你会得到一个惊喜(还会笑出声)。这个模型需要上下文。

在下一节中,我们将使用 RAG 提供一些上下文。

使用知识库构建 Amazon Bedrock 的 RAG

RAG 是一种用于生成 AI 模型的技术,通过在生成过程中为基础模型提供额外的上下文和知识。其工作原理是首先从知识库或文档语料库中检索相关信息,然后使用这些检索到的信息来增强输入到生成模型的内容。

RAG 是为生成 AI 模型提供上下文的良好选择,因为它允许模型访问并利用外部知识源,这可以显著提高生成输出的质量、准确性和相关性。如果没有 RAG,模型将仅限于在训练过程中学到的知识和模式,而这些可能并不总是充足或最新的,尤其是对于特定领域或快速发展的主题。

RAG 的一个关键优势是它使模型能够利用大型知识库或文档集合,这些内容在模型的训练数据中包含是不现实或不可能的。这使得模型能够生成更为知情和专业的输出,因为它可以从大量相关信息中提取内容。此外,RAG 有助于缓解如幻觉和偏见等问题,因为模型可以访问权威和事实性的来源。

然而,RAG 也存在一些局限性。生成输出的质量在很大程度上依赖于检索信息的相关性和准确性,而这些因素会受到知识库质量、检索机制效果以及模型是否能够正确整合检索信息的影响。此外,RAG 还可能带来计算开销和延迟,因为它需要在生成过程之前执行额外的检索步骤。

要使用 RAG 构建 AI 助手,我们将使用亚马逊 Bedrock 服务中的知识库功能,这是 Bedrock 中允许你无缝创建和管理知识库的功能。让我们开始吧。

对于我们的练习,我们将构建一个能够提供 AWS 能力计划信息的 AI 助手。该助手架构的视觉表示如 图 11.5 所示:

图 11.5 – 亚马逊 Bedrock 应用架构的知识库

图 11.5 – 亚马逊 Bedrock 应用架构的知识库

AWS 能力计划是 AWS 提供的一个验证计划,旨在认可在特定解决方案领域中展示出技术熟练度和客户成功的合作伙伴。AWS 能力授予 AWS 合作伙伴网络 (APN) 成员,这些成员经过与特定 AWS 服务或工作负载相关的技术验证,确保他们具备提供一致且高质量解决方案的专业能力。这些能力涵盖了 DevOps、迁移、数据与分析、机器学习和安全等多个领域。每个能力都有自己的规则文档,理解起来可能相当具有挑战性。

  1. 首先,我们将收集一些有关该程序的上下文信息。在 GitHub 上的第十一章/claude-kb/knowledge-base/文件夹下,您将找到一段 Python 代码,该代码将收集关于对话式 AI、数据与分析、DevOps、教育、能源、金融服务、机器学习和安全程序的信息。将此代码保存到本地后,使用以下命令安装 Beautiful Soup 库:

    pip install "beautifulsoup4==4.12.2"
    python get_competency_data.py
    

    几秒钟后,数据应保存到您本地的机器上。

  2. 接下来,创建一个 S3 存储桶并上传这些文件。这将成为我们 RAG 层的基础。

  3. 接下来,进入 AWS 控制台中的Bedrock页面。在侧边菜单中,点击知识库,然后点击创建知识库图 11.6)。

图 11.6 – 知识库首页

图 11.6 – 知识库首页

  1. 在下一页面,选择一个名称为您的知识库,并在IAM 权限部分选择创建并使用新服务角色。然后,点击下一步

  2. 接下来,您将配置数据源。为数据源名称选择一个您喜欢的名称。对于数据源位置,确保选中了此 AWS 账户选项框。然后,在S3 URI部分,点击浏览 S3,搜索包含 AWS Competency 数据集的 S3 存储桶(我们在步骤 2中创建的存储桶)。该配置的示例如图 11.7所示。选择 S3 存储桶后,点击下一步

图 11.7 – 选择知识库的数据源

图 11.7 – 选择知识库的数据源

接下来,我们将选择嵌入模型。此嵌入模型负责将文本或图像文件转换为称为嵌入的向量表示。这些嵌入捕捉输入数据的语义和上下文信息,从而支持高效的相似性比较和检索操作。默认情况下,Bedrock 的嵌入模型 Amazon Titan 应该是可用的。如果不可用,请在控制台中按照相同的过程申请访问权限。

  1. 在下一页面的嵌入模型部分,选择Titan Embeddings G1 - Text。在向量数据库部分,确保选中了快速创建新的向量存储选项。此快速创建选项基于 OpenSearch Serverless 创建一个向量数据库。将其他选项保持未选中状态,然后点击下一步

注意

OpenSearch 是一个开源分布式搜索与分析引擎,基于 Apache Lucene,并来源于 Elasticsearch。它是 RAG 向量数据库的一个优秀选择,因为它提供了高效的全文搜索和最近邻搜索功能,适用于向量嵌入的检索。OpenSearch 支持稠密向量索引与检索,非常适合存储和查询大量的向量嵌入,这对于 RAG 模型的检索组件至关重要。

  1. 接下来,检查信息是否正确提供。如果一切看起来正常,点击创建知识库。请耐心等待,创建过程将需要几分钟才能完成。

  2. 知识库启动并运行后,返回 Bedrock 中的知识库页面,点击你刚创建的知识库。在下一个页面中,滚动直到找到数据源部分(如图 11.8所示)。选择数据源并点击同步,以开始嵌入文本内容。这也需要几分钟。

图 11.8 – 同步知识库与其数据源

图 11.8 – 同步知识库与其数据源

在“同步”准备好之后,我们已经具备了运行生成式 AI 助手与 RAG 所需的一切。现在,是时候调整代码,让 Claude 与知识库配合使用。

调整 RAG 检索代码

我们将从之前开发的代码开始,使用纯 Claude 模型。由于我们只需要进行一些小修改,因此不需要重新查看整个代码。我们将仔细看看必要的修改。RAG 应用程序的完整代码可以在github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter11/claude-kb/app文件夹中找到。如果你不想自定义代码,可以使用我为此示例提供的现成 docker 镜像。

  1. 首先,我们需要额外的导入:

    import os
    from botocore.client import Config
    from langchain.prompts import PromptTemplate
    from langchain.retrievers.bedrock import AmazonKnowledgeBasesRetriever
    from langchain.chains import RetrievalQA
    

    在这里,我们导入os库以获取环境变量。Config类将帮助构建一个配置对象,以访问bedrock-agent API。所有其他导入与访问知识库并将检索到的文档与 AI 响应合并有关。

  2. 接下来,我们将从环境变量中获取 Amazon Bedrock 服务的知识库 ID。这是一个非常有用的方法。如果将来需要更改知识库,就无需重新构建镜像,只需更改环境变量。然后,我们设置一些配置并为bedrock-agent-runtime API(用于知识库)创建一个客户端:

    kb_id = os.getenv("KB_ID")
    bedrock_config = Config(connect_timeout=120, read_timeout=120, retries={'max_attempts': 0})
    bedrock_agent_client = boto3.client(
        "bedrock-agent-runtime", config=bedrock_config, region_name = "us-east-1"
    )
    
  3. 接下来,我们将配置一个提示模板,帮助我们将从知识库中检索的文档与用户问题串联起来。最后,我们将实例化一个对象,该对象将保存模板,并接收文档和用户问题作为输入:

    PROMPT_TEMPLATE = """
    Human: You are a friendly AI assistant and provide answers to questions about AWS competency program for partners.
    Use the following pieces of information to provide a concise answer to the question enclosed in <question> tags.
    Don't use tags when you generate an answer. Answer in plain text, use bullets or lists if needed.
    If you don't know the answer, just say that you don't know, don't try to make up an answer.
    <context>
    {context}
    </context>
    <question>
    {question}
    </question>
    The response should be specific and use statistics or numbers when possible.
    Assistant:"""
    claude_prompt = PromptTemplate(template=PROMPT_TEMPLATE,
                                   input_variables=["context","question"])
    
  4. 设置完choose_model()函数后,我们需要实例化一个retriever类,用于从知识库中拉取文档:

    retriever = AmazonKnowledgeBasesRetriever(
            knowledge_base_id=kb_id,
            retrieval_config={
                "vectorSearchConfiguration": {
                    "numberOfResults": 4
                }
            },
            client=bedrock_agent_client
        )
    
  5. 现在,在main函数中,我们将添加RetrievalQA。这个类用于构建能够从知识库中检索相关信息的问答系统:

    qa = RetrievalQA.from_chain_type(
            llm=model,
            chain_type="stuff",
            retriever=retriever,
            return_source_documents=False,
            chain_type_kwargs={"prompt": claude_prompt}
        )
    
  6. 最后,我们将修改响应以提供完整的答案:

    with st.chat_message("assistant"):
        response = qa.invoke(prompt)['result']
        st.write(response)
    

    就这样。代码已经准备好,可以构建新的镜像。你可以通过创建一个新的 Dockerfile,使用之前的代码重新构建它。在运行 docker build 命令时,记得选择一个不同的镜像名称(或者至少选择一个不同的版本)。

  7. 接下来,我们将开始部署。.yaml 文件与上一节中的文件非常相似(但记得更改部署、服务、容器和标签的所有名称为 rag-with-claude)。该代码的完整版可在 GitHub 仓库中找到。我们只需声明知识库 ID 的环境变量。由于这不是敏感凭证,因此我们不需要使用 Kubernetes 秘密来处理它。我们将使用 ConfigMap.yaml 文件中的 spec.template.spec.container.env 部分应该如下所示:

    env:
      - name: AWS_ACCESS_KEY_ID
        valueFrom:
          secretKeyRef:
            name: aws-credentials
            key: aws_access_key_id
      - name: AWS_SECRET_ACCESS_KEY
        valueFrom:
          secretKeyRef:
            name: aws-credentials
            key: aws_secret_access_key
      - name: KB_ID
        valueFrom:
          configMapKeyRef:
            name: kb-config
            key: kb_id
    

    注意,我们添加了一个新的环境变量 KB_ID,它将从 ConfigMap 导入。

  8. 要部署新应用程序,我们运行以下命令:

    kubectl create configmap kb-config --from-literal=kb_id=<YOUR_KB_ID> -n genai
    kubectl apply -f deploy_chat_with_claude.yaml -n genai
    

    我们运行前面的命令来部署应用程序。等待几分钟,直到 LoadBalancer 启动,然后使用以下命令:

    kubectl get svc -n genai
    

    使用前面的命令获取 LoadBalancer 的 URL。将名为 rag-with-claude 的服务复制并粘贴到浏览器中,并添加 :8501 以连接到暴露的端口。瞧! 你应该能看到新应用程序在运行,正如图 11.9 所示。

图 11.9 – RAG 应用 UI

图 11.9 – RAG 应用 UI

尝试稍微玩一下这个应用程序。你会发现,如果你提问与其范围无关的问题(如 AWS 能力计划以外的内容),助手会告诉你它无法回答。

现在,我们将进入本章的最后部分,学习如何让生成性 AI 模型通过代理执行操作。

使用代理构建行动模型

代理是生成性 AI 世界中最新的功能。它们是强大的工具,通过允许生成性 AI 模型代表我们执行任务,实现了任务的自动化。它们充当生成性 AI 模型与外部系统或服务之间的中介,促进了现实世界任务的执行。

在后台,代理“理解”用户的需求,并调用执行操作的后端函数。代理能够执行的范围由 OpenAPI 架构定义,它将用于“理解”它的功能以及如何正确调用后端函数。

总结来说,要构建一个代理,我们需要一个 OpenAPI 架构、一个后端函数和一个知识库。知识库是可选的,但它能大大提升用户与 AI 助手的交互体验。

在本节的练习中,我们将构建一个“了解” AWS 能力计划相关信息的代理。该代理的应用架构的可视化表示如图 11.10所示。

图 11.10 – 代理应用架构

图 11.10 – 代理应用架构

这个代理将创建一个简单的工作表,包含用例信息,将工作表保存到 Amazon S3,并在 DynamoDB 表中注册该信息以供查询。我们开始吧:

  1. 首先,我们需要一个 OpenAPI 模式,定义代理可用的方法。在这种情况下,我们将定义两个方法。第一个方法 generateCaseSheet 注册用例信息并创建工作表。第二个方法 checkCase 接收用例 ID 并返回相关信息。由于这是一个较长的 JSON 文件,我们不会在此展示。完整的代码可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter11/agent 文件夹中找到。复制这段代码并将其保存在 S3 桶中。

  2. 然后,我们将定义一个 Lambda 函数,作为代理的后端。该函数的完整 Python 代码可以在书籍的 GitHub 仓库中找到,位于 第十一章/agent/function 文件夹中。在你的机器上,创建一个名为 function 的文件夹,并将此代码保存为 lambda_function.py。这段代码定义了一个 Lambda 函数,作为 Bedrock 代理的后端。该函数处理两个不同的 API 路径:/generateCaseSheet/checkCase。让我们逐块分析这段代码。在导入必要的文件夹之后,我们定义了两个辅助函数,从事件对象中提取参数值(get_named_parameterget_named_property)。generateCaseSheet 函数负责根据提供的信息创建一个新的用例表格。它从事件对象中提取所需的参数,生成一个唯一的 ID,使用 CaseTemplate 类创建一个新的 Excel 工作簿,将提供的参数填充到模板中,保存工作簿到临时文件,上传到 S3 桶,并将用例信息存储到 DynamoDB 表中。最后,它返回一个包含用例详情的响应对象。checkCase 函数根据提供的 caseSheetId 参数从 DynamoDB 表中检索用例表格信息,并返回一个包含用例详情的响应对象。lambda_handler 函数是 Lambda 函数的入口点,它根据事件对象中的 apiPath 值决定执行的操作。该函数根据操作构建相应的响应对象并返回。

  3. 接下来,在 function 文件夹中,创建一个名为 lambda_requirements.txt 的新文件,在其中列出 Lambda 函数代码的依赖项。在 lambda_requirements.txt 文件中输入 openpyxl==3.0.10 并保存。

  4. 现在,在部署该函数之前,我们需要创建一个 IAM 角色,授予 Lambda 所需的权限。在 AWS 控制台中,进入 IAM 页面,选择侧边菜单中的 角色,然后点击 创建新角色

  5. 在下一页,选择AWS 服务作为受信实体类型,并选择Lambda作为使用案例(如图 11.11所示)。点击下一步

图 11.11 – 选择受信实体和 AWS 服务

图 11.11 – 选择受信实体和 AWS 服务

  1. 现在,我们将选择权限策略。选择Administrator Access并点击下一步。记住,拥有这样的开放权限在生产环境中不是一个好做法。你应该仅为所需的操作和资源设置权限。

  2. 然后,为你的 IAM 角色选择一个名称(例如BDOK-Lambda-service-role),并点击创建角色

  3. 然后,你将再次看到 IAM 角色页面。搜索你创建的角色并点击它(图 11.12)。

图 11.12 – 选择你创建的 IAM 角色

图 11.12 – 选择你创建的 IAM 角色

  1. 在角色页面,你将看到角色的Amazon 资源名称ARN)。复制并保存它,稍后我们将需要这个名称来部署 Lambda 函数。

  2. 接下来,在你创建的function文件夹内,创建一个名为worksheet的新文件夹。从github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter11/agent/function/worksheet复制两个文件,第一个命名为__init__.py,第二个命名为template.py,将这些代码文件放入worksheet文件夹中。这段代码包含一个名为CaseTemplate的类,它使用openpyxl Python 库构建一个 Excel 工作表。

  3. 接下来,复制github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter11/agent/scripts文件夹中的另外两个文件,分别命名为build_lambda_package.shcreate_lambda_function.sh。这些文件包含 bash 代码,将为 Lambda 函数安装依赖项并将其部署到 AWS。

  4. 现在,我们将部署 Lambda 函数。这是检查项目结构是否正确的好时机。文件夹和文件结构应该如下所示:

    ├── app
    │   └── main.py
    ├── function
    │   ├── lambda_function.py
    │   ├── lambda_requirements.txt
    │   ├── test_event.json
    │   └── worksheet
    │       ├── __init__.py
    │       └── template.py
    ├── openapi_schema.json
    └── scripts
        ├── build_lambda_package.sh
    scripts folder and run the following commands:
    
    

    sh build_lambda_package.sh

    sh create_lambda_function.sh "<YOUR_ROLE_ARN>"

    
    

记得将<YOUR_ROLE_ARN>替换为你实际的 Lambda IAM 角色 ARN。现在,我们还有一些工作要做。接下来,我们将创建 DynamoDB 表格,用于存储关于使用案例的信息。

创建 DynamoDB 表

DynamoDB 是一个完全托管的 NoSQL 数据库服务。它是一个键值和文档数据库,可以在任何规模下提供单数毫秒级的性能。DynamoDB 已针对运行无服务器应用进行了优化,并且设计上能够根据需求自动向上或向下扩展,无需预配置或管理服务器。它特别适合需要在任何规模下对数据进行低延迟读写访问的应用程序。其极低的延迟使其成为 AI 助手应用程序的一个非常好的选择。让我们开始吧:

  1. 在 AWS 控制台中,导航到DynamoDB页面。在侧边菜单中,点击表格,然后点击创建表格

  2. 在下一页中,填写case-sheetscaseSheetId。记得选择数字,表示此条目为数字,如图 11.13所示。将所有其他配置保留为默认,然后点击创建表格

图 11.13 – 创建 DynamoDB 表格

图 11.13 – 创建 DynamoDB 表格

几秒钟后,你应该能够看到已准备好的 DynamoDB 表格。现在,我们将配置 Bedrock 代理。

配置代理

现在,在本节的最后部分,我们将配置一个 Bedrock 代理,并将其链接到其后端 Lambda 函数和知识库数据库。让我们开始吧:

  1. 首先,在 AWS 控制台中,搜索Bedrock,然后在侧边菜单中点击代理

  2. 在弹出框中,输入你的代理名称(aws-competency-agent),然后点击创建

  3. 接下来,你将看到代理配置页面。滚动到选择模型,然后选择 Anthropic 模型Claude 3 Haiku(你也可以根据需要尝试其他可用的模型)。

  4. 你是一个友好的 AI 助手。你的主要目标是帮助 AWS 合作伙伴公司为 AWS 能力认证计划创建案例表格,注册这些案例,并向用户提供已注册案例的信息。当你生成案例表格时,总是要向用户展示案例表格的 ID(id)、客户的名称(client)和案例名称(casename),并确认案例已成功创建。同时,回答用户关于你能做什么以及如何帮助他们的问题。 这是代理配置中非常重要的一部分。可以根据这些指令进行多次操作。此屏幕的示例如图 11.14所示。

图 11.14 – 配置代理的指令

图 11.14 – 配置代理的指令

  1. 之后,点击页面顶部的保存按钮,让 AWS 创建必要的权限策略。

  2. 接下来,滚动到操作组部分,点击添加

  3. 在下一页中,为你的操作组选择一个名称。对于操作组类型,选择使用 API 模式定义。在操作组调用中,选择选择一个现有的 Lambda 函数,然后选择我们刚才创建的 Lambda 函数(图 11.15)。

图 11.15 – 为代理的操作组选择 Lambda 函数

图 11.15 – 为代理的操作组选择 Lambda 函数

  1. 现在,在操作组架构部分,选择选择现有 API 架构,然后点击浏览 S3,搜索我们保存在 S3 上的 OpenAPI 架构(图 11.16)。然后,点击创建

图 11.16 – 选择 OpenAPI 架构

图 11.16 – 选择 OpenAPI 架构

  1. 接下来,在知识库部分,点击添加

  2. 选择我们之前创建的知识库,并为代理输入一些使用说明。例如:此知识库包含以下 AWS 能力程序的信息:对话式 AI、数据与分析、DevOps、教育、能源、金融服务、机器学习和安全。确保知识库状态设置为启用图 11.17)。点击保存 并退出

图 11.17 – 将知识库附加到代理

图 11.17 – 将知识库附加到代理

  1. 现在,您已返回到代理的编辑页面。这里不需要其他操作,所以您可以点击顶部的准备,使代理准备运行,然后点击保存 并退出

  2. 现在,您将被带回代理的主页。向下滚动到别名部分并点击创建

  3. 输入一个别名名称(例如aws-competency),然后点击创建别名

图 11.18 – 创建别名

图 11.18 – 创建别名

  1. 现在,最后一步是为 Lambda 注册一个权限,以便这个代理能够触发函数执行。在代理的主页上,复制代理 ARN。

  2. 接下来,转到Lambda页面,点击我们为此练习创建的函数。在函数的主页上,向下滚动,点击配置,然后点击侧边菜单中的权限图 11.19)。

图 11.19 – Lambda 权限

图 11.19 – Lambda 权限

  1. 再次向下滚动到基于资源的策略声明部分,点击添加权限

  2. 在下一页中,填写lambda:InvokeFunction图 11.20)。然后,点击保存

图 11.20 – 配置 Bedrock 代理的 Lambda 权限

图 11.20 – 配置 Bedrock 代理的 Lambda 权限

这就是我们让代理运行所需的所有配置。现在,是时候进行部署了。让我们把 Streamlit 应用程序部署到 Kubernetes 上。

在 Kubernetes 上部署应用程序

在 Kubernetes 上部署代理 Streamlit 应用程序遵循与之前部署的其他两个应用程序相同的路径。唯一不同的是我们必须创建一个新的configmap,并包含代理的 ID 及其别名 ID:

  1. 进入 AWS 控制台中的代理页面,复制代理的 ID(位于顶部区域)和别名 ID(位于底部区域)。

  2. 现在,使用以下命令创建带有这些参数的 configmap

    kubectl create configmap agent-config --from-literal=agent_alias_id=<YOUR_ALIAS_ID> --from-literal=agent_id=<YOUR_AGENT_ID> -n genai
    

    记得将 <YOUR_ALIAS_ID><YOUR_AGENT_ID> 占位符替换为实际值。

  3. 如果你想自定义应用程序,可以构建一个自定义镜像。如果不想,可以使用来自 DockerHub 的现成镜像(hub.docker.com/r/neylsoncrepalde/chat-with-claude-agent)。

  4. 接下来,我们将为应用程序和服务部署定义一个 deploy_agent.yaml 文件。该文件的内容可以在 github.com/PacktPublishing/Bigdata-on-Kubernetes/tree/main/Chapter11/agent 文件夹中找到。

  5. 将此文件复制到本地后,现在运行以下命令:

    kubectl apply -f deploy_agent.yaml -n genai
    
  6. 等待几秒钟,直到 LoadBalancer 启动完成。然后,运行以下命令以获取 LoadBalancer 的 URL:

    kubectl get svc -n genai
    

    将其粘贴到浏览器中,并添加正确的端口(:8501),查看魔法的发生(图 11.21)。

图 11.21 – AWS 能力代理应用程序 UI

图 11.21 – AWS 能力代理应用程序 UI

尝试插入一个用于创建新用例的提示,如图 11.18所示。同时,你也可以通过传递用例的 ID 来查看该用例的具体信息(图 11.22)。

图 11.22 – 使用代理检查用例信息

图 11.22 – 使用代理检查用例信息

玩一玩。提出关于能力计划的问题,尝试注册不同的用例。同时,你也可以检查 AWS DynamoDB,查看我们创建的表中摄取的信息,并查看 S3 中代理创建的 Excel 文件。

就是这样!恭喜你!你刚刚部署了一个完整的生成式 AI 代理应用程序,它可以通过自然语言在 Kubernetes 上为你执行任务。

总结

在本章中,我们探索了生成式 AI 的激动人心的世界,并学习了如何在 Kubernetes 上利用其力量。我们首先了解了生成式 AI 的基本概念、其底层机制以及它与传统机器学习方法的区别。

然后,我们利用 Amazon Bedrock,一个全面的服务套件,来构建和部署生成式 AI 应用程序。我们学习了如何使用 Bedrock 的基础模型,如 Claude 3 Haiku 和 Claude 3 Sonnet,并将它们集成到 Streamlit 应用程序中,以提供交互式用户体验。

接下来,我们深入探讨了 RAG 的概念,它将生成式 AI 的强大功能与外部知识库相结合。我们使用 Amazon Bedrock 构建了一个 RAG 系统,使我们的应用程序能够访问和利用大量的结构化数据,从而提高生成输出的准确性和相关性。

最后,我们探讨了 Amazon Bedrock 的代理功能,这是一个强大的特性,允许生成式 AI 模型自动化任务并代替我们采取行动。我们学习了如何构建代理,通过 OpenAPI 架构定义其能力,并创建作为代理后台的 Lambda 函数。

在本章中,我们通过实践经验学习了如何在 Kubernetes 上构建和部署生成式 AI 应用。通过本章获得的技能和知识,在当今快速发展的技术环境中具有不可估量的价值。生成式 AI 正在改变各行各业,彻底革新我们与 AI 的互动方式及其应用。通过掌握本章中介绍的工具和技术,你将能够构建创新且智能的应用,生成类人内容,利用外部知识来源,并实现任务自动化。

在下一章,我们将讨论一些构建生产就绪的 Kubernetes 环境所需的重要要点,这些内容由于篇幅原因在本书中未能展开。

第十二章:下一步该往哪里走

恭喜你在掌握 Kubernetes 上的大数据的旅程中走到了这一阶段!到目前为止,你已经深入理解了运行大数据工作负载在 Kubernetes 上所涉及的核心概念和技术。然而,和任何复杂系统一样,总有更多的东西需要学习和探索。

在本章中,我们将引导你走向开发旅程的下一步,涵盖一些你应该关注的最重要的主题和技术,帮助你向 Kubernetes 上生产就绪的大数据部署迈进。我们将讨论关键方面,如监控服务网格安全性自动化可扩展性GitOps,以及 Kubernetes 的持续集成/持续部署CI/CD)。

对于每个话题,我们将为你提供概述,并介绍相关的技术和工具,帮助你打下坚实的基础。这将使你能够深入探讨最适合你特定用例和需求的领域。

到本章结束时,你将清楚了解运行大数据工作负载在 Kubernetes 生产环境中所需的关键概念和技术。你将拥有做出明智决策的知识,选择采用哪些工具和方法,并且你将拥有进一步探索和学习的路线图。

我们还将讨论能够优化你的组织,确保成功实施这一架构的技能和团队结构。

无论你是资深的 Kubernetes 用户,还是刚刚开始这段旅程,本章将为你提供宝贵的见解和指导,帮助你将 Kubernetes 上的大数据实现提升到一个新的层次。

在这一章中,我们将讨论以下主要话题:

  • Kubernetes 中的大数据的重要话题

  • 团队技能怎么样?

Kubernetes 中的大数据的重要话题

随着本书接近尾声,我们需要认识到,在掌握 Kubernetes 上的大数据的旅程还远未结束。在整个章节中,我们已经覆盖了广泛的主题,从在 Kubernetes 上部署和管理大数据应用,到优化性能和可扩展性。然而,还有几个关键领域我们没有机会深入探讨,但这些领域对于构建一个强大且生产就绪的大数据基础设施在 Kubernetes 上至关重要。

在本节中,我们将深入探讨一些你在继续大数据和 Kubernetes 之旅时应该熟悉的最重要的主题。这些主题包括 Kubernetes 监控与应用监控、构建服务网格、安全性考虑、自动扩展性、GitOps、Kubernetes 的 CI/CD 以及 Kubernetes 成本控制。虽然我们不会深入每个主题的复杂细节,但我们会提供所涉及的主要概念、在 Kubernetes 上实现这些解决方案的主要技术,以及在此过程中可能遇到的最大挑战的概述。

Kubernetes 监控与应用监控

监控是任何生产就绪系统的关键组成部分,在处理运行于 Kubernetes 上的复杂大数据应用时,它变得更加重要。Kubernetes 监控涉及追踪 Kubernetes 集群本身的健康状况和性能,包括控制平面、工作节点以及各种 Kubernetes 组件。另一方面,应用监控侧重于监控运行在 Kubernetes 集群中的应用程序,关注其性能、资源利用率以及整体健康状况。

对于 Kubernetes 监控,可以使用一些流行的工具,如PrometheusGrafana。这些工具从各种 Kubernetes 组件收集度量指标,并提供可视化和警报机制,帮助你时刻掌握集群的健康状况。另一方面,应用监控通常依赖于特定应用的监控解决方案,或者与诸如 Prometheus 等工具的集成,或者与其他第三方监控解决方案,如DatadogSplunkDynatrace等进行集成。

在 Kubernetes 上监控大数据应用的最大挑战之一是数据的庞大量和应用程序本身的复杂性。大数据应用通常由多个组件组成,每个组件都有自己的一组度量指标和监控要求。此外,这些应用程序的分布式特性使得跨组件关联度量指标并全面了解系统整体健康状况变得更加困难。

构建服务网格

随着你在 Kubernetes 上运行的大数据应用程序变得越来越复杂,管理网络通信、可观察性和流量控制将变得愈加具有挑战性。这时,服务网格就显得尤为重要。服务网格是位于应用程序组件与底层网络之间的基础设施层,提供了一种一致且集中的方式来管理服务间通信、流量路由和可观察性。

Kubernetes 的流行服务网格解决方案包括IstioLinkerdConsul。这些工具提供了负载均衡、断路器、重试和流量路由等功能,以及分布式追踪和度量收集等可观察性能力。通过实施服务网格,你可以将这些横向关注点从应用程序代码中解耦,从而更容易管理和维护大数据应用。

然而,将服务网格引入 Kubernetes 环境也带来了自己的挑战。服务网格可能会增加系统的复杂性和开销,其配置和管理可能不是那么简单。此外,确保你的应用程序与服务网格的兼容性,并理解其性能影响,是至关重要的考虑因素。

安全性考虑

安全性是处理 Kubernetes 上大数据应用时的首要问题,因为这些系统通常涉及敏感数据,并且必须遵守各种监管要求。Kubernetes 提供了多个内建的安全特性,例如基于角色的访问控制RBAC)、网络策略和密钥管理。然而,实施全面的安全策略需要一种多层次的方法,来解决大数据基础设施的各个方面。

一些关键的安全考虑包括保护 Kubernetes 控制平面和工作节点,实施网络分段和隔离,管理密钥和敏感数据,以及确保符合行业标准和监管要求。工具如FalcoKubesecKube-bench 可以帮助你评估并执行 Kubernetes 环境中的安全最佳实践。

在 Kubernetes 上确保大数据应用安全的最大挑战之一是这些系统的复杂性和分布式特性。大数据应用通常由多个组件组成,每个组件都有自己的安全要求和潜在漏洞。此外,在保持性能和可扩展性的同时,确保大量数据的安全处理和存储,可能是一个重大挑战。

自动化可扩展性

在 Kubernetes 上运行大数据应用的一个关键好处是能够根据需求动态扩展资源。然而,实现有效且高效的自动化可扩展性需要仔细的规划和实施。Kubernetes 提供了内建的水平和垂直扩展机制,如水平 Pod 自动扩展器HPA)和垂直 Pod 自动扩展器VPA)。这些工具允许你根据预定义的指标和阈值自动扩展副本数量或调整资源请求和限制。

除了内建的 Kubernetes 扩展机制外,还有像 Kubernetes 事件驱动自动扩展 (KEDA) 这样的第三方解决方案,可以提供更高级的扩展功能。KEDA 是一个开源的 Kubernetes 扩展解决方案,它允许你基于事件驱动模式自动扩展工作负载。它提供了一种简单轻量的方式来定义事件源,并根据需要处理的事件数量来扩展部署。

然而,为大数据应用程序在 Kubernetes 上实现有效的自动化可扩展性可能会面临挑战。大数据应用程序通常具有复杂的资源需求和依赖关系,这使得定义适当的扩展阈值和指标变得困难。此外,确保有状态组件(例如数据库或消息队列)的无缝扩展可能会带来额外的复杂性。

GitOps 和 Kubernetes 的 CI/CD

随着 Kubernetes 上的大数据基础设施在复杂性上增长,管理和部署变更变得越来越具有挑战性。这就是 GitOps 和 CI/CD 实践派上用场的地方。GitOps 是一种将基础设施视为代码的方法,使用 Git 作为 唯一真实来源 (SSOT) 来定义声明式的基础设施。而 CI/CD 是一组使应用程序自动构建、测试和部署的实践和工具。

流行的 GitOps 工具包括 Argo CDFluxJenkins X,而像 JenkinsGitLab CI/CDGitHub Actions 这样的 CI/CD 解决方案可以与 Kubernetes 集成,启用自动化部署。通过采用 GitOps 和 CI/CD 实践,您可以简化部署流程,确保跨环境的一致性,并减少人为错误的风险。

在 Kubernetes 上为大数据应用程序实现 GitOps 和 CI/CD 的最大挑战之一是应用程序本身的复杂性。大数据应用程序通常由多个组件组成,具有复杂的依赖关系和配置。确保正确的部署顺序、处理有状态组件以及管理复杂配置可能是一个重大障碍。此外,将 GitOps 和 CI/CD 实践与现有基础设施和流程集成可能需要付出相当大的努力,并在组织内部进行文化转变。

Kubernetes 成本控制

Kubernetes 成本控制是管理和优化与在 Kubernetes 集群上运行应用程序和工作负载相关的资源和开销的关键方面。随着组织采用 Kubernetes 部署应用程序,他们通常面临着理解和控制与基础设施相关的成本挑战,例如 虚拟机 (VMs)、存储和网络资源。

在 Kubernetes 环境中,成本控制涉及对整个 Kubernetes 生态系统中资源利用和支出的监控、分析和优化。它帮助组织更清晰地了解云支出,识别低效或过度配置的领域,并实施策略,在不影响应用性能或可用性的情况下降低成本。

Kubernetes 中成本控制的重要性源于容器化应用的动态性和可扩展性。Kubernetes 可以根据需求自动扩展资源,如果管理不当,可能会导致意外的成本增加。此外,组织可能会不小心过度配置资源或让未使用的资源保持运行,从而产生不必要的开销。

为了应对 Kubernetes 环境中成本控制的需求,出现了多个工具和解决方案。最受欢迎的开源工具之一是 Kubecost,它提供 Kubernetes 集群的实时成本监控、分配和优化。Kubecost 与多种云服务提供商集成,如亚马逊 Web 服务AWS)、Azure谷歌云平台GCP),并提供按命名空间、部署或服务进行成本分配、成本预测以及成本优化建议等功能。

Kubecost 通过收集 Kubernetes API 和云提供商 API 中的指标,分析资源利用和定价数据,并通过用户友好的界面或与其他监控和警报工具的集成呈现成本信息。它使团队能够识别成本驱动因素,设定预算,并在成本超过预设阈值时接收警报。

主要挑战之一是 Kubernetes 本身的复杂性,其众多组件和配置可能会影响资源利用和成本。此外,组织可能会在将成本准确分配到特定应用或团队时遇到困难,尤其是在多租户环境中。另一个挑战是在成本优化和应用性能之间找到合适的平衡。过于激进的成本节约措施,如资源配置过低或禁用自动扩展,可能导致性能下降或应用停机,最终导致收入损失或客户不满。

为了有效解决 Kubernetes 中的成本控制问题,组织必须采取一种全面的方法,包括持续的监控、分析和优化。这包括实施成本治理政策、设定预算和警报、定期审查资源利用情况和调整资源规模,并在开发、运维和财务团队中培养成本意识文化。

在生产环境中运行 Kubernetes 会带来许多技术挑战,但这只是事情的一半。人际技能、团队建设和组织共享知识对于成功实施至关重要。接下来,我们将讨论需要的技能,以便在 Kubernetes 上使用大数据具备生产准备环境,并进行技术团队建设。

那么团队技能呢?

正如我们在前一节中讨论的那样,在 Kubernetes 上实施和管理大数据应用涉及各种概念和技术。为确保在 Kubernetes 上成功推动大数据项目,拥有具备正确技能和专业知识的团队至关重要。在本节中,我们将探讨每个前述主题所需的关键技能,并讨论这些技能如何映射到技术团队中的特定角色。

监控的关键技能

有效的监控需要不同领域技能的结合。首先,您需要团队成员深刻理解 Kubernetes 及其各个组件。他们应精通部署和配置监控工具如 Prometheus 和 Grafana,并将其与 Kubernetes 生态系统集成。此外,他们应具备设计和实施监控策略的经验,定义相关指标,并设置警报和通知系统。

对于应用程序监控,您需要团队成员具备特定大数据技术和框架的专业知识。他们应能够识别和设置应用程序关键组件,了解其性能特征,并定义适当的监控指标。此外,他们应具备将应用程序监控解决方案与整体监控基础设施集成的技能。

这些技能通常在站点可靠性工程师(SREs)DevOps 工程师云工程师等角色中找到。这些专业人士通常具有系统管理、自动化和监控方面的深厚背景,结合对云计算和容器化技术(如 Kubernetes)的经验。

构建服务网格

实施服务网格需要深入理解网络概念、服务间通信模式和可观察性原则。您的团队应具备部署和配置服务网格解决方案(如 Istio、Linkerd 或 Consul)的技能。他们应熟练定义流量路由规则、实施安全策略,并利用服务网格提供的可观察性功能。

此外,他们还应该具备将服务网格与现有应用程序集成的经验,并确保与大数据基础设施中各个组件的兼容性。这些技能通常出现在平台工程师云工程师DevOps 工程师等角色中,重点关注网络和可观察性。

安全性考虑

在 Kubernetes 上保障大数据应用的安全需要一种多学科的方法,结合了多个安全领域的专业知识。你的团队应有成员精通实施和管理 Kubernetes 安全控制,例如 RBAC、网络策略和密钥管理。他们应精通 Kubernetes 集群加固、执行安全最佳实践,并进行定期的安全审计和漏洞评估。

此外,你还需要具备数据安全和合规性方面专业知识的团队成员,特别是在大数据应用的背景下。他们应该了解行业标准和法规,例如通用数据保护条例GDPR)、健康保险可携带性与责任法案HIPAA)或支付卡行业数据安全标准PCI DSS),并能够实施适当的安全措施,以确保合规。

这些技能通常出现在安全工程师云安全分析师合规专家等角色中。这些专业人士通常拥有网络安全、风险管理和合规性强大的背景,并且具备云计算和容器化技术的经验。

自动化可扩展性

在 Kubernetes 上为大数据应用实现有效的自动化可扩展性需要应用架构、性能优化和自动化技能的结合。你的团队应有成员精通设计和实现可扩展且具备弹性的应用,了解不同组件的资源需求和扩展模式,并定义适当的扩展指标和阈值。

他们还应该熟练使用前述的 Kubernetes 扩展机制,如 HPA、VPA 和 KEDA。此外,他们还应具备自动化扩展过程的经验,能够与监控和告警系统集成,并确保有状态组件的无缝扩展。

这些技能通常出现在云工程师DevOps 工程师SRE等角色中。这些专业人士通常拥有云计算、容器化和自动化方面的强大背景,并结合了性能优化和应用架构的经验。

GitOps 与 CI/CD 技能

在 Kubernetes 上为大数据应用采用 GitOps 和 CI/CD 实践需要版本控制、基础设施即代码IaC)和自动化方面的技能。你的团队应有成员擅长使用 Git 和基于 Git 的工具,如 Argo CD、Flux 或 Jenkins X,来管理和部署 Kubernetes 资源。他们应熟练编写和维护声明性基础设施定义,并具有实施 GitOps 工作流和最佳实践的经验。

此外,他们还应具备设置和配置 CI/CD 管道的专业技能,将其与 Kubernetes 集成,并实现构建、测试和部署过程的自动化。他们应熟练处理复杂的应用依赖、管理有状态组件,并确保在不同环境中进行一致的部署。

这些技能通常出现在DevOps 工程师平台工程师发布工程师等角色中。这些专业人员通常具有软件开发、自动化和基础设施即代码(IaC)的扎实背景,并结合云计算和容器化技术(如 Kubernetes)的经验。

成本控制技能

在 Kubernetes 环境中有效的成本控制需要团队成员的协作努力,这些成员具有不同的技能和专业知识。这些专业人员应具备技术知识、分析能力,以及对组织的业务目标和成本约束的深刻理解。

成本控制的关键角色之一是云成本工程师FinOps 工程师。这些专业人员应对云计算技术有深入了解,包括 Kubernetes、容器编排和云服务提供商服务。他们还需要对定价模型、资源利用模式和成本优化策略有深刻理解。

另一个关键角色是云架构师平台工程师。这些人员应具有丰富的经验,能够设计和实施云原生架构,包括 Kubernetes 集群、微服务和无服务器函数。他们应善于优化资源分配,实施自动扩展策略,并利用具有成本效益的云服务。他们在基础设施即代码(IaC)和 CI/CD 管道方面的专业知识,对于高效的资源管理和成本控制至关重要。

开发人员和 DevOps 工程师也是成本控制工作的关键贡献者。他们应深入理解应用架构、资源需求和性能特征。他们编写高效和优化代码的能力、实施容器化最佳实践,以及利用自动扩展和资源调整技术,能够显著影响资源利用和成本。

近年来,在专注于成本控制的组织中,出现了一个新的角色:成本冠军。这个角色充当跨团队成本意识和优化的倡导者。成本冠军与开发人员、运维人员和财务团队密切合作,推广成本意识的实践,提供培训和指导,并确保成本考虑因素融入软件开发生命周期SDLC)。他们应该具备强大的沟通和领导能力,并深入了解组织的成本结构和商业目标。

在 Kubernetes 中有效的成本控制需要跨职能团队的协作努力,这些团队具有多样化的技能和专业知识。通过培养成本意识的文化,利用合适的工具和技术,并为团队提供必要的知识和资源,组织可以在保持应用性能和可用性的同时实现显著的成本节省。

需要注意的是,虽然这些角色和技能集提供了一个通用的指南,但具体的要求可能会根据你组织的规模和复杂性以及你使用的具体大数据技术和框架有所不同。在某些情况下,你可能需要将这些职责结合或分配给多个角色或团队。

通过建立一个拥有正确技能和专业知识的团队,并营造一个支持性和协作的环境,你将能够充分应对在 Kubernetes 上实现和管理大数据应用的挑战,充分发挥这项强大技术组合的潜力。

总结

在本章中,我们探讨了你在掌握 Kubernetes 上的大数据的旅程中的下一步。我们涵盖了几个重要主题,这些主题对于在 Kubernetes 上构建强大且适合生产环境的大数据基础设施至关重要,包括 Kubernetes 监控和应用监控、构建服务网格、生产环境中需要考虑的重要安全因素、可扩展性自动化方法、Kubernetes 的 GitOps 和 CI/CD 以及 Kubernetes 成本控制。

我们还讨论了拥有正确技能和专业知识的团队对于成功应对这些挑战的重要性。我们涵盖了每个主题所需的关键角色和技能集,包括 SRE、DevOps 工程师、云工程师、安全工程师和发布工程师。

恭喜你已经走到了本书的结尾!你已经迈出了在 Kubernetes 上运行大数据工作负载的艺术的重大一步。前方的旅程可能会充满挑战,但你所获得的知识和技能将为你未来的努力奠定坚实的基础。

记住——大数据和 Kubernetes 领域是不断发展的,新技术和最佳实践不断涌现。保持持续学习的心态,保持好奇。

不要害怕尝试实验和新的方法。Kubernetes 和大数据提供了一个广阔的创新平台,你独特的视角和经验可以帮助找到定制化的解决方案,满足你项目、公司或客户的需求。

最后,记住在这个领域的成功并非单打独斗。参加会议,参与在线社区,和领域专家交流,保持与最新发展的同步。与团队合作,分享你的知识,并向他人学习。只有一起努力,你们才能克服挑战,解决复杂的问题,并推动大数据在 Kubernetes 上的可能性边界。

再次祝贺你,祝你在未来的旅程中好运!

posted @ 2025-06-30 19:28  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报