本稿ではKubernetesのHorizontal Pod Autoscaler(HPA)のクイックハンズオンを行います。 Kubernetes上にシンプルなCPU負荷の高いWebアプリをデプロイし、そのデプロイのためにHPAを設定します。 その後、負荷テストを行い、設定したメトリクスに基づいてレプリカの数が自動的にスケールすることを確認します。
検証環境
- MacBook Pro 2021 (M1 Pro) 32GB
 - Kubernetes v1.26.4 on Rancher Desktop v1.8.1 using VM with 4 CPUs and 12 GB memory
 
概要
- 検証用に簡単なGoアプリケーションを作成する
 - GoアプリケーションをDocker化する
 - Kubernetes上にアプリケーションをデプロイする
 - HPAを構成する
 - Goアプリケーションのポートフォワーディングを確立する
 - k6ロードテスターによるテスト
 
GitHubリポジトリはこちら
Step 1. 検証用Goアプリケーションの作成
まず、Ginフレームワークを用いてCPU負荷の大きなGoアプリケーションを作成します。
package main
import (
	"fmt"
	"math"
	"math/rand"
	"net/http"
	"github.com/gin-gonic/gin"
)
func cpu_intensive(rnd int) float64 {
	f := 0.
	for i := 0; i < 10_000*rnd; i++ {
		f += math.Sqrt(float64(i))
	}
	return f
}
func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		_rnd := rand.Intn(1000)
		f := cpu_intensive(_rnd)
		ret := fmt.Sprintf("%d: %.2f", _rnd, f)
		c.String(http.StatusOK, ret)
	})
	r.Run(":8080")
}
上記のプログラムはポート8080でGET /リクエストを受信し、sqrtを異なるループ回数行います。
Step 2: GoアプリケーションのDocker化
次にDockerfileを以下の通り作成します。
FROM golang:alpine AS builder
WORKDIR /src
COPY . /src
RUN CGO_ENABLED=0 go build -o app
FROM scratch
COPY --from=builder /src/app /app
EXPOSE 8080
ENTRYPOINT ["/app"]
上記のDockerfileを用いてイメージをビルドし、DockerHub上にryojpn/go-cpu-intensiveとしてアップロードしておきました。
よろしければご利用ください。
Step 3: Kubernetes上にデプロイ
以下のDeploymentを作成します。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-cpu-intensive
  labels:
    app: go-cpu-intensive
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-cpu-intensive
  template:
    metadata:
      labels:
        app: go-cpu-intensive
    spec:
      containers:
      - name: go-cpu-intensive
        image: ryojpn/go-cpu-intensive
        resources:
          limits:
              cpu: 1000m
              memory: 500Mi
            requests:
              cpu: 500m
              memory: 250Mi
        ports:
        - containerPort: 8080
続いて、作成したDeploymentをapplyします:
kubectl apply -f k8s
Step 4: HPAの設定
HPAを設定し、CPU利用率に基づいてPodのレプリカ数を自動調節します:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: go-cpu-intensive
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-cpu-intensive
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
こちらもapplyしましょう:
kubectl apply -f k8s
Step 5: ポートフォワード
まずGoアプリケーションのPod名を取得します:
kubectl get po
go-cpu-intensive Deploymentに対するPod名が取得できたら以下を実行し、8080番でのポートフォワードを行います:
kubectl port-forward go-cpu-intensive-54449f89d-rg4qz 8080:8080
Pod名はご利用の環境に適宜修正してください。
Step 6: k6負荷試験ツールによるテスト
k6をインストールし、以下のテスト設定ファイルを作成します:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
    stages: [
        { duration: '1m', target: 100 },
        { duration: '1m', target: 200 },
        { duration: '3m', target: 800 },
        { duration: '1m', target: 900 },
        { duration: '2m', target: 500 },
        { duration: '2m', target: 200 },
    ]
}
export default function () {
  const res = http.get('http://localhost:8080/');
  check(res, {
    'status was 200': (r) => r.status == 200,
  })
  sleep(1);
}
続いて、ターミナルウィンドウを2つ開いてください。
一つ目では、HPAリソースをwatchして、メトリクス値の変化とレプリカ数の変動を監視します:
kubectl get hpa -w
もう一つのターミナルではk6を実行し、Podに負荷をかけます:
k6 run loadtest.js
以下が実行例のスクリーンショットです。

ご覧のように、CPU使用率の指標は最初のうちは最大100%にまで達していますが、レプリカが4つにスケールすると45%程度で安定します。 負荷テストが終了した後、スケールインを開始するまでには数分(デフォルトでは5分)かかります。この時間は、必要に応じて調整することも可能です。
最後に
上記のデモでは、ターゲットメトリクスとしてCPU使用率を使用しました。 しかし、HPAは常に目標メトリクスを満たすようにレプリカ数を維持することができないことが確認されました。 リクエスト数の増加がある程度予測できる場合は、レプリカ数を手動で大まかに設定した上でHPAに調整させるのが良いかもしれません。
Prometheus Adapterを使うと、CPU/メモリ使用率以外の様々なメトリクスをオートスケールに利用することが可能です。 今度はHTTPレスポンスタイムを取得し、オートスケーリングのメトリクスに活用する方法について調べてみようと思います。
