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

74 阅读7分钟

这是我参与「第五届青训营 」笔记创作活动的第3天。

课程讲授内容

  1. 高质量编程讲解,包括高质量编程简述、编码规范和性能优化建议;
  2. 性能调优的 pprof 项目实战,包括性能优化简述、分析工具,以及通过性能调优案例掌握知识点。

1 高质量编程

1.1 高质量编程简介

  1. 高质量代码:正确可靠,简介清晰。
    • 各种边界条件是否考虑完备
    • 异常情况处理,稳定性保证
    • 易读易维护
  2. 编码原则
    • 简单性:代码简单清晰,避免导致不理解代码无法修复改进;
    • 可读性
    • 生产力:团队工作效率

1.2 常见编码规范

1.2.1 编写高质量代码

  • 代码格式
    • gofmt
    • goimports
  • 注释
    • 代码作用:注释公共符号
    • 实现方法
    • 实现原因
    • 预期错误
    • Good code has lots of comments, bad code requires lots of comments.
  • 命名规范
    • variable
      • 简洁胜于冗长
      • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写:例如使用 ServeHTTP 而不是ServeHttp、使用 XMLHTTPRequest 或者 xmlHTTPRequest
      • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
      • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
    • function
      • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的·函数名尽量简短
      • 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
      • 当名为foo的包某个函数返回类型T时(T并不是 Foo),可以在函数名中加入类型信息
    • package
      • 只由小写字母组成。不包含大写字母和下划线等字符
      • 简短并包含一定的上下文信息。例如schema、task 等
      • 不要与标准库同名。例如不要使用sync或者strings
  • 控制流程
    • 避免嵌套,保证正常清晰
    • 保持正常代码路径为最小缩进
      // good
      if err := xxx(); err != nil {
          return err
      }
      ...
      return nil
      
      // bad
      err := xxx()
      if err != nil {
          return err
      }
      return nil
      
  • 错误和异常处理
    • 简单错误(只出现一次,在其他地方不用捕获)
      • 优先使用 errors.New 来用常见匿名变量打印错误
      • fmt.Errorf 格式化
    • 错误的 Wrap 和 Unwrap
      • 错误的 Wrap 实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
      • fmt.Errorf 中使用: %w 关键字来将一个错误关联至错误链中
    • 错误判定 errors.Is,解决 == 不能解决的错误
    • errors.As 错误链上获取特定种类的错误,如取错误路径
    • panic
      • 用于真正的异常问题
      • 不建议在业务代码中使用 panic
      • 调用函数不包含 recover 会造成程序崩溃
      • 若问题可以被屏蔽或解决,建议使用 error 代替 panic
      • 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
    • recover
      • recover只能在被defer的函数中使用
      • 嵌套无法生效
      • 只在当前 goroutine 生效
      • defer的语句是后进先出
      • 若需要打印上下文信息,可以在 recover 中的日志中用 debug.Stack() 记录当前的调用堆栈

1.2.2 注释

  1. 公共符号要注释
    • 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
    • 任何既不明显也不简短的公共功能必须予以注释
    • 无论长度或复杂程度如何,对库中的任何函数都必须进行注释

1.3 性能优化建议

  1. 如何使用 Benchmark
    • 性能表现需要实际数据衡量
    • Go语言提供了支持基准性能测试的 benchmark 工具
    • go test bench=. -benchmem
  2. slice 预分配内存
    • 尽可能在使用 make() 初始化切片时提供容量信息
    • 另一个陷阱:大内存未释放
      • 在已有切片基础上创建切片,不会创建新的底层数组
      • 场景:原切片较大,代码在原切片基础上新建小切片
      • 原底层数组在内存中有引用,得不到释放可使用copy替代 re-slice
      // bad
      func GetLastByslice( origin []int) [ ]int {
          return origin[len(origin)-2:]
      }
      
      // good
      func GetLastByCopy ( origin []int) []int {
          result := make([]int2)
          copy(result,origin[len(origin)-2:])
          return result
      }
      
      // 测试:go test -run=. v
      
  3. 字符串处理
    • 使用strings.Builder
      • 使用 + 拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer更快
      • 分析:①字符串在Go语言中是不可变类型,占用内存大小是固定的;②使用 + 每次都会重新分配内存;③strings.Builder,bytes.Buffer底层都是[]byte 数组;④内存扩容策略,不需要每次拼接重新分配内存
  4. atomic 包
    • 比 mutex 上锁解锁性能高
    • 锁的实现是通过操作系统来实现,属于系统调用
    • atomic操作是通过硬件实现,效率比锁高
    • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量。对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}

