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

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

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

關因而否須要增長GLS的討論以及GLS帶來的性能和不兼容問題仍是挺多的,正好看到一篇文章對 Go 語言是否該引入GLS的討論進行了總結,在這裏分享給你們。golang

原文做者:蘭陵子安全

原文連接:lanlingzi.cn/post/techni…markdown

1 背景

最近在設計調用鏈與日誌跟蹤的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,用於存儲每個線程的變量。oop

ThreadLocal的API提供了以下的4個方法:post

public T get()
protected  T initialValue()
public void remove()
public void set(T value)
複製代碼
  • T get():返回此線程局部變量的當前線程副本中的值,若是這是線程第一次調用該方法,則建立並初始化此副本。
  • protected T initialValue(): 返回此線程局部變量的當前線程的初始值。最多在每次訪問線程來得到每一個線程局部變量時調用此方法一次,即線程第一次使用get()方法訪問變量的時候。若是線程先於get方法調用set(T)方法,則不會在線程中再調用initialValue方法。
  • void remove(): 移除此線程局部變量的值。這可能有助於減小線程局部變量的存儲需求。若是再次訪問此線程局部變量,那麼在默認狀況下它將擁有其 initialValue
  • void set(T value)將此線程局部變量的當前線程副本中的值設置爲指定值。許多應用程序不須要這項功能,它們只依賴於initialValue()方法來設置線程局部變量的值。

在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

2 爲何無獲取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)。

3 其它手段獲取GoId

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

  • 修改源代碼暴露GoId,但Go語言可能隨時修改源碼,致使不兼容

    在標準庫的runtime/proc.go(Go 1.6.3)中的newextram函數,會產生個GoId:

    mp.lockedg = gp
    gp.lockedm = mp
    gp.goid = int64(atomic.Xadd64(&sched.goidgen, 1))
    複製代碼
  • 經過runtime.Stack來分析Stack輸出信息獲取GoId。

    在標準庫的runtime/mprof.go(Go 1.6.3)中,runtime.Stack會獲取gp對象(包含GoId)並輸出整個Stack信息:

    func Stack(buf []byte, all bool) int {
        if all {
            stopTheWorld("stack trace")
        }
    
        n := 0
        if len(buf) > 0 {
            gp := getg()
            sp := getcallersp(unsafe.Pointer(&buf))
            pc := getcallerpc(unsafe.Pointer(&buf))
            systemstack(func() {
                g0 := getg()
                g0.m.traceback = 1
                g0.writebuf = buf[0:0:len(buf)]
                goroutineheader(gp)
                traceback(pc, sp, 0, gp)
                if all {
                    tracebackothers(gp)
                }
                g0.m.traceback = 0
                n = len(g0.writebuf)
                g0.writebuf = nil
            })
        }
    
        if all {
            startTheWorld()
        }
        return n
    }
    複製代碼

    從文件名就能夠看出,runtime/mprof.go是用於作Profile分析,獲取Stack確定性能不會太好。從上面的代碼來看,若第二個參數指定爲true,還會STW,業務系統不管如何都沒法接受。若Go語言修改了Stack的輸出,分析Stack信息也會致使沒法正常獲取GoId。

  • 通用runtime.Callers來給調用Stack來打標籤

    代碼參考:github.com/jtolds/gls/…

  • 經過內聯c或者內聯彙編

    go版本1.5,x86_64arc下彙編,估計也不通用

    // func GoID() int64
    TEXT s3lib GoID(SB),NOSPLIT,$0-8
    MOVQ TLS, CX
    MOVQ 0(CX)(TLS*1), AX
    MOVQ AX, ret+0(FP)
    RET
    複製代碼

4 開源goroutine local storage實現

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

  • tylerb/gls

    GoId是經過runtime.Stack來分析Stack輸出信息獲取GoId。

  • jtolds/gls

    GoId是通用runtime.Callers來給調用Stack來打標籤

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

BenchmarkGetValue 500000 2953 ns/op BenchmarkSetValues 500000 4050 ns/op

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

  • Goroutine不像Thread,它的個數能夠上十萬併發,當這麼多的Goroutine同時競爭同一把鎖時,性能會急劇惡化。
  • GoId是經過分析調用Stack的信息來獲取,也是一個高成本的調用,一個字:慢。

無論怎麼樣,沒有官方的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.」

5 小結

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

相關文章
相關標籤/搜索