Kube-Apiserverのメモリ最適化についてお話しましょう

Kube-Apiserverのメモリ最適化についてお話しましょう

原理

メモリの最適化は古典的な問題です。 K8S が具体的に何をするのかを見る前に、まずプロセスを抽象化し、自分たちだったらどのように最適化するかを考えてみましょう。このプロセスは、サーバーからデータを取得するための外部同時要求として単純に抽象化できます。スループットに影響を与えずにサーバーのメモリ消費を削減するにはどうすればよいですか?一般的にはいくつかの方法があります:

  • シリアル化された結果をキャッシュする
  • シリアル化中のメモリ割り当てを最適化する

このシナリオではデータ圧縮は適用できない可能性があります。確かに圧縮によってネットワーク伝送帯域幅が削減され、リクエスト応答速度が向上しますが、サーバー側メモリの最適化にはあまり効果がありません。 kube-apiserver はすでに gzip ベースのデータ圧縮をサポートしています。 Accept-Encoding を gzip に設定するだけです。詳細については公式サイト[1]を参照されたい。

もちろん、シリアル化された結果をキャッシュすることは、多数のクライアント要求があるシナリオ、特にサーバーが複数のクライアントに同時にデータを送信する必要がある場合に適しています。シリアル化された結果をキャッシュする利点は、シリアル化プロセスが 1 つだけ必要になるため、より明らかになります。シリアル化が一度完了していれば、後で他のクライアントにデータを送信するときに以前の結果を直接使用できるため、不要な CPU とメモリのオーバーヘッドが削減されます。もちろん、シリアル化された結果をキャッシュする操作自体も、ある程度のメモリを消費します。クライアントの数が少ない場合、この操作はあまり効果的ではなく、追加のメモリ消費を引き起こす可能性もあります。 kube-apiserver 監視リクエストはこのシナリオに完全に適合します。

次の記事では、kube-apiserver がこれら 2 つの点をどのように最適化するかを紹介します。

成し遂げる

以下のタイムラインに記載されているさまざまな問題と最適化は、おそらく、数多くある問題と最適化のほんの一部にすぎません。

キャッシュシリアル化結果

タイムライン

  1. 2019年には、コミュニティの誰かが問題を報告しました[2]。5,000ノードのクラスターで大きなエンドポイントオブジェクト(5,000ポッド、サイズが約1MB)を作成すると、kube-apiserverが5秒以内に完全に過負荷になる可能性があるという問題です。
  2. コミュニティはこの問題を認識し、オブジェクトのシリアル化を少なくするKEP 1152を提案しました[3]。この機能により、異なるウォッチャーに対して同じオブジェクトを複数回シリアル化することを回避することで、kube-apiserver のロード時間とメモリ割り当て時間が短縮されます。この機能はv1.17でリリースされました。 5,000 ノードのテスト結果によると、メモリ割り当ては約 15% 最適化され、CPU は約 5% 最適化されました。ただし、この最適化は HTTP プロトコルに対してのみ有効であり、WebSocket に対しては有効ではありません。
  3. 3年後の2023年には、Refactor apiserverエンドポイントトランスフォーマーを通じてシリアル化ロジックがリファクタリングされ、Encoderをよりネイティブに使用するようになりました#119801[4]。また、シリアル化操作にはEncoderインターフェースが統一的に使用されるようになりました。対応する問題83898[5]は2019年にすでに作成されていました。このリファクタリングは、2で言及したWebSocketが有効でないという問題も解決し、1.29でリリースされました。

したがって、WebSocket 形式でウォッチを使用していない場合 (デフォルトでは Http Transfer-Encoding: chunked が使用される)、理論的には 1.17 にアップグレードすると問題なくなります。

原理

写真

