Nacos——寻址机制

459 阅读6分钟

Nacos——寻址机制

1. 寻址机制

从上文Nacos——Distro一致性协议中可以知道需要向每个节点同步数据,但节点间是怎么知道对方的存在的呢?这个就是寻址机制的效果了。可以在运行中感知集群中其他节点的上下线。

2. 分享目的

通过Nacos寻址机制的分享,为在集群模式下部署的服务或中间件提供一种节点间感知对方的机制。

3. 文件寻址

Nacos1.4.2版本提供了三种寻址方式,分别是单节点、文件寻址、地址服务器寻址。单节点即Nacos在standalone模式下运行时获取本地的ip,当Nacos集群模式运行时,默认的寻址方式是文件寻址,所以这里只分析文件寻址。

文件寻址:在每个节点工作目录下维护一份cluster.conf,文件内容为集群中的所有节点的地址。当节点启动时会读取文件中的内容,并对文件进行监听,同时会有个定时任务发送本地节点状态给其他节点,从而节点之间能感知到其他节点的上下线。

Nacos寻址机制.png

4. 源码分析

  • 寻址方式的选择

    节点启动时,会初始化ServerMemberManager,在构造方法中完成寻址方式的选择

    ServerMemberManager

    /**
     * 集群中节点集合
     */
    private volatile ConcurrentSkipListMap<String, Member> serverList;
    
    /**
     * 寻址方式
     */
    private MemberLookup lookup;
    
    /**
     * 本地地址
     */
    private volatile Member self;
    
    /**
     * 健康状态的服务端节点集合
     */
    private volatile Set<String> memberAddressInfos = new ConcurrentHashSet<>();
    
    /**
     * 广播本地状态给其他节点任务
     */
    private final MemberInfoReportTask infoReportTask = new MemberInfoReportTask();
    
    public ServerMemberManager(ServletContext servletContext) throws Exception {
        this.serverList = new ConcurrentSkipListMap<>();
        EnvUtil.setContextPath(servletContext.getContextPath());
        // 初始化
        init();
    }
    
    protected void init() throws NacosException {
        Loggers.CORE.info("Nacos-related cluster resource initialization");
        this.port = EnvUtil.getProperty("server.port", Integer.class, 8848);
        // 本地ip:port
        this.localAddress = InetUtils.getSelfIP() + ":" + port;
        // 本地地址转为Member对象
        this.self = MemberUtil.singleParse(this.localAddress);
        this.self.setExtendVal(MemberMetaDataConstants.VERSION, VersionUtils.version);
        // 加入到节点集合中
        serverList.put(self.getAddress(), self);
    
        // register NodeChangeEvent publisher to NotifyManager
        registerClusterEvent();
    
        // 寻址方式初始化
        initAndStartLookup();
    
        if (serverList.isEmpty()) {
            throw new NacosException(NacosException.SERVER_ERROR, "cannot get serverlist, so exit.");
        }
    
        Loggers.CORE.info("The cluster resource is initialized");
    }
    
    private void initAndStartLookup() throws NacosException {
        // 获取寻址方式
        this.lookup = LookupFactory.createLookUp(this);
        // 开始寻址
        this.lookup.start();
    }
    

    LookupFactory: 寻址方式工厂类

    /**
     * 创建寻址对象
     */
    public static MemberLookup createLookUp(ServerMemberManager memberManager) throws NacosException {
        // 不是standalone模式时
        if (!EnvUtil.getStandaloneMode()) {
            // 环境变量nacos.core.member.lookup.type值
            String lookupType = EnvUtil.getProperty(LOOKUP_MODE_TYPE);
            // 寻址方式类型
            LookupType type = chooseLookup(lookupType);
            // 根据寻址类型找寻址方式实现
            LOOK_UP = find(type);
            currentLookupType = type;
        } else {
            // standalone模式下使用本地地址
            LOOK_UP = new StandaloneMemberLookup();
        }
        LOOK_UP.injectMemberManager(memberManager);
        Loggers.CLUSTER.info("Current addressing mode selection : {}", LOOK_UP.getClass().getSimpleName());
        return LOOK_UP;
    }
    
    /**
     * 选择寻址方式类型
     */
    private static LookupType chooseLookup(String lookupType) {
        if (StringUtils.isNotBlank(lookupType)) {
            LookupType type = LookupType.sourceOf(lookupType);
            if (Objects.nonNull(type)) {
                return type;
            }
        }
        File file = new File(EnvUtil.getClusterConfFilePath());
        // 当没有指定类型且工作目录下有cluster.conf文件,或者环境变量nacos.member.list不为空时,则用文件寻址方式
        if (file.exists() || StringUtils.isNotBlank(EnvUtil.getMemberList())) {
            return LookupType.FILE_CONFIG;
        }
        // 否则使用地址服务器寻址
        return LookupType.ADDRESS_SERVER;
    }
    
    private static MemberLookup find(LookupType type) {
        // 文件寻址
        if (LookupType.FILE_CONFIG.equals(type)) {
            LOOK_UP = new FileConfigMemberLookup();
            return LOOK_UP;
        }
        // 地址寻址
        if (LookupType.ADDRESS_SERVER.equals(type)) {
            LOOK_UP = new AddressServerMemberLookup();
            return LOOK_UP;
        }
        // unpossible to run here
        throw new IllegalArgumentException();
    } 
    

    这里获得的寻址方式为文件寻址,让我们看看文件寻址是如何加载节点地址的

    FileConfigMemberLookup

    @Override
    public void start() throws NacosException {
        // CAS防止并发执行
        if (start.compareAndSet(false, true)) {
            // 从文件中读取集群节点地址
            readClusterConfFromDisk();
    
            try {
                // 监听文件
                WatchFileCenter.registerWatcher(EnvUtil.getConfPath(), watcher);
            } catch (Throwable e) {
                Loggers.CLUSTER.error("An exception occurred in the launch file monitor : {}", e.getMessage());
            }
        }
    }
    
    private void readClusterConfFromDisk() {
        Collection<Member> tmpMembers = new ArrayList<>();
        try {
            // 读取cluster.conf文件
            List<String> tmp = EnvUtil.readClusterConf();
            // 地址转为Member对象
            tmpMembers = MemberUtil.readServerConf(tmp);
        } catch (Throwable e) {
            Loggers.CLUSTER
                .error("nacos-XXXX [serverlist] failed to get serverlist from disk!, error : {}", e.getMessage());
        }
    
        // 放入固定寻址逻辑中
        afterLookup(tmpMembers);
    }
    

    在固定逻辑afterLookup()中会调用ServerMemberManager.memberChange()

    ServerMemberManager

    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);
        }
    
        // 新节点集合跟旧节点集合个数是否相同
        boolean hasChange = members.size() != serverList.size();
        ConcurrentSkipListMap<String, Member> tmpMap = new ConcurrentSkipListMap<>();
        // 状态为UP的节点
        Set<String> tmpAddressInfo = new ConcurrentHashSet<>();
        // 循环新节点集合
        for (Member member : members) {
            final String address = member.getAddress();
    
            // 旧节点集合不包含该地址,hasChange设置为true,并将节点状态设置为DOWN
            // 即集群加入了新节点时,这个节点设置为DOWN
            if (!serverList.containsKey(address)) {
                hasChange = true;
                member.setState(NodeState.DOWN);
            } else {
                // 旧节点用回之前的状态
                member.setState(serverList.get(address).getState());
            }
    
            // Ensure that the node is created only once
            tmpMap.put(address, member);
            if (NodeState.UP.equals(member.getState())) {
                tmpAddressInfo.add(address);
            }
        }
    
        serverList = tmpMap;
        memberAddressInfos = tmpAddressInfo;
    
        // 获取所有节点
        Collection<Member> finalMembers = allMembers();
    
        Loggers.CLUSTER.warn("[serverlist] updated to : {}", finalMembers);
    
        // 当有新节点加入时
        if (hasChange) {
            // 同步节点信息到cluster.conf
            MemberUtil.syncToFile(finalMembers);
            // 创建MembersChangeEvent事件
            Event event = MembersChangeEvent.builder().members(finalMembers).build();
            // 发布事件
            NotifyCenter.publishEvent(event);
        }
    
        return hasChange;
    }
    

    当有新节点时会发送MembersChangeEvent事件,这个事件的订阅者有:

    DistroMapper: 更新写操作时路由节点集合

    ProtocolManager: 更新CP协议下的节点集合

    节点初始化时节点集合的状态:

