Quarkus-和-Kubernetes-的-Java-微服务高级教程-三-

248 阅读57分钟

Quarkus 和 Kubernetes 的 Java 微服务高级教程(三)

原文:Pro Java Microservices with Quarkus and Kubernetes

协议:CC BY-NC-SA 4.0

七、微服务架构模式

介绍

到目前为止,我们已经将 QuarkuShop 开发为一个单块应用。所有组件都封装在一个封装中。这个单块被称为整块

根据《牛津英语词典》,monolith 是指*“*一个单一的、非常大的组织,变化非常缓慢”。在软件架构世界中,monolithic 或 monolithic 应用是一个单块应用,其中所有组件都被组合到一个包中。

例如,在 QuarkuShop 项目中,如果开发人员想要更新一个Product实体的定义,他们将访问与另一个开发PaymentService的开发人员相同的代码库。在这些更新之后,他们必须重新构建并重新部署应用的更新版本。

在运行时,QuarkuShop 被部署为一个包,即我们使用 CI/CD 管道打包在 Docker 容器中的原生 JAR。所有的 Java 代码和 HTML/CSS 资源(我们在src/main/resources/META-INF/resources中有一个网页)将在同一个块中运行,即使我们在 JAR 中嵌入了一些 React 或 Angular 代码。

QuarkuShop 处理 HTTP 请求,执行一些特定于域的逻辑,从数据库中检索和更新数据,并处理要发送给 REST 客户机的有效负载。

由于所有这些原因,QuarkuShop 是一个庞然大物。img/509649_1_En_7_Figa_HTML.gif

这是好事还是坏事? img/509649_1_En_7_Figb_HTML.gif

在培训和讨论期间,当我说一个应用是一个整体时,许多人认为这是一件坏事。本章描述了这种架构的优缺点,我将让您决定它是好还是坏,或者好+坏(两者都有一点)。img/509649_1_En_7_Figc_HTML.gif

单体应用的主要好处是它的简单性。只开发、部署和扩展一个模块很容易。

因为整个应用的代码库都在一个地方,所以只需要配置一个插槽来构建和部署应用。

Monoliths 非常容易工作,尤其是在开始的时候,这是开始一个新项目的默认选择。虽然复杂性会随着时间的推移而增长,但代码库的敏捷管理将有助于在单体应用的整个生命周期中保持生产力。密切关注代码是如何编写的,以及架构是如何发展的,将会保护项目不会变成一大盘意大利面条。

虽然我告诉你敏捷将保证项目保持干净和容易进行,但是没有互惠。单体应用组件是非常紧密耦合的,随着产品的发展会变得非常复杂。因此,随着时间的推移,开发人员很难管理它们。

“构建一个有效的复杂系统的方法是从非常简单的有效系统中构建它。”

—凯文·凯利

在现实世界的项目中,无论是中型还是大型项目,开发人员通常只处理应用的特定部分。每个开发人员通常只理解一个整体的一部分,这意味着很少有开发人员能够解释整个应用。由于 monoliths 必须作为一个单元来开发和部署,因此很难将开发工作分成独立的团队。每次代码更改都必须仔细协调,这会减慢开发速度。

这种情况对新开发人员来说可能很困难,他们不想处理多年来发展的大量代码库。结果,更多的时间花在寻找正确的代码行,并确保它没有副作用。同样,花在编写新特性上的时间会更少,而这些新特性将会改善应用。

在一个整体中采用新技术可能意味着重写整个应用,这是一项既费钱又费时的苦差事。

独石很受欢迎,因为它们比它们的替代品微服务更容易开始建造。

微服务架构

虽然 monolith 是一个单一的大型单元,但微服务架构使用小型的模块化代码单元,可以独立于产品的其余组件进行部署。

“简单的可比复杂的难。你必须努力让你的思维变得干净,让它变得简单。”

—乔布斯

微服务架构的优势

微服务架构允许将一个大的应用分解成小的、松散耦合的服务。这种方法带来了很多好处:

  • 我们的 QuarkuShop 应用是使用单体架构开发的,随着时间的推移,它已经增加了许多新特性。代码库是巨大的,新人正在加入我们的团队。他们很难开始使用给定的应用,因为应用的各个组件之间没有明确的界限。微服务方法允许我们将我们的应用视为小型、松散耦合的组件,因此任何人都可以在短时间内开始使用现有的应用。给代码库添加一个新的补丁不会太难。

  • 想象一下,今天是黑色星期五,这是一年中最繁忙的购物日,所以我们的网站流量很大。我们的产品目录被高度要求,这导致整个应用下降。如果这发生在微服务架构中,我们不会面临这样的故障,因为它独立运行多个服务。如果一个服务关闭,不会影响任何其他服务。

  • 我们是开发人员,花了很多时间来理解我们的应用代码库。我们对增加新功能感到兴奋,如产品搜索引擎。我们必须重新部署整个应用,以便向最终用户展示这些功能。我们将致力于许多这样的功能,想想每次部署所花费的时间。因此,如果应用很大,它需要大量的精力和时间,这肯定会导致生产力的损失。没有人喜欢等着看代码修改的结果。在微服务架构中,我们尽量使每个服务的代码库尽可能小,这样就不会在构建、部署等方面花费太多时间。

  • 今天我们的流量很大。什么都没坏,服务器也启动了。假设我们需要扩展应用的某些组件,例如产品目录。我们不能在一个整体架构中扩展单个组件。但是,在微服务架构中,我们可以通过动态添加更多节点来单独扩展任何组件。

  • 明天,我们希望迁移到新的框架或技术堆栈。很难升级一个整体,因为随着时间的推移,它已经变得非常大和复杂。更新微服务架构并不困难,因为组件小且灵活。

这就是微服务架构如何让我们的生活变得轻松。有各种各样的策略可以帮助我们将应用划分成小的服务,比如通过领域驱动的设计,通过业务能力,等等。

什么是真正的微服务?

一个微服务通常实现一组不同的特性或功能,比如订单管理、客户管理等。每个微服务都是一个迷你应用,有自己的业务逻辑和边界,比如 REST web 服务。一些微服务公开一个 API,由其他微服务或应用的客户端使用。其他微服务可能会实现 web UI。我们还可以使用消息队列进行通信。

应用的每个功能区域现在都由自己的微服务来实现。此外,web 应用被分成一组更小的 web 应用。

每个后端服务通常公开一个 REST API,大多数服务使用其他服务提供的 API。UI 服务调用其他服务来呈现网页。服务也可能使用异步的基于消息的通信。

一些 REST APIs 也暴露给不能直接访问后端服务的客户端应用。相反,通信是通过一个称为 *API 网关的中介进行的。*API 网关负责负载平衡、缓存、访问控制、API 计量和监控等任务。

微服务架构模式极大地影响了应用和数据库之间的关系。每个服务都有自己的数据库模式,而不是与其他服务共享一个数据库模式。一方面,这种方法与企业范围的数据模型的想法不一致。此外,它通常会导致一些数据的重复。然而,如果您想从微服务中获益,每个服务拥有一个数据库模式是必不可少的,因为它确保了松耦合。

每个服务都有一个数据库。此外,服务可以使用最适合其需求的数据库类型,即所谓的多语言持久性架构

从表面上看,微服务架构模式类似于 SOA。使用这两种方法,架构都由一组服务组成。然而,考虑微服务架构模式的一种方式是,它是 SOA,没有商业化和 Web 服务规范(WS)和企业服务总线(ESB)的包袱。基于微服务的应用更喜欢 REST 等更简单、轻量级的协议,而不是繁重的协议。他们也非常避免使用 ESB,而是在微服务本身中实现类似 ESB 的功能。

结论:进行转换

那么,有没有可能不从零开始,从单体切换到微服务架构呢?值得吗?像网飞、亚马逊和推特这样的大公司都已经从整体架构转向微服务架构,并且没有回头的意思。

改变架构可以分阶段进行。您可以从单体应用中一个接一个地提取微服务,直到完成。一个好的架构选择可以让你的应用和组织变得更好。如果你正在考虑转换,你会在接下来的章节中得到一个很好的迁移指南。

八、分裂单体:轰击领域

介绍

我们已经定义了什么是微服务架构,并讨论了它所解决的问题。我们还了解了采用微服务架构的诸多优势。但是,如何将您的单体应用迁移到微服务架构呢?如何应用这种模式?如何在不重写整个应用的情况下拆分你的 monolith?

本章回答了这些问题。我们将使用领域驱动设计作为分割 QuarkuShop 整体的方法。领域驱动设计(DDD)是一种简化软件建模和设计的软件开发方法。

什么是领域驱动设计?

领域驱动的设计有很多优点:

  • 关注核心业务领域和业务逻辑。

  • 确保设计基于领域模型的最佳方式。

  • 技术和业务团队之间的紧密合作。

要理解领域驱动设计,你需要理解它的许多概念。

语境

语境是一个特定的环境,在这个环境中,一个动作或一个术语有特定的含义。在不同的环境中,这个意思会发生变化。

领域

领域是软件开发所针对的一组知识和规范。

模型

模型是领域中参与者和组件的抽象表示。

普遍存在的语言

公司中不同角色的人对他们面临的业务问题有不同的认识。例如,如果您正在开发一个事务平台,您通常会有一个由项目经理、开发人员、开发人员、测试人员和业务分析师组成的团队。通常,业务分析师是具有业务知识的人,在这种情况下是事务,他们的角色是为不一定精通经济和金融的其他团队成员翻译业务规范和需求。

这种行话将保证所有团队成员对业务环境有相同的理解。

演讲、会议、用户故事和门票中使用的语言/行话被称为通用语言

战略设计

战略设计是领域驱动设计世界中最重要的范例之一。这有助于将总是复杂的领域分割成更小的部分。这种分裂可能是危险的,可能会改变业务逻辑中的关键概念。战略设计的力量来了:它带来了许多方法和原则,保证了主要领域的完全完整性。

