golang 性能優(yōu)化實(shí)戰(zhàn)

調(diào)優(yōu)基本思路

  1. 對(duì)外接口協(xié)議不能改變
  2. 了解需求和代碼演進(jìn)過程
  3. 確定資源消耗類型
  4. 控制運(yùn)算數(shù)據(jù)輸入量
  5. 提高 CPU 利用率
  6. 提高緩存命中率

項(xiàng)目概況

  1. gin-swagger 解析使用 gin 的代碼乒融,生成 swagger2.0 的文檔,以保證文檔和代碼的一致性客叉。
  2. 使用 golang.org/x/tools/go/loader 將源碼解析成 go/types go/ast 相關(guān)結(jié)構(gòu)化數(shù)據(jù)。
  3. 通過遍歷 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ù)流程

  1. IDE 如 Golang/VSCode 都有相關(guān)工具或插件
  2. 命令行工具如 delve
  3. 梳理出程序運(yùn)行的主要步驟:
    1. loader.Load(): 掃描 service-card 代碼包括所有依賴
    2. HttpErrorScanner.Scan(): 遍歷所有 package 找到代碼里定義的 HTTP 錯(cuò)誤類型及其相關(guān)信息
    3. RoutesScanner.Scan(): 遍歷所有 package 找到用 gin 定義的 HTTP 路由及其相關(guān)信息
    4. 循環(huán)調(diào)用 collectOperation(): 找到請(qǐng)求和響應(yīng)類型胖腾,構(gòu)建 spec.Sawgger 的 Operation
    5. 將 spec.Swagger 序列化成 JSON 格式寫入文件

使用 trace 梳理資源消耗概況

  1. 標(biāo)準(zhǔn)庫中的 runtime/trace 包,用于追蹤程序運(yùn)行各個(gè)階段的指標(biāo),官方使用范例

  2. 查看結(jié)果:$ go tool trace service-card.trace

origin_trace_01.png
origin_trace_02.png
  1. 初步分析:

    1. 大部分運(yùn)行過程只使用了一個(gè)線程
    2. 內(nèi)存開始階段陡增咸作,中后期增速較小
    3. 沒有網(wǎng)絡(luò)請(qǐng)求
    4. 同步等待锨阿、系統(tǒng)調(diào)用、runtime調(diào)度的耗時(shí)操作都是 loader 庫相關(guān)
    5. 資源消耗特點(diǎn): CPU 密集记罚、內(nèi)存容量需求穩(wěn)定墅诡。
  2. 各主要步驟耗時(shí)情況:

    1. loader.Load(): 7.8s
    2. HttpErrorScanner.Scan(): 7s
    3. RoutesScanner.Scan(): 0.5s
    4. 122 * collectOperation(): 146.6s
    5. json.Marshal(): 0.1s

pprof 查看各方法耗時(shí)

  1. 標(biāo)準(zhǔn)庫中的 runtime/pprof 包,用于整體統(tǒng)計(jì)運(yùn)行過程桐智,各個(gè)方法的總的資源消耗情況末早,官方使用范例

  2. 手動(dòng)安裝最新版本 pprof 工具:$ go get -u github.com/google/pprof

  3. 用 web 方式查看 pprof CPU 分析結(jié)果:$ pprof -http=":8091" ./cpu.prof

  4. 先看 Top
    origin_cpu_top10.png
    1. 排名第一的 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ù)多。

    1. 排名第二的 runtime.mapiternext 也是標(biāo)準(zhǔn)庫遍歷 map 的方法口渔,耗時(shí)多的原因也是調(diào)用次數(shù)多
    2. 依次看下來,沒有明顯的耗時(shí)過高的業(yè)務(wù)方法
  5. 初步判斷:業(yè)務(wù)方法沒有明顯缺陷穿撮,業(yè)務(wù)層面需要調(diào)用的次數(shù)過多導(dǎo)致整體耗時(shí)高

優(yōu)化第零步:持續(xù) Diff

首先使用原始版本 gin-swagger 生成 swagger 文檔缺脉,在優(yōu)化的過程中每一次修改都要確保結(jié)果和原始版本一致。

