分布式限频组件设计思路

分布式限频组件

如果需要你设计一个分布式限频组件,你会怎么做,将考虑哪些方面的内容?
笔者整理自己对限频组件的理解,总结一些设计思路,希望与大家探讨。

明确需求

调用流程

在微服务的入口,把上游请求包拆解鉴权后,进行流量限制校验,流量校验通过的请求继续执行,否则返回流量限频。

请求路由

按client端的路由方式分类,一般分为三种:

固定路由

固定路由标识同样的请求只会路由到对应的一台Server
在这里插入图片描述

分组路由

后端server分组,同样的请求只会路由到某一组机器

在这里插入图片描述

随机路由

请求可以随机路由到后端任一台Server

在这里插入图片描述

他们三者的请求的可用性是逐渐提高的,如固定路由,当后端svr服务挂掉时,该组所有请求都将不能正常运行。
当然也可以采取降级措施:比如在后端服务异常时,可以降级路由到其他的机器,从而提升整体的可用性。
但请求能路由到其他机器的前提,是服务器是无状态的,如果在svr上有数据如限额组件缓存,则不可以路由否则会导致请求异常。

性能要求

限频组件要求可用性

限频前者指的是频率限制,比如是api限频,异常时有超出部分也能接受请求,限频失败的话会导致下游有短暂积压,在系统的容错性范围内即可。偏向追求性能,而不是正确性。

结论:用内存缓存即可,不用落地存储。

限额组件要求强一致性

限频和限额似乎是同一个问题,但是具体的场景不同:
限额指的是额度限制,比如是发券、抽奖场景下的重要数据限制。这种限频的要求的宁可不发也不能多发。偏向百分之百的正确性。

结论:限额组件需要保障数据的一致性,一般来说需要落地存储。

典型的限额算法

典型的限额算法有两个窗口算法,两个桶算法

固定窗口

算法过程

在每个时间窗口(如1分钟)设置一个计数和阈值,当该窗口的计数值大于阈值时,则拦截超出阈值的请求,具体见图:

在这里插入图片描述

算法分析

优点:

  1. 算法简单,易于理解

缺点:

  1. 窗口的大小决定了限频的时间粒度
    • 如果窗口太小则会导致系统对外交付的峰值过低
    • 如果窗口太大则会导致限频精度过低,在窗口的前半部分就消耗光该窗口的限额
  2. 在跨窗口前后,可能会有两倍请求发送到后端

滑动窗口

算法过程

将固定窗口细分为多个小的固定窗口,将一段固定窗口总和记为窗口总量,保证一段小窗口的总量不超过限额即可。

在这里插入图片描述

算法分析

优点:

  1. 依赖子窗口的划分粒度,支持时间更细粒度的限频,越细越精确

缺点:

  1. 单个时间窗口可能很高
  2. 可以将单个窗口使用固定限额,但需要额外的存储

令牌桶

算法过程

核心思路是将资源放在桶里面,同时以一定的速率向桶里面添加,如果满了就不能再添加了。
同时请求消耗资源则是从桶里面拿资源,如果取出则进行后续操作,如果为空就返回频率限制。
在这里插入图片描述

算法分析

优点:将资源抽象在桶里面,相当于缓存上历史周期没有处理完毕的请求量。
缺点:同样存在跨周期峰值的问题,如果桶是满的,那么一瞬间将请求消耗完毕后,同时马上补充下一个周期的资源,同样立刻消耗光,则会造成短暂的超负荷请求量到下游。

漏桶

算法过程

资源进入,和资源出的速率都是恒定的,后者的恒定是通过一个队列来进行调度的。

在这里插入图片描述

算法分析

优点:继承了令牌桶算法的优点,同时支持投入资源时以恒定的速率
缺点:请求不能是同步的,需要用一个队列来承载,同时组件来调度队列中的数据;
改进:将漏出数据从队列改为非丢列,直接从桶中拿取,拿取成功则返回成功。

算法对比

窗口算法

窗口算法的思想是将资源周期地进行分发,每一段固定长度时间,分发固定的请求额度。类似手机每个月的固定流量,月初重新补充满。
固定窗口的大小标识了统计的时间粒度,也就是常说的秒级还是分钟级。

