58.Nacos源码分析2

三、服务心跳。

3.服务心跳

Nacos的实例分为临时实例和永久实例两种,可以通过在yaml 文件配置:

spring:application:name: order-servicecloud:nacos:discovery:ephemeral: false # 设置实例为永久实例。true:临时; false:永久server-addr: 192.168.150.1:8845

临时实例基于心跳方式做健康检测,而永久实例则是由Nacos主动探测实例状态。

其中Nacos提供的心跳的API接口为:

接口描述:发送某个实例的心跳

请求类型:PUT

请求路径

/nacos/v1/ns/instance/beat

请求参数

名称类型是否必选描述
serviceName字符串服务名
groupName字符串分组名
ephemeralboolean是否临时实例
beatJSON格式字符串实例心跳内容

错误编码

错误代码描述语义
400Bad Request客户端请求中的语法错误
403Forbidden没有权限
404Not Found无法找到资源
500Internal Server Error服务器内部错误
200OK正常

3.1.客户端

在2.2.4.服务注册这一节中,我们说过NacosNamingService这个类实现了服务的注册,同时也实现了服务心跳:

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {NamingUtils.checkInstanceIsLegal(instance);String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);// 判断是否是临时实例。if (instance.isEphemeral()) {// 如果是临时实例,则构建心跳信息BeatInfoBeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);// 添加心跳任务beatReactor.addBeatInfo(groupedServiceName, beatInfo);}serverProxy.registerService(groupedServiceName, groupName, instance);
}

3.1.1.BeatInfo

这里的BeanInfo就包含心跳需要的各种信息:

3.1.2.BeatReactor

BeatReactor这个类则维护了一个线程池:

当调用BeatReactor.addBeatInfo(groupedServiceName, beatInfo)方法时,就会执行心跳:

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());BeatInfo existBeat = null;//fix #1733if ((existBeat = dom2Beat.remove(key)) != null) {existBeat.setStopped(true);}dom2Beat.put(key, beatInfo);// 利用线程池,定期执行心跳任务,周期为 beatInfo.getPeriod()executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

心跳周期的默认值在com.alibaba.nacos.api.common.Constants类中:

可以看到是5秒,默认5秒一次心跳。

3.1.3.BeatTask

心跳的任务封装在BeatTask这个类中,是一个Runnable,其run方法如下:

@Override
public void run() {if (beatInfo.isStopped()) {return;}// 获取心跳周期long nextTime = beatInfo.getPeriod();try {// 发送心跳JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);long interval = result.get("clientBeatInterval").asLong();boolean lightBeatEnabled = false;if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();}BeatReactor.this.lightBeatEnabled = lightBeatEnabled;if (interval > 0) {nextTime = interval;}// 判断心跳结果int code = NamingResponseCode.OK;if (result.has(CommonParams.CODE)) {code = result.get(CommonParams.CODE).asInt();}if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {// 如果失败,则需要 重新注册实例Instance instance = new Instance();instance.setPort(beatInfo.getPort());instance.setIp(beatInfo.getIp());instance.setWeight(beatInfo.getWeight());instance.setMetadata(beatInfo.getMetadata());instance.setClusterName(beatInfo.getCluster());instance.setServiceName(beatInfo.getServiceName());instance.setInstanceId(instance.getInstanceId());instance.setEphemeral(true);try {serverProxy.registerService(beatInfo.getServiceName(),NamingUtils.getGroupName(beatInfo.getServiceName()), instance);} catch (Exception ignore) {}}} catch (NacosException ex) {NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
​} catch (Exception unknownEx) {NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, unknown exception msg: {}",JacksonUtils.toJson(beatInfo), unknownEx.getMessage(), unknownEx);} finally {executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);}
}

3.1.5.发送心跳

最终心跳的发送还是通过NamingProxysendBeat方法来实现:

public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
​if (NAMING_LOGGER.isDebugEnabled()) {NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());}// 组织请求参数Map<String, String> params = new HashMap<String, String>(8);Map<String, String> bodyMap = new HashMap<String, String>(2);if (!lightBeatEnabled) {bodyMap.put("beat", JacksonUtils.toJson(beatInfo));}params.put(CommonParams.NAMESPACE_ID, namespaceId);params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());params.put("ip", beatInfo.getIp());params.put("port", String.valueOf(beatInfo.getPort()));// 发送请求,这个地址就是:/v1/ns/instance/beatString result = reqApi(UtilAndComs.nacosUrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT);return JacksonUtils.toObj(result);
}

3.2.服务端

对于临时实例,服务端代码分两部分:

  • 1)InstanceController提供了一个接口,处理客户端的心跳请求

  • 2)定时检测实例心跳是否按期执行

