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:
@@ -1,19 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { VotingProgram, VoteStamp } from '@gala/shared/types';
|
||||
import type { VotingProgram, VoteStamp, AwardConfig } from '@gala/shared/types';
|
||||
import Postmark from './Postmark.vue';
|
||||
|
||||
// 票据类型名称映射
|
||||
const TICKET_TYPE_NAMES: Record<string, string> = {
|
||||
creative: '最佳创意',
|
||||
visual: '最佳视觉',
|
||||
atmosphere: '最佳氛围',
|
||||
performance: '最佳表演',
|
||||
teamwork: '最佳团队',
|
||||
popularity: '最受欢迎',
|
||||
potential: '最具潜力',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
program: VotingProgram;
|
||||
isFocused: boolean;
|
||||
@@ -24,6 +13,7 @@ interface Props {
|
||||
rotateZ?: number; // 3D 旋转 Z
|
||||
z?: number; // 3D 位移 Z
|
||||
index?: number; // 传入索引用于生成邮编
|
||||
awards?: AwardConfig[]; // 奖项配置
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -77,6 +67,15 @@ function getStampColor(ticketType: string): string {
|
||||
};
|
||||
return colors[ticketType] || '#e8313f';
|
||||
}
|
||||
|
||||
// 从 awards 配置中查找奖项名称
|
||||
function getAwardName(type: string): string {
|
||||
if (!props.awards || props.awards.length === 0) {
|
||||
return type;
|
||||
}
|
||||
const award = props.awards.find(a => a.id === type);
|
||||
return award?.name || type;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -158,7 +157,7 @@ function getStampColor(ticketType: string): string {
|
||||
:style="getStampStyle(stamp)"
|
||||
>
|
||||
<Postmark
|
||||
:award-name="TICKET_TYPE_NAMES[stamp.ticketType] || stamp.ticketType"
|
||||
:award-name="getAwardName(stamp.ticketType)"
|
||||
:user-name="stamp.userName"
|
||||
color="red"
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||
import PostcardItem from './PostcardItem.vue';
|
||||
import type { VotingProgram } from '@gala/shared/types';
|
||||
import type { VotingProgram, AwardConfig } from '@gala/shared/types';
|
||||
|
||||
export interface Props {
|
||||
programs: VotingProgram[];
|
||||
awards?: AwardConfig[];
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
}
|
||||
@@ -65,6 +66,7 @@ defineExpose({
|
||||
:order="program.order"
|
||||
:votes="program.votes"
|
||||
:stamps="program.stamps"
|
||||
:awards="awards"
|
||||
class="grid-item"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { VoteStamp } from '@gala/shared/types';
|
||||
import type { VoteStamp, AwardConfig } from '@gala/shared/types';
|
||||
import stampImage from '../assets/images/stamp-horse-2026.png';
|
||||
import Postmark from './Postmark.vue';
|
||||
|
||||
// 票据类型名称映射
|
||||
const TICKET_TYPE_NAMES: Record<string, string> = {
|
||||
creative: '最佳创意',
|
||||
visual: '最佳视觉',
|
||||
atmosphere: '最佳氛围',
|
||||
performance: '最佳表演',
|
||||
teamwork: '最佳团队',
|
||||
popularity: '最受欢迎',
|
||||
potential: '最具潜力',
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -24,6 +13,7 @@ export interface Props {
|
||||
order: number;
|
||||
votes: number;
|
||||
stamps?: VoteStamp[];
|
||||
awards?: AwardConfig[]; // 奖项配置(从后端获取)
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -72,19 +62,14 @@ function getStampStyle(stamp: VoteStamp) {
|
||||
// Stamp target ref for particle animation
|
||||
const stampTargetRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// Award name mapping for stamps
|
||||
const awardNames: Record<string, string> = {
|
||||
creative: '最佳创意奖',
|
||||
visual: '最佳视觉奖',
|
||||
atmosphere: '最佳气氛奖',
|
||||
performance: '最佳表演奖',
|
||||
teamwork: '最佳团队奖',
|
||||
popularity: '最受欢迎奖',
|
||||
potential: '最具潜力奖',
|
||||
};
|
||||
|
||||
// 从 awards 配置中查找奖项名称
|
||||
function getAwardName(type: string) {
|
||||
return awardNames[type] || '优秀节目奖';
|
||||
if (!props.awards || props.awards.length === 0) {
|
||||
// 如果没有 awards 配置,返回票种ID本身
|
||||
return type;
|
||||
}
|
||||
const award = props.awards.find(a => a.id === type);
|
||||
return award?.name || type;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -334,24 +334,14 @@ const lotteryPhaseLabel = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Award type labels
|
||||
const awardLabels: Record<string, string> = {
|
||||
creative: '最佳创意奖',
|
||||
visual: '最佳视觉奖',
|
||||
atmosphere: '最佳气氛奖',
|
||||
performance: '最佳表演奖',
|
||||
teamwork: '最佳团队奖',
|
||||
popularity: '最受欢迎奖',
|
||||
potential: '最具潜力奖',
|
||||
};
|
||||
|
||||
// Compute award statistics grouped by award type
|
||||
const awardStats = computed(() => {
|
||||
const stats: Record<string, Array<{ programName: string; teamName: string; votes: number; programId: string }>> = {};
|
||||
|
||||
// Initialize all award types
|
||||
Object.keys(awardLabels).forEach(type => {
|
||||
stats[type] = [];
|
||||
// Initialize all award types from admin.awards
|
||||
const awards = admin.awards || [];
|
||||
awards.forEach((award: any) => {
|
||||
stats[award.id] = [];
|
||||
});
|
||||
|
||||
// Aggregate stamps by award type
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useDisplayStore } from '../stores/display';
|
||||
import PostcardGrid from '../components/PostcardGrid.vue';
|
||||
import type { VotingProgram, AdminState } from '@gala/shared/types';
|
||||
import type { VotingProgram, AdminState, AwardConfig } from '@gala/shared/types';
|
||||
import { SOCKET_EVENTS } from '@gala/shared/constants';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -11,6 +11,7 @@ const displayStore = useDisplayStore();
|
||||
|
||||
// 节目列表
|
||||
const programs = ref<VotingProgram[]>([]);
|
||||
const awards = ref<AwardConfig[]>([]);
|
||||
const votingOpen = ref(false);
|
||||
const totalVotes = ref(0);
|
||||
|
||||
@@ -29,6 +30,7 @@ function goBack() {
|
||||
// 处理状态同步
|
||||
function handleStateSync(state: AdminState) {
|
||||
programs.value = state.voting.programs;
|
||||
awards.value = state.voting.awards || [];
|
||||
votingOpen.value = state.voting.subPhase === 'OPEN';
|
||||
totalVotes.value = state.voting.totalVotes;
|
||||
}
|
||||
@@ -101,6 +103,7 @@ onUnmounted(() => {
|
||||
<PostcardGrid
|
||||
ref="gridRef"
|
||||
:programs="programs"
|
||||
:awards="awards"
|
||||
:columns="4"
|
||||
:rows="2"
|
||||
/>
|
||||
|
||||
@@ -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 # 长时间停留
|
||||
|
||||
@@ -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;
|
||||
|
||||
156
packages/server/load-test/vote-real-scenario.yaml
Normal file
156
packages/server/load-test/vote-real-scenario.yaml
Normal 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
|
||||
@@ -17,6 +17,8 @@
|
||||
"test:load:smoke": "artillery run load-test/vote-load-test.yaml -e smoke",
|
||||
"test:load:peak": "artillery run load-test/vote-load-test.yaml -e peak",
|
||||
"test:load:stress": "artillery run load-test/vote-load-test.yaml -e stress",
|
||||
"test:load:gala": "artillery run load-test/vote-load-test.yaml -e gala",
|
||||
"test:load:live": "artillery run load-test/vote-real-scenario.yaml -e live",
|
||||
"test:load:report": "artillery run load-test/vote-load-test.yaml -e standard --output load-test/report.json && artillery report load-test/report.json"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -41,9 +41,15 @@ export async function initializeSocket(httpServer: HttpServer): Promise<GalaServ
|
||||
origin: config.corsOrigins,
|
||||
credentials: true,
|
||||
},
|
||||
// 高并发优化配置
|
||||
pingTimeout: config.socket.pingTimeout,
|
||||
pingInterval: config.socket.pingInterval,
|
||||
maxHttpBufferSize: config.socket.maxHttpBufferSize,
|
||||
connectTimeout: 45000, // 连接超时 45 秒
|
||||
perMessageDeflate: false, // 禁用压缩,减少 CPU/内存消耗
|
||||
transports: ['websocket', 'polling'], // 优先 WebSocket,但保留 polling 作为降级
|
||||
allowUpgrades: true, // 允许从 polling 升级到 websocket
|
||||
upgradeTimeout: 10000, // 升级超时 10 秒
|
||||
});
|
||||
|
||||
// Set up Redis adapter for horizontal scaling
|
||||
|
||||
Reference in New Issue
Block a user