元々は私のブログに投稿されました
デフォルトでは、Kubebuilder とコントローラー ランタイムを使用して構築されたオペレーターは、一度に 1 つのリコンサイル リクエストを処理します。オペレータ開発者がアプリケーションのロジックを推論してデバッグするのが容易になるため、これは賢明な設定です。また、コントローラーから ectd や API サーバーなどのコア Kubernetes リソースへのスループットも制限されます。
しかし、ワークキューがバックアップを開始し、リクエストがキュー内に残されて処理を待っているために平均調整時間が増加した場合はどうなるでしょうか?幸いなことに、コントローラー ランタイムのコントローラー構造体には MaxConcurrentReconciles フィールドが含まれています (以前 Kubebuilder のヒント記事で述べたように)。このオプションを使用すると、単一のコントローラーで実行する同時調整ループの数を設定できます。したがって、1 より大きい値を指定すると、複数の Kubernetes リソースを同時に調整できます。
オペレーターとしての取り組みの初期に、私が抱いた疑問の 1 つは、同じリソースが 2 つ以上のゴルーチンによって同時に調整されていないことをどのように保証できるかということでした。 MaxConcurrentReconciles を 1 より大きく設定すると、調整ループ内のオブジェクトの状態が外部ソース (別のスレッドで実行されている調整ループ) からの副作用によって変化する可能性があるため、あらゆる種類の競合状態や望ましくない動作が発生する可能性があります。 .
私はこれについてしばらく考え、ゴルーチンが特定のリソース (名前空間/名前に基づいて) のロックを取得できるようにする sync.Map ベースのアプローチを実装しました。
コントローラーのワークキューにすでにこの機能が含まれていることを最近 (k8s Slack チャネルで) 知ったので、この努力はすべて無駄だったことがわかりました。ただし、実装はより単純です。
これは、k8s コントローラーのワークキューが固有のリソースが順番に調整されることをどのように保証するかについての簡単な説明です。したがって、MaxConcurrentReconciles が 1 より大きく設定されている場合でも、特定のリソースに対して一度に動作する調整関数は 1 つだけであると確信できます。
コントローラー ランタイムは、client-go/util/workqueue ライブラリを使用して、基礎となる調整キューを実装します。パッケージの doc.go ファイルのコメントには、ワークキューが次のプロパティをサポートしていることが記載されています:
ちょっと待ってください...私の答えはここの 2 番目の項目、「けちな」プロパティにあります。これらのドキュメントによると、キューはコードを 1 行も記述することなく、この同時実行性の問題を自動的に処理します。実装を見てみましょう。
ワークキュー構造体には、Add、Get、Done の 3 つの主要なメソッドがあります。コントローラー内では、情報提供者が調整リクエスト (汎用 k8s リソースの名前空間名) をワークキューに追加します。別のゴルーチンで実行されている調整ループは、キューから次のリクエストを取得します (キューが空の場合はブロックされます)。このループは、コントローラーに書き込まれたカスタム ロジックをすべて実行し、コントローラーはキュー上で Done を呼び出し、リコンサイル リクエストを引数として渡します。これにより、プロセスが再度開始され、調整ループは Get を呼び出して次の作業項目を取得します。
これは、RabbitMQ でのメッセージの処理に似ています。ワーカーがキューから項目を取り出して処理し、処理が完了し、項目をキューから削除しても安全であることを示す「Ack」をメッセージ ブローカーに送り返します。キュー。
それでも、QuestDB Cloud のインフラストラクチャを強化するオペレーターを運用環境で実行しており、ワークキューが宣伝どおりに動作することを確認したいと考えていました。そこで、動作を検証するための簡単なテストを作成しました。
これは、「Stingy」プロパティを検証する簡単なテストです:
package main_test import ( "testing" "github.com/stretchr/testify/assert" "k8s.io/client-go/util/workqueue" ) func TestWorkqueueStingyProperty(t *testing.T) { type Request int // Create a new workqueue and add a request wq := workqueue.New() wq.Add(Request(1)) assert.Equal(t, wq.Len(), 1) // Subsequent adds of an identical object // should still result in a single queued one wq.Add(Request(1)) wq.Add(Request(1)) assert.Equal(t, wq.Len(), 1) // Getting the object should remove it from the queue // At this point, the controller is processing the request obj, _ := wq.Get() req := obj.(Request) assert.Equal(t, wq.Len(), 0) // But re-adding an identical request before it is marked as "Done" // should be a no-op, since we don't want to process it simultaneously // with the first one wq.Add(Request(1)) assert.Equal(t, wq.Len(), 0) // Once the original request is marked as Done, the second // instance of the object will be now available for processing wq.Done(req) assert.Equal(t, wq.Len(), 1) // And since it is available for processing, it will be // returned by a Get call wq.Get() assert.Equal(t, wq.Len(), 0) }
ワークキューは内部でミューテックスを使用するため、この動作はスレッドセーフです。そのため、キューを壊すために複数の goroutine を使用してキューから高速で読み書きするテストをさらに作成したとしても、ワークキューの実際の動作はシングルスレッド テストの動作と同じになります。
Kubernetes の標準ライブラリには、このような小さな宝石がたくさん隠されており、その一部はあまり目立たない場所 (go クライアント パッケージにあるコントローラー ランタイム ワークキューなど) にあります。この発見や、私が過去に同様の発見をしたにもかかわらず、これらの問題を解決しようとするこれまでの試みは完全な時間の無駄ではないと今でも感じています。これらは、分散システム コンピューティングの基本的な問題について批判的に考えることを強制し、内部で何が起こっているのかをより深く理解するのに役立ちます。そのため、「Kubernetes が成功した」ことがわかるまでに、コードベースを簡素化し、おそらく不必要な単体テストをいくつか削除できると安心しています。
以上がKubernetes オペレーターは同時実行性をどのように処理しますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。