TFS数据服务器分析

TFS主要用于小文件存储,其将多个小文件存储在大的block中,并为这些小文件建立in-block索引,优化了文件的存储空间和寻址过程。TFS为了保证用户数据的安全性,为每个block在不同的数据服务器(dataserver,ds)上创建多个副本,另外,TFS还支持多集群数据同步。

TFS提供给用户使用的基本接口类似于vfs提供的文件系统,主要包括open、read/write、close、lseek、外加save_file、fetch_file等一些封装的接口。

用户往TFS里存储一个文件,须经历open、write、close三步,本文以ds为主线,分析了对应客户端的一次文件存储过程,ds都做了哪些工作来存储文件。

open过程

如果要写文件,客户端须以T_WRITE的模式调用open接口,首先client会连接到ns(ns的地址在初始化或open的时候指定),ns接到请求后,会为本次文件写请求分配一个逻辑块(如果文件尚不存在),将逻辑块号、及该块对应的ds信息返回给client(根据副本数的配置,一个逻辑块可能对应多个ds上的物理块,逻辑块的创建过程不是本文的主题,暂不讨论)。ns在分配某个逻辑块用于写时,就会将该逻辑块锁定,直到写操作完成或失败,或是超过租时间该写操作没有响应信息。

到这里,client知道了新的文件应该存储到哪些ds上,并以ns返回的第一个ds作为主ds,主ds的地位非常重要,其负责将文件数据转发到其他的ds上,同时如果配置了多集群的同步,在成功写完文件数据之后,其还会将文件写到集群中。(读的时候client 根据fileid选择一个作为主ds,规则为fileid对ds个数取模)。

TFS文件名由集群信息、文件的blockid、fileid以及suffix信息(可选)编码而成,在open阶段,client还需要做的一件事就是获取文件对应的fileid。client将文件的blockid、fileid(新创建的文件为0)发送给ds,ds收到请求后,会调用DataService::create_file_number处理请求。

create_file_number的主要任务是给文件分配一个seq_no和一个file_number,其中seq_no是block内部唯一的,初始值为0,随着block中文件数量的增加不断递增,该值为存储在block索引文件的头部中,fileid是由seq_no和suffix的hash值计算得出,分配了seq_no就相当于得到了文件的fileid。那么file_number又是干什么用的呢?

TFS文件的write过程实际上只是把文件数据写到了多个ds的内存中(或临时文件中),并没有实际的存储数据,在用户最终调用close的时候才会将数据刷新到磁盘中。而ds通过一个DataFile的结构来临时存储write过程中写的数据。DataFile结构包含一个固定大小的缓冲区,当write写的数据超出缓冲区的大小时,DataFile会将其写到临时文件。

当一个ds上有多个写请求时,就会有多个DataFile结构,每个DataFile由一个file_number标示,所有的DataFile存储在DataManagement:: data_file_map_中,这个map就是file_number到DataFile的映射。而open时分配的file_number也就是用来在ds上找到对应的数据缓冲区的,后面client在调用write时会带着该file_number。

open过程与ds交互的工作可以简化write时ds的工作,但该过程是可以合并到write中完成的,目前ds的读写对系统整体的影响并不大,这里既是优化了应该也没什么效果。

write过程

open调用成功后,client就可以调用write接口写数据,之前提到过,write写的数据只会存储在ds的内存(或临时文件中)。Write过程会带上文件的blockid、fileid、buf、size、offset、file_number信息,根据file_number信息,ds首先查找data_file_map_,如果没有找到file_number对应的DataFile,则会创建一个DataFile并将其添加到map中。

接下来,ds会将要写的数据写到DataFile,如果DataFile的内存缓冲区空间不足,则会创建一个临时文件来存储write写的数据。对于主ds(由client决定,会在写请求参数中标示),其写完数据后,还要将数据转发到其他的备ds上。备ds收到主ds转发的请求后,同样会根据file_number找到对应的DataFile存储write写的数据。

这里存在一个问题,假设由A、B两个ds,两次写请求的client分别选择A、B作为主ds(假设两次写对应的两个block的两个副本刚好在A、B上),此时如果两个ds为两个写请求分配了相同的file_number,则在副本同步时就会导致数据冲突问题,该file_number究竟对应哪个写操作为了解决这个问题,必须要求ds间不能分配相同的file_number给client,ds是通过在初始化file_number时,不同的ds从不同的基数开始递增分配(如ds1从1<<32开始,ds2从2<<32开始),则理论上不同的ds是不可能分配相同的file_number的,参见DataService::initialize中的实现。

