限流实现-专题一

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,经济版 1GB 1个月
简介: 在实际业务中,经常会碰到突发流量的情况。如果公司基础架构做的不好,服务无法自动扩容缩容,在突发高流量情况下,服务会因为压力过大而崩溃。更恐怖的是,服务崩溃如同多米诺骨牌,一个服务出问题,可能影响到整个公司所有组的业务。

在实际业务中,经常会碰到突发流量的情况。如果公司基础架构做的不好,服务无法自动扩容缩容,在突发高流量情况下,服务会因为压力过大而崩溃。更恐怖的是,服务崩溃如同多米诺骨牌,一个服务出问题,可能影响到整个公司所有组的业务。

​ 为了减少损失,比较常用的方案是限流和熔断。本文会讲述常用的几种方法:

  • 随机拒流
  • 计数器方式
  • 基于滑动时间窗口限流
  • 漏斗算法
  • 令牌桶算法
  • Hystrix

Hystrix主要用来做熔断处理,不过从另一个维度而言,也算实现了限流的功能,就放在一起讨论了。这些方法都会用go写一个实现,鉴于自己时间有限,分三到四期写完。本期先实现随机拒流、计数器方式、基于滑动时间窗口限流。

Go使用的Gin框架,并且引入了swagger,如果大家不熟悉,可以看一下我的这三篇文章

  1. Gin源码剖析https://mp.weixin.qq.com/s/g_MQldFaMmvnhSsCIMJ7xg
  2. Gin简单实现https://mp.weixin.qq.com/s/X9pyPZU63j5FF4SDT4sHew
  3. 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层的业务组,在发现流量不可控情况下,按比例开启随机拒流,在部分影响业务的情况下,保证生产的正常运行,另外也保护了后端服务。

代码实现如下:

package limit

import (
   "github.com/gin-gonic/gin"
   "math/rand"
   "net/http"
)

// @Tags limit
// @Summary 随机拒流
// @Produce  json
// @Success 200 {string} string "成功会返回ok"
// @Failure 502 "失败返回reject"
// @Router /limit/randomreject [get]
func RandomReject(c *gin.Context) {
   refuseRate := 200
   if refuseRate != 0 {
      temp := rand.Intn(1000)
      if temp <= refuseRate {
         c.String(http.StatusBadGateway, "reject")
         return
      }
   }
   c.String(http.StatusOK, "ok")
}

说明:refuseRate通常读取的是配置数据,通过refuseRate的值,可以控制拒流的比例。线上多次出现过大流量情况,开始随机拒流开关后,效果显著。

计数器

计数器使用分布式方案,此处用到了Redis,在本机调试的时候需要安装Redis,执行如下操作:

  1. 修改redis.conf的\#requirepass foobared可设置auth,修改后启动时需要加载该文件
  2. 启动redis命令:redis-server /usr/local/Cellar/redis/6.0.1/.bottle/etc/redis.conf
  3. 登录命令:redis-cli -h 127.0.0.1 -a 111111

代码实现如下:

package limit

import (
    "asap/aredis"
    "fmt"
    "github.com/gin-gonic/gin"
    "net/http"
    "time"
)

// @Tags limit
// @Summary 计数拒流,每秒超过指定次数会拒流掉
// @Produce  json
// @Success 200 {string} string "成功会返回ok"
// @Failure 502 "失败返回reject"
// @Router /limit/countreject [get]
func CountReject(c *gin.Context) {
    currentTime := time.Now().Unix()
    key := fmt.Sprintf("count:%d", currentTime)
    limitCount := 1
    fmt.Println(key)
    trafficCount, _ := aredis.GetRedis(aredis.BASEREDIS).Incr(key)
    if trafficCount == 1 {
        aredis.GetRedis(aredis.BASEREDIS).Expire(key, 86400)
    }
    if int(trafficCount) > limitCount {
        c.String(http.StatusOK, "reject")
        return
    }
    c.String(http.StatusOK, "ok")
}

说明:

  1. 实现计数器算法需要用到reids,这样能够确保整个服务在每秒内访问次数没有超过指定数值
  2. 使用时间戳为key
  3. 这种方案有个缺点,有可能在1s内最多有2倍的请求被处理,举个例子,在当前这一秒的后半秒,处理数量打满,在下一秒的前半秒,处理数量打满,这样在1s的时间内,处理了两倍的请求,某些极端情况下,仍然会导致服务崩溃。

当然,虽然有上面说的这个问题,不过一般而言问题不大,因为流量大多都是比较均匀的。不过有个场景需要大家关注一下,如果有用户刷接口,使用该方法会导致大量正常用户无法正常使用系统,遇到这种情况,可以使用随机方案,另外可以迅速找出用户,将其封禁,这自然也需要健壮的基础设施支持。

基于滑动时间窗口限流

计数器方法有可能导致1s内的流量超过指定阈值,使用基于滑动时间窗口限流方案可以解决这个问题。


滑动时间窗口的逻辑很简单,就是将单位时间拆分成更小的块,计算在滑动的单位时间内数值是否超过阈值。举个例子,我们将1s拆分为10小块,则每小块时间长度为100ms,假设我们设置的阈值是500/s,极端情况下,第一秒的前九个小块都没有流量,第十个小块有500流量,时间往后移动,在第二秒的第一个小块,因为和第一秒的第十个小块在一个滑动窗口内,所以会将所有流量拒掉,只有时间移动到第二秒第十个小块时,系统才能继续处理请求。

代码实现如下:

package limit

import (
   "container/ring"
   "github.com/gin-gonic/gin"
   "net/http"
   "sync"
   "sync/atomic"
   "time"
   "fmt"
)