您将在接下来的章节中发现战略设计的主要组成部分。

限界上下文

一个有界上下文是属于同一个业务子域的组件的逻辑集合。每个子域由一个专门的团队处理,这将优化新的开发和错误修复。加工小零件比加工大块零件容易。

一个有界的上下文有一个定义的范围,它将覆盖所有相关的模型。每个元素只需要归属于一个特定的有界上下文。从语义上来说,一个元素可以属于两个或更多的有界上下文,但是需要做出一个决定来将其归属于主有界上下文。

让我们检查一下我们一直在使用的表预订示例。当您开始设计系统时,您会看到客人会访问应用,并请求在选定的餐馆、日期和时间预订桌子。后端系统通知餐馆预订信息。类似地,假设餐馆也可以预订桌子,那么餐馆会用桌子预订来更新他们的系统。因此,当您查看系统的细微之处时,您会看到三个领域模型:

  • Product领域模型

  • Order领域模型

  • Customer领域模型

img/509649_1_En_8_Figa_HTML.jpg

它们有自己的有界上下文,您需要确保它们之间的接口工作正常。

轰炸采石场

在这里,您将学习如何将 QuarkuShop 应用分成许多步骤。

代码库

第一步是为每个有界上下文创建一个包。然后,您将把每个类移动到它相关的有界上下文包中。使用 NetBeans(和其他 ide),只需点击几下鼠标,就可以轻松地移动和重构代码。在移动代码时,您可能会发现一些您错过的新的有界上下文。

此时,保证重构不会破坏应用的唯一方法就是通过测试!

此任务是前面步骤中创建的设计的直接应用。在这个例子中,这个任务看起来很简单。但是当处理大型应用时,这将是困难的,并且需要几周甚至几个月的时间。为了优化重组操作,可以使用 Stan4j 和 Structure101 等结构分析工具。

依赖性和共有性

当您将部分代码移动到相应的包中时,您会看到一些公共类(如实用程序类)。这些常用的类必须移动到一个专用的COMMONS包中。

在这一点上,结构分析工具非常方便。

下一节将介绍域分割的最重要的元素:实体和关系。

实体

在这一层,您正在拆分源代码。因为 Java 实体被映射到 SQL 表,所以您也需要拆分数据库。

对于数据库访问代码,每个实体都有一个存储库。实体之间的关系揭示了表之间的外键关系,这代表了数据库级的约束。

在 QuarkuShop 中,一个给定的实体将属于一个特定的有界上下文。但是将实体转移到单独的包中并不是最后一步。您需要通过打破属于不同有界上下文的表之间的关系来打破 JPA 映射关系。您将通过示例了解如何在尊重应用业务逻辑完整性的同时做到这一点。

示例:断开外键关系

QuarkuShop 应用将产品信息存储在一个专用的表中,该表由 ORM 进行映射和管理。OrderItem对象将订购产品的参考和订购数量存储在一个专用表中。在OrderItem表中对Product记录的引用由一个外键约束组成。

那么怎么才能打破这种关系呢?

答案很容易。img/509649_1_En_8_Figb_HTML.gif

首先,您需要将OrderItem中的Product引用从Product改为Long,这是Product实体的主键类型。

原始代码如下:

@ManyToOne(fetch = FetchType.LAZY)
private Product product;

重构之后,看起来是这样的:

private Long product;

@ManyToOne注释是无用的,因为没有目标 JPA 实体。现在只是一个Long属性。

这一修改将限制OrderItem对产品的“知识”量。因此,当您读取OrderItem记录时,您需要在Order服务中有一个组件,它从Product服务中读取给定产品 ID 的数据。服务之间的这种通信将使用 REST 来完成,因为这是一种同步通信。

此交换在此架构中表示:

img/509649_1_En_8_Figc_HTML.png

您有两个数据库调用,而不是一个,因为这两个微服务是分开的,而您在 monolith 中只有一个调用。也许你想知道性能问题?额外的调用将花费一些额外的时间,但这并不重要。如果您在分割之前测试性能,那么您应该在进行更改时具有可见性。

因为外键关系已从代码中删除,所以 SQL 数据库中没有约束。我知道许多数据库管理员对数据完整性不满意。别难过,伙计们!您总是可以开发一个验证批次,确保数据是构建良好的,并且不受约束地不被破坏。删除一个Product也需要考虑这一点。您需要想出一个关于存储在OrderItem记录中的Productid 的策略。

每个微服务都可以自由地存储它的表。它可以使用所有微服务共享的相同数据库/模式,或者您甚至可以为每个微服务创建一个专用的数据库服务器实例。

结论

在这一章中,你学习了如何根据领域驱动的设计原则来拆分 QuarkuShop 应用。每个微服务代表一个有界的上下文。这项任务是巨大的,但是循序渐进地做将会减少达到目标所需的努力。

微服务在那里。您只需要在生产环境中解决它们,在那里它们将被消费。在下一章中,您将发现部署的世界。

九、将 DDD 应用于代码

在前一章中,你学习了如何使用领域驱动设计(DDD)将你的领域分割成有界的上下文。

在这一章中,你将学习如何使用上一章定义的切割线来分割你的代码。

将有界上下文应用于 Java 包

我们的类已经按照组件类型进行了分类,比如存储库服务等。

我们将把每个组件移动到一个新的包中,这个包包含了绑定的上下文名称。每个组件的名称具有以下格式:

img/509649_1_En_9_Figa_HTML.png

重构后,命名格式将为:

img/509649_1_En_9_Figb_HTML.png

重命名所有组件后,您将得到如下所示的项目树:

img/509649_1_En_9_Figc_HTML.jpg

等等!您可能想知道commonsconfiguration包包含什么。让我们现在讨论一下。

公地一揽子计划的诞生

回想一下,公共组件必须被收集到一个专用的commons包中,该包将由所有有界上下文共享。以下是我的commons包里的东西:

img/509649_1_En_9_Figd_HTML.jpg

请注意,这些类由配置和实用程序类组成。可以把它们想象成 Apache Commons 库,你可以在几乎每个 Java 项目中直接找到,或者通过某个依赖项间接获得。这个commons库由四个包组成:

  • config:包含用于配置 OpenAPI 实现和 Swagger UI 的OpenApiConfig

  • health:包含自定义键盘锁健康检查。

  • security:包含用于抓取 JWT 令牌的TokenService

  • utils:包含为测试提供 Keycloak 和 PostgreSQL 的实用程序类。

img/509649_1_En_9_Fige_HTML.gif请注意,所有组件都需要这些实用程序类,对于给定的微服务没有特定的风格。但是你可以随时去掉commons库。

我有一个在commons项目中分享 dto 的坏习惯。但是,我们伟大的技术评论家圣乔治·安德里亚纳基斯建议我停止这样做!我考虑了一下,我同意这是一个坏主意,即使对于概念验证项目也是如此。

img/509649_1_En_9_Figf_HTML.gif我们在讲微服务的时候说过,拆分之后,微服务之间可以互相对话。所以,想象一下这种情况:Order想要使用给定的Product IDProduct微服务获得一个ProductProduct中的 REST API 将返回一个由Product记录的数据填充的ProductDTO对象,该对象将被序列化为 JSON 并返回给请求服务Order。在这个层次上,每个微服务都有一个Product的定义。基于许多元素,如项目规范、文档,甚至通过 OpenAPI 资源,不同的微服务可以轻松地进行通信。如果 dto 在commons库中共享,我们可能会失去微服务的一个好处:摆脱紧耦合。

定位有界上下文关系

这一步旨在打破有界上下文(BCs)之间的依赖关系。为了能够打破它们,您需要首先找到这些依赖关系。有很多方法可以找到他们。例如,通过查看类图、源代码等。在前一章中,我们谈到了可以帮助你突出你的 monolith 块之间的依赖关系的工具。

我主要用的是 STAN (STAN4J),一个强大的 Java 结构分析工具。STAN 支持一套精心选择的度量标准,适合于覆盖结构质量的最重要的方面。特别关注视觉依赖分析,这是结构分析的关键。

STAN 有两种型号:

  • 作为 Windows 和 macOS 的独立应用,面向通常不使用 IDE 的架构师和项目经理。

  • 作为 Eclipse 集成开发环境(IDE)的扩展,它允许开发人员快速探索任何代码束的结构。

我们将使用第二种选择。我获得了 Eclipse IDE 的全新安装,并按照 http://stan4j.com/download/ide/ 中的描述安装了 IDE 扩展。

我们的 monolith 是一个基于 Maven 的项目,所以它可以很容易地导入到 Eclipse IDE 中。导入后,只需遵循以下步骤:

  1. 右键单击该项目。

  2. 选择作为➤ Maven Build 运行。

  3. 选择 Maven 配置,如果您已经有一个的话。

  4. 在目标中,只需输入clean install -DskipTests并选择运行。

  5. 你完蛋了!

接下来,创建项目的结构分析:

  1. 右键单击该项目。

  2. 选择“作为➤结构分析运行”。

  3. 耶!img/509649_1_En_9_Figg_HTML.gif结构图在这里!

img/509649_1_En_9_Figh_HTML.jpg

打破 BC 关系

现在,您将打破有界上下文之间的关系。您可以从实体之间的关系开始。通常,数据库中的表之间的关系更有效。这里,实体类被视为关系表(JPA 概念),因此实体类之间的关系如下:

  • @ManyToOne关系

  • @OneToMany关系

  • @OneToOne关系

  • @ManyToMany关系

更准确地说,您不会破坏属于同一个 BC 的实体之间的关系,但是会破坏 BC 间的关系。例如,考虑属于Order ContextOrderItem和属于Product ContextProduct之间的关系。

在 Java 中,这种关系由以下内容表示:

