一文徹底讀懂 hystrix-go 原始碼

Remember發表於2020-12-30

開篇

上篇文章主要介紹了 hystrix-go 的使用以及原理,這篇文章讓我們全面的解析原始碼。本文很長,請耐心看完。另外,由於直接放原始碼很是影響手機閱讀體驗,我把原始碼都截成圖片了。

還是上篇的例子


大部分文章只是說明每個模組的職責和功能,考慮到如果只是單純說明,讀者還是很難把整體的流程連線起來。因此我打算從這個例子一步步解析。
就直接從開頭 hystrix.ConfigureCommand 開始吧。

上篇文章提過,這個操作主要是為每個 commandName 自定義自己的規則配置。如果未自定義,那麼會使用預設值。 最終會把配置值儲存在 circuitSettings 這個 map型別中,它的初始化操作是在 init()執行的。

接下來執行 hystrix.DoDo 是一個同步的操作,它會阻塞等待,直到執行函式結束或者熔斷器返回錯誤,如:斷路器開啟、超出最大併發數。

此函式需要三個引數,第一個參數列示 commandName 的名稱,第二個引數就是正常的業務的匿名函式,比如在函式中進行外部服務呼叫。如果呼叫失敗,那麼就會執行第三個引數的操作,我們可以稱之為保底操作,當熔斷器開啟的時候,系統也是會直接呼叫此函式。這兩個引數的型別分別是匿名函式和閉包函式。

從圖中可以看出,Do 函式只是把傳入的後兩個引數進一步封裝成函式。然後呼叫 DoC

Doc 函式第一個引數是上下文 context.Contextcontext.Context 一般出現在不同 Goroutine 之間同步指定資料。如果你使用過 gin 框架,經常和它打交道。

第三和第四的引數即 Do 中進一步包裝的兩個閉包函式。所謂閉包,我的理解是:存在自由的變數。這個自由的變數取決於執行閉包函式時的環境,在 DoC中,runFuncfallbackFuncC 型別
hystrix-go 原始碼解析
也就是說這兩個閉包的自由變數是 context.Context

Doc函式中 變數 rf 不再解釋。由於我們在呼叫 Do函式時傳遞了第三個引數,因此執行 errChan = GoC(ctx, name, r, f)。最下面使用select 可以監控多 channel。當某個 channel 有資料時,從其中讀取。我們接著往下看 GoC

Goc是核心程式碼塊。它是真正執行你的核心業務函式的地方。我先大體介紹一些這個函式核心功能。
它會先去驗證一些規則,比如判斷熔斷器是否開啟,決定當前是否可以執行你的業務。判斷是否可以獲取訪問令牌。如果可以,執行你的業務邏輯,成功了上報成功的狀態,失敗了,除了上報狀態,如果傳入了異常處理的函式,那麼執行異常處理的函式。另外還有一些歸還令牌等操作。我們來看程式碼。

這段程式碼就不解釋了吧。但是我們可以來看看 command 結構體中還有啥引數。

command 裡面,關鍵的兩個欄位是 eventscircuit。其實 events 主要是儲存事件型別資訊,比如執行成功的 success,或者失敗的 timeoutcontext_canceled 等。 circuit 是指標型別 CircuitBreaker, CircuitBreaker就是真正的熔斷器 。command 主要是記錄單個執行的狀態以及和熔斷器進行一些執行互動。互動什麼? 主要向 CircuitBreaker 上報執行狀態事件。

接下來看下面的程式碼

GetCircuit(name),從函式名就知道是通過名稱獲取一個熔斷器。

這個函式程式碼很清晰,如果沒有從 circuitBreakers中查詢到對應的 CircuitBreaker,那麼就建立一個。

這裡的程式碼有點小細節。首先 circuitBreakersmap 型別,我們都知道 map 並不是併發安全的。所以在查詢的的時候加了讀鎖。如果沒找到值,那麼解除讀鎖。嘗試獲取寫鎖,我們在寫鎖裡面,又進一步確認是否存在circuitBreakers,為什麼需要這樣操作? 這是因為在我們釋放讀鎖到獲取寫鎖過程中有可能存在其他的 Goroutine 搶先一步建立。所以這裡需要進一步確認,此時不存在,那就真的不存在,通過 name 生成一個 circuitBreakers。具體看下 newCircuitBreaker(name) 函式。

主要是初始化建立一個熔斷器 CircuitBreaker操作,我們可以看看 CircuitBreaker 都有哪些欄位。

一文讀懂 hystrix-go 原始碼
主要說明幾個欄位,open 表示當前熔斷器是否開啟,executorPool 是流量控制中心,所有的請求都需要先獲取到令牌。metrics 的型別是 *metricExchange,可以看成是上報執行狀態事件的載體。通過它把執行狀態資訊儲存到實際熔斷器執行各個維度狀態(成功次數,失敗次數,超時……)的資料集合中。

newCircuitBreaker(name) 初始化的同時也初始化了 executorPoolmetrics

先看 newMetricExchange(name)。看看它 metricExchange 結構

Updates 是一個 channel 型別,通過 Updates 上報執行事件集合。metricCollectors 儲存的是 metricCollector.MetricCollector 切片,而 metricCollector.MetricCollector 是一個介面型別。

一文讀懂 hystrix-go 原始碼

newMetricExchange(name) 中,

可以看到,初始化 Updates 通道的的容量是 2000。初始化 metricCollectors 主要邏輯在 InitializeMetricCollectors

關鍵的地方我標明瞭。再看看 newDefaultMetricCollector

