Nacos源码学习系列第6篇NacosServer代理类NamingProxy解析

303 阅读5分钟

 NacosNamingService 底层的服务注册、下线服务及服务查询等功能都是通过NamingProxy发送api请求到Nacos Server 完成交互。

NamingProxy 类核心是完成不同接口的请求参数拼装 调用统一的请求处理流程。

目录

属性说明

初始化方法

服务注册

服务订阅

心跳发送


属性说明

    //默认的nacos Server 服务端口
    private static final int DEFAULT_SERVER_PORT = 8848;

    private int serverPort = DEFAULT_SERVER_PORT;

    //可以表示环境信息
    private String namespaceId;

    //远程拉取serverList的地址
    private String endpoint;
    
    //其中某个 nacos Server的地址
    private String nacosDomain;
    
    //配置文件中配置的serverList
    private List<String> serverList;
    //存储远程拉取的serverList
    private List<String> serversFromEndpoint = new ArrayList<String>();
    //最近一次拉取serverList时间
    private long lastSrvRefTime = 0L;
    //多久拉取一次
    private long vipSrvRefInterMillis = TimeUnit.SECONDS.toMillis(30);

    private Properties properties;

初始化方法

//namespaceId:命名空间
//endpoint: 拉取远程服务列表的ip:port
//serverList: 本地配置的serverList
public NamingProxy(String namespaceId, String endpoint, String serverList) {

        this.namespaceId = namespaceId;
        this.endpoint = endpoint;
        if (StringUtils.isNotEmpty(serverList)) {
            this.serverList = Arrays.asList(serverList.split(","));
            if (this.serverList.size() == 1) {
                this.nacosDomain = serverList;
            }
        }

        //如果设置了endpoint属性, 定时从远程拉取serverList
        initRefreshSrvIfNeed();
    }

    //每隔30s从远程更新ServerList
    private void initRefreshSrvIfNeed() {
        if (StringUtils.isEmpty(endpoint)) {
            return;
        }

        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.naming.serverlist.updater");
                t.setDaemon(true);
                return t;
            }
        });
        //vipSrvRefInterMillis:30s 执行一次
        executorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                refreshSrvIfNeed();
            }
        }, 0, vipSrvRefInterMillis, TimeUnit.MILLISECONDS);

        refreshSrvIfNeed();
    }

    private void refreshSrvIfNeed() {
        try {
            //如果本地已经设置了serverList 不从远程拉取serverList
            if (!CollectionUtils.isEmpty(serverList)) {
                NAMING_LOGGER.debug("server list provided by user: " + serverList);
                return;
            }

            //间隔时间未到
            if (System.currentTimeMillis() - lastSrvRefTime < vipSrvRefInterMillis) {
                return;
            }
            
            //从endpoint属性提供的地址拉取serverList
            List<String> list = getServerListFromEndpoint();

            if (CollectionUtils.isEmpty(list)) {
                throw new Exception("Can not acquire Nacos list");
            }

            if (!CollectionUtils.isEqualCollection(list, serversFromEndpoint)) {
                NAMING_LOGGER.info("[SERVER-LIST] server list is updated: " + list);
            }
            //存储结果
            serversFromEndpoint = list;
            //存储最近一次拉取serverList 时间
            lastSrvRefTime = System.currentTimeMillis();
        } catch (Throwable e) {
            NAMING_LOGGER.warn("failed to update server list", e);
        }
    }

serverList: 提供了2个种方式获取 1、本地化的配置 2、提供查询的端点地址

一般小集群或者服务的ip不怎么改变的情况可以直接本地化配置,

如果集群数量比较大或者ip会变化就只能用第二种方式

服务注册

    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        ...
        final Map<String, String> params = new HashMap<String, String>(9);
        //4个通用参数     
        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);

    }

ephemeral: 代表是否是临时节点[可以随时上下线的节点] 一般我们的服务注册的都是临时节点 相对的还有一种持久化的节点 这种一般我们得不多,后面讲到服务端代码时再作说明

