system design 俄罗斯大叔系列
限速

一个client忽然发送异常多的请求。

可能有很多原因:

测试人员在压测、流量高峰、恶意攻击

可能导致的问题:

他占用了大量cpu、内存、硬盘、网络IO资源

导致其它用户的请求产生高延迟或高失败率

所以要引入限速:要么延迟解决要么立刻拒绝

为什么不用自动化扩展?

因为有可能太慢,还没扩完就崩了

为什么不用LB来限速?

因为每个部件情况不一样有的快有的慢,LB不能识别。所以需要定制服务来限速

为什么一定要用分布式?不能各自服务器独立解决吗?

还是上面的原因,we will need a solution where application servers will communicate with each other and share information about how many client requests each one of them processed so far.

Requirements:

Functional:

  • allowRequest(request)
  • return boolean

Non-Functional:

  • low latency:make decision as soon as possible
  • accurate: as accurate as we can get
  • scalable: supports an arbitrarily large number of hosts in the cluster
  • 容错和高可用呢?不重要。如果不知道是否限速,就不限。

solution:

从简单的方案开始,逐渐扩展。

Untitled

so ① rules retriever:

是一个后台进程,每个rule指定每秒允许的特定客户端的请求数,并轮询检查是否有新rule或修改

rule自己定义并存在数据库,并且通过一个web服务来管理操作。并在内存中有存储。

请求到来时,第一件事是建立一个client identifier = key,是该client的唯一标识。

然后key传到rate limiter,rate limiter根据cache里的rule检查key,

如果match,rate limiter会检查这个client最后的请求是否超速。

如果没超,request就move further;如果超了,就拒绝请求rejected。

这时有三种选项:

①response 特定的status code

②延迟处理

③直接丢掉

总结:

需要一个数据库来存规则,这个数据库需要crud

需要一个过程定期检索规则,并将它放入内存

需要一个做决定的组件。(构建client key要单独还是放一起都可以)

接下来的几个走向:::

——> 算法:实现rate limiter算法

——> OOD: 定义这个库的主要类和接口

——> 分布式留解决方案,server之间如何共享数据

算法:

  • Google Guava RateLimiter

  • 滑动窗口

  • 令牌桶算法:最简单。好理解易于实现。

    每个桶有:可容纳的最大令牌量,当前可用的令牌数,令牌加入桶的速率

    有请求到来时,从桶中取一个令牌。如果没有,就拒绝请求。

    并且桶用恒定的速率填充令牌。

类和接口:

Untitled

  JobScheduler: 负责调度job和从规则库里检索规则

RulesCache:负责将规则存储在内存中

ClientIdentifier:构建唯一标识客户端的密钥

RateLimiter:负责决策

distributed:

有一个问题是,假设初始有三个桶,整个系统要求是每秒上限是4,初始时【每个桶内需要有4!】因为它们不知道请求要去哪个桶

然后当消耗完四个,假设A桶剩1,B桶剩2,C桶剩1

这时【这三个桶需要互相知道它们总共已经消耗了四个!】,所以下一刻这三个桶都更新成剩余0。

这时有一个问题,如果在三个桶互通消息的时候,又有请求进来了怎么办。

可用修改桶,让它允许负数令牌存在。

比如这时来了12个请求,共享消息后每个桶有-8个令牌,那么接下来两秒内都不允许有请求了。

所以关键是共享信息

Untitled

第一种:全网状广播:tell everyone everything

如何发现host?

①先择第三方服务,监听每个host的心跳,新的来了就注册,心跳停止就注销。

②解析特定用户的信息,有特别用户存了所有人的信息。

③通过配置文件提供主机列表。但是不太灵活,每次更新都要重新部署。

但是只适合小cluster。需要大量消息。

第二种:gossip protocal

随机的对等选择形式实现:每个机器随机跳另一台机器并共享数据。(雅虎)

异步是它的优点,

缺点:消息冗余,不适合用在对实时性要求较高的场景下

第三种:distributed cache

pros:distributed cache cluster相对较小,服务cluster可独立扩展,可在不同团队之间共享。

第四种:coordination service 单主

依赖第三方组件,帮助选择领导者的协调服务。

每个人消息都发给leader,leader计算并返回结果

pros:有助于减少集群内广播的消息数量

可以用paxos、raft等共识算法来实现coordination service

cons:必须维护这个协调系统,它很复杂,必须很可靠,而且要确保选出有且只有一个领导者

第五种:random leader selection

如果每个leader对得票率消息共享,可能过计算但就不会出现选出多个leader的情况。

communication protocals 通信协议:how hosts talk to each other

两种选择:TCP UDP

TCP:保证数据安全及按顺序传递。但慢。

UDP:不保证收到所有data也不保证数据。但快。

integrate 集成这些组件

两种

Untitled

① rate limiter 作为服务进程的一部分运行

作为类的集合分发

pros:更快。无需进程间调用,也就没有进程调用失败。

cons: 👇

② rate limiter 作为它自己的进程运行

两个库:守护进程本身和客户端,client和服务代码集成在一起 integrate

pros:可用不同的语言编写(守护进程而不是client),不用在代码级别集成。

有自己的内存空间,可用更好的控制服务和守护进程,内存分配可预测。

当有一个与集群种其它主机通信的守护进程时,很好用。

cons: 👆qi

其它:

如果有大量请求,是否会有大量桶在内存当中?

如果没有请求,可以不用把桶保存在内存里。请求间隔时常短时就创建放进内存,否则就删掉。

故障模式 failure scenarios:

守护进程fail,导致集群中其它主机无法看的这个失败的守护进程。所以这个机器失联了,但仍然继续执行任务。只是限制的请求更少罢了。

(通信:all 只允许4个。无法通信:每个允许4个,一共12个)

rule的管理:

需要引入一个自助服务工具,以便增删改rules

同步:synchronization

令牌桶中需要同步。更好的办法是在这个类中实现线程安全,like原子引用

令牌桶缓存需要同步,当删除不用桶、建立新桶时,会最终同步。所以需要用concurrentHashMap (线程安全)

大部分服务不会请求那么大,所以同步这一块不会增加太多开销成为瓶颈

client 应如何处理受限制的调用呢

重试。丢进队列。

exponential backoff and jitter 指数退避和抖动:

多次重试请求,但每次重试都等待更长的时间。

抖动增加了重试间隔的随机性以分散负载

Untitled

最终选择:

小 cluster <x000 nodes, active buckets/sec < 10,000 :

UDP, gossip communication

大 hosts > 10,000

需要单独集群