3.2.1.InstanceController

与服务注册时一样,在nacos-naming模块中的InstanceController类中,定义了一个方法用来处理心跳请求:

@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {// 解析心跳的请求参数ObjectNode result = JacksonUtils.createEmptyJsonNode();result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());
​String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);RsInfo clientBeat = null;if (StringUtils.isNotBlank(beat)) {clientBeat = JacksonUtils.toObj(beat, RsInfo.class);}String clusterName = WebUtils.optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME);String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));if (clientBeat != null) {if (StringUtils.isNotBlank(clientBeat.getCluster())) {clusterName = clientBeat.getCluster();} else {// fix #2533clientBeat.setCluster(clusterName);}ip = clientBeat.getIp();port = clientBeat.getPort();}String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);NamingUtils.checkServiceNameFormat(serviceName);Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName);// 尝试根据参数中的namespaceId、serviceName、clusterName、ip、port等信息// 从Nacos的注册表中 获取实例Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);// 如果获取失败,说明心跳失败,实例尚未注册if (instance == null) {if (clientBeat == null) {result.put(CommonParams.CODE, NamingResponseCode.RESOURCE_NOT_FOUND);return result;}
​Loggers.SRV_LOG.warn("[CLIENT-BEAT] The instance has been removed for health mechanism, "+ "perform data compensation operations, beat: {}, serviceName: {}", clientBeat, serviceName);// 这里重新注册一个实例instance = new Instance();instance.setPort(clientBeat.getPort());instance.setIp(clientBeat.getIp());instance.setWeight(clientBeat.getWeight());instance.setMetadata(clientBeat.getMetadata());instance.setClusterName(clusterName);instance.setServiceName(serviceName);instance.setInstanceId(instance.getInstanceId());instance.setEphemeral(clientBeat.isEphemeral());
​serviceManager.registerInstance(namespaceId, serviceName, instance);}// 尝试基于namespaceId和serviceName从 注册表中获取Service服务Service service = serviceManager.getService(namespaceId, serviceName);// 如果不存在,说明服务不存在,返回404if (service == null) {throw new NacosException(NacosException.SERVER_ERROR,"service not found: " + serviceName + "@" + namespaceId);}if (clientBeat == null) {clientBeat = new RsInfo();clientBeat.setIp(ip);clientBeat.setPort(port);clientBeat.setCluster(clusterName);}// 如果心跳没问题,开始处理心跳结果service.processClientBeat(clientBeat);
​result.put(CommonParams.CODE, NamingResponseCode.OK);if (instance.containsMetadata(PreservedMetadataKeys.HEART_BEAT_INTERVAL)) {result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, instance.getInstanceHeartBeatInterval());}result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());return result;
}

最终,在确认心跳请求对应的服务、实例都在的情况下,开始交给Service类处理这次心跳请求。调用了Service的processClientBeat方法

3.2.2.处理心跳请求

查看Serviceservice.processClientBeat(clientBeat);方法:

public void processClientBeat(final RsInfo rsInfo) {ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor();clientBeatProcessor.setService(this);clientBeatProcessor.setRsInfo(rsInfo);HealthCheckReactor.scheduleNow(clientBeatProcessor);
}

可以看到心跳信息被封装到了 ClientBeatProcessor类中,交给了HealthCheckReactor处理,HealthCheckReactor就是对线程池的封装,不用过多查看。

关键的业务逻辑都在ClientBeatProcessor这个类中,它是一个Runnable,其中的run方法如下:

@Override
public void run() {Service service = this.service;if (Loggers.EVT_LOG.isDebugEnabled()) {Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());}
​String ip = rsInfo.getIp();String clusterName = rsInfo.getCluster();int port = rsInfo.getPort();// 获取集群信息Cluster cluster = service.getClusterMap().get(clusterName);// 获取集群中的所有实例信息List<Instance> instances = cluster.allIPs(true);
​for (Instance instance : instances) {// 找到心跳的这个实例if (instance.getIp().equals(ip) && instance.getPort() == port) {if (Loggers.EVT_LOG.isDebugEnabled()) {Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString());}// 更新实例的最后一次心跳时间 lastBeatinstance.setLastBeat(System.currentTimeMillis());if (!instance.isMarked()) {if (!instance.isHealthy()) {instance.setHealthy(true);Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",cluster.getService().getName(), ip, port, cluster.getName(),UtilsAndCommons.LOCALHOST_SITE);getPushService().serviceChanged(service);}}}}
}

处理心跳请求的核心就是更新心跳实例的最后一次心跳时间,lastBeat,这个会成为判断实例心跳是否过期的关键指标!

3.3.3.心跳异常检测

