面对高并发、大流量的应用场景时,服务为了保护自己避免崩溃,经常采用限流的措施。限流分为不同方式限流,比如:
- 合法性验证限流:比如验证码、IP黑名单等
- 容器限流:tomcat、nginx配置限流
- 服务端限流:采用限流算法,限制服务响应的请求数
服务端限流算法介绍
服务端限流算法有多种,比如固定窗口计数、移动时间窗口计数、漏桶算法、令牌算法等。每个算法,都提供了具体的编码实现,可移步九妹的github:github.com/chenling852...
固定窗口计数
固定窗口技术,即以固定的时间端为一个计数周期,对时间窗内的请求数进行计数。当请求数超过限制时,服务端不再响应请求。等到下一个时间窗口,再将计数清空,重新计数。该算法的核心实现代码:
go
type Limiter struct {
WindowDuration time.Duration
CurrentReq int
MaxReq int
LastReSetTm int64
Lock *sync.Mutex
}
func (l *Limiter) Allow() bool {
l.Lock.Lock()
defer l.Lock.Unlock()
now := time.Now().UnixMilli()
if now-l.LastReSetTm > int64(l.WindowDuration/time.Millisecond) {
l.CurrentReq = 0
l.LastReSetTm = now
}
if l.CurrentReq < l.MaxReq {
l.CurrentReq++
return true
}
return false
}
移动窗口计数
移动时间窗计数,以当前时间为截止时间,判断往前的时间窗内,比如一秒内,计算该时间窗内的请求数。相比如固定窗口计数,该算法更为灵活,精度和实时性更高。然而该算法属于不公平算法,针对区间内的大量请求,如果开始的请求达到了限制,后续的请求会全部拒绝。该算法的核心实现代码:
go
type Limiter struct {
WindowDuration time.Duration
MaxReq int
ReqRecords []int64
Lock *sync.Mutex
}
func (l *Limiter) Allow() bool {
l.Lock.Lock()
defer l.Lock.Unlock()
now := time.Now().UnixMilli()
for {
if len(l.ReqRecords) == 0 {
break
}
if (now - l.ReqRecords[0]) <= int64(l.WindowDuration/time.Millisecond) {
break
}
l.ReqRecords = l.ReqRecords[1:]
}
if len(l.ReqRecords) < l.MaxReq {
l.ReqRecords = append(l.ReqRecords, now)
return true
}
return false
}
漏桶算法
漏桶算法,顾名思义,无论请求的速度多快,都以固定的速度流出。当漏斗满之后,会丢弃新来的请求。该算法处理的过程,输出更为平滑。但是,缺乏对突发流量的支持。当输入流量小于流出流量时,会有速率的浪费。该算法的核心实现代码:
go
type Limiter struct {
LeakyRate int64
Capacity int
RemainWater int
LastLeakTm int64
Lock *sync.Mutex
}
func (l *Limiter) Allow() bool {
l.Lock.Lock()
defer l.Lock.Unlock()
now := time.Now().UnixMilli()
tmFlow := now - l.LastLeakTm
leak := tmFlow * l.LeakyRate / int64(1000)
if leak > 0 {
l.RemainWater -= int(leak)
if l.RemainWater < 0 {
l.RemainWater = 0
}
}
l.RemainWater++
if l.RemainWater > l.Capacity {
l.RemainWater--
return false
}
l.LastLeakTm = now
return true
}
令牌桶算法
令牌桶算法,有一个程序以恒定的速度生成令牌并存入令牌桶。每个请求需要先获取令牌,才能被响应。没有获取到令牌的请求,可以选择等待或者放弃执行。该算法可以累积令牌,更好的处理突发流量。然而该算法的实现更为复杂,对时间的精准度要求高。该算法的核心实现代码:
go
type Limiter struct {
LeakyRate int64
Capacity int
Token int
LastTokenTm int64
Lock *sync.Mutex
}
func (l *Limiter) Allow() bool {
l.Lock.Lock()
defer l.Lock.Unlock()
now := time.Now().UnixMilli()
tmFlow := now - l.LastTokenTm
tokenGen := tmFlow * l.LeakyRate / int64(1000)
l.Token += int(tokenGen)
if l.Token > l.Capacity {
l.Token = l.Capacity
}
if l.Token > 0 {
l.Token--
l.LastTokenTm = now
return true
}
return false
}
分布式限流算法
以上限流算法,均为服务器单机限流。在分布式系统中,一个服务通常会有多个实例,此时,需要支持分布式限流。该算法的实现,可以移步到笔者的github自行浏览:分布式限流算法实现
中心化的限流方案
中心化的限流,通常以redis作为中心,管理令牌算法。
该方案依赖于Redis的性能,如果Redis的性能存在瓶颈,可以采取Redis集群,或者提高Redis的服务器硬件配置
基于负载均衡限流
每个服务保障自己的限流,多个实例之间通过负载均衡或者服务发现机制,均分请求。该实现方式,每个服务依赖自己的服务缓存,需要自己控制限流精度,和实现限流的动态配置;同时,依赖服务均衡/注册中心的高可用,同时,也需要适应多个实例的动态扩缩容
基于分布式协调服务
分布式协调服务,即通过zookeeper或者etcd进行协调,此算法类似中心话的限流方案,对zookeeper/etcd有较高的可用性和性能要求
总结
服务端的限流,没有更好的方案,只有更适合的方案,需要开发者针对自己的业务特点即基础设施进行选择。除了限流外,也可以结合负载均衡、缓存、异步处理等方式一起,共同保障服务质量。