TT Lab
はじめる
学ぶ 学習パス コース

壊すのは私 — 仮説を先に書くカオス実験室

CPU は遅くし、メモリは殺す

TT Labで続きを見る

一言でいうと

CPUの上限とメモリの上限は、名前が似ているだけで、まったく違うかみ方をします。1つは遅くし、1つは殺します。

なぜ必要なのか

limitsを書くことは、たいてい習慣です。前の人が書いた値をコピーするか、審査で「上限を指定してください」と言われて、適当な数字を入れます。そうすると、2種類の事件が、数か月後にやって来ます。1つは「何も変えていないのに、急に遅くなった」で、もう1つは「再起動が繰り返されるのに、ログには何の手がかりもない」です。2つの事件は原因が違うのに、どちらもlimitsの1行から出てきました。

どう動くのか

Podとコンテナのリソース管理のドキュメントは、2つの上限の強制方式が互いに違うと、明確に区別しています。

cpuの上限は、スロットリングで強制されます。カーネルが決められた分だけCPUを使わせ、その分を使い切ると、次の周期が来るまで、そのコンテナを止めておきます。カーネルが強制する固い上限なので、コンテナは上限より多くは使えません。その代わり、死にはしません。ドキュメントが「ランタイムは、CPUを多く使ったからといって、Podやコンテナを終了しない」と、わざわざ別に書くほど、よく誤解される部分です。症状は、遅くなることだけで、プロセスもプローブもすべて生きているので、ログには何も残りません。

memoryの上限は、OOM killで強制されます。コンテナが上限を超えると、カーネルが終了させることがあります。ただし、ドキュメントは、これがリアクティブだと書いています。カーネルがメモリ圧迫を検知したときに起きるので、上限を超えたコンテナが、すぐには死なないこともあります。死ぬときは、終了コード137とOOMKilledの理由が残ります。

             집행 방식        증상                  남는 흔적
  cpu        스로틀링        지연만 늘어난다        없음(로그가 조용하다)
  memory     OOM kill        재시작·CrashLoop       exit 137 · OOMKilled

ここに、あまり知られていない落とし穴がもう1つあります。メモリを媒体として使うemptyDirボリュームは、コンテナのメモリ使用量として計算されます。同じドキュメントが、「kubeletは、tmpfsのemptyDirボリュームを、ローカルの一時ストレージではなく、コンテナのメモリ使用として追跡する」と書いています。一時ファイルを速く書くためにメモリボリュームを付けておくと、ディスクにファイルを書いていると思っていたコードが、メモリの上限を押し上げて、コンテナを殺します。そして、症状は「ファイルを書いている最中に死んだ」ではなく、ただのOOMKilledです。

requestsとlimitsの役割も、分けて覚える必要があります。requestsは、スケジューラーがどこに置くかを決めるときに使う値で、limitsは、kubeletとカーネルがどれだけ使わせるかを決めるときに使う値です。そのため、requestsだけを書くと、ノードが空いているときは、それより多く使ってもよく、limitsだけを書くと、Kubernetesが同じ値をrequestsにコピーして入れます。

もう1つ。上限を下げるときは、リクエスト量との関係も一緒に見る必要があります。上限はリクエスト量より小さくできないので、メモリの上限だけを40Miに下げようとしても、リクエスト量が64Miと書かれていると、APIサーバーがその変更をそもそも拒否します。実験を準備していると、「故障が起きるはずだったのに、コマンドが拒否される」場合によく出会いますが、そのときはたいてい、このような整合性のルールに引っかかっています。このラボのアプリがリクエスト量を十分低く設定しているのも、実験中にこの壁にぶつからないようにするためです。

現場での姿

最もよくある事故は、「安全のために」cpuの上限を100mに下げておいた配置です。普段は何も起きず、トラフィックが少し上がった瞬間に、応答時間が数倍に跳ね上がります。成功率はそのままなのでアラートが鳴らず、CPU使用率のグラフは、上限にぴったりくっついて平らになっているので、かえって「余裕があるように見えます」。このような事件は、スロットリングを直接測ってみるまで、原因を見つけられません。

逆方向の事故もあります。メモリの上限を十分に与えればOOMはなくなりますが、リクエスト量を一緒に上げておかないと、ノードが逼迫したときに、そのPodが先に追い出されます。ドキュメントは、リクエスト量を超えて使っているコンテナがあるPodは、ノードのメモリが足りないときに、エビクションされる可能性が高いと書いています。つまり、上限は個々のコンテナの上限を決め、リクエスト量は、リソースが足りないときの順序を決めます。実験で確認するときも、この2つを別々に触って初めて、何が原因だったかを言えます。

そのため、リソースに関する事件を扱うときは、症状でまず分岐します。ログが静かな遅延ならcpu側を、再起動と終了コード137ならmemory側を、先に見ます。この分岐をきちんとするだけで、調査時間が大きく減ります。逆に、「遅くなったからメモリを増やしてみよう」のような対応は、何も変えられないまま、コストだけを上げます。上限を触る変更は、ロールアウトを起こしてPodを作り直すので、変更した直後は、一時的に良くなったように見える錯覚まで生まれます。その錯覚にだまされないためには、変更の前と後を同じ方法で測った数字が必要です。

次のクイズで確認すること

cpuの上限とmemoryの上限の強制方式の違い、OOMの強制がリアクティブだという意味、メモリ媒体のemptyDirが何として計算されるのかを確認します。