Java-企业级现代化指南-全-

Java 企业级现代化指南(全)

原文:zh.annas-archive.org/md5/a76d908bbfc783d8cb1c3ac05dbcd05b

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

我们为那些希望成功将他们的单片式、基于 Java 的模型带入未来的开发者编写了这本书。本书的路线图如下:

  • 第一章介绍了本书中将使用的基本技术和概念。

  • 第二章向您展示了如何基于微服务架构实施完整的体系结构,使用不同的 Java 框架来处理不同的组件。我们将概述如何将典型的单片式方法分割成更多样化和异构的环境。

  • 第三章介绍了一些基本的迁移策略,并展示了目标开发平台的评估路径。

  • 第四章讨论了 Java 开发人员如何通过 Kubernetes 能力来现代化和增强他们的应用程序。

  • 第五章探讨了经过验证的模式、标准化工具以及开源资源,这些资源将帮助您创建能够根据您的需求持续增长和变化的系统。

  • 第六章展示了 Kubernetes 中的基本任务,如日志记录、监控和调试应用程序。

  • 第七章分析了 Java 开发人员如何按照无服务器执行模型创建现代应用程序。我们概述了 Java 开发人员今天和明天可能会使用的一些最常见的用例和架构。

本书中使用的约定

本书中使用了以下排版约定:

斜体

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

常量宽度

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

常量宽度粗体

显示用户应直接输入的命令或其他文本。

常量宽度斜体

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

提示

这个元素表示提示或建议。

注意

这个元素表示一般注释。

警告

这个元素表示警告或注意。

使用代码示例

本书提供了补充材料(代码示例、练习等),可在https://oreil.ly/modernentjava下载。

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

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

我们感谢,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“现代化企业 Java 由 Markus Eisele 和 Natale Vinto(O’Reilly)编写。版权 2022 Markus Eisele 和 Natale Vinto,978-1-098-10214-2。”

如果您认为您使用的示例代码超出了公平使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。

致谢

我们都要感谢 Jason “Jay” Dobies,因为他对我们德意英文的极度热情。他不仅帮助我们成为更好的作家,还作为早期读者提供了宝贵的见解。感谢你能成为我们旅程的一部分。每个人都说写书不容易。在我们开始之前,我们就知道这一点。然而,我们毫不客气地低估了我们在疫情初期开始写作的事实。我们俩经历了比预期更多的起伏,唯有家人和朋友的支持,才使我们有机会完成这本书。我们也要非常感谢 O’Reilly 团队的耐心、灵活的截止日期以及在我们需要时倾听我们的意见。谢谢你们,Suzanne McQuade、Nicole Taché和 Amelia Blevins!谢谢你,Red Hat,为我们提供一个学习、成长的绝佳场所。我们热爱开源,也乐于分享知识,这或许也是公司里每个人的共同点。感谢许多“Hatters”在这条路上的支持!技术审阅员总是在你最脆弱的时候找到你。每当我们试图在他们眼皮底下溜过一点细节时,他们总是发现并提出正确的问题。感谢你们在这条路上的奉献和启发:Sébastien Blanc、Alex Soto 和 Marc Hildenbrand!这本书的核心不仅是我们积累的共同经验,更是一个典范。这个典范最初由 Madou Coulibaly 和 Alex Groom 创建。他们用它教导许多开发人员如何有效构建云原生应用,并允许我们以他们简明的例子为基础。#保持健康#戴口罩#保护您所爱的人

第一章:重温企业开发

企业开发一直是软件工程中最令人兴奋的领域之一,过去的十年是一个特别引人入胜的时期。2010 年代看到高度分布式的微服务逐渐取代了经典的三层架构,而基于云的基础设施几乎无限的资源推动了沉重的应用服务器走向过时。尽管开发人员面临将分布式世界的各个部分重新组合的挑战,但许多声音质疑这个复杂的微服务世界是否有必要。事实上,大多数应用程序仍然是精心制作的单体应用程序,遵循传统的软件开发流程。

然而,我们部署和运行软件的方式同样迅速改变。我们看到 DevOps 逐渐发展成为 GitOps,扩展了开发人员的责任范围,包括所需的基础设施。基于马库斯的书 现代 Java EE 设计模式(O'Reilly),这本书对现代化提供了更多的视角,而不仅仅是模块化。我们希望帮助您了解导致现代 Kubernetes 本地开发平台的各种因素以及如何在其上构建和维护应用程序。

这本书旨在回顾并评估应用现代化和云原生架构的成功因素和驱动因素。我们专注于现代化基于 Java 的企业应用程序,包括适合现代化的应用程序的选择过程以及帮助您管理现代化工作的工具和方法论的概述。与其讨论模式,这本书提供了一套示例,帮助您应用所学的一切。

话虽如此,这本书并没有广泛讨论单体与分布式应用程序。相反,我们的目标是帮助您了解如何无缝地将您的应用程序迁移到云端。

你可以将这本书作为参考,并按任意顺序阅读章节。虽然我们已经组织好了材料,但从更高层次的概念到逐步实施的方式,我们开始了。首先,重要的是从不同的云定义以及我们如何为它们构建应用程序开始。

从公有云到私有云。为什么云?

公有云、私有云、混合云和多云之间的区别曾经可以通过位置和所有权轻松定义。今天,这两者已不再是云分类的唯一相关驱动因素。让我们从对不同目标环境的更全面定义以及它们为什么被使用的角度开始。

公共云环境通常由非最终用户拥有并可重新分配给其他租户的资源创建。私有云环境专门为最终用户提供资源,通常位于用户的防火墙、数据中心或有时在本地。带有某种程度的工作负载可移植性、编排和管理的多云环境称为混合云。解耦、独立且不连接的云通常被称为多云。混合云和多云方法是互斥的;因为云要么相互连接(混合云),要么不相互连接(多云)。

将应用程序部署到云中,无论是哪种类型的云,都越来越普遍,因为企业希望通过扩展环境组合来提高安全性和性能。但安全性和性能只是将工作负载转移到混合或多云环境的众多原因中的两个。对许多人来说,主要动机是按使用量付费的模式。与投资昂贵且难以扩展的本地硬件不同,云在您需要时提供资源。您无需投资设施、公用事业或建设自己的数据中心。您甚至不需要专门的 IT 团队来处理您的云数据中心运营,因为您可以享受云提供商员工的专业知识。

对开发人员而言,云是关于自助服务和灵活性。您无需等待环境的晋升,可以根据需要选择基础设施组件(例如数据库、消息代理等),从而摆脱不必要的等待时间,最终加快开发周期。除了这些主要优势外,您还可以在某些云环境中找到开发人员的定制功能。例如,OpenShift 具有集成开发控制台,为开发人员提供对其应用程序拓扑的所有详细信息的直接编辑访问。基于云的 IDE(例如Eclipse Che)提供基于浏览器的开发工作区访问,并消除团队的本地环境配置需求。

此外,云基础设施鼓励您自动化部署过程。部署自动化使您能够通过点击按钮将软件部署到测试和生产环境中——这是敏捷开发和 DevOps 团队的强制要求。当您了解微服务架构时,您已经看到了对 100%自动化的需求。但自动化远不止应用部分,它延伸到基础设施和下游系统。AnsibleHelmKubernetes Operators能够帮助您。我们将在第四章更详细地讨论自动化,并且您将在第七章中使用运算符。

“云原生”是什么意思

你可能听说过开发应用程序和服务的云原生方法,尤其是自 2015 年以来,云原生计算基金会(CNCF)成立并发布 Kubernetes v1 以来更加频繁。Bill Wilder 首次在他的书籍Cloud Architecture Patterns (O’Reilly)中使用了“云原生”这个术语。根据 Wilder 的说法,云原生应用程序通过使用云平台服务和自动扩展来充分利用云平台。Wilder 在书写这本书时正值开发和部署云原生应用程序兴趣增长的时期。开发者可以选择各种公共和私有平台,包括亚马逊 AWS、谷歌云、微软 Azure 和许多较小的云服务提供商。但是那时也开始普及混合云部署,这也带来了挑战。

CNCF 将“云原生”定义为:

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

这些技术支持松耦合的系统,具有弹性、可管理性和可观察性。结合强大的自动化,它们允许工程师频繁且可预测地进行高影响变更,减少琐事。

CNCF 云原生定义 v1.0

类似于云原生技术的是十二因素应用。十二因素应用宣言定义了构建部署在云上的应用程序的模式。虽然这些模式与 Wilder 的云架构模式有重叠之处,但十二因素方法可以应用于任何编程语言编写的应用程序,并使用任何组合的后端服务(数据库、队列、内存缓存等)。

Kubernetes 原生开发

对于部署应用程序到混合云的开发人员来说,将焦点从云原生转向 Kubernetes 原生是有道理的。最早提到“Kubernetes 原生”可以追溯到 2017 年。Medium 上的一篇博文描述了Kubernetes 原生与云原生的区别,将其定义为一组针对 Kubernetes 进行优化的技术。关键要点在于,Kubernetes 原生是云原生的一种专业化,不脱离云原生的定义。而云原生应用程序是为云服务而设计的,而 Kubernetes 原生应用程序则是专门为 Kubernetes 设计和构建的。

在云原生开发的早期阶段,编排差异阻碍了应用程序真正成为云原生。Kubernetes 解决了编排问题,但 Kubernetes 并不涵盖云提供商的服务(例如,角色和权限)或提供事件总线(例如,Kafka)。Kubernetes 原生专业化意味着它与云原生之间有许多相似之处。主要区别在于云提供商的可移植性。充分利用混合云并使用多个云提供商要求应用程序可以部署到任何云提供商。如果没有这样的功能,你将被限制在单一云提供商,并依赖于它们全天候的服务。要充分利用混合云的好处,应用程序必须以 Kubernetes 原生方式构建。Kubernetes 原生是解决云可移植性问题的方案。我们将在第二章中详细讨论 Kubernetes 原生。

容器与开发者编排

可移植性的一个关键因素是容器。容器代表着主机系统资源的一部分,与应用程序一起。容器的起源可以追溯到早期的 Linux 时代,当时引入了 chroots,它们随着 Google 的进程容器(后来成为 cgroups)变得流行起来。它们的使用在 2013 年大量增加,主要因为 Docker 使它们对许多开发者可访问。Docker 公司、Docker 容器、Docker 镜像以及我们都习惯使用的 Docker 开发工具有所不同。虽然一切始于 Docker 容器,但 Kubernetes 更倾向于通过任何支持其容器运行时接口(如containerdCRI-O)的容器运行时运行容器。许多人所说的 Docker 镜像实际上是打包在开放容器倡议(OCI)格式中的镜像。

容器原生运行时

容器提供了 Linux 操作系统用户空间的轻量级版本,剥离了基本要素。然而,它仍然是一个操作系统,容器的质量和主机操作系统一样重要。支持容器镜像需要大量的工程、安全分析和资源。这不仅需要测试基础镜像,还需要测试它们在特定容器主机上的行为。依赖于经过认证和 OCI 兼容的基础镜像在跨平台应用迁移时能减少障碍。理想情况下,这些基础镜像已经包含了你所需的语言运行时。对于基于 Java 的应用程序,Red Hat 通用基础镜像是一个很好的起点。我们将在第四章中更多地了解容器及其开发者的使用。

Kubernetes 版本

到目前为止,我们已经讨论了 Kubernetes 作为一个通用概念。我们继续使用“Kubernetes”一词来谈论支持容器编排的技术。Kubernetes(有时仅称为 K8s)这个名词指的是被广泛认为是容器编排核心功能标准制定机构的开源项目。在本书中,如果我们提到 Kubernetes 内的标准功能,我们使用“普通”Kubernetes 这个术语。Kubernetes 社区创建了不同的发行版甚至不同的 Kubernetes 变体。CNCF 运行着Certified Kubernetes Conformance Program,目前列出了来自 108 个供应商的 138 个产品。该列表包括完整的发行版(例如 MicroK8s、OpenShift、Rancher)、托管的服务(例如 Google Kubernetes Engine、Amazon Elastic Kubernetes Service、Azure AKS Engine)以及安装程序(例如 minikube、VanillaStack)。它们都共享共同的核心功能,但供应商根据需要或机会增加了额外的功能或集成。本书中我们不对使用哪种 Kubernetes 变体提出建议。您需要自行决定如何处理生产工作负载。为了帮助您在本书中运行示例,我们使用minikube,不需要您在云端进行全面安装。

管理开发复杂性

Kubernetes 原生开发中最关键的一个领域是管理你的开发环境。成功部署或将其分阶段推向多个环境所需执行的任务数量呈指数级增长。一个原因是独立应用部件或微服务数量的增加。另一个原因是基础设施的应用特定配置。图 1-1 简要概述了一个示例开发环境及其必需的工具,用于完全自动化的开发。本书中我们将讨论其中的一部分,以帮助您在新环境中轻松入门。核心开发任务没有改变。您仍将使用适当的框架(如Quarkus)编写应用或服务,就像本书中的操作一样。开发者工作流程的这一部分通常被称为“内部循环”开发。

我们将在本书中大部分时间都在讨论“外循环”的变化和机会。外循环通过各种机制将您构建和测试的应用程序投入生产是至关重要的。了解,在本书中,我们表达了一些非常强烈的观点。它们反映了我们通过使用我们推荐的工具和技术使 Java 开发者变得高效、快速,甚至可能是快乐的经验。正如图 1-1 所示,在某些地方您有一两个选择。在本书中,我们选择了更传统的 Java 开发方式。我们使用 Maven 而不是 Gradle 来构建应用程序,使用 podman 而不是 Docker 来构建容器映像。我们还使用 OpenJDK 而不是 GraalVM,并且在示例中坚持使用 JUnit 而不是Testcontainers

但是,正如CNCF 景观所绘制的那样,云原生生态系统拥有更多工具供您选择。把这本书看作是企业 Java 开发者的一份路线图。

容器化世界中的开发

图 1-1. 内外循环开发,以及作者推荐的工具

除了技术选择之外,您还必须决定如何使用这个新的生态系统。有了各种可用的工具,您可以选择在 Kubernetes 中参与的程度。我们根据图 1-2 中所述区分了武断和灵活。作为一个对细节着迷的开发者,您可能希望从一线的所有示例中学习,并在制作 YAML 文件时使用纯 Kubernetes。

注意

最初,YAML 被说成是 Yet Another Markup Language 的缩写,这个名字是为了戏谑地指其作为一种标记语言的用途。但后来,它被重新定义为 YAML Ain’t Markup Language 的递归缩写,以区分其作为面向数据的用途。

您可能决定专注于源代码,并且不希望在实现业务逻辑时分心。这可以通过某些发行版提供的开发者工具来实现。根据您在开发过程中最重要的因素,有各种选择。您可以使用主要的 Kubernetes 命令行界面(CLI)kubctl,而不是像 OpenShift 的 CLI oc那样特定于产品。如果您想更接近一个完整的产品,我们建议您尝试CodeReady Containers。它是一个在您的笔记本电脑上的 OpenShift 集群,具有简单易上手的体验。但选择权在您手中。

我们推荐的另一个强大工具是odo,这是一个基于 Kubernetes 项目的通用开发者 CLI。现有的工具如kubectloc更注重操作,并要求深入理解底层概念。Odo 为开发者抽象了复杂的 Kubernetes 概念。外部开发环路的两个示例选择是持续集成(CI)解决方案。我们在本书中使用Tekton,您可以在第六章中使用它。还可以在 Kubernetes 上使用Jenkins Operator或甚至 Jenkins X。无论您做出何种选择,最终您将成为您的基于 Kubernetes 的旅程的主人。

主观与灵活

图 1-2. 主观与灵活——内外开发环路中的技术选择

DevOps 与敏捷性

当现代化你的企业 Java 应用程序时,下一个关键变化在于创建跨职能团队,这些团队从构思到运营分享责任。虽然有些人认为 DevOps 仅专注于操作方面,并与开发者的自助配对,但我们坚信,DevOps 是一种以长远影响为重点的团队文化。单词“DevOps”是“开发”和“运维”的混合词,但它代表了远比这两个术语之和更重要的一套思想和实践。DevOps 包括安全性、协作工作方式、数据分析以及许多其他内容。DevOps 描述了加速新业务需求从开发代码到在生产环境中部署的过程的方法。这些方法要求开发团队和运维团队频繁沟通,并对其团队成员充满同理心。可伸缩性和灵活的供应也是必要的。开发人员通常在熟悉的开发环境中编码,与 IT 运维密切合作,加快软件构建、测试和发布的速度,同时又不损害可靠性。所有这些加在一起会导致更频繁的代码变更和更动态的基础设施使用。将传统 IT 组织从传统方法转变为 DevOps 方法通常被描述为转型,超出了本书的范围。尽管如此,这是一个重要的因素,你将看到这种转型在书籍、文章和演示中被美丽地描述为“教大象跳舞”。

总结

在本章中,您了解了一些基本定义,并听说了本书将要使用的最重要的技术和概念。下一章将带您进入您的第一个应用现代化的源代码。

第二章:通向云原生 Java 之路

“Πάντα ῥεῖ”(一切都在流动)是哲学家赫拉克利特的一句著名格言,描述了我们存在的可变条件,一切都在流动,我们的呼唤是反应和适应。这完美地描述了我们在 IT 世界中以及具体到编程语言和框架中正在经历的演变的正确方法,其中异构、分布式、多云负载更加普遍且对业务目标至关重要。

Java 和 Jakarta EE(之前被称为 Java EE),也在这个方向上不断发展,平衡来自企业解决方案的成熟经验所带来的好处,以及快速变化的云感知场景的需求,在这一章节中,我们将概述迁移到云原生 Java 所需的组件,引导您完成一个名为 Coolstore 的电子商务商店 Java 实现。

云原生研讨会

微服务是一种被广泛认可和接受的实践。对于 JavaEE 开发者来说,这意味着一种范式的提升和转变,单个应用服务器不再包含所有业务逻辑。相反,它被拆分成运行在各自应用服务器上的不同微服务,如 Tomcat 或 Undertow,具有最小的足迹和优化,以保持在云原生世界中的功能和性能。

如今,单片式方法可以重构为异构甚至编程语言无关的模型,其中每个模块由在不同应用程序中运行的特定组件管理。除了像 API 驱动模型这样的最佳实践外,这里的挑战是如何维护这种多样性。然而,如今 Java 提供了一套工具和框架,帮助我们专注于我们喜爱的工具并轻松合作。在本章中,您将学习如何开发和部署基于微服务的应用程序,这些微服务跨越不同的 Java 框架。

架构

我们的电子商务应用 Coolstore 是一个典型的 Web 应用程序,包含三个组件:

展示层

一个前端用于显示可供获取的项目

模型层

一个后端提供业务逻辑来目录化和索引所有要出售的项目

数据层

存储所有交易和商品记录的数据库

这些组件的结果是一个在线商店,具有产品项目目录和我们可以使用图 2-1 中展示的架构进行组织的库存。

酷店架构

图 2-1 酷店架构

我们将这三个先前提到的组件映射到多个微服务中,每个微服务负责其层:

  • 目录服务 使用 REST API 公开存储在关系数据库中的目录内容。

  • 库存服务 使用 REST API 公开存储在关系数据库中的商品库存。

  • 网关服务以高效的方式调用目录服务库存服务

  • WebUI 服务调用网关服务以检索所有信息。

表示这些微服务的演示层和模型层,后者具有与某些数据库管理系统委托的数据层接口。我们的电子商店实现称为 Coolstore,并且看起来像图 2-2 中的图片。

Coolstore 仪表板

图 2-2. Coolstore 仪表板

使用 Quarkus 创建库存微服务

Quarkus是一个全栈、面向 Kubernetes 的 Java 框架,专为 Java 虚拟机(JVM)和本地编译而设计,优化 Java 特别适合容器,并使其成为服务器无服务、云和 Kubernetes 环境的有效平台。

它设计用于与流行的 Java 标准、框架和库(如 Eclipse MicroProfile 和 Spring,以及 Apache Kafka、RESTEasy(JAX-RS)、Hibernate ORM(JPA)、Infinispan、Camel 等)一起使用。它还为 GraalVM(用于运行多种语言编写的应用程序的通用虚拟机,包括 Java 和 JavaScript)提供了正确的信息,以便对应用程序进行本地编译。

Quarkus 是实施微服务架构的良好选择,并提供一套工具,帮助开发人员轻松调试和测试。对于我们的电子商店,我们将开始使用 Quarkus 来实现库存微服务(如图 2-3 所示)。

库存 Quarkus 微服务

图 2-3. 库存 Quarkus 微服务

您可以在书的 GitHub 仓库找到此示例的所有源代码。

创建 Quarkus Maven 项目

使用 Quarkus,您可以使用 Maven 或 Gradle 创建一个新项目。

提示

Maven 和 Gradle 都是设置 Java 项目和管理所有依赖项的流行方式。它们在依赖管理策略和配置格式(XML 与 Kotlin DSL)上有所不同,但在功能上基本相当。在本书中,我们将使用 Maven,因为它通过 IDE 和工具具有更广泛的支持。

我们使用以下命令设置了一个新的 Maven 项目,使用quarkus-maven-plugin

mvn io.quarkus:quarkus-maven-plugin:2.1.4.Final:create \
  -DprojectGroupId=com.redhat.cloudnative \
  -DprojectArtifactId=inventory-quarkus \
  -DprojectVersion=1.0.0-SNAPSHOT \
  -DclassName="com.redhat.cloudnative.InventoryResource" \
  -Dextensions="quarkus-resteasy,quarkus-resteasy-jsonb,↳
 quarkus-hibernate-orm-panache,quarkus-jdbc-h2"
提示

您还可以使用https://code.quarkus.io提供的在线配置器来引导一个 Quarkus 应用程序。

这将创建一个带有InventoryResource类的骨架项目,我们将用它来实现我们的电子商务库存微服务。

让我们来看一下生成的pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.redhat.cloudnative</groupId> ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
    <artifactId>inventory-quarkus</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
        <quarkus-plugin.version>2.1.4.Final</quarkus-plugin.version>
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
        <quarkus.platform.version>2.1.4.Final</quarkus.platform.version>
        <compiler-plugin.version>3.8.1</compiler-plugin.version>
        <surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <maven.compiler.parameters>true</maven.compiler.parameters>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.quarkus</groupId>
                <artifactId>quarkus-bom</artifactId> ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
                <version>${quarkus.platform.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies> ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-resteasy</artifactId>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-junit5</artifactId>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>io.rest-assured</groupId>
          <artifactId>rest-assured</artifactId>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-resteasy-jsonb</artifactId>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-hibernate-orm-panache</artifactId>
        </dependency>
        <dependency>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-jdbc-h2</artifactId>
        </dependency>
    </dependencies> ... <build>
                <plugins>
                    <plugin>
                        <groupId>io.quarkus</groupId>
                        <artifactId>quarkus-maven-plugin</artifactId>  ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png)
                        <version>${quarkus-plugin.version}</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <configuration>
                                    <enableHttpUrlHandler>true↳ </enableHttpUrlHandler>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin> ... </profiles>
</project>

1

在这里,我们设置了groupIdartifactIdversion。有关可用选项的完整列表,请参见表 2-1。

2

在这里,您可以找到 Quarkus BOM 的导入,允许您省略不同 Quarkus 依赖项的版本。

3

这里你可以找到项目的所有依赖项,我们将其表示为要添加的扩展。

  • JSON REST 服务:这使您能够 开发 REST 服务 来消费和生成 JSON 负载。

  • Hibernate ORM Panache:这个事实上的 JPA 实现为您提供了完整的对象关系映射功能。Hibernate ORM with Panache 专注于简化基于 Hibernate 的持久化层,使您的实体更易编写和维护。

  • 数据源(H2):数据源是获取与数据库连接的主要方式;在这个例子中,我们将使用 H2,一个内存数据库,适用于 Java 应用程序。

4

quarkus-maven-plugin 负责应用程序的打包和提供开发模式。

表 2-1. Quarkus Maven 项目选项

属性 默认值 描述
projectGroupId com.redhat.cloudnative 创建项目的 group id。
projectArtifactId 必填 创建项目的 artifact id。如果不传递,则触发交互模式。
projectVersion 1.0-SNAPSHOT 创建项目的版本。
platformGroupId io.quarkus 目标平台的 group id。考虑到所有现有的平台都来自 io.quarkus,这个不会被显式使用。但仍然是一个选项。
platformArtifactId quarkus-universe-bom 目标平台 BOM 的 artifact id。为了使用本地构建的 Quarkus,应该是 quarkus-bom。
platformVersion 如果未指定,将解析最新版本。 项目希望使用的平台版本。它还可以接受版本范围,此时将使用指定范围内的最新版本。
className 如果省略则不创建 生成的资源的完全限定名。
path /hello 资源路径,只有在设置 className 时才相关。
extensions [] 要添加到项目中的扩展列表(以逗号分隔)。
Tip

要查看所有可用的扩展,请使用以下命令从项目目录运行:./mvnw quarkus:list-extensions

创建一个领域模型

现在是时候编写一些代码了,创建一个 领域模型 和一个 RESTful 端点来创建 Inventory 服务。领域模型是软件工程中的一种流行模式,它在云原生世界中也非常合适。这种模式提供的抽象级别使其作为面向对象建模微服务业务逻辑的有效方式。

您可以在这本书的 GitHub 仓库中的 Inventory 类中找到领域模型的定义:book’s GitHub repository

我们的领域模型实现包括一个映射到持久化层的 Entity,代表一个物品清单:

package com.redhat.cloudnative;

import javax.persistence.Entity;
import javax.persistence.Table;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

import javax.persistence.Column;

@Entity ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
@Table(name = "INVENTORY") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
public class Inventory extends PanacheEntity{ ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)

    @Column
    public int quantity; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)

    @Override
    public String toString() {
        return "Inventory [Id='" + id + '\'' + ", quantity=" + quantity + ']';
    }
}

