美文网首页分布式系统
分布式系统 - 熔断机制

分布式系统 - 熔断机制

作者: HRocky | 来源:发表于2019-06-18 15:41 被阅读0次

    断路器(英文名称:circuit-breaker,circuit breaker)是指能够关合、承载和开断正常回路条件下的电流并能关合、在规定的时间内承载和开断异常回路条件下的电流的开关装置。断路器可用来分配电能,不频繁地启动异步电动机,对电源线路及电动机等实行保护,当它们发生严重的过载或者短路及欠压等故障时能自动切断电路,其功能相当于熔断器式开关与过欠热继电器等的组合。而且在分断故障电流后一般不需要变更零部件,已获得了广泛的应用。断路器按其使用范围分为高压断路器,和低压断路器,高低压界线划分比较模糊,一般将3kV以上的称为高压电器。在现代社会,无论是工业、农业、交通运输、国防、文教卫生、金融、商业、旅游服务和人民生活等领域都离不开电。电的产生、输送、使用中,配电是一个极其重要的环节。配电系统包括变压器和各种高低压电器设备,低压断路器则是一种使用量大面广的电器。

    一、Circuit breaker(断路器)设计模式

    原文链接:https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern

    断路器是现代软件开发中使用的设计模式。它用于检查故障并封装逻辑来阻止在系统维护期间、外部系统暂时出现故障期间和意外系统故障期间,错误反复不断地产生。

    通用用途

    假设应用程序每秒连接数据库100次,出现数据库连接失败。应用程序的设计者不希望反复不断地出现同样的错误。他们还希望快速,优雅地处理错误,而无需等待TCP连接超时。

    通常断路器可以用来检查外部服务是否可用。外部服务可以是数据库服务或者web服务。

    断路器检测到故障并阻止应用程序尝试执行注定要失败的操作(直到重试是安全的)。

    实现

    电路断路器设计模式的实现需要在一系列请求中保留连接的状态。它必须卸载逻辑以从实际请求中检测故障。因此,断路器内的状态机需要在某种意义上与通过它的请求同时工作。可以实现的一种方法是异步。

    在多节点(集群)服务器上,上游服务的状态需要反映在集群中的所有节点上。因此,实现可能需要使用持久存储层,例如, 诸如Memcached或Redis之类的网络缓存,或本地缓存(基于磁盘或存储器),以记录对应用程序外部服务的可用性。

    断路器在给定间隔内记录外部服务的状态。

    在从应用程序使用外部服务之前,将查询存储层以检索当前状态。

    性能影响

    虽然可以肯定地说,好处大于后果,但实施断路器会对性能产生负面影响。

    取决于所使用的存储层和一般可用资源的大小。这方面最大的因素是缓存的类型,例如,基于磁盘的缓存与基于内存的缓存和基于本地的缓存。

    二、通过简单的例子来了解Circuit breaker设计模式

    原文地址:https://itnext.io/understand-circuitbreaker-design-pattern-with-simple-practical-example-92a752615b42

    问题陈述

    我们有一个serviceA,有两个API

    • /data依赖于serviceB
    • /data2不依赖于任务的外部服务
    Use case for applying circuit breaker pattern

    现在让我们来尝试实现一下场景来看一下相关影响。

    无断路器

    下面是serviceB的实现。在前5分钟内,API将会延迟5秒对请求进行响应。serviceB运行在8000端口上。

    serviceB: Simulating delayed response

    server.route({
      method: 'GET',
      path: '/flakycall',
      handler: async (request, h) => {
        const currentTime = Date.now();
        if ((currentTime - serverStartTime) < (1000 * 60 * 5)) {
          const result = await new Promise((resolve) => {
            setTimeout(() => {
              resolve('This is a delayed repsonse');
            }, 5000);
          });
          return h.response(result);
        }
        return h.response('This is immediate response');
      },
    });
    

    serviceA的实现发送http请求给servcieB。

    ServiceA: calls the affected serviceA

    server.route({
      method: 'GET',
      path: '/data2',
      handler: (request, h) => {
        try {
          return h.response('data2');
        } catch (err) {
          throw Boom.clientTimeout(err);
        }
      },
    });
    
    server.route({
      method: 'GET',
      path: '/data',
      handler: async (request, h) => {
        try {
          const response = await axios({
            url: 'http://0.0.0.0:8000/flakycall',
            timeout: 6000,
            method: 'get',
    
          });
          return h.response(response.data);
        } catch (err) {
          throw Boom.clientTimeout(err);
        }
      },
    });
    

    我们将使用jMeter模拟负载。几秒钟之内,serviceA就会缺乏资源。所有请求都在等待http请求完成。第一个API将开始抛出错误,最终会崩溃,因为它将达到最大堆大小。

    jMeter report for failing API
    <--- Last few GCs --->
    [90303:0x102801600]    90966 ms: Mark-sweep 1411.7 (1463.4) -> 1411.3 (1447.4) MB, 1388.3 / 0.0 ms  (+ 0.0 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 1388 ms) last resort GC in old space requested
    [90303:0x102801600]    92377 ms: Mark-sweep 1411.3 (1447.4) -> 1411.7 (1447.4) MB, 1410.9 / 0.0 ms  last resort GC in old space requested
    <--- JS stacktrace --->
    ==== JS stack trace =========================================
    Security context: 0x2c271c925ee1 <JSObject>
        1: clone [/Users/abhinavdhasmana/Documents/Personal/sourcecode/circuitBreaker/client/node_modules/hoek/lib/index.js:~20] [pc=0x10ea64e3ebcb](this=0x2c2775156bd9 <Object map = 0x2c276089fe19>,obj=0x2c277be1e761 <WritableState map = 0x2c27608b1329>,seen=0x2c2791b76f41 <Map map = 0x2c272c2848d9>)
        2: clone [/Users/abhinavdhasmana//circuitBreaker/client/node_modul...
    

    现在,我们有两个不起作用的服务,而不是一个。 这将在整个系统中逐步扩大,整个基础设施将会崩溃。

    为什么需要一个断路器

    如果我们已关闭serviceB,serviceA仍应该尝试从此恢复,并尝试执行以下操作之一:

    • 自定义后备处理:尝试从其他资源获取相同数据。如果没法获取,那么使用缓存值。
    • 快速失败:如果serviceA知道serviceB是关闭的,那么超时等待和消耗它自己的资源是没有意义的。应该返回信息表示serviceB是关闭的。
    • 不要崩溃:如上例可以看到,serviceA不应该崩溃。
    • 自动恢复:定期检查服务B是否再次工作。
    • 其他API是可工作的:其他所有API应该可以继续工作。

    断路器设计模式是什么

    背后的想法很简单:

    • 一段servcieA知道serviceB是关闭的,那么就没有必要请求serviceB。serviceA应尽可能返回缓存数据或者是超时错误。这就是断路器的"OPEN"状态。
    • 一旦serviceA知道serviceB启动了,我们可以CLOSE断路器,这样就可以再次请求serviceB。
    • 定期地发送请求到serviceB来查看它是否能够成功响应。这个是HALF-OPEN。
    Circuit breaker in open position

    这就是我们的断路器状态图的样子:

    state diagram of circuit breaker

    断路器实现

    现在让我们来使用一个产生http GET调用的circuitBreaker。对于我们这个简单的circuitBreaker需要下面三个参数:

    • 打开断路器的请求失败阈值
    • 一旦断路器处于OPEN状态,我们应该间隔多久重试
    • 在我们的例子中,API请求的超时时间。

    根据上面的信息,我们可以创建circuitBreaker类:

    class CircuitBreaker {
      constructor(timeout, failureThreshold, retryTimePeriod) {
        // We start in a closed state hoping that everything is fine
        this.state = 'CLOSED';
        // Number of failures we receive from the depended service before we change the state to 'OPEN'
        this.failureThreshold = failureThreshold;
        // Timeout for the API request.
        this.timeout = timeout;
        // Time period after which a fresh request be made to the dependent
        // service to check if service is up.
        this.retryTimePeriod = retryTimePeriod;
        this.lastFailureTime = null;
        this.failureCount = 0;
      }
    }
    

    接下来,让我们实现一个函数,它将调用API来请求serviceB。

    async call(urlToCall) {
      // Determine the current state of the circuit.
      this.setState();
      switch (this.state) {
        case 'OPEN':
        // return  cached response if no the circuit is in OPEN state
          return { data: 'this is stale response' };
        // Make the API request if the circuit is not OPEN
        case 'HALF-OPEN':
        case 'CLOSED':
          try {
            const response = await axios({
              url: urlToCall,
              timeout: this.timeout,
              method: 'get',
            });
            // Yay!! the API responded fine. Lets reset everything.
            this.reset();
            return response;
          } catch (err) {
            // Uh-oh!! the call still failed. Lets update that in our records.
            this.recordFailure();
            throw new Error(err);
          }
        default:
          console.log('This state should never be reached');
          return 'unexpected state in the state machine';
      }
    }
    

    让我们来实现其他相关功能。

    // reset all the parameters to the initial state when circuit is initialized
    reset() {
      this.failureCount = 0;
      this.lastFailureTime = null;
      this.state = 'CLOSED';
    }
    
    // Set the current state of our circuit breaker.
    setState() {
      if (this.failureCount > this.failureThreshold) {
        if ((Date.now() - this.lastFailureTime) > this.retryTimePeriod) {
          this.state = 'HALF-OPEN';
        } else {
          this.state = 'OPEN';
        }
      } else {
        this.state = 'CLOSED';
      }
    }
    
    recordFailure() {
      this.failureCount += 1;
      this.lastFailureTime = Date.now();
    }
    

    下一步就是修改serviceA。我们会把我们的请求包装在我们刚刚创造的断路器里。

    let numberOfRequest = 0;
    
    server.route({
      method: 'GET',
      path: '/data2',
      handler: (request, h) => {
        try {
          return h.response('data2');
        } catch (err) {
          throw Boom.clientTimeout(err);
        }
      },
    });
    
    const circuitBreaker = new CircuitBreaker(3000, 5, 2000);
    
    server.route({
      method: 'GET',
      path: '/data',
      handler: async (request, h) => {
        numberOfRequest += 1;
        try {
          console.log('numberOfRequest received on client:', numberOfRequest);
          const response = await circuitBreaker.call('http://0.0.0.0:8000/flakycall');
          // console.log('response is ', response.data);
          return h.response(response.data);
        } catch (err) {
          throw Boom.clientTimeout(err);
        }
      },
    });
    

    与之前的代码相比,本代码中需要注意的重要更改:

    • 初始化断路器:const circuitBreaker = new CircuitBreaker(3000, 5, 2000);
    • 通过断路器来进行API调用:const response = await circuitBreaker.call(‘http://0.0.0.0:8000/flakycall');

    就这样!现在让我们再次运行JMeter测试,我们可以看到,我们的serviceA没有崩溃,我们的错误率已经显著降低。

    使用断路器后

    延伸阅读

    三、断路器和微服务架构

    原文地址:https://techblog.constantcontact.com/software-development/circuit-breakers-and-microservices/

    到目前为止,众所周知,微服务架构具有许多优点。 其中包括低耦合,可重用性,业务敏捷性和分布式云就绪应用程序。 但与此同时,它使架构变得脆弱,因为每个用户操作结果都会调用多个服务。 它通过网络上的远程调用替换单片体系结构的内存中调用,这在所有服务启动并运行时都能很好地工作。 但是,当一个或多个服务不可用或表现出高延迟时,会导致整个企业出现级联故障。 服务客户端重试逻辑只会使服务更糟糕,并且可以完全停止服务。

    断路器模式有助于防止跨多个系统的这种灾难性级联故障。 断路器模式允许您构建容错和弹性系统,当关键服务不可用或具有高延迟时,该系统可以正常运行。

    一切都失败了,接受它!

    让我们面对现实,所有服务都会在某个时间点失败或动摇。 断路器允许您的系统优雅地处理这些故障。 断路器概念很简单。 它包含一个跟踪故障的监视器的功能。 断路器有3种不同的状态,闭合(Closed),开路(Open)和半开路(Half-Open):

    • Closed:当一切正常时,断路器保持在闭合状态,所有调用都可以正常通过并传递到服务接口。 当故障数超过预定阈值时,断路器跳闸,并进入打开状态。
    • Open:断路器在不执行该功能的情况下返回调用错误。
    • Half-Open:在超时期限之后,电路切换到半开状态以测试潜在问题是否仍然存在。 如果在这种半开状态下单个呼叫失败,则断路器再次跳闸。 如果成功,则断路器重置回正常闭合状态。
    断路器状态图.jpg

    这是每个断路器状态的解释和流程图。

    Closed状态下的断路器

    当断路器处于CLOSED状态时,所有调用都会进入到Supplier Microservice,无任何延迟地响应。

    Closed State.jpg

    Open状态下的断路器

    如果Supplier Mircroservice正运行缓慢,则断路器会收到对该服务的任何请求的超时。 一旦超时次数达到预定阈值,它就会使断路器跳闸到OPEN状态。 在OPEN状态下,断路器为所有对服务的调用返回错误,而不调用Supplier Microservice。 此行为允许Supplier MicroService通过减少其负载来恢复。

    Open State.jpg

    Half-Open状态下的断路器

    断路器使用称为HALF-OPEN状态的监视和反馈机制来了解Supplier Mircoservice是否以及何时恢复。 它使用这种机制定期对Supplier Mircoservice进行尝试调用,以检查它是否已经恢复。 如果对Supplier Mircoservice的调用超时,则断路器保持在OPEN状态。 如果调用返回成功,则电路切换到CLOSED状态。 然后,断路器在HALF-OPEN状态期间将所有外部调用返回到服务并发生错误。

    Half-Open State.jpg

    使用Spring boot的断路器示例

    如果使用Spring Boot,则可以使用Netflix Hystrix容错库中的断路器实现。

    @EnableCircuitBreaker
    @RestController
    @SpringBootApplication
    public class CampaignApplication {
     
      @Autowired
      private TrackingService trackingService;
     
      @Bean
      public RestTemplate rest(RestTemplateBuilder builder) {
        return builder.build();
      }
     
      @RequestMapping("/to-track")
      public String toTrack() {
        return trackingService.getStats();
      }
     
      public static void main(String[] args) {
        SpringApplication.run(ReadingApplication.class, args);
      }
    }
    

    “EnableCircuitBreaker”注释告诉Spring Cloud,Campaign应用程序使用断路器,根据跟踪服务的可用性启用它们的监控,打开和关闭。

    优势

    断路器对监控很有价值; 监控,记录和恢复任何断路器状态变化,以确保服务可用性。 测试断路器状态可帮助您为容错系统添加逻辑。 例如,如果产品目录服务不可用(电路已打开),则UI可以从缓存中获取产品列表。断路器模式可以优雅地处理关键服务的停机时间和速度,并通过减少负载来帮助这些服务恢复。

    补充阅读

    相关文章

      网友评论

        本文标题:分布式系统 - 熔断机制

        本文链接:https://www.haomeiwen.com/subject/bvfcgqtx.html