golang大杀器GMP模型

Posted 每天都要加油呀!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang大杀器GMP模型相关的知识,希望对你有一定的参考价值。

golang 大杀器——GMP模型

文章目录

视频地址:Golang深入理解GPM模型

思维导图:

1. 发展过程

思维导图:

在单机时代是没有多线程、多进程、协程这些概念的。早期的操作系统都是顺序执行

单进程的缺点有

  • 单一执行流程、计算机只能一个任务一个任务进行处理
  • 进程阻塞所带来的CPU时间的浪费

处于对CPU资源的利用,发展出多线程/多进程操作系统,采用时间片轮训算法

宏观上来说,就算只有一个cpu,也能并发执行多个进程

这样的好处是充分利用了CPU,但是也带来了一些问题,例如时间片切换需要花费额外的开销

  • 进程/线程的数量越多,切换成本就越大,也就越浪费

对于开发人员来说,尽管线程看起来很美好,但实际上多线程开发设计会变得更加复杂,要考虑很多同步竞争等问题,如竞争冲突

进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU虽然利用起来了,但如果进程过多,CPU有很大的一部分都被用来进行进程调度

所以提高cpu的利用率成为我们需要解决的问题

既然问题出现在线程上下文切换中,那么首先我们需要好好想一想什么是线程的上下文切换

我们知道操作系统的一些核心接口是不能被进程随意调度的,例如进行io流的读写操作,需要将最终的执行权交给操作系统(内核态)进行调度,所以就会有用户态和内核态之前的切换

这个时候我们的线程模型是这样的

一个线程需要在内核态用户态之间进行切换,并且切换是受到操作系统控制的,可能这个现在需要等待多个时间片才能切换到内核态再调用操作系统底层的接口

那么我们是否可以用两个线程分别处理这两种状态呢?两个线程之间再做好绑定,当用户线程将任务提交给内核线程后,就可以不用堵塞了,可以去执行其他的任务了

对于CPU来说(多核CPU),不需要关注线程切换的问题,只需要分配系统资源给内核线程进行调度即可

我们来给用户线程换个名字——协程(co-runtine)

如果是一比一的关系的话,其实还是可能需要等待内核线程的执行

所以可以设计为N 比 1的形式,多个协程可以将任务一股脑的交给内核线程去完成,但是这样又有问题,如果其中一个问题在提交任务的过程中,堵塞住了,就会影响其他线程的工作

这个就是python的event-loop遇到的问题,一个阻塞,其余全阻塞

所以一般为M 比 N的关系

M 比 N的关系中,大部分的精力都会放在协程调度器上,如果调度器效率高就能让协程之间阻塞时间尽可能的少

在golang中对协程调度器协程内存进行了优化

  • 协程调度器:可以支持灵活调度
  • 内存轻量化:可以拥有大量的协程

在golang早期的协程调度器中,采用的是队列的方式,M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的

老调度器有几个缺点:

  1. 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争
  2. M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G',为了继续执行G,需要把G'交给M'执行,也造成了很差的局部性,因为G'G是相关的,最好放在M上执行,而不是其他M'
  3. 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销

2. GMP模型设计思想

思维导图:

2.1 GMP模型

GMP是goalng的线程模型,包含三个概念:内核线程(M),goroutine(G),G的上下文环境(P)

  • G:goroutine协程,基于协程建立的用户态线程
  • M:machine,它直接关联一个os内核线程,用于执行G
  • P:processor处理器,P里面一般会存当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度

在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上

  1. 全局队列(Global Queue):存放等待运行的G
  2. P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列
  3. P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个
  4. M:线程想运行任务就得获取P,从P的本地队列获取GP队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去

P和M的数量问题

  • P的数量:环境变量$GOMAXPROCS;在程序中通过runtime.GOMAXPROCS()来设置
  • M的数量:GO语言本身限定一万 (但是操作系统达不到);通过runtime/debug包中的SetMaxThreads函数来设置;有一个M阻塞,会创建一个新的M;如果有M空闲,那么就会回收或者休眠

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来

