作为开发者,你是否曾陷入这样的困境: 在流量洪峰来临的瞬间,亲眼目睹服务雪崩?当每秒请求量突破系统临界点时,再强悍的服务器都会变成脆弱的瓷器。而 限流 技术,正是守护系统的最后一道智能保险丝——它不会让服务更快,但能确保你在流量海啸中活着上岸。
限流就是对请求或并发数进行限制,通过对一定时间内的请求量进行限制来保障系统的正常运行
固定窗口算法又叫计数器算法,是一种简单方便的限流算法
主要通过一个支持原子操作的计数器来累计 1 秒内的请求次数,当 1 秒内计数达到限流阈值时触发拒绝策略,每过 1 秒,计数器重置为 0 开始重新计数
首先维护一个计数器,将单位时间段当做一个窗口,计数器记录这个窗口接收请求的次数。
当次数少于限流阀值,就允许访问,并且计数器+1,当次数大于限流阀值,就拒绝访问。
当前的时间窗口过去之后,计数器清零。
一段时间内(不超过时间窗口)系统服务不可用
比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第1ms来了100个请求,然后第2ms-999ms的请求就都会被拒绝,这段时间用户会感觉系统服务不可用。
窗口切换时可能会产生两倍于阈值流量的请求
假设限流阀值为5个请求,单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s,分别并发5个请求。虽然都没有超过阀值,但是如果算0.8-1.2s,则并发数高达10,已经超过单位时间1s不超过5阀值的定义啦,通过的请求达到了阈值的两倍。
滑动窗口限流解决固定窗口临界值的问题,可以保证在任意时间窗口内都不会超过阈值
相对于固定窗口,滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点,因此对内存的占用会比较多
记录每次请求的时间,统计每次请求的时间 至 往前推1秒这个时间窗口内请求数,并且 1 秒前的数据可以删除。
统计的请求数小于阈值就记录这个请求的时间,并允许通过,反之拒绝。
它将时间窗⼝划分为更⼩的时间⽚段,每过⼀个时间⽚段,时间窗⼝就会往右滑动⼀格,每个时间⽚段都有独⽴的计数器。
我们在计算整个时间窗⼝内的请求总数时会累加所有的时间⽚段内的计数器。
时间窗⼝划分的越细,那么滑动窗⼝的滚动就越平滑,限流的统计就会越精确
滑动窗口算法其实就是对请求数进行了更细粒度的限流,窗口划分的越多,则限流越精准。
注意
滑动窗口 和 固定窗口都无法解决短时间之内集中流量的突击
原理很简单,可以认为就是注水漏水的过程,往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。
流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。
桶的容量一般表示系统所能处理的请求数,如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)。流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。
和消息队列思想有点像,削峰填谷
面对突发请求,服务的处理速度和平时是一样的,这其实不是我们想要的,在面对突发流量我们希望在系统平稳的同时,提升用户体验即能更快的处理请求,而不是和正常流量一样,循规蹈矩的处理(看看,之前滑动窗口说流量不够平滑,现在太平滑了又不行,难搞啊)。
令牌桶算法是对漏斗算法的一种改进,除了能够起到限流的作用外,还允许一定程度的流量突发。
有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑,如果拿不到令牌,就直接拒绝这个请求。
Guava包中提供了令牌桶算法。
可以看出令牌桶在应对突发流量的时候,桶内假如有 100 个令牌,那么这 100 个令牌可以马上被取走,而不像漏桶那样匀速的消费。所以在应对突发流量的时候令牌桶表现的更佳。
记录下所有的请求时间点
新请求到来时先判断最近指定时间范围内的请求数量是否超过指定阈值,由此来确定是否达到限流
可以通过 Redis 的 Zset 实现。
这种方式没有了时间窗口突变的问题,限流比较准确,但是因为要记录下每次请求的时间点,所以占用的内存较多。
可以看到每个限流都有个阈值,这个阈值如何定是个难点,定大了服务器可能顶不住,定小了就“误杀”了,没有资源利用最大化,对用户体验不好
我能想到的就是限流上线之后先预估个大概的阈值,然后不执行真正的限流操作,而是采取日志记录方式,对日志进行分析查看限流的效果,然后调整阈值,推算出集群总的处理能力,和每台机子的处理能力(方便扩缩容)
然后将线上的流量进行重放,测试真正的限流效果,最终阈值确定,然后上线
固定窗口算法 实现简单,性能高,但是会有临界突发流量问题,瞬时流量最大可以达到阈值的2倍
为了解决临界突发流量,可以将窗口划分为多个更细粒度的单元,每次窗口向右移动一个单元,于是便有了 滑动窗口算法
滑动窗口当流量到达阈值时会瞬间掐断流量,所以导致流量不够平滑,想要达到限流的目的,又不会掐断流量,使得流量更加平滑?可以考虑 漏桶算法!需要注意的是,漏桶算法通常配置一个FIFO的队列使用以达到允许限流的作用
由于速率固定,即使在某个时刻下游处理能力过剩,也不能得到很好的利用,这是漏桶算法的一个短板
限流和瞬时流量其实并不矛盾,在大多数场景中,短时间突发流量系统是完全可以接受的,令牌桶算法 就是不二之选了,令牌桶以固定的速率v产生令牌放入一个固定容量为n的桶中,当请求到达时尝试从桶中获取令牌,当桶满时,允许最大瞬时流量为n;当桶中没有剩余流量时则限流速率最低,为令牌生成的速率v
如何实现更加灵活的多级限流呢?滑动日志限流算法了解一下!这里的日志则是请求的时间戳,通过计算制定时间段内请求总数来实现灵活的限流。当然,由于需要存储时间戳信息,其占用的存储空间要比其他限流算法要大得多。
如何选择合适的限流算法呢?不妨从 性能,是否允许超出阈值,落地成本,流量平滑度,是否允许突发流量以及系统资源大小限制 等多方面考虑
本文作者:柳始恭
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!