這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
我剛剛寫好新的部落格程式 Pugo,歡迎試用和體驗。這兩天我把個站 fuxiaohei.me 遷移到新的部落格程式。但是,經過一天的運行,發現記憶體從啟動的 14MB 上升到了 228 MB。顯然程式發生記憶體泄露,所以也開始以下調優過程。
PPROF
pprof 是 Golang 內建的調試工具,有很多可用的工具。pprof 的調試方式有代碼的方式和 HTTP 方式。其中 HTTP 調試比較方便,加入很簡單的代碼:
import _ "net/http/pprof" // pprof 的 http 路由註冊在內建路由上go func() { http.ListenAndServe("0.0.0.0:6060", nil) // 啟動預設的 http 服務,可以使用內建的路由}()
訪問 http://localhost:6060/debug/pprof/ 就可以查看 pprof 提供的資訊。分析記憶體使用量,可以關注 heap 上分配的變數都是哪些內容,訪問 http://localhost:6060/debug/pprof/heap?debug=1 可以看到的資料:
來自代碼 github.com/syndtr/goleveldb/leveldb/memdb.New 的對象在 heap 上最多。 Pugo 的資料庫底層是 基於 goleveldb 儲存的 tidb 資料庫。 goleveldb 有類似於 leveldb 的行為,就是半記憶體半儲存的資料分布。因而,有比較大量的記憶體對象是正常現象。但是使用 go tool 的時候發現了別的問題:
go tool pprof http://localhost:6060/debug/pprof/heap
go tool 暫存下當時的 heap 快照用於分析。同時進入了 pprof 工具,用命令:
top -10
展示佔用最多的 10 個對象堆
reflect.Value.call 是 heap 上最多的調用,呵呵。問題落在標準庫上,可能就是 golang 標準庫的問題。我本機還是 Go 1.5版本。試著更新了一下 Go 1.5.1 後,發現 heap 上的資料分布沒有什麼變化。那就不是標準庫的問題。
深入分析
既然不是標準庫的問題,就是調用reflect.Value.call的上級出現問題。用命令產生 svg 過程圖到瀏覽器:
web
時序圖中明顯有問題的部分:
發現 tango.(*Context).Next 是調度的上級。但是 Next() 方法源碼中沒有 reflect 的調用過程,不夠明確。用另一個命令輔助:
peek reflect.Value.Call
有圖:
可以看到上下文方法 tango.(*Context).Invoke,代碼中發現:
if ctx.action != nil { var ret []reflect.Value switch fn := ctx.route.raw.(type) { case func(*Context): fn(ctx) case func(*http.Request, http.ResponseWriter): fn(ctx.req, ctx.ResponseWriter) case func(): fn() case func(*http.Request): fn(ctx.req) case func(http.ResponseWriter): fn(ctx.ResponseWriter) default: ret = ctx.route.method.Call(ctx.callArgs) // 調用 reflect.Value.Call 的地方 } if len(ret) == 1 { ctx.Result = ret[0].Interface() } else if len(ret) == 2 { if code, ok := ret[0].Interface().(int); ok { ctx.Result = &StatusResult{code, ret[1].Interface()} } } // not route matched} else { if !ctx.Written() { ctx.NotFound() }}
把這個位置反饋給tango的作者 lunny 後,最終定位的問題在 router 池的構造方法:
func newPool(size int, tp reflect.Type) *pool { return &pool{ size: size, cur: 0, pool: reflect.MakeSlice(reflect.SliceOf(tp), size, size), // 這個地方申請了大記憶體 tp: reflect.SliceOf(tp), stp: tp, }}
reflect.MakeSlice 的 size 預設值是 800, 也就是創造了存有一個長度800的slice的pool,記憶體一直在不停增長。然後 pool 中存有的 reflect.Value 一直被調用,所以 heap 可以看到調度資訊。修改 size 預設值到 10 左右,一切就正常啦。
總結
Golang 本身提供的 profile 工具很好,可以提供很多的資訊。然後,我經過對代碼的分析和追蹤,發現問題所在。調試和最佳化是工作中經常遇到的事情。每一次分析過程都為自己積累了思考的方式和修改的經驗。