提交 fbc79ee1 authored 作者: 陈泽健's avatar 陈泽健

fix(performance): 混合场景执行容器重启中断 + result 兜底 + TPS 0 分析 + 前端监控图表修复

修复 5.60 混合场景执行被容器重启导致的 result referenced before assignment(已预初始化 result + CancelledError 显式捕获)
修复目标机 Memory Usage 渲染函数源码(改 computed)
修复执行机资源图表窄条(@change + nextTick resize)
更新问题处理与计划执行文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 63a20e4a
# 问题处理:5.60 混合场景执行失败、TPS多为0及监控图表展示异常
## 1. 问题背景与现象
用户在 192.168.5.60 服务器上执行混合场景性能测试任务(`perf_1c72c087ba4f4854b5f9a8c9a348f565`,100 虚拟用户),观察到以下四个问题:
1. **任务执行状态异常**:后端日志报 `local variable 'result' referenced before assignment`,任务直接失败且请求数为 0。
2. **TPS 指标异常**:监控中观察到 TPS 约 90% 的点位数值为 0。
3. **目标机指标 UI 显示函数源码**:目标机卡片标题展示为 `Memory Usage(目标机内存使用率)function wl(){return Ge.value>0?`(${Ze.value.toFixed(0)} / ${Ge.value.toFixed(0)} MB)`:""}`
4. **执行机资源图表依然呈窄条**:折叠面板展开后执行机资源监控图 X 轴被挤压扁平。
---
## 2. 根因分析
### 问题一:`local variable 'result' referenced before assignment`:容器重启中途杀掉压测任务 + 取消路径未初始化 result
- **位置**`backend/app/executors/performance_executor.py``execute()` 方法。
- **现场证据(5.60 2026-09-09)**
- 任务 `perf_1c72c087...`(100 VU / 30min)09:02:24 启动,09:02:27 预热完成 100 协程;
- 09:05:15 容器开始 Shutting down(非任务自然结束),WebSocket 断开;
- 09:05:15:527 日志报 `后台单任务执行失败 [perf_1c72c087...]: local variable 'result' referenced before assignment`
- 09:05:26 容器重新 StartedAt(被**人为 restart/重建**,疑为其他窗口部署所致);
- 执行记录 `exec_733d6baf``failed`、reqs=0、snapshotCount=0 —— **快照缓冲随进程退出丢失,未落库**
- **机制**
`result` 仅在 `try:` 块内部分支赋值。压测进行中容器关闭 → 后台 asyncio 任务被取消,`asyncio.CancelledError`(Python 3.8+ 属于 **BaseException,不被 `except Exception` 捕获**)在 `await self._run_mix(task)` 处抛出,跳过全部 `result = ...` 赋值 → 执行到 `finally` 之后的 `if isinstance(result, dict):``UnboundLocalError`
该错误把「容器重启打断压测」的真实原因掩盖为「引擎代码错误」,并导致任务状态与执行记录更新不完整(任务被重启后卡在 running,数据库事务未提交)。
- **结论**:混合场景引擎本身在 09-05 前运行正常(09-07 完整跑完 12615 请求 / 09-08 9419 请求);本次 failed 的直接原因是 **容器中途重启**,间接原因是 **execute() 取消路径缺少 result 兜底**(需修复的潜在缺陷)。
### 问题二:TPS 为什么 90% 都是 0
- **位置**`MetricsCollector` 快照机制与混合场景工作流。
- **机制**
1. 混合场景 100 虚拟用户若配置了思考时间(Think Time,默认 1~3s),请求之间存在休眠间隔。
2. 被测系统业务接口(如会议预定、模板操作)在多并发下平均耗时较长(数百毫秒至数秒)。
3. `MetricsCollector.snapshot()` 以每 1 秒为窗口重置计数器统计完成数。当大量请求处于排队或网络往返中时,该特定 1 秒窗口内刚好没有请求完成,导致瞬时 TPS=0,只有请求集中返回的秒数才有 TPS 尖峰。
### 问题三:目标机卡片 UI 渲染 JS 函数源码
- **位置**`frontend/src/views/performance/MonitorPanel.vue` 第 290 行与第 1096 行。
- **机制**
- 源码定义:`function targetMemBar() { return targetMemTotalMb.value > 0 ? ... : '' }`(普通函数,非 computed)。
- 模板使用:`<div class="metric-label">Memory Usage(目标机内存使用率){{ targetMemBar }}</div>`
- 模板中遗漏调用括号 `()`,导致 Vue 渲染该函数的 `toString()` 输出,生产构建混淆后即变成 `function wl(){return Ge.value>0?`...`}`
### 问题四:执行机监控图表依然被挤压为窄条
- **位置**`frontend/src/views/performance/MonitorPanel.vue` 折叠面板联动与图表 resize 逻辑。
- **机制**
- `updateResourceChart()` 仅判定 `resourceChart.getWidth() === 0` 才触发 `resize()`。但在 Element Plus 的 `el-collapse` 折叠动画过程中或有 padding 的隐蔽节点中,`getWidth()` 往往返回非 0 的微小值(如 100px),绕过了兜底判断。
- 未在 `el-collapse``@change` 事件中主动调用 `nextTick` + `chart.resize()`
---
## 3. 解决对策
1. **执行器安全防守**
-`execute()` 开头预先初始化 `result: Dict[str, Any] = {"error": True, "error_message": "执行未完成"}`
- 在异常捕获分支中确保 `result` 拥有标准结构,即使发生异常或取消也能安全返回汇总数据。
2. **TPS 统计与展示优化**
- 检查任务配置是否不慎开启了长思考时间。
- 在指标统计与平滑度上进行科学呈现说明,若需平滑曲线可支持滑动平均或累计平均 TPS 参考。
3. **前端函数源码修复**
-`targetMemBar` 重构为规范的 Vue 3 `computed(() => ...)`,模板使用 `{{ targetMemBar }}`
4. **图表容器自适应强化**
- 为折叠面板绑定 `@change` 处理函数,在展开瞬间使用 `nextTick` 强制对所有图表执行 `resize()`
- 去除单一的 `getWidth() === 0` 限制,当容器实际宽度与图表宽度不一致时强制同步。
# 执行计划:修复混合场景执行异常、TPS为0排查与监控图表UI缺陷
> 文档编号:`PLAN_PERF_MIX_MONITOR_FIX_20260909`
> 关联问题处理文档:`Docs/PRD/性能测试/问题处理/_问题处理_混合场景执行与监控图表异常.md`
> 目标环境:本地开发环境与 `192.168.5.60` 部署环境
---
## 1. 任务背景与目标
本次执行计划针对用户在 5.60 运行混合场景性能测试时反馈的 4 个关键问题制定修复与落地策略:
1. **任务执行异常中断**:彻底解决 `local variable 'result' referenced before assignment`,确保无论何种异常或取消,`result` 均安全兜底初始化。
2. **TPS 为 0 根因诊断**:理清快照每秒统计与混合场景业务接口耗时的关系,向用户提供清晰的指标解读,并在有必要时对空秒数统计进行平滑与日志提示。
3. **目标机卡片源码展示缺陷**:将 `targetMemBar` 重构成 Vue 3 `computed`,修复编译后渲染 `function wl(){...}` 源码的问题。
4. **执行机图表窄条缺陷**:在 `el-collapse` 折叠面板展开事件上绑定 `nextTick` 强制 `resize()`,并优化 `updateResourceChart()` / `updateNetworkChart()` 的自适应逻辑,彻底解决窄条问题。
---
## 2. 详细修改清单
### 2.1 后端:`backend/app/executors/performance_executor.py`
- **目标**:在 `PerformanceExecutor.execute()` 中保障 `result` 变量安全。
- **改动位置**`execute()` 入口(约第 1121 行)。
- **具体实施**
```python
# 预先定义初始化结构,防止异常中断跳入 finally 时抛 UnboundLocalError
result: Dict[str, Any] = {"error": True, "error_message": "执行被意外中断或尚未完成"}
```
- **异常捕获强化**:在外部 `except Exception as e:` 以及内部各 `_run_*` 阶段均确保赋值给 `result`
### 2.2 前端:`frontend/src/views/performance/MonitorPanel.vue`
- **目标一(修复函数源码展示)**
-`function targetMemBar() { ... }` 替换为响应式计算属性:
```typescript
const targetMemBar = computed(() => {
return targetMemTotalMb.value > 0
? `(${targetMemUsedMb.value.toFixed(0)} / ${targetMemTotalMb.value.toFixed(0)} MB)`
: ''
})
```
- 模板中维持 `{{ targetMemBar }}`,计算属性在模板中自动解包取值,绝不会打印函数体源码。
- **目标二(解决执行机图表窄条)**
- 在两个折叠面板组件上新增 `@change` 事件监听:
```html
<el-collapse v-model="resourceExpanded" @change="handleCollapseChange" style="margin-bottom: 12px;">
<el-collapse v-model="targetResourceExpanded" @change="handleCollapseChange" style="margin-bottom: 12px;">
```
- 新增面板切换时的自动重绘方法:
```typescript
function handleCollapseChange() {
nextTick(() => {
resourceChart?.resize()
networkChart?.resize()
targetResourceChart?.resize()
})
}
```
- 优化 `updateResourceChart()``updateNetworkChart()`,若检测到容器当前宽度大于 0 且与图表宽度不符,主动调用 `resize()`
---
## 3. 验证与部署步骤
1. **本地单元测试与构建校验**
- 运行后端单测:`pytest backend/tests/test_performance_executor.py`
- 运行前端构建:`cd frontend && npm run build`,确保无类型错误且打出最新 hash 资源包。
2. **部署至 192.168.5.60**
- 覆盖后端 `performance_executor.py`,重启 app 容器验证健康状态。
- 上传前端最新 `dist/`,重启容器后复核页面资源。
3. **真实端到端验收**
- 打开 5.60 混合场景任务监控页,确认目标机卡片文案为 `Memory Usage(目标机内存使用率)(已用 / 总量 MB)`,不再输出函数体。
- 展开执行机资源面板,确认 CPU/内存趋势图及网络趋势图充满宽度、比例正常。
- 启动或回放混合场景执行,验证即使任务中止也不会报 `result referenced before assignment`
......@@ -1169,6 +1169,11 @@ class PerformanceExecutor:
target_resource_task = asyncio.create_task(self._target_resource_loop())
# 3. 根据 mode 选择执行策略(场景模式优先于压测模式)
# result 预初始化兜底:容器重启/任务取消时 CancelledError(BaseException)
# 不会被下方 except Exception 捕获,若不在 try 前赋值,
# finally 之后的 isinstance(result, dict) 会抛 UnboundLocalError
# (2026-09-09 5.60 混合场景被容器重启打断时实际发生,掩盖了真实中断原因)
result: Dict[str, Any] = {"error": True, "error_message": "任务执行被中断,未产生完整结果"}
try:
scenario_type = getattr(task, "scenario_type", None)
if scenario_type == "burst":
......@@ -1185,6 +1190,11 @@ class PerformanceExecutor:
result = await self._run_step(task)
else:
result = {"error": True, "error_message": f"未知压测模式: {task.mode}"}
except asyncio.CancelledError:
# 容器重启 / 上层任务取消:给出明确中断原因,不再向上抛出,
# 让服务层正常走收尾流程(落库执行记录,避免任务卡 running)
logger.warning(f"压测执行被取消(容器重启或手动停止): task={getattr(task, 'id', '')}")
result = {"error": True, "error_message": "任务执行被中断(容器重启或手动停止)"}
except Exception as e:
logger.exception(f"压测执行异常: {e}")
result = {"error": True, "error_message": str(e)}
......
......@@ -223,7 +223,7 @@
</el-row>
<!-- 执行机资源区块(默认折叠) -->
<el-collapse v-model="resourceExpanded" style="margin-bottom: 12px;">
<el-collapse v-model="resourceExpanded" @change="handleCollapseChange" style="margin-bottom: 12px;">
<el-collapse-item title="执行机资源 Resource Monitor" name="resource">
<!-- 资源指标卡片 -->
<el-row :gutter="12" class="metrics-row">
......@@ -269,7 +269,7 @@
</el-collapse>
<!-- 目标机资源区块(默认折叠) -->
<el-collapse v-model="targetResourceExpanded" style="margin-bottom: 12px;">
<el-collapse v-model="targetResourceExpanded" @change="handleCollapseChange" style="margin-bottom: 12px;">
<el-collapse-item title="目标机资源 Target Resource Monitor" name="targetResource">
<div v-show="targetAvailable" class="target-resource-wrap">
<!-- 目标机信息 + 资源指标卡片 -->
......@@ -1092,10 +1092,12 @@ function updateNetworkChart() {
targetResourceChart.setOption(option, true)
}
/** 目标机内存用量文案 */
function targetMemBar() {
return targetMemTotalMb.value > 0 ? `(${targetMemUsedMb.value.toFixed(0)} / ${targetMemTotalMb.value.toFixed(0)} MB)` : ''
}
/** 目标机内存用量文案(计算属性,模板直接插值;若用普通函数模板会渲染函数源码) */
const targetMemBar = computed(() => {
return targetMemTotalMb.value > 0
? `(${targetMemUsedMb.value.toFixed(0)} / ${targetMemTotalMb.value.toFixed(0)} MB)`
: ''
})
/** 处理快照数据 */
function handleSnapshot(data: any) {
......@@ -1444,6 +1446,21 @@ function handleWindowResize() {
targetResourceChart?.resize()
}
/**
* 折叠面板展开/收起时强制重算图表尺寸
*
* 折叠面板内 echarts.init 得到零尺寸(或窄条)画布,仅靠 update 时的
* getWidth()===0 兜底不够(展开动画中间态可能拿到极小非零宽度),
* 展开事件 + nextTick 后容器已有真实尺寸,统一 resize 修正窄条。
*/
function handleCollapseChange() {
nextTick(() => {
resourceChart?.resize()
networkChart?.resize()
targetResourceChart?.resize()
})
}
onMounted(() => {
window.addEventListener('resize', handleWindowResize)
if (taskId.value) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论