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

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
H
Help Net Security
Last Week in AI
Last Week in AI
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
IT之家
IT之家
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
B
Blog
C
Check Point Blog
T
Tailwind CSS Blog
云风的 BLOG
云风的 BLOG
D
Docker
Recent Announcements
Recent Announcements
Vercel News
Vercel News
博客园 - 聂微东
阮一峰的网络日志
阮一峰的网络日志
MyScale Blog
MyScale Blog
The GitHub Blog
The GitHub Blog
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
月光博客
月光博客
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
The Register - Security
The Register - Security
V
Visual Studio Blog
F
Full Disclosure
Hugging Face - Blog
Hugging Face - Blog
T
Threat Research - Cisco Blogs
Latest news
Latest news
PCI Perspectives
PCI Perspectives
Cisco Talos Blog
Cisco Talos Blog
博客园 - Franky
D
DataBreaches.Net
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
P
Palo Alto Networks Blog
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
T
Tenable Blog
L
LINUX DO - 热门话题
Spread Privacy
Spread Privacy

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可用性(五) 自适应限流
Mohuishou · 2021-04-12 · via Mohuishou

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

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

在前面限流的三篇文章,我们学习了令牌桶、漏桶算法的原理、实现以及使用方式,不知道你有没有觉得这两种算法存在着一些问题。

这两种算法最大的一个问题就是他们都属于需要提前设置阈值的算法,基于 QPS 进行限流的时候最麻烦的就是这个阈值应该怎么设定。一般来说我们可以通过压测来决定这个阈值。

  • 但是如果每个系统上线前都要经过很严格的压测,那么成本相对来说会比较大
  • 并且我们很多时候压测都会在测试环境进行压测,测试环境一般来说和生产环境会有一定的差异,即使我们在生产环境做了压测,现在我们的应用都是以容器的形式跑在不同的宿主机上的,每台宿主机上的差异,以及不同的负载都会导致这个压测时的结果不一定就一定是正确的
  • 当我们的机器型号、数量等发生改变时,之前压测的指标能不能用其实是一个问题,这些数据对于系统负载的影响其实不是线性的,举个例子之前一台机器,后面再加一台,负载就一定能到 2 倍么?其实是不一定的
  • 如果需要修改限流的值,虽然之前我们将令牌桶的限流是可以动态调整,但是靠人去调整,如果真出现问题然后再叫运维或者是开发同学去调整可能黄花菜都凉了

既然这种方式有这么多的缺点,那有没有办法解决呢?答案就是今天讲到的 自适应限流

自适应限流

自适应限流怎么做

前面我们遇到的主要问题就是每个服务实例的限流阈值实际应该是动态变化的,我们应该根据系统能够承载的最大吞吐量,来进行限流,当当前的流量大于最大吞吐的时候就限制流量进入,反之则允许通过。那现在的问题就是

  • 系统的吞吐量该如何计算?
  • 什么时候系统的吞吐量就是最大的吞吐量了?

计算吞吐量:利特尔法则 L = λ * W

利特尔法则由麻省理工大学斯隆商学院(MIT Sloan School of Management)的教授 John Little﹐于 1961 年所提出与证明。它是一个有关提前期与在制品关系的简单数学公式,这一法则为精益生产的改善方向指明了道路。 —- MBA 智库百科 (mbalib.com)

image.png
如上图所示,如果我们开一个小店,平均每分钟进店 2 个客人(λ),每位客人从等待到完成交易需要 4 分钟(W),那我们店里能承载的客人数量就是 2 * 4 = 8 个人

同理,我们可以将 λ  当做 QPS, W  呢是每个请求需要花费的时间,那我们的系统的吞吐就是 L = λ * W ,所以我们可以使用利特尔法则来计算系统的吞吐量。

什么时候系统的吞吐量就是最大的吞吐量?

首先我们可以通过统计过去一段时间的数据,获取到平均每秒的请求量,也就是 QPS,以及请求的耗时时间,为了避免出现前面 900ms 一个请求都没有最后 100ms 请求特别多的情况,我们可以使用滑动窗口算法来进行统计。

最容易想到的就是我们从系统启动开始,就把这些值给保存下来,然后计算一个吞吐的最大值,用这个来表示我们的最大吞吐量就可以了。但是这样存在一个问题是,我们很多系统其实都不是独占一台机器的,一个物理机上面往往有很多服务,并且一般还存在一些超卖,所以可能第一个小时最大处理能力是 100,但是这台节点上其他服务实例同时都在抢占资源的时候,这个处理能力最多就只能到 80 了

所以我们需要一个数据来做启发阈值,只要这个指标达到了阈值那我们就进入流控当中。常见的选择一般是 CPU、Memory、System Load,这里我们以 CPU 为例

只要我们的 CPU 负载超过 80% 的时候,获取过去 5s 的最大吞吐数据,然后再统计当前系统中的请求数量,只要当前系统中的请求数大于最大吞吐那么我们就丢弃这个请求。

kratos 自适应限流分析

限流公式

1
2
3
// PS: 官方文档这里写的是 cpu > 800 AND (Now - PrevDrop) < 1s
// 应该是写错了,等下看源码就知道了
(cpu > 800 OR (Now - PrevDrop) < 1s) AND (MaxPass * MinRt * windows / 1000) < InFlight
  • cpu > 800  表示 CPU 负载大于 80% 进入限流
  • (Now - PrevDrop) < 1s  这个表示只要触发过 1 次限流,那么 1s 内都会去做限流的判定,这是为了避免反复出现限流恢复导致请求时间和系统负载产生大量毛刺
  • (MaxPass * MinRt * windows / 1000) < InFlight  判断当前负载是否大于最大负载
    • InFlight  表示当前系统中有多少请求
    • (MaxPass * MinRt * windows / 1000)  表示过去一段时间的最大负载
    • MaxPass  表示最近 5s 内,单个采样窗口中最大的请求数
    • MinRt  表示最近 5s 内,单个采样窗口中最小的响应时间
    • windows  表示一秒内采样窗口的数量,默认配置中是 5s 50 个采样,那么 windows 的值为 10。