如果在写数据的过程中,多个副本并没有都写成功、或者副本都写成功但client在后来并没有调用close操作来刷新数据,DataFile的缓冲区回不回一直存在着,从而导致内存空间不能回收使用呢? 答案是否定的,DataFile占用的内存或文件空间会由gc线程负责回收(ds的check线程会周期性的调用gc_data_file),当其发现某个DataFile结构存在的时间超过一定阈值,就将其空间释放掉。

对于多个副本没有全写成功的情况,ds会及时向ns汇报写失败的信息(通过req_block_write_complete),好让ns解锁对应的block,不然就只能等到该block的租约到期,才能解锁,以供分配给其他的写请求。

close过程

close的时候,需要考虑几种情况,如open失败、open成功但write失败。如果open就失败,即使调用了write也不会进行写数据,但close的时候需告知ds,ds再将失败信息汇报给ns,让ns及时解锁block。(为什么不是client直接汇报写失败给ns?)

ds在接收到close请求后,会将数据写到block文件中,同时将该文件对应的索引信息写到索引文件,对于写的详细处理过程,分以下三种情况。

  1. 如果要写的文件尚不存在,则直接将文件追加到block尾部,并在索引文件中添加一条索引记录;block索引结构稍后会介绍。
  2. 如果文件已经存在但新数据不超过文件的当前长度,则直接覆盖原来的文件,在索引中更新一下文件的大小;
  3. 如果文件已经存在,并且新的数据超出了当前长度,采取的策略是把文件数据写到block的末尾,如果block剩下的空间不足以存放文件,则需要使用扩展块。目前,正常block和扩展block的长度都是固定的。

对于第三种情况,如果client指定的写偏移不为0时,会存在问题。在这种情况下,写的数据会写到文件的末尾,如果offset不为0,数据写到DataFile时,0~offset之间的数据没有同步更新,导致0~offset之间的数据是不确定的,而ds在校验CRC时,则使用了整个DataFile的缓冲区,导致与客户端的CRC不匹配,导致close过程失败。

Ds上逻辑块号与物理块号的映射关系,以及逻辑块上对应的多个物理块(一个主、多个扩展)之间的顺序是通过物理块头的BlockPrefix实现的,如下图所示。logic_block_字段为该物理块对应的逻辑块号,而prev_physic_block_id_与next_physic_blockid_为指向前一个和后一个物理块的块号。

 

block索引存储结构

每个物理块对应一个index文件,index用于快速在block中根据fileid定位文件。

index = header + n * hash_slot + m * index_data;

其中header为index的头部信息,维护对应block的全局信息;

n为hash桶的个数,其由物理块数量和平均文件大小确定;

m为block中文件个数,index_data包含对应文件的offset和size信息;



dataserver的其他任务

DS除了完成基本的数据读写功能外,还需要负责完成实际的数据复制、迁移、删除、压缩等功能,但DS并不是主动执行这些任务,而是由nameserver(NS)触发。NS会周期性的进行检查,对副本数不足的block进行复制(需确定复制源和目的DS)、对删除文件数达到一定比例的block进行压缩,并将这些任务发送给DS,DS收到请求就加入到相应的任务队列中,并有专门的线程负责从任务队列中取任务并执行,当任务执行完毕后会向NS发送反馈信息,NS收到完成信息后将任务从任务表里移除;如果超过配置时间,DS没有反馈执行任务成功与否的信息,ns会过期这些任务,在DS端也会有相同的过期机制用于释放资源。

这里压缩任务由NS发起我觉得是没有必要的,ns应该尽量轻,压缩主要是为了回收block中已删除文件的存储空间,DS根据删除文件数就可以决定是否对block进行压缩;如果真的采取这样的策略,当客户端的写请求落在一个正在进行压缩的块上时,只有在连接DS时才能确定并向客户端返回BUSY信息(现在的方案在连接NS时就能确定块是否在压缩),因为目前DS不是瓶颈,增加点负担是没有影响的,关键是要减小NS的负担。

网上有人问到TFS为什么不采取镜像副本(两个或多个DS的所有对应block互为副本)的形式,这样会节省元数据空间,管理起来也简单;使用镜像方式固然简单,但在某个DS宕机时,需要再产生该DS上拥有block的副本时,它的镜像DS的压力就会很大,如果镜像DS再挂掉了,那么就会导致服务失效了。

目前DS上的写时同步写到多个副本,这种策略简单、但可能会提高写失败的比率,TFS目前在单集群内设置2个备份,然后在两个集群间(主备)在进行一次同步,相当于一个文件4个备份。主备集群的同步时异步进行的,DS的更新(写、删等)会以日志的形式记录,后台线程会读取日志,并重放到备集群(备集群个数可配置)。

posted @ 2013-04-19 14:13  ydzhang  阅读(457)  评论(0编辑  收藏  举报