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

134 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。

高质量编程

高质量代码的标准

正确可靠、简洁清晰

  • 考虑完备边界条件
  • 异常情况处理
  • 易读易维护

编码规范

目标是降低阅读理解代码的成本。

重点是考虑上下文信息,设计简洁清晰的名称。

代码格式

推荐使用gofmt自动格式化代码。

goimports还能管理依赖包。

注释

  • 解释代码作用
  • 解释代码如何做的
  • 解释代码实现的原因
  • 解释代码什么情况会出错

包中公共的部分必须要注释。

例外:不需要注释实现接口的方法。

代码是最好的注释。

注释应该提供代码未表达出的上下文信息。

命名规范

命名最好带有更多有效的信息。

  • 简洁胜于冗长
  • 缩略词全大写 、变量开头不需要公开就全小写
  • 变量距离被使用的地方越远,就需要携带更多的上下文信息。比如全局变量。

函数命名规范

  • 函数不携带包名的上下文信息,因为包名与函数名总是成对出现。
  • 函数名尽量简短。
  • 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义。
  • 当名为foo的包某个函数返回类型T(不是Foo)时,可以在函数名内加入类型信息。

包命名规范

  • 只由小写字母组成。
  • 简短并包含一定上下文信息。
  • 不要与标准库同名。
  • 不要使用常用变量名。
  • 使用单数。
  • 谨慎的使用缩写。

控制流程

  • 避免嵌套。
  • 优先处理错误情况、特殊情况,尽早返回或继续循环来减少嵌套。
  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支。
  • 正常流程代码沿着屏幕向下移动。
  • 提升代码可维护性和可读性。
  • 故障问题大多出现在复杂的条件语句和循环语句中。

错误和异常处理

  • error提供简明的上下文信息链,方便定位问题。
  • panic用于真正异常的情况。
简单错误

只出现一次,在其他地方不需要捕获该错误。

errors.New创建匿名变量来表示简单错误。

格式化需求用fmt.Errorf

错误的Wrap和Unwrap

错误的Wrap实际上提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链。

fmt.Errorf中使用%w关键字来将一个错误关联至错误链中。

错误判断

errors.Is判断错误是否是指定类型的错误,可以判定错误链上所有错误是否含有特定的错误。

errors.As在错误链上获取特定种类的错误。

panic
  • 不建议在业务代码中使用panic
  • 调用函数不包含recover会导致程序崩溃
  • 若问题可以被屏蔽或解决,建议使用error代替panic
  • 当程序启动阶段发生不可逆转的错误时,可以在initmain函数中使用panic
recover
  • recover只能在被defer的函数中使用。
  • 函数嵌套中recover无法生效。
  • 只在当前goroutine生效。
  • defer的语句是后进先出。
  • 如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈debug.Stack()

性能优化建议

  • 性能优化的前提是满足正确可靠、简介清晰等质量因素。
  • 性能优化是综合评估,有时候时间效率与空间效率可能对立。

避免常见的性能陷阱可以保证大部分程序的性能。

普通应用代码,不要一味地追求程序的性能。

越高级的性能优化手段越容易出现问题。

在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能。

Benchmark

  • 性能表现需要实际数据衡量。

go test -bench=. -benchmem命令

ns/op每次执行花费多少纳秒。

B/op每次执行申请多大的内存。

allocs/op每次执行申请几次内存。

slice

预分配内存
  • slice预分配内存。
  • 切片操作并不复制切片指向的元素。
  • 在已有切片上创建一个新的切片会复用原来切片的底层数组。
大内存未释放

在大切片上新建小切片会导致原底层数组的内存中有引用,得不到释放。

可以使用copy代替切片操作。

map

预分配内存
  • map预分配内存。
  • 提前分配空间减少内存拷贝与rehash的消耗。

字符串处理

使用strings.Builder

Grow()同样也可以预分配内存。

使用strings.Builder拼接字符串,底层是byte数组。

strings.Builder优于bytes.Buffer优于+

bytes.Buffer转化为字符串时重新申请了一块空间。

