Go 微服务,第11部分:Hystrix和Resilience

在Go微服务博客系列的这一部分,我们将探讨如何使用Netflix HystrixGo实现go-resilience重试包,使用断路器模式使我们的服务间通信更具弹性

内容

  1. 概述
  2. 断路器
  3. 回收器的弹性
  4. 场景概述
  5. 去代码——添加断路器和回收器
  6. 部署和运行
  7. Hystrix Dashboard和Netflix Turbine
  8. Turbine和服务发现
  9. 总结

源代码

已完成的源代码可以从GitHub复制:

> git clone https://github.com/callistaenterprise/goblog.git
> git checkout P11

1.概述

考虑以下虚构的系统场景,其中一些微服务处理传入的请求::

图1 - 系统景观

如果最右边的服务“服务Y”失败会发生什么?假设它将接受传入的请求,但只是让它们等待,或许底层的数据存储没有响应。消费者服务(服务N和服务A)的等待请求最终会超时,但如果你的系统每秒处理数十或数百个请求,则会导致线程池填满,内存使用率急剧上升,最终消费者(那些打电话给服务1的人)会很恼火地等待他们的回应。这甚至可以通过调用链一路返回到入口点服务,有效地使整个场景陷入停顿。

图2 - 级联失败

虽然正确实施的健康检查最终会通过容器协调器中的机制触发服务重新启动失败的服务,但这可能需要几分钟的时间。与此同时,重负载应用程序将遭受级联失败,除非我们实际已经实施了模式来处理这种情况。这是断路器模式出现的地方。

2.断路器

图3 - 断路器

在这里,我们看到服务A和服务Y之间逻辑上存在断路器(实际断路器总是在消费者服务中实施)。断路器的概念来自于电力领域。托马斯·爱迪生于1879年就提出了专利申请。断路器设计为在检测到故障时打开,确保级联的副作用,例如你的房子被烧毁或微服务崩溃不会发生。Hystrix断路器的工作原理是:

图4 - 断路器状态

2.1 状态

  1. 关闭:在正常运行时,断路器是关闭的,允许请求(或电)通过。
  2. 打开:当检测到故障时(在一段时间内有n个失败的请求,请求时间过长,电流大幅增加),电路将打开,确保用户服务短路而不是等待失败的生产者服务。
  3. 半开:电路断路器定期地让一个请求通过。如果成功,电路可以再次闭合,否则,它将保持打开。

当电路闭合时,Hystrix有两个关键要点:

  1. Hystrix允许我们提供一个回退函数,它将被执行,而不是运行正常的请求。这允许我们提供一种回退行为。有时,我们不能没有错误的生产者的数据或服务,但正如通常情况下,我们的回退方法可以提供一个默认结果,一个结构良好的错误消息,或者可能调用一个备份服务。
  2. 停止级联失败。虽然回退行为非常有用,但是断路器模式中最重要的部分是我们要立即返回对调用服务的响应。这样做没有任何线程池充满待处理请求,没有超时,希望更少惹恼终端消费者。

3.回收器的弹性

如果给定的生产者服务宕机,断路器确保我们既可以优雅地处理问题,又可以将应用程序的其余部分从级联故障中保存下来。但是,在微服务环境中,我们很少只有一个给定服务的单个实例。如果您有许多实例,其中可能只有一个出现问题,那么为什么要将第一次尝试视为断路器内部的故障呢?这就是回收器的来源::

在我们的上下文中,在Docker Swarm模式环境中使用Go微服务,如果我们假设给定的生产者服务有3个实例,那么我们知道Swarm负载均衡器会自动循环访问给定服务的请求。因此,与其在断路器内部失败,为什么不使用一种机制来自动执行可配置的重试次数,包括某种备份?

图5 - 回收器

也许有点简化,序列图应该能够很好地解释关键的概念:

  1. 回收器在断路器内部运行。
  2. 如果所有重试尝试失败,断路器仅认为请求失败。实际上,断路器不知道里面发生了什么,它只关心它封装的操作是否返回一个错误。

