技术中台K8s架构

spring boot 应用在 k8s 中的健康检查(三)

2021-06-20  本文已影响0人  惜鸟

一、概述

在 spring boot 2.3 中引入了容器探针,也就是增加了 /actuator/health/liveness/actuator/health/readiness 这两个健康检查路径,对于部署在 k8s 中的应用,spring-boot-actuator 将通过这两个路径自动进行健康检查。本文主要根据官方文档的描述实践并记录使用流程,从如下几个方面进行介绍:

  1. k8s 中的健康检查
  2. spring-boot-actuator 中的 k8s 探针
  3. spring boot 健康检查在 k8s 中的实践

二、spring boot 健康检查在 k8s 中的实践

本次实践的思路来自下文的参考文章,这里使用 spring boot 2.5.1 进行实践

1. 实践环境

2. 创建一个 spring boot 项目

1. 使用 idea 创建一个 spring boot 项目:

image.png

2. pom.xml 的依赖配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>probedemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>probedemo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--   用来做健康检查的 starter     -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3. 创建一个监听类,可以监听存活和就绪状态的变化:

package com.example.probedemo.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.AvailabilityState;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

/**
 * 监听系统事件的类
 *
 * @className: AvailabilityListener
 * @date: 2021/6/15 10:44
 */
@Slf4j
@Component
public class AvailabilityListener {

    /**
     * 基于 spring 的事件监听机制,监听系统的消息
     * 当监听到 AvailabilityChangeEvent 事件会触发此方法的调用
     * 这里使用日志记录事件的状态
     * @param event
     */
    @EventListener
    public void onStateChange(AvailabilityChangeEvent<? extends AvailabilityState> event) {
        log.info(event.getState().getClass().getSimpleName() + ": " + event.getState());
    }

}

@EventListener 注解说明:

将方法标记为应用程序事件侦听器的注解。

如果带注解的方法支持单个事件类型,则该方法可以声明一个反映要侦听的事件类型的参数。如果带注解的方法支持多个事件类型,则此注解可以使用classes属性引用一个或多个受支持的事件类型。有关详细信息,请参见类javadoc。

事件可以是ApplicationEvent实例,也可以是任意对象。

@EventListener注解的处理通过内部EventListenerMethodProcessor bean执行,该bean在使用Java config时自动注册,或者通过<context:annotation-config/>或者<context:component-scan/>使用XML配置时的元素。

带注解的方法可能具有非void返回类型。当它们这样做时,方法调用的结果将作为新事件发送。如果返回类型是数组或集合,则每个元素将作为新的单个事件发送。

此注解可用作元注解,以创建自定义组合注解。

4. 创建一个 stateController 用来修改状态

package com.example.probedemo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * 测试修改状态的 controller
 *
 * @className: StateWriter
 * @date: 2021/6/15 14:17
 */
@RestController
@RequestMapping("/state")
public class StateController {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    /**
     * 将存活状态改为 BROKEN
     * 这会导致 k8s 杀死 pod,并根据重启策略重启 pod
     *
     * @return
     */
    @GetMapping("broken")
    public String broken() {
        AvailabilityChangeEvent.publish(applicationEventPublisher, this, LivenessState.BROKEN);
        return "success broken, " + new Date();
    }

    /**
     * 将存活状态修改为 correct
     * @return
     */
    @GetMapping("correct")
    public String correct() {
        AvailabilityChangeEvent.publish(applicationEventPublisher, this, LivenessState.CORRECT);
        return "success correct, " + new Date();
    }

    /**
     * 将就绪状态修改为 ACCEPTING_TRAFFIC (接受流量)
     * k8s 会将外部请求转发到此 pod
     * @return
     */
    @GetMapping("accept")
    public String accept() {
        AvailabilityChangeEvent.publish(applicationEventPublisher, this, ReadinessState.ACCEPTING_TRAFFIC);
        return "success accept, " + new Date();
    }

    /**
     * 将就绪状态修改为 REFUSING_TRAFFIC
     * k8s 通过将 service 对应的后端 endpoint 中此 pod 的ip移除来拒绝外部请求
     * @return
     */
    @GetMapping("refuse")
    public String refuse() {
        AvailabilityChangeEvent.publish(applicationEventPublisher, this, ReadinessState.REFUSING_TRAFFIC);
        return "success refuse, " + new Date();
    }


}


5. 制作 docker 镜像

在pom.xml所在目录创建文件Dockerfile,内容如下:

