《基于云原生的Spring实战:使用Spring Boot和Kubernetes》第二章:云原生模式和技术

本章内容包括:

  1. 理解云原生应用的开发原则
  2. 使用Spring Boot构建云原生应用
  3. 使用Docker和Buildpacks将应用容器化
  4. 使用Kubernetes将应用部署到云端
  5. 介绍本书中使用的模式和技术

我们在设计云原生应用时与传统方法不同。《12-Factor》方法论包含了最佳实践和开发模式,是构建被视为云原生的应用的良好起点。我将在本章的第一部分解释这个方法论,并在本书的其余部分进行扩展。

在本章的后面部分,我们将构建一个简单的Spring Boot应用,并使用Java,Docker和Kubernetes运行它,如图2.1所示。在本书的其余部分,我将深入研究每个主题,所以如果有什么不太清楚的地方,不要担心。本章旨在为您提供一个从代码到生产的云环境之旅的思维导图,同时让您熟悉我们在本书中使用的模式和技术。

最后,我将向您介绍我们将在本书中使用Spring和Kubernetes构建的云原生项目。我们将应用本书第一部分介绍的所有云原生应用的特性和模式。

云原生开发原则:12因素及其他因素

Heroku云平台的工程师提出了"12-Factor方法论",作为设计和构建云原生应用的开发原则的集合。他们从他们的经验中总结出了一些构建Web应用程序的最佳实践,具有以下特点:

  • 适合在云平台上部署
  • 具备可扩展性
  • 可在不同系统之间移植
  • 支持持续部署和敏捷性

其目标是帮助开发者构建适用于云端的应用程序,突出了在实现最佳结果时应考虑的重要因素。

后来,该方法论被Kevin Hoffman在他的书《Beyond the Twelve-Factor App》中进行了修订和扩展,刷新了原始因素的内容,并增加了三个额外因素。从现在开始,我将称这个扩展的原则集合为"15-Factor方法论"。

这15个因素将在本书中指导您,因为它们是开发云原生应用的良好起点。如果您正在从零开始构建一个新的应用程序或将传统系统迁移到云端,这些原则可以帮助您在这一过程中。在相关章节,我将更详细地阐述它们,并说明如何将它们应用于Spring应用程序。熟悉这些原则是非常重要的。

让我们深入了解这些因素。

一个代码库,一个应用程序

这段文本描述了"15-Factor方法论",它在应用程序和其代码库之间建立了一对一的映射,因此每个应用程序都有一个对应的代码库。任何共享的代码应该被跟踪在自己的代码库中,可以作为一个库以依赖项的形式被包含,或者作为一个可以独立运行的服务,用作其他应用程序的后备服务。每个代码库可以选择性地在自己的存储库中进行跟踪。

部署是应用程序的运行实例。在不同的环境中可以有多个部署,它们都共享相同的应用程序构件。在部署应用程序到特定环境时,无需重新构建代码库:在部署之间发生变化的任何方面(如配置)应该位于应用程序代码库之外。

API先行

一个云原生系统通常由通过API进行通信的不同服务组成。在设计云原生应用程序时采用"API先行"方法鼓励您考虑将其适应分布式系统,并支持将工作分配给不同的团队。通过首先设计API,其他团队可以使用该应用程序作为后备服务,并针对该API创建自己的解决方案。通过预先设计契约,与其他系统的集成将在部署流程中更加稳健和可测试。在内部,API的实现可以更改,而不会影响依赖于它的其他应用程序(和团队)。

依赖管理

所有应用程序的依赖关系都应在清单中明确声明,并且应该能够通过依赖管理器从中央仓库下载。在Java应用程序的情况下,我们通常可以使用像Maven或Gradle这样的工具来很好地遵循这个原则。应用程序对周围环境的唯一隐含依赖是语言运行时和依赖管理器工具。这意味着私有依赖关系应该通过依赖管理器来解析。

设计、构建、发布、运行

