7.4.2 确保可弃置性:优雅关闭
仅有应用程序快速启动不足以满足我们的可扩展性需求。每当一个应用程序实例被关闭时,都必须优雅地进行,而不会让客户端经历停机或错误。优雅关闭意味着应用程序停止接受新请求,完成所有仍在进行中的请求,并关闭所有打开的资源,如数据库连接。
Spring Boot 中所有可用的内嵌服务器都支持优雅关闭模式,但方式略有不同。Tomcat、Jetty 和 Netty 在收到关闭信号时完全停止接受新请求。另一方面,Undertow 继续接受新请求,但立即回复 HTTP 503 响应。
默认情况下,Spring Boot 在收到终止信号(SIGTERM)后立即停止服务器。您可以通过配置 server.shutdown 属性切换到优雅模式。您还可以配置宽限期,即应用程序可以花多长时间处理所有待处理的请求。宽限期到期后,即使仍有待处理的请求,应用程序也会被终止。默认情况下,宽限期为 30 秒。您可以通过 spring.lifecycle.timeout-per-shutdown-phase 属性更改它。
让我们为 Catalog Service 配置优雅关闭。我们可以通过环境变量或将其设置为默认配置来实现。我们将选择第二种方式。打开位于 catalog-service/src/main/resources 文件夹中的 application.yml 文件,按如下方式更新配置:
server:
port: 9001
shutdown: graceful # 启用优雅关闭
tomcat:
connection-timeout: 2s
keep-alive-timeout: 15s
threads:
max: 50
min-spare: 5
spring:
application:
name: catalog-service
lifecycle:
timeout-per-shutdown-phase: 15s # 定义 15 秒宽限期
由于我们修改了应用程序源代码,我们需要构建新的容器镜像并将其加载到 minikube 中。这不是很高效,对吧?在本章后面,我将向您展示一种更好的方法。目前,请按照我之前描述的过程将 Catalog Service 打包为容器镜像(./gradlew bootBuildImage),并将其加载到我们用于 Polar Bookshop 的 Kubernetes 集群中(minikube image load catalog-service --profile polar)。
在为应用程序启用优雅关闭支持后,您需要相应地更新 Deployment 清单。当 Pod 必须被终止时(例如在缩减过程中或作为升级的一部分),Kubernetes 会向其发送 SIGTERM 信号。Spring Boot 将拦截该信号并开始优雅关闭。默认情况下,Kubernetes 等待 30 秒的宽限期。如果 Pod 在该期限后未终止,Kubernetes 发送 SIGKILL 信号以强制 Pod 终止。由于 Spring Boot 的宽限期低于 Kubernetes 的,应用程序控制着何时终止。
当 Kubernetes 向 Pod 发送 SIGTERM 信号时,它还会通知其自身组件停止将请求转发到正在终止的 Pod。由于 Kubernetes 是一个分布式系统,且两个操作并行发生,存在一个短暂的时间窗口,正在终止的 Pod 可能仍然收到请求,即使它已经开始了优雅关闭过程。当这种情况发生时,这些新请求将被拒绝,导致客户端出现错误。我们的目标是使关闭过程对客户端透明,因此该场景是不可接受的。
推荐的解决方案是延迟向 Pod 发送 SIGTERM 信号,以便 Kubernetes 有足够的时间在集群中传播消息。这样,当 Pod 开始优雅关闭过程时,所有 Kubernetes 组件都已经知道不要向其发送新请求。在技术上,可以通过 preStop 钩子来配置延迟。让我们看看如何更新 Catalog Service 的 Deployment 清单以支持透明和优雅的关闭。
打开 catalog-service/k8s 中的 deployment.yml 文件,添加 preStop 钩子以将 SIGTERM 信号延迟 5 秒。
# 代码清单 7.4 在 Kubernetes 中配置关闭开始前的延迟
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
labels:
app: catalog-service
spec:
...
template:
metadata:
labels:
app: catalog-service
spec:
containers:
- name: catalog-service
image: catalog-service
imagePullPolicy: IfNotPresent
lifecycle:
preStop: # 使 Kubernetes 在发送 SIGTERM 信号前等待 5 秒
exec:
command: ["sh", "-c", "sleep 5"]
...
最后,使用 kubectl apply -f k8s/deployment.yml 应用更新后的 Deployment 对象。Kubernetes 将协调新的期望状态,并用一个完全配置了优雅关闭的新 Pod 替换现有的 Pod。
注意:当 Pod 包含多个容器时,SIGTERM 信号会并行发送给所有容器。Kubernetes 最多等待 30 秒。如果 Pod 中的任何容器尚未终止,它将强制关闭它们。
现在我们已经为 Catalog Service 配置了优雅关闭行为,让我们看看如何在 Kubernetes 集群中扩展它。