nacos服务注册与发现之客户端

  1. 服务注册
    1.1 NamingService.registerInstance的方法为客户端提供的服务注册接口
    1.2 客户端通过调用NamingService.registerService上报到nacos节点
    1.3 客户端通过BeatReactor.addBeatInfo定时上报心跳信息到nacos节点

//客户端NamingService接口实现类
@SuppressWarnings("PMD.ServiceOrDaoClassShouldEndWithImplRule")
public class NacosNamingService implements NamingService {

     /**
     *
     * @param serviceName name of service; SpringCloud项目一般为spring.application.name
     * @param groupName   group of service;位置为“DEFAULT_GROUP”
     * @param instance    instance to register
     * @throws NacosException
     */
    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            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());
            //启动心跳任务
            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }
        //向nacos节点注册服务
        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }
}

public class NamingProxy {
    
    /**
     * 客户端调用此方法向nacos节点注册服务
     * 构造请求参数
     * @param serviceName name of service
     * @param groupName   group of service
     * @param instance    instance to register
     * @throws NacosException
     */
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        final Map<String, String> params = new HashMap<String, String>(9);
        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", JSON.toJSONString(instance.getMetadata()));

        reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);
    }

    /**
     * 从集群列表随机下标开始循环所有nacos集群节点注册服务,只要一个成功则返回
     * 都不成功则按指定REQUEST_DOMAIN_RETRY_COUNT次数重试
     * @param api = "/nacos/v1/ns/instance"
     * @param params 请求参数
     * @param body  = String.empty
     * @param servers = nacos集群节点集合
     * @param method = HttpMethod.POST
     * @return
     * @throws NacosException
     */
   public String reqAPI(String api, Map<String, String> params, String body, List<String> servers, String method) throws NacosException {

        params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
        NacosException exception = new NacosException();

        if (servers != null && !servers.isEmpty()) {
            Random random = new Random(System.currentTimeMillis());
            //随机集合下标
            int index = random.nextInt(servers.size());
            for (int i = 0; i < servers.size(); i++) {
                String server = servers.get(index);
                try {
                    //nacos节点注册服务
                    return callServer(api, params, body, server, method);
                } catch (NacosException e) {
                    exception = e;
                }
                index = (index + 1) % servers.size();
            }
        }
        //如果上面失败则重试
        if (StringUtils.isNotBlank(nacosDomain)) {
            for (int i = 0; i < UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT; i++) {
                try {
                    return callServer(api, params, body, nacosDomain, method);
                } catch (NacosException e) {
                    exception = e;
                }
            }
        }

        throw new NacosException(exception.getErrCode(), "failed to req API:/api/" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
    }

  /**
     * 通过HttpMethod.POST请求当前节点的"/nacos/v1/ns/instance"地址注册服务
     * @param api = "/nacos/v1/ns/instance"
     * @param params 请求参数
     * @param body = String.empty
     * @param curServer 当前nacos节点
     * @param method = HttpMethod.POST
     */
   public String callServer(String api, Map<String, String> params, String body, String curServer, String method) throws NacosException {
        List<String> headers = builderHeaders();
        // https://127.0.0.1:8848/nacos/v1/ns/instance
        String url = curServer + api;

        HttpClient.HttpResult result = HttpClient.request(url, headers, params, body, UtilAndComs.ENCODING, method);

        if (HttpURLConnection.HTTP_OK == result.code) {
            return result.content;
        }

        if (HttpURLConnection.HTTP_NOT_MODIFIED == result.code) {
            return StringUtils.EMPTY;
        }

        throw new NacosException(result.code, result.content);
    }

1.3 客户端通过BeatReactor.addBeatInfo定时上报心跳信息到nacos节点

public class BeatReactor {
    /**
     * 注册服务的时候启动心跳
     * @param serviceName  服务名
     * @param beatInfo     心跳信息
     */
    public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        //fix #1733
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
    }
    /**
     * BeatReactor内部类:心跳上报
     */
    class BeatTask implements Runnable {

        @Override
        public void run() {
            long nextTime = beatInfo.getPeriod();
            try {
                JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
                long interval = result.getIntValue("clientBeatInterval");
                if (interval > 0) {
                    nextTime = interval;
                }
                ...........................
            } catch (NacosException ne) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}", JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());
            }
            //本次心跳发送完成启动下次心跳任务
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }
    }
}

