Nacos 注册中心 - 服务注册源码
创始人
2024-06-02 05:15:39

目录

1. 服务注册源码

1.1 客户端如何完成注册

1.2 服务端如何处理注册请求

1.3 客户端 RPC 可靠性保证

2. 服务发现源码

2.1 客户端查询服务源码

2.2 服务端如何返回结果


1. 服务注册源码

1.1 客户端如何完成注册

进入 nacos/example 模块的 NamingExamples 类中即可看到相关 API 使用示例

如下方法调用后,可注册一个服务。通过 Nacos 后台管理可查看到刚注册的服务。

naming.registerInstance("user-service", "127.0.0.2", 8888, "default");

进入该方法,经过层层包装调用,最终进入到了如下源码

该方法做了两件事:

  1. 保证此次请求的可靠性:redoService.cache 缓存此次请求信息(如何保证可靠性后面再说)

  2. 注册服务 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 请求给服务端来完成注册的。

此时整体的流程已经完毕了,下面看看服务端如何处理。

1.2 服务端如何处理注册请求

服务端接受请求的处理方法在这里

com.alibaba.nacos.naming.remote.rpc.handler.InstanceRequestHandler#registerInstance

该方法做了两件事情

  1. 调用注册实例方法

  2. 发送 记录跟踪事件 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 节的源码讲解。

1.3 客户端 RPC 可靠性保证

由于网络的不稳定,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 请求发送完成后,将缓存中的数据做了一个改动,意思就是代表 缓存的这条数据 是正常的(不需要重试了)

再梳理一下这段方法:

  1. 发送 RPC 请求

  2. 改变 缓存中 的数据

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 里的数据并做处理。

2. 服务发现源码

2.1 客户端查询服务源码

服务查询 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;
}

可见,主要功能为 两点

  1. 订阅服务

  2. 调用 RPC 接口查询服务

订阅服务的意思是:订阅服务的变更,如果数据变更了,NacosServer 端会推送事件过来,这样 Client 的数据就会是最新的了

2.2 服务端如何返回结果

到底要怎么查询出服务信息呢?

服务端接受请求的方法为

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);}

这里说说为什么触发阈值保护后要将 原本不健康的实例 设置为 健康的。

为什么要有阈值保护?

请看这里 阈值保护功能

为什么要将不健康的设置为健康

就是让用户直接使用上坏的,让用户直接失败

相关内容

热门资讯

苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...