var (
   limitCount int = 5 // 1s限频
   limitBucket int = 10 // 滑动窗口个数
   curCount int32 = 0  // 记录限频数量
   head *ring.Ring     // 环形队列(链表)
   printRes = 0
)

func init(){
   // 初始化滑动窗口
   head = ring.New(limitBucket)
   for i := 0; i < limitBucket; i++ {
      head.Value = 0
      head = head.Next()
   }
   // 启动执行器
   go func() {
      //ms级别,limitBucket int = 10意味将每秒分为10份,每份100ms
      timer := time.NewTicker(time.Millisecond * time.Duration(1000/limitBucket))
      for range timer.C { // 定时每隔指定时间刷新一次滑动窗口数据
         //subCount的作用,是因为当移动到head的时候,意味着该head要被废弃了。所以总count的值需要减去
         //head的值,并将head的值重新赋值为0
         subCount := int32(0 - head.Value.(int))
         newCount := atomic.AddInt32(&curCount, subCount)

         arr := make([]int,limitBucket)
         for i := 0; i < limitBucket; i++ { //打印出当前每个窗口的请求数量
            arr[i] = head.Value.(int)
            head = head.Next()
         }
         if printRes == 1 {
            fmt.Println("move subCount,newCount,arr", subCount, newCount,arr)
         }
         head.Value = 0
         head = head.Next()
      }
   }()
}
// @Tags limit
// @Summary 滑动窗口计数拒流,每秒超过指定次数会拒流掉
// @Produce  json
// @Success 200 {string} string "成功会返回ok"
// @Failure 502 "失败返回reject"
// @Router /limit/slidewindowsreject [get]
func SlideWindowsReject(c *gin.Context){
   n := atomic.AddInt32(&curCount, 1)
   if n > int32(limitCount) { // 超出限频
      atomic.AddInt32(&curCount, -1) //将多增加的数据减少
      c.String(http.StatusBadGateway, "reject")
   } else {
      mu := sync.Mutex{}
      mu.Lock()
      pos := head.Prev()
      val := pos.Value.(int)
      val++
      pos.Value = val
      mu.Unlock()
      c.String(http.StatusOK, "ok")
   }
}

说明:

  1. 本算法使用go的ring实现
  2. 每次head移动,意味当前head已经过期,需要将总计数减去该head的计数,将head的值重新设置为0
  3. 在改变总计数的时候,使用了原子操作保证了了计数的准确性
  4. 在更改ring中计数的时候,需要使用锁,防止数据不一致。性能压测结果表明性能可用。
  5. 本算法使用了定时器,算是比较取巧的一种方案

资料

  1. https://www.cnblogs.com/xiangxiaolin/p/12386775.html
  2. https://jingyan.baidu.com/article/d5a880ebdbed2113f047cc4e.html 安装redis服务
  3. https://www.h3399.cn/201906/702263.html 设置auth
  4. https://blog.csdn.net/Hedon954/article/details/107146301/ Mac 上安装 Redis 和配置密码详细过程
  5. https://blog.csdn.net/gx864102252/article/details/102213616 Go实现滑动窗口限频
  6. http://docscn.studygolang.com/pkg/container/ring/
  7. 限频方案比较
  8. https://blog.csdn.net/micl200110041/article/details/82013032 Golang实现请求限流的几种办法
  9. https://blog.csdn.net/u014691098/article/details/105601511 滑动窗口实现访问频率限制
  10. 限流算法之滑动窗口法
  11. https://zhuanlan.zhihu.com/p/85166364

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

往期文章回顾:

  1. 限流实现1
  2. 对产品经理的一些思考
  3. Redis实现分布式锁
  4. Golang源码BUG追查
  5. 事务原子性、一致性、持久性的实现原理
  6. 如何锻炼自己的记忆力
  7. CDN请求过程详解
  8. 关于程序员职业发展的思考
  9. 记博客服务被压垮的历程
  10. 常用缓存技巧
  11. 如何高效对接第三方支付
  12. Gin框架简洁版
  13. 关于代码review的思考
  14. InnoDB锁与事务简析
  15. Markdown编辑器推荐-typora
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
缓存 算法 Java
限流算法 - 基本实现
限流算法 - 基本实现
47 0
|
11月前
|
消息中间件 算法 Sentinel
只需5分钟,了解常见的四种限流算法
只需5分钟,了解常见的四种限流算法
216 4
|
2月前
|
存储 算法 NoSQL
常见限流算法及其实现
在分布式系统中,随着业务量的增长,如何保护核心资源、防止系统过载、保证系统的稳定性成为了一个重要的问题。限流算法作为一种有效的流量控制手段,被广泛应用于各类系统中。本文将详细介绍四种常见的限流算法、两种常用的限流器工具,从原理、源码的角度进行分析。
235 0
|
2月前
|
缓存 Java 应用服务中间件
常见的限流降级方案
【1月更文挑战第21天】
|
2月前
|
算法 Go API
限流算法~
限流算法~
47 1
|
2月前
|
缓存 算法 NoSQL
常见限流算法解读
常见限流算法解读
|
缓存 算法 网络协议
限流实现2
剩下的几种本来打算能立即写完,没想到一下三个月过去了,很是尴尬。本次主要实现如下两种算法 - 令牌桶算法 - 漏斗算法
|
算法 NoSQL API
限流功能的实现
限流功能的实现
166 0
|
监控 算法 安全
限流
1. 为什么需要限流 2. 如何限流 限流主要就是考虑这两点
229 0
限流
|
算法 Java API