ReplicaSet Controllerは削除するPodをどうやって決めるのか

Kubernetes

これは「Kubernetes Advent Calendar 2021 (2)」 21日目の記事です。昨年も参加 させて頂いたのですが、1年経つの早すぎますね。

参加表明したものの何を書こうか全く決めていませんでしたので、最近勉強した内容をシェアさせてください。私が誤解している箇所やお気づきの点あれば、Twitterでおしえていただけると嬉しいです。

素朴な疑問

KubernetesでPodを作成したいとき、ご存知の通りDeploymentやReplicaSet を使う方法があります。DeploymentやReplicaSetのレプリカ数を増やした場合は、Schedulerのロジックによって適切なノードに追加のPodがスケジュールされるんだろうなと想像できます。Schedulerについては、いくつも有益な記事があります。例えば:

一方、ReplicaSetでレプリカ数を減らした場合に、削除するPodをどうやって選んでいるのか知りませんでした(知らないことに最近気づきました)。ググってみてもよくわからなかったので、今回調べてみます。

どこを調べるか

今回考えるのはDeployment及びReplicaSetを利用してPodを管理するときです。Deployment がReplicaSetを管理し、ReplicaSetはPodを管理するという依存関係にあるので、ReplicaSet Controllerを調べればよさそうです。

まず、ドキュメントだとこの辺に書かれていて、アルゴリズムも丁寧に説明されています。

https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#scaling-a-replicaset

でもこれで終わりだと記事にならないので、ReplicaSet Controllerのコードを読んでみようと思います。ReplicaSet Controllerのコードは pkg/controller/replicaset にあります。 release-1.23ブランチ を見ていきます。記事の中に該当箇所のコードを張ってますが、弊ブログのシンタックスハイライトがしょぼいせいで読みにくいと思います。ごめんなさい。

なお、KubernetesのContorollerや実装については、この辺の本がわかりやすいです。

調べてみた

syncReplicaSet()

syncReplicaSet() は、その名の通りReplicaSetの同期を行うメソッドです。

kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes
Production-Grade Container Scheduling and Management - kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes

まず与えられたkeyをnamespaceとnameに分割して、対象となるReplicaSetを取得しています。そして、このあたりでReplicaSetのSelectorを取得しています。

次にPodをフィルターします。まず、Namespace内の全Podを取得したあと、activeでないPodを除外し、さらに先程のSelectorにマッチするPodに絞り、filteredPods とします。

このfilteredPods を引数にして、manageReplicas()を呼び出しています。

manageReplicas()

manageReplicas() が、ReplicaSetのレプリカ数を確認・更新します。僕が知りたかった内容はこのメソッドに書いてそうです。

kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes
Production-Grade Container Scheduling and Management - kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes

まず、対象となるReplicaSetオブジェクトで定義されている最新のレプリカ数とfilteredPods の数を比較し、diffを取ります。今回調べているのは、レプリカ数を減らしたときなので、diff > 0 のケースです。

diff > 0 のケースでは、relatedPodsというものをgetIndirectlyRelatedPods() により取得しています。コードを読むとgetIndirectlyRelatedPods()は、対象のReplicaSetのOwnerが管理するPodをsliceを返していることがわかります。ここでいうOwnerとは、Deploymentのことです。

ん?それってrelatedPodsと何が違うの?という疑問が浮かぶかもしれません。これはDeploymentのローリングアップデートにおいて、新しいReplicaSetと古いReplicaSetが同時に存在するタイミングを考慮しています。

そして、次の処理でついに削除すべきPodを決めているようです!getPodsToDelete() を見てみましょう。

getPodsToDelete()

getPodsToDelete() で、削除すべきPodを決めます。

kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes
Production-Grade Container Scheduling and Management - kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes

manageReplicas() で呼び出されるときの引数をおさらいしておきます。

  • filteredPods: 当該ReplicaSetと同じNamespace内のPodで、現在active かつSelectorにマッチするPod
  • relatedPods: 当該ReplicaSetと同じOwnerが管理するPod。Deploymentのローリングアップデート時を考慮
  • diff: filteredPods の数と当該ReplicaSetのレプリカ数の差

diffの定義から[filteredPodsの数] – diff = [ReplicaSetのレプリカ数] (≥0)なので、diff ≤ [filteredPodsの数]が成り立つとわかります。diff = [filteredPodsの数] のときは、何も考えずfilteredPodsを消せばよい。一方 diff < [filteredPodsの数]のときはどのPodを削除するか優先度をつける必要があります。

if文の中身を見ていきます。まずgetPodsRankedByRelatedPodsOnSameNode() でRankとやらをつけたPodたちを取得し、それをsort.Sort()で並び替え、先頭からdiff個のPodを削除対象としているのが見て取れます。

getPodsRankedByRelatedPodsOnSameNode()

kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes
Production-Grade Container Scheduling and Management - kubernetes/replica_set.go at release-1.23 · kubernetes/kubernetes

getPodsRankedByRelatedPodsOnSameNode() では、第一引数として与えられたPodのslice(=filteredPods)をラップし、各PodについてRankを計算して返します。 なお、relatedPodsはRankを計算するのに使います。

podsOnNode というmapを定義しています。このmapのkeyはノード名が、valueにはrelatedPodsのうちkeyのノード上にあってかつactiveなPodの数が入ります。

