#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 通用集群指标对比分析程序 支持saas05/gosaas和gogame/poc两种集群对比 """ import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib.dates as mdates import os from datetime import datetime, timedelta import warnings warnings.filterwarnings('ignore') # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False class ClusterMetricsComparator: def __init__(self, data_dir, cluster1_name, cluster2_name, time_offset=None, output_dir=None): """ 初始化集群指标对比器 Args: data_dir: 数据目录路径 cluster1_name: 第一个集群名称(如saas05, gogame) cluster2_name: 第二个集群名称(如gosaas, poc) time_offset: 时间偏移量,用于对齐时间轴 """ self.data_dir = data_dir self.cluster1_name = cluster1_name self.cluster2_name = cluster2_name self.time_offset = time_offset self.results = {} self.output_dir = output_dir print(f"开始{cluster1_name} vs {cluster2_name}数据对比分析...") print(f"数据目录: {data_dir}") if self.time_offset: print(f"时间偏移: {self.time_offset}") print("-" * 50) def load_csv_data(self, filename): """加载CSV数据""" filepath = os.path.join(self.data_dir, filename) if not os.path.exists(filepath): print(f"警告: 文件 {filepath} 不存在") return None try: df = pd.read_csv(filepath) # 检查是否有Time列 if 'Time' not in df.columns: print(f"警告: 文件 {filename} 没有Time列") return None # 转换时间格式 df['Time'] = pd.to_datetime(df['Time']) # 如果是第二个集群的数据,应用时间偏移 if filename.startswith(f'{self.cluster2_name}_') and self.time_offset: df['Time'] = df['Time'] - self.time_offset print(f"调试: {filename} 时间已前移 {self.time_offset}") return df except Exception as e: print(f"错误: 加载文件 {filename} 失败: {e}") return None def load_all_data(self): """加载所有指标数据""" print("开始处理指标数据...") # 定义指标文件映射 metric_files = { # 'P999': [f'{self.cluster1_name}_P999.csv', f'{self.cluster2_name}_P999.csv'], # 'P99': [f'{self.cluster1_name}_P99.csv', f'{self.cluster2_name}_P99.csv'], # 'P95': [f'{self.cluster1_name}_P95.csv', f'{self.cluster2_name}_P95.csv'], # 'CPS': [f'{self.cluster1_name}_cps.csv', f'{self.cluster2_name}_cps.csv'], # 'CPU_TiDB': [f'{self.cluster1_name}_cpu_tidb.csv', f'{self.cluster2_name}_cpu_tidb.csv'], # 'CPU_TiKV': [f'{self.cluster1_name}_cpu_tikv.csv', f'{self.cluster2_name}_cpu_tikv.csv'], # 'CPU_TiFlash': [f'{self.cluster1_name}_cpu_tiflash.csv', f'{self.cluster2_name}_cpu_tiflash.csv'], # 'Memory_TiDB': [f'{self.cluster1_name}_memory_tidb.csv', f'{self.cluster2_name}_memory_tidb.csv'], # 'Memory_TiKV': [f'{self.cluster1_name}_memory_tikv.csv', f'{self.cluster2_name}_memory_tikv.csv'], # 'Memory_TiFlash': [f'{self.cluster1_name}_memory_tiflash.csv', f'{self.cluster2_name}_memory_tiflash.csv'], # 'Storage_TiKV': [f'{self.cluster1_name}_storage_tikv.csv', f'{self.cluster2_name}_storage_tikv.csv'], # 'Storage_TiFlash': [f'{self.cluster1_name}_storage_tiflash.csv', f'{self.cluster2_name}_storage_tiflash.csv'], # 'IOPS': [f'{self.cluster1_name}_IOPS.csv', f'{self.cluster2_name}_IOPS.csv'], # 'Bandwidth': [f'{self.cluster1_name}_bandwidth.csv', f'{self.cluster2_name}_bandwidth.csv'], # 'FailCount': [f'{self.cluster1_name}_failcount.csv', f'{self.cluster2_name}_failcount.csv'] # wlt test # 'P999': [f'{self.cluster1_name}_p999.csv', f'{self.cluster2_name}_p999.csv'], # 'P99': [f'{self.cluster1_name}_p99.csv', f'{self.cluster2_name}_p99.csv'], # 'P95': [f'{self.cluster1_name}_p95.csv', f'{self.cluster2_name}_p95.csv'], # 'CPS': [f'{self.cluster1_name}_cps.csv', f'{self.cluster2_name}_cps.csv'], # 'Duration_AVG': [f'{self.cluster1_name}_duration_avg.csv', f'{self.cluster2_name}_duration_avg.csv'], # 'CPU_TiDB': [f'{self.cluster1_name}_cpu_tidb.csv', f'{self.cluster2_name}_cpu_tidb.csv'], # 'CPU_TiKV': [f'{self.cluster1_name}_cpu_tikv.csv', f'{self.cluster2_name}_cpu_tikv.csv'], # 'CPU_TiFlash': [f'{self.cluster1_name}_cpu_tiflash.csv', f'{self.cluster2_name}_cpu_tiflash.csv'], 'P999': [f'{self.cluster1_name}_latency_p999_all.csv', f'{self.cluster2_name}_latency_p999_all.csv'], 'P999_update': [f'{self.cluster1_name}_latency_p999_update.csv', f'{self.cluster2_name}_latency_p999_update.csv'], 'P999_delete': [f'{self.cluster1_name}_latency_p999_delete.csv', f'{self.cluster2_name}_latency_p999_delete.csv'], 'P999_insert': [f'{self.cluster1_name}_latency_p999_insert.csv', f'{self.cluster2_name}_latency_p999_insert.csv'], 'P999_select': [f'{self.cluster1_name}_latency_p999_select.csv', f'{self.cluster2_name}_latency_p999_select.csv'], 'P99': [f'{self.cluster1_name}_latency_p99_all.csv', f'{self.cluster2_name}_latency_p99_all.csv'], 'P99_update': [f'{self.cluster1_name}_latency_p99_update.csv', f'{self.cluster2_name}_latency_p99_update.csv'], 'P99_delete': [f'{self.cluster1_name}_latency_p99_delete.csv', f'{self.cluster2_name}_latency_p99_delete.csv'], 'P99_insert': [f'{self.cluster1_name}_latency_p99_insert.csv', f'{self.cluster2_name}_latency_p99_insert.csv'], 'P99_select': [f'{self.cluster1_name}_latency_p99_select.csv', f'{self.cluster2_name}_latency_p99_select.csv'], 'P95': [f'{self.cluster1_name}_latency_p95_all.csv', f'{self.cluster2_name}_latency_p95_all.csv'], 'P95_update': [f'{self.cluster1_name}_latency_p95_update.csv', f'{self.cluster2_name}_latency_p95_update.csv'], 'P95_delete': [f'{self.cluster1_name}_latency_p95_delete.csv', f'{self.cluster2_name}_latency_p95_delete.csv'], 'P95_insert': [f'{self.cluster1_name}_latency_p95_insert.csv', f'{self.cluster2_name}_latency_p95_insert.csv'], 'P95_select': [f'{self.cluster1_name}_latency_p95_select.csv', f'{self.cluster2_name}_latency_p95_select.csv'], 'CPS': [f'{self.cluster1_name}_cps_ok.csv', f'{self.cluster2_name}_cps_ok.csv'], 'QPS': [f'{self.cluster1_name}_qps_all.csv', f'{self.cluster2_name}_qps_all.csv'], 'IOPS': [f'{self.cluster1_name}_iops.csv', f'{self.cluster2_name}_iops.csv'], 'Duration_AVG': [f'{self.cluster1_name}_latency_avg_all.csv', f'{self.cluster2_name}_latency_avg_all.csv'], 'Duration_AVG_update': [f'{self.cluster1_name}_latency_avg_update.csv', f'{self.cluster2_name}_latency_avg_update.csv'], 'Duration_AVG_delete': [f'{self.cluster1_name}_latency_avg_delete.csv', f'{self.cluster2_name}_latency_avg_delete.csv'], 'Duration_AVG_insert': [f'{self.cluster1_name}_latency_avg_insert.csv', f'{self.cluster2_name}_latency_avg_insert.csv'], 'Duration_AVG_select': [f'{self.cluster1_name}_latency_avg_select.csv', f'{self.cluster2_name}_latency_avg_select.csv'], 'CPU_TiDB': [f'{self.cluster1_name}_cpu_tidb.csv', f'{self.cluster2_name}_cpu_tidb.csv'], 'CPU_TiKV': [f'{self.cluster1_name}_cpu_tikv.csv', f'{self.cluster2_name}_cpu_tikv.csv'], 'CPU_TiFlash': [f'{self.cluster1_name}_cpu_tiflash.csv', f'{self.cluster2_name}_cpu_tiflash.csv'], 'Memory_TiDB': [f'{self.cluster1_name}_memory_tidb.csv', f'{self.cluster2_name}_memory_tidb.csv'], 'Memory_TiKV': [f'{self.cluster1_name}_memory_tikv.csv', f'{self.cluster2_name}_memory_tikv.csv'], # 'Memory_TiFlash': [f'{self.cluster1_name}_memory_tiflash.csv', f'{self.cluster2_name}_memory_tiflash.csv'], # 'Storage_TiKV': [f'{self.cluster1_name}_storage_tikv.csv', f'{self.cluster2_name}_storage_tikv.csv'], # 'Storage_TiFlash': [f'{self.cluster1_name}_storage_tiflash.csv', f'{self.cluster2_name}_storage_tiflash.csv'], # 'IOPS': [f'{self.cluster1_name}_IOPS.csv', f'{self.cluster2_name}_IOPS.csv'], # 'Bandwidth': [f'{self.cluster1_name}_bandwidth.csv', f'{self.cluster2_name}_bandwidth.csv'], # 'FailCount': [f'{self.cluster1_name}_failcount.csv', f'{self.cluster2_name}_failcount.csv'] } for metric_name, files in metric_files.items(): print(f"处理 {metric_name} 指标...") cluster1_file, cluster2_file = files cluster1_data = self.load_csv_data(cluster1_file) cluster2_data = self.load_csv_data(cluster2_file) if cluster1_data is not None and cluster2_data is not None: self.results[metric_name] = { 'cluster1': cluster1_data, 'cluster2': cluster2_data } print("指标数据处理完成!") def parse_numeric_value(self, value_str): """解析包含单位的数值字符串""" if pd.isna(value_str): return None value_str = str(value_str).strip() # 处理TB单位 if 'TB' in value_str: num_str = value_str.replace('TB', '').strip() try: return float(num_str) except: return None # 处理GB单位 - 转换为TB elif 'GB' in value_str: num_str = value_str.replace('GB', '').strip() try: return float(num_str) / 1024 # GB to TB except: return None # 处理GB/s单位 - 转换为MB/s elif 'GB/s' in value_str: num_str = value_str.replace('GB/s', '').strip() try: return float(num_str) * 1024 # GB/s to MB/s except: return None # 处理MB/s单位 elif 'MB/s' in value_str: num_str = value_str.replace('MB/s', '').strip() try: return float(num_str) except: return None # 处理%单位 elif '%' in value_str: num_str = value_str.replace('%', '').strip() try: return float(num_str) except: return None # 处理K单位 - 转换为数值 elif 'K' in value_str: num_str = value_str.replace('K', '').strip() try: return float(num_str) * 1000 # K to 数值 except: return None # 处理IOPS单位 elif 'IOPS' in value_str: num_str = value_str.replace('IOPS', '').strip() try: return float(num_str) except: return None # 处理纯数字 else: try: return float(value_str) except: return None def calculate_statistics(self, data, metric_name): """计算统计指标""" if data is None or len(data) == 0: return {} # 获取数值列(排除Time列) value_columns = [col for col in data.columns if col != 'Time'] if not value_columns: return {} # 计算所有数值列的统计指标 stats = {} for col in value_columns: # 使用新的解析函数处理包含单位的数据 values = [] for val in data[col]: parsed_val = self.parse_numeric_value(val) if parsed_val is not None: values.append(parsed_val) if len(values) > 0: values = pd.Series(values) stats[col] = { 'max': values.max(), 'avg': values.mean(), 'p99': values.quantile(0.99), 'p95': values.quantile(0.95) } # 对于延迟类指标,转换为毫秒 if metric_name.startswith('P999') or metric_name.startswith('P99') or metric_name.startswith('P95') or metric_name.startswith('Duration_AVG'): for col in stats: for stat in ['max', 'avg', 'p99', 'p95']: if stat in stats[col]: stats[col][stat] = stats[col][stat] * 1000 return stats def calculate_error_rate(self, failcount_data, cps_data): """计算错误率 - sum(failcount)/sum(cps)""" if failcount_data is None or cps_data is None: return 0.0 # 获取数值列 failcount_cols = [col for col in failcount_data.columns if col != 'Time'] cps_cols = [col for col in cps_data.columns if col != 'Time'] if not failcount_cols or not cps_cols: return 0.0 # 计算总失败数和总请求数 total_failures = 0.0 total_requests = 0.0 # 遍历所有数据点 for i in range(len(failcount_data)): if i < len(cps_data): # 解析失败数 fail_val = self.parse_numeric_value(failcount_data.iloc[i][failcount_cols[0]]) # 解析请求数 cps_val = self.parse_numeric_value(cps_data.iloc[i][cps_cols[0]]) if fail_val is not None and cps_val is not None: total_failures += fail_val total_requests += cps_val # 计算错误率 if total_requests > 0: return (total_failures / total_requests) * 100 else: return 0.0 def format_value(self, value, metric_name): """格式化数值显示""" if pd.isna(value): return "N/A" if metric_name == 'IOPS': # IOPS保留到万分之一 return f"{value:.4f}" elif metric_name in ['P999', 'P99', 'P95']: # 延迟指标保留1位小数 return f"{value:.1f}" elif metric_name in ['CPS', 'QPS']: # CPS使用千分位分隔符 return f"{int(value):,}" elif metric_name in ['Storage_TiKV', 'Storage_TiFlash']: # Storage指标保留2位小数 return f"{value:.2f}" elif metric_name in ['ErrorRate', 'Day_ErrorRate']: # 错误率指标保留10位小数 return f"{value:.10f}" else: # 其他指标保留2位小数 return f"{value:.2f}" def generate_comparison_table(self): """生成对比表格""" print("生成对比表格...") table_data = [] # 按照用户提供的顺序定义表格行,并添加单位 metrics_to_show = [ ('P999_Max (ms)', 'P999', 'max'), ('P999_AVG (ms)', 'P999', 'avg'), ('P999_P99 (ms)', 'P999', 'p99'), ('P999_P95 (ms)', 'P999', 'p95'), ('P99_Max (ms)', 'P99', 'max'), ('P99_AVG (ms)', 'P99', 'avg'), ('P99_P99 (ms)', 'P99', 'p99'), ('P99_P95 (ms)', 'P99', 'p95'), ('P95_Max (ms)', 'P95', 'max'), ('P95_AVG (ms)', 'P95', 'avg'), ('P95_P99 (ms)', 'P95', 'p99'), ('P95_P95 (ms)', 'P95', 'p95'), ('CPS_Max (QPS)', 'CPS', 'max'), ('CPS_Max_Time', 'CPS', 'max_time'), ('CPS_AVG (QPS)', 'CPS', 'avg'), ('QPS_Max (QPS)', 'QPS', 'max'), ('QPS_Max_Time', 'QPS', 'max_time'), ('QPS_AVG (QPS)', 'QPS', 'avg'), ('duration_avg_Max (ms)', 'Duration_AVG', 'max'), ('duration_avg_AVG (ms)', 'Duration_AVG', 'avg'), ('Day_ErrorRate (%)', 'FailCount', 'error_rate'), ('CPU_MAX_TiDB (%)', 'CPU_TiDB', 'max'), ('CPU_AVG_TiDB (%)', 'CPU_TiDB', 'avg'), ('CPU_P99_TiDB (%)', 'CPU_TiDB', 'p99'), ('CPU_P95_TiDB (%)', 'CPU_TiDB', 'p95'), ('CPU_MAX_TiKV (%)', 'CPU_TiKV', 'max'), ('CPU_AVG_TiKV (%)', 'CPU_TiKV', 'avg'), ('CPU_P99_TiKV (%)', 'CPU_TiKV', 'p99'), ('CPU_P95_TiKV (%)', 'CPU_TiKV', 'p95'), ('CPU_MAX_TiFlash (%)', 'CPU_TiFlash', 'max'), ('CPU_AVG_TiFlash (%)', 'CPU_TiFlash', 'avg'), ('CPU_P99_TiFlash (%)', 'CPU_TiFlash', 'p99'), ('CPU_P95_TiFlash (%)', 'CPU_TiFlash', 'p95'), ('Memory_Max_TiDB (%)', 'Memory_TiDB', 'max'), ('Memory_AVG_TiDB (%)', 'Memory_TiDB', 'avg'), ('Memory_P99_TiDB (%)', 'Memory_TiDB', 'p99'), ('Memory_P95_TiDB (%)', 'Memory_TiDB', 'p95'), ('Memory_Max_TiKV (%)', 'Memory_TiKV', 'max'), ('Memory_AVG_TiKV (%)', 'Memory_TiKV', 'avg'), ('Memory_P99_TiKV (%)', 'Memory_TiKV', 'p99'), ('Memory_P95_TiKV (%)', 'Memory_TiKV', 'p95'), ('Memory_Max_TiFlash (%)', 'Memory_TiFlash', 'max'), ('Memory_AVG_TiFlash (%)', 'Memory_TiFlash', 'avg'), ('Memory_P99_TiFlash (%)', 'Memory_TiFlash', 'p99'), ('Memory_P95_TiFlash (%)', 'Memory_TiFlash', 'p95'), ('Storage_Max_TiKV (TB)', 'Storage_TiKV', 'max'), ('Storage_Max_TiFlash (TB)', 'Storage_TiFlash', 'max'), ('IOPS_Exceed_Max (IOPS)', 'IOPS', 'max'), ('IOPS_Exceed_AVG (IOPS)', 'IOPS', 'avg'), ('Bandwidth_Max (MB/s)', 'Bandwidth', 'max'), ('Bandwidth_AVG (MB/s)', 'Bandwidth', 'avg') ] for metric_display, metric_key, stat_key in metrics_to_show: if metric_key in self.results: # 特殊处理CPS最大时间 if stat_key == 'max_time': cluster1_data = self.results[metric_key]['cluster1'] cluster2_data = self.results[metric_key]['cluster2'] cluster1_max_time = "N/A" cluster2_max_time = "N/A" if cluster1_data is not None and len(cluster1_data) > 0: value_cols = [col for col in cluster1_data.columns if col != 'Time'] if value_cols: max_idx = cluster1_data[value_cols[0]].idxmax() cluster1_max_time = cluster1_data.loc[max_idx, 'Time'].strftime('%Y-%m-%d %H:%M') if cluster2_data is not None and len(cluster2_data) > 0: value_cols = [col for col in cluster2_data.columns if col != 'Time'] if value_cols: max_idx = cluster2_data[value_cols[0]].idxmax() cluster2_max_time = cluster2_data.loc[max_idx, 'Time'].strftime('%Y-%m-%d %H:%M') table_data.append([metric_display, cluster1_max_time, cluster2_max_time, "N/A"]) # 特殊处理错误率 elif stat_key == 'error_rate': error_rate1 = self.calculate_error_rate( self.results['FailCount']['cluster1'], self.results['CPS']['cluster1'] ) error_rate2 = self.calculate_error_rate( self.results['FailCount']['cluster2'], self.results['CPS']['cluster2'] ) diff = ((error_rate2 - error_rate1) / error_rate1 * 100) if error_rate1 != 0 else "N/A" formatted_diff = f"{diff:.2f}%" if isinstance(diff, float) else diff table_data.append([metric_display, self.format_value(error_rate1, 'Day_ErrorRate'), self.format_value(error_rate2, 'Day_ErrorRate'), formatted_diff]) else: # 计算统计指标 stats1 = self.calculate_statistics(self.results[metric_key]['cluster1'], metric_key) stats2 = self.calculate_statistics(self.results[metric_key]['cluster2'], metric_key) # 获取第一个数值列的结果 value_cols1 = [col for col in stats1.keys()] value_cols2 = [col for col in stats2.keys()] if value_cols1 and value_cols2: col1 = value_cols1[0] col2 = value_cols2[0] val1 = stats1[col1].get(stat_key, 0) val2 = stats2[col2].get(stat_key, 0) formatted_val1 = self.format_value(val1, metric_key) formatted_val2 = self.format_value(val2, metric_key) diff = ((val2 - val1) / val1 * 100) if val1 != 0 else "N/A" formatted_diff = f"{diff:.2f}%" if isinstance(diff, float) else diff table_data.append([metric_display, formatted_val1, formatted_val2, formatted_diff]) # 生成表格字符串 table_str = f"| Metrics | {self.cluster1_name} | {self.cluster2_name} | Difference (%) |\n" table_str += "|---------|--------|--------|---------------|\n" for row in table_data: table_str += f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} |\n" return table_str def plot_comparison_charts(self): """绘制对比图表""" print("生成对比图表...") if self.output_dir: png_dir = os.path.join(self.output_dir, "png") else: png_dir = f"{self.cluster1_name}/png" if not os.path.exists(png_dir): os.makedirs(png_dir) print(f"创建图片目录: {png_dir}") # 定义固定的颜色 FIXED_COLOR_CLUSTER1 = '#2E86AB' # 蓝色 FIXED_COLOR_CLUSTER2 = '#A23B72' # 紫红色 # 定义指标和单位 metric_info = { 'P999': {'title': 'P999 延迟对比', 'unit': 'ms'}, 'P999_update': {'title': 'P999 Update 延迟对比', 'unit': 'ms'}, 'P999_delete': {'title': 'P999 Delete 延迟对比', 'unit': 'ms'}, 'P999_insert': {'title': 'P999 Insert 延迟对比', 'unit': 'ms'}, 'P999_select': {'title': 'P999 Select 延迟对比', 'unit': 'ms'}, 'P99': {'title': 'P99 延迟对比', 'unit': 'ms'}, 'P99_update': {'title': 'P99 Update 延迟对比', 'unit': 'ms'}, 'P99_delete': {'title': 'P99 Delete 延迟对比', 'unit': 'ms'}, 'P99_insert': {'title': 'P99 Insert 延迟对比', 'unit': 'ms'}, 'P99_select': {'title': 'P99 Select 延迟对比', 'unit': 'ms'}, 'P95': {'title': 'P95 延迟对比', 'unit': 'ms'}, 'P95_update': {'title': 'P95 Update 延迟对比', 'unit': 'ms'}, 'P95_delete': {'title': 'P95 Delete 延迟对比', 'unit': 'ms'}, 'P95_insert': {'title': 'P95 Insert 延迟对比', 'unit': 'ms'}, 'P95_select': {'title': 'P95 Select 延迟对比', 'unit': 'ms'}, 'CPS': {'title': 'CPS 吞吐量对比', 'unit': 'QPS'}, 'QPS': {'title': 'QPS 吞吐量对比', 'unit': 'QPS'}, 'Duration_AVG': {'title': 'duration_avg 对比', 'unit': 'ms'}, 'Duration_AVG_update': {'title': 'Duration AVG Update 对比', 'unit': 'ms'}, 'Duration_AVG_delete': {'title': 'Duration AVG Delete 对比', 'unit': 'ms'}, 'Duration_AVG_insert': {'title': 'Duration AVG Insert 对比', 'unit': 'ms'}, 'Duration_AVG_select': {'title': 'Duration AVG Select 对比', 'unit': 'ms'}, 'CPU_TiDB': {'title': 'TiDB CPU 使用率对比', 'unit': '%'}, 'CPU_TiKV': {'title': 'TiKV CPU 使用率对比', 'unit': '%'}, 'CPU_TiFlash': {'title': 'TiFlash CPU 使用率对比', 'unit': '%'}, 'Memory_TiDB': {'title': 'TiDB 内存使用率对比', 'unit': '%'}, 'Memory_TiKV': {'title': 'TiKV 内存使用率对比', 'unit': '%'}, 'Memory_TiFlash': {'title': 'TiFlash 内存使用率对比', 'unit': '%'}, 'Storage_TiKV': {'title': 'TiKV 存储使用量对比', 'unit': 'TB'}, 'Storage_TiFlash': {'title': 'TiFlash 存储使用量对比', 'unit': 'TB'}, 'IOPS': {'title': 'IOPS 超限对比', 'unit': 'IOPS'}, 'Bandwidth': {'title': '带宽对比', 'unit': 'MB/s'}, 'ErrorRate': {'title': '错误率对比', 'unit': '%'} } # 创建单独的图表文件 for metric_name, info in metric_info.items(): if metric_name in self.results: cluster1_data = self.results[metric_name]['cluster1'] cluster2_data = self.results[metric_name]['cluster2'] if cluster1_data is not None and cluster2_data is not None: # 获取数值列 value_cols1 = [col for col in cluster1_data.columns if col != 'Time'] value_cols2 = [col for col in cluster2_data.columns if col != 'Time'] if not value_cols1 or not value_cols2: continue col1 = value_cols1[0] col2 = value_cols2[0] # 处理数据单位转换 cluster1_values = [] cluster1_times = [] cluster2_values = [] cluster2_times = [] for i, val in enumerate(cluster1_data[col1]): parsed_val = self.parse_numeric_value(val) if parsed_val is not None: cluster1_values.append(parsed_val) cluster1_times.append(cluster1_data['Time'].iloc[i]) for i, val in enumerate(cluster2_data[col2]): parsed_val = self.parse_numeric_value(val) if parsed_val is not None: cluster2_values.append(parsed_val) cluster2_times.append(cluster2_data['Time'].iloc[i]) cluster1_values = pd.Series(cluster1_values) cluster2_values = pd.Series(cluster2_values) cluster1_times = pd.Series(cluster1_times) cluster2_times = pd.Series(cluster2_times) # 处理延迟类指标的数据转换(秒转毫秒) if metric_name.startswith('P999') or metric_name.startswith('P99') or metric_name.startswith('P95') or metric_name.startswith('Duration_AVG'): cluster1_values = cluster1_values * 1000 cluster2_values = cluster2_values * 1000 # 创建单个图表 fig, ax = plt.subplots(figsize=(14, 8)) # 绘制第一个集群的数据 ax.plot(cluster1_times, cluster1_values, label=f'{self.cluster1_name}', color=FIXED_COLOR_CLUSTER1, linewidth=2.5, alpha=0.8, marker='o', markersize=3) # 绘制第二个集群的数据 ax.plot(cluster2_times, cluster2_values, label=f'{self.cluster2_name}', color=FIXED_COLOR_CLUSTER2, linewidth=2.5, alpha=0.8, marker='s', markersize=3) # 设置图表样式 ax.set_title(info['title'], fontsize=16, fontweight='bold', pad=20) ax.set_xlabel('时间', fontsize=12, fontweight='bold') ax.set_ylabel(f'数值 ({info["unit"]})', fontsize=12, fontweight='bold') # 美化图例 ax.legend(fontsize=12, frameon=True, fancybox=True, shadow=True) # 美化网格 ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.8) ax.set_facecolor('#FAFAFA') # 设置x轴范围(使用合并的时间范围) all_times = pd.concat([cluster1_times, cluster2_times]) if not all_times.empty and not all_times.isna().all(): ax.set_xlim(all_times.min(), all_times.max()) # 格式化x轴时间显示 ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) ax.xaxis.set_major_locator(mdates.HourLocator(interval=4)) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, fontsize=10) # 美化y轴 ax.tick_params(axis='y', labelsize=10) # 添加统计信息文本框 cluster1_max = cluster1_values.max() cluster2_max = cluster2_values.max() cluster1_avg = cluster1_values.mean() cluster2_avg = cluster2_values.mean() stats_text = f'{self.cluster1_name}: Max={cluster1_max:.2f}, Avg={cluster1_avg:.2f}\n{self.cluster2_name}: Max={cluster2_max:.2f}, Avg={cluster2_avg:.2f}' ax.text(0.02, 0.98, stats_text, transform=ax.transAxes, fontsize=10, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) # 保存图表 filename = f'{metric_name.lower()}_comparison.png' filepath = os.path.join(png_dir, filename) plt.tight_layout() plt.savefig(filepath, dpi=300, bbox_inches='tight') plt.close() print(f"图表已保存为 {filepath}") # 生成错误率图表 self.plot_error_rate_chart(png_dir, FIXED_COLOR_CLUSTER1, FIXED_COLOR_CLUSTER2) print("所有图表生成完成!") def plot_error_rate_chart(self, png_dir, color1, color2): """生成错误率图表 - 每个时刻相对于全天总量的错误率""" if 'FailCount' in self.results and 'CPS' in self.results: failcount_data1 = self.results['FailCount']['cluster1'] failcount_data2 = self.results['FailCount']['cluster2'] cps_data1 = self.results['CPS']['cluster1'] cps_data2 = self.results['CPS']['cluster2'] if all(data is not None for data in [failcount_data1, failcount_data2, cps_data1, cps_data2]): # 先计算全天的总量 total_failures1, total_requests1 = self.calculate_total_failures_requests(failcount_data1, cps_data1) total_failures2, total_requests2 = self.calculate_total_failures_requests(failcount_data2, cps_data2) # 计算每个时刻相对于全天总量的错误率 error_rate1 = [] error_rate2 = [] time_points1 = [] time_points2 = [] # 计算第一个集群每个时刻的错误率 for i in range(len(failcount_data1)): if i < len(cps_data1): fail_cols = [col for col in failcount_data1.columns if col != 'Time'] cps_cols = [col for col in cps_data1.columns if col != 'Time'] if fail_cols and cps_cols: failures = self.parse_numeric_value(failcount_data1.iloc[i][fail_cols[0]]) requests = self.parse_numeric_value(cps_data1.iloc[i][cps_cols[0]]) if failures is not None and requests is not None and total_requests1 > 0: # 计算这个时刻的错误数相对于全天总量的比例 moment_error_rate = (failures / total_requests1) * 100 error_rate1.append(moment_error_rate) time_points1.append(failcount_data1.iloc[i]['Time']) # 计算第二个集群每个时刻的错误率 for i in range(len(failcount_data2)): if i < len(cps_data2): fail_cols = [col for col in failcount_data2.columns if col != 'Time'] cps_cols = [col for col in cps_data2.columns if col != 'Time'] if fail_cols and cps_cols: failures = self.parse_numeric_value(failcount_data2.iloc[i][fail_cols[0]]) requests = self.parse_numeric_value(cps_data2.iloc[i][cps_cols[0]]) if failures is not None and requests is not None and total_requests2 > 0: # 计算这个时刻的错误数相对于全天总量的比例 moment_error_rate = (failures / total_requests2) * 100 error_rate2.append(moment_error_rate) time_points2.append(failcount_data2.iloc[i]['Time']) # 绘制错误率图表 plt.figure(figsize=(16, 10)) if time_points1 and error_rate1: plt.plot(time_points1, error_rate1, color=color1, linewidth=2, label=f'{self.cluster1_name}', alpha=0.8, marker='o', markersize=3) if time_points2 and error_rate2: plt.plot(time_points2, error_rate2, color=color2, linewidth=2, label=f'{self.cluster2_name}', alpha=0.8, marker='s', markersize=3) plt.title('错误率对比', fontsize=16, fontweight='bold') plt.xlabel('时间', fontsize=12) plt.ylabel('错误率 (%)', fontsize=12) plt.grid(True, alpha=0.3) plt.legend(fontsize=12) # 设置时间轴范围(以第一个集群的时间为准) if time_points1: plt.xlim(min(time_points1), max(time_points1)) plt.xticks(rotation=45) # 保存图表 filepath = os.path.join(png_dir, 'errorrate_comparison.png') plt.tight_layout() plt.savefig(filepath, dpi=300, bbox_inches='tight') plt.close() print(f"错误率图表已保存为 {filepath}") def calculate_total_failures_requests(self, failcount_data, cps_data): """计算总失败数和总请求数""" if failcount_data is None or cps_data is None: return 0.0, 0.0 # 获取数值列 failcount_cols = [col for col in failcount_data.columns if col != 'Time'] cps_cols = [col for col in cps_data.columns if col != 'Time'] if not failcount_cols or not cps_cols: return 0.0, 0.0 # 计算总失败数和总请求数 total_failures = 0.0 total_requests = 0.0 # 遍历所有数据点 for i in range(len(failcount_data)): if i < len(cps_data): # 解析失败数 fail_val = self.parse_numeric_value(failcount_data.iloc[i][failcount_cols[0]]) # 解析请求数 cps_val = self.parse_numeric_value(cps_data.iloc[i][cps_cols[0]]) if fail_val is not None and cps_val is not None: total_failures += fail_val total_requests += cps_val return total_failures, total_requests def run_analysis(self): """运行完整分析""" # 加载数据 self.load_all_data() # 生成对比表格 table_str = self.generate_comparison_table() print("\n对比表格:") print(table_str) # 保存表格到对应目录 table_filename = f"{self.cluster1_name}_comparison_table.md" if self.output_dir: if not os.path.exists(self.output_dir): os.makedirs(self.output_dir) table_filepath = os.path.join(self.output_dir, table_filename) else: if not os.path.exists(f"{self.cluster1_name}"): os.makedirs(f"{self.cluster1_name}") table_filepath = os.path.join(f"{self.cluster1_name}", table_filename) with open(table_filepath, 'w', encoding='utf-8') as f: f.write(table_str) print(f"\n表格已保存为 {table_filepath}") # 生成图表 self.plot_comparison_charts() print("\n分析完成!") def main(): """主函数""" import sys if len(sys.argv) < 2: print("用法: python compare_cluster_metrics.py [output_dir]") print("支持的集群类型: saas05, gogame") sys.exit(1) cluster_type = sys.argv[1].lower() output_dir = sys.argv[2] if len(sys.argv) >= 3 else None time_offset = None if len(sys.argv) >= 4: try: days, hours, minutes = map(int, sys.argv[3].split(',')) time_offset = timedelta(days=days, hours=hours, minutes=minutes) except ValueError: print("无效的 time_offset 格式。请使用 'days,hours,minutes' 格式,例如 '13,11,23'") sys.exit(1) if cluster_type == 'prod': comparator = ClusterMetricsComparator( data_dir="prod/data", cluster1_name="prod", cluster2_name="replay", time_offset=time_offset, output_dir=output_dir ) else: print(f"不支持的集群类型: {cluster_type}") print("支持的集群类型: prod") sys.exit(1) comparator.run_analysis() if __name__ == "__main__": main()