2.2 调度器的设计策略

golang调度器的设计策略思想主要有以下几点:

  • 复用线程
  • 利用并行
  • 抢占
  • 全局G队列

2.2.1 复用线程

golang在复用线程上主要体现在work stealing机制hand off机制(偷别人的去执行,和自己扔掉执行)

首先我们看work stealing,我们在学习java的时候学过fork/join,其中也是通过工作窃取方式来提升效率,充分利用线程进行并行计算,并减少了线程间的竞争

干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行

hand off机制

当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行,此时M1如果长时间阻塞,可能会执行睡眠或销毁

2.2.2 利用并行

我们可以使用GOMAXPROCS设置P的数量,这样的话最多有GOMAXPROCS个线程分布在多个CPU上同时运行。GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行


2.2.3 抢占策略

  • 1对1模型的调度器,需要等待一个co-routine主动释放后才能轮到下一个进行使用
  • golang中,如果一个goroutine使用10ms还没执行完,CPU资源就会被其他goroutine所抢占


2.2.4 全局G队列

  • 全局G队列其实是复用线程的补充,当工作窃取时,优先从全局队列去取,取不到才从别的p本地队列取(1.17版本)

  • 在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G

2.3 go func()经历了那些过程

  1. 我们通过go func()来创建一个goroutine
  2. 有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列
  3. G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行
  4. 一个M调度G执行的过程是一个循环机制
  5. 当M执行某一个G时候如果发生了syscall或者其他阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P
  6. 当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列

2.4 调度器的生命周期

在了解调度器生命周期之前,我们需要了解两个新的角色M0G0

M0(跟进程数量绑定,一比一):

  • 启动程序后编号为0的主线程
  • 在全局变量runtime.m0中,不需要在heap上分配
  • 负责执行初始化操作和启动第一个G
  • 启动第一个G之后,M0就和其他的M一样了

G0(每个M都会有一个G0):

  • 每次启动一个M,都会第一个创建的gourtine,就是G0
  • G0仅用于负责调度G
  • G0不指向任何可执行的函数
  • 每个M都会有一个自己的G0
  • 在调度或系统调用时会使用M切换到G0,再通过G0进行调度

M0和G0都是放在全局空间的

具体流程为:

我们来分析一段代码:

package main

import "fmt"

func main() 
    fmt.Println("Hello world")

  1. runtime创建最初的线程m0和goroutine g0,并把2者关联。
  2. 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCSP构成的P列表
  3. 示例代码中的main函数是main.mainruntime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。
  4. 启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。
  5. G拥有栈,M根据G中的栈信息和调度信息设置运行环境
  6. M运行G
  7. G退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。

调度器的生命周期几乎占满了一个Go程序的一生,runtime.main的goroutine执行之前都是为调度器做准备工作,runtime.main的goroutine运行,才是调度器的真正开始,直到runtime.main结束而结束

2.5 可视化的CMP编程

2.5.1 trace方式

在这里我们需要使用trace编程,三步走:

  1. 创建trace文件:f, err := os.Create(“trace.out”)
  2. 启动trace:err = trace.Start(f)
  3. 停止trace:trace.Stop()

然后再通过go tool trace工具打开trace文件

go tool trace trace.out
package main

import (
	"fmt"
	"os"
	"runtime/trace"
)

// trace的编码过程
// 1. 创建文件
// 2. 启动
// 3. 停止
func main() 
	// 1.创建一个trace文件
	f, err := os.Create("trace.out")
	if err != nil 
		panic(err)
	
	defer func(f *os.File) 
		err := f.Close()
		if err != nil 
			panic(err)
		
	(f)
	// 2. 启动trace
	err = trace.Start(f)
	if err != nil 
		panic(err)
	
	// 正常要调试的业务
	fmt.Println("hello GMP")
	// 3. 停止trace
	trace.Stop()