strings.Builder直接将底层的byte数组转换成了字符串类型返回。

+拼接字符串每次都会重新分配内存。

空结构体

空结构体实例不占据任何内存空间,可以作为占位符,节省资源。

可以通过map空结构体实现set,只需要用到map的键。

atomic包

  • 锁的实现是通过操作系统来实现,属于系统调用。
  • atomic操作是通过硬件实现,效率比锁高。
  • sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量。
  • 对于非数值操作,可以使用atomic.Value,能承载一个interface{}

性能调优实战

性能调优原则

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

性能分析工具 pprof

go tool pprof [pprof网页URL CPU命令]

top查看占用资源最多的函数。

CPU

  • flat当前函数本身的执行耗时
  • flat%flat占CPU总时间的比例
  • sum%上面每一行的flat% 总和
  • cum指当前函数加上其调用函数的总耗时
  • cum%cum占CPU总时间的比例

flat==cum函数中没有调用其他函数。

flat==0函数中只有其他函数的调用。

list [正则表达式]查找代码行。

web生成可视化调用关系。

Heap堆内存

go tool pprof [网页URL heap命令]

  • alloc_objects程序累计申请的对像数。
  • alloc_space程序累计申请的内存大小。
  • inuse_objects程序当前持有的对像数。
  • inuse_space程序当前占用的内存大小。

goroutine协程

go tool pprof [网页URL goroutine命令]

火焰图

mutex锁

go tool pprof [网页URL mutex命令]

block阻塞

go tool pprof [网页URL bolck命令]

pprof采样过程和原理

CPU
  • 采样对象:函数调用和它们占用的时间
  • 采样率:100次/秒,固定值
  • 采样时间:从手动启动到手动结束
Heap堆内存
  • 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
  • 采样率:每分配512KB记录一次,可在运行开头修改,1位每次分配均记录
  • 采样时间:从程序运行开始到采样时
  • 采样指标:alloc_space,alloc_objects,inuse_space,inuse_objects
  • 计算方式:inuse = alloc - free
goroutine协程&ThreadCreate线程创建

goroutine

  • 记录所有用户发起且在运行的goroutine runtime.main的调用栈信息。

ThreadCreate

  • 记录程序创建的所有系统线程的信息
Block阻塞&Mutex锁

Block

  • 采样阻塞操作的次数和耗时
  • 采样率:阻塞耗时超过阈值的才会被记录 1为每次阻塞均记录

  • 采样争抢锁的次数和耗时
  • 采样率:只记录固定比例的锁操作,1位每次加锁均记录。

性能调优案例

业务服务优化

  • 服务:能单独部署,承载一定功能的程序
  • 依赖:A的功能依赖B的响应结果成为A依赖B
  • 调用链路:能支持一个接口请求的相关服务集合及相互之间的依赖关系
  • 基础库:公共的工具包、中间件
流程
  • 建立服务性能评估手段

    • 服务器性能评估方式
    • 请求流量构造
    • 压测范围
    • 性能数据采集
  • 分析性能数据,定位性能瓶颈

    • 使用库不规范
    • 高并发场景优化不足
  • 重点优化项改造

    • 正确性是基础,响应数据diff
    • 线上请求数据录制回放
    • 新旧逻辑接口数据diff
  • 优化效果验证

    • 重复压测验证

    • 上线评估优化效果

      • 关注服务监控
      • 逐步放量
      • 收集性能数据

进一步优化,服务整体链路分析

规范上游服务调用接口,明确场景需求

分析链路,通过业务流程优化提升服务性能

基础库优化

  • 分析基础库核心逻辑和性能瓶颈

    • 设计完整改造方案
    • 数据按需获取
    • 数据序列化协议优化
  • 内部压测验证

  • 推广业务服务落地验证

Go语言优化

  • 优化内存分配策略
  • 优化代码编译流程,生成更高效的程序
  • 内部压测验证
  • 推广业务服务落地验证

优点

  • 接入简单,只需要调整编译配置
  • 通用性强