阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

FastDFS 分布式系统需求分析

144次阅读
没有评论

共计 5640 个字符,预计需要花费 15 分钟才能阅读完成。

FastDFS 是一款开源的轻量级分布式文件系统、纯 C 实现,支持 Linux, FreeBSD 等 UNIX 系统 类 google FS, 不是通用的文件系统,只能够通过专有 API 访问,目前提供了 C,Java 和 PHP API为互联网应用量身定做,解决大容量文件存储问题,追求高性能和高扩展性。
FastDFS 可以看做是基于文件的 key-value 存储系统,称为分布式文件存储服务更为合适。

FastDFS 提供的功能
upload 上传文件
download 下载文件
delete 删除文件


心得:一个合适的(不需要选择最复杂的,而是最满足自己的需求。复杂的自己因为理解问题,导致无法掌控。当在出现一些突发性问题时,因为无法及时解决导致灾难性的后果)文件系统需要符合什么样的哲学,或者说应该使用什么样的设计理念?

一个好的分布式文件系统最好提供 Nginx 的模块,因为对于互联网应用来说,象文件这种静态资源,一般是通过 HTTP 的下载,此时通过容易扩展的 Nginx 来访问 Fastdfs,能够让文件的上传和下载变得特别简单。另外,网站型应用在互联网领域中的比例是非常高,因此 PHP 这种语言作为非常成熟,性能也完全能够让人满意的网站开发语言,提供相应的扩展,也是非常重要的。所以在应用领域上,Fastdfs 是非常合适的。

文件系统天生是静态资源,因此象可修改或者可追加的文件看起来就没有太大的意义了。文件属性也最好不要支持,因为可以通过文件扩展名和尺寸等属性,通过附加在文件名称上,来避免出现存储属性的信息。另外,通过添加属性支持,还不如用其他的东西,例如 redis 等来支持,以避免让此分布式文件系统变得非常复杂。

之所以说 FastDFS 简单,在于其架构中,只有两种角色,一个是 storage, 一个是 tracker。但从实现上讲,实际上有三个模块:tracker, storage 和 fastdfs client。fastdfs 纯粹是协议的解析,以及一些简单的策略。关键还是在于 tracker 和 storage。

在设计 FastDFS 时,除了如上的哲学外,很重要的就是上传,下载,以及删除。以及如何实现同步,以便实现真正的分布式,否则的话这样和普通的单机文件系统就没有什么区别了。

如果是我们自己来设计一下分布式的文件系统,如果我们要上传。那么,必然要面临着下面的一些选择:
上传到哪里去?难道由客户端来指定上传的服务器吗?
只上传一台服务器够吗?
上传后是原样保存吗?(chunk server 比较危险,没有把握不要去做)
对于多 IDC 如何考虑?
对于使用者来说,当需要上传文件的时候,他 / 她关心什么?

1- 上传的文件必须真实地保留着,不能够有任何的加工。虽然 chunk server 之类的看起来不错,但是对于中小型组织来说,一旦因为一些技术性的 bug,会导致 chunk server 破坏掉原来的文件内容,风险比较大
2- 上传成功后,能够立马返回文件名称,并根据文件名称马上完整地下载。原始文件名称我们不关心(如果需要关心,例如象论坛的附件,可以在数据库中保存这些信息,而不应该交给 DFS 来处理)。这样的好处在于 DFS 能够更加灵活和高效,例如可以在文件名称中加入很多的附属信息,例如图片的尺寸等。
3- 上传后的文件不能够是单点,一定要有备份,以防止文件丢失
4- 对于一些热点文件,希望能够做到保证尽可能快速地大量访问

上面的需求其实是比较简单的。首先让我们回到最原始的时代,即磁盘来保存文件。在这个时代,当我们需要管理文件的时候,通常我们都是在单机的磁盘上创建一个目录,然后在此目录下面存放文件。因为用户往往文件名称是很随意的,所以 使用用户指定的文件名称可能会错误地覆盖其他的文件 。因此,在处理的时候,绝对不能够使用用户指定的名称,这是分析后得到的第一个结论。

