這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
序言
在C/C++/Java等語言中,我們可以直接擷取Thread Id,然後通過映射Thread Id和二級調度Task Id的關係,可以在日誌中列印當前的TaskId,即使用者不感知Task Id的列印,適配層統一封裝,這使得多線程並發的日誌的查看或過濾變得非常容易。
Goroutine是Golang中輕量級線程的實現,由Go Runtime管理。Golang在語言層級支援輕量級線程,叫攜程。Golang標準庫提供的所有系統叫用作業(當然也包括所有同步IO操作),都會出讓CPU給其他Goroutine。這讓事情變得非常簡單,讓輕量級線程的切換管理不依賴於系統的線程和進程,也不依賴於CPU的核心數量。
Goroutine非常亮眼,但是自從go1.4版本以後,Goroutine Id無法直接從Go Runtime擷取了。
這是Golang的開發人員故意為之,避免開發人員濫用Goroutine Id實現Goroutine Local Storage(類似java的Thread Local Storage), 因為Goroutine Local Storage很難進行記憶體回收。因此儘管Go1.4之前暴露出了相應的方法,現在已經把它隱藏了。
這個決策有點因噎廢食,對於高並發日誌的查看和過濾就變得比較困難。儘管在日誌中可以使用業務本身的Id,但是在很多函數中僅僅為了列印而增加一些入參對於追求Clean Code的程式員實在無法接受。
筆者在本文中將找出一種簡單高效穩定的解決方案,並給出最佳實務。
既有的幾種方法
通過彙編擷取
複雜度高,位移地址隨版本可能有變化,不建議使用
通過第三方庫擷取
相關的第三方庫可以在github上找,比如:
https://github.com/jtolds/glshttps://github.com/huandu/goroutine
穩定性未知,效能也不高,不建議使用
通過runtime.Stack擷取
它利用runtime.Stack的堆棧資訊,將當前的堆棧資訊寫入到一個slice中,堆棧的第一行為 “goroutine #### […”,其中“####”就是當前的Goroutine Id,通過這個花招就可以實現Goid函數了。
採用該方法時,Goid函數的實現如下:
func Goid() int { defer func() { if err := recover(); err != nil { fmt.Println("panic recover:panic info:%v", err) } }() var buf [64]byte n := runtime.Stack(buf[:], false) idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] id, err := strconv.Atoi(idField) if err != nil { panic(fmt.Sprintf("cannot get goroutine id: %v", err)) } return id}
通過修改編譯器源碼擷取
在go源碼runtime包中增加函數Goid,直接調用runtime的getg函數擷取,具有簡單高效穩定的優點,同時每個團隊可以通過容器來部署自己的微服務。
該方法將在“最佳實務”一節中詳述。
方法三和方法四比較
分別採用方法三和方法四,將Goid函數連續調用10000次的效能資料如下:
對於方法三,擷取堆棧資訊會影響效能,所以建議對效能不敏感的情境採用;
對於方法四,直接調用runtime的getg函數擷取,效率最高,所以建議對效能有苛刻要求的情境採用。
本文關注效能,所以採用方法四。
最佳實務
下載go1.4版本的編譯器
在Golang的官方網站下載go1.4版本的編譯器,URL如下:
https://golang.org/dl/
解壓縮,將go檔案夾rename成go1.4,然後移動到$HOME目錄下。
修改go1.7.3版本的編譯器代碼
在Golang的官方網站下載go1.7.3版本的源碼。
編輯src/runtime/proc.go檔案,在尾部添加函數Goid:
func Goid() int64 { _g_ := getg() return _g_.goid}
運行src/make.bash命令(預設使用$HOME/go1.4目錄下的編譯器),編譯go1.7.3的新版本。
編譯完成後,將go檔案夾拷貝到GOROOT目錄下,使之生效:
$ go versiongo version go1.7.3 linux/amd64
測試代碼
我們類比一個完全可以並行的計算任務:計算N個整型數的總和。我們可以將所有整型數分成M份,M即CPU的個數。讓每個CPU開始計算分給它的那份計算任務,最後將每個CPU的計算結果再做一次累加,這樣就可以得到所有N個整型數的總和,實現代碼如下:
type Vector []intfunc (v Vector) DoSome(i, n int, u Vector, c chan int, add *int) int { for ; i < n; i++ { *add += u[i] } id := runtime.Goid(id) fmt.Println("id:", id) c <- 1 return 1}const NCPU = 16func (v Vector) DoAll(u Vector) int { c := make(chan int, NCPU) var add [NCPU]int sum := 0 for i := 0; i < NCPU; i++ { go v.DoSome(i * len(v) / NCPU, (i + 1)* len(v) / NCPU, u, c, &add[i]) } for i := 0; i < NCPU; i++ { <- c } for i := 0; i < NCPU; i++ { sum += add[i] } return sum}func main() { x := 0 y := 0 v := make(Vector, 160) for i := 0; i < 160; i++ { v[i] = i x += i } y = v.DoAll(v) fmt.Println("x =", x, "and y =", y)}
日誌
通過查看日誌,我們已將成功擷取到了Goroutine Id。一個字,完美!
id: 20id: 13id: 7id: 12id: 14id: 9id: 5id: 17id: 16id: 10id: 6id: 15id: 18id: 19id: 8id: 11x = 12720 and y = 12720
適配層封裝
我們可以將glog等第三方庫的日誌介面進行簡單封裝,隱藏goid的擷取和列印過程,使得使用者輕鬆。
小結
本文針對Golang中Goroutine的高並發的日誌難以查看或過濾的問題,分析了既有的幾種擷取Goroutine Id的方法,最後找到一種簡單高效穩定的方法,即通過修改編譯器源碼擷取,並給出了最佳實務,希望對讀者有一定的協助。