高质量编程+调优实战+作业|青训营笔记

175 阅读12分钟

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

0.课前准备

代码地址 github.com/wolfogre/go…

1.高质量编程

1.1 什么是高质量

  • 各种边界条件考虑完备,具有正确性
  • 对异常情况进行处理,保证稳定性
  • 易读易维护

1.2 编程原则

  • 简单性:以简单清晰的逻辑编写代码
  • 可读性:善用注释
  • 生产力:代码风格统一、功能模块划分妥当

1.3 编码规范

1.3.1 代码格式

  • gofmt:官方工具,自动格式化go语言代码为官方统一风格,常见IDE都支持方便的配置

  • goimports:官方工具,相当于gofmt加上依赖包管理(自动增删依赖的包引用,将依赖包按字母排序并分类)

1.3.2 注释: 好的代码有很多注释,坏的代码需要很多注释

注释应该做的:

  1. 体现代码作用:一般公共变量、公开对外的api需要注释。若函数名已经可以体现函数作用,可不加注释。
  2. 说明代码实现流程:相对复杂的程序,不明显的功能、代码行需要加注释说明代码在干什么。代码更改时需要维护注释。
  3. 解释代码为什么有效:解释外部因素,证明算法有效性,说明某一行的意义。
  4. 标注代码什么情况会出错:提示代码的限制条件以及输入需要进行什么处理。

1.3.3 命名规范

好的命名就像一个好笑话,如果你必须解释它,那就不好笑了。 1.3.3.1 变量命名

  • 尽可能简洁,但不应该减少变量名中的重要信息
  • 大小写得当
  • 变量距离被使用的地方越远,越需要变量名携带更多信息

1.3.3.2 函数命名

  • 函数名不用携带包名的上下文信息
  • 尽可能简短
  • 当返回类型容易被误会时,应加入返回类型信息

1.3.3.3 包名命名

  • 只由小写字母组成,不包含大写字母和下划线等字符
  • 简短且包含一定上下文信息
  • 不要与标准库同名
  • 尽量不用常用变量名作包名
  • 尽量使用英文单数而不是复数
  • 谨慎的使用缩写,缩写应该在不丢失信息时使用

1.3.4 控制流程

减少循环分支的嵌套,优先处理错误/特殊情况,尽早返回,尽量减小正常代码路径的缩进数。

1.3.4 错误和异常处理

简单错误:仅出现一次且不需要被捕获的错误,使用 error.New("simple error")创建匿名变量直接表示。如果有格式化的需求,使用fmt.Errorf

复杂错误:使用fmt.Errorf%w将错误嵌套进错误链。

if err != nil {
    return fmt.Errorf("error is %w",err)
}

错误判定:使用errors.Is(err, 特定错误)判断err是否为特定错误。不同于==,errors.Is可以判定错误链上的所有错误是否含有特定错误。使用errors.As(err, &pathError)可以获取特定种类的错误,pathError.Path为错误的路径。

panicerror更严重,出现时可能意味着程序将不能正常工作。

recover可用于记录panic的上下文信息,其必须在defer函数中使用。

1.3 性能优化

1.3.1 Benchmark

使用go test -bench=. -benchmem查看结果。每行结果都包括

测试函数名-cpu内核使用个数    函数被执行总次数    每次执行用时    每次申请内存大小    每次申请内存次数
  • Slice优化

  • 预先分配内存会减少分配次数和总大小(原理同C++的vector扩容)。

  • 如果一开始有一个大切片,将其部分为小切片赋值,此时不会创建新的切片,而是小切片引用大切片的部分。此时大切片在内存中有引用,得不到释放,导致内存占用过大。因此应使用copy(small, big[start:end])函数进行赋值。

  • map优化

  • 预先分配内存(原理同C++的unorder_map扩容)。

  • string优化

  • go中字符串是不可变类型,占用内存大小是固定的,每次+都会重新分配内存,string.Builderbytes.Buffer底层都是[]byte数组,每次扩容内存而不是字符串,因此效率更高。返回时,bytes.Buffer重新申请了一块空间来读取buffer并转化为字符串,而string.Builder直接将底层的[]byte转化为字符串并返回。因此string.Builder性能最优。

  • string.Builderbytes.Buffer都支持内存的预分配。

  • 空结构体

  • 空结构体不占据任何空间,可作为占位符使用(实现set)

  • atomic包

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

