Golang 问题排查指南

文|三藏(唐智坚)

当我们收到线上服务的报警,如何正确的处理?当遇到莫名的性能问题,如何定位到 RootCase ?线上问题诊断总是困难重重,但是我们可以通过成熟的方法论和工具链来帮助我们迅速定位问题。这里根据我们内部的实践和大家做一个分享。

1. 报警排查

报警是一个客观事实,即使是误报也说明了出现了一些没有预期的 case,我们无法穷尽所有的问题,但是我们可以建立标准的流程和 SOP手册,去拆分问题的颗粒度,简化难度,更加容易的去定位问题。

1.1 流程

  • don't panic,胸有激雷而面如平湖者,可拜上将军!深吸一口气不要慌张,在紧张和压力面前越是慌张越会犯错,反而忽略了正确的线索。
  • 然后在群里同步你已开始介入,让大家放心的把后背交给你。
  • 80% 的事故都是当日上线的变更。
  • 对于事故,止损是第一要务,即使会破坏现场,事后我们总是可以靠着蛛丝马迹找出线索。不能降级就重启,重启无效就回滚。
  • 从资源利用率到延迟建立不同的 SOP。遇到问题时我们只需要按图索骥,足矣解决90% 的问题。对于解决不了,要呼叫队友和大佬进行支援,语音是最有效的沟通。

1.2 建立 SOP 手册

建立好 SOP , 培养组织的战斗力,不能只是一个人的单打独斗。有幸我们建立了完善的 SOP,即使一个新人也能迅速投入战场,参与到线上排查。我们内部的文档如下:

  • 服务调用异常排查SOP
  • 响应延迟增长问题排查SOP
  • 熔断问题排查SOP
  • Mysql响应RT升高排查 SOP
  • Redis响应RT升高排查SOP
  • ES响应RT、错误率升高SOP
  • goroutine异常升高排查SOP
  • 实例CPU、内存异常排查SOP
  • 流量环比上涨排查SOP
  • 业务常见问题排查

限于内部的敏感性,这里基于上述资料做一个总结,处理手册应该做好以下几点:

  • 涵盖各种工具的链接,有服务的Owner,基建的负责人,要做到不靠搜,不靠问
  • Grafana 等工具做好 Dashboard ,一个好的 Dashboard 能够直观的定位到有问题的 API ,可以看到 P99, 95, 90 等延迟, QPS、流量等监控。错误率是重点监控的值,延迟只是表象,错误能帮我们接近真相

  • 查看对应的服务是否存活?是否存在资源瓶颈(CPU 打满等)?Goroutine 是否飙升?全家桶炸了?

这种就直接重启,大概率是资源连接申请了没有释放。重启无效就止血。

  • 基建(Redis,Mysql 等)的延迟,查看当前的连接数,慢请求数,硬件资源等。性能之巅里的 USE(utilization、saturation、erros)是个很好的切入点,对于所有的资源,查看它的使用率、饱和度和错误。
  • 部分接口慢,直接限流防止雪崩,抓一条请求看看 tracing ,看看链路的耗时。

一个合格的处理手册交给新人也能够定位到问题点,并能解决一部分的问题。

2. 性能排查

业务逻辑的 BUG 最终可以通过日志和监控定位到 Root Case。但是不可捉摸的性能问题,才让我们头秃,这里重点讲下这我们如何排查性能问题:

2.1 工具箱

2.1.1 pprof

这是 Go 最常规也是最好用的性能排查工具,如果你还没有用过那么强烈建议阅读下官方教程这篇

在时间窗口内,CPU Profiler 会向程序注册一个定时执行的 hook(有多种手段,譬如 SIGPROF 信号),在这个 hook 内我们每次会获取业务线程此刻的 stack trace。
然后将 hook 的执行频率控制在特定的数值,在 Golang 中 是100hz(可调整),也就是每 100 个 CPU 指令采集一个业务代码的调用栈样本。当时间窗口结束后,我们将采集到的所有样本进行聚合,最终得到每个函数被采集到的次数,相较于总样本数也就得到了每个函数的相对占比

内存泄漏的定位与排查:Heap Profiling 原理解析


这里做下简单介绍 :

  • pprof 支持两种模式 ,两者并无区别,第二种可以随时 Profiling ,更加实用 。
  • runtime/pprof 对于后台类服务,嵌入到服务中,程序结束后会自动完成采样
  • net/http/pprof 提供 HTTP Hanlder 接口来生成 Profiling
  • 支持 CPU 和 内存 Profiling 。
  • Benchmark 也可以支持 Profiling go test -bench . -cpuprofile=cpu.prof

这是一个 CPU 的 profile 结果,(示例来自 你不知道的 Go 之 pprof)。这么多数据我们究竟看什么?

建议优先看 cum: 当前函数和其调用函数的开销 然后在看 flat: 当前函数开销。先看 cum 的原因在于 flat 高有可能是被调用了很多次,大部分都是系统函数。而 cum 我们可以看到一个整体,往往我们的问题代码都可以在这里看到,当然这并不是一个绝对。

当我们发现异常函数后可以通过 list 来展开函数,找出关键耗时 。

Web 指令可以打开一个后台,在这里我们可以切换到 Flame Graph 也就是我们最喜欢的火焰图,可以直观的看到调用栈和开销。面试的时候我最喜欢在这里埋坑:颜色越深是不是问题越大?

pprof 可以分析出大部分程序的性能问题。

2.1.2 trace

