生产环境中的限流方案 -- 如果让你设计一个请求限流器,你会怎么做?
随着互联网应用的普及和用户数量的增长,系统面临的并发请求量不断增加。在这种情况下,如果没有适当的限制措施,系统很容易受到过多的请求压力,导致性能下降甚至崩溃。因此,限流算法应运而生,它可以根据系统的处理能力和资源情况,合理地限制请求的数量,以保证系统的稳定和可靠性。
常见的限流算法
限流算法的基本原理是通过控制请求的速率来限制系统的负载。它可以防止过多的请求同时涌入系统,使系统能够按照自身的处理能力有序地进行工作。常见的限流算法包括漏桶算法和令牌桶算法。
令牌桶算法(Token Bucket Algorithm)
算法原理:令牌桶算法通过维护一个令牌桶来控制请求的速率。令牌桶以固定的速率往桶中放入令牌(代表请求),桶的大小表示令牌的最大数量。当请求到达时,需要从令牌桶中获取一个令牌,如果桶中没有可用令牌,则请求被拒绝。如果有可用令牌,则取走一个令牌,表示请求被接受。
工作流程:令牌桶算法使用一个计时器以固定的速率向桶中添加令牌。当请求到达时,首先检查桶中是否有令牌可用。如果有令牌可用,则取走一个令牌,并处理该请求;如果没有令牌可用,则请求被拒绝。无论请求是否被接受,都会消耗一个令牌。
优点:令牌桶算法可以平滑地限制请求的速率,对于突发性请求可以有较好的处理能力。它还可以适应不同的请求速率和峰值流量。
缺点:在处理大量的突发性请求时,令牌桶可能会迅速耗尽,导致一段时间内无法处理请求。此外,算法的实现可能相对复杂。
漏桶算法(Leaky Bucket Algorithm)
算法原理:漏桶算法以固定的速率从一个漏桶中漏水。当请求到达时,如果桶还有空闲容量,则请求被接受并放入桶中;否则,请求被拒绝。桶中的容量代表系统可以处理的请求数量。如果请求被接受,漏桶以固定的速率漏水,释放出桶中的一个请求,这样就为下一个请求腾出空间。
工作流程:漏桶算法使用一个计时器以固定的速率漏水。当请求到达时,首先检查桶中是否有空闲容量。如果有空闲容量,则将请求放入桶中,并开始漏水。如果没有空闲容量,则请求被拒绝。无论请求是否被接受,都会按照固定速率漏水释放请求。
优点:漏桶算法可以平滑地限制请求的速率,对于突发性请求可以有较好的处理能力。它可以防止系统被大量请求压垮,提供了一定的请求处理能力保护机制。
缺点:在处理突发性请求时,漏桶的容量可能会迅速耗尽,导致一段时间内无法处理请求。此外,算法的实现可能相对复杂。
计数器算法(Counter Algorithm)
算法原理:计数器算法简单地统计在指定时间窗口内到达的请求数量,并与预设的阈值进行比较。如果请求数超过阈值,则请求被拒绝。该算法维护一个计数器,每当有请求到达时,计数器加1,并记录请求的时间戳。过了时间窗口后,计数器清零。
工作流程:计数器算法使用一个计时器来跟踪时间窗口。每当请求到达时,计数器加1,并检查计数器的值是否超过了阈值。如果超过了阈值,则请求被拒绝;否则,请求被接受。
优点:实现简单,对于控制请求速率较为精确。可以根据业务需求灵活地调整时间窗口的大小和阈值。
缺点:对于突发性请求的处理能力较差,容易出现请求被拒绝或阻塞的情况。
滑动窗口算法(Sliding Window Algorithm)
算法原理:滑动窗口算法将时间划分为固定大小的窗口,每个窗口内维护请求的计数器。随着时间的推移,旧的窗口被移除,新的窗口被添加。通过统计窗口内的请求数量,并与设定的阈值进行比较,可以实现限流的目的。
工作流程:滑动窗口算法维护一个固定大小的窗口队列,其中每个窗口记录了在该时间窗口内到达的请求数量。当请求到达时,首先根据当前时间确定所属的窗口,然后将请求计数器加1,并检查窗口队列中的总请求数是否超过了阈值。如果超过了阈值,则请求被拒绝;否则,请求被接受。
优点:对于控制请求速率较为精确,能够平滑地处理突发性请求。可以根据业务需求灵活地调整窗口大小和阈值。
缺点:算法的实现相对复杂一些,需要维护时间窗口队列,对于高并发场景可能会增加一定的计算和存储开销。
生产环境中的限流方案
生产环境中的限流往往比上述算法的实现更加灵活,这是因为网络环境中的请求并不是均匀分布的:有的系统流量具有周期性,与用户的工作时间或者娱乐时间密切关联,有的请求具有突发性,往往一瞬间就能耗尽整个系统中的所有资源。如果整体来看请求数量的图表(metrics dashboard / perf counter),会发现请求的数量时高时低;如果把图表放大到最小的单位,又能看到图中的折线存在一个个小的尖峰,生产环境中的限流必须要考虑到这类情况。
与此同时,在真实的生产环境中,用户的请求往往不是单一的,我们需要基于某些单元来对资源的占用进行限制,例如:在多租户系统中基于租户来对资源进行限制,在微服务系统中基于请求方的id 来对资源进行限制,在数据库(存储平台)基于表(或者数据分片)来对资源进行限制。这些实现往往是十分灵活的。另一方面,如果仅仅以某些单元来限制资源的占用,而忽略了系统整体的资源占用情况,这样也会导致系统整体的不可用。相信真正维护过线上服务的同学都曾遇到过请求把单台机器打爆的情况,在这种情况下,我们甚至无法连接到节点上进行恢复性的操作。一个生产环境的限流方案必须要考虑到这些情况。
下文介绍了一个生产环境中的限流方案,严格来讲是一个基于固定窗口的限流方案,并没有采取听起来更加先进的令牌桶或者漏斗桶的方式,分享出来提供给大家参考。
首先简单介绍一下限流器的使用背景,这个限流器主要用在单机场景,机器上维护了多个数据表,用户的请求流量往往是对单个表进行读取和写入,所以我们以表为单位进行资源的隔离。我们主要关注以下指标:
- CPU 占用量
- 线程时间(单个请求占用线程的时间)
- 网络 IO(Bytes per second)
- 硬盘读写 IO(包括固态盘和机械盘分别的读写 IO)
这些基本的指标都是我们要关注的。我们可以通过对这些指标进行统计来分析我们的系统的整体资源消耗情况。
1. 如何衡量资源的使用情况?
为了降低分配内存的开销,我们直接采用一个循环缓冲区,存储过去一定时间内资源的使用情况。这个环形缓存由一定数量的槽构成,每个槽代表着 10ms 内的资源使用情况。对于机器上每一张表,我们都创建一个这样的环形缓存,同时,我们也为整个机器创建一个同样的循环缓冲区,用来记录整台机器的资源使用情况。循环缓冲区的大小可以按需指定,比如存储 5 分钟左右的数据,我们则需要 5601000/10=3000 个槽。当请求被执行时,执行器会将资源使用情况报告到环形缓冲区中。其中,IO使用情况在请求执行结束之后报告,CPU 使用情况则是以小于槽的长度的时间间隔来定期报告。
1 | Note: 环形缓冲区是一种常用的数据结构,具有以下好处: |
基于环形缓冲区中各个 bucket 的数据,我们可以计算得到许多观测指标,比如平均资源消耗、p50、p99 等。
2. 如何限流?
为了隔离每个表的资源使用,我们对同一台机器上的每个表都构建这样一个环形缓冲区,用来记录该表可以接受请求的资源占用预算。当某个表的资源使用超出预算时,同时整台机器的资源使用超预算时,我们就对打到这个表的请求进行限流。这种操作允许单个表在机器整体资源仍然可用的情况下适当过载。具体实现如下:
对于机器上的每个表和机器本身,我们都维护一个以环形缓冲区为基础的资源窗口。窗口大小以毫秒为单位,维护一个最大值和一个最小值。最大值表示当前窗口的资源预算,每执行一次请求,我们都在这个值上减去当前消耗的资源数量。当这个值变为 0 时,表示这张表(或者这台机器)的资源已经完全耗尽。当请求继续被执行时,这个值可能会变为负数,这种情况我们称之为负债情况。最小值则是表示当前表(或者机器)可以有多少的负债。当一个时间窗口过去时,对应的资源又会增加回来到最大值。