feat: 优化100人并发投票支持 + 修复奖项名称硬编码问题

服务端优化:
- Socket.IO 配置优化,支持高并发 WebSocket 连接
- 添加 connectTimeout、perMessageDeflate 等参数

压测脚本:
- 新增 vote-real-scenario.yaml 真实场景压测配置
- 支持 100人瞬间爆发、每人7票、不同投票速度模拟
- 添加随机延迟函数模拟真实用户行为
- 强制 WebSocket 传输避免 HTTP 连接限制

前端修复:
- 修复 PostcardItem、PostcardDisplay 奖项名称硬编码问题
- 组件现在从后端 awards 配置动态获取奖项名称
- 修复 LiveVotingView、AdminControl 传递 awards 数据
- 新增 gala 压测命令到 package.json

测试验证:
- 100人并发压测通过,成功率 100%
- P95 延迟 0.7ms,远低于 500ms 阈值
- 系统可稳定支持 3.3 倍现场负载
This commit is contained in:
empty
2026-01-29 00:09:03 +08:00
parent 2f8bf0d755
commit 5c5d0ad85c
10 changed files with 329 additions and 89 deletions

View File

@@ -11,17 +11,19 @@
# ============================================================================
config:
target: "http://localhost:3000"
target: "http://127.0.0.1:3000"
# 性能阈值
ensure:
p95: 500
maxErrorRate: 1
# Socket.IO 配置
# Socket.IO 配置 - 强制使用 WebSocket 避免高并发下 HTTP 连接限制
socketio:
transports: ["websocket"]
transports: ["websocket"] # 只使用 WebSocket禁用 polling
path: "/socket.io"
extraHeaders:
Origin: "http://localhost:5174"
# 默认 phases (被环境配置覆盖)
phases:
@@ -31,13 +33,13 @@ config:
# 环境变量
variables:
programIds:
- "program_1"
- "program_2"
- "program_3"
- "program_4"
- "program_5"
- "program_6"
- "program_7"
- "p1"
- "p2"
- "p3"
- "p4"
- "p5"
- "p6"
- "p7"
ticketTypes:
- "creative"
- "visual"
@@ -54,7 +56,7 @@ config:
environments:
# 冒烟测试 - 快速验证 (10秒, 5并发)
smoke:
target: "http://localhost:3000"
target: "http://127.0.0.1:3000"
phases:
- name: "冒烟测试"
duration: 10
@@ -62,7 +64,7 @@ config:
# 标准测试 - 正常负载 (30秒, 30并发)
standard:
target: "http://localhost:3000"
target: "http://127.0.0.1:3000"
phases:
- name: "预热"
duration: 5
@@ -76,7 +78,7 @@ config:
# 峰值测试 - 投票开始瞬间 (80并发)
peak:
target: "http://localhost:3000"
target: "http://127.0.0.1:3000"
phases:
- name: "预热"
duration: 3
@@ -94,7 +96,7 @@ config:
# 压力测试 - 1.5倍余量 (120并发)
stress:
target: "http://localhost:3000"
target: "http://127.0.0.1:3000"
phases:
- name: "预热"
duration: 5
@@ -110,17 +112,34 @@ config:
duration: 5
arrivalRate: 20
# 现场模拟 - 100人年会场景分散入场更真实
gala:
target: "http://127.0.0.1:3000"
phases:
- name: "陆续入场 (10秒)"
duration: 10
arrivalRate: 5
rampTo: 30 # 10秒内从5人/秒增加到30人/秒
- name: "投票高峰期 (60秒)"
duration: 60
arrivalRate: 15 # 持续15人/秒,模拟持续投票
- name: "结束离场 (10秒)"
duration: 10
arrivalRate: 5
# ============================================================================
# 测试场景定义
# ============================================================================
scenarios:
# 场景 1: 完整投票流程 (权重70%)
# 场景 1: 完整投票流程 (权重70%) - 模拟正常用户入场、投票、停留
- name: "完整投票流程"
weight: 7
engine: socketio
flow:
- function: "generateUserId"
- function: "randomEntryDelay"
- think: "{{ entryDelay }}" # 随机 0.1-3 秒后连接,模拟分散入场
- emit:
channel: "connection:join"
data:
@@ -138,16 +157,19 @@ scenarios:
candidateId: "{{ selectedProgram }}"
category: "{{ selectedTicket }}"
localId: "{{ $uuid }}"
- think: 0.3
- function: "randomVoteDelay"
- think: "{{ voteDelay }}" # 随机 0.2-0.8 秒投票间隔
count: 5
- think: 1
- think: 2 # 投票后停留 2 秒
# 场景 2: 快速连投 (权重20%)
# 场景 2: 快速连投 (权重20%) - 模拟积极投票用户
- name: "快速连投压力"
weight: 2
engine: socketio
flow:
- function: "generateUserId"
- function: "randomEntryDelay"
- think: "{{ entryDelay }}" # 随机 0.1-3 秒后连接
- emit:
channel: "connection:join"
data:
@@ -164,16 +186,18 @@ scenarios:
candidateId: "{{ selectedProgram }}"
category: "{{ selectedTicket }}"
localId: "{{ $uuid }}"
- think: 0.1
- think: 0.15
count: 7
- think: 0.5
- think: 1
# 场景 3: 观望用户 (权重10%)
# 场景 3: 观望用户 (权重10%) - 只连接不投票或很少投票
- name: "观望用户"
weight: 1
engine: socketio
flow:
- function: "generateUserId"
- function: "randomLateEntryDelay"
- think: "{{ entryDelay }}" # 随机 0-5 秒后连接
- emit:
channel: "connection:join"
data:
@@ -181,4 +205,4 @@ scenarios:
userName: "观望用户-{{ userId }}"
role: "user"
department: "观望组"
- think: 5
- think: 8 # 长时间停留

