likes
comments
collection
share

面试官问:限流或RateLimit要怎么设计

作者站长头像
站长
· 阅读数 43

频率控制问题是面试考核案例和设计里面的一个经典问题。

这其实也是程序设计里面的一个经典问题。

作为后台接口服务提供方,如果不限制调用频率,调用方用不合理的频率调用接口就会给后端服务造成很大的压力。甚至,直接造成服务的奔溃,那么承诺的999的在线时长就满足不了了~ 作为服务调用者,首先也需要了解到服务会不会被限频并采取相应的措施处理。

频率控制场景和整体架构

频率控制场景主要包括这几个场景

  • 频控服务
  • 浏览数记录服务

面试官问:限流或RateLimit要怎么设计 调用方调用服务,先经过频控服务,如果频率没有超过限制就可以放行给后台服务。

另外对于生产内容的网站,一个文章的浏览数是一个很重要的积分指标,这个时候我们希望用户不要去刷这个数字。在日志服务记录日志的时候也要经过频率控制判断,该记录是否被记录到日志。 虽然使用的目的不同,但这两种场景的频控服务本事可以是一样的思想。

调用服务的频控+重试

如果服务端限制了频率,客户端频繁的调用就会失败,如果只考虑调用失败重试的做法,也会浪费不必要的资源。所以,客户端了解到服务端的频率控制策略后,应该也做同样的频率控制服务。

面试官问:限流或RateLimit要怎么设计

  • 客户端被限频后,就服务端的限频导致客户端异常❌
  • 客户端可以考虑使用失败重试,失败重新消耗资源❌
  • 客户端也使用rate limiting👌 调用服务,就和服务端配合完美了。 客户端调用可以是单线程、多线程并发或者也可能是分布式的(比较少)。 单个机器可以使用Guava的RateLimiter来做这个事情。

实现方案和思考优化

频控服务的目标是什么呢? 一般来说不要限制的太死,也不要过于宽泛。可以是:

  • 每秒钟内最多访问1000次
  • 每秒钟内最多访问100次
  • 每分钟内最多访问1000次
  • 5秒钟最多访问一次

怎么限频可以是根据业务规则来,或根据服务能力来。“每秒钟内最多访问1000次”的服务能力就需要比“每分钟内最多访问1000次”高。

其实另一方面,这也是对服务队列长度的一个权衡,“每分钟内最多访问1000次”的队列需要有1000,“每秒钟内最多访问152次”的处理队列只有152;虽然总体处理能力没有差不多,但前者需要的缓冲和应对瞬时压力的能力需要大很多倍才能扛住压力。

频控服务实现

一般可以使用Redis来实现频控服务,因为redis的key天然自带时间属性(里面有延时器)。

简单思想

假设限流为“每秒钟最多100次”,采用redis来实现。我们很容易想到的伪代码如下:

//对于某个user
key=ratelimiter-service-user
如果key不存在{
则设置为1
expire key 1s
}
else{
如果key的值>=100,则提示超频了
否则set key incr
}

看起来逻辑还是很清晰的。

但是这样的实现是有问题的。 达不到“每秒钟最多100次”的目标。 如果在一个1s的末尾1ms内访问了100次,在紧接着的1s的开头1ms内又访问了100次,那就是1s钟内访问了200次了。

优化

必须严格满足“1秒钟内最多访问100次”的目标。如果我们记下每次访问的时间,保留最近100次的时间的列表,在有新访问的时候,拿这个列表最早的时间和现在相比。如果列表不满100,则可以访问;否则:如果时间的查大于1s,则可以访问;否则不行。

伪代码实现如下:

//对于某个user
key=ratelimiter-service-user
if(key不存在){
  set key [timestamp]
}
else{
  if(列表长度小于100){
    add timestamp到列表
}else{
   if{现在时间和最早的时间的差<1s){
        限频
    }else{
     可以访问

    }
  }
}

redis是单进程而且还是单线程的,所以redis的操作在并发带调用场景下都是原子操作。 这样就可以满足目标了。 但是这种方案需要用到更多的内存。每个key都需要维护一个列表,空间复杂度比较高。

经典的模型:漏桶、令牌桶

你一定听说过,在流量整形或者限流方面,还有两种经典的算法模型,那就是漏桶算法和令牌桶算法。

也有很多工具,替我们实现了这些算法,比如Guava的RateLimiter实现了令牌桶算法。

Guava的RateLimiter是一种单机的实现方案。单机实现不需要用到redis,整体来说有更小的延时性能。对于我们上面提到的调用方的频控场景,是非常合适的

接下来看看这些模型算法具体是什么,和上面的实现有什么区别和联系。

漏桶算法

漏桶算法的基本思想是以恒定的速率发出调用请求。 如下图:

面试官问:限流或RateLimit要怎么设计

  • 业务请求速度看实际情况,将请求放入桶里,但是如果桶满了则会丢弃请求
  • 漏桶以最大的恒定速率将请求发出。

如果把流控交给一个新的线程去做,则实现的逻辑很清晰简单,伪代码如下

业务请求放入一个队列(队列大小为桶大小);
新的线程一恒定速率(每次sleep一个固定的时间)将队列中的请求发出去;

漏桶算法最大速率恒定,能满足服务端的目标“每秒最多100次”,但是如果业务请求过大,则会被抛弃,这在有的场景下是不能接受的。

令牌桶算法

令牌桶算法的基本思想是业务请求自己去拿令牌,能拿到令牌则可以发送请求。 如下图:

面试官问:限流或RateLimit要怎么设计

  • ticket服务以恒定速率生产ticket,并放入令牌桶
  • 业务请求获取需要的ticket的个数,
  • 如果能拿到足够的ticket,则执行请求
    • 否则阻塞限流。

如果也将流控交给一个新的线程去做,则实现也非常简单:

一个线程以恒定的速率往桶里放入ticket(一般桶的容量不会超过单位时间的流量);
业务请求从桶里获取对应数量的ticket,如果不能拿到,则等待;否则执行对应的请求;

guava实现

Guava RateLimiter也实现了令牌桶算法,但并不是将流控交给一个新的线程去做,基本思想是让业务方调用的线程自己去计算要阻塞的时长,这样就节省了一个ticket线程。

基本方案是:

维护一个Rate Limiter的实例limiter;
limiter里面有一些时间相关的变量()
假设业务线程acquire n个ticket,则先计算要阻塞的时间,更新那些便利,然后让自己阻塞一定的时间后开始调用

在Guava RateLimiter的实现里面,上一次调用的ticket的生成时间,一般由下一次调用的来承担。

令牌桶算法的总结

令牌桶算法能满足服务端的目标“每秒最多100次”,也没有将请求丢弃的风险。

总结

说到底限流或Rate Limit是为了限制对计算机资源的使用,防止服务奔溃。本文通过分析场景、和限频要达到的目标 ,绘制了整体的架构、总结了模型和实现的一些看法,希望对你有帮助。

觉得有帮助的小伙伴可以一键三连哦~

转载自:https://juejin.cn/post/7071420693337866276
评论
请登录