調(diào)優(yōu)基本思路
- 對外接口協(xié)議不能改變
- 了解需求和代碼演進過程
- 確定資源消耗類型
- 控制運算數(shù)據(jù)輸入量
- 提高 CPU 利用率
- 提高緩存命中率
項目概況
- gin-swagger 解析使用 gin 的代碼,生成 swagger2.0 的文檔,以保證文檔和代碼的一致性。
- 使用 golang.org/x/tools/go/loader 將源碼解析成 go/types go/ast 相關(guān)結(jié)構(gòu)化數(shù)據(jù)。
- 通過遍歷 package 找到目標代碼塊及其相關(guān)數(shù)據(jù),構(gòu)建 github.com/go-openapi/spec,序列化成 JSON 格式,完成所有操作。
性能現(xiàn)狀
以 service-card 項目為例:
$ system_profiler SPHardwareDataType
Hardware:
Hardware Overview:
Model Name: MacBook Pro
Model Identifier: MacBookPro12,1
Processor Name: Intel Core i5
Processor Speed: 2.7 GHz
Number of Processors: 1
Total Number of Cores: 2
L2 Cache (per Core): 256 KB
L3 Cache: 3 MB
Memory: 8 GB
Boot ROM Version: MBP121.0167.B17
SMC Version (system): 2.28f7
Serial Number (system): C02Q560DFVH5
Hardware UUID: 9BAB7C1A-0C07-5567-808A-0694D7C2C1B6
$ cd $GOPATH/src/demo/service-card
$ time gin-swagger
gin-swagger-old -t 158.54s user 7.45s system 101% cpu 2:42.85 total
1. debugger 工具分步調(diào)試,梳理業(yè)務(wù)流程
- IDE 如 Golang/VSCode 都有相關(guān)工具或插件
- 命令行工具如 delve
- 梳理出程序運行的主要步驟:
-
loader.Load(): 掃描 service-card 代碼包括所有依賴 -
HttpErrorScanner.Scan(): 遍歷所有 package 找到代碼里定義的 HTTP 錯誤類型及其相關(guān)信息 -
RoutesScanner.Scan(): 遍歷所有 package 找到用 gin 定義的 HTTP 路由及其相關(guān)信息 - 循環(huán)調(diào)用
collectOperation(): 找到請求和響應(yīng)類型,構(gòu)建 spec.Sawgger 的 Operation - 將 spec.Swagger 序列化成 JSON 格式寫入文件
-
使用 trace 梳理資源消耗概況
標準庫中的 runtime/trace 包,用于追蹤程序運行各個階段的指標,官方使用范例
查看結(jié)果:
$ go tool trace service-card.trace


-
初步分析:
- 大部分運行過程只使用了一個線程
- 內(nèi)存開始階段陡增,中后期增速較小
- 沒有網(wǎng)絡(luò)請求
- 同步等待、系統(tǒng)調(diào)用、runtime調(diào)度的耗時操作都是 loader 庫相關(guān)
- 資源消耗特點: CPU 密集、內(nèi)存容量需求穩(wěn)定。
-
各主要步驟耗時情況:
-
loader.Load(): 7.8s HttpErrorScanner.Scan(): 7s-
RoutesScanner.Scan(): 0.5s 122 * collectOperation(): 146.6s-
json.Marshal(): 0.1s
-
pprof 查看各方法耗時
標準庫中的 runtime/pprof 包,用于整體統(tǒng)計運行過程,各個方法的總的資源消耗情況,官方使用范例
手動安裝最新版本 pprof 工具:
$ go get -u github.com/google/pprof用 web 方式查看 pprof CPU 分析結(jié)果:
$ pprof -http=":8091" ./cpu.prof-
先看 Top origin_cpu_top10.png
- 排名第一的
go/types.(*Scope).Contains這個方法耗時占比近 25.98%,代碼來自 go1.10.8 標準庫 go/types/scope.go:121
// Contains returns true if pos is within the scope's extent. // The result is guaranteed to be valid only if the type-checked // AST has complete position information. func (s *Scope) Contains(pos token.Pos) bool { return s.pos <= pos && pos < s.end }就是簡單的 int 比較,所以不是方法耗時多,而是調(diào)用次數(shù)多。
- 排名第二的
runtime.mapiternext也是標準庫遍歷 map 的方法,耗時多的原因也是調(diào)用次數(shù)多 - 依次看下來,沒有明顯的耗時過高的業(yè)務(wù)方法
- 排名第一的
初步判斷:業(yè)務(wù)方法沒有明顯缺陷,業(yè)務(wù)層面需要調(diào)用的次數(shù)過多導(dǎo)致整體耗時高
優(yōu)化第零步:持續(xù) Diff
首先使用原始版本 gin-swagger 生成 swagger 文檔,在優(yōu)化的過程中每一次修改都要確保結(jié)果和原始版本一致。
優(yōu)化第一步:提高 CPU 利用率
- 從 trace 結(jié)果發(fā)現(xiàn),122 次調(diào)用
collectOperation(),耗時占比 90%,卻是單核執(zhí)行,如果能利用多核,將有相當可觀的性能提升。 - 利用多核需要確保并發(fā)安全和兼容亂序,通過調(diào)試 collectOperation() 發(fā)現(xiàn):
- 被競爭的資源是
Swagger.Paths.Paths和Swagger.Definitions,都是插入操作 - 由于
Swagger.Paths.Paths和Swagger.Definitions是 map 類型,所以沒有亂序的問題
- 被競爭的資源是
- 給競爭資源上鎖 sync.RWMutex,保證并發(fā)安全
- 啟多個 goroutine 執(zhí)行
collectOperation() - 重新編譯執(zhí)行,文檔結(jié)果沒有 diff,耗時: 162.85s => 76s
- trace 顯示 collectOperation 階段確實是啟動了多個 Processor
- top 發(fā)生了變化,
program.Program.WithFunc和program.Program.WhereDecl兩個方法耗達到 8.5%

