49 | 程序性能分析基础(下)

在上一篇文章中,我们围绕着“怎样让程序对 CPU 概要信息进行采样”这一问题进行了探讨,今天,我们再来一起看看它的拓展问题。

知识扩展

问题 1:怎样设定内存概要信息的采样频率?

针对内存概要信息的采样会按照一定比例收集 Go 程序在运行期间的堆内存使用情况。设定内存概要信息采样频率的方法很简单,只要为runtime.MemProfileRate变量赋值即可。

这个变量的含义是,平均每分配多少个字节,就对堆内存的使用情况进行一次采样。如果把该变量的值设为0,那么,Go 语言运行时系统就会完全停止对内存概要信息的采样。该变量的缺省值是512 KB,也就是512千字节。

注意,如果你要设定这个采样频率,那么越早设定越好,并且只应该设定一次,否则就可能会对 Go 语言运行时系统的采样工作,造成不良影响。比如,只在main函数的开始处设定一次。

在这之后,当我们想获取内存概要信息的时候,还需要调用runtime/pprof包中的WriteHeapProfile函数。该函数会把收集好的内存概要信息,写到我们指定的写入器中。

注意,我们通过WriteHeapProfile函数得到的内存概要信息并不是实时的,它是一个快照,是在最近一次的内存垃圾收集工作完成时产生的。如果你想要实时的信息,那么可以调用runtime.ReadMemStats函数。不过要特别注意,该函数会引起 Go 语言调度器的短暂停顿。

以上,就是关于内存概要信息的采样频率设定问题的简要回答。

package main

import (
"048程序性能分析基础/common"
"048程序性能分析基础/common/op"
"errors"
"fmt"
"os"
"runtime"
"runtime/pprof"
) var (
profileName = "memprofile.out"
memProfileRate = 8
) func main() {
f, err := common.CreateFile("", profileName)
if err != nil {
fmt.Printf("memory profile creation error: %v\n", err)
return
}
defer f.Close()
startMemProfile()
if err = common.Execute(op.MemProfile, 10); err != nil {
fmt.Printf("execute error: %v\n", err)
return
}
if err := stopMemProfile(f); err != nil {
fmt.Printf("memory profile stop error: %v\n", err)
return
}
} func startMemProfile() {
runtime.MemProfileRate = memProfileRate
} func stopMemProfile(f *os.File) error {
if f == nil {
return errors.New("nil file")
}
return pprof.WriteHeapProfile(f)
}

问题 2:怎样获取到阻塞概要信息?

我们调用runtime包中的SetBlockProfileRate函数,即可对阻塞概要信息的采样频率进行设定。该函数有一个名叫rate的参数,它是int类型的。

这个参数的含义是,只要发现一个阻塞事件的持续时间达到了多少个纳秒,就可以对其进行采样。如果这个参数的值小于或等于0,那么就意味着 Go 语言运行时系统将会完全停止对阻塞概要信息的采样。

在runtime包中,还有一个名叫blockprofilerate的包级私有变量,它是uint64类型的。这个变量的含义是,只要发现一个阻塞事件的持续时间跨越了多少个 CPU 时钟周期,就可以对其进行采样。它的含义与我们刚刚提到的rate参数的含义非常相似,不是吗?

实际上,这两者的区别仅仅在于单位不同。runtime.SetBlockProfileRate函数会先对参数rate的值进行单位换算和必要的类型转换,然后,它会把换算结果用原子操作赋给blockprofilerate变量。由于此变量的缺省值是0,所以 Go 语言运行时系统在默认情况下并不会记录任何在程序中发生的阻塞事件。

另一方面,当我们需要获取阻塞概要信息的时候,需要先调用runtime/pprof包中的Lookup函数并传入参数值"block",从而得到一个*runtime/pprof.Profile类型的值(以下简称Profile值)。在这之后,我们还需要调用这个Profile值的WriteTo方法,以驱使它把概要信息写进我们指定的写入器中。

这个WriteTo方法有两个参数,一个参数就是我们刚刚提到的写入器,它是io.Writer类型的。而另一个参数则是代表了概要信息详细程度的int类型参数debug。

