流量控制与RateLimiter

一背景

如何提高系统的稳定性,简单来说除了加机器外就是服务降级、限流。加机器就是常说的分布式,从整个架构的稳定性角度看,一般SOA每个接口的所能提供的单位时间服务能力是有上限。假如超过服务能力,一般会造成整个接口服务停顿,或者应用挂了,将延迟传递给服务调用方造成整个系统的服务能力丧失。要是对外的公开 API 接口服务,Rate limiting 应该是一个必备的功能,否极有可能被恶意调用导致服务宕掉,所以限流是必要的。这里本文就整理下常见的限流原理及方案。
流量控制更专业的叫法是:流量整形(traffic shaping),典型作用是限制流出某一网络的某一连接的流量与突发,使这类报文以比较均匀的速度向外发送。

二 常用方法

令牌桶(Token Bucket)和漏桶(leaky bucket)是 最常用的两种限流的算法。

2.1 漏桶算法

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


可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate),在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使某一个单独的流突发到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法可以结合起来为网络流量提供更大的控制。

2.2令牌桶算法


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

三 guava RateLimiter

3.1RateLimiter 简介

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用.RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到.

RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率。与Semaphore 相比,Semaphore 限制了并发访问的数量而不是使用速率。(注意尽管并发性和速率是紧密相关的,比如参考Little定律

通过设置许可证的速率来定义RateLimiter。在默认配置下,许可证会在固定的速率下被分配,速率单位是每秒多少个许可证。为了确保维护配置的速率,许可会被平稳地分配,许可之间的延迟会做调整。
可能存在配置一个拥有预热期的RateLimiter 的情况,在这段时间内,每秒分配的许可数会稳定地增长直到达到稳定的速率。

有一点很重要,那就是请求的许可数从来不会影响到请求本身的限制(调用acquire(1) 和调用acquire(1000) 将得到相同的限制效果,如果存在这样的调用的话),但会影响下一次请求的限制,也就是说,如果一个高开销的任务抵达一个空闲的RateLimiter,它会被马上许可,但是下一个请求会经历额外的限制,从而来偿付高开销任务。注意:RateLimiter 并不提供公平性的保证。

3.2 code

我们要实现一个基于速率的单机流控框架的时候,RateLimiter 是一个完善的核心组件,下面是demo

import java.util.concurrent.ConcurrentMap;

import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;

public class TrafficShaper {

	//key-value(serverice,qps)
    private static final ConcurrentMap<String, Double> resourceMap = Maps.newConcurrentMap();
    //userkey-service limiter
    private static final ConcurrentMap<String, RateLimiter> userresourceLimiterMap = Maps.newConcurrentMap();
    static {
    	//init
    	resourceMap.put("aaa", 50.0);
    }
    public static void updateResourceQps(String resource, double qps) {
    	resourceMap.put(resource, qps);
    }

    public static void removeResource(String resource) {
    	resourceMap.remove(resource);
    }

    public static int enter(String resource,String userkey)  {
    	long t1 = System.currentTimeMillis();
        double qps = resourceMap.get(resource);
        //服务不限流
        if (qps == 0.0) {
            return 0;
        }
        String keyser = resource+userkey;
        RateLimiter keyserlimiter = userresourceLimiterMap.get(keyser);
        //if null,new limiter 
        if(keyserlimiter == null)
        {
        	keyserlimiter =RateLimiter.create(qps);
        	RateLimiter putByOtherThread = userresourceLimiterMap.putIfAbsent(keyser, keyserlimiter);
        	if (putByOtherThread != null) {
        		keyserlimiter = putByOtherThread;
            }
        	keyserlimiter.setRate(qps);
        }
       
        //tryacquire
        if (!keyserlimiter.tryAcquire()) {        	
        	System.out.println("use:"+(System.currentTimeMillis()-t1)+"ms;"+resource+"  visited  too frequently  by key:"+userkey);
        	
        	return 99;
        }else{
        	System.out.println("use:"+(System.currentTimeMillis()-t1)+"ms;");
        	return 0;
        }
		
    }
	
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		int i=0;
		
		while(true){
			i++;
			long t2 = System.currentTimeMillis();
			System.out.println(t2+":qq:"+i);	
			
			int res =	TrafficShaper.enter("aaa", "qq");
			System.out.println((System.currentTimeMillis()-t2)+":qq:"+i);		
			if(res ==99)
			{					
				i=0;
				Thread.sleep(1000);
			}
			
		}		
	}
}
简单说明下,这里核心方法是enter,入参是两个,分别是服务名称跟用户key.预期效果就是开放API对于用户来说某个服务只允许调用最大次数。