1

@Entity标记类为 JPA 实体。

2

@Table通过定义表名和数据库约束来自定义表创建过程,在这种情况下是INVENTORY

3

Quarkus 将在您使用公共属性并扩展PanacheEntity时为您生成getter/setter。此外,还会自动添加id属性。

一旦我们定义了模型,我们可以更新我们在application.properties文件中表示的属性,以便提供有关如何为我们的微服务填充数据的说明:

quarkus.datasource.jdbc.url=jdbc:h2:mem:inventory;↳
DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png) quarkus.datasource.db-kind=h2
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png) %prod.quarkus.package.uber-jar=true ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)

1

用于内存 DB 的 JDBC 路径;这可以更改为其他类型的 DB,如任何 RDBMS。

2

以下是我们将使用的 SQL 脚本,用于向 Coolstore 填充一些数据:

INSERT INTO INVENTORY(id, quantity) VALUES (100000, 0);

INSERT INTO INVENTORY(id, quantity) VALUES (329299, 35);

INSERT INTO INVENTORY(id, quantity) VALUES (329199, 12);

INSERT INTO INVENTORY(id, quantity) VALUES (165613, 45);

INSERT INTO INVENTORY(id, quantity) VALUES (165614, 87);

INSERT INTO INVENTORY(id, quantity) VALUES (165954, 43);

INSERT INTO INVENTORY(id, quantity) VALUES (444434, 32);

INSERT INTO INVENTORY(id, quantity) VALUES (444435, 53);

3

uber-jar包含所有所需的依赖项,打包在 jar 中以便使用java -jar运行应用程序。在 Quarkus 中,默认情况下,uber-jar的生成已禁用。通过%prod前缀,此选项仅在构建供部署用的 jar 时激活。

创建 RESTful 服务

Quarkus 使用 JAX-RS 标准构建 REST 服务。当我们像之前看到的那样脚手架一个新项目时,将在我们定义的className路径中创建一个hello示例服务。现在我们想要公开 REST 服务以从库存中检索可用项目的数量,使用以下内容:

  • 路径:/api/inventory/{itemId};

  • HTTP 方法:GET

这将返回库存数据库中存在的给定项目 ID 的数量。

让我们将InventoryResource类定义更改为以下内容:

package com.redhat.cloudnative;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api/inventory")
@ApplicationScoped
public class InventoryResource {

    @GET
    @Path("/{itemId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Inventory getAvailability(@PathParam("itemId") long itemId) {
        Inventory inventory = Inventory.findById(itemId); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
        return inventory;
    }
}

1

通过扩展PanacheEntity,我们使用活动记录持久性模式而不是数据访问对象(DAO)。这意味着所有持久化方法都与我们自己的Entity混合。

我们刚刚为我们的微服务实现了一个参数化的 REST 端点,服务于我们 Coolstore 中包含的项目的 JSON 表示。通过这种方式,我们提供了一个通过 HTTP GET请求查询我们在上一步中实现的Inventory数据模型的层。

提示

使用 Quarkus,无需创建 Application 类。虽然支持,但不是必需的。此外,只创建一个资源实例,而不是每个请求创建一个。您可以使用不同的作用域注解(ApplicationScopedRequestScoped 等)进行配置。

在开发模式下运行应用程序

在 Quarkus 中,开发模式是云原生 Java 开发中最酷的功能之一。它通过热部署和后台编译实现,这意味着当你修改 Java 文件或资源文件并刷新浏览器时,更改会自动生效。这对于配置属性文件等资源文件同样有效。此外,刷新浏览器会触发对工作区的扫描,如果检测到任何更改,将重新编译 Java 文件并重新部署应用程序;然后由重新部署的应用程序处理您的请求。如果编译或部署有任何问题,错误页面将告诉你。

你可以使用内置的 Maven 目标 quarkus:dev 在开发模式下启动应用程序。它启用了热部署和后台编译,这意味着当你修改 Java 文件或资源文件并刷新浏览器时,这些更改会自动生效。这对于配置属性文件等资源文件也同样有效:

./mvnw compile quarkus:dev

在你启动应用程序的开发模式后,你应该会看到类似以下的输出:

...
Hibernate:

    drop table if exists INVENTORY CASCADE
Hibernate:

    create table INVENTORY (
       id bigint not null,
        quantity integer,
        primary key (id)
    )

Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (100000, 0)
Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (329299, 35)
Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (329199, 12)
Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (165613, 45)
Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (165614, 87)
Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (165954, 43)
Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (444434, 32)
Hibernate:
    INSERT INTO INVENTORY(id, quantity) VALUES (444435, 53)
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-12-02 13:11:16,565 INFO  [io.quarkus] (Quarkus Main Thread)↳
inventory-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.7.2.Final)↳
started in 1.487s. Listening on: http://0.0.0.0:8080
2020-12-02 13:11:16,575 INFO  [io.quarkus] (Quarkus Main Thread)↳
Profile dev activated. Live Coding activated.
2020-12-02 13:11:16,575 INFO  [io.quarkus] (Quarkus Main Thread)↳
Installed features: [agroal, cdi, hibernate-orm, jdbc-h2, mutiny, narayana-jta,↳
resteasy, resteasy-jsonb, smallrye-context-propagation]

从输出中可以看到,Hibernate 创建了一个数据库,名称与我们的领域模型相同,并使用我们属性文件中定义的一些初始数据进行了填充。

注意

当我们在本章开头搭建项目时,我们包含了一系列依赖项,如 Panache,并将其用作 Entity 映射到数据库中的数据模型。

我们还可以看到我们的应用程序正在运行,并且正在监听端口 8080。如果现在在浏览器中打开 http://localhost:8080,你将看到一个 Quarkus 欢迎页面(如图 2-4 所示)。

Quarkus 欢迎页面

图 2-4. Quarkus 欢迎页面
提示

你可以通过在启动应用程序的同一终端上使用 Ctrl-C 来停止运行开发模式下的应用程序。当你在 Quarkus 2 的开发模式下运行时,默认启用了连续测试功能,即在保存代码更改后立即运行测试。

现在你可以尝试查询我们从 import.sql 文件中插入的项目之一,以测试我们的微服务是否正常运行。

只需导航到 http://localhost:8080/api/inventory/329299

你应该看到以下输出:

{
   "id":"329299",
   "quantity":35
}

REST API 返回了一个 JSON 对象,表示此产品的库存数量。恭喜你完成了第一个使用 Quarkus 的云原生微服务!

注意

现在我们将开发其他将使用此微服务的微服务,因此请保持打开状态,以便在本章结束时使 Coolstore 运行起来。

使用 Spring Boot 创建目录微服务

Spring Boot是一种主张的框架,可以轻松创建独立的基于 Spring 的应用程序,其中包括内嵌的 Web 容器,如 Tomcat(或 JBoss Web Server)、Jetty 和 Undertow,可以直接在 JVM 上使用java -jar运行。Spring Boot 还允许生成可以部署在独立 Web 容器上的 war 文件。

这种主张的方法意味着关于 Spring 平台和第三方库的许多选择已经由 Spring Boot 做出,因此您可以用最少的工作量和配置开始。

Spring Boot 非常适合云原生 Java 开发,因为它可以轻松创建独立的、面向生产的基于 Spring 的应用程序,可以“一键运行”。我们将在我们的架构中包括 Spring Boot 作为目录微服务(如图 2-5 所示)。

Catalog Spring Boot 微服务

图 2-5. Catalog Spring Boot 微服务

您可以在书的 GitHub 存储库中找到创建 Spring Boot 微服务的所有源代码。

创建一个 Maven 项目

在这种情况下,您可以使用 Maven 或 Gradle 来启动 Spring Boot 项目的引导。最简单的方法是使用Spring Initializr,这是一个在线配置器,帮助生成带有所有必要依赖项的项目结构。

在这种情况下,我们将使用来自红帽 Maven 仓库支持的 Spring Boot 版本,使用在表 2-2 中定义的项目元数据。

表 2-2. Spring Boot Maven 项目选项

描述
modelVersion 4.0.0 POM 模型版本(始终为 4.0.0)。
groupId com.redhat.cloudnative 项目所属的组或组织。通常表示为反转的域名。
artifactId catalog 项目库组件的名称(例如其 JAR 或 WAR 文件的名称)。
version 1.0-SNAPSHOT 正在构建的项目版本。
name CoolStore Catalog Service 应用程序的名称。
description CoolStore Catalog Service with Spring Boot 应用程序的描述。

让我们来看看我们的pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project
  xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"↳
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0↳
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.redhat.cloudnative</groupId> ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
  <artifactId>catalog</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>CoolStore Catalog Service</name>
  <description>CoolStore Catalog Service with Spring Boot</description>
  <properties>
    <spring-boot.version>2.1.6.SP3-redhat-00001</spring-boot.version> ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
    <spring-boot.maven.plugin.version>2.1.4.RELEASE-redhat-00001↳ </spring-boot.maven.plugin.version>
    <spring.k8s.bom.version>1.0.3.RELEASE</spring.k8s.bom.version>
    <fabric8.maven.plugin.version>4.3.0</fabric8.maven.plugin.version>
  </properties>
  <repositories>
    <repository>
      <id>redhat-ga</id>
      <url>https://maven.repository.redhat.com/ga/</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>redhat-ga-plugins</id>
      <url>https://maven.repository.redhat.com/ga/</url>
    </pluginRepository>
  </pluginRepositories>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>me.snowdrop</groupId>
        <artifactId>spring-boot-bom</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-kubernetes-dependencies</artifactId>
        <version>${spring.k8s.bom.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies> ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-kubernetes-config</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
    </dependency>
  </dependencies> ... </project>

1

我们通过 Initializr 或手动生成的项目元数据

2

使用的 Spring Boot 版本

3

我们需要的依赖项:

  • JPA:Spring Data 与 JPA

  • Spring Cloud:Spring 用于云原生 Java 应用程序的支持和工具

  • H2:我们将用于此目的的内存数据库

这是一个支持 RESTful 服务和使用 Spring Data 与 JPA 连接数据库的最小 Spring Boot 项目。任何新项目除了主类(在本例中为 CatalogApplication 类,用于引导 Spring Boot 应用程序)外都不包含代码。

你可以在这本书的 GitHub 仓库中找到它:book’s GitHub repository

package com.redhat.cloudnative.catalog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
public class CatalogApplication {

    public static void main(String[] args) {
        SpringApplication.run(CatalogApplication.class, args);
    }
}

1

一个方便的注解,它添加了自动配置和组件扫描,并且还可以定义额外的配置。它等同于使用 @Configuration@EnableAutoConfiguration@ComponentScan 以它们的默认属性。

创建一个领域模型

接下来,我们需要提供一些数据来消费我们电子商务网站 Coolstore 的目录微服务。同样在这里,我们定义了一个与持久化层高级交互的领域模型,以及一个接口,用于暴露服务的 REST 端点和数据模型之间的通信(如 Figure 2-6 所示)。

数据模型流程

图 2-6. 数据模型流程

数据库是使用 Spring 应用程序配置文件进行配置的,该文件位于 application.properties 属性文件中。让我们查看此文件以查看数据库连接详细信息。

你可以在这本书的 GitHub 仓库中找到它:book’s GitHub repository

spring.application.name=catalog
server.port=8080
spring.datasource.url=jdbc:h2:mem:catalog;DB_CLOSE_ON_EXIT=FALSE ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png) spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)

1

H2 数据库的 JDBC URL

2

使用 H2 内存数据库

让我们创建我们的领域模型,这与我们之前为库存微服务创建的模型类似。

你可以在这本书的 GitHub 仓库中找到它:book’s GitHub repository

package com.redhat.cloudnative.catalog;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
@Table(name = "PRODUCT") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
public class Product implements Serializable {

  private static final long serialVersionUID = 1L;

  @Id ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
  private String itemId;

  private String name;

  private String description;

  private double price;

  public Product() {
  }

  public String getItemId() {
    return itemId;
  }

  public void setItemId(String itemId) {
    this.itemId = itemId;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getDescription() {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public double getPrice() {
    return price;
  }

  public void setPrice(double price) {
    this.price = price;
  }

  @Override
  public String toString() {
    return "Product [itemId=" + itemId + ", name=" + name
      + ", price=" + price + "]";
  }
}

1

@Entity 将类标记为 JPA 实体。

2

@Table 通过定义表名和数据库约束(在本例中为一个名为 CATALOG 的表)自定义了表创建过程。

3

@Id 标记表的主键。

创建一个数据仓库

Spring Data 仓库抽象简化了在 Spring 应用程序中处理数据模型的过程,通过减少实现各种持久性存储的数据访问层所需的样板代码量。Repository 及其子接口 是 Spring Data 的核心概念,是为正在管理的实体类提供数据操作功能的标记接口。应用程序启动时,Spring 查找所有标记为仓库的接口,对于每个找到的接口,基础设施会配置所需的持久技术,并为仓库接口提供实现。

我们现在将在com.redhat.cloudnative.catalog包中创建一个名为 ProductRepository 的新的 Java 接口,并扩展CrudRepository 接口以指示 Spring 想要公开一整套方法来操作实体。

你可以在这本书的 GitHub 仓库中找到它:

package com.redhat.cloudnative.catalog;

import org.springframework.data.repository.CrudRepository;

public interface ProductRepository extends CrudRepository<Product, String> { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
}

1

CrudRepository:用于指示 Spring 我们想要公开一整套方法来操作实体的接口

现在我们有了一个领域模型和一个用于检索领域模型的存储库,让我们创建一个返回产品列表的 RESTful 服务。

创建一个 RESTful 服务

Spring Boot 在 Spring 应用程序中使用 Spring Web MVC 作为默认的 RESTful 栈。现在我们将在com.redhat.cloudnative.catalog包中创建一个名为CatalogController的新的 Java 类,用于公开一个 REST 端点。我们将使用以下内容:

  • 路径:/api/catalog/

  • HTTP 方法:GET

这返回了商店中所有可用项目的目录,匹配来自 Inventory 服务的项目与来自 Catalog 服务的数据。

你可以在这本书的 GitHub 仓库中找到它:

package com.redhat.cloudnative.catalog;

import java.util.List;
import java.util.Spliterator;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/api/catalog") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
public class CatalogController {

    @Autowired ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
    private ProductRepository repository; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)

    @ResponseBody
    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public List<Product> getAll() {
        Spliterator<Product> products = repository.findAll().spliterator();
        return
          StreamSupport.stream(products, false).collect(Collectors.toList());
    }
}

1

@RequestMapping表示上述 REST 服务定义了一个通过 HTTP GET 可访问的端点,位于/api/catalog

2

Spring Boot 在运行时自动为ProductRepository提供实现,并使用@Autowired注解将其注入到控制器中。

3

控制器类上的repository属性用于从数据库中检索产品列表。

现在一切准备就绪,可以启动我们的第二个微服务了,它将监听端口 9000 以避免与其他端口冲突:

mvn spring-boot:run

您应该看到类似于这样的输出:

[INFO] --- spring-boot-maven-plugin:2.1.4.RELEASE-redhat-00001:run (default-cli)
↳ @ catalog ---
[INFO] Attaching agents: []
2020-12-02 17:12:18.528  INFO 61053 --- [           main]↳
trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.auto
configure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.
springframework.cloud.autoconfigure.ConfigurationPropertiesRebinder
AutoConfiguration$$EnhancerBySpringCGLIB$$e898759c] is not eligible for getting
processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.6.RELEASE)
StandardService   : Starting service [Tomcat]
2020-12-02 17:12:20.064  INFO 61053 --- [           main]↳
org.apache.catalina.core.StandardEngine  : Starting Servlet Engine:
  Apache Tomcat/9.0.7.redhat-16
2020-12-02 17:12:20.220  INFO 61053 --- [           main]↳
o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded
  WebApplicationContext
2020-12-02 17:12:20.220  INFO 61053 --- [           main]↳
...

您的应用程序现在正在监听端口9000以及我们配置的端点;您可以通过导航至http://localhost:9000/api/catalog来验证。

您应该看到来自 REST API 返回表示产品列表的 JSON 对象的输出:

[
   {
      "itemId":"100000",
      "name":"Red Fedora",
      "description":"Official Red Hat Fedora",
      "price":34.99
   },
   {
      "itemId":"329299",
      "name":"Quarkus T-shirt",
      "description":"This updated unisex essential fits like a well-loved
 favorite,↳ featuring a crew neck, short sleeves and designed with superior
 combed and↳ ring- spun cotton.",
      "price":10.0
   },
   {
      "itemId":"329199",
      "name":"Pronounced Kubernetes",
      "description":"Kubernetes is changing how enterprises work in the cloud.↳
 But one of the biggest questions people have is: How do you pronounce it?",
      "price":9.0
   },
   {
      "itemId":"165613",
      "name":"Knit socks",
      "description":"Your brand will get noticed on these full color knit
 socks.↳ Imported.",
      "price":4.15
   },
   {
      "itemId":"165614",
      "name":"Quarkus H2Go water bottle",
      "description":"Sporty 16\. 9 oz double wall stainless steel thermal bottle↳
 with copper vacuum insulation, and threaded insulated lid. Imprinted.
 Imported.",
      "price":14.45
   },
   {
      "itemId":"165954",
      "name":"Patagonia Refugio pack 28L",
      "description":"Made from 630-denier 100% nylon (50% recycled / 50%
 high-tenacity)↳ plain weave; lined with 200-denier 100% recycled polyester.
 ...",
      "price":6.0
   },
   {
      "itemId":"444434",
      "name":"Red Hat Impact T-shirt",
      "description":"This 4\. 3 ounce, 60% combed ringspun cotton/40% polyester↳
 jersey t- shirt features a slightly heathered appearance. The fabric↳
 laundered for reduced shrinkage. Next Level brand apparel. Printed.",
      "price":9.0
   },
   {
      "itemId":"444435",
      "name":"Quarkus twill cap",
      "description":"100% cotton chino twill cap with an unstructured,
 low-profile,↳ six-panel design. The crown measures 3 1/8 and this
 features a Permacurv↳ visor and a buckle closure with a grommet.",
      "price":13.0
   },
   {
      "itemId":"444437",
      "name":"Nanobloc Universal Webcam Cover",
      "description":"NanoBloc Webcam Cover fits phone, laptop, desktop, PC,↳
 MacBook Pro, iMac, ...",
      "price":2.75
   }
]
注意

书中代码清单中的输出已经以pretty模式进行了格式化。您将注意到我们从 Quarkus Inventory 微服务中获取的项目与 Spring Boot Catalog 微服务中的描述和价格的组合。如果您回想起先前对项目 329299 的测试信息,那就是一件 Quarkus T 恤。

祝贺您创建了第二个微服务;现在是时候将前端连接到我们的后端了。为了做到这一点,我们将在下一节中使用具有反应式 Java 的软件 API 网关。

使用 Vert.x 创建网关服务

Eclipse Vert.x 是一个在 Java 虚拟机(JVM)上构建反应式应用程序的事件驱动工具包。Vert.x 不强加特定的框架或打包模型;它可以在现有应用程序和框架中使用,只需将 Vert.x jar 文件添加到应用程序类路径中即可添加反应式功能。

Eclipse Vert.x 使构建符合 反应式宣言 定义的反应式系统成为可能,并构建了以下服务:

  • Responsive: 处理请求的响应时间合理

  • Resilient: 面对故障仍能保持响应

  • Elastic: 在各种负载下保持响应,并能够进行纵向和横向扩展

  • Message-driven: 组件使用异步消息传递进行交互

它被设计为事件驱动和非阻塞的。事实上,事件被传递到一个绝不能被阻塞的事件循环中。与传统应用程序不同,Vert.x 仅使用非常少量的线程负责将事件分派给事件处理程序。如果事件循环被阻塞,事件将不再被传递,因此代码需要注意这种执行模型(如图 2-7 所示)。

Vert.x 事件循环

图 2-7. Vert.x 事件循环

在我们的架构中,这个微服务将作为一个异步软件 API 网关,开发为一个反应式 Java 微服务,它能够高效地路由和分发流量到我们的云原生电子商务网站的库存和目录组件,如图 2-8 所示。

API 网关 Vert.x 微服务

图 2-8. API 网关 Vert.x 微服务

你可以在这本书的 GitHub 仓库 中找到这个微服务的源代码。

创建一个 Vert.x Maven 项目

Vert.x 支持 Maven 和 Gradle,而启动一个新的 Vert.x Maven 项目的最简单方法是通过 Vert.x 社区提供的模板项目结构。在我们的案例中,我们使用了 Red Hat Maven 仓库,并添加了 表 2-3 中显示的设置。

表 2-3. Vert.x Maven 项目选项

Key Value 描述
modelVersion 4.0.0 POM 模型版本(始终为 4.0.0)。
groupId com.redhat.cloudnative 项目所属的组织或机构。通常表示为倒置的域名。
artifactId gateway 项目库 artifact(本例中为 JAR 文件)的名称。
version 1.0-SNAPSHOT 正在构建的项目版本。
name CoolStore 网关服务 应用程序的名称。

让我们看看 pom.xml 的内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"↳
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"↳
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion> ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
    <groupId>com.redhat.cloudnative</groupId>
    <artifactId>gateway</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>CoolStore Gateway Service</name>
    <description>CoolStore Gateway Service with Eclipse Vert.x</description>

    <properties>
        <vertx.version>3.6.3.redhat-00009</vertx.version> ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
        <vertx-maven-plugin.version>1.0.15</vertx-maven-plugin.version>
        <vertx.verticle>com.redhat.cloudnative.gateway.GatewayVerticle↳ </vertx.verticle> ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
        <fabric8.maven.plugin.version>4.3.0</fabric8.maven.plugin.version>
        <slf4j.version>1.7.21</slf4j.version>
    </properties> ... <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.vertx</groupId>
                <artifactId>vertx-dependencies</artifactId>
                <version>${vertx.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies> ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png)
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-core</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-config</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-web-client</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-rx-java2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-health-check</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
    </dependencies> ... </project>

1

项目元数据

2

使用的 Vert.x 版本

3

GatewayVerticle:主 verticle 的名称;它是我们应用程序的入口点

4

依赖列表:

  • Vert.x 库:vertx-core, vertx-config, vertx-web, vertx-web-client

  • Rx 支持 Vert.x:vertx-rx-java2

创建一个 API 网关

接下来,我们希望创建一个 API 网关作为我们网站的 Web 前端的入口点,以从一个地方访问所有后端服务。这种模式可预见地称为API 网关,在微服务架构中是一种常见做法。

在 Vert.x 中,部署单位称为verticle。 verticle 在事件循环上处理传入事件,事件可以是任何内容,例如接收网络缓冲区,计时事件或其他 verticle 发送的消息。

我们将主 verticle 定义为 GatewayVerticle,因为我们之前在pom.xml中声明过它,并公开 REST 端点,该端点将路由到 Catalog 的/api/catalog

  • 路径:/api/catalog/

  • HTTP 方法:GET

这将流量路由到 Catalog,并返回一个 JSON 对象,其中包含商店中所有可用的商品,将 Inventory 服务的匹配商品与 Catalog 服务的数据匹配。

您可以在这本书的 GitHub 存储库中找到它:

package com.redhat.cloudnative.gateway;

import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.reactivex.config.ConfigRetriever;
import io.vertx.reactivex.core.AbstractVerticle;
import io.vertx.reactivex.ext.web.Router;
import io.vertx.reactivex.ext.web.RoutingContext;
import io.vertx.reactivex.ext.web.client.WebClient;
import io.vertx.reactivex.ext.web.client.predicate.ResponsePredicate;
import io.vertx.reactivex.ext.web.codec.BodyCodec;
import io.vertx.reactivex.ext.web.handler.CorsHandler;
import io.vertx.reactivex.ext.web.handler.StaticHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.reactivex.Observable;
import io.reactivex.Single;

import java.util.ArrayList;
import java.util.List;

public class GatewayVerticle extends AbstractVerticle { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
    private static final Logger LOG = LoggerFactory.getLogger(
        GatewayVerticle.class);

    private WebClient catalog;
    private WebClient inventory;

