閱讀113 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Facebook TSDB論文翻譯

本文為Facebook官方論文的翻譯,原文地址https://www.vldb.org/pvldb/vol8/p1816-teller.pdf


概要

大型互聯網服務一般以出現故障及時響應和保持高可用性為目標。為了提供正常穩定的服務,通常要每秒從大量係統中監控和分析數以千萬計的數據(性能數據和業務數據)。一個特別高效的解決方案是用TSDB對這些數據進行存儲和查詢。

設計TSDB時的一個關鍵挑戰是如何在性能、擴展性、穩定性這幾者之間做出恰當的平衡。本篇論文主要介紹Gorilla,Facebook的內存級TSDB,我們認為做為監控係統來說,用戶更關心數據的聚合分析,而不是具體某個時間點的數據;在快速檢測和診斷一個正在發生的問題的根本原因時,最近的數據會比舊的數據更有價值。Gorilla針對高可用的讀寫做了優化,發生故障時甚至會以丟失少量最近寫入的數據為代價來保證整體的可用性。為了提升查詢性能,我們專門使用了一些壓縮技術,例如delta-of-delta編碼的時間戳、浮點型值的XOR壓縮運算,這些技術讓Gorilla減少了10倍左右的存儲空間。存儲空間的大幅減少使得我們可以將數據存儲在內存中,與傳統的基於Hbase的TSDB相比,查詢耗時縮短了73倍,吞吐量提高了14倍。性能上的提升可以擴展出更多監控和排查問題的工具,例如時間序列關聯搜索,數據更加豐富和密集的可視化工具。Gorilla也非常優雅的解決了單節點故障,讓整個集群沒有額外的運維開銷。

 

1. 介紹

大型互聯網服務旨在保持高可用性和對用戶及時響應,即使是在出現意外故障的情況下。隨著業務的增長,要支持更為龐大的全球化業務就需要將“少量係統、數百機器”擴展到上千個單獨的係統,運行在數千甚至更多的機器上,通常還要跨越多個不同地域的數據中心。

運維這些大規模服務的一個重要前提是能夠非常精準的監控係統的運行狀況和性能,當問題出現時能夠快速定位和診斷。Facebook使用時間序列數據庫(TSDB)來存儲係統的各項指標數據,並提供快速查詢功能。後來在監控和運維Facebook服務時我們遇到了一些技術上的瓶頸,於是我們設計了Gorilla,一個內存級的TSDB,它每秒能存儲千萬級的數據(例如 cpu load, error rate, latency等),並能毫秒級返回基於這些數據的查詢。

寫入

我們對TSDB最核心的要求是能夠保障數據寫入的高可用性。由於我們有幾百個係統、大量數據維度,數據的寫入量很容易就會達到每秒千萬級。相反,數據的讀取量通常少好幾個量級,這是因為讀取主要是由一些自動化的監控係統發起,這些係統隻會關心相對重要的數據,數據可視化係統也會占用一些讀取量,它們將數據加工後通過數據麵板、圖表等方式呈現出來,另外,當需要去定位解決一個線上問題時,也會人為的發起一些數據讀取操作。

狀態轉換

我們希望當係統的運行狀況發生重大變化時能夠在第一時間發現問題,例如新版本發布、某個線上變更引發異常、網絡故障,或者其它一些原因。因此我們的TSDB需要具備在很短的時間內細粒度聚合計算的能力。這種在幾十秒內迅速檢測到係統狀態變化的能力是非常有價值的,基於它就可以在故障擴散之前自動的做快速修複。

高可用

如果因為網絡分區或者其它故障引發數據中心之間斷連,每個係統當前連接的數據中心都應該具備將數據寫入到本網絡域內TSDB服務器的能力,並且在需要時可以在這些數據上做查詢檢索。

容錯

我們希望寫操作能同時複製到多個節點上,當某些數據中心或不同地理位置的節點發生不可預知的災難時,也能承受損失。

 

Gorilla是Facebook研發的新的TSDB,在設計上實現了上文描述的幾個點。Gorilla采用write-through cache方式將最近據記錄到監控係統。我們的目標是讓大多數查詢在10ms內返回。

Gorilla的設計思路遵循一個觀點,我們認為人們在使用監控係統時不會關注於孤立的數據點,而是更在意整體的聚合分析。此外,這些係統不會存儲任何用戶數據,所以傳統的ACID特性並非TSDB的核心需求。但是,Gorilla在任何時候都要保障大部分的寫都能成功,即使是麵對那些可能使整個數據中心都無法訪問的重大故障時也要如此。還有一點,最近的數據比舊的數據更有價值,從運維工程師的角度來說,排查某個係統或服務現在正在發生故障比排查一個小時前出的故障更容易給出直覺上的判斷。Gorilla對高可用的讀寫做了優化,即使在發生故障時,也會以丟失少量數據為代價來保證整體的可用性。

技術上的挑戰來自於高性能寫入、承載的數據總量、實時聚合能力,以及穩定性方麵的需求。我們依次解決了這些問題。為了實現前兩個需求,我們分析了在Facebook使用很廣並且也用了很久的監控係統– ODS時間序列數據庫。我們注意到ODS中至少有85%的查詢來自過去26小時采集的數據。進一步分析後我們決定將基於磁盤的數據庫替換為基於內存的數據庫,這樣就能提供最好的服務。另一方麵,將內存數據庫做為磁盤存儲的緩存,可以實現內存級的寫入速度並擁有基於磁盤的數據持久性。

