ストーリー
サービスディスカバリとは
サービスディスカバリとは、動的に変化するサービスのネットワーク位置(IPアドレス・ポート)を自動的に解決する仕組みです。
なぜサービスディスカバリが必要か
| 課題 | 説明 |
|---|---|
| 動的なIPアドレス | コンテナは起動のたびにIPが変わる |
| スケーリング | オートスケールでインスタンス数が動的に増減 |
| 障害復旧 | 障害インスタンスの自動検知と除外 |
| デプロイ | ローリングデプロイ中にインスタンスが入れ替わる |
graph LR
subgraph "従来の方法(静的設定)"
O1["order-service"] -->|"192.168.1.100:8080<br/>← IPが変わったら?"| P1["payment-service"]
end
subgraph "サービスディスカバリ"
O2["order-service"] -->|"payment-service"| D["ディスカバリ<br/>(動的に解決)"]
D --> PA["192.168.1.105:8080"]
D --> PB["192.168.1.106:8080"]
D --> PC["192.168.1.107:8080"]
end
サービスディスカバリのパターン
1. クライアントサイドディスカバリ
クライアント(呼び出し元)がサービスレジストリに問い合わせて宛先を決定するパターンです。
graph TD
OS["Order Service<br/>(クライアント)"] -->|"① 問い合わせ"| SR["Service Registry<br/>(Consul, Eureka)<br/>payment-service:<br/>10.0.0.5:8080<br/>10.0.0.6:8080<br/>10.0.0.7:8080"]
OS -->|"② クライアントがLBアルゴリズムで選択"| PS["Payment Svc<br/>(10.0.0.6)<br/>10.0.0.6:8080 を選択"]
利点: ロードバランシングのカスタマイズが容易 欠点: 各クライアントにディスカバリロジックが必要
2. サーバーサイドディスカバリ
ロードバランサーがクライアントの代わりにサービスレジストリに問い合わせるパターンです。
graph LR
OS["Order Service<br/>(クライアント)"] --> LB["Load Balancer<br/>(Nginx, ALB)"]
LB --> PS["Payment Svc<br/>(10.0.0.6)"]
LB -.->|"問い合わせ"| SR["Service Registry"]
利点: クライアントはシンプルに保てる 欠点: ロードバランサーが単一障害点になる可能性
3. DNSベースディスカバリ
**DNS(Domain Name System)**を使ってサービスのアドレスを解決するパターンです。Kubernetesのデフォルトの仕組みです。
graph TD
OS["Order Service<br/>DNS query: payment-service.production.svc.cluster.local"]
DNS["CoreDNS (Kubernetes)<br/>→ 10.96.45.123 (ClusterIP)"]
KP["kube-proxy<br/>iptables/IPVS で実際のPodにルーティング<br/>10.0.0.5:8080, 10.0.0.6:8080, 10.0.0.7:8080"]
OS --> DNS --> KP
利点: アプリケーションコードの変更が不要、標準プロトコル 欠点: DNSキャッシュのTTL問題、L7レベルのLBが困難
サービスレジストリ
サービスレジストリは、サービスインスタンスの登録と検索を管理する中央データベースです。
主要なサービスレジストリ
| レジストリ | 提供元 | 特徴 |
|---|---|---|
| Consul | HashiCorp | ヘルスチェック内蔵、KVストア、マルチDC |
| Eureka | Netflix/Spring | Spring Cloud統合、REST API |
| etcd | CNCF | Kubernetes内部で使用、強整合性 |
| ZooKeeper | Apache | 分散協調サービス、古典的 |
サービス登録の仕組み
// 自己登録パターン(Self-Registration)
class ServiceRegistration {
private registryClient: ConsulClient;
private heartbeatInterval: NodeJS.Timer | null = null;
constructor(
private serviceName: string,
private servicePort: number,
private serviceHost: string
) {
this.registryClient = new ConsulClient();
}
async register(): Promise<void> {
await this.registryClient.agent.service.register({
name: this.serviceName,
address: this.serviceHost,
port: this.servicePort,
check: {
http: `http://${this.serviceHost}:${this.servicePort}/health`,
interval: '10s', // 10秒ごとにヘルスチェック
timeout: '5s',
deregisterCriticalServiceAfter: '30s', // 30秒間応答なしで登録解除
},
});
// ハートビートの送信(自己申告型ヘルスチェック)
this.heartbeatInterval = setInterval(async () => {
await this.registryClient.agent.check.pass({
id: `service:${this.serviceName}`,
});
}, 5000);
}
async deregister(): Promise<void> {
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
await this.registryClient.agent.service.deregister(this.serviceName);
}
}
ヘルスチェック
サービスディスカバリにおいて、ヘルスチェックは不健全なインスタンスを検知してトラフィックから除外するための重要な仕組みです。
ヘルスチェックの種類
| 種類 | 方式 | 説明 |
|---|---|---|
| アクティブ | プル型 | 定期的にヘルスエンドポイントをポーリング |
| パッシブ | 観測型 | 実際のトラフィックのエラー率を監視 |
| ハートビート | プッシュ型 | サービスが定期的に生存を通知 |
Kubernetesのヘルスチェック
apiVersion: v1
kind: Pod
metadata:
name: order-service
spec:
containers:
- name: order-service
image: myapp/order-service:v1.0
ports:
- containerPort: 8080
# Liveness Probe: コンテナが生きているかを確認
# 失敗するとコンテナが再起動される
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 15 # 起動後15秒待ってから開始
periodSeconds: 10 # 10秒間隔
failureThreshold: 3 # 3回連続失敗で再起動
# Readiness Probe: トラフィックを受け入れ可能か確認
# 失敗するとServiceのエンドポイントから除外される
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
# Startup Probe: 起動完了を確認
# 起動が遅いアプリケーション向け
startupProbe:
httpGet:
path: /health/started
port: 8080
failureThreshold: 30 # 30回まで待つ
periodSeconds: 10 # 10秒間隔 = 最大300秒
// ヘルスチェックエンドポイントの実装
// /health/live: プロセスが正常か(デッドロックしていないか)
app.get('/health/live', (req, res) => {
// イベントループが動いていれば200を返す
res.status(200).json({ status: 'alive' });
});
// /health/ready: リクエストを処理できる状態か
app.get('/health/ready', async (req, res) => {
const dbHealthy = await checkDatabaseConnection();
const cacheHealthy = await checkRedisConnection();
if (dbHealthy && cacheHealthy) {
res.status(200).json({ status: 'ready' });
} else {
res.status(503).json({
status: 'not ready',
checks: { database: dbHealthy, cache: cacheHealthy },
});
}
});
// /health/started: 初期化が完了したか
app.get('/health/started', (req, res) => {
if (appInitialized) {
res.status(200).json({ status: 'started' });
} else {
res.status(503).json({ status: 'starting' });
}
});
ロードバランシングアルゴリズム
主要なアルゴリズム比較
| アルゴリズム | 説明 | 適したケース |
|---|---|---|
| ラウンドロビン | 順番にリクエストを分配 | インスタンスのスペックが均一 |
| 重み付きラウンドロビン | 重みに応じて分配比率を変更 | インスタンスのスペックが不均一 |
| 最小接続数 | 接続数が最も少ないインスタンスに送信 | 処理時間にばらつきがある場合 |
| ランダム | ランダムに選択 | シンプルで十分な場合 |
| ハッシュベース | リクエスト属性のハッシュ値で振り分け | セッション親和性が必要な場合 |
| P2C(2択の力) | ランダムに2つ選び、負荷が低い方に送信 | 大規模クラスタ |
graph LR
subgraph ラウンドロビン
R1["Request 1"] --> IA1["Instance A"]
R2["Request 2"] --> IB1["Instance B"]
R3["Request 3"] --> IC1["Instance C"]
R4["Request 4"] --> IA1
R5["Request 5"] --> IB1
end
graph LR
subgraph "最小接続数"
IA2["Instance A<br/>接続数: 5"]
IB2["Instance B<br/>接続数: 2"]
IC2["Instance C<br/>接続数: 8"]
REQ["新規リクエスト"] -->|"← ここに送信"| IB2
end
graph LR
subgraph "ハッシュベース(セッション親和性)"
U1["user-123"] -->|"hash"| HA["Instance A"]
U2["user-456"] -->|"hash"| HC["Instance C"]
U3["user-789"] -->|"hash"| HB["Instance B"]
end
Istioでのロードバランシング設定
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
trafficPolicy:
loadBalancer:
# シンプルなアルゴリズム
simple: LEAST_REQUEST # ROUND_ROBIN | LEAST_REQUEST | RANDOM | PASSTHROUGH
---
# ハッシュベース(セッション親和性)
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: session-service
spec:
host: session-service
trafficPolicy:
loadBalancer:
consistentHash:
httpHeaderName: "x-user-id" # ユーザーIDでハッシュ
# 他の選択肢:
# httpCookie:
# name: "session-id"
# ttl: 3600s
# useSourceIp: true
クライアントサイドロードバランシング
gRPCのロードバランシング課題
gRPCはHTTP/2の長期接続を使用するため、従来のロードバランサーでは接続単位でしか分散できず、リクエスト単位の分散ができないという課題があります。
graph LR
subgraph "問題: L4ロードバランシング"
C1["Client"] --> L4["L4 LB"]
L4 -->|"全リクエストが偏る"| A1["Instance A"]
L4 -.-> B1["Instance B"]
L4 -.-> C1a["Instance C"]
end
graph LR
subgraph "解決策1: L7ロードバランシング"
C2["Client"] --> L7["L7 LB (Envoy)"]
L7 -->|"リクエスト単位で分散"| A2["Instance A"]
L7 --> B2["Instance B"]
L7 --> C2a["Instance C"]
end
graph LR
subgraph "解決策2: クライアントサイドLB"
C3["Client"]
C3 -->|"直接振り分け"| A3["Instance A"]
C3 --> B3["Instance B"]
C3 --> C3a["Instance C"]
end
Istioによる解決
Istioのサイドカープロキシ(Envoy)はL7レベルでgRPCリクエストを処理するため、HTTP/2の長期接続の問題を透過的に解決します。
# Envoyがリクエスト単位でgRPCのロードバランシングを実施
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: grpc-service
spec:
host: grpc-service
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN # gRPCでもリクエスト単位で分散
connectionPool:
http:
h2UpgradePolicy: DEFAULT # HTTP/2接続を管理
Kubernetesのサービスディスカバリ
Kubernetesには組み込みのサービスディスカバリが提供されています。
Serviceリソース
# Kubernetes Service
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: production
spec:
selector:
app: payment-service # このラベルを持つPodをバックエンドに
ports:
- port: 80 # Serviceのポート
targetPort: 8080 # Podのポート
type: ClusterIP # クラスタ内部からのみアクセス可能
DNS解決
# Kubernetes DNS命名規則
<service-name>.<namespace>.svc.cluster.local
# 例:
payment-service.production.svc.cluster.local
→ ClusterIP: 10.96.45.123
→ 実際のPod: 10.0.0.5:8080, 10.0.0.6:8080, 10.0.0.7:8080
# 同じNamespace内ではサービス名だけで解決可能
payment-service
→ 10.96.45.123
Headless Service(直接Pod IP取得)
# Headless Service: ClusterIPなし、Pod IPを直接返す
apiVersion: v1
kind: Service
metadata:
name: payment-service-headless
spec:
clusterIP: None # Headless
selector:
app: payment-service
ports:
- port: 8080
# DNS問い合わせ結果:
# payment-service-headless.production.svc.cluster.local
# → 10.0.0.5, 10.0.0.6, 10.0.0.7(Pod IPが直接返る)
まとめ
| ポイント | 内容 |
|---|---|
| サービスディスカバリ | 動的に変化するサービスアドレスを自動解決する仕組み |
| 3つのパターン | クライアントサイド、サーバーサイド、DNSベース |
| ヘルスチェック | Liveness/Readiness/Startupの3種類で健全性を管理 |
| LBアルゴリズム | ラウンドロビン、最小接続数、ハッシュベースなど用途に応じて選択 |
| gRPC課題 | HTTP/2長期接続のLB問題はIstio/EnvoyのL7 LBで解決 |
| Kubernetes | Service + CoreDNSで組み込みのディスカバリを提供 |
チェックリスト
- サービスディスカバリが必要な理由を説明できる
- クライアントサイド/サーバーサイド/DNSベースの違いを説明できる
- Kubernetesの3種類のヘルスチェック(Liveness/Readiness/Startup)を理解した
- 主要なロードバランシングアルゴリズムの特徴を説明できる
- gRPCのロードバランシング課題とIstioによる解決を理解した
- KubernetesのServiceとDNS解決の仕組みを理解した
次のステップへ
サービスディスカバリとロードバランシングの仕組みを理解したところで、次は演習に入ります。ここまで学んだサービスメッシュの知識を総合して、実際のマイクロサービスシステムに対するサービスメッシュ構成を設計しましょう。
推定読了時間: 20分