Nacos初探(3)-- 服务发现原理解析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Mr_Errol/article/details/85089129
上面已经大概了解了一下Nacos服务注册的逻辑,接下来来看看服务是如何被发现已经消费的。

一、服务发现前

目前在Spring Cloud,基本都是使用Feign去调用服务,Feign其实也是Ribbon的一个封装,主要功能,是将我们通常http请求服务这个过程帮我们封装起来,使我们使用时更加的简便,通过一个注解就能实现对服务的调用,对于ribbon的源码解析,参考这篇文章:

https://blog.csdn.net/forezp/article/details/74820899

文章讲的非常清楚了,这里大概总结一下,ribbon最最底层也是实现spring cloud common包下的

org.springframework.cloud.alibaba.nacos.ribbon.NacosServerList

接口,主要是ServiceInstanceChooser 下的继承接口:

org.springframework.cloud.client.loadbalancer.LoadBalancerClient

这是Ribbon实现负载均衡的父类接口,接下来一系列的接口实现最终会落到如何获取serverList这个问题是,答案在这个接口:

com.netflix.loadbalancer.ServerList

接下来,就是服务发现组件的事情了,比如,eureka对于这个接口的实现就是

DiscoveryEnabledNIWSServerList

而,Nacos的实现就是:org.springframework.cloud.alibaba.nacos.ribbon.NacosServerList,这也是我们的重点。

二、服务发现

上面主要是讲述了一下,服务发现的身世,Springcloud是如何走到获取服务列表这一步的,期间经过了ribbon的负载均衡,最后落到了Nacos的实现类NacosServerList中。来看代码

public class NacosServerList extends AbstractServerList<NacosServer> {
 
    @Autowired
    private NacosDiscoveryProperties discoveryProperties;
 
    private String serviceId;
 
    public NacosServerList() {
    }
 
    public NacosServerList(String serviceId) {
        this.serviceId = serviceId;
    }
 
    @Override
    public List<NacosServer> getInitialListOfServers() {
        return getServers();
    }
 
    @Override
    public List<NacosServer> getUpdatedListOfServers() {
        return getServers();
    }
 
    private List<NacosServer> getServers() {
        try {
            List<Instance> instances = discoveryProperties.namingServiceInstance()
                    .getAllInstances(serviceId);
            return instancesToServerList(instances);
        }
        catch (Exception e) {
            throw new IllegalStateException(
                    "Can not get service instances from nacos, serviceId=" + serviceId,
                    e);
        }
    }
 
    private List<NacosServer> instancesToServerList(List<Instance> instances) {
        List<NacosServer> result = new ArrayList<>(instances.size());
        for (Instance instance : instances) {
            if (instance.isHealthy()) {
                result.add(new NacosServer(instance));
            }
        }
 
        return result;
    }
 
    public String getServiceId() {
        return serviceId;
    }
 
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        this.serviceId = iClientConfig.getClientName();
    }
}
从ribbon的代码一路跟踪过来,最后落到了getServers()方法中,继续跟踪,com.alibaba.nacos.client.naming.NacosNamingService#getAllInstances(java.lang.String, java.util.List<java.lang.String>)

com.alibaba.nacos.client.naming.core.HostReactor#getServiceInfo(java.lang.String, java.lang.String, java.lang.String, boolean)

这里是具体逻辑的实现,在这个类中HostReactor,HostReactor这个类中维护了一个serviceInfoMap,顾名思义,维护了serverList的信息,key值是serverName,value是ServiceInfo;类中还有一个定时任务ScheduledExecutorService,后面会使用到。getServiceInfo()方法主要逻辑是:

1、先从已经存在的serviceInfoMap中通过serverName获取一个ServiceInfo,如果已经有了,需要再判断,另一个updatingMap是否存在这个key,如果存在,在wait 5秒,这个时间是写死的。在返回ServiceInfo前,调用scheduleUpdateIfAbsent()方法更新。

2、如果上面第一步serviceInfoMap不存在,则将传来的参数(erviceName, clusters, env)构建一个ServiceInfo,同时维护到serviceInfoMap和updatingMap中,同时根据allIPs这个参数的不同(我断点时为false)调用不同的接口去Nacos服务端拉取数据,通过方法updateService4AllIPNow 和 updateServiceNow,最后与上一步一样,调用scheduleUpdateIfAbsent方法。

