FastDFS概述

来源:互联网 发布:高斯混合模型 java 编辑:程序博客网 时间:2024/06/10 20:05
本篇文章是我上级老大所写。 留在这里为了不弄丢。


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万的文件 ,就都不是问题了。这个还是非常好解决的。


// TODO 如何避免文件重复上传? md5吗? 还是文件的计算可以避免此问题?这个信息存储在tracker服务器中吗?


FastDFS中给我们一个非常好的例子,请参考下面的代码:


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


[cpp] view plain copy
  1. /** 
  2. 1 byte: store path index 
  3. 8 bytes: file size 
  4. FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.) 
  5. file size bytes: file content 
  6. **/  
  7. static int storage_upload_file(struct fast_task_info *pTask, bool bAppenderFile)  
  8. {  
  9.  StorageClientInfo *pClientInfo;  
  10.  StorageFileContext *pFileContext;  
  11.  DisconnectCleanFunc clean_func;  
  12.  char *p;  
  13.  char filename[128];  
  14.  char file_ext_name[FDFS_FILE_PREFIX_MAX_LEN + 1];  
  15.  int64_t nInPackLen;  
  16.  int64_t file_offset;  
  17.  int64_t file_bytes;  
  18.  int crc32;  
  19.  int store_path_index;  
  20.  int result;  
  21.  int filename_len;  
  22.  pClientInfo = (StorageClientInfo *)pTask->arg;  
  23.  pFileContext = &(pClientInfo->file_context);  
  24.  nInPackLen = pClientInfo->total_length - sizeof(TrackerHeader);  
  25.  if (nInPackLen < 1 + FDFS_PROTO_PKG_LEN_SIZE +  
  26.    FDFS_FILE_EXT_NAME_MAX_LEN)  
  27.  {  
  28.   logError("file: "__FILE__", line: %d, " \  
  29.    "cmd=%d, client ip: %s, package size " \  
  30.    "%"PRId64" is not correct, " \  
  31.    "expect length >= %d", __LINE__, \  
  32.    STORAGE_PROTO_CMD_UPLOAD_FILE, \  
  33.    pTask->client_ip, nInPackLen, \  
  34.    1 + FDFS_PROTO_PKG_LEN_SIZE + \  
  35.    FDFS_FILE_EXT_NAME_MAX_LEN);  
  36.   return EINVAL;  
  37.  }  
  38.  p = pTask->data + sizeof(TrackerHeader);  
  39.  store_path_index = *p++;  
  40.  if (store_path_index == -1)  
  41.  {  
  42.   if ((result=storage_get_storage_path_index( \  
  43.    &store_path_index)) != 0)  
  44.   {  
  45.    logError("file: "__FILE__", line: %d, " \  
  46.     "get_storage_path_index fail, " \  
  47.     "errno: %d, error info: %s", __LINE__, \  
  48.     result, STRERROR(result));  
  49.    return result;  
  50.   }  
  51.  }  
  52.  else if (store_path_index < 0 || store_path_index >= \  
  53.   g_fdfs_store_paths.count)  
  54.  {  
  55.   logError("file: "__FILE__", line: %d, " \  
  56.    "client ip: %s, store_path_index: %d " \  
  57.    "is invalid", __LINE__, \  
  58.    pTask->client_ip, store_path_index);  
  59.   return EINVAL;  
  60.  }  
  61.  file_bytes = buff2long(p);  
  62.  p += FDFS_PROTO_PKG_LEN_SIZE;  
  63.  if (file_bytes < 0 || file_bytes != nInPackLen - \  
  64.    (1 + FDFS_PROTO_PKG_LEN_SIZE + \  
  65.     FDFS_FILE_EXT_NAME_MAX_LEN))  
  66.  {  
  67.   logError("file: "__FILE__", line: %d, " \  
  68.    "client ip: %s, pkg length is not correct, " \  
  69.    "invalid file bytes: %"PRId64 \  
  70.    ", total body length: %"PRId64, \  
  71.    __LINE__, pTask->client_ip, file_bytes, nInPackLen);  
  72.   return EINVAL;  
  73.  }  
  74.  memcpy(file_ext_name, p, FDFS_FILE_EXT_NAME_MAX_LEN);  
  75.  *(file_ext_name + FDFS_FILE_EXT_NAME_MAX_LEN) = '\0';  
  76.  p += FDFS_FILE_EXT_NAME_MAX_LEN;  
  77.  if ((result=fdfs_validate_filename(file_ext_name)) != 0)  
  78.  {  
  79.   logError("file: "__FILE__", line: %d, " \  
  80.    "client ip: %s, file_ext_name: %s " \  
  81.    "is invalid!", __LINE__, \  
  82.    pTask->client_ip, file_ext_name);  
  83.   return result;  
  84.  }  
  85.  pFileContext->calc_crc32 = true;  
  86.  pFileContext->calc_file_hash = g_check_file_duplicate;  
  87.  pFileContext->extra_info.upload.start_time = g_current_time;  
  88.  strcpy(pFileContext->extra_info.upload.file_ext_name, file_ext_name);  
  89.  storage_format_ext_name(file_ext_name, \  
  90.    pFileContext->extra_info.upload.formatted_ext_name);  
  91.  pFileContext->extra_info.upload.trunk_info.path. \  
  92.     store_path_index = store_path_index;  
  93.  pFileContext->extra_info.upload.file_type = _FILE_TYPE_REGULAR;  
  94.  pFileContext->sync_flag = STORAGE_OP_TYPE_SOURCE_CREATE_FILE;  
  95.  pFileContext->timestamp2log = pFileContext->extra_info.upload.start_time;  
  96.  pFileContext->op = FDFS_STORAGE_FILE_OP_WRITE;  
  97.  if (bAppenderFile)  
  98.  {  
  99.   pFileContext->extra_info.upload.file_type |= \  
  100.      _FILE_TYPE_APPENDER;  
  101.  }  
  102.  else  
  103.  {  
  104.   if (g_if_use_trunk_file && trunk_check_size( \  
  105.    TRUNK_CALC_SIZE(file_bytes)))  
  106.   {  
  107.    pFileContext->extra_info.upload.file_type |= \  
  108.       _FILE_TYPE_TRUNK;  
  109.   }  
  110.  }  
  111.  if (pFileContext->extra_info.upload.file_type & _FILE_TYPE_TRUNK)  
  112.  {  
  113.   FDFSTrunkFullInfo *pTrunkInfo;  
  114.   pFileContext->extra_info.upload.if_sub_path_alloced = true;  
  115.   pTrunkInfo = &(pFileContext->extra_info.upload.trunk_info);  
  116.   if ((result=trunk_client_trunk_alloc_space( \  
  117.    TRUNK_CALC_SIZE(file_bytes), pTrunkInfo)) != 0)  
  118.   {  
  119.    return result;  
  120.   }  
  121.   clean_func = dio_trunk_write_finish_clean_up;  
  122.   file_offset = TRUNK_FILE_START_OFFSET((*pTrunkInfo));  
  123.     pFileContext->extra_info.upload.if_gen_filename = true;  
  124.   trunk_get_full_filename(pTrunkInfo, pFileContext->filename, \  
  125.     sizeof(pFileContext->filename));  
  126.   pFileContext->extra_info.upload.before_open_callback = \  
  127.      dio_check_trunk_file_when_upload;  
  128.   pFileContext->extra_info.upload.before_close_callback = \  
  129.      dio_write_chunk_header;  
  130.   pFileContext->open_flags = O_RDWR | g_extra_open_file_flags;  
  131.  }  
  132.  else  
  133.  {  
  134.   char reserved_space_str[32];  
  135.   if (!storage_check_reserved_space_path(g_path_space_list \  
  136.    [store_path_index].total_mb, g_path_space_list \  
  137.    [store_path_index].free_mb - (file_bytes/FDFS_ONE_MB), \  
  138.    g_avg_storage_reserved_mb))  
  139.   {  
  140.    logError("file: "__FILE__", line: %d, " \  
  141.     "no space to upload file, "  
  142.     "free space: %d MB is too small, file bytes: " \  
  143.     "%"PRId64", reserved space: %s", \  
  144.     __LINE__, g_path_space_list[store_path_index].\  
  145.     free_mb, file_bytes, \  
  146.     fdfs_storage_reserved_space_to_string_ex( \  
  147.       g_storage_reserved_space.flag, \  
  148.           g_avg_storage_reserved_mb, \  
  149.       g_path_space_list[store_path_index]. \  
  150.       total_mb, g_storage_reserved_space.rs.ratio,\  
  151.       reserved_space_str));  
  152.    return ENOSPC;  
  153.   }  
  154.   crc32 = rand();  
  155.   *filename = '\0';  
  156.   filename_len = 0;  
  157.   pFileContext->extra_info.upload.if_sub_path_alloced = false;  
  158.   if ((result=storage_get_filename(pClientInfo, \  
  159.    pFileContext->extra_info.upload.start_time, \  
  160.    file_bytes, crc32, pFileContext->extra_info.upload.\  
  161.    formatted_ext_name, filename, &filename_len, \  
  162.    pFileContext->filename)) != 0)  
  163.   {  
  164.    return result;  
  165.   }  
  166.   clean_func = dio_write_finish_clean_up;  
  167.   file_offset = 0;  
  168.     pFileContext->extra_info.upload.if_gen_filename = true;  
  169.   pFileContext->extra_info.upload.before_open_callback = NULL;  
  170.   pFileContext->extra_info.upload.before_close_callback = NULL;  
  171.   pFileContext->open_flags = O_WRONLY | O_CREAT | O_TRUNC \  
  172.       | g_extra_open_file_flags;  
  173.  }  
  174.   return storage_write_to_file(pTask, file_offset, file_bytes, \  
  175.    p - pTask->data, dio_write_file, \  
  176.    storage_upload_file_done_callback, \  
  177.    clean_func, store_path_index);  
  178. }  
  179.    
  180. static int storage_get_filename(StorageClientInfo *pClientInfo, \  
  181.  const int start_time, const int64_t file_size, const int crc32, \  
  182.  const char *szFormattedExt, char *filename, \  
  183.  int *filename_len, char *full_filename)  
  184. {  
  185.  int i;  
  186.  int result;  
  187.  int store_path_index;  
  188.  store_path_index = pClientInfo->file_context.extra_info.upload.  
  189.     trunk_info.path.store_path_index;  
  190.  for (i=0; i<10; i++)  
  191.  {  
  192.   if ((result=storage_gen_filename(pClientInfo, file_size, \  
  193.    crc32, szFormattedExt, FDFS_FILE_EXT_NAME_MAX_LEN+1, \  
  194.    start_time, filename, filename_len)) != 0)  
  195.   {  
  196.    return result;  
  197.   }  
  198.   sprintf(full_filename, "%s/data/%s", \  
  199.    g_fdfs_store_paths.paths[store_path_index], filename);  
  200.   if (!fileExists(full_filename))  
  201.   {  
  202.    break;  
  203.   }  
  204.   *full_filename = '\0';  
  205.  }  
  206.  if (*full_filename == '\0')  
  207.  {  
  208.   logError("file: "__FILE__", line: %d, " \  
  209.    "Can't generate uniq filename", __LINE__);  
  210.   *filename = '\0';  
  211.   *filename_len = 0;  
  212.   return ENOENT;  
  213.  }  
  214.  return 0;  
  215. }  
  216. static int storage_gen_filename(StorageClientInfo *pClientInfo, \  
  217.   const int64_t file_size, const int crc32, \  
  218.   const char *szFormattedExt, const int ext_name_len, \  
  219.   const time_t timestamp, char *filename, int *filename_len)  
  220. {  
  221.  char buff[sizeof(int) * 5];  
  222.  char encoded[sizeof(int) * 8 + 1];  
  223.  int len;  
  224.  int64_t masked_file_size;  
  225.  FDFSTrunkFullInfo *pTrunkInfo;  
  226.  pTrunkInfo = &(pClientInfo->file_context.extra_info.upload.trunk_info);  
  227.  int2buff(htonl(g_server_id_in_filename), buff);  
  228.  int2buff(timestamp, buff+sizeof(int));  
  229.  if ((file_size >> 32) != 0)  
  230.  {  
  231.   masked_file_size = file_size;  
  232.  }  
  233.  else  
  234.  {  
  235.   COMBINE_RAND_FILE_SIZE(file_size, masked_file_size);  
  236.  }  
  237.  long2buff(masked_file_size, buff+sizeof(int)*2);  
  238.  int2buff(crc32, buff+sizeof(int)*4);  
  239.  base64_encode_ex(&g_fdfs_base64_context, buff, sizeof(int) * 5, encoded, \  
  240.    filename_len, false);  
  241.  if (!pClientInfo->file_context.extra_info.upload.if_sub_path_alloced)  
  242.  {  
  243.   int sub_path_high;  
  244.   int sub_path_low;  
  245.   storage_get_store_path(encoded, *filename_len, \  
  246.    &sub_path_high, &sub_path_low);  
  247.   pTrunkInfo->path.sub_path_high = sub_path_high;  
  248.   pTrunkInfo->path.sub_path_low = sub_path_low;  
  249.   pClientInfo->file_context.extra_info.upload. \  
  250.     if_sub_path_alloced = true;  
  251.  }  
  252.  len = sprintf(filename, FDFS_STORAGE_DATA_DIR_FORMAT"/" \  
  253.    FDFS_STORAGE_DATA_DIR_FORMAT"/", \  
  254.    pTrunkInfo->path.sub_path_high,  
  255.    pTrunkInfo->path.sub_path_low);  
  256.  memcpy(filename+len, encoded, *filename_len);  
  257.  memcpy(filename+len+(*filename_len), szFormattedExt, ext_name_len);  
  258.  *filename_len += len + ext_name_len;  
  259.  *(filename + (*filename_len)) = '\0';  
  260.  return 0;  
  261. }  

回头来看一下我们的问题:


1- 如何得到哈希值?md5还是SHA1
2- 哈希值得到后,如何构造哈希桶
3- 根据文件名称如何定位哈希桶


根据上面分析的结果,我们看到,当上传一个文件的时候,我们会获取到如下的信息


1- 文件的大小(通过协议中包的长度字段可以知道,这样的好处在于服务端实现的时候简单,不用过于担心网络缓冲区的问题)
2- CRC32(也是协议包中传输,以便确定网络传输是否出错)
3- 时间戳(获取服务器的当前时间)
4- 计数器(服务器自己维护)


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



0 0
原创粉丝点击