本文转载请联系网管谢bi公众号。在线服务的性能分析一直以来都是难点,主要是在性能异常的时候很难捕捉到现场信息。可能有人会说,有什么难的,用Go工具集中的pprof访问一下,取样分析就可以了。话虽然是真的,但一般不顾实际情况去谈解决办法,就很尴尬了,实在不好。举个现实中的例子,比如服务的内存使用率可能会因为代码写得不好而增加一点点。部署在K8s上,K8s集群会发现Pod的资源超过限制,会kill掉这个Pod,重新创建一个。即使可以24小时Oncall,程序也可能在接到报警的那一刻已经重启,所以这种场景下传统的主动采样访问pprof路由是不可取的。在前段时间发表的文章中,我学会了这些让Go程序自我监控的技巧。有读者问让Go程序监控自己进程的各种指标有什么用。我的回答是它可以做一些服务治理或者程序的内部分析。.今天就借此机会和大家分享一下如何对Go程序进行自动采样。下面我就带大家看看Go程序是如何采样的,然后分析如何让Go程序自动采样。Go的采样工具Go的pprof工具集提供了Go程序内部各种性能指标的采样能力。我们经常使用的性能采样指标如下:profile:CPU采样heap:采样堆中活跃对象的内存分配goroutine:当前所有goroutine的栈信息allocs:将采样程序以来所有对象的内存分配信息starts(包括已经被GC回收的内存)threadcreate:采样导致创建新系统线程的栈信息如何获取采样信息网上最常见的例子就是在server端打开端口以便客户端通过HTTP访问指定路由采样各种信息"/debug/pprof/profile",pprof.Profile)http.HandleFunc("/",index)......http.ListenAndServe(":80",nil)}$gotoolpprofhttp://localhost/debug/pprof/profile这种方式的缺点是客户端需要主动请求一个specific路由进行采样,无法在资源高峰时第一时间采样。会注册多个/debug/pprof路由,相当于部分入侵Web服务。对于非web服务,需要在服务所在节点单独开放http端口,然后注册web服务的debug路由进行收集,对原有服务的侵入性较大。runtimepprof除了通过上面的HTTP访问指定路由进行采样外,还有一个main方法是使用runtime.pprof提供的Lookup方法完成对各个资源维度的信息采样。//lookuptakesaprofilenamepprof.Lookup("heap").WriteTo(some_file,0)pprof.Lookup("goroutine").WriteTo(some_file,0)pprof.Lookup("threadcreate").WriteTo(some_file,0)CPU采样moderuntime/pprof提供了一个单独的方法在切换期间对CPU进行采样bf,err:=os.OpenFile('tmp/profile.out',os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)err=pprof.StartCPUProfile(bf)time.Sleep(2*time.Second)pprof.StopCPUProfile()这个方法操作简单,采样信息可以直接写入文件,不需要额外开端口,然后通过HTTP手动采样,但是缺点也很明显——不停的采样会影响性能。固定时间间隔采样(比如每五分钟采样一次)针对性不够,有可能采样时资源不紧张。适合采样的时间点已经通过了上面的分析。现在看来,当Go进程的资源使用量突然增加或超过某个阈值时,让Go进程使用pprof对程序Runtime进行采样是最合适的。那我们就想一想,可以用什么样的规则来判断当前周期适合采样。采样时间点CPU使用率、内存使用率和goroutine数量的判断规则都可以用数值来表示,所以无论使用率是缓慢上升直到超过阈值,还是突然上升后迅速下降,都可以是用简单的规则表示,比如:cpu/mem/goroutines的数量突然比正常情况下的平均值高出一定百分比。例如,资源使用率突然增加25%就是资源峰值。如果程序正常运行下cpu/mem/goroutine的数量超过阈值,比如80%,则定义为服务资源短缺。这两条规则可以描述大多数情况下的异常情况。第一条规则可以代表瞬时的、严重的抖动,然后可能很快恢复正常。规则2可以用来表示那些缓慢上升但最终超过阈值的情况。比如下图中的内存占用一直在缓慢上升,直到超过设定的阈值80%。如果内存使用率超过80%,规则1判断资源突然增加,需要和历史平均值进行比较。在没有历史资料的情况下,只能在程序本身收集。比如进程的内存占用,我们可以每10秒运行一次收集,在内存中保留最新的5到10个周期。内存占用,并继续与之前记录的内存占用平均值进行比较:内存占用突然增加超过25%。最近一个周期收集的数据是70%。这显然是突然增加导致的异常情况。那么我们就可以自动让程序调用pprof将这个时间点的mem信息采样到文件中。不管服务有没有重启,采样信息都可以拿下来分析。至于如何让Go程序获取系统中进程的CPU和Mem使用率,我们在上一篇让Go程序进行自我监控的文章中学习了这些技巧。可以在描述的两个采样时间执行性能采样。在开源自动采样库社区中,其实也有实现类似功能的开源库。比如曹达在还是蚂蚁的时候就设计了福尔摩斯。其实曹达在桃花源♂分享了一篇文章。使用起来也更方便。例如下面是当内存使用率突然上升25%,超过阈值80%时,让程序自动采样Mem信息的例子。如果你公司的基础设施还没有达到统一平台持续采样的水平,Holmes是更好的选择,它可以快速解决问题,更适合业务发展迅速的中小型团队。简单看一个Web服务访问Holmes的例子来分析内存使用情况。packagemainimport("net/http""time""github.com/mosn/holmes")funcinit(){http.HandleFunc("/make1gb",make1gbslice)gohttp.ListenAndServe(":10003",nil)}funcmain(){h,_:=holmes.New(holmes.WithCollectInterval("2s"),holmes.WithCoolDown("1m"),holmes.WithDumpPath("/tmp"),holmes.WithTextDump(),holmes.WithMemDump(3,25,80),)h.EnableMemDump().Start()time.Sleep(time.Hour)}funcmake1gbslice(wrhttp.ResponseWriter,req*http.Request){vara=make([]byte,1073741824)_=a}WithCollectInterval("2s")指定2s为间隔监控进程的资源使用率。在线建议设置大于10s的采样间隔。WithMemDump(3,25,80)指定进程mem占用率超过3%后(网上建议设置为30),如果突然增加25%,或者总占用率超过80%,sampling通过sampling可以得到资源突然增加时的程序调用栈,[1:1073741824]表示有一个对象消耗了1GB的内存。通过调用栈分析,我们也可以快速找到导致资源占用的代码位置。heapprofile:0:0[1:1073741824]@heap/10485760:0[1:1073741824]@0x42ba3ef0x42522540x42540950x4254fd30x425128c0x40650a1#0x42ba3eemain.make1gbslice+0x3e/Users/xargin/go/src/github.com/mosn/holmes/example/1gbslice.去:24#0x4252253net/http.HandlerFunc.ServeHTTP+0x43/Users/xargin/sdk/go1.14.2/src/net/http/server.go:2012#0x4254094net/http.(*ServeMux).ServeHTTP+0x1a4/Users/xargin/sdk/go1.14.2/src/net/http/server.go:2387#0x4254fd2net/http.serverHandler.ServeHTTP+0xa2/Users/xargin/sdk/go1.14.2/src/net/http/server.go:2807#0x425128bnet/http.(*conn).serve+0x86b/Users/xargin/sdk/go1.14.2/src/net/http/server.go:1895这个库更详细的介绍可以直接参考仓库入门教程https://github.com/mosn/holmes另外,我还做了一个docker方便实验。图像已上传到DockerHub。如果您有兴趣,可以下载并快速在您的计算机上进行测试。您可以通过以下命令快速体验。dockerrun--namego-profile-demo-v/tmp:/tmp-p10030:80--rm-dkevinyan001/go-profiling容器中Go服务提供的路由如下Holmes的三个路由实验容器分别对应路由内存、CPU过载、通道阻塞,可以直接测试访问,观察效果。除了在容器中查看自动采样的结果外,还可以在本地映射到容器的/tmp目录中找到它们。
