本文將通過對時序數據的基本概念、應用場景以及京東智聯云時序數據庫HoraeDB的介紹,為大家揭秘HoraeDB的核心技術架構和解決方案。

首先我們來了解下時序數據庫的基本概念。時序數據庫全稱時間序列數據庫,主要用于處理帶時間標簽的數據,帶時間標簽的數據也稱為時序數據。
時序數據庫是一種高性能、低成本、穩定可靠的在線時序時空數據庫服務,提供高效讀寫、高壓縮比存儲、時序數據插值及聚合計算等服務,廣泛應用于服務和服務器監控系統、物聯網(IoT)設備監控系統、生產安全監控系統和電力檢測系統等行業場景。此外,它還能提供時空場景的查詢和分析能力。
時序數據庫中存儲的是時序數據,時序數據的結構特點和簡單舉例如下:

上圖展示的是一個服務和時間點緊密關聯的流量變化數據,數據如下:

- Metric:時序數據的指標名稱;
- Tags:對時序數據指標進行補充描述的標簽,描述清楚數據是什么、來源等,方便后期對數據進行篩選和聚合計算等;
- Dps:時序數據的數據點,是一系列隨著時間變化的值,分為時間戳和具體的value兩個部分。

- 證券交易:可以用時序數據庫來保存隨著時間波動的交易價格等數據;
- 氣溫變化情況:可以用時序數據庫存儲氣溫變化的數據,便于記錄和分析某個地區的氣溫變化波動規律;
- 服務器監控:通過對大規模應用集群和機房設備的數據采集,存儲到時序數據庫中,就可以實時關注設備運行狀態、資源利用率和業務趨勢,實現數據化運營和自動化開發運維;
- 物聯網傳感器:物聯網設備無時無刻不在產生海量的設備狀態數據和業務消息數據,這些數據有助于進行設備監控、業務分析預測和故障診斷;
- 網站/服務監控數據:通過日志或者其他方式對原始指標數據進行采集和實時計算,最后將實時計算的結果數據存儲到時序數據庫,實現對網站和服務的監控和分析。

- 時序數據的寫入:每秒需要有百萬至千萬量級的時序數據點寫入,并且寫入流量沒有低峰期和高峰期的區分,所以時序數據庫需要解決好如何7*24小時支持好百萬甚至上千萬級的時序數據寫入問題;
- 時序數據的讀取:如何低延遲解決好單個請求讀取百萬級數據點的查詢以及幾十萬級別數據計算和聚合問題;
- 成本問題:時序數據數據量級較大,且在業務上可能至少需要存儲一年以上的歷史數據,以便于后期來對數據進行分析和處理,所以時序數據庫需要解決好存儲成本的問題。

HoraeDB是京東智聯云自研的一款時序數據庫,在數據的寫入協議上完全兼容OpenTSDB,數據的查詢上兼容OpenTSDB restful API和PromQL。
HoraeDB主要有以下特點:
- 高性能:支持數據批量異步寫入,高并發查詢以及強大的數據計算和聚合能力;
- 高可用:數據存儲分布式架構,副本數和數據的一致性級別靈活可調;可以根據需要,對數據的寫入做多AZ雙寫和HA查詢等;
- 使用成本低:豐富的數據類型,REST接口、數據寫入查詢均使用json格式,并且接口和協議上完全兼容OpenTSDB和PromQL,歷史服務前移學習成本較低;
- 兼容開源生態:兼容OpenTSDB和PromQL協議以及開源的Kibana組件,可以方便的使用開源生態中已存在的工具和組件對時序數據進行查詢、分析和展示等。