代码库在从设计到部署生产环境的过程中经历了不同的阶段:

  • 设计阶段:确定特定应用程序功能所需的技术、依赖和工具。

  • 构建阶段:代码库被编译,并与其依赖项一起打包成一个不可变的构建构件。构建构件必须具有唯一标识。

  • 发布阶段:将构建构件与特定部署的配置相结合。每个发布都是不可变的,应该具有唯一的标识,比如使用语义化版本号(例如3.9.4)或时间戳(例如2022-07-07_17:21)。发布应该存储在中央仓库中,以便轻松访问,比如当需要回滚到之前的版本时。

  • 运行阶段:应用程序在特定发布的执行环境中运行。

"15-Factor方法论"要求严格分离这些阶段,并且不允许在运行时更改代码,因为这会导致与构建阶段不匹配。构建和发布构件应该是不可变的,并带有唯一标识,以确保可复现性。

配置、凭据和代码

"15-Factor方法论"将配置定义为在部署之间可能发生变化的所有内容。每当需要更改应用程序的配置时,您应该能够在不更改代码的情况下进行更改,而且不需要重新构建应用程序。

配置可能包括用于连接后端服务(如数据库或消息系统)的资源句柄、访问第三方API的凭据和功能标志。请自问,如果您的代码库突然变为公共,是否会暴露任何凭据或特定于环境的信息。这将告诉您是否已正确地将配置外部化。

为了符合这个原则,配置不能包含在代码中或在同一个代码库中进行跟踪。唯一的例外是默认配置,可以与应用程序代码库一起打包。对于任何其他类型的配置,仍然可以使用配置文件,但应将其存储在一个单独的存储库中。

该方法论建议将配置存储为环境变量。通过这样做,您可以在不同的环境中部署相同的应用程序,但根据环境的配置具有不同的行为。

日志

一个云原生应用程序不涉及日志的路由和存储。应用程序应该将日志记录到标准输出,将日志视为按时间顺序发出的事件序列。日志的存储和轮换不再是应用程序的职责。一个外部工具(日志聚合器)将获取、收集并使日志可供检查。

可丢弃性

在传统环境中,您会非常关注您的应用程序,确保它们保持运行状态,永远不会终止。但在云环境中,您不需要那么在意:应用程序是短暂的。如果发生故障,应用程序不再响应,您可以终止它并启动一个新的实例。如果有高负载峰值,您可以启动更多应用程序实例以支撑增加的工作负载。我们称一个应用程序是可处置的,如果它可以随时启动或停止。

为了以这种动态的方式处理应用程序实例,您应该设计它们在需要新实例时能够快速启动,并在不再需要时优雅地关闭。快速启动可以实现系统的弹性,确保其健壮性和弹性。没有快速启动,您将会面临性能和可用性问题。

优雅的关闭是指应用程序在收到终止信号后,停止接受新的请求,完成已在处理中的请求,然后最终退出。对于Web进程,这很简单。但对于其他情况,比如工作者进程,它们负责的作业必须返回到工作队列,然后才能退出。

后备服务

后备服务可以定义为应用程序用于提供其功能的外部资源。后备服务的示例包括数据库、消息代理、缓存系统、SMTP服务器、FTP服务器或RESTful Web服务。将它们视为附加资源意味着您可以在不修改应用程序代码的情况下轻松更改它们。

考虑一下在软件开发生命周期中如何使用数据库。很可能在不同的阶段(开发、测试或生产)使用不同的数据库。如果将数据库视为附加资源,您可以根据环境使用不同的服务。附加是通过资源绑定来实现的。例如,数据库的资源绑定可能包括URL、用户名和密码。

环境一致性

环境一致性是指尽可能保持所有环境的相似性。实际上,这个因素试图解决三个差距:

  • 时间差距---代码变更和部署之间的时间可能相当长。该方法论努力促进自动化和持续部署,以缩短开发人员编写代码到生产部署的时间间隔。

  • 人员差距---开发人员构建应用程序,运维人员负责在生产环境中管理其部署。这个差距可以通过拥抱DevOps文化,改善开发人员和运维人员之间的合作,以及采用"你构建它,你运行它"的理念来解决。

  • 工具差距---环境之间的主要区别之一是如何处理后备服务。例如,开发人员可能在本地环境中使用H2数据库,而在生产环境中使用PostgreSQL。总体而言,在所有环境中应该使用相同类型和版本的后备服务。

