Go 性能优化 性能分析 性能调优 | 青训营笔记

91 阅读5分钟

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


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

part1 性能优化

性能优化原则

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

性能优化建议

  • 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率

  • 1. slice
  • 在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时

  • 原理

    • 切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景:

      • 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
      • 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
    • 因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能

  • 另一个陷阱:大内存得不到释放

    当我们想要以一个巨大的数组的后两位切片拓展下来进行下面的操作的时候 如果我们继续沿用之前的切片 之前的切片不会去清除数据 所以除了后两位之外的数据均为浪费的冗余数据

    解决方案: 使用 copy去复制后两位 创造新的底层数组

  • 2. map
    • 预分配内存 减少扩容导致的数据拷贝
  • 3. 使用 strings.Builder
    • 常见的字符串拼接方式

      • +的方式
      • strings.Builder
      • bytes.Buffer
    • strings.Builder 最快,bytes.Buffer 较快,+ 最慢

    • 原理

      • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
      • strings.Builder,bytes.Buffer 的内存是以倍数申请的
      • strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回
  • 4. 使用空结构体节省内存
    • 空结构体不占据内存空间,可作为占位符使用
  • 5. 使用 atomic 包
    • 原理

      • 锁的实现是通过操作系统来实现,属于系统调用,atomic 操作是通过硬件实现的,效率比锁高很多
      • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
      • 对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface{}
总结
  • 避免常见的性能陷阱可以保证大部分程序的性能

  • 很多性能优化都是通过减少内存的再次申请 而预先分配出来

  • 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能

part2 性能分析工具

我们下面仅讨论 pprof工具

  • runtime/pprof:采集程序(非 Server)的运行数据进行分析
  • net/http/pprof:采集 HTTP Server 的运行时数据进行分析
  1. 命令行分析

    go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
    top 命令
     flat/flat%:分别表示在当前层级cpu的占用时间和百分比。
     cum/cum%:当前函数及当前函数的子函数占用的cpu时间
     sum%:所有层级的cpu时间累积占用
    
    list 命令 -list xxx
     查看对应参数的具体代码
    
    web 打开页面
    
    火焰图关系图
    
    png     以png格式输出图 
    pdf	以pdf格式输出图
    svg	以svg格式生成图(需要安装Graphviz)
    
  2. 网页分析

    go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/profile?seconds=10"
    
    • 排查堆内存问题 heap

    • 排查申请又释放的空间 allocs

    • 排查协程问题 goroutine

    • 排查锁问题 mutex

    • 排查阻塞问题 block

  3. 打开pprof

     添加依赖 
     "net/http"
     _ "net/http/pprof"
     
     第一行加上 
         http.ListenAndServe("0.0.0.0:6060", nil)
    

part3 性能调优

基本概念

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

基本方向

  1. 业务优化
  2. 基础库优化
  3. Go 语言优化

具体实现

1. 业务优化
  • 流程

    • 建立服务性能评估手段
    • 分析性能数据,定位性能瓶颈
    • 重点优化项改造
    • 优化效果验证
  • 建立压测评估链路

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

    • pprof 火焰图
  • 重点优化项分析

    • 规范组件库使用
    • 高并发场景优化
    • 增加代码检查规则避免增量劣化出现
    • 优化正确性验证
  • 上线验证评估

    • 逐步放量,避免出现问题
  • 进一步优化,服务整体链路分析

    • 规范上游服务调用接口,明确场景需求
    • 分析业务流程,通过业务流程优化提升服务性能
2. 基础库优化
  • 适应范围更广,覆盖更多服务

  • AB 实验 SDK 的优化

    • 分析基础库核心逻辑和性能瓶颈
    • 完善改造方案,按需获取,序列化协议优化
    • 内部压测验证
    • 推广业务服务落地验证
3. Go 语言优化
  • 适应范围最广,Go 服务都有收益

  • 优化方式

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

参考资料


码风略丑 读者见谅 --2023/2/2