-
背景 =====
-
系统性能现状评估
基于历史性能测试数据分析,当前系统承载能力存在以下瓶颈:
- • 认证服务:最大并发处理能力为3000用户
- • 下游业务系统:最大并发支持1500用户。 然而,在实际业务场景中,系统瞬时请求量经常突破4000 QPS。这种突发性流量冲击极易导致认证服务和下游系统出现过载风险,进而引发服务雪崩效应,严重影响系统可用性。
高并发流量控制方案
为有效应对瞬时高并发流量冲击,业界通常采用以下两种流量管控策略:
(1)硬性限流方案:基于Sentinel或Redission等框架实现请求速率限制,当系统QPS超过预设阈值时,直接拒绝超额请求,确保核心系统稳定运行。
(2)柔性排队方案:通过异步排队机制实现流量削峰,仅允许可控数量的用户进入系统,其余用户自动转入排队流程,并实时展示排队序号、当前队列长度及预估等待时长等关键信息。
基于保障下游系统在高并发场景下的稳定运行,同时期望通过本次实践沉淀一套可复用的非侵入式流量管控方案,经过充分的技术论证和方案比选,最终采用OpenResty+Lua
技术组合实现。该解决方案具备以下优势:
- • 零改造:无需修改现有系统架构
- • 高性能:利用Nginx高性能特性
- • 可扩展:支持动态调整排队策略
- • 可视化:提供实时排队状态反馈
- 方案架构设计 =========
本方案采用四层架构设计,通过模块化组件实现高并发流量管控。
-
- 接入层(Gateway Layer)
基于OpenResty集群构建,作为系统流量入口,主要负责请求预处理和负载均衡功能。该层对进入系统的请求进行初步处理,确保后续模块能够高效运行。
-
- 核心控制层(Control Layer)
采用Lua实现业务逻辑,包含以下核心模块:
2.1. 流量管控模块 (queue_control.lua):实现排队准入控制逻辑,管理用户访问授权。
2.2. 队列调度模块(queue_status.lua): 维护用户排队队列,计算并更新等待时间,实现自动重定向机制
2.3. 资源释放模块 (queue_release.lua):回收已完成用户的系统资源,这部分主要由业务系统实现。
2.4. 系统管理模块(queue_init.lua): 提供管理接口 , 系统参数初始化配置及队列重置。
-
- 数据持久层(Storage Layer)
采用Redis集群存储,主要包含四个主要数据结构:
| 键名 | 类型 | 说明 | | queue:zset | 有序集合(ZSET) | 存储用户会话信息(用户IP+时间戳),基于分数排序实现FIFO队列管理 | | queue:active:count | 字符串(STRING) | 原子计数器,实时记录当前已授权进入业务系统的活跃用户数量 | | queue:config:max_active | 字符串(STRING) | 系统配置参数,动态设置最大允许并发用户数,支持运行时调整 | | queue:authorized:set | 集合(SET) | 存储已获得访问授权的用户标识,实现快速鉴权查询 | | queue💓set | 有序集合(ZSET) | 存储用户的心跳信息 |
用户交互层(Presentation Layer)
实时展示系统负载状态和用户排队进度,当系统资源释放时,自动将排队靠前的用户重定向至业务系统,确保用户体验的流畅性。
- 核心实现原理 =========
系统采用分层式流量管控架构,通过动态容量控制实现高并发场景下的稳定服务。
各模块协同工作流程如下:
3.1. 系统容量设置(queue_init.lua))
系统采用弹性可扩展的容量控制策略,初始默认最大并发访问量设置为1000。该配置参数持久化存储于Redis数据库的queue:config:max\_active
键中,支持管理员根据实时业务负载情况进行动态调整。
curl "http://localhost:8089/api/queue/init?value=1000"
3.2. 用户访问控制(queue_control.lua)
在用户访问业务系统前,系统通过 access\_by\_lua\_file
加载 queue_control.lua 脚本,执行动态流量控制逻辑。该模块基于 Redis 存储的容量状态进行实时决策,确保系统在高并发场景下的稳定性。具体流程如下:
3.2.1. 已授权用户处理
系统首先检查用户是否已通过 Cookie 或 Redis 授权记录获得访问权限。若已授权,则直接刷新授权 Cookie 并放行至业务系统。
3.2.2. 未授权用户处理
系统根据当前并发量进行智能分流:
- • 当系统资源充足时(当前用户数小于最大容量),系统会立即授权用户访问。该过程包含以下操作:首先原子性地增加活跃用户计数并设置过期时间,然后将用户标识加入已授权集合,同时清理可能的排队记录,最后设置客户端授权Cookie以便后续快速验证。
- • 当系统达到满载状态时(当前用户数达到最大容量),系统会将新用户加入基于时间戳排序的等待队列
queue:zset
,并重定向至排队页面queue.html
3.2.3. 核心代码
if active\_count < max\_active then
-- 系统未满,直接授权用户访问
ngx.log(ngx.INFO, "[Queue Control] 系统未满,可以直接访问")
-- 增加活跃用户计数
red:incr(active\_count\_key)
-- 设置计数器过期时间,防止计数器永远不重置
red:expire(active\_count\_key, 3600) -- 1小时过期
-- 标记用户已被授权(使用SET存储)
red:sadd(authorized\_set, user\_id)
-- 从队列中移除用户(一旦用户被授权,立即从队列中移除)
red:zrem(queue\_zset, user\_id)
ngx.log(ngx.INFO, "[Queue Control] 用户已从排队队列中移除")
-- 设置cookie,便于后续请求直接验证
ngx.header["Set-Cookie"] = "health\_authorized=true; path=/health; max-age=1800"
returntrue -- 允许访问
end
-- 系统已满,直接添加用户到队列(或更新分数)
local score = ngx.now() -- 使用当前时间戳作为分数
red:zadd(queue\_zset, score, user\_id)
在高并发场景下,使用
if active\_count < max\_active
进行资源检查时会出现竞态条件问题,可能导致多个请求同时通过校验,产生类似"超卖"的现象。从技术上讲,最严谨的解决方案是引入分布式锁机制来保证强一致性,但这会显著降低系统吞吐量。经过权衡考量,由于排队系统对数据一致性的要求相对宽松,就算部分额外用户进入系统也不影响后续流程。所以在本方案中我们主动选择牺牲严格一致性以换取更高的系统性能和并发处理能力。
3.3. 排队页面处理(queue_status.lua)
当用户进入排队页面时,系统会调用queue\_status
接口计算并展示以下关键指标:当前系统排队总人数、该用户的实时排队位置以及预估等待时间。排队页面采用自动刷新机制,持续监测用户是否满足进入下游系统的条件。这部分内容通过lua脚本控制redis事务,防止高并发下出现问题。
为避免因用户异常退出导致的队列阻塞问题,系统设计了双重保障机制:一方面,在每次获取排队状态时都会同步更新用户心跳数据;另一方面,后台设有定时任务持续扫描队列,自动移除长时间无心跳响应的用户。这种机制有效解决了因用户非正常退出(如关闭浏览器)而造成的队列资源占用问题,确保排队队列的动态流动性,从而保障后续用户的排队体验。
3.3.1. 允许进入系统逻辑
系统通过三个关键步骤判断用户是否可以进入下游系统:
- • 首先计算当前系统的可用空位数量,通过最大容量减去当前活跃用户数得到准确数值。
-- 计算系统当前可用空位数量
local available\_slots = max\_active - active\_count
- • 然后确定用户在排队队列中的具体位置。系统查询Redis有序集合获取用户排名,如果出现用户不在队列中的异常情况,系统会将其默认置于队尾位置。
local rank = red:zrank(queue\_zset, user\_id)
local user\_position
if rank then
user\_position = rank + 1 -- Redis的rank是从0开始的
else
-- 如果用户不在队列中,作为异常情况处理
user\_position = queue\_length
end
- • 最后进行准入条件判断,系统需要同时满足两个关键条件:当前系统存在可用空位,且用户的排队位置处于可用空位数量范围内。这种双重验证机制既保证了系统负载均衡,又确保了排队公平性。
-- 判断条件:系统有空位且用户排在队列最前面的可进入用户范围内
local is\_system\_available = (available\_slots > 0)
local is\_user\_next\_in\_line = (user\_position <= available\_slots)
if is\_system\_available and is\_user\_next\_in\_line then
can\_enter = true
end
当用户满足准入条件时,系统会执行一系列原子化操作:增加活跃用户计数、标记用户授权状态、从排队队列中移除用户记录、设置安全Cookie,最后将用户重定向至下游系统。
3.3.2. 排队时间计算逻辑
系统设定最大同时活跃用户数为1000(即max_active=1000)。基于用户平均停留时长为3分钟(180秒) ,可以推导出用户离开速率的计算方式:
用
户
离
开
速
率
(
每
秒
)
平
均
停
留
时
长
(
秒
)
当某用户当前排队位置为N时,其预计等待时间的计算公式为:
预
计
等
待
时
间
排
队
位
置
用
户
离
开
速
率
即
3.3.3. 页面限流
在排队系统的实际运行中,用户在等待过程中往往表现出焦虑心理,导致频繁刷新页面以获取最新排队状态。这种高频访问行为会触发大量Redis查询和复杂的排队逻辑计算,对后端资源造成压力。为有效保护后端资源,我们为排队页面引入了基于固定窗口计数器算法 的限流机制。该算法具有实现简单、性能优秀、Redis集群兼容等特性,能够有效控制用户访问频率。
我们认识到固定窗口算法在时间窗口边界处可能出现短暂的流量突发现象,但考虑到排队系统的业务特点和容忍度,这一缺陷完全在可接受范围内。经过综合评估,该方案的整体收益远大于潜在风险,是当前场景下的最优解决方案。
核心代码如下:
local \_M = {}
-- 默认限流配置
local DEFAULT\_LIMITS = {
window\_size = 60, -- 时间窗口:60秒
max\_requests = 30, -- 最大请求数:30次/分钟
}
-- 执行限流检查 - Redis集群兼容版本
function \_M.check\_limit(path)
-- 获取用户ID
local visitor\_id = user\_identity.get\_user\_id()
ifnot visitor\_id then
ngx.log(ngx.ERR, "[Queue Limiter] 无法获取用户ID")
returnfalse, "无法识别用户"
end
-- 构造Redis键 - 使用时间窗口分片
local current\_window = math.floor(ngx.time() / DEFAULT\_LIMITS.window\_size)
local redis\_key = string.format("rate\_limit:%s:%d", visitor\_id, current\_window)
-- 执行限流检查(单键操作,集群兼容)
local result, err = redis\_pool.execute(function(red)
-- 获取当前计数
local current\_count = red:get(redis\_key)
if current\_count == ngx.null then
current\_count = 0
else
current\_count = tonumber(current\_count) or0
end
-- 检查是否超限
if current\_count >= DEFAULT\_LIMITS.max\_requests then
return {
allowed = false,
current = current\_count,
limit = DEFAULT\_LIMITS.max\_requests,
reset\_time = (current\_window + 1) * DEFAULT\_LIMITS.window\_size
}
end
-- 增加计数
local new\_count = red:incr(redis\_key)
-- 设置过期时间(首次设置)
if new\_count == 1then
red:expire(redis\_key, DEFAULT\_LIMITS.window\_size + 10)
end
return {
allowed = true,
current = new\_count,
limit = DEFAULT\_LIMITS.max\_requests,
reset\_time = (current\_window + 1) * DEFAULT\_LIMITS.window\_size
}
end)
if err then
ngx.log(ngx.ERR, "[Queue Limiter] Redis操作失败: ", err)
-- Redis故障时允许通过,避免影响正常访问
returntrue, "Redis故障,允许访问"
end
return result.allowed, result
end
return \_M
运行时效果如下:
3.4. 资源释放(queue_release.lua)
当用户完成业务逻辑处理后:系统将活跃用户数-1,为排队用户释放资源。这部分业务逻辑需要业务系统自行完成。
在用户完成业务逻辑处理后,系统将执行以下操作:活跃用户数量减1,并为排队中的用户释放相应的资源。这部分功能需由业务系统自行实现。
3.5. 定时任务清理
用户在排队页面时,前端会每10秒调用一次 /api/queue/status
接口更新心跳。系统使用 Redis ZSet 存储心跳数据,其中用户ID作为 member,最新心跳时间戳作为 score。
服务端设置了定时任务,每2分钟执行一次清理。任务会检查 Redis 中所有用户的心跳时间,如果发现某用户的最新心跳时间距当前时间超过120秒(即 score ≤ 当前时间戳-120000),就会将该用户从排队队列中移除。被移除的用户需要重新排队才能继续操作。
在清理用户时为了减少Redis网络交互次数,通过引入Pipeline和Table实现批处理。
-
- Pipeline 解决了网络延迟问题,将多个Redis命令打包成一次网络请求
-
- Table 提供了灵活的数据结构来管理批次数据和结果统计
-
- 分批处理 平衡了性能和内存使用,避免了单次操作过大的问题
心跳维护机制
用户在排队页面期间,前端会定期每10秒调用一次 /api/queue/status
接口来获取最新的排名信息并同时更新心跳状态。系统采用 Redis ZSet 数据结构存储用户心跳信息,以用户ID作为 member,最新心跳时间戳作为 score,确保数据的有序性和快速查询能力。
自动清理策略
为保证系统资源的合理利用,服务端配置了定时清理任务,每2分钟自动执行一次心跳检查。清理逻辑会扫描 Redis 中所有用户的心跳记录,一旦发现某用户的最新心跳时间距当前时间超过120秒(即 score ≤ 当前时间戳 - 120),系统将自动将该用户从排队队列中移除。被清理的用户若需继续使用服务,必须重新进入排队流程。
高性能批处理优化
考虑到大规模用户场景下的性能需求,清理任务采用了三层优化策略来最小化Redis网络交互开销:
-
- Pipeline 技术:将多个Redis删除命令打包为单次网络请求,显著降低网络延迟影响,将原本 N 次网络往返优化为 ceil(N/批次大小) 次
-
- Table 数据管理:使用Lua Table作为灵活的数据容器,高效管理批次用户数据和清理结果统计,同时便于内存管理和垃圾回收
-
- 分批处理策略:设置每批1000个用户的处理上限,在保证高性能的同时避免单次操作数据量过大对Redis服务造成压力,实现了性能与稳定性的最佳平衡
这种设计确保了即使在高并发、大用户量的场景下,清理任务也能快速高效地完成,维护系统的整体稳定性。
核心代码如下:
local batch\_size = 1000-- 每批处理1000个用户
local total\_users = #inactive\_users
local batch\_count = math.ceil(total\_users / batch\_size)
for batch = 1, batch\_count do
local start\_idx = (batch - 1) * batch\_size + 1
local end\_idx = math.min(batch * batch\_size, total\_users)
local batch\_users = {}
-- 准备当前批次的用户列表
for i = start\_idx, end\_idx do
table.insert(batch\_users, inactive\_users[i])
end
-- 批量清理队列用户(使用Pipeline)
red:init\_pipeline()
-- 批量添加删除命令到Pipeline
for \_, user\_id inipairs(batch\_users) do
red:zrem(queue\_zset, user\_id)
end
-- 一次性执行当前批次的所有命令
local results, pipeline\_err = red:commit\_pipeline()
if results then
-- 统计当前批次实际清理的用户数
local batch\_cleanup\_count = 0
for \_, result inipairs(results) do
if result == 1then
batch\_cleanup\_count = batch\_cleanup\_count + 1
end
end
cleanup\_count = cleanup\_count + batch\_cleanup\_count
else
-- 回退到逐个清理方式
for \_, user\_id inipairs(batch\_users) do
local removed = red:zrem(queue\_zset, user\_id)
if removed == 1then
cleanup\_count = cleanup\_count + 1
end
end
end
end
- 性能测试 =======
通过性能测试结果表明:在单机测试中,受限于服务器硬件性能差异,最佳单机吞吐量为833.3 QPS,平均响应时间为0.348秒。采用10台服务器集群部署后,系统性能实现大幅跃升 ,在2000并发负载下展现出最佳性能表现,吞吐量大幅提升至4062 QPS,平均响应时间保持稳定在0.355秒。测试数据显示,系统在2000并发时达到最佳性能平衡点,超过此并发阈值后性能提升呈现边际递减效应。因此,建议生产环境优先采用集群部署架构,并将并发量控制在2000左右,以获得最佳的性能表现和资源利用效率。综合测试结果:集群架构能够稳定处理3000并发请求,峰值吞吐量可达4000+ QPS,响应时间严格控制在0.3-0.6秒区间内,完全满足中等规模业务场景下的排队系统性能需求。
最后欢迎加入苏三的星球,你将获得:100万QPS短链系统、复杂的商城微服务系统、苏三AI项目、刷题吧小程序、秒杀系统、商城系统、秒杀系统、代码生成工具等8个项目的源代码、开发教程和技术答疑。
系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。
还有1V1免费修改简历、技术答疑、职业规划、送书活动、技术交流。
目前星球已经更新了5800+篇优质内容,还在持续爆肝中.....
扫描下方二维码即可加入星球:
现在加入是非常合适的,本月底要涨价了。