注册服务的参数包括: 命名空间 服务名 分组名 集群名 ip port 权重 是否可用 是否健康 是否临时节点 扩展数据

public String reqAPI(String api, Map<String, String> params, List<String> servers, String method) {

        params.put(CommonParams.NAMESPACE_ID, getNamespaceId());

        if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(nacosDomain)) {
            throw new IllegalArgumentException("no server available");
        }

        Exception exception = new Exception();

        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 {
                    return callServer(api, params, server, method);
                } catch (NacosException e) {
                    exception = e;
                    NAMING_LOGGER.error("request {} failed.", server, e);
                } catch (Exception e) {
                    exception = e;
                    NAMING_LOGGER.error("request {} failed.", server, e);
                }
                //如果失败会逐个服务器做轮询
                index = (index + 1) % servers.size();
            }

            throw new IllegalStateException("failed to req API:" + api + " after all servers(" + servers + ") tried: "
                + exception.getMessage());
        }
       
        //如果服务地址列表为空, 就使用nacosDomain地址请求,重试次数3次
        for (int i = 0; i < UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT; i++) {
            try {
                return callServer(api, params, nacosDomain);
            } catch (Exception e) {
                exception = e;
                NAMING_LOGGER.error("[NA] req api:" + api + " failed, server(" + nacosDomain, e);
            }
        }

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

    }

 这里有2点要注意 1、服务会自动fail over 轮询下一个服务器  2、失败会重试3次 

 public String callServer(String api, Map<String, String> params, String curServer, String method)
        throws NacosException {
        long start = System.currentTimeMillis();
        long end = 0;
        // 加上签名数据
        checkSignature(params);
        List<String> headers = builderHeaders();

        //如果以http://或者https://开头 直接拼接api路径
        //如果么有端口信息则拼上默认端口8848或属性【nacos.naming.exposed.port】指定得端口信息
        //否则拼接上http://或者https://
        String url;
        if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {
            url = curServer + api;
        } else {
            if (!curServer.contains(UtilAndComs.SERVER_ADDR_IP_SPLITER)) {
                curServer = curServer + UtilAndComs.SERVER_ADDR_IP_SPLITER + serverPort;
            }
            url = HttpClient.getPrefix() + curServer + api;
        }
        //底层通过HttpUrlConnection发起请求
        HttpClient.HttpResult result = HttpClient.request(url, headers, params, UtilAndComs.ENCODING, method);
        end = System.currentTimeMillis();

        //基于Prometheus指标收集工具收集数据 
        MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code))
            .observe(end - start);

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

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

        throw new NacosException(NacosException.SERVER_ERROR, "failed to req API:"
            + curServer + api + ". code:"
            + result.code + " msg: " + result.content);
    }

 // 对数据(serviceName+时间戳)使用sk签名 一是身份验证 二是防篡改 个人赶紧这个场景身份验证的含义 
 // 更多
 private void checkSignature(Map<String, String> params) {
        String ak = getAccessKey();
        String sk = getSecretKey();
        params.put("app", AppNameUtils.getAppName());
        if (StringUtils.isEmpty(ak) && StringUtils.isEmpty(sk)) {
            return;
        }

        try {
            String signData = getSignData(params.get("serviceName"));
            String signature = SignUtil.sign(signData, sk);
            params.put("signature", signature);
            params.put("data", signData);
            params.put("ak", ak);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

其他的服务下线、服务实例查询等接口的通用上面的嗲用逻辑,区别在于请求路径和入参和method、以及是否需要签名。

其他几个需要单独说明的接口如下:

  public void updateService(Service service, AbstractSelector selector) throws 
             NacosException 
    {
        NAMING_LOGGER.info("[UPDATE-SERVICE] {} updating service : {}",
            namespaceId, service);

        final Map<String, String> params = new HashMap<String, String>(6);
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, service.getName());
        params.put(CommonParams.GROUP_NAME, service.getGroupName());
        params.put("protectThreshold", String.valueOf(service.getProtectThreshold()));
        params.put("metadata", JSON.toJSONString(service.getMetadata()));
        params.put("selector", JSON.toJSONString(selector));

        reqAPI(UtilAndComs.NACOS_URL_SERVICE, params, HttpMethod.PUT);
    }

    public ListView<String> getServiceList(int pageNo, int pageSize, String groupName, 
        AbstractSelector selector) throws NacosException {...}


   public class ListView<T> {
       //当前页的数据
       private List<T> data;
       //总的数据
       private int count;
       ....
   }

 AbstractSelector selector:是服务和实例的筛选条件 后面解析到服务端代码的时候详细介绍。

getServiceList:是一个分页查询 返回总数量和当页的数据列表

服务订阅

 @Override
    public ServiceInfo subscribe(String serviceName, String groupName, String clusters) 
                  throws NacosException {
        return queryInstancesOfService(serviceName, groupName, clusters, 
                   pushReceiver.getUdpPort(), false);
    }
    
    @Override
    public void unsubscribe(String serviceName, String groupName, String clusters) 
                   throws NacosException {
    }


    @Override
    public ServiceInfo queryInstancesOfService(String serviceName, String groupName, 
                    String clusters, int udpPort,
            boolean healthyOnly) throws NacosException {
        final Map<String, String> params = new HashMap<String, String>(16);
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, NamingUtils.getGroupedName(serviceName, 
           groupName));
        params.put(CLUSTERS_PARAM, clusters);
        //这2个参数,作用服务端后面主动推送udp消息到客户端
        params.put(UDP_PORT_PARAM, String.valueOf(udpPort));
        params.put(CLIENT_IP_PARAM, NetUtils.localIP());
        params.put(HEALTHY_ONLY_PARAM, String.valueOf(healthyOnly));
        String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, 
           HttpMethod.GET);
        if (StringUtils.isNotEmpty(result)) {
            return JacksonUtils.toObj(result, ServiceInfo.class);
        }
        return new ServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), 
           clusters);
    }