2 性能调优实战

2.1 性能调优简介

  • 原则
    • 要依靠数据不是猜测
    • 要定位最大瓶颈而不是细枝末节
    • 不要过早优化
    • 不要过度优化

2.2 性能分析工具

2.2.1 功能简介

3.png

2.2.2 排查实战

  • CPU
    • 命令 topN
      • flat:当前函数本身的执行耗时
      • flat%:flat占CPU总时间的比例
      • sum%:上面每一行的flat%总和
      • cum:指当前函数本身加上其调用函数的总耗时
      • cum%:cum占CPU总时间的比例
    • 命令 list
      • 根据指定的正则表达式查找代码行
    • 命令 web
      • 调用关系可视化

2.2.3 pprof 采样过程和原理

  1. CPU
    • 采样对象:函数调用和它们占用的时间采样率:100次/秒,固定值
    • 采样时间:从手动启动到手动结束
  2. 堆内存
    • 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
    • 采样时间:从程序运行开始到采样时
    • 采样指标: alloc_space, alloc_objects, inuse_space, inuse_objects
    • 计算方式: inuse = alloc - freo
  3. Goroutine-协程& ThreadCreate-线程创建
    • Goroutine
      • 记录所有用户发起且在运行中的 goroutine(即入口非runtime开头的)。runtime.main的调用栈信息
    • ThreadCreate
      • 记录程序创建的所有系统线程的信息
  4. Block-阻塞& Mutex-锁
    • 阻塞操作
      • 采样阻塞操作的次数和耗时
      • 采样率:阻塞耗时超过阈值的才会被记录,1为每次阻塞均记录
    • 锁竞争
      • 采样争抢锁的次数和耗时
      • 采样率:只记录固定比例的锁操作,1为每次加锁均记录

2.3 性能调优案例

2.3.1 业务服务优化

  1. 基本概念
    • 服务:能单独部署,承载一定功能的程序·依赖:Service A的功能实现依赖 Service B的响应结果,称为Service A依赖Service B
    • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
    • 基础库:公共的工具包、中间件
  2. 流程
    • 建立服务性能评估手段
    • 分析性能数据,定位性能瓶颈
    • 重点优化项改造
    • 优化效果验证
  3. 建立服务性能评估手段
    • 服务性能评估方式
    • 请求流量构造
    • 压测范围
    • 性能数据采集
  4. 分析性能数据,定位性能瓶颈
    • 使用库不规范
    • 高并发场景优化不足
  5. 重点优化项改造
    • 正确性是基础
    • 响应数据 diff
  6. 优化效果验证
    • 重复压测验证
    • 上线评估优化效果

2.3.2 Go 语言优化

  1. 编译器&运行时优化
    • 优化内存分配策略
    • 优化代码编译流程,生成更高效的程序·内部压测验证
    • 推广业务服务落地验证
    • 优点:接入简单,只需要调整编译配置·通用性强

2.4 总结

  • 性能调优原则
    • 要依靠数据不是猜测
  • 性能分析工具 pprof
    • 熟练使用pprof工具排查性能问题并了解其基本原理
  • 性能调优
    • 保证正确性
    • 定位主要瓶颈

课程总结

收获:了解了什么是高性能编程,以及编码规范等;通过使用调优工具 pprof 进行性能检测和调优,通过案例较为详细地介绍了调优过程和方法。

存在问题:调优工具 pprof 的使用,调优原理和策略等需要课下多实践和巩固。