    @Override
    public void start() { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
        Router router = Router.router(vertx); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
        router.route().handler(CorsHandler.create("*")↳
        .allowedMethod(HttpMethod.GET));
        router.get("/*").handler(StaticHandler.create("assets"));
        router.get("/health").handler(this::health);
        router.get("/api/products").handler(this::products); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png)

        ConfigRetriever retriever = ConfigRetriever.create(vertx);
        retriever.getConfig(ar -> {
            if (ar.failed()) {
                // Failed to retrieve the configuration
            } else {
                JsonObject config = ar.result();

                String catalogApiHost =↳
                config.getString("COMPONENT_CATALOG_HOST", "localhost");
                Integer catalogApiPort =↳
                config.getInteger("COMPONENT_CATALOG_PORT", 9000);

                catalog = WebClient.create(vertx,
                    new WebClientOptions()
                        .setDefaultHost(catalogApiHost)
                        .setDefaultPort(catalogApiPort)); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/5.png)

                LOG.info("Catalog Service Endpoint: " + catalogApiHost↳
                + ":" + catalogApiPort.toString());

                String inventoryApiHost =↳
                config.getString("COMPONENT_INVENTORY_HOST", "localhost");
                Integer inventoryApiPort =↳
                config.getInteger("COMPONENT_INVENTORY_PORT", 8080;

                inventory = WebClient.create(vertx,
                    new WebClientOptions()
                        .setDefaultHost(inventoryApiHost)
                        .setDefaultPort(inventoryApiPort)); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/6.png)

                LOG.info("Inventory Service Endpoint: "↳
                + inventoryApiHost + ":" + inventoryApiPort.toString());

                vertx.createHttpServer()
                    .requestHandler(router)
                    .listen(Integer.getInteger("http.port", 8090)); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/7.png)

                LOG.info("Server is running on port "↳
                + Integer.getInteger("http.port", 8090));
            }
        });
    }

    private void products(RoutingContext rc) {
 ...
    }

    private Single<JsonObject> getAvailabilityFromInventory(JsonObject product) {
...
    }

    private void health(RoutingContext rc) {
...
    }
}

1

通过扩展AbstractVerticle类来创建一个Verticle

2

start()方法创建一个 HTTP 服务器。

3

检索Router以映射 REST 端点。

4

创建一个 REST 端点,通过product()函数映射/api/catalog Catalog 端点以检索内容。

5

创建一个在端口 8090 上监听的 HTTP 服务器。

6

Inventory微服务提供主机名和端口以连接。

7

该微服务支持通过 Properties 更改其主机名和端口的 ENV vars;这对我们的架构在云中的可移植性非常重要。

注意

我们使用端口 8090 来避免在本地开发中发生冲突。端口号也可以通过属性文件更改,如Vert.x 配置文档中所述。在使用微服务开发时,强烈建议使用环境变量来动态映射主机和端口;我们使用它们来动态映射 Inventory 和 Catalog 端点。

我们现在准备启动我们的 API 网关:

mvn compile vertx:run

输出应类似于此:

[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.redhat.cloudnative:gateway >-------------------
[INFO] Building CoolStore Gateway Service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- vertx-maven-plugin:1.0.15:initialize (vmp) @ gateway ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ gateway ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources,↳
i.e. build is platform dependent!
[INFO] Copying 3 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.6.1:compile (default-compile) @ gateway ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8,↳
i.e. build is platform dependent!
[INFO] Compiling 1 source file to↳
/home/bluesman/git/cloud-native-java2/↳
labs/gateway-vertx/target/classes
...
 com.redhat.cloudnative.gateway.GatewayVerticle↳
lambda$start$0
[INFO] INFO: Catalog Service Endpoint: localhost:9000
[INFO] dic 02, 2020 6:56:56 PM com.redhat.cloudnative.gateway.GatewayVerticle↳
lambda$start$0
[INFO] INFO: Inventory Service Endpoint: localhost:8080
[INFO] dic 02, 2020 6:56:56 PM com.redhat.cloudnative.gateway.GatewayVerticle↳
lambda$start$0
[INFO] INFO: Server is running on port 8090
[INFO] dic 02, 2020 6:56:56 PM

通过导航到http://localhost:8090/api/products来验证它是否已启动并正确路由流量。

您应该从目录的端点获取 JSON 对象,格式为漂亮的格式:

[ {
  "itemId" : "165613",
  "name" : "Knit socks",
  "description" : "Your brand will get noticed on these full color knit socks.↳
 Imported.",
  "price" : 4.15,
  "availability" : {
    "quantity" : 45
  }
}, {
  "itemId" : "165614",
  "name" : "Quarkus H2Go water bottle",
  "description" : "Sporty 16\. 9 oz double wall stainless steel thermal bottle↳
 with copper vacuum insulation, and threaded insulated lid. Imprinted.
 Imported.",
  "price" : 14.45,
  "availability" : {
    "quantity" : 87
  }
}, {
  "itemId" : "329199",
  "name" : "Pronounced Kubernetes",
  "description" : "Kubernetes is changing how enterprises work in the cloud.↳
 But one of the biggest questions people have is: How do you pronounce it?",
  "price" : 9.0,
  "availability" : {
    "quantity" : 12
  }
}, {
  "itemId" : "100000",
  "name" : "Red Fedora",
  "description" : "Official Red Hat Fedora",
  "price" : 34.99,
  "availability" : {
    "quantity" : 0
  }
}, {
  "itemId" : "329299",
  "name" : "Quarkus T-shirt",
  "description" : "This updated unisex essential fits like a well-loved favorite,
 ↳ featuring a crew neck, short sleeves and designed with superior combed
 and ring-↳ spun cotton.",
  "price" : 10.0,
  "availability" : {
    "quantity" : 35
  }
}, {
  "itemId" : "165954",
  "name" : "Patagonia Refugio pack 28L",
  "description" : "Made from 630-denier 100% nylon (50% recycled/50% ↳
 high-tenacity) plain weave; lined with 200-denier 100% recycled polyester...",
  "price" : 6.0,
  "availability" : {
    "quantity" : 43
  }
}, {
  "itemId" : "444434",
  "name" : "Red Hat Impact T-shirt",
  "description" : "This 4\. 3 ounce, 60% combed ringspun cotton/40% polyester↳
 jersey t- shirt features a slightly heathered appearance. The fabric laundered↳
 for reduced shrinkage. Next Level brand apparel. Printed.",
  "price" : 9.0,
  "availability" : {
    "quantity" : 32
  }
}, {
  "itemId" : "444437",
  "name" : "Nanobloc Universal Webcam Cover",
  "description" : "NanoBloc Webcam Cover fits phone, laptop, desktop, PC,↳
 MacBook Pro, iMac, ...",
  "price" : 2.75
}, {
  "itemId" : "444435",
  "name" : "Quarkus twill cap",
  "description" : "100% cotton chino twill cap with an unstructured,
 low-profile,↳ six-panel design. The crown measures 3 1/8 and this features a
 Permacurv↳ visor and a buckle closure with a grommet.",
  "price" : 13.0,
  "availability" : {
    "quantity" : 53
  }
} ]

我们的后端现在已经完成。我们准备好提供一些数据,以展示一个漂亮的前端。

使用 Node.js 和 AngularJS 创建前端

Node.js 是一个流行的开源框架,用于异步事件驱动的 JavaScript 开发。即使这是一本关于现代 Java 开发的书籍,在微服务架构中,通常会涉及多种编程语言和框架的异构环境。这里的挑战在于如何让它们有效地进行通信。一种解决方案是通过 REST 调用或队列系统交换消息的 API 网关提供一个共同的接口。

AngularJS 是一个基于 JavaScript 的前端 Web 框架,其目标是通过提供客户端模型-视图-控制器(MVC)和模型-视图-视图模型(MVVM)架构的框架,简化这类应用程序的开发和测试,如 Figure 2-9 所示。与 Node.js 一起使用时,它提供了一个快速启动前端的方式。

Node.js + AngularJS 仪表板

图 2-9. Node.js + AngularJS 仪表板

您可以在这本 书籍的 GitHub 仓库 中找到这个微服务的源代码。

运行前端

所有 HTML 和 JavaScript 代码已准备好,我们准备将此前端与我们的后端连接起来,显示我们的 Coolstore 应用程序已经运行。

获取 NPM

NPM 是一个类似于 Maven 的 JavaScript 包管理器,将帮助我们下载所有依赖项并启动我们的前端。

安装依赖项

我们可以在 web-nodejs 目录中解析所有依赖项,并通过启动 npm 命令来完成。

npm install

你应该得到这样的输出:

...
added 1465 packages from 723 contributors and audited 1471 packages in 26.368s

52 packages are looking for funding
  run `npm fund` for details

found 228 vulnerabilities (222 low, 6 high)
  run `npm audit fix` to fix them, or `npm audit` for details

启动应用程序

现在我们已准备好验证我们的前端是否能正确地通过 API 网关消费后端服务,映射图像与接收到的数据。由于我们处于本地开发环境,我们将使用环境变量将 Node.js 的默认端口更改为避免冲突。我们还将使用一个环境变量来映射 API 网关的 REST 端点,如 Table 2-4 所示。

表 2-4. 前端环境变量

ENV Value Description
PORT 3000 全局环境变量,用于 Node.js 映射用于启动进程的端口;在这种情况下我们使用 3000。
COOLSTORE_GW_ENDPOINT http://localhost:8090 在前端定义的环境变量,用于映射 API 网关服务的主机名。

使用以下命令启动应用程序:

COOLSTORE_GW_ENDPOINT=http://localhost:8090 PORT=3000 npm start

转到我们公开的 Node.js 应用程序的地址 http://localhost:3000

恭喜!您的云原生 Coolstore 电子商务网站现在已经运行起来了;您可以在 Figure 2-10 中验证它。

Coolstore 演示完成

图 2-10. Coolstore 演示完成

摘要

在本章中,我们走过了一个完整的基于微服务的架构实现,使用不同的 Java 框架来实现各个组件。我们概述了如何将典型的单片式方法拆分为更多样化和异构化的环境,轻量且能够在多种场景下运行,比如本地开发或生产系统。这是我们所称的云原生开发的一个示例。

第三章:轻装上路

欲望旅行愉快者,必先轻装上路。

圣埃克絮佩里

在上一章中,您构建了一个基于微服务的系统,并且我们还向您展示了从现有应用程序迁移的一些步骤。但是所有示例的挑战在于它们为了更易于理解而删除了复杂性。对于较小的示例来说可能很清晰的事情,但在实际的业务系统中变得具有挑战性。特别是,请考虑复杂的遗留系统。正如第一章概述的那样,多年来开发的技术和方法已经导致了今天用于开发现代企业系统的最佳实践和工具。仅仅因为我们的行业现在拥有了更广泛的工具箱,可以使用闪亮新东西来工作,并不意味着您应该总是使用它们。如果您考虑到这一点以及我们日益增长的框架、方法和技术的数量,那么一个问题变得更加紧迫:对于您的下一个系统,您应该使用什么工具和架构,以及您将如何在何处运行它们?在您做出决定之前,您需要对过去几年中出现的最突出的企业应用程序架构风格进行一些思考(三层架构、企业集成、面向服务架构、微服务和事件驱动架构)。

三层或分布式系统

企业 Java 世界主要由单片应用程序所主导。它们通常被设计为随服务器实例和群集功能扩展的单一执行单元。它们也经常被称为“三层系统”,以反映它们由三个主要部分组成:客户端用户界面、服务器端业务逻辑实现和服务器端数据持久性或集成层。服务器端部分被称为“单体”,因为它们被打包为单个大型可执行文件。对系统的任何更改通常都涉及构建和部署新版本。

提示

了解更多关于构建微服务的信息,请参阅 Sam Newman 的杰出著作 Building Microservices(O’Reilly),目前已经推出第二版。

基于微服务的架构是开发单个应用程序的一种方法,将其作为一套小型服务的集合,每个服务在自己的进程中运行,并使用轻量级机制进行通信,通常是 HTTP 资源 API 或作为事件驱动架构(EDA)的一部分。这些服务围绕业务能力构建,并且可以通过完全自动化的部署机制独立部署。这些服务的集中管理最小化,它们可能是用不同的编程语言编写的,并且使用不同的数据存储技术。

单体架构和微服务风格之间的差异可以说是根本性的。同样的,非功能性需求也决定了采用哪种架构风格。最关键的需求源于极其灵活的扩展场景。作为一名经验丰富的开发者和架构师,您知道如何评估功能和非功能需求,以确定您具体项目的最佳选择。在本章中,我们将帮助您制定迁移策略和目标平台。您的旅程始于审视现代化的动机。让我们深入探讨一下普遍的现代化动机以及寻找机会的起点。

技术更新、现代化和转型

企业软件的开发旨在将业务价值转化为能够在非功能性和功能性需求内执行的代码。创造价值取决于我们快速交付应用程序的能力。不仅要提高质量,而且要快速响应变化,使企业能够应对市场上的新挑战或监管变化。而这些挑战是多方面的。首先,您通过云原生应用程序解决扩展挑战,以处理更大的交易量。新的业务案例还将要求您进一步分析数据,并可能通过人工智能(AI)和机器学习(ML)来解决。最后但同样重要的是,我们互联互通的世界从物联网(IoT)产生更多数据。从架构上看似乎是自然的进展,但实际上,不断变化的业务需求通过改变功能和非功能性需求来推动现代化和架构演进。

此外,您还会发现操作上的考虑在影响现代化需求方面起到了作用。例如,过期的维护合同或过时的技术可能促使技术更新。随着 Java 语言不断发展和缩短的发布周期,也会影响现代化决策。现代化可以发生在项目的任何层次,从执行环境(例如虚拟机到容器)到 Java 虚拟机(JVM 版本或供应商)、个别依赖项、外部接口和服务。

在这里,区分现代化的三个不同角度至关重要。虽然在现有流程和边界内进行的技术更新对软件项目来说是一个熟悉且早已建立的挑战,但现代化涉及的是另一种事物。通常与“数字化”一词搭配使用,“现代化”一词指的是采用新技术。它涉及使用新功能升级系统、平台和软件。可以是简单的将现有的纸质流程转变为使用新软件和硬件的数字流程,也可以是更复杂的任务,例如逐步淘汰现有基础设施并转移到云端。有时您会在讨论现代系统时听到“转型”。数字化转型意味着利用现代技术重新构想组织的流程、文化、人员和客户体验。它可能导致新的商业模型、收入流、政策和价值观。转型是一个对组织进行全面审视的镜头,专注于从根本上改变业务绩效。现代化被内嵌并成为软件开发人员和架构师需要导航的核心。

尽管您在现代化应用程序的首要步骤中有项目特定的原因,但必须记住,现代化本身并不对特定目标环境或技术有任何特定的要求。它是一组不断变化和增长的候选技术,使公司能够在其行业中竞争和成长。您可以在技术趋势报告(例如,ThoughtWorks 技术雷达)或炒作周期(Gartner 炒作周期)中找到其中一些。但正如您在第一章中看到的那样,持续创新的两个最强动力是速度和成本压力。现代化、云原生和基于微服务的架构都能解决这两个问题。

The 6 Rs

现在您已经了解了应用现代化背后的动机,您希望确定现代化的一般方法,并为现有应用程序定义分类。这样做有助于您管理各种不同的应用程序,特别是在平台现代化项目中。与查看单个应用程序的细节不同,考虑传统企业 Java 应用程序的完整运行时架构。在这种情况下,您通常会识别出本地硬件,这些硬件通常是虚拟化的,并通过一组单独的实例提供给项目使用。鉴于很少有单个项目被视为没有任何集成系统的孤立岛屿,您需要找到一个协调的方法,来处理不止一个项目的情况。

让我们首先看看 6 个 R 是什么,以及这个概念的来源。基本上,您可以将每个“R”看作是应用程序可用的迁移策略。每种策略都表示转换后应用程序的明确结果,但不一定是实际的迁移步骤。这个概念最初由 Gartner 分析师 Richard Watson 在 2011 年提到。最初的五种策略——即重新主机化、重构、修改、重建和替换——在 2016 年由 AWS 的 Stephen Orban 在一篇流行的博客文章中进行了修订和适应。Orban 保留了一些 Gartner 的策略,并添加了一个新的策略。因此,5 个 R 变成了 6 个 R。今天,这 6 个 R 被用作几乎任何云转型的基本指南。尽管对是否应该添加更多策略仍然存在争议,甚至可以找到 7 个 R,但在本书中我们坚持使用 6 个 R,如图 3-1 所示。

分类现有应用程序的六种方法

图 3-1。六种现代化方法,6 个 R 的概述

保留-稍后或根本不进行现代化

每个人都听过一个著名公司地下室里的大型机的刻板故事,那里存储了所有商业机密。这些大型机通常以 CICS(客户信息控制系统,一系列混合语言应用服务器,为 IBM 大型机系统上的在线事务管理和连接提供支持)编程,并且数据存储在 IMS(IBM 信息管理系统,一种早期的数据库)中。这并不一定是件坏事。也许现有系统非常适合业务,并且不需要参与现代化项目。为了正确确定转型和现代化工作的范围,您需要识别这些系统并将其排除在现代化进程之外。这类系统需要一种特定的集成方法,需要明确设计。想象一下,一个高度可扩展的移动应用后端直接连接到大型机上。在这种情况下,来自可能众多移动设备的请求会过载昂贵的大型机。在这种情况下,“保留”并不意味着“不动”,而是意味着“不迁移”。

关闭系统-关闭系统

有些候选人可能已经显然到达了生命周期的终点,已经迁移并替换,或者仅仅是一个未来不再需要的遗物。轻装前行,并确保标记这些系统。后续的清理工作与建设新事物同样重要。投入时间来验证并决定退役一个系统的价值,与重新设计同样宝贵。

重新购买-购买新版本

在某些情况下,您可以重新购买现成的软件并为其新的执行环境做好准备。这听起来很简单,但很可能需要包括迁移项目和重新评估功能列表,主要是因为您不太可能在不更改产品版本或其 API 的情况下进行更新。在一些罕见的情况下,您甚至可能会发现缺失的集成文档成为一个阻碍因素。这就是为什么将其视为现代化项目而不是简单的软件更新至关重要的原因。

重新托管—放入容器中

通常称为“提升和迁移”的一种选项是将现有架构简单地移植到容器内运行。尽管这听起来很简单,但在实施过程中可能会遇到一些挑战。特别是在优化 JVM 以适应受限容器运行时时可能会遇到困难。一些现有的中间件应用服务器配备了供应商支持的基础镜像,使切换运行时变得更加便捷。对于有状态应用程序运行时,应重点关注存储。Java 应用服务器需要一些数据在容器重新启动时保留,并需要持久卷映射。事务、负载平衡和内存会话复制需要扩展配置以确保正确的关闭行为和节点通信。计划充分的研究和测试,并确保遵循供应商的建议。此步骤解决的是基础设施现代化问题,并不直接涉及应用程序代码。符合此方法的现有应用程序是那些需要在重构之前或在切换数据中心概念的中间步骤之前移至容器运行时的应用程序。

注意

Martin Fowler 创造了术语“strangler pattern”,作为从单片应用中提取功能的一种方法。它以澳大利亚的勒死榕树为名,后者会从树枝上的种子生长根直至触及地面。

重新平台化—进行轻微调整

作为重新托管的延伸,重新平台化将经历概念或功能变化的应用程序分类为运行时切换。它也可以按照其“lift”名称变体,“提升和调整”进行引用。它可能与被勒死的功能相关联,后者可能在新技术堆栈上实现,或者在数据存储或集成系统中进行更改。我们建议将此方法用作重构和解耦合单片应用程序的初始步骤。在以下扩展和解耦合阶段执行时,通过在此步骤前加入重新平台化,可以实现更顺畅的操作。选择重新平台化,您正在为现代化应用程序和务实演化提供温和的起始。

重构—建立新的

重构是一种有纪律的技术,用于重构现有代码库,改变其内部结构而不改变其外部行为。重构是将现有应用程序迁移到新的运行时或平台上最耗时和昂贵的方式。它可能包括或不包括切换到不同的架构风格或本地或云主机。

分割和容器化

现在我们已经看过了现有应用程序的不同现代化策略,也知道如何何时应用它们。是时候考虑我们目标平台的其他先决条件了。

Kubernetes 作为新的应用服务器?

“平台”一词在企业 Java 世界中通常指应用服务器。应用服务器遵循有保护栏的软件开发方法,具有标准化的 API。垂直层次由通常被称为三层系统技术层的内容定义。在顶部是业务,下面是数据访问和/或集成。水平地,我们通常会找到业务组件或领域。虽然垂直层通常是良好分离和解耦的,但在水平组件之间通常会发现共享类和违反的访问规则。如果这种情况在代码库中频繁发生,我们称之为纠缠设计,随着时间的推移会变成难以维护的单块。但无论应用代码多么纠缠,它仍从标准应用服务器功能中受益,这些功能包括安全性、隔离、容错、事务管理、配置管理等。

如果我们快进到今天的分布式架构,应用程序由许多小服务组成,我们会观察到两件事:没有捷径可以获得良好的组件设计,标准应用服务器功能不再适用于我们的组件。

第一个观察结果导致了一个强制性要求。分布式服务必须是设计良好、松耦合和强封装的组件。我们将在第五章更多地讨论现代化单块的设计原则和方法。第二个观察结果列出了云原生运行时中缺失的功能清单。如果一个应用服务器不提供像我们提到的常用功能的支持,那么只剩下两个选择。一个可以是微服务框架的选择(例如 Quarkus),另一个可能是基于 Kubernetes 的附加框架或产品。

让我们在接下来的章节中详细查看一些最关键的功能需求。我们称它们为微服务能力。这个术语指的是除了业务逻辑外,一个服务必须实现的一系列横切关注点,以解决这些问题,如图 3-2 所总结的。

分布式应用程序的微服务化

图 3-2。分布式应用程序的微服务化

发现和配置

容器镜像是不可变的。在其内部存储不同环境或阶段的配置选项是不被鼓励的。相反,配置必须被外部化并通过实例进行配置。外部化配置也是云原生应用程序的关键原则之一。服务发现是从运行时环境获取配置信息的一种方式,而不是硬编码在应用程序中。其他方法包括使用 ConfigMaps 和 Secrets。Kubernetes 提供开箱即用的服务发现,但这可能不足以满足您的应用程序需求。虽然您可以通过 YAML 文件管理每个运行时环境的环境设置,但额外的用户界面或命令行界面可以使 DevOps 团队更轻松地共享责任。

基本调用

在容器内运行的应用程序通过 Ingress 控制器访问。Ingress 公开从集群外部到集群内部服务的 HTTP 和 HTTPS 路由。流量路由由在 Ingress 资源上定义的规则控制。传统上,这可以与基于 Apache HTTP 的负载均衡器进行比较。其他替代方案包括像 HAProxy 或 Nginx 的项目。您可以利用路由能力来进行滚动部署,作为复杂 CI/CD 策略的基础。对于像批处理这样的一次性作业,Kubernetes 提供了作业和定时作业功能。

弹性

Kubernetes 的 ReplicaSets 控制 pod 的扩展。这是一种协调期望状态的方式:你告诉 Kubernetes 系统应该处于什么状态,以便它能够确定如何达到预期结果。ReplicaSet 控制容器的副本数量,即任何时候应该运行的确切副本或拷贝数量。听起来像是一个大部分静态操作的东西可以被自动化。水平 Pod 自动缩放器基于观察到的 CPU 利用率来调整 pod 数量。可以使用自定义指标或几乎任何其他由应用程序提供的指标作为输入。

日志

分布式应用程序中较具挑战性的一个方面是每个活动部分日志的相关性。这是一个区别于传统应用服务器非常显著的领域,因为过去它非常简单,而在新世界中并非如此。不建议单独存储每个容器的日志,因为这样会失去整体视野,难以调试副作用和问题的根本原因。有各种方法处理这个问题,其中大多数都广泛使用ELKElasticsearchLogstashKibana)堆栈或其变体。在这些堆栈中,Elasticsearch 是对象存储,存储所有日志。Logstash 收集来自节点的日志并将其提供给 Elasticsearch。Kibana 是 Elasticsearch 的 Web UI,用于搜索来自各种来源的聚合日志文件。

监控

在分布式应用程序中,监控是确保所有部分持续工作的重要组成部分。与日志记录相比,监控是主动观察,通常与警报配对,而不仅仅是记录事件。Prometheus 是存储生成信息的事实标准。它实质上是一个完整的开源监控系统,包括时间序列数据库。Prometheus 的 Web UI 允许您访问指标查询、警报和可视化,帮助您深入了解系统。

构建和部署管道

CI/CD(持续集成/持续交付)对于企业 Java 应用程序或分布式应用程序并不新鲜。作为良好的软件开发实践,每个生产代码都应遵循严格的自动化发布周期。由于一个应用可能包含大量服务,自动化至少应该达到 100% 的覆盖率。传统上由开源工具Jenkins处理这些工作,现代容器平台已经摒弃了集中式构建系统,转向了分布式 CI/CD 的方法。一个例子是Tekton。其目标是通过构建、测试和部署创建可靠的软件发布。我们在第四章深入探讨了这一点。

