nacos源码阅读-服务注册分析

147 阅读6分钟

前言

nacos具有两大核心功能,配置管理功能和服务管理功能。从今天开始,基于1.4.3版本的源码进行分析实现这两个核心功能的细节。

服务注册中心管理服务的相关信息,如IP,端口,服务名称,服务的状态,服务集群名称等,服务注册中心的功能包括服务注册,服务注销,服务订阅,服务退订等。nacos也实现注册中心管理功能,因此也包括服务注册、服务注销,服务订阅和服务退订功能。

使用案例

在nacos中,NamingService类是服务注册中心管理功能的实现类。nacos提供了NamingService的服务注册的案例,代码如下:

public class NamingExample {
    
    public static void main(String[] args) throws NacosException {
        
        Properties properties = new Properties();
        //nacos服务端的地址
        properties.setProperty("serverAddr", "127.0.0.1:8848");
        //命名空间
        properties.setProperty("namespace","public");
        //使用命名工厂类创建NamingService
        NamingService naming = NamingFactory.createNamingService(properties);
        //往nacos服务端注册服务:服务名称、ip、端口、集群名称
        naming.registerInstance("nacos.test.3", "11.11.11.11", 8888, "TEST1");
        //往nacos服务端注册服务
        naming.registerInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT");
        //省略代码
        //....
}

根据nacos服务端的地址,命名空间,使用NamingFactory工厂类创建NamingService类,然后调用NamingService类的registerInstance方法进行服务注册,该方法的参数包括服务的名称、ip、端口以及集群的名称。这样就把服务相关的信息注册到nacos服务端了。

源码分析

客户端服务注册


//源码位置:com.alibaba.nacos.client.naming.NacosNamingService#registerInstance
public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName)
            throws NacosException {
        //设置ip、端口、权重、集群名称
        Instance instance = new Instance();
        instance.setIp(ip);
        instance.setPort(port);
        instance.setWeight(1.0);
        instance.setClusterName(clusterName);
        
        registerInstance(serviceName, groupName, instance);
}

registerInstance的groupName参数默认为DEFAULT_GROUP,创建Instance,并设置ip、端口、权重、集群名称,调用registerInstance重载方法进行服务注册。

Instance表示实例,用于承载服务的相关信息,属性主要包括ip、端口、权重、服务名称、集群名称、元数据等。Instance的属性如下:


public class Instance implements Serializable {
    
    private static final long serialVersionUID = -742906310567291979L;
    
    /**
     * unique id of this instance.
     */
    private String instanceId;
    
    /**
     * instance ip.
     */
    private String ip;
    
    /**
     * instance port.
     */
    private int port;
    
    /**
     * instance weight.
     */
    private double weight = 1.0D;
    
    /**
     * instance health status.
     */
    private boolean healthy = true;
    
    /**
     * If instance is enabled to accept request.
     */
    private boolean enabled = true;
    
    /**
     * If instance is ephemeral.
     *
     * @since 1.0.0
     */
    private boolean ephemeral = true;
    
    /**
     * cluster information of instance.
     */
    private String clusterName;
    
    /**
     * Service information of instance.
     */
    private String serviceName;
    
    /**
     * user extended attributes.
     */
    private Map<String, String> metadata = new HashMap<String, String>();
  
    //省略代码
}

metadata表示元数据,承载服务的一些变化的相关信息,有利于拓展。


//代码位置:com.alibaba.nacos.client.naming.NacosNamingService#registerInstance
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        //检验参数是否合法
        NamingUtils.checkInstanceIsLegal(instance);
        //创建 groupedServiceName 
        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
        //如果实例是临时的
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
            beatReactor.addBeatInfo(groupedServiceName, beatInfo);
        }
        //调用http往nacos服务端进行服务注册
        serverProxy.registerService(groupedServiceName, groupName, instance);
}

