TFS分布式文件系统

简介

TFS(Taobao !FileSystem)是一个高可扩展、高可用、高性能、面向互联网服务的分布式文件系统,主要针对海量的非结构化数据,它构筑在普通的Linux机器集群上,可为外部提供高可靠和高并发的存储访问。TFS为淘宝提供海量小文件存储,通常文件大小不超过1M,满足了淘宝对小文件存储的需求,被广泛地应用在淘宝各项应用中。它采用了HA架构和平滑扩容,保证了整个文件系统的可用性和扩展性。同时扁平化的数据组织结构,可将文件名映射到文件的物理地址,简化了文件的访问流程,一定程度上为TFS提供了良好的读写性能。

TFS的总体结构

一个TFS集群由两个!NameServer节点(一主一备)和多个!DataServer节点组成。这些服务程序都是作为一个用户级的程序运行在普通Linux机器上的。

在TFS中,将大量的小文件(实际数据文件)合并成为一个大文件,这个大文件称为块(Block), 每个Block拥有在集群内唯一的编号(Block Id), Block Id在!NameServer在创建Block的时候分配, !NameServer维护block与!DataServer的关系。Block中的实际数据都存储在!DataServer上。而一台!DataServer服务器一般会有多个独立!DataServer进程存在,每个进程负责管理一个挂载点,这个挂载点一般是一个独立磁盘上的文件目录,以降低单个磁盘损坏带来的影响。

!NameServer主要功能是: 管理维护Block和!DataServer相关信息,包括!DataServer加入,退出, 心跳信息, block和!DataServer的对应关系建立,解除。正常情况下,一个块会在!DataServer上存在, 主!NameServer负责Block的创建,删除,复制,均衡,整理, !NameServer不负责实际数据的读写,实际数据的读写由!DataServer完成。

!DataServer主要功能是: 负责实际数据的存储和读写。

同时为了考虑容灾,!NameServer采用了HA结构,即两台机器互为热备,同时运行,一台为主,一台为备,主机绑定到对外vip,提供服务;当主机器宕机后,迅速将vip绑定至备份!NameServer,将其切换为主机,对外提供服务。图中的HeartAgent就完成了此功能。

TFS分布式文件系统

TFS的块大小可以通过配置项来决定,通常使用的块大小为64M。TFS的设计目标是海量小文件的存储,所以每个块中会存储许多不同的小文件。!DataServer进程会给Block中的每个文件分配一个ID(File ID,该ID在每个Block中唯一),并将每个文件在Block中的信息存放在和Block对应的Index文件中。这个Index文件一般都会全部load在内存,除非出现!DataServer服务器内存和集群中所存放文件平均大小不匹配的情况。

另外,还可以部署一个对等的TFS集群,作为当前集群的辅集群。辅集群不提供来自应用的写入,只接受来自主集群的写入。当前主集群的每个数据变更操作都会重放至辅集群。辅集群也可以提供对外的读,并且在主集群出现故障的时候,可以接管主集群的工作。

1. 平滑扩容

原有TFS集群运行一定时间后,集群容量不足,此时需要对TFS集群扩容。由于DataServer与NameServer之间使用心跳机制通信,如果系统扩容,只需要将相应数量的新!DataServer服务器部署好应用程序后启动即可。这些!DataServer服务器会向!NameServer进行心跳汇报。!NameServer会根据!DataServer容量的比率和!DataServer的负载决定新数据写往哪台!DataServer的服务器。根据写入策略,容量较小,负载较轻的服务器新数据写入的概率会比较高。同时,在集群负载比较轻的时候,!NameServer会对!DataServer上的Block进行均衡,使所有!DataServer的容量尽早达到均衡。

进行均衡计划时,首先计算每台机器应拥有的blocks平均数量,然后将机器划分为两堆,一堆是超过平均数量的,作为移动源;一类是低于平均数量的,作为移动目的。

移动目的的选择:首先一个block的移动的源和目的,应该保持在同一网段内,也就是要与另外的block不同网段;另外,在作为目的的一定机器内,优先选择同机器的源到目的之间移动,也就是同台!DataServer服务器中的不同!DataServer进程。
当有服务器故障或者下线退出时(单个集群内的不同网段机器不能同时退出),不影响TFS的服务。此时!NameServer会检测到备份数减少的Block,对这些Block重新进行数据复制。

在创建复制计划时,一次要复制多个block, 每个block的复制源和目的都要尽可能的不同,并且保证每个block在不同的子网段内。因此采用轮换选择(roundrobin)算法,并结合加权平均。