debug参数主要的可选值有两个,即:0和1。当debug的值为0时,通过WriteTo方法写进写入器的概要信息仅会包含go tool pprof工具所需的内存地址,这些内存地址会以十六进制的形式展现出来。

当该值为1时,相应的包名、函数名、源码文件路径、代码行号等信息就都会作为注释被加入进去。另外,debug为0时的概要信息,会经由 protocol buffers 转换为字节流。而在debug为1的时候,WriteTo方法输出的这些概要信息就是我们可以读懂的普通文本了。

除此之外,debug的值也可以是2。这时,被输出的概要信息也会是普通的文本,并且通常会包含更多的细节。至于这些细节都包含了哪些内容,那就要看我们调用runtime/pprof.Lookup函数的时候传入的是什么样的参数值了。下面,我们就来一起看一下这个函数。

package main

import (
"048程序性能分析基础/common"
"048程序性能分析基础/common/op"
"errors"
"fmt"
"os"
"runtime"
"runtime/pprof"
) var (
profileName = "blockprofile.out"
blockProfileRate = 2
debug = 0
) func main() {
f, err := common.CreateFile("", profileName)
if err != nil {
fmt.Printf("block profile creation error: %v\n", err)
return
}
defer f.Close()
startBlockProfile()
if err = common.Execute(op.BlockProfile, 10); err != nil {
fmt.Printf("execute error: %v\n", err)
return
}
if err := stopBlockProfile(f); err != nil {
fmt.Printf("block profile stop error: %v\n", err)
return
}
} func startBlockProfile() {
runtime.SetBlockProfileRate(blockProfileRate)
} func stopBlockProfile(f *os.File) error {
if f == nil {
return errors.New("nil file")
}
return pprof.Lookup("block").WriteTo(f, debug)
}

问题 3:runtime/pprof.Lookup函数的正确调用方式是什么?

runtime/pprof.Lookup函数(以下简称Lookup函数)的功能是,提供与给定的名称相对应的概要信息。这个概要信息会由一个Profile值代表。如果该函数返回了一个nil,那么就说明不存在与给定名称对应的概要信息。

runtime/pprof包已经为我们预先定义了 6 个概要名称。它们对应的概要信息收集方法和输出方法也都已经准备好了。我们直接拿来使用就可以了。它们是:goroutine、heap、allocs、threadcreate、block和mutex。

当我们把"goroutine"传入Lookup函数的时候,该函数会利用相应的方法,收集到当前正在使用的所有 goroutine 的堆栈跟踪信息。注意,这样的收集会引起 Go 语言调度器的短暂停顿。

当调用该函数返回的Profile值的WriteTo方法时,如果参数debug的值大于或等于2,那么该方法就会输出所有 goroutine 的堆栈跟踪信息。这些信息可能会非常多。

如果它们占用的空间超过了64 MB(也就是64兆字节),那么相应的方法就会将超出的部分截掉。如果Lookup函数接到的参数值是"heap",那么它就会收集与堆内存的分配和释放有关的采样信息。这实际上就是我们在前面讨论过的内存概要信息。

在我们传入"allocs"的时候,后续的操作会与之非常的相似。在这两种情况下,Lookup函数返回的Profile值也会极其相像。只不过,在这两种Profile值的WriteTo方法被调用时,它们输出的概要信息会有细微的差别,而且这仅仅体现在参数debug等于0的时候。

"heap"会使得被输出的内存概要信息默认以“在用空间”(inuse_space)的视角呈现,而"allocs"对应的默认视角则是“已分配空间”(alloc_space)。

“在用空间”是指,已经被分配但还未被释放的内存空间。在这个视角下,go tool pprof工具并不会去理会与已释放空间有关的那部分信息。而在“已分配空间”的视角下,所有的内存分配信息都会被展现出来,无论这些内存空间在采样时是否已被释放。

此外,无论是"heap"还是"allocs",在我们调用Profile值的WriteTo方法的时候,只要赋予debug参数的值大于0,那么该方法输出内容的规格就会是相同的。

参数值"threadcreate"会使Lookup函数去收集一些堆栈跟踪信息。这些堆栈跟踪信息中的每一个都会描绘出一个代码调用链,这些调用链上的代码都导致新的操作系统线程产生。这样的Profile值的输出规格也只有两种,取决于我们传给其WriteTo方法的参数值是否大于0。

