Go K8s PDBとHPAプロダクション:ゼロダウンタイムオートスケーリングの6つの重要設定

云原生

オートスケーリングが災害になる時:K8s弾力性の至暗の刻

深夜3時、セール流量が急増し、HPAがスケールアップをトリガー。しかし新規Podの起動には15秒のコールドスタートが必要で、その間に既存PodがOOM Killされる。さらに悪いことに、スケールダウン時にPDBが未設定で、3つのPodが同時に退去され、サービスが即座に503に。最終的に障害は40分間続け、10万人のユーザーに影響。

これは決して例外ではない。スケーリングによるサービス中断、不適切なHPAメトリクス選択、PDB設定の欠落、深刻なリソース浪費は、K8sオートスケーリングの4つの主要なペインポイントとなっている。PDB(PodDisruptionBudget)が最小可用インスタンスを保障し、HPA(HorizontalPodAutoscaler)がオンデマンドスケーリングを実現する。この2つの組み合わせで初めて真のゼロダウンタイムオートスケーリングが実現できる。本記事では6つの重要設定から、プロダクション級K8s弾力性システムの構築を案内する。


コア概念クイックリファレンス

概念 フルネーム 役割 キーパラメータ
PDB PodDisruptionBudget 自発的中断時の最小可用Pod数を制限 minAvailable / maxUnavailable
HPA HorizontalPodAutoscaler メトリクスに基づきPodレプリカ数を自動スケーリング ターゲットCPU/メモリ、カスタムメトリクス
VPA VerticalPodAutoscaler Podリソースリクエスト/リミットを自動調整 minAllowed / maxAllowed
minAvailable PDBで最低限維持すべき可用Pod数 絶対値またはパーセンテージ
maxUnavailable PDBで許可される最大不可用Pod数 絶対値またはパーセンテージ
ターゲットCPU HPAスケールアップをトリガーするCPU使用率閾値 通常60%-80%
カスタムメトリクス ビジネスメトリクスに基づくHPAスケーリング QPS、キュー深度など
スケーリングポリシー HPAスケールアップ/ダウンの動作制御 scaleUp/scaleDownポリシー
コールドスタート 新規Podの起動からReadyまでの時間 スケールアップ応答速度に影響

問題分析:K8sオートスケーリングの5つの課題

課題1:スケールアップ遅延による過負荷。HPAがCPU急増を検知してスケールアップをトリガーするが、新規PodはスケジューリングからReadyまで10-30秒かかり、その間もトラフィックが流入し続け、既存Podが圧倒される可能性がある。

課題2:スケールダウンによるサービス中断。HPAはスケールダウン時にランダムにPodを選択して終了する。PDBが未設定の場合、同時に過多のPodが終了され、サービス能力が急減または不可用になる。

課題3:不適切なメトリクス選択。CPUのみに基づくスケーリングは実際の負荷を反映しない。GoサービスがCPU低でgoroutine蓄積時に、HPAはスケールアップせず、レイテンシが急増する。

課題4:コールドスタートの影響。Goアプリケーションはコネクションプールの初期化や設定の読み込みに時間が必要。readinessProbeの設定が不適切だと、Ready前にトラフィックが新規Podに到達し、リクエスト失敗が発生する。

課題5:リソースフラグメンテーション。HPAスケールアップ後、Podの分布が不均一になる。スケールダウン時に特定ノードに削除が集中し、リソース利用の不均衡やノードレベルのカスケード障害を引き起こす可能性がある。


設定1:PDB最小可用インスタンス保障

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-service-pdb
  namespace: production
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api-service
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: gateway-pdb
  namespace: production
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: gateway

PDBはminAvailableで少なくとも2つのPodを可用に保障し、maxUnavailableで最大1つのPodの不可用を制限する。重要原則minAvailableはレプリカ数が固定のサービスに適し、maxUnavailableはレプリカ数が動的に変化するサービスに適する。PDBは自発的中断(ノードメンテナンス、スケールダウン)にのみ有効で、非自発的中断(Podクラッシュ)には作用しない。


