微服务之熔断、降级、限流

伴随着微服务架构被宣传得如火如荼,一些概念也被推到了咱们面前(管你接受不接受),其实大多数概念之前就有,但不多被提的这么频繁(如今好像不说起都很差意思交流了)。想起有人总结的一句话,微服务架构的特色就是:“一解释就懂,一问就不知,一讨论就吵架”。html

服务熔断

在介绍熔断机制以前,咱们须要了解微服务的雪崩效应。在微服务架构中,微服务是完成一个单一的业务功能,这样作的好处是能够作到解耦,每一个微服务能够独立演进。可是,一个应用可能会有多个微服务组成,微服务之间的数据交互经过远程过程调用完成。这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。若是扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用愈来愈多的系统资源,进而引发系统崩溃,所谓的“雪崩效应”。
这里写图片描述
熔断机制是应对雪崩效应的一种微服务链路保护机制。咱们在各类场景下都会接触到熔断这两个字。高压电路中,若是某个地方的电压太高,熔断器就会熔断,对电路进行保护。股票交易中,若是股票指数太高,也会采用熔断机制,暂停股票的交易。一样,在微服务架构中,熔断机制也是起着相似的做用。当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。nginx

在Spring Cloud框架里,熔断机制经过Hystrix实现。Hystrix会监控微服务间调用的情况,当失败的调用到必定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。redis

在dubbo中也可利用nio超时+失败次数作熔断。
dubbo能够经过扩展Filter的方式引入Hystrix,具体代码以下:算法

package com.netease.hystrix.dubbo.rpc.filter;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcException;

@Activate(group = Constants.CONSUMER)
public class HystrixFilter implements Filter {

    @Override
    public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
        DubboHystrixCommand command = new DubboHystrixCommand(invoker, invocation);
        return command.execute();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

DubboHystrixCommand数据库

package com.netease.hystrix.dubbo.rpc.filter;

import org.apache.log4j.Logger;

import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolProperties;

public class DubboHystrixCommand extends HystrixCommand {

    private static Logger    logger                       = Logger.getLogger(DubboHystrixCommand.class);
    private static final int DEFAULT_THREADPOOL_CORE_SIZE = 30;
    private Invoker       invoker;
    private Invocation       invocation;

