spring cloud:eureka源码之我见(十)eureka server集群机制

一、注册表同步及高可用

eureka server集群之间数据同步以及高可用都是通过这个机制实现的,现在我们来剖析一下这个机制的原理。

(1)我们知道,eureka server的初始化是在eureka-core的EurekaBootStrap中完成的,在其中完成了PeerEurekaNodes的初始化。且在接下来的代码中会调用PeerEurekaNodes的start方法。在该方法中,eureka server会解析配置文件中其他eureka server的url,并且基于url地址构造PeerEurekaNode,一个PeerEurekaNode就代表了一个eureka server,然后更新PeerEurekaNodes。最后,启动一个定时任务,默认每隔10分钟运行一次,基于配置文件中的url来刷新eureka server的列表。

(2)registry.syncUp(),初始化当前eureka server的注册表。

eureka server初始化时,会把自己作为一个eureka client,从任意一个远端的eureka server中拉取注册表放在自己本地,作为自己初始的注册表。

在之前的文章中,我们知道eureka server初始化的时候,也会初始化一个eureka client,DiscoveryClient。在这个类的构造中,就有拉取注册表的方法,而注册表在拉取之后,就被放入了localRegionApps中。

localRegionApps.set(this.filterAndShuffle(apps));
private final AtomicReference<Applications> localRegionApps = new AtomicReference<Applications>();

而在syncUp方法中,eureka server会从当前这个eureka client的localRegionApps获取之前拉取远端的注册表,检查注册表中的服务实例是否已经在当前server中注册过了,如果没有,就会进行注册。

 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 {
                    //没有在自己本地获取注册表,说明本地还没有从其他的eureka server中拉取到注册表,等待30s
                    Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
                } catch (InterruptedException e) {
                    logger.warn("Interrupted during registry transfer..");
                    break;
                }
            }
            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;
    }

注意,这里获取eureka client中的注册表时,会重试5次(默认),每次会Thread.sleep等待30s,为什么?

因为eureka client中的CacheRefreshThread定时任务会每隔30s运行一次,去获取其他eureka server节点上的注册表信息。这里就是在等待这个定时任务去获取注册表信息。

(3)服务实例状态发生变化时,同步到其他server节点(注册、下线、心跳,而故障检测是每个server都会定时调度的,所以无需同步)

这些操作的代码分布在InstanceResource和ApplcationResource中,而这些操作无一例外都会调用到PeerAwareInstanceRegistry中,而PeerAwareInstanceRegistryImpl对这些方法的实现中,都会使用replicateToPeers方法,将这些服务实例的变化广播到eureka server集群。

    private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            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();
        }
    }

在replicateToPeers方法中,会从配置里获取其他eureka server的url,这里会跳过当前eureka server,然后将请求发送到这些url中。

isReplication在服务第一次向eureka server注册时,一定是false。而在replicateToPeers中广播到其他eureka server时,会调用到eureka-core-jersey2的工程,AbstractJersey2EurekaHttpClient类,在这里会调用addExtraHeaders方法,将isReplication设置为true。这样就避免了二次传播。

    @Override
    protected void addExtraHeaders(Builder webResource) {
        webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
    }

二、三层队列任务批处理机制

eureka server在进行集群同步时,并不是收到一个请求就去发送,而是使用了一个三层队列进行任务批处理。

以注册为例:

    public void register(final InstanceInfo info) throws Exception {
        long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
        //batchingDispatcher找到acceptorExecutor
        batchingDispatcher.process(
                taskId("register", info),
                //请求封装成task
                new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                    public EurekaHttpResponse<Void> execute() {
                        return replicationClient.register(info);
                    }
                },
                expiryTime
        );
    }

首先通过batchingDispatcher将请求封装成task,调用process方法,而这里其实调用的是acceptorExecutor的process方法

        return new TaskDispatcher<ID, T>() {
            @Override
            //acceptorExecutor把消息放入acceptorQueue中
            public void process(ID id, T task, long expiryTime) {
                acceptorExecutor.process(id, task, expiryTime);
            }

            @Override
            public void shutdown() {
                acceptorExecutor.shutdown();
                taskExecutor.shutdown();
            }
        };

而acceptorExecutor的excute方法就是将task放入队列acceptorQueue中。

    void process(ID id, T task, long expiryTime) {
        //任务放入队列
        acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
        acceptedTasks++;
    }

acceptorExecutor中有一个后台线程循环运行

    class AcceptorRunner implements Runnable {
        @Override
        public void run() {
            long scheduleTime = 0;
            while (!isShutdown.get()) {
                try {
                    //提取任务,放入processingOrder
                    drainInputQueues();

                    int totalItems = processingOrder.size();

                    long now = System.currentTimeMillis();
                    if (scheduleTime < now) {
                        scheduleTime = now + trafficShaper.transmissionDelay();
                    }
                    if (scheduleTime <= now) {
                        assignBatchWork();
                        assignSingleItemWork();
                    }

                    // If no worker is requesting data or there is a delay injected by the traffic shaper,
                    // sleep for some time to avoid tight loop.
                    if (totalItems == processingOrder.size()) {
                        Thread.sleep(10);
                    }
                } catch (InterruptedException ex) {
                    // Ignore
                } catch (Throwable e) {
                    // Safe-guard, so we never exit this loop in an uncontrolled way.
                    logger.warn("Discovery AcceptorThread error", e);
                }
            }
        }
}

在drainInputQueues()方法中,会将队列acceptorQueue中的任务提取到队列processingOrder中,

之后,在assignBatchWork()方法中,根据时间和任务数量(时间默认为500ms,大小默认为250个)将任务打包放到List<TaskHolder<ID, T>> holders中,然后放入队列batchWorkQueue。

在PeerEurekaNode中还有一个后台线程ReplicationTaskProcessor会循环调用,将batchWorkQueue中的holders通过AbstractJersey2EurekaHttpClient的submitBatchUpdates方法请求PeerReplicationResource的batch接口,将任务事件传播到其他eureka server去。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值