時間序列資料庫漫談
這篇文章是寫給東嶽網路工作室的小夥伴們的 (廣告:歡迎在交大的同學加入),適用於有一定資料庫背景並且想要了解時間序列資料庫的同學。 PS: 中文版是在英文版之後寫的,很生硬,請見諒。
目錄
- 什麼是時間序列資料庫 (TSDB)
- 時間序列資料庫數據模型
- 時間序列資料庫演變
- 時間序列資料庫類型
- KairosDB
- InfluxDB
- 熱點話題
- 低延遲
- 數據
- 元數據索引
- Tracing
什麼是時間序列資料庫 (TSDB)
時間序列資料庫 Time Series Database (TSDB) 相對於關係型資料庫 (RDBMS),NoSQL,NewSQL 還很年輕。 但是,隨著系統監控以及物聯網的發展,已經開始受到更多的關注。 維基百科上對於時間序列的定義是『一系列數據點按照時間順序排列』, 但是我個人的理解是存儲在服務端的客戶端歷史。 時間序列數據就是歷史,它具有不變性, 唯一性以及可排序性。 比如在2017年9月3日21點24分44秒,華東區的機器001的CPU使用率是10.02%, 這個值不會像銀行存款一樣隨著時間發生變化,它一旦產生了就不會有更新。 下一秒的使用率是一個新的數據點,其他機器的使用率在其他時間序列里。 並且數據到達伺服器的順序並不影響正確性,根據數據本身可以直接進行排序和去重。 客戶端發送本地的歷史到伺服器端,即使伺服器端掛掉了,客戶端依舊繼續他本來要做的事情而不受到影響。 對於很多客戶端來說,發送數據到 TSDB 跟它的本職工作並沒有關聯。 比如一個靜態文件伺服器的主要職責是傳送文件而不是上報 HTTP 狀態碼。 關係型資料庫則起著完全不一樣的作用,它是客戶端做決定的主要依據, 這就導致時間序列資料庫和關係型資料庫的讀寫規律有很大的不同。 比如你取錢之前,銀行的程序必須從資料庫里找到你的那條存款記錄,讀出你的餘額,確認不會透支才能把錢給你, 然後更新你的餘額。 然而大多數時間序列資料庫的客戶端是只讀(監控系統)或者只寫(被監控的系統), 並且讀取數據是並不是讀取特定的某條,而是讀取某個時間區間內的大量數據,比如最近1小時的CPU使用率遠比 2017年9月3日21點24分44秒的CPU使用率有用,脫離上下文的時間序列數據並沒有什麼作用。
時間序列數據跟關係型資料庫有太多不同,但是很多公司並不想放棄關係型資料庫。 於是就產生了一些特殊的用法,比如用 MySQL 的 VividCortex, 用 Postgres 的 Timescale。 很多人覺得特殊的問題需要特殊的解決方法,於是很多時間序列資料庫從頭寫起,不依賴任何現有的資料庫, 比如 Graphite,InfluxDB。
時間序列資料庫演變
時間序列資料庫有很多, 下面列出的是一些我個人認為具有里程碑意義的資料庫。 很多資料庫主頁上並沒有最初版本的發布日期,因此以 GitHub 上最早的 tag 作為發布日期。
- 1999/07/16 RRDTool First release
- 2009/12/30 Graphite 0.9.5
- 2011/12/23 OpenTSDB 1.0.0
- 2013/05/24 KairosDB 1.0.0-beta
- 2013/10/24 InfluxDB 0.0.1
- 2014/08/25 Heroic 0.3.0
- 2017/03/27 TimescaleDB 0.0.1-beta
RRDTool 是最早的時間序列資料庫,它自帶畫圖功能,現在大部分時間序列資料庫都使用Grafana來畫圖。 Graphite 是用 Python 寫的 RRD 資料庫,它的存儲引擎 Whisper 也是 Python 寫的, 它畫圖和聚合能力都強了很多,但是很難水平擴展。 來自雅虎的 OpenTSDB 使用 HBase 解決了水平擴展的問題。 KairosDB 最初是基於OpenTSDB修改的,但是作者認為兼容HBase導致他們不能使用很多 Cassandra 獨有的特性, 於是就拋棄了HBase僅支持Cassandra。 有趣的是,在新發布的 OpenTSDB 中也加入了對 Cassandra 的支持。 故事還沒完,Spotify 的人本來想使用 KairosDB,但是覺得項目發展方向不對以及性能太差,就自己擼了一個 Heroic。 InfluxDB 早期是完全開源的,後來為了維持公司運營,閉源了集群版本。 在 Percona Live 上他們做了一個開源資料庫商業模型正面臨危機的演講,裡面調侃紅帽的段子很不錯。 並且今年的 Percona Live 還有專門的時間序列資料庫單元。
時間序列資料庫數據模型
時間序列數據可以分成兩部分,序列和數據點。 序列就是標識符,比如華東區機器001的CPU使用率。 數據點是時間戳和數值構成的數組。
對於序列,主要的目的是方便使用者進行搜索和篩選。 比如你需要查詢華東區所有機器的CPU使用率。 序列 華東區機器001的CPU使用率 的標識符是 name=cpu.usage machine=001 region=cn-east, 查詢則是 name=cpu.usage machine=* region=cn-east。 為了處理大量的序列,需要建立(倒排)索引來提高查詢速度。 一些時間序列資料庫選擇使用外部搜索引擎來解決這個問題,比如 Heroic 使用了 Elasticsearch, 另一些則選擇自己寫索引,比如 InfluxDB, Prometheus。
對於數據點,有兩種模型,一個數組的點 [{t: 2017-09-03-21:24:44, v: 0.1002}, {t: 2017-09-03-21:24:45, v: 0.1012}] 或者兩個數組,一個存時間戳,一個存數值。前者是行存,後者是列存(不是列簇)。 大部分基於現有資料庫( Cassandra, HBase ) 的是第一種。 對於新的時間序列資料庫第二種更為普遍,TSDB 屬於 OLAP 的一個子集,列存能有更好的壓縮率和查詢性能。
時間序列資料庫類型
時間序列資料庫可以分成兩類,基於現有的資料庫或者專門為時間序列數據寫的資料庫。 我們以 KairosDB 和 InfluxDB 為例來分析。 有很多時間序列資料庫是基於 Cassandra 的, KairosDB 是其中比較早的一個。 InfluxDB 是專用於時間序列的資料庫,他們嘗試了很多存儲引擎,最後寫了自己的 Time Structured Merge Tree.
KairosDB
在看 KairosDB 之前我們先用一個簡化版本的預熱一下。 Xephon-K 是我寫的一個有多種存儲後端的時間序列資料庫(專門用來對付各種課程大作業)。 它有一個非常 naive 的基於 Cassandra 的實現。
如果你對 Cassandra 不熟的話,這裡有個簡單的介紹。 Cassandra 是一個列簇資料庫,是谷歌 BigTable 的開源實現。列簇又被稱作寬列。 實質上是一個多層嵌套的哈希表。它是一個行存儲,不是列存儲。 一些 Cassandra 的名詞可以跟關係型資料庫中的對應起來。 Cassandra 中的 Keyspace 就是指的 database, 比如一個博客和一個網店雖然使用同一個 MySQL 伺服器,但是各用一個資料庫以進行隔離。 Cassandra 中的 Table 是一個哈希表,他的 Partition Key 是哈希表的鍵(也被叫做物理行鍵),它的值也是一個哈希表,這個哈希表的鍵是 Cluster Key, 它的值還是一個蛤希表。 當使用 CQL 創建一個 Table 的時候,主鍵中的第一個列是 Partition Key,第二個列是 Cluster Key。 比如在下面的 CQL 中, Keyspace 是 naive, Table 是 metrics,Partition Key 是 metric_name, Cluster Key 是 metrics_timestamp。 最內層的哈希表是 {value: 10.2}, 如果需要我們可以存更多的值,比如 {value: 10.2, annotation: "新 bug 上線啦"}。
CREATE TABLE IF NOT EXISTS naive.metrics ( metric_name text, metric_timestamp timestamp, value int, PRIMARY KEY (metric_name, metric_timestamp))INSERT INTO naive.metrics (metric_name, metric_timestamp, value) VALUES (cpu, 2017/03/17:13:24:00:20, 10.2) INSERT INTO naive.metrics (metric_name, metric_timestamp, value) VALUES (mem, 2017/03/17:13:24:00:20, 80.3)
上圖顯示了使用 Cassandra 存儲時間序列數據時 naive 的表結構, Cluster Key 存儲時間戳,列的值存儲實際的數值。 它 naive 之處在於序列和 Cassandra 的物理行是一一對應的。 當單一序列的數據點超過 Cassandra 的限制(20億)時就會崩潰。
一個更加成熟的表結構是把一個時間序列按時間範圍分區,(KairosDB 按照 3 周來劃分,但是可以根據數據量進行不定長的劃分)。 為了存儲分區的信息,需要一張額外的表。 同時在 naive 里序列的名稱只是一個簡單的字元串,如果需要按照多種條件進行篩選的話,需要存儲更多的鍵值對,並且對於這些鍵值對需要建立索引以提高查詢速度。
下面是完整的 KairosDB 的表結構,data_points 表對應的是 naive 里的 metrics 表。 它看上去不像人寫的,因為它就是直接導出的,KairosDB 使用的舊版 Cassandra 的 Thrift API 創建表結構,沒有 .cql 文件。
CREATE TABLE IF NOT EXISTS data_points ( key blob, column1 blob, value blob, PRIMARY KEY ((key), column1)) WITH COMPACT STORAGE;CREATE TABLE IF NOT EXISTS row_key_index ( key blob, column1 blob, value blob, PRIMARY KEY ((key), column1)) WITH COMPACT STORAGE;CREATE TABLE IF NOT EXISTS row_key_time_index ( metric text, row_time timestamp, value text, PRIMARY KEY ((metric), row_time))CREATE TABLE IF NOT EXISTS row_keys ( metric text, row_time timestamp, data_type text, tags frozen<map<text, text>>, value text, PRIMARY KEY ((metric, row_time), data_type, tags))CREATE TABLE IF NOT EXISTS string_index ( key blob, column1 blob, value blob, PRIMARY KEY ((key), column1)) WITH COMPACT STORAGE
有很多基於 Cassandra 的時間序列資料庫,他們的結構大多相同,你可以看這個列表。 我最近正在寫一個如何用 Cassandra 和 Golang 自己寫個時間序列資料庫的博客,寫好之後會把地址更新在這裡。
InfluxDB
InfluxDB 在存儲引擎上糾結了很久, leveldb, rocksdb, boltdb 都玩了個遍,最後決定自己造個輪子叫 Time Structured Merge Tree。
Time Structured Merge Tree (TSM) 和 Log Structured Merge Tree (LSM) 的名字都有點誤導性,關鍵並不是樹,也不是日誌或者時間,而是 Merge。 寫入的時候,數據先寫入到內存里,之後批量寫入到硬碟。讀的時候,同時讀內存和硬碟然後合併結果。 刪除的時候,寫入一個刪除標記,被標記的數據在讀取時不會被返回。 後台會把小的塊合併成大的塊,此時被標記刪除的數據才真正被刪除,這個過程叫做 Compaction。 相對於普通數據,有規律的時間序列數據在合併的過程中可以極大的提高壓縮比。
下圖是一個簡化版的 TSM,每個塊包含序列標識符,一組時間戳,一組值。 注意時間戳和值是分開存儲的,而不是交替存儲的,所以 InflxuDB 是一個列存儲。 InfluxDB 會根據數據來選擇壓縮的方法,如果可以使用行程編碼是最好的, 否則會使用 Gorilla 中提到的浮點數壓縮方法以及變長編碼。 時間戳和數值一般會使用不同的壓縮方法,因為時間戳大多是非常大的整數而數值是非常小的浮點數。
chunk--------------------------------------------------| id | compressed timestamps | compressed values |--------------------------------------------------tsm file-------------------------------------------------------------------| header | chunk 0 | chunk 1 | ... | chunk 10086 | index | footer |-------------------------------------------------------------------
熱點話題
低延遲
時間序列資料庫主要是用來分析的,所以提高響應速度對於診斷生產環境的問題是十分重要的。
最直接的提速方法就是把所有數據都放在內存,Facebook 寫了叫 Gorilla 的純內存時間序列資料庫發表在 VLDB 上,現在已經開源,改名為 Beringei(都是猩猩…)。
另一種提速的方法是提前聚合。因為查詢中經常需要對一個很長的時間區間取一些粗粒度的值,比如6月到8月每天的平均CPU使用率。 這些聚合值(均值,最大,最小) 都可以在存儲數據的時候計算出來。BtrDB 和 Akumuli 都在內部節點中存儲聚合值,這樣在很多查詢中底層的節點不需要被訪問就可以得到結果。
同時一個好的數據傳輸格式也可以提高響應速度,雖然 JSON 被廣泛使用,但是二進位的格式對於有大量數字的數據會顯著的提升。 protobuf 可能會是一個更好的選擇。
處理舊數據
很多時間序列數據都沒有多大用處,特別是當系統長時間正常運行時,完整的歷史數據意義並不大。 所以有些資料庫比如 RDDTool 和 Graphite 會自動刪除高精度的數據,只保留低精度的。 但是對於很多新的時間序列資料庫,在聚合和刪除大量舊數據的同時保證系統正常運行並不像刪除一個本地文件那樣簡單。 如果監控系統比被監控系統還不穩定就比較尷尬了。
元數據索引
時間序列的標識符是時間序列資料庫里主要的元數據。 Heroic 使用 Elasticsearch 來存儲元數據, 查詢首先通過 Elasticsearch 來取得符合要求的序列標識符,之後從 Cassandra 根據標識符來讀取對應的數據。 但是維護一個完整的搜索引擎帶來的運維壓力和增加的通信時間都是不能忽視的。 因此 InfluxDB 和 Prometheus 就自己寫了倒排索引來索引元數據。
Tracing
InfluxDB 的人寫了一篇博客 Metrics are dead, 起因是在一個關於監控的會議 Monitorama 上有人說單純的監控數據已經不能滿足他們複雜的微服務架構了。 於是 InfluxDB 的人反駁說並不是所有人都在使用大規模的分散式系統,對於很多簡單的應用單純的監控數據已經完全夠用了。 我的看法是時間序列資料庫是可以用來存 Trace 的。 Trace 是更加複雜的時間序列數據,把單純的數值變成一個包含更多信息的對象,它就是一個 Trace。 並且很多流行的 Tracer 的存儲也是使用 Cassandra, 比如 Zipkin, Uber 的 Jaeger。更新: InfluxDB 現在已經支持存儲 Trace 了
由於篇幅限制,有很多話題我們沒有涉及,比如壓縮,Pull vs Push, 寫放大等,在以後的博客中會陸續介紹。
參考
- Awesome Time Series Database
- Akumuli
- Beringei
- BtrDB
- Gorilla
- Grafana
- Graphite
- Heroic
- InfluxDB
- Jaeger - Tracer
- KairosDB
- OpenTSDB
- Prometheus
- RRDTool
- Timescale - TSDB using Postgres
- VividCortex - TSDB using MySQL
- Zipkin - Tracer
許可
作者郭平雷,同步發表在個人博客和東嶽團隊博客上,採用知識共享署名-非商業性使用-相同方式共享 3.0 未本地化版本許可協議進行許可,非商業性轉載請註明出處(東嶽博客),其他需求請與我們聯繫。
推薦閱讀:
※建庫、搬家、開版與其他
※爬蟲會用到的小工具: LazySpider 發布啦!
※有沒有比較好的銀行理財產品資料庫?
※TiDB Best Practice
※爬取知乎60萬用戶信息之後的簡單分析
TAG:数据库 |