Rust Tokio 背压控制:异步系统别只会拼命 spawn

背景 刚开始写 Tokio 程序时,很多人都会觉得异步特别轻: 一个请求一个 task 来一个任务就 tokio::spawn channel 一接就处理 代码看起来很流畅,吞吐也不错。 但一到高负载场景,问题很快就出来了: 任务堆积越来越多 内存不断上涨 下游数据库或 HTTP 依赖被打爆 延迟从毫秒飙到秒级 这时候根问题通常不是 Tokio 不够快,而是系统没有背压。 什么是背压 背压的本质是:当下游处理不过来时,上游必须感知并减速。 如果没有这层机制,异步系统就很容易变成“把问题排队排到内存里”。 一个最典型的错误写法: loop { let job = accept_job().await; tokio::spawn(async move { process_job(job).await; }); } 这段代码的意思其实是: 来多少任务都收 能不能处理完以后再说 如果生产速度持续高于消费速度,系统一定会失控。 最简单的背压:有界 channel 相比无脑 spawn,更稳妥的起点通常是 bounded channel。 use tokio::sync::mpsc; #[tokio::main] async fn main() { let (tx, mut rx) = mpsc::channel::<Job>(1024); tokio::spawn(async move { while let Some(job) = rx.recv().await { process_job(job).await; } }); loop { let job = accept_job().await; if tx.send(job).await.is_err() { break; } } } struct Job; async fn accept_job() -> Job { Job } async fn process_job(_job: Job) {} mpsc::channel(1024) 的关键不是“1024 这个数字”,而是它有上限。 ...

2026年4月16日 · 2 分钟 · BvBeJ

Rust 异步数据库访问:连接池、超时与稳定性

背景 Rust 写后端服务时,数据库访问通常是绕不开的一层。 很多人刚开始用 sqlx 或 tokio-postgres 时,会把关注点放在: 能不能异步查询 类型映射是否方便 宏检查 SQL 是否好用 这些当然重要,但线上跑起来以后,更现实的问题通常是: 连接池应该开多大 请求等连接要等多久 数据库抖动时怎么避免把整个服务拖死 这些问题不处理好,异步只能让你“更高效地把数据库打爆”。 先建立一个基本事实 异步不是无限并发。 你的 Tokio 任务可以很多,但数据库连接永远是稀缺资源。无论是 PostgreSQL、MySQL 还是其他关系型数据库,都不可能让应用无限开连接而没有代价。 所以数据库访问的第一原则不是“尽快发查询”,而是: 连接数可控 排队时间可控 查询超时可控 以 sqlx 为例初始化连接池 use sqlx::postgres::PgPoolOptions; use std::time::Duration; async fn create_pool(database_url: &str) -> Result<sqlx::PgPool, sqlx::Error> { PgPoolOptions::new() .max_connections(32) .min_connections(4) .acquire_timeout(Duration::from_secs(2)) .idle_timeout(Duration::from_secs(300)) .max_lifetime(Duration::from_secs(1800)) .connect(database_url) .await } 这几个参数都很关键: max_connections:连接池上限 min_connections:最小保活连接数 acquire_timeout:拿连接最多等多久 max_lifetime:连接多久轮换一次 尤其是 acquire_timeout,它能防止高峰时请求无止境排队。 连接池大小不是越大越好 很多人看到连接池耗尽,就第一时间把池子调大。 这有时能缓一口气,但经常只是把压力继续往数据库推。 连接池大小应该结合几个因素来定: ...

2026年4月16日 · 2 分钟 · BvBeJ

Rust Tokio 优雅停机:让服务真正可控

为什么要关心停机 很多人写异步服务时,把注意力都放在“怎么启动”,很少认真想“怎么结束”。 但线上真正麻烦的,往往是停机阶段: Kubernetes 滚动发布,Pod 收到 SIGTERM 服务还在处理请求,但新流量已经切走 后台任务没停干净,日志和指标都丢了 数据库连接突然断掉,导致半成功半失败 如果退出流程没有设计好,服务看起来可用,实际上很难运维。 Tokio 默认不会帮你解决一切 Tokio 的运行时很好用,但它不会自动替你处理这些问题: 谁来监听退出信号 如何通知所有任务停止 正在跑的任务是立即取消,还是等它收尾 超时之后要不要强制退出 这些都需要业务自己定义。 一个基础模型 一个比较稳妥的思路是分三步: 接收退出信号 广播 shutdown 事件 等待任务收尾,必要时超时强退 先看一个简化版结构: use tokio::signal; use tokio::sync::broadcast; use tokio::time::{timeout, Duration}; #[tokio::main] async fn main() -> anyhow::Result<()> { let (shutdown_tx, _) = broadcast::channel::<()>(16); let server_handle = tokio::spawn(run_http_server(shutdown_tx.subscribe())); let worker_handle = tokio::spawn(run_background_worker(shutdown_tx.subscribe())); signal::ctrl_c().await?; println!("received shutdown signal"); let _ = shutdown_tx.send(()); let _ = timeout(Duration::from_secs(10), async { let _ = server_handle.await; let _ = worker_handle.await; }).await; Ok(()) } 这段代码表达的核心思想很重要: ...

