日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Golang 性能优化实战

發布時間:2024/2/28 编程问答 59 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Golang 性能优化实战 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

作者:trumanyan,騰訊 CSIG 后臺開發工程師

項目背景

網關服務作為統一接入服務,是大部分服務的統一入口。為了避免成功瓶頸,需要對其進行盡可能地優化。因此,特別總結一下 golang 后臺服務性能優化的方式,并對網關服務進行優化。

技術背景:

  • 基于 tarsgo 框架的 http 接入服務,下游服務使用 tarsgo 協議進行交互

性能指標

網關服務本身沒有業務邏輯處理,僅作為統一入口進行請求轉發,因此我們主要關注下列指標

  • 吞吐量:每秒鐘可以處理的請求數

  • 響應時間:從客戶端發出請求,到收到回包的總耗時

定位瓶頸

一般后臺服務的瓶頸主要為 CPU,內存,IO 操作中的一個或多個。若這三者的負載都不高,但系統吞吐量低,基本就是代碼邏輯出問題了。

在代碼正常運行的情況下,我們要針對某個方面的高負載進行優化,才能提高系統的性能。golang 可通過 benchmark 加 pprof 來定位具體的性能瓶頸。

benchmark 簡介

go test -v gate_test.go -run=none -bench=. -benchtime=3s -cpuprofile cpu.prof -memprofile mem.prof
  • -run 知道單次測試,一般用于代碼邏輯驗證

  • -bench=. 執行所有 Benchmark,也可以通過用例函數名來指定部分測試用例

  • -benchtime 指定測試執行時長

  • -cpuprofile 輸出 cpu 的 pprof 信息文件

  • -memprofile 輸出 heap 的 pprof 信息文件。

  • -blockprofile 阻塞分析,記錄 goroutine 阻塞等待同步(包括定時器通道)的位置

  • -mutexprofile 互斥鎖分析,報告互斥鎖的競爭情況

benchmark 測試用例常用函數

  • b.ReportAllocs() 輸出單次循環使用的內存數量和對象 allocs 信息

  • b.RunParallel() 使用協程并發測試

  • b.SetBytes(n int64) 設置單次循環使用的內存數量

pprof 簡介

生成方式

  • runtime/pprof: 手動調用如runtime.StartCPUProfile或者runtime.StopCPUProfile等 API 來生成和寫入采樣文件,靈活性高。主要用于本地測試。

  • net/http/pprof: 通過 http 服務獲取 Profile 采樣文件,簡單易用,適用于對應用程序的整體監控。通過 runtime/pprof 實現。主要用于服務器端測試。

  • go test: 通過 go test -bench . -cpuprofile cpuprofile.out生成采樣文件,主要用于本地基準測試。可用于重點測試某些函數。

查看方式

  • go tool pprof [options][binary] ...

    • --text 純文本

    • --web 生成 svg 并用瀏覽器打開(如果 svg 的默認打開方式是瀏覽器)

    • --svg 只生成 svg

    • --list funcname 篩選出正則匹配 funcname 的函數的信息

    • -http=":port" 直接本地瀏覽器打開 profile 查看(包括 top,graph,火焰圖等)

  • go tool pprof -base profile1 profile2

    • 對比查看 2 個 profile,一般用于代碼修改前后對比,定位差異點。

  • 通過命令行方式查看 profile 時,可以在命令行對話中,使用下列命令,查看相關信息

    • flat flat% sum% cum cum%5.95s 27.56% 27.56% 5.95s 27.56% runtime.usleep4.97s 23.02% 50.58% 5.08s 23.53% sync.(*RWMutex).RLock4.46s 20.66% 71.24% 4.46s 20.66% sync.(*RWMutex).RUnlock2.69s 12.46% 83.70% 2.69s 12.46% runtime.pthread_cond_wait1.50s 6.95% 90.64% 1.50s 6.95% runtime.pthread_cond_signal
    • flat: 采樣時,該函數正在運行的次數*采樣頻率(10ms),即得到估算的函數運行”采樣時間”。這里不包括函數等待子函數返回。

    • flat%: flat / 總采樣時間值

    • sum%: 前面所有行的 flat% 的累加值,如第三行 sum% = 71.24% = 27.56% + 50.58%

    • cum: 采樣時,該函數出現在調用堆棧的采樣時間,包括函數等待子函數返回。因此 flat <= cum

    • cum%: cum / 總采樣時間值

    • topN [-cum] 查看前 N 個數據:

  • list ncname 查看某個函數的詳細信息,可以明確具體的資源(cpu,內存等)是由哪一行觸發的。

