目录
1. 服务注册源码
1.1 客户端如何完成注册
1.2 服务端如何处理注册请求
1.3 客户端 RPC 可靠性保证
2. 服务发现源码
2.1 客户端查询服务源码
2.2 服务端如何返回结果
进入 nacos/example 模块的 NamingExamples 类中即可看到相关 API 使用示例

如下方法调用后,可注册一个服务。通过 Nacos 后台管理可查看到刚注册的服务。
naming.registerInstance("user-service", "127.0.0.2", 8888, "default");
进入该方法,经过层层包装调用,最终进入到了如下源码

该方法做了两件事:
保证此次请求的可靠性:redoService.cache 缓存此次请求信息(如何保证可靠性后面再说)
注册服务 doRegisterService
第二步的 doRegisterService 方法继续往里面看:
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {// 构建请求对象InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,NamingRemoteConstants.REGISTER_INSTANCE, instance);// 发送 RPC 请求requestToServer(request, Response.class);// 可靠性保证:记录当前注册成功的状态redoService.instanceRegistered(serviceName, groupName);
}
到这里就已经明确了,最终就是发送了一个 RPC 请求给服务端来完成注册的。
此时整体的流程已经完毕了,下面看看服务端如何处理。
服务端接受请求的处理方法在这里
com.alibaba.nacos.naming.remote.rpc.handler.InstanceRequestHandler#registerInstance

该方法做了两件事情
调用注册实例方法
发送 记录跟踪事件 TraceEvent
发送 记录跟踪事件 用于 监控调用链路
在微服务架构中,服务之间的调用非常频繁,而且调用链路很长,因此需要对服务之间的调用进行跟踪和监控,以便及时发现和解决问题
注册中心的 Trace/跟踪 事件类有如下这些

好了,事件发出去后,谁来处理呢?
这里由 SPI 机制实现

回到第一步 调用注册实例方法

看看 注册方法如何实现
public class EphemeralClientOperationServiceImpl {
@Override
public void registerInstance(Service service, Instance instance, String clientId) {// 省略部分非关键代码// 获取当前的 Service (没有就新创建)Service singleton = ServiceManager.getInstance().getSingleton(service);// 获取当前请求的 客户端Client client = clientManager.getClient(clientId);InstancePublishInfo instanceInfo = getPublishInfo(instance);// 将实例信息添加到当前客户端上client.addServiceInstance(singleton, instanceInfo);
// 发布事件 客户端已注册NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));}}
以下看两个关键的方法
将实例信息添加到当前客户端上
最终调用如下方法
public abstract class AbstractClient {ConcurrentHashMap publishers = new ConcurrentHashMap<>();
@Override
public boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {if (null == publishers.put(service, instancePublishInfo)) {if (instancePublishInfo instanceof BatchInstancePublishInfo) {MetricsMonitor.incrementIpCountWithBatchRegister(instancePublishInfo);} else {MetricsMonitor.incrementInstanceCount();}}
NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));return true;}}
也即是将注册信息存入了 Map
发布事件 客户端已注册
这个事件可以单独说明一下 ClientRegisterServiceEvent
该事件的处理方法在这里 com.alibaba.nacos.naming.core.v2.index.ClientServiceIndexesManager#addPublisherIndexes
private void addPublisherIndexes(Service service, String clientId) {publisherIndexes.computeIfAbsent(service, key -> new ConcurrentHashSet<>());publisherIndexes.get(service).add(clientId);NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true));
}
该方法的内容为:将服务注册信息存入索引map中
也就是冗余存储了,这样根据Service 直接查询出来注册该Service 的全部 客户端Id 列表
为什么注册一个服务的 client 是一个列表呢?因为一个实例可能部署多份、多台机器。
// service -> List
private final ConcurrentMap> publisherIndexes = new ConcurrentHashMap<>();
这里存起来后,查询就方便了,具体如何查询,请看 2.2 节的源码讲解。
由于网络的不稳定,RPC 请求可能失败,那么失败了就得有保障措施,比如说请求重试。
Nacos 中的服务注册的请求重试就是通过 RedoService 实现的。
其原理为:当注册服务等操作时,先将请求缓存到 map 中,然后定时任务每隔3秒检测一次,将需要重试的任务重新发起请求
本质就是这么简单,那么接下来看Nacos 如何实现。
负责请求的缓存 map 维护的类是:NamingGrpcRedoService
在调用注册服务 api 时,可见如下内容,在调用真正的发起请求方法时 先调用了 redoService.cacheInstanceForRedo 方法
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {redoService.cacheInstanceForRedo(serviceName, groupName, instance);doRegisterService(serviceName, groupName, instance);
}
解析来看看 redoService.cacheInstanceForRedo 如何实现
private final ConcurrentMap registeredInstances = new ConcurrentHashMap<>();
public void cacheInstanceForRedo(String serviceName, String groupName, Instance instance) {String key = NamingUtils.getGroupedName(serviceName, groupName);InstanceRedoData redoData = InstanceRedoData.build(serviceName, groupName, instance);synchronized (registeredInstances) {registeredInstances.put(key, redoData);}
}
好了,数据已经放进去了。
接着往下看:
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,NamingRemoteConstants.REGISTER_INSTANCE, instance);requestToServer(request, Response.class);redoService.instanceRegistered(serviceName, groupName);
}
可见,在发送完真正的 RPC 请求后,又调用了 redoService.instanceRegistered 方法。
doRegisterService 方法的意思是 发送完请求后,将上一步放入缓存map中的数据的 registered 改为 true 代表已注册成功