截止到2015年春天,Facebook的監控係統產生了超過20億個唯一的時間序列計數器,每秒增加約1200萬個數據點,這意味著每天會增加1萬億個數據點。每個數據點16個字節,每天就需要16TB的內存空間,這對於實際的部署而言是巨大的資源消耗。我們通過重用現有的基於XOR的浮點型數據壓縮算法以流的方式解決了這個問題,平均每個點的壓縮到1.37字節,縮小了12倍。

我們在多個不同地域的數據中心部署了Gorilla實例,並在實例之間做數據傳輸,但不會試圖去保證數據的一致性,基於這種方式實現了穩定性方麵的需求。數據讀取會路由到最近且可用的Gorilla實例上。請注意,這種設計方式是基於我們對實際業務的理解,因為我們認為個別數據的丟失對整體的數據聚合不會有太大的影響,除非兩個Gorilla實例之間存在重大的數據不一致。

Gorilla目前部署在Facebook的生產環境中,工程師們把它當做日常的實時數據工具,並協同其它監控和分析係統(例如Hive、Scuba)一起檢測和診斷問題。

 

2. 背景和需求

2.1 ODS

Facebook的基礎設施由數以百計個分布在不同數據中心的係統組成,如果沒有一個監控係統來跟蹤它們的健康和性能,要操作和管理它們是非常困難的。“操作數據存儲”(ODS)是Facebook監控係統的一個重要部分。ODS包括一個時間序列數據庫(TSDB),一個查詢服務,以及一個檢測和報警係統。ODS的TSDB是在HBase存儲係統之上建立的,相關描述見文末引用[26]。圖1整體展示了ODS的協作方式,時間序列數據由Facebook主機上的服務產生,通過ODS的寫入服務收集,最終寫入到HBase中。

ODS的時間序列數據有兩類使用者。第一類使用者是那些依賴報表係統做數據分析的工程師,係統提供的圖表以及其它可視化的交互分析都要從ODS中獲取數據;第二類使用者是我們的自動化報警係統,它從ODS中讀取計數器,將其與係統健康、性能、診斷指標等預設閾值進行比較,在必要時給oncall的工程師們和自動修複係統發出警報。

 

2.1.1 監控係統讀取數據時的性能問題

2013年初,Facebook的監控團隊逐漸意識到HBase時間序列存儲係統難以擴展支撐未來的讀取負載。在展現各種用於分析的圖表時,平均的查詢耗時是可以接受的,但是對於自動化係統來說,由於會產生大量的查詢操作,排在末段的查詢需要等前麵的操作完成,時間的疊加導致它們可能需要等待數秒才會被執行到,係統就會被阻塞住。另外,如果對幾千個時間序列進行中頻次查詢要花掉幾十秒的時間,人們也會對自己使用方式產生懷疑;對稀疏數據集做並發更高的查詢可能會超時,這是因為HBase的數據存儲已經將策略調整為寫入優先。雖然基於HBase的TSDB比較低效,但是我們也不能立刻就全部換掉,因為ODS上的HBase存儲占用了2PB的數據。Facebook的數據倉庫解決方案Hive也存在問題,它比ODS的查詢延時還要高幾個數量級,而查詢的延時和效率恰恰是我們最關心的。

 

接下來我們將注意力放在了內存級的緩存上。ODS先前使用了一個簡單的read-through cache讀取方式,但它主要針對的是圖表係統,它裏麵的多個顯示麵板會共享相同的時間序列數據。一個特別麻煩的問題是,當需要查詢最近的數據時,如果緩存miss了就會直接將請求打到HBase存儲上。我們也考慮了單獨基於memcache的write-through緩存方案,最終還是沒有采納,這是因為將新數據添加到已有的時間序列上時需要先讀再寫,這會對memcache服務器產生極大的壓力。我們需要一個更有效的解決方案。

 

2.2 Gorilla的需求

基於多方麵的考慮,我們確定了下列需求:

  • 支撐20億個通過字符串key唯一標識的時間序列
  • 每分鍾能增加7億個數據點(時間戳和值)
  • 內存能存儲26個小時的數據
  • 高峰時每秒能支撐40000次以上的查詢
  • 1毫秒內能讀取成功
  • 時間序列支持15秒的間隔粒度(即每分鍾每個時序序列有4個點)
  • 數據存儲在2個不同的副本中(容災能力)
  • 即使某個服務器掛了也能持續提供讀取
  • 能快速掃描內存中的所有數據
  • 每年能支撐最少兩倍的業務增長

本文的第3節,簡單比較了其它幾個TSDB,在第4節我們詳細介紹Gorilla的實現,4.1首次講述新的時間戳和數據值的壓縮方法,4.4介紹當出現區域性的故障時Gorilla怎樣保障高可用。第5節介紹圍繞Gorilla打造的新工具。論文會以第6節介紹我們在開發和部署Gorilla方麵的經驗來結束。

 

3. 和其它TSDB比較

有很多已出版的書刊或論文裏詳細介紹了通過數據挖掘技術來高效的搜索、分類、聚合大量時間序列數據。它們係統的描述了時間序列數據的很多種使用方式,從數據收集到分類,再到異常檢測,再到索引時間序列。然而詳細描述係統如何實時收集和存儲大量時間序列的示例比較少。Gorilla的設計專注於打造生產環境下可靠的實時監控,從其它TSDB的比較中脫穎而出。Gorilla有一個比較獨特的設計思想,麵對故障時保障讀寫的高可用比保障老數據的可用性具有更高的優先級。