CacheableObject インターフェースを追加し、すべてのエンコーダーで CacheableObject をサポートしました。

 // Identifier represents an identifier. // Identitier of two different objects should be equal if and only if for every // input the output they produce is exactly the same. type Identifier string type Encoder interface { ... // Identifier returns an identifier of the encoder. // Identifiers of two different encoders should be equal if and only if for every input // object it will be encoded to the same representation by both of them. Identifier() Identifier } // CacheableObject allows an object to cache its different serializations // to avoid performing the same serialization multiple times. type CacheableObject interface { // CacheEncode writes an object to a stream. The <encode> function will // be used in case of cache miss. The <encode> function takes ownership // of the object. // If CacheableObject is a wrapper, then deep-copy of the wrapped object // should be passed to <encode> function. // CacheEncode assumes that for two different calls with the same <id>, // <encode> function will also be the same. CacheEncode(id Identifier, encode func(Object, io.Writer) error, w io.Writer) error // GetObject returns a deep-copy of an object to be encoded - the caller of // GetObject() is the owner of returned object. The reason for making a copy // is to avoid bugs, where caller modifies the object and forgets to copy it, // thus modifying the object for everyone. // The object returned by GetObject should be the same as the one that is supposed // to be passed to <encode> function in CacheEncode method. // If CacheableObject is a wrapper, the copy of wrapped object should be returned. GetObject() Object } func (e *Encoder) Encode(obj Object, stream io.Writer) error { if co, ok := obj.(CacheableObject); ok { return co.CacheEncode(s.Identifier(), s.doEncode, stream) } return s.doEncode(obj, stream) } func (e *Encoder) doEncode(obj Object, stream io.Writer) error { // Existing encoder logic. } // serializationResult captures a result of serialization. type serializationResult struct { // once should be used to ensure serialization is computed once. once sync.Once // raw is serialized object. raw []byte // err is error from serialization. err error } // metaRuntimeInterface implements runtime.Object and // metav1.Object interfaces. type metaRuntimeInterface interface { runtime.Object metav1.Object } // cachingObject is an object that is able to cache its serializations // so that each of those is computed exactly once. // // cachingObject implements the metav1.Object interface (accessors for // all metadata fields). However, setters for all fields except from // SelfLink (which is set lately in the path) are ignored. type cachingObject struct { lock sync.RWMutex // Object for which serializations are cached. object metaRuntimeInterface // serializations is a cache containing object`s serializations. // The value stored in atomic.Value is of type serializationsCache. // The atomic.Value type is used to allow fast-path. serializations atomic.Value }

cachingObject は CacheableObject インターフェースを実装します。そのオブジェクトは、対象のイベント オブジェクト (Pod など) です。 serializations はシリアル化された結果を保存するために使用されます。シリアル化の方法には json、yaml、protobuf の 3 つがあるため、識別子はシリアル化の種類を表す識別子です。

cachingObject の生成 上の図では、Cacher の dispatchEvent が独自の受信 chan データを消費し、関連するすべての cacheWatchers にイベントを送信するときに、イベント オブジェクトを cachingObject に変換し、cacheWatcher の入力 chan に送信します。最終的な Encode 操作は、最終オブジェクトが serveWatch メソッドでシリアル化されるときに呼び出されます。まず、シリアル化された結果がすでに存在するかどうかを判断します。そうなる場合は、シリアル化の繰り返しを避けるために直接再利用されます。

知らせ:

上の図では、len(watchers) >= 3 が過去のものになった場合は、cachingObject にラップします。新しいコード ロジックでは、次の判断が削除されました。ウォッチャーの数に関係なく、cachingObject は均一にカプセル化されます。

Init イベント (watchcache 内のすべてのデータ) は cachingObject にカプセル化されません。 Cacher 受信チャネルに送信されたデータのみが cachingObject に変換されます。つまり、Get/List リクエストはウォッチキャッシュから直接データを返すため、この最適化は完全に無効です。 Watch リクエストの場合、一部の Init Event データが引き続き watchcache から取得され返される可能性があるため、一部のデータは返されるときに既存のシリアル化結果を再利用しません。ここは魔法のような場所です。 cacheWatcher の入力チャネルのイベント オブジェクトのオブジェクトは、Pod などの通常のリソース オブジェクト、または CacheableObject オブジェクトである可能性があり、実際のリソース オブジェクトは CacheableObject のオブジェクトに格納されます。

Init イベントもカバーしないのはなぜですか? KEP 1152 では、まず Cache incoming chan のカバレッジを実装すると、大きなメリットが得られ、以前に見つかった問題が解決されると述べられています。さらに最適化が必要な場合は、Init イベントもカバーする可能性を再評価します。ストリーミングウォッチエンコーダをリファクタリングしてキャッシュを有効にする#120300[6]のコメントにも関連する議論があります。

写真

この最適化項目はKEP 3157ウォッチリスト[7]にも記載されています。

メモリ割り当ての最適化

タイムライン

  1. オブジェクトのシリアル化中にWatchServerで割り当てられる回数を減らします。#108186[8]、主にprotobufに最適化されており、jsonおよびyamlのシリアル化には無効です。 2022 年の v1.24 のリリースでは、内部コンポーネントでは protobuf が一般的に使用され、外部コンポーネントでは k8s にアクセスするときに json または yaml シリアル化が一般的に使用されます。
  2. キャッシュされたシリアル化のバイトをコピーしない #118362[9]、cachingObjectのシリアル化結果のディープコピーを回避するためのカスタムSpliceBuffer、2023年にv1.28でリリース。
  3. ストリーミング ウォッチ エンコーダーをリファクタリングしてキャッシュを有効にします #120300。この修正では、最終的にクライアントに返されるのはリソース オブジェクトではなくイベントであるため、既存のキャッシュされたリソース オブジェクトのシリアル化結果に基づいてイベントのシリアル化結果をキャッシュします。