打开后我们进入网页点击view trace,然后就能看到分析信息

G的信息

M的信息

P的信息

2.5.2 debug方式

使用debug方式可以不需要trace文件

先搞一段代码

package main

import (
	"fmt"
	"time"
)

func main() 
	for i := 0; i < 5; i++ 
		time.Sleep(time.Second)
		fmt.Println("hello GMP")
	

 

debug执行一下

$ GODEBUG=schedtrace=1000 ./debug.exe
SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 1008ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 2009ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 3010ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
hello GMP
SCHED 4017ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
  • SCHED:调试信息输出标志字符串,代表本行是goroutine调度器的输出;
  • 0ms:即从程序启动到输出这行日志的时间;
  • gomaxprocs: P的数量,本例有2个P, 因为默认的P的属性是和cpu核心数量默认一致,当然也可以通过GOMAXPROCS来设置;
  • idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;
  • threads: os threads/M的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;
  • spinningthreads: 处于自旋状态的os thread数量;
  • idlethread: 处于idle状态的os thread的数量;
  • runqueue=0: Scheduler全局队列中G的数量;
  • [0 0]: 分别为2个P的local queue中的G的数量。

3. GMP场景分析

3.1 G1创建G3

P拥有G1,M1获取P后开始运行G1,G1使用go func()创建了G2,为了局部性G2优先加入到P1的本地队列

3.2 G1执行完毕

G1运行完成后(函数:goexit),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数:schedule)。从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:execute)。实现了线程M1的复用

3.3 G溢出

假设每个P的本地队列只能存3个G。G2要创建了6个G,前3个G(G3, G4, G5)已经加入p1的本地队列,p1本地队列满了

G2在创建G7的时候,发现P1的本地队列已满,需要执行负载均衡(把P1中本地队列中前一半的G,还有新创建G转移到全局队列)

  • 移走前一半的G是为了防止后面G饥饿

这些G被转移到全局队列时,会被打乱顺序。所以G3,G4,G7被转移到全局队列。

G2创建G8时,P1的本地队列未满,所以G8会被加入到P1的本地队列

3.4 唤醒正在休眠的M

规定:在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行