由于DataServer之间的通信是主要发生在数据写入转发的时候和数据复制的时候,集群扩容基本没有影响。假设一个Block为64M,数量级为1PB。那么NameServer上会有 1 * 1024 * 1024 * 1024 / 64 = 16.7M个block。假设每个Block的元数据大小为0.1K,则占用内存不到2G。

2. 存储机制

在TFS中,将大量的小文件(实际用户文件)合并成为一个大文件,这个大文件称为块(Block)。TFS以Block的方式组织文件的存储。每一个Block在整个集群内拥有唯一的编号,这个编号是由NameServer进行分配的,而DataServer上实际存储了该Block。在!NameServer节点中存储了所有的Block的信息,一个Block存储于多个!DataServer中以保证数据的冗余。对于数据读写请求,均先由!NameServer选择合适的!DataServer节点返回给客户端,再在对应的!DataServer节点上进行数据操作。!NameServer需要维护Block信息列表,以及Block与!DataServer之间的映射关系,其存储的元数据结构如下:

TFS分布式文件系统

在!DataServer节点上,在挂载目录上会有很多物理块,物理块以文件的形式存在磁盘上,并在!DataServer部署前预先分配,以保证后续的访问速度和减少碎片产生。为了满足这个特性,!DataServer现一般在EXT4文件系统上运行。物理块分为主块和扩展块,一般主块的大小会远大于扩展块,使用扩展块是为了满足文件更新操作时文件大小的变化。每个Block在文件系统上以“主块+扩展块”的方式存储。每一个Block可能对应于多个物理块,其中包括一个主块,多个扩展块。
在DataServer端,每个Block可能会有多个实际的物理文件组成:一个主Physical Block文件,N个扩展Physical Block文件和一个与该Block对应的索引文件。Block中的每个小文件会用一个block内唯一的fileid来标识。!DataServer会在启动的时候把自身所拥有的Block和对应的Index加载进来。

3. 容错机制

  1. 3.1 集群容错

TFS可以配置主辅集群,一般主辅集群会存放在两个不同的机房。主集群提供所有功能,辅集群只提供读。主集群会把所有操作重放到辅集群。这样既提供了负载均衡,又可以在主集群机房出现异常的情况不会中断服务或者丢失数据。

  1. 3.2 !NameServer容错

Namserver主要管理了!DataServer和Block之间的关系。如每个!DataServer拥有哪些Block,每个Block存放在哪些!DataServer上等。同时,!NameServer采用了HA结构,一主一备,主NameServer上的操作会重放至备NameServer。如果主NameServer出现问题,可以实时切换到备NameServer。
另外!NameServer和!DataServer之间也会有定时的heartbeat,!DataServer会把自己拥有的Block发送给!NameServer。!NameServer会根据这些信息重建!DataServer和Block的关系。

  1. 3.3 !DataServer容错

TFS采用Block存储多份的方式来实现!DataServer的容错。每一个Block会在TFS中存在多份,一般为3份,并且分布在不同网段的不同!DataServer上。对于每一个写入请求,必须在所有的Block写入成功才算成功。当出现磁盘损坏!DataServer宕机的时候,TFS启动复制流程,把备份数未达到最小备份数的Block尽快复制到其他DataServer上去。 TFS对每一个文件会记录校验crc,当客户端发现crc和文件内容不匹配时,会自动切换到一个好的block上读取。此后客户端将会实现自动修复单个文件损坏的情况。

4. 并发机制

对于同一个文件来说,多个用户可以并发读。
    现有TFS并不支持并发写一个文件。一个文件只会有一个用户在写。这在TFS的设计里面对应着是一个block同时只能有一个写或者更新操作。

5. TFS文件名的结构

TFS的文件名由块号和文件号通过某种对应关系组成,最大长度为18字节。文件名固定以T开始,第二字节为该集群的编号(可以在配置项中指定,取值范围 1~9)。余下的字节由Block ID和File ID通过一定的编码方式得到。文件名由客户端程序进行编码和解码,它映射方式如下图:

TFS分布式文件系统

TFS客户程序在读文件的时候通过将文件名转换为BlockID和FileID信息,然后可以在!NameServer取得该块所在!DataServer信息(如果客户端有该Block与!DataServere的缓存,则直接从缓存中取),然后与!DataServer进行读取操作。

6. TFS性能数据

  1. 软件环境描述

