淺析hystrix斷路器

斷路器&&hystrix簡介

  • 斷路器代理了服務調用方對提供方的請求。監控最近請求的失敗和超時次數,在下游服務因爲過載或者故障無法提供響應時,斷路器中請求失敗率會大大提升,超過一定閾值後,斷路器會打開,切斷服務調用方和提供方的聯繫,此時調用者會執行失敗邏輯或者直接返回異常。同時斷路器還有檢測恢復機制,允許服務調用者嘗試調用服務提供者以檢測它是否恢復正常,若恢復正常則關閉斷路器,恢復正常調用。
  • 斷路器的三態
    • 關閉狀態:程序正常運行時,大多數都處於此狀態,服務調用者正常訪問服務提供者。斷路器會統計週期時間內的請求總次數和失敗數的比例。
    • 打開狀態:最近失敗頻率超過了預設的閾值以後,斷路器進入打開狀態,服務調用者對服務提供者的調用失效,服務調用進入失敗邏輯或者返回異常。
    • 半開狀態:斷路器在進入打開狀態時候會啓動一個超時定時器,在定時器到達時,它會進入到半開狀態,此時執行“恢復檢測機制”,即調用者嘗試對服務提供者發起少量調用請求。如果這些請求都成功執行,那麼斷路器就認爲服務提供者已恢復正常,斷路器則關閉,失敗計數器復位。如果這些請求失敗,斷路器返回到打開狀態,並重新啓動超時定時器,重新進行檢測恢復。
  • hystrix

  hystrix執行流程

  

 

