桔妹导读:线上性能问题的定位和优化是程序员进阶的必经之路,定位问题的方式有多种多样,常见的有观察线程栈、排查日志和做性能分析。性能分析(profile)作为定位性能问题的大杀器,它可以收集程序执行过程中的具体事件,并且对程序进行抽样统计,从而能更精准的定位问题。本文会以 go 语言的 pprof 工具为例,分享两个线上性能故障排查过程,希望能通过本文使大家对性能分析有更深入的理解。
-
如何通过做 profiling 来精准定位故障源头 -
两个工作中通过 profiling 解决性能问题的实际例子 -
总结在做 profiling 时如何通过一些简单的现象来快速定位问题的排查方向 -
日常 golang 编码时要避开的一些坑 -
部分 golang 源码解析
1.
profiling是什么
2.
两次 profiling 线上实战
▍cpu 占用 99%
-
cpu idle 基本掉到了 0% ,内存使用量有小幅度增长但不严重; -
故障是偶发的,不是持续存在的; -
故障发生时3台机器的 cpu 几乎是同时掉底; -
故障发生后,两个小时左右能恢复正常。
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("0.0.0.0:8005", nil))
}()
// ..... 下面业务代码不用动
}
brew install graphviz # 安装graphviz,只需要安装一次就行了 go tool pprof -http=:1234 http://your-prd-addr:8005/debug/pprof/profile?seconds=30
go tool pprof http://your-prd-addr:8005/debug/pprof/heap
[net/http.HandlerFunc.ServeHTTP/server.go:1947] _com_request_in||traceid=091d682895eda2fsdffsd0cbe3f9a95||spanid=297b2a9sdfsdfsdfb8bf739|| hintCode=||hintContent=||method=GET||host=10.88.128.40:8000||uri=/lp-api/v2/leadCallRecord/getLeadCallRecord||params=leadId={"id":123123}|| from=10.0.0.0||proto=HTTP/1.0
func GetLeadCallRecord(leadId string, bizType int) ([]model.LeadCallRecords, error) { sql := "SELECT record.* FROM lead_call_record AS record " + "where record.lead_id = {{leadId}} and record.biz_type = {{bizType}}" conditions := make(map[string]interface{}, 2) conditions["leadId"] = leadId conditions["bizType"] = bizType cond, val, err := builder.NamedQuery(sql, conditions)
-
cpu idle 基本掉到了 0% ,内存使用量有小幅度增长但不严重; -
故障是偶发的,不是持续存在的; -
故障发生时3台机器的 cpu 几乎是同时掉底; -
故障发生后,两个小时左右能恢复正常。
-
GetLeadCallRecordByLeadId 函数每次在执行时从数据库取回的数据量过大,大量cpu时间浪费在反序列化构造对象 和 gc 回收对象上。 -
和前端确认 /lp-api/v2/leadCallRecord/getLeadCallRecord 接口并不是所有请求都会传入json,只在某个页面里才会有这种情况,所以故障是偶发的。 -
因为接口并没有直接挂掉报错,而是执行的很慢,所以应用前面的负载均衡会超时,负载均衡器会把请求打到另一台机器上,结果每次都会导致三台机器同时爆表。 -
虽然申请了上亿个对象,但 golang 的垃圾回收器是真滴靠谱,兢兢业业的回收了两个多小时之后,就把几亿个对象全回收回去了,而且奇迹般的没有影响线上业务。几亿个对象都扛得住,只能说厉害了我的go。
▍内存占用 90%
-
cpu占用并不高,最低idle也有85% -
内存占用呈锯齿形持续上升,且速度很快,半个月就从2G达到了15G
var ( firstActivationEventHandler FirstActivationEventHandler firstOnlineEventHandler FirstOnlineEventHandler ) func GetInstance(eventType string) Handler { if eventType == FirstActivation { firstActivationEventHandler.ChildHandler = firstActivationEventHandler return firstActivationEventHandler } else if eventType == FirstOnline { firstOnlineEventHandler.ChildHandler = firstOnlineEventHandler return firstOnlineEventHandler } // ... 各种类似的判断,略过 return nil }
-
firstActivationEventHandler.ChildHandler 是一个 interface,在给一个 interface 赋值的时候,如果等号右边是一个 struct,会进行值传递,也就意味着每次赋值都会在堆上复制一个此 struct 的副本。(golang默认都是值传递) -
firstActivationEventHandler.ChildHandler = firstActivationEventHandler 是一个自己引用自己循环引用。
func init() { firstActivationEventHandler.ChildHandler = &firstActivationEventHandler firstOnlineEventHandler.ChildHandler = &firstOnlineEventHandler // ... 略过 }
// interface 底层定义 type iface struct { tab *itab data unsafe.Pointer } // 空 interface 底层定义 type eface struct { _type *_type data unsafe.Pointer } // 将某变量转换为interface func convT2I(tab *itab, elem unsafe.Pointer) (i iface) { t := tab._type if raceenabled { raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I)) } if msanenabled { msanread(elem, t.size) } x := mallocgc(t.size, t, true) typedmemmove(t, x, elem) i.tab = tab i.data = x return }
3.
经验总结
在做内存问题相关的 profiling 时:
-
若 gc 相关函数占用异常,可重点排查对象数量
-
解决速度问题(CPU占用)时,关注对象数量( –inuse/alloc_objects )指标
-
解决内存占用问题时,关注分配空间( –inuse/alloc_space )指标
-
inuse 代表当前时刻的内存情况,alloc 代表从从程序启动到当前时刻累计的内存情况,一般情况下看 inuse 指标更重要一些,但某些时候两张图对比着看也能有些意外发现。
在日常 golang 编码时:
-
参数类型要检查,尤其是 sql 参数要检查(低级错误) -
传递struct尽量使用指针,减少复制和内存占用消耗(尤其对于赋值给interface,会分配到堆上,额外增加gc消耗) -
尽量不使用循环引用,除非逻辑真的需要 -
能在初始化中做的事就不要放到每次调用的时候做
–
–
–
评论前必须登录!
注册