原理

2 の場合、SpliceBuffer は、embeddedEncodeFn によるシリアル化された結果 []byte のディープ コピーを回避し、浅いコピーを通じてメモリ割り当てを効果的に最適化するように巧みに定義されています。

 // A spliceBuffer implements Splice and io.Writer interfaces. type spliceBuffer struct { raw []byte buf *bytes.Buffer } // Splice implements the Splice interface. func (sb *spliceBuffer) Splice(raw []byte) { sb.raw = raw }

ベンチマーク結果は重要

go test -benchmem -run=^$ -bench ^BenchmarkWrite k8s.io/apimachinery/pkg/runtime -v -count 1 goos: linux goarch: amd64 pkg: k8s.io/apimachinery/pkg/runtime cpu: AMD EPYC 7B12 BenchmarkWriteSplice BenchmarkWriteSplice-48 151164015 7.929 ns/op 0 B/op 0 allocs/op BenchmarkWriteBuffer BenchmarkWriteBuffer-48 3476392 357.8 ns/op 1024 B/op 1 allocs/op PASS ok k8s.io/apimachinery/pkg/runtime 3.619s

3に関しては、厳密に言えば、このPRはメモリ割り当てを最適化するために使用されているのではなく、v1.29でリリースされた問題110146[10]で言及されている、jsonのシリアル化中にjson.compactによって引き起こされるCPU使用率の上昇の問題を解決するために使用されています。問題は、リソース オブジェクトのシリアル化の結果が前述のように cachingObject を通じてキャッシュされるものの、最終的にクライアントに送り返されるのは、まだシリアル化が必要な Event オブジェクトであるため発生します。 json.compact は各 Marshal の後に呼び出されます。これは、golang に付属する JSON シリアル化の実装です。 golang jsonソースコード[11]を参照することができます。この修正は、json.compact の影響を回避するために、リソース オブジェクトのシリアル化結果をキャッシュすることに基づいて、イベントのシリアル化結果をキャッシュするものです。

このPRには多くの変更が含まれます。その実装についてはまだいくつか疑問があります。私はコミュニティに相談するために問題122153[12]を提起しました。それがわかったら、この実装について説明する別の記事を用意する予定です。これには、ウォッチ ハンドラーのシリアル化ロジック全体が関係します。エンコーダーは深くネストされています。 Google の神でさえ、コードをレビューした際に次のようなコメントを残しました。

写真

写真

このコードを読んでいるときに、インターフェースが前後にジャンプして混乱しました。これらのエンコーダーを理解するために、段階的にデバッグするための単体テストを作成しました。本当にネストされています。以下は、これら 5 つのネスト層の概要です。

ウォッチエンコーダ

—> ウォッチエンベデッドエンコーダ

—> エンコーダー付きアロケーター

—> コーデック

—> json.シリアライザー

それらはすべて Encoder インターフェースを実装しています...

cachingObject のシリアル化と同様に、イベントのシリアル化にも追加のメモリ領域が必要ですが、各イベントの複数のシリアル化によって発生するメモリと CPU の消費を回避できるため、メモリの最適化にも役立ちます。

効果

ウォッチリストと上記の最適化を通じて、コミュニティは最適化効果をもたらしました

最適化前

写真

最適化後

写真

やっと

Kube-apiserver メモリ最適化シリーズには、これまでの準備も含まれており、現在 6 つの記事があります。関連する知識をすべて理解すれば、kube-apiserver に対する理解が確実に深まります。今後もこの内容に注目して随時補足していきたいと思います〜

シリアル化は、メソッドを調整するだけなので単純に聞こえますが、それをうまく使用するのは簡単ではありません。多くの場合、このような場所が個人の能力を最もよく反映します。技術は日常の中に現れ、真実は細部の中に現れます。専門家が書いたコードを見て、設計やアイデアを理解し、要約し、変換して、自分の用途に取り入れます。

K8s は使いやすいですが、使いこなすのは簡単ではなく、舞台裏で何が起こっているのか理解するのは困難です。 10 年以上にわたる反復を経て、このプロジェクトはコードの量と複雑さの両面で非常に困難なものとなり、現在も反復と更新が続けられています。しかし、道は長くても歩き続ければ目的地にたどり着くでしょう。仕事は難しいですが、やれば成功するかもしれませんが、やらなければ絶対に成功しません。

