Chapter 08無料公開

[解説編] HTTP サーバの負荷分散をしたい

tennashi
tennashi
2022.09.29に更新

これまでの章で HTTP サーバを Pod として起動し、それにアクセスする方法を紹介してきました。

本章では同じコンテンツを返す HTTP サーバを複数台起動し、HTTP リクエストをそれらで分散して処理させる方法を紹介します。
Service リソースのマニフェストファイルを書いたときに Pod の指定方法が名前ではなくラベルであったことを覚えているでしょうか。
リソース名は Namespace 内で一意である必要がありますが、ラベルは複数のリソースで同じラベルを指定することができます。
つまり、Service リソースには最初から負荷分散機能が用意されていたのです。
あとは Pod を複数立てればおしまいです。

ここでは複数 Pod を立てるいくつかの方法について紹介します。

Pod を複数用意する

もちろん、素朴に Pod リソースを複数作成するだけで目的は達成できます。
まずはこの方法から試してみて、Service リソースが本当に負荷分散してくれるのかを見てみましょう。

pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: web
  labels:
    app: web
spec:
  containers:
    - name: http # コンテナ名
      image: nginx # コンテナイメージ名
      ports:
        - containerPort: 80 # 公開されているコンテナのポート番号
---
apiVersion: v1
kind: Pod
metadata:
  name: web2
  labels:
    app: web
spec:
  containers:
    - name: http # コンテナ名
      image: nginx # コンテナイメージ名
      ports:
        - containerPort: 80 # 公開されているコンテナのポート番号

マニフェストファイルには --- で区切ることで複数のマニフェストを記述できます。
web Pod と同じ設定内容の web2 Pod のマニフェストを追記しました。

このマニフェストファイルを適用することで、nginx コンテナを 2 つ起動できます。

$ kubectl apply -f pod.yaml
$ kubectl get pods

Service リソース経由でリクエストを送信することで、web Pod と web2 Pod にリクエストが分散されます。
どちらの Pod にアクセスされたかを確認するためにログを出力しておきましょう。
コンテナのログは kubectl logs コマンドで確認できるのでした。
kubectl logs-f オプションを使うとログの追記を即座に画面に表示してくれます。
今回はこれを利用してみましょう。

$ kubectl logs -f web

実行中はプロンプトが返ってこないので別のターミナルを起動して web2 Pod のログも出力しておきましょう。

$ kubectl logs -f web2

リクエスト方法は前章の内容を思い出しましょう。

ここでは type NodePort である前提で k8s ノードの IP アドレス経由でアクセスします。

$ curl 172.18.0.3:31651
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

何度かアクセスをしてみると、リクエストが分散されていることを確認できるかと思います。
これで負荷分散をしたいという要求は満たせました。

しかし 2 個程度であればコピペしても手間ではありませんが、数が増えたら面倒です。
このように同じ設定内容の Pod を複数用意したいときのために ReplicaSet リソースが存在します。

ReplicaSet リソース経由で Pod を複数起動する

まず pod.yaml で作成した web Pod web2 Pod を削除しておきます。

$ kubectl delete -f pod.yaml

そして web ReplicaSet を作成するためのマニフェストファイルを記述します。

replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web # この ReplicaSet の管理対象となっている Pod が持つラベルを指定
  template:
    metadata:
      labels:
        app: web # spec.selector の指定に合うラベルでないと作成時にエラーになる
    spec:
      containers:
        - name: http # コンテナ名
          image: nginx # コンテナイメージ名
          ports:
            - containerPort: 80 # 公開されているコンテナのポート番号
$ kubectl apply -f replicaset.yaml
$ kubectl get replicaset
$ kubectl get pods

web-XXXXX という名前の Pod が spec.replicas に記述した数だけ起動していることを確認しましょう。

これで 100 コンテナで負荷分散したくなったとしても spec.replicas100 に変更するだけで実現できるようになりました。

しかし実際の運用では ReplicaSet を直接利用することはありません。
ReplicaSet では何が問題なのでしょうか。

例えば nginx のバージョンを変更したいと思った場合、どのような手順で更新作業を行うでしょうか。

  • 新しいバージョンの Pod を作成してから古いバージョンの Pod を削除する
  • 古いバージョンの Pod を削除してから新しいバージョン Pod を作成する
  • 古いバージョンの Pod と新しいバージョンの Pod の合計数を保つように一つずつ作成と削除を繰り返す

上記のような更新手順が考えられます。
このような頻出の更新パターンを自動で制御するためのリソースがいくつか存在します。

実際の運用ではこの機能を持つリソースを利用します。

Deployment リソース経由で Pod を複数起動する

Deployment を使うことで以下のようなユースケースを満たせます。

  • 新しいバージョンの Pod を作成してから古いバージョンの Pod を削除する
  • 古いバージョンの Pod と新しいバージョンの Pod の合計数を保つように一つずつ作成と削除を繰り返す
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: http # コンテナ名
          image: nginx # コンテナイメージ名
          ports:
            - containerPort: 80 # 公開されているコンテナのポート番号

このマニフェストを適用すれば Deployment リソースの作成は完了です。

$ kubectl apply -f deployment.yaml
$ kubectl get pods

Deployment は自動で ReplicaSet を用意して、ReplicaSet が Pod を作成するという流れになっています。

$ kubectl get replicaset

nginx コンテナイメージのバージョンを更新してみましょう。

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: http # コンテナ名
          image: nginx:1.16.1 # コンテナイメージ名
          ports:
            - containerPort: 80 # 公開されているコンテナのポート番号
$ kubectl get pods
$ kubectl get replicaset

この更新時には以下のような動作が行なわれています。

  • nginx:1.16.1 コンテナイメージを指定した ReplicaSet を作成する
  • 新しい ReplicaSet が起動する Pod 数に応じて nginx コンテナイメージを指定している ReplicaSet のレプリカ数を下げる

この仕組みにより、後方互換性を保った更新であれば断時間なしにリリースが行なえることになります。

Deployment は古い ReplicaSet を残します。
これにより変更後の Pod に何か問題があったとしても古い ReplicaSet が残っているのでロールバックを容易に行なえます。

$ kubectl rollout undo deployment web

もう一度同じコマンドを実行することでロールバックを取り消せます。

StatefulSet リソース経由で Pod を複数起動する

Deployment では Pod 名にランダムな文字列を付加することで一意性を担保していました。
HTTP サーバのような役割が同じコンテナで負荷分散するケースではこれで問題にならないでしょう。

一方 RDB のようにコンテナイメージは同一だが Read 用と Write 用で役割が分かれているケースではどうでしょう。
他にもホスト名が重要なケースでは Deployment を使うのは難しくなります。

そのようなケースのために StatefulSet を利用します。

statefulset.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  clusterIP: None
  selector:
    app: web
  ports:
    - port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  serviceName: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: http # コンテナ名
          image: nginx # コンテナイメージ名
          ports:
            - containerPort: 80 # 公開されているコンテナのポート番号

StatefulSet は特定の名前で Pod が動作していることが重要なケースで利用するので、Service リソース供え付けの負荷分散機能を利用することはありません。
そのため clusterIP: None という指定をすることで、Service リソースに IP アドレスを付けないようにできます。

$ kubectl apply -f statefulset.yaml