RMDA 是近年越来越热门的高速网络传输协定,被广泛应用于超算核心和高端存储畛域。RDMA 的全称为 Remote Direct Memory Access,即容许本地内存被远端机器间接拜访,该拜访不通过被拜访机器的操作系统,间接由网卡代为实现。正式因为网卡实现了大部分的数据传输工作,操作系统的负载被升高,使得其在大量数据传输的状况下具备更好的拓展性(scalability)。为了保障远端可能正确和平安地拜访本地内存,RDMA 协定中有一系列标准来束缚用户的拜访,上面来简略介绍一下。
RDMA 的内存治理
远端想要拜访本地内存,首先须要本地的“批准”,即本地仅仅裸露想要裸露的内存,其余内存远端则不可拜访。该“批准”操作,咱们个别称为 Memory Region(简称 MR)注册,操作系统在收到该申请时会锁住该段申请内存,避免内存被 swap 到硬盘上,同时将这个注册信息告知 RDMA 专用网卡,RDMA 网卡会保留虚拟地址到物理地址的映射。通过此番操作,由 MR 代表的内存裸露进去了,远端能够对其进行拜访。处于平安的思考,只有被容许的远端才能够拜访,这些远端持有远端拜访密钥,即 Remote Key(简称 RKey),只有带有正确 RKey 的申请才可能拜访胜利。为了内存治理的细粒度化,RDMA 还提供了 Memory Window(简称 MW),一个 MR 上能够分列出多块 MW,并且每一块 MW 上都能够自定义拜访权限。
除了上述中的 MR 和 MW,RDMA 中的内存治理还和 Protect Domain(简称 PD)和 Queue Pair(简称 QP)相干,这里不具体论述这两个概念。下图具体介绍了,这些概念之间的依赖关系:
现有的 RDMA 开发接口,即 InfiniBand Verbs 接口(简称 IBV 接口)并没有显式地展示这种依赖关系,但在理论应用中,任何不按规定程序的资源开释都会造成谬误,而用户找到问题的根本原因则十分艰难。更进一步,当 MR 或者 MW 中的任何内存段被应用时,对应的 MR 和 MW 都不应该被开释或登记,这些在原有的 IBV 接口中也很难规范化。Rust 作为一门内存平安的语言,在解决相似问题上具备人造劣势,接下来咱们来剖析如何用 Rust 解决上述问题。
利用 Rust 个性治理 RDMA 内存
Allocator API
在 Rust 的 nightly feature 中有一个叫 Allocator API,这个 feature 容许用户创立本人的 Allocator,之后创立堆上数据时能够制订应用用户定制的 Allocator。大家很天然能想到,MR 或者 MW 很适宜作为一种 Allocator,后续用户创立的 Vector 或者 Box 都能够应用其中的内存,这些内存能够间接凋谢给远端拜访,既不便又高效。
但 Allocator API 有个外围问题无奈保障,即 Allocator 自身应该比所有从其调配的数据活得更久,不然就会产生数据拜访不平安的问题,如 use after free 问题。下列例子很好得论述这一问题:
fn alloc_vec<'a> () -> Vec<u32, &MemoryRegion> {
// Allocat missing
let alloc = CusAllocator::New();
let mut v = Vec::new_in(alloc);
v.push(1);
v
}
fn main() {let v = alloc_vec();
println!("{:?}", v);
}
在 alloc_vec 办法中咱们创立了一个新的 Allocator 叫 CusAllocator,在办法完结时,该 Allocator 曾经被开释,应用其内存的 vector 依然存活着,被后续应用。Rust 语言无奈判断出潜在的危险,惟一可能解决该问题的方法就是将 CusAllocator 变成 static 变量,这样其生命周期和整个程序一样长,也就不存在 use after free 的问题。然而该解决办法不实用 MR 和 MW 的场景,起因是 MR 和 MW 会随着用户的应用动静注册和登记,无奈被登记的 MR 和 MW 会影响应用的便利性。若初始化太大的内存块,零碎的内存压力太大,其余程序容易触发 OOM 问题;若初始化内存块太小,用户的应用会受到限制。联合上述思考,Allocator 不是一个可行的计划。
Reference 还是 Reference Count
Rust 语言中 Reference 带有生命周期属性,非常适合用来治理依赖关系,即被依赖 Ref 的生命周期不短于依赖者的生命周期,然而其在解决自援用时十分艰难,当结构复杂到肯定成都仅仅依赖 Reference 很难设计出用户易于应用的接口。因而咱们采纳了下列的设计形式:
struct MemoryRegion {
pd: Arc<ProtectDomain>,
...
}
这样外围数据接口都放到了堆上治理,同时保障了被依赖的数据结构肯定不会提前开释 —— RC 个性的保障。解决了外围数据结构的治理,内存应用的治理则更加简略,下列办法保障了当有内存被应用时,MR 或者 MW 肯定不会被开释。
impl MemoryRegion {pub fn get_ref(&self, offset: usize, len: usize) -> &[u8] {
// Just a demo, missing length check
&self.buf[offset..(offset + len)]
}
}
在此基础上,配合一些序列化办法,Memory Region 则能够解决各种数据结构的传输。
远端拜访
RDMA 是为了远端数据拜访而存在的,仅仅治理好本地内存还不够,如何保障远端拜访时本地内存的可靠性也很重要,不过 Rust 语言自身的个性只可能保护本地内存的安全性,近程拜访须要更下层的设计来实现。咱们在咱们的设计中提供了相似的接口来实现相应的工作:
impl MemoryRegion {async fn reserve (&self, timeout: Duration, f: T) where T: Future {timeout(timeout, f).await
}
}
timeout 示意该 MemoryRegion 内部能拜访的最长时间段,如果 f Future 提前结束,则咱们能够提前回收 MemoryRegion,否则至多期待 timeout 的工夫长度能力回收。其中 f 能够在以下场景进行不同的操作:
- 在一对一传输的场景中,f 将传输的必要信息传递给对方,期待对方实现的回复,一旦收到回复则完结 future。
- 在一对多传输的场景中,f 将传输的必要信息放到某个看板上,而后期待 timeout 工夫的完结。
这里之所以要在接口中固定一个 timeout,是为了避免内存被无限期得占用不可能开释,最终造成内存泄露。例如上述的第一个场景,对方如果因为软硬件的问题程序完结前并没有给出回复,则 timeout 至多能够保障 MemoryRegion 在 timeout 时间段之后还有开释的机会。
值得注意的是,这里的机制并不能完全避免远端拜访谬误的产生,本次程序无法控制远端程序的弱点依然存在,因而,RDMA 自带的爱护机制也可能防止谬误数据拜访的产生,相应申请的失败会带来一些性能损失,这是无奈防止的 tradeoff。
总结
Rust 语言的内存安全性局部地解决 RDMA 内存治理问题,同时下层的应用接口设计也局部解决了 RDMA 远端拜访的治理问题。欢送在咱们的 Rust RDMA 封装我的项目 交换探讨,促成我的项目倒退,使得 Rust 社区可能更不便地应用 RDMA 网络。
作者 | 施继成
转自《Rust Magazine 中文精选》