周一早高峰的地铁站,人潮汹涌。闸机在入口限流,站台有人数管控,列车会临时加编组,广播把大家调度得井井有条。这一整套“城市级调度”,就是微服务在高并发下要做的事。你的 Rust 服务,也需要同款四道闸门:限流、背压、批处理、以及一脑袋灵光的中间件。

先把结论放在前面。稳定不是“慢”,稳定是让“快”有序地发生。只要闸门装在对的位置,洪水进来都能分流成河。
为什么会堵:异步不等于无限并发
很多人一看到异步,就以为服务器像开挂了一样能无限接单。现实更像外卖平台:订单暴涨那一刻,骑手数量、后厨火力、商家出餐速度,任何一个短板都能卡住整条链路。吞吐永远受制于最慢的环节,排队本身不是原罪,失控排队才是事故的来源。所以第一原则很简单:别让请求在你不知道的地方悄悄堆积。该拒就拒、该排就排、该攒就攒、该协调就协调。
限流:像地铁闸机一样,按节奏放行
入口限流就是闸机。我们不是不欢迎大家,而是不想一窝蜂冲进站台把人挤翻。限流保护的是你自己和下游依赖,尤其是第三方 API、数据库、或拥贵资源位。在 Rust 里,基于 Tower 生态可以很优雅地加一层“闸机”。下面是一个把限速、并发上限、超时、追踪打包起来的最小示例,搭配 Axum 路由即可复用在全站:
use axum::{routing::post, Router};
use std::time::Duration;
use tower::{ServiceBuilder, timeout::TimeoutLayer};
use tower::limit::{ConcurrencyLimitLayer, RateLimitLayer};
use tower_http::trace::TraceLayer;
async fn create_order() -> &'staticstr {
"ok"
}
#[tokio::main]
async fn main() {
let middleware_stack = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(2)))
.layer(ConcurrencyLimitLayer::new(256))
.layer(RateLimitLayer::new(100, Duration::from_secs(1)))
.into_inner();
// 堆代码 duidaima.com
let app = Router::new()
.route("/orders", post(create_order))
.layer(middleware_stack);
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
这个“闸机”有两层意思:速率限制(每秒最多 100 个请求),和并发限制(最多同时处理 256 个)。既平滑了突发,也避免把下游打跪。更进一步,你可以按租户、按 API Key 维度做分桶配额,把“公平”落在数据上。
背压:后厨票夹满了,就先别接新单
后厨的票据夹一旦塞满,聪明的店长不会继续往里硬塞,而是会请客人稍后再来,或者指引到别的窗口。这就是背压的本质:我现在忙,给我个喘气的机会。在 Rust 里,背压的第一把锤子就是“有界”。无论是队列还是缓冲区,先把上限设出来,满了就要么等待、要么快速失败:
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
#[derive(Debug)]
struct Job(u64);
#[tokio::main]
async fn main() {
// 有界队列,容量 1024
let (tx, mut rx) = mpsc::channel::<Job>(1024);
// 生产者:如果队列满了,try_send 会返回错误,这里选择快速失败
let producer = tokio::spawn(asyncmove {
foriin0..10_000u64 {
if tx.try_send(Job(i)).is_err() {
// 背压信号:丢弃或记录并交给上游重试
}
}
});
// 消费者:模拟处理耗时
let consumer = tokio::spawn(asyncmove {
whileletSome(job) = rx.recv().await {
// 处理一条
let_ = job;
sleep(Duration::from_millis(2)).await;
}
});
let_ = tokio::join!(producer, consumer);
}
如果你在 HTTP 服务入口,还可以加一层“忙则立刻 503”,告诉上游“现在真的很忙,别挤了,稍后重试”,避免把延迟堆成雪崩:
use tower::{ServiceBuilder};
use tower::load_shed::LoadShedLayer;
let app = Router::new()
.route("/orders", post(create_order))
.layer(ServiceBuilder::new()
.layer(LoadShedLayer::new())
);
背压最大的坑,是“无界”。无界队列会把尾延迟拖到肉眼无法接受;忙时盲目重试,也会把系统拖进“重试风暴”。要让失败快一点到来,让重试更聪明地发生(如指数退避,并设置重试上限)。
批处理:攒一桶再送,既省力又提吞吐
洗衣店不会一件衣服就开一次机,快递员也会把同小区的包裹一起送。对微服务来说,很多写操作都更适合“攒够再发”,比如批量写数据库、批量发消息队列、或者调用支持批量的下游 API。在 Rust 里,常见做法是双阈值触发:攒到 N 条,或者等到 T 毫秒,就一起刷下去。示例骨架如下:
use tokio::{sync::mpsc, time::{self, Duration, Instant}};
#[derive(Clone, Debug)]
struct Event(String);
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<Event>(2048);
let batcher = tokio::spawn(asyncmove {
letmax_batch = 100usize;
letmax_wait = Duration::from_millis(50);
letmut buf = Vec::with_capacity(max_batch);
letmut deadline = Instant::now() + max_wait;
letmut ticker = time::interval(max_wait);
ticker.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
loop {
tokio::select! {
maybe = rx.recv() => {
match maybe {
Some(ev) => {
buf.push(ev);
if buf.len() >= max_batch {
flush(&mut buf).await;
deadline = Instant::now() + max_wait;
}
}
None => {
if !buf.is_empty() { flush(&mut buf).await; }
break;
}
}
}
_ = ticker.tick() => {
if !buf.is_empty() { flush(&mut buf).await; }
deadline = Instant::now() + max_wait;
}
}
}
});
let_ = batcher.await;
}
async fn flush(buf: &mutVec<Event>) {
// 批量写入下游(数据库/队列/批量 API)
// 记录本次批量大小与耗时,便于优化
buf.clear();
}
批处理提升吞吐的同时,也要盯住尾延迟。一般从“小批量、短等待”起步,观察 P95/P99,再慢慢调大。记得把每次批大小、等待时长、失败率打到指标里。
中间件:把规则装进大脑,优雅地一处生效
机场安检线有多道工序,但互不干扰且配合默契。微服务的中间件,就是把“规则”装进统一的大脑里:超时、重试、限流、压缩、追踪、鉴权……一处配置,多处受益。
Tower / Axum 生态里,常用组合是这样的:
use std::time::Duration;
use tower::{ServiceBuilder, timeout::TimeoutLayer};
use tower::limit::{ConcurrencyLimitLayer, RateLimitLayer};
use tower_http::{trace::TraceLayer, compression::CompressionLayer, classify::ServerErrorsFailureClass};
use tower_http::cors::CorsLayer;
let middleware_stack = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(CorsLayer::permissive())
.layer(TimeoutLayer::new(Duration::from_secs(1)))
.layer(ConcurrencyLimitLayer::new(256))
.layer(RateLimitLayer::new(100, Duration::from_secs(1)))
.into_inner();
小贴士是:把“参数”抽成配置,让它和指标联动。系统忙的时候,自动调小并发上限;下游稳定时,再慢慢放开。在塔式中间件里做这件事非常自然。
策略怎么选:一句话的矩阵
如果上游太猛,先用限流稳入口,再用背压防堆积。
如果下游很昂贵,优先批处理,配合超时与聪明的重试。
如果整体都在抖动,用限流稳住入口,再把并发、超时、重试参数挂在指标上动态调。
实战一条链:大促下单一路稳住
入口先按租户/用户限流,给重度用户保留突发额度。订单写入走“100 条或 50ms 就刷”的批处理。库存服务前面挂并发上限,忙则直接 503(load shed),上游收到后按指数退避重试。全链路用 tracing 打点、用 metrics 暴露,面板盯的是入/出 QPS、队列深度、批大小、超时率、P95/P99。
监控与优化:闭环才是稳定的灵魂
没有数据,调参就像摸黑走路。最少要有这些:入/出流量、当前并发、排队深度、批量大小、超时与重试次数、P95/P99 延迟。出事故时,一眼能看到“哪一段在发红”。有了指标,才谈得上自动化:SLO 警报触发后,临时把入口速率下调 20%,或把批处理等待时间减少 30%,顶住高峰再恢复。
最后一锤:稳不是慢,而是让快有序发生
今天你要做的,就是给关键接口装上四道闸门。把闸门装在入口,别让洪水直接冲进厨房。然后你会发现,流量再大,系统也只是“忙而不乱”。