2019 年 1 月 12 日,由又拍云、OpenResty 中国社区主办的 OpenResty × Open Talk 全国巡回沙龙·深圳站圆满结束,又拍云首席架构师张聪在活动上做了《 OpenResty 动态流控的几种姿势 》的分享。
OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起的,为促进 OpenResty 在技术圈的发展,增进 OpenResty 使用者的交流与学习的系列活动,活动将会陆续在深圳、北京、上海、广州、杭州、成都、武汉等地举办,欢迎大家关注。
张聪,又拍云首席架构师,多年 CDN 行业产品设计、技术开发和团队管理相关经验,个人技术方向集中在 Nginx、OpenResty 等高性能 Web 服务器方面,国内 OpenResty 技术早期推广者之一;目前担任又拍云内容加速部技术负责人,主导又拍云 CDN 技术平台的建设和发展。
以下是分享全文:
大家下午好,今天我主要和大家分享“在 OpenResty 上如何做动态的流量控制”,将会从以下几个方面来介绍:
- Nginx 如何做流控,介绍几种经典的速率和流量控制的指令和方法;
- OpenResty 如何动态化做流控;
- OpenResty 动态流控在又拍云的业务应用。
又拍云与 OpenResty 结缘
我目前在又拍云负责 CDN 的架构设计和开发工作,又拍云早在 2012 年就开始接触 OpenResty ,当时我们做调研选型,部分项目考虑用 Lua 来实现,在此之前是基于 Nginx C 模块来做业务开发,一个防盗链模块就好几千行代码,改成 Lua 之后大量减少了代码,并且整个开发的效率、维护的复杂度都降低了。此外我们通过测试和性能对比,几乎没有多的损耗,因为在这一层主要是字符串的处理,甚至在 LuaJIT 加速的情况下有很多的调用,比我们原先用 C 写的函数还高效得多。
目前又拍云整个 CDN 代理层系统、对外开放的 API 系统、数据中心的网关系统、分布式云存储代理层、逻辑层全部用 ngx_lua 进行了深度的改造,又拍云内部几个不同业务的团队都在 OpenResty 技术栈上有多年的实践和经验积累。
又拍云开放了一个 upyun-resty 的仓库(https://github.com/upyun/upyun-resty),我们内部孵化的开源项目以及对社区的补丁修复等都会发布在这个仓库。大家如果对又拍云这块的工作感兴趣可以关注这个仓库,我们今年还会陆续把内部使用非常成熟的一些库放出来,包括今天讲的两个与限速有关的 Lua 库也已经开源出来了。
什么是流控以及为什么要做流控
1、什么是流控
今天的主题,首先是针对应用层,尤其是 7 层的 HTTP 层,在业务流量进来的时候如何做流量的疏导和控制。我个人对“流控”的理解(针对应用层):
(1) 流控通常意义下是通过一些合理的技术手段对入口请求或流量进行有效地疏导和控制,从而使得有限资源的上游服务和整个系统能始终在健康的设计负荷下工作,同时在不影响绝大多数用户体验的情况下,整个系统的“利益”最大化。
因为后端资源有限,无论考虑成本、机器或者系统本身的瓶颈,不可能要求上游系统能够承受突发的流量,而需要在前面做好流量的控制和管理。
有时候我们不得不牺牲少数的用户体验,拒绝部分请求来保证绝大多数的请求正常地服务,其实没有完美能够解决所有问题的方案,所以这个在流量控制中要结合我们对业务的理解需要学会做取舍。
(2) 流控有时候也是在考虑安全和成本时的一个手段。
除了上面的通用场景,流控也在安全和成本上做控制。比如敏感账号的登录页面,密码失败次数多了就禁掉它,不允许反复暴力尝试;比如我们的上游带宽有限,需要确保传输的带宽在较低的水平中进行,不要把线路跑满,因为跑满有可能涉及到一些成本超支的问题等。
2、为什么要流控
针对上面的描述,下面介绍一些流控跟速率限制的方法。
(1)为了业务数据安全,针对关键密码认证请求进行有限次数限制,避免他人通过字典攻击暴力破解。
为了数据安全,我们会对一些敏感的请求尝试访问做累计次数的限制,比如一定时间内你输错了三次密码,接下来的几个小时内就不让你来尝试了,这是一种很常见的手段。如果没有这样的保护,攻击者会不断试你的密码,调用这个敏感的接口,最终可能会让他试出来,所以这是需要保护的。
(2)在保障正常用户请求频率的同时,限制非正常速率的恶意 DDoS 攻击请求,拒绝非人类访问。
我们需要保障一个 API 服务正常的请求流量,但是拒绝完全恶意的 DDoS 的攻击、大量非人类访问。这也是在最前面这层需要做的事情,否则这些请求串进上游,很多后端的服务器肯定是抗不住的。
(3)控制上游应用在同一时刻处理的用户请求数量,以免出现并发资源竞争导致体验下降。
我们需要控制上游只能同时并发处理几个任务或几个请求,此时关心的是“同时”,因为它可能有内部的资源竞争,或者有一些冲突,必须保证这个服务“同时”只能满足几个用户的处理。
(4)上游业务处理能力有限,如果某一时刻累计未完成任务超过设计最大容量,会导致整体系统出现不稳定甚至持续恶化,需要时刻保持在安全负荷下工作。
当我们整个上游系统的弹性伸缩能力还不错,它会有一个设计好的最大容量空间,即最多累计能够承受多大量的请求流入。如果超过它最大可处理范围性能就会下降。例如一个任务系统每小时能够完成 10 万个任务,如果一个小时内任务没有堆积超过 10 万,它都能够正常处理;但某一个小时出现了 20 万请求,那它处理能力就会下降,它原本一小时能处理 10 万,此时可能只能处理 5 万或 2 万甚至更少,性能变得很差,持续恶化,甚至最终导致崩溃。
因此,我们需要对这样的流量进行疏导,确保后端系统能够健康地运行,如果它每小时最多只能跑 10 万的任务,那么无论多大的任务量,每小时最多都应只让它跑 10 万的量,而不是因为量超过了,反而最后连 10 万都跑不到。
(5)集群模式下,负载均衡也是流控最基础的一个环节,当然也有些业务无法精确进行前置负载均衡,例如图片处理等场景就容易出现单点资源瓶颈,此时需要根据上游节点实时负载情况进行主动调度。
在做流量管理时,负载均衡是很基础的。如果一个集群基本负载均衡都没做好,流量还是偏的,上游某个节点很容易在集群中出现单点,这时去做流量控制就有点不合适。流量控制,首先在集群模式下要先做好负载均衡,在流量均衡的情况下再去做流量控制,识别恶意的流量。而不要前面的负载均衡都没做好,流量都集中在某一台机器上,那你在这一台上去做控制,吃力不讨好。
(6)在实际的业务运营中,往往出于成本考虑,还需要进行流量整形和带宽控制,包括下载限速和上传限速,以及在特定领域例如终端设备音视频播放场景下,根据实际码率进行针对性速率限制等。
出于成本的考虑,我们会对一些流量进行控制。比如下载限速是一个很常见的场景,终端用户尤其是移动端,在进行视频的播放,按正常的码率播放已经足够流畅。如果是家庭带宽,下载速度很快,打开没一会儿就把电影下载完成了,但实际上没有必要,因为电影播放已经足够流畅,一下子把它下载完浪费了很多流量。特别地对于音视频的内容提供商,他觉得浪费了流量而且用户体验差不多,所以此时一般会对这些文件进行下载限速。
经典的 Nginx 方式实现流量控制
Nginx 大家都非常熟悉,特别是数据中心或后端的服务,不管是什么语言写的,可能你也不明白为什么要这么做,但前面套一个 Nginx 总是让人放心一点,因为在这么多年的发展中,Nginx 已经默默变成一个非常基础可靠的反顶流量最外层入口的在向代理服务器,基本上很多开发者甚至感知不到它的存在,只知道运维帮忙前面架了一个转发的服务。
所以,如果我们要做流量管理,应该尽量往前做,不要等流量转发到后面,让应用服务去做可能已经来不及了,应用服务只需要关心业务,这些通用的事情就让上层的代理服务器完成。
1、Nginx 请求速率限制
(1)limit_req 模块
limit_req 是 Nginx 最常用的限速的模块,上图是一个简单的配置,它基于来源 IP 作为唯一的 Key,针对某个唯一的来源 IP 做速率控制,这里的速率控制配置是 5r/s( 1 秒内允许 5 个请求进来),基于这个模块的实现,再解释一下 5r/s,即每隔 200ms 能够允许进来一个请求,每个请求的间隔必须大于 200ms,如果小于 200ms 它就会帮你拒绝。
使用起来很简单,配置一个共享内存,为了多个 worker 能共享状态。具体可以在 location 做这样的配置,配完之后就会产生限速的效果。
上图可以更加直观地了解这个机制,图中整个灰色的时间条跨度是 1s,我按 200ms 切割成了五等份,时间条上面的箭头代表一个请求。0s 的时候第一次请求过来直接转发到后面去了;第二次间隔 200ms 过来的请求也是直接转发到上游;第三个请求也一样;第四个请求(第一个红色箭头)在 500ms 左右过来,它跟前一个请求的时间间隔只有 100ms,此时模块就发挥作用,帮你拒绝掉,后面也是类似的。
总结一下 limit_req 模块的特点:
- 针对来源IP,限制其请求速率为 5r/s。
- 意味着,相邻请求间隔至少 200ms,否则拒绝。
但实际业务中,偶尔有些突增也是正常的。
这样简单地使用,很多时候在实际的业务中是用得不舒服的,因为实际业务中很多场景是需要有一些偶尔的突增的,这样操作会过于敏感,一超过 200ms 就弹,绝大多数系统都需要允许偶尔的突发,而不能那么严格地去做速率限制。
(2)brust 功能参数
这样就引出了 limit_req 模块中的一个功能参数 brust(突发),为了方便演示,这里设置 brust=4,表示在超过限制速率 5r/s 的时候,同时最多允许额外有 4 个请求排队等候,待平均速率回归正常后,队列最前面的请求会优先被处理。
在 brust 参数的配合下,请求频率限制允许一定程度的突发请求。设置为 4 次后,表示在超过 5r/s 的瞬间,本来要直接弹掉的请求,现在系统允许额外有 4 个位置的排队等候,等到整体的平均速率回归到正常后,排队中的 4 个请求会挨个放进去。对于上游的业务服务,感知到的始终是 200ms 一个间隔进来一个请求,部分提前到达的请求在 Nginx 这侧进行排队,等到请求可以进来了就放进来,这样就允许了一定程度的突发。
如上图,时间条上面第四个请求跟第三个,间隔明显是小于 200ms ,按原来的设置应该就直接拒绝了,但现在我们允许一定程度的突发,所以第四个请求被排队了,等时间慢慢流转到 600ms 的时候,就会让它转发给后端,实际它等待了 100ms。下面也是挨个进来排队,第五个请求进来它排队了 200ms,因为前面的时间片已经被第四个请求占用了,它必须等到下一个时间片才能转发。可以看到上游服务接收到的请求间隔永远是恒定的 200ms。
在已经存在 4 个请求同时等候的情况下,此时“立刻”过来的请求就会被拒绝。上图中可以看到从第五个请求到第九个请求,一共排队了 5 个请求,第十个请求才被拒绝。因为时间一直是在流动的,它整体是一个动态排队的过程,解决了一定程度的突发,当然太多突发了还是会处理的。
虽然允许了一定程度的突发,但有些业务场景中,排队导致的请求延迟增加是不可接受的,例如上图中突发队列队尾的那个请求被滞后了 800ms 才进行处理。对于一些敏感的业务,我们不允许排队太久,因为这些延时根本就不是在进行有效处理,它只是等候在 Nginx 这侧,这时很多业务场景可能就接受不了,这样的机制我们也需要结合新的要求再优化。但是如果你对延时没有要求,允许一定的突发,用起来已经比较舒服了。
(3)nodealy 功能参数
limit_req 模块引入了 nodelay 的功能参数,配合 brust 参数使用。nodelay 参数配合 brust=4 就可以使得突发时需要等待的请求立即得到处理,与此同时,模拟一个插槽个数为 4 的“令牌”队列(桶)。
本来突发的请求是需要等待的,有了 nodelay 参数后,原本需要等待的 4 个请求一旦过来就直接转发给后端,落到后端的请求不会像刚刚那样存在严格的 200ms 间隔,在比较短的时间内就会落下去,它实际上没有在排队,请求进来直接往上游就转发,不过后续超出队列突发的请求仍然是会被限制的。
为了能够比较好理解这个场景,引入一个虚拟“桶”。从抽象的角度描述下这个过程,该“令牌”桶会每隔 200ms 释放一个“令牌”,空出的槽位等待新的“令牌”进来,若桶槽位被填满,随后突发的请求就会被拒绝。
本来第六到第九这 4 个请求是排队等候在 Nginx 一侧,现在它们没有等待直接下去了,可以理解为我们拿出了 4 个虚拟的令牌放入一个“桶”,4 个令牌模拟这 4 个请求在排队。“桶”每隔 200ms 就会释放出一个令牌,而一旦它释放出一个,新的虚拟令牌就可以过来,如果它还没释放出,“桶”是满的,这时请求过来还是会被拒绝。总而言之就是真实的请求没有在排队,而是引入了 4 个虚拟的令牌在排队,在它满的情况下是不允许其它请求进来。
如此,可以保证这些排队的请求不需要消耗在无谓的等待上,可以直接进去先处理,而对于后面超过突发值的请求还是拒绝的。这样就达到了折中,对于上游,它需要更短的时间间隔来处理请求,当然这需要结合业务来考虑,这里只是提供了一种方式和特定的案例。
总结,在这个模式下,在控制请求速率的同时,允许了一定程度的突发,并且这些突发的请求由于不需要排队,它能够立即得到处理,改善了延迟体验。
(4)delay 功能参数
Nginx 最新的版本 1.15.7 增加了 delay 参数,**支持 delay=number 和 brust=number 参数配合使用。 **delay 也是一个独立的参数,它支持 number(数量)的配置,和突发的数量配置是一样的,这两个参数解决的问题更加细致,通用场景中遇到的可能会少一点。
这个功能参数是这样描述的:在有些特定场景下,我们既需要保障正常的少量关联资源能够快速地加载,同时也需要对于突发请求及时地进行限制,而 delay 参数能更精细地来控制这类限制效果。
比如网站的页面,它下面有 4-6 个 JS 、CSS 文件,加载页面时需要同时快速地加载完这几个文件,才能确保整个页面的渲染没有问题。但如果同时超过十个并发请求在这个页面上出现,那可能就会是非预期的突发,因为一个页面总共才 4-6 个资源,如果刷一下同时过来的是 12 个请求,说明用户很快地刷了多次。在这种情况下,业务上是要控制的,就可以引入了 delay 参数,它能够更精细地来控制限制效果。
在上面的例子中,一个页面并发加载资源加载这个页面的时候会跑过来 4-6 个请求,某个用户点一下页面,服务端收到的是关于这个页面的 4-6 个并发请求,返回给它;如果他很快地点了两下,我们觉得需要禁止他很快地刷这个页面刷两次,就需要把超过并发数的这部分请求限制掉,但 burst 设置太小又担心有误伤,设置太大可能就起不到任何效果。
此时,我们可以配置一个策略,整体突发配置成 12,超过 12 个肯定是需要拒绝的。而在 12 范围内,我们希望前面过来的 4-6 个并发请求能够更快地加载,不要进行无效地等待,这里设置 delay=8 ,队列中前 8 个等候的请求会直接传给上游,而不会排队,而第 8 个之后的请求仍然会排队,但不会被直接拒绝,只是会慢一些,避免在这个尺度内出现一些误伤,同时也起到了一定限制效果(增大时延)。
上面 4 点都是讲 Nginx 怎么进行请求速率限制,简单总结一下,速率就是针对连续两个请求间的请求频率的控制,包括允许一定程度的突发,以及突发排队是否需要延后处理的优化,还有后面提到的 delay 和 brust 的配合使用。
2、Nginx 并发连接数限制
Nginx 有一个模块叫 limit_conn,在下载的场景中,会出现几个用户同时在下载同一个资源,对于处理中的请求,该模块是在读完请求头全部内容后才开始计数,比如同时允许在线 5 人下载,那就限制 5 个,超过的 503 拒绝。特别地,在 HTTP/2 和 SPDY 协议下,每一个并发请求都会当作一个独立的计数项。
3、Nginx 下载带宽限制
在 ngx_http_core_module 模块里面有 limit_rate_after 和 limit_rate 参数,这个是下载带宽限制。如上图,意思是在下载完前面 500KB 数据后,对接下来的数据以每秒 20KB 速度进行限制,这个在文件下载、视频播放等业务场景中应用比较多,可以避免不必要的浪费。例如视频播放,第一个画面能够尽快看到,对用户体验来说很重要,如果用户第一个页面看不到,那他的等待忍耐程度是很差的,所以这个场景下前面的几个字节不应该去限速,在看到第一个画面之后,后面画面是按照一定视频码率播放,所以没必要下载很快,而且快了也没用,它照样是流畅的,但却多浪费了流量资源,如果用户看到一半就关掉,整个视频下载完成,对于用户和内容提供商都是资源浪费。
OpenResty 动态流控
相比 Nginx ,OpenResty 具有很多的优势。
- 我们需要更加丰富的流控策略!Nginx 只有经典的几种。
- 我们需要更加灵活的配置管理!限速的策略配置规则是多样化的,我们需要更加灵活。
- 我们需要在 Nginx 请求生命周期的更多阶段进行控制!前面提到的的 limit_req 模块,它只能在 PREACCESS 阶段进行控制,我们可能需要在 SSL 的卸载过程中对握手的连接频率进行控制,我们也可能需要在其它任意阶段进行请求频率控制,那 Nginx 这个模块就做不到了。
- 我们需要跨机器进行状态同步!
请求速率限制 / 并发连接数限制
OpenResty 官方有一个叫做 lua-resty-limit-traffic 的模块,里面有三种限速的策略。
(1) resty.limit.req 模块
resty.limit.req 模块的设计与 NGINX limit_req 实现的效果和功能一样,当然它用 Lua 来表达限速逻辑,可以在任何的代码里面去引入,几乎可以在任意上下⽂中使⽤。
(2)resty.limit.conn 模块
功能和 NGINX limit_conn 一致,但 Lua 版本允许突发连接进行短暂延迟等候。
(3)resty.limit.count 模块
第三个是 resty.limit.count 模块,请求数量限制,这个目前 Nginx 没有,用一句话概括这个模块,就是在单位时间内确保累计的请求数量不超过一个最大的值。比如在 1 分钟之内允许累计有 100 个请求,累计超过 100 就拒绝。这个模块和 Github API Rate Limiting(https://developer.github.com/v3/#rate-limiting)的接口设计类似,也是一个比较经典的限制请求的方式。
跨机器速率限制
有了 OpenResty,可以做一些更加有意思的事情。比如我们有多台机器,想把限制的状态共享,又拍云之前开放了一个简单的模块叫 lua-resty-redis-ratelimit(resty.redis.ratelimit),顾名思义就是把这个状态扔到 Redis 保存。它和 Nginx limit req 以及 resty.limit.req 一样,都是基于漏桶算法对平均请求速率进行限制。不同的是,该模块将信息保存在 Redis 从而实现多 Nginx 实例状态共享。
借助于 Redis Lua Script 机制 ,Redis 有一个支持写 Lua 脚本的功能,这个脚本能够让一些操作在 Redis 执行的时候保证原子性,依赖这个机制,我们把一次状态的变更用 Lua Script 就能够完全原子性地在 Redis 里面做完。
同时,该模块支持在整个集群层⾯禁⽌某个非法⽤用户一段时间,可实现全局自动拉⿊功能。因为是全局共享,一旦全网有一个客户触发了设置的请求频率限制,我们可以在整个集群内瞬间把他拉黑几个小时。
当然这个模块是有代价的,而且代价也比较大,因为 Nginx 和 Redis 交互需要网络 IO,会带来一定延迟开销,仅适合请求量不大,但需要非常精确限制全局请求速率或单位统计时间跨度非常大的场景。
当然,这个模块也可以做一些自己的优化,不一定所有的状态都需要跟 Redis 同步,可以根据自己的业务情况做一些局部计算,然后定时做全局同步,牺牲一些精确性和及时性,这些都可以去抉择,这边只是多提供了一个手段。
知识点-漏桶算法
前面提到的多个模块都是基于漏桶算法的思想达到频率限速的效果,如上图,一个水桶,水滴一滴一滴往下滴,我们希望水往下滴的速度尽可能是恒定的,这样下游能够承载的处理能力是比较健康的,不要一下子桶就漏了一个大洞冲下去,希望它均衡地按序地往下滴,同时前面会有源源不断的水进来。
这个漏桶算法思想的核心就是上图中这个简单的公式,我们怎么把请求的 5r/s,即每 200ms 一个请求的频次限制代到这个公式呢?
首先,在具体实现中,一般定义最小速率为 0.001r/s,即最小的请求刻度是 0.001 个请求,为了直观计算,我们用 1 个水滴(假设单位t)来表达 0.001 个请求,那么 rate=5r/s 相当于 5000t/s。
前面提到该算法是计算两个相邻请求的频率,所以要计算当前请求和上一个请求的时间间隔,假设是 100 ms,单位是毫秒,下面公式中除以 1000 转换成秒等于 0.1s,即 0.1s 能够往下滴 500 个水滴,因为速率是 5000t/s,时间过去了 0.1 秒,当然只滴下去 500 滴水。
500 水滴下去的同时,速率一直是恒定的,但是同时又有请求进来,因为新的请求进来才会去计算这个公式,所以后面加了 1000,1000 个水滴代表当前这一个请求。就可以计算出当前桶的剩余水滴数。
excess 表示上一次超出的水滴数(延迟通过),一开始是 0 。特别地,如果 excess<0,说明这个桶空了,就会把 excess 重置为 0 ;如果 excess>0,说明这个桶有水滴堆积,这时水滴的流入速度比它的流出速度快了,返回 BUSY,表示繁忙。通过这样动态的标记就可以把这个速率给控制起来。
前面提到的突发,只要把这里的 0 换成 4 ,就是允许一定程度的突发了。
令牌桶限速
令牌桶和漏桶从一些特殊的角度(特别是从效果)上是有一些相似的,但是它们在设计思想上有比较明显的差异。
令牌桶是指令牌以一定的速率往桶里进令牌,进来的请求是恒定的速率来补充这个桶,只要桶没有满就可以一直往里面放,如果是补充满了就不会再补充了。每处理一个请求就从令牌桶拿出一块,如果没有令牌可以拿那么请求就无法往下走。
lua-resty-limit-rate(resty.limit.rate)是又拍云最近开源的一个库,基于令牌桶实现。
上图是个简化的演示,首先申请两个令牌桶,一个是全局的令牌桶,一个是针对某个用户的令牌桶,因为系统内肯定有很多用户调用,全局是一个桶,每个用户是一个桶,可以做一个组合的设置。如果全局的桶没有满,单个用户超过了用户单独的频次限制,我们一般会允许其突发,后端对于处理 A 用户、B 用户的消耗一般是相同的,只是业务逻辑上分了 A 用户和 B 用户。
因此,整体容量没有超过限制,单个用户即便超过了他的限制配置,也允许他突发。只有全局桶拿不出令牌,此时再来判断每个用户的桶,看是否可以拿出令牌,如果它拿不出来了就拒绝掉。此时整体系统达到瓶颈,为了用户体验,我们不可能无差别地去弹掉任意用户的请求,而是挑出当前突发较大的用户将其请求拒绝而保障其他正常的用户请求不受任何影响,这是基于用户体验的角度来考虑限速的方案配置。
相比 limit.req 基于漏桶的设计,令牌桶的思想更关注容量的变化,而非相邻请求间的速率的限制,它适合有一定弹性容量设计的系统,只有在全局资源不够的时候才去做限制,而非两个请求之间频率超了就限制掉,速率允许有较⼤大的波动。
相比 limit.count 对单位窗口时间内累计请求数量进行限制,该模块在特定配置下,也能达到类似效果,并且能避免在单位时间窗口切换瞬间导致可能双倍的限制请求情况出现。 limit.count 模块在单位时间内,比如在 1 分钟内限制 100 次,在下一个 1 分钟统计时,上一个 1 分钟统计的计数是清零的,固定的时间窗口在切换的时候,在这个切换的瞬间,可能前 1 分钟的最后 1 秒上来了 99 个请求,下一个 1 分钟的第 1 秒上来 99 个请求,在这 2 秒内,它超过了设计的单位时间最多 100 个请求的限制,它的切换瞬间会有一些边界的重叠。而基于令牌桶后,因为它的流入流出有一个桶的容量在保护,所以它切换是比较平滑的,流入速度和流出速度中间有一个缓冲。
除了请求速率限制(一个令牌一个请求),还能够对字节传输进行流量整形,此时,一个令牌相当于一个字节。因为流量都是由一个个字节组成的。如果把字节变成令牌,那流量的流出流入也可以通过令牌桶来给流量做一些整形。整形就是流量按你期望设计的形状带宽(单位时间内的流量)进行传输。
OpenResty 动态流控在又拍云的业务应用
- 海外代理进行上传流量整形,避免跑满传输线路带宽(流量整形);
- 某 API 请求基于令牌桶针对不同账户进行请求速率控制(令牌桶应用);
- CDN 特性:IP 访问限制,支持阶梯策略升级(IP访问限制);
- CDN 特性:码率适配限速
又拍云和 KONG
KONG 是一个非常著名的 OpenResty 的应用,又拍云在 2018 年在网关层引入了 KONG ,内部也维护了一个 KONG 的 Fork 版本,做了一些插件的改造和适配。
流量整形
我们在 KONG 上怎么去做流量呢?因为香港到国内数据中心的传输线路价格非常昂贵,我们购买线路带宽是有一定限制的。但是我们在这条线路传输有很多 API ,如果有一个 API 突发流量,就会影响到其他,所以我们在 KONG 上做了改造。
KONG 的设计不允许管控请求的 socket 字节流,也是用 Nginx 的核心模块来转发字节流,我们需要去管控所有从 req socket 进来的字节流,因为要做字节流限制,所以我们这里用纯 Lua 接管了。
Lua 接管之后,可以看到每 8192 个字节,都会拿 8192 个令牌,如果能拿出来,就让这 8192 个字节往后端传;如果拿不出来,说明当时已经往后传太多字节了,就让它等一等,起到一些限制效果。
令牌桶应用
我们在某一个 API 系统中用令牌桶怎么做策略的限制呢?上图是一个简单的配置示例,我们针对全局有一个桶,一个令牌的添加速度是 40r/s,令牌的容量是 12000,每次是 4 个令牌一起添加,这是全局桶的策略;每个用户空间的策略是:桶的容量是 6000,每次 2 个令牌一起添加,它的限制大概是 10r/s ;对于一些特殊的操作,比如 delete,我们会限制得更加严格一点,引入了第三个,专门针对 delete 操作的桶。
所以这里可以有好多桶来配合,全局的,局部的以及特殊的操作,大家的限制等级都不太一样,策略都可以灵活去配置。
上图是我们实际的限制效果,蓝色部分是通过令牌桶屏蔽掉的,绿色的是健康的,这部分被弹的,看业务数据的话,不是任意空间被弹掉,它被弹的时候都是那么几个空间被弹掉,会比较集中那几个空间,特别出头的被弹掉。而不是说一大堆的空间,甚至请求流量很小的,你随机去弹几个。肯定要挑出那些捣乱的把它弹掉,从而保护整个后端的请求能维持在一个健康的水位下。
IP 访问限制
又拍云的产品中有一个 IP 访问的限制的功能,针对单位时间内的 IP 进行频率的保护。当你的网站或者静态资源被一些恶意的 IP 疯狂下载,浪费你很多流量的时候是有帮助的。而且我们支持阶梯的配置,达到第一个阶梯禁止多少时间,如果继续达到第二个阶梯,阶梯升级禁用的力度就会更大。
码率适配限速
针对视频播放,我们需要对码率进行适配。这个功能可以动态读取 MP4 的元数据,读到它的码率情况,从而做出相应的下载带宽控制的策略,使得这个文件在播放的时候看到的是很流畅的,用户体验没有受到任何影响,但是不会因为客户端网速较快而多浪费流量资源。这是下载带宽限速,结合实际应用的一个例子。
分享视频及PPT可前往:
OpenResty 动态流控的几种姿势 - 又拍云