Golang服务可观测和思路分享

省流

中医四诊"望闻问切"与程序诊断有异曲同工之妙。在Golang问题排查中,我们需要建立系统化的诊断思维:通过观察表象(望)、收集信息(闻)、追溯根源(问)、精准施治(切)四个维度,形成完整的诊断闭环。

引入prometheus上报

Prometheus能有效暴露线上服务实时数据,包括程序指标和运营指标,也能对指标设定阈值告警,是服务可观测的标配组件。

官方地址:https://prometheus.io/docs/prometheus/latest/getting_started/

引入jaeger链路追踪系统

链路追踪系统可以看作加强版的日志,能解决日志关联性差、信息易缺失的问题,更是分布式服务排查问题的好帮手。能对开发和线上问题调试提供重要助力。

其中数据存储方式有很多种,建议使用ES部署,并在框架提供上报频率配置。
https://www.jaegertracing.io/
探活机制

实现一些实时对服务进行检测的接口,用于异常时验证服务状态,以及使用类k8s的方式对服务进行巡检。

  1. 返回工作队列任务数。
  2. 检测DB和缓存健康。
  3. 返回服务发现状态。

望+闻:察其表象,观其脉络
第一道防线:Prometheus

通过prometheus,我们能制作进程内存、CPU等系统指标,也能对在线、QPS等业务指标进行上报监控。指标监控和日志往往是上线项目发现问题的第一现场。

当某项数值过高、过低,或变动较大时,我们可以设置告警。版本更新发布后也常通过prometheus观察发布后在线、接口报错等时候符合预期。

prometheus常结合grafana使用,prometheus负责收集服务数据,grafana负责显示数据,通过编写hql语句生成线形图。

复制代码
sum rate大法,常用于查看平均值。这里查询对2min取平均值,计算每秒每个服务、接口发生了多少次调用
sum(rate(requests_total[2m])) by (server,cmd) 

日志系统

日志系统也是日常开发用的最多的,那么一个好的日志系统应具备哪些能力?

  1. 应具备中间件,支持框架、项目手动给日志打tag,记录关键信息。默认追加调用接口名、UID等关键信息。
  2. 针对error、fatal级别日志,打印其错误堆栈、traceID等关键信息;Error级别日志做每日汇总,FATAL级别日志需立刻告警。
  3. 支持线上实时调整日志级别。
    日志和监控往往是发现问题的第一现场,但打日志对于业务是冗杂工作,会在主线开发之外增大工作量,在框架层开发中间件等机制能明显让业务开发放空大脑。

系统性能观测(vmstat/top)

vmstat能够实时输出系统的各种资源使用情况,比如进程信息、内存使用、CPU使用以及IO情况。监控系统级 资源瓶颈(CPU、内存、I/O、进程队列)。

复制代码
$ vmstat 1  # 每秒刷新
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 250000  10240 180000    0    0    50    20  100  200 30 10 55  5  0
  • procs:进程队列。r表示可运行进程数,反应CPU饱和度。b表示阻塞的进程数(例如等IO)。
  • memory:内存使用。swpd表示已用交换分区(若持续增长警惕内存不足)。free为空闲内存。
  • cpu:us:用户态,sy:内核态,id:空闲,wa:I/O 等待

top显示当前系统正在执行的进程的相关信息,包括进程ID、内存占用率、CPU占用率等。实时监控单个进程 的资源占用(CPU、内存等)。

  • load average:分别是1分钟、5分钟、15分钟的负载情况。超过18需要重点关注。
  • cpu:当sy过高时,需要考虑是否过度使用系统函数。
  • 进程信息:重点关注每个进程使用的内存、CPU。

有一次服务接口调用耗时非常高,我们排查到机器发现机器load值超过了20,进一步排查发现业务侧开启了过多time.Timer导致CPU调用忙。

资源限制诊断(ulimit/lsof)

另一类常见报错是系统资源限制,例如:too many open files,或文件描述符泄露。

复制代码
显示指定pid进程打开的fd
lsof -p

通过ulimit -n命令查看系统文件描述符上限;若确诊,通过prlimit --pid --nofile=65535:65535临时调整fd限制,再通过ulimit -n 65535修改当前会话fd限制,后续再修改系统文件保证机器重启也生效。

问:追本溯源,审证求因 ****

这里的问可能并不是问go哪里病了,而是定位问题原因,针对具体问题获取更明确的信息。

实时火焰图:pprof

复制代码
CPU火焰图生成
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30

内存分配分析
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap

通过web查看pprof火焰图
go tool pprof -http :18000 pprof.comet.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb

在第一步中我们可以通过prometheus和top发现进程病了,接下来我们可以通过go tool火焰图观察病灶在哪里,往往就能确定病灶。

  • 对于CPU问题,火焰图会显示占用CPU最高的调用在哪里。
  • 对于内存问题,火焰图会显示目前使用堆内存最高的函数调用堆栈。