这里也就是说,RPC 请求发送完成后,将缓存中的数据做了一个改动,意思就是代表 缓存的这条数据 是正常的(不需要重试了)
再梳理一下这段方法:
发送 RPC 请求
改变 缓存中 的数据
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,NamingRemoteConstants.REGISTER_INSTANCE, instance);requestToServer(request, Response.class);redoService.instanceRegistered(serviceName, groupName);
}
请注意第一步:由于网络的不稳定性,所以请求请求可能失败!所以后面的状态就会设置失败,所以此时缓存中的数据还会被定时扫到并重试。
这里就是如果请求成功了,告诉定时任务不要重试了。
负责定时检测发起重试的类是:RedoScheduledTask
public class RedoScheduledTask extends AbstractExecuteTask {/*** 启动后延迟3秒后 每隔3每秒调度一次*/@Overridepublic void run() {if (!redoService.isConnected()) {// 已经断开连接,直接返回return;}// instance 的重试恢复redoForInstances();// 省略} private void redoForInstances() { // 拿到全部缓存数据for (InstanceRedoData each : redoService.findInstanceRedoData()) {// 开始 redoredoForInstance(each);}}private void redoForInstance(InstanceRedoData redoData) throws NacosException {// 需要 redo 的类型RedoData.RedoType redoType = redoData.getRedoType();String serviceName = redoData.getServiceName();String groupName = redoData.getGroupName();switch (redoType) {case REGISTER:// 需要注册if (isClientDisabled()) {return;}// 重试:发送注册服务请求processRegisterRedoType(redoData, serviceName, groupName);break;case UNREGISTER:// 需要取消注册if (isClientDisabled()) {return;}// 重试:发送卸载服务请求clientProxy.doDeregisterService(serviceName, groupName, redoData.get());break;case REMOVE:// 需要删除:删除服务redoService.removeInstanceForRedo(serviceName, groupName);break;default:}}
}
好了,这里已经完成了可靠性的保证,定时任务 RedoScheduledTask 会定时扫描 缓存 map 里的数据并做处理。
服务查询 api 调用方法如下

最终调用如下:
/*** @param subscribe 默认 true 即 订阅该服务 (当服务数据变更时,推送过来)*/
@Override
public List getAllInstances(String serviceName, String groupName, List clusters,boolean subscribe) throws NacosException {ServiceInfo serviceInfo;String clusterString = StringUtils.join(clusters, ",");if (subscribe) {serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);if (null == serviceInfo || !clientProxy.isSubscribed(serviceName, groupName, clusterString)) {// 订阅服务serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);}} else {// 调用 RPC 接口查询服务serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);}List list;if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {return new ArrayList<>();}return list;
}
可见,主要功能为 两点
订阅服务
调用 RPC 接口查询服务
订阅服务的意思是:订阅服务的变更,如果数据变更了,NacosServer 端会推送事件过来,这样 Client 的数据就会是最新的了
到底要怎么查询出服务信息呢?
服务端接受请求的方法为
com.alibaba.nacos.naming.remote.rpc.handler.ServiceQueryRequestHandler#handle
public class ServiceQueryRequestHandler extends RequestHandler {@Override
@Secured(action = ActionTypes.READ)
public QueryServiceResponse handle(ServiceQueryRequest request, RequestMeta meta) throws NacosException {String namespaceId = request.getNamespace();String groupName = request.getGroupName();String serviceName = request.getServiceName();Service service = Service.newService(namespaceId, groupName, serviceName);String cluster = null == request.getCluster() ? "" : request.getCluster();boolean healthyOnly = request.isHealthyOnly();// 获取到 ServiceInfo 信息ServiceInfo result = serviceStorage.getData(service);// 获取元数据信息ServiceMetadata serviceMetadata = metadataManager.getServiceMetadata(service).orElse(null);// 请求过滤:根据筛选条件返回符合条件的 服务数据result = ServiceUtil.selectInstancesWithHealthyProtection(result, serviceMetadata, cluster, healthyOnly, true,meta.getClientIp());return QueryServiceResponse.buildSuccessResponse(result);}}
看看 获取ServiceInfo 信息
serviceDataIndexes 里的数据是从哪里来的呢?
public ServiceInfo getData(Service service) {return serviceDataIndexes.containsKey(service) ? serviceDataIndexes.get(service) : getPushData(service);
}
这是在注册订阅时放入的
在上节中:客户端查询服务时,会先订阅一下,然后才去查询RPC 查询服务数据。
而就是在 订阅 这里完成了数据的放入

数据查出来后,接下来就是进入数据过滤的步骤。
下方的 InstancesFilter filter 就是过滤逻辑
public static ServiceInfo selectInstancesWithHealthyProtection(ServiceInfo serviceInfo, ServiceMetadata serviceMetadata, String cluster,boolean healthyOnly, boolean enableOnly, String subscriberIp) {InstancesFilter filter = (filteredResult, allInstances, healthyCount) -> {if (serviceMetadata == null) {return;}allInstances = filteredResult.getHosts();int originalTotal = allInstances.size();// filter ips using selectorSelectorManager selectorManager = ApplicationUtils.getBean(SelectorManager.class);allInstances = selectorManager.select(serviceMetadata.getSelector(), subscriberIp, allInstances);filteredResult.setHosts(allInstances);// 计算健康实例数量long newHealthyCount = healthyCount;if (originalTotal != allInstances.size()) {for (com.alibaba.nacos.api.naming.pojo.Instance allInstance : allInstances) {if (allInstance.isHealthy()) {newHealthyCount++;}}}// 获取阈值保护的配置float threshold = serviceMetadata.getProtectThreshold();if (threshold < 0) {threshold = 0F;}if ((float) newHealthyCount / allInstances.size() <= threshold) {// 触发阈值保护,返回全部的 IPLoggers.SRV_LOG.warn("protect threshold reached, return all ips, service: {}", filteredResult.getName());filteredResult.setReachProtectionThreshold(true);List filteredInstances = allInstances.stream().map(i -> {if (!i.isHealthy()) {i = InstanceUtil.deepCopy(i);// set all to `healthy` state to protect// 原本不健康的数据设置为 健康 返回回去, 实现保护i.setHealthy(true);} // else deepcopy is unnecessaryreturn i;}).collect(Collectors.toCollection(LinkedList::new));filteredResult.setHosts(filteredInstances);}};return doSelectInstances(serviceInfo, cluster, healthyOnly, enableOnly, filter);}
这里说说为什么触发阈值保护后要将 原本不健康的实例 设置为 健康的。
为什么要有阈值保护?
请看这里 阈值保护功能
为什么要将不健康的设置为健康
就是让用户直接使用上坏的,让用户直接失败
下一篇:项目重点问题