public class OrderItem extends AbstractEntity {
    @NotNull
    @Column(name = "quantity", nullable = false)
    private Long quantity;

    @ManyToOne
    private Product product;

    @ManyToOne
    private Order order;
}

注释product字段的@ ManyToOne是这里的目标。

你怎么打破这种关系?很简单,这个街区:

@ManyToOne
private Product product;

将被更改如下:

private Long productId;

所以,你可能想知道为什么我们用Long类型替换Product类型?很简单。LongProduct's ID 的类型。

太好了。如果OrderItemProduct之间的关系是双向关系,你必须对Product类做同样的事情。

当您尝试使用mvn clean install构建项目时,会遇到编译问题。这是显而易见的,因为您编辑了OrderItem,所以许多使用这个类的组件必须知道这些修改。在这里,OrderItemService才是问题所在。

让我们看看OrderItemService类中有错误的块。这是第一个:

public OrderItemDto create(OrderItemDto orderItemDto) {
    log.debug("Request to create OrderItem : {}", orderItemDto);

    var order = this.orderRepository
          .findById(orderItemDto.getOrderId())
          .orElseThrow(() -> new IllegalStateException("The Order does not exist!"));

    var product = this.productRepository
          .findById(orderItemDto.getProductId())
          .orElseThrow(() -> new IllegalStateException("The Product does not exist!"));

    return mapToDto(
        this.orderItemRepository.save(
            new OrderItem(
                    orderItemDto.getQuantity(),
                    product,
                    order
            )));
}

我们从ProductRepository中获取一个Product实例,并将它传递给OrderItem构造函数。在OrderItem类中不再有Product字段(您将其改为Product ID ) ,因此您需要将 ID 传递给OrderItem构造函数,而不是Product

因此,Product对象不再存在于Order有界上下文中。现在怎么处理ProductProductRepository这两个职业?

你忘了吗?在Order有界上下文的范围内,我们有commons模块,它包含了ProductDTO,我们可以免费使用!

对于ProductRepository,显而易见。它将被一个 REST 客户端取代,该客户端将从Product微服务收集Product数据,并在需要时填充已知的ProductDTO对象。但是在这里,我们只需要用Product ID来创建OrderItem实例。所以,在这种情况下,我们不需要获取Product记录。

第二个块是将OrderItem映射到OrderItemDto的方法:

public static OrderItemDto mapToDto(OrderItem orderItem) {
        return new OrderItemDto(
                orderItem.getId(),
                orderItem.getQuantity(),
                orderItem.getProductId(),
                orderItem.getOrder().getId()
        );
    }

必须从这个方法中删除对Product的引用,因为它已经从OrderItem类中删除了。我们没有产品作为成员,但是我们有OrderItem类中的Product ID。所以orderItem.getProduct().getId()指令必须改为orderItem.getProductId()。为了从订单总价中增加/减去产品价格,我们在OrderItemService中注入ProductRepository

产生的OrderItemService将如下所示:

@Slf4j
@ApplicationScoped
@Transactional
public class OrderItemService {

    @Inject OrderItemRepository orderItemRepository;
    @Inject OrderRepository orderRepository;
    @Inject ProductRepository productRepository;

    public static OrderItemDto mapToDto(OrderItem orderItem) {
        return new OrderItemDto(orderItem.getId(),
                orderItem.getQuantity(), orderItem.getProductId(),
                orderItem.getOrder().getId());
    }

    public OrderItemDto findById(Long id) {
        log.debug("Request to get OrderItem : {}", id);
        return this.orderItemRepository.findById(id)
                    .map(OrderItemService::mapToDto).orElse(null);
    }

    public OrderItemDto create(OrderItemDto orderItemDto) {
        log.debug("Request to create OrderItem : {}", orderItemDto);
        var order = this.orderRepository
                .findById(orderItemDto.getOrderId()).orElseThrow(() ->
                    new IllegalStateException("The Order does not exist!"));

        var orderItem = this.orderItemRepository.save(
                new OrderItem(orderItemDto.getQuantity(),
                        orderItemDto.getProductId(), order
                ));

        var product = this.productRepository.getOne(orderItem.getProductId());

        order.setPrice(order.getPrice().add(product.getPrice()));
        this.orderRepository.save(order);

        return mapToDto(orderItem);
    }

    public void delete(Long id) {
        log.debug("Request to delete OrderItem : {}", id);

        var orderItem = this.orderItemRepository.findById(id)
                .orElseThrow(() ->
                    new IllegalStateException("The OrderItem does not exist!"));

        var order = orderItem.getOrder();
        var product = this.productRepository.getOne(orderItem.getProductId());

        order.setPrice(order.getPrice().subtract(product.getPrice()));

        this.orderItemRepository.deleteById(id);

        order.getOrderItems().remove(orderItem);
        this.orderRepository.save(order);
    }

    public List<OrderItemDto> findByOrderId(Long id) {
        log.debug("Request to get all OrderItems of OrderId {}", id);
        return this.orderItemRepository.findAllByOrderId(id)
                .stream()
                .map(OrderItemService::mapToDto)
                .collect(Collectors.toList());
    }
}

在这个重构之后,您可以使用 STAN 这个伟大的工具来看看这些修改是如何改变项目的结构的。

经过一些检查后,剩下要做的修改是:

  • order包装中:

    1. Order:private Cart cart;将变为private Long cartId;

    2. OrderService:Cart参考将变为CartDto

  • customer包装中:

    1. Cart:第private Order order;会改成private Long orderId

    2. CartService:Order参考将变为OrderDto

在这些修改之后,我们在CartService中仍然有一个OrderService引用。如前所述,这个OrderService将被一个 REST 客户端取代,该客户端将调用由Order微服务公开的 API。在分析结构之前,您可以引用OrderService来注释这些行。

在这次重构之后,以下是这些修改如何改变了项目的结构:

img/509649_1_En_9_Figi_HTML.jpg

img/509649_1_En_9_Figj_HTML.gif你没有完全切断orderproduct包之间的联系。这是因为,要计算订单总额,您需要有产品价格。您将在以后的步骤中看到如何处理这个链接。

结论

这很好!您刚刚完成了迁移过程中最繁重的任务之一!

接下来,您可以开始构建独立的微服务。在此之前,您需要了解更多关于最佳实践和微服务模式的信息。

十、满足微服务的需求和模式

当我们处理架构和设计时,我们立即开始考虑配方和模式。这些设计模式对于在云中构建可靠、可伸缩、安全的应用非常有用。

一个模式是一个在特定环境中出现的问题的可重用解决方案。这是一个源于现实世界架构的想法,已经被证明在软件架构和设计中是有用的。

实际上,微服务架构是最强大的架构模式之一。我们在第八章中详细讨论了这种模式。

当呈现一个模式时,我们从定义上下文和问题开始,然后提供模式给出的解决方案。

云图案

本章讨论这些模式:

  • 外部化配置

  • 服务发现和注册

  • 断路器

  • 每个服务的数据库

  • API 网关

  • CQRS(消歧义)

  • 活动采购

  • 日志聚合

  • 分布式跟踪

  • 审核日志记录

  • 应用指标

  • 运行状况检查 API

服务发现和注册

背景和问题

在微服务领域,服务注册和发现扮演着重要的角色,因为您很可能会运行多个服务实例,并且需要一种机制来调用其他服务,而无需硬编码它们的主机名或端口号。除此之外,在云环境中,服务实例可能随时增加和减少。所以你需要一个自动的服务注册和发现机制。

在一个单一的应用中,组件之间的调用是通过语言级别的调用进行的。但是,在微服务架构中,服务通常需要使用 REST(或其他)调用来相互调用。为了发出请求,服务需要知道给定服务实例的网络位置(IP 地址和端口)。如前几章所述,微服务会动态分配网络位置。这是由许多因素造成的,例如不同的部署频率。此外,一个服务可以有多个实例,由于自动伸缩、失败等原因,这些实例可以动态地保持变化。

因此,您必须实现一种机制,使给定服务的客户端能够向一组动态变化的服务实例发出请求。

服务的客户端如何发现服务实例的位置?服务的客户端如何知道服务的可用实例?

解决办法

您可以创建服务注册中心,它是可用服务实例的数据库。它是这样工作的:

  • 微服务实例的网络位置在实例启动时向服务注册中心注册。

  • 当实例终止时,微服务实例的网络位置将从服务注册表中删除。

  • 微服务实例的可用性通常使用心跳机制定期刷新。

这种模式有两种类型:

  • 客户端发现模式:请求服务(客户端)负责寻找可用服务实例的网络位置。客户端查询服务注册中心,然后使用一些负载平衡算法选择一个可用的服务实例并发出请求。这种机制遵循客户端发现模式。

  • 服务器端发现模式:服务器端发现模式建议客户端通过负载均衡器向服务发出请求。负载平衡器负责查询服务注册中心,并将每个请求转发给可用的服务实例。因此,如果您想要遵循这种模式,您需要拥有(或实现)一个负载平衡器。

外部化配置

背景和问题

应用通常使用一个或多个基础设施(消息代理和数据库服务器等。)和第三方服务(支付网关、电子邮件和消息等)。).这些服务需要配置信息(例如凭证)。此配置信息存储在与应用一起部署的文件中。

在某些情况下,可以在部署后编辑这些文件来改变应用的行为。但是,更改配置需要重新部署应用,这通常会导致不可接受的停机时间和其他管理开销。

本地配置文件还将配置限制到单个应用,有时在多个应用之间共享配置设置会很有用。示例包括数据库连接字符串以及相关应用集使用的队列和存储的 URL。

我们的整体被分成许多微服务。所有这些微服务都需要提供给 monolith 的配置信息。假设我们需要更新数据库的 URL。所有微服务都需要完成此任务。如果我们忘记在某个地方更新数据,可能会导致在部署更新时实例使用不同的配置设置。

