一般来说,项目开发过程中,代码编写占开发总时间的40%,剩下的时间基本就是自测和联调的过程。程序出错很正常,关键是如何迅速的去定位它,修掉它。本文将介绍自己程序调试的一些常用方法,这边我以golang为例,总结为望问切闻---debug四部曲。
第一步先看看程序的外部指标,如进程启动关系,系统调用的使用,消耗的内存,cpu,磁盘io,文件句柄连接数,网络连接情况等等资源是否符合预期。
内存,cpu部分我们可以用工具top查看,-d 指定每两次屏幕信息刷新之间的时间间隔(单位为秒);-H 显示所有线程的运行状态指标。如果没有该参数,会显示一个进程中所有线程的总和。在运行过程中,可以通过H命令进行交互控制:
进程启动关系可以用ps auxf查看,可以看到进程启动的时间以及进程调用的树形图:
跟踪程序的系统调用情况,可以使用strace。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间:
网络的使用情况可以用命令ss查看,ss 命令可以用来获取 socket 统计信息,它显示的内容和 netstat 类似。但 ss 的优势在于它能够显示更多更详细的有关 TCP 和连接状态的信息,而且比 netstat 更快。当服务器的 socket 连接数量变得非常大时,无论是使用 netstat 命令还是直接 cat /proc/net/tcp,执行速度都会很慢。
查看使用的文件句柄,ring队列,共享内存等等,可以使用lsof命令,它主要用来获取被进程打开文件的信息。
对于磁盘io的监控,我们可以采用iostat。它的特点是汇报磁盘活动统计情况,同时也会汇报出CPU使用情况。iostat也有一个弱点,就是它不能对某个进程进行深入分析,仅对系统的整体情况进行分析。
我们在工位中经常听到过如下的自言自语---"不可能,之前程序跑的还好好的啊?现在怎么会出问题呢?",每次听到这种情况,我都会安慰同事说一定是太阳黑子辐射的原因,导致程序跑错了,哈哈,开个玩笑。
一般出现这种情况,都是由于近期出现了改动导致的,你需要做的首先查看,是不是最近修改bug然后新引入了一个新的bug,如果确定不是自己的问题,那么一个是要问,关联的服务(上下游)是否最近合入过什么功能,另一个是问,是不是最近数据库发生了变动。
能否顺利定位出程序的bug所在,这步是最为关键的了。一般是看两个地方,一个是日志流(debug日志),一个是dlv attach进去调试,查看具体的调用栈。
那么如何打好日志呢,首先就是一定要做好分类,比如logrus库它有5个级别:
// 排查问题时打印,常见的用法是对函数的返回值做判断,非预期的返回值就打印debug日志
logrus.Debug("Useful debugging information.")
//info 出现关键事件时的打印,如程序初始化成功或者作为跟踪日志,
//一般在每个函数的入口和出口打印,用以跟踪程序的执行流
logrus.Info("Something noteworthy happened!")
// 当出现非预期的逻辑,但业务还是可以继续处理的情况,如web服务请求数超过指定的阈值
logrus.Warn("You should probably take a look at this.")
logrus.Error("Something failed but I'm not quitting.")
logrus.Fatal("Bye.") //log之后会调用os.Exit(1)
logrus.Panic("I'm bailing.")
其次,打印日志结构体的时候不要用%+v或者在经常跑的地方不要加日志打印,这会导致刷屏,眼睛直接瞎啦。一般通过info级别的日志基本就能确定程序的数据流走向,变量和服务调用情况是否已经符合预期了。
dlv工具和C语言的gdb很像,也可以attach进去通过打断点的方式查看变量和调用栈,或者程序core dump的时候,可以通过dlv core的方式获取到所有的goroutine和frame上下文信息。golang产生core是需要在编译前配置的,配置方法如下:
unlimit -a unlimited
GOTRACEBACK=crash ./crash
当然有的时候出现段错误的时候,golang是会打印出崩溃处的堆栈,如果没有的话,一般就是被其他程序kill掉了,我们可以用mv大法,在调用/usr/bin/kill之前,先加一点信息重定向到文本:
mv /usr/bin/kill /usr/bin/has.log.kill
#!/bin/sh
date >> /var/crash/kill.log
ps auxf >> /var/crash/kill.log
/usr/bin/has.log.kill $*
chmod +x usr/bin/has.log.kill
alias kill=/usr/bin/has.log.kill
dlv使用方法可以参考这篇文章:
GO语言调试利器dlv快速上手 - 博客园 (cnblogs.com)
结合以上几个步骤的定位,我们基本可以胸有成足了,这个时候打开代码,好好review一番。如果还是搞不定,这个时候只能用杀手锏pprof了,它是go性能分析神器,相当于在你的程序中暴露一个http服务端口,让你可以获得以下程序运行信息:
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
// 你的业务代码
}()
http.ListenAndServe("0.0.0.0:7001", nil)
}
如果还是搞不定啊,那我建议就是摇人啦,拉个会议室,点几杯奶茶,叫上几个大佬一起review一下。
Linux top命令详解 - 小a玖拾柒 - 博客园 (cnblogs.com)
linux top swap 为0,Linux:top_逃命的饼干的博客-CSDN博客
Go进阶10:logrus日志使用教程 | Go&Rust (mojotv.cn)
Golang性能测试工具PProf应用详解 - 知乎 (zhihu.com)
shell脚本中$0 $1 $# $@ $* $? $$ 的各种符号意义详解_一口Linux的博客-CSDN博客_shell脚本$@
linux 用strace查看系统调用 - jasononline - 博客园 (cnblogs.com)
linux ss命令详解 - 腾讯云开发者社区-腾讯云 (tencent.com)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。