報表的效能問題是怎樣產生的?又該如何解決?

bubblegum發表於2020-07-21

報表效能是總也避不開的話題,報表作為 OLAP(線上聯機分析)中的主要應用場景,無論從涉及資料的寬度(表數量),還是資料的廣度(查詢範圍)都可能非常巨大;而且在報表中還經常伴隨非常複雜的資料處理邏輯,這些都會影響報表的執行速度。而伺服器環境、資料庫環境、JDBC 效率、網路環境、客戶端環境這些也都都跟報表效能密切相關。

報表效能可能跟很多因素有關,非常複雜。這裡我們試著從報表執行的各個階段來分析報表效能問題產生的主要原因及其應對方法。未盡之處,歡迎討論。

我們知道報表執行主要分報表解析、資料準備、資料傳輸、報表計算和報表生成 5 個階段。除了報表解析是引擎載入解析模板,還未開始運算外,其他 4 個階段(示意圖中黃色的部分)均可能引起效能問題。

imagepng

我們在分析報表效能問題時一定要先定位是哪個階段引起的,抓主要矛盾。定位的方法也很簡單,就是分析報表執行日誌,很多報表工具都會輸出各個階段的運算時間,看看哪個階段耗時最長,就是問題發生的主要階段了。

1. 資料準備
報表資料準備是指從資料來源中讀取資料並將資料組織成報表可用的結果集的過程。在報表中往往是以資料集的形式存在,可以透過 SQL、儲存過程或 JAVA 實現。

資料準備並不是簡單地將資料來源的原始資料取出就結束了,而是會伴隨一些計算過程,有些還很複雜,可以想想一下平時我們開發報表時編寫的 SQL 絕大多數情況下都不是簡單的 select * from tbl。如果這個 SQL 比較複雜(由於大多數情況都是使用 SQL 準備資料,所以這裡我們僅以 SQL 為例,其他資料來源可以參考 SQL 資料集的解決辦法)執行較慢,就會導致報表變慢。

這個階段在資料庫中執行,本質上跑的快慢都跟報表無關,但好的報表工具是可以干預最佳化這個過程的(後面再說)。

資料準備階段慢,可以先試著最佳化 SQL,但 SQL 最佳化往往復雜度較高,所以也經常採用預計算的方式進行效能最佳化(這裡不討論硬體升級、資料庫擴容等物理最佳化手段)。

(1) 最佳化 SQL
我們知道複雜 SQL(關聯多、巢狀多)是比較難最佳化的。資料庫的透明化機制讓寫 SQL 時不用關心底層的執行順序,由資料庫最佳化器自動執行,這樣可以簡化編寫 SQL 的難度,但過於透明的機制讓我們很難干預 SQL 的執行路徑,也就很難最佳化 SQL 了。

(2) 預計算
SQL 無法最佳化或最佳化效果不理想時,透過預計算可以提升報表資料準備效率。將報表需要的結果集事先加工出來儲存到中間表中,報表查詢時直接讀取加工好的結果集,這樣就可以節省大量計算時間,從而提升報表效能。預計算本質是用空間換時間的手段。

不過,預計算的缺點也非常明顯,你不妨先思考一下有哪些問題?

主要的問題有兩個:時間問題和空間問題。

時間問題
預計算需要“事先”加工,其本質是一個 ETL 過程,這樣就會引起兩個時間問題,資料的實時性和預計算的時間成本。

報表基於預計算的結果查詢只能查詢預計算時點以前的資料,無法查詢預計算到當前時間的資料,這樣就導致了資料時效性差,不適合資料實時性要求高的業務場景(比如交易系統)。

預計算往往會放到業務空閒的階段進行,比如前一天夜裡到第二天上班前,通常還要預留一些時間容錯(跑失敗了要重新跑),這樣可能留給預計算的時間也就 5、6 個小時,這個時長基本是固定不變的,而資料規模、業務複雜度、報表數量都會不斷增長,未來極有可能會引發預計算跑不完而影響業務使用的問題。

空間問題
預計算的結果是要落地的,往往會儲存在資料庫物理表中,這樣的表多了會佔用大量資料庫空間,引起資料庫容量問題;另外這樣的表多了還會帶來管理上的問題。

預計算的應用範圍很窄,而 SQL 又比較難最佳化,還有什麼其他最佳化手段嗎?

(3) 藉助其他高效能運算引擎
在資料庫內搞不定報表資料準備階段最佳化時可以引入其他高效能運算引擎,這在業內並不少見,但幾乎所有技術都是採用獨立儲存 + 分散式架構來輸出高效能的,這對報表應用架構(主要是資料庫)的改變就有些大了,難度很大。

比較簡單直接的方式是在報表內部就能提供改變 SQL 執行路徑的手段,並且可以改善那些顯著低效的 SQL 演算法(比如 topN),這樣就可以在不改變應用架構的情況下實現資料準備階段的最佳化。

這就要求報表工具提供資料準備階段的計算能力,既可以分步執行 SQL(指定執行順序),還可以改善演算法效率。其表現形式可以是一種內建在報表工具中的計算指令碼,指令碼具備分步計算和強計算能力。

imagepng
報表工具計算模組提供可干預 SQL 執行路徑能力和高效的強計算能力


