性能调优总结 | 青训营

169 阅读7分钟

性能优化

  1. 前提是满足正确可靠、简洁清晰等质量因素
  2. 综合评估,时间和空间效率对立
  3. 根据go语言特性,提出go相关的优化建议

 

Benchmark-性能测试工具: Go test -bench=. -benchmem

执行结果

  • BenchmarkFib10-8:测试函数名,-8标识GOMAXPROCS(CPU核数)值为8
  • 1866870:表示一共执行1855870次
  • 602.5 ns/op 每次执行花费时间
  • 0.8 B/op  每次执行申请多大内存
  • 0 allocs/op 每次执行申请几次内存

Slice预分配内存: 尽可能在使用make()初始化切片时提供容量信息 image.png 扩容过程会耗时

大内存未释放(在已有的切片基础上创建切片,不会创建新的底层数组) 1.根据原始数组创建新的切片 2.创建新的切片,将原始数组内容拷贝

使用copy代替re-slice

Map预分配内存(添加元素时会导致扩容,因此提前分配空间可以减少rehash的消耗)

字符串处理

  • +,拼接//最差(会开辟新空间)
  • String.Builder//耗时最短
  • Byte.Buffer//

字符串是不可变类型,内存大小固定. 每次使用+会重新分配内存. String.Builder和bytes.Buffer底层都是[]bytes数组. 内存扩容策略,拼接时不需要重新分配内存.

String.Builder:直接将底层的byte数组转为字符串类型返回 bytes.Buffer:将bytebuffer转为字符串时重新申请了一块空间

空结构体不占据任何内存空间

Atomic包(优于加锁)

  • 锁实现通过系统调用实现
  • Atomic通过硬件实现,效率比锁高
  • Sync.Mutex应该用来保护一段逻辑而不仅仅是一个变量
  • 对非数值操作,可以使用atomic.Value,能承载一个interface()

Go语言优化

  • 性能优化:减少组件中不必要的性能消耗
  • 业务层优化:针对特定场景,具体问题具体分析
  • 语言运行时优化
  • 解决通用性能问题

数据驱动

  1. 采用pprof,定位优化问题
  2. 依靠数据而非猜测
  3. 优先优化最大瓶颈 image.png
  • 保证接口稳定前提下改进具体实现
  • 更多测试
  • 撰写文档
  • 隔离性:选项控制是否开启优化
  • 可观测:必要的日志输出

自动内存管理

  • 对象:动态内存(运行时根据需求分配的内存)‘
  • 自动内存管理(垃圾回收):由程序余元运行时系统管理动态内存(避免手动管理,保证正确性和安全性)
  • Double-free:两次释放内存
  • Use-after-free:释放之后再次使用

核心任务:

  1. 微信对象分配空间
  2. 找到存活空间
  3. 回收死亡对象的内存空间

相关概念:

  • Mutator:业务线程,分配新对象,修改对象指向关系
  • Collector:GC线程,找到存货对象,回收死亡对象的内存空间

image.png

Serial GC:只有一个collector/会有pause

image.png

Parallel GC:支持多个collectors同时回收GC算法/会有pause

image.png

Concurrent GC:业务线程和GC线程可以同时执行,/Collector必须感知对象指向关系的改变

image.png

在gc过程中产生了新的引用应该如何标记?因此该过程中必须感知到指向关系的改变如何解决?

Go v1.3(serial 和parallel GC)(同Mark and sweep法)

  1. 将所有业务逻辑暂停并找到所有可达和不可达的对象
  2. 启动collector开始标记所有可达的对象
  3. 清除所有未标记的对象
  4. 结束暂停

Go v1.5

三色标记法

  • 白色:垃圾对象
  • 灰色:被标记,但其对象下的属性未被完全标记
  • 黑色:被标记,且对象下的属性也被完全标记

步骤:

  1. 只要创建新对象,默认都标记为“白色”

image.png

  1. 每次GC开始回收,从根节点遍历所有对象,将遍历到的对象从“白色”标记为“灰色”

image.png

  1. 遍历灰色对象,将灰色对象引用的对象全部标记为灰色,再将该灰色对象标记为黑色,重复操作直到没有灰色对象

image.png

  1. 回收所有白色对象

