大家好,我是王普,来自DaytonLaw的今天很高兴给大家介绍一下我们用Rust一部空架实现RDM编程这方面的工作先简单自我介绍一下,我名字是王普然后我在Google美国工作了一些年然后后来我回国,现在我创建了DaytonLaw的DaytonLaw是一个高性能的开源分布式存储平台我们打造DaytonLaw的有一个很重要的特性,就是要实现高性我们怎么实现高性能呢,对存储平台来讲是要通过软件和硬件的联合深入优化来实现高性能这个是我们在github上开源项目的地址DaytonLaw的软件部分是由Rust写的然后DaytonLaw硬件部分主要是实现RDMA还有NVME Over Fabric这些都是高性能存储常用的技术那今天我的套格主要分这么几个部分先简单介绍一下Rust和RDMA然后介绍一下我们为什么要用Rust的异部框架来做RDMA编程然后最后详细介绍一下我们用Rust的异部框架主要解决的RDMA编程里面的一个痛点就是RDMA的内存管理先简要的介绍一下Rust的一个很大的特性就是Rust它从与法的角度实现了内存安全和现存安全所以的话用Rust来实现一个复杂的系统比如说多线程分布式高并发这么样一个系统用Rust来实现的时候它的开发效率会很高调试的成本会很低所以像Linux内核这样复杂的操作系统操作系统内核Linux内核社区也注意到Rust的这些优良的特性所以Linux内核社区现在也开始接受Rust来实现一些一些Colonel里面的Module这是Rust的一个简要介绍那接下来是Rust的异部框架Rust异部框架它主要有三个主从部分异部的执行器还有一个唤醒器然后还有一个工作工作线程池Rust的异部框架里面把每一个IO任务都抽象成一个异部任务这个异部任务由异部执行器来调动这个异部任务比如说它被阻塞了它要等待其他的其他的IO世界发生异部任务才能执行下去那么异部任务就要被阻塞了当异部任务所等待的IO世界发生那么由唤醒器来唤醒异部任务唤醒之后接下来Excutor执行器再把唤醒的异部任务放到工作线程池里面继续执行这是Rust异部框架简要的介绍Rust异部框架非常适合IO编程因为首先Rust的整个异部编程框架它里面的工作线程池它是无组色的即便异部任务本身对吧因为它要等待IO世界发生异部任务本身可以被阻塞但是Rust的编程框架是异部框架里面的工作线程池是不会被阻塞的因为当一个异部任务阻塞之后这个任务就进入休眠了异部的执行器会给工作线程让给它分配另外的任务去工作所以异部编程框架Rust异部框架它的工作线程池本身是不会被阻塞的然后另外一个没有上向文切换因为不同异部任务之间的切换是Rust的异部框架里面的执行器来执行的所以它整个异部任务切换都发生在诱惑它然后Rust异部编程框架有个很大的特点就是它采用同步的编程风格来实现异部的业务逻辑也就是同步的编程风格串形之形很符合我们人类的思维方式传统的异部编程方式都是靠回调那整个异部逻辑散落在不同的回调函数里面就很支离破碎代码的维护成本很高可读性不好那Rust异部编程框架最大好处就是它采用同步的风格来实现异部的逻辑那让整个代码看上去看上去像是串形执行但其实是异部执行极大地降低了Rust异部编程的复杂度减少了挑适成本提高了这个开发效能所以Rust异部框架非常适合Io任务Io方面的编程好 接下来再简单介绍一下RDMARDMA是一个高性能网络框架RDMA最大的特点就是它的数据通路bypass kernel就是RDMA在数据传输的时候数据不会经过内核所以RDMA的数据传输的时候它有这么几个特性它是无组色它是底线RDMA的硬件直通到用户台的内存然后因为RDMA这个数据传输的时候它又不经过cernel所以也没有上线文切换这个代价然后再一个因为RDMA这个数据通路在数据传输的时候不经过cernel所以它也实现了领考位就RDMA传输的数据直接从硬件网卡上就传输到了用户台的内存空间所以RDMA由于这些特性实现了一个非常高兴的网络协议站它比tzpip网络传输性高很多所以RDMA经常用在超算中心或者高端存储这些领域然后接下来简要介绍一下RDMA的它传统的编程接口它传统API叫verbs所以RDMA API接口的名字是verbs它是用C写的下面这是一个RDMA Verbs接口的一些例子从这个例子可以看出RDMA Verbs接口它有一些很显著的缺点首先到时候都是罗指针这都是罗指针这些罗指针带来了很多问题首先它罗指针就不是类型安全的比如说这有Voy的新的指针Voy的新的指针可以转换任何类型所以Voy的新的指针它就本质让它们不带任何的类型信息所以Voy的新的指针做不到类型安全再一个这些罗指针罗指针它做不到内存安全因为罗指针本身并不表征它指针指向的内存空间现在是还能继续使用还是说已经被释放掉了不知道所以罗指针本身也不保证内存安全然后再一个这些2DMA Verbs这些编程接口太过于底层比如说对于内存的管理几乎都是手动管理比如说我要创建一个什么数据结构掉创建函数创建一个2DMA的数据结构销毁的时候那就还有相应的DLK的这些函数去销毁这数据结构所以基本上创建的删除都是手动实行的那这个就很不好地吧当你这个代码复杂了之后很容易忘掉说这个数据结构在哪里这个创建的那它有没有被释放这很不容易搞清楚所以这就是说RDMA它用C写的API接口 Verbs使用很不友好那这也就是说为什么我们要Rust来做RDMA编程就首先我们为了方便Rust开发者来做RDMA编程我们用Rust实现了一个RDMA的一步的编程接口这是我们用Rust实现的一步编程接口在github上开刃的地址我们用Rust实现RDMA一步编程接口的时候不只是简简单单的把Rust这些C语言的函数用Rust重新定义一遍更多的我们是按照Rust的一些语法语言的特性来实现RDMA编程首先就是要调试友好不要太过底层把很多底层的细节要封装掉再一个内性安全和内存安全我们借助于Rust的语法特身来保证在RDMA做编程的时候用Rust做编程的时候能够实现内性安全和内存安全RDMA本身的API Verbs它是支持现成安全的所以这样的话我们用Rust实现RDMA编程它的用Rust封装了一个RDMA的API接口我们提供内性安全和内存安全然后再把底线很多RDMA接口太过于底层的细节封装掉这样的话让整个Rust的开发者在开发RDMA程序的时候就会很友好调试成本比较低符合Rust的语法语言的特性另外一个为什么我们要把用Rust实现RDMA的API封装做成一部的方式本质还是因为一部的编程方式对于IO更加的友好所谓对IO更加友好什么意思呢就是能够实现更高的性能首先RDMA本身它的数据通路是无组色的也是没有上前文切换的当然RDMA的数据通路还实现了领考RDMA在做开发的时候RDMA的应用程序因为它是RDMA本身它是一个网络应用它本质属于IO应用IO应用涉及到通过RDMA去做数据的传输所以RDMA的应用用户的逻辑就要把共享的数据通过RDMA传输数据的数据要准备好比如说从硬盘读一块数据到内存然后内存的数据通过RDMA共享给远端那么从本地硬盘读一块数据到内存这个操作用ROS的异布来实现就可以实现无组色和没有上前文切换前面介绍ROS的异布框架的时候介绍过ROS的异布编程框架可以实现无组色和没有上前文切换这样的话用了ROS的异布编程框架再加上RDMA的特性能让整个RDMA应用它整体都能够实现无组色和没有上前文切换这会大大的提高Io的效率Io的性能所以为什么要用ROS异布框架来实现RDMA编程本质就是要提升Io的性能好接下来介绍一下RDMA在做内存管理的方面的一些核心概念这个是RDMA API接口Verbs里面定义的一些核心概念首先RDMA在做内存管理的时候通过Verbs接口它的API接口来实现内存管理的时候基本上都是手动来管理首先它有三个核心概念首先是Protection的保护域的概念保护域是用于在不同的RDMA程序不同的RDMA进程之间来做隔离域或者可以更细腻懂不同的线程之间隔离都可以所以Protection董妹也是个保护域也是个隔离域让Protection董妹里面的这些内存只能被同属于同一个Protection董妹下面的这些数以结构可以访问跨Protection董妹是不能访问这些里面的内存这些数以结构另外一个Protection董妹里面可以注册多个Memory Region多个Memory RegionMemory Region它的注册创建是一个重型操作因为它要陷入内核因为RDMA要求Memory Region它申请的内存必须常驻在内存里面不能被交换到硬盘上不能被Swap硬盘上因为RDMA它只能访问内存它不能直接访问硬盘所以这就是RDMA它本地的内存它要求常驻在内存里面所以Memory Region创建和销毁操作都是很重的操作然后为了降低Memory Region它的创建删除很重的开销RDMA又引入了一个概念Memory Window在Memory Region里面可以再创建多个Memory Window不同的Memory Window它来把Memory Region这一大块内存再划成不同的小的块给它不同的数以结构去使用Memory Region的创建删除销毁都是比较轻量的操作所以RDMA的内存管理三个核心概念Protection DominionMemory Region 还有Memory Window但是RDMA内存管理的时候复杂度比较高它很多操作太过于底层了比如说Memory Window创建它要好几个步骤首先先做Allocate的Memory Window这个数结构本身先创建出来然后再把Memory Window绑定到Memory Region上但Bind的操作执行完了之后不代表Memory Window绑定到Memory Window绑定成功了不是的而是说Bind的本身它其实是个异步操作它函数执行不代表Bind的操作执行完成Bind函数调整完之后需要RDMA用户程序再去伦询去不停的查查这个Bind的结果最后查出来Bind是否成功Bind成功以后才能真正开始使用Memory Window执行的这款内存所以就是说Bind的Memory Window这个操作它是异步操作所以它非常清凉但是它有时候有很大的代价就是说它开发复杂度高后面真正Bind的成功没有要用户程序自己去伦询出来所以这是个很detail很low-level的一个变成方式它让开发的复杂度增加了很多所以这是Verbz这个结果很大的一个缺点非常底层那我们用Rust来实现RDMA内存管理的时候我们就是希望说把很多底层细节都封装掉所以首先我们就是采用Rust的标准库里面定义的Allocator这个Trade内存分配器这个Trade用Allocator这个Trade来定义Memory Region这个数据结构这样的话用Memory Region这个数据结构去封装它的创建和销毁这些RDMA这些Verbz这些接口但是这么做有个很大的缺点就是采用Rust的标准库的Allocator内存分配器这个Trade来定义Memory Region它就丢失了内存使用的上下文怎么理解呢就是比如看这个例子Memory Region这是我们按照Rust的标准库里面的它的Allocator内存分配器定义的这么一个数据结构Memory Region这个分配器定义出来的分配器好 传给Rust的另外一个数据结构叫Vac这个叫VacterVacter这个数据结构在Rust里面非常常用那它要创建内存创建要申请一块内存的时候它就找Memory RegionMemory Region创建好之后传递给了Vac但是当Vac第一次创建好之后逐渐往里面写写数据快写满了Vac会做Reallocate它会再申请一块内存再申请一块更大的内存这是新的内存再把原来老的内存数据拷贝到新的内存拷完之后再把老的内存释放掉但是Vac的语意和RDMA的语意是冲突的因为RDMA要求Memory Region它在内存里面是固定的位置必须是固定的不能被移动因为远端要来访问Memory Region你Memory Region的本地的内存位置发生变化远端不知道所以远端过来一访问就变成非法访问了但是Vac这个数据结构它显然是不感知RDMA这个语意的所以这样的话如果把Memory Region采用RAS的标准库的内存分配系这个方式来实现的时候它无法感知是哪个数据结构是什么场景来找Memory Region来分配内存所以我们的方式是说我们自己定义了Memory Region这个数据结构它完全遵从RDMA的语意来管理内存也就是Memory Region我们自定义的数据结构它完全能够感知到是什么场景下来做内存分配比如说举这个例子RDMA原子操作的例子Tomic原子操作的例子原子操作它在做内存分配的时候它要分配一个8个字节的一个内存空间也就是一个64位的整数但这64位整数8个字节内存空间RDMA的Tomic是有要求的要求它必须按照8个字节对期也就是说RDMA原子操作它申请这块内存空间它在Memory的Layout上是有要求的要求按8个字节对期因为Memory Region这是我们自定义实现的所以我们可以完全按照RDMA这个语义这个特殊的语义来给比如说Tomic操作来分配一块内存这样的话一方面也符合RUS的语义对吧分配内存那回收的时候会自动回收这都是RUS的语法特色保证的而且我们自定义Memory Region这个数据结构能够完全的遵从RDMA的语义这是用RUS的来实现RDMA这个内存管理对一个对一个很重要的方式要感知RDMA的使用场景另外我们还用RUS的异部异部框架来实现在RDMA内存管理里面很重要的一个功能就是要来管理Memory RegionMemory Region它的生命周期并且把Memory Region的生命周期跟远端进行同步因为RDMA本质它是把一块内存空间和远端之间进行共享所以RDMA本地的Memory Region它内存的生命周期远端是一定要知道的不然远端来访问的时候本地内存已经消亡了远端访问就是个非法访问就失败了但是Memory Region它的生命周期在本地和远端之间进行同步这件事情进行沟通这件事情在RDMA的API接口Verbus里面是缺失的它要用户自己来实现这样的话它的用户来做RDMA变成复杂度就高很多了RDMA用户开发的时候还要自己来管理内存的生命周期和远端之间进行同步这个复杂度是比较高的我们用ROS的一部空甲来封装RDMA的API接口里面很重要的一块工作就是我们实现了RDMA内存的生命周期管理而且生命周期和远端是可以进行同步的我们怎么实现呢我们对本地的Memory Region它创建的内存它跟内核申请到这块内存我们给每个Memory Region都挂一个Atomic Reference Counter一个原子的引用技术器这样这块内存当它要共享给远端的时候它首先要注册给ROS的一部空甲一部支援器这样的话注册进来以后这块内存的RF Counter就要加一这样的话一部支援器杂有一个对Memory Region内存的一个引用技术之后即便用户的程序把这块内存销毁了但这块内存因为它的引用技术不可能真正变成零因为一部支援器杂还有一个引用技术所以只要引用技术不为零这块内存就不会真正销毁只有当引用技术为零这块内存才真正销毁那这样的话用户怎么来操作这块内存对于远端来访问的时候都是安全的因为这个一部支援器它还有一个指向内存的引用这是第一部注册这块内存注册给一部支援器之后一部支援器通知远端这个通知可以是点对点也可以是通过一个中间的代理来通不给远端通知远端远端机器拿到内存的通知之后它过来访问内存访问结束之后通知一部支援器一部支援器发现这块内存使用完了返回给用户程序另外还有个机制就是当一部支援器接受到内存的注册之后它通知到远端同时它还要有一个胎貌的超时机制就在超时之内远端访问这是合法的超时之后远端再来访问就是非法的了因为超时机制保证这块内存它不会被泄漏如果没有超时机制这块内存注册到了一部支援器然后也通知远端但远端迟迟不来访问这块内存如果没有超时机制就一直挂在一部支援器下面所以我们用超时机制保持这块内存不会被泄漏这样一个方式通过这样一个机制我们就实现了Memory Regions它的生命周期会同步给远端的机器这样的话上面的LDME的开发者就不需要再去关心Memory Regions它的生命周期怎么跟远端同步的问题它就只要创建内存使用然后它也不用关心释放释放都是Ross的语法特性包括Reference Counter这些来保证内存的自动释放但这个机制也有一个缺点就是Atomic Reference Counter它是个原子操作它引来一些额外的开销所以就是说如果Memory Regions这个内存它不是频繁的床间删除Atomic Reference Counter带来的开销还是可控的可以接受的好我就介绍这么多细节大家如果感兴趣我们在用Ross的异部编程框架实现的RMA API接口这方面的内容的话可以扫这个RMA添加我们的微信群多谢大家联听大家有什么问题可以问我好谢谢