Rust AI CLI 的网络层优化:连接池、HTTP/2 和请求合并的关键技术

发布时间:2026/7/17 15:06:13
Rust AI CLI 的网络层优化:连接池、HTTP/2 和请求合并的关键技术 Rust AI CLI 的网络层优化连接池、HTTP/2 和请求合并的关键技术一、连接池别再每次都重新握手了最开始我的代码长这样——每次请求都创建一个新的reqwest::Client// ❌ 每次请求都新建 Client每次都要做 TLS 握手 async fn call_api_naive(prompt: str) - ResultString, reqwest::Error { // 创建一个全新的 HTTP 客户端 —— 这会导致每次请求都重新建立连接 let client reqwest::Client::new(); let resp client .post(https://api.openai.com/v1/chat/completions) .json(build_body(prompt)) .send() .await?; Ok(resp.text().await?) }这么写的问题很明显每次调用都要完整地走一遍 TCP 三次握手 TLS 四次握手光握手就要几十毫秒。如果你的 CLI 工具需要连续发多个请求比如先做代码分析再做代码生成这个延迟就叠加起来了。正确的做法是用连接池复用连接。reqwest的Client本身就是带连接池的关键是全局只创建一个实例use once_cell::sync::Lazy; use reqwest::Client; // ✅ 全局单例 Client内部维护连接池复用 TCP 连接 static HTTP_CLIENT: LazyClient Lazy::new(|| { Client::builder() // 连接池中最大空闲连接数每个 host .pool_max_idle_per_host(20) // 空闲连接的最长存活时间秒 .pool_idle_timeout(Some(Duration::from_secs(90))) // HTTP/2 默认开启这里显式确认 .http2_prior_knowledge() .build() .expect(Failed to build HTTP client) }); async fn call_api_optimized(prompt: str) - ResultString, reqwest::Error { // 复用全局 Client底层自动从连接池获取连接 let resp HTTP_CLIENT .post(https://api.openai.com/v1/chat/completions) .json(build_body(prompt)) .send() .await?; Ok(resp.text().await?) }这里有几个关键参数pool_max_idle_per_host(20)每个目标 host 最多保留 20 个空闲连接。如果并发量大可以适当调高。pool_idle_timeout空闲连接超过 90 秒就关闭避免占用服务端资源。http2_prior_knowledge()强制使用 HTTP/2下一节详细讲。二、HTTP/2 多路复用一个连接跑多路请求HTTP/1.1 虽然支持 Keep-Alive但请求是串行的——同一个连接上上一个请求没完成下一个就得等着队头阻塞。HTTP/2 通过多路复用解决了这个问题在 Rust 里启用 HTTP/2 很简单reqwest默认就会优先尝试 HTTP/2通过 ALPN 协商。如果想强制使用let client Client::builder() .http2_prior_knowledge() // 跳过 HTTP/1.1 协商直接用 HTTP/2 .build() .unwrap();但要注意http2_prior_knowledge()只有在确认服务器支持 HTTP/2 的情况下才使用否则连接会失败。更稳妥的方式是不加这一行让客户端和服务端自动协商。三、请求合并相同问题别重复发在 AI CLI 场景里有时候用户会连续问类似的问题比如这段代码还有问题吗紧接着能再详细解释下吗或者我们的工具内部需要并发查询多个 AI 接口。这时候请求合并就很有用。思路是用一个请求队列把短时间内比如 50ms 内对同一个 endpoint 的请求合并成一次批量调用use std::collections::HashMap; use tokio::sync::Mutex; use std::time::{Duration, Instant}; /// 请求合并器将短时间内的多个请求合并为一次批量调用 struct RequestBatcherT: Clone { // 待合并的请求缓冲区key → (内容, 回调发送端, 过期时间) buffer: MutexHashMapString, (T, tokio::sync::oneshot::SenderString, Instant), // 合并窗口多长时间内的请求会被合并 batch_window: Duration, } implT: Clone std::fmt::Debug RequestBatcherT { fn new(batch_window: Duration) - Self { Self { buffer: Mutex::new(HashMap::new()), batch_window, } } /// 将请求加入合并队列返回一个接收响应的 channel async fn submit(self, key: String, payload: T) - String { let (tx, rx) tokio::sync::oneshot::channel(); let mut buffer self.buffer.lock().await; buffer.insert(key, (payload, tx, Instant::now())); drop(buffer); // 释放锁 // 等待批量处理的结果 rx.await.unwrap_or_else(|_| 请求合并失败.to_string()) } } // 后台任务定时检查缓冲区批量处理过期或满额的请求 async fn batch_processorT: Clone std::fmt::Debug(batcher: std::sync::ArcRequestBatcherT) { let mut interval tokio::time::interval(Duration::from_millis(50)); loop { interval.tick().await; let mut buffer batcher.buffer.lock().await; let now Instant::now(); // 收集所有到期的请求 let expired: VecString buffer .iter() .filter(|(_, (_, _, deadline))| now.duration_since(*deadline) batcher.batch_window) .map(|(k, _)| k.clone()) .collect(); for key in expired { if let Some((payload, tx, _)) buffer.remove(key) { // 模拟批量处理的 API 调用 let result format!(处理完成: {:?}, payload); let _ tx.send(result); // 发送结果给等待者 } } } }核心思想就是用oneshotchannel 让多个调用者等待同一个批量结果后台定时器每隔 50ms 把积压的请求一起处理掉。四、实战一个优化前后的完整对比我把上面三个优化整合进了一个简单的 AI CLI 工具里对比优化前后的性能use std::time::Instant; use reqwest::Client; use once_cell::sync::Lazy; // 全局 HTTP 客户端连接池 HTTP/2 static CLIENT: LazyClient Lazy::new(|| { Client::builder() .pool_max_idle_per_host(10) .pool_idle_timeout(Some(Duration::from_secs(60))) .build() .unwrap() }); async fn optimized_call(prompt: str) - String { // 1. 复用连接池一次握手多次请求 // 2. HTTP/2 自动多路复用 let start Instant::now(); let resp CLIENT .post(https://api.example.com/v1/chat) .json(serde_json::json!({prompt: prompt})) .send() .await .unwrap(); let text resp.text().await.unwrap(); let elapsed start.elapsed(); println!(请求耗时: {}ms, elapsed.as_millis()); text } // 对比连续发 5 个请求 async fn benchmark() { let prompts vec![ 分析这段代码的复杂度, 给出优化建议, 生成单元测试, 检查安全问题, 写注释文档, ]; // 并发发送 5 个请求 —— 得益于 HTTP/2 和连接池速度远超串行 let start Instant::now(); let tasks: Vec_ prompts .iter() .map(|p| optimized_call(p)) .collect(); let results futures::future::join_all(tasks).await; println!(5 个请求总耗时: {}ms, start.elapsed().as_millis()); }实测下来5 个串行请求在优化前要大约 3-4 秒每次都要 TLS 握手优化后用连接池 HTTP/2 多路复用总耗时降到 600-800ms基本等于最慢的那个请求的时间。实际项目里我还观察到一个更隐蔽的现象连接池的pool_max_idle_per_host设太大比如 50在调用多个不同的 API 域名时TCP 连接总数超过了 Linux 的net.ipv4.ip_local_port_range上限触发无法分配本地端口的奇怪错误。排查了半天才发现不是 Rust 代码的问题而是内核参数。后续把pool_max_idle_per_host调到 5并监控TIME_WAIT连接数问题消失。五、总结连接池——全局一个Client别每次都new让底层自动管理 TCP 连接。HTTP/2 多路复用——一个连接上并行发多个请求避免 HTTP/1.1 的队头阻塞。请求合并——用 channel 和定时器把短时间内的重复请求合并成一次批量调用。这些优化说起来都不复杂但真正落地到自己写的工具里体验提升是实打实的。作为一个转行的程序员我的学习方式就是遇到问题 → 查文档 → 写 demo → 做对比实验把每个点都吃透。网络层优化还有很多东西要学QUIC、零拷贝、自定义 DNS 解析等慢慢来吧。