>백엔드 개발 >Golang >go pprof가 뭐야?

go pprof가 뭐야?

青灯夜游
青灯夜游앞으로
2023-01-31 19:00:405323검색

pprof는 프로그램 실행 과정에서 CPU 사용량, 메모리 사용량, 고루틴 실행 상태 등 프로그램 실행 정보를 기록할 수 있는 Go의 성능 분석 도구입니다. 성능 조정이나 버그 찾기가 필요한 경우 , 이러한 기록은 정보가 매우 중요합니다. pprof를 사용하는 방법은 다양합니다. Go에는 이미 "net/http/pprof"가 패키지되어 있습니다. 몇 줄의 간단한 명령만으로 pprof를 열고, 실행 정보를 기록하고, 웹 서비스를 제공할 수 있습니다.

go pprof가 뭐야?

이 튜토리얼의 운영 환경: Windows 7 시스템, GO 버전 1.18, Dell G3 컴퓨터.

go pprof Introduction

프로파일을 일반적으로 성능분석이라고 하는데, 사전에 있는 번역은 프로필(명사) 또는 프로필(동사) 설명... 컴퓨터 프로그램의 경우 프로필은 CPU 점유, 메모리, 스레드 상태, 스레드 차단 등을 포함하여 실행 중인 프로그램에 대한 다양한 일반 정보입니다. 프로그램에 대한 이러한 정보를 알면 프로그램의 문제와 실패 원인을 쉽게 찾을 수 있습니다.

pprof는 프로그램 실행 과정에서 CPU 사용량, 메모리 사용량, 고루틴 실행 상태 등 프로그램 실행 정보를 기록할 수 있는 Go의 성능 분석 도구입니다. 성능 조정이나 버그 찾기가 필요한 경우 , 이러한 기록은 정보가 매우 중요합니다.

golang은 프로파일링을 비교적 잘 지원합니다. 표준 라이브러리는 프로파일 라이브러리 "runtime/pprof" 및 "net/http/pprof"를 제공하며 개발자의 프로파일링을 지원하는 많은 유용한 시각적 도구도 제공합니다.

온라인 서비스의 경우 HTTP 서버의 경우 pprof에서 제공하는 HTTP 인터페이스에 액세스하여 성능 데이터를 얻습니다. 물론, 실제로 여기의 최하위 계층에서는 런타임/pprof가 제공하는 함수도 호출하는데, 이 함수는 외부 네트워크 액세스를 제공하기 위해 인터페이스로 캡슐화되어 있습니다. 이 기사에서는 주로 "net/http/pprof"의 사용을 소개합니다.

기본 사용

pprof를 사용하는 방법은 여러 가지가 있습니다. Go에는 이미 net/http/pprof가 패키지되어 있습니다. 몇 줄의 간단한 명령으로 pprof를 열고 실행 정보를 기록하고 제공할 수 있습니다. 웹 서비스에서는 실행 중인 데이터를 브라우저와 명령줄을 통해 얻을 수 있습니다.

웹 서비스에서 모니터링을 활성화하는 방법, 간단한 예를 살펴보겠습니다.

package main

import (
"fmt"
 "net/http"
 _ "net/http/pprof"
)

func main() {
// 开启pprof,监听请求
 ip := "0.0.0.0:8080"
 if err := http.ListenAndServe(ip, nil); err != nil {
 	fmt.Printf("start pprof failed on %s\n", ip)
 }
 dosomething()
}

"net/http/pprof" 패키지를 프로그램으로 가져오고 청취 포트를 엽니다. 이때, 실제 제작에서는 일반적으로 이 기능을 고루틴에 캡슐화합니다. 그럼 개봉 후 어떻게 확인하나요? 세 가지 방법이 있습니다:

브라우저 방법

브라우저를 열고 ip:port/debug/pprof를 입력하고 Enter를 누르세요.

go pprof가 뭐야?

pprof는 많은 성능 데이터를 제공하겠습니다. 구체적인 의미는 다음과 같습니다.

  • allocs: 메모리 할당에 대한 샘플링 정보
  • blocks: 차단 작업에 대한 샘플링 정보 cmdline: 프로그램 시작 명령 및 해당 매개변수
  • goroutine: 현재 모든 코루틴의 스택 정보
  • heap: 힙 샘플링 메모리 사용량 정보 mutex: 잠금 경쟁에 대한 샘플링 정보
  • profile: CPU 점유에 대한 샘플링 정보
  • threadcreate: 시스템 스레드 생성에 대한 샘플링 정보
  • trace: 실행 중인 프로그램에 대한 추적 정보

