限速&限并发
速度qps = 并发*(1/平均处理时间)
区别
- 限速-限流量
- 限并发-限最大同时处理量
(限速 餐馆受限服务员人力,每小时最多接待100人;限并发,餐馆受限餐桌数,无论何时最多接待20桌;)
如何选择
-
大多数情况限那个都可以
-
流量越大越适合限速 (如常见低耗时接口限速,如果接口耗时过长,更适合改成异步队列方式
-
处理周期越长、资源绑定越高 越适合限并发 (如脚本任务队列处理时间长,依赖cpu核数
(事实上餐馆主要限的就是并发)
限速方法
周期计数-sleep法
思路,处理了限速量时还没超过限速周期,则睡眠。伪代码如下
qpsLimit=10
t1=time.now()
curProccessCount=0
limitedFunc(){
//do something
curProccessCount++ // need atomic
if curProccessCount>=qpsLimit {
now=time.now()
if (now-t1)<1s sleep (t1+1s-now);
curProccessCount=0
t1=time.now()
}
}
周期内限速不均匀,更适合放在生产消费模型中生产端限速,在高限速下开销最小
计时限速器 time.tiker 、 rateLimiter
- go time.ticker
var limiterTicker <-chan time.Time
func init() {
// qmsLimit:=50
limiterTicker = time.Tick(time.Duration(int64(1000/50) * int64(time.Millisecond)))
}
func limitedFunc() {
<-limiterTicker
//do something
}
- ratelimiter 如 “go.uber.org/ratelimit”
// "go.uber.org/ratelimit"
var uberRatelimter ratelimit.Limiter
func init() {
// qmsLimit:=50
//ratelimit.WithSlack(10) 类似令牌桶中的桶容量,无slack,表示均匀每20ms一次, slack 10意思为 假如前半秒一个请求没来,那么后半秒则最多可处理 50/2+10个
// ratelimter2 = ratelimit.New(50,ratelimit.WithoutSlack)
uberRatelimter = ratelimit.New(50,ratelimit.WithSlack(10))
}
func limitedFunc() {
uberRatelimter.Take()
//do something
}
ratelimit原理简化如下
同时uber/ratelimit 无法进行< 1q/s的限速,这里支持
type RateLimiter interface {
Take() int64 //获取下一个时间戳
}
type myRateLimiter struct {
lastTime int64
msInterval int64 // us
}
func NewRateLimiterByInterval(msInterval int64) (r RateLimiter) {
mr := &myRateLimiter{
msInterval: msInterval,
}
return mr
}
func (r *myRateLimiter) Take() (unixMsTimestamp int64) {
t := time.Now().UnixNano() / 1e6
if r.msInterval == 0 {
return t
}
t2 := r.lastTime
dec := t2 - t
if dec <= 0 {
r.lastTime = t + r.msInterval
return t
} else {
atomic.AddInt64(&r.lastTime, r.msInterval)
time.Sleep(time.Duration(dec * 1e6))
return t2
}
}
限并发方法
- 用消费队列数量限制,消费者和队列一对一同步消费
- 控制处理线程(协程)数量,单线程内同步处理
- golang 有缓冲 chan
var limiterChan chan struct{}
func init() {
// 最大并发数10
limiterChan=make(chan struct{},10)
}
func limitedFunc() {
limiterChan<- struct{}{}
//do something
<-limiterChan
}
线上环境限速,接口限速
-
总限制/pod数(限速 限并发)
假设服务某接口限速100,16个pod,那么 100/16 = 6 ,每个pod限速6即可;
优点简单、缺点 限速受pod水平扩缩容影响,负载不均时无法打到最大限速 ;
限并发情况负载不均问题在长周期任务特性下不明显,也可通过队列数量控制; -
令牌桶、漏桶服务 (限速)
令牌桶以限速速度均匀向桶中放置token;
处理限速操作时,先请求令牌桶服务、获得token正常处理,未获取则等待或者返回错误;
令牌桶 漏桶区别是,令牌桶匀速向桶中放token,漏桶匀速漏token,由于桶有一定容量,令牌桶遇到请求量突增时会短暂超过实际限速。 -
接口请求参数相关的限速(比如用户、用户行为相关)
用redis这种单点来记录 ;
例如对 用户user1、getData1 接口限速1分钟1次;
我们可以构建一个key myservice:getData1:usr1 , 值存储最近访问时间 ;
在请求到来时、 setNxByEx(myservice:limit:getData1:usr1,1min),若ok则继续,否则返回受到限制;
那要想获取受限时间呢? 取出对应值-now ;
那要是限制1分钟5次呢?存个长度为5的数组当队列,访问到来时,若队尾<now-1min 则push(now),否则受到限速; -
redis 脚本(限速-限并发,不优先推荐, 低效)
- 限速 key={限速器名字}_{时间戳秒}
限速前,执行脚本 获取key值,若为空(setex 1s)或小于最大限速量则 执行INCR 返回正常、若大于最大限速则返回错误local key = "key1_time1" local threshold = 8 if redis.call("EXISTS", key) == 0 or tonumber(redis. call("GET", key)) < threshold then redis.call("INCR", key) return true else return false end
- 限制并发 key={限速器名字}
和限速比key没有时间成分、并发限制前操作同限速,完成并发操作后进行 DECR key处理
- 限速 key={限速器名字}_{时间戳秒}