Go高质量编程与性能调优 | 青训营笔记

189 阅读11分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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-81855870602.5 ns/op0 B/op0 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 功能简介

ppof简介.jpg

1.2 搭载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 调用关系可视化

    采样过程和原理

    1. 采样对象: 函数调用和他们占用的时间
    2. 采样率: 100次/s,固定值
    3. 采样时间: 从手动启动到手动结束
  • heap 堆内存

    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
    • top视图类似于topN
    • Source视图
      • alloc_objects: 程序累计申请的对象数
      • alloc_space:程序累计申请的内存大小
      • inuse_objects:程序当前持有的对象数
      • inuse_space:程序当前占用的内存大小

    采样过程和原理

    1. 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
    2. 采样率: 每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
    3. 采样时间:从程序运行开始到采样时
    4. 采样指标: alloc_space; alloc_objects; inuse_object; inuse_space
    5. 计算方式: inuse = alloc - free
  • goroutine 协程

    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
    • 从上到下表示调用顺序
    • 每一块代表一个函数,越长表示占用cpu时间越长
    • 火焰图是动态的,支持点击块进行分析

    采样过程和原理

    1. Goroutine:记录所有用户发起且在运行中1的goroutine(即入口非routine开头的)runtime.main的调用栈信息
    2. ThreadCreate: 记录程序创建的所有系统线程的信息
    3. 流程:
      1. goroutine: stop the world - iterate allg slice - 输出创建g的堆栈 - start the world
      2. threadcreate: stop the world - iterate allm slice - 输出创建m的堆栈 - start the world
  • mutex 锁

    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"

    采样过程和原理

    1. 采样争抢锁的次数和耗时
    2. 采样率: 只记录固定比例的锁操作,1为每次加锁均记录
  • block 阻塞

    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
    • 两个block为什么只展示了一个
      • 终端执行查看 go tool pprof "http://localhost:6060/debug/pprof/block"
      • 看看第二个阻塞操作是什么,是否被过滤掉

    采样过程和原理

    1. 采样阻塞操作的次数和耗时
    2. 采样率:阻塞耗时超过阈值的才会被记录,1为每次阻塞均记录
1.4 展示-View

​ 待添加

2.案例
  • 业务服务优化
  • 基础库优化
  • Go语言优化
2.1基本概念
  1. 服务:能单独部署,承载一定功能的程序
  2. 依赖:Service A B, A的功能实现依赖于B的相应结果
  3. 调用链路: 能支持一个接口请求的相关服务集合及其相互之间依赖关系
  4. 基础库: 公共的工具包、中间件
2.2业务服务优化流程
  1. 建立服务性能评估手段
    1. 服务性能评估方式
      • 单独benchmark无法满足复杂逻辑分析
      • 不同负载情况下性能表现差异
    2. 请求流量构造
      • 不同请求参数覆盖逻辑不同
      • 线上真实流量情况
    3. 压测范围
      • 单机器压测
      • 集群压测
    4. 性能数据采集
      • 单机性能数据
      • 集群性能数据
  2. 分析性能数据,定位性能瓶颈
    1. 使用库不规范
    2. 高并发场景优化不足
  3. 重点优化项改造
    1. 正确性是基础
    2. 响应数据diff
      • 线上请求数据录制回放
      • 新旧逻辑接口数据diff
  4. 优化效果验证
    1. 重复压测验证
    2. 上线评估优化效果
      • 关注服务监控
      • 逐步放量
      • 收集性能数据
2.3整体服务链路优化
  • 规范上游服务调用接口,明确场景需求
  • 分析链路,通过业务流程优化提升服务性能
2.4 基础库优化

AB实验SDK优化 关于什么是AB实验可以参考该文章

  • 分析基础库核心逻辑和性能瓶颈
    • 设计完善改造方案
    • 数据按需获取
    • 数据序列化协议优化
  • 内部压测验证
  • 推广业务服务落地验证
2.5 Go语言优化

编译器&运行时优化

  1. 优化内存分配策略
  2. 优化代码编译流程
  3. 内部压测验证
  4. 推广业务服务落地验证
  • 优点

    • 接入简单
    • 通用性强

    比如用高版本优化后的sdk重新编译,压测指标就可能有改进