容器环境—nacos 注册中心自动上下负载(优化)
大家好,我是烤鸭:
年初已经写了一篇 https://blog.csdn.net/Angry_Mills/article/details/122745072,事实证明在生产使用还是有些问题,算是优化了下。
懒惰了3个多月,也该学习了。
api问题
之前使用的api有点问题,使用下线api,服务在nacos注册中心列表就看不到了,期望是列表中还在,只是显示的状态是"下线"。
nacos-client 1.2.1 的版本。
老api
nacosRegistration.getNacosNamingService().deregisterInstance(serviceName, groupName, ip, port, clusterName);
新api
Instance instance = getNacosInstance(nacosDiscoveryProperties.getIp(),port);
instance.setEnabled(false);
nacosDiscoveryProperties.namingMaintainServiceInstance()
.updateInstance(serviceName,groupName, instance);
nacos-client 1.4.2 的版本。
Instance instance = getNacosInstance(ip, port);
instance.setEnabled(false);
Properties nacosProperties = nacosDiscoveryProperties.getNacosProperties();
nacosServiceManager.getNamingMaintainService(nacosProperties).updateInstance(
serviceName, nacosDiscoveryProperties.getGroup(), instance);
上线问题
服务启动就自动上线会导致第一次请求或者数据库查询初始化响应耗时高,期望是服务启动后,进行预热,预热完成后进行上负载。
本来想把预热写到公共包里,不过考虑到每个服务需要预热的场景不一样,比如 数据库、缓存、一些工具类或者池化的初始化,后续应该可以优化后配置预热类或者方法,采用反射的方式进行预热。
@RequestMapping("/register2nacos")
public String register2nacos(HttpServletRequest request) {
// 1.0 初始化数据库
// TODO
// 1.1 初始化redis
// 1.2 初始化mq
// 1.3 手动上线
return "成功";
}
上面的初始化逻辑可以自己写,比如mq发送一条测试消息后,再进行上线。
下线后再上线问题
采用上面的方式后,发现在我们的系统里无法使用,原因是使用的容器的就绪时调用方法,运维反馈是个循环调用的方法。
本来期望是循环调用探活,探活成功后再进行上线,同时将老节点下线。不过这种就会老节点下线后又被探活上线的问题。
插件里增加两个字段,需要业务方法里自己判断。(源码:https://gitee.com/fireduck_admin/nacos-ez-updown)
/**
* 是否需要手动上线/下线
*/
private volatile AtomicBoolean nedStandUp = new AtomicBoolean(true);
/**
* 当前在线状态
*/
private volatile AtomicBoolean byStandUp = new AtomicBoolean(false);
public synchronized Boolean getNedStandUp() {
return nedStandUp.get();
}
public synchronized void setNedStandUp(Boolean nedStandUp) {
this.nedStandUp.set(nedStandUp);
}
public synchronized Boolean getByStandUp() {
return byStandUp.get();
}
public synchronized void setByStandUp(Boolean byStandUp) {
this.byStandUp.set(byStandUp);
}
修改后业务方法如下:
@RequestMapping("/register2nacos")
public String register2nacos(HttpServletRequest request) {
// 1.0 初始化数据库
// TODO
// 1.1 初始化redis
// 1.2 初始化mq
// 1.3 手动上线
// 强制上线
if (request.getHeader("force") != null) {
gracefulRefresh.up();
} else {
// 当前下线状态 && 当前可以上线
if (!gracefulRefresh.getByStandUp() && gracefulRefresh.getNedStandUp()) {
gracefulRefresh.up();
}
}
return "成功";
}
业务方法别忘了改为由插件注册:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NiNzSHX9-1665400793859)(.\1.png)]
部分已知问题
尽管使用了这种方式,还是会有少量请求500的情况。猜测是请求没有处理完,容器就把服务kill之后导致的5xx,这里抄一下k8s的服务延缓终止流程。
我们这里使用的自己的注册中心,也就是方式2。手动配置上线和下线。
下线是在容器销毁之前的生命周期执行,上线是在服务准备就绪后接口上线。
问了运维,如果想解决这个问题,可以使用下面的方式(待验证)。
- 生命周期sleep 时间+90s
- 滚动策略 1%(每次只升级一个pod)
- 延缓终止 120s
待修复
由于增加了变量,引入jar包后无法使用原来的自动注册,也就是必须手动注册。
这是个bug需要修复。
nacos服务下线源码解析
这个人写的挺好的,就不重复造了。
【nacos源码解析——服务注册】https://blog.csdn.net/ZXMSH/article/details/125246790
【nacos源码解析——服务下线】https://blog.csdn.net/ZXMSH/article/details/125248011
升级优化
今天同事写了一个配置类,可以完成上线的预热和下线的线程池管理,感谢挺好的, 拿出来分享下。
搭配前面的插件,避免手写接口,二次下负载,避免流量没下掉的情况。
package com.mys.maggie.demo.config;
import com.mys.maggie.nacos.plugin.refresh.GracefulRefresh;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.SmartLifecycle;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.Executor;
@Component
@Slf4j
public class ExitConfig implements SmartLifecycle {
/**
* 自定义线程池bean,没有的话可以删掉
*/
@Autowired
@Qualifier("executor")
private Executor executor;
@Autowired
private RestTemplate restTemplate;
@Autowired
GracefulRefresh gracefulRefresh;
@Value("${server.port}")
private int port;
@Value("${server.servlet.context-path:/}")
private String contextPath;
private boolean running = false;
private String pauseUrl = "http://localhost:%s%s/actuator/graceful-pause";
/**
* 返回Integer.MAX_VALUE表示
* 我们将是第一个关闭的 bean 和最后一个启动的 bean
* 容器初始化后,最后调用start()方法
* 容器销毁时的第一时间调用stop(Runnable callback)方法
* 类似于优先级的概念
*/
@Override
public int getPhase(){
return Integer.MAX_VALUE;
}
/**
* 这里可以添加容器初始化就绪后的检查逻辑
*/
@Override
public void start() {
log.info("ExitConfig_info 执行 start() 方法");
// TODO 预热部分
// 执行上线
gracefulRefresh.up();
this.running = true;
}
/**
* 只有该方法返回false时,start方法才会被执行
* 只有该方法返回true时,stop(Runnable callback)或stop()方法才会被执
*/
@Override
public boolean isRunning() {
log.info("ExitConfig_info 执行 isRunning() 方法");
return this.running;
}
/**
* 实现SmartLifecycle接口时,容器销毁执行stop(Runnable callback)方法
* 实现Lifecycle接口时,容器销毁执行stop()方法
* 这里是优雅关闭逻辑
*/
@Override
public void stop(Runnable callback) {
log.info("ExitConfig_info 执行 stop(Runnable callback) 方法");
stop();
callback.run();
}
@Override
public void stop() {
log.info("ExitConfig_info 执行 stop() 方法");
log.info("ExitConfig_info 服务下负载。。。");
try {
String url = String.format(pauseUrl, port, contextPath);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String result = restTemplate.postForObject(url, new HttpEntity<>(null, headers), String.class);
log.info("ExitConfig_info 下负载结果 result:{}", result);
//下负载后等待30s,nacos中心节点状态更新可能有延迟
Thread.sleep(30000);
} catch (Exception e) {
log.warn("ExitConfig_info 退出异常", e);
}
//睡5秒,保证普通线程执行完毕
log.info("ExitConfig_info 等待普通线程。。。");
try {
Thread.sleep(5000);
} catch (Exception e) {
log.warn("ExitConfig_info 退出异常", e);
}
//TODO 非必须
log.info("ExitConfig_info 检查自定义线程池。。。");
ThreadPoolTaskExecutor taskExecutor = (ThreadPoolTaskExecutor) executor;
int num = 0;
while (true){
int activeCount = taskExecutor.getActiveCount();
log.info("ExitConfig_info 计次:{}, taskExecutor activeCount:{}", num, activeCount);
if(activeCount == 0){
break;
}
num++;
try {
Thread.sleep(3000);
}catch (Exception e){
log.warn("ExitConfig_info 退出异常", e);
}
}
log.info("ExitConfig_info 退出!");
}
}