アプリケーションのスケールと負荷分散

KubernetesBeginner
オンラインで実践に進む

はじめに

Service は、複数の Pod に対してクライアントが利用できる安定した単一のアドレスを提供します。需要が変化する場面では、特にその価値が高まります。Deployment はレプリカを追加または削除できますが、Service の識別情報は変わりません。

この実験では、各バックエンドが自身の Pod のホスト名を返します。2 個のレプリカから 4 個へスケールし、1 つの Service を通じて個別のリクエストを送信し、複数の Pod からレスポンスが返ることを確認します。その後、2 個へスケールダウンし、Deployment と EndpointSlice の両方が新しい望ましい状態に収束する様子を観察します。

これは手動による水平スケーリングです。HPA による自動スケーリングは、リソース要求、メトリクス、制御ポリシーに依存します。そのため、まずは手動でのレプリカ動作を十分に理解してから扱うのが適切です。

観測可能なレプリケーションアプリケーションを構築する

環境の起動: この実験では、完全な Kubernetes クラスターが起動します。コントロールプレーン、ノード、ネットワークコンポーネントの構成には通常 2~3 分かかります。環境の読み込みが完了するまで、そのままお待ちください。

このステップでは、各リクエストを処理した Pod を特定できる 2 つのバックエンドを作成します。これにより、Service によるトラフィック分散を抽象的なものではなく、実際に確認できるようになります。

最初のコマンドでは cd を使って、あらかじめ用意された作業ディレクトリへ移動します。次のコマンドでは ヒアドキュメントを使用します。cat <<'EOF' > hostname-web.yaml は、終了文字列 EOF までのすべての行を YAML ファイルへ書き込み、以前の内容があれば置き換えます。

cd /home/labex/project/scale-lab
cat <<'EOF' > hostname-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostname-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hostname-web
  template:
    metadata:
      labels:
        app: hostname-web
    spec:
      containers:
        - name: web
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c"]
          args:
            - mkdir -p /www; hostname > /www/index.html; exec httpd -f -p 8080 -h /www
          ports:
            - name: http
              containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: hostname-web
spec:
  selector:
    app: hostname-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web

終了文字列 EOF の後、kubectl apply -f はファイル内のすべてのオブジェクトを API サーバーへ送信します。rollout status は、目的の 2 個の Pod が準備されるまで最大 60 秒待機します。-l app=hostname-web は、そのラベルを持つ Pod だけを一覧表示します。

コンテナーのコマンド内では、セミコロンによって処理を順番に実行します。/www を作成し、ホスト名を index.html にリダイレクトし、その後 exec を使って Web サーバーをコンテナーのメインプロセスにします。2 つの Pod 名は異なるため、各バックエンドは異なるページの値を返します。

基準となるバックエンドを確認する

このステップでは、Deployment のレプリカ数と Service の準備済みバックエンド数が対応していることを確認します。

関連する 3 つのビューを確認します。get deployment は望ましいレプリカ数と準備済みレプリカ数を表示します。-l はアプリケーション Pod を選択し、-o wide は IP アドレスやノードの列を追加します。最後のラベルセレクターは、この Service 用に作成された EndpointSlice を検索します。

kubectl get deployment hostname-web
kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web

Deployment には 2/2 と表示され、EndpointSlice には 2 つのアドレスが表示されます。長時間実行する 1 つのクライアント Pod から、Service へ複数回リクエストを送信します。

kubectl run は、--restart=Never が指定されているため、単独の Pod を作成します。区切り文字 -- によって kubectl のオプション指定が終了し、sleep 3600 がコンテナーを動作し続けさせるコマンドになります。for ループは seq 1 6 を使って 6 回の反復を生成し、kubectl exec POD -- COMMAND はそのたびにクライアント内で wget を実行します。

kubectl run load-client --image=busybox:1.36 --image-pull-policy=IfNotPresent --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/load-client --timeout=30s
for i in $(seq 1 6); do kubectl exec load-client -- wget -qO- http://hostname-web; done

各レスポンスは Pod 名です。短いサンプルでは、どちらか一方の名前、または両方の名前が表示される場合があります。Service による分散は、厳密なラウンドロビン順序を保証するものではありません。

宣言的にスケールアップする

このステップでは、保存されている望ましい状態を 2 個のレプリカから 4 個へ変更します。マニフェストを編集することで、ファイルと実行中のオブジェクトの状態を一致させます。

sed -i 's/old/new/' file は、ファイル内の一致する文字列を直接置換します。続いて grep -nreplicas: を検索し、行番号を表示します。これにより、変更を適用する前に簡単に確認できます。

cd /home/labex/project/scale-lab
sed -i 's/replicas: 2/replicas: 4/' hostname-web.yaml
grep -n 'replicas:' hostname-web.yaml
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get deployment hostname-web

Deployment には 4/4 と表示されるはずです。実際の状態が新しい望ましい状態を下回っていたため、コントローラーが Pod を 2 個追加で作成しました。

Service のバックエンド集合が拡大する様子を確認する

このステップでは、変更していない Service が新しい Pod を自動的に検出することを確認します。

EndpointSlice のコマンドでは、通常の表形式出力では詳細が省略される場合があるため、JSONPath を使用します。range は各エンドポイントに対してテンプレートを繰り返します。各回では、最初のアドレス、文字列 ready、準備状態の値、改行を順に出力します。バックスラッシュは、表示上複数行に分かれたシェルコマンドを継続させます。

kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'

現在は、準備済みのアドレスが 4 つあります。Service 自体は編集していません。app=hostname-web を持つ準備済み Pod がすべてセレクターに一致し続けているためです。