如果用户上传文件后,分配一个文件名称(具体文件名称的分配策略以后再考虑),那么如果所有的文件都存储在同一个目录下面,在做目录项的遍历时将非常麻烦。根据网上的资料,一般单目录下的文件个数一般限制不能够超过 3 万;同样的,一个目录下面的目录数也最好不要超过这个数。但实际上,为了安全考虑,一般都不要存储这么多的内容。假定,一个目录下面,存储 1000 个文件,每个文件的平均大小为 10KB,则单目录下面可存储的容量是 10MB。这个容量太小了,所以我们要多个目录,假定有 1000 个目录,每个目录存储 10MB,则可以存储 10GB 的内容;这对于目前磁盘的容量来说,利用率还是不够的。我们再想办法,转成两级目录,这样的话,就是第一层目录有 1000 个子目录,每一级子目录下面又有 1000 级的二级子目录,每个二级子目录,可以存储 10MB 的内容,此时就可以存储 10T 的内容,这基本上超过了目前单机磁盘的容量大小了。所以,使用二级子目录的办法,是平衡存储性能和利用存储容量的办法

这样子的话,就回到了上面的问题,如果我们开始只做一个单机版的基于文件系统的存储服务,假如提供 TCP 的服务(不基于 HTTP,因为 HTTP 的负载比太低)。很简单,客户端需要知道存储服务器的地址和端口 。然后,指定要上传的文件内容;服务器收到了文件内容后,如何选择要存储在哪个目录下呢?这个选择要保证均衡性,即 尽量保证文件能够均匀地分散在所有的目录下

负载均衡性很重要的就是哈希,例如,在 PHP 中常用的 md5,其返回一个 32 个字符,即 16 字节的输出,即 128 位。哈希后要变成桶,才能够分布,自然就有了如下的问题:
1- 如何得到哈希值?md5 还是 SHA1
2- 哈希值得到后,如何构造哈希桶
3- 根据文件名称如何定位哈希桶

首先来回答第 3 个问题,根据文件名称如何定位哈希桶。很简单,此时我们只有一个文件名称作为输入,首先要计算哈希值,只有一个办法了,就是根据文件名称来得到哈希值。这个函数可以用整个文件名称作为哈希的输入,也可以根据文件名称的一部分来完成。结合上面说的两级目录,而且每级目录不要超过 1000. 很简单,如果用 32 位的字符输出后,可以取出实现上来说,由于文件上传是防止唯一性,所以如果根据文件内容来产生哈希,则比较好的办法就是截取其中的 4 位,例如:

md5sum fdfs_storaged.pid
52edc4a5890adc59cec82cb60f8af691 fdfs_storaged.pid


上面,这个 fdfs_storage.pid 中,取出最前面的 4 个字符,即 52 和 ed。这样的话,假如 52 是一级目录的名称,ed 是二级目录的名称。因为每一个字符有 16 个取值,所以第一级目录就有 16 * 16 = 256 个。总共就有 256 * 256 = 65526 个目录。如果每个目录下面存放 1000 个文件,每个文件 30KB,都可以有 1966G,即 2TB 左右。这样的话,足够我们用好。如果用三个字符,即 52e 作为一级目录,dc4 作为二级目录,这样子的目录数有 4096,太多了。所以,取二个字符比较好。

这样的话,上面的第 2 和第 3 个问题就解决了,根据文件名称来得到 md5,然后取 4 个字符,前面的 2 个字符作为一级目录名称,后面的 2 个字符作为二级目录的名称。服务器上,使用一个专门的目录来作为我们的存储根目录,然后下面建立这么多子目录,自然就很简单了。

这些目录可以在初始化的时候创建出来,而不用在存储文件的时候才建立。

也许你会问,一个目录应该不够吧,实际上很多的廉价机器一般都配置 2 块硬盘,一块是操作系统盘,一块是数据盘。然后这个数据盘挂在一个目录下面,以这个目录作为我们的存储根目录就好了。这样也可以很大程度上减少运维的难度。

现在就剩下最后一个问题了,就是上传文件时候,如何分配一个唯一的文件名称,避免同以前的文件产生覆盖。

如果没有变量作为输入,很显然,只能够采用类似于计数器的方式,即一个 counter,每次加一个文件就增量。但这样的方式会要求维护一个持久化的 counter,这样比较麻烦。最好不要有历史状态的纪录。

string md5 (string $str [, bool $raw_output = false] )
Calculates the MD5 hash of str using the » RSA Data Security, Inc. MD5 Message-Digest Algorithm, and returns that hash.

raw_output
If the optional raw_output is set to TRUE, then the md5 digest is instead returned in raw binary format with a length of 16.
Return Values 

Returns the hash as a 32-character hexadecimal number.

