Nacosソースコード - Nacos 2.0の性能が9倍向上する秘密

要約:

Nacos 1.4.1バージョンの登録センターと設定センターの主要な機能モジュールについて、单体およびクラスタ環境での使用方法を学びました。Nacos 1.4.1バージョンの学習を通じて、多くのことを学ぶことができました。清潔なコードスタイル、非同期タスク、メモリキュー、プッシュプル結合などの優れた設計です。

しかし、Nacosはまだ更新されており、Nacos 2.xバージョンでは、以下を含む多くの更新が行われています:

  • Nacosクラスタ内の通信がHTTP方式からgRPC方式に変更されました
  • NacosのRaftプロトコルは蚂蚁内部のJRaftを使用し、最適化されています
  • Nacos 1.xバージョンでは登録表に二重Map構造を使用していましたが、Nacos 2.xでは軽量化処理が行われました
  • Nacos 1.xバージョンでは、一つのサービスに同時に永続化インスタンスと非永続化インスタンスが存在することが許可されていました。永続化属性はインスタンスのメタデータとしてのみ保存・識別されていましたが、これは実際の運用において問題となり、システムアーキテクチャの観点からも矛盾が生じていました。そのため、Nacos 2.xではNacosのサービスデータモデルが簡素化され、永続化の有無がサービスレベルに抽象化され、一つのサービスに同時に永続化と非永続化のインスタンスが存在することは許可されなくなりました。インスタンスの永続化属性設定はサービスの永続化属性設定を継承します
  • Nacosの設定管理では、SDKとServer間の整合性プロトコルはMD5値が一致するかどうかで判断されます。

Nacos 1.4バージョンでは、HTTP 1.1の短い接続を長い接続としてシミュレートし、30秒ごとにハートビートを送信してSDK設定のMD5値がServerと一致しているか確認していました。一致していれば接続を維持し、不一致であれば不一致の設定を返し、SDKが最新の設定値を取得していました。Nacos 2.xバージョンでは、30秒ごとの長いポーリングは使用されず、長い接続モードにアップグレードされました。設定変更時に長い接続を確立し、設定変更後にサーバーが変更された設定リストをプッシュし、SDKが設定更新を取得します。通信効率が大幅に向上しました

阿里のテストレポートによると、大規模な登録の場合、Nacos 2.0は安定したシナリオにおいて少なくともNacos 1.xの9倍の性能を発揮し、安定状態に達した後もNacos 2.0のパフォーマンスはより優れています。クライアントとインスタンスの数量が約10倍の場合でも、より少ないCPU消費で動作します。この大幅な最適化は、コードレベルで多くの最適化が行われた結果であり、Nacos 2.xバージョンは学ぶ価値のあるものです。

クライアントがgRPCでサービス登録を開始する

皆さんはNacos 1.4でクライアントがどのように自動的に登録を開始し、登録ロジックがどのように処理されたか覚えていますか? 覚えていない方は、Nacosソースコード学習計画-Day02-クライアント自動登録とクライアントハートビート検出の原理 - ZealSingerのブログを参照してください~

Nacos 1.4バージョンでは、クライアントはSpringのEventイベント通知メカニズムを利用し、Springコンテナの初期化が完了すると、WebServerInitializedEvent初期化完了イベントを送信します。Nacosではこのイベントをリッスンするリスナーが定義されており、このEventをリッスンすると登録ロジックの送信を開始できます。つまり、NacosServiceRegistryクラスのregisterメソッドで、その基盤はHTTPリクエストを通じてNacosサーバーの登録インターフェースを呼び出すものです

では、Nacos 2.xバージョンでのこのregisterメソッドのロジックを見てみましょう。register全体のロジックはNacos 1.4バージョンと同じであることがわかります。

@Override
public void register(Registration registration) {
​
   if (StringUtils.isEmpty(registration.getServiceId())) {
      log.warn("No service to register for nacos client...");
      return;
   }
​
   NamingService namingService = namingService();
   
   // サービスID、グループを取得
   String serviceId = registration.getServiceId();
   String group = nacosDiscoveryProperties.getGroup();
   // Instanceオブジェクトを作成
   Instance instance = createNacosInstanceFromRegistration(registration);
​
   try {
      // サービス登録を開始、コアメソッド
      namingService.registerInstance(serviceId, group, instance);
      log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
            instance.getIp(), instance.getPort());
   }
   catch (Exception e) {
      if (nacosDiscoveryProperties.isFailFast()) {
         log.error("nacos registry, {} register failed...{},", serviceId,
               registration.toString(), e);
         throwRuntimeException(e);
      }
      else {
         log.warn("Failfast is false. {} register failed...{},", serviceId,
               registration.toString(), e);
      }
   }
}
​