运行结果:


3.3 API接口

修饰符和类型 方法和描述
doubleacquire()
从RateLimiter获取一个许可,该方法会被阻塞直到获取到请求
doubleacquire(int permits)
从RateLimiter获取指定许可数,该方法会被阻塞直到获取到请求
static RateLimitercreate(double permitsPerSecond)
根据指定的稳定吞吐率创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少查询)
static RateLimitercreate(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
根据指定的稳定吞吐率和预热期来创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少个请求量),在这段预热时间内,RateLimiter每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率。(只要存在足够请求数来使其饱和)
doublegetRate()
返回RateLimiter 配置中的稳定速率,该速率单位是每秒多少许可数
voidsetRate(double permitsPerSecond)
更新RateLimite的稳定速率,参数permitsPerSecond 由构造RateLimiter的工厂方法提供。
StringtoString()
返回对象的字符表现形式
booleantryAcquire()
从RateLimiter 获取许可,如果该许可可以在无延迟下的情况下立即获取得到的话
booleantryAcquire(int permits)
从RateLimiter 获取许可数,如果该许可数可以在无延迟下的情况下立即获取得到的话
booleantryAcquire(int permits, long timeout, TimeUnit unit)
从RateLimiter 获取指定许可数如果该许可数可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可数的话,那么立即返回false (无需等待)
booleantryAcquire(long timeout, TimeUnit unit)
从RateLimiter 获取许可如果该许可可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可的话,那么立即返回false(无需等待)

3.4 源码分析

RateLimiter主要源码分析
两个create函数用于构建不同形式的RateLimiter
public static RateLimiter create(double permitsPerSecond)
用于创建SmoothBursty类型的RateLimiter
public static RateLimiter create(double permitsPerSecond,long warmupPeriod,TimeUnit unit)
用于创建SmoothWarmingUp类型的RateLimiter.API注释上比较长,如下:
根据指定的稳定吞吐率和预热期来创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少查询),在这段预热时间内,RateLimiter每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率(只要存在足够请求数来使其饱和)。同样地,如果RateLimiter 在warmupPeriod时间内闲置不用,它将会逐步地返回冷却状态。也就是说,它会像它第一次被创建般经历同样的预热期。返回的RateLimiter 主要用于那些需要预热期的资源,这些资源实际上满足了请求(比如一个远程服务),而不是在稳定(最大)的速率下可以立即被访问的资源。返回的RateLimiter 在冷却状态下启动(即预热期将会紧跟着发生),并且如果被长期闲置不用,它将回到冷却状态。
下面以acquire为例子,看下源码的实现
 public double acquire() {
    return acquire(1);
  }
  public double acquire(int permits) {
    long microsToWait = reserve(permits);
    stopwatch.sleepMicrosUninterruptibly(microsToWait);//<span style="color: rgb(0, 128, 0); font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; line-height: 1.42857143; background-color: rgb(245, 245, 245);"><span style="font-size:10px;">等待,当未达到限制时,microsToWait为0</span></span>

    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }
  final long reserve(int permits) {
    checkPermits(permits);//<span style="color:#006600;">参数校验>0</span>
    synchronized (mutex()) {//<span style="color:#006600;">并发的情况下同步</span>
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());//<span style="color:#006600;">获取需要等待的时间</span>
    }
  }
  final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
  }
 abstract long reserveEarliestAvailable(int permits, long nowMicros);
这是个抽象接口,我们看下具体实现类SmoothRateLimiter:
  @Override
  final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);//<span style="color:#006600;">补充令牌</span>
    long returnValue = nextFreeTicketMicros;
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);//<span style="color:#006600;">本次请求消耗的令牌数</span>
    double freshPermits = requiredPermits - storedPermitsToSpend;

    long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
        + (long) (freshPermits * stableIntervalMicros);

    this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;//<span style="color:#006600;">计算下次可用时间</span>
    this.storedPermits -= storedPermitsToSpend;//<span style="color:#006600;">消耗令牌</span>
    return returnValue;
  }
 private void resync(long nowMicros) {//<span style="color:#006600;">补充令牌数,及更新下次可用令牌毫秒数</span>
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
      storedPermits = min(maxPermits,
          storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
      nextFreeTicketMicros = nowMicros;
    }
  }

