Golang线上内存爆掉问题排查(pprof)

2024-03-30 16:04

本文主要是介绍Golang线上内存爆掉问题排查(pprof),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Golang线上内存爆掉问题排查(pprof)

1 问题描述

某天,售后同事反馈,我们服务宕掉了,客户无法预览我们的图片了。

  • 我们预览图片是读取存储在我们S3服务的数据,然后返回给前端页面展示。
  • 因为客户存在几百M的图片,所以一旦请求并发一上来,很容易就把内存打爆。

2 pprof分析

声明: 涉及到数据敏感,以下代码是我模拟线上故障的一个情况。

好在我们程序都有添加pprof监控,于是直接通过go tool pprof分析:

①获取堆内存分配情况:go tool pprof http://xx/debug/pprof/heap

# localhost直接改成自己程序的IP:端口
go tool pprof http://localhost:80/debug/pprof/heap

在这里插入图片描述

②过滤出占用堆内存前10的方法:top 10

# 过滤占用堆内存排名前10方法
top 10

在这里插入图片描述

参数解析:

  • flat:表示此函数分配、并由该函数持有的内存空间。
  • cum:表示由这个函数或它调用堆栈下面的函数分配的内存总量。

③查看方法详情:list testReadAll

可以看到我们自己程序的方法是main包下面的testAll方法占用了875MB多内存。

# 查看方法详情
list testReadAll

在这里插入图片描述

最后定位到ioutil.ReadAll这个方法占用了太多内存。

  • 熟悉的朋友都清楚,ioutil.ReadAll是直接将文件或者流数据一次性读取到内存里。如果文件过大或者多个请求同事读取多个文件,会直接将服务器内存打爆。

因为我们的客户有几百M的图片,所以一旦并发以上来很可能打爆。因此这里需要改成流式的io.Copy

3 解决:改用流式io.Copy()

定位到问题后,直接改用流式方式给前端返回。

_, err = io.Copy(ctx.ResponseWriter(), file)

🚀:由于这次新人的失误,加上测试数据量不够大,导致出现线上问题,所以大家以后还是要多review代码+增加压力测试。

4 本地测试io.Copy与ioutil.ReadAll

  1. 编写demo代码
package mainimport ("github.com/kataras/iris/v12""github.com/kataras/iris/v12/context""io""io/ioutil""net/http"_ "net/http/pprof""os"
)func main() {app := iris.New()go func() {http.ListenAndServe(":80", nil)}()//readAllapp.Get("/readAll", testReadAll)//io.Copyapp.Get("/ioCopy", func(ctx *context.Context) {file, err := os.Open("/Users/ziyi2/GolandProjects/MyTest/demo_home/io_copy_demo/xx.zip")if err != nil {panic(err)}defer file.Close()_, err = io.Copy(ctx.ResponseWriter(), file)if err != nil {panic(err)}})app.Listen(":8080", nil)
}func testReadAll(ctx *context.Context) {file, err := os.Open("/Users/ziyi2/GolandProjects/MyTest/demo_home/io_copy_demo/xx.zip")if err != nil {panic(err)}defer file.Close()//simulate onLine errbytes, err := ioutil.ReadAll(file)if err != nil {panic(err)}_, err = ctx.Write(bytes)if err != nil {panic(err)}
}
  1. 打开资源监视器,同时发起readAll请求,观察内存占用
  • 发起readAll请求前
    在这里插入图片描述
  • 发送readAll请求
localhost:8080/readAll

我本地是模拟读取差不多1G左右的文件,可以看到ioutil.ReadAll直接一次性将内容读取到了内存。(一旦并发量上来,或者图片文件超大,后果不敢想象)
在这里插入图片描述
3. 再观察io.Copy方法

  • 发送ioCopy请求
localhost:8080/ioCopy
  • 流式传输,最后程序内存并没有暴涨
    在这里插入图片描述
    在这里插入图片描述

结论:

ioutil.ReadAll:会将数据一次性加载到内存。
io.Copy:流式拷贝,不会导致内存暴涨
因此对于大文件或者数据量不确定的场景推荐使用io.Copy

拓展:pprof使用

① 引入pprof

  1. 引入pprof包
  2. 开启一个协程监听
package mainimport ("github.com/kataras/iris/v12""github.com/kataras/iris/v12/context""net/http"_ "net/http/pprof""os"
)func main() {app := iris.New()go func() {http.ListenAndServe(":80", nil)}()app.Listen(":8080", nil)
}