解决办法

将所有应用配置具体化,包括数据库凭证和网络位置。例如,您可以在外部存储配置信息,并提供一个可用于快速有效地读取和更新配置设置的界面。您可以将这个配置存储称为配置服务器。

当微服务启动时,它从给定的配置服务器中读取配置。

断路器

背景和问题

微服务主要使用 HTTP REST 请求进行通信。当一个微服务与另一个同步时,总是存在另一个服务由于高延迟而不可用或不可达的风险,这意味着它本质上是不可用的。这些不成功的调用可能会导致资源耗尽,这将使调用服务无法处理其他请求。一个服务的失败可能会影响整个应用中的其他服务。

解决办法

发出请求的微服务应该通过代理调用远程服务,代理的工作机制类似于电路断路器。当连续失败的次数超过阈值时,断路器跳闸,并且在超时期间,所有调用远程服务的尝试都将立即失败。超时后,断路器允许有限数量的测试请求通过。如果这些请求成功,断路器恢复正常运行。如果出现故障,超时时间将重新开始。

断路器模式,由迈克尔·尼加德在他的书*中推广开来!,*可以防止应用反复尝试执行可能失败的操作。这允许应用继续运行,而无需等待故障被修复或浪费 CPU 周期,同时确定故障是长期持续的。断路器模式还使应用能够检测故障是否已经解决。如果问题似乎已经解决,应用可以尝试调用操作。

每个服务的数据库

背景和问题

在微服务架构世界中,服务必须是松散耦合的,这样它们才能独立开发、部署和扩展。

大多数服务需要将数据保存在某种数据库中。在我们的应用中,Order Service存储订单信息,Customer Service存储客户信息。

微服务应用中的数据库架构是什么?

解决办法

将每个微服务的持久数据保持为该服务私有,并且只能通过其 API 访问。

该服务的数据库实际上是该服务实现的一部分。其他服务不能直接访问它。

有几种不同的方法可以使服务的持久数据保持私有。您不需要为每个服务提供一个数据库服务器。例如,如果您使用关系数据库,选项如下:

  • 每个服务拥有一组只能由该服务访问的表。

  • 每个服务都有一个专用于该服务的数据库模式。

  • 每个服务都有自己的数据库服务器。

Private-tables-per-serviceschema-per-service开销最低。为每个服务使用一个模式很有吸引力,因为它使所有权更加清晰。一些高吞吐量服务可能需要自己的数据库服务器。

创建壁垒来加强这种模块化是一个好主意。例如,您可以为每个服务分配不同的数据库用户 ID,并使用数据库访问控制机制,比如 grants。如果没有某种强制封装的障碍,开发人员总是会试图绕过服务的 API,直接访问其数据。

应用编程接口网关

背景和问题

对于 QuarkuShop 精品店,假设您正在实现产品详细信息页面。假设您需要开发多个版本的产品详细信息用户界面:

  • 基于 HTML5/JavaScript 的桌面和移动浏览器 UI:HTML 由服务器端 web 应用生成。

  • 本地 Android 和 iPhone 客户端:这些客户端通过 REST APIs 与服务器交互。

此外,QuarkuShop 必须通过 REST API 公开产品细节,供第三方应用使用。

产品详细信息 UI 可以显示关于产品的大量信息。例如:

  • 产品的基本信息,如名称、描述、价格等。

  • 您的产品购买历史。

  • 可用性。

  • 购买期权。

  • 经常与本产品一起购买的其他物品。

  • 购买该产品的顾客购买的其他物品。

  • 顾客评论。

因为 QuarkuShop 遵循微服务架构模式,所以产品细节数据分布在多个服务上:

  • 产品服务:产品的基本信息,如名称、描述、价格、客户评论和产品可用性。

  • 订单服务:产品的购买历史。

  • QuarkuShop: 顾客、推车等。

因此,显示产品详细信息的代码需要从所有这些服务中获取信息。

基于微服务的应用的客户端如何访问单个服务?

解决办法

实现一个 API 网关,它是所有客户端的单一入口点。API 网关以两种方式之一处理请求。一些请求被简单地代理/路由到适当的服务。它通过分散到多个服务来处理其他请求。

API 网关可以为每个客户端提供不同的 API,而不是提供一种通用的 API。API 网关还可以实现安全性,例如验证客户端是否被授权执行请求。

CQRS(消歧义)

背景和问题

在传统的数据管理系统中,命令(数据更新)和查询(数据请求)都是针对单个数据存储库中的同一组实体执行的。这些实体可以是关系数据库(如 SQL Server)中一个或多个表中行的子集。

通常在这些系统中,所有创建、读取、更新和删除(CRUD)操作都应用于实体的相同表示。例如,代表客户的数据传输对象(DTO)由数据访问层(DAL)从数据存储中检索并显示在屏幕上。用户更新 DTO 的一些字段(可能通过数据绑定),然后 DAL 将 DTO 保存回数据存储中。相同的 DTO 用于读取和写入操作。

当只有有限的业务逻辑应用于数据操作时,传统的 CRUD 设计工作得很好。开发工具提供的支架机制可以非常快速地创建数据访问代码,然后可以根据需要定制这些代码。

然而,传统的 CRUD 方法有一些缺点:

  • 这通常意味着数据的读写表示之间存在不匹配,例如必须正确更新的附加列或属性,即使它们不是操作的一部分。

  • 当记录被锁定在协作域中的数据存储中时,存在数据争用的风险,在协作域中,多个参与者对同一组数据进行并行操作。当使用乐观锁定时,由并发更新引起的更新冲突也是一个问题。随着系统复杂性和吞吐量的增加,这些风险也会增加。此外,由于数据存储和数据访问层的负载,以及检索信息所需的复杂查询,传统方法可能会对性能产生负面影响。

  • 这可能会使管理安全性和权限变得更加复杂,因为每个实体都受到读和写操作的影响,这可能会在错误的上下文中公开数据。

解决办法

命令和查询责任分离(CQRS)是一种模式,它通过使用不同的接口将读取数据(查询)的操作与更新数据(命令)的操作分离开来。这意味着用于查询和更新的数据模型是不同的。然后可以隔离这些模型。

与基于 CRUD 的系统中使用的单一数据模型相比,在基于 CQRS 的系统中为数据使用单独的查询和更新模型简化了设计和实现。然而,一个缺点是,与 CRUD 设计不同,CQRS 代码不能使用支架机制自动生成。

用于读取数据的查询模型和用于写入数据的更新模型可以访问同一个物理存储,这可能是通过使用 SQL 视图或动态生成投影来实现的。

读取存储可以是写入存储的只读副本,或者读取和写入存储可以具有完全不同的结构。使用读取存储的多个只读副本可以大大提高查询性能和应用 UI 响应能力,尤其是在只读副本位于应用实例附近的分布式方案中。

活动采购

背景和问题

大多数应用处理数据,典型的方法是应用通过在用户处理数据时更新数据来维护数据的当前状态。例如,在传统的创建、读取、更新和删除(CRUD)模型中,典型的数据流程是从存储中读取数据,对其进行一些修改,并用新值更新数据的当前状态—通常是通过使用锁定数据的事务。

CRUD 方法有一些限制:

  • CRUD 系统直接对数据存储执行更新操作,这会降低性能和响应速度,并限制可伸缩性,因为这需要处理开销。

  • 在有许多并发用户的协作域中,数据更新冲突更有可能发生,因为更新操作发生在单个数据项上。

  • 除非有额外的审计机制在单独的日志中记录每个操作的细节,否则历史就会丢失。

解决办法

Event Sourcing 模式定义了一种处理由一系列事件驱动的数据操作的方法,每个事件都记录在一个只加存储中。应用代码将一系列事件发送到事件存储区,这些事件强制性地描述了数据上发生的每个操作,事件存储区保存了这些事件。每个事件代表一组数据的变化(比如AddedItemToOrder)。

事件保存在事件存储中,事件存储充当有关数据当前状态的记录系统(权威数据源)。事件存储通常会发布这些事件,以便用户可以得到通知,并在需要时处理它们。例如,消费者可以启动将事件中的操作应用到其他系统的任务,或者执行完成操作所需的任何其他相关操作。请注意,生成事件的应用代码与订阅事件的系统是分离的。

事件存储发布的事件的典型用途是在应用中的操作改变实体的物化视图时维护它们,以及与外部系统集成。例如,系统可以维护所有客户订单的物化视图,该视图用于填充部分 UI。当应用添加新订单、添加或删除订单上的项目以及添加运输信息时,描述这些更改的事件可以被处理并用于更新物化视图。

此外,在任何时候,应用都可以读取事件的历史,并通过回放和使用与该实体相关的所有事件来使用它来具体化该实体的当前状态。这可以根据需要在处理请求时具体化一个域对象,也可以通过一个调度任务来实现,以便实体的状态可以存储为一个物化视图来支持表示层。

日志聚合

背景和问题

在微服务架构中,我们的应用由运行在不同服务器和位置上的多个服务和服务实例组成。请求经常跨越多个服务实例。

当我们使用 monolith 时,应用生成一个日志流,它通常存储在一个日志文件/目录中。现在,每个服务实例都生成自己的日志文件。

当日志以这种方式分割时,如何识别应用的行为并解决问题?

解决办法

使用集中的日志记录服务,该服务聚合来自每个服务实例的日志。当日志被聚集时,用户可以搜索和分析日志。他们可以配置当日志中出现某些消息时触发的警报。

分布式跟踪

背景和问题

在微服务架构中,请求通常跨越多个服务。每个服务通过执行一个或多个操作来处理请求,例如数据库查询、消息发布等。

当请求失败时,您如何识别行为并解决问题?

解决办法

