diva-notes
  • README
  • Ads
    • 定价策略
    • 广告层级
    • 归因模型
    • 买量
    • Chat GPT
    • Google
  • AI
    • 参考资料
    • Chat GPT
    • stable-diffusion-webui安装
  • Algorithm
    • 倍增
    • 并查集
    • 参考
    • 环的判断
    • 凸包
    • 蓄水池抽样
    • 最短路径
    • 最小生成树
    • KMP算法
    • Rabin-Karp算法
    • Tarjan桥算法
  • Architecture
    • Serverless
  • Career
  • CICD
    • 代码质量
    • CICD实践
  • Data Structure
    • 布谷鸟过滤器
    • 布隆过滤器
    • 浮点
    • 红黑树
    • 锁
    • LSM树
  • DB
    • My SQL
      • 隔离级别
      • 架构
      • 索引
      • 锁
      • 页结构
      • 主从同步
      • ACID
      • Log
      • MVCC
      • Questions
    • Postgres
      • 持久化
      • 对比MySQL
      • 隔离级别
      • 索引
      • Greenpulm
      • MVCC
    • 倒排索引
    • 列式存储
    • H Base
    • HDFS
    • MPP数据库选型
    • Questions
  • Distributed System
    • 分布式事务
    • 服务网格
    • BASE理论
    • CAP
    • Etcd
    • Raft协议
    • ZAB协议
  • Go
    • 1.语言基础
      • 1.CPU寄存器
      • 2-1.函数调用
      • 2-2.函数调用栈
      • 2.接口
      • 3.汇编
      • 4.调试
    • 2.编译
      • 1.编译
      • 2.词法与语法分析
      • 3.类型检查
      • 4.中间代码生成
      • 5.机器码生成
    • 3.数据结构
      • 1.数组array
      • 2.切片slice
      • 3.哈希表map
      • 4.字符串
    • 4.常用关键字
      • 1.循环
      • 2.defer
      • 3.panic和recover
      • 4.make和new
    • 5.并发编程
      • 1.上下文Context的实现
      • 2-1.runtime.sema信号量
      • 2-2.sync.Mutex的实现
      • 2-3.sync.WaitGroup
      • 2-4.sync.Once的实现
      • 2-5.sync.Map的实现
      • 2-6.sync.Cond
      • 2-7.sync.Pool的实现
      • 2-8.sync.Semaphore的实现
      • 2-9.sync.ErrGroup
      • 3.定时器Timer的实现
      • 4.Channel的实现
      • 5-1.调度-线程
      • 5-2.调度-MPG
      • 5-3.调度-程序及调度启动
      • 5-4.调度-调度策略
      • 5-5.调度-抢占
      • 6.netpoll实现
      • 7.atomic
    • 6.内存管理
      • 1-1.内存分配基础-TCmalloc
      • 1-2.内存分配
      • 2.垃圾回收
      • 3.栈内存管理
    • 参考
    • 各版本特性
    • 坑
    • Go程序性能优化
    • http.Client
    • net.http路由
    • profile采样的实现
    • Questions
    • time的设计
  • Kafka
    • 高可用
    • 架构
    • 消息队列选型
    • ISR
    • Questions
  • Network
    • ARP
    • DNS
    • DPVS
    • GET和POST
    • HTTP 2
    • HTTP 3
    • HTTPS
    • LVS的转发模式
    • NAT
    • Nginx
    • OSI七层模型
    • Protobuf
    • Questions
    • REST Ful
    • RPC
    • socket缓冲区
    • socket详解
    • TCP滑动窗口
    • TCP连接建立源码
    • TCP连接四元组
    • TCP三次握手
    • TCP数据结构
    • TCP四次挥手
    • TCP拥塞控制
    • TCP重传机制
    • UDP
  • OS
    • 磁盘IO
    • 调度
    • 进程VS线程
    • 零拷贝
    • 内存-虚拟内存
    • 内存分配
    • 用户态VS内核态
    • 中断
    • COW写时复制
    • IO多路复用
    • Questions
  • Redis
    • 安装
    • 参考
    • 高可用-持久化
    • 高可用-主从同步
    • 高可用-Cluster
    • 高可用-Sentinel
    • 缓存一致性
    • 事务
    • 数据结构-SDS
    • 数据结构-Skiplist
    • 数据结构-Ziplist
    • 数据结构
    • 数据类型-Hashtable
    • 数据类型-List
    • 数据类型-Set
    • 数据类型-Zset
    • 数据淘汰机制
    • 通信协议-RESP
    • Questions
    • Redis6.0多线程
    • Redis分布式锁
    • Redis分片
  • System Design
    • 本地缓存
    • 错误处理
    • 大文件处理
    • 点赞收藏关注
    • 短链接生成系统
    • 负载均衡
    • 高并发高可用
    • 规则引擎
    • 集卡活动
    • 秒杀系统
    • 评论系统
    • 熔断
    • 限流
    • 延迟队列
    • Docker
    • ES
    • K 8 S
    • Node.js
    • Questions
  • Work
    • Bash
    • Charles
    • Code Review
    • Ffmpeg
    • Git
    • intellij插件
    • I Term 2
    • Mac
    • mysql命令
    • Nginx
    • postgresql命令
    • Protoc
    • Ssh
    • Systemd
    • Tcp相关命令
    • Vim
