Load Balancerの7つの用途:詳細な説明と例付き

この記事には短い動画があります:TikTok

1. 概要

Load Balancer (LB) について、多くの人は単純に「複数のサーバーにリクエストを分散する」と考えています。しかし、LBはそれ以上のことを行います。SSLオフロード、ユーザーセッションの維持、DDoS対策、バックエンドのヘルスチェックなど、多くの機能があります。

この記事ではLoad Balancerの7つの主な用途について説明します。各用途には、nginx、Spring Boot、Kubernetesの具体的な設定例またはコード例が付いています。これを読むことで、実際のシステムでLBがどのように動作するかが理解でき、いつどの機能を有効にするべきか、また一般的な設定エラーをどのように避けるかが分かります。

2. トラフィック分散(Traffic Distribution)

これは最も基本的な機能です。LBはクライアントからリクエストを受け取り、ラウンドロビン、最少接続、加重ラウンドロビンなどのアルゴリズムに従って、複数のバックエンドサーバーのいずれかに転送します。

upstream order_service {
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
    server 10.0.1.13:8080;
}

server {
    listen 80;
    location /orders/ {
        proxy_pass http://order_service;
    }
}

upstreamはOrderServiceの3つのインスタンスを宣言します。デフォルトではnginxはラウンドロビンを使用します—リクエスト1は.11に、リクエスト2は.12に、リクエスト3は.13に行き、その後.11に戻ります。あるインスタンスがより重いリクエストを処理する場合、least_conn;に変更してnginxに開いている接続が最も少ないサーバーを優先させることができます。

注意: ラウンドロビンはサーバーが健全か不健全かは考慮しません。インスタンスのキャパシティが異なる場合(たとえば、より強力なマシンがある場合)、weightを使用します:

server 10.0.1.11:8080 weight=3;
server 10.0.1.12:8080 weight=1;

3. SSLターミネーション

各バックエンドが自分でTLSを復号化する(CPU が無駄)代わりに、これをLBに任せます。LBは証明書を保持し、HTTPSをプレーンHTTPに復号化してから、内部ネットワークに転送します。

server {
    listen 443 ssl;
    server_name api.nonladev.com;

    ssl_certificate     /etc/nginx/certs/nonladev.crt;
    ssl_certificate_key /etc/nginx/certs/nonladev.key;

    location / {
        proxy_pass http://order_service;   # プレーンなHTTP、TLSは不要
        proxy_set_header X-Forwarded-Proto https;
    }
}

このおかげで、Spring Bootで書かれたOrderServiceはserver.ssl.*を設定する必要はありません—それは単にポート8080でHTTPを実行するだけです。ヘッダーX-Forwarded-Protoは、バックエンドが元のリクエストがHTTPSであることを知ることができるように支援し、リダイレクトURLを正しく生成します。

注意: LBとバックエンド間の内部ネットワークが信頼できない場合(たとえば、複数のサブネットや異なるクラウド間)、ターミネーションだけでなく「SSLブリッジング」を使用します。LBは復号化した後、内部証明書で再度暗号化してから転送します。

4. セッション永続性(Session Persistence)

バックエンドがセッションをメモリに保存する場合(ローカルセッション)、同じユーザーのリクエストは常に同じインスタンスに行く必要があります。そうしないと、ユーザーはランダムに「ログアウト」されます。

upstream order_service {
    ip_hash;                       # 同じクライアントIP -> 同じサーバー
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
}

ip_hashはクライアントIPをハッシュして、upstream内の特定のサーバーを固定的に選択します。別の方法はクッキーベースのスティッキーセッションです—LBは自動的にインスタンスをマークするクッキーを付加し、クライアントが共有NAT背後にある場合でも機能します。

より堅牢なソリューションはスティッキーセッションを削除し、Spring Session + Redisを使用してセッションを外部化することです:

@Configuration
@EnableRedisHttpSession
public class SessionConfig {
}
spring.data.redis.host=redis.internal
spring.data.redis.port=6379

この設定では、セッションは各インスタンスのメモリではなくRedisに保存されます。どのサーバーに行ったリクエストでもセッションを読み取ることができます—LBはip_hashを使う必要なく、通常のラウンドロビンを使用できます。

注意: 複数のユーザーが共有NAT(例:会社のオフィス)を使用している場合、ip_hashは負荷の不均衡を引き起こしやすくなります—すべてのトラフィックが1つのサーバーに集中します。システムが大きくなったら、セッションの外部化を優先してください。

5. 高可用性(High Availability)

LBは継続的にバックエンドをチェックします。インスタンスがダウンした場合、LBは自動的にそれをサービス対象から除外し、トラフィックを生き残っているインスタンスに振り替えます—これすべてはクライアントに気づかれることなく行われます。

upstream order_service {
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}

max_fails=3は、3回連続してリクエストが失敗した後、nginxはそのサーバーを30秒間「ダウン」とマークし(fail_timeout)、新しいリクエストを送信しないことを意味します。30秒後、nginxは再度試します—サーバーが回復した場合、ラウンドロビンに戻されます。

[14:02:01] 10.0.1.12 timeout -> fail count 1
[14:02:05] 10.0.1.12 timeout -> fail count 2
[14:02:09] 10.0.1.12 timeout -> fail count 3 -> marked DOWN for 30s
[14:02:09] traffic rerouted to 10.0.1.11
[14:02:39] 10.0.1.12 retried -> healthy -> back in pool