在服务注册时,一定会创建一个Service对象,而Service中有一个init方法,会在注册时被调用:

public void init() {// 开启心跳检测的任务HealthCheckReactor.scheduleCheck(clientBeatCheckTask);for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {entry.getValue().setService(this);entry.getValue().init();}
}

其中HealthCheckReactor.scheduleCheck就是执行心跳检测的定时任务:

可以看到,该任务是5000ms执行一次,也就是5秒对实例的心跳状态做一次检测。

此处的ClientBeatCheckTask同样是一个Runnable,其中的run方法为:

@Override
public void run() {try {// 找到所有临时实例的列表List<Instance> instances = service.allIPs(true);
​// first set health status of instances:for (Instance instance : instances) {// 判断 心跳间隔(当前时间 - 最后一次心跳时间) 是否大于 心跳超时时间,默认15秒if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {if (!instance.isMarked()) {if (instance.isHealthy()) {// 如果超时,标记实例为不健康 healthy = falseinstance.setHealthy(false);// 发布实例状态变更的事件getPushService().serviceChanged(service);ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));}}}}
​if (!getGlobalConfig().isExpireInstance()) {return;}
​// then remove obsolete instances:for (Instance instance : instances) {
​if (instance.isMarked()) {continue;}// 判断心跳间隔(当前时间 - 最后一次心跳时间)是否大于 实例被删除的最长超时时间,默认30秒if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {// 如果是超过了30秒,则删除实例Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(),JacksonUtils.toJson(instance));deleteIp(instance);}}
​} catch (Exception e) {Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);}
​
}

其中的超时时间同样是在com.alibaba.nacos.api.common.Constants这个类中:

3.3.4.主动健康检测

对于非临时实例(ephemeral=false),Nacos会采用主动的健康检测,定时向实例发送请求,根据响应来判断实例健康状态。

入口在2.3.2小节的ServiceManager类中的registerInstance方法:

创建空服务时:

public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {// 如果服务不存在,创建新的服务createServiceIfAbsent(namespaceId, serviceName, local, null);
}

创建服务流程:

public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)throws NacosException {// 尝试获取服务Service service = getService(namespaceId, serviceName);if (service == null) {// 发现服务不存在,开始创建新服务Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);service = new Service();service.setName(serviceName);service.setNamespaceId(namespaceId);service.setGroupName(NamingUtils.getGroupName(serviceName));// now validate the service. if failed, exception will be thrownservice.setLastModifiedMillis(System.currentTimeMillis());service.recalculateChecksum();if (cluster != null) {cluster.setService(service);service.getClusterMap().put(cluster.getName(), cluster);}service.validate();// ** 写入注册表并初始化 **putServiceAndInit(service);if (!local) {addOrReplaceService(service);}}
}

关键在putServiceAndInit(service)方法中:

