五分钟搞懂分布式流控算法

news2024/11/17 11:32:44

流控是任何一个复杂系统都必须考虑的问题,本文介绍并比较了不同的流控算法,从而帮助我们可以基于系统需求和架构选择合适的方案。原文:Distributed Rate-Limiting Algorithms[1]

当我们设计分布式流控系统(distributed rate-limiting system)时,需要用到哪些工具和算法?

Joshua Hoehne @Unsplash
Joshua Hoehne @Unsplash

Criteo是全球最大的广告技术公司之一,随着广告市场的不断发展,Criteo在过去几年里一直致力于改进API,帮助客户更好的通过可编程接口访问需要的服务。

随着越来越多的客户使用新的API,很明显,需要实现某种流量控制,以确保所有客户端都能平等访问资源,并保护API免受(恶意或错误的)频繁调用。

流控似乎很简单: 只允许给定的客户端每分钟执行X个调用。在单个服务器实例上实现流控非常容易,可以很容易找到相关的库来实现。但问题是我们的API托管在6个数据中心(欧洲、北美和亚洲),每个数据中心都有多个实例,这意味着我们需要某种分布式流控系统。

流控不仅与调用次数有关,还需要和客户端同步当前被限制的状态(例如,使用专用的报头和状态码)。但是本文将主要关注用于流控的算法和系统。


利用负载均衡

在尝试开发自己的系统之前,更重要的是查看现有的基础设施是否能够提供想要的特性。

那么,部署在数据中心所有实例之前,并且已经在负责检查、路由流量的是什么?负载均衡器。大多数负载均衡器都提供了流控特性或某种可用于实现流控的抽象。例如,HAProxy有现成的可用于设置流控的stick tables[2],可以在实例之间同步状态,并且工作得很好。

不幸的是,负载均衡不支持我们需要的某些特性(动态限制、令牌自省token introspection、……),因此我们需要自己实现这些特定的需求。


初级方案

会话粘连(Sticky sessions)

说到负载均衡,如果给定客户端的负载并不均衡,并且总是与单个实例交互🤓,那么就不需要分布式流控系统。大多数客户端访问距离最近的数据中心(通过geo-DNS),如果在负载均衡器上启用“stickiness”,客户端应该总是访问相同的实例,这种情况下我们可以使用简单的“本地”速率限制。

这在理论上可行,但在实践中行不通。Criteo系统面临的负载不是恒定的,例如,黑色星期五/Cyber Week是一年中最重要的时段。在此期间,团队随时处于戒备状态,准备扩大基础设施,以应对客户不断增长的需求。但是会话粘连和可伸缩性不能很好的配合(如果所有客户端都粘连在旧实例上,那么创建新实例又有什么用呢?)

使用更智能的会话粘连(在扩展时重新分配令牌)会有所帮助,但这意味着每次扩展时,客户端都可能切换到另一个实例上,而且并不知道客户端在前一个实例上执行了多少调用。本质上说,这将使我们的流控在每次伸缩时不一致,客户端可能在每次系统面临压力时会进行更多调用。

Chatty服务器

如果客户端可以访问任何实例,意味着“调用计数”必须在实例之间共享。一种方案是让每个实例调用所有其他实例,请求给定客户端的当前计数并相加。另一种方案反过来,每个服务器向其他服务器广播“计数更新”。

这会造成两个主要问题:

  • 实例越多,需要进行的调用就越多。
  • 每个实例都需要知道其他实例的地址,并且每次服务扩缩容时都必须更新地址。

虽然可以实现这个解决方案(本质上是一个点对点环,许多系统已经实现了),但成本较高。

Kafka

如果不想让每个实例都和其他实例通信,可以利用Kafka同步所有实例中的计数器。

例如,当某个实例被调用时,就将一个事件推到对应的topic。这些事件会被滑动窗口聚合(Kafka Stream在这方面做得很好),每个客户端最后一分钟的最新计数会被发布到另一个topic上。然后,每个实例通过此topic获得所有客户端的共享计数。

