断路器(英文名称: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不依赖于任务的外部服务
现在让我们来尝试实现一下场景来看一下相关影响。
无断路器
下面是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将开始抛出错误,最终会崩溃,因为它将达到最大堆大小。
<--- 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
现在,我们有两个不起作用的服务,而不是一个。 这将在整个系统中逐步扩大,整个基础设施将会崩溃。
为什么需要一个断路器
如果我们已关闭serviceB,serviceA仍应该尝试从此恢复,并尝试执行以下操作之一:
- 自定义后备处理:尝试从其他资源获取相同数据。如果没法获取,那么使用缓存值。
- 快速失败:如果serviceA知道serviceB是关闭的,那么超时等待和消耗它自己的资源是没有意义的。应该返回信息表示serviceB是关闭的。
- 不要崩溃:如上例可以看到,serviceA不应该崩溃。
- 自动恢复:定期检查服务B是否再次工作。
- 其他API是可工作的:其他所有API应该可以继续工作。
断路器设计模式是什么
背后的想法很简单:
- 一段servcieA知道serviceB是关闭的,那么就没有必要请求serviceB。serviceA应尽可能返回缓存数据或者是超时错误。这就是断路器的"OPEN"状态。
- 一旦serviceA知道serviceB启动了,我们可以CLOSE断路器,这样就可以再次请求serviceB。
- 定期地发送请求到serviceB来查看它是否能够成功响应。这个是HALF-OPEN。
这就是我们的断路器状态图的样子:
断路器实现
现在让我们来使用一个产生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:在超时期限之后,电路切换到半开状态以测试潜在问题是否仍然存在。 如果在这种半开状态下单个呼叫失败,则断路器再次跳闸。 如果成功,则断路器重置回正常闭合状态。
这是每个断路器状态的解释和流程图。
Closed状态下的断路器
当断路器处于CLOSED状态时,所有调用都会进入到Supplier Microservice,无任何延迟地响应。
Open状态下的断路器
如果Supplier Mircroservice正运行缓慢,则断路器会收到对该服务的任何请求的超时。 一旦超时次数达到预定阈值,它就会使断路器跳闸到OPEN状态。 在OPEN状态下,断路器为所有对服务的调用返回错误,而不调用Supplier Microservice。 此行为允许Supplier MicroService通过减少其负载来恢复。
Half-Open状态下的断路器
断路器使用称为HALF-OPEN状态的监视和反馈机制来了解Supplier Mircoservice是否以及何时恢复。 它使用这种机制定期对Supplier Mircoservice进行尝试调用,以检查它是否已经恢复。 如果对Supplier Mircoservice的调用超时,则断路器保持在OPEN状态。 如果调用返回成功,则电路切换到CLOSED状态。 然后,断路器在HALF-OPEN状态期间将所有外部调用返回到服务并发生错误。
使用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