这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
高质量编程
性能调优
1.高质量编程
1.1 高质量定义
高质量的定义——正确可靠、简洁清晰
- 正确性:是否考虑各种边界条件,错误的调用是否能够处理
- 可靠性:异常情况或者错误的处理策略是否明确,稳定性保证
- 简洁:易读,逻辑是否简单,后续调整功能或新增功能是否能够快速理解结构和实现支持
- 清晰:易维护,重构或修改功能时高效
IN GO
- 简单性:消除多余的复杂性,以简单清晰的逻辑编写代码,不好理解的代码无法修复改进
- 可读性:编写可维护代码的第一步是确保代码对人可读
- 生产力:团队整体的工作效率非常重要,Go语言甚至通过工具强制统一所有代码格式,以降低后续联调/测试/验证/上线等各节点出现问题的概率
1.2 编码规范
- 代码格式
- gofmt/go fmt 可格式化代码
- goimports = fmt+依赖包管理
- 注释
- 注释应该解释代码作用/实现过程/实现原因(外部因素-提供额外上下文)/出错场景(限制条件)
- 公共符号始终要注释
- 不简短不明显的变量/常量/结构
- 所有函数
- ex.无需注释实现接口的方法
- 命名规范
- varaibles
- 简洁胜于冗长
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- function 函数名不携带包名的上下文信息, 尽量简短;大写开头
- package名只由小写字母组成; 使用单数,谨慎缩写
- varaibles
- 控制流程
- 线性原理,处理逻辑尽量走直线,避免嵌套,故障问题的大多出现在复杂的条件语句和循环语句中,尽量化简
- 尽量保持正常代码路径为最小缩进,能对称就对称(从正常流程调整到先处理错误
- 错误和异常处理
-
简单错误:指仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用
errors.New来创建匿名变量来直接表示简单错误 - 如果有格式化需求,请使用
fmt.Errorf
- 优先使用
-
错误的wrap和unwrap
-
错误的
Wrap打包提供了一个error嵌套另一个error的能力,从而生成error跟踪链 -
在
fmt.Errorf中使用%w关键字来将一个错误关联至错误链中fmt.Errorf("reading srcfiles list: %w", err)
-
-
错误判定:[error尽量提供简明的上下文信息链,方便定位问题]
-
判定一个错误是否为特定错误,用
errors.ls,不同于使用==,该方法可以判定错误链上的所有错误是否含有特定的错误data, err = lockedfile.Read(targ) if errors.Is(err, fs.ErrNotExist) { return []byte{}, nil } return data, err -
在错误链上获取特定种类的错误,使用
errors.As- 与Is的区别是,As会提取出调用链中指定类型的错误并赋值给定义好的变量,方便后续处理
if _, err := os.Open("non-existing"); err != nil { var pathError *fs.PathError // if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }
-
-
panic:用于不可逆转的错误,如果没有recover会导致程序崩溃
-
recover:与panic对应,如果需要更多的上下文信息可以在recover后在log中记录当前的调用栈,(应用场景:引入库的bug影响自身逻辑)
- 只能在当前goroutine的被defer(相关语句后进先出)的函数中使用
- 嵌套无法生效
-
1.3 性能优化
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时时间效率和空间效率可能对立,可以时间换空间 / 空间换时间
1.3.1 Benchmark
go test -bench=. -benchmen
执行结果说明
1.3.2 slice预分配内存
尽可能在使用make()初始化切片的时候就提供容量信息
- 切片本质是一个数组片段的描述,包括不安全的数组指针、片段的长度以及片段的容量
- 切片操作并不复制切片指向的元素
- 创建一个新的切片会复用原来切片的底层数组
- 另一个陷阱:大内存未释放
- 原切片由大量元素构成,在原切片的基础上新建小切片,原底层数组在内存有引用,得不到释放
- 可以用
copy代替re-slice
1.3.3 Map预分配内存
- 不断向map中添加元素会触发map的扩容
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
1.3.4 字符串处理 strings.Builder
在字符串拼接的过程中,使用strings.Builder往往比直接+要快,分析如下:
-
字符串在Go语言中是不可变类型,占用内存大小是固定的,使用
+每次都会重新分配内存 -
strings.Builder, bytes.Buffer底层都是[]byte数组,内存扩容策略,不需要每次拼接重新分配内存,也可预分配(用Grow函数)var builder strings.Builder//直接将底层的[]byte数组转换成字符串类型返回,效率更高 buf:=new(btyes.Buffer)//后续转换为字符串时重新申请了一块空间
1.3.5 空结构体——节省内存
- 空结构体
struct{}实例不占据任何的内存空间 - 可作为各种场景下的占位符使用(如set实现,用map代替
- 节省资源
- 空
1.3.6 atomic包
提供了底层的原子级内存操作,对于同步算法的实现很有用。但除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好
- atomic提供的原子操作能够确保任意时刻只有一个goroutine对变量进行操作
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作是通过硬件实现,效率更高,善用atomic能够避免程序中出现大量的锁操作
- sync.Mutex应该用于保护一段逻辑,而不是仅仅一个变量
- 对于非数值操作,可以使用atomic.Value,能承载一个interface{}
2. 性能调优实战
2.1 原则
- 依靠数据而非猜测
- 找到决速反应,而非细枝末节
- 不要过早/过度的优化
- 保证正确性,定位主要瓶颈
2.2 性能分析工具pprof实战
2.2.1 pprof功能简介
2.2.2 pprof排查实战
搭建pprof实践项目
- 克隆代码
https://github.com/wolfogre/go-pprof-practice - 运行后打开
http://localhost:6060/debug/pprof可以查看pprof在web上的采样数据,但是可读性很差 - 这时可通过pprof工具来查看
- 其采样结果是将一段时间内的信息汇总输出到文件中,首先要拿到这个profile文件
- 可直接使用暴露的接口链接下载文件后使用
- 也可直接用pprof工具链接这个接口下载需要的数据。
- 其采样结果是将一段时间内的信息汇总输出到文件中,首先要拿到这个profile文件
go tool pprof + <采样链接> 启动采样
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
// 链接结尾的profile代表采样对象是CPU使用,如果直接在浏览器里打开这个链接,会启动一个60s的采样,并在结束后下载文件
//seconds=10, 采样10秒,随后采样数据就记录和下载完成,并展示出pprof终端。
//如果出现端口占用,可以通过命令 netstat -ano 查看端口占用情况
//-http://8080 参数 可直接打开pprof自带的web UI界面
pprof终端下:
topN查看CPU占用最高的函数,参数说明:- flat 当前函数本身的执行耗时
- cum 当前函数+其调用函数的总耗时
list根据指定的正则表达式查找代码行web调用关系可视化,生成一张调用关系图,会默认使用浏览器打开,非常直观(graph视图)
定位到问题所在后可以继续修改参数为heap等处理其他方面的问题
- heap
- inuse
- alloc
- goroutine泄露易导致内存泄露
- 可使用火焰图
- 支持搜索(source视图下)
进阶参考 一文搞懂pprof - 知乎
2.2.3 pprof采样过程与原理
-
CPU [信号机制]
-
Heap [不记录栈/cgo等内存]
- 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
- 采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
- 采样时间:从程序运行开始到采样时,持续的过程
- 采样指标:
alloc_space, alloc_objects, inuse_space, inuse_objects - 计算方式:
inuse = alloc - free
-
Goroutine-协程 & ThreadCreate-线程创建
-
Block-阻塞 & Mutex-锁
3. 性能调优案例
基本概念:
- 服务:能单独部署,承载一定功能的程序
- 依赖:Service A的功能实现依赖,Service B的响应结果,称为Service A依赖Service B
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
3.1 业务服务优化
-
流程:
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
- 建立服务性能评估手段
-
建立压测评估链路
- 服务性能评估
- 构造请求流量
- 压测范围
- 性能数据采集
-
分析性能数据,定位性能瓶颈(pprof 火焰图)
- 使用库不规范
- 高并发场景优化不足
-
重点优化项分析
- 增加代码检查规则避免增量劣化出现
- 优化正确性验证
- 响应数据diff
- 线上请求数据录制回放
- 新旧逻辑接口数据diff
-
优化效果验证
- 重复压测验证
- 上线评估
- 关注服务监控
- 逐步放量,避免出现问题
- 收集性能数据
-
进一步优化,服务整体链路分析
- 规范上游服务调用接口,明确场景需求
- 分析业务流程,通过业务流程优化提升服务性能
3.2 基础库优化
适应范围更广,覆盖更多服务
AB 实验 SDK 的优化
- 分析基础库核心逻辑和性能瓶颈
- 完善改造方案
- 数据按需获取
- 数据序列化协议优化
- 内部压测验证
- 推广业务服务落地验证
3.3 Go 语言优化
适应范围最广,Go 服务都有收益
接入简单,只需要调整编译配置,通用性强
优化方式
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证