这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
该文章是听课随手笔记,主要内容包括代码规范、高性能调优的原则,工具pprof使用等
学习课程之前笔者一直用java和python作为常用语言
关于命名规范,只是知道命名方法,驼峰法之类以及java中对变量函数方法类文件等的命名规则;但是对于名称缩写词使用,命名构成十分纠结(比如命名是否要加入上下文信息),经过老师的讲解对这部分知识有了新的体会,非常感谢老师!
关于性能调优实战,之前只知道优化cpu利用率,内存,堆栈占用情况,经过课程系统讲解:还有go语言层面优化、库优化、业务逻辑优化等多层次优化,系统化学习知识真的很重要!
== 高质量编程标准与规范 ==
1.1 编码原则
1. 各种边界条件考虑充分
2. 异常处理,系统稳定性保证
3. 易读易维护(高扩展,封装调用)
Go 语言开发者 Dave Cheney 提供的标准:
1. 简单性,易理解
2. 可读性,可维护
3. 生产力
1.2 编码注释
-
解释代码作用 : 适合注释公共符号
-
解释代码如何做的 : 适合注释实现过程
-
解释代码实现的原因 : 适合解释代码的外部因素,提供额外上下文
-
解释代码什么时候会出错 : 适合解释代码限制条件
1. 公共符号始终要注释:包中声明的每个公共符号(变量、常量、函数等)都需要添加注释《全局变量声明要加注释》
2. 任何既不明显也不简短的公共功能必须予以注释《复杂函数要加注释》
3. 无论长度或复杂程度如何,对自定义库中任何函数都必须进行注释《自定义库中函数加注释》
!不需要注释实现接口的方法!
代码就是最好的注释
1.3 编码格式
1. 推荐使用gofmt自动格式化代码: gofmt,go官方提供的代码自动格式化的工具
2. goimports:也是go语言官方提供的工具,实际上等于gofmt加上依赖包管理
1.4 编码命名
1.4.1 变量
1. 简单 > 冗长
2. 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
3. 变量使用与声明越远,命名需包含的上下文信息应越多《比如全局变量》
1.4.2 函数名
1. 函数名不应携带包名的上下文信息
2. 函数名尽量简短
3. 函数返回值不是包名时,函数名应加入返回值类型信息
例如http包:Serve方法 √ ServeHTTP方法 ×
1.4.3 包
1. 只由小写字母组成。不要包含大写字母和下划线等字符
2. 简短并包含一定的上下文信息。例如schema,task
3. 不要与标准库同名
!注意!
1. 不适用常用变量名作为包名,防止后续使用冲突
2. 使用单数而不是复数
3. 谨慎的使用缩写(要用,但是需要谨慎)
结:降低阅读成本,重点考虑上下文信息,设计简洁清晰名称
1.5 编码流控
- 避免嵌套,保持正常流程清晰
- 尽量保证正常代码路径为最小缩进
- 优先处理错误/特殊情况,尽早返回或继续循环来减少嵌套
1. 处理逻辑尽量向下走直线,避免复杂嵌套分支
2. 正常流程代码沿着屏幕向下移动
1.6 编码错误异常处理
1.6.1 简单错误
- 一次性错误,其他地方不需要捕获该错误
- 优先使用 errors.New 来创建匿名变量来直接表示简单错误
- 如果有格式化的需要,使用 fmt.Errorf
1.6.2 错误的Wrap和Unwrap
1. 错误的Wrap实际上时提供了一个error嵌套另一个error的能力,从而生成了一个error的跟踪链
2. 在fmt.Errorf中使用:%w 关键字来将一个错误关联至错误链中
1.6.3 错误判定
1. 在错误链上获取特定种类的错误,使用errors.As
1.6.4 panic
1. 不建议在业务代码中使用panic
2. 调用函数不包含recover会造成程序崩溃
3. 若问题可以被屏蔽或解决,建议使用error代替panic
4. 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
1.6.5 recover
1. recover只能在defer函数中使用
2. 嵌套无法生效
3. 只在当前goroutine生效
4. defer的语句时后进先出
5. 如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈
error尽可能包含上下文信息,方便定位问题;panic属于真正异常;recover生效范围,在当前goroutine的被defer的函数中生效
== 性能优化 ==
1. 性能优化建议-Benchmark
-
go自带的测评工具benchmark
-
命令:
go test -bench=. -benchmem
| BenchmarkFib10-8 | 1855870 | 602.5 ns/op | 0 B/op | 0 allocs/op |
|---|---|---|---|---|
| BenchmarkFib10是测试函数名称,-8表示GOMAXPROCS的值=8《1.5版本以后,默认值为cpu核数》 | 表示一共执行1855870次,即b.N的值 | 每次执行花费602.5ns | 每次执行申请多大内存 | 每次执行申请几次内存 |
2. 性能优化建议-Slice
- slice预分配内存,尽可能在使用make()初始化切片时提供容量信息
- 大于默认分配大小 造成 分配内存次数增加
- 大内存未释放
- 在已有切片上创建切片,不会创建新的底层数组
- 原切片较大,在之上新建小切片
- 原底层数组在内存中有引用,得不到释放
- 可使用copy代替re-slice
- 在已有切片上创建切片,不会创建新的底层数组
3. 性能优化建议-Map
- map预分配内存 make
- 减少map自动扩容次数
- 减少内存拷贝和Rehash的消耗
4. 性能优化建议-String处理
-
使用strings.Builder提高效率
- 默认字符串时常量,拼接设计内存拷贝
- builder是可变string
如果bytes.Buffer强转string时,要先申请一块空间,然后拷贝进去,很低效 -
strings.Builder也是支持预分配的,builder.Grow(size)来预分配
5. 性能优化建议-空结构体
- 使用空结构体节省内存
- 空结构体struct{}实例不占据任何的内存空间
- 可作为任何场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符(struct{}{})
- 实现set,可以考虑用map来代替
- 只用map键,不用值
- 即使将map的值设置为bool类型,也会多占据1个字节的空间
6. 性能优化建议atomic包
- 使用atomic包
- 保护一个变量时,性能高于lock机制
- 锁是通过操作系统实现的,内含系统调用
- atomic通过硬件实现,效率高于锁
- 锁机制,也就是sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value, 能承载一个interface{}
== 性能调优实战 ==
- 性能调优原则
- 要依靠数据,不是猜测
- 要定位最大瓶颈,而不是细枝末节
- 不要过早优化
- 不要过度优化
1.性能分析工具pprof
1.1 功能简介
1.2 搭载pprof实践项目
- GitHub(来自Wolfogre)
- githhub.com/wolfogre/go…
- 项目提前买入了一些炸弹代码,降低性能
- 游览器查看指标:localhost:6060/debug/pprof/
1.3 采样-Sample
-
cpu
-
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" -
topN 查看占用资源最多的函数
- flat 当前函数本身的执行耗时
- flat% flat占CPU总时间的比例
- sum% 上面每一行的flat%总和
- cum 指当前函数本身加上其调用函数的总耗时
- cum% cum占CPU总时间的比例
-
根据指定的正则表达式查找代码行 eg: list Eat
-
web 调用关系可视化
采样过程和原理
- 采样对象: 函数调用和他们占用的时间
- 采样率: 100次/s,固定值
- 采样时间: 从手动启动到手动结束
-
-
heap 堆内存
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"- top视图类似于topN
- Source视图
- alloc_objects: 程序累计申请的对象数
- alloc_space:程序累计申请的内存大小
- inuse_objects:程序当前持有的对象数
- inuse_space:程序当前占用的内存大小
采样过程和原理
- 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
- 采样率: 每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
- 采样时间:从程序运行开始到采样时
- 采样指标: alloc_space; alloc_objects; inuse_object; inuse_space
- 计算方式: inuse = alloc - free
-
goroutine 协程
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"- 从上到下表示调用顺序
- 每一块代表一个函数,越长表示占用cpu时间越长
- 火焰图是动态的,支持点击块进行分析
采样过程和原理
- Goroutine:记录所有用户发起且在运行中1的goroutine(即入口非routine开头的)runtime.main的调用栈信息
- ThreadCreate: 记录程序创建的所有系统线程的信息
- 流程:
- goroutine: stop the world - iterate allg slice - 输出创建g的堆栈 - start the world
- threadcreate: stop the world - iterate allm slice - 输出创建m的堆栈 - start the world
-
mutex 锁
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
采样过程和原理
- 采样争抢锁的次数和耗时
- 采样率: 只记录固定比例的锁操作,1为每次加锁均记录
-
block 阻塞
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"- 两个block为什么只展示了一个
- 终端执行查看
go tool pprof "http://localhost:6060/debug/pprof/block" - 看看第二个阻塞操作是什么,是否被过滤掉
- 终端执行查看
采样过程和原理
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值的才会被记录,1为每次阻塞均记录
1.4 展示-View
待添加
2.案例
- 业务服务优化
- 基础库优化
- Go语言优化
2.1基本概念
- 服务:能单独部署,承载一定功能的程序
- 依赖:Service A B, A的功能实现依赖于B的相应结果
- 调用链路: 能支持一个接口请求的相关服务集合及其相互之间依赖关系
- 基础库: 公共的工具包、中间件
2.2业务服务优化流程
- 建立服务性能评估手段
- 服务性能评估方式
- 单独benchmark无法满足复杂逻辑分析
- 不同负载情况下性能表现差异
- 请求流量构造
- 不同请求参数覆盖逻辑不同
- 线上真实流量情况
- 压测范围
- 单机器压测
- 集群压测
- 性能数据采集
- 单机性能数据
- 集群性能数据
- 服务性能评估方式
- 分析性能数据,定位性能瓶颈
- 使用库不规范
- 高并发场景优化不足
- 重点优化项改造
- 正确性是基础
- 响应数据diff
- 线上请求数据录制回放
- 新旧逻辑接口数据diff
- 优化效果验证
- 重复压测验证
- 上线评估优化效果
- 关注服务监控
- 逐步放量
- 收集性能数据
2.3整体服务链路优化
- 规范上游服务调用接口,明确场景需求
- 分析链路,通过业务流程优化提升服务性能
2.4 基础库优化
AB实验SDK优化 关于什么是AB实验可以参考该文章
- 分析基础库核心逻辑和性能瓶颈
- 设计完善改造方案
- 数据按需获取
- 数据序列化协议优化
- 内部压测验证
- 推广业务服务落地验证
2.5 Go语言优化
编译器&运行时优化
- 优化内存分配策略
- 优化代码编译流程
- 内部压测验证
- 推广业务服务落地验证
-
优点
- 接入简单
- 通用性强
比如用高版本优化后的sdk重新编译,压测指标就可能有改进