由於Gorilla從一開始就被設計成將所有數據都存儲在內存中,因此在內存結構上也不同於現有的TSDB。如果將Gorilla看作一個中間存儲,用來在基於磁盤的TSDB上層的內存中存儲時間序列數據,那麼Gorilla可以以一個write-through cache方式用在任意的TSDB上(通過相對簡單的修改)。Gorilla在數據寫入速度和水平擴展能力上與已有方案類似。

 

3.1 OpenTSDB

OpenTSDB基於HBase,和ODS中用來存儲long term數據的HBase存儲層非常相似。兩個係統都依賴相似的表結構,在優化和水平擴展上也有比較近似的方案結論。但是,我們發現在支撐構建更高級監控工具的高查詢量時,要比基於磁盤的存儲所能提供的查詢更快。

和OpenTSDB不同,ODS HBase存儲層會定時的將老數據進行聚合以節省空間,這導致ODS中的老數據相比更近的數據時間間隔粒度更大,而OpenTSDB永久保存全量數據。我們發現從成本較低的長時間片查詢以及空間的節省上來說,數據精度的丟失是可以接受的。

OpenTSDB還有一個更豐富的數據模型來唯一識別時間序列,每個時間通過一組任意的k/v對來標識,也稱為tags。Gorilla使用單個字符串key來標識時間序列,並依賴更高級的工具來提取和識別時間序列元數據。

 

3.2 Whisper(Graphite)

Graphite是一個RRD數據庫,它用Whisper內置的格式將時間序列數據存儲在本地磁盤上,這個格式假設時間序列數據是按固定時間間隔產生的,不支持間隔跳動時間序列。Gorilla在對固定時間間隔的數據處理上效率更高,並且能支持任意和不斷變化的時間間隔。Whisper中的每個時間序列都存儲在單獨的文件中,一定時間之後新的數據會覆蓋老的數據。Gorilla使用了類似的方式,隻不過最近的數據是存儲在內存中。但是,由於Graphite/Whisper采用的是磁盤存儲,對於Gorilla要解決的問題來說,查詢耗時還是太高了。

 

3.1 InfluxDB

InfluxDB是一個新的開源時間序列數據庫,和OpenTSDB相比有更豐富的數據模型,時間序列中的每一個事件都可以包含完整的元數據,盡管這樣更具靈活性,但是和隻在數據庫中保存時間序列相比,必然導致更大的磁盤占用率。

InfluxDB還包含一些可以擴展的代碼,允許用戶的這些代碼上將它水平擴展為分布式存儲集群,而不需要像OpenTSDB那樣還有運維HBase/Hadoop集群的開銷。在Facebook,我們已經有專門的團隊來支持HBase設施,將他們用在ODS不需要投入大量額外的資源。和其它係統一樣,InfluxDB將數據保存在磁盤上,這也導致查詢速度比存儲在內存中慢。

 

4. Gorilla架構

Gorilla是一個基於內存的TSDB,在監控數據寫入HBase存儲時,起到一個write-through cache的作用。存儲在Gorilla的監控數據是一個簡單的3元組字符串key,時間戳是64位整型,值是雙精度浮點型。Gorilla采用了一種新的時間序列壓縮算法,可以按照時間序將數據從16字節壓縮到平均1.37字節,縮小12倍。此外,我們專門設計了Gorilla的內存數據結構,在保持對單個時間序列進行時間段查找的同時也能快速和高效的進行全數據掃描。

監控數據中定義的key用來唯一標識一個時間序列,通過對基於key的數據進行分片,每個時間序列數據集都會被映射到一台單獨的Gorilla主機上。因此,我們可以通過簡單的擴展主機並調整分片算法將新的時間序列數據映射到新的主機上,從而達到擴展Gorilla的目的。Gorilla 18個月前在生產環境運行時,26小時內的全量時間序列數據占用1.3TB的內存,均勻分布在20台機器上。在那之後,我們必須將集群的規模擴為兩倍來應對兩倍的數據增長,現在每個Gorilla集群有80台機器在運行。擴展的過程很簡單,這是因為無狀態的架構有非常好的水平擴展能力。

Gorilla將時間序列數據寫到不同地域的主機中,這樣就能容忍單節點故障,網絡切換,甚至是整個數據中心故障。在檢測到故障時,所有讀取操作會failed over到備用節點,以確保用戶不會感知到任何中斷。

 

4.1 時間序列壓縮

在評估創建新內存級時間序列數據庫的可行性時,我們考慮了幾種現有的壓縮方案,以減少存儲上的開銷。我們認為僅適用於整型數據的壓縮技術不能滿足存儲雙精度浮點型數據的需求;其它的一些技術作用於完整的數據集,但不支持對存儲在Gorilla中的數據流進行壓縮;我們還發現數據挖掘領域會使用有損的時間序列近似技術,這樣會更適合用內存來存儲,但是Gorilla更關注於保持數據的完整性。

我們受到了從科學計算中推導出來的浮點型壓縮方法的啟發,該方法利用與前麵值的XOR比較來生成一個差值編碼。

