48 | 程序性能分析基础(上)

作为拾遗的部分,今天我们来讲讲与 Go 程序性能分析有关的基础知识。

Go 语言为程序开发者们提供了丰富的性能分析 API,和非常好用的标准工具。这些 API 主要存在于:

1、runtime/pprof;

2、net/http/pprof;

3、runtime/trace;

这三个代码包中。

另外,runtime代码包中还包含了一些更底层的 API。它们可以被用来收集或输出 Go 程序运行过程中的一些关键指标,并帮助我们生成相应的概要文件以供后续分析时使用。

至于标准工具,主要有go tool pprof和go tool trace这两个。它们可以解析概要文件中的信息,并以人类易读的方式把这些信息展示出来。

此外,go test命令也可以在程序测试完成后生成概要文件。如此一来,我们就可以很方便地使用前面那两个工具读取概要文件,并对被测程序的性能加以分析。这无疑会让程序性能测试的一手资料更加丰富,结果更加精确和可信。

在 Go 语言中,用于分析程序性能的概要文件有三种,分别是:CPU 概要文件(CPU Profile)、内存概要文件(Mem Profile)和阻塞概要文件(Block Profile)。

这些概要文件中包含的都是:在某一段时间内,对 Go 程序的相关指标进行多次采样后得到的概要信息。

对于 CPU 概要文件来说,其中的每一段独立的概要信息都记录着,在进行某一次采样的那个时刻,CPU 上正在执行的 Go 代码。

而对于内存概要文件,其中的每一段概要信息都记载着,在某个采样时刻,正在执行的 Go 代码以及堆内存的使用情况,这里包含已分配和已释放的字节数量和对象数量。至于阻塞概要文件,其中的每一段概要信息,都代表着 Go 程序中的一个 goroutine 阻塞事件。

注意,在默认情况下,这些概要文件中的信息并不是普通的文本,它们都是以二进制的形式展现的。如果你使用一个常规的文本编辑器查看它们的话,那么肯定会看到一堆“乱码”。

这时就可以显现出go tool pprof这个工具的作用了。我们可以通过它进入一个基于命令行的交互式界面,并对指定的概要文件进行查阅。就像下面这样:

