熔断限流 --- 自我保护机制
-
首先为什么需要有服务端的自我保护机制?
举个例子,假如我们要发布一个服务,作为服务端接收调用端发送过来的请求,这时服务端的某个节点负载压力过高了,我们该如何保护这个节点?
既然负载压力高 ,那就不让它再接收太多的请求就好了,等接收和处理的请求数量下来后,这个节点的负载压力自然就下来了。
那如何限制服务节点不再接收太多的请求呢?那就引入到了我们的熔断限流的机制了
限流机制 --- 服务端的自我保护机制
我们还可以在服务端添加限流逻辑,当调用端发送请求过来时,服务端在执行业务逻辑之前先执行限流逻辑,如果发现访问量过大并且超出了限流的阈值,就让服务端直接抛回给调用端一个限流异常,否则就执行正常的业务逻辑。
那么我们应该如何实现一个限流器呢?最简单的就是通过令牌桶算法来实现一个限流器
-
思考
我们可以假设这样一个场景:我发布了一个服务,提供给多个应用的调用方去调用 ,这时有一个应用的调用方发送过来的请求流量要比其它的应用大很多 ,这时我们就应该对这个应用下的调用端发送过来的请求流量进行限流 。所以说我们在做限流的时候要考虑应用级别的维度,甚至是 IP 级别的维度,这样做不仅可以让我们对一个应用下的调用端发送过来的请求流量做限流,还可以对一个 IP 发送过来的请求流量做限流。
也就是说我们的限流器可以做到是我一个服务节点对总体访问量的限流 --- 应用级别纬度
也可以让我们的限流器做到对某个调用端的ip进行限流 --- IP级别纬度
这样也有一个好处就是可以预防一些DDOS?不让一个请求反复的刷接口?
在请求进入的时候,进行判断能否放行
java
public class TokenBuketRateLimiter implements RateLimiter{
private int tokens; // 代表令牌的数量,>0 说明有令牌,可放行,放行了就--,==0,就是没有令牌了,不放行,对请求进行拦截
private int capacity; // 限流的本质就是令牌数
// 令牌桶的令牌,如果令牌用完了怎么办?--> 按照一定的速率给令牌桶加令牌,比如每秒加500个,不能超过caoacity
// 可以用定时任务去加 --> 启动一个定时任务,每秒执行一次 token + 500 不能超过 capacity
private int rate;
private Long lastTokenTime;
public TokenBuketRateLimiter(int capacity, int rate) {
this.capacity = capacity;
this.rate = rate;
this.lastTokenTime = System.currentTimeMillis();
this.tokens = capacity;
}
public synchronized boolean allowRequest(){
// 1. 给令牌桶添加令牌
Long currentTime = System.currentTimeMillis();
Long timeInterval = currentTime - lastTokenTime;
if(timeInterval >= 1000){
int needAddTokens = (int) (timeInterval * rate / 1000);
// 2. 给令牌桶添加令牌了
tokens = Math.min(capacity, tokens + needAddTokens);
this.lastTokenTime = System.currentTimeMillis();
}
// 3. 获取令牌
if(tokens > 0){
tokens--;
return true;
}else {
return false;
}
}
}
java
// 完成限流相关的ip限流器
Channel channel = channelHandlerContext.channel();
SocketAddress socketAddress = channel.remoteAddress();
Map<SocketAddress, RateLimiter> everyIpRateLimiter =
LRpcBootStrap.getInstance().getConfiguration().getEveryIpRateLimiter();
RateLimiter rateLimiter = everyIpRateLimiter.get(socketAddress);
if(rateLimiter == null){
rateLimiter = new TokenBuketRateLimiter(10,10);
everyIpRateLimiter.put(socketAddress, rateLimiter);
}
boolean allowRequest = rateLimiter.allowRequest();
熔断机制 --- 调用端的自我保护机制
服务端如何进行自我保护,最简单有效的方式就是限流。那么调用端呢?调用端是否需要自我保护呢?
举个例子,假如我要发布一个服务 B,而服务 B 又依赖服务 C,当一个服务 A 来调用服务 B 时,服务 B 的业务逻辑调用服务 C,而这时服务 C 响应超时了,由于服务 B 依赖服务 C,C 超时直接导致 B 的业务逻辑一直等待,而这个时候服务 A 在频繁地调用服务 B,服务 B 就可能会因为堆积大量的请求而导致服务宕机。
由此可见,服务 B 调用服务 C,服务 C 执行业务逻辑出现异常时,会影响到服务 B,甚至可能会引起服务 B 宕机。这还只是 A->B->C 的情况,试想一下 A->B->C->D->......呢?在整个调用链中,只要中间有一个服务出现问题,都可能会引起上游的所有服务出现一系列的问题,甚至会引起整个调用链的服务都宕机,这是非常恐怖的。
所以说,在一个服务作为调用端调用另外一个服务时,为了防止被调用的服务出现问题而影响到作为调用端的这个服务,这个服务也需要进行自我保护。而最有效的自我保护方式就是熔断。
了解一下熔断机制,熔断器的工作机制主要是关闭、打开和半打开这三个状态之间的切换
- 在正常情况下,熔断器是关闭的。
- 当调用端调用下游服务出现异常时,熔断器会收集异常指标信息进行计算 ,当达到熔断条件时熔断器打开,这时调用端再发起请求是会直接被熔断器拦截,并快速地执行失败逻辑;
- 当熔断器打开一段时间后,会转为半打开状态 ,这时熔断器允许调用端发送一个请求给服务端,如果这次请求能够正常地得到服务端的响应,则将状态置为关闭状态,否则设置为打开。
-
如何实现一个熔断器
最重要的是上面说的熔断机制
需要记录总请求数(正常和错误的都要记录)
需要记录失败的请求数(记录错误的请求次数)
java
public class CircuitBreaker {
// 一开始断路器是闭合的
private volatile boolean isOpen = false;
// 总的请求书
private AtomicInteger requestCount = new AtomicInteger(0);
// 异常的请求书
private AtomicInteger errorRequestCount = new AtomicInteger(0);
// 异常的阈值
private int maxErrorRequest; // 最大的异常数
private float maxErrorRate; // 最大的异常比例
public CircuitBreaker(int maxErrorRequest, float maxErrorRate) {
this.maxErrorRequest = maxErrorRequest;
this.maxErrorRate = maxErrorRate;
}
public boolean isBreak(){
// 如果断路器已经打开了,则直接返回true
if(isOpen){
return true;
}
// 需要判断数据指标,是否满足当前的阈值
if(errorRequestCount.get() > maxErrorRequest){
this.isOpen = true;
return true;
}
if(errorRequestCount.get() > 0 && requestCount.get() > 0 &&
errorRequestCount.get()/(float)requestCount.get() > maxErrorRate
){
this.isOpen = true;
return true;
}
return false;
}
// 每次发生请求,获取发生异常应该进行记录
public void recordRequest(){
this.requestCount.getAndIncrement();
}
public void recordErrorRequest(){
this.errorRequestCount.getAndIncrement();
}
public void reset(){
this.isOpen = false;
this.requestCount.set(0);
this.errorRequestCount.set(0);
}
}
总结
熔断器和限流器的区别是什么呢?
限流器是作用在服务端用于限制流量请求数量的
熔断器是作用在调用端用于解决服务端长时间无响应的问题