public class NamingProxy {
    /**
     * 发送心跳到nacos节点"/nacos/v1/ns/instance/beat"
     * @param beatInfo  心跳信息
     * @param lightBeatEnabled=false: 用body传数据
     */
    public JSONObject sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
        Map<String, String> params = new HashMap<String, String>(8);
        String body = StringUtils.EMPTY;
        if (!lightBeatEnabled) {
            try {
                body = "beat=" + URLEncoder.encode(JSON.toJSONString(beatInfo), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new NacosException(NacosException.SERVER_ERROR, "encode beatInfo error", e);
            }
        }
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
        params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
        params.put("ip", beatInfo.getIp());
        params.put("port", String.valueOf(beatInfo.getPort()));
        String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, body, HttpMethod.PUT);
        return JSON.parseObject(result);
    }
}
  1. 服务发现
    2.1 NamingService.subscribe()的方法为客户端提供的服务发现接口
    2.2 客户端通过调用NamingService.subscribe()注册服务变更监听器
    2.3 客户端通启动线程通过UDP协议接收服务的变更通知
    2.4 客户端通启动线程通过HTTP协议拉取服务信息
    2.5 解析报文判断服务是否变更,有EventDispatcher分发变更通知

public class NacosNamingService implements NamingService {

    /**
     * 客户端服务发现API
     * @param serviceName name of service
     * @param groupName   group of service
     * @param clusters    list of cluster
     * @param listener    event listener
     * @throws NacosException
     */
    @Override
    public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener) throws NacosException {
        eventDispatcher.addListener(hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
            StringUtils.join(clusters, ",")), StringUtils.join(clusters, ","), listener);
    }
}

/**
 * addListener注册监听器
 * serviceChanged方法添加事件到队列
 * Notifier拉取事件队列分发到各个监听器
 */
public class EventDispatcher {
    private ExecutorService executor = null;
    //服务变更队列
    private BlockingQueue<ServiceInfo> changedServices = new LinkedBlockingQueue<ServiceInfo>();

    //单个服务监听列表
    private ConcurrentMap<String, List<EventListener>> observerMap = new ConcurrentHashMap<String, List<EventListener>>();

    public EventDispatcher() {
        //初始化线程池
        ...............
        //启动通知线程
        executor.execute(new Notifier());
    }

    /**
     * 客户端注册监听事件
     * @param serviceInfo 要监听的服务信息
     * @param clusters
     * @param listener    事件回调对象
     */
     public void addListener(ServiceInfo serviceInfo, String clusters, EventListener listener) {
        List<EventListener> observers = Collections.synchronizedList(new ArrayList<EventListener>());
        observers.add(listener);
        observers = observerMap.putIfAbsent(ServiceInfo.getKey(serviceInfo.getName(), clusters), observers);
        if (observers != null) {
            observers.add(listener);
        }

        serviceChanged(serviceInfo);
    }

    /**
     * 变更的服务对象添加到队列
     * @param serviceInfo
     */
    public void serviceChanged(ServiceInfo serviceInfo) {
        if (serviceInfo == null) {
            return;
        }
        changedServices.add(serviceInfo);
    }

    private class Notifier implements Runnable {
        @Override
        public void run() {
            while (true) {
                ServiceInfo serviceInfo = null;
                try {
                    //从队列拉取变更的服务对象
                    serviceInfo = changedServices.poll(5, TimeUnit.MINUTES);
                } catch (Exception ignore) {
                }

                try {
                    //通知每个监听对象
                    List<EventListener> listeners = observerMap.get(serviceInfo.getKey());
                    if (!CollectionUtils.isEmpty(listeners)) {
                        for (EventListener listener : listeners) {
                            List<Instance> hosts = Collections.unmodifiableList(serviceInfo.getHosts());
                            listener.onEvent(new NamingEvent(serviceInfo.getName(), serviceInfo.getGroupName(), serviceInfo.getClusters(), hosts));
                        }
                    }

                } catch (Exception e) {
                    NAMING_LOGGER.error("[NA] notify error for service: " + serviceInfo.getName() + ", clusters: " + serviceInfo.getClusters(), e);
                }
            }
        }
    }
}

2.3 客户端通启动线程通过UDP协议接收服务的变更通知


public class PushReceiver implements Runnable {
   @Override
    public void run() {
        while (true) {
            try {
                byte[] buffer = new byte[UDP_MSS];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                udpSocket.receive(packet);

                String json = new String(IoUtils.tryDecompress(packet.getData()), "UTF-8").trim();
                PushPacket pushPacket = JSON.parseObject(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\":" + "\"\"}";
                }
                ....................
                udpSocket.send(new DatagramPacket(ack.getBytes(Charset.forName("UTF-8")), ack.getBytes(Charset.forName("UTF-8")).length, packet.getSocketAddress()));
            } catch (Exception e) {
                NAMING_LOGGER.error("[NA] error while receiving push data", e);
            }
        }
    }
}

