分布式限流设计

文章目录

  • 1. 分布式限流概述
  • 2. 分布式限流的几种纬度
    • 2.1. QPS和连接数控制
    • 2.2. 传输速率
    • 2.3. 黑白名单
    • 2.4. 分布式环境
  • 3. 分布式限流的主流方案
    • 3.1. 客户端限流
    • 3.2. 网关层限流
    • 3.3. 中间件限流
    • 3.4. 限流组件
  • 4. 限流方案常用的算法分析
    • 4.1. 令牌桶算法
      • 4.1.1. 令牌生成
      • 4.1.2. 令牌获取
    • 4.2. 漏桶算法
    • 4.3. 漏桶vs令牌桶的区别
    • 4.4. 滑动窗口和计数器
  • 5. 使用Guava RateLimiter实现客户端限流
    • 5.1. 非阻塞式限流
    • 5.2. 阻塞式限流
  • 6. 使用Nginx实现分布式限流
    • 6.1. 来源IP限流
    • 6.2. 服务器级别做限流
    • 6.3. 基于连接数的限制


提示:以下是本篇文章正文内容,下面案例可供参考


1. 分布式限流概述

我们首先思考一下实际生活中有没有被限流的经历

DID:设计(10)-实现(5)-部署(2-3)考虑最大的问题,就是我们的部署成本

系统如果出现流量并发过大导致崩溃,我们如何去进行处理

系统架构的性能永远是设计出来的(做预判和评估)

主动型:

  • 动静分离(CDN)
  • 柔性扩展,水平扩容(无状态)

被动型:

  • 服务降级
  • 限流设计

降级系统压力的最有效的方式就是减少访问的流量,就是将用户拒之门外。可以通过一句友好提示“服务正忙,请稍后重试”,之后就将用户丢弃不管了。这种方式比较直接,对于用户体验不好。

买火车票,基本都是在流量特别大的高峰阶段,让你每次都要输入对应的验证信息的结果,这就是赤裸裸的限流方法:业务限流

2. 分布式限流的几种纬度

对一般的限流场景来说它具有两个维度信息:

  • 时间:限流基于某段时间范围或者某个时间点,也就是我们常说的时间窗口,比如对每分钟,每秒钟的时间窗口做限定
  • 资源:基于可用资源的限制,比如设定最大访问次数,或最高可用连接数

限流其实就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。但在真正的场景里,我们不止设置一种限流规则,而是限制多个限流规则共同使用,主要的几种限流规则如下:

2.1. QPS和连接数控制

基于QPS,可以设定基于IP纬度的限制,也可以设置基于某个服务器的限制,真实环境中通常会设置多个维度的限流规则,比如同一个IP每秒访问频率小于10,连接数小于5,再设定每台机器QPS最高1000,连接最大保持200

2.2. 传输速率

下载方式的传输流量限制,普通用于下载速度100k/s,会员10m/s,这背后就是用户组或用户标签下的限流逻辑

2.3. 黑白名单

黑名单就是用来做IP访问限制的

白名单就是将IP加入后,跳出限流规则的方式

2.4. 分布式环境

所谓的分布式限流,其实道理很简单,它将整个分布式环境中所有服务器当做一个整体来考量。比如IP的限流,1秒可以访问10次,无论这个IP落在哪个机器上,只要访问的是集群中的服务节点,都会受到限流规则的制约

从上面这个例子来看,我们必须要将限流信息保存在一个“中心化”的组件上,这样它就可以获取到集群中所有机器的访问状态,目前两个比较主流限流方案:

  • 网关层限流:将限流规则应用在所有流量的入口处
  • 中间件限流:将限流信息存储在分布式环境中某个中间里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量

3. 分布式限流的主流方案

3.1. 客户端限流

Google的Guava客户端组件,是Google的一款非常好用的工具包,Guava提供了一个RateLimiter的实现,主要就是进行限流支持的,既然Guava是一个客户端组件,也就说明它的作用范围仅限于当前的服务器,不能对集群内的其他服务器施加流量限制

分布式限流设计_第1张图片

尽管Guava不是面向分布式系统的解决方案,但作为一个简单轻量级的客户端限流组件,非常适合进行业务限流

3.2. 网关层限流

在整个分布式系统中,服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访问的请求,我们进行系统访问的过程中,网关限流将所有流量都在入口处进行屏蔽了

3.3. 中间件限流

