1、概述
嗨嘍,大家好呀!我是簡凡,一位游走于各互聯(lián)網(wǎng)大廠間的新時(shí)代農(nóng)民工。對于C端在線業(yè)務(wù),服務(wù)的穩(wěn)定性和吞吐量常常是評估一個(gè)系統(tǒng)的重要指標(biāo),所以本文將從以下4點(diǎn)進(jìn)行展開,逐步講解golang中如何進(jìn)行性能優(yōu)化。
- 為什么要做性能優(yōu)化
- 性能優(yōu)化基礎(chǔ)
- 優(yōu)化思路
- 常見的優(yōu)化場景
2、性能優(yōu)化的目的(Why?)
我們常常在以下時(shí)候考慮到性能優(yōu)化:
- 日常優(yōu)化系統(tǒng):
- 接口相應(yīng)時(shí)間優(yōu)化,以滿足對上游的SLA
- CPU優(yōu)化,保證在線業(yè)務(wù)cpu idl處于一個(gè)較高水平,降低業(yè)務(wù)量突增對系統(tǒng)穩(wěn)定性帶來的沖擊
- 內(nèi)存優(yōu)化,減少內(nèi)存占用,釋放多余的服務(wù)器資源
- 解決線上業(yè)務(wù)問題:
- 接口相應(yīng)超時(shí)
- CPU利用率飆升
3、性能優(yōu)化基礎(chǔ)(What?)
3.1 性能優(yōu)化指標(biāo)
在Golang服務(wù)中,我們常常從以下4點(diǎn)觸發(fā)去做服務(wù)的優(yōu)化:
- CPU profile:報(bào)告程序的 CPU 使用情況,按照一定頻率去采集應(yīng)用程序在 CPU 和寄存器上面的數(shù)據(jù)
- Memory Profile(Heap Profile):報(bào)告程序的內(nèi)存使用情況
- Block Profiling:報(bào)告 goroutines 不在運(yùn)行狀態(tài)的情況,可以用來分析和查找死鎖等性能瓶頸
- Goroutine Profiling:報(bào)告 goroutines 的使用情況,有哪些 goroutine,它們的調(diào)用關(guān)系是怎樣的
4. 性能分析過程(How?)
4.1 如何獲取性能快照
golang中有兩種類型的應(yīng)用,工具性應(yīng)用和服務(wù)型應(yīng)用,工具性型應(yīng)用的main函數(shù)僅一段時(shí)間,我們本地跑單元測試的性能測試其實(shí)原理就是應(yīng)用的這種。服務(wù)型應(yīng)用為長期存活的后端應(yīng)用,例如RPC服務(wù),HTTP服務(wù),我們后端系統(tǒng)通常都是服務(wù)型應(yīng)用。
4.1.1 工具型應(yīng)用獲取CPU快照
測試Demo如下,這里用了一個(gè)快排的例子,應(yīng)用執(zhí)行結(jié)束后,就會(huì)生成一個(gè)文件,保存了我們的 CPU profiling 數(shù)據(jù)。得到采樣數(shù)據(jù)之后,使用go tool pprof工具進(jìn)行 CPU 性能分析。
package main
import (
"math/rand"
"os"
"runtime/pprof"
"time"
)
func generate(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
func bubbleSort(nums []int) {
for i := 0; i < len(nums); i++ {
for j := 1; j < len(nums)-i; j++ {
if nums[j] < nums[j-1] {
nums[j], nums[j-1] = nums[j-1], nums[j]
}
}
}
}
func main() {
pprof.StartCPUProfile(os.Stdout)
defer pprof.StopCPUProfile()
n := 10
for i := 0; i < 5; i++ {
nums := generate(n)
bubbleSort(nums)
n *= 10
}
}
這里使用的runtime/pprof這個(gè)分析工具,需要指定快照打印的位置,這里打印到標(biāo)準(zhǔn)輸出了??梢詴?huì)與程序中的打印沖突。我們可以自己實(shí)現(xiàn)寫到文件中,這里可以用另一個(gè)開源工具替代github.com/pkg/profile,它會(huì)生成一個(gè)日志快照文件到臨時(shí)目錄。
package main
import (
"math/rand"
"github.com/pkg/profile"
"time"
)
func generate(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
func bubbleSort(nums []int) {
for i := 0; i < len(nums); i++ {
for j := 1; j < len(nums)-i; j++ {
if nums[j] < nums[j-1] {
nums[j], nums[j-1] = nums[j-1], nums[j]
}
}
}
}
func main() {
defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
n := 10
for i := 0; i < 5; i++ {
nums := generate(n)
bubbleSort(nums)
n *= 10
}
}
4.1.1 服務(wù)型應(yīng)用CPU分析
如果你的應(yīng)用程序是一直運(yùn)行的,比如 web 應(yīng)用,那么可以使用net/http/pprof庫,它能夠在提供 HTTP 服務(wù)進(jìn)行分析。這樣你的 HTTP 服務(wù)都會(huì)多出/debug/pprof endpoint,訪問它會(huì)得到類似下面的內(nèi)容:
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
http.ListenAndServe("0.0.0.0:8000", nil)
}

