問題背景
發電裝置中常常會放置感測器(DCS)來採集資料以監控裝置運轉的狀況,某集團設計的電力監控統計系統,需要實時採集感測器的資料後儲存,然後提供按時段的實時查詢統計功能。
系統設計規模將支援20萬個感測器(以下稱為測點),採集頻率為每秒一個資料,即每秒總共會有20萬條資料,總時間跨度在1年以上。在這個基礎上實現任意指定時段的多個測點資料統計,包括最大、最小、平均、方差、中位數等。
系統原結構圖為:
系統中,使用者期望的統計響應延遲為:從20萬個測點中任取100個測點,統計頻率最高可能每隔若干秒呼叫一次,從總時間跨度中統計任意一天的資料,預期執行時間在1分鐘內,另外還會有少許離線任務,最長的時間段跨度長達一年。
現有的資料中臺中沒有計算能力,僅儲存資料,計算時需要透過RESTful介面取出資料再統計。經測試,透過RESTful介面從資料中臺取數,取出100個測點一天的資料量就需要10分鐘時間,還沒有開始計算,取數的時間已經遠遠超出了完成計算的預期時間。
基於現有結構,完成上述統計任務,效能上無法達到預期要求,需要將資料重新儲存。
解決辦法
第一步,梳理資料和計算要求
資料結構如下:
| 欄位名 | 型別 | 中文名|
| --- | ---| ---|
| TagFullName | 字串 | 測點名|
| Time | 長整型| 時間戳|
| Type | 數值| 資料型別|
| Qualitie | 數值 | 質量碼|
| Value | 浮點數| 數值|
計算要求為:在每秒生成20萬條記錄的時序資料中,任意時間段內,從20萬個測點中任取100個測點的資料,分別基於每個測點的數值序列統計最大、最小、方差、中位數等結果。
第二步,確定儲存和計算方案
20萬測點一天的資料,僅Value欄位,就要200000*86400*4位元組,至少64g記憶體,當總時間跨度為1年時,資料量會有數十T,單臺伺服器記憶體顯然裝不下。多臺伺服器叢集,又會帶來很高的管理和採購成本。
簡單按時間為序儲存的資料,可以迅速找到相應時間區間,但即使是這樣,單個測點一天也有86400條記錄,20萬個測點共17.28億條,每次統計都要遍歷這個規模的資料,也很難滿足效能要求。
那麼測點號上建立索引是否可行?
索引只能快速定位資料,但這些資料如果在外存中不是連續儲存的,硬碟有最小讀取單位,會導致大量無用資料量讀出,使得計算變得很慢,同樣也無法滿足效能要求。此外,索引佔用空間會隨著資料量增大而增大,並且插入資料的維護開銷也更大。
如果資料可以按測點號物理有序儲存,並在測點號上建立索引,相比時序物理有序儲存,查詢時,待查詢的測點記錄變得緊湊了,需要讀入的塊也就少了。100個測點的資料存成文字約300m不到,這樣即使使用外存也可以滿足效能要求。
只有歷史冷資料時,處理起來比較簡單,定時將資料按指定欄位排序即可。但實際上,每秒都會有20萬個測點的新資料,因為歷史資料規模巨大,如果每次獲取幾秒熱資料都與歷史資料整體按測點號、時間排序,即使不算排序,僅是重寫一遍的時間成本上都無法接受。
這時,需要將冷熱資料區分對待。不再變化的冷資料可以按測點次序準備好。這裡有一點變通,因為要將非常早期的資料刪除(比如一年前的),如果所有冷資料都按測點排序時,會導致資料維護比較麻煩,刪除早期資料會導致重寫一遍所有資料。因此,可以設計為先按時間分段,每段時間內的資料按測點、時間有序,整體資料還是按時間有序。任務需求是按天計算,這裡按天分段就比較合適,更長跨度的離線計算效能損失也不是很大。每當一天過去時,將昨天資料按上述規則排序後儲存,當天的資料作為熱資料處理。但是,當天內的資料量還是太大了,依然無法全部裝入記憶體,還需要再分。
經過一些測試後確認,我們發現將資料按熱度分為三層可以滿足要求。第一層,十分鐘內的熱資料透過介面讀入記憶體;第二層,每過10分鐘,將過去10分鐘的記憶體資料按測點、時間有序儲存到外存;第三層,每過一天,將過去24小時內的所有每10分鐘的資料按測點、時間有序歸併。總資料為:一年的資料由365段每天資料,加144段當天資料和一段記憶體資料。
分層後的冷熱資料屬於不同的資料來源,需要獨立計算同源資料的結果後,再將結果合併起來,算出最終的統計結果。即使計算方差、中位數這種需要全記憶體統計的情況,100個測點一天的資料量,也只需要64m記憶體。
第三步,確定技術選型和方案
從上述的儲存方案中得知,需要將實時資料按時間分段,段內按測點號、時間物理有序儲存,常規資料庫顯然沒辦法做到這點。此外,拆分資料需要可以支援按自定義時間段靈活地拆分;資料儲存時要具備高效能索引;冷熱資料屬於不同層(不在同一個資料來源),計算時需要分別計算後再合併。
完成該任務,用Java硬編碼工作量巨大,Spark寫起來也很麻煩。開源的集算器SPL語言提供上述所有的演算法支援,包括高效能檔案、物理有序儲存、檔案索引等機制,能夠讓我們用較少的程式碼量快速實現這種個性化的計算。
取數不能再用原系統的RESTful介面,也不合適直接透過API從DCS獲取資料。使用者方商定後引入kafka緩衝資料,遮蔽DCS層,同時還可以將DCS的資料提供給不同的消費者使用。變更後的系統結構圖如下:
說明:
- DCS系統每秒推送20萬個測點資料至Kafka MQ。
- Kafka MQ到SPL:使用SPL基於Kafka API封裝的Kafka函式,連線Kafka、消費資料。
- 記憶體緩衝:迴圈從Kafka消費資料(kafka_poll),每輪迴圈確保10秒以上的資料量,將每輪前10秒的資料補全後,按測點、時間序,儲存成檔案並讀入記憶體。
- 分層資料檔案:按不同時間段將冷熱資料檔案分層。
- 統計時將冷熱資料混合計算。
- 支援每個測點名對應一個CSV檔案作為資料來源計算。
- 統計介面以HTTP服務方式供外部應用呼叫並將統計結果透過回撥介面返回給外部應用。
第四步,實施最佳化方案
現有的RESTful介面取數太慢了,介面變為從kafka消費資料。儲存資料時,將字串型別的測點名數字化後儲存,以獲得更小的儲存量和更好的運算效能。
在第二步中已經提到,資料量較大時,無法將資料都放在記憶體中計算,所以考慮採用冷熱分層方案,將資料分為三層,每天的冷資料按測點號、時間有序(下文中的所有外存檔案儲存均採用該序,不再重複說明),用組表儲存,因為大表對效能的影響很大,儲存成組表有利於提升系統整體效能;當天的每10分鐘的冷資料用,集檔案存,因為集檔案建立和使用都更簡單,用來儲存小表會很便捷,也不會因為索引塊而降低儲存效率;10分鐘內的熱資料從kafka直接讀到記憶體,因為資料本身是透過kafka介面獲取的,另外資料可能有一定的延遲,不適合每秒取數即寫出。
測試後發現,10分鐘內的熱資料,從kafka獲取後再解析json,不但需要消耗大量記憶體,而且解析json也需要花費很長的時間。這樣在記憶體中直接載入熱資料是沒辦法用來統計計算的,所以將熱資料改為每10秒存成一個集檔案。
接下來開始實現統計計算部分。每天組表中的冷資料計算較快,但是當天的144個集檔案計算很慢。透過計算可以知道,每10分鐘的資料量約1.2億條記錄,這個規模的資料可以用組表來儲存,另外還可以再加一層每2小時一個組表檔案,來減少當天總檔案數的數量(從144個變成了24個)。實際上,計算時採用的二分查詢是對單個檔案內有序的測點號使用的,減少了檔案個數,也就是減少了總查詢次數。
最終,我們把資料分成了4層。第一層:延遲10秒的集檔案熱資料;第二層,每10分鐘的組表冷資料;第三層,每2小時的組表冷資料;第四層,每天的組表冷資料。由於每層資料都按測點號、時間有序,所以每一層都可以用歸併,快速生成下一層資料檔案。
這時的冷資料計算已經很快了,可以滿足實際使用,但是熱資料的計算相比冷資料還是很慢。觀察發現,熱資料的所有集檔案都加起來大約3G,不算很大,記憶體可以裝下。實際測試,把檔案讀到記憶體中再查詢相比直接外存檔案查詢可以快出好幾倍。
已知的統計計算,分為最大值、最小值、中位數、方差、平均值等,不盡相同,但是之前的資料查詢是一樣的。都用二分法,找出對應測點號組的資料,再用時間過濾,即可得到相應的value值。
實測效果
經過幾天時間的SPL編碼、測試,最佳化的效果非常明顯。最佳化之後的測試結果如下(耗時為毫秒):
| 測點數
時間段| 10 | 50| 100|
| --- | --- | --- | --- |
| 10分鐘 | 467| 586 | 854|
| 1小時 | 1739 | 3885 | 4545|
| 6小時 | 2599 | 7489| 13138|
| 1天| 4923| 16264 | 30254|
說明:測試環境使用的機械硬碟,對併發計算不友好,更換為固態硬碟後,測試結果還會有較大的提升。
後記
解決效能最佳化難題,最重要的是設計出高效能的計算方案,有效降低計算複雜度,最終把速度提上去。因此,一方面要充分理解計算和資料的特徵,另一方面也要熟知常見的高效能演算法和儲存方案,才能因地制宜地設計出合理的最佳化方案。本次工作中用到的基本高效能演算法和儲存方案,都可以從下面這門課程中找到:點選這裡學習效能最佳化課程,有興趣的同學可以參考。
傳統資料庫的功能比較單一,只能解決一個環節的問題,比如記憶體資料庫解決熱資料問題,大資料平臺解決冷資料。而當前問題需要多種技術組合,如果運用多種產品混合實現,又會帶來架構的複雜性,增加系統的風險。而且業界中的大資料庫產品的架構也較為死板,對儲存層基本不提供可程式設計能力,很難基於這些產品實現某些特殊設計的方案。
相比之下,集算器則擁有開放的技術架構和強大的程式設計能力(SPL語言),可以被深度控制,從而實現各種因地制宜設計的方案。
SPL資料