降级限流

本文详细介绍了服务降级和限流在高并发系统中的重要性,阐述了降级的自动和人工策略,以及限流的滑动窗口、漏桶和令牌桶算法。并提供了Guava实现的单机限流示例以及基于Redis的分布式限流解决方案,如lua脚本和Redission的实现。强调了在系统设计中实施降级限流以保证服务的稳定性和可用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     对于高可用的服务,除了尽量保证自身的服务的可靠性外, 还的防止不被上游压垮,控制上游的访问一般使用限流策略,避免被下游服务不可用拖垮,一般采取降级策略,只有更好的了解和控制上下游才能使自身的服务的稳定性增强。所以服务降级限流是系统面临挑战的最后一道保护屏障,不可忽视,也是在突发情况下保证系统稳定性和可用性的有效手段。

1、限流和降级

目的: 当流量快速增长的时候,一定要保证核心服务的可用,即便是有损服务。方案包括: 有限重试, 快速失败, 降级限流方案。
对于高并发C端业务系统,一般都会采取相应的手段来保护系统不被意外的请求流量压垮,进行服务的降级处理, 包括熔断和限流:
  • 熔断:类似于电流的保险丝机制,当我们的服务出问题或者影响到核心服务流程的时候需要暂时熔断某些功能+友好的提示,等高峰或者问题解决后再打开;
  • 限流:当流量快速增长、防止脉冲式流量导致服务可能会出现问题(响应超时),或者非核心服务影响到核心流程时, 仍然需要保证服务的可用性, 即便是有损服务
  • 缓存:目的是提升系统访问速度和增大系统处理的容量,可以说是 抗高并发流量的银弹;
所以意味着我们在设计服务的时候,在使用缓存和异步处理的同时,还需要通过一些关键数据进行 自动降级,或者配置 人工降级的开关。

1.1、降级

对于系统的降级,请求的backup,降级一般有几种实现手段:
  • 自动降级:达到某一阈值后自动熔断降级;
    • 例如达到请求QPS阈值,下游服务接口错误率达到一定的阈值;对业务的重要程度可实现服务降级,自动返回预设数据和通知信息,或者延后处理;
    • 中间件服务故障等使用降级掉部分功能服务,例如mq故障使用rpc方式;
    • 多级缓存的使用;
  • 人工降级: 通过配置降级开关,人为实现对流程的控制;
    • 前置化降级开关, 基于配置中心实现附加业务的降级;
    • 业务降级,首先可以进行服务隔离,资源隔离,分渠道分地域分区进行控制,在业务高峰期,我们会优先保证核心业务的流程可用,对附加业务进行降级处理;
降级方案分为:有损降级和无损降级
  • 无损降级例如多级缓存,异步发送mq转rpc调用,全局id转本地生成后备方案;
  • 有损降级:限制资源,降级服务不可用+友好提示等等;

1.2、限流

限流的目的是防止恶意请求流量、恶意攻击、或者防止流量超过系统峰值。通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统。
主要是对资源访问做控制,那么控制这块主要有两个功能:
限流策略
限流策略由 限流算法可调节的参数两部份组成,算法和参数都需要结合具体的业务特点和并发量来选择和设置。
熔断策略
对于服务的 熔断策略,不同的系统有不同的熔断策略诉求,例如:
  • 直接拒绝服务.(友好提示页面告知资源没有了);
  • 排队等待 异步处理(秒杀、抢购下单);
  • 服务降级 (返回兜底数据或默认数据,如商品详情页等等)

2、限流算法

限流算法,对突发流量的整形形成稳定的流量,对系统起到保护作用,将脉冲式流量压力均衡分摊,减少系统瞬时的压力。

2.1、滑动窗口协议

是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没的目的。

2.2、漏桶

