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