allocs 전체 메모리 할당입니다. 힙은 활성 개체의 메모리 할당이며 이에 대해서는 나중에 자세히 설명합니다.

1. CPU 성능 분석이 활성화되면 Go 런타임은 10ms마다 일시 중지되어 현재 실행 중인 고루틴의 호출 스택 및 관련 데이터를 기록합니다. 성능 분석 데이터가 하드 디스크에 저장된 후 코드의 핫스팟을 분석할 수 있습니다.
2. 메모리 성능 분석은 힙이 할당될 때 호출 스택을 기록하는 것입니다. 기본적으로 1000개의 할당마다 한 번씩 샘플링되며 이 값은 변경할 수 있습니다. 스택 할당은 언제든지 해제되므로 메모리 분석에 의해 기록되지 않습니다. 메모리 분석은 샘플링 방법이고 메모리 사용이 아닌 메모리 할당을 기록하기 때문입니다. 따라서 메모리 성능 분석 도구를 사용하여 프로그램의 특정 메모리 사용량을 정확하게 결정하는 것은 어렵습니다.
3. 블로킹 분석은 CPU 성능 분석과 다소 유사하지만, 고루틴이 리소스를 기다리는 데 소비한 시간을 기록하는 매우 독특한 분석입니다. 차단 분석은 프로그램 동시성 병목 현상을 분석하는 데 매우 유용합니다. 차단 성능 분석은 다수의 고루틴이 차단되는 시기를 보여줄 수 있습니다. 차단 성능 분석은 특수 분석 도구이므로 CPU 및 메모리 병목 현상이 제거되기 전에는 분석에 사용해서는 안 됩니다.

물론, 어떤 링크라도 클릭하면 가독성이 떨어지고 분석이 거의 불가능하다는 것을 알게 될 것입니다. 그림에 표시된 대로:

go pprof가 뭐야?

힙을 클릭하고 아래쪽으로 스크롤하면 가끔 문제 해결에 도움이 될 수 있지만 일반적으로 사용되지 않습니다.

heap profile: 3190: 77516056 [54762: 612664248] @ heap/1048576
1: 29081600 [1: 29081600] @ 0x89368e 0x894cd9 0x8a5a9d 0x8a9b7c 0x8af578 0x8b4441 0x8b4c6d 0x8b8504 0x8b2bc3 0x45b1c1
#    0x89368d    github.com/syndtr/goleveldb/leveldb/memdb.(*DB).Put+0x59d
#    0x894cd8    xxxxx/storage/internal/memtable.(*MemTable).Set+0x88
#    0x8a5a9c    xxxxx/storage.(*snapshotter).AppendCommitLog+0x1cc
#    0x8a9b7b    xxxxx/storage.(*store).Update+0x26b
#    0x8af577    xxxxx/config.(*config).Update+0xa7
#    0x8b4440    xxxxx/naming.(*naming).update+0x120
#    0x8b4c6c    xxxxx/naming.(*naming).instanceTimeout+0x27c
#    0x8b8503    xxxxx/naming.(*naming).(xxxxx/naming.instanceTimeout)-fm+0x63

......

# runtime.MemStats
# Alloc = 2463648064
# TotalAlloc = 31707239480
# Sys = 4831318840
# Lookups = 2690464
# Mallocs = 274619648
# Frees = 262711312
# HeapAlloc = 2463648064
# HeapSys = 3877830656
# HeapIdle = 854990848
# HeapInuse = 3022839808
# HeapReleased = 0
# HeapObjects = 11908336
# Stack = 655949824 / 655949824
# MSpan = 63329432 / 72040448
# MCache = 38400 / 49152
# BuckHashSys = 1706593
# GCSys = 170819584
# OtherSys = 52922583
# NextGC = 3570699312
# PauseNs = [1052815 217503 208124 233034 ......]
# NumGC = 31
# DebugGC = false
  • Sys: 进程从系统获得的内存空间,虚拟地址空间
  • HeapAlloc:进程堆内存分配使用的空间,通常是用户new出来的堆对象,包含未被gc掉的。
  • HeapSys:进程从系统获得的堆内存,因为golang底层使用TCmalloc机制,会缓存一部分堆内存,虚拟地址空间
  • PauseNs:记录每次gc暂停的时间(纳秒),最多记录256个最新记录。
  • NumGC: 记录gc发生的次数

命令行方式

除了浏览器,Go还提供了命令行的方式,能够获取以上信息,这种方式用起来更方便。

使用命令go tool pprof url可以获取指定的profile文件,此命令会发起http请求,然后下载数据到本地,之后进入交互式模式,就像gdb一样,可以使用命令查看运行信息,以下为使用示例:

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

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

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