具有以下代码的仪表服务:

  • 为每个外部请求分配一个唯一的外部请求 ID

  • 将外部请求 ID 传递给处理请求所涉及的所有服务

  • 在所有日志消息中包含外部请求 ID

  • 记录关于在集中式服务中处理外部请求时所执行的请求和操作的信息(例如,开始时间和结束时间)

审核日志记录

背景和问题

在微服务架构中,除了日志系统之外,我们还需要更多关于服务的可见性,以监控事情的进展。

您如何监控用户和应用的行为并解决任何问题?

解决办法

您可以在数据库或一些特殊的专用日志系统中记录用户活动。

应用指标

背景和问题

在微服务架构中,除了我们已经拥有的指标之外,我们还需要更多关于服务的可见性,以了解正在发生的事情。

你如何识别和阐明一个应用的行为?

解决办法

推荐的解决方案是拥有一个集中的度量服务,该服务收集和存储每个服务操作的决策支持统计信息。微服务可以将它们的度量信息推送到度量服务。另一方面,指标服务可以从微服务中提取指标。

运行状况检查 API

背景和问题

监控 web 应用和后端服务是一个很好的实践,通常也是一个业务需求,目的是确保它们可用并正常运行。然而,监控云中运行的服务比监控内部服务更困难。有许多因素会影响应用,例如网络延迟、底层计算和存储系统的性能和可用性以及网络带宽。这些因素中的任何一个都可能导致服务完全或部分失败。因此,您必须定期验证服务是否正常运行,以确保所需的可用性级别。

解决办法

通过向应用上的端点发送请求来实现健康监控。应用应该执行必要的检查,并返回其状态指示。

健康监控检查通常结合了两个因素:

  • 应用或服务为响应对健康验证端点的请求而执行的检查(如果有)。

  • 通过执行运行状况验证检查的工具或框架对结果进行分析。

响应代码指示应用的状态,并且可选地指示它使用的任何组件或服务的状态。延迟或响应时间检查由监控工具或框架执行。

服务之间的安全性:访问令牌

背景和问题

在微服务架构中,通过使用 API 网关模式,应用由许多服务组成。API 网关是客户端请求的单一入口点。它对请求进行身份验证,并将它们转发给其他服务,这些服务可能会调用其他服务。

如何将请求者的身份传达给处理请求的服务?

解决办法

API 网关对请求进行认证,并将访问令牌(例如,JSON web 令牌)传递给服务,该令牌安全地标识每个请求中的请求者。服务可以在向其他服务发出的请求中包含访问令牌。

结论

既然您已经拆分了 QuarkuShop monolith 并了解了一些有用的模式,那么您可以开始构建独立的微服务了。

十一、Kubernetes 入门

介绍

为了部署我们的(如此庞大的)应用,img/509649_1_En_11_Figa_HTML.gif我们将使用img/509649_1_En_11_Figb_HTML.gif Docker。我们将在一个容器中部署我们的代码,因此我们可以享受由img/509649_1_En_11_Figc_HTML.gif Docker 提供的强大功能。

Docker 已经成为开发和运行容器化应用的标准。

使用img/509649_1_En_11_Fige_HTML.gif Docker 非常简单,尤其是在开发阶段。在同一个服务器中部署容器(docker-machine)很简单,但是当您需要将许多容器部署到许多服务器时,事情就变得复杂了(管理服务器、管理容器状态等等。).

这就是编排系统发挥作用的时候,它提供了许多出色的功能:

  • 在运行的任务之间协调资源

  • 基于许多因素,如资源需求、相似性要求等,调度容器并使其与机器匹配。

  • 处理复制

  • 处理故障

对于本教程,我们使用img/509649_1_En_11_Figf_HTML.gif Kubernetes,容器编排之星。

什么是 Kubernetes?

Kubernetes(又名 K8s)是一个从 Google 分离出来的项目,是一个开源的下一代容器调度器。它是利用从开发和管理博格和欧米茄中学到的经验设计的。

img/509649_1_En_11_Figi_HTML.jpg

Kubernetes 被设计成具有松散耦合的组件,以部署、维护和扩展应用为中心。K8s 抽象了节点的底层基础设施,并为部署的应用提供了统一的层。

永恒的建筑

Kubernetes 集群由两项组成:

  • 主节点:Kubernetes 的主控制平面。它包含一个 API 服务器、一个调度器、一个控制器管理器(K8s 集群管理器)和一个用于保存集群状态的数据存储库Etcd

  • Worker node :运行 pod 的单个主机,物理或虚拟机。它由主节点管理。

让我们看看主节点的内部:

  • Kube API-Server :允许通过 REST APIs,在主节点和它的客户端之间进行通信,例如工作节点、kube-cli等。

  • Kube 调度器(Kube Scheduler):调度器就像一个餐厅服务员,根据特定的逻辑(也就是一个策略)给你分配“第一个可用”的桌子。Kube 调度器根据特定的策略将新成员分配到最合适的节点。

  • Kube 控制器管理器:一个永久运行的进程,负责确保 Kubernetes 集群状态与管理员请求的状态相同。如果管理员发出了一些配置命令,Kube 控制器管理器负责验证这些命令是否应用于整个集群。

  • Etcd:Kubernetes 用来保存集群配置的键值数据库。

让我们看看 worker 节点的内部:

  • Kubelet :在集群中每个节点上运行的代理。它确保节点在主节点中正确注册,并验证 POD 运行正常。

  • Kube-Proxy :集群中每个节点上运行的代理。它确保网络规则在节点中正确应用。

img/509649_1_En_11_Figj_HTML.jpg

img/509649_1_En_11_Figk_HTML.gif我们使用的容器运行时是 Docker。Kubernetes 兼容很多其他的,比如cri-orkt等。

不可思议的核心概念

K8s 生态系统涵盖了很多概念和组件。下一节将简要讨论它们。

库布特雷

kubectl是一个 CLI,用于在已配置的 Kubernetes 集群上执行命令。

一个集群是集合了它们的资源(CPU、RAM、磁盘等)的主机的集合。)放入一个公共的可用池中,然后由集群资源共享(和控制)。

命名空间

名称空间想象成一个用来保存不同种类资源的文件夹。这些资源可以被一个或多个用户使用。

要列出所有的名称空间,可以使用kubectl get namespacekubectl get ns命令。

标签

标识和选择相关对象集的键值对。标签有严格的语法和定义的字符集。

豆荚

一个 pod 是 Kubernetes 的基本工作单元。pod 代表共享资源(如 IP 地址和存储)的容器集合。见清单 11-1 。

apiVersion: v1
kind: Pod
metadata:
  name: example-pod
  labels:
    app: example
spec:

  containers:
  - name: example-container
    image: busybox
    command: ['sh', '-c', 'echo Hello World :) !']

Listing 11-1Pod Example

要列出所有窗格,运行kubectl get podkubectl get po命令。

replication set-复制集

该组件负责在任何给定时间运行所需数量的副本容器。参见清单 11-2 。

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: mongodb
  labels:
    app: mongodb
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
      - name: mongodb
        image: mongo:4.4
        imagePullPolicy: Always

Listing 11-2ReplicaSet Example

要列出所有副本集,请运行kubectl get replicasetkubectl get rs命令。

部署

这包括 pod 模板和副本字段。Kubernetes 将确保实际状态(副本的数量和 pod 模板)总是与期望的状态相匹配。当您更新部署时,它将执行“滚动更新”参见清单 11-3 。

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

Listing 11-3Deployment Example

要列出所有部署,使用kubectl get deployment命令。

有状态任务

该组件负责管理必须保持或维护状态的 pod。包括主机名、网络和存储在内的 pod 标识将被保留。参见清单 11-4 。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "my-storage-class"
      resources:
        requests:
          storage: 1Gi

Listing 11-4StatefulSet Example

要列出所有状态集,运行kubectl get statefulset命令。

达蒙塞特

该组件在集群中的所有(或部分)节点上创建每个 pod 的实例。参见清单 11-5 。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule
      containers:
        - name: fluentd-elasticsearch
          image: gcr.io/google-containers/fluentd-elasticsearch:1.20
      terminationGracePeriodSeconds: 30

Listing 11-5DaemonSet Example

要列出所有 DaemonSets,请运行kubectl get daemonsetkubectl get ds命令。

服务

这定义了一个 IP/端口组合,提供对一组 pod 的访问。它使用标签选择器将多组 pod 和端口映射到一个集群独有的虚拟 IP。参见清单 11-6 。

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    run: my-nginx

Listing 11-6Service Example

要列出所有服务,运行kubectl get servicekubectl get svc命令。

进入

入口控制器是向外界公开集群服务(通常是http)的主要方法。这些是负载平衡器或路由器,通常提供 SSL 终端、基于名称的虚拟主机等。见清单 11-7 。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - http:
        paths:
          - path: /testpath
            backend:
              service:
                name: test
                port:
                  number: 80

Listing 11-7Ingress Example

要列出所有入口,请使用kubectl get ingress

这表示与 pod 生命周期相关的存储,可由 pod 中的一个或多个容器使用。

持久卷

持久卷(PV)代表一种存储资源。PVs 通常链接到后备存储资源,如 NFS、GCEPersistentDisk、RBD 等。、和是提前配置的。它们的生命周期独立于 pod 进行处理。

要列出所有持久卷,请运行kubectl get persistentvolumekubectl get pv命令。

PersistentVolumeClaim

PersistentVolumeClaim (PVC)是满足一组要求的存储请求。它通常用于动态调配的存储。

要列出所有 PersistentVolumeClaims,请使用kubectl get persistentvolumeclaimkubectl get pvc命令。

存储类

存储类是对外部存储资源的抽象。它们包括置备程序、置备程序配置参数和 PV reclaimPolicy

要列出所有存储类,请运行kubectl get storageclasskubectl get sc

职位

作业控制器确保一个或多个 pod 被执行并成功终止。它将这样做,直到它满足完成和/或并行条件。

