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