# 下载block profile
go tool pprof http://localhost:8080/debug/pprof/block # goroutine blocking profile

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

接下来用一个例子来说明最常用的四个命令:web、top、list、traces

接下来以内存分析举例,cpu和goroutine等分析同理,读者可以自行举一反三。

首先,我们通过命令go tool pprof url获取指定的profile/heap文件,随后自动进入命令行。如图:

2-go pprof가 뭐야?

第一步,我们首先输入web命令,这时浏览器会弹出各个函数之间的调用图,以及内存的之间的关系。如图:

go pprof가 뭐야?

这个图的具体读法,可参照:中文文档 或者英文文档 这里不多赘述。只需要了解越红越大的方块,有问题的可能性就越大,代表可能占用了更多的内存,如果在cpu的图中代表消耗了更多cpu资源,以此类推。
接下来 top、list、traces三步走可以看出很多想要的结果。

top 按指标大小列出前10个函数,比如内存是按内存占用多少,CPU是按执行时间多少。
top会列出5个统计数据:

  • flat: 本函数占用的内存量。
  • flat%: 本函数内存占使用中内存总量的百分比。
  • sum%: 之前函数flat的累计和。
  • cum:是累计量,假如main函数调用了函数f,函数f占用的内存量,也会记进来。
  • cum%: 是累计量占总量的百分比。

go pprof가 뭐야?

这样我们可以看到到底是具体哪些函数占用了多少内存。

当然top后也可以接参数,top n可以列出前n个函数。

list可以查看某个函数的代码,以及该函数每行代码的指标信息,如果函数名不明确,会进行模糊匹配,比如list main会列出main.mainruntime.main。现在list sendToASR试一下。

go pprof가 뭐야?

可以看到切片中增加元素时,占用了很多内存,左右2个数据分别是flatcum

traces 打印所有调用栈,以及调用栈的指标信息。使用方式为traces+函数名(模糊匹配)。

go pprof가 뭐야?

在命令行之中,还有一个重要的参数 -base,假设我们已经通过命令行得到profile1与profile2,使用go tool pprof -base profile1 profile2,便可以以profile1为基础,得出profile2在profile1之上出现了哪些变化。通过两个时间切片的比较,我们可以清晰的了解到,两个时间节点之中发生的变化,方便我们定位问题(很重要!!!!)

可视化界面

打开可视化界面的方式为:go tool pprof -http=:1234 http://localhost:8080/debug/pprof/heap 其中1234是我们指定的端口

go pprof가 뭐야?

Top

go pprof가 뭐야?

该视图与前面所讲解的 top 子命令的作用和含义是一样的,因此不再赘述。

Graph

为函数调用图,不在赘述.

Peek

go pprof가 뭐야?

Top 뷰와 비교하여 이 뷰는 상황별 정보, 즉 함수의 출력 호출자/호출 수신자 표시를 추가합니다.

Source

go pprof가 뭐야?

이 뷰는 주로 소스 코드 중심의 추적 및 분석을 추가하며, 오버헤드가 주로 어디에 소비되는지 확인할 수 있습니다.

Flame Graph

1go pprof가 뭐야?

플레임 그래프 읽기는 여기서는 다루지 않습니다.

두 번째 드롭다운 메뉴는 그림과 같습니다.

1go pprof가 뭐야?

alloc_objects,alloc_space表示应用程序分配过的资源,不管有没有释放,inuse_objects,inuse_space은 아직 출시되지 않은 애플리케이션의 정기적인 리소스 할당을 나타냅니다.

이름 의미
inuse_space 할당되었지만 아직 해제되지 않은 메모리의 양
inuse_objects 할당되었지만 아직 해제되지 않은 개체의 양
alloc_space 총액 할당된 메모리의 양(해제 여부에 관계없음)
alloc_objects 할당된 총 객체 양(해제 여부에 관계없음)

第一个下拉菜单可以与第二个下拉菜单相组合,可以查看临时变量的火焰图,常驻变量的内存调用图等。

tips

  • 程序运行占用较大的内存,可以通过 inuse_space 来体现.
  • 存在非常频繁的 GC 活动,通常意味着 alloc_space非常高,而程序运行过程中并没有消耗太多的内存(体现为 inuse_space 并不高),当然也可能出现 GC 来不及回收,因此c出现inuse_space 也变高的情况。这种情况下同样会大量消耗CPU。
  • 内存泄漏,通常 alloc_space 较高,且
    inuse_space 也较高。

操作方法

