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

99 阅读5分钟

Go 高质量编程和性能调优

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

2 性能调优

2.1 性能调优原则

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

2.2 性能分析工具 pprof

希望知道应用在什么地方耗费了多少 CPU、 Memory pprof 是用于可视化和分析性能分析数据的工具

2.2.1 功能简介

具体 pprof 有哪些内容?可以看下图片

  • 分析部分 - 有两种方式
  • 具体的工具 - 可以在 runtime/pprof 中找到源码, 同时 Golang 的 http 标准库中也对 pprof 做了一些封装,能让你在 http 服务中直接使用它
  • 采样部分 - 它可以采样程序运行时的CPU、 堆内存、goroutine、锁竞争、 阻塞调用和系统线程的使用数据
  • 展示 - 用户可以通过列表、调用图、火焰图、 源码、 反汇编等视图去展示采集到的性能指标,方便分析

image.png

2.2.2 排查实战

我们介绍了五种使用 pprof 采集的常用性能指标:CPU、堆内存、Goroutine、 锁竞争和阻塞;两种展示方式:交互式终端和网页;四种视图:Top、Graph、源码和火焰图。 pprof 除了 http 的获取方式之外, 也可以直接在运行时调用 runtime/pprof包将指标数据输出到本地文件。视图中还有一个更底层的反汇编视图。

2.2.3 采样过程和原理

2.2.3.1 CPU
  • 采样对象:函数调用和它们占用的时间
  • 采样率:100次/秒,固定值
  • 采样时间:从手动启动到手动结束

首先来看CPU。CPU采样会记录所有的调用栈和它们的占用时间。在采样时,进程会每秒暂停一百次,每次会记录当前的调用栈信息。汇总之后,根据调用栈在采样中出现的次数来推断函数的运行时间。你需要手动地启动和停止采样。每秒100次的暂停频率也不能更改。这个定时暂停机制在unix或类unix系统上是依赖信号机制实现的。每次「暂停」都会接收到一个信号,通过系统计时器来保证这个信号是固定频率发送的。

image.png

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

接下来看看堆内存采样。提到内存指标的时候说的都是「堆内存」而不是 「内存」,这是因为 pprof 的内存采样是有局限性的。内存采样在实现上依赖了 内存分配器的记录, 所以它只能记录在堆上分配,且会参与 GC 的内存,一些其他的内存分配, 例如调用结束就会回收的栈内存、一些 更底层使用 cgo 调用分配的内存,是不会被内存采样记录的。 它的采样率是一个大小, 默认每分配 512KB 内存会采样一次, 采样率是可以在运行开头调整的,设为 1 则为每次分配都会记录。

与 CPU 和 goroutine 都不同的是, 内存的采样是一个持续的过程, 它会记录从程序运行起的所有分配或释放的内存大小和对象数量, 并在采样时遍历这些结果进行汇总。 还记得刚才的例子中, 堆内存采样的四种指标吗? alloc 的两项指标是从程序运行开始的累计指标,而 inuse 的两项指标是通过累计分配减去累计释放得到的程序当前持有的指标。 你也可以通过比较两次alloc 的差值来得到某 一段时间程序分配的内存大小和数量。

2.2.3.3 Goroutine 协程和 ThreadCreate 线程创建

接下来我们来看看 goroutine 和系统线程的采样。这两个采样指标在概念,上和实现上都比较相似,所以在这里进行对比。 Goroutie 采样会记录所有用户发起,也就是入口不是runtime开头的goroutine,以及main函数所在goroutine的信息和创建这些goroutine的调用栈;他们在实现上非常的相似,都是会在STW之后,遍历所有goroutinel/所有线程的列表(图中的m就是GMP模型中的m,在golang中和线程一 对应)并输出堆栈,最后Start The World继续运行。这个采样是立刻触发的全量记录,你可以通过比较两个时间点的差值来得到某- 时间段的指标。

image.png

2.2.3.4 阻塞和锁竞争

最后是阻塞和锁竞争这两种采样。 这两个指标在流程和原理上也非常相似,这两个采样记录的都是对应操作发生的调用栈、次数和耗时,不过这两个指标的采样率含义并不相同。 阻塞操作的采样率是一个「阈值」,消耗超过阈值时间的阻塞操作才会被记录,1 为每次操作都会记录。锁竞争的采样率是一个「比例」,运行时会通过随机数来只记录固定比例的锁操作,1伪每次操作都会记录。

它们在实现上也是基本相同的。都是一个「主动上报」的过程。在阻塞操作或锁操作发生时,会计算出消耗的时间,连同调用栈一起主动上报给采样器,采样器会根据采样率可能会丢弃一些记录。在采样时,采样器会遍历已经记录的信息,统计出具体操作的次数、调用栈和总耗时。和堆内存一样,你可以对比两个时间点的差值计算出段时间内的操作指标。

image.png