575
技術社區[雲棲]
基於表格存儲的高性能監控數據存儲計算方案
概述
隨著軟件架構的愈發複雜,了解係統現狀、調查問題的困難度也增加了很多。此時,一套完善的監控方案能夠讓開發和運維工程師快速排查問題,更好的維護係統的穩定性。
開源監控方案中,Zabbix、Nagios都是不錯的監控軟件,可以針對數十萬的設備監控數百萬的指標,強大的功能讓開發和運維都很讚歎。但是,網上經常看到的抱怨是其寫入和存儲能力的不足,以Zabbix為例,文章[1]提到使用NoSQL方案(HBase、Cassandra、Riak)比利用傳統RDBMS方案(MySQL、PostgreSQL、Oracle)其性能提高了1.5-3倍。如果考慮到架構的擴展性以及存儲空間的成本,NoSQL方案還會更有優勢,因為一般來說,NoSQL的壓縮效果都會更好。
下麵我們將基於一個實例案例,來講解如何使用阿裏雲NoSQL服務“表格存儲”來進行監控數據的處理。下麵敘述的過程中,我們不會直接拿出最好的方案,而是將逐步優化的思路整理出來分享給大家,期望大家跟我們一起尋求更優的解決方案。心急的同學可以直接跳到最後看結論。
需求定義
一個典型的監控係統包括數據的采集、計算(實時計算和離線計算)、存儲和展示,采集和展示是相對獨立的模塊,這裏我們隻以存儲為重點,在必要的時候也會對計算做相應的說明。
從業務角度看,監控係統需要完成如下功能:
- 任何時候都要求能寫入,不得丟點;
- 給定某個機器,某個指標,能夠查詢該機器該指標在一段時間內的連續的值;
- 給定某個機器,查詢該機器所有指標在一段時間內的連續的值;
- 給定某個指標,查詢所有機器在一段時間內的連續的值;
- 以上三點,時間段可以任意指定,從分鍾到月均支持;
從係統設計角度看,監控對存儲係統的核心要求如下:
- 在線變更表模式:監控指標變動頻繁,表模式必須做到自由改變,同時不對線上業務產生任何影響;
- 寫入性能高:一般批量寫數百條數據延時在數百ms內;
- 高擴展性:隨著監控的設備和指標越來越多,寫入能力和存儲能力需求越來越大,寫入要支持每秒千萬行監控數據,存儲係統要支持數十P數據;擴容過程用戶無感知;
- 低存儲成本:監控數據一般比較多,存儲成本需要重點關注,存儲係統要能夠在滿足訪問需求的前提下盡可能的降低成本;
- 老數據自動清理:一段時間前的數據一般不再需要,為了節約成本需要係統自動刪除,方便用戶;
上麵列的需求是從宏觀層麵上理解的,是架構師和CTO關心的事情。而作為一個幹活的程序員,我們需要從微觀層麵,從動手寫代碼的角度再一次細化需求。擴展、成本咱就不管了,這些老大都決定好了(關於成本最後有個具體的示例),我們就關心如何快速的構建係統。
- 對某個機器的某個指標,采集間隔為5秒,每行記錄約100Byte;平均寫入延時低於50ms;
- 數據保留6個月;
- 機器個數可以數百萬,指標個數可以數千萬,數據量可能過P;
- 典型指標查詢的時間範圍是最近10分鍾、1小時、24小時,亦可自定義起始和終止時間;
- 對大範圍數據做聚合:如果查詢的時間範圍太大,返回數據點不可太多,否則傳輸和展示都是負擔;
- 查詢要求,已知機器、指標,查詢某個時間範圍內的指標數據;
- 查詢要求,已知機器,查詢某個時間範圍內的所有指標數據;
- 查詢要求,已知指標,查詢某個時間範圍內所有機器的該指標數據;
- 典型的業務查詢操作,延時要求低於200ms;
通用監控架構
下麵圖1描述了通用的監控係統架構圖,以方便繼續講解。監控係統是一個龐大複雜的體係,網上找了個更完善的監控架構圖,見[2]。
圖1 通用報警係統架構,其中數據聚合任務亦可以使用流計算工具代替,道理類似。某些情況下,數據聚合任務也可以和采集代理合並為一個進程,簡化架構。
各個模塊的作用大概如下:
- 監控指標采集代理:部署在應用服務器上收集指標的軟件,一些流行的開源軟件比如MySQL/Nginx等本身就有很多第三方的監控代理;對於自己開發的係統,可以在程序中埋點,定製化代理;
- 數據聚合任務:上麵需求裏麵提到需要看長時間周期的數據,如果查看過去一個月的監控圖,需要將所有秒級的數據直接拿出來展示,對於傳輸帶寬和前端展示係統都是極大的考驗,而且一般業務並無此種精度需求;這個任務的目的就是定時將原始數據拉出來計算更粗粒度的指標,以便觀察指標的趨勢性;
- 報警係統:數據聚合任務在計算指標過程中會發現異常指標,此時可以將該指標通知報警係統,報警係統從存儲係統裏麵拿出更細的信息之後,可以通過短信、電話等方式通知相關人員;如果相關事件也能輸入報警係統,那麼指標展示界麵上就可以將異常指標和事件關聯,加快問題的解決,如圖
- 可視化展示:豐富的可視化工具可以加速問題的發現,將可視化和後端的數據係統解耦能方便嚐試不同的可視化工具;
- 表格存儲係統:無限水平擴展的NoSQL服務,寫入能力強大,過期數據自動清理;
圖2 報警和係統事件存儲在一起,協同處理之後可以得到更有價值的展示
從上麵的介紹中能夠意識到,最困難的問題是表結構的設計,要求該設計能夠避免數據熱點,能讓聚合任務快速的讀取過去一段周期的數據進行聚合,也能讓報警任務快速讀取異常指標細節,下麵我們就開始結構設計之旅,看看如何一步步構建高性能的監控數據存儲計算係統。
表結構設計
上述架構中,一個實際的場景如下:機器N指標M通過指標收集代理每5秒將指標數據寫入秒表table_5s, 一分鍾後N-M向table_5s寫入了12條數據,此時聚合任務可以將table_5s最近寫入的12條數據讀出來,計算均值(也可以按照業務需求做各種計算),然後寫入分鍾表table_1m,這樣1分鍾後table_1m寫入了一條數據。依次類推,60分鍾後小時表table_1h得到了一條數據,24小時後日表table_1d得到了一條數據。如上,有了秒表、分鍾表、小時表、日表之後,我們就可以根據不同的精度需求滿足業務的查詢需要。如果用戶要查詢最近一個月的趨勢,就應該讀日表table_1d,一次查詢數據在數百條內,如果用戶要查一分鍾內的細節問題,就應該讀表table_5s。
下麵表1給出了我們首先想到的表結構設計(秒/分鍾/小時/日表結構是類似的),有3列PK,value是INTEGER類型,
表1 最簡單粗暴的表結構設計
NodeName(*) |
MetricName(*) |
Timestamp(*) |
Value |
STRING |
STRING |
INTEGER |
INTEGER |
Node1 |
CPU |
1000 |
43 |
Node1 |
CPU |
1005 |
50 |
Node1 |
Disk |
1000 |
10 |
Node1 |
Disk |
1005 |
20 |
Node2 |
CPU |
1000 |
60 |
Node2 |
CPU |
1005 |
80 |
從上麵的需求來看,代理每5秒針對每個機器每個指標寫入一條數據沒有問題,
// 偽代碼,實際示例見各個SDK中example[3]
tableStore.putRow(table_5s, {Node1, CPU, 1000}, {Value = 43})
而實際上,在某一個時刻會多個機器多個指標都會產生數據,所以更好的做法是使用批量接口減少網絡IO次數,也利於存儲係統優化實現;
// 偽代碼,實際示例見各個SDK中example[3]
// 關於如何高效的將數據批量寫入表格存儲,[6]提供了非常詳盡的說明
tableStore.BatchWrite (table_5s, [{Node1, CPU, 1000}, {Value = 43}],
[ {Node1, Disk, 1000}, {Value = 10}])
聚合任務則定期從高精度表拉數據計算然後寫入低精度表,
// 偽代碼,實際示例見各個SDK中example[3]
// 最近5s的機器Node1的CPU數據全部讀取出來
rows = tableStore.getRange(table_5s, {Node1, CPU, 940}, {Node1, CPU, 1000}, {Value})
For row in rows:
sum += row.Value
avg = sum/rows.size()
// 下麵的PutRow同樣可以使用上麵提到的BatchWriteRow優化,
// 比如可以同時將多個指標的數據聚合好,然後統一寫入
tableStore.PutRow(table_1m, {Node1, CPU, 1000}, {Value = avg})
至此,計算、存儲相關的代碼已經寫完了,係統已經可以正常的運轉,如果業務量不大,這個方案是可以的。
隨著業務量的增加,上麵的設計可能有如下幾個問題:
- 表第一列PK為機器名,可能導致熱點問題。想象一下,你打算做一個監控平台,服務其他的用戶,那麼表第一列不再是機器名,而是用戶名,那麼一些大用戶,其監控指標多達數十萬,而這些監控指標因為擁有共同的第一列PK,表格存儲無法對其做自動分區(這是表格存儲的設計決定的,關於分區的概念,見[4]),從而使得該熱點無法被很好的平衡;
- 擴展性不夠靈活:比如有個需求是這樣的,業務希望看到機器N的CPU指標超過90%的所有記錄,上麵的表結構就沒法滿足了,此時需要的表結構是表2中所示:
表2 為了查找特定機器的CPU高點,需要跟表1不同的表結構
NodeName(*) |
MetricName(*) |
Value |
Timestamp(*) |
PlaceHolder |
STRING |
STRING |
INTEGER |
INTEGER |
INTEGER |
Node1 |
CPU |
43 |
1000 |
43 |
Node1 |
CPU |
50 |
1005 |
50 |
Node1 |
Disk |
10 |
1000 |
10 |
Node1 |
Disk |
20 |
1005 |
20 |
Node2 |
CPU |
60 |
1000 |
60 |
Node2 |
CPU |
80 |
1005 |
80 |
/// 偽代碼,實際示例見各個SDK中example[3]
Rows = tableStore.GetRowByRange(table_5s, {Node1, CPU, 90, MIN}, {Node1, CPU, MAX, MAX})
這樣就可以將該機器CPU超過90%的記錄都拿出來。大家看到了這個表結構跟上麵的不同,因為PK的列數不同,此時要滿足這個業務需求,我們當然可以重建一個表,但是首先表格存儲不鼓勵建立很多小表,其次類似業務變更可能很多,每次建表影響業務靈活性。
因此,如果我們的表結構變為如表3
表3 新的表結構設計,增加了業務變更的靈活性
NodeName_MetricName(*) |
Timestamp(*) |
Value |
STRING |
INTEGER |
INTEGER |
Node1_CPU |
1000 |
43 |
Node1_CPU |
1005 |
50 |
Node1_DISK |
1000 |
10 |
Node1_DISK |
1005 |
20 |
Node2_CPU |
1000 |
60 |
Node2_CPU |
1005 |
80 |
這種表結構設計比初始設計就要好很多了,第一列是我們自己拚起來的字符串,我們有足夠的自由按照業務需求來拚,表格存儲也能夠方便的做負載均衡。按照上述結構設計,上麵提到的新的業務得到的表數據如下,如表4
表4 按照新的表結構設計,監控數據展示如下
NodeName_MetricName(*) |
Timestamp(*) |
PlaceHolder |
Value |
STRING |
INTEGER |
INTEGER |
INTEGER |
Node1_CPU_43 |
1000 |
43 |
|
Node1_CPU_50 |
1005 |
50 |
|
Node1_DISK |
1000 |
|
10 |
Node1_DISK |
1005 |
|
20 |
Node2_CPU_60 |
1000 |
60 |
|
Node2_CPU_80 |
1005 |
80 |
|
請注意,上麵隻是按照業務需求將CPU的PK拚裝格式改變了,DISK因為沒有這個需求,並不需要改變,表格存儲允許不同行的列不同,這樣也不會帶來額外的存儲空間占用。由此我們也能看到,在PK的組織方式上,是有很多花樣可以玩的。
問題都解決了嗎?還沒。我們注意到聚合任務需要定期的讀取最近的K條數據做聚合之用,比如從table_5s中讀12條生成分鍾級別數據,從table_1m中讀60條數據生成小時級別數據等,這些讀都是通過getRange實現的,也即是一個小的scan讀,而表格存儲采用LSM[5]模型實現,在寫入量巨大的時候scan讀會導致大量磁盤IO,從而也容易引起性能下降。而且,隨著這些表數據越來越多,這種讀取的性能也會越來越難以保證。
如何解決呢?答案是對前麵的每類表,建立一個表結構相同的buffer表。比如table_5s表會建立一個對應的table_5s_buffer表。這個表裏麵隻存最近一段時間的數據,比如1天,超過1天的數據自動過期刪除,這樣數據少了,訪問的時候IO次數可控,性能可控。上麵保留1天是假設聚合任務可能出問題而多保留了一段時間,實際上對表table_5s_buffer我們幾乎隻會讀最近數秒的數據,而對這種訪問剛剛寫入的數據的場景,表格存儲是有特定的優化的,就是類似文件係統的page cache的概念,數據寫入磁盤前首先寫入內存,這樣訪問最新寫入的數據命中內存的可能性就變大了。上麵的兩個特點共同保障了表table_5s_buffer的讀性能。
是否還可以繼續優化?答案是Yes。我們回頭看看,一個機器可能有數千個指標需要監控,包括係統級別的和應用級別的,那麼聚合任務對每個指標都要執行scan就有點浪費了,scan的次數跟機器X指標數成正比。實際上,我們可以重新設計各個buffer表,結構如表5所示,第二列是時間,也就是按照時間對指標進行排序,
表5 重新設計buffer表,避免針對每個機器、每個指標都要scan讀
NodeName |
Timestamp(*) |
MetricName(*) |
Value |
STRING |
INTEGER |
STRING |
INTEGER |
Node1_ |
1000 |
CPU |
43 |
Node1 |
1000 |
DISK |
10 |
Node1 |
1005 |
CPU |
50 |
Node1 |
1005 |
DISK |
20 |
Node2 |
1000 |
CPU |
60 |
Node2 |
1005 |
CPU |
80 |
有了上麵的設計,聚合任務想拿Node1最近1分鍾(12行)的所有秒級監控指標,隻要一次getRange查詢就可以了,如果指標太多,SDK會自行分階段多次讀取。
// 偽代碼,實際示例見各個SDK中example[3] // 注意,這裏數據可能較多,我們使用iterator iter = tableStore. createRangeIterator (table_5s_buffer, {Node1, 940}, {Node1, 1000}, Value) Map<metric, list<int> > metrics; While (iter.MoveNext()): metrics[iter.Get(“Metric”)].append(iter->Get(“Value”)) For k,v in metrics: avg = sum(v) tableStore.PutRow(table_1m, {Node1_k, 1000}, {Value : avg})
這樣每次scan都可以拿到所有指標的數據,讀的IOPS已經降低到很低的水平了,但是還有一個隱藏的問題,如果某個機器的指標特別多,那麼會有熱點,因為在某個時間點,所有這個機器的指標都是寫到一個分區的(還記得我們上麵說第一列PK相同的行都在一個分區嗎),可以繼續優化。新的表結構設計如表6:
表6 為了避免頭部熱點,對每個機器上的若幹指標做分桶,這樣也利於聚集任務負載均衡
BucketId(*) |
NodeName |
Timestamp(*) |
MetricName(*) |
Value |
STRING |
STRING |
INTEGER |
STRING |
INTEGER |
00001 |
Node1_ |
1000 |
CPU |
43 |
00001 |
Node1 |
1000 |
DISK |
10 |
00003 |
Node1 |
1005 |
CPU |
50 |
00002 |
Node1 |
1005 |
DISK |
20 |
00003 |
Node2 |
1000 |
CPU |
60 |
00003 |
Node2 |
1005 |
CPU |
80 |
其中BucketId是hash(NodeName + MetricName) % K得到的,K可以根據自己的業務規模調整。有了這個設計後,即使某個機器下麵需要監控數十萬指標,也可以被多個分區均勻處理,避免了隻寫頭部分區的熱點問題。這種方案還有一個好處就是,聚合任務也可以啟動多個實例,每個實例負責一定數量的桶就可以了,這樣聚合任務的負載也是比較均衡的。
總結來說,就是基礎表數據采用表1裏麵給出的結構,而相對應的buffer表采用表6是一個比較好的方案。
上麵的各個步驟以一個真實的監控數據處理方案為背景,我們能看到係統如何從一個最簡單但是存在性能問題的版本一步步演進到最終的解決性能、訪問均勻性等問題的方案。采用最終方案後,該應用至今已經穩定運行4個月,之前的性能問題、訪問熱點問題均得到了解決。
總結
使用高可擴展性的NoSQL存儲監控數據是一個較為理想的方案,因為監控數據是時間序列數據,定期生產,聚合度較好,寫入效率高。而NoSQL係統如表格存儲一般采用LSM模型實現,其本身就是利於寫的,正好匹配。同時表格存儲還提供強大的水平擴展能力,支持每秒寫入千萬行,支持存儲數十P的數據。
思路擴展一下,上麵的方案對時間序列數據都是可以借鑒的,比如應用性能管理(APM)中某APP下PV/UV聚合信息,比如券商係統中各類交易數據的聚合信息等。
關於可用性、可靠性等問題,也可以參考[4],關於價格計算,可以直接使用[7]中提供的工具,需要注意的是,7月份會正式上線大容量存儲產品,采用SATA磁盤存儲,價格會大幅下降。
[1]. https://www.miraclelinux.com/labs/pdf/zabbix-write-performance
[2]. https://www.eventsentry.com/documentation/overview/html/monitoringarchitecture.htm
[3]. 表格存儲SDK:https://www.aliyun.com/product/ots/?spm=5176.7960203.237031.54.QnGdrR
[4]. 表格存儲介紹:https://yq.aliyun.com/articles/53687?spm=5176.100239.blogrightarea.7.JF5hkZ
[5]. https://en.wikipedia.org/wiki/Log-structured_merge-tree
[6]. 表格存儲高吞吐數據寫入:https://yq.aliyun.com/articles/51531?spm=5176.team4.teamshow1.25.a8GJz5
[7]. 表格存儲價格計算器:https://www.aliyun.com/price/product?spm=5176.54465.203792.6.smhvgp
最後更新:2017-06-05 11:33:03