二.Go微服务--令牌桶
1. 令牌桶
1.1 原理

- 我们以 r/s 的速度向桶内放置令牌,桶的容量为 b , 如果桶满了令牌将会丢弃
- 当请求到达时,我们向桶内获取令牌,如果令牌足够,我们就通过转发请求
- 如果桶内的令牌数量不够,那么这个请求会被缓存等待令牌足够时转发,或者是被直接丢弃掉
由于桶的存在,所以令牌桶算法不仅可以限流还可以应对突发流量的情况
举个例子:假设我们桶的容量是 100,速度是 10 rps,那么在我们桶满的情况下,如果突然来 100 个请求是可以满足的,但是后续的请求就会被限制到 10 rps
存在下面两种特殊情况
- 如果桶的容量为 0,那么相当于禁止请求,因为所有的令牌都被丢弃了
- 如果令牌放置速率为无穷大,那么相当于没有限制
令牌桶最常见的实现就是 Go 官方的 golang.org/x/time/rate
1.2 使用方法
方法如下
type Limiter struct {
// contains filtered or unexported fields
}
// 构建一个限流器,r 是每秒放入的令牌数量,b 是桶的大小
func NewLimiter(r Limit, b int) *Limiter
// 分别返回 b 和 r 的值
func (lim *Limiter) Burst() int
func (lim *Limiter) Limit() Limit
// token 消费方法
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool
func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation
func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)
// 动态流控
func (lim *Limiter) SetBurst(newBurst int)
func (lim *Limiter) SetBurstAt(now time.Time, newBurst int)
func (lim *Limiter) SetLimit(newLimit Limit)
func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit)
1.2.1 初始化令牌桶
直接调用 NewLimiter(r Limit, b int) 即可, r 表示每秒产生 token 的速度, b 表示桶的大小
1.2.2 Token 消费
总共有三种 token 消费的方式,最常用的是使用 Wait 阻塞等待
Allow
Allow 就是 AllowN(now,1) 的别名, AllowN 表示截止到 now 这个时间点,是否存在 n 个 token,如果存在那么就返回 true 反之返回 false,如果我们限流比较严格,没有资源就直接丢弃可以使用这个方法
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool
Reserve
同理 Reserve 也是 ReserveN(now, 1) 的别名, ReserveN 其实和 AllowN 类似,表示截止到 now 这个时间点,是否存在 n 个 token,只是 AllowN 直接返回 true or false,但是 ReserveN 返回一个 Reservation 对象
func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation
Reservation 有 5 个方法,通过调用 OK 我们可以知道是否通过等待可以获取到 N 个 token,如果可以通过 Delay 方法我们可以得知需要等待的时间,如果我们不想等了可以调用 Cancel 方法归还 token
type Reservation
func (r *Reservation) Cancel()
func (r *Reservation) CancelAt(now time.Time)
func (r *Reservation) Delay() time.Duration
func (r *Reservation) DelayFrom(now time.Time) time.Duration
func (r *Reservation) OK() bool
Wait
Wait 是最常用的, Wait 是 WaitN(ctx, 1) 的别名, WaitN(ctx, n) 表示如果存在 n 个令牌就直接转发,不存在我们就等,等待存在为止,传入的 ctx 的 Deadline 就是等待的 Deadline
func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)
1.2.3 动态流控
通过调用 SetBurst 和 SetLimit 可以动态的设置桶的大小和 token 生产速率,其中 SetBurstAt 和 SetLimitAt 会将传入的时间 now 设置为流控最后的更新时间
func (lim *Limiter) SetBurst(newBurst int)
func (lim *Limiter) SetBurstAt(now time.Time, newBurst int)
func (lim *Limiter) SetLimit(newLimit Limit)
func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit)
1.3 基于ip的gin限流中间件
主要就是使用了 sync.map 来为每一个 ip 创建一个 limiter,当然这个 key 也可以是其他的值,例如用户名等
func NewLimiter(r rate.Limit, b int, t time.Duration) gin.HandlerFunc {
limiters := &sync.Map{}
return func(c *gin.Context) {
// 获取限速器
// key 除了 ip 之外也可以是其他的,例如 header,user name 等
key := c.ClientIP()
l, _ := limiters.LoadOrStore(key, rate.NewLimiter(r, b))
// 这里注意不要直接使用 gin 的 context 默认是没有超时时间的
ctx, cancel := context.WithTimeout(c, t)
defer cancel()
if err := l.(*rate.Limiter).Wait(ctx); err != nil {
// 这里先不处理日志了,如果返回错误就直接 429
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": err})
}
c.Next()
}
}
使用的时候只需要 use 一下中间件就可以了
func main() {
e := gin.Default()
// 新建一个限速器,允许突发 10 个并发,限速 3rps,超过 500ms 就不再等待
e.Use(NewLimiter(3, 10, 500*time.Millisecond))
e.GET("ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
e.Run(":8080")
}
我们使用 go-stress-testing 来压测一下,20 个并发
~/gopath/bin/go-stress-testing -c 20 -n 1 -u http://127.0.0.1:8080/ping
开始启动 并发数:20 请求数:1 请求参数:
─────┬───────┬───────┬───────┬────────┬────────┬────────┬────────┬────────┬────────┬────────
耗时│ 并发数│ 成功数│ 失败数│ qps │ 最长耗时│ 最短耗时│ 平均耗时│ 下载字节│ 字节每秒│ 错误码
─────┼───────┼───────┼───────┼────────┼────────┼────────┼────────┼────────┼────────┼────────
1s│ 20│ 11│ 9│ 63.79│ 438.48│ 45.37│ 313.53│ 152│ 259│200:11;429:9
************************* 结果 stat ****************************
处理协程数量: 20
请求总数(并发数*请求数 -c * -n): 20 总请求时间: 0.586 秒 successNum: 11 failureNum: 9
************************* 结果 end ****************************
可以发现总共成功了 11 个请求,失败了 9 个,这是因为我们桶的大小是 10 ,所以前 10 个请求都很快就结束了,第 11 个请求等待 333.3 ms 就可以完成,小于超时时间 500ms,所以可以放行,但是后面的请求确是等不了了,所以就都失败了,并且可以看到最后一个成功的请求的耗时为 336.83591ms 而其他的请求耗时都很短
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2021/03/29 - 13:15:55 | 200 | 1.48104ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 1.107689ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 1.746222ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 866.35µs | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 1.870403ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 2.231912ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 1.832506ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 613.741µs | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 1.454753ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 1.37802ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 1.428062ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 40.782µs | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 1.046146ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 1.7624ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 429 | 1.803124ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 41.67µs | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 1.42315ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 1.371483ms | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 731.091µs | 127.0.0.1 | GET "/ping"
[GIN] 2021/03/29 - 13:15:55 | 200 | 336.83591ms | 127.0.0.1 | GET "/ping"
1.3 完整代码
demo.main
package main import (
"context"
"fmt"
"net/http"
"sync"
"time" "github.com/gin-gonic/gin"
"golang.org/x/time/rate"
) // NewLimiter, 定义中间件
func NewLimiter(r rate.Limit, b int, t time.Duration) gin.HandlerFunc {
limiters := &sync.Map{} return func(c *gin.Context) {
// 获取限速器
// key 除了 ip 之外也可以是其他的,例如 header,user name 等
key := c.ClientIP()
l, _ := limiters.LoadOrStore(key, rate.NewLimiter(r, b)) // 这里注意不要直接使用 gin 的 context 默认是没有超时时间的
ctx, cancel := context.WithTimeout(c, t)
defer cancel() if err := l.(*rate.Limiter).Wait(ctx); err != nil {
// 这里先不处理日志了,如果返回错误就直接 429
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": err})
}
c.Next()
}
} func main() {
e := gin.Default()
// 新建一个限速器,允许突发 10 个并发,限速 3rps,超过 500ms 就不再等待
e.Use(NewLimiter(3, 10, 500*time.Millisecond)) e.GET("ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
}) err := e.Run(":8080")
if err != nil {
fmt.Print("start server err:", err.Error())
}
}
下载go-stress-test
wget https://github.91chifun.workers.dev/https://github.com//link1st/go-stress-testing/releases/download/v1.0.3/go-stress-testing-linux
将gostress-tesing添加环境变量
mv go-stress-testing-linux /usr/local/bin/go-stress-testing
启动测试
go-stress-testing -c 20 -n 1 -u http://172.20.80.1:8080/ping
2. 参考
- https://lailin.xyz/post/go-training-week6-2-token-bucket-1.html
- https://github.com/link1st/go-stress-testing#11-go-stress-testing
二.Go微服务--令牌桶的更多相关文章
- 三.Go微服务--令牌桶实现原理
1. 前言 在上一篇文章 Go微服务: 令牌桶 当中简单的介绍了令牌桶实现的原理,然后利用 /x/time/rate 这个库 10 行代码写了一个基于 ip 的 gin 限流中间件,那这个功能是怎么实 ...
- SpringCloud学习(二):微服务入门实战项目搭建
一.开始使用Spring Cloud实战微服务 1.SpringCloud是什么? 云计算的解决方案?不是 SpringCloud是一个在SpringBoot的基础上构建的一个快速构建分布式系统的工具 ...
- spring cloud实战与思考(二) 微服务之间通过fiegn上传一组文件(上)
需求场景: 微服务之间调用接口一次性上传多个文件. 上传文件的同时附带其他参数. 多个文件能有效的区分开,以便进行不同处理. Spring cloud的微服务之间接口调用使用Feign.原装的Feig ...
- 猪齿鱼_01_环境搭建(二)_微服务支撑组件部署(Docker形式)
一.前言 上一节,我们以源码形式部署好了猪齿鱼微服务组件,过程繁琐,且启动后占用了服务器大量的资源,对开发极其不友好.
- springcolud 的学习(二).微服务架构的介绍
什么是微服务微服务架是从SOA架构演变过来,比SOA架构粒度会更加精细,让专业的人去做专业的事情(专注),目的提高效率,每个服务于服务之间互不影响,微服务架构中,每个服务必须独立部署,互不影响,微服务 ...
- SpringCloudAlibaba 微服务讲解(二)微服务环境搭建
微服务环境搭建 我们这次是使用的电商项目的商品.订单.用户为案例进行讲解 2.1 案例准备 2.1.1 技术选型 maven :3.3.9 数据库:mysql 持久层:SpringData JPA S ...
- 第四十二章 微服务CICD(4)- jenkins + gitlab + webhooks + publish-over-ssh(2)
上一节完成了"当git客户端push代码到gitlab后,jenkins会立即去gitlab拉取代码并构建". 目的:本节完成jenkins自动构建之后,自动的将jar包部署到应用 ...
- 《springcloud 二》微服务动态网关,网关集群
动态网关 实际上是网关和分布式配置中心的整合,通过post手动刷新,生效 动态网关 传统方式将路由规则配置在配置文件中,如果路由规则发生了改变,需要重启服务器.结合整合SpringCloud C ...
- SpringCloud学习笔记(二):微服务概述、微服务和微服务架构、微服务优缺点、微服务技术栈有哪些、SpringCloud是什么
从技术维度理解: 微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底 地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事, 从技术角度看就是一种小而独立的处理过程,类 ...
随机推荐
- Web 字体 font-family 浅谈
前言 最近研究各大网站的font-family字体设置,发现每个网站的默认值都不相同,甚至一些大网站也犯了很明显的错误,说明字体还是有很大学问的,值的我们好好研究. 不同的操作系统.不同浏览器下内嵌的 ...
- selenium定位,操作元素
1.定位方式 1.id driver.find_element_by_id('username').send_keys('byhy') 2.name driver.find_element_by_na ...
- 02_Java基础类型和包装类型
基本数据类型 包装类名称 所占字节数 默认值 byte Byte 1 0 short Short 2 0 Int Integer 4 0 long Long 8 0L double Double 8 ...
- python安全编程之指纹识别
什么是cms CMS是Content Management System的缩写,意为"内容管理系统",这是百度百科的解释,意思是相当于网站的建站模板,整个网站架构已经集成好了,只需 ...
- GoogleTest死亡测试的跨平台BUG
最近工作用到了GoogleTest来作单元测试,但是死亡测试的ASSERT_DEATH语句一直跑不通. GoogleTest会启动子进程来运行代码,并捕捉子进程的错误消息,这就是所谓的"死亡 ...
- 绿色djvu阅读软件
官方的djvu viewer都需要安装,总算找到一个绿色版的,名为STDU Viewer,可以阅读的格式包括DjVu, PDF, TIFF, XPS, FB2等,版本为1.6.2.
- go配置私有仓库 (go mod配置私有仓库)
windows 配置go私有仓库 一.环境 1.私有gitlab (gitlab.xxx.com) 2.go 1.16.3 3.win10系统, 家目录:C:\Users\Administrator, ...
- 关于下载远程文件为未知文件.txt的解决方法
本地下载文件后缀正常,服务器下载文件后缀都为.txt的解决方法: 后缀为 未知文件.txt 的原因为前端无权限获取Content-Disposition中的文件名 response.setHeader ...
- 从安装到使用——Odoo常见问题及故障处理
小九今天分享了Odoo一键部署.高效安装的图文详解,接下来,针对Odoo使用过程中的一些问题,小九整理了详细的常见问题问答.这样的直观方式往往能快速高效地解决一些疑惑. 也欢迎提出其他问题,共同探讨, ...
- Python 读写文件的正确方式
当你用 Python 写程序时,不论是简单的脚本,还是复杂的大型项目,其中最常见的操作就是读写文件.不管是简单的文本文件.繁杂的日志文件,还是分析图片等媒体文件中的字节数据,都需要用到 Python ...