设计一个限速器

限速器 (Rate Limiter) 相信大家都不会陌生,在网络系统中,限速器可以控制客户端发送流量的速度,比如 TCP, QUIC 等协议。而在 HTTP 的世界中, 限速器可以限制客户端在一段时间内发送请求的次数,如果超过设定的阈值,多余的请求就会被丢弃。

生活中也有很多这样的例子,比如

• 用户一分钟最多能发 5 条微博• 用户一天最多能投 3 次票• 用户一小时登录超过5次后,需要等待一段时间才能重试。

5e893d3a5f0074a23489b130b2060b34.png

限速器(Rate Limiter)有很多好处,可以防止一定程度的 Dos 攻击,也可以屏蔽掉一些网络爬虫的请求,避免系统资源被耗尽,导致服务不可用。

   设计要求   

让我们从一个面试开始吧!

f3a3af838af3aa6a5da7cace1168cd03.png

面试官:你好,我想考察一下你的设计能力,如果让你设计一个限速器 (Rate Limiter),你会怎么做?

面试者:我们需要什么样的限速器?它是在客户端限速还是在服务端限速?

面试官:这个问题很好,没有固定要求,取决于你的设计。

面试者:我想了解一下限速的规则,我们是根据 IP、UserId,或者手机号码进行限速吗?

面试官:这个不固定,限速器应该是灵活的,要能很方便的支持不同的规则。

面试者:如果请求被限制了,需要提示给用户吗?

面试官:需要提示,要给用户提供良好的体验。

面试者:好吧,那系统的规模是多大的?是单机还是分布式场景?

面试官:我们是 TOC 的产品, 系统流量很大,并且是分布式的环境, 所以限速器要支持海量请求。

面试者:(小声嘀咕)你这是造火箭呢?

我们总结一下限速器的设计要求:

• 低延迟,性能要好• 需要适用于分布式场景。• 用户的请求受到限制时,需要提示具体的原因。• 高容错,如果限速器故障,不应该影响整个系统。


   限速器应该放在哪里?   

从系统整体的角度上来看,我们的限速器应该放在哪里?通常有三种选择,如下

客户端

是的,我们可以在客户端设置限速器。但是有个问题是,我们都知道在 Web 前端做一些限制实际上是不安全的,同样客户端也是一样的, 限速客户端可以做,但是远远不够。

服务端

在服务端设置限速器是很常见且安全的,如下

84e8ab85a388872b33c367ffdb8490d9.png

中间件

还有一种做法是,我们可以提供一个单独的限速中间件,如下

b4aae799c8167f48ed76c9b6517dc644.png

假如限速器设置了每秒最大允许2个请求,那么客户端发出的多余的请求就会被拒绝,并返回 HTTP 状态码 429, 表示用户发送了太多的请求。

94b15c69726a36fa68b4fc57a14796dd.png

实际上,很多网关都有限速的实现,包括认证、IP 白名单功能。

限速器应该放在哪里?没有固定的答案,它取决于公司的技术栈,系统规模。


   限速算法   

实际上,我们可以用算法实现限速器,下面是几种经典的限速算法,每种算法都有自己的优点和缺点,了解这些可以帮助我们选择更适合的算法。

• 令牌桶 (Token bucket)• 漏桶(Leaking bucket)• 固定窗口计数器(Fixed window counter)• 滑动窗口日志(Sliding window log)• 滑动窗口计数器(Sliding window counter)


   令牌桶算法   

令牌桶算法是实现限速使用很广泛的算法,它很简单也很好理解。

令牌桶是固定容量的容器。

一方面,按照一定的速率,向桶中添加令牌,桶装满后,多余的令牌就会被丢弃。

如下图,桶的容量为4,每次填充2个令牌。

1515780183fa9b6da3f3f1316ed12864.png

另一方面,一个请求消耗一个令牌,如果桶中没有令牌了,则拒绝请求。直到下个时间段,继续向桶中填充新的令牌。

78c14af4f364a9642cc0710d44287cd2.png

令牌桶算法有两个重要的参数,分别是桶大小和填充率,另外有时候可能需要多个桶,比如多个 api 限速的规则是不一样的。

令牌桶算法的优点是简单易实现,并且允许短时间的流量并发。缺点是,在应对流量变化时,正确地调整桶大小和填充率,会比较有挑战性。


   漏桶算法   

漏桶算法和令牌桶算法是类似的,同样有一个固定容量的桶。

一方面,当一个请求进来时,会被填充到桶里,如果桶满了,就拒绝这个请求。

另一方面,想象桶下面有一个漏洞,桶里的元素以固定的速率流出。

通常可以用先入先出的队列实现,如下图

996314d799e221fe02dcd027f92f955c.png

漏桶算法有两个参数,分别是桶大小和流出率,优点是使用队列易实现,缺点是,面对突发流量时,虽然有的请求已经推到队列中了,但是由于消费的速率是固定的,存在效率问题。


   固定窗口计数器算法   

固定窗口计数器算法的工作原理是,把时间划分成固定大小的时间窗口,每个窗口分配一个计数器,接收到一个请求,计数器就加一,一旦计数器达到设定的阈值,新的请求就会被丢弃,直到新的时间窗口,新的计数器。

让我们通过下面的例子,来看看它是如何工作的。一个时间窗口是1秒,每秒最多允许3个请求,超出的请求就会被丢弃。

f30e04a36203ca11792d642eac45ab7a.png

不过这个算法有一个问题是,如果在时间窗口的边缘出现突发流量时,可能会导致通过的请求数超过阈值,什么意思呢?我们看看下面的情况

06c8ab714f9f162b5059d42af6d52fa6.png

一个时间窗口是1分钟,每分钟最多允许5个请求。如果前一个时间窗口的后半段有5个请求,后一个时间窗口的前半段有5个请求,也就是 2:00:30 到 2:01:30 的一分钟内,是可以通过10个请求的,这明显超过了我们设置的阈值。

固定窗口计数器的优点是,简单易于理解,缺点是,时间窗口的边缘应对流量高峰时,可能会让通过的请求数超过阈值。


   滑动窗口日志算法   

我们上面看到了,固定窗口计数器算法有一个问题,在窗口边缘可能会突破限制,而滑动窗口日志算法可以解决这个问题。

它的工作原理是,假如设定1分钟内最多允许2个请求,每个请求都需要记录请求时间,比如保存在 Redis 的 sorted sets 中,保存之后还需要删除掉过时的日志,过时日志是如何定义的?比如某次请求的时间是 1:01:36,那么往前推1分钟,1:00:36 之前的日志都算过时的,需要从集合中删掉。同时,判断集合中的数量是否大于阈值,如果大于2则丢弃请求,如果小于或等于2,则处理这个请求。

让我们看看下面的例子

8154e87323937d0a43adeb47c00ddb4b.png


1.在 1:00:01 来了一个请求,第一步,记录请求时间到日志中,第二步,判断是否有过时的日志, 也就是 0:59:01 之前的日志,明显没有,第三步判断日志中的数量,没有大于2,处理这次请求。

2.在 1:00:30 来了一个请求,执行上面的三个步骤,最后处理这次请求。

3.在 1:00:50 的新请求,没有过时的日志,然后发现日志的数量为3,拒绝这次请求。

4.在 1:01:40 的新请求,清除2条过时的日志,也就是 1:00:40 之前的日志,此时,日志中的数量为2,处理这次请求。

这个算法实现的限速非常准确,但是它可能会消耗较多的内存。


   滑动窗口计数器算法   

滑动窗口计数器可以说是固定窗口计数器的升级版,上面提到了,固定窗口计数器存在窗口边缘可能会有超出限制的情况,如下

4a182bc32d94be7f652e8c63efa1d47c.png

而滑动窗口把固定的窗口又分成了很多小的窗口单位,比如下图,每个固定窗口的大小为1分钟,又拆分成了6份,每次移动一个小的单位,保证总和不超过阈值。

43a1dbe7f785670dea0a39a290bb5ee1.png

这样就可以避免上面的窗口边缘超出限制的问题。

ff9b9f7562416af2588c2a9192995607.png


   使用 Redis 实现高效计数器   

限速器算法的思想其实很简单,我们需要使用计数器记录用户的请求,如果超过阈值,服务这个请求,否则,拒绝这个请求。

一个很重要的问题是,我们应该把计数器放在哪里?我们知道,磁盘速度比较慢,使用数据库明显是不太现实的方案,想要更快的话,可以使用内存缓存,最常见的就是 Redis,是的,我们可以使用 Redis 实现高效计数器,如下

55e34e0bc713103cebcc2a96440c094b.png


   规则引擎   

Lyft 是一个开源的限速组件,可以供我们参考,它通过 Yaml 配置文件实现灵活的限速规则,看下面的示例

9fd250a82f89048b3405f298be1c1fd8.png

这个配置表示系统每天只能发送 5 条营销信息。

dee94c82e24413ffd4a160a52531f3cd.png

这个配置表示 1分钟的登录次数不能超过 5 次。

可以看到,基于配置文件,声明式的限速规则是非常灵活的,我们可以把配置文件保存到磁盘中。


   限速提示   

当请求超过限制时,限速器会拒绝掉其他的请求,这样其实不够,为了更好的用户体验,我们需要返回友好的错误信息给用户,并提示。

首先,限速器拒绝请求后,可以返回 HTTP 状态码 429,表示请求过多。

其次,我们可以返回更详细的信息,比如,剩余请求次数、等待时间等。一种很常见的做法时,把这些信息放到 Http 响应的 Header 中返回,如下

• X-Ratelimit-Remaining:表示剩余次数 • X-Ratelimit-Limit:表示客户端每个时间窗口可以进行多少次调用 • X-Ratelimit-Retry-After:表示等待多长时间可以进行重试

看起来不错!让我们看看现在的架构设计

5be58bfce807878f7d0bce6aacad3860.png

首先,限速规则存储在磁盘上,因为要经常访问,可以添加到缓存中。当客户端向服务器发送请求时,会先发送到限速中间件。限速中间件从缓存中拉取限速规则,同时把请求数据写入到 Redis 的计数器,然后判断是否超出限制。如果没有超出限制,把请求转发给我们的后端服务器。如果超出了限制,第一种方案,丢弃多余的请求,返回 429,第二种方案,把多余的请求推送到消息队列中,后续再进行处理。使用哪种方案,取决于您的实际场景。


   分布式环境   

构建一个在单服务器运行的限速器是很简单的,但是在分布式环境中,支持多台服务器,那就完全是另外一回事了。

我们主要要考虑两个问题:

• 并发问题• 数据同步问题

并发问题,我们的限速器的工作原理是,当接收到新的请求时,从 Redis 中读取计数器 counter,然后做加一的操作,在高并发场景中,可能存在多个线程读到了旧的值,比如,两个线程同时读取到 counter 的值为3,然后都把 counter 改成了4,这样是有问题的,其实最终应该是 5。

有朋友说,我们可以用锁,但实际上,锁的效率是不高的。解决这个问题通常有两个方案,第一个是使用 Lua 脚本,第二个是使用 Redis 的 sorted sets 数据结构,具体的细节本文不做过多介绍。

数据同步问题,在流量比较大的情况下,一个限速器是难以支撑的,我们需要多个限速器。由于 Web 层通常是无状态的,客户端的请求会随机发送给不同的限速器,如下

f6a0cc1c110daff735b76783e7dd524d.png

这种情况下,如果没有数据同步,我们的限速器肯定是没办法正常工作的。

我们可以使用像 Redis 这样的集中式数据存储,如下

2e428ffd3f3fd383ecdce7f6c97a84ea.png


   性能优化   

当我们的系统是面向全球用户时,为了让各个地区的用户都能有一个不错的体验,通常会在不同的地区设置多个数据中心。另外,现在很多云服务商在全球各地都有边缘服务器,流量会自动路由到最近的边缘服务器,来减少网络的延迟。

25654e5302b322a891cea707b67acfa7.png

当然,存在多个数据中心时,可能还要考虑到数据的最终一致性。


   总结   

在本文中,我们讨论了不同的限速算法,以及它们有优缺点,算法包括:

• 令牌桶• 漏桶• 固定窗口计数器• 滑动窗口日志• 滑动窗口计数器

然后,我们讨论了分布式环境中的系统架构,并发问题和数据同步问题,和灵活配置的限速规则,最后你会发现,实现一个限速器,其实没有那么难!

希望对您有用!

  译:等天黑

  作者:Alex Xu

  来源:System Design Interview

7418f3b6e8ddfdbf3def2acd6b9ae830.png

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

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

相关文章

C语言新手的100个报错解法 已更新11个错误

学习目标 收藏文章报错可以过来查 [更新数据] 此文将会持续更新,收录错误信息,若本文没有收录记得联系我~ CSDN 1_bit 持续更新中… [发布日期:2020年11月16日 14:55:00] 更新: 暂无 C语言教程 C语言真的很难吗?那…

【遥感数字图像处理】实验:遥感图像显示与数据输入/输出(Erdas版)

一、实验平台:Erdas 9.1 二、实验内容:视窗功能简介、图形和图像显示操作、实用菜单操作、显示操作、AOI菜单操作、矢量和栅格菜单、数据的输入输出等。 三、实验目的:初步了解Erdas的主要功能模块,在此基础上,掌握视…

在Windows Server2016中安装SQL Server2016(转)

在Windows Server2016中安装SQL Server2016(转) 转自: http://blog.csdn.net/yenange/article/details/52980135 参考: SQL Server2016企业版 附全版本key - moonpure的专栏 - CSDN博客 http://blog.csdn.net/moonpure/article/d…

Unity3D 之UGUI 滑动条(Slider)

这里来讲解下UGUI 滑动条(Slider)的用法 控件下面有三个游戏对象 Background -->背景 Fill Area --> 前景区域 Handle Slide Area --> 滑动条 Slider的属性 其他几个设置和其他控件都差不多,这里来讲解几个特有的属性。 Direction -->方向 Whole Number…

C语言真的很难吗?那是你没看这张图,化整为零轻松学习C语言。

真不难 C语言难不难?这个问题是相对的,对于找到合适方法学习C语言的同学想必是觉得很简单;但对于一部分同学来说,没有众观全局就会误以为刚入门就需要学习庞大的知识,学着学着开始看不懂,由于心理作怪&…

【中间件】.net Core中使用HttpReports进行接口统计,分析, 可视化, 监控,追踪等...

HttpReports 基于.Net Core 开发的APM监控系统,使用MIT开源协议,主要功能包括,统计, 分析, 可视化, 监控,追踪等,适合在微服务环境中使用。官方地址:https://www.yuque.com/httpreports/docs/u…

【遥感数字图像处理】实验:遥感影像辐射纠正(大气纠正)完整操作图文教程(Erdas版)

一、实验平台:Erdas 9.1 二、实验数据:dmtm.img 三、实验内容:利用回归分析法校正影像 四、实验原理:大气散射只影响短波波段,长短波进行对比,找出影响短波的程辐射值,将其减去 五、实验目的:掌握回归分析法校正影像的方法及步骤,能熟练地对影像进行校正 六、实…

Acitivty生命周期

为什么80%的码农都做不了架构师?>>> Acitivty 有七个生命周期: onCreate:当第一次调用一个Activity就会执行onCreate方法 onStart:当Activity处于可见状态的时候就会调用onStart方法 onResume:当Activity可…

还不懂你现在学习的编程语言能做什么?还不懂如何进阶?过来看图

前言说七说八 本篇文章的配图标注、内容并不代表仅有;本篇仅以个人经验及当前大学(大专、本科)相关课程作对比,列出比较常规的语言发展走向及相关技术;再次重申,本图及本文所涉及的技术发展走向并不代表着…

【遥感数字图像处理】实验:遥感影像几何纠正完整操作流程(Erdas版)

☆☆☆ 几何纠正预备知识 ☆☆☆ 1、几何变形误差的影响因素 遥感器本身引起的畸变外部因素引起的畸变处理过程中引起的畸变2、需要做精纠正的情况 景与景之间作比较GIS建模之前监督分类时提取样本创建高精度比例尺的影像地图与矢量数据叠加源于不同比例尺的地图之间比较提取精…

openid 钉钉_钉钉开发入门,微应用识别用户身份,获取用户免登授权码code,获取用户userid,获取用户详细信息...

最近有个需求,在钉钉内,点击微应用,获取用户身份,根据获取到的用户身份去企业内部的用户中心做校验,校验通过,相关子系统直接登陆;就是在获取这个用户身份的时候,网上的资料七零八落的,找的人烦躁的很,所以自己记录一下;实现这个要求,有好几种方式,使用ISV方式相对来说比较简单…

趣味二维码生成

1背景介绍 最近在 Github 看到了一个有趣的项目 amazing-qr,它支持生成普通二维码,带图片的艺术二维码,动态二维码。项目是用 python 编写的,以命令行的方式运行生成,不太方便调用,因此,我…

《零基础看得懂的C++入门教程 》——(1)第一个C++程序就让你知其所以然

一、学习目标 了解第一个C程序了解第一个C程序结构了解什么是注释了解什么是命名空间了解C语言的输出(如何在程序运行时显示内容)了解语句结束后需要使用什么符号表示结束 了解程序入口 目录 预备第一篇,使用软件介绍在这一篇,…

1、Locust压力测试环境搭建

环境准备:阿里云服务器一台、python2.7、pip Locust 介绍Locust 是一个开源负载测试工具。使用 Python 代码定义用户行为,也可以仿真百万个用户。 Locust 简单易用,分布式,用户负载测试工具。Locust 主要为网站或者其他系统进行负…

交互式 .Net

1名词解析 1. 交互式交互式是指输入代码后可直接运行该代码,然后持续输入运行代码。2. 交互式 .Net.Net 是一种编译型语言,不像 python 这类的脚本型语言,可以边输入代码边运行结果。幸运的是,软微推出了 interactive 这个项…

mysql signed 长度_浅谈mysql字段长度设置

mysql 中最常用的数据类型是tinyint,smallint,int,bigint,char,varchar;char(n)和varchar(n)存储固定长度的字符数据,长度最大为254字节。使用 ‘n’字节的存储空间;有符号和无符号区别:有符号可以存储负值,无符号只能存储0和非负值数值;tinyint 占用1字节的存储空间…

R语言-异常数据处理2

在R中进行基于稳健马氏距离的异常检验 前言 我们研究的数据中经常包含着一些不同寻常的样本,这称之为异常值(Outlier)。这些异常值会极大的影响回归或分类的效果。异常值产生的原因有很多,其中可能是人为错误、数据测量误差,或者是实际确实存…

《零基础看得懂的C++入门教程 》——(2)什么是数据类型、变量?一看便会

一、学习目标 了解基本常用的数据类型了解什么是变量 目录 预备第一篇,使用软件介绍在这一篇,C与C使用的软件是一样的,查看这篇即可:《软件介绍》 想了解编译原理和学习方法点这篇,学习方法和一些原理C与C都是相同的…

pycharm 快捷键介绍

CtrlN (Navigate | Class) 打开输入框输入要查找的类名 Ctrl空格 代码自动补全 Ctrl空格(按两次)对于没有导入的类自动完成导入代码并自动补全 AltF7 查找方法变量在工程中的所有应用 CtrlQ (View | Quick Documentation). 快速查看文档 …

db2和mysql语句区别_db2和mysql语法的区别是什么

MySQL默认使用大小写敏感的数据库名、表名和列名(可以通过lower_case_table_names参数控制是否大小写敏感),DB2数据库对大小写不敏感。虽然MySQL与DB2都遵循并符合SQL92标准且大多数SQL相互兼容,但是在一些细节的实现上有一些不同的地方。比如&#xff1…