マイクロサービスアーキテクチャにおける同期通信メカニズムの詳細分析

マイクロサービスアーキテクチャ概要

マイクロサービスアーキテクチャは、アプリケーションを一連の小さなサービスに分割する設計手法で、各サービスは独立したプロセスで実行され、通常はビジネス能力に基づいて組織化されます。これらのサービスは、多様な通信方法を介して相互作用し、全体のアプリケーション機能を実現します。本稿では同期通信に焦点を当て、非同期通信やメッセージキュー(MQ)などの内容は後日解説します。

ここで言う通信とは、クライアント内部でのサービス間通信を指し、外部のWebサービスを呼び出すアクセスではありません。それでは、本題に入りましょう。

ロードバランシング

サーバーサイドロードバランシング

クライアントサイドロードバランシングを深く探求する前に、まずnginxコンポーネントのようなサーバーサイドロードバランシングについてより深く理解する必要があります。nginxは通常、サーバー内部で負荷転送リクエストに使用され、クライアントは各サービス内部で負荷分散を行います。

クライアントサイドロードバランシング

Spring Cloudでは、Ribbonを使用する場合、クライアントはサーバーアドレスリストを維持し、リクエストを送信する前にロードバランシングアルゴリズムを使用してアクセスするサーバーを選択します。この方式はクライアントサイドロードバランシングと呼ばれ、クライアント内部でロードバランシングアルゴリズムの割り当て作業が完了するためです。

同期通信

通常、HTTPリクエストを行う際には様々なツールクラスを利用します。以前は独自にHttpUtilsなどのクラスをカプセル化したり、公式が提供する他のツールを使用したりした経験があるでしょう。例えば、本稿ではRestTemplateツールクラスについて解説します。これはHTTPリクエストを送信するためのツールですが、その上で追加の作業を行っています。まず基本的な使い方を見てみましょう。

RestTemplate

RestTemplateはSpringフレームワークが提供する強力なクラスで、同期クライアントのHTTPアクセスに特化しています。その設計は、HTTPクライアントを使用してREST呼び出しを行う複雑なプロセスを簡素化することを目指し、様々なHTTPリクエストとレスポンスを処理するための豊富なメソッドと機能を提供します。

RestTemplateインスタンスの作成

まず、RestTemplateのインスタンスを作成する必要があります。これは直接インスタンス化するか、Springの自動アセンブリを使用して行うことができます。

import org.springframework.web.client.RestTemplate;

RestTemplate restTemplate = new RestTemplate();

リクエストの送信

RestTemplateを使用してGETリクエストを送信し、レスポンスボディを取得します。

String url = "http://example.com/api/resource";
String result = restTemplate.getForObject(url, String.class);
System.out.println(result);

POSTリクエストを送信する場合、通常はリクエストボディを含みます。

String url = "http://example.com/api/resource";
Map<String, Object> requestMap = new HashMap<>();
requestMap.put("key1", "value1");
requestMap.put("key2", "value2");

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestMap, headers);
String response = restTemplate.postForObject(url, entity, String.class);
System.out.println(response);

ここで思うかもしれませんが、これはマイクロサービス間の通信と何の関係があるのでしょう。単なるHTTP呼び出しに過ぎないのでは?さらに深く探求してみましょう。

@LoadBalancedアノテーション

マイクロサービスアーキテクチャでは通常、レジストリセンターが関わってきます。本稿ではマイクロサービス間の通信に焦点を当て、Nacosがすべてのマイクロサービスの登録をどのように管理し、あるサービスノードが他のサービスノードをどのように発見するかなどのレジストリセンターについては深く解説しません。すでに他のサービスノードのIPアドレスを取得したと仮定すると、上記の例のドメインをIPアドレスに置き換えることを考えるかもしれませんが、高可用性を保証するマルチノードのマイクロサービスに対して、コードにIPアドレスをハードコーディングすると災害的な結果をもたらします。すべてのIPアドレスは、統一されたコンポーネントによって管理・選択されるべきです。

そのため、Ribbonが登場しました。プロジェクトのpomファイルにNacos依存関係が統合されている場合、通常はデフォルトでRibbonコンポーネントが含まれるため、Ribbon依存関係を導入するためにpomファイルを個別に構成する必要はありません。

