スタディサプリにおけるKarpenterの導入トラブル振り返り - スタディサプリ Product Team Blog

スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

スタディサプリにおけるKarpenterの導入トラブル振り返り

スタディサプリにおけるKarpenterの導入トラブル振り返り

こんにちは。スタディサプリ小中高SREの@aoi1です。

スタディサプリでは、Kubernetesを利用しているのですが、Nodeの運用自動化のために2023年3月から本番環境を含む全環境でKarpenterを導入しています。

Karpenterのおかげで開発者体験を向上させることができたり、コスト削減を行うことができました。便利で良いことが沢山ある一方、本番環境で問題が発生するなどいくつかハマったこともありました。

本ブログでは私たちがハマったポイントを通じて、Karpenterの導入を検討している方、あるいは既に本番環境でKarpenterを運用している方にとって参考になればと思います。

Karpenterとは

KarpenterAmazon Web Sevice(AWS)が開発しているOSSで、「Karpenter simplifies Kubernetes infrastructure with the right nodes at the right time.」と公式ホームページに記載されています。

つまりKubernetesのNodeを適切なタイミングで適切なサイズにスケールすることができるツールです。

https://karpenter.sh/karpenter-overview.png

Karpenter公式ホームページに描かれている How It Works

Karpenterを利用するためにはNodePoolというリソースを用意し、NodePoolの設定に応じてNodeの選定が行われます。 各PodはそれぞれNodePoolと紐づいており、Podのリソース要求×NodePoolの設定によって最終的に利用するNodeが決定されます。

NodePoolは複数用意することができ、それぞれのNodePoolにはそれぞれのスペックを指定することができます。

例えばスタディサプリではJob専用のNodePool, GitHub Actions self-hosted runner専用のNodePoolを用意しています。JobのPodが起動する際にはJob用NodePoolに書かれたNodeのスペック内から最適なNodeが選定される、という仕組みになっています。

そしてなんと先日betaに昇格しましたね!おめでとうございます!

実はこのブログを書いている今、Karpenterのバージョンアップを計画している最中です。 用語などはbeta版にあわせていますが、起きたトラブルは全てalpha版で発生したものです。

スタディサプリでKarpenterを導入して得た効果

ハマりどころを説明する前に、まずKarpenterで得られた効果を説明します。

効果1: Nodeの起動が早い

私たちの環境ではGitHub Actionsのself-hosted runnerを利用しています。ジョブの増減がかなり激しいため、Node数をあらかじめ用意するということも難しく、適宜Node数も増減させる必要があります。

KarpenterはCluster AutoscalerよりもNodeの起動が早く、私たちのself-hosted runnerの起動時間を大幅に短縮することができました。 Karpenterの導入により、CIに関わる開発体験の向上にもつながりました。

また、本番環境でも極端にトラフィックが増えた際にNodeの供給がおいつかない、ということもなく今日まで運用できています。

効果2: コスト削減ができる

AWSではスポットインスタンスを活用することで大幅にコスト削減を行うことができます。

スポットインスタンスとは最大90%割引されるなど非常に安い代わりに、インスタンスの価格変動などによってインスタンスが突然終了することが頻繁に発生します。 頻繁にインスタンスが終了してしまうので、ステートフルなアプリケーションには使えないのはもちろんのこと、ステートレスなアプリケーションでも安全にシャットダウンできるように実装できている必要があります(実はこのスポットインスタンスの利用でハマったことがありますが、後ほど説明します)。

スポットインスタンス自体はKarpenterとは関係なく利用可能なものですが、Karpenterを利用することでスポットインスタンスの不便な点を補うことができます。

頻繁にインスタンスが終了してしまう、ということはNodeの起動が頻繁に行われる、つまりNodeの起動が早いKarpenterと相性が良いということです。 また、スポットインスタンスには在庫がなくなるということがありますが、このときオンデマンドなインスタンスに切り替えるときの速さもKarpenterを利用していれば数秒で完了します。

Karpenter導入後のトラブル振り返り

沢山良い効果がある一方、導入後に一定の苦労はしました。ここでは私たちがハマったポイントを紹介します。

ハマりポイント1: スポットインスタンスの利用

Karpenterを本番に導入する前に数ヶ月間ステージング環境で運用を試していました。ステージング環境では問題なくても本番環境で問題が発生する...あるあるですね。 Karpenterももれなく本番環境でいくつか問題が発生し、ハマりました。特にスポットインスタンスに関連した問題が多かったです。

いくつかトラブル事例を紹介する前に、スポットインスタンスについてもう少し詳しく説明します。

スポットインスタンスは安い代わりに空きキャパシティがなくなるとインスタンスが終了してしまう、というのは前述した通りですが、これをSpot Instance Terminationと呼びます。 このSpot Instance Terminationが引き起こしたいくつかのトラブルについてみていきましょう。

