-1.本文很长
一不小心就写了老长,本文主要是关于MySQL,SQLite和PostgreSQL在我的特殊应用中使用想法和总结。MySQL部分是上个月的实践,PostgreSQL和非数据库解决方案是我这几天的心得。
`
本文努力地比较了MySQL内存数据库和SQLite数据库在特定应用下的优劣,MySQL一般数据库?它太笨了所以被我放弃了。随后兴头所至干脆加入了PostgreSQL和非数据库解决方案。
`
本文的结论是非数据库解决方案>PostgreSQL>SQLite~MySQL内存方案,P在个方面都要要好于S,但差距不大,而S和M则互有优劣,在此应用中我更偏向于S。
`
0.缘起
前一阵发过一文,愤恨地对MySQL的Select+Limit性能提出质疑。全文除了一些测试数据外,大部分抱怨其实只是阐述了如下一个事实:MySQL的order by语句不使用索引;limit分页性能很差;所以MySQL总体性能很差。
我个人因为这次数据测试而对SQLite极度推崇,然而上个月发生了几起事故,使得我对SQLite愈发不满起来。
事情是这样的,SimpleCD网站架设起来以后,有了一个意想不到的功效:VeryCD自从去年末的广电许可证风波后,日益和谐化,不停地删贴,电影,电视剧,删得非常之勤快;而SimpleCD则成了意外的抢险队员,相当大部分的资源索引被抢救了下来,如果说VeryCD变为“全年龄版”了的话(误),SimpleCD等若变相成为了“18X版”(大误)。举个例子好了,这两天网站近1/5的流量都是“2010春节联欢晚会”带来的,因为VC把春晚的资源都删了,只让发几个小品。其他的例子还有2012,avatar等等。
总之,自从有了SimpleCD以后,因为访问速度和搜索速度快很多,而且搜索结果直接包含VeryCD的结果,我找电驴的资源就直接上SimpleCD而不去VeryCD了。但是当时SimpleCD介绍没有图,资源没有评论,界面又很丑(现在也好不到哪里去就是了@.@),往往找到了还是得链到VeryCD去看看评论和介绍。所以自然而然地,我就打算要改进SimpleCD了。
—-话说,缘起好长,再话说,以上不是广告啊不是广告。再再话说,缘起部分还有一半—-
终于说到正题了,在改版中,遇到了一些问题,最严重的莫过于数据库损坏错误。那时SimpleCD的流量每天大概也有5000PV,加上有不少人用搜索功能,其实会把数据库锁住一阵,不让写;然后后台有爬虫进程在不停地更新数据,也会锁住数据库;最严重的是,因为需要加入新功能,所以要抓一些以前没有抓的数据,这个爬虫需要几乎时刻不停地写数据库,是导致悲剧发生的最重要原因。
以上这些进程全部都是多线程的,SQLite难免就会有长时间处于锁状态的时候,好死不死我又在调试SimpleCD发布资源的代码,需要解锁,我就很流氓地杀掉锁住的进程。几次之后,悲剧发生了,网页瘫痪,显示database is malformed。还好有备份,于是恢复,重抓,折腾一阵,搞定。过了一阵,又悲剧了,再来。。。
以上悲剧不断重演,导致我最后干脆写了个solve_mal.py的脚本,每次出问题就:啊拉,Tea Time了么,然后非常潇洒地输入python solve_mal.py,泡茶闲逛去了。
平心而论,SQLite的并发能力要比我想象的更为出色,要不是升级数据库,这种问题应该即使在50WPV的情况下也不会出现,远远好于我原本的预期。而这个问题应该更类似于一个处理机制上的bug。因为就算进程被终止,也不应该把数据库给写坏了啊,这不是并发问题了,而是严重地数据安全问题了,试想假如不是杀进程而是断电怎么办?数据库就活该损坏吗?显然SQLite的设计者无论如何也想不到会有每半小时把后台运行的数据库程序杀掉重开的变态存在,又或者没有考虑到断电的数据保护,所以疏忽了吧。
如此一来对SQLite愈发不满起来,再加上想试试WSGI(因为不知为何FCGI占了我好多内存,而且spawn-fcgi一天到晚内存泄漏,我每隔一个小时就要kill掉spawn-fcgi重新spawn一下,不然最夸张的时候不出4小时他们就会吃掉所有内存让网站瘫痪。),而不论是nginx的mod_wsgi还是apache的mod_啥来着,对wsgi+web.py+sqlite的组合都有古怪的bug,sqlite数据库只能读不能写,这都是啥破事啊,太ft了,我一度打算换django,因为那时候自己处理页面缓存的架构痛苦不堪,不过wsgi+django也有sqlite数据库只读的怪事,所以就暂时搁下了。
于是虽然很烦恼MySQL的低性能,但是那不是因为我数据结构不好么,再加上怀疑SQLite”作弊”地利用了内存才获得了高性能。反正mysql是后台程序,基本上几百年都不会关的,我让在初始化运行的时候生成一个搜索用的动态内存数据库不就解决性能问题了么?
以上,缘起结束。
1.MySQL的数据库结构优化
要使MySQL版本的SimpleCD跑起来,做大手术是必不可少的。首先要优化数据结构,其次要换用内存数据库,这时候我做了一个猜想。
猜想一:MySQL自动对text类型做了压缩处理,这才是搜索性能低下的原因。
这段时间断断续续看了一点MySQL的东东,首先抱怨一下他的文档,这是我见过的最混乱的文档,文档做得差到这个地步也不容易了。看文档无比地累,往往看完文档一头雾水,相关链接又链到完全不相关的地方,不得不google到别人的博客看别人的经验。我不要求做到像python的文档那么赞,你好歹也做到像你的同类型的sqlite的文档那样吧?真是的。
本来这种猜想只要到官网查查文档就能验证了,可是我楞是在文档区兜了半天圈子没有发现相关的内容,所以没办法了,还是继续猜想,说到压缩,因为MySQL的数据类型非常之多,什么CHAR,VARCHAR的;而sqlite就只有text了,以至于在sqlite中我根本就没有意识到不同的文本类型的处理方式可能会是不一样的。(请尽情吐槽我对数据库的无知好了,我本来还以为数据库数据类型就只有像sqlite那样的null,text,integer,real,blob这几种呢)。好的,那么改进一就是把标题等TEXT类型改成VARCHAR类型
猜想二:MySQL不用索引也是因为数据类型的原因,MySQL认为数据库的设计者不会愚蠢到用text类型来做order by,因为他们根本不会想到会有人把日期写成text保存起来,而不是专门的date类型或者int/real类型。
555,sqlite里面没有date类型啊,我当时也考虑过要不要转int,后来觉得转为int存还要去查mktime,strptime,gmtime等一堆乱七八糟的函数的用法,而且转来转去很麻烦。再加上觉得也就20W的数据量,二叉树的话也就是10多次的比较的事情,10多次integer比较能比10多次长度为20的字符串比较快多少秒?恐怕得用纳秒来记了吧。虽然我写程序时经常斤斤计较这个那个的,不过在明显IO-bound的这种应用里面,我还真是懒得去花时间优化这个。真不知道是我想太多了还是MySQL想太多了,反正结果就是,MySQL很可能默认了text做order by就是不用索引,MySQL就是逮到一切机会用filesort,效率怎么低怎么来。
猜想三:MySQL因为嫌弃我的数据表太臃肿,而没有全部载入内存进行比较。
因为我就一个表,而一般的设计都是大块头的东东另外放一个表,什么标题,类别等可以用char限定字符个数的东东放一个表,这样的话用作索引显示的表的大小就小了,而表小的话显然好处是很多的,比如可以允许更为频繁的读写等。(因为要写入的数据少了,那么锁住表的时间也会显著减少)惭愧,我一开始设计的时候尽想着偷懒和简单,没有想过规模变大的问题,所以才有这种问题。不过没事,现在反正重新搞数据库了,再加上要弄个内存表,到时候你MySQL就算真是filesort魔,你也是在内存里面filesort,逃不出我的五指心。
全部改动:
分析了半天,痛定思痛,最终我敲定了改动方案,就是如上所述的,内存表+数据类型的改动。
`
2.MySQL的逆袭
期待已久的MySQL的逆袭终于来了,扛起MEMORY大旗,装备VARCHAR利器,启用日期INT,面对如此犀利的MySQL娘,SQLite娘该如何抵挡呢?(什么?我这个为什么是百合大战?那当然是因为百合美啊)
`
创建内存表:create table memcd (id bigint primary key,title varchar(255),updtime integer) engine=memory;
修改内存表上限my.cnf,默认的16M不够用: max_heap_table_size = 128M
迁移内存表:我写了个python的脚本干这事
import sqlite3 import MySQLdb import time mysql_user="username" mysql_pw="password" mysql_db="simplecd" db = sqlite3.connect("verycd.sqlite3.db") dbm = MySQLdb.connect(user=mysql_user,passwd=mysql_pw,db=mysql_db) c = db.cursor() for i in range(0,250): print i # 显示一下进度,没有什么特殊含义 c.execute("select verycdid,title,updtime from verycd order by verycdid limit ?,?",(i*1000,1000)) data = c.fetchall() data2 = [] for r in data: title = r[1].encode("utf-8") updtime = int(time.mktime(time.strptime(r[2],"%Y/%m/%d %H:%M:%S"))) data2.append((r[0],title,updtime)) cm = dbm.cursor() cm.executemany("""replace into memcd values (%s,%s,%s)""",data2) dbm.commit() c.close() cm.close()
然后在建立索引,因为memory表默认用hash索引,所以我们得申明用btree的索引
mysql> create index updtidx on memcd (updtime) using btree;
Query OK, 243819 rows affected (1.15 sec)
Records: 243819 Duplicates: 0 Warnings: 0
`
结果,猜想二得到验证,mysql用了index
mysql> explain select * from memcd order by updtime limit 10,1; (Key:updtidx)
`
但是一定要有limit,而且limit的数据不能太大,一旦分页很大时MySQL又变身为filesort魔王了
mysql> explain select * from memcd order by updtime limit 20000,1; (Extra:Using filesort)
`
等到做like的时候,又是filesort魔王了
explain select * from memcd where title like ‘%7%’ order by updtime limit 20000,1;(Extra: Using filesort)
`
不管了,反正都在内存里面玩,你应该会快点了吧?果然在查找不存在的数据时,MySQL争气地跑了0.19多, 可是在查找搜索结果很多时,MySQL的分页性能又给自己找麻烦了,select * from memcd order by updtime desc limit 240000,20;这种语句都执行了0.36秒,除非我禁止查看最后一页,否则要解决这个问题可是非常的麻烦。问题是MySQL用了内存数据库跑到0.19的代价是吃掉了我128MB的内存,相比SQLite来说才快了1倍不到点,这是在我的电脑上的结果,在服务器上差距还要小一点,内存更为吃紧一点;再加上还有分页慢的bug,从用户体验上来说可能更为得不偿失:毕竟0.2秒和0.1秒的搜索时间差距可能很难察觉,可是0.04秒和0.4秒的翻页差距可是很明显的。而且从统计数据来看,翻页也是经常性行为。虽然说很大一撮都是各种搜索引擎在翻页,可挡不住人多啊,要是每个都0.4这样下去,只要有超过3个人同时翻页网站就卡成一坨,一想到这就雷到不行,这怎么挡得住@.@
`
三个猜想除了猜想二稍微有点准头外几乎全灭,limit很慢的问题还是没有解决,消耗掉的内存过于庞大(真不知道它怎么用的,我算算应该不需要那么128M那么多内存才对,后来测试SQLite表明,只需要55M左右就够了),MySQL的这次逆袭可谓失败多过成功,略微提升了一点搜索速度(都内存数据库了再不提高就好去死了),但是代价高昂,内存消耗过大+分页性能照旧很差。再有一个问题就是内存数据库的数据一致性问题,主力数据库必然还会是硬盘数据库,那么每次数据库操作都要做双份的,还必须不时做同步检查,以防mysql重启后数据库丢失。仔细算算,收益抵不上造成的麻烦啊。
`
在经过一番挣扎,MySQL娘交出了一份just so so的答卷,我对其表现比较不满,就这样的状况我实在无法决定改用MySQL。
`
3.PostgreSQL的乱入
兔死狐悲,作为更为老牌的数据库,PostgreSQL一直和MySQL一起被人们视作开源数据库的两大领军人物,似乎MySQL因为各种讨人欢喜的易用小功能而更受欢迎一点,而PostgreSQL一贯的稳重保守使得它在性能上似乎有弱于MySQL的风声。有篇过期评测可以说明这个现象:http://www.sqlite.org/speed.html。
不过这都是老黄历了,P娘后来卧薪尝胆,据说现在性能完全压倒MySQL。在偏僻的Google搜索角落里面,随处可以见到MySQL数据量上去相比PostgreSQL像蜗牛一样的评语,而P娘在稳定性方面的评价更是远远胜过MySQL娘,在我们M娘和S娘进行百合大战的时候,P娘正在外围围观,不时地出现在我搜索结果中。等到M娘失败,P娘立刻挺身而出:M娘那种不专业啊,就那个内存引擎有点亮点,让我来对付S娘,要让你知道什么叫做专业。
`
P娘实在太专业了,一上来安装完毕之后我一头雾水,都不知道怎么创建用户,好不容易搞定后命令行界面更看不懂了,都不知道怎么看schema,语法也有很多不支持,比如replace into都不支持让我比较无语,而limit a,b的不支持让我有点惊讶,搞了半天P娘也是有limit offset语法的,那为啥不支持limit a,b?可以想见这P娘的性格一定是很别扭的,核心函数都写了,只是多提供一个接口函数都不肯,到底算是她傲娇呢还是大小姐属性过重呢?
`
被P娘折腾地有气无力我再也没精力写详细过程了,直接说结果好了,P娘果然不负专业二字,不但在select速度上胜过SQLite,在资源占用上也要优于MySQL的内存表,就连limit性能也有了很大的改进,不过仍然需要0.2秒,我大惊之下看了看sqlite,发现居然也要0.04秒,这一事实让我非常之想不通。
`
如果只有MySQL性能差那我还可以理解,全那么差那就是我有问题了。可是limit那么常用的操作,稍微做点改进就能把速度提上去的。当然limit如果和where混用,不计算出整个表的搜索结果是没有办法知道第几个的,在这种情况下limit性能取决于offset的个数。那么数据库在做limit的时候(针对我的应用的情况)很可能就是按照order by的顺序来access数据库,然后再根据where的like语句进行比对,符合了再用计数器计数。
`
但是按照这种思路来得话,例如 ’select * from db order by x limit 200000,1′ 这样的语句只需要访问索引,以MySQL Memory引擎来说,这是一个在内存中的数据库,只需要遍历一下BTree索引就可以了,遍历一个大小仅为20w的BTree需要0.4秒么?这极度不合理啊,我觉得要再快个1000倍甚至1w倍才是正常速度,理解不能。如果说P娘算是个性比较别扭的话,那M娘简直就是莫名其妙了。因为P娘和S娘遍历速度慢还可以找借口说那是因为IO原因,(实际上SQLite的内存数据库速度也没有提高),可M娘实在有点慢得过分。
`
4.数据库什么的才不要呢
口胡,数据库众娘实在太难伺候了,要么就是select性能不能让我完全满意(S娘和P娘终究还是要比M娘的内存模式慢一点),要么就是性能满意了但是内存消耗超乎常理的大,而且都有分页慢的奇怪现象。
`
既然MySQL这种莫名傲娇娘都可以提速,那么我索性自己动手丰衣足食好了,不就是个树么。我再给添加一个表明自己是老几的数据结构,把分页做到纳秒级别也毫无问题啊。至于用什么树呢?BTree这种树是为了减少IO而设计的,既然都跑内存了那还是用二叉树性能要好一点。经典的自平衡二叉树好像就是AVL,RedBlack,还有Splay了,研究了一下,RB和SPLAY其实是提升了插入和删除速度,我这应用不太在意插入和删除,因为这两种操作较少,所以平衡性更佳的AVL树就是我的不二选择了。
`
不过这些个树真的要我来实现也是很头痛地事情,看算法就复杂得一坨,写实现又不知道会有多少bug,不知道要死掉多少脑细胞,@@。好在pypi太猛了,居然有一个叫做pyavl的包,http://pypi.python.org/pypi/pyavl/1.12_1 这个包用c实现了avl的加强版,可以直接获取rank,遍历也极为方便。做了个测试
import avl import sqlite3 import time import timeit # order by the third element(updtime) desc def compare3(a,b): if a[2] < b[2]: return +1 if a[2]==b[2]: return 0 return -1 db = sqlite3.connect("verycd.sqlite3.db") c = db.cursor() c.execute("select * from vcidx") data = c.fetchall() t = avl.new(data,compare=compare3) c.close() def access(): for x in xrange(200000,240000,40): t[x:x+20] #测试时间 timer = timeit.Timer("access()","from __main__ import access") print timer.timeit(number=1000)
其中vcidx的schema如下
CREATE TABLE vcidx ( id integer primary key, title varchar(255), updtime integer );
`
上面代码测试了分页获取的情况,从limit 200000,20一致测到limit 240000,20,测下来是6us,也就是每取20个指定位置的东东平均只需要6×10e-6秒。而且看清楚,这个可不是定位,而是获取了20个数据所花费的时间,我觉得大部分时间其实用在内存存取上了,看源码就可以知道,get_rank是一个位运算,定位实际上只需要10多次内存存取加10多次位运算而已。
`
搜索的代码也非常简单,如下
def search(q,limit=1,offset=0): ans = [] for x in t: if q in x[1]: if offset == 0: ans.append(x) limit -= 1 if limit == 0: break else: offset -= 1 return ans #测试时间 timer = timeit.Timer("search("kekek",limit=1,offset=0)","from __main__ import search") print timer.timeit(number=10)
这个代码做了10次kekek的搜索,因为找不到任何东西,所以等若遍历了整个数据库,共花费1.6秒,也就是平均每个0.16秒,优于M娘的0.19秒,同时内存占用也好过M娘,只有70M,虽然还是大了点,但是解决了搜索速度和分页的速度问题。M娘要是知道我用12行代码完成了它Memory引擎的工作,而且速度更快,还解决了分页慢的顽疾,估计会哭出来吧,真可怜,还是不让她知道算了。代码中的字符串和比较判断还是用python做的,意味着如果纯C来实现的话性能和内存占用可能会更佳,不过这个方案简洁直接,又能和python结合得很好,我很满意了。
`
要把代码实用化,可能还需要加一些东西,比如做成daemon运行在后端当服务用,通信的话用socket?不过我没用过进程间通信,估计写起来会费点周章,还有可能需要增加同步数据库内容的代码,这样作为一个单独的查询用进程放在后台,性能会比我现在使用的这套流程提高4倍,(因为用sql的count计算搜索结果总数会多消耗一倍时间,再加上本来内存数据库就快一倍。)更重要的是因为这是最容易导致数据库锁住的部分,这部分不用数据库的好处就是sqlite基本上不会锁了,也就不需要换数据库了(谢天谢地,否则更麻烦了)。所以严格说来虽然性能提高了4倍,但是真正服务器负载量提高的估计不止4倍,10倍也是有可能的。要说这个方案的缺点么,那就是增加了不少工作量,想想都觉得写起来必定会很麻烦。
5.结论
正如我在做SimpleCD的一开始就提到的,数据库什么的效率也不见得高,只是省了麻烦而已,自己实现性能会更好,没想到那么快就做到这一步了。暂时因为SimpleCD现有的各种优化下,短期内这个搜索优化是不会派上用处的,在可以预见的未来也不至于会派上用处。要是真的要用上了,那SimpleCD的流量恐怕得是100wPV级的了吧?这种流量我牙齿都要笑掉了。
`
最后附上我测试的一些数据吧,再有对于SMP这三个数据库,我更新一下我个人的偏好:
sqlite: 适合简单高性能低并发的应用, sqlite真是有够lite,但是却也有够强大的,我真是非常喜欢sqlite的低内存使用和高性能,泪流满面。
postgresql: 适合复杂高性能高并发的应用,这东东我必须说很好很强大,它的表结构还能继承的,真是orz了,某些古怪的设定只是其高傲的一种体现。性能和功能上平衡得相当好,就是我乍一看上去貌似有点难用,不像mysql和sqlite那么方便。不过我一向信奉一力降十会的,花哨不能当饭吃,底子厚了才有花哨的本钱啊。我的下一个应用估计就会是P娘了,有了体会再和S娘和M娘做比较吧。
mysql: M娘真是多才多艺到一定境界了,虽然我用不来那些高端的应用,但是看看介绍就觉得实用。奈何我实在是觉得功能再多,性能不高也是渣。所以一如既往地黑化MySQL,而且因为这次对数据库有了更多了解,所以我黑MySQL黑得更有底气一点了。M娘其实骨子里也是轻量级数据库啊。看看人家facebook和youtube在用,但是他们的用法一般人都学不来,说实话我觉得那么用和自己开发一套适合自己的DB的复杂度也差不多了。说到底可以那么用MySQL,当然也就可以那么用PostgreSQL,etc…
`
我目前的偏好: postgresql>sqlite>mysql
`
PS.本文数据均在T43笔记本上所得,相比服务器烂了不少,呵呵,是想尽量排除服务器可用资源不稳定导致测试不准的情况。
#####################
MySQL内存表的一些搜索数据
#####################
$ mysql -V
mysql Ver 14.14 Distrib 5.1.37, for debian-linux-gnu (i486) using EditLine wrapper
mysql> select * from memcd where title like ‘%5%’ order by updtime limit 30000,1;
Empty set (0.17 sec)
mysql> select * from memcd where title like ‘%jiong%’ order by updtime;
Empty set (0.17 sec)
mysql> select * from memcd where title like ‘%rm%’ or title like ‘%mp%’ order by updtime limit 200000,1;
Empty set (0.31 sec)
mysql> select title from memcd order by updtime desc limit 240000,1;
1 row in set (0.48 sec)
##########################
同一条语句,不同数据库的横向比较
##########################
select * from verycd where title like ‘%kekek%’ order by updtime limit 1;
`
MySQL普通表(优化过数据结构)Empty set (1.58 sec)
MySQL内存表 Empty set (0.19 sec)
PostgreSQL普通表 Total query runtime: 318 ms.
SQLite普通表 CPU Time: user 0.896056 sys 0.080005
SQLite附加索引(updtime,title) CPU Time: user 0.360023 sys 0.048003
SQLite原版数据库(不改数据结构)CPU Time: user 0.432027 sys 0.036002
SQLite Memory数据库不加索引 CPU Time: user 0.792049 sys 0.000000
SQLite Memory数据库附加索引 CPU Time: user 0.340021 sys 0.000000
自制搜索引擎+psyco优化(共12行代码): 0.13s
`
##############
内存数据库的对比:
##############
额外内存消耗:
MySQL: >128M
SQLite: ~50M
PostgreSQL: 不支持
速度提升:
MySQL: 800%
SQLite: 15%
PostgreSQL: 0%