zoukankan      html  css  js  c++  java
  • FastDFS概要

    本篇文章是我上级老大所写。 留在这里为了不弄丢。


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


    FastDFS提供的功能
    upload 上传文件
    download 下载文件
    delete 删除文件
    心得:一个合适的(不须要选择最复杂的,而是最满足自己的需求。复杂的自己由于理解问题,导致无法掌控。当在出现一些突发性问题时,由于无法及时解决导致灾难性的后果)文件系统须要符合什么样的哲学,或者说应该使用什么样的设计理念?


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


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






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


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


    假设是我们自己来设计一下分布式的文件系统,假设我们要上传。那么,必定要面临着以下的一些选择:
    上传到哪里去?难道由client来指定上传的server吗?
    仅仅上传一台server够吗?
    上传后是原样保存吗?(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的负载比太低)。非常easy,client须要知道存储server的地址和port。然后,指定要上传的文件内容;server收到了文件内容后,怎样选择要存储在哪个文件夹下呢?这个选择要保证均衡性,即尽量保证文件可以均匀地分散在全部的文件夹下。


    负载均衡性非常重要的就是哈希,比如,在PHP中经常使用的md5,其返回一个32个字符,即16字节的输出,即128位。哈希后要变成桶,才可以分布,自然就有了例如以下的问题:


    1- 怎样得到哈希值?md5还是SHA1
    2- 哈希值得到后,怎样构造哈希桶
    3- 依据文件名怎样定位哈希桶


    首先来回答第3个问题,依据文件名怎样定位哈希桶。非常easy,此时我们仅仅有一个文件名作为输入,首先要计算哈希值,仅仅有一个办法了,就是依据文件名来得到哈希值。这个函数能够用整个文件名作为哈希的输入,也能够依据文件名的一部分来完毕。结合上面说的两级文件夹,并且每级文件夹不要超过1000.非常easy,假设用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个字符作为二级文件夹的名称。server上,使用一个专门的文件夹来作为我们的存储根文件夹,然后以下建立这么多子文件夹,自然就非常easy了。


    这些文件夹能够在初始化的时候创建出来,而不用在存储文件的时候才建立。


    或许你会问,一个文件夹应该不够吧,实际上非常多的便宜机器一般都配置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吗? 还是文件的计算能够避免此问题?这个信息存储在trackerserver中吗?


    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) = '';
     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 = '';
      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 = '';
     }
     if (*full_filename == '')
     {
      logError("file: "__FILE__", line: %d, " 
       "Can't generate uniq filename", __LINE__);
      *filename = '';
      *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)) = '';
     return 0;
    }

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


    1- 怎样得到哈希值?md5还是SHA1
    2- 哈希值得到后,怎样构造哈希桶
    3- 依据文件名怎样定位哈希桶


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


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


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





  • 相关阅读:
    hdu5728 PowMod
    CF1156E Special Segments of Permutation
    CF1182E Product Oriented Recurrence
    CF1082E Increasing Frequency
    CF623B Array GCD
    CF1168B Good Triple
    CF1175E Minimal Segment Cover
    php 正则
    windows 下安装composer
    windows apache "The requested operation has failed" 启动失败
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/4557733.html
Copyright © 2011-2022 走看看