LESSON 30分

ストーリー

あなた
Order Serviceのインスタンスを3つに増やしたんだけど、Payment ServiceからのURLがハードコードで…
高橋アーキテクト
サービスが動的に増減する環境で、IPアドレスをハードコードするのは非現実的だね。サービスディスカバリが必要だ

サービスディスカバリとは

動的にサービスのネットワーク位置(IP・ポート)を発見する仕組みです。

問題: サービスのインスタンスは動的に変化する
  Order Service
    ├─ Instance 1: 10.0.1.5:3000  ← 起動中
    ├─ Instance 2: 10.0.1.8:3000  ← 新規追加
    └─ Instance 3: 10.0.1.3:3000  ← ダウン中

  → Payment ServiceはどのIPに接続すべきか?

クライアントサイドディスカバリ

// クライアントサイドディスカバリ
// サービスが直接レジストリに問い合わせる

class ServiceRegistry {
  private services: Map<string, ServiceInstance[]> = new Map();

  async register(name: string, instance: ServiceInstance): Promise<void> {
    const instances = this.services.get(name) || [];
    instances.push(instance);
    this.services.set(name, instances);
  }

  async discover(name: string): Promise<ServiceInstance[]> {
    return this.services.get(name) || [];
  }

  async deregister(name: string, instanceId: string): Promise<void> {
    const instances = this.services.get(name) || [];
    this.services.set(name, instances.filter(i => i.id !== instanceId));
  }
}

// 利用側
async function callOrderService(orderId: string) {
  const instances = await registry.discover("order-service");
  const healthy = instances.filter(i => i.status === "healthy");
  const target = loadBalancer.select(healthy); // クライアント側でLB
  return fetch(`http://${target.host}:${target.port}/orders/${orderId}`);
}

サーバーサイドディスカバリ

サーバーサイドディスカバリ(ロードバランサー経由):

  Payment    ──→  [Load Balancer]  ──→  Order Instance 1
  Service         (DNS名で解決)   ──→  Order Instance 2
                                   ──→  Order Instance 3

  → クライアントはLBのアドレスだけ知ればよい
  → Kubernetes Service がこの方式

比較

観点クライアントサイドサーバーサイド
ロードバランシングクライアント側で実装LB/プロキシが担当
クライアント複雑度高い(LBロジック必要)低い(DNS名だけ)
ネットワークホップ少ない(直接通信)多い(LB経由)
代表例Netflix Eureka + RibbonKubernetes Service, AWS ALB

サービスレジストリ

サービスの登録情報を管理する中央のデータストアです。

// サービスレジストリのデータ構造
interface ServiceInstance {
  id: string;
  name: string;
  host: string;
  port: number;
  status: "healthy" | "unhealthy" | "draining";
  metadata: {
    version: string;
    region: string;
    weight: number;
  };
  lastHeartbeat: Date;
}

// ヘルスチェックの仕組み
class HealthChecker {
  // 定期的にヘルスチェック(TTLベース)
  async checkHealth(instance: ServiceInstance): Promise<boolean> {
    try {
      const response = await fetch(
        `http://${instance.host}:${instance.port}/health`,
        { signal: AbortSignal.timeout(3000) }
      );
      return response.ok;
    } catch {
      return false;
    }
  }

  // 一定時間ヘルスチェック失敗 → 自動登録解除
  async evictUnhealthy(registry: ServiceRegistry): Promise<void> {
    const allServices = await registry.listAll();
    for (const instance of allServices) {
      const elapsed = Date.now() - instance.lastHeartbeat.getTime();
      if (elapsed > 30_000) { // 30秒間応答なし
        await registry.deregister(instance.name, instance.id);
      }
    }
  }
}

主要なサービスレジストリ

ツール特徴一貫性
Consulヘルスチェック内蔵、KV StoreCP
etcdKubernetes標準、高信頼CP
ZooKeeper実績豊富、複雑CP
EurekaNetflix製、AP型AP
Kubernetes DNSK8s標準、追加インストール不要-

ロードバランシング

複数のサービスインスタンスにトラフィックを分散する仕組みです。

// ロードバランシングアルゴリズム
interface LoadBalancer {
  select(instances: ServiceInstance[]): ServiceInstance;
}

// ラウンドロビン: 順番に振り分け
class RoundRobinLB implements LoadBalancer {
  private index = 0;
  select(instances: ServiceInstance[]): ServiceInstance {
    const instance = instances[this.index % instances.length];
    this.index++;
    return instance;
  }
}

// 重み付きラウンドロビン: 性能に応じて比率を変更
class WeightedRoundRobinLB implements LoadBalancer {
  select(instances: ServiceInstance[]): ServiceInstance {
    const totalWeight = instances.reduce((sum, i) => sum + i.metadata.weight, 0);
    let random = Math.random() * totalWeight;
    for (const instance of instances) {
      random -= instance.metadata.weight;
      if (random <= 0) return instance;
    }
    return instances[0];
  }
}

// 最小接続数: 接続が少ないインスタンスを優先
class LeastConnectionsLB implements LoadBalancer {
  private connections: Map<string, number> = new Map();
  select(instances: ServiceInstance[]): ServiceInstance {
    return instances.reduce((min, inst) => {
      const count = this.connections.get(inst.id) || 0;
      const minCount = this.connections.get(min.id) || 0;
      return count < minCount ? inst : min;
    });
  }
}

アルゴリズム比較

アルゴリズム特徴適用場面
ラウンドロビン均等分散、シンプル同一スペックのインスタンス
重み付きラウンドロビン性能に応じた分散異なるスペック混在
最小接続数負荷に応じた動的分散処理時間が不均一
IPハッシュ同一クライアント→同一サーバーセッション維持

まとめ

ポイント内容
サービスディスカバリ動的にサービスの位置を発見
2つの方式クライアントサイド / サーバーサイド
サービスレジストリ登録情報の中央管理 + ヘルスチェック
ロードバランシングラウンドロビン、重み付き、最小接続数

チェックリスト

  • サービスディスカバリの必要性を説明できる
  • クライアントサイドとサーバーサイドの違いを理解した
  • サービスレジストリの仕組みを説明できる
  • 3つ以上のロードバランシングアルゴリズムを挙げられる

次のステップへ

次は分散設定管理を学びます。数十のサービスの設定をどう一元管理し、安全に変更するかを理解しましょう。


推定読了時間: 30分