问题场景

image.png
服务提供方采用了 SpringCloud Alibaba 框架,将服务注册到 Nacos 上,部署采用的是 k8s + docker 容器部署。当服务升级发布时,新的 pod 被创建——Nacos 上服务实例加 1,旧的 pod 被关闭——Nacos 上服务实例减 1。

Nacos 通过心跳检测机制,将旧的 pod 实例下线。

在关闭旧的 pod 到下线 pod 之间,存在一定的时间差,导致服务消费方调用接口会发生报错的情况,对业务操作造成一定影响。

向 Nacos 上注册服务实例流程

Spring Cloud Nacos Discovery 遵循了 spring cloud common 标准,实现了 AutoServiceRegistrationServiceRegistryRegistration 这三个接口。

基于 2.0.0 版本,源码分析如下。

1、入口 NacosServiceRegistry

AbstractAutoServiceRegistration 中会执行 serviceRegistry.register(registration),注册实现在 NacosServiceRegistry.register() 方法中,关键代码如下。


String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
Instance instance = this.getNacosInstanceFromRegistration(registration);
this.namingService.registerInstance(serviceId, group, instance);

2、NacosNamingService

NacosNamingService 关键代码,主要逻辑是添加心跳检测和注册。


public void registerInstance(String serviceName, String groupName, Instance instance)  {
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            long instanceInterval = instance.getInstanceHeartBeatInterval();
            beatInfo.setPeriod(instanceInterval == 0L ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
            this.beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }
        this.serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }

3、NamingProxy

serverProxy.registerService() 中组装参数通过 HTTP 调用 API。


public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
        Map<String, String> params = new HashMap(9);
        params.put("namespaceId", this.namespaceId);
        params.put("serviceName", serviceName);
        params.put("groupName", groupName);
        params.put("clusterName", instance.getClusterName());
        params.put("ip", instance.getIp());
        params.put("port", String.valueOf(instance.getPort()));
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JSON.toJSONString(instance.getMetadata()));
        this.reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, (String)"POST");
    }

从 Nacos 上下线服务实例流程

通过心跳检测,Nacos 发现实例不存在后,自动下线掉此实例。
NacosNamingService 同时提供了主动下线的接口,代码如下。


public void deregisterInstance(String serviceName, String groupName, String ip, int port, String clusterName) throws NacosException {
        Instance instance = new Instance();
        instance.setIp(ip);
        instance.setPort(port);
        instance.setClusterName(clusterName);
        this.deregisterInstance(serviceName, groupName, instance);
    }

其中处理逻辑是去除客户端心跳检测和下线。deregisterService 与 registerService 逻辑相仿,组装参数通过 HTTP 调用执行下线。


public void deregisterInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        if (instance.isEphemeral()) {
            this.beatReactor.removeBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), instance.getIp(), instance.getPort());
        }
        this.serverProxy.deregisterService(NamingUtils.getGroupedName(serviceName, groupName), instance);
    }

容器优雅停机方式

Nacos 客户端提供了下线接口,就可以封装一个应用系统的接口来执行下线。


@GetMapping(value = "/api/nacos/deregister")
    public String deregisterInstance() {
        String serviceName = nacosDiscoveryProperties.getService();
        String groupName = nacosDiscoveryProperties.getGroup();
        String clusterName = nacosDiscoveryProperties.getClusterName();
        String ip = nacosDiscoveryProperties.getIp();
        int port = nacosDiscoveryProperties.getPort();
        log.info("deregister from nacos, serviceName:{}, groupName:{}, clusterName:{}, ip:{}, port:{}", serviceName, groupName, clusterName, ip, port);
        try {
            nacosRegistration.getNacosNamingService().deregisterInstance(serviceName, groupName, ip, port, clusterName);
        } catch (NacosException e) {
            log.error("deregister from nacos error", e);
            return "error";
        }
        return "success";
    }

在 Pod 关闭前设置一个 preStop 钩子,在 preStop 脚本中执行主动从 Nacos 下线本机实例, sleep 25 秒后再执行 Pod 的销毁,从而实现优雅停机。


# 调用从nacos下线接口
curl http://localhost:8081/api/nacos/deregister
# 延迟发送关闭信号到容器进程
sleep 25