上述代码的逻辑如下:

  • checkInstanceIsLegal方法检验实例心跳超时时间是否小于实例心跳间隔,.
  • NamingUtils.getGroupedName方法检验serviceName和groupName是否为空,如果为空,则抛出异常,并将返回groupName@@serviceName的拼接字符串作为groupedServiceName。
  • 如果实例是临时的,则创建心跳信息,并且将新创建的心跳信息添加到队列。心跳相关的分析,将会在另外的文章进行分析,这里只需要知道向nacos客户端向nacos服务端发送心跳信息。
  • 调用http请求向nacos服务端进行服务注册。

//代码位置:com.alibaba.nacos.client.naming.NacosNamingService#registerInstance
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                instance);
        /**
        封装http请求参数,包括命名空间、服务名称、组名称、集群名称、ip、端口
        权重、服务健康状态、服务是否是临时的、元数据。
        **/
        final Map<String, String> params = new HashMap<String, String>(16);
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, serviceName);
        params.put(CommonParams.GROUP_NAME, groupName);
        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
        params.put("ip", instance.getIp());
        params.put("port", String.valueOf(instance.getPort()));
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
        // post请求:/nacos/v1/ns/instance
        reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
        
}

既然通过http向nacos服务端发送服务注册请求,那么需要构建封装请求参数以及选择请求的方法,上面代码将Instance的相关属性封装为post请求的请求参数,包括命名空间、服务名称、组名称、集群名称、ip、端口 权重、服务健康状态、服务是否是临时的、元数据。请求参数构建好了,nacos提供了一套http请求模板,通过该模板nacos将服务注册请求发送给nacos服务端/nacos/v1/ns/instance的接口。

这样,客户端的服务注册请求到这里分析完成了,http请求模板的源码不具体分析了,如果感兴趣可以看看。

服务端服务注册

客户端服务注册,将请求发送给nacos服务的/nacos/v1/ns/instance的接口,该接口如下:


//代码位置:com.alibaba.nacos.naming.controllers.InstanceController#register
public String register(HttpServletRequest request) throws Exception {
        //从请求中获取namespaceId
       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 = parseInstance(request);
        //服务注册
       serviceManager.registerInstance(namespaceId, serviceName, instance);
        return "ok";
}

register接口承接客户端服务注册的请求,主要逻辑如下:

  • 从请求中解析请求参数。

    • 解析namespaceId,如果该参数为空,则默认为public。
    • 解析serviceName,判断该参数是否为空,如果为空,则抛出异常。checkServiceNameFormat方法检验serviceName是否是groupName@@serviceName形式,如果不是,则抛出异常。
    • 解析Instance,在nacos客户端中,将Instance的属性封装为相关请求参数,在nacos服务端,将请求参数解析为Instance。
  • 调用serviceManager的registerInstance方法进行服务端注册。


//代码位置:com.alibaba.nacos.naming.core.ServiceManager#registerInstance
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        
        //创建service
        createEmptyService(namespaceId, serviceName, instance.isEphemeral());
        //获取Service
        Service service = getService(namespaceId, serviceName);
        
        if (service == null) {
            throw new NacosException(NacosException.INVALID_PARAM,
                    "service not found, namespace: " + namespaceId + ", service: " + serviceName);
        }
        //添加实例
        addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
  • 调用createEmptyService方法创建Service。
  • 根据namespaceId和serviceName获取Service。
  • 添加Instance,实际就是保存服务相关信息。

接下来详情分析createEmptyService、getService和addInstance方法。

getService

//代码位置:com.alibaba.nacos.naming.core.ServiceManager#getServiceprivate final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
​
public Service getService(String namespaceId, String serviceName) {
        
        if (serviceMap.get(namespaceId) == null) {
            return null;
        }
        return chooseServiceMap(namespaceId).get(serviceName);
}

serviceMap根据namespaceId保存着Service,getServices从serviceMap和serviceName获取到对应的Service。

createEmptyService