安定した Service IP と、拡大したバックエンド集合を比較します。

kubectl get service hostname-web -o wide

EndpointSlice のメンバー構成が変化しても、ClusterIP は安定したままです。

複数の Pod にリクエストが到達することを観察する

このステップでは、1 つの Service を通じて独立した HTTP リクエストを送信し、どの Pod のホスト名が応答したかを集計します。

まず、rm -f で古い結果ファイルが存在する場合に削除します。-f により、ファイルがなくてもエラーになりません。ループは 20 回リクエストを送信します。>> は各ホスト名を上書きせず、以前の結果に追記します。最後に、パイプでソート済みの行を uniq -c へ渡します。uniq -c は隣接する重複行をまとめ、各ホスト名の前に出現回数を付けます。

rm -f /tmp/hostname-responses.txt
for i in $(seq 1 20); do
  kubectl exec load-client -- wget -qO- http://hostname-web >> /tmp/hostname-responses.txt
done
sort /tmp/hostname-responses.txt | uniq -c

複数のホスト名が表示されるはずです。件数には偏りが生じる可能性があり、短い実行で 4 つすべてのバックエンドに到達するとは限りません。Kubernetes の Service ルーティングは接続を分散しますが、完全に均等な順序や配分を保証するものではありません。

サンプルが到達した一意なバックエンド数を確認します。sort -u は各ホスト名を 1 つだけ残し、パイプでその行を wc -l へ渡します。wc -l は行数を数えます。

sort -u /tmp/hostname-responses.txt | wc -l

1 より大きい値であれば、安定した Service アドレスが複数の Pod へリクエストをルーティングしたことを直接示しています。

命令的にスケールダウンする

このステップでは、kubectl scale を使って、実行中の状態を 4 個のレプリカから 2 個へすばやく調整します。

kubectl scale は、実行中の Deployment が求めるレプリカ数を即座に変更します。--replicas=2 で新しい数を指定しますが、YAML ファイル自体は編集されません。

kubectl scale deployment/hostname-web --replicas=2
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web

Kubernetes は Pod を 2 個終了し、2 個を残します。Service のバックエンド集合も収束するまで待ちます。

次の上限付きポーリングループは、最大 30 回試行します。各回で、準備済みエンドポイント数を count に保存します。jq は EndpointSlice の JSON から準備済みエンドポイントだけを抽出し、配列の長さを返します。[ "$count" -eq 2 ] は数値を比較するシェルテストです。&& break は条件が成立したときにループを終了し、sleep 1 は再試行前に 1 秒間待機します。

for i in $(seq 1 30); do
  count=$(kubectl get endpointslices -l kubernetes.io/service-name=hostname-web -o json | jq '[.items[].endpoints[] | select(.conditions.ready == true)] | length')
  [ "$count" -eq 2 ] && break
  sleep 1
done
echo "Ready backends: $count"

実行中の Deployment は現在 2 個のレプリカを要求していますが、ファイルにはまだ 4 個と記載されています。この違いは、次のステップのために意図的に残しています。

マニフェストを同期し、コントローラーの履歴を確認する

このステップでは、保存されたマニフェストを実行中の 2 レプリカ状態に合わせ、スケーリング操作とコントローラーの証拠を関連付けます。

まず、2 つの望ましい状態を比較します。grep -n は保存された行を表示し、JSONPath は実行中の .spec.replicas フィールドだけを抽出して改行を追加します。

grep -n 'replicas:' /home/labex/project/scale-lab/hostname-web.yaml
kubectl get deployment hostname-web -o jsonpath='Live replicas: {.spec.replicas}{"\n"}'

ファイルの値を 4 から 2 に戻して適用します。

sed -i 's/replicas: 4/replicas: 2/' /home/labex/project/scale-lab/hostname-web.yaml
kubectl apply -f /home/labex/project/scale-lab/hostname-web.yaml

実行中の状態がすでに 2 レプリカであるため、この apply では新しい Pod は作成されないはずです。Deployment のイベントを確認します。パイプによって describe の完全な出力が sed -n に渡されます。/Events:/,$p は、「Events: を含む行から最後までを表示する」という意味です。

kubectl describe deployment hostname-web | sed -n '/Events:/,$p'

スケールアップおよびスケールダウンの判断を示す ScalingReplicaSet メッセージを探してください。最後に、一時的なクライアントを削除します。--ignore-not-found により、Pod がすでに消えていてもクリーンアップは成功します。

kubectl delete pod load-client --ignore-not-found

手動スケーリングは望ましいレプリカ数を変更します。Deployment コントローラーは Pod を作成または終了し、Service は準備済みのメンバーを自動的に追跡します。マニフェストを同期しておけば、後で kubectl apply を実行した際に古いレプリカ数が予期せず復元されることを防げます。

まとめ

Deployment を 2 レプリカから 4 レプリカへ、そして再び 2 レプリカへ手動でスケールしました。コントローラーが Pod を調整する様子を確認し、Service を変更せずに EndpointSlice のメンバー構成が準備済みバックエンドに追随することを観察しました。また、1 つの Service アドレスが独立したリクエストを複数の Pod へルーティングしたことを直接確認しました。

重要なメンタルモデルは次のとおりです。レプリカ数は望ましい状態であり、Deployment コントローラーが実際の Pod を調整し、Service が準備済みでラベルに一致するバックエンドを追跡します。宣言的なファイルは、実行中の意図的な変更と同期させることで、将来の apply 操作を予測しやすくできます。