通过上节分析:Nacos 服务注册概述及客户端注册实例源码分析(一)
清晰地了解:客户端在注册服务时实际上是调用的 NamingService#registerInstance 方法来完成实例的注册,而且在最后知道 Nacos 2.0 以前的版本通过 Http 方式进行服务注册就是调用的接口:/nacos/v1/ns/intance
下图摘自官网的架构图:
在这里就能分析出我们要找的接口就在 NamingService 这个服务中,从源码角度来看,其实就是存放在 naming 这个子模块上的,而且该模块实际上就是实现服务注册的
从「naming」模块中找到 controller,所有的接口都是在其中的,从这些 Controller 中可以很明显的看到一个 InstanceController,很明显它和注册实例的逻辑相关.
InstanceController 类上的 @RequestMapping 注解值就是我们访问的注册接口,前面提到过的官网 OpenAPI 调用开放接口实现注册
/v1/ns/instance
UtilsAndCommons.NACOS_NAMING_CONTEXT + UtilsAndCommons.NACOS_NAMING_INSTANCE_CONTEXT
下面查看 RESTFUL API 接口 POST 请求类型的方法 register,在这个方法中实际上就是接受用户请求,把收到的信息进行解析,还原成 Instance,然后调用 registerInstance 方法完成注册,这个方法才是服务端注册的核心,源码如下:
@CanDistro
@PostMapping
@Secured(action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
final Instance instance = HttpRequestInstanceBuilder.newBuilder()
.setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
// 区分不同协议获取到操作类
getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
NotifyCenter.publishEvent(new RegisterInstanceTraceEvent(System.currentTimeMillis(), "",
false, namespaceId, NamingUtils.getGroupName(serviceName), NamingUtils.getServiceName(serviceName),
instance.getIp(), instance.getPort()));
return "ok";
}
主要注意一下这个方法:getInstanceOperator().registerInstance(namespaceId, serviceName, instance)
其中的 getInstanceOperator():判断是否采用 Grpc 协议,很明显这个位置走的是 instanceServiceV2
private InstanceOperator getInstanceOperator() {
return upgradeJudgement.isUseGrpcFeatures() ? instanceServiceV2 : instanceServiceV1;
}
实际上 instanceServiceV2 就是 InstanceOperatorClientImpl,所以我们来看这里的 registerInstance 方法
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
// 判断是否为瞬时对象(临时客户端)
boolean ephemeral = instance.isEphemeral();
// 获取客户端 ID
String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
// 通过客户端 ID 创建客户端连接
createIpPortClientIfAbsent(clientId);
// 获取服务
Service service = getService(namespaceId, serviceName, ephemeral);
// 具体注册服务
clientOperationService.registerInstance(service, instance, clientId);
}
在这里我们要分析一些细节,在 Nacos2.0 以后新增 Client 模型,一个客户端 gRPC 长连接对应一个 client,每个 client 有自己唯一的 id(clientId),Client 负责管理一个客户端的服务注册实例 Publish 和服务订阅 Subscribe,我们可以看一下这个模型其实就是一个接口
public interface Client {
// 客户端ID/gRPC connectionId
String getClientId();
// 是否为临时客户端
boolean isEphemeral();
// 客户端更新时间
void setLastUpdatedTime();
long getLastUpdatedTime();
// 服务实例注册/注销/查询
boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo);
InstancePublishInfo removeServiceInstance(Service service);
InstancePublishInfo getInstancePublishInfo(Service service);
Collection<Service> getAllPublishedService();
// 服务订阅/取消订阅/查询订阅
boolean addServiceSubscriber(Service service, Subscriber subscriber);
boolean removeServiceSubscriber(Service service);
Subscriber getSubscriber(Service service);
Collection<Service> getAllSubscribeService();
// 生成同步给其他节点 client 数据
ClientSyncData generateSyncData();
// 是否过期
boolean isExpire(long currentTime);
// 释放资源
void release();
}
EphemeralClientOperationServiceImpl:实际负责处理服务注册,看具体的源码
public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
// 确保 Service 单例存在
Service singleton = ServiceManager.getInstance().getSingleton(service);
if (!singleton.isEphemeral()) {
throw new NacosRuntimeException(NacosException.INVALID_PARAM,
String.format("Current service %s is persistent service, can't register ephemeral instance.",
singleton.getGroupedServiceName()));
}
// 通过 clientId 找到对应的客户端
Client client = clientManager.getClient(clientId);
if (!clientIsLegal(client, clientId)) {
return;
}
// 客户端 Instance 模型,转换为服务端 Instance 模型
InstancePublishInfo instanceInfo = getPublishInfo(instance);
// 将 Instance 存储到 Client 里
client.addServiceInstance(singleton, instanceInfo);
client.setLastUpdatedTime();
// 建立 Service 与 ClientId 关系
NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
NotifyCenter
.publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
}
Service 的容器是 ServiceManager,但是在 com.alibaba.nacos.naming.core.v2 包下,容器中 的 Service 都是单例
public class ServiceManager {
private static final ServiceManager INSTANCE = new ServiceManager();
// 单例 Service,可以查看 Service equals、hashCode 方法
private final ConcurrentHashMap<Service, Service> singletonRepository;
// namespace 下所有的 service
private final ConcurrentHashMap<String, Set<Service>> namespaceSingletonMaps;
}
从这个位置可以看出,当调用这个注册方法时 ServiceManager 负责管理 Service 单例
public Service getSingleton(Service service) {
// 通过 ConcurrentHashMap 来存储单例的 Service
singletonRepository.putIfAbsent(service, service);
Service result = singletonRepository.get(service);
namespaceSingletonMaps.computeIfAbsent(result.getNamespace(), (namespace) -> new ConcurrentHashSet<>());
namespaceSingletonMaps.get(result.getNamespace()).add(result);
return result;
}
它是一个接口,看它对应的实现类 ConnectionBasedClientManager,这个实现类负责管理长连接 clientId 与 Client 模型的映射关系
// 通过 clientId 查询 Client
public Client getClient(String clientId) {
return clients.get(clientId);
}
负责存储当前客户端的服务注册表,即 Service 与 Instance 关系;注意:对于单个客户端来说,同一个服务只能注册一个实例
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));
Loggers.SRV_LOG.info("Client change for service {}, {}", service, getClientId());
return true;
}
当前这里的目的是为了过滤目标服务得到最终的 Instance 列表建立 Service 与 Client 关系,建立 Service 与 Client 关系就是为了加速查询
发布 ClientRegisterServiceEvent 事件,ClientServiceIndexesManager 负责监听,ClientServiceIndexesManager 维护了两个索引:
private void handleClientOperation(ClientOperationEvent event) {
Service service = event.getService();
String clientId = event.getClientId();
if (event instanceof ClientOperationEvent.ClientRegisterServiceEvent) {
addPublisherIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientDeregisterServiceEvent) {
removePublisherIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientSubscribeServiceEvent) {
addSubscriberIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientUnsubscribeServiceEvent) {
removeSubscriberIndexes(service, clientId);
}
}
// 建立 Service 与发布 clientId 关系
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));
}
这个索引关系建立好以后,还会触发 ServiceChangedEvent,代表服务注册表变更;对于注册表变更,后面还要做两件事情「1、通知订阅客户端;2、Nacos 集群数据同步」这部分源码剖析后续文章分享出来
Nacos 客户端的服务发现,其实就是封装参数、调用服务接口、获得返回实例列表
Nacos 客户端—>NamingService—>封装参数—>Nacos Server
如果细化这个流程,会发现不仅包括了通过 NamingService 获取服务列表,在获取服务列表的过程中还涉及到通信流程协议(Http、gRPC)、订阅流程、故障转移流程等,下面再详细的捋一捋
入口「NamingTest」可以看到
public class NamingTest {
@Test
public void testServiceList() throws Exception {
......
NamingService namingService = NacosFactory.createNamingService(properties);
namingService.registerInstance("nacos.test.1", instance);
ThreadUtils.sleep(5000L);
List<Instance> list = namingService.getAllInstances("nacos.test.1");
System.out.println(list);
}
}
在这里我们主要关注 getAllIntances 方法,那么就需要看一下这个方法的具体操作,当然这其中需要经过一系列的重载方法调用
其实这里的方法比入口多出了几个参数,这里不仅有服务名称,还有分组名、集群列表、是否订阅,重载方法中的其他参数已经在各种重载方法的调用过程中设置了默认值,比如:
@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
String clusterString = StringUtils.join(clusters, ",");
// 是否是订阅模式
if (subscribe) {
// 先从客户端缓存获取服务信息
serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, , clusterString);
if (null == serviceInfo) {
// 如果本地缓存不存在服务信息,则进行订阅
serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
}
} else {
// 如果未订阅服务信息,则直接从服务器进行查询
serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
}
// 从服务信息中获取实例列表
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
return list;
}
此方法的流程图
流程基本逻辑:
在刚才的流程中,涉及到了订阅逻辑,入口代码为获取实例列表中的方法
serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
以下是具体的分析,首先这里的 clientProxy 是 NamingClientProxy 类对象,对应的实现类 NamingClientProxyDelegate,对应 subscribe 实现如下:
public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
NAMING_LOGGER.info("[SUBSCRIBE-SERVICE] service:{}, group:{}, clusters:{} ", serviceName, groupName, clusters);
String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
// 定时调度 UpdateTask
serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
// 获取缓存中的 ServiceInfo
ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
if (null == result || !isSubscribed(serviceName, groupName, clusters)) {
// 如果为 null,则进行订阅逻辑处理,基于 gRPC 协议
result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
}
// ServiceInfo 本地缓存处理
serviceInfoHolder.processServiceInfo(result);
return result;
}
在这段代码中,可以看到在获取服务实例列表时(特别是首次)也进行了订阅逻辑的扩展,基本流程图如下:
至此「Nacos 服务端注册、客户端服务发现源码分析」分析到这里,基本只需要掌握大致的脉路即可
欢迎大家在评论框分享您的看法,喜欢该文章帮忙给个赞和收藏,感谢!!
分享个人学习源码的几部曲
更多技术文章可以查看:vnjohn 个人博客