桶算法

桶算法的思想是将资源分配在桶里面,类似一个缓冲设计。缓冲设计的好处是,在相对短的一个时间内的高负载是可以处理的,但是不能长期处于高负载。
桶的作用是一个蓄水池,桶的大小决定了某一时刻并发的上限,也就是峰值流量总和。
向桶加资源速率决定了svr端恢复的能力
向桶取资源的速率上限决定了svr端的供给能力

设计

数据缓存方案

选用全局数据缓存,还是单机内存数据缓存?

全局数据缓存

redis,或者其他的组件构建全局的分布式缓存,可以保证数据量在全局是唯一的;这是最优解,但是难点在于如何设计全局的分布式缓存。

单机内存数据缓存

用单个svr构建单机内存数据缓存,方案更易设计,难点在于各svr之间数据不一致时如何处理。
下文以单个svr构建单机内存数据缓存的方案,进行分析。

路由问题

根据上述提到的多种路由方式,最简单的是固定路由,每台svr只处理同样的数据。每台svr可以开辟一篇内存区域,用于缓存对应数据段的记录。
如果是分组路由或者随机路由,那么用svr的内存缓存方案,无法svr之间的数据一致性。

规避方案:

用分组路由方案,路由到不同的svr概率是相同的,那么可以用缩小单个svr上限值的方式来近似模拟全局的限频。但是相比固定路由来说,它承担的数据量也更多了,每台svr需要内存更多。

在这里插入图片描述

如Client-1中流量可能到Server 1到3中任意一台,且概率相同都是1/3,那么将其每台频率限制为总和的1/3,可以得到近似的限额效果。

客户端缓存

客户端需要缓存吗,客户端能缓存哪种类型的数据?
因为是分布式限额组件,客户端是分散的,它能得到的信息仅仅是一部分,需要rpc获取全局数据才能进行决策,该请求是否满足限额要求。

在这里插入图片描述

但是正因为它的信息是一部分,我们可以利用这一部分进行缓存:
当请求被拦截时,客户端也可以知道请求被拦截,那么客户端可以把该请求的拦截信息存下来,在规则描述的一定时间内,该client直接返回限频即可。

限频规则存在哪

限频规则初始化在svr端加载,还是在请求中传过来。
限频规则在svr中:
优点:

  • 客户端简单,无需感知规则,将请求包传来即可
    缺点:
  • 业务规则变动时,svr端需要感知同时修改变更

限频规则在请求中传来:
优点:

  • 在请求包中,将规则传来即可
    缺点:
  • 规则建立好后,请求包中的规则将成为无效字段

svr端内存管理

内存对齐

内存管理中最麻烦的地方在于每个item的大小是非规整的。那么在这里是否可能存在这个问题?
在请求中传来的规则id必然是规整的,但是一个规则id后面对应的key不一定是规整的,我们可以要求传过来必须是uint_64或者64bytes,从而保证我们的内存是规整的。如果不能保证只能让key是str,那么需要svr提供hash算法,将str哈希成固定长度的hash-str。

过期数据清理

过期数据包括历史某规则的某个请求,在某一个时间周期里面的计数。如果该规则或者该请求不再被访问时,应该将该请求对应内存数据进行清理。清理的逻辑就是看其计数的数据周期是否在当前周期,如果不在则应该进行清理。
按照清理任务的调度,有如下2种清理方式:

定时清理

开启一个独立的线程,定时进行清理。

access到时清理

某个请求access时,会去尝试清理所有已经过期的数据。这种办法会将所有的规则都进行加锁,严重降低效率。
但是每次access时可以清理该请求部分对应的规则数据。

总结

分布式限频组件面对的主要是全局数据一致性的问题,同时中间也有内存管理的挑战,如路由分发,节点内存规整等问题。
本文主要介绍了分布式限频组件的需求,场景的限频算法,以及一些实际的设计思路,希望可以对大家有益。

关于作者:
sheldon,2年分布式存储实习经历,2年电商后端开发经验;对分布式存储,高可用系统设计,有浓厚兴趣,期待与您的交流。

猜你喜欢

转载自blog.csdn.net/mistakk/article/details/115445060