假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为自旋线程(没有G但为运行状态的线程,不断寻找G)

  • 先从全局队列中获取,没有再从其他线程中偷取(先全后偷

3.5 自旋线程获取G

M2尝试从全局队列(简称“GQ”)取一批G放到P2的本地队列(函数:findrunnable())。M2从全局队列取的G数量符合下面的公式:

n =  min(len(GQ) / GOMAXPROCS +  1,  cap(LQ) / 2 )
  • GQ:全局队列总长度(队列中现在元素的个数)
  • GOMAXPROCS:p的个数
  • 至少从全局队列取1个g,但每次不要从全局队列移动太多的g到p本地队列,给其他p留点。这是从全局队列到P本地队列的负载均衡

  • 假定我们场景中一共有4个P(GOMAXPROCS设置为4,那么我们允许最多就能用4个P来供M使用)。所以M2只从能从全局队列取1个G(即G3)移动P2本地队列,然后完成从G0到G3的切换,运行G3

  • 当M2有了新的G(不再是G0),便不是自旋线程

相关源码参考:

// 从全局队列中偷取,调用时必须锁住调度器
func globrunqget(_p_ *p, max int32) *g 
	// 如果全局队列中没有 g 直接返回
	if sched.runqsize == 0 
		return nil
	

	// per-P 的部分,如果只有一个 P 的全部取
	n := sched.runqsize/gomaxprocs + 1
	if n > sched.runqsize 
		n = sched.runqsize
	

	// 不能超过取的最大个数
	if max > 0 && n > max 
		n = max
	

	// 计算能不能在本地队列中放下 n 个
	if n > int32(len(_p_.runq))/2 
		n = int32(len(_p_.runq)) / 2
	

	// 修改本地队列的剩余空间
	sched.runqsize -= n
	// 拿到全局队列队头 g
	gp := sched.runq.pop()
	// 计数
	n--

	// 继续取剩下的 n-1 个全局队列放入本地队列
	for ; n > 0; n-- 
		gp1 := sched.runq.pop()
		runqput(_p_, gp1, false)
	
	return gp

3.6 M2从M1中偷取G

全局队列已经没有G,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G,本例中一半则只有1个G8,放到P2的本地队列并执行。

  • 偷取队列元素的一半

3.7 自旋线程的最大限制

G1本地队列G5、G6已经被其他M偷走并运行完成,当前M1和M2分别在运行G2和G8,M3和M4没有goroutine可以运行,M3和M4处于自旋状态,它们不断寻找goroutine

  • 正在运行的M + 自旋线程 <= GOMAXPROCS
  • 如果M大于P,则进入休眠线程队列

  • 为什么要让m3和m4自旋,自旋本质是在运行,线程在运行却没有执行G,就变成了浪费CPU
  • 为什么不销毁现场,来节约CPU资源。因为创建和销毁CPU也会浪费时间,我们希望当有新goroutine创建时,立刻能有M运行它,如果销毁再新建就增加了时延,降低了效率
  • 当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有GOMAXPROCS个自旋的线程(当前例子中的GOMAXPROCS=4,所以一共4个P),多余的没事做线程会让他们休眠。

3.8 G发送系统调用/阻塞

假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程(没有得到P的绑定,注意我们这里最多就只能够存在4个P,所以P的数量应该永远是M>=P,大部分都是M在抢占需要运行的P),G8创建了G9,G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行以下判断:如果P2本地队列有G、全局队列有G或有空闲的M,P2都会立马唤醒1个M和它绑定,否则P2则会加入到空闲P列表,等待M来获取可用的p。本场景中,P2本地队列有G9,可以和其他空闲的线程M5绑定

  • 自旋线程抢占G,不抢占P

3.9 G发送系统调用/非阻塞

上述G8如果执行完毕,此时M2会首先寻找之前的P,如果没有则尝试从空闲p队列中获取,如果没获取不到,会进入M阻塞队列中(长时间休眠等待GC回收销毁)

Golang 大杀器之性能剖析 PProf

Golang 大杀器之性能剖析 PProf

前言

写了几吨代码,实现了几百个接口。功能测试也通过了,终于成功的部署上线了

结果,性能不佳,什么鬼? Golang 大杀器之性能剖析 PProf

想做性能分析

PProf

想要进行性能优化,首先瞩目在 Go 自身提供的工具链来作为分析依据,本文将带你学习、使用 Go 后花园,涉及如下:

  • runtime/pprof:采集程序(非 Server)的运行数据进行分析

  • net/http/pprof:采集 HTTP Server 的运行时数据进行分析

是什么

pprof 是用于可视化和分析性能分析数据的工具

pprof 以 profile.proto 读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)

profile.proto 是一个 Protocol Buffer v3 的描述文件,它描述了一组 callstack 和 symbolization 信息, 作用是表示统计分析的一组采样的调用栈,是很常见的 stacktrace 配置文件格式

支持什么使用模式

  • Report generation:报告生成

  • Interactive terminal use:交互式终端使用

  • Web interface:Web 界面

可以做什么

  • CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置

  • Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏

  • Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置

  • Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况

一个简单的例子

我们将编写一个简单且有点问题的例子,用于基本的程序初步分析

编写 demo 文件

(1)demo.go,文件内容:

package mainimport (	"log"
	"net/http"
	_ "net/http/pprof"
	"github.com/EDDYCJY/go-pprof-example/data")func main() {	go func() {		for {
			log.Println(data.Add("https://github.com/EDDYCJY"))
		}
	}()

	http.ListenAndServe("0.0.0.0:6060", nil)
}

