Rust中处理部分借用的最佳实践困境
核心问题
在 Rust 开发中,有一个长期困扰开发者的难题:当结构体包含多个关联的数据字段时,由于借用检查器无法理解跨函数边界的部分借用,一些看似合理的代码模式常常无法通过编译。
具体案例
考虑以下数据结构:
struct Data {
stuff: Vec<u32>,
queue: Vec<u32>,
}
假设我们有一个 process_all 方法,需要在遍历 self.stuff 的同时,调用另一个会修改 self.queue 的 process 方法。这时,编译器会报错:无法借用 self,因为 self.stuff 已被借用。
开发者的困惑
这种情况引出了几个常见疑问:
- 是否应该放弃使用结构体,改为分别传递各个成员?
- 是否需要根据每个具体场景手动拆分结构体?
- 到底有没有一种高效且地道的处理方式?
一位拥有两年以上 Rust 开发经验的程序员分享了他的挫败感:
- 这个问题严重影响了开发体验,甚至让他考虑放弃使用 Rust。
- 他感到沮丧,因为结构体本应是基础的数据分组和抽象单元。
- 他的目标只是“把功能实现”,但似乎总要在性能与代码组织之间做出妥协(例如使用中间 Vec 传值或按需拆分数据)。
原讨论地址:https://old.reddit.com/r/rust/comments/1rg3ftw/whats_the_most_idiomatic_way_to_deal_with_partial/
Apache Iggy 迁移到基于 io_uring 的线程核心架构
背景
Apache Iggy 是一个将性能作为核心设计原则的项目。在其原有架构达到硬件性能极限后,团队开始寻求新的方法来突破瓶颈。
迁移动因
原有架构的问题:
Apache Iggy 最初使用 tokio 作为异步运行时,采用了多线程工作窃取(work-stealing)执行器模型。这一模型带来了几个限制:
- 缺乏精细控制:任务在哪个线程上执行不可预测。
- 缓存失效:任务在工作线程间迁移会导致 CPU 缓存效率降低。
- 执行路径不可预测:不利于进行深度的性能调优。
关键痛点:处理块设备I/O
tokio 基于 epoll 的通知机制在处理常规文件(块设备)I/O 时存在固有缺陷:
- Linux 内核将常规文件视为始终“就绪”,这使得基于“就绪”通知的 epoll 机制不适用。
- 因此,文件 I/O 操作实际上仍会阻塞执行线程。
- tokio 依赖一个线程池(默认最多512个线程)来处理这些阻塞操作。
- 对于 Apache Iggy 这样的高性能系统,很容易就会耗尽整个线程池的容量,成为性能瓶颈。
新架构:线程每核心无共享模型
为了从根本上解决问题,Apache Iggy 转向了 线程每核心(Thread-per-Core)无共享架构。
核心理念:
- 将单个线程固定绑定到每个 CPU 核心。
- 通过启发式方法(如哈希)对数据资源进行分区。
- 消除或极大减少共享状态,从而减少锁竞争。
- 显著提高 CPU 缓存的局部性。
- 线程(或称为“分片”)之间通过消息传递进行通信。
关键改进:
- 从“工作窃取”到“工作引导”:任务被预先分配到固定的核心线程上执行,避免了迁移开销。
- 采用 io_uring 实现真异步I/O:这是解决文件 I/O 阻塞问题的技术关键。
io_uring 带来的优势
io_uring 是 Linux 内核提供的高性能异步 I/O 接口,其设计哲学与 epoll 有本质不同:
- 基于完成的通知:应用程序提交 I/O 操作请求后,由内核驱动其完成并通知,而非等待“就绪”信号。
- 高效的核心机制:通过用户空间和内核之间共享的两个无锁环形缓冲区实现零拷贝通信:
- 提交队列(SQ):应用将 I/O 请求放入此队列。
- 完成队列(CQ):内核将已完成的 I/O 操作结果放入此队列。
- 虽然 io_uring 的完成回调模型与 Rust Future 的轮询(poll)模型在理念上不完全吻合,但其带来的性能提升远超这点微小的适配开销。
这次 后端 & 架构 层面的深度重构,使得 Apache Iggy 能够充分发挥现代硬件的潜力,尤其在高吞吐、低延迟的 I/O 密集型场景下表现更为出色。如果你想深入了解高性能系统设计的更多实践,欢迎到 云栈社区 与更多开发者交流探讨。
项目博客原文:https://iggy.apache.org/blogs/2026/02/27/thread-per-core-io_uring/
|