管理流程

通常需要一些管理任务来支持应用程序。例如数据库迁移、批处理作业或维护作业等任务应被视为一次性的过程。与应用程序进程类似,管理任务的代码应该在版本控制中进行跟踪,与其支持的应用程序一起交付,并在与应用程序相同的环境中执行。

通常将管理任务作为小型独立服务进行设计是一个不错的主意,这些服务运行一次后就被丢弃,或者将其配置为在无状态平台上的函数,在某些事件发生时触发执行,或者将它们嵌入到应用程序本身中,通过调用特定的端点来激活它们。

端口绑定

遵循 "15-Factor方法论" 的应用程序应该是自包含的,并通过端口绑定导出其服务。在生产环境中,可能会有一些路由服务,用于将公共端点的请求转换为内部端口绑定的服务。

如果应用程序在执行环境中不依赖于外部服务器,则它是自包含的。例如,Java Web应用程序可能会在像Tomcat、Jetty或Undertow这样的服务器容器中运行。相比之下,云原生应用程序不需要环境中有可用的Tomcat服务器;它会像处理其他依赖项一样自己管理它。例如,Spring Boot允许您使用内嵌服务器:应用程序将包含服务器而不是依赖于执行环境中是否有可用服务器。这种方法的一个结果是应用程序和服务器之间始终存在一对一的映射,而不像传统方法中将多个应用程序部署到同一台服务器。

然后,应用程序通过端口绑定导出提供的服务。Web应用程序会将HTTP服务绑定到特定端口,并可能成为另一个应用程序的后备服务。这通常是云原生系统中的情况。

无状态进程

在前一章中,你了解到高可伸缩性是我们转向云端的原因之一。为了确保可伸缩性,我们将应用程序设计为无状态过程,并采用无共享架构:不应该在不同的应用程序实例之间共享状态。请自问,如果你的应用程序的一个实例被销毁并重新创建,是否会丢失任何数据。如果答案是肯定的,那么你的应用程序就不是无状态的。

无论如何,在大多数情况下,我们总是需要保存一些状态,否则我们的应用程序将毫无用处。因此,我们将应用程序设计为无状态,并且只在特定的有状态服务(如数据存储)中处理状态。换句话说,无状态应用程序将状态管理和存储委托给后备服务。

并发性(Concurrency)

仅仅创建无状态的应用程序并不足以确保可伸缩性。如果你需要扩展,那意味着你需要为更多用户提供服务。因此,你的应用程序应该允许并发处理,以同时为许多用户提供服务。

"15-Factor方法论"将进程视为一等公民。这些进程应该具有横向扩展性,即将工作负载分布在不同机器上的许多进程中,而这种并发处理只有在应用程序无状态的情况下才可能实现。在Java虚拟机(JVM)应用程序中,我们通过使用线程池中的多个线程来处理并发。

进程可以根据其类型进行分类。例如,你可能有处理HTTP请求的Web进程,以及在后台执行定时作业的工作进程。

遥测

可观察性是云原生应用程序的特性之一。在云中管理分布式系统是复杂的,唯一的管理方法是确保每个系统组件提供正确的数据,以便远程监视系统的行为。遥测数据的示例包括日志、指标、跟踪信息、健康状态和事件。Hoffman使用了一个非常引人注目的比喻来强调遥测的重要性:将你的应用程序视为太空探测器。为了远程监控和控制你的应用程序,你需要哪种类型的遥测数据呢?

认证(Authentication)和授权(Authorization)

安全性是软件系统的基本品质之一,但通常并未得到足够的关注。遵循"零信任"(zero-trust)方法,我们必须在系统的任何架构和基础设施层面保护系统内的所有交互。毫无疑问,安全性不仅仅涉及认证和授权,但这些是一个良好的起点。