韧性和容错性

心理学家将“韧性”定义为在逆境、创伤、悲剧、威胁或重大压力来源面前适应良好的过程。在分布式应用程序中,这意味着在没有人为干预的情况下从失败或负载场景中恢复。Kubernetes 为集群本身提供了韧性选项,但只零星支持应用程序的韧性和容错性。例如,可以通过支持复制卷的 PersistentVolumes 或确保集群中的 Pod 副本数量一致的 ReplicaSets 来促进应用程序级的韧性。在应用程序级别,可以通过 Istio 或像 Cloudstate 这样的各种框架来支持韧性和容错性。你可能会使用诸如重试规则、断路器和池摘除等功能。

注意

Istio 是一个开源的服务网格,可透明地叠加到现有的分布式应用程序上。它还是一个平台,包括集成到任何日志平台、遥测或策略系统的 API。

安全

Kubernetes 本身不包括服务之间的身份验证或授权。有两种实现方式。使用 Istio,每个服务都提供了一个强身份,代表其角色,并支持跨集群和云的互操作性。它保护了服务与服务之间的通信,并提供了一个关键管理来自动化密钥和证书的生成、分发、旋转和吊销。一个更加应用程序中心的替代方案是使用类似 Keycloak 这样的单点登录组件,或依赖于 Eclipse MicroProfile JSON Web Token (JWT)。

追踪

追踪为你提供了一种通过追踪回到起源的方式跟踪系统中的请求路径和事件,跨越个别应用程序部分。今天,你可以在社区中找到不同的方法。与你打算使用的语言、框架或技术无关,Istio 可以实现分布式追踪。还有其他商业和开源项目可用于帮助跨应用程序组件进行分布式追踪。ZipkinJaeger 是两种可能的解决方案。

定义目标平台

需要注意的是,前面提到的九个元素侧重于应用程序开发,并没有涵盖现代容器平台的所有必需品。仅仅看这个狭隘的焦点会忽略重要的领域。容器平台需要为从开发到运维的完整团队提供功能和能力。根据具体需求,没有一种一刀切的解决方案。定义目标平台的综合方法是从核心、客户体验和集成三个主要层面开始,然后在优化的技术栈上构建应用程序景观。听起来像一个可直接使用的检查表,但实际上并非如此。公司在文化、技术和需求方面各不相同,以下列表仅作为建议的起点,没有宣称全面性。我们建议将项目作为评估类别使用,并定义个别功能和非功能需求,根据从零(不可用)到三(完全支持)的满足评分,中间得分为二(可行),作为中等评估。最后,根据产品比较添加加权逻辑以达到完整评估。它可以作为直接产品与自主实施(DIY)比较的核心框架,并且也是平台文档的起点。

定义核心部分

从评估平台的核心部分开始。此类别包括基本功能,如容器编排,存储映射,滚动升级,站点可靠性工程(SRE)要求,对所需部署模型的开箱即用支持,甚至可能包括对虚拟机的进一步支持。这一类别代表了目标平台的技术基础:

  • 现有的核心能力

  • 功能差距评估

  • 混合云支持

  • 安全集成

  • 管理服务支持

  • 可用的运营商/市场(例如,OperatorHubRed Hat Marketplace

  • 可用的支持级别

  • 目标部署模型

  • 核心现代化方法

定义客户体验层

在考虑平台时,有一个部分得到了太少的关注:客户体验层,其中包含对平台客户通道的技术定义。通道可以是 B2X(业务到某些东西)门户或其他特定的前端。一个完整的平台可以托管各种应用程序,还需要包括对个别服务技术组成的清晰定义:

  • 定义客户中心需求

  • 评估现有的 CX 框架与构建之间的差异

  • 微前端(例如,Entando

  • 集成需求

  • 数据差距分析

  • 移动支持

定义集成

在容器化的世界中,集成成为一个新的挑战。从传统的企业景观出发,集成通常是一个集中的解决方案(企业服务总线或类似的),或者是各个应用程序的一部分,使用类似 Apache Camel 的某些常见集成框架。这两种方法都不完全适合于无状态容器导向的平台。在目标平台中,您寻找的是消息组件、数据转换逻辑和服务集成之间的无缝集成。所有相关部分都需要在分布式系统的无状态环境中良好地扩展,并且应该可以轻松地通过新功能扩展组合的应用程序:

  • 现有的集成能力

  • 评估合作伙伴解决方案生态系统

  • 定义集成需求(数据源、服务集成、消息传递、API)

  • 定义标准和框架(例如,骆驼 K

  • 评估无服务器/Knative 集成(例如 Camel K)

定义技术栈

剩余类别关注的是个别技术和框架。可以将其视为定义生产环境中相关技术、服务和方法论的蓝图存储库。在这一类别的需求中被低估的因素是组织中可用的开发技能。对于传统的企业 Java 背景来说,完全转向反应式开发方法和无状态应用设计并不容易。此外,熟悉现有 API 并在新平台上提高生产力所需的时间在选择最合适的技术栈时起着至关重要的作用:

  • 在核心、CX 和外部服务方面进行技术栈评估

  • 微服务框架(例如 Quarkus、Spring Boot)

  • 实施建议(反应式,命令式,消息驱动等)

  • 部署模型(IaaS,PaaS,混合)

  • 定义目标开发平台

  • 开发技能缺口分析

完成此评估后,您将为迁移到容器化应用平台做好充分准备。接下来,您需要制定并计划您的容器化策略。

强制性迁移步骤和工具

遵循的基本假设是您已经有了现有的应用程序景观,不能将所有事情都作为全新项目开始。回顾之前的六个 R,您首先要考虑的应用程序应该属于以下几种 R 中的一种:重新托管(Rehost)、重新平台化(Replatform)和重构(Refactor)(图 3-3)。尽管它们在描述上看起来相似,但这三种方法之间最重要的区别在于业务价值与迁移时间及成本之间的平衡。

业务价值评估

图 3-3. 工作负载迁移模式

开始现代化的具体步骤和起点取决于应用程序。尽管具体步骤可能会有所不同,但首先要确定正确的候选者。因此,我们需要分析现有应用程序,进行目录分类,并将其分组以分配给最终的迁移模式。最后一步是执行各个迁移项目。

创建应用程序组合

创建这样一个应用程序目录或者组合的方法有很多种。你很可能已经有了一种方式来选择与某个业务领域相关的应用程序。如果没有,可以直接跳转到第五章,我们在那里讨论了Konveyor 项目

准备迎接重大事务

现代化中最负盛名的过程是重构现有应用程序。为了覆盖将现有单体系统转换为微服务架构的成熟方法,我们推荐 Sam Newman 的Monolith to Microservices(O’Reilly)。虽然他向你展示了许多不同的方法,并为各种情况创建了详细的流程,但也有更简单的方法,比如卡内基梅隆大学软件工程研究所的 Brent Frye 提出的方法。他推荐了八个简单的步骤来拆分单体系统。他专注于组件和组件组。组件是逻辑数据对象集合及系统对这些对象执行的操作。组件组成了他所称的宏服务。宏服务与微服务类似,但有两个主要区别。首先,宏服务可能与遗留的单体系统或其他宏服务共享数据存储。其次,与微服务不同,宏服务可能提供对多个数据对象的访问。在最后一步,宏服务进一步分解。

根据 Frye 拆分单体的逻辑步骤如下:

  1. 确定逻辑组件。

  2. 平铺并重构组件。

  3. 确定组件依赖关系。

  4. 确定组件组。

  5. 为远程用户界面创建 API。

  6. 迁移组件组到宏服务:

    1. 将组件组移动到单独的项目中。

    2. 进行独立部署。

  7. 将宏服务迁移到微服务。

  8. 重复步骤 6-7 直到完成。

这也是 Chris Richardson 的更一般性建议。正如他在他的O’Reilly SACON 伦敦主题演讲中概述的那样,他寻求一种从最有前途的功能开始的增量方法。

逐步进行,并重复提取步骤,直到单体最终消除或解决初始软件交付问题,如图 3-4 所示。

移动功能,尽可能长。

图 3-4. 逐步将单体架构移至服务的过程中,通过增量方式逐步提取它们

这三种方法在深度、角度和细节上有所不同。虽然理查德森谈论了最有价值的功能,并专注于首先提取它,弗莱创造了一种简单的方法论,可适用于所有情况。最后,纽曼为现代化旅程中的各种情况开发了最详细的手册。这三者对你的个人旅程都将有所帮助。然而,我们确信,理查德森采取的方法是最佳的起点。就像 Thomas Huijskens 对数据科学家说的那样,我们也坚信:“你编写的代码只有在成为生产代码时才有用。”

每一次现代化努力都必须遵循业务需求并支持生产功能。基于这一思路,只有当你确定了正确的候选者时,整个现代化项目才能成功。

摘要

本章介绍了一些迁移策略的基本定义,并展示了目标开发平台的评估路径。我们已经看过技术建议,现在你知道如何评估现有应用程序以进行重新托管、重新平台化和重构。

第四章:基于 Kubernetes 的软件开发平台

在前一章中,我们概述了我们围绕现代化的方法论以及设计和开发现代架构所需的步骤。我们描述了像 Kubernetes 这样的平台的必要性,它可以帮助您满足将应用程序云原生化的需求,以便根据业务需求比例进行扩展。

我们还展示了通常使用容器技术实现的微服务架构,这使得应用程序具有可移植性和一致性。现在让我们详细看看 Kubernetes 如何帮助我们现代化我们的 Java 应用程序,并通过其声明性方法和丰富的 API 集实现这一目标的步骤。

开发人员和 Kubernetes

Kubernetes,在希腊语中翻译为“领航员”或“统治者”,是当前现代架构的事实标准目标环境和最流行的容器编排平台;简单示意图见图 4-1。始于 Google 2015 年在管理其软件堆栈的分布式复杂应用程序方面的经验,今天它是最大的开源社区之一;由云原生计算基金会(CNCF)管理,并受到供应商和个人贡献者的欢迎。

一个运行在节点上的 Kubernetes 集群

图 4-1. 一个运行在节点上的 Kubernetes 集群

作为一个容器编排平台,其主要关注点是确保我们的应用程序正确运行,提供开箱即用的自愈、恢复功能以及强大的 API 来控制这一机制。您现在可能会问:作为开发人员,如果 Kubernetes 如此自足,我为什么要关心它呢?

这是一个很好的问题,也许一个好的答案是类比:您有一辆配备自动驾驶的一级方程式赛车,但如果您想赢得比赛,您需要调整和设置您的车辆以与其他优秀车辆竞争。对于您的应用程序而言也是如此,它可以受益于平台提供的所有功能,使其以最佳方式运行。

Kubernetes 的功能

当您将 Kubernetes 作为目标平台来运行您的应用程序时,您可以依赖于一套 API 和组件生态系统,以使部署更加简单,使开发人员只需关注最重要的部分:编码。Kubernetes 为您提供了一个可靠运行分布式系统的框架

实际操作中,这意味着当涉及到以下情况时,您无需重新实现定制解决方案:

服务发现

Kubernetes 使用内部 DNS 解析来公开您的应用程序;这是自动分配的,也可以用于将流量发送到您应用程序的多个实例。

负载均衡

Kubernetes 负责管理应用程序的负载、平衡流量并相应地分发用户请求。

自愈能力

Kubernetes 可自动发现并替换失败的容器,自带健康检查和自愈机制。

部署和回滚

Kubernetes 确保您的应用程序始终以所需状态一致运行,提供控制以扩展和缩减工作负载。此外,它还提供了滚动部署或回滚到应用程序特定版本的能力。

Kubernetes 不做什么

开发者通常在生产环境中需要处理的许多头痛问题已经被解决并委托给一个平台,其主要目标是确保应用程序正常运行。但这是否提供了您现代化应用程序所需的一切呢?可能不。

正如我们在前一章讨论的,朝着云原生方法的现代化步骤更加紧密地与方法论而非特定技术联系在一起。一旦您将思维转变为构建微服务而不是单片应用,我们就可以开始有远大眼光地思考了。如今,许多应用程序在面向 Kubernetes 的云平台上运行,并且这些应用程序正在运行具有全球覆盖范围的工作负载。以下是一些需要考虑的事项:

  • Kubernetes 不知道如何处理您的应用程序。如果应用程序失败,它可以重新启动,但无法理解失败原因,因此我们需要确保我们完全控制基于微服务的架构,并能够调试每个容器。这在大规模部署的情况下尤为重要。

  • Kubernetes 不提供任何中间件或应用程序级服务。需要通过与 Kubernetes API 交互或依赖于 Kubernetes 之上的某些服务(如服务网格框架)来解决细粒度发现服务。它没有开箱即用的开发者生态系统。

  • Kubernetes 不会构建您的应用程序。您需要负责将您的应用程序编译打包为容器镜像或依赖于 Kubernetes 之上的其他组件。

有了这些想法,让我们开始深入探讨开发者在 Kubernetes 旅程中的步骤,以便为将我们的应用程序带入下一个云原生生产环境迈出第一步。

基础设施即代码

Kubernetes 提供一组 API 来管理我们应用程序的期望状态以及整个平台。Kubernetes 中的每个组件都有一个可以使用的 API 表示。Kubernetes 提供了一种声明式部署模式,允许您自动化执行一组 Pod 的升级和回滚过程。声明式方法是细粒度的,也用于通过自定义资源概念扩展 Kubernetes API。

注意

自定义资源是 Kubernetes API 的扩展。自定义资源代表对特定 Kubernetes 安装的定制化,引入了额外的对象以扩展集群功能。您可以从官方 Kubernetes 文档获取更多信息。

应用程序在 Kubernetes 中管理的一些核心对象包括:

Pod

一组一个或多个容器部署到 Kubernetes 集群中。这是 Kubernetes 管理和编排的实体,因此任何打包为容器的应用程序都需要声明为 Pod。

Service

负责服务发现和负载均衡的资源。为了使任何 Pod 可被发现和使用,它需要映射到一个 Service。

部署

这允许描述应用程序的生命周期,驱动 Pod 的创建,确定使用哪些镜像来构建应用程序,应该有多少个 Pod,以及它们应该如何更新。此外,它还有助于定义健康检查和约束应用程序的资源。

每个这些对象以及集群中的所有其他资源,可以通过 YAML 表示或通过 Kubernetes API 定义和控制。还有其他有用的 API 对象,如与存储相关的(PersistentVolume)或专门用于管理有状态应用程序的对象(StatefulSet)。在本章中,我们将重点关注在 Kubernetes 平台上使您的应用程序启动和运行所需的基本对象。

容器映像

在这段旅程中,您的第一步是将您的微服务容器化,以便它们可以作为 Pod 部署到 Kubernetes 中,这由使用 YAML 文件,调用 API 或使用 Kubernetes Java 客户端控制。

您可以使用 Coolstore 的 Inventory Quarkus 微服务作为示例来创建您的第一个容器映像。容器由一个称为 Dockerfile 或 Containerfile 的清单定义,您将在其中定义您的软件堆栈作为一个层,从操作系统层到应用程序二进制层。这种方法的好处是多方面的:易于跟踪版本,继承现有层,添加层和扩展容器。图层的图示如 图 4-2 所示。

容器映像图层

图 4-2. 容器映像图层

Dockerfile

对于简单的用例来说,编写 Dockerfile 来将我们的应用程序打包为容器非常简单。有一些基本指令称为 Instructions,如在 表 4-1 中列出的那些。

表 4-1. Dockerfile 指令

指令 描述
FROM 用于继承基础镜像。例如,可以是像fedoracentosrhelubuntu这样的 Linux 发行版。
ENV 为容器使用环境变量。这些变量对应用程序可见,并且可以在运行时设置。
RUN 在当前层中执行命令,如安装软件包或执行应用程序。
ADD 将文件从工作站复制到容器层,如 JAR 文件或配置文件。
EXPOSE 如果你的应用程序监听某个端口,可以将其暴露给容器网络,以便 Kubernetes 将其映射到一个 Pod 和一个 Service。
CMD 启动应用程序的命令:容器镜像构建过程的最后一步,其中您在各层中拥有所有依赖项,可以安全运行应用程序。

从您的 Dockerfile 创建容器的过程也在图 4-3 中描述。

构建容器镜像

图 4-3. 构建容器镜像

在第二章中创建的库存 Quarkus Java 微服务的 Dockerfile 示例如下,并且您可以在本书的 GitHub 仓库中找到它:

FROM registry.access.redhat.com/ubi8/openjdk-11 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png) ENV PROFILE=prod ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png) ADD target/*.jar app.jar ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png) EXPOSE 8080 ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png) CMD java -jar app.jar ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/5.png)

1

我们从 OpenJDK 11 层开始构建我们的容器镜像。

2

设置一个环境变量,在应用程序内部可以使用它来区分不同的配置文件或配置项。

3

复制在编译过程中构建的 JAR 工件到容器镜像中。 这假设您已经编译了一个包含所有依赖项的“fat-jar”或“uber-jar”,它们在同一个 JAR 文件中。

4

将端口 8080 映射到容器网络。

5

运行应用程序以调用我们复制到层中的工件。

在本节中,我们定义了一个 Dockerfile,其中包含用于构建容器镜像的最小指令集。 现在让我们看看如何从 Dockerfile 创建容器镜像。

构建容器镜像

现在您需要创建容器镜像。 Docker 是一个流行的开源项目,用于创建容器;您可以下载适用于您操作系统的版本,并开始使用它来构建和运行容器。 Podman 是另一个开源替代方案,也可以生成 Kubernetes 对象。

当您的工作站上安装了 Docker 或 Podman 后,您可以使用以下命令从 Dockerfile 开始构建您的容器:

docker build -f Dockerfile -t docker.io/modernizingjavaappsbook/
  inventory-quarkus:latest

这将通过读取 Dockerfile 中的指令来生成您的容器镜像。 然后,它将标记您的容器镜像为 <repository>/<name>:<tag> 的形式,例如 docker.io/modernizingjavaappsbook/inventory-quarkus:latest。 您将看到类似于此的输出:

STEP 1: FROM registry.access.redhat.com/ubi8/openjdk-11
Getting image source signatures
Copying blob 57562f1b61a7 done
Copying blob a6b97b4963f5 done
Copying blob 13948a011eec done
Copying config 5d89ab50b9 done
Writing manifest to image destination
Storing signatures
STEP 2: ENV PROFILE=prod
STEP 3: ADD target/*.jar app.jar
STEP 4: EXPOSE 8080
STEP 5: CMD java -jar app.jar
STEP 6: COMMIT inventory-quarkus:latest
Getting image source signatures
Copying blob 3aa55ff7bca1 skipped: already exists
Copying blob 00af10937683 skipped: already exists
Copying blob 7f08faf4d929 skipped: already exists
Copying blob 1ab317e3c719 done
Copying config b2ae304e3c done
Writing manifest to image destination
Storing signatures
--> b2ae304e3c5
b2ae304e3c57216e42b11d8be9941dc8411e98df13076598815d7bc376afb7a1

您的容器镜像现在存储在 Docker 或 Podman 的本地存储中,称为Docker 缓存容器缓存,已准备好在本地使用。

注意

您可以使用此命令为库存服务创建一个生产用的 Uber-Jar:./mvnw package -Dquarkus.profile=prod。 您可以让 Docker 或 Podman 编译您的软件,并使用称为多阶段的特定类型容器镜像构建来创建容器。 查看此 Dockerfile 作为示例。

运行容器

运行容器 指的是从容器缓存中拉取容器镜像以运行应用程序。这个过程将由容器运行时(如 Docker 或 Podman)从我们工作站中的其他进程隔离开来,提供了一个便携式的应用程序,所有依赖项都在容器内管理,而不是在我们的工作站上。

要开始测试打包为容器镜像的 Inventory 微服务,您可以运行以下命令:

docker run -ti docker.io/modernizingjavaappsbook/inventory-quarkus:latest

Quarkus 微服务已在容器中启动并运行,监听 8080 端口。Docker 或 Podman 负责将容器网络映射到您的工作站;在http://localhost:8080 打开浏览器,您将看到 Quarkus 欢迎页面(如图 2-4 所示)。

提示

Docker 网络文档 包含了关于如何在运行 Docker 的容器和主机内映射端口和网络的更多信息。

注册表

正如我们在前一节中所描述的,容器镜像存储在本地缓存中。然而,如果您想要将它们在工作站外部提供,您需要以某种便捷的方式发送它们。容器镜像的大小通常为几百兆字节。这就是为什么您需要一个容器镜像注册表。

注册表基本上充当存储容器镜像并通过上传(推送)和下载(拉取)分享它们的地方。一旦镜像位于另一个系统上,其中包含的原始应用程序也可以在该系统上运行。

注册表可以是公共的或私有的。流行的公共注册表包括Docker HubQuay.io。它们作为互联网上的 SaaS 提供,并允许以公开或不需要身份验证的方式提供镜像。私有注册表通常专为特定用户而设,并且不能公开使用。然而,您可以将它们提供给私有环境,例如私有 Kubernetes 集群。

在这个例子中,我们在 DockerHub 上为这本书创建了一个名为 modernizingjavaappsbook 的组织,它映射到这个公共注册表的一个仓库,我们希望将我们的容器镜像推送到这里。

首先,您需要登录到注册表。您需要进行身份验证以能够推送新内容,然后您将使得容器镜像公开可用:

docker login docker.io

成功登录后,您可以开始将 Inventory 容器镜像上传到注册表:

docker push docker.io/modernizingjavaappsbook/inventory-quarkus:latest

此命令将镜像推送到注册表,并且您应该得到类似以下内容的输出作为确认:

Getting image source signatures
Copying blob 7f08faf4d929 done
Copying blob 1ab317e3c719 done
Copying blob 3aa55ff7bca1 skipped: already exists
Copying blob 00af10937683 skipped: already exists
Copying config b2ae304e3c done
Writing manifest to image destination
Storing signatures

已打包为容器镜像的 Quarkus 微服务现已准备好在任何地方部署!

部署到 Kubernetes

通过与 Kubernetes API 交互来部署应用程序,以创建代表应用程序所需状态的对象,这是在 Kubernetes 集群中完成的。正如我们讨论的那样,Pod、Service 和 Deployment 是在 Kubernetes 管理整个应用程序生命周期和连接性所创建的最小对象。

注意

如果您尚未拥有 Kubernetes 集群,可以下载并使用 minikube,这是一个专为本地开发设计的独立 Kubernetes 集群。

Kubernetes 中的每个对象都包含以下值:

apiVersion

用于创建此对象的 Kubernetes API 版本

kind

对象类型(例如 Pod、Service)

metadata

有助于唯一标识对象的信息片段,如名称或 UID

spec

对象的期望状态

在本节中,我们定义了任何 Kubernetes 对象的基本结构。现在,让我们探讨在 Kubernetes 上运行应用程序所需的基本对象。

Pod

Pod 是一个或多个容器的组合,共享存储和网络资源,并具有运行容器的规范。在 图 4-4 中,您可以看到 Kubernetes 集群中两个 Pod 的表示,每个 Pod 都分配了 Kubernetes 分配的示例 IP 地址。

Pod 和容器

图 4-4. Pod 和容器

Kubernetes 不直接与容器一起工作;它依赖 Pod 概念来编排容器。因此,您需要提供与您的容器匹配的 Pod 定义:

apiVersion: v1
kind: Pod
metadata:
  name: inventory-quarkus ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
  labels:
    app: inventory-quarkus ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
spec:
  containers: ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
    - name: inventory-quarkus
      image: docker.io/modernizingjavaappsbook/inventory-quarkus:latest ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png)
      ports:
        - containerPort: 8080 ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/5.png)

1

Pod 对象的名称,在每个命名空间中唯一

2

一组要应用于此对象的键/值对

3

用于此 Pod 中的容器列表

4

容器镜像 URI,在本例中是 Docker Hub 上公开可用的存储库

5

由此容器公开的端口,要映射到 Pod 中

提示

通常,一个 Pod 包含一个容器,因此映射为 1 Pod:1 应用程序。尽管对于某些用例(例如边车),您可以在一个 Pod 中有多个容器,但最佳实践是将 1 Pod 映射到 1 个应用程序,因为这确保了可扩展性和可维护性。

您可以将前面描述的任何 Kubernetes 对象作为 YAML 文件使用 Kubernetes CLI kubectl 创建。运行下面显示的命令,部署您的第一个微服务作为单个 Pod。您可以在本书的 GitHub 代码库 中找到它。

kubectl create -f pod.yaml

要检查它是否在 Kubernetes 上运行:

kubectl get pods

您应该会得到类似以下输出:

NAME               READY  STATUS   RESTARTS  AGE
inventory-quarkus  1/1    Running  0         30s

如果查看 STATUS 列,它显示 Pod 正确运行,并且所有默认健康检查都正确满足。

提示

如果你想进一步了解如何进行更精细的健康检查,请参考官方 Kubernetes 文档中的 活跃检测和准备检测

服务

Kubernetes 服务用于暴露运行在一组 Pod 上的应用程序。这很有用,因为 Pod 从 Kubernetes 网络中获得随机 IP 地址,如果重新启动或移动到 Kubernetes 集群中的另一个节点,该地址可能会更改。服务提供了一种更一致的方式与 Pod 进行通信,充当 DNS 服务器和负载均衡器。

一个服务映射到一个或多个 Pod;它使用内部 DNS 解析到一个来自助记短主机名(例如 inventory-quarkus)的内部 IP,并根据 图 4-5 中显示的方式平衡流量。每个服务从专用 IP 地址范围中获得自己的 IP 地址,这与 Pod 的 IP 地址范围不同。

Kubernetes 服务

图 4-5. Kubernetes 服务
注意

Kubernetes 服务提供的负载均衡方法是第 4 层(TCP/UDP)。唯一可用的两种策略是轮询和源 IP。对于应用层负载均衡(例如 HTTP),还有其他对象如 Ingress,本书未涵盖,但你可以在这里找到它们的文档 here

让我们看一下可能映射我们 Pod 的服务:

apiVersion: v1
kind: Service
metadata:
  name: inventory-quarkus-service ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
spec:
  selector:
    app: inventory-quarkus ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
  ports:
    - protocol: TCP ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
      port: 8080 ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png)
      targetPort: 8080 ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/5.png)

1

服务对象的名称

2

Pod 暴露的标签,用于匹配服务

3

使用的 L4 协议,TCP 或 UDP

4

这个服务使用的端口

5

Pod 使用的端口,并映射到服务中

要创建你的服务,请按照下面显示的命令运行。你也可以在这本书的 GitHub 仓库 中找到它。

kubectl create -f service.yaml

在 Kubernetes 上检查它是否在运行:

kubectl get svc

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

NAME                       TYPE       CLUSTER-IP    EXTERNAL-IP  PORT(S)   AGE
inventory-quarkus-service  ClusterIP  172.30.34.73  <none>       8080/TCP  6s
注意

你刚刚定义了一个服务,映射到一个 Pod。这只能从内部 Kubernetes 网络访问,除非你使用像 Ingress 这样的对象来接受来自集群外部的流量,使其暴露出去。

部署

部署是用于管理应用程序生命周期的 Kubernetes 对象。部署描述了一个期望的状态,Kubernetes 将使用“滚动”或“重新创建”部署策略来实现它。部署的部署生命周期包括进行中、完成和失败状态。当部署正在执行更新任务(例如更新或扩展 Pod)时,部署处于进行中状态。

Kubernetes 部署在基本 Pod 和服务概念之上提供了一组功能,如下列出的内容和 图 4-6 中显示的内容:

  • 部署 ReplicaSet 或 Pod

  • 更新 Pods 和 ReplicaSets

  • 回滚到先前的部署版本

  • 扩展一个部署

  • 暂停或继续一个部署

  • 定义健康检查

  • 定义资源约束

部署管理应用程序的生命周期和更新

图 4-6. 部署管理应用程序的生命周期和更新

使用 Kubernetes 部署管理应用程序包括应用程序应该如何更新的方式。部署的一个主要优势是能够可预测地启动和停止一组 Pods。在 Kubernetes 中部署应用程序有两种策略:

滚动更新

它提供了对应用程序的 Pod 的可控、分阶段的替换,确保始终有一定数量的 Pod 可用。这对于应用程序的业务连续性非常有用,直到部署的所需数量的 Pod 的健康检查(探针)得到满足,流量才会被路由到应用程序的新版本中。

重新创建

它会在创建新的 Pods 之前移除所有现有的 Pods。Kubernetes 首先终止当前版本的所有容器,然后在旧容器消失时同时启动所有新容器。这会为应用程序带来停机时间,但它确保没有多个版本同时运行。

在下面的示例中列出了在 Kubernetes 上驱动 Pods 部署的Deployment对象:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inventory-quarkus-deploy ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
  labels:
    app: inventory-quarkus ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
spec:
  replicas: 1 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
  selector:
    matchLabels:
      app: inventory-quarkus ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png)
  template: ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/5.png)
    metadata:
      labels:
        app: inventory-quarkus
    spec:
      containers:
      - name: inventory-quarkus
        image: docker.io/modernizingjavaappsbook/inventory-quarkus:latest ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/6.png)
        ports:
        - containerPort: 8080
        readinessProbe: ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/7.png)
          httpGet:
            path: /
            port: 8080
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          failureThreshold: 3
        livenessProbe: ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/8.png)
          httpGet:
            path: /
            port: 8080
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          failureThreshold: 3

1

部署对象的名称。

2

此对象的标签。

3

Pod 副本的期望数量。

4

使用标签找到要管理的 Pods 的选择器。

5

要使用的 Pod 模板,包括要继承的标签或要创建的容器。

6

要使用的容器镜像。

7

Kubernetes 使用就绪探针来知道何时容器已准备好开始接受流量,当其所有容器都准备好时,Pod 被视为已准备好。在这里,我们将根路径上的 HTTP 健康检查定义为就绪探针。

8

Kubernetes 使用存活探针来知道何时重新启动容器。在这里,我们将根路径上的 HTTP 健康检查定义为存活探针。

运行以下命令来创建您的 Deployment。您也可以在本书的 GitHub 存储库 中找到它:

kubectl create -f deployment.yaml

运行以下命令来验证已创建 Deployment,并获取其状态:

kubectl get deploy

您应该得到类似于以下输出:

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
inventory-quarkus-deploy   1/1     1            1           10s

查看 READY 列,您的期望状态已正确匹配,请求在 Kubernetes 上运行的 Inventory 微服务的一个副本。您可以交叉检查是否已创建 Pod:

kubectl get pods

您应该会得到类似的输出:

NAME                                        READY   STATUS    RESTARTS   AGE
inventory-quarkus                           1/1     Running   0          1m
inventory-quarkus-deploy-5cb46f5d8d-fskpd   1/1     Running   0          30s

现在使用随机生成的名称创建了一个新的 Pod,从 inventory-quarkus-deploy 部署名称开始。如果应用程序崩溃或我们杀死由部署管理的 Pod,则 Kubernetes 将自动重新创建它。这对没有部署的 Pod 并不适用:

kubectl delete pod inventory-quarkus inventory-quarkus-deploy-5cb46f5d8d-fskpd

您可以看到始终满足期望状态:

kubectl get pods

您应该会得到类似于以下的输出:

NAME                                        READY   STATUS    RESTARTS   AGE
inventory-quarkus-deploy-5cb46f5d8d-llp7n   1/1     Running   0          42s

Kubernetes 和 Java

Kubernetes 具有管理应用程序生命周期的巨大潜力,有许多研究关于开发人员和架构师如何最好地适应其架构,例如模式。Kubernetes 模式是针对基于容器的应用程序和服务的可重用设计模式。

从 Java 开发者的角度来看,第一步是从单块应用程序方法迁移到基于微服务的方法。完成后,下一步是进入 Kubernetes 环境,并最大化该平台提供的优势:API 可扩展性,声明式模型以及 IT 行业正在收敛的标准化流程。

有一些 Java 框架可以帮助开发人员连接到 Kubernetes 并将其应用程序转换为容器。您已经使用 Dockerfile 对 Inventory Quarkus 微服务进行了容器化。现在让我们从 Java 驱动这种容器化过程,使用 Maven 和 Gradle 为 Catalog Spring Boot 微服务生成一个容器镜像。

Jib

Jib 是由 Google 开发的开源框架,用于构建符合开放容器倡议(OCI)镜像格式的容器镜像,无需 Docker 或任何容器运行时。您甚至可以从您的 Java 代码库中创建容器,因为它为此提供了 Maven 和 Gradle 插件。这意味着 Java 开发人员可以在不编写和/或维护任何 Dockerfile 的情况下容器化其应用程序,将这种复杂性委托给 Jib。

我们看到从这种方法中获得的好处如下:

纯 Java

无需 Docker 或 Dockerfile 知识;只需将 Jib 添加为插件,它将为您生成容器镜像。由此产生的镜像通常被称为“distroless”,因为它不继承任何基础镜像。

速度

应用程序被分成多个层,从类中分离依赖项。无需像 Dockerfile 那样重新构建容器镜像;Jib 负责部署更改的层。

可复现性

不会触发不必要的更新,因为相同的内容始终生成相同的镜像。

使用命令行添加插件,以现有 Maven 快速启动使用 Jib 构建容器镜像的最简单方法是:

mvn compile com.google.cloud.tools:jib-maven-plugin:2.8.0:build
  -Dimage=<MY IMAGE>

或者,您可以通过将 Jib 添加为 pom.xml 中的插件来完成:

<project>
  ...
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.8.0</version>
        <configuration>
          <to>
            <image>myimage</image>
          </to>
        </configuration>
      </plugin>
      ...
    </plugins>
  </build>
  ...
</project>

通过这种方式,你也可以管理其他设置,比如身份验证或构建的参数。如果你想要构建 Catalog 服务并直接推送到 Docker Hub,请运行下面的命令:

mvn compile com.google.cloud.tools:jib-maven-plugin:2.8.0:build↳
-Dimage=docker.io/modernizingjavaappsbook/catalog-spring-boot:latest↳
-Djib.to.auth.username=<USERNAME>↳
-Djib.to.auth.password=<PASSWORD>

这里的身份验证是通过命令行选项管理的,但 Jib 能够使用 Docker CLI 管理现有的身份验证,或者从你的 settings.xml 读取凭据。

构建需要一些时间,结果是一个本地构建并直接推送到仓库的 distroless 容器镜像,在这种情况下是 Docker Hub:

[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.redhat.cloudnative:catalog >-------------------
[INFO] Building CoolStore Catalog Service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ catalog ---
[INFO] Copying 4 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.6.1:compile (default-compile) @ catalog ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- jib-maven-plugin:2.8.0:build (default-cli) @ catalog ---
[INFO]
[INFO] Containerizing application to modernizingjavaappsbook/catalog-spring-boot
  ...
[WARNING] Base image 'gcr.io/distroless/java:11' does not use a specific image
  digest↳ - build may not be reproducible
[INFO] Using credentials from <to><auth> for modernizingjavaappsbook/
  catalog-spring-boot
[INFO] Using base image with digest:↳
sha256:65aa73135827584754f1f1949c59c3e49f1fed6c35a918fadba8b4638ebc9c5d
[INFO]
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/
  libs/*, com.redhat.cloudnative.catalog.CatalogApplication]
[INFO]
[INFO] Built and pushed image as modernizingjavaappsbook/catalog-spring-boot
[INFO] Executing tasks:
[INFO] [==============================] 100,0% complete
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  27.817 s
[INFO] Finished at: 2021-03-19T11:48:16+01:00
[INFO] ------------------------------------------------------------------------
注意

由于你不需要任何容器运行时来使用 Jib 构建镜像,因此你的容器镜像不会出现在本地缓存中。你不会在 docker images 命令中看到它,但之后你可以从 Docker Hub 拉取它并存储在你的缓存中。如果你也想从一开始就将其存储在本地,Jib 也可以连接到 Docker 主机并为你执行此操作。

JKube

Eclipse JKube 是由 Eclipse 基金会和红帽支持的社区项目,是另一个帮助 Java 开发人员与 Kubernetes 交互的开源 Java 框架。它支持使用 Docker/Podman、Jib 和 Source-to-Image (S2I) 构建容器镜像。Eclipse JKube 还提供了一组工具,可自动部署到 Kubernetes 并管理应用程序,其中包括调试和日志记录的助手。它来自 Fabric8 Maven 插件,经过重新品牌和增强,以目标 Kubernetes 的项目形式发布。

提示

JKube 支持 Kubernetes 和 OpenShift。OpenShift 在 Kubernetes 之上提供 Source-to-Image ,这是一种从源代码自动编译容器镜像的机制。这样一来,构建就在 Kubernetes 上进行,因此开发人员可以直接在目标平台上测试和部署他们的应用。

与 Jib 一样,JKube 提供了零配置模式,用于快速上手,其中会预先选择有见地的默认值。它提供内联配置,在插件配置中使用 XML 语法。此外,它提供了真实部署描述符的外部配置模板,这些模板会由插件增强。

JKube 提供三种形式:

Kubernetes 插件

它可以在任何 Kubernetes 集群中使用,提供 distroless 或 Dockerfile 驱动的构建。

OpenShift 插件

它可以在任何 Kubernetes 或 OpenShift 集群中使用,提供 distroless、Dockerfile 驱动的构建,或者 Source-to-Image (S2I) 构建。

JKube 工具包

一个与 JKube Core 交互的工具包和命令行工具,它也作为一个 Kubernetes 客户端,并提供了一个扩展 Kubernetes manifests 的 Enricher API。

JKube 提供了比 Jib 更多的功能;事实上,它可以被视为一个超集。你可以进行 distroless Jib 构建,但也可以使用 Dockerfile 并从 Java 部署 Kubernetes manifests。在这种情况下,我们不需要编写 Deployment 或 Service;JKube 将负责构建容器并将其部署到 Kubernetes。

让我们在我们的目录 POM 文件中包含 JKube,并配置它进行 Jib 构建和部署到 Kubernetes。这样做将使插件持久化。你也可以在这本书的 GitHub 存储库中找到源代码。

首先,我们需要将 JKube 添加为插件:

<project>
  ...
  <build>
    <plugins>
      ...
    <plugin>
       <groupId>org.eclipse.jkube</groupId>
       <artifactId>kubernetes-maven-plugin</artifactId>
       <version>1.1.1</version>
    </plugin>
      ...
    </plugins>
  </build>
  ...
</project>

之后,你可以使用属性驱动容器镜像构建。在这种情况下,你可能想要使用 Jib 来构建镜像并将其推送到 Docker Hub。然后,你将部署到 Kubernetes:

...
<properties>
...
    <jkube.build.strategy>jib</jkube.build.strategy>
    <jkube.generator.name>docker.io/modernizingjavaappsbook/catalog-spring-boot:
      ${project.version}</jkube.generator.name>
</properties>
...

让我们构建镜像:

mvn k8s:build

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

JIB>... modernizingjavaappsbook/catalog-spring-boot/1.0-SNAPSHOT/build/
  deployments/catalog-1.0-SNAPSHOT.jar
JIB>    :
JIB>... modernizingjavaappsbook/catalog-spring-boot/1.0-SNAPSHOT/build/Dockerfile
...
JIB> [========================      ] 80,0% complete > building image to tar file
JIB> Building image to tar file...
JIB> [========================      ] 80,0% complete > writing to tar file
JIB> [==============================] 100,0% complete
[INFO] k8s: ... modernizingjavaappsbook/catalog-spring-boot/1.0-SNAPSHOT/tmp/↳
docker-build.tar successfully built
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  36.229 s
[INFO] Finished at: 2021-03-19T13:03:19+01:00
[INFO] ------------------------------------------------------------------------

JKube 使用 Jib 在本地创建了容器镜像,现在已准备好推送到 Docker Hub。你可以通过以下三种方式之一指定凭据:

Docker 登录

你可以登录到你的注册表,这里是 Docker Hub,JKube 将读取 ~/.docker/config.json 文件以获取认证详细信息。

在 POM 中提供凭据

将注册表凭据作为 XML 配置的一部分提供。

在 Maven 设置中提供凭据

你可以在你的 ~/.m2/settings.xml 文件中提供注册表凭据,插件将从中读取。

在这种情况下,你使用第三个选项,并将凭据设置到 Maven 设置中,这样你就可以使用你的凭据复制这个文件。你也可以在这本书的 GitHub 存储库中找到源代码:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0↳
 http://maven.apache.org/xsd/settings-1.0.0.xsd">

  <servers>
    <server>
      <id>https://index.docker.io/v1</id>
      <username>USERNAME</username>
      <password>PASSWORD</password>
    </server>
  </servers>
</settings>

要将其推送到 Docker Hub,只需运行此 Maven 目标:

mvn k8s:push

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

JIB> [=========================] 81,8% complete > scheduling pushing manifests
JIB> [=========================] 81,8% complete > launching manifest pushers
JIB> [=========================] 81,8% complete > pushing manifest for latest
JIB> Pushing manifest for latest...
JIB> [=========================] 90,9% complete > building images to registry
JIB> [=========================] 90,9% complete > launching manifest list pushers
JIB> [=========================] 100,0% complete
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:08 min
[INFO] Finished at: 2021-03-19T13:21:28+01:00

现在是时候在 Kubernetes 上部署目录了。JKube 将连接到你的 Kubernetes 集群,读取你工作站上的 ~/.kube/config 文件:

mvn k8s:resource k8s:apply

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

[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.redhat.cloudnative:catalog >-------------------
[INFO] Building CoolStore Catalog Service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- kubernetes-maven-plugin:1.1.1:resource (default-cli) @ catalog ---
[INFO] k8s: Running generator spring-boot
  ...
[INFO] k8s: Creating a Service from kubernetes.yml namespace default name catalog
[INFO] k8s: Created Service: target/jkube/applyJson/default/service-catalog.json
[INFO] k8s: Creating a Deployment from kubernetes.yml namespace default name
  catalog
[INFO] k8s: Created Deployment: target/jkube/applyJson/default/deployment-
  catalog.json
[INFO] k8s: HINT: Use the command `kubectl get pods -w` to watch your pods start
  up
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  7.464 s
[INFO] Finished at: 2021-03-19T13:38:27+01:00
[INFO] ------------------------------------------------------------------------

应用程序已成功部署到 Kubernetes,使用生成的清单:

kubectl get pods
NAME                       READY   STATUS    RESTARTS   AGE
catalog-64869588f6-fpjj8   1/1     Running   0          2m2s
kubectl get deploy
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
catalog   1/1     1            1           3m54s

为了测试它,让我们看一下 Service:

kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
catalog      ClusterIP   10.99.26.127   <none>        8080/TCP   4m44s
提示

默认情况下,Kubernetes 仅在集群内部公开应用程序,使用 ClusterIP 服务类型。你可以使用 Service 类型 NodePort 或使用 Ingress 在外部公开它。在这个例子中,你将使用 kubectl port-forward 将 Kubernetes 公开的端口映射到我们工作站的端口。

让我们使用 kubectl port-forward 命令尝试我们的应用程序:

 kubectl port-forward deployment/catalog 8080:8080

如果你现在在浏览器中打开 http://localhost:8080/api/catalog,你将看到 Coolstore 的目录 JSON 输出。

摘要

在本章中,我们讨论了 Java 开发人员如何从 Kubernetes 的能力中受益,以使其应用程序现代化并增强其功能,展示了开发人员在 Kubernetes 环境中的内部循环。我们演示了如何创建容器镜像以及如何将其部署到 Kubernetes。我们还介绍了如何通过 Maven 直接从 Java 驱动容器创建和部署,感谢 Jib 和 JKube 的支持。

现代化对于开发人员非常重要,以使应用程序云原生且可移植,为提供高可用性的生产和服务做好准备。在下一章中,我们将更深入地研究现有 Java 应用程序的现代化以及实现它所需的步骤。

第五章:超越简单迁移:与遗留系统合作

遗留不是我为自己做的事。这是我为下一代正在做的事。

维托尔·贝尔福特

许多组织面临着在保持现有业务运营的同时试图创新的挑战。通常期望更快地交付新功能并降低成本,看起来似乎是一项具有挑战性的任务,特别是在审视现有应用程序景观和遗留系统的普及时。

我们经常使用术语“遗留系统”来描述一种旧的方法、技术或应用程序,该方法、技术或应用程序不符合最新的方法或使用过时的技术堆栈。诚然,我们在职业生涯早期创建的许多系统都属于这一类别。我们知道其中大多数仍在使用。其中一些甚至为后来的新方法或标准铺平了道路。我们通常也意味着这些系统需要替换,这最终会导致负面含义的感知。幸运的是,这并不总是真的。遗留也是一个美丽的词汇,用来描述成就和遗产。称某物为“遗留”并不自动使其过时和无用。保留遗留系统的理由有很多,包括:

  • 系统按设计工作,无需更改。

  • 实施的业务流程已不再为人所知或记录,而替换它们是昂贵的。

  • 替换系统的成本高于保持不变的好处。

由 Michael Feathers(O’Reilly)著作的与遗留代码有效工作为程序员提供了技术,以成本效益地处理常见的遗留代码问题,而无需重新编写所有现有代码的巨大费用。

Feathers 说:“对我而言,遗留代码就是没有测试的代码。”如果我们今天阅读术语“遗留”,它主要指的是单块应用程序。在现代企业景观中处理遗留应用程序有多种方法,选择正确的方法是现代化旅程的第一步,也是最关键的部分。

到目前为止,我们只谈论了个别系统。开发人员通常只关心这个特定的系统范围。现代化计划应遵循全面的公司目标,还应考虑公司整体的 IT 战略。云迁移的一种特别激动人心的方法在 Gregor Hohpe 的书云策略:成功云迁移的基于决策的方法中提出。如果你想了解更多关于在个别迁移工作之上构建抽象的信息,这本书是必读之选。

管理遗留

每一次成功的旅程都始于第一步。应用程序迁移旅程的第一步是评估现有应用程序。我们假设您了解公司的整体目标和指令。现在我们可以将它们映射到评估类别中。评估类别的另一个来源是技术要求,例如现有的蓝图或推荐的主要解决方案或框架版本。构建和更新这个评估类别列表应该成为您管理流程的一部分。最终,您可以从这些评估标准中导出迁移标准,并将它们用作现代化旅程的决策基石。

评估迁移应用程序

在评估迁移或现代化工作时,重要的是考虑到激励或影响您的组织的特定挑战。组织可能面临的一些挑战的例子包括:

开发的有限预算

开发团队需要变得更加高效,并且他们的速度必须增加。他们的目标不是使用复杂的规范,而是转向轻量级框架和预构建功能。现代化通常应该作为正在进行的开发或维护项目的一部分来安排。

缺乏内部技能

内部现有技术的团队技能正在减少。这样的例子包括主机编程甚至是早期版本的企业 Java 规范,这些规范已经不再教授或不再是最先进的。更改使用较旧技术的现有系统可能意味着需要为开发项目添加特定技能。

感知风险

按照 1977 年左右广为流传的一句名言,“如果它没有坏,就不要修理”,我们确实看到了许多围绕更改已经建立并运行的软件的感知风险。导致这种情况的原因有很多,从对系统的知识问题到对工厂停产的恐惧。这些风险需要单独解决,并通过迁移计划中的适当行动来缓解。

没有可预测的过程

这本书将帮助您解决这个特定问题。探索未知可能是一个巨大的挑战。对于现代化工作,已经有一个经过验证且可重复的流程,并且所有参与方都尊重并遵循这个流程对于成功至关重要。

真正的工作量估计

估计迁移工作量不应该是魔术。不幸的是,许多公司对现代化企业 Java 应用程序的真实工作量几乎一无所知。遵循可预测和优化的方法将消除这种挑战。

将这些挑战转化为可操作项,供您的评估看起来可以像这样:

  • 预测工作量和成本水平

  • 安排应用程序迁移并处理冲突

  • 识别代码、基础架构、流程或知识层面的所有潜在风险

  • 预测投资回报以制定业务案例

  • 识别和缓解对业务的风险

  • 最小化对现有业务运营的干扰

如果只是查看单个应用程序,可以在电子表格或文档中完成这项工作就足够了。然而,每一个中大型工作都需要一个更好的解决方案。大规模工作需要自动化例行程序和规则来评估安装基础,并将应用程序链接到业务服务,以可靠地规划下一步操作。一个开放源码且直接的方法是来自于Konveyor 项目。它结合了一系列工具,旨在帮助现代化和迁移到 Kubernetes。

Konveyor 子项目 Forklift 提供将虚拟机迁移到 KubeVirt 的能力,且最小化停机时间。子项目 Crane 则专注于在 Kubernetes 集群之间迁移应用程序。套件的一部分还包括 Move2Kube,以帮助加速将 Swarm 和基于 Cloud Foundry 的应用程序重新平台化到 Kubernetes。

特别是对于应用现代化,Konveyor 提供了Tackle 项目。它评估和分析应用程序,以便重构为容器,并提供标准清单。

Tackle 应用程序清单

用户可以通过这种方式维护其应用程序组合,将它们链接到它们支持的业务服务,并定义它们之间的相互依赖关系。应用程序清单使用可扩展的标记模型来添加元数据,这是一种链接迁移类别的好方法,正如前文所讨论的。应用程序清单用于通过 Pathfinder 进行评估选择应用程序。

Tackle Pathfinder

这是一个基于问卷的交互式工具,评估应用程序是否适合现代化,以便在企业 Kubernetes 平台上部署容器。Pathfinder 生成关于应用程序适合 Kubernetes 的报告,包括相关风险,并创建采纳计划。Pathfinder 根据应用程序清单和额外的评估问题进行操作。如果应用程序依赖于直接主机系统连接,这可能会使得此特定应用程序不适合迁移到 Kubernetes,因为它会过载主机部分。一些评估问题的例子包括:

  • 第三方供应商组件在容器中是否受支持?

  • 应用程序是否处于积极开发中?

  • 应用程序是否有任何法律要求(例如 PCI、HIPAA)?

  • 应用程序是否提供指标?

我们强烈建议查看 Pathfinder 来管理完整景观的大规模现代化项目。它将帮助您对今天的范围内的应用程序进行分类和优先级排序,并持续跟踪您的迁移评估以适应未来的变化。

Tackle 控制

控制项是一组实体,它们为应用清单和路径评估添加不同的值。它们包括业务服务、利益相关者、利益相关者群体、工作职能、标签类型和标签。此外,您可以通过实施自己的实体来捕获公司或项目特定的属性。例如,这将过滤您的应用清单,识别所有由某个“工作职能”使用的应用程序,以识别人力资源部门使用的所有应用程序。

解决 DiVA

最后,DiVA是一款以数据为中心的应用程序分析工具。作为项目Windup的继任者,如果您想评估单个应用程序,它是最令人兴奋的项目。它专注于传统的单片应用程序,目前支持 Servlets 和 Spring Boot 应用程序。您可以导入一组应用程序源文件(Java/XML),然后 DiVA 会提供以下内容:

  • 服务入口(导出的 API)清单

  • 数据库清单

  • 事务清单

  • 代码到数据库的依赖关系(调用图)

  • 数据库对数据库的依赖关系

  • 事务对事务的依赖关系

  • 事务重构建议

DiVA 目前正在积极开发中,原始 Windup 项目的整合尚未完成。但是,它仍然为您的现代化工作提供了坚实的基础。此外,它提供了一个绝佳的机会,让您贡献自己的发现,并成为致力于自动化迁移的更大社区的一部分。

应用程序迁移工具包

当我们等待 Windup 完全集成到 DiVA 时,您仍然可以使用应用程序迁移工具包(MTA)对基于 Enterprise Java 的应用程序进行自动迁移评估。

MTA 组装了支持大规模企业 Java 应用现代化和迁移项目的工具,涵盖了许多转换和用例。您可以将您的应用程序二进制文件或存档导入其中,它会自动执行代码分析,包括应用程序组合、应用程序依赖关系、迁移挑战和以故事点形式的迁移工作量估算。最初它是设计用于支持 Java EE 服务器迁移(例如,从 WebSphere 或 WebLogic 到 JBoss EAP)。但是,它具有高度可扩展的规则集机制,允许开发人员创建自己的规则集,甚至是适应其需求的现有规则。今天它还涵盖了 Spring Boot 到 Quarkus 的迁移。

Java 示例规则摘录如下:

//...
JavaClass.references("weblogic.servlet.annotation.WLServlet")
    .at(TypeReferenceLocation.ANNOTATION)
        )
        .perform(
            Classification.as("WebLogic @WLServlet")
               .with(Link.to("Java EE 6 @WebServlet",
                             "https://some.url/index.html"))
               .withEffort(0)
               .and(Hint.withText("Migrate to Java EE 6 @WebServlet.")
               .withEffort(8))
        );
//...

此规则扫描 Java 类以查找@WLServlet注释,并为此结果添加一项工作量(故事点)。您可以在Windup 文档中了解更多关于规则以及如何开发它们的信息。

此外,它还可以作为构建过程的一部分支持非迁移用例(通过 Maven 插件命令行界面),定期验证代码是否符合组织标准或确保应用程序的可移植性。

MTA 可以检测到的一些模式包括以下内容:

  • 专有库

  • 专有配置

  • 服务定位器

  • Web 服务

  • EJB 描述符

  • 废弃的 Java 代码

  • 事务管理器

  • 注入框架

  • 线程池机制

  • 定时器服务

  • WAR/EAR 描述符

  • 静态 IP 地址

MTA 和 DiVA 是两个强大的工具,帮助我们识别总体技术债务,导致迁移需求和风险的分类。然而,它们不能帮助我们识别应首先迁移或现代化的功能。为此,我们需要深入研究应用程序设计和功能。

评估迁移功能

传统的单体应用有各种形状、形式和大小。当有人使用术语“单体”时,他们通常指的是部署工件本身。在企业 Java 中,这传统上是企业归档(EAR)或 Web 归档(WAR)。你也可以将它们视为单一进程应用程序。它们可以按照像 OSGi(开放服务网关倡议)这样的模块化建议进行设计,或者按照更技术化的方法,如三层设计,没有显著的业务模块。您的现代化努力的整体方向在很大程度上取决于您正在处理的单体类型。作为一个经验法则,现有应用程序越模块化,现代化的难度就越小。在理想的情况下,模块直接转化为服务边界。但这种情况很少发生。

如果单体看起来像一个巨大的盒子,我们必须对其应用逻辑模型。我们意识到在这个盒子内部有组织良好的业务和技术组件,例如订单管理、PDF 渲染、客户通知等。虽然代码可能并未围绕这些概念组织,但从业务领域模型的角度来看,它们存在于代码库中。这些业务领域边界,在领域驱动设计(DDD)中通常称为“有界上下文”,成为新的服务。

注意

如果您有兴趣了解更多信息,许多人认为 Eric Evans 的书 《领域驱动设计:软件核心复杂性应对之道》(O’Reilly) 是 DDD 的事实标准介绍。

一旦确定了模块和功能,您可以开始考虑现代化的顺序。但首先,请确保查看每个模块的成本与收益权衡,并从最佳候选模块开始。图 5-1 为一个包含六个模块的样本应用程序提供了一个非常高层次的概述。在本例中,我们假设讨论的是一个虚构的在线商店。例如,对于强烈相互依赖的模块,例如订单和客户,单独提取它们将会很复杂。如果还考虑到可扩展性的必要性以及从单体架构中移除它们的好处,其效益可能并不高。这两个模块位于图表的左下角。在相反的一侧,我们可能会找到目录服务。它列出可用的产品,并且是一个只读服务,几乎没有任何相互依赖。在网站需求高峰期间,这是最受欢迎的模块,并且从图 5-1 中显示的绿色模块位于图表的右上方,受益匪浅。对于应用程序中的所有模块进行类似的练习,以评估成本与收益。

成本与收益

图 5-1. 成本与收益

现在您已经达到验证先前战略应用评估的最后检查点。估计的现代化收益是否超过了估计的现代化成本?不幸的是,没有普适的建议,因为这严重依赖于应用本身、业务需求以及公司的总体目标和挑战。记录您的决策和结论,因为现在是决定您现代化努力未来方向的时候。还记得第三章中的 6R 吗?保留(无需更改)、退出(停用)、重新购买(新版本)、重新托管(放入容器)、重新平台化(进行轻微调整)或重构(构建新内容)。

我们现在已经评估了迁移的应用,并且评估了迁移的功能。我们知道应用的哪些方面我们准备现代化。您已经得出结论,不希望构建新应用,而是轻度现代化现有的遗留系统。在下一节中,我们将深入探讨一些迁移方法。

迁移方法

上述工具和评估将帮助您确定最适合的应用程序和服务。现在是深入了解单个应用程序的策略和挑战的时候了。

保护遗留系统(重新平台化)

只有一个或两个模块需要业务刷新或增加功能时,最简单的方法是专注于这两个模块,并尽可能保留现有应用程序,在现代基础设施上运行。除了相关模块的更改,还包括对运行时、库或目标基础设施的重新评估,同时尽量少触及代码。

这可以通过简单地将应用程序和数据库容器化,并修改良构单体的相关模块,或完全提取某些功能并重新集成到部分分布式系统中来实现,正如图 5-2 所示。

Putting the pieces back together

图 5-2. 把碎片重新组合起来

言之易,行之难。有许多非功能性需求需要从应用服务器平台重新分配到外部架构。我们将在下一章节重点讨论更关键的部分。在本章中,我们希望专注于应用程序和数据库本身的迁移。

服务对应用的服务

一旦您提取了特定功能,最紧迫的问题是如何将剩余的单体与新提取的服务集成起来。假设您切换到容器运行时,应使用 API 网关基于 URL 进行负载均衡和流量切换。我们将在第六章中更详细地讨论此问题。

另一种方法是使用 HTTP 代理。在尝试从单体应用程序中提取部分之前,必须确保代理已经在生产环境中运行。确保它不会破坏现有的单体,并且花一些时间定期将新服务推送到生产环境中,即使尚未被最终用户使用。如果一切看起来良好,逐渐通过重定向流量进行切换。

对于更简单的服务与单体交互,甚至可以考虑实现简单的 JAX-RS 直接通信。然而,此方法仅适用于处理极少数量服务的情况。确保从单体的视角处理提取出的服务作为一个集成系统。

所有这三种方法(API、网关、HTTP 代理和 JAX-RS 接口)都是迈向首个成功微服务的途径。它们都实施了窒息模式(参见第三章),并帮助将单体应用程序重构为独立系统作为第一步。

拦截是一条潜在危险的路径:如果您开始构建一个自定义协议转换层,该层由多个服务共享,则可能会向共享代理添加过多的智能。这种设计方法使得独立微服务变得困难,并且变成了一个具有过多智能的路由层的面向服务的体系结构。一个更好的替代方案是所谓的边车模式,它基本上描述了 Pod 中的另一个附加容器。与将自定义代理逻辑放在共享层中不同,它成为新服务的一部分。作为 Kubernetes 边车,它成为运行时绑定,并可以为传统客户端和新客户端提供服务。

注意

边车只是在与应用程序容器相同的 Pod 上运行的容器。它与应用程序容器共享相同的卷和网络,并且可以通过这种方式“帮助”或增强应用程序行为。典型的例子是日志记录,或更一般的代理功能。

数据库到数据库

一旦我们确定了功能边界和集成方法,我们就需要决定如何处理数据库分离问题。虽然单体应用通常依赖于单个大型数据库,但每个提取的服务应该操作自己的数据。再次解决这个难题的正确方式取决于现有数据布局和事务。

相对容易的第一步是将服务所需的表分离为只读视图和写入表,并调整单体应用程序的流程,以便同时使用读写操作的接口。这些接口可以更容易地在后续步骤中抽象为服务访问。此选项仅需要更改单体应用程序,并且对现有代码库的影响应该最小。我们可以将表移到一个单独的数据库中,并在下一步中调整相关查询。

所有这些都仅在旧的单体应用中作为准备工作而发生。将现有代码演变为更模块化的结构可能存在风险。特别是,随着数据模型复杂性的增加,风险也会增加。在最后一步中,我们可以将提取的表分离到一个新数据库,并调整单体应用程序以使用新创建的服务与业务对象进行交互。这在纸上和纸笔上相对容易,并且如果数据访问需要在表之间执行许多连接,则很快就会达到实用性的尽头。简单的候选对象是主数据对象,例如“用户”。更复杂的可能是组合对象,如“订单”。对于应用程序代码的模块化所说的事情,对于数据库而言更为真实。设计和模块化程度越好,将功能和数据提取到单独服务中就越容易。会有一些情况下,你找不到一个很好的解决方案来从数据模型中提取对象。或者您可能会看到不同的方法不再提供合适的性能。这是重新审视您选择的现代化路径的时候了。

在继续“快乐路径”的过程中,你现在有两个独立的数据库和两个非常不平等的“服务”组成的系统。现在是时候考虑你的服务之间的数据同步策略了。大多数数据库实现了某些功能来在数据变更时执行行为。简单情况下,支持在更改的行上触发功能以将副本添加到其他表中,甚至在更改时调用更高级别的功能(例如 WebServices)。这通常是专有功能,并且在很大程度上依赖于正在使用的数据库。如果你有全公司使用某些功能的指令或者对进一步改变原始遗留数据库有足够的信心,这可能是一个选择。

如果这不可能,那么有基于批处理作业的同步选项。更改的时间戳、版本或状态列表明需要复制。你可以依赖这种非常成熟和众所周知的数据同步版本,它存在于许多遗留系统中。主要缺点是无论实现方式如何,你最终都会在目标系统的数据准确性上出现差异。较高的复制间隔也可能导致交易成本增加或源系统负载增加。这种方法只适用于不频繁更新,理想情况下在两者之间有非时间敏感的过程步骤。它不适用于实时或接近实时的更新要求。

解决数据同步挑战的现代方法依赖于日志读取器。作为第三方库,它们通过扫描数据库事务日志文件来识别变更。这些日志文件用于备份和恢复操作,并提供一种可靠的捕获所有变更(包括删除)的方法。这个概念也称为变更数据捕获。这里最显著的项目之一是Debezium。使用日志读取器是在数据库之间同步更改的最不具破坏性的选项,因为它们不需要修改源数据库,也不会对源系统产生查询负载。变更数据事件通过 Outbox 模式为其他系统生成通知。

构建新东西(重构)

如果因为任何原因,你发现自己面临一个岔路口,决定重新实现和重构你的完整系统为一个新的分布式架构,那么你很可能在思考如何协同工作以保持工作量小而可预测。考虑到完整微服务堆栈的复杂性,这并不是一件容易的任务。这种方法的一个关键因素是团队知识。在企业 Java 应用服务器上经过多年的开发后,团队应该能从持续的 API 和标准知识中获益。有多种方式可以在 JVM 上实现服务,所有这些方式都有助于团队重用我们从企业 Java/Jakarta EE 标准中已知的最关键的功能。让我们讨论一些在 JVM 上实现服务的方法。

注意

Jakarta EE 是一套规范,使 Java 开发人员能够开发 Java 企业应用程序。这些规范由知名行业领袖开发,为技术开发者和消费者带来信心。它是 Java 企业版的开源版本。

MicroProfile

MicroProfile 成立于 2016 年,并迅速加入 Eclipse 基金会。MicroProfile 的主要目的是以厂商中立的方式创建 Java 企业框架,用于实现可移植的微服务。MicroProfile 包括厂商中立的编程模型、配置以及追踪、容错、健康和指标等服务。MicroProfile API 组件建立在 Jakarta EE 模型之上,使 Java 开发人员更自然地过渡到微服务。您可以重复使用在职业生涯中已积累的 Jakarta EE 知识。MicroProfile 定义了 12 个规范,如 图 5-3 所示,并且组件模型在底层使用 Jakarta EE 标准的一个子集。与完整的 Jakarta EE 规范相比,更重的规范如企业 JavaBeans (EJB) 和 Jakarta XML Web Services 是缺失的。

MicroProfile 技术概览

图 5-3. MicroProfile 技术概览

有多种 MicroProfile 规范的实现可供选择:Open Liberty、Thorntail、Paraya Server、TomEE、SmallRye 等。由于 MicroProfile 基于与 Jakarta EE Web Profile 接近的原则和组件,因此现有应用程序的迁移相对容易。

Quarkus

Quarkus 是所谓的微服务框架中的一个相对较新的成员。它是一个为 JVM 和原生编译优化的 Kubernetes 本地 Java 框架。它专为容器和受限运行时环境进行了优化。其主要目的是成为无服务器、云和 Kubernetes 环境的理想运行时。

它与流行的 Java 标准、框架和库兼容,如 Eclipse MicroProfile、Spring Boot、Apache Kafka、RESTEasy (JAX-RS)、Hibernate ORM (JPA)、Infinispan、Camel 等等。

依赖注入解决方案基于来自 Jakarta EE 的 CDI(上下文和依赖注入),使其与已建立的组件模型兼容。一个有趣的部分是扩展框架,它帮助扩展功能以配置、引导和集成公司特定的库到您的应用程序中。它运行在 JVM 上,并支持 GraalVM(多语言通用虚拟机)。

组件模型到服务

开发人员中最常见的问题之一是如何将企业 Java 应用程序的现有组件模型迁移到微服务中。通常,此问题指的是企业 Java Beans 或 CDI Beans,特别是容器管理的持久性 Beans(在 EJB3 之前),需要基于 Java 持久性 API(JPA)重新创建。我们强烈建议检查底层的数据/对象映射是否仍然准确并适合新的要求,并完全重新创建它。这不是现代化过程中最耗时和费用的部分。通常,更具挑战性的部分是编码的业务需求。虽然 CDI Beans 在技术上是 MicroProfile 兼容实现的一部分,但是否适合进行简单的代码迁移的决定取决于新的业务需求。必须注意查找现有代码事务边界,以确保不需要涉及下游资源。一个普遍的建议是尽可能少地重用源代码。这里的原因主要是两种技术之间系统设计方法的不同。虽然我们在部分模块化的单体架构中得以脱身,但在微服务中却不可能。特别关注定义有界上下文将为最终解决方案的性能和设计付出回报。

Spring 应用到服务

我们可以采用类似的方法来处理采用不同编程框架(如 Spring)的应用程序。虽然从技术上讲,更新和复制现有实现是容易的,但缺点依然存在。特别是,对于基于 Spring 的开发团队,可能会发现在不同框架(如 Quarkus)中使用兼容性 API 有所帮助。

Quarkus 的 Spring API 兼容性包括 Spring DI、Spring Web 和 Spring Data JPA。额外的 Spring API 部分支持,如 Spring Security、Spring Cache、Spring Scheduled 和 Spring Cloud Config。Quarkus 中的 Spring API 兼容性并不意味着要成为一个完整的 Spring 平台来重新托管现有的 Spring 应用程序。目的是提供足够的 Spring API 兼容性,以便使用 Quarkus 开发新的应用程序。

挑战

通过评估、规划和关怀,您可以分解和现代化现有的单体应用程序。这在大多数情况下都不是自动化过程,而需要大量的工作。需要注意一些特定的挑战。

避免双写

一旦您建立了几个微服务,您很快会意识到它们最具挑战性的部分是数据。作为它们业务逻辑的一部分,微服务经常需要更新它们的本地数据存储。同时,它们还需要通知其他服务发生的更改。这个挑战在单体应用程序的世界中并不那么明显,也不在操作单一数据模型的遗留分布式事务中。这种情况并不容易解决。随着分布式应用程序的转变,您很可能会失去一致性。这在 CAP 定理中有描述。

注意

CAP 定理,或“三选二”概念,说明我们只能同时提供以下三种保证中的两种:一致性、可用性和分区容错性。

现代分布式应用使用事件总线,比如 Apache Kafka,用于在服务之间传输数据。将你的事务从单体架构中的两阶段提交(2PC)迁移到分布式世界将显著改变你的应用行为并对故障做出反应。你需要一种控制长时间运行和分布式事务的方法。

长时间运行事务

Saga 模式提供了对双写和长时间运行事务的解决方案。虽然 Outbox 模式解决了更简单的服务间通信问题,但不足以解决更复杂的长时间运行的分布式业务事务用例。后者需要在多个服务之间执行多个操作,并具有一致的全部或无操作语义。每个多步骤业务流程都可能是一个例子,分布在多个服务之间。购物车应用需要生成确认电子邮件并在库存中打印运输标签。所有操作必须一起执行,否则就不执行。在传统世界或单片体系结构中,您可能不会意识到这个问题,因为模块之间的协调是在单个进程和单个事务上下文中完成的。分布式世界需要不同的方法。

Saga 模式通过将一个总体业务事务拆分为由参与服务执行的多个本地数据库事务,为这个问题提供了解决方案。一般来说,有两种实现分布式 saga 的方式:

  • 协同作业:在这种方法中,参与的一个服务在执行完其本地事务后向下一个服务发送消息。

  • 编排:在这种方法中,一个中心协调服务协调和调用参与的服务。参与服务之间的通信可以是同步的,通过 HTTP 或 gRPC,也可以是异步的,通过像 Apache Kafka 这样的消息传递。

移除旧代码太快

一旦我们提取一个服务,我们就希望摆脱旧的源代码、维护成本和重复的开发。但是要小心。你可以把旧代码视为参考,并针对两个代码库测试行为变化。偶尔检查新创建的服务的时间也可能会有帮助。建议在一个定义的时间段内并行运行它们并比较结果。之后,你可以删除旧的实现。这样做早了些会更好。

集成方面

传统的单体应用与复杂的集成逻辑有着紧密的关系。这主要是通过会话外观或与数据同步逻辑集成来进行代理。每个整合到全局业务流程中的单一系统都需要被视为一个独立的服务。你可以在从现有数据模型中提取数据部分并逐步进行此操作时应用相同的原则。另一种方法是从一开始就将你的集成逻辑视为一个服务。这种方法最初是为支持微服务而设计的,Camel K是一种方法。它基于著名的 Apache Camel 集成库,并将集成路由包装成容器或更好的单独服务。通过这种方式,你可以将单体应用的完整集成逻辑与你的服务分离开来。

总结

现代企业 Java 系统就像家族的世代传承:它们在旧系统的基础上不断发展。使用成熟的模式、标准化工具和开源资源将有助于你创建能够随着需求增长和变化的持久系统。从根本上讲,你的迁移方法直接关系到你今天和明天试图解决的问题。你试图解决什么问题,你的当前架构无法扩展到?也许微服务是答案,或者也许有其他答案。你必须理解你试图实现的目标,因为如果没有这种理解,将很难确定如何迁移现有系统。理解你的最终目标将改变你如何分解一个系统以及如何优先处理这项工作。

第六章:构建 Kubernetes 本地应用程序

在上一章中,我们概述了如何从传统的 Java 企业模式迁移到以容器为中心的方法。在本章中,我们将详细介绍迁移到基于微服务的架构所需的组件以及 Kubernetes 如何连接各个点。

我们还在前几章中学习了基于微服务的方法如何帮助我们使软件可靠、可移植,并且在需求增加时已做好扩展准备。现代架构从一开始就计划了可扩展性,这既提供了机会也带来了挑战。企业 Java 开发人员知道他们的代码通常是业务逻辑的一部分,依赖于框架使其稳健并符合公认的软件设计模式。如今,同一应用程序在公共云上可能服务数百万请求,甚至在地理上分布广泛。为了做到这一点,必须对其进行架构设计,使之符合这一模型,解耦功能,避免单点故障,并将负载分布到架构的多个部分,以避免服务中断。

在可扩展性和复杂性之间找到合适的平衡点

在理想的世界中,所有应用程序都是无状态的,它们可以独立扩展。它们不会崩溃,并且网络连接始终可靠。现实看起来不同。从单体应用迁移到基于微服务的架构使得云原生部署成为可能,我们已经介绍了这带来的一些好处。然而,这也带来了一些挑战:管理应用程序的多个入口点,保持多个微服务之间的关系一致性,以及管理分布式数据库和模式。

在 图 6-1 中,您可以看到从单体到基于微服务的应用程序的过渡带来了一种新的方法,有多个互连或甚至多个数据库可供使用。

从单体到微服务架构

图 6-1. 从单体架构到微服务架构

类似于 CAP 定理,同时提供可扩展性而不增加系统复杂性是非常困难的。这就是为什么 Kubernetes 如此有帮助,因为它是无处不在的,在任何云中运行,并且您可以将大部分复杂性委托给这个平台。这使您可以“只是”专注于应用程序开发。另一方面,在云原生世界中,我们也需要为有状态应用找到解决方案,我们将看到 Kubernetes 在这方面也提供了帮助。

现代架构的功能需求

Kubernetes 在定义分布式应用方面非常有帮助,如图 3-1 所示。任何 Java 开发者都应该对《设计模式》有所了解,这是软件工程的杰作,作者在其中定义了最常用的软件设计模式。Kubernetes 扩展了这些模式集合,创建了一组新的云原生特定要求,使应用程序能够抵御各种负载,如图 6-2 所示。接下来让我们深入探讨其中的一些内容。

现代架构的功能要求

图 6-2. 现代架构的功能要求

API 驱动

微服务的口号是“API 先行”。如果你再看一下图 6-1,你会注意到将单体应用程序拆分为一堆微服务的第一个挑战:如何让这些软件片段彼此通信?在单体应用中,你依赖于模块、包的应用范围。而微服务通常通过 REST 调用彼此通信,每个微服务可以是服务的生产者或消费者。连接微服务的方式不仅限于这种方法;使用队列、消息或缓存也是常见用例。但总体而言,每个微服务通过一组 API 暴露其原语或函数,并且这也可以通过我们在第二章讨论的 Coolstore 示例中的 API 网关来进行中介。

Kubernetes 本身是 API 驱动的软件。平台的所有核心组件,如 Pod、Service 和 Deployment,都通过 REST API 进行操作。所有组件之间的操作和与外部用户命令的通信都是通过 REST API 调用处理的,API 服务器处理这些调用。当我们通过kubectl或 JKube 与 Kubernetes 交互时,实际上是通过 HTTPS 调用 API 发送和接收 JSON 内容。这种 API 生态系统是 API 驱动架构的理想环境,比如使用微服务的架构。既然我们知道了我们的微服务如何通信,那么我们如何发现新的服务呢?

发现

让微服务之间通过 REST 调用相互通信非常直接。此外,让组件和函数调用变得轻松也是很好的,例如导入模块或包到我们的应用程序中。在现代架构中,需要调用和连接的微服务数量可能非常多,因此仅存储网络端点(如 IP 地址或主机名)可能不足够。正如我们在第四章讨论的那样,Kubernetes 通过Service对象简化了网络,允许两个或多个 Pods 在平台的内部网络中互相通信。Kubernetes 还通过其 API 提供了从应用程序内部列出集群中对象的能力。Java 开发人员可以使用像 JKube 这样的框架来实现 Java Kubernetes 客户端。

列出 Kubernetes 服务和 Pods,这些 Pods 表示一些微服务,是实时组件清单的第一步,这进一步有助于在运行时维护和扩展应用程序。此外,Kubernetes 还支持与外部工具或框架的集成,例如 Service Mesh,提供服务发现协议以检测服务的上线。

提示

服务网格 是基于微服务架构越来越受欢迎的选择。它提供一个控制面板,还与 Kubernetes 交互以管理服务发现、双向认证、A/B 测试、路由和断路器模式。更多详细信息可在线找到

安全性与授权

现代应用程序开发人员需要考虑的另一个挑战是整个堆栈的安全性。从应用程序到平台,最佳实践同样适用于现代架构,当需要连接多个服务、查询多个数据库和服务多个端点时,复杂性和所需的工作量可能显著增加。再次提到,Kubernetes 可以提供帮助。

Kubernetes 为整个生态系统提供安全性。基于角色的访问控制(RBAC)和精细化的权限规则是可能的。此外,Pods 由一个名为Service Account的特殊用户运行,该用户可以访问 Kubernetes API Server,通常仅限于用户的命名空间。除此之外,Kubernetes 还提供了一个专用的 API 来管理密码和证书,称为Secrets。Secret 是一个在运行时由平台挂载到 Pod 中的卷,其值与 Kubernetes 的数据库 etcd 一起存储,以及集群状态和配置。

注意

etcd 是 Kubernetes 使用的分布式键值数据库,用于存储集群状态。数据库内容也可以加密,只有集群管理员可以访问其内容。

正如我们所讨论的,微服务之间的通信通常是通过 HTTPS REST 调用完成的,其证书是通过 Secrets 管理的。容器和 Kubernetes 为确保应用程序安全提供了一个良好的起点,从中 Java 开发人员可以开始实施应用程序安全最佳实践。

监控

在现代架构中,测量资源消耗是至关重要的,在具有按使用量付费的消费模型的云环境中更是如此。估计在压力下您的应用程序将需要多少计算资源并不容易,而且过度估计可能会增加成本。Kubernetes 通过其 API 和工具生态系统使监视在操作系统级别到应用程序级别成为可能。

从平台和应用程序收集指标的热门云原生工具是Prometheus,这是一个时间序列数据库,可以使用一种称为 PromQL 的查询语言从 Kubernetes 集群和应用程序导出指标。

指标也被用来帮助 Kubernetes 根据应用程序的监控负载决定何时扩展或缩减您的应用程序。您可以使用自定义指标(如 JVM 线程或队列大小)驱动此规模,并使监视成为赋予服务力量的积极工具。Prometheus 还提供了警报和报警功能,对于在应用程序需要更快地做出反应时调度自动操作非常有用。

Java 开发人员还可以使用Micrometer与 Kubernetes 内部的 Prometheus 和指标进行交互,Micrometer 是一个提供指标注册机制和核心指标类型的开源工具。它适用于任何基于 JVM 的工作负载,并且是与 Prometheus 和 Kubernetes 进行交互的 Spring Boot 和 Quarkus 项目的热门选择。“就像 SLF4J,但用于指标。”

追踪

可观察性是现代架构的另一个关键方面,而测量 REST API 调用之间的延迟是管理基于微服务的应用程序的重要方面。确保通信始终清晰且延迟最小是至关重要的。当微服务的数量增加时,架构的某些部分出现小延迟可能会导致对用户来说可接受的服务中断。在这些情况下,Kubernetes 对于调试移动到分布式架构时出现的大多数操作问题非常有帮助。

Jaeger 是一个流行的开源工具,连接到 Kubernetes 以提供可观察性。它使用分布式跟踪来跟踪请求通过不同微服务的路径。它通过仪表板提供了调用流程的视觉表示,通常也与服务网格集成。Jaeger 对于开发人员监视分布式事务、优化性能和延迟以及执行根本原因分析非常有帮助。

日志

正如我们讨论的,微服务应用程序中的单个调用,例如 Coolstore 示例,可能会调用与其他服务进行交互的不同服务。监视和观察应用程序非常重要,同时还要将相关信息存储在日志中。现代应用程序的日志记录方法与单体应用程序不同。在单体应用中,我们通常依赖于存储在磁盘上不同路径的多个日志文件,通常由应用服务器管理,而分布式应用程序则会 流式传输 日志。由于您的应用程序可能会快速扩展并移动到不同的节点甚至云中,因此访问单个实例以检索日志是没有意义的;因此,需要分布式日志系统。

Kubernetes 使日志记录变得简单。默认情况下,它提供访问 Pod 日志的能力,通过读取应用程序的标准流,例如 STDOUT(标准输出)和 STDERR(标准错误)。因此,应用程序不应该将日志写入特定路径,而是发送到标准流。

仍然可以将日志存储在特定路径中,这些路径也可以在 Kubernetes 中保持持久化,但这被认为是一种反模式。

Kubernetes 还与分布式日志系统如 Elasticsearch 交互,这是一个基于 Apache Lucene 的开源文档导向型 NoSQL 数据库,用于存储日志和事件。Elasticsearch 通常带有转发器,如 Fluentd,以及用于可视化日志的仪表板,例如 Kibana。这些共同组成了 EFK 堆栈(Elasticsearch、Fluentd、Kibana)。借助这种日志堆栈,开发人员可以通过 Kibana 仪表板的聚合视图查看多个微服务的日志,并能够在称为 Kibana 查询语言(KQL)的查询语言中进行查询。

云原生应用的默认标准是分布式日志记录,Kubernetes 与许多服务如 EFK 连接并进行交互,为整个集群提供集中日志记录。

CI/CD

连续集成(CI)是软件开发周期中的一个阶段,在这个阶段,来自不同团队成员或不同功能的代码被集成。通常涉及代码合并(集成)、应用程序构建(容器化)和在临时环境中执行基本测试。

连续交付(CD)是指自动化软件交付各个方面的一组实践。其中之一是交付管道,这是一个自动化过程,用于定义代码或配置更改必须经历的步骤,以达到更高的环境并最终到达生产环境。

它们经常被称为 CI/CD,并且是 DevOps 方法论的关键技术推动者之一。

现代服务需要快速响应变化或问题。正如我们可以监控、跟踪和记录分布式架构一样,我们还应该能够更快地更新基于微服务的应用程序。流水线是在生产环境中部署应用程序的最佳方式,遵循如 图 6-3 所示的阶段。

持续集成和持续交付

图 6-3. 持续集成和持续交付

流水线 是一系列步骤,顺序或并行执行,用于在所有预生产环境中构建和测试应用程序,最终发布到生产环境。它可以完全自动化,也可以与外部工具交互以进行手动步骤批准(例如 Service Now、JIRA 等)。Kubernetes 与许多外部 CI/CD 工具(如 Jenkins)进行交互,并提供称为 Tekton 的本地 CI/CD 子系统。

Tekton 是一个 Kubernetes 本地的 CI/CD 系统,这意味着它扩展了 Kubernetes API 并提供其自定义资源,您可以使用它们来创建流水线。它依赖于捆绑在 Tekton 中的任务目录来组成您的流水线,例如 Maven 或 Java 源到镜像任务。

提示

Tekton 可以通过 OperatorHub.io 上的 Operator 在 Kubernetes 中安装。

要创建 Kubernetes 本机流水线,Tekton 提供了以下自定义资源:

Task

一个可重复使用、松耦合的步骤集,执行特定功能(例如构建容器镜像)。任务以 Kubernetes Pods 形式执行,而任务中的步骤映射到容器。

Pipeline

用于构建和/或部署您的应用所需的任务列表。

TaskRun

任务实例的执行和结果。

PipelineRun

流水线实例的执行和结果,其中包括多个任务运行。

在我们创建的库存 Quarkus 微服务的 Tekton 流水线示例见 第二章,您也可以在这本书的 GitHub 存储库中找到它:book’s GitHub repository

apiVersion: tekton.dev/v1alpha1
kind: Pipeline
metadata:
  name: inventory-pipeline
spec:
  resources:
  - name: app-git
    type: git
  - name: app-image
    type: image
  tasks:
  - name: build
    taskRef:
      name: s2i-java-11
    params:
      - name: TLSVERIFY
        value: "false"
    resources:
      inputs:
      - name: source
        resource: app-git
      outputs:
      - name: image
        resource: app-image
  - name: deploy
    taskRef:
      name: kubectl
    runAfter:
      - build
    params:
    - name: ARGS
      value:
        - rollout
        - latest
        - inventory-pipeline

Java 开发者也可能会发现,使用 Fabric8 Tekton Java 客户端直接从代码创建和控制 Tekton 流水线和任务非常方便。这种选择能够从单一点实现完全控制,无需维护外部清单文件(如 YAML 文件)。

首先,在 POM 文件中导入 Maven 依赖:

    <dependencies>
        <dependency>
            <groupId>io.fabric8</groupId>
            <artifactId>tekton-client</artifactId>
            <version>${tekton-client.version}</version>
        </dependency>
    </dependencies>
    <properties>
        <tekton-client.version>4.12.0</tekton-client.version>
    </properties>

然后,您可以使用 Tekton Java API 创建任务或流水线:

package io.fabric8.tekton.api.examples;

import io.fabric8.tekton.client.*;
import io.fabric8.tekton.resource.v1alpha1.PipelineResource;
import io.fabric8.tekton.resource.v1alpha1.PipelineResourceBuilder;

public class PipelineResourceCreate {

  public static void main(String[] args) {
    try ( TektonClient client = ClientFactory.newClient(args)) {
      String namespace = "coolstore";
      PipelineResource resource = new PipelineResourceBuilder()
        .withNewMetadata()
        .withName("client-repo")
        .endMetadata()
        .withNewSpec()
        .withType("git")
        .addNewParam()
        .withName("revision")
        .withValue("v4.2.2")
        .endParam()
        .addNewParam()
        .withName("url")
        .withValue("https://github.com/modernizing-java-applications-book/
 inventory-quarkus.git")
        .endParam()
        .endSpec()
        .build();

      System.out.println("Created:" + client.v1alpha1().pipelineResources().
        inNamespace(namespace).create(resource).getMetadata().getName());
    }
  }
}

微服务调试

尽管分布式架构有很多好处,但也存在一些挑战。即使最终在 Kubernetes 集群中运行代码,您仍然通常在本地开发,拥有 IDE、编译器等。有几种方法可以解释开发周期。如图 6-4 所示,有两个循环。靠近开发者的一个称为内部循环,是您进行编码、测试和迭代调试的地方。另一个循环,远离开发者,称为外部循环,是您的代码运行在您必须构建、推送和部署的容器镜像内,这需要更长时间。

内部循环和外部循环

图 6-4. 内部循环和外部循环

外部循环是 CI/CD 世界的一部分,内部循环是您在启动 Tekton Pipeline 将应用程序部署到 Kubernetes 之前开始编码和测试软件的地方。调试微服务也是内部循环的一部分。

开发人员可以采用不同的方法来开始调试微服务:

  • 使用Docker Compose在本地部署所有服务

  • 使用minikube,或任何本地 Kubernetes 集群,并在那里部署所有服务

  • 模拟您与之交互的所有服务

注意

Docker Compose 帮助创建在任何 Docker 主机上运行的容器,而不需要 Kubernetes。它用于管理本地开发中的多个容器,但不映射到任何目标 Kubernetes 集群;因此,保持本地开发设置与目标设置分开可能会很困难。

这些都是有效的方法,但有时服务是外部的,并且只能从远程 Kubernetes 集群访问,或者模拟该部分代码很难或不可能。

Microcks是一个开源的 Kubernetes 本地调试工具,用于 API 模拟和测试。它有助于将 API 合同、集合或 SoapUI 项目转换为实时模拟。在 Kubernetes 上快速开发而无需依赖项时,它可以是一种方便的方式。

让我们看看一些用于 Kubernetes 中微服务调试的额外选项。

端口转发

Kubernetes 提供了远程 Shell 进入 Pod 的功能,用于快速调试任务,例如文件系统检查。此外,您可以设置端口转发,将您的本地机器与运行在 Pod 中的应用程序连接到连接到 Kubernetes 集群。此选项在您希望连接运行在 Pod 中的数据库、附加您不希望暴露给公众的管理 Web 界面,或者像本例中一样附加 JVM 运行的应用程序服务器的调试器时非常有用。

通过将应用服务器的调试端口进行端口转发,您可以从 IDE 附加调试器,并实时步进 Pod 中运行的代码。请记住,如果您的应用程序不处于调试模式,则需要首先打开调试端口。

要开始调试,您需要公开调试端口。例如,要调试 Inventory 微服务,您需要访问调试端口 5005:

kubectl port-forward service/inventory-quarkus 5005:5005

现在当我们在localhost:5005上连接时,它将被转发到运行在 Pod 中的 Inventory 实例。

注意

端口转发仅在允许kubectl port-forward命令运行期间处于活动状态。由于我们在前台运行它,因此可以通过按下 Ctrl+C(或 Mac 上的 Cmd+C)停止端口转发。

要调试源代码,您可以使用您选择的 IDE,或者可以按照以下步骤从控制台调试:

jdb -sourcepath $(pwd)/src/main/java -attach localhost:5005

Quarkus 远程开发模式

Quarkus 提供了一个远程开发模式,允许您在诸如 Kubernetes 之类的容器环境中运行 Quarkus,并立即对本地文件所做的更改生效。

要启用它,请在您的application.properties中添加此部分:

quarkus.package.type=mutable-jar ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png) quarkus.live-reload.password=changeit ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png) quarkus.live-reload.url=http://my.cluster.host.com:8080 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)

1

在开发模式下,可变应用程序用于在 Quarkus Java 应用程序中实时应用和测试更改,而不需要重新加载构件。

2

用于保护远程端和本地端通信的密码。

3

应用程序运行在开发模式下的 URL。

您可以使用 Maven 生成可变 JAR。如果您已连接到 Kubernetes 注册表,可以让 Quarkus 将应用程序部署到 Kubernetes,如下所示:

eval $(minikube docker-env)
注意

您可以使用以下命令添加 Quarkus Kubernetes 扩展:./mvnw quarkus:add-extension -Dextensions="kubernetes"

将应用程序部署到 Kubernetes:

mvn clean install -DskipTests -Dquarkus.kubernetes.deploy=true

最后,您可以连接到应用的远程开发模式:

mvn quarkus:remote-dev

这允许您使用 Quarkus 将您的本地机器上的现场编码功能连接到远程容器环境,例如 Kubernetes。

Telepresence

Telepresence 是一个开源工具,用于帮助调试 Kubernetes 中的微服务。它在本地运行单个服务,同时将该服务连接到远程 Kubernetes 集群。Telepresence 是编程语言无关的,为您提供了一个方便的方式将您的本地环境连接到在 Kubernetes 上运行的任何工作负载以进行调试。

使用 Telepresence 在 Kubernetes 上调试应用非常容易。首先,下载并安装Telepresence CLI,并与您的集群保持活动会话,因为 Telepresence 将读取~/.kube/config文件以连接到 Kubernetes。

注意

Telepresence 将修改 Kubernetes 中的网络,以便服务可以从您的笔记本电脑访问,反之亦然。

一旦在您的工作站上安装并配置了 CLI,您可以运行此命令来初始化并测试与 Telepresence 连接到您的集群的连接:

$ telepresence connect

您应该会得到类似以下的输出:

Connected to context minikube (https://192.168.39.69:8443)

我们可以开始调试您在前面步骤中部署的库存微服务。在此之前,让我们列出可用于调试的应用程序:

$ telepresence list

您应该会得到类似以下的输出:

inventory-quarkus-deploy: ready to intercept (traffic-agent not yet installed)

要开始调试此微服务,您需要让 Telepresence 拦截由服务表示的内部 Kubernetes 流量。

库存的 Kubernetes 服务使用端口 8080,如以下命令所示:

$ kubectl get svc inventory-quarkus-service

您应该会得到类似以下的输出:

NAME                      TYPE      CLUSTER-IP     EXTERNAL-IP PORT(S)  AGE
inventory-quarkus-service ClusterIP 172.30.117.178 <none>      8080/TCP 84m

现在您可以开始拦截连接到部署的流量,使用服务使用的端口。您还可以指定 Telepresence 应将环境变量写入的文件路径,该环境变量当前正在您的服务中运行:

$ telepresence intercept inventory-quarkus-deploy --port 8080:http --env-file
  inventory.env

您应该会得到类似以下的输出:

Using Deployment inventory-quarkus-deploy
intercepted
    Intercept name         : inventory-quarkus-deploy
    State                  : ACTIVE
    Workload kind          : Deployment
    Destination            : 127.0.0.1:8080
    Service Port Identifier: http
    Volume Mount Point     : /tmp/telfs-844792531
    Intercepting           : all TCP connections

查看刚刚创建的环境文件 inventory.env 的内容:

INVENTORY_QUARKUS_SERVICE_PORT=tcp://172.30.117.178:8080
INVENTORY_QUARKUS_SERVICE_PORT_8080_TCP=tcp://172.30.117.178:8080
INVENTORY_QUARKUS_SERVICE_PORT_8080_TCP_ADDR=172.30.117.178
INVENTORY_QUARKUS_SERVICE_PORT_8080_TCP_PORT=8080
INVENTORY_QUARKUS_SERVICE_PORT_8080_TCP_PROTO=tcp
INVENTORY_QUARKUS_SERVICE_SERVICE_HOST=172.30.117.178
INVENTORY_QUARKUS_SERVICE_SERVICE_PORT=8080
INVENTORY_QUARKUS_SERVICE_SERVICE_PORT_HTTP=8080
KO_DATA_PATH=/var/run/ko
KUBERNETES_PORT=tcp://172.30.0.1:443
KUBERNETES_PORT_443_TCP=tcp://172.30.0.1:443
KUBERNETES_PORT_443_TCP_ADDR=172.30.0.1
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_HOST=172.30.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
LOG_LEVEL=debug
NSS_SDB_USE_CACHE=no
TELEPRESENCE_CONTAINER=inventory-quarkus
TELEPRESENCE_MOUNTS=/var/run/secrets/kubernetes.io
TELEPRESENCE_ROOT=/tmp/telfs-777636888
TERM=xterm

现在您可以像连接到内部 Kubernetes 网络一样访问库存微服务,并使用刚刚检索到的环境变量进行操作:

curl http://inventory-quarkus-service.coolstore:8080/api/inventory/329299

您应该会得到类似以下的输出:

{"id":"329299","quantity":35}

摘要

在本章中,我们讨论了 Kubernetes 模式如何帮助 Java 开发人员现代化其应用程序,提供了一个平台,提供许多组件来扩展应用程序的功能。 Kubernetes 的 API 驱动,可插拔的架构轻松地使外部工具能够提供软件和实用程序的生态系统,这些工具提醒并扩展了 Java 企业应用服务器模型。诸如日志记录、监控或调试应用程序等基本任务以适合云原生模型的方式提供,其中应用程序普及并可以同时在多个地方和多个云中运行。

在下一章中,我们将讨论一种新的服务和交付企业应用程序的概念,节省资源且云就绪:无服务器方式。

第七章:明日的解决方案:无服务器

第二次工业革命与第一次不同,没有给我们带来滚动的钢铁和熔化的金属的令人压抑的景象,而是“位”在信息流中传输,沿着电路以电子脉冲的形式。铁器仍然存在,但它们服从于无重量位的命令。

伊塔洛·卡尔维诺

无服务器计算模型在公共云提供的强大推动下,最近也在开源社区内因许多项目的支持而受到关注。但究竟什么是无服务器?无服务器有哪些用例?它如何用于现代 Java 应用?

什么是无服务器?

无服务器的最佳定义来自CNCF 无服务器白皮书

无服务器计算是指构建和运行不需要服务器管理的应用程序的概念。它描述了一种更细粒度的部署模型,其中应用程序作为一个或多个函数捆绑上传到平台,然后根据实际需求在响应时执行、扩展和计费。

运行一个“不需要服务器管理”的应用程序是该定义中最相关的部分。在前几章中,我们探讨了 Kubernetes 如何帮助现代架构的功能要求,以及容器镜像如何代表一种方便的方式将应用程序打包并部署到任何云平台。在无服务器中仍然有服务器,但它们被抽象出来,与应用开发无关。虽然第三方负责维护和管理这些服务器的复杂性,开发人员只需将他们的代码打包到容器中进行部署。

我们讨论的部署模型在 Kubernetes 和无服务器模型之间的主要区别是所谓的零扩展方法。通过这种方法,应用程序在被调用时自动启动,并在不使用时处于空闲状态。这种执行模型也称为事件驱动,是无服务器的核心基础。我们稍后在本章讨论事件驱动的无服务器架构。

通常,一系列事件可以触发应用程序的启动,这将产生结果,正如您在图 7-1 中所见。这可以是单个操作,也可以是一系列操作,其中一个应用程序的输出是下一个应用程序的输入。事件可以是任何形式,例如 HTTP 请求、Kafka 消息或数据库事务。应用程序可以按需自动扩展到多个副本,以处理流量负载,当没有活动时则会缩减规模。

无服务器执行模型

图 7-1。无服务器执行模型

架构演进

无服务器模型并不适用于所有使用案例。一般来说,任何异步、并发、易于并行化为独立工作单元的应用程序都非常适合这种模型。如果您查看 图 7-2 中的图表,您可以看到基于微服务的架构演变始于使用面向服务的体系结构(SOA)模型的单片应用程序方法,现在正在演变为一个新的函数模型。

架构演变

图 7-2. 架构演变

这些函数代表了完成特定范围或任务的最小计算单元。例如:

  • 处理 Web 钩子

  • 数据转换(图像、视频)

  • PDF 生成

  • 移动设备的单页应用程序

  • 聊天机器人

使用这种方法,您可以专注于方便性,因为它通常作为尽力而为提供。容忍失败,并优先短期操作。这就是为什么无服务器模型不适合实时应用程序、长时间运行的任务或可靠性和/或原子性关键的使用案例。开发者需要负责验证所有涉及的无服务器函数是否成功处理了输入和输出。这为整体架构的某些部分提供了极大的灵活性和高可扩展性。

使用案例:数据、人工智能和机器学习

无服务器模型有助于避免项目容量规划中的常见问题,因为它减少了过度配置和不足配置,从而降低了闲置资源的 IT 成本。通过无服务器,所有消耗的资源都是根据实际使用量定制的,因为应用程序仅在被调用时启动,无需预先分配或测量和更新硬件资源。

当您需要实时分析大量数据时,这一点非常重要,这也是为什么无服务器模型在数据科学家和机器学习专家中引起了极大关注,因为能够处理分析数据的函数灵活且占用资源极少。另一方面,无服务器与现有机器学习框架的所有设计原则并不完全匹配。需要一定的容忍度,特别是对于可能需要更长时间(例如模型训练)的过程。

如果您查看图 7-3,您将看到一个基于无服务器驱动的机器学习分类和预测架构的示例。该过程从触发器开始,以从经过训练的模型中获取一组对象的推断。这启动了一系列并行运行的异步函数,用于基于其特征预测对象的类别并返回分类作为输出。我们期望的容忍度是这些函数中的一个可能会失败或无法及时完成任务。但这些都是独立的工作负载。重要的是要有可以并行运行的工作负载,无需特定顺序,因此单个功能的任何失败不会影响整个系统。此外,无服务器架构的自动缩放组件将确保任何高数据负载将比传统方法更快地按需处理。

使用无服务器进行机器学习

图 7-3. 使用无服务器进行机器学习

应用案例:边缘计算和 IoT

边缘和 IoT 设备无处不在。从语音助手到家庭自动化,如今我们家里几乎每件物品都可以连接到互联网,与某个控制应用程序进行通信。作为开发人员,您可能负责后端逻辑或设备应用程序逻辑的任何一部分。

Java 开发人员的一个示例情景来自于用于 IoT 的 Quarkus项目,该项目旨在使用 Quarkus 和容器在设备和服务器端后端上收集传感器的污染数据,后者正在运行一个无服务器应用程序,以提供对大量传感器数据的按需高可扩展性处理,可能以突发方式传入。

该项目还提供了如何在 Red Hat OpenShift 上实现 IoT 架构的很好参考,如图 7-4 所示。

用于 IoT 项目的 Quarkus 架构

图 7-4. 用于 IoT 项目的 Quarkus 架构

无服务器用于使用 MQTT 协议进行数据摄入的 Quarkus 微服务的扩展,使用 Kafka 流作为架构和数据收集器。这使得架构既完整又可靠,同时由于没有资源的分配直到需要时才会成本效益。

Knative:Kubernetes 的无服务器

无服务器可以被认为是函数即服务(FaaS)的引擎,这是开发人员更方便地打包和部署应用程序的一种方式。通常情况下,特别是在公共云中,无服务器和 FaaS 是匹配的,因为将应用程序打包到容器中也是自动化的。然而,零级扩展的应用程序不一定是一个函数。正如我们讨论的,无服务器不仅仅是公共云的专利。例如,任何人也可以通过一个名为Knative的开源项目在任何 Kubernetes 集群上采用这种模型。

注意

我们稍后将在本章中更详细地讨论 FaaS。

Knative 在 Kubernetes 上实现了无服务器,支持面向事件驱动的规模缩减至零的应用程序。它为常见应用用例提供了更高级别的抽象。

提示

Knative 可通过来自 OperatorHub.io 的运算符轻松安装到 Kubernetes 上。

Knative 有两个主要组件:

Knative 服务

处理规模缩减至零,创建所有 Kubernetes 资源所需的操作(例如 Pod、Deployment、Service、Ingress)

Knative 事件

处理集群内外事件(例如 Kafka 消息、外部服务)的订阅、传递和管理组件

使用 Knative 在 Kubernetes 上轻松实现无服务器应用程序。以下是您在第 2 章中创建的 Inventory Quarkus 微服务的 Knative 服务示例。

您还可以在本书籍的 GitHub 存储库中找到示例:

apiVersion: serving.knative.dev/v1
kind: Service ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
metadata:
 name: inventory-svc
spec:
 template:
   spec:
     containers:
       - image: docker.io/modernizingjavaappsbook/inventory-quarkus:latest ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
         ports:
          - containerPort: 8080

1

这是 Knative 服务的定义,它是 Kubernetes 上表示无服务器工作负载的自定义资源。

2

它使用我们为 Deployment 对象使用的相同容器映像。使用 Knative 服务时,将为您自动创建一个 Deployment 和一个 Service。

要为 Inventory 微服务创建一个无服务器版本,您可以使用以下命令创建一个 Knative 服务对象:

kubectl create -f kubernetes/ksvc.yaml
提示

Knative 还提供了一个方便的 CLI,称为 kn,用于创建 Knative 服务和管理所有 Knative 无服务器组件。您可以在官方文档中找到更多信息。

立即,您可以使用以下命令验证是否已创建新的 Knative 服务:

kubectl get ksvc

您应该获得类似以下的输出:

NAME           URL                                                  ↳
LATESTCREATED        LATESTREADY          READY  REASON
inventory-svc  http://inventory-svc.coolstore.192.168.39.69.nip.io↳
  inventory-svc-00001  inventory-svc-00001  True

正如您所见,所有 Kubernetes 清单(如 Pod、Deployment 和 Service)都已从 Knative 服务自动创建。在这种情况下,无需维护它们,因为您可以依赖一个控制部署和网络的单个对象:

kubectl get deploy,pod,svc
NAME                                            READY  UP-TO-DATE  AVAILABLE  AGE
deployment.apps/inventory-svc-00001-deployment  1/1    1           1          58m

NAME                                            READY  STATUS   RESTARTS  AGE
pod/inventory-svc-00001-deployment-58...8-8lh9b 2/2    Running  0         13s

NAME                                 TYPE          CLUSTER-IP     EXTERNAL-IP
service/inventory-svc                ExternalName  <none>         ↳
kourier-internal.kourier-system.svc.cluster.local  80/TCP         58m
service/inventory-svc-00001          ClusterIP     10.109.47.140  <none>
service/inventory-svc-00001-private  ClusterIP     10.101.59.10   <none>

在幕后,对 Knative 服务的流量通过 Knative 网络路由到集群中。调用 Inventory 微服务也将触发 Pod 创建,如果应用程序处于空闲状态:

curl http://inventory-svc.coolstore.192.168.39.69.nip.io/api/inventory/329299

您应该获得类似以下的输出:

{"id":"329299","quantity":35}

在一段时间没有新请求后,将应用规模缩减至零模型,并将 Pod 数量缩减至零:

kubectl get deploy
NAME                            READY  UP-TO-DATE  AVAILABLE  AGE
inventory-svc-00001-deployment  0/0    0           0          75m

面向事件驱动的无服务器架构

事件无处不在。正如我们在上一节中讨论的那样,一个 HTTP 请求可以触发一个应用程序的启动,在不使用时可能处于空闲状态,这与图 7-1 中表示的无服务器执行模型一致。但是,有很多事件存在,比如 Kafka 消息、数据库流或来自 Kubernetes 的任何事件,一个应用程序可能希望订阅这些事件。

在这种情况下的一种流行模式是发布-订阅消息传递模式,其中许多发送方可以向服务器上的一个实体发送消息,通常称为主题,接收者可以订阅该主题以获取消息。根据无服务器模型,你的应用程序可以注册并连接以处理传入事件。Kubernetes 的一个例子是Knative Eventing组件,它实现了CloudEvents,这是一种描述多种协议和格式的事件数据 (如 Kafka、AMQP 和 MQTT) 的通用方式的规范。

使用 Knative Eventing,事件生产者和事件消费者是独立的。Knative 服务通过经纪人触发事件源,正如你可以在图 7-5 中看到的那样。事件框架的目标是解耦一切。发送方不直接调用订阅者,甚至不知道有多少个订阅者。而是经纪人和触发器处理通信。

Knative 事件架构

图 7-5. Knative Eventing 架构

与依赖于入站请求级联通过所有微服务不同,你可以使用任意 HTTP 事件作为唤醒 Inventory 服务的示例事件。

首先,我们需要创建一个 Knative Broker。下面列出了我们在 第二章 中创建的 Inventory Quarkus 微服务的 Knative Broker 示例,你也可以在这本书的 GitHub 仓库中找到它:

apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
 name: default
 namespace: coolstore

创建 Broker:

 kubectl create -f kubernetes/kbroker.yaml

你应该会得到类似以下的输出:

NAME     URL   AGE  READY  REASON
default  http://broker-ingress.knative-eventing...coolstore/default ↳
 11s  True
注意

对于此部分,我们正在使用内部 Kubernetes 网络,因此我们使用的任何端点都是 Kubernetes Service,格式为完全限定域名 (FQDN),只能在集群内部访问。

现在让我们创建一个触发器来唤醒 Inventory 微服务。它可以是符合 CloudEvents 规范的任何事件。在这种情况下,你可以使用另一个 Pod 的 HTTP 请求。

在 第二章 中创建的 Inventory Quarkus 微服务的 Knative 触发器示例如下;你可以在这本书的 GitHub 仓库中找到它:

apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
  name: inventory-trigger
spec:
  broker: default ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
  filter:
    attributes:
      type: web-wakeup ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
  subscriber:
    ref:
     apiVersion: serving.knative.dev/v1 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
     kind: Service
     name: inventory

1](#co_tomorrow_s_solutions__serverless_CO2-1)

Broker 的名称。

2](#co_tomorrow_s_solutions__serverless_CO2-2)

属性类型。这可用于过滤要唤醒的事件。

3](#co_tomorrow_s_solutions__serverless_CO2-3)

要连接到的 Knative 服务的名称,并在事件字段中唤醒它。

让我们创建 Knative 触发器如下:

 kubectl create -f kubernetes/ktrigger.yaml

你应该会得到类似以下的输出:

NAME               BROKER   SUBSCRIBER_URI  AGE  READY  REASON
inventory-trigger  default  http://inventory.coolstore...local/  10s  True

现在你可以模拟一个可以唤醒你的微服务的外部事件。在这种情况下,它是一个简单的 HTTP 调用,但也可以是像 Debezium 的数据库流或 Kafka 消息这样的东西。

提示

Debezium.io是一个开源数据捕获平台,可以从流行的数据库(如 PostgreSQL、MySQL 等)进行流式处理。请查看在线文档以了解更多信息。

运行此命令以下载包含curl命令的最小容器映像,直接在 Kubernetes 上作为 Pod 运行,并发送 HTTP POST到 Knative Broker 以触发微服务启动:

kubectl run curl --image=radial/busyboxplus:curl -ti --↳
curl -v "http://broker-ingress.knative-eventing.svc.cluster.local/
  coolstore/default"
  -X POST \
  -H "Ce-Id: wakeup" \
  -H "Ce-Specversion: 1.0" \
  -H "Ce-Type: web-wakeup" \ ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
  -H "Ce-Source: web-coolstore" \ ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
  -H "Content-Type: application/json"
  -d ""

1

我们在 Knative Broker 中定义的属性。

2

事件的名称。

应该获得类似以下的输出:

> POST /coolstore/default HTTP/1.1
> User-Agent: curl/7.35.0
> Host: broker-ingress.knative-eventing.svc.cluster.local
> Accept: */*
> Ce-Id: wakeup
> Ce-Specversion: 1.0
> Ce-Type: web-wakeup
> Ce-Source: web-coolstore
> Content-Type: application/json
>
< HTTP/1.1 202 Accepted
< Date: Wed, 16 Jun 2021 11:03:31 GMT
< Content-Length: 0
<

您现在应该看到已启动库存 Pod:

NAME                                             READY  STATUS   RESTARTS  AGE
curl                                             1/1    Running  1         30m
inventory-svc-00001-deployment-58485ffb58-kpgdt  1/2    Running  0         7s

用于 Java 应用程序的函数即服务

我们先前讨论过,Knative Serving 如何帮助减少维护多个 Kubernetes 对象的复杂性,以及如何通过按需缩放来优化资源使用。但还有另一层抽象帮助根据无服务器模型自动构建和部署应用程序:我们早先介绍过的 FaaS 模型。

FaaS 是一种事件驱动的计算执行模型,开发人员编写的应用程序会自动部署在由平台完全管理的容器中,然后根据按需缩放模型在需要时执行。作为开发人员,您无需编写像 Kubernetes 清单这样的东西。您只需简单地编写应用程序逻辑,让平台将应用程序打包为容器,并将其作为按需缩放的无服务器应用程序部署到集群中。

诸如 AWS Lambda、Azure Functions 或 Google Cloud Run 等流行的公共云无服务器解决方案提供了便捷的 SDK,以开始开发用最流行的编程语言编写的函数,并在 FaaS 模型中打包和部署。还有一些开源解决方案可用,例如Apache OpenWhiskFn 项目,它们使用 Docker 实现了 FaaS。在接下来的章节中,我们将专注于 Knative 和 Kubernetes,因为我们已经在本书中讨论了 Kubernetes 如何为简化 Java 企业应用程序向云原生范式迁移提供完整的生态系统。

用于 Java 应用程序的函数部署

函数是根据无服务器模型交付的代码片段,可以在不同的基础设施配置之间移植。函数的生命周期在图 7-6 中描述,从编写代码、规范和元数据开始。构建阶段随后自动发生,并且部署将函数发布到平台上。这使得当需要进行新变更时,触发新的构建和新的发布的更新机制成为可能。

函数部署模型

图 7-6. 函数部署模型

Boson Function CLI(func)

Boson Function CLI 是一个开源 CLI 和框架,连接到 Knative 以提供 Kubernetes 的 FaaS 能力。使用此工具,您可以避免手动编写 Kubernetes 清单和构建容器映像:

$ func
...
Available Commands:
 build      Build a function project as a container image
 completion Generate completion scripts for bash, fish, and zsh
 create     Create a function project
 delete     Undeploy a function
 deploy     Deploy a function
 describe   Show details of a function
 emit       Emit a CloudEvent to a function endpoint
 help Help about any command
 list       List functions
 run        Run the function locally
 version    Show the version
 ...
提示

您可以从官方网站下载最新的func CLI,并将其配置到您的系统中。

函数可以部署到任何已配置以支持无服务器工作负载的 Kubernetes 集群,例如 Knative。

目前,func CLI 支持以下编程语言和框架:

  • Golang

  • Node.js

  • Python

  • Quarkus

  • Rust

让我们在您之前创建的 coolstore 命名空间内创建一个 Quarkus 函数。您还可以在此书籍的 GitHub 存储库中找到此函数。

要创建新的 Quarkus 函数,请运行以下命令,并指定 -l 选项来选择语言:

$ func create -l quarkus quarkus-faas

您应该获得类似的输出:

Project path: /home/bluesman/git/quarkus-faas
Function name: quarkus-faas
Runtime: quarkus
Trigger: http

这创建了一个 Quarkus 的 Maven 项目骨架,其中包含所有需要的依赖项的 POM 文件:

$ tree
.
├── func.yaml ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
├── mvnw
├── mvnw.cmd
├── pom.xml ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
├── README.md
└── src
   ├── main
   │   ├── java
   │   │   └── functions
   │   │       ├── Function.java ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
   │   │       ├── Input.java
   │   │       └── Output.java
   │   └── resources
   │       └── application.properties
   └── test
       └── java
           └── functions
               ├── FunctionTest.java
               └── NativeFunctionIT.java
8 directories, 11 files

1

这是包含函数项目配置信息的文件。

2

这是 Quarkus 项目的 POM 文件。

3

包含注解和代码以运行函数的 Java 类。

让我们为func.yaml函数配置文件添加一些内容,以在 Kubernetes 上将您的函数转换为可运行的容器映像:

name: quarkus-faas ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
namespace: "coolstore" ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
runtime: quarkus ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
image: "docker.io/modernizingjavaappsbook/quarkus-faas:latest" ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/4.png)
imageDigest: ""
trigger: http ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/5.png)
builder: quay.io/boson/faas-quarkus-jvm-builder ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/6.png)
builderMap:
 default: quay.io/boson/faas-quarkus-jvm-builder
 jvm: quay.io/boson/faas-quarkus-jvm-builder
 native: quay.io/boson/faas-quarkus-native-builder