2026年4月15日 · 2 分钟 · BvBeJ

写给 C++ 程序员的 Rust 入门指南

为什么要学 Rust C++ 写多了,总会遇到这些问题: 悬空指针、野指针 内存泄漏 数据竞争(data race) 未定义行为(UB) Rust 从语言层面就帮你杜绝了这些。它的所有权系统(Ownership)是核心——编译期检查,零运行时开销。 本文用 C++ 对比着讲 Rust,帮你快速上手。 基础类型:差不多 // C++ int x = 42; double pi = 3.14; bool flag = true; char c = 'A'; auto* ptr = new int(42); // Rust let x: i32 = 42; let pi: f64 = 3.14; let flag: bool = true; let c: char = 'A'; let mut ptr = Box::new(42); // 堆分配,Box 就是智能指针 关键区别: Rust 变量默认不可变,let mut x 才是可变的 i32、f64、bool 这些名字和 C++ 相似 没有裸指针,但有 *const T 和 *mut T(安全受限) 所有权:核心概念 这是 Rust 和 C++ 最核心的区别。 C++ 的问题 int* create_buffer() { int* buffer = new int[100]; // ... 处理 ... return buffer; // 谁负责释放? } void process() { int* data = create_buffer(); // 用完了 delete[] data; // 忘了 delete 就内存泄漏 } Rust 的解决方案:所有权转移 fn create_buffer() -> Vec<i32> { let buffer = vec![0; 100]; // Vec 是堆数组,类似 std::vector buffer // 所有权转移给调用方,函数结束后 buffer 不被 drop } fn process() { let data = create_buffer(); // 用完了,data 离开作用域,自动释放(drop) } // data 在这里被 drop,无需手动管理 Rust 三条规则: ...

2026年4月15日 · 5 分钟 · BvBeJ

Rust 错误处理:从 panic 到 anyhow

C++ 错误处理的痛 C++ 里错误处理方式一大堆,但没一个完美的: // 方式1: 返回值 + 特殊值 int get_value() { if (failed) return -1; // -1 是魔法值 } // 方式2: 异常 try { do_something(); } catch (const std::exception& e) { // 异常才是正文... } 异常的问题是:不知道会抛什么,不知道该不该 catch,析构函数里抛异常还会 std::terminate。 Rust 的错误哲学 Rust 把错误分为两类: 可恢复错误 → Result<T, E> 不可恢复错误 → panic! // 可恢复:用 Result fn read_file(path: &str) -> Result<String, std::io::Error> { std::fs::read_to_string(path) } // 不可恢复:用 panic fn main() { let v = vec![1, 2, 3]; v.get(10).expect("索引超出范围"); // 程序员的bug } 实战:错误处理的几种模式 1. 基本用法 use std::fs::File; use std::io::{self, Read}; fn read_config() -> Result<String, io::Error> { let mut file = File::open("config.toml")?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } fn main() { match read_config() { Ok(config) => println!("配置: {}", config), Err(e) => eprintln!("读取配置失败: {}", e), } } ? 操作符是灵魂——错误自动向上传播,不需要手写 match。 ...

2026年4月9日 · 2 分钟 · BvBeJ

Rust 所有权与借用:我的理解之路

缘起 从 C++ 转 Rust,最不适应的不是语法,而是一种全新的思维模式。 Rust 的所有权系统(Ownership)是语言最核心的创新,也是最陡峭的学习曲线。 C++ 的惯性思维 在 C++ 里,我们习惯了这样的写法: std::string get_name() { return "BvBeJ"; // 编译器会处理返回值优化 } void process() { std::string name = get_name(); std::string alias = name; // 拷贝?还是引用? // ... } // name 和 alias 都会析构 直觉告诉我们这里发生了拷贝。但在 Rust 里,同样的思维会让你碰壁。 Rust 的所有权规则 Rust 遵循三条简单规则: 每个值有一个所有者(Owner) 同一时间只有一个所有者 当所有者离开作用域,值被丢弃(Dropped) fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 被"移动"到 s2 // println!("{}", s1); // ❌ 编译错误!s1 已经无效 println!("{}", s2); // ✅ } // s2 离开作用域,内存被释放 “移动"语义取代了 C++ 的拷贝——这是最大的思维转变。 ...

2026年4月8日 · 2 分钟 · BvBeJ