【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

分布式服务限流实战,已经为你排好坑了

  • 2019-08-16
  • 本文字数:4712 字

    阅读完需:约 15 分钟

分布式服务限流实战,已经为你排好坑了

开头:本文由 dbaplus 社群授权转载

一、限流的作用

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


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


限流可以应对:


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

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

  • 恶意攻击请求。


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

二、为什么要分布式限流


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



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



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

三、限流的算法

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


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


  • 固定窗口计数器;

  • 滑动窗口计数器;

  • 漏桶;

  • 令牌桶。

1、固定窗口计数器算法


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


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

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

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


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


2、滑动窗口计数器算法


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


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

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

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

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


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

3、漏桶算法


漏桶算法概念如下:


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

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

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


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


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

4、令牌桶算法


令牌桶算法概念如下:


  • 令牌以固定速率生成;

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

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


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

四、代码实现

作为如此重要的功能,在 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_rateif current_token == nil then  current_token = max_token  last_time = current_timeelse  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  endendlocal result = 0if(current_token>0) then  result = 1  current_token = current_token-1endredis.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、执行限流

这里使用 SpringDataRedis 来进行 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 脚本是通过请求时传入的时间做计算,因此务必保证分布式节点上获取的时间同步,如果时间不同步会导致限流无法正常运作。


作者介绍


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


原文链接


https://mp.weixin.qq.com/s/qb3rg_ZpcMcvyaIRsvc1fw


2019-08-16 08:0040444
用户头像
dbaplus社群 数据连接未来

发布了 176 篇内容, 共 70.0 次阅读, 收获喜欢 598 次。

关注

评论 9 条评论

发布
用户头像
集群中不同机器的时间戳有差异,会不会有问题?
2023-03-30 16:12 · 广东
回复
用户头像
redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time)))
return result
剩余存活时间直接用剩余令牌生成时间不行吗,为什么要加(current_time-last_time)))这段
2021-05-26 00:39
回复
用户头像
经过测试发现current_token能减为负数,不是说redis的lua脚本是原子性的吗,有哪位大神能解答一下
2021-03-04 09:38
回复
用户头像
lua脚本中1000/token_rate是什么意思
2019-11-11 15:45
回复
不好意思当时理解错了,入参单位是秒,lua脚本单位是毫秒,故需要转换下。
2019-11-11 15:59
回复
带入下面的公式,local reverse_token = math.floor(past_time/reverse_time),懂了吗
2020-08-02 19:49
回复
没错,这个是配合后面公式使用的 local reverse_token = math.floor(past_time/reverse_time)
2020-08-02 19:51
回复
查看更多回复
没有更多了
发现更多内容

Media Encoder 2024 for mac(ame媒体转码器) v24.0.2中文激活版

mac

苹果mac Windows软件 媒体转码软件 Media Encoder 2024 ME2024

Sermant框架下的服务治理插件快速开发及使用指南

华为云开源

开源 云原生 微服务治理 sermant

从技术到人文,找打理想团队,互联网众包平台祝您软件开发无忧虑

知者如C

Amazon Bedrock 划算吗?Bedrock 大模型服务定价与分析

魏临

硬件开发少走弯路,来华秋这场研讨会提升技能

华秋电子

工程师

华润啤酒走进用友,数智化赋能酒业新世界

用友BIP

SATA硬件驱动器接口的可制造性问题详解

华秋电子

SATA

用友BIP,助力轴承行业数智化转型

用友BIP

数智化转型

如何配置支付宝密钥之如何配置证书|保姆级教学(二)

盐焗代码虾

证书 支付宝 经验分享 密钥

推送效率低?MobPush带着APP消息推送一站式解决方案来了

MobTech袤博科技

大数据

iOS应用加固方案解析:ipa加固安全技术全面评测

服务器删除文件后磁盘空间没有立刻释放问题

javaNice

Java Linux

WorkPlus定制化的沟通协作平台,助您实现企业级完全掌控

WorkPlus

AntDB-M高性能设计之hash索引动态rehash

亚信AntDB数据库

数据库 AntDB 亚信科技AIDB数据库 AntDB数据库

Go 接口:Go中最强大的魔法,接口应用模式或惯例介绍

不在线第一只蜗牛

Go 接口 编程语言

ios安全加固 ios 加固方案

如何构建新一代实时湖仓?袋鼠云基于数据湖的探索升级之路

袋鼠云数栈

数据仓库 数据湖 湖仓一体 大数据仓库 实时湖仓

币币合约交易系统搭建(秒合约交易平台开发规则解析)

V\TG【ch3nguang】

矩阵起源加入 OpenCloudOS 操作系统开源社区,完成技术兼容互认证

MatrixOrigin

数据库 分布式 云原生 MatrixOrigin MatrixOne

城市:天际线Cities: Skylines for Mac(模拟建造游戏)+dlc

影影绰绰一往直前

解读 Swagger enum:完整示例教程

Liam

前端 swagger Enum API 文档 #程序员

文明6 for mac 1.4.5 最新DLC 越南和忽必烈包 文明6 mac破解版

晴雯哥

Mac电脑上最受欢迎的SSH工具,让你轻松远程控制服务器!

影影绰绰一往直前

SSH工具 终端SSH仿真工具

WorkPlus安全专属移动数字化航空母舰,助力企业掌控业务和生态

WorkPlus

iOS移动应用安全加固:保护您的App免受恶意攻击的重要步骤

雪奈椰子

IBM SPSS Statistics 26 (spss统计软件)中文特别版下载

影影绰绰一往直前

IBM SPSS Statistics中文 IBM SPSS Statistics破解 IBM SPSS Statistics下载 IBM SPSS Statistics 26

武创院区块链产业研究所:基于鲲鹏DevKit开发区块链政务服务协同数据治理平台,数据管理更放心

彭飞

继电器的工作原理与应用案例

华秋电子

继电器

Flink 替换 Logstash 解决日志收集丢失问题

字节跳动云原生计算

大数据 flink 云原生 Logstash

超级APP,All in one APP

WorkPlus

合约交易所开发之永续合约交易所系统开发规则

V\TG【ch3nguang】

分布式服务限流实战,已经为你排好坑了_技术管理_dbaplus社群_InfoQ精选文章