对于业务系统需要有一个类似中心节点的存储限流状态数据的地方,由这个地方进行限流的设置,而Redis就是最适合的中间件限流组件,对于限流能够提供相关请求和存储的支持

3.4. 限流组件

比如sentinel就提供限流的具体实施方式,很好的集成在sentinel中了

4. 限流方案常用的算法分析

无论是Guava还是Nginx、Redis限流,到最后都是应用几种具体的限流算法

常见的限流算法:令牌桶算法、漏桶算法、滑动窗口和计数器算法来结合使用

4.1. 令牌桶算法

Token Bucket令牌桶算法目前应用最为广泛的限流算法,顾名思义,他有以下两个关键角色

1、令牌:只有获得到令牌的Request请求才会被处理,其他Request请求要么排队要么被直接丢弃

2、桶:用来装令牌的地方,所有Request都从这个桶里获取令牌

分布式限流设计_第2张图片

4.1.1. 令牌生成

对于令牌生成器来说,他会根据一个预定的速率向桶中添加令牌,发放速率是匀速的

4.1.2. 令牌获取

每个访问请求到来后,必须获取到一个令牌才能执行后面的逻辑,如果访问请求过多,无法获得令牌,这个时候就可以在获取令牌前将请求消费放入MQ做缓存,如果令牌够就ACK,如过不够就NACK重归队列,通过MQ来缓存暂时没有拿到令牌的请求

4.2. 漏桶算法

分布式限流设计_第3张图片

漏桶算法的前半段和令牌桶类似,但操作的对象不同,令牌桶是将令牌放入桶中,而漏桶是将请求放入桶中,如果桶满了,后面来的数据包将会被丢失

漏桶算法的后半段永远只会以一个恒定的速率将数据包从桶内流出

4.3. 漏桶vs令牌桶的区别

根据各自的特点不难看出,这两种算法都有一个恒定的速率和不定的速率,令牌桶是以恒定速率创建令牌,但是访问请求获取令牌的速率不定,反正有多少令牌就发多少,令牌没了就干等。而漏桶是以恒定的速率处理请求,但是这些请求流入桶的速率是不定的

从这两个特点来讲,漏桶的天然特性决定了它不会发生突发流量,就算1000个请求到来,那么它对于后的服务输出的访问速率永远恒定,而令牌桶不一样,因为可以预存一定量的令牌,因此在应对突发流量的时候可以在短时间内消耗所有的令牌,突发流量的处理效率会比漏桶高,但是导向后台系统的压力也会相应增多

4.4. 滑动窗口和计数器

分布式限流设计_第4张图片

上面的大框就是时间窗口,我们设定时间窗口是5秒,随着时间向后滑动,时间盒子不断向后刷新,第1秒有5个请求,第5秒有10个请求,一共有15个请求,时间盒子向后移动了1秒,将第1秒的5个请求已经移出,将第6秒加入,还可以有20-10=10个请求加入

5. 使用Guava RateLimiter实现客户端限流

5.1. 非阻塞式限流

POM依赖

        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>28.2-jreversion>
        dependency>

controller

package com.icodingedu.supermall.controller;

import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@Slf4j
public class LimiterController {

    //允许这个限流组件每秒发放两个令牌
    RateLimiter rateLimiter = RateLimiter.create(2.0);

    @GetMapping("/tryAcquire")
    public String tryAcquire(Integer count){
        //如果每秒消耗1个令牌,一秒可以接收2次请求,消耗2个则1秒接收1个,消耗4个需要2秒
        if(rateLimiter.tryAcquire(count)){
            log.info("success rate is {}",rateLimiter.getRate());
            return "success";
        }else{
            log.info("fail rate is {}",rateLimiter.getRate());
            return "fail";
        }
    }

  	//增加了等待时间的方式
    @GetMapping("/tryAcquirewithtimeout")
    public String tryAcquire(Integer count,Integer timeout){
        //如果每秒消耗1个令牌,一秒可以接收2次请求,消耗2个则1秒接收1个,消耗4个需要2秒
        //timeout是指请求的时间间隔
        if(rateLimiter.tryAcquire(count,timeout, TimeUnit.SECONDS)){
            log.info("success rate is {}",rateLimiter.getRate());
            return "success";
        }else{
            log.info("fail rate is {}",rateLimiter.getRate());
            return "fail";
        }
    }
}

5.2. 阻塞式限流

    //使用阻塞式限流
    @GetMapping("/acquire")
    public String acquire(Integer count){
        rateLimiter.acquire(count);
        log.info("success rate is {}",rateLimiter.getRate());
        return "success";
    }