hystrix使用實例

  1. 準備兩個http server的 service
  2. 通過配置hystrix的命令,驗證斷路器的熔斷,檢測恢復功能
  3. 示例代碼中demo1,配置service1的請求併發數量大於100時,觸發斷路器打開,配置service2的併發請求數量大於5時,觸發斷路器打開。兩個服務同時用50個goroutine去併發請求,可以看到,請求service2會打印“”失敗回滾日誌;
  4. 示例代碼demo2中,service1 先正常執行,然後關閉五秒後,讓hystrix因爲服務請求失敗進入到打開狀態;因爲配置了5秒的超時窗口時間,繼而在五秒窗口時間後進入到半開狀態,此時service1保持關閉,hystrix將會重新打開;最後,等再過五秒窗口時間,hystrix進入到半開狀態,此時service1打開,使所有請求正常,hystrix檢測所有請求正常,斷路器進入關閉狀態;

 

  1 // Service1代碼
  2 package main
  3 
  4 import (
  5    "github.com/gin-gonic/gin"
  6 )
  7 
  8 func main() {
  9    r := gin.Default()
 10    r.GET("/ping", func(c *gin.Context) {
 11       c.JSON(200, gin.H{
 12          "message": "this is service1",
 13       })
 14    })
 15    r.Run("127.0.0.1:11012")
 16 }
 17 // Service2代碼
 18 package main
 19 
 20 import (
 21    "github.com/gin-gonic/gin"
 22 )
 23 
 24 func main() {
 25    r := gin.Default()
 26    r.GET("/ping", func(c *gin.Context) {
 27       c.JSON(200, gin.H{
 28          "message": "this is service2",
 29       })
 30    })
 31    r.Run("127.0.0.1:11013") 
 32 }
 33 
 34 // hystrix使用示例代碼
 35 package main
 36 
 37 import (
 38    "fmt"
 39    "io/ioutil"
 40    "net/http"
 41    "sync"
 42    "time"
 43 
 44    "github.com/afex/hystrix-go/hystrix"
 45 )
 46 
 47 const (
 48    service1Command = "service1_command"
 49    service2Command = "service2_command"
 50    service1Url     = "http://127.0.0.1:11012/ping"
 51    service2Url     = "http://127.0.0.1:11013/ping"
 52 )
 53 
 54 func main() {
 55    //demo1()
 56    demo2()
 57 }
 58 
 59 // 併發數超過配置最大請求數,hystrix直接打開進入失敗邏輯
 60 func demo1() {
 61 
 62    //hystrix 基礎配置
 63    hystrix.ConfigureCommand(service1Command, hystrix.CommandConfig{
 64       Timeout:                1000, //命令執行超時時間
 65       MaxConcurrentRequests:  100,  //最大併發請求數量,命令最大執行的併發goroutine,同種hystrix命令超出該值後,請求直接進入失敗回滾邏輯
 66       RequestVolumeThreshold: 1,    //最小請求閾值,只有窗口時間內請求數量超過該值,斷路器才執行對應的判斷邏輯
 67       SleepWindow:            5000, //超時窗口時間,斷路器打開多久時長後進入半開狀態,重新允許遠程調用發生,試探下游服務是否正常。如果接下來請求都成功,斷路器將關閉
 68       ErrorPercentThreshold:  25,   //錯誤比例閾值,當滑動窗口時間內的錯誤請求頻率超過該值時,斷路器將打開
 69    })
 70 
 71    hystrix.ConfigureCommand(service2Command, hystrix.CommandConfig{
 72       Timeout:                1000,
 73       MaxConcurrentRequests:  5,
 74       RequestVolumeThreshold: 1,
 75       SleepWindow:            5000,
 76       ErrorPercentThreshold:  25,
 77    })
 78    wg1 := sync.WaitGroup{}
 79    wg1.Add(50)
 80    wg2 := sync.WaitGroup{}
 81    wg2.Add(50)
 82    //開50個goroutine去跑
 83    for i := 0; i < 50; i++ {
 84       go RequestService1(&wg1)
 85       go RequestService2(&wg2)
 86    }
 87    wg1.Wait()
 88    wg2.Wait()
 89    fmt.Println("main process done")
 90 }
 91 
 92 // service1 正常執行五秒後關閉,讓hystrix進入到打開狀態
 93 //繼而在五秒窗口時間進入到半開狀態,此時service1保持關閉,hystrix重新打開。
 94 //再過五秒窗口時間,hystrix進入到半開狀態,此時service1打開,使所有請求正常,hystrix檢測所有請求正常,進入關閉狀態
 95 func demo2() {
 96    //hystrix 基礎配置
 97    hystrix.ConfigureCommand(service1Command, hystrix.CommandConfig{
 98       Timeout:                1000, //命令執行超時時間
 99       MaxConcurrentRequests:  100,  //最大併發請求數量,命令最大執行的併發goroutine,同種hystrix命令超出該值後,請求直接進入失敗回滾邏輯
100       RequestVolumeThreshold: 1,    //最小請求閾值,只有窗口時間內請求數量超過該值,斷路器才執行對應的判斷邏輯
101       SleepWindow:            5000, //超時窗口時間,斷路器打開多久時長後進入半開狀態,重新允許遠程調用發生,試探下游服務是否正常。如果接下來請求都成功,斷路器將關閉
102       ErrorPercentThreshold:  10,   //錯誤比例閾值,當滑動窗口時間內的錯誤請求頻率超過該值時,斷路器將打開
103    })
104    wg1 := sync.WaitGroup{}
105    wg1.Add(5000)
106    start := time.Now() // 獲取當前時間
107    for i := 0; i < 5000; i++ {
108       time.Sleep(time.Second)
109       RequestService1(&wg1)
110    }
111    wg1.Wait()
112    elapsed := time.Since(start)
113    fmt.Println("該函數執行完成耗時:", elapsed)
114    fmt.Println("main process done")
115 }
116 
117 func RequestService1(wg *sync.WaitGroup) {
118    // 請求service1
119    defer wg.Done()
120    err := hystrix.Do(service1Command, func() error {
121       client := http.Client{}
122       resp, err := client.Get(service1Url)
123       if err != nil {
124          return err
125       }
126       defer resp.Body.Close()
127       if resp.StatusCode != 200 {
128          return fmt.Errorf("request err status code is %v \n", resp.StatusCode)
129       }
130       connect, err := ioutil.ReadAll(resp.Body)
131       if err != nil {
132          return fmt.Errorf("connect err  %v \n", err)
133       }
134       fmt.Printf("service1 resp : %s \n", string(connect))
135       return nil
136    }, func(err error) error {
137       // 失敗回滾方法
138       fmt.Printf("service1_command exec err : %v \n", err)
139       return nil
140    })
141    if err != nil {
142       fmt.Printf("hystrix do service1_command fail : %v \n", err)
143    }
144 }
145 
146 func RequestService2(wg *sync.WaitGroup) {
147    // 請求service2
148    defer wg.Done()
149    err := hystrix.Do(service2Command, func() error {
150       client := http.Client{}
151       resp, err := client.Get(service2Url)
152       if err != nil {
153          return err
154       }
155       defer resp.Body.Close()
156       if resp.StatusCode != 200 {
157          return fmt.Errorf("request err status code is %v \n", resp.StatusCode)
158       }
159       connect, err := ioutil.ReadAll(resp.Body)
160       if err != nil {
161          return fmt.Errorf("connect err  %v \n", err)
162       }
163       fmt.Printf("service2 resp : %s \n", string(connect))
164       return nil
165    }, func(err error) error {
166       // 失敗回滾方法
167       fmt.Printf("service2_command exec err : %v \n", err)
168       return nil
169    })
170    if err != nil {
171       fmt.Printf("hystrix do service2_command fail : %v \n", err)
172    }
173 }
View Code

 

  demo1執行響應圖,請求sevice2因爲併發量超過hystrix配置斷路器打開,請求service1正常

 

 

   demo2執行響應圖

 

 

 

