Loading

浅析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() 
   //....
}
  • 命令执行情况截图
posted @ 2022-07-13 15:15  3WLineCode  阅读(123)  评论(0编辑  收藏  举报