上面我们已经看完了go pprof 的所有操作,接下来讲解一下go tool pprof 的具体使用流程。

  • 通过监控平台监测到内存或cpu问题。
  • 通过浏览器方式大致判断是哪些可能的问题。
  • 通过命令行方式抓取几个时间点的profile
  • 使用web命令查看函数调用图
  • 使用top 、traces、list 命令定位问题
  • 如果出现了goroutine泄漏或者内存泄漏等随着时间持续增长的问题,go tool pprof -base比较两个不同时间点的状态更方便我们定位问题。

具体案例

案例一:goroutine泄漏

启动程序后,用浏览器方式打开profile:

1go pprof가 뭐야?

发现内存持续上升,同时goroutine也在持续上升,初步判断,内存泄漏是由于goroutine泄漏导致的。

接下来通过命令行方式抓取goroutine的情况:命令行输入:go tool pprof localhost:8080/debug/pprof/goroutine,获取结果。

分析的流程

一、使用web命令查看调用图,大概了解目前的goroutine的泄露情况:

1go pprof가 뭐야?

通过观察,最引入注目的便是runtime.gopark这个函数,这个函数在所有goroutine泄漏时都会出现,并且是大头,接下来我们研究一下这个函数的作用:

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
 mp := acquirem()
 gp := mp.curg
 status := readgstatus(gp)
 mp.waitlock = lock
 mp.waitunlockf = unlockf
 gp.waitreason = reason
 mp.waittraceev = traceEv
 mp.waittraceskip = traceskip
 releasem(mp)
 
 mcall(park_m)
}

该函数的作用为:

1、调用acquirem函数:获取当前goroutine所绑定的m,设置各类所需参数。调用 releasem 函数将当前 goroutine 和其 m 的绑定关系解除。

2、调用 park_m 函数:将当前 goroutine 的状态从 _Grunning 切换为 _Gwaiting,也就是等待状态。删除 m 和当前 goroutine m→curg(简称gp)之间的关联。

3、调用 mcall 函数,仅会在需要进行 goroutiine 切换时会被调用:切换当前线程的堆栈,从 g 的堆栈切换到 g0 的堆栈并调用 fn(g) 函数。将 g 的当前 PC/SP 保存在 g->sched 中,以便后续调用 goready 函数时可以恢复运行现场。

综上:该函数的关键作用就是将当前的 goroutine 放入等待状态,这意味着 goroutine 被暂时被搁置了,也就是被运行时调度器暂停了。
所以出现goroutine泄漏一定会调用这个函数,这个函数不是goroutine泄漏的原因,而是goroutine泄漏的结果。

此外,我们发现有两个函数的goroutine的达到了67,很可疑,在我们接下来的验证中要格外留意。

二、使用top命令,获取更加具体的函数信息:

1go pprof가 뭐야?

与上面分析的结论相似,我们要将关注点放在三个开启了67个goroutine的函数。

三、traces+函数名,查看调用栈,这一步在函数调用很复杂,无法从调用图里面清晰的看出时使用,帮助我们更清晰的了解函数的调用堆栈:

1go pprof가 뭐야?

四、使用list+函数名,查看具体代码的问题。

1go pprof가 뭐야?

1go pprof가 뭐야?

通过list命令我们可以清楚的看出问题代码是堵塞在哪里。接下来的篇幅我们来分析一下这个问题:

goroutine泄漏知识

什么是goroutine泄漏:如果你启动了一个 goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是 goroutine 泄露。当 goroutine 泄露发生时,该 goroutine 的栈(一般 2k 内存空间起)一直被占用不能释放,goroutine 里的函数在堆上申请的空间也不能被 垃圾回收器 回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃。
什么时候出现goroutine泄漏:goroutine泄露一般是因为channel操作阻塞而导致整个routine一直阻塞等待或者 goroutine 里有死循环。
具体细分一下:

  • 从 channel 里读,但是没有写。
  • 向 unbuffered channel 写,但是没有读。
  • 向已满的 buffered channel 写,但是没有读。
  • select操作在所有case上阻塞。
  • goroutine进入死循环中,导致资源一直无法释放。

select底层也是channel实现的,如果所有case上的操作阻塞,select内部的channel便会阻塞,goroutine也无法继续执行。所以我们使用channel时一定要格外小心。

通过分析上面两幅图的情况,可以判断是因为select在所有case上死锁了,再深入代码分析,是因为项目中的的语音模型并发能力弱,在语音发包速度快起来的时候无法处理,导致select不满足条件,导致goroutine泄漏,应该在for循环之外加一个asr←nil,保证func2的select一定会满足,同时提高模型的并发能力,使得func1的不会阻塞。
防止goroutine泄漏的建议:

  • 创建goroutine时就要想好该goroutine该如何结束。
  • 使用channel时,要考虑到 channel阻塞时协程可能的行为,是否会创建大量的goroutine。
  • goroutine中不可以存在死循环。