为了尽可能地生成唯一的文件名称,可以使用文件长度(假如是 100MB 的话,相应的整型可能会是 4 个字节,即不超过 2^32, 即 uint32_t,只要程序代码中检查一下即可)。但是长度并不能够保证唯一,为了填充尽可能有用的信息,CRC32 也是很重要的,这样下载程序后,不用做额外的交互就可以知道文件的内容是否正确。一旦发现有问题,立马要报警,并且想办法修复。这样的话,上传的时候也要注意带上 CRC32,以防止在网络传输和实际的硬盘存储过程中出现问题(文件的完整性至关重要)。再加上时间戳,即 long 型的 64 位,8 个字节。最后再加上计数器,因为这个计数器由 storage 提供,这样的话,整个结构就是:len + crc32 + timestamp + uint32_t = 4 + 4 + 8 + 4 = 20 个字节,这样生成的文件名就算做 base64 计算出来,也就不是什么大问题了。而且,加上计数器,每秒内只要单机不上传超过 1 万的文件,就都不是问题了。这个还是非常好解决的。

 
【文件长度 +CRC32+TimeStamp+Counter】

// TODO 如何避免文件重复上传?md5 吗?还是文件的计算可以避免此问题?这个信息存储在 tracker 服务器中吗?
FastDFS 中给我们一个非常好的例子,请参考下面的代码:

// 参考 FastDFS 的文件名称生成算法

/**
1 byte: store path index
8 bytes: file size 
FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.)
file size bytes: file content
**/
static int storage_upload_file(struct fast_task_info *pTask, bool bAppenderFile)
 
根据上面分析的结果,我们看到,当上传一个文件的时候,我们会获取到如下的信息
1- 文件的大小(通过协议中包的长度字段可以知道,这样的好处在于服务端实现的时候简单,不用过于担心网络缓冲区的问题)
2- CRC32(也是协议包中传输,以便确定网络传输是否出错)
3- 时间戳(获取服务器的当前时间)
4- 计数器(服务器自己维护)

根据上面的 4 个数据,组织成 base64 的编码,然后生成此文件名称。根据此文件名称的唯一性,就不会出现被覆盖的情况。同时,唯一性也使得接下来做 md5 运算后,得到的 HASH 值离散性得么保证。得到了 MD5 的哈希值后,取出最前面的 2 部分,就可以知道要定位到哪个目录下面去。哈希桶的构造是固定的,即二级 00-ff 的目录情况。
 
补充说明 Base64 编码

Base64 编码要求把 3 个 8 位字节(3*8=24)转化为 4 个 6 位的字节(4*6=24),之后在 6 位的前面补两个 0,形成 8 位一个字节的形式。如果剩下的字符不足 3 个字节,则用 0 填充,输出字符使用 ’=’,因此编码后输出的文本末尾可能会出现 1 或 2 个 ’=’。

为了保证所输出的编码位可读字符,Base64 制定了一个编码表,以便进行统一转换。编码表的大小为 2^6=64,这也是 Base64 名称的由来。

Base64 编码表

码值 字符
 
码值 字符
 
码值 字符
 
码值 字符
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /


标准的 Base64 并不适合直接放在 URL 里传输,因为 URL 编码器会把标准 Base64 中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为 ANSI SQL 中已将“%”号用作通配符。
改进 Base64 编码,它不仅在末尾填充 ’=’ 号,并将标准 Base64 中的“+”和“/”分别改成了“-”和“_”,这样就免去了在 URL 编解码和数据库存储时所要作的转换。
 

Base64 的原理很简单,首先,准备一个包含 64 个字符的数组:

['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/'] 就是上面那张表格

然后,对二进制数据进行处理,每 3 个字节一组,一共是3x8=24bit,划为 4 组,每组正好 6 个 bit(这样子 2^6 才能查表)

 

FastDFS 分布式系统需求分析

 

这样我们得到 4 个数字作为索引,然后查表,获得相应的 4 个字符,就是编码后的字符串。

所以,Base64 编码会把 3 字节的二进制数据编码为 4 字节的文本数据,长度增加 33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。

如果要编码的二进制数据不是 3 的倍数,最后会剩下 1 个或 2 个字节怎么办?Base64 用 \x00 字节在末尾补足后,再在编码的末尾加上 1 个或 2 个 = 号,表示补了多少字节,解码的时候,会自动去掉。

本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-09/134991.htm

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计5640字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中