Eurekaサーバーのクラスターレジストリ同期メカニズムを説明する図とソースコード

一緒に書く習慣をつけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して9日目です。クリックしてイベントの詳細をご覧ください。

Eurekaサーバーサービスカリングロジック

達成への忍耐は失敗への粘り強さよりも重要です-Laroshvko

関連記事
図+ソースコードの説明Eurekaサーバー起動プロセス分析
図+ソースコードの説明Eurekaクライアント起動プロセス分析
図+ソースコードの説明Eurekaサーバーレジストリキャッシュロジック
図+ソースコードの説明Eurekaクライアントプルレジストリフロー
図+ソースコードの説明Eurekaクライアントサービス登録プロセス
図+Eurekaクライアントのハートビートメカニズムを説明するソースコードフロー
図+Eurekaクライアントのオフラインプロセス分析
を説明するソースコード図+Eurekaサーバーサービス除去ロジックを説明するソースコード

コア回路図

    eurekaサーバーのサービスノード間の相互通信により、どのサービスノードがダウンしていても、他のサービスノードにアクセスしてアクセスするノードにアクセスできます。たとえば、図では、クライアントはサービス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を介してバッチ更新操作タスクを送信します。また、ジャージフレームワークを介してsubmitBatchUpdatesメソッドを要求し、peerreplication / batch/pathの要求を検索します。

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

バッチタスクコア処理要求クラス

    eureka-coreのプロジェクトにPeerReplicationResourceがあります。このクラスのリクエストパスにはpeerreplicationがあり、パスにバッチ識別子を持つメソッドがあるので、ここにあります。

@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. ハートビートリクエストの処理

ハイライト

    3層キューは、タスク処理操作、受信キュー、処理キュー、バッチ実行タスクキュー、および同期操作の実行を実行します。

おすすめ

転載: juejin.im/post/7084364130508865572