惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

Mohuishou

如何实现支持多集群的 Kubernetes Operator? 第三方应用如何调用我们 kubebuilder 生成的自定义资源? Kubernetes 简明教程 k8s job 为何迟迟不能结束? Go 工程化(十一) 如何优雅的写出 repo 层代码 Go 工程化(十) 如何在整洁架构中使用事务? 给博客添加章节目录 使用 Notion Database 管理静态博客文章 一个普通 Go 开发的三年 4. localhost 就一定是 localhost 么? Go可用性(七) 总结: 一张图串联可用性知识点 Go可用性(六) 熔断 10. 总结 9. kubebuilder 进阶: 源码分析 8. kubebuilder 进阶: webhook 7. kubebuilder 进阶: 测试 6. kubebuilder 实战: status & event 5. kubebuilder 实战: CRUD 4. kustomize 简明教程 3. KubeBuilder 简明教程 2. Kind: 如何快速搭建本地 K8s 开发环境? 1. Operator概述: 如何对 Kubernetes 进行扩展 Go可用性(五) 自适应限流 Go可用性(三) 令牌桶的实现 rate/limt Go可用性(二) 令牌桶原理及使用 Go可用性(一) 隔离设计 Go并发编程(十二) Singleflight Go工程化(九) 项目重构实践 Go工程化(八) 单元测试 Go工程化(七) Go Module Go工程化(六) 配置管理 Go工程化(五) API 设计下: 基于 protobuf 自动生成 gin 代码 Go工程化(四) API 设计上: 项目结构 & 设计 Go工程化(三) 依赖注入框架 wire Go工程化(二) 项目目录结构 Go工程化(一) 架构整洁之道阅读笔记 Go并发编程(十一) 总结 Go并发编程(十) 深入理解 Channel Go并发编程(九) 深入理解 Context Go并发编程(八) 深入理解 sync.Once Go并发编程(七) 深入理解 errgroup Go并发编程(六) 深入理解 WaitGroup Go并发编程(五) 深入理解 sync/atomic Go并发编程(四) 深入理解 Mutex Go并发编程(三) data race Go并发编程(二) Go 内存模型 Go并发编程(一) goroutine Go错误处理最佳实践 微服务(二) 服务发现&多租户 微服务(一) 微服务概览 5. 栈下: 深入理解 defer 4. 栈上: 如何实现一个计算器 Go Struct 初始化风格的抉择 3. 数组下: 使用 GDB 调试 Golang 代码 2. 数组上: 深入理解 slice 1. 链表: 深入理解container/list&LRU缓存的实现 Go设计模式24-总结(更新完毕) Go设计模式23-中介模式 Go设计模式22-解释器模式 Go设计模式21-命令模式 Go设计模式20-备忘录模式 Go设计模式19-访问者模式 Go设计模式18-迭代器模式 Go设计模式17-状态模式 Go设计模式16-职责链模式(Gin的中间件实现) Go设计模式15-策略模式 Go模板模式14-模板模式 Go设计模式13-观察者模式(实现简单的EventBus) Go设计模式12-享元模式 Go设计模式11-组合模式 Go设计模式10-门面模式 Go设计模式09-适配器模式 Go设计模式08-装饰器模式 Go设计模式07-桥接模式 Go设计模式06-代理模式(generate实现类似动态代理) Go设计模式05-创建型模式总结 Go设计模式04-原型模式 Go设计模式03-建造者模式 Go设计模式02-工厂模式&DI容器 笔记-让你最快速地改善代码质量的20条编程规范 Go设计模式01-单例模式 一点拙见-如何写好一个技术预研报告? Go Web小技巧(四)在单个仓库中支持多个 go mod 模块 Go Web 小技巧(三)Gin 参数绑定 Go Web 小技巧(二)GORM 使用自定义类型 Go Web 小技巧(一)简化Gin接口代码 善用工具之postman高级用法概述 go generate and ast hexo-next-algolia-search全文搜索 docker镜像瘦身&优化 GORM避坑指南之含关联关系的更新 Github Actions介绍&自动构建Github Pages博客 在blog中内嵌在线PPT 记一次net http内存泄漏 使用TravisCI自动部署Blog 使用Goland调试Go程序 一个十分边缘的gorm的bug Httprouter介绍及源码阅读 Gin源码阅读 从0.1开始
Go可用性(四) 漏桶算法
2021-04-07 · via Mohuishou

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