②查看分析报告

1 浏览器直接访问
http://IP:Port/debug/pprof

在这里插入图片描述

2 go tool 命令行直接分析
# 查看堆内存信息
go tool pprof http://IP:Port/debug/pprof/heap# 查看cpu信息
go tool pprof http://IP:Port/debug/pprof/profile## -seconds=5设置采样时间为5s
# go tool pprof -seconds=5 http://IP:Port/debug/pprof/profile# 查看协程信息
go tool pprof http://IP:Port/debug/pprof/goroutine# 查看代码阻塞信息
go tool pprof http://IP:Port/debug/pprof/block# 需要查看什么信息将URL默认的Type更换为对应类型即可

-seconds=30 设置采样30s,也可以自定义时间范围。需要注意的是,对于profile而言,总是需要采样一段时间,才可以看到数据。而其他历史累计的数据,则可以直接获取从程序开始运行到现在累积的数据,也可以设置-seconds来获取一段时间内的累计数据。而其他实时变化的指标,设置这个参数没什么用,只会让你多等一会。

  1. allocs: A sampling of all past memory allocations【所有内存分配,历史累计】
  2. block: Stack traces that led to blocking on synchronization primitives【导致阻塞同步的堆栈,历史累计】
  3. cmdline: The command line invocation of the current program【当前程序命令行的完整调用路径】
  4. goroutine: Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.【当前所有运行的goroutine堆栈信息,实时变化】
  5. heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.【查看活动对象的内存分配情况,实时变化】
  6. mutex: Stack traces of holders of contended mutexes【导致互斥锁竞争持有者的堆栈跟踪,历史累计】
  7. profile: CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.【默认进行30s的CPU Profing,用于观察CPU使用情况】
  8. threadcreate: Stack traces that led to the creation of new OS threads【查看创建新OS线程的堆栈跟踪信息】
  9. trace: A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.【当前程序执行链路】

注意:默认情况下是不追踪block和mutex的信息的,如果想要看这两个信息,需要在代码中加上两行:

runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪,block  
runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪,mutex
3 导出为.out文件+命令行分析(推荐)

推荐使用导出文件方式,虽然步骤繁琐,但是有文件落地,保证重要数据不会丢失

//导出为文件
curl -o heap.out http://IP:Port/debug/pprof/heap//解析文件并进入命令行交互
go tool pprof heap.out
//后续操作就和命令行直接分析如出一辙//top 10 
//list funcName

在这里插入图片描述

③参数解析

1 采样类型
allocs:所有内存分配,历史累计

allocs: A sampling of all past memory allocations【所有内存分配,历史累计】

block:导致阻塞同步的堆栈信息,历史累计(每发生一次阻塞取样一次)

block: Stack traces that led to blocking on synchronization primitives【导致阻塞同步的堆栈,历史累计】

  • Block Goroutine阻塞事件的记录 默认每发生一次阻塞事件时取样一次
cmdline:程序命令行的完整调用路径

cmdline: The command line invocation of the current program【当前程序命令行的完整调用路径】

goroutine:当前程序运行的所有goroutine,实时变化(获取时取样一次)

goroutine: Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.【当前所有运行的goroutine堆栈信息,实时变化】

  • 活跃Goroutine信息的记录 仅在获取时取样一次
heap:查看堆内存分配情况,实时变化(每分配512K取样一次)

heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.【查看活动对象的内存分配情况,实时变化】

  • Heap 堆内存分配情况的记录 默认每分配512K字节时取样一次
mutex:导致互斥锁竞争的堆栈跟踪,历史累计

mutex: Stack traces of holders of contended mutexes【导致互斥锁竞争持有者的堆栈跟踪,历史累计】

profile:CPU使用情况

profile: CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.【默认进行30s的CPU Profing,用于观察CPU使用情况】

threadcreate:创建新线程的堆栈信息(获取时取样)

threadcreate: Stack traces that led to the creation of new OS threads【查看创建新OS线程的堆栈跟踪信息】

  • 系统线程创建情况的记录 仅在获取时取样一次
trace:程序整个执行链路

trace: A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.【当前程序执行链路】

注意:默认情况下是不追踪block和mutex的信息的,如果想要看这两个信息,需要在代码中加上两行:

runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪,block  
runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪,mutex
2 统计维度(以内存取样为例)

如果是初次接触pprof,可能会疑惑flat、sum、cum代表什么意思