对于 storedPermitsToWaitTime,这是一个抽象接口,
  /**
   * Translates a specified portion of our currently stored permits which we want to
   * spend/acquire, into a throttling time. Conceptually, this evaluates the integral
   * of the underlying function we use, for the range of
   * [(storedPermits - permitsToTake), storedPermits].
   *
   * <p>This always holds: {@code 0 <= permitsToTake <= storedPermits}
   */
  abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake);

RateLimiter实际上由两种实现策略,其实现分别见SmoothBursty和SmoothWarmingUp。

3.4.1SmoothBursty

SmoothBursty使用storedPermits不需要额外等待时间。并且默认maxBurstSeconds为1,因此maxPermits为permitsPerSecond,即最多可以存储1秒的剩余令牌,比如QPS=4,则maxPermits=4.
<span style="font-size:12px;"> static final class SmoothBursty extends SmoothRateLimiter {
    /** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */
    final double maxBurstSeconds;     
    SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
      super(stopwatch);
      this.maxBurstSeconds = maxBurstSeconds;
    }  
    @Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = this.maxPermits;
      maxPermits = maxBurstSeconds * permitsPerSecond;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = maxPermits;
      } else {
        storedPermits = (oldMaxPermits == 0.0)
            ? 0.0 // initial state
            : storedPermits * maxPermits / oldMaxPermits;
      }
    }  
    @Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
      return 0L;
    }
  }</span>
 RateLimiter 允许某次请求拿走超出剩余令牌数的令牌,但是下一次请求将为此付出代价,一直等到令牌亏空补上,并且桶中有足够本次请求使用的令牌为止 2 。这里面就涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,还是让它先走掉后面的请求等一等呢?Guava 的设计者选择的是后者,先把眼前的活干了,后面的事后面再说。这里我看网上举了例子便于理解,以每秒qps=4,头两步消耗4个,剩余存储4个。在第三步的时候之前存储了4个,加上本次的共8个,但是本次请求了10个,所以透支了2个,第四次请求的时候,需要补上2个,等待0.5秒。
(1).t=0,这时候storedPermits=0,请求1个令牌,等待时间=0;
(2).t=1,这时候storedPermits=3,请求3个令牌,等待时间=0;
(3).t=2,这时候storedPermits=4,请求10个令牌,等待时间=0,超前使用了2个令牌;
     (4).t=3,这时候storedPermits=0,请求1个令牌,等待时间=0.5;

3.4.2SmoothWarmingUp

static final class SmoothWarmingUp extends SmoothRateLimiter {
    private final long warmupPeriodMicros;
    /**
     * The slope of the line from the stable interval (when permits == 0), to the cold interval
     * (when permits == maxPermits)
     */
    private double slope;
    private double halfPermits;
  
    SmoothWarmingUp(SleepingStopwatch stopwatch, long warmupPeriod, TimeUnit timeUnit) {
      super(stopwatch);
      this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod);
    }
  
    @Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = maxPermits;
      maxPermits = warmupPeriodMicros / stableIntervalMicros;
      halfPermits = maxPermits / 2.0;
      // Stable interval is x, cold is 3x, so on average it's 2x. Double the time -> halve the rate
      double coldIntervalMicros = stableIntervalMicros * 3.0;
      slope = (coldIntervalMicros - stableIntervalMicros) / halfPermits;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = 0.0;
      } else {
        storedPermits = (oldMaxPermits == 0.0)
            ? maxPermits // initial state is cold
            : storedPermits * maxPermits / oldMaxPermits;
      }
    }
  
    @Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
      double availablePermitsAboveHalf = storedPermits - halfPermits;
      long micros = 0;
      // measuring the integral on the right part of the function (the climbing line)
      if (availablePermitsAboveHalf > 0.0) {
        double permitsAboveHalfToTake = min(availablePermitsAboveHalf, permitsToTake);
        micros = (long) (permitsAboveHalfToTake * (permitsToTime(availablePermitsAboveHalf)
            + permitsToTime(availablePermitsAboveHalf - permitsAboveHalfToTake)) / 2.0);
        permitsToTake -= permitsAboveHalfToTake;
      }
      // measuring the integral on the left part of the function (the horizontal line)
      micros += (stableIntervalMicros * permitsToTake);
      return micros;
    }
  
    private double permitsToTime(double permits) {
      return stableIntervalMicros + permits * slope;
    }
  }
