【Nacos源码篇(八)】Nacos服务注册源码剖析

细化分为两个步骤,一个是客户端,一个是服务端

客户端

代码地址

如果你拉了nacos的源码(本人是拿的2.0.3的版本)的话,客户端发起服务注册代码地址如下:
com.alibaba.nacos.client.naming.remote.http.NamingHttpClientProxy#registerService

注册代码剖析

    @Override
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                instance);
        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
            beatReactor.addBeatInfo(groupedServiceName, beatInfo);
        }
        final Map<String, String> params = new HashMap<String, String>(32);
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, groupedServiceName);
        params.put(CommonParams.GROUP_NAME, groupName);
        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
        params.put(IP_PARAM, instance.getIp());
        params.put(PORT_PARAM, String.valueOf(instance.getPort()));
        params.put(WEIGHT_PARAM, String.valueOf(instance.getWeight()));
        // 2.0.3的客户端代码里面这里写的不规范有点,因为上面是已经定义过了的
        params.put("enable", String.valueOf(instance.isEnabled())); 
        params.put(HEALTHY_PARAM, String.valueOf(instance.isHealthy()));
        params.put(EPHEMERAL_PARAM, String.valueOf(instance.isEphemeral()));
        params.put(META_PARAM, JacksonUtils.toJson(instance.getMetadata()));
        
        reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
    }
  • groupedServiceName = groupName + Constants.SERVICE_INFO_SPLITER + serviceName
  • instance.isEphemeral()判断当前实例是临时实例还是持久实例,其默认值是private boolean ephemeral = true;即临时实例,如果是临时实例的话,就会添加其心跳信息
public BeatInfo buildBeatInfo(String groupedServiceName, Instance instance) {
        BeatInfo beatInfo = new BeatInfo();
        beatInfo.setServiceName(groupedServiceName);
        beatInfo.setIp(instance.getIp());
        beatInfo.setPort(instance.getPort());
        beatInfo.setCluster(instance.getClusterName());
        beatInfo.setWeight(instance.getWeight());
        beatInfo.setMetadata(instance.getMetadata());
        beatInfo.setScheduled(false);
        beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
        return beatInfo;
    }

⚡️我们可以看到这里设置的该实例的心跳信息period就是比较关心的心跳间隔时间,其默认的时间是 DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5),即默认5s为其心跳间隔时间,心跳信息是保存在客户端的BeatReactor里面,当然,心跳的代码就不在这里梳理了,后续会出心跳的代码分析
⚡️再看看上面的代码片段,放入了实例的一些相关信息,如服务名,IP,端口,权重等,这些也是对于一个服务注册中心来说会比较重要的
⚡️请求地址 UtilAndComs.nacosUrlInstance/nacos/v1/ns/instance,即向nacos的服务端发送一个POST请求

服务端

代码地址

com.alibaba.nacos.naming.controllers#register

服务端代码剖析

register
    /**
     * Register new instance.
     * 服务注册--->client 端 post请求 nacos/v1/ns/instance
     * client : {@link com.alibaba.nacos.client.naming.remote.http.NamingHttpClientProxy#registerService}
     * @param request http request
     * @return 'ok' if success
     * @throws Exception any error during register
     */
    @CanDistro
    @PostMapping
    @Secured(parser = NamingResourceParser.class, 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();
        // InstanceOperatorClientImpl
        getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
        return "ok";
    }

