在实际业务中,经常会碰到突发流量的情况。如果公司基础架构做的不好,服务无法自动扩容缩容,在突发高流量情况下,服务会因为压力过大而崩溃。更恐怖的是,服务崩溃如同多米诺骨牌,一个服务出问题,可能影响到整个公司所有组的业务。
为了减少损失,比较常用的方案是限流和熔断。本文会讲述常用的几种方法:
- 随机拒流
- 计数器方式
- 基于滑动时间窗口限流
- 漏斗算法
- 令牌桶算法
- Hystrix
Hystrix主要用来做熔断处理,不过从另一个维度而言,也算实现了限流的功能,就放在一起讨论了。这些方法都会用go写一个实现,鉴于自己时间有限,分三到四期写完。本期先实现随机拒流、计数器方式、基于滑动时间窗口限流。
Go使用的Gin框架,并且引入了swagger,如果大家不熟悉,可以看一下我的这三篇文章
- Gin源码剖析https://mp.weixin.qq.com/s/g_MQldFaMmvnhSsCIMJ7xg
- Gin简单实现https://mp.weixin.qq.com/s/X9pyPZU63j5FF4SDT4sHew
- Gin框架集成swagger过程
具体代码可以查看https://github.com/shidawuhen/asap/tree/master
代码里做了简单的单元测试和性能测试,大家可以执行如下命令查看效果
go test controller/limit/randomReject_test.go
go test -v -test.bench controller/limit/slidewindowsReject*
限流有分布式限流和单机限流两种大类,分布式限流是将集群看做整体,一般需要用到其他分布式工具,如Redis等,单机限流是将每个机器单做独立物理单元。两种方式各有利弊,分布式限流复杂度会更高一些,而且一般需要保证机器时间一致,单机限流实现相对简单,但是在扩缩容后需要更改限流的阈值。
随机拒流和基于滑动窗口的拒流,使用的是单机限流方案,技术限流使用的是分布式方案。
如下是三个方案的实现:
随机拒流
随机拒流代码最简单,也往往是最有效的。
一般的服务架构如下图所示,有一个客户端,有一个BFF层直接和客户端交互,BFF后面隐藏了公司内部的众多服务。
随机拒流一般用于客户端和BFF层之间。负责BFF层的业务组,在发现流量不可控情况下,按比例开启随机拒流,在部分影响业务的情况下,保证生产的正常运行,另外也保护了后端服务。
代码实现如下:
1 | package limit |
说明:refuseRate通常读取的是配置数据,通过refuseRate的值,可以控制拒流的比例。线上多次出现过大流量情况,开始随机拒流开关后,效果显著。
计数器
计数器使用分布式方案,此处用到了Redis,在本机调试的时候需要安装Redis,执行如下操作:
- 修改redis.conf的#requirepass foobared可设置auth,修改后启动时需要加载该文件
- 启动redis命令:redis-server /usr/local/Cellar/redis/6.0.1/.bottle/etc/redis.conf
- 登录命令:redis-cli -h 127.0.0.1 -a 111111
代码实现如下:
1 | package limit |
说明:
- 实现计数器算法需要用到reids,这样能够确保整个服务在每秒内访问次数没有超过指定数值
- 使用时间戳为key
- 这种方案有个缺点,有可能在1s内最多有2倍的请求被处理,举个例子,在当前这一秒的后半秒,处理数量打满,在下一秒的前半秒,处理数量打满,这样在1s的时间内,处理了两倍的请求,某些极端情况下,仍然会导致服务崩溃。
当然,虽然有上面说的这个问题,不过一般而言问题不大,因为流量大多都是比较均匀的。不过有个场景需要大家关注一下,如果有用户刷接口,使用该方法会导致大量正常用户无法正常使用系统,遇到这种情况,可以使用随机方案,另外可以迅速找出用户,将其封禁,这自然也需要健壮的基础设施支持。
基于滑动时间窗口限流
计数器方法有可能导致1s内的流量超过指定阈值,使用基于滑动时间窗口限流方案可以解决这个问题。
滑动时间窗口的逻辑很简单,就是将单位时间拆分成更小的块,计算在滑动的单位时间内数值是否超过阈值。举个例子,我们将1s拆分为10小块,则每小块时间长度为100ms,假设我们设置的阈值是500/s,极端情况下,第一秒的前九个小块都没有流量,第十个小块有500流量,时间往后移动,在第二秒的第一个小块,因为和第一秒的第十个小块在一个滑动窗口内,所以会将所有流量拒掉,只有时间移动到第二秒第十个小块时,系统才能继续处理请求。
代码实现如下:
1 | package limit |
说明:
- 本算法使用go的ring实现
- 每次head移动,意味当前head已经过期,需要将总计数减去该head的计数,将head的值重新设置为0
- 在改变总计数的时候,使用了原子操作保证了了计数的准确性
- 在更改ring中计数的时候,需要使用锁,防止数据不一致。性能压测结果表明性能可用。
- 本算法使用了定时器,算是比较取巧的一种方案
资料
- https://www.cnblogs.com/xiangxiaolin/p/12386775.html
- https://jingyan.baidu.com/article/d5a880ebdbed2113f047cc4e.html 安装redis服务
- https://www.h3399.cn/201906/702263.html 设置auth
- https://blog.csdn.net/Hedon954/article/details/107146301/ Mac 上安装 Redis 和配置密码详细过程
- https://blog.csdn.net/gx864102252/article/details/102213616 Go实现滑动窗口限频
- http://docscn.studygolang.com/pkg/container/ring/
- 限频方案比较
- https://blog.csdn.net/micl200110041/article/details/82013032 Golang实现请求限流的几种办法
- https://blog.csdn.net/u014691098/article/details/105601511 滑动窗口实现访问频率限制
- 限流算法之滑动窗口法
- https://zhuanlan.zhihu.com/p/85166364