Spring云原生实战指南:9 API 网关和断路器

Spring云原生实战指南:9 API 网关和断路器

首页休闲益智堆栈断路器3D更新时间:2024-04-21

本章涵盖

在上一章中,您学习了使用反应式范例构建弹性、可伸缩且经济高效的应用程序的几个方面。在本章中,Spring 反应式堆栈将成为为 Polar Bookshop 系统实现 API 网关的基础。API 网关是分布式体系结构(如微服务)中的常见模式,用于将内部 API 与客户端分离。在为系统建立此类入口点时,还可以使用它来处理横切问题,例如安全性、监视和复原能力。

本章将教您如何使用 Spring Cloud Gateway 构建边缘服务应用程序并实现 API 网关以及其中的一些横切问题。您将通过使用 Spring Cloud 断路器配置断路器、使用 Spring Data Redis Reactive 定义速率限制器以及使用重试和超时来提高系统的弹性,就像您在上一章中学到的那样。

接下来,我将讨论如何设计无状态应用程序。需要保存某些状态才能使应用程序有用 - 您已经使用了关系数据库。本章将教你如何使用Spring Session Data Redis(一种NoSQL内存数据存储)来存储Web会话状态。

最后,你将了解如何通过依赖 Kubernetes Ingress API 来管理对 Kubernetes 集群中运行的应用程序的外部访问。

图 9.1 显示了完成本章后 Polar 书店系统的外观。

图9.1 添加边缘服务和Redis后极地书店系统的架构