要列出所有作业,运行kubectl get job

克朗乔布

作为作业控制器的扩展,CronJob 提供了一种按照类似 cron 的时间表执行作业的方法。

要列出所有 CronJobs,运行kubectl get cronjob

ConfigMap(配置地图)

ConfigMap 是存储在 Kubernetes 中的外部化数据,可以作为命令行参数或环境变量引用,或者作为文件注入到卷挂载中。它们是实现外部配置存储模式的理想选择。

要列出所有配置图,运行kubectl get configmapkubectl get cm

秘密

功能上与 ConfigMaps 相同,但存储编码为 base64,静态加密(如果已配置)。

要列出所有秘密,运行kubectl get secret

在本地运行 Kubernetes

对于本教程,我们不会构建一个真正的 Kubernetes 集群。我们使用 Minikube。

Minikube 用于在本地运行单节点 Kubernetes 集群,没有任何痛苦。这个工具非常有用,尤其是对于开发。

如需安装 Minikube,请访问 https://github.com/kubernetes/minikube

安装完成后,运行以下命令启动 Minikube:

minikube start

当运行 Minikube 时,一个新的 Kubernetes 上下文被创建并可用。minikube start命令创建一个名为minikubekubectl context

Kubernetes 上下文包含与 Kubernetes 集群通信所需的配置。

要访问 Kubernetes 仪表板,请运行:

minikube dashboard

仪表板将在您的默认浏览器中打开,如下所示:

img/509649_1_En_11_Figm_HTML.jpg

这些minikube命令特别有用:

  • minikube stop:停止 K8s 集群并关闭 Minikube 虚拟机,而不会丢失集群内容。

  • minikube delete:删除 K8s 集群和 Minikube 虚拟机。

您可以使用以下命令为minikube分配资源:

minikube config set memory 8192                 ①
minikube config set cpus 4                      ②
minikube config set kubernetes-version 1.16.2   ③
minikube config set vm-driver kvm2              ④
minikube config set container-runtime crio      ⑤

每行定义:

  • ①分配的内存为 8192MB (8GB)

  • ②分配给 4 个 CPU

  • ③kubler 版本至 1.16.2

  • ④虚拟机驱动程序到kvm2。要了解更多关于司机的信息,请访问 https://minikube.sigs.k8s.io/docs/drivers/

  • ⑤容器运行时使用crio而不是 Docker(默认选择)

img/509649_1_En_11_Fign_HTML.gif在设置新配置之前,您需要删除当前的minikube集群实例。

要检查配置是否正确保存,只需运行minikube config view:

$ minikube config view

- cpus: 4
- kubernetes-version: 1.16.2
- memory: 8192
- vm-driver: kvm2
- container-runtime: crio

实践总结和结论

Kubernetes 是市场上使用最广泛的容器 orchestrator。许多解决方案现在都基于它,比如 OpenShift 容器平台和 Rancher Kubernetes 引擎。Kubernetes 成为了云原生架构和基础设施的标准。几乎所有的云提供商都管理 Kubernetes 托管以下内容:

  • 蓝色忽必烈服务

  • 亚马逊弹性库柏服务

  • 谷歌库比厄引擎

  • IBM 云库服务

  • 用于 Kubernetes 的 Oracle 容器引擎

  • 阿里巴巴容器服务公司

甚至 OVH 和数字海洋也加入了提供托管 Kubernetes 托管解决方案的竞赛。img/509649_1_En_11_Figp_HTML.gif

如本章第一部分所列,有许多 Kubernetes 对象。每一种都可以用来满足特定的需求。在下一节中,我们将看看这些对象如何满足我们的需求。

img/509649_1_En_11_Figq_HTML.jpg

QuarkuShop 被打包成一个 Docker 容器。在 Kubernetes 中,我们的应用将在 pod 对象中运行,这是最基本的 Kubernetes 对象!对于给定的微服务,我们可能希望有多个 pod。为了避免手动管理它们,您可以使用 Deployment 对象,该对象将处理每个 pod 集。它还将确保有所需数量的实例,以防您希望某个特定 pod 有许多实例。

要存储这些属性,可以使用 ConfigMap。要存储凭证,可以使用SECRET对象。要访问它们,您需要与 Kubernetes API 服务器通信,以获取/读取所需的数据。

当我们将单体应用拆分为微服务时,我们说它们之间的通信是基于 HTTP 协议的(直接或间接)。每个微服务都在一个 pod 内运行,它将拥有一个专用的动态 IP 地址。因此,例如,如果Order微服务与Product微服务通信,它就无法猜测其目标的 IP 地址。我们需要使用类似 DNS 的解决方案,使用域名而不是 IP 地址,让系统动态解析域名。这正是 K8s 服务所做的。属于同一个服务的所有单元在同一个 DNS 名称下共享它们的 IP 地址,在那里你可以对它们进行负载平衡。

除了动态 IP 地址解析,该服务还包括负载平衡功能。

要在集群外部公开 QuarkuShop,请使用INGRESS对象。它对外公开了一个 Kubernetes 服务,并具有许多优秀的特性,包括负载平衡、SSL 等。

附加阅读

我无法在一章中完全涵盖 Kubernetes。这一章只是对 Kubernetes 世界的一个小介绍。我建议你读读马尔科·卢卡写的、曼宁出版社出版的《Kubernetes 在行动中的 ??》。我个人认为这是 Kubernetes 写的最好的书。

img/509649_1_En_11_Figs_HTML.jpg

如果你更喜欢视频,我推荐这个伟大的 Kubernetes 学习 YouTube 播放列表,它是由我的朋友 Houssem Dellai 制作的,他是微软的云工程师。

img/509649_1_En_11_Figt_HTML.jpg

十二、实现云模式

介绍

你已经知道你将在本书中使用 Kubernetes 作为云平台。在前一章中,您学习了将与 QuarkuShop 一起使用的 Kubernetes 对象。在这一章中,您将开始实现一些云模式,并将 monolithic universe(monolithic application、PostgreSQL 和 Keycloak)引入 Kubernetes。

将庞大的宇宙带到 Kubernetes

在开始处理应用代码之前,您需要将img/509649_1_En_12_Figa_HTML.gif PostgreSQL 数据库和 Keycloak 放到 Kubernetes 集群中。

将 PostgreSQL 部署到 Kubernetes

要将一个img/509649_1_En_12_Figb_HTML.gif PostgreSQL 数据库实例部署到 Kubernetes,您将使用:

  • 存储 PostgreSQL 用户名和数据库名的ConfigMap

  • 存储 PostgreSQL 密码的Secret

  • 一个为 PostgreSQL pods 请求存储空间的PersistentVolumeClaim

  • 一个Deployment提供了想要的 PostgreSQL pods 的描述

  • 用作指向 PostgreSQL pods 的 DNS 名称的Service

PostgreSQL 的ConfigMap如清单 12-1 所示。

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres
data:
  POSTGRES_DB: demo
  POSTGRES_USER: developer

Listing 12-1postgres-cm.yaml

PostgreSQL 密码存储在一个Secret中。该值需要编码为 Base64。您可以在本地使用openssl库对p4SSW0rd字符串进行编码:

echo -n 'p4SSW0rd' | openssl base64

结果是cDRTU1cwcmQ=。你在Secret对象内部使用它,如清单 12-2 所示。

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  labels:
    app: postgres
type: Opaque
data:
  POSTGRES_PASSWORD: cDRTU1cwcmQ=

Listing 12-2postgres-secret.yaml

清单 12-3 显示了用于请求 PostgreSQL 存储访问的PersistentVolumeClaim

  • PVC 名称。

  • ②访问类型。许多 pod 可以在这个 PVC 中同时读写。

  • ③所需的 PVC 存储。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc        ①
  labels:
    app: postgres
spec:
  accessModes:
    - ReadWriteMany         ②
  resources:
    requests:
      storage: 2Gi          ③

Listing 12-3postgres-pvc.yaml

清单 12-4 展示了 PostgreSQL Deployment文件。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1                         ①
  selector:
    matchLabels:
      app: postgres                   ②
  template:
    metadata:
      labels:
        app: postgres
    spec:
      volumes:                        ③
        - name: data                  ③
          persistentVolumeClaim:      ③
            claimName: postgres-pvc   ③
      containers:
        - name: postgres
          image: postgres:12.3
          envFrom:
            - configMapRef:           ④
                name: postgres-config ④
            - secretRef:              ④
                name: postgres-secret ④
          ports:
            - containerPort: 5432
          volumeMounts:               ③
            - name: data              ③
              mountPath: /var/lib/postgresql/data
              subPath: postgres
          resources:
            requests:
              memory: '512Mi'           ⑤
              cpu: '500m'               ⑤
            limits:
              memory: '1Gi'             ⑥
              cpu: '1'                  ⑥

Listing 12-4postgres-deployment.yaml

deployment资源将使用以下内容部署 PostgreSQL:

  • ①一个 pod 实例。

  • ②瞄准带有app=postgres标签的单元。

  • ③将postgres-pvc定义为持久性卷。

  • ④从postgres-configpostgres-secret加载环境变量。

  • ⑤每个 pod 所需的最低资源是 512MB 和 0.5 个 CPU 单元。

  • ⑥每个 pod 允许的最大资源是 1GB 和 1 个 CPU 单元。

PostgreSQL Service文件如清单 12-5 所示。

apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  selector:
    app: postgres
  ports:
   - port: 5432
  type: LoadBalancer

Listing 12-5postgres-svc.yaml

我们正在创建一个类型为LoadBalancerService。在支持负载平衡器的云提供商上,将提供一个外部 IP 地址来访问Service。当我们在minikube时,LoadBalancer类型使Service可以通过minikube service命令访问:

minikube service SERVICE_NAME

接下来,我们使用 IntelliJ 数据库浏览器测试已部署的 PostgreSQL 实例。