3、scheduleUpdateIfAbsent方法,维护另一个map --futureMap,看代码:

public void scheduleUpdateIfAbsent(String serviceName, String clusters, String env, boolean allIPs) {
        if (futureMap.get(ServiceInfo.getKey(serviceName, clusters, env, allIPs)) != null) {
            return;
        }
 
        synchronized (futureMap) {
            if (futureMap.get(ServiceInfo.getKey(serviceName, clusters, env, allIPs)) != null) {
                return;
            }
 
            ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters, env, allIPs));
            futureMap.put(ServiceInfo.getKey(serviceName, clusters, env, allIPs), future);
        }
    }
最重要的是,将一个UpdateTask 添加到了定时任务当中启动了,这个UpdateTask.run() 的代码:

@Override
        public void run() {
            try {
                ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters, env, allIPs));
 
                if (serviceObj == null) {
                    if (allIPs) {
                        updateService4AllIPNow(serviceName, clusters, env);
                    } else {
                        updateServiceNow(serviceName, clusters, env);
                        executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
                    }
                    return;
                }
 
                if (serviceObj.getLastRefTime() <= lastRefTime) {
                    if (allIPs) {
                        updateService4AllIPNow(serviceName, clusters, env);
                        serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters, env, true));
                    } else {
                        updateServiceNow(serviceName, clusters, env);
                        serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters, env));
                    }
 
                } else {
                    // if serviceName already updated by push, we should not override it
                    // since the push data may be different from pull through force push
                    refreshOnly(serviceName, clusters, env, allIPs);
                }
 
                executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
 
                lastRefTime = serviceObj.getLastRefTime();
            } catch (Throwable e) {
                LogUtils.LOG.warn("NA", "failed to update serviceName: " + serviceName, e);
            }
 
        }
非常的清晰明朗,如果serviceInfoMap中不存在需要的serverName,则,通过方法updateService4AllIPNow 和 updateServiceNow调用接口去获取,所以,serviceInfoMap并不是只在调用服务时才去获取更新,而是通过定时任务,通过心跳式的方式,不停的异步更新,10秒更新一次。

4、最后获取到ServiceInfo,将ServiceInfo.hosts返回为一个集合,因为同一个服务,可能有多个实例,host可能不一样。

public List<Instance> getAllInstances(String serviceName, List<String> clusters) throws NacosException {
 
        ServiceInfo serviceInfo = hostReactor.getServiceInfo(serviceName, StringUtils.join(clusters, ","), StringUtils.EMPTY, false);
        List<Instance> list;
        if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
            return new ArrayList<Instance>();
        }
        return list;
    }
以上就是Nacos 客户端服务发现的逻辑。

下面来看服务端如何处理请求的。

 

三、服务端处理服务发现请求

上面说到的两个接口分别是:

/nacos/v1/ns/api/srvAllIP  断点调用,我们就只看这一个接口。

/nacos/v1/ns/api/srvIPXT