源码分析

BBR 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type BBR struct {
cpu cpuGetter
// 请求数,和响应时间的采样数据,使用滑动窗口进行统计
passStat metric.RollingCounter
rtStat metric.RollingCounter

// 当前系统中的请求数
inFlight int64
// 每秒钟内的采样数量,默认是10
winBucketPerSec int64
// 单个 bucket 的时间
bucketDuration time.Duration
// 窗口数量
winSize int
// 配置
conf *Config
prevDrop atomic.Value
// 表示最近 5s 内,单个采样窗口中最大的请求数的缓存数据
maxPASSCache atomic.Value
// 表示最近 5s 内,单个采样窗口中最小的响应时间的缓存数据
minRtCache atomic.Value
}

Allow: 判断请求是否允许通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (l *BBR) Allow(ctx context.Context, opts ...limit.AllowOption) (func(info limit.DoneInfo), error) {
// ... 省略配置修改代码

if l.shouldDrop() {
return nil, ecode.LimitExceed
}

atomic.AddInt64(&l.inFlight, 1)
stime := time.Since(initTime)

return func(do limit.DoneInfo) {
rt := int64((time.Since(initTime) - stime) / time.Millisecond)
l.rtStat.Add(rt)
atomic.AddInt64(&l.inFlight, -1)
switch do.Op {
case limit.Success:
l.passStat.Add(1)
return
default:
return
}
}, nil
}

这个方法主要是给中间件使用的

  • 首先使用 shouldDrop  方法判断这个请求是否应该丢弃
  • 如果成功放行,那么当前系统中的请求数就 +1
  • 然后返回一个 function  用于请求结束之后
    • 统计请求的响应时间
    • 如果请求成功了,给成功的请求数 +1
    • 并且当前系统中的请求数量 Inflight  -1

shouldDrop: 判断请求是否应该被丢弃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (l *BBR) shouldDrop() bool {
if l.cpu() < l.conf.CPUThreshold {
prevDrop, _ := l.prevDrop.Load().(time.Duration)
if prevDrop == 0 {
return false
}
if time.Since(initTime)-prevDrop <= time.Second {
inFlight := atomic.LoadInt64(&l.inFlight)
return inFlight > 1 && inFlight > l.maxFlight()
}
l.prevDrop.Store(time.Duration(0))
return false
}
inFlight := atomic.LoadInt64(&l.inFlight)
drop := inFlight > 1 && inFlight > l.maxFlight()
if drop {
prevDrop, _ := l.prevDrop.Load().(time.Duration)
if prevDrop != 0 {
return drop
}
l.prevDrop.Store(time.Since(initTime))
}
return drop
}

这个方法其实就是开头讲到的限流公式了,逻辑如下图所示
Go可用性 限流-分布式限流-kratos 限流策略.png

  • 首先看 CPU 的使用率是否达到了阈值
  • 如果没到,则回去判断一下上次触发限流到现在是否在一秒以内
    • 如果在一秒内,就判断当前负载是否超过限制,如果超过了就需要丢弃
    • 如果不在 1s 内或者是请求数量已经降下来了,那么就吧 prevDrop  清零然后返回 false
  • 如果到了,则判断一下当前负载是否超过限制
    • 如果超过了,则设置丢弃时间 prevDrop,返回 true 需要丢弃请求
    • 如果没超过直接返回 false

maxFlight: 系统的最大负载

1
2
3
func (l *BBR) maxFlight() int64 {
return int64(math.Floor(float64(l.maxPASS()*l.minRT()*l.winBucketPerSec)/1000.0 + 0.5))
}

这个就是计算过去一段时间系统的最大负载是多少

总结

这篇文章我们讲了一下为什么需要自适应限流,令牌桶和漏桶这类需要手动设置 rps 算法的问题所在,了解了自适应限流的实现原理,最后看了一下 kratos 当中是如何实现自适应限流的。但是由于篇幅关系,CPU 的数据如何进行统计,文章中提到了很多次的滑动窗口是个什么原理这些知识点大家可以自行查看 kratos 中的源码,或者去看极客时间的 Go 进阶训练营都有讲到。

kratos 中的限流算法其实灵感源于 Google SRE,实现上参考了 sentinel,其中一个有意思的点是 sentinel 默认使用 load 作为启发阈值,而 kratos 使用了 cpu,kratos 为什么要使用 cpu 呢?这个大家可以自己想想(答案可以自行观看极客时间的 Go 进阶训练营)

而 sentinel 的实现其实是参考了 TCP 中的 BBR 算法,在 BBR 的基础上加上了 load 作为启发阈值的判断,所以多了解一下基础知识总是没错的,指不定当下遇到的场景就能解决。

参考文献

  1. Go 进阶训练营-极客时间
  2. B 站高可用架构实践 | 赵坤的个人网站 (kunzhao.org)
  3. 利特尔法则 - MBA 智库百科 (mbalib.com)
  4. 从流量控制算法谈网络优化 – 从 CUBIC 到 BBRv2 算法 | 亚马逊 AWS 官方博客 (amazon.com)
  5. kratos/ratelimit.md at v1.0.x · go-kratos/kratos (github.com)
  6. 限流器系列(3)–自适应限流 - 郝洪范的个人空间 - OSCHINA - 中文开源技术交流社区
  7. TCP congestion control - Wikipedia
  8. 系统自适应限流 · alibaba/Sentinel Wiki · GitHub

关注我获取更新

猜你喜欢