2.4 客户端启动线程通过HTTP协议拉取服务信息

public class HostReactor {
   
   /**
     * 定时器根据条件从服务器拉取服务信息
     */
   public class UpdateTask implements Runnable {
        @Override
        public void run() {
            long delayTime = -1;
            try {
                ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
                
                if (serviceObj == null) {
                    updateServiceNow(serviceName, clusters);
                    delayTime = DEFAULT_DELAY;
                    return;
                }

                if (serviceObj.getLastRefTime() <= lastRefTime) {
                    updateServiceNow(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
                    refreshOnly(serviceName, clusters);
                }


            } catch (Throwable e) {
                NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
            } finally {
                if (delayTime > 0) {
                    executor.schedule(this, delayTime, TimeUnit.MILLISECONDS);
                }
            }

        }
    }
   
    /**
     * 从服务器拉取服务信息: 地址:"/nacos/v1/ns/instance/list"
     * @param serviceName 
     * @param clusters
     */
    public void updateServiceNow(String serviceName, String clusters) {
        ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
        try {
            String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);
            if (StringUtils.isNotEmpty(result)) {
                processServiceJSON(result);
            }
        } catch (Exception e) {
            NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
        } finally {
            if (oldService != null) {
                synchronized (oldService) {
                    oldService.notifyAll();
                }
            }
        }
    }
}

2.5 解析报文判断服务是否变更,有EventDispatcher分发变更通知


     /**
     * 解析报文如果本地服务集合没有此服务或服务已变更测调用eventDispatcher.serviceChanged()通知所有监听器
     * @param json
     * @return
     */
     public ServiceInfo processServiceJSON(String json) {
        ServiceInfo serviceInfo = JSON.parseObject(json, ServiceInfo.class);
        ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
        boolean changed = false;

        //如果已存在服务则判断是否变更
        if (oldService != null) {
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);

            Map<String, Instance> oldHostMap = new HashMap<String, Instance>(oldService.getHosts().size());
            for (Instance host : oldService.getHosts()) {
                oldHostMap.put(host.toInetAddr(), host);
            }

            Map<String, Instance> newHostMap = new HashMap<String, Instance>(serviceInfo.getHosts().size());
            for (Instance host : serviceInfo.getHosts()) {
                newHostMap.put(host.toInetAddr(), host);
            }

            Set<Instance> modHosts = new HashSet<Instance>();
            Set<Instance> newHosts = new HashSet<Instance>();
            Set<Instance> remvHosts = new HashSet<Instance>();

            List<Map.Entry<String, Instance>> newServiceHosts = new ArrayList<Map.Entry<String, Instance>>(newHostMap.entrySet());
            for (Map.Entry<String, Instance> entry : newServiceHosts) {
                Instance host = entry.getValue();
                String key = entry.getKey();
                if (oldHostMap.containsKey(key) && !StringUtils.equals(host.toString(),
                    oldHostMap.get(key).toString())) {
                    modHosts.add(host);
                    continue;
                }

                if (!oldHostMap.containsKey(key)) {
                    newHosts.add(host);
                }
            }

            for (Map.Entry<String, Instance> entry : oldHostMap.entrySet()) {
                Instance host = entry.getValue();
                String key = entry.getKey();
                if (newHostMap.containsKey(key)) {
                    continue;
                }

                if (!newHostMap.containsKey(key)) {
                    remvHosts.add(host);
                }

            }
            
            if (newHosts.size() > 0) {
                changed = true;
            }

            if (remvHosts.size() > 0) {
                changed = true;
            }

            if (modHosts.size() > 0) {
                changed = true;
            }
            serviceInfo.setJsonFromServer(json);

            //
            if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
                eventDispatcher.serviceChanged(serviceInfo);
                DiskCache.write(serviceInfo, cacheDir);
            }

        } else {
            //服务不存在则加入到服务集合
            changed = true;
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
            eventDispatcher.serviceChanged(serviceInfo);
            serviceInfo.setJsonFromServer(json);
            DiskCache.write(serviceInfo, cacheDir);
        }

        MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());

        if (changed) {
            NAMING_LOGGER.info("current ips:(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() +
                " -> " + JSON.toJSONString(serviceInfo.getHosts()));
        }

        return serviceInfo;
    }
posted @ 2020-11-09 20:25  wenlongliu  阅读(1151)  评论(0)    收藏  举报