《基于云原生的Spring实战:使用Spring Boot和Kubernetes》第三章:开始云原生开发

2023年 7月 28日 44.1k 0

本章内容包括:

  • 初始化一个云原生项目
  • 使用嵌入式服务器和Tomcat
  • 使用Spring MVC构建RESTful应用程序
  • 使用Spring Test测试RESTful应用程序
  • 使用GitHub Actions自动化构建和测试

云原生的范围如此广泛,初步开始可能会让人感到无所适从。在本书的第1部分,您已经对云原生应用程序及其支持过程有了理论上的介绍,并且在第一章中亲身体验了如何构建一个最简化的Spring Boot应用程序,并将其部署到Kubernetes作为一个容器。所有这些都将帮助您更好地理解整体的云原生框架,并正确地将我在后续章节中将要涵盖的主题放置在其中。

云计算为我们能够实现各种类型的应用程序提供了无限的可能性。在本章中,我将从其中最常见的类型之一开始:一个通过REST API在HTTP上暴露其功能的Web应用程序。我将引导您通过开发过程,您将在所有后续章节中都要遵循这个过程,涉及传统和云原生Web应用程序之间的重要差异,巩固Spring Boot和Spring MVC的一些必要方面,并强调重要的测试和生产考虑因素。我还将解释15因素方法论推荐的一些建议,包括依赖管理、并发性和API优先。

在这个过程中,您将实现在前一章中初始化的Catalog Service应用程序。它将负责管理极地书店系统中的图书目录。

注意:本章示例的源代码位于Chapter03/03-begin和Chapter03/03-end文件夹中,这些文件夹分别包含项目的初始状态和最终状态(github.com/ThomasVital…)。

启动云原生项目

