一、概述
在 spring boot 2.3 中引入了容器探針哆窿,也就是增加了 /actuator/health/liveness
和 /actuator/health/readiness
這兩個(gè)健康檢查路徑农曲,對(duì)于部署在 k8s 中的應(yīng)用社搅,spring-boot-actuator 將通過(guò)這兩個(gè)路徑自動(dòng)進(jìn)行健康檢查。本文主要根據(jù)官方文檔的描述實(shí)踐并記錄使用流程乳规,從如下幾個(gè)方面進(jìn)行介紹:
二形葬、spring boot 健康檢查在 k8s 中的實(shí)踐
本次實(shí)踐的思路來(lái)自下文的參考文章,這里使用
spring boot 2.5.1
進(jìn)行實(shí)踐
1. 實(shí)踐環(huán)境
- 開(kāi)發(fā)工具: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. 創(chuàng)建一個(gè) spring boot 項(xiàng)目
1. 使用 idea 創(chuàng)建一個(gè) spring boot 項(xiàng)目:
2. pom.xml
的依賴(lài)配置如下:
<?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>
<!-- 用來(lái)做健康檢查的 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. 創(chuàng)建一個(gè)監(jiān)聽(tīng)類(lèi)暮的,可以監(jiān)聽(tīng)存活和就緒狀態(tài)的變化:
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;
/**
* 監(jiān)聽(tīng)系統(tǒng)事件的類(lèi)
*
* @className: AvailabilityListener
* @date: 2021/6/15 10:44
*/
@Slf4j
@Component
public class AvailabilityListener {
/**
* 基于 spring 的事件監(jiān)聽(tīng)機(jī)制笙以,監(jiān)聽(tīng)系統(tǒng)的消息
* 當(dāng)監(jiān)聽(tīng)到 AvailabilityChangeEvent 事件會(huì)觸發(fā)此方法的調(diào)用
* 這里使用日志記錄事件的狀態(tài)
* @param event
*/
@EventListener
public void onStateChange(AvailabilityChangeEvent<? extends AvailabilityState> event) {
log.info(event.getState().getClass().getSimpleName() + ": " + event.getState());
}
}
@EventListener
注解說(shuō)明:
將方法標(biāo)記為應(yīng)用程序事件偵聽(tīng)器的注解。
如果帶注解的方法支持單個(gè)事件類(lèi)型冻辩,則該方法可以聲明一個(gè)反映要偵聽(tīng)的事件類(lèi)型的參數(shù)猖腕。如果帶注解的方法支持多個(gè)事件類(lèi)型,則此注解可以使用classes屬性引用一個(gè)或多個(gè)受支持的事件類(lèi)型恨闪。有關(guān)詳細(xì)信息倘感,請(qǐng)參見(jiàn)類(lèi)javadoc。
事件可以是ApplicationEvent實(shí)例咙咽,也可以是任意對(duì)象钠乏。
@EventListener注解的處理通過(guò)內(nèi)部EventListenerMethodProcessor bean執(zhí)行明刷,該bean在使用Java config時(shí)自動(dòng)注冊(cè)挽荠,或者通過(guò)<context:annotation-config/>
或者<context:component-scan/>
使用XML配置時(shí)的元素良瞧。
帶注解的方法可能具有非void返回類(lèi)型。當(dāng)它們這樣做時(shí),方法調(diào)用的結(jié)果將作為新事件發(fā)送。如果返回類(lèi)型是數(shù)組或集合,則每個(gè)元素將作為新的單個(gè)事件發(fā)送弄诲。
此注解可用作元注解,以創(chuàng)建自定義組合注解娇唯。
異常處理:雖然事件偵聽(tīng)器可以聲明它拋出任意異常類(lèi)型齐遵,但是從事件偵聽(tīng)器拋出的任何選中的異常都將包裝在未聲明的ThrowableException中,因?yàn)槭录l(fā)布器只能處理運(yùn)行時(shí)異常视乐。
異步偵聽(tīng)器:如果希望某個(gè)特定的偵聽(tīng)器異步處理事件洛搀,可以使用Spring的
@Async
支持,但在使用異步事件時(shí)要注意以下限制佑淀。如果異步事件偵聽(tīng)器拋出異常留美,則不會(huì)將其傳播到調(diào)用方。有關(guān)詳細(xì)信息伸刃,請(qǐng)參閱AsyncUncaughtExceptionHandler谎砾。異步事件偵聽(tīng)器方法無(wú)法通過(guò)返回值來(lái)發(fā)布后續(xù)事件。如果需要作為處理結(jié)果發(fā)布另一個(gè)事件捧颅,請(qǐng)插入ApplicationEventPublisher以手動(dòng)發(fā)布該事件景图。排序偵聽(tīng)器:還可以定義調(diào)用某個(gè)事件的偵聽(tīng)器的順序。為此碉哑,將Spring的公共@Order注解添加到這個(gè)事件偵聽(tīng)器注解旁邊挚币。
4. 創(chuàng)建一個(gè) stateController 用來(lái)修改狀態(tài)
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;
/**
* 測(cè)試修改狀態(tài)的 controller
*
* @className: StateWriter
* @date: 2021/6/15 14:17
*/
@RestController
@RequestMapping("/state")
public class StateController {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
/**
* 將存活狀態(tài)改為 BROKEN
* 這會(huì)導(dǎo)致 k8s 殺死 pod,并根據(jù)重啟策略重啟 pod
*
* @return
*/
@GetMapping("broken")
public String broken() {
AvailabilityChangeEvent.publish(applicationEventPublisher, this, LivenessState.BROKEN);
return "success broken, " + new Date();
}
/**
* 將存活狀態(tài)修改為 correct
* @return
*/
@GetMapping("correct")
public String correct() {
AvailabilityChangeEvent.publish(applicationEventPublisher, this, LivenessState.CORRECT);
return "success correct, " + new Date();
}
/**
* 將就緒狀態(tài)修改為 ACCEPTING_TRAFFIC (接受流量)
* k8s 會(huì)將外部請(qǐng)求轉(zhuǎn)發(fā)到此 pod
* @return
*/
@GetMapping("accept")
public String accept() {
AvailabilityChangeEvent.publish(applicationEventPublisher, this, ReadinessState.ACCEPTING_TRAFFIC);
return "success accept, " + new Date();
}
/**
* 將就緒狀態(tài)修改為 REFUSING_TRAFFIC
* k8s 通過(guò)將 service 對(duì)應(yīng)的后端 endpoint 中此 pod 的ip移除來(lái)拒絕外部請(qǐng)求
* @return
*/
@GetMapping("refuse")
public String refuse() {
AvailabilityChangeEvent.publish(applicationEventPublisher, this, ReadinessState.REFUSING_TRAFFIC);
return "success refuse, " + new Date();
}
}
5. 制作 docker 鏡像
在pom.xml所在目錄創(chuàng)建文件Dockerfile扣典,內(nèi)容如下:
# 指定基礎(chǔ)鏡像妆毕,這是多階段構(gòu)建的前期階段
FROM openjdk:11-jre-slim as builder
# 指定工作目錄,目錄不存在會(huì)自動(dòng)創(chuàng)建
WORKDIR /app
# 將生成的 jar 復(fù)制到容器鏡像中
COPY target/*.jar application.jar
# 通過(guò)工具spring-boot-jarmode-layertools從application.jar中提取拆分后的構(gòu)建結(jié)果
RUN java -Djarmode=layertools -jar application.jar extract
# 正式構(gòu)建鏡像
FROM openjdk:11-jre-slim
# 指定工作目錄贮尖,目錄不存在會(huì)自動(dòng)創(chuàng)建
WORKDIR /app
# 前一階段從jar中提取除了多個(gè)文件笛粘,這里分別執(zhí)行COPY命令復(fù)制到鏡像空間中,每次COPY都是一個(gè)layer
COPY --from=builder app/dependencies ./
COPY --from=builder app/spring-boot-loader ./
COPY --from=builder app/snapshot-dependencies ./
COPY --from=builder app/application ./
# 指定時(shí)區(qū)
ENV TZ="Asia/Shanghai"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 定義一些環(huán)境變量湿硝,方便環(huán)境變量傳參
ENV JVM_OPTS=""
ENV JAVA_OPTS=""
# 指定暴露的端口薪前,起到說(shuō)明的作用,不指定也會(huì)暴露對(duì)應(yīng)端口
EXPOSE 8080
# 啟動(dòng) jar 的命令
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]
使用以下命令編譯構(gòu)建項(xiàng)目:
mvn clean package -U -DskipTests
使用以下命令構(gòu)建 docker 鏡像(最后有一個(gè) .
表示當(dāng)前目錄作為docker構(gòu)建的上下文環(huán)境):
docker build -t probedemo:1.0.0 .
使用下面的命令將 docker 鏡像推送到遠(yuǎn)程倉(cāng)庫(kù)(這里推送到docker hub倉(cāng)庫(kù)关斜,需要自己注冊(cè)一個(gè)賬號(hào)):
# 給鏡像打一個(gè)標(biāo)簽示括,[倉(cāng)庫(kù)地址/鏡像名:鏡像標(biāo)簽]
docker tag probedemo:1.0.0 wangedison98/probedemo:1.0.0
# 推送到遠(yuǎn)程倉(cāng)庫(kù)
docker push wangedison98/probedemo:1.0.0
6. k8s 部署 deployment 和 service
創(chuàng)建名為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
這里要重點(diǎn)關(guān)注的是 livenessProbe
的 initialDelaySeconds
和 failureThreshold
參數(shù),initialDelaySeconds
等于5蚤吹,表示 pod 創(chuàng)建5秒后檢查存活探針例诀,如果10秒內(nèi)應(yīng)用沒(méi)有完成啟動(dòng)随抠,存活探針不返回200裁着,就會(huì)重試10次(failureThreshold等于10)繁涂,每一次等待 5 秒(periodSeconds 等于5),如果重試10次二驰,也就是50秒后扔罪,存活探針依舊無(wú)法返回200,該pod就會(huì)被kubernetes殺死重建桶雀,要是每次啟動(dòng)都耗時(shí)這么長(zhǎng)矿酵,pod就會(huì)不停的被殺死重建,這種情況下可以考慮延長(zhǎng) failureThreshold
失敗重試的次數(shù)矗积。
使用如下命令創(chuàng)建 deployment 和 service:
kubectl apply -f probedemo.yaml
查看運(yùn)行的 pod:
使用如下命令暴露服務(wù)端口:
kubectl port-forward service/probedemo 8080 8080
調(diào)用存活性檢查的 broken 事件全肮,地址如下:
curl http://localhost:8080/state/broken
等待大概一分鐘,發(fā)現(xiàn) pod 已經(jīng)重啟一次
請(qǐng)求拒絕流量棘捣,地址如下:
curl http://localhost:8080/state/refuse
可以看到服務(wù)已經(jīng)處于未準(zhǔn)備狀態(tài):
查看 pod 的事件:
kubectl describe probedemo-86cb7cc84b-djrjn
當(dāng)再次調(diào)用接受流量的請(qǐng)求:
curl http://localhost:8080/state/accept
發(fā)現(xiàn)服務(wù)已經(jīng)恢復(fù)正常:
根據(jù)這個(gè)特性辜腺,可以通過(guò)程序控制什么時(shí)候?qū)ν馓峁┓?wù),當(dāng)處理一些異常情況時(shí)乍恐,可以手動(dòng)拒絕請(qǐng)求评疗,待恢復(fù)正常后再提供服務(wù)。
三茵烈、總結(jié)
通過(guò)上面的實(shí)踐百匆,我們測(cè)試了spring boot 應(yīng)用在 k8s 中的健康檢查,配置非常簡(jiǎn)單:
- 只需要引入
spring-boot-starter-actuator
依賴(lài)即可呜投,不需要其他額外配置 - 在 k8s 的部署清單中根據(jù)官方文檔做如下配置:
參考文章
https://blog.csdn.net/boling_cavalry/article/details/106607225