Go 线上服务问题排查指南

一、简介

软件开发过程中 Code Review,Lint,QA 等手段都是为了提高软件质量,减少流入到线上的 Bug。在软件的生命周期中,维护的时间远远比开发花费的多。而排查线上服务问题的手段和在本地 Debug 有一些区别,本文介绍 Go 线上服务出现问题时如何排查。

二、预防、发现、止血

在介绍排查手段之前,首先明确对待线上问题的处理原则。

  1. 预防为主:通过架构层面的容错设计,以及代码层面编码规范,辅助测试发现 Bug,提高软件质量,把问题消灭在源头
  2. 及时发现:微服务架构下,分布式系统的观测性建设非常重要。日志,监控告警,链路追踪,可以在问题出现初期及时发现,避免扩大化
  3. 优先止血:当发现问题时,优先止血是第一要务,可以采取回滚或重启等手段。排查具体原因可以在尽可能保留现场的前提下抓取线上 Profile 后续分析,或者尝试线下复现。

三、典型问题

3.1 内存泄漏

内存泄漏根据现象分类有两种:1. 临时性泄漏 2. 永久性泄漏

临时性泄漏是应用代码临时申请大量内存,使用完后又释放,这在监控面板上体现为尖刺。

永久性泄漏是应用代码申请了大量内存没释放或持续申请,内存使用量维持在一个较高的水平或持续增长,持续增长的情况下最终会触发 OOM,进程被杀死。

虚拟内存是很复杂的话题,在操作系统和业务代码之间,业务程序员一般可以忽略 GC 的存在,但是 GC 的一些行为细节,也会影响内存的实际使用情况。例如应用代码释放了内存但是 GC 没有归还给操作系统,在监控上看到内存使用量很高,但是 Heap Profile 又没有显示出应用代码使用量特别高。这是 Go 官方的 issues,1.16 版本之后该默认行为已经改善。

3.2 CPU 使用率过高

CPU 使用率过高虽然不会造成业务逻辑问题或者进程被杀死,但是会体现在时延变高,严重甚至服务不可用。所以也应该引起重视,如果能捕捉到 CPU 飙高的现场,CPU Profile 火焰图能很方便找到根源。

3.3 协程泄漏

协程泄漏的本质也是对内存资源的占用,但是因为协程是 Go Runtime 的概念,占用不会体现在 Heap Profile 里。造成协程泄漏通常有两个原因:

  1. 死锁,协程阻塞在锁或者 channel 上,不能正常结束。除了造成协程泄漏,在应用逻辑上也会体现出系统问题。
  2. 应用代码本身逻辑有问题,计算已经结束,但是协程没有正确退出。这只会单纯的浪费资源,如果泄漏的协程持续增长,最终也会 OOM。

3.5 Panic

Panic 一般会打印日志,如果业务有 Recover 打印日志,堆栈输出就在业务日志里。即使业务没有打印,Go Runtime 也会打印到标准输出,如果日志平台有采集,也能很方便的搜索到。

3.6 Network

网络堆栈的问题很少遇到。只遇到过服务发现的问题,下游服务某个容器已经重启了,但是 IP 没有从服务发现注销掉,导致上游部分请求不能正常完成。

四、排查手段

工欲善其事,必先利其器

线上服务开启 Profile 已经是业界共识,出现问题时请求接口采集 Profile,然后拉下来分析也是常规操作了。采集时不会对性能造成太大的影响。

但是出现问题时再去采集经常现场已经不在了,如果还不能复现的话,就非常难排查了。针对这种情形,结合监控,业界提出了持续 Profile 的概念,具体操作参见曹大的文章 无人值守的自动 dump(一)

除了上述两种排查的手段,在测试环境引入 Race Detect 也是非常有意义的实践,数据竞争本来就是很难排查的,如果能提前发现何必难为自己去瞎猜。

五、总结

本文简单的介绍了一些 Go 线上服务常见的问题,已经排查手段,具体实操没有详细介绍,去看看 Go 官方介绍上手非常简单。重要的还是对基础知识有一定了解,遇到问题时有大致的排查思路,具体的排查工具都是可以现学的。不知己不知的状态可能就会陷入玄学编程,「不知道为什么程序能正常运行,不知道为什么程序出现异常」。

1