# 指定基础镜像,这是多阶段构建的前期阶段
FROM openjdk:11-jre-slim as builder
# 指定工作目录,目录不存在会自动创建
WORKDIR /app
# 将生成的 jar 复制到容器镜像中
COPY target/*.jar application.jar
# 通过工具spring-boot-jarmode-layertools从application.jar中提取拆分后的构建结果
RUN java -Djarmode=layertools -jar application.jar extract

# 正式构建镜像
FROM openjdk:11-jre-slim
# 指定工作目录,目录不存在会自动创建
WORKDIR /app
# 前一阶段从jar中提取除了多个文件,这里分别执行COPY命令复制到镜像空间中,每次COPY都是一个layer
COPY --from=builder app/dependencies ./
COPY --from=builder app/spring-boot-loader ./
COPY --from=builder app/snapshot-dependencies ./
COPY --from=builder app/application ./
# 指定时区
ENV TZ="Asia/Shanghai"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 定义一些环境变量,方便环境变量传参
ENV JVM_OPTS=""
ENV JAVA_OPTS=""
# 指定暴露的端口,起到说明的作用,不指定也会暴露对应端口
EXPOSE 8080
# 启动 jar 的命令
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]


使用以下命令编译构建项目:

mvn clean package -U -DskipTests

使用以下命令构建 docker 镜像(最后有一个 . 表示当前目录作为docker构建的上下文环境):

docker build -t probedemo:1.0.0 .

使用下面的命令将 docker 镜像推送到远程仓库(这里推送到docker hub仓库,需要自己注册一个账号):

# 给镜像打一个标签,[仓库地址/镜像名:镜像标签]
docker tag probedemo:1.0.0 wangedison98/probedemo:1.0.0
# 推送到远程仓库
docker push wangedison98/probedemo:1.0.0

6. k8s 部署 deployment 和 service

创建名为probedemo.yaml的文件:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: probedemo
  labels:
    app: probedemo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: probedemo
  template:
    metadata:
      labels:
        app: probedemo
    spec:
      containers:
        - name: probedemo
          imagePullPolicy: IfNotPresent
          image: wangedison98/probedemo:1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: "512Mi"
              cpu: "100m"
            limits:
              memory: "1Gi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 5
            failureThreshold: 10
            timeoutSeconds: 10
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 10
            periodSeconds: 5


---
apiVersion: v1
kind: Service
metadata:
  name: probedemo
spec:
  ports:
    - port: 8080
      targetPort: 8080
  selector:
    app: probedemo
  type: NodePort

这里要重点关注的是 livenessProbeinitialDelaySecondsfailureThreshold 参数,initialDelaySeconds 等于5,表示 pod 创建5秒后检查存活探针,如果10秒内应用没有完成启动,存活探针不返回200,就会重试10次(failureThreshold等于10),每一次等待 5 秒(periodSeconds 等于5),如果重试10次,也就是50秒后,存活探针依旧无法返回200,该pod就会被kubernetes杀死重建,要是每次启动都耗时这么长,pod就会不停的被杀死重建,这种情况下可以考虑延长 failureThreshold 失败重试的次数。

使用如下命令创建 deployment 和 service:

kubectl apply -f probedemo.yaml

查看运行的 pod:

image.png

使用如下命令暴露服务端口:

kubectl port-forward service/probedemo 8080 8080

调用存活性检查的 broken 事件,地址如下:

curl http://localhost:8080/state/broken

等待大概一分钟,发现 pod 已经重启一次

image.png

请求拒绝流量,地址如下:

curl http://localhost:8080/state/refuse

可以看到服务已经处于未准备状态:

image.png

查看 pod 的事件:

kubectl describe  probedemo-86cb7cc84b-djrjn
image.png

当再次调用接受流量的请求:

curl http://localhost:8080/state/accept

发现服务已经恢复正常:

image.png

根据这个特性,可以通过程序控制什么时候对外提供服务,当处理一些异常情况时,可以手动拒绝请求,待恢复正常后再提供服务。

三、总结

通过上面的实践,我们测试了spring boot 应用在 k8s 中的健康检查,配置非常简单:

  1. 只需要引入 spring-boot-starter-actuator 依赖即可,不需要其他额外配置
  2. 在 k8s 的部署清单中根据官方文档做如下配置:
image.png

参考文章

https://blog.csdn.net/boling_cavalry/article/details/106607225

https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.kubernetes-probes

上一篇下一篇

猜你喜欢

热点阅读