(2)data/d.go,文件内容:

package datavar datas []stringfunc Add(str string) string {	data := []byte(str)	sData := string(data)
	datas = append(datas, sData)	return sData
}

运行这个文件,你的 HTTP 服务会多出 /debug/pprof 的 endpoint 可用于观察应用程序的情况

分析

一、通过 Web 界面

查看当前总览:访问 http://127.0.0.1:6060/debug/pprof/

/debug/pprof/

profiles:
0	block
5	goroutine
3	heap
0	mutex
9	threadcreate

full goroutine stack dump

这个页面中有许多子页面,咱们继续深究下去,看看可以得到什么?

  • cpu(CPU Profiling): $HOST/debug/pprof/profile,默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件

  • block(Block Profiling):$HOST/debug/pprof/block,查看导致阻塞同步的堆栈跟踪

  • goroutine:$HOST/debug/pprof/goroutine,查看当前所有运行的 goroutines 堆栈跟踪

  • heap(Memory Profiling): $HOST/debug/pprof/heap,查看活动对象的内存分配情况

  • mutex(Mutex Profiling):$HOST/debug/pprof/mutex,查看导致互斥锁的竞争持有者的堆栈跟踪

  • threadcreate:$HOST/debug/pprof/threadcreate,查看创建新OS线程的堆栈跟踪

二、通过交互式终端使用

(1)go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60

$ go tool pprof http://localhost:6060/debug/pprof/profile\?seconds\=60

Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=60
Saved profile in /Users/eddycjy/pprof/pprof.samples.cpu.007.pb.gz
Type: cpu
Duration: 1mins, Total samples = 26.55s (44.15%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

执行该命令后,需等待 60 秒(可调整 seconds 的值),pprof 会进行 CPU Profiling。结束后将默认进入 pprof 的交互式命令模式,可以对分析的结果进行查看或导出。具体可执行 pprof help 查看命令说明

(pprof) top10
Showing nodes accounting for 25.92s, 97.63% of 26.55s total
Dropped 85 nodes (cum <= 0.13s)
Showing top 10 nodes out of 21
      flat  flat%   sum%        cum   cum%
    23.28s 87.68% 87.68%     23.29s 87.72%  syscall.Syscall
     0.77s  2.90% 90.58%      0.77s  2.90%  runtime.memmove
     0.58s  2.18% 92.77%      0.58s  2.18%  runtime.freedefer
     0.53s  2.00% 94.76%      1.42s  5.35%  runtime.scanobject
     0.36s  1.36% 96.12%      0.39s  1.47%  runtime.heapBitsForObject
     0.35s  1.32% 97.44%      0.45s  1.69%  runtime.greyobject
     0.02s 0.075% 97.51%     24.96s 94.01%  main.main.func1
     0.01s 0.038% 97.55%     23.91s 90.06%  os.(*File).Write
     0.01s 0.038% 97.59%      0.19s  0.72%  runtime.mallocgc
     0.01s 0.038% 97.63%     23.30s 87.76%  syscall.Write
  • flat:给定函数上运行耗时

  • flat%:同上的 CPU 运行耗时总比例

  • sum%:给定函数累积使用 CPU 总比例

  • cum:当前函数加上它之上的调用运行总耗时

  • cum%:同上的 CPU 运行耗时总比例

最后一列为函数名称,在大多数的情况下,我们可以通过这五列得出一个应用程序的运行情况,加以优化  Golang 大杀器之性能剖析 PProf

(2)go tool pprof http://localhost:6060/debug/pprof/heap

$ go tool pprof http://localhost:6060/debug/pprof/heap
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /Users/eddycjy/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.008.pb.gz
Type: inuse_space
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 837.48MB, 100% of 837.48MB total
      flat  flat%   sum%        cum   cum%
  837.48MB   100%   100%   837.48MB   100%  main.main.func1
  • -inuse_space:分析应用程序的常驻内存占用情况

  • -alloc_objects:分析应用程序的内存临时分配情况

(3) go tool pprof http://localhost:6060/debug/pprof/block

(4) go tool pprof http://localhost:6060/debug/pprof/mutex

三、PProf 可视化界面

这是令人期待的一小节。在这之前,我们需要简单的编写好测试用例来跑一下

编写测试用例

(1)新建 data/d_test.go,文件内容:

package data

import "testing"

const url = "https://github.com/EDDYCJY"

func TestAdd(t *testing.T) {
	s := Add(url)
	if s == "" {
		t.Errorf("Test.Add error!")
	}
}

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Add(url)
	}
}