节点初始化.png

  • 感知其他节点的健康情况

    当节点启动完后,除本地节点外其他节点的状态都设置为DOWN,那当其他节点启动后,DOWN状态如何变为UP呢?而且在运行过程中,节点宕机了怎么通知其他节点将状态从UP变为DOWN呢?

    回到ServerMemberManager,可以看到实现了ApplicationListener,监听WebServerInitializedEvent事件,当web服务器启动完后会发布该事件,根据Spring的发布订阅模式,让我们看看onEvent()做了哪些操作

    ServerMemberManager

    /**
     * 广播本地状态给其他节点任务
     */
    private final MemberInfoReportTask infoReportTask = new MemberInfoReportTask();
    
    @Override
    public void onApplicationEvent(WebServerInitializedEvent event) {
        // 本地节点状态设置为UP
        getSelf().setState(NodeState.UP);
        // 集群模式下执行广播本地节点信息任务
        if (!EnvUtil.getStandaloneMode()) {
            GlobalExecutor.scheduleByCommon(this.infoReportTask, 5_000L);
        }
        EnvUtil.setPort(event.getWebServer().getPort());
        EnvUtil.setLocalAddress(this.localAddress);
        Loggers.CLUSTER.info("This node is ready to provide external services");
    }
    

    MemberInfoReportTask: 继承Task,主要逻辑是定时执行executeBody(),而方法中的逻辑就是调用其他节点的POST /v1/core/cluster/report接口告知本地节点状态,如果调用失败则表示该节点不健康,将状态设置为DOWN

    class MemberInfoReportTask extends Task {
    
        private final GenericType<RestResult<String>> reference = new GenericType<RestResult<String>>() {
        };
    
        private int cursor = 0;
    
        @Override
        protected void executeBody() {
            // 获取除本地之外的其他节点
            List<Member> members = ServerMemberManager.this.allMembersWithoutSelf();
    
            if (members.isEmpty()) {
                return;
            }
    
            // 获取其中一个节点下标
            this.cursor = (this.cursor + 1) % members.size();
            Member target = members.get(cursor);
    
            Loggers.CLUSTER.debug("report the metadata to the node : {}", target.getAddress());
    
            final String url = HttpUtils
                .buildUrl(false, target.getAddress(), EnvUtil.getContextPath(), Commons.NACOS_CORE_CONTEXT,
                          "/cluster/report");
    
            try {
                Header header = Header.newInstance().addParam(Constants.NACOS_SERVER_HEADER, VersionUtils.version);
                AuthHeaderUtil.addIdentityToHeader(header);
                // 调用选中节点的/v1/core/cluster/report接口,传递本地接口信息给其他节点
                asyncRestTemplate
                    .post(url, header,
                          Query.EMPTY, getSelf(), reference.getType(), new Callback<String>() {
                              @Override
                              public void onReceive(RestResult<String> result) {
                                  if (result.getCode() == HttpStatus.NOT_IMPLEMENTED.value()
                                      || result.getCode() == HttpStatus.NOT_FOUND.value()) {
                                      Loggers.CLUSTER
                                          .warn("{} version is too low, it is recommended to upgrade the version : {}",
                                                target, VersionUtils.version);
                                      return;
                                  }
                                  if (result.ok()) {
                                      MemberUtil.onSuccess(ServerMemberManager.this, target);
                                  } else {
                                      Loggers.CLUSTER
                                          .warn("failed to report new info to target node : {}, result : {}",
                                                target.getAddress(), result);
                                      // 调用失败则将选中的节点状态设置为DOWN,表示该节点不健康
                                      MemberUtil.onFail(ServerMemberManager.this, target);
                                  }
                              }
    
                              @Override
                              public void onError(Throwable throwable) {
                                  Loggers.CLUSTER
                                      .error("failed to report new info to target node : {}, error : {}",
                                             target.getAddress(),
                                             ExceptionUtil.getAllExceptionMsg(throwable));
                                  MemberUtil.onFail(ServerMemberManager.this, target, throwable);
                              }
    
                              @Override
                              public void onCancel() {
    
                              }
                          });
            } catch (Throwable ex) {
                Loggers.CLUSTER.error("failed to report new info to target node : {}, error : {}", target.getAddress(),
                                      ExceptionUtil.getAllExceptionMsg(ex));
            }
        }
    
        @Override
        protected void after() {
            // 延迟2秒后重新执行任务
            GlobalExecutor.scheduleByCommon(this, 2_000L);
        }
    }
    

    POST /v1/core/cluster/report接口逻辑是怎样的呢?

    NacosClusterController

    @PostMapping(value = {"/report"})
    public RestResult<String> report(@RequestBody Member node) {
        if (!node.check()) {
            return RestResultUtils.failedWithMsg(400, "Node information is illegal");
        }
        LoggerUtils.printIfDebugEnabled(Loggers.CLUSTER, "node state report, receive info : {}", node);
        // 设置收到的节点状态为UP
        node.setState(NodeState.UP);
        node.setFailAccessCnt(0);
    
        boolean result = memberManager.update(node);
    
        return RestResultUtils.success(Boolean.toString(result));
    }
    

    ServerMemberManager

    public boolean update(Member newMember) {
        Loggers.CLUSTER.debug("member information update : {}", newMember);
    
        String address = newMember.getAddress();
        if (!serverList.containsKey(address)) {
            return false;
        }
    
        // 更新到serverList集合中
        serverList.computeIfPresent(address, (s, member) -> {
            if (NodeState.DOWN.equals(newMember.getState())) {
                memberAddressInfos.remove(newMember.getAddress());
            }
            boolean isPublishChangeEvent = MemberUtil.isBasicInfoChanged(newMember, member);
            newMember.setExtendVal(MemberMetaDataConstants.LAST_REFRESH_TIME, System.currentTimeMillis());
            MemberUtil.copy(newMember, member);
            if (isPublishChangeEvent) {
                // 发送MemberChangeEvent事件
                notifyMemberChange();
            }
            return member;
        });
    
        return true;
    }
    

    当本地节点收到节点node2发来的请求后,表示node2节点是健康的,则将node2节点的状态设置为UP,加入到serverList中。

    节点间感知:

节点间感知.png

5. 文件寻址的缺点

文件寻址下集群中的每个节点都需要维护一份cluster.conf文件,不仅麻烦而且易出错。这时候nacos-k8s就可以解决这个问题,部署时自动创建cluster.conf文件,github地址为https://github.com/nacos-group/nacos-k8s.



谢谢阅读,就分享到这,未完待续...

欢迎同频共振的那一部分人

作者公众号:Tarzan写bug

公众号二维码.jpg