@RequestMapping("/srvIPXT")
    @ResponseBody
    public JSONObject srvIPXT(HttpServletRequest request) throws Exception {
 
        JSONObject result = new JSONObject();
 
        if (DistroMapper.getLocalhostIP().equals(UtilsAndCommons.LOCAL_HOST_IP)) {
            throw new Exception("invalid localhost ip: " + DistroMapper.getLocalhostIP());
        }
 
        String dom = BaseServlet.required(request, "dom");
 
        VirtualClusterDomain domObj = (VirtualClusterDomain) domainsManager.getDomain(dom);
        String agent = request.getHeader("Client-Version");
        String clusters = BaseServlet.optional(request, "clusters", StringUtils.EMPTY);
        String clientIP = BaseServlet.optional(request, "clientIP", StringUtils.EMPTY);
        Integer udpPort = Integer.parseInt(BaseServlet.optional(request, "udpPort", "0"));
        String env = BaseServlet.optional(request, "env", StringUtils.EMPTY);
        String error = BaseServlet.optional(request, "unconsistentDom", StringUtils.EMPTY);
        boolean isCheck = Boolean.parseBoolean(BaseServlet.optional(request, "isCheck", "false"));
 
        String app = BaseServlet.optional(request, "app", StringUtils.EMPTY);
 
        String tenant = BaseServlet.optional(request, "tid", StringUtils.EMPTY);
 
        boolean healthyOnly = Boolean.parseBoolean(BaseServlet.optional(request, "healthOnly", "false"));
 
        if (!StringUtils.isEmpty(error)) {
            Loggers.ROLE_LOG.info("ENV-NOT-CONSISTENT", error);
        }
 
        if (domObj == null) {
            throw new NacosException(NacosException.NOT_FOUND, "dom not found: " + dom);
        }
 
        checkIfDisabled(domObj);
 
        long cacheMillis = Switch.getCacheMillis(dom);
 
        // now try to enable the push
        try {
            if (udpPort > 0 && PushService.canEnablePush(agent)) {
                PushService.addClient(dom,
                        clusters,
                        agent,
                        new InetSocketAddress(clientIP, udpPort),
                        pushDataSource,
                        tenant,
                        app);
                cacheMillis = Switch.getPushCacheMillis(dom);
            }
        } catch (Exception e) {
            Loggers.SRV_LOG.error("VIPSRV-API", "failed to added push client", e);
            cacheMillis = Switch.getCacheMillis(dom);
        }
 
        List<IpAddress> srvedIPs;
 
        srvedIPs = domObj.srvIPs(clientIP, Arrays.asList(StringUtils.split(clusters, ",")));
 
        if (CollectionUtils.isEmpty(srvedIPs)) {
            String msg = "no ip to serve for dom: " + dom;
 
            Loggers.SRV_LOG.debug(msg);
        }
 
        Map<Boolean, List<IpAddress>> ipMap = new HashMap<>(2);
        ipMap.put(Boolean.TRUE, new ArrayList<IpAddress>());
        ipMap.put(Boolean.FALSE, new ArrayList<IpAddress>());
 
        for (IpAddress ip : srvedIPs) {
            ipMap.get(ip.isValid()).add(ip);
        }
 
        if (isCheck) {
            result.put("reachProtectThreshold", false);
        }
 
        double threshold = domObj.getProtectThreshold();
 
        if ((float) ipMap.get(Boolean.TRUE).size() / srvedIPs.size() <= threshold) {
 
            Loggers.SRV_LOG.warn("protect threshold reached, return all ips, " +
                    "dom: " + dom);
            if (isCheck) {
                result.put("reachProtectThreshold", true);
            }
 
            ipMap.get(Boolean.TRUE).addAll(ipMap.get(Boolean.FALSE));
            ipMap.get(Boolean.FALSE).clear();
        }
 
        if (isCheck) {
            result.put("protectThreshold", domObj.getProtectThreshold());
            result.put("reachLocalSiteCallThreshold", false);
 
            return new JSONObject();
        }
 
        JSONArray hosts = new JSONArray();
 
        for (Map.Entry<Boolean, List<IpAddress>> entry : ipMap.entrySet()) {
            List<IpAddress> ips = entry.getValue();
 
            if (healthyOnly && !entry.getKey()) {
                continue;
            }
 
            for (IpAddress ip : ips) {
                JSONObject ipObj = new JSONObject();
 
                ipObj.put("ip", ip.getIp());
                ipObj.put("port", ip.getPort());
                ipObj.put("valid", entry.getKey());
                ipObj.put("marked", ip.isMarked());
                ipObj.put("instanceId", ip.getInstanceId());
                ipObj.put("metadata", ip.getMetadata());
                ipObj.put("enabled", ip.isEnabled());
                ipObj.put("weight", ip.getWeight());
                ipObj.put("clusterName", ip.getClusterName());
                ipObj.put("serviceName", ip.getServiceName());
                hosts.add(ipObj);
 
            }
        }
 
        result.put("hosts", hosts);
 
        result.put("dom", dom);
        result.put("cacheMillis", cacheMillis);
        result.put("lastRefTime", System.currentTimeMillis());
        result.put("checksum", domObj.getChecksum() + System.currentTimeMillis());
        result.put("useSpecifiedURL", false);
        result.put("clusters", clusters);
        result.put("env", env);
        result.put("metadata", domObj.getMetadata());
        return result;
    }
代码逻辑也非常直白,就是根据请求的参数dom,去DomainManager管理的domMap中获取对应的Domain信息,如果为空则抛异常,否则,将Domain信息已一个JSONObject返回,其中服务对应的IP信息以数组的形式返回(单个服务多个实例)。
————————————————
版权声明:本文为CSDN博主「Errol的杂货铺」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Mr_Errol/article/details/85089129

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值