开始一个新的开发项目总是令人兴奋的。15-Factor方法论包含了一些实用的准则来启动一个云原生应用程序。

  • 一个代码库,一个应用程序 - 云原生应用程序应该由一个单一的代码库组成,并在版本控制系统中进行跟踪管理。
  • 依赖管理 - 云原生应用程序应该使用一个显式管理依赖的工具,而不应依赖于其部署环境中的隐式依赖。
  • 在本节中,我将提供关于这两个原则的更多细节,并解释如何将它们应用到极地书店系统中的第一个云原生应用程序 - Catalog Service。

    一个代码库,一个应用

    云原生应用程序应该由一个单一的代码库组成,并在类似Git的版本控制系统中进行跟踪管理。每个代码库必须生成不可变的构建产物,称为构建(builds),可以部署到多个环境。图3.1显示了代码库、构建和部署之间的关系。

    image.png

    正如您将在下一章中看到的,任何与环境有关的配置都应该放在应用程序代码库之外。如果有一些代码需要被多个应用程序使用,您应该将其转化为一个独立的服务,或者将其转化为一个库,作为依赖项导入到项目中。对于后者,您应该仔细评估,以避免系统变成分布式单体。

    注意:考虑代码是如何组织成代码库和存储库可以帮助您更多地关注系统架构,并确定可能作为独立服务的部分。如果做得正确,代码库的组织可以有利于模块化和松耦合。

    根据15-Factor方法论,每个代码库都应该与一个应用程序进行映射,但并没有关于存储库的具体规定。您可以决定将每个代码库跟踪在单独的存储库中,也可以跟踪在同一个存储库中。在云原生业务中,这两种选项都有使用。在本书中,您将构建多个应用程序,我建议您将每个代码库跟踪在其自己的Git存储库中,因为这样可以提高可维护性和可部署性。

    在上一章中,您初始化了极地书店系统的第一个应用程序——目录服务(Catalog Service),并将其放置在了catalog-service Git存储库中。我建议您使用GitHub存储您的代码库,因为在后面的内容中,我们将使用GitHub Actions作为工作流引擎来定义支持持续交付的部署流水线。

    使用Gradle和Maven进行依赖管理

    你如何管理应用程序的依赖项对于它们的可靠性和可移植性非常重要。在Java生态系统中,最常用的依赖管理工具是Gradle和Maven。两者都提供了在清单中声明依赖项并从中央仓库下载它们的功能。列出项目所需的所有依赖项的原因在于确保您不依赖于从周围环境中泄漏的任何隐式库。

    注意:除了依赖管理外,Gradle和Maven还提供了用于构建、测试和配置Java项目的其他功能,这些功能对于应用程序开发至关重要。书中的所有示例都将使用Gradle,但您也可以自由选择使用Maven。

    尽管您已经有了一个依赖清单,但您仍然需要提供依赖管理器本身。Gradle和Maven都提供了从名为gradlew或mvnw的包装脚本中运行工具的功能,您可以将其包含在您的代码库中。例如,您可以运行./gradlew build,而不是运行Gradle命令gradle build(假设您已在计算机上安装了Gradle)。该脚本会调用项目中定义的特定版本的构建工具。如果构建工具尚不存在,则包装脚本将首先下载它,然后再运行命令。使用包装程序,您可以确保所有团队成员和自动化工具在构建项目时使用相同的Gradle或Maven版本。当您从Spring Initializr生成一个新项目时,您还将获得一个就绪可用的包装脚本,因此您无需下载或配置任何内容。

    注意:无论如何,通常您至少会有一个外部依赖项:运行时。在我们的例子中,这是Java运行时环境(JRE)。如果将应用程序打包为容器映像,则Java运行时将包含在映像本身中,从而使您对其具有更多控制。另一方面,最终的应用程序构件将依赖于运行所需的容器运行时。关于容器化过程,您将在第6章中了解更多信息。

    现在,让我们来看代码。Polar Bookshop系统有一个名为Catalog Service的应用程序,负责管理目录中可用的图书。在上一章中,我们初始化了该项目。系统的架构再次显示在图3.2中。

    image.png

    所有应用程序所需的依赖项都列在自动生成的build.gradle文件中(catalog-service/build.gradle)。

    dependencies {
      implementation 'org.springframework.boot:spring-boot-starter-web'
      testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    

    这些是主要的依赖项:

    • Spring Web (org.springframework.boot:spring-boot-starter-web) 提供了构建使用Spring MVC的Web应用程序所需的必要库,并包括Tomcat作为默认的嵌入式服务器。
    • Spring Boot Test (org.springframework.boot:spring-boot-starter-test) 提供了许多用于测试应用程序的库和实用工具,包括Spring Test、JUnit、AssertJ和Mockito。它会自动包含在每个Spring Boot项目中。 Spring Boot 的一个重要特性是它对依赖项管理的处理方式。像 spring-boot-starter-web 这样的Starter依赖项使您不必管理更多的依赖项,并验证导入的特定版本是否与彼此兼容。这是Spring Boot的另一个特性,可以让您简单高效地开始工作。

    在接下来的部分,您将了解Spring Boot中嵌入式服务器的工作原理以及如何配置它。

    与嵌入式服务器一起工作

    使用Spring Boot,您可以构建不同类型的应用程序(例如Web、事件驱动、无服务器、批处理和任务应用程序),其特点是各种用例和模式。在云原生环境中,它们都共享一些共同的特点:

    它们完全自包含,除了运行时之外没有外部依赖。 它们被打包为标准的可执行文件。 考虑一个Web应用程序。传统上,您会将其打包为WAR或EAR文件(用于打包Java应用程序的存档格式),并将其部署到Web服务器(如Tomcat)或应用服务器(如WildFly)。对服务器的外部依赖会限制应用程序本身的可移植性和演进,并增加维护成本。

    在本节中,您将了解如何使用Spring Boot、Spring MVC和嵌入式服务器解决云原生Web应用程序的这些问题,但类似的原则也适用于其他类型的应用程序。您将了解传统应用程序和云原生应用程序之间的区别,嵌入式服务器(如Tomcat)的工作原理以及如何进行配置。我还将详细介绍15-Factor方法论中关于服务器、端口绑定和并发性的一些建议:

    • 端口绑定 - 与依赖于执行环境中的外部服务器的传统应用程序不同,云原生应用程序是自包含的,并通过绑定到可以根据环境进行配置的端口来导出其服务。
    • 并发性 - 在JVM应用程序中,我们通过多线程处理并发性,这些线程作为线程池可用。当并发限制达到时,我们更倾向于水平而不是垂直扩展。我们不是向应用程序添加更多的计算资源,而是更喜欢部署更多的实例并在它们之间分配工作负载。

    遵循这些原则,我们将继续处理Catalog Service,以确保它是自包含的并且被打包为可执行的JAR文件。

    可执行的JAR文件和嵌入式服务器

    传统方法与云原生方法之间的一个区别在于如何打包和部署应用程序。传统上,我们使用应用程序服务器或独立的Web服务器。由于在生产环境中设置和维护这些服务器的成本较高,它们被用于部署多个应用程序,以EAR或WAR文件的形式打包以提高效率。这种场景中的应用程序之间存在耦合。如果其中任何一个应用程序想要在服务器级别进行更改,该更改必须与其他团队协调,并应用于所有应用程序,限制了敏捷性和应用程序演进。此外,应用程序的部署依赖于机器上是否可用服务器,这限制了应用程序在不同环境中的可移植性。

    当您进入云原生领域时,情况就不同了。云原生应用程序应该是自包含的,不依赖于执行环境中是否可用服务器。相反,必要的服务器功能已包含在应用程序本身中。Spring Boot提供了内置的服务器功能,帮助您消除外部依赖项,并使应用程序独立运行。Spring Boot附带预配置的Tomcat服务器,但也可以用Undertow、Jetty或Netty替换它。

    解决了服务器依赖问题后,我们需要相应地更改应用程序的打包方式。在JVM生态系统中,云原生应用程序被打包为JAR文件。由于它们是自包含的,它们可以作为独立的Java应用程序运行,除了JVM外没有外部依赖项。Spring Boot非常灵活,允许使用JAR和WAR两种类型的打包方式。然而,对于云原生应用程序,您应该使用自包含的JAR文件,也称为fat-JARs或uber-JARs,因为它们包含应用程序本身、依赖项和嵌入式服务器。图3.3比较了传统方式和云原生方式的打包和运行Web应用程序。

    image.png

    通常,用于云原生应用程序的嵌入式服务器包括一个Web服务器组件和一个执行上下文,使Java Web应用程序与Web服务器进行交互。例如,Tomcat包含一个Web服务器组件(Coyote)和一个基于Java Servlet API的执行上下文,通常称为Servlet容器(Catalina)。我将在这里将Web服务器和Servlet容器互换使用。另一方面,不建议将应用程序服务器用于云原生应用程序。

    在上一章中,当生成Catalog Service项目时,我们选择了JAR打包选项。然后,我们使用bootRun Gradle任务运行了应用程序。这是在开发过程中构建项目并将其作为独立应用程序运行的便捷方式。但是现在您已经更多了解了嵌入式服务器和JAR打包,我将向您展示另一种方式。

    首先,让我们将应用程序打包为JAR文件。打开终端窗口,导航到Catalog Service项目的根文件夹(catalog-service),然后运行以下命令。

    $ ./gradlew bootJar

    bootJar Gradle任务编译代码并将应用程序打包成JAR文件。默认情况下,JAR文件生成在build/libs文件夹中。您将获得一个名为catalog-service-0.0.1-SNAPSHOT.jar的可执行JAR文件。一旦获得JAR构件,您可以像运行任何标准Java应用程序一样运行它。

    $ java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar

    注意:另一个实用的Gradle任务是build,它将bootJar和test任务的操作合并在一起。

    由于项目包含spring-boot-starter-web依赖,Spring Boot会自动配置一个嵌入式的Tomcat服务器。从图3.4的日志中可以看到,其中一个最早执行的步骤是初始化一个嵌入在应用程序中的Tomcat服务器实例。

    image.png

    在下一节中,您将进一步了解Spring Boot中嵌入式服务器的工作原理。在继续之前,您可以使用Ctrl-C停止应用程序运行。

    理解基于请求的线程模型

    让我们考虑在Web应用程序中常用的请求/响应模式,以在HTTP上建立同步交互。客户端发送HTTP请求到服务器,服务器执行一些计算,然后用HTTP响应进行回复。

    在运行在Tomcat等Servlet容器中的Web应用程序中,请求的处理是基于线程模型的。对于每个请求,应用程序会专门分配一个线程来处理该特定请求;该线程在返回响应给客户端之前不会被用于其他任何事情。当请求处理涉及到诸如I/O等密集操作时,线程会被阻塞,直到操作完成为止。例如,如果需要数据库读取操作,线程将等待直到从数据库返回数据。因此,我们称这种类型的处理是同步和阻塞的。

    Tomcat初始化时会使用线程池来管理所有传入的HTTP请求。当所有线程都在使用时,新的请求将排队等待线程空闲。换句话说,Tomcat中的线程数量定义了支持同时处理的请求数的上限。在调试性能问题时,这点非常有用。如果连续达到线程并发限制,您可以调整线程池配置以接受更多的工作负载。对于传统应用程序,我们会为特定实例添加更多计算资源。对于云原生应用程序,我们依赖于水平扩展并部署更多副本。

    注意:在某些需要响应高要求的应用程序中,基于请求的线程模型可能不是理想的选择,因为由于阻塞而无法高效地使用可用的计算资源。在第8章中,我将介绍使用Spring WebFlux和Project Reactor的异步和非阻塞替代方案,采用反应式编程范式。

    Spring MVC是Spring Framework中包含的用于实现Web应用程序的库,可以实现完整的MVC或基于REST的功能。无论哪种方式,功能都基于像Tomcat这样的服务器,它提供了符合Java Servlet API的Servlet容器。图3.5显示了在Spring Web应用程序中如何工作的REST-based请求/响应交互。

    image.png

    DispatcherServlet组件提供了请求处理的中心入口点。当客户端发送一个特定URL模式的新HTTP请求时,DispatcherServlet会向HandlerMapping组件请求负责该端点的控制器,最终将实际的请求处理委托给指定的控制器。控制器处理请求,可能通过调用其他服务,然后将响应返回给DispatcherServlet,DispatcherServlet最后用HTTP响应回复客户端。

    请注意Tomcat服务器是嵌入在Spring Boot应用程序中的。Spring MVC依赖于Web服务器来完成其功能。对于实现Servlet API的任何Web服务器,都是如此。但由于我们明确地使用的是Tomcat,让我们继续探索一些配置它的选项。

    配置嵌入式Tomcat

    Tomcat是随Spring Boot Web应用程序预配置的默认服务器。有时默认配置可能足够使用,但对于生产中的应用程序,您可能需要自定义其行为以满足特定要求。

    注意,在传统的Spring应用程序中,您会在专用文件(例如server.xml和context.xml)中配置像Tomcat这样的服务器。通过Spring Boot,您可以通过属性或WebServerFactoryCustomizer bean的方式来配置嵌入式Web服务器。

    本节将向您展示如何通过属性来配置Tomcat。在下一章中,您将学习有关配置应用程序的更多内容。现在,只需了解您可以在项目的src/main/resources文件夹中的application.properties或application.yml文件中定义属性。您可以自由选择使用哪种格式:.properties文件依赖于键/值对,而.yml文件使用YAML格式。在本书中,我将使用YAML来定义属性。Spring Initializr默认生成一个空的application.properties文件,因此在继续之前,请将其扩展名从.properties更改为.yml。

    让我们继续为Catalog Service应用程序(catalog-service)配置嵌入式服务器。所有配置属性都将放在application.yml文件中。

    HTTP 端口

    默认情况下,嵌入式服务器在端口8080上监听。如果您在开发过程中运行多个Spring应用程序(这通常是云原生系统的情况),您会希望使用server.port属性为每个应用程序指定不同的端口号。

    server:
      port: 9001
    

    连接超时

    server.tomcat.connection-timeout属性定义了Tomcat在接受客户端的TCP连接和实际接收HTTP请求之间应等待的时间限制。它有助于防止拒绝服务(DoS)攻击,其中建立连接,Tomcat保留一个线程来处理请求,但请求从未到达。同样的超时时间也用于限制读取HTTP请求正文的时间(如果有的话)。

    默认值为20秒,这对于标准的云原生应用程序可能太长了。在云中高度分布的系统环境下,我们可能不希望等待超过几秒钟,以免因Tomcat实例长时间挂起而导致级联故障。设置为2秒可能更合适。您还可以使用server.tomcat.keep-alive-timeout属性来配置在等待新的HTTP请求时保持连接打开的时间。

    server:
      port: 9001
      tomcat: 
        connection-timeout: 2s 
        keep-alive-timeout: 15s
    

    线程池

    Tomcat拥有一个线程池来处理请求,遵循线程-每请求模型。可用线程的数量将决定可以同时处理多少个请求。您可以通过server.tomcat.threads.max属性配置最大的请求处理线程数。您还可以定义始终保持运行的最小线程数(server.tomcat.threads.min-spare),这也是启动时创建的线程数。

    确定线程池的最佳配置是复杂的,没有计算它的魔法公式。通常需要进行资源分析、监视和多次试验,以找到合适的配置。默认的线程池最多可增长到200个线程,并且始终有10个工作线程在运行,这些值在生产环境中是很好的起始值。在本地环境中,您可能希望降低这些值以优化资源消耗,因为它随线程数线性增加。

    server:
      port: 9001
      tomcat:
        connection-timeout: 2s
        keep-alive-timeout: 15s
        threads: 
          max: 50 
          min-spare: 5
    

    到目前为止,您已经了解了使用Spring Boot的云原生应用程序是如何打包为JAR文件,并依赖于嵌入式服务器来消除对执行环境的额外依赖,从而实现敏捷性。您学习了线程-每请求模型的工作原理,熟悉了Tomcat和Spring MVC的请求处理流程,并对Tomcat进行了配置。在下一节中,我们将继续讨论Catalog Service的业务逻辑以及使用Spring MVC实现REST API的内容。

    使用Spring MVC构建RESTful应用程序

    如果你正在构建云原生应用程序,很有可能你正在开发一个由多个服务组成的分布式系统,比如微服务,这些服务相互交互以完成产品的整体功能。你的应用程序可能会被组织内其他团队的服务使用,或者你可能会将其功能暴露给第三方。无论哪种情况,任何服务之间的交流都有一个至关重要的因素:API。

    15-Factor方法论倡导API优先模式。它鼓励您首先建立服务接口,然后再着手实现。API代表您的应用程序与其使用者之间的公共契约,因此最好首先定义它。

    假设您同意一个合同并首先定义了API。那么其他团队可以开始着手开发他们的解决方案,并根据您的API开发来实现与您的应用程序的集成。如果您不先开发API,将会出现瓶颈,其他团队将不得不等待直到您完成您的应用程序。事先讨论API还可以与相关方进行有意义的讨论,这可能导致您明确应用程序的范围,甚至定义用户故事来实现。

    在云中,任何应用程序都可以作为另一个应用程序的支持服务。采用API优先的理念将帮助您发展应用程序并使其适应未来的需求。

    本节将通过定义Catalog Service的REST API合同来引导您,REST API是云原生应用程序中最常用的服务接口模型。您将使用Spring MVC来实现REST API,并对其进行验证和测试。我还将概述一些关于未来需求的API演进的考虑事项,在高度分布式的云原生应用程序中这是一个常见问题。

    相关文章

    KubeSphere 部署向量数据库 Milvus 实战指南
    探索 Kubernetes 持久化存储之 Longhorn 初窥门径
    征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
    那些年在 Terraform 上吃到的糖和踩过的坑
    无需 Kubernetes 测试 Kubernetes 网络实现
    Kubernetes v1.31 中的移除和主要变更

    发布评论