$ go tool pprof cpuprofile.out
Type: cpu
Time: Nov 9, 2018 at 4:31pm (CST)
Duration: 7.96s, Total samples = 6.88s (86.38%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

关于这个工具的具体用法,我就不在这里赘述了。在进入这个工具的交互式界面之后,我们只要输入指令help并按下回车键,就可以看到很详细的帮助文档。

我们现在来说说怎样生成概要文件。

你可能会问,既然在概要文件中的信息不是普通的文本,那么它们到底是什么格式的呢?一个对广大的程序开发者而言,并不那么重要的事实是,它们是通过 protocol buffers 生成的二进制数据流,或者说字节流。

概括来讲,protocol buffers 是一种数据序列化协议,同时也是一个序列化工具。它可以把一个值,比如一个结构体或者一个字典,转换成一段字节流。

也可以反过来,把经过它生成的字节流反向转换为程序中的一个值。前者就被叫做序列化,而后者则被称为反序列化。

换句话说,protocol buffers 定义和实现了一种“可以让数据在结构形态和扁平形态之间互相转换”的方式。

Protocol buffers 的优势有不少。比如,它可以在序列化数据的同时对数据进行压缩,所以它生成的字节流,通常都要比相同数据的其他格式(例如 XML 和 JSON)占用的空间明显小很多。

又比如,它既能让我们自己去定义数据序列化和结构化的格式,也允许我们在保证向后兼容的前提下去更新这种格式。

正因为这些优势,Go 语言从 1.8 版本开始,把所有 profile 相关的信息生成工作都交给 protocol buffers 来做了。这也是我们在上述概要文件中,看不到普通文本的根本原因了。

Protocol buffers 的用途非常广泛,并且在诸如数据存储、数据传输等任务中有着很高的使用率。不过,关于它,我暂时就介绍到这里。你目前知道这些也就足够了。你并不用关心runtime/pprof包以及runtime包中的程序是如何序列化这些概要信息的。

继续回到怎样生成概要文件的话题,我们依然通过具体的问题来讲述。

我们今天的问题是:怎样让程序对 CPU 概要信息进行采样?

这道题的典型回答是这样的。

这需要用到runtime/pprof包中的 API。更具体地说,在我们想让程序开始对 CPU 概要信息进行采样的时候,需要调用这个代码包中的StartCPUProfile函数,而在停止采样的时候则需要调用该包中的StopCPUProfile函数。

问题解析

runtime/pprof.StartCPUProfile函数(以下简称StartCPUProfile函数)在被调用的时候,先会去设定 CPU 概要信息的采样频率,并会在单独的 goroutine 中进行 CPU 概要信息的收集和输出。

注意,StartCPUProfile函数设定的采样频率总是固定的,即:100赫兹。也就是说,每秒采样100次,或者说每10毫秒采样一次。

赫兹,也称 Hz,是从英文单词“Hertz”(一个英文姓氏)音译过来的一个中文词。它是 CPU 主频的基本单位。

CPU 的主频指的是,CPU 内核工作的时钟频率,也常被称为 CPU clock speed。这个时钟频率的倒数即为时钟周期(clock cycle),也就是一个 CPU 内核执行一条运算指令所需的时间,单位是秒。

例如,主频为1000Hz 的 CPU,它的单个内核执行一条运算指令所需的时间为0.001秒,即1毫秒。又例如,我们现在常用的3.2GHz 的多核 CPU,其单个内核在1个纳秒的时间里就可以至少执行三条运算指令。

StartCPUProfile函数设定的 CPU 概要信息采样频率,相对于现代的 CPU 主频来说是非常低的。这主要有两个方面的原因。

一方面,过高的采样频率会对 Go 程序的运行效率造成很明显的负面影响。因此,runtime包中SetCPUProfileRate函数在被调用的时候,会保证采样频率不超过1MHz(兆赫),也就是说,它只允许每1微秒最多采样一次。StartCPUProfile函数正是通过调用这个函数来设定 CPU 概要信息的采样频率的。

另一方面,经过大量的实验,Go 语言团队发现100Hz 是一个比较合适的设定。因为这样做既可以得到足够多、足够有用的概要信息,又不至于让程序的运行出现停滞。另外,操作系统对高频采样的处理能力也是有限的,一般情况下,超过500Hz 就很可能得不到及时的响应了。

在StartCPUProfile函数执行之后,一个新启用的 goroutine 将会负责执行 CPU 概要信息的收集和输出,直到runtime/pprof包中的StopCPUProfile函数被成功调用。

StopCPUProfile函数也会调用runtime.SetCPUProfileRate函数,并把参数值(也就是采样频率)设为0。这会让针对 CPU 概要信息的采样工作停止。

同时,它也会给负责收集 CPU 概要信息的代码一个“信号”,以告知收集工作也需要停止了。

在接到这样的“信号”之后,那部分程序将会把这段时间内收集到的所有 CPU 概要信息,全部写入到我们在调用StartCPUProfile函数的时候指定的写入器中。只有在上述操作全部完成之后,StopCPUProfile函数才会返回。

好了,经过这一番解释,你应该已经对 CPU 概要信息的采样工作有一定的认识了。你可以去看看 demo96.go 文件中的代码,并运行几次试试。这样会有助于你加深对这个问题的理解。

package main

import (
"errors"
"fmt"
"os"
"puzzlers/article37/common"
"puzzlers/article37/common/op"
"runtime/pprof"
) var (
profileName = "cpuprofile.out"
) func main() {
f, err := common.CreateFile("", profileName)
if err != nil {
fmt.Printf("CPU profile creation error: %v\n", err)
return
}
defer f.Close()
if err := startCPUProfile(f); err != nil {
fmt.Printf("CPU profile start error: %v\n", err)
return
}
if err = common.Execute(op.CPUProfile, 10); err != nil {
fmt.Printf("execute error: %v\n", err)
return
}
stopCPUProfile()
} func startCPUProfile(f *os.File) error {
if f == nil {
return errors.New("nil file")
}
return pprof.StartCPUProfile(f)
} func stopCPUProfile() {
pprof.StopCPUProfile()
}

总结

我们这两篇内容讲的是 Go 程序的性能分析,这其中的内容都是你从事这项任务必备的一些知识和技巧。

首先,我们需要知道,与程序性能分析有关的 API 主要存在于runtime、runtime/pprof和net/http/pprof这几个代码包中。它们可以帮助我们收集相应的性能概要信息,并把这些信息输出到我们指定的地方。

Go 语言的运行时系统会根据要求对程序的相关指标进行多次采样,并对采样的结果进行组织和整理,最后形成一份完整的性能分析报告。这份报告就是我们一直在说的概要信息的汇总。

一般情况下,我们会把概要信息输出到文件。根据概要信息的不同,概要文件的种类主要有三个,分别是:CPU 概要文件(CPU Profile)、内存概要文件(Mem Profile)和阻塞概要文件(Block Profile)。

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言实战与应用二十六)--学习笔记的更多相关文章

  1. Go语言核心36讲(Go语言实战与应用二十)--学习笔记

    42 | bufio包中的数据类型 (上) 今天,我们来讲另一个与 I/O 操作强相关的代码包bufio.bufio是"buffered I/O"的缩写.顾名思义,这个代码包中的程 ...

  2. Go语言核心36讲(Go语言实战与应用二)--学习笔记

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

  3. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

  4. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  5. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  6. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  7. Go语言核心36讲(Go语言实战与应用十二)--学习笔记

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

  8. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  9. Go语言核心36讲(Go语言实战与应用十八)--学习笔记

    40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...

  10. Go语言核心36讲(Go语言实战与应用二十二)--学习笔记

    44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...