優(yōu)化第一步:提高 CPU 利用率

  1. 從 trace 結(jié)果發(fā)現(xiàn)悦穿,122 次調(diào)用 collectOperation()攻礼,耗時(shí)占比 90%,卻是單核執(zhí)行栗柒,如果能利用多核礁扮,將有相當(dāng)可觀的性能提升。
  2. 利用多核需要確保并發(fā)安全和兼容亂序瞬沦,通過調(diào)試 collectOperation() 發(fā)現(xiàn):
    1. 被競(jìng)爭(zhēng)的資源是 Swagger.Paths.PathsSwagger.Definitions太伊,都是插入操作
    2. 由于 Swagger.Paths.PathsSwagger.Definitions是 map 類型,所以沒有亂序的問題
  3. 給競(jìng)爭(zhēng)資源上鎖 sync.RWMutex逛钻,保證并發(fā)安全
  4. 啟多個(gè) goroutine 執(zhí)行 collectOperation()
  5. 重新編譯執(zhí)行僚焦,文檔結(jié)果沒有 diff,耗時(shí): 162.85s => 76s
  6. trace 顯示 collectOperation 階段確實(shí)是啟動(dòng)了多個(gè) Processor
  7. top 發(fā)生了變化曙痘,program.Program.WithFuncprogram.Program.WhereDecl兩個(gè)方法耗達(dá)到 8.5%
step_01_top10.png

優(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
}

  1. 業(yè)務(wù)邏輯:遍歷所有的 package芳悲,找到 pos 所在的 *types.Func

  2. 看到熟悉身影:scope.Contains(pos),確定是上文出現(xiàn)的 go/types.(*Scope).Contains

  3. 結(jié)論:大量 WitchFunc 調(diào)用边坤,導(dǎo)致過多 go/types.(*Scope).Contains 調(diào)用名扛,拖慢了執(zhí)行速度

  4. 分析業(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] }
    
    
  5. 重新編譯執(zhí)行殿较,文檔結(jié)果沒有 diff,耗時(shí): 76s => 61s

  6. 使用相同的思路構(gòu)建其他緩存 pos => ast.File, types.Func => ast.Expr

  7. 重新編譯執(zhí)行桩蓉,文檔結(jié)果沒有 diff淋纲,耗時(shí)縮短到 61s => 20s

  8. 通過 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() 我們來分析下其火焰圖

step_03_torch_httperr_scan.png

可以看到耗時(shí)的大頭依然是 go/types.(*Scope).Containsruntime.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

  1. 第9行 tpeFunc.Scope().Contains(id.Pos()) 上有四層 for 循環(huán),估計(jì)調(diào)用次數(shù)很多
  2. 第 9业汰、10伙窃、11 行連續(xù) 3 個(gè) if 判斷,相互獨(dú)立样漆,顯然可以調(diào)換順序为障。
  3. 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
}

  1. IsTypeName 的邏輯可以簡(jiǎn)化為

    tpe.String() == typeName || strings.HasSuffix(tpe.String(), "."+typeName)
    
    
  2. types.Type 可以做緩存

  3. 重新編譯運(yùn)行侦香,27s,看來 http_error_code.IsHttpCode(obj.Type()) 雖然過濾度高纽疟,但是消耗也大罐韩,看到三個(gè) if 之一的第10行,只是一個(gè)類型判斷仰挣,消耗不大伴逸,放在第一個(gè)試試。

  4. 重新編譯運(yùn)行膘壶,20s => 16s

更多優(yōu)化可能

  1. 掃描中代碼中错蝴,原則上講,只需要參與 HTTP 接口定義的 package颓芭,目前的方案會(huì)對(duì)所有依賴庫建緩存掃描顷锰。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亡问,隨后出現(xiàn)的幾起案子官紫,更是在濱河造成了極大的恐慌肛宋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件束世,死亡現(xiàn)場(chǎng)離奇詭異酝陈,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)毁涉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門沉帮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贫堰,你說我怎么就攤上這事穆壕。” “怎么了其屏?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵喇勋,是天一觀的道長。 經(jīng)常有香客問我偎行,道長川背,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任蛤袒,我火速辦了婚禮渗常,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘汗盘。我一直安慰自己,他們只是感情好询一,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布隐孽。 她就那樣靜靜地躺著,像睡著了一般健蕊。 火紅的嫁衣襯著肌膚如雪菱阵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天缩功,我揣著相機(jī)與錄音晴及,去河邊找鬼。 笑死嫡锌,一個(gè)胖子當(dāng)著我的面吹牛虑稼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播势木,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蛛倦,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了啦桌?” 一聲冷哼從身側(cè)響起溯壶,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后且改,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體验烧,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年又跛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了碍拆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡效扫,死狀恐怖倔监,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情菌仁,我是刑警寧澤浩习,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站济丘,受9級(jí)特大地震影響谱秽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摹迷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一疟赊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧峡碉,春花似錦近哟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至地来,卻和暖如春戳玫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背未斑。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工咕宿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜡秽。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓府阀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親载城。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肌似,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容