此函式返回一個 MetricCollector 型別,結構體 DefaultMetricCollector 實現了 MetricCollector 所有方法。再看看 DefaultMetricCollector,不正是儲存熔斷器執行狀態所有資訊嘛。看看指標型別 rolling.Number

rolling.Number 是真正儲存各個執行事件狀態資訊的底層儲存結構。它是如何只儲存 10 秒內的資訊的。

驚不驚喜?
newMetricExchange(name) 還有一個細節,會單獨開啟一個 g 執行 go m.Monitor() 去接收 channel 型別的 Updates 資訊,即執行事件狀態資訊。

在接收到事件資訊後,呼叫 IncrementMetrics 先做狀態資訊的整合,最終把整合後的執行狀態事件資訊上報 collector.Update(r)

接著回頭看初始化流量控制中心 newExecutorPool(name)。先看看 executorPool結構。

主要關注兩個欄位,Tickets表示的就是訪問令牌帶緩衝通道的 channel ,初始化 channel 容量取決於一開始你設定的MaxConcurrentRequests。當有請求到來時,從 channel 中拿出一個令牌,呼叫後重新歸還。 poolMetrics 就是流量控制的具體指標。

Executed表示當前桶已經處理的請求數量。此外在 newExecutorPool(name) 函式中,和剛才套路一樣,啟動一個 go m.Monitor() 專門去更新當前桶的最大值。

到這裡,GetCircuit(name) 獲取一個熔斷器的程式碼講完了。
回到 GoC ,我們得到一個 circuitBreakers 的指標。

接下來我們建立一個條件變數 sync.NewCond。條件變數的場景是當共享資源發生變化時,通知那些被互斥鎖鎖住的執行緒。
在這裡它是用來協調通知你可以歸還訪問令牌了。

接著有一句 returnOnce := &sync.Once{},關於 sync.Once,之前解析過一篇文章你真的瞭解 sync.Once 嗎。這裡它存在的意義是什麼? 我們往下看,就會發現其實到後面開啟了兩個 Goroutine

它的作用是確保由最快那個 Goroutine 執行 errWithFallback()reportAllEvent(),而且保證只會執行一次。

接著往下看,
一文讀懂 hystrix-go 原始碼
這個函式就是就是用來上報執行事件的。

前面都好懂,我們從這段開始看:

這裡操作這句話是什麼意思?因為存在一種情況:當前熔斷器是開啟的,並且已經過了 SleepWindow 時間,此時請求就屬於半開的狀態,允許嘗試執行,如果執行成功,那麼就說明服務恢復了,可以關閉熔斷器了。接下來,

組裝執行狀態狀態事件,然後塞進 Updates 通道中。正好被初始化 metricExchange 另開的 Goroutine 接收,這樣,這個上報流程就對應上了。

接下來就是剛才截圖的兩個 Goroutine,我們先看第一個。

截圖了上前半部分。上來一個 defer func() { cmd.finished <- true }() 作為正常執行結束的通知。然後就是 cmd.circuit.AllowRequest()判斷是否能請求。

有兩種情況可以往下走,第一種熔斷器是關閉的。對應 circuit.IsOpen()

首先判斷熔斷器是否被強制開啟或者已經開啟,如果是,直接返回 true。否則說明是當前熔斷器處於關閉。接著判斷過去十秒內各個桶值的和是否小於設定的 RequestVolumeThreshold值,如果小於,說明熔斷器還應該是關閉狀態,返回 false 。如果大於等於,那麼應該進一步去判斷錯誤百分比 是否超出自己的設定的ErrorPercentThreshold。如果超出了,那麼說明錯誤率過高,此時需要開啟熔斷器。

這段程式碼很好懂,有意思的是 int(errPct + 0.5)+0.5 是為了四捨五入,如果直接浮點數轉換成整形,那麼必然就是去除小數點。加了 0.5,那麼假設 ErrorPercentThreshold設定 50,如果是 <45.5,熔斷器就繼續關閉,否則開啟熔斷器。

如果circuit.IsOpen()不符合,那麼再看 circuit.allowSingleTest()。雖然熔斷器是開啟的,但是如果當前的時間已經大於 (上次開啟熔斷器的時間+SleepWindow 的時間),這時候熔斷器屬於半開的狀態,可以執行下一步。那麼就會返回 true

如果 cmd.circuit.AllowRequest 返回 false,那麼就是執行 returnTicket 歸還令牌(儘管這時候還沒有令牌可言)。這段程式碼很有趣,通過變數 ticketCheckedsync.NewCond 實現的邏輯。cmd.errorWithFallback(),上報熔斷器已開啟事件(circuit open)以及執行 fallBakck 保底函式(如果存在的話),執行結束,響應。

如果返回 true,接著玩下走,


如果拿不到訪問令牌,那麼和剛才一樣,上報當前請求已超過併發數事件(max concurrency),執行保底操作,響應。

如果拿到訪問令牌,那麼真正執行自己的業務程式碼 run(ctx)。套路和上面相似。
再看第二個 Goroutine

三個 case分別表示:正常執行結束、業務執行被取消以及超時。至於為什麼說 Do是同步操作,因為在 Doc 最後,
,而 Go 則是一個非同步過程。
到這裡,從 Do 開始執行的大體流程就走完了。
最後我們可以直接給出它的大體流程圖。

他們結構體之間的一些關係圖。

分享 一些奇奇怪怪的東西

圖片

? 長按關注庫裡的深夜食堂

本作品採用《CC 協議》,轉載必須註明作者和本文連結
吳親庫裡

相關文章