Bug:当一个灰色对象C和一个白色对象D断开引用(D应该被回收)此时一个黑色对象A(已经被标记扫描后,其引用的应该全为黑)再被扫描之后再次引用了D,此时D本应该不会被回收,但是它错过了A的扫描时间,因此还是会被回收,这样就出现了bug。

解决方法:

  1. 强三色不变式:破坏条件一、不允许黑色对象引用白色对象
  2. 弱三色不变式:破坏条件二、黑色可以引用白色对象,但必须保证其必须存在灰色对象的引用(这样才能保证其能被再次扫描)
屏障机制:(只能用于堆)

插入屏障:在对象别引用时将被引用对象强制变成灰色(强三色不变,黑色不会引用白色)

删除屏障:断开引用时将被断开的标记为灰色(弱三色不变式)

Go v1.8

  1. GC开始时将栈上对象全部扫描并标记为黑色,之后不在进行重复扫描
  2. GC期间,在栈上创建任何对象均标记为黑色
  3. 被删除的对象标记为灰色
  4. 被添加的对象标记为灰色

Go v1.3版本,普通的标记清除法,整体需要STW,效率较低;

Go v1.5版本,三色标记法,堆空间启用屏障,栈空间不启用,全部扫描后,需要重新扫描一次栈(需要STW),效率普通;

Go v1.8版本后,三色标记法+混合写屏障,栈空间不启动屏障,堆空间启动,几乎不需要STW,整体效率较高;

 

GC算法评价

  1. Safety:不能回收存活的对象
  2. Throughout:1-GC时间/程序执行总时间
  3. Pause time:STW 业务感知到暂停
  4. 内存开销:GC的内存开心

Tracing garbage collection:追踪垃圾回收

  1. 标记根对象(静态变量、全局变量、常量、线程栈)
  2. 找到可达对象
  3. 清理所有不可达对象

清理策略

  1. copying GC:将存活对象复制到另外的内存空间

image.png

  1. mark-sweep;将死亡对象的内存标记为可分配

image.png

  1. mark-compact(原地整理对象):移动并整理存活对象

image.png

分代GC

对象年龄:经过GC的次数

将年轻的对象和老年对象制定不同的GC策略,降低内存管理的开销(不同年龄对象处于heap不同区域)

image.png

Young generation()

  1. 常规的对象分配
  2. 存活对象较少
  3. GC吞吐量很高
  • 数量少,可以采用copy
  • Old generation
  • 对象一直活着,反复复制开销大
  • 可以采用mark-sweep方法

引用计数

每个对象都有一个与之关联的引用计数,存活条件:当且仅当引用计数大于0 image.png

优点:

  1. 管理操作被平摊到程序执行过程
  2. 不需要了解runtime细节

缺点

  1. 维护引用计数开销大
  2. 无法回收环形数据结构(weak reference)
  3. 内存开销,引入额外内存空间
  4. 回收仍然会引发暂停(回收大数据结构)

性能调优实战

  1. 要依靠数据而不是猜测
  2. 要定位最大瓶颈而不是细枝末节
  3. 不要过早优化
  4. 不要过度优化

Pprof:耗费cpu和内存

image.png

找cpu占用的原因,定位:

Top命令:

  • Falt:当前函数本身耗时
  • Falt% flat占CPU总时间比例
  • Sum% 上面每一行的flat%总和
  • Cum 指当前函数本身加上其调用函数的总耗时
  • Cum cum占cpu总时间比例
  • Flat==cum:当前函数没有调用函数
  • Falt==0 :只调用了其他函数
Heap-堆内存
  • Go too pprof -http=:8080
  • 显示各个方法占用内存比例
  • Top视图
  • Source视图(类似list)

Alloc_obejects:程序累计申请对象数 Alloc_space:程序累计申请内存大小 Inuse_objects:程序当前持有的对象数 Inuse_space:程序当前占用的内存大小

Goroutine

Go tool pprof -http-:8080

火焰图(从上到下表示对应函数,一块代表一个函数,越长表示cpu越长) 搜素定位到对应函数进行注释

Mutex-锁

  • 修改命令后缀为mutex
  • 在流程图找方法
  • 在source中找有问题的代码
  • 进行修改

业务服务优化:

  1. 建立服务性能评估手段
  2. 分析性能数据,定位性能瓶颈
  3. 重点优化项改造
  4. 优化效果验证