在这篇博文中,我们将使用go-resilience的retries包。

4.场景概述

在这篇博客文章和我们稍后将要实现的示例代码中,我们将向accountservice添加断路器,用于对quot- service和一个名为imageservice的新服务的传出调用。我们还将安装运行Netflix Hystrix监控仪表板Netflix Turbine Hystrix流聚合器的服务。稍后再谈这两个。

图6 - 场景概述

5. Go代码 - 添加断路器和回收器

终于到了去编程的时候了!在这一部分,我们推出了一个全新的基础服务——imageservice。但是,我们不会花费任何宝贵的博客空间来描述它。它只返回一个给定的“acountId”的URL以及服务器的IP地址。它为场景提供了更多的复杂性,适合展示如何在一个服务中使用多个命名断路器。

让我们深入了解“accountservice”和/goblog/accountservice/service/handlers.go文件。从GetAccount 函数的代码中,我们希望使用go-hystrix和go-resilience/retrier调用底层报价服务和新的imageservice。这是报价服务的起点:

func getQuote() (model.Quote, error) {
 body, err := cb.CallUsingCircuitBreaker("quotes-service", "http://quotes-service:8080/api/quote?strength=4", "GET")
    // Code handling response or err below, omitted for clarity
    ...
 }

5.1断路器代码

我在我们的/common/circuitbreaker/hystrix.go文件中添加了cb.CallUsingCircuitBreaker 函数。它有点过于简单了,但基本上是包装了go-hystrix和重试库。出于可读性原因,我故意让代码更加冗长和不紧凑。

func CallUsingCircuitBreaker(breakerName string, url string, method string) ([]byte, error) {
        output := make(chan []byte, 1)  // Declare the channel where the hystrix goroutine will put success responses.
        errors := hystrix.Go(breakerName,    // Pass the name of the circuit breaker as first parameter.
            // 2nd parameter, the inlined func to run inside the breaker.
            func() error {
                    // Create the request. Omitted err handling for brevity
                    req, _ := http.NewRequest(method, url, nil)
                    // For hystrix, forward the err from the retrier. It's nil if successful.  
                    return callWithRetries(req, output)
            }, 
            // 3rd parameter, the fallback func. In this case, we just do a bit of logging and return the error.
            func(err error) error {
                    logrus.Errorf("In fallback function for breaker %v, error: %v", breakerName, err.Error())
                    circuit, _, _ := hystrix.GetCircuit(breakerName)
                    logrus.Errorf("Circuit state is: %v", circuit.IsOpen())
                    return err
        })
        // Response and error handling. If the call was successful, the output channel gets the response. Otherwise,
        // the errors channel gives us the error.
        select {
        case out := <-output:
                logrus.Debugf("Call in breaker %v successful", breakerName)
                return out, nil
        case err := <-errors:
                return nil, err
        }
}

如上所示,go-hystrix允许我们给断路器命名,我们也可以给出名称的细粒度配置。请注意,hystrix.Go函数将在一个新的goroutine中执行实际工作,在这个goroutine中,结果稍后会通过未缓冲(例如阻塞)的输出通道传递到选择代码段,该通道将有效地阻塞,直到输出错误通道接收到消息为止。

5.2重新编码

接下来,使用go-resilience retrrie包的callWithRetries(...) 函数:

func callWithRetries(req *http.Request, output chan []byte) error {
    // Create a retrier with constant backoff, RETRIES number of attempts (3) with a 100ms sleep between retries.
    r := retrier.New(retrier.ConstantBackoff(RETRIES, 100 * time.Millisecond), nil)
    // This counter is just for getting some logging for showcasing, remove in production code.
    attempt := 0
    // Retrier works similar to hystrix, we pass the actual work (doing the HTTP request) in a func.
    err := r.Run(func() error {
            attempt++
            // Do HTTP request and handle response. If successful, pass resp.Body over output channel,
            // otherwise, do a bit of error logging and return the err.
            resp, err := Client.Do(req)
            if err == nil && resp.StatusCode < 299 {
                    responseBody, err := ioutil.ReadAll(resp.Body)
                    if err == nil {
                            output <- responseBody
                            return nil
                    }
                    return err
            } else if err == nil {
                    err = fmt.Errorf("Status was %v", resp.StatusCode)
            }
            logrus.Errorf("Retrier failed, attempt %v", attempt)
            return err
    })
    return err
}