pprof 接入 tarsgo

  • 服務中 main 方法插入代碼

    cfg?:=?tars.GetServerConfig() profMux?:=?&tars.TarsHttpMux{} profMux.HandleFunc("/debug/pprof/",?pprof.Index) profMux.HandleFunc("/debug/pprof/cmdline",?pprof.Cmdline) profMux.HandleFunc("/debug/pprof/profile",?pprof.Profile) profMux.HandleFunc("/debug/pprof/symbol",?pprof.Symbol) profMux.HandleFunc("/debug/pprof/trace",?pprof.Trace) tars.AddHttpServant(profMux,?cfg.App+"."+cfg.Server+".ProfObj")
  • taf 管理平臺中,添加 servant:ProfObj (名字可自己修改)

  • 發布服務

  • 查看 tasrgo 服務的 pprof

  • 保證開發機能直接訪問到 tarsgo 節點部署的 ip 和 port。

  • 查看 profile(http 地址中的 ip,port 為 ProfObj 的 ip 和 port)

    # 下載cpu profile go tool pprof http://ip:port/debug/pprof/profile?seconds=120 # 等待120s,不帶此參數時等待30s# 下載heap profile go tool pprof http://ip:port/debug/pprof/heap# 下載goroutine profile go tool pprof http://ip:port/debug/pprof/goroutine# 下載block profile go tool pprof http://ip:port/debug/pprof/block# 下載mutex profile go tool pprof http://ip:port/debug/pprof/mutex# 下載20秒的trace記錄(遇到棘手問題時,查看trace會比較容易定位)curl http://100.97.1.35:10078/debug/pprof/trace?seconds=20 > trace.outgo tool trace trace.out 查看
  • 直接在終端中通過 pprof 命令查看

  • sz 上面命令執行時出現的Saved profile in /root/pprof/pprof.binary.alloc_objects.xxxxxxx.xxxx.pb.gz到本地

  • 在本地環境,執行go tool pprof -http=":8081" pprof.binary.alloc_objects.xxxxxxx.xxxx.pb.gz 即可直接通過http://localhost:8081頁面查看。包括topN,火焰圖信息等,會更方便一點。

  • GC Trace

    golang 具備 GC 功能,而 GC 是最容易被忽視的性能影響因素。尤其是在本地使用 benchmark 測試時,由于時間較短,占用內存較少。往往不會觸發 GC。而一旦線上出現 GC 問題,又不太好定位。目前常用的定位方式有兩種:

    本地 gctrace

    • 在執行程序前加 GODEBUG=gctrace=1,每次 gc 時會輸出一行如下內容

      gc 1 @0.001s 11%: 0.007+1.5+0.004 ms clock, 0.089+1.5/2.8/0.27+0.054 ms cpu, 4->4->3 MB, 5 MB goal, 12 P scvg: inuse: 4, idle: 57, sys: 62, released: 57, consumed: 4 (MB)
      • 也通過日志轉為圖形化:

        GODEBUG=gctrace=1 godoc -index -http=:6060 2> stderr.log cat stderr.log | gcvis
      • inuse:使用了多少 M 內存

      • idle:剩下要清除的內存

      • sys:系統映射的內存

      • released:釋放的系統內存

      • consumed:申請的系統內存

      • gc 1 表示第 1 次 gc

      • @0.001s 表示程序執行的總時間

      • 11% 表示垃圾回收時間占用總的運行時間百分比

      • 0.007+1.5+0.004 ms clock 表示工作線程完成 GC 的 stop-the-world,sweeping,marking 和 waiting 的時間

      • 0.089+1.5/2.8/0.27+0.054 ms cpu 垃圾回收占用 cpu 時間

      • 4->4->3 MB 表示堆的大小,gc 后堆的大小,存活堆的大小

      • 5 MB goal 整體堆的大小

      • 12 P 使用的處理器數量

      • scvg: inuse: 4, idle: 57, sys: 62, released: 57, consumed: 4 (MB) 表示系統內存回收信息

      • 采用圖形化的方式查看:https://github.com/davecheney/gcvis

        GODEBUG=gctrace=1 go test -v *.go -bench=. -run=none -benchtime 3m |& gcvis

    線上 trace

    在線上業務中添加net/http/pprof后,可通過下列命令采集 20 秒的 trace 信息

    curl http://ip:port/debug/pprof/trace?seconds=20 > trace.out

    再通過go tool trace trace.out 即可在本地瀏覽器中查看 trace 信息。

    • View trace:查看跟蹤

    • Goroutine analysis:Goroutine 分析

    • Network blocking profile:網絡阻塞概況

    • Synchronization blocking profile:同步阻塞概況

    • Syscall blocking profile:系統調用阻塞概況

    • Scheduler latency profile:調度延遲概況

    • User defined tasks:用戶自定義任務

    • User defined regions:用戶自定義區域

    • Minimum mutator utilization:最低 Mutator 利用率

    GC 相關的信息可以在 View trace 中看到

    可通過點擊 heap 的色塊區域,查看 heap 信息。

    點擊 GC 對應行的藍色色塊,查看 GC 耗時及相關回收信息。

    通過這兩個信息就可以確認是否存在 GC 問題,以及造成高 GC 的可能原因。

    使用問題

    trace 的展示僅支持 chrome 瀏覽器。但是目前常用的 chrome 瀏覽器屏蔽了 go tool trace 使用的 HTML import 功能。即打開“view trace”時,會出現一片空白。并可以在 console 中看到警告信息:

    HTML Imports is deprecated and has now been removed as of M80. See https://www.chromestatus.com/features/5144752345317376 and https://developers.google.com/web/updates/2019/07/web-components-time-to-upgrade for more details.
    解決辦法
    申請 token
    • https://developers.chrome.com/origintrials/#/register_trial/2431943798780067841 然后登錄

    • web origin 處填寫 http://localhost:8001 端口只能是 8000 - 8003,支持 http 和 https。(也可以填寫 127.0.0.1:8001,依賴于你瀏覽器中顯示的地址,否則對不上的話,還要手動改一下)

    • 點擊注冊后即可看到 token

    修改 trace.go
    • 編輯${GOROOT}/src/cmd/trace/trace.go 文件,在文件中找到 templTrace 然后在 ?標簽的下一行添加<meta http-equiv="origin-trial" content="你復制的token">

    重新編譯 go
    • ${GOROOT}/src 目錄,執行 ./all.bash

    • 若提示:ERROR: Cannot find go1.4\bin\go Set GOROOT_BOOTSTRAP to a working Go tree >= Go 1.4 則需要先安裝一個 go1.4 的版本,再通過它來編譯 go。(下載鏈接https://dl.google.com/go/go1.4-bootstrap-20171003.tar.gz) 在 go1.4/src 下執行./make.bash. 指定 GOROOT_BOOTSTRAP 為 go1.4 的根目錄。然后就可以重新編譯 go

    查看 trace

    go tool trace -http=localhost:8001 trace.out

    若打開 view trace 還是空白,則檢查一下瀏覽器地址欄中的地址,是否與注冊時的一樣。即注冊用的 localhost 或 127.0.0.1 則地址欄中也要一樣。

    常見性能瓶頸

    業務邏輯

    出現無效甚至降低性能的邏輯。常見的有

    • 邏輯重復:相同的操作在不同的位置做了多次或循環跳出的條件設置不當。

    • 資源未復用:內存頻繁申請和釋放,數據庫鏈接頻繁建立和銷毀等。

    • 無效代碼。

    存儲

    未選擇恰當的存儲方式,常見的有:

    • 臨時數據存放到數據庫中,導致頻繁讀寫數據庫。

    • 將復雜的樹狀結構的數據用 SQL 數據庫存儲,出現大量冗余列,并且在讀寫時要進行拆解和拼接。

    • 數據庫表設計不當,無法有效利用索引查詢,導致查詢操作耗時高甚至出現大量慢查詢。

    • 熱點數據未使用緩存,導致數據庫負載過高,響應速度下降。

    并發處理

    并發操作的問題主要出現在資源競爭上,常見的有:

    • 死鎖/活鎖導致大量阻塞,性能嚴重下降。

    • 資源競爭激烈:大量的線程或協程搶奪一個鎖。

    • 臨界區過大:將不必要的操作也放入臨界區,導致鎖的釋放速度過慢,引起其他線程或協程阻塞。

    golang 部分細節簡介

    在優化之前,我們需要對 golang 的實現細節有一個簡單的了解,才能明白哪些地方有問題,哪些地方可以優化,以及怎么優化。以下內容的詳細講解建議查閱網上優秀的 blog。對語言的底層實現機制最好有個基本的了解,否則有時候掉到坑里都不知道為啥。

    協程調度

    Golang 調度是非搶占式多任務處理,由協程主動交出控制權。遇到如下條件時,才有可能交出控制權

    • I/O,select

    • channel

    • 等待鎖

    • 函數調用(是一個切換的機會,是否會切換由調度器決定)

    • runtime.Gosched()

    因此,若存在較長時間的 for 循環處理,并且循環內沒有上述邏輯時,會阻塞住其他的協程調度。在實際編碼中一定要注意。

    內存管理

    Go 為每個邏輯處理器(P)提供了一個稱為mcache的本地內存線程緩存。每個 mcache 中持有 67 個級別的 mspan。每個 msapn 又包含兩種:scan(包含指針的對象)和 noscan(不包含指針的對象)。在進行垃圾收集時,GC 無需遍歷 noscan 對象

    GC 處理

    GC 的工作就是確定哪些內存可以釋放,它是通過掃描內存查找內存分配的指針來完成這個工作的。GC 觸發時機:

    • 到達堆閾值:默認情況下,它將在堆大小加倍時運行,可通過 GOGC 來設定更高閾值(不建議變更此配置)

    • 到達時間閾值:每兩分鐘會強制啟動一次 GC 循環

    為啥要注意 GC,是因為 GC 時出現 2 次 Stop the world,即停止所有協程,進行掃描操作。若是 GC 耗時高,則會嚴重影響服務器性能。

    變量逃逸

    注意,golang 中的棧是跟函數綁定的,函數結束時棧被回收。

    變量內存回收:
    • 如果分配在棧中,則函數執行結束可自動將內存回收;

    • 如果分配在堆中,則函數執行結束可交給 GC(垃圾回收)處理;

    而變量逃逸就意味著增加了堆中的對象個數,影響 GC 耗時。一般要盡量避免逃逸。

    逃逸分析不變性:
  • 指向棧對象的指針不能存在于堆中;

  • 指向棧對象的指針不能在棧對象回收后存活;

  • 在逃逸分析過程中,凡是發現出現違反上述約定的變量,就將其移到堆中。

    逃逸常見的情況:
    • 指針逃逸:返回局部變量的地址(不變性 2)

    • 棧空間不足

    • 動態類型逃逸:如 fmt.Sprintf,json.Marshel 等接受變量為...interface{}函數的調用,會導致傳入的變量逃逸。

    • 閉包引用

    包含指針類型的底層結構

    string

    type?StringHeader?struct?{Data?uintptrLen??int }

    slice

    type?SliceHeader?struct?{Data?uintptrLen??intCap??int }

    map

    type?hmap?struct?{count?????intflags?????uint8B?????????uint8noverflow?uint16hash0?????uint32buckets????unsafe.Pointeroldbuckets?unsafe.Pointernevacuate??uintptrextra?*mapextra }

    這些是常見會包含指針的對象。尤其是 string,在后臺應用中大量出現。并經常會作為 map 的 key 或 value。若數據量較大時,就會引發 GC 耗時上升。同時,我們可以注意到 string 和 slice 非常相似,從某種意義上說它們之間是可以直接互相轉換的。這就可以避免 string 和[]byte 之間類型轉換時,進行內存拷貝

    類型轉換優化

    func?String(b?[]byte)?string?{return?*(*string)(unsafe.Pointer(&b)) } func?Str2Bytes(s?string)?[]byte?{x?:=?(*[2]uintptr)(unsafe.Pointer(&s))h?:=?[3]uintptr{x[0],?x[1],?x[1]}return?*(*[]byte)(unsafe.Pointer(&h)) }

    性能測試方式

    本地測試

    將服務處理的核心邏輯,使用 go test 的 benchmark 加 pprof 來測試。建議上線前,就對整個業務邏輯的性能進行測試,提前優化瓶頸。

    線上測試

    一般 http 服務可以通過常見的測試工具進行壓測,如 wrk,locust 等。taf 服務則需要我們自己編寫一些測試腳本。同時,要注意的是,壓測的目的是定位出服務的最佳性能,而不是盲目的高并發請求測試。因此,一般需要逐步提升并發請求數量,來定位出服務的最佳性能點。

    注意:由于 taf 平臺具備擴容功能,因此為了更準確的測試,我們應該在測試前關閉要測試節點的自動擴容。

    實際項目優化

    為了避免影響后端服務,也為了避免后端服務影響網關自身。因此,我們需要在壓測前,將對后端服務的調用屏蔽。

    • 測試準備:屏蔽遠程調用:下游服務調用,健康度上報,統計上報,遠程日志。以便關注網關自身性能。

    QPS 現狀

    首先看下當前業務的性能指標,使用 wrk 壓測網關服務

    可以看出,在總鏈接數為 70 的時候,QPS 最高,為 13245。

    火焰圖

    根據火焰圖我們定位出 cpu 占比較高的幾個方法為:

    • json.Marshal

    • json.Unmarshal

    • rogger.Infof

    為了方便測試,將代碼改為本地運行,并通過 benchmark 的方式來對比修改前后的差異。

    由于正式環境使用的 golang 版本為 1.12,因此本地測試時,也要使用同樣的版本。

    benchmark

    Benchmark 50000000 3669 ns/op 4601 B/op 73 allocs/op

    查看 cpu 和 memory 的 profile,發現健康度上報的數據結構填充占比較高。這部分邏輯基于 tars 框架實現。暫時忽略,為避免影響其他測試,先注釋掉。再看看 benchmark。

    Benchmark 500000 3146 ns/op 2069 B/op 55 allocs/op

    優化策略

    JSON 優化

    先查看 json 解析的部分,看看是否有優化空間

    請求處理

    //RootHandle?view.ReadReq2Json?readJsonReq?中進行json解析 type?GatewayReqBody?struct?{Header??GatewayReqBodyHeader???`json:"header"`Payload?map[string]interface{}?`json:"payload"` } func?readJsonReq(data?[]byte,?req?*model.GatewayReqBody)?error?{dataMap?:=?make(map[string]interface{})err?:=?jsoniter.Unmarshal(data,?&dataMap)...headerMap,?ok?:=?header.(map[string]interface{})businessName,?ok?:=?headerMap["businessName"]qua,?ok?:=?headerMap["qua"]sessionId,?ok?:=?headerMap["sessionId"]...payload,?ok?:=?dataMap["payload"]req.Payload,?ok?=?payload.(map[string]interface{}) }

    這個函數本質上將 data 解析為 model.GatewayReqBody 類型的結構體。但是這里卻存在 2 個問題

  • 使用了復雜的解析方式,先將 data 解析為 map,再通過每個字段的名字來取值,并進行類型轉換。

  • Req.Playload 解析為一個 map。但又未使用。我們看看后面這個 payload 是用來做啥。確認是否為無效代碼。

  • func?invokeTafServant(resp?http.ResponseWriter,?gatewayHttpReq?*model.GatewayHttpReq)?{...payloadBytes,?err?:=?json.Marshal(gatewayHttpReq.ReqBody.Payload)if?err?==?nil?{commonReq.Payload?=?string(payloadBytes)}?else?{responseData(gatewayHttpReq,?StatusInternalServerError,?"封裝json異常",?"",?resp)return}...}

    后續的使用中,我們可以看到,又將這個 payload 轉為 string。因此,我們可以確定,上面的 json 解析是沒有意義,同時也會浪費資源(payload 數據量一般不小)。

    優化方法
    • golang 自帶的 json 解析性能較低,這里我們可以替換為github.com/json-iterator來提升性能

    • 在 golang 中,遇到不需要解析的 json 數據,可以將其類型聲明為json.RawMessage. 即,可以將上述 2 個方法優化為

    type?GatewayReqBody?struct?{Header??GatewayReqBodyHeader?`json:"header"`Payload?json.RawMessage??????`json:"payload"` } func?readJsonReq(data?[]byte,?req?*model.GatewayReqBody)?error?{err?:=?jsoniter.Unmarshal(data,?req)if?err?!=?nil?{return?jsonParseErr}for?k,?v?:=?range?req.Header.Qua?{req.Header.Qua[k]?=?vif?len(req.Header.QuaStr)?==?0?{req.Header.QuaStr?=?k?+?"="?+?v}?else?{req.Header.QuaStr?+=?"&"?+?k?+?"="?+?v}}return?nil } func?invokeTafServant(resp?http.ResponseWriter,?gatewayHttpReq?*model.GatewayHttpReq)?{commonReq.Payload?=?string(gatewayHttpReq.ReqBody.Payload) }
    • 這里注意!出現了 string 和[]byte 之間的類型轉換.為了避免內存拷貝,這里將 string()改為上面的類型轉換優化中所定義的轉換函數,即commonReq.Payload = encode.String(gatewayHttpReq.ReqBody.Payload)

    回包處理

    type?GatewayRespBody?struct?{Header??GatewayRespBodyHeader??`json:"header"`Payload?map[string]interface{}?`json:"payload"` }func?responseData(gatewayReq?*model.GatewayHttpReq,?code?int32,?message?string,?payload?string,?resp?http.ResponseWriter)?{jsonPayload?:=?make(map[string]interface{})if?len(payload)?!=?0?{err?:=?json.Unmarshal([]byte(payload),?&jsonPayload)if?err?!=?nil?{...}}body?:=?model.GatewayRespBody{Header:?model.GatewayRespBodyHeader{Code:????code,Message:?message,},Payload:?jsonPayload,}data,?err?:=?view.RenderResp("json",?&body)...resp.WriteHeader(http.StatusOK)resp.Write(data) }

    同樣的,這里的 jsonPayload,也是出現了不必要的 json 解析。我們可以改為

    type?GatewayRespBody?struct?{Header??GatewayRespBodyHeader??`json:"header"`Payload?json.RawMessage?`json:"payload"` }body?:=?model.GatewayRespBody{Header:?model.GatewayRespBodyHeader{Code:????code,Message:?message,},Payload:?encode.Str2Bytes(payload),}

    然后在 view.RenderResp 方法中

    func?RenderResp(format?string,?resp?interface{})?([]byte,?error)?{if?"json"?==?format?{return?jsoniter.Marshal(resp)}return?nil,?errors.New("format?error") }
    benchmark
    Benchmark 500000 3326 ns/op 2842 B/op 50 allocs/op

    雖然對象 alloc 減少了,但單次操作內存使用增加了,且性能下降了。這就有點奇怪了。我們來對比一下 2 個情況下的 pprof。

    逃逸分析及處理

    go tool pprof -base
    • cpu 差異

      flat flat% sum% cum cum%0.09s 1.17% 1.17% 0.40s 5.20% runtime.mallocgc0.01s 0.13% 1.30% 0.35s 4.55% /vendor/github.com/json-iterator/go.(*Iterator).readObjectStart0 0% 1.30% 0.35s 4.55% /vendor/github.com/json-iterator/go.(*twoFieldsStructDecoder).Decode
    • mem 差異

      flat flat% sum% cum cum%478.96MB 20.33% 20.33% 279.94MB 11.88% gateway.RootHandle0 0% 20.33% 279.94MB 11.88% command-line-arguments.BenchmarkTestHttp.func10 0% 20.33% 279.94MB 11.88% testing.(*B).RunParallel.func1

    可以看出 RootHandle 多了 478.96M 的內存使用。通過 list RootHandle 對比 2 個情況下的內存使用。發現修改后的 RootHandle 中多出了這一行:475.46MB 475.46MB 158: gatewayHttpReq := model.GatewayHttpReq{} 這一般意味著變量 gatewayHttpReq 出現了逃逸。

    • go build -gcflags "-m -m" gateway/*.go

      gateway/logic.go:270:26: &gatewayHttpReq escapes to heap

      可以看到確實出現了逃逸。這個對應的代碼為err = view.ReadReq2Json(&gatewayHttpReq),而造成逃逸的本質是因為上面改動了函數 readJsonReq(動態類型逃逸,即函數參數為 interface 類型,無法在編譯時確定具體類型的)

      func?readJsonReq(data?[]byte,?req?*model.GatewayReqBody)?error?{err?:=?jsoniter.Unmarshal(data,?req)... }

      因此,這里需要特殊處理一下,改為

      func readJsonReq(data []byte, req *model.GatewayReqBody) error {var tmp model.GatewayReqBodyerr := jsoniter.Unmarshal(data, &tmp)... }
    benchmark
    Benchmark 500000 2994 ns/op 1892 B/op 50 allocs/op

    可以看到堆內存使用明顯下降。性能也提升了。再看一下 pprof,尋找下個瓶頸。

    cpu profile

    拋開 responeseData(他內部主要是日志打印占比高),占比較高的為 util.GenerateSessionId,先來看看這個怎么優化。

    隨機字符串生成

    var?letterRunes?=?[]rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func?RandStringRunes(n?int)?string?{b?:=?make([]rune,?n)for?i?:=?range?b?{b[i]?=?letterRunes[rand.Intn(len(letterRunes))]}return?string(b) }

    目前的生成方式使用的類型是 rune,但其實用 byte 就夠了。另外,letterRunes 是 62 個字符,即最大需要 6 位的 index 就可以遍歷完成了。而隨機數獲取的是 63 位。即每個隨機數,其實可以產生 10 個隨機字符。而不用每個字符都獲取一次隨機數。所以我們改為

    const?(letterBytes???=?"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"letterIdxBits?=?6letterIdxMask?=?1<<letterIdxBits?-?1letterIdxMax??=?63?/?letterIdxBits ) func?RandStringRunes(n?int)?string?{b?:=?make([]byte,?n)for?i,?cache,?remain?:=?n-1,?rand.Int63(),?letterIdxMax;?i?>=?0;?{if?remain?==?0?{cache,?remain?=?rand.Int63(),?letterIdxMax}if?idx?:=?int(cache?&?letterIdxMask);?idx?<?len(letterBytes)?{b[i]?=?letterBytes[idx]i--}cache?>>=?letterIdxBitsremain--}return?string(b) }

    benchmark

    Benchmark 1000000 1487 ns/op 1843 B/op 50 allocs/op

    類型轉換及字符串拼接

    一般情況下,都會說將 string 和[]byte 的轉換改為 unsafe;以及在字符串拼接時,用 byte.Buffer 代替 fmt.Sprintf。但是網關這里的情況比較特殊,字符串的操作基本集中在打印日志的操作。而 tars 的日志打印本身就是通過 byte.Buffer 拼接的。所以這可以避免。另外,由于日志打印量大,使用 unsafe 轉換[]byte 為 string 帶來的收益,往往會因為逃逸從而影響 GC,反正會影響性能。因此,不同的場景下,不能簡單的套用一些優化方法。需要通過壓測及結果分析來判斷具體的優化策略。

    優化結果

    可以看到優化后,最大鏈接數為 110,最高 QPS 為21153.35。對比之前的13245,大約提升 60%。

    后續

    從 pprof 中可以看到日志打印,遠程日志,健康上報等信息占用較多 cpu 資源,且導致多個數據逃逸(尤其是日志打印)。過多的日志基本等于沒有日志。后續可考慮裁剪日志,僅保留出錯時的上下文信息。

    總結

    • 性能查看工具 pprof,trace 及壓測工具 wrk 或其他壓測工具的使用要比較了解。

    • 代碼邏輯層面的走讀非常重要,要盡量避免無效邏輯。

    • 對于 golang 自身庫存在缺陷的,可以尋找第三方庫或自己改造。

    • golang 版本盡量更新,這次的測試是在 golang1.12 下進行的。而 go1.13 甚至 go1.14 在很多地方進行了改進。比如 fmt.Sprintf,sync.Pool 等。替換成新版本應該能進一步提升性能。

    • 本地 benchmark 結果不等于線上運行結果。尤其是在使用緩存來提高處理速度時,要考慮 GC 的影響。

    • 傳參數或返回值時,盡量按 golang 的設計哲學,少用指針,多用值對象,避免引起過多的變量逃逸,導致 GC 耗時暴漲。struct 的大小一般在 2K 以下的拷貝傳值,比使用指針要快(可針對不同的機器壓測,判斷各自的閾值)。

    • 值類型在滿足需要的情況下,越小越好。能用 int8,就不要用 int64。

    • 資源盡量復用,在 golang1.13 以上,可以考慮使用 sync.Pool 緩存會重復申請的內存或對象。或者自己使用并管理大塊內存,用來存儲小對象,避免 GC 影響(如本地緩存的場景)。

    推薦閱讀:

    總結

    以上是生活随笔為你收集整理的Golang 性能优化实战的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。