系统工具的高并发压测:用 goose 模拟 1000 并发,找到真正瓶颈
系统工具的高并发压测用 goose 模拟 1000 并发找到真正瓶颈我第一次给一个 Rust HTTP 服务做压测时用的是abApache Benchmark结果跑了 30 秒就报错了——ab不支持复杂的请求序列我需要模拟先登录再查询再退出的三步流程。后来找到 goose一个 Rust 写的负载测试框架支持多步骤场景、实时统计、甚至可以动态调整并发数。这篇就把我用 goose 模拟 1000 并发、从看起来没问题到找到真正瓶颈的过程写出来。一、为什么选 goose 而不是 ab/wrk——一个的实用选择我试过三个压测工具ab最简单一行命令就行但只支持单步请求不支持场景流程。wrk支持多线程比 ab 快但脚本需要用 Lua 写Lua 我不会。gooseRust 写的场景用 Rust 代码定义对我这种 Rust 学习者最友好。goose 的核心优势use goose::prelude::*; /// goose 的基本用法定义一个负载测试场景 fn main() - Result(), GooseError { // 创建负载测试 GooseAttack::initialize()? // 注册场景先登录再查询再退出 .register_scenario( scenario!(UserFlow) .set_weight(10)? // 这个场景占10的权重多个场景时按权重分配 .register_task( task!(登录) .set_weight(5)? // 登录步骤权重5在场景内按权重分配 .execute(login_task) ) .register_task( task!(查询) .set_weight(8)? // 查询步骤权重8比登录更频繁 .execute(query_task) ) .register_task( task!(退出) .set_weight(2)? // 退出步骤权重2最少 .execute(logout_task) ) ) // 设置并发参数 .set_default(GooseDefault::HatchRate, 50)? // 每秒增加50个用户 .set_default(GooseDefault::Users, 1000)? // 总共1000并发用户 .set_default(GooseDefault::RunTime, 300)? // 运行300秒 .set_default(GooseDefault::Host, http://localhost:8080)? .execute()? .print_stats(); Ok(()) } /// 登录步骤发送登录请求 async fn login_task(user: mut GooseUser) - GooseTaskResult { let response user.post(/api/login, usernametestpassword123456).await?; // 检查登录是否成功 if response.status_code ! 200 { return Err(GooseTaskError::TaskFailed(登录失败)); } Ok(()) } /// 查询步骤发送查询请求 async fn query_task(user: mut GooseUser) - GooseTaskResult { let response user.get(/api/query?keywordrust).await?; // 检查查询结果 if response.status_code ! 200 { return Err(GooseTaskError::TaskFailed(查询失败)); } Ok(()) } /// 退出步骤发送退出请求 async fn logout_task(user: mut GooseUser) - GooseTaskResult { let response user.post(/api/logout, ).await?; Ok(()) }flowchart TD A[压测工具选择] -- B{场景需求} B --|单步请求| C[ab / wrkbr/简单够用] B --|多步骤流程| D[goosebr/Rust 场景定义] B --|复杂逻辑数据驱动| E[k6br/JavaScript 脚本] D -- F[✅ 场景用 Rust 写br/✅ 实时统计br/✅ 动态调整并发] style D fill:#6f6二、压测前的准备别一上来就 1000 并发很多新手包括我做压测时一上来就设 1000 并发结果服务直接崩溃什么数据都拿不到。正确的做法是渐进式压测从小并发开始逐步增加观察每个阶段的服务表现。# 第一轮10并发跑60秒——验证基本功能 goose --host http://localhost:8080 --users 10 --hatch-rate 2 --run-time 60 # 第二轮100并发跑120秒——找性能拐点 goose --host http://localhost:8080 --users 100 --hatch-rate 10 --run-time 120 # 第三轮500并发跑180秒——接近极限 goose --host http://localhost:8080 --users 500 --hatch-rate 50 --run-time 180 # 第四轮1000并发跑300秒——极限测试 goose --host http://localhost:8080 --users 1000 --hatch-rate 50 --run-time 300我用 Rust 代码实现渐进式压测方便统计每个阶段的数据use goose::prelude::*; /// 渐进式压测逐步增加并发观察服务性能变化 fn progressive_load_test() - Result(), GooseError { // 第一阶段10并发验证功能 let phase1 GooseAttack::initialize()? .register_scenario( scenario!(BasicFlow) .register_task(task!(健康检查).execute(health_check)) .register_task(task!(简单查询).execute(simple_query)) ) .set_default(GooseDefault::Users, 10)? .set_default(GooseDefault::HatchRate, 2)? // 每秒加2个用户 .set_default(GooseDefault::RunTime, 60)? .set_default(GooseDefault::Host, http://localhost:8080)?; println!( 第一阶段10并发验证基本功能 ); let result1 phase1.execute()?; // 检查第一阶段结果如果有大量错误不需要继续了 let error_rate1 result1.error_rate(); if error_rate1 0.05 { // 错误率超过5%就停止 println!(第一阶段错误率 {:.1}%超过阈值停止压测, error_rate1 * 100); return Ok(()); } // 第二阶段100并发找拐点 println!( 第二阶段100并发寻找性能拐点 ); // 继续增加并发... Ok(()) } async fn health_check(user: mut GooseUser) - GooseTaskResult { let response user.get(/api/health).await?; if response.status_code ! 200 { return Err(GooseTaskError::TaskFailed(健康检查失败)); } Ok(()) } async fn simple_query(user: mut GooseUser) - GooseTaskResult { let response user.get(/api/items?page1).await?; if response.status_code ! 200 { return Err(GooseTaskError::TaskFailed(查询失败)); } Ok(()) }graph LR A[10并发br/验证功能br/60秒] -- B[100并发br/找拐点br/120秒] B -- C[500并发br/接近极限br/180秒] C -- D[1000并发br/极限测试br/300秒] A --|错误率5%br/停止| A1[❌ 功能有问题br/先修复] B --|错误率10%br/停止| B1[⚠️ 拐点已到br/分析瓶颈] style A1 fill:#f66 style B1 fill:#f96三、1000 并发压测数据解读找到真正的瓶颈1000 并发下我的服务数据如下请求总数: 45,678 平均响应时间: 125ms P50: 80ms P90: 320ms P99: 1,200ms 错误率: 12.3% 吞吐量: 152 req/s这个数据一看就知道有问题P99 是 1.2 秒而 P50 只有 80ms说明有少量请求极度缓慢。错误率 12.3% 也不正常。我用 goose 的set_default(GooseDefault::MetricsFile, metrics.csv)导出详细统计发现瓶颈在数据库查询use goose::prelude::*; /// 详细的压测任务每个步骤都计时 async fn db_query_task(user: mut GooseUser) - GooseTaskResult { // 第一步快速请求缓存命中 let fast_response user.get(/api/items/cached).await?; user.set_task_data(cached_time, fast_response.response_time); // 第二步慢速请求数据库查询 let slow_response user.get(/api/items/db-query).await?; user.set_task_data(db_time, slow_response.response_time); // 比较缓存请求 50ms数据库请求 500ms // 瓶颈确认数据库查询是性能拐点 Ok(()) } /// 分析压测数据找出瓶颈所在 fn analyze_bottleneck(metrics: GooseMetrics) { println!( 瓶颈分析 ); // 找出最慢的请求路径 let slowest_path metrics.task_data.iter() .max_by_key(|(_, data)| data.average_response_time) .map(|(path, data)| (path, data.average_response_time)); if let Some((path, avg_time)) slowest_path { println!(最慢路径: {} (平均 {}ms), path, avg_time); if avg_time 500 { println!(⚠️ 瓶颈确认: {} 平均超过 500ms, path); println!(建议: 添加缓存 / 优化查询 / 增加连接池); } } // 分析错误分布 let error_paths metrics.task_data.iter() .filter(|(_, data)| data.error_rate 0.01) .map(|(path, data)| (path, data.error_rate)); for (path, rate) in error_paths { println!(错误率异常: {} ({:.1}%), path, rate * 100); } }实际的瓶颈定位graph TD A[1000并发压测数据] -- B[P5080ms P991.2sbr/差距巨大] B -- C{慢请求在哪} C -- D[/api/items/cachedbr/平均50ms ✅] C -- E[/api/items/db-querybr/平均600ms ⚠️] C -- F[/api/items/writebr/平均800ms ⚠️] E -- G[瓶颈数据库读取br/连接池不够/查询慢] F -- H[瓶颈数据库写入br/锁竞争严重] G -- I[修复方案br/1. 添加 Redis 缓存br/2. 增大连接池br/3. 优化 SQL] style E fill:#f96 style F fill:#f66 style I fill:#6f6四、瓶颈修复与二次压测从 152 req/s 到 850 req/s根据压测数据我做了三个修复use sqlx::postgres::PgPoolOptions; use redis::Client; use std::sync::Arc; /// 修复1增大数据库连接池从10到100 async fn fix_connection_pool() - Arcsqlx::PgPool { let pool PgPoolOptions::new() .max_connections(100) // 从10增加到1001000并发需要更多连接 .min_connections(10) // 保持10个空闲连接 .acquire_timeout(std::time::Duration::from_secs(5)) // 等待5秒获取连接 .connect(postgres://localhost/mydb) .await .expect(数据库连接失败); Arc::new(pool) } /// 修复2热点查询加 Redis 缓存 async fn fix_with_cache(redis: ArcClient, pool: Arcsqlx::PgPool) { let mut redis_conn redis.get_connection().unwrap(); // 查询前先查缓存 let cached: OptionString redis::cmd(GET) .arg(items:page:1) .query(mut redis_conn) .ok(); if let Some(data) cached { // 缓存命中从 600ms 降到 5ms println!(缓存命中响应时间从 600ms → 5ms); } else { // 缓存未命中查数据库然后缓存结果 let items sqlx::query_as::_, Item(SELECT * FROM items LIMIT 100) .fetch_all(*pool) .await .expect(查询失败); // 缓存30秒避免频繁更新时缓存过期太快 redis::cmd(SETEX) .arg(items:page:1) .arg(30) .arg(serde_json::to_string(items).unwrap()) .execute(mut redis_conn) .ok(); } } /// 修复3写入操作用批量插入减少锁竞争 async fn fix_batch_write(pool: sqlx::PgPool) { // 原来逐条插入每次都要锁表 // for item in items { // sqlx::query(INSERT INTO items ...).bind(item).execute(pool).await; // } // 修复后批量插入一次操作搞定 sqlx::query(INSERT INTO items (name, value) SELECT name, value FROM unnest($1, $2)) .bind(vec![item1, item2, item3]) // 批量名称 .bind(vec![100, 200, 300]) // 批量值 .execute(pool) .await .expect(批量插入失败); } struct Item { name: String, value: i32, }修复后的二次压测数据对比指标修复前修复后提升吞吐量152 req/s850 req/s5.6xP5080ms12ms6.7xP991,200ms180ms6.7x错误率12.3%0.1%123x连接池等待500ms2ms250xgraph LR subgraph 修复前 A1[吞吐 152 req/s] A2[P99 1.2s] A3[错误率 12.3%] end subgraph 修复后 B1[吞吐 850 req/s] B2[P99 180ms] B3[错误率 0.1%] end A1 --|连接池100br/Redis缓存br/批量写入| B1 style A1 fill:#f96 style A3 fill:#f66 style B1 fill:#6f6 style B3 fill:#6f6我在实际压测中还踩过一个坑goose 默认用--no-reset-metrics模式如果压测中途手动重启了服务goose 不会感知错误率统计会乱。后来通过--reset-metrics每次 run 前清零计数配合 goose 的on_starthook 做健康检查数据才准确。压测还有一个容易被忽略的环节冷启动性能。很多工具第一次请求比稳态慢 2-5 倍因为连接池还没建立、缓存还没预热。我的做法是压测分两阶段先跑 30 秒预热忽略指标再跑 5 分钟正式统计。另外goose 的--hatch-rate不宜设太高从 50 用户/秒开始逐步加一口气 1000 并发会把还没预热完的服务冲垮。五、总结压测不是一上来就跑1000并发而是渐进式找瓶颈。我的实战经验渐进式压测10 → 100 → 500 → 1000每个阶段观察数据发现异常就停下来分析。不要一上来就极限测试。关注 P99P50 和 P99 的差距是判断瓶颈的关键。P5080ms 但 P991.2s说明少量请求极慢瓶颈就在那些慢请求上。goose 的场景化压测比 ab/wrk 更适合先登录再查询再退出这类真实用户流程。用 Rust 写场景代码对 Rust 开发者来说学习成本最低。瓶颈修复三板斧连接池不够→增大查询慢→加缓存写入慢→批量操作。这三招覆盖了 80% 的数据库瓶颈。作为的学习者压测让我对性能有了真实的感觉——以前只知道代码跑得快现在能说出1000并发下P99是180ms。这种感觉不是看教程能获得的必须自己跑一遍压测看一遍数据修一遍瓶颈。goose 的文档写得比较详细入门不难。但压测最大的坑是不知道怎么看数据——我的建议是重点看 P50/P90/P99 的差距和错误率分布这两个指标能帮你快速定位瓶颈在哪。平均响应时间有时候会掩盖真相因为大量快请求会把平均值拉低P99 才是最慢的用户体验的真实反映。