引言

在云原生时代,保证服务的稳定性和高可用性至关重要。本文将深入探讨如何结合 Kubernetes (K8s) 与 Spring Boot,实现零宕机发布。我们将覆盖以下核心主题:

  • 健康检查:确保流量只被路由到健康的实例。
  • 滚动更新:平滑地升级应用,不中断服务。
  • 优雅停机:安全地关闭应用,避免数据丢失或请求失败。
  • 弹性伸缩:根据负载自动调整实例数量。
  • Prometheus 监控:收集关键指标,洞察应用性能。
  • 配置分离:实现镜像复用,提高交付效率。

参考资料:


1. 健康检查

健康检查是实现零宕机发布的基础。K8s 通过就绪探针 (Readiness Probe)存活探针 (Liveness Probe) 来判断应用实例是否准备好接收流量或是否需要重启。

  • 就绪探针 (Readiness): 告诉 K8s 应用是否准备好处理请求。如果失败,K8s 会将该 Pod 从 Service 的端点列表中移除。
  • 存活探针 (Liveness): 判断应用是否仍在运行。如果失败,K8s 会重启该 Pod。

探针类型主要有三种:exec(执行脚本)、tcpSocket(检查端口)和 httpGet(发起 HTTP 请求)。

1.1. 应用层配置 (Spring Boot)

首先,在 pom.xml 中添加 spring-boot-starter-actuator 依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后,在 application.yaml 中配置 Actuator,暴露健康检查端点:

1
2
3
4
5
6
7
8
9
10
11
management:
server:
port: 50000 # 启用独立的运维端口,与业务端口分离
endpoint:
health:
probes:
enabled: true # 启用 K8s 探针
endpoints:
web:
exposure:
include: health # 暴露 health 端点

配置完成后,Actuator 会暴露以下端点:

  • /actuator/health/readiness: 就绪状态检查
  • /actuator/health/liveness: 存活状态检查

1.2. 平台层配置 (Kubernetes)

deployment.yaml 中配置探针,指向 Actuator 提供的端点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: {APP_NAME}
# ... 其他配置 ...
ports:
- containerPort: {APP_PORT}
- name: management-port
containerPort: 50000
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: management-port
initialDelaySeconds: 90 # 容器启动后延迟 90s 开始探测
periodSeconds: 30 # 每 30s 探测一次
timeoutSeconds: 10 # 探测超时时间
successThreshold: 1 # 连续 1 次成功即视为健康
failureThreshold: 3 # 连续 3 次失败即视为不健康
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: management-port
initialDelaySeconds: 90
periodSeconds: 30
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 3

2. 滚动更新

滚动更新 (Rolling Update) 是 K8s 的默认部署策略,它通过逐个替换旧 Pod 来实现平滑升级。结合健康检查,可以确保在整个更新过程中服务不中断。

1
2
3
4
5
6
7
8
9
apiVersion: apps/v1
kind: Deployment
spec:
replicas: {REPLICAS}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 更新期间,最多允许超出期望副本数的 Pod 数量
maxUnavailable: 0 # 更新期间,最多允许多少个 Pod 处于不可用状态
  • maxSurge: 保证了在更新时有额外的 Pod 来处理流量。
  • maxUnavailable: 设为 0 确保了在任何时候都有足够数量的 Pod 在运行。

3. 优雅停机

优雅停机的核心目标是:不再接收新流量完成在途任务有序释放资源

3.1. 核心原则

  1. 停止流量: 首先让 Pod 的就绪探针失败,K8s 会自动将其从流量入口移除。
  2. 处理在途: 应用内部等待正在处理的任务(如 HTTP 请求、消息消费)完成。
  3. 释放资源: 按顺序关闭线程池、数据库连接等资源。
  4. 设置超时: 所有等待过程都应有超时机制,防止无限期阻塞。

3.2. 应用层配置 (Spring Boot)

HTTP 请求

Spring Boot 2.3+ 默认支持优雅停机。在 application.yaml 中配置:

1
2
3
4
5
6
server:
shutdown: graceful

spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 每个停机阶段的超时时间

@Async@Scheduled 任务

对于异步和定时任务,可以配置等待其完成:

1
2
3
4
5
6
7
8
9
10
spring:
task:
execution:
shutdown:
await-termination: true
await-termination-period: 30s
scheduling:
shutdown:
await-termination: true
await-termination-period: 30s

消息队列 (Kafka)

对于消息消费,关键是手动确认 (ack),确保消息被完全处理后再提交位移。