实时堆栈:pprof

pprof还有一个功能是抓取当前所有go routine实时堆栈,查看有无问题。

复制代码
排查协程泄露:
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2

经典输出结果如下:

goroutine 1 [running]:

main.main()

/path/to/file.go:10 +0x45

goroutine 2 [runnable]:

runtime.gopark(0x123456, 0x0, 0x0, 0x0)

/usr/local/go/src/runtime/proc.go:307 +0x89

...

  1. 阻塞分析:特别关注状态为 [semacquire]、[IO wait] 或 [select] 的 Goroutine
  • IO Wait的go routine可以认为是在等待网络回包。可能是DB或RPC连接。
  • semacquire状态的协程一般是在等待锁,可以根据堆栈判断是在竞争哪里的锁。重点注意阻塞时间。
  • select就是我们写的select,在等待channel,重点注意阻塞时间。
    所有 Goroutine 都处于等待状态时可能发生死锁。
  1. 泄漏检测:长期处于 [runnable] 但无实际工作的 Goroutine 可能是泄漏
  • 占用大量CPU资源的罪魁祸首

更详细的日志:Jaeger

链路追踪系统用于将请求中发生的离散事件记录并连接。

比如一个简单的请求会从:gatesrv->gamesrv->db,我们可以给UID、RedisKey等关键指标打上标签,文本中记录请求、响应内容,记录从DB中读出了什么,并对出现error的接口进行定向上报。将记录和上报放置于框架中,业务开发时无需操作即可完成自定义集成。

这将极大提高测试环境开发以及线上定位问题的效率:当请求和响应,中间发生的任何rpc和db调用都一目了然,又能对函数调用进行自定义补充时,我们能掌控接口的所有。

jaeger在公司也是战功赫赫:

  • 项目出现诡异报错,通过对指定UID进行灰度抓取,发现其DB数据有问题。
  • 业务时常反馈第三方登录、支付偶现报错,通过jaeger快速发现第三方报错。
  • 开发时减少bug定位修复时间,调用中发生了什么都心中有数。

系统工具(tcpdump/netstat)

针对网络问题,我们往往通过抓包直接获取原始数据。

复制代码
tcpdump -i any tcp -nn 'tcp port 9096' -w game_20240110.pcap

抓到包后,直接扔到Wireshark进行分析。

  1. 连接问题,检查是否有断连、重连,检查握手是否正常,检查断开连接。
  • 客户端SYN无响应 → 服务未监听/防火墙拦截
  • SYN后立即收到RST → 服务崩溃或拒绝连接
  1. 检查消息收发。出现异常也可能会导致C/S主动断开连接。
  • 请求后无响应 → 服务端未处理
  • 连续包间隔 >200ms → 网络延迟或应用处理慢
  • 检查是否回空包。

有一次遇到线上问题,现象为部分接口回空包并直接关闭连接。

通过tcpdump定位到是proxy,proxy设置http超时时间为3s,但上传文件消耗时间过长,导致该请求在代理被认为是超时,连接被关闭。

切:精准把脉,对症下药
综合案例:计时器滥用诊治

望:接口耗时一路飚高,但观察到DB耗时没有异常。且部分队列出现 "queue busy"触发熔断,观察到CPU也在一路增高,通过top发现服务"us"调用明显高。重启后过短暂恢复,但会继续呈现一路增高。

问:通过查pprof,我们发现CPU耗时指向计时器------但正常情况下大部分CPU会集中在proto的序列化,我们还发现go routine在不断增多。通过查阅业务代码,定位为计时器滥用。

综合案例:网络问题诊治

望:公司有个客户端日志收集服务,接口之一需要上传日志文件,耗时大部分为500ms。测试阶段一切正常,上线后发现有时需要的日志没传上来。通过检查服务日志,发现日志服务正常响应了,

问:我们模拟了巨大文件的上传,并用tcpdump抓取了回包,发现网关层回了空包。检查代码发现我们使用proxy时,给http.transport设置了1s的超时。线上网络环境非常复杂,时常就会上传耗时超过1s。

综合案例:业务BUG

望:这个BUG不会看到接口异常或耗时增加,但会损伤玩家利益。线上玩家反馈,进行游玩有时会丢棋盘上的道具。该功能项目的实现是,客户端进行一系列合成操作并生成结果,并周期性地将合成操作和棋盘同步到服务器,若出现检查异常,服务器将对客户端操作进行回滚。

棋盘同步会有大量的请求,通过日志难以完整复现玩家操作。

问:项目打开了链路追踪的UID尾号采样机制,对出现棋盘回滚+被尾号命中的玩家一系列请求进行分析,发现客户端和服务器在某步出现逻辑不一致。