目次を表示する

システム設計とCS概念

サービスメッシュ / サービスディスカバリ ── マイクロサービスの通信制御

サービスメッシュ / サービスディスカバリ ── マイクロサービスの通信制御

扱うCS概念:サービスディスカバリ(クライアントサイド / サーバーサイド)、サイドカーパターン、mTLS、Gossip プロトコル、ヘルスチェックの一貫性


サービスメッシュ — サイドカー・ディスカバリ・Gossip

この章で何ができるようになるか:マイクロサービスが「相手のIPアドレスを知らない状態」でどう通信するかを説明できる。サービスメッシュがなぜ必要になり、どんな問題を解決しているかを理解できる。


問題設定

モノリスからマイクロサービスに移行すると、新しい問題が生まれる。

モノリス時代:
  UserService.getUser(id) → 同一プロセス内のメソッド呼び出し
  → アドレス解決不要、暗号化不要、リトライはフレームワーク任せ

マイクロサービス時代:
  user-service はどの IP:port で動いている?
  → 3台のうちどれに送る? 1台が落ちたら?
  → 通信は暗号化されている?
  → タイムアウトとリトライのポリシーは?
  → 各サービスのトラフィック量の可視化は?

これらの「横断的関心事(cross-cutting concerns)」を各サービスに個別実装するのは現実的でない。


サービスディスカバリ:相手をどう見つけるか

方式1:クライアントサイドディスカバリ

graph LR
    Client[Order Service] -->|1. 問い合わせ| Registry[(サービスレジストリ<br/>Consul / etcd)]
    Registry -->|2. user-svc の IP一覧| Client
    Client -->|3. LB して直接通信| US1[user-svc :8080]
    Client -->|3.| US2[user-svc :8081]
# Consul を使ったクライアントサイドディスカバリ
import consul
import random

class ServiceDiscovery:
    def __init__(self):
        self.consul = consul.Consul(host='consul-server', port=8500)
        self._cache: dict[str, list] = {}  # ローカルキャッシュ

    def get_instance(self, service_name: str) -> str:
        """サービス名からインスタンスのアドレスを返す"""
        # キャッシュを確認(TTL 30秒)
        if service_name in self._cache:
            instances = self._cache[service_name]
        else:
            _, instances = self.consul.health.service(
                service_name, passing=True  # ヘルスチェックが通っているものだけ
            )
            self._cache[service_name] = instances
            # 30秒後にキャッシュ無効化
            threading.Timer(30, lambda: self._cache.pop(service_name, None)).start()

        if not instances:
            raise ServiceUnavailableError(f"No healthy instances of {service_name}")

        # クライアント側でロードバランシング
        instance = random.choice(instances)
        addr = instance['Service']['Address']
        port = instance['Service']['Port']
        return f"http://{addr}:{port}"

# 使用例
discovery = ServiceDiscovery()
user_svc_url = discovery.get_instance("user-service")
response = requests.get(f"{user_svc_url}/users/123")

方式2:サーバーサイドディスカバリ

graph LR
    Client[Order Service] -->|1. user-service/users/123| LB[ロードバランサー / DNS]
    LB -->|2. レジストリ参照| Registry[(レジストリ)]
    LB -->|3. ルーティング| US1[user-svc]
    LB -->|3.| US2[user-svc]
  • AWS ALB + ECS:ECS がサービスを登録、ALB がルーティング
  • Kubernetes Service:kube-dns が user-service.default.svc.cluster.local を解決
# Kubernetes の Service 定義
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP  # クラスタ内DNSで解決
---
# → curl http://user-service/users/123 で到達可能
# → kube-proxy がバックエンドの Pod に LB

2方式の比較

クライアントサイドサーバーサイド
LB のカスタマイズ性✅ 高い(アプリ側で制御)△ LB の機能に依存
クライアントの複雑さ❌ ディスカバリライブラリが必要✅ DNS/LB に任せてシンプル
障害点レジストリレジストリ + LB
代表例Consul + Envoy, Netflix EurekaK8s Service, AWS ALB

Gossip プロトコル:メンバーシップ管理

Consul や Cassandra は Gossip プロトコルでクラスタのメンバーシップを管理する。

Gossip のアイデア(噂話プロトコル):
  1. 各ノードが定期的にランダムな相手に「自分が知っている状態」を送る
  2. 受信者は自分の情報と統合して更新する
  3. 繰り返すと、全ノードが最終的に同じ情報を持つ(結果整合性)