設定2:HPA CPU/メモリ自動スケーリング

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-service-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60

HPAはCPU 70%とメモリ80%でスケーリングをトリガーする。scaleUpポリシーは60秒以内にレプリカ数を倍増可能、scaleDownポリシーは60秒ごとに最大10%削減、stabilizationWindowSecondsはスケールダウンのフラッピングを防止する。重要:スケールダウンクールダウンウィンドウを300秒に設定し、トラフィック変動による頻繁なスケーリングを回避。


設定3:HPAカスタムメトリクススケーリング

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-service-custom-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: 1000
    - type: Pods
      pods:
        metric:
          name: goroutine_count
        target:
          type: AverageValue
          averageValue: "5000"
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

カスタムメトリクスはPrometheus Adapterを通じてHPAに公開される。http_requests_per_secondはQPSに基づきスケーリング、goroutine_countはGoランタイムのgoroutine数に基づきスケーリングする。重要:GoサービスがCPU低でgoroutine蓄積時、CPUのみのメトリクスではスケールアップがトリガーされない — カスタムgoroutineメトリクスはプロダクションで必須。

カスタムメトリクスを公開するGoコード:

package main

import (
    "net/http"
    "runtime"
    "sync/atomic"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    requestCounter atomic.Int64
    goroutineGauge = prometheus.NewGaugeFunc(
        prometheus.GaugeOpts{
            Name: "goroutine_count",
            Help: "Current number of goroutines",
        },
        func() float64 {
            return float64(runtime.NumGoroutine())
        },
    )
    httpRequestsPerSecond = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "http_requests_per_second",
            Help: "HTTP requests per second",
        },
        []string{"method", "path"},
    )
)

func init() {
    prometheus.MustRegister(goroutineGauge)
    prometheus.MustRegister(httpRequestsPerSecond)
}

func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        httpRequestsPerSecond.WithLabelValues(r.Method, r.URL.Path).Inc()
        next.ServeHTTP(w, r)
    })
}

設定4:スケーリングポリシーとクールダウン期間

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-service-behavior-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      selectPolicy: Max
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
        - type: Pods
          value: 4
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 600
      selectPolicy: Min
      policies:
        - type: Percent
          value: 5
          periodSeconds: 120
        - type: Pods
          value: 1
          periodSeconds: 120

スケールアップポリシーselectPolicy: Maxは最も積極的なポリシーを選択し、負荷増加への迅速な対応を確保する。スケールダウンポリシーselectPolicy: Minは最も保守的なポリシーを選択し、600秒のクールダウンウィンドウで誤ったスケールダウンを防止する。プロダクションの鉄則:スケールアップは速く、スケールダウンは遅く — 余分なリソースを使ってもサービス中断のリスクは冒さない。


設定5:Goアプリ起動最適化とレディネスプローブ

package main

import (
    "context"
    "database/sql"
    "fmt"
    "net/http"
    "time"

    _ "github.com/go-sql-driver/mysql"
    "github.com/redis/go-redis/v9"
)

type App struct {
    db    *sql.DB
    redis *redis.Client
    ready bool
}

func (a *App) Init(ctx context.Context) error {
    var err error
    a.db, err = sql.Open("mysql", "user:pass@tcp(mysql:3306)/db")
    if err != nil {
        return fmt.Errorf("open mysql: %w", err)
    }
    a.db.SetMaxOpenConns(50)
    a.db.SetMaxIdleConns(10)
    a.db.SetConnMaxLifetime(5 * time.Minute)

    for i := 0; i < 10; i++ {
        if err = a.db.PingContext(ctx); err == nil {
            break
        }
        time.Sleep(time.Second)
    }
    if err != nil {
        return fmt.Errorf("ping mysql after retries: %w", err)
    }

    a.redis = redis.NewClient(&redis.Options{
        Addr:         "redis:6379",
        PoolSize:     50,
        MinIdleConns: 10,
    })
    if err = a.redis.Ping(ctx).Err(); err != nil {
        return fmt.Errorf("ping redis: %w", err)
    }

    a.ready = true
    return nil
}