再说"block"和"mutex"。"block"代表的是,因争用同步原语而被阻塞的那些代码的堆栈跟踪信息。还记得吗?这就是我们在前面讲过的阻塞概要信息。

与之相对应,"mutex"代表的是,曾经作为同步原语持有者的那些代码,它们的堆栈跟踪信息。它们的输出规格也都只有两种,取决于debug是否大于0。

这里所说的同步原语,指的是存在于 Go 语言运行时系统内部的一种底层的同步工具,或者说一种同步机制。

它是直接面向内存地址的,并以异步信号量和原子操作作为实现手段。我们已经熟知的通道、互斥锁、条件变量、”WaitGroup“,以及 Go 语言运行时系统本身,都会利用它来实现自己的功能。

好了,关于这个问题,我们已经谈了不少了。我相信,你已经对Lookup函数的调用方式及其背后的含义有了比较深刻的理解了。demo99.go 文件中包含了一些示例代码,可供你参考。

package main

import (
"048程序性能分析基础/common"
"048程序性能分析基础/common/op"
"fmt"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"time"
) // profileNames 代表概要信息名称的列表。
var profileNames = []string{
"goroutine",
"heap",
"allocs",
"threadcreate",
"block",
"mutex",
} // profileOps 代表为了生成不同的概要信息而准备的负载函数的字典。
var profileOps = map[string]common.OpFunc{
"goroutine": op.BlockProfile,
"heap": op.MemProfile,
"allocs": op.MemProfile,
"threadcreate": op.BlockProfile,
"block": op.BlockProfile,
"mutex": op.BlockProfile,
} // debugOpts 代表debug参数的可选值列表。
var debugOpts = []int{
0,
1,
2,
} func main() {
prepare()
dir, err := createDir()
if err != nil {
fmt.Printf("dir creation error: %v\n", err)
return
}
for _, name := range profileNames {
for _, debug := range debugOpts {
err = genProfile(dir, name, debug)
if err != nil {
return
}
time.Sleep(time.Millisecond)
}
}
} func genProfile(dir string, name string, debug int) error {
fmt.Printf("Generate %s profile (debug: %d) ...\n", name, debug)
fileName := fmt.Sprintf("%s_%d.out", name, debug)
f, err := common.CreateFile(dir, fileName)
if err != nil {
fmt.Printf("create error: %v (%s)\n", err, fileName)
return err
}
defer f.Close()
if err = common.Execute(profileOps[name], 10); err != nil {
fmt.Printf("execute error: %v (%s)\n", err, fileName)
return err
}
profile := pprof.Lookup(name)
err = profile.WriteTo(f, debug)
if err != nil {
fmt.Printf("write error: %v (%s)\n", err, fileName)
return err
}
return nil
} func createDir() (string, error) {
currDir, err := os.Getwd()
if err != nil {
return "", err
}
path := filepath.Join(currDir, "profiles")
err = os.Mkdir(path, 0766)
if err != nil && !os.IsExist(err) {
return "", err
}
return path, nil
} func prepare() {
runtime.MemProfileRate = 8
runtime.SetBlockProfileRate(2)
}

问题 4:如何为基于 HTTP 协议的网络服务添加性能分析接口?

这个问题说起来还是很简单的。这是因为我们在一般情况下只要在程序中导入net/http/pprof代码包就可以了,就像这样:

import _ "net/http/pprof"

然后,启动网络服务并开始监听,比如:

log.Println(http.ListenAndServe("localhost:8082", nil))

在运行这个程序之后,我们就可以通过在网络浏览器中访问http://localhost:8082/debug/pprof这个地址看到一个简约的网页。如果你认真地看了上一个问题的话,那么肯定可以快速搞明白这个网页中各个部分的含义。

在/debug/pprof/这个 URL 路径下还有很多可用的子路径,这一点你通过点选网页中的链接就可以了解到。像allocs、block、goroutine、heap、mutex、threadcreate这 6 个子路径,在底层其实都是通过Lookup函数来处理的。关于这个函数,你应该已经很熟悉了。

