图解+源码讲解 Eureka Server 集群注册表同步机制

1,202 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情

Eureka Server 服务剔除逻辑

取得成就时坚持不懈,要比遭到失败时顽强不屈更重要 —— 拉罗什夫科 相关文章
eureka-server 项目结构分析
图解+源码讲解 Eureka Server 启动流程分析
图解+源码讲解 Eureka Client 启动流程分析
图解+源码讲解 Eureka Server 注册表缓存逻辑
图解+源码讲解 Eureka Client 拉取注册表流程
图解+源码讲解 Eureka Client 服务注册流程
图解+源码讲解 Eureka Client 心跳机制流程
图解+源码讲解 Eureka Client 下线流程分析
图解+源码讲解 Eureka Server 服务剔除逻辑
图解+源码讲解 Eureka Server 集群注册表同步机制

核心原理图

    eureka server 服务节点之间的互相通信操作,这样一来无论是哪个服务节点宕机了,那么访问其他的服务节点也能访问到所要访问的节点,比如图中,客户端向服务A 进行注册的时候,如果注册成功了那么就会将当前节点同步到其他的服务节点中去,这样就是集群同步机制
image.png

从哪里开始分析

    既然都是拿注册进行举例了那么就是看看注册后的操作流程是如何进行集群信息同步的,先看看注册的代码

public void register(final InstanceInfo info, final boolean isReplication) {
    // 90
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    /**
     * 真正的注册是在父类的方法中
     */
    super.register(info, leaseDuration, isReplication);
    /**
     * 同步当前实例信息到其他的eureka-server节点中
     */
    replicateToPeers(Action.Register, info.getAppName(), 
                     info.getId(), info, null, isReplication);
}

    注册的方法我们已经分析过了在客户端注册的文章里面,我们主要是来看看是如何进行后续的节点信息同步的

replicateToPeers(Action.Register, info.getAppName(), info.getId(), 
                 info, null, isReplication);

核心流程

代码核心流程图

image.png
    对于同步来说,我们在实例的注册、下线、心跳、状态更新等等都会同步给到其他的服务节点去

public enum Action {
    // 心跳、注册、下线、状态更新
    Heartbeat, Register, Cancel, StatusUpdate, DeleteStatusOverride;
}

    只不过就是 replicateToPeers 方法调用的时候传入的值是不同的,先看注册的流程

是自己发起注册同步还是别人发起的

    如果是别人发起的同步那么就放入到自己的注册表中就好了,如果不是的话那么就是自己发起的同步,这样一来就是将自己过滤除去同步给其他人,isReplication 这个值就是控制它的

private void replicateToPeers(Action action, String appName, String id,
                      InstanceInfo info /* optional */,
                      InstanceStatus newStatus /* optional */,
                      boolean isReplication) {
    /**
     * isReplication 如果当前实例有eureka-server已经同步过的话那么
     * 就不用其他eureka-server在进行同步了
     * 或者服务节点为null的话也不需要同步了
     */
    if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
        return;
    }

for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
    /**
     * 同步的时候排除自己,只需要同步给其他eureka-server即可
     */
    if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
        continue;
    }
    /**
     * 如果是某台eureka client来找eureka server进行注册,此时会给其他所有的你配置的
     * eureka server都同步这个注册请求,
     * 此时一定会基于jersey,调用其他所有的eureka server的restful接口,
     * 去执行这个服务实例的注册的请求
     */
    replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
    }
}

遍历所有节点去进行注册当前实例

    其实就是在遍历peerEurekaNodes.getPeerEurekaNodes() 里面的值,这里面的值是在服务端初始化的时候获取的其他服务中心节点的值,所以就知道里面有多少服务中心了,将要注册的客户端注册给其他服务中心

replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node)
    
private void replicateInstanceActionsToPeers(Action action, 
       String appName, String id, InstanceInfo info, 
              InstanceStatus newStatus, PeerEurekaNode node) {
    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;
    }
}

    上面的switch 操作分别对应了下线、发送心跳、注册、状态更新等操作,我们这里是注册所以看一下node.register(info) 方法

节点注册

    客户端注册之前我们都讲过了,就是将当前的服务节点通过jersey框架进行访问之后在走一遍注册逻辑,我们看看同步的时候是怎么操作的是不是我们想的样子

public void register(final InstanceInfo info) throws Exception {
    // 到期时间 默认是当前的系统时间 + 90s
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(
            // 创建了一个任务ID
            taskId("register", info),
            // 创建了一个实例复制任务
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                public EurekaHttpResponse<Void> execute() {
                    return replicationClient.register(info);
                }
            },
        // 到期时间
            expiryTime
    );
}

    我们看到这里用了一个 batchingDispatcher 批量调度器,这个调度器是在创建节点的时候尽心初始化的调度器

