以下文章来源于技术岁月 ,作者贺鹏Kavin

在高并发大流量系统中,由于并发大造成服务资源不足,负载过高,进而引发致一系列问题,这里的流量一般都是突发性的,由于系统准备不足,很难短期扩容来应对 ,进行限流是最常用的手段,所以说限流也是服务稳定性治理重要的手段。

限流可能发生在多个层面:

  1. 用户网络层:突发的流量场景如热点事件流量(秒杀事件、热门抢购,微博热搜),恶意刷流,竞对爬虫等。

  2. 内部应用层:上游服务的异常调用,脚本异常请求,失败重试策略造成流量突发。

实现限流方案

常用的限流方法主要有三种:计数器算法,漏斗桶算法,令牌桶算法。

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 windowtick := 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 windowquantum, 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.Minutevar limitNum int64 = 5waitTime := 30fmt.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()) / segmentIntevalcurrentKey := 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 timefor 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 onceclient.HSetNX(currentKey, numKey, capacity).Result()client.HSetNX(currentKey, lastTimeKey, currentTime).Result()
//compute current available numberresult, _ := 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 currenttimefmt.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内存模型深入分析

  • 改善 Java 代码质量的工具与方法

  • Java 语言中锁的设计与应用

  • 如何搭建一个大数据平台:从新项目到成熟阶段

  • 建立主动性团队文化:如何打造鼓励失败的氛围

技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

分布式高并发服务三种常用限流方案简介相关推荐

  1. 可能要用心学高并发核心编程,限流原理与实战,分布式令牌桶限流

    实战:分布式令牌桶限流 本节介绍的分布式令牌桶限流通过Lua+Java结合完成,首先在Lua脚本中完成限流的计算,然后在Java代码中进行组织和调用. 分布式令牌桶限流Lua脚本 分布式令牌桶限流Lu ...

  2. 高并发系统处理之——限流

    高并发系统处理之--限流 对于高并发应用服务,有三个很好的方案可以保护系统 1.缓存 缓存的目的是提升系统访问速度和增大系统处理容量 2.降级 降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉 ...

  3. springcloud 并发_SpringCloud-Zuul高并发请求下的限流处理

    高并发的情况肯定是实际场景中经常碰到的情况,那么这种情况下限流措施肯定是必须的,本文我们就来看看zuul中限流的处理 高并发的限流处理 1.创建项目 创建一个SpringCloud项目 2.添加依赖 ...

  4. 程序员修神之路--高并发优雅的做限流(有福利)

    点击上方蓝色字体,关注我们 菜菜哥,有时间吗? YY妹,什么事? 我最近的任务是做个小的秒杀活动,我怕把后端接口压垮,X总说这可关系到公司的存亡 简单呀,你就做个限流呗 这个没做过呀,菜菜哥,帮妹子写 ...

  5. 高并发场景下的限流策略

    点击上方"方志朋",选择"置顶公众号" 技术文章第一时间送达! 在高并发的场景下,我们的优化和保护系统的方式通常有:多级缓存.资源隔离.熔断降级.限流等等. 今 ...

  6. 高并发系统中的限流应该如何做?

    缓存 缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪. 使用缓存不单单能够提升系统访问速度.提高并发访问量,也是保护数据库.保护系统的有效方式.大型网站一般主要是 ...

  7. 高并发解决方案之“Nginx限流”

    本文将分4个步骤讲解: 1.api压力测试 2.查看api响应性能 3.nginx限流进行优化 4.查看优化结果 1 api压力测试 1.1 安装压测工具 yum -y install httpd-t ...

  8. 深入理解 RPC : 基于 Python 自建分布式高并发 RPC 服务

    RPC(Remote Procedure Call)服务,也即远程过程调用,在互联网企业技术架构中占据了举足轻重的地位,尤其在当下微服务化逐步成为大中型分布式系统架构的主流背景下,RPC 更扮演了重要 ...

  9. 基于 Python 自建分布式高并发 RPC 服务

    RPC(Remote Procedure Call)服务,也即远程过程调用,在互联网企业技术架构中占据了举足轻重的地位,尤其在当下微服务化逐步成为大中型分布式系统架构的主流背景下,RPC 更扮演了重要 ...

最新文章

  1. unity人物旋转移动代码_Unity3D研究院之脚本实现模型的平移与旋转(六)
  2. 我来分析委托的协变与逆变
  3. js如何动态向 fileaddress: [fromurl]添加数据_N+增强能力系列(3) | 动态KV模块
  4. reflective dll injection 反射注入
  5. 方舟原始恐惧mod生物代码_重磅!命令与征服和红色警戒源代码在GitHub公布了
  6. 物料评估类型视图扩充
  7. mysql定位前后端问题_Web 前后端分离的意义大吗?
  8. c++与java,有什么区别?
  9. What he did
  10. 你还认为中国没有桌面虚拟化核心技术?
  11. java spark读写hdfs文件,Spark1.4从HDFS读取文件运行Java语言WordCounts
  12. 【转】粒子群算法----粒子群算法简介
  13. 知识分享|日本面试常考问题+巧妙回答 ②
  14. 非平稳时间序列突变检测 -- Bernaola Galvan分割算法
  15. adc0808模数转换实验报告_AD转换程序(ADC0808 TLC2543)
  16. XMAPP 的安装与配置
  17. java web 开发资料链接
  18. 2013年MBA、MPA、MPAcc入学考试英语辅导教材
  19. DSP、SSP、RTB的理解--计算广告
  20. 痛彻心扉:学了半年 Python,还是找不到工作!

热门文章

  1. k8s UAT改环境
  2. 基于matlab数字滤波器设计,毕业设计 基于MATLAB的数字滤波器设计
  3. Android配置文件,所有权限ZZ
  4. 独立循环神经网络(indRNN)
  5. 微信打开页面,下载东西时调用其他浏览器下载
  6. qq邮箱服务器发信怎么配置,WordPress网站实现使用QQ邮箱作为SMTP发信服务器配置教程...
  7. 宽带认证计费系统的认证技术主要有哪些
  8. 数据结构:链表逆序输出
  9. 访问Oracle数据库的四款工具软件介绍
  10. 进制转换(计算机基础)