Gorilla對時間序列中的數據點進行壓縮,不會有額外的跨時間序列壓縮。每個數據點是一對64位的值,代表那個時間的時間戳和值。時間戳和值根據前麵值分別進行壓縮。整體的壓縮方案見圖2,圖裏展示了時間戳和值是如何在壓縮塊中運算的。

圖2-a表明時間序列數據是由時間戳和值組成的數據流,Gorilla按照時間分區將數據流壓縮到數據塊中。這裏先定義了一個由基線時間構建的Header(圖例中從2點開始),然後將第一個值進行了簡單的壓縮存儲,圖2-b是通過delta-of-delta壓縮後的時間戳,這個在4.1.1節會做更詳細的描述。圖中給出的delta of delta值為-2,用2位來存儲header(‘10’),7位來存儲值,總位數隻有9位。圖2-c顯示了XOR壓縮後的浮點值,4.1.2節有更詳細的描述。通過將浮點值與前麵的值進行XOR操作,我們發現隻有一個有意義的位。用兩位編碼header(‘11’),編碼中有11個前置0,一個有意義的位,其實際值為(‘1’),一共用14位進行存儲。

 

4.1.1 時間戳壓縮

我們分析了ODS中存儲的時間序列數據,決定對Gorilla的壓縮方案做優化。我們發現絕大部分ODS數據在固定的時間間隔產生,例如每60秒記錄一條數據的時間序列普遍存在,偶爾有一些數據有提前或推遲1秒生產出來,但在入口一般都是有約束的。

相比於存儲整個時間戳,我們隻存儲的“差值的差值”,這樣會更高效。如果某個時間序列後續的時間與前麵時間的差值分別為60,60,59,61,那麼“差值的差值”是用當前的時間戳差值前去前一個差值,那麼計算出的“差值的差值”為0,-1,2。圖2給出了示例。

接下來我們用下麵的規則對“差值的差值”做可變長的編碼:

1. 數據塊的header存儲了一個開始的時間戳t-1,這裏對齊到2點鍾;第一個時間戳t0,用14位存儲與t-1的差值
2. 對於時間戳tn:
  1. 計算差值的差值為:D = (tn – tn-1) – (tn-1 – tn-2)
  2. 如果D=0,用一個單獨的位來存儲’0’
  3. 如果D在[-63,64]之間,存’10’,後麵為值(7位)
  4. 如果D在[-255,256]之間,存’110’,後麵為值(9位)
  5. 如果D在[-2047,2048]之間,存’1110’,後麵為值(12位)
  6. 其它情況存’1111’,後麵用32位存D的值

 

這些不同取值範圍是從生產環境真實的時間序列中采樣出來的,每個值都能選擇合適的範圍以達到最好的壓縮比。雖然一個時間序列可能有時會丟失部分數據,但是它現存的數據很可能都是以固定的時間間隔產生的。舉個例子,假設在丟失了一個數據點後的差值為60,60,121,59,那麼差值的差值就是0,61,-62。61和-62適配最小的取值範圍,就會更少的位數來編碼。下一個取值範圍[-255, 256]也很有用,當每4分鍾產生一條數據時,如果丟失了某條數據仍然可以適配這個取值範圍。

圖3展示了Gorilla中時間戳最終值的分布情況,我們發現有96%的時間戳都能被壓縮到1個單獨的位來存儲。

 

4.1.2 值壓縮

除了對時間戳做壓縮外,Gorilla也對值進行了壓縮,3元組字符串中的數據值為雙精度浮點型。我們使用的壓縮方案和現在已有的浮點型數據壓縮算法類似,文末的參考文獻[17]和[25]有相關描述。

通過分析ODS的數據發現,大多數時間序列內相鄰數據點的值不會有明顯的變化,此外,很多數據來源隻會存儲整型的值。這就使得我們可以將文末參考文獻[25]的昂貴方案調整為更簡單的實現,僅用將當前值和前麵的值做比較。如果值接近,那麼浮點型數據的符號位,指數位和尾數部分的前幾位,會是完全相同的,基於這點,我們對當前值和前序值使用一個簡單的XOR運算,而不是像時間戳那樣用差值編碼的方案。

我們用下麵的規則對XOR後的值進行可變長編碼:

1. 第一個值不做壓縮
2. 如果與前序值的XOR結果為0(即值相同),僅用一位存儲,值為’0’
3. 如果XOR結果非0,控製位的第一位存’1’,接下來的值為下麵兩種之一
   a) 控製位’0’ — 有意義的位(即中間非0部分)的數據塊被前一個數據塊包含,例如,與前序值相比至少有同樣多的         前置0和同樣多的尾部0,那麼就可以直接的數據塊中使用這些信息,並且僅需要存儲非0的XOR值。
   b) 控製位’1’ — 用接下來的5位來存儲前置0的數量,然後用6位存儲XOR中間非0位的長度,最後再存儲中間非0位

 

使用XOR運算編碼對時間序列高效的存儲壓縮方案在圖2有直觀的展現。

圖5是Gorilla中實際的數據分布情況,大約有59%值隻用了1位存儲,也就是當前值和前序值完全一樣;28.3%控製位為’10’(上麵提到的規則a),平均占用26.6位;餘下12.6%的控製位為’11’,平均占用36.9位,位數多是因為對前置0和中間非0位的長度編碼需要額外13位。

這種壓縮算法同時使用了前序值和前序XOR值,這樣會使最終的結果值具有更好的壓縮率,這是因為一段連續XOR值的前置0和尾部0個數往往非常接近,見圖4。這種算法對整型的壓縮效果更好,整型值經過XOR運算後的中間段位的位置一般在整個時間序列中對齊的,意味著大多數XOR值有相同個數的尾部0。

 