随机推荐

  1. [atARC105F]Lights Out on Connected Graph

    记$G[S]$表示图$G$在点集$S$上的导出子图,即$G[S]=(S,{(x,y)|x,y\in S且(x,y)\in E})$ 定义$g(S)$为所有$E'$(满足$E'\subseteq G[S ...

  2. [loj3048]异或粽子

    先对其求出前缀异或和,然后$o(k)$次枚举,每次选择最大值,考虑如何维护可以全局开一个堆,维护出每一个点的最大值的最大值,那么相当于要在一个点中删去一个点再找到最大值将这些删去的点重新建成一颗tri ...

  3. 数字逻辑实践2->Verilog编写规范

    来源:数字逻辑与Verilog设计实验课讲解,个人做的笔记与整理. 00 规范的重要性 良好的编程风格有利于减少消耗的硬件资源,提高设计的工作频率 . 提高系统的可移植性和可维护性. 程序的格式化能体 ...

  4. 【HTML】基础

    HTML基础 2019-07-23  10:16:28  by冲冲 在线编辑HTML/CSS/JS效果,实时查看效果 https://c.runoob.com/front-end/61 1. 概念 ① ...

  5. 最难忘的一次bug:谢谢实习时候爱学习的自己

    前言 时间的车轮一直向前不停,试图在时光洪流中碾碎一些久远的记忆.虽然记忆中的人离我越来越远,但是故事却越来越深刻. 当在博客园看到这次的正文题目是"最难忘的bug",脑海里瞬间浮 ...

  6. Codeforces 997D - Cycles in product(换根 dp)

    Codeforces 题面传送门 & 洛谷题面传送门 一种换根 dp 的做法. 首先碰到这类题目,我们很明显不能真的把图 \(G\) 建出来,因此我们需要观察一下图 \(G\) 有哪些性质.很 ...

  7. 洛谷 P4707 - 重返现世(扩展 Min-Max 容斥+背包)

    题面传送门 首先看到这种求形如 \(E(\max(T))\) 的期望题,可以套路地想到 Min-Max 容斥 \(\max(S)=\sum\limits_{T\subseteq S}(-1)^{|T| ...

  8. R合并数据框有重复匹配时只保留第一行

    前言 合并数据框有重复匹配时通常会返回所有的匹配,如何只保留匹配的第一行呢?其实这个需求也很常见.如芯片探针ID和基因ID往往多对一,要合并ID对应矩阵和芯片表达矩阵时. 数据例子 data = da ...

  9. Flink 实践教程-进阶(2):复杂格式数据抽取

    作者:腾讯云流计算 Oceanus 团队 流计算 Oceanus 简介 流计算 Oceanus 是大数据产品生态体系的实时化分析利器,是基于 Apache Flink 构建的具备一站开发.无缝连接.亚 ...

  10. Freeswitch 安装爬坑记录1

    2 Freeswitch的安装 2.1 准备工作 服务器安装CentOS 因为是内部环境,可以关闭一些防火墙设置,保证不会因为网络限制而不能连接 关闭防火墙 查看防火墙 systemctl statu ...