From da35150a8db6a647216863ff1ca78d7a8ceed35c Mon Sep 17 00:00:00 2001 From: hi2hi Date: Tue, 11 Nov 2025 11:38:04 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=84=20=E4=BC=98=E5=8C=96=E5=89=8A?= =?UTF-8?q?=E5=B3=B0=E7=9A=84=E8=AE=A1=E7=AE=97=EF=BC=8C=E5=B0=BD=E9=87=8F?= =?UTF-8?q?=E5=88=87=E6=8E=89=E4=B8=8E=E5=B9=B3=E5=9D=87=E5=80=BC=E4=B8=8D?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E7=9A=84=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server-detail/server-monitor.vue | 46 +++++----- src/views/composable/server-monitor.js | 86 ++++++++++++++----- 2 files changed, 89 insertions(+), 43 deletions(-) diff --git a/src/views/components/server-detail/server-monitor.vue b/src/views/components/server-detail/server-monitor.vue index 99c46b8..7159b3f 100644 --- a/src/views/components/server-detail/server-monitor.vue +++ b/src/views/components/server-detail/server-monitor.vue @@ -280,13 +280,6 @@ const monitorChartData = computed(() => { * - valueList {Array}: 包含以下内容的对象列表: * - name {String}: 监控名称。 * - data {Array}: [时间戳, 平均延迟] 对的数组。 - * - * 该函数执行以下步骤: - * 1. 遍历监控数据以分类和过滤平均延迟。 - * 2. 如果启用了削峰,则应用削峰以过滤异常值。 - * 3. 构建监控名称到其各自时间戳和平均延迟的映射。 - * 4. 将映射转换为监控名称、时间戳和平均延迟数据的列表。 - * 5. 删除重复的时间戳并对其进行排序。 */ const cateMap = {}; monitorData.value.forEach((i) => { @@ -319,27 +312,30 @@ const monitorChartData = computed(() => { } } const { - threshold, - mean, - max, - min, - } = peakShaving.value ? getThreshold(showAvgDelay, 2) : {}; + median, + tolerancePercent, + } = peakShaving.value ? getThreshold(showAvgDelay) : {}; showCreateTime.forEach((o, index) => { if (Object.prototype.hasOwnProperty.call(dateMap, o)) { return; } const avgDelay = showAvgDelay[index]; + // 没有数据或延迟为0,算作监控失败,计入成功率 + if (avgDelay === null || avgDelay === 0) { + dateMap[o] = undefined; + return; + } + // 只对有效的延迟值进行削峰判断 if (peakShaving.value) { - if (avgDelay === 0) { - dateMap[o] = null; - return; - } - // 削峰过滤:检测到异常值时直接跳过,不加入dateMap,避免影响成功率计算 - if (Math.abs(avgDelay - mean) > threshold && max / min > 2) { + // 削峰过滤:根据中位数和动态容差百分比判断异常值 + const threshold = median * tolerancePercent; + // 当偏离中位数超过阈值时,视为异常值 + if (Math.abs(avgDelay - median) > threshold) { + dateMap[o] = undefined; return; } } - dateMap[o] = avgDelay ? (avgDelay).toFixed(2) * 1 : null; + dateMap[o] = (avgDelay).toFixed(2) * 1; }); }); let dateList = []; @@ -362,9 +358,11 @@ const monitorChartData = computed(() => { showCates.value[id] = true; } // 计算平均延迟和成功率 - const validAvgs = avgs.filter((a) => a[1] !== 0 && a[1] !== null); + // 排除被削峰过滤的点(undefined),只统计真实的监控数据 + const realAvgs = avgs.filter((a) => a[1] !== undefined); + const validAvgs = realAvgs.filter((a) => a[1] !== 0 && a[1] !== null); const avg = validAvgs.reduce((a, b) => a + b[1], 0) / validAvgs.length; - const over = avgs.filter((a) => a[1] !== 0 && a[1] !== null).length / avgs.length; + const over = validAvgs.length / realAvgs.length; const cateItem = { id, name: i, @@ -568,6 +566,12 @@ onUnmounted(() => { justify-content: space-between; gap: 10px; + @media screen and (min-width: 768px) { + position: sticky; + top: var(--layout-header-height); + z-index: 1000; + } + .module-title { width: max-content; height: 30px; diff --git a/src/views/composable/server-monitor.js b/src/views/composable/server-monitor.js index 71a2ba1..e757e6c 100644 --- a/src/views/composable/server-monitor.js +++ b/src/views/composable/server-monitor.js @@ -1,33 +1,75 @@ import uniqolor from 'uniqolor'; /** - * 计算数据的阈值和平均值 + * 计算数据的统计信息,使用截尾中位数作为基准值 + * 根据平均延迟的不同范围,使用不同的容差百分比进行削峰 * * @param {number[]} data - 要计算的数据数组 - * @param {number} [tolerance=2] - 容差倍数,默认值为2 - * @returns {{threshold: number, mean: number}} 返回包含阈值和平均值的对象 - * @property {number} threshold - 计算得到的阈值 - * @property {number} mean - 数据的平均值 + * @returns {{median: number, tolerancePercent: number, min: number, max: number}} + * 返回包含统计信息的对象 + * @property {number} median - 截尾中位数(去掉极端值后的中位数) + * @property {number} tolerancePercent - 根据中位数计算的容差百分比 + * @property {number} min - 最小值 + * @property {number} max - 最大值 */ -export function getThreshold(data, tolerance = 2) { - // 计算数据的平均值 - const mean = data.reduce((sum, value) => sum + (value || 0), 0) / data.length; - // 计算数据的方差 - const variance = data.reduce((sum, value) => sum + ((value || 0) - mean) ** 2, 0) / data.length; - // 计算标准差 - const stdDev = Math.sqrt(variance); - // 计算阈值 - const threshold = tolerance * stdDev; - // 过滤掉值为0的数据 +export function getThreshold(data) { + // 过滤掉null和0的数据,只对有效延迟值计算统计量 const filteredData = data.filter((value) => value !== 0 && value !== null); - // 计算过滤后数据的最小值 - const min = Math.min(...filteredData); - // 计算过滤后数据的最大值 - const max = Math.max(...filteredData); - // 返回包含阈值、平均值、最小值和最大值的对象 + + if (filteredData.length === 0) { + return { + median: 0, + tolerancePercent: 0.2, + min: 0, + max: 0, + }; + } + + // 排序数据 + const sortedData = [...filteredData].sort((a, b) => Math.ceil(a) - Math.ceil(b)); + const len = sortedData.length; + + // 计算需要裁剪的数量(10%) + const trimCount = Math.floor(len * 0.1); + + // 用于计算中位数的数据:如果10%的数量>=1,则去掉最大和最小的10% + let dataForMedian; + if (trimCount >= 1) { + // 截尾:去掉最小的10%和最大的10% + dataForMedian = sortedData.slice(trimCount, len - trimCount); + } else { + // 数据量太少,不裁剪 + dataForMedian = sortedData; + } + + // 计算截尾中位数 + const medianLen = dataForMedian.length; + const median = medianLen % 2 === 0 + ? (dataForMedian[medianLen / 2 - 1] + dataForMedian[medianLen / 2]) / 2 + : dataForMedian[Math.floor(medianLen / 2)]; + + // 根据中位数确定容差百分比,延迟越小容差越大 + let tolerancePercent; + if (median <= 10) { + tolerancePercent = 0.50; // 50% + } else if (median <= 30) { + tolerancePercent = 0.35; // 35% + } else if (median <= 50) { + tolerancePercent = 0.25; // 25% + } else if (median <= 100) { + tolerancePercent = 0.20; // 20% + } else { + tolerancePercent = 0.15; // 15% + } + + const min = sortedData[0]; + const max = sortedData[len - 1]; + + // console.log(min, max, median, sortedData); + return { - threshold, - mean, + median, + tolerancePercent, min, max, };