系列文章:
SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化
SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约
SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表
SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制
Eureka Server 集群
在实际的生产环境中,可能有几十个或者几百个的微服务实例,Eureka Server 承担了非常高的负载,而且为了保证注册中心高可用,一般都要部署成集群的,下面就来看看 eureka server 的集群。
搭建 Eureka Server 集群
首先来搭建一个三个节点的 eureka-server 集群,看看效果。
1、集群配置
首先在本地 hosts 文件中配置如下映射:
127.0.0.1 peer1
127.0.0.1 peer2
127.0.0.1 peer3
更改注册中心的 application.yml 配置文件,增加三个 profile,分别对应三个 eureka-server 的客户端配置。
eureka-server 在集群中作为客户端就需要抓取注册表,并配置 eureka-server 的地址。
spring:
application:
name: sunny-register
---
spring:
profiles: peer1
server:
port: 8001
eureka:
instance:
hostname: peer1
client:
# 是否向注册中心注册自己
register-with-eureka: false
# 是否抓取注册表
fetch-registry: true
service-url:
defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka
---
spring:
profiles: peer2
server:
port: 8002
eureka:
instance:
hostname: peer2
client:
# 是否向注册中心注册自己
register-with-eureka: false
# 是否抓取注册表
fetch-registry: true
service-url:
defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka
---
spring:
profiles: peer3
server:
port: 8003
eureka:
instance:
hostname: peer3
client:
# 是否向注册中心注册自己
register-with-eureka: false
# 是否抓取注册表
fetch-registry: true
service-url:
defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka
2、启动集群
分别启动三个注册中心,环境变量 spring.profiles.active 激活对应的集群配置。
启动之后访问 http://peer1:8001/ 进入 peer1 这个注册中心,就可以看到另外两个分片 peer2、peer3,说明集群中有3个节点了。
3、启动客户端
首先客户端配置增加集群地址:
eureka:
client:
serviceUrl:
defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka
启动几个客户端实例,过一会 之后,会发现三个 eureka-server 上都注册上去了:
到此 eureka-server 集群就搭建起来了,可以看到注册中心的实例会互相同步,每隔注册注册都可以接收注册、续约、下线等请求,它们是对等的。
Eureka Server 集群架构
一般来说,分布式系统的数据在多个副本之间的复制方式,可分为主从复制和对等复制。
1、主从复制
主从复制就是 Master-Slave 模式,即一个主副本,其它副本都为从副本。所有对数据的写操作都提交到主副本,然后再由主副本同步到从副本。
对于主从复制模式来说,写操作的压力都在主副本上,它是整个系统的瓶颈,而从副本则可以帮助主副本分担读请求。
2、对等复制
对等复制就是 Peer to Peer 的模式,副本之间不分主从,任何副本都可以接收写操作,每个副本之间相互进行数据更新同步。
Peer to Peer 模式每个副本之间都可以接收写请求,不存在写操作压力瓶颈。但是由于每个副本都可以进行写操作,各个副本之间的数据同步及冲突处理是一个棘手的问题。
3、Eureka Server 集群架构
Eureka Server 采用的就是 Peer to Peer 的复制模式,比如一个客户端实例随机向其中一个server注册,然后它就会同步到其它节点中。
Eureka Server 启动时抓取注册表
前面已经分析过了,在 eureka server 启动初始化的时候,即 EurekaBootStrap 初始化类,先初始化了 DiscoveryClient,DiscoveryClient 会向注册中心全量抓取注册表到本地。
初始化的最后调用了 registry.syncUp() 来同步注册表,就是将 DiscoveryClient 缓存的实例注册到 eureka-server 的注册表里去。
需要注意的是 eureka 配置的注册表同步重试次数默认为 5,springcloud 中默认为 0,因此需要添加如下配置来开启注册表同步。
eureka:
server:
registry-sync-retries: 5
将 DiscoveryClient 本地的实例注册到注册表中:
集群节点同步
1、注册、续约、下线
前面也分析过了,在客户端注册、续约、下线的时候,都会同步到集群其它节点。可以看到都调用了 replicateToPeers 方法来复制到其它集群。
/////////////////////// 注册 ///////////////////////
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
// 如果实例中没有周期的配置,就设置为默认的 90 秒
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
// 注册实例
super.register(info, leaseDuration, isReplication);
// 复制到集群其它 server 节点
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
/////////////////////// 下线 ///////////////////////
public boolean cancel(final String appName, final String id, final boolean isReplication) {
// 实现下线
if (super.cancel(appName, id, isReplication)) {
// 复制到集群其它 server 节点
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
return true;
}
return false;
}
/////////////////////// 续约 ///////////////////////
public boolean renew(final String appName, final String id, final boolean isReplication) {
// 调用父类(AbstractInstanceRegistry)的 renew 续约
if (super.renew(appName, id, isReplication)) {
// 续约完成后同步到集群其它节点
replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
return true;
}
return false;
}
2、同步到其它节点
来看看 replicateToPeers 方法:
- 首先判断
isReplication参数,如果是集群复制操作,最近一分钟复制次数numberOfReplicationsLastMin + 1。isReplication 是在请求头中指定的,请求头为PeerEurekaNode.HEADER_REPLICATION(x-netflix-discovery-replication)。 - 接着遍历集群列表,复制实例操作到集群节点中。前面也分析过了,
PeerEurekaNode就代表了一个 eureka-server,PeerEurekaNodes就代表了 eureka-server 集群。 - 复制实例操作到集群的方法
replicateInstanceActionsToPeers就是根据不同的操作类型调用集群 PeerEurekaNode 对应的方法完成操作复制。
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) {
// 如果是来自其它server节点的注册请求,则最近一分钟集群同步次数+1
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;
}
// 如果是来自客户端的注册请求,就同步到集群中其它server节点
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// 同步到集群其它节点
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
private void replicateInstanceActionsToPeers(
Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node {
try {
InstanceInfo infoFromRegistry;
switch (action) {
case Cancel:
// 下线
node.cancel(appName, id);
break;
case Heartbeat:
// 续约
node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
break;
case Register:
// 注册
node.register(info);
break;
case StatusUpdate:
// 更新状态
node.statusUpdate(appName, id, newStatus, infoFromRegistry);
break;
case DeleteStatusOverride:
// 删除覆盖状态
node.deleteStatusOverride(appName, id, infoFromRegistry);
break;
}
} catch (Throwable t) {
logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
}
}
3、isReplication
PeerEurekaNode 与 eureka-server 通信的组件是 JerseyReplicationClient,这个类重写了 addExtraHeaders 方法,并添加了请求头 PeerEurekaNode.HEADER_REPLICATION,设置为 true。
这样其它 eureka-server 收到这个复制操作后,就知道是来自集群节点的同步操作,就不会再同步给其它节点了,从而避免死循环。
@Override
protected void addExtraHeaders(Builder webResource) {
webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
}
集群同步机制
Eureka Server 集群间同步机制还是比较复杂的,试想如果每次客户端的请求一过来,比如注册、心跳,然后 eureka-server 就立马同步给集群中其它 server 节点,那 eureka-server 这种 Peer to Peer 的模式实际上就无法分担客户端的写操作压力,相当于每个 eureka-server 接收到的请求量都是一样的。那 eureka server 为了避免这种情况,底层采用了三层队列,加批量任务的方式来进行集群间的同步。简单来说就是先将客户端操作放入队列中,然后从队列中取出一批操作,然后将这一批操作发送给其它 Server 节点,Server节点接收到之后再将这批操作解析到本地。下面就来详细看看是如何实现的。
集群节点 PeerEurekaNode
之前分析 eureka-server 启动初始化的时候,EurekaBootStrap 初始化了代表集群的 PeerEurekaNodes,它里面又根据配置的注册中心地址构造了 PeerEurekaNode,集群间同步核心的组件就是这个 PeerEurekaNode 了。下面以客户端注册为例来看下是如何同步的。
1、注册同步
replicateInstanceActionsToPeers 中调用了 PeerEurekaNode 的 register 方法来同步注册操作到集群。
node.register 方法:
- 可以看到先计算了过期时间,为
当前时间 + 租约间隔时间(默认90秒) - 然后调用了
batchingDispatcher批量任务分发器来处理任务,提交了一个InstanceReplicationTask的实例,其execute方法中调用了replicationClient来向这个 server 注册同步。
public void register(final InstanceInfo info) throws Exception {
// 过期时间:当前时间 + 租约时间(默认90秒)
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
batchingDispatcher.process(
taskId("register", info),
new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
public EurekaHttpResponse<Void> execute() {
// 同步到Server
return replicationClient.register(info);
}
},
expiryTime
);
}
再看下 getLeaseRenewalOf 这个方法,这里应该是有bug的,这个方法返回的是毫秒数,可以看到它的卫语句的else部分是乘以 1000 了的,而 if 部分则没有,返回的是 90,不过这里 info.getLeaseInfo() 应该都不会为 null。
private static int getLeaseRenewalOf(InstanceInfo info) {
// bug : Lease.DEFAULT_DURATION_IN_SECS * 1000
return (info.getLeaseInfo() == null ? Lease.DEFAULT_DURATION_IN_SECS : info.getLeaseInfo().getRenewalIntervalInSecs()) * 1000;
}
2、PeerEurekaNode 的构造
batchingDispatcher 是在 PeerEurekaNode 的构造方法中初始化的,来看下它的构造方法:
- registry:本地注册表
- targetHost:eureka-server host
- replicationClient:基于 jersey 的集群复制客户端通信组件,它在请求头中设置了 PeerEurekaNode.HEADER_REPLICATION 为 true
- serviceUrl:eureka-server 地址
- maxProcessingDelayMs:最大处理延迟毫秒数,默认为30000毫秒,即30秒,在下线的时候有用到
- batcherName:批处理器名称
- taskProcessor:复制任务处理器,它封装了 targetHost 和 replicationClient,主要就是 ReplicationTaskProcessor 在处理批量任务的提交
- batchingDispatcher:批量任务分发器,它会将任务打成一个批次提交到 eureka-server,避免多次请求eureka-server,注册时就是先用这个分发器提交的任务
- nonBatchingDispatcher:非批量任务分发器,就是一个任务一个任务的提交
public PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl, HttpReplicationClient replicationClient, EurekaServerConfig config) {
this(registry, targetHost, serviceUrl, replicationClient, config, BATCH_SIZE, MAX_BATCHING_DELAY_MS, RETRY_SLEEP_TIME_MS, SERVER_UNAVAILABLE_SLEEP_TIME_MS);
}
PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl,
HttpReplicationClient replicationClient, EurekaServerConfig config,
int batchSize, long maxBatchingDelayMs,
long retrySleepTimeMs, long serverUnavailableSleepTimeMs) {
this.registry = registry;
// 集群节点 host
this.targetHost = targetHost;
this.replicationClient = replicationClient;
// 集群节点地址
this.serviceUrl = serviceUrl;
this.config = config;
// 最大延迟时间 默认30秒
this.maxProcessingDelayMs = config.getMaxTimeForReplication();
// 批处理器名称
String batcherName = getBatcherName();
// 复制任务处理器
ReplicationTaskProcessor taskProcessor = new ReplicationTaskProcessor(targetHost, replicationClient);
// 批量任务分发器
this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
batcherName,
// 复制池里最大容量,默认 10000
config.getMaxElementsInPeerReplicationPool(),
batchSize, // 250
// 同步使用的最大线程数 默认 20
config.getMaxThreadsForPeerReplication(),
maxBatchingDelayMs, // 500
serverUnavailableSleepTimeMs, // 1000
retrySleepTimeMs, // 100
taskProcessor
);
// 单个任务分发器
this.nonBatchingDispatcher = TaskDispatchers.createNonBatchingTaskDispatcher(
targetHost,
config.getMaxElementsInStatusReplicationPool(),
config.getMaxThreadsForStatusReplication(),
maxBatchingDelayMs,
serverUnavailableSleepTimeMs,
retrySleepTimeMs,
taskProcessor
);
}
批量分发器 TaskDispatcher
创建 batchingDispatcher 时调用了 TaskDispatchers.createBatchingTaskDispatcher 方法来创建。
首先看下 createBatchingTaskDispatcher 的参数及默认值,后面分析代码的时候会用到这些参数:
- id:批量分发器的名称
- maxBufferSize:缓存池最大数量,默认 10000
- workloadSize:工作负载数量,即一个批次最多多少任务,默认 250
- workerCount:工作者数量,这个是线程池线程工作线程的数量,默认20
- maxBatchingDelay:批量任务最大延迟毫秒数,默认为 500 毫秒
- congestionRetryDelayMs:阻塞重试延迟毫秒数,默认为 1000 毫秒
- networkFailureRetryMs:网络失败重试延迟毫秒数,默认为 100 毫秒
- taskProcessor:任务处理器,即 ReplicationTaskProcessor
再看下这个方法:
- 首先创建了一个接收者执行器
AcceptorExecutor,主要的参数是缓存、时间相关的 - 再创建了一个任务处理器
TaskExecutors,主要的参数是工作线程数、任务处理器以及接收者执行器,可以猜测这应该就是最终执行批量任务提交的执行器 - 最后创建了任务分发器
TaskDispatcher,从它的process方法可以看出,分发器提交的任务实际上又提交给了AcceptorExecutor
从这里可以知道,前面注册时 batchingDispatcher.process() 提交的任务其实就是分发到 acceptorExecutor 这个接收者执行器了。创建的这个分发器 TaskDispatcher 主要有接收者执行器 AcceptorExecutor 和 任务处理器 TaskExecutors 这两个组件,核心的分发功能就在这两个组件中。
public static <ID, T> TaskDispatcher<ID, T> createBatchingTaskDispatcher(String id, int maxBufferSize, int workloadSize,
int workerCount, long maxBatchingDelay, long congestionRetryDelayMs,
long networkFailureRetryMs, TaskProcessor<T> taskProcessor) {
// 接收者执行器 AcceptorExecutor
final AcceptorExecutor<ID, T> acceptorExecutor = new AcceptorExecutor<>(
id, maxBufferSize, workloadSize, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs
);
// 任务处理器 TaskExecutors, workerCount = 20
final TaskExecutors<ID, T> taskExecutor = TaskExecutors.batchExecutors(id, workerCount, taskProcessor, acceptorExecutor);
return new TaskDispatcher<ID, T>() {
@Override
public void process(ID id, T task, long expiryTime) {
// 任务由 acceptorExecutor 处理
acceptorExecutor.process(id, task, expiryTime);
}
@Override
public void shutdown() {
acceptorExecutor.shutdown();
taskExecutor.shutdown();
}
};
}
接收者执行器 AcceptorExecutor
先看下创建 AcceptorExecutor 的构造方法:
- 根据
congestionRetryDelayMs、networkFailureRetryMs创建了一个时间调整器TrafficShaper,应该主要就是用来调整补偿时间的 - 然后创建了一个后台线程
acceptorThread,它运行的任务是AcceptorRunner,主要就是将任务转成批量任务的 - 最后就是注册了一些监控统计之类的
AcceptorExecutor(String id, int maxBufferSize, int maxBatchingSize, long maxBatchingDelay, long congestionRetryDelayMs, long networkFailureRetryMs) {
// 批处理器名称
this.id = id;
// 最大缓冲数:10000
this.maxBufferSize = maxBufferSize;
// 每批最大数量:250
this.maxBatchingSize = maxBatchingSize;
// 最大延迟时间:500 ms
this.maxBatchingDelay = maxBatchingDelay;
// 时间调整器
// congestionRetryDelayMs 阻塞重试延迟时间,1000ms
// networkFailureRetryMs 网络异常重试时间,100ms
this.trafficShaper = new TrafficShaper(congestionRetryDelayMs, networkFailureRetryMs);
// 接收者后台处理线程
ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
this.acceptorThread = new Thread(threadGroup, new AcceptorRunner(), "TaskAcceptor-" + id);
this.acceptorThread.setDaemon(true);
this.acceptorThread.start();
// 监控统计相关
final double[] percentiles = {50.0, 95.0, 99.0, 99.5};
final StatsConfig statsConfig = new StatsConfig.Builder()
.withSampleSize(1000)
.withPercentiles(percentiles)
.withPublishStdDev(true)
.build();
final MonitorConfig config = MonitorConfig.builder(METRIC_REPLICATION_PREFIX + "batchSize").build();
this.batchSizeMetric = new StatsTimer(config, statsConfig);
try {
Monitors.registerObject(id, this);
} catch (Throwable e) {
logger.warn("Cannot register servo monitor for this object", e);
}
}
然后看看 AcceptorExecutor 的属性,它定义了几个队列以及容器来处理批量任务,我们先知道有这些东西,后面再来看看都怎么使用的。
然后可以看到 AcceptorExecutor 大量使用了并发包下的一些类,以及队列的特性,这里我们需要了解下这些类的特性:
LinkedBlockingQueue:基于链表的单端阻塞队列,就是队尾入队,队首出队Deque:双端队列,就是队首、队尾都可以入队、出队Semaphore:信号量,需要通过 acquire(或 tryAcquire) 获取到许可证之后才可以进入临界区,通过 release 释放许可证。只要能拿到许可证,Semaphore 是可以允许多个线程进入临界区的。另外注意它这里设置的许可证数量是0,说明要先调用了 release 放入一个许可证,才有可能调用 acquire 获取到许可证。
// 接收任务的队列
private final BlockingQueue<TaskHolder<ID, T>> acceptorQueue = new LinkedBlockingQueue<>();
// 重试任务的队列
private final BlockingDeque<TaskHolder<ID, T>> reprocessQueue = new LinkedBlockingDeque<>();
// 后台接收者线程
private final Thread acceptorThread;
// 待处理任务容器
private final Map<ID, TaskHolder<ID, T>> pendingTasks = new HashMap<>();
// 处理中的队列
private final Deque<ID> processingOrder = new LinkedList<>();
// 单项队列请求的信号量
private final Semaphore singleItemWorkRequests = new Semaphore(0);
// 单项任务队列
private final BlockingQueue<TaskHolder<ID, T>> singleItemWorkQueue = new LinkedBlockingQueue<>();
// 批量队列请求的信号量
private final Semaphore batchWorkRequests = new Semaphore(0);
// 批量任务队列
private final BlockingQueue<List<TaskHolder<ID, T>>> batchWorkQueue = new LinkedBlockingQueue<>();
// 时间调整器
private final TrafficShaper trafficShaper;
TaskDispatcher 调用 acceptorExecutor.process 将任务转给 AcceptorExecutor,可以看到就是将任务添加到接收者队列 acceptorQueue 的队尾了。
void process(ID id, T task, long expiryTime) {
acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
acceptedTasks++;
}
接收者任务 AcceptorRunner
任务添加到 acceptorQueue 了,那任务在哪处理的呢?这就是在 AcceptorRunner 这个任务里去处理的了,这个任务比较复杂,我先把整个代码放出来,再来分析。
class AcceptorRunner implements Runnable {
@Override
public void run() {
long scheduleTime = 0;
while (!isShutdown.get()) {
try {
// 排出输入队列的任务:将 reprocessQueue、acceptorQueue 队列的任务转移到 pendingTasks
drainInputQueues();
// 待处理的数量
int totalItems = processingOrder.size();
long now = System.currentTimeMillis();
if (scheduleTime < now) {
// 时间补偿,正常情况下 transmissionDelay() 返回 0
scheduleTime = now + trafficShaper.transmissionDelay();
}
if (scheduleTime <= now) {
// 分配批量工作任务:将 pendingTasks 的任务分一批到(最多250个) batchWorkQueue 队列中
assignBatchWork();
// 分配单项工作任务:pendingTasks 如果还有剩余任务,将没有过期的转移到 singleItemWorkQueue 队列中
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);
}
}
}
private boolean isFull() {
// 待处理的任务 >= 10000,也就是说 pendingTasks 最多放 10000 个任务
return pendingTasks.size() >= maxBufferSize;
}
private void drainInputQueues() throws InterruptedException {
do {
// 排出 reprocessQueue,将 reprocessQueue 队列的任务转移到 pendingTasks
drainReprocessQueue();
// 排出 acceptorQueue,将 acceptorQueue 队列的任务转移到 pendingTasks
drainAcceptorQueue();
if (isShutdown.get()) {
break;
}
// If all queues are empty, block for a while on the acceptor queue
if (reprocessQueue.isEmpty() && acceptorQueue.isEmpty() && pendingTasks.isEmpty()) {
// 等待任务放入 acceptorQueue,等待 10 毫秒
TaskHolder<ID, T> taskHolder = acceptorQueue.poll(10, TimeUnit.MILLISECONDS);
if (taskHolder != null) {
// 放入之后 acceptorQueue、pendingTasks 就不为空了
appendTaskHolder(taskHolder);
}
}
// pendingTasks 为空、acceptorQueue 不为空、reprocessQueue不为空时,就会一直循环
// 如果所有任务都处理完了,reprocessQueue、acceptorQueue、pendingTasks 都是空的,
// 这时就会循环等待任务进入 acceptorQueue,每次等待 10 毫秒
} while (!reprocessQueue.isEmpty() || !acceptorQueue.isEmpty() || pendingTasks.isEmpty());
}
private void drainAcceptorQueue() {
while (!acceptorQueue.isEmpty()) {
// 将 acceptorQueue 的任务转移到 pendingTasks
appendTaskHolder(acceptorQueue.poll());
}
}
private void drainReprocessQueue() {
long now = System.currentTimeMillis();
while (!reprocessQueue.isEmpty() && !isFull()) {
// 从 reprocessQueue 队尾取出任务
TaskHolder<ID, T> taskHolder = reprocessQueue.pollLast();
ID id = taskHolder.getId();
if (taskHolder.getExpiryTime() <= now) {
// 任务过期
expiredTasks++;
} else if (pendingTasks.containsKey(id)) {
// pendingTasks 已存在
overriddenTasks++;
} else {
// 将 reprocessQueue 队列的任务放到 pendingTasks
pendingTasks.put(id, taskHolder);
// 添加到 processingOrder 队列的头部,reprocessQueue 是失败重试的队列,所以优先级高一些
processingOrder.addFirst(id);
}
}
if (isFull()) {
queueOverflows += reprocessQueue.size();
// pendingTasks 满了,就清空 reprocessQueue
reprocessQueue.clear();
}
}
private void appendTaskHolder(TaskHolder<ID, T> taskHolder) {
if (isFull()) {
// pendingTasks 满了就移除一个元素
pendingTasks.remove(processingOrder.poll());
queueOverflows++;
}
// 将 acceptorQueue 里的任务放到 pendingTasks
TaskHolder<ID, T> previousTask = pendingTasks.put(taskHolder.getId(), taskHolder);
if (previousTask == null) {
// 原本不存在,将任务ID添加到 processingOrder 队列的最后
processingOrder.add(taskHolder.getId());
} else {
// 已经存在了,就是覆盖
overriddenTasks++;
}
}
void assignSingleItemWork() {
if (!processingOrder.isEmpty()) {
if (singleItemWorkRequests.tryAcquire(1)) {
long now = System.currentTimeMillis();
while (!processingOrder.isEmpty()) {
ID id = processingOrder.poll();
TaskHolder<ID, T> holder = pendingTasks.remove(id);
if (holder.getExpiryTime() > now) {
// 将 pendingTasks 的任务移到 singleItemWorkQueue
singleItemWorkQueue.add(holder);
return;
}
expiredTasks++;
}
singleItemWorkRequests.release();
}
}
}
void assignBatchWork() {
// 有足够的任务做一个批处理
if (hasEnoughTasksForNextBatch()) {
if (batchWorkRequests.tryAcquire(1)) {
long now = System.currentTimeMillis();
// 一批任务最多 250 个
int len = Math.min(maxBatchingSize, processingOrder.size());
List<TaskHolder<ID, T>> holders = new ArrayList<>(len);
// 将 pendingTasks 中的任务移动一批到 holders 中
// 也就是说,如果队列中有500个任务,这一批任务最多也是250个
while (holders.size() < len && !processingOrder.isEmpty()) {
ID id = processingOrder.poll();
TaskHolder<ID, T> holder = pendingTasks.remove(id);
if (holder.getExpiryTime() > now) {
holders.add(holder);
} else {
expiredTasks++;
}
}
if (holders.isEmpty()) {
batchWorkRequests.release();
} else {
batchSizeMetric.record(holders.size(), TimeUnit.MILLISECONDS);
// 添加到批量队列中
batchWorkQueue.add(holders);
}
}
}
}
// 是否有足够的任务做一个批处理
private boolean hasEnoughTasksForNextBatch() {
if (processingOrder.isEmpty()) {
return false;
}
if (pendingTasks.size() >= maxBufferSize) {
return true;
}
// 从 processingOrder 队首取一个任务ID,然后从 pendingTasks 读取这个任务。注意 peek() 只是取出元素,并不会移除队首的元素
TaskHolder<ID, T> nextHolder = pendingTasks.get(processingOrder.peek());
// 判断任务提交到现在的时间差是否超过最大批任务延迟时间(500毫秒)
long delay = System.currentTimeMillis() - nextHolder.getSubmitTimestamp();
return delay >= maxBatchingDelay;
}
}
先看它的 run 方法:
1、队列中的任务转移到待处理容器中
drainInputQueues 将输入队列(reprocessQueue、acceptorQueue)的任务转移到 pendingTasks 这个待处理容器中。
先是 drainReprocessQueue 将重处理队列 reprocessQueue 中的任务转移到 pendingTasks:
- 如果
pendingTasks已满(超过10000),就直接清空了reprocessQueue。任务丢弃会不会有影响呢? - 否则,如果
reprocessQueue非空,就从 reprocessQueue 队尾一个个取出来:- 如果过期了就丢掉这个任务,说明已经超过续约周期了(90秒)。比如实例注册,如果多次同步失败后,然后就直接丢弃,那不是其它 server 永远无法知道注册的这个实例?后面再分析这个问题。
- 如果
pendingTasks已经存在了,也丢弃这个重试任务 - 否则就添加到 pendingTasks 中,并且往
processingOrder的头部添加了任务ID - 注意它这里是从 reprocessQueue 队尾一个个取出,放入 processingOrder 头部,最终任务在 processingOrder 中的顺序跟 reprocessQueue 是一样的
然后是 drainAcceptorQueue 将接收者队列 acceptorQueue 中的任务转移到 pendingTasks:
- 只要 acceptorQueue 非空,就从队首取出任务
- 如果 pendingTasks 已满,则从
processingOrder队首取出第一个任务的ID,并从 pendingTasks 中移除这个任务 - 否则就将任务添加到 pendingTasks,如果之前不存在相同ID的任务,就将任务ID添加到 processingOrder 队尾
- 注意它这里是从 acceptorQueue 队首取出任务,放到 processingOrder 队尾,最终任务在 processingOrder 中的顺序跟 acceptorQueue 是一样的
从这段任务转移以及后面的使用来看,processingOrder 将决定任务的处理顺序,最前面的将最先处理,也说明了 reprocessQueue 的优先级比 acceptorQueue 更高。而 pendingTasks 是一个 key-value 的队列,便于快速通过ID读取任务。
private void drainAcceptorQueue() {
while (!acceptorQueue.isEmpty()) {
// 将 acceptorQueue 的任务转移到 pendingTasks
appendTaskHolder(acceptorQueue.poll());
}
}
private void drainReprocessQueue() {
long now = System.currentTimeMillis();
while (!reprocessQueue.isEmpty() && !isFull()) {
// 从 reprocessQueue 队尾取出任务
TaskHolder<ID, T> taskHolder = reprocessQueue.pollLast();
ID id = taskHolder.getId();
if (taskHolder.getExpiryTime() <= now) {
// 任务过期
expiredTasks++;
} else if (pendingTasks.containsKey(id)) {
// pendingTasks 已存在
overriddenTasks++;
} else {
// 将 reprocessQueue 队列的任务放到 pendingTasks
pendingTasks.put(id, taskHolder);
// 添加到 processingOrder 队列的头部,reprocessQueue 是失败重试的队列,所以优先级高一些
processingOrder.addFirst(id);
}
}
if (isFull()) {
queueOverflows += reprocessQueue.size();
// pendingTasks 满了,就清空 reprocessQueue
reprocessQueue.clear();
}
}
private void appendTaskHolder(TaskHolder<ID, T> taskHolder) {
if (isFull()) {
// pendingTasks 满了就移除一个元素
pendingTasks.remove(processingOrder.poll());
queueOverflows++;
}
// 将 acceptorQueue 里的任务放到 pendingTasks
TaskHolder<ID, T> previousTask = pendingTasks.put(taskHolder.getId(), taskHolder);
if (previousTask == null) {
// 原本不存在,将任务ID添加到 processingOrder 队列的最后
processingOrder.add(taskHolder.getId());
} else {
// 已经存在了,就是覆盖
overriddenTasks++;
}
}
2、接下来通过 trafficShaper 获取了一个补偿时间,它主要是在发生阻塞或网络异常导致任务提交失败后,在任务调度周期内做一个时间补偿,这块等分析到提交任务失败的时候再回来看看。
long now = System.currentTimeMillis();
if (scheduleTime < now) {
// 时间补偿,正常情况下 transmissionDelay() 返回 0
scheduleTime = now + trafficShaper.transmissionDelay();
}
3、任务打包
接着看 assignBatchWork ,它就是将任务打包成一个批次:
- 首先调用
hasEnoughTasksForNextBatch判断是否有足够的任务来打成一个批次,注意它判断了最新提交的任务的时间是否超过了延迟时间maxBatchingDelay(500ms),也就是说批次任务每隔500毫秒运行一次。 - 能够打包后,要获取
batchWorkRequests信号量的一个许可证,因为许可证默认数量是0,那一定是先有地方调用了batchWorkRequests.release()放入许可证,否则这里就不会打包了。 - 然后可以看出,一个批次的任务数量最多是
250个 - 它从 processingOrder 的队首取出这个批次的任务ID,并从 pendingTasks 中取出任务,如果是过期的任务就直接丢弃了。
- 然后如果这个批次并没有任务,他才调用
batchWorkRequests.release()释放了许可证,否则就把这个批次任务添加到批量工作队列batchWorkQueue中,注意并没有释放许可证。
void assignBatchWork() {
// 有足够的任务做一个批处理
if (hasEnoughTasksForNextBatch()) {
// 获取许可证
if (batchWorkRequests.tryAcquire(1)) {
long now = System.currentTimeMillis();
// 一批任务最多 250 个
int len = Math.min(maxBatchingSize, processingOrder.size());
List<TaskHolder<ID, T>> holders = new ArrayList<>(len);
// 将 pendingTasks 中的任务移动一批到 holders 中
// 也就是说,如果队列中有500个任务,这一批任务最多也是250个
while (holders.size() < len && !processingOrder.isEmpty()) {
ID id = processingOrder.poll();
TaskHolder<ID, T> holder = pendingTasks.remove(id);
if (holder.getExpiryTime() > now) {
holders.add(holder);
} else {
expiredTasks++;
}
}
if (holders.isEmpty()) {
batchWorkRequests.release();
} else {
batchSizeMetric.record(holders.size(), TimeUnit.MILLISECONDS);
// 添加到批量队列中
batchWorkQueue.add(holders);
}
}
}
}
// 是否有足够的任务做一个批处理
private boolean hasEnoughTasksForNextBatch() {
if (processingOrder.isEmpty()) {
return false;
}
if (pendingTasks.size() >= maxBufferSize) {
return true;
}
// 从 processingOrder 队首取一个任务ID,然后从 pendingTasks 读取这个任务。注意 peek() 只是取出元素,并不会移除队首的元素
TaskHolder<ID, T> nextHolder = pendingTasks.get(processingOrder.peek());
// 判断任务提交到现在的时间差是否超过最大批任务延迟时间(500毫秒)
long delay = System.currentTimeMillis() - nextHolder.getSubmitTimestamp();
return delay >= maxBatchingDelay;
}
接着看分配单项任务的方法 assignSingleItemWork:
- 如果 processingOrder 非空且获取到了
singleItemWorkRequests信号量的许可证,就将 processingOrder 队列剩余的任务都取出来,放入单项工作队列singleItemWorkQueue中 - 也就是前面已经打了一批任务(
250个)之后,processingOrder 中还有任务,就全部取出来放到singleItemWorkQueue队列中
void assignSingleItemWork() {
if (!processingOrder.isEmpty()) {
if (singleItemWorkRequests.tryAcquire(1)) {
long now = System.currentTimeMillis();
while (!processingOrder.isEmpty()) {
ID id = processingOrder.poll();
TaskHolder<ID, T> holder = pendingTasks.remove(id);
if (holder.getExpiryTime() > now) {
// 将 pendingTasks 的任务移到 singleItemWorkQueue
singleItemWorkQueue.add(holder);
return;
}
expiredTasks++;
}
singleItemWorkRequests.release();
}
}
}
任务处理器 TaskExecutors
batchWorkQueue 中的批量任务以及 singleItemWorkQueue 中的单项任务都已经准备好了,那是在哪里发送到集群节点的呢,那就是任务执行器 TaskExecutors 了。
1、创建 TaskExecutors
从创建 TaskExecutors 的方法中可以看出:
- 批量处理任务的类是
BatchWorkerRunnable,它主要就是处理批量任务队列batchWorkQueue中的任务 - 处理单项任务的类是
SingleTaskWorkerRunnable,它主要就是处理单项任务队列singleItemWorkQueue中的任务 - TaskExecutors 创建了一个线程池,batchExecutors 默认有
20个工作线程(不太理解他为什么不用JDK现成的线程池。。),singleItemExecutors 默认只有一个工作线程。
static <ID, T> TaskExecutors<ID, T> singleItemExecutors(final String name, int workerCount, final TaskProcessor<T> processor, final AcceptorExecutor<ID, T> acceptorExecutor) {
final AtomicBoolean isShutdown = new AtomicBoolean();
final TaskExecutorMetrics metrics = new TaskExecutorMetrics(name);
registeredMonitors.put(name, metrics);
// workerCount = 1
return new TaskExecutors<>(idx -> new SingleTaskWorkerRunnable<>("TaskNonBatchingWorker-" + name + '-' + idx, isShutdown, metrics, processor, acceptorExecutor), workerCount, isShutdown);
}
////////////////////////////////////////////////
static <ID, T> TaskExecutors<ID, T> batchExecutors(final String name, int workerCount, final TaskProcessor<T> processor, final AcceptorExecutor<ID, T> acceptorExecutor) {
final AtomicBoolean isShutdown = new AtomicBoolean();
final TaskExecutorMetrics metrics = new TaskExecutorMetrics(name);
registeredMonitors.put(name, metrics);
// BatchWorkerRunnable 批量任务处理
return new TaskExecutors<>(idx -> new BatchWorkerRunnable<>("TaskBatchingWorker-" + name + '-' + idx, isShutdown, metrics, processor, acceptorExecutor), workerCount, isShutdown);
}
////////////////////////////////////////////////
private final List<Thread> workerThreads;
TaskExecutors(WorkerRunnableFactory<ID, T> workerRunnableFactory, int workerCount, AtomicBoolean isShutdown) {
this.isShutdown = isShutdown;
// 工作线程集合
this.workerThreads = new ArrayList<>();
// 创建20个线程,相当于是搞了一个线程池
ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
for (int i = 0; i < workerCount; i++) {
WorkerRunnable<ID, T> runnable = workerRunnableFactory.create(i);
Thread workerThread = new Thread(threadGroup, runnable, runnable.getWorkerName());
workerThreads.add(workerThread);
workerThread.setDaemon(true);
workerThread.start();
}
}
2、BatchWorkerRunnable
看批量处理的任务:
- 首先
getWork获取批量任务,它调用taskDispatcher.requestWorkItems(),实际就是返回了 taskDispatcher 的batchWorkQueue,并且调用batchWorkRequests.release()往信号量放入一个许可证,这样前面 AcceptorRunner 就可以得到许可证然后去打包批量任务了 - 如果 batchWorkQueue 中没有批量任务,可以看到是一直在 while 循环等待的,直到拿到一个批量任务。它这个
BatchWorkerRunnable任务和前面的AcceptorRunner任务,感觉通过信号量的方式就形成了一个等待通知的机制,BatchWorkerRunnable 放入一个许可证,让 AcceptorRunner 拿到这个许可证去打个批次的任务过来。 - 拿到这个批次任务后,就调用
processor(ReplicationTaskProcessor)来处理任务。 - 如果任务处理结果是
Congestion(阻塞)、TransientError(传输失败)就要重处理,调用了taskDispatcher.reprocess将这个批次的任务提交到重处理队列reprocessQueue中。
static class BatchWorkerRunnable<ID, T> extends WorkerRunnable<ID, T> {
BatchWorkerRunnable(String workerName, AtomicBoolean isShutdown, TaskExecutorMetrics metrics, TaskProcessor<T> processor, AcceptorExecutor<ID, T> acceptorExecutor) {
super(workerName, isShutdown, metrics, processor, acceptorExecutor);
}
@Override
public void run() {
try {
while (!isShutdown.get()) {
// 获取一个批量任务
List<TaskHolder<ID, T>> holders = getWork();
metrics.registerExpiryTimes(holders);
// TaskHolder 提取 ReplicationTask
List<T> tasks = getTasksOf(holders);
// processor => 任务复制处理器 ReplicationTaskProcessor
ProcessingResult result = processor.process(tasks);
switch (result) {
case Success:
break;
case Congestion:
case TransientError:
// 阻塞或网络失败就重新处理这批任务
taskDispatcher.reprocess(holders, result);
break;
case PermanentError:
logger.warn("Discarding {} tasks of {} due to permanent error", holders.size(), workerName);
}
metrics.registerTaskResult(result, tasks.size());
}
} catch (InterruptedException e) {
// Ignore
} catch (Throwable e) {
// Safe-guard, so we never exit this loop in an uncontrolled way.
logger.warn("Discovery WorkerThread error", e);
}
}
private List<TaskHolder<ID, T>> getWork() throws InterruptedException {
// 获取批量队列 batchWorkQueue
BlockingQueue<List<TaskHolder<ID, T>>> workQueue = taskDispatcher.requestWorkItems();
List<TaskHolder<ID, T>> result;
do {
result = workQueue.poll(1, TimeUnit.SECONDS);
// 循环等待,直到取到一个批量任务
} while (!isShutdown.get() && result == null);
return (result == null) ? new ArrayList<>() : result;
}
}
BlockingQueue<TaskHolder<ID, T>> requestWorkItem() {
singleItemWorkRequests.release();
return singleItemWorkQueue;
}
BlockingQueue<List<TaskHolder<ID, T>>> requestWorkItems() {
batchWorkRequests.release();
return batchWorkQueue;
}
3、任务重处理
可以看到处理失败后,就是将这批任务添加到重处理队列 reprocessQueue 中去,然后向时间调整期注册失败,这就和前面 AcceptorRunner 处理 reprocessQueue 对应起来了。
void reprocess(List<TaskHolder<ID, T>> holders, ProcessingResult processingResult) {
// 添加到重处理队列 reprocessQueue
reprocessQueue.addAll(holders);
replayedTasks += holders.size();
// 时间调整器注册失败
trafficShaper.registerFailure(processingResult);
}
4、TrafficShaper
还记得前面 AcceptorRunner 中又这样一段代码,可以看到是通过 trafficShaper 计算了一个延迟时间,这里就来看看是如何计算的。
long now = System.currentTimeMillis();
if (scheduleTime < now) {
// 时间补偿,正常情况下 transmissionDelay() 返回 0
scheduleTime = now + trafficShaper.transmissionDelay();
}
if (scheduleTime <= now) {
// 分配批量工作任务:将 pendingTasks 的任务分一批到(最多250个) batchWorkQueue 队列中
assignBatchWork();
// 分配单项工作任务:pendingTasks 如果还有剩余任务,将没有过期的转移到 singleItemWorkQueue 队列中
assignSingleItemWork();
}
时间调整器 TrafficShaper:
registerFailure就是设置了失败的最后时间- 然后看
transmissionDelay,以阻塞为例,如果上一次阻塞失败到现在 500 毫秒,那么 transmissionDelay 返回 500,那么 transmissionDelay 就大于 now 了,就不会打包任务了。 - 总结下来就是如果上一次阻塞导致批量任务提交失败,就延迟1000毫秒后执行。如果上一次网络导致批量任务提交失败,就延迟100毫秒执行。
TrafficShaper(long congestionRetryDelayMs, long networkFailureRetryMs) {
// 1000
this.congestionRetryDelayMs = Math.min(MAX_DELAY, congestionRetryDelayMs);
// 100
this.networkFailureRetryMs = Math.min(MAX_DELAY, networkFailureRetryMs);
}
void registerFailure(ProcessingResult processingResult) {
if (processingResult == ProcessingResult.Congestion) {
// 最后一次阻塞导致提交批处理失败的时间
lastCongestionError = System.currentTimeMillis();
} else if (processingResult == ProcessingResult.TransientError) {
// 最后一次网络原因导致提交批处理失败的时间
lastNetworkFailure = System.currentTimeMillis();
}
}
// 计算传输延迟的时间
long transmissionDelay() {
if (lastCongestionError == -1 && lastNetworkFailure == -1) {
return 0;
}
long now = System.currentTimeMillis();
if (lastCongestionError != -1) {
// 阻塞延迟时间
long congestionDelay = now - lastCongestionError;
if (congestionDelay >= 0 && congestionDelay < congestionRetryDelayMs) {
return congestionRetryDelayMs - congestionDelay;
}
lastCongestionError = -1;
}
if (lastNetworkFailure != -1) {
// 网络延迟时间
long failureDelay = now - lastNetworkFailure;
if (failureDelay >= 0 && failureDelay < networkFailureRetryMs) {
return networkFailureRetryMs - failureDelay;
}
lastNetworkFailure = -1;
}
return 0;
}
5、SingleTaskWorkerRunnable
单项任务处理跟批量任务处理的流程是类似的,只不过是一个个的发送同步操作,处理失败同样也会放入重处理队列中。
一个批量任务250个对于大部分场景来说其实不会触发单项任务的处理,如果微服务集群中有很多的实例,eureka 通过不断的轮询也能尽量使用批量处理,我觉得单项任务处理更像是对批量任务处理的一种补充。
复制任务处理器 ReplicationTaskProcessor
批量任务最终是提交到 ReplicationTaskProcessor 去处理的,可以看到,就是调用了 replicationClient 提交了批量任务,提交的接口是 POST peerreplication/batch,那我们就可以从这个入口去看 eureka-server 如何接收批量任务的。
public ProcessingResult process(List<ReplicationTask> tasks) {
// 任务封装到 ReplicationList
ReplicationList list = createReplicationListOf(tasks);
try {
// 提交批量任务:POST peerreplication/batch/
EurekaHttpResponse<ReplicationListResponse> response = replicationClient.submitBatchUpdates(list);
int statusCode = response.getStatusCode();
if (!isSuccess(statusCode)) {
if (statusCode == 503) {
return ProcessingResult.Congestion;
} else {
return ProcessingResult.PermanentError;
}
} else {
// 处理批量任务结果
handleBatchResponse(tasks, response.getEntity().getResponseList());
}
} catch (Throwable e) {
if (maybeReadTimeOut(e)) {
return ProcessingResult.Congestion;
} else if (isNetworkConnectException(e)) {
return ProcessingResult.TransientError;
} else {
return ProcessingResult.PermanentError;
}
}
return ProcessingResult.Success;
}
接收复制同步请求
很容易找到批量任务提交的接口在 PeerReplicationResource 的 batchReplication 方法中。
可以看到,其实遍历批量任务,然后根据不同的操作类型,调用 XxxResource 接口进行对应的操作。比如注册,就是调用 applicationResource.addInstance 完成实例的注册。
@Path("/{version}/peerreplication")
@Produces({"application/xml", "application/json"})
public class PeerReplicationResource {
private static final Logger logger = LoggerFactory.getLogger(PeerReplicationResource.class);
private static final String REPLICATION = "true";
private final EurekaServerConfig serverConfig;
private final PeerAwareInstanceRegistry registry;
@Inject
PeerReplicationResource(EurekaServerContext server) {
this.serverConfig = server.getServerConfig();
this.registry = server.getRegistry();
}
public PeerReplicationResource() {
this(EurekaServerContextHolder.getInstance().getServerContext());
}
@Path("batch")
@POST
public Response batchReplication(ReplicationList replicationList) {
try {
ReplicationListResponse batchResponse = new ReplicationListResponse();
for (ReplicationInstance instanceInfo : replicationList.getReplicationList()) {
try {
// dispatch 分发任务
batchResponse.addResponse(dispatch(instanceInfo));
} catch (Exception e) {
batchResponse.addResponse(new ReplicationInstanceResponse(Status.INTERNAL_SERVER_ERROR.getStatusCode(), null));
}
}
return Response.ok(batchResponse).build();
} catch (Throwable e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
}
}
private ReplicationInstanceResponse dispatch(ReplicationInstance instanceInfo) {
ApplicationResource applicationResource = createApplicationResource(instanceInfo);
InstanceResource resource = createInstanceResource(instanceInfo, applicationResource);
String lastDirtyTimestamp = toString(instanceInfo.getLastDirtyTimestamp());
String overriddenStatus = toString(instanceInfo.getOverriddenStatus());
String instanceStatus = toString(instanceInfo.getStatus());
Builder singleResponseBuilder = new Builder();
// 根据不同的类型分别处理
switch (instanceInfo.getAction()) {
case Register: // 注册
singleResponseBuilder = handleRegister(instanceInfo, applicationResource);
break;
case Heartbeat: // 续约
singleResponseBuilder = handleHeartbeat(serverConfig, resource, lastDirtyTimestamp, overriddenStatus, instanceStatus);
break;
case Cancel: // 下线
singleResponseBuilder = handleCancel(resource);
break;
case StatusUpdate: // 状态变更
singleResponseBuilder = handleStatusUpdate(instanceInfo, resource);
break;
case DeleteStatusOverride:
singleResponseBuilder = handleDeleteStatusOverride(instanceInfo, resource);
break;
}
return singleResponseBuilder.build();
}
private static Builder handleRegister(ReplicationInstance instanceInfo, ApplicationResource applicationResource) {
// addInstance
applicationResource.addInstance(instanceInfo.getInstanceInfo(), REPLICATION);
return new Builder().setStatusCode(Status.OK.getStatusCode());
}
private static Builder handleCancel(InstanceResource resource) {
// cancelLease
Response response = resource.cancelLease(REPLICATION);
return new Builder().setStatusCode(response.getStatus());
}
private static Builder handleHeartbeat(EurekaServerConfig config, InstanceResource resource, String lastDirtyTimestamp, String overriddenStatus, String instanceStatus) {
Response response = resource.renewLease(REPLICATION, overriddenStatus, instanceStatus, lastDirtyTimestamp);
int responseStatus = response.getStatus();
Builder responseBuilder = new Builder().setStatusCode(responseStatus);
if ("false".equals(config.getExperimental("bugfix.934"))) {
if (responseStatus == Status.OK.getStatusCode() && response.getEntity() != null) {
responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
}
} else {
if ((responseStatus == Status.OK.getStatusCode() || responseStatus == Status.CONFLICT.getStatusCode())
&& response.getEntity() != null) {
responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
}
}
return responseBuilder;
}
private static Builder handleStatusUpdate(ReplicationInstance instanceInfo, InstanceResource resource) {
Response response = resource.statusUpdate(instanceInfo.getStatus(), REPLICATION, toString(instanceInfo.getLastDirtyTimestamp()));
return new Builder().setStatusCode(response.getStatus());
}
private static Builder handleDeleteStatusOverride(ReplicationInstance instanceInfo, InstanceResource resource) {
Response response = resource.deleteStatusUpdate(REPLICATION, instanceInfo.getStatus(),
instanceInfo.getLastDirtyTimestamp().toString());
return new Builder().setStatusCode(response.getStatus());
}
private static <T> String toString(T value) {
if (value == null) {
return null;
}
return value.toString();
}
}
集群数据同步冲突问题
Peer to Peer 模式重点要解决的一个问题是数据复制冲突的问题,因为 peer 节点间的相互复制并不能保证所有操作都成功。eureka 主要通过 lastDirtyTimestamp 标识和心跳来进行数据的最终修复,下面就来看下 eureka 如何处理数据冲突问题的。
1、先看续约的这个方法
- 在续约
renewLease里,如果lastDirtyTimestamp不为空且允许时间戳不一致时进行同步(默认开启),就调用了validateDirtyTimestamp方法校验 lastDirtyTimestamp。 - 接着看 validateDirtyTimestamp,如果 lastDirtyTimestamp 与本地实例的 lastDirtyTimestamp 一致,说明数据是一致的,就续约成功,返回
OK(200)。 - 如果 lastDirtyTimestamp 大于 本地实例的 lastDirtyTimestamp,说明复制的实例最新更新的,出现数据冲突,返回
NOT_FOUND(404)。 - 如果 lastDirtyTimestamp 小于 本地实例的 lastDirtyTimestamp ,说明复制的实例是旧的,出现数据冲突,返回
CONFLICT(409),并且返回了本地的实例。
@PUT
public Response renewLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
boolean isFromReplicaNode = "true".equals(isReplication);
// 调用注册表的 renew 进行服务续约
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
if (!isSuccess) {
logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
return Response.status(Status.NOT_FOUND).build();
}
Response response;
// 如果是复制操作,就校验 lastDirtyTimestamp
if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
// Store the overridden status since the validation found out the node that replicates wins
if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
&& (overriddenStatus != null)
&& !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
&& isFromReplicaNode) {
registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
}
} else {
response = Response.ok().build();
}
return response;
}
private Response validateDirtyTimestamp(Long lastDirtyTimestamp, boolean isReplication) {
InstanceInfo appInfo = registry.getInstanceByAppAndId(app.getName(), id, false);
if (appInfo != null) {
// 如果复制传过来的实例中 lastDirtyTimestamp 不等于本地实例的 lastDirtyTimestamp
if ((lastDirtyTimestamp != null) && (!lastDirtyTimestamp.equals(appInfo.getLastDirtyTimestamp()))) {
Object[] args = {id, appInfo.getLastDirtyTimestamp(), lastDirtyTimestamp, isReplication};
if (lastDirtyTimestamp > appInfo.getLastDirtyTimestamp()) {
// 如果复制实例的 lastDirtyTimestamp > 本地实例的 lastDirtyTimestamp,表示数据出现冲突,返回 404,要求应用实例重新进行 register 操作
return Response.status(Status.NOT_FOUND).build();
} else if (appInfo.getLastDirtyTimestamp() > lastDirtyTimestamp) {
// In the case of replication, send the current instance info in the registry for the
// replicating node to sync itself with this one.
if (isReplication) {
// 如果本地实例的 lastDirtyTimestamp > 复制实例的 lastDirtyTimestamp,就返回 CONFLICT(409),说明数据冲突,要求其同步自己最新的数据
// 注意这里将本地实例 appInfo 放入 Response entity 中了
return Response.status(Status.CONFLICT).entity(appInfo).build();
} else {
return Response.ok().build();
}
}
}
}
return Response.ok().build();
}
2、接着看 PeerReplicationResource 处理心跳的方法
- 首先就是调用了续约的方法
renewLease进行续约 - 如果返回的状态是
OK或者CONFLICT,就在 resposeEntity 中返回本地实例
private static Builder handleHeartbeat(EurekaServerConfig config, InstanceResource resource, String lastDirtyTimestamp, String overriddenStatus, String instanceStatus) {
// 调用 renewLease 续约
Response response = resource.renewLease(REPLICATION, overriddenStatus, instanceStatus, lastDirtyTimestamp);
int responseStatus = response.getStatus();
Builder responseBuilder = new Builder().setStatusCode(responseStatus);
if ("false".equals(config.getExperimental("bugfix.934"))) {
if (responseStatus == Status.OK.getStatusCode() && response.getEntity() != null) {
responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
}
} else {
if ((responseStatus == Status.OK.getStatusCode() || responseStatus == Status.CONFLICT.getStatusCode())
&& response.getEntity() != null) {
// 续约成功或 CONFLICT 冲突时,将本地实例 appInfo 返回到客户端
responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
}
}
return responseBuilder;
}
3、PeerEurekaNode 发送心跳
ReplicationTaskProcessor 收到批量任务返回结果后,会处理响应结果,对于心跳任务,可以找到,失败后就会回调 handleFailure 方法。
- 如果返回状态是
404(NOT_FOUND),就会重新注册,也是提交到队列中。通过重新注册来实现数据同步。 - 如果是其它状态
(409 CONFLICT)并且开启了时间戳不一致就同步的配置,就将服务端返回的实例注册到本地,实现数据的同步。
public void heartbeat(final String appName, final String id, final InstanceInfo info,
final InstanceStatus overriddenStatus, boolean primeConnection) throws Throwable {
ReplicationTask replicationTask = new InstanceReplicationTask(targetHost, Action.Heartbeat, info, overriddenStatus, false) {
@Override
public EurekaHttpResponse<InstanceInfo> execute() throws Throwable {
// 向集群Server发送心跳
return replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
}
@Override
public void handleFailure(int statusCode, Object responseEntity) throws Throwable {
super.handleFailure(statusCode, responseEntity);
if (statusCode == 404) {
logger.warn("{}: missing entry.", getTaskName());
if (info != null) {
// 复制返回 404 时,重新注册
register(info);
}
} else if (config.shouldSyncWhenTimestampDiffers()) {
// 409(CONFLICT),将服务端返回的实例同步到本地
InstanceInfo peerInstanceInfo = (InstanceInfo) responseEntity;
if (peerInstanceInfo != null) {
syncInstancesIfTimestampDiffers(appName, id, info, peerInstanceInfo);
}
}
}
};
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
}
private void syncInstancesIfTimestampDiffers(String appName, String id, InstanceInfo info, InstanceInfo infoFromPeer) {
try {
if (infoFromPeer != null) {
// 将服务端的实例注册到本地,实现数据同步
registry.register(infoFromPeer, true);
}
} catch (Throwable e) {
logger.warn("Exception when trying to set information from peer :", e);
}
}
至此,我们就可以总结出,eureka server 通过对比 lastDirtyTimestamp 和心跳操作来实现集群数据的复制和最终同步。
前面提到的实例过期就丢弃任务这样看来就没问题,它也不保证peer节点间相互复制的所有操作都成功,eureka 采用的是最终一致性,它是通过心跳的方式实现集群数据的最终修复和同步,只是集群间可能会同步延迟。
一张图总结集群同步
下面总结下 eureka-server 集群节点间的同步:
- 首先 eureka-server 集群采用的是
Peer To Peer的模式,即对等复制,各个 server 不分主从,每个 server 都可以接收写请求,然后互相之间进行数据更新同步。 - 数据同步采用了
多层任务队列+批量处理的机制:- eureka-server 接收到客户端请求(注册、下线、续约)后都会调用集群 PeerEurekaNode 进行操作的同步
- PeerEurekaNode 将操作封装成 InstanceReplicationTask 实例复制任务,并用批量分发器 batchingDispatcher(TaskDispatcher)来分发处理
- batchingDispatcher 内部则将任务交给接收者执行器 AcceptorExecutor 处理,任务首先进入到 AcceptorExecutor 内的接收者队列 acceptorQueue 中
- AcceptorExecutor 有个后台工作线程(AcceptorRunner)不断轮询,将接收者队列 acceptorQueue 和 重处理队列 reprocessQueue 中的任务转移到处理中队列中(processingOrder + pendingTasks)
- 接着将处理中队列中的任务打包,一次最多 250 个任务,然后放到批量工作队列 batchWorkQueue。如果处理中队列中还有任务,就将任务放到单项任务队列 singleItemWorkQueue
- 任务都打包好了,任务执行器 TaskExecutors 内分别有批量任务处理器(BatchWorkerRunnable)和单项任务处理器(SingleTaskWorkerRunnable)来处理 batchWorkQueue 和 singleItemWorkQueue 中的任务
- 处理器会利用任务复制处理器(ReplicationTaskProcessor)来提交任务,批量任务会提交给 server 节点的批量接口(
peerreplication/batch/),单项任务则提交到对应的操作接口 - 任务提交如果阻塞或者网络失败就会被放入重处理队列 reprocessQueue,然后再次被 AcceptorRunner 轮询处理,不过过期(超过90秒)的任务会被丢弃掉
- 其它 eureka-server 同步:
- 其它 eureka-server 接收到批量复制请求后,会轮询批量任务列表,根据不同的操作类型(Register、Heartbeat、Cancel 等)分别调用 Resource 的接口进行处理
- 如果是续约操作,会判断复制实例的 lastDirtyTimestamp 与本地实例的 lastDirtyTimestamp,如果是一致的,就任务数据一致
- 如果复制实例的
lastDirtyTimestamp > 本地实例的 lastDirtyTimestamp,则复制实例的数据是最新的,返回 404(NOT_FOUND) 要求客户端重新发送一个注册操作过来 - 如果复制实例的
lastDirtyTimestamp < 本地实例的 lastDirtyTimestamp,则本地实例的数据是最新的,返回 409(CONFLICT)和本地实例,客户端用返回来的实例覆盖本地的实例
下面再用一张图总结集群同步: