分布式服务限流实战,已经为你排好坑了 | 总结的很全面

2024-09-02 10:32

本文主要是介绍分布式服务限流实战,已经为你排好坑了 | 总结的很全面,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

点击上方“朱小厮的博客”,选择“设为星标”

当当满300-50优惠码:TMWCP4

640

一、限流的作用

由于API接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。 

限流(Rate limiting)指对应用服务的请求进行限制,例如某一接口的请求限制为100个每秒,对超过限制的请求则进行快速失败或丢弃。

限流可以应对:

  • 热点业务带来的突发请求;

  • 调用方bug导致的突发请求;

  • 恶意攻击请求。

因此,对于公开的接口最好采取限流措施。

二、为什么要分布式限流

640?wx_fmt=png

当应用为单点应用时,只要应用进行了限流,那么应用所依赖的各种服务也都得到了保护。

640?wx_fmt=png

但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。

640?wx_fmt=png

而如果实现了分布式限流,那么就可以方便地控制整个服务集群的请求限制,且由于整个集群的请求数量得到了限制,因此服务依赖的各种资源也得到了限流的保护。

三、限流的算法

实现限流有很多办法,在程序中时通常是根据每秒处理的事务数(Transaction per second)来衡量接口的流量。 

本文介绍几种最常用的限流算法:

  • 固定窗口计数器;

  • 滑动窗口计数器;

  • 漏桶;

  • 令牌桶。

1、固定窗口计数器算法

640?wx_fmt=png

固定窗口计数器算法概念如下:

  • 将时间划分为多个窗口;

  • 在每个窗口内每有一次请求就将计数器加一;

  • 如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃当时间到达下一个窗口时,计数器重置。

固定窗口计数器是最为简单的算法,但这个算法有时会让通过请求量允许为限制的两倍。考虑如下情况:限制1秒内最多通过5个请求,在第一个窗口的最后半秒内通过了5个请求,第二个窗口的前半秒内又通过了5个请求。这样看来就是在1秒内通过了10个请求。 

640?wx_fmt=png

2、滑动窗口计数器算法

640?wx_fmt=png

滑动窗口计数器算法概念如下:

  • 将时间划分为多个区间;

  • 在每个区间内每有一次请求就将计数器加一维持一个时间窗口,占据多个区间;

  • 每经过一个区间的时间,则抛弃最老的一个区间,并纳入最新的一个区间;

  • 如果当前窗口内区间的请求计数总和超过了限制数量,则本窗口内所有的请求都被丢弃。

滑动窗口计数器是通过将窗口再细分,并且按照时间"滑动",这种算法避免了固定窗口计数器带来的双倍突发请求,但时间区间的精度越高,算法所需的空间容量就越大。

3、漏桶算法

640?wx_fmt=png

漏桶算法概念如下:

  • 将每个请求视作"水滴"放入"漏桶"进行存储;

  • "漏桶"以固定速率向外"漏"出请求来执行如果"漏桶"空了则停止"漏水";

  • 如果"漏桶"满了则多余的"水滴"会被直接丢弃。

漏桶算法多使用队列实现,服务的请求会存到队列中,服务的提供方则按照固定的速率从队列中取出请求并执行,过多的请求则放在队列中排队或直接拒绝。

漏桶算法的缺陷也很明显,当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。

4、令牌桶算法

640?wx_fmt=png

令牌桶算法概念如下:

  • 令牌以固定速率生成;

  • 生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行;

  • 如果桶空了,那么尝试取令牌的请求会被直接丢弃。

令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法。

四、代码实现

作为如此重要的功能,在Java中自然有很多实现限流的类库,例如Google的开源项目guava提供了RateLimiter类,实现了单点的令牌桶限流。 

而分布式限流常用的则有Hystrix、resilience4j、Sentinel等框架,但这些框架都需引入第三方的类库,对于国企等一些保守的企业,引入外部类库都需要经过层层审批,较为麻烦。 

分布式限流本质上是一个集群并发问题,而Redis作为一个应用广泛的中间件,又拥有单进程单线程的特性,天然可以解决分布式集群的并发问题。本文简单介绍一个通过Redis实现单次请求判断限流的功能。

1、脚本编写

经过上面的对比,最适合的限流算法就是令牌桶算法。而为实现限流算法,需要反复调用Redis查询与计算,一次限流判断需要多次请求较为耗时。因此我们采用编写Lua脚本运行的方式,将运算过程放在Redis端,使得对Redis进行一次请求就能完成限流的判断。 

令牌桶算法需要在Redis中存储桶的大小、当前令牌数量,并且实现每隔一段时间添加新的令牌。最简单的办法当然是每隔一段时间请求一次Redis,将存储的令牌数量递增。 

但实际上我们可以通过对限流两次请求之间的时间和令牌添加速度来计算得出上次请求之后到本次请求时,令牌桶应添加的令牌数量。因此我们在Redis中只需要存储上次请求的时间和令牌桶中的令牌数量,而桶的大小和令牌的添加速度可以通过参数传入实现动态修改。 

由于第一次运行脚本时默认令牌桶是满的,因此可以将数据的过期时间设置为令牌桶恢复到满所需的时间,及时释放资源。 

编写完成的Lua脚本如下:

local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token')

local last_time = ratelimit_info[1]

local current_token = tonumber(ratelimit_info[2])

local max_token = tonumber(ARGV[1])

local token_rate = tonumber(ARGV[2])

local current_time = tonumber(ARGV[3])

local reverse_time = 1000/token_rate

if current_token == nil then

  current_token = max_token

  last_time = current_time

else

  local past_time = current_time-last_time

  local reverse_token = math.floor(past_time/reverse_time)

  current_token = current_token+reverse_token

  last_time = reverse_time*reverse_token+last_time

  if current_token>max_token then

    current_token = max_token

  end

end

local result = 0

if(current_token>0) then

  result = 1

  current_token = current_token-1

end 

redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token)

redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time)))

return result

2、执行限流

这里使用Spring Data Redis来进行Redis脚本的调用。

编写Redis脚本类:

public class RedisReteLimitScript implements RedisScript<String> { 

   private static final String SCRIPT = 

      "local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token') local last_time = ratelimit_info[1] local current_token = tonumber(ratelimit_info[2]) local max_token = tonumber(ARGV[1]) local token_rate = tonumber(ARGV[2]) local current_time = tonumber(ARGV[3]) local reverse_time = 1000/token_rate if current_token == nil then current_token = max_token last_time = current_time else local past_time = current_time-last_time; local reverse_token = math.floor(past_time/reverse_time) current_token = current_token+reverse_token; last_time = reverse_time*reverse_token+last_time if current_token>max_token then current_token = max_token end end local result = '0' if(current_token>0) then result = '1' current_token = current_token-1 end redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_toke  redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_tokencurrent_token)+(current_time-last_time))) return result"; 

  @Override   public String getSha1() { 

    return DigestUtils.sha1Hex(SCRIPT); 

  } 

  @Override   public Class<String> getResultType() {     return String.class; 

  } 

  @Override   public String getScriptAsString() {     return SCRIPT; 

  } 

通过RedisTemplate对象执行脚本:

public boolean rateLimit(String key, int max, int rate) {

    List<String> keyList = new ArrayList<>(1);

    keyList.add(key);

    return "1".equals(stringRedisTemplate

        .execute(new RedisReteLimitScript(), keyList, Integer.toString(max), Integer.toString(rate),

            Long.toString(System.currentTimeMillis())));

  } 

rateLimit方法传入的key为限流接口的ID,max为令牌桶的最大大小,rate为每秒钟恢复的令牌数量,返回的boolean即为此次请求是否通过了限流。为了测试Redis脚本限流是否可以正常工作,我们编写一个单元测试进行测试看看。

@Autowired

  private RedisManager redisManager;

  @Test

  public void rateLimitTest() throws InterruptedException {

    String key = "test_rateLimit_key";

    int max = 10;  //令牌桶大小

    int rate = 10; //令牌每秒恢复速度

    AtomicInteger successCount = new AtomicInteger(0);

    Executor executor = Executors.newFixedThreadPool(10);

    CountDownLatch countDownLatch = new CountDownLatch(30);

    for (int i = 0; i < 30; i++) {

      executor.execute(() -> {

        boolean isAllow = redisManager.rateLimit(key, max, rate);

        if (isAllow) {

          successCount.addAndGet(1);

        }

        log.info(Boolean.toString(isAllow));

        countDownLatch.countDown();

      });

    }

    countDownLatch.await();

    log.info("请求成功{}次", successCount.get());

  }

设置令牌桶大小为10,令牌桶每秒恢复10个,启动10个线程在短时间内进行30次请求,并输出每次限流查询的结果。日志输出:

[19:12:50,283]true 

[19:12:50,284]true 

[19:12:50,284]true 

[19:12:50,291]true 

[19:12:50,291]true 

[19:12:50,291]true 

[19:12:50,297]true 

[19:12:50,297]true 

[19:12:50,298]true 

[19:12:50,305]true 

[19:12:50,305]false 

[19:12:50,305]true 

[19:12:50,312]false 

[19:12:50,312]false 

[19:12:50,312]false 

[19:12:50,319]false 

[19:12:50,319]false 

[19:12:50,319]false 

[19:12:50,325]false 

[19:12:50,325]false 

[19:12:50,326]false 

[19:12:50,380]false 

[19:12:50,380]false 

[19:12:50,380]false 

[19:12:50,387]false 

[19:12:50,387]false 

[19:12:50,387]false 

[19:12:50,392]false 

[19:12:50,392]false 

[19:12:50,392]false 

[19:12:50,393]请求成功11次

可以看到,在0.1秒内请求的30次请求中,除了初始的10个令牌以及随时间恢复的1个令牌外,剩下19个没有取得令牌的请求均返回了false,限流脚本正确的将超过限制的请求给判断出来了,业务中此时就可以直接返回系统繁忙或接口请求太过频繁等提示。

3、开发中遇到的问题

1)Lua变量格式

Lua中的String和Number需要通过tonumber()和tostring()进行转换。

2)Redis入参

Redis的pexpire等命令不支持小数,但Lua的Number类型可以存放小数,因此Number类型传递给 Redis时最好通过math.ceil()等方式转换以避免存在小数导致命令失败。

3)Time命令

由于Redis在集群下是通过复制脚本及参数到所有节点上,因此无法在具有不确定性的命令后面执行写入命令,因此只能请求时传入时间而无法使用Redis的Time命令获取时间。

3.2版本之后的Redis脚本支持redis.replicate_commands(),可以改为使用Time命令获取当前时间。

4)潜在的隐患

由于此Lua脚本是通过请求时传入的时间做计算,因此务必保证分布式节点上获取的时间同步,如果时间不同步会导致限流无法正常运作。

作者:段然,甜橙金融创新中心开发工程师,目前负责公司平台化建设及媒介能力聚合。

想知道更多?描下面的二维码关注我

640?wx_fmt=png


加技术群入口(备注:技术):>>>Learn More<<

免费资料入口(备注:1024):>>>Learn More<<

免费星球入口:>>>Free<<<

内推通道>>>>


今天开始到9月7日,当当开学季促销,满600满300,用我的优惠码还可以减50,相当于250买600的书,支持全品类。结算的时候用优惠码 TMWCP4 即可。

点个"在看"呗^_^

这篇关于分布式服务限流实战,已经为你排好坑了 | 总结的很全面的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1129714

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

git使用的说明总结

Git使用说明 下载安装(下载地址) macOS: Git - Downloading macOS Windows: Git - Downloading Windows Linux/Unix: Git (git-scm.com) 创建新仓库 本地创建新仓库:创建新文件夹,进入文件夹目录,执行指令 git init ,用以创建新的git 克隆仓库 执行指令用以创建一个本地仓库的

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

二分最大匹配总结

HDU 2444  黑白染色 ,二分图判定 const int maxn = 208 ;vector<int> g[maxn] ;int n ;bool vis[maxn] ;int match[maxn] ;;int color[maxn] ;int setcolor(int u , int c){color[u] = c ;for(vector<int>::iter

整数Hash散列总结

方法:    step1  :线性探测  step2 散列   当 h(k)位置已经存储有元素的时候,依次探查(h(k)+i) mod S, i=1,2,3…,直到找到空的存储单元为止。其中,S为 数组长度。 HDU 1496   a*x1^2+b*x2^2+c*x3^2+d*x4^2=0 。 x在 [-100,100] 解的个数  const int MaxN = 3000

状态dp总结

zoj 3631  N 个数中选若干数和(只能选一次)<=M 的最大值 const int Max_N = 38 ;int a[1<<16] , b[1<<16] , x[Max_N] , e[Max_N] ;void GetNum(int g[] , int n , int s[] , int &m){ int i , j , t ;m = 0 ;for(i = 0 ;