第3期:功夫都在報(bào)表外-漫談報(bào)表性能優(yōu)化
應(yīng)用系統(tǒng)中的報(bào)表,作為面向業(yè)務(wù)用戶的窗口,其性能一直被高度關(guān)注。用戶輸入?yún)?shù)后都希望立即就能看到統(tǒng)計(jì)查詢結(jié)果,等個(gè)十幾二十秒還能接受,等到三五分鐘的用戶體驗(yàn)就非常惡劣了。
那么,報(bào)表為什么會(huì)慢,又應(yīng)當(dāng)從哪里入手進(jìn)行性能調(diào)優(yōu)呢?
數(shù)據(jù)準(zhǔn)備
當(dāng)前應(yīng)用中的報(bào)表大都用報(bào)表工具開發(fā),當(dāng)報(bào)表響應(yīng)太慢時(shí),不明就里的用戶就會(huì)把矛頭指向使用報(bào)表工具的開發(fā)人員或者報(bào)表工具廠商。其實(shí),大多數(shù)情況報(bào)表的慢只是個(gè)表現(xiàn),背后的原因是數(shù)據(jù)準(zhǔn)備太慢,在數(shù)據(jù)進(jìn)入報(bào)表環(huán)節(jié)之前就已經(jīng)慢了,這時(shí)再去優(yōu)化報(bào)表開發(fā)或壓迫報(bào)表工具并沒有用處。
報(bào)表是給人看的,人類視力限制不可能查看過(guò)多數(shù)據(jù),也就沒有大數(shù)據(jù)的呈現(xiàn)需求。報(bào)表工具為了直觀而采用的狀態(tài)式計(jì)算模型,也不適合實(shí)現(xiàn)有復(fù)雜過(guò)程的計(jì)算。報(bào)表環(huán)節(jié)不應(yīng)當(dāng)也無(wú)能力解決大數(shù)據(jù)和復(fù)雜計(jì)算問(wèn)題,只要處理小數(shù)據(jù)的擺位和簡(jiǎn)單計(jì)算,這不會(huì)耗用太多時(shí)間。
八成左右的報(bào)表慢是因?yàn)閿?shù)據(jù)準(zhǔn)備造成的。報(bào)表呈現(xiàn)的數(shù)據(jù)量雖然小,但涉及的原始數(shù)據(jù)量可能巨大,把大數(shù)據(jù)匯總和過(guò)濾成小數(shù)據(jù)需要很長(zhǎng)時(shí)間;復(fù)雜計(jì)算也是類似,主要時(shí)間消耗在數(shù)據(jù)準(zhǔn)備階段。數(shù)據(jù)準(zhǔn)備的優(yōu)化是報(bào)表提速的關(guān)鍵。
1. 優(yōu)化數(shù)據(jù)準(zhǔn)備代碼:一般是SQL(或存儲(chǔ)過(guò)程),某些時(shí)候是應(yīng)用程序的代碼(涉及非數(shù)據(jù)庫(kù)或多數(shù)據(jù)庫(kù)時(shí));
2. 數(shù)據(jù)庫(kù)擴(kuò)容:數(shù)據(jù)量大,代碼不能再優(yōu)化時(shí),還可以擴(kuò)容數(shù)據(jù)庫(kù),比如采用集群方案;
3. 采用高性能計(jì)算引擎:傳統(tǒng)數(shù)據(jù)庫(kù)在實(shí)現(xiàn)某些運(yùn)算時(shí)性能較差或成本太高,可以更換為其它計(jì)算機(jī)制。
數(shù)據(jù)計(jì)算
報(bào)表環(huán)節(jié)本身計(jì)算性能差的情況相對(duì)少,但也是有的。
一個(gè)典型的場(chǎng)景是多源關(guān)聯(lián)報(bào)表,即把多個(gè)二維數(shù)據(jù)集按某個(gè)主鍵對(duì)齊呈現(xiàn),有時(shí)可能還需要分組匯總。報(bào)表工具要求把計(jì)算都寫進(jìn)單元格,這樣只能用數(shù)據(jù)集過(guò)濾來(lái)描述本格和其它數(shù)據(jù)集的關(guān)聯(lián),類似ds2.select(ID==ds1.ID)的表達(dá)式。這個(gè)運(yùn)算復(fù)雜度是平方級(jí)的,在數(shù)據(jù)量不大時(shí)也無(wú)所謂,但數(shù)據(jù)量稍大(幾千行)且涉及數(shù)據(jù)集較多時(shí),性能就會(huì)急劇下降,從幾秒到幾十分鐘都有可能。
如果我們把這個(gè)運(yùn)算移到報(bào)表外,在數(shù)據(jù)準(zhǔn)備階段時(shí)處理,就可以大幅度提升性能。如果數(shù)據(jù)來(lái)自同一個(gè)數(shù)據(jù)庫(kù),那么用SQL寫JOIN語(yǔ)句就可以了,如果數(shù)據(jù)集來(lái)自多庫(kù)或者希望減輕數(shù)據(jù)庫(kù)計(jì)算壓力,也可以在外部實(shí)現(xiàn)HASH JOIN算法。HASH JOIN算法可以整體地看待幾個(gè)數(shù)據(jù)集,效率比報(bào)表工具采用的過(guò)濾式關(guān)聯(lián)要高得多,幾千行規(guī)模時(shí)幾乎是零等待。
報(bào)表計(jì)算性能差雖然發(fā)生在報(bào)表環(huán)節(jié)本身,但經(jīng)常卻要在報(bào)表外去解決。
其它類似場(chǎng)景還有,如帶部分明細(xì)行的分組匯總表,表現(xiàn)出來(lái)是由于報(bào)表環(huán)節(jié)處理數(shù)據(jù)量大導(dǎo)致運(yùn)算變慢,而解決方法也是把運(yùn)算移到報(bào)表外。
數(shù)據(jù)傳輸
報(bào)表還有個(gè)慢的瓶頸在于數(shù)據(jù)傳輸。
目前很多應(yīng)用都是J2EE架構(gòu)的,采用的報(bào)表工具也是Java寫的,這時(shí)訪問(wèn)數(shù)據(jù)庫(kù)都要用JDBC接口。然而,某些常用數(shù)據(jù)庫(kù)的JDBC驅(qū)動(dòng)性能很差(這里就不點(diǎn)名了),取出數(shù)據(jù)量稍多(幾萬(wàn)行)時(shí)就會(huì)有明顯的等待感。這就導(dǎo)致一個(gè)無(wú)奈的現(xiàn)象:數(shù)據(jù)庫(kù)壓力很輕計(jì)算很快,報(bào)表端計(jì)算也不算復(fù)雜,但報(bào)表仍然很慢。
無(wú)論應(yīng)用開發(fā)商還是報(bào)表工具廠商都沒辦法改變數(shù)據(jù)庫(kù)的JDBC驅(qū)動(dòng),只能在外面想辦法。經(jīng)過(guò)多次實(shí)驗(yàn),我們發(fā)現(xiàn)啟用多線程并行取數(shù)就能獲得數(shù)倍的性能(前提是數(shù)據(jù)庫(kù)負(fù)擔(dān)輕)。但是,目前還沒有報(bào)表工具直接提供了并行取數(shù)的功能(由于數(shù)據(jù)分段方法和數(shù)據(jù)庫(kù)及取數(shù)語(yǔ)法相關(guān),需要代碼控制,也不容易做成報(bào)表功能),這個(gè)方案仍然要在報(bào)表環(huán)節(jié)外的數(shù)據(jù)準(zhǔn)備階段來(lái)實(shí)施。
可控緩存
把近期訪問(wèn)過(guò)的報(bào)表緩存起來(lái),短時(shí)間內(nèi)再次訪問(wèn)同參數(shù)的報(bào)表時(shí)可以不必計(jì)算而直接返回,顯然這能改善用戶體驗(yàn)。很多報(bào)表工具也都提供有緩存功能,不過(guò)并不細(xì)致,緩存只能針對(duì)整個(gè)報(bào)表,而且各個(gè)報(bào)表的緩存是無(wú)關(guān)的。在報(bào)表外下點(diǎn)功夫可以實(shí)現(xiàn)控制力度更細(xì)致的緩存功能:
1. 部分緩存。有些報(bào)表、特別是常見的多源報(bào)表,其中大部分?jǐn)?shù)據(jù)相對(duì)穩(wěn)定(歷史數(shù)據(jù)),只有小部分?jǐn)?shù)據(jù)時(shí)效性差(當(dāng)期數(shù)據(jù))。而整個(gè)報(bào)表的緩存的有效期只能以較短的為準(zhǔn),這樣會(huì)導(dǎo)致報(bào)表經(jīng)常被重算。如果能只緩存部分?jǐn)?shù)據(jù),就能延長(zhǎng)這部分緩存的生命期,從而減少計(jì)算量。
2. 緩存復(fù)用。不同的報(bào)表可能引用到同樣的數(shù)據(jù),而互相無(wú)關(guān)的報(bào)表緩存機(jī)制則會(huì)迫使這些報(bào)表多次重復(fù)計(jì)算同樣的數(shù)據(jù)。如果能讓某個(gè)報(bào)表引用到其它報(bào)表已經(jīng)計(jì)算出來(lái)的緩存數(shù)據(jù),也能有效減少計(jì)算量。
這些復(fù)雜的緩存控制需要編寫代碼來(lái)實(shí)現(xiàn),不容易在報(bào)表工具中提供,但在可編程的數(shù)據(jù)準(zhǔn)備階段實(shí)施卻相對(duì)容易。
清單列表
前面說(shuō)過(guò),報(bào)表和大數(shù)據(jù)的直接關(guān)系并不大。甚至可以說(shuō)老是喊大數(shù)據(jù)報(bào)表的廠商多半是忽悠。
不過(guò)有一種清單列表確實(shí)是大數(shù)據(jù)報(bào)表。清單列表在金融行業(yè)經(jīng)常碰到,把一段時(shí)間的交易清單列出來(lái)。其特點(diǎn)是數(shù)據(jù)量特別大,可能會(huì)有幾千上萬(wàn)頁(yè),不過(guò)計(jì)算會(huì)相對(duì)簡(jiǎn)單,經(jīng)常只是羅列,最多有些按頁(yè)按組的匯總。
報(bào)表工具為了處理靈活的格間運(yùn)算,一般都會(huì)采用全內(nèi)存方式。這樣,把清單列表加載進(jìn)報(bào)表工具時(shí),會(huì)大概率出現(xiàn)內(nèi)存溢出;而且太大數(shù)據(jù)量全部取出并加載也需要很長(zhǎng)時(shí)間,用戶難以容忍。
容易想到的辦法是邊讀取邊呈現(xiàn),每次只呈現(xiàn)一頁(yè),不會(huì)溢出;讀滿一頁(yè)后立即呈現(xiàn),用戶不會(huì)有太強(qiáng)的等待感。數(shù)據(jù)庫(kù)都提供有游標(biāo)可以逐步讀出數(shù)據(jù),但用戶可能在前端翻頁(yè),這還需要高速隨機(jī)按頁(yè)(行)取數(shù)的能力。數(shù)據(jù)庫(kù)就沒有這種接口了,用條件過(guò)濾取數(shù)不僅很慢,而且還由于數(shù)據(jù)可能仍在更新而不能保證報(bào)表在生命周期內(nèi)的數(shù)據(jù)一致性。
結(jié)果還是要在數(shù)據(jù)準(zhǔn)備階段解決。兩個(gè)異步線程:一個(gè)負(fù)責(zé)從數(shù)據(jù)庫(kù)取數(shù)并緩存到外存(假定數(shù)據(jù)量大內(nèi)存裝不下),另一個(gè)接受前端請(qǐng)求從緩存中按頁(yè)(行)取出數(shù)據(jù)返回。