env: {} ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/7.png)
annotations: {} ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/8.png)

1

函数的名称。

2

将部署函数的 Kubernetes 命名空间。

3

在创建时声明函数的语言运行时。

4

这是函数构建后的映像名称。

5

触发函数的调用事件。例如,在这种情况下为http用于纯 HTTP 请求,或event用于云事件触发的函数。

6

指定构建函数时要使用的 Buildpack 构建器映像。

7

在运行时可用于函数的任何环境变量的引用。

8

用于标记项目中的功能的注释。

提示

func 会使用 Buildpack 将函数构建并转换为容器映像,Buildpack 是一个流行的开源项目,用于将源代码构建为可运行的应用程序容器映像。

让我们来审查一下 POM 文件:

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0↳
 https://maven.apache.org/xsd/maven-4.0.0.xsd" ↳
xmlns="http://maven.apache.org/POM/4.0.0"↳
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <modelVersion>4.0.0</modelVersion>
 <groupId>org.acme</groupId>
 <artifactId>function</artifactId>
 <version>1.0.0-SNAPSHOT</version>
 <properties>
   <compiler-plugin.version>3.8.1</compiler-plugin.version>
   <maven.compiler.parameters>true</maven.compiler.parameters>
   <maven.compiler.source>1.8</maven.compiler.source>
   <maven.compiler.target>1.8</maven.compiler.target>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
   <quarkus-plugin.version>1.13.0.Final</quarkus-plugin.version>
   <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.
    artifact-id> <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
   <quarkus.platform.version>1.13.0.Final</quarkus.platform.version> ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
   <surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
 </properties>
 <dependencyManagement>
   <dependencies>
     <dependency>
       <groupId>${quarkus.platform.group-id}</groupId>
       <artifactId>${quarkus.platform.artifact-id}</artifactId>
       <version>${quarkus.platform.version}</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
   </dependencies>
 </dependencyManagement>
 <dependencies>
   <dependency>
     <groupId>io.quarkus</groupId>
     <artifactId>quarkus-funqy-knative-events</artifactId> ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
   </dependency> ... </dependencies> ... <profiles>
   <profile> ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/3.png)
     <id>native</id>
     <activation>
       <property>
         <name>native</name>
       </property>
     </activation>
     <build>
       <plugins>
         <plugin>
           <artifactId>maven-failsafe-plugin</artifactId>
           <version>${surefire-plugin.version}</version>
           <executions>
             <execution>
               <goals>
                 <goal>integration-test</goal>
                 <goal>verify</goal>
               </goals>
               <configuration>
                 <systemPropertyVariables>
                   <native.image.path>${project.build.directory}/${project.build.
                    finalName}↳