案例二:内存泄漏

我们通过grafana发现内存出现泄漏:

go pprof가 뭐야?

这一次我们不使用命令行,而是使用图形化界面来定位问题。
输入 go tool pprof -http=:1234 localhost:8080/debug/pprof/heap:

2go pprof가 뭐야?

发现内存占用很有可能是byte.makeSlice()导致的,火焰图看的更加清晰:

2go pprof가 뭐야?

而调用byte.makeSlice()的函数为标准库中的ioutil.ReadAll(),接下来我们只需要研究这个标准库函数的实现即可。

func readAll(r io.Reader, capacity int64) (b []byte, err error) {
	buf := bytes.NewBuffer(make([]byte, 0, capacity))
	defer func() {
		e := recover()
		if e == nil {
			return
		}
		if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
			err = panicErr
		} else {
			panic(e)
		}
	}()
	_, err = buf.ReadFrom(r)
	return buf.Bytes(), err
}

// bytes.MinRead = 512
func ReadAll(r io.Reader) ([]byte, error) {
	return readAll(r, bytes.MinRead)
}

可以看到 ioutil.ReadAll 每次都会分配初始化一个大小为 bytes.MinRead 的 buffer ,bytes.MinRead 在 Golang 里是一个常量,值为 512 。就是说每次调用 ioutil.ReadAll 都先会分配一块大小为 512 字节的内存。
接下来看一下ReadFrom函数的实现:

func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
	b.lastRead = opInvalid
	// If buffer is empty, reset to recover space.
	if b.off >= len(b.buf) {
		b.Truncate(0)
	}
	for {
		if free := cap(b.buf) - len(b.buf); free < MinRead {
			// not enough space at end
			newBuf := b.buf
			if b.off+free < MinRead {
				// not enough space using beginning of buffer;
				// double buffer capacity
				newBuf = makeSlice(2*cap(b.buf) + MinRead)
			}
			copy(newBuf, b.buf[b.off:])
			b.buf = newBuf[:len(b.buf)-b.off]
			b.off = 0
		}
		m, e := r.Read(b.buf[len(b.buf):cap(b.buf)])
		b.buf = b.buf[0 : len(b.buf)+m]
		n += int64(m)
		if e == io.EOF {
			break
		}
		if e != nil {
			return n, e
		}
	}
	return n, nil // err is EOF, so return nil explicitly
}

ReadFrom函数主要作用就是从 io.Reader 里读取的数据放入 buffer 中,如果 buffer 空间不够,就按照每次 2x + MinRead 的算法递增,这里 MinRead 的大小也是 512 Bytes 。
项目读取的音频文件一般很大,buffer不够用,会一直调用makeSlice扩容,消耗大量内存,但是仅仅这样,只是程序执行时消耗了比较多的内存,并未有内存泄露的情况,那服务器又是如何内存不足的呢?这就不得不扯到 Golang 的 GC 机制。

GC算法的触发时机

golang的GC算法为三色算法,按理说会回收临时变量,但是触发GC的时机导致了这个问题:

  • 已分配的 Heap 到达某个阈值,会触发 GC, 该阈值由上一次 GC 时的 HeapAlloc 和 GCPercent 共同决定
  • 每 2 分钟会触发一次强制的 GC,将未 mark 的对象释放,但并不还给 OS
  • 每 5 分钟会扫描一个 Heap, 对于一直没有被访问的 Heap,归还给 OS

ioutil.ReadAll会将全部的数据加载到内存,一个大请求会多次调用makeSlice 分配很多内存空间,并发的时候,会在很短时间内占用大量的系统内存,然后将 GC 阈值增加到一个很高的值,这个时候要 GC 就只有等 2 分钟一次的强制 GC。这样内存中的数据无法及时GC,同时阈值还在不停的升高,导致GC的效率越来越低,最终导致缓慢的内存泄漏。

解决方法

//req.AduioPack,err=ioutil.ReadAll(c.Resquest.Body)

buffer:=bytes.NewBuffer(make[]byte,0,6400)
_,err:=io.Copy(buffer,c.Resquest.Body)
temp:=buffer.Bytes()
req.AduioPack=temp

不是一次性把文件读入内存,而是申请固定的内存大小。

【관련 추천: Go 비디오 튜토리얼, 프로그래밍 교육

위 내용은 go pprof가 뭐야?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 csdn.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제