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

Kubernetes運用実務

429を誰が受け取るかは私たちが決める

TT Labで続きを見る

一言でいうと

APIサーバーは、すべてのリクエストをFlowSchemaで分類してPriorityLevelConfigurationというスロットに入れ、スロットごとに同時実行数を割り当てます。暴走するクライアントが現れたとき、誰が飢餓状態になるかを、この2つのオブジェクトであらかじめ設計しておけます。

なぜこの仕組みが必要だったのか

以前のkube-apiserverには、--max-requests-inflightと--max-mutating-requests-inflightの2つのつまみしかありませんでした。同時に処理するリクエスト数を、全体でまるごと制限するものです。この方式の問題は明らかです。誰がその枠を占有しているのかを区別しません。誤って作られたコントローラー1つが毎秒数千回の一覧取得を行うと、その枠をすべて食い尽くし、そのあとに来たkubeletのノード状態の更新も、運用担当者のkubectl get podsも、同じように後回しになります。重要なリクエストとノイジーなリクエストを分ける手段がなかったのです。

API Priority and Fairnessは、この状況を2つの段階に分けます。まずリクエストを分類し、分類されたスロットごとに別々に幅を与えます。さらに、短い暴走は拒否せずに、少しのあいだ列に並ばせます。1.29で安定機能になり、APIグループはflowcontrol.apiserver.k8s.io/v1です。

どう動くのか

FlowSchemaが分類表です。入ってくるリクエストは、matchingPrecedenceが数字の小さいものから順に照合され、最初に一致した1つで止まります。名前と違って、値が小さいほど先だという点が、いつも混同しやすいところです。値がまったく同じスキーマが2つあると、名前を辞書順に比較して小さいほうが勝ちますが、公式ドキュメントは、そのような状況に頼らず値を重複させないよう勧めています。

ルールは、subjects(誰が)とresourceRules/nonResourceRules(何を)のペアで書きます。各フィールドに*を使うと、その項目はまったく問わないという意味です。そしてdistinguisherMethodが、1つのスロットの中でリクエストをフロー(flow)にさらに細かく分けます。ByUserなら、1人のユーザーがほかのユーザーの取り分を食い尽くせず、ByNamespaceなら、ネームスペース単位で同じ保護がかかります。空にしておくと、そのスキーマに該当するすべてのリクエストが、1つのフローとして扱われます。

PriorityLevelConfigurationがスロットです。typeがExemptなら、審査そのものを受けずに即座に処理されます。Limitedなら、同時実行数を分けてもらいますが、絶対的なシート数ではなく、nominalConcurrencySharesというシェアで書きます。サーバーの総同時実行数を変えると、すべてのスロットが同じ割合で一緒に増減するようにする設計です。ここにlendablePercent(余ったシートを貸し出せる割合)とborrowingLimitPercent(借りてこられる上限)が加わり、1つのスロットが遊んでいるとき、そのシートがほかのスロットへ流れます。

あふれたときの動作は、limitResponseが決めます。Rejectなら即座に429、Queueなら列に並べます。列の形はqueues・handSize・queueLengthLimitの3つの値が決め、この3つが、シャッフルシャーディングという手法で「象がネズミを踏みつぶす確率」を調整します。1つのフローが積み上げられるリクエストの最大値は、handSize掛けるqueueLengthLimitです。

デフォルトで入っているものも、知っておく価値があります。必須(mandatory)のオブジェクト4つのうち、exemptスキーマはsystem:mastersグループのリクエストを審査から外し、catch-allは、何にも一致しなかったリクエストの行き先を保証します。そのほかに、推奨(suggested)設定として、system-leader-election・system-nodes・kube-controller-manager・service-accountsのようなスキーマが入ってきます。

観測は、apiserver_flowcontrol_で始まるメトリクスが担います。拒否数はapiserver_flowcontrol_rejected_requests_totalで、reasonラベルがqueue-full・concurrency-limit・time-out・cancelledのどれかで理由を示します。スロットごとに実際に何シートを受け取ったかは、apiserver_flowcontrol_nominal_limit_seatsにそのまま出ます。

現場での姿

最も多いのは、Operator1つの暴走です。キャッシュを使わず、毎回全体の一覧を取得するコントローラーがデプロイされると、同じスロットを使うほかのサービスアカウントも一緒に遅くなります。このときにすべきことは、そのアカウントだけを狭いスロットへ振り分けるFlowSchemaを1つ追加することです。その瞬間、被害がそのスロットの中に閉じ込められます。

2つ目は、exemptの乱用です。429が出るので、焦ってそのワークロードをexemptへ振り分けてしまいます。症状は消えますが、そのクライアントは、サーバーを守る仕組みをまるごと迂回するようになります。次に暴走したときは、APIサーバーが一緒に倒れます。そのため、exemptを指すスキーマの一覧は、人が監視すべき短い一覧です。

3つ目は、静かに死んだスキーマです。指している優先度レベルの名前を1文字間違えると、エラーは出ず、statusにDanglingコンディションがTrueとして残るだけです。そのスキーマはないものとして動作し、隔離しようとしたワークロードは、引き続きデフォルトのスロットでほかを飢餓状態にします。

このラボ環境の限界

このクラスターには、実際に暴走させるワークロードがありません。kwokのPodはコンテナではないので、その中でクライアントを動かせません。そのため、429を本当に発生させる実験は行いません。代わりに、分類がどうなるかは実測します。kubectl --v=8が出力するX-Kubernetes-Pf-Flowschema-Uidヘッダーに、そのリクエストが実際に該当したスキーマのUIDが含まれており、分類は認可より先に行われるので、権限のないアカウントになりすましたリクエストが403で終わっても、ヘッダーはそのまま付きます。シート数も、実際のメトリクスで確認できます。

次のラボですること

デフォルトの分類表とスロット一覧を、優先度順に読み取って保存するところから始めます。次に、サービスアカウントになりすましてリクエストを送り、レスポンスヘッダーで「今はどのスロットに行くのか」を実測します。暴走するOperatorのための狭いスロットと、そこへ振り分けるルールを作成し、分類が変わったかどうかを同じ方法で証明します。続いて、2つのルールが1つのリクエストに該当したときの勝敗を実測し、exemptを指すスキーマを監視するスクリプトを作成したあと、新しいスロットが実際にいくつのシートを受け取ったかをメトリクスで確認します。

参考ドキュメント: