最全【微服务】Nacos服务发现源码分析_nacos 服务发现源码(1),“金三银四”春招指南

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  1. 获取配置文件组信息
  2. 调用API模块中NamingService接口的selectInstances()方法。引用是NacosNamingService的,通过反射获取,之前文章已详细分析。NacosNamingService是Nacos的client模块里面的一个组件,下面分析。
  3. 将Nacos的服务实例适配为SpringCloud的ServiceInstance服务实例

✨NacosNamingService初始化流程

它的构造方法是在NamingFactory通过反射方式调用的,上面也提到了。因为这个流程也是不小的,故在获取服务实例前先讲解。

💖NacosNamingService构造初始化
    
    public NacosNamingService(Properties properties) throws NacosException {
        init(properties);
    }
    
    private void init(Properties properties) throws NacosException {
        ValidatorUtils.checkInitParam(properties);
        this.namespace = InitUtils.initNamespaceForNaming(properties);
        InitUtils.initSerialization();
        initServerAddr(properties);
        InitUtils.initWebRootContext();
        initCacheDir();
        initLogName(properties);
        
        this.eventDispatcher = new EventDispatcher();
        // 初始化服务代理
        this.serverProxy = new NamingProxy(this.namespace, this.endpoint, this.serverList, properties);
        // 初始化心跳组件
        this.beatReactor = new BeatReactor(this.serverProxy, initClientBeatThreadCount(properties));
        // 初始化hostReactor 
        this.hostReactor = new HostReactor(this.eventDispatcher, this.serverProxy, beatReactor, this.cacheDir,
                isLoadCacheAtStart(properties), initPollingThreadCount(properties));
    }

初始化服务代理、心跳发送组件以及hostReactor,重点看hostReactor的构造初始化

💫HostReactor构造初始化

    public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, BeatReactor beatReactor,
            String cacheDir, boolean loadCacheAtStart, int pollingThreadCount) {
        // init executorService
        this.executor = new ScheduledThreadPoolExecutor(pollingThreadCount, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                thread.setName("com.alibaba.nacos.client.naming.updater");
                return thread;
            }
        });
        this.eventDispatcher = eventDispatcher;
        this.beatReactor = beatReactor;
        this.serverProxy = serverProxy;
        this.cacheDir = cacheDir;
        // 初始化本地缓存
        if (loadCacheAtStart) {
            this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(DiskCache.read(this.cacheDir));
        } else {
            this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(16);
        }
        
        this.updatingMap = new ConcurrentHashMap<String, Object>();
        this.failoverReactor = new FailoverReactor(this, cacheDir);
        // 初始化pushReceiver
        this.pushReceiver = new PushReceiver(this);
    }

初始化本地缓存、pushReceiver,重点关注PushReceiver的构造方法

💖PushReceiver构造初始化

    public PushReceiver(HostReactor hostReactor) {
        try {
            this.hostReactor = hostReactor;
            // 初始化udp套接字
            this.udpSocket = new DatagramSocket();
            // 启动一个线程
            this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setDaemon(true);
                    thread.setName("com.alibaba.nacos.naming.push.receiver");
                    return thread;
                }
            });
            // 执行任务,下面的run()
            this.executorService.execute(this);
        } catch (Exception e) {
            NAMING_LOGGER.error("[NA] init udp socket failed", e);
        }
    }

初始化udp套接字用于监听注册中心变更服务推送以及发送ack确认、启动一个线程死循环用于监听注册中心udp推送服务变更、执行任务,this就是PushReceiver的引用即任务,所以执行下面的run()逻辑。

💫PushReceiver#run

    @Override
    public void run() {
        while (!closed) {
            try {
                
                // byte[] is initialized with 0 full filled by default
                byte[] buffer = new byte[UDP_MSS];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                // 监听Nacos服务端服务实例信息变更后的通知
                udpSocket.receive(packet);
                
                String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
                NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
                
                PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
                String ack;
                if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
                    // 将数据缓存到本地
                    hostReactor.processServiceJson(pushPacket.data);
                    
                    // send ack to server
                    ack = "{\"type\": \"push-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime + "\", \"data\":"
                            + "\"\"}";
                } else if ("dump".equals(pushPacket.type)) {
                    // dump data to server
                    ack = "{\"type\": \"dump-ack\"" + ", \"lastRefTime\": \"" + pushPacket.lastRefTime + "\", \"data\":"
                            + "\"" + StringUtils.escapeJavaScript(JacksonUtils.toJson(hostReactor.getServiceInfoMap()))
                            + "\"}";
                } else {
                    // do nothing send ack only
                    ack = "{\"type\": \"unknown-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime
                            + "\", \"data\":" + "\"\"}";
                }
                // 发送ack到服务端
                udpSocket.send(new DatagramPacket(ack.getBytes(UTF_8), ack.getBytes(UTF_8).length,
                        packet.getSocketAddress()));
            } catch (Exception e) {
                NAMING_LOGGER.error("[NA] error while receiving push data", e);
            }
        }
    }

主要逻辑:

  1. 监听Nacos服务端服务实例信息变更后的通知
  2. 解析注册中心推送的结果,组装回调ack报文,将注册中心推送的变更服务信息缓存到本地
  3. 发送ack到注册中心,以便注册中心决定是否需要重试。

✨从集成的client模块本地服务发现

本节点讲解的就是客户端服务发现,之所以这样说是因为SpringBoot的自动装配将Nacos的client模块集成进来了,想了解更多去看前面的文章分析。

💫获取服务实例列表