1
2
3
4
5
6
7
8
spring:
cloud:
stream:
kafka:
bindings:
testConsumer-in-0:
consumer:
ack-mode: manual # 设置为手动 ack

在消费逻辑中,处理完成后再调用 ack.acknowledge()

3.3. 平台层配置 (Kubernetes)

K8s 在删除 Pod 时,会先发送 SIGTERM 信号,并等待 terminationGracePeriodSeconds 定义的时间。我们可以利用 preStop 钩子来主动触发应用的优雅停机逻辑。

1
2
3
4
5
6
7
8
spec:
terminationGracePeriodSeconds: 45 # 优雅停机宽限期,应大于应用停机所需时间
containers:
- name: {APP_NAME}
lifecycle:
preStop:
exec:
command: ["curl", "-XPOST", "http://localhost:50000/actuator/shutdown"]

推荐的停机顺序:

  1. K8s 发送 SIGTERM 信号。
  2. preStop 钩子执行,调用 /actuator/shutdown
  3. Spring Boot 开始优雅停机,新请求被拒绝。
  4. 等待在途的 HTTP 请求、异步任务和消息处理完成。
  5. 应用进程退出。

4. 弹性伸缩

通过水平 Pod 自动伸缩器 (HPA),K8s 可以根据 CPU 或内存使用率自动调整 Pod 数量。

首先,为容器设置资源请求和限制:

1
2
3
4
5
6
7
8
9
10
11
12
spec:
template:
spec:
containers:
- name: {APP_NAME}
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"

然后,创建 HPA 资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {APP_NAME}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {APP_NAME}
minReplicas: {REPLICAS}
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80 # CPU 平均使用率达到 80% 时扩容

5. Prometheus 集成

为了监控应用性能,我们可以集成 Prometheus。

5.1. 应用层配置

添加 micrometer-registry-prometheus 依赖:

1
2
3
4
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

暴露 prometheus 端点:

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: health, prometheus

5.2. 平台层配置

在 Deployment 中添加注解,让 Prometheus 能够自动发现并抓取指标:

1
2
3
4
5
6
7
spec:
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "50000"

6. 配置分离

为了实现镜像复用和配置的灵活管理,我们使用 ConfigMap 来外挂配置文件。

  1. 创建 ConfigMap:

    1
    kubectl create configmap {APP_NAME}-config --from-file=application-prod.yaml
  2. 挂载 ConfigMap 到 Pod:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    spec:
    template:
    spec:
    containers:
    - name: {APP_NAME}
    env:
    - name: SPRING_PROFILES_ACTIVE
    value: "prod" # 激活对应的 profile
    volumeMounts:
    - name: config-volume
    mountPath: /app/config
    volumes:
    - name: config-volume
    configMap:
    name: {APP_NAME}-config

7. 汇总配置示例

pom.xml

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>

application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
application:
name: my-awesome-app
lifecycle:
timeout-per-shutdown-phase: 30s
task:
# ... 异步与定时任务优雅停机配置 ...

server:
port: 8080
shutdown: graceful

management:
server:
port: 50000
endpoints:
web:
exposure:
include: health, prometheus, shutdown
endpoint:
health:
probes:
enabled: true

Dockerfile

1
2
3
4
5
6
7
FROM openjdk:11-jre-slim
ARG JAR_FILE=target/*.jar
WORKDIR /app
COPY ${JAR_FILE} app.jar
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
EXPOSE 8080 50000
ENTRYPOINT ["java", "-jar", "app.jar"]

deployment.yaml (部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
name: {APP_NAME}
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "50000"
spec:
terminationGracePeriodSeconds: 45
containers:
- name: {APP_NAME}
image: {IMAGE_URL}
ports:
- containerPort: 8080
- name: management
containerPort: 50000
lifecycle:
preStop:
exec:
command: ["curl", "-XPOST", "http://localhost:50000/actuator/shutdown"]
# ... 探针和资源配置 ...

常见问题

问题: 应用中有 while(true) 循环(例如在 CommandLineRunner 中),导致就绪探针永远返回 503 Service Unavailable。

原因: CommandLineRunner 在 Spring Boot 应用启动的主线程中执行。如果它进入一个无限循环,启动过程将永远不会完成,导致 Actuator 端点无法响应。

解决方案: 将耗时的或无限循环的任务放在一个单独的子线程中执行,避免阻塞主线程。

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MyTaskRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
new Thread(() -> {
while (true) {
// ... 执行任务 ...
}
}).start();
}
}

通过遵循这些最佳实践,您可以构建一个健壮、高可用的 Spring Boot 应用,并充满信心地在 Kubernetes 上进行部署和运维。