golang pprof 监控系列—— cpu 占用率 统计原理

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

http 接口暴露的方式

我们启动了一个http服务来暴露各种性能指标信息。让我们再回到当时启动http服务看到的网页图。

image.png

当点击上图中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 = 100cpu.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 = trueruntime.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 itimervalit.it_interval.tv_sec = 0it.it_interval.set_usec(1000000 / hz)it.it_value = it.it_intervalsetitimer(_ITIMER_PROF, &it, nil)
....	

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

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

线程处理信号的时机

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

信号处理时机.jpg

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

内核发送信号的方式

内核向进程发信号的方式是对进程中的一个线程发送信号,而通过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阻塞的函数将不能被统计到,关于这点业内已经有开源库帮助解决,GitHub - felixge/fgprof: 🚀 fgprof is a sampling Go profiler that allows you to analyze On-CPU as well as Off-CPU (e.g. I/O) time together.,不过由于这个库进行采样时会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 = hzunlock(&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 int32var sevp sigeventsevp.notify = _SIGEV_THREAD_IDsevp.signo = _SIGPROFsevp.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工作时长就越长。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/25117.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Pytorch]卷积运算conv2d

文章目录 [Pytorch]卷积运算conv2d一.F.Conv2d二.nn.Conv2d三.nn.Conv2d的运算过程 [Pytorch]卷积运算conv2d 一.F.Conv2d torch.nn.functional.Conv2d()的详细参数&#xff1a; conv2d(input: Tensor, weight: Tensor, bias: Optional[Tensor]None, stride: Union[_int, _s…

常见Charles在Windows10抓包乱码问题

废话不多说 直接开整 最近反复安装证书还是乱码 网上各种百度还是不行 首先计算机查看安装好的证书 certmgr.msc 找到并删除掉 重新安装证书 具体解决方法&#xff1a; 第一步&#xff1a;点击 【工具栏–>Proxy–>SSL Proxying Settings…】 第二步&#xff1a;配置…

C++路线(全网20篇高赞文章总结)

为节省时间&#xff0c;可直接跳转到 --> &#x1f33c;干货 目录 &#x1f33c;前言 &#x1f33c;来源 &#x1f416;现状 &#x1f33c;干货 入门阶段 入门项目 学习顺序 &#x1f409;大二打算 &#x1f33c;前言 来源的20篇博客&#xff0c;视频中&#x…

XML约束和解析

文章目录 概述使用场景语法dtd约束Schema约束解析DOM4j&#xff08;重点&#xff09; 概述 可扩展的标记性语言 使用场景 以前: 传输数据的媒介。 例如&#xff1a;微服务架构中&#xff0c;可以用xml文件进行多语言之间的的联系。 现在: 做配置文件 现在作为传输数据的媒介…

【java】使用maven完成一个servlet项目

一、创建项目 创建一个maven项目 maven是一个管理java项目的工具&#xff0c;根据maven的pom.xml可以引入各种依赖&#xff0c;插件。 步骤 打开idea&#xff0c;点击新建项目 点击创建项目&#xff0c;项目创建就完成了 进入时会自动打开pom.xml文件。 pom是项目的配置文件…

css, resize 拖拉宽度

效果如下&#xff1a; 可直接复制预览查看属性值: 关键样式属性&#xff1a; resize: horizontal; overflow-x: auto; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content…

【一次调频】考虑储能电池参与一次调频技术经济模型的容量配置方法(Matlab代码实现)

目录 &#x1f4a5;1 概述 1.1 文献来源 1.2 储能电池参与一次调频的方法 1.3 流程图 &#x1f4da;2 运行结果 2.1 数据 2.2 文献结果 2.3 复现结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码、数据、文章讲解 &#x1f4a5;1 概述 1.1 文献来源 摘要&#xff1a;规…

【学习笔记】Java安全之反序列化

文章目录 反序列化方法的对比PHP的反序列化Java的反序列化Python反序列化 URLDNS链利用链分析触发DNS请求 CommonCollections1利用链利用TransformedMap构造POC利用LazyMap构造POCCommonsCollections6 利用链 最近在学习Phith0n师傅的知识星球的Java安全漫谈系列&#xff0c;随…

Django实现音乐网站 ⑶

使用Python Django框架制作一个音乐网站&#xff0c;在系列文章2的基础上继续开发&#xff0c; 本篇主要是后台单曲、专辑、首页轮播图表模块开发。 目录 后台单曲、专辑表模块开发 表结构设计 单曲表&#xff08;singe&#xff09;结构 专辑表&#xff08;album&#xff0…