⚡️这里打印一下instance的toString,能更加直观的看看实例信息: Instance{instanceId=‘192.168.1.10#10001#DEFAULT#DEFAULT_GROUP@@server1’,ip=‘192.168.1.10’,port=10001,weight=1.0,healthy=true,enabled=true,ephemeral=true,clusterName=‘DEFAULT’,serviceName=‘DEFAULT_GROUP@@server1’,metadata={preserved.register.source=SPRING_CLOUD}} 懒得排版了,就这样看吧TAT
⚡️上面大部分代码都是格式化了一下服务相关的信息,主要的注册是走的InstanceOperatorClientImpl的registerInstance方法,跟踪源码后,服务注册的核心代码:EphemeralClientOperationServiceImpl#registerInstance 接下来贴一下代码:

    /**
     * This method creates {@code IpPortBasedClient} if it don't exist.
     */
    @Override
    public void registerInstance(String namespaceId, String serviceName, Instance instance) {
        boolean ephemeral = instance.isEphemeral();
        String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
        createIpPortClientIfAbsent(clientId);
        Service service = getService(namespaceId, serviceName, ephemeral);
        clientOperationService.registerInstance(service, instance, clientId);
    }    

	@Override
    public void registerInstance(Service service, Instance instance, String clientId) {
        Service singleton = ServiceManager.getInstance().getSingleton(service);
        Client client = clientManager.getClient(clientId);
        if (!clientIsLegal(client, clientId)) {
            return;
        }
        InstancePublishInfo instanceInfo = getPublishInfo(instance);
        client.addServiceInstance(singleton, instanceInfo);
        client.setLastUpdatedTime();
        // 发布事件
        // 客户端注册事件
        NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
        // 实例元数据事件
        NotifyCenter.publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
    }

⚡️ 上面的第一个registerInstance方法,实际上是创建了一个client(如果这个client是第一次注册上来,则创建,如果不是则不做处理,如 192.168.0.6:8080#true,这里的true指短暂的客户端,第一次注册的话,则会生成一个新的client), createIpPortClientIfAbsent(clientId),以及心跳相关信息,这个会在后续文章中详解,也就是在这里,向下文中的ClientManager的clients这个map中放入的客户端的信息等
⚡️在这里,有几个可能会感兴趣的东西,ServiceManager,Client,InstancePublishInfo,NotifyCenter 所以,实列注册的时候实际做的事情都在这几个里面了

ServiceManager
public class ServiceManager {
    private static final ServiceManager INSTANCE = new ServiceManager();
    private final ConcurrentHashMap<Service, Service> singletonRepository;
    private final ConcurrentHashMap<String, Set<Service>> namespaceSingletonMaps;
    //...
}

⚡️首先很明确的点就是,ServiceManager是单例的,这点在上面的getInstance()表现的很明确
⚡️ 两个ConcurrentHashMap,分别保存单例的服务,和命名空间下的服务集合
在这里插入图片描述
⚡️贴下debug的图片,客户端我写了两个服务,server1和server2在singletonRepository中保存了这两个服务的信息,所以size为2,同时namespaceSingletonMaps保存了这两个服务,server1和server2,他们均在public的这个命名空间下,即public->(server1,server2),所以,说明了singletonRepository仅保存服务这个维度,namespaceSingletonMaps保存的是该命名空间下的所有的服务
⚡️ 注意Service{namespace='public', group='DEFAULT_GROUP', name='server1', ephemeral=true, revision=0}这里的Service里面是的属性是没有端口,ip等属性的,意味着这里的Service是指服务(server.name指定的那个值),就算有几个server1,这里的Service都是泛指的server1

Client

意思就是客户端,接下来看看getClient做了啥
在这里插入图片描述
⚡️ 可以看到 clientId是IP:端口+是否是短暂 的形式的字符串
⚡️ 当然这里提一嘴,本人debug过程中,虽然只启动了一个服务,但是会存在192.168.1.6:6xxxx的clientId进来,暂时我理解是客户端一些其他端口在做心跳等工作,先暂时这么理解~ 后来看的时候发现这个 好像是1.x的客户端有个线程在调用/list接口拿服务列表,这个时候也会被当做一个客户端
⚡️ getClientManagerById(clientId)实际做的事情是,根据后缀,即判断当前服务是短暂还是持久的,来获取相应的ClientManager,短暂的ClientManager所对应的就是EphemeralIpPortClientManager

@Component("ephemeralIpPortClientManager")
public class EphemeralIpPortClientManager implements ClientManager {
     private final ConcurrentMap<String, IpPortBasedClient> clients = new ConcurrentHashMap<>();
}

⚡️ 上面已经描述了client是在什么时候注册进clients这个map的,就不多做阐述了。这里定义了一个ConcurrentMap,key就是我们的服务的IP:端口+true,value保存的是客户端的IP,端口等信息,里面还有一些关于客户端心跳的一些任务,暂时就不往里面深挖了~ 不过可以扒一点这块的源码,感兴趣的话可以看看,不然就跳过到下一截

@Component("ephemeralIpPortClientManager")
public class EphemeralIpPortClientManager implements ClientManager {   
   private final ConcurrentMap<String, IpPortBasedClient> clients = new ConcurrentHashMap<>();
    
	@Override
    public boolean clientConnected(final Client client) {
        clients.computeIfAbsent(client.getClientId(), s -> {
            Loggers.SRV_LOG.info("Client connection {} connect", client.getClientId());
            IpPortBasedClient ipPortBasedClient = (IpPortBasedClient) client;
            ipPortBasedClient.init();
            return ipPortBasedClient;
        });
        return true;
    }

    /**
     * Init client.
     */
    public void init() {
        if (ephemeral) {
            beatCheckTask = new ClientBeatCheckTaskV2(this);
            HealthCheckReactor.scheduleCheck(beatCheckTask);
        } else {
            healthCheckTaskV2 = new HealthCheckTaskV2(this);
            HealthCheckReactor.scheduleCheck(healthCheckTaskV2);
        }
    }
}

/**
 * Schedule client beat check task with a delay.
 *
 * @param task client beat check task
 */
public class HealthCheckReactor {
    private static Map<String, ScheduledFuture> futureMap = new ConcurrentHashMap<>();
    
    public static void scheduleCheck(BeatCheckTask task) {
        Runnable wrapperTask =
                task instanceof NacosHealthCheckTask ? new HealthCheckTaskInterceptWrapper((NacosHealthCheckTask) task)
                        : task;
        futureMap.computeIfAbsent(task.taskKey(),
                k -> GlobalExecutor.scheduleNamingHealth(wrapperTask, 5000, 5000, TimeUnit.MILLISECONDS));
    }
}

/**
 * Instance beat checker for expired instance.
 */
public class ExpiredInstanceChecker implements InstanceBeatChecker {
    @Override
    public void doCheck(Client client, Service service, HealthCheckInstancePublishInfo instance) {
        boolean expireInstance = ApplicationUtils.getBean(GlobalConfig.class).isExpireInstance();
        if (expireInstance && isExpireInstance(service, instance)) {
            deleteIp(client, service, instance);
        }
    }
}

⚡️ clientConnected这个方法是向clients这个存储客户端的map中添加client的,同时我们能够看到初始化client的同时,执行了其内部的init()方法,当客户端是短暂的时候,向HealthCheckReactor.scheduleCheck(beatCheckTask)传入了一个ClientBeatCheckTaskV2我的理解是向健康检查的反应堆传入了一个客户端心跳检查的任务,跟踪源码后,可以看到 ExpiredInstanceChecker就是实际的节拍检测的落地,isExpireInstance的实际值等于 System.currentTimeMillis() - instance.getLastHeartBeatTime() > deleteTimeout即当前时间减去实例的最后一次心跳时间是否大于超时时间,当然,关于心跳的这部分源码,后续有机会会单独邻出来扒拉

InstancePublishInfo

贴下获取InstancePublishInfo的代码,瞅两眼就差不多了

    /**
     * get publish info.
     */
    default InstancePublishInfo getPublishInfo(Instance instance) {
        InstancePublishInfo result = new InstancePublishInfo(instance.getIp(), instance.getPort());
        if (null != instance.getMetadata() && !instance.getMetadata().isEmpty()) {
            result.getExtendDatum().putAll(instance.getMetadata());
        }
        if (StringUtils.isNotEmpty(instance.getInstanceId())) {
            result.getExtendDatum().put(Constants.CUSTOM_INSTANCE_ID, instance.getInstanceId());
        }
        if (Constants.DEFAULT_INSTANCE_WEIGHT != instance.getWeight()) {
            result.getExtendDatum().put(Constants.PUBLISH_INSTANCE_WEIGHT, instance.getWeight());
        }
        if (!instance.isEnabled()) {
            result.getExtendDatum().put(Constants.PUBLISH_INSTANCE_ENABLE, instance.isEnabled());
        }
        String clusterName = StringUtils.isBlank(instance.getClusterName()) ? UtilsAndCommons.DEFAULT_CLUSTER_NAME
                : instance.getClusterName();
        result.setHealthy(instance.isHealthy());
        result.setCluster(clusterName);
        return result;
    }

在这里插入图片描述
⚡️ 其实里面没啥,Instance POJO of client published for Nacos v2.,准确的说,这个保存了实例的信息**(对于2.0+而言)**
⚡️ 拿到InstancePublishInfo信息后,client会根据这些信息做些处理,代码如下

// 这里贴下上面的代码,即获取到InstancePublishInfo的地方
public void registerInstance(Service service, Instance instance, String clientId) {
         //...
         InstancePublishInfo instanceInfo = getPublishInfo(instance);
         client.addServiceInstance(singleton, instanceInfo);
        //...
}

public abstract class AbstractClient implements Client {
    @Override
    public boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {
        if (null == publishers.put(service, instancePublishInfo)) {
            MetricsMonitor.incrementInstanceCount();
        }
        NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
        Loggers.SRV_LOG.info("Client change for service {}, {}", service, getClientId());
        return true;
    }
}

⚡️ MetricsMonitor.incrementInstanceCount()MetricsMonitor,源码文档给出的解释是指标监视器它是一个单例的,全局的,里面有很多AtomicInteger,AtomicLong定义的变量,其中就有private final AtomicInteger ipCount = new AtomicInteger();说明此时会告诉指标监视器,客户端的ip数+1了!
⚡️ 同时,发布了一个ClientEvent.ClientChangedEvent即客户端变化事件

这就是拿到InstancePublishInfo后做的相关操作

NotifyCenter
NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
NotifyCenter.publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
  • 通过事件发布中心,发布了两个事件ClientRegisterServiceEvent以及InstanceMetadataEvent关于NotifyCenter的具体细节可以参考之前的文章,如果你看了上章节的事件通知,那么就能够串起来这个过程了,这里不做太多的阐述,简单的来说:这里通过一个单例NotifyCenter,向这个事件所对应的发布者添加消息,接着该发布者(线程)会不断的将消息广播给订阅了这个事件的订阅者们,由此,就能够让其订阅者去处理这个事件所需要做的相关操作。

流程总结~

至此,客户端注册的流程已经完结,接下来再从宏观的角度过一下流程
在这里插入图片描述
⚡️ 客户端通过post请求,请求服务端的nacos,请求地址是nacos/v1/ns/instance包装了当前服务的一些实例信息,如IP,端口,服务名等
⚡️ Nacos服务端通过ClientManager保存客户端维度的信息,ServiceManager保存服务维度的信息,这里的客户端维度,你可以理解为是微服务集群中的一个服务,如订单微服务中的其中一个服务(可以理解成机器,或者pod),而服务维度呢,就是泛指你的这一组微服务,如订单服务,这么一说,即可联想到,服务对于客户端而言,是1对多的关系,每个客户端也会维持其心跳相关的信息,这个会在后续心跳相关源码梳理
⚡️ NotifyCenter和MetricsMonitor均为单例,MetricsMonitor是Nacos的性能指标监控器,保存nacos的全局监控信息,NotifyCenter是事件通知,会将事件通过发布者通知其订阅者,交给他们处理

Nacos源码篇

语雀版文档

Nacos源码注释

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值