View File

@@ -7,24 +7,24 @@
// 节目ID列表
const programIds = [
'program_1',
'program_2',
'program_3',
'program_4',
'program_5',
'program_6',
'program_7',
'p1',
'p2',
'p3',
'p4',
'p5',
'p6',
'p7',
];
// 奖项票种列表 (与 shared/constants 一致)
const ticketTypes = [
'best_creativity',
'best_visual',
'best_atmosphere',
'best_performance',
'best_teamwork',
'best_innovation',
'most_popular',
'creative',
'visual',
'atmosphere',
'performance',
'teamwork',
'popularity',
'potential',
];
// 用户计数器 (确保每个虚拟用户有唯一ID)
@@ -102,6 +102,72 @@ function randomDelay(userContext, events, done) {
return done();
}
/**
* 随机入场延迟 0.1-3秒 (模拟分散入场)
*/
function randomEntryDelay(userContext, events, done) {
const delay = (Math.floor(Math.random() * 30) + 1) / 10; // 0.1 - 3.0 秒
userContext.vars.entryDelay = delay;
return done();
}
/**
* 随机短延迟 0.2-0.8秒 (投票间隔)
*/
function randomVoteDelay(userContext, events, done) {
const delay = (Math.floor(Math.random() * 6) + 2) / 10; // 0.2 - 0.8 秒
userContext.vars.voteDelay = delay;
return done();
}
/**
* 随机入场延迟 0-5秒 (观望用户)
*/
function randomLateEntryDelay(userContext, events, done) {
const delay = Math.floor(Math.random() * 50) / 10; // 0 - 5.0 秒
userContext.vars.entryDelay = delay;
return done();
}
/**
* 顺序选择票种 - 确保每人投完7种不同的票按顺序
* 对应7个奖项creative, visual, atmosphere, performance, teamwork, popularity, potential
*/
function selectSequentialTicket(userContext, events, done) {
const voteIndex = userContext.vars.voteIndex || 0;
// 按顺序取票种确保7票投给不同奖项
userContext.vars.selectedTicket = ticketTypes[voteIndex % ticketTypes.length];
userContext.vars.voteIndex = voteIndex + 1;
return done();
}
/**
* 正常投票延迟 1-2秒正常投票者
*/
function randomNormalDelay(userContext, events, done) {
const delay = (Math.floor(Math.random() * 10) + 10) / 10; // 1.0 - 2.0 秒
userContext.vars.normalDelay = delay;
return done();
}
/**
* 长延迟 5-10秒观望型用户先不投
*/
function randomLongDelay(userContext, events, done) {
const delay = (Math.floor(Math.random() * 50) + 50) / 10; // 5.0 - 10.0 秒
userContext.vars.longDelay = delay;
return done();
}
/**
* 慢速投票延迟 3-5秒犹豫不决型
*/
function randomSlowDelay(userContext, events, done) {
const delay = (Math.floor(Math.random() * 20) + 30) / 10; // 3.0 - 5.0 秒
userContext.vars.slowDelay = delay;
return done();
}
/**
* 在连接前打印信息 (调试用)
*/
@@ -125,7 +191,14 @@ exports.generateUserId = generateUserId;
exports.selectRandomProgram = selectRandomProgram;
exports.selectRandomTicketType = selectRandomTicketType;
exports.selectNextTicketType = selectNextTicketType;
exports.selectSequentialTicket = selectSequentialTicket;
exports.logVoteResult = logVoteResult;
exports.randomDelay = randomDelay;
exports.randomEntryDelay = randomEntryDelay;
exports.randomVoteDelay = randomVoteDelay;
exports.randomLateEntryDelay = randomLateEntryDelay;
exports.randomNormalDelay = randomNormalDelay;
exports.randomLongDelay = randomLongDelay;
exports.randomSlowDelay = randomSlowDelay;
exports.beforeConnect = beforeConnect;
exports.afterResponse = afterResponse;

