服务提供方采用了 SpringCloud Alibaba 框架,将服务注册到 Nacos 上,部署采用的是 k8s + docker 容器部署。当服务升级发布时,新的 pod 被创建——Nacos 上服务实例加 1,旧的 pod 被关闭——Nacos 上服务实例减 1。
Nacos 通过心跳检测机制,将旧的 pod 实例下线。
在关闭旧的 pod 到下线 pod 之间,存在一定的时间差,导致服务消费方调用接口会发生报错的情况,对业务操作造成一定影响。
Spring Cloud Nacos Discovery 遵循了 spring cloud common 标准,实现了 AutoServiceRegistration
、ServiceRegistry
、Registration
这三个接口。
基于 2.0.0 版本,源码分析如下。
在 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);
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);
}
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 发现实例不存在后,自动下线掉此实例。
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