subscribe方法: pushReceiver.getUdpPort() 这个参数是为了后面方便服务端通过udp推送服务变更消息给客户端

unsubscribe方法: 方法体为空说明不提供取消订阅的功能

queryInstancesOfService: 返回的【ServiceInfo】对象包装了服务名、组名、集群及集群下的实例列表的信息, 如果没有查询到就创建一个空的【ServiceInfo】对象

心跳发送方法

    public long sendBeat(BeatInfo beatInfo) {
        try {
            ...
            Map<String, String> params = new HashMap<String, String>(4);
            params.put("beat", JSON.toJSONString(beatInfo));
            params.put(CommonParams.NAMESPACE_ID, namespaceId);
            params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
            String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat",                     
                  params, HttpMethod.PUT);
            JSONObject jsonObject = JSON.parseObject(result);

            if (jsonObject != null) {
                return jsonObject.getLong("clientBeatInterval");
            }
        }...
        return 0L;
    }

    public boolean serverHealthy() {

        try {
            String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/operator/metrics", new 
            HashMap<String, String>(2));
            JSONObject json = JSON.parseObject(result);
            String serverStatus = json.getString("status");
            return "UP".equals(serverStatus);
        } catch (Exception e) {
            return false;
        }
    }

public class BeatInfo {

    private int port;
    private String ip;
    private double weight;
    private String serviceName;
    private String cluster;
    private Map<String, String> metadata;
    private volatile boolean scheduled;
    private volatile long period;
    private volatile boolean stopped;
    ...
}

sendBeat: 发送心跳服务接收一个BeatInfo 参数,包含了Instance实例的基本属性在此不赘述,其他几个新的属性说明如下:

stopped:心跳信息是否被移除(移除后不会被执行)  

period: 心跳任务多久一行执行

serverHealthy(): 检查Nacos Server的状态