MySQL大戰SQLite(PostgreSQL強勢亂入)

http://obmem.info/?p=493

評論也很有意思,值得一看

--------------------------------------------------

[其實是技術貼]MySQL大戰SQLite(PostgreSQL強勢亂入)
Posted on February 16, 2010 by observer
-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個指定位置的東東平均只需要6x10e-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%


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章