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

一、概述

在 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. 实践环境

  • 开发工具:IntelliJ IDEA 2021.1.1 (Ultimate Edition)
  • jdk 1.8
  • Apache Maven 3.6.3
  • docker 20.10.5
  • minikube v1.18.1
  • spring boot 2.5.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返回类型。当它们这样做时,方法调用的结果将作为新事件发送。如果返回类型是数组或集合,则每个元素将作为新的单个事件发送。

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

  • 异常处理:虽然事件侦听器可以声明它抛出任意异常类型,但是从事件侦听器抛出的任何选中的异常都将包装在未声明的ThrowableException中,因为事件发布器只能处理运行时异常。

  • 异步侦听器:如果希望某个特定的侦听器异步处理事件,可以使用Spring的@Async支持,但在使用异步事件时要注意以下限制。如果异步事件侦听器抛出异常,则不会将其传播到调用方。有关详细信息,请参阅AsyncUncaughtExceptionHandler。异步事件侦听器方法无法通过返回值来发布后续事件。如果需要作为处理结果发布另一个事件,请插入ApplicationEventPublisher以手动发布该事件。

  • 排序侦听器:还可以定义调用某个事件的侦听器的顺序。为此,将Spring的公共@Order注解添加到这个事件侦听器注解旁边。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269