Powered by GitBook
On this page
  • 为什么要限流?
  • 限流分类
  • 分布式限流
  • 常见的限流算法
  • 计数限流
  • 固定窗口限流
  • 滑动窗口限流
  • 漏桶算法
  • 令牌桶算法
  1. System Design

限流

为什么要限流?

日常的业务上有类似秒杀活动、双十一、突发新闻等场景,用户流量突增,后端服务的处理能力是有限的,如果不能处理好突发流量,后端服务很容易就被打垮。一旦服务器卡死,可能你连服务器都登录不上,更别说处理问题了

亦或是爬虫 / 攻击等不正常流量,我们对外暴露的服务都要以最大恶意去防备调用者。

还有对于很多第三方开放平台来说,不仅仅要防备不正常流量,还要保证资源的公平利用,大家平均分配配额。

限流分类

  1. 通过鉴权限制掉不合法的流量,如验证码、token、IP白名单等

  2. 网关层面限流,如 tomcat 可以设置最大线程数;nginx 可以限制速率、连接数

  3. 服务限流。这一层又可以细分为分布式限流和本地限流

分布式限流

可以用 redis 来实现。

固定窗口:将时间窗作为 key,对其 inc 后判断即可。

令牌桶:见最下方的实现,可以写成 lua 脚本后加载到 redis

漏桶:redis 有人提供了插件,和布隆过滤器一样的加载方式,用 rust 写的:https://github.com/brandur/redis-cell

阿里的 sentinel 也是分布式限流。

常见的限流算法

计数限流

保存一个计数器,处理了一个请求,计数器就加一,一个请求处理完毕之后计数器减一。每次请求来的时候看看计数器的值,如果超过阈值就拒绝。

优点就是:简单粗暴,单机在 Java 中可用 Atomic 等原子类、分布式就 Redis incr。

但一般的限流都是为了限制在指定时间间隔内的访问量,因此一般要用下面的窗口来做。

固定窗口限流

它相比于计数限流主要是多了个时间窗口的概念,计数器每过一个时间窗口就重置。规则如下:

请求次数小于阈值,允许访问并且计数器 +1;

请求次数大于阈值,拒绝访问;

这个时间窗口过了之后,计数器清零。

固定窗口临界问题

假设系统每秒允许 100 个请求,假设第一个时间窗口是 0-1s,在第 0.55s 处一下次涌入 100 个请求,过了 1 秒的时间窗口后计数清零,此时在 1.05 s 的时候又一下次涌入100个请求。

虽然窗口内的计数没超过阈值,但是全局来看在 0.55s-1.05s 这 0.1 秒内涌入了 200 个请求,这其实对于阈值是 100/s 的系统来说是无法接受的。

固定窗口为了解决这个问题引入了滑动窗口限流。

滑动窗口限流

相对于固定窗口,滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点(保存成有序数组就可以),因此对内存的占用会比较多。

规则如下,假设时间窗口为 1 秒:

  • 记录每次请求的时间

  • 统计每次请求的时间 至 往前推1秒这个时间窗口内请求数。并且 1 秒前的数据可以删除。

  • 统计的请求数小于阈值就记录这个请求的时间,并允许通过,反之拒绝。

漏桶算法

水滴持续滴入漏桶中,底部定速流出。如果水滴滴入的速率大于流出的速率,当存水超过桶的大小的时候就会溢出。

