在 K8s + Nacos 的集群中实现单节点滚动部署

公司项目使用 Spirng Cloud Alibaba,使用 Nacos 注册中心,部署在华为云的 CCE(基于 K8s)中。为节约资源,开发测试环境的服务只部署了一个节点。这时出现一个问题:每次节点部署,系统就会出现服务短暂不可用的情况。

原因:K8s 支持滚动部署,但如果只有一个节点,容器中 Java 应用有启动时间,从启动到成功注册进 Nacos 有一个时间间隔,不是马上就可用。此时 K8s 杀掉老的服务,并将流量切到还没成功注册进 Nacos 中的新服务时,服务请求不能处理,服务就不可用。

解决方式就是加一个就绪指针,当时新服务已成功注册成功到 Nacos 中时,再杀掉老服务,将流量切换到新服务。

就绪检查方式有很多种,我选择的是「HTTP 请求检查」。所以我需要为每个服务添加一个「就绪接口」,用于判断服务是否已成功注册进 Nacos。我们服务有很多个,我不想每个服务添加一个相同作用的接口,想对当前业务无侵入。我利用 starter + spring-boot-actuator 实现。

新建一个监控 starter,starter 中添加自定义 spring-boot-actuator 端点,让部署服务引用 starter。

starter 核心代码:

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
* Description: 自己定义就绪指针处理逻辑
* Author: xingquan.wang
* Date: 17/10/2023
*/
@Component("nacosReadiness")
public class ReadinessHealthIndicator implements HealthIndicator {

@Autowired
DiscoveryClient discoveryClient;

@Value("${spring.application.name}")
private String applicationName;

@Override
public Health health() {
if (!isReady()) {
return Health.outOfService().down().build();
}
return Health.up().build();
}

/**
* 当前服务已注册进 Nacos,且 Nacos 中已有实例信息,即为就绪状态
* @return
*/
private boolean isReady() {
// 已注册返回 true;否则返回 false
InetAddress localhost;
try {
localhost = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
// 判断当前应用实例的 IP 是否与名为当前应用的已注册实例的 IP 相同,如果相同,则代表已注册成功
return discoveryClient.getInstances(applicationName).stream()
.anyMatch(instance -> instance.getHost().equals(localhost.getHostAddress()));
}
}

K8s 具体配置:

readinessProbe:
failureThreshold: 10 # 当就绪探测失败 10 次后将 Pod 标记为不可用
httpGet:
path: /actuator/health/nacosReadiness
port: 48088
scheme: HTTP
initialDelaySeconds: 60 # 容器启动后 60 秒开始进行首次就绪检查
periodSeconds: 10 # 每10秒执行一次就绪检查
successThreshold: 1 # 只要有 1 次成功,就将 Pod 标记为可用
timeoutSeconds: 1 # 就绪检查的超时时间为 1s。如果在此时间内没有收到响应,则将其视为失败

注意:不要设置启动探针(startupProbe),启动探针在检测失败时,会重启容器。存活探针(livenessProbe)首次检查时间也要设置长一点,要预估在服务已就绪之后。

Depp Wang wechat
个人公众号