スロットリングの解除: 有効な修正が不具合の原因になってしまった理由

この投稿は、2部構成となっているシリーズの第2部です。

第1部では、CFS-Cgroup の帯域幅の制御が関係したスロットリングに関する大きな問題が認識された状況について説明しました。この問題を明らかにするために、問題が発生した環境を再現し、git bisect を使用して、問題が最初に発生したバージョンを特定しました。しかし、このバージョンにはまったく問題がないように見えたので、状況はより複雑になりました。この投稿では、このスロットリングの問題に関する根本原因をどのように突き止めたのか、そしてその解決方法について説明します。

Photo of highway at night

Photo by Jake Givens on Unsplash

多数のスレッドを使用した、複数の CPU のスケジュール

第1部で説明した概念モデルは正確ではありましたが、カーネルのスケジューラの複雑性を完全に表すものではありませんでした。スケジューリングプロセスにあまり詳しくない状態でカーネルについての資料を参照すると、カーネルは使用された時間の合計を追跡しているのだと思われるかもしれませんが、そうではなく、カーネルはまだ利用可能な時間の合計を追跡しているのです。その仕組みは次のとおりです。

カーネルのスケジューラは cfs_bandwidth->quota にあるグローバルなクォータバケットを使用します。スケジューラはこのクォータのスライスを、必要に応じて各コアに割り当てます (cfs_rq->runtime_remaining)。このスライスの長さはデフォルトで5ms に設定されていますが、この値は kernel.sched_cfs_bandwidth_slice_us sysctl tunable を使用して調整できます。

IO でのブロックなど、cgroup 内のすべてのスレッドが特定の CPU で実行不可能になっている場合、そのカーネルがグローバルなバケットに対して返すのは、この余剰クォータの1ms を除くすべてのクォータです。カーネルが1ms を残すのは、これにより多数の高性能なコンピューティングアプリケーションでグローバルバケットのロックの競合が低減されるためです。スケジューラは、期間の終了時に残っているコアローカルのタイムスライスを期限切れにし、グローバルなクォータバケットを補充します。

ともあれ、これがコミット 512ac999 およびカーネル v4.18 以降の状態です。

分かりやすくするために、独自のコアに固定された2つのワーカースレッドがある、マルチスレッドのデーモンの例を挙げます。上のグラフは、cgroup のグローバルなクォータを経時的に示しています。このグラフは0.2 CPU に相関した20ms のクォータで開始されています。中間のグラフは CPU のキューごとに割り当てられたクォータを示し、下のグラフはワーカーが実際にいつ CPU で実行されたのかを示します。

Multi-threaded daemon with two worker threads

 

時間 アクション
10ms
  • ワーカー1に対する要求が送信されます。 
  • クォータのスライスがグローバルクォータから、CPU 1の CPU ごとのキューに転送されます。  
  • ワーカー1 がこの要求に応答処理するのに要した正確な時間は5ms です。
17ms
  • ワーカー2に対する要求が送信されます。 
  • クォータのスライスがグローバルクォータから、CPU 2の CPU ごとのキューに転送されます。

この場合、ワーカー1が要求への応答に正確に5ms を使用するというのは極めて非現実的です。その要求で別の長さの処理時間が要求されていたとしたらどうなるでしょうか?Multi-threaded daemon with two worker threads

時間 アクション
30ms
  • ワーカー1に対する要求が送信されます。 
  • ワーカー1が要求の処理に必要な時間は1ms のみであるため、CPU 1の CPU ごとのバケットには4ms が残ります。
  • CPU ごとの実行キューでは時間が残りますが、CPU 1には実行可能なスレッドがこれ以上無いため、余っているクォータをグローバルなバケットにもどすようにタイマーが設定されます。ワーカー1が実行を停止した後、このタイマーは7ms に設定されます。
38ms
  • CPU 1に設定された余剰タイマーがトリガーされ、1 msのクォータ以外をグローバルクォータのプールに返します。
  • これにより、CPU 1には1 ms のクォータが残ります。
41ms
  • ワーカー2が長い要求を受け取ります。
  • 残っている時間がすべて、グローバルバケットから CPU 2の CPU ごとのバケットに転送され、ワーカー2がそれをすべて使用します。
49ms
  • CPU 2のワーカー2が、要求を完了しない状態でスロットリングの対象になります。
  • この状態は、CPU 1がまだ1ms のクォータを保有しているにも関わらず発生します。