桶本身具有一个恒定的速率(接口的响应时间)往下漏水,而上方时快时慢的会有水进入桶内。当桶还未满时,上方的水可以加入。一旦水满,上方的水就无法加入。桶满正是算法中的一个关键的触发条件(即流量异常判断成立的条件)。而此条件下如何处理上方流下来的水,有两种方式在桶满水之后,常见的两种处理方式为:
  • 1) 阻塞:暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水;
  • 2) 抛弃:溢出的上方水直接抛弃,执行拒绝策略;
特点:
  • 1. 漏水的速率是固定的
  • 2. 即使存在注水 burst(突然注水量变大)的情况, 漏水的速率是固定的,不能解决突发流量; 

2.3、令牌桶

令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最 常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目, 能解决突发请求的流量;
令牌桶是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌; 令牌是按一定的速率来生成;1个令牌/10ms;
令牌桶算法实际上由两部分组成: 
  • 两个流:分别是 令牌流、数据流
  • 一个桶: 令牌桶
令牌桶和漏桶的区别: 令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。
分析:比如我们的目标现在是每秒钟处理10个请求,使用漏桶法,我们设置桶的大小为10,流出的速率为0.1s流出一个请求,我们可以达成我们的目标;而使用令牌桶的话,我们会设置每1s向桶中放入10个令牌,请求如果是平稳的,每0.1s过来一个请求,来十个,同样能达到我们的目标,这里所说的某种程度的突发传输是指,比如在一秒内前0.5请求是平稳的,每0.1s来一个,但在0.6s的时候突发请求同时来个5个请求,此时令牌算法是可以承受这个突发流量的,并且让5个请求成功立刻同时得到处理,完成了所谓的某种程度的突发传输,而漏桶算法,也可以应对这种突发流量,但不同的是没有让5个请求同时立刻通过,而是缓冲在桶中,然后仍然以固定速率每0.1s处理一个。
    这两种算法,都可以实现流速的控制,1s处理10个请求,多于10个的请求都会被拒绝,但由于漏桶算法流出速率是一定的,所以请求可能会被缓冲在桶中,不能马上得到处理,从而徒增了等待时间,而对于令牌桶算法,没有这种等待时间,有令牌则通过,无令牌则抛弃。

3、限流实践

3.1、单机Guava实现令牌桶和漏桶

具体实例如下:
public class GuavaTokenDemo {
    private int qps;
    private int countOfReq;

    private RateLimiter rateLimiter;

    public GuavaTokenDemo(int qps, int countOfReq) {
        this.qps = qps;
        this.countOfReq = countOfReq;
    }

    //初始化一个令牌桶
    public GuavaTokenDemo processWithTokenBucket() {
        rateLimiter = RateLimiter.create(qps);                          //1min钟的请求量qps;峰值
        return this;
    }

    //初始化一个漏桶
    public GuavaTokenDemo processWithLeakyBucket() {
        rateLimiter = RateLimiter.create(qps, 0, TimeUnit.MILLISECONDS); //预热时间 0 
        return this;
    }

    private void processRequest() {
        System.out.println("RateLimiter:" + rateLimiter.getClass());
        long start = System.currentTimeMillis();
        for (int i = 0; i < countOfReq; i++) {
            rateLimiter.acquire();//获取令牌 acquire()是阻塞的,tryAcquire()是非阻塞的;
        }
        long end = System.currentTimeMillis() - start;
        System.out.println("处理的请求数量:" + countOfReq + "," +
                "" + "耗时:" + end + ",qps:" + rateLimiter.getRate() + ",实际qps:" +
                Math.ceil(countOfReq / (end / 1000.00)));
    }

    public void doProcessor() throws InterruptedException {
        for (int i = 0; i < 20; i = i + 5) {
            TimeUnit.SECONDS.sleep(i);
            processRequest();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new GuavaTokenDemo(50, 100).processWithTokenBucket().doProcessor();
        new GuavaTokenDemo(50, 100).processWithLeakyBucket().doProcessor();
    }

}

通过运行结果的耗时和处理请求数可以发现,当请求书低于qps的时候,结果是恒定的,对于突发差不多2倍qps的流量请求,令牌桶是可以处理的;但是漏桶的处理速率就是恒定的;

3.2、分布式限流器实现

在分布式环境下,Guava的限流就无法起到作用了,所以必须借助第三方中间件来实现,而redis的高性能使其成了首选。

3.2.1、Redis实现

基于原生Redis的分布式限流器RateLimiter可以用来在分布式环境下进行限流,例如可以用来限制调用接口的次数。
//基于redis实现的限流器
public class RedisLimiter {