hystrix集成到項目(網關)

  斷路器一般會作用在BFF層或者一些網關服務,在針對某些下游服務的請求併發量過大時做熔斷處理,下面代碼,示例如何將hystrix集成到具體的項目。

  1 package main
  2 
  3 import (
  4    "errors"
  5    "fmt"
  6    "log"
  7    "net/http"
  8    "net/http/httputil"
  9    "strings"
 10    "sync"
 11 
 12    "github.com/afex/hystrix-go/hystrix"
 13 )
 14 
 15 // HystrixHandler  Hystrix的實際應用
 16 type HystrixHandler struct {
 17 
 18    // 記錄hystrix是否已配置
 19    hystrixs      map[string]bool
 20    hystrixsMutex *sync.Mutex
 21    // 服務的名稱或者域名
 22    serviceName string
 23    // 服務的端口
 24    serviceHost int
 25    logger      *log.Logger
 26 }
 27 
 28 func NewHystrixHandler(serviceName string, serviceHost int, logger *log.Logger) *HystrixHandler {
 29 
 30    return &HystrixHandler{
 31       logger:        logger,
 32       hystrixs:      make(map[string]bool),
 33       hystrixsMutex: &sync.Mutex{},
 34       serviceName:   serviceName,
 35       serviceHost:   serviceHost,
 36    }
 37 
 38 }
 39 
 40 func (hystrixHandler *HystrixHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 41 
 42    // 假設服務請求路徑約定爲 http://localhost:10098/oauth/token?grant_type=password
 43    reqPath := req.URL.Path
 44    if reqPath == "" {
 45       return
 46    }
 47    //按照分隔符'/'對路徑進行分解,獲取服務名稱serviceName
 48    pathArray := strings.Split(reqPath, "/")
 49    serviceName := pathArray[1]
 50 
 51    if serviceName == "" {
 52       // 路徑不存在
 53       rw.WriteHeader(404)
 54       return
 55    }
 56 
 57    if _, ok := hystrixHandler.hystrixs[serviceName]; !ok {
 58       hystrixHandler.hystrixsMutex.Lock()
 59       if _, ok := hystrixHandler.hystrixs[serviceName]; !ok {
 60          //把serviceName作爲 hystrix 命令命名
 61          hystrix.ConfigureCommand(serviceName, hystrix.CommandConfig{
 62             // 進行 hystrix 命令自定義
 63          })
 64          hystrixHandler.hystrixs[serviceName] = true
 65       }
 66       hystrixHandler.hystrixsMutex.Unlock()
 67    }
 68 
 69    err := hystrix.Do(serviceName, func() error {
 70       //創建Director
 71       director := func(req *http.Request) {
 72 
 73          //重新組織請求路徑,去掉服務名稱部分
 74          destPath := strings.Join(pathArray[2:], "/")
 75 
 76          hystrixHandler.logger.Println("service name ", hystrixHandler.serviceName)
 77 
 78          //設置代理服務地址信息
 79          req.URL.Scheme = "http"
 80          req.URL.Host = fmt.Sprintf("%s:%d", hystrixHandler.serviceName, hystrixHandler.serviceHost)
 81          req.URL.Path = "/" + destPath
 82       }
 83       var proxyError error
 84       // 返回代理異常,用於記錄 hystrix.Do 執行失敗
 85       errorHandler := func(ew http.ResponseWriter, er *http.Request, err error) {
 86          proxyError = err
 87       }
 88       proxy := &httputil.ReverseProxy{
 89          Director:     director,
 90          ErrorHandler: errorHandler,
 91       }
 92 
 93       proxy.ServeHTTP(rw, req)
 94       // 將執行異常反饋 hystrix
 95       return proxyError
 96 
 97    }, func(e error) error {
 98       hystrixHandler.logger.Println("proxy error ", e)
 99       return errors.New("fallback excute")
100    })
101 
102    // hystrix.Do 返回執行異常
103    if err != nil {
104       rw.WriteHeader(500)
105       rw.Write([]byte(err.Error()))
106    }
107 
108 }
View Code

啓用 Metrics dashboard

  • hystrix可以添加Metrics控制器,上報hystrix命令的狀態信息。步驟如下:
  1. 下載docker鏡像hystrix-dashboard:docker pull mlabouardy/hystrix-dashboard
  2. 啓動hystrix-dashboard:docker run -d -p 8080:9002 --name hystrix-dashboard mlabouardy/hystrix-dashboard:latest
  3. 填寫斷路器數據上報指標的地址(局域網ip和端口以及路由)
func main() {
   //...
   //添加Metrics控制器 上報hystrix命令的狀態信息
   hystrixStreamHandler := hystrix.NewStreamHandler()
   hystrixStreamHandler.Start()
   go http.ListenAndServe(net.JoinHostPort("", "81"), hystrixStreamHandler)
   demo2() 
   //....
}
  • 命令執行情況截圖
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章