我們的編碼方案有一個折衷是壓縮算法的時間跨度,在更長的時間跨度上使用同樣的編碼能夠獲得更好的壓縮比,但是這個跨度上的短時間區間查詢可能需要在數據解碼上消耗額外的計算資源。圖6是存儲在ODS中的時間序列在不同數據塊大小下的平均壓縮率,可以看出塊大小超過兩個小時以上後,數據的壓縮率收益是逐漸減少的,一個兩小時時長的塊可以將每個點的數據壓縮到1.37字節。

 

4.2 內存數據結構

Gorilla實現中主要的數據結構是一個時間序列Map (TSmap),圖7提供了這個數據結構的整體概覽。TSmap包含一個C++標準庫中的vector,裏麵是指向時間序列的指針;還包含一個map,key為時間序列的名稱,不區分大小寫並保留原有大小寫,值是和vector中一樣的指針。vector可以實現全數據分頁查詢,而map可以支撐指定時間序列的定長時間段查詢,要滿足快速查詢的需求必須要具備定長時間段查詢的能力,同時也要滿足有效的數據掃描。

 

C++的指針可以在掃描數據時僅用幾微秒就能將整個vector或其中的幾頁拷貝,避免對新寫入到數據流的數據產生影響。被刪掉的時間序列在vector中為“墓碑狀態”,它的索引會被放置到一個空間的池中,當產生新的時間序列時會複用它。“墓碑狀態”實際上是將一段內存標記為’dead’,並準備好被重用,而不會實際將資源釋放到底層係統。

在TSmap上有一個讀-寫自旋鎖來保護對map和vector的訪問,每個時間序列上也有一個1字節的自旋鎖,通兩個鎖保證了並發的能力。對於每個單獨的時間序列來說寫的量相對校少,所以讀和寫也隻有非常少的鎖爭用。

 

如圖7所示,分片唯一標識(shardId)與TSmap之間的映射存在ShardMap中,它也是一個vector,存儲了TSmaps的指針,它使用了和TSmap一樣對大小寫不敏感的hash算法將時間序列名稱映射到各個分片,hash後的值在 [0,shard數量)區間內。由於係統中分片的數量是恒定的,並且總量在幾千以內,所以存儲空指針的額外開銷基本上可以忽略。和TSmaps一樣,ShardMap有一個自旋鎖來處理並發訪問。

由於數據已經劃分為分片,單個map可以保持足夠小(約100條記錄),C++標準庫中的unordered-map有足夠好的性能,沒有鎖爭用的問題。

 

時間序列的數據結構有兩個重要組成部分,一部分是一係列關閉的數據塊,塊中的數據超過2小時;另一部分是一個開放的數據塊,存最近的數據。開放塊是個append-only字符串,新的時間戳和值壓縮後追加這個字符串上。每個塊隻存儲2小時的壓縮數據,當數據寫滿後塊會變為關閉,一旦塊關閉了就不能再對其做修改,除非將它從內存中剔除。關閉後,會根據使用的slab總大小分配出一個新的塊來存數據,這是因為每次開放塊在關閉時實際用掉的空間都不一樣,我們發現使用這種方式在整體上會減少Gorilla產生的內存碎片。

時間範圍查詢關聯的數據塊被會拷貝出來直接讀到調用端,返回給客戶端的是整個數據塊,使得解壓過程在Gorilla外完成。

 

4.3 磁盤數據結構

Gorilla的目標之一是能應對單機故障。Gorilla通過將數據存儲在GlusterFS來實現持久化,GlusterFS是一個分布式文件係統,三複本存儲,兼容POSIX。HDFS或者其它分布式文件係統也同樣很容易應對單機故障,我們同時也考慮了單主機數據庫比如MySQL和RocksDB,不過還是決定不使用這類數據庫,因為我們的持久化使用場景不需要數據庫層麵的查詢語言。

一台Gorilla主機能存儲多個數據分片,每個分片上維護著一個文件目錄,每個文件目錄包括4種類型的文件:key列表,append-only日誌,完整的塊文件,checkponit文件。

Key列表中的值是時間序列名和一個整型標識的簡單映射,整型標識就是內存中vector的下標。新的key追加在這個列表中,Gorilla會定期對每個分片上的key做掃描,以便重寫到文件。

當數據流入到Gorilla時也會被存儲到一個日誌文件中,時間戳和值用前麵4.1節描述的格式壓縮。但是每個分片上隻有唯一的一個append-only日誌文件,因此數據會交叉跨越多個時間序列。和內存編碼不同的是,每個時間戳和值還要加上32位的整型ID做標記,所以相比之下每個分片上的日誌文件會增加明顯的存儲開銷。

Gorilla不提供ACID特性,同樣,上麵提到的日誌文件也不是WAL日誌,數據被刷到磁盤之前會先到緩存區,最多到64K,一般會包含1到2秒的數據。雖然在正常退出係統時緩衝區的數據會刷到磁盤,但是當發生異常崩潰時可能會導致少部分數據丟失。相比傳統的WAL日誌帶來的收益,我們覺得這個取舍是值得的,因為可以以更快速率將數據刷到磁盤,也能支撐更加高可用的數據寫入。

每隔兩小時, Gorilla將數據塊中的壓縮數據拷貝到磁盤,這種格式的數據遠小於日誌文件中的數據。 每兩小時的數據有一個完整的數據塊文件,它有兩部分:一組連續的64KB數據塊,它們直接從內存中複製而來,以及一係列由<時間序列ID,數據塊指針>組成的值對。一旦某個塊文件完全刷到磁盤,Gorilla會刷下checkpoint文件並將相應的日誌刪除,checkpoint文件用來標記一個完整的數據塊什麼時候被刷到磁盤。如果在遇到進程崩潰時塊文件沒有被成功刷到磁盤,那麼在新的進程啟動時對應的checkpoint文件是不存在的,因此這個時候每次啟動新的進程時除了讀取塊文件之外,還會從日誌文件中讀取checkpoint之後的數據。

 

4.4 故障處理

對於容錯,我們選擇優先考慮單節點故障,大規模的感知不到當機時間的臨時性故障,以及區域性故障(比如整個區域網絡中斷)。這是因為單節點故障發生比較頻繁,而大規模的,區域性故障已經成為整個Facebook比較關注的問題,需要有應對自然或人為災害的能力。我們對待故障的處理方式有一個另外的好處,那就是可以將滾動式的軟件升級模擬成一組可控的單節點故障,對這種情況做優話意味著我們可以輕而易舉並且很頻繁的做代碼推送。對於其它故障我們選擇折衷處理,如果故障會引起數據丟失,將優先考慮最近數據的可用性而不是老數據,這是因為對曆史數據的查詢可以依賴已有的Hbase TSDB,一些自動化係統檢測時間序列的變化對部分數據仍然有用,隻要有最新的數據產生就會有新老數據比較。

Gorilla通過在不同的數據中心中維護兩個完全獨立的實例,來確保在數據中心故障或網絡分區情況下的高可用性。一筆數據寫入會流入到每個Gorilla實例,而不會嚐試去保證數據的一致性,這就使得大規模故障比較容易處理。當整個區域出現故障時,查詢會指向到其它可用節點,直到之前的節點已經備份了26小時的數據。這對於處理真實的或模擬的大規模故障非常重要,舉個例子,區域A的Gorilla實例完全掛掉了,對這個區域實例的寫入和讀取會失敗,失敗的讀取會透明地路由到健康的區域B中的實例。如果故障持續了很久(超過1分鍾),數據將從區域A中刪除,請求不再會被重試。發生這種情況時,區域A上的所有讀都會被拒絕,直到區域A的集群重新健康運行26小時,這種處理方式在故障發生時可以手動或自動執行。

 

在每個域內都有一個基於Paxos算法名為ShardManager的係統為節點分配分片,當某個節點發生故障時,ShardManager會將這個節點的分片分發給集群中的其它節點。分片在節點之間遷移時,寫入的數據先緩存在客戶端緩衝區,緩衝區可以保存1分鍾的數據,超過1分鍾的數據將會被丟棄,以方便為更新的數據留出空間。我們發現大多數情況下這個時長足夠用來重新分片,而對於需要消耗更長時間的情況,最新的數據優先級也更高,因為數據越新從直觀上看對操作自動檢測係統越有用。當區域A的一台主機α崩潰或者由於其它任何原因提供不了服務,寫入操作至少會緩衝1分鍾,這時Gorilla集群會嚐試重啟這台主機。如果集群內的其它主機是健康的,故障主機的分片會在30秒或更少的時間內發生遷移,以確保沒有數據丟失。如果分片遷移的動作沒有及時發生,數據的讀取將會被指向到區域B中的Gorilla實例上,這個操作可以通過手動或自動完成。

當分片被分配到某台主機時,會從GlusterFS讀取全部數據,這些分片在調整之前可能是屬於同一主機。新主機從GlusterFS讀取和處理完整可用的數據大約需要5分鍾時間,這是因為係統中存儲的shard數量和總數據量的原因,每個分片標誌著16GB的磁盤存儲,這些數據分布在不同的物理機上,幾分鍾就可以從GlusterFS中讀取出來。當主機正在讀取分片數據時,也會接受新的數據寫入,新的寫入會被放到一個緩衝隊列,隊列中的數據會被盡可能快的處理。分片數據處理完成後立刻開始消費緩衝隊列,將數據寫到這台新的主機上。回到前麵區域A中的主機α崩潰的例子:當α崩潰時,它的分片被重新分配給同集群的主機β,一旦β被分配了這些分片就開始接受數據寫入,因此從內部來看沒有數據丟失。如果Gorilla的主機α能夠以一個更可控的方式中斷服務,那麼在它停服之前就能安全的所有數據都刷到磁盤上,所以對於規模化的軟件升級來說也不會有數據丟失。

 

在我們這個例子中,如果主機α在數據刷盤成功之前掛掉,數據就會丟失。實際中這種情況很少發生,即使發生了通常也僅會丟失幾秒鍾的數據。這種處理方式是我們的一種權衡,它可以讓集群能有更高的寫入吞吐量,並且在故障停機之後能夠更快接收最新的數據。此外,我們也對這種情況有監控,在故障發生後能夠將讀切到更健康的節點。

要注意的是,當節點故障時有些分片可能有部分數據不可讀,要等到新的節點將這些分片的數據完全從磁盤讀取出來。查詢可能隻返回部分數據(塊文件的讀取順序按時間從近到遠)並在結果中標記為部分數據。