【测试机软件情况描述】
(1) Red Hat Enterprise Linux AS release 4 (Nahant Update 8) 
(2) gcc (GCC) 3.4.6 20060404 (Red Hat 3.4.6-11) 
(3) 部署了TFS客户端程序 
【服务器软件情况描述】
(1) Red Hat Enterprise Linux Server release 5.4 (Tikanga) 
(2) gcc (GCC) 3.4.6 20060404 (Red Hat 3.4.6-9) 
(3) 部署了2台!DataServer程序。
【服务器软件情况描述】
(1) Red Hat Enterprise Linux Server release 5.4 (Tikanga) 
(2) gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-46) 
(3) 部署了2台!NameServer(HA)程序。

  1. 硬件环境描述

【测试机硬件情况描述】
(1) 一枚八核Intel(R) Xeon(R) CPU E5520 @ 2.27GHz 
(2) 内存总数8299424 kB 
【服务器硬件情况描述】cpu/memory等
(1) 一枚八核Intel(R) Xeon(R) CPU E5520 @ 2.27GHz 
(2) 内存总数8165616 kB

  1. 随机读取1K~50K大小的文件性能

TFS分布式文件系统

Read的TPS随着线程数的增加而增加,增长逐渐趋缓,到90线程的时候达到第一个高峰,此时再增加读线程,则TPS不再稳定增长。

  1. 随机写入1K~50K大小的文件

TFS分布式文件系统

Write的TPS在线程数60左右达到高峰,此时再增加写入线程,TPS不再稳定增长。

  1. 在不同线程写压力下的读文件性能

TFS分布式文件系统

TFS分布式文件系统

可以看出随着写压力的增加,读文件的TPS会大幅下滑。当写压力达到一定程度时读文件TPS趋缓。

同时,对平均大小为20K的文件进行了测试,测试中读:写:更新:删除操作的比率为100:18:1:1时,在!DataServer服务器磁盘util访问达到80%以上时,响应时间如下:

TYPE SUCCCOUNT FAILCOUNT AVG(us) MIN(us) MAX(us)
read 100000 0 20886 925 1170418
write 18000 0 17192 2495 1660686
update 1000 0 48489 5755 1205119
delete 1000 0 14221 382 591651

TYPE:操作类型
SUCCCOUNT:成功个数
FAILCOUNT:失败个数
AVG:平均响应时间
MIN:最短响应时间
MAX: 最大响应时间

TFS写操作数据流

TFS系统中,nameserver会保证一个文件有多个副本存储于不同的dataserver上以保证冗余。当由于dataserver服务器宕机或由于其他原因退出系统导致某些文件副本数量下降时,nameserver将会调度新的dataserver节点存储文件备份。同样为了保证数据一致性,当写入一个文件时,只有所有参与的dataserver均写入成功时,该操作才算成功。TFS的写操作数据流图如下所示:

TFS分布式文件系统

客户端首先向nameserver发起写请求,nameserver需要根据dataserver上的可写块,容量和负载加权平均来选择一个可写的block。并且在该block所在的多个dataserver中选择一个作为写入的master,这个选择过程也需要根据dataserver的负载以及当前作为master的次数来计算,使得每个dataserver作为master的机会均等。master一段选定,除非master宕机,不会更换,一旦master宕机,需要在剩余的dataserver中选择新的master。返回一个dataserver列表。
客户端向master dataserver开始数据写入操作。master server将数据传输为其他的dataserver节点,只有当所有dataserver节点写入均成功时,master server才会向nameserver和客户端返回操作成功的信息。

获得Block ID和File ID

根据TFS文件名解析出Block ID和block中的File ID.

获取dataserver地址

向nameserver发送查询请求得到Block ID所在的dataserver地址。

由于nameserver中维护了block和dataserver的对应关系,所以nameserver能够提供相应的信息。

Note: 由于TFS是把大量小文件放在一个block里面,

所以TFS的文件复制是基于block的,而且复制出来的block的block id应该是一致的

请求文件

通过发送Block_ID、File_ID和offset为参数的读请求到对应的dataserver,得到文件内容。

dataserver会根据本地记录的信息来得到File ID所在block的偏移量,从而读取到正确的文件内容.

TFS分布式文件系统

NS中BlockManager和ServerManager介绍

