25 限速器
限速器
目的:
- 防止黑客的攻击
- 防止对资源的访问超过服务器的承载能力
- 防止在爬虫项目中被服务器封杀
在爬虫项目中,保持合适的速率也有利于我们稳定地爬取数据。大多数限速的机制是令牌桶算法(Token Bucket)来完成的。
令牌桶原理
令牌桶算法的原理很简单,我们可以想象这样一个场景,你去海底捞吃饭,里面只有 10 个座位,我们可以将这 10 个座位看作是桶的容量。现在,由于座位已经满了,服务员就帮我们叫了个号,我们随即进入到了等待的状态。
一桌客人吃完之后,下一位并不能马上就座,因为服务员还需要收拾饭桌。由于服务员的数量有限,因此即便很多桌客人同时吃完,也不能立即释放出所有的座位。如果每 5 分钟收拾好一桌,那么“1 桌 /5 分钟”就叫做令牌放入桶中的速率。轮到我们就餐时,我们占据了一个座位,也就是占据了一个令牌,这时我们就可以开吃了。
通过上面简化的案例能够看到,令牌桶算法通过控制桶的容量和令牌放入桶中的速率,保证了系统能在最大的处理容量下正常工作。
库
在 Go 中,我们可以使用官方的限速器实现:golang.org/x/time/rate,它提供了一些简单好用的 API。
// 速率,代表每秒钟放入到桶中的令牌个数
type Limit float64
// 第一个参数传递的是 Limit 速率,第二个参数 b 表示桶的数量
func NewLimiter(r Limit, b int) \*Limiter
// 参数是两个令牌之间的时间间隔,它会转化为对应的 Limit 速率。
func Every(interval time.Duration) Limit
// Wait是WaitN(ctx, 1)的简化形式, 参数 ctx 可以设置超时退出的时间,它可以避免协程一直陷在堵塞状态中。
func (lim \*Limiter) Wait(ctx context.Context) (err error)
// WaitN阻塞当前直到lim允许n个事件的发生。当没有可用或足够的事件时,将阻塞等待,推荐实际程序中使用这个方法。
func (lim \*Limiter) WaitN(ctx context.Context, n int) (err error)
// Allow是函数AllowN(time.Now(), 1)的简化函数
func (lim \*Limiter) Allow() bool
// AllowN标识在时间now的时候,n个事件是否可以同时发生(也意思就是now的时候是否可以从令牌桶中取n个token)。
// 适合在超出频率的时候丢弃或跳过事件的场景。
func (lim \*Limiter) AllowN(now time.Time, n int) bool
// ReserveN(time.Now(), 1)的简化形式
func (lim \*Limiter) Reserve() \*Reservation
// 用于标识调用者需要等多久才能等到n个事件发生(意思就是等多久令牌桶中至少含有n个token)。
// Wait/WaitN和Allow/AllowN其实就是基于其之上实现的,通过sleep等待时间和直接返回状态。
// 如果想对事件发生的频率和等待处理逻辑更加精细的话就可以使用它。
func (lim \*Limiter) ReserveN(now time.Time, n int) \*Reservation
func (lim \*Limiter) Burst() int
// 改变Token桶大小
func (lim \*Limiter) SetBurst(newBurst int)
func (lim \*Limiter) SetBurstAt(t time.Time, newBurst int)
func (lim \*Limiter) Limit() Limit
// 改变放入Token的速率
func (lim \*Limiter) SetLimit(newLimit Limit)
func (lim \*Limiter) SetLimitAt(t time.Time, newLimit Limit)
// 返回现在可用的令牌数。
func (lim \*Limiter) Tokens() float64
func (lim \*Limiter) TokensAt(t time.Time) float64
示例
package main
import (
"context"
"fmt"
"golang.org/x/time/rate"
"time"
)
func main() {
//生成了一个限速器,其中桶的最大容量为 2,rate.Limit(1) 表示每隔 1 秒钟向桶中放入 1 个令牌。
limit := rate.NewLimiter(rate.Limit(1), 2)
for {
if err := limit.Wait(context.Background()); err == nil {
fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
}
}
}
// output
2023-04-17 21:03:18
2023-04-17 21:03:18
2023-04-17 21:03:19
2023-04-17 21:03:20
// 前两次打印是在同一秒,这是因为桶中一开始有两个令牌可以用。之后,每一次打印都需要间隔一秒,因为每隔一秒钟才会往桶中填充一个令牌
使用 rate.Every 来生成 Limit 速率
package main
import (
"context"
"fmt"
"golang.org/x/time/rate"
"time"
)
func main() {
// 每 500 毫秒放入一个令牌,换算过来就是每秒钟放入 2 个令牌。
limit := rate.NewLimiter(rate.Every(500\*time.Millisecond), 2)
for {
if err := limit.Wait(context.Background()); err == nil {
fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
}
}
}
多条件限速器
有时候我们还会有一些更复杂的需求,例如有多层限速器的需求(细粒度限速器限制每秒的请求,粗粒度限速器限制每分钟、每小时或每天的请求)。
假设我们的爬虫项目希望每分钟只能够访问 10 次目标网站,但是只有每分钟的限制是不够的。因为这样我们可能会一秒钟直接访问 10 次,这样服务器就能直接检测出我们是爬虫机器人了。所以,我们还需要控制一下瞬时的请求量,例如每秒钟访问的频率不超过 0.5 次。这里我也借鉴了《Concurrency in Go》中多层限速器的设计,在新的 Limiter 包中将限速器抽象为了 RateLimiter 接口,golang.org/x/time/rate实现的 Limiter 自动就实现了该接口:
package limiter
type RateLimiter interface {
Wait(context.Context) error
Limit() rate.Limit
}
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
片转存中…(img-kP3E9waE-1725681176324)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0