提交 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
......@@ -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
......
......@@ -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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论