2コアのマシンでは1ms の影響はそれほど大きくないかもしれませんが、コア数が多いマシンでは、こうしたミリ秒の積み重ねが大きな影響を与えます。88コア (n) のマシンでは、期間ごとに 87 (n-1) ミリ秒の遅延が発生する可能性があります。この結果、87ms (870ミリコアまたは0.87 CPU) が使用できなくなる可能性があります。このようにして、過剰なスロットリングによってクォータの使用量の低下が発生することが明らかになりました。

8コアや10コアのマシンが大型だと考えられていた当時、この問題が認識されることはほとんどありませんでしたが、コア数の多いマシンが一般的である現在では、この問題が表面化するようになったのです。そのため、同じアプリケーションであっても、コア数が多いマシンで実行した場合の方がスロットリングが増えていたのです。


注意: アプリケーションが100ms のクォータ (1 CPU) のみ保有しており、カーネルが5ms のスライスを使用した場合、クォータがなくなるまでにそのアプリケーションで使用できるコアは20のみになります (100 ms / 5 ms のスライス = 20スライス)。そのため、88という大規模なコアのうち、別の68コアでスケジュール設定されたすべてのスレッドはスロットリングの対象になり、これらのスレッドは実行されるまで、余剰の時間がグローバルバケットに返されるのを待機しなければなりません。

長い待機時間のバグを解消する

では、なぜクロックのずれによるスロットリングの問題を修正するパッチによって、別のスロットリングが発生したのでしょうか。このシリーズの第1部では、問題があるコミットとして512ac999が特定されました。このパッチを確認したところ、以下が確認されました。

