计算密集型服务 性能优化实战始末
背景
项目背景
worker 服务数据链路图
worker 服务消费上游数据(工作日高峰期产出速度达近 200 MB/s,节假日高峰期可达 300MB/s 以上),进行中间处理后,写入多个下游。在实践中结合业务场景,基于快慢隔离的思想,以三个不同的 consumer group 消费同一 Topic,隔离三种数据处理链路。
面对问题
worker 服务在高峰期时 CPU Idle 会降至 60%,因其属于数据处理类计算密集型服务,CPU Idle 过低会使服务吞吐降低,在数据处理上产生较大延时,且受限于 Kafka 分区数,无法进行横向扩容;
对上游数据的采样率达 **30%**,业务方对数据的完整性有较大诉求,但系统 CPU 存在瓶颈,无法满足;
性能优化
针对以上问题,开始着手对服务 CPU Idle 进行优化;抓取服务 pprof profile 图如下:go tool pprof -http=:6061 http://「ip:port」/debug/pprof/profile
优化前的 pprof profile 图
服务与存储之间置换压力
背景
worker 服务消费到上游数据后,会先写全部写入 Apollo 的数据库,之后每分钟定时捞取处理,但消息体大小 P99 分位达近 1.5M,对于 Apollo 有较严重的大 Key 问题,再结合 RocksDB 特有的写放大问题,会进一步加剧存储压力。在这个背景下,我们采用 zlib 压缩算法,对消息体先进行压缩后再写入 Apollo,减缓读写大 Key 对 Apollo 的压力。
优化
在 CPU 的优化过程中,我们发现服务在压缩操作上占用了较多的 CPU,于是对压缩等级进行调整,以减小压缩率、增大下游存储压力为代价,减少压缩操作对服务 CPU 的占用,提升服务 CPU 。这一操作本质上是在服务与存储之间进行压力置换,是一种空间换时间的做法。
关于压缩等级
这里需要特别注意的是,在压缩等级的设置上可能存在较为严重的边际效用递减问题。在进行基准测试时发现,将压缩等级由 BestCompression 调整为 DefaultCompression 后,压缩率只有近 1‱ 的下降,但压缩方面的 CPU 占用却相对提高近 **50%**。此结论不能适用于所有场景,需从实际情况出发,但在使用时应注意这个问题,选择相对最优的压缩方式。
zlib 可设置的压缩等级
使用更高效的序列化库
背景
worker 服务在设计之初基于快慢隔离的思想,使用三个不同的 consumer group 进行分开消费,导致对同一份数据会重复消费三次,而上游产出的数据是在 PB 序列化之后写入 Kafka,消费侧亦需要进行 PB 反序列化方能使用,因此导致了 PB 反序列化操作在 CPU 上的较大开销。
优化
经过探讨和调研后发现,gogo/protobuf 三方库相较于原生的 golang/protobuf 库性能更好,在 CPU 上占用更低,速度更快,因此采用 gogo/protobuf 库替换掉原生的 golang/protobuf 库。
gogo/protobuf 为什么快
通过对每一个字段都生成代码的方式,取消了对反射的使用;
采用预计算方式,在序列化时能够减少内存分配次数,进而减少了内存分配带来的系统调用、锁和 GC 等代价。
用过去或未来换现在的时间:页面静态化、池化技术、预编译、代码生成等都是提前做一些事情,用过去的时间,来降低用户在线服务的响应时间;另外对于一些在线服务非必须的计算、存储的耗时操作,也可以异步化延后进行处理,这就是用未来的时间换现在的时间。出处:https://mp.weixin.qq.com/s/S8KVnG0NZDrylenIwSCq8g
关于序列化库
这里只列举的了 PB 序列化库的优化 Case,但在 JSON 序列化方面也存在一样的优化手段,如 json-iterator、sonic、gjson 等等,我们在 Feature 服务中先后采用了 json-iterator 与 gjson 库替换原有的标准库 JSON 序列化方式,均取得了显著效果。JSON 库调研报告:https://segmentfault.com/a/1190000041591284
调整压缩等级与更换 PB 序列化库之后
数据攒批 减少调用
背景
在观察 pprof 图后发现写 hbase 占用了近 50% 的相对 CPU,经过进一步分析后,发现每次在序列化一个字段时 Thrift 都会调用一次 socket->syscall,带来频繁的上下文切换开销。
优化
阅读代码后发现, 原代码中使用了 Thrift 的 TTransport 实现,其功能是包装 TSocket,裸调 Syscall,每次 Write 时都会调用 socket 写入进而调用 Syscall。这与通常我们的编码习惯不符,认为应该有一个 buffer 充当中间层进行数据攒批,当 buffer 写完或者写满后再向下层写入。于是进一步阅读 Thrift 源码,发现其中有多种 Transport 实现,而 TTBufferedTransport 是符合我们编码习惯的。
对 Thrift 调用进行优化,使用带 buffer 的 transport,大大减少对 Syscall的调用
更换 transport 之后,对 HBase 的调用消耗只剩最右侧的一条了
对 HBase 的访问耗时大幅下降
Thrift Client 部分源码分析
Transport 使用了装饰器模式
Transport 实现 | 作用 |
---|---|
TTransport | 包装 TSocket,裸调 Syscall,每次 Write 都会调用 syscall; |
TTBufferedTransport | 需要提前声明 buffer 的大小,在调用 Socket 之前加了一层 buffer,写满或者写完之后再调用 Syscall; |
TFramedTransport | 与 TTBufferedTransport 类似,但只会在全部写入 buffer 后,再调用 Syscall。数据格式为:size+content,客户端与服务端必须都使用该实现,否则会因为格式不兼容报错; |
streamTransport | 传入自己实现的 IO 接口; |
TMemoryBufferTransport | 纯内存交换,不与网络交互; |
Protocol
Protocol 实现 | 作用 |
---|---|
TBinaryProtocol | 直接的二进制格式; |
TCompactProtocol | 紧凑型、高效和压缩的二进制格式; |
TJSONProtocol | JSON 格式; |
TSimpleJSONProtocol | SimpleJSON 产生的输出适用于 AJAX 或脚本语言,它不保留Thrift的字段标签,不能被 Thrift 读回,它不应该与全功能的 TJSONProtocol 相混淆;https://cwiki.apache.org/confluence/display/THRIFT/ThriftUsageJava |
关于数据攒批
数据攒批:将数据先写入用户态内存中,而后统一调用 syscall 进行写入,常用在数据落盘、网络传输中,可降低系统调用次数、利用磁盘顺序写特性等,是一种空间换时间的做法。有时也会牺牲一定的数据实时性,如 kafka producer 侧。相似优化可见:https://mp.weixin.qq.com/s/ntNGz6mjlWE7gb_ZBc5YeA
语法调整
除在对库的使用上进行优化外,在 GO 语言本身的使用上也存在一些优化方式;
slice、map 预初始化,减少频繁扩容导致的内存拷贝与分配开销;
字符串连接使用 strings.builder(预初始化) 代替 fmt.Sprintf();
func ConcatString(sl ...string) string {n := 0for i := 0; i < len(sl); i++ {n += len(sl[i])}var b strings.Builderb.Grow(n)for _, v := range sl {b.WriteString(v)}return b.String()
}
buffer 修改返回 string([]byte) 操作为 []byte,减少内存 []byte -> string 的内存拷贝开销;
string <-> []byte 的另一种优化,需确保 []byte 内容后续不会被修改,否则会发生 panic;
func String2Bytes(s string) []byte {sh := (*reflect.StringHeader)(unsafe.Pointer(&s))bh := reflect.SliceHeader{Data: sh.Data,Len: sh.Len,Cap: sh.Len,}return *(*[]byte)(unsafe.Pointer(&bh))
}func Bytes2String(b []byte) string {return *(*string)(unsafe.Pointer(&b))
}
关于语法调整
更多语法调整,见以下文章
https://www.bacancytechnology.com/blog/golang-performance
https://mp.weixin.qq.com/s/Lv2XTD-SPnxT2vnPNeREbg
GC 调优
背景
在上次优化完成之后,系统已经基本稳定,CPU Idle 高峰期也可以维持在 80% 左右,但后续因业务诉求对上游数据采样率调整至 100%,CPU.Idle 高峰期指标再次下降至近 70%,且由于定时任务的问题,存在 CPU.Idle 掉 0 风险;
优化
经过对 pprof 的再次分析,发现 runtime.gcMarkWorker 占用不合常理,达到近 30%,于是开始着手对 GC 进行优化;
GC 优化前 pprof 图
方法一:使用 sync.pool()
通常来说使用 sync.pool() 缓存对象,减少对象分配数,是优化 GC 的最佳方式,因此我们在项目中使用其对 bytes.buffer 对象进行缓存复用,意图减少 GC 开销,但实际上线后 CPU Idle 却略微下降,且 GC 问题并无缓解。原因有二:
sync.pool 是全局对象,读写存在竞争问题,因此在这方面会消耗一定的 CPU,但之所以通常用它优化后 CPU 会有提升,是因为它的对象复用功能对 GC 带来的优化,因此 sync.pool 的优化效果取决于锁竞争增加的 CPU 消耗与优化 GC 减少的 CPU 消耗这两者的差值;
GC 压力的大小通常取决于 inuse_objects,与 inuse_heap 无关,也就是说与正在使用的对象数有关,与正在使用的堆大小无关;
本次优化时选择对 bytes.buffer 进行复用,是想做到减少堆大小的分配,出发点错了,对 GC 问题的理解有误,对 GC 的优化因从 pprof heap 图 inuse_objects 与 alloc_objects 两个指标出发。
甚至没有依赖经验, 只是单纯的想当然了
计算密集型服务 性能优化实战始末相关推荐
- IO 密集型服务 性能优化实战记录
背景 项目背景 Feature 服务作为特征服务,产出特征数据供上游业务使用.服务压力:高峰期 API 模块 10wQPS,计算模块 20wQPS.服务本地缓存机制: 计算模块有本地缓存,且命中率较高 ...
- 2 周流量激增百倍的腾讯课堂后台扩容和性能优化实战
作者:andyawang,腾讯 CSIG 后台开发工程师 疫情期间,学校网课需求激增,腾讯课堂 2 天上线极速版,2 周内支持同时在线人数超百倍增长,对整个后台挑战非常大.整整 2 个月下来,同合作团 ...
- JavaScript系列—性能优化之《网站性能优化实战——从12.67s到1.06s的故事》
本篇博文来源于网络 226 人赞同了该文章 原文作者:IMWeb jerryOnlyZRJ 原文链接:网站性能优化实战--从12.67s到1.06s的故事 - 腾讯Web前端 IMWeb 团队社区 ...
- linux性能优化实战学习笔记-(1)CPU性能分析工具与套路
版权归Linux性能优化实战 作者倪鹏飞,本文主要是为学习.整理相关知识点,请勿用作商用,侵删. linux性能分析工具 下图来自:Brendan D. Gregg http://www.brenda ...
- 《java性能优化实战》之编程性能优化
目录 一.java编程性能优化实战 1.如何使用String.intern 节省内存 2.如何使用字符串的分割方法? 3.ArrayList还是LinkedList?使用不当性能差千倍 4.Strea ...
- Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿
原标题:Android性能优化实战之界面卡顿 作者:红橙Darren https://www.jianshu.com/p/18bb507d6e62 今天是个奇怪的日子,有三位同学找我,都是关于界面卡顿 ...
- Linux性能优化实战学习笔记:第四十六讲=====实战分析
Linux性能优化实战学习笔记:第四十六讲 一.上节回顾 不知不觉,我们已经学完了整个专栏的四大基础模块,即 CPU.内存.文件系统和磁盘 I/O.以及网络的性能分析和优化.相信你已经掌握了这些基础模 ...
- Linux性能优化实战学习笔记:第十讲==中断
Linux性能优化实战学习笔记:第十讲 一.坏境准备 1.拓扑图 2.安装包 在第9节的基础上 在VM2上安装hping3依奈包 ? 1 2 3 4 5 6 7 wget http://www.tcp ...
- 网站性能优化实战(二)
转自IMWeb社区,作者:jerryOnlyZRJ,原文链接 --从Webkit内部渲染机制出发,谈网站渲染性能优化 本文是对前文:imweb.io/topic/5b6fd- 相关知识的补充,文中的& ...
最新文章
- linux java进程消失_Linux系统下的Java进程无故消失怎么办?
- BERT可以上几年级了?Seq2Seq“硬刚”小学数学应用题
- wxWidgets:存档格式
- dtsi与dts_[dts]DTS实例分析
- UML 数据建模EA的基本使用——《用例图的使用》
- LeetCode之Island Perimeter
- JMX和Spring –第1部分
- 设计模式6---(单例模式的概念及其实现(懒汉式和饿汉式),线程安全)
- spring Autowired(required = false) 说明
- C# 实现软件自动更新升级程序
- pytorch flatten函数_1. PyTorch中的基本数据类型——张量
- DOTNET零碎总结---VB.NET修改数据存在多个txtbox时,SQL语句的操作
- python图书管理实训报告总结_图书管理系统设计实训报告
- Ubuntu18.04安装后检测不到集成声卡问题
- Code33 整数转罗马数字
- html怎么让音乐隐藏在网页中循环播放,怎么在网页中循环播放声音
- 基于mysql+php的英语四六级过级成绩管理
- 理解 Serenity,Part-1:深度抽象
- CSR蓝牙开发调试经验
- 快速检索2021年EI会议论文的方法