動手寫一個基於golang的微服務熔斷器
上一篇咱們介紹了一些熔斷器的概念和原理,咱們今天就談一下功能組件的劃分和具體的實現。git
對目前開源熔斷器的對比
針對上一篇內容出現的hystrix和go-breaker,我梳理了兩方優勢github
熔斷器名稱 | hystrix | go-breaker |
---|---|---|
滑動窗口計數 | 支持 | 不支持 |
限流 | 支持 | 不支持 |
阻塞讀 | 是 | 是 |
對半開啓的處理 | 滑動計數器+閾值 | 連續成功則轉移到close |
對監控的支持 | 支持metric採集 | 不支持 |
降級處理 | 支持hook | 支持hook |
解決併發尖刺 | 不支持 | 支持 |
代碼結構易讀性 | 稍差 | 較好 |
咱們一一展開來說golang
計數模塊
計數模塊是熔斷器的核心,網上有針對計數器的大篇幅的分析針對這裏引用知乎上一位大佬的比較類型的文章,根據最後的比較咱們選擇滑動窗口的算法來完成計數需求。 在hystrix的設計中,滑動窗口的比較重要的是寫入時刻和讀取時刻,由於咱們很容易想到在這兩個環節涉及到對一塊內存併發讀寫的問題,首先咱們不建議採用go-breaker的全加鎖(讀寫都加鎖)的設計,由於鎖在發生競爭時會掛起線程,從而下降了CPU的使用率和共享內存總線上的同步通訊量,那麼咱們參考hystrix,採用異步提交的方法,也就是將結果放入一個隊列中,不斷消費這個隊列,這麼作有幾點好處算法
消息串行化,減小寫入讀取數據沒必要要競爭 在數據生產層--->數據存儲層中間構造出中間層,方便進行監控統計收集等操做 方便控制消息的消費狀況
在實現上採用channel的數據結構,消費有高效保證。可是事物都有兩面性,這種設計帶來的問題有markdown
滑動窗口統計須要訪問當前窗口內全部數據 串行化沒有將統計性能發揮最大(雖然在計數豐方面表現很快) 業務要容許流量尖刺的出現(假設沒有加限流)
其中2,3點通過調研都在業務容許範圍內,且針對第三點咱們能夠增長限流策略來完善這一點。 數據結構
限流
hystrix天生限流,全部請求先過令牌桶而後進入熔斷統計,go-breaker尚未這方面支持,在限流這裏咱們懷疑要不要在一塊兒作(畢竟熔斷是熔斷,限流是限流),因此作了另外一個方案,在半開啓時進行限流放行請求,這樣比較符合半開啓時的請求經過策略,同時進行統計,限流策略採用退化版本令牌桶,方法以下:併發
type limitPoolManager struct { max int tickets chan *struct{} lock *sync.RWMutex } /* 方法返回一個限流器 */ func NewLimitPoolManager(max int) *limitPoolManager { lpm := new(limitPoolManager) tickets := make(chan *struct{}, max) for i := 0; i < max; i++ { tickets <- &struct{}{} } lpm.max = max lpm.tickets = tickets lpm.lock = &sync.RWMutex{} return lpm } /* 方法填充限流器全部令牌 */ func (this *limitPoolManager) ReturnAll() { this.lock.Lock() defer this.lock.Unlock() if len(this.tickets) == 0 { for i := 0; i < this.max; i++ { this.tickets <- &struct{}{} } } } /* 方法返回一個令牌,獲得令牌返回true,令牌用完後返回false */ func (this *limitPoolManager) GetTicket() bool { this.lock.RLock() defer this.lock.RUnlock() select { case <-this.tickets: return true default: return false } } /* 方法返回剩餘令牌數 */ func (this *limitPoolManager) GetRemaind() int { this.lock.RLock() defer this.lock.RUnlock() return len(this.tickets) }
阻塞讀
由於串行化設計因此在每次收失敗請求時能夠對窗口內數據進行錯誤率轉化。避免hystrix與go-breaker的鎖爭搶異步
半開啓處理
以上+本節主流程基本完結,現梳理整個流程明確half-open時處理:函數
當熔斷器爲close時。只有當出現錯誤請求時,才進行錯誤率統計,統計過閾值則狀態轉移到open,正確請求則正常計數。 當熔斷器爲half-open時,僅當令牌桶中還有令牌時接收請求不然熔斷。令牌桶中還有令牌時,出現錯誤請求則更新熔斷休眠時間並返回全部令牌等待下次半開啓,正常請求則進入半開啓時統計達到閾值則狀態轉移到close。 當熔斷器爲open時,僅當熔斷休眠時間小於當前時間時,當熔斷器狀態轉移到half-open,能夠進行第二條,不然執行熔斷
首先判斷是不是半開啓狀態微服務
switch this.counter.GetStatus() { case STATE_OPEN: if this.cycleTime < time.Now().Local().Unix() { return OPEN_TO_HALF_ERROR } return BREAKER_OPEN_ERROR }
其次若是是半開啓狀態則取令牌,取到令牌則執行請求,進入熔斷時計數,不然直接熔斷
/*取令牌*/ if !this.lpm.GetTicket() { this.safelCalllback(fallback, BREAKER_OPEN_ERROR) return nil } /*執行方法*/ runErr := run() if runErr != nil { this.fail() this.safelCalllback(fallback, runErr) return runErr } this.success() return nil
流量尖刺處理
流量尖刺的削峯伴隨着限流的邏輯,因此能夠在請求到達時優先進入令牌桶
監控&降級
提供hook函數,在限流或者執行失敗時能夠提供降級或者回掉
/* 執行函數 */ type runFunc func() error /* 回調函數 */ type fallbackFunc func(error) /* Do方法結合熔斷策略執行run函數 其中參數包括:上下文ctx,策略名name,將要執行方法run,以及回調函數fallback.其中ctx,name,run必傳 run函數的錯誤會直接同步返回,回調函數fallback接收除了run錯誤之外還會接收熔斷時錯誤,調用方若是須要降級可在fallback中本身判斷 */ func Do(ctx context.Context, name string, run runFunc, fallback fallbackFunc) error { ........ }
代碼結構易讀性
hystrix代碼會比go-breaker冗餘一些(畢竟go-breaker300行搞定。。。不要槓)。go-breaker將請求階段分爲請求前,請求中,請求後三個階段,這裏能夠借鑑一下,容易理清思路。 以上是動手寫個熔斷器的設計和分析思路。 下面是github地址:https://github.com/EAHITechnology/breaker.git ,歡迎給出意見。