首先获取 PostgreSQL Kubernetes 服务 URL。您可以使用这个命令从 Minikube 集群获取 Kubernetes 服务 URL:

$ minikube service postgres --url

http://192.168.39.79:31450

该命令将返回 Minikube 集群中服务的 Kubernetes URL。如果有很多 URL,将一次打印一个。

我们将使用以下内容:

  • 192.168.39.79:31450作为数据库的 URL

  • developer作为用户

  • p4SSW0rd作为密码

  • demo作为数据库名称

只需点击测试连接,以验证一切正常。如果没问题,您将得到一个确认连接成功的img/509649_1_En_12_Fige_HTML.gif

img/509649_1_En_12_Figf_HTML.jpg

将 Keycloak 部署到 Kubernetes

为了将 Keycloak 部署到 Kubernetes 集群,我们将使用 Helm。

What is Kubernetes Helm?

Helm 是 Kubernetes 的一个包管理器,允许开发人员和操作人员更容易地将应用和服务打包、配置和部署到 Kubernetes 集群上。

Helm 现在是 Kubernetes 的一个官方项目,也是 Cloud Native Computing Foundation 的一部分,这是一个支持 Kubernetes 生态系统内外开源项目的非营利组织。

舵柄可以:

  • 安装软件。

  • 自动安装软件依赖项。

  • 升级软件。

  • 配置软件部署。

  • 从存储库中获取软件包。

Helm 通过以下组件提供此功能:

  • 一个名为helm的命令行工具,提供了所有舵功能的用户界面。

  • 一个名为tiller的配套服务器组件运行在您的 Kubernetes 集群上,监听来自helm的命令,并处理集群上软件版本的配置和部署。

  • 舵的包装格式,称为图表

What is a Helm Chart ?

Helm 包被称为 charts ,它们由几个 YAML 配置文件和一些呈现在 Kubernetes 清单文件中的模板组成。

helm命令可以从本地目录安装图表,或者从这个目录结构的.tar.gz打包版本安装图表。这些打包的图表也可以从图表存储库或 repos 自动下载和安装。

要安装 Helm CLI,只需转到本指南: helm.sh/docs/intro/install

安装 Helm CLI 后,您可以将 Keycloak Helm chart 安装到 Kubernetes 集群中。

您需要安装的 Keycloak helm 图表可以在codecentric存储库中找到,因此您需要将其添加到 helm 存储库中:

helm repo add codecentric https://codecentric.github.io/helm-charts

接下来,创建一个 Kubernetes 名称空间来安装 Keycloak:

$ kubectl create namespace keycloak

namespace/keycloak created

接下来,使用这个 Helm 命令安装 Keycloak:

helm install keycloak --namespace keycloak codecentric/keycloak

要列出在keycloak名称空间中创建的对象,使用kubectl get all -n keycloak命令:

NAME                        READY   STATUS    RESTARTS
pod/keycloak-0              1/1     Running   0
pod/keycloak-postgresql-0   1/1     Running   0

NAME                                   TYPE        CLUSTER-IP      PORT(S)
service/keycloak-headless              ClusterIP   None            80/TCP
service/keycloak-http                  ClusterIP   10.96.104.85    80/TCP 8443/TCP 9990/TCP
service/keycloak-postgresql            ClusterIP   10.107.175.90   5432/TCP
service/keycloak-postgresql-headless   ClusterIP   None            5432/TCP

NAME                                   READY
statefulset.apps/keycloak              1/1
statefulset.apps/keycloak-postgresql   1/1

请注意,我们有两个 pod:

  • keycloak-0:包含 Keycloak 应用

  • keycloak-postgresql-0:包含一个专用于 Keycloak 应用的 PostgreSQL 数据库实例

现在你需要像在第五章中一样配置 Keycloak 实例。要访问键盘锁盒,使用端口转发从盒到localhost:

kubectl -n keycloak port-forward service/keycloak-http 8080:80

现在,访问localhost:8080/auth上的钥匙锁控制台:

img/509649_1_En_12_Figg_HTML.jpg

在此屏幕中,您可以创建管理员用户:

img/509649_1_En_12_Figh_HTML.jpg

接下来,单击管理控制台以访问登录屏幕:

img/509649_1_En_12_Figi_HTML.jpg

登录后,您将进入 Keycloak 管理控制台:

img/509649_1_En_12_Figj_HTML.jpg

你现在可以使用与第五章相同的步骤来创建钥匙锁领域。

将单块夸库商店部署到库贝内特斯

在将img/509649_1_En_12_Figk_HTML.gif PostgreSQL 数据库和 Keycloak 引入 Kubernetes 之后,您可以继续将 monolithic QuarkuShop 应用部署到 Kubernetes。这个练习对于学习如何只将一个应用部署到 Kubernetes 非常有用。

我们首先向 quark shoppom.xml添加两个依赖项,如下所示:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-kubernetes</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-container-image-jib</artifactId>
</dependency>

当使用 Maven 构建项目时,这两个库将:

  • 为应用创建 Docker 图像

  • 生成部署应用所需的 Kubernetes 对象描述符

为了验证我说的是事实,img/509649_1_En_12_Figl_HTML.gif让我们构建应用并检查它:

$ mvn clean install -Dquarkus.container-image.build=true

target目录中,有两个新文件夹——jibkubernetes。要检查它们的内容,请运行以下命令:

  • ①生成 Docker 映像时 JIB 使用的目录。

  • ②用于存储生成的 Kubernetes 描述符的目录。相同的内容以两种格式生成:YAML 和 JSON。

$ ls -l target/jib target/kubernetes

target/jib:         ①
total 32
-rw-rw-r-- 1 nebrass nebrass 1160 sept.  8 21:01 application.properties
-rw-rw-r-- 1 nebrass nebrass  427 sept.  8 21:01 banner.txt
drwxrwxr-x 3 nebrass nebrass 4096 sept.  8 21:01 com
drwxrwxr-x 3 nebrass nebrass 4096 sept.  8 21:01 db
drwxrwxr-x 7 nebrass nebrass 4096 sept.  8 21:01 io
drwxrwxr-x 7 nebrass nebrass 4096 sept.  8 21:01 javax
drwxrwxr-x 7 nebrass nebrass 4096 sept.  8 21:01 META-INF
drwxrwxr-x 4 nebrass nebrass 4096 sept.  8 21:01 org

target/kubernetes:  ②
total 12
-rw-rw-r-- 1 nebrass nebrass 5113 sept.  8 21:01 kubernetes.json
-rw-rw-r-- 1 nebrass nebrass 3478 sept.  8 21:01 kubernetes.yml

如果希望基于本机二进制文件构建 Docker 映像,请运行以下命令:

mvn clean install -Pnative -Dquarkus.native.container-build=true -Dquarkus.container-image.build=true

您还可以验证本地是否有新创建的 Docker 映像:

$ docker images

REPOSITORY           TAG              IMAGE ID       CREATED          SIZE
nebrass/quarkushop   1.0.0-SNAPSHOT   eb2c67d7fa27   21 minutes ago   244MB

接下来,将该图像推送到 Docker Hub:

$ docker push nebrass/quarkushop:1.0.0-SNAPSHOT

在导入 Kubernetes 描述符之前,您需要做一个小的修改。该应用当前指向本地 PostgreSQL 和 Keycloak 实例。你有两个选择:

  • 更改硬编码的属性。

  • 使用环境变量覆盖这些属性。这是最好的解决方案,因为它不需要新的版本。

为了覆盖这些属性,我们将使用环境变量。例如,要将quarkus.http.port属性的值覆盖为9999,您可以创建一个名为QUARKUS_HTTP_PORT=9999的环境变量。正如您所注意到的,这是一个带下划线而不是分隔点.的大写字符串,在本例中,我们想要覆盖这些属性:

  • quarkus.datasource.jdbc.url:指向 PostgreSQL URL。其对应的环境变量将是QUARKUS_DATASOURCE_JDBC_URL

  • mp.jwt.verify.publickey.location:指向 Keycloak 网址。其对应的环境变量将是MP_JWT_VERIFY_PUBLICKEY_LOCATION

  • mp.jwt.verify.issuer:指向 Keycloak 网址。其对应的环境变量将是MP_JWT_VERIFY_ISSUER

这三个属性指向localhost作为 PostgreSQL 和 Keycloak 实例的主机。我们希望它们现在指向 PostgreSQL 和 Keycloak 各自的 pod。

如前一章所述,K8s 服务对象可以用作每组 pod 的 DNS 名称。

让我们列出集群中可用的Service对象:

  • ①PostgreSQLServicedefault名称空间中可用。

  • ②我们在keycloak名称空间中有几个 Keycloak Service对象:

    • 指向 Keycloak pods 的无头 Kubernetes 服务。无头服务用于实现与匹配 pod 的直接通信,而无需中间层。通信中不涉及代理或路由层。无头服务列出所有选定的后备 pod,而其他服务类型将呼叫转发到随机选择的 pod。

    • Kubernetes 服务,为 Keycloak pods 提供负载平衡路由器。

    • Kubernetes 服务,为 PostgreSQL pods 提供负载平衡路由器。

    • keycloak-postgresql-headless:指向 PostgreSQL pods 的无头 Kubernetes 服务。

$ kubectl get svc --all-namespaces

NAMESPACE             NAME                          PORT(S)
default               kubernetes                    443/TCP
default               postgres                      5432:31450/TCP              ①
keycloak              keycloak-headless             80/TCP                      ②
keycloak              keycloak-http                 80/TCP,8443/TCP,9990/TCP    ②
keycloak              keycloak-postgresql           5432/TCP                    ②
keycloak              keycloak-postgresql-headless  5432/TCP                    ②
kube-system           kube-dns                      53/UDP,53/TCP,9153/TCP
kubernetes-dashboard  dashboard-metrics-scraper     8000/TCP
kubernetes-dashboard  kubernetes-dashboard          80/TCP