注意本章中示例的源代码位于第 09/09 章开始和第 09/09 章结束文件夹中,其中包含项目的初始和最终状态 (https://Github.com/ThomasVitale/cloud-native-spring-in-action)。

9.1 边缘服务器和 Spring 云网关

Spring Cloud Gateway是一个建立在Spring WebFlux和Project Actor之上的项目,旨在提供一个API网关和一个中心位置来处理安全性,弹性和监控等跨领域问题。它是为开发人员构建的,非常适合 Spring 架构和异构环境。

API 网关提供系统的入口点。在微服务等分布式系统中,这是一种将客户端与内部服务 API 的任何更改分离的便捷方法。您可以自由更改系统分解为服务及其 API 的方式,这取决于网关可以从更稳定、客户端友好的公共 API 转换为内部 API。

假设你正在从整体式架构迁移到微服务。在这种情况下,API 网关可以用作整体式扼*器,并且可以包装旧应用程序,直到它们迁移到新架构,从而使流程对客户端透明。对于不同的客户端类型(单页应用程序、移动应用程序、桌面应用程序、IoT 设备),API 网关为您提供了根据每个客户端需求为其提供精心设计的 API 的选项(也称为后端对前端模式)。有时,网关还可以实现 API 组合模式,允许您在将结果返回给客户端之前查询和联接来自不同服务的数据(例如,使用新的 Spring for GraphQL 项目)。

呼叫根据指定的路由规则从网关转发到下游服务,类似于反向代理。这样,客户端就不需要跟踪事务中涉及的不同服务,从而简化了客户端的逻辑并减少了必须进行的调用次数。

由于 API 网关是系统的入口点,因此它也可以是处理安全性、监视和复原能力等横切问题的绝佳场所。边缘服务器是系统边缘的应用程序,用于实现 API 网关和横切关注点等方面。您可以配置断路器,以防止在下游调用服务时出现级联故障。您可以为对内部服务的所有调用定义重试和超时。您可以控制入口流量并实施配额策略,以根据某些条件(例如用户的成员资格级别:基本、高级、专业)限制系统的使用。您还可以在边缘实现身份验证和授权,并将令牌传递给下游服务(如第 11 章和第 12 章所示)。

但是,请务必记住,边缘服务器会增加系统的复杂性。它是在生产中构建、部署和管理的另一个组件。它还向系统添加了新的网络跃点,因此响应时间将增加。这通常是一个微不足道的成本,但你应该记住这一点。由于边缘服务器是系统的入口点,因此它有成为单点故障的风险。作为基本的缓解策略,应按照我们在第 4 章中为配置服务器讨论的相同方法部署边缘服务器的至少两个副本。

Spring 云网关极大地简化了边缘服务的构建,专注于简单性和生产力。此外,由于它基于反应式堆栈,因此可以有效地扩展以处理系统边缘自然发生的高工作负载。

以下部分将教您如何使用 Spring 云网关设置边缘服务器。您将了解路由、谓词和筛选器,它们是网关的构建基块。您将把上一章中学到的重试和超时模式应用于网关和下游服务之间的交互。

注意如果您没有遵循前面章节中实现的示例,您可以参考本书随附的存储库并使用 Chapter09/09-begin 中的项目作为起点 (https://github.com/ThomasVitale/cloud-native-spring-in-action)。

9.1.1 使用 Spring 云网关引导边缘服务器

Polar 书店系统需要一个边缘服务器来将流量路由到内部 API,并解决几个横切问题。您可以从 Spring Initializr (https://start.spring.io) 初始化我们新的边缘服务项目,将结果存储在新的边缘服务 Git 存储库中,并将其推送到 GitHub。初始化的参数如图 9.2 所示。

图 9.2 初始化边缘服务项目的参数说明

提示在本章的 begin 文件夹中,您将找到一个可以在终端窗口中运行的 curl 命令。它下载一个 zip 文件,其中包含入门所需的所有代码,而无需在 Spring Initializr 网站上手动生成。

自动生成的 build.gradle 文件的依赖项部分如下所示:

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

这些是主要的依赖项:

Spring Cloud Gateway的核心是一个Spring Boot应用程序。它提供了我们在前几章中使用的所有方便的功能,例如自动配置、嵌入式服务器、测试实用程序、外部化配置等。它也建立在 Spring 响应式堆栈之上,因此您可以使用在上一章中学到的有关 Spring WebFlux 和 Reactor 的工具和模式。让我们从配置嵌入式 netty 服务器开始。

首先,将 Spring Initializr (edge-service/src/main/resources) 生成的 application.properties 文件重命名为 application.yml。然后打开文件并配置 Netty 服务器,如上一章中所述。

清单 9.1 配置 Netty 服务器和正常关闭

server: port: 9000 ❶ netty: connection-timeout: 2s ❷ idle-timeout: 15s ❸ shutdown: graceful ❹ spring: application: name: edge-service lifecycle: timeout-per-shutdown-phase: 15s ❺

❶ 服务器将接受连接的端口

❷ 等待与服务器建立TCP连接的时间

❸ 如果没有传输数据,在关闭 TCP 连接之前等待多长时间

❹ 启用正常关机

❺ 定义 15 秒宽限期

该应用程序已设置,因此您可以继续并开始探索Spring Cloud Gateway的功能。

9.1.2 定义路由和谓词

Spring Cloud Gateway提供三个主要构建块:

假设客户端向 Spring Cloud 网关发送请求。如果请求通过其谓词与路由匹配,则网关处理程序映射会将请求发送到网关 WebHandler,网关 Web 处理程序又将通过筛选器链运行请求。

有两个筛选器链。一条链包含在将请求发送到下游服务之前要运行的筛选器。另一条链在向下游发送请求后和转发响应之前运行。您将在下一节中了解不同类型的筛选器。图 9.3 显示了路由在 Spring 云网关中的工作方式。

图 9.3 请求与谓词匹配、筛选,最后转发到下游服务,下游服务回复响应,该响应在返回到客户端之前经过另一组筛选器。

在 Polar 书店系统中,我们构建了两个应用程序,其中包含可从外部世界访问的 API(公共 API):目录服务和订购服务。我们可以使用边缘服务将它们隐藏在 API 网关后面。首先,我们需要定义路由。

最小路由必须配置唯一 ID、应转发请求的 URI 以及至少一个谓词。打开边缘服务项目的 application.yml 文件,并配置目录服务和订购服务的两个路由。

清单 9.2 配置下游服务的路由

spring: cloud: gateway: routes: ❶ - id: catalog-route ❷ uri: ${CATALOG_SERVICE_URL:http://localhost:9001}/books predicates: - Path=/books/** ❸ - id: order-route uri: ➥${ORDER_SERVICE_URL:http://localhost:9002}/orders ❹ predicates: - Path=/orders/**

❶ 路由定义列表

❷ 路线编号

❸ 谓词是匹配的路径

❹ URI 值来自环境变量,或者来自默认值。

目录服务和订单服务的路由都基于路径谓词进行匹配。路径以 /books 开头的所有传入请求都将转发到目录服务。如果路径以 /orders 开头,则订单服务将收到请求。URI 是使用环境变量(CATALOG_SERVICE_URL 和 ORDER_SERVICE_URL)中的值计算的。如果未定义它们,则将使用在第一个冒号 (:) 符号之后写入的默认值。与上一章中基于自定义属性定义 URL 的方式相比,这是一种替代方法;我想向您展示这两种选择。

该项目内置了许多不同的谓词,您可以在路由配置中使用这些谓词来匹配 HTTP 请求的任何方面,包括 Cookie、标头、主机、方法、路径、查询和 RemoteAddr。您还可以将它们组合在一起以形成 AND 条件。在前面的示例中,我们使用了 Path 谓词。有关Spring Cloud Gateway:https://spring.io/projects/spring-cloud-gateway 中可用的谓词的广泛列表,请参阅官方文档。

使用 Java/Kotlin DSL 定义路由

Spring Cloud Gateway是一个非常灵活的项目,可让您以最适合您需求的方式配置路由。在这里,您已经在属性文件(application.yml 或 application.properties)中配置了路由,但也有一个 DSL 可用于在 Java 或 Kotlin 中以编程方式配置路由。该项目的未来版本还将实现一项功能,即使用 Spring Data 从数据源获取路由配置。

如何使用它取决于您。将路由放在配置属性中,您可以根据环境轻松自定义路由,并在运行时更新它们,而无需重新生成和重新部署应用程序。例如,使用Spring Cloud Config Server时,您将获得这些好处。另一方面,Java 和 Kotlin 的 DSL 允许您定义更复杂的路由。配置属性仅允许您使用 AND 逻辑运算符组合不同的谓词。DSL 还允许您使用其他逻辑运算符,如 ORNOT

让我们验证它是否按预期工作。我们将使用 Docker 来运行下游服务和 PostgreSQL,而我们将在 JVM 上本地运行边缘服务,以使其更高效,因为我们正在积极实现应用程序。

首先,我们需要启动并运行目录服务和订单服务。从每个项目的根文件夹中,运行 ./gradlew bootBuildImage 将它们打包为容器映像。然后通过 Docker Compose 启动它们。打开终端窗口,导航到 docker-compose.yml 文件所在的文件夹(polar-deployment/docker),然后运行以下命令:

$ docker-compose up -d catalog-service order-service

由于这两个应用程序都依赖于PostgreSQL,因此Docker Compose也将运行PostgreSQL容器。

当下游服务全部启动并运行时,就可以启动边缘服务了。在终端窗口中,导航到工程的根文件夹(Edge 服务),然后运行以下命令:

$ ./gradlew bootRun

边缘服务应用程序将开始接受端口 9000 上的请求。对于最终测试,请尝试对账簿和订单执行操作,但这次是通过 API 网关(即,使用端口 9000,而不是目录服务和订单服务正在侦听的各个端口)。它们应返回 200 OK 响应:

$ http :9000/books $ http :9000/orders

结果与直接调用目录服务和订单服务相同,但这次只需要知道一个主机名和端口。测试完应用程序后,使用 Ctrl-C 停止其执行。然后使用 Docker Compose 终止所有容器:

$ docker-compose down

在后台,边缘服务使用 Netty 的 HTTP 客户端将请求转发到下游服务。正如上一章中广泛讨论的那样,每当应用程序调用外部服务时,都必须配置超时,以使其能够灵活应对进程间通信故障。Spring Cloud Gateway提供专用属性来配置HTTP客户端超时。

再次打开 Edge 服务 application.yml 文件,并定义连接超时(与下游服务建立连接的时间限制)和响应超时(接收响应的时间限制)的值。

示例 9.3 配置网关 HTTP 客户端的超时

spring: cloud: gateway: httpclient: ❶ connect-timeout: 2000 ❷ response-timeout: 5s ❸

❶ HTTP 客户端的配置属性

❷ 建立连接的时间限制(毫秒)

❸ 收到回复的时间限制(持续时间)

默认情况下,Spring Cloud Gateway 使用的 Netty HTTP 客户端配置了弹性连接池,以随着工作负载的增加动态增加并发连接数。根据系统同时接收的请求数,您可能希望切换到固定连接池,以便更好地控制连接数。您可以通过 application.yml 文件中的 spring.cloud.gateway.httpclient.pool 属性组在 Spring Cloud Gateway 中配置 Netty 连接池。

示例 9.4 为网关 HTTP 客户端配置连接池

spring: cloud: gateway: httpclient: connect-timeout: 5000 response-timeout: 5s pool: type: elastic ❶ max-idle-time: 15s ❷ max-life-time: 60s ❸

❶ 连接池类型(弹性、固定或禁用)

❷ 空闲时间,之后通信通道将关闭

❸ 通信通道关闭的时间

您可以参考官方 Reactor Netty 文档,了解有关连接池工作原理、可用配置以及基于特定场景使用哪些值的提示的更多详细信息 (https://projectreactor.io/docs)。

在下一节中,我们将开始实现一些比转发请求更有趣的东西——我们将看看 Spring Cloud Gateway 过滤器的强大功能。

9.1.3 通过过滤器处理请求和响应

路由和谓词本身使应用程序充当代理,但过滤器使Spring Cloud Gateway真正强大。

筛选器可以在将传入请求转发到下游应用程序(预筛选器)之前运行。它们可用于:

其他筛选器可以在从下游应用程序接收到传出响应之后以及在将其发送回客户端之前应用于传出响应(后筛选器)。它们可用于:

Spring Cloud Gateway 捆绑了许多过滤器,您可以使用这些过滤器执行不同的操作,包括向请求添加标头、配置断路器、保存 Web 会话、失败时重试请求或激活速率限制器。

在上一章中,您学习了如何使用重试模式来提高应用程序的弹性。现在,您将了解如何将其应用为通过网关中定义的路由的所有 GET 请求的默认筛选器。

使用重试筛选器

您可以在位于 src/main/ resources 下的 application.yml 文件中定义默认过滤器。Spring Cloud Gateway提供的筛选器之一是重试筛选器。配置类似于我们在第 8 章中所做的。

让我们为所有 GET 请求定义最多三次重试尝试,只要错误在 5xx 范围内 (SERVER_ERROR)。当错误在 4xx 范围内时,我们不希望重试请求。例如,如果结果是 404 响应,则重试请求是没有意义的。我们还可以列出应该尝试重试的异常,例如 IOException 和 TimeoutException。

到目前为止,您知道不应该一个接一个地重试请求。应改用退避策略。默认情况下,延迟是使用公式 firstBackoff *(因子 ^ n)计算的。如果将 basedOnPreviousValue 参数设置为 true,则公式将为 prevBackoff * 因子。

清单 9.5 对所有路由应用重试过滤器

spring: cloud: gateway: default-filters: ❶ - name: Retry ❷ args: retries: 3 ❸ methods: GET ❹ series: SERVER_ERROR ❺ exceptions: java.io.IOException, ➥ java.util.concurrent.TimeoutException ❻ backoff: ❼ firstBackoff: 50ms maxBackOff: 500ms factor: 2 basedOnPreviousValue: false

❶ 默认过滤器列表

❷ 过滤器的名称

❸ 最多 3 次重试

❹ 仅重试 GET 请求

❺ 仅在 5XX 错误时重试

❻ 仅在引发给定异常时重试

❼ 重试,延迟计算为“firstBackoff *(因子 ^ n)”

当下游服务暂时不可用时,重试模式非常有用。但是,如果它停留超过几个瞬间怎么办?此时,我们可以停止向它转发请求,直到我们确定它回来了。继续发送请求对调用方或被叫方没有好处。在这种情况下,断路器模式会派上用场。这是下一节的主题。

9.2 弹簧云断路器的容错和弹性4J

如您所知,弹性是云原生应用程序的关键属性。实现复原的原则之一是阻止故障级联并影响其他组件。考虑一个分布式系统,其中应用程序 X 依赖于应用程序 Y。如果应用程序 Y 失败,应用程序 X 也会失败吗?断路器可以阻止一个组件中的故障传播到其他组件,从而保护系统的其余部分。这是通过暂时停止与故障组件的通信来实现的,直到它恢复。这种模式来自电气系统,其中电路被物理打开以断开电气连接,并避免在系统的一部分因电流过载而发生故障时破坏整个房屋。

在分布式系统中,您可以在组件之间的集成点建立断路器。考虑边缘服务和目录服务。在典型情况下,电路是闭合的,这意味着两个服务可以通过网络进行交互。对于目录服务返回的每个服务器错误响应,边缘服务中的断路器将记录故障。当故障次数超过某个阈值时,断路器跳闸,电路转换为断开

当线路打开时,不允许边缘服务和目录服务之间的通信。应转发到目录服务的任何请求都将立即失败。在此状态下,要么向客户端返回错误,要么执行回退逻辑。在允许系统恢复的适当时间后,断路器将转换为半打开状态,从而允许对目录服务的下一次调用通过。这是一个探索阶段,用于检查在联系下游服务时是否仍然存在问题。如果调用成功,断路器将复位并转换为闭合。否则,它将恢复开放状态。图 9.4 显示了断路器如何更改状态。

图 9.4 断路器通过阻止上游和下游服务之间的任何通信,确保下游服务超过允许的最大故障数时的容错能力。该逻辑基于三种状态:封闭、打开和半打开。

与重试不同,当断路器跳闸时,不再允许调用下游服务。与重试一样,断路器的行为取决于阈值和超时,它允许您定义要调用的回退方法。弹性的目标是使系统对用户可用,即使面对故障也是如此。在最坏的情况下,例如当断路器跳闸时,应保证正常降级。您可以对回退方法采用不同的策略。例如,在发生 GET 请求时,您可能决定从缓存中返回默认值或最后一个可用值。

Spring Cloud Circuit Breaker 项目提供了一个抽象,用于在 Spring 应用程序中定义断路器。您可以在基于 Resilience4J (https://resilience4j.readme.io) 的响应式和非响应式实现之间进行选择。Netflix Hystrix是微服务架构的热门选择,但它早在2018年就进入了维护模式。之后,Resilience4J成为首选,因为它提供了与Hystrix相同的功能。

Spring Cloud Gateway 与 Spring Cloud Circuit Breaker 原生集成,为您提供 Breaker 网关过滤器,可用于保护与所有下游服务的交互。在以下部分中,你将为到目录服务和从边缘服务订购服务的路由配置断路器。

9.2.1 弹簧云断路器介绍断路器

要在Spring Cloud Gateway中使用Spring Cloud Breaker,您需要将依赖项添加到要使用的特定实现中。在本例中,我们将使用 Resilience4J 响应式版本。继续在边缘服务项目(边缘服务)的 build.gradle 文件中添加新依赖项。请记住在新添加后刷新或重新导入 Gradle 依赖项。

示例 9.6 为春云断路器添加依赖关系

dependencies { ... implementation 'org.springframework.cloud: ➥ spring-cloud-starter-circuitbreaker-reactor-resilience4j' }

Spring Cloud Gateway 中的 Breaker 筛选器依赖于 Spring Cloud Circuit Breaker 来包装路由。与重试筛选器一样,您可以选择将其应用于特定路由或将其定义为默认筛选器。让我们选择第一个选项。还可以指定可选的回退 URI,以便在线路处于打开状态时处理请求。在此示例 (application.yml) 中,两个路由都将配置一个断路器筛选器,但只有目录路由将具有回退 Uri 值,以便我可以向您展示这两种方案。

清单 9.7 为网关路由配置断路器

spring: cloud: gateway: routes: - id: catalog-route uri: ${CATALOG_SERVICE_URL:http://localhost:9001}/books predicates: - Path=/books/** filters: - name: CircuitBreaker ❶ args: name: catalogCircuitBreaker ❷ fallbackUri: forward:/catalog-fallback ❸ - id: order-route uri: ${ORDER_SERVICE_URL:http://localhost:9002}/orders predicates: - Path=/orders/** filters: - name: CircuitBreaker ❹ args: name: orderCircuitBreaker

❶ 过滤器的名称

❷ 断路器名称

❸ 在电路打开时将请求转发到此 URI

❹ 没有为此断路器定义回退。

下一步是配置断路器。

9.2.2 使用弹性4J配置断路器

定义要应用断路器筛选器的路由后,需要配置断路器本身。与Spring Boot中一样,您有两个主要选择。您可以通过 Resilience4J 提供的属性或通过定制器 Bean 配置断路器。由于我们使用的是 Resilience4J 的反应式版本,因此特定的配置 bean 的类型为 Customizer<ReactiveResilience4JCircuitBreakerFactory>。

无论哪种方式,您都可以选择为在 application.yml 文件中使用的每个断路器定义特定配置(在本例中为 catalogBreaker 和 orderBreaker),或者声明一些将应用于所有这些断路器的默认值。

对于当前示例,我们可以定义断路器以考虑 20 次调用的窗口(滑动窗口大小)。每个新调用都会使窗口移动,丢弃最早的已注册呼叫。当窗口中至少 50% 的调用产生错误(failureRateThreshold)时,断路器将跳闸,电路将进入打开状态。15 秒后(waitDurationInOpenState),电路将被允许转换为半打开状态,在该状态下允许 5 个调用(allowtedNumberOfCallsInHalfOpenState)。如果其中至少50%导致错误,电路将回到打开状态。否则,断路器将跳闸进入闭合状态。

转到代码。在边缘服务项目(边缘服务)中,在 application.yml 文件的末尾,为所有 Resilience4J 断路器定义默认配置。

清单 9.8 配置断路器和限时器

resilience4j: circuitbreaker: configs: default: ❶ slidingWindowSize: 20 ❷ permittedNumberOfCallsInHalfOpenState: 5 ❸ failureRateThreshold: 50 ❹ waitDurationInOpenState: 15000 ❺ timelimiter: configs: default: ❻ timeoutDuration: 5s ❼

❶ 所有断路器的默认配置 bean

❷ 电路闭合时用于记录呼叫结果的滑动窗口的大小

❸ 电路半开时允许的呼叫数

❹ 当故障率高于阈值时,电路将断开。

❺ 从打开变为半打开之前的等待时间(毫秒)

❻ 所有时间限制器的默认配置 Bean

❼ 配置超时(秒)

我们配置断路器和时间限制器,这是使用 Spring Cloud 断路器的 Resilience4J 实现时所需的组件。通过 Resilience4J 配置的超时将优先于我们在上一节中为 Netty HTTP 客户端 (spring.cloud.gateway.httpclient.response-timeout) 定义的响应超时。

当断路器切换到打开状态时,我们至少希望优雅地降低服务级别,并使用户体验尽可能愉快。我将在下一节中向您展示如何执行此操作。

9.2.3 使用 Spring WebFlux 定义回退 REST API

将断路器筛选器添加到目录路由时,我们为 fallbackUri 属性定义了一个值,以便在线路处于打开状态时将请求转发到 /catalog-fallback 终结点。由于重试筛选器也应用于该路由,因此即使给定请求的所有重试尝试都失败,也会调用回退终结点。是时候定义该终结点了。

正如我在前面的章节中提到的,Spring 支持使用 @RestController 类或路由器函数定义 REST 端点。让我们使用声明回退终结点的功能方式。

在边缘服务项目的新 com.polarbookshop.edgeservice.web 包中,创建一个新的 WebEndpoint 类。Spring WebFlux 中的功能端点被定义为 RouterFunction<ServerResponse> bean 中的路由,使用 RouterFunctions 提供的流畅 API。对于每个路由,需要定义终结点 URL、方法和处理程序。

清单 9.9 目录服务关闭时的回退端点

package com.polarbookshop.edgeservice.web; import reactor.core.publisher.Mono; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; @Configuration public class WebEndpoints { @Bean ❶ public RouterFunction<ServerResponse> routerFunction() { return RouterFunctions.route() ❷ .GET("/catalog-fallback", request -> ❸ ServerResponse.ok().body(Mono.just(""), String.class)) .POST("/catalog-fallback", request -> ❹ ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).build()) .build(); ❺ } }

❶ 功能性 REST 端点在 Bean 中定义。

❷ 提供流畅的 API 来构建路由

❸ 用于处理 GET 端点的回退响应

❹ 用于处理 POST 端点的回退响应

❺ 构建功能端点

为简单起见,GET 请求的回退返回空字符串,而 POST 请求的回退返回 HTTP 503 错误。在实际方案中,您可能希望根据上下文采用不同的回退策略,包括引发要从客户端处理的自定义异常或返回原始请求的缓存中保存的最后一个值。

到目前为止,我们已经使用了重试、超时、断路器和故障转移(回退)。在下一节中,我将扩展如何结合使用所有这些复原模式。

9.2.4 组合断路器、重试和时间限制器

组合多个复原模式时,应用它们的顺序至关重要。Spring Cloud Gateway负责首先应用TimeLimiter(或HTTP客户端上的超时),然后应用Breaker过滤器,最后重试。图 9.5 显示了这些模式如何协同工作以提高应用程序的弹性。

图 9.5 实现多个弹性模式时,它们将按特定顺序应用。

您可以使用 Apache 基准测试 (https://httpd.apache.org/docs/2.4/programs/ab.html) 等工具验证将这些模式应用于边缘服务的结果。如果您使用的是 macOS 或 Linux,则可能已安装此工具。否则,您可以按照官方网站上的说明进行安装。

确保目录服务和订单服务均未运行,以便可以在故障情况下测试断路器。然后为 Resilience4J 启用调试日志记录,以便跟踪断路器的状态转换。在边缘服务项目中 application.yml 文件的末尾,添加以下配置。

示例 9.10 为 Resilience4J 启用调试日志记录

logging: level: io.github.resilience4j: DEBUG

接下来,构建并运行边缘服务 (./gradlew bootRun)。由于没有下游服务正在运行(如果正在运行,则应停止它们),因此从 Edge 服务发送给它们的所有请求都将导致错误。让我们看看如果我们运行 21 个连续的 POST 请求 (-n 21 -c 1 -m POST) 到 /orders 端点会发生什么。请记住,POST 请求没有重试配置,订单路由没有回退,因此结果只会受到超时和断路器的影响:

$ ab -n 21 -c 1 -m POST http://localhost:9000/orders

从 ab 输出中,您可以看到所有请求都返回了错误:

Complete requests: 21 Non-2xx responses: 21

断路器配置为在 50 大小的时间窗口中至少有 20% 的呼叫失败时跳闸到打开状态。由于您刚刚启动应用程序,因此线路将在 20 个请求后转换为打开状态。在应用程序日志中,可以分析如何处理请求。所有请求都失败了,因此断路器为每个请求注册一个 ERROR 事件:

Event ERROR published: CircuitBreaker 'orderCircuitBreaker' recorded an error.

在第 20 个请求中,将记录FAILURE_RATE_EXCEEDED事件,因为它超出了失败阈值。这将导致一个STATE_TRANSITION事件,该事件将打开电路:

Event ERROR published: CircuitBreaker 'orderCircuitBreaker' recorded an error. Event FAILURE_RATE_EXCEEDED published: CircuitBreaker 'orderCircuitBreaker' exceeded failure rate threshold. Event STATE_TRANSITION published: CircuitBreaker 'orderCircuitBreaker' changed state from CLOSED to OPEN

第 21 个请求甚至不会尝试联系订单服务:电路是开放的,因此无法通过。注册一个NOT_PERMITTED事件以指示请求失败的原因:

Event NOT_PERMITTED published: CircuitBreaker 'orderCircuitBreaker' recorded a call which was not permitted.

注意监控生产中断路器的状态是一项关键任务。在第 13 章中,我将向您展示如何将这些信息导出为 Prometheus 指标,您可以在 Grafana 仪表板中可视化这些信息,而不是检查日志。同时,如需更直观的解释,请随时观看我在 Spring I/O, 2022 (http://mng.bz/z55A) 上关于断路器的“Spring 云网关:弹性、安全性和可观察性”会议。

现在,让我们看看当我们调用已配置重试和回退的 GET 终结点时会发生什么。在继续之前,请重新运行应用程序,以便您可以从明确的断路器状态 (./gradlew bootRun) 开始。然后运行以下命令:

$ ab -n 21 -c 1 -m GET http://localhost:9000/books

如果您检查应用程序日志,您将看到断路器的行为方式与以前完全相同:20 个允许的请求(闭路),然后是一个不允许的请求(开路)。但是,上一个命令的结果显示 21 个请求已完成且没有错误:

Complete requests: 21 Failed requests: 0

这一次,所有请求都已转发到回退终结点,因此客户端不会遇到任何错误。

我们将重试筛选器配置为在发生 IOException 或超时异常时触发。在这种情况下,由于下游服务未运行,因此引发的异常类型为 ConnectException,因此可以方便地不重试请求,这使我能够向您展示断路器和回退的组合行为而无需重试。

到目前为止,我们已经研究了使边缘服务与下游应用程序之间的交互更具弹性的模式。系统的入口点呢?下一节将介绍速率限制器,这些速率限制器将控制通过边缘服务应用程序进入系统的请求流。在继续之前,请使用 Ctrl-C 停止应用程序的执行。

9.3 Spring Cloud Gateway 和 Redis 的请求速率限制

速率限制是一种模式,用于控制发送到应用程序或从应用程序接收的流量速率,有助于使系统更具弹性和健壮性。在 HTTP 交互的上下文中,可以应用此模式分别使用客户端和服务器端速率限制器来控制传出或传入网络流量。

客户端速率限制器用于限制给定时间段内发送到下游服务的请求数。当第三方组织(如云提供商)管理和提供下游服务时,这是一种有用的模式。你需要避免因发送的请求多于订阅允许的请求而产生额外费用。对于按使用付费服务,这有助于防止意外费用。

如果下游服务属于您的系统,则可以使用速率限制器来避免自己导致 DoS 问题。但是,在这种情况下,隔板模式(或并发请求限制器)将更适合,它设置允许的并发请求数的限制,并对被阻止的请求进行排队。更好的是自适应舱壁,其并发限制由算法动态更新,以更好地适应云基础架构的弹性。

服务器端速率限制器用于限制上游服务(或客户端)在给定时间段内收到的请求数。在 API 网关中实现此模式时非常方便,可以保护整个系统免受过载或 DoS 攻击。当用户数量增加时,系统应以弹性方式扩展,确保所有用户都能获得可接受的服务质量。预计用户流量会突然增加,通常最初通过向基础结构添加更多资源或更多应用程序实例来解决。但是,随着时间的推移,它们可能会成为一个问题,甚至导致服务中断。服务器端速率限制器有助于实现这一点。

当用户超过特定时间范围内允许的请求数时,所有额外的请求都将被拒绝,并显示“HTTP 429 - 请求过多”状态。限制根据给定的策略应用。例如,可以限制每个会话、每个 IP 地址、每个用户或每个租户的请求。总体目标是在逆境中使系统对所有用户可用。这就是复原力的定义。此模式对于根据用户的订阅层向用户提供服务也很方便。例如,可以为基本用户、高级用户和企业用户定义不同的速率限制。

Resilience4J 支持反应式和非反应式应用的客户端速率限制器和隔板模式。Spring Cloud Gateway 支持服务器端限速器模式,本节将向您展示如何使用 Spring Cloud Gateway 和 Spring Data Redis Reactive 将其用于边缘服务。让我们从设置 Redis 容器开始。

9.3.1 将 Redis 作为容器运行

假设您希望限制对 API 的访问,以便每个用户每秒只能执行 10 个请求。实现这样的要求需要一种存储机制来跟踪每个用户每秒执行的请求数。达到限制时,应拒绝以下请求。当秒结束时,每个用户可以在下一秒内再执行 10 个请求。速率限制算法使用的数据很小且是临时的,因此您可能会考虑将其保存在应用程序本身的内存中。

但是,这将使应用程序有状态并导致错误,因为每个应用程序实例都会根据部分数据集限制请求。这意味着让用户每个实例每秒执行 10 个请求,而不是总体执行,因为每个实例只会跟踪自己的传入请求。解决方案是使用专用数据服务来存储速率限制状态,并使其可用于所有应用程序副本。输入雷迪斯。

Redis (https://redis.com) 是一种内存存储,通常用作缓存、消息代理或数据库。在边缘服务中,我们将它用作支持Spring Cloud Gateway提供的请求速率限制器实现的数据服务。Spring Data Redis Reactive 项目提供了 Spring Boot 应用程序和 Redis 之间的集成。

让我们首先定义一个 Redis 容器。打开您在极地部署存储库中创建的 docker-compose.yml 文件。(如果你还没有按照这些例子进行操作,你可以使用本书随附源代码中的 Chapter09/09-begin/polar-deployment/docker/docker-compose.yml 作为起点。然后使用 Redis 官方镜像添加新的服务定义,并通过端口 6379 公开该定义。

清单 9.11 定义 Redis 容器

version: "3.8" services: ... polar-redis: image: "redis:7.0" ❶ container_name: "polar-redis" ports: - 6379:6379 ❷

❶ 使用 Redis 7.0

❷ 通过端口 6379 公开 Redis

接下来,打开终端窗口,导航到 docker-compose.yml 文件所在的文件夹,然后运行以下命令以启动 Redis 容器:

$ docker-compose up -d polar-redis

在下一节中,你将配置 Redis 与边缘服务的集成。

9.3.2 将 Spring 与 Redis 集成

Spring 数据项目具有支持多个数据库选项的模块。在前面的章节中,我们使用Spring Data JDBC和Spring Data R2DBC来使用关系数据库。现在我们将使用 Spring Data Redis,它为这种内存中的非关系数据存储提供支持。支持命令式和反应式应用程序。

首先,我们需要在边缘服务项目(边缘服务)的 build.gradle 文件中添加一个对 Spring Data Redis Reactive 的新依赖。请记住在新添加后刷新或重新导入 Gradle 依赖项。

示例 9.12 为 Spring 数据添加依赖关系 Redis 反应式

dependencies { ... implementation ➥ 'org.springframework.boot:spring-boot-starter-data-redis-reactive' }

然后,在 application.yml 文件中,通过 Spring Boot 提供的属性配置 Redis 集成。除了 spring.redis.host 和 spring.redis.port 用于定义到达 Redis 的位置之外,您还可以分别使用 spring.redis.connect-timeout 和 spring.redis.timeout 指定连接和读取超时。

清单 9.13 配置 Redis 集成

spring: redis: connect-timeout: 2s ❶ host: localhost ❷ port: 6379 ❸ timeout: 1s ❹

❶ 建立连接的时间限制

❷ 默认 Redis 主机

❸ 默认 Redis 端口

❹ 收到回复的时间限制

在下一节中,你将了解如何使用 Redis 来支持提供服务器端速率限制支持的 RequestRateLimiter 网关筛选器。

9.3.3 配置请求速率限制器

根据要求,您可以为特定路由配置请求速率限制器筛选器或将其配置为默认筛选器。在本例中,我们将它配置为默认筛选器,以便将其应用于当前和将来的所有路由。

Redis 上的 RequestRateLimiter 实现基于令牌桶算法。每个用户都被分配了一个桶,随着时间的推移,代币以特定的速率(补充率)滴入其中。每个存储桶都有一个最大容量(突增容量)。当用户发出请求时,令牌将从其存储桶中删除。当没有更多的令牌时,不允许请求,用户将不得不等待更多令牌滴入其存储桶。

注意如果您想了解有关令牌桶算法的更多信息,我建议您阅读 Paul Tarjan 的“使用速率限制器扩展您的 API”文章,了解他们如何使用它在 Stripe (https://stripe.com/blog/rate-limiters) 上实现速率限制器。

对于此示例,让我们配置算法,以便每个请求花费 1 个令牌(redis-rate-limiter.requestTokens)。代币按照配置的补充速率(redis-rate-limiter.replenishRate)滴入存储桶中,我们将该速率设置为每秒 10 个代币。有时可能会出现峰值,从而导致请求数量比平时更多。您可以通过为存储桶定义更大的容量 (redis-rate-limiter.burstCapacity) 来允许临时突发,例如 20。这意味着当出现峰值时,每秒最多允许 20 个请求。由于补充速率低于突发容量,因此不允许后续突发。如果两个峰值按顺序发生,则只有第一个峰值会成功,而第二个峰值将导致某些请求被丢弃,并显示 HTTP 429 - 请求过多响应。应用程序.yml 文件中的结果配置显示在以下列表中。

清单 9.14 配置请求速率限制器作为网关过滤器

spring: cloud: gateway: default-filters: name: RequestRateLimiter args: redis-rate-limiter: replenishRate: 10 ❶ burstCapacity: 20 ❷ requestedTokens: 1 ❸

❶ 每秒滴入桶中的代币数量

❷ 允许最多 20 个请求的请求突发

❸ 请求需要多少代币

在为请求速率限制器提供良好的数字时,没有一般规则可遵循。您应该从应用程序需求开始,然后采用反复试验的方法:分析生产流量,调整配置,然后重新执行此操作,直到实现既能保持系统可用性又不会严重影响用户体验的设置。即使在那之后,您也应该继续监控速率限制器的状态,因为将来情况可能会发生变化。

Spring Cloud Gateway依靠Redis来跟踪每秒发生的请求数量。默认情况下,为每个用户分配一个存储桶。但是,我们尚未引入身份验证机制,因此我们将对所有请求使用单个存储桶,直到我们解决第 11 章和第 12 章中的安全问题。

注意如果 Redis 不可用,会发生什么情况?Spring Cloud Gateway在构建时考虑了弹性,因此它将保持其服务水平,但在Redis启动并再次运行之前,速率限制器将被禁用。

RequestRateLimiter 过滤器依赖于密钥解析程序 Bean 来确定要用于每个请求的存储桶。默认情况下,它使用 Spring 安全性中当前经过身份验证的用户。在向 Edge 服务添加安全性之前,我们将定义一个自定义密钥解析程序 Bean,并使其返回一个常量值(例如,匿名),以便所有请求都将映射到同一存储桶。

在边缘服务项目中,在新的 com.polarbookshop.edgeservice.config 包中创建 RateLimiterConfig 类,并声明密钥解析程序 bean,实现返回常量键的策略。

示例 9.15 定义一个策略来解析每个请求使用的存储桶

package com.polarbookshop.edgeservice.config; import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RateLimiterConfig { @Bean public KeyResolver keyResolver() { return exchange -> Mono.just("anonymous"); ❶ } }

❶ 速率限制适用于使用常量键的请求。

Spring Cloud Gateway 配置为在每个 HTTP 响应后附加带有有关速率限制的详细信息的标头,我们可以使用这些标头来验证其行为。重新生成并运行边缘服务 (./gradlew bootRun),然后尝试调用其中一个终结点。

$ http :9000/books

响应正文取决于目录服务是否正在运行,但这在本例中无关紧要。需要注意的有趣方面是响应的 HTTP 标头。它们显示速率限制器的配置和时间窗口(1 秒)内允许的剩余请求数:

HTTP/1.1 200 OK Content-Type: application/json X-RateLimit-Burst-Capacity: 20 X-RateLimit-Remaining: 19 X-RateLimit-Replenish-Rate: 10 X-RateLimit-Requested-Tokens: 1

您可能不希望将此信息暴露给客户端,以防该信息可以帮助不良行为者对您的系统进行攻击。或者,您可能需要不同的标头名称。无论哪种方式,都可以使用 spring.cloud.gateway.redis-rate-limiter 属性组来配置该行为。完成应用程序测试后,使用 Ctrl-C 将其停止。

注意当速率限制器模式与其他模式(如时间限制器、断路器和重试)结合使用时,首先应用速率限制器。如果用户的请求超出速率限制,则会立即被拒绝。

Redis 是一种高效的数据存储,可确保快速数据访问、高可用性和弹性。在本节中,我们使用它为速率限制器提供存储,下一节将向您展示如何在另一个常见场景中使用它:会话管理。

9.4 使用 Redis 进行分布式会话管理

在前面的章节中,我经常强调云原生应用程序应该是无状态的。我们扩展和缩减它们,如果它们不是无状态的,则每次关闭实例时都会丢失状态。需要保存某些状态,否则应用程序可能毫无用处。例如,目录服务和订单服务是无状态的,但它们依赖于有状态服务(PostgreSQL 数据库)来永久存储有关书籍和订单的数据。即使应用程序关闭,数据也会保留并可供所有应用程序实例使用。

边缘服务不处理它需要存储的任何业务实体,但它仍然需要一个有状态服务 (Redis) 来存储与 RequestRateLimiter 筛选器相关的状态。复制边缘服务时,请务必跟踪在超过阈值之前剩余的请求数。使用 Redis,可以保证速率限制器功能始终如一且安全。

此外,在第 11 章中,您将扩展边缘服务以添加身份验证和授权。由于它是 Polar 书店系统的入口点,因此在那里对用户进行身份验证是有意义的。有关经过身份验证的会话的数据必须保存在应用程序外部,原因与速率限制器信息相同。如果不是,则每次请求命中不同的边缘服务实例时,用户可能都必须对自己进行身份验证。

一般的想法是保持应用程序无状态,并使用数据服务来存储状态。正如您在第 5 章中学到的,数据服务需要保证高可用性、复制性和持久性。在本地环境中,您可以忽略这一方面,但在生产环境中,您将依赖云提供商提供的数据服务,包括PostgreSQL和Redis。

以下部分将介绍如何使用 Spring 会话数据 Redis 建立分布式会话管理。

9.4.1 使用春季会话数据 Redis 处理会话

Spring 在 Spring 会话项目中提供了会话管理功能。默认情况下,会话数据存储在内存中,但这在云原生应用程序中是不可行的。您希望将其保留在外部服务中,以便数据在应用程序关闭后继续存在。使用分布式会话存储的另一个根本原因是,您通常具有给定应用程序的多个实例。您希望他们访问相同的会话数据,以便为用户提供无缝体验。

Redis 是会话管理的常用选项,Spring Session Data Redis 支持它。此外,您已经为速率限制器设置了它。只需最少的配置即可将其添加到边缘服务。

首先,您需要将 Spring 会话数据 Redis 的新依赖项添加到边缘服务项目的 build.gradle 文件中。您还可以添加 Testcontainers 库,以便在编写集成测试时使用轻量级 Redis 容器。请记住在新添加后刷新并重新导入 Gradle 依赖项。

示例 9.16 为 Spring 会话和测试容器添加依赖关系

ext { ... set('testcontainersVersion', "1.17.3") } dependencies { ... implementation 'org.springframework.session:spring-session-data-redis' testImplementation 'org.testcontainers:junit-jupiter' } dependencyManagement { imports { ... mavenBom ➥ "org.testcontainers:testcontainers-bom:${testcontainersVersion}" } }

接下来,您需要指示 Spring 引导使用 Redis 进行会话管理(spring.session.store 型),并定义一个唯一的命名空间,为来自边缘服务 (spring.session.redis.namespace) 的所有会话数据添加前缀。您还可以定义会话的超时(spring.session.timeout)。如果未指定超时,则默认值为 30 分钟。

在 application.yml 文件中配置春季会话,如下所示。

示例 9.17 配置 Spring 会话以在 Redis 中存储数据

spring: session: store-type: redis timeout: 10m redis: namespace: polar:edge

在网关中管理 Web 会话需要格外小心,以确保在正确的时间保存正确的状态。在此示例中,我们希望在将请求转发到下游之前将会话保存在 Redis 中。我们该怎么做呢?如果您正在考虑是否有网关过滤器,那么您是对的!

在边缘服务项目的 application.yml 文件中,将 SaveSession 添加为默认过滤器,以指示 Spring 云网关在将请求转发到下游之前始终保存 Web 会话。

示例 9.18 配置网关以保存会话数据

spring: cloud: gateway: default-filters: - SaveSession ❶

❶ 确保在将请求转发到下游之前保存会话数据

当春季会话与春季安全性相结合时,这是一个关键点。第11章和第12章将介绍有关会话管理的更多详细信息。现在,让我们设置一个集成测试来验证边缘服务中的 Spring 上下文是否正确加载,包括与 Redis 的集成。

我们将使用的方法类似于我们在上一章中用于定义 PostgreSQL 测试容器的方法。让我们扩展由 Spring Initializr 生成的现有 EdgeServiceApplicationTests 类,并配置一个 Redis 测试容器。对于此示例,验证当 Redis 用于存储与 Web 会话相关的数据时是否正确加载 Spring 上下文就足够了。

示例 9.19 使用 Redis 容器测试 Spring 上下文加载

package com.polarbookshop.edgeservice; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @SpringBootTest( ❶ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) @Testcontainers ❷ class EdgeServiceApplicationTests { private static final int REDIS_PORT = 6379; @Container static GenericContainer<?> redis = ❸ new GenericContainer<>(DockerImageName.parse("redis:7.0")) .withExposedPorts(REDIS_PORT); @DynamicPropertySource ❹ static void redisProperties(DynamicPropertyRegistry registry) { registry.add("spring.redis.host", () -> redis.getHost()); registry.add("spring.redis.port", () -> redis.getMappedPort(REDIS_PORT)); } @Test void verifyThatSpringContextLoads() { ❺ } }

❶ 加载完整的 Spring Web 应用程序上下文和侦听随机端口的 Web 环境

❷ 激活测试容器的自动启动和清理

❸ 定义用于测试的 Redis 容器

❹ 覆盖 Redis 配置以指向测试 Redis 实例

❺ 空测试,用于验证应用程序上下文是否已正确加载以及是否已成功建立与 Redis 的连接

最后,按如下所示运行集成测试:

$ ./gradlew test --tests EdgeServiceApplicationTests

如果要在某些测试中禁用通过 Redis 进行的会话管理,可以通过使用 @TestPropertySource 注释在特定测试类中将 spring.session.store-type 属性设置为 none 来实现,或者如果要将其应用于所有测试类,则可以在属性文件中将其设置为无。

极地实验室

请随意应用在前面章节中学到的知识,并准备 Edge 服务应用程序以进行部署。

  1. 将 Spring 云配置客户端添加到边缘服务,使其从配置服务获取配置数据。
  2. 配置云原生构建包集成,容器化应用程序,并定义部署管道的提交阶段,如第 3 章和第 6 章所述。
  3. 编写部署和服务清单,以便将边缘服务部署到 Kubernetes 群集。
  4. 配置 Tilt 以自动将边缘服务部署到使用 minikube 初始化的本地 Kubernetes 集群。

您可以参考本书随附的代码库中的 Chapter09/09-end 文件夹来检查最终结果 (https://github.com/ThomasVitale/cloud-native-spring-in-action)。您还可以使用 kubectl apply -f 服务从 Chapter09/09-end/polar-deployment/kubernetes/platform/development 文件夹中的清单中部署支持服务。

9.5 使用 Kubernetes 入口管理外部访问

Spring 云网关可帮助您定义边缘服务,您可以在系统的入口点实现多种模式和横切关注点。在前面的部分中,您了解了如何将其用作 API 网关、实现速率限制和断路器等弹性模式,以及如何定义分布式会话。在第 11 章和第 12 章中,我们还将向边缘服务添加身份验证和授权功能。

边缘服务表示 Polar 书店系统的入口点。但是,当它部署在 Kubernetes 集群中时,它只能从集群本身内部访问。在第 7 章中,我们使用端口转发功能将 minikube 集群中定义的 Kubernetes 服务公开给您的本地计算机。在开发过程中,这是一个有用的策略,但它不适合生产。

本节将介绍如何使用 Ingress API 管理对在 Kubernetes 集群中运行的应用程序的外部访问。

注意本部分假设您已完成前面“Polar Labs”侧栏中列出的任务,并准备了边缘服务以在 Kubernetes 上部署。

9.5.1 了解入口 API 和入口控制器

当涉及到在 Kubernetes 集群中公开应用程序时,我们可以使用 ClusterIP 类型的服务对象。这就是我们到目前为止所做的,使 Pod 能够在集群内相互交互。例如,这就是 Catalog Service Pod 与 PostgreSQL Pod 通信的方式。

服务对象也可以是负载均衡器类型,它依赖于云提供商预配的外部负载均衡器来向 Internet 公开应用程序。我们可以为边缘服务定义负载均衡器服务,而不是 ClusterIP 服务。在公有云中运行系统时,供应商将预配负载均衡器,分配公共 IP 地址,来自该负载均衡器的所有流量都将定向到 Edge Service Pod。这是一种灵活的方法,可让您将服务直接公开给互联网,并且适用于不同类型的流量。

负载均衡器服务方法涉及为我们决定向互联网公开的每个服务分配不同的 IP 地址。由于服务是直接公开的,因此我们没有机会应用任何进一步的网络配置,例如 TLS 终止。我们可以在边缘服务中配置 HTTPS,通过网关路由定向到集群的所有流量(甚至是不属于 Polar Bookshop 的平台服务),并在那里应用进一步的网络配置。Spring 生态系统提供了解决这些问题所需的一切,这可能是我们在许多情况下会做的事情。但是,由于我们希望在 Kubernetes 上运行我们的系统,我们可以在平台级别管理这些基础设施问题,并使我们的应用程序更简单、更易于维护。这就是入口API派上用场的地方。

入口是“管理对集群中服务的外部访问(通常是 HTTP)”的对象。入口可以提供负载平衡,SSL终止和基于名称的虚拟主机“(https://kubernetes.io/docs)。入口对象充当 Kubernetes 集群的入口点,能够将流量从单个外部 IP 地址路由到集群内运行的多个服务。我们可以使用入口对象来执行负载均衡,接受定向到特定 URL 的外部流量,并管理 TLS 终止以通过 HTTPS 公开应用程序服务。

入口对象本身不会完成任何操作。我们使用入口对象在路由和 TLS 终止方面声明所需的状态。强制实施这些规则并将流量从群集外部路由到内部应用程序的实际组件是入口控制器。由于有多个实现可用,因此核心 Kubernetes 发行版中不包含默认入口控制器 - 由您来安装一个。入口控制器是通常使用反向代理(如 NGINX、HAProxy 或 Envoy)构建的应用程序。一些例子是大使使者,轮廓和入口NGINX。

在生产中,云平台或专用工具将用于配置入口控制器。在我们的本地环境中,我们需要一些额外的配置才能使路由正常工作。对于 Polar Bookshop 示例,我们将在这两种环境中使用 Ingress NGINX (https://github.com/kubernetes/ingress-nginx)。

注意有两种流行的基于NGINX的入口控制器。Ingress NGINX项目(https://github.com/kubernetes/ingress-nginx)在Kubernetes项目本身中开发,支持和维护。它是开源的,这就是我们将在本书中使用的内容。NGINX控制器(www.nginx.com/products/nginx-controller)是由F5 NGINX公司开发和维护的产品,它提供免费和商业选项。

让我们看看如何在本地 Kubernetes 集群上使用 Ingress NGINX。入口控制器是一个工作负载,就像在 Kubernetes 上运行的任何其他应用程序一样,它可以通过不同的方式部署。最简单的选择是使用 kubectl 将其部署清单应用于集群。由于我们使用minikube来管理本地的Kubernetes集群,因此我们可以依靠内置的附加组件来启用基于Ingress NGINX的Ingress功能。

首先,让我们开始我们在第7章中介绍的极地本地集群。由于我们将 minikube 配置为在 Docker 上运行,因此请确保您的 Docker 引擎已启动并运行:

$ minikube start --cpus 2 --memory 4g --driver docker --profile polar

接下来,我们可以启用入口附加组件,这将确保将入口NGINX部署到我们的本地集群:

$ minikube addons enable ingress --profile polar

最后,您可以获取有关使用Ingress NGINX部署的不同组件的信息,如下所示:

$ kubectl get all -n ingress-nginx

前面的命令包含一个我们还没有遇到的参数:-n ingress-nginx。这意味着我们要获取在 ingress-nginx 命名空间中创建的所有对象。

命名空间是“Kubernetes 用来支持单个集群内资源组隔离的抽象。命名空间用于组织群集中的对象,并提供一种划分群集资源的方法“(https://kubernetes.io/docs/reference/glossary)。

我们使用命名空间来保持集群井井有条,并定义网络策略以出于安全原因隔离某些资源。到目前为止,我们一直在使用默认命名空间,我们将继续为所有 Polar Bookshop 应用程序执行此操作。但是,当涉及到Ingress NGINX等平台服务时,我们将依靠专用命名空间来保持这些资源的隔离。

现在 Ingress NGINX 已安装,让我们继续部署 Polar Bookshop 应用程序使用的后备服务。检查本书随附的源代码存储库(第 09/09 章完),并将 polar-deployment/kubernetes/platform/development 文件夹的内容复制到 polar-deployment 存储库中的相同路径中,覆盖我们在前几章中使用的任何现有文件。该文件夹包含运行 PostgreSQL 和 Redis 的基本 Kubernetes 清单。

打开终端窗口,导航到 polar-deployment 存储库中的 kubernetes/platform/development 文件夹,然后运行以下命令在本地集群中部署 PostgreSQL 和 Redis:

$ kubectl apply -f services

您可以使用以下命令验证结果:

$ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE polar-postgres 1/1 1 1 73s polar-redis 1/1 1 1 73s

提示为了您的方便,我准备了一个脚本,该脚本使用单个命令执行所有先前的操作。你可以运行它来创建一个本地的 Kubernetes 集群,启用 Ingress NGINX 插件,并部署 Polar Bookshop 使用的后备服务。您将在刚刚复制到极地部署存储库的 kubernetes/platform/development 文件夹中找到 create-cluster.sh 和 destroy-cluster.sh 文件。在 macOS 和 Linux 上,您可能需要通过 chmod x create-cluster.sh 命令使脚本可执行。

最后,我们将边缘服务打包为容器映像,并将项目加载到本地 Kubernetes 群集。打开终端窗口,导航到 Edge 服务根文件夹(边缘服务),然后运行以下命令:

$ ./gradlew bootBuildImage $ minikube image load edge-service --profile polar

在下一节中,您将定义一个入口对象,并将其配置为管理对在 Kubernetes 集群中运行的 Polar 书店系统的外部访问。

9.5.2 使用入口对象

边缘服务负责应用程序路由,但不应关注底层基础架构和网络配置。使用入口资源,我们可以分离这两个职责。开发人员将维护边缘服务,而平台团队将管理入口控制器和网络配置(可能依赖于像Linkerd或Istio这样的服务网格)。图 9.6 显示了引入入口后 Polar 书店的部署架构。

图9.6 引入入口管理集群外部访问后的极地书店系统部署架构

让我们定义一个入口,用于将来自群集外部的所有 HTTP 流量路由到 Edge 服务。通常根据用于发送 HTTP 请求的 DNS 名称定义入口路由和配置。由于我们在本地工作,并且假设我们没有 DNS 名称,因此我们可以调用为入口预配的外部 IP 地址,以便从群集外部访问。在 Linux 上,您可以使用分配给 minikube 集群的 IP 地址。可以通过运行以下命令来检索该值:

$ minikube ip --profile polar 192.168.49.2

在 macOS 和 Windows 上,入口插件尚不支持在 Docker 上运行时使用 minikube 集群的 IP 地址。相反,我们需要使用 minikube 隧道 --profile polar 命令将集群暴露给本地环境,然后使用 127.0.0.1 IP 地址调用集群。这类似于 kubectl 端口转发命令,但它适用于整个集群而不是特定服务。

确定要使用的 IP 地址后,让我们定义 Polar 书店的入口对象。在边缘服务项目中,在 k8s 文件夹中创建新的入口.yml 文件。

清单 9.20 通过入口在集群外部公开边缘服务

apiVersion: networking.k8s.io/v1 ❶ kind: Ingress ❷ metadata: name: polar-ingress ❸ spec: ingressClassName: nginx ❹ rules: - http: ❺ paths: - path: / ❻ pathType: Prefix backend: service: name: edge-service ❼ port: number: 80 ❽

❶ 入口对象的 API 版本

❷ 要创建的对象类型

❸ 入口的名称

❹ 配置负责管理此对象的入口控制器

❺ HTTP 流量的入口规则

❻ 所有请求的默认规则

❼ 应转发流量的服务对象的名称

❽ 应转发流量的服务端口号

此时,我们已准备好将边缘服务和入口部署到本地 Kubernetes 集群。打开终端窗口,导航到 Edge 服务根文件夹(边缘服务),然后运行以下命令:

$ kubectl apply -f k8s

让我们使用以下命令验证是否已正确创建入口对象:

$ kubectl get ingress NAME CLASS HOSTS PORTS AGE polar-ingress nginx * 80 21s

是时候测试边缘服务是否通过入口正确可用了。如果您使用的是 Linux,则不需要任何进一步的准备步骤。如果您使用的是 macOS 或 Windows,请打开一个新的终端窗口并运行以下命令以向本地主机公开您的 minikube 集群。该命令将继续运行以使隧道可访问,因此请确保使终端窗口保持打开状态。首次运行此命令时,系统可能会要求您输入计算机的密码以授权对群集的隧道:

$ minikube tunnel --profile polar

最后,打开一个新的终端窗口并运行以下命令来测试应用程序(在 Linux 上,使用 minikube 的 IP 地址而不是 127.0.0.1):

$ http 127.0.0.1/books

由于目录服务未运行,Edge 服务将执行我们之前配置的回退行为,并返回正文为空的 200 OK 响应。这就是我们所期望的,它证明了入口配置有效。

完成部署试用后,可以使用以下命令停止并删除本地 Kubernetes 集群:

$ minikube stop --profile polar $ minikube delete --profile polar

提示为了您的方便,您还可以使用您之前从本书的源代码中复制的 destroy-cluster.sh 脚本(在 polar-deployment 存储库的 kubernetes/platform/development 文件夹中可用)。在 macOS 和 Linux 上,您可能需要通过 chmod x destroy-cluster.sh 命令使脚本可执行。

干得好!现在,我们已准备好通过添加身份验证和授权来改进边缘服务。但是,在配置安全性之前,我们仍然需要完成用于调度订单的 Polar 书店业务逻辑。在下一章中,您将在学习事件驱动架构、Spring Cloud Function 和 Spring Cloud Stream with RabbitMQ 的同时做到这一点。

总结
查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved