Go性能优化-性能调优实战 | 青训营笔记

167 阅读12分钟

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

1. 简介

性能调优原则

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

2. 性能分析工具-pprof

性能调优的前提是对应用程序性能表现有实际的数据指标,例如希望知道应用在什么地方耗费了多少CPUMemory,而对 go 来说,pprof 是用于数据可视化和分析性能分析数据的非常方便的工具

2.1 功能简介

image-20230125104945896

  • 分析:有两种可视化方式

  • 工具:可以在 runtime/pprof 中找到源码,同时 Golanghttp 标准库中也对 pprof 做了一些封装,能让你在 http 服务中直接使用它

  • 采样:它可以采样程序运行时的 CPU、堆内存、goroutine、锁竞争、阻塞调用和系统线程的使用数据

  • 展示:用户可以通过列表、调用图、火焰图、源码、反汇编等视图去展示采集到的性能指标,方便分析

说这么多不如来个实践项目来熟悉 pprof 的使用😆

2.2 排查实战

1、搭建 pprof 项目

既然是练习排查性能问题,那么就需要构造一个有问题的出现,然后利用 pprof 来定位性能问题点,这里有一个开源项目,已经构造了一些性能问题来供我们练习

开源项目的详细介绍和操作步骤:blog.wolfogre.com/posts/go-pp…

// 使用命令将其下载下来
go get -d github.com/wolfogre/go-pprof-practice
// 移动到该项目文件下
cd $GOPATH/src/github.com/wolfogre/go-pprof-practice
// 构建该项目
go build
// 执行该项目,控制台里应该会不停的打印日志,都是一些“猫狗虎鼠在不停地吃喝拉撒”的屁话,没有意义,不用细看
./go-pprof-practice
2、浏览器查看指标

浏览器打开http://localhost:6060/debug/pprof/,会看到:

image-20230125120355850

类型描述
alloc内存分配情况的采样信息
blocks阻塞操作情况的采样信息
cmdline显示程序启动命令及参数
goroutine当前所有协程的堆栈信息
heap堆上内存使用情况的采样信息
mutex锁争用情况的采样信息
profileCPU 占用情况的采样信息
threadcreate系统线程创建情况的采样信息
trace程序运行跟踪信息

因为 cmdline 显示运行进程的命令,没有什么实验价值,trace 需要另外的工具解析,且与本文主题关系不大,threadcreate 涉及的情况偏复杂不透明,所以这三个类型的采样信息这里暂且不提。除此之外,其他所有类型的采样信息本文都会涉及到,且炸弹程序已经为每一种类型的采样信息埋藏了一个对应的性能问题,以供我们进行实践。

image-20230125132259244

看到的数据可读性很差,长这样,可以看出一些信息但很难阅读它,所以一会儿我们会借助 pprof 工具帮我们「阅读」这些指标。

3、CPU

我们先从CPU问题排查开始,不同的操作系统工具可能不同,我们首先使用自己熟悉的工具看看程序进程的资源占用,CPU占用了20.1%,显然这里是有问题的。

image-20230125135132516

pprof 的采祥结果是将一段时间内的信息汇总输出到文件中,所以首先需要拿到这个 profile 文件。你可以直接使用暴露的接口链接下载文件后使用,也可以直接用 pprof 工具连接这个接口下载需要的数据,这里我们使用 go tool pprof +采样链接 来启动采样。

go tool pprof http://loaclhost:6060/debug/pprof/profile?seconds=10

链接中就是【炸弹】程序暴露出来的推口,链接结尾的 profile 代表采样的对象是 CPU 使用。如果你在浏览器里直接打开这个链接,会启动一个60秒的采样,并在结束后下载文件,这里我们加上 seconds=10 的参数,让它采样十秒,稍等片刻,我们需要的采样数据已经记录和下载完成,并展示出 pprof 终端。

image-20230125134212904

命令

1、topN

查看占用资源最多的函数

image-20230125134526335

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

可以简单的理解为数字越大占用情况越严重

表格前面描述了采样的总体信息,默认会展示资源占用最高的 10 个函数,如果只需要直看最高的 N 个函数,可以输入 topN,例如查看最高的 3 个调用,输入 top3 可以看到表格的第一行里,Tiger.Eat 函数本身占用 3.41 秒的 CPU 时间,占总时间的 96.60%,显然问题就是这里引起的。

但是可以看到上图,flat和cum有的是相等的,有点不相等,有的一边直接为0了,why?

cum-flat 得到的是函数中调用其他函数所消耗的资源,所以在函数中没有对其他函数进行调用时,cum-flat=0,也就是 flat=cum 相应地,函数中除了调用另外的函数,没有其他逻辑时,flat=0

2、list

根据指定的正则表达式查找代码行

image-20230125140152152

list 命令会根据后面给定的正则表达式查找代码,并按行展示出每一行的占用,可以看到,第 24 行有一个100亿次的空循环,占用了 3.41 秒的CPU时间,问题就在这儿了,定位成功。

3、web

调用关系可视化

4、Heap-堆内存

可以看到当我们注释掉问题代码,重新运行后,CPU消耗一下加下来了,然而内存使用依然很高。

image-20230125143636244

后面因为某些问题,无法执行 graphviz,请大家移步blog.wolfogre.com/posts/go-pp…

2.3 采样过程和原理

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

image-20230125151936002

image-20230125153111796

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

image-20230125163123710

2.3.5 小结
  • 掌握常用 pprof 工具功能
  • 灵活运用 pprof 工具分析解决性能问题
  • 了解 pprof 的采样过程和工作原理

3. 性能调优案例

介绍实际业务服务性能优化的案例,对逻辑相对复杂的程序如何进行性能调优

3.1 业务服务优化

业务服务一般指直接提供功能的程序,比如专门处理用户评论操作的程序

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

image-20230125153941351

上面是系统部署的简单示意图,客户端请求经过网关转发,由不同的业务服务处理,业务服务可能依赖其他的服务,也可能会依赖存储、消息队列等组件。接下来我们以业务服务优化为例,说明性能调优的流程,图中的 Service BService A 依赖,同时也依赖了存储和 Service D

2、流程
  • 建立服务性能评估手段

  • 分析性能数据,定位性能瓶颈

  • 核心,这里也是用的 pprof 采样性能数据,分析服务的表现

  • 重点优化项改造

  • 进行重构代码,使用更高效的组件

  • 优化效果验证

  • 通过压测对比和正确性验证之后,服务可以上线进行实际收益评估

整体的流程可以循环并行执行,每个优化点可能不同,可以分别评估验证

3、建立服务性能评估手段
  • 服务性能评估方式

  • 单独 benchmark 无法满足复杂逻辑分析

  • 不同负载情况下性能表现差异(下图是负载和单核qps的对应数据)

image-20230125155448314

image-20230125155549368

  • 请求流量构造

  • 不同请求参数覆盖逻辑不同

  • 线上真实流量情况,才能分析真正的性能瓶颈

  • 压测范围(会录制线上流量请求,通过控制回放速度来对服务进行测试)

  • 单机器压测

  • 集群压测

  • 性能数据采集

  • 单机性能数据

  • 集群性能数据

评估手段建立后,它的产出实际是一个服务的性能指标分析报告

实际的压测报告截图,会统计压测期间服务的各项监控指标,包括qps,延迟等内容,同时在压测过程中,也可以采集服务的 pprof 数据,使用之前的方式分析性能问题

有了服务优化前的性能报告和一些性能采样数据,我们可以进行性能瓶颈分析了

业务服务常见的性能问题可能是使用基础组件不规,比如这里通过火焰图看出 JSON 的解析部分占用了较多的CPU资源,那么我们就能定位到具体的逻辑代码,是在每次使用配置时都会进行 JSON 解析,拿到配置项,实际组件内部提供了缓存机制,只有数据变更的时候才需要重新解析JSON

image-20230125160740003

image-20230125160805399

4、分析性能数据,定位性能瓶颈
  • 使用库不规范

    还有是类似日志使用不规范,一部分是调试日志发布到线上,一部分是线上服务在不同的调用链路上数据有差别,测试场景日志量还好,但是到了真实线上全量场景,会导致日志量增加,影响性能

    image-20230125161012809
  • 高并发场景优化不足

    另外常见的性能问题就是高并发场景的优化不足,上者是服务高峰期的火焰图,下者是低峰期的火焰图,可以发现 metrics,即监控组件的 CPU 资源占用变化较大,主要原因是监控数据上报是同步请求,在请求量上涨,监控打点数据量增加时,达到性能瓶颈,造成阻塞,影响业务逻辑的处理,后续是改成异步上报的机制提升了性能

    image-20230125161315255 image-20230125161402087
5、重点优化项改造
  • 正确性是基础
  • 响应数据 diff
  • 线上请求数据录制回放
  • 新旧逻辑接口数据 diff

性能忧化的前提是保证正确性,所以在变动较大的性能优化上线之前,还需要进行正确性验证,因为线上的场景和流程太多,所以要借助自动化手段来保证优化后程序的正确性

同样是线上请求的录制,不过这里不仅包含请求参数录制,还会录制线上的返回内容,重放时对比线上的返回内容和优化后服务的返回内容进行正确性验证

比如图中作者信息相关的字段值在优化有有变化,需要进一步排查原因

image-20230125161748262

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

image-20230125162011970image-20230125162037251

验证分两部分,首先依然是用同样的数据对优化后的服务进行压测,可以看到现在的数据比优化前好很多,能够支持更多的qps正式上线的时候会逐步放量,记录真正的优化效果

同时压测并不能保证和线上表现完全一致,有时还要通过线上的表现再进行分析改进,是个长期的过程

7、进一步优化,服务整体链路分析
  • 规范上游服务调用接口,明确场景需求
  • 分析链路,通过业务流程优化提升服务性能

在熟悉服务的整体部署情况后,可以针对具体的接口链路进行分析调优,比如 Service A 调用 Service B 是否存在重复调用的情况,调 Service B 服务时,是否更小的结果数据集就能满足需求,接口是否一定要实时数据,能否在 Service A 层进行缓存,减轻调用压力

这种优化只使用与特定业务场景,适用范围窄,不过能更合理的利用资源

3.2 基础库优化

image-20230125162835686

3.3 Go语言优化

适用范围最广的优化,就是针对Go本身进行的优化,会优化编译器和运行时的内存分配策略,构建更高效的go发行版本

编译器&运行时优化

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

优点

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

4. 总结

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

因为目前还是有点问题,以后会补,如有错误,还请见谅,欢迎指正!