文档
一个 项目

Caddy 性能分析

程序性能分析 是程序在运行时资源使用情况的快照。性能分析对于识别问题区域、排除故障和崩溃以及优化代码非常有帮助。

Caddy 使用 Go 的工具来捕获性能分析,这被称为 pprof,它内置于 go 命令中。

性能分析报告 CPU 和内存的消耗者,显示 goroutine 的堆栈跟踪,并帮助跟踪死锁或高争用的同步原语。

在报告 Caddy 中的某些错误时,我们可能会要求提供性能分析。本文可以提供帮助。它描述了如何使用 Caddy 获取性能分析,以及如何使用和解释生成的 pprof 性能分析文件。

开始之前需要了解的两件事

  1. Caddy 性能分析不涉及安全敏感信息。 它们包含良性的技术读数,而不是内存的内容。它们不会授予对系统的访问权限。它们可以安全共享。
  2. 性能分析是轻量级的,可以在生产环境中收集。 实际上,对于许多用户来说,这是一个推荐的最佳实践;请参阅本文后面的内容。

获取性能分析

性能分析可通过 管理界面/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。确保根据需要替换 usernameexample.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): 特别阻塞在空 select select {} 上。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 中相关的补丁 就是这样做的)。

可视化

如果我们改为运行 svgweb 命令,我们将获得性能分析的可视化

CPU profile visualization

这是一个 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

如您所见,我们减少了大约一半的内存分配!

差异也可以可视化

CPU profile visualization

这使得更改如何影响程序某些部分的性能变得非常明显。

进一步阅读

程序性能分析有很多要掌握的内容,而我们只是触及了皮毛。

要真正将“pro”放入“profiling”中,请考虑以下资源