🐶

Datadog Agent(DaemonSet)の起動ができないパターンに対処した話

2024/05/08に公開

前提条件

本記事では以下のバージョンを前提としています。

  • EKS(EC2) 1.28
    • Cluster Autoscaler 1.28.2
    • Horizontal Pod Autoscaler
  • Datadog
    • Helm マニフェスト 3.33.1
    • Datadog Agent 7.46.0

Datadog Agent の起動台数が足りないアラートが発生

Datadog モニターから以下のアラートが届きました。

DaemonSet は全ての Node に1台ずつ起動するという特性があるので、 このモニターでは Node の数だけ DaemonSet が起動しているかどうかを監視しています。

監視内容は以下のクエリで、要約すると「直近40分間で理想とする DaemonSet Pod の起動数を満たせているかどうか」を監視しています。

min(last_40m):min:kubernetes_state.daemonset.desired{env:production} by {kube_daemon_set,kube_cluster_name} - max:kubernetes_state.daemonset.ready{env:production} by {kube_daemon_set,kube_cluster_name} > 0``

この時のアラートでは Node30台に対して Datadog Agent Pod が 29台しか起動していない状態でした。
ただ、このアラートは以前からサーバー縮小(Nodeの最小台数を減らす)などの作業で一時的に発生するケースが稀にありました。
また、数分後にはアラート状態から復旧していたことや、直近で Horizontal Pod Autoscaler の設定を変更して Node数が変動しやすくなっていたという経緯もあって一旦は深追いせずにクローズしました。

アラート再発・調査

数日後、同様のアラートが再発しました。
しかも、今度は1時間以上アラート状態が継続しており、一時的な Node数の増減だけが原因ではないと判断し調査することにしました。

以下のコマンドで全ての Pod を確認したところ、Pending 状態になっている Datadog Agent Pod を発見しました。

kubectl get pod -A
NAMESPACE  NAME  READY  STATUS  RESTARTS  AGE
~略~
monitoring  datadog-5w697  0/3  Pending  0  25m

Pending 状態の Pod の詳細を kubectl describe で確認したところ、 CPU の空き容量不足が原因で Pod のスケジューリングに失敗しているようでした。

kubectl describe po datadog-5w697 -n monitoring
~略~
Events:
  Type     Reason            Age                   From               Message
  ----     ------            ----                  ----               -------
  Warning  FailedScheduling  3m19s (x29 over 13m)  default-scheduler  0/32 nodes are available: 1 Insufficient cpu. preemption: 0/32 nodes are available: 32 No preemption victims found for incoming pod..

CPU の空き容量不足の実態を確かめるためにスケジューリングに失敗している Node の状態を確認したところ、予約済み含めて空き CPU 容量 9% になっていました。
Datadog Agent の Requests 設定は 全体の10%(200m) となっているので、空き CPU 容量 9% では収まりきらず起動できないということみたいです。

どの Pod が占拠しているのか確認するために kubectl describe node で詳細確認したところ、 Puma(Railsを動かすためのアプリケーションサーバー)が 15% × 4台 = 60% と、多めに枠を取っていることがわかりました。

$ kubectl describe node ip-10-0-221-239.ap-northeast-1.compute.internal の結果の一部
  Namespace                   Name                                             CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                                             ------------  ----------  ---------------  -------------  ---
  kube-system                 aws-node-z5nst                                   50m (2%)      0 (0%)      0 (0%)           0 (0%)         72m
  kube-system                 kube-proxy-bplll                                 100m (5%)     0 (0%)      0 (0%)           0 (0%)         72m
  logging                     fluent-bit-dsdf4                                 1m (0%)       0 (0%)      60Mi (0%)        0 (0%)         72m
  socialplus                  socialplus-puma-659fd5fd7f-gn59q                 300m (15%)    0 (0%)      450M (6%)        0 (0%)         73m
  socialplus                  socialplus-puma-659fd5fd7f-h244z                 300m (15%)    0 (0%)      450M (6%)        0 (0%)         73m
  socialplus                  socialplus-puma-659fd5fd7f-w5bjf                 300m (15%)    0 (0%)      450M (6%)        0 (0%)         73m
  socialplus                  socialplus-puma-659fd5fd7f-wgkdf                 300m (15%)    0 (0%)      450M (6%)        0 (0%)         73m
  socialplus                  socialplus-sidekiq-low-67c8959c4f-89vj9          200m (10%)    0 (0%)      750Mi (10%)      0 (0%)         73m
  socialplus                  socialplus-sidekiq-replicadb-7f78568fcd-mrsrm    200m (10%)    0 (0%)      1Gi (14%)        0 (0%)         73m

一時対応

Node の CPU 使用率の問題で Datadog Agent Pod が起動しないことがわかったので、手っ取り早くアラート状態を解除するために一番容量を食っている Puma の再起動をすることにしました。
Puma 再起動後、 Node 毎の Puma の配置が入れ替わったおかげで全ての Datadog Agent Pod が起動できるようになりました。
恒久対策をするまでの間、1~2週間に1回くらいの頻度で同じ状況が発生しており、どうやら Cluster Autoscaler による Node のスケールアウト/スケールインのタイミングにランダムで同じ状況が再現しているようでした。

DaemonSet なのに起動に失敗する謎