func (a *App) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
    if !a.ready {
        http.Error(w, "not ready", http.StatusServiceUnavailable)
        return
    }
    if err := a.db.PingContext(r.Context()); err != nil {
        http.Error(w, "db unhealthy", http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-service
  template:
    metadata:
      labels:
        app: api-service
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: api-service
          image: api-service:latest
          ports:
            - containerPort: 8080
          startupProbe:
            httpGet:
              path: /healthz
              port: 8080
            failureThreshold: 30
            periodSeconds: 2
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 10
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 512Mi

重要設計startupProbeは起動が遅いPodに十分な初期化時間(最大60秒)を与え、readinessProbeは依存関係の健全性をチェックし、preStopフックはPodに10秒のグレースフルシャットダウン時間を与え、terminationGracePeriodSecondsはSIGTERM後も処理中のリクエストを完了できるようにする。


設定6:エンドツーエンドレジリエンステスト

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    autoscalingv2 "k8s.io/api/autoscaling/v2"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

type ResilienceTester struct {
    clientset *kubernetes.Clientset
    namespace string
}

func NewResilienceTester(kubeconfig, namespace string) (*ResilienceTester, error) {
    config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
    if err != nil {
        return nil, fmt.Errorf("build kubeconfig: %w", err)
    }
    cs, err := kubernetes.NewForConfig(config)
    if err != nil {
        return nil, fmt.Errorf("create clientset: %w", err)
    }
    return &ResilienceTester{clientset: cs, namespace: namespace}, nil
}

func (t *ResilienceTester) TestScaleUp(ctx context.Context, deployName string) error {
    hpa, err := t.clientset.AutoscalingV2().HorizontalPodAutoscalers(t.namespace).Get(ctx, deployName+"-hpa", metav1.GetOptions{})
    if err != nil {
        return fmt.Errorf("get hpa: %w", err)
    }
    fmt.Printf("HPA %s: min=%d max=%d current=%d\n",
        hpa.Name, *hpa.Spec.MinReplicas, hpa.Spec.MaxReplicas, hpa.Status.CurrentReplicas)

    deploy, err := t.clientset.AppsV1().Deployments(t.namespace).Get(ctx, deployName, metav1.GetOptions{})
    if err != nil {
        return fmt.Errorf("get deploy: %w", err)
    }
    fmt.Printf("Deployment %s: replicas=%d ready=%d available=%d\n",
        deploy.Name, deploy.Status.Replicas, deploy.Status.ReadyReplicas, deploy.Status.AvailableReplicas)
    return nil
}

func (t *ResilienceTester) TestPDB(ctx context.Context, pdbName string) error {
    pdb, err := t.clientset.PolicyV1().PodDisruptionBudgets(t.namespace).Get(ctx, pdbName, metav1.GetOptions{})
    if err != nil {
        return fmt.Errorf("get pdb: %w", err)
    }
    fmt.Printf("PDB %s: disruptionsAllowed=%d currentHealthy=%d desiredHealthy=%d\n",
        pdb.Name, pdb.Status.DisruptionsAllowed, pdb.Status.CurrentHealthy, pdb.Status.DesiredHealthy)
    return nil
}

func (t *ResilienceTester) RunFullTest(ctx context.Context) error {
    fmt.Println("=== PDB Test ===")
    if err := t.TestPDB(ctx, "api-service-pdb"); err != nil {
        fmt.Fprintf(os.Stderr, "PDB test failed: %v\n", err)
    }

    fmt.Println("=== HPA Test ===")
    if err := t.TestScaleUp(ctx, "api-service"); err != nil {
        fmt.Fprintf(os.Stderr, "HPA test failed: %v\n", err)
    }

    fmt.Println("=== Scale Up Simulation ===")
    scale, err := t.clientset.AppsV1().Deployments(t.namespace).GetScale(ctx, "api-service", metav1.GetOptions{})
    if err != nil {
        return fmt.Errorf("get scale: %w", err)
    }
    newScale := scale.DeepCopy()
    newScale.Spec.Replicas = scale.Spec.Replicas * 2
    _, err = t.clientset.AppsV1().Deployments(t.namespace).UpdateScale(ctx, "api-service", newScale, metav1.UpdateOptions{})
    if err != nil {
        return fmt.Errorf("update scale: %w", err)
    }
    fmt.Printf("Scaled from %d to %d replicas\n", scale.Spec.Replicas, newScale.Spec.Replicas)

    time.Sleep(30 * time.Second)
    return t.TestScaleUp(ctx, "api-service")
}

func main() {
    kubeconfig := os.Getenv("KUBECONFIG")
    if kubeconfig == "" {
        kubeconfig = clientcmd.RecommendedHomeFile
    }
    tester, err := NewResilienceTester(kubeconfig, "production")
    if err != nil {
        fmt.Fprintf(os.Stderr, "init tester: %v\n", err)
        os.Exit(1)
    }
    if err := tester.RunFullTest(context.Background()); err != nil {
        fmt.Fprintf(os.Stderr, "test failed: %v\n", err)
        os.Exit(1)
    }
}

エンドツーエンドテストはPDB保護、HPAスケーリング、Deploymentステータスを検証する。プロダクションでは低トラフィック期間に実行し、スケーリングがスムーズか、PDBが有効か、Podが正常にReadyになるかを観察する。


5つのよくある落とし穴

❌ 落とし穴1:PDB minAvailableを100%に設定 ✅ 100%は自発的中断を一切許可しないことを意味し、ノードメンテナンスが不可能になる。50%-66%に設定し、少なくとも半数のPodを可用に確保。

❌ 落とし穴2:HPAターゲットCPUを90%に設定 ✅ 90%の閾値はスケーリングトリガー前にPodが限界に近いことを意味し、リクエストレイテンシの急増は避けられない。60%-75%に設定し、スケーリングバッファを確保。

❌ 落とし穴3:CPUメトリクスのみ設定し、メモリとカスタムメトリクスを無視 ✅ GoサービスはCPU低でもメモリ/goroutineが高い場合がある。CPU+メモリ+ビジネスメトリクスを組み合わせて負荷を正確に反映する必要がある。

❌ 落とし穴4:readinessProbeとlivenessProbeに同じエンドポイントを使用 ✅ レディネスプローブは依存関係(DB/Redis)をチェックし、ライブネスプローブはプロセスのみをチェックすべき。同じエンドポイントを使用すると、依存関係の変動時にPodが再起動され、障害が悪化する。

❌ 落とし穴5:preStopフックを無視 ✅ preStopがないと、SIGTERM受信後にPodが即座にServiceから削除され、処理中のリクエストが失われる可能性がある。sleep 10でPodにリクエスト完了の十分な時間を与える。


10のエラートラブルシューティング

エラー症状 考えられる原因 デバッグコマンド 解決策
HPAがCPUメトリクスを取得できない Metrics Serverが未インストール kubectl get pods -n kube-system | grep metrics Metrics Serverをインストール
PDB DisruptionsAllowed=0 minAvailableが現在のレプリカ数と等しい kubectl get pdb -o yaml minAvailableを下げるかレプリカ数を増やす
HPAスケールアップがトリガーされない メトリクスが閾値に達していない kubectl get hpa -o yaml 現在のメトリクス値と閾値を確認
スケールアップ後PodがPending ノードリソース不足 kubectl describe pod <pending-pod> ノードを追加するかリソースリクエストを削減
スケールダウン後サービス503 PDBが未設定または低すぎる kubectl get pdb PDBを設定して最小可用性を保障
Pod起動直後にCrashLoopBackOff readinessProbeが失敗 kubectl logs <pod> 依存関係の初期化とプローブ設定を確認
HPAが頻繁にスケーリング スケールダウンクールダウンが短すぎる kubectl describe hpa stabilizationWindowSecondsを増加
カスタムメトリクスが利用不可 Prometheus Adapterが未設定 kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 Prometheus Adapterをデプロイ
ノードメンテナンス時にPodが強制退去 PDBが未作成 kubectl get pdb -A 重要サービスにPDBを作成
スケールアップが遅すぎる scaleUpポリシーが保守的すぎる kubectl describe hpa scaleUpポリシーをPercent:100に調整

高度な最適化テクニック

1. 予測的スケーリング。過去のトラフィックパターンに基づき、ピーク時間の前にスケールアップ。KEDAのCronトリガーやカスタム予測コントローラーと組み合わせて「トラフィック到着前にリソース準備」を実現。

2. Podトポロジー分散制約topologySpreadConstraintsと組み合わせて、スケールアップ後のPodがアベイラビリティゾーン間で均等に分散されるようにし、単一AZ障害によるサービス不可用を防止。

3. 優先度とプリエンプション。重要サービスに高い優先度クラスを設定。リソース不足時に重要サービスを優先し、低優先度サービスはプリエンプション可能に。

4. VPAとHPAの協調。VPAはリソースリクエストを調整し、HPAはレプリカ数を調整する。VPAは推奨のみモード(mode: Off)を推奨し、HPAとの競合を回避。

5. FinOpsコスト最適化。SpotインスタンスとCluster Autoscalerを組み合わせる。非重要サービスはSpotインスタンスでコスト削減、重要サービスはOn-Demandインスタンスで安定性を確保。


比較:HPA vs VPA vs KEDA vs Cluster Autoscaler

特徴 HPA VPA KEDA Cluster Autoscaler
スケーリング次元 水平(レプリカ数) 垂直(リソース量) 水平(レプリカ数) ノード数
トリガー方法 CPU/メモリ/カスタムメトリクス 過去のリソース使用量 イベント駆動(マルチソース) Podスケジューリング失敗
ユースケース 負荷変動が大きい リソース設定が不適切 イベント駆動/バッチ処理 ノードリソース不足
PDBとの互換性 ✅ 必須 ⚠️ 競合の可能性 ✅ 必須 ✅ 必須
コールドスタートの影響 ⚠️ 影響あり ✅ 影響なし ⚠️ 影響あり ⚠️ 影響あり
Goサービスへの適合 ⚠️ カスタムメトリクスが必要 ✅ 自動調整 ✅ 豊富なトリガー ✅ 透過的
プロダクション成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
推奨組み合わせ HPA+PDB VPA推奨モード KEDA+PDB CA+HPA+PDB

まとめ

K8sオートスケーリングはHPAを設定して終わりではない。PDBで可用性を保障、HPAで弾力性を実現、プローブでレディネスを確保、ポリシーでペースを制御する4つの統合システムである。6つの重要設定——PDB最小可用性保障、HPA CPU/メモリスケーリング、カスタムメトリクススケーリング、スケーリングポリシーとクールダウン、Go起動最適化とプローブ、エンドツーエンドレジリエンステスト——がプロダクション弾力性の完全なチェーンをカバーする。覚えておくべきは:スケールアップは速く、スケールダウンは遅く、PDBは必須、プローブは分離——これが真のゼロダウンタイムオートスケーリングの鍵である。将来的には、AIベースの予測的スケーリングとサーバーレス弾力性が運用の複雑さをさらに削減するだろう。


おすすめツール

  • JSONフォーマッター — HPA/PDBのYAML/JSON設定をフォーマット、リソース定義の問題を素早く特定
  • ハッシュ計算ツール — ConfigMapとSecretのチェックサムを計算、スケーリング設定データの整合性を確保
  • cURL to Code — cURLテストコマンドをGoコードに変換、K8s APIクライアント開発を加速

参考資料

ブラウザローカルツールを無料で試す →

#K8s PDB#K8s HPA#PodDisruptionBudget#自动伸缩#Go微服务#2026#云原生