    public DubboHystrixCommand(Invoker invoker,Invocation invocation){
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(invoker.getInterface().getName()))
                    .andCommandKey(HystrixCommandKey.Factory.asKey(String.format("%s_%d", invocation.getMethodName(),
                                                                                 invocation.getArguments() == null ? 0 : invocation.getArguments().length)))
              .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                                            .withCircuitBreakerRequestVolumeThreshold(20)//10秒钟内至少19此请求失败,熔断器才发挥起做用
                                            .withCircuitBreakerSleepWindowInMilliseconds(30000)//熔断器中断请求30秒后会进入半打开状态,放部分流量过去重试
                                            .withCircuitBreakerErrorThresholdPercentage(50)//错误率达到50开启熔断保护
                                            .withExecutionTimeoutEnabled(false))//使用dubbo的超时,禁用这里的超时
              .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(getThreadPoolCoreSize(invoker.getUrl()))));//线程池为30


        this.invoker=invoker;
        this.invocation=invocation;
    }

    /**
     * 获取线程池大小
     * 
     * @param url
     * @return
     */
    private static int getThreadPoolCoreSize(URL url) {
        if (url != null) {
            int size = url.getParameter("ThreadPoolCoreSize", DEFAULT_THREADPOOL_CORE_SIZE);
            if (logger.isDebugEnabled()) {
                logger.debug("ThreadPoolCoreSize:" + size);
            }
            return size;
        }

        return DEFAULT_THREADPOOL_CORE_SIZE;

    }

    @Override
    protected Result run() throws Exception {
        return invoker.invoke(invocation);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

线程池大小能够经过dubbo参数进行控制,当前其余的参数也能够经过相似的方式进行配置apache

 

代码添加好后在,resource添加加载文本编程

|-resources
|-META-INF
|-dubbo
|-com.alibaba.dubbo.rpc.Filter (纯文本文件,内容为:hystrix=com.netease.hystrix.dubbo.rpc.filter.HystrixFilterapi

因为Filter定义为自动激活的,因此启动代码全部消费者都被隔离起来啦!缓存

熔段解决以下几个问题:
    当所依赖的对象不稳定时,可以起到快速失败的目的
    快速失败后,可以根据必定的算法动态试探所依赖对象是否恢复网络

参考:http://www.roncoo.com/article/detail/126834
https://www.cnblogs.com/lvgg/p/7843809.html

服务降级

降级是指本身的待遇降低了,从RPC调用环节来说,就是去访问一个本地的假装者而不是真实的服务。

    当双11活动时,把无关交易的服务通通降级,如查看蚂蚁深林,查看历史订单,商品历史评论,只显示最后100条等等。

区别

相同点:
目的很一致,都是从可用性可靠性着想,为防止系统的总体缓慢甚至崩溃,采用的技术手段;
最终表现相似,对于二者来讲,最终让用户体验到的是某些功能暂时不可达或不可用;
粒度通常都是服务级别,固然,业界也有很多更细粒度的作法,好比作到数据持久层(容许查询,不容许增删改);
自治性要求很高,熔断模式通常都是服务基于策略的自动触发,降级虽然说可人工干预,但在微服务架构下,彻底靠人显然不可能,开关预置、配置中心都是必要手段;

区别:
触发缘由不太同样,服务熔断通常是某个服务(下游服务)故障引发,而服务降级通常是从总体负荷考虑;
管理目标的层次不太同样,熔断实际上是一个框架级的处理,每一个微服务都须要(无层级之分),而降级通常须要对业务有层级之分(好比降级通常是从最外围服务开始)
实现方式不太同样;服务降级具备代码侵入性(由控制器完成/或自动降级),熔断通常称为自我熔断。

服务限流

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提高系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则须要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,好比稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),所以需有一种手段来限制这些场景的并发/请求量,即限流。

限流的目的是经过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则能够拒绝服务(定向到错误页或告知资源没有了)、排队或等待(好比秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。

通常开发高并发系统常见的限流有:限制总并发数(好比数据库链接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发链接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其余还有如限制远程接口调用速率、限制MQ的消费速率。另外还能够根据网络链接数、网络流量、CPU或内存负载等来限流。

限流算法

常见的限流算法有:令牌桶、漏桶。计数器也能够进行粗暴限流实现。

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以必定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),而后就拒绝请求,能够看出漏桶算法能强行限制数据的传输速率.示意图以下:
这里写图片描述

令牌桶算法(Token Bucket)和 Leaky Bucket 效果同样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(若是QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),若是桶已经满了就再也不加了.新请求来临时,会各自拿走一个Token,若是没有Token可拿了就阻塞或者拒绝服务.
这里写图片描述
令牌桶的另一个好处是能够方便的改变速度. 一旦须要提升速率,则按需提升放入桶中的令牌的速率. 通常会定时(好比100毫秒)往桶中增长必定数量的令牌, 有些变种算法则实时的计算应该增长的令牌的数量.

应用级限流

对于一个应用系统来讲必定会有极限并发/请求数,即总有一个TPS/QPS阀值,若是超了阀值则系统就会不响应用户请求或响应的很是慢,所以咱们最好进行过载保护,防止大量请求涌入击垮系统。

若是你使用过Tomcat,其Connector其中一种配置有以下几个参数:
acceptCount:若是Tomcat的线程都忙于响应,新来的链接会进入队列排队,若是超出排队大小,则拒绝链接;

maxConnections:瞬时最大链接数,超出的会排队等待;

maxThreads:Tomcat能启动用来处理请求的最大线程数,若是请求处理量一直远远大于最大线程数则可能会僵死。

详细的配置请参考官方文档。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都会有相似的限制链接数的配置。

###池化技术
若是有的资源是稀缺资源(如数据库链接、线程),并且可能有多个系统都会去使用它,那么须要限制应用;可使用池化技术来限制总资源数:链接池、线程池。好比分配给每一个应用的数据库链接是100,那么本应用最多可使用100个资源,超出了能够等待或者抛异常。
限流某个接口的总并发/请求数

若是接口可能会有突发访问状况,但又担忧访问量太大形成崩溃,如抢购业务;这个时候就须要限制这个接口的总并发/请求数总请求数了;由于粒度比较细,能够为每一个接口都设置相应的阀值。可使用Java中的AtomicLong进行限流:

try {
if(atomic.incrementAndGet() > 限流数) {
//拒绝请求
    }
//处理请求
} finally {
    atomic.decrementAndGet();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

参考:http://www.javashuo.com/article/p-sokyxguy-t.html

分布式限流

分布式限流最关键的是要将限流服务作成原子化,而解决方案可使使用redis+lua或者nginx+lua技术进行实现,经过这两种技术能够实现的高并发和高性能。

首先咱们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后能够改造为限流总并发/请求数和限制总资源数。Lua自己就是一种编程语言,也可使用它实现复杂的令牌桶或漏桶算法。

有人会纠结若是应用并发量很是大那么redis或者nginx是否是能抗得住;不过这个问题要从多方面考虑:你的流量是否是真的有这么大,是否是能够经过一致性哈希将分布式限流进行分片,是否是能够当并发量太大降级为应用级限流;对策很是多,能够根据实际状况调节;像在京东使用Redis+Lua来限流抢购流量,通常流量是没有问题的。

对于分布式限流目前遇到的场景是业务上的限流,而不是流量入口的限流;流量入口限流应该在接入层完成,而接入层笔者通常使用Nginx。

基于Redis功能的实现限流

参考:https://www.cnblogs.com/exceptioneye/p/4783904.html
简陋的设计思路:假设一个用户(用IP判断)每分钟访问某一个服务接口的次数不能超过10次,那么咱们能够在Redis中建立一个键,并此时咱们就设置键的过时时间为60秒,每个用户对此服务接口的访问就把键值加1,在60秒内当键值增长到10的时候,就禁止访问服务接口。在某种场景中添加访问时间间隔仍是颇有必要的。

基于令牌桶算法的实现

令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并容许突发数据的发送。

令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型状况下,令牌桶算法用来控制发送到网络上的数据的数目,并容许突发数据的发送。

大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。若是令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中能够保存的最大令牌数永远不会超过桶的大小。

传送到令牌桶的数据包须要消耗令牌。不一样大小的数据包,消耗的令牌数量不同。

令牌桶这种控制机制基于令牌桶中是否存在令牌来指示何时能够发送流量。令牌桶中的每个令牌都表明一个字节。若是令牌桶中存在令牌,则容许发送流量;而若是令牌桶中不存在令牌,则不容许发送流量。所以,若是突发门限被合理地配置而且令牌桶中有足够的令牌,那么流量就能够以峰值速率发送。


算法描述:

假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中(每秒会有r个令牌放入桶中);

假设桶中最多能够存放b个令牌。若是令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;

当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌(不一样大小的数据包,消耗的令牌数量不同),而且数据包被发送到网络;

若是令牌桶中少于n个令牌,那么不会删除令牌,而且认为这个数据包在流量限制以外(n个字节,须要n个令牌。该数据包将被缓存或丢弃);

算法容许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。对于在流量限制外的数据包能够以不一样的方式处理:(1)它们能够被丢弃;(2)它们能够排放在队列中以便当令牌桶中累积了足够多的令牌时再传输;(3)它们能够继续发送,但须要作特殊标记,网络过载的时候将这些特殊标记的包丢弃。

Java实现

咱们可使用Guava 的 RateLimiter 来实现基于令牌桶的流控,RateLimiter 令牌桶算法是单桶实现。RateLimiter 对简单的令牌桶算法作了一些工程上的优化,具体的实现是 SmoothBursty。须要注意的是,RateLimiter 的另外一个实现SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也许是出于简单起见,RateLimiter 中的时间窗口能且仅能为 1s。

SmoothBursty 有一个能够放 N 个时间窗口产生的令牌的桶,系统空闲的时候令牌就一直攒着,最好状况下能够扛 N 倍于限流值的高峰而不影响后续请求。RateLimite容许某次请求拿走超出剩余令牌数的令牌,可是下一次请求将为此付出代价,一直等到令牌亏空补上,而且桶中有足够本次请求使用的令牌为止。当某次请求不能获得所须要的令牌时,这时涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,仍是让它先走掉后面的请求等一等呢?Guava 的设计者选择的是后者,先把眼前的活干了,后面的过后面再说。