MongoDB图片存储测试

Web应用中对于图片的存储方案一直是存放在CDN或者服务器的文件系统中,通过索引数据库中的文件路径,分步获得资源后再响应客户端的请求。MongoDB这种文档型数据库通过将数据直接存放到文件的设计无疑可以给我们提供一些新思路。并且对于直接存入MongoDB也有两种方式:直接存入Collection或者是存入GridFS。Collection在以前的文章介绍过不再冗述,对于GridFS还需要好好简述一下它的存储逻辑,这几天通过MongoDB的Node.JS Driver实现了一个粗糙的测试脚本。本文先介绍MongoDB提供的GridFS解决方案,然后再通过Node.js编写的一个测试脚本来粗略地比较MongoDB存储图片的性能。

  1. GridFS

    直接在磁盘上读取和输入数据是非常困难的。将文件读取到服务器就涉及到为Web服务器赋予本地文件系统权限等问题,而怎么搜索文件也是要在高效和经济上进行细致地设计。MongoDB提供了一种与BSON不同的数据结果,也就是这章需要介绍的GridFS。BSON是文档对数据抽象后的表现形式,存储在磁盘中的轻量级二进制数据格式。之所以说它是轻量级是与JSON相比较而言的。出于对RAM和带宽的考虑,BSON文件大小不得超过16MB。这时存储一些大非文档类型数据时可以使用GridFS。

    GridFS是一个建立在普通MongoDB文档基础上的轻量级文件存储规范,所有的操作有客户端驱动或者工具来完成。MongoDB服务器GridFS由两个集合组成。一个集合用于存储文件名等元数据,称为files;一个集合保存文件数据本身,称为chunks。默认的chunk大小为255k。我们先分别看看这两种集合的数据结构。

    structure of files

    files集合存储与文件相关信息的元数据,其中与文件大小相关的字段有length和chunkSize,length记录的是实际的数据大小,chunkSize记录的是每个块的大小,默认的就是上面提到的255k。contentType字段采用的是MIME内容类型来描述存储的数据类型,还有一些其他信息可以使用metadata字段来定义。

    structure of chunks

    GridFS将大文件分割称称之为chunks的小文件,存储在chunks集合中的文档以一个个独立的chunk文件存储,其数据字段n来表示块(chunk)的序列。实际数据存放在data字段里面。而files_id指向files集合中的文档。

    GridFS的一个基本思想就是可以将大文件分成很多chunks(块),每块作为一个单独的文档存储,这样就能将大文件存储起来。首先透过驱动提供的API创建GridFS,通过GridFS对象将文件存储到MongoDB里,当第一次插入数据时,MongoDB才会建立files和chunks的集合,fs.是GridFS集合的默认前缀,客户端可以进行修改。MongoDB内部将文件分别存储到上述的两个集合内,中间当然还有这些MongoDB自行填充的字段,比如插入时间和MD5;以及用户自定义的字段,比如metadata。过程就是这么简单。

    GridFS除了能存储超过16MB的文件外,在获取数据时也不需要将整个文件载入内存。

    这就是GridFS。它可以降低项目技术栈的复杂度,要是已经使用来MongoDB,GriDFS就不需要使用独立的文件存储架构。并且MongoDB的分片预计复杂机制也能在GridFS上使用。当然,相比直接通过文件系统来存储文件,GridFS的读写性能表现还是比较低的。是方便快捷地使用工具,还是比较麻烦地使用其他高性能的文件系统,依据项目需求来决定。

  2. 简单的测试

    整个测试考量的是Node通过中间件(MongoDB Driver/Mongoose)向数据库服务器发出命令的请求以及获得数据库操作响应的时间间隔来衡量不同存储方案的性能。这种粗略的测试可能不能充分说明MongoDB的特性,对于数据库性能的考究也还需更加严谨。不过测试的结果还是能说明一点就是MongoDB的使用确比较方便。这个测试代码放在来github上。

    先说说硬件配置,总共有3种不同配置的数据库服务器。

    hardware

    前两种服务器类型为虚拟机内的服务器。硬件配置均采用512MB内存,64GB容量的机器。单机服务器只运行一个MongoDB进程。MongoDB集群采用3复本,总共15节点。每个复本内运行5个分片节点,按功能设置不同节点:路由节点、配置节点、仲裁节点和2个数据存储节点。第三种服务器类型为在局域网内的实体机,其配置为16GB,300GB的容量,2个复本,6个节点。服务器的操作系统均使用CentOS 6.3,MongoDB也使用老版本2.7.8。Node脚本则直接在Win7上运行,编译版本使用v0.10.31。

    测试内容主要是通过计算MongoDB图片的存储的处理时间来比较不同配置下的MongoDB性能。脚本先读取指定目录内300个28kb~50KB大小图片文件。接下来的具体流程依据存储存储目标略有不同如下:

    • 存入collection的数据首先通过mongoose连接到指定MongoDB服务器,创建Collection的Schema。通过一次批量写入操作(Img.collection.insert(fileArr,...)),存储到MongoDB的collection中。这时通过高精度的时间函数(process.hrtime())记录写操作调用前的时间,当脚本获得数据库响应后在回调函数中记录时间差值(diff = process.hrtime(startTime)),我们通过这个时间差来比较不同硬件情况下的性能。

      //存储到mongoDB开始计时
      var startTime = process.hrtime();
      var diff;
      Img.collection.insert(fileArr, function(err, res){
          //存储完成返回计第二次时
          diff = process.hrtime(startTime);
          if(err){
              console.log('\033[31mError: Failed to save docs  with error: ', err, '\033[39m');
              process.emit('DB_OPS_DONE', err.message);
          }else{
              console.log('%d images were successfully stored.', res.result.n);
              console.log('benchmark took %d nanoseconds', diff[0] * 1e9 + diff[1]);
              process.emit('DB_OPS_DONE', 'Successful insert docs');
          }
      });
      
    • 读取数据则通过find()函数获取前300个图片数据。也是通过调用find()函数与成功获得数据之间的时间差来比较不同硬件下的

      //读取操作 开始计时
      var startTime = process.hrtime();
      var diff;
      Img.find(null, null, { limit: 300 }).lean().exec(function(err, res){
          //存储完成返回计第二次时
          diff = process.hrtime(startTime);
          if(err){
              console.log('\033[31mError: Failed to save docs  with error: ', err, '\033[39m');
              process.emit('DB_OPS_DONE', err.message);
          }else{
              console.log('%d images were successfully read.', res.length);
              console.log('benchmark took %d nanoseconds', diff[0] * 1e9 + diff[1]);
              process.emit('DB_OPS_DONE', 'Successful insert docs');
          }
      });
      
    • 存入Grid的方式略有不同,Mongoose是一款建模工具,不支持GridFS的调用,我们改用原生的Driver来进行编写。原生Driver通过GridStore对象来操作GridFS的存储,每一个GridStore为一个需要存储到GridFS的文件实例,这对测试的控制造成了不少的麻烦。我的解决方法是通过消息机制来记录成功存储到GridFS的次数。建立一个函数(putFile)将这处理过程封装起来。存储数据流程就变成了通过一个循环来调用这个函数,循环开始前开始计数,当前面所述的成功次数达到预定值时才触发第二个计时器来记录处理过程运行时间。

       /**
        * Put the file to the database.
        *
        * @function
        * @param {Db} db a database instance to interact with.
        * @param {String} fileName the file name 
        * @param {String} filePath the local file path 
        * @return {null}
        */
       function putFile(db, fileName, filePath){
           var fileId = new ObjectID();    
           //创建GridStore实例
           var gridStore = new GridStore(db, fileId, fileName, 'w', { root: 'fs', content_type: 'image/jpeg', metadata: { name: fileName} });
           gridStore.open(function(err, gridStore) {
               gridStore.writeFile(filePath, function(err, doc){
                   if(err){
                               console.log('\033[31mError: Failed to write file  with error: ', err, '\033[39m');
                       }
                       //成功存储,触发计数事件
                       process.emit('COUNT_MSG', db);
               });
           });
       };
      
    • 读取GridFS的操作涉及驱动的API多次调用,若需精确把握时间需要进行异步调用的修改,而通过前期测试GridFS在当前需求下性能并不比普通存储到collection的强。所以不在测试GridFS的读取性能。但为了文章的完整性,还是把读取GridFS的方法记录下来。在成功链接数据库后,我们需要先查找数据库中的文件,然后通过GridStore读取出来。

      db.collection('fs.chunks')
          .find()
              .toArray(function(err, files) {
                  if(err){
                      console.log('\033[31mError: Failed to find file  with error: ', err, '\033[39m');
                  }
                  //历遍集合内的文件
                  files.forEach(function(file) {
                      var gridStore = new GridStore(db, file._id, 'r');
                      //打开数据库中的文件,如果不存在就创建新的
                      gridStore.open(function(err, gridStore) {
                          //开始读取的指针位置
                          gridStore.seek(0, function(err, gridStore){
                              gridStore.read(function(err, data){
                                  if(err){
                                      console.log('\033[31mError: Failed to write file  with error: ', err, '\033[39m');
                                  }
                                  console.log('Finished reading file from Mongo GridFS', data);
                              });
                          });
                      });   
                  });
              });
      

    依据前面所说,总共分为5种测试项目,分别为虚拟单机下collection存储、虚拟集群下collection存储、虚拟单机下GridFS存储、虚拟集群下GridFS存储以及实体集群下Collection存储的测试项目。每个测试项目重复测试4次,最后将平均时间记录。

    集群的GridFS测试

    最后测试结果如下表:

    通过测试结果,可以得出在满足当前测试图片存储的需求下,普通的MongoDB数据库的性能比GridFS要强。虽然结果表明单机MongoDB耗时比集群MongoDB要小,但是单机在扩展性和可用性上达不到集群的能力,集群多出来的这一部分开销主要是网络。当然还是那句好,具体选择怎么样的配置看应用的需求。在当前的应用环境下,集群的选择是比较好的。


Reference:

  1. stackoverflow GridFS

  2. Building MongoDB Applications with Binary Files Using GridFS:P2