这些子路径都可以接受查询参数debug。它用于控制概要信息的格式和详细程度。至于它的可选值,我就不再赘述了。它的缺省值是0。另外,还有一个名叫gc的查询参数。它用于控制是否在获取概要信息之前强制地执行一次垃圾回收。只要它的值大于0,程序就会这样做。不过,这个参数仅在/debug/pprof/heap路径下有效。

一旦/debug/pprof/profile路径被访问,程序就会去执行对 CPU 概要信息的采样。它接受一个名为seconds的查询参数。该参数的含义是,采样工作需要持续多少秒。如果这个参数未被显式地指定,那么采样工作会持续30秒。注意,在这个路径下,程序只会响应经 protocol buffers 转换的字节流。我们可以通过go tool pprof工具直接读取这样的 HTTP 响应,例如:

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

除此之外,还有一个值得我们关注的路径,即:/debug/pprof/trace。在这个路径下,程序主要会利用runtime/trace代码包中的 API 来处理我们的请求。

更具体地说,程序会先调用trace.Start函数,然后在查询参数seconds指定的持续时间之后再调用trace.Stop函数。这里的seconds的缺省值是1秒。至于runtime/trace代码包的功用,我就留给你自己去查阅和探索吧。

前面说的这些 URL 路径都是固定不变的。这是默认情况下的访问规则。我们还可以对它们进行定制,就像这样:

mux := http.NewServeMux()
pathPrefix := "/d/pprof/"
mux.HandleFunc(pathPrefix,
func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, pathPrefix)
if name != "" {
pprof.Handler(name).ServeHTTP(w, r)
return
}
pprof.Index(w, r)
})
mux.HandleFunc(pathPrefix+"cmdline", pprof.Cmdline)
mux.HandleFunc(pathPrefix+"profile", pprof.Profile)
mux.HandleFunc(pathPrefix+"symbol", pprof.Symbol)
mux.HandleFunc(pathPrefix+"trace", pprof.Trace) server := http.Server{
Addr: "localhost:8083",
Handler: mux,
}

可以看到,我们几乎只使用了net/http/pprof代码包中的几个程序实体,就完成了这样的定制。这在我们使用第三方的网络服务开发框架时尤其有用。

我们自定义的 HTTP 请求多路复用器mux所包含的访问规则与默认的规则很相似,只不过 URL 路径的前缀更短了一些而已。

我们定制mux的过程与net/http/pprof包中的init函数所做的事情也是类似的。这个init函数的存在,其实就是我们在前面仅仅导入"net/http/pprof"代码包就能够访问相关路径的原因。

在我们编写网络服务程序的时候,使用net/http/pprof包要比直接使用runtime/pprof包方便和实用很多。通过合理运用,这个代码包可以为网络服务的监测提供有力的支撑。关于这个包的知识,我就先介绍到这里。

package main

import (
"log"
"net/http"
_ "net/http/pprof"
) func main() {
log.Println(http.ListenAndServe("localhost:8082", nil))
}
package main

import (
"log"
"net/http"
"net/http/pprof"
"strings"
) func main() {
mux := http.NewServeMux()
pathPrefix := "/d/pprof/"
mux.HandleFunc(pathPrefix,
func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, pathPrefix)
if name != "" {
pprof.Handler(name).ServeHTTP(w, r)
return
}
pprof.Index(w, r)
})
mux.HandleFunc(pathPrefix+"cmdline", pprof.Cmdline)
mux.HandleFunc(pathPrefix+"profile", pprof.Profile)
mux.HandleFunc(pathPrefix+"symbol", pprof.Symbol)
mux.HandleFunc(pathPrefix+"trace", pprof.Trace) server := http.Server{
Addr: "localhost:8083",
Handler: mux,
} if err := server.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
log.Println("HTTP server closed.")
} else {
log.Printf("HTTP server error: %v\n", err)
}
}
}

总结

这两篇文章中,我们主要讲了 Go 程序的性能分析,提到的很多内容都是你必备的知识和技巧。这些有助于你真正地理解以采样、收集、输出为代表的一系列操作步骤。

我提到的几种概要信息有关的问题。你需要记住的是,每一种概要信息都代表了什么,它们分别都包含了什么样的内容。

