調(diào)優(yōu)基本思路
- 對(duì)外接口協(xié)議不能改變
- 了解需求和代碼演進(jìn)過程
- 確定資源消耗類型
- 控制運(yùn)算數(shù)據(jù)輸入量
- 提高 CPU 利用率
- 提高緩存命中率
項(xiàng)目概況
- gin-swagger 解析使用 gin 的代碼乒融,生成 swagger2.0 的文檔,以保證文檔和代碼的一致性客叉。
- 使用 golang.org/x/tools/go/loader 將源碼解析成 go/types go/ast 相關(guān)結(jié)構(gòu)化數(shù)據(jù)。
- 通過遍歷 package 找到目標(biāo)代碼塊及其相關(guān)數(shù)據(jù)买喧,構(gòu)建 github.com/go-openapi/spec港粱,序列化成 JSON 格式,完成所有操作禁炒。
性能現(xiàn)狀
以 service-card 項(xiàng)目為例:
$ 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
- 梳理出程序運(yùn)行的主要步驟:
-
loader.Load()
: 掃描 service-card 代碼包括所有依賴 -
HttpErrorScanner.Scan()
: 遍歷所有 package 找到代碼里定義的 HTTP 錯(cuò)誤類型及其相關(guān)信息 -
RoutesScanner.Scan()
: 遍歷所有 package 找到用 gin 定義的 HTTP 路由及其相關(guān)信息 - 循環(huán)調(diào)用
collectOperation()
: 找到請(qǐng)求和響應(yīng)類型胖腾,構(gòu)建 spec.Sawgger 的 Operation - 將 spec.Swagger 序列化成 JSON 格式寫入文件
-
使用 trace 梳理資源消耗概況
標(biāo)準(zhǔn)庫中的 runtime/trace 包,用于追蹤程序運(yùn)行各個(gè)階段的指標(biāo),官方使用范例
查看結(jié)果:
$ go tool trace service-card.trace
-
初步分析:
- 大部分運(yùn)行過程只使用了一個(gè)線程
- 內(nèi)存開始階段陡增咸作,中后期增速較小
- 沒有網(wǎng)絡(luò)請(qǐng)求
- 同步等待锨阿、系統(tǒng)調(diào)用、runtime調(diào)度的耗時(shí)操作都是 loader 庫相關(guān)
- 資源消耗特點(diǎn): CPU 密集记罚、內(nèi)存容量需求穩(wěn)定墅诡。
-
各主要步驟耗時(shí)情況:
-
loader.Load()
: 7.8s HttpErrorScanner.Scan()
: 7s-
RoutesScanner.Scan()
: 0.5s 122 * collectOperation()
: 146.6s-
json.Marshal()
: 0.1s
-
pprof 查看各方法耗時(shí)
標(biāo)準(zhǔn)庫中的 runtime/pprof 包,用于整體統(tǒng)計(jì)運(yùn)行過程桐智,各個(gè)方法的總的資源消耗情況末早,官方使用范例
手動(dò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
這個(gè)方法耗時(shí)占比近 25.98%,代碼來自 go1.10.8 標(biāo)準(zhǔn)庫 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 }
就是簡(jiǎn)單的 int 比較酵使,所以不是方法耗時(shí)多荐吉,而是調(diào)用次數(shù)多。
- 排名第二的
runtime.mapiternext
也是標(biāo)準(zhǔn)庫遍歷 map 的方法口渔,耗時(shí)多的原因也是調(diào)用次數(shù)多 - 依次看下來,沒有明顯的耗時(shí)過高的業(yè)務(wù)方法
- 排名第一的
初步判斷:業(yè)務(wù)方法沒有明顯缺陷穿撮,業(yè)務(wù)層面需要調(diào)用的次數(shù)過多導(dǎo)致整體耗時(shí)高
優(yōu)化第零步:持續(xù) Diff
首先使用原始版本 gin-swagger 生成 swagger 文檔缺脉,在優(yōu)化的過程中每一次修改都要確保結(jié)果和原始版本一致。
優(yōu)化第一步:提高 CPU 利用率
- 從 trace 結(jié)果發(fā)現(xiàn)悦穿,122 次調(diào)用
collectOperation()
攻礼,耗時(shí)占比 90%,卻是單核執(zhí)行栗柒,如果能利用多核礁扮,將有相當(dāng)可觀的性能提升。 - 利用多核需要確保并發(fā)安全和兼容亂序瞬沦,通過調(diào)試 collectOperation() 發(fā)現(xiàn):
- 被競(jìng)爭(zhēng)的資源是
Swagger.Paths.Paths
和Swagger.Definitions
太伊,都是插入操作 - 由于
Swagger.Paths.Paths
和Swagger.Definitions
是 map 類型,所以沒有亂序的問題
- 被競(jìng)爭(zhēng)的資源是
- 給競(jìng)爭(zhēng)資源上鎖 sync.RWMutex逛钻,保證并發(fā)安全
- 啟多個(gè) goroutine 執(zhí)行
collectOperation()
- 重新編譯執(zhí)行僚焦,文檔結(jié)果沒有 diff,耗時(shí): 162.85s => 76s
- trace 顯示 collectOperation 階段確實(shí)是啟動(dòng)了多個(gè) Processor
- top 發(fā)生了變化曙痘,
program.Program.WithFunc
和program.Program.WhereDecl
兩個(gè)方法耗達(dá)到 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茧痒,即做一個(gè)
go/types.Func
數(shù)組肮韧,按照 pos 排序,withFunc(pos token.Pos)
邏輯轉(zhuǎn)化為:二分搜索 pos,進(jìn)而確定是哪個(gè)tyeps.Func
惹苗,時(shí)間復(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,耗時(shí): 76s => 61s
使用相同的思路構(gòu)建其他緩存 pos => ast.File, types.Func => ast.Expr
重新編譯執(zhí)行桩蓉,文檔結(jié)果沒有 diff淋纲,耗時(shí)縮短到 61s => 20s
通過 trace 發(fā)現(xiàn)原來
122 * collectOperation()
步驟耗時(shí)已經(jīng)縮短到 7.5s,但HttpErrorScanner.Scan()
步驟還是有 6.5s 的耗時(shí)院究,可見已有緩存對(duì)其影響不大
優(yōu)化第三步:?jiǎn)尾襟E邏輯調(diào)優(yōu)
針對(duì) HttpErrorScanner.Scan()
我們來分析下其火焰圖
可以看到耗時(shí)的大頭依然是 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),估計(jì)調(diào)用次數(shù)很多 - 第 9业汰、10伙窃、11 行連續(xù) 3 個(gè) if 判斷,相互獨(dú)立样漆,顯然可以調(diào)換順序为障。
- Scan 方法為的是找到個(gè)別類型,且數(shù)量很少放祟,推斷第三個(gè)條件
http_error_code.IsHttpCode(obj.Type())
的范圍最小鳍怨,將第三個(gè)條件放到最前面,重新編譯執(zhí)行跪妥,130s鞋喇,尷尬了,看來http_error_code.IsHttpCode(obj.Type())
比tpeFunc.Scope().Contains(id.Pos())
耗時(shí)要多得多眉撵。
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
的邏輯可以簡(jiǎn)化為tpe.String() == typeName || strings.HasSuffix(tpe.String(), "."+typeName)
types.Type
可以做緩存重新編譯運(yùn)行侦香,27s,看來
http_error_code.IsHttpCode(obj.Type())
雖然過濾度高纽疟,但是消耗也大罐韩,看到三個(gè) if 之一的第10行,只是一個(gè)類型判斷仰挣,消耗不大伴逸,放在第一個(gè)試試。重新編譯運(yùn)行膘壶,20s => 16s
更多優(yōu)化可能
- 掃描中代碼中错蝴,原則上講,只需要參與 HTTP 接口定義的 package颓芭,目前的方案會(huì)對(duì)所有依賴庫建緩存掃描顷锰。