-runner</native.image.path>
                   <java.util.logging.manager>org.jboss.logmanager.LogManager↳ </java.util.logging.manager>
                   <maven.home>${maven.home}</maven.home>
                 </systemPropertyVariables>
               </configuration>
             </execution>
           </executions>
         </plugin>
       </plugins>
     </build>
     <properties>
       <quarkus.package.type>native</quarkus.package.type>
     </properties>
   </profile>
 </profiles>
</project>

1

Quarkus 的版本

2

Quarkus Funqy 依赖项,一个用于 FaaS 环境的 Java API

3

用于构建 Quarkus 本机应用程序的本机配置文件

Quarkus Funqy是 Quarkus 对无服务器工作负载的支持的一部分,旨在提供一个便携的 Java API,用于编写可部署到各种 FaaS 环境(如 AWS Lambda、Azure Functions、Knative 和 Knative Events(Cloud Events))的函数。Funqy 是一个跨多个不同 FaaS 提供商和协议的抽象。它针对小型工作负载和更快的执行进行了优化,同时提供了一个简单的框架,没有额外的开销。

让我们来看一下在src/main/java/functions/Function.java路径下生成的 Java 函数的源代码:

package functions;
import io.quarkus.funqy.Funq;
public class Function {
   @Funq ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/1.png)
   public Output function(Input input) {  ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/mdn-etp-java/img/2.png)
       return new Output(input.getMessage());
   }
}