本系列为 Go 进阶训练营 笔记,访问 博客: Go进阶训练营, 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳。

在前面两篇文章当中我们学习了令牌桶算法的使用和实现,今天我们就一起来看一看另外一种常见的限流算法,漏桶算法

漏桶算法

原理

漏桶算法(Leaky Bucket) 是网络世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 — 百度百科

漏桶算法其实非常形象,如下图所示可以理解为一个漏水的桶,当有突发流量来临的时候,会先到桶里面,桶下有一个洞,可以以固定的速率向外流水,如果水的从桶中外溢了出来,那么这个请求就会被拒绝掉。具体的表现就会向下图右侧的图表一样,突发流量就被整形成了一个平滑的流量。
image.png

漏桶算法的主要作用就是避免出现有的时候流量很高,有的时候又很低,导致系统出现旱的旱死,涝的涝死的这种情况。

Go 中比较常用的漏桶算法的实现就是来自 uber 的 ratelimit,下面我们就会看一下这个库的使用方式和源码

API

1
2
3
4
5
6
7
8
type Clock
type Limiter
func New(rate int, opts ...Option) Limiter
func NewUnlimited() Limiter
type Option
func Per(per time.Duration) Option
func WithClock(clock Clock) Option
func WithSlack(slack int) Option

Clock  是一个接口,计时器的最小实现,有两个方法,分别是当前的时间和睡眠

1
2
3
4
type Clock interface {
Now() time.Time
Sleep(time.Duration)
}

Limiter  也是一个接口,只有一个 Take  方法,执行这个方法的时候如果触发了 rps 限制则会阻塞住

1
2
3
4
type Limiter interface {
// Take should block to make sure that the RPS is met.
Take() time.Time
}

NewLimter  和 NewUnlimited  会分别初始化一个无锁的限速器和没有任何限制的限速器

Option  是在初始化的时候的额外参数,这种使用姿势在之前 Go 工程化的文章《Go 工程化(六) 配置管理》当中有讲到,这里我们就不再赘述了

Option  有三个方法

  • Per  可以修改时间单位,默认是秒所以我们默认限制的是 rps,如果改成分钟那么就是 rpm 了
  • WithClock  可以修改时钟,这个用于在测试的时候可以 mock 掉不使用真实的时间
  • WithSlack  用于修改松弛时间,也就是可以允许的突发流量的大小,默认是 Pre / 10 ,这个后面会讲到

案例: 10 行代码实现一个基于漏桶算法的 ip 限流中间件

案例我们使用和令牌桶类似的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
func NewLimiter(rps int) gin.HandlerFunc {
limiters := &sync.Map{}

return func(c *gin.Context) {
// 获取限速器
// key 除了 ip 之外也可以是其他的,例如 header,user name 等
key := c.ClientIP()
l, _ := limiters.LoadOrStore(key, ratelimit.New(rps))
now := l.(ratelimit.Limiter).Take()
fmt.Printf("now: %s\n", now)
c.Next()
}
}

使用上也是比较简单的

