分布式高并发服务三种常用限流方案简介
nanshan 2024-10-22 12:58 12 浏览 0 评论
服务限流场景
在高并发大流量系统中,由于并发大造成服务资源不足,负载过高,进而引发致一系列问题,这里的流量一般都是突发性的,由于系统准备不足,很难短期扩容来应对 ,进行限流是最常用的手段,所以说限流也是服务稳定性治理重要的手段。
限流可能发生在多个层面:
用户网络层:突发的流量场景如热点事件流量(秒杀事件、热门抢购,微博热搜),恶意刷流,竞对爬虫等。
内部应用层:上游服务的异常调用,脚本异常请求,失败重试策略造成流量突发。
实现限流方案
常用的限流方法主要有三种:计数器算法,漏斗桶算法,令牌桶算法。
1.计算器限流
1.1 实现原理
设计限流条件,如根据用户id/商户id/IP/UUID+请求url作为限流对象,对限流对象的每次流量访问进行全局计数,设置限流阈值(1000次/秒,10000/分钟),如果统计时间窗口期内达到阈值就进行限流。
对单机限流来说,使用全局内存计数即可,但对分布式系统需要有一个公共存储计数,redis是最佳存储方案,且redis的incr能保障原子性操作。
1.2 代码实现
//@param key string object for rate limit such as uid/ip+url
//@param fillInterval time.Duration such as 1*time.Second
//@param limitNum max int64 allowed number per fillInterval
//@return whether reach rate limit, false means reach.
func fixedWindowRateLimit(key string, fillInterval time.Duration, limitNum int64) bool {
//current tick time window
tick := int64(time.Now.Unix / int64(fillInterval.Seconds))
currentKey := fmt.Sprintf("%s_%d_%d_%d", key, fillInterval, limitNum, tick)
startCount := 0
_, err := client.SetNX(currentKey, startCount, fillInterval).Result
if err != nil {
panic(err)
}
//number in current time window
quantum, err := client.Incr(currentKey).Result
if err != nil {
panic(err)
}
if quantum > limitNum {
return false
}
return true
}
完整代码参见:
https://github.com/skyhackvip/ratelimit/blob/master/fixedwindow.go
测试代码:
func test1 {
for i := 0; i < 10; i++ {
go func {
rs := fixedWindowRateLimit("test1", 1*time.Second, 5)
fmt.Println("result is:", rs)
}
}
}
测试执行结果:
根据执行结果可以看到,1秒中有10个请求,只有5个通过,另5个被限流返回false。
这个代码实现的是固定时间窗口,有一个问题,当流量在上一个时间窗口下半段和下一个时间窗口上半段集中爆发,那么这两段组成的时间窗口内流量是会超过limit限制的。
测试代码如下,拉长时间窗口为1分钟,1分钟限流5个,前30s没流量,之后每10s一个请求:
func test2 {
fillInteval := 1 * time.Minute
var limitNum int64 = 5
waitTime := 30
fmt.Printf("time range from 0 to %d\n", waitTime)
time.Sleep(time.Duration(waitTime) * time.Second)
for i := 0; i < 10; i++ {
fmt.Printf("time range from %d to %d\n", i*10+waitTime, (i+1)*10+waitTime)
rs := fixedWindowRateLimit("test2", fillInteval, limitNum)
fmt.Println("result is:", rs)
time.Sleep(10 * time.Second)
}
}
根据执行结果可以看到,0-60s总共4个true满足1分钟窗口5个,60-120总共5个true,1个false满足限流,但30-90这1分钟的时间窗总共6个true,超过5个限制。
1.3 方案改进:使用滑动窗口
//segmentNum split inteval time into smaller segments
func slidingWindowRatelimit(key string, fillInteval time.Duration, segmentNum int64, limitNum int64) bool {
segmentInteval := fillInteval.Seconds / float64(segmentNum)
tick := float64(time.Now.Unix) / segmentInteval
currentKey := fmt.Sprintf("%s_%d_%d_%d_%f", key, fillInteval, segmentNum, limitNum, tick)
startCount := 0
_, err := client.SetNX(currentKey, startCount, fillInteval).Result
if err != nil {
panic(err)
}
quantum, err := client.Incr(currentKey).Result
if err != nil {
panic(err)
}
//add in the number of the previous time
for tickStart := segmentInteval; tickStart < fillInteval.Seconds; tickStart += segmentInteval {
tick = tick - 1
preKey := fmt.Sprintf("%s_%d_%d_%d_%f", key, fillInteval, segmentNum, limitNum, tick)
val, err := client.Get(preKey).Result
if err != nil {
val = "0"
}
num, err := strconv.ParseInt(val, 0, 64)
quantum = quantum + num
if quantum > limitNum {
client.Decr(currentKey).Result
return false
}
}
return true
}
完整代码参见:
https://github.com/skyhackvip/ratelimit/blob/master/slidingwindow.go
滑动窗口增加一个参数segmentNum,表示把固定窗口再分成几段,如上图的0-10 ... 50-60,把1分钟分成6段,代码执行结果如下,30-90,40-100,任意1分钟滑动窗口都满足5个最大限制。
1.4 计数器的适用场景
适用于做API限流,比如对外提供ip定位查询服务api,天气查询api等,可以根据ip做粒度控制,防止恶意刷接口造成异常,也适用于提供API查询服务做配额限制,一般限流后会对请求做丢弃处理。
局限:窗口算法对于流量限制是定速的,对细粒度时间控制突发流量控制能力就有限了。
2.漏斗桶限流
2.1 实现原理
漏斗桶形象比喻为一个滤水漏斗,水滴(请求)可能很快把漏斗填满(流量流入),漏斗出来的水滴(流量处理)是匀速固定的,桶满则新进入水滴(请求)会被限流。
图片来自网络
常用队列方式来实现,请求到达后放入队列中,有一个处理器从队列匀速取出进行处理。当桶满了,新流量过来会被限流。
Uber提供了基于漏斗桶的算法实现可以参考:
https://github.com/uber-go/ratelimit
另外:redis4.0提供了限流模块,redis-cell,该模块使用漏斗算法,并提供原子限流指令。
cl.throttle key capacity limitNum fillInteval
2.2 漏斗桶适用场景
漏斗桶更像是对流量进行整形Traffic Shaping,所有流量过来都要进行排队,依次出去,可用于做一些论坛博客发帖频率限制。
相对于计数器限流,达到限流后该时间窗口会丢弃一切请求,漏斗在桶满后,由于还会有持续流出,新到达请求还有机会流入。
局限:由于出口处理速率是匀速的,短时有大量突发请求,即使负载压力不大,请求仍需要在队列等待处理。
3.令牌桶限流
3.1 实现原理
令牌桶算法是一个桶,匀速向桶里放令牌,控制桶最大容量(令牌最大数)和放入令牌速率(生成令牌/秒)。请求从桶中拿令牌,拿到令牌可以通过,拿不到就被限流了。
当访问量小时,令牌桶可以积累令牌到桶满,而当短时突发流量,积累的令牌能保障大量请求可以立刻拿到令牌,令牌用完了,请求会依赖于新令牌申请速度,这时会退化成类似漏斗桶算法。
图片来自网络
具体实现上,可以使用redis的list,启动任务向list匀速放置数据,当有请求时从list取数据,取到代表通过,否则被限流。这么实现是可行的,但有个弊端,就是需要不断操作list,浪费内存空间,而实际上可以使用实时算法计算的方式来计算可用令牌数。
公式:可用令牌数=(当前请求时间-上次请求时间)*令牌生成速率 + 上次使用后剩余令牌数,当然这个数需要再和桶容量比较求小。
如果可用令牌数 > 0代表有令牌,剩余令牌数-1,并更新保存本次剩余令牌数和本次请求时间用于下次计算,这种方式也是惰性加载/计算的一种体现。
3.2 代码实现
//rate increment number per second
//capacity total number in the bucket
func bucketTokenRateLimit(key string, fillInterval time.Duration, limitNum int64, capacity int64) bool {
currentKey := fmt.Sprintf("%s_%d_%d_%d", key, fillInterval, limitNum, capacity)
numKey := "num"
lastTimeKey := "lasttime"
currentTime := time.Now.Unix
//only init once
client.HSetNX(currentKey, numKey, capacity).Result
client.HSetNX(currentKey, lastTimeKey, currentTime).Result
//compute current available number
result, _ := client.HMGet(currentKey, numKey, lastTimeKey).Result
lastNum, _ := strconv.ParseInt(result[0].(string), 0, 64)
lastTime, _ := strconv.ParseInt(result[1].(string), 0, 64)
rate := float64(limitNum) / float64(fillInterval.Seconds)
fmt.Println(rate)
incrNum := int64(math.Ceil(float64(currentTime-lastTime) * rate)) //increment number from lasttime to currenttime
fmt.Println(incrNum)
currentNum := min(lastNum+incrNum, capacity)
//can access
if currentNum > 0 {
var fields = map[string]interface{}{lastTimeKey: currentTime, numKey: currentNum - 1}
a := client.HMSet(currentKey, fields)
fmt.Println(a)
return true
}
return false
}
完整代码参见:
https://github.com/skyhackvip/ratelimit/blob/master/buckettoken.go
还有更多需要可实现细节如预热桶、一次性放入多个令牌、一次性取多个令牌。同时由于原子性问题,通过redis+lua脚本操作(lua实现令牌桶)会更好。
3.3 令牌桶适用场景
令牌桶既能够将所有请求平均分布到时间区间内,又能接受突发请求,因此使用最广泛的限流算法,像java中比较有名的guava就有实现。
4.方案对比选择
计数器 | 漏斗桶 | 令牌桶 | |
具体实现 | 使用全局计数 | 使用队列+处理器 | 使用漏斗算法 |
适用场景 | API配额/限流 适合限流后丢弃处理 | 流量整形 适合限流后阻塞排队 | 大多数场景均可 |
5.限流部署
5.1 “分布式部署” 限流单个服务实例
限流代码在应用服务内,使用aop方式(如gin的middleware),当应用请求时(request)进行拦截检查,通过则继续执行请求,否则将被限流进行处理。
func rateLimitMiddleware gin.HandlerFunc {
return func(c *gin.Context) {
bucketTokenRateLimit(c.Param("uid"))
}
}
由于应用服务是分布式集群,每个服务实例中的限流拦截器只能拦截本实例中的请求数,那么对于总体限流就需要有一定策略分摊到每个单体实例中。比如10000次/秒,服务部署10个实例,每个实例限流可以平均分配(1000次/秒),也可根据不同实例不同权重分配。
优点:可以有效防止单机突发流量导致的压垮,满足限流初衷,适合对并发做流量限制。
缺点:由于每个实例的流量不均等,可能有的实例已经限流,有的机器实例仍很空闲,牺牲部分流量。
5.2 “集中式部署”使用统一限流服务中心
5.2.1 部署统一限流中心
所有服务实例去请求统一限流中心,中心根据流量情况告知服务是否通过,这种方案最大的问题就是多了一次服务调用,同时集中限流器也会成为最大性能瓶颈。
5.2.2 限流部署在接入层
一般分布式服务都设有网关层/路由层/接入层,如果集中限流器可部署到其中,可以解决上述多调用问题。一般常用nginx + lua做网关层限流,lua脚本也可以使用上述几种算法。
优点:适合做细粒度限流或访问配额
缺点:对下游单个服务实例或依赖的服务不够平滑,仍有流量突发过载的可能,所以可以结合上面的方式一起部署,多重防护。
5.3 服务中心与单机限流结合
可以使用基于请求日志收集,分析日志,根据限流规则做限流服务,分析出限流结果后,下发限流指令(通过队列或集中配中心)到服务节点,节点进行限流控制。架构图如下:
此方案关键在于:日志处理分析的及时性,可采用flink流式计算方式。
5.4 限流规则配置
限流关键在于限流规则配置,是针对某个url还是针对一个服务,阈值应该如何设置,时间窗口如何设计,都是需要考虑的因素。
一般分几部分:接口粒度,时间粒度,最大限流数
接口粒度:限流对象可以配置多种限流策略针对服务单个实例,针对整个服务集群,针对某个接口,针对某类接口等。
时间粒度:如上述计数器算法中举例,使用1分钟做限流粒度更容易出某个小粒度时间窗口期出现异常流量。60000次/分钟,1000次/秒,10次/毫秒看似一样,但限流效果不同,时间粒度越细流量整形越好,越平滑,但也不越小越好。对秒杀类场景,瞬时流量非常大,QPS大,适合时间粒度小的。对QPS不大的场景,可以使用大的时间粒度。
最大限流数:一般需要性能压测、业务预期评估、线上监控、往期经验等来做参考设置。
更多考虑,如API接口服务针对vip用户针对普通用户,限流不同,可以用预留、权重、上限等维度进行不同调度,参考dmclock,mclock算法。
5.5 限流处理方式
限流后处理方式可以做服务降级(返回默认值、默认页面)、请求丢弃(拒绝请求)、请求排队(阻塞请求)、发送报警人工介入处理等。有直接结合服务降级熔断的如Sentinel、Hystrix。
更多参考资料
文章相关实现代码:
https://github.com/skyhackvip/ratelimit
dmclock算法参考:
https://github.com/ceph/dmclock
技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。
高可用架构
改变互联网的构建方式
相关推荐
- 实战派 | Java项目中玩转Redis6.0客户端缓存
-
铺垫首先介绍一下今天要使用到的工具Lettuce,它是一个可伸缩线程安全的redis客户端。多个线程可以共享同一个RedisConnection,利用nio框架Netty来高效地管理多个连接。放眼望向...
- 轻松掌握redis缓存穿透、击穿、雪崩问题解决方案(20230529版)
-
1、缓存穿透所谓缓存穿透就是非法传输了一个在数据库中不存在的条件,导致查询redis和数据库中都没有,并且有大量的请求进来,就会导致对数据库产生压力,解决这一问题的方法如下:1、使用空缓存解决对查询到...
- Redis与本地缓存联手:多级缓存架构的奥秘
-
多级缓存(如Redis+本地缓存)是一种在系统架构中广泛应用的提高系统性能和响应速度的技术手段,它综合利用了不同类型缓存的优势,以下为你详细介绍:基本概念本地缓存:指的是在应用程序所在的服务器内...
- 腾讯云国际站:腾讯云服务器如何配置Redis缓存?
-
本文由【云老大】TG@yunlaoda360撰写一、安装Redis使用包管理器安装(推荐)在CentOS系统中,可以通过yum包管理器安装Redis:sudoyumupdate-...
- Spring Boot3 整合 Redis 实现数据缓存,你做对了吗?
-
你是否在开发互联网大厂后端项目时,遇到过系统响应速度慢的问题?当高并发请求涌入,数据库压力剧增,响应时间拉长,用户体验直线下降。相信不少后端开发同行都被这个问题困扰过。其实,通过在SpringBo...
- 【Redis】Redis应用问题-缓存穿透缓存击穿、缓存雪崩及解决方案
-
在我们使用redis时,也会存在一些问题,导致请求直接打到数据库上,导致数据库挂掉。下面我们来说说这些问题及解决方案。1、缓存穿透1.1场景一个请求进来后,先去redis进行查找,redis存在,则...
- Spring boot 整合Redis缓存你了解多少
-
在前一篇里面讲到了Redis缓存击穿、缓存穿透、缓存雪崩这三者区别,接下来我们讲解Springboot整合Redis中的一些知识点:之前遇到过,有的了四五年,甚至更长时间的后端Java开发,并且...
- 揭秘!Redis 缓存与数据库一致性问题的终极解决方案
-
在现代软件开发中,Redis作为一款高性能的缓存数据库,被广泛应用于提升系统的响应速度和吞吐量。然而,缓存与数据库之间的数据一致性问题,一直是开发者们面临的一大挑战。本文将深入探讨Redis缓存...
- 高并发下Spring Cache缓存穿透?我用Caffeine+Redis破局
-
一、什么是缓存穿透?缓存穿透是指查询一个根本不存在的数据,导致请求直接穿透缓存层到达数据库,可能压垮数据库的现象。在高并发场景下,这尤其危险。典型场景:恶意攻击:故意查询不存在的ID(如负数或超大数值...
- Redis缓存三剑客:穿透、雪崩、击穿—手把手教你解决
-
缓存穿透菜小弟:我先问问什么是缓存穿透?我听说是缓存查不到,直接去查数据库了。表哥:没错。缓存穿透是指查询一个缓存中不存在且数据库中也不存在的数据,导致每次请求都直接访问数据库的行为。这种行为会让缓存...
- Redis中缓存穿透问题与解决方法
-
缓存穿透问题概述在Redis作为缓存使用时,缓存穿透是常见问题。正常查询流程是先从Redis缓存获取数据,若有则直接使用;若没有则去数据库查询,查到后存入缓存。但当请求的数据在缓存和数据库中都...
- Redis客户端缓存的几种实现方式
-
前言:Redis作为当今最流行的内存数据库和缓存系统,被广泛应用于各类应用场景。然而,即使Redis本身性能卓越,在高并发场景下,应用于Redis服务器之间的网络通信仍可能成为性能瓶颈。所以客户端缓存...
- Nginx合集-常用功能指导
-
1)启动、重启以及停止nginx进入sbin目录之后,输入以下命令#启动nginx./nginx#指定配置文件启动nginx./nginx-c/usr/local/nginx/conf/n...
- 腾讯云国际站:腾讯云怎么提升服务器速度?
-
本文由【云老大】TG@yunlaoda360撰写升级服务器规格选择更高性能的CPU、内存和带宽,以提供更好的处理能力和网络性能。优化网络配置调整网络接口卡(NIC)驱动,优化TCP/IP参数...
- 雷霆一击服务器管理员教程
-
本文转载莱卡云游戏服务器雷霆一击管理员教程(搜索莱卡云面版可搜到)首先你需要给服务器设置管理员密码,默认是空的管理员密码在启动页面进行设置设置完成后你需要重启服务器才可生效加入游戏后,点击键盘左上角E...
你 发表评论:
欢迎- 一周热门
-
-
爱折腾的特斯拉车主必看!手把手教你TESLAMATE的备份和恢复
-
如何在安装前及安装后修改黑群晖的Mac地址和Sn系列号
-
[常用工具] OpenCV_contrib库在windows下编译使用指南
-
WindowsServer2022|配置NTP服务器的命令
-
Ubuntu系统Daphne + Nginx + supervisor部署Django项目
-
WIN11 安装配置 linux 子系统 Ubuntu 图形界面 桌面系统
-
解决Linux终端中“-bash: nano: command not found”问题
-
Linux 中的文件描述符是什么?(linux 打开文件表 文件描述符)
-
NBA 2K25虚拟内存不足/爆内存/内存占用100% 一文速解
-
K3s禁用Service Load Balancer,解决获取浏览器IP不正确问题
-
- 最近发表
-
- 实战派 | Java项目中玩转Redis6.0客户端缓存
- 轻松掌握redis缓存穿透、击穿、雪崩问题解决方案(20230529版)
- Redis与本地缓存联手:多级缓存架构的奥秘
- 腾讯云国际站:腾讯云服务器如何配置Redis缓存?
- Spring Boot3 整合 Redis 实现数据缓存,你做对了吗?
- 【Redis】Redis应用问题-缓存穿透缓存击穿、缓存雪崩及解决方案
- Spring boot 整合Redis缓存你了解多少
- 揭秘!Redis 缓存与数据库一致性问题的终极解决方案
- 高并发下Spring Cache缓存穿透?我用Caffeine+Redis破局
- Redis缓存三剑客:穿透、雪崩、击穿—手把手教你解决
- 标签列表
-
- linux 查询端口号 (58)
- docker映射容器目录到宿主机 (66)
- 杀端口 (60)
- yum更换阿里源 (62)
- internet explorer 增强的安全配置已启用 (65)
- linux自动挂载 (56)
- 禁用selinux (55)
- sysv-rc-conf (69)
- ubuntu防火墙状态查看 (64)
- windows server 2022激活密钥 (56)
- 无法与服务器建立安全连接是什么意思 (74)
- 443/80端口被占用怎么解决 (56)
- ping无法访问目标主机怎么解决 (58)
- fdatasync (59)
- 405 not allowed (56)
- 免备案虚拟主机zxhost (55)
- linux根据pid查看进程 (60)
- dhcp工具 (62)
- mysql 1045 (57)
- 宝塔远程工具 (56)
- ssh服务器拒绝了密码 请再试一次 (56)
- ubuntu卸载docker (56)
- linux查看nginx状态 (63)
- tomcat 乱码 (76)
- 2008r2激活序列号 (65)