基本的な使い方

前述のように、1つの方法は直接インスタンス化すること、もう1つはSpringコンテナを通じて管理・注入することです。

@Configuration
public class RestConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

これにより、使用が必要な場合に簡単にサービス呼び出し操作を行うことができます。

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value = "/findOrderByUserId/{id}")
    public R findOrderByUserId(@PathVariable("id") Integer id) {
        String url = "http://mall-order/order/findOrderByUserId/"+id;
        R result = restTemplate.getForObject(url,R.class);
        return result;
    }
}

注意点として、mall-orderは私たちが言及しているドメイン名ではなく、Nacosなどのレジストリセンターに設定されたサービス名です。

@LoadBalancedアノテーションを追加すると、RestTemplateは必要な依存関係を自動的に注入します。具体的な実装はソースコードを確認することで理解できます。

ソースコード分析

Springの自動構成については、以前Springを解説する際に何度も言及しましたので、ここでは詳しく展開しません。SmartInitializingSingletonインターフェースを実装していることがわかります。そのため、ロードバランシング機能を使用するには、すべてのBeanがロードされるのを待つ必要があります。

ここでは、RestTemplateクラスにインターセプターを追加していることがわかります。次に、このインターセプターが具体的に何を行うのか探求してみましょう。段階的にプロセス全体を示さないのは、それが不要なためです。まず、覚えきれないからです。次に、業務処理と同じように、どのデータテーブルにたどり着いたかだけに注目すればよいのです。そうすることで、脳の負担を軽減できます。

public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
        final ClientHttpRequestExecution execution) throws IOException {
    final URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();
    Assert.state(serviceName != null,
            "Request URI does not contain a valid hostname: " + originalUri);
    return this.loadBalancer.execute(serviceName,
            this.requestFactory.createRequest(request, body, execution));
}

実際にここまで見れば、推測する必要はありません。serviceNameつまり私たちが以前定義したマイクロサービス名を取得すると、executeメソッド内で実際のIPアドレスに置き換えられ、最終的にHTTPリクエストが完了するまでのプロセスが実行されます。ソースコードを詳しく見てみましょう。

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
        throws IOException {
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    Server server = getServer(loadBalancer, hint);
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }
    RibbonServer ribbonServer = new RibbonServer(serviceId, server,
            isSecure(server, serviceId),
            serverIntrospector(serviceId).getMetadata(server));

    return execute(serviceId, ribbonServer, request);
}

すべてのロードバランシングルールはgetServerメソッドで実装されており、ここでは深く追跡しません。このステップで、呼び出すべきマイクロサービスが見つかります。まだ疑問がある場合は、Serverクラスの具体的な実装を見れば理解できます。

最終的にはHTTP呼び出しが渡されます。しかし、これだけでは不十分です。各サービスにこのような本質的でないサービス呼び出しコードを記述したいですか?それは非常に煩雑で、ビジネスロジックの複雑さを増し、不便に感じさせます。幸いなことに、Spring Cloud OpenFeignのおかげで、すべてがずっと簡単になりました。OpenFeignはまさにこのようなサービス間通信の問題を解決するために登場し、これらの煩雑な詳細をカプセル化しました。

しかし、このカプセル化が通信の実質に影響することはありません。次回はSpring Cloud OpenFeignとDubbo呼び出しコンポーネントの違いと使用方法について詳しく説明します。

まとめ

本日、マイクロサービス間のネットワーク通信に焦点を当てました。フレームワークの最終目標が、プログラマーがビジネスロジックにより集中できるようにし、様々な本質的でないコードを書くことを強制しないものであることが明確に見えます。まとめると、フレームワークや様々な抽象化を使用していても、最終的にはHTTPを介して呼び出しを行っているということです。違いは、実際の呼び出しの前に、マイクロサービスのロードバランシングを実現するインターセプターを導入している点です。このインターセプターには様々な均衡アルゴリズムが実装されており、最終的にアクセスして必要なデータを取得するための実際のIPアドレスとポートが確定します。

タグ: マイクロサービス 同期通信 RestTemplate ロードバランancing Spring Cloud

5月15日 20:06 投稿