5.3单元测试

我在/goblog/common/circuitbreaker/hystrix_test.go文件中创建了三个单元测试,并运行CallUsingCircuitBreaker()函数。我们不会遍历所有的测试代码,一个例子就足够了。在这个测试中,我们使用gock来模拟对三个传出HTTP请求的响应,其中两个失败,最后一个成功:

func TestCallUsingResilienceLastSucceeds(t *testing.T) {
        defer gock.Off()
        buildGockMatcherTimes(500, 2)        // First two requests respond with 500 Server Error
        body := []byte("Some response")
        buildGockMatcherWithBody(200, string(body))   // Next (3rd) request respond with 200 OK
        hystrix.Flush()     // Reset circuit breaker state
        Convey("Given a Call request", t, func() {
                Convey("When", func() {
                        // Call single time (will become three requests given that we retry thrice)
                        bytes, err := CallUsingCircuitBreaker("TEST", "http://quotes-service", "GET")
                        Convey("Then", func() {
                                // Assert no error and expected response
                                So(err, ShouldBeNil)
                                So(bytes, ShouldNotBeNil)
                                So(string(bytes), ShouldEqual, string(body))
                        })
                })
        })
}

上面测试的控制台输出如下所示:

ERRO[2017-09-03T10:26:28.106] Retrier failed, attempt 1                    
ERRO[2017-09-03T10:26:28.208] Retrier failed, attempt 2                    
DEBU[2017-09-03T10:26:28.414] Call in breaker TEST successful

其他测试断言,如果所有重试都失败,那么Hystrix回退函数c将运行,而另一个测试则确保在足够数量的请求失败时,Hhystrix断路器被打开。

5.4配置Hystrix

Hystrix断路器可以通过多种方式进行配置。下面是一个简单的例子,我们指定了应该打开电路的失败请求数和重试超时:

hystrix.ConfigureCommand("quotes-service", hystrix.CommandConfig{
    SleepWindow:            5000,
RequestVolumeThreshold: 10,
})

有关详细信息,请参阅文档。我的/common/circuitbreaker/hystrix.go “库”有一些代码可以自动尝试使用此命名约定从配置服务器中获取配置值:

hystrix.command.[circuit name].[config property] = [value]