本来 DaemonSet は、全ての利用可能な Node に1台ずつ Pod を起動することを保証する挙動をするはずです。
DaemonSet の仕様について調べてみると、DaemonSet Pod に以下の Toleration を自動的に追加すると書いてあり、スケジューリング先の Node の Status が悪くてもスケジューリングを優先するような設定になっていることがわかります。

Toleration key Effect Details
node.kubernetes.io/not-ready NoExecute 健康でないNodeや、Podを受け入れる準備ができていないNodeにDaemonSet Podをスケジュールできるように設定します。そのようなNode上で動作しているDaemonSet Podは退避されることがありません。
node.kubernetes.io/unreachable NoExecute Nodeコントローラーから到達できないNodeにDaemonSet Podをスケジュールできるように設定します。このようなNode上で動作しているDaemonSet Podは、退避されません。
node.kubernetes.io/disk-pressure NoSchedule ディスク不足問題のあるNodeにDaemonSet Podをスケジュールできるように設定します。
node.kubernetes.io/memory-pressure NoSchedule メモリー不足問題のあるNodeにDaemonSet Podをスケジュールできるように設定します。
node.kubernetes.io/pid-pressure NoSchedule 処理負荷に問題のあるNodeにDaemonSet Podをスケジュールできるように設定します。
node.kubernetes.io/unschedulable NoSchedule スケジューリング不可能なNodeにDaemonSet Podをスケジュールできるように設定します。
node.kubernetes.io/network-unavailable NoSchedule ホストネットワークを要求するDaemonSet Podにのみ追加できます、つまりspec.hostNetwork: trueと設定されているPodです。このようなDaemonSet Podは、ネットワークが利用できないNodeにスケジュールできるように設定します。

結局、 DaemonSet なのに他の Pod より後からスケジューリングされ、Node の空き CPU が足りず起動に失敗する原因についてはわかりませんでした。
個人的な予想としては、

  1. Horizontal Pod Autoscaler が Puma Pod をスケールアウト
  2. Pod が増えたことによって、要求 Node 数が増加
  3. Cluster Autoscaler が Node をスケールアウト
  4. スケジューリング待ちだった Pod が一気に新しい Node に流れ込む
  5. 運が悪いと DaemonSet が後から起動することになる
    みたいなことが裏で起こっていたのではないかと考えています(もし知っている方がいれば教えていただきたいです)。

恒久対策

恒久対策として以下のような案がありました。

  • 優先起動処理を実装
  • Kubernetes の Resource Requests と Resource Limits を調整する
    • これ以上 Requests を下げると Pod の稼働に影響するので下げられない
  • リソース拡張
    • インスタンスタイプの変更(t3.large -> t3.xlarge)
      • コスト増だし、これで解決する確証がない
    • 最小Node数をあらかじめ増やす
      • 試したけど再発したので意味がなかった

この中でも確実に効果がありそうで手軽にできる PriorityClass の設定を行うことにしました。

PriorityClass の実装

PriorityClass の実装をするにあたり、事前にPodの優先度とプリエンプションを読んで理解しておくことをお勧めします。

まず、既存の構成で PriorityClass が実装されていないか以下のコマンドで確認しました。

$ kubectl get priorityclasses.scheduling.k8s.io
NAME                      VALUE        GLOBAL-DEFAULT   AGE
system-cluster-critical   2000000000   false            316d
system-node-critical      2000001000   false            316d

system-cluster-criticalsystem-node-critical が実装されていますが、これらは最初から kubernetes に用意されている PriorityClass で、退去されるとクラスターの動作に関わる重要な Pod を保護するための優先度クラスです。
デフォルトで用意されている PriorityClass を Datadog Agent Pod に紐づけるだけでも優先起動させるという要件は満たせそうなのですが、 Datadog Agent Pod が Node の存続に関わるほど優先度が高いかと言われるとそうでもなさそうなので別途 Datadog Agent のための PriorityClass を作成することにしました。

以下は PriorityClass を新しく設定するために設定した Helm の values.yaml です。

# Datadog Agent のための優先度設定を作成する
priorityClassCreate: true
priorityClassName: datadog-agent-priority
# リソース不足などで Datadog Agent を起動できない場合他の優先度の低い Pod の Preempt を認める
priorityPreemptionPolicyValue: PreemptLowerPriority
# デフォルトの priority が 0 なので、 10 で十分
priorityClassValue: 10

コメントに書いてある通り、PriorityClass がない Pod(Pumaなど)の優先度はデフォルトで0なので、優先度10で十分優先されることになります。
また、 priorityPreemptionPolicyValuePreemptLowerPriority に設定することで、より優先度の低い Pod を Preempt(差し替え)することを許可しています。
preemptionPolicy の他の設定についてはこちらをご覧ください。

上記の設定により、Puma などがリソースを占拠している状況でも優先度の低い Pod と差し替えることで Datadog Agent Pod の起動を実現できるようになりました。
その後、設定から2ヶ月経ちましたが、DaemonSet の起動数が足りないアラートは一度も起きていないです。

まとめ

  • DaemonSet はスケジューリングを最優先するが、絶対ではない
  • PriorityClass を適切に設定することで詳細な優先起動設定が可能
SocialPLUS Tech Blog

Discussion