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

feat(performance): 目标机新增 MySQL 服务资源监控

- target_resource_monitor.py: 通过 docker exec 采集 umysql 容器指标(连接数/线程/慢查询/Buffer Pool 命中率/流量),与系统资源并行采集,失败不影响系统指标
- performance_executor.py: 报告构建时汇总 MySQL 指标到 target_resource_summary
- performance.ts: 新增 TargetResourceData.mysql 类型定义
- MonitorPanel.vue: 新增 MySQL 指标卡片 + 实时趋势图(连接数/活跃线程)
- performance_service.py: 报告摘要填充任务关联字段(target_url/method/mode/scenario_type)
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 702af195
......@@ -3,8 +3,64 @@
> **生成时间**: 2026-08-24
> **当前分支**: `platform-auto-test`
> **最近提交**: `cf359994` feat(performance): WebSocket 批量进度订阅端点 + 项目详情实时进度条
> **会话窗口**: 性能测试 — 执行跳转闭环 + 执行历史;本次:登录压测响应验证 + 双 bug 修复部署
> **状态**: ✅ 执行跳转闭环与执行历史全链路已实现/验证/部署 5.60;本会话登录压测响应验证通过 + 修复 2 个 bug(outputs 500 / 僵尸 running 双条)已部署 5.60 + 清理 3 条僵尸记录
> **会话窗口**: 性能测试 — 目标机 MySQL 资源监控(后端采集扩展 + 前端实时展示)
> **状态**: ✅ 目标机 MySQL 服务资源监控完整实现(后端采集扩展 + 前端 MonitorPanel 实时展示 + 报告汇总)
---
## ⚡ 最新会话更新(2026-08-25)— 目标机 MySQL 服务资源监控(已完整实现)
### A. 功能背景
用户要求目标机资源监控中增加 MySQL 服务(umysql 容器)的指标采集和展示。核心决策:
- **采集方式**:通过 SSH 在目标机执行 `docker exec umysql mysql ...` 命令,从容器外采集 MySQL 指标,不进入容器内部
- **采集命令**`SHOW GLOBAL STATUS` 一次性采集全部指标,制表符分隔输出,单次往返
- **并行采集**:MySQL 采集作为独立 SSH 命令(与系统资源采集并行),失败不影响系统 CPU/内存/负载指标
- **监控对象**`umysql` 容器(业务数据库,端口 8306),非平台自用 `plat-auto-test-mysql`
- **凭证获取**:通过 `docker inspect umysql` 获取 root 密码为 `dNrprU&2S`
### B. 已实现 ✅
| 组件 | 文件 | 说明 |
|------|------|------|
| 后端采集 | `backend/app/executors/target_resource_monitor.py` | 新增 `_MYSQL_CMD` 常量(`docker exec umysql mysql ... SHOW GLOBAL STATUS`)、`_mysql_enabled` 标志、`_execute()` 中并行采集 MySQL、`_parse_mysql()` 解析制表符输出、`sample()` 返回值扩展 `mysql` 字段(20+ 指标,含 bufferPoolHitRate 计算) |
| 后端汇总 | `backend/app/executors/performance_executor.py` | `_build_target_resource_summary()` 扩展 MySQL 汇总指标提取(最新快照值 + 趋势序列) |
| 前端类型 | `frontend/src/types/performance.ts` | `TargetResourceData.mysql` 可选字段,含 15 个 MySQL 指标 |
| 前端展示 | `frontend/src/views/performance/MonitorPanel.vue` | MySQL 指标卡片(连接数/活跃线程/慢查询/缓存命中率/最大连接数/查询总量/收发数据量) + 趋势图(MySQL 连接数 + 活跃线程两条线叠加在目标机资源图中) |
**关键实现细节**
- **采集命令**`docker exec umysql mysql -u root -p'dNrprU&2S' -N --batch -e "SELECT 'version', VERSION(); SHOW GLOBAL STATUS WHERE ...; SHOW VARIABLES LIKE 'max_connections'"` 2>/dev/null
- **累计值指标**(Connections、Bytes_received、Slow_queries):取最新快照值展示
- **瞬时值指标**(Threads_connected、Threads_running):可实时展示趋势
- **Buffer Pool 命中率**`bufferPoolHitRate = read_requests / (read_requests + disk_reads) * 100`
- **MySQL 采集失败**:标记 `mysql.available=False`,不影响系统资源采集
- **前端指标卡片**:Threads Connected(连接数,>10 红色)、Threads Running(活跃线程,>5 红色)、Slow Queries(慢查询累计,>10 红色)、Buffer Pool Hit Rate(缓存命中率,<99% 红色)、Max Used / Max Connections、Questions、Bytes Received/Sent
- **趋势图**:MySQL 连接数(绿色实线)和活跃线程(灰色虚线)叠加在目标机资源趋势图的 Load/连接数坐标系上
### C. 验证结果 ✅
| 验证项 | 结果 |
|--------|------|
| 后端 MySQL 采集自测 | ✅ 通过(conn=30, run=2, max_conn=151, slow=0, buf_hit=100%, recv=923.5MB) |
| 前端类型检查 `npm run build` | ✅ 通过(27.32s) |
### D. 本次修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `backend/app/executors/target_resource_monitor.py` | MySQL 采集命令 + 解析 + sample() 扩展 |
| `backend/app/executors/performance_executor.py` | `_build_target_resource_summary()` 扩展 MySQL 汇总 |
| `frontend/src/types/performance.ts` | `TargetResourceData.mysql` 可选字段 |
| `frontend/src/views/performance/MonitorPanel.vue` | MySQL 指标卡片 + 趋势图 + handleSnapshot/replaySnapshots 解析 |
### E. 本次会话待办(交接给下一窗口)
| # | 任务 | 说明 |
|---|------|------|
| 1 | **git commit + push** | 4 个文件,用 `/GitCommit` skill |
| 2 | **部署 5.60** | 上传 `target_resource_monitor.py` + `performance_executor.py` + 前端 `dist/*``docker compose restart app` |
| 3 | **ReportPanel 报告页 MySQL 汇总展示** | 当前报告页 `targetResourceSummary` 已有 MySQL 汇总字段,但 ReportPanel.vue 未展示,可参考 MonitorPanel 的卡片布局补充 |
| 4 | 遗留未提交文件 | `backend/data/test_platform.db` 有工作区变更 |
---
......@@ -21,6 +77,78 @@
---
## ⚡ 最新会话更新(2026-08-24 晚)— 性能报告 AI 分析功能(已完整实现 + 部署 5.60)
### A. 功能背景
报告页(ReportPanel.vue)跑完压测后只能人工查看指标数字,用户要求引入 AI 自动分析报告数据,给出瓶颈定位和优化建议。用户确认的决策:
- **调用方式**:新增后端 API 直接调用 LLM(Claude CLI)
- **展示方式**:嵌入报告页(ReportPanel.vue)
- **分析维度**:单次执行 + 多版本对比 + 资源瓶颈分析,尽量详细
- **LLM 通道**:新建独立 `PerformanceAiService`,复用 ClaudeService 的 3 级 CLI 回退调用链(本地 `claude --print` → SSH sshpass → paramiko),不重用元素定位逻辑
### B. 已实现 ✅
| 组件 | 文件 | 说明 |
|------|------|------|
| 后端服务 | `backend/app/services/performance_ai_service.py`(新建,~700 行) | `analyze_single` / `analyze_comparison`(报告数组排序按 start_time)/ `_build_prompt` / `_call_claude_cli`(3 级回退)/ `_parse_json_response` / `_rule_based_analysis` / `_rule_based_comparison` |
| 后端 Schema | `backend/app/schemas/performance.py` | `AiAnalysisResponse`(execution_id/analysis_type/overall_verdict/key_findings/bottlenecks/suggestions/resource_analysis/trend_analysis/comparison/raw_response/source)+ `AiAnalysisCompareRequest`(execution_ids min_length=2,to_camel 别名) |
| 后端路由 | `backend/app/routers/performance.py` | `POST /api/performance/executions/{execution_id}/ai-analysis`(单次);`POST /api/performance/ai-analysis/compare`(多版本对比) |
| 前端类型 | `frontend/src/types/performance.ts` | `AiAnalysisResponse` 接口 |
| 前端 API | `frontend/src/api/performance.ts` | `getAiAnalysis(executionId)` / `getAiComparison(executionIds[])` |
| 前端页面 | `frontend/src/views/performance/ReportPanel.vue` | 「AI 分析」按钮(MagicStick 图标)+ 可折叠 AI 分析卡片(el-skeleton 加载 / 总体评价 Markdown / 关键发现表 / 瓶颈分析表 / 优化建议表 P0P1P2 / 资源分析 / 版本对比表,对比方向着色:回归红 / 改善绿)+ `marked` 渲染 |
**关键实现细节**
- **阈值常量**:P95 <300ms 绿 / 300-1000 / 1000 红;Apdex 0.94 绿 / 0.85 黄;错误率 >5% 异常;CPU >80% 资源瓶颈
- **资源字段兼容**:resource_summary 键可能是 `cpu_avg``cpu_average`,读取时两者都检查
- **规则兜底**:Claude CLI 不可用时返回规则分析,`source: "rule"`(前端显示「规则分析」tag)
- **对比分析**:逐指标计算 delta 标注方向(improved/regressed/flat)
- 执行记录型报告的 `summary` 中 target_url/method/mode/scenario_type 为空(由 execution 聚合而来),prompt 构造时已适配
### C. 验证结果 ✅
| 验证项 | 结果 |
|--------|------|
| 前端构建 `npm run build` | ✅ 通过(41.05s) |
| 后端 import 检查 | ✅ imports OK |
| 本地 git commit | ✅ `702af195`(24 文件 / +5907 行) |
| 推送远程 | ✅ `cf359994..702af195 → platform-auto-test` |
| 5.60 部署 | ✅ 前端 dist 热替换 + 8 个后端文件 scp + `docker compose restart app`,health 正常 |
| 5.60 实测 AI 单次分析 | ✅ `POST /executions/exec_54309f7e.../ai-analysis` → source:rule,识别 P95 1852ms 瓶颈 + P0 建议(5.60 容器内无可用的 claude CLI,走规则兜底,符合预期) |
### D. 本次修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `backend/app/services/performance_ai_service.py` | **新建** AI 分析服务 |
| `backend/app/schemas/performance.py` | `AiAnalysisResponse` + `AiAnalysisCompareRequest` |
| `backend/app/routers/performance.py` | 2 个 AI 分析端点 |
| `frontend/src/types/performance.ts` | `AiAnalysisResponse` 类型 |
| `frontend/src/api/performance.ts` | `getAiAnalysis` / `getAiComparison` |
| `frontend/src/views/performance/ReportPanel.vue` | AI 分析按钮 + 卡片 + marked 渲染 |
| `Docs/PRD/性能测试/问题处理/`(4 份) | 上一窗口遗留:捕获输出 500 + 执行记录双条僵尸 running 的问题处理/执行计划文档 |
| `backend/data/test_platform.db` | 一并提交(用户确认) |
> 提交 `702af195` 同时包含了上一窗口遗留的「执行跳转闭环 / 捕获输出查询 500 / 执行记录双条僵尸 running」修复(与 AI 分析一同提交)。
### E. 部署记录(5.60)
- 上传文件:`performance_ai_service.py` + `routers/performance.py` + `schemas/performance.py` + `services/performance_service.py` + `database.py` + `executors/performance_executor.py` + `models/performance.py` + `routers/performance_output.py` + 前端 `dist/*`
- 重启:`cd /data/third_party/plat-auto-test/deploy && docker compose restart app`
- 验证:`curl http://192.168.5.60/health``{"status":"healthy"...}`;AI 分析端点实测返回结构化 JSON
### F. 本次会话待办(交接给下一窗口)
| # | 任务 | 说明 |
|---|------|------|
| 1 | **AI 对比分析前端入口(可选)** | `getAiComparison` API 已就绪,ReportPanel 目前仅按钮触发单次分析,多版本对比入口未接入 UI |
| 2 | **PRD 文档(可选)** | 按 CLAUDE.md 工作流可补 `_PRD_性能报告AI分析.md`,本次用户直接要求实现,跳过 |
| 3 | **端到端回归** | 浏览器打开 http://192.168.5.60 → 性能测试报告 → 选执行记录 → 「AI 分析」(5.60 无 claude CLI 走规则兜底;若本地 Windows 有 `where claude` 可验证完整 AI 链路) |
| 4 | 遗留未提交文件 | `backend/scripts/probe_44_202.py``frontend/public/模板_CSV用户数据.xlsx`(用户确认不提交) |
| 5 | backend/data/test_platform.db 未提交差异 | `git status` 显示仍 M(工作区变更未暂存,上次因 gitignore 仅提交了暂存态),如需同步可 `git add -f backend/data/test_platform.db` 后提交 |
---
## ⚡ 最新会话更新(2026-08-24 晚)— 登录压测响应验证 + 双 bug 修复部署
### A. 会话背景
......
......@@ -2209,6 +2209,31 @@ class PerformanceExecutor:
latest_target = dict(self._latest_target_resource) if self._latest_target_resource else None
if latest_target:
snapshot["targetResource"] = latest_target
# 兼容字段:同时输出 camelCase 别名(前端 WebSocket 监控直接读取)
camel_aliases = {
"concurrentUsers": "concurrent_users",
"avgResponseTime": "avg_response_time",
"minResponseTime": "min_response_time",
"maxResponseTime": "max_response_time",
"p50ResponseTime": "p50",
"p90ResponseTime": "p90",
"p95ResponseTime": "p95",
"p99ResponseTime": "p99",
"stdDev": "std_dev",
"latencyAvg": "latency_avg",
"connectTimeAvg": "connect_time_avg",
"peakTps": "peak_tps",
"successCount": "success_count",
"failCount": "fail_count",
"status2xx": "status_2xx",
"status3xx": "status_3xx",
"status4xx": "status_4xx",
"status5xx": "status_5xx",
"totalRequests": "total_requests",
}
for alias, key in camel_aliases.items():
if key in snapshot and alias not in snapshot:
snapshot[alias] = snapshot[key]
if self._metrics.snapshot_callback:
self._metrics.snapshot_callback(snapshot)
except Exception as e:
......@@ -2366,16 +2391,25 @@ class PerformanceExecutor:
cpu_vals = [s.get("cpuPercent", 0.0) for s in usable]
mem_vals = [s.get("memPercent", 0.0) for s in usable]
load_vals = [s.get("loadAvg1", 0.0) for s in usable]
# MySQL 指标(仅统计 available 的样本;直接取最新值——累计值无平均意义)
mysql_samples = [s.get("mysql", {}) for s in usable if s.get("mysql", {}).get("available")]
mysql_latest = mysql_samples[-1] if mysql_samples else None
first_ts = usable[0].get("timestamp", 0.0)
series = []
for s in usable:
series.append({
entry = {
"elapsed": round(s.get("timestamp", 0.0) - first_ts, 1) if first_ts else 0.0,
"cpuPercent": s.get("cpuPercent", 0.0),
"memPercent": s.get("memPercent", 0.0),
"loadAvg1": s.get("loadAvg1", 0.0),
})
}
# 合并 MySQL 系列数据(连接数与运行线程数)
mysql = s.get("mysql", {})
if mysql.get("available"):
entry["mysqlThreadsConnected"] = mysql.get("threadsConnected")
entry["mysqlThreadsRunning"] = mysql.get("threadsRunning")
series.append(entry)
# ====== Java 进程汇总 ======
# 收集所有样本中出现的 Java 进程按 processName 组织
......@@ -2436,6 +2470,24 @@ class PerformanceExecutor:
},
"series": series,
}
if mysql_latest:
result["mysql"] = {
"available": True,
"container": mysql_latest.get("container", "umysql"),
"threadsConnected": mysql_latest.get("threadsConnected"),
"threadsRunning": mysql_latest.get("threadsRunning"),
"maxUsedConnections": mysql_latest.get("maxUsedConnections"),
"maxConnections": mysql_latest.get("maxConnections"),
"connections": mysql_latest.get("connections"),
"abortedConnects": mysql_latest.get("abortedConnects"),
"questions": mysql_latest.get("questions"),
"queries": mysql_latest.get("queries"),
"slowQueries": mysql_latest.get("slowQueries"),
"bytesReceivedMb": mysql_latest.get("bytesReceivedMb"),
"bytesSentMb": mysql_latest.get("bytesSentMb"),
"bufferPoolHitRate": mysql_latest.get("bufferPoolHitRate"),
"uptime": mysql_latest.get("uptime"),
}
if java_process_stats:
result["javaProcessStats"] = java_process_stats
result["javaProcessSeries"] = java_process_series
......
......@@ -39,6 +39,25 @@ _SSH_CMD = (
"ps -eo %cpu=,%mem=,rss=,pid=,comm=,args= | grep -E 'java|jdk' | grep -v grep"
)
# MySQL 容器采集命令(目标机 Docker 内的 umysql)
# 容器名 / 数据库:umysql(ubains 业务库,含登录/会议等被测服务依赖数据)
# MySQL 连接:docker exec 容器内执行 mysql 客户端(root 密码见容器 MYSQL_ROOT_PASSWORD)
# 采集 SHOW GLOBAL STATUS 中的连接/查询/流量/缓存指标 + max_connections
# -N --batch:跳过表头、制表符分隔,便于解析
# 2>/dev/null:丢弃 mysql 客户端的密码明文告警
_MYSQL_CMD = (
"docker exec umysql mysql -u root -p'dNrprU&2S' -N --batch -e \""
"SELECT 'version', VERSION(); "
"SHOW GLOBAL STATUS WHERE Variable_name IN ("
"'Uptime','Threads_connected','Threads_running','Threads_created',"
"'Connections','Max_used_connections','Aborted_connects',"
"'Questions','Queries','Slow_queries',"
"'Bytes_received','Bytes_sent',"
"'Innodb_buffer_pool_read_requests','Innodb_buffer_pool_reads',"
"'Innodb_rows_read','Com_select','Com_insert','Com_update','Com_delete'"
"); SHOW VARIABLES LIKE 'max_connections';\" 2>/dev/null"
)
# 匹配的 Java 进程关键字(命中任一即纳入监控;覆盖会议相关服务)
_JAVA_PROCESS_KEYWORDS = [
"ubains-meeting-inner-api",
......@@ -120,6 +139,9 @@ class TargetResourceMonitor:
# CPU 差值基准(首次采样建基线,第二次才有差值)
self._cpu_last: Optional[Tuple[float, float]] = None # (idle, total)
# MySQL 监控独立开关:默认开启,MySQL 采集失败不影响系统指标采集
self._mysql_enabled = True
# ==================== 对外接口 ====================
def sample(self) -> dict:
......@@ -158,6 +180,32 @@ class TargetResourceMonitor:
"loadAvg5": 0.0,
"loadAvg15": 0.0,
"javaProcesses": [],
# MySQL 指标(采样失败或不可用时为 0 / None)
"mysql": {
"available": False,
"container": "umysql",
"version": None,
"uptime": None,
"threadsConnected": None,
"threadsRunning": None,
"maxUsedConnections": None,
"maxConnections": None,
"connections": None,
"abortedConnects": None,
"questions": None,
"queries": None,
"slowQueries": None,
"bytesReceivedMb": None,
"bytesSentMb": None,
"bufferPoolReadRequests": None,
"bufferPoolReads": None,
"bufferPoolHitRate": None,
"rowsRead": None,
"comSelect": None,
"comInsert": None,
"comUpdate": None,
"comDelete": None,
},
"timestamp": time.time(),
"available": not self._disabled,
}
......@@ -184,6 +232,7 @@ class TargetResourceMonitor:
mem = self._parse_mem(output)
load = self._parse_load(output)
java_processes = self._parse_java_processes(output)
mysql = self._parse_mysql(output)
result["cpuPercent"] = cpu
result["memPercent"] = mem[0]
......@@ -193,6 +242,7 @@ class TargetResourceMonitor:
result["loadAvg5"] = load[1]
result["loadAvg15"] = load[2]
result["javaProcesses"] = java_processes
result["mysql"] = mysql
result["available"] = True
except Exception as e:
......@@ -217,6 +267,7 @@ class TargetResourceMonitor:
self._client = None
self._connected = False
self._cpu_last = None
self._mysql_enabled = True
# ==================== SSH 连接管理 ====================
......@@ -264,7 +315,7 @@ class TargetResourceMonitor:
def _execute(self) -> Optional[str]:
"""
执行 SSH 采集命令
执行 SSH 采集命令(系统资源 + MySQL 指标)
Returns:
Optional[str]: 命令输出文本,失败返回 None
......@@ -273,11 +324,31 @@ class TargetResourceMonitor:
return None
try:
# 系统资源采集
_, stdout, stderr = self._client.exec_command(_SSH_CMD, timeout=15) # type: ignore
output = stdout.read().decode("utf-8", errors="replace")
err = stderr.read().decode("utf-8", errors="replace").strip()
if err:
logger.debug(f"SSH 命令 stderr: {err}")
# MySQL 指标采集(独立命令,失败不影响系统资源输出)
try:
_, mysql_stdout, mysql_stderr = self._client.exec_command(
"(" + _MYSQL_CMD + ") 2>/dev/null || echo '---MYSQL-FAIL---'",
timeout=15,
)
mysql_output = mysql_stdout.read().decode("utf-8", errors="replace")
mysql_err = mysql_stderr.read().decode("utf-8", errors="replace").strip()
if mysql_err and "mysql" in mysql_err.lower():
logger.debug(f"MySQL 采集 stderr: {mysql_err}")
# 失败标记:后续拼接的 MySQL 输出以该标记开头
if "---MYSQL-FAIL---" in mysql_output:
output += "\n---MYSQL-FAIL---\n"
else:
output += "\n---MYSQL---\n" + mysql_output
except Exception as e:
logger.debug(f"MySQL 采集失败(不影响系统指标): {e}")
return output
except Exception as e:
logger.debug(f"SSH 命令执行失败: {e}")
......@@ -464,6 +535,115 @@ class TargetResourceMonitor:
return processes
def _parse_mysql(self, output: str) -> dict:
"""
从 SSH 输出中解析 MySQL 容器指标
输出格式(docker exec mysql ... -N --batch,制表符分隔):
Variable_name<TAB>Value
...
max_connections<TAB>151
Args:
output: SSH 命令完整输出
Returns:
dict: MySQL 指标字典(见 sample() 返回值定义),
MySQL 采集失败时 available=False、各值为默认值。
"""
# 默认值字典
defaults = {
"available": False,
"container": "umysql",
"version": None,
"uptime": None,
"threadsConnected": None,
"threadsRunning": None,
"threadsCreated": None,
"maxUsedConnections": None,
"maxConnections": None,
"connections": None,
"abortedConnects": None,
"questions": None,
"queries": None,
"slowQueries": None,
"bytesReceivedMb": None,
"bytesSentMb": None,
"bufferPoolReadRequests": None,
"bufferPoolReads": None,
"bufferPoolHitRate": None,
"rowsRead": None,
"comSelect": None,
"comInsert": None,
"comUpdate": None,
"comDelete": None,
}
in_mysql = False
kv = {}
for line in output.splitlines():
if line.strip() == "---MYSQL---":
in_mysql = True
continue
if not in_mysql or not line.strip():
continue
parts = line.split("\t", 1)
if len(parts) != 2:
continue
key = parts[0].strip()
val = parts[1].strip()
kv[key] = val
if not kv or "Uptime" not in kv:
# MySQL 采集失败(Docker 未运行 / 凭据变更 / 容器缺失)
logger.debug("MySQL 指标采集失败(输出中无 Uptime/数据)")
return defaults
def _f(key: str) -> Optional[float]:
raw = kv.get(key)
if raw is None:
return None
try:
return float(raw)
except (TypeError, ValueError):
return None
bytes_recv = _f("Bytes_received") or 0.0
bytes_sent = _f("Bytes_sent") or 0.0
read_req = _f("Innodb_buffer_pool_read_requests") or 0.0
disk_reads = _f("Innodb_buffer_pool_reads") or 0.0
hit_rate = None
if (read_req + disk_reads) > 0:
hit_rate = round(read_req / (read_req + disk_reads) * 100.0, 2)
return {
"available": True,
"container": "umysql",
"version": kv.get("version"),
"uptime": _f("Uptime"),
"threadsConnected": _f("Threads_connected"),
"threadsRunning": _f("Threads_running"),
"threadsCreated": _f("Threads_created"),
"maxUsedConnections": _f("Max_used_connections"),
"maxConnections": _f("max_connections"),
"connections": _f("Connections"),
"abortedConnects": _f("Aborted_connects"),
"questions": _f("Questions"),
"queries": _f("Queries"),
"slowQueries": _f("Slow_queries"),
"bytesReceivedMb": round(bytes_recv / 1024.0 / 1024.0, 1),
"bytesSentMb": round(bytes_sent / 1024.0 / 1024.0, 1),
"bufferPoolReadRequests": read_req,
"bufferPoolReads": disk_reads,
"bufferPoolHitRate": hit_rate,
"rowsRead": _f("Innodb_rows_read"),
"comSelect": _f("Com_select"),
"comInsert": _f("Com_insert"),
"comUpdate": _f("Com_update"),
"comDelete": _f("Com_delete"),
}
# ==================== 模块自测 ====================
......@@ -481,6 +661,7 @@ if __name__ == "__main__":
_time.sleep(1.0)
data = monitor.sample()
procs = data.get("javaProcesses", [])
mysql = data.get("mysql", {})
print(
f"[{i + 1}] cpu={data['cpuPercent']}% mem={data['memPercent']}% "
f"({data['memUsedMb']}/{data['memTotalMb']}MB) "
......@@ -493,4 +674,12 @@ if __name__ == "__main__":
)
if not procs:
print(" (未匹配到 Java 进程)")
if mysql.get("available"):
print(
f" mysql: conn={mysql['threadsConnected']} run={mysql['threadsRunning']} "
f"max_conn={mysql['maxConnections']} slow={mysql['slowQueries']} "
f"buf_hit={mysql['bufferPoolHitRate']}% recv={mysql['bytesReceivedMb']}MB"
)
else:
print(" (MySQL 不可用)")
monitor.dispose()
\ No newline at end of file
......@@ -696,6 +696,11 @@ class PerformanceService:
if not execution:
return None
# 获取关联任务,填充报告摘要字段(target_url/method/mode/scenario_type 等)
task = None
if execution.task_id:
task = await self.db.get(PerformanceTask, execution.task_id)
# 获取该执行记录关联的快照
result = await self.db.execute(
select(PerformanceSnapshot)
......@@ -714,16 +719,16 @@ class PerformanceService:
"summary": {
"task_id": execution.task_id,
"task_name": execution.task_name,
"target_url": "",
"method": "",
"mode": "",
"scenario_type": None,
"concurrency": 0,
"duration_setting": 0,
"target_url": task.target_url if task else "",
"method": task.method if task else "",
"mode": task.mode if task else "",
"scenario_type": task.scenario_type if task else None,
"concurrency": task.concurrency if task else 0,
"duration_setting": task.duration if task else 0,
"duration_actual": execution.duration_actual,
"start_time": execution.start_time.isoformat() if execution.start_time else None,
"end_time": execution.end_time.isoformat() if execution.end_time else None,
"account_key": "",
"account_key": task.account_key if task else "",
},
"metrics": {
"total_requests": execution.total_requests,
......
......@@ -191,6 +191,24 @@ export interface TargetResourceData {
host?: string
/** Java 进程 CPU/内存占用明细 */
javaProcesses?: JavaProcessSample[] | null
/** MySQL 服务指标 */
mysql?: {
available: boolean
container?: string
version?: string
threadsConnected?: number
threadsRunning?: number
slowQueries?: number
bufferPoolHitRate?: number
maxUsedConnections?: number
maxConnections?: number
uptime?: number
connections?: number
questions?: number
queries?: number
bytesReceivedMb?: number
bytesSentMb?: number
} | null
timestamp: number
available: boolean
}
......
......@@ -342,6 +342,75 @@
</el-table>
</el-card>
</template>
<!-- 目标机 MySQL 指标(仅采集成功时展示) -->
<template v-if="mysqlAvailable">
<el-card shadow="never" class="chart-card" style="margin-top: 12px;">
<template #header>
<span>MySQL 服务指标 MySQL Metrics{{ mysqlContainer }} v{{ mysqlVersion }}</span>
</template>
<el-row :gutter="12" class="metrics-row">
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value" :style="{ color: mysqlThreadsRunning > 10 ? '#f56c6c' : '#67c23a' }">
{{ mysqlThreadsConnected }}
</div>
<div class="metric-label">Threads Connected(当前连接数)</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value" :style="{ color: mysqlThreadsRunning > 5 ? '#f56c6c' : '#67c23a' }">
{{ mysqlThreadsRunning }}
</div>
<div class="metric-label">Threads Running(活跃线程)</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value" :style="{ color: mysqlSlowQueries > 10 ? '#f56c6c' : '#67c23a' }">
{{ mysqlSlowQueries }}
</div>
<div class="metric-label">Slow Queries(慢查询累计)</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value" :style="{ color: mysqlBufferPoolHitRate < 99 ? '#f56c6c' : '#67c23a' }">
{{ mysqlBufferPoolHitRate.toFixed(2) }}%
</div>
<div class="metric-label">Buffer Pool Hit Rate(缓存命中率)</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="12" class="metrics-row" style="margin-top: 8px;">
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value">{{ mysqlMaxUsedConnections }} / {{ mysqlMaxConnections }}</div>
<div class="metric-label">Max Used / Max Connections(最大已用连接 / 最大连接数)</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value">{{ mysqlQuestions.toLocaleString() }}</div>
<div class="metric-label">Questions(查询总数)</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value">{{ mysqlBytesReceivedMb.toFixed(1) }} MB</div>
<div class="metric-label">Bytes Received(接收数据量)</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="metric-card">
<div class="metric-value">{{ mysqlBytesSentMb.toFixed(1) }} MB</div>
<div class="metric-label">Bytes Sent(发送数据量)</div>
</el-card>
</el-col>
</el-row>
</el-card>
</template>
</div>
<el-empty
v-show="!targetAvailable"
......@@ -536,6 +605,29 @@ async function loadReplayData() {
rssMb: p.rssMb != null ? p.rssMb : 0,
}))
javaProcessAvailable.value = targetJavaProcesses.value.length > 0
// 回放模式:MySQL 指标
const m = tr.mysql
if (m && m.available) {
mysqlAvailable.value = true
mysqlContainer.value = m.container || 'umysql'
mysqlVersion.value = m.version || ''
mysqlThreadsConnected.value = m.threadsConnected ?? 0
mysqlThreadsRunning.value = m.threadsRunning ?? 0
mysqlSlowQueries.value = m.slowQueries ?? 0
mysqlBufferPoolHitRate.value = m.bufferPoolHitRate ?? 100
mysqlMaxUsedConnections.value = m.maxUsedConnections ?? 0
mysqlMaxConnections.value = m.maxConnections ?? 0
mysqlUptime.value = m.uptime ?? 0
mysqlConnections.value = m.connections ?? 0
mysqlQuestions.value = m.questions ?? 0
mysqlQueries.value = m.queries ?? 0
mysqlBytesReceivedMb.value = m.bytesReceivedMb ?? 0
mysqlBytesSentMb.value = m.bytesSentMb ?? 0
mysqlThreadsConnectedData.value.push(mysqlThreadsConnected.value)
mysqlThreadsRunningData.value.push(mysqlThreadsRunning.value)
}
}
}
......@@ -658,6 +750,23 @@ const targetJavaProcesses = ref<Array<{
}>>([])
const javaProcessAvailable = ref(false)
// 目标机 MySQL 指标
const mysqlAvailable = ref(false)
const mysqlContainer = ref('')
const mysqlVersion = ref('')
const mysqlThreadsConnected = ref(0)
const mysqlThreadsRunning = ref(0)
const mysqlSlowQueries = ref(0)
const mysqlBufferPoolHitRate = ref(100)
const mysqlMaxUsedConnections = ref(0)
const mysqlMaxConnections = ref(0)
const mysqlUptime = ref(0)
const mysqlConnections = ref(0)
const mysqlQuestions = ref(0)
const mysqlQueries = ref(0)
const mysqlBytesReceivedMb = ref(0)
const mysqlBytesSentMb = ref(0)
// 图表数据缓冲区
const timeLabels = ref<string[]>([])
const tpsData = ref<number[]>([])
......@@ -686,6 +795,10 @@ const targetCpuData = ref<number[]>([])
const targetMemData = ref<number[]>([])
const targetLoadData = ref<number[]>([])
// 目标机 MySQL 趋势数据
const mysqlThreadsConnectedData = ref<number[]>([])
const mysqlThreadsRunningData = ref<number[]>([])
// 图表引用
const tpsChartRef = ref<HTMLElement>()
const rtChartRef = ref<HTMLElement>()
......@@ -912,17 +1025,23 @@ function updateNetworkChart() {
}
const option: EChartsOption = {
tooltip: { trigger: 'axis' },
legend: { data: ['CPU %', 'Memory %', 'Load 1m'], top: 0, itemWidth: 12, itemHeight: 8 },
legend: {
data: ['CPU %', 'Memory %', 'Load 1m', 'MySQL 连接数', 'MySQL 活跃线程'],
top: 0, itemWidth: 12, itemHeight: 8,
type: 'scroll',
},
grid: { left: 50, right: 20, bottom: 30, top: 30 },
xAxis: { type: 'category', data: targetResourceTimeLabels.value, axisLabel: { fontSize: 11 } },
yAxis: [
{ type: 'value', name: '%', max: 100 },
{ type: 'value', name: 'Load', splitLine: { show: false } },
{ type: 'value', name: 'Load / 连接数', splitLine: { show: false } },
],
series: [
{ name: 'CPU %', type: 'line', data: targetCpuData.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, color: '#f56c6c' }, areaStyle: { opacity: 0.08 } },
{ name: 'Memory %', type: 'line', data: targetMemData.value, smooth: true, showSymbol: false, lineStyle: { width: 1.5, color: '#409eff' }, areaStyle: { opacity: 0.08 } },
{ name: 'Load 1m', type: 'line', data: targetLoadData.value, smooth: true, showSymbol: false, yAxisIndex: 1, lineStyle: { width: 1.5, type: 'dashed', color: '#e6a23c' } },
{ name: 'MySQL 连接数', type: 'line', data: mysqlThreadsConnectedData.value, smooth: true, showSymbol: false, yAxisIndex: 1, lineStyle: { width: 1.5, color: '#67c23a' } },
{ name: 'MySQL 活跃线程', type: 'line', data: mysqlThreadsRunningData.value, smooth: true, showSymbol: false, yAxisIndex: 1, lineStyle: { width: 1.5, type: 'dotted', color: '#909399' } },
],
}
targetResourceChart.setOption(option, true)
......@@ -1017,6 +1136,38 @@ function handleSnapshot(data: any) {
// 仅限 Java 进程存在时展示(避免无进程时空表头)
javaProcessAvailable.value = targetJavaProcesses.value.length > 0
// 目标机 MySQL 指标
const m = tr.mysql
if (m && m.available) {
mysqlAvailable.value = true
mysqlContainer.value = m.container || 'umysql'
mysqlVersion.value = m.version || ''
mysqlThreadsConnected.value = m.threadsConnected ?? 0
mysqlThreadsRunning.value = m.threadsRunning ?? 0
mysqlSlowQueries.value = m.slowQueries ?? 0
mysqlBufferPoolHitRate.value = m.bufferPoolHitRate ?? 100
mysqlMaxUsedConnections.value = m.maxUsedConnections ?? 0
mysqlMaxConnections.value = m.maxConnections ?? 0
mysqlUptime.value = m.uptime ?? 0
mysqlConnections.value = m.connections ?? 0
mysqlQuestions.value = m.questions ?? 0
mysqlQueries.value = m.queries ?? 0
mysqlBytesReceivedMb.value = m.bytesReceivedMb ?? 0
mysqlBytesSentMb.value = m.bytesSentMb ?? 0
mysqlThreadsConnectedData.value.push(mysqlThreadsConnected.value)
mysqlThreadsRunningData.value.push(mysqlThreadsRunning.value)
if (mysqlThreadsConnectedData.value.length > 300) {
mysqlThreadsConnectedData.value.shift()
mysqlThreadsRunningData.value.shift()
}
} else {
mysqlAvailable.value = false
// 无 MySQL 数据时对齐数组长度(避免图表联动错位)
mysqlThreadsConnectedData.value.push(null as any)
mysqlThreadsRunningData.value.push(null as any)
}
const relapsed = d.elapsed != null ? `${d.elapsed.toFixed(0)}s` : `${targetResourceTimeLabels.value.length + 1}s`
targetResourceTimeLabels.value.push(relapsed)
targetCpuData.value.push(targetCpu.value)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论