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 はなぜこんなに混乱しているのか?

推薦する

ウェブサイトのSEOデータ分析でよくある3つの間違い

2012年に国内のSEO業界が非常に発展したことに気づいているかどうかはわかりません。この年、Bai...

モンデリーズ中国がアリババクラウドと提携、世界有数のスナック企業がデジタルアップグレードを開始

6月7日、雲奇会議上海サミットにおいて、世界的スナック食品大手のモンデリーズ・インターナショナルの代...

キーワードウェブサイトプロモーションは欠かせないツールです

キーワードは検索エンジン最適化の重要な要素です。オンラインプロモーションの分野では、キーワードの適切...

ミックスアイスシティの100億人民元の収益の秘密を解明

2022年9月22日、 Mixue Bingchengは深セン証券取引所のメインボードに目論見書を公...

クラウドネイティブ時代の機会と課題: DevOps で打開する方法

クラウドネイティブ時代において、国内外の多くのクラウドベンダーが強力な技術的配当をリリースしています...

予算ノード - 年間 12 ドル / 50gDDoS 保護 / 256m メモリ / 20g ハード ドライブ / 500g トラフィック

budgetnode は Access Internet Ltd のブランドであり、今年登録番号10...

ハイブリッド クラウドの成功について企業が忘れるべき 5 つのことと覚えておくべき 4 つのこと

ハイブリッド クラウドの導入を推進している企業の CIO の中には、クラウド コンピューティングによ...

派手な仮面を脱ぎ捨て、ウェブサイトの信頼性と人気の秘密を明らかにします

Baidu Kステーション事件は終わってからかなり経っているが、多くのウェブマスターは事件のことを考...

VPS に移行するにはどうすればいいですか?

VPS (仮想プライベート サーバー) への移行には、通常、次の手順が含まれます。適切な VPS プ...

zji: 香港サーバー、月額 348 元、cn2+BGP ネットワーク、e5-2630L/8g メモリ/240gSSD/5M 帯域幅

zji現在、香港の葵湾データセンターの独立サーバーでは特別クリアランスセールが行われています。CN2...

iniz-シンガポール VPS/リースウェブデータセンター/1000mポート/年間支払い18ポンドから

iniz.com のシンガポール VPS は、実は数日前から市場に出回っているのですが、迂回問題のた...

クラウド ストレージ アーキテクチャは DevOps のどのような問題を解決できますか?

1. クラウド ストレージ アーキテクチャの概要クラウド ストレージは、サービスとしてのデータ スト...

クラウド ERP とオンプレミス ERP: どちらが優れているか?

企業が新しい ERP システムを選択する際に最も重要な考慮事項の 1 つは、オンプレミスの ERP ...

Ceen TaobaoアフィリエイトコンテストIIレポート:新兵が動員され、クリスマスの戦いが激化

クリスマスの雰囲気がますます強くなり、CEEN「世界名靴淘宝顧客」プロモーションコンテスト[シーズン...