Java-可扩展数据架构-全-

Java 可扩展数据架构(全)

原文:zh.annas-archive.org/md5/579bef92b525a1e726e776ef1e76ea33

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

当我开始写这本书时,我回顾了自己在架构和开发数据工程解决方案、有效地交付和运行这些解决方案以及在许多公司构建和管理可扩展和健壮的数据管道方面的经验,并问自己——我能分享哪些最有用的事情来帮助有志或初学的数据架构师、数据工程师或 Java 开发者成为数据架构师专家? 这本书反映了我在日常工作中所做的工作,即设计、开发和维护针对不同数据工程问题的可扩展、健壮和成本效益的解决方案。

Java 架构模式和工具使架构师能够开发可靠、可扩展和安全的工程解决方案,用于收集、处理、管理和发布数据。有许多书籍和在线资料讨论了数据架构的一般概念。还有其他一系列书籍和在线资料专注于并深入探讨了技术栈。虽然这些资料为架构师提供了必要知识,但它们通常缺乏关于架构师如何实际处理数据工程问题以及如何通过逻辑推理创建最适合的架构的细节。在这本书中,我尝试通过一些技术手段,使数据架构师能够以问题为导向,创建有效的解决方案。

在这本书中,我将带您踏上学习数据工程基础以及如何使用这些基础知识来分析和提出数据工程问题解决方案的旅程。我还讨论了初学者架构师如何选择正确的技术栈来实现解决方案。我还涉及了这些解决方案的数据安全和治理问题。

架构师面临的一个挑战是,做事情的方式总是不止一种。我们还讨论了如何衡量不同的架构选择,以及您如何使用数据驱动技术正确选择最适合的替代方案。

本书面向对象

《可扩展数据架构》是为具有至少一些后端系统或数据工程解决方案工作知识的 Java 开发者、数据工程师和有志成为数据架构师的人编写的。本书假设您至少有一些 Java 的工作知识,并了解 Java 的基本概念。这本书将帮助您成长为一名成功的基于 Java 的数据架构师。

数据架构师和副架构师会发现这本书有助于磨练他们的技能并在工作中脱颖而出。非 Java 后端开发者或数据工程师也可以使用本书的概念。然而,他们可能难以理解解决方案的代码和实现。

本书涵盖内容

第一章,《现代数据架构基础》,是对数据工程、数据工程的基本概念以及 Java 数据架构师在数据工程中所扮演角色的简要介绍。

第二章, 数据存储和数据库,简要讨论了各种数据类型、存储格式、数据格式和数据库,并讨论了何时使用它们。

第三章, 确定合适的数据平台,概述了各种用于部署数据管道的平台,以及如何选择正确的平台。

第四章, ETL 数据加载 - 数据仓库中的批量数据摄取解决方案,讨论了如何使用 Spring Batch 和 Java 来处理、分析和构建一个有效的批量数据摄取解决方案。

第五章, 构建批量处理管道,讨论了如何使用 S3、Apache Spark(Java)、AWS 弹性映射减少EMR)和 AWS Athena 在 AWS 中构建和实现一个数据分析管道,以用于大数据场景。

第六章, 构建实时处理管道,提供了构建实时流解决方案的步骤指南,用于使用 Java、Kafka 和相关技术预测贷款申请的风险类别。

第七章, 核心架构设计模式,讨论了用于解决数据工程问题的各种常见架构模式以及何时使用它们。

第八章, 启用数据安全和治理,介绍了数据治理,并讨论了如何通过实际案例应用它。同时也简要提到了数据安全的话题。

第九章, 将 MongoDB 数据作为服务公开,提供了如何构建数据作为服务以使用 REST API 公开 MongoDB 数据的分步指南。

第十章, 使用 GraphQL 的联邦和可扩展 DaaS,讨论了 GraphQL 是什么,各种 GraphQL 模式,以及如何使用 GraphQL 发布数据。

第十一章, 衡量性能和基准测试您的应用程序,概述了性能工程,如何衡量性能和创建基准,以及如何优化性能。

第十二章, 评估、推荐和展示您的解决方案,讨论了如何在各种架构中选择最适合的替代方案,以及如何有效地展示推荐的架构。

为了充分利用这本书

预期您具备 Core Java 和 Maven 的知识,以便充分利用本书。对于第五章**,构建批处理管道架构,希望您对 Apache Spark 有基本了解。对于第六章**,构建实时处理管道架构,希望您对 Kafka 有基本了解。此外,对 MongoDB 有基本了解对于理解第 6、9 和 10 章的实现也是有益的。

图片

您可以通过确保安装 Java SDK、Maven 和 IntelliJ IDEA Community Edition 来设置您的本地环境。您可以使用以下链接进行安装:

如果您正在使用这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java。如果代码有更新,它将在 GitHub 仓库中更新。

我们还从我们丰富的图书和视频目录中提供了其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/feLcH

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“因此,KStream bean 被创建为一个KStream<String,String>的实例。”

A block of code is set as follows:
public interface Transformer<K, V, R> {
    void init(ProcessorContext var1);
    R transform(K var1, V var2);
    void close();
}

任何命令行输入或输出都按照以下方式编写:

bin/connect-standalone.sh config/connect-standalone.properties connect-riskcalc-mongodb-sink.properties

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“在这里,点击构建数据库按钮以创建一个新的数据库实例。”

小贴士或重要提示

看起来像这样。

联系我们

我们欢迎读者反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现任何错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上发现我们作品的任何非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将非常感谢。请通过 copyright@packt.com 与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了使用 Java 的可伸缩数据架构,我们非常乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的审阅对我们和科技社区都非常重要,并将帮助我们确保提供高质量的内容。

第一部分 – 数据系统基础

在本节中,您将了解各种类型的数据工程问题和数据架构师在解决问题中的作用。您还将学习构建解决方案所需的数据格式、存储、数据库和数据平台的基础知识。

本节包括以下章节:

  • 第一章**,现代数据架构基础

  • 第二章**,数据存储和数据库

  • 第三章**,识别合适的数据平台

第一章:现代数据架构基础

随着 21 世纪的到来,由于互联网使用越来越多,以及越来越强大的数据洞察工具和技术不断涌现,数据爆炸现象已经出现,数据已成为新的黄金。这暗示了对有用和可操作数据的需求增加,以及对高质量数据工程解决方案的需求。然而,构建可扩展、可靠和安全的数据工程解决方案通常很复杂且具有挑战性。

一个设计不佳的解决方案往往无法满足业务需求。要么数据质量差,无法满足服务水平协议(SLAs),要么随着生产中数据的增长,它无法持续或扩展。为了帮助数据工程师和架构师构建更好的解决方案,每年都会有数十个开源和预操作工具发布。即使是一个设计良好的解决方案,有时也会因为工具选择或实施不当而失败。

本书讨论了各种架构模式、工具和技术,通过逐步的动手实践解释,帮助架构师选择最合适的解决方案和技术堆栈来解决数据工程问题。特别是,它专注于使架构决策更容易的技巧和窍门。它还涵盖了数据架构师所需的其他基本技能,例如数据治理、数据安全、性能工程以及向客户或高级管理层进行有效的架构展示。

在本章中,我们将探讨数据工程的领域以及现代商业生态系统中数据的基本特征。我们将涵盖数据架构师试图解决的现代数据工程问题的各种类别。然后,我们将了解 Java 数据架构师的角色和职责。我们还将讨论数据架构师在设计数据工程解决方案时面临的挑战。最后,我们将概述本书中将要讨论的技术和工具,以及它们如何帮助有抱负的数据架构师更高效地工作并提高生产力。

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

  • 探索数据工程领域

  • Java 数据架构师的责任和挑战

  • 缓解这些挑战的技术

探索数据工程领域

在本节中,你将了解数据工程是什么以及为什么需要它。你还将了解数据工程问题的各种类别以及它们在现实世界中的应用场景。在学习如何为这类现实世界问题构建解决方案之前,了解数据工程问题的多样性是很重要的。

什么是数据工程?

根据定义,数据工程是软件工程的一个分支,专注于以可用和可操作的形式收集、分析、转换和存储数据。

随着社交平台、搜索引擎和在线市场的增长,数据生成的速度呈指数级增长。仅 2020 年,人类每天就产生了约 2500 个 PB 的数据。预计到 2025 年,这一数字将增加到每天 468 个 EB。数据的高体积和可用性已经使人工智能和数据分析领域的技术发展迅速。这导致企业、公司和政府能够以前所未有的方式收集洞察力,为客户提供更好的服务体验。

然而,原始数据通常很少被使用。因此,对创建可用的、安全可靠的数据的需求增加了。数据工程围绕创建可扩展的解决方案来收集原始数据,然后分析、验证、转换,并以可用的和可操作的形式存储它。在特定场景和组织中,现代数据工程中,企业期望可用的和可操作的数据作为一项服务发布。

在我们深入探讨之前,让我们探索一些数据工程的实际应用案例:

  • 用例 1美国运通Amex)是一家领先的信用卡提供商,但它需要将具有相似消费行为的客户分组在一起。这确保了 Amex 可以为目标客户提供个性化的优惠和折扣。为此,Amex 需要在数据上运行聚类算法。然而,数据来自不同的来源。一些数据来自 MobileApp,一些来自不同的 Salesforce 组织,如销售和营销,还有一些数据来自日志和 JSON 事件。这些数据被称为原始数据,可能包含垃圾字符、缺失字段、特殊字符,有时还包含如日志文件这样的非结构化数据。在这里,数据工程团队从不同的来源摄取这些数据,对其进行清理、转换,并以可用的结构化格式存储。这确保了执行聚类的应用程序可以在清洁和排序后的数据上运行。

  • 用例 2:一家健康保险公司从多个来源接收数据。这些数据来自各种面向消费者的应用程序、第三方供应商、Google Analytics、其他营销平台和主机批量作业。然而,公司希望创建一个单一的数据存储库,可以为不同的团队提供清洁和排序后的数据来源。这样的需求可以通过数据工程来实现。

现在我们已经了解了数据工程,让我们来看看它的几个基本概念。我们将从数据的维度开始。

数据维度

任何关于数据工程的讨论如果没有谈到数据的维度都是不完整的。数据的维度是一些基本特征,通过这些特征可以分析数据的性质。数据工程的起点是分析和理解数据。

要成功分析和构建面向数据解决方案,现代数据分析的四个 V 非常重要。这些可以在以下图表中看到:

图 1.1 – 数据维度

图 1.1 – 数据维度

让我们详细看看这些 V 的每个方面:

  • 体积:这指的是数据的大小。数据的大小可以从几字节到几百拍字节不等。体积分析通常涉及理解整个数据集的大小或单个数据记录或事件的大小。了解大小对于选择处理和存储数据的技术和基础设施规模决策至关重要。

  • 速度:这指的是数据生成的速度。高速数据需要分布式处理。分析数据生成的速度对于企业需要实时或近实时提供可用数据的情况尤为重要。

  • 多样性:这指的是数据源可以生成数据的各种格式变化。通常,它们可以是以下三种类型之一:

    • 结构化:结构化数据是指列数、数据类型及其位置固定。所有能够完美适应关系数据模型的传统数据集都是结构化数据的完美例子。

    • 非结构化:这些数据集不符合特定的结构。此类数据集中的每条记录可以具有任意数量的列,格式任意。例如,音频和视频文件。

    • .json.xml 文件。

  • 真实性:这指的是数据的可信度。简单来说,它与数据的质量相关。分析数据的噪声与分析数据的任何其他方面一样重要。这是因为这种分析有助于创建一个稳健的处理规则,最终决定数据工程解决方案的成功程度。许多精心设计和构建的数据工程解决方案在生产中失败,原因是对源数据的质量和噪声缺乏了解。

现在我们对分析数据性质的特征有了相当的了解,让我们了解它们在不同类型的数据工程问题中扮演着至关重要的角色。

数据工程问题的类型

从广义上讲,数据工程师解决的问题可以分为两种基本类型:

  • 处理问题

  • 发布问题

让我们更详细地看看这些问题。

处理问题

与收集原始数据或事件、处理它们并将它们存储在可用或可操作的数据格式中相关的问题,可以广泛地归类为处理问题。典型用例可以是数据摄取问题,如提取、转换、加载ETL)或数据分析问题,如生成年度报告。

再次,处理问题可以分为三个主要类别,如下:

  • 批处理

  • 实时处理

  • 近实时处理

这可以在以下图中看到:

图 1.2 – 处理问题的类别

图 1.2 – 处理问题的类别

让我们详细看看这些类别中的每一个。

批处理

如果处理的 SLA 超过 1 小时(例如,如果处理需要每 2 小时、每天、每周或每两周进行一次),那么这种问题被称为批处理问题。这是因为,当系统以较长的时间间隔处理数据时,它通常处理的是一批数据记录而不是单个记录/事件。因此,这种处理被称为批处理

图 1.3 – 批处理问题

图 1.3 – 批处理问题

通常,批处理解决方案取决于数据量。如果数据量超过数十个太字节,通常需要将其作为大数据处理。此外,由于大数据处理是调度驱动的,需要一个工作流程管理器或调度器来运行其作业。我们将在本书的后面更详细地讨论批处理。

实时处理

实时处理问题是一个用例,其中原始数据/事件需要即时处理,响应或处理结果应在几秒内或最多在 2 到 5 分钟内可用。

如以下图所示,实时过程以事件流的形式接收数据,并立即处理它。然后,它要么将处理后的事件发送到接收器,要么发送到另一个事件流以进一步处理。由于这种处理发生在事件流上,因此这种处理被称为实时流处理:

图 1.4 – 实时流处理

图 1.4 – 实时流处理

图 1.4所示,事件 E0 被流应用程序处理并发送出去,而事件 E1、E2 和 E3 则在队列中等待处理。在 t1 时,事件 E1 也被处理,显示了流应用程序对事件的连续处理

事件可以在任何时间生成(24/7),这创造了一种新的问题。如果事件的产生者应用程序直接将事件发送给消费者,就有可能发生事件丢失,除非消费者应用程序 24/7 都在运行。即使将消费者应用程序关闭进行维护或升级也是不可能的,这意味着消费者应用程序应该没有停机时间。然而,任何没有停机时间的应用程序都是不现实的。这种应用程序之间的通信模型被称为点对点通信。

在实时问题中的点对点通信面临的另一个挑战是处理速度,因为这应该始终等于或大于生产者的速度。否则,将会有事件丢失或消费者可能发生内存溢出。因此,他们不是直接将事件发送到消费者应用程序,而是异步发送到事件总线消息总线。事件总线是一个高可用性容器,可以存储诸如队列或主题的事件。通过在中间引入高可用性事件总线来异步发送和接收数据,这种模式被称为发布-订阅框架

以下是与实时处理问题相关的一些重要术语:

  • 事件:这可以定义为由于动作、触发器或发生而产生的数据包。它们在发布-订阅框架中也普遍被称为消息

  • 生产者:生产和向消息总线发送事件的是发布者生产者

  • 消费者:从消息总线中消费事件以进行处理的是消费者订阅者

  • 队列:它有一个单一的生产者和一个单一的消费者。一旦消费者消费了消息/事件,该事件就从队列中删除。作为一个类比,它就像你朋友发送给你的短信或电子邮件。

  • 主题:与队列不同,主题可以有多个消费者和生产者。它是一个广播频道。作为一个类比,它就像 HBO 这样的电视频道,多个生产者都在主持他们的节目,如果你订阅了这个频道,你将能够观看任何这些节目。

一个现实世界中的实时问题示例是信用卡欺诈检测,你可能经历过银行对你的交易进行自动确认电话,如果在执行过程中有任何交易看起来可疑。

近实时处理

近实时处理,正如其名称所暗示的,是一个响应或处理时间不需要像实时那样快,但应该小于 1 小时的问题。近实时处理的一个特点是它以微批次处理事件。例如,一个近实时过程可能每 5 分钟处理一次批次,每 100 条记录一个批次大小,或者两者的组合(满足条件优先)。

在时间 tx 时,所有在 t0 和 tx 之间生成的事件(E1、E2 和 E3)都由近实时处理作业一起处理。同样,所有在时间 tx 和 tn 之间的事件(E4、E5 和 E6)也一起处理。

图 1.5 – 近实时处理

图 1.5 – 近实时处理

典型的近实时用例包括推荐问题,如亚马逊的产品推荐或 YouTube 和 Netflix 的视频推荐。

发布问题

发布问题涉及将处理后的数据发布到不同的企业和团队,以便数据可以轻松获取,同时具备适当的安全性和数据治理。由于发布问题的主要目标是向下游系统或外部应用程序暴露数据,因此拥有极其强大的数据安全和治理至关重要。

通常,在现代数据架构中,数据以以下三种方式之一发布:

  • 排序后的数据存储库

  • 网络服务

  • 可视化

让我们更详细地看看每个部分。

排序后的数据存储库

排序后的数据存储库是一个常用的术语,用于指代各种用于存储处理数据的存储库。这些数据是可用和可操作的,可以直接由企业、分析团队和其他下游应用程序根据其用例进行查询。它们大致分为三种类型:

  • 数据仓库

  • 数据湖

  • 数据枢纽

数据仓库是一个集成的结构化数据集中存储库,主要用于报告、数据分析和企业智能(BI)。数据湖由结构化和非结构化数据组成,主要用于数据准备、报告、高级分析、数据科学和机器学习ML)。数据枢纽是受信任、受管理和共享数据的集中存储库,它使不同端点之间的数据共享无缝,并将业务应用程序连接到数据仓库和数据湖等分析结构。

网络服务

另一种发布模式是将数据作为服务发布,通常称为数据即服务。这种数据发布模式具有许多优点,因为它通过设计实现了安全性、不可变性和治理。如今,随着云计算技术和 GraphQL 的普及,数据即服务在行业中获得了很大的关注。

发布数据作为服务的两种流行机制如下:

  • REST

  • GraphQL

我们将在本书的后面详细讨论这些技术。

可视化

有句流行的话:一图胜千言。可视化是一种技术,通过图表和图形等视觉方式捕捉关于数据的报告、分析和统计信息。

可视化有助于企业和领导层理解、分析和了解其业务中流动的数据概览。这在决策和业务规划中非常有帮助。

一些最常见和流行的可视化工具如下:

  • Tableau 是一款专有的数据可视化工具。该工具包含多个源连接器,可以将其导入并使用拖放可视化组件(如图表和图形)创建快速直观的可视化。您可以在www.tableau.com/了解更多关于此产品信息。

  • Microsoft Power BI是微软的一个专有工具,允许您从各种数据源收集数据,连接并创建用于商业智能的强大仪表板和可视化。虽然 Tableau 和 Power BI 都提供数据可视化和商业智能,但 Tableau 更适合经验丰富的数据分析师,而 Power BI 对非技术或经验不足的用户更有用。此外,与 Power BI 相比,Tableau 更适合处理大量数据。您可以在powerbi.microsoft.com/了解更多关于此产品的信息。

  • Elasticsearch-Kibana是一个开源工具,其源代码是开源的,并且提供免费版本用于本地安装,以及付费订阅的云安装。这个工具可以帮助您从任何数据源将数据导入 Elasticsearch,并使用 Kibana 创建可视化和仪表板。Elasticsearch 是一个强大的基于文本的Lucene搜索引擎,不仅存储数据,还支持各种类型的数据聚合和分析(包括机器学习分析)。Kibana 是一个仪表板工具,与 Elasticsearch 协同工作,创建非常强大和有用的可视化。您可以在www.elastic.co/elastic-stack/了解更多关于这些产品的信息。

重要提示

Lucene 索引是一个全文反向索引。这个索引在基于文本的搜索中非常强大且快速,是大多数搜索引擎背后的核心索引技术。Lucene 索引会将所有文档拆分成单词或标记,然后为每个单词创建索引。

  • Apache Superset是一个完全开源的数据可视化工具(由 Airbnb 开发)。它是一个强大的仪表板工具,并且完全免费,但其数据源连接器支持有限,主要限于 SQL 数据库。一些有趣的功能包括其内置的角色基础数据访问、用于定制的 API 以及可扩展性以支持新的可视化插件。您可以在superset.apache.org/了解更多关于此产品的信息。

尽管我们简要讨论了市场上的一些可视化工具,但还有许多可视化工具和竞争性替代品。更深入地讨论数据可视化超出了本书的范围。

到目前为止,我们已经概述了数据工程及其各种类型的数据工程问题。在下一节中,我们将探讨 Java 数据架构师在数据工程领域扮演的角色。

Java 数据架构师的责任和挑战

数据架构师是高级技术领导者,他们负责将业务需求映射到技术需求,构想解决业务问题的技术解决方案,并建立数据标准和原则。数据架构师扮演着独特的角色,他们既了解业务也了解技术。他们就像是商业和技术领域的雅努斯,一方面他们可以观察、理解和与业务沟通,另一方面,他们也以同样的方式与技术沟通。数据架构师创建用于规划、指定、启用、创建、获取、维护、使用、归档、检索、控制和清除数据的流程。根据 DAMMA 的数据管理知识体系,数据架构师提供标准化的通用业务词汇,表达战略需求,概述满足这些需求的高级集成设计,并与企业战略和相关的业务架构保持一致

下面的图显示了数据架构师处理的多重关注点:

图 1.6 – 数据架构师的多重关注点

图 1.6 – 数据架构师的多重关注点

Java 数据架构师的典型职责如下:

  • 将业务需求解释为技术规范,包括数据存储和集成模式、数据库、平台、流、转换和技术栈

  • 建立架构框架、标准和原则

  • 开发和设计用作模式、供他人遵循以创建和改进数据系统的参考架构

  • 定义数据流及其治理原则

  • 在考虑可扩展性、性能、资源可用性和成本的同时,推荐最合适的解决方案及其技术栈

  • 协调和与多个部门、利益相关者、合作伙伴和外部供应商合作

在现实世界中,数据架构师应该扮演三种不同的角色,如下面的图所示:

图 1.7 – 数据架构师的多面角色

图 1.7 – 数据架构师的多面角色

让我们更详细地看看这三个架构角色:

  • 数据架构守护者:架构守护者是一个人或一个角色,确保数据模型遵循必要的标准,架构遵循适当的架构原则。他们寻找解决方案或业务期望方面的任何差距。在这里,数据架构师在产品或解决方案设计及交付(包括数据模型、架构、实施技术、测试程序、持续集成/持续交付CI/CD)努力或业务期望中的任何缺乏或差距)中扮演着负面角色,寻找错误或差距。

  • 数据顾问:数据顾问是一个更专注于寻找解决方案而不是寻找问题的数据架构师。数据顾问强调问题,但更重要的是,他们展示了机会或提出了解决方案。数据顾问应该理解问题的技术以及业务方面和解决方案,并且应该能够就改进解决方案提供建议。

  • 业务高管:除了数据架构师扮演的技术角色外,数据架构师还需要扮演一个高管角色。正如之前所述,数据架构师就像是商业和技术之间的雅努斯,因此他们被期望成为一个优秀的沟通者和销售高管,能够向非技术人员推销他们的想法或解决方案(即技术性的)。通常,数据架构师需要向高层领导进行电梯演讲,展示机会并说服他们接受针对商业问题的解决方案。要在这个角色中取得成功,数据架构师必须像业务高管一样思考——ROI 是什么?或者我能从中得到什么?我们通过这个解决方案或机会能节省多少时间和金钱? 此外,数据架构师在表达他们的想法时应该简洁明了,以便在听众中(主要是业务高管、客户或投资者)产生立即的兴趣。

让我们了解数据架构师和数据工程师之间的区别。

数据架构师与数据工程师的比较

数据架构师和数据工程师是相关联的角色。数据架构师负责可视化、概念化和创建数据工程解决方案和框架的蓝图,而数据工程师则根据蓝图实施解决方案。

数据架构师负责整理由大量业务数据产生的数据混乱。每个数据分析或数据科学团队都需要一个数据架构师,他们能够可视化和设计数据框架,以创建干净、分析、管理、格式化和安全的数据。这个框架可以进一步被数据工程师、数据分析师和数据科学家用于他们的工作。

数据架构师面临的挑战

数据架构师在日常工作中面临许多挑战。我们将重点关注数据架构师在日常工作中面临的主要挑战:

  • 选择正确的架构模式

  • 选择最佳的技术堆栈

  • 缺乏可操作的数据治理

  • 向领导层推荐和有效沟通

让我们更深入地了解一下。

选择正确的架构模式

一个单一的数据工程问题可以通过许多方式解决。然而,随着客户不断变化的期望和新技术的演变,选择正确的架构模式变得更加具有挑战性。更有趣的是,随着技术环境的改变,架构的灵活性和可扩展性需求增加了许多倍,以避免不必要的成本和确保架构随时间的可持续性。

选择最适合的技术栈

数据架构师需要解决的问题之一是技术栈。即使你已经创建了一个非常完善解决方案,你的解决方案能否成功或失败,将取决于你选择的技术栈以及你计划如何使用它。随着越来越多的工具、技术、数据库和框架的开发,数据架构师面临的一个大挑战是选择一个最佳的技术栈,以帮助创建可扩展、可靠和健壮的解决方案。通常,数据架构师还需要考虑其他非技术因素,例如工具的未来增长预测、这些工具在市场上的熟练资源可用性、供应商锁定、成本和社区支持选项。

缺乏可操作的数据治理

数据治理在数据业务中是一个热门词汇,但它究竟意味着什么?治理是一个广泛的领域,包括治理数据的工作流程和工具集。如果工具或工作流程过程存在限制或不存在,那么数据治理就不完整。当我们谈论可操作治理时,我们指的是以下要素:

  • 将数据治理与所有数据工程系统整合,以维护标准元数据,包括事件和日志的跟踪和标准时间线

  • 整合涉及所有安全政策和标准的治理数据

  • 基于角色和用户的对所有数据元素和系统的访问管理策略

  • 遵守持续跟踪的既定指标

  • 整合数据治理和数据架构

数据治理应始终与战略和组织目标保持一致。

向领导层推荐和有效沟通

创建最佳架构和正确的一组工具是一项具有挑战性的任务,但除非它们被付诸实践,否则永远不够。数据架构师经常需要戴上的一个帽子是销售执行者,他们需要向业务执行者或高层领导销售他们的解决方案。这些人通常不是技术人员,他们也没有太多时间。大多数数据架构师都有强大的技术背景,他们面临着向这些人沟通和销售他们想法的艰巨任务。为了说服他们关于机会和想法,数据架构师需要提供适当的决策指标和信息,以便将这个机会与组织的更广泛业务目标对齐。

到目前为止,我们已经看到了数据架构师的角色和他们面临的常见问题。在下一节中,我们将概述数据架构师如何在日常基础上缓解这些挑战。

缓解这些挑战的技术

在本节中,我们将讨论数据架构师如何减轻上述挑战。为了理解缓解计划,了解数据架构的生命周期以及数据架构师如何贡献于它是很重要的。以下图表显示了数据架构的生命周期:

图 1.8 – 数据架构的生命周期

图 1.8 – 数据架构的生命周期

数据架构从定义业务面临的问题开始。在这里,这主要是由业务团队或客户识别或报告的。然后,数据架构师与业务紧密合作,定义业务需求。然而,在数据工程领域,这还不够。在很多情况下,存在隐藏的需求或异常。为了减轻这些问题,业务分析师与数据架构师团队合作分析数据以及系统的当前状态,包括任何现有解决方案、当前成本或由于问题导致的收入损失以及数据所在的基础设施。这有助于细化业务需求。一旦业务需求大致确定,数据架构师将业务需求映射到技术需求。

然后,数据架构师定义架构的标准和原则,并根据业务需求和预算确定架构的优先级。之后,数据架构师创建最合适的架构,以及它们所提出的科技栈。在这个阶段,数据架构师与数据工程师紧密合作,实施概念验证POCs),并从可行性、可扩展性和性能等方面评估所提出的解决方案。

最后,架构师根据之前定义的评估结果和架构优先级推荐解决方案。数据架构师向业务展示所提出的解决方案。根据成本、时间表、运营成本和资源可用性等优先级,从业务和客户那里获得反馈。需要几次迭代才能巩固并就架构达成一致。

一旦达成一致,解决方案将被实施。根据实施挑战和特定用例,架构可能会或可能不会进行修订或稍作调整。一旦架构实施并投入生产,它就进入了维护和运营阶段。在维护和运营期间,有时会提供反馈,这可能会导致一些架构的改进和变化,但如果解决方案最初就设计得很好,这些变化通常很少。

在前面的图中,蓝色框表示客户有主要参与,绿色框表示数据架构师有主要参与,黄色框表示数据架构师与其他利益相关者平等分担参与,灰色框表示数据架构师在该场景中参与最少

现在我们已经了解了数据架构的生命周期和数据架构师在各个阶段的角色,我们将关注如何减轻数据架构师面临的一些挑战。本书将以下方式介绍如何减轻这些挑战:

  • 理解业务数据、其特征和存储选项:

    • 数据及其特征在本章前面已讨论;它也将部分在第二章数据存储和数据库)中讨论

    • 存储选项将在第二章数据存储和数据库)中讨论

  • 分析和定义业务问题:

    • 理解各种数据工程问题(本章涵盖)

    • 我们在第四章ETL 数据加载 - 数据仓库中数据摄取的基于批处理解决方案,第五章构建批处理管道,以及第六章构建实时处理管道中提供了如何分析业务问题、分类和定义的逐步分析

  • 选择正确架构的挑战。为了选择正确的架构模式,我们应该了解以下内容:

    • 数据工程问题的类型和数据维度(本章已讨论)

    • 不同类型的数据和各种可用的数据存储(第二章数据存储和数据库

    • 如何在数据库中存储和建模不同类型的数据(第二章数据存储和数据库

    • 理解各种数据处理问题的架构模式(第七章核心架构设计模式

    • 理解发布数据的架构模式(第三部分使数据成为服务

  • 选择最佳匹配的技术堆栈和数据平台的挑战。为了选择正确的工具集,我们需要知道如何使用工具以及何时使用我们拥有的工具:

    • 如何选择正确的数据库将在第二章数据存储和数据库)中讨论

    • 如何选择正确的平台将在第三章识别正确的数据平台)中讨论

    • 第四章《批处理中不同工具的使用步骤指南 - 数据仓库中数据摄取的基于批处理解决方案》和第五章《构建批处理管道》中,我们将涵盖使用不同工具在批处理中进行操作的步骤指南。

    • 第六章《构建实时处理管道》中,我们将详细介绍如何逐步构建实时流处理和选择正确的工具。

    • 第九章《将 MongoDB 数据作为服务公开》和第十章《使用 GraphQL 的联邦和可扩展 DaaS》中,我们将讨论在数据发布中使用的不同工具和技术。

  • 第十一章《衡量性能和基准测试您的应用程序》中,我们将讨论构建可扩展性和性能的设计挑战。在这里,我们将讨论以下内容:

    • 性能工程基础

    • 发布性能基准

    • 性能优化和调整

  • 数据治理不足的挑战。在第八章《启用数据安全和治理》中,我们将讨论各种数据治理和安全原则以及工具。

  • 评估架构解决方案并向领导层推荐的挑战。在本书的最后一章第十二章《评估、推荐和展示您的解决方案》中,我们将利用本书学到的各种概念来创建可操作的数据指标,并确定最优化解决方案。最后,我们将讨论建筑师可以应用的技术,以有效地与业务利益相关者、高管领导和投资者进行沟通。

在本节中,我们讨论了这本书如何帮助建筑师克服他们将要面临的种种挑战,并使他们在其角色中更加高效。现在,让我们总结本章内容。

摘要

在本章中,我们学习了数据工程是什么,并查看了一些数据工程的实际示例。然后,我们涵盖了数据工程的基础,包括数据的维度和数据工程师解决的问题类型。我们还提供了对数据工程领域中各种处理问题和发布问题的概述。然后,我们讨论了数据架构师的角色和责任以及他们面临的挑战。我们还简要介绍了本书将如何指导你克服数据架构师面临的挑战和困境,帮助你成为一名更好的 Java 数据架构师。

现在你已经了解了数据工程的基本格局以及本书将重点关注的内容,在下一章中,我们将逐一介绍各种数据格式、数据存储选项以及数据库,并学习如何为当前的问题选择合适的方案。

第二章:数据存储和数据库

在上一章中,我们了解了现代数据工程的基础以及架构师应该做什么。我们还介绍了数据是如何以指数速度增长的。然而,为了利用这些数据,我们需要了解如何高效有效地存储它们。

在本章中,我们将专注于学习如何存储数据。我们将从了解各种数据类型和可用数据的各种格式开始。我们将简要讨论编码和压缩以及它们与各种数据类型的配合程度。然后,我们将学习关于文件和对象存储的知识,并比较这些数据存储技术。之后,我们将介绍现代数据工程中可用的各种数据库。我们将简要讨论选择特定用例的正确数据库的技术和技巧。然而,选择正确的数据库并不能保证构建出良好的解决方案。作为一名数据架构师,了解如何围绕数据库最佳设计解决方案非常重要,这样我们才能充分利用我们所选择的技术,并实施一个有效、健壮和可扩展的数据工程解决方案。

我们将本章的结尾讨论如何为不同类型的数据库设计数据模型。为了帮助您理解这些关键概念,我们将尽可能提供实际场景。

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

  • 理解数据类型、格式和编码

  • 理解文件、块和对象存储

  • 数据湖、数据仓库和数据集市

  • 数据库及其类型

  • 数据模型设计考虑因素

理解数据类型、格式和编码

在本节中,您将了解各种数据类型和数据格式。我们还将介绍压缩以及压缩和格式如何结合在一起。之后,我们将简要讨论数据编码。本节将为您理解这些数据的基本特征做好准备,这些特征将在我们讨论即将到来的数据存储和数据库部分时有用。

数据类型

所有在现代数据工程中使用的数据集可以大致分为以下三个类别:

  • 结构化数据:这是一种可以轻松映射到预定义结构或模式的类型的数据集。它通常指的是关系数据模型,其中每个数据元素都可以映射到预定义的字段。在结构化数据集中,通常字段的数量、数据类型以及字段的顺序都是明确定义的。最常见的例子是关系数据结构,我们用实体和关系来描述数据结构。这种关系数据结构可以用脚注形符号表示。如果您想了解脚注形符号的基础知识,请参阅vertabelo.com/blog/crow-s-foot-notation。以下图表展示了结构化数据在脚注形符号中的示例:

图 2.1 – 结构化数据表示

图 2.1 – 结构化数据表示

在前面的图表中,我们可以看到一个由三个结构化数据集组成的系统,称为“客户”、“订单”和“产品”。这些数据集每个都有固定数量的字段和相应的数据类型。我们还可以看到数据集之间的关系。例如,在这里,“订单”通过customer_id与“客户”相关联,“订单”通过product_id与“产品”相关联。由于结构化数据集之间存在关系,它们也被称为关系数据模型。

  • 不结构化数据:这是一种不符合任何预定义数据模型的数据或数据集。由于缺乏任何内部结构,它们不能被任何关系数据存储,如关系数据库管理系统RDBMSs)存储。此外,由于没有与之关联的模式,查询和搜索不像在结构化数据模型中那样容易。

大约 70%的系统生成数据是不结构化的。它们可以由人类或机器生成:

  • 人类生成的不结构化数据:一些人类生成的不结构化数据集的例子包括媒体文件,如音频和视频文件、聊天、即时消息、电话录音和短信

  • 机器生成的不结构化数据:一些机器生成的不结构化数据的例子包括科学数据,如地震图像、数字监控和卫星图像

  • 半结构化数据:这是一种类型的数据集,与关系数据模型不同,它不包含表格结构,但仍然包含标记或标签来定义数据模型的层次结构和字段名称。半结构化数据是分层的。半结构化数据对于不同系统之间的平台和编程语言无关的通信特别有用。在我们讨论几种半结构化数据类型之前,让我们看看一个现实世界的例子。

万事达卡、维萨和 美国运通Amex)是连接支付处理器和发行人的卡网络。通常,在卡网络上有很多 商业对商业B2B)的销售,其中商家购买订阅计划以接受卡网络,从而增加卡网络的收入流。例如,我的牙医只接受万事达卡和美国运通,而美国仓储式会员商店现在在美国各地只接受维萨卡。每个这些庞大的卡网络都有许多 Salesforce org 或业务单元,如会计、销售和营销。

假设维萨想要生成一个销售评分和最佳接触 B2B 客户的时间。通过 Salesforce 从营销和会计收集的信息将被一个基于实时 机器学习ML)的应用程序使用,该应用程序将生成并附加销售评分和最佳接触客户的时间。这个增强的记录,连同这些额外信息,必须流向 Salesforce org 以进行销售。Salesforce 通常在 Salesforce 云上使用 APEX 作为语言(该云可能托管在不同的操作系统上),而生成评分和最佳通话时间的 AI 应用程序是用 Java 和 Python 编写的,并位于本地 Kubernetes 集群之上。为了使这些异构系统(具有不同的操作系统和不同的语言)之间的消息能够轻松通信,我们将使用一种半结构化数据(JSON)的形式,这种数据与操作系统或涉及此用例的不同应用程序的语言无关。

现在,让我们看看几种最流行的半结构化数据类型:

  • JSONJavaScript 对象表示法的缩写。根据维基百科,它是一种“使用人类可读的文本来传输由属性-值对组成的数据对象的开放标准格式。”以下示例由键值对组成,其中值可以是另一个 JSON。这个 JSON 至少有一个键值对,是一个值;这被称为嵌套 JSON。JSON 对象的数组称为 JSON 数组

以下是一个 JSON 的示例:

{
  "customerId": 47,
  "firstname": "Lilith",
  "lastname": "Wolfgram",
  "address": "324 Spring Ln",
  "city": "Hanover",
  "country": "Micronesia, Federated States of",
  "countryCode": "FO",
  "email": "Lilith.Wolfgram@gmail.com",
  "bills": [
    {
      "billId": 0,
      "billAmount": 4801.98,
      "paymentStatus": false,
      "dueDt": "2020-12-20"
    },
    {
      "billId": 1,
      "billAmount": 668.71,
      "paymentStatus": false,
      "dueDt": "2020-12-27"
    },
    {
      "billId": 2,
      "billAmount": 977.94,
      "paymentStatus": true,
      "dueDt": "2020-11-24"
    }
  ]
}

如您所见,有一些表示字段名称的标签,例如 ‘customerId’‘bills’ 标签的值是一个 JSON 对象的数组,因此其值是一个 JSON 数组。此外,由于 ‘bills’ 不是一个基本数据类型,而是另一个 JSON,因此前面的 JSON 是一个嵌套的 JSON 对象,显示了 JSON 的层次结构。

  • XML 表示 可扩展标记语言。从其名称可以看出,它是一种开放的数据格式,既适合人类阅读也适合机器阅读,其中每个数据值都通过一个标签进行标记或标记,该标签表示字段的名称。XML 在以平台和语言无关的方式在异构系统之间传递信息方面与 JSON 非常相似。像 JSON 一样,XML 也是一种层次化的数据结构。XML 是 wsdl SOAP API 的既定标准。以下是为之前描述的 JSON 描述的 XML 结构:

    <?xml version="1.0" encoding="UTF-8" ?>
    <root>
        <customerId>47</customerId>
        <firstname>Lilith</firstname>
        <lastname>Wolfgram</lastname>
        <address>324 Spring Ln</address>
        <city>Hanover</city>
        <country>Micronesia, Federated States of</country>
        <countryCode>FO</countryCode>
        <email>Lilith.Wolfgram@gmail.com</email>
        <bills>
            <bill>
                <billId>0</billId>
                <billAmount>4801.98</billAmount>
                <paymentStatus>false</paymentStatus>
                <dueDt>2020-12-20</dueDt>
            </bill>
            ...
        </bills>
    </root>
    

前一个代码片段的完整源代码可在 GitHub 上找到:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/blob/main/Chapter02/sample.xml

如您所见,每个 XML 都以一个root标签开始,每个值都被标签名称封装。在本节中,我们探讨了各种数据类型,但我们需要了解这些类型的数据是如何格式化的。因此,在下一节中,我们将讨论各种数据格式。

数据格式

在数据工程中,数据集可以存储在不同的文件格式中。每种格式都有其优缺点。然而,了解哪种数据格式比其他数据格式更适合某些类型的用例是很重要的。在本节中,我们将讨论数据格式的各种特点、一些流行的数据格式以及它们适用的用例。

数据格式的特点

首先,让我们回顾一下数据格式的各种特点,这些特点使它们彼此不同。这些特点还决定了在解决业务问题时,何时应该选择特定数据类型而不是其他数据类型。数据格式的主要特点如下:

  • .jpg文件只能由 Photos 等应用程序打开。另一方面,文本文件只能包含字符,可以在任何文本编辑器中打开,并且是可读的。例如,任何可以通过记事本等文本编辑器打开的.txt文件都是文本文件。

  • 模式支持:模式是一个大纲、图表或模型,它定义了各种类型数据的结构。模式存储字段级别的信息,如数据类型最大大小默认值。模式可以与数据相关联,这有助于以下方面:

    • 数据验证

    • 数据序列化和压缩

    • 一种将数据传达给所有消费者以便轻松理解和解释的方法

数据格式可能支持或不支持模式强制。此外,模式可以与数据一起包含,或者可以与模式注册表分开共享。模式注册表是一个集中注册模式的地方,以便不同的应用程序可以独立地添加或删除字段,从而实现更好的解耦。这使得它适合模式演变和验证。

  • 模式演变:随着业务的增长,更多的列被添加或列数据类型发生变化,这导致模式随时间而变化。即使模式在演变,保持旧数据的向后兼容性也很重要。模式演变提供了一种在保持向后兼容性的同时更新模式的方法。一些数据格式,如Avro,支持模式演变,这在敏捷业务中很有帮助;因为数据集的模式可能会随时间变化。

  • 行与列存储:为了理解行与列存储的区别,让我们看一下以下图表:

图 2.2 – 行与列式存储

图 2.2 – 行与列式存储

前面的图表显示了相同的数据在基于行存储和基于列存储中的存储方式。这个例子显示了销售数据以列式格式存储与基于行的数据存储格式相比的情况。在基于行的格式中,所有数据都是按行存储的——也就是说,特定行的列是相邻存储的。由于数据是按行存储的,因此它非常适合那些更倾向于按行读取或写入数据的场景,例如在在线事务处理OLTP)中。

另一方面,正如前一个图表所示,列式存储将同一列的值存储在相邻的内存块中。因此,列是存储在一起的。由于它以列式存储数据,可以通过一次存储重复的列值和每行的指针来优化存储空间(这在前一个图表的“列式存储”部分通过重复的Clothes值表示)。这种存储方式对于只重复读取部分列且不期望进行事务性写入的场景非常有用。两个典型的用例是在线分析处理OLAP)和大数据处理。两者主要用于对大型数据集进行查询分析。在大数据中,列式格式提供了可分割的优势,并允许创建分区,这有助于加快数据处理速度。

  • 可分割性:另一个重要因素是文件是否可以被分割或拆分为多个文件。当数据量巨大或速度过高时,例如在大数据中,这个因素就会发挥作用。如果底层数据格式是可分割的,大数据文件可以存储在分布式文件系统如HDFS中。这样做,处理这种分割的大数据会变得更快。

  • 压缩:数据处理性能通常取决于数据的大小。压缩可以减小磁盘上的数据大小,从而提高网络 I/O 性能(然而,在处理时可能需要更多时间来解压)。它还可以在数据通过网络流动时减小数据包的大小,从而降低数据传输速率。下表显示了几个流行的数据压缩算法及其特性:

名称 无损压缩 压缩比率 可分割 压缩速度 解压速度
Gzip 2.7x—3x 100 MBps 440 MBps
Snappy 2x 580 MBps 2020 MBps
LZ4 2.5x 800 MBps 4220 MBps
Zstd 2.8x 530 MBps 1360 MBps

表 2.1 – 不同的压缩技术

  • 配套技术:有时,数据格式的选择取决于配套技术。例如,在一个 Hadoop 环境中,如果我们计划使用 Hive MapReduce 作业处理数据,那么使用 ORC 格式而不是 Parquet 格式可能是一个好主意。但另一方面,如果我们所有的转换都是使用 Apache Spark 完成的,那么 Parquet 可能是一个更好的选择。

在本节中,我们学习了各种数据格式的特性和特点,以及它们如何影响数据元素的存储和处理。然而,对于一个架构师来说,了解流行的数据格式以及如何明智地使用它们是非常重要的。

流行数据格式

在本节中,我们将讨论一些值得了解的流行数据格式。当你尝试开发数据工程解决方案时,你将遇到这些格式。它们如下(我们在“半结构化数据”部分和“数据类型”子部分中介绍了两种流行的数据格式,JSON 和 XML):

  • 分隔符分隔格式:这是一种文本数据格式,其中换行符用作记录分隔符,并且可以根据我们处理的是哪种类型的分隔符分隔文件来使用特定的字段分隔符。两种最流行的分隔符分隔格式是逗号分隔值CSV)和制表符分隔值TSV)。在 CSV 中,字段分隔符是逗号,而对于 TSV,它是制表符。它们可以可选地包含一个标题记录。尽管它不支持拆分,但它提供了非常好的压缩率。此格式不支持空值或模式演变。

由于格式的简单性,它在批量处理场景以及实时流处理中也非常受欢迎。然而,缺乏模式演变、分区能力和非标准化格式使得其使用有限,并且不推荐用于许多用例。

  • avro-tools-<version>.jar。将 Avro 转换为人类可读的 JSON 格式的命令如下:

    java -jar ~/avro-tools-1.7.4.jar tojson filename.avro
    

avro数据总是伴随着其模式,可以使用avro-tools-<version>.jar读取,如下所示:

java -jar ~/avro-tools-1.7.4.jar getschema filename.avro

如果我们有一个与在“数据类型”部分解释半结构化数据时描述的 JSON 等效的二进制 Avro 文件,那么avro模式将如下所示:

{
  "name": "MyClass",
  "type": "record",
  "namespace": "com.sample.avro",
  "fields": [
    {
      "name": "customerId",
      "type": "int"
    },
    {
      "name": "firstname",
      "type": "string"
    },
    {
      "name": "lastname",
      "type": "string"
    }
   ...  ]
}

上一段代码片段的完整源代码可在 GitHub 上找到,链接为github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/blob/main/Chapter02/avroschema.json

  • Parquet 是一种开源的基于列的数据存储,非常适合分析负载。它是由 Cloudera 与 Twitter 合作创建的。Parquet 在 大数据工程 中非常受欢迎,因为它提供了许多存储优化选项,以及提供了出色的列式压缩和优化。与 Avro 一样,它也支持可分割文件和模式演变。它非常灵活,并且对嵌套数据结构有很好的支持。Parquet 提供了出色的读取性能;它与 Apache Spark 工作得非常好

  • 让我们尝试理解 Parquet 文件的结构。以下图表显示了 Parquet 文件的结构:图 2.3 – Parquet 文件格式

图 2.3 – Parquet 文件格式

Parquet 文件包含一个头部和一个尾部。头部仅由一个名为 PAR1 的标记组成,表示它是一个 Parquet 文件。然后,文件被分为行组,其中每个行组表示包含在数据块中的一组行。每个数据块的大小等于 Parquet 文件的块大小(默认为 128 MB)。每个行组包含每个列的一个数据块。同样,每个列数据块由一个或多个页面组成。列中的每个页面包含 n 行,其大小小于或等于配置的页面大小。每个列数据块还存储元数据(最小/最大值、空值数量等)。尾部包含所有行组的元数据以及数据的模式。

  • 优化行列存储ORC):这是由 Hortonworks 在与 Facebook 合作下,在 Hadoop 生态系统内开发的另一种开源文件格式。ORC 是一种支持出色压缩和列优化的列式数据存储形式。让我们看看 ORC 文件结构,以了解它与 Parquet 格式的不同之处。以下图表显示了 ORC 文件格式的结构:

图 2.4 – ORC 文件结构

图 2.4 – ORC 文件结构

在 ORC 格式中,每个文件由多个条带组成。默认条带大小为 250 MB。每个条带被细分为索引数据、行数据和条带尾部。索引数据包含索引,行数据由实际数据组成,但它们都以列式格式存储。条带尾部包含列编码及其位置。文件尾部包含有关条带列表、每个条带的行数以及每列的数据类型的信息。除此之外,它还包含条带级别的统计信息,如最小值、最大值和总和。最后,后缀包含有关文件尾部长度、元数据部分和压缩相关信息的说明。

现在我们来了解如何从不同的数据格式中进行选择。

如何在 Avro、Parquet 和 ORC 之间进行选择

要选择正确的数据格式,我们必须考虑以下因素:

  • 读写密集型查询模式:对于读写密集型用例,基于行的格式工作得更好,因为追加新记录变得更容易。因此,对于读写密集型用例,Avro 会是一个更好的选择。另一方面,如果读写密集型用例需要更频繁地读取列的子集,那么像 Parquet 或 ORC 这样的列式数据格式是一个合适的选择。

  • 压缩:在选择数据格式时,这是一个非常重要的方面,因为压缩可以减少存储或传输数据所需的时间和存储空间。对于大数据用例,压缩起着巨大的作用。基于行的存储不适合这种场景。因此,对于大数据分析用例,像 Parquet 或 ORC 这样的列式存储更受欢迎。此外,如果转换/处理大数据产生了大量的中间读取和写入,则需要更多的压缩。在这种情况下,ORC 是首选的,因为它比 Parquet 提供了更好的压缩比率。例如,如果您在 Hive 上使用 MapReduce 引擎运行 MapReduce 作业或Hive 查询语言HQL)查询,ORC 的表现将优于 Parquet。

  • 模式演进:在许多数据工程用例中,随着新列的添加或删除以及业务需求的变化,模式会随着时间的推移而频繁变化。如果数据模式经常发生变化,并且您需要向后兼容性,那么 Avro 是最佳选择。Avro 支持非常先进的模式演进、兼容性和版本控制,同时保持模式定义在 JSON 格式中的简单性。

  • 嵌套列:如果您的用例适合基于行的格式,Avro 与嵌套列结构配合得很好。否则,如果用例适合列式数据格式,并且您有很多嵌套的复杂列,那么 Parquet 是这种用例的理想数据格式。

  • 平台支持:最后,平台或框架起着非常重要的作用。Hive 与 ORC 配合得最好,而 Apache Spark 和 Delta Lake 对 Parquet 有很好的支持。对于 Kafka,Avro 或 JSON 通常是一个不错的选择。

在本节中,我们学习了各种数据格式,如文本、Parquet、Avro 等。在下一节中,我们将学习如何使用不同的数据存储格式来存储数据(这些数据可以是文本、Parquet 或任何其他格式)。

理解文件、块和对象存储

在本节中,我们将介绍对计划存储数据的架构师至关重要的各种数据存储格式。数据存储格式以不同的方式组织、保存和展示数据,每种格式都有其优缺点。可用的数据存储格式有文件、块和对象。

文件存储将数据组织并暴露为文件和文件夹的层次结构,而块存储将数据划分为块,并将它们存储在组织良好、大小均匀的卷中。最后,对象存储以空间优化的方式管理数据,并将其与其关联的元数据链接起来。

现在,让我们深入探讨它们的基本概念、优缺点以及它们被应用的使用场景。让我们从最简单、最古老的一种:文件存储开始讨论。

文件存储

在文件级存储中,数据以单个信息块的形式存储在文件中。这个文件被赋予一个名称,可以包含元数据,并位于目录或子目录中。当你需要查找文件时,计算机需要知道文件的绝对路径以进行搜索和读取文件。

文件存储的优缺点如下:

  • 优点:简单、功能广泛,可以存储任何东西

  • 缺点:不适合存储大量数据,因为没有扩展上规模的选择,只有扩展到规模的选择

一些典型的用例如下:

  • 文件存储因其简单性非常适合办公室和其他环境中的文件共享;例如,NAS。

  • 本地归档。NAS 为存储归档数据提供了出色的支持。

  • 数据保护和安全。文件级存储是一种老技术,但由于时间的考验和广泛的应用,其策略、标准和保护能力都很先进。这使得它成为数据保护用例的理想选择。

让我们现在来看看块级存储。

块级存储

在块级存储中,数据被分成小块数据,并分配唯一的块标识符。由于数据块小且具有唯一标识符,它可以存储在任何地方。此外,一组数据块组成一个逻辑单元,称为卷。在块级存储中,你可以通过添加块轻松地添加数据卷以扩展基础设施。

其中一个有趣的地方在于它如何处理元数据。与基于文件的架构不同,除了地址之外,块存储没有其他附加细节。在这里,操作系统控制存储管理,这使得它成为高性能用例的理想存储。

块级存储的优缺点如下:

  • 优点:通过控制操作系统或数据库来处理元数据,使其性能极高。你也可以轻松地扩展和缩减存储。

  • 缺点:可能很昂贵。此外,在应用层外部化元数据处理意味着在管理元数据时会有更多麻烦。

一些典型的用例如下:

  • 数据库:数据库通常使用块存储。例如,AWS 关系数据服务使用 AWS Elastic Block Storage 卷作为其存储来存储数据。

  • 虚拟化:如VMwareHyper-VOracle VirtualBox等虚拟化软件使用块存储作为虚拟操作系统的文件系统。

  • 基于云的实例:如 AWS EC2 等基于云的实例使用块存储(AWS Elastic Block Storage)作为其硬盘存储。

  • 电子邮件服务器:微软的电子邮件服务器 Exchange 使用块存储作为其标准存储系统。

让我们接下来看看对象级存储。

对象存储

对象级存储将数据存储在称为对象的独立容器中,这些对象具有唯一的标识符和平坦的结构。这使得数据检索变得非常容易,因为你可以通过使用唯一的标识符来检索对象,而不管它存储的位置在哪里。

对象级存储的优缺点如下:

  • 优点:对象存储提供了极大的元数据灵活性。例如,你可以自定义元数据,以便应用程序与对象相关联,或者你可以将应用程序的优先级设置为对象。你几乎可以做任何自定义。这种灵活性使得对象存储强大且易于管理。

除了元数据灵活性外,对象存储因其可访问性而闻名,因为它有一个 REST API 来访问,这使得它可以从任何平台或语言访问。

对象存储具有极高的可扩展性。扩展对象架构就像向现有的存储集群添加节点一样简单。随着数据的快速增长和云计算的按需付费模式,这一特性帮助对象存储成为当前和未来数据工程需求中最受欢迎的存储方式。

  • 缺点:尽管有这么多优点,对象存储也存在一些缺点。最明显和最突出的一点是对象无法修改。然而,你可以创建一个新版本的对象。在某些用例中,如大数据处理,这反而是一个福音而不是头痛。

一些典型的用例如下:

  • 大数据:由于可扩展性和元数据灵活性,大量数据以及非结构化数据可以轻松存储和从对象存储中读取。这使得它非常适合大数据存储。

  • :再次,由于可扩展性,对象存储是云系统的理想候选者。Amazon S3 是亚马逊的对象存储解决方案,并且非常受欢迎。此外,可定制的元数据有助于通过 AWS 控制台或其 SDKs 定义 Amazon S3 对象的生存周期。

  • Web 应用:对象存储通过 REST API 轻松访问,使其成为用作 Web 应用后端的理想候选者。例如,AWS S3 单独就被用作静态网站的廉价且快速的备份。

有了这些,我们已经涵盖了各种数据存储方式。在下一节中,我们将学习企业数据(存储在任何上述存储格式中)是如何组织成不同类型的数据仓库的,这使得其他应用程序能够检索、分析和查询这些数据。

数据湖、数据仓库和数据集市

要构建数据架构,架构师需要了解数据湖、数据仓库和数据集市之间的基本概念和区别。在本节中,我们将介绍现代数据架构生态系统,以及数据湖、数据仓库和数据集市在该景观中的位置。

以下图表展示了现代数据架构的概览:

图 2.5 – 现代数据架构概览

图 2.5 – 现代数据架构景观

如我们所见,各种类型的数据被摄入到数据湖中,并在原始区域着陆。数据湖由直接从数据源摄入的结构化、半结构化和非结构化数据组成。数据湖有一个区域,包含清洗、转换和排序后的数据集,为各种下游数据处理活动提供服务,如数据分析、高级分析、作为数据即服务的发布、人工智能、机器学习等。这被称为精选区域。数据湖作为创建数据仓库的来源,而数据仓库是一个为特定业务线构建的结构化数据存储库。

数据湖

在现代数据架构中,来自各种来源的数据被摄入到数据湖中。数据湖是一个包含结构化、半结构化和非结构化数据的数据存储库。在大多数情况下,数据湖中数据的用途不是预先定义的。通常,一旦数据被摄入并存储在数据湖中,各个团队就会使用这些数据进行分析、报告、商业智能和其他用途。

然而,在内部,数据湖包含不同的数据区域。以下是在数据湖中可用的不同数据区域:

  • 原始数据区域:来自各种数据源的原生数据被加载到这个区域。在这里,加载的数据是原始形式的。这些数据可能是非结构化的、未清洗的、未格式化的。这也被称为着陆区。

  • 主数据区域:这个数据区域通常包含参考数据,它增强了原始区域或精选区域中数据的分析或转换活动。

  • 用户数据区域:在某些数据湖中,用户可以手动删除某些数据。它们通常是静态的。这个数据湖的部分被称为用户数据区域。

  • 精选数据区域:这是数据湖的数据发布层。它包含清洗、转换和排序后的数据。这个层中的数据通常是结构化的。数据可能存储在大平面文件中、作为键值存储、作为数据文档、在星型模式中或以非规范化格式。所有数据治理、数据管理和安全策略都适用于这一层,因为这是数据湖的主要消费层。

  • 存档数据区域:存档区域由其他系统(如数据仓库或精选区域)因老化而卸载的数据组成。这个区域中的数据通常不能修改,但可以追加。这类数据用于历史分析或审计目的。通常,使用更便宜的数据存储技术来存储存档数据。例如,Amazon S3 等技术提供更高级的能力,可以自动使用 S3 存储桶的生命周期策略随着时间的推移将数据逐步迁移到更便宜解决方案。

让我们继续讨论数据仓库。

数据仓库

数据仓库是一个经过排序的中心存储库,它以结构化且用户友好的方式收集了来自多个数据源的信息,用于数据分析。在将数据导入数据仓库之前,需要进行大量的发现、分析、规划和数据建模工作。数据仓库中的数据经过高度清洗、转换和结构化。正如图 2.6所示,数据仓库在现代数据工程管道中是从数据湖构建的。虽然数据湖通常是企业或组织的集中式原始数据区域,但数据仓库通常是按业务单元或部门构建的。每个数据仓库的结构都是针对特定部门的需求而设计的。关于数据仓库及其模式类型将详细讨论于第四章ETL 数据加载 – 数据仓库中的批量数据导入解决方案

数据集市

数据集市通常是数据仓库的一个子集,专注于单一业务线。虽然数据仓库的大小通常是几个 100GB 到 TB,但数据集市的大小通常小于 100GB。数据集市提供了出色的读取性能,因为它包含针对特定业务线分析、设计和存储的数据。例如,从集中的公司数据仓库中,可以为人力资源部门、财务部门和销售部门分别有一个特定的数据集市。

下表捕捉了数据湖与数据仓库之间的差异:

特征 数据湖 数据仓库
加载模式 ETL(提取、加载和转换) ETL(提取、转换和加载)
存储的数据类型 结构化、半结构化和非结构化 结构化
分析模式 获取、分析,然后确定整理数据的结构 首先创建结构,然后获取数据以获取洞察
数据摄入模式 批处理、实时、接近实时的批处理 批处理
模式应用时间 读取时应用模式,即模式在读取数据时应用 写入时确定模式,即模式在数据写入时确定并可用

表 2.2 – 数据湖与数据仓库的比较

到目前为止,我们已经学习了各种数据存储库的使用方法以及它们如何使企业数据平台成为可能。这些存储库中的数据可以存储为文件或对象,但它们也可以存储在称为数据库的有序数据集中,这样就可以轻松检索、管理和搜索数据。在下一节中,我们将详细讨论数据库。

数据库及其类型

在本节中,我们将介绍各种常用的数据库类型,用于创建现代数据工程解决方案。我们还将尝试探索在特定情况下使用特定类型数据库的可能场景。

数据库是一个系统化的数据或信息集合,以易于访问、检索和管理的方式存储。在现代数据工程中,数据库可以大致分为两大类,如下所示:

  • 关系型数据库:这是一种以存储结构化数据集而闻名的数据库。每种数据集都与另一种数据集相关联,关系型数据库提供了一种简单的方法来建立不同类型数据集之间的关系。我们将在本章后面详细讨论关系型数据库。

  • NoSQL 数据库非关系型数据库:NoSQL 数据库是非关系型数据库,数据可以以表格格式以外的某种形式存储。NoSQL 支持非结构化、半结构化和结构化数据。难怪 NoSQL 代表的是不仅限于 SQL

以下图展示了在现代数据工程环境中使用的数据库类型:

图 2.6 – 数据库类型

图 2.6 – 数据库类型

现在,让我们详细讨论各种数据库类型。

关系型数据库

如前所述,关系型数据库存储结构化数据。关系型数据库中的每种数据类型都存储在一个称为数据库表或简称为表的容器中。在数据加载到表中之前,必须首先定义每个表。表定义包含列名或字段名、它们的数据类型以及它们的大小(可选)。关系型数据库进一步细分为两种类型:层次数据库和关系数据库管理系统(RDBMS)。

层次数据库

这些是数据以树状结构存储的数据库。数据库由一系列数据记录组成。每个记录包含一组字段,这些字段由记录类型确定(这也可以称为段)。每个段可以通过称为链接的关系与另一个段相关联。这类数据库以父子关系而闻名。模型简单,但只能支持一对一和多对一的关系。以下图展示了层次数据库模型的一个示例:

图 2.7 – 层次数据模型的示例

图 2.7 – 层次数据模型的示例

如前图所示,Member是根段。Member段的记录包含 ID 001。根段有两个子段,称为AddressLanguage。在Address Segment部分,我们可以看到三个记录实例——即Address MailAddress HomeAddress WorkLanguage Segment部分也有例如SpokenWritten这样的实例。

层次数据库的例子包括 IBM 信息管理系统IMS)和 RDM Mobile。

RDBMS

RDBMS 是一种使用 SQL 作为其编程和查询接口的关系型数据库管理系统。它是整个行业中最为流行和成熟的数据库类型。数据存储在表中,表代表特定的实体。表有一组明确定义的列,以及它们的数据类型。表中的每一行称为一条记录。每个表可以包含唯一标识记录的主键。每个表支持多种索引。一个表可以通过外键索引与另一个表相连接。RDBMS 可以支持一对一和多对多关系。它们非常强大,并且已经成为了过去几十年大多数现代应用背后的动力源泉。

RDBMS 的例子包括 MySQL、Oracle、PostgreSQL、Amazon RDS 和 Azure SQL。

何时使用:RDBMS 几乎在需要多行 ACID 事务和需要复杂连接的任何地方都会被使用。Web 应用、员工管理系统和金融机构的在线交易是一些 RDBMS 被使用的例子。

NoSQL 数据库

如本节前面所述,NoSQL 支持非结构化数据以及半结构化数据。这是因为它支持灵活的模式。此外,NoSQL 数据库以分布式方式存储和处理数据,因此可以无限扩展。NoSQL 数据库架构中分布式计算的使用有助于它们支持巨大的数据量,使它们成为大数据处理的一个很好的选择。以下图表展示了关系型数据库和 NoSQL 数据库处理扩展的不同方式:

图 2.8 – 升级与扩展对比

图 2.8 – 升级与扩展对比

如我们所见,关系型数据库扩展的是相同的实例。然而,这创造了一个扩展的限制。此外,升级是一个成本较高的操作。另一方面,NoSQL 使用的是廉价的通用硬件,其架构是这样的,为了扩展,它需要向外扩展。这意味着 NoSQL 可以无限扩展,并且扩展成本更低。

NoSQL 数据库可以进一步细分为特定类型的数据库。我们将简要讨论每个类型,并提供示例和用法。

键值存储

键值存储是最简单的 NoSQL 数据库类型。存储的数据以键值格式存在。属性名称存储在中,而属性值存储在中。在这里,键需要是一个字符串,但值可以是任何类型的对象。这意味着值可以是 JSON、XML 或某些自定义序列化对象。

一些键值存储的例子包括 Redis、Memcached 和 RocksDB。

何时使用

  • 在一个微服务或应用程序中。如果你需要一个需要快速读取的查找表,那么内存中的键值存储,如 Redis 和 Memcached,是一个不错的选择。同样,虽然 Memcached 支持并发读取,但它不支持像 Redis 那样复杂的值。云服务如 AWS ElastiCache 支持这两种数据库。如果你感兴趣,你可以在aws.amazon.com/elasticache/redis-vs-memcached/找到 Redis 和 Memcached 之间更详细的比较。

  • 在实时事件流处理中,如果当前事件处理依赖于较旧事件的状态,则需要维护状态。这种实时处理称为有状态流处理。在有状态流处理中,RocksDB 是一个很好的选择,可以将状态作为键值对来维护。Kafka Streams 内部使用 RocksDB 来维护有状态流处理的状态。

接下来,让我们来看看基于文档的数据库。

基于文档的数据库

文档数据库是 NoSQL 数据库,它为你提供了一种简单的方式来存储和查询文档数据。文档被定义为一种半结构化数据格式,如 JSON 或 XML。文档数据库还支持嵌套元素,如嵌套 JSON 和 JSON 数组。文档数据库中的每个文档都存储在一个键值对中,其中键是文档的唯一 ID,值是存储的文档。文档数据库支持对文档的任何字段进行索引,即使它是嵌套字段。

一些基于文档的数据库的例子包括 MongoDB、Apache CouchDB、Azure Cosmos DB、AWS DocumentDB 和 ElasticSearch。

何时使用

  • 当你想要通过微服务或 REST API 将数据湖或数据集市中的精选数据发布到 Web 应用程序时。由于 Web 应用程序运行在 JavaScript 上,它们可以轻松解析 JSON 文档。在 MongoDB 或 AWS DocumentDB 等文档数据库中存储精心设计的 JSON 文档,可以为 Web 应用程序提供惊人的性能。

  • 如果你正在接收来自多个动态数据源的数据,例如来自 Twitter、LinkedIn 和 Facebook 的社交媒体源,并且这些源的模式正在演变,你必须通过提取某些数据点或对它们进行某种聚合来一起处理和发布这些数据,那么 Apache CouchDB 可能是一个极好的选择。简单来说,如果你正在消费文档数据并且无法控制传入的模式,基于文档的数据存储是一个很好的选择。

  • 如果你的查找需求无法由键值存储来满足。如果值是一个具有非常复杂模式或由于数据量过大而导致键值存储中的存储成本变得过高的文档,那么基于文档的数据库是下一个最明显的选择。

  • 如果你正在为业务创建一个搜索存储库,那么你可能希望将数据存储在搜索引擎存储中,如基于文档的数据库 Elasticsearch。它在存储数据时创建反向文本索引(称为 Lucene 索引)。这是一个特殊的基于文档的数据库,其中每个记录都存储为文档,并附带一个唯一的键。Elasticsearch 提供了惊人的搜索性能。然而,只有当你想要在数据上执行高性能的基于文本的搜索或从数据中创建一些可视化时,才应该将数据存储在 Elasticsearch 中。

现在我们来探讨列式数据库。

列式数据库

列式数据库以列式格式存储数据。列式数据库是使用 Bigtable 创建的。根据谷歌发布的一篇介绍 Bigtable 的论文,它是一个稀疏的、分布式的、持久的、多维排序映射。在其核心,每个列式数据库都是一个映射。在这里,每个数据记录都与一个称为行键的键相关联。这些键是唯一的,并且按字典顺序排序。存储在列式数据库中的数据持久化在提供高数据可用性的分布式文件系统中。在列式数据库中,我们定义列族而不是列。每个列族可以包含任意数量的列。列族内部的列对于所有记录不是固定的,并且可以动态添加。这意味着在大多数数据记录中,一个或多个列可能是空的或不存在,因此这种数据结构是稀疏的。这允许你动态地向记录添加列。这使得列式数据库成为存储非结构化数据的一个很好的选择。以下图表试图捕捉列式数据库的精髓:

图 2.9 – 列式数据库结构

图 2.9 – 列式数据库结构

如前图所示,记录被划分为区域。一个或多个区域位于分布式文件系统(如 HDFS 或 GFS)的节点上。区域内部的每个列族都存储为单独的文件。同样,列族内部的每个列都可以支持版本控制,这使得列式存储真正是多维的。

例如,包括 Apache HBase、Cassandra 和 Apache Kudu。

何时使用:

  • 在广告公司和营销活动中,列式数据存储用于实时存储用户点击和用户选择的事件。这些实时事件被即时用于优化向用户展示的广告或发送给客户的优惠。

  • 另一个例子是从 Kafka 接收的事件流数据,这些事件数据量小。这些数据需要存储在 HDFS 中,以便可以定期使用某种形式的批量应用程序进行分析或处理。在这里,列式数据库是首选,因为直接在 HDFS 或类似 Hive 的仓库中存储数据将创建太多小文件,这反过来又会创建太多元数据,从而降低 Hadoop 集群的整体性能。当达到区域大小时,列式存储被写入磁盘,并且通常放置在顺序文件中,因此它们非常适合这种存储。

  • 它们是处理大量动态数据波动的优秀数据库。例如,在假日季节销售活动期间,它们非常适合处理大量数据激增。

接下来,让我们看看图数据库。

图数据库

图数据库是一种将数据存储在图结构中的数据库。本质上,这意味着图数据库不仅存储数据,还存储数据之间的关系。随着社交网络的出现以及每个领域的数据都变得更加相互连接,不仅需要查询数据,还需要查询数据之间的连接。在社交网络中,探索邻近数据点(例如,LinkedIn 需要探索数据邻近度以显示一个人是否作为一级、二级或三级连接与您的个人资料相连)是必要的。尽管可以使用连接来获取关系,但只有数据库原生支持关系时,才能有效地存储、处理和查询连接。

大多数图数据库使用一种流行的建模方法,称为属性图模型。在这里,数据被组织成节点、关系和属性。以下图显示了使用属性图模型存储数据的示例:

图 2.10 – 属性图模型的示例

图 2.10 – 属性图模型的示例

在属性图模型中,有节点和关系。例如,namedata_of_birthemployee_ID

关系是有向的,并使用命名连接在两个命名实体或节点之间。例如,如前图所示,HAS_CEOHAS_CEO关系,它有一个名为start_date的属性。

就像 SQL 标准用于查询 RDBMS 一样,图数据库可以使用 GQL 进行查询。GQL 是一个新宣布的 ISO 标准,有助于查询图数据库。更受欢迎的开源 GQL 之一是 openCypher。(您可以在opencypher.org/了解更多关于 openCypher 的信息。)其他流行的图数据库查询语言包括 Cypher、TinkerPop3 和 SPARQL。

一些图数据库的例子包括 Neo4J、ArangoDB、RedisGraph、Amazon Neptune 和 GraphDB。

何时使用

  • 欺诈电话检测。

  • 推荐引擎。

  • 旅行网站上的客户参与度。

  • 引用关系。例如,使用图数据库,医疗保健提供者可以识别他们可以从中获得推荐的各种其他提供者。这有助于针对特定客户并建立对双方都有益的关系。

  • 在营销活动中帮助识别连接网络中的影响者,通过查询特定节点的传入连接数。

在本节中,我们讨论了各种类型的数据库及其适用场景。我们涵盖了一些示例和样本用例,说明了为什么应该选择特定的数据库。在下一节中,我们将探讨数据架构师在设计各种数据库的数据模型时应考虑的一些因素。

数据模型设计考虑因素

在本节中,我们将简要讨论在设计前节所述的各种数据库的数据模型时应考虑的各种设计因素。在设计数据模型时需要考虑以下方面:

  • 规范化与反规范化:规范化是一种数据组织技术。它用于减少关系或关系集中的冗余。这在 RDBMS 中高度使用,并且在 RDBMS 中创建规范化的数据模型始终是最佳实践。在规范化的数据模型中,你将一个列存储在一个表中的一个(最合适的一个),而不是在多个表中存储相同的列。在获取数据时,如果你需要该列的数据,你可以连接表来获取该列。以下图表显示了使用鸟嘴图符号的规范化数据建模的示例:

图 2.11 – 规范化数据建模

图 2.11 – 规范化数据建模

在前面的图表中,没有任何列是重复或冗余的。现在,假设我们需要显示一个应该显示“客户名称”、“客户 ID”、“订单 ID”、“项目名称”和“订单日期”的订单。为了获取这些信息,我们可以编写一个连接查询,如下所示:

SELECT cust.CustomerID, orders.OrderID, items.ItemName,orders.OrderDate FROM Orders orders JOIN Customers cust ON orders.CustomerID=cust.CustomerID JOIN Order_items orderitem on orderitem.OrderID = orders.OrderID JOIN Items items ON items.ItemID = orderitem.ItemID

另一方面,如果我们为 NoSQL 数据库设计相同的内容,重点不应放在减少冗余或规范化数据上。相反,我们应该关注读取速度。这种设计思维方式的改变是由两个重要因素触发的。首先,NoSQL 与大量数据一起工作,并将数据存储在分布式商用硬件上。因此,数据存储成本不高,如果数据量在数百 TB 或 PB 级别,连接可能不会高效。其次,NoSQL 没有JOIN类型的查询,因为 NoSQL 数据库是非关系型的。以下是一个示例文档数据模型,用于存储需要获取的相同信息:

{
  OrderId: Int, //documentKey
  CustomerID: Int,
  OrderDes: String
  Items: [{
    itemId: Int,
    itemName: String,
    OrderDate: Date 
    }]
}

如我们所见,一个单独的文档包含了所有必要的信息,这意味着有很多冗余数据。然而,在大数据场景下,NoSQL 表现得非常出色,并且提供了优异的性能。

  • 查询优先与模式优先模式:在设计 NoSQL 数据模型时,你必须问自己将要在这个数据模型上运行哪些查询。NoSQL 中的数据模型设计通常从将在模型上运行的类型分析查询开始。这有助于为 NoSQL 数据库中的文档或记录设计正确的键。此外,在列式数据库的情况下,它有助于根据将要运行在数据上的查询来对列族中的列进行分组。这种优化有助于 NoSQL 数据库在大数据或非结构化数据上以惊人的性能运行查询。

另一方面,RDBMS 是设计用来存储在预定义的、经过规范化的模式中,其中关系定义得非常明确。由于 SQL 是一种声明性语言,并且可以在运行时查询任何相关表,因此在设计 RDBMS 数据模型时不会考虑查询。

  • 成本与速度优化:随着云数据库和基于云的解决方案的出现,理解成本考虑因素对于现代数据架构师来说是一个非常重要的因素。例如,当涉及到存储与每秒输入/输出操作数(IOPS)相比时,在基于云的模型中,IOPS 总是比存储更昂贵。然而,了解 RDBMS 或文档存储中 IOPS 的计算差异可以帮助你在长期内节省成本和精力。RDBMS 的 IOPS 基于页面或块大小。因此,RDBMS 的 IOPS 由它访问的页面数量决定。然而,在文档数据库中,IOPS 基于该数据库中发生的 DB 读取/写入次数。

另一个例子是,如果在 AWS DocumentDB 中,你提供了更多的索引,你可能会获得更好的速度,但过多的索引会显著增加 IOPS,因此可能会花费你更多。每个集合的索引安全限制是五个。

  • 索引:如果你有一个数据库,其中你有很多读取操作并且需要拥有出色的读取性能,那么你应该考虑在你的数据库中拥有索引。索引有助于提高你在数据库中的读取和更新性能。另一方面,如果你有一个写入密集型的应用程序,索引可能会减慢你的插入性能。

  • 数据分布:NoSQL 数据库基于横向扩展架构,数据存储和分布在商品节点上。NoSQL 数据库对大量数据具有出色性能的一个原因是它们可以在分布式节点上并行读取或写入数据。然而,如果设计不当,数据可能会存储不均匀,这可能导致大量数据存在于一个节点上。这种在分布式数据库中的数据不均匀分布称为数据倾斜

通常,一个节点包含异常大量数据的问题,这可能导致数据库的读写瓶颈,被称为热点问题。这种情况通常是由于对 NoSQL 数据库的设计原则缺乏理解以及关键设计不当造成的。在列式数据库中,选择增量序列号作为键通常会导致热点问题。相反,在文档和列式数据库中,应选择唯一键,并将几个键列值的组合以特定顺序连接起来,最好至少有一个是文本值。在设计键时使用如盐值和 MD5 加密等技术,有助于避免热点问题。

在本节中,我们介绍了在选择数据库后你应该考虑的最明显的设计考虑因素。虽然这些考虑因素对任何数据模型设计都是基本的,但还有其他更精细的数据模型设计技术,这些技术是特定于你所选择的数据库的。我们强烈建议你在设计数据模型之前,仔细阅读你所选择数据库的官方文档。

摘要

在本章中,我们介绍了各种可用的数据类型和数据格式。我们还讨论了在现代数据工程中使用的各种流行数据格式以及与每种格式兼容的压缩技术。一旦我们了解了数据类型和格式,我们就探索了各种数据存储格式——文件、块和对象存储——我们可以用来存储数据。然后,我们详细讨论了各种企业数据存储库——数据湖、数据仓库和数据集市。一旦我们涵盖了数据的基础知识,包括不同类型及其存储,我们简要地讨论了数据库及其类型。我们讨论了各种数据库的例子,每种数据库的独特卖点,以及在何时应该选择一种数据库而不是另一种。我们还探讨了何时应该使用数据库的可能用例。

最后,我们简要地概述了数据架构师在设计数据模型时应考虑的基本设计考虑因素。

现在你已经了解了数据类型、格式、数据库以及何时使用什么,在下一章中,我们将探讨数据工程解决方案可以部署和运行的各种平台。

第三章:确定合适的数据平台

在上一章中,我们讨论了各种数据类型、它们的格式和存储。我们还介绍了不同的数据库,并提供了它们的概述。然后,我们了解了在选择数据格式、存储类型或数据库时,我们应该比较的因素和特性,以有效地解决数据工程问题。

在本章中,我们将探讨各种流行的平台,这些平台可用于运行数据工程解决方案。你还将了解作为架构师在选择其中之一时应考虑的因素。为此,我们将讨论每个平台的详细信息和这些平台提供的替代方案。最后,你将学习如何充分利用这些平台,为业务问题构建一个高效、健壮且成本效益高的解决方案。

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

  • 虚拟化和容器化平台

  • Hadoop 平台

  • 云平台

  • 选择正确的平台

技术要求

完成本章内容,你需要以下内容:

  • JDK 1.8 或更高版本

  • Apache Maven 3.3 或更高版本

本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter03

虚拟化和容器化平台

随着信息技术(IT)在生活各个领域的普及,对 IT 基础设施的依赖性和可靠性成倍增加。现在,IT 运行着许多关键和实时的业务。这意味着维护或故障期间可以零或极小的停机时间。此外,快速实时需求也在增长。例如,在假日季节,在线购物网站上的流量巨大。因此,现在 IT 需要高度可用、弹性、灵活和快速。这些正是推动虚拟平台如虚拟化和容器化创建的原因。例如,总部位于英国的跨国金融公司巴克莱斯,由于创新和项目交付速度缓慢,正面临来自竞争对手的困难。其主要障碍之一是新服务器配置所需的时间。因此,他们决定使用 Red Hat OpenShift 来容器化他们的应用程序。这显著减少了从周级到小时级的配置时间。结果,上市时间变得超级快,这帮助巴克莱斯在竞争中保持领先。

虚拟化抽象化了硬件,并允许您在单个服务器或硬件上运行多个操作系统。它使用软件在硬件资源上创建一个虚拟抽象,这样多个虚拟机(VM)就可以在物理硬件上运行,它们拥有自己的虚拟操作系统(virtual OS)、虚拟 CPU(vCPU)、虚拟存储和虚拟网络。以下图表显示了虚拟化是如何工作的:

图 3.1 – 虚拟化

图 3.1 – 虚拟化

如前图所示,虚拟机在宿主机的帮助下运行。虚拟机可以在物理硬件(如服务器或计算机)上运行的软件或固件称为虚拟机管理程序。虚拟机管理程序创建虚拟机的物理机器称为宿主机,而虚拟机称为客户机。宿主机中的操作系统称为宿主操作系统,而虚拟机中的操作系统称为客户操作系统。

虚拟化的好处

以下是虚拟化的好处:

  • 更好的资源利用率:由于多个虚拟机在同一硬件上运行,存储/内存和网络等硬件资源可以更有效地用于服务可能在不同时间具有高负载的更多应用程序。由于创建虚拟机比创建新服务器快得多,因此可以在高需求负载周期中创建虚拟机,并在应用程序负载下降时关闭它们。

  • 更少的停机时间/更高的可用性:当物理服务器出现问题时,需要例行维护或需要升级,这会导致昂贵的停机时间。使用虚拟服务器时,应用程序可以轻松地在客户主机之间移动,以确保停机时间最小,仅为几分钟,而不是数小时或数天。

  • 更快上市和可扩展性:由于配置虚拟机只需几分钟而不是数周或数月,整体软件交付周期大大缩短。这使得测试更快,因为您可以使用虚拟机模拟生产环境。

  • 更快的灾难恢复(DR):与需要数小时或数天的物理服务器相比,虚拟机可以在几分钟内恢复。因此,虚拟机使我们能够实现更快的灾难恢复。

以下是一些流行的虚拟机示例:

  • 微软的 Hyper-V

  • VMware 的 vSphere

  • Oracle 的 VirtualBox

让我们看看虚拟机是如何工作的。我们将通过下载和安装 Oracle VirtualBox 来开始这个练习:

  1. 根据您的宿主操作系统,您可以从www.virtualbox.org/下载 Oracle VirtualBox 的相应安装程序。

然后,按照www.virtualbox.org/manual/ch02.xhtml中的安装说明安装 VirtualBox。这些说明可能因操作系统而异。

  1. 安装完成后,打开 Oracle VirtualBox。您将看到Oracle VM VirtualBox 管理器主页,如下所示:

图 3.2 – Oracle VirtualBox 管理器主页

图 3.2 – Oracle VirtualBox 管理器主页

  1. 然后,点击新建按钮在你的机器上创建一个新的虚拟机(在此,这作为客户操作系统)。以下截图显示了创建虚拟机对话框弹出窗口(在点击新建按钮时出现):

图 3.3 – 使用 Oracle VirtualBox 配置虚拟机

图 3.3 – 使用 Oracle VirtualBox 配置虚拟机

在前面的截图中,你可以看到你需要为虚拟机提供一个唯一的名称。你还可以选择操作系统类型及其版本,以及配置内存(RAM)大小。最后,你可以选择配置或添加一个新的虚拟硬盘。如果你选择添加一个新的硬盘,那么将出现一个类似于以下弹出的窗口:

图 3.4 – 使用 Oracle VirtualBox 创建虚拟硬盘

图 3.4 – 使用 Oracle VirtualBox 创建虚拟硬盘

如前一个截图所示,在配置虚拟硬盘时,你可以从各种可用的虚拟硬盘驱动器中选择。主要的流行虚拟硬盘如下:

  • VirtualBox 磁盘镜像VDI

  • 虚拟硬盘VHD

  • 虚拟机磁盘VMDK

一旦你配置了所需的虚拟硬盘配置,你可以通过点击创建按钮来创建虚拟机。

  1. 一旦创建了一个虚拟机,它将列在Oracle VM VirtualBox 管理器屏幕上,如下面的截图所示。你可以通过选择适当的虚拟机并点击启动按钮来启动虚拟机:

图 3.5 – 在 Oracle VirtualBox 中创建并列出的虚拟机

图 3.5 – 在 Oracle VirtualBox 中创建并列出的虚拟机

尽管虚拟机简化了我们的交付,使平台比传统服务器更可用且更快,但它们有一些局限性:

  • 虚拟机是重量级组件:这意味着每次进行灾难恢复时,你都需要获取所有资源并启动客户操作系统,以便运行你的应用程序。重启需要几分钟时间。此外,启动一个新的客户操作系统资源密集。

  • 它们会降低操作系统的性能:由于虚拟机中资源有限,并且它们只能进行厚配置,这会降低宿主操作系统的性能,进而影响客户操作系统的性能。

  • 有限的便携性:由于在虚拟机上运行的应用程序与客户操作系统紧密耦合,当迁移到具有不同类型或配置的不同客户操作系统时,总会存在便携性问题。

容器化可以帮助我们克服这些缺点。我们将在下一节中查看容器化。

容器化

容器化是一种技术,它抽象了操作系统(而不是硬件)并允许应用程序直接在其上运行。与虚拟化相比,容器化更高效,因为应用程序不需要客户操作系统来运行。应用程序使用宿主操作系统的相同内核来运行针对不同类型操作系统的多个应用程序。以下图表显示了容器化是如何工作的:

图 3.6 – 容器化

图 3.6 – 容器化

在容器化中,一个名为容器引擎的软件在宿主操作系统上运行。这允许应用程序在容器引擎上运行,而无需创建单独的客户操作系统。每个应用程序的运行实例及其依赖项被称为容器。在这里,应用程序及其依赖项可以被打包成一个可移植的包,称为镜像。

容器化的好处

以下是与虚拟化相比容器化的优势:

  • 轻量级: 容器使用依赖和二进制文件直接在容器引擎上运行应用程序。容器不需要创建虚拟机,因此不需要初始化专用的虚拟内存/硬盘来运行应用程序。容器的启动速度比虚拟机快得多。虽然虚拟机需要几分钟才能启动,但容器可以在几秒钟内启动。

  • 便携性: 应用程序及其依赖和基础容器可以打包成一个名为镜像的包,可以轻松地跨任何在任意主机上运行的容器引擎进行移植。

  • 减少单点故障: 由于容器易于移植和轻量级特性,测试、部署和扩展应用程序变得更加容易。这导致了微服务的开发,从而确保了单点故障的减少。

  • 提高开发速度: 在容器化中,容器可以无缝地从一种环境迁移到另一种环境,从而实现无缝的持续部署。它还允许在构建和打包的同时进行即时测试,从而提高持续集成工作流程。如果应用程序在容器上运行,应用程序扩展变得超级快。这些特性使得开发更加容易和快速,使企业能够快速将解决方案推向市场。

Docker 是最受欢迎的容器引擎。让我们看看与 Docker 相关的一些最重要和常见的术语:

  • Docker 镜像: Docker 镜像是一个蓝图或模板,包含创建 Docker 容器的指令。我们可以通过将现有镜像与应用程序及其依赖项打包来创建 Docker 镜像。

  • Docker 容器: Docker 容器是 Docker 镜像的运行实例。Docker 容器包含一个或多个只读层之上的写层。可写层允许我们在容器上写入任何内容,以及执行命令。

  • Docker 仓库:这是一个存储由开发者开发和上传的 Docker 镜像的仓库,以便其他开发者可以利用它们。容器仓库是存储您的 Docker 镜像的物理位置。具有相同名称的相关镜像也可以存储,但每个镜像都将通过一个标签唯一标识。Maven 仓库对 Java 艺术品的作用类似于 Docker 仓库对 Docker 镜像的作用。就像 Maven 仓库支持具有相同名称的相关 JAR 文件的多版本一样,Docker 仓库支持具有相同名称的镜像的多标签。Docker Hub 是官方基于云的公共 Docker 仓库。

  • Docker 网络:Docker 网络负责 Docker 主机和 Docker 应用程序之间的通信。它还负责基本的容器间通信。外部应用程序和开发者可以通过暴露给外部世界的端口访问在 Docker 容器中运行的应用程序。

  • Docker 存储:Docker 有多个存储驱动程序,允许您与底层存储设备(如 Device Mapper、AUFS 和 Overlay)一起工作。数据卷可以在多个 Docker 容器之间共享。这使得共享资源可以存储在这样的数据卷中。然而,Docker 容器之间无法共享内存。

现在我们已经了解了与 Docker 相关的重要术语,让我们学习如何在本地机器上使用 Docker Desktop 设置 Docker。我们还将向您展示如何部署镜像并启动 Docker 容器:

  1. 首先,您必须在本地机器上安装 Docker Desktop。您可以根据操作系统类型和版本,从以下链接下载合适的 Docker Desktop 版本:www.docker.com/products/docker-desktop

根据您的操作系统,您可以在以下链接中找到安装说明:docs.docker.com/desktop/mac/install/(适用于 Mac)或 docs.docker.com/desktop/windows/install/(适用于 Windows)。

如果您的系统中没有安装 Maven,请下载并安装它(安装 Maven 的说明可以在 maven.apache.org/install.xhtml 找到)。

  1. 安装 Docker Desktop 后,打开它。您将需要接受一个协议。请阅读并接受它。完成此操作并打开应用程序后,您将看到以下主页:

图 3.7 – Docker Desktop 主页

图 3.7 – Docker Desktop 主页

  1. 接下来,您必须创建一个 Docker Hub 个人账户,以便有效地使用 Docker Desktop。请在 hub.docker.com/signup 上注册以创建个人 Docker 账户。

一旦您成功创建了账户,点击登录按钮,并输入您的 Docker ID 和密码进行登录,如下所示:

图 3.8 – 登录 Docker Desktop

图 3.8 – 登录 Docker Desktop

  1. 现在,让我们构建自己的 Dockerfile。要构建 Dockerfile,我们需要了解基本的 Docker build 命令。以下表格列出了几个重要的 Docker build 命令:

图 3.9 – Docker 构建命令

图 3.9 – Docker 构建命令

要构建 Dockerfile,首先,从 github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/blob/main/Chapter03/sourcecode/DockerExample 下载代码。在这个项目中,我们将使用 Spring Boot 创建一个简单的 REST API,并使用我们的本地 Docker 环境部署和运行此应用程序。当我们构建此项目时将生成的工件是 DockerExample-1.0-SNAPSHOT.jar。Dockerfile 将如下所示:

# Each step creates a read-only layer of the image.
# For Java 8
FROM openjdk:8-jdk-alpine
# cd /opt/app
WORKDIR /opt/app
# cp target/DockerExample-1.0-SNAPSHOT.jar /opt/app/app.jar
COPY target/DockerExample-1.0-SNAPSHOT.jar app.jar
# exposing the port on which application runs
EXPOSE 8080
# java -jar /opt/app/app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

在 Dockerfile 源代码的 步骤 1 中,我们从 Docker Hub 导入一个基础镜像。在 步骤 2 中,我们将 Docker 容器内的工作目录设置为 /opt/app。在下一步中,我们将我们的工件复制到 Docker 的工作目录。之后,我们从 Docker 暴露端口 8080。最后,我们使用 java -jar 命令执行 Java 应用程序。

  1. 接下来,我们将使用 Maven 构建 JAR 文件。首先,使用命令行(Windows)或终端(Mac),转到 DockerExample 项目的根目录。从项目根目录运行以下命令以构建 JAR 文件:

    > mvn clean install
    
  2. 然后,运行以下命令从我们刚刚创建的 Dockerfile 创建一个名为 hello-docker 的自定义 Docker 镜像:

    > docker build --tag=hello-docker:latest .
    

一旦您运行此命令,您将能够在 Docker Desktop 的镜像标签页中看到 Docker 镜像,如下所示:

图 3.10 – 成功创建 Docker 容器并列入列表

图 3.10 – 成功创建 Docker 容器并列入列表

  1. 现在,您可以通过点击运行按钮来启动容器,如下所示:

图 3.11 – 运行 Docker 容器

图 3.11 – 运行 Docker 容器

提供一个容器名称和主机端口值,然后在弹出对话框中点击运行以启动容器,如下所示:

图 3.12 – 设置 Docker 运行配置

图 3.12 – 设置 Docker 运行配置

一旦您点击运行,容器将被实例化,您将能够在 Docker Desktop 的容器/应用标签页中看到容器列表(其状态设置为运行中),如下所示:

图 3.13 – 在 Docker Desktop 上运行实例

图 3.13 – 在 Docker Desktop 上运行实例

  1. 现在,您可以通过在浏览器中测试应用程序来验证应用程序。请确保在验证应用程序时使用主机端口(在容器创建期间配置),在 HTTP 地址中使用。对于我们的示例,您可以使用端口 8887(您之前已映射)来验证应用程序,如下所示:

图 3.14 – 测试在 Docker 上部署的应用

图 3.14 – 测试在 Docker 上部署的应用

您可以使用 Docker Desktop 中的 CLI 按钮登录 Docker CLI,如下所示:

图 3.15 – 从 Docker Desktop 打开 Docker CLI

图 3.15 – 从 Docker Desktop 打开 Docker CLI

在本节中,我们学习了 Docker。虽然 Docker 让我们的生活变得更简单,并且使开发和部署变得相当快,但它也带来了一系列挑战:

  • 容器间通信通常不可能或非常复杂

  • 没有入站流量分发机制,这可能会导致入站流量在一系列容器中的分布不均

  • 手动管理集群的容器管理是开销

  • 自动扩展不可行

在生产环境中,如果我们想要运行一个健壮、高效、可扩展且成本效益高的解决方案,我们需要解决这些不足。在这里,容器调度器就派上用场了。市场上有很多容器调度器。然而,由 Google 开发并开源的 Kubernetes 是最受欢迎和最广泛使用的容器调度器之一。在下一节中,我们将更详细地讨论 Kubernetes。

Kubernetes

Kubernetes 是一个开源容器调度器,它有效地管理容器化应用程序及其容器间通信。它还自动化了容器的部署和扩展。每个 Kubernetes 集群都有多个组件:

  • 主节点

  • 节点

  • Kubernetes 对象(命名空间、pod、容器、卷、部署和服务)

下面的图显示了 Kubernetes 集群的各个组件:

图 3.16 – Kubernetes 集群及其组件

图 3.16 – Kubernetes 集群及其组件

现在,让我们简要描述一下前面图中显示的每个组件:

  • configMap。除此之外,还有一个默认命名空间。

  • 用户命名空间:用户/团队可以在默认命名空间内创建命名空间。

  • 主节点:主节点是集群的调度器。每当 Kubernetes 集群收到新的部署请求时,它会扩展和分配应用容器。

  • 工作节点:这些是部署 pod 和运行应用程序的节点。

  • Pod:Pod 是在容器之上的一个抽象,它帮助容器在不同运行时之间轻松便携,自动检测集群中可用的端口,并为其分配一个唯一的 IP 地址。Pod 可以包含多个容器,其中多个辅助容器可以无缝通信并协助主要应用程序。这些单个 Pod 中的多个容器不仅共享卷,还共享内存空间,例如 可移植操作系统接口POSIX)共享内存。

  • 代理:有两种类型的代理,如下所示:

    • Kubelet 代理:这是一个在每个节点上运行的服务。它确保该节点内的所有容器都处于运行状态。

    • Docker 代理:这是一个用于运行容器的服务。

现在我们已经简要地了解了 Kubernetes 的组件及其在容器化中的作用,让我们尝试在本节中创建的 Docker 镜像在本地 Kubernetes 集群中部署。为此,我们必须安装 minikube(一个在本地机器上运行 Kubernetes 的 Kubernetes 集群):

  1. 您可以通过遵循 minikube.sigs.k8s.io/docs/start/ 中的说明来安装合适的 minikube 版本。

  2. 一旦安装了 minikube,您可以使用以下命令启动 minikube

    > minikube start
    

成功的 minikube start 看起来如下:

图 3.17 – 启动 minikube

图 3.17 – 启动 minikube

  1. 现在,就像我们必须创建 Dockerfile 来创建 Docker 镜像一样,我们必须创建一个 YAML 文件来向 Kubernetes 集群提供部署指令。在我们的项目中,我们将此 YAML 文件命名为 deployment.yaml。以下是在 deployment.yaml 文件中的代码:

    apiVersion: v1
    kind: Service
    metadata:
      name: hello-docker-service
    spec:
      selector:
        app: hello-docker-app
      ports:
        - protocol: "TCP"
          port: 8080
          targetPort: 8080
          nodePort: 30036
      type: LoadBalancer
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: hello-docker-app
    spec:
      selector:
        matchLabels:
          app: hello-docker-app
      replicas: 5
      template:
        metadata:
          labels:
            app: hello-docker-app
    spec:
          containers:
            - name: hello-docker-app
              image: hello-docker-app
              imagePullPolicy: Never
              ports:
                - containerPort: 8080
    

deployment.yaml 文件包含两种类型的配置:一种用于 Service,另一种用于 Deployment。每个 Kubernetes 组件配置主要由以下三个部分组成:

  • metadata 包含名称和任何其他元信息。

  • spec 包含规范。这直接取决于正在配置的组件类型。

  • status 不是我们必须配置的内容。Kubernetes 集群会添加这部分并在部署完成后持续更新它。

  1. 首先,您必须构建 hello-docker-app 镜像并将其暴露给 minikube Docker 环境。您可以通过执行以下命令来完成此操作:

    > eval $(minikube docker-env)
    > docker build --tag hello-docker-app:latest .
    
  2. 现在,您可以使用以下命令从项目根目录部署此应用程序:

    > kubectl apply -f deployment.yaml
    

执行此命令后,您应该能够看到 hello-docker-servicehello-docker-app 已成功创建,如下面的截图所示:

图 3.18 – 在 Kubernetes 集群中创建的应用程序

图 3.18 – Kubernetes 集群中创建的应用程序

  1. 您还可以通过执行以下命令在 minikube 仪表板中检查部署及其状态:

    > minikube dashboard
    

执行后,您应该在默认浏览器中看到仪表板出现。在这里,您将能够看到您的部署状态以及其他监控信息:

图 3.19 – minikube 仪表板

图 3.19 – minikube 仪表板

  1. 现在,您可以使用以下命令访问已部署的应用程序并启动其服务:

    > minikube start service: hello-docker-service
    

一旦启动了服务,您可以通过执行以下命令来检查由与您的应用程序关联的 Docker 服务暴露的基本 URL:

> minikube service --url hello-docker-service

此命令将显示类似于以下内容的输出:

图 3.20 – 检查基本 URL

图 3.20 – 检查基本 URL

  1. 您可以通过使用此示例的http://127.0.0.1:63883/hello URL 来通过浏览器验证运行中的应用程序,如下所示:

图 3.21 – 使用 Kubernetes 部署的应用程序测试

图 3.21 – 使用 Kubernetes 部署的应用程序测试

在本节中,我们讨论了虚拟化和容器化如何帮助您以更有效、更快、成本优化的方式管理、部署和开发应用程序。通用 Web 应用程序、后端应用程序和其他处理应用程序在可扩展的虚拟平台(如容器和虚拟机)上运行得非常好。然而,大数据,其数据量达到千兆或太字节,需要一个具有不同架构的平台才能良好运行。从下一节开始,我们将讨论适合大数据处理的平台。

Hadoop 平台

随着搜索引擎、社交网络和在线市场的出现,数据量呈指数级增长。搜索和处理如此大的数据量需要不同的方法来满足服务级别协议SLAs)和客户期望。谷歌和 Nutch 都使用了一种新的技术范式来解决这个问题,因此自动以分布式方式存储和处理数据。由于这种方法,Hadoop 于 2008 年出生,并已被证明是存储和处理大量数据(以千兆或更多)的有效和快速的生命线。

Apache Hadoop 是一个开源框架,它使计算机集群中的大数据集的分布式存储和处理成为可能。它设计得可以从单台服务器轻松扩展到数千台机器。它通过强大的节点故障转移和恢复功能提供高可用性,这使得 Hadoop 集群能够在廉价的通用硬件上运行。

Hadoop 架构

在本节中,我们将讨论 Hadoop 集群的架构和各种组件。以下图表提供了 Hadoop 生态系统的顶层概述:

图 3.22 – Hadoop 生态系统概述

图 3.22 – Hadoop 生态系统概述

如前图所示,Hadoop 生态系统由三个独立的层组成,如下所述:

  • 存储层:Hadoop 中的存储层被称为Hadoop 分布式文件系统HDFS)。HDFS 支持大型数据集的分布式和复制存储,这为数据提供了高可用性和高性能访问。以下图表提供了 HDFS 架构的概述:

图 3.23 – HDFS 架构

图 3.23 – HDFS 架构

HDFS 采用主从架构,其中NameNode是主节点,所有的DataNode都是从节点。NameNode负责存储 HDFS 中所有文件和目录的元数据。它还负责存储哪些块存储在哪个DataNode的映射。存在一个二级NameNode,负责NameNode的维护工作,如压缩等。在 HDFS 系统中,DataNode 是真正的动力所在。它们负责存储块级数据,并对其执行所有必要的块级操作。DataNode会定期向NameNode发送心跳信号,以表明它们正在运行。它还会在每个第十次心跳时向NameNode发送一个块报告。

当客户端发起读取请求时,它会从NameNode获取关于文件和块元数据信息。然后,它使用这些元数据从正确的DataNode(s)获取所需的块。当客户端发起写入调用时,数据会被写入到分布在不同 DataNode 上的分布式块中。这些块随后会在不同的节点(位于不同的机架上)进行复制,以确保在当前机架出现故障时仍能保持高可用性。

  • 资源管理器层:资源管理器是一个管理集群资源的框架,同时也负责调度 Hadoop 作业。以下图表展示了资源管理器的工作方式:

图 3.24 – 资源管理器的工作原理

图 3.24 – 资源管理器的工作原理

如我们所见,当客户端在 Hadoop 中提交处理作业时,他们会向资源管理器发送请求。资源管理器由一个调度器和一个应用程序管理器组成。在这里,应用程序管理器通过与应用程序的主容器进行通信,与不同数据节点中的节点管理器协商。每个应用程序主节点负责执行单个应用程序。然后,资源管理器中的调度器通过根据应用程序主节点的资源请求与节点管理器交互,协商其他容器资源。Hadoop 中最受欢迎的两个资源管理器是Apache YARNApache Mesos

  • 处理层:处理层负责在 Hadoop 生态系统中并行处理分布式数据集。Hadoop 中最受欢迎的两个处理引擎是MapReduceApache Spark。MapReduce 程序与 Hadoop 环境紧密耦合。它主要使用两个强制阶段——映射阶段和减少阶段——以及几个可选的数据处理阶段来处理数据。它在这些阶段之间将中间数据写回 HDFS。另一方面,Spark 读取逻辑上分布的分布式数据集,称为弹性分布式数据集RDD),并创建一个由阶段和任务组成的有向无环图DAG)来处理数据。由于它通常不需要将中间数据写入磁盘(除非需要显式地进行洗牌),它通常比 MapReduce 快 10 倍。

虽然这三个层次相互依赖,但设计是这样的,即层次之间是解耦的。这种解耦层架构使 Hadoop 更加灵活、强大和可扩展。这就是为什么尽管数据集的大小以惊人的速度增长,并且处理数据的预期 SLA 随着时间的推移而降低,Hadoop 处理仍然得到了改进和演变。

尽管 Hadoop 是一个开源框架,但所有生产 Hadoop 集群都运行在以下分发之一中:

  • Hortonworks 数据平台HDP(已停售)

  • Cloudera Hadoop 分发CDH(已停售)

  • Cloudera 数据平台(HDP 和 CDH 在 Hortonworks 和 Cloudera 合并后均可迁移到该平台)

  • MapR 分发

除了这些针对本地 Hadoop 部署的分发之外,还有一些流行的 Hadoop 云分发可供选择:

  • Cloudera 的CDP 公共云

  • 弹性映射减少EMR)来自亚马逊网络服务AWS

  • Microsoft AzureHDInsight

  • Google Cloud PlatformGCP)的Cloud Dataproc

在本节中,我们简要讨论了 Hadoop 分发及其工作方式。我们还介绍了来自不同供应商的各种 Hadoop 分发,这些分发可以在生产环境中运行 Hadoop。

随着数据的不断增长,需要扩展本地基础设施。这种基础设施容量需要规划以支持最大负载。如果发生意外负载,这可能会导致资源利用率不足或资源过度利用。解决这个问题的答案是云计算。在下一节中,我们将讨论各种云平台以及它们为数据工程解决方案带来的好处。

云平台

云计算涉及通过互联网提供计算服务,如存储、计算、网络和智能。它提供按使用付费的模式,这意味着你只为使用的服务付费。这有助于降低你的运营成本以及资本支出CapEx)成本。云使资源利用最优化,实现即时可扩展性、敏捷性和易于维护,从而促进更快的技术创新和规模经济。例如,Canva 是一个任何人都可以通过其简单的用户界面访问的设计工具。到 2019 年,它有 5500 万用户。在撰写本文时,它在全球有 8500 万用户,每秒创建 100 多个设计。为了无缝地适应这种指数级的客户和数据量增长,同时保持相似或更好的性能,Canva 使用了 AWS 平台。

以下云计算分布是云计算市场的领导者,通常被称为云计算的“三大巨头”:

  • AWS,由亚马逊提供

  • 微软 Azure,由微软提供

  • GCP,由谷歌提供

除了“三大巨头”之外,还有其他较小或不太知名的云分布,如 Red Hat OpenShift、HPE GreenLake 和 IBM Cloud。

云计算的好处

以下列出的是云计算的好处:

  • 经济高效: 云计算通过消除基础设施设置中的巨大成本来降低资本支出(CapEx)成本。它还通过应用按使用付费的模式来降低成本。

  • 可扩展性: 由于云服务都是虚拟化的,它们可以在几分钟或几秒钟内启动,从而实现极快的可扩展性。

  • 弹性: 云服务可以根据资源和计算需求轻松地进行扩展或缩减。

  • 可靠: 由于每个服务都在可用区域以及地区内进行了复制,因此服务非常可靠,并保证最小化停机时间。

  • 全球性: 由于云服务在互联网上,计算能力可以从全球的任何地理位置提供。

  • 提高生产力: 由于资源和服务配置、管理和部署不再是开发者的头疼问题,他们可以专注于业务功能,并更快、更有效地交付解决方案。

  • 安全性: 除了其他好处之外,云拥有许多安全层和服务,使云成为一个安全的基础设施。

云计算有三种类型,如下所示:

  • 公有云: 公有云由第三方供应商拥有和运营,通过互联网提供计算资源和服务。在这里,作为公有云的用户,你必须为所使用的服务付费。

  • 私有云:私有云是一种云计算形式,其中计算资源由客户拥有,通常是在私有本地数据中心。云服务提供商仅提供云软件及其支持。通常,这种云被大型企业使用,因为安全和合规性是向公共云迁移的约束。在这里,客户负责管理和监控云资源。

  • 混合云:混合云结合了公共云和私有云,通过技术将数据和应用连接在一起,可以在私有云和公共云之间无缝通信和迁移。这为安全、合规性和敏捷性提供了更高的灵活性。

既然我们已经讨论了不同类型的云计算,让我们尝试了解在公共云分布中可用的各种云服务类型。以下是一些云服务的类型:

  • 基础设施即服务IaaS

  • 平台即服务PaaS

  • 软件即服务SaaS

在云中,应用开发中拥有各种堆栈的责任在云服务提供商和客户之间共享。以下图表显示了这类云计算服务的共享责任模型:

图 3.25 – 共享责任模型

图 3.25 – 共享责任模型

如我们所见,如果客户正在运行私有云,所有资源、服务、应用和数据都是客户的责任。然而,如果您选择公共云,则可以选择 IaaS、PaaS 和 SaaS。云服务提供商承诺在 IaaS 模型中管理和拥有计算、存储和网络等基础设施服务。如果您选择 PaaS 模型,除了 IaaS 中提供的内容外,云提供商还会管理操作系统、虚拟机和运行时,以便您可以拥有、开发和管理工作负载、数据和访问。在 SaaS 中,除了数据和访问之外的所有内容都由您的云服务提供商管理。即使与另外两种模型相比,单个单元的成本可能更高,但根据您的业务,它可能更便宜且更省事。

通过讨论,我们已经讨论了数据工程应用可能部署的各种平台。现在,让我们讨论架构师需要了解的各种设计选择,以便为他们选择正确的平台。

选择正确的平台

在本节中,我们将探讨架构师必须做出的最重要的决策之一——如何选择最适合用例的平台。在这里,我们将了解在考虑各种云数据平台时,何时选择虚拟化与容器化,以及本地与云之间的选择。

何时选择虚拟化与容器化

虽然这两种技术都确保我们可以通过配置虚拟资源来最大限度地利用资源,但每种技术基于应用程序的类型都有其优势。

微服务是面向服务架构的一种变体,其中应用程序被视为松散耦合的服务集合。每个服务都是细粒度和轻量级的。微服务最适合基于容器的平台。例如,可以使用容器轻松部署 REST 服务。由于微服务由松散耦合的服务组成,它们应该易于部署和扩展。由于每个服务都可以由其他服务和堆栈独立消费和重用,因此它们需要是可移植的,以便可以快速迁移到任何容器化平台。

另一方面,单体应用程序旨在执行多个相关任务,但它被构建为一个紧密耦合的单个应用程序。这类应用程序更适合小型团队或概念验证POC)目的。这种单体架构被用于遗留应用程序的另一个用例。这类单体应用程序最适合虚拟化。在依赖操作系统或直接与特定操作系统通信的应用程序中,虚拟化平台比容器化更受欢迎。

然而,在云中,所有配置的服务器都是虚拟机。例如,亚马逊弹性容器服务ECS)和亚马逊弹性 Kubernetes 服务EKS)都是在虚拟服务器,如亚马逊弹性计算云EC2)之上运行的。因此,在现代架构中,特别是在云中,问题不是在容器化和虚拟化之间选择——而是在容器化加虚拟化与虚拟化之间选择。

何时使用大数据

如果我们处理的数据大小为太字节或拍字节,大数据是一个不错的选择。随着人工智能AI)和机器学习ML)应用的日益流行,我们需要处理大量数据——数据量越大,AI 模型越准确。这些数据量达到太字节级别。通过大数据应用以可扩展的方式处理这些数据是可行的。在某些情况下,由于处理复杂性,处理数百吉字节的数据需要不必要的时间。在这种情况下,大数据可能是一个好的解决方案。大多数大数据用例是用于分析、在线分析处理OLAP)、AI 和 ML。

在本地部署与基于云的解决方案之间选择

这是架构师今天面临的一个明显问题。在本节中,我们将尝试了解影响这一决策的因素,并推荐一些一般标准以帮助您决定选择哪一个。以下因素将帮助您决定本地部署与云解决方案:

  • 成本:企业负责所有基础设施和维护成本,包括本地环境的人力资源。成本还包括从本地到云的迁移成本。另一个重要的成本指标是关于资本支出(CapEx)与运营支出(OpEx)的比较,以及 OpEx 相对于 CapEx 对企业的成本效益。所有这些因素决定了总拥有成本,这最终决定了什么最适合您的业务。

  • 控制:企业完全控制数据、其存储以及所有与本地基础设施相关的硬件。然而,尽管企业拥有数据,但存储及其硬件由云服务提供商管理。

  • 资源需求模式:如果资源需求是弹性的,基础设施需求是季节性的,那么云可能是正确的选择。另一方面,如果资源需求是静态的,那么选择本地化可能是正确的选项。

  • 敏捷性和可扩展性:如果您的公司是一家初创公司且呈指数级增长,这意味着您的需求根据您收到的反馈和波动性客户群上下波动,那么云服务将是您的更好选择。

  • 安全性:对于一些行业,如金融和医疗保健,安全性是一个大问题。尽管云计算的安全性已经取得了许多进步,并且它们有一个强大的稳健安全模型,但由于数据存储在由公共云服务提供商管理的硬件中,许多拥有非常敏感数据的此类企业出于安全原因选择本地化而非云服务。

  • 合规性:一些行业有非常严格的监管控制和政策,如联邦机构和医疗保健。在这些企业中,对数据和其存储的完全控制更有意义。因此,本地化选项更为合适。

根据这些因素,以下是一些广泛指南,您可以使用这些指南来做出这个决定。然而,请注意,这些只是建议——实际的选择将取决于您的具体业务需求和背景。

在以下情况下,您应该选择本地化架构:

  • 安全性是一个主要关注点,您不希望冒任何数据风险发生的风险

  • 监管政策和控制非常严格,规定数据及其存储的控制应保持在组织内部

  • 旧系统难以移动或复制

  • 迁移数据和处理从本地到云所需的时间、努力和成本是不合理的

在以下情况下,您应该选择云架构:

  • 需要灵活性和敏捷性以进行扩展和增长

  • 您是一家初创公司,您有有限的客户群和有限的资本支出,但您有很高的增长潜力

  • 您希望环境具有动态配置,可以轻松按需修改

  • 您不希望对基础设施进行资本支出投资,而更倾向于按需付费模式

  • 你对业务需求不确定,需要频繁地调整资源的大小

  • 你不希望花费资源和时间来维护你的基础设施及其相关成本

  • 你需要一个敏捷的设置,更快的交付,以及更快的运营周转时间

最后,让我们比较大三大云供应商,以决定哪个供应商最适合你的业务。

在各种云供应商之间进行选择

在本节中,我们将比较大三大公有云供应商,并探讨它们在各个类别中的表现,尽管对于“哪个云供应商最适合我的业务?”这个问题没有明确的答案。以下表格提供了大三大云供应商的比较,并揭示了它们的优点和缺点:

AWS Azure GCP
服务 服务种类繁多 可用的服务范围良好。在 AI/ML 方面有卓越的服务。 可用的服务有限。
成熟度 最成熟 追赶 AWS。 相对来说不如其他两个成熟。
市场 place 所有供应商都提供他们的产品 良好的供应商支持,但不如 AWS。
可靠性 极佳 极佳。 极佳。
安全性 极佳 极佳。 比 AWS 和 Azure 少一些等级。
成本 各不相同 最具成本效益。 各不相同。
AWS Azure GCP
支持 支付开发者/企业支持 支付开发者/企业支持。比 AWS 有更多支持选项。 支付开发者/高级支持。比其他两个更昂贵。
混合云支持 有限 极佳。 良好。
特别说明 与 Azure 和 GCP 相比,计算能力更强 现有 Microsoft 服务的集成和迁移简单。 对容器化工作负载有出色的支持。全球光纤网络。

图 3.26 – 大三大云供应商的比较

简而言之,AWS 是市场领导者,但 Azure 和 GCP 都在迎头赶上。如果你在寻找全球可用的服务数量最多,AWS 将是你的明显选择,但它有一个更高的学习曲线。

如果你的用例仅围绕 AI/ML,并且你有 Microsoft 本地基础设施,Azure 可能是正确的选择。他们有出色的企业支持和混合云支持。如果你需要一个强大的混合云基础设施,Microsoft Azure 是你的首选选项。

GCP 进入比赛较晚,但他们有出色的开源和第三方服务的集成和支持。

但最终,这取决于你的具体用例。随着市场的增长,大多数企业都在寻找多云策略,以利用每个供应商的最佳功能。

现在,让我们总结本章内容。

摘要

在本章中,我们讨论了各种虚拟化平台。首先,我们简要介绍了虚拟化、容器化和容器编排框架的架构。然后,我们部署了虚拟机(VMs)、Docker 容器和 Kubernetes 容器,并在其上运行应用程序。在这个过程中,我们学习了如何配置 Dockerfile 和 Kubernetes 部署脚本。之后,我们讨论了 Hadoop 架构和市场上可用的各种 Hadoop 发行版。接着,我们简要介绍了云计算及其基本概念。最后,我们讨论了每位数据架构师必须做出的决策:容器还是虚拟机? 我需要大数据处理吗? 云还是本地? 如果是云,哪个云?

因此,我们对数据架构的一些基本概念和细微差别有了良好的理解,包括基本概念、数据库、数据存储以及这些解决方案在生产环境中运行的各种平台。在下一章中,我们将更深入地探讨如何架构各种数据处理和数据摄取管道。

第二部分 – 构建数据处理管道

本节重点指导您学习如何使用 Java 堆栈中的各种技术来架构和开发批处理和流处理解决方案。最后,它还将帮助您实际理解和应用数据治理和安全。

本节包括以下章节:

  • 第四章**,ETL 数据加载 – 数据仓库中数据摄取的基于批处理解决方案

  • 第五章**,架构批处理管道

  • 第六章**,架构实时处理管道

  • 第七章**,核心架构设计模式

  • 第八章**,启用数据安全和治理

第四章:ETL 数据加载 - 在数据仓库中摄入数据的基于批次的解决方案

在前面的章节中,我们讨论了围绕数据工程的各种基础概念,从不同类型的数据工程问题开始。然后,我们讨论了各种数据类型、数据格式、数据存储和数据库。我们还讨论了可用于在生产环境中部署和运行数据工程解决方案的各种平台。

在本章中,我们将学习如何架构和设计一个从数据源到数据仓库的低到中等数据摄入的基于批次的解决方案。在这里,我们将讨论一个实时用例,讨论、建模和设计一个适用于此类场景的数据仓库。我们还将学习如何使用基于 Java 的技术栈开发此解决方案,并运行和测试我们的解决方案。到本章结束时,您应该能够使用 Java 及其相关堆栈设计并开发一个提取、转换、加载ETL)基于批次的管道。

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

  • 理解问题和源数据

  • 构建有效的数据模型

  • 设计解决方案

  • 实施和单元测试解决方案

技术要求

您可以在本书的 GitHub 仓库中找到本章的所有代码文件:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter04/SpringBatchApp/EtlDatawarehouse.

理解问题和源数据

数据工程通常涉及收集、存储和分析数据。但几乎所有数据工程领域都是从将原始数据摄入数据湖或数据仓库开始的。在本章中,我们将讨论这样一个典型用例,并构建一个针对以下章节讨论的问题的端到端解决方案。

问题陈述

公司 XYZ 是一家提供数据中心建设和维护服务的第三方供应商。现在,公司 XYZ 计划为其客户开发一个数据中心监控工具。客户希望看到各种有用的指标,例如任何设备在每小时、每月或每季度报告的事件数量。他们还希望看到关于关闭比率和平均关闭时间的报告。他们还希望根据设备类型或事件类型搜索事件。他们还希望找到基于时间的故障模式,以预测任何一组资源的季节性或每小时使用激增。这些报告需要每 12 小时生成一次。为了生成此类报告,需要构建一个数据仓库,并且必须每天将数据摄入并存储在该数据仓库中,以便可以轻松生成此类报告。

为了为这个数据工程问题创建解决方案,我们必须分析数据在这个用例中的四个维度(参考第一章现代数据架构基础中的数据维度部分)。我们的第一个问题将是,数据的速度是多少? 这个问题的答案帮助我们确定它是一个实时还是批量处理问题。尽管根据问题描述,关于数据输入频率的信息不多,但明确指出报告需要在每 12 小时或每天两次后生成。无论数据到达的速度如何,如果下游系统需要数据的时间频率超过一小时,我们可以安全地决定我们正在处理一个基于批量的数据工程问题(请参考第一章现代数据架构基础中的数据工程问题类型部分)。

我们的第二个问题将是,数据量是多少?它是否巨大?未来有可能增长到数百个太字节吗? 这些问题通常帮助我们选择我们应该使用的科技。如果数据量巨大(以太字节或数百个太字节计),那么我们才应该选择大数据技术来解决我们的问题。很多时候,架构师倾向于在非大数据用例中使用大数据,这使得解决方案在成本、维护和时间上都不可持续且昂贵。在我们的案例中,需要摄取的数据是事件日志数据。这类数据通常并不巨大。然而,架构师应该确认将要发送进行摄取的数据。在这种情况下,让我们假设客户已经回应并表示数据将以每两小时为一个平面文件的形式发送,包含已新记录或更新的事件的 Delta。这意味着我们的数据集将要么是小型文件,要么是中等大小的文件。这意味着作为一个架构师,我们应该选择一个非大数据基础的解决方案。

理解源数据

架构师必须提出的第三个重要问题是,数据的多样性是什么?它是结构化、非结构化还是半结构化? 这个问题通常有助于确定如何处理和存储此类数据。如果数据是非结构化的,那么我们需要将其存储在 NoSQL 数据库中,但结构化数据可以存储在 RDBMS 数据库中。还有一个与数据多样性相关的问题——即,数据的格式是什么?是 CSV 格式、JSON 格式、Avro 格式还是 Parquet 格式?接收到的数据是否被压缩? 通常,这些问题有助于确定处理和摄取数据所需的技巧、技术、处理规则和管道设计。在我们的案例中,由于初始要求中没有提到,我们需要向客户提出这些问题。假设我们的客户同意以 CSV 文件的形式发送数据。因此,在这种情况下,我们正在处理结构化数据,数据以 CSV 文件的形式到来,没有任何压缩。由于它是结构化数据,我们非常适合使用关系数据模型或 RDBMS 数据库来存储我们的数据。

这引出了关于数据维度的最后一个问题:数据的真实性如何? 或者,用更简单的话说,我们接收到的数据质量如何?数据中是否有太多的噪声? 在所有未能解决客户问题的数据工程解决方案中,大多数失败是因为在分析源数据时投入的时间不足。理解即将到来的数据的性质非常重要。我们必须在分析结束时提出并能够回答以下类型的问题:

  • 源数据是否包含需要删除的垃圾字符?

  • 它是否包含任何特殊字符?

  • 源数据是否包含非英语字符(如法语或德语)?

  • 是否有任何数值列包含空值?哪些列可以是或不可以是可空列?

  • 是否有什么独特之处可以用来确定每条记录?

列表还在继续。

为了分析源数据,我们应该运行数据概要分析工具,例如 Talend Open Studio、DataCleaner 或 AWS Glue DataBrew,以分析和可视化数据的各种指标。这项活动有助于我们更好地理解数据。

在这里,我们将使用 DataCleaner 工具分析我们需要用于用例的 CSV 数据文件。请按照以下步骤操作:

  1. 首先,您可以通过访问datacleaner.github.io/downloads下载 DataCleaner 社区版。

  2. 然后,在所需的安装文件夹中解压缩下载的 ZIP 文件。根据您的操作系统,您可以使用根安装文件夹下的datacleaner.sh命令或datacleaner.cmd文件启动 DataCleaner。您将看到一个主屏幕,如下面的截图所示。在这里,您可以点击构建新作业按钮开始一个新的数据概要分析作业:

图 4.1 – DataCleaner 欢迎屏幕

图 4.1 – DataCleaner 欢迎屏幕

  1. 然后,将弹出一个对话框,您可以在其中选择数据存储库,如图下所示。在这里,我们将浏览并选择名为inputData.csv的输入文件:

图 4.2 – DataCleaner – 选择数据存储库弹出窗口

图 4.2 – DataCleaner – 选择数据存储库弹出窗口

一旦选择了数据存储库,我们将在左侧面板的顶部看到我们的数据源。我们应该能够看到我们 CSV 文件的列名。

  1. 现在,我们将把我们的数据源inputData.csv文件拖放到右侧面板,即管道构建画布。为了分析数据,DataCleaner 在左侧面板的分析菜单下提供了各种分析工具,如图所示。对于我们的用例,我们将使用字符串分析器

图 4.3 – 创建分析管道

图 4.3 – 创建分析管道

字符串分析器分析各种与字符串相关的指标,如 NULL 计数、空白计数、空白字符、字符大小写等。以下截图显示了字符串分析器的各种配置选项:

图 4.4 – 添加字符串分析器

图 4.4 – 添加字符串分析器

  1. 我们将添加另一个名为incidentNumberdeviceSerialNumeventCodeloggedTime的分析器,使其成为我们数据仓库的合格条目。

如果缺少任何此类信息,此类记录将不会为我们要解决的问题增加价值。在这里,完整性分析器将帮助我们确定我们是否需要特殊检查来处理这些约束,并在这些字段为空时删除记录。以下截图显示了完整性分析器的各种配置选项:

图 4.5 – 添加完整性分析器

图 4.5 – 添加完整性分析器

我们用例的最终概要分析管道如图下所示:

图 4.6 – 最终分析管道

图 4.6 – 最终分析管道

  1. 一旦我们执行此管道,分析结果将生成,如图下所示:

图 4.7 – 分析结果

图 4.7 – 分析结果

这种数据概要分析可以为我们提供有关数据的各种信息,这有助于我们调整我们的工具、技术和转换,以创建一个有效且成功的数据工程解决方案。如图所示,我们可以推断出总数据量是 300 行。其中,53 行是开放事件。解决评论中可以有空白,所有deviceSerialNum值都是小写,而status值都是大写。此类信息有助于我们在设计解决方案时做出有效的决策。

为了讨论的简洁性,我们只展示了一种源数据文件的数据概要分析形式。然而,我们也可以对其他类型的数据集进行同样的分析。在这个用例中,你可以对device_dm.csvevent_dm.csv文件中的数据进行类似的数据概要分析。

现在我们已经了解了需求,并对源数据有了相当的了解,在下一节中,我们将讨论如何设计模型,以便它可以存储导入的数据。

构建有效的数据模型

从我们之前的讨论和数据分析中,我们得出结论,我们的数据是有结构的,因此适合存储在关系型数据模型中。根据需求,我们收集到我们的最终数据存储应该是一个数据仓库。考虑到这两个基本因素,让我们了解关系型数据仓库方案。

关系型数据仓库方案

让我们探索在创建我们的数据模型时可以考虑的流行关系型数据仓库方案:

  • 星型模式:这是最流行的数据仓库方案类型。如图所示,中间有一个事实表,每个记录代表一个随时间发生的事实或事件:

图 4.8 – 星型模式

图 4.8 – 星型模式

这个事实表包含各种维度,其详细信息需要从相关的查找表中查找,这些查找表被称为维度表。这个事实表通过外键与每个维度表相关联。前面的图示显示了星型模式的外观。由于中间有一个事实表被多个侧面的维度表包围,其结构看起来像一颗星,因此得名。

  • 雪花模式:这是星型模式的一个扩展。就像星型模式一样,在这里,中间有一个事实表,周围有多个维度表。然而,在雪花模式中,每个维度表进一步引用其他子维度表,使得结构看起来像雪花:

图 4.9 – 雪花模式

图 4.9 – 雪花模式

在这里,我们可以看到每个维度表是如何通过外键关系与子维度表连接的,这使得结构看起来像雪花,因此得名。

  • 银河模式:银河模式是一种包含多个事实表的方案。在这里,一个或多个维度表被多个事实表共享。这种方案可以看作是两个或更多星型模式的集合,因此得名。

方案设计的评估

对于我们的用例,我们需要评估哪种方案设计最适合我们的用例。

我们应该问的第一个问题是,在我们的用例中,我们需要多个事实表吗? 由于我们的事实表只包含设备事件或事故,我们只能有一个事实表。这消除了我们拥有银河模式作为候选数据模型的可能性。现在,我们必须确定星型模式或雪花模式是否适合我们的用例。

要在这两种选择之间做出选择,让我们看看我们的 inputData.csv 文件中的以下列:

  • incidentNumber

  • deviceSerialNo

  • eventCode

  • loggedTime

  • closureTime

  • status

  • assignedTo

  • resolutionComments

通过查看此文件的列名,我们可以断定这是一个设备事件日志文件。这意味着来自 inputData.csv 文件的数据需要被摄入到我们的中心事实表中。但是首先,我们需要确定我们是否只需要引用完整的维度表,或者我们的维度表是否需要在另一组维度表中进行进一步的查找。

让我们先从确定 inputData.csv 中存在的数据集的候选维度开始。重要的是要记住,候选维度是由构建数据仓库的目的或目标决定的。我们正在构建的数据仓库的目的是获取 eventType、设备在不同时间间隔(如小时、月和季度)上的指标以及关闭持续时间指标。

在我们的情况下,deviceSerialNoeventCode 可以对应于两个称为 incidentNumber 的维度,这个维度在每条事实记录中都会变化,因此它不是一个维度的候选者。statusloggedTimeclosureTime 会从一条记录变化到另一条记录,因此它们最适合作为事实而不是维度。由于我们不对 assignedToresolutionComment 字段进行任何分析,我们可以在我们的数据模型中忽略这些列。在现实世界的场景中,通常,传入的源数据文件包含数百列。然而,其中只有一小部分列对于解决问题是有用的。

总是建议只摄入你需要的列。这可以节省空间、复杂性和金钱(记住,如今许多解决方案都是部署在云平台上,或者未来有迁移到云平台的可能,云平台遵循按使用付费的原则,因此你应该只摄入你打算使用的数据)。除此之外,我们的需求要求我们按小时、月和季度对每个事件进行标记,以便可以轻松地对这些间隔进行聚合,并分析小时、月和季度的模式。这种间隔标记可以在保存记录时从 loggedTime 中提取,然而,hourmonthquarter 可以作为与我们的中心事实表关联的派生维度存储。

因此,从我们的分析来看,很明显我们的事实表只引用那些自身完整的事实表。所以,我们可以得出结论,我们将使用以下一系列表来构建我们的星型模式数据模型:

  • DEVICE_EVENT_LOG_FACT:这是一个集中式的事实表,它由每个事件条目组成

  • DEVICE_DIMENSION:这是一个维度表,它包含设备查找数据

  • EVENT_DIMENSION:这是一个维度表,它包含事件查找数据

  • HOUR_DIMENSION:这是一个维度表,它包含静态小时查找数据

  • MONTH_DIMENSION:这是一个维度表,它包含静态月份查找数据

  • QUARTER_DIMENSION:这是一个维度表,它包含静态季度查找数据

以下图表展示了我们正在构建的数据仓库的详细星型模式数据模型:

图 4.10 – 我们数据仓库的数据模型

图 4.10 – 我们数据仓库的数据模型

现在,让我们了解前面图表中显示的表及其列:

  • DEVICE_EVENT_LOG_FACT表中,以下情况正在发生:

    1. 我们使用eventLogId作为主键,它映射到文件中的incidentNumber

    2. 我们为DEVICE_DIMENSIONEVENT_DIMENSIONHOUR_DIMENSIONMONTH_DIMENSIONQUARTER_DIMENSION表设置了外键字段

    3. eventTimestampclosurestatusclosureDuration都是事实表中每一行的事实

  • DEVICE_DIMENSIONEVENT_DIMENSION的列由需求以及输入文件(即device_dm.csvevent_dm.csv)中设备和事件的数据/属性决定。也就是说,这两个表的主键(deviceIdeventId)应该是系统生成的序列号,分配给记录。这两个表的主键是事实表外键关系中的参考列。

  • 除了设备和事件之外,我们还设计了三个其他维度表,表示一天中的小时(HOUR_DIMENSION)、月份(MONTH_DIMENSION)和季度(QUARTER_DIMENSION)。这些是静态查找表,其数据将随着时间的推移始终保持不变。

在数据模型方面,接下来需要做出的设计决策是选择数据库。各种关系数据库管理系统(RDBMS)非常适合数据仓库,例如 Snowflake、AWS Redshift、PostgreSQL 和 Oracle。虽然前两种选项是基于云的数据仓库,但其他两种选项可以在本地和云中运行。对于我们的用例,我们应该选择既经济高效又兼容未来的数据库。

在这些选择中,我们将选择 PostgreSQL,因为它是一个功能强大且丰富的免费数据库,适合托管数据仓库。此外,我们的应用程序未来可能迁移到云端。在这种情况下,它可以轻松迁移到 AWS Redshift,因为 AWS Redshift 基于行业标准的 PostgreSQL。

现在我们已经设计好了数据模型并选择了数据库,让我们继续设计解决方案。

设计解决方案

为了设计当前问题声明的解决方案,让我们分析我们现在可用的数据点或事实:

  • 当前问题是基于批处理的数据工程问题

  • 当前的问题是数据摄取问题

  • 我们的源是包含结构化数据的 CSV 文件

  • 我们的目标是 PostgreSQL 数据仓库

  • 我们的数据仓库遵循星型模式,包含一个事实表,两个动态维度表和三个静态维度表

  • 考虑到我们的解决方案未来可能迁移到云端,我们应该选择一个与部署平台无关的技术

  • 对于本书的背景和范围,我们将基于 Java 技术探索最佳解决方案

基于上述事实,我们可以得出结论,我们必须构建三个类似的数据摄取管道 – 一个用于事实表,另外两个用于动态维度表。在这个时候,我们必须问自己,如果文件摄取成功或失败,文件会发生什么?我们如何避免再次读取文件?

我们将从input文件夹中读取文件并将其摄取到数据仓库中。如果失败,我们将文件移动到error文件夹;否则,我们将文件移动到archive文件夹。以下图表展示了我们的发现,并提供了我们提出的解决方案的概述:

图 – 4.11 – 解决方案概述

图 – 4.11 – 解决方案概述

然而,这个提出的解决方案只是一个高层次的概述。在这个解决方案中,仍有许多问题尚未得到解答。例如,关于摄取过程或我们应该使用什么技术来解决这个问题,没有详细的说明。

首先,让我们根据我们手头的事实尝试决定一个技术。我们需要找到一个支持批处理摄取的基于 Java 的 ETL 技术。它还应该具有易于使用的 JDBC 支持,以便从 PostgreSQL 写入和读取数据。我们还需要一个调度器来安排批处理摄取作业,并应该有一个重试机制。此外,我们的数据量不大,所以我们想避免基于大数据的 ETL 工具。

Spring Batch 符合所有这些要求。Spring Batch 是一个基于 Java 的优秀的 ETL 工具,用于构建批处理作业。它包含一个作业调度器和作业存储库。此外,由于它是 Spring 框架的一部分,它可以轻松地与 Spring Boot 和 Spring 集成等工具和技术集成。以下图表显示了 Spring Batch 架构的高级组件:

图 4.12 – Spring Batch 架构

图 4.12 – Spring Batch 架构

以下图表表示 Spring Batch 作业的工作方式。让我们看看 Spring Batch 作业在执行过程中经历的各个步骤:

  1. Spring Batch 作业使用 Spring 的作业调度器来安排作业。

  2. Spring 作业调度器运行作业启动器,它反过来执行Spring Batch 作业。它还在此处创建一个作业实例,并将此信息持久化到作业仓库数据库中。

  3. batch_job_instance

  4. batch_job_execution

  5. batch_job_execution_params

  6. batch_step_execution

  7. batch_job_execution_context

  8. batch_step_execution_context

  9. 作业启动器执行的Spring Batch 作业启动单个步骤以执行作业。每个步骤执行特定任务以实现作业的整体目标。

  10. 虽然在作业实例的所有步骤中都有一个作业执行上下文,但在每个执行步骤中都有一个步骤执行上下文

  11. 通常,Spring Batch 配置有助于将每个步骤按所需顺序连接起来,以创建 Spring Batch 管道。

  12. 每一步,依次使用ReaderItemReader读取数据,使用ProcessorItemProcessor处理数据,并使用WriterItemWriter写入处理后的数据。

现在我们对 Spring Batch 架构有了相当的了解,我们将使用 Spring Batch 作业框架来设计我们的数据摄取管道。以下图表显示了我们的数据摄取管道的架构:

图 4.13 – 解决方案架构

图 4.13 – 解决方案架构

让我们更详细地看看这个解决方案:

  1. 像所有Spring Batch 作业一样,Spring 作业调度器安排一个作业启动器,该启动器实例化 Spring 作业。

  2. 在我们的用例中,我们将使用总共三个顺序步骤和两个条件步骤来完成作业。

  3. 在第一步中,应用程序检查输入文件夹中是否有新文件或JobExecutionContext,并将退出状态标记为完成

  4. 如果JobExecutionContext着陆区处理区

  5. 完成第二步后,第三步(Spring 处理步骤)被启动。第三步将数据转换并加载到数据仓库中。

  6. 完成第三步后,启动Spring 存档步骤,该步骤将处理后的文件从处理文件夹移动到存档文件夹。

  7. 然而,如果字符串处理步骤失败,将启动Spring 错误处理步骤,其中将文件从处理区域文件夹移动到错误区域文件夹。

在本节中,我们学习了如何使用可用的事实和数据点逻辑上划分解决方案,并为问题提出一个最佳架构。我们还学习了每个解决方案的有效性取决于我们选择的技术堆栈。

在下一节中,我们将学习如何使用 Spring Batch 和相关技术实现和测试我们的解决方案。

实现和单元测试解决方案

在本节中,我们将构建 Spring Batch 应用程序来实现我们在上一节中设计的解决方案。我们还将运行和测试该解决方案。

首先,我们必须理解不同的作业将有自己的计划。然而,维度表需要在事实表之前加载,因为维度表是查找表。

为了讨论的简洁性,我们将只实现事实表的 Spring Batch 应用程序。在这个实现中,我们将手动将设备数据和事件数据从 CSV 文件加载到表中。然而,您可以通过实现解决方案并开发两个不同的 Spring Batch 应用程序来跟踪设备和事件维度表来跟随讨论的引导。在这个实现中,我们假设设备数据和事件数据已经加载到数据仓库中。

您可以通过执行以下 GitHub 链接中提供的 DML 来手动完成此操作:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/blob/main/Chapter04/SQL/chapter4_ddl_dml.sql.

我们需要首先创建一个 Spring Boot Maven 项目并添加所需的 Maven 依赖项。以下 Maven 依赖项应添加到pom.xml文件中,如下所示:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-batch</artifactId>
        <version>2.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
        <version>2.4.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.3.1</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.0-alpha0</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>2.0.0-alpha0</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

在这里添加了两个 Spring 依赖项:spring-boot-starter-batch用于 Spring Batch,spring-boot-starter-jdbc用于与postgreSQL数据库(用作数据仓库和 Spring Batch 存储库数据库)通信。除此之外,还添加了 PostgreSQL 的 JDBC 驱动程序和日志依赖项。

根据我们的架构,让我们首先创建 Spring Boot 应用程序的入口点,即Main类,并初始化作业调度器。以下代码表示我们的Main类:

@EnableConfigurationProperties
@EnableScheduling
@ComponentScan ({"com.scalabledataarchitecture.etl.*", "com.scalabledataarchitecture.etl.config.*"})
@SpringBootApplication
public class EtlDatawarehouseApplication {
    private static Logger LOGGER = LoggerFactory.getLogger(EtlDatawarehouseApplication.class);

    @Autowired
    JobLauncher jobLauncher;
    @Autowired
    Job etlJob;
    public static void main(String[] args) {
        try {
            SpringApplication.run(EtlDatawarehouseApplication.class, args);
        }catch (Throwable ex){
            LOGGER.error("Failed to start Spring Boot application: ",ex);
        }
    }
    @Scheduled(cron = "0 */1 * * * ?")
    public void perform() throws Exception
    {
        JobParameters params = new JobParametersBuilder().addString("JobID", String.valueOf(System.currentTimeMillis())).toJobParameters();
        jobLauncher.run(etlJob, params);
    }
}

@SpringBootApplication注解表示这个类是 Spring Boot 应用程序的入口点。此外,请注意,@EnableScheduling注解表示这个应用程序支持 Spring 作业调度。带有@Scheduled注解的方法有助于在配置的计划时间间隔内执行计划功能。

Spring Batch 作业调度器支持以下代码块中显示的所有三种格式:

@Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds}")
@Scheduled(fixedRateString = "${fixedRate.in.milliseconds}")
@Scheduled(cron = "${cron.expression}")

在这里,fixedDelayString确保在作业结束和另一个作业开始之间有n毫秒的延迟。fixedRateStringn毫秒运行计划中的作业,而cron使用某些 cron 表达式来安排作业。在我们的情况下,我们使用 cron 表达式来安排perform()方法。

perform() 方法添加了一个名为 JobID 的作业参数,并使用 jobLauncher 触发名为 etlJob 的 Spring Batch 作业。jobLauncherJobLauncher 类型的自动装配实例。

如前所述,EtlDatawarehouseApplication 类中的 etlJob 字段也是自动装配的,因此它是一个 Spring 实例。

接下来,我们将探讨创建 etlJob 实例的 Spring Batch 配置文件:

@Configuration
@EnableBatchProcessing
public class BatchJobConfiguration {
    @Bean
    public Job etlJob(JobBuilderFactory jobs,
                      Step fileCheck, Step fileMoveToProcess, Step processFile,Step fileMoveToArchive, Step fileMoveToError) {
        return jobs.get("etlJob")
                .start(fileCheck).on(ExitStatus.STOPPED.getExitCode()).end()
                .next(fileMoveToProcess)
                .next(processFile).on(ExitStatus.COMPLETED.getExitCode()).to(fileMoveToArchive)
                .from(processFile).on(ExitStatus.FAILED.getExitCode()).to(fileMoveToError)
                .end()
                .build();
    }
}

如您所见,该类被注解为 @Configuration@EnableBatchProcessing。这确保了 BatchJobConfiguration 类被注册为 Spring 中的一个配置实例,以及一些其他与批量相关的实例组件,例如 JobLauncherJobBuilderFactoryJobRepositoryJobExplorer

etlJob() 函数使用 JobBuilderFactory 创建步骤管道,正如设计阶段所描述的。etlJob 管道从 fileCheck 步骤开始。如果 fileCheck 步骤的退出状态是 STOPPED,则批量作业结束;否则,它将移动到下一个步骤——即 fileMoveToProcess。下一个步骤是 processFile。从 processFile 步骤返回 COMPLETED 后,将调用 moveToArchive 步骤。然而,如果返回 ExitStatusFAILED,则调用 moveToError 步骤。

然而,我们可以创建一个 etlJob 实例。为此,我们需要创建所有拼接在一起形成批量作业管道的步骤实例。让我们首先看看如何创建 fileCheck 实例。

要创建 fileCheck 实例,我们编写了以下两个类:

  • FileCheckConfiguration:一个配置类,其中初始化了 fileCheck 实例。

  • FileCheckingTasklet:用于 fileCheck 步骤的 Tasklet 类。Tasklet 的目的是在步骤内执行单个任务。

FileCheckingTasklet 是一个 Tasklet,因此它将实现 Tasklet 接口。代码将类似于以下内容:

public class FileCheckingTasklet implements Tasklet{
//...
}

Tasklet 只包含一个必须实现的方法——execute(),它具有以下类型签名:

public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception

FileCheckingTasklet 中,我们希望检查是否在目标区域中存在任何文件。我们使用此 Tasklet 的主要目的是根据文件是否存在来更改任务的 EXITSTATUS 属性。Spring Batch 提供了一个名为 StepExecutionListener 的接口,使我们能够根据我们的需求修改 EXITSTATUS。这可以通过实现 StepExecutionListenerafterStep() 方法来完成。StepExecutionListener 的接口定义如下:

public interface StepExecutionListener extends StepListener {
    void beforeStep(StepExecution var1);
    @Nullable
    ExitStatus afterStep(StepExecution var1);
}

因此,我们的 FileCheckingTasklet 将类似于以下内容:

public class FileCheckingTasklet implements Tasklet, StepExecutionListener {
//...
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
//...
}
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
//...
}
}

现在,让我们理解我们想要在这个Tasklet中执行的逻辑。我们想要列出着陆区目录中的所有文件。如果没有文件存在,我们想要将EXITSTATUS设置为STOPPED。如果我们找到一个或多个文件,我们想要将EXITSTATUS设置为COMPLETED。如果在列出目录时发生错误,我们将设置EXITSTATUSFAILED。由于我们可以在afterStep()方法中修改EXITSTATUS,因此我们将在这个方法中编写我们的逻辑。然而,我们想要在我们的应用程序中配置我们的着陆区文件夹。我们可以通过使用一个名为EnvFolderProperty的配置 POJO 来实现这一点(我们将在本章后面讨论这个类的代码)。以下是afterstep()方法的逻辑:

@Override
public ExitStatus afterStep(StepExecution stepExecution) {
    Path dir = Paths.get(envFolderProperty.getRead());
    LOGGER.debug("Checking if read directory {} contains some files...", dir);
    try {
        List<Path> files = Files.list(dir).filter(p -> !Files.isDirectory(p)).collect(Collectors.toList());
        if(files.isEmpty()) {
            LOGGER.info("Read directory {} does not contain any file. The job is stopped.", dir);
            return ExitStatus.STOPPED;
        }
        LOGGER.info("Read directory {} is not empty. We continue the job.", dir);
        return ExitStatus.COMPLETED;
    } catch (IOException e) {
        LOGGER.error("An error occured while checking if read directory contains files.", e);
        return ExitStatus.FAILED;
    }
}

由于我们不想在这个Tasklet中执行任何其他处理,我们将让execute()方法通过一个RepeatStatusFINISHED的状态通过。因此,FileCheckingTasklet的完整代码如下:

public class FileCheckingTasklet implements Tasklet, StepExecutionListener {
    private final static Logger LOGGER = LoggerFactory.getLogger(FileCheckingTasklet.class);
    private final EnvFolderProperty envFolderProperty;
    public FileCheckingTasklet(EnvFolderProperty envFolderProperty) {
        this.envFolderProperty = envFolderProperty;
    }
    @Override
    public void beforeStep(StepExecution stepExecution) {
    }
    @Override
    public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
        return RepeatStatus.FINISHED;
    }
    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        // Source code as shown in previous discussion ...
    }
}

现在,让我们看看我们如何使用FileCheckingTasklet来创建fileCheck步骤。在FileCheckConfiguration配置类中,首先,我们为FileCheckingTasklet创建一个 bean,如下所示:

@Bean
public Tasklet fileCheckingTasklet(EnvFolderProperty envFolderProperty) {
    return new FileCheckingTasklet(envFolderProperty);
}

然后,我们使用这个 bean 创建fileCheck步骤 bean,如下所示:

@Bean
public Step fileCheck(StepBuilderFactory stepBuilderFactory, Tasklet fileCheckingTasklet) {
    return stepBuilderFactory.get("fileCheck")
            .tasklet(fileCheckingTasklet)
            .build();
}

最后,FileCheckConfiguration配置类的完整代码如下:

@Configuration
public class FileCheckConfiguration {
    @Bean
    public Tasklet fileCheckingTasklet(EnvFolderProperty envFolderProperty) {
        return new FileCheckingTasklet(envFolderProperty);
    }
    @Bean
    public Step fileCheck(StepBuilderFactory stepBuilderFactory, Tasklet fileCheckingTasklet) {
        return stepBuilderFactory.get("fileCheck")
                .tasklet(fileCheckingTasklet)
                .build();
    }
}

在前面的步骤中,我们学习了如何使用TaskletStepExecutionListener接口创建步骤,并使用 Spring 的强大注解(如@Bean@Configuration@AutoWired)实例化和利用它们。

重要提示

Spring Batch 提供了各种监听器(监听器接口),可以在不同的级别拦截、监听并对 Spring Batch 作业流程做出反应。如果您感兴趣,您可以在howtodoinjava.com/spring-batch/spring-batch-event-listeners/了解更多关于 Spring Batch 监听器的信息。

现在,我们将继续到下一个步骤,称为moveFileToProcess,并看看我们如何实现其设计。同样,为了实现moveFileToProcess步骤,我们将编写一个名为FileMoveToProcessConfiguration的配置文件和一个名为FileMoveToProcessTasklet的任务文件。

首先,让我们构建我们的FileMoveToProcessTasklet任务。为了构建我们的任务,我们将定义在使用它时想要完成的任务。以下是我们要使用此任务完成的任务:

  • 列出着陆区中存在的文件

  • 一次移动一个文件到处理区

  • 将目标完整文件路径(处理区的文件路径)作为键值条目添加到JobExecutionContext

就像我们之前开发的那个任务FileMoveToProcessTasklet一样,它也将实现TaskletStepExecutionListener接口。

以下代码显示了我们对Tasklet接口的execute()函数的实现:

@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
    Path dir = Paths.get(envFolderProperty.getRead());
    assert Files.isDirectory(dir);
    List<Path> files = Files.list(dir).filter(p -> !Files.isDirectory(p)).collect(Collectors.toList());
    if(!files.isEmpty()) {
        Path file = files.get(0);
        Path dest = Paths.get(envFolderProperty.getProcess() + File.separator + file.getFileName());
        LOGGER.info("Moving {} to {}", file, dest);
        Files.move(file, dest);
        filepath = dest;
    }
    return RepeatStatus.FINISHED;
}

首先,我们从着陆区(读取目录路径)列出文件,如果文件列表不为空,我们就获取第一个文件并将其移动到目标路径。在这里,我们通过将文件名和文件分隔符附加到处理目录来创建目标路径。

一旦文件成功移动到目标位置,我们将filepath实例变量的值设置为文件已移动到的目标路径。我们将在我们的afterStep()方法实现中使用它。现在,让我们看看afterStep()方法的实现,如下所示:

@Override
public ExitStatus afterStep(StepExecution stepExecution) {
    if(filepath != null) {
        stepExecution.getJobExecution().getExecutionContext().put("filepath", filepath);
        stepExecution.getJobExecution().getExecutionContext().put("filepathName", filepath.toString());
    }
    return ExitStatus.COMPLETED;
}

afterStep()方法实现中,如果filePath不为空(这意味着在任务执行期间至少有一个文件存在于着陆区,并且已被任务成功移动到处理区),我们在JobExecutionContext中存储两个键值条目(filePathfilePathName)。

现在,让我们看看FileMoveToProcessTasklet类的完整代码:

public class FileMoveToProcessTasklet implements Tasklet, StepExecutionListener {
    private final static Logger LOGGER = LoggerFactory.getLogger(FileMoveToProcessTasklet.class);
    private final EnvFolderProperty envFolderProperty;
    private Path filepath;
    public FileMoveToProcessTasklet(EnvFolderProperty envFolderProperty) {
        this.envFolderProperty = envFolderProperty;
    }
    @Override
    public void beforeStep(StepExecution stepExecution) {
    }
    @Override
    public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
   // Source code as shown in previous discussion ...
 }
    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
     // Source code as shown in previous discussion ...            
    }
}

FileMoveToProcessConfiguration的源代码将与之前讨论的FileCheckConfiguration非常相似。

FileMoveToProcessConfiguration的源代码如下:

@Configuration
public class FileMoveToProcessConfiguration {
    @Bean
    public Tasklet fileMoveToProcessTasklet(EnvFolderProperty envFolderProperty) {
        return new FileMoveToProcessTasklet(envFolderProperty);
    }
    @Bean
    public Step fileMoveToProcess(StepBuilderFactory stepBuilderFactory, Tasklet fileMoveToProcessTasklet) {
        return stepBuilderFactory.get("fileMoveToProcess")
                .tasklet(fileMoveToProcessTasklet)
                .build();
    }
}

现在,我们将学习如何开发processFile步骤。这是一个重要的步骤,因为所有的转换和摄取都发生在这里。这个步骤遵循典型的SpringBatch步骤模板,其中有一个ItemReaderItemProcessorItemWriter。它们被缝合在一起形成步骤管道。

首先,让我们看看ProcessFileConfiguration配置类中processFile()方法构建步骤管道的源代码:

@Bean
public Step processFile(StepBuilderFactory stepBuilderFactory, ItemReader<EventLogODL> csvRecordReader, JdbcBatchItemWriter<DeviceEventLogFact> jdbcWriter) {
    return stepBuilderFactory.get("processFile")
            .<EventLogODL, DeviceEventLogFact>chunk(chunkSize)
            .reader(csvRecordReader)
            .processor(deviceEventProcessor)
            .writer(jdbcWriter)
            .build();
}

在这里,我们正在从名为csvRecordReaderItemReader Bean 构建步骤,该 Bean 从 CSV 文件中读取记录并返回一组EventLogODL POJO 对象。一个名为deviceEventProcessorItemProcessor Bean,它读取每个EventLogODL POJO 并将它们转换成DeviceEventLogFact POJOs,以及一个名为jdbcWriterItemWriter Bean,它读取每个记录作为DeviceEventLogFact POJO 并将它们持久化到 PostgreSQL 数据仓库。我们在构建管道时也提到了chunk,同时使用chunkSize作为可配置参数(为了学习目的,我们将使用chunkSize1进行测试)。

在解释如何开发ItemReader Bean 之前,让我们看看EventLogODL POJO 类的源代码:

public class EventLogODL {
    private String incidentNumber;
    private String deviceSerialNum;
    private String eventCode;
    private String loggedTime;
    private String closureTime;
    private String status;
    private String assignedTo;
    private String resolutionComment;
  // Getters and Setter of the instance fields
 } 

现在,让我们看看创建ItemReader Bean 的方法:

@Bean
@StepScope
public FlatFileItemReader<EventLogODL> csvRecordReader(@Value("#{jobExecutionContext['filepathName']}")  String filePathName)
        throws UnexpectedInputException, ParseException {
    FlatFileItemReader<EventLogODL> reader = new FlatFileItemReader<EventLogODL>();
    DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
    String[] tokens = { "incidentNumber","deviceSerialNum","eventCode","loggedTime","closureTime","status","assignedTo","resolutionComment" };
    tokenizer.setNames(tokens);
    reader.setResource(new FileSystemResource(filePathName));
    reader.setLinesToSkip(1);
    DefaultLineMapper<EventLogODL> lineMapper =
            new DefaultLineMapper<EventLogODL>();
    lineMapper.setLineTokenizer(tokenizer);
    lineMapper.setFieldSetMapper(new BeanWrapperFieldSetMapper<EventLogODL>(){
        {
            setTargetType(EventLogODL.class);
        }
    });
    reader.setLineMapper(lineMapper);
    return reader;
}

在这里,我们使用 Spring Batch 内置的FlatFileItemReader来读取 CSV 源文件。由于我们需要从jobExecutionContext中动态读取filePathName,这是我们之前设置的,所以我们使用了@SetScope注解,它将默认的 bean 作用域从单例改为步骤特定的对象。这个注解在需要从JobExecutionContextStepExecutionContext动态读取某些参数的后期绑定中特别有用。此外,我们创建了一个带有fieldNames的分隔符标记化器,以及一个BeanWrapperFieldSetMapper来将每条记录映射到EventLogODL POJO,并设置FlatfileItemReader实例的相应属性。

故障排除技巧

在一个理想的世界里,所有数据都是完美的,我们的作业应该每次都能正常运行。但现实并非如此。如果数据损坏会怎样?如果文件中的几个记录没有遵循正确的模式会怎样?我们如何处理这种情况?

没有简单的答案。然而,Spring Batch 为你提供了一些处理失败的能力。如果文件本身损坏且不可读,或者文件没有适当的读写权限,那么在构建步骤时将进入faultTolerance状态。这可以通过使用skipLimit()skip()noSkip()方法或使用自定义的SkipPolicy来实现。在我们的例子中,我们可以在ProcessFileConfiguration类的processFile方法中添加容错性,并跳过某些类型的异常,同时确保其他类型的异常导致步骤失败。以下代码展示了示例:

return stepBuilderFactory.get("processFile")

        .<EventLogODL, DeviceEventLogFact>chunk(chunkSize)

        .reader(csvRecordReader)

.faultTolerant().skipLimit(20).skip(SAXException.class).noSkip(AccessDeniedException.class)

        .processor(deviceEventProcessor)

        .writer(jdbcWriter)

        .build();

正如我们所见,我们可以在stepBuilderFactory.build()中通过链式调用faultTolerant()方法来增加容错性。然后,我们可以链式调用skip()方法,使其跳过 20 个SAXException类型的错误,并使用noSkip()方法确保AccessDeniedException总会导致步骤失败

现在,让我们看看如何开发我们的自定义ItemProcessor。名为DeviceEventProcessor的自定义ItemProcessor的源代码如下所示:

@Component
public class DeviceEventProcessor implements ItemProcessor<EventLogODL, DeviceEventLogFact> {
    @Autowired
    DeviceEventLogMapper deviceEventLogMapper;
    @Override
    public DeviceEventLogFact process(EventLogODL eventLogODL) throws Exception {
        return deviceEventLogMapper.map(eventLogODL);
    }
}

正如你所见,我们已经实现了ItemProcessor接口,其中我们需要实现process()方法。为了将EventLogODL POJO 转换为DeviceEventLogFact POJO,我们创建了一个名为DeviceEventLogMapper的代理组件。DeviceEventLogMapper的源代码如下:

@Component
public class DeviceEventLogMapper {
    @Autowired
    JdbcTemplate jdbcTemplate;
    public DeviceEventLogFact map(EventLogODL eventLogODL){
        String sqlForMapper = createQuery(eventLogODL);
        return jdbcTemplate.queryForObject(sqlForMapper,new BeanPropertyRowMapper<>(DeviceEventLogFact.class));
    }
    private String createQuery(EventLogODL eventLogODL){
        return String.format("WITH DEVICE_DM AS\n" +
                "\t(SELECT '%s' AS eventLogId "+ ...,eventLogODL.getIncidentNumber(),eventLogODL.getDeviceSerialNum(),...);
    }

由于我们正在为事实表编写代码,我们需要从不同的维度表中获取各种主键来填充我们的 DeviceEventLogFact POJO。在这里,我们通过使用 jdbcTemplate 动态创建查询来从数据仓库中获取维度主键,并从其 resultset 中填充 DeviceEventLogFact POJO。DeviceEventLogMapper 的完整源代码可在 GitHub 上找到,链接为 github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/blob/main/Chapter04/SpringBatchApp/EtlDatawarehouse/src/main/java/com/scalabledataarchitecture/etl/steps/DeviceEventLogMapper.java

最后,我们将在 ProcessFileConfiguration 类中创建一个名为 jdbcwriterItemWriter,如下所示:

@Bean
public JdbcBatchItemWriter<DeviceEventLogFact> jdbcWriter() {
    JdbcBatchItemWriter<DeviceEventLogFact> writer = new JdbcBatchItemWriter<>();
    writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>());
    writer.setSql("INSERT INTO chapter4.device_event_log_fact(eventlogid,deviceid,eventid,hourid,monthid,quarterid,eventtimestamp,closurestatus,closureduration) VALUES (:eventLogId, :deviceId, :eventId, :hourId, :monthId, :quarterId, :eventTimestamp, :closureStatus, :closureDuration)");
    writer.setDataSource(this.dataSource);
    return writer;
}

最后,我们的 ProcessFileConfiguration 类的源代码如下所示:

@Configuration
@SuppressWarnings("SpringJavaAutowiringInspection")
public class ProcessFileConfiguration {
    private final static Logger LOGGER = LoggerFactory.getLogger(ProcessFileConfiguration.class);

    @Value("${process.chunk_size:1}")
    int chunkSize;
    @Autowired
    DataSource dataSource;
    @Autowired
    DeviceEventProcessor deviceEventProcessor;

    @Bean
    @StepScope
    public FlatFileItemReader<EventLogODL> csvRecordReader(@Value("#{jobExecutionContext['filepathName']}")  String filePathName)
            throws UnexpectedInputException, ParseException {
        // Source code as shown in previous discussion ...
    }
    @Bean
    public JdbcBatchItemWriter<DeviceEventLogFact> jdbcWriter() {
// Source code as shown in previous discussion ...

 }
    @Bean
    public Step processFile(StepBuilderFactory stepBuilderFactory, ItemReader<EventLogODL> csvRecordReader, JdbcBatchItemWriter<DeviceEventLogFact> jdbcWriter) {

    // Source code as shown in previous discussion ...
    }
}

现在我们已经构建了代码,我们将在 resource 文件夹中存在的 application.yaml 文件中配置我们的属性,如下所示:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/sinchan
    username: postgres
    driverClassName: org.postgresql.Driver
  batch:
    initialize-schema: always
env:
  folder:
    read: /Users/sinchan/Documents/Personal_Docs/Careers/Book-Java_Data_Architect/chapter_4_pgrm/landing
    process: /Users/sinchan/Documents/Personal_Docs/Careers/Book-Java_Data_Architect/chapter_4_pgrm/process
    archive: /Users/sinchan/Documents/Personal_Docs/Careers/Book-Java_Data_Architect/chapter_4_pgrm/archive
    error: /Users/sinchan/Documents/Personal_Docs/Careers/Book-Java_Data_Architect/chapter_4_pgrm/error

.yaml 文件所示,我们必须提及 spring.datasource 属性,以便 Spring JDBC 可以自动自动连接数据源组件。

我们最终的代码结构如下所示:

图 4.14 – 代码的项目结构

图 4.14 – 代码的项目结构

我们可以通过在最喜欢的 IDE 中运行 Main 类(即 EtlDataWarehouseApplication)来运行和测试我们的程序。在运行我们的 Spring Batch 应用程序之前,我们必须安装 Postgres,创建模式和数据表,并填充所有维度表。详细的运行说明可以在本书的 GitHub 仓库中找到。

一旦我们运行了应用程序并将数据放置在着陆区,它就会被导入我们的数据仓库事实表,CSV 文件会被移动到存档区,如下面的截图所示:

图 4.15 – Spring Batch 作业运行后事实表中的数据导入

图 4.15 – Spring Batch 作业运行后事实表中的数据导入

我们还可以看到与批处理相关的表,其中包含各种运行统计信息,如下面的截图所示:

图 4.16 – 批处理作业执行日志

图 4.16 – 批处理作业执行日志

前面的截图显示了已运行的各个批处理作业的批执行日志。我们可以通过查看批步骤执行日志来了解更多关于特定作业的信息,如下面的截图所示:

图 4.17 – 批处理作业的步骤执行日志

图 4.17 – 批处理作业的步骤执行日志

通过这样,我们已经成功分析了、架构设计、开发并测试了一个基于批次的 ETL 数据摄取管道。正如在技术要求章节中提到的,详细的源代码可以在本书的 GitHub 仓库中找到。

摘要

在本章中,我们学习了如何从头开始分析数据工程需求,得出明确的结论,并提取出有助于我们在架构决策过程中的事实。接下来,我们学习了如何分析源数据,以及这种分析如何帮助我们构建更好的数据工程解决方案。进一步地,我们利用事实、需求和我们的分析,为具有低或中等数据量的基于批次的 数据工程问题构建了一个稳健且有效的架构。最后,我们将设计映射到构建一个有效的使用 Spring Batch 的 ETL 批处理数据摄取管道,并对其进行测试。在这个过程中,你学习了如何从头开始分析数据工程问题,以及如何在下次遇到类似问题时有效地构建类似的管道。

现在我们已经成功架构和开发了一个针对中等和低量数据工程问题的基于批次的解决方案,在下一章中,我们将学习如何构建一个有效的数据工程解决方案来处理大量数据。在下一章中,我们将讨论构建一个有效的基于批次的大数据解决方案的有趣用例。

第五章:架构批处理管道

在上一章中,我们学习了如何使用 Spring Batch 架构中等到低量的批处理解决方案。我们还学习了如何使用 DataCleaner 对此类数据进行分析。然而,随着数据增长呈指数级,大多数公司不得不处理大量数据并分析以获得优势。

在本章中,我们将讨论如何分析、分析和架构一个基于批处理的大数据解决方案。在这里,我们将学习如何选择技术栈并设计一个数据管道,以创建一个优化且成本效益高的大数据解决方案。我们还将学习如何使用 Java、Spark 以及各种 AWS 组件来实现此解决方案,并测试我们的解决方案。之后,我们将讨论如何优化解决方案以更节省时间和成本。到本章结束时,您将了解如何使用 S3、Apache Spark(Java)、AWS EMR、AWS Lambda 和 AWS Athena 在 AWS 中架构和实现数据分析管道。您还将了解如何微调代码以实现优化性能,以及如何规划和优化实施成本。

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

  • 架构开发和选择合适的工具

  • 实现解决方案

  • 使用 AWS Athena 查询 ODL

技术要求

为了跟随本章内容,您需要以下内容:

  • 熟悉 Java

  • 熟悉 Apache Spark 的基础知识

  • 在您的本地机器上安装了 Java 1.8 或更高版本

  • 在您的本地机器上安装了 IntelliJ Idea 社区版或终极版

  • 一个 AWS 账户

本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter05

架构开发和选择合适的工具

在数据工程中,数据成功摄入并存储在数据湖或数据仓库后,通常需要对其进行挖掘和存储,以满足特定需求,以更有序和定制化的形式进行报告和分析。在本章中,我们将讨论这样一个问题,即需要分析并存储大量数据,以便为特定的下游受众提供更定制化的格式。

问题陈述

假设一家电商公司 ABC 想要分析其产品上的各种用户互动,并确定每个月每个类别的畅销产品。他们希望为每个类别的畅销产品提供激励措施。他们还希望为那些具有最高浏览到销售比率的但不是畅销产品的产品提供特别优惠和营销推广工具。此外,他们还希望向每个类别中销量最低的产品团队提供卖家工具和培训,以及营销服务。目前,ABC 将其产品交易的所有用户交易存储在其产品交易数据库中,但没有提供关于顶级卖家、最差卖家和最高浏览到销售比率的月度数据视图。他们希望创建一个组织化数据层ODL),以便可以轻松地以最佳性能每月执行此类分析查询。

分析问题

让我们来分析给定的问题。首先,让我们从数据的四个维度来分析需求。

首先,我们将尝试回答这个问题:数据速度是什么? 从需求中可以看出,我们需要创建一个包含月度分析数据的 ODL。因此,我们的数据将在一个月后使用,所以我们没有实时数据处理需求。因此,我们可以安全地假设我们正在处理一个批处理问题。然而,了解数据到达的频率将有助于我们确定可以安排批处理作业的频率。因此,我们必须询问电商公司,源数据将以多高的频率提供给我们? ABC 告诉我们,源数据将以每月或双月为基础以 CSV 文件的形式提供给我们,但不会每天两次。这个信息对我们很有帮助,但也引发了一些其他问题。

现在,我们心中最明显的问题就是,每月或每两个月共享的数据/文件有多大? 考虑到每条记录都将是在电商市场中任何用户对任何产品进行的每次交易的单独事件,数据可能非常庞大。ABC 告诉我们,交易可以是查看、购物车或购买交易。因此,对于每个动作,例如查看产品、添加到购物车和购买产品,文件中都会有单独的条目。ABC 还告诉我们,产品和类别的数量可能会在未来增加。根据我们的估计和 ABC 的内部数据,发送给我们的每个文件的数据量可能从数百 GB 到数 TB 不等。需要处理的数据量在数百 GB 到数 TB 之间,非常适合大数据处理。我们的分析还表明,电商流量将会随着时间的推移而增加。这些观察结果表明这是一个大数据问题。因此,我们需要开发一个大数据解决方案来解决这个批处理问题。

现在,我们将探讨数据的多样性。从我们之前的讨论中,我们知道数据是以 CSV 格式到达的。因此,这是需要分析和处理的结构化数据。我们将暂时讨论数据的多样性,因为我们将在实施阶段进行这一讨论。

作为架构师,我们必须做出的下一个决定是选择正确的平台。我们应该在本地运行这个应用程序还是在云中?两者都有利弊。然而,有几个关键点说明了为什么在这种情况下云可能是一个更好的选择:

  • 成本节约:运行一个包含数 TB 数据的大数据作业需要一个非常好的大数据基础设施来在本地运行。然而,这些作业每月只会运行一次或两次。如果我们选择创建本地环境,那么花费大量美元创建一个每月只使用一次或两次的集群 Hadoop 基础设施就没有意义了,但基础设施需要始终维护和运行。创建和维护此类基础设施的成本和努力并不合理地证明其利用率。在云中,你只需为作业运行期间使用的资源付费,这会便宜得多。云可以让你选择只为你使用的资源付费。

例如,在云中,你可以选择每天只让 Hadoop 环境运行 2 小时;这样,你只需为那 2 小时付费,而不是一整天。更重要的是,它支持弹性,这意味着你可以根据你的使用情况自动扩展你的节点数量到更高的或更低的数量。这给了你每次只使用所需资源的灵活性。例如,如果我们知道 11 月需要处理的数据量会很大,作业在 11 月需要更多的资源和时间,我们可以在 11 月增加资源容量,并在数据量减少到正常水平时降低它。云技术的这些能力使得整个系统的执行(尤其是资本支出CapEx)成本)可以大幅节省。

  • 工作负载的季节性变化:通常,在电子商务网站上,节假日或节庆期间的活跃度很高,而在其他时间,活跃度则较低。用户活动直接影响到该月的文件大小。因此,我们必须能够根据需要调整基础设施的规模。这在云中可以轻松实现。

  • 未来的弹性:作为明确要求之一,产品类别数量很可能会增加,这意味着我们将来需要扩大处理和存储容量。虽然此类更改在本地环境中需要大量时间和资源,但在云中可以轻松实现。

  • 缺乏敏感数据:在我们的用例中,没有特定的联邦或受保护的健康信息PHI)数据需要加密或标记后存储在云中。因此,我们应该符合法律和数据安全要求。

尽管我们可以选择任何公共云平台,但为了方便起见,我们将在这本书中使用 AWS 作为我们的云平台。

构建解决方案

既然我们已经收集并分析了我们的需求,让我们尝试构建解决方案的架构。为了构建这个解决方案,我们需要回答以下问题:

  • 我们应该在何处存储或放置输入数据?

  • 我们应该如何处理输入数据?

  • 我们应该在何处以及如何存储输出数据/ODL?

  • 我们应该如何提供一个查询接口给 ODL?

  • 我们应该如何以及何时安排处理作业?

让我们看看我们存储输入数据有哪些选项。我们可以将数据存储在以下服务之一:

  • S3S3简单存储服务是一个非常流行的对象存储服务。它既便宜又非常可靠。

  • EMR/EC2 附加的 EBS 卷弹性块存储EBS)是一种可以将存储卷附加到任何虚拟服务器(如 EC2 实例)的块存储解决方案。对于大数据解决方案,如果您使用弹性 MapReduceEMR),EBS 卷可以附加到该 EMR 集群中的每个参与 EC2 节点。

  • 弹性文件系统(EFS):EFS 是一个共享文件系统,通常连接到 NAS 服务器。它通常用于内容存储库、媒体存储或用户主目录。

让我们讨论在选择存储之前需要考虑的不同因素。

影响您存储选择的因素

成本是我们在选择任何云组件时需要考虑的重要因素。然而,让我们看看除了成本之外影响我们存储选择的其他因素。以下是一些因素:

  • 性能:从 IOPS 的角度来看,EBS 和 EFS 可以比 S3 更快地执行。尽管 S3 的性能较慢,但读取其他存储选项中的数据并不显著较慢。从性能角度来看,EFS 或 EBS 卷仍然会被优先考虑。

  • 可扩展性:尽管所有三种存储选项都是可扩展的,但 S3 具有最无缝的可扩展性,无需任何手动努力或中断。由于可扩展性是我们随着数据随时间增长的重要需求之一,并且未来可能存在更大的文件大小(根据要求),从这个角度来看,S3 是一个明显的赢家。

  • 生命周期管理:S3 和 EFS 都具有生命周期管理功能。假设你认为旧文件(超过一年的文件)需要存档;这些程序可以无缝地移动到另一个更便宜的存储类别,这提供了无缝的存档存储以及成本节约。

  • 无服务器架构支持:S3 和 EFS 都提供无服务器架构支持。

  • 高可用性和鲁棒性:再次强调,S3 和 EFS 都是高度鲁棒和可用的存储选项。在这方面,EBS 与其他两种存储选项不相上下。

  • 大数据分析工具兼容性:从 S3 或 EBS 卷读取和写入数据对于 Spark 和 MapReduce 等大数据处理引擎来说要容易得多。如果数据存储在 S3 或 EBS 中,创建外部 Hive 或 Athena 表也要容易得多。

如我们所见,S3 和 EFS 似乎都是很有前景的选项。现在,让我们看看成本在确定云存储解决方案中的重要性。

基于成本确定存储

对于任何云解决方案架构师来说,最重要的工具之一是成本估算器或定价计算器。由于我们使用 AWS,我们将使用 AWS 定价计算器:calculator.aws/#/

我们将使用这个工具来比较在 EFS 和 S3 存储中存储输入数据的成本。在我们的用例中,我们假设每月获得 2 TB 的数据,并且我们必须存储每月数据 3 个月才能存档。我们还需要存储长达 1 年的数据。让我们看看我们的成本如何根据我们的存储选择而变化。

在这里,对于任何类型的存储,我们将使用S3 智能分层(它支持自动生命周期管理和降低成本)来进行计算。它要求我们提供每月的平均数据存储量以及频繁访问层、不频繁访问层和存档层的存储量。

为了计算每月所需的数据存储平均量,在我们的用例中,每月会生成 2 TB 的新数据。因此,在第一个月,我们需要存储 2 TB 的数据,第二个月需要存储 4 TB 的数据,第三个月需要存储 6 TB 的数据,以此类推。所以,为了计算平均数据量,我们必须将每个月的存储需求相加,然后将结果除以 12。这个数学方程如下:

前面的公式给出了每月 13 TB 的计算结果。现在,它要求我们提供频繁访问层存储的百分比——即我们将从该层读取数据的层。频繁访问层中的数据每月只能有 2 TB(大约是 13 TB 的 15%)。使用这些值,我们可以计算出估算成本,如下面的截图所示:

图 5.1 – AWS S3 成本估算工具

图 5.1 – AWS S3 成本估算工具

使用之前提到的计算方法,Amazon S3 的平均估算成本为每月 97.48 美元。然而,对 Amazon EFS 的类似计算将花费 881.92 美元。这表明使用 EFS 将比使用 Amazon S3 贵九倍。

因此,从成本和其他参数综合考虑,我们可以安全地决定选择 Amazon S3 作为我们的输入存储。基于类似的逻辑和计算,我们也可以将 ODL 存储在 S3 中。

然而,在未决定输出文件的架构和格式的情况下,关于输出数据存储层的讨论是不完整的。根据要求,我们可以得出结论,输出数据应包含以下列:

  • year

  • month

  • category_id

  • product_id

  • tot_sales

  • tot_onlyview

  • sales_rev

  • rank_by_revenue

  • rank_by_sales

由于 ODL 上的大多数查询都是按月进行的,因此建议按年度和月度对表进行分区。现在我们已经最终确定了存储层的所有细节,让我们讨论解决方案的处理层。

现在,我们必须考虑 AWS 中可用于处理大数据批处理作业的选项。主要有两个本地替代方案。一个是运行 EMR 集群上的 Spark,另一个是运行 Glue 作业(Glue 是一个完全管理的无服务器 AWS 服务)。AWS Glue 允许你用 Scala 或 Python 编写脚本,并通过 AWS 管理控制台或编程方式触发 Glue 作业。由于我们感兴趣的是用 Java 实现解决方案,因此 AWS Glue 不是我们的选择。此外,AWS Glue 脚本的可移植性较低,学习曲线较高。在这里,我们将坚持使用 EMR 上的 Spark。然而,Spark 作业可以在 EMR 集群中以两种方式运行:

  • 从命令行运行spark submit的经典方法

  • 在创建集群时添加spark submit步骤的云特定方法

现在,让我们看看成本矩阵如何帮助我们确定在前面提到的两种选项中,我们应该采取哪种方法来提交 Spark 作业。

处理层中的成本因素

第一种选择是始终保持 EMR 集群运行,并使用 cronjob 在特定时间间隔从 shell 脚本中触发spark submit命令(这与我们在本地 Hadoop 集群上所做的工作非常相似)。这种集群被称为持久 EMR 集群

第二种选择是在创建集群时添加 EMR 步骤以运行 Spark 作业,一旦成功运行,就终止它。这种类型的 EMR 集群被称为临时 EMR 集群。让我们看看每种选择的成本估计如何变化。

在 EMR 集群中,有三种类型的节点:主节点核心节点任务节点。主节点管理集群,充当 NameNode 和 Jobtracker。核心节点充当 DataNode,同时也是工作节点,负责处理数据。任务节点是可选的,但它们对于单独的任务跟踪活动是必需的。

由于它们的工作性质,通常选择计算优化实例对主节点工作很好,而对于核心节点,混合实例效果最佳。在我们的计算中,我们将使用 c4.2xlarge 实例类型作为主节点,m4.4xlarge 实例类型作为核心节点。如果我们需要四个核心节点,一个持久 EMR 集群将每月花费我们大约 780 美元。在临时 EMR 集群上的类似配置每月只需花费大约 7 美元,考虑到作业每月运行两到三次,每次作业运行时间不超过 2 小时。正如我们所见,第二种选择几乎提高了 100 倍的成本效益。因此,我们将选择临时 EMR 集群。

现在,让我们弄清楚如何创建和调度临时的 EMR 集群。在我们的用例中,数据到达 S3 桶。S3 桶中的每个成功创建事件都会生成一个事件,可以触发 AWS Lambda 函数。我们可以使用这样的 Lambda 函数在 S3 桶的着陆区每次有新文件到达时创建临时集群。

根据前面的讨论,以下图展示了所提出解决方案的架构:

图 5.2 – 解决方案架构

图 5.2 – 解决方案架构

如前图所示,输入数据作为源数据进入 S3 桶(也称为着陆区)。在这里,一个源数据文件每月到达两次。架构图描述了四个步骤,用数字表示。让我们更详细地看看这些步骤:

  1. 当传入的源文件在 S3 桶中完全写入时,将生成一个 CloudWatch 事件。这会生成一个 Lambda 触发器,反过来,它会调用一个 Lambda 函数。

  2. Lambda 函数接收创建事件记录并创建一个临时 EMR 集群,其中配置了一个步骤来运行 Spark 作业以读取和处理新的输入文件。

  3. EMR 步骤中的 Spark 作业读取并处理数据。然后,它将转换后的输出数据写入 ODL 层的 S3 桶。在 Spark 步骤成功终止后,临时集群将被终止。

  4. ODL 层中驻留的所有数据都将作为 Athena 表公开,可以用于任何分析目的。

这为我们提供了一个非常简单但强大的架构来解决这样的大数据批处理问题。所有处理和存储组件中的日志和指标都将由 AWS CloudWatch 日志捕获。我们可以通过添加审计和警报功能来进一步改进此架构,这些功能使用 CloudWatch 日志和指标。

重要注意事项

由于以下因素,建议使用 Parquet 格式作为输出存储格式:

成本节省和性能:由于多个输出列可能具有低基数,ODL 数据存储格式应该是一个列式格式,这不仅可以节省成本,还可以提高性能。

技术兼容性:由于我们处理的是大数据处理,并且我们的处理层基于 Spark,Parquet 将是 ODL 层使用最合适的数据格式。

现在我们已经分析了问题并开发了一个稳健、可靠且成本效益高的架构,让我们来实施解决方案。

实施解决方案

任何实施的第一步始终是理解源数据。这是因为我们所有低级转换和清洗都将依赖于数据的多样性。在前一章中,我们使用了 DataCleaner 来概览数据。然而,这次我们处理的是大数据和云。如果数据量达到千兆字节,DataCleaner 可能不是进行数据概览的一个非常有效的工具。对于我们的场景,我们将使用一个名为 AWS Glue DataBrew 的基于 AWS 云的数据概览工具。

源数据概览

在本节中,我们将学习如何进行数据概览和分析,以了解传入的数据(您可以在 GitHub 上找到此示例文件:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter05。按照以下步骤操作:

  1. 使用 AWS 管理控制台创建一个名为 scalabledataarch 的 S3 存储桶,并将示例输入数据上传到 S3 存储桶:

图 5.3 – 创建 S3 存储桶并上传输入文件

图 5.3 – 创建 S3 存储桶并上传输入文件

  1. 从 AWS 管理控制台进入 AWS Glue DataBrew 服务。点击 DATASET 侧边栏。然后,点击 连接新数据集 按钮。将出现一个类似于以下截图的对话框。选择 Amazon S3 并输入 S3 存储桶的源数据路径。最后,点击 创建数据集 按钮以创建新的数据集:

图 5.4 – 向 AWS Glue DataBrew 添加新数据集

图 5.4 – 向 AWS Glue DataBrew 添加新数据集

  1. 现在,让我们使用我们添加的数据集创建一个数据概览作业。首先,选择新添加的数据集并转到 数据概览 选项卡,如图所示:

图 5.5 – 数据概览选项卡

图 5.5 – 数据概览选项卡

现在,点击 运行数据概览 按钮,这将带您到一个 创建作业 弹出窗口。输入作业名称并选择使用 全数据集 选项运行样本,如图所示:

图 5.6 – 创建数据概览作业

图 5.6 – 创建数据概览作业

将输出位置设置为 scalablearch-dataprof(我们的 S3 存储桶)。数据概览作业的输出文件将存储在此:

图 5.7 – 配置数据概览作业

图 5.7 – 配置数据概览作业

然后,配置 product_idcategory_idbrand

我们已相应地配置了它们,如下面的屏幕截图所示:

图 5.8 – 配置关联小部件

图 5.8 – 配置关联小部件

然后,我们必须设置数据概要作业的安全角色。一旦完成此操作,点击 创建作业 按钮来创建数据概要作业:

图 5.9 – 设置数据概要作业的安全权限

图 5.9 – 设置数据概要作业的安全权限

  1. 最后,我们可以在 概要作业 选项卡中看到新创建的数据概要作业。我们可以通过在此屏幕上点击 运行作业 按钮来运行数据概要作业:

图 5.10 – 已创建并列出的数据概要作业

图 5.10 – 已创建并列出的数据概要作业

一旦作业成功运行,我们可以转到数据集,打开 数据谱系 选项卡,查看数据谱系以及最后一次成功的数据概要作业运行之前的时间间隔:

图 5.11 – 数据概要作业的谱系

图 5.11 – 数据概要作业的谱系

  1. 我们可以将报告可视化以查找缺失值、基数、列之间的相关性以及数据的分布。这些度量帮助我们确定数据中是否存在需要清理的异常值,或者是否存在缺失值以及是否需要处理。这也有助于我们了解我们正在处理的数据质量。这有助于我们进行适当的清理和转换,以免在实施过程中出现意外。以下屏幕截图显示了 AWS Glue DataBrew 显示的一些示例度量:

图 5.12 – 数据概要度量

图 5.12 – 数据概要度量

在这里,我们可以看到一些有用的统计数据。例如,event_type 没有噪声,并且具有非常低的基数。它还显示数据不是通过此列均匀分布的。

现在我们已经分析了数据,让我们开发一个 Spark 应用程序来处理记录。

编写 Spark 应用程序

基于上一节的分析,我们将创建输入记录模式字符串。然后,我们将使用该模式字符串来读取输入数据,如下面的代码片段所示:

private static final String EVENT_SCHEMA = "event_time TIMESTAMP,event_type STRING,product_id LONG,category_id LONG,category_code STRING,brand STRING,price DOUBLE,user_id LONG,user_session STRING";
. . .
Dataset<Row> ecommerceEventDf = spark.read().option("header","true").schema(EVENT_SCHEMA).csv(inputPath);

然后,我们将使用 Spark 的 count_if 聚合函数计算每个产品的总销售额和总浏览量,如下面的代码片段所示:

Dataset<Row> countAggDf = spark.sql("select year(event_time) as year,month(event_time) as month,category_id,product_id,count_if(event_type='purchase') as tot_sales,count_if(event_type='view') as tot_onlyview from ecommerceEventDf where event_type!='cart' group by year,month,category_id,product_id ");

我们将创建另一个 DataFrame 来计算仅购买事件的收入总额。以下代码片段显示了如何进行此操作:

Dataset<Row> revenueAggDf = spark.sql("select year(event_time) as year,month(event_time) as month,category_id,product_id,sum(price) as sales_rev from ecommerceEventDf where event_type='purchase' group by year,month,category_id,product_id");

现在,我们将使用 LEFT OUTER JOIN SparkSQL 查询将 countAggDfrevenueAggDf DataFrame 合并,如下面的代码片段所示。对于没有单次销售的产品,使用 Spark 的 na.fill() 方法将 total_sales 的空值设置为 0.0

Dataset<Row> combinedAggDf = spark.sql("select cadf.year,cadf.month,cadf.category_id,cadf.category_id,cadf.product_id,tot_sales,tot_onlyview,sales_rev from countAggDf cadf LEFT OUTER JOIN revenueAggDf radf ON cadf.year==radf.year AND cadf.month== radf.month AND cadf.category_id== radf.category_id AND cadf.product_id == radf.product_id");
Dataset<Row> combinedEnrichAggDf = combinedAggDf.na().fill(0.0,new String[]{"sales_rev"});

现在,我们将对结果combinedEnrichedDf DataFrame 应用窗口函数,以推导出列 – 即rank_by_revenuerank_by_sales

Dataset<Row> finalTransformedDf = spark.sql("select year,month,category_id,product_id,tot_sales,tot_onlyview,sales_rev,dense_rank() over (PARTITION BY category_id ORDER BY sales_rev DESC) as rank_by_revenue,dense_rank() over (PARTITION BY category_id ORDER BY tot_sales DESC) as rank_by_sales from combinedEnrichAggDf");

结果已准备好,并且与输出格式相同。因此,我们必须使用 Parquet 格式将转换后的数据写入输出 S3 桶,同时确保它按年份和月份分区:

finalTransformedDf.write().mode(SaveMode.Append).partitionBy("year","month").parquet(outputDirectory);

此应用程序的完整源代码可在 GitHub 上找到,网址为github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter05/sourcecode/EcommerceAnalysis

在下一节中,我们将学习如何在 EMR 集群上部署和运行 Spark 应用程序。

部署和运行 Spark 应用程序

现在我们已经开发了 Spark 作业,让我们尝试使用临时 EMR 集群运行它。首先,我们将手动创建 EMR 集群并运行作业。要手动创建临时 EMR 集群,请按照以下步骤操作:

  1. 首先,构建 Spark 应用程序 JAR 文件并将其上传到 S3 桶。

  2. 前往 AWS 管理控制台中的 AWS EMR。点击创建集群按钮以手动创建新的临时集群。

  3. 设置 EMR 配置。确保将启动模式设置为步骤执行。确保在软件配置部分的版本字段中选择emr-6.4.0。此外,对于添加步骤,选择Spark 应用程序作为步骤类型。保留所有其他字段不变。您的配置应如下所示:

图 5.13 – 手动创建临时 EMR 集群

图 5.13 – 手动创建临时 EMR 集群

  1. 现在,要添加 Spark 步骤,请点击配置按钮。这将弹出一个对话框,您可以在其中输入各种与 Spark 步骤相关的配置,如下面的屏幕截图所示:

图 5.14 – 向 EMR 集群添加 Spark 步骤

图 5.14 – 向 EMR 集群添加 Spark 步骤

请确保在Spark-submit 选项区域中指定驱动类名称,并在应用程序位置参数框中提供必要的信息。

  1. 点击添加以添加步骤。一旦添加,它将类似于以下屏幕截图所示。然后,点击创建集群。这将创建临时集群,运行 Spark 作业,并在 Spark 作业执行完毕后终止集群:

图 5.15 – 添加 Spark 步骤的 EMR 集群配置

图 5.15 – 添加 Spark 步骤的 EMR 集群配置

  1. 一旦成功运行,您将在集群的步骤标签页中看到作业已成功:

图 5.16 – EMR 集群中的作业监控

图 5.16 – EMR 集群中的作业监控

故障排除 Spark 错误

Spark 作业在大量数据上运行并可能抛出多个异常。它还可以报告多个阶段失败,例如 OutOfMemoryException、大帧错误、来自 AWS S3 上传的多部分文件的节流错误等等。涵盖所有这些内容超出了本书的范围。然而,您可以参考一个非常简洁的 Spark 故障排除指南 docs.qubole.com/en/latest/troubleshooting-guide/spark-ts/troubleshoot-spark.xhtml#troubleshooting-spark-issues 以获取更多信息。

现在我们已经手动部署和运行了 Spark 应用程序,让我们通过实现 Lambda 触发器来自动化 Spark 作业的创建和运行。

开发和测试 Lambda 触发器

AWS Lambda 函数是完全托管的无服务器服务,有助于处理信息。它们支持多种语言,如 Python、JavaScript、Java 等。尽管 Python 或 JavaScript 运行时更快,但我们将在这本书中使用 Java 运行时来实现解决方案(因为我们在这本书中专注于基于 Java 的实现)。

要编写一个对 S3 事件做出反应的 Lambda 函数,我们必须创建一个实现 RequestHandler 接口并接受 S3Event 作为其泛型 Input 类型的 Java 类,如下代码块所示:

import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
. . .
public class Handler implements RequestHandler<S3Event, Integer> {
. . .

在这个类中,我们必须实现 RequestHandler 接口的 handleRequest 方法。在 handleRequest 方法中,我们遍历每个 S3EventNotificationRecord,它表示一个新文件的创建或更新。我们在 S3ObjectNames 中收集附加到该 S3EventNotificationRecord 的所有 S3 对象名称。对于 S3ObjectNames 中存在的每个不同的 S3 对象名称,我们创建并启动一个 AWS 短暂 EMR 集群。以下代码片段显示了 handleRequest 方法的实现:

Set<String> s3ObjectNames = new HashSet<>();
for (S3EventNotificationRecord record:
        s3Event.getRecords()) {
    String s3Key = record.getS3().getObject().getKey();
    String s3Bucket = record.getS3().getBucket().getName();
    s3ObjectNames.add("s3://"+s3Bucket+"/"+s3Key);
}
s3ObjectNames.forEach(inputS3path ->{
    createClusterAndRunJob(inputS3path,logger);
});

现在,让我们看看 createClusterAndRunJob 方法的实现。该方法接受两个参数:inputS3path 和 Lambda 记录器。此方法使用 AWS SDK 创建一个 ElasticMapReduce 对象。此方法使用 StepConfig API 构建一个 spark submit 阶段。然后,它使用所有配置细节以及 SparkSubmitStep 来配置 RunJobFlowRequest 对象。

最后,我们可以通过 ElasticMapReduce 对象的 runJobFlow 方法提交请求以创建和运行 EMR 集群,如下所示:

private void createClusterAndRunJob(String inputS3path, LambdaLogger logger) {
    //Create a EMR object using AWS SDK
    AmazonElasticMapReduce emr = AmazonElasticMapReduceClientBuilder.standard()
            .withRegion("us-east-2")
            .build();

    // create a step to submit spark Job in the EMR cluster to be used by runJobflow request object
    StepFactory stepFactory = new StepFactory();
    HadoopJarStepConfig sparkStepConf = new HadoopJarStepConfig()
            .withJar("command-runner.jar")
            .withArgs("spark-submit","--deploy-mode","cluster","--class","com.scalabledataarchitecture.bigdata.EcomAnalysisDriver","s3://jarandconfigs/EcommerceAnalysis-1.0-SNAPSHOT.jar",inputS3path,"s3://scalabledataarch/output");
    StepConfig sparksubmitStep = new StepConfig()
            .withName("Spark Step")
            .withActionOnFailure("TERMINATE_CLUSTER")
            .withHadoopJarStep(sparkStepConf);
    //Create an application object to be used by runJobflow request object
    Application spark = new Application().withName("Spark");
    //Create a runjobflow request object
    RunJobFlowRequest request = new RunJobFlowRequest()
            .withName("chap5_test_auto")
            .withReleaseLabel("emr-6.4.0")
            .withSteps(sparksubmitStep)
            .withApplications(spark)
            .withLogUri(...)
            .withServiceRole("EMR_DefaultRole")
            ...            ;
    //Create and run a new cluster using runJobFlow method
    RunJobFlowResult result = emr.runJobFlow(request);
    logger.log("The cluster ID is " + result.toString());
}

现在我们已经开发了 Lambda 函数,让我们来部署、运行和测试它:

  1. 为 Lambda 函数创建一个 IAM 安全角色以触发 EMR 集群,如下截图所示:

图 5.17 – 创建新的 IAM 角色

图 5.17 – 创建新的 IAM 角色

  1. 使用 AWS 管理控制台创建一个 Lambda 函数。请提供函数的名称和运行时,如下截图所示:

图 5.18 – 创建 AWS Lambda 函数

图 5.18 – 创建 AWS Lambda 函数

  1. 在创建 Lambda 函数时,请确保您将默认执行角色更改为在步骤 1中创建的 IAM 角色:

图 5.19 – 将 IAM 角色设置到 AWS Lambda 函数

图 5.19 – 将 IAM 角色设置到 AWS Lambda 函数

  1. 现在,您必须为 Lambda 函数添加一个 S3 触发器,如下面的截图所示。请确保您输入了正确的存储桶名称和前缀,您将在此处推送源文件:

图 5.20 – 为 Lambda 函数创建 S3 事件触发器

图 5.20 – 为 Lambda 函数创建 S3 事件触发器

  1. 然后,您必须使用我们的 Maven Java 项目(项目的完整源代码可以在github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter05/sourcecode/S3lambdaTriggerEmr找到)开发的 Lambda 函数来本地构建 JAR 文件。一旦构建了 JAR 文件,您必须使用 | .zip 或 .jar 文件选项上传它,如下面的截图所示:

图 5.21 – 部署 AWS Lambda JAR 文件

图 5.21 – 部署 AWS Lambda JAR 文件

  1. 现在,您可以通过将新的数据文件放置在 S3 触发器中提到的 S3 存储桶中来测试整个工作流程。一旦 Lambda 函数执行,它将创建一个临时的 EMR 集群,其中 Spark 作业将在其中运行。您可以从 AWS 管理控制台监控 Lambda 函数的指标:

图 5.22 – 监控 AWS Lambda 函数

图 5.22 – 监控 AWS Lambda 函数

您可以通过查看临时集群的摘要选项卡中的持久用户界面选项来从 AWS EMR 管理控制台监控 Spark 应用程序,如下面的截图所示:

图 5.23 – EMR 集群管理控制台

图 5.23 – EMR 集群管理控制台

Lambda 函数故障排除

在现实世界中,在调用或执行 Lambda 函数时可能会遇到麻烦。AWS 已发布了一份非常简洁的故障排除指南来解决所有这些问题。有关更多信息,请参阅docs.aws.amazon.com/lambda/latest/dg/lambda-troubleshooting.xhtml

现在,让我们看看我们是否可以通过监控 Spark 作业来进一步优化 Spark 应用程序。

调优 Spark 作业

我们可以检查 Spark UI 来查看其有向无环图DAG)。在我们的案例中,我们的 DAG 看起来是这样的:

图 5.24 – Spark 作业的 DAG

图 5.24 – Spark 作业的 DAG

如我们所见,阶段 5阶段 6 都在进行相同的工作,即扫描和读取 CSV 文件到 DataFrame 中。这是因为我们有一个名为 ecommerceEventDf 的 DataFrame,它被用来派生出两个不同的 DataFrame。由于 Spark 的懒加载技术,这两个派生 DataFrame 分别计算 ecommerceEventDf,这导致性能变慢。我们可以通过持久化 ecommerceEventDf DataFrame 来解决这个问题,如下面的代码片段所示:

ecommerceEventDf.persist(StorageLevel.MEMORY_AND_DISK());

在进行此更改后,新的 DAG 将如下所示:

图 5.25 – Spark 作业的优化 DAG

图 5.25 – Spark 作业的优化 DAG

在新的 DAG 中,InMemoryTableScan 任务中有一个绿色圆点。这个绿色圆点代表 Spark 通过内存持久化数据,这样它就不会两次扫描 CSV 文件,从而节省处理时间。在这个用例中,它将提高 Spark 作业的性能大约 20%。

现在我们已经实施并测试了我们的解决方案,让我们学习如何在输出文件夹上构建 Athena 表并启用结果的简单查询。

使用 AWS Athena 查询 ODL

在本节中,我们将学习如何使用我们的架构在创建的 ODL 上执行数据查询。我们将重点介绍如何在输出文件夹上设置 Athena 以进行简单的数据发现和查询:

  1. 通过 AWS 管理控制台导航到 AWS Athena。点击 探索查询编辑器。首先,转到 查询编辑器 区域的 管理设置 表单,并设置一个可以存储查询结果的 S3 桶。您可以为这个目的创建一个空桶:

图 5.26 – 设置 AWS Athena

图 5.26 – 设置 AWS Athena

  1. 我们将在我们的 S3 输出桶上创建一个 Athena 表。为此,我们将创建一个 DDL 来创建一个名为 ecom_odl 的表,这是一个基于 yearmonth 列的分区表。该表的 DDL 可以在下面的代码片段中看到:

    CREATE EXTERNAL TABLE IF NOT EXISTS ecom_odl(
         category_id bigint,
         product_id bigint,
         tot_sales bigint,
         tot_onlyview bigint,
         sales_rev double,
         rank_by_revenue int,
         rank_by_sales int
    ) PARTITIONED BY (year int, month int) STORED AS parquet LOCATION 's3://scalabledataarch/output/';
    

我们将在 Athena 的 查询编辑器 区域的 查询编辑器 区域运行此 DDL 语句来创建以下屏幕截图所示的表:

图 5.27 – 基于输出数据创建 Athena 表

图 5.27 – 基于输出数据创建 Athena 表

  1. 一旦表创建完成,我们需要添加分区。我们可以通过使用 MSCK REPAIR 命令(类似于 Hive)来完成此操作:

    MSCK REPAIR TABLE ecom_odl
    
  2. 运行前面的命令后,所有分区都将自动从 S3 桶中检测到。现在,您可以在 ecom_odl 表上运行任何查询并获取结果。如下面的屏幕截图所示,我们运行了一个示例查询,以找到 2019 年 10 月每个类别的收入最高的前三个产品:

图 5.28 – 使用 Athena 表查询 ODL

图 5.28 – 使用 Athena 表查询 ODL

通过这样,我们已经成功架构、设计和开发了一个大数据批处理解决方案,并为下游团队创建了一个使用 AWS Athena 查询我们分析数据的接口。现在,让我们总结一下本章所学的内容。

摘要

在本章中,我们学习了如何分析问题,并确定这是一个大数据问题。我们还学习了如何选择一个性能高效、优化且成本效益高的平台和技术。我们学习了如何明智地使用所有这些因素来在云中开发大数据批处理解决方案。然后,我们学习了如何使用 AWS Glue DataBrew 分析、分析并从大数据文件中得出推论。之后,我们学习了如何在 AWS 云中开发、部署和运行一个 Spark Java 应用程序来处理大量数据并将其存储在 ODL 中。我们还讨论了如何用 Java 编写 AWS Lambda 触发函数来自动化 Spark 作业。最后,我们学习了如何通过 AWS Athena 表暴露处理后的 ODL 数据,以便下游系统可以轻松查询和使用 ODL 数据。

现在我们已经学会了如何为不同类型的数据量和需求开发优化且成本效益高的基于批处理的数据处理解决方案,在下一章中,我们将学习如何有效地构建帮助我们实时处理和存储数据的解决方案。

第六章:架构实时处理管道

在上一章中,我们学习了如何为高吞吐量的基于批处理的数据工程问题架构大数据解决方案。然后,我们学习了如何使用 Glue DataBrew 对大数据进行概要分析。最后,我们学习了如何逻辑上选择各种技术,在云中构建基于 Spark 的完整大数据解决方案。

在本章中,我们将讨论如何分析、设计和实现一个实时数据分析解决方案来解决业务问题。我们将学习如何借助分布式消息系统(如 Apache Kafka)实现数据的流式传输和处理,从而实现处理速度和可靠性的提升。在这里,我们将讨论如何编写 Kafka Streams 应用程序来处理和分析流式数据,并使用 Kafka 连接器将实时处理引擎的结果存储到 NoSQL 数据库(如 MongoDB、DynamoDB 或 DocumentDB)中。

在本章结束时,你将了解如何使用 Java 和 Kafka 相关技术构建一个实时流式解决方案来预测贷款申请的风险类别。你还将了解实时数据分析问题的设计和架构。在整个过程中,你将学习如何将事件发布到 Kafka,使用 Kafka Streams 分析数据,并将分析结果实时存储到 MongoDB 中。通过这样做,你将了解如何处理实时数据工程问题并构建有效的流式解决方案。

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

  • 理解和分析流式问题

  • 架构解决方案

  • 实施和验证设计

技术要求

要完成本章,你需要以下内容:

  • 熟悉 Java

  • 在本地系统上安装 Java 1.8 或更高版本、Maven、Apache Kafka 和 PostgreSQL

  • 在云中拥有 MongoDB Atlas 订阅

  • 在本地系统上安装 IntelliJ IDEA Community 或 Ultimate 版本

理解和分析流式问题

到目前为止,我们已经探讨了涉及数据摄取、存储或分析的数据工程问题。然而,在当今竞争激烈的市场环境中,在线 Web 应用和移动应用使得消费者更加苛刻且缺乏耐心。因此,企业必须适应并实时做出决策。在本章中,我们将尝试解决这样一个实时决策问题。

问题陈述

一家名为 XYZ 的金融公司,提供信用卡服务,其信用卡申请是实时工作并使用各种用户界面,如移动和在线网络应用程序。由于客户有多种选择并且不太有耐心,XYZ 想确保信贷贷款官员可以在瞬间或实时决定信用卡批准。为此,需要分析应用程序并为每个申请生成一个信用风险评分。这个风险评分,连同必要的申请参数,将帮助信贷贷款官员快速做出决定。

分析问题

让我们分析给定的问题。首先,让我们从数据的四个维度来分析需求。

首先,我们将尝试回答数据速度是多少?这是这个问题的关键因素。正如问题陈述所示,与我们的先前问题不同,源数据是实时接收的,数据分析也需要实时进行。这类问题非常适合实时流解决方案。

现在,我们需要讨论的下一个维度是体积。然而,由于我们的问题涉及流数据,讨论数据的总体体积是没有意义的。相反,我们应该回答诸如平均每分钟或每小时提交多少应用程序?在高峰时段会怎样?这个体积在未来会增加吗?如果会增加,它可能会增加多少次以及频率如何?等问题。我们应该带着这些问题回到客户那里。通常情况下,如果客户是第一次创建实时管道,这些答案可能并不容易获得。在这种情况下,我们应该要求客户提供最细粒度的平均数据速度信息(在这种情况下,指的是每天、每周或每月提交的申请数量),然后计算每分钟的平均预期体积。此外,为了了解体积的增加,我们可以询问关于一年内销售方面的目标预测,并尝试预测体积增加。

假设客户每天接收一百万个申请,并且他们的目标是未来两年内将销售额增加 50%。考虑到通常的批准率为 50%,我们可以预期申请提交率将增加两倍。这意味着我们可能会在未来每天预期到两百万个申请的体积。

由于我们的解决方案需要是实时的,必须处理超过一百万条记录,并且预计体积在未来可能会增加,因此以下特性对于我们的流解决方案是必不可少的:

  • 应该健壮

  • 应支持解决方案内部各种系统以及外部源/汇之间的异步通信

  • 应确保零数据丢失

  • 应该具有容错性,因为我们正在实时处理数据

  • 应该可扩展

  • 即使数据量增加,也应提供出色的性能

考虑到所有这些因素,我们应该选择一个发布/订阅消息系统,因为这可以确保可伸缩性、容错性、更高的并行性和消息投递保证。分布式消息/流平台,如 Apache Kafka、AWS Kinesis 和 Apache Pulsar,最适合解决我们的问题。

接下来,我们将关注数据的多样性。在一个典型的流平台中,我们以事件的形式接收数据。每个事件通常包含一条记录,尽管有时可能包含多条记录。通常,这些事件以平台无关的数据格式(如 JSON 和 Avro)传输。在我们的用例中,我们将以 JSON 格式接收数据。在实际的生产场景中,数据可能是 Avro 格式。

实时流解决方案面临的一个挑战是数据的真实性。通常,真实性是基于数据可能出现的各种噪声可能性来确定的。然而,真实性的准确分析是在实时项目实施和用真实数据进行测试时发生的。与许多软件工程解决方案一样,实时数据工程解决方案会随着时间的推移而成熟,以处理噪声和异常。

为了简化问题,我们假设输入主题中发布的数据已经是干净的,因此我们不会在我们的当前用例中讨论真实性。然而,在现实世界中,通过输入主题接收到的数据可能包含异常和噪声,这需要我们注意。在这种情况下,我们可以编写一个 Kafka Streams 应用程序来清理和格式化数据,并将其放入处理主题。此外,从输入主题中移动错误记录到错误主题;它们不会被发送到处理主题。然后,数据分析流应用程序从处理主题(仅包含干净数据)中消费数据。

现在我们已经分析了该问题的数据维度,并得出结论,我们需要构建一个实时流管道,我们的下一个问题将是,哪个平台?云还是本地?

为了回答这些问题,让我们看看我们有哪些限制。为了分析流数据,我们必须提取并读取每位客户的信用历史记录。然而,由于客户的信用历史是敏感信息,我们更愿意从本地应用程序中使用这些信息。然而,公司的移动或 Web 后端系统部署在云上。因此,将分析数据存储在云上是有意义的,因为移动或其他 Web 应用程序从云上获取数据比从本地获取数据所需的时间更少。因此,在这种情况下,我们将采用混合方法,其中信用历史数据将存储在本地,数据将在本地进行分析和处理,但结果数据将存储在云上,以便可以轻松地从移动和 Web 后端系统检索。

在本节中,我们分析了数据工程问题,并意识到这是一个实时流处理问题,处理将在本地进行。在下节中,我们将使用这次分析的结果,连接点来设计数据管道和选择正确的技术堆栈。

设计解决方案

为了设计解决方案,让我们总结一下上一节中讨论的分析。以下是我们可以得出的结论:

  • 这是一个实时数据工程问题。

  • 此问题可以使用 Kafka 或 Kinesis 等流平台解决。

  • 每天将发布 100 万条事件,事件量有可能随时间增加。

  • 解决方案应托管在混合平台上,数据处理和分析在本地进行,结果存储在云中以方便检索。

由于我们的流媒体平台是本地部署的,并且可以在本地服务器上维护,因此 Apache Kafka 是一个很好的选择。它支持分布式、容错、健壮和可靠的架构。它可以通过增加分区数量来轻松扩展,并提供至少一次投递保证(这确保了所有事件至少会投递一次,不会发生事件丢失)。

现在,让我们看看我们将如何确定结果和其他信息将如何存储。在本用例中,个人的信用历史具有结构化格式,应存储在本地。关系数据库管理系统(RDBMS)是此类数据存储的绝佳选择。在这里,我们将使用 PostgreSQL,因为 PostgreSQL 是开源的、企业级的、健壮的、可靠的、高性能的(而且因为我们将其用作 RDBMS 选项在第四章ETL 数据加载 - 数据仓库中数据摄入的基于批处理解决方案))。与信用历史不同,应用程序需要由运行在 AWS 上的移动和 Web 后端访问,因此数据存储应在云上。

此外,让我们考虑这些数据将主要被移动和 Web 后端应用程序消费。那么,将数据存储在可以由 Web 和移动后端轻松拉取和使用的文档格式中是否值得?MongoDB Atlas 在 AWS 云上是一个以可扩展方式存储文档的绝佳选择,并且具有按使用付费的模式。我们将使用 AWS 上的 MongoDB Atlas 作为结果数据的汇点。

现在,让我们讨论我们将如何实时处理数据。数据将以事件的形式发送到 Kafka 主题。我们将编写一个流应用程序来处理并将结果事件写入输出主题。生成的记录将包含风险评分。要将数据从 Kafka 导出到任何其他数据存储或数据库,我们可以编写一个消费者应用程序或使用 Kafka Sink 连接器。编写 Kafka 消费者应用程序需要开发和维护工作。然而,如果我们选择使用 Kafka Connect,我们只需配置它即可获得 Kafka 消费者的好处。Kafka Connect 交付更快,维护更简单,并且更健壮,因为所有异常处理和边缘情况都已妥善处理,并有良好的文档记录。因此,我们将使用 Kafka Sink 连接器将输出主题的结果事件保存到 MongoDB Atlas 数据库。

下图描述了解决方案架构:

图 6.1 – 我们实时信用风险分析器的解决方案架构

图 6.1 – 我们实时信用风险分析器的解决方案架构

如前图所示,我们的解决方案架构如下:

  • 一个新的应用程序事件被发布到输入 Kafka 主题

  • Kafka Streams 应用程序(风险计算器应用程序)读取应用程序事件并从信用历史数据库获取申请人的相应信用历史

  • 风险计算器应用程序创建并发送一个包含所有必需参数的 HTTP 请求到风险评分生成器应用程序

  • 风险评分生成器应用程序使用已经训练好的机器学习模型来计算应用程序的风险评分,并将结果返回给风险计算器应用程序

  • 风险计算器应用程序生成丰富的应用程序事件,并将结果事件写入输出主题

  • 配置在输出主题上的 Kafka Sink 连接器负责消费并将数据写入 MongoDB Atlas 云数据库

  • 如果在 Kafka 流处理过程中出现处理错误,将错误消息以及输入事件写入错误数据库

现在我们已经学习了如何为我们的实时数据分析需求构建解决方案,让我们学习如何实现架构。

实施和验证设计

在这种实时实现中的第一步是设置流平台。为了实现我们的架构,我们需要在本地机器上安装 Apache Kafka 并创建必要的主题。

在您的本地机器上设置 Apache Kafka

在本节中,您将学习如何设置 Apache Kafka 集群,运行它,以及创建和列出主题。按照以下步骤操作:

  1. archive.apache.org/dist/kafka/2.8.1/kafka_2.12-2.8.1.tgz下载 Apache Kafka 版本 2.8.1。

  2. 提取kafka_2.12-2.8.1.tgz存档文件。以下命令将帮助您在 Linux 或 macOS 上执行相同的操作:

    $ tar -xzf kafka_2.12-2.8.1.tgz
    $ cd kafka_2.12-2.8.1
    
  3. 导航到 Kafka 安装根目录,并使用以下命令启动 zookeeper:

    $ bin/zookeeper-server-start.sh config/zookeeper.properties
    
  4. 接下来,使用以下命令运行 Kafka 服务器:

    $ bin/kafka-server-start.sh config/server.properties
    
  5. 接下来,使用以下命令创建主题:

    $ bin/kafka-topics.sh --create --topic landingTopic1 --bootstrap-server localhost:9092
    $ bin/kafka-topics.sh --create --topic enrichedTopic1 --bootstrap-server localhost:9092
    

为了简化,我们定义了一个分区,并将副本因子设置为 1。但在实际的生产环境中,副本因子应该是三个或更多。分区的数量基于需要处理的数据量及其速度,以及它们应该以何种最佳速度进行处理。

  1. 我们可以使用以下命令列出在集群中创建的主题:

    $ bin/kafka-topics.sh --describe --bootstrap-server localhost:9092
    

现在我们已经安装了 Apache Kafka 并创建了所需的主题,您可以将注意力集中在在本地机器上安装的 PostgreSQL 实例中创建信用记录表和错误表。这些表的 DDL 和 DML 语句可在github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter06/SQL找到。

参考笔记

如果您是 Kafka 的新手,我建议通过阅读官方 Kafka 文档来学习基础知识:kafka.apache.org/documentation/#gettingStarted。或者,您可以参考由Neha NarkhedeGwen SharipaTodd Palino合著的书籍《Kafka,权威指南》。

在本节中,我们设置 Kafka 流平台和信用记录数据库。在下一节中,我们将学习如何实现 Kafka 流应用程序,以实时处理到达landingTopic1的应用程序事件。

开发 Kafka 流应用程序

在我们实现解决方案之前,让我们探索并理解一些关于 Kafka Streams 的基本概念。Kafka Streams 提供了一个客户端库,用于实时处理和分析数据,并将处理后的结果发送到接收器(最好是输出主题)。

流是一个抽象,表示 Kafka Streams 中无界、持续更新的数据。流处理应用程序是使用 Kafka Streams 库编写的程序,用于处理流中的数据。它使用拓扑定义处理逻辑。Kafka Streams 拓扑是一个由流处理器作为节点和流作为边的图。以下图显示了 Kafka Streams 的一个示例拓扑:

图 6.2 – Kafka Streams 拓扑示例

图 6.2 – Kafka Streams 拓扑示例

如您所见,一个拓扑由流处理器组成——这些节点和边代表流。可以有两种特殊流处理器节点,如下所示:

  • 源处理器:这是一个特殊的流处理节点,它从消费一个或多个 Kafka 主题的消息中产生数据输入流

  • 汇处理器:正如其名所示,汇处理器从上游消耗数据并将其写入汇点或目标主题

在 Kafka 流应用程序中,可以使用低级处理器 API 或使用高级领域特定语言DSL)API 构建拓扑。当一个事件发布到源 Kafka 主题时,拓扑被触发,使用拓扑定义处理该事件,并将处理后的事件发布到汇点主题。一旦拓扑在源事件上成功调用并完成,事件偏移量将被提交。

在我们的用例中,Kafka Streams 应用程序将执行以下操作:

  • 对于接收到的应用程序事件,从信用记录数据库中查找信用历史。

  • 使用从 Kafka 接收的数据和从信用记录数据库中提取的数据创建 ML 请求体

  • 向风险评分生成器应用程序发出 REST 调用

  • 形成最终输出记录

  • 使用汇处理器将最终输出记录发送到汇点主题

首先,我们需要创建一个 Spring Boot Maven 项目并添加所需的 Maven 依赖项。以下 Spring Maven 依赖项应添加到pom.xml文件中,如下所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

除了这些之外,由于我们计划开发 Kafka 流应用程序,我们还需要添加 Kafka 相关的 Maven 依赖项,如下所示:

<!-- Kafka dependencies -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <version>2.6.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka-test</artifactId>
    <version>2.6.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-streams</artifactId>
    <version>3.0.0</version>
</dependency>

首先,让我们编写main类,我们将在这里初始化 Kafka Spring Boot 应用程序。然而,在我们的应用程序中,我们必须排除KafkaAutoConfiguration(因为我们打算使用自己的属性名来表示与 Kafka 相关的字段,而不是 Spring Boot 的默认 Kafka 属性名),如下所示:

@SpringBootApplication(exclude = KafkaAutoConfiguration.class)
@Configuration
public class CreditRiskCalculatorApp {
    public static void main(String[] args) {
        SpringApplication.run(CreditRiskCalculatorApp.class);
    }
. . .
}

在创建main类之后,我们将创建主要的KafkaStreamConfiguration类,其中将定义和实例化所有流化 bean。这是我们使用 Kafka Streams DSL 构建拓扑的地方。此类必须使用以下代码片段中的@EnableKafka@EnableKafkaStreams进行注解:

@Configuration
@EnableKafka
@EnableKafkaStreams
public class KStreamConfiguration {
...

接下来,我们将创建KafkaStreamsConfiguration bean。以下代码片段显示了KafkaStreamsConfiguration bean 的实现:

@Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME)
public KafkaStreamsConfiguration kStreamsConfig(){
    Map<String,Object> props = new HashMap<>();
    props.put(StreamsConfig.APPLICATION_ID_CONFIG,appId);
    props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG,bootstrapServer);
    props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
    props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG,Serdes.String().getClass());
    props.put(StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, LogAndContinueExceptionHandler.class);
    return new KafkaStreamsConfiguration(props);
}

在创建 KafkaStreamsConfiguration 对象时,我们必须传递所有与 Kafka 流相关的属性。在这里,设置 StreamsConfig.APPLICATION_IDStreamsConfig.BOOTSTRAP_SERVERS_CONFIG 是强制性的。在这种情况下,StreamsConfig.APPLICATION_ID 对应于 Kafka Streams 应用程序的消费者组 ID,而 StreamsConfig.BOOTSTRAP_SERVERS_CONFIG 对应于 Kafka 代理地址。没有这些值,无法运行或连接到 Kafka 集群的 Kafka 流或消费者应用程序。Kafka Streams 应用程序可以在具有相同消费者组 ID 的多个消费者之间分配来自消费者组内主题的流量。通过使用相同的 ID 增加运行实例的数量,我们可以获得更多的并行性和更好的吞吐量。然而,将实例数量增加到 Kafka 主题分区数量以上将不会对吞吐量产生任何影响。

现在我们已经创建了 KafkaStreamsConfiguration 对象,让我们创建 KStream。在创建此 KStream 对象时,我们必须定义拓扑。以下代码创建了 KStream 对象:

@Bean
public KStream<String,String> kStream(StreamsBuilder builder){
    KStream<String,String> kStream = builder.stream(inputTopic);
    kStream.transform(()->new RiskCalculateTransformer (jdbcTemplate,restTemplate,mlRequestUrl)).to(outTopic);
    return kStream;
}

Kafka 主题中的每条消息都由一个键和一个值组成。值包含实际的消息,而键有助于在消息发布时确定分区。然而,当我们使用流来消费消息时,我们必须提到我们期望的键和值的类型。在我们的情况下,我们期望键和值都是 String 类型。因此,KStream 对象被创建为一个 KStream<String,String> 的实例。首先,我们必须使用 StreamsBuilder 类创建一个流,它是 Kafka Streams API 的一部分。在我们的用例中,拓扑构建如下:

  1. 首先,使用 StreamsBuilder API,从 inputTopic 创建输入流。

  2. 使用 transform() DSL 函数将转换处理器应用于结果输入流。

  3. 一个名为 RiskCalculatorTransformer 的自定义 Transformer 用于转换/处理来自输入流的 数据。

  4. 处理后的输出事件被写入 outputTopic

现在,让我们学习如何为 Kafka Streams 应用程序编写自定义 Transformer。在我们的场景中,我们创建了 RiskCalculatorTransformer。以下讨论解释了如何开发自定义 Transformer:

首先,我们必须创建一个实现 org.apache.kafka.streams.kstream.Transformer 接口的类。它有三个方法——inittransformclose——需要实现。以下代码显示了 Transformer 接口的定义:

public interface Transformer<K, V, R> {
    void init(ProcessorContext var1);
    R transform(K var1, V var2);
    void close();
}

如您所见,Transformer 接口期望三个泛型类型 – KVRK 指定消息键的数据类型,V 指定消息值的数据类型,R 指定消息结果的数据类型。而 initclose 只在需要在进行消息处理之前进行预处理或后处理时使用,transform 是一个强制方法,它定义了实际的转换或处理逻辑。

在我们的用例中,我们以 JSON 字符串的形式接收消息的值,处理它,添加风险评分,并以 JSON 字符串的形式发送结果值。键的数据类型保持不变。因此,我们发送一个 KeyValue 对象作为结果。我们的最终 Transformer 概述如下:

public class RiskCalculateTransformer implements Transformer<String,String, KeyValue<String,String>> {

    @Override
    public void init(ProcessorContext processorContext) {
     ...
    }
    @Override
    public KeyValue<String, String> transform(String key, String value) {
        ...
    }

    @Override
    public void close() {
     ...
    }
}

如前述代码所示,我们的 Transformer 期望消息的键和值都是 String 类型,并返回一个键值对,其中键和值都是 String 类型。

在我们的 Transformer 中,我们不需要任何预处理或后处理。那么,让我们继续讨论如何实现我们的 Transformertransform 方法。transform 方法的代码如下:

@Override
public KeyValue<String, String> transform(String key, String value) {
    try {
        ApplicationEvent event = mapper.readValue(value,ApplicationEvent.class);
        List<CreditRecord> creditRecord = jdbcTemplate.query(String.format("select months_balance,status from chapter6.creditrecord where id='%s'",event.getId()),new BeanPropertyRowMapper<CreditRecord>(CreditRecord.class));
        MLRequest mlRequest = new MLRequest();
        mlRequest.setAmtIncomeTotal(event.getAmtIncomeTotal());
        ...
        HttpEntity<MLRequest> request = new HttpEntity<>(mlRequest);
        ResponseEntity<RiskScoreResponse> response = restTemplate.exchange(mlRequestUrl, HttpMethod.POST, request, RiskScoreResponse.class);
        if(response.getStatusCode()== HttpStatus.OK){
            EnrichedApplication enrichedApplicationEvent = new EnrichedApplication();
            enrichedApplicationEvent.setApplicationforEnrichedApplication(event);
            enrichedApplicationEvent.setRiskScore(response.getBody().getScore());
            return KeyValue.pair(key,mapper.writeValueAsString(enrichedApplicationEvent));
        }else{
            throw new Exception("Unable to generate risk score.Risk REST response - "+ response.getStatusCode());
        }
    } catch (Exception e) {
        ...
    }
    return null;
}

下面是实现我们的 transform 方法的逐步指南:

  1. 首先,我们使用 Jackson 的 ObjectMapper 类将传入的值(一个 JSON 字符串)反序列化为一个名为 ApplicationEvent 的 POJO。

  2. 然后,我们使用 Spring 的 JdbcTemplate 初始化一个对信用记录数据库的 JDBC 调用。在构建 SQL 时,我们使用之前步骤中反序列化的应用程序 ID。由于 JDBC 调用,我们获得了一个 CreditRecord 对象列表。

  3. 接下来,我们为即将进行的 HTTP REST 调用构建请求体。在这里,我们使用之前反序列化的 ApplicationEvent 对象和之前步骤中获得的 CreditRecord 对象列表填充一个 MLRequest 对象。

  4. 然后,我们将 MLRequest 对象包装在一个 HTTPEntity 对象中,并使用 Spring 的 RestTemplate API 进行 REST 调用。

  5. 我们将 REST 响应反序列化为 RiskScoreResponse 对象。RiskScoreResponse 对象的模型如下所示:

    public class RiskScoreResponse {
        private int score;
        public int getScore() {
            return score;
        }
        public void setScore(int score) {
            this.score = score;
        }
    }
    
  6. 如果 REST 响应是 OK,则使用 ApplicationEventRiskScoreResponse 对象构建 EnrichedApplication 对象。

  7. 最后,我们创建并返回一个新的 KeyValue 对象,其中键保持不变,但值是我们创建的 EnrichedApplication 对象的序列化字符串,该对象是在 步骤 6 中创建的。

  8. 对于异常处理,我们将任何错误记录下来,并将错误事件发送到错误数据库以供未来分析、报告和调整。报告和调整过程在此不涉及,通常由某种批量编程完成。

在本节中,我们学习了如何从头开始开发一个 Kafka Streams 应用程序。然而,我们应该能够成功地对流式应用程序进行单元测试,以确保我们的预期功能运行良好。在下一节中,我们将学习如何对 Kafka Streams 应用程序进行单元测试。

Kafka Streams 应用程序的单元测试

要对 Kafka Streams 应用程序进行单元测试,我们必须将 Kafka Streams 测试实用程序依赖项添加到 pom.xml 文件中:

<!-- test dependencies -->
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-streams-test-utils</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-core</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

此外,在我们进行 JUnit 测试之前,我们需要对我们的代码进行一点重构。我们必须将 KStream bean 的定义拆分为两个方法,如下所示:

@Bean
public KStream<String,String> kStream(StreamsBuilder builder){
    KStream<String, String> kStream = StreamBuilder.INSTANCE.getkStream(builder,inputTopic,outTopic,mlRequestUrl,jdbcTemplate,restTemplate);
    return kStream;
}
...
public enum StreamBuilder {
    INSTANCE;
    public KStream<String, String> getkStream(StreamsBuilder builder, String inputTopic,String outTopic, String mlRequestUrl, JdbcTemplate jdbcTemplate, RestTemplate restTemplate) {
        KStream<String,String> kStream = builder.stream(inputTopic);
        kStream.transform(()->new RiskCalculateTransformer (jdbcTemplate,restTemplate,mlRequestUrl)).to(outTopic);
        return kStream;
    }
}

如前述代码所示,我们将 KStream 形成代码提取出来,放入一个名为 StreamBuilder 的单例类中的工具方法中,并在其上方使用 Bean 方法作为包装器。

现在,让我们学习如何编写 JUnit 测试用例。首先,我们的转换需要 JDBC 调用和 REST 调用。要做到这一点,我们需要模拟 JDBC 调用。为此,我们将使用 Mockito 库。

我们可以这样模拟我们的 JdbcTemplate 调用:

@Mock
JdbcTemplate jdbcTemplate;
...
public void creditRiskStreams(){
   ...
    List<CreditRecord> creditRecords = new ArrayList<>();
    CreditRecord creditRecord = new CreditRecord();
    . . .
    creditRecords.add(creditRecord);
    Mockito. lenient().when(jdbcTemplate.query("select months_balance,status from chapter6.creditrecord where id='5008804'",new BeanPropertyRowMapper<CreditRecord>(CreditRecord.class)))
            .thenReturn(creditRecords);
      ...

首先,我们使用 @Mock 注解创建一个模拟的 JdbcTemplate 对象。然后,我们使用 Mockito 的 when().thenReturn() API 定义一个模拟输出,用于通过模拟的 JdbcTemplate 对象进行的调用。

可以使用类似的技术来模拟 RestTemplate。模拟 RestTemplate 的代码如下:

@Mock
private RestTemplate restTemplate;
public void creditRiskStreams(){
    ...
    RiskScoreResponse riskScoreResponse = new RiskScoreResponse();
    ...
    Mockito
            .when(restTemplate.exchange(Mockito.anyString(), HttpMethod.POST, Mockito.any(), RiskScoreResponse.class))
      .thenReturn(new ResponseEntity(riskScoreResponse, HttpStatus.OK));

如您所见,首先,我们使用 @Mock 注解模拟 RestTemplate。然后,使用 Mockito API 模拟任何返回 RiskScoreResponse 对象的 POST 调用。

现在,让我们构建拓扑。您可以使用以下代码创建拓扑:

@Test
public void creditRiskStreamsTest() throws JsonProcessingException {
    //test input and outputTopic
    String inputTopicName = "testInputTopic";
    String outputTopicName = "testOutputTopic";
    ...
   StreamsBuilder builder = new StreamsBuilder();
StreamBuilder.INSTANCE.getkStream(builder,inputTopicName,outputTopicName,"any url",jdbcTemplate,restTemplate);
Topology testTopology = builder.build();
  ...

在这里,我们创建了一个 org.apache.kafka.streams.StreamsBuilder 类的实例。使用我们的 StreamBuilder 工具类,我们通过调用 getkStream 方法定义了拓扑。最后,我们通过调用 org.apache.kafka.streams.StreamsBuilder 类的 build() 方法构建了拓扑。

Kafka Streams 的测试工具包含一个名为 Utility 的类,称为 TopologyTestDriverTopologyTestDriver 通过传递拓扑和配置详细信息来创建。一旦创建了 TopologyTestDriver,它有助于创建 TestInputTopicTestOutputTopic。以下代码描述了如何实例化 TopologyTestDriver 并创建 TestInputTopicTestOutputTopic

public class CreditRiskCalculatorTests {
    private final Properties config;
public CreditRiskCalculatorTests() {
    config = new Properties();
    config.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, "testApp");
    config.setProperty(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "test:1234");
    config.setProperty(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
    config.setProperty(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
}
. . .
@Test
public void creditRiskStreamsTest() throws JsonProcessingException {
. . .
TopologyTestDriver testDriver = new TopologyTestDriver(testTopology,config);
TestInputTopic<String,String> inputTopic = testDriver.createInputTopic(inputTopicName, Serdes.String().serializer(), Serdes.String().serializer());
TestOutputTopic<String,String> outputTopic = testDriver.createOutputTopic(outputTopicName, Serdes.String().deserializer(), Serdes.String().deserializer());
. . .

要创建一个 TestInputTopic,我们需要指定主题名称,以及键和值序列化器。同样,TestOutputTopic 需要键和值反序列化器,以及输出主题名称。我们可以使用以下代码将测试事件推送到 TestInputTopic

inputTopic.pipeInput(inputPayload);

最后,我们可以使用 org.junit.Assert.assertEquals 静态方法,通过以下方式对我们的预期结果和实际结果进行断言:

assertEquals(mapper.readTree(outputTopic.readValue()), mapper.readTree("{ \"id\": \"5008804\", \"genderCode\": \"M\", \"flagOwnCar\": \"Y\", \"flagOwnRealty\": \"Y\", \"cntChildren\": 0, \"amtIncomeTotal\": 427500.0, \"nameIncomeType\": \"Working\", \"nameEducationType\": \"Higher education\", \"nameFamilyStatus\": \"Civil marriage\", \"nameHousingType\": \"Rented apartment\", \"daysBirth\": -12005, \"daysEmployed\": -4542, \"flagMobil\": 1, \"flagWorkPhone\": 1, \"flagPhone\": 0, \"flagEmail\": 0, \"occupationType\": \"\", \"cntFamMembers\": 2 , \"riskScore\": 3.0}"));

我们可以通过右键单击并运行 Test 类来运行此 JUnit 测试,如下面的截图所示:

图 6.3 – 运行 Kafka Streams JUnit 测试用例

图 6.3 – 运行 Kafka Streams JUnit 测试用例

一旦你运行了 JUnit 测试用例,你将在 IntelliJ IDE 的运行窗口中看到测试结果,如下面的截图所示:

图 6.4 – 验证 JUnit 测试的结果

图 6.4 – 验证 JUnit 测试的结果

在本节中,我们学习了如何为 Kafka 流式应用程序编写 JUnit 测试用例并对我们的 Streams 应用程序进行单元测试。在下一节中,我们将学习如何配置流式应用程序并在我们的本地系统上运行应用程序。

配置和运行应用程序

要运行此应用程序,我们必须配置 application.yaml 文件,其中包含以下详细信息:

  • 应用程序端口号(因为我们将在本地机器上启动两个 Spring Boot 应用程序)

  • 数据源细节

  • Kafka 细节,如引导服务器和主题

  • 风险评分生成器应用程序的 REST HTTP URL

我们的示例 application.yaml 文件将如下所示:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/database
    username: postgres
    driverClassName: org.postgresql.Driver
riskcalc:
  bootstrap-servers: localhost:9092
  appId: groupId1
  inputTopic: landingTopic1
  outTopic: enrichedTopic1
  mlRequestUrl: "http://localhost:8081/riskgenerate/score"

现在,我们可以通过运行 CreditRiskCalculator 应用程序的 main 类 CreditRiskCalculatorApp 来运行应用程序。但在我们启动 CreditRiskCalculator 应用程序之前,我们应该通过运行其 main 类 – 即 RiskScoreGenerator 来运行 RiskScoreGenerator 应用程序。这两个应用程序都是 Spring Boot 应用程序;请参阅 第四章实现和单元测试解决方案 部分,ETL 数据加载 – 数据仓库中数据摄取的基于批处理解决方案,以了解如何运行 Spring Boot 应用程序。

故障排除技巧

如果在启动 CreditRiskCalculator 应用程序时,你在日志中注意到类似于 无法建立到节点 -1 (localhost/127.0.0.1:9092) 的连接。代理可能不可用 的警告消息,请确保你的 Kafka 服务器是可访问的并且正在运行。

如果你注意到一个异常,例如 max.poll.interval.ms 或减少 max.poll.records 的值。这通常发生在检索的记录数量处理时间超过配置的最大检索间隔时间时。

如果你遇到一个错误,例如 java.lang.IllegalArgumentException: 分区已分配

application.id。将你的 application.id 更改以解决这个问题。

在本节中,我们学习了如何创建和单元测试 Kafka Streams 应用程序。该应用程序的源代码可在 GitHub 上找到,地址为 github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter06/sourcecode/CreditRiskCalculator

由于实现基于机器学习的风险分数生成器应用程序超出了本书的范围,我们已创建了一个 Spring Boot REST 应用程序,该应用程序生成介于 1 到 100 之间的虚拟风险分数。此虚拟应用程序的代码库可在 GitHub 上找到,地址为 github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter06/sourcecode/RiskScoreGenerator

在现实世界的场景中,基于机器学习的应用程序更有可能用 Python 编写而不是 Java,因为 Python 对 AI/ML 库的支持更好。然而,Kafka Streams 应用程序将能够发出 REST 调用并从该应用程序获取之前展示的生成风险分数,如图所示。

到目前为止,我们已经从输入 Kafka 主题接收事件,即时处理它,生成风险分数,并将丰富的事件写入输出 Kafka 主题。在下一节中,我们将学习如何将 Kafka 与 MongoDB 集成,并将事件流式传输到 MongoDB,一旦它们在输出 Kafka 主题中发布。

创建 MongoDB Atlas 云实例和数据库

在本节中,我们将使用 MongoDB Atlas 创建一个基于云的实例。按照以下步骤设置 MongoDB 云实例:

  1. 如果您尚未注册,请注册 MongoDB Atlas 账户(www.mongodb.com/atlas/database)。在注册过程中,您将被要求选择所需的订阅类型。对于这个练习,您可以选择共享订阅,它是免费的,并选择 AWS 集群作为您首选的云服务。

  2. 您将看到以下屏幕。在这里,点击构建数据库按钮以创建新的数据库实例:

图 6.5 – MongoDB Atlas 欢迎屏幕

图 6.5 – MongoDB Atlas 欢迎屏幕

  1. 要配置新的数据库,我们将被要求设置用户名和密码,如下面的截图所示:

图 6.6 – 配置新的数据库实例

图 6.6 – 配置新的数据库实例

  1. 然后,我们将被要求输入所有我们希望授予 MongoDB 实例访问权限的 IP 地址。在这里,由于我们将从我们的本地系统运行应用程序,我们将添加我们的本地 IP 地址到 IP 访问列表。然后,点击完成并关闭

图 6.7 – 在数据库配置期间设置 IP 访问列表

图 6.7 – 在数据库配置期间设置 IP 访问列表

  1. 一旦集群创建完成,我们将在仪表板上看到集群,如下面的截图所示。现在,点击浏览集合按钮以查看此数据库实例中的集合和数据:

图 6.8 – MongoDB Atlas 的集群仪表板

图 6.8 – MongoDB Atlas 的集群仪表板

  1. 如以下截图所示,目前没有任何集合或数据。然而,您可以通过点击添加我的数据按钮在此界面使用时手动创建集合或数据:

图 6.9 – 在 MongoDB 数据库实例中探索集合和数据

图 6.9 – 在 MongoDB 数据库实例中探索集合和数据

在本节中,我们学习了如何使用 MongoDB Atlas 的在线界面创建基于云的 MongoDB 实例。在下一节中,我们将学习如何配置和部署我们的 MongoDB Kafka 连接器,以实时将数据从 Kafka 发送到 MongoDB。

配置 Kafka Connect 以将结果存储在 MongoDB 中

Kafka Connect 是一个开源的、可插拔的数据集成框架,用于 Kafka。它使得数据源和数据汇能够轻松地连接到 Kafka。无需编写繁琐的代码来从数据源发布消息或从 Kafka 消费消息写入数据汇,Kafka Connect 提供了声明性配置来连接到数据源或汇。

Kafka Connect 集群已经预装了一些类型的连接器,例如FileSourceConnector。然而,我们可以通过将它们放置在plugins文件夹中来安装任何可用的连接器。对于我们的用例,我们将部署 MongoDB 连接器插件(本章后面将讨论)。

Kafka Connect 实例可以以集群模式或独立模式部署和运行。然而,在生产环境中,它通常以集群模式运行。当我们以集群模式运行时,我们可以使用 Kafka Connect 的 REST API 注册 Kafka 连接器配置。在独立模式下,我们可以在启动 Kafka Connect 实例时注册连接器配置。

由于我们将在本地机器上运行我们的 Kafka 集群,因此我们将以独立模式部署我们的 Kafka Connect 实例进行此实现。但请记住,如果您是为了生产目的而实施,您应该以集群模式运行 Kafka 以及 Kafka Connect(这可以是一个物理集群或虚拟集群,例如虚拟机、AWS ECS 或 Docker 容器)。

首先,让我们设置 Kafka Connect 集群:

  1. 首先,在 Kafka 根安装文件夹下创建一个名为plugins的新文件夹,如图所示:

图 6.10 – 创建插件文件夹

图 6.10 – 创建插件文件夹

  1. 接下来,导航到connect-standalone.properties,它位于<Kafka-root>/config文件夹中。将以下属性添加到connect-standalone.properties文件中:

    .. .
    plugin.path=/<full path of Kafka installation root>/plugins
    
  2. 然后,从www.confluent.io/hub/mongodb/kafka-connect-mongodb下载 MongoDB Kafka 连接器插件。

将下载一个 ZIP 文件。在 Kafka 根安装文件夹下创建的plugin文件夹中复制并解压该 ZIP 文件。此时,文件夹结构应如下所示:

图 6.11 – 部署 mongo-kafka-connect 插件后的 Kafka 文件结构

图 6.11 – 部署 mongo-kafka-connect 插件后的 Kafka 文件结构

现在,让我们学习如何创建和部署 Kafka Connect 工作员配置,以在 Kafka 主题和 MongoDB 溢出之间创建管道。

要编写 Kafka Connect 工作员,我们必须了解 Kafka Connect 支持的各种声明性属性类型。以下图表描述了 Kafka Connect 工作员由哪些类型的组件组成:

图 6.12 – Kafka Connect 组件

图 6.12 – Kafka Connect 组件

Kafka 连接器由三种类型的组件组成。具体如下:

  • 连接器:此接口将 Kafka 与外部数据源连接。它负责实现数据源和连接器与 Kafka 通信所需的外部协议。

  • 转换器:转换器用于序列化和反序列化事件。

  • 转换器:这是一个可选属性。它是一个无状态的函数,用于对数据进行轻微的转换,以便使其适合目标格式。

对于我们的用例,我们不需要转换器,但我们需要设置所有与连接器和转换器相关的属性。以下代码是 Kafka Sink Connect 工作员的代码:

name=mongo-sink
topics=enrichedTopic1
connector.class=com.mongodb.kafka.connect.MongoSinkConnector
tasks.max=1
# converter configs
key.converter=org.apache.kafka.connect.storage.StringConverter
value.converter=org.apache.kafka.connect.json.JsonConverter
key.converter.schemas.enable=false
value.converter.schemas.enable=false
...

如前述配置代码所示,配置了连接器属性,如 connector.class 以及特定于 MongoDB 的其他属性,并设置了转换器属性,如 key.convertervalue.converter。接下来,在连接器配置中,我们定义了所有 MongoDB 连接属性,如下所示:

# Specific global MongoDB Sink Connector configuration
connection.uri=mongodb+srv://username:password@cluster0.ipguv.mongodb.net/CRRD?retryWrites=true&w=majority
database=CRRD
collection=newloanrequest
max.num.retries=1
retries.defer.timeout=5000

现在,我们将在连接器配置中设置 document.idwritemodel.strategy 属性,如下所示:

document.id.strategy=com.mongodb.kafka.connect.sink.processor.id.strategy.PartialValueStrategy
document.id.strategy.partial.value.projection.list=id
document.id.strategy.partial.value.projection.type=AllowList
writemodel.strategy=com.mongodb.kafka.connect.sink.writemodel.strategy.ReplaceOneBusinessKeyStrategy

将这些配置保存到名为 connect-riskcalc-mongodb-sink.properties 的属性文件中,并将其放置在 Kafka Connect 的 config 文件夹中。

现在,我们可以以独立模式运行 Kafka Connect 实例,并使用以下命令启动 mongodb-sink 连接器:

bin/connect-standalone.sh config/connect-standalone.properties connect-riskcalc-mongodb-sink.properties

现在,让我们学习如何排查我们可能遇到的潜在问题。

Kafka Sink 连接器的故障排除

当源或连接器在 Kafka Connect 集群上运行时,你可能会遇到多个问题。以下列表指定了一些常见问题及其解决方法:

  • 如果你遇到以下类似的错误,请检查 JSON 消息是否应该包含模式。如果 JSON 消息不应该包含模式,请确保将 key.converter.schemas.enablevalue.converter.schemas.enable 属性设置为 false

    org.apache.kafka.connect.errors.DataException: JsonConverter with schemas.enable requires "schema" and "payload" fields and may not contain additional fields.
    
  • 如果您遇到类似org.apache.kafka.common.errors.SerializationException: Error deserializing Avro message for id -1的错误,请检查负载是否为 Avro 或 JSON。如果消息是 JSON 负载而不是 Avro,请将连接器中value.converter属性的值更改为org.apache.kafka.connect.json.JsonConverter

  • 在向 MongoDB 写入时,您可能会遇到BulkWriteExceptionsBulkWriteExceptions可能是一个WriteError、一个WriteConcernError或一个WriteSkippedError(由于在有序批量写入中先前的记录失败)。尽管我们无法防止此类错误,但我们可以设置以下参数将拒绝的消息移动到名为dead-letter-queue的错误主题:

    errors.tolerance=all
    errors.deadletterqueue.topic.name=<name of topic to use as dead letter queue>
    errors.deadletterqueue.context.headers.enable=true
    

在本节中,我们成功创建、部署并运行了 MongoDB Kafka Sink 连接器。在下一节中,我们将讨论如何测试端到端解决方案。

验证解决方案

要测试端到端管道,我们必须确保所有服务,如 Kafka、PostgreSQL、Kafka Connect 和 MongoDB 实例,都已启动并运行。

此外,Kafka Streams 应用程序和风险评分生成器 REST 应用程序应该已经启动并运行。我们可以通过运行主 Spring Boot 应用程序类来启动这些应用程序。

要测试应用程序,打开一个新的终端并导航到 Kafka 根安装文件夹。要启动 Kafka 控制台生产者实例,请使用以下命令:

bin/kafka-console-producer.sh --topic landingTopic1 --bootstrap-server localhost:9092

然后,我们可以使用控制台生产者发布输入消息,如下面的截图所示:

图 6.13 – 使用 Kafka 控制台生产者发布消息

图 6.13 – 使用 Kafka 控制台生产者发布消息

一旦我们在输入主题中发布消息,它就会被处理,并将结果写入 MongoDB 实例。您可以在 MongoDB 中这样验证结果:

图 6.14 – 验证结果

图 6.14 – 验证结果

在本节中,我们学习了如何测试实时数据处理问题的端到端解决方案并验证结果。现在,让我们总结本章所学的内容。

摘要

在本章中,我们讨论了如何分析实时数据工程问题,确定流平台,并考虑我们的解决方案必须具备的基本特征以成为有效的实时解决方案。首先,我们学习了如何选择一个混合平台以满足法律需求以及性能和成本效益。

然后,我们学习了如何利用我们的问题分析结论来构建一个稳健、可靠和有效的实时数据工程解决方案。之后,我们学习了如何在本地机器上安装和运行 Apache Kafka 并在那个 Kafka 集群中创建主题。我们还学习了如何开发 Kafka Streams 应用程序来进行流处理并将结果写入输出主题。接着,我们学习了如何对 Kafka Streams 应用程序进行单元测试,以使代码更加稳健且无缺陷。之后,我们学习了如何在 AWS 云上设置 MongoDB Atlas 实例。最后,我们了解了 Kafka Connect 以及如何配置和使用 Kafka MongoDB Sink 连接器,将处理后的事件从输出主题发送到 MongoDB 集群。在这个过程中,我们还学习了如何测试和验证我们所开发的实时数据工程解决方案。

通过这样,我们已经学会了如何为基于批处理和实时数据工程问题开发优化且成本效益高的解决方案。在下一章中,我们将学习在数据摄取或分析问题中常用的一些架构模式。

第七章:核心架构设计模式

在前面的章节中,我们学习了如何使用特定用例来构建批处理和实时处理的数据工程解决方案。然而,我们还没有讨论有关批处理和实时流处理引擎的架构设计模式的多种选择。

在本章中,我们将学习一些常用的数据工程架构模式,用于解决数据工程问题。我们将从学习基于批处理的常见模式和它们常用的场景开始。然后,我们将学习现代数据架构中基于流的各种处理模式以及它们如何帮助解决业务问题。我们还将讨论两个著名的混合数据架构模式。最后,我们将学习在云中常用的一些无服务器数据摄取模式。

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

  • 核心批处理模式

  • 核心流处理模式

  • 混合数据处理模式

  • 数据摄取的无服务器模式

核心批处理模式

在本节中,我们将探讨一些常用的数据工程模式,用于解决批处理问题。尽管实现方式可能有多种变化,但这些模式是通用的,与实现模式所使用的具体技术无关。在接下来的章节中,我们将讨论常用的批处理模式。

阶段性收集-处理-存储模式

阶段性收集-处理-存储模式是批处理中最常见的模式。它也常被称为数据工程中的提取-转换-加载ETL)模式。这种架构模式用于摄取数据并将其存储为信息。以下图展示了这种架构模式:

图 7.1 – 阶段性收集-处理-存储模式

图 7.1 – 阶段性收集-处理-存储模式

我们可以将此模式分解为一系列阶段,如下所示:

  1. 在这种架构模式中,一个或多个数据源被提取并保存在称为原始区域或着陆区的数据存储形式中。着陆区数据通常是原始数据,它包含诸如额外空格、垃圾字符、重要字段缺失等噪声。提取或收集作业负责从原始区域提取和存储数据。用于着陆区的数据存储可以是从文件系统、Hadoop 分布式文件系统HDFS)、S3 存储桶或基于用例和选择的平台来解决问题的某些关系型数据库。

  2. 处理作业从原始区域读取数据,并对数据进行一系列转换,例如数据清洗、数据标准化和数据验证。作业将输出存储在中间处理区域(s)。根据项目和技术的不同,可能有一个或多个转换作业,以及多个中间处理区域。有时,处理作业会从中间数据区域获取相关信息以丰富处理后的数据。在这种情况下,它从中间处理区域或任何外部参考数据库读取数据。最终的中间处理区域包含经过清洗、转换、验证和良好组织的数据。

  3. 检索和加载过程检索转换后的数据并将其加载到排序数据集层。排序数据集层包含以特定格式存储的干净和可用的数据,这种格式可以轻松被下游应用程序用于数据分析、参考等。排序数据集层也通常被称为组织数据层ODL)。关于用于排序数据层的数据库或数据存储的类型没有硬性规定。然而,根据排序数据是否将用于在线事务处理OLTP)或在线分析处理OLAP),选择数据库。通常,这种模式用于摄入和存储用于 OLAP 目的的数据。

此架构模式的作业通常根据预定的计划定期运行,例如每天一次或每周一次,或者每周五和周三晚上 8 点。这种模式的一个优点是它以一系列阶段摄入和处理数据。每个阶段的输出存储在中间处理区域,下一个阶段从上一个阶段的输出中检索数据。这种分阶段架构使得设计松散耦合。通常,在生产中,数据处理作业会失败。在这种情况下,我们不需要重新运行整个摄入管道;相反,我们可以从失败的作业重新启动管道。

现在,让我们看看一个现实世界的用例,这个模式将非常适合。一家健康保险公司每天都会收到大量的保险索赔。为了处理这些索赔并确定保险公司将支付的成本,需要对数据进行清洗、丰富、组织和存储。在这种情况下,可以使用这种架构模式来摄入来自不同来源的各种索赔,例如医疗、牙科和视力索赔;然后,它们可以被提取、转换并加载到 ODL。这种模式的另一个示例实现已在第四章中讨论,ETL 数据加载 – 数据仓库中摄入数据的基于批处理解决方案

常见文件格式处理模式

假设存在一个场景,其中存在多个文件(例如,25 个源文件)用于数据源,并且这些源的结构彼此之间相当不同。现在的问题是,分阶段收集-处理-存储模式能否处理此类用例? 是的,它可以。但是它是否优化了这样做?不是的。问题是,对于所有 25 种不同的源文件,我们需要编写一组单独的转换逻辑来处理并将它们存储到排序数据集中。我们可能需要 25 个单独的数据管道来摄取数据。这不仅增加了开发工作量,也增加了分析和测试工作量。此外,我们可能需要调整所有 25 个数据管道中的所有作业。公共文件格式处理模式非常适合克服此类问题。以下图表描述了公共文件格式处理模式的工作原理:

图 7.2 – 公共文件格式处理模式

图 7.2 – 公共文件格式处理模式

此模式分为以下阶段:

  1. 在这种架构模式中,具有明显不同源文件结构的多个源文件从源存储或发送到目标区域。目标区域可以是文件系统、NAS 挂载、SFTP 位置或 HDFS 位置。

  2. 运行一个公共文件格式转换过程,它将不同的传入源文件转换为称为公共文件格式的统一结构。执行此转换的作业或管道应该是轻量级的。它不应该在这一层进行清理或业务转换。公共文件格式转换过程将其输出写入公共文件格式区域。

  3. 现在所有文件都处于相同的格式或结构中,一组处理和加载作业可以在位于公共文件格式区域的文件上运行。处理和加载过程可以是一个作业或一系列作业,它们将最终组织好的和排序好的数据写入 ODL 或排序数据集层。如果需要,处理和加载作业可能会将其中间结果写入临时存储区域。

现在,让我们看看一个现实世界的场景。一家信用卡公司希望根据客户的购买和消费模式,以及一系列复杂的规则,为其客户生成和提供优惠。然而,交易数据可以从各种来源接收,包括基于网络的支付网关、实体交易网关、如 PayPal 和 Cash App 这样的支付应用、外国支付网关以及各种类似的 app。然而,从所有这些来源接收的交易文件格式各不相同。一个选择是为每个来源创建一组单独的转换映射,并分别应用规则。然而,这将导致大量的开发时间和成本,以及维护挑战。在这种情况下,可以使用常见的文件格式处理模式将来自不同源系统的所有交易文件转换为通用的文件格式。然后,一套规则引擎作业可以处理来自不同来源的交易。

提取-加载-转换模式

在本书之前的部分,我们学习了基于经典 ETL 的模式,其中我们首先提取数据,然后转换和处理数据,最后将其存储在最终数据存储中。然而,随着现代处理能力和云提供的可扩展性,我们已经看到许多 大规模并行处理(MPP)数据库,如 Snowflake、Redshift 和 Google 的 Big Query 变得越来越流行。这些 MPP 数据库使数据摄入的新模式成为可能,即我们首先提取和加载数据到这些 MPP 数据库中,然后处理数据。这种模式通常被称为 提取-加载-转换(ELT)模式或收集-存储-处理模式。这种模式对于构建包含大量数据的高性能数据仓库非常有用。以下图提供了 ELT 模式的概述:

图 7.3 – 提取-加载-转换(ELT)模式

图 7.3 – 提取-加载-转换(ELT)模式

前图描述了 ELT 模式的典型流程。这可以描述如下:

  1. 如前图所示,原始数据被提取并加载到 MPP 数据库中。这些数据存储在 MPP 数据库的暂存区。

  2. 然后,使用 MPP 查询和转换管道,数据被转换成最终的表格集。这些最终表格作为数据仓库公开。出于安全考虑,有时会在表格之上创建视图并作为数据仓库公开。

再次,让我们看看这个模式在行业中的应用示例。随着客户体验的持续提升,企业在满足客户期望所需的数据和利用当前数据管理实践提供的能力之间面临差距。客户 360 涉及构建一个包含组织内与客户相关的所有结构化和非结构化数据的完整且准确的数据库。它是将所有客户数据聚合到单个统一位置,以便可以查询并用于分析以改善客户体验。为了构建客户 360 解决方案,我们可以利用 MPP 数据库的力量来创建一个单一的统一客户 360 数据仓库。以下是在 AWS 上使用 Snowflake 的客户 360 设计示例:

图 7.4 – AWS 上的 ELT 模式示例

图 7.4 – AWS 上的 ELT 模式示例

在这里,来自云存储、事件流和第三方数据源的所有数据都落在 Snowflake(一个 MPP 数据库)的临时区域。然后,使用 Snowflake 管道,数据被清洗、转换和丰富,并存储在最终表中,供组织作为集中式企业数据仓库使用。

压缩模式

数据仓库不仅建立在 MPP 数据库之上。对于大数据需求,很多时候它们是建立在 HDFS 之上,使用 Hive 作为查询引擎。然而,在现代管道中,大量数据由 Kafka 或 Pulsar 等实时处理引擎直接倒入着陆区。尽管使用案例需要我们的处理作业每天运行几次或每天运行一次,但文件是在任何记录到达时落地的。这创造了一个不同的问题。由于前面描述的场景,创建了包含少量记录的过多小文件。HDFS 不是为处理小文件而设计的,尤其是如果它比 HDFS 块大小显著小;例如,128 MB。如果存储的是少量大文件而不是大量小文件,HDFS 的工作效果会更好。

最终,随着小文件的增多,查询性能降低,最终 Hive 无法查询这些记录。为了克服这个问题,通常使用一种模式。这被称为压缩模式。以下图表提供了压缩模式的概述:

图 7.5 – 压缩模式

图 7.5 – 压缩模式

在这种架构模式中,小文件存储在着陆区。一个基于批次的定期作业运行并压缩这些小文件以创建一个大型文件。在此期间,它使用状态和状态存储来存储作业审计信息。它还用于存储可能由后续压缩作业使用的状态信息。

阶段性报告生成模式

我们已经讨论了多个模式,以展示数据是如何作为排序数据集或数据仓库中的数据被摄取和存储的。另一方面,本模式侧重于运行数据分析作业并从ODL或数据仓库生成报告。以下图表显示了该模式的通用架构:

图 7.6 – 阶段性报告生成模式

图 7.6 – 阶段性报告生成模式

阶段性报告生成模式主要由两个阶段和一个辅助步骤组成,具体如下:

  1. 报告生成阶段:各种分析作业在排序数据或组织数据层上运行。这些作业甚至可以在数据仓库中存储的数据上运行。然后,这些作业将分析报告保存到报告数据库中。报告数据库可以是关系数据库、NoSQL 数据库或如 Elasticsearch 这样的搜索引擎。

  2. 摘要生成阶段:摘要报告作业从报告数据库中获取数据,并在汇总数据库中报告摘要数据。汇总数据库通常是关系数据库、数据仓库或搜索引擎。

  3. 使用导出器和连接器,可以可视化报告数据库或汇总数据库中存在的数据,或用于数据科学和分析目的,或者简单地用于提取包含报告的平面文件。

现在,让我们看看一个适合此模式的实际场景。假设一家公司有一个本地数据中心。每天,数据中心中所有服务器和存储、备份存储和网络设备都会生成监控和解决日志。这些数据被摄取并存储在一个包含每日、每周和每月故障和解决详情的数据仓库中。利用这个数据仓库,组织希望生成各种报告,包括各种类型事件的平均 SLA、解决前后的性能或 KPI 比率以及按团队解决事件的速度。

最后,公司希望按周、月和季度生成所有事件的摘要。这个用例非常适合使用此模式。在这个用例中,我们可以生成所有报告并将它们存储在报告数据库中,同时生成摘要报告到汇总数据库。一般报告和摘要报告都可以使用 BI 工具(如 Tableau)通过使用适当的连接器从报告数据库中提取数据来可视化。

在本节中,我们了解了一些流行的批量处理架构模式和几个可以应用的实际场景。在下一节中,我们将介绍一些用于实时流处理的常见模式。

核心流处理模式

在上一节中,我们了解了一些常用的批量处理模式。在本节中,我们将讨论各种流处理模式。让我们开始吧。

出盒模式

随着现代数据工程的进步,单体应用已被一系列协同工作的微服务应用所取代。值得注意的是,微服务通常不会与其他微服务共享数据库。数据库会话提交和跨服务通信应该是原子性和实时的,以避免不一致性和错误。在这里,出盒模式非常有用。以下图表显示了出盒模式的通用架构:

图 7.7 – 出盒模式

图 7.7 – 出盒模式

如我们所见,一个微服务(在此处为服务 1)不仅将事务写入在线读取和写入所需的表(在图中表示为在线表),还将写入一个出盒表,其结构是消息应该发布到消息代理的地方。就像办公桌上曾经存放过发出的信件和文件的物理托盘一样,出盒模式使用出盒表将消息发送到消息代理。一个变更数据捕获CDC)发布者从出盒表区域选择 CDC 事件并将它们发布到我们的消息代理。需要从服务 1 获取数据的下游服务消费这些数据。

悲剧模式

悲剧模式是一种设计模式,用于成功管理和处理跨多个应用程序或服务的分布式事务。在现实世界的场景中,单个业务交易不可能仅通过一个应用程序或后端服务完成。通常,多个应用程序协同工作以完成一个成功的业务交易。然而,我们需要有一种异步、可靠和可扩展的方式来在这些系统之间进行通信。跨越多个服务的每个业务交易都称为悲剧。实现此类交易的模式称为悲剧模式。

为了理解悲剧模式,让我们看看一个电子商务应用程序。以下图表显示了电子商务应用程序中简化订单系统的流程:

图 7.8 – 简化的电子商务订单系统

图 7.8 – 简化的电子商务订单系统

如我们所见,一个订单系统由多个服务组成,每个服务都有其执行的一套功能。本质上,有三个服务:订单服务、信用管理服务和支付服务。为了成功完成订单交易,订单服务接收订单。如果订单成功接收,它将转到信用管理服务,该服务检查信用卡余额并验证卡片。如果信用检查成功,系统将使用支付服务请求支付。如果支付成功,订单将被标记为已接受。如果在任何阶段失败,交易将被终止,订单将被拒绝。

现在,让我们看看在这个情况下如何实现叙事模式。这里通过引入一个消息代理平台来交换它们之间的消息,实现了服务间通信的解耦和异步化。以下图表显示了如何使用叙事模式来实现电子商务应用的订单系统:

图 7.9 – 应用到实现订单系统的叙事模式

图 7.9 – 应用到实现订单系统的叙事模式

在这里,叙事模式应用于放置订单的叙事交易。在这里,订单服务将订单存储在本地数据库中。使用 CDC 发布者将包含订单的消息发布到流平台中的主题 2。发送到主题 2 的数据被信用管理服务消费以执行信用检查功能(在前面的图表中标记为流程*1)。信用检查功能的输出被发送到本地数据库。包含信用检查结果的消息从本地数据库发送到主题 1。订单服务消费并存储输出以供进一步处理。

如果信用检查报告是积极的,则使用 CDC 处理器(在前面的图表中描述为流程3)在主题 3发布一个支付请求事件。在流程3中发布的事件被支付服务捕获并请求支付。支付请求的结果被保存在本地支付数据库中。支付数据库的 CDC 发布者将支付输出生产到主题 4,在前面图表中标记为流程4。使用在主题 4**上共享的信息,订单服务确定订单是已放置还是被拒绝。你可以看到的一个有趣的事情是,叙事模式的每个步骤都遵循前面描述的出箱模式。我们可以说一系列出箱模式以某种方式编织在一起,以创建叙事模式。

编舞模式

这种模式特别适用于每个组件独立参与决策过程以完成一项业务交易的情况。所有这些独立组件都与一个集中的编排器应用程序或系统进行通信。就像在编舞中,编舞模式使得所有独立的舞者可以分别表演并创造出一个精彩同步的表演一样,编排器协调去中心化的决策组件以完成一项业务交易。这就是为什么这种模式被称为编舞模式。以下图表提供了编舞模式的概述:

图 7.10 – 编舞模式

图 7.10 – 编舞模式

如我们所见,来自客户端的事件被流式传输到主题。每个事件都包含一个特定的消息头或消息键。根据消息头值或键值的类型,每个消费应用程序都可以过滤和处理该应用程序所需的消息。一旦处理了事件,它就会生成一个结果事件发送到同一个主题,但具有不同的键或头部值。客户端消费所有结果事件以创建最终输出或决策。

当你有频繁添加、删除或更新应用程序的场景,或者集中编排层存在瓶颈时,这种模式非常有用。

让我们看看一个现实世界的用例,在这个用例中,这种模式可能会派上用场。当客户进行充值时,服务提供商会接收到不同的事件。在单次充值中,客户可以购买不同的套餐,例如充值套餐、数据套餐等。每个套餐都会向事件添加一个消息头。不同的客户端应用程序为每种套餐提供定制化的优惠。这个用例适合编排模式。假设一个事件同时包含充值套餐和数据套餐;这将添加两块头部信息,因此将有两个基于套餐类型消费的应用程序;它们将根据主题生成并发送回客户端的自己的优惠套餐。在这里使用编排模式是有意义的,因为套餐类型是动态的,可以逐年和季节变化。因此,消费应用程序可能会频繁地添加或从生态系统中移除。

命令查询责任分离(CQRS)模式

这是一个非常著名的模式,其中读取责任和写入责任被分离出来。这意味着数据被写入不同的数据存储,并从另一个数据存储中读取。虽然写入数据存储针对快速写入进行了优化,但读取数据存储针对快速数据读取进行了优化。以下图表显示了 CQRS 模式的工作原理:

图 7.11 – CQRS 模式

图 7.11 – CQRS 模式

上述图表描述了 CQRS 模式的工作原理。该模式的流程如下:

  1. 首先,生产者或发布者将事件写入主题。

  2. 使用流应用程序,此记录被流式传输到读取数据库。虽然主题针对快速数据写入进行了优化,但读取数据库针对高性能数据读取进行了优化。这种模式在需要高写入速度和高读取速度的场景中非常有用。

例如,对于像亚马逊这样的大型电子商务网站,在亚马逊促销日,流量会大幅增加。在这种情况下,可能会有大量的写入和搜索。在这种情况下,各种来源,如移动应用、网络门户等,将接受订单并更新库存。此外,卖家和亚马逊代表使用亚马逊大日促销活动管理门户每小时更改优惠和折扣。尽管会有大量的读取和写入,但客户对搜索结果期望有亚秒级的响应时间。这可以通过维护单独的写入和搜索数据库来实现。

因此,这个用例非常适合 CQRS 模式。在这里,当客户搜索时,数据从搜索数据库中检索,而当客户下单或添加到购物车时,它被写入写入数据库。写入数据库中的信息将实时流式传输到搜索数据库,如 Elasticsearch 或 AWS OpenSearch。因此,搜索产品和折扣的用户应该能在几秒钟内得到搜索结果。

奇异榕树模式

奇异榕树模式的名字来源于一种热带榕树,这种榕树在其宿主树周围生长,逐渐勒死宿主树,导致其死亡。这种模式最初由马丁·福勒提出。尽管基本模式可以以不同的方式实现,但流式管道为我们提供了一种本地化的方式来使用这种模式。为了理解这种模式,让我们来看一个例子。

假设有一个由三个模块组成(A、B 和 C)的单体应用。A、B 和 C 读取和写入数据库。最初,架构如下所示:

图 7.12 – 单体应用的初始状态

图 7.12 – 单体应用的初始状态

如我们所见,所有模块都有双向箭头,表示读取和写入都在发生。现在,使用奇异榕树模式,我们可以通过逐步迁移各个模块作为独立的微服务——一次一个——将这个单体遗留应用转换为基于微服务的应用。以下图表显示了模块 A正在从单体应用迁移到微服务模式:

图 7.13 – 模块 A 被微服务 A 取代

图 7.13 – 模块 A 被微服务 A 取代

如我们所见,微服务 A(它已成功取代了模块 A)读取和写入数据到事件流。这个事件流反过来又通过事件源或接收器连接器连接到数据库。慢慢地,单体应用将被勒死,最终的转换架构将如下所示:

图 7.14 – 使用奇异榕树模式迁移所有模块

图 7.14 – 使用奇异榕树模式迁移所有模块

如我们所见,所有模块都已从单体应用迁移到联邦微服务模式,从而使单体应用得以退役。

日志流分析模式

在这个模式中,我们将学习如何使用来自各种应用、Web 门户、后端服务和物联网设备的日志进行分析和监控。以下图展示了用于促进日志分析和监控的典型日志流模式:

图 7.15 – 日志流分析模式

图 7.15 – 日志流分析模式

让我们学习这个模式是如何工作的:

  1. 从前面的图中可以看出,来自各种物联网设备、应用、Web 门户和服务的所有日志事件都流式传输到一个事件流中。

  2. 然后,使用事件接收器连接器,事件被发送到搜索数据库和查询数据库。

搜索数据库可以是搜索引擎,如 Elasticsearch、AWS OpenSearch、Splunk 或 Apache Solr。这个数据库可以支持使用复杂查询模式的快速搜索。它还利用搜索引擎的功能进行可视化和分析。查询数据库可以是 MPP 数据库,如 Redshift 或 Snowflake,或者查询引擎,如 Athena。查询引擎允许用户在 ObjectStores(如 S3 对象)上运行 SQL 查询。

以下图展示了在 AWS 中此类模式的一个示例实现:

图 7.16 – AWS 中的日志分析模式示例

图 7.16 – AWS 中的日志分析模式示例

在这里,来自各种 AWS 服务(如 EC2、ECR、EKS 等)的日志事件通过 Kinesis Firehose 流式传输到一个 Kinesis 主题。使用 Kinesis Analytics 进行转换,并通过 Kinesis Firehose 流式传输到 AWS OpenSearch 进行搜索和分析。另一方面,数据从第一个 Kinesis Firehose 流式传输到 S3。在 S3 对象上创建了 Athena 表。然后 Athena 提供了一个易于使用的查询界面,以便对日志数据进行基于批次的查询分析。

在本节中,我们学习了各种流行的流处理模式,这些模式可以用来解决常见的数据工程问题。我们还查看了一些示例,并了解了何时应该使用这些模式。接下来,我们将研究一些流行的混合模式,这些模式结合了批处理和流处理。这些被称为混合数据处理模式。

混合数据处理模式

在本节中,我们将讨论两种非常著名的模式,它们支持批处理和实时处理。由于这些模式同时支持批处理和流处理,它们被归类为混合模式。让我们看看最流行的混合架构模式。

Lambda 架构

首先,让我们了解 Lambda 架构的需求。在分布式计算中,CAP 定理表明任何分布式数据只能保证数据的三种特性中的两种——即一致性、可用性和分区容错性。然而,Nathan Marz 在 2011 年提出了一种新的模式,使得分布式数据存储中可以同时具备这三种特性。这种模式被称为 Lambda 模式。Lambda 架构由三层组成,如下所示:

  • 批量层:这一层负责批量处理

  • 速度层:这一层负责实时处理

  • 服务层:这一层作为统一的查询服务层,下游应用程序可以在此进行查询

下面的图展示了 Lambda 架构的概述:

图 7.17 – Lambda 架构

图 7.17 – Lambda 架构

在 Lambda 架构中,输入数据或源数据被写入批量层中的主数据存储,以及速度层中的事件流。主数据存储可能是一个关系型数据库或 NoSQL 数据库,或者是一个如 HDFS 的文件系统。在数据存储之上运行批量处理作业,以进行数据处理,并将数据加载到服务层中存在的批量视图中。事件流中写入的事件被流处理作业拾取、处理,并加载到实时视图中(如图 7.17 所示)。可以分别查询批量视图和实时视图,或者同时在这两个视图中查询以查看结果。批量视图主要用于历史数据,而实时视图用于 Delta 数据。

虽然它解决了最终一致性查询的问题,因为查询可以结合实时视图和基于批量的视图中的数据,但它也有一些缺点。其中一个主要的缺点是我们必须维护两个不同的工作流程——一个用于批量层,另一个用于速度层。由于在许多场景中,实现流式应用程序的技术与基于批量的应用程序的技术有很大不同,我们必须维护两个不同的源代码。此外,对批量处理和流处理系统进行调试和监控也成为一个额外的负担。我们将在下一个模式中讨论如何克服这些挑战。

Kappa 架构

Lambda 架构被广泛接受的一个原因是它能够克服 CAP 定理的限制,并使整个行业更多地使用流处理。在 Lambda 架构之前,企业对使用流处理持怀疑态度,因为他们担心在实时处理中丢失消息。然而,这种假设在现代分布式流平台(如 Kafka 和 Pulsar)中并不成立。让我们看看 Kappa 架构如何为 Lambda 架构提供一个更简单的替代方案。以下图表描述了 Kappa 架构:

图 7.18 – Kappa 架构

图 7.18 – Kappa 架构

在 Kappa 架构中,理念不是使用两种不同的流程——一个用于批处理,一个用于流处理。相反,它提出所有处理都应使用流处理引擎来完成。这意味着基于批处理的工作负载和基于流的工作负载都可以由单个管道处理。在这里,输入数据或源数据被写入一个特殊的事件流。这个事件流是一个不可变的追加只事务日志。由于它是一个追加日志,因此具有快速的写入能力。要读取数据,我们可以从之前读取数据停止的位置读取。除了这个最后的读取偏移量之外,它还应支持可重放性,这意味着我们可以从第一条消息开始读取。

由于我们正在讨论分布式计算,这个事务日志将被分区,这将提高读写性能。流处理作业读取事件,处理它们,并将输出写入统一视图(包含批处理和实时数据)。一个可能的问题就是,这种类型的流程如何支持高容量的批处理负载? 巨大的数据量也被发送到事务日志,然后由流处理作业拾取。输出存储在服务层中现有的视图中。为了处理这种高容量的事件流,我们需要通过增加事件流中的分区数量来增加更多的并行性。

此事件流日志应该具有保留功能。此外,消费者应能够使用事件时间或事件偏移量重新播放流。每个事件都有一个偏移量和事件时间戳。可重放功能允许消费者通过将事件偏移量或事件时间戳设置为较旧值来重新读取已检索的数据。

到目前为止,我们已经讨论了常用的基于批处理、实时和混合架构模式。在最后一节,我们将快速浏览一些常见的无服务器模式。

数据摄取的无服务器模式

我们将首先回答问题,什么是无服务器计算? 无服务器计算是一种云执行模型,其中云服务提供商根据需求分配资源,如存储和计算,同时代表客户管理服务器。无服务器计算消除了维护和管理服务器及其相关资源的负担。在这里,无服务器计算的客户不关心作业或应用程序如何以及在哪里运行。他们只关注业务逻辑,并让云服务提供商负责管理运行和执行该代码的资源。以下是一些无服务器计算的示例:

  • AWS Lambda 函数或 Azure 函数:这用于运行任何应用程序或服务

  • AWS Glue:用于运行基于大数据的 ETL 作业

  • AWS Kinesis:这是一个无服务器事件流和数据分析平台

虽然有许多有用的无服务器模式,但在此部分中,我们将讨论两个最相关的模式,这些模式可以帮助我们构建数据工程解决方案。以下是我们将讨论的无服务器模式:

  • 事件驱动触发模式:这是一个在云架构中非常常见的模式。在此模式中,在对象存储(如 S3 存储桶)中创建或更新任何文件时,将触发无服务器函数。以下图表提供了此模式的概述:

图 7.19 – 事件驱动触发模式

图 7.19 – 事件驱动触发模式

在此模式中,对象存储中任何对象的变化,例如创建或删除,都可以触发无服务器函数。此无服务器函数可以直接处理数据或用于触发大数据作业。AWS Lambda 和 Azure Function 等无服务器函数可以设置触发器来触发它们。例如,可以配置 Lambda 函数以从 Bucket1 的任何新创建或更新的对象中触发 S3 触发器。触发的 Lambda 函数可以反过来触发 EMR 作业或无服务器 Glue 作业,这些作业转换并处理必要的数据,并将最终输出写入数据存储。或者,Lambda 函数可以进行一些数据处理并将输出结果存储在最终数据存储中。最终数据存储可以是 SQL 数据库、NoSQL 数据库、MPP 数据库或对象存储,如 AWS S3。

第五章“构建批处理管道”中详细解释了使用此模式及其解决方案的实际场景。

  • 无服务器实时模式:这是一个非常流行的无服务器模式,用于云中的数据摄取。以下图表中可以看到此模式的概述:

图 7.20 – 无服务器实时模式

图 7.20 – 无服务器实时模式

在无服务器实时模式中,事件或数据流以及数据处理都是通过云中的无服务器服务来完成的。来自不同源系统的事件、日志和消息将事件发布到无服务器数据流平台,如 AWS Kinesis。数据流触发一个或一系列无服务器函数,这些函数一个接一个地执行数据处理。一旦数据处理完成,它就被写回到最终数据存储中。最终数据存储可以是 SQL、NoSQL、MPP 数据库、对象存储或搜索引擎。

一个可能使用此模式的实际示例是在信用卡使用的实时欺诈检测系统中。以下图展示了使用此模式在 AWS 中进行欺诈检测的示例解决方案:

图 7.21 – AWS 中无服务器实时模式的示例实现

图 7.21 – AWS 中无服务器实时模式的示例实现

在这里,API 网关将实时信用卡交易直接流式传输到 Kinesis 数据流(一个无服务器数据流平台)。在 Kinesis 数据流中写入的交易事件触发 Lambda 函数对事件进行欺诈和异常检测。Lambda 函数利用 AWS SageMaker,它反过来使用存储在 S3 中的已存储数据科学模型来确定交易中的欺诈和异常。然后,输出传递给 Kinesis 数据火 hose,它捕获 Lambda 函数的结果并将其存储在一个 S3 桶中。这个 S3 桶包含实时结果。我们可以使用像 Amazon QuickSight 这样的服务来可视化结果,并在需要时采取任何行动。

通过这样,我们已经讨论了什么是无服务器计算,并讨论了两种高度使用的无服务器计算数据摄取模式。现在,让我们总结一下本章所学的内容。

摘要

在本章中,我们首先讨论了各种流行的批处理模式。我们涵盖了五种常用的模式来解决批处理问题。我们还研究了这些模式的示例以及在实际场景中使用这些模式的情况。然后,我们探讨了可用于构建流处理管道的五种流行模式以及它们如何用于解决数据工程中的实际问题。接下来,我们学习了 Lambda 和 Kappa 架构以及它们在批处理和流处理中的有用性。最后,我们学习了无服务器架构是什么,并探讨了两种流行的无服务器架构,这些架构用于解决云中许多数据工程问题。

到目前为止,我们已经知道如何实现批处理和流式处理解决方案,并对行业内常用的不同数据工程模式有一个相当的了解。现在,是时候在我们的解决方案中加入一些安全性和数据治理措施了。在下一章中,我们将讨论各种数据治理技术和工具。我们还将涵盖为什么以及如何将数据安全应用于数据工程解决方案。

第八章:启用数据安全和治理

在前面的章节中,我们学习了如何评估需求,以及如何分析和应用各种架构模式来解决实时和基于批处理的问题。我们学习了如何选择最佳的技术栈,并开发、部署和执行所提出的解决方案。我们还讨论了数据摄取的各种流行架构模式。然而,任何关于数据架构的讨论如果没有提及数据治理和数据安全都是不完整的。在本章中,我们将重点关注理解和应用数据层中的数据治理和安全。

在本章中,我们首先将讨论数据治理是什么,以及为什么它如此重要。我们还将简要讨论市场上可用的几个开源数据治理工具。然后,我们将通过向数据摄取管道添加数据治理层来实际演示数据治理的实施。数据摄取管道的 ID 将使用 Apache NiFi 开发,数据治理将通过 DataHub 实现。然后,我们将讨论数据安全的需求以及有助于实现它的解决方案类型。最后,我们将讨论可用于启用数据安全的开源工具。

到本章结束时,您将了解数据治理框架的定义和需求。您还将了解何时需要数据治理,以及关于数据治理框架的所有内容,包括数据治理研究所DGI)的内容。此外,您将了解如何使用 DataHub 实施实际的数据治理。最后,您将了解可用于启用数据安全的各种解决方案和工具。

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

  • 介绍数据治理——是什么以及为什么

  • 使用 DataHub 和 NiFi 的实际数据治理

  • 理解数据安全的需求

  • 可用于数据安全性的解决方案和工具

技术要求

对于本章,您将需要以下内容:

  • 在您本地机器上安装的 OpenJDK 1.11

  • 在您本地机器上安装的 Docker

  • 一个 AWS 账户

  • 在您本地机器上安装的 NiFi 1.12.0

  • 在您本地机器上安装的 DataHub

  • 建议您具备 YAML 的相关知识

本章的代码可以从本书的 GitHub 仓库下载:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter08

介绍数据治理——是什么以及为什么

首先,让我们尝试了解数据治理是什么以及它做什么。用通俗易懂的话说,数据治理是分配适当的权限并就数据相关事务做出决策的过程。根据 DGI 的定义,数据治理被定义为“一个关于信息相关流程的决策权和问责制系统,根据已同意的模型执行,这些模型描述了谁可以在什么情况下使用什么信息采取什么行动,以及何时、在何种情况下、使用什么方法。

如定义所示,它是一种创建明确的策略、政策和规则的做法,这些规则定义了谁可以做出什么决定或执行与数据相关的行动。它还规定了如何做出与数据相关的决策的指南。

数据治理考虑以下方面:

  • 规则

  • 企业级组织

  • 决策权和程序

  • 责任制

  • 监控和控制数据

现在我们已经对数据治理有了基本的了解,让我们来探讨在哪些情况下推荐采用数据治理。

考虑数据治理的时机

在以下任何情况下,都应考虑采用正式的数据治理框架:

  • 组织中的数据量增长如此之大,变得如此复杂,以至于传统的数据管理工具无法解决跨职能的数据需求。

  • 水平聚焦的业务单元和团队需要由其他聚焦团队生成或维护的跨职能数据。在这种情况下,需要企业级的数据可用性和管理解决方案(而不是孤岛式),例如,对于维萨(Visa)的 B2B 销售,市场营销和会计部门在孤岛中维护数据。然而,销售部门需要这两个孤岛的数据来创建准确的销售预测。因此,这些不同部门之间的数据应存储在企业级的中央存储库中,而不是孤岛中。因此,这个企业级的数据需要数据治理以确保适当的访问、使用和管理。

  • 合规性、法律和合同义务也可能要求正式的数据治理。例如,健康保险可携带性和问责法案(HIPAA)强制要求所有受保护的健康信息(PHI)数据应得到良好的治理并受到盗窃或未经授权访问的保护。

到目前为止,我们已经讨论了数据治理是什么以及何时应该有一个正式的数据治理框架。现在我们将学习关于 DGI 数据治理框架的内容。

DGI 数据治理框架

DGI 数据治理框架是在组织内实施适当数据治理的逻辑结构,以使做出更好的数据相关决策成为可能。以下图表展示了这个数据治理框架:

图 8.1 – 数据治理框架

图 8.1 – 数据治理框架

在本节中,我们将讨论 DGI 数据治理框架的各个组成部分。让我们看看前面图表中突出显示的每个组件:

  1. 使命:DGI 框架的使命可以归因于三个主要责任:

    1. 定义规则

    2. 通过为数据利益相关者提供持续的保护和服务来执行和实施规则

    3. 处理因不遵守规则而出现的场景

这种使命形式与政治治理非常相似。在政治治理模型中,政府制定规则,行政部门执行规则,司法部门处理不遵守规则或违反规则的人。就像政治治理一样,在这里,一组数据利益相关者定义规则。另一组利益相关者确保规则得到遵守。最后,第三组利益相关者做出与不遵守规则相关的决策。组织可以选择在使命周围制定一个愿景声明,这可能被用来激发数据利益相关者展望可能性并设定数据相关目标。

  1. 重点领域:使命和愿景引导我们到主要关注领域。我们有两个主要关注领域。具体如下:

    • 目标应该是SMART – 即具体、可衡量、可操作、相关和及时。在决定我们想要追求的目标时,我们应该记住四个 P 的原则 – 即项目、项目、专业学科和人员。我们必须问这些努力如何从收入、成本、复杂性和确保生存(即安全、合规性和隐私)的角度帮助我们的组织。

    • 指标也应该 SMART。数据治理中的每个人都应该知道如何量化并衡量成功。

这次讨论引出了一个问题:我们可以在哪里为我们的数据治理计划提供资金。为了做到这一点,我们必须提出以下问题:

  • 我们如何为数据治理办公室提供资金?

  • 我们如何为数据架构师/分析师提供资金以定义规则和数据?

  • 我们如何为数据治理活动提供资金?

在规划正式数据治理时,重点领域非常重要。

  1. 数据规则和定义:在这个组件中,围绕数据设定了规则、政策、标准和合规性。典型活动可能包括创建新规则、探索现有规则以及解决差距和重叠。

  2. 决策权:这个组件提出了问题,谁可以在何时以及使用什么流程做出与数据相关的决策? 数据治理计划允许我们将决策权作为与数据相关的决策的元数据存储。

  3. 问责制:这个组件在做出决策后,为规则的执行或决策创造了问责制。数据治理计划可能需要将问责制整合到日常的软件开发生命周期SDLC)中。

  4. 控制措施:数据是新的黄金,如果发生敏感数据泄露,将涉及巨大的安全风险。我们如何确保这些风险得到缓解和处理?它们可以通过控制措施来处理。控制措施可以是预防性的或反应性的。数据治理团队通常负责在不同控制层(网络/操作系统/数据库/应用程序)上制定创建这些控制措施的建议。有时,数据治理团队还被要求修改现有的控制措施,以确保更好的数据治理。

  5. 数据利益相关者:数据利益相关者是那些可能影响或受数据影响的人或团体。由于他们与数据有直接关联,因此在做出数据决策时会咨询他们。同样,根据情景,他们可能希望参与某些与数据相关的决策,在决策最终确定之前应进行咨询,或者在决策做出后应被告知。

  6. 数据治理办公室:它促进、支持和运行数据治理计划以及数据管理员活动。它是中央治理机构,通常由数据架构师、数据分析师以及那些从事元数据创建工作的人组成。它收集并协调来自组织不同利益相关者的政策、规则和标准,并提出组织层面的规则和标准。它负责提供中央数据治理相关的沟通。它还负责收集数据治理指标,并向所有数据利益相关者发布报告和成功措施。

  7. 数据管理员:他们是数据治理委员会的一部分,负责做出与数据相关的决策,例如制定政策、指定标准或向 DGO 提供建议。根据组织的规模和复杂性,数据治理委员会(们)可能有一个等级结构。在以数据质量为重点的治理项目中,可能有一个可选的数据质量管理员。

  8. 数据处理:这些是用于管理数据的手段。这些过程应该被记录、标准化和可重复。它们旨在遵循数据管理、隐私和安全方面的监管和合规要求。

所有这些组件协同工作,形成一个反馈循环,确保数据治理持续改进并保持最新。

在本节中,我们了解了数据治理是什么,何时应该实施,以及 DGI 框架。在下一节中,我们将提供实施数据治理的逐步指南。

使用 DataHub 和 NiFi 进行实际数据治理

在本节中,我们将讨论一个名为 DataHub 的工具,以及不同的数据利益相关者和管理员如何利用它来实现更好的数据治理。但首先,我们将了解用例以及我们试图实现的目标。

在本节中,我们将围绕数据摄取管道构建数据治理能力。此数据摄取管道将从 S3 位置获取任何新对象,对其进行丰富,并将数据存储在 MySQL 表中。在这个特定的用例中,我们从移动或网络等不同来源的 S3 桶中获取电话充值或充值(套餐)事件。我们使用 Apache NiFi 管道丰富这些数据并将其存储在 MySQL 数据库中。

Apache NiFi 是一个强大且可靠的拖放式可视化工具,它允许您轻松处理和分发数据。它创建有向图以创建工作流程或数据管道。它由以下高级组件组成,以便您创建可靠的数据路由和转换能力:

  • FlowFile: 在 NiFi 管道中,每个数据记录都被序列化并作为 FlowFile 对象进行处理。FlowFile 对象由表示数据内容和元数据的 flowfile 内容和属性组成。

  • 处理器: 这是 NiFi 的基本单元之一。该组件主要负责处理数据。它以 FlowFile 作为输入,对其进行处理,并生成一个新的 FlowFile。大约有 300 个内置处理器。NiFi 允许您使用 Java 开发和部署额外的自定义处理器。

  • 队列: NiFi 遵循分阶段事件驱动架构。这意味着处理器之间的通信是异步的。因此,需要一个消息总线来保存一个处理器生成的 FlowFile,直到它被下一个处理器取走。这个消息总线被称为 NiFi 队列。此外,它支持设置背压阈值、负载均衡和优先级策略。

  • 控制器服务: 这允许您在 JVM 中干净且一致地共享功能状态。它负责创建和维护数据库连接池或分布式缓存等任务。

  • 处理器组: 当数据流变得复杂时,将一组组件(如处理器和队列)组合成一个称为处理器组的封装更有意义。一个复杂的管道可能连接多个处理器组。每个处理器组内部都有一个数据流。

  • 端口: 可以使用端口连接处理器组。要从外部处理器组获取输入,使用输入端口。要将处理后的 FlowFile 从处理器组发送出去,使用输出端口。

现在,让我们为我们的用例构建 NiFi 管道。我们的源是一个名为chapter8input的 S3 桶,而我们的输出是一个 MySQL 集群。我们将从不同的来源接收chapter8input文件夹中的 S3 对象。每个 S3 对象将以 JSON 格式存在,并包含一个电话充值或充值(套餐)事件。我们的数据汇是一个名为bundle_events的 MySQL 表。该表的数据定义语言DDL)如下:

CREATE TABLE `dbmaster`.`bundle_events` (
  `customerid` INT NOT NULL,
  `bundleid` INT NOT NULL,
  `timestamp` VARCHAR(45) NOT NULL,
  `source` VARCHAR(45) NULL,
  PRIMARY KEY (`customerid`, `bundleid`, `timestamp`));

现在,NiFi 管道会轮询 S3 存储桶并检查任何更改事件,例如创建或更新 JSON 文件。一旦文件上传到 S3 存储桶,NiFi 应该获取该文件,使用 S3 对象名称丰富其源类型,然后将丰富后的数据写入 MySQL 的bundle_events表。

如果您系统中没有安装 Apache NiFi,请下载并安装 Apache NiFi-1.12.0。您可以在github.com/apache/nifi/blob/rel/nifi-1.12.0/nifi-docs/src/main/asciidoc/getting-started.adoc#downloading-and-installing-nifi下载、安装并遵循启动说明进行安装。或者,您可以在 AWS EC2 实例上启动一个 NiFi 集群/节点。

创建 NiFi 管道

接下来,我们将讨论如何构建 NiFi 管道。我们将构建的 NiFi 管道如下截图所示:

图 8.2 – 从 S3 读取数据并写入 MySQL 的 NiFi 管道

图 8.2 – 从 S3 读取数据并写入 MySQL 的 NiFi 管道

让我们尝试理解整个 NiFi 管道以及我们是如何构建它的:

  1. 首先,我们使用ListS3处理器来捕获配置的 S3 存储桶中是否有任何 S3 对象被插入或更新。它列出了所有更改事件。

  2. 然后,使用SplitRecord处理器将记录拆分为单个事件。

  3. 使用 FlowFile 的sourceKey属性。

  4. 然后,我们使用FetchS3Object来获取 S3 对象。FetchS3Object处理器负责读取实际的 S3 对象。如果FetchS3Object成功读取文件,它将 S3 对象内容作为 FlowFile 发送到JoltTransformRecord处理器。

  5. JoltTransformRecord用于在丰富数据被PutDatabaseRecord处理器写入 MySQL 之前丰富数据。

  6. PutDatabaseRecord处理器的成功状态被发送到LogSuccessMessage处理器。如图中所示,所有在失败场景中的 FlowFiles 都被发送到LogErrorMessage处理器。

现在,让我们配置 NiFi 管道中存在的每个 NiFi 处理器(如图 8.2 所示):

  • ListS3 处理器:以下截图显示了ListS3处理器的配置:

图 8.3 – 配置 ListS3 处理器

图 8.3 – 配置 ListS3 处理器

如我们所见,ListS3处理器被配置为轮询并监听chapter8input S3 存储桶中的更改。除了存储桶名称外,我们还必须为 NiFi 实例配置区域访问密钥 ID秘密访问密钥详细信息,以便连接到 AWS S3 存储桶。最后,我们已配置记录编写器属性,将其设置为JsonRecordSetWriter类型的控制器服务。

  • SplitRecord 处理器:接下来,我们将配置SplitRecord处理器,如下截图所示:

图 8.4 – 配置 SplitRecord 处理器

图 8.4 – 配置 SplitRecord 处理器

SplitRecord负责将包含多个写入事件的单个 FlowFile 在 S3 中分割成单独的事件。现在,每个事件都是单个S3Object的元数据。

  • EvaluateJsonPath 处理器:我们使用EvaluateJsonPath处理器从 FlowFile 内容中提取key列的值,并将其作为属性添加到 FlowFile 属性中。EvaluateJsonPath处理器的配置如下截图所示:

图 8.5 – 配置 EvaluateJsonPath 处理器

图 8.5 – 配置 EvaluateJsonPath 处理器

在这里,我们已配置了flowfile-attribute值。这表示将向 FlowFile 属性中添加一个新的键值对。要添加的属性应提供为动态属性,其中属性名称将是属性键,属性值将是属性值。在这里,我们添加了一个名为$.key的属性。此表达式从 FlowFile 内容(这是一个 JSON)中获取key字段的值。

  • FetchS3Object 处理器:以下截图显示了FetchS3Object处理器的配置:

图 8.6 – 配置 FetchS3Object 处理器

图 8.6 – 配置 FetchS3Object 处理器

如我们所见,${sourcekey}是一个 NiFi 表达式,用于获取sourcekey属性值。除此之外,还需要在此处理器中设置与 S3 相关的属性,如BucketAccess Key IDSecret Access Key。此处理器的输出是我们S3Object的内容(以 JSON 格式)。

  • JoltTransformRecord 处理器JoltTransform处理器用于向 JSON 中添加新的键值对。JoltTransformRecord处理器的配置如下:

图 8.7 – 配置 JoltTransformRecord 处理器

图 8.7 – 配置 JoltTransformRecord 处理器

shiftchaindefault。有关 Apache Jolt 的更多信息,请访问github.com/bazaarvoice/jolt#jolt

在这里,我们将讨论我们用于向 JSON FlowFile 内容添加键值对的 Jolt 转换。我们使用Jolt 规范属性添加source键,如下所示:

图 8.8 – 使用 Jolt 规范属性添加源键

图 8.8 – 使用 Jolt 规范属性添加源键

如您所见,通过在 source 键中将 operation 设置为 default 并使用 NiFi 表达式语言计算出的动态值。${sourcekey:substringBefore('_')} 是一个 NiFi 表达式。此表达式返回 FlowFile 属性 '_' 的子字符串)。

  • PutDatabaseRecord 处理器:一旦此 JSON 被添加的新键值对丰富,记录就会使用 PutDatabaseRecord 处理器写入 MySQL 数据库。以下屏幕截图显示了 PutDatabaseRecord 处理器的配置:

图 8.9 – 配置 PutDatabaseRecord 处理器

图 8.9 – 配置 PutDatabaseRecord 处理器

如此配置所示,我们需要配置两个控制器服务 – MySQLINSERTbundle_events。现在,让我们看看名为 DBCPConnectionPool 的服务(称为 MysqlDBCPConnectionPool)是如何配置的。以下屏幕截图显示了 MysqlDBCPConnectionPool 服务的配置:

图 8.10 – 配置 DBCPConnectionPool 服务

图 8.10 – 配置 DBCPConnectionPool 服务

MysqlDBCPConnectionPool 控制器服务用于创建 JDBC 连接池。为此,您必须配置 数据库连接 URL(JDBC URL)、数据库驱动类名称数据库驱动位置 属性,如前一个屏幕截图所示。

通过这样,我们已经构建了整个 NiFi 管道,用于从 S3 提取数据,对其进行丰富,并将其写入 MySQL 表。您可以通过访问 github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/blob/main/Chapter08/sourcecode/nifi_s3ToMysql.xml 来查看整个 NiFi 管道。

设置 DataHub

现在,我们将向我们的解决方案添加一个数据治理层。尽管有许多数据治理工具可用,但其中大多数是付费工具。云中有几种按需付费的数据治理工具,例如 AWS Glue 目录。然而,由于我们的解决方案将在本地运行,我们将选择一个平台无关的开源工具。LinkedIn 开发的 DataHub 就是这样的开源工具之一,它附带了一套相当不错的功能。我们将在此章中使用它来实际解释数据治理。在本节中,我们将学习如何配置 DataHub。在此管道中,S3 是源,MySQL 是目标,NiFi 是处理引擎。要创建数据治理,我们需要连接到所有这些系统并从中提取元数据。DataHub 为我们完成这项工作。

在我们将这些组件(数据管道的组件)连接到 DataHub 之前,我们需要在我们的本地 Docker 环境中下载并安装 DataHub。详细的安装说明可以在 datahubproject.io/docs/quickstart 找到。

在下一节中,我们将学习如何将不同的数据源连接到 DataHub。

向 DataHub 添加摄取源

要将数据源或管道组件连接到 DataHub,我们必须从右上角的菜单栏转到摄取标签,如图所示:

图 8.11 – 在 DataHub 中连接新的元数据源

图 8.11 – 在 DataHub 中连接新的元数据源

然后,我们必须点击创建新源按钮来创建到数据源的新连接。点击此按钮后,我们会得到以下弹出窗口:

图 8.12 – 选择要创建新连接的数据源类型

图 8.12 – 选择要创建新连接的数据源类型

此弹出窗口是一个多页向导,您可以在其中创建新的数据源连接。首先,如图所示,您被要求选择数据源类型。例如,如果数据存储在 MySQL 数据库中,那么我们会从向导中提供的各种缩略图中选择 MySQL。对于未列出的源,例如 NiFi 和 S3,我们可以选择自定义缩略图。一旦选择了类型,点击下一步;您将被带到第二个步骤,称为配置食谱,如图所示:

图 8.13 – 配置数据源的食谱

图 8.13 – 配置数据源的食谱

在提供的空间中的YAML代码中。无论数据源如何,YAML 都有两个顶级元素——sourcesink。同样,sourcesink都包含两个元素——typeconfig。在这里,type表示源或目的地的类型。例如,在上面的截图中,我们正在配置 NiFi,因此源类型是nifi。如果我们上一步选择了 MySQL 作为连接类型,那么在此步骤中源的类型将是mysql

目前在 DataHub 中主要有三种类型的目的地,如下所示:

  • stdout

  • DataHub:元数据通过 GMS REST API 发送到 DataHub

  • 文件:元数据被发送到配置的文件

在这里,我们将使用 DataHub 作为目的地,因为这允许我们利用 DataHub 的功能进行治理和监控。在这种情况下,我们的目的地类型将是datahub-rest。我们还需要指定 GMS REST API 的 HTTP 地址作为基本路径。由于我们的 DataHub 安装使用 Docker,我们将使用localhost:9002作为服务器 IP 地址(如图8.13所示)。

一切添加完毕后,点击下一步进入执行计划步骤,如图所示:

图 8.14 – 创建执行计划

图 8.14 – 创建执行计划

在此步骤中,我们为 DataHub 设置执行计划以从源获取元数据。在此情况下,我们配置了一个 CRON 表达式,0,30 * * * *,这意味着它将每 30 分钟运行一次。

点击下一步进入最后一步,如图下所示截图:

图 8.15 – 新建数据源向导的最后一步

图 8.15 – 新建数据源向导的最后一步

在向导的最后一步,我们需要输入数据源名称。这个名称可以是任何有助于唯一识别该源的任意名称。最后,我们必须点击完成来添加新的数据源。以下截图显示了添加的源:

图 8.16 – 快速查看添加的源

图 8.16 – 快速查看添加的源

如前述截图所示,我们可以监控作业运行次数以从每个源获取元数据,以及最后一次运行是否成功。我们用于用例的所有 YAML 文件都可以在 GitHub 上找到,网址为github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter08

在下一节中,我们将讨论如何使用此工具执行不同的数据治理任务。

治理活动

如前所述章节讨论,一旦数据源或管道与 DataHub 连接,它将提供许多工具来支持围绕这些源的数据治理模型。以下是我们将在本章中探讨的一些功能:

  • 添加域:在 DataHub 中,域可以是任何逻辑分组;它可能是特定于组织的。这有助于我们分析按域的资源利用和其他统计数据。例如,组织可以使用业务单元名称作为域。在我们的特定场景中,DataServices是组织中的一个业务单元,我们创建了一个以业务单元命名的域。要创建新域,我们可以导航到管理 | 并点击新建域按钮。一旦点击此按钮,将打开一个弹出对话框,如图下所示:

图 8.17 – 创建新域

图 8.17 – 创建新域

如我们所见,我们必须为域提供一个名称和描述。不能创建两个具有相同名称的域。

  • 添加用户组:我们可以通过进入设置并选择选项卡来管理用户和组。点击创建组按钮后,将出现一个类似于以下对话框:

图 8.18 – 添加新的用户组

图 8.18 – 添加新的用户组

如我们所见,我们必须提供一个组名和可选的描述。用户组有助于使任何用户成为组的一部分,并将责任、所有权、访问策略和规则分配给组。

  • 探索元数据:在数据治理中,这项活动属于数据定义。大多数数据治理工具支持元数据管理。在此,如图下所示,DataHub 仪表板提供了元数据的摘要:

图 8.19 – 平台和域资源概览

图 8.19 – 平台和域资源概览

前面的截图显示了每个平台上存在的各种平台和对象。它还显示了每个域中有多少资源。对于企业数据治理,监控和理解不同业务单元用于审计、跟踪和财务目的的资源数量非常重要。下面的截图显示了一个 MySQL 表资源和相应的数据定义:

图 8.20 – 向元数据添加描述

图 8.20 – 向元数据添加描述

如我们所见,我们可以向元数据添加描述。另一方面,下面的截图显示了如何在 DataHub 中加载和查看表的架构:

图 8.21 – 表的架构

图 8.21 – 表的架构

如我们所见,描述、术语和标签可以添加并维护在模式的每一列中。这为数据治理的数据定义活动提供了可能。

  • 探索血缘:DataHub 允许我们探索数据血缘。在数据治理的数据定义中,除了维护元数据和描述外,适当的治理还必须了解数据的来源以及它的使用方式。这一方面由血缘所涵盖,如下面的截图所示:

图 8.22 – 数据血缘

图 8.22 – 数据血缘

在这里,我们可以看到1 个下游chapter8input S3 存储桶没有上游事件。我们可以点击可视化血缘按钮来以图表的形式查看血缘,如下面的截图所示:

图 8.23 – 将数据血缘可视化成工作流

图 8.23 – 将数据血缘可视化成工作流

除了可视化血缘之外,您还可以探索此资源的影響分析。这种影响分析有助于在发生更改或维护事件时与受影响的群体进行沟通。

  • 通过添加域和所有者到资源来建立问责制:我们可以从资源的着陆页开始,通过将域和一个或多个所有者附加到资源上来创建资源的问责制,如下面的截图所示:

图 8.24 – 分配域和所有者

图 8.24 – 分配域和所有者

所有者可以是组或用户。然而,建议将所有权分配给组,并将用户添加到该组中。在添加所有者时,您可以选择所有权的类型,如下面的截图所示:

图 8.25 – 添加所有者

图 8.25 – 添加所有者

在这里,当我们添加offer_analytics所有者时,我们将所有者类型设置为技术所有者。所有者可以是三种类型之一 – 技术所有者业务所有者数据管理员技术所有者负责生成、维护和分发资产。业务所有者是与资产相关的领域专家。最后,数据管理员负责资产的管理。

  • 设置策略:DataHub 允许我们设置策略。策略是一组规则,用于定义对资产或 DataHub 平台的权限。策略分为两类 – 平台元数据平台策略允许我们将 DataHub 平台级别的权限分配给用户或组,而元数据策略允许我们将元数据权限分配给用户或组。要创建新策略,请转到 设置 | 权限。点击 创建新策略按钮后,将出现一个向导,如下截图所示:

图 8.26 – 创建 View_Analytics 策略

图 8.26 – 创建 View_Analytics 策略

在这里,我们正在创建一个名为 View_Analytics 的新策略。我们已选择 平台 作为策略类型。可选地,我们可以添加策略的描述。在这里,我们添加了一个描述,说明 此策略仅用于查看分析

点击下一步进入配置权限部分,如下截图所示:

图 8.27 – 配置策略权限

图 8.27 – 配置策略权限

在这里,我们正在配置我们定义的策略的平台权限。最后,我们必须选择/指定此策略将应用到的用户或组。以下截图显示了如何将用户或组分配给策略:

图 8.28 – 将用户/组分配给策略

图 8.28 – 将用户/组分配给策略

如我们所见,View_Analytics策略已分配给executives组。

  • 可视化分析:DataHub 还允许用户创建和保存用于组织数据治理所需分析的仪表板。以下截图显示了各种指标,例如最常查看的数据集和数据治理完整性(文档/血缘/模式的定义程度):

图 8.29 – 各种可视化分析指标

图 8.29 – 各种可视化分析指标

以下截图显示了其他仪表板图表,其中我们可以找到按领域使用的平台及其计数。我们还可以看到每个平台上的实体数量(数据集或管道组件):

图 8.30 – 数据景观摘要

图 8.30 – 数据景观摘要

现在我们已经讨论并学习了数据治理的概念,并在一个用例中实际实施了数据治理,让我们来了解数据安全。

理解数据安全的需求

在我们理解数据安全的需求之前,让我们尝试定义一下什么是数据安全。数据安全是保护企业数据并防止数据因恶意或未经授权的访问而丢失的过程。数据安全包括以下任务:

  • 保护敏感数据免受攻击。

  • 保护数据和应用程序免受任何勒索软件攻击。

  • 防御任何可能删除、修改或破坏企业数据的攻击。

  • 在组织内部允许必要的用户访问和控制数据。再次强调,根据角色及其用途,数据提供只读、写入和删除访问权限。

一些行业可能对数据安全有严格的要求。例如,一家美国健康保险公司需要确保根据 HIPAA 标准,PHI 数据得到极好的保护。另一个例子是,像美国银行这样的金融机构必须确保卡和账户数据极其安全,因为这可能导致直接的货币损失。但即使在没有严格的数据安全要求的情况下,数据安全对于防止数据丢失和防止组织客户对组织的信任损失也是至关重要的。

尽管数据隐私和数据安全是重叠的术语,但了解两者之间的细微差别是有帮助的。数据隐私确保只有经过身份验证的用户才能访问数据,即使数据被某种方式访问,它也应该被加密或标记化,以便未经授权的用户无法使用数据。数据安全包括数据隐私问题,但除此之外,它还专注于解决与数据相关的任何恶意活动。一个简单的例子是,为了保护 PHI 数据,Cigna 健康保险公司已对其所有敏感数据进行加密。这确保了数据隐私,但并未确保数据安全。尽管黑客可能无法解密加密数据,但他们仍然可以删除数据或对数据进行双重加密,使数据变得无法使用。

现在,让我们讨论一下为什么数据安全是必要的。

每年,仅在美国,数据泄露造成的损失就约为 80 亿美元,平均每次事件导致大约 25,000 个账户受到损害。让我们探讨一下过去几年发生的几起最大的数据泄露事件:

  • 2013 年 6 月,Capital One 报告了一起数据泄露事件,影响了其 1.06 亿个账户。个人信息,如信用记录、社会保障号码和银行账户信息遭到泄露。

  • 2021 年 6 月,LinkedIn 发生了一起大规模数据泄露事件,当时有 7 亿个账户(当时总账户数的 92%)受到影响。用户数据被发布在暗网上出售。

  • 2019 年 4 月,Facebook 报告了一起攻击事件,影响了 5.33 亿个账户。

如我们所见,这些泄露不仅对受损害的数据构成安全风险,还损害了公司的声誉和信任。现在,让我们讨论一些常见的数据安全威胁,如下所示:

  • 钓鱼和其他社会工程学攻击:社会工程学是欺骗或操纵个人以获取对公司数据未授权访问或收集机密信息的常用方法。钓鱼是社会工程学攻击的一种常见形式。在这里,看似来自可信来源的消息或电子邮件被发送给个人。它们包含恶意链接,当点击时,可以未授权访问公司网络和数据。

  • 内部威胁:内部威胁是由无意或有意威胁公司数据安全的员工引起的。它们可以是以下类型:

    • 恶意行为者,他们出于个人利益故意窃取数据或对组织造成损害。

    • 非恶意行为者,他们意外地造成威胁或因为他们不了解安全标准。

    • 被入侵的用户,他们的系统在没有他们意识的情况下被外部攻击者入侵。然后,攻击者假装成合法用户执行恶意活动。

  • 勒索软件:这是一种感染企业系统并加密数据的恶意软件,使其在没有解密密钥的情况下变得无用。然后,攻击者显示勒索信息以支付解密密钥。

  • SQL 注入:在这里,攻击者获取对数据库及其数据的访问权限。然后,他们向看似无害的 SQL 查询中注入不需要的代码以执行不需要的操作,从而删除或损坏数据。

  • 分布式拒绝服务(DDoS)攻击:DDoS 攻击是一种恶意尝试破坏网络资源,如网络服务的攻击。它通过向网络服务器发送大量虚假调用来实现。由于在极短的时间内服务调用量极大,网络服务器崩溃或变得无响应,导致网络资源停机。

  • 云中的数据泄露:确保云中的安全是一个巨大的挑战。在通过网络发送数据以及在云中静止的数据都需要安全措施。

在本节中,我们通过一些现实世界的例子讨论了数据安全是什么以及缺乏数据安全可能发生的威胁。现在,让我们讨论可用于数据安全性的解决方案和工具。

可用于数据安全性的解决方案和工具

在前一节中,我们简要讨论了数据安全是什么以及为什么需要它。我们还查看了一些常见的数据安全威胁。这里描述的解决方案和工具有助于减轻或最小化前一节中讨论的威胁的风险:

  • 数据发现和分类:为确保数据安全,发现敏感信息非常重要。这项技术使用数据发现将数据分类到各种安全标签(如机密、公开等)。一旦完成分类,就可以根据组织的需要将安全策略应用于各种数据分类。

  • 防火墙:这是任何网络入侵的第一道防线。它们阻止任何不受欢迎的流量进入网络。它们还帮助打开到外部网络的特定端口,这给黑客进入网络的机会更少。

  • 备份和恢复:这有助于组织在数据被删除或损坏的情况下保护自己。

  • 杀毒软件:这用于检测可能窃取、修改或损坏您数据的任何病毒、特洛伊木马和后门程序。它们被广泛用于个人和企业数据安全。

  • 入侵检测和预防系统IDS/IPS):IDS/IPS 对网络流量中的数据包进行深度检查,并记录任何恶意活动。这些工具有助于阻止 DDoS 攻击。

  • 安全信息和事件管理SIEM):这分析来自各种设备(如网络设备、服务器和应用程序)的记录日志,并根据特定标准和阈值生成安全警报事件。

  • 数据丢失预防DLP):此机制监控不同设备,以确保敏感数据未经适当授权不会被复制、移动或删除。它还监控并记录谁在使用数据。

  • 访问控制:这项技术允许或拒绝对单个数据资源的读取、写入或删除访问。访问控制可以通过使用访问控制列表ACL)或基于角色的访问控制RBAC)来实现。云安全系统,如 IAM,用于在云上强制执行访问控制。

  • 安全即服务:这是基于软件即服务的模式。在这里,服务提供商基于订阅模式提供企业基础设施的安全服务。它使用按使用付费的模式。

  • 数据加密:如 PHI 这样的数据可能非常敏感。如果丢失或泄露,这可能导致监管问题和严重的经济损失。为了保护此类数据,我们可以加密数据(其中被屏蔽的数据失去了所有原始数据属性,如大小)或对其进行标记(其中被屏蔽的数据保留了原始数据的属性)。

  • 物理安全:最后,物理安全对于阻止对数据的未授权访问至关重要。可以通过创建强大的安全策略并实施它们来启用物理安全。例如,每次离开时锁定系统、不允许尾随他人等策略可以帮助防止社会工程攻击和对数据的未授权访问。

在本节中,我们了解了行业中所提供的各种数据安全解决方案和工具。现在,让我们总结一下本章所学的内容。

摘要

在本章中,我们学习了数据治理是什么以及为什么需要它。然后,我们简要讨论了数据治理框架。之后,我们讨论了如何使用 DataHub 开发一个实用的数据治理解决方案。接下来,我们学习了数据安全是什么以及为什么需要它。最后,我们简要讨论了确保数据安全所提供的各种解决方案和工具。

通过这些,我们已经学习了如何使用实时和基于批次的管道来摄取数据,以及数据摄取的流行架构模式,以及数据治理和数据安全。在下一章中,我们将讨论如何将数据作为服务发布给下游系统。

第三部分 – 启用数据即服务

本书本节主要关注构建数据即服务(Data as a Service,DaaS)解决方案的架构。在本部分,你将学习如何构建各种类型的企业级数据即服务DaaS)解决方案,并正确地管理和保护它们。

本节包括以下章节:

  • 第九章**,将 MongoDB 数据作为服务暴露

  • 第十章**,使用 GraphQL 的联邦和可扩展 DaaS

第九章:将 MongoDB 数据作为服务暴露

在前面的章节中,我们学习了如何分析和设计针对各种数据导入和存储问题的解决方案。我们还学习了如何分析和分类这些问题。之后,我们学习了如何应用可扩展的设计原则,并选择最佳技术来实现这些解决方案。最后,我们学习了如何开发、部署、执行和验证这些解决方案。然而,在现实世界的场景中,将整个数据库暴露给下游系统并不总是好主意。如果我们计划这样做,我们必须确保数据库上实施了适当的授权和访问规则(请参阅第一章现代数据架构基础中的发布问题部分,了解各种发布数据的方式)。提供选择性授权访问数据的一种方法是通过数据即服务DaaS)进行发布。

DaaS 使数据可以通过一个平台和语言无关的 Web 服务(如 SOAP、REST 或 GraphQL)进行发布。在本章中,我们将分析和实现一个使用 REST API 发布从 MongoDB 数据库中已导入和排序数据的 DaaS 解决方案。在这里,我们将学习如何设计、开发和单元测试用于发布数据的 REST 应用程序。我们还将学习如何将我们的应用程序部署到 Docker 中。此外,我们将简要了解如何使用 API 管理工具,以及它们如何提供帮助。尽管 Apigee 是最受欢迎的 API 管理工具,但我们将使用 AWS API Gateway,因为我们将在 AWS 集群上部署和运行我们的应用程序。到本章结束时,您将了解 DaaS 是什么以及何时应该使用它。您还将了解如何设计和开发 DaaS API,以及如何使用 API 管理工具启用 DaaS API 的安全性并监控/控制流量。

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

  • 介绍 DaaS – 什么和为什么

  • 使用 Spring Boot 创建一个 DaaS 以暴露数据

  • API 管理

  • 使用 AWS API Gateway 在 DaaS API 上启用 API 管理

技术要求

为了完成本章,您需要以下内容:

  • 具备 Java 的先验知识

  • 在您的本地系统上安装了 OpenJDK 1.11

  • 在您的本地系统上安装了 Maven、Docker 和 Postman

  • AWS 账户

  • 在您的本地系统上安装了 AWS CLI

  • 在您的本地系统上安装了 IntelliJ Idea Community 或 Ultimate Edition

本章的代码可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter09.

介绍 DaaS – 什么和为什么

在介绍中,我们简要讨论并确定 DaaS 对于安全发布已导入和分析了的数据是有用的。

什么是 DaaS?它是一种数据管理策略,使数据成为业务资产,这使得有价值和业务关键数据能够按需提供给各种内部和外部系统。软件即服务SaaS)在 90 年代末开始流行,当时软件是按需提供给消费者的。同样,DaaS 使数据按需访问成为可能。借助面向服务的架构SOAs)和 API,它实现了安全且平台无关的数据访问。以下图表提供了一个 DaaS 堆栈的概述:

图 9.1 – DaaS 堆栈概述

图 9.1 – DaaS 堆栈概述

如我们所见,来自不同类型的数据存储,如数据仓库、数据湖或在线数据库的数据可以通过虚拟数据层进行统一,该层可用于构建服务或 API 层。API 管理层位于数据用户和 API 层之间。API 管理层负责注册、保护、记录和编排底层服务 API。所有消费者应用程序都与 API 管理层交互,以从服务 API 中消费数据。

现在我们已经知道了什么是 DaaS,让我们来探讨一下为什么它很重要。

数据是新的黄金。每个组织都收集了大量的数据,但成功的组织知道如何最优地使用这些数据来推动其利润。数据收集和存储在组织内使用不同的介质进行。但为了充分利用这些数据的潜力,组织内的团队应该能够轻松且安全地访问它们。由团队维护的孤岛数据可能在整个组织中产生价值。

另一方面,将数据库暴露给组织内的各个团队可能会成为治理或安全上的头疼问题。这正是 DaaS 发挥关键作用的地方。通过使用 API 公开有价值和业务关键数据,DaaS 使团队能够在数据集上实施必要的访问和安全检查的同时与内部团队共享数据。这有助于团队轻松注册和订阅所需的数据,而不是不得不进行不必要的繁琐工作,如分析、摄取和维护由组织中的其他团队已经摄取和维护的数据集。这有助于组织内快速开发和节省在开发和维护冗余数据集上的成本。此外,DaaS 还使许多企业能够有选择地向外部世界提供数据以获取利润或实用性。

根据 Gartner 的炒作周期,DaaS 距离达到其生产力的顶峰还有很长的路要走,这意味着它有潜力成为未来十年数据工程中最具影响力的进步之一。

使用 DaaS 的好处

以下是使用 DaaS 的一些主要好处:

  • 敏捷性:DaaS 使用户能够访问数据,而无需全面了解数据存储的位置或其索引方式。它还使团队能够专注于其业务功能,而不必不必要地花费时间存储和管理数据。这有助于显著缩短上市时间。

  • 易于维护:使用 DaaS 获取数据的团队在维护方面遇到的麻烦较少,因为他们不必担心管理和维护它,也不必担心其存储和数据管道。

  • 数据质量:由于数据是通过 API 提供的,数据更加非冗余,因此关注数据质量变得更加容易和稳健。

  • 灵活性:DaaS 为公司提供了在初始投资与运营成本之间进行权衡的灵活性。它有助于组织在存储数据的初始设置以及持续维护成本上节省成本。它还使团队能够按需获取数据,这意味着团队不需要对这项服务做出长期承诺。这使得团队能够更快地开始使用 DaaS 提供的数据。另一方面,如果在一段时间后不再需要这些数据或用户希望迁移到更新的技术堆栈,迁移过程将变得更快、更无烦恼。DaaS 还使用户能够轻松地在本地环境或云端进行集成。

现在我们已经了解了使用 DaaS 发布数据的概念和好处,在下一节中,我们将学习如何使用 REST API 实现 DaaS 解决方案。

使用 Spring Boot 创建 DaaS 以公开数据

在本节中,我们将学习如何使用 Java 和 Spring Boot 公开基于 REST 的 DaaS API。但在我们尝试创建解决方案之前,我们必须了解我们将要解决的问题。

重要提示

REST代表表征状态转移。它不是一个协议或标准;相反,它提供了一些架构约束来暴露数据层。REST API 允许您将数据资源的表征状态传输到 REST 端点。这些表征可以是 JSON、XML、HTML、XLT 或纯文本格式,以便可以通过 HTTP/(S)协议进行传输。

问题陈述

第六章“构建实时处理管道”中描述的解决方案中,我们分析了 MongoDB 基础集合中的分析数据。现在,我们希望通过一个可以按ApplicationIdCustomerId进行搜索的 DaaS 服务来公开集合中的文档。在下一节中,我们将分析、设计和实现该解决方案。

分析和设计解决方案

让我们分析解决给定问题的需求。首先,我们将记录所有可用的事实和信息。以下是我们所知道的事实:

  • 要发布的数据存储在云中托管的 MongoDB 集合中

  • 我们需要以安全和 API 管理的方式发布 DaaS

  • 我们需要创建端点,以便可以通过 ApplicationIdCustomerId 获取数据

  • 为此数据构建的 DaaS 应该是平台或语言无关的

基于这些事实,我们可以得出结论,我们不一定需要一个虚拟数据层。然而,我们需要应用程序发布两个端点——一个基于 ApplicationId 暴露数据,另一个基于 CustomerId 暴露数据。另外,由于 MongoDB 实例在云端,并且大多数使用此 DaaS 的前端应用程序也在云端,因此将此应用程序部署在 EC2 或 弹性容器仓库ECR)实例中是有意义的。然而,由于我们没有关于将使用此 DaaS 的流量的信息,因此将应用程序容器化更有意义,这样在未来,我们可以通过添加更多容器轻松扩展应用程序。

下面的图示展示了我们问题的提出的解决方案架构:

图 9.2 – DaaS 提出的解决方案架构

图 9.2 – DaaS 提出的解决方案架构

如我们所见,我们使用我们的 REST 应用程序从 MongoDB 读取数据并为 REST 端点检索结果。我们使用 Spring Boot 作为技术栈来构建 REST API,因为它模块化、灵活、可扩展,并提供了一个惊人的 I/O 集成范围和社区支持。我们将为该应用程序创建 Docker 容器,并使用 AWS Fargate 服务在 AWS 弹性容器服务集群中部署它们。这使我们能够在未来流量增加时快速扩展或缩减。最后,我们在部署的应用程序之上应用 API 管理层。市场上有很多 API 管理工具,包括谷歌的 APIJEE、微软的 Azure API 管理、AWS API Gateway 和 IBM 的 API Connect。然而,由于我们的堆栈部署在 AWS 上,我们将使用 AWS 的原生 API 管理工具:AWS API Gateway。

既然我们已经讨论了解决方案的整体架构,让我们更多地了解 Spring Boot 应用程序的设计。下面的图示展示了 Spring Boot 应用的底层设计:

图 9.3 – Spring Boot REST 应用程序的底层设计

图 9.3 – Spring Boot REST 应用程序的底层设计

我们的 Spring 应用程序有一个名为 DaaSController 的控制器类,它公开了两个 GET 端点,如下所示:

  • GET /rdaas/application/{applicationId}

  • GET /rdaas/customer/{id}/application

第一个 REST 端点根据 applicationId 返回应用程序文档,而第二个 REST 端点使用 customerId 作为搜索条件返回给定客户的全部应用程序。

MongoConfig 类用于配置和初始化 mongoTemplateMongoRepository。我们创建了一个名为 ApplicationRepository 的自定义 MongoRepository,它使用 QueryDSL 在运行时动态生成自定义 MongoDB 查询。控制器类使用 ApplicationRepository 类连接到 MongoDB,并根据请求从集合中获取文档。

因此,我们已经分析了问题并创建了解决方案设计。现在,让我们讨论如何实施这个解决方案。首先,我们将开发 Spring Boot REST 应用程序。

实现 Spring Boot REST 应用程序

在本节中,我们将学习如何构建 Spring Boot 应用程序以实现上一节中设计的解决方案。

首先,我们必须使用我们的 IDE 创建一个 Maven 项目,并添加以下 Spring 依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

这些依赖项确保了所有 Spring Boot 基础依赖项以及基于 REST 的依赖项都得到满足。然而,我们必须添加与 MongoDB 相关的依赖项。以下是与 Spring 的 MongoDB 集成相关的依赖项,以及编写自定义 MongoDB 查询所需的必要 QueryDSL 依赖项:

<!-- mongoDB dependencies -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- Add support for Mongo Query DSL -->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-mongodb</artifactId>
    <version>5.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.mongodb</groupId>
            <artifactId>mongo-java-driver</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>5.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api -->
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>

除了这些依赖项之外,我们还需要将构建插件添加到 pom.xml 文件中。这些插件有助于动态生成 Q 类,这对于 QueryDSL 正常工作至关重要。以下是需要添加的插件:

<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <!-- Add plugin for Mongo Query DSL -->
    <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <dependencies>
            <dependency>
                <groupId>com.querydsl</groupId>
                <artifactId>querydsl-apt</artifactId>
                <version>5.0.0</version>
            </dependency>
        </dependencies>
        <executions>
            <execution>
                <phase>generate-sources</phase>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/apt</outputDirectory>
                    <processor>org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor</processor>
                    <logOnlyOnError>false</logOnlyOnError>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

现在我们已经添加了所有必要的依赖项,我们将创建 Spring Boot 应用的入口点,即 main 类,如下所示:

@SpringBootApplication(scanBasePackages = "com.scalabledataarch.rest")
public class RestDaaSApp {
    public static void main(String[] args) {
        SpringApplication.run(RestDaaSApp.class);
    }
}

根据前面的代码,com.scalabledataarch.rest 包中的所有 Bean 组件将在 Spring Boot 应用程序启动时递归扫描并实例化。现在,让我们使用名为 MongoConfigConfiguration 类创建 Mongo 配置 Bean。相应的源代码如下:

@Configuration
@EnableMongoRepositories(basePackages = "com.scalabledataarch.rest.repository")
public class MongoConfig {
    @Value(value = "${restdaas.mongoUrl}")
    private String mongoUrl;
    @Value(value = "${restdaas.mongoDb}")
    private String mongoDb;
    @Bean
    public MongoClient mongo() throws Exception {
        final ConnectionString connectionString = new ConnectionString(mongoUrl);
        final MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString).serverApi(ServerApi.builder()
                .version(ServerApiVersion.V1)
                .build()).build();
        return MongoClients.create(mongoClientSettings);
    }
    @Bean
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(mongo(), mongoDb);
    }
}

如我们所见,MongoConfig 类使用了 @EnableMongoRepositories 注解,其中配置了仓库的基本包。在基本包下扩展 MongoRepository 接口的所有类都将被扫描,并将创建 Spring Bean。除此之外,我们还创建了 MongoClientMongoTemplate Bean。在这里,我们使用了 com.mongodb.client.MongoClients API 来创建 MongoClient Bean。

接下来,我们将创建一个模型类,它可以存储从 MongoDB 文档反序列化出来的数据。我们可以创建名为 Application 的模型类,如下所示:

import com.querydsl.core.annotations.QueryEntity;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@QueryEntity
@Document(collection = "newloanrequest")
public class Application  {
    @Id
    private String _id;
    private String applicationId;

在类上使用 @Document 注解并给出集合名称的值作为其参数,对于 spring-data-mongo 来说是重要的,以便理解这个 POJO 代表了指定集合的 MongoDB 文档结构。此外,使用 @QueryEntity 注解对于 QueryDSL 使用 apt-maven-plugin 动态生成 Q 类是必不可少的。

现在,我们将使用这个 Application POJO 来编写我们的自定义 Mongo 仓库,如下面的代码所示:

public interface ApplicationRepository extends MongoRepository<Application, String>, QuerydslPredicateExecutor<Application> {
    @Query("{ 'applicationId' : ?0 }")
    Application findApplicationsById(String applicationId);
    @Query("{ 'id' : ?0 }")
    List<Application> findApplicationsByCustomerId(String id);
}

要实现自定义仓库,它必须实现或扩展MongoRepository接口。由于我们的ApplicationRepository使用 QueryDSL,它必须扩展QuerydslPredicateExecutor。我们可以使用@Query注解指定 Mongo 查询,如前面的代码所示,这将当调用相应的方法时执行。

现在,我们将创建一个名为DaasController的控制器类。DaasController类应该被注解为@RestController,以表明它是一个发布 REST 端点的 Spring 组件。可以使用@RequestMapping注解在DaaSController中创建端点的基路径,如下面的代码片段所示:

@RestController
@RequestMapping(path = "/rdaas")
public class DaasController {
...
}

现在,我们将添加我们的方法,每个方法对应一个 REST 端点。下面的代码显示了其中一个方法的源代码:

...
@Autowired
ApplicationRepository applicationRepository;
@GetMapping(path= "/application/{applicationId}", produces = "application/json")
public ResponseEntity<Application> getApplicationById(@PathVariable String applicationId){
  Application application = applicationRepository.findById(applicationId).orElseGet(null);
  return ResponseEntity.ok(application);
}
...

如我们所见,当调用 REST 端点时将被触发的那个方法被注解为@GetMapping@PostMapping,这取决于 HTTP 方法类型。在我们的情况下,我们需要一个GET请求。每个映射都应该由 URL 路径和其他必要的属性作为这些注解的参数来伴随。在这个方法中,使用了自动装配的applicationRepository豆来使用applicationId字段获取 Mongo 文档。

最后,我们将创建application.yml文件来设置运行 Spring Boot 应用程序的配置参数。在我们的情况下,application.yml文件将如下所示:

restdaas:
  mongoUrl: <mongo url>
  mongoDb: newloanrequest

如前所述的application.yml文件的源代码所示,我们在application.yml文件中配置了各种 Mongo 连接细节。

现在,我们可以通过运行main类在我们的本地机器上运行应用程序,并使用 Postman 进行测试,如下所示:

  1. 首先,点击ApplicationDaaS

图 9.4 – 创建新的 Postman 集合

图 9.4 – 创建新的 Postman 集合

  1. 使用添加请求选项向集合添加请求,如下面的屏幕截图所示:

图 9.5 – 向 Postman 集合添加请求

图 9.5 – 向 Postman 集合添加请求

  1. 接下来,填写 HTTP 方法、REST URL 和头部的配置。现在,您可以使用发送按钮执行请求,如下面的屏幕截图所示:

图 9.6 – 通过 Postman 测试 REST API

图 9.6 – 通过 Postman 测试 REST API

现在我们已经在本地测试了应用程序,让我们看看如何将其部署到 ECR。按照以下步骤在 ECR 中部署:

  1. 首先,我们需要将这个应用程序容器化。为此,我们必须创建我们的 Docker 文件。这个DockerFile的源代码如下:

    FROM openjdk:11.0-jdk
    VOLUME /tmp
    RUN useradd -d /home/appuser -m -s /bin/bash appuser
    USER appuser
    ARG JAR_FILE
    COPY ${JAR_FILE} app.jar
    EXPOSE 8080
    ENTRYPOINT ["java","-jar","/app.jar"]
    

在这里,第一行导入了一个包含预配置 OpenJDK 11 软件的基图像。在那里,我们创建了一个名为 /tmp 的卷。然后,我们使用 RUN useradd 命令向这个 Docker 容器中添加一个名为 appuser 的新用户。使用 USER 命令,我们以 appuser 身份登录。然后,我们将 JAR 文件复制到 app.jar。JAR 文件路径作为参数传递给此 DockerFile。将 JAR 文件路径作为参数传递将有助于我们在将来如果想要构建一个 8080 端口从容器中暴露出来。最后,我们使用 ENTRYPOINT 命令运行 java -jar 命令。

  1. 现在,我们可以通过在命令行或终端中运行以下命令来构建 Docker 图像:

    docker build -t apprestdaas:v1.0 .
    
  2. 这将在本地 Docker 图像仓库中创建一个名为 apprestdaas 并带有 v1.0 标签的 Docker 图像。您可以通过以下命令在 Docker Desktop 或终端中查看图像列表:

    docker images
    

现在我们已经创建了 Docker 图像,让我们讨论如何在 ECS 集群中部署此图像。

在 ECS 集群中部署应用程序

在本节中,我们将讨论如何在 AWS ECS 集群中部署我们的 REST 应用程序。请按照以下步骤操作:

  1. 首先,我们将在 AWS ECR 中创建一个存储我们的 Docker 图像的仓库。我们需要这个仓库的 Amazon Resource NameARN)来标记和上传图像。首先,我们将导航到 AWS 管理控制台中的 ECR 并点击 创建仓库。您将看到一个 创建仓库 页面,如下面的截图所示:

图 9.7 – 创建仓库页面

图 9.7 – 创建仓库页面

在这里,填写仓库的名称,其余字段保持不变,并提交请求。一旦创建,您将能够在 ECR 的 私有仓库 页面上看到仓库列表,如下所示:

图 9.8 – ECR 仓库已创建

图 9.8 – ECR 仓库已创建

  1. 下载最新的 AWS CLI 并安装它。然后,使用以下命令配置 AWS CLI:

    aws configure
    

在配置 AWS CLI 时,您需要提供访问密钥 ID 和秘密访问密钥。您可以通过遵循 docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.xhtml#Using_CreateAccessKey 中的说明来生成这些变量。

  1. 接下来,我们需要使用以下命令为 Docker 生成一个 ECR 登录令牌:

    aws ecr get-login-password --region <region>
    

当您运行此命令时,将生成一个由 Docker 用于将图像推送到 ECR 所需的认证令牌。我们可以使用以下命令将前面的命令与以下命令连接起来,其中我们直接将令牌传递给 docker login 命令。最终的命令如下所示:

aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <accountid>.dkr.ecr.<region>.amazonaws.com

在前面的命令中,请将 region 替换为正确的 AWS 区域,例如 us-east-2us-east-1,并将 accountid 替换为正确的 AWS 账户 ID。

  1. 接下来,我们将使用 ECR 仓库 URI 对本地 Docker 镜像进行标记。这对于 ECR 将正确的仓库与我们要推送的镜像进行映射是必需的。此操作的命令如下:

    docker tag apprestdaas:v10 <accountid>.dkr.ecr.<region>.amazonaws.com/restdaas:v1
    
  2. 现在,我们将使用以下命令将 Docker 镜像推送到 ECR 仓库:

    docker push <accountid>.dkr.ecr.us-east-2.amazonaws.com/restdaas:v1
    

运行此命令后,本地 Docker 镜像将带有 restdaas:v1 标签推送到 ECR 仓库。

  1. 现在,让我们创建一个 AWS Fargate 集群。为此,我们必须再次登录 AWS 管理控制台。在这里,搜索 弹性容器服务 并选择它。从 弹性容器服务 仪表板,在左侧面板中导航到 集群 并选择 创建集群

图 9.9 – 创建 ECS 集群

图 9.9 – 创建 ECS 集群

  1. 接下来,将集群模板设置为 仅网络 并点击 下一步

图 9.10 – 选择 ECS 集群模板

图 9.10 – 选择 ECS 集群模板

  1. 然后,输入集群的名称。在这里,我们将命名为 daas-cluster。保留其他字段不变。现在,点击 创建 按钮以创建新的 ECS 集群:

图 9.11 – 命名并创建 ECS 集群

图 9.11 – 命名并创建 ECS 集群

  1. 现在,我们将创建一个 ECS 任务。从 AWS ECS 控制台仪表板,从左侧菜单中选择 任务定义,然后点击 创建新任务定义,如图所示:

图 9.12 – 创建新的任务定义

图 9.12 – 创建新的任务定义

  1. 然后,在 选择启动类型兼容性 下,选择 FARGATE 并点击 下一步,如图所示:

图 9.13 – 选择 ECS 任务的启动类型

图 9.13 – 选择 ECS 任务的启动类型

  1. 接下来,我们必须设置 restdaas。将 任务角色 设置为 并将 操作系统家族 设置为 Linux,如图所示:

图 9.14 – 设置任务定义

图 9.14 – 设置任务定义

保持 任务执行角色 不变,将 任务内存(GB) 设置为 1GB,并将 任务 CPU(vCPU) 设置为 0.5 vCPU,如图所示:

图 9.15 – 选择任务内存和任务 CPU(vCPU)值

图 9.15 – 选择任务内存和任务 CPU(vCPU)值

  1. 现在,我们将向 ECS 任务添加容器。我们可以通过点击 restdaas 作为容器名称并填写我们镜像的 ECR ARN 来添加容器,如图所示:

图 9.16 – 向 ECS 任务添加容器

图 9.16 – 向 ECS 任务添加容器

  1. 点击 添加 按钮以添加容器。然后,在 创建新任务定义 页面上点击 创建 按钮。这将创建新的任务,如图所示:

图 9.17 – 创建的 ECS 任务

图 9.17 – 创建的 ECS 任务

如前述截图所示,我们创建的任务restdaas处于活动状态,但它尚未运行。

  1. 现在,让我们运行任务。点击操作下拉按钮,选择运行任务。这将提交任务,使其处于可运行状态。点击运行任务后,将出现一个屏幕,我们必须填写运行任务的各种配置,如下截图所示:

图 9.18 – 运行任务

图 9.18 – 运行任务

启动类型设置为FARGATE,并将操作系统家族设置为Linux。同时,选择任何可用的 VPC 和子网组,如下截图所示:

图 9.19 – 设置 VPC 和安全组

图 9.19 – 设置 VPC 和安全组

  1. 如前述截图所示,请确保端口为8080,因为我们的 Spring Boot 应用程序将在端口8080上运行,如下截图所示:

图 9.20 – 允许端口 8080

图 9.20 – 允许端口 8080

  1. 保持其他字段不变,然后点击运行任务。通过刷新任务选项卡,我们可以看到任务状态从配置中变为运行中。以下截图显示了一个运行中的任务:

图 9.21 – 运行状态的任务

图 9.21 – 运行状态的任务

  1. 现在,我们将测试我们在 ECS 中部署的 DaaS。为此,点击前述截图中任务列下的文本。这会带我们到运行任务屏幕,如下截图所示:

图 9.22 – 运行任务详情

图 9.22 – 运行任务详情

如前述截图所示,我们将获取公网 IP。我们将使用此 IP 进行测试。现在,从 Postman,我们可以使用此 IP 地址和端口8080测试我们的 REST 端点,如下截图所示:

图 9.23 – 测试在 AWS ECS 中部署的 REST 端点

图 9.23 – 测试在 AWS ECS 中部署的 REST 端点

在本节中,我们学习了如何开发一个 REST 应用程序以发布 DaaS,将应用程序容器化,并在 AWS ECS 集群中部署它,并测试端点。在下一节中,我们将了解 API 管理的必要性,并提供一个逐步指南,将 API 管理层附加到本节中开发和部署的 REST 端点。

API 管理

API 管理是一套工具和技术,使我们能够分发、分析和控制组织内部暴露数据和服务的 API。它可以在 API 之上充当包装器,无论这些 API 是在本地部署还是云中部署。在我们架构解决方案以通过 API 发布数据时,使用 API 管理总是一个好主意。首先,让我们了解 API 管理是什么以及它如何帮助。以下图表显示了 API 管理层在哪里以及如何提供帮助:

图 9.24 – API 管理如何提供帮助

图 9.24 – API 管理如何提供帮助

如我们所见,API 管理是一个介于面向客户的 API 和内部服务 API 之间的包装层。我们可以在 API 管理层中定义资源和方法,这些资源和方法会被暴露给客户。主要来说,使用 API 管理层,架构可以得到以下好处:

  • 灵活性:API 管理通过启用持续部署和测试,使在不同环境中轻松部署成为可能。它还提供了一个统一的接口,其中单个面向客户的 API 可以从多个复杂的内部服务 API 中获取。这使集成变得容易,并允许你发布资源,而无需创建额外的 API 来集成和管理多个 API。另一方面,在非常动态的技术环境中,内部服务 API 可能会频繁更新或其结构可能会改变。API 管理层使我们能够轻松地从旧的内部服务 API 迁移到新的 API,而无需更改或影响面向客户的 API。这为我们提供了巨大的设计灵活性,并帮助我们轻松克服内部服务 API 层中的技术债务。

  • 安全性:API 管理以不同的方式提供安全性。首先,它使 API 能够拥有自定义授权器,如 OAuth。其次,它使客户特定的使用计划和 API 密钥成为可能。这确保只有注册了使用计划和 API 密钥的消费者才能访问应用程序。此外,它还限制了消费者每秒可以进行的交易数量。这一点加上节流功能,有助于我们避免对服务 API 的任何分布式拒绝服务DDoS)攻击。除此之外,还可以通过 API 的 RBAC 功能启用基于角色的访问。所有这些安全特性使 API 管理成为设计 DaaS 架构的必要组件。

  • 文档:这允许你轻松创建、发布和维护 API 的文档。发布的文档可以很容易地被 API 的消费者访问,使他们的生活变得简单。除此之外,甚至SwaggerOpenAPI规范也可以通过 API 管理进行发布和维护。

  • 分析:使用 API 管理的主要优势之一是能够在 API 部署并用于生产时监控和分析流量、延迟和其他参数。

在本节中,我们了解了 API 管理是什么以及它如何帮助我们为 DaaS 解决方案创建一个强大的架构。在下一节中,我们将为我们之前开发的 ECS REST Service API 附加一个 API 管理层。

使用 AWS API 网关在 DaaS API 上启用 API 管理

在本节中,我们将讨论如何使用 AWS API 网关设置 API 管理。我们将使用本章前面在 ECS 中开发和部署的 REST DaaS API。按照以下步骤为我们的 REST DaaS API 设置 API 管理层:

  1. 在 AWS 管理控制台中,搜索AWS API Gateway并导航到AWS API Gateway服务仪表板。从这里,选择REST API并点击构建,如图下所示截图:

图 9.25 – AWS API 网关仪表板

图 9.25 – AWS API 网关仪表板

  1. 将打开一个新窗口,如图下所示截图。选择REST作为协议,然后在创建新 API下选择新建 API。填写 API 的名称和描述,然后点击创建 API

图 9.26 – 创建 REST API

图 9.26 – 创建 REST API

  1. 资源创建完成后,我们将进入 API 的详细信息。我们可以通过此界面中的操作下拉菜单添加资源或方法,如图下所示截图:

图 9.27 – 向 API 添加资源

图 9.27 – 向 API 添加资源

在这里,我们将点击/loanapplications。然后,在loanapplications下添加另一个名为appId、路径为/{appId}的资源。请注意,{}表示appId是一个路径变量。最后,在appId资源中,我们将通过选择创建方法添加一个方法,如图下所示截图:

图 9.28 – 配置 GET 方法

图 9.28 – 配置 GET 方法

/loanapplications/{appId} API 资源设置为/rdaas/application/{appId} DaaS API 资源。

  1. 通过从操作下拉菜单中选择部署 API选项来部署 API,如图下所示截图:

图 9.29 – 部署 API

图 9.29 – 部署 API

执行此操作后,将出现一个窗口,如图下所示截图。将部署阶段设置为[新阶段],填写阶段的名称和描述。最后,点击部署

图 9.30 – 部署 API 配置

图 9.30 – 部署 API 配置

  1. 现在,我们将通过 Postman 测试新的面向客户的 API。但在那之前,我们必须找出 API 的基础 URL。要做到这一点,导航到左侧面板中的阶段并选择dev。将出现dev 阶段编辑器页面,如下截图所示:

图 9.31 – 使用开发阶段编辑器获取 API 的基础 URL

图 9.31 – 使用开发阶段编辑器获取 API 的基础 URL

如前一个屏幕截图所示,我们可以获取基础 URL。

现在,我们可以通过将消费者 API 的 URI 添加到基础路径来形成客户 API;例如,http://<baseurl>/loanapplications/<some_app_id>。我们可以使用这个 API 并在 Postman 中测试它,如下所示:

图 9.32 – 测试 AWS API Gateway 公开的外部 API(无安全措施)

图 9.32 – 测试 AWS API Gateway 公开的外部 API(无安全措施)

我们还可以看到用于监控 API 的仪表板,如下截图所示:

图 9.33 – RESTDaasAPI 的 AWS API Gateway 仪表板

图 9.33 – RESTDaasAPI 的 AWS API Gateway 仪表板

从这个仪表板中,我们可以收集有关每天 API 调用次数的有用信息。我们还可以监控响应的延迟或随着时间的推移注意到的任何内部服务器错误。

现在我们已经添加了 API 管理层,我们将尝试向面向消费者的 API 添加基于 API 密钥的安全措施。

  1. 导航到RestDaasAPI资源面板,并在{appId}资源下选择GET方法。在配置中,将必需 API 密钥的值从false更改为true

图 9.34 – 将 API 密钥的必需值更改为 true

图 9.34 – 将 API 密钥的必需值更改为 true

然后,导航到使用计划并创建一个使用计划。设置 API 级别的节流参数和月度配额,如下截图所示:

图 9.35 – 创建使用计划

图 9.35 – 创建使用计划

点击下一步并设置方法级别的节流参数,如下所示:

图 9.36 – 配置方法级别的节流参数

图 9.36 – 配置方法级别的节流参数

  1. 点击下一步,通过点击创建 API 密钥并添加到使用计划按钮来为这个使用计划设置一个新的 API 密钥:

图 9.37 – 生成新的 API 密钥以附加到使用计划

图 9.37 – 生成新的 API 密钥以附加到使用计划

  1. 点击此按钮后,将出现API 密钥窗口。提供名称和描述。同时,将API 密钥设置为自动生成

图 9.38 – API 密钥窗口

图 9.38 – API 密钥窗口

  1. 保存新生成的 API 密钥并创建使用计划后,我们可以通过导航到API 密钥并点击显示选项来获取 API 密钥的值。这样做,我们可以查看生成的 API 密钥:

图 9.39 – 通过点击“显示”来展示生成的 API 密钥(突出显示)

图 9.39 – 通过点击“显示”来展示生成的 API 密钥(突出显示)

  1. 一旦你在 API 中按要求设置了 API 密钥,并且在调用 REST 端点时不在头部提供apikey,你将在响应体中收到一个错误消息,类似于{"message":"Forbidden"}。现在,你必须添加一个名为x-api-key的头部,其值应该是你在步骤 9中生成的 API 密钥。然后,你可以通过 Postman 测试安全的 API,并附带 API 密钥,如下面的截图所示:

图 9.40 – 测试 AWS API Gateway 暴露的外部 API(带有安全功能)

图 9.40 – 测试 AWS API Gateway 暴露的外部 API(带有安全功能)

在本节中,我们学习了如何在我们的 REST DaaS API 之上创建一个 API 管理层。我们还讨论了 AWS API Gateway 如何帮助监控和保障 API。

现在,让我们总结一下本章所学的内容。

摘要

在本章中,我们学习了 DaaS 的基础知识。首先,我们讨论了如何使用 Spring Boot 开发和测试基于 REST 的 DaaS API。然后,我们学习了如何将应用程序容器化并将容器发布到 AWS ECR 仓库。我们还学习了如何将发布在 AWS ECR 仓库中的容器部署到 AWS ECS 集群。之后,我们学习了如何使用云管理的 Fargate 服务运行此应用程序。然后,我们学习了 API 管理和其优势。最后,我们使用 AWS API Gateway 实现了一个 API 管理层,以在 REST DaaS API 之上提供安全和监控。

现在,我们已经学会了如何构建、部署、发布和管理基于 REST 的 DaaS API,在下一章中,我们将学习何时以及如何选择基于 GraphQL 的 DaaS 作为良好的设计选项。我们还将学习如何设计和开发 GraphQL DaaS API。

第十章:联邦和可扩展的 GraphQL 数据即服务(DaaS)

在上一章中,我们讨论了如何使用 REST API 以平台和语言无关的格式发布摄取的数据。我们还学习了如何使用 REST API 设计和开发 数据即服务(DaaS)层,以及如何将应用程序容器化并在 AWS ECS 上部署。然后,我们学习了什么是 API 管理系统以及它如何帮助更有效地监控和管理 API。最后,我们学习了如何在我们基于 REST 的 DaaS 应用程序之上创建一个 API 管理层,使用 Amazon API Gateway。

在本章中,我们将学习如何使用 GraphQL 而不是 REST 来实现 DaaS。为此,我们将学习 GraphQL 是什么,以及为什么和何时应该使用它。我们将探讨 GraphQL 相对于 REST 的优点和缺点,同时讨论适用于基于 GraphQL 解决方案的各种架构模式。最后,我们将了解 GraphQL 层中联邦的力量。到本章结束时,你应该了解围绕 GraphQL 的基本概念以及何时在数据工程解决方案中使用此工具。你还将了解如何设计、实现和测试一个 GraphQL 解决方案。

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

  • 介绍 GraphQL - 什么是 GraphQL,何时使用,为什么使用

  • GraphQL 的核心架构模式

  • 一个实际用例 - 使用 GraphQL 暴露联邦数据模型

技术要求

对于本章,你需要以下内容:

  • 需要具备 Java 的先验知识

  • OpenJDK-1.11 已安装在本地的系统上

  • Maven 已安装在本地的系统上

  • GraphQL Playground 已安装在本地的系统上

  • IntelliJ Idea 社区版或终极版已安装在本地的系统上

本章的代码可以从本书的 GitHub 仓库下载:github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/tree/main/Chapter10

介绍 GraphQL - 什么是 GraphQL,何时使用,为什么使用

在本节中,我们将探讨 GraphQL 是什么。根据 graphql.org,GraphQL 的官方定义是:“GraphQL 是一种用于 API 的查询语言,以及用于使用现有数据满足这些查询的运行时。”让我们深入一点,了解 GraphQL。

表示状态转移(REST)一直是跨系统发布数据的标准方式,它是平台、设备和工具/语言无关的。然而,REST 存在两个主要瓶颈:

  • 为了获取不同的相关实体,我们需要多个 REST 请求。我们还必须注意 API 的不同版本。为每个实体或功能的不同端点和版本维护是一个头疼的问题。

  • 在 REST 中,请求和响应参数始终是固定的。例如,有一个 REST API 返回 100 个字段。假设有一个消费者只需要 10 个字段。然而,由于响应是固定的,REST 请求将始终计算并发送所有 100 个字段。这反过来会影响性能,因为形成响应需要更多时间,以及传输更大的负载需要更多的带宽。

GraphQL 是克服这些挑战的答案。GraphQL 是由 Facebook 创建的一个开放标准或规范。它是一种用于 API 的查询语言,客户端可以在发送 GraphQL 请求时查询多个实体及其所需的字段。以下图表描述了 GraphQL 的工作方式:

图 10.1 – GraphQL 的工作方式

图 10.1 – GraphQL 的工作方式

如前图所示,在 GraphQL 中,是 GraphQL 客户端定义它需要什么数据,而 GraphQL 服务器发布可用的数据。因此,本质上,GraphQL 是一种通过 API 获取和更新数据的声明式方法。

让我们通过一个例子来尝试理解这一点。由于 GraphQL 是由 Facebook 创建的,我们将从一个社交网络用例中取一个例子。假设我们想要获取用户、他们的帖子以及与该帖子相关的评论。以下图表显示了如何使用 REST 来设计这一点:

图 10.2 – 基于 REST 的设计需要三个单独的端点

图 10.2 – 基于 REST 的设计需要三个单独的端点

如我们所见,REST 将有三个端点 – 一个用于用户,一个用于帖子,一个用于评论。要获取用户的帖子及其评论,REST 将需要一次用户调用,一次帖子调用,以及每个帖子一次评论调用。以下图表显示了这一点:

图 10.3 – 使用 REST 时需要三个单独的调用

图 10.3 – 使用 REST 时需要三个单独的调用

如我们所见,在这个用例中,至少需要三个 REST 调用来获取数据(假设用户只有一个帖子)。想想一个真实社交平台上的用户需要多少次调用。如果用户发布了 n 个帖子,那么获取这些信息的调用次数将是 n+2。这会严重影响用户体验和网站性能。然而,在 GraphQL 中,只需要一次调用就可以获取这些信息。以下图表显示了 GraphQL 请求的外观:

图 10.4 – 单个 GraphQL 调用可以获取所有所需数据

图 10.4 – 单个 GraphQL 调用可以获取所有所需数据

如您所见,GraphQL 的请求负载声明了它需要获取的实体和字段。因此,GraphQL 客户端确定它需要什么数据。

操作类型

现在我们已经了解了什么是 GraphQL,让我们尝试了解 GraphQL 支持的各种操作类型,如下所示:

  • 查询:这些有助于查询 API,并且仅支持数据读取操作。以下是一个示例查询:

    query myquery{
      byApplicationId(applicationId:"CT20210809"){
        applicationId
        id
        riskScore
      }
    }
    

如前一个有效载荷所示,你可以选择性地使用一个关键字查询,后跟你想分配给查询的名称(在这里,它是myquery)。byApplicationId是 GraphQL 中的一个查询(类似于 REST 中的端点),它将请求参数作为参数。在这里,byApplicationId查询接受一个名为applicationId的参数。此外,正如你所看到的,请求包含了它希望返回的字段名称,例如applicationIdidriskscore

  • 突变:突变支持读取和写入操作。以下是一个示例查询:

      mutation updateApplicationMutation { updateApplication(status:"closed") { 
    applicationId
    custId
    status
    } 
    } 
    

如前述代码所示,可以使用mutation关键字对突变进行标记。这里,它更新数据库中的应用程序状态。因此,它用于写入数据。

  • 订阅:除了查询和突变之外,GraphQL 还支持订阅。与查询类似,它们用于获取数据,但它们使用长期连接,其结果会随时间变化。这允许通过服务器向客户端推送更改的事件通知模式。以下代码展示了订阅查询的示例:

     type subscription{
       commentsforPost(postId: "123ty4567"){
       commentId
       text
      ...
      }
    }
    

在这里,我们通过postId订阅了帖子的评论。因此,客户端和 GraphQL 服务器之间建立了一个长期连接。GraphQL 服务器会自动将评论上的任何更改推送到客户端。

现在,让我们讨论 GraphQL 模式。GraphQL 模式是客户端和服务器之间的一个合约。它声明了在 GraphQL 中可用的操作和字段。它是强类型的,并使用标准的模式定义语言SDL)编写。以下代码块展示了 GraphQL 模式的一个示例:

type Application {
  applicationId: ID!
  id: String!
  genderCode: String
  cars: [String]
 ...
}

在前面的代码块中,applicationId字段是ID数据类型,genderCodeString数据类型,等等。在 SDL 中,GraphQL 模式的数据类型也称为标量;内置的数据类型表示为内置标量,自定义数据类型称为自定义标量。在定义applicationIdID后面的感叹号表示它是一个必填字段。同样,由于其数据类型被[](方括号)包裹,cars被定义为列表。

既然我们已经内化了 GraphQL 的基本概念,我们将探讨为什么以及何时使用 GraphQL。

为什么使用 GraphQL?

在本节中,我们将探讨 GraphQL 的各种优势,这些优势使其成为一个优秀的解决方案。GraphQL 的优势如下:

  • 为平台无关的 API 创建强类型模式:通常,在 SOAP 和 REST API 中,我们的响应是一个平台无关的结构,如 XML 或 JSON。然而,这些格式都不是强类型的。在 GraphQL 中,模式中的每个字段都必须有一个标量类型(它可以是内置的标量类型或自定义的)。这确保了 GraphQL 更少出错,更经过验证,并为像 GraphQL Playground 这样的编辑器提供了易于自动补全的功能。以下截图显示了 GraphQL Playground 提供的自动补全建议:

图 10.5 – GraphQL Playground 中的自动补全功能

图 10.5 – GraphQL Playground 中的自动补全功能

  • 无过度获取或未充分获取:在 GraphQL 中,客户端只能获取它需要的数据。例如,如果一个 GraphQL API 在其 API 响应中支持数百个字段,客户端不需要获取所有这些字段。如果客户端只需要 10 个字段,客户端可以请求 GraphQL API 仅发送这些字段。然而,如果相同的 API 用 REST 编写,即使客户端只需要 10 个字段,响应也会返回所有字段。

在 REST 中,过度获取是一个常见问题,无论客户端需要多少字段,它总是获取响应体中定义的所有字段。例如,对于像 LinkedIn 这样的社交网络网站,一个人的个人资料包含大量列,包括人口统计列、技能集列、奖项和认证列等。可以设计两种基于 REST 的解决方案来解决这个问题:

  • 创建包含所有列的单个 API:如果我们使用这种方法,只需要人口统计信息的客户端会遇到过度获取的问题。

  • 为人口统计、技能集、奖项和认证等创建单独的 API:让我们看看一个客户端需要所有可用信息的场景。如果我们使用这种方法,需要多次调用才能获取数据。这会导致数据未充分获取。

因此,我们需要一个单一解决方案来解决这两种类型客户端请求的问题。GraphQL 通过允许客户端选择它想要获取的字段来解决此问题。

  • 节省时间和带宽:GraphQL 允许你在单个 GraphQL 调用中发出多个资源请求,通过减少到 GraphQL 服务器的网络往返次数,节省了大量时间和带宽。这对于提高客户端应用程序的数据获取体验和速度特别有用。

  • 无需版本控制:在 REST 中,当添加新字段或删除旧字段时,需要发布为新版本以支持消费者兼容性。在 GraphQL 中,版本控制不再需要——一方面,因为 GraphQL 支持从响应结构中获取部分数据,另一方面,它支持向 GraphQL 客户端发布关于已弃用字段的降级警告。

  • 模式拼接或组合多个 GraphQL 模式:GraphQL 提供了多种方法将不同的 GraphQL 模式和 API 组合到单个端点,而无需大量编码或实现上的麻烦。此功能有助于开发一个单一、集中的 GraphQL 网关。一个 GraphQL 网关允许从单个端点消费多个 GraphQL API。它还允许未来无缝地动态添加新的 GraphQL API。这使得 GraphQL 兼容和可扩展。通过 Apollo GraphQL Federation 和 Atlassian GraphQL Braids 等技术可以实现模式组合。

现在,让我们看看何时应该使用 GraphQL。

何时使用 GraphQL

以下是一些 GraphQL API 比 REST API 更好的场景:

  • 带宽使用量重要的应用程序,例如移动应用程序或物联网设备应用程序。

  • 需要获取嵌套数据的应用程序。GraphQL 可以节省大量时间和带宽,从而提高 GraphQL 客户端的性能。

  • 发布 DaaS 的应用程序。在这里,这个 DaaS 被多个下游团队消费,他们有不同的数据获取需求。

  • 当启用 GraphQL 功能,如响应的部分数据获取,通过组合多个 API 来暴露单个端点,以改善基于遗留 REST 的应用程序的消费者体验时。

在本节中,我们学习了 GraphQL 是什么,并探讨了 GraphQL 常用的典型用例。在下一节中,我们将讨论在行业中广泛使用的最流行的 GraphQL 模式。

GraphQL 的核心架构模式

在本节中,我们将讨论用于 GraphQL 的各种架构模式。这些模式与实现它的技术或部署和执行的平台无关。以下是五种不同的 GraphQL 模式:

  • DaaS 模式:在这里,GraphQL 服务器用于暴露数据库层。它可以暴露三种操作——查询、突变和订阅(请参阅本章的 操作类型 部分)。使用这些操作,它可以实现 创建、读取、更新和删除CRUD)操作,就像 REST 一样,但还支持在其之上进行订阅。以下图表展示了此模式:

图 10.6 – DaaS 模式

图 10.6 – DaaS 模式

如我们所见,GraphQL 使用 HTTP 协议公开其查询和操作。GraphQL 在多种语言中提供服务器库,使用这些库,团队可以构建和运行 GraphQL 服务器应用程序。此外,GraphQL 还支持多种不同语言的 GraphQL 客户端库。支持的语言列表可在 graphql.org/code/ 查找。

  • 集成层模式:在这里,GraphQL 服务器提供数据,一次提供对多个数据源的访问。这使得 GraphQL 能够像一个数据集成中心。以下图表描述了集成层模式的工作原理:

图 10.7 – 集成层模式

图 10.7 – 集成层模式

如我们所见,GraphQL 服务器正在充当集成中心。它使客户端能够进行单个调用,但 GraphQL 服务器从不同的生态系统(如微服务、遗留应用程序和云 API)中获取数据,并向客户端发送统一的响应。这自动减少了 GraphQL 客户端必须进行的复杂性和调用次数。

  • 混合模式:第三个 GraphQL 模式被称为混合模式,因为它探索了前两种模式的混合方法。在这里,GraphQL 服务器不仅需要连接到微服务和遗留系统,还需要连接到数据库。以下图显示了此模式:

图 10.8 – 混合模式

图 10.8 – 混合模式

如我们所见,GraphQL 服务器除了连接到不同的应用程序(如微服务和遗留系统)之外,还有自己的数据库。因此,当使用此模式时,GraphQL 为其客户端提供了对不同数据源的统一访问。

  • 带有管理 API 的 GraphQL:为了在企业中公开 GraphQL API,必须启用安全和监控。在此模式中,API 网关为 GraphQL 服务器提供监控、安全和节流。以下图显示了此模式:

图 10.9 – 带有管理 API 的 GraphQL

图 10.9 – 带有管理 API 的 GraphQL

  • 联邦 GraphQL 模式:在这里,创建了一个集中的 GraphQL 编织或联邦 GraphQL 服务器。其他 GraphQL 节点连接到这个 GraphQL 编织。这些节点中的每一个,反过来,从数据库、微服务或遗留应用程序中获取数据。以下图显示了联邦 GraphQL 模式:

图 10.10 – 联邦 GraphQL 模式

图 10.10 – 联邦 GraphQL 模式

这种模式的真正力量在于其惊人的可扩展性和数据联邦。可以在任何时间无缝地将新节点添加到 GraphQL 编织中,而无需任何应用程序停机时间。

在本节中,我们了解了各种核心 GraphQL 模式,它们的操作方式以及何时使用它们。在下一节中,我们将学习如何开发 GraphQL 服务器应用程序。

一个实际用例 – 使用 GraphQL 暴露联邦数据模型

在本节中,我们将学习如何使用 Java 开发 DaaS(数据即服务)使用 GraphQL。为了实现解决方案,我们将发布与之前使用 REST 发布的相同的一组 API,但这次我们将使用 GraphQL 来实现解决方案。

在我们开始实现 GraphQL 之前,设计适合我们用例的 GraphQL 模式非常重要。在我们的用例中,我们需要使用应用程序 ID 或消费者 ID 从 MongoDB 读取信用卡申请。这就是为什么在基于 REST 的解决方案中需要两个单独的端点(请参阅 第九章将 MongoDB 数据作为服务公开,有关基于 REST 的 DaaS 解决方案)。

让我们从不同的角度分析需求 – 即,在考虑基于 GraphQL 的解决方案的同时。GraphQL 带来的最大不同之处在于它减少了端点数量以及调用次数。因此,对于我们的用例,我们将只有一个端点。此外,根据我们的用例,我们只对获取数据感兴趣。因此,我们只会使用 Query 类型的操作。为了支持 GraphQL 中的多个功能,我们必须在查询中有多个这样的字段,这些字段可以接受如下参数:

type Query{
    customerById(id: String):Customer
    customerByName(firstname: String, lastName: String): [Customers]
}

在我们的情况下,我们需要两个这样的字段 – byApplicationIdbyCustomerId,它们都应该返回一个名为 Application 的自定义类型。以下代码片段显示了我们的 GraphQL 模式的一部分:

type Query{
    byApplicationId(applicationId: ID):Application
    byCustomerId(custId: String):[Application]
}

如前述代码块所示,byApplicationId 总是只返回一个 Application,因为 applicationId 是主键。因此,byApplicationIdApplication 类型。然而,由于同一个客户可能有多个应用程序,byCustomerId[Application] 类型,表示 Application 的列表。现在,让我们在 GraphQL 模式中定义类型 – Application。以下代码块显示了 Application 类型的 SDL:

type Application {
    applicationId: ID!
    id: String!
    genderCode: String
    flagOwnCar: String
    flagOwnRealty: String
    cntChildren: Int
    amtIncomeTotal: Float
    nameIncomeType: String
    nameEducationType: String
    nameFamilyStatus: String
    nameHousingType: String
...
}

在此,在 Application 类型的 SDL 中,applicationIdID 类型,表示它是 Application 类型的唯一键。此外,在 applicationIdid 字段中看到的感叹号(!)表示这些字段是非可空的。完整的模式可在 github.com/PacktPublishing/Scalable-Data-Architecture-with-Java/blob/main/Chapter10/sourcecode/GraphQLDaas/src/main/resources/schema.graphqls 找到。

要创建 Spring Boot Maven 项目并添加所需的 Maven 依赖项,应在 pom.xml 文件中添加以下 Maven 依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

除了这个之外,还应该在 Spring Boot 应用程序中添加以下依赖项以支持 MongoDB 相关依赖项以及 QueryDSL 相关依赖项:

<!-- mongoDB dependencies -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- Add support for Mongo Query DSL -->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-mongodb</artifactId>
    <version>5.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.mongodb</groupId>
            <artifactId>mongo-java-driver</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>5.0.0</version>
</dependency>

除了这些依赖项之外,我们还需要在 pom.xml 文件中添加构建插件。这些插件有助于动态生成 Q 类,这对于 QueryDSL 正常工作来说是必需的。以下插件需要添加:

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <dependencies>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>5.0.0</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources/apt</outputDirectory>
                <processor>org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor</processor>
                <logOnlyOnError>false</logOnlyOnError>
            </configuration>
        </execution>
    </executions>
</plugin>

还需要在项目的 POM 文件中添加 GraphQL 相关依赖项。以下 GraphQL 依赖项需要添加:

<!-- GraphQL dependencies -->
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>11.0</version>
</dependency>
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-spring-boot-starter-webmvc</artifactId>
    <version>1.0</version>
</dependency>

如前述代码块所示,要在 Java 中实现 GraphQL 服务器,我们需要导入 graphql-java 依赖项和名为 graphql-java-spring-boot-starter-webmvc 的 GraphQL Spring Boot Starter JAR 文件。

现在我们已经添加了所有必要的依赖项,我们将创建 Spring Boot 应用程序的入口点,即 Main 类,如下所示:

@SpringBootApplication
public class GraphqlDaaSApp {
    public static void main(String[] args) {
        SpringApplication.run(GraphqlDaaSApp.class,args);
    }
}

首先,我们将创建 MongoConfig 类,它创建两个 MongoClientMongoTemplate 类型的 Mongo Spring Bean,如下面的代码块所示:

@Bean
public MongoClient mongo() throws Exception {
    final ConnectionString connectionString = new ConnectionString(mongoUrl);
    final MongoClientSettings mongoClientSettings = MongoClientSettings.builder().applyConnectionString(connectionString).serverApi(ServerApi.builder()
            .version(ServerApiVersion.V1)
            .build()).build();
    return MongoClients.create(mongoClientSettings);
}
@Bean
public MongoTemplate mongoTemplate() throws Exception {
    return new MongoTemplate(mongo(), mongoDb);
}

现在,我们将创建一个名为 Application 的 POJO 类,它代表数据模型。它应该由 org.springframework.data.mongodb.core.mapping.Document 注解和 com.querydsl.core.annotations.QueryEntity 注解标注。以下代码表示 Application 实例:

@QueryEntity
@Document(collection = "newloanrequest")
public class Application {
    @Id
    private String _id;
    private String applicationId;

在这里,@Document 表示 POJO 是一个映射到 MongoDB 文档的 Bean,而 @QueryEntity 是必需的,以启用 QueryDSL 在 Application Bean 上的查询功能。

现在,就像在第九章中讨论的基于 REST 的解决方案一样,将 MongoDB 数据作为服务公开,我们必须创建一个 ApplicationRepository 接口,它扩展了 MongoRepositoryQuerydslPredicateExecutor 接口。使用这个类,我们将定义两个方法,使用 QueryDSL 从 MongoDB 获取应用程序数据。以下代码片段是 ApplicationRepository 类的:

public interface ApplicationRepository extends MongoRepository<Application, String>, QuerydslPredicateExecutor<Application> {
    @Query(value = "{ 'applicationId' : ?0 }")
    Application findApplicationsById(String applicationId);
    @Query(value = "{ 'id' : ?0 }")
    List<Application> findApplicationsByCustomerId(String id);
}

我们将跳过解释这个仓库接口,因为它与我们之前在第十章创建的接口相同。

现在我们已经完成了 DAO 层的开发,让我们创建一个名为 helper 的包。在 GraphQL Java 应用程序中,我们需要两种类型的类——一个是 GraphQL 提供者,另一个是 GraphQL 数据获取器。在这里,我们将从 helper 包开始编写提供者类。在 GraphQLProvider 类中,首先,我们将定义一个 graphql.GraphQL 类型的属性,并在 GraphQLProvider Bean 由 Spring Boot 初始化时立即初始化它。以下是这个的代码片段:

...
import graphql.GraphQL;
...
@Component
public class GraphQLProvider {
...
private GraphQL graphQL;
@PostConstruct
public void init() throws IOException {
    URL url = Resources.getResource("schema.graphqls");
    String sdl = Resources.toString(url, Charsets.UTF_8);
    GraphQLSchema graphQLSchema = buildSchema(sdl);
    this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
private GraphQLSchema buildSchema(String sdl) {
    TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
    RuntimeWiring runtimeWiring = buildWiring();
    SchemaGenerator schemaGenerator = new SchemaGenerator();
    return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
private RuntimeWiring buildWiring() {
    return RuntimeWiring.newRuntimeWiring()
            .type(newTypeWiring("Query")
                    .dataFetcher("byApplicationId", graphQLDataFetchers.getApplicationbyApplicationIdDataFetcher())
                    .dataFetcher("byCustomerId",graphQLDataFetchers.getApplicationsbyCustomerIdDataFetcher()))
            .build();
}

一个 GraphQL 服务器应该有一个强类型、定义良好的模式(请参阅关于 GraphQL 模式的早期讨论)。在这里,在 init 方法中,我们从资源中加载 GraphQL 模式。使用 com.google.common.io.Resources 的实用方法读取并存储 GraphQL 模式定义在名为 sdl 的字符串中。然后,从 sdl 对象中派生出 GraphQLSchema 对象,该对象使用 buildSchema 方法构建。

buildSchema 方法中,使用 SchemaParser 解析 sdl 对象并将其转换为 TypeDefinitionRegistry 对象。运行时连接是将数据获取器、类型解析器和自定义标量连接起来的过程。首先,我们使用 buildWiring 方法构建完成 GraphQL 模式所需的连接。然后,使用 SchemaGenerator.makeExecutableSchema,我们创建一个具有所需 runtimeWiringGraphQLSchema 对象。

必要的参考资料

通常,为了完全创建一个可执行的 GraphQL 模式,可能需要三种类型的绑定。它们如下所示:

  • 数据获取器,是帮助为 GraphQL 模式获取数据的接口。

  • 类型解析器,是自定义方法,用于解析 GraphQL 字段的值。

  • 自定义标量,指的是任何自定义的数据类型。在 GraphQL 中,数据类型被称为 标量

现在,让我们讨论 buildWiring 方法的实现。此方法通过将 GraphQL 数据获取器附加到 GraphQL 模式中的两个不同字段(byApplicationIdbyCustomerId)来创建一个新的 Runtimewiring(在本章中之前讨论了此用例的 GraphQL 模式)。

最后,使用此 GraphQLSchema,我们构建并实例化 GraphQL 属性。现在,我们可以通过返回此 GraphQL 对象来公开一个名为 GraphQL 的 Bean,如下所示:

@Bean
public GraphQL graphQL() {
    return graphQL;
}

在实现 buildWiring 方法时,调用了 GraphQLDataFetcher 类中的两个方法 getApplicationbyApplicationIdDataFetchergetApplicationsbyCustomerIdDataFetcher。因此,让我们讨论一下 GraphQLDatafetcher 类是如何实现的。所有数据获取器方法都必须返回一个 graphql.schema.DataFetcher 类型的对象。DataFetcher 接口的定义如下:

@PublicSpi
public interface DataFetcher<T> {
    T get(DataFetchingEnvironment var1) throws Exception;
}

如前述代码块所示,DataFetcher 接口只有一个方法,该方法接受一个 graphql.schema.DataFetchingEnvironment 类型的参数。因此,我们可以在 Java 中将其实现为一个 Lambda 函数。在我们的案例中,我们调用 applicationRepository 类来获取用于填充我们发布的 Application 对象所需的数据。以下代码展示了 getApplicationbyApplicationIdDataFetcher 的实现:

public DataFetcher getApplicationbyApplicationIdDataFetcher() {
    return dataFetchingEnvironment -> {
        String applicationId = dataFetchingEnvironment.getArgument("applicationId");
        return applicationRepository.findApplicationsById(applicationId);
    };
}

在实现 getApplicationbyApplicationIdDataFetcher 方法时,我们返回一个 Lambda 函数,该函数接受 dataFetchingEnvironment 参数。数据获取器为字段编写的所有参数都可以通过 DataFetchingEnvironment 类的 getArgument 方法访问。在这种情况下,它正在获取 applicationId 参数。最后,如代码片段所示,我们使用 applicationRepository 从 MongoDB 获取数据。类似的逻辑用于编写 getApplicationsbyCustomerIdDataFetcher 方法。

现在,我们需要将包含 GraphQL 模式的 schema.graphqls 文件保存在 resource 文件夹中。

最后,我们需要定义 application.yaml 文件来运行 Spring Boot 应用程序。application.yaml 文件如下所示:

grahqldaas:
  mongoUrl: mongodb+srv://<mongodburl>/CRRD?retryWrites=true&w=majority
  mongoDb: CRRD

现在我们已经开发出了 GraphQL 服务器应用程序,让我们探索如何测试它。

首先,通过运行 Main 类来启动应用程序,如下截图所示:

图 10.11 – 运行 GraphQL 服务器应用程序

图 10.11 – 运行 GraphQL 服务器应用程序

现在,为了测试应用程序,请打开 GraphQL Playground 并输入 DaaS 端点。DaaS 端点应如下所示:

http://<host_name>:8080/graphql

一旦你在 GraphQL Playground 上点击此 URL 并输入适当的 graphql 请求有效负载,你将在 GraphQL Playground 中获得结果,如下截图所示:

图 10.12 – 使用 GraphQL Playground 测试 GraphQL DaaS

图 10.12 – 使用 GraphQL Playground 测试 GraphQL DaaS

如我们所见,在调用 GraphQL 服务应用程序时,客户端发送它想要获取的字段列表。正如请求有效负载所示,尽管 GraphQL 服务器应用程序支持更多的字段,但在本例中,客户端只请求了四个字段 – 即 applicationIdgenderCodeidriskScore。因此,GraphQL 仅解析并发送这四个字段回消费者。GraphQL 的这一特性有助于克服过度获取或不足获取的问题(通常在基于 REST 的 DaaS 中看到)。

此外,GraphQL 支持在单个调用中执行多个功能。在我们的用例中,我们有两个功能 – 通过 applicationId 获取应用程序和通过 customerid 获取客户的全部应用程序。这两个功能都可以使用 GraphQL 在单个调用中实现。以下截图展示了这一示例:

图 10.13 – 在单个 GraphQL 调用中支持多个业务操作

图 10.13 – 在单个 GraphQL 调用中支持多个业务操作

如我们所见,两个字段 – byApplicationIdbyCustomerId – 都可以在单个 GraphQL 调用中请求,该调用在单个 GraphQL 响应中获取这两个字段的数据。这减少了 GraphQL 服务的调用次数,客户端可以通过减少对 GraphQL 服务器应用程序的调用来提高其应用程序的性能。

除了这两个优点之外,GraphQL 还使得架构和文档的共享变得容易。如图所示,在最右侧有两个侧边标签,分别称为 DOCSSCHEMASCHEMA 可以显示服务器支持的 GraphQL 架构。它告诉我们哪些数据点可以作为此 GraphQL API 的一部分。以下截图显示了客户端如何查看 GraphQL 服务的架构:

图 10.14 – 从 GraphQL Playground 检查 GraphQL 架构

图 10.14 – 从 GraphQL Playground 检查 GraphQL 架构

除了这个之外,文档是 GraphQL 提供的另一个酷炫功能。在 REST 世界中,仅与客户端共享 API 是不够的。因此,我们需要单独构建和维护文档(无论是使用 Swagger 还是其他方式)并共享它。然而,GraphQL 允许你轻松维护和发布文档,如下面的截图所示:

图 10.15 – 简单分享 GraphQL 文档

图 10.15 – 简单分享 GraphQL 文档

更有趣的是,GraphQL 文档的配置和维护非常简单。我们只需在每个字段上方添加一个文档注释,并查询它以支持文档。例如,applicationId 字段的文档可以轻松地集成到 GraphQL Schema SDL 中,如下面的代码块所示:

...
"""
Type Application represents the entity/schema of the response payload for both byApplicationId and byCustomerId fields
"""
type Application {
    """
    Application Id which uniquely identifies each credit card application
    """
    applicationId: ID!
...

如我们所见,所有文档都写在文档注释中,注释以三个双引号("")开始,以三个双引号("")结束。GraphQL 自动使用 schema SDL 发布文档,因此维护和发布文档变得超级简单和容易。

在本节中,我们学习了如何使用 Java 开发 GraphQL 服务器应用程序。我们还了解了用于开发 GraphQL 模式的 GraphQL DSL。最后,我们学习了如何有效地使用 GraphQL Playground 测试和验证 GraphQL DaaS API。现在,让我们总结一下本章所学的内容。

摘要

在本章中,我们学习了 GraphQL 的基本概念。首先,我们了解了 GraphQL 如何克服基于 REST 的 DaaS 的陷阱以及它提供的优势。然后,我们讨论了何时选择 GraphQL 作为首选解决方案。最后,我们学习了如何使用 Java 开发 GraphQL 服务器应用程序以及如何使用 GraphQL Playground 测试该应用程序。

现在我们已经学习了如何为数据摄取场景和数据发布场景设计和发展数据工程解决方案,在下一章中,我们将讨论性能工程并学习如何使用数据驱动的方法来做出架构决策。

第四部分 – 选择合适的数据架构

在本书的最后一节,你将学习如何衡量解决方案并确定解决方案的效率。你还将学习如何向可能不太懂技术的领导/客户沟通和展示他们的解决方案。

本节包括以下章节:

  • 第十一章**,衡量性能和基准测试你的应用程序

  • 第十二章**,评估、推荐和展示你的解决方案

第十一章:测量性能和基准测试你的应用程序

在前面的章节中,我们学习了如何为数据摄取和数据发布问题设计解决方案。我们还讨论了如何选择正确的技术栈和平台来实现成本效益高且可伸缩的解决方案。除此之外,我们还了解了各种数据摄取的架构模式。我们还讨论了数据治理和数据安全。然而,作为架构师,我们的工作不仅仅是创建可伸缩的解决方案,还要创建高性能的解决方案。这就是性能工程在数据架构师工具箱中发挥作用的地方。

在本章中,我们将讨论性能工程的意义以及为什么它如此重要。我们还将了解它与性能测试有何不同。然后,我们将学习如何规划我们的性能测试和其他性能工程活动。然后,我们将简要讨论性能基准测试技术。最后,我们将了解在数据摄取或数据发布过程中缓解或避免各种性能瓶颈的常见方法来微调我们解决方案的性能。

到本章结束时,你将了解性能工程是什么以及如何为其制定计划。你将知道如何进行基准测试和发布性能结果。你将了解可用的性能工具以及何时使用它们。最后,你将了解如何微调性能以创建针对数据问题的优化、高性能解决方案,以及如何进行性能基准测试。

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

  • 性能工程与规划

  • 性能工程工具

  • 发布性能基准

  • 优化性能

性能工程与规划

软件性能工程SPE)是一种系统性和量化的基于软件的方法,旨在设计、架构和实施解决方案,以最佳方式满足各种非功能性需求NFRs),如性能、容量、可伸缩性、可用性和可靠性。在本书的早期,我们讨论了可伸缩性、可用性和可靠性。在本章中,我们将重点关注性能和容量。或者,SPE 被定义为一种主动和持续的性能测试和监控过程。它涉及不同的利益相关者,如测试人员、开发者、性能工程师、业务分析师和架构师。正如我们将在本章后面讨论的,性能工程是一个无缝的过程,它与开发活动并行运行,为开发者和架构师提供持续的反馈循环,以便在软件开发过程中吸收性能需求。

既然我们已经定义了性能工程,那么让我们讨论一下性能工程生命周期的各个阶段以及它是如何与软件开发生命周期(SDLC)活动并行的。以下图表展示了这一点:

图 11.1 – 性能工程生命周期

图 11.1 – 性能工程生命周期

如我们所见,以下是性能工程生命周期的各个阶段:

  • 收集 NFR:为了开发高性能的数据管道,了解解决方案的非功能性需求非常重要。例如,在设计 DaaS 时,了解所需的每秒事务数(TPS)和系统平均并行负载更有意义。在设计管道时,可能还有另一个要求,即它应该能够与 Datadog 集成以进行监控。在这种情况下,应选择支持 Datadog 集成的技术堆栈。为了收集所有这些信息,我们需要产品负责人、系统分析师、领域专家(SMEs)、架构师、SPE 团队和 DevOps 之间有紧密的联系。

  • 针对性能和性能建模进行设计:在瀑布模型中,性能测试和优化是在功能性和集成测试周期结束后进行的。这种方法的缺点是,与小型数据集完美工作的架构在负载测试方面可能根本不起作用。因此,我们必须再次重新设计解决方案。这导致在努力、时间和金钱方面造成了大量的浪费。由于现代数据工程团队越来越多地采用敏捷方法,同时采用性能工程的机会也增加了。在收集了非功能性需求之后,针对性能的设计需要满足以下标准:

    • 以最佳速度满足 NFRs

    • 解决方案必须足够可扩展,即使在负载增加的情况下也能保持相似的性能

我们的架构应该设计成随着业务数据的指数增长而扩展。这使设计失败、扩展应用而不是向上扩展应用以及云中自动扩展资源等想法变得生动起来。在以性能为导向的设计中,通常还应用了另一种常见方法,即性能建模

性能建模是基于数据增长率中涉及的特征来建模应用性能的过程,目的是找出可能的 SLA 违规情况。它还有助于验证设计决策和基础设施决策。让我们来看一个例子——假设输入消息以速率x进入一个应用,该应用的响应速率是y。如果消息的到达速率是其自身的四倍会发生什么?我们如何确保相同的响应时间?我们需要将服务速率增加到四倍还是增加到两倍?这种决策可以通过性能建模来完成。在现代数据架构中,应用往往运行在容器化或虚拟化平台上,这可以确定未来如何分配资源以进行扩展。

  • 模块化性能测试和优化:在这个阶段,使用 NFRs,非功能性测试(NFTs)被分类为压力测试、负载测试或浸泡测试。一旦测试用例被分类,就准备一个测试用例文档,该文档将 NFTs 与运行测试场景的详细步骤相对应。可选地,创建一个 NFT 到 NFR 性能矩阵。然后,使用这些文档,可以在模块开发并功能测试时测试 NFRs。运行这些测试用例与功能测试相结合,可以确保早期发现任何性能问题。优化和性能调整可以根据开发或 DevOps 团队的需求进行。

  • 完全集成的性能测试:一旦完成模块化性能测试,就可以运行端到端性能测试。随着场景的质量保证(QA)完成,该场景将移动到测试集成性能。通常,在这一层,不需要太多调整或优化。然而,在此次活动期间,可能需要优化整体端到端流程。

  • 监控和容量管理:一旦管道投入生产,我们需要持续监控和评估任何异常活动。根据未来和当前的工作负载状态,可以预测并适当管理容量。

在本节中,我们学习了性能工程生命周期的各个阶段。接下来,让我们了解性能工程与性能测试之间的区别。

性能工程与性能测试

性能工程与性能测试之间的关键区别如下:

  • 性能测试是一个 QA 活动,它运行测试用例来检查 NFRs 的质量并找出任何问题。它是为了检查系统在负载生产方面的行为,并预测在重负载期间可能出现的任何问题。另一方面,性能工程是一个与 SDLC 同步进行的整体过程。与性能测试不同,性能工程从分析阶段开始。它还促进了在开发生命周期早期发现性能问题。

  • 性能测试遵循软件开发过程的水下模型。它仅在软件开发和功能测试完成后进行。这种方法的缺点是,如果应用程序在生产负载下无法正常工作,我们可能需要重新设计和重新实现,这会导致不必要的耗时和财务损失。然而,性能工程是一个持续的过程,与 SDLC 的所有阶段相辅相成,通常由敏捷团队实施,并具有持续反馈循环到开发和设计团队。通过提供性能需求的前期分析和问题的早期发现,性能工程帮助我们节省时间和金钱。

  • 性能测试由 QA 团队执行,而性能工程涉及架构师、开发者、SMEs、性能工程师和 QA。

在本节中,我们学习了性能工程,为什么需要它,以及它的生命周期。我们还讨论了性能测试和性能工程之间的区别。在下一节中,我们将简要讨论市场上可用的性能工程工具。

性能工程工具

在本节中,我们将简要讨论各种性能工程工具。

以下是可以用的不同类别的性能工程工具:

  • 可观测性工具:这些工具监控并收集关于应用程序的信息。这些工具可能有助于识别瓶颈、跟踪吞吐量和延迟、内存使用等情况。在数据工程中,每个系统都是不同的,吞吐量和延迟的要求也不同。可观测性工具有助于确定我们的应用程序在吞吐量或延迟方面是否落后,以及落后多少。它们还有助于识别可能仅在长期运行中、在生产中出现的隐藏问题。例如,应用程序中的微小内存泄漏在部署后的几天内可能不明显。当这样的应用程序持续运行时,JVM 堆空间的旧区域会缓慢增加,直到超过堆空间。以下是一些可观测性工具的例子:

    • Datadog:这是一个非常流行的监控工具,可以进行应用程序监控、网络监控、数据库监控、容器监控、无服务器监控等。它具有内置的仪表板和根据您的需求自定义仪表板的功能。它具有警报、日志集成和其他酷炫功能。这是一个付费产品,提供企业级支持。更多信息,请访问www.datadoghq.com/

    • Grafana 与 Graphite/Prometheus: 这是一个开源的监控和仪表盘工具。它可以与 Prometheus 或 Graphite 一起使用。Prometheus 和 Graphite 都是开源的监控工具包,有助于生成和发布各种指标。Prometheus 有一个数据收集模块,可以拉取数据以生成指标。另一方面,Graphite 只能被动监听数据,但不能收集它。一些其他工具,如 Collectd,需要收集并将数据推送到 Graphite。查询 Graphite 指标时使用函数,而 PromQL 用于查询 Prometheus 指标。这些生成的指标与 Grafana 集成,以创建不同类型的仪表盘,例如统计仪表盘、时间序列监控、状态时间线和历史记录、警报仪表盘等。更多信息,请访问grafana.com/

    • Dynatrace: Dynatrace 是另一个具有与 Datadog 非常相似功能的商业监控和仪表盘工具。它还提供了一个 AI 助手,可以帮助动态回答您的查询。它支持 DevOps 和 CloudOps 集成,如 CI/CD 管道等。更多信息,请访问www.dynatrace.com/

    • Confluent Control Center: 这是一个内置的 Confluent Kafka 监控工具,随 Confluent Kafka 的企业(授权)版本一起提供。它有助于监控各种 Kafka 组件,如主题、生产者、消费者、Kafka Connect 集群、KSQL 查询以及整体 Kafka 集群健康。更多信息,请访问docs.confluent.io/platform/current/control-center/index.xhtml

    • Lenses: 这是一个提供 Kafka 主题、集群和流可观测性的工具。Lenses 不仅支持可观测性,还支持 Kafka 集群的 DataOps。更多信息,请访问docs.lenses.io/

  • 性能测试和基准测试工具: 这些工具用于进行各种性能测试,例如烟雾测试、负载测试和压力测试。其中一些还提供基准测试功能。以下是一些可用于性能测试和基准测试的工具:

    • JMeter: 这是一个用 Java 编写的免费开源工具,用于性能测试。它特别适用于大数据性能测试以及任何 API 的性能测试,如 REST 和 GraphQL。JMeter Hadoop 插件可用于进行大数据性能测试。我们可以运行负载测试并将结果导出为多种格式的文件。更多信息,请访问jmeter.apache.org/

    • SoapUI:这是另一个用于功能测试的开源性能测试工具。它支持对 REST、SOAP 和 GraphQL 等 Web 服务的多用户、线程和并行负载测试。它还有一个名为 ReadyAPI 的专业商业版,该商业版支持更多高级功能和针对测试 GraphQL 和 Kafka 流应用的特定插件。更多信息,请访问www.soapui.org/

    • Blazemeter:这是另一个开源性能测试工具,用于运行微服务(如 REST 或 GraphQL API)的可扩展测试。它还支持一些监控功能。更多信息,请访问www.blazemeter.com/

    • LoadRunner:LoadRunner 是 Microfocus 的一个商业产品,它能够为各种工作负载和各种类型的应用程序提供负载测试。它支持超过 50 种应用程序的测试,例如微服务、HTML、MQTT、Oracle 等。更多信息,请访问www.microfocus.com/en-us/products/loadrunner-professional/overview

    • SandStorm:这是一个商业基准测试和企业级性能测试工具。它对各种应用程序和工具提供巨大支持,从 JDBC 连接到大数据测试。它支持 Cassandra、HBase 和 MongoDB 等 NoSQL 数据库,以及 Hadoop、Elasticsearch 和 Solar 等大数据组件。它还提供对 Kafka 和 RabbitMQ 等消息平台的支持。更多信息,请访问www.sandstormsolution.com/

    • kafka-producer-perf-test.shkafka-consumer-perf-test.sh。前一个脚本用于测试生产者性能,后一个脚本用于测试消费者性能。要了解更多关于此功能的信息,请访问docs.cloudera.com/runtime/7.2.10/kafka-managing/topics/kafka-manage-cli-perf-test.xhtml

    • OpenMessaging Benchmark Framework:这是一套工具,允许您轻松地在云上对分布式消息系统进行基准测试。它支持 Apache Kafka、Apache Pulsar、Apache RocketMQ 等多个消息平台。更多信息,请访问openmessaging.cloud/docs/benchmarks/

在本节中,我们简要讨论了可以用于性能工程的多款工具。现在我们已经对性能工程有了相当的了解,包括如何进行以及我们可以使用的工具,让我们看看如何利用我们所获得的知识来创建性能基准。

发布性能基准

在本节中,我们将学习关于性能基准测试以及如何开发和发布它们。我们将从定义性能基准是什么开始。软件性能测试中的基准被定义为评估软件解决方案质量度量的参考点。它可以用来对同一问题的不同解决方案进行比较研究,或比较软件产品。

基准测试就像统计数据或度量标准,用于确定软件的质量。就像在足球等运动中,每位球员的价值或质量是通过各种统计数据来确定的,例如他们总共进球数、每场比赛进球数、锦标赛进球数等。这些统计数据有助于在不同规格下比较不同的球员。同样,软件世界中的基准测试有助于在特定条件下确定软件产品或解决方案的价值。

现在,让我们实际运行一些性能测试并创建性能基准。我们将使用我们在第九章,“将 MongoDB 作为服务公开”中开发的 REST API 来进行性能测试。我们将使用 JMeter 来测试和记录应用程序的性能。我们选择 JMeter,因为它易于使用,并且是一个基于 Java 的开源产品。

按照以下步骤进行性能测试和基准测试:

  1. 添加线程组:首先,我们必须添加一个线程组。要添加线程组,我们需要执行以下操作:

    1. 启动 JMeter 工具。

    2. 从左侧的树中选择测试计划

    3. 右键单击测试计划。然后,点击添加 | 线程(用户) | 线程组以添加线程组,如图下所示:

图-11.2 – 添加线程组

图-11.2 – 添加线程组

线程组创建向导中,填写线程组的名称并输入线程属性,如图下所示:

图 11.3 – 线程组创建向导

图 11.3 – 线程组创建向导

如您所见,线程数(用户)循环次数预热时间(秒)已经配置。让我们尝试理解这些术语:

  • 线程数(用户)对应于同时发起请求的用户数量,如图下所示:

图 11.4 – 线程数与循环次数的关系

图 11.4 – 线程数与循环次数的关系

  • 5。因此,一个用户向服务器发送 5 个请求。

  • 5050。因此,在启动下一个用户之前有一个 1 秒的延迟。

现在我们已经配置了线程组,让我们尝试添加 JMeter 元素。

  1. 配置 JMeter 元素:现在,我们将添加一个名为 HTTP 请求的 JMeter 配置元素。这有助于在负载测试中进行 REST 或 Web 服务调用。右键点击 REST 线程组 并选择 添加 | 采样器 | HTTP 请求,如图下截图所示:

图 11.5 – 添加采样器

图 11.5 – 添加采样器

设置 http8080(如果运行在本地机器上)、GET 和我们的 路径,如图下截图所示:

图 11.6 – 配置 HTTP 请求采样器

图 11.6 – 配置 HTTP 请求采样器

现在我们已经配置了 HTTP 请求采样器,我们需要添加一些 JMeter 元素来监控和发布性能报告。为此,我们必须配置一个或多个监听器。

  1. 添加监听器:要创建性能基准,我们必须添加三个监听器,如下所示:

    1. 总结报告

    2. 聚合报告

    3. 响应时间图

添加监听器的步骤与配置任何新的监听器类似。在此,我们将演示如何添加聚合性能基准,如下所示:

  1. 右键点击 REST 线程组,然后选择 添加 | 监听器 | 聚合报告,如图下截图所示:

图 11.7 – 添加监听器

图 11.7 – 添加监听器

  1. 聚合报告 的配置向导中将报告重命名为 聚合性能基准,如图下截图所示:

图 11.8 – 配置聚合报告监听器

图 11.8 – 配置聚合报告监听器

按照类似的步骤设置 总结报告响应时间图 监听器。通过这样做,我们将准备好运行测试并生成报告。

  1. 运行负载测试并创建基准:通过点击以下截图中用红色圈出的启动符号来运行测试:

图 11.9 – 通过点击运行按钮运行性能测试

图 11.9 – 通过点击运行按钮运行性能测试

在成功执行性能测试后,将生成以下报告:

  • 总结报告:此报告提供了性能基准的总结,并显示了请求的平均、最小和最大响应时间。您还可以看到应用程序的平均吞吐量。以下截图显示了基准结果的总结:

图 11.10 – 生成的总结基准报告

图 11.10 – 生成的总结基准报告

注意前一个截图中的 # 样本 列;其值为 250。样本值的计算使用以下公式:

  • 综合报告:这表示聚合基准测试报告。除了显示平均、中位数和最大响应时间外,它还有如90%线95%线等列。90%线列表示 90%请求的平均响应时间。它假设 10%的请求包含异常值。同样,95%线假设 5%的请求是异常值。以下截图显示了聚合的性能基准测试:

图 11.11 – 生成的聚合基准测试报告

图 11.11 – 生成的聚合基准测试报告

  • 响应时间图:性能基准测试可以包含多个图表或表格来测试应用性能。响应时间图描绘了在不同时间线记录的响应时间:

图 11.12 – 生成的响应时间图

图 11.12 – 生成的响应时间图

在性能基准测试活动中,很多时候我们会进行对比研究,这些报告被用来创建一个综合报告或图形可视化,比较两个不同解决方案的性能基准。

在本节中,我们学习了为什么需要基准测试,以及在我们对解决方案进行基准测试时需要考虑什么。基准测试为我们提供了一种将我们的应用程序性能分类或分类为好、坏或一般的方法。在下一节中,我们将了解如何提高我们的性能并优化我们的数据工程解决方案。

优化性能

做基准测试、性能测试和监控系统应用和系统的主要原因是出于一个目标——优化性能,以便系统能够发挥其最佳潜力。卓越软件与普通软件之间的区别在于系统调整得有多好以实现更好的性能。在本节中,我们将了解您可以使用各种技术来微调您的数据工程管道。尽管性能调整是一个广泛的话题,但当涉及到各种数据工程解决方案时,我们将尝试涵盖基于 Java 的数据工程解决方案优化的基础知识。在接下来的子节中,我们将简要介绍各种性能调整技术。

Java 虚拟机(JVM)和垃圾回收优化

Java 虚拟机JVM)性能调整是调整各种 JVM 参数或参数以适应我们应用程序的需求,以便它能发挥最佳性能的过程。

JVM 调整涉及两种优化,如下所示:

  • 堆空间优化

  • 垃圾回收GC)优化

但在我们讨论这些优化之前,值得注意的是,JVM 调整应该是调整应用程序性能的最后手段。我们应该从调整应用程序代码库、数据库和资源可用性开始。

JVM 堆空间概述

在我们深入探讨 JVM 和 GC 调优之前,让我们花些时间了解 JVM 堆空间。

以下图表显示了 JVM 堆空间的外观:

图 11.13 – JVM 堆空间

图 11.13 – JVM 堆空间

如我们所见,JVM 堆空间被划分为四个部分,如下所示:

  1. 元空间或 Perm 空间

  2. Eden Space

  3. Survivor 空间

  4. Tenured 空间

元空间(在较老的 JDK 版本中称为 Perm 空间)存储堆的元数据。

Java 对象根据其存活时间从 Eden Space 提升到 Tenured Space。以下步骤显示了 Java 对象如何被提升:

  1. Java 应用程序新创建的对象被存储在Eden Space中。

  2. Eden Space满时,会发生一个小的 GC 事件,并且仍然被 Java 应用程序引用的对象被提升到Survivor Space 0

  3. 再次,在下一个周期中,当Eden Space满时,会触发第二个小的 GC 事件。首先,它会将所有仍然被应用程序引用的对象从Survivor Space 0移动到Survivor Space 1,然后它将引用对象从Eden Space提升到Survivor Space 0

  4. 当引用对象从Survivor Space 1提升到Tenured Space时,会发生一个主要的 GC 事件。被提升到 tenured space 的对象被称为老年代对象。Eden SpaceSurvivor Space 0Survivor Space 1对象是年轻代对象。

之前,我们讨论了如何通过小 GC 和大 GC 来释放堆空间。但是,GC 是否有单一的方式或多种方式?如果是的话,我们应该选择哪种,以及在何时选择?我们将在下一节中探讨这个问题。

重要注意事项

垃圾收集是一个自动确定 JVM 中哪些内存不再被 Java 应用程序使用,并将该内存回收以供其他用途的过程。

垃圾收集器类型

以下是一些不同的 GC 类型:

  • 串行垃圾收集器:单个 GC 适用于单线程应用程序。在执行垃圾收集时,它会冻结所有应用程序线程,并使用单个线程进行操作。

  • 并行垃圾收集器:并行 GC 也会冻结应用程序的所有线程,但使用多个线程进行垃圾收集。因此,应用程序线程的暂停间隔大大减少。它旨在适用于多处理器环境或具有中等和大数据量的多线程环境。

  • 并发标记清除(CMS)垃圾收集器:从其名称中可以看出,垃圾收集工作与应用程序并发执行。因此,它不需要应用程序线程暂停。相反,它与应用程序线程共享线程以进行并发清除执行。然而,它需要短暂暂停应用程序线程进行初始标记暂停,在那里它最初标记活动对象。然后,第二次暂停,称为remark 暂停,暂停应用程序线程,并用于查找需要收集的任何 Java 对象。这些 Java 对象是在并发跟踪阶段创建的。以下图表解释了串行、并行和 CMS GC 之间的区别:

图 11.14 – 单一、并行和 CMS 垃圾收集器的区别

图 11.14 – 单一、并行和 CMS 垃圾收集器的区别

如我们所见,串行收集器使用单个线程进行垃圾收集,同时暂停所有应用程序线程。并行收集器暂停应用程序线程,但由于它使用多个线程来完成工作,因此暂停时间更短。另一方面,CMS 在初始标记阶段之后与应用程序线程并发运行。

  • G1 垃圾收集器:这是一个相对较新的 GC,在 Java 7 及以后的版本中引入。它依赖于一个用于并发垃圾收集的新算法。它与应用程序线程并行运行其较长时间的工作,并通过暂停线程来运行较快的任务。它使用内存清理的驱逐风格工作。对于内存清理的驱逐风格,G1 收集器将堆划分为区域。每个区域都是一个小的、独立的堆,可以动态分配给 Eden、Survivor 或 Tenured Space。以下图表显示了 G1 收集器如何看待数据:

图 11.15 – G1 垃圾收集器将堆空间划分为区域

图 11.15 – G1 垃圾收集器将堆空间划分为区域

GC 简单地从某一区域复制数据到另一区域。这需要保留并标记旧区域为空白。

  • Z 垃圾收集器:这是一个用于非常可扩展的低延迟实现的实验性 GC。

在 GC 方面,以下几点应予以注意:

  • 小型 GC 事件应尽可能收集尽可能多的已死亡对象,以减少完全 GC 的频率。

  • 当 GC 事件有更多可用内存时,可以实现更有效的对象清理。更有效的对象清理确保完全 GC 事件的频率更低。

  • 在使用 GC 进行性能调整的情况下,你只能调整三个参数中的两个 – 那就是吞吐量、延迟和内存使用。

接下来,让我们看看如何使用 GC 调整性能。

使用 GC 调整性能

使用 GC 设置进行性能调整也称为 GC 调整。我们必须遵循以下步骤来执行 GC 调整:

  1. 调查内存占用:查找由于 GC 引起的任何性能问题的最常用方法之一是内存占用,它存在于 GC 日志中。GC 日志可以在不影响性能的情况下从 Java 应用程序中启用和生成。因此,它是调查生产中性能问题的流行工具。您可以使用以下命令启用 GC 日志:

    -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:<filename>
    

下面的屏幕截图显示了典型的 GC 日志看起来像什么:

图 – 11.16 – 示例 GC 日志

图 – 11.16 – 示例 GC 日志

在前一个屏幕截图中,每一行都显示了各种 GC 信息。让我们关注日志的第三行,它显示了一个完全 GC 事件:

图 – 11.17 – GC 日志语句的解剖

图 – 11.17 – GC 日志语句的解剖

如前图所示,让我们分析 GC 日志语句并了解其各个部分:

  • 2022-07-01T16:46:0.434: GC 事件发生的日期和时间戳。

  • Full GC: 此字段描述了 GC 的类型。它可以是完全 GC 或 GC。

  • [PSYoungGen: 10752K->0K(141824K)]: GC 事件发生后,年轻代中使用的所有空间都被回收了。括号内的值(141824K)表示年轻代分配的总空间。

  • [ParOldGen: 213644K->215361K(459264K)]: GC 运行后,旧代的使用空间从213644K增加到215361K。旧代分配的总内存为459264K

  • 224396K->215361K(601088K): GC 运行后,使用的总内存从224396K减少到215361K。总分配内存为601088K

  • [Metaspace: 2649K->2649K(1056768K)]: 由于 GC,元空间没有回收任何内存。总共分配的元空间为1056768K

  • 3.4609247 secs: GC 所花费的总时间。

  • [Times: user=3.40 sys=0.02, real=3.46 secs]: 这部分日志语句告诉我们垃圾收集所花费的时间。user时间告诉处理器 GC 执行所花费的时间。sys时间表示 I/O 和其他系统活动所花费的时间。最后,real时间表示完成 GC 事件所花费的总时间。

根据这些痕迹,我们可以确定是否需要增加堆空间或元空间,并指定应用程序中发生的任何内存泄漏。

  1. 内存调整:如果列出以下观察结果,则可能会发生内存泄漏:

    • JVM 堆大小频繁被填满

    • 年轻代空间正在被完全回收,但每次 GC 运行后,旧代的使用空间都在增加。

在决定是否是真正的内存泄漏问题之前,您应该使用以下命令增加堆空间:

-Xms<heap size>[unit] // for min heap size
-Xmx<heap size>[unit] //for max heap size
//unit can be g(GB),m(MB) or k(KB)

如果这不起作用,那么最可能的原因是内存泄漏。

如果我们发现年轻代空间频繁被填满,或者元空间被大量使用,我们可以计划使用以下命令更改所有这些区域的总分配空间:

  • -XX:MaxMetaspaceSize:这设置可以分配给类元数据的最大内存量。默认值是 infinite(或与堆空间相同)。

  • -XX:MetaspaceSize:设置分配的类元数据阈值的阈值,超过该阈值将触发垃圾回收。

  • -XX:MinMetaspaceFreeRatio:垃圾回收后需要可用的元空间内存区域的最小百分比。如果剩余的内存量低于阈值,元空间区域将调整大小。

  • -XX:MaxMetaspaceFreeRatio:垃圾回收后需要可用的元空间内存区域的最大百分比。如果剩余的内存量高于阈值,元空间区域将调整大小。

  • -XX:NewSize:这设置年轻代空间的初始大小。

  • -XXMaxNewSize:这指定了年轻代空间的最大大小。

  • -Xmn:这指定了整个年轻代空间的大小,即 Eden 和两个幸存空间。

  1. -XX:+G1GC 命令。

  2. -Xmx-Xms 设置为相同的值以减少应用程序暂停间隔。

  3. -XX:+AlwaysPreTouch 标志设置为 true,以便在应用程序启动时加载内存页面。

  4. 如果你正在使用 G1,检查是否是小垃圾回收(Minor GC)或完全垃圾回收(Full GC)花费了更多的时间。如果小垃圾回收花费了更多的时间,我们可以减少 -XX:G1NewSizePercent-XX:G1MaxNewSizePercent 的值。如果主要垃圾回收花费了更多的时间,我们可以增加 -XX:G1MixedGCCountTarget 标志的值,这将有助于将持久垃圾回收分散到多次运行中,并减少完全垃圾回收事件的频率。

  • 使用 -XX:MaxGCPauseMillis 属性在单个垃圾回收运行中清理更多垃圾。然而,这可能会影响你的延迟。* 通过设置 -XX:+AlwaysPreTouch-XX:+UseLargePages 标志,在应用程序启动时将内存页面加载到内存中。

重要提示

延迟是处理和发送事件、消息或数据到其目的地所花费的总时间。另一方面,吞吐量是在指定时间段内处理的记录、事件或消息的数量。

虽然 JVM 和 GC 调优是一个庞大的主题,但我们简要尝试涵盖一些重要的 JVM 和 GC 调优技术,以提高吞吐量和延迟。在下文中,我们将讨论如何优化大数据加载。

大数据性能调优

大数据性能调优是一个巨大的主题。为了简洁,我们将限制自己只介绍一些通常应用于最流行的大数据处理技术(即 Spark 和 Hive)的性能调优技巧和窍门。

Hive 性能调优

Hive 性能调优是提高和加速您 Hive 环境性能的过程和技术的集合。以下是一些常见的 Hive 性能问题:

  • 运行缓慢的查询:通常,您会注意到您的 Hive 查询需要花费大量时间才能完成。运行缓慢的查询可能有几个原因。以下是一些常见的运行缓慢查询场景及其解决方案:

    • 编写不良的查询会导致交叉连接或全外连接。当任一表的连接列有重复或查询中发生自连接时,可能会发生意外的交叉连接。尽可能优化您的 Hive 查询以避免交叉连接。

    • 如果其中一个连接表包含少量数据,可以通过应用 map-side join 来提高运行缓慢查询的速度。在 map-side join 中,较小的数据集被广播到所有 mapper 节点,以便在本地进行连接而无需大量洗牌。map-side join 的缺点是,较小表中的数据需要足够小,以便适合内存。

    • 对于适合 map-side join 的场景,我们可以进一步通过将需要连接的表进行分桶来提高速度。分桶是一种技术,根据连接列将 Hive 表中的数据分成固定数量的范围或簇。分桶表可用于 桶映射连接排序合并分桶(SMB)映射连接,这两种方式都比普通映射连接表现更好。然而,只有当一张表的总桶数是另一张表桶数的倍数时,分桶表才能相互连接。例如,Table1 有 2 个桶,Table2 有 4 个桶。由于 4 是 2 的倍数,这些表可以连接。

    • 有时,数据会迅速增长,这会导致 Hive 作业变慢。在这种情况下,map-side 操作会花费大量时间。为了克服这些问题,请使用分区。

    • 如果您注意到数据读取速度慢或数据洗牌速度相当慢,请检查所使用的 数据格式压缩。例如,Parquet 和 ORC 等列式结构比 JSON 性能高 5% 到 10%。列式格式也已知比 JSON 具有大约 90% 的更高压缩率。更多压缩的数据可以减少大量的网络延迟,从而提高整体性能。

    • 通过将 执行引擎 从 map-reduce 更改为 Tez 或 Spark,可以显著提高 Hive 的执行速度。您可以通过在 Hive 配置文件中设置以下参数来实现这一点:

      <property>
        <name>hive.execution.engine</name>
        <value>spark</value>
      </property>
      
  • TimeoutException,很可能是你遇到了小文件问题。这发生在数百万个小文件(其大小小于 128MB 的块大小)被写入 Hive 表的外部路径时。每个 HDFS 文件都有存储在 Hadoop 的 NameNode 中的元数据。Hive 表中过多的文件会导致作业读取过多的元数据。因此,作业可能由于内存溢出或超时异常而失败。在这种情况下,可以运行压缩作业(参考第七章核心批量处理模式部分,核心架构设计模式)或将数据存储在顺序文件中。

因此,我们已经讨论了各种 Hive 优化技术,这些技术用于微调 Hive 查询速度和性能。现在,让我们看看 Spark 性能调优。

Spark 性能调优

Spark 是最受欢迎的大数据处理引擎。当 Spark 应用程序得到适当调整时,它可以在保持关键流程 SLA 的同时降低资源成本。这对云和本地环境都很重要。

一个 Spark 调优作业从调试和观察 Spark 作业执行期间发生的问题开始。您可以使用 Spark UI 或任何分析应用程序(如 Datadog)来观察各种指标。

以下是一些在处理 Spark 应用程序时常用的 Spark 优化最佳实践:

  • 序列化

    • 问题:从 Hive 表或 HDFS 读取数据缓慢

    • 原因:默认的 Java 序列化器降低了从 Hive 或 HDFS 读取数据的读取速度

    • 解决方案:为了克服这个问题,我们应该将默认序列化器设置为 Kryo 序列化器,它执行更快的 Serde 操作

  • 分区大小

    • 问题:由于一个或两个执行器在众多执行器中花费了更多的时间来完成任务,Spark 作业运行缓慢。有时,作业会因为性能较慢的执行器而挂起。

    • 原因:可能的原因是数据倾斜。在这种情况下,你会发现性能较慢的执行器正在处理大部分数据。因此,数据没有很好地分区。

    • 解决方案:在进一步处理数据之前,重新分区加载到 Spark DataFrame 中的数据。

让我们看看另一个与分区相关的常见问题:

  • 问题:由于所有执行器都负载过重,所有执行器处理作业的时间都很长,Spark 作业正在变慢。

  • 原因:可能的原因是数据量过大,而你运行的 Spark 作业分区数量却很少。

  • 解决方案:重新分区 Spark DataFrame 中加载的数据,并增加分区数量。调整分区数量,直到达到最佳性能。

在 Spark 中遇到的另一个常见问题是以下内容:

  • 问题:Spark 作业需要写入单个文件作为输出,但作业在最后一步卡住了。

  • 原因:由于您使用了repartition()来创建单个输出文件,因此发生了完整的数据洗牌,这阻碍了性能。

  • 解决方案:使用coalesce()代替repartition()coalesce()避免进行完整的洗牌操作,因为它从所有其他分区收集数据并将数据复制到包含最多数据的选定分区。

  • OutofMemoryException、GC 开销内存超过或 JVM 堆空间溢出。

  • 原因:驱动器和执行器内存以及 CPU 核心的配置效率低下。

  • 解决方案:没有一种解决方案。在这里,我们将讨论可以用来避免低效的驱动器和执行器资源配置的各种最佳实践。在我们开始之前,我们将假设您对 Apache Spark 有基本的了解,并知道其基本概念。如果您是 Apache Spark 的新手,您可以阅读这篇关于 Spark 架构基础的简要博客:www.edureka.co/blog/spark-architecture/

  • 对于执行器和驱动器的大小调整,我们需要为 Spark 作业优化设置总共五个属性。它们如下:

    • 使用以下属性计算5
    5. So, in a node, the maximum number of executors will be 3 (16 cores / 5 executor-cores per executor). So, for three nodes, we will have a total of 9 executors (3 nodes x 3 executors/nodes). However, according to the best practice documentation of Spark, the executor cores per executor should be the same as the driver core. Hence, we would require 1 worker to work out of the 9 workers as the application driver. Hence, the number of executors will become 8 (9 workers – 1 driver worker). As a general formula, you can use the following: 
    

在这里,Nc 表示每个节点的核心数,Ec 表示每个执行器的执行器核心数,Tn 表示节点的总数。

在这个用例中,Nc是 16,Ec是 5,Tn是 3。因此,我们得到相同的结果 8(FLOOR(16*3/5) -1)。

通过这样,我们已经讨论了如何最优地设置驱动器和执行器属性。然而,对于在 YARN 集群上运行的 Spark 3.0 及以上版本,启用动态分配更有意义。虽然您可以设置执行器核心的最小值和最大值,但您必须让环境本身确定所需的执行器数量。在讨论过这个问题后,您可能还想设置最大执行器数量的上限(通过使用spark.dynamicAllocation.maxExecutors属性)。

  • 执行器内存或 executor-memory:为了计算最优执行器内存,我们需要了解执行器使用的总内存量。执行器使用的总内存是执行器内存和内存开销的总和:

图 11.18 – 执行器使用的总内存 = 内存开销 + 执行器内存

图 11.18 – 执行器使用的总内存 = 内存开销 + 执行器内存

执行器的内存开销默认为执行器内存大小的 10%或 384 MB 之间的较大值。现在,为了找到正确的执行器内存大小,我们需要查看 YARN 资源管理器的节点标签页。节点标签页中的每一条记录都将有一个总内存列,如下面的截图所示:

图 11.19 – 节点中执行器的可用总内存

图 11.19 – 节点中执行器的可用总内存

在前面的屏幕截图上,节点为执行器和驱动器提供了总共112 GB的内存,在为集群管理器预留内存之后。现在,我们必须使用以下公式来计算这个执行器的内存开销:

让我们尝试使用之前描述的例子来计算执行器内存。我们将把可用的节点内存除以每个节点的总执行器数量。然后,我们将这个结果除以 1.1。计算执行器内存的公式如下:

在这里,Nm 是总节点内存,而 Ne 是每个节点的执行器数量。

在我们的场景中使用前面的公式,执行器内存应该是 36.94 GB(112/3 – .384)。因此,我们可以将执行器内存设置为 36 GB。

  • 1.

  • 驱动器内存或 driver-memory:驱动器内存要么小于或等于执行器内存,要么等于执行器内存。根据特定场景,这个值可以设置为小于或等于执行器的内存。在驱动器内存问题的情况下,建议的一种优化是将驱动器内存设置为与执行器内存相同。这可以加快性能。

  • 有向无环图(DAG)优化:让我们看看一些你可以通过 DAG 优化解决的问题:

    • 问题:在 DAG 中,我们可以看到多个阶段在所有任务或操作方面都是相同的。

    • 原因:DataFrame,表示为 d1,正在被用来派生出多个 DataFrame(例如 – d2、d3 和 d4)。由于 Spark 是惰性计算的,当创建每个 DataFrame(d2、d3 和 d4)时,d1 会每次都重新计算。

    • 解决方案:在这种情况下,我们必须使用Dataset<T> persist(StorageLevel newLevel)方法持久化依赖的 DataFrame(d1)。

既然我们已经讨论了大数据的性能调优,那么让我们学习如何调整实时应用。

优化流式应用

现在,让我们学习如何优化流式应用。在这里,我们的讨论将主要关注 Kafka,因为我们在这本书的早期已经讨论过它。当涉及到流式应用时,我们可以调整它们的延迟和吞吐量。在本节中,我们将学习如何观察 Kafka 中的性能瓶颈。然后,我们将学习如何优化生产者和消费者。最后,我们将讨论一些有助于调整整体 Kafka 集群性能的小技巧和技巧。

观察流式应用中的性能瓶颈

任何调优的第一件事就是监控和找出问题所在及其发生的位置。对于实时流处理应用来说,这一点至关重要。市面上有许多 Kafka 可观察性工具,例如 Lenses 和Confluent Control CenterC3)。在这里,我们将看看 C3 如何帮助我们观察异常。

当您导航到 C3 中的任何主题时,您将看到三个标签页 – 生产者消费者消费者延迟消费者延迟标签页可以告诉您消费者是否缓慢。以下截图显示,在从主题读取数据时,消费者(组)落后1,654条记录:

图 11.20 – 消费者延迟

图 11.20 – 消费者延迟

前面的截图还显示了该消费者组的活跃消费者数量。这可以表明消费者是否被设置为利用主题的全部性能潜力。要了解更多信息,请点击消费者组 ID属性(在前面截图中为analyticsTeam)。您将看到以下屏幕:

图 11.21 – 分区消费者延迟

图 11.21 – 分区消费者延迟

前面的截图显示了消费者组分区级别的延迟。从图 11.19图 11.20可以看出,只有一个消费者正在运行,但该主题有三个分区。正如我们所看到的,通过增加消费者组中的消费者数量,我们可以调整消费者应用程序。同样,我们可以查看主题的生产者速度与消费者速度,并找出生产者中的任何缓慢之处。

生产者调优

以下是一些针对 Kafka 生产者的常见调优技术:

  • 当生产者向 Kafka 代理发送消息时,它会收到一个确认。acks=all属性以及min.insync.replicas的值决定了生产者的吞吐量。如果acks=all被设置,并且确认需要更长的时间来到来(因为它必须在发送确认之前写入所有副本),那么生产者就不能再产生任何消息。这会大大降低吞吐量。在这里,我们必须在持久性和吞吐量之间做出选择。

  • 具有幂等性的生产者确保消息的精确一次投递。然而,这也有代价:它会降低生产者的吞吐量。因此,系统必须在去重和吞吐量之间做出选择。在使用幂等性生产者时,可以通过增加max.in.flight.requests.per.connection的值来略微提高吞吐量。

  • 提高吞吐量的方法之一是增加batch.size属性的值。尽管生产者用于发送消息,但它是以异步方式进行的(对于大多数应用)。生产者通常有一个缓冲区,在将记录发送到 Kafka 代理之前,生产记录会被批量处理。将记录批量发送到代理可以提高生产者的吞吐量。然而,随着batch.size值的增加,延迟也会增加。将batch.size设置为平衡的值,以便在不严重影响延迟的情况下获得最佳吞吐量。

  • Linger.ms是另一个属性,当 Kafka 生产者缓冲区中的消息发送到 Kafka 代理时受到影响。linger.ms的值越高,吞吐量和延迟就越高。同样,它必须以平衡的方式设置,以获得最佳吞吐量和延迟。对于极大量的数据,较高的linger.ms值可以显著提高性能。

有了这些,我们已经简要讨论了生产者优化的技术。现在,让我们找出如何优化 Kafka 消费者的性能。

消费者调优

以下是一些你可以利用的技巧和窍门来优化消费者,以充分利用消费者可能带来的潜在改进:

  • 为了使消费者以最优化方式消费,消费者的总数应等于分区的数量。在多线程 Kafka 消费者中,消费者组中所有消费者线程的总数应等于主题分区的数量。

  • 另一个导致消费者变慢的典型场景是消费者过多的重新平衡。如果轮询和处理max.poll.records所需的时间超过max.poll.interval.ms的值,就会发生这种情况。在这种情况下,你可能想增加max.poll.interval.ms的值或减少max.poll.records的值。

  • 在消费者无法发送心跳或由于网络延迟导致心跳包发送缓慢的场景中,可能会发生重新平衡。如果我们对静态消费者(消费者静态映射到分区)没有问题,我们可以为消费者组中的每个消费者实例配置一个唯一的group.instance.id值。这将增加下线消费者的延迟,但将确保其他分区的延迟和吞吐量极高,因为它将避免不必要的消费者重新平衡。

  • 由于consumer.commitSync()在成功提交之前会阻塞线程,所以在大多数情况下,它可能比consumer.commitAsync()慢。然而,如果发生致命错误,使用commitSync()来确保在消费者应用程序关闭之前消息被提交是有意义的。

尽管我们主要讨论了 Apache Kafka,但其他支持流处理的替代产品,如 Apache Pulsar 和 AWS Kinesis,也有类似的性能调优技术。

数据库调优

数据库调优是性能调优中的重要活动。它包括 SQL 调优、从数据库表中的数据读写调优、数据库级调优以及在创建数据模型时进行优化。由于这些内容在各个数据库之间差异很大(包括 SQL 和 NoSQL 数据库),因此数据库调优不在此书的范围之内。

现在,让我们总结一下本章所学的内容。

摘要

我们从理解性能工程是什么以及学习性能工程生命周期开始本章。我们还指出了性能工程与性能测试之间的区别。然后,我们简要讨论了各种可用的工具,这些工具可以帮助进行性能工程。我们了解了性能基准测试的基础以及创建基准时需要考虑的因素。接着,我们学习了各种性能优化技术以及如何将它们应用到 Java 应用程序、大数据应用程序、流式应用程序和数据库中。

有了这些,我们已经学会了如何为基于批处理和实时数据工程问题进行性能工程。在下一章中,我们将学习如何评估多个架构解决方案以及如何提出建议。

第十二章:评估、推荐和展示您的解决方案

我们从数据工程的基础知识开始,学习了各种解决数据摄取和数据发布问题的方法。我们了解了各种架构模式,以及解决方案的治理和安全。在前一章中,我们讨论了如何实现性能工程以及如何为我们的解决方案创建性能基准。

到目前为止,我们已经掌握了构建高效、可扩展和优化的数据工程解决方案的多种技能。然而,正如在第一章中“Java 数据架构师的责任和挑战”部分所讨论的,数据架构师扮演着多个角色。在执行角色中,数据架构师成为业务和技术之间的桥梁,能够与相关利益相关者有效地、轻松地沟通。架构师的工作不仅是要创造解决方案,还要向高管和领导层(包括职能和技术)展示并推销他们的想法。在本章中,我们将关注如何推荐和展示解决方案。

在本章中,我们将从讨论基础设施和人力资源估算开始。作为数据架构师,我们需要推荐解决方案。为此,我们将讨论如何创建一个决策矩阵来评估我们的解决方案并比较不同的替代方案。然后,我们将学习如何有效地使用决策图来选择最佳架构替代方案。最后,我们将了解一些基本技巧和窍门,以有效地展示解决方案。

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

  • 创建成本和资源估算

  • 创建架构决策矩阵

  • 数据驱动架构决策以降低风险

  • 展示解决方案和建议

创建成本和资源估算

在本节中,我们将讨论我们应用以创建估算的各种考虑因素、方法和技巧。我们将简要讨论基础设施估算和人力资源估算。基础设施估算与容量规划密切相关。因此,我们将从容量规划开始讨论。

存储和计算容量规划

要创建基础设施估算,你必须找出数据存储需求(RAM、硬盘、卷等)和计算需求(应具备的 CPU/vCPU 数量和核心数)。确定存储和计算需求的过程称为容量规划。我们将首先学习在执行容量规划时需要考虑的因素。

执行容量规划时需要考虑的因素

在创建容量计划时,需要考虑以下各种因素:

  • 输入数据速率:根据应用类型,需要考虑数据速率。例如,对于实时或准实时应用,在规划存储和计算能力时,应考虑峰值数据速率。对于基于批处理的应用,应考虑中值数据速率或平均数据速率。如果批处理作业每天运行,建议在容量规划中使用中值数据速率。中值数据速率比平均数据速率更受欢迎,因为中值数据速率基于数据速率的密集分布。因此,它表示最频繁记录的数据速率的中点。因此,中值值永远不会受到任何异常值的影响。另一方面,平均数据速率在一段时间内找到所有数据速率的平均值,包括一些异常的高或低数据速率。

  • 数据复制和 RAID 配置:复制确保了高可用性和数据局部性。由于它将数据复制到多个节点、系统或分区,因此在规划存储容量时,我们必须考虑复制因子。例如,如果使用复制因子 3 存储 5GB 的数据,这意味着它在不同的系统中存储了两个副本,以及原始消息。因此,存储 5GB 数据的总存储需求是 15GB。复制因子常被误认为是 RAID。虽然复制因子通过数据局部性确保了高可用性,但 RAID 通过在存储阵列层确保冗余,在物理存储级别确保数据安全。对于关键任务用例,在规划存储容量时,建议同时考虑复制和 RAID。

  • 数据保留:另一个重要因素是数据保留。数据保留是指数据在删除之前需要在存储中保留的时间。这起着重要作用,因为它决定了为了积累目的需要多少存储。在云中,另一个需要考虑的因素是存档需求。数据是否需要存档?如果是这样,何时应该存档?

初始阶段,数据可能被频繁访问。然后,它可能被不频繁访问,公司可能希望将数据存储在存档区域以供长期审计和报告需求。这种场景可以通过在云中使用特定的策略来处理以节省资金。例如,我们可以使用 S3 智能分层,它根据访问频率自动将数据从 S3 标准访问层发送到 S3 不频繁访问层,再到 S3 Glacier 层。这降低了运营成本OpEx)成本,因为当你将数据从标准访问层移动到 Glacier 层时,你可以节省很多。

  • 数据平台类型:您是否在本地或公共云上运行应用程序也很重要。在规划本地部署时,容量规划应考虑最大所需容量。但如果您正在规划云部署,建议采用中值容量需求,并为峰值负载配置自动扩展。由于云提供了即时扩展的选项,以及仅为您使用的部分付费,因此启动处理常规数据量所需的资源是有意义的。

  • 数据增长:您还必须考虑数据的增长率。基于各种因素,增长率可能会有所不同。考虑到数据工程管道通常是长期投资,因此考虑增长率非常重要。

  • 共享模式下的并行执行:我们必须考虑的其他因素之一是共享资源及其对并发执行的影响。例如,如果我们知道在集群上可能同时运行 10 个平均负载约为 100 GB 的作业,这将帮助我们正确估计大数据集群的资源需求。同样,为了估计 Kubernetes 集群的资源需求,我们应该了解将同时运行的最大 Pod 数量。这将有助于确定您想要启动的物理机器和虚拟机的数量和大小。

通过这些,我们已经了解了在进行存储和计算容量规划时需要考虑的各种因素。在下一节中,我们将探讨这些因素如何帮助进行容量规划。

将这些考虑因素应用于计算容量

在本节中,我们将讨论一些例子,说明上述因素如何被巧妙地用于计算容量。以下是一些用例:

  • 示例 1:假设我们需要为本地数据中心中的大数据集群制定容量计划。在这里,输入数据速率是每秒 R 条记录,每条记录是 B 字节。一天的存储需求()可以通过将 R 乘以 B,然后将结果乘以 86,400 秒来计算。然而,这个计算不包括复制或 RAID 因素。我们必须将复制因子(在这里,RF)以及由于 RAID 配置(RAID 0 的过载因子为 1,RAID 5 为 1.5,RAID 10 为 2)的过载因子(在这里,OF)乘以它。因此,计算一天存储需求的公式如下:

但实际容量需求可能超过这个数值。如果需要保留数据 7 天,我们将通过将 7 乘以图片 3来得到所需的总存储量。现在,让我们看看增长因子如何影响容量规划。根据技术栈、数据量、数据访问以及读取模式和频率,我们可以设置总内存和计算能力。假设计算出的存储容量为图片 4,内存为图片 5,计算能力为图片 6每秒输入/输出操作数IOPS)为图片 7。此外,假设年增长率为g。因此,下一年度的最终资源需求如下:

图片 8图片 9图片 10图片 11

在这个例子中,我们看到了我们的因素是如何帮助确定 Hadoop 大数据集群的规模的。硬件资本支出CapEx)代表了一笔重大的前期投资,并需要持续的操作支出(OpEx),因此需要在这两者之间取得平衡,以实现更好的总拥有成本(TOC)。在下一个例子中,我们将探讨如何为实时负载确定 Kafka 集群的规模。

  • 示例 2:在这个例子中,我们试图预测一个每秒接收 100 条消息的 Kafka 集群的容量,保留期为 1 周,平均消息大小为 10 KB。此外,所有主题的副本因子均为 3。在这里,一个 Kafka 集群包含两个主集群——一个 Kafka 集群和一个 zookeeper 集群。对于生产中的 zookeeper 集群,应使用双核或更高 CPU,并配备 16 至 24 GB 的内存。对于 zookeeper 节点,500 GB 至 1 TB 的磁盘空间应该足够。对于 Kafka 代理节点,我们应该运行具有 12 个节点以上的多核服务器,并支持超线程。Kafka 代理的正常内存需求通常在 24 至 32 GB 之间。现在,让我们计算代理的存储需求。以下公式有助于计算每个节点的存储需求:

图片 1图片 2图片 3图片 4图片 5图片 6

将此公式应用于我们的例子,我们得到每个代理的存储需求为 604 GB。

通过这些例子,我们看到了如何应用各种因素来预测解决方案的容量需求。这有助于为业务创建详细的 CapEx 和 OpEx 估算。

现在我们已经了解了如何计算基础设施估算,我们将讨论如何估算执行项目的人力资源相关成本和时间。

努力和时间线估算

除了架构师必须处理的各项责任外,努力和时间估算对于数据架构师来说也是一个重要的责任。通常,架构师负责在项目实施初期创建一个高级估算。考虑到大多数团队遵循敏捷软件开发方法,详细的努力估算是在实施阶段由敏捷团队完成的。以下活动需要完成以创建良好的估算:

  • 创建任务和依赖关系图:首先,为了创建估算,我们必须分析解决方案并将其划分为高级开发和质量保证任务。在创建高级任务列表时,我们还应考虑所有性能工程任务。然后,我们必须创建一个依赖关系任务列表,该列表将指定特定任务是否依赖于另一个任务(或多个任务)。以下表格显示了创建任务和依赖关系列表的一种格式:
任务编号 任务名称 依赖关系
1 创建 Git 用户注册和主仓库
2 在 PC 上创建本地仓库 任务 1
3 创建一个简单的 Hello World,其输出将在 Java 中以印地语显示
4 创建 R2
5 审查 R2 的代码 任务 4
6 推送 R2 的代码 任务 2 和任务 5
7 创建 R2 的数据模型
8 创建 R3-b 的数据模型
9 审查 R3-b 的数据模型 任务 8
10 推送 R3-b 的数据模型 任务 1 和任务 8

图 12.1 – 示例任务和依赖关系列表

在前一个表中,请注意任务 2 依赖于任务 1,同样,任务 6 依赖于任务 2 和任务 5。注意我们如何通过添加依赖关系列来表示这种依赖关系。这个依赖关系矩阵帮助我们理解风险和依赖关系。它还帮助我们了解各种任务如何并行运行。这有助于为各种功能发布创建路线图。

  • 根据任务复杂度分类:架构师必须做的事情之一是将任务分类为以下三个类别之一:高复杂度、中等复杂度和低复杂度。然而,对于一些特殊情况,可以定义更细粒度的复杂度级别。

  • 根据技术分类:另一个有助于估算的分类是技术。这是因为基于 Spark 的任务的复杂任务可能不同于基于 DataStage 的任务的复杂任务。因此,需要投入的平均工作量不仅取决于复杂性,还取决于技术。

  • 创建任务的预估时间:要创建时间预估,首先,我们必须创建一个地图,其中包括特定技术组合和复杂性的时间消耗,如果是一项技术任务的话。如果是一项分析任务,我们必须创建一个任务时间与其复杂性的映射。例如,一个 Spark 作业的复杂任务可能需要 8 人天来完成,而一个 Informatica 作业的复杂任务可能需要 5 人天来完成。基于这样的映射,我们可以估算完成项目所需的总时间,以人天或人时为单位。对于一些敏捷项目,这种努力可以通过点系统来估算。例如,一个复杂分析任务可能需要 5 点努力。

  • 创建总努力预估:基于之前的预估,我们可以通过将所有单个任务的努力相加来计算总努力。

  • 在预估中添加缓冲:正如书中《实用程序员》(Hunt 等,1999 年)所提到的,我们应该记住:“与其说是建造,软件更像园艺——它比混凝土更有机。你不断地监控花园的健康状况,并根据需要做出调整(土壤、植物、布局)。”由于开发任何应用程序或数据管道都是有机的,我们必须在我们的预估中添加缓冲,以便在项目计划中容纳任何有机变化。

  • 创建产品路线图和发布时间表:基于总预估、依赖项、风险和预期的交付范围,我们可以创建路线图和时间表。理解预期的交付时间表对于确保我们能够进行适当的人力资源加载,并在业务所期望的时间范围内交付项目至关重要。话虽如此,很多时候,业务对交付时间表有不合实际的期望。这是架构师(在项目经理和产品所有者的帮助下)的工作,向业务沟通和证明预估,以便技术和业务团队能在交付时间表上达成共识。

  • 在预估旁边列出所有风险和依赖项:在创建预估时,列出所有风险、依赖项和假设非常重要,这样所有利益相关者都能了解正在交付的内容以及交付解决方案所涉及的风险。

现在我们已经学会了如何创建和记录深思熟虑的努力预估,我们必须找出解决方案的总交付或开发成本。要做到这一点,我们必须进行人力资源加载。人力资源加载是一个过程,通过这个过程我们确定需要多少具有特定技能的开发者、测试人员和分析师才能在约定的时间内交付项目。找到具有特定角色的合适人员组合是交付解决方案的关键。然后,我们根据角色、人口统计和技术分配一个特定的每小时成本。

然后,我们考虑一个角色或资源所需的小时数,并将其乘以分配的费率,以得出项目的人力资源总成本。通过汇总每种资源的总成本,我们可以找到总开发成本(或劳动力成本;不包括任何基础设施或许可费用)。

通过这样,我们已经学会了如何创建成本和资源估算以实施解决方案。在这本书的早期,我们学习了如何为各种类型的数据工程问题开发不同的架构。我们还学习了如何进行性能测试和基准测试。

在本节中,我们学习了如何创建成本和资源估算以实施解决方案。有没有一种方法可以将所有这些信息结合起来,以推荐最合适的解决方案? 在现实场景中,每个数据工程问题都可以通过多个架构解决方案来解决。我们如何知道哪个是最合适的解决方案?有没有一种逻辑方法来确定最合适的解决方案? 让我们来看看。

创建架构决策矩阵

关于数据工程,架构决策矩阵是一个帮助架构师以清晰和客观的方式评估不同架构方法的工具。决策矩阵是一个概述了制定架构决策的各种期望标准的网格。这个工具根据每个标准的得分对不同的架构替代方案进行排名。决策矩阵被其他决策过程所使用。例如,决策矩阵被业务分析师用来分析和评估他们的选项。

决策矩阵,也称为 Pugh 矩阵、决策网格或网格分析,可以用于许多类型的决策过程。然而,它最适合于我们必须在多个替代方案中选择一个选项的场景。由于我们必须为推荐选择一个架构,因此使用决策矩阵得出结论是有意义的。现在,让我们讨论创建用于架构决策的决策矩阵的逐步指南。创建决策矩阵的步骤如下:

  1. 头脑风暴并最终确定各种标准:为了创建一个可以用于架构评估的决策矩阵,重要的是要头脑风暴并最终确定决策所依赖的各种标准。在这个头脑风暴会议中,重要的是要涉及领导和业务高管作为利益相关者。如果你是来自服务公司的架构师,正在为客户开发解决方案,那么涉及客户方的架构师也很重要。这一步骤非常重要,因为各种标准和它们的优先级有助于在一系列架构替代方案中缩小我们的最终推荐。

  2. 创建矩阵表:接下来,我们应该创建决策矩阵表,其中每一行代表一个特定的标准,而每一列代表一个特定的建筑替代方案。这些是帮助我们确定当前用例中架构适用性的选定标准集。以下图表显示了表格的外观:

图 12.2 – 创建决策矩阵表

图 12.2 – 创建决策矩阵表

  1. 分配排名或刻度:现在,我们必须为每个架构的标准分配排名或刻度。排名或刻度是一个相对度量,刻度越高,越符合标准。以下图表显示了如何根据各种标准将刻度分配给不同的架构:

图 12.3 – 为每个架构与每个标准分配刻度值

图 12.3 – 为每个架构与每个标准分配刻度值

如我们所见,每个架构相对于每个标准在 1 到 5 的相对刻度上分配了不同的刻度值。在这里,5 是给定标准的可能最高匹配度,而 1 是给定标准的可能最低匹配度。在这个例子中,架构 1刻度方面的得分是3架构 2的得分是2,而架构 3架构 4标准 1方面的得分是4。因此,架构 3架构 4标准 1方面是最合适的。

  1. 分配权重:接下来,我们必须为每个标准分配权重。这将有助于确定各种标准的优先级。以下图表显示了如何将权重分配给每个架构与每个标准:

图 12.4 – 为每个标准分配权重

图 12.4 – 为每个标准分配权重

如我们所见,分配给每个标准的权重与架构无关。这为每个标准赋予了一个优先级。因此,最理想的标准获得最高的优先级。权重越高,优先级越高。在这个例子中,标准 1标准 2的优先级最低,优先级得分为2,而标准 4标准 5的优先级最高,优先级得分为4

  1. 计算得分:每个架构针对一个标准的个别得分是通过将刻度值乘以标准的权重来计算的。架构的总可取性得分是通过将每个标准的所有得分相加来计算的。以下图表显示了这样的决策矩阵的外观:

图 12.5 – 建筑决策完成决策矩阵的示例

图 12.5 – 建筑决策完成决策矩阵的示例

如我们所见,架构 4看起来是最吸引人的解决方案,因为它具有最高的总吸引力得分57.5,而架构 1是最不吸引人的,得分为43.5

在本节中,我们学习了如何创建决策矩阵。现在的问题是,总吸引力得分是否总是足以推荐一个架构? 在下一节中,我们将学习如何通过使用我们之前学到的技术进一步评估架构。

数据驱动的架构决策以减轻风险

决策矩阵帮助我们评估架构的吸引力。然而,并不总是需要选择具有最高吸引力得分的架构选项。有时,每个标准都需要有一个最低阈值得分,架构才能被选中。这种场景可以通过蜘蛛图来处理。

蜘蛛图,也称为雷达图,通常用于显示跨多个维度的数据。每个维度由一个轴表示。通常,维度是量化的,并归一化以匹配特定的范围。然后,每个选项与所有维度相对比,以创建一个封闭的多边形结构,如下面的图表所示:

图 12.6 – 蜘蛛图或雷达图

图 12.6 – 蜘蛛图或雷达图

在我们的案例中,每个用于做出架构决策的标准都可以被视为一个维度。此外,每个架构替代方案都在雷达图上绘制为图表。让我们看看图 12.5中显示的决策矩阵的使用案例。以下图表显示了相同使用案例的雷达图:

图 12.7 – 之前讨论的示例场景的雷达图

图 12.7 – 之前讨论的示例场景的雷达图

如我们所见,每个轴代表一个标准,如标准 1标准 2等。每个标准总共有 25 分,沿着其轴分为五个相等的部分。每个标准的划分标记与相邻标准的划分标记相连,形成一个对称的五边形蜘蛛网。每个标准的最高得分是 25 分,因为它是最大规模值(5)和最高权重(5)的乘积。我们还创建了一个阈值多边形,如前图中虚线所示。这是通过连接每个标准的阈值标记(在这种情况下,得分为 8 分)来创建的。一个最优解是这样一个多边形,要么比阈值多边形大,要么等于阈值多边形。最优解的所有标准都应该比每个标准的阈值得分得分更高。

如前图所示,我们每个标准的阈值分数是 8。根据架构的每个标准的分数,我们绘制多边形图。在这里,架构 1的图是蓝色,架构 2是粉色,架构 3是绿色,架构 4是紫色。根据这些图,我们可以看到,在这个用例中,只有架构 3是最优的。尽管架构 4的总期望分数大于架构 3,但它没有满足每个标准至少有 8 分的最小阈值条件,因为它在标准 2中只得了 7.5 分。此外,如果架构 3的每个标准的分数都大于或等于阈值分数。因此,架构 3是此用例的最佳选择。

评估决策矩阵以找到最优化解决方案的另一种方法是使用决策树。决策树是决策支持工具,它使用树形模型来提问,并根据这些问题的答案对结果进行分类或剪枝。通常,叶节点表示类别或决策。以下图显示了使用蜘蛛/雷达图评估我们刚才讨论的场景的决策树示例:

图 12.8 – 评估架构替代方案的决策树

图 12.8 – 评估架构替代方案的决策树

如我们所见,我们可以根据决策矩阵中之前记录的分数创建一个决策树,以找到最优化解决方案。在这里,我们提出的问题例如标准 1 的分数是否大于 8?标准 2 的分数是否大于 8?等等。根据答案,我们在该级别剪枝掉非优化解决方案。最后,我们问,这是候选架构的最大分数,并且不能被剪枝吗?这个问题的答案帮助我们确定最优化解决方案。

在本节中,我们学习了如何使用数据驱动的方法来寻找和评估问题的最优化解决方案。现在,建筑师的任务是将解决方案作为建议展示出来。在下一节中,我们将讨论如何有效地展示你的解决方案的指南。

展示解决方案和建议

作为一名架构师,你的工作并不仅仅是在基于问题、平台、标准和优先级创建和评估最优化架构替代方案后结束。作为商业和技术之间的桥梁,架构师还负责有效地传达解决方案并推荐最优化替代方案。根据你的项目和业务类型,你可能需要说服利益相关者投资解决方案。以下是一些有助于你展示解决方案并更有效地说服利益相关者的指南:

  • 在演示之前进行演示: 如果可能的话,尽早与客户或最终客户接触,并向他们展示你正在考虑的可能解决方案。同时,告诉他们评估每个解决方案需要多少时间。在开发架构的过程中,保持他们的参与和知情。如果利益相关者参与过程并保持知情,这对双方都是双赢的局面。利益相关者会感到他们是解决方案的一部分,并且有足够的时间理解或预测实施解决方案的任何影响。另一方面,架构师会获得关于优先级和标准的持续反馈,这有助于他们制定一个经过充分研究的决策矩阵。一个更准确的决策矩阵最终有助于架构师做出最期望的推荐。

  • 了解你的受众并确保他们出席: 虽然这对任何演示都适用,但在展示解决方案之前了解受众非常重要。了解他们是否来自业务、执行领导层或技术方面。同时,考虑是否有任何外部团队或供应商参与也很重要。了解你受众的人口统计信息将帮助你定制演示,使其与他们的工作相关。如果是一个混合受众,确保你为所有不同的受众群体都有相关的内容。同时,邀请所有重要的利益相关者也很重要,以确保你的解决方案能够触及每一个目标受众。

  • 展示解决方案的投资回报率 (ROI): 通常,在解决方案的演示中会有高层领导、执行人员和业务负责人出席。对他们来说,了解解决方案如何产生或节省美元非常重要。这可能意味着解决方案将创造额外的收入来源,或者它可能只是像降低总拥有成本或降低开发成本这样微不足道的事情。为了展示解决方案的 ROI,你可以包括解决方案是否为顾客体验或产品接受度增加了任何价值。一个好的数据架构师应该仔细思考并找出解决方案如何为业务增加价值。

  • 通过比较替代方案进行推荐: 虽然我们作为架构师通常会推荐一个架构,但展示所有替代架构及其优缺点是一个好的做法。然后,我们必须确定最合适的架构。展示我们为什么选择那个架构也是一个好主意。

  • 使用他们的语言使演示更出色: 每个公司和业务都有自己的语言。由于许多利益相关者来自业务方面,所以在演示时适应组织内部流行的通用语言会更好。这确保了受众能够轻松理解我们所展示的内容,并能将点联系起来。

  • 注意上下文:对于演示文稿来说,上下文也很重要。根据受众,你的演示文稿应该定制化,以便在技术内容与业务内容之间保持正确的平衡。

  • 确保你的演示文稿具有视觉吸引力和相关性:图表比文字更能说明问题。演示文稿必须包含清晰、相关且自解释的图表。避免在演示文稿中使用过多的文字。一个视觉上吸引人的演示文稿更容易解释,并使各种利益相关者对演示文稿保持兴趣。

在本节中,我们讨论了几种以简洁、有效和有影响力方式向利益相关者展示解决方案的技巧和窍门。除了开发和架构解决方案外,我们还了解如何有效地评估、推荐和展示解决方案。现在,让我们总结一下本章所学的内容。

摘要

我们本章一开始学习了如何规划和估算基础设施资源。然后,我们讨论了如何进行工作量估算、如何配置人力资源以及如何计算总开发成本。通过这样做,我们学习了如何创建架构决策矩阵以及如何在不同架构之间进行数据驱动的比较。接着,我们深入探讨了我们可以使用决策矩阵通过蜘蛛/雷达图或决策树来评估最优化解决方案的不同方法。最后,我们讨论了一些指导原则和技巧,以更有效和有影响力地向各种商业利益相关者展示优化后的解决方案。

恭喜你——你已经完成了这本书的所有 12 章,在这本书中,你学习了关于 Java 数据架构师角色的所有内容,数据工程的基础知识,如何为各种数据工程问题构建解决方案,各种架构模式,数据治理和安全,以及性能工程和优化。在本章的最后,你学习了如何使用数据驱动技术选择最适合的架构,以及如何向高管层展示它。我希望你已经学到了很多,这将有助于你作为成功的数据架构师发展你的职业生涯,并帮助你在你当前的角色中成长。

posted @ 2025-09-11 09:43  绝不原创的飞龙  阅读(38)  评论(0)    收藏  举报