一、注册表同步及高可用
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去。