新聞中心
原理
內(nèi)存優(yōu)化是一個(gè)經(jīng)典問(wèn)題,在看具體 K8S 做了哪些工作之前,可以先抽象一些這個(gè)過(guò)程,思考一下如果是我們的話,會(huì)如何來(lái)優(yōu)化。這個(gè)過(guò)程可以簡(jiǎn)單抽象為外部并發(fā)請(qǐng)求從服務(wù)端獲取數(shù)據(jù),如何在不影響吞吐的前提下降低服務(wù)端內(nèi)存消耗?一般有幾種方式:

創(chuàng)新互聯(lián)公司是一家專注于成都網(wǎng)站建設(shè)、成都做網(wǎng)站與策劃設(shè)計(jì),劍川網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)公司做網(wǎng)站,專注于網(wǎng)站建設(shè)10余年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:劍川等地區(qū)。劍川做網(wǎng)站價(jià)格咨詢:18980820575
- 緩存序列化的結(jié)果
- 優(yōu)化序列化過(guò)程內(nèi)存分配
數(shù)據(jù)壓縮在這個(gè)場(chǎng)景可能不適用,壓縮確實(shí)可以降低網(wǎng)絡(luò)傳輸帶寬,從而提升請(qǐng)求響應(yīng)速度,但對(duì)服務(wù)端內(nèi)存的優(yōu)化沒(méi)有太大的作用。kube-apiserver 已經(jīng)支持基于 gzip 的數(shù)據(jù)壓縮,只需要設(shè)置 Accept-Encoding 為 gzip 即可,詳情可以參考官網(wǎng)[1]介紹。
當(dāng)然緩存序列化的結(jié)果適用于客戶端請(qǐng)求較多的場(chǎng)景,尤其是服務(wù)端需要同時(shí)把數(shù)據(jù)發(fā)送給多個(gè)客戶時(shí),緩存序列化的結(jié)果收益會(huì)比較明顯,因?yàn)橹恍枰淮涡蛄谢倪^(guò)程即可,只要完成一次序列化,后續(xù)給其他客戶端直接發(fā)送數(shù)據(jù)時(shí)直接使用之前的結(jié)果即可,省去了不必要的 CPU 和內(nèi)存的開銷。當(dāng)然緩存序列化的結(jié)果這個(gè)操作本身來(lái)說(shuō)也是會(huì)占用一些內(nèi)存的,如果客戶端數(shù)量較少,那么這個(gè)操作可能收益不大甚至可能帶來(lái)額外的內(nèi)存消耗。kube-apiserver watch 請(qǐng)求就與這個(gè)場(chǎng)景非常吻合。
下文會(huì)就 kube-apiserver 中是如何就這兩點(diǎn)進(jìn)行的優(yōu)化做一個(gè)介紹。
實(shí)現(xiàn)
下文列出的時(shí)間線中的各種問(wèn)題和優(yōu)化可能而且有很大可能只是眾多問(wèn)題和優(yōu)化中的一部分。
緩存序列化結(jié)果
時(shí)間線
- 早在 2019 年的時(shí)候,社區(qū)有人反饋了一個(gè)問(wèn)題[2]:在一個(gè)包含 5000 個(gè)節(jié)點(diǎn)的集群中,創(chuàng)建一個(gè)大型的 Endpoints 對(duì)象(5000 個(gè) Pod,大小接近 1MB),kube-apiserver 可能會(huì)在 5 秒內(nèi)完全過(guò)載;
- 接著社區(qū)定位了這個(gè)問(wèn)題,并提出了 KEP 1152 less object serializations[3],通過(guò)避免為不同的 watcher 重復(fù)多次序列化相同的對(duì)象,降低 kube-apiserver 的負(fù)載和內(nèi)存分配次數(shù),此功能在 v1.17 中發(fā)布,在 5000 節(jié)點(diǎn)的測(cè)試結(jié)果,內(nèi)存分配優(yōu)化 ~15%,CPU 優(yōu)化 ~5%,但這個(gè)優(yōu)化僅對(duì) Http 協(xié)議生效,對(duì) WebSocket 不生效;
- 3 年后,也就是 2023 年,通過(guò) Refactor apiserver endpoint transformers to more natively use Encoders #119801[4] 對(duì)序列化邏輯進(jìn)行重構(gòu),統(tǒng)一使用 Encoder 接口進(jìn)行序列化操作,早在 2019 年就已經(jīng)創(chuàng)建對(duì)應(yīng)的 issue 83898[5]。本次重構(gòu)同時(shí)還解決了 2 提到的針對(duì) WebSocket 不生效的問(wèn)題,于 1.29 中發(fā)布;
所以如果你不是在以 WebSocket 形式(默認(rèn)使用 Http Transfer-Encoding: chunked)使用 watch,那么升級(jí)到 1.17 之后理論上就可以了。
原理
圖片
新增了 CacheableObject 接口,同時(shí)在所有 Encoder 中支持對(duì) 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 function will
// be used in case of cache miss. The function takes ownership
// of the object.
// If CacheableObject is a wrapper, then deep-copy of the wrapped object
// should be passed to function.
// CacheEncode assumes that for two different calls with the same ,
// 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 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 實(shí)現(xiàn)了 CacheableObject 接口,其 object 為關(guān)注的事件對(duì)象(例如 Pod),serializations 用來(lái)保存序列化之后的結(jié)果,Identifier 是一個(gè)標(biāo)識(shí),代表序列化的類型,因?yàn)榇嬖?json、yaml、protobuf 三種序列化方式。
cachingObject 的生成在上圖 Cacher dispatchEvent 消費(fèi)自身 incoming chan 數(shù)據(jù),將 event 發(fā)給所有相關(guān)的 cacheWatchers 的時(shí)候,會(huì)將事件對(duì)象轉(zhuǎn)化為 cachingObject 發(fā)給 cacheWatcher 的 input chan。最終的 Encode 操作是在 serveWatch 方法中將最終的對(duì)象進(jìn)行序列化時(shí)調(diào)用的,會(huì)先判斷是否已經(jīng)存在序列化的結(jié)果,存在則直接復(fù)用,避免重復(fù)的序列化。
注意:
上圖 wrap into cachingObject if len(watchers) >= 3 已成為過(guò)去式,新的代碼邏輯中已經(jīng)去掉了后面的判斷,不管 watchers 數(shù)量,統(tǒng)一都進(jìn)行 cachingObject 的封裝;
并沒(méi)有對(duì) Init Event(watchcache 中的全量數(shù)據(jù)) 進(jìn)行 cachingObject 的封裝,只有發(fā)給 Cacher incoming chan 的數(shù)據(jù)會(huì)轉(zhuǎn)化為 cachingObject。也就是說(shuō)這個(gè)優(yōu)化對(duì) Get/List 請(qǐng)求完全無(wú)效,因?yàn)樗麄兪侵苯訌?watchcache 返回?cái)?shù)據(jù)的,針對(duì) Watch 請(qǐng)求,也將會(huì)有部分?jǐn)?shù)據(jù)在返回時(shí)沒(méi)有復(fù)用已有序列化結(jié)果,因?yàn)槿匀豢赡軙?huì)有部分 Init Event 數(shù)據(jù)是從 watchcache 獲取并返回的,這是一個(gè)很神奇的地方,cacheWatcher 的 input chan 的 event 對(duì)象的 object 有可能是正常的資源對(duì)象,例如 Pod,也有可能是 CacheableObject 對(duì)象,而真正的資源對(duì)象則保存在 CacheableObject 的 object 中;
為什么不把 Init Event 也覆蓋了,KEP 1152 中給的說(shuō)法是先實(shí)現(xiàn) Cache incoming chan 的覆蓋,收益就已經(jīng)比較可觀了,解決了之前發(fā)現(xiàn)的問(wèn)題。如果需要進(jìn)一步優(yōu)化的話,再來(lái)重新評(píng)估把 Init Event 也覆蓋的可能。而在 Refactor streaming watch encoder to enable caching #120300[6] 的評(píng)論中也有相關(guān)討論
圖片
同時(shí)在 KEP 3157 watch-list[7] 中也提到了這個(gè)待優(yōu)化項(xiàng)。
優(yōu)化內(nèi)存分配
時(shí)間線
- reduce the number of allocations in the WatchServer during objects serialisation #108186[8],主要針對(duì) protobuf 進(jìn)行優(yōu)化,對(duì)于 json 和 yaml 序列化無(wú)效,2022 年隨著 v1.24 發(fā)布,protobuf 一般是內(nèi)部組件使用,而外部組件訪問(wèn) k8s 時(shí)一般都是使用 json 或者 yaml 序列化;
- Do not copy bytes for cached serializations #118362[9],自定義 SpliceBuffer,避免對(duì) cachingObject 的序列化結(jié)果進(jìn)行深拷貝,2023 年隨著 v1.28 發(fā)布;
- Refactor streaming watch encoder to enable caching #120300,這個(gè)修復(fù)是在已有的緩存資源對(duì)象的序列化結(jié)果的基礎(chǔ)上,把 Event 的序列化結(jié)果也做緩存,因?yàn)樽罱K返回給客戶端的是 Event 而不是資源對(duì)象;
原理
針對(duì) 2,巧妙地定義了 SpliceBuffer 通過(guò)淺拷貝的方式有效的優(yōu)化了內(nèi)存分配,避免 embeddedEncodeFn 對(duì)已經(jīng)序列化后的結(jié)果 []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
}
Benchmark 效果顯著
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
針對(duì) 3,嚴(yán)格來(lái)說(shuō)這個(gè) pr 不是用來(lái)優(yōu)化內(nèi)存分配的,而是來(lái)解決 issue 110146[10] 的提到的 json 序列化時(shí) json.compact 導(dǎo)致的 CPU 使用率過(guò)高的問(wèn)題,隨著 v1.29 發(fā)布。問(wèn)題產(chǎn)生的原因是雖然上面提到了通過(guò) cachingObject 來(lái)緩存資源對(duì)象的序列化結(jié)果,但最終發(fā)回到客戶端的是 Event 對(duì)象,還是需要做一次 Event 的序列化操作,而 json.compact 會(huì)在每次 Marshal 后被調(diào)用,這是 golang 自帶的 json 序列化的實(shí)現(xiàn),可以參考 golang json 源碼[11]。這個(gè)修復(fù)是在緩存資源對(duì)象的序列化結(jié)果的基礎(chǔ)上,把 Event 的序列化結(jié)果也做緩存,用來(lái)規(guī)避 json.compact 帶來(lái)的影響。
這個(gè) PR 涉及到的改動(dòng)較大,筆者目前對(duì)其實(shí)現(xiàn)仍然存在一些疑問(wèn),已經(jīng)提了 issue 122153[12] 咨詢社區(qū),等搞清楚后可以再專門安排一篇來(lái)講講這個(gè)實(shí)現(xiàn),這塊涉及到了 watch handler 的整個(gè)序列化邏輯,Encoder 的嵌套非常深,連 google 大神在 review 代碼時(shí)都有如下感嘆
圖片
圖片
筆者在看這塊代碼時(shí)被接口的來(lái)回跳轉(zhuǎn)搞暈了,寫了個(gè) unit test 來(lái)一步步調(diào)試才搞清楚這些 Encoder,真的是層層嵌套,梳理如下,可以感受下這五層嵌套
watchEncoder
—> watchEmbeddedEncoder
—> encoderWithAllocator
—> codec
—> json.Serializer
他們都實(shí)現(xiàn)了 Encoder 接口...
類似 cachingObject 序列化,對(duì) Event 進(jìn)行序列化同樣需要額外的內(nèi)存空間,但可以避免對(duì)每個(gè) Event 進(jìn)行多次序列化帶來(lái)的內(nèi)存消耗和 CPU 消耗,所以也起到了內(nèi)存優(yōu)化的作用。
效果
通過(guò) WatchList 以及上述的種種優(yōu)化,社區(qū)給出了優(yōu)化效果
優(yōu)化前
圖片
優(yōu)化后
圖片
最后
Kube-apiserver 內(nèi)存優(yōu)化系系列包含前面的鋪墊,到此也 6 篇了,如果把這其中涉及到的知識(shí)都搞懂了,對(duì) kube-apiserver 的理解一定可以上一個(gè)臺(tái)階,后續(xù)也會(huì)持續(xù)關(guān)注這塊的內(nèi)容,不定時(shí)補(bǔ)充~
序列化,聽(tīng)上去簡(jiǎn)單,調(diào)個(gè)方法的事情,但用好了也不容易,往往這種地方最能體現(xiàn)能力,尋常見(jiàn)功力,細(xì)微見(jiàn)真章,看看大牛寫的代碼,領(lǐng)會(huì)其中的設(shè)計(jì)和思想,總結(jié)轉(zhuǎn)化吸收為我所用。
k8s 使用起來(lái)容易,用好了不容易,搞明白背后是怎么回事難。項(xiàng)目經(jīng)過(guò) 10 來(lái)年的迭代,無(wú)論代碼量還是復(fù)雜度上面都已經(jīng)比較恐怖了,而且還在不斷地迭代更新,但路雖遠(yuǎn),行則將至,事雖難,做雖然不一定成吧,不做一定成不了。
Talk is cheap, Show me the code and PPT
最后,歡迎加筆者微信 YlikakuY,一起交流前沿技術(shù),行業(yè)動(dòng)態(tài)~
參考資料
[1]
kubernetes-api: https://kubernetes.io/zh-cn/docs/concepts/overview/kubernetes-api/
[2]issue#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]issue#83898: https://github.com/kubernetes/kubernetes/issues/83898
[6]pr#120300: https://github.com/kubernetes/kubernetes/pull/120300
[7]kep#3157 watch-list: 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]issue#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]issue#122153: https://github.com/kubernetes/kubernetes/issues/122153
新聞標(biāo)題:聊聊Kube-Apiserver內(nèi)存優(yōu)化進(jìn)階
文章網(wǎng)址:http://m.fisionsoft.com.cn/article/cdpdhch.html


咨詢
建站咨詢