在这种情况下,我们将不会使用 headless 服务,因为我们希望实现请求负载平衡,并且我们不需要直接与 pod 通信。

quarkus.datasource.jdbc.url属性包含jdbc:postgresql://localhost:5432/demo。我们将使用来自default名称空间的端口 5432 的postgres服务,而不是localhost。因此,新的属性定义将是:

quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/demo

环境变量将是:

QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgres:5432/demo

很好!我们将对mp.jwt.verify.publickey.locationmp.jwt.verify.issuer属性应用相同的逻辑。

1 mp.jwt.verify.publickey.location=http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
2 mp.jwt.verify.issuer=http://localhost:9080/auth/realms/quarkushop-realm

我们将使用来自keycloak名称空间的端口 80 的keycloak-http来代替localhost。新的properties值将为:

1 mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
2 mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

img/509649_1_En_12_Figm_HTML.gif注意,为了可见性,我们在 Keycloak 服务的service name后面附加了namespace name,但是我们没有为 PostgreSQL 服务这样做。这是因为在我们部署应用的default名称空间中存在 PostgreSQL 服务。Keycloak 服务在keycloak名称空间中可用。

MP JWT环境变量将是:

1 MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
2 MP_JWT_VERIFY_ISSUER=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

现在我们需要将这些新的环境变量添加到 Kubernetes 描述符中,该描述符是由 Quarkus Kubernetes 扩展在target/kubernetes/目录中生成的。

在 Kubernetes 世界中,如果我们想要将环境变量传递给 pod,我们使用 Kubernetes Deployment 对象在spec.template.spec.containers.env部分传递它们。

Kubernetes.json描述符文件中更新后的Deployment对象如清单 12-6 所示。

{
  "apiVersion": "apps/v1",
  "kind": "Deployment",
  "metadata": { ...,
    "name": "quarkushop"
  },
  "spec": {
    "replicas": 1,
    "selector": ...,
    "template": {
      "metadata": {
        "annotations": ...,
        "labels": {
          "app.kubernetes.io/name": "quarkushop",
          "app.kubernetes.io/version": "1.0.0-SNAPSHOT"
        }
      },
      "spec": {
        "containers": [
          {
            "env": [
              { "name": "KUBERNETES_NAMESPACE",
                "valueFrom": {
                  "fieldRef": { "fieldPath": "metadata.namespace" }
                }
              },
              { "name": "QUARKUS_DATASOURCE_JDBC_URL",
                "value": "jdbc:postgresql://postgres:5432/demo"
              },
              { "name": "MP_JWT_VERIFY_PUBLICKEY_LOCATION",
                "value": "http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs"
              },
              { "name": "MP_JWT_VERIFY_ISSUER",
                "value": "http://keycloak-http.keycloak/auth/realms/quarkushop-realm"
              }
            ],
            "image": "nebrass/quarkushop:1.0.0-SNAPSHOT",
            "imagePullPolicy": "IfNotPresent",
  ...
}

Listing 12-6target/kubernetes/kubernetes.json

每次都必须添加这些值是很烦人的,尤其是在每次构建代码时都删除目标文件夹的情况下。这就是为什么 Quarkus 团队为 Kubernetes 描述符添加了一个新的环境变量定义机制。您可以在 https://quarkus.io/guides/deploying-to-kubernetes#env-vars 了解更多信息。

您可以使用带有前缀quarkus.kubernetes.env.vars.application.properties文件为每个环境变量添加相同的环境变量:

1 quarkus.kubernetes.env.vars.quarkus-datasource-jdbc-url=jdbc:postgresql://postgres:5432/demo
2 quarkus.kubernetes.env.vars.mp-jwt-verify-publickey-location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
3 quarkus.kubernetes.env.vars.mp-jwt-verify-issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

要导入生成的 Kubernetes 描述符,请使用:

$ kubectl apply -f target/kubernetes/kubernetes.json

serviceaccount/quarkushop created
service/quarkushop created
deployment.apps/quarkushop created

要验证 QuarkuShop 是否正确部署到 Kubernetes,只需列出当前(default)名称空间中的所有对象,如下所示:

$ kubectl get all

NAME                              READY   STATUS    RESTARTS
pod/postgres-69c47c748-pnbbf      1/1     Running   3
pod/quarkushop-78c67844ff-7fzbv   1/1     Running   0

NAME                 TYPE           CLUSTER-IP    PORT(S)
service/kubernetes   ClusterIP      10.96.0.1     443/TCP
service/postgres     LoadBalancer   10.106.7.15   5432:31450/TCP
service/quarkushop   ClusterIP      10.97.230.13  8080/TCP

NAME                         READY   UP-TO-DATE   AVAILABLE
deployment.apps/postgres     1/1     1            1
deployment.apps/quarkushop   1/1     1            1

NAME                                    DESIRED   CURRENT   READY
replicaset.apps/postgres-69c47c748      1         1         1
replicaset.apps/quarkushop-78c67844ff   1         1         1

在资源列表中,有一个名为quarkushop-78c67844ff-7fzbv的 pod。为了检查一切是否正常,您需要访问打包在其中的应用。为此,您从quarkushop-78c67844ff-7fzbv pod 的端口 8080 到localhost的端口 8080 执行一次port-forward

$ kubectl port-forward quarkushop-78c67844ff-7fzbv 8080:8080

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

现在打开http://localhost:8080/api/health-ui,如截图所示:

img/509649_1_En_12_Fign_HTML.jpg

您可以看到,运行状况检查验证了 PostgreSQL 数据库和 Keycloak 是可访问的。

使用application.properties定义环境变量并不总是一个好主意。例如,如果您想要更改/添加一个属性,您需要构建并重新部署应用。有一种替代方法,不需要所有这些努力,就可以使用ConfigMaps为应用提供额外的properties。应用将ConfigMap内容解析为环境变量,这是将属性传递给应用的一种很好的方式。

为此,您需要另一个 Maven 依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-kubernetes-config</artifactId>
</dependency>

您需要删除之前添加的ENV VARs属性:

quarkus.kubernetes.env.vars.quarkus-datasource-jdbc-url=jdbc:postgresql://postgres: ...
quarkus.kubernetes.env.vars.mp-jwt-verify-publickey-location=http://keycloak-http.k...
quarkus.kubernetes.env.vars.mp-jwt-verify-issuer=http://keycloak-http.keycloak/a...

接下来,您启用 Kubernetes ConfigMap访问,并告诉应用哪个ConfigMap具有您需要的属性:

quarkus.kubernetes-config.enabled=true
quarkus.kubernetes-config.config-maps=quarkushop-monolith-config

清单 12-7 展示了如何定义新的ConfigMap称为quarkushop-monolith-config

1 apiVersion: v1
2 kind: ConfigMap
3 metadata:
4   name: quarkushop-monolith-config
5 data:
6   application.properties: |-
7     quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/demo
8     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
9     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

Listing 12-7quarkushop-monolith-config.yml

您只需将quarkushop-monolith-config.yml导入到 Kubernetes 集群中:

kubectl apply -f quarkushop-monolith-config.yml

现在,如果您再次构建 QuarkuShop 应用,您会注意到在生成的 Kubernetes 描述符中,有一个新的RoleBinding对象。这个对象由quarkus-kubernetes-config Quarkus 扩展生成。

  • ①当前对象是RoleBinding

  • RoleBinding对象名称为quarkushop:view

  • ③这将把ClusterRoleview角色绑定,以读取ConfigMapsSecrets

  • ④这个RoleBinding应用于quarkus-kubernetes扩展产生的ServiceAccount称为quarkushop

{
  "apiVersion" : "rbac.authorization.k8s.io/v1",
  "kind" : "RoleBinding",                             ①
  "metadata" : {
    "annotations" : {
      "prometheus.io/path" : "/metrics",
      "prometheus.io/port" : "8080",
      "prometheus.io/scrape" : "true"
    },
    "labels" : {
      "app.kubernetes.io/name" : "quarkushop",
      "app.kubernetes.io/version" : "1.0.0-SNAPSHOT"
    },
    "name" : "quarkushop:view"                        ②
  },
  "roleRef" : {
    "kind" : "ClusterRole",                           ③
    "apiGroup" : "rbac.authorization.k8s.io",
    "name" : "view"                                   ③
  },
  "subjects" : [ {
    "kind" : "ServiceAccount",                        ④
    "name" : "quarkushop"                             ④
  } ]
}

让我们构建并打包应用映像,并将其再次部署到 K8s 集群:

mvn clean install -DskipTests -DskipITs -Pnative \
      -Dquarkus.native.container-build=true \
      -Dquarkus.container-image.build=true

接下来,推送图片:

docker push nebrass/quarkushop:1.0.0-SNAPSHOT

接下来,再次将应用部署到 Kubernetes:

kubectl apply -f target/kubernetes/kubernetes.json

现在要测试应用,只需在 QuarkuShop pod 上执行port-forward:

$ kubectl get pods

NAME                          READY   STATUS    RESTARTS   AGE
postgres-69c47c748-pnbbf      1/1     Running   5          19d
quarkushop-77dcfc7c45-tzmbs   1/1     Running   0          73m

$ kubectl port-forward quarkushop-77dcfc7c45-tzmbs 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

然后打开http://localhost:8080/api/health-ui/,如截图所示:

img/509649_1_En_12_Figo_HTML.jpg

太棒了!您了解了如何将单体应用引入 Kubernetes。在下一章中,您将使用相同的步骤来创建微服务并将其引入 Kubernetes 集群。

结论

本章试图介绍一些基于 Kubernetes 生态系统的云模式。将单体应用及其依赖项引入 Kubernetes 的练习是创建和部署微服务的第一步。