CircuitBreaker--熔断器设计模式
熔断器模式可以防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器模式也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。
翻译:Kyle 作者:Abhinav Dhasmana 原文:https://itnext.io/understand-circuitbreaker-design-pattern-with-simple-practical-example-92a752615b42
问题描述
我们有一个serviceA
有两个API
-
/data
依赖于serviceB
-
/data2
不依赖于外部服务
让我们尝试实现这个场景,看看它如何影响我们的整个系统。完整的源代码可以在Github上找到。
没有断路器
serviceB
实施如下。API对前5分钟的请求,将延迟5s响应。它在端口8000上运行。
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');
},
});
<center>serviceB:模拟延迟响应</center>
serviceA
将向serviceB
发出http请求
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);
}
},
});
<center>ServiceA:调用受影响的serviceA</center>
我们将使用jMeter模拟负载 。在几秒钟内,serviceA将资源短缺。所有请求都在等待http请求完成。第一个API会开始抛出错误,它最终将会崩溃,因为它会达到最大堆的上限。
<center>jMeter报告API失败</center>
<---最后几个GC --->
[90303:0x102801600] 90966 ms:标记扫描1411.7(1463.4) - > 1411.3(1447.4)MB,1388.3 / 0.0 ms(自标记开始以0步开始+ 0.0 ms,最大步长0.0 ms,标记开始后的停机时间1388 ms)旧空间GC请求
[90303:0x102801600] 92377 ms:标记扫描1411.3(1447.4) - > 1411.7(1447.4)MB,1410.9 / 0.0 ms最后请求旧空间GC
<--- JS stacktrace --->
==== JS堆栈跟踪=========================================
安全上下文: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>,see = 0x2c2791b76f41 <Map map = 0x2c272c2848d9>)
2:clone [/ Users / abhinavdhasmana // circuitBreaker / client / node_modul ...
现在,我们有两个不工作的的服务,而不是一个。这将在整个系统中升级,从而导致整个基础设施将会崩溃。
为什么我们需要一个断路器
如果我们serviceB
失败了,serviceA
应该仍然尝试从中恢复并尝试执行以下操作之一:
- 自定义回退:尝试从其他来源获取相同的数据。如果不可能,请使用自己的缓存值。
- 快速失败:如果
serviceA
知道失败serviceB
了,那么就没有必要等待超时并消耗自己的资源。它应该尽快返回“知道”serviceB
已关闭 - 不要崩溃:正如我们在这种情况下看到的那样,
serviceA
不应该崩溃。 - 自动修复:定期检查是否serviceB再次起作用。
- 其他API应该有效:所有其他API应该继续工作。
什么是断路器设计?
背后的想法很简单:
- 一旦
serviceA
“知道”serviceB
失败,就没有必要提出要求serviceB
。serviceA
应该尽快返回缓存数据或超时错误。这是电路的开路状态 - 一旦
serviceA
“知道”serviceB
了,我们就可以关闭电路,以便serviceB
再次提出请求。 - 定期进行新的调用以
serviceB
查看是否成功返回结果。这种状态是HALF-OPEN。
<center>断路器处于打开位置</center>
这就是我们的电路状态图的样子
<center>断路器状态图</center>
使用断路器实现
让我们实现一个circuitBreaker进行GET http调用的方法。我们的简单需要三个参数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;
}
}
<center>circuitBreaker类及其构造函数</center>
接下来,让我们实现一个可以调用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';
}
}
<center>circuitBreaker调用函数</center>
让我们实现所有相关的功能。
// 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();
}
<center>circuitBreaker.js具有关于状态,失败和重置的所有功能</center>
下一步是修改我们的serviceA 。我们将把我们的调用包装在circuitBreaker刚刚创建的内部。
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);
}
},
});
与以前的代码相关的此代码中需要注意的重要更改:
- 我们正在初始化circuitBreaker const circuitBreaker = new CircuitBreaker(3000, 5, 2000);
- 我们通过断路器调用API const response = await circuitBreaker.call(‘http://0.0.0.0:8000/flakycall');
搞定!现在让我们再次运行我们的JMeter的测试,可以我们看到我们serviceA
没有崩溃,我们的错误率有了显著下降。