スマートキャンプ、エンジニアの入山です。
弊社で技術的挑戦の意味も込めて始めたKubernetes(k8s)も、小規模ながら運用を開始して1年以上が経ちました!
現在では、k8sでのインフラを採用したプロダクトが無事に本番リリースを迎え、ユーザーが本番稼働を行うまでになっており、躓きながらも少しずつ運用知見が溜まってきています。
今回は、k8sを実際に運用してわかった3つの知見を紹介したいと思います!
PodのNode配置が偏る
k8sではPodを新規作成する場合に、kube-schedulerが各ノードのリソース使用状況等から判断した最適なNodeへスケジューリング(配置)を行います。 しかし、このスケジューリング機能はPodの新規作成時にしか実施されないため、クラスタで障害が起こった際はもちろんのこと、Pod数や負荷状況などの要因によって新規スケジューリング時に偏りが発生した場合、Podがうまく各ノードに分散されずに偏ったままとなってしまいます。
弊社の運用時には、ノードの障害が起こっていないにもかかわらず、デプロイを繰り返しているうちに気付いたらPodがあるノードに偏っており、そのノードの負荷が高くなるだけでなく、冗長性・信頼性が低下してしまっていたことがありました。
このような状況に陥った場合は、k8s側では自動再スケジューリングが行われないため、何らかの手段を使って再スケジューリングを行う必要があります。
解決策
Deschedulerを利用することで、任意のタイミングでPodの再スケジューリングを行うことができます。
Deschedulerは動作として、設定したポリシーに該当するPodをNodeから削除させることで、kube-schedulerがPodを再スケジューリングする契機を作ります。現時点で5つのポリシーがあり、用途に合わせて柔軟にPodを再スケジューリングさせることができます。
尚、DeschedulerはJobで動作させる必要があるため、実際に運用する場合はCronJobで実行するなどの工夫が必要です。
GitHub - kubernetes-sigs/descheduler: Descheduler for Kubernetes
Deschedulerについては@ponde_mさんの資料がとてもわかりやすく、参考にさせていただきました!
ローリングアップデート時にダウンタイムが発生する
k8sはDeployment定義のDockerイメージのバージョンを変更してapplyすることで、自動でPodのローリングアップデートを行ってくれます。 このローリングアップデートは、新バージョンのPodが起動したことを契機に、旧バージョンのPodの削除が開始される仕組みになっています。
しかし、この「新バージョンのPodが起動した」状態は、Pod内のすべてのコンテナが起動したことだけを検知したものであり、各コンテナのプロセスがサービス開始できる状態になったことを保証するものではないため、コンテナ起動からサービス開始できる状態になるまでに時間を要する場合などでは切替時にダウンタイムが発生する可能性があります。
解決策
readinessProbeを利用したヘルスチェックの仕組みを入れることで、サービス開始できない状態のPodを検知し、トラフィックを送らないように制御することができます。これによって、ローリングアップデート時のダウンタイムも防ぐことができます。
readinessProbeは、コンテナに対して任意のヘルスチェックを設定することでサービス開始ができる状態か確認する機能で、ヘルスチェックに失敗したPodはServiceのエンドポイントから外される仕組みになっています。また、一度エンドポイントから外されたPodについてもヘルスチェックに成功した場合は、再度Serviceのエンドポイントへ登録されます。
ヘルスチェックの方法は、httpGet、exec、tcpSocketの3つから選ぶことができるため、コンテナに合わせて柔軟に設定ができます。
同じようなヘルスチェック機能としてlivenessProbeもあり、こちらはヘルスチェックに失敗した際にPodを破棄・新規作成する機能を備えています。
Configure Liveness, Readiness and Startup Probes | Kubernetes
readinessProbeについては以下の記事が参考になります!
Pod削除時にコンテナによってプロセスが終了するタイミングが異なる
k8sでは、Podの削除が開始されると各コンテナに対してTERMシグナル(正常終了要求)を送信し、TERMシグナルを受け取った各コンテナでプロセスの終了処理が開始されます。 この終了処理には、猶予期間(デフォルト30秒)が設定されており、猶予期間内にプロセスが正常終了されないコンテナについてはKILLシグナル(強制終了要求)が送信され、Podごと強制的に削除されます。また、TERMシグナルの送信と同時にPodをServiceから削除する処理も実行されます。
ここで問題になるのが、各コンテナのプロセスが終了するまでの時間に差が発生する可能性があることです。
Pod内のコンテナ同士で連携処理を行う構成においては、この時間差によって連携処理がエラーになる可能性があり、最悪の場合は強制終了される猶予であるの30秒の間ずっとエラーになる可能性もあります。また、Serviceからの削除が完了するまでは新規トラフィックの制御も行われないため、このような状況に陥った場合はエラー状態のままトラフィックを受け続けることになります。
弊社においてもこの事象が実際に過去発生していました。
弊社の例では、Pod内にRailsアプリコンテナとauth認証コンテナが存在しており、コンテナ間で認証処理を行っていたのですが、即時終了するauth認証コンテナに対して、Railsアプリコンテナが正常終了されず強制終了まで生き続けるという現象が起きていました。これにより、auth認証コンテナの終了後からRailsコンテナが強制終了されるまでの30秒間は、Railsからauthコンテナの認証ができないために処理がエラーになっていました。
解決策
preStopフックを利用してコンテナが終了する直前の動作を指定することで、各コンテナの終了動作やタイミングを操作することができます。
preStopフックは、コンテナごとに終了時のTERMシグナルが送信される前に任意のコマンド実行ができる機能で、プロセス終了のための任意コマンドの投入だけでなく、sleepなどのコマンドを実行することでTERMシグナルの投入タイミングを調整することもできます。尚、preStopフックが設定されている場合は、preStopフックが完了したタイミングでTERMシグナルが送信されます。
preStopフックの設定もヘルスチェック同様にhttpGet、exec、tcpSocketの3つから選ぶことができます。ただし、execを利用する際は同期処理である必要があり、非同期処理のコマンドを実行した場合は、即時にpreStopフックが完了したと判断されてしまうため、非同期処理を行う場合は終了処理に必要な時間を目安にsleepさせる必要があります。
尚、Pod終了時の詳しい挙動は、以下公式ドキュメントに記載されています。
Podの終了動作やpreStopフックについては、以下の記事が参考になります!
最後に
今回はk8sを実際に運用してみてわかった知見を紹介しました!
今回紹介したようなPodの配置や終了などのk8s側で自動的に制御されている部分の動作について、導入・設計時からしっかり把握しておくのはなかなか難しいと思います。
今回の対策で挙げた例以外にも同様に問題を解消する方法はあると思いますが、一例として参考になれば嬉しいです!