记一次 golang 的 zstd 压缩、解压缩 50%性能优化
问题背景
1、开发反馈 trs 的 stg 环境开启 zstd 解压缩后,内存有明显持续上涨趋势,最终导致 OOM
如图,内存频繁申请释放,当时分析导致 OOM 的原因是因为 stg 的 CPU 不够,导致 GC 不及时,调整 CPU 资源后确实 OOM 没有了。并未怀疑程序本身的性能问题
2、infra 同学发现 adx 的服务存在 zstd 压缩导致 CPU 资源消耗异常的问题,发现是压缩对象的 init 操作非常重导致。
问题分析
结合上面两次问题,想到 Redis 压缩降本时提交的 go 的 zstd 代码有很大优化空间的。可将 zstd.NewWriter 、zstd.NewReader 等重对象使用 sync.Pool 缓存起来,每次使用时从池中取,用完在放回去,避免频繁 New 对象造成内存申请多从而造成 GC 压力大,CPU 资源消耗高的问题。
预期关键结果(收益)
-
开压缩相关的接口 RT 明显降低,压缩&解压缩申请的内存变少
-
CPU 资源显著降低,部分实例可减少申请 CPU 的 request 和 limit (减少实例数)
总的来说应该可以提高性能,降低资源消耗,可降本增效。
解决
原来的 zstd 压缩代码
// Deprecated // 该方法已废弃,请使用 CompressWithZstd 代替 func CompressWithZstdOld(data []byte) ([]byte, error) { if len(data) == 0 { return data, errors.New("data is empty") } var compressedData bytes.Buffer enc, err := zstd.NewWriter(&compressedData) if err != nil { return nil, err } _, err = enc.Write(data) if err != nil { err := enc.Close() if err != nil { return nil, err } return nil, err } err = enc.Close() if err != nil { return nil, err } return compressedData.Bytes(), nil }
优化后的 zstd 压缩代码
var encoderPool = sync.Pool{ New: func() interface{} { enc, err := zstd.NewWriter(nil) if err != nil { log.Fatalf("Failed to create new Zstd Encoder: %v", err) } return enc }, } // CompressWithZstd zstd 压缩,空字符串返回空字符串 func CompressWithZstd(data []byte) ([]byte, error) { if len(data) == 0 { return data, errors.New("data is empty") } enc := encoderPool.Get().(*zstd.Encoder) defer encoderPool.Put(enc) var compressedData bytes.Buffer enc.Reset(&compressedData) _, err := enc.Write(data) if err != nil { err := enc.Close() return nil, err } return compressedData.Bytes(), nil }
原来的 zstd 解压缩代码
// Deprecated // 该方法已废弃,请使用 DeCompressWithZstd 代替 func DeCompressWithZstdOld(compressedData []byte) ([]byte, error) { if len(compressedData) == 0 { return compressedData, errors.New("compressedData is empty") } var decompressedData bytes.Buffer dec, err := zstd.NewReader(bytes.NewReader(compressedData)) if err != nil { return nil, err } _, err = io.Copy(&decompressedData, dec) if err != nil { dec.Close() return nil, err } return decompressedData.Bytes(), nil }
优化后的 zstd 解压缩代码
var decoderPool = sync.Pool{ New: func() interface{} { dec, err := zstd.NewReader(nil) if err != nil { log.Fatalf("Failed to create new Zstd Decoder: %v", err) } return dec }, } // DeCompressWithZstd zstd 解压,空字符串返回空字符串 func DeCompressWithZstd(compressedData []byte) ([]byte, error) { if len(compressedData) == 0 { return compressedData, errors.New("compressedData is empty") } dec := decoderPool.Get().(*zstd.Decoder) defer decoderPool.Put(dec) var decompressedData bytes.Buffer dec.Reset(bytes.NewReader(compressedData)) _, err := io.Copy(&decompressedData, dec) if err != nil { return nil, err } return decompressedData.Bytes(), nil }
benchmark
测试在本地开发机进行,测试字符串保持一致
goos: darwin goarch: amd64 pkg: git.gametaptap.com/tapad/go-utils/utils cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz BenchmarkCompressWithNewGzip-16 25621 45620 ns/op 4145 B/op 6 allocs/op BenchmarkCompressWithNewGzipOld-16 6871 186002 ns/op 817830 B/op 23 allocs/op BenchmarkCompressWithZstd-16 231540 5177 ns/op 3456 B/op 2 allocs/op BenchmarkCompressWithZstdOld-16 530 1994072 ns/op 23740092 B/op 60 allocs/op BenchmarkDeCompressWithZstd-16 2183894 538.2 ns/op 1418 B/op 1 allocs/op BenchmarkDeCompressWithZstdOld-16 76734 15489 ns/op 11662 B/op 36 allocs/op PASS
性能测试结果解析:从两个维度解析,RT 性能和内存分配都有非常大的提升
-
zstd 压缩:RT 优化前一次压缩需要
1994072
ns, 优化后只需要5177
ns 。内存更甚,优化前一次压缩需要分配 60次内存,优化后只需要 2次(实际多协程下不只一次) -
zstd 解压缩:RT 优化前一次解压缩需要
15489
ns, 优化后只需要538.2
ns 。 内存优化前一次解压缩需要分配 36次内存,优化后也只需要分配1次。(实际多协程下不止一次)
从结果看应该是一次性价比很高的优化。
关键结果回收
内存申请数据
- 优化前 zstd 的内存申请量占到了整个应用的 28%,优化后占用 1% 都不到
GC 数据
- 优化前,因为频繁申请内存,GC 压力很大,GC 占用了 CPU 的 61.92%的时间。
- 优化后,GC 占用了 CPU 的时间不到 10%。
接口 RT 数据
优化后 RT 减少 1~3ms ,且 RT 非常稳定,消除了毛刺现象(可能是GC 压力大影响的)
CPU 资源数据
不完全统计,多个服务优化后,总计释放了 542C CPU 的资源。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
bean的一生
你曾读spring源码 “不知所云”、“绞尽脑汁”、“不知所措”嘛🤣🤣🤣 那这篇文章可能会对你有所帮助,小编尝试用简单、易懂的例子来模拟spring经典代码👉Spring Bean生命周期及扩展点, 让你能够轻松的读懂Spring Bean的生命周期,更加深入的理解Spring。 那好,下面小编将从如下几个步骤来介绍✍️✍️✍️ 1》回顾Spring Bean相关知识点 1.1》什么是Bean 1.2》什么是 Spring Bean 的生命周期 1.3》 Spring Bean 的生命周期的扩展点 2》模拟 Spring Bean 生命周期及扩展点 2.1》 流程图 2.2》 代码功能介绍 2.3》 代码实现 2.3.1》 指定扫描路径 2.3.2》 扫描、生成classList 2.3.3》 bean定义、建立beanName映射关系、保存beanPostProcessor对象 2.3.4》 实例化bean、对象填充属性、执行award方法、BeanPostProcessor干预、初始化、放入单例池、获取bean 2.3.5》 业务类实现 2.3.6》 运行结果 3》总结...
- 下一篇
一种轻量分表方案-MyBatis 拦截器分表实践
背景 部门内有一些亿级别核心业务表增速非常快,增量日均100W,但线上业务只依赖近一周的数据。随着数据量的迅速增长,慢SQL频发,数据库性能下降,系统稳定性受到严重影响。本篇文章,将分享如何使用MyBatis拦截器低成本的提升数据库稳定性。 业界常见方案 针对冷数据多的大表,常用的策略有以2种: 1. 删除/归档旧数据。 2. 分表。 归档/删除旧数据 定期将冷数据移动到归档表或者冷存储中,或定期对表进行删除,以减少表的大小。此策略逻辑简单,只需要编写一个JOB定期执行SQL删除数据。我们开始也是用这种方案,但此方案也有一些副作用: 1. 数据删除会影响数据库性能,引发慢sql,多张表并行删除,数据库压力会更大。 2. 频繁删除数据,会产生数据库碎片,影响数据库性能,引发慢SQL。 综上,此方案有一定风险,为了规避这种风险,我们决定采用另一种方案:分表。 分表 我们决定按日期对表进行横向拆分,实现让系统每周生成一张周期表,表内只存近一周的数据,规避单表过大带来的风险。 分表方案选型 经调研,考虑2种分表方案:Sharding-JDBC、利用Mybatis自带的拦截器特性...
相关文章
文章评论
共有0条评论来说两句吧...