GKFS源码阅读笔记:服务器说明

avatar
作者
猴君
阅读量:1

GKFS的客户端不能独立运行,它只是作为一个动态链接库,被动地由使用它的应用程序调用,与客户端不同,GKFS的服务器实现为可以独立运行的守护进程,因此和普通可执行程序一样,从main()函数开始执行。不过,由于截屏范围有限,这里只展示其主要的关键的工作。

主函数接受并解析命令行参数,用来初始化ADAFS_DATA这个类的成员,其中挂载目录和根目录是必须的参数。挂载目录是客户端访问GKFS文件系统的地方,只有当客户端打开的文件在GKFS挂载目录下时,针对该文件的调用才会由GKFS处理;根目录是GKFS在服务器节点上存放数据的地方,客户端放在挂载目录下的数据,实际存储在根目录下,在服务器上。元数据目录不是必选项,如果没有指定元数据目录,那么元数据和数据一起放在根目录下。

和客户端一样,服务器也有日志记录器。服务器的启动工作之一就是初始化日志记录器。

和客户端的CTX类似,ADAFS_DATA也是一个定义的宏,用于获取FsData的实例,FsData的实例整个服务器进程只有唯一的一个,日志记录器就封装在FsData中。

FsData是服务器一个重要的数据结构。FaData封装了根目录和挂载目录的路径,这是客户端和服务器都需要知道的信息。FsData中最关键的是元数据指针mdb_和块存储指针storage_,读写数据和元数据都要通过FsData,通过这两个指针。

GKFS使用RocksDB来存储元数据,RocksDB是一个键值存储数据库,针对SSD优化,GKFS本身是一个突发缓冲层的文件系统,现代超算的突发缓冲层大多以SSD作为存储设备,所以RocksDB很适合GKFS。元数据库以目录和文件的全路径为键,元数据为值,所以它的名字空间是扁平的,因此具有很好的扩展性。

GKFS的文件划分为一个一个的块,块是GKFS的基本读写单位。对文件的一个读写范围的操作,最终实现为对相应块的操作。

read_chunk()用于读一个块。参数中的file_path和chunk_id结合,说明要读的是哪个文件的哪个块,buff指向一个用户空间的缓冲区,从存储设备上读出数据,把这些数据送到这个缓冲区中,offset说明要从一个块的哪个位置开始读,size说明读的大小,eventual是一个ABT_eventual对象,关于这个对象,稍后再讲。

读一个块,首先断言读的范围不超过块的范围。断言是个好习惯,在独立编写一个模块的时候,你不能保证调用这个模块的人传递的参数都是合理的,所以你可以先断言一下,为后面的代码提供保证。确保读的范围合理之后,把文件名和块号拼接在一起,作为本地一个块文件的路径,使用open()以只读模式打开块文件,因为一个块有512kb之大,本地文件系统的pwrite()一次可能读不完,所以在一个循环体中多次调用pwrite(),直到读完所有数据。最后,read_chunk()使用ABT_eventual_set(),配合eventual返回总共读取的字节数,这里,把字节数同步给调用read_chunk()的函数,使用的是最终一致性模型。

eventual是一个ABT_eventual对象,这是Argobots这个线程库提供的一个信号量,它的用法如下图所示:

假设有A、B两个线程,A要使用最终一致性模型等待B对变量 val 的更新,那么A首先要声明并初始化一个ABT_eventual对象,说明变量 val 的大小。这里本地线程间的通信实际上是通过共享内存来实现的,这个用于通信的共享内存要知道大小,才能预先开辟,所以ABT_eventual_create()这个函数除了eventual对象的指针,还要告诉它变量所占内存的大小。eventual对象实际可以理解为一个用于线程间同步的信号量,它和线程B的参数一起打包在一个结构体task_args当中,使用ABT_task_create()创建一个线程来运行B,并通过task_args的指针向B传递参数,这里,eventual也传递给了B。B独立地定义了一个变量val,这个变量的作用域只在B当中,A是看不见的,但是A和B之间应当协议好变量的大小,B在对val进过一系列操作之后,最终使用ABT_eventual_set(),通过A传递过来的eventual对象,将val的值写入共享内存,所以在B中,val的类型和A一样,也是TYPE(类型一样意味着大小一样)。

那么,怎么理解val这个变量的最终一致呢?B和A是并行的,A需要val这个值,但是这个值由B来计算,所以A使用ABT_eventual_wait()阻塞自己,等待B的计算结果。在B中,val经过了许多次更新,但并不是像强一致模型那样,每次更新都同步给A,而是在所有事都做完之后,把最终的版本发给A,这就是最终一致,最终一致降低了线程间的通信开销,GKFS具有线性可扩展性,其中一个原因就是它放宽了POSIX-I/O的强一致性,否则,随着线程或进程的数量增多,通信开销也跟着增大,逐渐成为瓶颈,导致系统性能并不因为进程增多而提高,甚至可能下降。POSIX-I/O一开始是为本地文件系统定义的接口,不过由于它被广泛使用,所以很HPC应用调用这个接口,很多分布式并行文件系统兼容这个接口,但问题是它面对现代高性能计算的I/O负载,越来越暴露出它对HPC的不合适,现在更提倡使用MPI-I/O的接口。GKFS的一个好处就在于HPC应用不必修改代码就能使用它,GKFS拦截POSIX-I/O的调用,然后在实现这些接口的时候又把POSIX-I/O导致扩展性不高的强一致性放宽了,从而对应用程序提供较好的性能。

write_chunk()的逻辑与read_chunk()类似。不同之处在于,写一个块的时候首先要调用init_chunk_space()用文件名创建一个文件夹,这个文件夹用于存放文件的块,如果文件夹已经存在,那也没什么影响。init_chunck_space是一个私有函数,它只在write_chunk()中被调用。保证文件夹存在之后,使用open()打开块文件才能成功,如果块文件不存在,说明客户端的写是追要加一个文件,如果块文件已经存在,说明这次写是在文件中进行覆盖写,这个覆盖写是客户端配合lseek()调整文件的读写指针发起的。

destroy_chunck_space()和trim_chunk_space()都是对一个范围内的块进行删除,不同之处就在于,destroy_chunk_space()用来响应客户端的unlink()调用,这个调用的语义是指定一个文件名,删除它,实现在GKFS中就是要删除文件对应的所有块,所以destroy_chunck_space()把一个服务器上所有属于该文件的块无差别地删除,什么都不保留。

而trim_chunk_space()响应的是客户端的truncate()调用,这个调用的语义是截断一个文件的尾部,文件的尾部可能是多个块,分散在多个服务器上,使用哈希函数定位到这些目标服务器后,目标服务器执行trim_chunk_space(),因为文件的头部是要保留的,而一个目标服务器上的块,可能是头部块也可能是尾部块,所以不能像destroy_chunk_sapce()那样简单的无差别地删除,还需要做一个判断,遍历一个服务器上的所有块,看它是否落在尾部区间,是的话才删除,否的话则跳过。

还有一个函数truncate_chunk(),它截断一个块的尾部,和trim_chunck_space()配合来实现truncate(),负责truncate()的边界处理。

delete_chunk()删除整个块。

chunk_stat()统计单个服务器上文件系统的信息,并将字节级别的信息转换为GKFS的块抽象。它首先调用本地文件系统的statfs()获取一个路径下的文件系统的信息,需要用到的是该路径挂载的本地文件系统的块大小为多少,一共有多少块,有多少空闲块,这些信息通过sfs这个指针返回。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!