アプリケーションが504を頻発する

ある日を境に、アプリケーションの多くが504を頻発するようになってしまった、という報告がありました。これが最も大きいトラブルだったといえるでしょう。

原因は、スポットインスタンスの中断通知を受け取ってからインスタンスが終了するまでにアプリケーションがシャットダウンできなかったことでした。

スポットインスタンスインスタンスの価格変動やAWSの事情によって頻繁に入れ替わるのですが、入れ替わる前に通知を受け取ることができます。 スポットインスタンスの中断通知は、Amazon EC2がスポットインスタンスを停止または終了する2分前に発行される警告です(ref. https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html )。

2分もあれば十分なのではないかと思われたのですが、Pod Disruption Budget(PDB)の値が2分でアプリケーションが安全にシャットダウンしきることを妨げていました。

ではなぜPDBの値が適切に設定できていなかったのでしょうか?

新規マイクロサービスを作成する際の開発フローでテンプレートからマニフェストを作成できるようになっているのですが、 PDBの値が標準でmaxUnavailable: 1となっていたため、Pod数が多いアプリケーションにおいてはこの値が適切ではありませんでした。 Karpenterによって1Nodeあたりの集約効率が向上した結果、1Nodeがシャットダウンするときに大量のPodが影響を受けるようになり、maxUnavailable: 1を遵守していたのでは2分には到底間に合わないということが起こっていたのです。