你还需要知道获取它们的正确方式,包括怎样启动和停止采样、怎样设定采样频率,以及怎样控制输出内容的格式和详细程度。

此外,runtime/pprof包中的Lookup函数的正确调用方式也很重要。对于除了 CPU 概要信息之外的其他概要信息,我们都可以通过调用这个函数获取到。

除此之外,我还提及了一个上层的应用,即:为基于 HTTP 协议的网络服务,添加性能分析接口。这也是很实用的一个部分。

虽然net/http/pprof包提供的程序实体并不多,但是它却能够让我们用不同的方式,实现性能分析接口的嵌入。这些方式有的是极简的、开箱即用的,而有的则用于满足各种定制需求。

以上这些,就是我今天为你讲述的 Go 语言知识,它们是程序性能分析的基础。如果你把 Go 语言程序运用于生产环境,那么肯定会涉及它们。对于这里提到的所有内容和问题,我都希望你能够认真地去思考和领会。这样才能够让你在真正使用它们的时候信手拈来。

思考题

我今天留给你的思考题其实在前面已经透露了,那就是:runtime/trace代码包的功用是什么?

笔记源码

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...

随机推荐

  1. Mybatis类型转换BUG

    案例:mybatis框架的使用中是否遇到过前台传入数据后mybatis后台并不执行sql的情况呢? 比如:前台传入一个状态var flag //空字符,0,1 然后你用int接收,到mybatis框架 ...

  2. 题解 P5320 - [BJOI2019]勘破神机(推式子+第一类斯特林数)

    洛谷题面传送门 神仙题(为什么就没能自己想出来呢/zk/zk) 这是我 AC 的第 \(2\times 10^3\) 道题哦 首先考虑 \(m=2\) 的情况,我们首先可以想到一个非常 trivial ...

  3. Atcoder Grand Contest 006 D - Median Pyramid Hard(二分+思维)

    Atcoder 题面传送门 & 洛谷题面传送门 u1s1 Atcoder 不少思维题是真的想不出来,尽管在 Atcoder 上难度并不高 二分答案(这我倒是想到了),检验最上面一层的数是否 \ ...

  4. 洛谷 P6667 - [清华集训2016] 如何优雅地求和(下降幂多项式,多项式)

    题面传送门 wjz:<如何优雅地 AK NOI> 我:如何优雅地爆零 首先,按照这题总结出来的一个小套路,看到多项式与组合数结合的题,可以考虑将普通多项式转为下降幂多项式,因为下降幂和组合 ...

  5. PowerDotNet平台化软件架构设计与实现系列(05):ETCD分布式键值存储平台

    ETCD目前在PowerDotNet已经被用于注册中心和配置管理(常见的配置中心在PowerDotNet中仅仅是一个小小的模块而已)中,作为基础设施的重要组成部分,ETCD的重要性不言而喻. 本文简单 ...

  6. Markdown—.md文件是什么?怎么打开?

    md全称markdown,markdown也是一种标记语言. md文件其实可以用常用的文本编辑器都可以打开.   用记事本打开,把markdown文件拖到记事本图标上就可以打开 .   用 subli ...

  7. JavaScript | 新手村(一)变量,运算和变量方法

    资料来自:JavaScript 第一步 1. 向 html 页面添加 JavaScript 1.1 内部 JavaScript 在 html 文件中的 </body> 标签前插入代码: & ...

  8. 利用抖音Cookie充值接口提取支付链接,个人调起原生微信h5支付宝h5支付

    最近开始搞一些个人支付通道的开发,方便个人不用和第三方平台签约就能收款,省去很多流程手续的成本. 然后翻了一下网上并没有太多现成的技术教程,只能自己研究着搞了. 这次要分享的是利用抖音的充值接口,去分 ...

  9. 学习java 7.14

    学习内容: 标准输入输出流 输出语言的本质:是一个标准的输出流 字节打印流 字符打印流 对象序列化流 明天内容: 进程和线程 遇到问题: 用对象序列化流序列化一个对象后,假如我们修改了对象所属的类文件 ...

  10. three.js很好玩

    能用鼠标拉着转. https://files.cnblogs.com/files/blogs/714801/%E7%A9%BA%E9%97%B4%E5%87%A0%E4%BD%95.7z var po ...