nacos优雅停机
# 需求
服务提供方关闭时,主动从nacos下线,并且服务调用方能感知到,不再调用这个服务。
# 实现方案
# 需要满足的条件
- 服务下线接口 : nacos提供了 服务上下线的接口,在服务关闭时主动调用下线接口
- 服务关闭感知的方法 :
- 方案一 : 添加钩子 Shutdown Hook , 钩子执行的时间比web容器关闭的时间要晚,会导致服务下线滞后于web容器关闭,这个不符合我们的需求,弃用
- 方案二:监听Spring 容器的关闭事件 ContextClosedEvent ,发布事件先于web容器关闭,服务我们的要求。
# 服务提供方实现(provider)
- 监听Spring容器【ContextClosedEvent】事件,当关闭服务会收到这个事件
@Slf4j
public class ApplicationShutdownListener implements ApplicationListener<ContextClosedEvent> {
/**
* 优雅停机等待时间
*/
@Value("${spring.lifecycle.timeout-per-shutdown-phase:5}")
private Integer timeout;
@Resource
private NacosAutoServiceRegistration nacosAutoServiceRegistration;
/**
* 监听关闭事件
* @param event 关闭容器事件
*/
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("[nacos]开始下线。。。");
nacosAutoServiceRegistration.destroy();
ThreadUtil.sleep(timeout, TimeUnit.SECONDS);
log.info("[nacos]开始完成。。。");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 服务调用方实现(client)
客户端比较坑的地方是 spring-cloud-starter-loadbalancer
有本地缓存,所以服务提供方下线了,并不会马上感知到,所以需要实现监听服务变更事件,发现变更的时候清空缓存,当再次发起调用时客户端发现没有缓存会再次去拉去nacos的最新配置。
defaultLoadBalancerCacheManager实现了Spring的缓存接口,所以我们直接找到CacheManager
的实例去清空缓存即可。
@Slf4j
public class NacosInstancesChangeEventListener extends Subscriber<InstancesChangeEvent> {
@Resource
private CacheManager defaultLoadBalancerCacheManager;
@PostConstruct
public void registerToNotifyCenter(){
NotifyCenter.registerSubscriber(this);
}
@Override
public void onEvent(InstancesChangeEvent event) {
log.info("【nacos】接收微服务实例刷新事件:{}, 开始刷新本地存储的微服务实例信息的缓存", JacksonUtils.toJson(event));
Cache cache = defaultLoadBalancerCacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
if (cache != null) {
String key = StrUtil.subAfter(event.getServiceName(),"@@",false);
log.info("【nacos】开始清空缓存, 服务 ={} ,", key);
cache.evict(key);
log.info("【nacos】清空缓存完成, 服务 ={} ,", key);
}
log.info("【nacos】实例刷新完成");
}
@Override
public Class<? extends com.alibaba.nacos.common.notify.Event> subscribeType() {
return InstancesChangeEvent.class;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 启动脚本
如果你使用的是脚本的启动方式,一般会有 start ,stop , restart
这几个方法,stop方法我们不能使用 kill -9 ${pid}
,这种方式无法正常停止程序,应该使用 kill ${pid}
。
#此u处修改脚本名称:
APP_NAME=app.jar
#脚本菜单项
usage() {
echo "Usage: sh 脚本名.sh [start|stop|restart|status]"
exit 1
}
is_exist(){
pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}' `
#如果不存在返回1,存在返回0
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}
#启动脚本
start(){
echo "start.................."
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is already running. pid=${pid} ."
else
#此处注意修改jar和log文件文件位置:
nohup /usr/local/java/bin/java -Xms512M -Xmx2048M -jar ./${APP_NAME}.jar --spring.profiles.active=dev > /dev/null 2>&1 &
fi
}
#停止脚本
stop(){
is_exist
if [ $? -eq "0" ]; then
kill $pid
while true
do
is_exist
if [[ "$?" -eq 1 ]]; then
echo "do stop ok ! "
break;
fi
echo "do stop ing"
sleep 0.1
done
else
echo "${APP_NAME} is not running"
fi
}
#显示当前jar运行状态
status(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is running. Pid is ${pid}"
else
echo "${APP_NAME} is NOT running."
fi
}
#重启脚本
restart(){
stop
start
}
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
上次更新: 2024/03/26, 14:40:10