次にPodのRank付けを行います。第一引数として与えられたPodのslice(=filteredPods)の各Podがどのノード上にあるか見ていき、そのノードをキーとして対応するpodsOnNodeの値をRankとします。そして、PodにRankと現在のタイムスタンプ(Now: metav1.Now())を付与して返します。現在の時間もソート時に考慮されます。

getPodsToDelete()のつづき

getPodsToDelete() の続きを見ていきます。getPodsRankedByRelatedPodsOnSameNode() によって返されたRank付きのPodの情報podsWithRanks をソートします。

ソートはsort.Sort()で行っているようです。またpodsWithRanksはcontroller.ActivePodsWithRanks型です。したがって、controllerパッケージのActivePodsWithRanks構造体の実装を見てみます。以下のようになっています。

ご存知の通り、sort.Interfaceはこのようになっていて、構造体のソートはLen(), Swap(), Less()を用意すれば実現できます。

ActivePodsWithRanks構造体に関しては、Len(), Swap()はまあ想像通りです。一方、Less()がややこしそうですが、コメントで丁寧に説明されています。まとめると以下のようになります。ソートされたPodのうち”小さい”ものから削除されていきます。

  1. Unassigned < assigned: NodeにアサインされていないPodが小さい
  2. PodPending < PodUnknown < PodRunning: PodのPhaseによる判定
  3. Not ready < ready: Not ReadyなPodが小さい
  4. lower pod-deletion-cost < higher pod-deletion cost: Pod Deletion Costが小さいPodが小さい
  5. Doubled up < not doubled up: 2重化されたPod(doubled-up pods)が小さい
  6. Been ready for empty time < less time < more time: Readyになった時間が一番若いPodが小さい
  7. Pods with containers with higher restart counts < lower restart counts: コンテナのRestart回数多いPodが小さい
  8. Empty creation time pods < newer pods < older pods: 作成時間が若いPodが小さい。作成時間が空のPodはもっと小さい

たくさんありますが、当然そうだろうなというものがほとんどです。例えば、PendingなPodの削除を優先したほうがいいだろうし、コンテナのRestartが少ないPodは安定して動いているので削除しないほうがいいだろうし。ただ5. Doubled up < not doubled upはなんでや?そもそもdoubled upとは?と私には理解できませんでした。

これがmanageReplicas()の節にも少し書いたDeploymentのローリングアップデートへの考慮です。以下のCommitログにいろいろ説明が載っていました。

関連するCommit 980b640

Prefer to delete doubled-up pods of a ReplicaSet · kubernetes/kubernetes@980b640
When scaling down a ReplicaSet, delete doubled up replicas first, where a "doubled up replica" is defined as one that is on the same node as an active...(続く)

タイトルは「Prefer to delete doubled-up pods of a ReplicaSet」となっており、「2重化されたPodの削除を優先するほうがいい」という感じの意味だと思います。なぜ、2重化されたPod(doubled-up pods)の削除を優先すべきなのか、その理由も丁寧に書かれています。以下抜粋です。

雰囲気訳:

  • ReplicaSetをスケールダウンする場合、2重化されたレプリカを先に削除します。「2重化されたレプリカ」とは、同じノードにあるレプリカを指します。共通のController(通常はDeployment)を持っているReplicaSetを「related」とみなします。
  • この変更の意図は、Deploymentのローリングアップデートを行う際に、新ReplicaSetのReadyなPodと一緒に配置されている旧ReplicaSetのPodを削除することにより、新ReplicaSetをスケールアップしつつ旧ReplicaSetをスケールダウンできるようにすることです。このローリングアップデートの動作の変更により、PodAffinityルールと組み合わせることで、ロールアウト中にデプロイメントのポッドの局所性を保持することができます。
  • この変更の恩恵を受ける具体的なシナリオは、DeploymentのPodが、タイプ「LoadBalancer」、外部トラフィックポリシー「Local」を持つサービスによって公開されている場合です。 このシナリオでは、ロードバランサーはヘルスチェックを使用して、サービスのトラフィックを特定のノードに転送すべきかどうかを判断します。 そのノードにサービスのローカルエンドポイントがない場合、そのノードのヘルスチェックは失敗し、最終的にロードバランサーはそのノードへのトラフィックの転送を停止します。 その間サービスプロキシはそのサービスに対するトラフィックをドロップします。したがって、ローリングアップデート中にトラフィックをドロップするリスクを低減するために、エンドポイントのノードローカリティを維持することが望ましいです。

なるほど、完全に理解しました。

manageReplicas()のつづき

最後にmanageReplicas()に戻って記事も終わりたいと思います。Rank付けをしたりソートもしたりして、削除対象のPodが決まりました。その名の通りpodsToDeleteという変数がそれを保持しています。そしてpodsToDeleteをforで回しつつ、goroutineを使って並列処理でPodを削除しているのがわかります。podControl.DeletePod()は、Pod IDを指定してPodを削除するメソッドです。

これで無事に、ReplicaSet Controllerは削除対象のPodを決定し、削除することができました!えらい!

さいごに

2021年ありがとうございました!ブログ更新めちゃくっちゃサボった年だったので、2022年は頑張りたいです。

参考

コメント