1.限流
为什么要限流?
正常来说,一个员工A他每天能够处理的工作是10个,突然某一天来了100个工作量,这时候,如果员工A还处理100个,只有一种可能,这个员工被压垮。
如果我们能预先知道会有100个任务会来,我们通过增加员工数或定义消息队列等等来临时解决。
但是我们很多时候无法预料这些意外的。根据墨菲定律,坏事往往会接踵而来,有可能某个点挂了会引起全局的挂掉(雪崩)。因此我们不得不对我们的系统做一些保护措施。限流是其中之一。
针对秒杀这类场景,我们也可以做一些限流措施,而不影响到系统全局
思路:限速,我们可能第一个想到的应该是,我通过一个计数器,进行计数,如果超过了计数器阀值,表示速度太快了。一秒一个计数器
这样有个问题就是:粒度太大了,不均匀,针对1秒以下的,没法辨析。
我们能不能把粒度拆细了,1秒拆成10个100毫秒。每一个100毫秒有一个计数器。了解TCP/IP的应该知道,TCP/IP为了增加传输速度和控制传输速度,有个叫“滑动窗口协议”。
就算拆得再细,也无法解决匀速限制速度的问题。
而且还有个临界点问题,假如,一秒限制10个请求,在第1秒和第2秒之间,第1秒后半段时间10个请求,第2秒前半段10个请求,那第1秒后半段+第2秒前半段时间组成的一秒钟里就有20个请求,没有起到限速的作用
在生活中,如果一桶有一个细眼,我们往里面装水,可以看到水是一滴一滴匀速的下落的,哪我们能不能通过程序来实现这种方式呢。
思路:桶为容器,一滴水为一请求。如果桶满了就拒绝请求,没满处理请求
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
什么意思呢?就是说我服务前面闲了很久,突然来了很多请求(在桶的容量内),我得快速的把这些处理了。
思路:匀速的产生令牌,往桶里面丢,每次请求来,看是否有多余的令牌。如果有获取令牌执行正常业务,偌没有限速
简单的基于GUAVA的令牌桶的使用:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import GUAVA.concurrent.BaseRateLimiter;
/**
* 有很多任务,但希望每秒不超过N个
*
* @data 2019年5月22日 下午2:40:31
* @author ztq
**/
public class BaseRateLimiterTest {
public static void main(String[] args) {
// 2代表1秒最多多少个
BaseRateLimiter rateLimiter = BaseRateLimiter.create(2);
List<Runnable> tasks = new ArrayList<>(10);
for (int i = 0; i < 9; i++) {
tasks.add(new UserRequest(i));
}
// 建立线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
for (Runnable runnable : tasks) {
// rateLimiter.acquire()该方法会阻塞线程,直到令牌桶中能取到令牌为止才继续向下执行,并返回等待的时间。
System.out.println("等待时间: " + rateLimiter.acquire());
// 开始执行
threadPool.execute(runnable);
}
for (Runnable runnable : tasks) {
// 判断能否在500毫秒内得到令牌,如果不能则立即返回false,不会阻塞程序
System.out.println("超时是否获取令牌: " + (rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS) ? "成功" : "失败"));
// 开始执行
threadPool.execute(runnable);
}
}
// 执行的任务
private static class UserRequest implements Runnable {
private int id;
public UserRequest(int id) {
this.id = id;
}
public void run() {
System.out.println(id);
}
}
}
比如这样一个需求,我作为客户端要向kafka生产数据,而kafka的消费者则源源不断的消费数据,并将消费的数据全部请求到web服务器。当业务量巨大的时候,每秒可能有上万条数据产生,如果生产者直接生产数据的话极有可能把web服务器拖垮
读取本地文件,获得数据,将数据放入到队列中,以备调用。读取文件的舒服非常之快。每秒大概可以读取1g的数据。如果直接将数据全部塞到队列中。会造成内存溢出等问题
实现QPS速率的最简单的方式就是记住上一次请求的最后授权时间,然后保证1/QPS秒内不允许请求进入.比如QPS=5,如果我们保证最后一个被授权请求之后的200ms的时间内没有请求被授权,那么我们就达到了预期的速率.如果一个请求现在过来但是最后一个被授权请求是在100ms之前,那么我们就要求当前这个请求等待100ms.按照这个思路,请求15个新令牌(许可证)就需要3秒.
对一个每秒产生一个令牌的RateLimiter,每有一个没有使用令牌的一秒,我们就将storedPermits加1,如果RateLimiter在10秒都没有使用,则storedPermits变成10.0.这个时候,一个请求到来并请求三个令牌(acquire(3)),我们将从storedPermits中的令牌为其服务,storedPermits变为7.0.这个请求之后立马又有一个请求到来并请求10个令牌,我们将从storedPermits剩余的7个令牌给这个请求,剩下还需要三个令牌,我们将从RateLimiter新产生的令牌中获取.我们已经知道,RateLimiter每秒新产生1个令牌,就是说上面这个请求还需要的3个请求就要求其等待3秒.
想象一个RateLimiter每秒产生一个令牌,现在完全没有使用(处于初始状态),限制一个昂贵的请求acquire(100)过来.如果我们选择让这个请求等待100秒再允许其执行,这显然很荒谬.我们为什么什么也不做而只是傻傻的等待100秒,一个更好的做法是允许这个请求立即执行(和acquire(1)没有区别),然后将随后到来的请求推迟到正确的时间点.这种策略,我们允许这个昂贵的任务立即执行,并将随后到来的请求推迟100秒.这种策略就是让任务的执行和等待同时进行.
所以,允许先消费后付款。意思是它可以来一个请求一次性取走几个或者剩下所有的令牌。甚至更多,但是后面的请求就得为上一次请求买单,它需要等待桶中的令牌补齐之后才能继续获取令牌。
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
* 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
*/
public static RateLimiter create(double permitsPerSecond);
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
* 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
*
* 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
* 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
*/
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit);
2.熔断
起因:
服务雪崩效应:是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程。
如:A为服务提供者, B为A的服务调用者, C和D是B的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了
1、当A处理变的很慢时,B发送的请求就会变成无效请求,每次都会等返回结果。等来的却是超时结果。
2、A的问题积攒,就会导致B处理效率变慢,同理C和D也会变的不可用。问题越来越大,所有的服务不可用。
雪崩原因:
产生的因素: | 产生原因 | 解决方法 |
硬件原因 | 1、硬件损坏造成的服务器主机宕机 |
|
资源耗尽 | 同步等待造成的资源耗尽;使用 同步调用 时,会产生大量的等待线程占用系统资源 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态,造成服务雪崩效应产生 | 1、资源隔离:主要是对调用服务的线程池进行隔离. |
流量原因 | 1、用户大量请求:在秒杀、大和节假日,如果准备不充分,用户发起大量请求造成服务提供者的不可用
2、用户重试: 3、代码逻辑重试: | 1、用户交互限流 采用加载动画,提高用户的忍耐等待时间. 提交按钮添加强制等待时间机制 网关限流 3、关闭重试 4、服务自动扩容 |
代码逻辑 | 程序Bug |
|
缓存击穿 | 缓存击穿:缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时: 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用 | 1、缓存预加载 2、同步改为异步刷新 |
解决方案:
1) 使用Hystrix预防服务雪崩
2) Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力
3) Hystrix的设计原则包括:资源隔离、熔断器、命令模式
介绍:https://github.com/Netflix/Hystrix/wiki/Configuration
简单的熔断器的应用:
1、首先引入hystrix-core的依赖:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.12</version>
</dependency>
2、创建熔断器的类,并且继承HystrixCommand类,用这个类的实例化对象去访问服务提供者。
public class DelayCommand extends HystrixCommand<Code> {
private Envelope envelope;
private String Id;
private String defId;
private static HystrixThreadPoolProperties.Setter newPropertes = HystrixThreadPoolProperties.Setter()// 创建setter
.withMaxQueueSize(Constant.EOT_QUEUE_CAPACITY)// 设置队列长度
.withCoreSize(Constant.EOT_THREAD_POOL_CORE * 4)// 设置核心线程数量
.withQueueSizeRejectionThreshold(Constant.EOT_QUEUE_CAPACITY)
.withMaximumSize(Constant.EOT_THREAD_POOL_CORE * 32);// 设置最大线程数量
private static HystrixCommandProperties.Setter CommandProperties = HystrixCommandProperties.Setter()// 创建setter
.withCircuitBreakerSleepWindowInMilliseconds(Constant.HYSTRIX_BREAKER_MILLISECOND)// 设置熔断器打开时间
.withExecutionTimeoutInMilliseconds(Constant.HYSTRIX_TIMEOUT_MILLISECOND)// 设置run超时时间
.withCircuitBreakerErrorThresholdPercentage(50);
public DelayCommand(Envelope envelope, String flowId, String defId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(envelope.getDefCode() + "_regist"))// 域名作为GroupKey,必须有的参数
.andCommandKey(HystrixCommandKey.Factory.asKey(envelope.getDefCode() + "_regist"))// itype作为CommandKey
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(envelope.getDefCode() + "_regist"))// itype作为线程池名称
.andThreadPoolPropertiesDefaults(newPropertes).andCommandPropertiesDefaults(CommandProperties));
this.envelope = envelope;
this.flowId = Id;
this.defId = defId;
}
// 调用服务提供者的服务,及我们利用熔断器去发送Http调用其服务
@Override
protected EotCode run() throws Exception {
EotCode result = HttpSyncTransmitter.getInstance().post("http://localhost:8080/xxx",
envelope, Id, defId);
return result;
}
@Override
protected EotCode getFallback() {
// fallcallBack为空则检查当前熔断状态,打开则生成熔断拒绝报文;
if (this.circuitBreaker.isOpen()) {
// 熔断器开启
LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=Hystrix熔断器启动,请求拒绝", null);
return EotCode.ERROR_HYTRIX_OPEN;
} else if (this.isResponseThreadPoolRejected()) {
// 线程池拒绝,队列已满
LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=Hystrix服务拒绝,请求过多", null);
return EotCode.ERROR_HYTRIX_QUEUE_FULL;
} else if (this.isResponseTimedOut()) {
// 发生超时
LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=Hystrix等待响应超时", null);
return EotCode.ERROR_HYTRIX_TIMEOUT;
} else if (this.isFailedExecution()) {
// run方法执行失败
Throwable e = this.getFailedExecutionException();
if (e == null) {
// 执行失败,未获取到异常
LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=无异常失败", null);
} else {
// 执行失败,发现异常
LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=" + ThrowUtil.cause(e), null);
}
return EotCode.ERROR_HYTRIX_EXECUTE_FAIL;
} else {
// 未知异常
LocalLog.error(envelope.getFlowId(), "注册过程发生异常. cause=未知原因", null);
return EotCode.ERROR_HYTRIX_UNKNOWN_EXCEPTION;
}
}
}