1

要启用函数,您只需使用来自 Quarkus Funqy API 的@Funq注解标记您的方法。

2

Java 类也可以用作输入和输出。它们必须遵循 JavaBean 约定并具有默认构造函数。在这里,我们正在使用InputOutput Beans。

让我们来看一下在src/main/java/functions/Input.java路径下生成的Input JavaBean 的源代码,该 JavaBean 用于表示函数的输入消息:

package functions;

public class Input {
   private String message;

   public Input() {}
   public Input(String message) {

       this.message = message;
   }
   public String getMessage() {
       return message;
   }

   public void setMessage(String message) {
       this.message = message;
   }
}

现在让我们来看一下在src/main/java/functions/Output.java路径下生成的Output JavaBean 的源代码:

package functions;

public class Output {
   private String message;

   public Output() {}

   public Output(String message) {
       this.message = message;
   }

   public String getMessage() {
       return message;
   }

   public void setMessage(String message) {
       this.message = message;
   }
}

现在我们已经准备好构建该函数了。默认情况下,Boson CLI 将连接到本地 Docker 实例,使用构建包创建容器,然后推送到您在func.yaml配置文件中声明的容器注册表:

$ func build
提示

在未来的版本中,Boson CLI 还将通过 Tekton 将构建阶段委托给 Kubernetes。

几分钟后,您应该会得到类似于以下内容的输出:

Function image built: docker.io/modernizingjavaappsbook/quarkus-faas:latest

函数构建完成后,您可以将其作为运行中的容器映像在本地测试,然后再部署到 Kubernetes:

$ func run

您应该会得到类似于以下内容的输出:

exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.
logmanager.LogManager -XX:+ExitOnOutOfMemoryError -cp . -jar /layers/dev.
boson.quarkus-jvm/app/app.jar
__ ____ __ _____  ___ __ ____ ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-06-25 16:51:24,023 INFO [io.quarkus] (main) function 1.0.0-SNAPSHOT on JVM
(powered by Quarkus 1.13.0.Final) started in 1.399s. Listening on:
http://0.0.0.0:8080
2021-06-25 16:51:24,027 INFO [io.quarkus] (main) Profile prod activated.
2021-06-25 16:51:24,028 INFO [io.quarkus] (main) Installed features: [cdi,
 funqy-knative-events]

在另一个终端中,验证进程是否正在运行:

$ docker ps | grep modernizingjavaappsbook/quarkus-faas:latest
cd1dd0ccc9b2  modernizingjavaappsbook/quarkus-faas:latest  "/cnb/process/web"
 3 minutes ago  Up 3 minutes  5005/tcp, 127.0.0.1:8080->8080/tcp
  musing_carson

尝试访问它:

$ curl \
 -X POST \
 -H "Content-Type: application/json" \
 -d '{"message":"Hello FaaS!"}' \
http://localhost:8080

您应该会得到类似于以下内容的输出:

{"message":"Hello FaaS!"}

现在让我们将其部署到 Kubernetes,并让 Knative 将其用作零缩放应用程序。当我们通过 HTTP 调用该函数时,Knative 将自动启动它,并在不使用时将其缩减为零:

$ func deploy

几秒钟后,您应该会看到类似于以下内容的输出:

Deploying function to the cluster
  Function deployed at URL: http://quarkus-faas.coolstore.192.168.39.69.nip.io

最后,在 Kubernetes 上启动您的 Quarkus 函数!

$ curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"message":"Hello FaaS on Kubernetes!"}' \
http://quarkus-faas.coolstore.192.168.39.69.nip.io

您应该会得到类似于以下内容的输出:

{"message":"Hello FaaS on Kubernetes!"}

您可以验证在coolstore命名空间内的 Kubernetes 集群中已启动了一个新的 pod:

kubectl get pods -n coolstore
NAME                                            READY  STATUS   RESTARTS  AGE
curl                                            1/1    Running  3         9d
quarkus-faas-00001-deployment-5b789c84b5-kc2jb  2/2    Running  0         80s

并且,您应该看到已创建了一个新的 Knative 服务:

kubectl get ksvc quarkus-faas -n coolstore
NAME         URL                     LATESTCREATED      LATESTREADY        ...
quarkus-faas http://quarkus...nip.io quarkus-faas-00001 quarkus-faas-00001 ...

现在您可以通过以下命令查看新部署的函数的所有详细信息:

kubectl describe ksvc quarkus-faas -n coolstore

你应该得到一个类似于这样的输出:

Name:        quarkus-faas
Namespace:   coolstore
Labels:      boson.dev/function=true
             boson.dev/runtime=quarkus
Annotations: serving.knative.dev/creator: minikube-user
             serving.knative.dev/lastModifier: minikube-user
API Version: serving.knative.dev/v1
Kind:        Service
Metadata:
 Creation Timestamp: 2021-06-25T17:14:12Z
 Generation:         1
....
Spec:
 Template:
   Metadata:
     Creation Timestamp: <nil>
   Spec:
     Container Concurrency: 0
     Containers:
       Env:
         Name:  BUILT
         Value: 20210625T171412
       Image:   docker.io/modernizingjavaappsbook/quarkus-faas@↳
sha256:a8b9cfc3d8e8f2e48533fc885c2e59f6ddd5faa9638fdf65772133cfa7e1ac40
       Name:    user-container
       Readiness Probe:
         Success Threshold: 1
         Tcp Socket:
           Port: 0
       Resources:
     Enable Service Links: false
     Timeout Seconds:      300
 Traffic:
   Latest Revision: true
   Percent:         100
Status:
 Address:
   URL: http://quarkus-faas.coolstore.svc.cluster.local
 Conditions:
   Last Transition Time:       2021-06-25T17:14:22Z
   Status:                     True
   Type:                       ConfigurationsReady
   Last Transition Time:       2021-06-25T17:14:22Z
   Status:                     True
   Type:                       Ready
   Last Transition Time:       2021-06-25T17:14:22Z
   Status:                     True
   Type:                       RoutesReady
 Latest Created Revision Name: quarkus-faas-00001
 Latest Ready Revision Name:   quarkus-faas-00001
 Observed Generation:          1
 Traffic:
   Latest Revision: true
   Percent:         100
   Revision Name:   quarkus-faas-00001
 URL:               http://quarkus-faas.coolstore.192.168.39.69.nip.io
Events:
 Type   Reason  Age   From               Message
 ----   ------  ----  ----               -------
 Normal Created 4m15s service-controller Created Configuration "quarkus-faas"
 Normal Created 4m15s service-controller Created Route "quarkus-faas"

概要

在这一章中,我们分析了 Java 开发者如何在遵循无服务器执行模型的情况下创建现代应用程序。我们概述了 Java 开发者今天和明天可能会使用的一些最常见的用例和架构。边缘计算、物联网、数据摄入和机器学习都是事件驱动架构自然选择的情境,而无服务器和 Java 可以在其中发挥战略性和支持性角色。我们讨论了 FaaS,它代表了软件开发的最新演变,以及 Kubernetes 如何可以自动化应用程序的整个生命周期,将其部署为解耦的、异步的、易于并行化处理的函数。

通过这一章,我们完成了这本“开发者简明的云原生指南”。从微服务到函数,如今 Java 开发者拥有一整套的框架、工具和平台,例如 Kubernetes,可以帮助他们现代化他们的架构,创新他们的解决方案,并展望解决今天 IT 环境中的下一个挑战。这个环境变得越来越异构、普遍、大规模和云原生。

我给了你翅膀,让你可以在其中飞翔

在无边的海洋和整个地球之上 […]

对所有关心它们的人,甚至对尚未出生的人,你将会是

就像一首歌的主题,只要地球和太阳存在。

梅加拉的提奥尼斯

posted @ 2024-06-15 12:22  绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报