令牌不够则等待令牌获取够了才执行后续内容

目前还是在使用Guava RateLimiter,是单机限流,还不是分布式限流

6. 使用Nginx实现分布式限流

6.1. 来源IP限流

# 首先在/etc/hosts增加一个本地域名映射
# 127.0.0.1 limit.icodingedu.com
# 根据IP地址限制访问流量
# 1.$binary_remote_addr:相当于获取来源ip的一个系统变量
# 2.zone=iplimit:20m:iplimit相当于自己开辟的存放限流数据的一个内存区域,20m是指内存大小
# 3.rate=1r/s:标示限流的频率,100r/m
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=1r/s;

server {
	server_name limit.icodingedu.com;
	location /access-limit/ {
		proxy_pass http://127.0.0.1:8080/;
		# 1.zone=iplimit:引用zone中的变量
		# 2.burst=2,设置一个大小为2的缓存区域,当大量请求到来时,请求超过限流频率时,放入缓冲区
		# 3.nodelay:缓存区满了以后直接返回503
		limit_req zone=iplimit burst=2 nodelay;
	}
}

加入controller测试

@RestController
public class NginxController {

    @GetMapping("/nginx-conn")
    public String nginxConn(){
        return "nginx-success";
    }
}

springboot访问地址:http://limit.icodingedu.com:8080/nginx-conn

nginx访问地址:http://limit.icodingedu.com/access-limit/nginx-conn

6.2. 服务器级别做限流

# 首先在/etc/hosts增加一个本地域名映射
# 127.0.0.1 limit.icodingedu.com
# 根据IP地址限制访问流量
# 1.$binary_remote_addr:相当于获取来源ip的一个系统变量
# 2.zone=iplimit:20m:iplimit相当于自己开辟的存放限流数据的一个内存区域,20m是指内存大小
# 3.rate=1r/s:标示限流的频率,100r/m
limit_req_zone $binary_remote_addr zone=iplimit:10m rate=10r/s;

# 根据服务器级别做限流
limit_req_zone $server_name zone=serverlimit:10m rate=1r/s;

server {
	server_name limit.icodingedu.com;
	location /access-limit/ {
		proxy_pass http://127.0.0.1:8080/;
		# 1.zone=iplimit:引用zone中的变量
		# 2.burst=2,设置一个大小为2的缓存区域,当大量请求到来时,请求超过限流频率时,放入缓冲区
		# 3.nodelay:缓存区满了以后直接返回503
		limit_req zone=iplimit burst=2 nodelay;
		# 基于服务器的限流,一般来讲server级别限流是比较大的,但这里做测试就缩小来看
		limit_req zone=serverlimit burst=1 nodelay;
	}
}

6.3. 基于连接数的限制

# 首先在/etc/hosts增加一个本地域名映射
# 127.0.0.1 limit.icodingedu.com
# 根据IP地址限制访问流量
# 1.$binary_remote_addr:相当于获取来源ip的一个系统变量
# 2.zone=iplimit:20m:iplimit相当于自己开辟的存放限流数据的一个内存区域,20m是指内存大小
# 3.rate=1r/s:标示限流的频率,100r/m
limit_req_zone $binary_remote_addr zone=iplimit:10m rate=20r/s;

# 根据服务器级别做限流
limit_req_zone $server_name zone=serverlimit:10m rate=200r/s;

# 基于IP的连接数配置
limit_conn_zone $binary_remote_addr zone=perip:10m;

server {
	server_name limit.icodingedu.com;
	location /access-limit/ {
		proxy_pass http://127.0.0.1:8080/;
		# 1.zone=iplimit:引用zone中的变量
		# 2.burst=2,设置一个大小为2的缓存区域,当大量请求到来时,请求超过限流频率时,放入缓冲区
		# 3.nodelay:缓存区满了以后直接返回503
		limit_req zone=iplimit burst=2 nodelay;
		# 基于服务器的限流,一般来讲server级别限流是比较大的,但这里做测试就缩小来看
		limit_req zone=serverlimit burst=1 nodelay;
		# 基于IP的连接数设置
		limit_conn perip 1;
	}
}

controller的方法

    @GetMapping("/nginx-count")
    public String nginxCount(int sec){
        try {
            Thread.sleep(1000 * sec);
        }catch(Exception ex){
            ex.printStackTrace();
        }
        return "nginx-count success";
    }

你可能感兴趣的:(springboot,Sptring,spring,cloud,gateway,nginx)