1. Ns中的BlockManager用来管理所有来自Ds的Block信息。因为Block的数量比较多,因此,BlockManager将Block组织成HashMap的数据结构,Hash的桶的个数由MAX_BLOCK_CHUNK_NUMS决定。另外,为了组织方便,定义了一个双向队列std::deque<std::pair<uint32, uint64> > delete_block_queue_,用来对删除的Block(以及Block所在的Server)进行一个管理并且将最近(在规定时间内)写过的Block也组织成一个HashMap便于管理(难道这个和延迟删有关,就是最近时间内有写入的不马上删除?)。通过这些数据结构,BlockManager可以实现insert一个Block,remove一个Block,将删除的Block以及对应的Server加入和从删除队列中移除,dump所有的Block信息以及最近写入比较频繁的Block信息。 此外,BlockManager还可以判断某个Block是否存在(通过BlockId)以及先通过BlockId获得BlockCollect结构,进而获取到该Block所对应的Ds的信息(这里提供多个重载)。在与Ds的关系方便,BlockManager提供了建立、解除以及更新具体Block与Ds关系接口,以及判断某个Block是否需要复制、压缩、迁移的接口。最后,BlockManager还会根据时间在last_write_blocks_[i]中插入和删除最近写入的Block。

2. Ns中的ServerManager用来管理所有的Server信息。为了管理好活动的和不可服务的DS,ServerManager定义了两个Server列表servers_和dead_servers_。针对具体的DS的操作大致包括加入Server到活动列表(分为是新加入的还是暂时不可服务又好了的),从活动列表中移除Server到不可服务列表(这种情况可能发生在Ds某种原因退出)。当Server在不可服务列表中超过一定时间后,就会将它从不可服务列表中移除(这种情况可能是磁盘坏掉了,所以等换好新盘启动需要一定的时间)。另外,通过ServerManager可以得到活动Server列表以及不可服务Server列表以及某一个范围内的Server列表。 与BlockManager类似,ServerManager也提供了建立和解除具体Ds与Block的关系接口,但这些过程是以各个Server为中心来完成的。此外,ServerManager还负责挑选可写主块,先由ServerManager挑一个Server,再由Server挑一个Block。当BlockManager中发现某些Block需要复制时,由于每个Block对应多个Server,ServerManager负责挑选出要复制的源Server和目标Server。当ServerManager发现某个Server不满足均衡(目前是将活动列表中的前32个server根据容量百分比,特别小的作为目标Server,特别大的作为源Server)时,针对该Server(作为Source Server)里面的具体Block,ServerManager负责挑选出可做为目标的Server。当某种原因导致Block的副本数大于最大副本数时,ServerManager会根据包含该Block的Server的容量进行排序并在满足一定条件下选择一个Server将多余的Block进行删除。(在选择复制、迁移目标Server时需要考虑Server是否不在任务队列里,是否有空间,以及是否和已经挑选的Server在不同机架)

Dataserver后台线程介绍

一、 心跳线程

这里的心跳是指Ds向Ns发的周期性统计信息。原先的做法是当Ds需要汇报block时会将blockInfo的信息通过心跳包的形式发给Ns。而现在的心跳只负责keepalive,汇报block的工作由专门的包进行发送。(所以之前的做法是Ns会在心跳的回复包中带上一个状态(status),Ds在收到这个状态包后,会根据状态进行一些相应的操作(比如淘汰过期的Block以及新增Block操作等))。

二、 复制线程(replicate_block.cpp)

人工或者Ns可以添加复制Block任务至复制队列中,复制线程会从复制队列中取出并执行。结合Ns,整个复制的大致过程是ns向复制的源Ds发起复制任务,源Ds将复制任务所需要的信息结构(ReplBlockExt)加入复制队列中。复制线程取出一个复制任务后,会先通过ReadRawData接口将源Ds的Block数据读出,然后向目标Ds发WriteRawData消息,目标ds在接到writeRawData消息后复制数据,然后通过batch_write_info进行index的复制。然后源Ds将复制是否成功的状态向Ns进行回复,Ns在收到复制成功的消息后会进行Block与Ds关系的更新。当从ns中收到move的操作后,还会将源ds上的Block删除掉。在管理复制的过程中,还用到两个重要的数据结构ReplicateBlockMap_和ClonedBlockMap_,前者用来记录源中将要进行复制的Block,后者用来记录目标中正在复制Block的状态。

三、 压缩线程(compact_block.cpp)

真正的压缩线程也从压缩队列中取出并进行执行(按文件进行,小文件合成一起发送)。压缩的过程其实和复制有点像,只是说不需要将删除的文件数据以及index数据复制到新创建的压缩块中。要判断某个文件是否被删除,还需要拿index文件的offset去fileinfo里面取删除标记,如果标记不是删除的,那么就可以进行write_raw_data的操作,否则则滤过。

