内存优化是一个经典问题,在看具体 K8S 做了哪些工作之前,可以先抽象一些这个过程,思考一下如果是我们的话,会如何来优化。这个过程可以简单抽象为外部并发请求从服务端获取数据,如何在不影响吞吐的前提下降低服务端内存消耗?一般有几种方式:
创新互联公司是一家专注于成都网站建设、成都做网站与策划设计,剑川网站建设哪家好?创新互联公司做网站,专注于网站建设10余年,网设计领域的专业建站公司;建站业务涵盖:剑川等地区。剑川做网站价格咨询:18980820575
数据压缩在这个场景可能不适用,压缩确实可以降低网络传输带宽,从而提升请求响应速度,但对服务端内存的优化没有太大的作用。kube-apiserver 已经支持基于 gzip 的数据压缩,只需要设置 Accept-Encoding 为 gzip 即可,详情可以参考官网[1]介绍。
当然缓存序列化的结果适用于客户端请求较多的场景,尤其是服务端需要同时把数据发送给多个客户时,缓存序列化的结果收益会比较明显,因为只需要一次序列化的过程即可,只要完成一次序列化,后续给其他客户端直接发送数据时直接使用之前的结果即可,省去了不必要的 CPU 和内存的开销。当然缓存序列化的结果这个操作本身来说也是会占用一些内存的,如果客户端数量较少,那么这个操作可能收益不大甚至可能带来额外的内存消耗。kube-apiserver watch 请求就与这个场景非常吻合。
下文会就 kube-apiserver 中是如何就这两点进行的优化做一个介绍。
下文列出的时间线中的各种问题和优化可能而且有很大可能只是众多问题和优化中的一部分。
所以如果你不是在以 WebSocket 形式(默认使用 Http Transfer-Encoding: chunked)使用 watch,那么升级到 1.17 之后理论上就可以了。
图片
新增了 CacheableObject 接口,同时在所有 Encoder 中支持对 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 实现了 CacheableObject 接口,其 object 为关注的事件对象(例如 Pod),serializations 用来保存序列化之后的结果,Identifier 是一个标识,代表序列化的类型,因为存在 json、yaml、protobuf 三种序列化方式。
cachingObject 的生成在上图 Cacher dispatchEvent 消费自身 incoming chan 数据,将 event 发给所有相关的 cacheWatchers 的时候,会将事件对象转化为 cachingObject 发给 cacheWatcher 的 input chan。最终的 Encode 操作是在 serveWatch 方法中将最终的对象进行序列化时调用的,会先判断是否已经存在序列化的结果,存在则直接复用,避免重复的序列化。
注意:
上图 wrap into cachingObject if len(watchers) >= 3 已成为过去式,新的代码逻辑中已经去掉了后面的判断,不管 watchers 数量,统一都进行 cachingObject 的封装;
并没有对 Init Event(watchcache 中的全量数据) 进行 cachingObject 的封装,只有发给 Cacher incoming chan 的数据会转化为 cachingObject。也就是说这个优化对 Get/List 请求完全无效,因为他们是直接从 watchcache 返回数据的,针对 Watch 请求,也将会有部分数据在返回时没有复用已有序列化结果,因为仍然可能会有部分 Init Event 数据是从 watchcache 获取并返回的,这是一个很神奇的地方,cacheWatcher 的 input chan 的 event 对象的 object 有可能是正常的资源对象,例如 Pod,也有可能是 CacheableObject 对象,而真正的资源对象则保存在 CacheableObject 的 object 中;
为什么不把 Init Event 也覆盖了,KEP 1152 中给的说法是先实现 Cache incoming chan 的覆盖,收益就已经比较可观了,解决了之前发现的问题。如果需要进一步优化的话,再来重新评估把 Init Event 也覆盖的可能。而在 Refactor streaming watch encoder to enable caching #120300[6] 的评论中也有相关讨论
图片
同时在 KEP 3157 watch-list[7] 中也提到了这个待优化项。
针对 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
}
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
针对 3,严格来说这个 pr 不是用来优化内存分配的,而是来解决 issue 110146[10] 的提到的 json 序列化时 json.compact 导致的 CPU 使用率过高的问题,随着 v1.29 发布。问题产生的原因是虽然上面提到了通过 cachingObject 来缓存资源对象的序列化结果,但最终发回到客户端的是 Event 对象,还是需要做一次 Event 的序列化操作,而 json.compact 会在每次 Marshal 后被调用,这是 golang 自带的 json 序列化的实现,可以参考 golang json 源码[11]。这个修复是在缓存资源对象的序列化结果的基础上,把 Event 的序列化结果也做缓存,用来规避 json.compact 带来的影响。
这个 PR 涉及到的改动较大,笔者目前对其实现仍然存在一些疑问,已经提了 issue 122153[12] 咨询社区,等搞清楚后可以再专门安排一篇来讲讲这个实现,这块涉及到了 watch handler 的整个序列化逻辑,Encoder 的嵌套非常深,连 google 大神在 review 代码时都有如下感叹
图片
图片
笔者在看这块代码时被接口的来回跳转搞晕了,写了个 unit test 来一步步调试才搞清楚这些 Encoder,真的是层层嵌套,梳理如下,可以感受下这五层嵌套
watchEncoder
—> watchEmbeddedEncoder
—> encoderWithAllocator
—> codec
—> json.Serializer
他们都实现了 Encoder 接口...
类似 cachingObject 序列化,对 Event 进行序列化同样需要额外的内存空间,但可以避免对每个 Event 进行多次序列化带来的内存消耗和 CPU 消耗,所以也起到了内存优化的作用。
通过 WatchList 以及上述的种种优化,社区给出了优化效果
优化前
图片
优化后
图片
Kube-apiserver 内存优化系系列包含前面的铺垫,到此也 6 篇了,如果把这其中涉及到的知识都搞懂了,对 kube-apiserver 的理解一定可以上一个台阶,后续也会持续关注这块的内容,不定时补充~
序列化,听上去简单,调个方法的事情,但用好了也不容易,往往这种地方最能体现能力,寻常见功力,细微见真章,看看大牛写的代码,领会其中的设计和思想,总结转化吸收为我所用。
k8s 使用起来容易,用好了不容易,搞明白背后是怎么回事难。项目经过 10 来年的迭代,无论代码量还是复杂度上面都已经比较恐怖了,而且还在不断地迭代更新,但路虽远,行则将至,事虽难,做虽然不一定成吧,不做一定成不了。
Talk is cheap, Show me the code and PPT
最后,欢迎加笔者微信 YlikakuY,一起交流前沿技术,行业动态~
[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
当前文章:聊聊Kube-Apiserver内存优化进阶
当前链接:http://www.shufengxianlan.com/qtweb/news16/487816.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联