Goroutine Local Storage 的一些實現方案和必要性討論

Java 的 ThreadLocal 是 Java 爲每個線程提供的專用存儲,把一些信息放在 ThreadLocal 上,可以用於來簡化上層應用的 API 使用。一個顯著的應用場景是,有了 ThreadLocal 後,就不需要在調用棧裏的每個函數上都增加額外的參數來傳遞一些與調用鏈和日誌鏈路追蹤相關的上下文信息了。

Go Team 針對增加 LocalStorage 的提案,明確說明過,他們更推薦顯式地使用 Context 參數而不是使用 LocalStorage 來進行上下文信息的傳遞。社區裏倒是有幾個 GLS(Goroutine Local Storage)的實現方案,我們團隊也在系統裏使用了 GLS,應用後並沒有明顯的性能降低,主要還是不想在每個函數定義上都添加參數來傳遞用來做日誌鏈路追蹤的 TraceId,但是並不建議業務邏輯依賴這些三方的 GLS 庫

關於是否需要增加 GLS 的討論以及 GLS 帶來的性能和不兼容問題還是挺多的,正好看到一篇文章對 Go 語言是否該引入 GLS 的討論進行了總結,在這裏分享給大家。

原文作者:蘭陵子

原文鏈接:http://lanlingzi.cn/post/technical/2016/0813_go_gls/

 背景

最近在設計調用鏈與日誌跟蹤的 API,發現相比於 Java 與 C++,Go 語言中沒有原生的線程(協程)上下文,也不支持 TLS(Thread Local Storage),更沒有暴露 API 獲取 Goroutine 的 Id(後面簡稱GoId)。這導致無法像 Java 一樣,把一些信息放在 TLS 上,用於來簡化上層應用的 API 使用:不需要在調用棧的函數中通過傳遞參數來傳遞調用鏈與日誌跟蹤的一些上下文信息。

在 Java 與 C++ 中,TLS 是一種機制,指存儲在線程環境內的一個結構,用來存放該線程內獨享的數據。進程內的線程不能訪問不屬於自己的 TLS,這就保證了 TLS 內的數據在線程內是全局共享的,而對於線程外卻是不可見的。

在 Java 中,JDK 庫提供Thread.CurrentThread()來獲取當前線程對象,提供ThreadLocal來存儲與獲取線程局部變量。由於 Java 能通過Thread.CurrentThread()獲取當前線程,其實現的思路就很簡單了,在 ThreadLocal 類中有一個 Map,用於存儲每一個線程的變量。

ThreadLocal 的 API 提供瞭如下的 4 個方法:

public T get()
protected  T initialValue()
public void remove()
public void set(T value)

在 Go 語言中,而 Google 提供的解決方法是採用golang.org/x/net/context包來傳遞 GoRoutine 的上下文。對 Go 的 Context 的深入瞭解可參考我之前的分析:理解 Go Context 機制Context也是能存儲 Goroutine 一些數據達到共享,但它提供的接口是WithValue函數來創建一個新的Context對象。

func WithValue(parent Context, key interface{}, val interface{}) Context {
	return &valueCtx{parent, key, val}
}

type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

從上面代碼中可以看出,Context設置一次 Value,就會產生一個Context對象,獲取 Value 是先找當前Context存儲的值,若沒有再向父一級查找。獲取Value可以說是多 Goroutine 訪問安全,因爲它的接口設計上,是隻一個 Goroutine 一次設置Key/Value,其它多 Goroutine 只能讀取KeyValue

爲什麼無獲取 GoId 接口

This, among other reasons, to prevent programmers for simulating thread local storage using the goroutine id as a key.

官方說,就爲了避免採用Goroutine Id當成Thread Local StorageKey

Please don’t use goroutine local storage. It’s highly discouraged. In fact, IIRC, we used to expose Goid, but it is hidden since we don’t want people to do this.

用戶經常使用 GoId 來實現goroutine local storage,而 Go 語言不希望用戶使用goroutine local storage

when goroutine goes away, its goroutine local storage won’t be GCed. (you can get goid for the current goroutine, but you can’t get a list of all running goroutines)

不建議使用goroutine local storage的原因是由於不容易 GC,雖然能獲當前的 GoId,但不能獲取其它正在運行的 Goroutine。

what if handler spawns goroutine itself? the new goroutine suddenly loses access to your goroutine local storage. You can guarantee that your own code won’t spawn other goroutines, but in general you can’t make sure the standard library or any 3rd party code won’t do that.

另一個重要的原因是由於產生一個 Goroutine 非常地容易(而線程通用會採用線程池),新產生的 Goroutine 會失去訪問goroutine local storage。需要上層應用保證不會產生新的 Goroutine,但我們很難確保標準庫或第三庫不會這樣做。

thread local storage is invented to help reuse bad/legacy code that assumes global state, Go doesn’t have legacy code like that, and you really should design your code so that state is passed explicitly and not as global (e.g. resort to goroutine local storage)

TLS 的應用是幫助重用現有那些不好(遺留)的採用全局狀態的代碼。而 Go 語言建議是重新設計代碼,採用顯示地傳遞狀態而不是採用全局狀態(例如採用goroutine local storage)。

其它手段獲取 GoId

雖然 Go 語言有意識地隱藏 GoId,但目前還是有手段來獲取 GoId:

開源 goroutine local storage 實現

只要有機制獲取 GoId,就可以像 Java 一樣來採用全局的 map 實現goroutine local storage,在 Github 上搜索一下,發現有兩個:

第二個有人在 2013 年測試過性能,數據如下:

BenchmarkGetValue 500000 2953 ns/op 

BenchmarkSetValues 500000 4050 ns/op

上面的測試結果看似還不錯,但goroutine local storage實現無外乎是map+RWMutex,存在性能瓶頸:

不管怎麼樣,沒有官方的 GLS,的確不是很方便,第三方實現又存在性能與不兼容風險。連jtolds/gls作者也貼出其它人的評價:

“Wow, that’s horrifying.”

“This is the most terrible thing I have seen in a very long time.”

“Where is it getting a context from? Is this serializing all the requests? What the heck is the client being bound to? What are these tags? Why does he need callers? Oh god no. No no no.”

小結

Go 語言官方認爲 TLS 來存儲全局狀態是不好的設計,而是要顯示地傳遞狀態。Google 給的解決方法是golang.org/x/net/context

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/luUxiLZCf_x7ZA1LUlP7sA