mpsc 通道的隐藏成本
摘要
本文分析了 Rust 中 Tokio 的 mpsc 通道中意想不到的内存分配开销,揭示了由于内部块大小导致的每个通道的固定开销。文章展示了这一开销如何影响诸如 Agent Gateway 这样的大规模应用程序,并建议采用 futures-channel 等替代方案以提高内存效率。
<p><a href="https://lobste.rs/s/jihmlg/hidden_cost_mpsc_channels">评论</a></p>
查看缓存全文
缓存时间:
2026/05/13 00:23
# mpsc 通道的隐藏成本
来源:https://blog.howardjohn.info/posts/mpsc-cost/
最近,我花了很多时间分析并优化我们的 Rust 反向代理 AgentGateway(https://agentgateway.dev/)的内存使用情况。反复出现的一个问题是,看似无害的 Tokio `mpsc` 通道竟然分配了惊人的大量内存。
按照我天真的理解,我原本认为分配模式如下:
```rust
struct BigStruct {
data: [u8; 1024],
}
fn main() {
// 分配约 1024 字节
let _ = tokio::sync::mpsc::channel::<BigStruct>(1);
// 分配约 1024*1024 字节
let _ = tokio::sync::mpsc::channel::<BigStruct>(1024);
}
```
然而,在实践中这两者都是错误的:它们各自分配了 32KB!在我们的应用程序中,有两个特定区域因此产生了相当显著的性能影响。
## 发生了什么
为了更深入地探究这一问题,我搭建了一个小型 Playground(https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1a0da74fc4e775c30fb461f2b5090047)来分析来自 `mpsc` 的内存分配。结果相当有趣:
| msg_size | capacity | heap_after_create | heap_after_fill |
|----------|----------|-------------------|-----------------|
| 81 | 800 B | 800 B | 816 |
| 800 B | 800 B | 1024 | 9728 B |
| 128 | 14640 B | 4640 B | 128 |
| 164640 B | 4640 B | 128 | 1024 |
| 4640 B | 132608 B | 1024 | 133312 B |
| 33312 B | 1024 | 16 | 33312 B |
| 33312 B | 1024 | 1024 | 33312 B |
| 1050112 B| | | |
这里我们有:
- `msg_size`:结构体 `T` 的大小。
- `capacity`:有界 `mpsc` 通道的容量。
- `heap_after_create`:创建后立即分配的内存。
- `heap_after_fill`:我们在通道上发送 `capacity` 个消息将其填满后立即分配的内存。
这里有几点引人注目:
- 通道的容量对初始分配大小没有影响。
- 即使开始发送消息,在达到某个特定点之前(剧透:是在 32 条消息之后),内存不会开始增长。
- 通过简单的计算可以看出,初始成本为 `(msg_size * 32) + 544`。因此存在一个固定成本,以及一个基于消息大小的倍数。
### 实现细节
查看 Tokio 源代码可以很容易地找到这个 `32` 的倍数。该通道由 `Block`(块)链表构成(https://github.com/tokio-rs/tokio/blob/bdcea6b2cd716b0a378626796bf5dc049608663d/tokio/src/sync/mpsc/block.rs#L31)。每个 `Block` 存储 `BLOCK_CAP` 个 `T`(https://github.com/tokio-rs/tokio/blob/bdcea6b2cd716b0a378626796bf5dc049608663d/tokio/src/sync/mpsc/block.rs#L47),其中 `BLOCK_CAP` 被硬编码为 `32`(https://github.com/tokio-rs/tokio/blob/bdcea6b2cd716b0a378626796bf5dc049608663d/tokio/src/sync/mpsc/mod.rs#L140)。因此,分配 `mpsc` 基本上就是分配一个 `[T; 32]`。剩余的 544 字节固定开销来自通道的其他部分,我没有进行深入分析。
## 实际影响
### 大量微小通道
对我们而言,第一个实际影响在于我们为集群中的每个 Kubernetes `Service` 对象创建了一个通道,用于发送有关服务健康状态的事件。在许多环境中,这些对象的数量高达数千。我们在每个通道上发送的消息很小(仅 24 字节),并且对这些通道的吞吐量和延迟要求非常低。然而,正如我们上面所学到的,每个通道占用了 `1312` 字节!我们将此迁移到了另一个通道库 `futures-channel`,它每次只分配一个 `T`(代价是吞吐量下降,但这与我们无关)。最终结果是在代表性测试中将我们的整体内存使用量减半:
Agentgateway 优化前后的内存
### Hyper 连接
当使用 Hyper(https://hyper.rs/)时,我们看到每个连接会产生 16KB 的内存分配。当服务于数千个连接时,这累积起来相当可观。其中一部分显而易见:每个连接都有一个硬编码的缓冲區 `INIT_BUFFER_SIZE: usize = 8192`。然而,另外 8KB 正是源于同一个 `mpsc` 问题!每个 `SendRequest`(用于发送请求的 API)都利用了一个分发 `http::Request` 的通道。每个请求通常约为 `250` 字节,乘以 32 就得到了剩余的 8KB。与我们之前的用例不同,此代码路径对延迟/吞吐量非常敏感。前端分配对端到端基准测试有显著影响(https://github.com/hyperium/hyper/issues/4057#issuecomment-4346493767),但我们实际上一次只需要通道中有 1-2 个项目——32 的块大小几乎完全是开销。此问题在 Hyper issue #4057(https://github.com/hyperium/hyper/issues/4057)中被跟踪。
相似文章
Lobsters Hottest
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。
Anthropic Engineering
本文来自 Anthropic,探讨了如何将代码执行与 Model Context Protocol (MCP) 相结合,以提升 AI 智能体的效率。文章分析了工具定义和中间结果导致的 token 过载等挑战,并提出代码执行作为降低延迟和成本的解决方案。
Reddit r/artificial
本文探讨了 AI 智能体设计中的局限性,指出仅靠增加记忆容量不足以解决智能体构建与运行机制中的根本性架构问题。
Product Hunt
<p>通过自剪枝 MCP 记忆,Token 浪费减少 84%</p> <p> <a href="https://www.producthunt.com/products/yourmemory?utm_campaign=producthunt-atom-posts-feed&utm_medium=rss-feed&utm_source=producthunt-atom-posts-feed">讨论</a> | <a href="https://www.producthunt.com/r/p/1128311?app_id=339">链接</a> </p>
Lobsters Hottest
NearlyFreeSpeech.NET 将其生产环境的C++前端基础设施(nfsncore)重写为Rust,该系统负责所有传入请求的路由、缓存和访问控制。迁移的动机是Rust的安全性保证、性能、生态系统优势以及老化的C++代码库的局限性。