2012年06月1日

将网站中用户上传的零散小文件存储在MongoDB中的.net解决方案

作者 非鱼

这几年来Web服务器中用户文件的存储一直是我的一个心病,基于成本考虑,网站初始没有单独的服务器可供存储专用,基于上就只能放在Web站点所在的机器上,所以目录式存储就顺理成章了。然后,当数据量大到这个分区放不下的时候,只好加硬盘,而如果不想改变原来读写代码,就要把原来的整个目录再复制到新的分区上。虽然在Uploads目录下通过日期建立子目录,可以通过在IIS中将不同日期的子目录使用虚拟目录指向不同磁盘的方式,但是这样同样会导致另外一些非Web式的处理程序无法读取文件的真实位置。后来虽然采购了专用的存储服务器,但是使用的IPSAN存储方式,对Windows服务器来讲,它还是个普通的硬盘分区。当同一个分区上文件数量达到一定量的时候,光分区表本身的容量就已经是个恶梦,无论是性能还是文件的安全性,一旦系统崩溃,这些海量文件根本恢复不出来。以前找到的最好的解决方案是一种Hash存储的服务器,通过Api或者HTTP接口调用,返回一个Key(就是文件的MD5),读取文件的时候也使用这个MD5,同时还可以解决内容一样文件名不同的文件重复浪费空间的问题。不过这个方案国内没有成熟的提供商,EMC家倒是有,40万起。

现在终于有了另一个便宜的解决方案,那就是MongoDB的GridFS。在其官方文档中对于适用场景的解释是:1、大量文件。2、用户上传的文件。3、经常需要修改的文件。不适用的场景:1、少量静态文件。2、需要经常原子级修改的文件。作为初始的使用环境,只需要一个Server。当内容的量大到一定程度的时候,就可以很容易的建立负载均衡,同时,所有的零散小文件都存储在有限的几个大文件中,备份也变得简单了。而且MongoDB会自动把需要使用的数据放到内存中,虽然内存使用量会增加不少,但是性能却比直接读写目录文件还要快。

另外,还有一个好处,它提供了原生Windows程序,而且是单一exe文件,使用极其方便。虽然性能和稳定性可能不如Linux版,不过,目前应该够用了。以后随时可以单独加一台大内存大硬盘的Linux服务器专门用来做存储。

下载:(win2008 64位版)

http://downloads.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.0.5.zip

下载后直接解压到任意目录,里面的mongod.exe就是服务主程序,在你准备用来存储文件的地方建立一个data目录,再建一个logs目录,就可以把这个程序安装成windows服务来运行了。用cmd进入程序所在的目录,执行

mongod.exe –dbpath=f:/mongo/data –logpath=f:/mongo/logs/ –logappend –directoryperdb –install

服务就安装完成了,可以进入服务管理启动它,然后去logs目录下看看里面的日志,有没有出现错误就可以了。

在.net中读写mongodb也很简单,去git下载官方C#驱动,https://github.com/mongodb/mongo-csharp-driver,自己编译一下项目,引用生成的两个dll就可以了。

读写前需要两个固定的变量:

MongoServer server = MongoServer.Create(“mongodb://localhost:27017”);

MongoDatabase db = server.GetDatabase(“Main”);

每一个名字的db会自动创建一个独立的目录,以后拆分到独立的服务器也会方便一些。这两个连接可以每次建立新连接,也可以建立一个连接,放到静态变量里全局调用。

剩下的事就容易多了:

把文件保存到db里:

db.GridFS.Upload(stream, fullPath);

这个stream可以是HttpPostedFile.InputStream对象,所以对于用户上传的文件,直接把这个变量传进来就可以了,也可以是一个本地文件的路径,它会自己去读这个文件,后面的FullPath变量是这个文件的文件名,也是该文件在系统中的身份标志,所以必须使用带完整路径的文件名,当然,可以使用相对路径,它只是个标志,没有实际用途。所以对于原来的计算出一个路径,然后调用file.SaveAs(path)的代码,只需要改成上面这个db.GridFS.Upload(file.InputStream, path);就OK了。其它代码全部都不影响。

读取db中的某个文件:

var file = db.GridFS.FindOne(path); 这个path就是保存的时候传入的那个文件名,返回的这个对象包含了文件的大小,ContentType,要读取内容可以使用:

using (var stream = file.OpenRead())
{
var bytes = new byte[stream.Length];
stream.Read(bytes, 0, (int)stream.Length);
}

就得到了完整内容的byte数组,返回给用户或者进行其它操作就可以了。

删除文件:db.GridFS.Delete(path);

当我批量把原来的某个目录下500多M的900多个文件写入数据库以后,在数据库目录下生成了四个存储文件,大小分别是64M,128M,256M,512M,估计继续写下去下面生成的文件就1G了,另外有一个16M的索引文件。服务占用内存400多M。并不是所有文件都会读到内存里,只是把最近用到的会放到内存里面,所以目前也不用太担心内存占用问题。