FastDFS目录及文件名的一些资料

根据网上的资料,一般单目录下的文件个数一般限制不能够超过3万;同样的,一个目录下面的目录数也最好不要超过这个数。

但实际上,为了安全考虑,一般都不要存储这么多的内容。

假定,一个目录下面,存储1000个文件,每个文件的平均大小为10KB,则单目录下面可存储的容量是10MB。这个容量太小了,所以我们要多个目录,假定有1000个目录,每个目录存储10MB,则可以存储10GB的内容;这对于目前磁盘的容量来说,利用率还是不够的。再想办法,转成两级目录,这样的话,就是第一层目录有1000个子目录,每一级子目录下面又有1000级的二级子目录,每个二级子目录,可以存储10MB的内容,此时就可以存储10T的内容,这基本上超过了目前单机磁盘的容量大小了。

所以,使用二级子目录的办法,是平衡存储性能和利用存储容量的办法。

 

 

 

 

 

服务器收到了文件内容后,如何选择要存储在哪个目录下呢?这个选择要保证均衡性,即尽量保证文件能够均匀地分散在所有的目录下。

负载均衡性很重要的就是哈希,例如,在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的文件名称生成算法

/**
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)
{
 StorageClientInfo *pClientInfo;
 StorageFileContext *pFileContext;
 DisconnectCleanFunc clean_func;
 char *p;
 char filename[128];
 char file_ext_name[FDFS_FILE_PREFIX_MAX_LEN + 1];
 int64_t nInPackLen;
 int64_t file_offset;
 int64_t file_bytes;
 int crc32;
 int store_path_index;
 int result;
 int filename_len;
 pClientInfo = (StorageClientInfo *)pTask->arg;
 pFileContext = &(pClientInfo->file_context);
 nInPackLen = pClientInfo->total_length - sizeof(TrackerHeader);
 if (nInPackLen < 1 + FDFS_PROTO_PKG_LEN_SIZE +
   FDFS_FILE_EXT_NAME_MAX_LEN)
 {
  logError("file: "__FILE__", line: %d, " \
   "cmd=%d, client ip: %s, package size " \
   "%"PRId64" is not correct, " \
   "expect length >= %d", __LINE__, \
   STORAGE_PROTO_CMD_UPLOAD_FILE, \
   pTask->client_ip, nInPackLen, \
   1 + FDFS_PROTO_PKG_LEN_SIZE + \
   FDFS_FILE_EXT_NAME_MAX_LEN);
  return EINVAL;
 }
 p = pTask->data + sizeof(TrackerHeader);
 store_path_index = *p++;
 if (store_path_index == -1)
 {
  if ((result=storage_get_storage_path_index( \
   &store_path_index)) != 0)
  {
   logError("file: "__FILE__", line: %d, " \
    "get_storage_path_index fail, " \
    "errno: %d, error info: %s", __LINE__, \
    result, STRERROR(result));
   return result;
  }
 }
 else if (store_path_index < 0 || store_path_index >= \
  g_fdfs_store_paths.count)
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, store_path_index: %d " \
   "is invalid", __LINE__, \
   pTask->client_ip, store_path_index);
  return EINVAL;
 }
 file_bytes = buff2long(p);
 p += FDFS_PROTO_PKG_LEN_SIZE;
 if (file_bytes < 0 || file_bytes != nInPackLen - \
   (1 + FDFS_PROTO_PKG_LEN_SIZE + \
    FDFS_FILE_EXT_NAME_MAX_LEN))
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, pkg length is not correct, " \
   "invalid file bytes: %"PRId64 \
   ", total body length: %"PRId64, \
   __LINE__, pTask->client_ip, file_bytes, nInPackLen);
  return EINVAL;
 }
 memcpy(file_ext_name, p, FDFS_FILE_EXT_NAME_MAX_LEN);
 *(file_ext_name + FDFS_FILE_EXT_NAME_MAX_LEN) = '\0';
 p += FDFS_FILE_EXT_NAME_MAX_LEN;
 if ((result=fdfs_validate_filename(file_ext_name)) != 0)
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, file_ext_name: %s " \
   "is invalid!", __LINE__, \
   pTask->client_ip, file_ext_name);
  return result;
 }
 pFileContext->calc_crc32 = true;
 pFileContext->calc_file_hash = g_check_file_duplicate;
 pFileContext->extra_info.upload.start_time = g_current_time;
 strcpy(pFileContext->extra_info.upload.file_ext_name, file_ext_name);
 storage_format_ext_name(file_ext_name, \
   pFileContext->extra_info.upload.formatted_ext_name);
 pFileContext->extra_info.upload.trunk_info.path. \
    store_path_index = store_path_index;
 pFileContext->extra_info.upload.file_type = _FILE_TYPE_REGULAR;
 pFileContext->sync_flag = STORAGE_OP_TYPE_SOURCE_CREATE_FILE;
 pFileContext->timestamp2log = pFileContext->extra_info.upload.start_time;
 pFileContext->op = FDFS_STORAGE_FILE_OP_WRITE;
 if (bAppenderFile)
 {
  pFileContext->extra_info.upload.file_type |= \
     _FILE_TYPE_APPENDER;
 }
 else
 {
  if (g_if_use_trunk_file && trunk_check_size( \
   TRUNK_CALC_SIZE(file_bytes)))
  {
   pFileContext->extra_info.upload.file_type |= \
      _FILE_TYPE_TRUNK;
  }
 }
 if (pFileContext->extra_info.upload.file_type & _FILE_TYPE_TRUNK)
 {
  FDFSTrunkFullInfo *pTrunkInfo;
  pFileContext->extra_info.upload.if_sub_path_alloced = true;
  pTrunkInfo = &(pFileContext->extra_info.upload.trunk_info);
  if ((result=trunk_client_trunk_alloc_space( \
   TRUNK_CALC_SIZE(file_bytes), pTrunkInfo)) != 0)
  {
   return result;
  }
  clean_func = dio_trunk_write_finish_clean_up;
  file_offset = TRUNK_FILE_START_OFFSET((*pTrunkInfo));
    pFileContext->extra_info.upload.if_gen_filename = true;
  trunk_get_full_filename(pTrunkInfo, pFileContext->filename, \
    sizeof(pFileContext->filename));
  pFileContext->extra_info.upload.before_open_callback = \
     dio_check_trunk_file_when_upload;
  pFileContext->extra_info.upload.before_close_callback = \
     dio_write_chunk_header;
  pFileContext->open_flags = O_RDWR | g_extra_open_file_flags;
 }
 else
 {
  char reserved_space_str[32];
  if (!storage_check_reserved_space_path(g_path_space_list \
   [store_path_index].total_mb, g_path_space_list \
   [store_path_index].free_mb - (file_bytes/FDFS_ONE_MB), \
   g_avg_storage_reserved_mb))
  {
   logError("file: "__FILE__", line: %d, " \
    "no space to upload file, "
    "free space: %d MB is too small, file bytes: " \
    "%"PRId64", reserved space: %s", \
    __LINE__, g_path_space_list[store_path_index].\
    free_mb, file_bytes, \
    fdfs_storage_reserved_space_to_string_ex( \
      g_storage_reserved_space.flag, \
          g_avg_storage_reserved_mb, \
      g_path_space_list[store_path_index]. \
      total_mb, g_storage_reserved_space.rs.ratio,\
      reserved_space_str));
   return ENOSPC;
  }
  crc32 = rand();
  *filename = '\0';
  filename_len = 0;
  pFileContext->extra_info.upload.if_sub_path_alloced = false;
  if ((result=storage_get_filename(pClientInfo, \
   pFileContext->extra_info.upload.start_time, \
   file_bytes, crc32, pFileContext->extra_info.upload.\
   formatted_ext_name, filename, &filename_len, \
   pFileContext->filename)) != 0)
  {
   return result;
  }
  clean_func = dio_write_finish_clean_up;
  file_offset = 0;
    pFileContext->extra_info.upload.if_gen_filename = true;
  pFileContext->extra_info.upload.before_open_callback = NULL;
  pFileContext->extra_info.upload.before_close_callback = NULL;
  pFileContext->open_flags = O_WRONLY | O_CREAT | O_TRUNC \
      | g_extra_open_file_flags;
 }
  return storage_write_to_file(pTask, file_offset, file_bytes, \
   p - pTask->data, dio_write_file, \
   storage_upload_file_done_callback, \
   clean_func, store_path_index);
}
 
static int storage_get_filename(StorageClientInfo *pClientInfo, \
 const int start_time, const int64_t file_size, const int crc32, \
 const char *szFormattedExt, char *filename, \
 int *filename_len, char *full_filename)
{
 int i;
 int result;
 int store_path_index;
 store_path_index = pClientInfo->file_context.extra_info.upload.
    trunk_info.path.store_path_index;
 for (i=0; i<10; i++)
 {
  if ((result=storage_gen_filename(pClientInfo, file_size, \
   crc32, szFormattedExt, FDFS_FILE_EXT_NAME_MAX_LEN+1, \
   start_time, filename, filename_len)) != 0)
  {
   return result;
  }
  sprintf(full_filename, "%s/data/%s", \
   g_fdfs_store_paths.paths[store_path_index], filename);
  if (!fileExists(full_filename))
  {
   break;
  }
  *full_filename = '\0';
 }
 if (*full_filename == '\0')
 {
  logError("file: "__FILE__", line: %d, " \
   "Can't generate uniq filename", __LINE__);
  *filename = '\0';
  *filename_len = 0;
  return ENOENT;
 }
 return 0;
}
static int storage_gen_filename(StorageClientInfo *pClientInfo, \
  const int64_t file_size, const int crc32, \
  const char *szFormattedExt, const int ext_name_len, \
  const time_t timestamp, char *filename, int *filename_len)
{
 char buff[sizeof(int) * 5];
 char encoded[sizeof(int) * 8 + 1];
 int len;
 int64_t masked_file_size;
 FDFSTrunkFullInfo *pTrunkInfo;
 pTrunkInfo = &(pClientInfo->file_context.extra_info.upload.trunk_info);
 int2buff(htonl(g_server_id_in_filename), buff);
 int2buff(timestamp, buff+sizeof(int));
 if ((file_size >> 32) != 0)
 {
  masked_file_size = file_size;
 }
 else
 {
  COMBINE_RAND_FILE_SIZE(file_size, masked_file_size);
 }
 long2buff(masked_file_size, buff+sizeof(int)*2);
 int2buff(crc32, buff+sizeof(int)*4);
 base64_encode_ex(&g_fdfs_base64_context, buff, sizeof(int) * 5, encoded, \
   filename_len, false);
 if (!pClientInfo->file_context.extra_info.upload.if_sub_path_alloced)
 {
  int sub_path_high;
  int sub_path_low;
  storage_get_store_path(encoded, *filename_len, \
   &sub_path_high, &sub_path_low);
  pTrunkInfo->path.sub_path_high = sub_path_high;
  pTrunkInfo->path.sub_path_low = sub_path_low;
  pClientInfo->file_context.extra_info.upload. \
    if_sub_path_alloced = true;
 }
 len = sprintf(filename, FDFS_STORAGE_DATA_DIR_FORMAT"/" \
   FDFS_STORAGE_DATA_DIR_FORMAT"/", \
   pTrunkInfo->path.sub_path_high,
   pTrunkInfo->path.sub_path_low);
 memcpy(filename+len, encoded, *filename_len);
 memcpy(filename+len+(*filename_len), szFormattedExt, ext_name_len);
 *filename_len += len + ext_name_len;
 *(filename + (*filename_len)) = '\0';
 return 0;
}



 

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

1- 文件的大小(通过协议中包的长度字段可以知道,这样的好处在于服务端实现的时候简单,不用过于担心网络缓冲区的问题)

2- CRC32(也是协议包中传输,以便确定网络传输是否出错)

3- 时间戳(获取服务器的当前时间)

4- 计数器(服务器自己维护)

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

此条目发表在technologys分类目录。将固定链接加入收藏夹。