1
2
3
4
5
6
7
8
9
func main() {
e := gin.Default()
// 新建一个限速器,允许突发 3 个并发
e.Use(NewLimiter(3))
e.GET("ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
e.Run(":8080")
}

我们用 go-stress-testing  进行压测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go-stress-testing-linux -c 100 -u http://localhost:8080/ping

─────┬───────┬───────┬───────┬────────┬────────┬────────┬────────┬────────┬────────┬────────
耗时 │ 并发数│ 成功数│ 失败数 │ qps │最长耗时│最短耗时 │平均耗时 │下载字节 │字节每秒 │ 错误码
─────┼───────┼───────┼───────┼────────┼────────┼────────┼────────┼────────┼────────┼────────
1s│ 13130233.55676.105.8285.645251200:13
2s│ 1616062.251675.175.82321.306431200:16
3s│ 1919031.242673.945.82640.207625200:19
3s│ 2020026.373006.495.82758.518026200:20

************************* 结果 stat ****************************
处理协程数量: 20
请求总数(并发数*请求数 -c * -n): 20 总请求时间: 3.011 秒 successNum: 20 failureNum: 0
************************* 结果 end ****************************

查看结果发现为什么第一秒的时候完成了 13 个请求,不是限制的 3rps 么?不要慌,我们看看它的实现就知道了

实现

这个库有基于互斥锁的实现和基于 CAS 的无锁实现,默认使用的是无锁实现版本,所以我们主要看无锁实现的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type state struct {
last time.Time
sleepFor time.Duration
}

type atomicLimiter struct {
state unsafe.Pointer
//lint:ignore U1000 Padding is unused but it is crucial to maintain performance
// of this rate limiter in case of collocation with other frequently accessed memory.
padding [56]byte // cache line size - state pointer size = 64 - 8; created to avoid false sharing.

perRequest time.Duration
maxSlack time.Duration
clock Clock
}

atomicLimiter  结构体

  • state  是一个状态的指针,用于存储上一次的执行的时间,以及需要 sleep  的时间
  • padding  是一个无意义的填充数据,为了提高性能,避免 cpu 缓存的 false sharing
    • 之前在讲 Go 并发编程(二) Go 内存模型 的时候有讲到,为了能够最大限度的利用 CPU 的能力,会做很多丧心病狂的优化,其中一种就是 cpu cache
    • cpu cache 一般是以 cache line 为单位的,在 64 位的机器上一般是 64 字节
    • 所以如果我们高频并发访问的数据小于 64 字节的时候就可能会和其他数据一起缓存,其他数据如果出现改变就会导致 cpu 认为缓存失效,这就是 false sharing
    • 所以在这里为了尽可能提高性能,填充了 56 字节的无意义数据,因为 state 是一个指针占用了 8 个字节,所以 64 - 8 = 56
  • 剩下三个字段和 Option  中的三个方法意义对应
    • perRequest  就是单位,默认是秒
    • maxSlack  松弛时间,也就是可以允许的突发流量的大小,默认是 Pre / 10 ,这个后面会讲到
    • clock  时钟,这个用于在测试的时候可以 mock 掉不使用真实的时间

接下来看看最主要的 Take  方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
func (t *atomicLimiter) Take() time.Time {
var (
// 状态
newState state
// 用于表示原子操作是否成功
taken bool
// 需要 sleep 的时间
interval time.Duration
)

// 如果 CAS 操作不成功就一直尝试
for !taken {
// 获取当前的时间
now := t.clock.Now()

// load 出上一次调用的时间
previousStatePointer := atomic.LoadPointer(&t.state)
oldState := (*state)(previousStatePointer)

newState = state{
last: now,
sleepFor: oldState.sleepFor,
}

// 如果 last 是零值的话,表示之前就没用过,直接保存返回即可
if oldState.last.IsZero() {
taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState))
continue
}

// sleepFor 是需要睡眠的时间,由于引入了松弛时间,所以 sleepFor 可能是一个
// maxSlack ~ 0 之间的一个值,所以这里需要将现在的需要 sleep 的时间和上一次
// sleepFor 的值相加
newState.sleepFor += t.perRequest - now.Sub(oldState.last)

// 如果距离上一次调用已经很久了,sleepFor 可能会是一个很小的值
// 最小值只能是 maxSlack 的大小
if newState.sleepFor < t.maxSlack {
newState.sleepFor = t.maxSlack
}

// 如果 sleepFor 大于 0 的话,计算出需要 sleep 的时间
// 然后将 state.sleepFor 置零
if newState.sleepFor > 0 {
newState.last = newState.last.Add(newState.sleepFor)
interval, newState.sleepFor = newState.sleepFor, 0
}

// 保存状态
taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState))
}

// sleep interval
t.clock.Sleep(interval)
return newState.last
}

总结

今天学习了漏桶的实现原理以及使用方式,漏桶和令牌桶的最大的区别就是,令牌桶是支持突发流量的,但是漏桶是不支持的。但是 uber 的这个库通过引入弹性时间的方式也让漏桶算法有了类似令牌桶能够应对部分突发流量的能力,并且实现上还非常的简单,值得学习。

多看看好的轮子的实现总会学到一些新姿势,今天就学到了使用 padding 填充来避免 false sharing 提高性能的操作

参考文献

  1. Go 进阶训练营-极客时间
  2. “带你快速了解:限流中的漏桶和令牌桶算法”
  3. ratelimit · pkg.go.dev
  4. ratelimit/limiter_atomic.go at main · uber-go/ratelimit (github.com)
  5. 漏桶算法_百度百科 (baidu.com)
  6. 利用 CPU cache 特性优化 Go 程序

关注我获取更新

wechat

知乎

github

猜你喜欢