Caddy 性能分析
程序性能分析 是程序在运行时资源使用情况的快照。性能分析对于识别问题区域、排除故障和崩溃以及优化代码非常有帮助。
Caddy 使用 Go 的工具来捕获性能分析,这被称为 pprof,它内置于 go
命令中。
性能分析报告 CPU 和内存的消耗者,显示 goroutine 的堆栈跟踪,并帮助跟踪死锁或高争用的同步原语。
在报告 Caddy 中的某些错误时,我们可能会要求提供性能分析。本文可以提供帮助。它描述了如何使用 Caddy 获取性能分析,以及如何使用和解释生成的 pprof 性能分析文件。
开始之前需要了解的两件事
- Caddy 性能分析不涉及安全敏感信息。 它们包含良性的技术读数,而不是内存的内容。它们不会授予对系统的访问权限。它们可以安全共享。
- 性能分析是轻量级的,可以在生产环境中收集。 实际上,对于许多用户来说,这是一个推荐的最佳实践;请参阅本文后面的内容。
获取性能分析
性能分析可通过 管理界面 在 /debug/pprof/
访问。在运行 Caddy 的机器上,在浏览器中打开它
https://127.0.0.1:2019/debug/pprof/
您会注意到一个简单的计数和链接表,例如
计数 | 性能分析 |
---|---|
79 | allocs |
0 | block |
0 | cmdline |
22 | goroutine |
79 | heap |
0 | mutex |
0 | profile |
29 | threadcreate |
0 | trace |
完整 goroutine 堆栈转储 |
计数是快速识别泄漏的一种便捷方法。如果您怀疑存在泄漏,请反复刷新页面,您会看到其中一个或多个计数不断增加。如果堆计数增长,则可能是内存泄漏;如果 goroutine 计数增长,则可能是 goroutine 泄漏。
点击性能分析并查看它们的外观。有些可能是空的,这在很多时候是正常的。最常用的性能分析是 goroutine(函数堆栈)、heap(内存)和 profile(CPU)。其他性能分析对于排除 mutex 争用或死锁很有用。
在底部,有每个性能分析的简单描述
- allocs: 所有过去内存分配的抽样
- block: 导致阻塞在同步原语上的堆栈跟踪
- cmdline: 当前程序的命令行调用
- goroutine: 所有当前 goroutine 的堆栈跟踪。使用 debug=2 作为查询参数以与未恢复的 panic 相同的格式导出。
- heap: 活动对象的内存分配抽样。您可以指定 gc GET 参数在获取堆样本之前运行 GC。
- mutex: 争用 mutex 的持有者的堆栈跟踪
- profile: CPU 性能分析。您可以以秒为单位在 seconds GET 参数中指定持续时间。获取性能分析文件后,使用 go tool pprof 命令来调查性能分析。
- threadcreate: 导致创建新 OS 线程的堆栈跟踪
- trace: 当前程序执行的跟踪。您可以以秒为单位在 seconds GET 参数中指定持续时间。获取跟踪文件后,使用 go tool trace 命令来调查跟踪。
下载性能分析
单击上面 pprof 索引页面上的链接将为您提供文本格式的性能分析。这对于调试很有用,这也是 Caddy 团队的首选,因为我们可以扫描它以查找明显的线索,而无需额外的工具。
但二进制实际上是默认格式。HTML 链接附加 ?debug=
查询字符串参数以将它们格式化为文本,除了(CPU)“profile”链接,它没有文本表示形式。
这些是您可以设置的查询字符串参数(来自 Go 文档)
debug=N
(除 cpu 之外的所有性能分析): 响应格式:N = 0:二进制(默认),N > 0:纯文本gc=N
(heap 性能分析): N > 0:在性能分析之前运行垃圾回收周期seconds=N
(allocs、block、goroutine、heap、mutex、threadcreate 性能分析): 返回增量性能分析seconds=N
(cpu、trace 性能分析): 给定持续时间的性能分析
由于这些是 HTTP 端点,您还可以使用任何 HTTP 客户端(如 curl 或 wget)来下载性能分析。
下载性能分析后,您可以将它们上传到 GitHub issue 评论或使用像 pprof.me 这样的站点。对于 CPU 性能分析,flamegraph.com 是另一个选择。
远程访问
如果您已经能够本地访问管理 API,请跳过此部分。
默认情况下,Caddy 的管理 API 仅可通过环回套接字访问。但是,至少有 3 种方法可以远程访问 Caddy 的 /debug/pprof
端点
通过您的站点进行反向代理
一个简单的选项是从您的站点简单地反向代理到它
reverse_proxy /debug/pprof/* localhost:2019 {
header_up Host {upstream_hostport}
}
当然,这将使可以连接到您的站点的人员可以访问性能分析。如果不需要这样,您可以使用您选择的 HTTP 身份验证模块添加一些身份验证。
(不要忘记 /debug/pprof/*
匹配器,否则您将代理整个管理 API!)
SSH 隧道
另一种方法是使用 SSH 隧道。这是您的计算机和服务器之间使用 SSH 协议的加密连接。在您的计算机上运行如下命令
ssh -N username@example.com -L 8123:localhost:2019
这会将 localhost:8123
(在您的本地机器上)隧道传输到 example.com
上的 localhost:2019
。确保根据需要替换 username
、example.com
和端口。
然后在另一个终端中,您可以像这样运行 curl
curl -v https://127.0.0.1:8123/debug/pprof/ -H "Host: localhost:2019"
您可以通过在隧道的两侧都使用端口 2019
来避免需要 -H "Host: ..."
(但这要求端口 2019
在您自己的计算机上尚未被占用,即没有本地运行 Caddy)。
在隧道处于活动状态时,您可以访问任何和所有管理 API。在 ssh
命令上键入 Ctrl+C 以关闭隧道。
长时间运行的隧道
使用上述命令运行隧道要求您保持终端打开。如果您想在后台运行隧道,您可以像这样启动隧道
ssh -f -N -M -S /tmp/caddy-tunnel.sock username@example.com -L 8123:localhost:2019
这将会在后台启动并在 /tmp/caddy-tunnel.sock
创建一个控制套接字。然后您可以使用控制套接字在您完成操作后关闭隧道
ssh -S /tmp/caddy-tunnel.sock -O exit e
远程管理 API
您还可以配置管理 API 以接受远程连接到授权客户端。
(TODO:撰写关于此的文章。)
Goroutine 性能分析
goroutine 转储对于了解存在哪些 goroutine 以及它们的调用堆栈很有用。换句话说,它让我们了解当前正在执行或正在阻塞/等待的代码。
如果您点击“goroutines”或转到 /debug/pprof/goroutine?debug=1
,您将看到 goroutine 及其调用堆栈的列表。例如
goroutine profile: total 88
23 @ 0x43e50e 0x436d37 0x46bda5 0x4e1327 0x4e261a 0x4e2608 0x545a65 0x5590c5 0x6b2e9b 0x50ddb8 0x6b307e 0x6b0650 0x6b6918 0x6b6921 0x4b8570 0xb11a05 0xb119d4 0xb12145 0xb1d087 0x4719c1
# 0x46bda4 internal/poll.runtime_pollWait+0x84 runtime/netpoll.go:343
# 0x4e1326 internal/poll.(*pollDesc).wait+0x26 internal/poll/fd_poll_runtime.go:84
# 0x4e2619 internal/poll.(*pollDesc).waitRead+0x279 internal/poll/fd_poll_runtime.go:89
# 0x4e2607 internal/poll.(*FD).Read+0x267 internal/poll/fd_unix.go:164
# 0x545a64 net.(*netFD).Read+0x24 net/fd_posix.go:55
# 0x5590c4 net.(*conn).Read+0x44 net/net.go:179
# 0x6b2e9a crypto/tls.(*atLeastReader).Read+0x3a crypto/tls/conn.go:805
# 0x50ddb7 bytes.(*Buffer).ReadFrom+0x97 bytes/buffer.go:211
# 0x6b307d crypto/tls.(*Conn).readFromUntil+0xdd crypto/tls/conn.go:827
# 0x6b064f crypto/tls.(*Conn).readRecordOrCCS+0x24f crypto/tls/conn.go:625
# 0x6b6917 crypto/tls.(*Conn).readRecord+0x157 crypto/tls/conn.go:587
# 0x6b6920 crypto/tls.(*Conn).Read+0x160 crypto/tls/conn.go:1369
# 0x4b856f io.ReadAtLeast+0x8f io/io.go:335
# 0xb11a04 io.ReadFull+0x64 io/io.go:354
# 0xb119d3 golang.org/x/net/http2.readFrameHeader+0x33 golang.org/x/net@v0.14.0/http2/frame.go:237
# 0xb12144 golang.org/x/net/http2.(*Framer).ReadFrame+0x84 golang.org/x/net@v0.14.0/http2/frame.go:498
# 0xb1d086 golang.org/x/net/http2.(*serverConn).readFrames+0x86 golang.org/x/net@v0.14.0/http2/server.go:818
1 @ 0x43e50e 0x44e286 0xafeeb3 0xb0af86 0x5c29fc 0x5c3225 0xb0365b 0xb03650 0x15cb6af 0x43e09b 0x4719c1
# 0xafeeb2 github.com/caddyserver/caddy/v2/cmd.cmdRun+0xcd2 github.com/caddyserver/caddy/v2@v2.7.4/cmd/commandfuncs.go:277
# 0xb0af85 github.com/caddyserver/caddy/v2/cmd.init.1.func2.WrapCommandFuncForCobra.func1+0x25 github.com/caddyserver/caddy/v2@v2.7.4/cmd/cobra.go:126
# 0x5c29fb github.com/spf13/cobra.(*Command).execute+0x87b github.com/spf13/cobra@v1.7.0/command.go:940
# 0x5c3224 github.com/spf13/cobra.(*Command).ExecuteC+0x3a4 github.com/spf13/cobra@v1.7.0/command.go:1068
# 0xb0365a github.com/spf13/cobra.(*Command).Execute+0x5a github.com/spf13/cobra@v1.7.0/command.go:992
# 0xb0364f github.com/caddyserver/caddy/v2/cmd.Main+0x4f github.com/caddyserver/caddy/v2@v2.7.4/cmd/main.go:65
# 0x15cb6ae main.main+0xe caddy/main.go:11
# 0x43e09a runtime.main+0x2ba runtime/proc.go:267
1 @ 0x43e50e 0x44e9c5 0x8ec085 0x4719c1
# 0x8ec084 github.com/caddyserver/certmagic.(*Cache).maintainAssets+0x304 github.com/caddyserver/certmagic@v0.19.2/maintain.go:67
...
第一行 goroutine profile: total 88
告诉我们正在查看的内容以及有多少 goroutine。
goroutine 列表如下。它们按其调用堆栈分组,按频率降序排列。
goroutine 行具有以下语法:<count> @ <addresses...>
该行以具有关联调用堆栈的 goroutine 计数开始。@
符号表示调用指令地址(即函数指针)的开始,这些地址是 goroutine 的起源。每个指针都是一个函数调用或调用帧。
您可能会注意到,您的许多 goroutine 共享相同的第一个调用地址。这是您程序的主入口点或入口点。某些 goroutine 不会从那里开始,因为程序有各种 init()
函数,并且 Go 运行时也可能生成 goroutine。
接下来的行以 #
开头,实际上只是为了读者利益的注释。它们包含 goroutine 的当前堆栈跟踪。顶部代表堆栈的顶部,即当前正在执行的代码行。底部代表堆栈的底部,或 goroutine 最初开始运行的代码。
堆栈跟踪具有以下格式
<address> <package/func>+<offset> <filename>:<line>
地址是函数指针,然后您将看到 Go 包和函数名称(如果它是方法,则带有相关的类型名称),以及函数中的指令偏移量。然后,也许是最有用的信息,文件和行号,在末尾。
完整 goroutine 堆栈转储
如果我们将查询字符串参数更改为 ?debug=2
,我们将获得完整转储。这包括每个 goroutine 的详细堆栈跟踪,并且相同的 goroutine 不会被折叠。此输出在繁忙的服务器上可能非常大,但它是有趣的信息!
让我们看一下与上面第一个调用堆栈对应的那个(已截断)
goroutine 61961905 [IO wait, 1 minutes]:
internal/poll.runtime_pollWait(0x7f9a9a059eb0, 0x72)
runtime/netpoll.go:343 +0x85
...
golang.org/x/net/http2.(*serverConn).readFrames(0xc001756f00)
golang.org/x/net@v0.14.0/http2/server.go:818 +0x87
created by golang.org/x/net/http2.(*serverConn).serve in goroutine 61961902
golang.org/x/net@v0.14.0/http2/server.go:930 +0x56a
尽管它很冗长,但此转储唯一提供的最有用的信息是每个 goroutine 的第一行和最后一行。
第一行包含 goroutine 的编号 (61961905)、状态 (“IO wait”) 和持续时间 (“1 分钟”)
-
Goroutine 编号: 是的,goroutine 有编号!但它们没有暴露给我们的代码。但是,这些编号在堆栈跟踪中特别有用,因为我们可以看到哪个 goroutine 生成了这个 goroutine(请参阅末尾:“由 ... 在 goroutine 61961902 中创建”)。下面显示的工具可以帮助我们绘制此过程的可视化图形。
-
状态: 这告诉我们 goroutine 当前正在做什么。以下是您可能会看到的一些可能的状态
running
:正在执行代码 - 棒极了!IO wait
:等待网络。不消耗 OS 线程,因为它停放在非阻塞网络轮询器上。sleep
:我们都需要更多睡眠。select
:阻塞在 select 上;等待一个 case 变为可用。select (no cases):
特别阻塞在空 selectselect {}
上。Caddy 在其主函数中使用一个来保持运行,因为关闭是从其他 goroutine 发起的。chan receive
:阻塞在通道接收 (<-ch
) 上。semacquire
:等待获取信号量(低级同步原语)。syscall
:执行系统调用。消耗 OS 线程。
-
持续时间: goroutine 存在了多长时间。对于查找 goroutine 泄漏等错误很有用。例如,如果我们期望所有网络连接在几分钟后关闭,那么当我们发现大量 netconn goroutine 存活数小时时,这意味着什么?
解释 goroutine 转储
在不查看代码的情况下,我们能从上面的 goroutine 中学到什么?
它大约在一分钟前创建,正在等待通过网络套接字传输的数据,并且其 goroutine 编号非常大 (61961905)。
从第一个转储 (debug=1) 中,我们知道它的调用堆栈执行频率相对较高,并且大的 goroutine 编号与短持续时间相结合表明,已经有数千万个相对短暂的 goroutine。它在一个名为 pollWait
的函数中,其调用历史记录包括从使用 TLS 的加密网络连接读取 HTTP/2 帧。
因此,我们可以推断出这个 goroutine 正在处理 HTTP/2 请求!它正在等待来自客户端的数据。更重要的是,我们知道生成它的 goroutine 不是进程的第一个 goroutine 之一,因为它也有一个很高的编号;在转储中找到该 goroutine 可以发现,它是为了在现有请求期间处理新的 HTTP/2 流而生成的。相比之下,其他编号较高的 goroutine 可能是由编号较低的 goroutine(例如 32)生成的,这表明一个全新的连接是从套接字的 Accept()
调用中新鲜产生的。
每个程序都不同,但在调试 Caddy 时,这些模式往往是成立的。
内存性能分析
内存(或堆)性能分析跟踪堆分配,堆分配是系统上内存的主要消耗者。分配也是性能问题的常见嫌疑对象,因为分配内存需要系统调用,这可能很慢。
堆性能分析在几乎所有方面都与 goroutine 性能分析相似,除了顶行的开头。这是一个例子
0: 0 [1: 4096] @ 0xb1fc05 0xb1fc4d 0x48d8d1 0xb1fce6 0xb184c7 0xb1bc8e 0xb41653 0xb4105c 0xb4151d 0xb23b14 0x4719c1
# 0xb1fc04 bufio.NewWriterSize+0x24 bufio/bufio.go:599
# 0xb1fc4c golang.org/x/net/http2.glob..func8+0x6c golang.org/x/net@v0.17.0/http2/http2.go:263
# 0x48d8d0 sync.(*Pool).Get+0xb0 sync/pool.go:151
# 0xb1fce5 golang.org/x/net/http2.(*bufferedWriter).Write+0x45 golang.org/x/net@v0.17.0/http2/http2.go:276
# 0xb184c6 golang.org/x/net/http2.(*Framer).endWrite+0xc6 golang.org/x/net@v0.17.0/http2/frame.go:371
# 0xb1bc8d golang.org/x/net/http2.(*Framer).WriteHeaders+0x48d golang.org/x/net@v0.17.0/http2/frame.go:1131
# 0xb41652 golang.org/x/net/http2.(*writeResHeaders).writeHeaderBlock+0xd2 golang.org/x/net@v0.17.0/http2/write.go:239
# 0xb4105b golang.org/x/net/http2.splitHeaderBlock+0xbb golang.org/x/net@v0.17.0/http2/write.go:169
# 0xb4151c golang.org/x/net/http2.(*writeResHeaders).writeFrame+0x1dc golang.org/x/net@v0.17.0/http2/write.go:234
# 0xb23b13 golang.org/x/net/http2.(*serverConn).writeFrameAsync+0x73 golang.org/x/net@v0.17.0/http2/server.go:851
第一行格式如下
<live objects> <live memory> [<allocations>: <allocation memory>] @ <addresses...>
在上面的示例中,我们有一个由 bufio.NewWriterSize()
进行的单次分配,但当前没有来自此调用堆栈的活动对象。
有趣的是,我们可以从该调用堆栈推断出 http2 包使用了池化的 4 KB 来向客户端写入 HTTP/2 帧。如果您已优化热路径以重用分配,您通常会在 Go 内存性能分析中看到池化对象。这减少了新的分配,堆性能分析可以帮助您了解池是否被正确使用!
CPU 性能分析
CPU 性能分析帮助您了解 Go 程序在处理器上花费最多调度时间的位置。
但是,这些没有纯文本形式,因此在下一节中,我们将使用 go tool pprof
命令来帮助我们读取它们。
要下载 CPU 性能分析,请向 /debug/pprof/profile?seconds=N
发出请求,其中 N 是您要收集性能分析的秒数。在 CPU 性能分析收集期间,程序性能可能会受到轻微影响。(其他性能分析几乎没有性能影响。)
完成后,它应该下载一个二进制文件,恰当地命名为 profile
。然后我们需要检查它。
go tool pprof
我们将使用 Go 的内置性能分析分析器来读取 CPU 性能分析作为示例,但您可以将其与任何类型的性能分析一起使用。
运行此命令(如果文件路径不同,请将“profile”替换为实际文件路径),这将打开一个交互式提示符
go tool pprof profile
File: caddy_master
Type: cpu
Time: Aug 29, 2022 at 8:47pm (MDT)
Duration: 30.02s, Total samples = 70.11s (233.55%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
这是您可以探索的东西。输入 help
会为您提供命令列表,o
会显示当前选项。如果您输入 help <command>
,您可以获得有关特定命令的信息。
有很多命令,但一些常见的命令是
top
:显示哪些内容占用了最多的 CPU。您可以附加一个数字,例如top 20
以查看更多内容,或使用正则表达式来“聚焦”或忽略某些项目。web
:在您的 Web 浏览器中打开调用图。这是直观地查看 CPU 使用率的好方法。svg
:生成调用图的 SVG 图像。它与web
相同,只是它不会打开您的 Web 浏览器,并且 SVG 保存在本地。tree
:调用堆栈的表格视图。
让我们从 top
开始。我们看到类似这样的输出
(pprof) top
Showing nodes accounting for 38.36s, 54.71% of 70.11s total
Dropped 785 nodes (cum <= 0.35s)
Showing top 10 nodes out of 196
flat flat% sum% cum cum%
10.97s 15.65% 15.65% 10.97s 15.65% runtime/internal/syscall.Syscall6
6.59s 9.40% 25.05% 36.65s 52.27% runtime.gcDrain
5.03s 7.17% 32.22% 5.34s 7.62% runtime.(*lfstack).pop (inline)
3.69s 5.26% 37.48% 11.02s 15.72% runtime.scanobject
2.42s 3.45% 40.94% 2.42s 3.45% runtime.(*lfstack).push
2.26s 3.22% 44.16% 2.30s 3.28% runtime.pageIndexOf (inline)
2.11s 3.01% 47.17% 2.56s 3.65% runtime.findObject
2.03s 2.90% 50.06% 2.03s 2.90% runtime.markBits.isMarked (inline)
1.69s 2.41% 52.47% 1.69s 2.41% runtime.memclrNoHeapPointers
1.57s 2.24% 54.71% 1.57s 2.24% runtime.epollwait
CPU 的前 10 个消费者都在 Go 运行时中——特别是,大量的垃圾回收(记住 syscall 用于释放和分配内存)。这是一个提示,我们可以减少分配以提高性能,并且堆性能分析将是值得的。
好的,但是如果我们想查看我们自己代码的 CPU 使用率怎么办?我们可以像这样忽略包含“runtime”的模式
(pprof) top -runtime
Active filters:
ignore=runtime
Showing nodes accounting for 0.92s, 1.31% of 70.11s total
Dropped 160 nodes (cum <= 0.35s)
Showing top 10 nodes out of 243
flat flat% sum% cum cum%
0.17s 0.24% 0.24% 0.28s 0.4% sync.(*Pool).getSlow
0.11s 0.16% 0.4% 0.11s 0.16% github.com/prometheus/client_golang/prometheus.(*histogram).observe (inline)
0.10s 0.14% 0.54% 0.23s 0.33% github.com/prometheus/client_golang/prometheus.(*MetricVec).hashLabels
0.10s 0.14% 0.68% 0.12s 0.17% net/textproto.CanonicalMIMEHeaderKey
0.10s 0.14% 0.83% 0.10s 0.14% sync.(*poolChain).popTail
0.08s 0.11% 0.94% 0.26s 0.37% github.com/prometheus/client_golang/prometheus.(*histogram).Observe
0.07s 0.1% 1.04% 0.07s 0.1% internal/poll.(*fdMutex).rwlock
0.07s 0.1% 1.14% 0.10s 0.14% path/filepath.Clean
0.06s 0.086% 1.23% 0.06s 0.086% context.value
0.06s 0.086% 1.31% 0.06s 0.086% go.uber.org/zap/buffer.(*Buffer).AppendByte
好吧,很明显 Prometheus 指标是另一个主要消费者,但您会注意到,累积起来,它们比上面的 GC 小几个数量级。这种鲜明的差异表明我们应该专注于减少 GC。
让我们使用 q
退出此性能分析,并在堆性能分析上使用相同的命令
(pprof) top
Showing nodes accounting for 22259.07kB, 81.30% of 27380.04kB total
Showing top 10 nodes out of 102
flat flat% sum% cum cum%
12300kB 44.92% 44.92% 12300kB 44.92% runtime.allocm
2570.01kB 9.39% 54.31% 2570.01kB 9.39% bufio.NewReaderSize
2048.81kB 7.48% 61.79% 2048.81kB 7.48% runtime.malg
1542.01kB 5.63% 67.42% 1542.01kB 5.63% bufio.NewWriterSize
...
答对了。几乎一半的内存严格分配用于我们使用 bufio 包的读写缓冲区。因此,我们可以推断出优化我们的代码以减少缓冲将非常有利。(Caddy 中相关的补丁 就是这样做的)。
可视化
如果我们改为运行 svg
或 web
命令,我们将获得性能分析的可视化
这是一个 CPU 性能分析,但类似的图表也适用于其他性能分析类型。
要了解如何阅读这些图表,请阅读 pprof 文档。
差异性能分析
在您进行代码更改后,您可以使用差异分析(“diff”)来比较之前和之后的情况。这是堆的差异
go tool pprof -diff_base=before.prof after.prof
File: caddy
Type: inuse_space
Time: Aug 29, 2022 at 1:21am (MDT)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for -26.97MB, 49.32% of 54.68MB total
Dropped 10 nodes (cum <= 0.27MB)
Showing top 10 nodes out of 137
flat flat% sum% cum cum%
-27.04MB 49.45% 49.45% -27.04MB 49.45% bufio.NewWriterSize
-2MB 3.66% 53.11% -2MB 3.66% runtime.allocm
1.06MB 1.93% 51.18% 1.06MB 1.93% github.com/yuin/goldmark/util.init
1.03MB 1.89% 49.29% 1.03MB 1.89% github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy.glob..func2
1MB 1.84% 47.46% 1MB 1.84% bufio.NewReaderSize
-1MB 1.83% 49.29% -1MB 1.83% runtime.malg
1MB 1.83% 47.46% 1MB 1.83% github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy.cloneRequest
-1MB 1.83% 49.29% -1MB 1.83% net/http.(*Server).newConn
-0.55MB 1.00% 50.29% -0.55MB 1.00% html.populateMaps
0.53MB 0.97% 49.32% 0.53MB 0.97% github.com/alecthomas/chroma.TypeRemappingLexer
如您所见,我们减少了大约一半的内存分配!
差异也可以可视化
这使得更改如何影响程序某些部分的性能变得非常明显。
进一步阅读
程序性能分析有很多要掌握的内容,而我们只是触及了皮毛。
要真正将“pro”放入“profiling”中,请考虑以下资源