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クライアント開発を加速
参考資料
ブラウザローカルツールを無料で試す →