服务限流算法与实现

面对高并发、大流量的应用场景时,服务为了保护自己避免崩溃,经常采用限流的措施。限流分为不同方式限流,比如:

  • 合法性验证限流:比如验证码、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有较高的可用性和性能要求

总结

服务端的限流,没有更好的方案,只有更适合的方案,需要开发者针对自己的业务特点即基础设施进行选择。除了限流外,也可以结合负载均衡、缓存、异步处理等方式一起,共同保障服务质量。

相关推荐
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
WX187021128735 小时前
在分布式光伏电站如何进行电能质量的治理?
分布式