Nacos 2.0原理解析(二):寻址机制

什么是Nacos的寻址机制?

Nacos 支持单机部署以及集群部署,针对单机模式,Nacos 只是自己和自己通信;对于集群模式,则集群内的每个 Nacos 成员都需要相互通信。因此这就带来一个问题,该以何种方式去管理集群内的 Nacos 成员节点信息,这就是 Nacos 内部的寻址机制。

源码分析

寻址初始化

在Nacos中,ServerMemberManager 类存储着本节点所知道的所有成员节点列表信息,提供了针对成员节点的增删改查操作,同时维护了一个 MemberLookup 列表,方便进行动态切换成员节点寻址方式,是寻址逻辑的核心。

首先看ServerMemberManager类中的初始化方法init()。主要负责一些基本的设置、发布MembersChangeEvent事件并订阅IPChangeEvent事件,初始化寻址模式适配器并启动。

protected void init() throws NacosException {
    Loggers.CORE.info("Nacos-related cluster resource initialization");
    this.port = EnvUtil.getProperty(SERVER_PORT_PROPERTY, Integer.class, DEFAULT_SERVER_PORT);
    this.localAddress = InetUtils.getSelfIP() + ":" + port;
    this.self = MemberUtil.singleParse(this.localAddress);
    this.self.setExtendVal(MemberMetaDataConstants.VERSION, VersionUtils.version);
    
    // init abilities.
    this.self.setAbilities(initMemberAbilities());
    
    serverList.put(self.getAddress(), self);
    //发布MembersChangeEvent事件并订阅IPChangeEvent事件
    // register NodeChangeEvent publisher to NotifyManager
    registerClusterEvent();
    //初始化寻址模式适配器并启动
    // Initializes the lookup mode
    initAndStartLookup();
    
    if (serverList.isEmpty()) {
        throw new NacosException(NacosException.SERVER_ERROR, "cannot get serverlist, so exit.");
    }
    
    Loggers.CORE.info("The cluster resource is initialized");
}

首先看registerClusterEvent方法,发布了MembersChangeEvent事件,订阅了订阅IPChangeEvent事件,事件触发后回调onEvent方法,将节点的属性赋上更新后的值。

private void registerClusterEvent() {
    //发布MembersChangeEvent事件
    // Register node change events
    NotifyCenter.registerToPublisher(MembersChangeEvent.class,
            EnvUtil.getProperty(MEMBER_CHANGE_EVENT_QUEUE_SIZE_PROPERTY, Integer.class,
                    DEFAULT_MEMBER_CHANGE_EVENT_QUEUE_SIZE));
    
    //订阅IPChangeEvent事件
    // The address information of this node needs to be dynamically modified
    // when registering the IP change of this node
    NotifyCenter.registerSubscriber(new Subscriber<InetUtils.IPChangeEvent>() {
        @Override
        public void onEvent(InetUtils.IPChangeEvent event) {
            String newAddress = event.getNewIP() + ":" + port;
            ServerMemberManager.this.localAddress = newAddress;
            EnvUtil.setLocalAddress(localAddress);
            
            Member self = ServerMemberManager.this.self;
            self.setIp(event.getNewIP());
            
            String oldAddress = event.getOldIP() + ":" + port;
            ServerMemberManager.this.serverList.remove(oldAddress);
            ServerMemberManager.this.serverList.put(newAddress, self);
            
            ServerMemberManager.this.memberAddressInfos.remove(oldAddress);
            ServerMemberManager.this.memberAddressInfos.add(newAddress);
        }
        
        @Override
        public Class<? extends Event> subscribeType() {
            return InetUtils.IPChangeEvent.class;
        }
    });
}

然后看initAndStartLookup方法,该方法进行寻址模式适配器的初始化与启动,寻址模式包括单机寻址、文件寻址、地址服务器寻址。

private void initAndStartLookup() throws NacosException {
    //获取寻址模式适配器
    this.lookup = LookupFactory.createLookUp(this);
    isUseAddressServer = this.lookup.useAddressServer();
    //适配器启动
    this.lookup.start();
}

先看createLookUp方法,查看获取适配器的逻辑,寻址模式通过LOOKUP_MODE_TYPE静态变量表示的"nacos.core.member.lookup.type"指定,取值为“file”或者“address-server”,也就是文件寻址和地址服务器寻址。

public static MemberLookup createLookUp(ServerMemberManager memberManager) throws NacosException {
    if (!EnvUtil.getStandaloneMode()) {
        String lookupType = EnvUtil.getProperty(LOOKUP_MODE_TYPE);
        LookupType type = chooseLookup(lookupType);
        LOOK_UP = find(type);
        currentLookupType = type;
    } else {
        LOOK_UP = new StandaloneMemberLookup();
    }
    //给寻址适配器注入ServerMemberManager对象,方便利用 ServerMemberManager 的存储、查询能力
    LOOK_UP.injectMemberManager(memberManager);
    Loggers.CLUSTER.info("Current addressing mode selection : {}", LOOK_UP.getClass().getSimpleName());
    return LOOK_UP;
}