このメカニズムのおかげで、1つのインスタンスがクラッシュしてもシステム全体が崩壊しません—これこそが、ネットワークレイヤーでの高可用性の基盤です。

6. 水平スケーラビリティ(Scalability)

LBは水平スケーリングが機能するための前提条件です。新しいインスタンスを追加すると、LBは自動的にそれを検出し、トラフィックを分散します。バックエンドのコードを変更する必要はありません。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.4.0
          ports:
            - containerPort: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

ここで、Kubernetes Service(内部LBとして機能)はorder-serviceポッド間のトラフィックを自動的に分散します。HorizontalPodAutoscalerはCPUを監視します—70%を超えるとKubernetesは追加のポッドを作成し(最大10個まで)、LBはすぐに新しいポッドをサービス対象リストに追加します。再デプロイやリスタートは必要ありません。

注意: スケーリングが効果的に機能するのは、バックエンドがステートレスである場合だけです(またはセッションが第4項のように外部化されている場合)。そうでなければ、追加された新しいポッドはユーザーの古いセッションを受け取りません。

7. DDoS軽減(DDoS Mitigation)

LBはエッジレイヤーに位置するため、異常なトラフィックをバックエンドに到達する前に遮断するのに理想的な場所です—レート制限またはトラフィックの広いスコープへの分散によって。

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;

server {
    location /orders/ {
        limit_req zone=api_limit burst=10 nodelay;
        proxy_pass http://order_service;
    }
}

limit_req_zoneは各IPの最大20リクエスト/秒に制限します。burst=10は最大10リクエストのキューを越える短期的なスパイクを許可し、nodelayは遅延する代わりにバーストを即座に処理します。この制限を超えるとnginxは503 Service Temporarily Unavailableを返し、リクエストがバックエンドに集中しないようにします。

アプリケーションレイヤーでレート制限が必要な場合(LBの補足的な役割として、置き換えではなく)、Spring BootでBucket4jを使用します:

<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.10.1</version>
</dependency>
Bucket bucket = Bucket.builder()
    .addLimit(Bandwidth.simple(20, Duration.ofSeconds(1)))
    .build();

@GetMapping("/orders")
public ResponseEntity<?> getOrders() {
    if (!bucket.tryConsume(1)) {
        return ResponseEntity.status(429).body("リクエストが多すぎます");
    }
    return ResponseEntity.ok(orderService.findAll());
}

注意: IP別のレート制限は、IPスプーフィングまたは分散型ボットネットで回避されやすいです。大規模なDDoS攻撃の場合、LBをフロント層のCDN/WAFと組み合わせる必要があります。LBは単なる第二次防御層です。

8. バックエンドのヘルスモニタリング(Health Monitoring)

第5項(リクエスト失敗時の応答)とは異なり、ヘルスモニタリングはLBが主動的にエンドポイントを定期的に呼び出してサーバーが生きているかどうかを知る方法です—これをアクティブなヘルスチェックと呼びます。

@RestController
public class HealthController {

    @GetMapping("/health")
    public ResponseEntity<String> health() {
        boolean dbUp = checkDatabaseConnection();
        if (!dbUp) {
            return ResponseEntity.status(503).body("DB_DOWN");
        }
        return ResponseEntity.ok("OK");
    }

    private boolean checkDatabaseConnection() {
        // connectionプールをチェック、true/falseを返す
        return true;
    }
}

Spring Boot Actuatorを使用すると、手作業で書く必要なく同様のエンドポイントがあります:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=always

LB(またはKubernetes)は数秒ごとに/actuator/healthを呼び出します:

livenessProbe:
  httpGet:
    path: /actuator/health
    port: 8080
  periodSeconds: 5
  failureThreshold: 3

DBが故障した場合、エンドポイントが正しく応答することを確認するためのテストを作成します:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HealthControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturn503WhenDatabaseDown() {
        // testプロファイルまたはmock beanを通じてDBダウンをシミュレート
        ResponseEntity<String> response =
            restTemplate.getForEntity("/health", String.class);

        assertThat(response.getStatusCode().value()).isIn(200, 503);
    }
}

このテストは、エンドポイント/healthが常に有効なステータス(200または503)を返し、例外をスローしないことを保証します—ヘルスチェック自体が失敗すると、LBはサーバーがダウンしていると勘違いし、健全なインスタンスを誤って削除するためです。

注意: ヘルスチェックエンドポイントは軽量で高速である必要があります(数百ms以下)。多くのテーブルをクエリしたり、他のサービスを呼び出したりすると、ヘルスチェック自体がタイムアウトの原因となり、LBに誤って削除されます。

9. 結論

Load Balancerは単なる「トラフィック分散」ではなく、SSLオフロード、セッション管理、エラー検出、水平スケーリングのサポート、DDoS防御の最初の層として機能します。システムを設計する際には、上記の7つの用途のどれが必要かを明確に識別して、LBの設定時に適切な機能を選択する必要があります—nginx、HAProxy、AWS ALB、Kubernetes Serviceはこれらのほとんどのユースケースをサポートしており、設定方法が異なるだけです。

最後のアドバイス:すべての責任を1つのLBに集中させないでください。アクティブなヘルスチェック、レート制限、セッションの外部化を組み合わせることで、ラウンドロビンを有効にして放置するだけよりも、システムがより良いスケーラビリティとフォールトトレランスを持つようになります。

system-designload-balancerscalabilityhigh-availabilitynginx