マイクロサービスディスカバリのナビゲーション:クライアントサイドとサーバーサイドのパターンの解明
Lukas Schneider
DevOps Engineer · Leapcell

はじめに
マイクロサービスアーキテクチャスタイルは、その柔軟性、スケーラビリティ、回復力により、絶大な人気を博しています。しかし、この分散型ネイチャーは新たな複雑さを導入しており、その中でも最も根本的なものの1つが、サービスが互いをどのように見つけ、通信するかという問題です。サービスインスタンスが常に起動、スケールアウト、または終了される動的な環境では、ネットワークの場所をハードコーディングすることは現実的ではありません。この課題は、サービスディスカバリの概念、つまり、サービスが他のサービスの利用可能なインスタンスを見つけることを可能にする重要なメカニズムを生み出しました。クライアントサイドとサーバーサイドのサービスディスカバリパターンのニュアンスを理解することは、堅牢で保守可能なマイクロサービスエコシステムを構築するために不可欠です。この記事では、これらの2つの主要なアプローチを詳細に比較し、その基盤となるメカニズム、実際の実装、および適切なシナリオを検討し、最終的に開発者が情報に基づいたアーキテクチャ上の意思決定を行えるようにすることを目指します。
サービスディスカバリパターンの解読
クライアントサイドとサーバーサイドのサービスディスカバリの具体例に入る前に、議論で頻繁に登場するいくつかの必須用語を明確にしましょう。
- サービスレジストリ: 利用可能なすべてのサービスインスタンスのネットワーク場所(IPアドレスとポート)を保存する中央データベースまたはリポジトリ。サービスは起動時に自身を登録し、シャットダウン時に登録解除します。
- サービスインスタンス: 特定のサービスの実行中のプロセス。ネットワークアドレスと多くの場合一意のIDによって識別されます。
- サービスプロバイダー: 他のサービスにAPIまたは機能を提供するサービス。
- サービスコンシューマー(クライアント): サービスプロバイダーが提供する機能を利用する必要があるサービス。
これらの定義を踏まえて、2つの主要なサービスディスカバリパターンを探ってみましょう。
クライアントサイドサービスディスカバリ
クライアントサイドサービスディスカバリパターンでは、クライアント(サービスコンシューマー)がサービスレジストリをクエリして、通信したいサービスの利用可能なインスタンスを見つける責任を負います。ネットワークの場所を取得したら、クライアントはロードバランシングアルゴリズムを使用して利用可能なインスタンスの1つを選択し、直接リクエストを行います。
仕組み:
- サービス登録: サービスプロバイダーインスタンスが起動すると、そのネットワーク場所(IPアドレス、ポート)をサービスレジストリに登録します。多くの場合、その健全性と可用性を示すために定期的なハートビートを送信します。
- サービスディスカバリ: サービスコンシューマーがサービスプロバイダーを呼び出す必要がある場合、そのサービスの利用可能なすべてのインスタンスのサービスレジストリをクエリします。
- ロードバランシング: サービスコンシューマーは、組み込みまたは外部のロードバランサー(またはカスタムアルゴリズム)を使用して、正常なサービスインスタンスのリストから1つを選択します。
- 直接通信: サービスコンシューマーは、選択されたサービスインスタンスと直接通信します。
実装例:
クライアントサイドサービスディスカバリの一般的な実装は、サービスレジストリとしてNetflix Eureka、クライアントサイドロードバランサーとしてNetflix Ribbonを使用します。
ProductServiceがOrderServiceを呼び出す必要があると仮定しましょう。
OrderService(サービスプロバイダー):
// OrderService の Spring Boot アプリケーション @SpringBootApplication @EnableEurekaClient // サービス登録のために Eureka クライアントを有効にする public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } @RestController class OrderController { @GetMapping("/orders/{id}") public String getOrder(@PathVariable Long id) { return "Order details for ID: " + id; } } }
ProductService(サービスコンシューマー):
// ProductService の Spring Boot アプリケーション @SpringBootApplication @EnableEurekaClient // サービスディスカバリのために Eureka クライアントを有効にする public class ProductServiceApplication { public static void main(String[] args) { SpringApplication.run(ProductServiceApplication.class, args); } @RestController class ProductController { // クライアントサイドのロードバランシングのために Ribbon を使用した Spring Cloud の RestTemplate を使用 // @LoadBalanced アノテーションは RestTemplate を Ribbon を認識させる private final RestTemplate restTemplate; public ProductController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @GetMapping("/products/{productId}/order-info") public String getProductOrderInfo(@PathVariable Long productId) { // "ORDER-SERVICE" は Eureka に登録された論理サービス名です String orderInfo = restTemplate.getForObject("`http://ORDER-SERVICE/orders/`" + productId, String.class); return "Product " + productId + " order details: " + orderInfo; } } @Bean @LoadBalanced // Ribbon 統合に不可欠 public RestTemplate restTemplate() { return new RestTemplate(); } }
この例では:
OrderServiceは自身をEurekaに登録します。ProductServiceは@LoadBalanced RestTemplateを使用します。restTemplate.getForObject("http://ORDER-SERVICE/...")が呼び出されると、Ribbonはリクエストをインターセプトし、Eurekaに"ORDER-SERVICE"のインスタンスをクエリし、1つを選択し、URLを実際のIPとポートに書き換えます。
利点:
- シンプルなネットワークトポロジー: クライアントはインスタンスと直接通信するため、追加のホップが回避されます。
- 費用対効果の高いロードバランシング: クライアントサイドライブラリを活用することで、専用のハードウェアロードバランサーよりも経済的になる可能性があります。
- より柔軟なロードバランシングルール: クライアントサイドライブラリは、コンシューマーのニーズに合わせた洗練されたロードバランシングアルゴリズムを許可することがよくあります。
- レイテンシの削減: 直接通信は、追加のホップをプロキシ経由で行う場合と比較して、レイテンシが低くなる可能性があります。
欠点:
- 言語のカップリング: サービスディスカバリロジック(およびロードバランシング)は、すべてのクライアントアプリケーションに実装または統合する必要があり、異なるプログラミング言語にわたる可能性があります。
- クライアントの複雑さの増加: クライアントは、ディスカバリ、ロードバランシング、および場合によってはサーキットブレーキングを管理する必要があるため、より複雑になります。
- 更新が困難: ディスカバリメカニズムへの変更には、すべてのクライアントサービスの更新と再デプロイが必要です。
アプリケーションシナリオ:
- クライアントテクノロジーの数が限られている環境(例:主にJavaサービスがSpring Cloudを使用)。
- クライアントによるロードバランシングのきめ細かな制御が必要な場合。
- 専用ロードバランサーのインフラストラクチャコストが大きな懸念事項である場合。
サーバーサイドサービスディスカバリ
サーバーサイドサービスディスカバリパターンでは、クライアント(サービスコンシューマー)は、よく知られたURLのプロキシ(APIゲートウェイまたは専用ロードバランサーであることが多い)にリクエストを送信します。このプロキシは、サービスレジストリをクエリし、利用可能なインスタンスを選択し、そのインスタンスにリクエストをルーティングする責任を負います。クライアントは、サービス登録およびロードバランシングの詳細を認識しません。
仕組み:
- サービス登録: クライアントサイドと同様に、サービスプロバイダーインスタンスはネットワーク場所をサービスレジストリに登録します。
- リクエストルーティング: サービスコンシューマーがサービスを呼び出す必要がある場合、プロキシ(例:ロードバランサー、APIゲートウェイ)のよく知られたエンドポイントにリクエストを送信します。
- プロキシによるディスカバリ: プロキシはサービスレジストリをクエリして、ターゲットサービスの利用可能なインスタンスを見つけます。
- ロードバランシングと転送: プロキシはロードバランシングアルゴリズムを使用して正常なサービスインスタンスを選択し、クライアントのリクエストをそれに転送します。
- 応答: サービスインスタンスからの応答がプロキシ経由でクライアントに返されます。
実装例:
一般的な実装には、Consulやetcdのようなサービスレジストリとともに、**ロードバランサー(AWS ELB/ALB、Nginx、またはKubernetes Ingressなど)**の使用が含まれます。Kubernetesの場合、内部DNSベースのサービスディスカバリは、サーバーサイドディスカバリの主要な例です。
Spring Cloud GatewayのようなAPIゲートウェイまたはリバースプロキシとの統合を考えてみましょう。
OrderService(サービスプロバイダー):
// OrderService の Spring Boot アプリケーション @SpringBootApplication public class OrderServiceApplication { // Consulなどの外部ディスカバリを使用する場合、サービス自体に@EnableEurekaClientは不要 public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } @RestController class OrderController { @GetMapping("/orders/{id}") public String getOrder(@PathVariable Long id) { return "Order details for ID: " + id + " from instance: " + System.getenv("HOSTNAME"); // または一意の識別子 } } }
注意:Kubernetesのような真のサーバーサイドディスカバリシナリオでは、サービス自体は明示的なディスカバリクライアントアノテーションを必要としないことがよくあります。単にポートを公開するだけです。
APIゲートウェイ(サーバーサイドディスカバラ―/ルーター):
// Spring Cloud Gateway アプリケーション @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { // "order-service" が DNS またはゲートウェイと統合されたサービスレジストリ経由で解決可能であると仮定 return builder.routes() .route("order_route", r -> r.path("/api/orders/**") .uri("lb://ORDER-SERVICE")) // "ORDER-SERVICE" は通常、Eureka/Consulのようなレジストリに登録されています .build(); } }
この例では:
OrderServiceは単に実行され、エンドポイントを公開します。GatewayApplicationはサーバーサイドディスカバリとして機能します。/api/orders/**にリクエストが来ると、ゲートウェイは内部ルーティングメカニズム(多くの場合、サービスレジストリまたはKubernetes DNSと統合されている)を使用してORDER-SERVICEを実際のインスタンスに解決し、リクエストを転送します。ProductService(クライアント)は、ディスカバリロジックを必要とせずに、単純にhttp://gateway-host/api/orders/{id}を呼び出すことになります。
利点:
- クライアントの疎結合: クライアントはディスカバリプロセスを全く認識しません。単にプロキシにリクエストを送信するだけです。
- 言語非依存: ディスカバリロジックはプロキシに reside するため、任意の言語で書かれたクライアントとシームレスに連携します。
- 一元化された制御: サービスディスカバリ、ロードバランシング、ルーティングの管理は、1つの場所で中央化されます。
- 更新が容易: サービスディスカバリロジックまたはロードバランシングアルゴリズムの変更は、各クライアントではなく、プロキシの更新のみで済みます。
- 強化されたセキュリティ: プロキシは、セキュリティポリシー、レート制限、その他のクロスコーティングの懸念事項の強制ポイントとして機能できます。
欠点:
- 追加のネットワークホップ: すべてのリクエストはプロキシを通過するため、追加のレイテンシホップが発生します。
- 単一障害点(適切に管理されていない場合): プロキシ自体が、高可用性とスケーラビリティが確保されていない場合、ボトルネックまたは単一障害点になる可能性があります。
- インフラストラクチャの複雑さの増加: 専用プロキシレイヤーのデプロイと管理が必要です。
- コスト: プロキシインフラストラクチャとメンテナンスに追加のコストがかかる可能性があります。
アプリケーションシナリオ:
- 多様なクライアントテクノロジー(ポリグロット環境)を持つマイクロサービスアーキテクチャ。
- ルーティング、セキュリティ、クロスコーティングの懸念事項に対する一元化された制御が必要な場合。
- APIゲートウェイが自然に存在する公開API。
- 内部DNSベースのサービスディスカバリやIngressコントローラーが本質的にこの機能を提供するKubernetesのような環境。
結論
クライアントサイドとサーバーサイドのサービスディスカバリパターンは、動的なマイクロサービス環境でのサービスの位置特定の問題を効果的に解決しますが、ディスカバリロジックがどこに reside するかで根本的に異なります。クライアントサイドディスカバリはコンシューマーに責任を負わせ、クライアントサイドの複雑さとカップリングの増加を犠牲にして、柔軟性と潜在的なレイテンシの削減を提供します。サーバーサイドディスカバリは、プロキシにディスカバリとルーティングを一元化し、強力な疎結合、言語独立性、および一元化された制御を提供しますが、追加のネットワークホップとインフラストラクチャの複雑さの増加を犠牲にしています。最適な選択は、特定のアーキテクチャのニーズ、開発チームの専門知識、テクノロジースタック、および運用上の考慮事項にかかっています。最終的に、両方のパターンは、堅牢でスケーラブルなマイクロサービスアーキテクチャの重要なイネーブラーです。

