每日一课,不论长短,有所学有所得

业精于勤技在专,行则将至事必成

给大家分享一些书籍,希望能够提供一些帮助

上篇内容我们介绍了golang中可能导致内存泄露的一些情况,属于预防阶段,但是我们可能又很难保证自己的经验和能力,以及对线上代码的掌控力,因此如何对已经运行的代码进行内存泄露的排查和发现,就需要通过监控的方法了。

pprof是go的可视化性能分析工具,它可以记录程序运行的过程中的一些cpu,内存,协程的运行情况,当需要性能调优或者定位一些bug的时候,可以通过该工具作为辅助。

runtime/pprofnet/http/pprofnet/http/pprof

pprof的使用

开始一个简单的demo:

第一步:添加代码:

package main

import (
"fmt"
"net/http"
_ "net/http/pprof" // 引入pprof库
)

func main() {
// 制造一个协程泄露事件
for i := 0; i < 5; i++ {
go func() {
select {
default:
return
} // 不阻塞
}()
go func() {
select {} // 泄漏了 1000 个协程
}()
}

// 开启pprof的监听端口
if err := http.ListenAndServe(":6069", nil); err != nil {
fmt.Printf("start pprof failed")
}
}

第二步:运行代码后,浏览器查看/命令行查看

http://127.0.0.1:6069/debug/pprof/

或者通过命令行下载数据到本地进行分析:

# 下载cpu profile,默认从当前开始收集30s的cpu使用情况,需要等待30s
# 30-second CPU profile
go tool pprof http://localhost:6069/debug/pprof/profile  
# wait 120s
go tool pprof http://localhost:6069/debug/pprof/profile?seconds=120    

# 下载heap profile
go tool pprof http://localhost:6069/debug/pprof/heap

# 下载goroutine profile
go tool pprof http://localhost:6069/debug/pprof/goroutine

# 下载block profile
go tool pprof http://localhost:6069/debug/pprof/block

# 下载mutex profile
go tool pprof http://localhost:6069/debug/pprof/mutex

pprof参数

以上使用pprof的demo已启动完成,接下来我们的重点就是怎么用怎么看pprof的各种参数。

pprof web首页中的几个参数:

allocs: 内存分配情况
block: 阻塞堆栈的信息
cmdline: 程序启动命令及其参数
goroutine: 当前协程的堆栈信息
heap: 堆内存的信息
mutex: 锁竞争的信息
profile: cpu使用情况的信息
threadcreate: 系统程序创建情况的信息
trace: 程序运行的跟踪信息
full goroutine stack dump: 输出所有goroutine的调用栈,是goroutine的debug=2

定位goroutine内存泄露问题

我们重点关注的goroutine相关的参数(因为大部分的内存泄露是协程导致的):goroutine,full goroutine stack dump

第一步:查看goroutine:http://127.0.0.1:6069/debug/pprof/goroutine?debug=1

goroutine profile: total 85 @ 0x43c225 0x44c319 0x694e85 0x471d41/gotest/main.go:19

第二步:full goroutine stack dump:http://127.0.0.1:6069/debug/pprof/goroutine?debug=2

goroutine 7 [select (no cases), 72 minutes]:chan receive, 4minuteschan send, 4minutes/gotest/main.go:19 +0x25

以上两步骤结合一起,可以简单快速的定位出goroutine导致的内存溢出。

总结:

  1. string和slice不正确的使用会引起短暂的内存泄露

  2. 一些句柄的连接未释放会触发内存泄露

  3. 最主要的内存泄露还是在协程的内存泄露(本质是channel的阻塞),因此goroutine的正确使用很重要

  4. 一是认真学习好代码的知识点避免使用错误,二是借助工具review和排查代码

补充

本文中缺少对pprof的命令行使用方法的介绍,但其实大同小异,命令行方法适合在没有浏览器可访问的时候使用。

goroutine?debug=1goroutine?debug=2go tool pprof http://0.0.0.0:6069/debug/pprof/goroutine
OptiPlex-3050:~/gotest$ go tool pprof http://0.0.0.0:6069/debug/pprof/goroutine
Fetching profile over HTTP from http://0.0.0.0:6069/debug/pprof/goroutine
Saved profile in /home/xxx/pprof/pprof.main.goroutine.002.pb.gz
File: main
Type: goroutine
Time: Jan 24, 2022 at 2:00pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
pprof的使用命令行查看

接下来就可以通过命令行操作对已下载到本地的数据进行分析了,我们只需要记住三个指令(top+traces+list)的用法:

  1. top用法:按指标大小列出前10个函数,比如内存是按内存占用多少,CPU是按执行时间多少;

(pprof) top
Showing nodes accounting for 8, 100% of 8 total
Showing top 10 nodes out of 27
     flat  flat%   sum%        cum   cum%
        6 75.00% 75.00%          6 75.00%  runtime.gopark
        1 12.50% 87.50%          1 12.50%  net/http.(*connReader).backgroundRead
...
/**
flat: 本函数占用的内存量。
flat%: 本函数内存占使用中内存总量的百分比。
sum%: 前面每一行flat百分比的和,比如第2行虽然的100% 是 100% + 0%。
cum: 是累计量,加入main函数调用了函数f,函数f占用的内存量,也会记进来;此处cum表示协程数量
cum%: 是累计量占总量的百分比。
**/
// runtime.gopark表示挂起状态
// 此处cum表示协程阻塞的数量
  1. traces用法:打印所有调用栈,以及调用栈的指标信息。

(pprof) traces
File: main
Type: goroutine
Time: Jan 24, 2022 at 2:00pm (CST)
-----------+-------------------------------------------------------
        5   runtime.gopark
            runtime.block
            main.main.func2
-----------+-------------------------------------------------------
...
// 5   runtime.gopark 表示有5个阻塞的协程都执行此调用栈
list mainmain.mainruntime.main
(pprof) list main.main.func2
Total: 8
ROUTINE ======================== main.main.func2 in /home/shawanning/ecovacs_project/test/node/gotest/main.go
        0          5 (flat, cum) 62.50% of Total
        .          .     14:                   default:
        .          .     15:                           return
        .          .     16:                   } // 不阻塞
        .          .     17:           }()
        .          .     18:           go func() {
        .          5     19:                   select {} // 泄漏了 1000 个协程
        .          .     20:           }()
        .          .     21:   }
        .          .     22:
        .          .     23:   // 开启pprof的监听端口
        .          .     24:   if err := http.ListenAndServe(":6069", nil); err != nil {
           
// 通过list指定该调用栈,可以查看其代码,但前提是服务器中必须有未打包的源码;因此还是推荐我们通过web的访问方式去排查问题更方便
graphviz


预告:

明天去外地参加朋友的婚礼,可能就没法更文了。

恭喜我的朋友新婚快乐!愿长长久久!