本文档对应Rust开发的高性能HTTP压测工具
cargo-whero的完整源码,按代码书写顺序逐行拆解,兼顾零基础入门与深度原理分析,严格区分语法知识点、业务逻辑、底层实现与安全规范。
一、文件整体说明
这是一个兼容cargo子命令规范的HTTP压测工具,核心能力是:
- 支持自定义并发数、请求总数、压测时长、QPS限流
- 全维度的请求耗时统计(DNS、TCP连接、TTFB等全阶段)
- 支持多种HTTP方法、自定义请求头、认证、代理、HTTPS等
- 输出汇总报告或CSV格式明细数据
- 基于Tokio异步 runtime 实现高并发,充分利用多核CPU性能
二、依赖导入模块(代码最开头的use部分)
逐行解释与设计说明
rust
use anyhow::{anyhow, Result};
- 语法知识点 :
use是Rust的导入语句,引入外部库的类型/宏/函数;{}批量导入同一个库的多个项 - 库作用 :
anyhow是Rust生态最常用的轻量化错误处理库,专门用于CLI工具/应用程序Result:anyhow封装的通用结果类型,自带错误上下文,不用手动定义几十种错误枚举anyhow!:宏,快速生成带自定义提示的错误对象,简化错误抛出
- 设计原因:压测工具是CLI程序,需要简洁的错误处理,不用为每个错误场景定义专属类型,提升开发效率,同时给用户友好的错误提示
rust
use clap::{Parser, ValueEnum};
- 库作用 :
clap是Rust生态标准的命令行参数解析库,几乎所有Rust CLI工具都用它Parser:派生宏,给结构体打上这个宏,就能自动生成命令行参数解析代码,不用手动拆分命令行字符串ValueEnum:派生宏,让自定义枚举可以作为命令行参数的可选值,自动处理用户输入的字符串到枚举的转换
- 设计原因 :手动解析命令行参数极易出错,还要处理参数校验、帮助文档生成等重复工作,
clap一站式解决,符合Rust生态最佳实践
rust
use reqwest::{ClientBuilder, Method};
- 库作用 :
reqwest是Rust生态最成熟、功能最全的异步HTTP客户端库,和Tokio异步 runtime 深度适配ClientBuilder:构建器模式,用来配置HTTP客户端的各种参数(超时、连接池、证书、代理等)Method:标准HTTP方法枚举(GET/POST/PUT等)
- 设计原因 :压测工具的核心是发送HTTP请求,
reqwest内置连接池、异步IO、全HTTP特性支持,不用自己手写TCP连接和HTTP协议解析,避免重复造轮子,同时保证稳定性和性能
rust
use std::collections::HashMap;
- 语法知识点 :
std是Rust标准库,无需额外安装;HashMap是键值对哈希表,无序的键值映射结构 - 业务用途:用来统计HTTP状态码分布、错误类型分布,比如key是状态码200,value是出现的次数
- 设计原因:O(1)的查找和插入性能,适合高并发场景下的统计计数
rust
use std::env;
- 作用:标准库的环境变量与命令行参数模块,用来获取程序启动时的命令行参数、编译时的环境变量
- 业务用途 :处理
cargo whero子命令的参数适配,获取Cargo注入的版本号环境变量
rust
use std::fmt::Write as FmtWrite;
- 语法知识点 :
as是Rust的类型别名,解决命名冲突,因为后面还有std::io::Write,两个Write trait重名了 - 作用 :字符串格式化写入的trait,用来高效拼接字符串,比如生成汇总报告的时候,避免频繁的
+字符串拼接导致的内存拷贝
rust
use std::fs;
- 作用:标准库的文件系统模块,用来读取请求体文件、写入压测报告/CSV文件
- 业务用途 :支持
--data-file从文件加载请求体,支持把结果写入指定文件
rust
use std::io::{self, BufWriter, Write};
- 作用 :标准库的输入输出模块
io:根模块,提供IO相关的通用类型/错误BufWriter:带缓冲区的写入器,批量写入数据,而不是每写一个字节就调用一次系统调用,极大提升文件/终端写入性能Write:写入trait,定义了写入数据的通用方法,所有可写入的类型(文件、终端、内存缓冲区)都实现了这个trait
- 设计原因:压测时每秒会产生大量统计数据,用缓冲写入能避免IO成为性能瓶颈,这是高性能IO的标准优化方案
rust
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
- 语法知识点 :原子类型,是CPU硬件支持的无锁线程安全类型,多线程同时读写不会出现数据竞争
AtomicBool:原子布尔值,用来做全局停止标志,控制所有worker退出AtomicU64:原子64位无符号整数,用来做请求计数、总耗时累加等高并发统计Ordering:内存序,控制原子操作的内存可见性和指令重排规则
- 设计原因:压测是高并发场景,多个worker线程同时更新统计数据,用原子类型是无锁操作,性能比加锁高几个数量级,完全避免锁竞争导致的性能损耗
- 安全规范 :这里用
Ordering::Relaxed最宽松的内存序,因为只是计数,不需要保证和其他操作的先后顺序,性能最高,是计数场景的最佳实践
rust
use std::sync::Arc;
- 语法知识点 :
Arc全称Atomic Reference Counting,原子引用计数智能指针 - 核心作用:让多个异步任务/线程可以安全的共享同一个数据的所有权,引用计数为0时自动释放内存,完全避免内存泄漏,没有GC开销
- 业务用途:包裹全局统计对象、停止标志、信号量,让所有worker任务都能访问到同一个实例
- Rust安全特性 :Rust的所有权规则禁止多个线程同时拥有同一个变量的所有权,
Arc就是解决这个问题的标准方案,原子操作保证引用计数的增减是线程安全的
rust
use std::time::{Duration, Instant};
- 作用 :标准库的时间处理模块
Duration:时间段,比如30秒、5分钟,用来表示超时、压测时长、请求耗时Instant:单调时钟,时间只会向前走,不会受系统时间修改(比如用户改了系统时间、时区同步)的影响
- 设计原因 :计时必须用
Instant,绝对不能用SystemTime(系统时间),因为系统时间可能回退,导致耗时计算出现负数,这是计时场景的铁律,保证统计数据的准确性
rust
use tokio::sync::{mpsc, Semaphore};
- 库作用 :
tokio是Rust生态事实标准的异步运行时,用来实现高并发异步IOmpsc:全称Multi-Producer Single-Consumer,多生产者单消费者通道,异步消息队列Semaphore:异步信号量,用来控制同时运行的任务数量
- 业务用途 :
mpsc:所有worker任务(生产者)把单个请求的统计数据发送到通道,专门的CSV写入任务(消费者)统一写入文件,避免多个线程同时写文件导致的锁竞争,性能提升极大Semaphore:压测的核心并发控制,初始许可数等于用户设置的并发数,每个worker发请求前必须先获取许可,保证同时最多有指定数量的请求在运行,精准控制并发
- 设计原因 :tokio的同步原语是异步友好的,
await的时候不会阻塞操作系统线程,只会让出CPU给其他任务,极大提升并发量,这是异步压测比同步多线程压测性能高的核心原因
三、常量定义
rust
const VERSION: &str = env!("CARGO_PKG_VERSION");
const USER_AGENT: &str = concat!("cargo-whero/", env!("CARGO_PKG_VERSION"));
逐行解释
const VERSION: &str = env!("CARGO_PKG_VERSION");- 语法知识点 :
const定义编译期常量,值在编译时就确定,运行时不可修改;&str是字符串切片,Rust的不可变字符串类型 - 宏作用 :
env!是编译时宏,编译时读取指定的环境变量,把值硬编码到二进制文件里 - 业务用途 :
CARGO_PKG_VERSION是Cargo自动注入的环境变量,值就是Cargo.toml里写的版本号,保证程序版本和包配置完全一致,不用手动修改代码里的版本号
- 语法知识点 :
const USER_AGENT: &str = concat!("cargo-whero/", env!("CARGO_PKG_VERSION"));- 宏作用 :
concat!是编译时宏,把多个字符串字面量拼接成一个,编译时完成,运行时无开销 - 业务用途 :HTTP请求的
User-Agent头,标识请求来自哪个工具,符合HTTP协议规范,服务器可以识别压测流量 - 设计原因:编译期拼接,运行时直接用固定字符串,没有运行时拼接的性能开销
- 宏作用 :
四、程序入口main函数
rust
fn main() {
let args: Vec<String> = env::args().collect();
// Check if called as "cargo whero" (args: [.../cargo-whero, whero, ...])
if args.len() >= 2 && args[1] == "whero" {
// Filter out the "whero" subcommand argument at index 1
let filtered: Vec<String> = args.iter()
.enumerate()
.filter(|(i, _)| *i != 1)
.map(|(_, s)| s.clone())
.collect();
if let Err(e) = run_from(filtered) {
eprintln!("Error: {e}");
std::process::exit(1);
}
return;
}
run_async();
}
逐行拆解与设计说明
核心设计目的
这个函数的核心是兼容cargo子命令规范 :Rust的cargo子命令要求二进制文件名为cargo-xxx,当用户执行cargo xxx时,cargo会自动调用这个二进制,并且把xxx作为第一个参数传入。如果不处理这个参数,clap解析会出错。
逐行解释
-
let args: Vec<String> = env::args().collect();- 收集程序启动时的所有命令行参数,转成字符串数组,比如执行
cargo whero http://example.com,args就是["/path/to/cargo-whero", "whero", "http://example.com"]
- 收集程序启动时的所有命令行参数,转成字符串数组,比如执行
-
if args.len() >= 2 && args[1] == "whero" {- 判断是不是被cargo调用的:如果第二个参数是
whero,说明是cargo whero的调用方式
- 判断是不是被cargo调用的:如果第二个参数是
-
参数过滤逻辑
rustlet filtered: Vec<String> = args.iter() .enumerate() .filter(|(i, _)| *i != 1) .map(|(_, s)| s.clone()) .collect();- 迭代器链式调用,Rust的函数式编程风格,零成本抽象,和手写循环性能一致
enumerate():给每个参数加上索引filter(|(i, _)| *i != 1):过滤掉索引为1的参数(也就是whero)map(|(_, s)| s.clone()):把剩下的参数克隆出来,转成新的数组collect():把迭代器转换成最终的Vec数组
-
错误处理
rustif let Err(e) = run_from(filtered) { eprintln!("Error: {e}"); std::process::exit(1); } return;if let Err(e):Rust的模式匹配,判断函数执行是否出错eprintln!:把错误信息打印到标准错误流(stderr),而不是标准输出(stdout),符合CLI工具规范,用户可以把正常输出重定向到文件,错误信息依然能在终端看到std::process::exit(1):退出程序,退出码1表示执行失败,符合Linux/Unix程序的规范
-
run_async();- 如果不是cargo调用的(比如直接执行
./cargo-whero),直接执行普通的启动逻辑
- 如果不是cargo调用的(比如直接执行
五、辅助启动函数
rust
fn run_async() {
if let Err(e) = run_from(env::args().collect()) {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
fn run_from(args: Vec<String>) -> Result<()> {
let cli = Cli::parse_from(args);
// Build runtime with specified CPU cores
let runtime = build_runtime(cli.cpus);
runtime.block_on(async_main(cli))
}
逐行解释
run_async函数- 纯转发函数,直接收集命令行参数传给
run_from,统一错误处理逻辑,避免代码重复
- 纯转发函数,直接收集命令行参数传给
run_from函数- 核心作用:解析命令行参数、构建Tokio运行时、启动异步主函数
let cli = Cli::parse_from(args);- 用clap的
parse_from方法,把传入的参数解析成Cli结构体实例,自动做参数校验,参数不合法会直接打印帮助信息并退出
- 用clap的
let runtime = build_runtime(cli.cpus);- 调用自定义的
build_runtime函数,构建Tokio多线程运行时,支持用户指定使用的CPU核心数
- 调用自定义的
runtime.block_on(async_main(cli))block_on是Tokio运行时的核心方法:同步等待异步任务执行完成,把异步世界和同步的main函数桥接起来- Rust的异步函数必须在Tokio运行时内才能执行,
block_on就是启动运行时并执行异步任务的入口 - 把解析好的cli参数传给异步主函数
async_main
六、CLI参数结构体Cli定义
rust
#[derive(Parser, Debug)]
#[command(name = "whero")]
#[command(author = "Rust Hey")]
#[command(version = VERSION)]
#[command(about = "A high-performance HTTP load testing tool", long_about = None)]
struct Cli {
/// Target URL (required)
#[arg(value_name = "URL")]
url: String,
/// Number of requests to run
#[arg(short, long, default_value = "200")]
n: u64,
/// Number of concurrent workers
#[arg(short, long, default_value = "50")]
c: u64,
/// Rate limit, in queries per second (QPS) per worker
#[arg(short, long, default_value = "0")]
q: f64,
/// Duration of application to send requests (e.g., 10s, 3m)
#[arg(short, long)]
z: Option<String>,
/// Output type: default (summary) or csv
#[arg(short, long, value_enum, default_value = "default")]
o: OutputType,
/// Output file path (for csv output, writes to file instead of stdout)
#[arg(short = 'f', long)]
output_file: Option<String>,
/// HTTP method
#[arg(short, long, value_enum, default_value = "GET")]
m: HttpMethod,
/// Custom HTTP header (can be used multiple times)
#[arg(short = 'H', long, value_name = "KEY:VALUE")]
headers: Vec<String>,
/// Timeout for each request (e.g., 10s, 5m, 0 for no timeout)
#[arg(short, long, default_value = "30s")]
t: Option<String>,
/// HTTP Accept header
#[arg(long)]
accept: Option<String>,
/// HTTP request body
#[arg(short = 'd', long)]
data: Option<String>,
/// HTTP request body from file
#[arg(long)]
data_file: Option<String>,
/// Content-Type header
#[arg(short = 'T', long, default_value = "text/html")]
content_type: String,
/// User-Agent header
#[arg(short = 'U', long)]
user_agent: Option<String>,
/// Basic authentication (username:password)
#[arg(short, long)]
a: Option<String>,
/// HTTP Proxy address (host:port)
#[arg(short = 'x', long)]
proxy: Option<String>,
/// HTTP Host header
#[arg(long)]
host: Option<String>,
/// Disable compression
#[arg(long)]
disable_compression: bool,
/// Disable keep-alive
#[arg(long)]
disable_keepalive: bool,
/// Disable following redirects
#[arg(long)]
disable_redirects: bool,
/// Enable HTTP/2
#[arg(long)]
h2: bool,
/// Number of CPU cores to use (default: all available cores)
#[arg(long)]
cpus: Option<usize>,
}
核心设计说明
这是整个工具的参数配置中心,用clap的派生宏自动生成参数解析、帮助文档、参数校验,完全符合CLI工具的设计规范。
关键语法与属性解释
- 派生宏 :
#[derive(Parser, Debug)]Parser:告诉clap这个结构体是命令行参数的定义,自动生成解析代码Debug:自动生成调试打印的实现,方便开发时打印结构体内容
- 命令属性 :
#[command(...)]- 定义命令的基本信息,用户执行
--help时会显示这些内容 name:命令名author:作者信息version:版本号,用前面定义的常量about:命令的简短描述
- 定义命令的基本信息,用户执行
- 参数属性 :
#[arg(...)]- 给每个字段配置命令行参数的规则,核心属性:
short:短参数,比如short = 'n'对应-nlong:长参数,比如long对应--n,默认用字段名default_value:默认值,用户不输入时用这个值value_enum:表示这个字段的类型是实现了ValueEnum的枚举,clap会自动处理字符串到枚举的转换value_name:帮助文档里显示的参数值名称short = 'f':自定义短参数,比如字段名是output_file,短参数用-f
- 给每个字段配置命令行参数的规则,核心属性:
- 字段类型设计
- 必选参数:用
String/u64等非Option类型,用户不输入会报错,比如url是必填的目标地址 - 可选参数:用
Option<T>类型,用户不输入就是None,不会报错 - 可重复参数:用
Vec<T>类型,比如headers,用户可以多次输入-H,所有值都会被收集到数组里 - 开关参数:用
bool类型,用户输入--disable-compression就为true,不输入为false,不需要传值
- 必选参数:用
每个参数的业务用途
| 字段 | 业务作用 |
|---|---|
| url | 压测的目标URL,必填 |
| n | 总请求数,默认200 |
| c | 并发worker数,默认50,核心压测参数 |
| q | 每个worker的QPS限制,默认0不限制,总QPS = c * q |
| z | 压测时长,比如10s、3m,和n二选一,设置了z就会忽略n |
| o | 输出类型,默认汇总报告,可选CSV明细 |
| output_file | 输出文件路径,把结果写入文件而不是终端 |
| m | HTTP请求方法,默认GET |
| headers | 自定义请求头,可多次使用 |
| t | 单个请求的超时时间,默认30s |
| accept | HTTP Accept头,指定接受的响应类型 |
| data | HTTP请求体,比如POST的JSON数据 |
| data_file | 从文件读取请求体,适合大请求体 |
| content_type | Content-Type头,默认text/html |
| user_agent | 自定义User-Agent头 |
| a | 基础认证,格式username:password |
| proxy | HTTP代理地址,host:port |
| host | 自定义Host头 |
| disable_compression | 关闭响应压缩,默认开启 |
| disable_keepalive | 关闭TCP长连接,默认开启长连接复用 |
| disable_redirects | 关闭自动跟随3xx重定向,默认开启 |
| h2 | 启用HTTP/2协议,默认用HTTP/1.1 |
| cpus | 指定使用的CPU核心数,默认用所有核心 |
七、枚举类型定义
1. OutputType 输出类型枚举
rust
#[derive(Debug, Clone, ValueEnum)]
enum OutputType {
Default,
Csv,
}
- 派生宏 :
ValueEnum让clap可以把用户输入的字符串(default/csv)自动转成对应的枚举变体 - 业务作用 :控制输出格式,
Default是人类可读的汇总报告,Csv是机器可读的明细数据,方便导入Excel/数据分析工具 - 设计原因:用枚举而不是字符串,避免拼写错误导致的bug,Rust编译器会强制检查所有变体的处理,不会遗漏分支
2. HttpMethod HTTP方法枚举
rust
#[derive(Debug, Clone, ValueEnum)]
#[clap(rename_all = "uppercase")]
#[allow(clippy::upper_case_acronyms)]
enum HttpMethod {
GET,
POST,
PUT,
DELETE,
HEAD,
OPTIONS,
PATCH,
}
- 属性解释 :
#[clap(rename_all = "uppercase")]:告诉clap把用户输入的字符串转成全大写,比如用户输入get/Get都会转成GET,匹配枚举变体#[allow(clippy::upper_case_acronyms)]:关闭clippy的命名规范警告,因为HTTP方法都是全大写,符合行业习惯
- 业务作用 :覆盖所有常用的HTTP方法,用户可以通过
-m参数指定 - 设计原因:解耦CLI参数和reqwest库的类型,自己定义枚举,不直接暴露第三方库的类型给用户,代码更健壮,后续换HTTP库也不用改CLI参数的逻辑
3. From trait 实现(类型转换)
rust
impl From<HttpMethod> for Method {
fn from(m: HttpMethod) -> Self {
match m {
HttpMethod::GET => Method::GET,
HttpMethod::POST => Method::POST,
HttpMethod::PUT => Method::PUT,
HttpMethod::DELETE => Method::DELETE,
HttpMethod::HEAD => Method::HEAD,
HttpMethod::OPTIONS => Method::OPTIONS,
HttpMethod::PATCH => Method::PATCH,
}
}
}
- 语法知识点 :
From是Rust标准库的类型转换trait,实现了From<A> for B,就可以用B::from(a)或者a.into()把A类型转成B类型 - 业务作用 :把自定义的
HttpMethod枚举转成reqwest库的Method类型,这样就可以直接用在reqwest的请求构建里 - 设计原因:这是Rust的最佳实践,通过类型转换解耦自己的业务代码和第三方库,避免第三方库的类型泄露到业务代码的各个地方,提升代码的可维护性
八、核心数据结构定义
1. RequestStats 单请求统计结构体
rust
#[derive(Debug, Clone)]
struct RequestStats {
duration: Duration,
status: u16,
content_length: i64,
error: Option<String>,
dns_duration: Option<Duration>,
conn_duration: Option<Duration>,
req_duration: Option<Duration>,
res_duration: Option<Duration>,
delay_duration: Option<Duration>,
offset: Duration,
}
设计目的
记录单个HTTP请求的全维度统计数据,是压测统计的最小单元,每个请求都会生成一个这个结构体的实例。
逐字段解释
| 字段 | 类型 | 业务含义 | 设计说明 |
|---|---|---|---|
| duration | Duration | 整个请求的总耗时(从发送到响应完全接收) | 压测的核心指标 |
| status | u16 | HTTP响应状态码 | 比如200、404、500,请求失败为0 |
| content_length | i64 | 响应体的字节长度 | 用来统计流量 |
| error | Option | 请求错误信息 | 成功为None,失败为Some(错误信息),用Option避免空指针 |
| dns_duration | Option | DNS域名解析耗时 | 失败为None,定位DNS性能瓶颈 |
| conn_duration | Option | TCP连接建立耗时 | 失败为None,定位网络连接瓶颈 |
| req_duration | Option | HTTP请求写入耗时 | 失败为None,定位请求发送性能 |
| res_duration | Option | 响应体读取耗时 | 失败为None,定位响应接收性能 |
| delay_duration | Option | TTFB首字节等待耗时(从请求发完到收到第一个响应字节) | 核心指标,直接反映服务器的处理性能 |
| offset | Duration | 这个请求相对于压测开始时间的偏移量 | 用来做时序分析,看压测过程中耗时的变化趋势 |
Rust安全设计
所有可能不存在的字段(请求失败时就没有的耗时)都用Option<T>包裹,绝对不会出现空指针,Rust编译器会强制你处理Some和None两种情况,不会出现空指针异常,这是Rust内存安全的核心特性之一。
2. AggregatedStats 汇总统计结构体
rust
#[derive(Debug)]
struct AggregatedStats {
total_requests: AtomicU64,
success_requests: AtomicU64,
error_requests: AtomicU64,
total_duration: AtomicU64, // nanoseconds
min_duration: AtomicU64, // nanoseconds
max_duration: AtomicU64, // nanoseconds
// For percentile calculation
durations: parking_lot::Mutex<Vec<u64>>,
// Status code distribution
status_codes: parking_lot::Mutex<HashMap<u16, u64>>,
// Error distribution
errors: parking_lot::Mutex<HashMap<String, u64>>,
// Size statistics
total_bytes: AtomicU64,
// Detailed stage statistics (all in nanoseconds)
// DNS lookup
dns_durations: parking_lot::Mutex<Vec<u64>>,
// Connection (DNS + dial)
conn_durations: parking_lot::Mutex<Vec<u64>>,
// Request write
req_durations: parking_lot::Mutex<Vec<u64>>,
// Response delay (wait for first byte)
delay_durations: parking_lot::Mutex<Vec<u64>>,
// Response read
res_durations: parking_lot::Mutex<Vec<u64>>,
}
设计目的
汇总所有请求的统计数据,是压测报告的核心数据源,所有worker都会把自己的请求数据记录到这个结构体里。
核心设计思路
- 高频更新的计数用原子类型:无锁、高并发、性能极高,适合总请求数、总耗时这种只需要累加的场景
- 需要批量写入/排序的集合用Mutex包裹:比如所有请求的耗时列表,需要后续排序计算百分位数,用互斥锁保证多线程写入的安全性
- 选用parking_lot::Mutex而不是标准库Mutex :
- 性能更高,锁的开销更小,适合高并发写入场景
- 不会出现标准库Mutex的poisoned问题(持有锁的线程panic后,锁无法再被获取),程序更健壮
- 锁的API更简洁,使用更方便
逐字段解释
| 字段 | 类型 | 业务作用 |
|---|---|---|
| total_requests | AtomicU64 | 总请求数,所有请求(成功+失败)的总数 |
| success_requests | AtomicU64 | 成功请求数(无错误,有正常响应) |
| error_requests | AtomicU64 | 失败请求数 |
| total_duration | AtomicU64 | 所有成功请求的总耗时(纳秒),用来计算平均耗时 |
| min_duration | AtomicU64 | 所有成功请求的最小耗时(纳秒) |
| max_duration | AtomicU64 | 所有成功请求的最大耗时(纳秒) |
| durations | Mutex<Vec> | 所有成功请求的耗时列表,用来排序计算百分位数(P50/P90/P99等) |
| status_codes | Mutex<HashMap<u16, u64>> | 状态码分布,key是状态码,value是出现次数 |
| errors | Mutex<HashMap<String, u64>> | 错误类型分布,key是错误信息,value是出现次数 |
| total_bytes | AtomicU64 | 所有成功请求的总传输字节数 |
| dns_durations | Mutex<Vec> | 所有DNS解析耗时列表,用来统计DNS阶段的性能 |
| conn_durations | Mutex<Vec> | 所有TCP连接耗时列表 |
| req_durations | Mutex<Vec> | 所有请求写入耗时列表 |
| delay_durations | Mutex<Vec> | 所有TTFB首字节耗时列表 |
| res_durations | Mutex<Vec> | 所有响应读取耗时列表 |
3. AggregatedStats的方法实现
(1)new 构造方法
rust
impl AggregatedStats {
fn new() -> Self {
Self {
total_requests: AtomicU64::new(0),
success_requests: AtomicU64::new(0),
error_requests: AtomicU64::new(0),
total_duration: AtomicU64::new(0),
min_duration: AtomicU64::new(u64::MAX),
max_duration: AtomicU64::new(0),
durations: parking_lot::Mutex::new(Vec::with_capacity(1_000_000)),
status_codes: parking_lot::Mutex::new(HashMap::new()),
errors: parking_lot::Mutex::new(HashMap::new()),
total_bytes: AtomicU64::new(0),
dns_durations: parking_lot::Mutex::new(Vec::with_capacity(1_000_000)),
conn_durations: parking_lot::Mutex::new(Vec::with_capacity(1_000_000)),
req_durations: parking_lot::Mutex::new(Vec::with_capacity(1_000_000)),
delay_durations: parking_lot::Mutex::new(Vec::with_capacity(1_000_000)),
res_durations: parking_lot::Mutex::new(Vec::with_capacity(1_000_000)),
}
}
逐行解释与设计优化
- 构造方法 :
fn new() -> Self是Rust的惯用构造函数写法,返回结构体的实例 - 原子类型初始化 :所有计数都初始化为0,
min_duration初始化为u64::MAX(u64的最大值),这样第一个请求的耗时一定会比它小,正确更新最小值 - 预分配内存优化 :所有Vec都用
Vec::with_capacity(1_000_000)预分配了100万条的内存空间- 为什么这么做?:Rust的Vec是动态数组,当元素超过当前容量时,会自动扩容(申请2倍的新内存,把旧数据拷贝过去,释放旧内存),频繁扩容会带来大量的内存拷贝和系统调用,严重影响性能
- 边界处理:最多预分配100万条,避免压测几亿请求时内存溢出,100万条数据足够计算出非常准确的百分位数,不会影响统计精度
(2)record 核心记录方法
rust
fn record(&self, stat: &RequestStats) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
if stat.error.is_none() {
self.success_requests.fetch_add(1, Ordering::Relaxed);
let duration_ns = stat.duration.as_nanos() as u64;
// Update min/max
loop {
let current_min = self.min_duration.load(Ordering::Relaxed);
if duration_ns >= current_min ||
self.min_duration.compare_exchange(current_min, duration_ns, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
break;
}
}
loop {
let current_max = self.max_duration.load(Ordering::Relaxed);
if duration_ns <= current_max ||
self.max_duration.compare_exchange(current_max, duration_ns, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
break;
}
}
self.total_duration.fetch_add(duration_ns, Ordering::Relaxed);
if stat.content_length > 0 {
self.total_bytes.fetch_add(stat.content_length as u64, Ordering::Relaxed);
}
// Store duration for percentile calculation
if self.durations.lock().len() < 1_000_000 {
self.durations.lock().push(duration_ns);
}
// Status code distribution
*self.status_codes.lock().entry(stat.status).or_insert(0) += 1;
// Store detailed stage durations
if let Some(dns) = stat.dns_duration {
let dns_ns = dns.as_nanos() as u64;
if self.dns_durations.lock().len() < 1_000_000 {
self.dns_durations.lock().push(dns_ns);
}
}
if let Some(conn) = stat.conn_duration {
let conn_ns = conn.as_nanos() as u64;
if self.conn_durations.lock().len() < 1_000_000 {
self.conn_durations.lock().push(conn_ns);
}
}
if let Some(req) = stat.req_duration {
let req_ns = req.as_nanos() as u64;
if self.req_durations.lock().len() < 1_000_000 {
self.req_durations.lock().push(req_ns);
}
}
if let Some(delay) = stat.delay_duration {
let delay_ns = delay.as_nanos() as u64;
if self.delay_durations.lock().len() < 1_000_000 {
self.delay_durations.lock().push(delay_ns);
}
}
if let Some(res) = stat.res_duration {
let res_ns = res.as_nanos() as u64;
if self.res_durations.lock().len() < 1_000_000 {
self.res_durations.lock().push(res_ns);
}
}
} else {
self.error_requests.fetch_add(1, Ordering::Relaxed);
if let Some(ref err) = stat.error {
*self.errors.lock().entry(err.clone()).or_insert(0) += 1;
}
}
}
}
核心设计目的
把单个请求的RequestStats数据,安全的记录到全局汇总统计里,是高并发场景下的核心写入方法,兼顾性能和线程安全。
逐段拆解与重点分析
-
总请求数累加
rustself.total_requests.fetch_add(1, Ordering::Relaxed);fetch_add:原子类型的加法方法,原子的把值加1,多线程同时调用也不会出现数据竞争Ordering::Relaxed:最宽松的内存序,只保证这个操作本身是原子的,不保证和其他操作的顺序,性能最高,适合纯计数场景,因为计数不需要和其他操作有先后顺序的依赖
-
成功请求的处理逻辑
rustif stat.error.is_none() {- 判断请求是否成功,没有错误就进入成功请求的统计逻辑
-
最小/最大耗时的无锁原子更新(重点难点)
rustloop { let current_min = self.min_duration.load(Ordering::Relaxed); if duration_ns >= current_min || self.min_duration.compare_exchange(current_min, duration_ns, Ordering::Relaxed, Ordering::Relaxed).is_ok() { break; } }- 核心问题 :多线程同时更新最小值,不能直接
if 新值 < 当前最小值 { 当前最小值 = 新值 },因为两个线程同时读到当前最小值,都判断自己的新值更小,然后同时写入,会出现数据丢失,导致最小值统计错误 - 解决方案 :用
compare_exchange(比较并交换)的循环重试模式,这是无锁编程的经典范式- 第一步:读取当前的最小值
current_min - 第二步:如果当前请求的耗时
duration_ns大于等于current_min,不需要更新,直接退出循环 - 第三步:否则,调用
compare_exchange,原子的检查当前值是不是还是current_min,如果是,就更新成duration_ns,返回成功;如果不是(说明被其他线程改了),返回失败,循环重试
- 第一步:读取当前的最小值
- 设计原因:完全无锁,性能比加锁高很多,同时保证了多线程场景下最小值更新的正确性,不会出现数据竞争
- 最大值的更新逻辑和最小值完全一致,只是判断条件反过来
- 核心问题 :多线程同时更新最小值,不能直接
-
总耗时、总字节数累加
rustself.total_duration.fetch_add(duration_ns, Ordering::Relaxed); if stat.content_length > 0 { self.total_bytes.fetch_add(stat.content_length as u64, Ordering::Relaxed); }- 用原子加法累加总耗时和总传输字节数,无锁高性能
-
耗时数据存储(百分位数计算用)
rustif self.durations.lock().len() < 1_000_000 { self.durations.lock().push(duration_ns); }self.durations.lock():获取Mutex的锁,拿到Vec的可变引用,只有拿到锁的线程才能写入,保证线程安全- 长度判断:最多存100万条,避免内存溢出,边界处理,防止OOM
- 各个阶段的耗时存储逻辑完全一致,都有长度限制
-
状态码分布统计
rust*self.status_codes.lock().entry(stat.status).or_insert(0) += 1;- HashMap的entry API:Rust的HashMap最优雅的写法,一行代码完成"不存在就插入默认值0,然后值加1"的逻辑
- 等价于:先判断状态码是否存在,不存在就插入0,然后把对应的值加1,但是entry API是原子的,不会出现竞态,而且写法更简洁
- 因为有Mutex包裹,所以多线程写入是安全的
-
失败请求的处理逻辑
rust} else { self.error_requests.fetch_add(1, Ordering::Relaxed); if let Some(ref err) = stat.error { *self.errors.lock().entry(err.clone()).or_insert(0) += 1; } }- 失败请求数原子加1
- 用entry API统计错误类型的分布,记录每个错误出现的次数
九、工具函数实现
1. parse_duration 时长解析函数
rust
fn parse_duration(s: &str) -> Result<Duration> {
let s = s.trim();
if let Some(n) = s.strip_suffix('s') {
let v: f64 = n.parse()?;
return Ok(Duration::from_secs_f64(v));
}
if let Some(n) = s.strip_suffix('m') {
let v: f64 = n.parse()?;
return Ok(Duration::from_secs_f64(v * 60.0));
}
if let Some(n) = s.strip_suffix('h') {
let v: f64 = n.parse()?;
return Ok(Duration::from_secs_f64(v * 3600.0));
}
// Try parsing as seconds directly
let v: f64 = s.parse()?;
Ok(Duration::from_secs_f64(v))
}
设计目的
把用户输入的时长字符串(比如10s、3m、2h、1.5)解析成标准库的Duration类型,给压测时长、超时时间等参数使用。
逐行解释
let s = s.trim();:去掉字符串首尾的空格,兼容用户输入的多余空格,比如10s也能正常解析- 后缀匹配逻辑 :
strip_suffix('s'):判断字符串是不是以s结尾,如果是,去掉后缀,把前面的部分解析成f64,转成秒数的Durationm结尾:转成分钟,乘以60得到秒数h结尾:转成小时,乘以3600得到秒数
- 兜底逻辑:没有匹配到后缀,直接把整个字符串解析成f64,当成秒数处理
- 错误处理 :用
?操作符,解析失败的话直接把错误向上返回,调用方会拿到友好的错误提示 - 设计亮点 :用f64解析,支持小数,比如
1.5s、0.5m都能正常解析,非常灵活,用户体验好
2. build_runtime Tokio运行时构建函数
rust
fn build_runtime(cpus: Option<usize>) -> tokio::runtime::Runtime {
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_all();
if let Some(n) = cpus {
builder.worker_threads(n);
}
builder.build().expect("Failed to build Tokio runtime")
}
设计目的
构建Tokio多线程异步运行时,支持用户指定使用的CPU核心数,是异步程序的核心引擎。
逐行解释
-
let mut builder = tokio::runtime::Builder::new_multi_thread();- 创建Tokio多线程运行时的构建器,多线程运行时会把异步任务调度到多个操作系统线程上,充分利用多核CPU性能,适合压测这种CPU密集+IO密集的场景
-
builder.enable_all();- 启用Tokio的所有功能:IO驱动、时间驱动、信号处理等,不然运行时不支持定时器、文件IO等功能
-
CPU核心数配置
rustif let Some(n) = cpus { builder.worker_threads(n); }- 如果用户指定了
--cpus参数,就设置worker线程数为指定的数量,默认是等于CPU的逻辑核心数,充分利用硬件性能
- 如果用户指定了
-
builder.build().expect("Failed to build Tokio runtime")- 构建运行时,
expect是如果构建失败,直接panic,因为运行时构建失败,程序根本无法运行,直接panic是合理的,没有必要继续执行
- 构建运行时,
十、核心异步主函数 async_main
这是整个压测工具的业务逻辑核心,包含了参数校验、客户端构建、worker启动、压测控制、结果输出的全流程。
rust
async fn async_main(cli: Cli) -> Result<()> {
- 语法知识点 :
async fn定义异步函数,返回值是Result<()>,执行成功返回Ok(()),失败返回错误 - 异步函数必须在Tokio运行时内执行,前面的
runtime.block_on就是用来执行这个异步函数的
第一部分:参数合法性校验
rust
// Validation
if cli.c == 0 {
return Err(anyhow!("-c cannot be 0"));
}
let duration = if let Some(ref z) = cli.z {
Some(parse_duration(z)?)
} else {
None
};
if duration.is_none() && cli.n == 0 {
return Err(anyhow!("-n cannot be 0"));
}
if duration.is_none() && cli.n < cli.c {
return Err(anyhow!("-n cannot be less than -c"));
}
设计目的
提前校验用户输入的参数是否合法,避免程序运行时出现逻辑错误或者panic,是CLI工具的必备安全规范。
逐行解释
- 并发数校验 :
if cli.c == 0,并发数不能为0,不然没有worker发请求,压测无法进行,直接返回错误 - 压测时长解析 :如果用户设置了
-z参数,调用parse_duration解析成Duration类型,解析失败直接返回错误 - 请求数校验 :如果没有设置压测时长,总请求数
-n不能为0,不然不知道要发多少请求 - 请求数与并发数校验:如果没有设置压测时长,总请求数不能小于并发数,不然每个worker连一个请求都分不到,逻辑错误
- 设计亮点:所有校验都在压测开始前执行,提前给用户友好的错误提示,不会让程序跑起来之后才出错,用户体验好
第二部分:请求体构建
rust
// Build request body
let body: Option<Vec<u8>> = if let Some(ref d) = cli.data {
Some(d.clone().into_bytes())
} else if let Some(ref f) = cli.data_file {
Some(fs::read(f).map_err(|e| anyhow!("Failed to read file {}: {}", f, e))?)
} else {
None
};
设计目的
构建HTTP请求的请求体,支持直接输入字符串或者从文件读取,适配POST/PUT等需要请求体的HTTP方法。
逐行解释
- 优先级:
-d参数 >--data-file参数 > 无请求体 - 直接输入请求体:如果有
-d参数,把字符串转成字节数组Vec<u8>,作为请求体 - 从文件读取请求体:如果有
--data-file参数,调用fs::read读取文件的全部内容,转成字节数组- 错误处理:用
map_err把文件读取的错误包装成带上下文的友好错误,告诉用户哪个文件读失败了,为什么,用户体验好
- 错误处理:用
- 都没有的话,请求体为
None,也就是没有请求体
- 设计原因 :请求体用
Vec<u8>字节数组,而不是字符串,因为请求体可以是二进制数据(比如图片、文件),兼容性更强
第三部分:HTTP请求头构建
rust
// Parse headers
let mut headers = vec![
("Content-Type".to_string(), cli.content_type),
];
if let Some(ref accept) = cli.accept {
headers.push(("Accept".to_string(), accept.clone()));
}
for h in &cli.headers {
if let Some((k, v)) = h.split_once(':') {
headers.push((k.trim().to_string(), v.trim().to_string()));
}
}
// Parse auth
let (username, password) = if let Some(ref a) = cli.a {
if let Some((u, p)) = a.split_once(':') {
(Some(u.to_string()), Some(p.to_string()))
} else {
return Err(anyhow!("Invalid auth format, expected username:password"));
}
} else {
(None, None)
};
// User-Agent
let ua = if let Some(ref u) = cli.user_agent {
format!("{} {}", u, USER_AGENT)
} else {
USER_AGENT.to_string()
};
headers.push(("User-Agent".to_string(), ua));
设计目的
构建所有HTTP请求的默认请求头,包括用户自定义的头、认证、UA等。
重点细节与设计亮点
-
Content-Type默认值 :先把Content-Type加入头列表,用用户指定的
-T参数,默认是text/html -
Accept头处理 :如果用户设置了
--accept,加入Accept头 -
自定义请求头解析(重点细节)
rustfor h in &cli.headers { if let Some((k, v)) = h.split_once(':') { headers.push((k.trim().to_string(), v.trim().to_string())); } }- 用
split_once(':')分割key和value,只分割第一个冒号,避免value里有冒号的时候解析错误(比如Cookie头、Authorization头里经常有冒号) - 用
trim()去掉key和value首尾的空格,兼容用户输入的-H "Content-Type: application/json"这种带空格的格式 - 这是非常严谨的细节处理,很多新手会用
split(':')分割所有冒号,导致value里的冒号破坏解析,这里的写法完全避免了这个问题
- 用
-
基础认证解析 :把
-a参数分割成username和password,格式必须是username:password,分割失败直接返回友好的错误提示 -
User-Agent处理 :如果用户指定了
-U,把用户的UA和默认的UA拼接起来,不然用默认的UA,保证每个请求都有合法的UA,符合HTTP规范
第四部分:reqwest HTTP客户端构建
rust
// Build client
let mut client_builder = ClientBuilder::new()
.danger_accept_invalid_certs(true)
.tcp_keepalive(if cli.disable_keepalive { None } else { Some(Duration::from_secs(30)) })
.tcp_nodelay(true)
.pool_max_idle_per_host(cli.c as usize);
if cli.h2 {
client_builder = client_builder.http2_adaptive_window(true);
}
if cli.disable_compression {
client_builder = client_builder.no_gzip();
}
if cli.disable_redirects {
client_builder = client_builder.redirect(reqwest::redirect::Policy::none());
}
if let Some(ref timeout_str) = cli.t {
let timeout = parse_duration(timeout_str)?;
if timeout != Duration::ZERO {
client_builder = client_builder.timeout(timeout);
}
}
if let Some(ref proxy) = cli.proxy {
let proxy = reqwest::Proxy::http(proxy)?;
client_builder = client_builder.proxy(proxy);
}
let client = client_builder.build()?;
设计目的
构建全局的reqwest HTTP客户端,配置所有HTTP相关的参数,是压测工具的核心组件,所有worker都会克隆这个客户端,共享同一个连接池。
逐行配置解释
- 基础配置
.danger_accept_invalid_certs(true):跳过SSL证书验证,压测工具经常会压测自签名证书的内部服务,不跳过的话会报错,这是压测工具的标准配置.tcp_keepalive(...):设置TCP保活时间,如果没有禁用keepalive,就设置30秒的保活,开启TCP长连接复用,避免频繁建立TCP连接带来的性能损耗,压测场景下长连接能极大提升性能.tcp_nodelay(true):禁用Nagle算法,Nagle算法会攒小包一起发送,减少网络包数量,但会增加延迟,压测场景需要低延迟,所以禁用,这是高性能网络编程的标准优化.pool_max_idle_per_host(cli.c as usize):设置每个主机的最大空闲连接数,等于用户设置的并发数c,保证每个worker都有一个空闲连接可用,不用频繁新建连接,最大化连接池复用率
- HTTP/2配置 :如果启用了
--h2,开启HTTP/2的自适应流量控制窗口,优化HTTP/2的传输性能 - 压缩配置 :如果禁用了压缩,设置
no_gzip(),不接受gzip压缩的响应 - 重定向配置 :如果禁用了重定向,设置重定向策略为
none,不会自动跟随3xx重定向 - 超时配置 :解析
-t超时参数,如果不是0,设置每个请求的超时时间,避免请求卡死 - 代理配置 :如果设置了
-x代理,构建HTTP代理,加入客户端配置 - 客户端构建 :
client_builder.build()?,构建客户端实例,失败的话返回错误
- 设计核心亮点 :reqwest的Client是轻量克隆的,克隆Client只会复制一个指针,共享同一个连接池,所以我们只需要构建一个全局Client,然后每个worker克隆一份,就能共享连接池,极大提升性能,这是reqwest的最佳实践
第五部分:全局共享对象初始化
rust
let stats = Arc::new(AggregatedStats::new());
let stop_flag = Arc::new(AtomicBool::new(false));
let start_time = Instant::now();
逐行解释
let stats = Arc::new(AggregatedStats::new());- 把汇总统计结构体用Arc包裹,让所有worker任务都能安全的共享同一个统计实例,多个worker可以同时调用record方法记录数据
let stop_flag = Arc::new(AtomicBool::new(false));- 全局停止标志,用Arc包裹,初始值为false,当压测时长到了、用户按了Ctrl+C,就会把它设为true,所有worker看到true就会退出循环,停止压测
let start_time = Instant::now();- 记录压测开始的时间,用来计算总压测时长、每个请求的时间偏移量,用单调时钟Instant,保证计时准确
第六部分:压测任务分配与QPS配置
rust
// Calculate work distribution
let requests_per_worker = if duration.is_some() {
None // Will run until duration ends
} else {
Some(cli.n.div_ceil(cli.c)) // Distribute requests among workers
};
// QPS throttle interval
let qps_interval = if cli.q > 0.0 {
Some(Duration::from_secs_f64(1.0 / cli.q))
} else {
None
};
println!("\nRunning {} requests with {} concurrency",
if let Some(n) = requests_per_worker {
n * cli.c
} else { cli.n },
cli.c);
println!("Target: {}\n", cli.url);
逐行解释
- worker请求数分配
- 如果设置了压测时长,就没有固定请求数,worker会一直跑到stop_flag为true
- 否则,用
cli.n.div_ceil(cli.c)向上取整,把总请求数平均分给每个worker,比如总请求数201,并发50,每个worker分5个请求,总请求数250,保证总请求数至少是用户设置的n,不会少
- QPS限流配置
- 如果设置了
-q参数(每个worker的QPS),计算每个请求的间隔时间,公式是1.0 / q秒,比如q=10,每个请求间隔0.1秒,保证每个worker的QPS稳定在设定值 - 总QPS = 并发数c * 每个worker的QPS q,设计非常合理,每个worker自己限流,不用全局限流,减少锁竞争,性能更高
- 如果设置了
- 压测信息打印:给用户打印当前的压测配置,让用户清楚知道要跑多少请求、多少并发、目标URL,用户体验好
第七部分:并发控制与CSV通道初始化
rust
// Create worker semaphore
let semaphore = Arc::new(Semaphore::new(cli.c as usize));
// Channel for collecting stats for CSV output
let (tx, mut rx) = mpsc::channel::<RequestStats>(100_000);
逐行解释
- 信号量初始化
Arc::new(Semaphore::new(cli.c as usize)):创建异步信号量,初始许可数等于用户设置的并发数c- 核心作用:每个worker发请求前必须先获取信号量的许可,请求完成后许可自动释放,保证同时最多有c个请求在运行,精准控制并发数,这是压测并发控制的核心实现
- 用Arc包裹,让所有worker都能共享同一个信号量实例
- mpsc通道初始化
mpsc::channel::<RequestStats>(100_000):创建多生产者单消费者通道,缓冲区大小10万- 核心作用:所有worker(生产者)把每个请求的统计数据发送到通道,专门的CSV写入任务(消费者)统一接收并写入文件
- 设计原因:如果每个worker自己写文件,会有严重的锁竞争,多个线程同时写一个文件必须加锁,性能极差;用mpsc通道,单线程写文件,完全避免锁竞争,性能提升极大,这是Rust异步编程的经典模式
- 缓冲区10万:避免压测时请求量太大,通道满了导致worker发送阻塞,影响压测精度
第八部分:CSV写入异步任务
rust
// CSV writer task
let csv_handle = if matches!(cli.o, OutputType::Csv) {
let output_file = cli.output_file.clone();
Some(tokio::spawn(async move {
let mut writer: Box<dyn Write + Send> = if let Some(ref path) = output_file {
match fs::File::create(path) {
Ok(f) => {
let buf = BufWriter::new(f);
println!("CSV output written to: {}", path);
Box::new(buf) as Box<dyn Write + Send>
}
Err(e) => {
eprintln!("Failed to create output file: {}, using stdout", e);
Box::new(io::stdout()) as Box<dyn Write + Send>
}
}
} else {
Box::new(io::stdout()) as Box<dyn Write + Send>
};
writeln!(writer, "response-time,DNS+dialup,DNS,Request-write,Response-delay,Response-read,status-code,offset").ok();
while let Some(stat) = rx.recv().await {
let duration_s = stat.duration.as_secs_f64();
let conn_sec = stat.conn_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0);
let dns_sec = stat.dns_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0);
let req_sec = stat.req_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0);
let delay_sec = stat.delay_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0);
let res_sec = stat.res_duration.map(|d| d.as_secs_f64()).unwrap_or(0.0);
let offset_sec = stat.offset.as_secs_f64();
if stat.error.is_none() {
writeln!(writer, "{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{},{:.6}",
duration_s, conn_sec, dns_sec, req_sec, delay_sec, res_sec, stat.status, offset_sec).ok();
} else {
writeln!(writer, "{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{},{:.6},ERROR:{}",
duration_s, conn_sec, dns_sec, req_sec, delay_sec, res_sec, 0, offset_sec, stat.error.as_ref().unwrap()).ok();
}
}
writer.flush().ok();
}))
} else {
None
};
设计目的
只有当输出类型是CSV时,才启动这个异步任务,专门负责把所有请求的明细数据写入CSV文件或终端,避免阻塞worker任务。
核心设计与逐行解释
-
任务启动条件 :
if matches!(cli.o, OutputType::Csv),只有输出类型是CSV时才启动这个任务 -
写入器构建
- 用
Box<dyn Write + Send>trait对象,统一处理文件写入和终端写入,不用写两套逻辑,这是Rust多态的经典用法 - 如果用户指定了输出文件,创建文件,用
BufWriter缓冲写入,提升IO性能;创建文件失败的话,降级到终端输出,给用户打印警告 - 没有指定输出文件的话,直接用终端标准输出
- 用
-
CSV表头写入:先写入CSV的表头,定义每一列的含义,方便用户导入数据分析工具
-
数据接收循环
rustwhile let Some(stat) = rx.recv().await {- 循环等待通道里的统计数据,
rx.recv().await异步等待,不会阻塞线程 - 当所有的发送端
tx都被drop之后,recv()会返回None,循环退出
- 循环等待通道里的统计数据,
-
数据格式化 :把所有耗时从Duration转成秒数,保留6位小数,也就是微秒级精度,足够压测使用
- 用
map(|d| d.as_secs_f64()).unwrap_or(0.0),如果耗时是None(请求失败),就用0.0填充,保证CSV的列数一致
- 用
-
行写入 :成功请求和失败请求分别写入不同的内容,失败请求会把错误信息写到最后一列
- 用
.ok()忽略写入错误,因为写入终端/文件失败的时候,程序已经无法处理了,忽略不会导致panic
- 用
-
缓冲区刷新 :
writer.flush().ok(),循环退出后,把缓冲区里的所有数据强制写入磁盘/终端,避免数据丢失
第九部分:Worker任务预克隆与启动
rust
// Spawn workers
let mut handles = Vec::new();
let url = cli.url.clone();
let method: Method = cli.m.into();
let host_header = cli.host.clone();
let username = username.clone();
let password = password.clone();
let body = body.clone();
// Pre-clone values for each worker
let clients: Vec<_> = (0..cli.c).map(|_| client.clone()).collect();
let stats_clones: Vec<_> = (0..cli.c).map(|_| stats.clone()).collect();
let stop_flags: Vec<_> = (0..cli.c).map(|_| stop_flag.clone()).collect();
let semaphores: Vec<_> = (0..cli.c).map(|_| semaphore.clone()).collect();
let txs: Vec<_> = (0..cli.c).map(|_| tx.clone()).collect();
let urls: Vec<_> = (0..cli.c).map(|_| url.clone()).collect();
let headerss: Vec<_> = (0..cli.c).map(|_| headers.clone()).collect();
let requests_per_workers: Vec<_> = (0..cli.c).map(|_| requests_per_worker).collect();
let methods: Vec<_> = (0..cli.c).map(|_| method.clone()).collect();
let host_headers: Vec<_> = (0..cli.c).map(|_| host_header.clone()).collect();
let usernames: Vec<_> = (0..cli.c).map(|_| username.clone()).collect();
let passwords: Vec<_> = (0..cli.c).map(|_| password.clone()).collect();
let bodys: Vec<_> = (0..cli.c).map(|_| body.clone()).collect();
for i in 0..cli.c as usize {
let client = clients[i].clone();
let stats = stats_clones[i].clone();
let stop_flag = stop_flags[i].clone();
let semaphore = semaphores[i].clone();
let tx = txs[i].clone();
let url = urls[i].clone();
let headers = headerss[i].clone();
let requests_per_worker = requests_per_workers[i];
let method = methods[i].clone();
let host_header = host_headers[i].clone();
let username = usernames[i].clone();
let password = passwords[i].clone();
let body = bodys[i].clone();
let handle = tokio::spawn(async move {
// Worker核心逻辑(后面单独拆解)
});
handles.push(handle);
}
设计目的
预克隆所有worker需要的变量,然后启动指定数量的worker异步任务,每个worker负责发送请求、统计数据。
设计亮点
- 预克隆优化:提前把每个worker需要的变量克隆好,放到数组里,避免在循环里克隆,代码更清晰,性能更好
- 轻量克隆:所有用Arc包裹的变量,克隆只是增加引用计数,开销极小,不会复制底层数据
- 变量捕获 :每个worker的闭包用
async move,把克隆好的变量的所有权转移到闭包里,符合Rust的所有权规则,不会出现生命周期问题
第十部分:Worker核心循环逻辑
rust
let handle = tokio::spawn(async move {
let mut request_count = 0u64;
let worker_start = Instant::now();
loop {
// Check stop conditions
if stop_flag.load(Ordering::Relaxed) {
break;
}
// Check if we've done our share
if let Some(max) = requests_per_worker {
if request_count >= max {
break;
}
}
// QPS throttling
if let Some(interval) = qps_interval {
let elapsed = Instant::now() - worker_start;
let expected = Duration::from_nanos(
(request_count as f64 * interval.as_nanos() as f64) as u64
);
if elapsed < expected {
tokio::time::sleep(expected - elapsed).await;
}
}
// Acquire permit for concurrency control
let _permit = semaphore.acquire().await;
// Make request
let request_start = Instant::now();
let offset = request_start - start_time;
let mut req_builder = client.request(method.clone(), &url);
for (k, v) in &headers {
req_builder = req_builder.header(k, v);
}
if let (Some(ref u), Some(ref p)) = (&username, &password) {
req_builder = req_builder.basic_auth(u, Some(p));
}
if let Some(ref h) = host_header {
req_builder = req_builder.header("Host", h);
}
if let Some(ref b) = body {
req_builder = req_builder.body(b.clone());
}
// High-resolution timing using Instant
let dns_start = Instant::now();
let dns_duration = Some(dns_start - request_start);
let conn_start = Instant::now();
let conn_duration = Some(conn_start - dns_start);
let req_start = Instant::now();
match req_builder.send().await {
Ok(response) => {
let req_duration = req_start - conn_start;
let delay_start = Instant::now();
let delay_duration = Some(delay_start - req_start);
let status_code = response.status().as_u16();
let content_length = response.content_length().unwrap_or(0) as i64;
// Read and discard body
let res_begin = Instant::now();
let _ = response.bytes().await;
let res_duration = res_begin.elapsed();
let total_duration = request_start.elapsed();
let stat = RequestStats {
duration: total_duration,
status: status_code,
content_length,
error: None,
dns_duration,
conn_duration,
req_duration: Some(req_duration),
res_duration: Some(res_duration),
delay_duration,
offset,
};
stats.record(&stat);
let _ = tx.send(stat).await;
}
Err(e) => {
let total_duration = request_start.elapsed();
let stat = RequestStats {
duration: total_duration,
status: 0,
content_length: 0,
error: Some(e.to_string()),
dns_duration,
conn_duration,
req_duration: None,
res_duration: None,
delay_duration: None,
offset,
};
stats.record(&stat);
let _ = tx.send(stat).await;
}
}
request_count += 1;
}
});
设计目的
每个worker的核心循环,负责不停的发送HTTP请求,统计每个请求的耗时,记录到全局统计里,直到满足停止条件。
逐段拆解
-
初始化变量
request_count:记录这个worker已经发送的请求数worker_start:worker启动的时间,用来做QPS限流的计时
-
无限循环 :
loop {}是Rust的无限循环,直到遇到break退出 -
停止条件检查
- 先检查全局stop_flag是不是true,如果是,直接break退出循环
- 再检查是不是已经发完了分配的请求数,如果是,break退出
-
QPS限流逻辑(重点)
rustif let Some(interval) = qps_interval { let elapsed = Instant::now() - worker_start; let expected = Duration::from_nanos( (request_count as f64 * interval.as_nanos() as f64) as u64 ); if elapsed < expected { tokio::time::sleep(expected - elapsed).await; } }- 核心原理:计算当前已经发送的请求数,理论上应该用的时间,如果实际耗时比理论时间少,就sleep补够时间,保证QPS稳定
- 比如每个请求间隔0.1秒,发了5个请求,理论上应该用0.5秒,如果实际只用了0.3秒,就sleep 0.2秒,保证平均QPS是10
- 这种限流方式非常稳定,不会出现QPS忽高忽低的情况,是压测工具的标准限流方案
-
并发控制
rustlet _permit = semaphore.acquire().await;- 异步获取信号量的许可,只有拿到许可,才能继续执行,保证同时最多有c个请求在运行
_permit是许可的守卫,当它离开作用域的时候,会自动释放许可,不用手动释放,Rust的RAII机制,不会出现许可泄漏
-
请求构建与计时
- 记录请求开始的时间
request_start,计算这个请求相对于压测开始的偏移量offset - 用
client.request()构建请求,设置HTTP方法和URL - 循环添加所有请求头、基础认证、Host头、请求体
- 记录请求开始的时间
-
请求发送与响应处理
req_builder.send().await:异步发送请求,等待响应,不会阻塞线程- 成功响应处理 :
- 计算各个阶段的耗时,读取响应体(必须读取,不然连接无法复用),然后丢弃
- 构建
RequestStats结构体,调用stats.record()记录到全局统计 - 把统计数据发送到CSV通道
- 失败响应处理 :
- 计算总耗时,构建带错误信息的
RequestStats结构体 - 同样记录到全局统计,发送到CSV通道
- 计算总耗时,构建带错误信息的
-
请求数累加 :
request_count += 1,这个worker的请求数加1,继续下一次循环
第十一部分:压测停止控制
rust
// Duration-based stop
if let Some(dur) = duration {
let stop_flag = stop_flag.clone();
let handle = tokio::spawn(async move {
tokio::time::sleep(dur).await;
stop_flag.store(true, Ordering::Relaxed);
});
handles.push(handle);
}
// Signal handler for Ctrl+C
let stop_flag_sig = stop_flag.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
eprintln!("\nReceived interrupt signal, stopping...");
stop_flag_sig.store(true, Ordering::Relaxed);
});
逐行解释
- 时长控制停止
- 如果设置了压测时长,启动一个异步任务,sleep对应的时长,然后把stop_flag设为true,所有worker看到后就会退出循环,停止压测
- 把这个任务的handle加入handles数组,后面等待它完成
- Ctrl+C信号处理
- 启动一个异步任务,等待用户按下Ctrl+C,收到信号后,打印提示,把stop_flag设为true,让用户可以随时中断压测,不会导致程序卡死
- 用
.ok()忽略信号监听的错误,因为信号监听失败不影响压测的核心功能
第十二部分:任务等待与收尾
rust
// Wait for all workers
for handle in handles {
let _ = handle.await;
}
// Close CSV channel
drop(tx);
if let Some(h) = csv_handle {
let _ = h.await;
}
// Print summary report
if !matches!(cli.o, OutputType::Csv) {
let total_time = start_time.elapsed();
let summary = generate_summary(&stats, total_time);
if let Some(ref path) = cli.output_file {
match fs::File::create(path) {
Ok(mut f) => {
f.write_all(summary.as_bytes()).map_err(|e| anyhow!("Failed to write to file: {}", e))?;
println!("Summary written to: {}", path);
}
Err(e) => {
eprintln!("Failed to create output file: {}, printing to stdout", e);
print!("{}", summary);
}
}
} else {
print!("{}", summary);
}
}
Ok(())
}
逐行解释
- 等待所有worker完成:循环等待所有worker任务和时长控制任务完成,保证所有请求都处理完
- 关闭CSV通道 :
drop(tx),把所有的发送端都drop掉,这样CSV任务的rx.recv()会返回None,循环退出 - 等待CSV任务完成:如果启动了CSV任务,等待它完成,保证所有数据都写入了文件
- 汇总报告生成与输出
- 只有输出类型不是CSV的时候,才生成汇总报告
- 计算总压测时长,调用
generate_summary生成汇总报告字符串 - 如果用户指定了输出文件,把报告写入文件,失败的话降级到终端输出
- 没有指定输出文件的话,直接打印到终端
- 程序正常结束 :返回
Ok(()),压测完成
十一、报告生成相关函数
1. generate_summary 汇总报告生成函数
rust
fn generate_summary(stats: &AggregatedStats, total_time: Duration) -> String {
let total = stats.total_requests.load(Ordering::Relaxed);
let success = stats.success_requests.load(Ordering::Relaxed);
let errors = stats.error_requests.load(Ordering::Relaxed);
let mut output = String::new();
output.push_str("Summary:\n");
// General stats - matching hey format
let rps = total as f64 / total_time.as_secs_f64();
let durations = stats.durations.lock();
if !durations.is_empty() {
let mut sorted = durations.clone();
sorted.sort_unstable();
let total_ns: u64 = stats.total_duration.load(Ordering::Relaxed);
let avg_ns = total_ns / success;
let min_ns = stats.min_duration.load(Ordering::Relaxed);
let max_ns = stats.max_duration.load(Ordering::Relaxed);
writeln!(&mut output, " Total:\t{:.4} secs", total_time.as_secs_f64()).ok();
writeln!(&mut output, " Slowest:\t{:.4} secs", Duration::from_nanos(max_ns).as_secs_f64()).ok();
writeln!(&mut output, " Fastest:\t{:.4} secs", Duration::from_nanos(min_ns).as_secs_f64()).ok();
writeln!(&mut output, " Average:\t{:.4} secs", Duration::from_nanos(avg_ns).as_secs_f64()).ok();
writeln!(&mut output, " Requests/sec:\t{:.4}", rps).ok();
// Bytes transferred
let total_bytes = stats.total_bytes.load(Ordering::Relaxed);
if total_bytes > 0 {
let avg_bytes = total_bytes / success;
write!(&mut output, "\n Total data:\t{} bytes\n", total_bytes).ok();
writeln!(&mut output, " Size/request:\t{} bytes", avg_bytes).ok();
}
// Histogram
output.push_str("\nResponse time histogram:\n");
output.push_str(&generate_histogram(&sorted));
// Percentiles (matching Go hey: 10, 25, 50, 75, 90, 95, 99)
output.push_str("\nLatency distribution:\n");
let percentiles: [f64; 7] = [10.0, 25.0, 50.0, 75.0, 90.0, 95.0, 99.0];
for p in percentiles {
let idx = ((p / 100.0) * (sorted.len() - 1) as f64) as usize;
if idx < sorted.len() {
writeln!(&mut output, " {}%% in {:.4} secs", p as u32, Duration::from_nanos(sorted[idx]).as_secs_f64()).ok();
}
}
// Detailed stage statistics (matching Go hey)
output.push_str("\nDetails (average, fastest, slowest):\n");
output.push_str(&generate_stage_details(&sorted, stats));
} else if errors > 0 {
// Only errors, no successful requests
writeln!(&mut output, " Total:\t{:.4} secs", total_time.as_secs_f64()).ok();
output.push_str(" Slowest:\t0.0000 secs\n");
output.push_str(" Fastest:\t0.0000 secs\n");
output.push_str(" Average:\t0.0000 secs\n");
writeln!(&mut output, " Requests/sec:\t{:.4}", rps).ok();
}
// Status code distribution
let status_codes = stats.status_codes.lock();
if !status_codes.is_empty() {
output.push_str("\nStatus code distribution:\n");
let mut codes: Vec<_> = status_codes.iter().collect();
codes.sort_by_key(|k| k.0);
for (code, count) in codes {
writeln!(&mut output, " [{}]\t{} responses", code, count).ok();
}
}
// Error distribution
drop(status_codes);
let error_dist = stats.errors.lock();
if !error_dist.is_empty() {
output.push_str("\nError distribution:\n");
let mut errs: Vec<_> = error_dist.iter().collect();
errs.sort_by_key(|k| *k.1);
errs.reverse();
for (err, count) in errs.iter().take(10) {
writeln!(&mut output, " [{}]\t{}", count, err).ok();
}
if errs.len() > 10 {
writeln!(&mut output, " ... and {} more errors", errs.len() - 10).ok();
}
}
output.push('\n');
output
}
设计目的
生成人类可读的压测汇总报告,包含压测的核心指标、耗时分布、状态码分布、错误分布等,和业界标准的压测工具hey的报告格式兼容。
核心逻辑拆解
- 基础数据读取:读取总请求数、成功数、失败数,计算RPS(每秒请求数)
- 核心指标计算 :
- 如果有成功请求,把耗时列表排序,计算总时长、最慢/最快/平均耗时、RPS
- 计算总传输字节数、平均每个请求的字节数
- 直方图生成 :调用
generate_histogram生成响应时间的直方图,直观展示耗时分布 - 百分位数计算:计算P10/P25/P50/P75/P90/P95/P99延迟分布,这是压测报告的核心指标,比如P99表示99%的请求都在这个耗时以内,反映了系统的长尾延迟
- 阶段详情生成 :调用
generate_stage_details生成每个请求阶段的耗时统计 - 全失败场景处理:如果没有成功请求,只有错误,也生成基础的报告,不会panic
- 状态码分布:把状态码排序,输出每个状态码的数量
- 错误分布:把错误按出现次数倒序排序,最多显示10个,超过的话显示还有多少个,避免报告太长
- 返回报告字符串:把所有内容拼接到output字符串里,返回给调用方
2. generate_histogram 响应时间直方图生成函数
rust
fn generate_histogram(sorted: &[u64]) -> String {
let mut output = String::new();
if sorted.is_empty() {
return output;
}
let min_val = sorted[0] as f64;
let max_val = sorted[sorted.len() - 1] as f64;
if (max_val - min_val).abs() < 1e-9 {
writeln!(&mut output, " {:.3} [1]\t■", min_val / 1e9).ok();
return output;
}
let buckets = 10;
let bs = (max_val - min_val) / buckets as f64;
// Create buckets
let mut bucket_marks: Vec<f64> = (0..=buckets).map(|i| min_val + bs * i as f64).collect();
bucket_marks[buckets] = max_val;
let mut counts = vec![0u64; buckets + 1];
let mut bi = 0;
for &lat in sorted {
let lat = lat as f64;
while bi < buckets && lat > bucket_marks[bi + 1] {
bi += 1;
}
counts[bi] += 1;
}
let max_count = *counts.iter().max().unwrap_or(&1);
let max_bar = 20;
for i in 0..=buckets {
let bar_len = if max_count > 0 {
(counts[i] * max_bar / max_count) as usize
} else {
0
};
writeln!(&mut output, " {:.3} [{}]\t{}",
bucket_marks[i] / 1e9,
counts[i],
"■".repeat(bar_len)).ok();
}
output
}
设计目的
把响应时间分成10个桶,统计每个桶里的请求数,用条形图展示,直观的让用户看到耗时的分布情况。
核心逻辑
- 边界处理:如果耗时列表为空,直接返回空字符串;如果所有耗时都一样,直接输出一行,避免除以0的错误
- 分桶计算:把耗时范围分成10个等宽的桶,计算每个桶的边界
- 统计每个桶的请求数:遍历排序后的耗时列表,统计每个桶里的请求数量
- 生成条形图:最多显示20个■符号,按比例计算每个桶的条形长度,输出直方图
3. generate_stage_details 请求阶段详情生成函数
rust
fn generate_stage_details(_sorted: &[u64], stats: &AggregatedStats) -> String {
let mut output = String::new();
// DNS+dialup (combined)
let mut dns = stats.dns_durations.lock().clone();
let mut conn = stats.conn_durations.lock().clone();
if !dns.is_empty() && !conn.is_empty() {
dns.sort_unstable();
conn.sort_unstable();
let dns_avg = dns.iter().map(|x| *x as f64).sum::<f64>() / dns.len() as f64;
let dns_min = dns[0] as f64;
let dns_max = dns[dns.len() - 1] as f64;
let conn_avg = conn.iter().map(|x| *x as f64).sum::<f64>() / conn.len() as f64;
let conn_min = conn[0] as f64;
let conn_max = conn[conn.len() - 1] as f64;
// DNS+dialup = DNS + dialup times combined
let combined_avg = dns_avg + conn_avg;
let combined_min = dns_min + conn_min;
let combined_max = dns_max + conn_max;
writeln!(&mut output, " DNS+dialup:\t{:.4} secs, {:.4} secs, {:.4} secs",
combined_avg / 1e9,
combined_min / 1e9,
combined_max / 1e9).ok();
writeln!(&mut output, " DNS-lookup:\t{:.4} secs, {:.4} secs, {:.4} secs",
dns_avg / 1e9,
dns_min / 1e9,
dns_max / 1e9).ok();
}
// req write
let mut req = stats.req_durations.lock().clone();
if !req.is_empty() {
req.sort_unstable();
let avg = req.iter().map(|x| *x as f64).sum::<f64>() / req.len() as f64;
let min = req[0] as f64;
let max = req[req.len() - 1] as f64;
writeln!(&mut output, " req write:\t{:.4} secs, {:.4} secs, {:.4} secs",
avg / 1e9, min / 1e9, max / 1e9).ok();
}
// resp wait (response delay)
let mut delay = stats.delay_durations.lock().clone();
if !delay.is_empty() {
delay.sort_unstable();
let avg = delay.iter().map(|x| *x as f64).sum::<f64>() / delay.len() as f64;
let min = delay[0] as f64;
let max = delay[delay.len() - 1] as f64;
writeln!(&mut output, " resp wait:\t{:.4} secs, {:.4} secs, {:.4} secs",
avg / 1e9, min / 1e9, max / 1e9).ok();
}
// resp read
let mut res = stats.res_durations.lock().clone();
if !res.is_empty() {
res.sort_unstable();
let avg = res.iter().map(|x| *x as f64).sum::<f64>() / res.len() as f64;
let min = res[0] as f64;
let max = res[res.len() - 1] as f64;
writeln!(&mut output, " resp read:\t{:.4} secs, {:.4} secs, {:.4} secs",
avg / 1e9, min / 1e9, max / 1e9).ok();
}
output
}
设计目的
统计每个请求阶段(DNS、TCP连接、请求写入、首字节等待、响应读取)的平均、最快、最慢耗时,帮助用户定位性能瓶颈。
核心逻辑
- 对每个阶段的耗时列表排序,计算平均值、最小值、最大值
- 把纳秒转成秒,格式化输出,保留4位小数
- 每个阶段都做了非空判断,没有数据的阶段不会输出,避免panic
十二、整体设计思想与编码思路分析
1. 模块化设计
- 每个函数只做一件事,职责单一:
parse_duration只负责解析时长,record只负责记录统计数据,generate_summary只负责生成报告 - 代码结构清晰,可读性高,易维护,易扩展,后续加新功能只需要加对应的函数,不用修改核心逻辑
2. 高性能设计
- 无锁计数:高频更新的计数用原子类型,完全无锁,性能极高
- 高性能同步原语:用parking_lot的Mutex替代标准库Mutex,锁开销更小
- 内存预分配:所有Vec都预分配容量,避免频繁扩容带来的性能损耗
- 连接池复用:reqwest Client全局共享连接池,长连接复用,避免频繁建立TCP连接
- 缓冲IO:用BufWriter做文件写入,批量写入,减少系统调用,提升IO性能
- 单线程写IO:用mpsc通道把所有写IO集中到一个线程,完全避免锁竞争,性能提升极大
- 零成本抽象:所有迭代器、模式匹配、trait抽象都是零成本的,和手写循环性能一致,没有额外的运行时开销
3. 内存与线程安全设计
- 完全遵循Rust的所有权规则,没有野指针、空指针、内存泄漏
- 所有多线程共享的数据都用Arc包裹,保证引用计数安全
- 多线程写入用原子类型或者Mutex,完全避免数据竞争
- 所有可能不存在的值都用Option包裹,编译器强制处理,不会出现空指针异常
- 所有用户输入都做了合法性校验,提前拦截非法参数,避免运行时panic
4. 异步架构设计
- 基于Tokio多线程异步运行时,高并发场景下性能远超同步多线程模型
- 异步任务的开销极小,支持上万级别的并发,不会像操作系统线程那样有巨大的栈开销
- 所有IO操作都是异步的,不会阻塞线程,能充分利用CPU的时间片,不会在IO等待时浪费CPU资源
5. 生态最佳实践
- 用clap做命令行参数解析,anyhow做错误处理,reqwest做HTTP客户端,parking_lot做同步原语,都是Rust生态里最成熟、最稳定、最常用的库
- 完全符合Rust的编码规范,命名规范、错误处理、类型转换、所有权管理都是业界标准写法
- 兼容cargo子命令规范,符合Rust CLI工具的设计标准
6. 用户体验设计
- 丰富的参数配置,覆盖了压测的所有常用场景
- 友好的错误提示,所有错误都带上下文,告诉用户哪里错了,怎么改
- 支持Ctrl+C中断,不会导致程序卡死
- 输出格式兼容业界标准的hey工具,用户上手成本低
- 支持汇总报告和CSV明细两种输出格式,既适合人读,也适合机器分析
十三、关键语法知识点总结
| 知识点 | 用途 | 安全/性能优势 |
|---|---|---|
| Arc | 多线程共享数据 | 原子引用计数,线程安全,自动内存释放,无GC开销 |
| AtomicU64/AtomicBool | 高并发计数/标志 | 无锁线程安全,性能远超锁,适合高频更新的计数场景 |
| Mutex | 多线程共享集合的写入安全 | 保证同一时间只有一个线程能修改数据,避免数据竞争 |
| Option | 可能不存在的值 | 完全避免空指针,编译器强制处理两种情况,无空指针异常 |
| Result<T, E> | 错误处理 | 强制处理错误,不会出现未捕获的异常,错误带上下文,易排查 |
| async/await | 异步IO编程 | 无阻塞异步IO,高并发性能好,代码写法和同步代码一样简洁 |
| mpsc通道 | 多线程间消息传递 | 无锁消息传递,避免共享内存的锁竞争,线程安全,解耦生产者和消费者 |
| Semaphore | 并发数控制 | 异步非阻塞,精准控制同时运行的任务数量,适合压测场景 |
| From trait | 类型转换 | 解耦业务类型和第三方库类型,代码更健壮,可维护性强 |
| 迭代器 | 集合处理 | 零成本抽象,和手写循环性能一致,代码更简洁,不易出错 |