-       if (cfs_rq->runtime_expires != cfs_b->runtime_expires) {
+       if (cfs_rq->expires_seq == cfs_b->expires_seq) {
               /* extend local deadline, drift is bounded above by 2 ticks */
                cfs_rq->runtime_expires += TICK_NSEC;
       } else {
                /* global deadline is ahead, expiration has passed */
                cfs_rq->runtime_remaining = 0;
        }

このプレパッチコードは、各 CPU の期限切れの時間がグローバルな期限切れの時間と一致した場合 (cfs_rq->runtime_expires != cfs_b->runtime_expires) にのみ、ランタイムを期限切れにしていました。カーネルについての測定を行うことにより、私は自分が使用しているノードでは、この状態がまったくといってよいほど発生しないことに気づきました。そのため、こうした1ミリ秒が期限切れになることはなかったのです。このパッチはこのロジックを、クロック時間ベースから期間シーケンス数へと変更して、カーネルにおける長期の待ち時間というバグを解消していました。

このコードの元々の意図は、期間の終了時に残っていた CPU ローカルの時間をすべて期限切れにすることでした。コミット512ac999はこの問題を修正していたので、クォータは適切に期限切れになっていました。そのため、クォータが期間ごとに厳密に制限されることになったのです。

CFS-Cgroup の帯域幅制御が作成された当初、スーパーコンピュータでの時間共有は重要な機能の1つでした。この厳密な適用は CPU にバインドされたアプリケーションに対して効果的でした。これらのアプリケーションは、期間ごとにすべてのクォータを使用しており、期限切れになるクォータは存在しなかったからです。小さなワーカースレッドが多用される Java の Web アプリケーションでは、これは期間ごとに多数のクォータが期限切れになる (一度に1ms) ということを意味します。

解決策

状況を把握したら、次は問題を修正しなければなりません。問題へ対処するために、いくつかのアプローチが取られました。

最初に実装されたのは「時間の繰り越し」でした。これは、期限切れになったクォータを確保して、次の期間で使用できるようにするものです。この対処は、期間の境界におけるグローバルバケットのロックで thundering herd 問題が発生する原因となりました。私たちは次に、クォータの期限切れを期間ごとに個別に設定できるようにしようとしました。すると、バーストアプリケーションでクォータの消費が増える場合があるという別の問題が発生しました。スレッドが実行できなくなった場合に、余剰のクォータをすべて返すことも試しましたが、多数のロック競合やパフォーマンス上の問題となりました。CFS スケジューラの作者である Ben Segall は、コアローカルの余剰を追跡し、必要な場合にのみ再度呼び出すことを提案しましたが、この解決策には、それ自体にコア数の多いマシンにおけるパフォーマンスの問題がありました。

実は、解決策は最初から提示されていたのです。2014年以降、CFS CPU の帯域幅の制約に関する問題は誰も認識していませんでした。そして期限切れのバグがコミット512ac999で修正されると、多数の人々がスロットリングに関する問題を報告し始めました。

では、この期限切れのロジックを完全に削除すればよいのではないでしょうか。これこそが、最終的にメインラインカーネルに対して再度適用されたソリューションでした。期間ごとにクォータの時間を厳密に制限する代わりに、長期間にわたって平均的な CPU 使用量を厳密に強制することにしました。加えて、アプリケーションがバーストできる量は、CPU キューごとに1ms に制限されました。対策に関するすべての対話と、5つの後続パッチについては、Linux カーネルについてのメーリングリストのアーカイブを参照してください。

これらの変更は、現在5.4以上のメインラインカーネルに適用されており、以下のように、使用可能なカーネルの多くにバックポートされています。

  • Linux-stable: 4.14.154+, 4.19.84+, 5.3.9+
  • Ubuntu: 4.15.0-67+, 5.3.0-24+
  • Redhat Enterprise Linux:
    • RHEL 7: 3.10.0-1062.8.1.el7+
    • RHEL 8: 4.18.0-147.2.1.el8_1+
  • CoreOS: v4.19.84+

結果

この最適なシナリオでは、今回の修正により Indeed アプリケーションの各インスタンスで使用可能な CPU を0.87増やすか、必要な CPU クォータで同じ量を減らすことができます。こうした利点により、アプリケーション密度の増加の制約が解消され、クラスタ全体におけるアプリケーションの応答時間が短縮されます。Decrease in required CPU load

問題の緩和方法

以下は、ご使用のシステムにおいて、CFS-Cgroup 帯域幅の制御によりスロットリングに関する問題が発生するのを防ぐための対策です

  • throttled percentage を確認する
  • カーネルをアップグレードする
  • Kubernetes を使用している場合は CPU クォータをすべて使用して、cgroup でスケジュール設定可能な CPU の数を減らすようにする
  • 必要に応じてクォータを増やす

現在進行中のスケジューラの開発

Yandex の Konstantin Khlebnikov は、Linux カーネルのメーリングリストで「バーストバンク」を作成するためのパッチを提案しています。これらの変更は、この投稿で説明したとおり期限切れのロジックが削除されたことから現在は実現可能になっています。バーストに関するこれらのパッチは、より多数のアプリケーションのセットに小さなクォータの制限を設定することができるようにします。このアイデアに興味を持った方は、Linux カーネルのメーリングリストに参加し、サポートをお願いします。

Kubernetes でのカーネルのスケジューラのバグについてさらに詳しく把握するには、GitHub での以下の注目すべき問題をご覧ください。

  • CFS クォータが不要なスロットリングの原因になる可能性がある (GH #67577)
  • Kubernetes 内から CFS 期間を設定する (GH #51135)
  • CPU の設定によって CFS クォータの設定を解除する (GH #70585) (GH #75682)

ご不明な点は、お気軽に @dchiluk 宛てにツイートしてください。

スロットリング解除: クラウドにおける CPU の制限の修正

この投稿は、2部構成となっているシリーズの第1部です。

私たちのチームは、2019年に、Kubernetes、Docker、Mesos など、ハードリミットが設定されたほぼすべてのコンテナオーケストレータに影響する CPU スロットリングの問題を解決しました。この過程で、応答遅延で大きな問題があった Indeed のあるアプリケーションでは、遅延が2秒から30ミリ秒に短縮されました。この2部構成のシリーズでは、根本的な原因を見つけ、最終的にソリューションへとたどり着いた取り組みについて説明します。

10 MPH road sign

Photo by twinsfisch on Unsplash

この問題が発生したのは昨年、Linux カーネルの v4.18 がリリースされてから間もなくのことでした。Indeed のある Web アプリケーションで応答時間が長くなっていることを把握したのですが、CPU の使用量を確認しても問題はありませんでした。さらに調査した結果、この現象は、CPU スロットリングが高い期間と直接関係があることが明らかになりました。何かが正常に動作していなかったのです。通常の CPU 使用量でスロットリングが高くなることはあり得ません。最終的には犯人が見つかったのですが、まずは問題のメカニズムについて確認する必要がありました。

背景: コンテナの CPU の制約の仕組み

ほぼすべてのコンテナオーケストレータは、kernel control group (cgroup) メカニズムによってリソースの制限を管理しています。コンテナオーケストレータで CPU のハードリミットが設定されている場合、カーネルは Completely Fair Scheduler (CFS) Cgroup の帯域幅制御を使用して、ハードリミットを適用しています。 この CFS-Cgroup の帯域幅制御のメカニズムは、クォータと期間の2つの設定を使用して CPU の割り当てを制御しています。アプリケーションで、割り当てられた CPU クォータが一定の期間使用された場合、そのアプリケーションは次の期間までスロットリングの対象となります。

cgroup に関する CPU のすべての指標は /sys/fs/cgroup/cpu,cpuacct/<container> にあります。クォータおよび期間の設定は、cpu.cfs_quota_us および cpu.cfs_period_us にあります。

CPU metrics for a cgroup

また、スロットリングの指標は cpu.stat で確認できます。cpu.stat には以下が記載されています。

  • nr_periods – cgroup 内のいずれかのスレッドが実行可能だった期間の数
  • nr_throttled – アプリケーションがクォータ全体を使用し、スロットリングの対象となった実行期間の数
  • throttled_time – cgroup 内の個々のスレッドがスロットリングの対象だった期間の合計

応答時間の悪化を調査していたとき、あるエンジニアが、応答時間が長いアプリケーションではスロットリング期間 (nr_throttled) が非常に長いことに気づきました。私たちは nr_throttlednr_periods で除算し、スロットリング期間が非常に長いアプリケーションを特定するための重要な指標を見つけました。この指標は「スロットリング率」と呼ばれています。throttled_time は、スレッドの使用程度に応じてアプリケーション間で開きがある可能性があるため、使用しませんでした。

CPU 制限の概念モデル

CPU 制限の仕組みを理解するために、ある例を見てみましょう。シングルスレッドのアプリケーションが、cgroup の制限が設定された CPU で実行されているとします。このアプリケーションが要求を処理するには 200 ミリ秒が必要です。制限がない状態では、応答は次のグラフのようになります。

A request comes in at time 0, the application is scheduled on the processor for 200 consecutive milliseconds, and responds at time 200ms

ここで、アプリケーションに0.4 CPU の CPU 制限を割り当てます。アプリケーションに対して100ミリ秒の期間ごとに 40ミリ秒の実行時間が付与されるということを意味します。CPU に他のタスクがない場合も含まれます。これで200ミリ秒の要求は完了まで440ミリ秒かかるようになりました。

A request comes in at time 0, the application runs for 5, 100ms periods in which it runs for 40ms, and then is throttled for 60 in each period. Response is completed at 440ms

1,000ミリ秒間の指標を収集すると、この例の統計は次のようになります。

指標 理由
nr_periods 5 440ミリ秒~1000ミリ秒まで、アプリケーションは何もしなかったので実行できなかった
nr_throttled 4 アプリケーションは実行可能ではなくなったため、5つ目の期間にはスロットリングが適用されなかった
throttled_time 240ミリ秒 アプリケーションは100ミリ秒ごとに40ミリ秒しか実行できず、60ミリ秒の間スロットリングの対象だった。4つの期間がスロットリングの対象だったため、スロットリングの合計期間は4に60を乗算した240ミリ秒になる。
throttled percentage 80% 4つの nr_throttled を5つの nr_periods で除算

しかし、この統計は概念上のものであり、現実のものではありません。この概念モデルにはいくつかの問題があります。まず、私たちが生きているのはマルチコア、マルチスレッドアプリケーションの世界です。次に、この統計が完全に正しかったとしても、問題が発生しているアプリケーションでは CPU クォータが枯渇するまでスロットリングは開始されません。

問題の再現

私たちは、問題が実際に存在し、修正の必要があるとカーネルのコミュニティを説得するには、簡明な再現のためのテストケースが役に立つということを理解していました。そこで多数のストレステストと Bash スクリプトを試行しましたが、動作の確実な再現は難航しました。

この状況を打破できたのは、多くの Web アプリケーションでは非同期のワーカースレッドが使用されていることに気づいたからでした。このスレッドモデルでは、各ワーカーに対して完了すべき小規模のタスクが付与されます。たとえば、こうしたワーカーは IO などの少量の作業を処理します。こうしたワークロードを再現するために、私たちは C で Fibtest という小規模な再現のためのテストを作成しました。予測できない IO を使用するのではなく、フィボナッチシーケンスと休止期間を組み合わせて使用することで、こうしたワーカースレッドの動作を再現し、高速なスレッドと低速なワーカースレッドを区別しました。高速スレッドはフィボナッチシーケンスを可能な限り反復処理しますが、低速スレッドは100 の反復を処理した後に、10ミリ秒休止します。

スケジューラに対して、これらの低速なスレッドは、小規模な作業を実行した後休止するという、非同期ワーカースレッドにより近い形で動作しました。確認ですが、最終的な目標はフィボナッチの反復処理を最大にすることでなく、低い CPU 使用量と同時に高いスロットリングが実施される状況を確実に再現するテストケースを作成することです。こうした高速スレッドと低速スレッドをそれぞれの CPU に固定することにより、ついに、問題となっている CPU スロットリング動作を再現するテストケースを作成することができました。

最初のスロットリングの修正と不具合

次のステップは、カーネルで git bisect を実行するために Fibtest を条件として使用することでした。このテクニックを利用することで、過剰なスロットリングの原因となっているコミットである512ac999d275 “sched/fair: Fix bandwidth timer clock drift condition” を素早く検出することができました。この変更は、4.18 カーネルで導入されたものです。このコミットを削除した後にカーネルをテストすると、低い CPU 使用量で高いスロットリングが実行されるという問題が修正されました。しかし、このコミットと関連するソースを分析したところ、この修正は完全に有効なようでした。さらに混乱することに、このコミットは不慮のスロットリングを修正するために導入されていたのです。

このコミットが修正した問題は、たとえば実際の CPU 使用量とは関係がないように見えるスロットリングでした。原因はコア間のクロックのずれであり、これにより、ある期間のクォータをカーネルが早めに有効期限切れにしていたのです。

幸い、この問題が発生するのは非常にまれでした。ノードの大半がすでに修正が適用されたカーネルを実行されていたためです。しかし運悪く、1つのアプリケーションでこの問題が発生してしまいました。このアプリケーションはほとんどアイドル状態で、4.1 CPU が割り当てられていました。その結果、CPU の使用量とスロットリング率のグラフは次のようになりました。

CPU usage graph with 4 CPUs allocated and usage not exceeding .5 CPU

4 CPU が割り当てられ、使用量が 0.5 CPU を超えていない CPU 使用量のグラフ

Graph of throttled percentage showing excessive throttling

過剰なスロットリングを示すスロットリング率

コミット512ac999d275 ではこの問題を修正し、多数の Linux-stable ツリーにバックポートされました。このコミットは RHEL、CentOS、Ubuntu など、大半の主要なディストリビューションカーネルに適用されました。その結果、一部のユーザーは、おそらくスロットリングの改善を確認しているはずですが、その他のユーザーの多くは、この調査を開始することになった問題を目にしている可能性があります。

取り組みのこの時点では、大きな問題を確認し、再現のためのテストを作成し、問題となっているコミットを特定しました。このコミットにはまったく問題がないように見えますが、悪影響をもたらす副次的効果がありました。このシリーズの第2部では、根本原因についてさらに説明し、概念モデルを更新して CFS-Cgroup CPU 制約の実際の仕組みを説明し、カーネルに対して最終的にプッシュされたソリューションについて説明します。

The FOSS Contributor Fund: Forming a Community of Adopters

In January 2019, Indeed launched a new program that democratizes the way we provide financial support to open source projects that we use. We call it The FOSS Contributor Fund. The fund enables Indeed employees who make open source contributions to nominate and vote for projects. Each month, the winning project receives funding. This program encourages support of projects we use and more engagement with the open source community.

foss contributor fund logo

Join our community of FOSS Fund Adopters

Now, we want to help other companies start similar funds. Our goal is to collaborate for the benefit of the open source community. Regardless of a company’s size or resources, we want to discover what we can accomplish when we work together. Indeed is forming a community of FOSS Fund Adopters—companies that will run their own FOSS Contributor Fund initiatives in 2020. We invite you to join us and other FOSS Funders in sharing knowledge and experiences. We’re looking for adopters who are willing to run the same experiment we ran, or something similar. We will work with the community of Funders to set up regular events, exploring different models of open source support and funding. 

We’ve seen great results

In our blog post at the six month mark, we described how the program helped encourage Indeed employees to make open source contributions. Since program launch, we’ve seen thousands of such contributions. Indeedians have reported and fixed bugs. They’ve reviewed pull requests and developed features. They’ve improved documentation and designs. 

Even better, Indeed developers now have an avenue to advocate for projects in need of our support. And the program has inspired some employees to make their first open source contributions.

The FOSS Contributor Fund is one of the ways Indeed’s Open Source Program Office honors our commitment to helping sustain the projects we depend on. We gave our open source contributors a voice in the process, and we’ve seen some great benefits from doing so: increased contribution activity, better visibility into our dependencies, and a short list of projects where we can send new contributors. 

Watching the program evolve and grow is exciting. We’ve learned a lot this year and look forward to more growth in 2020. Now, we’d like you to join us. 

Use Indeed’s blueprint to start your FOSS Fund

To find out how we administer the FOSS Fund at Indeed, read our blueprint (released under a Creative Commons license). We’ve also released an open source tool called Starfish that we use to determine voter eligibility. In the coming months, FOSS Funders will publish additional documentation and tools to support these programs. We want to make it easy for anyone to run their own FOSS Fund.

If you are interested in joining the community of FOSS Fund Adopters, want more information, or would like to join a Q&A session, please email us at opensource@indeed.com

Learn more about Indeed’s open source program.