K8s集群安全机制(RBAC)

RBAC 基于角色的访问控制

MacBook触控板窗口管理 Swish for Mac

Swish for Mac是一款用于通过手势来控制mac应用窗口的软件&#xff0c;你可以通过这款软件在触控板上进行手势控制&#xff0c;你可以在使用前预设好不同手势的功能&#xff0c;然后就能直接通过这些手势让窗口按照你想要的方式进行变动了 Swish 支持 Haptick Feedback 震动反…

是什么让科大讯飞1个月股价翻倍?

目录 1、讯飞星火认知大模型的特点 2、讯飞星火认知大模型有多强大&#xff1f; 2.1、语言理解方面 2.2、知识问答方面 2.3、逻辑推理方面 2.4、数学题解答方面 2.5、代码理解与编写方面 3、如何使用讯飞星火认知大模型&#xff1f; 4、总结 我关注科大讯飞源于2021年…

接口测试——认知(一)

目录 引言 环境准备 1. 为什么要进行接口测试 2. 什么是接口 3. 接口测试与功能测试的区别 引言 为什么要做接口自动化测试&#xff1f; 在当前互联网产品迭代频繁的背景下&#xff0c;回归测试的时间越来越少&#xff0c;很难在每个迭代都对所有功能做完整回归。 但接…

Java抽象类和接口【超详细】

文章目录 一、抽象类1.1 抽象类概念1.2 抽象类语法1.3 抽象类特性1.4 抽象类的作用 二、接口2.1 接口的概念2.2 语法规则2.3 接口使用2.4 接口特性2.5 实现多个接口2.6 接口间的继承2.7 接口使用实例2.8Clonable 接口和深拷贝2.9 抽象类和接口的区别 一、抽象类 1.1 抽象类概念…

bash的特性(二)IO重定向与管道

bash的I/O重定向及管道 一、概述 在shell中&#xff0c;最常使用的fd(file descriptor)有三个&#xff0c;标准输入&#xff0c;标准输出&#xff0c;错误输出。进程用文件描述符来管理打开的文件。 名称 文件描述符 标准输入&#xff08;stdin) 0 键盘&#xff0c;也可以…

(十一)大数据实战——hadoop高可用之HDFS手动模式高可用

前言 本节内容我们介绍一下hadoop在手动模式下如何实现HDFS的高可用&#xff0c;HDFS的高可用功能是通过配置多个 NameNodes(Active/Standby)实现在集群中对 NameNode 的热备来解决上述问题。如果出现故障&#xff0c;如机器崩溃或机器需要升级维护&#xff0c;这时可通过此种…

DP-GAN剩余代码

在前面计算完损失后&#xff0c;该进行更新&#xff1a; 1&#xff1a;netEMA是模型的生成器&#xff1a; 遍历生成器的state_dict&#xff0c;将每一个键对应的值乘以EMA_decay。 接着根据当前迭代步数计算num_upd&#xff0c;每1000,2500,10000代倍数就执行一次。 当num…

❤ npm不是内部或外部命令,也不是可运行的程序 或批处理文件

❤ npm不是内部或外部命令,也不是可运行的程序 或批处理文件 cmd或者终端用nvm 安装提示&#xff1a; npm不是内部或外部命令,也不是可运行的程序或批处理文件 原因&#xff08;一&#xff09; 提示这个问题&#xff0c;有可能是Node没有安装&#xff0c;也有可能是没有配置…

ardupilot 中坐标变换矩阵和坐标系变换矩阵区别

目录 文章目录 目录摘要1.坐标变换矩阵与坐标系变换矩阵摘要 本节主要记录ardupilot 中坐标变换矩阵和坐标系变换矩阵的区别,这里非常重要,特别是进行姿态误差计算时,如果理解错误,很难搞明白后面算法。 1.坐标变换矩阵与坐标系变换矩阵 坐标变换矩阵的本质含义:是可以把…

【elementui】解决el-select组件失去焦点blur事件每次获取的是上一次选中值的问题

目录 【问题描述】 【问题摘要】 【分析问题】 【完整Test代码】 【封装自定义指令】 ↑↑↑↑↑↑↑↑↑↑↑↑ 不想看解决问题过程的可点击上方【封装自定义指令】目录直接跳转获取结果即可~~~ 【问题描述】 一位朋友遇到这么一个开发场景&#xff1a;在表格里面嵌入el-…