问题在于Kafka本质上是异步的,虽然通常延迟很小,但当API负载增加时,也会增加延迟。如果实例使用了过时的计数器,那么可能会漏过那些本应被阻止的调用。


这些解决方案都有一个共同点: 当一切正常时,可以很好的工作,但当负载过重时,就会退化。我们的大部分系统都是这样设计的,通常情况下没有问题,但流控并不是典型组件,其目标就是保护系统的其他部分免受这种过重负载的影响。

流控系统的目标是在系统负载较重时工作良好,其构建目标是为最差的1%而不是好的99%的情况服务。


分布式算法

我们需要一个中心化的同步存储系统,以及为每个客户端计算当前速率的算法。内存缓存(如Memcached或Redis)是理想的系统,但并不是所有的流控算法都可以在缓存系统中实现。下面我们看看有什么样的算法。

简化起见,我们考虑尝试实现“每分钟100次调用”的流控。

看看有哪些工具可用。

Barn Images @Unsplash
Barn Images @Unsplash
基于事件日志的滑动窗口(Sliding window via event log)

如果想知道某个客户端在过去一分钟内进行了多少次调用,可以在缓存中为每个客户端存储一个时间戳列表。每次调用时,相应的时间戳都会添加到列表中。然后循环遍历列表中的每一项,丢弃超过一分钟的旧项,并计算新项。

👍优点:

  • 非常精确
  • 简单

👎缺点:

  • 需要强大的事务支持(处理同一客户端的两个实例需要更新相同的列表)。
  • 根据不同的调用限制和次数,存储对象(列表)的大小可能相当大。
  • 性能不稳定(更多的调用意味着需要处理更多的时间戳)。
固定窗口(Fixed window)

大多数分布式缓存系统都有特定的、高性能的“计数器”抽象(一个可以自动增加的整数值,附加在一个字符串键上)。

以“{clientId}”为key为每个客户端维护一个计数器非常容易,但只会计算自计数器创建以来客户端调用的次数,而不是最后一分钟的次数。以“{clientId}_{yyyyMMddHHmm}”为key可以每分钟都为客户端维护一个计数器(换句话说: 以1分钟为固定窗口),查找与当前时间相对应的计数器可以告诉我们这一分钟客户端执行的调用数量,如果这个值超过上限,就可以阻止调用。

请注意,这与“最近一分钟”不是一回事。如果在上午07:10:23有一次调用,固定窗口计数器会显示在上午07:10:00到07:10:23之间调用的数量。但我们真正想要的是早上07:09:23到07:10:23之间的调用数量。

在某种程度上,固定窗口算法每过一分钟都会“忘记”之前有多少次呼叫,因此客户端理论上可以在07:09:59执行100次调用,然后在07:10:00执行100次额外的调用。

👍优点:

  • 非常快(单个原子增量+读取操作)
  • 只需要非常基本的事务支持(原子计数器)
  • 性能稳定
  • 简单

👎缺点:

  • 不准确(最多会允许2倍调用)
令牌桶(Token bucket)

回到1994年,父母把你送到游戏厅和朋友们一起玩《超级街霸2》。他们给你一个装了5美元硬币的小桶,然后去了街对面的酒吧,并且每个小时都会过来往桶里扔5美元硬币。因此你基本上被限制每小时玩5美元(希望你在《街头霸王》中表现出色)。

这就是“令牌桶”算法背后的主要思想: 与简单计数器不同,“桶”存储在每个客户端缓存中。桶是由两个属性组成的对象:

  • 剩余“令牌”的数量(或剩余可以进行的调用的数量)
  • 最后一次调用的时间戳。

当API被调用时,检索桶,根据当前调用和最后一次调用之间的时间间隔,向桶中添加新的令牌,如果有多余令牌,递减并允许调用。

所以,和“街头霸王”的例子相反,没有“父母”帮我们每分钟重新装满桶。桶在与令牌消耗相同的操作中被有效的重新填充(令牌的数量对应于上次调用之后的时间间隔)。如果最后一次调用是在半分钟之前,那么每分钟100次调用的限制意味着将向桶中添加50个令牌。如果桶太“旧”(最后一次调用超过1分钟),令牌计数将重置为100。

事实上,可以在初始化的时候填充超过100个令牌(但补充速度为100令牌/分钟): 这类似于“burst”功能,允许客户端在一小段时间内超过流控的限制,但不能长期维持。

注意: 正确计算要添加的令牌数很重要,否则有可能错误的填充桶。

该算法提供了完美的准确性,同时提供了稳定的性能,主要问题是需要事务(不希望两个实例同时更新缓存对象)。

100次调用/分钟的令牌桶的分步示例
100次调用/分钟的令牌桶的分步示例

👍优点:

  • 非常精确
  • 快速
  • 性能稳定
  • 优化初始令牌数量可以允许客户端“burst”调用

👎缺点:

  • 更复杂
  • 需要强大的事务支持

漏桶(Leaky bucket): 该算法的另一个版本。在这个版本中,调用堆积在bucket中,并以恒定的速率(匹配速率限制)处理。如果桶溢出,则拒绝调用。这实现起来比较复杂,但可以平滑服务负载(这可能是您想要的,也可能不是)。

🏆最好的算法?

比较这三种算法,令牌桶似乎在性能和准确性方面提供了最好的折衷。但只有当系统提供良好的事务支持时,才有可能实现。如果有Redis集群,这是完美方案(甚至可以实现基于Lua的算法,直接运行在Redis集群上,以提高性能),但Memcached只支持原子计数器,而不是事务。

可以基于Memcached实现令牌桶的乐观并发(optimistic concurrent)版本[3],但这更加复杂,而且乐观并发的性能在负载较重的情况下会下降。


用固定窗口近似模拟滑动窗口

如果没有强大的事务支持,是否注定要使用不准确的固定窗口算法?

算是吧,但还有改进的空间。请记住,固定窗口的主要问题是它每过一分钟都会“忘记”之前发生的事情,但我们仍然可以访问相关信息(在前一分钟的计数器中),所以可以通过计算加权平均值来估计前一分钟的调用次数。

通过60s固定窗口组合近似模拟60s滑动窗口
通过60s固定窗口组合近似模拟60s滑动窗口

例如: 如果在00:01:43进行了一次调用,递增得到“00:01”计数器的值。由于这是当前的日历分钟,现在包含00:01:00至00:01:43之间的调用数(最后17秒还没有发生)。
但我们想要的是60s滑动窗口中的调用数,意味着我们错过了00:00:43到00:01:00这段时间的计数。为此我们可以读取“00:00”计数器,并以17/60的因子进行调整,从而说明我们只对最后17秒感兴趣。

如果负载不变,这一近似是完美的。但如果大多数调用都集中在前一分钟,那就会获得一个高估的值。而当大多数调用都集中在前一分钟结束后,这个数字就会被低估。

比较

为了更准确的了解精度差异,最好是在相同的条件下模拟两种算法。

下面的图显示了“固定计数器”算法在输入随机流量时将返回什么。灰色的线是一个“完美”的滑动窗口输出,该窗口在任何时间点对应于过去60秒内的呼叫次数,这是我们的目标。橙色虚线表示固定窗口算法对相同流量的“计数”。

alt

它们在第一分钟的输出是相同的,但很快就可以看到固定窗口版本在每分钟标记处有很大的下降。固定窗口算法很少会超过100个调用的限制,这意味着会允许很多本应被阻止的调用。

下面的图表示相同的场景,具有相同的流量,但使用了近似的滑动窗口。同样,灰色线代表“完美”滑动窗口。橙色虚线表示近似算法。

alt

在每分钟标记附近不再看到下降,可以看到新版本算法与完美算法更接近,它有时略高,有时略低,但总体上是巨大的进步。

收益递减

但我们能做得更好吗?

我们的近似算法只使用当前和以前的60秒固定窗口。但是,也可以使用几个更小的子窗口,一种极端方法是使用60个1s窗口来重建最后一分钟的流量。显然这意味着为每个调用读取60个计数器,这将极大增加性能成本。不过我们可以选择任意固定窗口时间,从中拟合近似值。窗口越小,需要的计数器就越多,近似值也就越精确。

alt

我们看看组合5个15秒窗口会有什么效果:

alt

正如预期的那样,准确率有所提高,但仍然不够完美。

我们处在一个经典的更好的准确性=更差的性能的情况下。没有绝对的最佳方案,必须平衡对于准确性和性能的要求,找到最适合的解决方案。如果你只关心保护自己的服务不被过度使用,而不需要持续控制,那么甚至最简单的固定窗口就可能是可行的解决方案。


结论

流控是一种非常容易描述但却隐藏了很多复杂性的特性。希望本文能够帮助你理解在复杂分布式系统中实现流控所涉及的工具和算法。

References:
[1] Distributed Rate-Limiting Algorithms: https://medium.com/criteo-engineering/distributed-rate-limiting-algorithms-a35f7e24783
[2] Introduction to HAProxy stick tables: https://www.haproxy.com/blog/introduction-to-haproxy-stick-tables/
[3] Optimistic currency control: https://en.wikipedia.org/wiki/Optimistic_concurrency_control

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind

- END -

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/642143.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华硕TUF GAMING B460M i5-10500 电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网,转载需注明出处。(下载请直接百度黑果魏叔) 硬件配置 硬件型号驱动情况 主板华硕TUF GAMING B460M-PLUS (LPC Controller B460芯片组 处理器特尔 Core i5-10500 3.10GHz六核已驱动 内存16 GB (IstRI DDR4 2666MH…

在字节划水的4年,很真实...

先简单交代一下,我是某不知名211的计算机本硕,18年毕业加入滴滴,之后跳槽到了头条,一直从事测试开发相关的工作。之前没有实习经历,算是四年半的工作经验吧。 这四年半之间完成了一次晋升,换了一家公司&am…

微信可以聚合聊天吗?如何同时管理多个微信?

现在很多企业用微信做私域流量运营,在里面搭建自己的私域流量。随着客户资源逐渐增多,需求增加,不仅需要联系客户,还要联系各大代理,开通多个微信号无疑是最佳方案。 但是不少做电商的朋友表示,微信号越来…

代理ip数据采集的优缺点

随着互联网时代的到来,数据已经成为企业发展和决策的关键。但是,不同的网站它对于数据访问的限制和反爬虫措施却是给企业的数据采集带来了挑战。针对这一问题,代理IP数据采集技术应运而生。但是使用代理ip来进行数据采集也有优缺点。 一、代理…

JS WEB框架Express日志模块winston和express-winston以及winston-daily-rotate-file优化

1.前言 1.Express的日志模块winston和express-winston已经提供了开箱即用的大多数功能,但是和其他语言相比,还缺失对日志记录的当前文件和行号的支持,需要自己实现,以此记录一下。 2.express-winston主要用于记录请求进入和结束时…

【运筹优化】ALNS自适应大领域搜索算法求解TSP问题 + Java代码实现

文章目录 一、TSP问题简介二、数学建模三、实现细节四、案例实战4.1 测试案例说明4.2 Java 完整代码4.2.1 TSP_Instance 实例类4.2.2 TSP_Solution 结果类4.2.3 TSP_Util 工具类4.2.4 TSP_Solver_ALNS 算法类4.2.5 RunAndPlot 运行类 4.3 运行结果展示 一、TSP问题简介 旅行推…

MySQL登录时报错:ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘解决办法

问题描述 在云服务器使用 docker安装的Mysql5.7数据库,刚开始的时候使用正常,后面突然有一天就连接不上了,报错为: ERROR 1045 (28000): Access denied for user root1xxx(using password:YES), 当登录MySQL数据库出现 Error 1045…

python套接字(三):结合pyside2实现多人聊天室

文章目录 前言一、准备1、安装pyside22、设计界面 二、代码实现1、服务器端2、客户端 三、运行 前言 上一章python套接字(二):实现一个服务器和多客户端连接,大概实现了多人聊天室功能,但是比较简陋,本篇内容将结合pyside2做一个…

车间主任、班组长必读:生产车间的现场管理

与工厂车间操作层(一线员工)接触最多的基层管理者,即我们通常所说的班组长、车间主任等,他们是将企业战略规划落实到具体工作当中的终端管理者。 一线班组长的“角色” 1、责任者 对企业来说,班组长是基层的治理员&am…

MySQL高级篇第二天

文章目录 一、Mysql的体系结构概览 二、 存储引擎 三、优化SQL步骤 一、Mysql的体系结构概览 整个MySQL Server由以下组成 Connection Pool : 连接池组件 Management Services & Utilities : 管理服务和工具组件 SQL Interface : SQL接口组件 Parser : 查询分析器组件 O…

游戏测试与一般的软件测试的区别在哪里?

有很多同学进入测试行业之后,一直从事的是软件测试的工作,然后跳槽时遇到一些游戏的公司的面试,就会有点慌,我做的都是软件测试,能胜任游戏测试么? 所以,今天我们需要先来了解一下,…

科技政策 | 《深圳市加快加快推动人工智能高质量发展高水平应用行动方案(2023—2024年)》发布

原创 | 文 BFT机器人 导语 Introduction 近日,深圳市发布了《深圳市加快推动人工智能高质量发展高水平应用行动方案(2023-2024年)》旨在以更大热情拥抱创新,打造最好生态,推动人工智能高质量发展和全方位各领域高水平…

C语言-关键字

关键字就是c语言已经定义好的名字,直接可以拿过来使用,不需要再次定义 1 数据类型相关的关键字 用于定义变量或者类型 定义变量的语法结构: 类型 变量名; 拓展:变量名属于标识符,标识符(变量…

关于Axios发请求(get或post)的参数问题

版本说明: {"name": "wx_vue_3.0","version": "0.1.0","private": true,"scripts": {"serve": "vue-cli-service serve","build": "vue-cli-service build"…

K-Means聚类算法

引言 聚类算法是传统机器学习算法中比较重要的一个算法,也是工程项目当中一个比较常用的算法。 一. 分类与聚类 分类 分类其实是从特定的数据中挖掘模式,作出判断的过程。 分类学习主要过程: (1)训练数据集存在一个类…

mathtype公式右编号对齐

mathtype公式右编号对齐 1.选中文中编辑好的公式,复制 2.mathtype里的点击右编号,将上面复制的公式粘贴到新出现的框内 3.编号设置

聚类效果评估

目录 1.轮廓系数(Silhouette Coefficient) 1.1 为什么轮廓系数可以评价聚类效果的好坏? 1.2 平均轮廓系数 2. 其他聚类质量函数 2.1方差比准则(Variance Ratio Criterion, VRC) 2.2 戴维斯-博尔丁指数(Davies-Bouldin指数,DB指数) 评价聚…

linux- 定时任务清理日志

定时任务清理日志 一、查找并删除文件1.1 查找文件1.2 查找并删除 二、计划任务:2.1 创建shell脚本,并分配权限2.2 编辑shell脚本2.3 计划任务 linux是一个很能自动产生文件的系统,在实际部署运行中,发现日志文件会占用大量内存&a…

SpringBoot+Thymeleaf 后端转html,pdf HTML生成PDF SpringBoot生成PDF Java PDF生成

SpringBoot 生成PDF Thymeleaf企业级真实应用:将HTML界面数据转换为PDF输出 参考: https://blog.51cto.com/u_13146445/6190475 https://blog.csdn.net/qq_27242695/article/details/115654447 0. 需求 后端渲染pdf生成 (thymeleaf根据已有…

Android开发之数据传递的桥梁——Bundle

解释 在安卓sdk源码中,Bundle类的说明是这样的 A mapping from String keys to various Parcelable values. See Also: PersistableBundle public final class Bundle extends BaseBundle implements Cloneable, Parcelable 字符串的键到持久化值的映射。 作用 …