大圣魔方——美团点评酒旅BI报表工具平台开发实践
當前的互聯網數據倉庫系統里,數據中心往往存放了大量Cube化或者半Cube化的數據。如果需要將這些數據的內在關系體現出來,需要寫大量的程序和SQL來發現數據之間的內在規律,往往會造成用戶做非常多的重復性工作;而且由于沒有數據校驗的機制,還容易出錯,無法直觀查看各種數據(沒有可視化的UI圖表)。這時就急需一款基于Cube的報表工具快速為用戶提供報表服務,可以完成多維查詢、上卷、下鉆等各種功能。為此,美團點評酒旅技術團隊開發了大圣魔方。
一款好的BI報表工具,需要考慮并能夠解決如下問題:
- 統一數據源
- SQL生成
- 跨數據源數據聚合
- 自定義計算指標
- 數據權限
- 標準化UI組件,自助生成可視化報表
體系架構
圖1 大圣魔方體系架構
具體方案
1. 統一數據源
提供多數據源查詢服務,需要解決的問題主要是兩個:
圖2 大圣魔方多數據源
大圣魔方上對能夠通過SQL查詢的數據源,例如MySQL和Kylin都通過統一SQL查詢來獲取數據;對于ES(Elasticsearch)采用ES提供的API來查詢;對于普通文本格式的數據采用自定義API從數據源獲取數據。
如圖2所示,大圣魔方只是從數據源里面獲取基礎的數據,之后通過實現自己的計算引擎對數據進行聚合、切割等操作,對此,魔方中設置了四個引擎,用于實現不同的功能。
2. SQL生成
對于SQL的生成也存在兩個問題:
針對第一個問題,我們對SQL模板進行了定義,當選擇不同的數據源時,根據數據源的Dialect選擇不同的SQL模板,而這就決定了SQL的組成部分(骨架)。
為了解決第二個問題,我們在SQL模板的基礎之上做了內容填充和替換操作,選擇具體的維度、指標和篩選項的值,再填充到SQL模板的不同地方,最終就會生成能夠被數據源執行的SQL。
在SQL生成的時候也考慮過其它的框架,如Apache Calcite Avatica、Alibaba的Druid,但是最終都放棄了,原因也是基于兩個方面:
最終,我們采用了SQL模板和字符串填充替換操作來完成。為此我們在Java的正則表達式基礎之上做了一個功能很多的字符串操作類庫。
3. 跨數據源數據聚合
一般情況下,同一個數據源的大部分數據源引擎都能夠支持多表的join操作,但是也存在不支持的,例如老版本的Kylin就不支持多Cube的join操作,還有一個更重要的問題是數據源引擎無法解決跨數據源的數據聚合問題,必須要自己實現數據的聚合操作,一般的情況下需要自己去實現inner join、left outer join和full outer join的邏輯。
大圣魔方實現了inner join和left outer join兩個邏輯,因為full outer join的需求場景不是很多,所以沒有實現。下面是大圣魔方的實現代碼:
inner join核心代碼
private void join(List<Map<String,String>>[] contents,List<Project> sharedList,final int n,int[] rowsStatus,LinkedList<MatchRow> result){if(this.cubeJoin==1){throw new java.lang.IllegalArgumentException("left join call leftJoin method,not call join method");}if(n<contents.length){List<Map<String,String>> list = contents[n];for(int k=0;k<list.size();k++) {boolean equal = true;if(n!=0) {Map<String, String> prev = contents[n - 1].get(rowsStatus[n - 1]);Map<String, String> cur = list.get(k);for (Project proj : sharedList) {String key = proj.fieldName.toUpperCase();if (key.matches("^\\d+$") || key.equals("*")) {key = "_";}key = proj.isCompanion() ? key + proj.getFactId() : key;String prevValue = prev.get(key);String curValue = cur.get(key);if (prevValue == curValue) {continue;}if (prevValue == null || curValue == null || !prevValue.equals(curValue)) {equal = false;break;}}}if (equal) {rowsStatus[n] = k;if(n==contents.length-1){//last dataset matchMatchRow mr = new MatchRow();List<MatchRow.DatasetRow> tmp = new ArrayList<>();for(int i=0;i<rowsStatus.length;i++){MatchRow.DatasetRow dr = new MatchRow.DatasetRow();dr.setDatasetIndex(i);dr.setRowIndex(rowsStatus[i]);tmp.add(dr);}mr.addMatchRow(tmp);result.add(mr);}else{join(contents,sharedList,n+1,rowsStatus,result);}}}}}上述代碼就是通過回溯算法實現inner join的核心邏輯,具體解析如下:
- contents參數表示每個數據源里面的結果集。
- sharedList表示關聯的字段。
- n和rowsStatus是回溯算法記錄狀態用的。
- result里面包含的是符合join條件的記錄。
- MatchRow里面記錄的是一個數據源里面的某一行與其余的數據源里面的那一行是相等的,記錄的是下標號。
只有當sharedList里面的每個字段都相等的時候,兩條記錄才滿足inner join的條件。這個算法是一個通用算法,因為是通過回溯算法實現的,所以要join的數據源理論上可以有無限個。
left outer join核心代碼
private boolean leftJoin(List<Map<String,String>>[] contents,List<Project> sharedList,final int n,int[] rowsStatus,LinkedList<MatchRow> result){boolean leftJoinMatch = false;if(n<contents.length){List<Map<String,String>> list = contents[n];for(int k=0;k<list.size();k++) {boolean equal = true;if(n!=0) {//in left join,compare with the first dataset.Map<String, String> prev = contents[0].get(rowsStatus[0]);Map<String, String> cur = list.get(k);for (Project proj : sharedList) {String key = proj.fieldName.toUpperCase();if (key.matches("^\\d+$") || key.equals("*")) {key = "_";}key = proj.isCompanion() ? key + proj.getFactId() : key;String prevValue = prev.get(key);String curValue = cur.get(key);if (prevValue == curValue) {continue;}if (prevValue == null || curValue == null || !prevValue.equals(curValue)) {equal = false;break;}}}if (equal) {leftJoinMatch = true;rowsStatus[n] = k;if(n==contents.length-1){//last dataset matchMatchRow mr = new MatchRow();List<MatchRow.DatasetRow> tmp = new ArrayList<>();for(int i=0;i<rowsStatus.length;i++){MatchRow.DatasetRow dr = new MatchRow.DatasetRow();dr.setDatasetIndex(i);dr.setRowIndex(rowsStatus[i]);tmp.add(dr);}mr.addMatchRow(tmp);result.add(mr);}else{//if next dataset is not match,use the next's next...for(int loopFlag=n+1;loopFlag<rowsStatus.length;loopFlag++){boolean match = leftJoin(contents,sharedList,loopFlag,rowsStatus,result);if(match){break;}rowsStatus[loopFlag]=-1;if(loopFlag==contents.length-1){MatchRow mr = new MatchRow();List<MatchRow.DatasetRow> tmp = new ArrayList<>();for(int i=0;i<rowsStatus.length;i++){MatchRow.DatasetRow dr = new MatchRow.DatasetRow();dr.setDatasetIndex(i);dr.setRowIndex(rowsStatus[i]);tmp.add(dr);}mr.addMatchRow(tmp);result.add(mr);}}}}}}return leftJoinMatch;}上面的代碼是left outer join的實現邏輯,同樣也是用的回溯算法,它與inner join有2個不同之處:
4. 自定義計算指標
使用自定義計算的原因,主要是基于下面的兩個方面:
對此,我們對大圣魔方做了如下操作:
5. 數據權限的問題
只要是有數據展示,數據權限問題就無法避免,權限主要是分為報表的可查看權限和維度、指標權限。權限遇到的最主要的問題是構成權限矩陣的數據量太大,參與者有用戶和組織,權限的實體有維度和指標,這樣大的數據維護起來的成本很高;其次是權限數據配置會占用很多的人力。
對此,我們做了如下操作:
6. 標準化UI組件,自助生成可視化報表
報表上展示數據需要有各種各樣的圖表,沒法為用戶只做一個統一的報表,這個時候需要用戶能夠創建自己想要的報表,這時需要提供一個標準的組件庫、布局庫和一些常用的模板。用戶選擇好想要的模板,然后選擇布局對報表頁面進行布局,接著在每個布局里面填充不同的組件,這樣就可以構建一張報表了,也就是我們常說的所見即所得的方式。
大圣魔方就是采用上述的機制提供了一套可視化報表編輯工具。使用它可以快速地創建一個報表,管理人員只需要維護對應的組件、布局和模板就行了。
上述幾點就是大圣魔方的一個總綱,其中大部分功能已經實現了,還有一小部分處于開發之中(標準化UI組件、自助生成可視化報表)。目前大圣魔方已經上線將近一年了,支持了內部眾多業務,后續我們還會在UI易用性、星型模型、配置簡化、元數據同步等方面做一些提高。
最后插播一個招聘廣告,有對BI工具開發感興趣的可以發郵件給 fuyishan@meituan.com
總結
以上是生活随笔為你收集整理的大圣魔方——美团点评酒旅BI报表工具平台开发实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 智能投放系统之场景分析最佳实践
- 下一篇: 美团数据仓库-数据脱敏