寻址机制的实现

  1. 单机寻址

单机寻址对应StandaloneMemberLookup类,查看核心的doStart方法。单机模式的寻址模式很简单,就是找到自己的IP:PORT组合信息,然后格式化为一个节点信息,调用afterLookup 然后将信息存储到 ServerMemberManager 中。

public void doStart() {
    String url = InetUtils.getSelfIP() + ":" + EnvUtil.getPort();
    afterLookup(MemberUtil.readServerConf(Collections.singletonList(url)));
}
  1. 文件寻址

文件寻址模式就是每个 Nacos 节点需要维护一个叫做 cluster.conf 的文件,其中填写了每个成员节点的 IP 信息。

文件寻址对应FileConfigMemberLookup类,查看核心的doStart方法。调用readClusterConfFromDisk方法读取本节点的cluster.conf文件,获取保存的节点成员列表。并注册FileWatcher监听cluster.conf的变化,有变更会被监听并更新缓存地址列表。

public void doStart() throws NacosException {
    readClusterConfFromDisk();
    
    // Use the inotify mechanism to monitor file changes and automatically
    // trigger the reading of cluster.conf
    try {
        WatchFileCenter.registerWatcher(EnvUtil.getConfPath(), watcher);
    } catch (Throwable e) {
        Loggers.CLUSTER.error("An exception occurred in the launch file monitor : {}", e.getMessage());
    }
}

查看readClusterConfFromDisk方法。从磁盘中的cluster.conf文件里读取节点列表并存储到 ServerMemberManager 中。

private void readClusterConfFromDisk() {
    Collection<Member> tmpMembers = new ArrayList<>();
    try {
        //从磁盘文件中读取节点列表
        List<String> tmp = EnvUtil.readClusterConf();
        tmpMembers = MemberUtil.readServerConf(tmp);
    } catch (Throwable e) {
        Loggers.CLUSTER
            .error("nacos-XXXX [serverlist] failed to get serverlist from disk!, error : {}", e.getMessage());
    }
	//将节点列表信息存储到 ServerMemberManager 中。
    afterLookup(tmpMembers);
}

查看注册的监听器FileWatcher,监听器会自动发现文件修改,重新读取文件内容、加载 IP 列表信息、更新新增的节点。

private FileWatcher watcher = new FileWatcher() {
    @Override
    public void onChange(FileChangeEvent event) {
        readClusterConfFromDisk();
    }
    
    @Override
    public boolean interest(String context) {
        return StringUtils.contains(context, DEFAULT_SEARCH_SEQ);
    }
};

缺点:每一个Nacos节点都需要单独手动维护一个 cluster.conf 文件,既增大了运维难度,又容易造成集群间成员节点列表数据的不一致性。

  1. 地址服务器寻址

地址服务器寻址模式是 Nacos 官方推荐的一种集群成员节点信息管理,该模式利用了一个简易的 web 服务器,用于管理 cluster.conf 文件的内容信息,这样,运维人员只需要管理这一份集群成员节点内容即可,而每个Nacos 成员节点,只需要向这个 web 节点定时请求当前最新的集群成员节点列表信息即可。

地址服务器寻址对应AddressServerMemberLookup类,查看核心的doStart方法。initAddressSys方法进行一些简单的初始化定义,run方法为核心的逻辑。

public void doStart() throws NacosException {
    this.maxFailCount = Integer.parseInt(EnvUtil.getProperty(HEALTH_CHECK_FAIL_COUNT_PROPERTY, DEFAULT_HEALTH_CHECK_FAIL_COUNT));
    initAddressSys();
    run();
}

查看run方法。在启动时需要执行同步成员节点的pull操作,也就是进行syncFromAddressUrl方法从地址服务器中获取节点列表,最多进行5次重试。并且创建一个5秒一次的定时任务AddressServerSyncTask,每五秒请求一次地址服务器。

private void run() throws NacosException {
    // With the address server, you need to perform a synchronous member node pull at startup
    // Repeat three times, successfully jump out
    boolean success = false;
    Throwable ex = null;
    int maxRetry = EnvUtil.getProperty(ADDRESS_SERVER_RETRY_PROPERTY, Integer.class, DEFAULT_SERVER_RETRY_TIME);
    for (int i = 0; i < maxRetry; i++) {
        try {
            syncFromAddressUrl();
            success = true;
            break;
        } catch (Throwable e) {
            ex = e;
            Loggers.CLUSTER.error("[serverlist] exception, error : {}", ExceptionUtil.getAllExceptionMsg(ex));
        }
    }
    if (!success) {
        throw new NacosException(NacosException.SERVER_ERROR, ex);
    }
    
    GlobalExecutor.scheduleByCommon(new AddressServerSyncTask(), DEFAULT_SYNC_TASK_DELAY_MS);
}

