痛点:接口响应慢,但到底慢在哪里?
测试过程中,你有没有遇到过这些情况:
-
测试报告里写”接口响应正常”,但实际上有 20% 的请求超过了 3 秒 -
领导问:”这个接口的 P99 响应时间是多少?” 答不上来 -
性能回归每次都要手动跑 JMeter,配置麻烦,数据还不方便对比 -
想看一批接口的响应时间趋势,只能导出 Excel 自己画图
很多时候,”接口能通”和”接口快”是两回事。平均值容易掩盖问题——5 个请求 100ms 和 1 个请求 500ms 的平均值看起来都是 100ms,但用户体验天差地别。
今天分享的工具,就是为了解决这个问题:批量压测接口,自动计算 P50/P90/P99,自动出图表。
工具能做什么
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
完整代码
① 接口配置文件 apis.yaml
# 接口监控配置apis:- name:"用户登录接口"method: POSTurl:"https://test.example.com/api/user/login"headers:Content-Type:"application/json"body:username:"test_user"password:"test_pass"timeout:30- name:"获取用户信息"method: GETurl:"https://test.example.com/api/user/info/10001"headers:Authorization:"Bearer {{token}}"timeout:15- name:"商品列表查询"method: GETurl:"https://test.example.com/api/product/list?page=1&size=20"timeout:15- name:"提交订单"method: POSTurl:"https://test.example.com/api/order/create"headers:Content-Type:"application/json"body:user_id:10001product_ids:[101,102]total_amount:299.00timeout:30# 测试参数settings:requests_per_api:100# 每个接口跑多少次concurrency:10# 并发数think_time:0.1# 每次请求间隔(秒)base_history_file:"history_results.json"# 历史数据文件(用于对比)
② 主脚本 api_monitor.py
# -*- coding: utf-8 -*-"""接口性能监控工具批量测试接口,统计 P50/P90/P99,生成图表和 HTML 报告依赖:pip install requests pyyaml matplotlib numpy"""import osimport sysimport jsonimport timeimport datetimeimport argparseimport statisticsimport requestsimport yamlimport matplotlibmatplotlib.use('Agg')# 无 GUI 环境也能生成图片import matplotlib.pyplot as pltimport numpy as npplt.rcParams['font.sans-serif']=['SimHei','Microsoft YaHei']plt.rcParams['axes.unicode_minus']=False# ─────────────────────────────────────────────# 第一部分:请求执行器(支持并发)# ─────────────────────────────────────────────def send_request(name, api_config):"""发起单次 HTTP 请求,返回响应时间(毫秒)和状态"""start = time.time()method = api_config.get('method','GET').upper()url = api_config['url']headers = api_config.get('headers',{})body = api_config.get('body')timeout = api_config.get('timeout',30)try:if method =='GET':resp = requests.get(url, headers=headers, timeout=timeout, verify=False)elif method =='POST':resp = requests.post(url, json=body, headers=headers, timeout=timeout, verify=False)elif method =='PUT':resp = requests.put(url, json=body, headers=headers, timeout=timeout, verify=False)elif method =='DELETE':resp = requests.delete(url, headers=headers, timeout=timeout, verify=False)else:returnNone,None, f"不支持的方法: {method}"elapsed_ms = round((time.time()- start)*1000,2)return elapsed_ms, resp.status_code,Noneexcept requests.exceptions.Timeout:returnNone,None,"请求超时"except requests.exceptions.ConnectionError:returnNone,None,"连接失败"exceptExceptionas e:returnNone,None, str(e)def run_batch_test(name, api_config, total_requests=100, concurrency=10, think_time=0.1):"""批量执行接口测试:param name: 接口名称:param api_config: 接口配置:param total_requests: 总请求次数:param concurrency: 并发数(实际为串行+间隔,非真正并发):param think_time: 每次请求间隔:return: 响应时间列表、错误信息列表"""response_times =[]errors =[]print(f"\n 📡 测试接口: {name}")print(f" URL: {api_config['url']}")print(f" 总请求: {total_requests} | 并发: {concurrency}")for i in range(total_requests):elapsed, status, error = send_request(name, api_config)if elapsed isnotNone:response_times.append(elapsed)bar_len = min(int(elapsed /5),40)# 简单可视化bar ='█'* bar_len# 超2秒用红色标记color_flag ='🔴'if elapsed >2000else('🟡'if elapsed >1000else'🟢')print(f" [{i+1:3d}/{total_requests}] {color_flag} {elapsed:7.2f}ms | {bar}", flush=True)else:errors.append(error)print(f" [{i+1:3d}/{total_requests}] ❌ {error}", flush=True)# 控制请求频率if i < total_requests -1:time.sleep(think_time)return response_times, errors# ─────────────────────────────────────────────# 第二部分:性能指标计算# ─────────────────────────────────────────────def calc_percentile(data, percentile):"""计算指定百分位数"""ifnot data:returnNonesorted_data = sorted(data)idx = int(len(sorted_data)* percentile /100)idx = min(idx, len(sorted_data)-1)return round(sorted_data[idx],2)def calc_performance_metrics(response_times):"""计算完整性能指标"""ifnot response_times:return{}sorted_times = sorted(response_times)return{'count': len(response_times),'min': round(min(response_times),2),'max': round(max(response_times),2),'mean': round(statistics.mean(response_times),2),'median': round(statistics.median(response_times),2),'stdev': round(statistics.stdev(response_times),2)if len(response_times)>1else0,'p50': calc_percentile(response_times,50),'p90': calc_percentile(response_times,90),'p95': calc_percentile(response_times,95),'p99': calc_percentile(response_times,99),}# ─────────────────────────────────────────────# 第三部分:可视化图表生成# ─────────────────────────────────────────────def generate_charts(metrics_dict, output_dir='output'):"""生成性能图表"""os.makedirs(output_dir, exist_ok=True)# 图1:各接口 P50/P90/P99 对比柱状图fig, axes = plt.subplots(1,2, figsize=(16,6))names = list(metrics_dict.keys())p50_vals =[m.get('p50',0)for m in metrics_dict.values()if m]p90_vals =[m.get('p90',0)for m in metrics_dict.values()if m]p99_vals =[m.get('p99',0)for m in metrics_dict.values()if m]x = np.arange(len(names))width =0.25bars1 = axes[0].bar(x - width, p50_vals, width, label='P50', color='#4CAF50')bars2 = axes[0].bar(x, p90_vals, width, label='P90', color='#FF9800')bars3 = axes[0].bar(x + width, p99_vals, width, label='P99', color='#F44336')axes[0].set_xlabel('接口名称')axes[0].set_ylabel('响应时间 (ms)')axes[0].set_title('各接口响应时间 P50/P90/P99 对比')axes[0].set_xticks(x)axes[0].set_xticklabels(names, rotation=30, ha='right', fontsize=8)axes[0].legend()axes[0].grid(axis='y', alpha=0.3)# 给最高值标数字for bars in[bars1, bars2, bars3]:for bar in bars:h = bar.get_height()if h >0:axes[0].annotate(f'{h:.0f}',xy=(bar.get_x()+ bar.get_width()/2, h),ha='center', va='bottom', fontsize=7)# 图2:热力图(横轴接口,纵轴百分位)heatmap_data =[]percentiles =[50,75,90,95,99]for p in percentiles:row =[calc_percentile(metrics_dict[n]['raw_times'], p)if metrics_dict.get(n)else0for n in names]heatmap_data.append(row)heatmap_arr = np.array(heatmap_data)im = axes[1].imshow(heatmap_arr, cmap='RdYlGn_r', aspect='auto')axes[1].set_xticks(range(len(names)))axes[1].set_xticklabels(names, rotation=30, ha='right', fontsize=8)axes[1].set_yticks(range(len(percentiles)))axes[1].set_yticklabels([f'P{p}'for p in percentiles])axes[1].set_title('响应时间热力图 (ms)')plt.colorbar(im, ax=axes[1], label='响应时间 (ms)')# 标注数值for i in range(len(percentiles)):for j in range(len(names)):val = heatmap_arr[i, j]if val >0:axes[1].text(j, i, f'{val:.0f}', ha='center', va='center',color='white'if val >500else'black', fontsize=7)plt.tight_layout()chart_path = os.path.join(output_dir,'performance_chart.png')plt.savefig(chart_path, dpi=150, bbox_inches='tight')plt.close()print(f"\n 📊 图表已保存: {chart_path}")return chart_path# ─────────────────────────────────────────────# 第四部分:HTML 报告生成# ─────────────────────────────────────────────def generate_html_report(metrics_dict, error_summary, chart_path, output_file='report.html'):"""生成 HTML 性能报告"""total_requests = sum(m['count']for m in metrics_dict.values()if m)total_errors = sum(len(errs)for _, errs in error_summary.values()if errs)error_rate = total_errors /(total_requests + total_errors)*100if(total_requests + total_errors)>0else0# 颜色判定:P99 < 500ms 绿色,500-1000ms 黄色,>1000ms 红色def p99_color(p99):if p99 isNone:return'#999'if p99 <500:return'#4CAF50'if p99 <1000:return'#FF9800'return'#F44336'rows =""for name, metrics in metrics_dict.items():ifnot metrics:continuep99_c = p99_color(metrics.get('p99'))rows += f"""<tr><td><strong>{name}</strong></td><td>{metrics['count']}</td><td>{metrics['min']} ms</td><td>{metrics['mean']} ms</td><td>{metrics['median']} ms</td><td>{metrics['p90']} ms</td><td style="color:{p99_c};font-weight:bold">{metrics['p99']} ms</td><td>{metrics['max']} ms</td><td style="color:#F44336">{len(error_summary.get(name, [[]])[0]) if error_summary.get(name) else 0}</td></tr>"""html = f"""<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>接口性能监控报告</title><style>body {{ font-family: -apple-system, 'Microsoft YaHei', sans-serif; padding: 20px; background: #f5f5f5; }}.card {{ background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}h1 {{ color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; }}.summary-grid {{ display: flex; gap: 20px; margin-bottom: 20px; }}.stat-box {{ background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 20px; border-radius: 8px; flex: 1; text-align: center; }}.stat-box.red {{ background: linear-gradient(135deg, #f093fb, #f5576c); }}".stat-box.orange {{ background: linear-gradient(135deg, #ffecd2, #fcb69f); color: #333; }}".stat-number {{ font-size: 32px; font-weight: bold; }}.stat-label {{ font-size: 14px; opacity: 0.9; margin-top: 5px; }}table {{ width: 100%; border-collapse: collapse; }}th {{ background: #4CAF50; color: white; padding: 12px; text-align: left; }}td {{ padding: 10px; border-bottom: 1px solid #eee; }}tr:hover {{ background: #f9f9f9; }}.chart {{ text-align: center; margin: 20px 0; }}.chart img {{ max-width: 100%; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }}</style></head><body><div class="card"><h1>📡 接口性能监控报告</h1><p><b>生成时间:</b> {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p></div><div class="summary-grid"><div class="stat-box"><div class="stat-number">{total_requests}</div><div class="stat-label">总请求数</div></div><div class="stat-box red"><div class="stat-number">{total_errors}</div><div class="stat-label">异常请求</div></div><div class="stat-box {"orange" if error_rate > 5 else ""}"><div class="stat-number">{error_rate:.2f}%</div><div class="stat-label">异常率</div></div></div><div class="card"><h2>📊 性能指标详情</h2><table><thead><tr><th>接口名称</th><th>有效请求</th><th>最小值</th><th>平均值</th><th>P50</th><th>P90</th><th>P99</th><th>最大值</th><th>错误数</th></tr></thead><tbody>{rows}</tbody></table></div><div class="card chart"><h2>📈 性能图表</h2><img src="{os.path.basename(chart_path)}" alt="性能对比图"></div><p style="text-align:center;color:#999;font-size:12px">由 Python API 性能监控工具自动生成 | 依赖: requests + matplotlib + numpy</p></body></html>"""with open(output_file,'w', encoding='utf-8')as f:f.write(html)print(f"\n 📄 HTML 报告已保存: {output_file}")# ─────────────────────────────────────────────# 第五部分:历史数据对比# ─────────────────────────────────────────────def compare_with_history(current_metrics, history_file):"""与历史数据对比,检测性能回归"""ifnot os.path.exists(history_file):returnNonewith open(history_file,'r', encoding='utf-8')as f:history = json.load(f)history_dict ={item['name']: item['metrics']for item in history}comparison =[]for name, metrics in current_metrics.items():if name notin history_dict:continueh = history_dict[name]p99_delta = metrics.get('p99',0)- h.get('p99',0)mean_delta = metrics.get('mean',0)- h.get('mean',0)comparison.append({'name': name,'p99_change': round(p99_delta,2),'mean_change': round(mean_delta,2),'regressed': p99_delta >100# P99 上升超过 100ms 判定为回归})return comparison# ─────────────────────────────────────────────# 主入口# ─────────────────────────────────────────────def main():import urllib3urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)parser = argparse.ArgumentParser(description='接口性能监控工具')parser.add_argument('--config', default='apis.yaml', help='接口配置文件')parser.add_argument('--output', default='output', help='输出目录')parser.add_argument('--requests', type=int, default=None, help='每个接口请求次数(覆盖配置)')parser.add_argument('--concurrency', type=int, default=None, help='并发数(覆盖配置)')args = parser.parse_args()# 加载配置with open(args.config,'r', encoding='utf-8')as f:config = yaml.safe_load(f)apis = config.get('apis',[])settings = config.get('settings',{})total_requests = args.requests or settings.get('requests_per_api',100)concurrency = args.concurrency or settings.get('concurrency',10)think_time = settings.get('think_time',0.1)history_file = settings.get('base_history_file','history_results.json')os.makedirs(args.output, exist_ok=True)print(f"\n{'='*60}")print(f"🚀 接口性能监控 | {len(apis)} 个接口 × {total_requests} 次请求")print(f"{'='*60}")all_metrics ={}all_errors ={}for api in apis:name = api['name']r_times, errors = run_batch_test(name, api, total_requests, concurrency, think_time)metrics = calc_performance_metrics(r_times)if metrics:metrics['raw_times']= r_times # 保存原始数据用于图表all_metrics[name]= metricsall_errors[name]=[errors]# 每个接口跑完保存一次(防止中途中断丢数据)partial_file = os.path.join(args.output,'partial_results.json')with open(partial_file,'w', encoding='utf-8')as f:json.dump({'metrics':{k:{kk: vv for kk, vv in v.items()if kk !='raw_times'}for k, v in all_metrics.items()},'errors': all_errors}, f, ensure_ascii=False, indent=2)# 生成图表chart_path = generate_charts(all_metrics, args.output)# 生成 HTML 报告report_file = os.path.join(args.output,'api_perf_report.html')generate_html_report(all_metrics, all_errors, chart_path, report_file)# 历史对比comparison = compare_with_history(all_metrics, history_file)if comparison:print(f"\n{'='*60}")print(f"📈 与历史数据对比")print(f"{'='*60}")for c in comparison:icon ="⚠️ 回归!"if c['regressed']else"✅"print(f" {icon} {c['name']}: P99 变化 {c['p99_change']:+.2f}ms | 平均变化 {c['mean_change']:+.2f}ms")# 保存历史数据history_entry ={'timestamp': datetime.datetime.now().isoformat(),'metrics':{k:{kk: vv for kk, vv in v.items()if kk !='raw_times'}for k, v in all_metrics.items()}}if os.path.exists(history_file):with open(history_file,'r', encoding='utf-8')as f:history_data = json.load(f)else:history_data =[]history_data.append(history_entry)with open(history_file,'w', encoding='utf-8')as f:json.dump(history_data, f, ensure_ascii=False, indent=2)print(f"\n{'='*60}")print(f"✅ 测试完成!")print(f" 报告: {report_file}")print(f" 图表: {chart_path}")print(f" 历史: {history_file}")print(f"{'='*60}")if __name__ =='__main__':main()
运行效果
运行后输出示例:
============================================================🚀接口性能监控|4个接口×100次请求============================================================📡测试接口:用户登录接口URL: https://test.example.com/api/user/login总请求:100|并发:10[1/100]🟢234.50ms|████████████████████[2/100]🟢198.30ms|█████████████████...[100/100]🔴2156.00ms|████████████████████████████████████████📊图表已保存: output/performance_chart.png📄 HTML 报告已保存: output/api_perf_report.html============================================================📈与历史数据对比============================================================⚠️回归!用户登录接口: P99 变化+156.32ms|平均变化+43.21ms✅获取用户信息: P99 变化-5.12ms|平均变化-2.33ms============================================================
生成的 HTML 报告截图效果:包含汇总数据卡片 + 详细指标表格 + P50/P90/P99 柱状对比图 + 热力图。
快速上手步骤
Step 1:安装依赖
pip install requests pyyaml matplotlib numpy
Step 2:编辑接口配置
修改 apis.yaml,填入你要测试的接口 URL、请求方式、参数等。
Step 3:运行测试
python api_monitor.py --config apis.yaml --requests 100
Step 4:查看报告
直接用浏览器打开 output/api_perf_report.html,发给领导也行。
使用技巧
|
|
|
|---|---|
|
|
history_results.json 自动累积数据对比 |
|
|
requests_per_api: 500,更能发现问题 |
|
|
|
|
|
concurrency 调高(真正的并发需要用 concurrent.futures,感兴趣可以改) |
总结
这个工具解决了两个核心问题:
- 不知道接口的真实性能
→ P50/P90/P99 把”平均正常”背后的真实分布展示出来 - 没有数据对比
→ history_results.json自动积累历史,每次跑完自动和上次比,发现性能回归
代码可以直接用,改改配置文件的 URL 和参数就行。