private void putServiceAndInit(Service service) throws NacosException {// 将服务写入注册表putService(service);service = getService(service.getNamespaceId(), service.getName());// 完成服务的初始化service.init();consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}

进入初始化逻辑:service.init(),这个会进入Service类中:

/*** Init service.*/
public void init() {// 开启临时实例的心跳监测任务HealthCheckReactor.scheduleCheck(clientBeatCheckTask);// 遍历注册表中的集群for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {entry.getValue().setService(this);// 完成集群初识化entry.getValue().init();}
}

这里集群的初始化entry.getValue().init();会进入Cluster类型的init()方法:

/*** Init cluster.*/
public void init() {if (inited) {return;}// 创建健康检测的任务checkTask = new HealthCheckTask(this);// 这里会开启对 非临时实例的 定时健康检测HealthCheckReactor.scheduleCheck(checkTask);inited = true;
}

这里的HealthCheckReactor.scheduleCheck(checkTask);会开启定时任务,对非临时实例做健康检测。检测逻辑定义在HealthCheckTask这个类中,是一个Runnable,其中的run方法:

public void run() {
​try {if (distroMapper.responsible(cluster.getService().getName()) && switchDomain.isHealthCheckEnabled(cluster.getService().getName())) {// 开始健康检测healthCheckProcessor.process(this);// 记录日志 。。。}} catch (Throwable e) {// 记录日志 。。。} finally {if (!cancelled) {// 结束后,再次进行任务调度,一定延迟后执行HealthCheckReactor.scheduleCheck(this);// 。。。}}
}

健康检测逻辑定义在healthCheckProcessor.process(this);方法中,在HealthCheckProcessor接口中,这个接口也有很多实现,默认是TcpSuperSenseProcessor

进入TcpSuperSenseProcessor的process方法:

@Override
public void process(HealthCheckTask task) {// 获取所有 非临时实例的 集合List<Instance> ips = task.getCluster().allIPs(false);
​if (CollectionUtils.isEmpty(ips)) {return;}
​for (Instance ip : ips) {// 封装健康检测信息到 BeatBeat beat = new Beat(ip, task);// 放入一个阻塞队列中taskQueue.add(beat);MetricsMonitor.getTcpHealthCheckMonitor().incrementAndGet();}
}

可以看到,所有的健康检测任务都被放入一个阻塞队列,而不是立即执行了。这里又采用了异步执行的策略,可以看到Nacos中大量这样的设计。

TcpSuperSenseProcessor本身就是一个Runnable,在它的构造函数中会把自己放入线程池中去执行,其run方法如下:

public void run() {while (true) {try {// 处理任务processTask();// ...} catch (Throwable e) {SRV_LOG.error("[HEALTH-CHECK] error while processing NIO task", e);}}
}

通过processTask来处理健康检测的任务:

private void processTask() throws Exception {// 将任务封装为一个 TaskProcessor,并放入集合Collection<Callable<Void>> tasks = new LinkedList<>();do {Beat beat = taskQueue.poll(CONNECT_TIMEOUT_MS / 2, TimeUnit.MILLISECONDS);if (beat == null) {return;}
​tasks.add(new TaskProcessor(beat));} while (taskQueue.size() > 0 && tasks.size() < NIO_THREAD_COUNT * 64);// 批量处理集合中的任务for (Future<?> f : GlobalExecutor.invokeAllTcpSuperSenseTask(tasks)) {f.get();}
}

任务被封装到了TaskProcessor中去执行了,TaskProcessor是一个Callable,其中的call方法:

@Override
public Void call() {// 获取检测任务已经等待的时长long waited = System.currentTimeMillis() - beat.getStartTime();if (waited > MAX_WAIT_TIME_MILLISECONDS) {Loggers.SRV_LOG.warn("beat task waited too long: " + waited + "ms");}SocketChannel channel = null;try {// 获取实例信息Instance instance = beat.getIp();// 通过NIO建立TCP连接channel = SocketChannel.open();channel.configureBlocking(false);// only by setting this can we make the socket close event asynchronouschannel.socket().setSoLinger(false, -1);channel.socket().setReuseAddress(true);channel.socket().setKeepAlive(true);channel.socket().setTcpNoDelay(true);
​Cluster cluster = beat.getTask().getCluster();int port = cluster.isUseIPPort4Check() ? instance.getPort() : cluster.getDefCkport();channel.connect(new InetSocketAddress(instance.getIp(), port));// 注册连接、读取事件SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);key.attach(beat);keyMap.put(beat.toString(), new BeatKey(key));
​beat.setStartTime(System.currentTimeMillis());
​GlobalExecutor.scheduleTcpSuperSenseTask(new TimeOutTask(key), CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);} catch (Exception e) {beat.finishCheck(false, false, switchDomain.getTcpHealthParams().getMax(),"tcp:error:" + e.getMessage());
​if (channel != null) {try {channel.close();} catch (Exception ignore) {}}}
​return null;
}

3.3.总结

Nacos的健康检测有两种模式:

  • 临时实例:

    • 采用客户端心跳检测模式,心跳周期5秒

    • 心跳间隔超过15秒则标记为不健康

    • 心跳间隔超过30秒则从服务列表删除

  • 永久实例:

    • 采用服务端主动健康检测方式

    • 周期为2000 + 5000毫秒内的随机数

    • 检测异常只会标记为不健康,不会删除

那么为什么Nacos有临时和永久两种实例呢?

以淘宝为例,双十一大促期间,流量会比平常高出很多,此时服务肯定需要增加更多实例来应对高并发,而这些实例在双十一之后就无需继续使用了,采用临时实例比较合适。而对于服务的一些常备实例,则使用永久实例更合适。

与eureka相比,Nacos与Eureka在临时实例上都是基于心跳模式实现,差别不大,主要是心跳周期不同,eureka是30秒,Nacos是5秒。

另外,Nacos支持永久实例,而Eureka不支持,Eureka只提供了心跳模式的健康监测,而没有主动检测功能。

四、服务发现。

4.服务发现

Nacos提供了一个根据serviceId查询实例列表的接口:

接口描述:查询服务下的实例列表

请求类型:GET

请求路径

/nacos/v1/ns/instance/list

请求参数

名称类型是否必选描述
serviceName字符串服务名
groupName字符串分组名
namespaceId字符串命名空间ID
clusters字符串,多个集群用逗号分隔集群名称
healthyOnlyboolean否,默认为false是否只返回健康实例

错误编码

错误代码描述语义
400Bad Request客户端请求中的语法错误
403Forbidden没有权限
404Not Found无法找到资源
500Internal Server Error服务器内部错误
200OK正常

4.1.客户端

4.1.1.定时更新服务列表

4.1.1.1.NacosNamingService

在2.2.4小节中,我们讲到一个类NacosNamingService,这个类不仅仅提供了服务注册功能,同样提供了服务发现的功能。

多个重载的方法最终都会进入一个方法:

@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,boolean subscribe) throws NacosException {
​ServiceInfo serviceInfo;// 1.判断是否需要订阅服务信息(默认为 true)if (subscribe) {// 1.1.订阅服务信息serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),StringUtils.join(clusters, ","));} else {// 1.2.直接去nacos拉取服务信息serviceInfo = hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),StringUtils.join(clusters, ","));}// 2.从服务信息中获取实例列表并返回List<Instance> list;if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {return new ArrayList<Instance>();}return list;
}
4.1.1.2.HostReactor

进入1.1.订阅服务消息,这里是由HostReactor类的getServiceInfo()方法来实现的:

public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
​NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());// 由 服务名@@集群名拼接 keyString key = ServiceInfo.getKey(serviceName, clusters);if (failoverReactor.isFailoverSwitch()) {return failoverReactor.getService(key);}// 读取本地服务列表的缓存,缓存是一个Map,格式:Map<String, ServiceInfo>ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);// 判断缓存是否存在if (null == serviceObj) {// 不存在,创建空ServiceInfoserviceObj = new ServiceInfo(serviceName, clusters);// 放入缓存serviceInfoMap.put(serviceObj.getKey(), serviceObj);// 放入待更新的服务列表(updatingMap)中updatingMap.put(serviceName, new Object());// 立即更新服务列表updateServiceNow(serviceName, clusters);// 从待更新列表中移除updatingMap.remove(serviceName);
​} else if (updatingMap.containsKey(serviceName)) {// 缓存中有,但是需要更新if (UPDATE_HOLD_INTERVAL > 0) {// hold a moment waiting for update finish 等待5秒中,待更新完成synchronized (serviceObj) {try {serviceObj.wait(UPDATE_HOLD_INTERVAL);} catch (InterruptedException e) {NAMING_LOGGER.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);}}}}// 开启定时更新服务列表的功能scheduleUpdateIfAbsent(serviceName, clusters);// 返回缓存中的服务信息return serviceInfoMap.get(serviceObj.getKey());
}

基本逻辑就是先从本地缓存读,根据结果来选择:

  • 如果本地缓存没有,立即去nacos读取,updateServiceNow(serviceName, clusters)

  • 如果本地缓存有,则开启定时更新功能,并返回缓存结果:

    • scheduleUpdateIfAbsent(serviceName, clusters)

    在UpdateTask中,最终还是调用updateService方法:

不管是立即更新服务列表,还是定时更新服务列表,最终都会执行HostReactor中的updateService()方法:

public void updateService(String serviceName, String clusters) throws NacosException {ServiceInfo oldService = getServiceInfo0(serviceName, clusters);try {// 基于ServerProxy发起远程调用,查询服务列表String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
​if (StringUtils.isNotEmpty(result)) {// 处理查询结果processServiceJson(result);}} finally {if (oldService != null) {synchronized (oldService) {oldService.notifyAll();}}}
}
4.1.1.3.ServerProxy

而ServerProxy的queryList方法如下:

public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)throws NacosException {// 准备请求参数final Map<String, String> params = new HashMap<String, String>(8);params.put(CommonParams.NAMESPACE_ID, namespaceId);params.put(CommonParams.SERVICE_NAME, serviceName);params.put("clusters", clusters);params.put("udpPort", String.valueOf(udpPort));params.put("clientIP", NetUtils.localIP());params.put("healthyOnly", String.valueOf(healthyOnly));// 发起请求,地址与API接口一致return reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
}

4.1.2.处理服务变更通知

除了定时更新服务列表的功能外,Nacos还支持服务列表变更时的主动推送功能。

在HostReactor类的构造函数中,有非常重要的几个步骤:

基本思路是:

  • 通过PushReceiver监听服务端推送的变更数据

  • 解析数据后,通过NotifyCenter发布服务变更的事件

  • InstanceChangeNotifier监听变更事件,完成对服务列表的更新

4.1.2.1.PushReceiver

我们先看PushReceiver,这个类会以UDP方式接收Nacos服务端推送的服务变更数据。

先看构造函数:

public PushReceiver(HostReactor hostReactor) {try {this.hostReactor = hostReactor;// 创建 UDP客户端String udpPort = getPushReceiverUdpPort();if (StringUtils.isEmpty(udpPort)) {this.udpSocket = new DatagramSocket();} else {this.udpSocket = new DatagramSocket(new InetSocketAddress(Integer.parseInt(udpPort)));}// 准备线程池this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setDaemon(true);thread.setName("com.alibaba.nacos.naming.push.receiver");return thread;}});// 开启线程任务,准备接收变更数据this.executorService.execute(this);} catch (Exception e) {NAMING_LOGGER.error("[NA] init udp socket failed", e);}
}

PushReceiver构造函数中基于线程池来运行任务。这是因为PushReceiver本身也是一个Runnable,其中的run方法业务逻辑如下:

@Override
public void run() {while (!closed) {try {// byte[] is initialized with 0 full filled by defaultbyte[] buffer = new byte[UDP_MSS];DatagramPacket packet = new DatagramPacket(buffer, buffer.length);// 接收推送数据udpSocket.receive(packet);// 解析为json字符串String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());// 反序列化为对象PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);String ack;if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {// 交给 HostReactor去处理hostReactor.processServiceJson(pushPacket.data);
​// send ack to server 发送ACK回执,略。。} catch (Exception e) {if (closed) {return;}NAMING_LOGGER.error("[NA] error while receiving push data", e);}}
}
4.1.2.2.HostReactor

通知数据的处理由交给了HostReactorprocessServiceJson方法:

public ServiceInfo processServiceJson(String json) {// 解析出ServiceInfo信息ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);String serviceKey = serviceInfo.getKey();if (serviceKey == null) {return null;}// 查询缓存中的 ServiceInfoServiceInfo oldService = serviceInfoMap.get(serviceKey);
​// 如果缓存存在,则需要校验哪些数据要更新boolean changed = false;if (oldService != null) {// 拉取的数据是否已经过期if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "+ serviceInfo.getLastRefTime());}// 放入缓存serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);// 中间是缓存与新数据的对比,得到newHosts:新增的实例;remvHosts:待移除的实例;// modHosts:需要修改的实例if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {// 发布实例变更的事件NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),serviceInfo.getClusters(), serviceInfo.getHosts()));DiskCache.write(serviceInfo, cacheDir);}
​} else {// 本地缓存不存在changed = true;// 放入缓存serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);// 直接发布实例变更的事件NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),serviceInfo.getClusters(), serviceInfo.getHosts()));serviceInfo.setJsonFromServer(json);DiskCache.write(serviceInfo, cacheDir);}// 。。。return serviceInfo;
}

4.2.服务端

4.2.1.拉取服务列表接口

在2.3.1小节介绍的InstanceController中,提供了拉取服务列表的接口:

/*** Get all instance of input service.** @param request http request* @return list of instance* @throws Exception any error during list*/
@GetMapping("/list")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
public ObjectNode list(HttpServletRequest request) throws Exception {// 从request中获取namespaceId和serviceNameString namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);NamingUtils.checkServiceNameFormat(serviceName);
​String agent = WebUtils.getUserAgent(request);String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);// 获取客户端的 UDP端口int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));String env = WebUtils.optional(request, "env", StringUtils.EMPTY);boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false"));
​String app = WebUtils.optional(request, "app", StringUtils.EMPTY);
​String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY);
​boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));
​// 获取服务列表return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,healthyOnly);
}

进入doSrvIpxt()方法来获取服务列表:

public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent,String clusters, String clientIP,int udpPort, String env, boolean isCheck,String app, String tid, boolean healthyOnly) throws Exception {ClientInfo clientInfo = new ClientInfo(agent);ObjectNode result = JacksonUtils.createEmptyJsonNode();// 获取服务列表信息Service service = serviceManager.getService(namespaceId, serviceName);long cacheMillis = switchDomain.getDefaultCacheMillis();
​// now try to enable the pushtry {if (udpPort > 0 && pushService.canEnablePush(agent)) {// 添加当前客户端 IP、UDP端口到 PushService 中pushService.addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort),pushDataSource, tid, app);cacheMillis = switchDomain.getPushCacheMillis(serviceName);}} catch (Exception e) {Loggers.SRV_LOG.error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, udpPort, e);cacheMillis = switchDomain.getDefaultCacheMillis();}
​if (service == null) {// 如果没找到,返回空if (Loggers.SRV_LOG.isDebugEnabled()) {Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);}result.put("name", serviceName);result.put("clusters", clusters);result.put("cacheMillis", cacheMillis);result.replace("hosts", JacksonUtils.createEmptyArrayNode());return result;}// 结果的检测,异常实例的剔除等逻辑省略// 最终封装结果并返回 。。。
​result.replace("hosts", hosts);if (clientInfo.type == ClientInfo.ClientType.JAVA&& clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {result.put("dom", serviceName);} else {result.put("dom", NamingUtils.getServiceName(serviceName));}result.put("name", serviceName);result.put("cacheMillis", cacheMillis);result.put("lastRefTime", System.currentTimeMillis());result.put("checksum", service.getChecksum());result.put("useSpecifiedURL", false);result.put("clusters", clusters);result.put("env", env);result.replace("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));return result;
}

4.2.2.发布服务变更的UDP通知

在上一节中,InstanceController中的doSrvIpxt()方法中,有这样一行代码:

pushService.addClient(namespaceId, serviceName, clusters, agent,new InetSocketAddress(clientIP, udpPort),pushDataSource, tid, app);

其实是把消费者的UDP端口、IP等信息封装为一个PushClient对象,存储PushService中。方便以后服务变更后推送消息。

PushService类本身实现了ApplicationListener接口:

这个是事件监听器接口,监听的是ServiceChangeEvent(服务变更事件)。

当服务列表变化时,就会通知我们:

4.3.总结

Nacos的服务发现分为两种模式:

  • 模式一:主动拉取模式,消费者定期主动从Nacos拉取服务列表并缓存起来,再服务调用时优先读取本地缓存中的服务列表。

  • 模式二:订阅模式,消费者订阅Nacos中的服务列表,并基于UDP协议来接收服务变更通知。当Nacos中的服务列表更新时,会发送UDP广播给所有订阅者。

与Eureka相比,Nacos的订阅模式服务状态更新更及时,消费者更容易及时发现服务列表的变化,剔除故障服务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/211775.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL-备份+日志:介质故障与数据库恢复

目录 第1关&#xff1a;备份与恢复 第2关&#xff1a;备份日志&#xff1a;介质故障的发生与数据库的恢复 第1关&#xff1a;备份与恢复 任务描述 本关任务: 备份数据库&#xff0c;然后再恢复它。 test1_1.sh # 你写的命令将在linux的命令行运行 # 对数据库residents作海…

【C/C++笔试练习】多态的概念、虚函数的概念、虚表地址、派生类的虚函数、虚函数的访问、指针引用、动态多态、完全数计算、扑克牌大小

文章目录 C/C笔试练习选择部分&#xff08;1&#xff09;多态的概念&#xff08;2&#xff09;虚函数的概念&#xff08;3&#xff09;虚表地址&#xff08;4&#xff09;派生类的虚函数&#xff08;5&#xff09;虚函数的访问&#xff08;6&#xff09;分析程序&#xff08;7&…

C# WPF上位机开发(会员管理软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 好多同学都认为上位机只是纯软件开发&#xff0c;不涉及到硬件设备&#xff0c;比如听听音乐、看看电影、写写小的应用等等。如果是消费电子&#…

HibernateJPA快速搭建

1. 先创建一个普通Maven工程&#xff0c;导入依赖 <dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><depe…

Java 匿名内部类使用的外部变量,为什么一定要加 final?

问题描述 Effectively final Java 1.8 新特性&#xff0c;对于一个局部变量或方法参数&#xff0c;如果他的值在初始化后就从未更改&#xff0c;那么该变量就是 effectively final&#xff08;事实 final&#xff09;。 这种情况下&#xff0c;可以不用加 final 关键字修饰。 …

报错:Parsed mapper file: ‘file mapper.xml 导致无法启动

报错 &#xff1a; Logging initialized using class org.apache.ibatis.logging.stdout.StdOutImpl adapter. Registered plugin: com.github.yulichang.interceptor.MPJInterceptor3b2c8bda Parsed mapper file: file [/Mapper.xml] application无法启动 我这边产生原因是项…

K8S学习指南(4)-minikube的使用

文章目录 简介安装 Minikube启动 Minikube 集群基本概念创建和管理资源1. 创建 Pod2. 创建 Deployment3. 创建 Service 监视和调试1. 查看集群状态2. 查看集群信息3. 访问 Kubernetes Dashboard4. 使用 kubectl 命令 清理资源1. 删除 Pod2. 删除 Deployment3. 删除 Service4. 停…

! [remote rejected] master -> master (pre-receive hook declined)

! [remote rejected] master -> master (pre-receive hook declined) 如图&#xff1a; 出错解决方法 首先输入命令 git branch xindefenzhi然后&#xff0c;进入这个新创建的分支 git checkout branch然后重新git push就可以了

爬虫学习-基础库的使用(urllib库)

目录 一、urllib库介绍 二、request模块使用 &#xff08;1&#xff09;urlopen ①data参数 ②timeout参数 &#xff08;2&#xff09;request &#xff08;3&#xff09;高级用法 ①验证 ②代理 ③Cookie 三、处理异常 ①URLError ②HTTPError 四、解析链接 ①urlparse ②…

LeetCode-10. 正则表达式匹配

LeetCode-10. 正则表达式匹配 问题分析算法描述程序代码CGo 问题分析 这道题的难点主要在于*号的匹配&#xff0c;这里记dp[i][j]表示s[1...i]和p[1...j]能否完成匹配&#xff0c;先根据特殊情况归纳总结&#xff1a; *号匹配 0 次&#xff0c;则dp[i][j] dp[i][j-2]*号匹配…

Mybatis源码解析4:获取Session、Mapper

Mybatis源码解析4&#xff1a;获取Session、Mapper 1.项目结构2. 源码分析2.1 获取Session DefaultSqlSessionFactory#openSession2.2 获取Mapper DefaultSqlSession#getMapper 1.项目结构 2. 源码分析 2.1 获取Session DefaultSqlSessionFactory#openSession private SqlSe…

利用人工智能算法解决内存垃圾回收问题

内存垃圾回收是计算机领域中的一个重要问题&#xff0c;可以利用人工智能算法解决此问题。常用的人工智能算法包括遗传算法、模拟退火算法、禁忌搜索算法等。 其中&#xff0c;遗传算法是一种基于自然选择和遗传进化的算法&#xff0c;可以用于优化问题。在内存垃圾回收中&…

Python实战演练之Python实现一个简单的天气查询应用

今天&#xff0c;晓白给大家分享Python实现一个简单的天气查询应用&#xff0c;帮助大家获取实时的天气信息&#xff0c;内容仅供学习交流。 首先&#xff0c;我们需要安装一个名为"requests"的Python库&#xff0c;它可以帮助我们发送HTTP请求并获取响应数据。你可…

Kernel(一):基础

本文主要讨论210的kernel基础相关知识。 内核驱动 驱动是内核中的硬件设备管理模块,工作在内核态,程序故障可能导致内核崩溃,程序漏洞会使内核不安全 根文件系统提供根目录,进程存放在根文件系统中,内核启动最后会装载根文件系统 应用程序不属于内核,…

1828_ChibiOS中的对象FIFO

全部学习汇总&#xff1a; GreyZhang/g_ChibiOS: I found a new RTOS called ChibiOS and it seems interesting! (github.com) 1. 最初的这个理解&#xff0c;当看到后面之后就知道有点偏差了。其实&#xff0c;这个传输就是一个单纯的FIFO而不是两个FIFO之间的什么操作。 2.…

去掉参数中第一个“,”

记录一下&#xff0c;前端传参中&#xff0c;传给我参数是“categoryIds: ,1731557494586241026,1731569816263311362,1731569855534579713,1731858335179223042,1731858366821052418” 但是后端&#xff0c;因为我的mybati是in查询&#xff0c;所以因为第一个是“,”。所以会导…

RabbitMQ安装在Linux系统详细教程

安装教程&#xff1a; 1.首先将下载好的文件上传到服务器&#xff0c;拉到opt文件夹中(可以用xftp&#xff09; 2.输入命令&#xff1a; cd /opt 3.安装erlang rpm -ivh erlang-23.3.4.11-1.el7.x86_64.rpm rpm -ivh&#xff08;复制配置文件的名字&#xff09; 4.在Rab…

sap增强

四代增强 2种显示增强1种隐式增强 隐式增强 光标放在增强点或其中的代码点击修改即可修改代码 显示增强 1.ENHANCEMENT-POINT 在代码修改界面选择空行 光标所在位置 可以创建多个增强实施且激活后都会执行. 2.ENHANCEMENT-SECTION 1,选中程序中空行 2.编辑->创建选项 …

回顾2023 亚马逊云科技 re_Invent,创新AI,一路同行

作为全球云计算龙头企业的亚马逊云科技于2023年11月27日至12月1日在美国拉斯维加斯举办了2023 亚马逊云科技 re:Invent&#xff0c;从2012年开始举办的亚马逊云科技 re:Invent 全球大会,到现如今2023 亚马逊云科技 re:Invent&#xff0c;回顾历届re:Invent大会&#xff0c;亚马…

Spring 动态代理时是如何解决循环依赖的?为什么要使用三级缓存?

首先&#xff0c;我将简单介绍一下Spring框架中的动态代理和循环依赖问题。 动态代理与循环依赖 1. 动态代理 在Spring框架中&#xff0c;动态代理是一种常用的技术&#xff0c;用于实现AOP&#xff08;面向切面编程&#xff09;。动态代理允许Spring在运行时为目标对象创建…