四、 检查线程

a 清理过期的Datafile; b 修复check_file_queue_中的逻辑块(block_checker.cpp) c 清理过期的复制块(由于复制过程中出错导致的错误复制块,复制目标的ds做) d 清理过期的压缩块(由于压缩过程中出错导致的错误压缩块,压缩在同一个ds上做) e 每天rotate读写日志,清理过期的错误逻辑块 f 读日志累积后刷磁盘

b的详细过程: 每次对文件进行读写删操作失败的时候,会try_add_repair_task(blockid, ret)来将ret错误的block加入check_file_queue_中,正常情况下加入的为-EIO(I/O错误)的错误Block,那什么时候加入的是CRC的错误呢?人工进行修复的时候发该类型的CRC_ERROR_MESSAGE消息,然后也会加入check_file_queue_中.也就是说人工修复是认为CRC错误的。然后在check的时候会根据类型进行do_repair_crc还是do_repair_eio操作,对各自类型进行错误统计,其中check_block的过程就是通过crc_error和eio_error数量来判断该Block是否过期(对于过期的逻辑块,在错误位图上进行相应物理块的设置),如果是,则请求Ns进行update_block_info, 如果不是,对于eio请求,则无法修复,设置Block不正常(abnormal)的最新时间,对于Crc的则尝试修复,修复过程中会从其他Ds上读副本来进行修复,若出错则会请求Ns进行update_block_info,否则设置Block不正常的最新时间。

rcserver介绍

TFS 在2.0版本增加了一个server, 叫做 rcserver. 这个 server 主要是为了淘宝内部管理使用 TFS 的各个应用. 我们给每个应用分配一个唯一的 AppKey. TFS 客户端使用这个 AppKey 登录到 rcserver, 取得自己应该访问的 TFS 集群信息. 客户端还会定期把自己的一些统计值发送给 rcserver. 具体信息可以参看源码中 doc 目录下的关于 rcserve 的文档.

metaserver介绍

1. 简介

metaserver是我们在2.0版本引进的一个服务. 用来存储一些元数据信息, 这样原本不支持自定义文件名的 TFS 就可以在 metaserver 的帮助下, 支持自定义文件名了.

2. 组成

metaserver 由一个主控节点(rootserver), 若干服务节点(metaserver) 组成. rootserver 主要管理所有的 metaserver. 而metaserver 完成跟文件相关的操作. metaserver 缓存最近的被访问到目录和文件信息. 对于任何写入, 除了更改自己的缓存外还要更改后端持久化存储中的内容. 目前我们暂时使用 mysql 数据库提供后端持久化存储, 将来会替换成淘宝自己的分布式数据库 oceanbase.

3. 访问过程

客户端在做自定义文件名的读操作的时候, 会先从 rootserver 得到关于 metaserver 的信息, 并缓存在自己的内存中. 然后用自定义文件名去 metaserver 中查找 TFS 文件名信息, 再去 TFS 中访问该文件. 客户端在做自定义文件名的写操作的时候, 会先写入到 TFS 中, 再把 TFS 文件名和自定义文件的对应关系写入metaserver中.

4. 自定义文件名的限制

我们目前要求使用自定义文件名的时候必须传入一个app_id 一个 uid. 这两个 id 成为所有自定义文件名的固定前缀. mv 操作只能在相同的app_id, uid 之下进行. 这样做是我们为了简化实现复杂度. 我们的应用都是要面向海量客户, 每个客户自身的数据量有限. 有了上面的限制, 我们可以总是把对同一个app_id uid的请求用相同的 metaserver 来响应. 这样使得我们可以很容易的处理数据一致性问题.

5. 写文件的特殊点

在使用自定义文件名写文件的时候, 必须先调用 creat_file 接口建立文件. 文件建立之后, 可以多进程并发的写这个文件. 这样是为了便于大文件的分片上传. 我们还是不支持对已有文件的修改, 所以分片上传的时候各个分片是不能互相覆盖的.

6. 后续计划

目前自定义文件名提供的功能还比较简单初级, 我们会根据应用的需求逐步完善功能, 提高性能. 我们将来计划在 oceanbase 团队的帮助下, 把后端存储替换成 oceanbase 数据库. 另:编译时候我们设置了 mysql 的最低版本, 这个版本设置的比较高, 其实只要是5.0以上版本就可以支持这个应用.

上一篇:Android Gradle 技巧之二: 最爱命令行


下一篇:Git 的深入理解与GitHub托管服务(转)