當處理數據讀取的客戶端庫從區域A的Gorilla實例上接收到一個“部分的”結果時,它會從區域B的實例中再次讀取那些受影響的時間序列,如果區域B有完整的數據,就使用B的這份數據。如果A和B都隻有部分數據,會把這兩部分數據都返回給調用者,並在結果裏麵做打個標,標明是因為某些錯誤導致數據不完整。接下來調用者可以決定這些數據是否有足夠的信息量來繼續處理請求,或者可以認為本次處理失敗。我們做出這樣的選擇是因為Gorilla最常用於自動化係統來檢測時間序列的數據變化狀況,即使隻有部分數據,這些係統也可以運行得很好,隻要這些數據是最近最新的。

 

將讀取從不正常的主機自動轉發到正常運行的主機意味著用戶可以免受重啟和軟件升級的影響,我們發現升級軟件的版本時不會導致數據丟失,並且在沒有人工幹預的情況下所有的讀取也能繼續成功執行這就使得Gorilla從單機故障到區域性故障都能夠透明的提供讀取服務。

最終,我們仍然使用我們的HBase TSDB做long-term storage。如果內存中所有的數據丟失,我們的工程師們仍然可以在更加持久的存儲係統中繼續處理數據分析和專門的查詢,並且一旦服務重啟並開始接受新的數據寫入,Gorilla就可以繼續進行實時數據檢測。

 

5. Gorilla上的新工具

Gorilla的低延時查詢特性推動產生了一些新的分析工具。

 

5.1 關聯引擎

首先是一個運行在Gorilla上的時間序列關聯引擎,關聯搜索可以讓用戶對大量時間序列做交互式,蠻力搜索,目前限製在每次100萬個時間序列。

關聯引擎將測試時間序列和大的時間序列集做比較來計算皮爾森產品-時間相關係數(PPMCC)。PPMCC具有在相同形狀走勢的時間序列之間找到他們的關聯性的能力,無論時間序列是什麼樣的規模。這大大有助於通過自動化方式分析故障的根本原因,並回答“當服務掛掉時發生了什麼”。我們發現這種方法能夠帶來比較滿意的結果,並且比本文末尾引用的文獻中描述的類似方法實現起來更簡單。

要計算PPMCC,測試時間序列需要和全量時間序列一起分布在每台Gorilla主機上,然後各個主機各自計算出前N個有關聯關係的時間序列,根據與測試數據相比的PPMCC絕對值排序,並將時間序列值返回。在將來,我們希望Gorilla在時間序列數據的監控上能拓展出更先進的數據挖掘技術,例如文末引用的文獻[10,11,16]中描述的分類歸並和異常檢測技術。

5.2 圖表

低延時的查詢還能擴展出更大查詢量級的工具。舉個例子,與監控團隊無關的工程師們創建了一個新的可視化數據界麵,它要展示大量線型圖表,而這些圖表數據本身就是從大量時間序列中化簡計算來的。這種數據可視化方式讓用戶能夠快速直觀的瀏覽大批量數據,以發現有異常數據值以及與時間相關的異常現象

5.3 聚合

最近,我們將在後台對數據做匯總疊加的程序從一組map-reduce任務中遷移到了Gorilla上直接執行。回想前麵對ODS的介紹,ODS對老數據進行基於時間的聚合(或匯總疊加)壓縮,這種壓縮是有損的,會讓數據之間的間隔度更大,類似於Whisper使用的壓縮格式。在Gorilla之前,map-reduce任務運行在HBase集群上,先讀取出過去一小時的全部數據,進行計算,然後輸出到一張新的低粒度的表中。現在,一個後台定時程序每隔兩小時掃描全量數據,再生成新的數據到低粒度表中。我們之所以更換實現方案是因為在Gorilla中做全數據掃描是非常高效的,方案的更改減少了HBase集群的負載,我們再也不用將所有高密度的數據寫到磁盤,並在HBase上執行開銷昂貴的全表掃描。

 

6. 經驗

6.1 容錯

我們接下來介紹過去6個月發生的幾個預期內和預期外的事件,這些事件在一定程度上影響了Facebook站點的可用性,這裏我們隻限於討論對Gorilla有影響的事件,因為其它問題超出了本文的範疇。

網絡中斷。3起預期外的發生在部分機器的類似網絡中斷/故障事件,網絡中斷被自動檢測到,Gorilla自動將讀重定向到未受影響的區域,沒有任何服務中斷。

應對計劃內的災難。1起計劃內的大型消防演練,模擬某個後端存儲所在處的網絡全部中斷。根據上麵描述的做法,Gorilla將讀切到未受影響的區域,一旦故障區域被恢複,手動從日誌文件拉取故障時間段的數據,從而使故障區域提供的數據麵板可以向最終用戶展示預期內的數據

配置變更和代碼推送。有6次配置變更和6次代碼發布需要在重啟指定區域內的Gorilla。

Bug。一個帶有重大bug的發布部署到了某個區域,Gorilla馬上將負載轉移到其它區域繼續提供服務,直到bug解決。在輸出的數據中,隻有極小的數據準確性問題。

單節點故障。有5次單機故障(與上麵說的bug無關),沒有引起數據丟失,無需修複。

過去6個月,Gorilla沒有出現任何引發檢測異常和報警問題的事故。自從Gorilla推出以來,隻有1次事件影響了實時監控。在任何時候 ,持久化存儲為所有與監控相關的查詢扮演備份的角色。

 

