这是我参与「第三届青训营 -后端场」笔记创作活动的第7篇笔记
1 通过完成实验来了解pprof的使用流程、
- 首先我们需要了解到pprof是将当前采集到的数据输出到profile文件中的,所以我们就需要先拿到这个profile文件,我们可以通过该文件来获取对应的信息,输入指令
go tool pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=10"同时可以通过topN来查看运行时间(占用)最高的几个服务 - 接着我们使用
list eat这个指令来查看代码的所在位置来排除低性能。可以看到图中,运行时间最多的是这个for循环,于是我们将有关的代码注释掉,然后对数据进行观测。
如果想要使用web指令,则需要先安装插件
Graphviz,安装后配置bin环境变量即可同样可以定位到炸弹所在位置
- 接下来分析发现内存的利用率依然很高,我们继续来排除原因
对于内存问题,可以通过查看堆内存的方法来查看到底是哪里存在问题
go tool pprof -http=:8080 "http://127.0.0.1:6060/debug/pprof/heap",如图:从而定位到原因代码2
还可以通过alloc_space来查看目前是存在无意义的内存分配,比如说这里的话就是一直在申请16MB的内存,但是由于申请了没有用,会马上被GC回收掉,所以就不会显示在insue里面
alloc_objects:程序累计申请的对象数alloc_spoace:程序累计申请的内存大小insue_objects:程序当前持有的对象数insue_space:程序当前占用的内存大小然后我们继续分析,发现开启的协程到达了100的量级,这对于一个简单的小程序来说是很不正常的,我们使用
火焰图对其进行排查我们发现此循环不断启动新协程,而且每启动还强制睡眠阻塞,我们将其注释掉
完成后继续分析锁的问题
至此就已经将程序中的所有炸弹排除了
2. 业务服务优化基本概念
- 服务:能够单独部署,承载一定功能的程序
- 依赖:Service A的功能实现依赖Service B的响应结果,称为Service A依赖Service B
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
3. 内存管理基础
3.1 自动内存管理
- 动态内存:程序在运行时根据需求动态分配的内存:
malloc - 自动内存管理(
垃圾回收):由程序语言的运行时系统管理动态内存- 避免手动管理内存,专注于实现业务逻辑
- 保证内存的使用性和安全性
Mutator:业务线程,分配新对象,修改对象的指向关系Collector:GC线程,找到存活对象, 回收死亡对象的内存空间Serial GC:只有一个CollectorParallel GC:支持多个Collector同时回收的GC算法Concurrent GC:Mutator(s)和collector(s)可以同时执行,Collector(s)必须感知到对象指向关系的改变在GC前一个对象所指向的对象均未被标记,当GC启动后,会将存活的对象都标记上,但是当此时业务进程正在执行,将对象又指向了一个新对象的时候,这时候GC可能会疏忽漏标记了b对象,导致b对象被回收。
- 评价GC算法的指标
- 基本要求:不能回收存活的对象
- 吞吐率:1-(GC时间/程序运行时间)
- 暂停时间:stop the world(STW)业务是否感知到了
- 内存开销:GC元数据开销
- 追踪垃圾回收
- 对象被回收的条件:指针指向关系不可达的对象
- 标记根对象
- 静态变量、全局变量、常量、线程栈等
- 标记:找到可达对象
- 求指针指向关系的闭包:从根对象出发,找到所有可达对象
- 清理:清理所有不可达对象
- 将存活对象复制到另外的内存空间(copying GC)
- 将死亡对象的内存区域标记为可分配(mark-sweep GC),使用free-list管理内存
- 移动并整理存活对象(mark-compact GC),原地整理
- 根据对象的生命周期,使用不同的标记和清理策略
- 引用计数
- 每个对象存有一个引用次数的字段,对象存活的条件就是当其引用次数>0的时候
- 优点
- 内存管理的操作被平摊到程序执行的过程中
- 内存管理不需要了解runtime的实现细节,只需要维护引用次数即可
- 缺点
- 当在多线程并发执行的时候,有可能会对同一个对象进行操作,因此需要对对象的操作原子化,通过原子操作保证对引用计数操作的原子性和可见性
- 当存在环形的数据结构的时候,是无法回收的
- 内存开销,每个对象都引用额外内存空间存储引用数目
- 回收内存时可能引发暂停
3.2 分代GC
分代假说:most objects die young,很多对象在分配出来之后就不再使用了- 每个对象都有年龄:所谓年龄就是该对象经历GC的次数
- 目的:针对不同年龄的对象制定不同的GC策略,降低整体内存的开销
- 不同年龄的对象处于heap的不同区域
- 对于年轻代而言,由于
分代假说的存在,年轻代总是趋向于年龄较低面临被GC的威胁,因此存活的对象较少,此时可以采用copying GC的策略,这时候的代价将会比较低 - 对于老年代而言,由于对象趋向于一致活着,反复复制开销很大,采用
copying GC的策略是不合适的,这时候采用Mark-sweep collection的策略较好
4. GO内存管理
4.1 分块
- 目标:为对象在heap上分配内存
- 提前将内存进行分配
- 调用系统调用mmap(),向OS申请一大块内存,例如4MB
- 先将内存分成大块,例如8KB,称为mspan
- 再将大块继续划分成
特定大小的小块,用于对象分配(有点类似伙伴系统?) - noscan mspan:分配不包含指针的对象,GC不需要扫描
- scan mspan:分配包含指针的对象,GC需要扫描
- 对象分配:根据对象大小,选择最合适的块返回
4.2 缓存
- TCMalloc:thread caching
- 每个p包含一个mcache用于快速分配,用于绑定于p上的g分配对象
- mcache管理一组mspan
- 当mcache中的mspan分配完毕,向mcentral申请带有未分配块的mspan
- 当mspan中没有未分配对象的时候,mspan会被缓存在mcentral中,而不是立即返回给OS
4.3 GO内存管理优化
- 对象分配是非常高频的操作:每秒分配
GB级的内存 - 小对象的
占比比较高 - Go内存分配比较耗时
- 分配路径长 :g->m->p->mcache->mspan->memory block->return pointer
- pprof:对象分配函数是最频繁调用的函数之一
4.4 Balanced GC
- 将 noscan 对象在 per-g allocation buffer (GAB) 上分配,并使用移动对象 GC 管理这部分内存,提高对象分配和回收效率
- 每个 g 会附加一个较大的 allocation buffer (例如 1 KB) 用来分配小于 128 B 的 noscan 小对象
- 分配对象时,根据对象大小移动 top 指针并返回,快速完成一次对象分配
- 同原先调用 mallocgc() 进行对象分配的方式相比,balanced GC 缩短了对象分配的路径,减少了对象分配执行的指令数目,降低 CPU 使用
- 从 Go runtime 内存管理模块的角度看,一个 allocation buffer
其实是一个大对象。本质上 balanced GC 是将多次小对象的分配合并成一次大对象的分配。因此,当 GAB 中哪怕只有一个小对象存活时,Go runtime 也会认为整个大对象(即 GAB)存活。为此,balanced GC 会根据 GC 策略,将 GAB 中存活的对象移动到另外的 GAB 中,从而压缩并清理 GAB 的内存空间,原先的 GAB 空间由于不再有存活对象,可以全部释放. 指针碰撞风格:无需与其他分配请求互斥,分配动作简单高效。
5. Go编译器优化
5.1 函数内联
- 函数内联:将
被调用函数的函数体(callee)的副本替换到调用位置上,同时重写代码以反映参数的绑定 - 优点
- 消除函数调用的开销,例如传递参数、保存现场等
- 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸优化等
- 缺点
- 函数体变大,对于CPU的instruction cache(可能导致大量的iCache的miss)不友好
- 编译生成的Go镜像变大了
5.2 Beast mode
- Go函数内联受到的限制较多
- 语言特性,例如interface、defer等,限制了函数内联
- 内联策略非常保守
Beast mode:调整函数内联的策略,使更多函数被内联- 降低函数调用的开销
- 增加了其他优化的机会:
逃逸分析
- 开销
- Go镜像增大~10%
- 编译时间增加
5.3 逃逸分析
逃逸分析:分析代码中指针的动态作用域,指针在何处可以被访问- 大致思路:从对象分配处出发,沿着控制流,观察对象的数据流
- 若发现指针p在当前作用域s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的goroutine
- 传递给已逃逸的指针指向的对象
- 则认为指针p逃出了作用域s,否则没有
- 若发现指针p在当前作用域s:
- Beast mode:函数内联扩展了函数边界,更多对象不逃逸
- 优化:未逃逸的对象可以在
栈上分配-
对象在栈上分配和回收很快,移动sp
-
减少在heap上的分配,减小GC的负担
-