2. 資料傳輸
報表透過 JDBC 介面訪問資料庫讀取所需資料時,如果資料量比較大或者資料庫 JDBC 效能較差(要知道各種資料庫的 JDBC 效率是不同的)會導致資料傳輸時間過長,導致報表變慢。

由於我們沒法改變資料庫的驅動,我們只能在報表層面想辦法。一個可行的辦法是透過並行取數來提速。

(1) 並行取數
透過建立多個資料庫連線(這時要求資料庫相對空閒),採用多執行緒的方式同時讀取報表所需資料,可能是同一個表,也可能是多個表關聯計算後的結果,這樣資料傳輸的時間理論上就會縮短到原來的 1/n(n 是執行緒數),從而提升報表效能。

那麼這種並行取數實現起來難度如何呢?

因為目前大部分報表工具不支援並行取數,要想透過並行來加速資料傳輸只能自己使用 JAVA 硬編碼來做。我們曾經討論過在報表中使用 JAVA 硬編碼準備資料的弊端,而編寫並行程式難度又提升了一級,要知道,並行以後還要歸併(merge)各個執行緒的計算結果,merge 不是簡單地縱向拼接就完了,有時還涉及分組和順序。

使用支援並行取數的報表工具
比較簡單有效的方式是使用支援並行取數的報表工具,報表開發人員不需要考慮資料資源衝突、結果歸併等複雜問題就可以簡單實現。

(2) 非同步傳輸
當資料量較大時,可以透過非同步機制將資料分批返回給報表,報表接收部分資料後就立刻呈現,後臺同時進行不間斷的資料傳輸,這樣就可以提升報表的呈現速度。

非同步傳輸需要考慮很多方面,不建議自己硬編碼實現,最好使用支援非同步傳輸功能的報表工具,簡單快捷。


3. 報表計算
資料傳輸完成後,報表引擎會根據已準備資料和報表表示式運算報表,這個階段也是非常容易出現效能問題的。

效能問題常見於多資料集報表。

現在很多報表工具都提供了多資料集能力,允許開發者在一個報表中建立多個資料集,這樣可以分別組織資料,尤其當資料來自不同資料庫時,多資料集尤其方便。但這種方式為報表開發帶來便利的同時卻會帶來很嚴重的效能問題。

我們知道,多資料集的關聯是在報表單元格的表示式中完成的,類似這樣 ds2.select(ID==ds1.ID),報表引擎在解析這個表示式時會按照順序遍歷的方式完成關聯,即從 ds2 中拿出一條記錄,到 ds1 中遍歷,查詢 ID 相同的記錄;然後再拿第二條再去遍歷查詢;…
這個運算複雜度是平方級的,資料量小的時候沒什麼影響,資料量稍大時效能就會急劇下降。
imagepng

另外,報表單元格還帶有大量呈現屬性(顏色、字型、邊框等),帶著這些屬性運算也會拖慢報表的執行速度。所以,對於資料量較大的多個資料集,最好在資料準備階段使用更高效的關聯演算法完成關聯。

在資料準備階段完成關聯

將多資料集關聯轉移到資料準備階段完成,透過 SQL 就可以實施更加高效的關聯,只是 SQL 只能基於單一資料庫操作,如果資料來自多個資料來源時就不靈了。這時,最好報表工具在資料準備階段還提供諸如 HASH JOIN 等高效能關聯演算法,以便滿足異構資料來源時的關聯運算。HASH JOIN 演算法可以整體地看待幾個資料集,效率比報表工具採用的過濾式關聯要高得多,幾千行規模時幾乎是零等待。


4. 報表生成
報表計算完成後會生成引擎會將計算結果以 HTML 的形式輸出,生成最終要呈現報表。這個階段的效能問題主要有兩方面。

(1) 頁面渲染慢
當報表中新增了大量的呈現效果(隔行異色、背景圖、警戒色等)時,頁面渲染的速度就會變慢。減少過多的報表效果使用可以提升渲染速度;如果這些效果是必須的,那就要比拼使用的報表工具的能力了。

另外,如果報表單頁的內容過大也會影響頁面呈現的速度。對於比較高或者比較寬的報表呈現時建議採用分頁方式輸出,從而提升頁面呈現效率。

(2)客戶端環境差
總體上是跟使用者端環境有關係,比如網路問題、裝置問題等,遇到時需要具體分析。


透過了解報表執行各階段可能出現的效能問題,以及對應解決辦法,有些我們可以硬編碼或藉助其他技術來解決,有些還需要報表自身來提升。最直接的方式是使用能夠提供這些能力的報表工具,這個工具大概要具備這些特性才能幫助我們有效解決報表效能問題。

1. 支援分步
這樣可以有效最佳化 SQL 執行路徑,提升資料準備效率;

2. 內建強計算能力和高效演算法
能夠改善原有低效的演算法,提高資料計算效率;
支援 HASH JOIN 等高效能關聯演算法,以減少在呈現模板中進行多資料集關聯;

3. 支援並行取數
能夠透過多執行緒並行方式提升資料傳輸速度效率;

4. 支援非同步讀取
提供非同步取數非同步呈現機制,加速大資料量報表的呈現效率;

5. 支援多資料來源
提供多樣性資料來源介面,並能夠進行多資料來源關聯運算(同 2)。

參考資料:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69900830/viewspace-2701431/,如需轉載,請註明出處,否則將追究法律責任。

相關文章