Golang pprof监控之cpu占用率统计原理详解

目录
  • http 接口暴露的方式
  • 程序代码生成profile
  • cpu 统计原理分析
    • 线程处理信号的时机
    • 内核发送信号的方式
  • 采样数据的公平性
  • 总结

经过前面的几节对pprof的介绍,对pprof统计的原理算是掌握了七八十了,我们对memory,block,mutex,trace,goroutine,threadcreate这些维度的统计原理都进行了分析,但唯独还没有分析pprof 工具是如何统计cpu使用情况的,今天我们来分析下这部分。

http 接口暴露的方式

还记得 golang pprof监控系列(2) —— memory,block,mutex 使用 里我们启动了一个http服务来暴露各种性能指标信息。让我们再回到当时启动http服务看到的网页图。

当点击上图中profile链接时,便会下载一个关于cpu指标信息的二进制文件。这个二进制文件同样可以用go tool pprof 工具去分析,同样,关于go tool pprof的使用不是本文的重点,网上的资料也相当多,所以我略去了这部分。

紧接着,我们来快速看下如何用程序代码的方式生成cpu的profile文件。

程序代码生成profile

os.Remove("cpu.out")
	f, _ := os.Create("cpu.out")
	defer f.Close()
	pprof.StartCPUProfile(f)
	defer 	pprof.StopCPUProfile()
	// .... do other things

代码比较简单,pprof.StartCPUProfile 则开始统计 cpu使用情况,pprof.StopCPUProfile则停止统计cpu使用情况,将程序使用cpu的情况写入cpu.out文件。cpu.out文件我们则可以用go tool pprof去分析了。

好的,在快速的看完如何在程序中暴露cpu性能指标后,我们来看看golang是如何统计各个函数cpu使用情况的。接下来,正戏开始。

cpu 统计原理分析

首先要明白,我们究竟要统计的是什么内容?我们需要知道cpu的使用情况,换言之就是cpu的工作时间花在了哪些函数上,最后是不是就是看函数在cpu上的工作时长

那么函数的在cpu上工作时长应该如何去进行统计?

golang还是采用部分采样的方式,通过settimmer 系统调用设置了 发送SIGPROF 的定时器,当达到runtime.SetCPUProfileRate设置的周期间隔时,操作系统就会向进程发送SIGPROF 信号,默认情况下是100Mz(10毫秒)。

一旦设置了 发送SIGPROF信号的定时器,操作系统便会定期向进程发送SIGPROF信号。

设置定时器的代码便是在我们调用pprof.StartCPUProfile方法开启cpu信息采样的时候。代码如下,

// src/runtime/pprof/pprof.go:760
func StartCPUProfile(w io.Writer) error {
	const hz = 100
	cpu.Lock()
	defer cpu.Unlock()
	if cpu.done == nil {
		cpu.done = make(chan bool)
	}
	// Double-check.
	if cpu.profiling {
		return fmt.Errorf("cpu profiling already in use")
	}
	cpu.profiling = true
	runtime.SetCPUProfileRate(hz)
	go profileWriter(w)
	return nil
}

在倒数第三行的时候调用了设置采样的周期,并且紧接着profileWriter 就是用一个协程启动后去不断的读取cpu的采样数据写到文件里。而调用settimer的地方就是在runtime.SetCPUProfileRate里,runtime.SetCPUProfileRate最终会调用 setcpuprofilerate方法 ,setcpuprofilerate 又会去调用setProcessCPUProfiler方法设置settimer 定时器。

