目录
- 一.什么是限流
- 二.怎么做限流呢
- 2.1 有哪些常见的系统限流算法
- 2.1.1 固定窗口
- 2.1.1 滑动窗口
- 2.1.2 令牌桶
- 2.1.3 漏桶算法
- 2.2 常见的限流方式
- 2.2.1 单机限流&集群限流
- 2.2.2 前置限流&后置限流
- 2.3 实际落地是怎么做的
- 2.3.1 流量链路
- 2.3.2 各链路限流
- 2.3.2.1 网关层
- 2.3.2.2 nginx 层
- 2.3.3.3 服务本身的限流
一.什么是限流
应用系统在自身有限资源下,在面对流量突发场景下的自我保护.这可能是最初的一个目标.在此之上,怎么在有限的资源下去完成系统的最好的性能表现.
那么总结下其实有两个原则.
- 系统保护(主要目标)
- 有限资源下的系统最佳吞吐(理想目标)
有了这两个原则,那么其实我们后续关注的就是如何友好的落地这些点.
二.怎么做限流呢
2.1 有哪些常见的系统限流算法
2.1.1 固定窗口
描述:这是一种简单的限流算法,它根据一个固定的时间窗口来控制流量.在给定时间窗口内,它允许固定数量的请求通过,超出的请求将被丢弃.
实现步骤:
- 初始化一个计数器(counter)为0,并设置一个固定的时间窗口(window).
- 当收到一个请求时,检查计数器的值.
- 如果计数器小于限制的请求数量(limit),则允许请求通过并增加计数器的值.
- 如果计数器已经达到限制的请求数量,丢弃请求或者返回一个错误.
- 如果请求的时间超过了时间窗口的结束时间,重置计数器为1,并更新时间窗口为当前时间窗口.
limit = 100 # 限制的请求数量
window = 60 # 时间窗口为60秒
def handle_request():
global counter, window_start_time
//开始时间
current_time = time.time()
//60 秒的窗口
if current_time > window_start_time + window:
counter = 0
//重置开始时间
window_start_time = current_time
if counter < limit:
counter += 1
# 处理请求
else:
# 丢弃请求或返回错误
相比之下 固定窗口算法的问题在于,无法应对在流量在突发场景下的冲击.在固定窗口下.el:10:00:00 - 10:00:01 时间窗口 60s. 限流值为 1w.
现实过程可能是如下这种场景.
这种场景对于固定窗口算法来说,造成的问题就是大量的请求都集中在固定窗口的某一段小窗口时间,导致窗口的 pass count 快速被消耗.这样即使后续的平滑流量甚至小
流量都难以继续通过窗口.
2.1.1 滑动窗口
描述:这是一种基于固定窗口算法优化之后的限流算法,在固定窗口算法的基础上引入了滑动窗口的概念.它允许窗口内的请求速率平滑地变化,而不仅仅是在固定窗口内进行限制.
实现步骤:
- 初始化一个计数器(counter)为0,并设置一个滑动窗口的时间长度(window)和一个固定时间间隔(interval).
- 当收到一个请求时,检查计数器的值.
- 如果计数器小于限制的请求数量(limit),则允许请求通过并增加计数器的值.
- 如果计数器已经达到限制的请求数量,丢弃请求或者返回一个错误.
- 每隔固定时间间隔,将计数器的值减去窗口内的请求数量,并更新窗口内的请求数量.
limit = 100 # 限制的请求数量
window = 60 # 滑动窗口的时间长度为60秒
interval = 10 # 固定时间间隔为10秒
def handle_request():
global counter, window_start_time, window_requests
current_time = time.time()
if current_time > window_start_time + window:
counter -= window_requests.pop(0)
window_requests.append(0)
window_start_time = current_time
if counter < limit:
counter += 1
window_requests[-1] += 1
# 处理请求
else:
# 丢弃请求或返回错误
2.1.2 令牌桶
描述:原理是系统以恒定的速率产生令牌,并将这些令牌放入一个桶中.当有请求到达时,需要从桶中获取一个令牌才能继续处理该请求.如果桶中没有足够的令牌,则请求被拒绝.
特点:既能够面对突发的流量峰值,也能处理平滑的系统请求.
具体来说,令牌桶算法的实现包括两个关键参数:令牌生成速率(rate)和桶的容量(capacity).令牌生成速率确定了每秒产生的令牌数量,桶的容量确定了桶中最多可以存放多少个令牌.
import time
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # 令牌生成速率(令牌/秒)
self.capacity = capacity # 桶的容量(令牌数量)
self.tokens = 0 # 当前桶中的令牌数量
self.last_time = time.time() # 上次令牌生成时间
def get_token(self):
now = time.time()
elapsed = now - self.last_time # 计算时间间隔
self.last_time = now
self.tokens = min(self.tokens + elapsed * self.rate, self.capacity) # 生成令牌
if self.tokens >= 1:
self.tokens -= 1
return True
else:
return False
# 示例用法
bucket = TokenBucket(rate=1, capacity=5) # 令牌生成速率为1个/秒,桶的容量为5个令牌
for i in range(10):
if bucket.get_token():
print(f"请求 {i+1} 被处理")
else:
print(f"请求 {i+1} 被限流")
time.sleep(1) # 等待1秒再重新尝试获取令牌
2.1.3 漏桶算法
漏桶算法(Leaky Bucket Algorithm)是一种流量控制算法,用于平滑网络流量.它模拟了一个漏桶,当网络流量超过桶的容量时,多余的流量会被丢弃或者缓存,以保持流量的稳定性.
漏桶算法的原理是,维护一个固定容量的漏桶,每次流量到达时,先将流量放入漏桶中.如果漏桶已满,则丢弃超出容量的流量;如果漏桶未满,则将流量处理完成.然后,漏桶以固定速率漏水,即以固定速度释放流量.这样,即使流量突然增大,漏桶也可以平滑地处理流量.
class LeakyBucket:
def __init__(self, capacity, rate):
self.capacity = capacity # 漏桶容量
self.rate = rate # 漏水速率
self.water = 0 # 当前水量
self.last_leak_time = time.time() # 上次漏水时间
def process(self, amount):
current_time = time.time()
time_passed = current_time - self.last_leak_time
self.water = max(0, self.water - self.rate * time_passed) # 漏水
if self.water + amount <= self.capacity:
self.water += amount
self.last_leak_time = current_time
return True # 流量处理成功
else:
return False # 漏桶已满,丢弃流量
令牌桶算法更适合平滑限制流量,可以灵活地处理不同请求的流量需求;而漏桶算法更适合于固定速率的流量处理,可以有效地限制请求的处理速度 .对应到实际项目过程中,
漏桶的特点的就是恒定流速,令牌的特点就是可以应对突击的流量,应对秒杀大促等的场景.
2.2 常见的限流方式
2.2.1 单机限流&集群限流
单机限流和集群限流是常见的流量控制方式,它们在实现和应用上有一些区别,并且各自具有不同的优缺点.下面是单机限流和集群限流的区别以及它们的优缺点:
- 区别:
单机限流:在单个服务器或节点上进行限流,仅对该节点的流量进行控制.
集群限流:在整个集群或多个服务器上进行限流,可以对整个系统的流量进行控制.
-
单机限流的优缺点:
- 优点:
实现简单:单机限流通常只需要在单个节点上实现限流逻辑,不需要进行节点间的数据共享和协调.
高效低延迟:限流操作只涉及当前节点,因此具有较高的效率和低延迟.
- 缺点:
有限的扩展性:单机限流的处理能力受限于单个节点的性能,当流量过大时,单个节点可能无法承受.
-
集群限流的优缺点:
- 优点:
全局控制能力:集群限流可以在整个集群或多个节点上进行流量控制,实现全局的流量控制策略.
高可伸缩性:通过增加节点来扩展处理能力,可以更好地应对高流量的情况.
- 缺点:
复杂性增加:集群限流需要在多个节点之间进行数据共享和协调,增加了系统的复杂性.
网络开销:集群限流需要节点间的通信和协调,可能会带来一定的网络延迟和额外的开销.
总的来说单机限流适用于对单个节点或服务器进行流量控制的场景,简单、高效,但扩展性有限;
集群限流适用于对整个系统或多个节点进行流量控制的场景,具有全局控制能力和高可伸缩性,但复杂度和网络开销较高.
选择合适的限流方式应根据具体的应用需求和系统架构来决定.
2.2.2 前置限流&后置限流
前置限流和后置限流是两种常见的流量控制方式,它们在应用时机和实现方式上有一些区别,并且各自具有不同的优缺点.
区别:
前置限流:在请求到达服务之前进行流量控制,即在请求进入系统之前进行限流处理.
后置限流:在请求离开服务之后进行流量控制,即在请求处理完成后进行限流处理.
前置限流的优缺点:
- 优点:
提前过滤请求:前置限流可以在请求进入系统之前进行过滤和拒绝,减轻服务的负担.
快速响应:由于限流操作在请求进入系统之前进行,可以快速地进行限流判断,避免不必要的请求处理.
- 缺点:
精确度有限:前置限流通常只能基于部分请求信息进行限流判断,无法对具体请求的处理结果进行评估.
需要额外资源:前置限流需要在请求进入系统之前进行处理,可能需要额外的流量控制组件或网络设备.
后置限流的优缺点:
- 优点:
基于实际处理结果:后置限流可以根据请求的实际处理结果进行限流判断,更加准确地控制流量.
动态调整:后置限流可以根据实时的系统负载和性能情况,动态调整限流策略.
- 缺点:
延迟较高:后置限流需要等待请求处理完成后才能进行判断和限流操作,可能会引入较高的延迟.
需要额外资源:后置限流需要在请求处理完成后进行处理,可能需要额外的流量控制组件或网络设备.
从上面的描述知道前置限流适用于在请求进入系统之前进行快速的流量控制,能够提前过滤请求和快速响应,但精确度有限;
后置限流适用于根据请求的实际处理结果进行流量控制,能够基于实际情况进行动态调整,但可能引入较高的延迟.
2.3 实际落地是怎么做的
2.3.1 流量链路
在介绍实际的限流方案需要简单介绍下整体流量链路.
以上是大致的一个流量链路.
step1:从整个入口到网关是整体流量的入口.
step2:从入口到各个 service 的 nginx
step3:从 nginx 再到本地的 service 接口
目前系统提供的服务分为两个部分.http 接口以及 rpc 接口.先说 http 接口这一块.这块主要支撑的是整个
主站的服务.具体的协议也是 http 的形式.通过反向代理挂载每一个逻辑分组(一个分组多个机器).
2.3.2 各链路限流
2.3.2.1 网关层
首先是整个网关这一层
这一层主要是用来代理整个公司级别的服务.这一层目前是用后置限流来处理进行处理的.
后置限流:后置限流是指在请求处理完之后,再根据请求的处理结果来判断是否需要限制请求的处理速率.即先处理请求,然后根据处理结果来决定是否限流.
这种方式可以确保请求的处理逻辑不受限流策略的影响,但可能会导致一些请求在处理完后才被限制,从而可能导致系统的负载过大.
前置限流:前置限流是指在请求处理之前,根据系统的负载情况或者其他指标来判断是否需要限制请求的处理速率.即在处理请求之前就根据限流策略来决定是否拒绝该请求.
这种方式可以避免系统负载过大,但可能会导致一些请求被提前拒绝,从而可能影响系统的正常运行.
通过上面的描述能知道后置限流的逻辑是其实目的是不干扰整个业务逻辑的执行,但是在整体流量的防控上做一次兜底.其实本质是没有做到下游的系统保护,这种方式的限流对象
仅仅是流量,就没有系统保护这一层的能力.
2.3.2.2 nginx 层
第二层是nginx 到服务这一层.
这一层目前是有两个维度.
根据 request 请求的方法指定方法请求级别的限流.
这个基本是对于单机限流的一个兜底,在服务的集群限流可能出现的一些漂移现象后的系统保护.
还有一个维度是每次请求都通过 lua 获取本地的物理机 cpu 指标. 具体的限制指标度量,可以依照压测对于 cpu 的观测.
-- 执行系统命令并获取输出结果
function execute_command(command)
local handle = io.popen(command)
local result = handle:read("*a")
handle:close()
return result
end
-- 获取系统的CPU信息
function get_cpu_info()
local cpu_info = execute_command("cat /proc/cpuinfo")
return cpu_info
end
-- 示例代码
local cpu_info = get_cpu_info()
print(cpu_info)
2.3.3.3 服务本身的限流
上面的限流基本是在服务本身之外的.或网关或流量代理实现的.
对于大型的分布式系统一般是选用集群限流.能很好的因为一些节点的故障带来的整体流量的承载能力的下载如果是单机限流,在节点故障后相当于整体的系统负载能力就削弱了.
具体的限流阈值有两个点.
- 或者根据相应的往年的监控流量值来进行推算.
- 具体根据压测时的阈值来进行调整.
第一点需要系统具备完整的监控体系.以获取足够的历史数据来进行推演.有的同学说了,我的系统或者我的接口是第一次上线无法获取足够的历史监控来推测.
系统第一次上线的场景:只能与 qa 测试这边打一个配合,完整的链路压测来彻底了解你的系统.获取到系统在可接受的接口性能下如:200ms.下的最大 单机qps或整体的集群最大 qps.
如果是单机压测,那么集群的限流就是线上机器数*单机承压的 qps 了.
还有一个点对于集群限流的场景,还是觉得有必要 mark 下.对于 rpc 服务来说,特别是上下游关系复杂调用方较多.这时候有单纯的一个集群限流
往往解决不了问题.很可能需要根据不同的调用方来实现不同的集群限流.例如:对于核心的,量大的 调用方单独的集群限流.如 2 个这种调用方就需要两个
集群限流 key.其他的一些小流量这样可以归并到 other 这样的集群限流渠道就好了.
集群限流的实现方式 sentinel
预取率:指的是在集群限流场景下.本地的调用端对于集群 token 的预取占比.举个例子:集群限流是 10000.预取:7000.那么预取率则是 70%.
假如有 70 台机器.则每个机器的 token 预取个数则是 10.
目前集群限流的过程
通过图示可以看出图上为令牌桶限流算法.
通过平台设置指定 key 的限流阈值.
consumer 启动完成 token 预取.预取的逻辑很常见.(如:美团的 leaf 的框架也有设计.好的框架总是借鉴来借钱去,这里也是套用电影里的说法
致敬)
实际请求过程中通过token 验证通过,完成对 provider 的调用.验证不通过则触发限流.
赠人玫瑰 手有余香 我是柏修 一名持续更新的晚熟程序员
期待您的点赞,关注加收藏,加个关注不迷路,感谢
您的鼓励是我更新的最大动力
↓↓↓↓↓↓