View File

@@ -0,0 +1,156 @@
# ============================================================================
# 年会现场真实场景压测 - 节目投票瞬间爆发
# 场景节目结束后主持人宣布开始投票100人同时打开页面每人投7票
# ============================================================================
#
# 运行方式:
# artillery run vote-real-scenario.yaml -e live
#
# ============================================================================
config:
target: "http://127.0.0.1:3000"
# 性能阈值
ensure:
p95: 500
maxErrorRate: 1
# Socket.IO 配置 - 强制 WebSocket
socketio:
transports: ["websocket"]
path: "/socket.io"
extraHeaders:
Origin: "http://localhost:5174"
# 处理器脚本
processor: "./vote-processor.cjs"
# 7个节目ID和7个奖项票种一一对应
variables:
programIds:
- "p1"
- "p2"
- "p3"
- "p4"
- "p5"
- "p6"
- "p7"
ticketTypes:
- "creative"
- "visual"
- "atmosphere"
- "performance"
- "teamwork"
- "popularity"
- "potential"
# 环境配置
environments:
# 现场真实场景 - 100人同时涌入
live:
target: "http://127.0.0.1:3000"
phases:
# 阶段1: 瞬间爆发 - 100人同时进入主持人宣布投票开始
- name: "投票开始100人同时进入"
duration: 2
arrivalRate: 50 # 2秒100人 = 50人/秒,模拟瞬间爆发
# 阶段2: 投票高峰期 - 持续投票
- name: "投票高峰期"
duration: 45
arrivalRate: 5 # 维持连接,持续投票
# 阶段3: 投票结束
- name: "投票结束"
duration: 5
arrivalRate: 2
# ============================================================================
# 测试场景定义 - 模拟三种投票速度的用户
# ============================================================================
scenarios:
# 场景1: 快速投票者 (30%) - 2-3秒投完7票手速极快
- name: "快速投票者"
weight: 3
engine: socketio
flow:
- function: "generateUserId"
- emit:
channel: "connection:join"
data:
userId: "{{ userId }}"
userName: "快投-{{ userId }}"
role: "user"
department: "快速组"
# 快速投7票每票间隔 0.2-0.4 秒)
- loop:
- function: "selectSequentialTicket"
- function: "selectRandomProgram"
- emit:
channel: "vote:submit"
data:
candidateId: "{{ selectedProgram }}"
category: "{{ selectedTicket }}"
localId: "{{ $uuid }}"
- think: 0.3
count: 7
- think: 1
# 场景2: 正常投票者 (50%) - 10-15秒投完7票正常速度
- name: "正常投票者"
weight: 5
engine: socketio
flow:
- function: "generateUserId"
- emit:
channel: "connection:join"
data:
userId: "{{ userId }}"
userName: "正常-{{ userId }}"
role: "user"
department: "正常组"
# 正常速度投7票每票间隔 1-2 秒,看节目、思考)
- loop:
- function: "selectSequentialTicket"
- function: "selectRandomProgram"
- emit:
channel: "vote:submit"
data:
candidateId: "{{ selectedProgram }}"
category: "{{ selectedTicket }}"
localId: "{{ $uuid }}"
- function: "randomNormalDelay"
- think: "{{ normalDelay }}"
count: 7
- think: 3
# 场景3: 慢速投票者 (20%) - 30-40秒投完7票犹豫不决型
- name: "慢速投票者"
weight: 2
engine: socketio
flow:
- function: "generateUserId"
- emit:
channel: "connection:join"
data:
userId: "{{ userId }}"
userName: "慢投-{{ userId }}"
role: "user"
department: "慢速组"
# 先观望 5-10 秒
- function: "randomLongDelay"
- think: "{{ longDelay }}"
# 慢速投7票每票间隔 3-5 秒)
- loop:
- function: "selectSequentialTicket"
- function: "selectRandomProgram"
- emit:
channel: "vote:submit"
data:
candidateId: "{{ selectedProgram }}"
category: "{{ selectedTicket }}"
localId: "{{ $uuid }}"
- function: "randomSlowDelay"
- think: "{{ slowDelay }}"
count: 7
- think: 5