当前位置: 首页 > 科技观察

必学的Go进程诊断工具gops

时间:2023-03-19 01:43:07 科技观察

本文转载自微信公众号《大脑是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。在类Unix系统中,我们经常使用ps命令来查看系统当前运行的进程信息。该命令为我们提供了很大的帮助,可以快速定位到某些进程的运行状态和状态。在Go语言中,有一个类似的命令工具,那就是gops[1](GoProcessStatus)。gops是谷歌官方出品的命令行工具。它具有与ps命令类似的功能。可以查看和诊断当前系统中围棋程序的运行状态和内部情况。它在一些使用场景中有很大的意义,是一种常用的工具。.在本文中我们将对gops进行全面的使用和介绍。基本使用我们先创建一个示例工程,然后在工程根目录下执行如下模块安装命令:$goget-ugithub.com/google/gops写入如下启动代码:import(..."github.com/google/gops/agent")funcmain(){//创建并监控gopsagent,gops命令会通过连接agent读取进程信息//如果需要远程访问,可以配置agent.Options{Addr:"0.0.0.0:6060"},否则默认只允许本地访问iferr:=agent.Listen(agent.Options{});err!=nil{log.Fatalf("agent.Listenerr:%v",err)}http.HandleFunc("/hello",func(whttp.ResponseWriter,r*http.Request){_,_=w.Write([]byte(`Go语言编程之旅`))})_:=http.ListenAndServe(":6060",http.DefaultServeMux)}示例启动代码写完后,我们启动程序,在命令行执行gops命令查看:37393725main*go1.14/private/var/folders/jm/.../b001/exe/main372571093gogo1.14/usr/local/Cellar/go/1.14/libexec/bin/go6235746131gogo1.14/usr/local/Cellar/go/1.14/libexec/bin/go38723742gopsgo1.14/Users/eddycjy/go/bin/gops6237962357maingo1。14/private/var/folders/jm/.../b001/exe/main...在上面的输出中,你很快会发现有区别,这就是为什么某行的输出中包含一个*的原因symbol如下:37393725main*go1.14/private/var/folders/jm/.../b001/exe/main实际代表Go进程,agentisincludedsoitcanenablemorepowerfuldiagnostics,includingcurrentstacktrace,Goversion,memorystatistics,etc.AttheendthereisalsoamainGoprocess,whichdoesnotcontainthe*symbol,whichmeansitisanormalTheGoprogram,thatis,noagentisembedded,canonlyusethemostbasicfunctions.常规命令gops工具包含了大量的分析命令,我们可以通过gopshelp进行查看:$gopshelpgopsisatooltolistanddiagnoseGoprocesses.Usage:gops...gops#displaysprocessinfogopshelp#displaysthishelpmessageCommands:stackPrintsthestacktrace.gcRunsthegarbagecollectorandblocksuntilsuccessful.setgcSetsthegarbagecollectiontargetpercentage.memstatsPrintstheallocationandgarbagecollectionstats.versionPrintstheGoversionusedtobuildtheprogram.statsPrintsruntimestats.traceRunstheruntimetracerfor5secsandlaunches"gotooltrace".pprof-heapReadstheheapprofileandlaunches"gotoolpprof".pprof-cpuReadstheCPUprofileandlaunches"gotoolpprof".在接下来的小节中,我们将针对几个常用的分析功能进行概要分析。查看指定进程信息$gopsparentPID:3725threads:7memoryusage:0.042%cpuusage:0.003%username:eddycjycmd+args:/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/go-build943691423/b001edtime/exe5local//exeremote:127.0.0.1:59369<->:0(LISTEN)local/remote:*:6060<->:0(LISTEN)获取Go进程的概要信息,包括父PID、线程数、内存/CPU使用率、运营商的账户名、进程的启动命令行参数、启动后经过的时间、gops的代理监控信息(没有植入代理就没有这些信息)。查看调用栈信息$gopsstack3739goroutine19[running]:runtime/pprof.writeGoroutineStacks(0x1385aa0,0xc000132038,0x30,0xd0).../Users/eddycjy/go/src/github.com/google/gops/agent/agent.go:185+0x1afgithub.com/google/gops/agent.listen()/Users/eddycjy/go/src/github.com/google/gops/agent/agent.go:133+0x2bfcreatedbygithub.com/google/gops/agent。听/Users/eddycjy/go/src/github.com/google/gops/agent/agent.go:111+0x36bgoroutine1[IOwait]:internal/poll.runtime_pollWait(0x2f55e38,0x72,0x0)/usr/local/Cellar/go/1.14/libexec/src/runtime/netpoll.go:203+0x55...获取对应进程的代码调用栈信息,可用于分析调用链路。查看内存使用情况$gopsmemstats3739alloc:1.15MB(1205272bytes)total-alloc:1.15MB(1205272bytes)sys:69.45MB(72827136bytes)lookups:0mallocs:644frees:12heap-alloc:1.15MB(1205272MB63bytes)heap4(1205272MB63bytes)heap4(1205272MB63bytes)heap4(1205272MB63bytes)heap4(1205272MB63bytes)heap4(1267bytes)heap4-idle:62.05MB(65060864bytes)heap-in-use:1.61MB(1687552bytes)heap-released:62.02MB(65028096bytes)heap-objects:632...获取Go当前运行时的内存使用情况,主要是相关字段信息runtime.MemStats[2]。查看运行时信息$gopsstats3739goroutines:2OSthreads:8GOMAXPROCS:4numCPU:4获取Go运行时的基本信息,包括当前的Goroutine数、系统线程数、GOMAXPROCS值、当前系统的CPU核数。查看跟踪信息$gopstrace3739Tracingnow,willtake5secs...Tracedumpsavedto:/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/trace092133110Parsingtrace...Splittingtrace...Openingbrowser.Traceviewerislisteningonhttp://127.gool。查看profile信息$gopspprof-cpu3739ProfilingCPUnow,willtake30secs...Profiledumpsavedto:/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/profile563685966Binaryfilesavedto:/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/binary265411413File:binary265411413Type:cpu...(pprof)$gopspprof-heap3739Profiledumpsavedto:/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/profile967076057Binaryfilesavedto:/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/binary904879716File:binary904879716Type:inuse_space...(pprof)与gotoolpprof作用基本一致。Howdoyou知道我是谁在学习了gops的使用之后,我们突然发现了一个问题,那就是gops如何知道哪些进程是Go相关的进程呢?如果是植入了代理的应用,可以理解为Buriedrecognitionpoints。但实际情况是没有植入agent的Go程序也被识别,这说明gops本身并不是这样实现的。考虑到植入的agent应该只是用于诊断信息的扩展,而不是识别点,那么什么是gops呢?如何找出哪些进程与Go相关?我们回到问题的前置要求。假设我们想知道哪些进程与Go有关。第一步是要知道我们当前系统中有哪些进程正在运行,这些记录在哪里?仔细想想,答案就出来了。假设是Linux相关的系统,它会按照约定的数据结构,将进程的所有相关信息写入/proc目录,所以我们有足够的疑问,gops是从/proc目录中读取相关信息,出处代码如下:funcPidsWithContext(ctxcontext.Context)([]int32,error){varret[]int32d,err:=os.Open(common.HostProc())iferr!=nil{returnil,err}deferd.Close()fnames,err:=d.Readdirnames(-1)iferr!=nil{returnil,err}for_,fname:=rangefnames{pid,err:=strconv.ParseInt(fname,10,32)iferr!=nil{continue}ret=append(ret,int32(pid))}returnret,nil}//common.HostProcfuncHostProc(combineWith...string)string{returnGetEnv("HOST_PROC","/proc",combineWith...)}上述代码中,该方法通过调用os.Open方法打开proc目录,并使用Readdirnames方法对该目录进行扫描,最终获取到所有需要的pid,最后完成其使命。返回所有pid。在确认gops是通过扫描/proc目录获取到进程信息后,我们又遇到了一个新的疑点,即gops是如何判断这个进程是Go进程的,又是如何知道它的具体版本信息的呢?代码如下:funcisGo(prps.Process)(path,versionstring,agent,okbool,errerror){...path,_=pr.Path()iferr!=nil{return}varversionInfogoversion.VersionversionInfo,err=goversion.ReadExe(路径)iferr!=nil{return}ok=trueversion=versionInfo.Releasepidfile,err:=internal.PIDFile(pr.Pid())iferr==nil{_,err:=os.Stat(pidfile)agent=err==nil}returnpath,version,agent,ok,nil}可以看出,该方法的主要作用是在扫描/proc目录得到的二进制文件地址中查找相关标识,判断是否是是一个Go程序,如果是Go程序,那么会返回进程的pid,二进制文件的名字,以及二进制文件的完整存放路径。判断的标识如下:ifname=="runtime.main"||name="main.main"{isGo=true}ifname=="runtime.buildVersion"{isGo=true}至于版本号编译Go语言后,Go编译器会在二进制文件中加入runtime.buildVersion标志,可以快速识别其Compile信息,gops正是利用了这一点。我们可以使用gdb查看Go编译出来的二进制文件的版本信息,如下:$exportGOFLAGS="-ldflags=-compressdwarf=false"&&gobuild.$gdbawesomeProject...(gdb)p'runtime.buildVersion'$1=0x131bbb0"go1.14"在上面的输出中,我们首先编译了示例工程,然后查看了gdb中的runtime.buildVersion变量,最后发现编译出来的Go程序的版本是Go1.14。但是在编译的时候需要注意一点,就是我们在编译的时候指定了exportGOFLAGS="-ldflags=-compressdwarf=false"参数。如果不指定,会出现ReadingsymbolsfromawesomeProject...(nodebuggingsymbolsfound)...done.相关的错误,会影响部分功能的使用。这是因为从Go的1.11版本开始,调试信息被压缩以减少编译后的二进制文件的大小,但是Mac上的gdb无法理解压缩后的DWARF,从而导致问题。调试时需要指定不做DWARF压缩,方便Mac上的gdb。需要注意的一点假设我们要在一些特殊场景下对Go编译出来的二进制文件进行压缩,那么我们往往会在最后使用upx工具来减小它的整体大小,命令如下:$upxawesomeProject这时候,我们会重新运行所有编译好的awesomeProject文件,这个时候需要考虑的是,gops能不能识别为Go程序呢?答案是否定的,upx压缩后的二进制文件不会被识别为Go程序,而在gopsv0.3.7版本中,由于这种加壳进程的存在,直接导致了gops命令的执行由空指针调用的恐慌(panic)。很明显,这是一个BUG。在实际环境中需要多加注意。如果你想使用gops尽量不要使用upx进行压缩。总结在本文中,我们对Google官方出品的gops的基本用法和原理进行了部分分析。如果仔细研究,你会发现gops几乎包含了大部分Go分析工具的功能,是名副其实的进程诊断工具。gops集成了大量围棋界常用的分析链,排查问题也很方便。不需要一一寻找具体的工具,只需要使用gops即可,可以根据实际情况进行更深层次的使用。了解更多。参考文献[1]gops:https://github.com/google/gops[2]runtime.MemStats:https://golang.org/pkg/runtime/#MemStats