Eureka源码分析--集群模式下服务同步(8)

181 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

前言

首先eureka server采用的架构模型是peer模型,意思是说所有节点地位相等不分主次,那么在每个server中就必须保持一致的可用的server的地址集合。而后再根据具体的地址将自身的变化同步过去。

保持地址一致可用

在EurekaBootstrap找了一大圈没看到,就顺手点到initialize()中,看看初始化话线程调用的时候有没有。结果确实在这里,这个方法中启动了一个peerEurekaNodes.start(),而在这个方法中又调用了一个updatePeerEurekaNodes的方法,这个名字一看就知道是更新server节点信息用的。

try {
    //resolvePeerUrls()会解析出配置文件中的地址.并剔除掉自己本身的节点地址
    updatePeerEurekaNodes(resolvePeerUrls());
    Runnable peersUpdateTask = new Runnable() {
        @Override
        public void run() {
            try {
                updatePeerEurekaNodes(resolvePeerUrls());
            } catch (Throwable e) {
                logger.error("Cannot update the replica Nodes", e);
            }

        }
    };
    taskExecutor.scheduleWithFixedDelay(
        peersUpdateTask,
        serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
        serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
        TimeUnit.MILLISECONDS
    );
} 
 protected void updatePeerEurekaNodes(List<String> newPeerUrls) {
        if (newPeerUrls.isEmpty()) {
            logger.warn("The replica size seems to be empty. Check the route 53 DNS Registry");
            return;
        }

        /**
         * 这里是将本身存在的地址和每次运行线程时计算的地址进行比对
         * 如果线下list为并且者添加节点为空 说明没有新注册的节点和下线的节点,反之怎进行节点的维护
         */
        Set<String> toShutdown = new HashSet<>(peerEurekaNodeUrls);
        toShutdown.removeAll(newPeerUrls);
        Set<String> toAdd = new HashSet<>(newPeerUrls);
        toAdd.removeAll(peerEurekaNodeUrls);

        if (toShutdown.isEmpty() && toAdd.isEmpty()) { // No change
            return;
        }

        // Remove peers no long available
        List<PeerEurekaNode> newNodeList = new ArrayList<>(peerEurekaNodes);

        //删除已经下线的节点
        if (!toShutdown.isEmpty()) {
            logger.info("Removing no longer available peer nodes {}", toShutdown);
            int i = 0;
            while (i < newNodeList.size()) {
                PeerEurekaNode eurekaNode = newNodeList.get(i);
                if (toShutdown.contains(eurekaNode.getServiceUrl())) {
                    newNodeList.remove(i);
                    eurekaNode.shutDown();
                } else {
                    i++;
                }
            }
        }

        // Add new peers
        //添加新上线的节点
        if (!toAdd.isEmpty()) {
            logger.info("Adding new peer nodes {}", toAdd);
            for (String peerUrl : toAdd) {
                newNodeList.add(createPeerEurekaNode(peerUrl));
            }
        }

        this.peerEurekaNodes = newNodeList;
        this.peerEurekaNodeUrls = new HashSet<>(newPeerUrls);
    }

这里的比较方式也很简单,首先他会获得在这一次计算的可用的节点集合,然后会把当前本地保存的集合放入两个集合中,分别为下线集合和新增集合,然后将这两个集合中的本次计算结果删除,如果均为空的话,那么就说明节点没有发生改变,如果不为空的话,那么具体逻辑走具体方法。有了节点之间的集合后那么就可以进行服务间数据的同步了。这个线程每10min调度一次。

同步注册表

同步注册表是在registry.syncUp中进行的

public int syncUp() {
    // Copy entire entry from neighboring DS node
    int count = 0;

    for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
        if (i > 0) {
            try {
                Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
            } catch (InterruptedException e) {
                logger.warn("Interrupted during registry transfer..");
                break;
            }
        }
        //从自己client的注册表中获取注册信息
        //这里其实server端在启动时回去注册另一个server,并从另一个server中拉取对应的注册表信息
        //放入本地的注册表中去,然后本机会先去判断这些实例是不是可以被注册,如果可以被注册
        //那么就在本机中进行注册,如果第一次没有拉取到注册信息的话,会停30s中再次去尝试,最多尝试5次
        Applications apps = eurekaClient.getApplications();
        for (Application app : apps.getRegisteredApplications()) {
            for (InstanceInfo instance : app.getInstances()) {
                try {
                    if (isRegisterable(instance)) {
                        register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                        count++;
                    }
                } catch (Throwable t) {
                    logger.error("During DS init copy", t);
                }
            }
        }
    }
    return count;
}

实例间状态的同步

当client在某一台server的状态发生改变的时候,这台server也会把改变同步给其他的server,以保持数据的一致,同步的方法在PeerAwareInstanceRegisterImpl类中

image20220501174723608.png

这些地方调用,而这些地方分别对应着注册,下线等地方,并且对应着几种状态

public enum Action {
        Heartbeat, Register, Cancel, StatusUpdate, DeleteStatusOverride;
    }

而同步的方法即为replicationToPeers

 private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            //isReplication这个属性是用来判断是否是复制过来的,如果为false则需要给其他server进行同步,反之不需要
            if (isReplication) {
                numberOfReplicationsLastMin.increment();
            }
            // If it is a replication already, do not replicate again as this will create a poison replication
            //如果它已经是复制,请不要再次复制,因为这将创建有毒复制
            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }

            //向其他节点开始进行同步
            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
                //删除自己的节点
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }

在调用replicateInstanceActionsToPeers方法

    private void replicateInstanceActionsToPeers(Action action, String appName,
                                                 String id, InstanceInfo info, InstanceStatus newStatus,
                                                 PeerEurekaNode node) {
        try {
            InstanceInfo infoFromRegistry;
            CurrentRequestVersion.set(Version.V2);
            switch (action) {
                case Cancel:
                    node.cancel(appName, id);
                    break;
                case Heartbeat:
                    InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
                    break;
                case Register:
                    node.register(info);
                    break;
                case StatusUpdate:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.statusUpdate(appName, id, newStatus, infoFromRegistry);
                    break;
                case DeleteStatusOverride:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.deleteStatusOverride(appName, id, infoFromRegistry);
                    break;
            }
        } catch (Throwable t) {
            logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
        } finally {
            CurrentRequestVersion.remove();
        }
    }

这里其实就是针对不同的操作进行不同的属性拼装

查看各个node.XX方法后,发现最后都会进入到一个batchingDispatcher

//cancel方法
public void cancel(final String appName, final String id) throws Exception {
      	....
        batchingDispatcher.process(
               ....
               expiryTime
        );
    }
//heartbeat方法
public void heartbeat(final String appName, final String id,
                          final InstanceInfo info, final InstanceStatus overriddenStatus,
                          boolean primeConnection) throws Throwable {
        ....
        batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
    }

//register方法
public void register(final InstanceInfo info) throws Exception {
        batchingDispatcher.process(
                taskId("register", info),
                ....
                ,
                expiryTime
        );
    }

//statusUpdate方法
public void statusUpdate(final String appName, final String id,
                             final InstanceStatus newStatus, final InstanceInfo info) {
        batchingDispatcher.process(
                ....
                },
                expiryTime
        );
    }

//deleteStatusOverride方法
public void deleteStatusOverride(final String appName, final String id, final InstanceInfo info) {
        batchingDispatcher.process(
                ....
                expiryTime);
    }

后面会走一个三层队列来进行批量的请求,后面太过于麻烦,在网上找了张画的差不多的图

image20220501203436645.png