在现代大模型应用生产上线前压测Stress Testing Benchmarking是检验推理集群高可用性与吞吐极限的必经之路。然而许多团队在使用通用 HTTP 压测工具如 JMeter、wrk、locust对基于llama.cpp的推理服务进行压测时往往会遭遇严重的客户端性能瓶颈客户端自身算力枯竭高并发场景下Python 或 Java 编写的压测脚本在维持数千个并发流式长连接时其自身的垃圾回收GC和线程调度开销甚至超过了被测的大模型服务端TTFT 与 TPS 统计失真通用工具通常只统计全局响应时间无法以微秒级精度精准捕获流式传输中的关键 AI 指标——首字延迟TTFT, Time to First Token、Token 生成速率TPS, Tokens Per Second以及P99 抖动长尾无法模拟真实的自回归流式背压通用压测工具一旦收到数据就丢弃无法模拟移动端弱网、高延迟客户端或者突发断连对推理服务端 KV Cache 回收的压力。为了以最小的客户端物理资源榨干推理集群的极限算力我们必须利用 Rust 原生的高性能异步运行时 Tokio、强类型 HTTP 协议栈 Hyper手写一套工业级、轻量且具备纳秒级统计精度的高并发推理压测发生器Inference Load Generator。本文将直接从底层协议与异步流调度出发手写一套能够单机稳定压测数万并发、实时捕获 TTFT 与生成吞吐的现代化压测内核。压测客户端微架构核心拓扑为了在单台测试机上支撑数万并发连接而不爆内存压测器的微观调度流水线如下[ 虚拟客户端生成器 (Virtual Clients Generator) ] │ (按泊松分布或阶梯步进并发派发) ▼ [ Tokio 协程任务池 (10,000 ~ 50,000 异步并发 Tasks) ] │ │ ▼ (保持极轻量内存状态仅 4KB/Task) ▼ [ Hyper 零拷贝 HTTP/1.1 与 HTTP/2 连接池 ] ───────┘ │ ▼ (向目标 llama.cpp 推理网关发送 Prompt) [ 目标被测 llama.cpp 服务端集群 ] │ ▼ (SSE 流式逐字返回) [ 流式字节解码器 (Streaming SSE Parser) ] │ ├── 1. 捕获第一块数据包时间戳 ── 计算单次请求 TTFT ├── 2. 统计 Token 数量与耗时 ── 计算单次请求 TPS └── 3. 无锁原子指标累加器 ── 实时刷入 HdrHistogram 高精度直方图高精度流式指标度量衡HdrHistogram 纳秒级直方图在微基准压测中简单的“平均值Average”具有极强的欺骗性它会彻底掩盖由于系统 GC 或锁争用导致的严重长尾毛刺。我们必须使用支持动态高动态范围的HdrHistogramHigh Dynamic Range Histogram在纳秒级精度下记录从 P50、P90、P99 到 P999 的全貌分布。我们在 Rust 中使用原子通道将每个并发任务捕获的采样数据汇总至无锁直方图use hdrhistogram::Histogram; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Instant; /// 单个推理请求的微观性能画像 #[derive(Debug, Clone, Copy)] pub struct RequestMetricSample { pub ttft_micros: u64, // 首字延迟微秒 pub total_duration_micros: u64, // 端到端总延迟微秒 pub tokens_count: usize, // 实际生成的 Token 总量 } /// 全局压测聚合统计看板 pub struct GlobalMetricsCollector { pub total_requests: AtomicU64, pub successful_requests: AtomicU64, pub failed_requests: AtomicU64, pub total_tokens_generated: AtomicU64, } impl GlobalMetricsCollector { pub fn new() - Self { Self { total_requests: AtomicU64::new(0), successful_requests: AtomicU64::new(0), failed_requests: AtomicU64::new(0), total_tokens_generated: AtomicU64::new(0), } } }工业级实战万级并发流式压测客户端核心实现下面展示基于reqwest/hyper与 Tokio 构建的高并发流式压测内核use reqwest::Client; use std::sync::Arc; use tokio::sync::mpsc; use tokio::time::{sleep, Duration}; use futures_util::StreamExt; pub struct LoadTestConfig { pub target_url: String, pub concurrency: usize, pub duration_secs: u64, pub prompt: String, } pub struct InferenceLoadTester { config: LoadTestConfig, client: Client, metrics: ArcGlobalMetricsCollector, } impl InferenceLoadTester { pub fn new(config: LoadTestConfig) - Self { // 构建底层经过极致调优的 HTTP 客户端长连接复用与连接池最大化 let client Client::builder() .pool_max_idle_per_host(config.concurrency) .tcp_nodelay(true) // 彻底消除 Nagle 算法的 40ms 延迟 .timeout(Duration::from_secs(60)) .build() .expect(构建 HTTP 客户端失败); Self { config, client, metrics: Arc::new(GlobalMetricsCollector::new()), } } /// 启动全链路压测 pub async fn run_benchmark(self) { println!( 开始对目标服务 [{}] 进行极限压测并发数: {}, 持续时间: {} 秒, self.config.target_url, self.config.concurrency, self.config.duration_secs ); let (metric_tx, mut metric_rx) mpsc::channel::RequestMetricSample(100_000); let start_time Instant::now(); let is_running Arc::new(std::sync::atomic::AtomicBool::new(true)); // 1. 启动后台高精度直方图聚合协程 let metrics_aggregator tokio::spawn(async move { let mut ttft_hist Histogram::u64::new_with_bounds(1, 60_000_000, 3).unwrap(); let mut total_hist Histogram::u64::new_with_bounds(1, 120_000_000, 3).unwrap(); while let Some(sample) metric_rx.recv().await { let _ ttft_hist.record(sample.ttft_micros); let _ total_hist.record(sample.total_duration_micros); } (ttft_hist, total_hist) }); // 2. 并发拉起指定数量的工作协程 let mut workers Vec::with_capacity(self.config.concurrency); for worker_id in 0..self.config.concurrency { let client self.client.clone(); let target_url self.config.target_url.clone(); let prompt self.config.prompt.clone(); let tx metric_tx.clone(); let running is_running.clone(); let global_metrics self.metrics.clone(); workers.push(tokio::spawn(async move { while running.load(Ordering::Relaxed) { global_metrics.total_requests.fetch_add(1, Ordering::Relaxed); // 执行单个长连接流式请求并捕获 TTFT let req_start Instant::now(); let mut first_token_time None; let mut tokens_received 0; let body_json format!(r#{{prompt: {}, max_tokens: 128, stream: true}}#, prompt); let res client.post(target_url) .header(Content-Type, application/json) .body(body_json) .send() .await; match res { Ok(resp) if resp.status().is_success() { let mut stream resp.bytes_stream(); while let Some(chunk_res) stream.next().await { if let Ok(chunk) chunk_res { // 核心捕获首包到达的物理时间差 if first_token_time.is_none() { first_token_time Some(Instant::now()); } // 模拟解析 SSE 数据块并计数 Token if chunk.starts_with(bdata: ) { tokens_received 1; } } } let req_total_micros req_start.elapsed().as_micros() as u64; let ttft_micros first_token_time .map(|t| t.duration_since(req_start).as_micros() as u64) .unwrap_or(req_total_micros); global_metrics.successful_requests.fetch_add(1, Ordering::Relaxed); global_metrics.total_tokens_generated.fetch_add(tokens_received as u64, Ordering::Relaxed); let _ tx.try_send(RequestMetricSample { ttft_micros, total_duration_micros: req_total_micros, tokens_count: tokens_received, }); } _ { global_metrics.failed_requests.fetch_add(1, Ordering::Relaxed); } } // 避免微秒级无缝自旋打死客户端自身微小休眠 tokio::task::yield_now().await; } })); } // 3. 压测倒计时运行 sleep(Duration::from_secs(self.config.duration_secs)).await; is_running.store(false, Ordering::SeqCst); println!(⏱️ 压测周期结束正在等待残留并发任务优雅排空...); // 关闭通道并等待聚合器完成 drop(metric_tx); for w in workers { let _ w.await; } let (ttft_hist, total_hist) metrics_aggregator.await.unwrap(); let elapsed_secs start_time.elapsed().as_secs_f64(); // 4. 输出工业级压测报告 self.print_final_report(ttft_hist, total_hist, elapsed_secs); } fn print_final_report( self, ttft: Histogramu64, total: Histogramu64, elapsed: f64, ) { let total_req self.metrics.total_requests.load(Ordering::Relaxed); let succ_req self.metrics.successful_requests.load(Ordering::Relaxed); let failed_req self.metrics.failed_requests.load(Ordering::Relaxed); let total_tokens self.metrics.total_tokens_generated.load(Ordering::Relaxed); println!(\n LLM 推理性能压测报告 ); println!(压测总时长: {:.2} 秒, 目标并发: {}, elapsed, self.config.concurrency); println!(总请求数: {}, 成功: {}, 失败: {} (失败率: {:.2}%), total_req, succ_req, failed_req, (failed_req as f64 / total_req as f64) * 100.0); println!(系统全局生成吞吐: {:.2} Tokens/s, total_tokens as f64 / elapsed); println!(平均请求处理 QPS: {:.2} req/s, succ_req as f64 / elapsed); println!(---------------------------------------------------------------); println!(首字延迟 (TTFT) 分布:); println!( P50: {:.2} ms, ttft.value_at_quantile(0.50) as f64 / 1000.0); println!( P90: {:.2} ms, ttft.value_at_quantile(0.90) as f64 / 1000.0); println!( P99: {:.2} ms, ttft.value_at_quantile(0.99) as f64 / 1000.0); println!( P999: {:.2} ms, ttft.value_at_quantile(0.999) as f64 / 1000.0); println!(端到端总时延 (E2E Latency) 分布:); println!( P50: {:.2} ms, total.value_at_quantile(0.50) as f64 / 1000.0); println!( P99: {:.2} ms, total.value_at_quantile(0.99) as f64 / 1000.0); println!(); } }客户端压测调优清单突破系统级句柄与端口限制在压测机尝试发起万级并发连接之前必须完成操作系统内核参数调优# 1. 调大进程最大打开文件描述符上限 ulimit -n 1000000 # 2. 调大本地可用端口范围与快速 TIME_WAIT 回收 sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1 # 3. 调大 TCP 最大全连接与半连接队列 sysctl -w net.core.somaxconn65535 sysctl -w net.ipv4.tcp_max_syn_backlog65535结语高并发压测工具的编写是对系统级异步 I/O、内存开销与统计精度的一次全面大考。通过 Rust 赋予的轻量级协程与零抽象损耗网络协议栈我们仅用单台 8 核 16GB 的普通测试主机就能轻而易举地模拟出上万名真实用户同时在线高频流式交互的极端负载。只有用最严密的压测工具将生产集群推向物理极限我们才能在大规模流量洪峰真正降临之时拥有稳如磐石的系统底气。
阅读完成 · 觉得有帮助?