では、さらに下に進みましょう。登録のコアメソッドregisterInstance()メソッドを見てみましょう。2.xバージョンでのregisterInstance()メソッドは1.4バージョンとは異なることがわかります。ここで1.4ののコードを貼り付けて比較してみましょう。

1.4では一時インスタンスかどうかを判断していましたが、明らかに2.xバージョンでは区別判断を行いません

// 2.xバージョン
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    NamingUtils.validateInstance(instance);
    // 登録メソッドを呼び出す
    clientProxy.registerService(serviceName, groupName, instance);
}
​
​
// 1.4.xバージョン
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws  NacosException {
    NamingUtils.validateInstance(instance);
    
    String groupedServiceName = NamingUtils.buildGroupedName(serviceName, groupName);
    
    if (instance.isTemporary()) {
​
        BeatInfo beatInfo = beatReactor.constructBeatInfo(groupedServiceName, instance);
​
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

さらに下に進みましょう。つまりregisterService()メソッドのロジックを見てみましょう。これはインターフェースのメソッドで、3つの実装があります。対応する実装クラスを見つける方法は、私たちが紹介した多くの方法と同じです。主な方法は3つです:

  • 暴力的な方法:直接各実装クラスをすべて見てみる
  • 直接的な方法:対応するサービスを実行し、デバッグで追跡する
  • 観察法:対応する呼び出しメソッドのオブジェクトを見つけ、その初期化方法を確認する

ここではどの方法でも使用できます。以下のように、registerServiceメソッドを呼び出しているのはNacosNamingServiceクラスのclientProxyメンバーであり、このメンバーの初期化は同じクラスのinitメソッドで行われ、initメソッドブロックからその対応する型がNamingClientProxyDelegateであることがわかります

コードをNamingClientProxyDelegateクラスのregisterServiceメソッドまで追跡すると、getExecuteClientProxyメソッドが呼び出されていることがわかります。そして、そのインスタンスが一時インスタンスかどうかを判断し、一時インスタンスであればgrpcClientを返し、そうでなければhttpClientオブジェクトを返します。コードは以下の通りです:

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}
​
​
private NamingClientProxy determineClientProxy(Instance instance) {
    return instance.isTemporary() ? grpcClientProxy : httpClientProxy;
}
​

ここからもNacosが一時インスタンスと非一時インスタンスを異なる方法で処理していることがわかります:登録されるインスタンスが一時インスタンスの場合、gRPCリクエストが使用され、非一時インスタンスの場合は依然としてHTTP方式が使用されます

HTTP方式については詳しく説明しません。以前1.4で分析しましたので、今回は主にgRPCのロジックを見ていきましょう。

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
            instance);
    redoService.cacheInstanceForRedo(serviceName, groupName, instance);
    // サービス登録を実行
    executeServiceRegistration(serviceName, groupName, instance);
}
​
public void executeServiceRegistration(String serviceName, String groupName, Instance instance) throws NacosException {
    // リクエストパラメータオブジェクトを作成
    InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
            NamingRemoteConstants.REGISTER_INSTANCE, instance);
    // サーバーにリクエストを送信
    sendRequestToServer(request, Response.class);
    redoService.markInstanceAsRegistered(serviceName, groupName);
}

次にsendRequestToServerメソッドを見てみましょう。

private <T extends Response> T sendRequestToServer(AbstractNamingRequest request, Class<T> responseClass)
        throws NacosException {
    try {
        request.putAllHeader(
                getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
        Response response =
                requestTimeout < 0 ? rpcClient.sendRequest(request) : rpcClient.sendRequest(request, requestTimeout);
        if (ResponseCode.SUCCESS.getCode() != response.getResultCode()) {
            throw new NacosException(response.getErrorCode(), response.getMessage());
        }
        if (responseClass.isAssignableFrom(response.getClass())) {
            return (T) response;
        }
        NAMING_LOGGER.error("Server return unexpected response '{}', expected response should be '{}'",
                response.getClass().getName(), responseClass.getName());
    } catch (Exception e) {
        throw new NacosException(NacosException.SERVER_ERROR, "Request nacos server failed: ", e);
    }
    throw new NacosException(NacosException.SERVER_ERROR, "Server return invalid response");
}
​

rpcClient.sendRequest(request)を見ると、rpcClientオブジェクトのリクエストメソッドが呼び出されています。ソースコード分析をここまで進めると、NacosがRPCの基盤の上にGrpcClientオブジェクトをラップしていることがわかります。基盤は依然としてRPCの仕組みを使用しています。以下のように**RequestGrpc.RequestFutureStub grpcFutureServiceStubというメンバーの型はgRPCのstubオブジェクトです**

RPC関連の基本的な理論知識を学習していない方は、後で学習してください

タグ: Nacos gRPC Java 微服务 服务发现

5月18日 11:48 投稿