Spring Cloud Alibaba Nacos 服务注册--实现原理与源码解析

好久不见,最近工作一直被一堆事所捆绑,也没有大块的时间去看技术相关的东西,以至于部分伙伴的留言都没有及时的回复,先和大家分享一句话突然想起来的一句吧,不变的可能是这个世界,不断变化的可能是我们自己。

微服务架构在近几年变的越来越流行,这可能会成为我们的一个必备技能(本人的建议是如果现有的系统没有遇到什么运维、业务瓶颈的话不要为了单纯的使用微服务技术而去进行改造)。

在微服务的早期随着Spring Cloud的支撑,好多都使用了Spring默认的Netflix全家桶,但是现在Netflix也不再进行维护且社区活跃度也比较低,所以在注册中心部分很多团队都会在Nacos、Zookeeper、Kubernetes中进行选择,本文不对其中的优缺点进行比较和分析,直接来干货Spring Cloud Alibaba Nacos 服务如何注册。

一、服务注册初始化阶段
其实对于服务的注册基本上所有开源项目的实现思路基本一致,无非就是服务的注册、心跳维持,看下图直接找到服务注册的源码,一般这种代码我都是从AutoConfig开始看了,大家可以先大体看一下,这几个AutoConfig功能其实就是初始化红框内的其他的的非AutoConfig类。

源码结构图

项目启动以后首先会通过NacosDiscoveryAutoConfiguration根据配置文件初始化NacosDiscoveryProperties,NacosDiscoveryProperties为配置文件中配置的服务注册相关参数(如serverAddr、group等),然后紧接着初始化NacosServiceDiscovery,该类主要是通过的NacosDiscoveryProperties的namingServiceInstance属性实现获取注册中心的相关信息。最后初始化NacosDiscoveryClient对NacosServiceDiscovery进行封装。

在服务注册方面,会通过NacosServiceRegistryAutoConfiguration依次初始化NacosServiceRegistry、NacosRegistration、NacosAutoServiceRegistration,在初始化NacosServiceRegistry时会对之前的NacosDiscoveryProperties的namingServiceInstance属性进行实例化,其中服务的注册的核心代码(com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register)也在该类中。在初始化AbstractAutoServiceRegistration时候,大家会发现它的父类实现了ApplicationListener,当发布事件时候会执行onApplicationEvent中的代码(可以参考我伙伴的文章),这也是服务注册的入口。

二、服务注册阶段
在AbstractAutoServiceRegistration的onApplicationEvent方法中执行了start方法,start方法中会执行register方法,最终调用的方法就是之前说到的com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register,完成对服务进行注册。

public void register(Registration registration) {
        if (StringUtils.isEmpty(registration.getServiceId())) {
            log.warn("No service to register for nacos client...");
        } else {
            String serviceId = registration.getServiceId();
            String group = this.nacosDiscoveryProperties.getGroup();
            Instance instance = this.getNacosInstanceFromRegistration(registration);

            try {
                this.namingService.registerInstance(serviceId, group, instance);
                log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), instance.getPort()});
            } catch (Exception var6) {
                log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var6});
                ReflectionUtils.rethrowRuntimeException(var6);
            }

        }
    }

在服务注册的代码中,最核心的是this.namingService.registerInstance(serviceId, group, instance);这一行,最终实现注册的方法是com.alibaba.nacos.client.naming.net.NamingProxy#registerService,在这个方法中通过调用nacas的api包中的reqAPI来与注册中心的服务端进行交互从而完成注册。

public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
        Map<String, String> params = new HashMap(9);
        params.put("namespaceId", this.namespaceId);
        params.put("serviceName", serviceName);
        params.put("groupName", groupName);
        params.put("clusterName", 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", JSON.toJSONString(instance.getMetadata()));
        this.reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, "POST");
    }


public String callServer(String api, Map<String, String> params, String body, String curServer, String method) throws NacosException {
        long start = System.currentTimeMillis();
        long end = 0L;
        this.injectSecurityInfo(params);
        List<String> headers = this.builderHeaders();
        String url;
        if (!curServer.startsWith("https://") && !curServer.startsWith("http://")) {
            if (!curServer.contains(":")) {
                curServer = curServer + ":" + this.serverPort;
            }

            url = HttpClient.getPrefix() + curServer + api;
        } else {
            url = curServer + api;
        }

        HttpResult result = HttpClient.request(url, headers, params, body, "UTF-8", method);
        end = System.currentTimeMillis();
        MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code)).observe((double)(end - start));
        if (200 == result.code) {
            return result.content;
        } else if (304 == result.code) {
            return "";
        } else {
            throw new NacosException(result.code, result.content);
        }
    }

在reqAPI的最后部分的代码com.alibaba.nacos.client.naming.net.NamingProxy#callServer(java.lang.String, java.util.Map<java.lang.String,java.lang.String>, java.lang.String, java.lang.String, java.lang.String)中,与服务端进行交互的过程中,会将自身的信息发送给服务端,服务端响应OK后完成注册,请求服务端的信息如下。

