KubernetesのPod AffinityとAnti-Affinityの仕組み:配置制御の設定方法と実装パターン

Published on: | Last updated:

Kubernetesを使い始めの頃って、PodがどのNodeに配置されるかなんて、正直あまり気にしてなかったな。とりあえず動けばいい、みたいな。でも、サービスが複雑になってくると、そうも言ってられなくなる。あいつとこいつは近くにいてほしい、とか、逆にこの二つは絶対に同じ場所に置きたくない、とか。そういう人間関係みたいなのが、Podの世界にも出てくるんだよね。

よく聞かれるのが、この「Podの配置をどうコントロールするのか」って話。今日はその中でも、Pod AffinityとAnti-Affinityについて、ちょっと頭の中を整理してみようかなと思う。まあ、メモみたいなもんだけど。

重点一句話

要するに、Podたちを「仲良しグループ(Affinity)」と「距離を置きたいグループ(Anti-Affinity)」に分けるための、Kubernetesスケジューラへの指示書みたいなもの。これを使うと、どのPodをどのNodeに置くか、もっと細かく制御できる。

Pod Affinity:こいつらは一緒にいてほしい

まずAffinityから。これは「親和性」とか訳されるけど、まあ「仲良しルール」だと思えばいい。特定のPod同士を、できるだけ同じ場所に配置するためのルール。場所っていうのは、同じNodeだったり、同じアベイラビリティゾーンだったりする。

なんでそんなことしたいかって?一番わかりやすいのは、通信のレイテンシかな。頻繁にやり取りするマイクロサービス、例えばWebサーバーとAPIサーバーが物理的に近くにいれば、ネットワークの遅延が減ってパフォーマンスが上がる。すごく単純な話。あとは、アプリとキャッシュサーバーを同じNodeに置くとか。そういうケースで使う。

AffinityとAnti-Affinityの概念的なイメージ
AffinityとAnti-Affinityの概念的なイメージ

書き方の基本

Podのspecに affinity っていうフィールドを追加して、そこにルールを書いていく。大事なのは、このルールには「強いルール」と「弱いルール」があるってこと。

ルールタイプ YAMLでの書き方 個人的な使い分けの感覚
強いルール(必須) requiredDuringSchedulingIgnoredDuringExecution 「これじゃないとダメ」。ルールを満たすNodeがなければ、Podは永遠にPending状態になる。本当にクリティカルな要件じゃなきゃ、あんまり使いたくない。後で困ることが多いから…。
弱いルール(推奨) preferredDuringSchedulingIgnoredDuringExecution 「できればこうしてほしい」。スケジューラは頑張ってくれるけど、無理なら別の場所にも配置してくれる。こっちのほうが断然使いやすい。柔軟性があるからね。大体はこれで事足りるはず。

あと、もう一つ重要なのが topologyKey。これで「どこまでを同じ場所と見なすか」を決める。例えば…

  • kubernetes.io/hostname: これが一番よく使うかな。Node名が同じ、つまり完全に同じNode。
  • topology.kubernetes.io/zone: 同じアベイラビリティゾーン(AZ)。AWSとかGCPでマルチAZ構成してるときに便利。
  • topology.kubernetes.io/region: 同じリージョン。まあ、あまり使わないかも。

このtopologyKeyの選び方で、Podをどれくらい近くに集めるかが決まるわけだ。

YAMLの例を見てみる

例えば、こんな感じのPodがあったとして。

apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - <a href="https://docs.confluent.io/operator/current/co-schedule-workloads.html" target="_blank" class="blogHightLight_css nobox">labelSelector</a>:
          matchExpressions:
          - key: app
            operator: In
            values:
            - backend
        topologyKey: "kubernetes.io/hostname"
  containers:
  - name: web-app-container
    image: nginx

これは、app: backend っていうラベルが付いたPodが動いているNodeに、この web-app Podを配置してね、という「強い」お願い。もしそんなNodeが一つもなければ、このPodは起動しない。うん、やっぱりちょっと怖いよね、この設定。

Pod Anti-Affinity:こいつらは離れていてほしい

次はAnti-Affinity。Affinityの逆。つまり「あいつとは一緒にしないで」っていうルール。これも可用性を考えると、めちゃくちゃ重要。

例えば、あるアプリケーションのレプリカを3つ作るとする。もしその3つが全部同じNodeに乗ってたら…そのNodeが落ちた瞬間に全滅だよね。それを防ぐために、Anti-Affinityを使って「同じアプリのレプリカは、それぞれ違うNodeに配置してね」とお願いする。これで、1つのNodeが死んでもサービスは生き残れる。

あとは、GPUとか、特定のハードウェアを大量に食うPod同士が、同じNodeでリソースの奪い合いをしないようにするためにも使える。これも大事。

YAMLの例はこんな感じ

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                    - redis
              topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis

これは、app: redis というラベルを持つPod同士は、できれば同じNodeに置かないでほしい、という「弱い」お願い。weight は優先度で、数字が大きいほど強く推奨される。複数の推奨ルールがあるときに、どれを優先するかを決めるために使う。この場合は、レプリカが3つあるから、スケジューラは頑張って3つの異なるNodeに配置しようとしてくれる。でも、もしNodeが2つしかなかったら?…その時は仕方なく、どこかのNodeに2つ配置されることになる。`preferred`だからね。これが`required`だったら、3つ目のPodは永遠に起動できない。

kubectlの出力でPodの配置を確認するイメージ
kubectlの出力でPodの配置を確認するイメージ

どうやって使うか、ちょっと考えてみる

じゃあ、実際にどういう流れで設定していくか。ステップ・バイ・ステップっていうよりは、思考の流れに近いけど。

まず、クラスターに何かしらのPodが動いてる状態を想像する。例えば、node-01app: backendっていうラベルの付いたPodがいるとする。

そこに、新しいWebサーバーのPodをデプロイしたい。このWebサーバーは、さっきのbackendと頻繁に通信する。だから、同じNodeに置きたい。こういう時にPod Affinityを使う。

Deploymentのtemplateに、さっきみたいな podAffinity の設定を書き加える。topologyKeykubernetes.io/hostname にして、labelSelectorapp: backend を指定する。これで、新しく作られるWebサーバーのPodは、node-01 を目指してスケジュールされるわけだ。

逆に、高可用性を担保したいデータベースのレプリカをデプロイする時はどうだろう。今度は podAntiAffinity の出番。Deploymentのtemplateに、「自分自身(この場合はapp: databaseみたいなラベル)と同じラベルを持つPodとは、違うNodeに置いてくれ」というルールを書く。topologyKeyはやっぱりkubernetes.io/hostnameでいいかな。そうすれば、レプリカが複数Nodeにいい感じにばらけてくれる。

トポロジーキーの階層(Node、Zone、Region)
トポロジーキーの階層(Node、Zone、Region)

反例と誤解釐清

よくある間違いとか、個人的にハマった点をいくつか。これは公式ドキュメントにはあまり書いてない、現場の感覚みたいなものだけど。

  • required ルールの乱用:さっきから何回も言ってるけど、requiredDuringSchedulingIgnoredDuringExecution は本当に諸刃の剣。例えば、「このPodはGPU搭載Nodeにしか置けない」みたいな絶対的な制約ならいい。でもPod間の関係性でこれを使うと、片方のPodが消えたり、ラベルが変わったりしただけで、もう片方のPodがスケジュール不能になる。クラスターがすごく脆くなる感じ。個人的には、9割くらいのケースで preferred で十分だと思う。
  • Anti-Affinityとコストの無視:Anti-Affinityで「全Podを別々のNodeに」って設定するのは簡単。でも、それってつまりPodの数だけNodeが必要になる可能性があるってこと。Nodeが足りなければ、Podは起動できない。結果的に、オートスケーラーがどんどん新しいNodeを立ち上げて、クラウド料金が思ったより高くなる…なんてことも。特に`required`でやると危険。可用性とコストのトレードオフは常に意識しないとダメだね。この辺りは、AWSのEKSのドキュメントとかでもベストプラクティスとして触れられてるけど、日本のユーザーブログとか見てても、コスト面での失敗談はよく見かける気がする。
  • Pod AffinityとNode Affinityの混同:似てるけど、全然違うもの。Pod Affinity/Anti-Affinityは「他のPodとの関係」で配置を決めるルール。一方でNode Affinityは「Nodeが持つラベル」で配置を決めるルール。「このPodはSSD搭載のNodeに置きたい」みたいなのはNode Affinityの仕事。どっちを使うべきか、ちゃんと整理してから書かないと、意図しない動きになる。
  • IgnoredDuringExecutionの意味を忘れる:ルールの名前の最後についてる IgnoredDuringExecution っていう部分。これは「一度スケジュールされたら、その後はルールを無視する」っていう意味。例えば、AffinityでPod AとPod Bを同じNodeに置いた後、Pod Bが何らかの理由で別のNodeに移動したとする。その時、Pod Aは元のNodeに居座り続ける。ルールはスケジューリングの時だけ評価されるから。この挙動を忘れていると、「あれ?ルール違反の状態になってる?」って混乱することがある。

結局のところ、こういうスケジューリングのルールって、クラスターという名の街で、交通整理をするようなものなんだと思う。ルールが厳しすぎると誰も動けなくなって大渋滞するし、ゆるすぎるとあちこちで衝突が起きる。Kubernetesの公式ドキュメントで基本を学びつつも、実際に動かして、時にはわざと壊してみて、「あ、こうなるんだ」っていう肌感覚を掴むのが一番大事なのかもしれない。

完璧な秩序と、実用的な混沌の間の、ちょうどいいバランスを見つける作業。それがインフラ管理の面白いところでもあるんだけどね。…さて、あなたはどういうルールでPodたちを整理してる?もしAffinityで面白い使い方とか、痛い目にあった話があったら、ぜひ聞いてみたい。

Related to this topic:

Comments