優(yōu)化第二步:提供緩存命中率
分析 WitchFunc
func (program *Program) WitchFunc(pos token.Pos) *types.Func {
for _, pkgInfo := range program.AllPackages {
for _, obj := range pkgInfo.Defs {
if tpeFunc, ok := obj.(*types.Func); ok {
scope := tpeFunc.Scope()
if scope != nil && scope.Contains(pos) {
return tpeFunc
}
}
}
}
return nil
}
業(yè)務(wù)邏輯:遍歷所有的 package,找到 pos 所在的
*types.Func看到熟悉身影:
scope.Contains(pos),確定是上文出現(xiàn)的go/types.(*Scope).Contains結(jié)論:大量 WitchFunc 調(diào)用,導(dǎo)致過多 go/types.(*Scope).Contains 調(diào)用,拖慢了執(zhí)行速度
-
分析業(yè)務(wù)邏輯,做緩存映射 pos => go/types.Func,即做一個
go/types.Func數(shù)組,按照 pos 排序,withFunc(pos token.Pos)邏輯轉(zhuǎn)化為:二分搜索 pos,進而確定是哪個tyeps.Func,時間復(fù)雜度:O(log2n)type fn struct { pkg *types.Package pkgInfo *loader.PackageInfo tfn *types.Func pos token.Pos } type fns []*fn func (f fns) Len() int { return len(f) } func (f fns) Less(i, j int) bool { return f[i].pos < f[j].pos } func (f fns) Swap(i, j int) { f[i], f[j] = f[j], f[i] } 重新編譯執(zhí)行,文檔結(jié)果沒有 diff,耗時: 76s => 61s
使用相同的思路構(gòu)建其他緩存 pos => ast.File, types.Func => ast.Expr
重新編譯執(zhí)行,文檔結(jié)果沒有 diff,耗時縮短到 61s => 20s
通過 trace 發(fā)現(xiàn)原來
122 * collectOperation()步驟耗時已經(jīng)縮短到 7.5s,但HttpErrorScanner.Scan()步驟還是有 6.5s 的耗時,可見已有緩存對其影響不大
優(yōu)化第三步:單步驟邏輯調(diào)優(yōu)
針對 HttpErrorScanner.Scan() 我們來分析下其火焰圖

可以看到耗時的大頭依然是 go/types.(*Scope).Contains 和 runtime.mapiternext,看業(yè)務(wù)邏輯:
1 func (scanner *HttpErrorScanner) Scan(prog *program.Program) {
2 // ... initialization
3 for pkg, pkgInfo := range prog.AllPackages {
4 for id, obj := range pkgInfo.Defs {
5 // ... do something
6 for pkgDefHttpError, httpErrorMap := range scanner.HttpErrors {
7 if pkg == pkgDefHttpError || program.PkgContains(pkg.Imports(), pkgDefHttpError) {
8 for id, obj := range pkgInfo.Uses {
9 if tpeFunc.Scope() != nil && tpeFunc.Scope().Contains(id.Pos()) {
10 if constObj, ok := obj.(*types.Const); ok {
11 if http_error_code.IsHttpCode(obj.Type()) {
12 code := constObj.Val().String()
13 if httpErrorValue, ok := httpErrorMap[code]; ok {
14 if scanner.ErrorType == nil {
15 // ... do something
16 }
17 // ... do something
- 第9行
tpeFunc.Scope().Contains(id.Pos())上有四層 for 循環(huán),估計調(diào)用次數(shù)很多 - 第 9、10、11 行連續(xù) 3 個 if 判斷,相互獨立,顯然可以調(diào)換順序。
- Scan 方法為的是找到個別類型,且數(shù)量很少,推斷第三個條件
http_error_code.IsHttpCode(obj.Type())的范圍最小,將第三個條件放到最前面,重新編譯執(zhí)行,130s,尷尬了,看來http_error_code.IsHttpCode(obj.Type())比tpeFunc.Scope().Contains(id.Pos())耗時要多得多。
http_error_code.IsHttpCode 業(yè)務(wù)代碼:
var HttpErrorVarName = "HttpErrorCode"
var StatusErrorVarName = "StatusErrorCode"
func IsHttpCode(tpe types.Type) bool {
return program.IsTypeName(tpe, HttpErrorVarName) || program.IsTypeName(tpe, StatusErrorVarName)
}
// package program
func IsTypeName(tpe types.Type, typeName string) bool {
pkgPaths := strings.Split(tpe.String(), ".")
return pkgPaths[len(pkgPaths)-1] == typeName
}
-
IsTypeName的邏輯可以簡化為tpe.String() == typeName || strings.HasSuffix(tpe.String(), "."+typeName) types.Type可以做緩存重新編譯運行,27s,看來
http_error_code.IsHttpCode(obj.Type())雖然過濾度高,但是消耗也大,看到三個 if 之一的第10行,只是一個類型判斷,消耗不大,放在第一個試試。重新編譯運行,20s => 16s
更多優(yōu)化可能
- 掃描中代碼中,原則上講,只需要參與 HTTP 接口定義的 package,目前的方案會對所有依賴庫建緩存掃描。
