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