地址:http://192.168.***.***:8848/nacos/v1/ns/instance
内容:{app=nacos-provider, metadata={"preserved.register.source":"SPRING_CLOUD"}, ip=192.168.***.***, weight=1.0, ephemeral=true, serviceName=DEFAULT_GROUP@@nacos-provider, encoding=UTF-8, groupName=DEFAULT_GROUP, namespaceId=public, port=8081, enable=true, healthy=true, clusterName=DEFAULT}

三、心跳的维持
服务注册到注册中心服务端后,服务端与客户端会一直保持一个心跳,来对服务的可用性进行维护,在之前的部分我们有说过NacosServiceRegistry类中会对namingService进行实例化,同样也是利用nacos的api包通过反射来进行初始化。

  public static NamingService createNamingService(Properties properties) throws NacosException {
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            NamingService vendorImpl = (NamingService)constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable var4) {
            throw new NacosException(-400, var4);
        }
    }

在对NacosNamingService进行初始化的过程中,我们会在构造函数中发现他还会初始化com.alibaba.nacos.client.naming.NacosNamingService#beatReactor,而BeatReactor中存在一个线程池,我们在服务注册之前(com.alibaba.nacos.client.naming.NacosNamingService#registerInstance(java.lang.String, java.lang.String, com.alibaba.nacos.api.naming.pojo.Instance)方法)就会向这个线程池中发布Task。

public BeatReactor(NamingProxy serverProxy, int threadCount) {
        this.lightBeatEnabled = false;
        this.dom2Beat = new ConcurrentHashMap();
        this.serverProxy = serverProxy;
        this.executorService = new ScheduledThreadPoolExecutor(threadCount, new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                thread.setName("com.alibaba.nacos.naming.beat.sender");
                return thread;
            }
        });
    }

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        LogUtils.NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = this.buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        if ((existBeat = (BeatInfo)this.dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }

        this.dom2Beat.put(key, beatInfo);
        this.executorService.schedule(new BeatReactor.BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set((double)this.dom2Beat.size());
    }

下面我们在看一下这个定时任务中的内容,其实与服务的注册原理相似,都是与服务端进行一个交互,如果服务端响应该服务不可用,则重新进行注册,如果依旧可用(或已经重新注册)则继续将新的Task(BeatTask)放入到线程池中,从而不断的与服务端维持心跳。

地址:http://192.168.***.***:8848/nacos/v1/ns/instance/beat
内容:{app=nacos-provider, namespaceId=public, port=8081, clusterName=DEFAULT, ip=192.168.***.***, serviceName=DEFAULT_GROUP@@nacos-provider, encoding=UTF-8}
    class BeatTask implements Runnable {
        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }

        public void run() {
            if (!this.beatInfo.isStopped()) {
                long nextTime = this.beatInfo.getPeriod();

                try {
                    JSONObject result = BeatReactor.this.serverProxy.sendBeat(this.beatInfo, BeatReactor.this.lightBeatEnabled);
                    long interval = (long)result.getIntValue("clientBeatInterval");
                    boolean lightBeatEnabled = false;
                    if (result.containsKey("lightBeatEnabled")) {
                        lightBeatEnabled = result.getBooleanValue("lightBeatEnabled");
                    }

                    BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                    if (interval > 0L) {
                        nextTime = interval;
                    }

                    int code = 10200;
                    if (result.containsKey("code")) {
                        code = result.getIntValue("code");
                    }

                    if (code == 20404) {
                        Instance instance = new Instance();
                        instance.setPort(this.beatInfo.getPort());
                        instance.setIp(this.beatInfo.getIp());
                        instance.setWeight(this.beatInfo.getWeight());
                        instance.setMetadata(this.beatInfo.getMetadata());
                        instance.setClusterName(this.beatInfo.getCluster());
                        instance.setServiceName(this.beatInfo.getServiceName());
                        instance.setInstanceId(instance.getInstanceId());
                        instance.setEphemeral(true);

                        try {
                            BeatReactor.this.serverProxy.registerService(this.beatInfo.getServiceName(), NamingUtils.getGroupName(this.beatInfo.getServiceName()), instance);
                        } catch (Exception var10) {
                        }
                    }
                } catch (NacosException var11) {
                    LogUtils.NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}", new Object[]{JSON.toJSONString(this.beatInfo), var11.getErrCode(), var11.getErrMsg()});
                }

                BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
            }
        }
    }

小结
上文介绍了Spring Cloud Alibaba Nacos 服务注册的实现原理与源码解析,相信大家应该大体了解了服务是如何注册到Nacos中,但是可能大家接下来还有疑惑,我们在服务调用的时候,是从哪里获取的所要调用的服务可用列表,以及这个列表是如何与服务端共同维护的,请听下回分解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350