オペレーター1つがクラスタ全体を遅くした
目標
デフォルトのFlowSchemaとPriorityLevelConfigurationを順に読み取り、暴走するサービスアカウントのための隔離スロットを作成して、分類が実際に変わったことをレスポンスヘッダーで証明したうえで、優先度の値が重複したときのルールと、exemptの乱用を防ぐ監視スクリプト、さらにシート数のメトリクスまで確認します。
なぜ重要なのか
APIサーバーは、入ってくるすべてのリクエストを分類して隔離します。FlowSchemaが分類表で、リクエストはmatchingPrecedenceが小さいものから照合され、最初に一致した1つに決まります。そのスキーマが指すPriorityLevelConfigurationが、そのスロットの幅(同時にいくつ実行でき、あふれたら列に並べるのかすぐ拒否するのか)を決めます。この仕組みが重要な理由は1つです。暴走するクライアントがいるときに、誰が飢餓状態になるかを設計できるからです。分類表を読めなければ、429が積み上がる日にできることは再起動だけで、それは同じ暴走をもう一度受けるということです。
ステップ
- クラスターがデフォルトで入れておいたFlowSchema(このラボで作成するものと区別するために、名前が
ops-やzz-ops-で始まらないものと定義します)をすべて/root/ops-apf/flowschemas.tsvに保存してください。<matchingPrecedence>、<이름>、<우선순위 등급 이름>をタブ区切りで1行ずつ、リクエストが審査される順序で並べます。ソートは数値で行います(プレースホルダーは名前と優先度レベルの名前です)。 - クラスターがデフォルトで入れておいたPriorityLevelConfiguration(名前が
ops-で始まらないもの)をすべて/root/ops-apf/levels.tsvに保存してください。<이름>、<type>、<nominalConcurrencyShares>、<limitResponse.type>をタブ区切りで1行ずつ、名前の昇順です(プレースホルダーは名前です)。Exemptレベルのように値がない列は、-と書いてください。 - ネームスペース
ops-apfを作成し、サービスアカウントを3つ作成してください。widget-operator、plain-reader、bulk-writerです。そのあと/root/ops-apf/classify.shを作成してください。引数で受け取ったサービスアカウントになりすましてリクエストを1つ送り、レスポンスヘッダーのX-Kubernetes-Pf-Flowschema-Uidから、該当したFlowSchemaの名前だけを標準出力に出力します。plain-readerで実行して、その結果を/root/ops-apf/baseline.tsvに1行で保存してください。plain-readerと<스키마 이름>をタブで区切ります(プレースホルダーはスキーマ名です)。 /root/ops-apf/ops-noisy.yamlにPriorityLevelConfigurationops-noisyを書いてください。type: Limited、nominalConcurrencyShares: 5、lendablePercent: 50、borrowingLimitPercent: 20、limitResponseはtype: Queueで、queuingはqueues: 16、handSize: 4、queueLengthLimit: 50です。適用してください。/root/ops-apf/ops-noisy-operator.yamlにFlowSchemaops-noisy-operatorを書いてください。matchingPrecedence: 950、priorityLevelConfiguration.nameはops-noisy、distinguisherMethod.typeはByUserで、ルールは、subjectがkind: ServiceAccountでops-apfネームスペースのwidget-operator、resourceRulesはverbs・apiGroups・resources・namespacesをすべて*にします。適用してください。classify.shをwidget-operatorとplain-readerのそれぞれで実行して、結果を/root/ops-apf/classified.tsvに2行で保存してください。<어카운트 이름>と<스키마 이름>をタブで区切り、アカウント名の昇順で並べます(プレースホルダーはアカウント名とスキーマ名です)。/root/ops-apf/ops-bulk-a.yamlと/root/ops-apf/zz-ops-bulk-b.yamlに、FlowSchemaを2つ書いてください。どちらもops-apfのサービスアカウントbulk-writerを対象とし、ops-noisyを指し、resourceRulesはすべて*です。ops-bulk-aはmatchingPrecedence: 960・distinguisherMethod.type: ByUser、zz-ops-bulk-bはmatchingPrecedence: 940・distinguisherMethod.type: ByNamespaceです。両方を適用したあと、classify.sh bulk-writerの結果を、/root/ops-apf/precedence.tsvに2行で保存してください。ops-bulk-a、960、<win 또는 lose>をタブで区切った行と、zz-ops-bulk-b、940、<win 또는 lose>をタブで区切った行です(プレースホルダーはwinまたはloseです)。そして、もし2つのスキーマの値がどちらも960だったとしたら、どちらが勝つかを、その名前だけ/root/ops-apf/tie.txtに1行で書いてください。/root/ops-apf/exempt-allow.txtに、exempt優先度レベルを指してもよいFlowSchemaの名前を1行ずつ書いてください(現在のクラスターに実際にそのようなスキーマがどれなのかを先に確認し、その名前だけを書きます)。そして/root/ops-apf/exempt-guard.shを作成してください。exemptを指すFlowSchemaをすべて探し、許可リストにあればOK <이름>、なければVIOLATION <이름>を、名前順に標準出力にだけ出力し(プレースホルダーは名前です)、違反が1つでもあれば、0でないコードで終了する必要があります。現在の状態で実行したとき、違反がない必要があります。- APIサーバーのメトリクスから
apiserver_flowcontrol_nominal_limit_seatsを読み取り、/root/ops-apf/seats.tsvに保存してください。<우선순위 등급 이름>と<좌석 수>をタブ区切りで1行ずつ、名前の昇順です(プレースホルダーは優先度レベルの名前とシート数です)。ops-noisyがその中に含まれている必要があります。
参考
matchingPrecedenceは、値が小さいほど先に審査されます。kubectl --v=8は、レスポンスヘッダーを出力してくれます。分類の結果が、その中にUIDとして入っています。- APIの分類は認可より先に行われるので、403が返ってもヘッダーは付きます。
- FlowSchemaのstatusでDanglingがTrueなら、そのスキーマは無視されています。
- よくある間違い: 文字列のソートで優先度を並べてしまい、1000が2より前に来ることです。
- よくある間違い: 急いでいるからとexemptを指すようにして、サーバーを守る仕組みをまるごとなくしてしまうことです。
- 参考: https://kubernetes.io/docs/concepts/cluster-administration/flow-control/
分類表を優先度順に読み取る
クラスターがデフォルトで入れておいたFlowSchema(このラボで作成するものと区別するために、名前がops-やzz-ops-で始まらないものと定義します)をすべて/root/ops-apf/flowschemas.tsvに保存してください。<matchingPrecedence>、<이름>、<우선순위 등급 이름>をタブ区切りで1行ずつ、リクエストが審査される順序で並べます。ソートは数値で行います(プレースホルダーは名前と優先度レベルの名前です)。
入ってくるすべてのリクエストは、FlowSchemaを1つずつ照合しながら下りていき、最初に一致したもので止まります。照合の順序を決めるのがmatchingPrecedenceで、名前と違って、値が小さいほど先に見ます。文字列のソートでは1000が2より前に来てしまうので、数値のソートを使ってください。
各スロットの幅を読み取る
クラスターがデフォルトで入れておいたPriorityLevelConfiguration(名前がops-で始まらないもの)をすべて/root/ops-apf/levels.tsvに保存してください。<이름>、<type>、<nominalConcurrencyShares>、<limitResponse.type>をタブ区切りで1行ずつ、名前の昇順です(プレースホルダーは名前です)。Exemptレベルのように値がない列は、-と書いてください。
Limitedのレベルだけが、同時実行数を分け合います。Exemptは審査をまったく受けないので、関連するフィールドがありません。値がない列をjqで扱うとき、//はfalseを空の値として扱うので、ここではnullだけを比較するほうが安全です。
今はどのスロットに行くのかを実測する
ネームスペースops-apfを作成し、サービスアカウントを3つ作成してください。widget-operator、plain-reader、bulk-writerです。そのあと/root/ops-apf/classify.shを作成してください。引数で受け取ったサービスアカウントになりすましてリクエストを1つ送り、レスポンスヘッダーのX-Kubernetes-Pf-Flowschema-Uidから、該当したFlowSchemaの名前だけを標準出力に出力します。plain-readerで実行して、その結果を/root/ops-apf/baseline.tsvに1行で保存してください。plain-readerと<스키마 이름>をタブで区切ります(プレースホルダーはスキーマ名です)。
APIの分類は認可より先に行われます。そのため、権限がなくて403が返っても、ヘッダーは付いています。ヘッダーを見るにはkubectl --v=8を使い、標準エラー出力まで受け取る必要があります。なりすましは--as=system:serviceaccount:<네임스페이스>:<이름>です(プレースホルダーはネームスペースと名前です)。
暴走するOperatorのためのスロットを作成する
/root/ops-apf/ops-noisy.yamlにPriorityLevelConfigurationops-noisyを書いてください。type: Limited、nominalConcurrencyShares: 5、lendablePercent: 50、borrowingLimitPercent: 20、limitResponseはtype: Queueで、queuingはqueues: 16、handSize: 4、queueLengthLimit: 50です。適用してください。
シート数を直接書かずにシェアで書くのには、理由があります。サーバーの総同時実行数が変わっても、すべてのレベルが同じ割合で一緒に増減するからです。lendablePercentは余ったシートを貸し出せる割合で、borrowingLimitPercentはほかから借りてこられる上限です。
そのスロットへ振り分けるルールを書く
/root/ops-apf/ops-noisy-operator.yamlにFlowSchemaops-noisy-operatorを書いてください。matchingPrecedence: 950、priorityLevelConfiguration.nameはops-noisy、distinguisherMethod.typeはByUserで、ルールは、subjectがkind: ServiceAccountでops-apfネームスペースのwidget-operator、resourceRulesはverbs・apiGroups・resources・namespacesをすべて*にします。適用してください。
FlowSchemaのstatusにDanglingコンディションがあります。指している優先度レベルがなければTrueになり、そのスキーマは無視されます。適用したあと、このコンディションがFalseであることを確認する習慣を付けると、タイプミスでスキーマがまるごと死ぬ事故をすぐに見つけられます。
分類が本当に変わったことをヘッダーで証明する
classify.shをwidget-operatorとplain-readerのそれぞれで実行して、結果を/root/ops-apf/classified.tsvに2行で保存してください。<어카운트 이름>と<스키마 이름>をタブで区切り、アカウント名の昇順で並べます(プレースホルダーはアカウント名とスキーマ名です)。
ルールを作成したからといって、分類が変わったわけではありません。対象でないアカウントは、そのまま以前のスキーマに該当し、対象のアカウントだけが新しいスキーマへ行く必要があります。2行を一緒に残すことが、ルールが広く当たりすぎていない証拠になります。
同じリクエストに2つのルールが該当するとき
/root/ops-apf/ops-bulk-a.yamlと/root/ops-apf/zz-ops-bulk-b.yamlに、FlowSchemaを2つ書いてください。どちらもops-apfのサービスアカウントbulk-writerを対象とし、ops-noisyを指し、resourceRulesはすべて*です。ops-bulk-aはmatchingPrecedence: 960・distinguisherMethod.type: ByUser、zz-ops-bulk-bはmatchingPrecedence: 940・distinguisherMethod.type: ByNamespaceです。両方を適用したあと、classify.sh bulk-writerの結果を、/root/ops-apf/precedence.tsvに2行で保存してください。ops-bulk-a、960、<win 또는 lose>をタブで区切った行と、zz-ops-bulk-b、940、<win 또는 lose>をタブで区切った行です(プレースホルダーはwinまたはloseです)。そして、もし2つのスキーマの値がどちらも960だったとしたら、どちらが勝つかを、その名前だけ/root/ops-apf/tie.txtに1行で書いてください。
2つのスキーマが同じリクエストに該当すると、先に審査されるほうが勝ち、そこで照合が終わります。値がまったく同じときのルールは、公式ドキュメントに書かれています。名前を辞書順に比較して、小さいほうが勝ちます。ただし、ドキュメントは同じ値を置かないよう勧めています。
exemptを指すルールを監視する
/root/ops-apf/exempt-allow.txtに、exempt優先度レベルを指してもよいFlowSchemaの名前を1行ずつ書いてください(現在のクラスターに実際にそのようなスキーマがどれなのかを先に確認し、その名前だけを書きます)。そして/root/ops-apf/exempt-guard.shを作成してください。exemptを指すFlowSchemaをすべて探し、許可リストにあればOK <이름>、なければVIOLATION <이름>を、名前順に標準出力にだけ出力し(プレースホルダーは名前です)、違反が1つでもあれば、0でないコードで終了する必要があります。現在の状態で実行したとき、違反がない必要があります。
exemptレベルへ振り分けたリクエストは、同時実行数の制限も待ち行列もなく、即座に処理されます。便利に見えますが、その分、サーバーを守る仕組みがなくなるので、新しくこのレベルを指すスキーマができたら、人が知る必要があります。このような監視は、CIで実行するのに向いています。
新しいスロットがシートを実際に受け取ったかをメトリクスで確認する
APIサーバーのメトリクスからapiserver_flowcontrol_nominal_limit_seatsを読み取り、/root/ops-apf/seats.tsvに保存してください。<우선순위 등급 이름>と<좌석 수>をタブ区切りで1行ずつ、名前の昇順です(プレースホルダーは優先度レベルの名前とシート数です)。ops-noisyがその中に含まれている必要があります。
メトリクスはkubectl get --raw /metricsで取得します。レベルを1つ増やすと、総シート数が増えるのではなく、同じ総量をシェアの割合で再分配します。そのため、新しいレベルを作成すると、既存のレベルのシート数が一緒に減ります。その数字を自分の目で見ると、シェアの意味がはっきりします。