特性:
  - 全ノードが全ノードと通信する必要がない(O(log N) ラウンドで収束、総メッセージ数は O(N log N))
  - 中央のコーディネーターが不要(分散型)
  - 障害ノードの検知も Gossip で伝播する
class GossipNode:
    def __init__(self, node_id: str, peers: list[str]):
        self.node_id = node_id
        self.peers = peers
        # 各ノードの状態:{node_id: (status, version)}
        self.membership: dict[str, tuple[str, int]] = {
            node_id: ("alive", 0)
        }

    def gossip_round(self):
        """定期的に実行:ランダムなピアに状態を送信"""
        target = random.choice(self.peers)
        remote_state = self.send_state(target, self.membership)

        # 受信した状態とマージ(バージョンが高い方を採用)
        for nid, (status, version) in remote_state.items():
            local = self.membership.get(nid)
            if local is None or version > local[1]:
                self.membership[nid] = (status, version)

    def mark_suspicious(self, node_id: str):
        """ハートビート未応答 → suspicious → 一定時間後に dead"""
        current = self.membership.get(node_id)
        if current:
            self.membership[node_id] = ("suspicious", current[1] + 1)

サービスメッシュ:サイドカーパターン

サービスディスカバリ、LB、mTLS、リトライ、サーキットブレーカー、メトリクス収集……これらを各サービスに実装するのではなく、サイドカープロキシに任せるのがサービスメッシュだ。

graph LR
    subgraph Pod A
        AppA[Order Service] <--> ProxyA[Envoy Proxy<br/>サイドカー]
    end
    subgraph Pod B
        ProxyB[Envoy Proxy<br/>サイドカー] <--> AppB[User Service]
    end
    ProxyA -->|mTLS| ProxyB
    ControlPlane[Istio Control Plane] -.->|設定配信| ProxyA
    ControlPlane -.->|設定配信| ProxyB

サイドカーが担う責務

1. サービスディスカバリ → 相手の IP:port を解決
2. ロードバランシング → 複数インスタンスに分散
3. mTLS → サービス間通信を暗号化(ゼロトラスト)
4. リトライ / タイムアウト → 失敗時の自動リトライ
5. サーキットブレーカー → 障害伝播の防止
6. メトリクス / トレーシング → リクエスト量・レイテンシの収集
7. トラフィック制御 → カナリアデプロイ(5%だけ新バージョンへ)

アプリケーションは「localhost にリクエストを送る」だけ。横断的関心事は全てサイドカーが処理する。

# アプリケーション側のコード(メッシュ導入前)
response = requests.get(
    "https://user-service.prod:8443/users/123",
    headers={"Authorization": f"Bearer {token}"},
    timeout=5,
    verify="/certs/ca.pem"  # TLS 証明書の検証
)

# アプリケーション側のコード(メッシュ導入後)
response = requests.get("http://localhost:8080/users/123")
# → Envoy サイドカーが:
#    1. user-service の実アドレスを解決
#    2. mTLS で暗号化
#    3. LB して送信
#    4. リトライ・タイムアウト・メトリクス収集

Istio のトラフィック制御

# Istio VirtualService: カナリアデプロイ
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: user-service
spec:
  hosts:
    - user-service
  http:
    - match:
        - headers:
            x-canary:
              exact: "true"
      route:
        - destination:
            host: user-service
            subset: v2  # 新バージョン
    - route:
        - destination:
            host: user-service
            subset: v1  # 安定版
          weight: 95
        - destination:
            host: user-service
            subset: v2  # 新バージョン
          weight: 5

サービスメッシュが必要なとき / 不要なとき

必要なケース:
  - マイクロサービスが 20 以上(横断的関心事を個別管理できない)
  - マルチ言語環境(Go, Python, Java が混在し、共通ライブラリが作れない)
  - ゼロトラストセキュリティ(mTLS を全通信に必須)
  - 細かなトラフィック制御(A/Bテスト、カナリア、フォールトインジェクション)

不要なケース:
  - サービスが 5 つ以下(オーバーヘッドの方が大きい)
  - 単一言語(共通ライブラリで横断的関心事を解決できる)
  - レイテンシに極端にシビア(サイドカーの追加レイテンシが問題になる)

まとめ

課題解決策CS概念
サービスの IP を知らないサービスディスカバリ(Consul/K8s DNS)名前解決・レジストリ
障害ノードの検知Gossip プロトコル結果整合性・噂話伝播
横断的関心事の重複サイドカーパターン(Envoy)プロキシ・関心の分離
サービス間暗号化mTLS(自動証明書ローテーション)公開鍵暗号基盤
カナリアデプロイトラフィック重み付けルーティング段階的ロールアウト