2.性能调优实战

2.1 性能调优原则

  • 要依靠数据而不是猜测
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化(此时的优化以后可能用不上)
  • 不要过度优化(需要保证优化方法能兼容后续版本) 2.2 性能调优工具

pprof是用于可视化和分析性能、分析数据的工具,能观察在什么地方耗费了多少cpu和内存。

2.2.1 pprof-功能简介

2022-05-08 22-44-34 的屏幕截图.png

2.2.2 pprof-排查实战

1.一定要用go mod init github.com/wolfogre/go-pprof-practice指令构建go.mod!!!无论是否执行go mod tidy,你的go.mod文件中应该只有2行文本(如下图)。

2022-05-09 14-27-35 的屏幕截图.png

2.运行实验代码go build以及./go-pprof-practice-master,或使用go run main.go,两种方法在资源管理器中的进程名称会不同,但都会排在最上面(因为此时这个程序消耗资源很多)。

3.浏览器打开http://localhost:6060/debug/pprof/可以查看可用的程序运行采样数据。

4.终端输入gnome-system-monitor可查看任务管理器。可以看到此程序cpu和内存都是最高的。

5.终端输入go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"等待出现输入行出现(pprof)标识。

6.输入命令:top查看占用资源最多的函数。结果含义如下:

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

因此flat==cum代表当前函数没有调用其它函数,flat==0代表函数中只有其它函数的调用。可以看到tiger的Eat函数占用了大量资源。

2022-05-08 23-27-21 的屏幕截图.png

7.输入命令:list Eat查看细节。list后的参数是正则表达式形式。

2022-05-08 23-32-36 的屏幕截图.png

可以看到函数中的for循环占用了大量的flat和cum时间。

8.使用sudo apt install graphviz安装可视化功能后就能在(pprof)处输入web进行可视化。

9.注释掉23-26行的循环代码重新运行。则cpu占用率下降,但内存占用仍然很高。

10.使用go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"查看内存情况。该命令能直接打开可视化图,可以看到mouse的Steal占用了大量内存。

11.可视化图菜单的View中包含很多有用的图和表。其中VIEW的Source提供了类似第6步list的可视化。如果程序是使用go run main.go运行的,则可能加载不出来,需要使用go build运行可执行文件才能看到此视图。

12.找到内存占用的原因

2022-05-09 14-53-07 的屏幕截图.png

将48-51行代码注释掉并重新运行。

13.重新运行程序并打开可视化。菜单栏的SAMPLE中有下拉菜单,四个选项分别为alloc_objects(程序累计申请对象数)、alloc_space(程序累积申请的内存大小)、inuse_objects(程序当前持有的对象数)、inuse_space(程序当前占用的内存大小)。

14.点击alloc_space可以看到dog的run函数累积申请内存最大。

2022-05-09 15-00-22 的屏幕截图.png

因为是累积量,所以这个数字会越来越大,但我们的视图是截取的某一时刻。可以通过重新输入打开可视化界面命令的方法更新数据,以更好的进行观察。之后我们注释掉第43行重新运行代码。

15.goroutine泄漏也会导致内存泄漏。在第3步打开的网页中可以看到此程序有100+个goroutine,因此接下来查看goroutine相关的信息。输入指令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"查看信息。

16.点击VIEW的Flame Graph(火焰图,将调用关系平铺)可以更直观的观察数据。火焰图从上到下表示调用顺序,每一个小块代表一个函数,小块的长度代表占用cpu的时间。火焰图支持简单交互,鼠标移动之小块上或点击都能得到额外信息。此时看到wolf的Drink函数占用cpu最长(因为其协程数过多)。

VIEW的Top视图按协程数降序排列可以看到下图结果,其中每个Drink都会调用Sleep函数等待30秒。

2022-05-09 15-28-44 的屏幕截图.png

在VIEW的Source中搜索Drink查看细节如下:

2022-05-09 15-28-03 的屏幕截图.png

注释掉第34行代码,刷新第3步的界面,可以看到协程数降低。同时注意到锁(mutex)的数量为1。

17.输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"查看锁信息。

18.按之前的方法找到锁时间最长的地方(wolf的Howl),注释掉第54-60行并重新运行程序。这一步由于Howl调用的Unlock函数占据主要时间,所以可视化结果中可能仅显示Unlock的部分(可视化界面会过滤资源占用少的函数,无法显示与搜索),使用go tool pprof "http://localhost:6060/debug/pprof/mutex"+top+list即可搜索到目标函数。