在这里插入图片描述
官网解析:

  • The first two columns show the number of samples in which the function was running (as opposed to waiting for a called function to return), as a raw count and as a percentage of total samples.

  • The third column shows the running total during the listing.

  • The fourth and fifth columns show the number of samples in which the function appeared (either running or waiting for a called function to return). To sort by the fourth and fifth columns, use the -cum (for cumulative) flag.

  • 官网地址:https://go.dev/blog/pprof

以获取内存为例:

flat:当前函数分配的内存,不包含它调用其他函数造成的内存分配
flat%:当前函数分配内存占比
sum%:自己和前面所有的flat%累积值
cum:当前函数及当前函数调用其他函数的分配内存的汇总
cum%:这个函数分配的内存,以及它调用其他函数分配的内存之和

这篇关于Golang线上内存爆掉问题排查(pprof)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/861933

相关文章

MySQL主从同步延迟问题的全面解决方案

《MySQL主从同步延迟问题的全面解决方案》MySQL主从同步延迟是分布式数据库系统中的常见问题,会导致从库读取到过期数据,影响业务一致性,下面我将深入分析延迟原因并提供多层次的解决方案,需要的朋友可... 目录一、同步延迟原因深度分析1.1 主从复制原理回顾1.2 延迟产生的关键环节二、实时监控与诊断方案

SQLyog中DELIMITER执行存储过程时出现前置缩进问题的解决方法

《SQLyog中DELIMITER执行存储过程时出现前置缩进问题的解决方法》在SQLyog中执行存储过程时出现的前置缩进问题,实际上反映了SQLyog对SQL语句解析的一个特殊行为,本文给大家介绍了详... 目录问题根源正确写法示例永久解决方案为什么命令行不受影响?最佳实践建议问题根源SQLyog的语句分

解决IDEA报错:编码GBK的不可映射字符问题

《解决IDEA报错:编码GBK的不可映射字符问题》:本文主要介绍解决IDEA报错:编码GBK的不可映射字符问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录IDEA报错:编码GBK的不可映射字符终端软件问题描述原因分析解决方案方法1:将命令改为方法2:右下jav

MyBatis模糊查询报错:ParserException: not supported.pos 问题解决

《MyBatis模糊查询报错:ParserException:notsupported.pos问题解决》本文主要介绍了MyBatis模糊查询报错:ParserException:notsuppo... 目录问题描述问题根源错误SQL解析逻辑深层原因分析三种解决方案方案一:使用CONCAT函数(推荐)方案二:

Redis 热 key 和大 key 问题小结

《Redis热key和大key问题小结》:本文主要介绍Redis热key和大key问题小结,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、什么是 Redis 热 key?热 key(Hot Key)定义: 热 key 常见表现:热 key 的风险:二、

IntelliJ IDEA 中配置 Spring MVC 环境的详细步骤及问题解决

《IntelliJIDEA中配置SpringMVC环境的详细步骤及问题解决》:本文主要介绍IntelliJIDEA中配置SpringMVC环境的详细步骤及问题解决,本文分步骤结合实例给大... 目录步骤 1:创建 Maven Web 项目步骤 2:添加 Spring MVC 依赖1、保存后执行2、将新的依赖

Spring 中的循环引用问题解决方法

《Spring中的循环引用问题解决方法》:本文主要介绍Spring中的循环引用问题解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录什么是循环引用?循环依赖三级缓存解决循环依赖二级缓存三级缓存本章来聊聊Spring 中的循环引用问题该如何解决。这里聊

Golang HashMap实现原理解析

《GolangHashMap实现原理解析》HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持高效的插入、查找和删除操作,:本文主要介绍GolangH... 目录HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持

Spring Boot中JSON数值溢出问题从报错到优雅解决办法

《SpringBoot中JSON数值溢出问题从报错到优雅解决办法》:本文主要介绍SpringBoot中JSON数值溢出问题从报错到优雅的解决办法,通过修改字段类型为Long、添加全局异常处理和... 目录一、问题背景:为什么我的接口突然报错了?二、为什么会发生这个错误?1. Java 数据类型的“容量”限制

关于MongoDB图片URL存储异常问题以及解决

《关于MongoDB图片URL存储异常问题以及解决》:本文主要介绍关于MongoDB图片URL存储异常问题以及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录MongoDB图片URL存储异常问题项目场景问题描述原因分析解决方案预防措施js总结MongoDB图