// src/runtime/signal_unix.go:269
func setProcessCPUProfiler(hz int32) {
  .....
		var it itimerval
		it.it_interval.tv_sec = 0
		it.it_interval.set_usec(1000000 / hz)
		it.it_value = it.it_interval
		setitimer(_ITIMER_PROF, &it, nil)
....	

经过上述步骤后,cpu的采样就真正开始了,之后就是定时器被触发送SIGPROF信号,进程接收到这个信号后,会对当前函数的调用堆栈进行记录,由于默认的采样周期设置的是100Mz,所以,你可以理解为每10ms,golang就会统计下当前正在运行的是哪个函数,在采样的这段时间内,哪个函数被统计的次数越多,是不是就能说明这个函数在这段时间内占用cpu的工作时长就越多了。

由于golang借助了linux的信号机制去进行cpu执行函数的采样,这里有必要额外介绍下linux 进程与信号相关的知识。首先来看下线程处理信号的时机在什么时候。

线程处理信号的时机

线程对信号的处理时机一般 是在由内核态返回到用户态之前,也就是说,当线程由于系统调用或者中断进入内核态后, 当系统调用结束或者中断处理完成后,在返回到用户态之前,操作系统会检查这个线程是不是有未处理的信号,如果有的话,那么会先切回到用户态让 线程会首先处理信号,信号处理完毕后 又返回内核态,内核此时才会将调用栈设置为中断或者系统调用时 用户进程中断的地方 ,然后切换到用户态后就继续在用户进程之前中断的地方继续执行程序逻辑了。由于进程几乎每时每刻都在进行诸如系统调用的工作,可以认为,信号的处理是几乎实时的。 如下是线程内核态与用户态切换的过程,正式信号处理检查的地方。整个过程可以用下面的示意图表示。

知道了信号是如何被线程处理的,还需要了解下,内核会如何发送信号给进程。

内核发送信号的方式

内核向进程发信号的方式是对进程中的一个线程发送信号,而通过settimmer 系统调用设置定时器 发送SIGPROF 信号的方式就是随机的对进程中的一个运行中线程去进行发送。而运行中线程接收到这个信号后,就调用自身的处理函数对这个信号去进行处理,对于SIGPROF 信号而言,则是将线程中断前的函数栈记录下来,用于后续分析函数占用cpu的工作时长。

由于只是随机的向一个运行中的线程发送SIGPROF 信号,这里涉及到了两个问题?

第一因为同一个进程中只有一个线程在进行采样,所以在随机选择运行线程发送SIGPROF信号时,要求选择线程时的公平性,不然可能会出现A,B两个线程,A线程接收到SIGPROF信号的次数远远大于B 线程接收SIGPROF信号的次数,这样对A线程进行采样的次数将会变多,影响了我们采样的结果。

而golang用settimmer 设置定时器发送SIGPROF 信号 的方式的确被证实在linux上存在线程选择公平性问题(但是mac os上没有这个问题) 关于这个问题的讨论在github上有记录,这是链接 这个问题已经在go1.18上得到了解决,解决方式我会在下面给出,我们先来看随机的向一个运行中的线程发送SIGPROF 信号 引发的第二个问题。

第二 因为是向一个运行中的线程去发送信号,所以我们只能统计到采样时间段内在cpu上运行的函数,而那些io阻塞的函数将不能被统计到,关于这点业内已经有开源库帮助解决,https://github.com/felixge/fgprof,不过由于这个库进行采样时会stop the world ,所以其作者强烈建议如果go协程数量比较多时,将go版本升级到1.19再使用。后续有机会再来探讨这个库的实现吧,我们先回到如何解决settimer函数在选择线程的公平性问题上。

采样数据的公平性

为了解决公平性问题,golang在settimer的系统调用的基础上增加了timer_create系统调用timer_create 可以单独的为每一个线程都创建定时器,这样每个运行线程都会采样到自己的函数堆栈了。所以在go1.18版本对pprof.StartCPUProfile内部创建定时器的代码进行了改造。刚才有提到pprof.StartCPUProfile 底层其实是调用setcpuprofilerate 这个方法去设置的定时器,所以我们来看看go1.18和go1.17版本在这个方法的实现上主要是哪里不同。

// go1.17 版本 src/runtime/proc.go:4563
func setcpuprofilerate(hz int32) {
	if hz < 0 {
		hz = 0
	}
	_g_ := getg()
	_g_.m.locks++
	setThreadCPUProfiler(0)
	for !atomic.Cas(&prof.signalLock, 0, 1) {
		osyield()
	}
	if prof.hz != hz {
	   // 设置进程维度的 SIGPROF 信号发送器
		setProcessCPUProfiler(hz)
		prof.hz = hz
	}
	atomic.Store(&prof.signalLock, 0)
	lock(&sched.lock)
	sched.profilehz = hz
	unlock(&sched.lock)
	if hz != 0 {
	   // 设置线程维度的SIGPROF 信号定时器
		setThreadCPUProfiler(hz)
	}
	_g_.m.locks--
}

上述是go1.17版本的setcpuprofilerate 代码,如果你再去看 go1.18版本的代码,会发现他们在这个方法上是一模一样的,都是调用了setProcessCPUProfiler 和setThreadCPUProfiler,setProcessCPUProfiler 设置进程维度的发送SIGPROF信号定时器,setThreadCPUProfiler设置线程维度的发送SIGPROF信号的定时器,但其实setThreadCPUProfiler 在go1.17的实现上并不完整。

// go 1.17  src/runtime/signal_unix.go:314
func setThreadCPUProfiler(hz int32) {
	getg().m.profilehz = hz
}

go1.17版本上仅仅是为协程里代表线程的m变量设置了一个profilehz(采样的频率),并没有真正实现线程维度的采样。

// go 1.18 src/runtime/os_linux.go:605
....
// setThreadCPUProfiler 方法内部 timer_create的代码段
var timerid int32
	var sevp sigevent
	sevp.notify = _SIGEV_THREAD_ID
	sevp.signo = _SIGPROF
	sevp.sigev_notify_thread_id = int32(mp.procid)
	ret := timer_create(_CLOCK_THREAD_CPUTIME_ID, &sevp, &timerid)
	if ret != 0 {
		return
	}
	....

在go1.18版本上的setThreadCPUProfiler则真正实现了这部分逻辑,由于go1.18版本它同时调用了setProcessCPUProfiler以及setThreadCPUProfiler,这样在接收SIGPROF信号时就会出现重复计数的问题。

所以go1.18在处理SIGPROF信号的时候也做了去重处理,所以在golang信号处理的方法sighandler 内部有这样一段逻辑。

func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
	_g_ := getg()
	c := &sigctxt{info, ctxt}

	if sig == _SIGPROF {
		mp := _g_.m
		// Some platforms (Linux) have per-thread timers, which we use in
		// combination with the process-wide timer. Avoid double-counting.
		if validSIGPROF(mp, c) {
			sigprof(c.sigpc(), c.sigsp(), c.siglr(), gp, mp)
		}
		return
	}
	.....

如果发现信号是_SIGPROF 那么会通过validSIGPROF 去检测此次的_SIGPROF信号是否应该被统计。validSIGPROF的检测逻辑这里就不展开了。

总结

cpu的统计原理与前面所讲的指标统计的原理稍微复杂点,涉及到了linux信号处理相关的内容,cpu统计的原理,简而言之,就是通过设置一个发送SIGPROF信号的定时器,然后用户程序通过接收操作系统定时发送的SIGPROF信号来对用户程序正在执行的堆栈函数进行统计。在采样时间内,同一个函数被统计的越多,说明该函数占用的cpu工作时长就越长。

到此这篇关于Golang pprof监控之cpu占用率统计原理详解的文章就介绍到这了,更多相关Golang pprof cpu占用率统计内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang pprof监控memory block mutex使用指南

    目录 profile trace 网页显示 如何使用 http 接口暴露的方式 allocs ,heap block mutex 代码生成profile文件的方式 总结 profile profile的中文被翻译轮廓,对于计算机程序而言,抛开业务逻辑不谈,它的轮廓是是啥呢?不就是cpu,内存,各种阻塞开销,线程,协程概况 这些运行指标或环境.golang语言自带了工具库来帮助我们描述,探测,分析这些指标或者环境信息,让我们来学习它. 在上一篇golang pprof 监控系列(1) —— go

  • golang pprof监控memory block mutex统计原理分析

    目录 引言 bucket结构体介绍 记录指标细节介绍 memory block mutex 总结 引言 在上一篇文章 golang pprof监控系列(2) —— memory,block,mutex 使用里我讲解了这3种性能指标如何在程序中暴露以及各自监控的范围.也有提到memory,block,mutex 把这3类数据放在一起讲,是因为他们统计的原理是很类似的.今天来看看它们究竟是如何统计的. 先说下结论,这3种类型在runtime内部都是通过一个叫做bucket的结构体做的统计,bucke

  • golang pprof 监控goroutine thread统计原理详解

    目录 引言 http 接口暴露的方式 goroutine profile 输出信息介绍 threadcreate 输出信息介绍 程序代码暴露指标信息 统计原理介绍 goroutine fetch 函数实现 threadcreate fetch 函数实现 总结 引言 在之前 golang pprof监控 系列文章里我分别介绍了go trace以及go pprof工具对memory,block,mutex这些维度的统计原理,今天我们接着来介绍golang pprof工具对于goroutine 和th

  • golang pprof 监控系列 go trace统计原理与使用解析

    目录 引言 go trace 使用 统计原理介绍 Goroutine analysis Execution Network wait Sync block,Blocking syscall,Scheduler wait 各种profile 图 引言 服务监控系列文章 服务监控系列视频 关于go tool trace的使用,网上有相当多的资料,但拿我之前初学golang的经验来讲,很多资料都没有把go tool trace中的相关指标究竟是统计的哪些方法,统计了哪段区间讲解清楚.所以这篇文章不仅仅

  • Golang pprof性能测试与分析讲解

    目录 一.性能分析类型 1.CPU性能分析 2.内存性能分析 3.阻塞性能分析 二.cpu性能分析 1.生成pporf 2.分析数据 三.内存性能分析 四.benchmark 生成 profile 一.性能分析类型 1.CPU性能分析 CPU性能分析是最常见的性能分析类型.启动CPU分析时,运行时每隔10ms中断一次,采集正在运行协程的堆栈信息. 程序运行结束后,可以根据收集的数据,找到最热代码路径. 一个函数在分析阶段出现的次数越多,则该函数的代码路径(code path)花费的时间占总运行时

  • golang利用pprof与go-torch如何做性能分析

    前言 软件开发过程中,项目上线并不是终点.上线后,还要对程序的取样分析运行情况,并重构现有的功能,让程序执行更高效更稳写. golang的工具包内自带pprof功能,使找出程序中占内存和CPU较多的部分功能方便了不少.加上uber的火焰图,可视化显示,让我们在分析程序时更简单明了. pprof有两个包用来分析程序一个是net/http/pprof另一个是runtime/pprof,net/http/pprof只是对runtime/pprof包进行封装并用http暴露出来,如下图源码所示: 使用n

  • Golang pprof监控之cpu占用率统计原理详解

    目录 http 接口暴露的方式 程序代码生成profile cpu 统计原理分析 线程处理信号的时机 内核发送信号的方式 采样数据的公平性 总结 经过前面的几节对pprof的介绍,对pprof统计的原理算是掌握了七八十了,我们对memory,block,mutex,trace,goroutine,threadcreate这些维度的统计原理都进行了分析,但唯独还没有分析pprof 工具是如何统计cpu使用情况的,今天我们来分析下这部分. http 接口暴露的方式 还记得 golang pprof监

  • 解决开机时svchost.exe的CPU占用率过高导致系统异常缓慢

    网上有很多关于开机时SVCHOST.exe的CPU占用率过高问题的文章,基本上都说出了绝大部分用户的情况并给出解决方案,我引用一篇网上随便搜索的关于这SVCHOST的文章给大家看看,如果大家也有类似的问题可参考一下. 可是,今天X-Force遇到的问题,似乎文中提到的这些都没关系...因为经我的验证和排除,发现问题均与文章中提到的无关.X-Force被逼着再去查找真凶--经一番推理之后,终于找出了元凶就是-- Windows Upadate(Windows自动更新)!只要在我将Windows U

  • CPU占用率高的N种原因

    1.防杀毒软件造成故障 由于新版的KV.金山.瑞星都加入了对网页.插件.邮件的随机监控,无疑增大了系统负担.处理方式:基本上没有合理的处理方式,尽量使用最少的监控服务吧,或者,升级你的硬件配备. 2.驱动没有经过认证,造成CPU资源占用100% 大量的测试版的驱动在网上泛滥,造成了难以发现的故障原因. 处理方式:尤其是显卡驱动特别要注意,建议使用微软认证的或由官方发布的驱动,并且严格核对型号.版本. 3.病毒.木马造成 大量的蠕虫病毒在系统内部迅速复制,造成CPU占用资源率据高不下.解决办法:用

  • 解决正则表示式匹配($regex)引起的一次mongo数据库cpu占用率高的问题

    某一天,监控到mongo数据库cpu使用率高了很多,查了一下,发现是下面这种语句引起的: db.example_collection.find({ "idField" : { "$regex" : "123456789012345678" } , "dateField" : { "$regex" : "2019/10/10" }}) 通常,遇到这种情况,我第一反应是缺少相关字段的索引,导

  • C#程序优化-有效减少CPU占用率

    最近开发的项目中,由于会用到比较耗费CPU资源的第三方程序ffmpeg来处理视频.所以在网上找了一下,如何解决这种问题. 于是乎,就得到一个结论,减少CPU占用率,可以通过减少使用的CPU数量,在Window系统下,打开一个exe程序,系统会默认使用所有CPU作为处理. 是不是减少CPU使用数量,就可以减少CPU占用率呢,答案是肯定的. 参考代码:这里使用calc作为例子. Process p = new Process(); p.StartInfo.FileName = @"c:\window

  • 详解Node.js 应用高 CPU 占用率分析方法

    目录 本地运行 Node.js 应用 如何采集生产系统上的 Node.js 应用性能数据 本地运行 Node.js 应用 我们在本地运行 Node.js 应用,使用 --inspect 标志启动应用程序,再次执行负载测试,在 Chrome 浏览器中打开 chrome://inspect: 单击应用下方的 inspect 按钮,然后开始 CPU 占用率分析: 等待一段时间后,就能看到 CPU profile 的结果: 如何采集生产系统上的 Node.js 应用性能数据 在大多数情况下,如果性能问题

  • golang 一次性定时器Timer用法及实现原理详解

    目录 前言 Timer timer结构体 创建定时器 停止定时器 重置定时器 实现原理 数据结构 runtimeTimer 创建Timer 停止Timer 重置Timer 前言 定时器在Go语言应用中使用非常广泛,Go语言的标准库里提供两种类型的计时器,一种是一次性的定时器Timer,另外一种是周期性的定时器Ticker.本文主要来看一下Timer的用法和实现原理,需要的朋友可以参考以下内容,希望对大家有帮助. Timer Timer是一种单一事件的定时器,即经过指定的时间后触发一个事件,因为T

  • Python实现监控远程主机实时数据的示例详解

    目录 0 简述 1 程序说明文档 1.1 服务端 1.2 客户端 2 代码 0 简述 实时监控应用程序,使用Python的Socket库和相应的第三方库来监控远程主机的实时数据,比如CPU使用率.内存使用率.网络带宽等信息.可以允许多个用户同时访问服务端.注:部分指令响应较慢,请耐心等待. 1 程序说明文档 1.1 服务端 本程序为一个基于TCP协议的服务端程序,可以接收客户端发送的指令并执行相应的操作,最终将操作结果返回给客户端.程序运行在localhost(即本机)的8888端口. 主要功能

随机推荐