通过认证,我们可以追踪使用应用程序的用户。有了这个信息,我们可以检查用户权限,以验证用户是否被允许执行特定的操作。有几个标准可用于实现身份和访问管理,包括OAuth 2.1和OpenID Connect,这本书中我们会使用它们。

使用Spring构建云原生应用程序

现在是时候更具体地讨论技术了。到目前为止,你已经了解了云原生方法和我们将遵循的主要开发实践。现在让我们来看看Spring。如果你正在阅读本书,你可能有一些之前使用Spring的经验,并且想学习如何使用它来构建云原生应用程序。

Spring生态系统提供了处理几乎任何应用程序可能具有的要求的功能,包括云原生应用程序的要求。Spring是迄今为止最广泛使用的Java框架。它已经存在了多年,非常强大可靠。Spring背后的社区非常出色,愿意推动它前进并不断改进。技术和开发实践不断演进,Spring非常擅长跟上这些变化。因此,将Spring用于你的下一个云原生项目是一个绝佳的选择。

本节将介绍Spring生态系统的一些有趣特性。然后我们将开始创建一个Spring Boot应用程序。

Spring生态系统概览

Spring包含多个项目,涵盖了软件开发的许多不同方面:Web应用程序、安全性、数据访问、集成、批处理、配置、消息传递、大数据等等。Spring平台的优势在于其设计是模块化的,因此你可以仅使用和组合你所需的项目。无论你需要构建哪种类型的应用程序,Spring都有可能帮助你。

Spring Framework是Spring平台的核心,也是启动整个项目的项目。它支持依赖注入、事务管理、数据访问、消息传递、Web应用程序等功能。该框架为企业应用程序建立了"管道",让你可以专注于业务逻辑。

Spring Framework提供了一个执行上下文(称为Spring上下文或容器),它在整个应用程序生命周期中管理着Bean、属性和资源。我假设你已经熟悉了该框架的核心功能,所以我不会花太多时间在此介绍。特别是,你应该了解Spring上下文的作用,并且能够熟练地使用Spring Bean、基于注解的配置和依赖注入。我们将依赖于这些特性,因此你应该已经掌握了它们。

基于Spring Framework,Spring Boot使得快速构建独立的、生产就绪的应用程序成为可能。Spring Boot对于Spring和第三方库有一种倾向性的看法,并且它预置了一个明智的默认配置,让开发人员能够以最少的前期工作开始,并且仍然提供了完整的自定义可能性。

在本书中,你将有机会使用多个Spring项目来实现云原生应用程序的模式和最佳实践,包括Spring Boot、Spring Cloud、Spring Data、Spring Security、Spring Session和Spring Native。