查看定时任务AddressServerSyncTask,核心也是执行syncFromAddressUrl方法。

class AddressServerSyncTask implements Runnable {
    
    @Override
    public void run() {
        if (shutdown) {
            return;
        }
        try {
            syncFromAddressUrl();
        } catch (Throwable ex) {
            addressServerFailCount++;
            if (addressServerFailCount >= maxFailCount) {
                isAddressServerHealth = false;
            }
            Loggers.CLUSTER.error("[serverlist] exception, error : {}", ExceptionUtil.getAllExceptionMsg(ex));
        } finally {
            GlobalExecutor.scheduleByCommon(this, DEFAULT_SYNC_TASK_DELAY_MS);
        }
    }
}

查看核心的syncFromAddressUrl方法,该方法向地址服务器发送请求获取节点列表,然后调用afterLookup方法将节点列表存储到 ServerMemberManager 中。

private void syncFromAddressUrl() throws Exception {
    RestResult<String> result = restTemplate
            .get(addressServerUrl, Header.EMPTY, Query.EMPTY, genericType.getType());
    if (result.ok()) {
        isAddressServerHealth = true;
        Reader reader = new StringReader(result.getData());
        try {
            afterLookup(MemberUtil.readServerConf(EnvUtil.analyzeClusterConf(reader)));
        } catch (Throwable e) {
            Loggers.CLUSTER.error("[serverlist] exception for analyzeClusterConf, error : {}",
                    ExceptionUtil.getAllExceptionMsg(e));
        }
        addressServerFailCount = 0;
    } else {
        addressServerFailCount++;
        if (addressServerFailCount >= maxFailCount) {
            isAddressServerHealth = false;
        }
        Loggers.CLUSTER.error("[serverlist] failed to get serverlist, error code {}", result.getCode());
    }
}

afterLookup方法

不同的寻址模式都会调用afterLookup方法将节点信息存储到ServerMemberManager中,核心方法为ServerMemberManager类中的memberChange方法。

synchronized boolean memberChange(Collection<Member> members) {
    
    if (members == null || members.isEmpty()) {
        return false;
    }
    //首先判断是否包含本地地址,不包含的话就将本地地址加入节点列表。
    boolean isContainSelfIp = members.stream()
            .anyMatch(ipPortTmp -> Objects.equals(localAddress, ipPortTmp.getAddress()));
    
    if (isContainSelfIp) {
        isInIpList = true;
    } else {
        isInIpList = false;
        members.add(this.self);
        Loggers.CLUSTER.warn("[serverlist] self ip {} not in serverlist {}", self, members);
    }
    //如果新旧集群个数不一致,则显示集群信息;如果集群数量相同,则比较是否有一个区别;如果存在差异,则集群节点将发生更改。涉及到所有收件人需要通知节点更改事件
    // If the number of old and new clusters is different, the cluster information
    // must have changed; if the number of clusters is the same, then compare whether
    // there is a difference; if there is a difference, then the cluster node changes
    // are involved and all recipients need to be notified of the node change event
    
    boolean hasChange = members.size() != serverList.size();
    ConcurrentSkipListMap<String, Member> tmpMap = new ConcurrentSkipListMap<>();
    Set<String> tmpAddressInfo = new ConcurrentHashSet<>();
    for (Member member : members) {
        final String address = member.getAddress();
        
        Member existMember = serverList.get(address);
        if (existMember == null) {
            hasChange = true;
            tmpMap.put(address, member);
        } else {
            //to keep extendInfo and abilities that report dynamically.
            tmpMap.put(address, existMember);
        }
        
        if (NodeState.UP.equals(member.getState())) {
            tmpAddressInfo.add(address);
        }
    }
    
    serverList = tmpMap;
    memberAddressInfos = tmpAddressInfo;
    
    Collection<Member> finalMembers = allMembers();
    
    Loggers.CLUSTER.warn("[serverlist] updated to : {}", finalMembers);
    
    // Persist the current cluster node information to cluster.conf
    // <important> need to put the event publication into a synchronized block to ensure
    // that the event publication is sequential
    //如果节点列表有变更,就将新数据写入cluster.conf文件中,并发布MembersChangeEvent事件。
    if (hasChange) {
        MemberUtil.syncToFile(finalMembers);
        Event event = MembersChangeEvent.builder().members(finalMembers).build();
        NotifyCenter.publishEvent(event);
    }
    
    return hasChange;
}

小结

Nacos默认使用文件推荐使用地址服务器寻址,但是都还是属于人工操作,未来的可扩展点就是实现全自动的管理。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值