HoraeDB在整體架構上從上往下進行分層,大概可以分為以下幾層:
- Http-Server層:主要負責HTTP服務端口的監聽、接收以及響應用戶的請求;
- 協議處理層:這層是負責對接各種已有的開源組件的協議。主要是包括OpenTSDB-Parser、Prometheus-Adapter、Remote-Read、Remote-Write等幾個組件:
◆OpenTSDB-Parser:負責對當前開源的OpenTSDB的請求協議進行解析和處理,包括查詢請求和響應請求;
◆ Prometheus-Adapter:對PromQL查詢語法進行解析,解析成HoraeDB內部標準的查詢協議;
◆ Remote-Read:實現Prometheus的Remote-Read模式,可以使用Prometheus來查詢HoraeDB中的數據,具體配置使用方式可以參考Prometheus配置說明;
◆ Remote-Writer:實現Prometheus的Remote-Write模式,可以在生產環境中將HoraeDB作為Prometheus的分布式集群存儲解決方案,將Prometheus中采集到的數據寫入到HoraeDB。具體配置使用方式可以參考Prometheus配置說明。 - HoraeDB協議層:包括HoraeDB-PutReq和HoraeDB-QueryReq,這一層主要是定義好HoraeDB自身寫入的數據的結構和查詢請求的結構;
- 處理引擎層:包括了write-engine和query-engine,這層是分別進行數據寫入和查詢的處理邏輯;
- 數據隊列:這一層主要是針對數據寫入而言的,數據要寫入到底層存儲,會先寫到隊列中,然后消費端根據配置文件的指定的后端,進行數據的處理;
- 存儲Client層:適配各種各樣的三方存儲組件,負責數據的持久化存儲處理,目前實現了ES-Client、Cassandra-Client、Cache-Client、Local-Storage:
◆ ES-Client:負責將時序數據中的Meta部分信息寫入到ES進行存儲;
◆ Cassandra-Client:負責將時序數據中的數據點寫入到Cassandra中進行持久化存儲;
◆ Cache-Client:將新寫入的熱點時序數據寫入到高速緩存組件,對熱點數據進行Cache。 - 數據存儲層
◆ Cassandra:存儲時序數據中的數據點;
◆ ES:存儲時序數據中的Meta信息;
◆ TS-Cache:自研的時序數據Cache系統,內部使用delta-delta和XOR編碼方式對時序數據的時間戳和值進行壓縮,可以存儲最近三小時的熱點時序數據。


HoraeDB在底層數據存儲將時序數據拆分成兩部分進行存儲,一部分是meta數據,這部分數據主要是對時序數據進行描述的,描述了一條時間序列是什么,來自于哪里;另外一部分是時序數據點,包括時間戳和具體的值,這部分數據是會隨著時間變化,周期性上報的部分。例如下面一條數據:

- Meta部分包括了name、tags、additionTag等三個部分,對一條時間線進行了描述;
- 數據點部分包括了timeStamp和value,是具體的時序數據點。
它們各自的特點如下:
- Meta
◆ 寫入后,基本不會有變更;
◆ 需要支持多維度,多種方式進行數據篩選,比如Tag精確匹配,前綴搜索、正則匹配等多種搜索;
◆ 數據量相對小。 - 時序數據點
◆ 周期性匯報;
◆ 和時間戳強關聯;
◆ 需要支持按照時間范圍進行數據篩選;
◆ 數據量大。
根據以上特點,我們在數據持久化存儲中,針對Meta和時序數據點,分別選擇了Elasticsearch和Cassandra來進行數據存儲。
Elasticsearch
Elasticsearch是一個基于RESTful web接口并且構建在Apache Lucene之上的開源分布式搜索引擎。
同時ES還是一個分布式文檔數據庫,其中每個字段均可被索引,而且每個字段的數據均可被搜索,能夠橫向擴展至數以百計的服務器存儲以及處理PB級的數據。可以在極短的時間內存儲、搜索和分析大量的數據。
Cassandra
Cassandra是一套開源分布式NoSQL數據庫系統。具有以下特點:
- 線性擴展,輕松應對速度、多樣性和復雜性問題:Cassandra是線性擴展,可以根據前臺數據流量輕松確定集群規模;
- 架構簡單,運維成本低:Cassandra不依賴外部組件,所有必須的操作都集成在Cassandra內部了。同時,由于它是P2P對等架構,無主,環上的節點都是對等的,極度簡化部署及后續運維工作,適合大規模部署;
- 高可用:Cassandra采用了許多容錯機制。由于Cassandra是無主的,所以沒有單點故障,可以做到不停服滾動升級。這是因為Cassandra可以支持多個節點的臨時失效(取決于群集大小),對集群的整體性能影響可以忽略不計。Cassandra提供多地域容災,允許您將數據復制到其他數據中心,并在多個地域保留多副本。
HoraeDB在Cassandra中的Schema定義:

- 行定位:uuid + partitionKey來唯一定位到一行數據,uuid是根據name + 排序后的Tag來計算出來的,唯一可以表示單個時間序列的ID, partitionKey是由程序來定義的劃分行的字符串,比如按照天來劃分行,那么我們就可以傳入數據時間戳的當天日期;
- 列定位:每列由數據的時間戳來表示,每個時序點占一列,每列中包含value和createTime字段。value是經過編碼后的數據,createTime是由時序數據點到來的時間計算出的timeUUID來表示,用于支持多版本的特性。