話は安い、コードとPPTを見せてください

最後に、最先端の技術や業界の動向について議論するために、私のWeChat:YlikakuYを追加することを歓迎します〜

参考文献

[1]

kubernetes-api: https://kubernetes.io/zh-cn/docs/concepts/overview/kubernetes-api/

[2]問題#75294: https://github.com/kubernetes/kubernetes/issues/75294

[3]kep#1152-less-object-serializations: https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1152-less-object-serializations

[4]pr#119801: https://github.com/kubernetes/kubernetes/pull/119801

[5]問題#83898: https://github.com/kubernetes/kubernetes/issues/83898

[6]pr#120300: https://github.com/kubernetes/kubernetes/pull/120300

[7]kep#3157 ウォッチリスト: https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/3157-watch-list/README.md

[8]pr#108186: https://github.com/kubernetes/kubernetes/pull/108186

[9]pr#118362: https://github.com/kubernetes/kubernetes/pull/118362/

[10]問題#11014: https://github.com/kubernetes/kubernetes/issues/110146

[11]golang#json: https://github.com/golang/go/blob/d8762b2f4532cc2e5ec539670b88bbc469a13938/src/encoding/json/encode.go#L498

[12]問題#122153: https://github.com/kubernetes/kubernetes/issues/122153

<<:  クラウド移行に関する注意事項 | ByteDanceの超大規模データベース製品技術の進化を理解するための図

>>:  中国の SaaS はなぜこんなに混乱しているのか?

推薦する

事例分析: Baidu Knows ページが特定のウェブサイトに大量に表示されています

なぜ一部のウェブサイトにはBaidu Knowsページを含むBaidu関連のドメインがあるのに、Ba...

Yahoo 外部リンク クエリが閉じられた後の新しい外部リンク クエリ

Yahoo はなくなりましたが、ウェブサイトのバックリンクを確認するにはどうすればよいでしょうか。こ...

シナジーリサーチグループ:Amazon、Microsoft、Googleがクラウド市場をリード

海外メディアの報道によると、シナジーリサーチグループが発表した最新の報告書によると、企業のクラウドイ...

羅吉思薇の成功から主要クリエイターの解散まで:セルフメディアの人々の未来はどこにあるのでしょうか?

(文/Heven) 数日前、ネット界で大きなニュースが飛び込んできた。Luoji Siweiの主要ク...

Xiaohongshuの価値はいくらですか?

資本市場で何度も上場の噂が流れていた小紅書は、最新の資金調達ラウンドを迎えた。 11月8日、メディア...

クラウドコンピューティングのコストに関する3つの誤解

現在、企業が業務をクラウドに移行する傾向が広まっており、そうすることで企業の CIO とその組織のコ...

モバイルインターネット時代のマーケティングにおける10の大きな変化

以前、私はWeChatでアンケートを実施し、以下のどれが一番ひどいかを全員に尋ねました。 1. イン...

マルチクラウドの導入が簡単に

SAS が後援した「Every Cloud Has a Silver Lining」と題するレポート...

Sogou入札チュートリアル:科学的にSogou広告を設定することが鍵

Sogou が検索エンジン市場で努力を続けるにつれ、Sogou の入札サービスも人々にとってますます...

マイクロソフトのタブレット戦略は明確:モバイルオフィスがマーケティングのハイライトに

マイクロソフトの創業者ビル・ゲイツが、IT技術の現在の発展と応用の傾向は、伝統的なPCをタブレットコ...

WeChat パブリックアカウント 10 週以上のデータレポート

10w+ とはどういう意味ですか?新しいメディア環境が成熟するにつれ、ヒット商品を生み出す手法も確立...

Anolis OS 23正式版は2023年に発売予定

最近、Anolis コミュニティは次世代 Anolis オペレーティング システム (Anolis ...

アリババの自動車戦略の次のステップ:「スマートハイウェイ」の構築

9月20日、アリババグループは2018年杭州雲旗大会で、自動車戦略のアップグレードを発表した。自動車...

PPC 広告が SEO にどのように役立つかについての簡単な説明

中小企業にとっては、毎月数万元をPPC広告に投資する余裕はありません。しかし、検索エンジンマーケティ...

ハイブリッドクラウドと将来のクラウドアプリケーションの利点

過去 10 年間で、クラウド コンピューティングは、インターネット ベースのコンピューティングの一種...