背景

项目背景

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 问题并无缓解。原因有二:

  1. sync.pool 是全局对象,读写存在竞争问题,因此在这方面会消耗一定的 CPU,但之所以通常用它优化后 CPU 会有提升,是因为它的对象复用功能对 GC 带来的优化,因此 sync.pool 的优化效果取决于锁竞争增加的 CPU 消耗与优化 GC 减少的 CPU 消耗这两者的差值;

  2. GC 压力的大小通常取决于 inuse_objects,与 inuse_heap 无关,也就是说与正在使用的对象数有关,与正在使用的堆大小无关;

本次优化时选择对 bytes.buffer 进行复用,是想做到减少堆大小的分配,出发点错了,对 GC 问题的理解有误,对 GC 的优化因从 pprof heap 图 inuse_objects 与 alloc_objects 两个指标出发。

甚至没有依赖经验, 只是单纯的想当然了

计算密集型服务 性能优化实战始末相关推荐

  1. IO 密集型服务 性能优化实战记录

    背景 项目背景 Feature 服务作为特征服务,产出特征数据供上游业务使用.服务压力:高峰期 API 模块 10wQPS,计算模块 20wQPS.服务本地缓存机制: 计算模块有本地缓存,且命中率较高 ...

  2. 2 周流量激增百倍的腾讯课堂后台扩容和性能优化实战

    作者:andyawang,腾讯 CSIG 后台开发工程师 疫情期间,学校网课需求激增,腾讯课堂 2 天上线极速版,2 周内支持同时在线人数超百倍增长,对整个后台挑战非常大.整整 2 个月下来,同合作团 ...

  3. JavaScript系列—性能优化之《网站性能优化实战——从12.67s到1.06s的故事》

    本篇博文来源于网络 226 人赞同了该文章 原文作者:IMWeb jerryOnlyZRJ  原文链接:网站性能优化实战--从12.67s到1.06s的故事 - 腾讯Web前端 IMWeb 团队社区 ...

  4. linux性能优化实战学习笔记-(1)CPU性能分析工具与套路

    版权归Linux性能优化实战 作者倪鹏飞,本文主要是为学习.整理相关知识点,请勿用作商用,侵删. linux性能分析工具 下图来自:Brendan D. Gregg http://www.brenda ...

  5. 《java性能优化实战》之编程性能优化

    目录 一.java编程性能优化实战 1.如何使用String.intern 节省内存 2.如何使用字符串的分割方法? 3.ArrayList还是LinkedList?使用不当性能差千倍 4.Strea ...

  6. Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿

    原标题:Android性能优化实战之界面卡顿 作者:红橙Darren https://www.jianshu.com/p/18bb507d6e62 今天是个奇怪的日子,有三位同学找我,都是关于界面卡顿 ...

  7. Linux性能优化实战学习笔记:第四十六讲=====实战分析

    Linux性能优化实战学习笔记:第四十六讲 一.上节回顾 不知不觉,我们已经学完了整个专栏的四大基础模块,即 CPU.内存.文件系统和磁盘 I/O.以及网络的性能分析和优化.相信你已经掌握了这些基础模 ...

  8. Linux性能优化实战学习笔记:第十讲==中断

    Linux性能优化实战学习笔记:第十讲 一.坏境准备 1.拓扑图 2.安装包 在第9节的基础上 在VM2上安装hping3依奈包 ? 1 2 3 4 5 6 7 wget http://www.tcp ...

  9. 网站性能优化实战(二)

    转自IMWeb社区,作者:jerryOnlyZRJ,原文链接 --从Webkit内部渲染机制出发,谈网站渲染性能优化 本文是对前文:imweb.io/topic/5b6fd- 相关知识的补充,文中的& ...

最新文章

  1. linux java进程消失_Linux系统下的Java进程无故消失怎么办?
  2. BERT可以上几年级了?Seq2Seq“硬刚”小学数学应用题
  3. wxWidgets:存档格式
  4. dtsi与dts_[dts]DTS实例分析
  5. UML 数据建模EA的基本使用——《用例图的使用》
  6. LeetCode之Island Perimeter
  7. JMX和Spring –第1部分
  8. 设计模式6---(单例模式的概念及其实现(懒汉式和饿汉式),线程安全)
  9. spring Autowired(required = false) 说明
  10. C# 实现软件自动更新升级程序
  11. pytorch flatten函数_1. PyTorch中的基本数据类型——张量
  12. DOTNET零碎总结---VB.NET修改数据存在多个txtbox时,SQL语句的操作
  13. python图书管理实训报告总结_图书管理系统设计实训报告
  14. Ubuntu18.04安装后检测不到集成声卡问题
  15. Code33 整数转罗马数字
  16. html怎么让音乐隐藏在网页中循环播放,怎么在网页中循环播放声音
  17. 基于mysql+php的英语四六级过级成绩管理
  18. 理解 Serenity,Part-1:深度抽象
  19. CSR蓝牙开发调试经验
  20. 快速检索2021年EI会议论文的方法

热门文章

  1. linux下qt实现计算器,QT实现计算器
  2. 使用 GraalVM 将基本的 Java 项目打包成 EXE
  3. Cortex-M3-异常与中断-向量表 s
  4. Python 面向对象 --- 文件
  5. 《C语言编程初学者指南》一1.9 本章小结
  6. 【Java】Float计算不准确
  7. 扩展的母函数(可以做减法的母函数)(当然只要你愿意也可以做乘除!)
  8. android升级SDK后,XML graphical layout无法预览的解决
  9. 分享字符串右移的算法
  10. 牛客 - 小朋友你是否有很多问号(容斥+组合数学)