6.2 排查和修複網站故障

關於Facebook如何使用時間序列數據來支撐業務監控的例子,可以看看最近一個依靠監控數據來快速檢測和修複的問題,我們在SREcon15中首次對外介紹了這次事件。

一個神秘的問題導致網站錯誤率出現高峰,錯誤率上升幾分鍾後在Gorilla可以觀察到異常,這時由監控係統發出一個異常警報,幾分鍾後警報通知到相關的技術團隊。然後,辛苦的問題修複工作開始了,一組工程師緩解了這個問題,其它人開始尋找問題的根源。通過使用基於Gorilla構建的工具,包括前麵第5節介紹的時間序列關聯搜索,他們發現將發布的二進製包拷貝到Facebook web服務器這個常規流程出了問題,導致整個網站內存使用率下跌,見圖10。問題的檢測,各種各樣的調試和故障原因分析,依賴於在Gorilla高性能查詢引擎上構建的時間序列分析工具。

 

自從約18個月前推出以來,Gorilla已經幫助Facebook的工程師們識別和排查出了幾個類似的生產環境問題。通過將前90%的查詢的響應速度降到10ms,Gorilla也提升了開發人員的工作效率。另外,現在85%的監控數據都來自Gorilla,隻有少量查詢會打到HBase TSDB上,這也讓HBase集群負載變得更低。

 

6.3 經驗教訓

重點考慮最近的數據而不是曆史數據。Gorilla在優化和設計定位上比較獨特,雖然必須表現得非常可靠,但是它不需要ACID規則來為數據做保障。事實上,我們發現最近的數據在可用性上比過去的數據更為重要,這讓我們在設計上做了比較有意思的權衡,例如在從磁盤讀取出老數據之前保持Gorilla在數據讀取上的可用性。

讀取的延時。高效的壓縮和內存級數據結構極大的加快了數據讀取的速度,並且促進增加了很多使用場景。當Gorilla推出時ODS每秒支撐450次查詢,很快Gorilla就超過了它,目前每秒處理超過5000次常規查詢業務,峰值時達到每秒40000的查詢,如圖9所示。低延時的讀取鼓勵我們的用戶在Gorilla之上構建更高級的數據分析工具,如第5節的描述。

高可用性勝過資源使用效率。容錯能力是Gorilla的一個重要設計目標,它需要具備在不影響數據可用性的情況下承受單機故障的能力。此外,提供的服務還必須能夠承受可能影響到整個區域的災難性事件。基於這個目標,我們在內存中保存兩份冗餘的數據副本,即使這樣會影響資源的使用效率。

 

我們發現開發一個可靠的,有容錯能力的係統是整個項目中最耗時的部分。雖然開發團隊在非常短時間內就開發出了一個高性能、數據壓縮存儲的內存級TSDB原型,但是接下來通過幾個月的努力工作才讓它具備容錯能力。不過當係統的生命力在麵臨真實或模擬的故障挑戰時,容錯能力帶來的好處是顯而易見的。一個可以安全重啟,升級,能隨時新增節點的係統總能讓技術人員從中受益。容錯能力也讓我們能夠以較低的運維成本有效擴展Gorilla,同時為我們的客戶提供高度可靠的服務。

 

7. 接下來的工作

我們希望通過幾種方式來擴展Gorilla。一種方向是在Gorilla內存存儲和HBase存儲之間增加一個更大的基於閃存的二級存儲。這個存儲用來存放每兩小時生成一次的經過數據壓縮之後的分片,但是總容量會比26小時更長,我們發現閃存可以存儲約2周的全量無損的、Gorilla格式壓縮後的數據,數據時段拉長對工程師們排查問題是很有用的。圖8是初步的性能測試結果。

 

在創建Gorilla之前,ODS依賴HBase背後的存儲做為實時數據存儲:在數據寫入到ODS存儲後很短時間,需要被用於讀取操作,這給HBase的磁盤I/O帶來了很大的壓力。現在Gorilla充當最近數據的write-through緩存,在數據發送到ODS存儲後的26小時內都不用從HBase讀取。我們正在利用這個特點重新調整數據寫入鏈路,讓數據在寫入到HBase之前多等待一段時間,這個優化應該會對HBase更有效果,但是目前這個方向還處於早期,沒有相當的對比數據。

 

8. 總結

Gorilla是我們在Facebook開發和部署的一個新的內存時間序列數據庫,Gorilla做為一個write-through cache,用來收集所有Facebook係統上過去26小時的監控數據。在這篇文章中,我們介紹了一種新的壓縮方案,讓我們能夠每分鍾高效的存儲700萬個數據點的監控數據。此外,與磁盤級的TSDB相比,Gorilla使我們生產環境的查詢耗時縮短了70多倍。基於Gorilla創建了一些新的監控工具,包括報警、自動修複以及一個在線異常檢查器。Gorilla已經部署運行了18個月,在這期間經曆了兩次翻倍擴容,而沒有太多運維上的工作,這證明我們的解決方案具有可擴展性。我們還通過幾次大規模模擬真實線上故障的演練驗證了Gorilla的容錯能力 — Gorilla在這些事件中仍然保證了讀寫的高可用,幫助網站在故障中恢複。

最後更新:2017-08-18 12:02:23

  上一篇:go  PostgreSQL SQL 語言:類型轉換
  下一篇:go  Fastjson 在HiTSDB中的應用