熔断保护
在软件系统中进行远程函数调用是很常见的操作。本地和远程函数调用最大的一个区别是远程调用可能会由于各种原因导致调用失败。例如:远程服务没有响应而导致调用挂起,最终直到超时。更糟糕的是,如果有大量的远程调用都出现问题阻塞,这是可能会导致系统
关键资源耗尽,例如,线程或连接对象等。最终使多个系统发生级联失败。为了解决这个问题提出了“断路器模式”来防止这种情况发生。
断路器的思想很简单,将调用函数包装在一个断路器对象中,这个包装对象会监控调用失败次数。一旦调用失败次数达到阈值,断路器将会打开,对函数的调用将会直接返回错误。这样避免调用阻塞在这里对系统进行保护。此外还需要主动报警的能力,一旦断路器打开,我们能及时得到通知。
下面是一个简单的例子,超时异常发生时产生断路保护。原文通过ruby进行演示,本文通过Java进行演示。
我们定义一个简单的断路器,当调用发生失败的次数超过阈值,再次调用时会阻断调用。定义一个包装类,当发生异常记录失败次数,每次方法调用时,检测是否超过失败次数即可。
enum Status {
OPEN,
HALF_OPEN,
CLOSE;
}
abstract class CircuitBreaker<T> {
// 失败次数阈值
private int failureThreshold = 5;
// 失败计数
private int failureCount = 0;
private long lastFailureTime = 0;
private long resetTimeout = 10;
protected abstract T call() throws Exception;
public T execute() throws Exception {
try {
Status status = getStatus();
if (status == Status.HALF_OPEN || status == Status.CLOSE) {
T ret = call();
// 出现成功调用了重置状态
failureCount = 0;
return ret;
}
if (getStatus() == Status.OPEN) {
throw new InterruptedException("失败直接中断");
}
throw new IllegalStateException("状态错误");
} catch (Exception e) {
failureCount++;
lastFailureTime = System.currentTimeMillis();
// 失败次数增加到10了
throw e;
}
}
private Status getStatus() {
if (failureCount > failureThreshold &&
System.currentTimeMillis() - lastFailureTime > resetTimeout) {
return Status.HALF_OPEN;
}
if (failureCount > failureThreshold) {
return Status.OPEN;
}
// 过一段时间,重试
return Status.CLOSE;
}
}
当系统异常超过阈值后断路器打开,对系统进行保护。但是我们还需要在外部系统恢复后,将断路器关闭,使正常调用恢复。通过在一定时间后尝试重新调用接口,来检测系统是否可用,随后并根据检测结果重置断路器。
我们可以通过重试阈值及上一个错误产生的时间来判断是否可恢复。此外还可以引入一个新状态 半开打开(half open)状态,当在这个状态时,可以准备开始重试,确定系统调用是否恢复正常。
上述介绍了一个简单的例子解释断路器的概念。实际的场景中异常的种类很多,如网络链接失败、超时等。但实际长并不是所有的错误都需要通过断路处理。有些异常反应的是业务逻辑错误,这类错误不应该被用来作为断路的异常。
远程调用通常很慢,所以批量操作经常通过多线行并发调用执行。这时通过断路器可以防止线程池中线程资源耗尽。
本文的例子演示了一个简单的断路器,通过设置失败次数来判断是否可调用。复杂点的方式是通过判断错误发生率来决定是否熔断。此外还可以针对不同的错误设置不同的阈值。 例如:超时异常阈值为10,网络连接失败的的阈值为3。