createEmptyService方法最终调用了createServiceIfAbsent方法:


//代码位置:com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
            throws NacosException {
        //根据namespaceId和serviceName获取Service
        Service service = getService(namespaceId, serviceName);
        if (service == null) {
            
            Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
            //创建Service
            service = new Service();
            service.setName(serviceName);
            service.setNamespaceId(namespaceId);
            service.setGroupName(NamingUtils.getGroupName(serviceName));
            // now validate the service. if failed, exception will be thrown
            //最后更新时间
            service.setLastModifiedMillis(System.currentTimeMillis());
            //计算检验和,将实例的所有的ip进行排序拼接并生成md5
            service.recalculateChecksum();
            if (cluster != null) {
                cluster.setService(service);
                service.getClusterMap().put(cluster.getName(), cluster);
            }
            //服务名称、集群名称合法性的校验
            service.validate();
            //服务的初始化和添加监听器
            putServiceAndInit(service);
            //如果非临时数据,则进行nacos集群间同步服务数据
            if (!local) {
                addOrReplaceService(service);
            }
        }
}

createServiceIfAbsent的逻辑如下:

  • 根据namespaceId和serviceName获取Service
  • service等于null,创建Service,并计算检验和,检验服务名称和集群名称的合法性。
  • putServiceAndInit方法进行服务的初始化以及添加监听器,监听数据的改变。
  • 如果不是临时数据(非本地数据),则进行nacos集群间同步服务数据。

createServiceIfAbsent方法中,有两个比较重要的方法:putServiceAndInit和addOrReplaceService。

putServiceAndInit方法是对服务进行初始化,并添加监听器监听数据的改变。addOrReplaceService方法是nacos集群间同步服务数据。


private void putServiceAndInit(Service service) throws NacosException {
        //将service添加到serviceMap中缓存
        putService(service);
        //根据namespaceId和serviceName获取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方法如下:


//代码位置:com.alibaba.nacos.naming.core.Service#init
public void init() {
        //检查service的健康状态
        HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
        //遍历服务的集群,并检查集群的健康状态
        for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
            entry.getValue().setService(this);
            entry.getValue().init();
        }
}

service的init方法的作用就是检查service和该service的集群的健康状态,这里就点到为止,后续有文章会进行分析。

addInstance


//代码位置:com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent#addInstance
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
            throws NacosException {
        //创建key
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
        //获取service
        Service service = getService(namespaceId, serviceName);
        
        synchronized (service) {
            //获取服务的所有实例
            List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
            
            Instances instances = new Instances();
            instances.setInstanceList(instanceList);
            //一致性服务,集群间的数据同步
            consistencyService.put(key, instances);
        }
}

addInstance方法的逻辑如下

  • 首先根据buildInstanceListKey方法生成key,ephemeral参数为ture,则生成com.alibaba.nacos.naming.iplist.ephemeral.namespaceId##serviceName,否则生成com.alibaba.nacos.naming.iplist.namespaceId##serviceName。
  • 获取service
  • 获取服务的所有的实例,并且调用consistencyService的put方法进行集群间的数据同步。addIpAddresses方法调用了updateIpAddresses方法,并且action参数为add,表示添加。

分析到这里,客户端和服务端的服务注册已经完成,但是还有很多细节没有进行深入分析。如下:

  • 当服务注册的实例是临时的,nacos客户端与nacos服务端之间的心跳同步。
  • service健康状态和集群间的健康状态的检查。
  • 集群间的数据一致性是如何保证的。

这些细节虽然没有分析,但是服务注册的主体流程还是比较清晰的。nacos客户端组装Instance,通过http请求将Instance发送给nacos服务端,nacos服务端则进行解析请求参数,并且生产Instance,然后尝试获取service,如果没有service则创建,创建service的过程中,对service进行初始化以及集群的初始化,最后调用nacos一致性服务进行集群间数据的同步。

\