19.刷新第三步视图,还有2个block,这不一定有问题,但我们可以检查一下。于是输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"查看阻塞信息。

20.Cat的Pee会阻塞,注释掉第38行,再次运行。此时只剩0-1个阻塞。实验结束。

2.2.2 pprof-采样过程和原理

2.2.2.1 cpu采样

  • 采样对象:函数调用和它们占用的时间
  • 采样率:100次/秒,固定值
  • 采样时间:从手动开启到手动结束
  • 采样过程:pprof进程会在操作系统上启动一个计时器,操作系统每10ns会向进程发送一次SIGPROF信号,pprof接到信号后会记录当前的调用堆栈。pprof每100ns将记录的信息写入缓冲区。进程结束时取消操作系统的定时器,并将写入的缓冲区转化为文件方便后续处理。

2.2.2.2 堆内存采样

  • 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和次数。
  • 采样率:每分配512KB记录一次,可在运行开头修改,设为1代表每次分配都记录。
  • 采样时间:从程序运行开始到采样时。
  • 采样指标:alloc_objects、alloc_space、inuse_objects、inuse_space。
  • 计算方式:inuse = alloc - free

2.2.2.3 协程与线程

  • Goroutine:记录所有用户发起且在运行中的goroutine(即入口不是runtime开头的)runtime.main的调用栈信息。
  • Threadreate:记录程序创建的所有系统线程的信息。
  • 遍历记录协程或线程的表,记录它们的调用信息。

2.2.2.4 阻塞和锁

  • 阻塞:采样阻塞操作的次数和耗时。耗时超过阈值才会被记录,阈值可设定,设为1代表每次阻塞都记录。
  • 锁竞争:采样争抢锁的次数和耗时。通过随机数来记录一定比例的锁操作。若设定1则记录每次加锁。

2.3 性能调优案例

2.3.1业务服务优化

基本概念:

  • 服务:能单独部署,承载一定功能的程序。
  • 依赖:A的功能实现依赖B的响应结果,则称A依赖B。
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
  • 基础库:公共的工具包、中间件

流程:

  1. 建立服务性能评估手段:

     服务性能评估方式:单独benchmark无法满足复杂逻辑分析;不同负载情况下性能表现差异
     请求流量构造:不同请求参数覆盖逻辑不同;线上真实流量情况
     压测范围:单机器压测;集群压测
     性能数据采集:单极性能数据;集群性能数据
    
  2. 分析性能数据,定位性能瓶颈

     使用库不规范:json库、日志库
     高并发场景优化不足:用异步优化同步
    
  3. 重点优化项的改造

     正确性是基础:和优化前的结果做对比,分析正确率是否下降
    
  4. 优化效果验证

     重复压测验证
     上线评估优化效果:关注服务监控;逐步放量;收集性能数据
    

整体链路分析:

  • 规范上游服务调用接口,明确场景需求
  • 分析链路,通过业务优化提升服务性能

2.3.2基础库优化

AB实验SDK的优化:

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

2.3.3go语言优化

编译器&运行时优化:

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

优点:

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

4.课后作业(copy整理的官方答案)

1.其他语言的编码规范是否与Go语言有相通之处?

可以了解开源项目的编码规范,比如Google开源项目风格指南
google.github.io/styleguide/
中文版:
https://zh-google-styleguide.readthedocs.io/en/latest/

2.有没有方式能够自动化对代码进行检测?

Go 语言有代码检查工具,可以和CI进行集成
https://github.com/golangci/golangci-lint

3.查看看Go的源码github.com/golang/go/t…

可以先从sync包和net包入手
https://github.com/golang/go/tree/master/src/sync
https://github.com/golang/go/tree/master/src/net

4.使用 Go 进行并发编程时有哪些性能陷阱或者优化手段?

可以了解Effective Go的并发编程章节 https://go.dev/doc/effective_go#concurrency
以及 https://github.com/geektutu/high-performance-go 的并发编程部分

5.了解现实的性能优化的案例

https://eng.uber.com/category/oss-projects/oss-go/
https://eng.uber.com/how-we-saved-70k-cores-across-30-mission-critical-services/

6.Go 语言每个版本在性能上有哪些重要的优化点?

每次更新有文档说明
https://go.dev/doc/devel/release