nacos优雅停机

2020/1/1

点击勘误issues (opens new window),哪吒感谢大家的阅读

# nacos优雅停机

服务提供方关闭时,主动从nacos下线,并且服务调用方能感知到,不再调用这个服务。

img_7.png

# 实现方案

# 需要满足的条件

服务下线接口 : 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]开始完成。。。");
    }

}

# 服务调用方实现(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;
    }

}

# 启动脚本

如果你使用的是脚本的启动方式,一般会有 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