• 高并发应用实践——限流手段


    每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。

    简介:

    每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。

    限流算法:

    令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法。

    实例

    单机–应用级

    应用级限流方式只是单应用内的请求限流,不能进行全局限流。

    1. 限流总资源数
    2. 限流总并发/连接/请求数
    3. 限流某个接口的总并发/请求数
    4. 限流某个接口的时间窗请求数
    5. 平滑限流某个接口的请求数
    6. Guava RateLimiter

    方案一:令牌桶(Token Bucket

    令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。先有一个木桶,系统按照固定速度,往桶里加入Token,如果桶已经满了就不再添加。当有请求到来时,会各自拿走一个Token,取到Token 才能继续进行请求处理,没有Token 就拒绝服务。

    arch-xianliu-1.png
    这里如果一段时间没有请求时,桶内就会积累一些Token,下次一旦有突发流量,只要Token足够,也能一次处理,所以令牌桶算法的特点是_允许突发流量_。

    Guava RateLimiter - 平滑突发限流(SmoothBursty)

    Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流
    (SmoothWarmingUp)实现。

    RateLimiter limiter = RateLimiter.create(5);
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    
    // 将得到类似如下的输出:
    0.0
    0.198239
    0.196083
    0.200609
    0.199599
    0.19961
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15



    1、RateLimiter.create(5)表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌;
    2、limiter.acquire()表示消费一个令牌,如果当前桶中有足够令牌则成功(返回值为0),如果桶中没有令牌则暂停一段时间,比如发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌(如上测试用例返回的为0.198239,差不多等待了200毫秒桶中才有令牌可用),这种实现将突发请求速率平均为了固定请求速率。如果结构不想等待可以采用tryAcquire立刻返回

    RateLimiter limiter = RateLimiter.create(5);
    System.out.println(limiter.acquire(5));
    System.out.println(limiter.acquire(1));
    System.out.println(limiter.acquire(1))
    
    // 将得到类似如下的输出:
    0.0
    0.98745
    0.183553
    0.199909
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    limiter.acquire(5)表示桶的容量为5且每秒新增5个令牌,令牌桶算法允许一定程度的突发,所以可以一次性消费5个令牌,但接下来的limiter.acquire(1)将等待差不多1秒桶中才能有令牌,且接下来的请求也整形为固定速率了。

    RateLimiter limiter = RateLimiter.create(5);
    System.out.println(limiter.acquire(10));
    System.out.println(limiter.acquire(1));
    System.out.println(limiter.acquire(1));
    
    // 将得到类似如下的输出:
    0.0
    1.997428
    0.192273
    0.200616
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    同上边的例子类似,第一秒突发了10个请求,令牌桶算法也允许了这种突发(允许消费未来的令牌),但接下来的limiter.acquire(1)将等待差不多2秒桶中才能有令牌,且接下来的请求也整形为固定速率了。

    RateLimiter limiter = RateLimiter.create(2);
    System.out.println(limiter.acquire());
    Thread.sleep(2000L);
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    System.out.println(limiter.acquire());
    
    // 将得到类似如下的输出:
    0.0
    0.0
    0.0
    0.0
    0.499876
    0.495799
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    方案二:漏桶

    水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。arch-xianliu-2.png
    可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。
    因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.、

    令牌桶和漏桶对比

    • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
    • 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
    • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
    • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
    • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
    • 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。

    方案三:计数器

    计数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。也是最简单粗暴的算法。

    采用AtomicInteger

    使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。
    弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求。

    采用令牌Semaphore

    使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
    相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。

    采用ThreadPoolExecutor java线程池

    固定线程池大小,超出固定先线程池和最大的线程数,拒绝线程请求;

  • 相关阅读:
    android 11源码编译系统介绍
    【VUE+Elemet 】正则验证 + 表单验证 + 注意事项
    XAF中XPO与EFCore的探讨
    【Java实战】大厂都是怎样进行单元测试的
    移动端适配和响应式页面中的常用单位
    unity 运行时创建一个新的场景
    FFplay播放avsync学习
    DAY 66 数据库缓存服务——NoSQL之Redis配置与优化
    C语言函数指针和返回指针值的函数
    【Unity3D】素描特效
  • 原文地址:https://blog.csdn.net/qq_45748351/article/details/126526878