    static String key = "keyWords"; //限流关键字
    static int limitCount = 10;     //限流访问频次
    static int limitTimeSecond = 5; //限流单位时间

    public static void main(String[] args) throws InterruptedException {
        Jedis j = new Jedis("127.0.0.1", 6379);
        new Thread(() -> {
            while (true) {
                if (!checkKeyLimit(j)) {
                    System.out.println("超过阈值,被限流");
                    return;
                }
                incrKey(j);
            }
        }).start();

    }
    //利用超时时间内的访问频次阈值来实现访问限制;
    private static void incrKey(Jedis j) {
        if (j.ttl(key) < 0) {
            j.incr(key); //访问一次,计数自增一次
            j.expire(key, limitTimeSecond);
        } else {
            j.incr(key);
        }
        System.out.println("总访问次数为:" + j.get(key));
    }
    //判断时间内是否到达阈值
    private static boolean checkKeyLimit(Jedis j) {
        if (null != j.get(key)) {
            if (Integer.parseInt(j.get(key)) >= limitCount) {
                return false;
            }
        }
        return true;
    }
}

3.2.2、lua脚本实现

其实和原生Redis的实现思想一样,只是利用了lua脚本执行的原子性,使用lua脚本实现限流代码如下
    -- ip_limit.lua
    -- IP 限流,对某个 IP 频率进行限制 ,1 秒钟访问 10 次 
   local num=redis.call('incr',KEYS[1])
   if tonumber(num)==1 then
       redis.call('expire',KEYS[1],ARGV[1])
       return 1
   else if tonumber(num)>tonumber(ARGV[2]) then
       return 0
   else
       return 1
   end

例如想实现对ip:192.168.8.111,限制1秒内请求超过10次就拒绝,运行命令:

./redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 1 10

3.2.3、Redission的实现

其实使用基于Redis的客户端Redisson提供的api来实现更加方便。主要实现原理是令牌桶机制,需要先获取指定的令牌,限流器每秒会产生n个令牌放入令牌桶,调用接口需要先去令牌桶里面拿令牌,起到限流的作用。
实例如下:
public class SingleTest {
    public static void main(String[] args) {
        Config config = new Config();
        config.setCodec(new org.redisson.client.codec.StringCodec());

        //指定使用单节点部署方式
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);
       
        //获取令牌桶限流
        RRateLimiter rRateLimiter = redisson.getRateLimiter("myRateLimiter");
        //初始化  最大流速 = 每1秒钟产生100个令牌
        rRateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);
        //需要1个令牌
        if(rateLimiter.tryAcquire(1)){
            //TODO: do something
        }
        //批量获取10个令牌
        if(rateLimiter.acquire(10)){
            //TODO: do something
        }

        //关闭RedissonClient
        redisson.shutdown();
    }
}

4、小结

为了保证系统的可用性,系统设计必须考虑到降级限流。
应用按层划分限流:
  • 前置化限流:例如Nginx负载均衡的分流与限流;
  • Server层接口限流方法:令牌桶及漏桶算法,hytrix熔断,请求排队+异步处理,线程池排队+拒绝策略;
  • Dao层数据库资源-池化技术,连接池-数据库资源的合理利用;
单进程的限流:guava中的Ratlimiter.create(10);
分布式下的限流策略:分布式的限流需要借助于第三方才可实现;
  • Mysql: 存储限流策略的参数等元数据
  • 基于 Redis的计数 令牌桶算法,或者lua脚本实现计数限流;
OK---待从头、收拾旧山河,朝天阙。
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值