2020年2月7日
一旦你了解了基础知识,Golang 可以让你比以前更有效率。但是当出现问题的时候怎么办呢?
你可能不知道的是, Go 本身包含 pprof ,用于记录和可视化运行时分析数据。像 delve 这样的第三方工具增加了对逐行调试的支持。泄漏和竞争探测器可以抵御不确定性行为。
如果你以前没有见过或使用过这些工具,它们将很快成为你的 Golang 工具库的强大补充。
为什么我不把所有内容都打印出来呢?
我遇到过很多开发人员,他们在代码出现问题时很少打开调试器。我认为这没有错。如果你正在编写单元测试、编选代码并进行重构,那么快捷的方法可以适用于大多数情况。
相反,我一直在排查故障问题,意识到在某些断点处打开一个交互式调试器要比连续添加断点然后打印语句快得多。
图显示了 Kent Gruber 修复的内存泄漏
例如,有一天我在查看一个我帮助维护的 web 应用程序的内存图。每天总内存使用量都在缓慢增加,以至于需要重新启动服务器才能保持稳定。这是一个典型的内存泄漏示例。
快捷的方法建议我们通读代码,确保生成的 goroutines 退出,分配的变量被垃圾回收,连接正确关闭,等等。相反,我们分析了应用程序,在几分钟内发现了内存泄漏。是一个复杂的单个语句导致造成这种错误。
在此,我将向你介绍一些我几乎每天都在使用的工具来解决像这样的问题。
剖析记录和可视化
那我们现在开始,先以关闭正常的基本 Golang Web 服务器为例,发送一些人工流量。然后我们将使用 pprof 工具收集尽可能多的信息。
// main.go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!\n")
}
func main() {
srv := http.Server{
Addr: ":8080",
ReadTimeout: time.Minute,
WriteTimeout: time.Minute,
}
http.HandleFunc("/", handler)
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
srv.ListenAndServe()
}()
<-done
err := srv.Shutdown(context.Background())
if err != nil {
log.Fatal(err)
}
}
我们可以通过执行以下命令确保这一点:
$ go run main.go &
$ curl localhost:8080
Hello World!
现在我们可以通过以下代码段来分析 CPU:
...
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
log.Fatal(err)
}
defer pprof.StopCPUProfile()
...
我们将使用负载测试工具来彻底地运行 web 服务器,以模拟正常到繁忙的流量。我使用测试工具 vegeta 完成了以下操作:
$ echo "GET http://localhost:8080" | vegeta attack -duration=5s
Hello world!
...
当我们关闭 go web 服务器时,我们会看到一个文件cpu.prof
,它包含 CPU 配置文件。然后可以使用 pprof
工具可视化此配置文件:
$ go tool pprof cpu.prof
Type: cpu
Time: Jan 16, 2020 at 4:51pm (EST)
Duration: 9.43s, Total samples = 50ms ( 0.53%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 10
Showing nodes accounting for 50ms, 100% of 50ms total
Showing top 10 nodes out of 24
flat flat% sum% cum cum%
20ms 40.00% 40.00% 20ms 40.00% syscall.syscall
...
这是一个好的开始,但 Go 可以做得更好。我们希望在应用程序接收流量时对其进行分析,这样我们就不必依赖模拟流量或添加额外的代码来将分析文件写入文件。添加 net/http/pprof
导入将自动向我们的 web 服务器添加其他处理程序:
import _ "net/http/pprof"
添加了这些之后,我们可以通过 web 浏览器点击 /debug/pprof/
路由,并看到充满信息的 pprof
页面。
导航到 /debug/pprof/route
时将看到的示例
我们可以通过运行以下命令获得与以前相同的信息:
$ go tool pprof -top http://localhost:8080/debug/pprof/heap
你还可以:
- 根据配置文件的类型生成图像。
- 创建 Flame Graphs 来可视化应用程序。
- 跟踪 Goroutines 以在导致服务降级之前检测泄漏。
请注意,对于生产 web 服务器,我们尽量避免将这些信息公开,而是应该将它们绑定到不同的内部端口。
深入研究交互式调试器
Delve 的广告语是:
…一个简单、功能齐全的 Go 调试工具。Delve 易于调用和使用。如果您使用的是调试器,则可能会出现问题。考虑到这一点,Delve 会尽可能避开你。
为此,当你有一个需要花很长时间才能解决的 bug 时,它的效果非常好。
开始使用该工具相当简单,只需按照安装步骤操作即可。添加 runtime.Breakpoint()
并使用 dlv
运行代码:
$ dlv debug main.go
Type 'help' for list of commands.
(dlv) continue
一旦到达断点,您将看到代码块,例如在上面的 Web 服务器中,我将把 runtime.Breakpoint()
放入处理程序:
> main.handler() ./main.go:20 (PC: 0x1495476)
15: _ "net/http/pprof"
16: )
17:
18: func handler(w http.ResponseWriter, r *http.Request) {
19: runtime.Breakpoint()
=> 20: fmt.Fprintf(w, "Hello World!\n")
21: }
22:
23: func main() {
24: srv := http.Server{
25: Addr: ":8080",
(dlv)
现在,你可以使用 next
或 n
命令逐行执行,也可以使用 step
或 s
命令深入研究函数。
带有 Golang 扩展名的 VS Code 示例,显示调试测试按钮
如果你喜欢漂亮的 UI 和点击按钮而不是使用键盘,那么 VS Code 有很好的支持。当使用本机测试库编写单元测试时,您将看到一个调试测试的按钮,该按钮将初始化 delve,并允许您在交互会话中通过 VS Code 逐步完成代码。
有关使用 VS Code 调试 Go 代码的更多信息,请查看 https://github.com/Microsoft/vscode-go/wiki/Debugging-Go-code-using-VS-Code
。
深入研究可以使添加断点、测试断点和深入研究软件包变得轻而易举。当你下一次被困在一个 bug 上并且想知道更多的事情发生时,不要害怕使用它。
泄漏和竞争探测器
我要讨论的最后一个主题是如何在测试中添加 Golang 泄漏检测和竞争检测器。如果您还没有遇到过比赛状况或经历过 Goroutine 内存泄漏,你真是太幸运了。
2017年,Uber 开源了 goleak 包,这是一个简单的检查工具,一旦通过 Find
发现任何额外的 goroutine,它会将给定的 TestingT
标记为失败。
看起来是这样的:
func TestA(t *testing.T) {
defer goleak.VerifyNone(t)
// test logic here.
}
在进行复杂的异步工作时,您可以确保避免倒退,并遵循 The Zen of Go 的第五条原则:
在你启动 goroutine 之前,要知道它什么时候会停止。
最后,在确保你没有 Goleaks 之后,你会想保护自己免受比赛条件的影响。谢天谢地,数据竞争检测器是内置的。考虑竞争探测器文档中的示例:
//race.go
func main() {
c := make(chan bool)
m := make(map[string]string)
go func() {
m["1"] = "a" // First conflicting access.
c <- true
}()
m["2"] = "b" // Second conflicting access.
<-c
for k, v := range m {
fmt.Println(k, v)
}
}
这是一场可能导致崩溃和内存损坏的数据竞赛。使用 -race
标志运行此代码段会导致宕机,并显示一条有用的错误消息:
go run -race main.go
==================
WARNING: DATA RACE
Write at 0x00c0000e2210 by goroutine 8:
runtime.mapassign_faststr()
/usr/local/Cellar/go/1.13.6/libexec/src/runtime/map_faststr.go:202 +0x0
main.main.func1()
/PATH/main.go:19 +0x5d
Previous write at 0x00c0000e2210 by main goroutine:
runtime.mapassign_faststr()
/usr/local/Cellar/go/1.13.6/libexec/src/runtime/map_faststr.go:202 +0x0
main.main()
/PATH/main.go:22 +0xc6
Goroutine 8 (running) created at:
main.main()
/PATH/main.go:18 +0x97
==================
2 b
1 a
Found 1 data race(s)
虽然您可以在代码执行期间使用该标志,但在编写测试时添加到 go test
命令中以检测比赛是最有用的。
结论
这些只是 Golang 生态系统中可以用来帮助观察、调试和防止代码库中的生产故障的一些优秀工具。如果你想更进一步,我建议你看一下:
- 分布式跟踪就像 Open-tracing Go.
- 使用 Prometheus 这样的工具进行时间序列监测
- 使用 logrus 进行结构化日志记录。
有关上面列出的任何工具的更多信息,请查看参考资料部分的完整文档和手册。