前段时间看到群上讨论用NoSQL数据库管理大量小文件的思路,觉得很有意思,正好自己也面临着这样的需求,所以做了一些实验和分析,也有一些问题,希望和大家分享一下。
Best Regards,
Yan
你确定没打错0的个数?如果只有8000个文件,每个文件里只有145个double,写个C程序开个二维数组,初始化时把所有数据读入,我打赌比任何缓存策略都快。
>
> 原先的想法是NoSQL数据库一般有很好的内存缓存机制(其实这个假定没有读过相关资料,只是印象里是这样),所以可以取代自己写的缓存。但动手把MongoDB用到实际的代码中去后发现速度下降了很多(时间约是原来的4~5倍),而且不怎么稳定,有时会出现连接丢失。
与MongoDB之间的连接是用的TCP socket还是UNIX socket?会不会是时间都耗在通信上了?可以用gprof看看(我没用过)。
我觉得对于key-value的应用,MySQL比redis慢很大程度上不是因为数据结构或者算法上的因素,而是MySQL需要解析SQL语句并生成查询计划、做用户权限检查、锁表等,这些overhead是相当可观的,而redis因为专一,所以overhead很小。其实redis传说中的几万Query
Per Second在进程内部实现面前都是浮云,单是进程间通信的代价就可以让两者没有可比性。因此我觉得对于计算不复杂的统计类数据分析任务,只要瓶颈不在磁盘I/O上,都有优化的余地。
我现在批量处理MySQL数据时,都不是用PHP一条一条地执行INSERT语句,这样会慢死;而是把数据库各字段按照字段顺序输出到文件(FIFO),然后用MySQL的load
data infile来把文件导入进去。反复执行SQL查询,网络通信成本和MySQL解析SQL的成本都承受不起。
>
> 所以初步的结论是在近乎同等时间的投入下,小文件+缓存机制比MongoDB性能优越不少。究其原因我最怀疑的就是MongoDB本来就不是拿来当cache用的。(找到了这个帖子但不是很理解。。)
我不了解MongoDB。现代文件系统大多用B+树组织目录结构,MySQL的索引好像用的也是B+树,这东西没有什么高深的,自己实现一个性能也不会差到哪里去。
我觉得数据库相对小文件+缓存的主要优势有:
1、磁盘文件系统的小文件在物理存储上很可能是不连续的(尽管文件系统可能会尽力将同一目录的文件分配到相近的物理块),即使存储在同一片磁盘区域,遍历目录结构时也很可能是跳跃式的访问;而数据库每个表是一个单独的文件,所有文件系统都会尽力将单个文件存储到连续的磁盘空间。因此访问大量小文件会带来很多的磁盘寻道,首次访问(没有读进文件系统缓存)时会非常慢。而数据库肯定会尽量连续地扫描文件,因此不会有特别多的磁盘寻道,在无缓存情况下性能比随机磁盘访问高。
2、数据库的实现是用户态的,除了从磁盘上读取文件,不需要切换到内核态;文件系统的实现是内核态的,所有文件操作不论大小,都要经过一次用户态/内核态的切换,可能还被进程调度插上一腿。
3、文件系统要做路径解析和权限检查,key-value数据库不需要,这方面文件系统可能有overhead。
4、数据库一般会有一块相当大的预读缓冲,文件系统预读缓冲的大小设置一般比较保守。数据库知道表结构的元数据部分是会被经常用到的,它们不会被清除出缓存;而文件系统则不知道哪些文件是会被经常用到的,只能根据一些算法来动态清除“可能以后不太会被用到”的文件缓存。我估计这两个区别对性能影响不大。
因此我不理解小文件为什么会比数据库还要慢。
>
> 关于如何处理这种大量小文件读写的问题,应该问的第一个问题就是:这种大量小文件的存取是不是必须的?我面临的情况基本就是一个对象数组需要持久化存储。它有很多域,会一个一个域进行计算,比如最开始填文件名进去,然后每个文件做一些处理extract
> features,然后根据feature每个文件又有个classifier等等。我采用的是对文件的每个域都用一个文件存储的方案,比如1.png是原来的文件名,那么1_feature.txt就是feature域的内容,然后1_classifier.txt就是classifier的内容。
> 与此相对的应该就是大文件serialization的方案。我们可以建立一个大对象的数组,每个对象里面包含了图片文件名(或者内容),feature,
> classifier的所有信息,然后serialize在磁盘上。每轮处理的时候集中读入,然后处理,然后集中写出。但这样会有一些缺点:
我认为,文件系统就是天然的大文件数据库,对于大图片之类的东西,完全没有必要把图片内容存进数据库。可以像git那样,用文件内容的SHA1值后38位做文件名,前2位做目录名,这样还兼具了自动合并相同文件的功能;如果以后一台机器放不下,也很容易做相对均匀的分片。也可以跟discuz论坛程序一样,用文件上传日期作为目录名,管理起来比较方便。
关于图片的元数据,比如作者、权限之类,我觉得从逻辑上应该放在文件的元数据里(与UNIX的owner、group、权限等放在一起)。但考虑到开发成本,一般网站不会这样做。文件系统应该可以给元数据添加任意字段吧?
我看过一篇文章,某网站每次访问图片文件都查询数据库的方式扛不住之后,就干脆把元数据编码到文件名里,相当于用户请求这些文件时,URL里已经包含这些元数据了。
> 1) serialization对对象的结构非常敏感。如果我们用binary
> serialization,一旦更改这个大对象的设计,比如新加了一个域,原来的文件就全废了,读不进来。相反,一大堆小文件却没有这样的问题,如果新加一个域,大不了加一种小文件比如1_newField.txt就是了。如果自己设计serialization的格式和读写函数的话,固然可以解决这个问题,但一方面麻烦,一方面面临着下一个问题。
自己设计serialize的格式,我觉得不如用NoSQL的对象存储更合适吧,那些成熟系统里的serialize方法估计比自己设计的更合理,而且可以支持一些条件查询。
> 2)
> 持久化和持久化之间间隔太长。由于我们每次要处理几千个数据,如果中间因为内存爆了,数据格式不对等等原因程序挂了,前面的结果就全白跑了。但如果用小文件的话很容易找到哪里断了然后继续跑。
> 这两个原因:数据结构的灵活性和数据存储的稳定性是我选择小文件存储的原因。
用的什么编程语言?数据是自动生成的还是手工输入的?为什么会出现“内存爆了、数据格式不对”这些问题?即使用NoSQL或者MySQL,已经插入的数据也不会因为用户程序崩溃而丢失啊。
扯了这么多,算是抛砖引玉,我没有经验 :)
>
> 考虑到这其实是个很常见的问题(一个对象数组需要持久化),所以一方面分享一下实验的结果,一方面想请教一下大家,你们在实际中有没有遇到这样的问题,是怎么处理的呢?传统数据库?NoSQL?RAMDisk?为什么这么做呢?
>
> 谢谢!
>
> Best Regards,
>
> Yan
>
>
> --
> -- 来自USTC LUG
> 请使用gmail订阅,不要灌水。
> 更多信息more info:http://groups.google.com/group/ustc_lug?hl=en?hl=en
>
>
>
2012/11/18 Yan Wang <gra...@gmail.com>:
> 前段时间看到群上讨论用NoSQL数据库管理大量小文件的思路,觉得很有意思,正好自己也面临着这样的需求,所以做了一些实验和分析,也有一些问题,希望和大家分享一下。你确定没打错0的个数?如果只有8000个文件,每个文件里只有145个double,写个C程序开个二维数组,初始化时把所有数据读入,我打赌比任何缓存策略都快。
>
> 这个主要是用自己写的缓存系统和MongoDB做了一些实验。基本的设定是这样的:我有8000个左右的零碎小文件,每个文件存储的是145行纯文本的小数(就是一个145维的向量)。缓存系统的实现很土鳖,输入是一个整数id,如果这个id之前没有被读取过,会去取相应的文件,parse成一个double数组,缓存在内存里,然后返回。MongoDB那边采用C#
> driver的FindOneAs直接读,在这个整数id上面(不仅是ObjectID)加了indexer,本机做服务器,没有SSL/Username限定,没有自己再实现一轮缓存机制了。
与MongoDB之间的连接是用的TCP socket还是UNIX socket?会不会是时间都耗在通信上了?可以用gprof看看(我没用过)。
>
> 原先的想法是NoSQL数据库一般有很好的内存缓存机制(其实这个假定没有读过相关资料,只是印象里是这样),所以可以取代自己写的缓存。但动手把MongoDB用到实际的代码中去后发现速度下降了很多(时间约是原来的4~5倍),而且不怎么稳定,有时会出现连接丢失。
我觉得对于key-value的应用,MySQL比redis慢很大程度上不是因为数据结构或者算法上的因素,而是MySQL需要解析SQL语句并生成查询计划、做用户权限检查、锁表等,这些overhead是相当可观的,而redis因为专一,所以overhead很小。其实redis传说中的几万Query
Per Second在进程内部实现面前都是浮云,单是进程间通信的代价就可以让两者没有可比性。因此我觉得对于计算不复杂的统计类数据分析任务,只要瓶颈不在磁盘I/O上,都有优化的余地。
我现在批量处理MySQL数据时,都不是用PHP一条一条地执行INSERT语句,这样会慢死;而是把数据库各字段按照字段顺序输出到文件(FIFO),然后用MySQL的load
data infile来把文件导入进去。反复执行SQL查询,网络通信成本和MySQL解析SQL的成本都承受不起。
>
> 所以初步的结论是在近乎同等时间的投入下,小文件+缓存机制比MongoDB性能优越不少。究其原因我最怀疑的就是MongoDB本来就不是拿来当cache用【的。(找到了这个帖子但不是很理解。。)
我不了解MongoDB。现代文件系统大多用B+树组织目录结构,MySQL的索引好像用的也是B+树,这东西没有什么高深的,自己实现一个性能也不会差到哪里去。
我觉得数据库相对小文件+缓存的主要优势有:
1、磁盘文件系统的小文件在物理存储上很可能是不连续的(尽管文件系统可能会尽力将同一目录的文件分配到相近的物理块),即使存储在同一片磁盘区域,遍历目录结构时也很可能是跳跃式的访问;而数据库每个表是一个单独的文件,所有文件系统都会尽力将单个文件存储到连续的磁盘空间。因此访问大量小文件会带来很多的磁盘寻道,首次访问(没有读进文件系统缓存)时会非常慢。而数据库肯定会尽量连续地扫描文件,因此不会有特别多的磁盘寻道,在无缓存情况下性能比随机磁盘访问高。
2、数据库的实现是用户态的,除了从磁盘上读取文件,不需要切换到内核态;文件系统的实现是内核态的,所有文件操作不论大小,都要经过一次用户态/内核态的切换,可能还被进程调度插上一腿。
3、文件系统要做路径解析和权限检查,key-value数据库不需要,这方面文件系统可能有overhead。
4、数据库一般会有一块相当大的预读缓冲,文件系统预读缓冲的大小设置一般比较保守。数据库知道表结构的元数据部分是会被经常用到的,它们不会被清除出缓存;而文件系统则不知道哪些文件是会被经常用到的,只能根据一些算法来动态清除“可能以后不太会被用到”的文件缓存。我估计这两个区别对性能影响不大。
因此我不理解小文件为什么会比数据库还要慢。
我认为,文件系统就是天然的大文件数据库,对于大图片之类的东西,完全没有必要把图片内容存进数据库。可以像git那样,用文件内容的SHA1值后38位做文件名,前2位做目录名,这样还兼具了自动合并相同文件的功能;如果以后一台机器放不下,也很容易做相对均匀的分片。也可以跟discuz论坛程序一样,用文件上传日期作为目录名,管理起来比较方便。
>
> 关于如何处理这种大量小文件读写的问题,应该问的第一个问题就是:这种大量小文件的存取是不是必须的?我面临的情况基本就是一个对象数组需要持久化存储。它有很多域,会一个一个域进行计算,比如最开始填文件名进去,然后每个文件做一些处理extract
> features,然后根据feature每个文件又有个classifier等等。我采用的是对文件的每个域都用一个文件存储的方案,比如1.png是原来的文件名,那么1_feature.txt就是feature域的内容,然后1_classifier.txt就是classifier的内容。
> 与此相对的应该就是大文件serialization的方案。我们可以建立一个大对象的数组,每个对象里面包含了图片文件名(或者内容),feature,
> classifier的所有信息,然后serialize在磁盘上。每轮处理的时候集中读入,然后处理,然后集中写出。但这样会有一些缺点:
关于图片的元数据,比如作者、权限之类,我觉得从逻辑上应该放在文件的元数据里(与UNIX的owner、group、权限等放在一起)。但考虑到开发成本,一般网站不会这样做。文件系统应该可以给元数据添加任意字段吧?
我看过一篇文章,某网站每次访问图片文件都查询数据库的方式扛不住之后,就干脆把元数据编码到文件名里,相当于用户请求这些文件时,URL里已经包含这些元数据了。
> 1) serialization对对象的结构非常敏感。如果我们用binary自己设计serialize的格式,我觉得不如用NoSQL的对象存储更合适吧,那些成熟系统里的serialize方法估计比自己设计的更合理,而且可以支持一些条件查询。
> serialization,一旦更改这个大对象的设计,比如新加了一个域,原来的文件就全废了,读不进来。相反,一大堆小文件却没有这样的问题,如果新加一个域,大不了加一种小文件比如1_newField.txt就是了。如果自己设计serialization的格式和读写函数的话,固然可以解决这个问题,但一方面麻烦,一方面面临着下一个问题。
用的什么编程语言?数据是自动生成的还是手工输入的?为什么会出现“内存爆了、数据格式不对”这些问题?即使用NoSQL或者MySQL,已经插入的数据也不会因为用户程序崩溃而丢失啊。
> 2)
> 持久化和持久化之间间隔太长。由于我们每次要处理几千个数据,如果中间因为内存爆了,数据格式不对等等原因程序挂了,前面的结果就全白跑了。但如果用小文件的话很容易找到哪里断了然后继续跑。
> 这两个原因:数据结构的灵活性和数据存储的稳定性是我选择小文件存储的原因。
扯了这么多,算是抛砖引玉,我没有经验 :)
有个重要的问题:这台机器是单核的还是多核的?负载高吗?如果是单核,应用进程和mongodb进程就要不停地切换,这代价也太高了。
另外能否说下你测试结果的具体数据?文件系统的吞吐量是多少?MongoDB又是多少?我记得文件系统的读QPS是几千到一万。
Mongodb会自动给value的每个字段建索引?这样开销有点大啊。MySQL里除了主键(相当于NoSQL里的key)之外,默认是不建索引的,如果为不可能用来查询的字段建立索引,不仅增加了存储空间,插入、删除、修改数据时还要更新这些索引,浪费时间。
Mongodb会自动给value的每个字段建索引?这样开销有点大啊。MySQL里除了主键(相当于NoSQL里的key)之外,默认是不建索引的,如果为不可能用来查询的字段建立索引,不仅增加了存储空间,插入、删除、修改数据时还要更新这些索引,浪费时间。
自己实现的缓存是进程内操作,不涉及进程间通信,除非选择的语言本身比较慢,或者实现缓存的数据结构太烂,否则几乎肯定比外置缓存快的。
中学搞计算机竞赛的时候,经常有100000条数据进行插入、查询、删除操作的题目,文件输入输出,时限1秒,内存限制50M,只要算法写对,评测机器不太烂,都能通过。当然这些数据是全部载入内存的。因此像你这种需求,目前的实现方式比MongoDB快也就不难理解了。
我觉得使用数据库而不是进程内缓存的主要目的是:
(1)数据量真的很大,无法全部载入内存,需要比较好的缓存策略。但我觉得现在内存这么便宜,硬盘又这么慢,规模不算太大的在线服务数据库超过100G的应该不多,买上几十G的内存,很多问题都解决了。
(2)需要持久化,要保证数据的一致性、容灾性,这个应该是你主要考虑的问题吧。要享受进程内缓存的性能,又要数据持久化,我觉得可以使用一种替代方案:每来一条数据,就输出到FIFO文件里,然后load
data infile到数据库的表里。或者另开一个“垃圾回收”线程,一旦进程缓存内积累了一定量的未保存数据,就把它们保存到NoSQL里并标记为已保存。
(3)需要进行非主键查询,自己实现索引太蛋疼。
> 谢谢费心指教!
>
指教谈不上,我没有做过NoSQL,只是扯一下自己的看法……
这三项优化直击SQL速度慢的要害(当然MySQL也可以增大内存缓存,但不如memsql来得彻底)。我们不用memsql,但在自己写数据处理程序时也可以让常用的数据驻留内存,将改动以日志的形式顺序存储而不是随机写磁盘上的源数据,把常用的代码用预编译的方式优化。
个人感觉memsql还有一些可圈可点的地方:
1.对key-value查询使用HashTable(这个相对B树的优势是显然的),对区间查询使用跳表(skip
list)取代B树(不知道好在哪里,按说跳表的缓存局部性不好于平衡树,难道是因为写不出B树的无锁算法?)
2.使用无锁数据结构支持多CPU并发。(没写过,听起来不错)
3.多版本数据(MVCC)支持并发事务,每个事务操作自己的版本,仅在写冲突时上锁。(想起去年跟@tux搞的实时磁盘文件系统了,虽然这个多版本数据最后没实现)
4.预编译的SQL语句能够预知需要多少内存,不使用malloc,也就不需要垃圾回收。
分析得不一定对,欢迎拍砖。