(2)执行测试用例

$ go test -bench=. -cpuprofile=cpu.prof
pkg: github.com/EDDYCJY/go-pprof-example/data
BenchmarkAdd-4   	10000000	       187 ns/op
PASS
ok  	github.com/EDDYCJY/go-pprof-example/data	2.300s

-memprofile 也可以了解一下

启动 PProf 可视化界面
方法一:
$ go tool pprof -http=:8080 cpu.prof
方法二:
$ go tool pprof cpu.prof 
$ (pprof) web

如果出现 Could not execute dot; may need to install graphviz.,就是提示你要安装 graphviz 了 (请右拐谷歌)

查看 PProf 可视化界面

(1)Top

Golang 大杀器之性能剖析 PProf

(2)Graph

Golang 大杀器之性能剖析 PProf

框越大,线越粗代表它占用的时间越大哦

(3)Peek

Golang 大杀器之性能剖析 PProf

(4)Source

Golang 大杀器之性能剖析 PProf

通过 PProf 的可视化界面,我们能够更方便、更直观的看到 Go 应用程序的调用链、使用情况等,并且在 View 菜单栏中,还支持如上多种方式的切换

你想想,在烦恼不知道什么问题的时候,能用这些辅助工具来检测问题,是不是瞬间效率翻倍了呢  Golang 大杀器之性能剖析 PProf

四、PProf 火焰图

另一种可视化数据的方法是火焰图,需手动安装原生 PProf 工具:

(1) 安装 PProf

$ go get -u github.com/google/pprof

(2) 启动 PProf 可视化界面:

$ pprof -http=:8080 cpu.prof

(3) 查看 PProf 可视化界面

打开 PProf 的可视化界面时,你会明显发现比官方工具链的 PProf 精致一些,并且多了 Flame Graph(火焰图)

它就是本次的目标之一,它的最大优点是动态的。调用顺序由上到下(A -> B -> C -> D),每一块代表一个函数,越大代表占用 CPU 的时间更长。同时它也支持点击块深入进行分析!

Golang 大杀器之性能剖析 PProf

总结

在本章节,粗略地介绍了 Go 的性能利器 PProf。在特定的场景中,PProf 给定位、剖析问题带了极大的帮助

希望本文对你有所帮助,另外建议能够自己实际操作一遍,最好是可以深入琢磨一下,内含大量的用法、知识点  Golang 大杀器之性能剖析 PProf

思考题

你很优秀的看到了最后,那么有两道简单的思考题,希望拓展你的思路

(1)flat 一定大于 cum 吗,为什么?什么场景下 cum 会比 flat 大?

(2)本章节的 demo 代码,有什么性能问题?怎么解决它?


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。



Golang语言社区

ID:Golangweb

www.bytedancing.com

游戏服务器架构丨分布式技术丨大数据丨游戏算法学习



以上是关于golang大杀器GMP模型的主要内容,如果未能解决你的问题,请参考以下文章

golang的线程模型——GMP模型

深入Golang调度器之GMP模型

Dropout大杀器已过时?视网络模型而定!

机器学习大杀器Stacking堆叠模型-English

百度飞桨登顶图神经网络权威榜单3项榜首,推出大杀器UniMP

Golang---GMP调度策略