例如:(在accountservice-test.yml中

hystrix.command.quotes-service.SleepWindow: 5000

6.部署并运行

在这一部分的git的分支,还有更新的微服务代码和./copyall.sh,它建立并部署新的imageservice。这个真的没什么新鲜的。所以我们来看看断路器的运行情况。

在这种情况下,我们将运行一个小负载测试,默认情况下每秒将运行10个请求到/ accounts / {accountId}端点。

> go run *.go -zuul=false

(不要介意-zuul属性,这是博客系列的后面部分。)

假设我们分别有两个imageServicequotes-service实例。在所有服务运行正常的情况下,一些示例响应可能如下所示:

{"name":"Person_6","servedBy":"10.255.0.19","quote":{"quote":"To be or not to be","ipAddress":"10.0.0.22"},"imageUrl":"http://imageservice:7777/file/cake.jpg"} 
{"name":"Person_23","servedBy":"10.255.0.21","quote":{"quote":"You, too, Brutus?","ipAddress":"10.0.0.25"},"imageUrl":"http://imageservice:7777/file/cake.jpg"}

如果我们关闭quotes-service:

> docker service scale quotes-service=0

我们几乎马上就会看到(由于连接被拒绝)回退函数如何进入并返回fallbackQuote:

{name":"Person_23","servedBy":"10.255.0.19","quote":{"quote":"May the source be with you, always.","ipAddress":"circuit-breaker"},"imageUrl":"http://imageservice:7777/file/cake.jpg"}

6.2负载下会发生什么?

更有趣的是,当报价服务开始缓慢响应时,看看整个应用程序是如何反应的。在引用服务中有一个小的“功能”,可以在调用报价服务时指定散列强度。

http://quotes-service:8080/api/quote?strength=4

这样的请求通常在大约10毫秒内完成。通过将强度查询参数更改为13,quotes-service将使用大量的CPU,并且需要稍微少于一秒才能完成。这是一个很好的案例,可以看到我们的断路器在系统处于负载状态时如何反应,并且可能会导致CPU不足。让我们使用Gatling来分析两种情况:一种是我们禁用了断路器,另一种是断路器处于活动状态。

6.2.1残疾断路器

没有断路器,只使用标准的http.Get(url字符串)

第一个请求需要略少于一秒,但随后延迟增加,每个请求15-20 。我们两个报价服务实例(都使用100%CPU)的峰值吞吐量实际上不超过约3 req / s,因为它们完全缺乏CPU资源(老实说,它们都运行在我笔记本上的同一个群集节点上,在所有运行的微服务上共享两个CPU内核)。

6.2.2带断路器

断路器,超时设置为5000毫秒,即当有足够的请求等待超过5000毫秒时,电路将打开并返回报警。(注意最右边4-5秒左右的小条,这是电路处于“半开放”状态时的请求,以及电路打开前的一些早期请求)。

在该图中,我们看到测试中途的响应时间分布。在标记的数据点,断路器肯定是开放的,95%的第三百分位是10ms,而99%的第三百分位是超过4秒。换句话说,大约95%的请求在10ms内处理,但是一小部分(可能是半开的重试)在超时之前最多使用5秒。

在前15秒左右,绿/黄部分,我们看到或多或少的所有请求都是线性增加的延迟,接近5000毫秒阈值。这种行为与预期的一样,类似于我们在没有断路器的情况下运行。即请求可以成功处理,但需要很长时间。然后越来越多的延迟使断路器跳闸,我们立即看到大部分请求的响应时间如何回落到几毫秒而不是约5秒。如上所述,当处于“半开”状态时,断路器每隔一段时间发出请求。两个报价服务实例可以处理这些“半开放”请求中的几个,但是电路将几乎立即再次打开,因为引号服务 在延迟再次变得过高并且断路器重新跳闸之前,实例不能服务超过几个请求。

我们在这里看到关于断路器的两件巧妙的事情:

  • 当底层报价服务出现问题时,开路断路器会将延迟保持在最低水平,同时它也会“迅速反应”比任何健康检查/自动缩放/服务重启都快。
  • 断路器的超时时间为5000毫秒,确保用户无需等待约15秒的响应时间。5000毫秒配置的超时处理。(当然,除了使用断路器之外,您可以用其他方式处理超时)。

7. Hystrix仪表板和Netflix涡轮机

关于Hystrix的一个很好的事情是,有一个名为Hystrix Dashboard的配套Web应用程序,它可以提供微服务器内断路器当前正在进行的操作的图形表示。

它的工作原理是生成每秒更新一次的每个配置的断路器的状态和统计信息。然而,Hystrix控制面板一次只能读取一个这样的数据流,因此Netflix Turbine存在一种软件,它可以收集景观中所有断路器的数据流,并将这些数据聚合到仪表板可以消耗的一个数据流中:

图7 - 服务 - > Turbine - > Hystrix仪表板关系

在图7中,注意仪表盘猬请求/turbine.stream从涡轮机服务器,和涡轮反过来请求/hystrix.stream从一些微服务的。凭借Turbine收集来自我们账户服务的断路器指标,仪表板输出可能如下所示:

图8 - Hystrix仪表板

Hystrix仪表板的GUI绝对不是最容易掌握的。在上面,我们看到帐户服务中的两个断路器以及它们在上述负载测试运行中的状态。对于每个断路器,我们可以看到断路器状态,请求次数,平均等待时间,每个断路器名称连接的主机数量和错误百分比。其中之一。下面还有一个线程池部分,但我不确定它们在根统计生产者是go-hystrix库而不是启用hystrix的Spring Boot应用程序时能够正常工作。毕竟 - 在使用标准goroutines时,我们并没有在Go中使用线程池的概念。

以下是运行上述部分负载测试时账户服务内部的“报价服务”断路器的简短视频:( 点击图片开始视频)

总而言之,Turbine和Hystrix仪表板提供了一个相当不错的监控功能,可以非常容易地实时查明不健康的服务或意外延迟。务必确保您的服务间通话在断路器内执行。

8.涡轮和服务发现

将Netflix Turbine&Hystrix仪表板与非Spring微服务和/或基于容器协调器的服务发现结合使用有一个问题。原因是Turbine需要知道在哪里找到那些/hystrix.stream端点,例如http://10.0.0.13:8181/hystrix.stream。在不断变化的微服务环境中,服务的扩展和缩减等等,必须存在确保Turbine尝试连接哪些 URL以消耗Hystrix数据流的机制。

默认情况下,Turbine依赖Netflix Eureka,微服务正在向Eureka注册。然后,Turbine可以在内部查询Eureka以获得可能的服务IP连接。

在我们的上下文中,我们运行在Docker Swarm模式上,并依靠群集模式中的内置服务抽象Docker为我们提供。我们如何将我们的服务IP加入Turbine?

幸运的是,Turbine支持插入自定义发现机制。除了协调者的服务发现机制之外,我想除了加倍使用尤里卡之外,还有两种选择。我认为这是第7部分中一个非常糟糕的主意。

8.1.1发现令牌

该解决方案使用AMQP消息总线(RabbitMQ)和“发现”通道。当我们的微型服务有断路器启动时,他们找出他们自己的IP地址,然后通过我们的定制涡轮插件可以读取并转换成Turbine所理解的代理的代理发送消息。

图9 - 使用消息传递的Hystrix流发现。

帐户服务启动时运行的注册码:

func publishDiscoveryToken(amqpClient messaging.IMessagingClient) {
        // Get hold of our IP adress (reads it from /etc/hosts) and build a discovery token.
        ip, _ := util.ResolveIpFromHostsFile()
        token := DiscoveryToken{
                State:   "UP",
                Address: ip,
        }
        bytes, _ := json.Marshal(token)
        // Enter an eternal loop in a new goroutine that sends the UP token every
        // 30 seconds to the "discovery" channel.
        go func() {
                for {
                        amqpClient.PublishOnQueue(bytes, "discovery")
                        time.Sleep(time.Second * 30)
                }
        }()
}

我的小断路器库的完整源代码可以在这里找到,它包装了go-hystrix和go-resilience 。

8.1.2。Docker远程API

另一种选择是让自定义的Turbine插件使用Docker Remote API来获取容器及其IP地址,然后将其转换为Turbine可以使用的内容。这也应该起作用,但也有一些缺点,例如将插件绑定到特定容器协调器以及在Docker群模式管理器节点上运行Turbine。

8.2涡轮插件

我编写的Turbine插件的源代码和一些基本文档可以在我的个人GitHub页面上找到。由于它是基于Java的,因此我不会花费宝贵的博客空间在此上下文中详细描述它。

您还可以使用我在hub.docker.com放置的预先构建的容器图像。只需启动Docker群集服务即可

8.3使用选项1运行

/ goblog / support / monitor-dashboard中存在Hystrix仪表板的可执行jar文件和Dockerfile 。定制涡轮机是从我上面链接的容器图像中最容易使用的。

8.3.1建设和运行

我已经更新了我的shell脚本以启动自定义Turbine和Hystrix仪表板。在springcloud.sh中

# Hystrix Dashboard
docker build -t someprefix/hystrix support/monitor-dashboard
docker service rm hystrix
docker service create --constraint node.role==manager --replicas 1 -p 7979:7979 --name hystrix --network my_network --update-delay 10s --with-registry-auth  --update-parallelism 1 someprefix/hystrix
# Turbine
docker service rm turbine
docker service create --constraint node.role==manager --replicas 1 -p 8282:8282 --name turbine --network my_network --update-delay 10s --with-registry-auth  --update-parallelism 1 eriklupander/turbine

另外,帐户服务 Dockerfile现在公开端口8181,因此可以从群集内读取Hystrix流。您不应将8181映射到docker service create命令中的公共端口。

8.3.2故障排除

我不知道涡轮机是否有轻微的错误或者是什么问题,但是我倾向于为Hystrix仪表板从Turbine接收一个流:

  • 有时重新启动我的涡轮机服务,使用码头服务规模= 0最容易完成
  • 通过断路器请求一些请求。如果没有或没有正在进行的流量通过,则不确定hystrix流是否由go-hystrix生成。
  • 确保输入Hystrix仪表板的URL是正确的。http:// turbine:8282 / turbine.stream?cluster = swarm适合我。

9.总结

在博客系列的第11部分中,我们研究了断路器和弹性以及这些机制如何用于构建更容错和弹性的系统。

在博客系列的下一部分,我们将介绍两个新概念:Zuul EDGE服务器和使用Zipkin和Opentracing的分布式追踪。

本文的版权归 Aaroncang 所有,如需转载请联系作者。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大魏分享(微信公众号:david-share)

技术派:谁说API网关只能集成REST APIs?

23230
来自专栏Linyb极客之路

工作流引擎之Activiti使用总结

在第一家公司工作的时候主要任务就是开发OA系统,当然基本都是有工作流的支持,不过当时使用的工作流引擎是公司一些牛人开发的(据说是用一个开源的引擎修改的),名称叫...

80040
来自专栏自由而无用的灵魂的碎碎念

解决因为卸载vmware后键盘不能使用的问题

我之前安装的是vmware workstation 7.1,虽然添加与删除程序里有卸载选项,不过不管用,无奈用windows 优化大师将其卸载了。然后手动删除一...

12730
来自专栏JackieZheng

探秘Tomcat(一)——Myeclipse中导入Tomcat源码

前言:有的时候自己不知道自己是井底之蛙,这并没有什么可怕的,因为你只要蜷缩在方寸之间的井里,无数次的生活轨迹无非最终归结还是一个圆形;但是可怕的是有一天你不得...

22280
来自专栏架构师小秘圈

基于springCloud构建微云架构技术分享

一,什么是微服务 微服务英文名称Microservice,Microservice架构模式就是将整个Web应用组织为一系列小的Web服务。这些小的Web服务可以...

52840
来自专栏张善友的专栏

使用Hystrix提高系统可用性

今天稍微复杂点的互联网应用,服务端基本都是分布式的,大量的服务支撑起整个系统,服务之间也难免有大量的依赖关系,依赖都是通过网络连接起来。 ? (图片来源:htt...

22350
来自专栏Laoqi's Linux运维专列

ssh访问控制,多次失败登录即封掉IP,防止暴力破解

近期一直发现站内的流量和IP不太正常,读取/var/log/secure 很多失败的登录信息!必须要整个方法整死他们,虽然我已经把ssh port修改为了XXX...

51540
来自专栏SeanCheney的专栏

爬虫框架整理汇总

53960
来自专栏挖掘大数据

Apache NiFi 简介及Processor实战应用

Apache NiFi是什么?NiFi官网给出如下解释:“一个易用、强大、可靠的数据处理与分发系统”。通俗的来说,即Apache NiFi 是一个易于使用、功能...

2.1K100
来自专栏Kirito的技术分享

浅析Spring中的事件驱动机制

今天来简单地聊聊事件驱动,其实写这篇文章挺令我挺苦恼的,因为事件驱动这个名词,我没有找到很好的定性解释,担心自己的表述有误,而说到事件驱动可能立刻联想到如此众多...

74290

扫码关注云+社区

领取腾讯云代金券