業務對于監控數據的使用需求多種多樣,有查最新數據的異常告警,也有查看一整年指標數據的趨勢圖展示,數據量越大查詢耗時就越久,如果放在瀏覽器端處理也要耗費大量的內存。這不但對系統造成了很大的壓力,也給用戶帶來了難以忍受的查詢體驗。鑒于此,我們引入了多級的降采樣機制來應對不同跨度的數據查詢。

如上圖所示,提前降低采樣算法就是將連續不斷流入的時序數據點來進行分桶,計算出這個桶內的均值、最大值、最小值、總和等,這樣用戶在查詢數據的時候,直接返回合適的采樣數據即可。
為了提供多種粒度的數據,HoraeDB支持對原始數據進行多級降采樣,比如可以將原始數據降采樣成10m和1h粒度的數據。

流式抽樣的核心處理思想如上圖所示:在HoraeDB的內存中保存了每個時間序列。每個序列上都維護了當前這個序列的cnt、sum、max、min、last等值,隨著時間的推移,時序數據點源源不斷的流入,相應序列上的cnt、sum、max、last都會進行累加或者值更新,當到達設定的時間窗口,比如10分鐘,就會將計算后的結果存入到Cassandra中。
流式抽樣中需要解決的難點主要有兩個:數據遲到問題和時間窗口到達后對底層Cassandra的壓力沖擊。HoraeDB對這些問題的解決方案如下:
- 數據遲到問題:為了解決數據遲到問題,HoraeDB的數據抽樣器對每個時間序列的抽樣結果暫存一個周期,等待遲到的數據到來,然后再存入到底層Cassandra;
- 時間窗口到達,對底層尖峰流量:數據發送使用了令牌桶算法來進行數據的發送,控制數據發送頻率。

在時序的場景下,我們經常會有一些大批量數據分析計算的需求。比如需要計算10萬個服務器30分鐘內的cpu變化趨勢。這樣的查詢會很慢,同時也會導致我們的時序存儲服務不穩定。
為了應該對這種業務場景,我們做了預聚合計算方案,預聚合計算分為流式聚合計算和批量計算。如下圖所示:

- 流式聚合計算
對于大規模時序數據分析場景,我們采用流式聚合計算,如上圖所示,數據上報到kafka。我們的Flink Job會根據用戶提前配置好的聚合規則,對流入的數據進行聚合計算,計算完成后,寫回到我們的存儲中,流式聚合計算有以下的特點:
◆ 計算性能強,可橫向擴展;
◆ 計算支持靈活性相對較弱;
◆ 需要部署單獨的計算等組件。 - 批量聚合計算
批量聚合計算使用的是拉模式進行數據聚合計算,核心邏輯是HoraeDB根據用戶創建的聚合規則,設置一些定時計算任務,計算任務會定時向底層存儲層發起數據拉取和計算,計算結果寫回到存儲中,批量聚合計算有以下的特點:
◆ 計算靈活性強;
◆ 無需單獨部署計算組件和消息隊列;
◆ 支持到5w線左右,對底層存儲開銷高。


HoraeDB原有架構下,做一些大批量數據查詢和計算時存在以下問題:
- 聚合性能低:原有引擎在執行聚合運算的時候,也和傳統數據庫所通常采用的iterative執行模式一樣,迭代執行聚合運算。問題在于每次iteration執行,返回的是一個時間點。Iterative 執行每次返回一條時間點,但對HoraeDB查詢有可能需要訪問大量時間線數據,這樣的執行方式效率上并不可取;
- 查詢速度慢
◆ 從Cassandra查詢數據是一個比較耗時的過程,會耗費比較長的時間在數據準備上;
◆ 整個計算過程需要等Cassandra中的數據全加載到內存中,才開始計算。
- 內存和CPU資源消耗高
◆ HoraeDB有可能在短時間內下發太多Cassandra讀請求,一個查詢涉及到的Cassandra讀請求同時異步提交,有可能在很短時間內向Cassandra下發大量的讀請求。這樣,一個大查詢就有可能把底層的Cassandra打爆;
◆ 同時拉取大量的數據點到內存中,會導致HoraeDB內存打爆。
新的查詢引擎針對老的查詢計算引擎進行了優化:
借鑒傳統數據庫執行模式,引入Pipeline的執行模式。Pipeline包含不同的執行計算算子(operator),一個查詢被物理計劃生成器解析分解成一個Query Plan, 由不同的執行算子組成,DAG上的root operator負責驅動查詢的執行,并將查詢結果返回調用者。在執行層面,采用的是top-down需求驅動的方式,從root operator驅動下面operator的執行。這樣的執行引擎架構具有如下優點:
- 這種架構方式被很多數據庫系統采用并證明是有效的;
- 接口定義清晰,不同的執行計算算子可以獨立優化,而不影響其他算子;
- 易于擴展:通過增加新的計算算子,很容易實現擴展功能。比如目前查詢協議里只定義了tag上的查詢條件。如果要支持指標值上的查詢條件(cpu.usage >= 70% and cpu.usage <=90%),可以通過增加一個新的ValueFilterOp來實現。

時序數據中,一般情況下,最近三個小時的數據是被查詢比較多的熱點數據,在HoraeDB中,為了加速這部分數據的訪問,我們把最近三小時的數據放到自研的分布式緩存組件-TS-Cache中去。
TS-Cache是一個分布式的數據緩存系統,數據通過Hash算法均勻的分布在各個TS-Cache節點中。因為TS-Cache是個緩存組件,對數據的穩定性要求并沒有那么高,所以我們的時序數據在TS-Cache中保存的是單副本。

數據在內存中的組織如上圖所示,數據的頂層是一個SharedMap,它其實就是一個數組,目的是用來在內部對時序數據進行分片,降低gc對服務性能的影響,以及適當的減小鎖的粒度,提升服務的讀寫性能。數據中的一個項就是SeriesMap,用來保存時序數據項和它的時序數據點的值,其內部的核心數據結構是一個跳表,跳表中的每一項對應的是一個時序數據中的一個項,在這邊用SeriesData來進行表示。
SeriesData中有兩個核心的數據結構,一個是cs,一個是blocks,cs是我們的一個流式壓縮序列,這個流式壓縮需要時根據上面所述的壓縮算法來進行實現的,時序數據寫入系統后,會直接寫到這個壓縮序列cs中,每隔一定時間,會把壓縮序列中的數據導出,形成一個block數據塊,寫到blocks數組中。
為了用更少的內存來存儲更多的數據,在TS-Cache中的數據是壓縮成為block后進行存儲,放到了上圖的blocks數組中進行存儲;數據壓縮算法使用的是Gorilla這篇論文所提出的數據壓縮算法,Gorilla引入了對timestamp和value的高壓縮比算法,可大幅降低數據存儲的大小。
Timestamp根據時間關聯的條目進行差值計算、以及差值的差值計算得到占用字節數非常小的數值并進行保存。同樣Value使用XOR算法進行計算得到占用存儲更小的數值進行保存。

- 時間戳壓縮:時序數據的產生大部分情況下都是有周期性的,可能是30s、1m等,所以在存儲時間戳的時候,我們只需要存儲時間戳直接的差值;
- 值壓縮:通過對歷史數據進行分析,發現大部分相鄰時間的時序數據的值比較接近,而如果Value的值比較接近,則在浮點二進制表示的情況下,相鄰數據的Value會有很多相同的位。整數型數據的相同位會更多。相同位比較多,意味著如果進行XOR運算的話會有很多位都為0,那么我們將當前值與前序值取XOR(異或)運算,保存XOR運算結果。


HoraeDB作為一個海量時序數據的存儲系統,經常會遇到一些突發性的流量高峰或者一些不合理的大查詢、慢查詢,如何保障HoraeDB的穩定性便成了一個要攻破的技術難點。
為了保障HoraeDB的穩定性,我們做了以下幾個方面的工作:
- 服務隔離;
- HoraeDB讀寫分離:讀寫分別部署不同實例,避免大查詢慢查詢影響數據寫入;
- 計算處理層和存儲層分離:數據寫入和查詢處理邏輯在HoraeDB層做,底層數據存儲放到存儲層進行;
- 機房隔離:HoraeDB部署上支持多寫,底層存儲可以配置不同機房,實現數據多寫互備,查詢支持多機房HA查詢;
- 限流
◆ 在HTTP接口層支持接口調用頻率進行限流;
◆ 針對寫入,HoraeDB內部通過寫入隊列來感知當前寫入負載,當隊列滿,即丟棄數據,保護HoraeDB正常工作;
◆ 查詢上,基于查詢時間跨度、時間線條數、數據點規模等進行了限制,避免大查詢影響系統穩定性。 - 查詢負載管理:因為在我們的時序數據查詢場景中,80%以上的情況都是查詢最近三小時內的數據,都是一些查詢和計算量比較小的查詢任務,所以在查詢上我們根據查詢請求進行計算量簡單預估,分為慢查詢和快查詢,分別放入到對應的查詢任務隊列,避免慢查詢影響快查詢;
- 綜合全面的服務監控指標,HoraeDB的問題可以快速被發現和定位。