また、KarpenterではEC2 instance rebalance recommendationをサポートしていません(ref. https://github.com/aws/karpenter/issues/2813 )。

これは何かというと、スポットインスタンスの中断通知よりも早く代替Nodeを用意するというものです。 Managed Node Groupでスポットインスタンスを利用している環境ではこの再調整によって2分よりももっと長い時間リバランスにかけることができます*1

しかし、Karpenterではサポートされていないため、なんとしても2分を守る必要があります。

KarpenterにEC2インスタンスの再調整に関する推奨事項をサポートして欲しいと思いつつ、私たちはPDBの値を調整することでこの問題を回避することができました。

アプリケーションが度々接続できなくなる

最初に開発者の方から「アプリケーションが度々接続できなくなる」という報告がありました。みなさんはここから原因を推測できるでしょうか?

Kubernetesという分散システムを利用しているとトラブルシューティングが難しいというのは読んでいるみなさんは身に染みているところかもしれませんが、 Karpenterを導入したことでNode自体が常に流動するようになり、これによりトラブルシューティングが難しくなりました。

本件の原因は、Amazon SQS キューの名前を変更したことにより、IAM Roleが適切に割り当たっていなかったことでした。 これだけ聞くと、本ブログの趣旨であるKarpenterと何の関係があるんだ!?と思われるかもしれませんが、一つずつ解説して行きます。

KarpenterはAmazon EventBridge→Amazon SQS→KarpenterでSpot Instance Terminationが通知される仕組みになっています(ref. https://karpenter.sh/v0.32/concepts/disruption/#interruption )。

ここで利用しているAmazon SQSの権限が足りず、Karpenterがキューを読み取ることができていませんでした。

これにより、Spot Instance Terminationが発生してもKarpenterには伝わらず、安全にNodeをdrainできないまま強制的にNodeが失われる状態になっていました。

安全にNodeがシャットダウンできないので、アプリケーションももちろん安全にシャットダウンできません。 Nodeが突然失われる状況が頻発し、アプリケーションが接続できなくなっていました。

権限が足りていなかったことが原因だったため権限追加で問題を解決することができましたが、表題の問い合わせから問題解決までがなかなか難しかった一件でもあります。

Jobに関連するPodが消えてしまう

これはトラブルというよりは困りごとに近いのですが、私たちはJob用にNodePoolを用意している関係上Jobが完了時にNodeごと消えるということがよくあります。

Jobの実行頻度は低いため、Jobを利用するタイミングでNodeが起動し、Jobの完了とともにNodeがシャットダウンします。 こうなると、Jobが失敗した時にPodを確認したくてもPodが消えている(failedJobsHistoryLimitの値が意味をなさない)という状況が頻発します。

Amazon CloudWatchでログを収集しているため、ログが参照できなくなるということはありませんが、kubectl logsで簡単にログを参照しづらくなっていました。

現状これに対する対応策はありませんが、Argo Workflowsを利用することでこの問題を解決できるかもしれないと考えています(絶賛導入検証中!)。

ハマりポイント2: デカすぎるNode

Kubernetesでは公式に1Nodeあたり110Podまでを推奨値としています。

しかし私たちのステージング環境ではコストメリットを重視し、1Nodeあたり詰め込めるだけPodを詰め込んでいます。 Kubernetes公式推奨値から外れているため、Karpenterのハマりどころというよりは「推奨値を外れるとどうなるのか」という話になりますが、 Karpenterを利用していると簡単にNodeにPodをつめこめてしまうため、参考までにどうなるか書いておきます。

Datadog Agentがメモリ不足になる

Datadog Agentはさまざまなメトリクスを取得するためのPodで、1Nodeにつき1つ動かします。

Datadog Agentは自身がスケジュールされているNodeのPod数に応じて使用するメモリ量が変わるため、 大量のPodが動いているNodeにDatadog Agentを動かすとメモリ不足になります。 Nodeのサイズの違いが大きくDatadog Agentのメモリ使用量も一律に設定することはできません。

今の所大きな問題にはなっていないため、私たちはDatadog AgentのOOMは許容することにしています。

1Nodeを再起動したときの影響が大きい

1Nodeに大量のPodが動いていると、1Nodeを再起動したときに影響が大きくなります。

大量のPodがNode間の移動を行うため、安全にシャットダウンする設計がうまく効かず、タイムアウトなど発生してしまう可能性があります。 ステージング環境はある程度落ちても問題ない、としてもどれくらい大きいNodeを使用するかは検討した方が良いでしょう。

ハマりポイント3: preemptionの発生

preemptionが何かを知らない方もいらっしゃるのではないでしょうか。私もKarpenterが導入されるまでは知りませんでした。

preemptionとは日本語で「強制排除」という意味で、より優先度の高いPodをスケジュールするために、優先度の低いPodが強制的に終了されることを指します。 Karpenterでは使用予定ギリギリのスペックのNodeを選定するため、Podが想定より少し多くリソースを使用するなどでpreemptionが発生しやすくなります。

特に私たちが使用しているJob用のNodePoolでは、普段Nodeがそもそも存在しないため、Jobが実行されるたびにNodeが起動します。

前述した通り、ギリギリのスペックでNodeを起動するため、preemptionが発生し、JobのPodがNodeから追い出されてしまうことが度々発生するようになりました。

preemptionの発生源となっている理由がdatadog-agentのPodがスケジュールできていないことによるものであり、Node内のメトリクスをできるだけ取得するためにはdatadog-agentを最優先でスケジュールする必要があります。 そのためPodの優先度は変えれない一方、JobのPodがスケジュールできないとなるとJobがエラーとなってしまいます。

現状この問題に対してできる手立てとしてはJobのリトライ回数を増やすことや、Jobのリソースをチューニングし、よりキャパシティのあるNodeが選定されやすくすることです。 しかしDatadog Agentのリソースが想定外に増えることを防げないことや、たまたま同時に別のJobが起動してしまうなどpreemptionの発生を完全に防げるものではありません。

Job用のNodePoolの利用をやめるという方法もありますが、今の所Jobのリトライ回数を増やすことで対応しています。

学びと実践へのヒント

かなりハマりにハマったKarpenterですが、現在はかなり安定して運用できています。 ハマりポイントから、今後Karpenterの導入を検討している方に向けてこういうことに注意をすれば良いのではと思ったことをまとめます。

アプリケーションが安全にシャットダウンできるように実装すること。また、マニフェストの設定としても安全なシャットダウンが実施できるようになっていること

私たちの環境ではステートレスなアプリケーションがメインで、かつ実装としては安全にシャットダウンできるようになっていたためこれだけのトラブルで済んだのかなと思います。

本番導入前に万全を期したいのであれば、一定期間ステージング環境でも本番同様に監視をすることでアプリケーション接続断に気づけたかもしれません。また、私たちは利用しませんでしたが、AWS FIS を使用してスポットインスタンスの中断をテストすることもできるようです。

特にスポットインスタンスを利用するとかなりの頻度でNodeが増減することになるため、重要なコンポーネントはスポットインスタンスを使わないという選択肢もありだと思います。

Nodeに関するメトリクスやEvent、Podのログなどを収集できているようにしておくこと

これまで半固定だったNodeという要素が流動的になったことで、トラブルシューティングがかなり難しくなったと感じました。

「アプリケーションが接続できなくなった」という抽象的な問い合わせからSQSの権限が原因だと特定できたのは、ひとえにさまざまなメトリクスを収集できていたからだと思います。

使用するNodeは環境にあわせて選定すること

Karpenterではこまかく使用するNodeのスペックを指定することができます。

例えば私たちはデカすぎるNodeを使わないよう本番環境の標準設定ではNodeのメモリ上限を72GiBに指定するなど、さまざまな指定をしています。

皆様も導入の際には環境に合わせて細かくスペックを指定することをおすすめします。

おわりに

Karpenterの導入の参考になったでしょうか。以前はバージョンをあげると壊れてしまうようなことがありましたが、先日betaに昇格したこともあり今ではかなり安定しています。 それでは良いKubernetesライフを!

*1:Karpenterのリバランスに関しては株式会社MIXI みてね事業部の技術ブログが参考になります。Karpenterを導入した話