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的文件夹情况。





  • 相关阅读:
    [转]回车和换行
    计算机常见缩略词备忘录
    Linux多线程编程阅读链接
    字符串匹配KMP算法
    k8s测试集群部署
    搭建Vmware Harbor 镜像仓库
    GitLab搭建
    Gerrit2安装配置
    linux文件系统问题:wrong fs type, bad option, bad superblock
    Docker容器内不能联网的6种解决方案
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/4557733.html
Copyright © 2011-2022 走看看