咱们常说的N个9,就是对SLA的一个描述。html
SLA全称是ServiceLevel Agreement,翻译为服务水平协议,也称服务等级协议,它代表了公有云提供服务的等级以及质量。java
例如阿里云对外承诺的就是一个服务周期内集群服务可用性不低于99.99%,若是低于这个标准,云服务公司就须要赔偿客户的损失。git
对互联网公司来讲,SLA就是网站或者API服务可用性的一个保证。github
9越多表明整年服务可用时间越长服务更可靠,4个9的服务可用性,听起来已经很高了,但对于实际的业务场景,这个值可能并不够。算法
咱们来作一个简单的计算,假设一个核心链路依赖20个服务,强依赖同时没有配置任何降级,而且这20个服务的可用性达到4个9,也就是99.99%,数据库
那这个核心链路的可用性只有99.99的20次方 = 99.8%,api
若是有10亿次请求则有3,000,000次的失败请求缓存
理想情况下,每一年仍是有17小时服务不可用服务器
这是一个理想的估算,在实际的生产环境中,因为服务发布,宕机等各类各样的缘由,状况确定会比这个更差,网络
对于一些业务比较敏感的业务,好比金融,或是对服务稳定要求较高的行业,好比订单或者支付业务,这样的状况是不能接受的。
除了对服务可用性的追求,微服务架构一个绕不过去的问题就是服务雪崩。
在一个调用链路上,微服务架构各个服务之间组成了一个松散的总体,牵一发而动全身,
服务雪崩是一个多级传导的过程,首先是某个服务提供者不可用,因为大量超时等待,继而致使服务调用者不可用,而且在整个链路上传导,继而致使系统瘫痪。
如同上面咱们分析的,在大规模微服务架构的场景下,避免服务出现雪崩,要减小停机时间,要尽量的提升服务可用性。
提升服务可用性,能够从不少方向入手,好比缓存、池化、异步化、负载均衡、队列和降级熔断等手段。
缓存以及队列等手段,增长系统的容量
限流和降级则是关心在到达系统瓶颈时系统的响应,更看重稳定性
缓存和异步等提升系统的战力,限流降级关注的是防护。
限流和降级,具体实施方法能够概括为八字箴言,分别是限流,降级,熔断和隔离。
限流顾名思义,提早对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,再也不调用后续资源。
限流须要结合压测等,了解系统的最高水位,也是在实际开发中应用最多的一种稳定性保障手段。
降级则是当服务器压力剧增的状况下,根据当前业务状况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
从降级配置方式上,降级通常能够分为主动降级和自动降级。
主动降级是提早配置,自动降级则是系统发生故障时,如超时或者频繁失败,自动降级。
其中,自动降级,又能够分为如下策略:
超时降级
失败次数降级
故障降级
在系统设计中,降级通常是结合系统配置中心,经过配置中心进行推送,下面是一个典型的降级通知设计
若是某个目标服务调用慢或者有大量超时,此时熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。
熔断通常须要设置不一样的恢复策略,若是目标服务状况好转则恢复调用。
服务隔离与前面的三个略有区别,咱们的系统一般提供了不止一个服务,可是这些服务在运行时是部署在一个实例,或者一台物理机上面的,
若是不对服务资源作隔离,一旦一个服务出现了问题,整个系统的稳定性都会受到影响!
服务隔离的目的就是避免服务之间相互影响。
通常来讲,隔离要关注两方面,一个是在哪里进行隔离,另一个是隔离哪些资源。
何处隔离
一次服务调用,涉及到的是服务提供方和调用方,咱们所指的资源,也是两方的服务器等资源,服务隔离一般能够从提供方和调用方两个方面入手。
隔离什么
广义的服务隔离,不只包括服务器资源,还包括数据库分库,缓存,索引等,这里咱们只关注服务层面的隔离。
服务降级和熔断在概念上比较相近,经过两个场景,谈谈我本身的理解。
熔断,通常是中止服务
典型的就是股市的熔断,若是大盘不受控制,直接休市,不提供服务,是保护大盘的一种方式。
降级,一般是有备用方案
从北京到济南,下雨致使航班延误,我能够乘坐高铁,若是高铁票买不到,也能够乘坐汽车或者开车过去。
二者的区别
降级通常是主动的,有预见性的,熔断一般是被动的,
服务A降级之后,通常会有服务B来代替,而熔断一般是针对核心链路的处理。
在实际开发中,熔断的下一步一般就是降级。
刚才讲了限流的概念,那么怎样判断系统到达设置的流量阈值了?
这就须要一些限流策略来支持,不一样的限流算法有不一样的特色,平滑程度也不一样。
计数器法是限流算法里最简单也是最容易实现的一种算法。
假设一个接口限制一分钟内的访问次数不能超过100个,维护一个计数器,每次有新的请求过来,计数器加一,这时候判断,若是计数器的值小于限流值,而且与上一次请求的时间间隔还在一分钟内,
容许请求经过,不然拒绝请求,若是超出了时间间隔,要将计数器清零。
public class CounterLimiter { //初始时间 private static long startTime = System.currentTimeMillis(); //初始计数值 private static final AtomicInteger ZERO = new AtomicInteger(0); //时间窗口限制 private static final long interval = 10000; //限制经过请求 private static int limit = 100; //请求计数 private AtomicInteger requestCount = ZERO; //获取限流 public boolean tryAcquire() { long now = System.currentTimeMillis(); //在时间窗口内 if (now < startTime + interval) { //判断是否超过最大请求 if (requestCount.get() < limit) { requestCount.incrementAndGet(); return true; } return false; } else { //超时重置 startTime = now; requestCount = ZERO; return true; } } }
计数器限流能够比较容易的应用在分布式环境中,用一个单点的存储来保存计数值,好比用Redis,而且设置自动过时时间,这时候就能够统计整个集群的流量,而且进行限流。
计数器方式的缺点是不能处理临界问题,或者说限流策略不够平滑。
假设在限流临界点的先后,分别发送100个请求,实际上在计数器置0先后的极短期里,处理了200个请求,这是一个瞬时的高峰,可能会超过系统的限制。
计数器限流容许出现 2*permitsPerSecond 的突发流量,可使用滑动窗口算法去优化,具体不展开。
假设咱们有一个固定容量的桶,桶底部能够漏水(忽略气压等,不是物理问题),而且这个漏水的速率可控的,那么咱们能够经过这个桶来控制请求速度,也就是漏水的速度。
咱们不关心流进来的水,也就是外部请求有多少,桶满了以后,多余的水会溢出。
漏桶算法的示意图以下:
将算法中的水换成实际应用中的请求,能够看到漏桶算法从入口限制了请求的速度。使用漏桶算法,咱们能够保证接口会以一个常速速率来处理请求,因此漏桶算法不会出现临界问题。
这里简单实现一下,也可使用Guava的SmoothWarmingUp类,能够更好的控制漏桶算法,
public class LeakyLimiter { //桶的容量 private int capacity; //漏水速度 private int ratePerMillSecond; //水量 private double water; //上次漏水时间 private long lastLeakTime; public LeakyLimiter(int capacity, int ratePerMillSecond) { this.capacity = capacity; this.ratePerMillSecond = ratePerMillSecond; this.water = 0; } //获取限流 public boolean tryAcquire() { //执行漏水,更新剩余水量 refresh(); //尝试加水,水满则拒绝 if (water + 1 > capacity) { return false; } water = water + 1; return true; } private void refresh() { //当前时间 long currentTime = System.currentTimeMillis(); if (currentTime > lastLeakTime) { //距上次漏水的时间间隔 long millisSinceLastLeak = currentTime - lastLeakTime; long leaks = millisSinceLastLeak * ratePerMillSecond; //容许漏水 if (leaks > 0) { //已经漏光 if (water <= leaks) { water = 0; } else { water = water - leaks; } this.lastLeakTime = currentTime; } } } }
漏桶是控制水流入的速度,令牌桶则是控制留出,经过控制token,调节流量。
假设一个大小恒定的桶,桶里存放着令牌(token)。桶一开始是空的,如今以一个固定的速率往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。
若是令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中能够保存的最大令牌数永远不会超过桶的大小,
每当一个请求过来时,就会尝试从桶里移除一个令牌,若是没有令牌的话,请求没法经过。
public class TokenBucketLimiter { private long capacity; private long windowTimeInSeconds; long lastRefillTimeStamp; long refillCountPerSecond; long availableTokens; public TokenBucketLimiter(long capacity, long windowTimeInSeconds) { this.capacity = capacity; this.windowTimeInSeconds = windowTimeInSeconds; lastRefillTimeStamp = System.currentTimeMillis(); refillCountPerSecond = capacity / windowTimeInSeconds; availableTokens = 0; } public long getAvailableTokens() { return this.availableTokens; } public boolean tryAcquire() { //更新令牌桶 refill(); if (availableTokens > 0) { --availableTokens; return true; } else { return false; } } private void refill() { long now = System.currentTimeMillis(); if (now > lastRefillTimeStamp) { long elapsedTime = now - lastRefillTimeStamp; int tokensToBeAdded = (int) ((elapsedTime / 1000) * refillCountPerSecond); if (tokensToBeAdded > 0) { availableTokens = Math.min(capacity, availableTokens + tokensToBeAdded); lastRefillTimeStamp = now; } } } }
这两种算法的主要区别在于漏桶算法可以强行限制数据的传输速率,而令牌桶算法在可以限制数据的平均传输速率外,还容许某种程度的突发传输。
在令牌桶算法中,只要令牌桶中存在令牌,那么就容许突发地传输数据直到达到用户配置的门限,所以它适合于具备突发特性的流量。
漏桶和令牌桶算法实现能够同样,可是方向是相反的,对于相同的参数获得的限流效果是同样的。
主要区别在于令牌桶容许必定程度的突发,漏桶主要目的是平滑流入速率,考虑一个临界场景,令牌桶内积累了100个token,能够在一瞬间经过,可是由于下一秒产生token的速度是固定的,
因此令牌桶容许出现瞬间出现permitsPerSecond的流量,可是不会出现2*permitsPerSecond的流量,漏桶的速度则始终是平滑的。
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用方便。
RateLimiter使用的是令牌桶的流控算法,RateLimiter会按照必定的频率往桶里扔令牌,线程拿到令牌才能执行,好比你但愿本身的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌,看下方法的说明:
修饰符和类型 |
方法和描述 |
---|---|
修饰符和类型 |
方法和描述 |
double |
acquire() |
double |
acquire(int permits) |
static RateLimiter |
create(double permitsPerSecond) |
static RateLimiter |
create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) |
double |
getRate() |
void |
setRate(double permitsPerSecond) |
boolean |
tryAcquire() |
boolean |
tryAcquire(int permits) |
boolean |
tryAcquire(int permits, long timeout, TimeUnit unit) |
boolean |
tryAcquire(long timeout, TimeUnit unit) |
RateLimter提供的API能够直接应用,其中acquire会阻塞,相似JUC的信号量Semphore,tryAcquire方法则是非阻塞的:
public class RateLimiterTest { public static void main(String[] args) throws InterruptedException { //容许10个,permitsPerSecond RateLimiter limiter = RateLimiter.create(10); for(int i=1;i<20;i++){ if (limiter.tryAcquire(1)){ System.out.println("第"+i+"次请求成功"); }else{ System.out.println("第"+i+"次请求拒绝"); } } } }
本文从服务可用性开始,分析了在业务高峰期经过限流降级保障服务高可用的重要性。
接下来分别探讨了限流,降级,熔断,隔离的概念和应用,而且介绍了经常使用的限流策略,图片引用网络和维基百科。