maxPermits等于热身(warmup)期间能产生的令牌数,比如QPS=4,warmup为2秒,则maxPermits=8.halfPermits为maxPermits的一半.

这个图还不是很理解,对比上一个实现方式,就是不能透支,需要的资源就等待。这里待测试验证。

四 其他常见实现方式

4.1Proxy 层的实现,针对部分 URL 或者 API 接口进行访问频率限制

Nginx 模块
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
    location /search/ {
        limit_req zone=one burst=5;
    }

 详细参见:  ngx_http_limit_req_module

Haproxy 提供的功能

详细参见:  Haproxy Rate limit 模块

4.2基于 Redis 功能的实现

这个在 Redis 官方文档有非常详细的实现。一般适用于所有类型的应用,比如 PHP、Python 等等。Redis 的实现方式可以支持分布式服务的访问频率的集中控制。Redis 的频率限制实现方式还适用于在应用中无法状态保存状态的场景。

参见:Redis INCR rate limiter


参考:
http://xiaobaoqiu.github.io/blog/2015/07/02/ratelimiter/
  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: SpringBoot可以通过使用RateLimiter来实现流量控制,可以通过在SpringBoot应用中定义RateLimiter Bean来实现,例如: @Bean public RateLimiter rateLimiter(){ return RateLimiter.create(5.0); // 每秒最多处理5个请求 } 然后,在需要控制流量的方法上添加@RateLimiter注解,例如: @RateLimiter(limit=5, timeout=100) // 每秒最多处理5个请求,超时时间为100ms public void doSomething(){ // do something } ### 回答2: 在SpringBoot中,可以通过使用RateLimiter来实现流量控制。下面是具体的实现方式: 1. 引入依赖:首先,在项目的pom.xml文件中添加以下依赖项,以引入RateLimiter库。 ```xml <dependencies> ... <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency> </dependencies> ``` 2. 创建限流器:在需要进行流量控制的地方,创建一个RateLimiter对象。 ```java import com.google.common.util.concurrent.RateLimiter; ... // 创建一个每秒允许处理10个请求的限流RateLimiter rateLimiter = RateLimiter.create(10); ``` 3. 限流处理:在需要限制流量的代码块中,使用RateLimiter的`acquire`方法来获取令牌。如果令牌不可用,则该方法会阻塞,直到有可用的令牌。 ```java // 尝试获取一个令牌,如果没有可用的令牌,则等待 rateLimiter.acquire(); // 执行需要流量控制的代码逻辑 ``` 4. 控制流量速率:可以通过调整RateLimiter的速率来控制每秒处理请求的数量。 ```java // 设置每秒处理请求数为20个 rateLimiter.setRate(20); ``` 通过上述步骤,就可以在SpringBoot项目中使用RateLimiter来实现流量控制。请注意,在实际应用中,需要根据具体的需求和业务场景调整速率和限流的位置,以达到合理的流量控制效果。 ### 回答3: 在Spring Boot中使用RateLimiter来实现流量控制需要进行以下具体的实现步骤: 1. 导入依赖:在项目的pom.xml文件中添加RateLimiter的依赖,例如: ``` <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency> ``` 2. 创建RateLimiter对象:在需要进行流量控制的地方,如Controller的方法中,创建RateLimiter对象,设置限流阈值,例如: ``` RateLimiter rateLimiter = RateLimiter.create(10); // 设置限流阈值为每秒最多处理10个请求 ``` 3. 控制流量:在需要控制流量的地方调用RateLimiter对象的acquire方法,用于尝试获取许可,当许可不足时会进行等待,例如: ``` if (rateLimiter.tryAcquire()) { // 执行业务逻辑 } else { // 返回限流提示信息或进行其他处理 } ``` 4. 配置相关参数:可以通过配置文件为RateLimiter设置相关参数,例如将限流阈值配置为可动态调整的: ``` @Value("${rateLimiter.threshold}") private double threshold; ... RateLimiter rateLimiter = RateLimiter.create(threshold); ``` 通过上述步骤,就可以在Spring Boot项目中使用RateLimiter来实现流量控制了。需要注意的是,RateLimiter是基于令牌桶算法实现的,限流的精度是毫秒级别的,可以根据具体需求进行调整。另外,还可以配合其他组件(如拦截器、AOP等)进行更细粒度的流量控制,提高系统的稳定性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值