规则如下:

  • 请求来了放入桶中

  • 桶内请求量满了拒绝请求

  • 服务定速从桶内拿请求处理

它的优点也即缺点:平滑。过于平均。

面对突发请求,服务的处理速度和平时是一样的,有时候不是我们想要的。

令牌桶算法

令牌桶其实是对固定窗口的优化。它和固定窗口一样将时间划分为了多个时间窗,限制每个时间窗内的流量。在此基础之上,又以比时间窗更小的粒度定速向桶里放入令牌,这个更小的粒度令牌桶里叫做 tick。

这样由于是定速放令牌,不会有固定窗口的临界问题;由于限制的是取令牌的速度而不是消耗令牌的速度,又不会有漏桶过于匀速、无法处理突发流量的问题。

比如限流 1000/s,每 ms 放入 1 个令牌。如果有 1000 个请求在 1ms 到来了:

  • 令牌桶可以一下全部放行,及时处理。漏桶只能每 0.01s 放一个,无法及时响应。因此令牌桶更适合应对高并发下突增的流量。

  • 如果又有 1000 个请求在下 1ms 到来了,由于每 ms 只会填充 1 个令牌,即使走到下一个时间窗 tick ,也只有 1 个令牌,只会放行 1 个,不会有固定窗口的临界问题。

规则

  • 定速的往桶内放入令牌

  • 令牌数量超过桶的限制,丢弃

  • 请求来了先向桶内索要令牌,索要成功则通过被处理,反之拒绝/等待

实现

实现上不用这么复杂,没有单独放令牌的函数,放令牌的步骤直接写在取令牌的函数里了。

只需要记录上一次请求所属的 tick,和本次请求所属的 tick,判断该 tick 内是否有余额即可。如果距离上一次的时间差超过一个 tick,就看时间差有多少,填充(速率×时间差)个令牌。

// https://github.com/juju/ratelimit/blob/master/ratelimit.go
type Bucket struct {
	clock Clock
	startTime time.Time        // 桶的创建时间
	capacity int64             // 每个时间窗限制的量
	quantum int64              // 每个 tick 填充的量
	fillInterval time.Duration // 填充间隔
	mu sync.Mutex              // 锁
	availableTokens int64      // 当前 tick 剩余的令牌数
	latestTick int64           // 上一个请求所属的 tick 
}

// 取令牌
func (tb *Bucket) Take(count int64) time.Duration {
	tb.mu.Lock() // 加锁
	defer tb.mu.Unlock()
	d, _ := tb.take(tb.clock.Now(), count, infinityDuration)
	return d
}

// 取令牌
func (tb *Bucket) take(now time.Time, count int64, maxWait time.Duration) (time.Duration, bool) {
	if count <= 0 {
		return 0, true
	}
    
	tick := tb.currentTick(now)    // 离 now 最近的一次填充时间轮
	tb.adjustavailableTokens(tick) // 比对最近一次 take 的时间和 tick,判断应该填充多少令牌
	avail := tb.availableTokens - count
	if avail >= 0 {
		tb.availableTokens = avail
		return 0, true
	}

    //如果令牌不够,返回应等待的时长
	endTick := tick + (-avail+tb.quantum-1)/tb.quantum
	endTime := tb.startTime.Add(time.Duration(endTick) * tb.fillInterval)
	waitTime := endTime.Sub(now)
	if waitTime > maxWait {
		return 0, false
	}
	tb.availableTokens = avail
	return waitTime, true
}

// 判断当前时间所属 tick
func (tb *Bucket) currentTick(now time.Time) int64 {
	return int64(now.Sub(tb.startTime) / tb.fillInterval)
}

// 调整 tick 内的令牌数
func (tb *Bucket) adjustavailableTokens(tick int64) {
	if tb.availableTokens >= tb.capacity {
		return
	}
	tb.availableTokens += (tick - tb.latestTick) * tb.quantum // 如果当前 tick > 上一次 tick,就将令牌桶填速率*时间个
	if tb.availableTokens > tb.capacity {
		tb.availableTokens = tb.capacity
	}
	tb.latestTick = tick
	return
}

问题

为什么采用xxx方案而不是别的?

结合业务场景,挑选适合的方式,而不是最好的方式。

Previous熔断Next延迟队列

Last updated 2 years ago

img