注:如果你有兴趣了解更多关于Spring核心功能的内容,你可以在Manning的图书目录中找到几本相关书籍,包括Laurențiu Spilcă的《Spring Start Here》(Manning,2021)和Craig Walls的第六版《Spring in Action》(Manning,2022)。你也可以参考Mark Heckler的《Spring Boot: Up & Running》(O'Reilly,2021)。

构建一个Spring Boot应用程序

假设你被聘用来为Polarsophia构建一个Polar Bookshop应用程序。这个组织管理着一家专业的书店,希望在网上销售关于北极和北极地区的图书。他们正在考虑采用云原生的方法。

作为一个试点项目,你的老板指派你向同事们演示如何从实现到在云端部署。你被要求构建的Web应用程序是"Catalog Service",暂时只有一个职责:欢迎用户访问图书目录。如果这个试点项目成功并得到好评,它将成为构建实际的云原生应用程序的基础。

考虑到任务的目标,你可能决定将该应用程序实现为一个RESTful服务,它只有一个HTTP端点,负责返回欢迎信息。令人惊讶的是,你选择采用Spring作为构建该应用程序的主要技术栈(Catalog Service)。系统的架构如图2.2所示,接下来的部分中,你将尝试构建和部署该应用程序。

在图2.2中,你可以看到我在整本书中使用的用于表示架构图的符号表示法,遵循由Simon Brown创建的C4模型(c4model.com)。为了描述Polar Bookshop项目的架构,我依赖该模型中的三个抽象:

  • Person(人)--- 代表软件系统的一个人类用户。在我们的例子中,它是书店的顾客。

  • System(系统)--- 代表你将构建的整体应用程序,用于向用户提供价值。在我们的例子中,它是Polar Bookshop系统。

  • Container(容器)--- 代表一个服务,可以是应用程序或数据。请注意,这里的容器与Docker不同。在我们的例子中,它是Catalog Service。

对于这个任务,我们将使用Spring Framework和Spring Boot来完成以下工作:

  1. 声明实现应用程序所需的依赖项。
  2. 使用Spring Boot引导应用程序。
  3. 实现一个控制器以暴露一个HTTP端点,用于返回欢迎消息。
  4. 运行并测试应用程序。

本书中的所有示例都基于Java 17,这是撰写时的最新长期支持版本。在继续之前,请按附录A中A.1节的说明安装OpenJDK 17发行版。然后确保你的IDE支持Java、Gradle和Spring。我将使用IntelliJ IDEA,但你可以选择其他的,比如Visual Studio Code。最后,如果你还没有GitHub账号(github.com),请创建一个免费账号。你将使用它来存储你的代码并定义持续交付流水线。

初始化项目

在整本书中,我们将构建几个云原生应用程序。我建议你为每个应用程序定义一个Git存储库,并使用GitHub来存储它们。在下一章中,我将更多地讨论代码库的管理。现在,继续并创建一个名为catalog-service的Git存储库。

接下来,你可以从Spring Initializr(start.spring.io)生成项目,并将其存储在刚创建的catalog-service Git存储库中。Spring Initializr是一个方便的服务,你可以从浏览器中使用,也可以通过其REST API生成基于JVM的项目。它甚至集成到流行的IDE中,如IntelliJ IDEA和Visual Studio Code。Catalog Service的初始化参数如图2.3所示。

在初始化过程中,你可以提供关于你想要构建的应用程序的一些细节,如表2.1所示。

新生成的项目的结构如图2.4所示。在接下来的部分,我将引导你了解这个项目的结构。

在附带本书的代码仓库(github.com/ThomasVital...)中,你可以找到每一章的"begin"和"end"文件夹,这样你就可以始终从与我相同的设置开始,并查看最终结果。例如,你目前正在阅读第二章,因此你会在Chapter02/02-begin和Chapter02/02-end中找到相关的代码。

提示:在本章的"begin"文件夹中,你会找到一个curl命令,在终端窗口中运行该命令,可以下载一个包含所有所需代码的zip文件,以便你可以开始,而不必通过Spring Initializr网站进行手动项目生成。

探索构建配置

打开你刚刚初始化的项目,使用你喜欢的IDE,然后查看Catalog Service应用程序的Gradle构建配置,它在build.gradle文件中定义。在这里,你可以找到你提供给Spring Initializr的所有信息。

bash 复制代码
plugins {
  id 'org.springframework.boot' version '2.7.3'    ❶
  id 'io.spring.dependency-management'
➥ version '1.0.13.RELEASE'                        ❷
  id 'java'                                        ❸
}
 
group = 'com.polarbookshop'                        ❹
version = '0.0.1-SNAPSHOT'                         ❺
sourceCompatibility = '17'                         ❻
 
repositories {                                     ❼
  mavenCentral()
}
 
dependencies {                                     ❽
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
 
tasks.named('test') {
  useJUnitPlatform()                               ❾
}

❶ 在Gradle中提供对Spring Boot的支持,并声明要使用的版本

❷ 提供Spring的依赖管理功能

❸ 在Gradle中提供对Java的支持,设置任务来编译、构建和测试应用程序

❹ Catalog Service项目的组ID

❺ 应用程序的版本。默认情况下,为0.0.1-SNAPSHOT。

❻ 用于构建项目的Java版本

❼ 搜索依赖项的Artifact存储库

❽ 应用程序使用的依赖项

❾ 启用使用JUnit 5提供的JUnit Platform进行测试

该项目包含以下主要依赖项:

  1. Spring Web(org.springframework.boot:spring-boot-starter-web)提供了构建使用Spring MVC的Web应用程序所需的必要库,并默认包含Tomcat作为嵌入式服务器。
  2. Spring Boot Test(org.springframework.boot:spring-boot-starter-test)提供了多个用于测试应用程序的库和实用工具,包括Spring Test、JUnit、AssertJ和Mockito。它会自动包含在每个Spring Boot项目中。 注:Spring Boot提供了方便的Starter依赖项,将所有用于特定用例的库捆绑在一起,并确保它们之间的版本兼容。这个特性大大简化了构建配置。

项目的名称在一个名为settings.gradle的第二个文件中定义:

rootProject.name = 'catalog-service'

引导应用程序

在前面的部分中,你初始化了Catalog Service项目,并选择了JAR打包选项。任何打包为JAR的Java应用程序都必须有一个public static void main(String[] args)方法,在启动时执行该方法,Spring Boot也不例外。在Catalog Service中,一个CatalogServiceApplication类在初始化时被自动生成;这是main()方法所在的位置,也是运行Spring Boot应用程序的方式。

typescript 复制代码
package com.polarbookshop.catalogservice;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication                           ❶
public class CatalogServiceApplication {
  public static void main(String[] args) {       ❷
   SpringApplication.run(CatalogServiceApplication.class, args);
  }
}

❶ 定义一个Spring配置类并触发组件扫描和Spring Boot自动配置。

❷ 用于启动应用程序的方法。它在应用程序的引导阶段将当前类注册为要运行的类。

@SpringBootApplication注解是一个快捷方式,包含了三个不同的注解:

  1. @Configuration将该类标记为Bean定义的源头。
  2. @ComponentScan启用组件扫描,自动在Spring上下文中找到并注册Bean。
  3. @EnableAutoConfiguration启用Spring Boot提供的自动配置功能。

Spring Boot的自动配置是由多个条件触发的,例如类路径中是否存在特定的类、是否存在特定的Bean,或者某些属性的值。由于Catalog Service项目依赖于spring-boot-starter-web,Spring Boot会初始化一个嵌入式的Tomcat服务器实例,并应用所需的最小配置,以几乎零等待时间启动Web应用程序。

至此,应用程序的设置就完成了。现在让我们继续,从Catalog Service中暴露一个HTTP端点。

实现控制器

相关推荐
catoop23 分钟前
K8s 无头服务(Headless Service)
云原生·容器·kubernetes
小峰编程1 小时前
独一无二,万字详谈——Linux之文件管理
linux·运维·服务器·云原生·云计算·ai原生
快乐非自愿2 小时前
分布式系统架构2:服务发现
架构·服务发现
小马爱打代码2 小时前
云原生服务网格Istio实战
云原生
2401_854391082 小时前
SSM 架构中 JAVA 网络直播带货查询系统设计与 JSP 有效实现方法
java·开发语言·架构
264玫瑰资源库2 小时前
从零开始C++棋牌游戏开发之第二篇:初识 C++ 游戏开发的基本架构
开发语言·c++·架构
神一样的老师2 小时前
面向高精度网络的时间同步安全管理架构
网络·安全·架构
2401_857026232 小时前
基于 SSM 架构的 JAVA 网络直播带货查询系统设计与 JSP 实践成果
java·开发语言·架构
9527华安2 小时前
FPGA实现MIPI转FPD-Link车载同轴视频传输方案,基于IMX327+FPD953架构,提供工程源码和技术支持
fpga开发·架构·mipi·imx327·fpd-link·fpd953
DT辰白2 小时前
如何解决基于 Redis 的网关鉴权导致的 RESTful API 拦截问题?
后端·微服务·架构