调用重载的selectInstances()方法,healthy默认true即健康,subscribe默认true即订阅

    @Override
    public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
            boolean subscribe) throws NacosException {
        
        ServiceInfo serviceInfo;
        // 默认订阅模式
        if (subscribe) {
            // 委托hostReactor处理
            serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
                    StringUtils.join(clusters, ","));
        } else {
            serviceInfo = hostReactor
                    .getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
                            StringUtils.join(clusters, ","));
        }
        // 选取健康实例
        return selectInstances(serviceInfo, healthy);
    }

默认使用订阅模式,但是委托hostReactor去获取服务信息,以服务名、分组拼接作为入参即Nacos可识别的服务名。

💖从本地缓存/发送http从服务端获取服务信息
    public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
        // failover-mode:默认false
        NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
        String key = ServiceInfo.getKey(serviceName, clusters);
        if (failoverReactor.isFailoverSwitch()) {
            return failoverReactor.getService(key);
        }
        // 从本地缓存serviceInfoMap获取
        ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
        // 如果本地缓存中没有,则发送HTTP调用从Nacos服务端获取
        if (null == serviceObj) {
            serviceObj = new ServiceInfo(serviceName, clusters);
            
            serviceInfoMap.put(serviceObj.getKey(), serviceObj);
            
            updatingMap.put(serviceName, new Object());
            // 更新服务
            updateServiceNow(serviceName, clusters);
            updatingMap.remove(serviceName);
            
        } else if (updatingMap.containsKey(serviceName)) {
            
            if (UPDATE_HOLD_INTERVAL > 0) {
                // hold a moment waiting for update finish等待更新完成
                synchronized (serviceObj) {
                    try {
                        serviceObj.wait(UPDATE_HOLD_INTERVAL);
                    } catch (InterruptedException e) {
                        NAMING_LOGGER
                                .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
                    }
                }
            }
        }
        // 开启一个定时任务,每隔1秒从Nacos服务端获取最新的服务实例信息,
        // 更新到本地缓存seriveInfoMap中
        scheduleUpdateIfAbsent(serviceName, clusters);
        // 从本地缓存serviceInfoMap中获取服务实例信息
        return serviceInfoMap.get(serviceObj.getKey());
    }

主要逻辑:

  1. 从本地缓存serviceInfoMap获取
  2. 如果本地缓存中没有,则发送HTTP调用从Nacos服务端获取
  3. 开启一个定时任务,每隔1秒从Nacos服务端获取最新的服务实例信息, 更新到本地缓存seriveInfoMap中
  4. 从本地缓存serviceInfoMap中获取服务实例信息
💫从本地缓存获取
    private ServiceInfo getServiceInfo0(String serviceName, String clusters) {
        
        String key = ServiceInfo.getKey(serviceName, clusters);
        // 本地缓存serviceInfoMap获取
        return serviceInfoMap.get(key);
    }

就单纯地从本地缓存serviceInfoMap获取

💫发送HTTP调用从Nacos服务端获取
    public void updateService(String serviceName, String clusters) throws NacosException {
        ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
        try {
            // 通过NamingProxy发送HTTP调用,获取服务信息
            String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
            
            if (StringUtils.isNotEmpty(result)) {
                // 更新本地缓存serviceInfoMap
                processServiceJson(result);
            }
        } finally {
            if (oldService != null) {
                synchronized (oldService) {
                    oldService.notifyAll();
                }
            }
        }
    }

通过NamingProxy发送HTTP调用,获取服务信息;响应结果不为空更新本地缓存serviceInfoMap

💫scheduleUpdateIfAbsent()
​
    public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
        if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
            return;
        }
        
        synchronized (futureMap) {
            if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
                return;
            }
            // 启动定时任务
            ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
            futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
        }
    }

​

DEFAULT_DELAY默认1000在这里即1秒。启动定时任务,每隔1秒执行一次,任务逻辑如下:

💫UpdateTask#run()任务逻辑
        @Override
        public void run() {
            long delayTime = DEFAULT_DELAY;
            
            try {
                // 尝试从本地获取
                ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
                
                if (serviceObj == null) {
                    // 本地还是没有则发送http从服务端获取,并缓存到本地
                    updateService(serviceName, clusters);
                    return;
                }

                // 过期服务(服务的最新更新时间小于等于缓存刷新时间),从注册中心重新查询
                if (serviceObj.getLastRefTime() <= lastRefTime) {
                    updateService(serviceName, clusters);
                    serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
                } else {
                    // if serviceName already updated by push, we should not override it
                    // since the push data may be different from pull through force push
                    // 如果 serviceName 已经通过 push 更新,我们不应该覆盖它,
                    // 因为 push 数据可能与 pull through force push 不同
                    refreshOnly(serviceName, clusters);
                }
                // 刷新更新时间
                lastRefTime = serviceObj.getLastRefTime();
                
                if (!eventDispatcher.isSubscribed(serviceName, clusters) && !futureMap
                        .containsKey(ServiceInfo.getKey(serviceName, clusters))) {
                    // abort the update task
                    NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
                    return;
                }
                if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
                    incFailCount();
                    return;
                }


![img](https://img-blog.csdnimg.cn/img_convert/85966f5bbf8c264db8e8afafd01cfa60.png)
![img](https://img-blog.csdnimg.cn/img_convert/548d5e9c11f38373a0158e666d556892.png)
![img](https://img-blog.csdnimg.cn/img_convert/6b9a6863ad61b037432aabca6f4d505c.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

();
                    return;
                }


[外链图片转存中...(img-ZQ2HnRup-1715473938464)]
[外链图片转存中...(img-jwhFqOSk-1715473938464)]
[外链图片转存中...(img-xpEop0pO-1715473938465)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值