分布式系统 - 熔断机制

断路器(英文名称: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不依赖于任务的外部服务
分布式系统 - 熔断机制_第1张图片
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 
    1: clone [/Users/abhinavdhasmana/Documents/Personal/sourcecode/circuitBreaker/client/node_modules/hoek/lib/index.js:~20] [pc=0x10ea64e3ebcb](this=0x2c2775156bd9 ,obj=0x2c277be1e761 ,seen=0x2c2791b76f41 )
    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。
分布式系统 - 熔断机制_第2张图片
Circuit breaker in open position

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

分布式系统 - 熔断机制_第3张图片
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没有崩溃,我们的错误率已经显著降低。

使用断路器后

延伸阅读

  • Martin Fowler on CircuitBreaker
  • Netflix: Making the Netflix API More Resilient
  • Netflix: Fault Tolerance in a High Volume, Distributed System

三、断路器和微服务架构

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

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

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

一切都失败了,接受它!

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

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

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

Closed状态下的断路器

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

分布式系统 - 熔断机制_第5张图片
Closed State.jpg

Open状态下的断路器

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

分布式系统 - 熔断机制_第6张图片
Open State.jpg

Half-Open状态下的断路器

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

分布式系统 - 熔断机制_第7张图片
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可以从缓存中获取产品列表。断路器模式可以优雅地处理关键服务的停机时间和速度,并通过减少负载来帮助这些服务恢复。

补充阅读

  • Martin Fowler’s Circuit Breaker blog post
  • Spring’s Circuit Breaker tutorial

你可能感兴趣的:(分布式系统 - 熔断机制)