this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
            // 调度器名字
            batcherName,
            // 默认是 10000 个
            config.getMaxElementsInPeerReplicationPool(),
            // 批量的大小
            batchSize,
            // 最大实例同步个数默认20个
            config.getMaxThreadsForPeerReplication(),
            // 时间
            maxBatchingDelayMs,
            serverUnavailableSleepTimeMs,
            retrySleepTimeMs,
            // 任务执行器
            taskProcessor
);

    这样一来就是通过这个批量调度器在一定的时间内也就是请求到来的时间+90s内进行同步请求注册就可以了,通过这个 replicationClient 进行注册请求,后续的逻辑就是走的上面的注册逻辑,唯一的区别就是同步的时候是放到了调度其中去执行的,不是直接去请求访问注册的

批量调度器的创建以及运行流程

// 任务调度器
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) {
    // 任务接收执行器
    final AcceptorExecutor<ID, T> acceptorExecutor = new AcceptorExecutor<>(
            id, maxBufferSize, workloadSize, maxBatchingDelay, 
        congestionRetryDelayMs, networkFailureRetryMs
    );
    // 任务执行器
    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.process(id, task, expiryTime);
        }

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

任务接收器创建

AcceptorExecutor(String id,
                 int maxBufferSize,
                 int maxBatchingSize,
                 long maxBatchingDelay,
                 long congestionRetryDelayMs,
                 long networkFailureRetryMs) {
    this.id = id;
    this.maxBufferSize = maxBufferSize;
    this.maxBatchingSize = maxBatchingSize;
    this.maxBatchingDelay = maxBatchingDelay;
    ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
    this.acceptorThread = new Thread(threadGroup, new AcceptorRunner(),
                                     "TaskAcceptor-" + id);
    this.acceptorThread.setDaemon(true);
    this.acceptorThread.start();
}

    核心代码是 new AcceptorRunner() 这个代码,接收运行任务代码,进行任务队列处理,就是接收队列进入处理队列的逻辑,之后通过定义的任务执行器进行执行

任务执行器创建

    上面将要执行的请求任务进行了处理操作得到了一些需要处理的批量任务

static class BatchWorkerRunnable<ID, T> extends WorkerRunnable<ID, T> {
// 线程运行的主要逻辑代码是这块了
@Override
public void run() {
    while (!isShutdown.get()) {
        List<TaskHolder<ID, T>> holders = getWork();
        metrics.registerExpiryTimes(holders);

        List<T> tasks = getTasksOf(holders);
        // 任务执行器真正处理代码的地方
        ProcessingResult result = processor.process(tasks);
    }
}

任务执行器执行任务

    processor.process(tasks) 方法调用的是 ReplicationTaskProcessor 中的 process 方法进行处理请求的,通过replicationClient 进行提交批量的更新操作任务,也是通过 jersey 框架去请求的 submitBatchUpdates 方法,找到peerreplication/batch/ 路径的请求

public ProcessingResult process(List<ReplicationTask> tasks) {
  // 进行任务的细节处理
    ReplicationList list = createReplicationListOf(tasks);
    EurekaHttpResponse<ReplicationListResponse> response = 
        replicationClient.submitBatchUpdates(list);
}

批量任务核心处理请求类

    在eureka-core的项目工程里面有一个 PeerReplicationResource 这个类里面的请求路径带有peerreplication,并且有一个方法上面的路径带有batch 标识所以就在这里面了

@Path("/{version}/peerreplication")
@Produces({"application/xml", "application/json"})
public class PeerReplicationResource {
@Path("batch")
@POST
public Response batchReplication(ReplicationList replicationList) {
    ReplicationListResponse batchResponse = new ReplicationListResponse();
    for (ReplicationInstance instanceInfo : replicationList.getReplicationList()) {
            batchResponse.addResponse(dispatch(instanceInfo));
    }
    return Response.ok(batchResponse).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();
}

处理心跳请求

    handleRegister(instanceInfo, applicationResource),这个方法是处理注册请求的方法

private static Builder handleRegister(ReplicationInstance instanceInfo,
                                      ApplicationResource applicationResource) {
    applicationResource.addInstance(instanceInfo.getInstanceInfo(), REPLICATION);
    return new Builder().setStatusCode(Status.OK.getStatusCode());
}

    在往下看就是我们之前看过的实例注册请求的代码了,不过这个时候,REPLICATION 这个值就是true了,所以到当前服务节点只是一个注册的请求,不会再向其他节点进行注册同步了

public Response addInstance(InstanceInfo info, 
           @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    /**
     *  "true".equals(isReplication) 注意这个参数
     */
    registry.register(info, "true".equals(isReplication));
    return Response.status(204).build();  // 204 to be backwards compatible
}

小结

  1. 客户端发起向服务端的注册
  2. 根据是否是自己发起的注册进行是否同步操作
  3. 创建任务调度器进行任务的批量接收和调度
  4. 批量处理任务核心请求
  5. 处理心跳请求

亮点

    三层队列进行任务处理操作、接收队列,处理队列,批量执行任务队列,进行同步操作的执行