現(xiàn)在數(shù)據(jù)已經(jīng)可以采集了,那如何獲取快照呢?我們上一步的操作,在后臺(tái)起了一個(gè)http server服務(wù),我們直接點(diǎn)擊ui中的鏈接就可以拿到內(nèi)存快照了,例如點(diǎn)擊profile,我們就可以拿到一個(gè)30s的CPU快照,是一個(gè)*.pb.gz類型的二進(jìn)制文件,可用于我們后面的分析。
- /debug/pprof/profile:訪問這個(gè)鏈接會(huì)自動(dòng)進(jìn)行 CPU profiling,持續(xù) 30s,并生成一個(gè)文件供下載
- /debug/pprof/heap: Memory Profiling 的路徑,訪問這個(gè)鏈接會(huì)得到一個(gè)內(nèi)存 Profiling 結(jié)果的文件
- /debug/pprof/block:block Profiling 的路徑
- /debug/pprof/goroutines:運(yùn)行的 goroutines 列表,以及調(diào)用關(guān)系
4.2 go tool分析性能快照
不管是工具型應(yīng)用還是服務(wù)型應(yīng)用,我們使用相應(yīng)的 pprof 庫獲取數(shù)據(jù)之后,下一步的都要對這些數(shù)據(jù)進(jìn)行分析,我們可以使用go tool pprof命令行工具。
go tool pprof最簡單的使用方式為:
go tool pprof [binary] [source]
其中:
- binary 是應(yīng)用的二進(jìn)制文件,用來解析各種符號(hào);例如:go tool pprof -http=:9999 /Users/xxxx/pprof/pprof.samples.cpu.001.pb.gz
- source 表示 profile 數(shù)據(jù)的來源,可以是本地的文件,也可以是 http 地址。此方式會(huì)在命令窗口中按照交互模式例如:go tool pprof http://127.0.0.1:8000/debug/pprof/profile
注意事項(xiàng): 獲取的 Profiling 數(shù)據(jù)是動(dòng)態(tài)的,要想獲得有效的數(shù)據(jù),請保證應(yīng)用處于較大的負(fù)載(比如正在生成中運(yùn)行的服務(wù),或者通過其他壓測工具模擬訪問壓力)。否則如果應(yīng)用處于空閑狀態(tài),得到的結(jié)果可能沒有任何意義。
可以增加些參數(shù)來獲取更多信息,例如:
# 我們想獲取70s的內(nèi)存快照,可以增加-seconds參數(shù):
gotool pprof -seconds 70 http://127.0.0.1:8912/debug/pprof/profile
# 指定http接口,可以在ui上看到內(nèi)存快照,參見本文4.2.2
gotool pprof -http=0.0.0.0:8234 http://127.0.0.1:8912/debug/pprof/profile
4.2.1 直連服務(wù)分析
go tool + 線上服務(wù)http接口地址的方式:
go tool pprof http://127.0.0.1:8000/debug/pprof/profile
執(zhí)行上面的代碼會(huì)進(jìn)入交互界面如下:
runtime_pprof $ go tool pprof cpu.pprof
Type: cpu
Time: Jun 28, 2019 at 11:28am (CST)
Duration: 20.13s, Total samples = 1.91mins (568.60%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
我們可以在交互界面輸入top3來查看程序中占用 CPU 前 3 位的函數(shù):
(pprof) top3
Showing nodes accounting for 100.37s, 87.68% of 114.47s total
Dropped 17 nodes (cum <= 0.57s)
Showing top 3 nodes out of 4
flat flat% sum% cum cum%
42.52s 37.15% 37.15% 91.73s 80.13% runtime.selectnbrecv
35.21s 30.76% 67.90% 39.49s 34.50% runtime.chanrecv
22.64s 19.78% 87.68% 114.37s 99.91% main.logicCode
其中:
- flat:當(dāng)前函數(shù)占用 CPU 的耗時(shí)
- flat:: 當(dāng)前函數(shù)占用 CPU 的耗時(shí)百分比
- sun%:函數(shù)占用 CPU 的耗時(shí)累計(jì)百分比
- cum:當(dāng)前函數(shù)加上調(diào)用當(dāng)前函數(shù)的函數(shù)占用 CPU 的總耗時(shí)
- cum%:當(dāng)前函數(shù)加上調(diào)用當(dāng)前函數(shù)的函數(shù)占用 CPU 的總耗時(shí)百分比
- 最后一列:函數(shù)名稱
在大多數(shù)的情況下,我們可以通過分析這五列得出一個(gè)應(yīng)用程序的運(yùn)行情況,并對程序進(jìn)行優(yōu)化。
我們還可以使用list 函數(shù)名命令查看具體的函數(shù)分析,例如執(zhí)行list logicCode查看我們編寫的函數(shù)的詳細(xì)分析。
(pprof) list logicCode
Total: 1.91mins
ROUTINE ================ main.logicCode in .../runtime_pprof/main.go
22.64s 1.91mins (flat, cum) 99.91% of Total
. . 12:func logicCode() {
. . 13: var c chan int
. . 14: for {
. . 15: select {
. . 16: case v := <-c:
22.64s 1.91mins 17: fmt.Printf("recv from chan, value:%v\n", v)
. . 18: default:
. . 19:
. . 20: }
. . 21: }
. . 22:}
通過分析發(fā)現(xiàn)大部分 CPU 資源被 17 行占用,我們分析出 select 語句中的 default 沒有內(nèi)容會(huì)導(dǎo)致上面的case v:=<-c:一直執(zhí)行。我們在 default 分支添加一行time.Sleep(time.Second)即可。
4.2.2 快照文件+圖形化工具
這種快照文件的方式好處是更加直觀,可以通過圖形化界面來分析:
想要查看圖形化的界面首先需要安裝 graphviz 圖形化工具。Mac:brew install graphviz
接下來,可以用 go tool pprof 分析這份數(shù)據(jù)
go tool pprof -http=:9999 cpu.pprof
訪問 localhost:9999,可以看到這樣的頁面:

當(dāng)然我們還可以選擇VIEW,然后看火焰圖:

至此,我們就成功的獲取了每個(gè)函數(shù)占用的CPU時(shí)間了,下面就可以對占用較長的函數(shù)(平頂山部分)進(jìn)行優(yōu)化了。
5、常見性能優(yōu)化手段
5.1 使用高效的性能包
5.1.1 Json解析
我們將Json數(shù)據(jù)存放到Redis時(shí),取出時(shí)需要將其解析為Struct,但go官方自帶的庫性能較差,所以常常出現(xiàn)瓶頸,可選擇github.com/json-iterator 替換標(biāo)準(zhǔn)庫的 encoding/json(該庫主要的優(yōu)化手段詳見:http://jsoniter.com/benchmark.html#optimization-used)。 json-iterator 宣傳的性能如下圖:

5.1.2 深拷貝
還有時(shí)我們需要在項(xiàng)目中使用到深拷貝的場景,可以參考這篇文章,深拷貝性能對比:https://www.yuque.com/jinsesihuanian/gpwou5/xg20gn。
5.2 空間換時(shí)間
- 對于常見的Json解析問題,Redis大key問題,我們可以進(jìn)行多級(jí)緩存,將Redis中的大key數(shù)據(jù)緩存到內(nèi)存中,這里別忘了考慮帶來的緩存一致性問題。
- 對于一些map,slice,盡量在初始化時(shí)指定大小,減少內(nèi)存的重新分配
5.3 字符串拼接
字符串的拼接優(yōu)先考慮bytes.Buffer。由于string類型是一個(gè)不可變類型,但拼接會(huì)創(chuàng)建新的string。GO中字符串拼接常見有如下幾種方式,對性能要求很高的服務(wù)盡量使用bytes.Buffer進(jìn)行字符串拼接
- string + 操作 :導(dǎo)致多次對象的分配與值拷貝
- fmt.Sprintf :會(huì)動(dòng)態(tài)解析參數(shù),效率好不哪去
- strings.Join :內(nèi)部是[]byte的append
- bytes.Buffer :可以預(yù)先分配大小,減少對象分配與拷貝

使用strconv包替代fmt.Sprintf的格式化方式,性能比對見:https://www.cnblogs.com/yumuxu/p/4077234.html
5.4 異步處理
既然選用了Golang,自然要用到它簡單易用的并發(fā)機(jī)制啦,我們可以把一些不影響主流程的操作完全可以異步化,例如發(fā)送郵件、寫日志等??梢园岩恍I(yè)務(wù)場景并行處理,例如你要一次性讀取多個(gè)文件。
6、總結(jié)
代碼層面的優(yōu)化,是 us 級(jí)別的,而針對業(yè)務(wù)對存儲(chǔ)進(jìn)行優(yōu)化,可以做到 ms 級(jí)別的,所以優(yōu)化越靠近應(yīng)用層效果越好。對于代碼層面,優(yōu)化的步驟是:
- 利用壓測工具模擬場景所需的真實(shí)流量。壓測工具推薦使用 https://github.com/wg/wrk 或 https://github.com/adjust/go-wrk
- pprof 等工具查看服務(wù)的 CPU、MEM 耗時(shí)
- 鎖定平頂山邏輯,看優(yōu)化可能性:異步處理,空間換時(shí)間,使用高性能包 等
- 局部優(yōu)化完寫 benchmark 工具查看優(yōu)化效果
- 整體優(yōu)化完回到步驟一,重新進(jìn)行 壓測+pprof 看效果,看耗時(shí)能否滿足要求,如果無法滿足需求,那就換存儲(chǔ)吧~??
后續(xù)我會(huì)給大家出一篇關(guān)于Golang服務(wù)的代碼開發(fā)建議,我們下期見,Peace??
我是簡凡,一個(gè)勵(lì)志用最簡單的語言,描述最復(fù)雜問題的新時(shí)代農(nóng)民工。求點(diǎn)贊,求關(guān)注,如果你對此篇文章有什么疑惑,歡迎在我的微信公眾號(hào)中留言,我還可以為你提供以下幫助:
- 幫助建立自己的知識(shí)體系
- 互聯(lián)網(wǎng)真實(shí)高并發(fā)場景實(shí)戰(zhàn)講解
- 不定期分享Golang、Java相關(guān)業(yè)內(nèi)的經(jīng)典場景實(shí)踐
我的博客:https://besthpt.github.io/
微信公眾號(hào):"簡凡丶"