当 runtime 出现瓶颈,比如 goroutine 调度延迟,GC STW 过大,可以通过 trace 帮助我们查看 runtime 细节。curl host/debug/pprof/trace?seconds=10 > trace.out 这里我们生成 10s 内的数据,然后通过 go tool trace trace.out 如果数据量很大我们要等待一段时间,然后会在浏览器打开一个新的 tab 里面的数据非常有用。

我们可以通过 view trace 了解到在此期间我们的程序跑的情况如何,我们随便先一个会进入下面这个界面,我们可以通过 wsad 当缩放,在这里我们可以看到 gc 的时间,STW 的影响,函数的调用栈,goroutine 的调度。

比如示例中从收到数据到Goroutine 被唤起耗时了 4.368 毫秒,数据源于 pingcap 的一个分享。

2.1.3 Goroutine 可视化

除此之外我们可以还可以通过divan/gotrace 把 goroutine 运行时的关系渲染出来,可视化的渲染非常有趣。

2.1.4 perf

有些时候 pprof 可能会失效,比如应用程序 hang 死。比如调度打满(抢占式调度解决了这个问题)。比如我们可以通过 perf top 可以看到耗时最多的符号(Go 编译的时候会嵌入符号表,无需手动注入)。

2.1.5 瑞士军刀

Brendan gregg 大佬绘制了一个性能指南,被称为瑞士军刀。当我们怀疑 OS 问题的时候可以按图使用对应的工具,当然最有效的是喊上运维大佬们来支援。

2.2 如何优化

性能问题往往是来自多方面的,可能是最近才有即使你没有做任何变更;也可能偶尔出现;也可能是有的机器出现。我们应该做好benchmark ,任何的优化都应该有基线对比,数字是最直观的。 应用层和底层的处理逻辑往往是完全不同的,我们应该分开来思考。

2.2.1 应用层

应用层的优化应该是我们首先要想到的,也是最值得关注的。往往很多性能问题是业务逻辑设计的不合理导致的。在此之后尝试一些常规优化手段包括:

  • 资源池化,引入 sync.pool
  • 锁的收敛,控制使用范围
  • JSON 库的替换等,内存分配永远是性能杀手

Fasthttp best practices 非常值得我们学习。性能不是一招提升的,要方方面面的抠细节。

2.2.2 系统层

上述依然解决不了问题,那么恭喜你遇到了最有趣的问题,这个时候不要急于修复。尝试升级 Golang 到最新版本大概率就会解决。每个版本茫茫多的优化和修复就是解决你我所遇到的问题,你会发现你遇到的问题有着很多类似的 issue。对着优化的 MR 你会学到很多,比如 优化 tls。升级版本是奇效,升级硬件就是大力出奇迹(基建同学请忽略此条)。

系统层的优化没有银弹只有 trade-off,关闭 Swap 和 NUAM 往往要看场景。具体可以参照 redhat tuning guide

3. 演进

3.1 Continuous Profiling

当我们遇到性能问题再做 Profiling ,这是见招拆招。但是很多情况下我们无法保留事故现场,或者原因和表象在两个时间维度,这就对我们提出了更大的挑战。业内的大佬们提出了 Continuous Profiling。和 CI/CD 一样。持续不断的做采样,Google 又又又一次走到了前面 research.google/pubs/pub365…

通过 cron 周期性的做 pprof,数据做好归档,再通过 Web 界面选择任意时间的分析,甚至可以对不同时间段的 ppof 做diff 从而发现潜在的问题。Conprof 是很好用的开源替代品。

3.2 eBPF + GO

eBPF 是最近大热的动态调试技术,无侵入,无埋点的来 debug。 源于bpf ,没错就是大家熟知的 bpf( Berkeley Packet Filter),tcpdump ,wireshark 都是基于此,原本是一个网络的抓包工具。扩展可以抓 内核的包。内核通过暴露探针(probes),可以对系统做 trace。通过和 eBPF 我们可以在 pprof 失效的时候来看系统层面发生了什么

比如我们可以通过工具可以产看某个函数的耗时和延迟,也可以用来追踪调用栈

package main

import "fmt"

func main() {
        fmt.Println("Hello, BPF!")
}



# funclatency 'go:fmt.Println'
Tracing 1 functions for "go:fmt.Println"... Hit Ctrl-C to end.
^C

Function = fmt.Println [3041]
     nsecs               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 0        |                                        |
       128 -> 255        : 0        |                                        |
       256 -> 511        : 0        |                                        |
       512 -> 1023       : 0        |                                        |
      1024 -> 2047       : 0        |                                        |
      2048 -> 4095       : 0        |                                        |
      4096 -> 8191       : 0        |                                        |
      8192 -> 16383      : 27       |****************************************|
     16384 -> 32767      : 3        |****                                    |
Detaching...


示例来自 https://www.brendangregg.com/blog/2017-01-31/golang-bcc-bpf-function-tracing.html
复制代码

4. 总结

我们会面临很多奇怪的问题,也会遇到不同的挑战。既然无法控制问题的爆发,但是可以通过 Golangci-lint 等工具和 CodeReview 去避免潜在的问题,降低事故的频次和影响范围。大多数的问题要么是问题太简单没有想到,要么是太复杂那难以发现。 保持代码简洁,遵循 KISS 原则是个永不失过时的观点。

问题的修复并不只是结束,而是一切的开始。每一起严重事故的背后,必然有29次轻微事故和300起未遂先兆以及1000起事故隐患。做好复盘和改进项才是事故给我们最大的价值。

最后的如果你对性能优化非常感兴趣,你不应该错过这本 性能之巅

引用

猜你喜欢

转载自juejin.im/post/7041469280298205198