"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

聽說過時序數據庫嗎?

合併之後就變成了:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

聽說過時序數據庫嗎?

合併之後就變成了:

聽說過時序數據庫嗎?

可以看到,行變成了列了。每一列可以代表這一分鐘內一秒的數據。

Elasticsearch有一個功能可以實現類似的優化效果,那就是Nested Document。我們可以把一段時間的很多個數據點打包存儲到一個父文檔裡,變成其嵌套的子文檔。示例如下:

{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

這樣可以把數據點公共的維度字段上移到父文檔裡,而不用在每個子文檔裡重複存儲,從而減少索引的尺寸。

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

聽說過時序數據庫嗎?

合併之後就變成了:

聽說過時序數據庫嗎?

可以看到,行變成了列了。每一列可以代表這一分鐘內一秒的數據。

Elasticsearch有一個功能可以實現類似的優化效果,那就是Nested Document。我們可以把一段時間的很多個數據點打包存儲到一個父文檔裡,變成其嵌套的子文檔。示例如下:

{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

這樣可以把數據點公共的維度字段上移到父文檔裡,而不用在每個子文檔裡重複存儲,從而減少索引的尺寸。

聽說過時序數據庫嗎?

在存儲的時候,無論父文檔還是子文檔,對於Lucene來說都是文檔,都會有文檔Id。但是對於嵌套文檔來說,可以保存起子文檔和父文檔的文檔id是連續的,而且父文檔總是最後一個。有這樣一個排序性作為保障,那麼有一個所有父文檔的posting list就可以跟蹤所有的父子關係。也可以很容易地在父子文檔id之間做轉換。把父子關係也理解為一個filter,那麼查詢時檢索的時候不過是又AND了另外一個filter而已。前面我們已經看到了Elasticsearch可以非常高效地處理多filter的情況,充分利用底層的索引。

使用了嵌套文檔之後,對於term的posting list只需要保存父文檔的doc id就可以了,可以比保存所有的數據點的doc id要少很多。如果我們可以在一個父文檔裡塞入50個嵌套文檔,那麼posting list可以變成之前的1/50。

如何利用索引和主存儲,是一種兩難的選擇。

  • 選擇不使用索引,只使用主存儲:除非查詢的字段就是主存儲的排序字段,否則就需要順序掃描整個主存儲。
  • 選擇使用索引,然後用找到的row id去主存儲加載數據:這樣會導致很多碎片化的隨機讀操作。

沒有所謂完美的解決方案。MySQL支持索引,一般索引檢索出來的行數也就是在1~100條之間。如果索引檢索出來很多行,很有可能MySQL會選擇不使用索引而直接掃描主存儲,這就是因為用row id去主存儲裡讀取行的內容是碎片化的隨機讀操作,這在普通磁盤上很慢。

Opentsdb是另外一個極端,它完全沒有索引,只有主存儲。使用Opentsdb可以按照主存儲的排序順序快速地掃描很多條記錄。但是訪問的不是按主存儲的排序順序仍然要面對隨機讀的問題。

Elasticsearch/Lucene的解決辦法是讓主存儲的隨機讀操作變得很快,從而可以充分利用索引,而不用懼怕從主存儲裡隨機讀加載幾百萬行帶來的代價。

Opentsdb 的弱點

Opentsdb沒有索引,主存儲是Hbase。所有的數據點按照時間順序排列存儲在Hbase中。Hbase是一種支持排序的存儲引擎,其排序的方式是根據每個row的rowkey(就是關係數據庫裡的主鍵的概念)。MySQL存儲時間序列的最佳實踐是利用MySQL的Innodb的clustered index特性,使用它去模仿類似Hbase按rowkey排序的效果。所以Opentsdb的弱點也基本適用於MySQL。Opentsdb的rowkey的設計大致如下:

[metric_name][timestamp][tags]

舉例而言:

Proc.load_avg.1m 12:05:00 ip=10.0.0.1
Proc.load_avg.1m 12:05:00 ip=10.0.0.2
Proc.load_avg.1m 12:05:01 ip=10.0.0.1
Proc.load_avg.1m 12:05:01 ip=10.0.0.2
Proc.load_avg.5m 12:05:00 ip=10.0.0.1
Proc.load_avg:5m 12:05:00 ip=10.0.0.2

也就是行是先按照metric_name排序,再按照timestamp排序,再按照tags來排序。

對於這樣的rowkey設計,獲取一個metric在一個時間範圍內的所有數據是很快的,比如Proc.load_avg.1m在12:05到12:10之間的所有數據。先找到Proc.load_avg.1m 12:05:00的行號,然後按順序掃描就可以了。

但是以下兩種情況就麻煩了。

  • 獲取12:05 到 12:10 所有 Proc.load_avg.* 的數據,如果預先知道所有的metric name包括Proc.load_avg.1m,Proc.load_avg.5m,Proc.load_avg.15m。這樣會導致很多的隨機讀。如果不預先知道所有的metric name,就無法知道Proc.load_avg.*代表了什麼。
  • 獲取指定ip的數據。因為ip是做為tags保存的。即便是訪問一個ip的數據,也要把所有其他的ip數據讀取出來再過濾掉。如果ip總數有十多萬個,那麼查詢的效率也會非常低。為了讓這樣的查詢變得更快,需要把ip編碼到metric_name裡去。比如ip.10.0.0.1.Proc.load_avg.1m 這樣。

所以結論是,不用索引是不行的。如果希望支持任意條件的組合查詢,只有主存儲的排序是無法對所有查詢條件進行優化的。但是如果查詢條件是固定的一種,那麼可以像Opentsdb這樣只有一個主存儲,做針對性的優化。

DocValues為什麼快?

DocValues是一種按列組織的存儲格式,這種存儲方式降低了隨機讀的成本。傳統的按行存儲是這樣的:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

聽說過時序數據庫嗎?

合併之後就變成了:

聽說過時序數據庫嗎?

可以看到,行變成了列了。每一列可以代表這一分鐘內一秒的數據。

Elasticsearch有一個功能可以實現類似的優化效果,那就是Nested Document。我們可以把一段時間的很多個數據點打包存儲到一個父文檔裡,變成其嵌套的子文檔。示例如下:

{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

這樣可以把數據點公共的維度字段上移到父文檔裡,而不用在每個子文檔裡重複存儲,從而減少索引的尺寸。

聽說過時序數據庫嗎?

在存儲的時候,無論父文檔還是子文檔,對於Lucene來說都是文檔,都會有文檔Id。但是對於嵌套文檔來說,可以保存起子文檔和父文檔的文檔id是連續的,而且父文檔總是最後一個。有這樣一個排序性作為保障,那麼有一個所有父文檔的posting list就可以跟蹤所有的父子關係。也可以很容易地在父子文檔id之間做轉換。把父子關係也理解為一個filter,那麼查詢時檢索的時候不過是又AND了另外一個filter而已。前面我們已經看到了Elasticsearch可以非常高效地處理多filter的情況,充分利用底層的索引。

使用了嵌套文檔之後,對於term的posting list只需要保存父文檔的doc id就可以了,可以比保存所有的數據點的doc id要少很多。如果我們可以在一個父文檔裡塞入50個嵌套文檔,那麼posting list可以變成之前的1/50。

如何利用索引和主存儲,是一種兩難的選擇。

  • 選擇不使用索引,只使用主存儲:除非查詢的字段就是主存儲的排序字段,否則就需要順序掃描整個主存儲。
  • 選擇使用索引,然後用找到的row id去主存儲加載數據:這樣會導致很多碎片化的隨機讀操作。

沒有所謂完美的解決方案。MySQL支持索引,一般索引檢索出來的行數也就是在1~100條之間。如果索引檢索出來很多行,很有可能MySQL會選擇不使用索引而直接掃描主存儲,這就是因為用row id去主存儲裡讀取行的內容是碎片化的隨機讀操作,這在普通磁盤上很慢。

Opentsdb是另外一個極端,它完全沒有索引,只有主存儲。使用Opentsdb可以按照主存儲的排序順序快速地掃描很多條記錄。但是訪問的不是按主存儲的排序順序仍然要面對隨機讀的問題。

Elasticsearch/Lucene的解決辦法是讓主存儲的隨機讀操作變得很快,從而可以充分利用索引,而不用懼怕從主存儲裡隨機讀加載幾百萬行帶來的代價。

Opentsdb 的弱點

Opentsdb沒有索引,主存儲是Hbase。所有的數據點按照時間順序排列存儲在Hbase中。Hbase是一種支持排序的存儲引擎,其排序的方式是根據每個row的rowkey(就是關係數據庫裡的主鍵的概念)。MySQL存儲時間序列的最佳實踐是利用MySQL的Innodb的clustered index特性,使用它去模仿類似Hbase按rowkey排序的效果。所以Opentsdb的弱點也基本適用於MySQL。Opentsdb的rowkey的設計大致如下:

[metric_name][timestamp][tags]

舉例而言:

Proc.load_avg.1m 12:05:00 ip=10.0.0.1
Proc.load_avg.1m 12:05:00 ip=10.0.0.2
Proc.load_avg.1m 12:05:01 ip=10.0.0.1
Proc.load_avg.1m 12:05:01 ip=10.0.0.2
Proc.load_avg.5m 12:05:00 ip=10.0.0.1
Proc.load_avg:5m 12:05:00 ip=10.0.0.2

也就是行是先按照metric_name排序,再按照timestamp排序,再按照tags來排序。

對於這樣的rowkey設計,獲取一個metric在一個時間範圍內的所有數據是很快的,比如Proc.load_avg.1m在12:05到12:10之間的所有數據。先找到Proc.load_avg.1m 12:05:00的行號,然後按順序掃描就可以了。

但是以下兩種情況就麻煩了。

  • 獲取12:05 到 12:10 所有 Proc.load_avg.* 的數據,如果預先知道所有的metric name包括Proc.load_avg.1m,Proc.load_avg.5m,Proc.load_avg.15m。這樣會導致很多的隨機讀。如果不預先知道所有的metric name,就無法知道Proc.load_avg.*代表了什麼。
  • 獲取指定ip的數據。因為ip是做為tags保存的。即便是訪問一個ip的數據,也要把所有其他的ip數據讀取出來再過濾掉。如果ip總數有十多萬個,那麼查詢的效率也會非常低。為了讓這樣的查詢變得更快,需要把ip編碼到metric_name裡去。比如ip.10.0.0.1.Proc.load_avg.1m 這樣。

所以結論是,不用索引是不行的。如果希望支持任意條件的組合查詢,只有主存儲的排序是無法對所有查詢條件進行優化的。但是如果查詢條件是固定的一種,那麼可以像Opentsdb這樣只有一個主存儲,做針對性的優化。

DocValues為什麼快?

DocValues是一種按列組織的存儲格式,這種存儲方式降低了隨機讀的成本。傳統的按行存儲是這樣的:

聽說過時序數據庫嗎?

1和2代表的是docid。顏色代表的是不同的字段。

改成按列存儲是這樣的:

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

聽說過時序數據庫嗎?

合併之後就變成了:

聽說過時序數據庫嗎?

可以看到,行變成了列了。每一列可以代表這一分鐘內一秒的數據。

Elasticsearch有一個功能可以實現類似的優化效果,那就是Nested Document。我們可以把一段時間的很多個數據點打包存儲到一個父文檔裡,變成其嵌套的子文檔。示例如下:

{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

這樣可以把數據點公共的維度字段上移到父文檔裡,而不用在每個子文檔裡重複存儲,從而減少索引的尺寸。

聽說過時序數據庫嗎?

在存儲的時候,無論父文檔還是子文檔,對於Lucene來說都是文檔,都會有文檔Id。但是對於嵌套文檔來說,可以保存起子文檔和父文檔的文檔id是連續的,而且父文檔總是最後一個。有這樣一個排序性作為保障,那麼有一個所有父文檔的posting list就可以跟蹤所有的父子關係。也可以很容易地在父子文檔id之間做轉換。把父子關係也理解為一個filter,那麼查詢時檢索的時候不過是又AND了另外一個filter而已。前面我們已經看到了Elasticsearch可以非常高效地處理多filter的情況,充分利用底層的索引。

使用了嵌套文檔之後,對於term的posting list只需要保存父文檔的doc id就可以了,可以比保存所有的數據點的doc id要少很多。如果我們可以在一個父文檔裡塞入50個嵌套文檔,那麼posting list可以變成之前的1/50。

如何利用索引和主存儲,是一種兩難的選擇。

  • 選擇不使用索引,只使用主存儲:除非查詢的字段就是主存儲的排序字段,否則就需要順序掃描整個主存儲。
  • 選擇使用索引,然後用找到的row id去主存儲加載數據:這樣會導致很多碎片化的隨機讀操作。

沒有所謂完美的解決方案。MySQL支持索引,一般索引檢索出來的行數也就是在1~100條之間。如果索引檢索出來很多行,很有可能MySQL會選擇不使用索引而直接掃描主存儲,這就是因為用row id去主存儲裡讀取行的內容是碎片化的隨機讀操作,這在普通磁盤上很慢。

Opentsdb是另外一個極端,它完全沒有索引,只有主存儲。使用Opentsdb可以按照主存儲的排序順序快速地掃描很多條記錄。但是訪問的不是按主存儲的排序順序仍然要面對隨機讀的問題。

Elasticsearch/Lucene的解決辦法是讓主存儲的隨機讀操作變得很快,從而可以充分利用索引,而不用懼怕從主存儲裡隨機讀加載幾百萬行帶來的代價。

Opentsdb 的弱點

Opentsdb沒有索引,主存儲是Hbase。所有的數據點按照時間順序排列存儲在Hbase中。Hbase是一種支持排序的存儲引擎,其排序的方式是根據每個row的rowkey(就是關係數據庫裡的主鍵的概念)。MySQL存儲時間序列的最佳實踐是利用MySQL的Innodb的clustered index特性,使用它去模仿類似Hbase按rowkey排序的效果。所以Opentsdb的弱點也基本適用於MySQL。Opentsdb的rowkey的設計大致如下:

[metric_name][timestamp][tags]

舉例而言:

Proc.load_avg.1m 12:05:00 ip=10.0.0.1
Proc.load_avg.1m 12:05:00 ip=10.0.0.2
Proc.load_avg.1m 12:05:01 ip=10.0.0.1
Proc.load_avg.1m 12:05:01 ip=10.0.0.2
Proc.load_avg.5m 12:05:00 ip=10.0.0.1
Proc.load_avg:5m 12:05:00 ip=10.0.0.2

也就是行是先按照metric_name排序,再按照timestamp排序,再按照tags來排序。

對於這樣的rowkey設計,獲取一個metric在一個時間範圍內的所有數據是很快的,比如Proc.load_avg.1m在12:05到12:10之間的所有數據。先找到Proc.load_avg.1m 12:05:00的行號,然後按順序掃描就可以了。

但是以下兩種情況就麻煩了。

  • 獲取12:05 到 12:10 所有 Proc.load_avg.* 的數據,如果預先知道所有的metric name包括Proc.load_avg.1m,Proc.load_avg.5m,Proc.load_avg.15m。這樣會導致很多的隨機讀。如果不預先知道所有的metric name,就無法知道Proc.load_avg.*代表了什麼。
  • 獲取指定ip的數據。因為ip是做為tags保存的。即便是訪問一個ip的數據,也要把所有其他的ip數據讀取出來再過濾掉。如果ip總數有十多萬個,那麼查詢的效率也會非常低。為了讓這樣的查詢變得更快,需要把ip編碼到metric_name裡去。比如ip.10.0.0.1.Proc.load_avg.1m 這樣。

所以結論是,不用索引是不行的。如果希望支持任意條件的組合查詢,只有主存儲的排序是無法對所有查詢條件進行優化的。但是如果查詢條件是固定的一種,那麼可以像Opentsdb這樣只有一個主存儲,做針對性的優化。

DocValues為什麼快?

DocValues是一種按列組織的存儲格式,這種存儲方式降低了隨機讀的成本。傳統的按行存儲是這樣的:

聽說過時序數據庫嗎?

1和2代表的是docid。顏色代表的是不同的字段。

改成按列存儲是這樣的:

聽說過時序數據庫嗎?

按列存儲的話會把一個文件分成多個文件,每個列一個。對於每個文件,都是按照docid排序的。這樣一來,只要知道docid,就可以計算出這個docid在這個文件裡的偏移量。也就是對於每個docid需要一次隨機讀操作。

那麼這種排列是如何讓隨機讀更快的呢?祕密在於Lucene底層讀取文件的方式是基於memory mapped byte buffer的,也就是mmap。這種文件訪問的方式是由操作系統去緩存這個文件到內存裡。這樣在內存足夠的情況下,訪問文件就相當於訪問內存。那麼隨機讀操作也就不再是磁盤操作了,而是對內存的隨機讀。

那麼為什麼按行存儲不能用mmap的方式呢?因為按行存儲的方式一個文件裡包含了很多列的數據,這個文件尺寸往往很大,超過了操作系統的文件緩存的大小。而按列存儲的方式把不同列分成了很多文件,可以只緩存用到的那些列,而不讓很少使用的列數據浪費內存。

按列存儲之後,一個列的數據和前面的posting list就差不多了。很多應用在posting list上的壓縮技術也可以應用到DocValues上。這不但減少了文件尺寸,而且提高數據加載的速度。因為我們知道從磁盤到內存的帶寬是很小的,普通磁盤也就每秒100MB的讀速度。利用壓縮,我們可以把數據以壓縮的方式讀取出來,然後在內存裡再進行解壓,從而獲得比讀取原始數據更高的效率。

如果內存不夠是不是會使得隨機讀的速度變慢?肯定會的。但是mmap是操作系統實現的API,其內部有預讀取機制。如果讀取offset為100的文件位置,默認會把後面16k的文件內容都預讀取出來都緩存在內存裡。因為DocValues是隻讀,而且順序排序存儲的。相比b-tree等存儲結構,在磁盤上沒有空洞和碎片。而隨機讀的時候也是按照DocId排序的。所以如果讀取的DocId是緊密相連的,實際上也相當於把隨機讀變成了順序讀了。Random_read(100), Random_read(101), Random_read(102)就相當於Scan(100~102)了。

分佈式計算

分佈式聚合如何做得快?Elasticsearch/Lucene從最底層就支持數據分片,查詢的時候可以自動把不同分片的查詢結果合併起來。Elasticsearch的document都有一個uid,默認策略是按照uid 的 hash把文檔進行分片。

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

聽說過時序數據庫嗎?

合併之後就變成了:

聽說過時序數據庫嗎?

可以看到,行變成了列了。每一列可以代表這一分鐘內一秒的數據。

Elasticsearch有一個功能可以實現類似的優化效果,那就是Nested Document。我們可以把一段時間的很多個數據點打包存儲到一個父文檔裡,變成其嵌套的子文檔。示例如下:

{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

這樣可以把數據點公共的維度字段上移到父文檔裡,而不用在每個子文檔裡重複存儲,從而減少索引的尺寸。

聽說過時序數據庫嗎?

在存儲的時候,無論父文檔還是子文檔,對於Lucene來說都是文檔,都會有文檔Id。但是對於嵌套文檔來說,可以保存起子文檔和父文檔的文檔id是連續的,而且父文檔總是最後一個。有這樣一個排序性作為保障,那麼有一個所有父文檔的posting list就可以跟蹤所有的父子關係。也可以很容易地在父子文檔id之間做轉換。把父子關係也理解為一個filter,那麼查詢時檢索的時候不過是又AND了另外一個filter而已。前面我們已經看到了Elasticsearch可以非常高效地處理多filter的情況,充分利用底層的索引。

使用了嵌套文檔之後,對於term的posting list只需要保存父文檔的doc id就可以了,可以比保存所有的數據點的doc id要少很多。如果我們可以在一個父文檔裡塞入50個嵌套文檔,那麼posting list可以變成之前的1/50。

如何利用索引和主存儲,是一種兩難的選擇。

  • 選擇不使用索引,只使用主存儲:除非查詢的字段就是主存儲的排序字段,否則就需要順序掃描整個主存儲。
  • 選擇使用索引,然後用找到的row id去主存儲加載數據:這樣會導致很多碎片化的隨機讀操作。

沒有所謂完美的解決方案。MySQL支持索引,一般索引檢索出來的行數也就是在1~100條之間。如果索引檢索出來很多行,很有可能MySQL會選擇不使用索引而直接掃描主存儲,這就是因為用row id去主存儲裡讀取行的內容是碎片化的隨機讀操作,這在普通磁盤上很慢。

Opentsdb是另外一個極端,它完全沒有索引,只有主存儲。使用Opentsdb可以按照主存儲的排序順序快速地掃描很多條記錄。但是訪問的不是按主存儲的排序順序仍然要面對隨機讀的問題。

Elasticsearch/Lucene的解決辦法是讓主存儲的隨機讀操作變得很快,從而可以充分利用索引,而不用懼怕從主存儲裡隨機讀加載幾百萬行帶來的代價。

Opentsdb 的弱點

Opentsdb沒有索引,主存儲是Hbase。所有的數據點按照時間順序排列存儲在Hbase中。Hbase是一種支持排序的存儲引擎,其排序的方式是根據每個row的rowkey(就是關係數據庫裡的主鍵的概念)。MySQL存儲時間序列的最佳實踐是利用MySQL的Innodb的clustered index特性,使用它去模仿類似Hbase按rowkey排序的效果。所以Opentsdb的弱點也基本適用於MySQL。Opentsdb的rowkey的設計大致如下:

[metric_name][timestamp][tags]

舉例而言:

Proc.load_avg.1m 12:05:00 ip=10.0.0.1
Proc.load_avg.1m 12:05:00 ip=10.0.0.2
Proc.load_avg.1m 12:05:01 ip=10.0.0.1
Proc.load_avg.1m 12:05:01 ip=10.0.0.2
Proc.load_avg.5m 12:05:00 ip=10.0.0.1
Proc.load_avg:5m 12:05:00 ip=10.0.0.2

也就是行是先按照metric_name排序,再按照timestamp排序,再按照tags來排序。

對於這樣的rowkey設計,獲取一個metric在一個時間範圍內的所有數據是很快的,比如Proc.load_avg.1m在12:05到12:10之間的所有數據。先找到Proc.load_avg.1m 12:05:00的行號,然後按順序掃描就可以了。

但是以下兩種情況就麻煩了。

  • 獲取12:05 到 12:10 所有 Proc.load_avg.* 的數據,如果預先知道所有的metric name包括Proc.load_avg.1m,Proc.load_avg.5m,Proc.load_avg.15m。這樣會導致很多的隨機讀。如果不預先知道所有的metric name,就無法知道Proc.load_avg.*代表了什麼。
  • 獲取指定ip的數據。因為ip是做為tags保存的。即便是訪問一個ip的數據,也要把所有其他的ip數據讀取出來再過濾掉。如果ip總數有十多萬個,那麼查詢的效率也會非常低。為了讓這樣的查詢變得更快,需要把ip編碼到metric_name裡去。比如ip.10.0.0.1.Proc.load_avg.1m 這樣。

所以結論是,不用索引是不行的。如果希望支持任意條件的組合查詢,只有主存儲的排序是無法對所有查詢條件進行優化的。但是如果查詢條件是固定的一種,那麼可以像Opentsdb這樣只有一個主存儲,做針對性的優化。

DocValues為什麼快?

DocValues是一種按列組織的存儲格式,這種存儲方式降低了隨機讀的成本。傳統的按行存儲是這樣的:

聽說過時序數據庫嗎?

1和2代表的是docid。顏色代表的是不同的字段。

改成按列存儲是這樣的:

聽說過時序數據庫嗎?

按列存儲的話會把一個文件分成多個文件,每個列一個。對於每個文件,都是按照docid排序的。這樣一來,只要知道docid,就可以計算出這個docid在這個文件裡的偏移量。也就是對於每個docid需要一次隨機讀操作。

那麼這種排列是如何讓隨機讀更快的呢?祕密在於Lucene底層讀取文件的方式是基於memory mapped byte buffer的,也就是mmap。這種文件訪問的方式是由操作系統去緩存這個文件到內存裡。這樣在內存足夠的情況下,訪問文件就相當於訪問內存。那麼隨機讀操作也就不再是磁盤操作了,而是對內存的隨機讀。

那麼為什麼按行存儲不能用mmap的方式呢?因為按行存儲的方式一個文件裡包含了很多列的數據,這個文件尺寸往往很大,超過了操作系統的文件緩存的大小。而按列存儲的方式把不同列分成了很多文件,可以只緩存用到的那些列,而不讓很少使用的列數據浪費內存。

按列存儲之後,一個列的數據和前面的posting list就差不多了。很多應用在posting list上的壓縮技術也可以應用到DocValues上。這不但減少了文件尺寸,而且提高數據加載的速度。因為我們知道從磁盤到內存的帶寬是很小的,普通磁盤也就每秒100MB的讀速度。利用壓縮,我們可以把數據以壓縮的方式讀取出來,然後在內存裡再進行解壓,從而獲得比讀取原始數據更高的效率。

如果內存不夠是不是會使得隨機讀的速度變慢?肯定會的。但是mmap是操作系統實現的API,其內部有預讀取機制。如果讀取offset為100的文件位置,默認會把後面16k的文件內容都預讀取出來都緩存在內存裡。因為DocValues是隻讀,而且順序排序存儲的。相比b-tree等存儲結構,在磁盤上沒有空洞和碎片。而隨機讀的時候也是按照DocId排序的。所以如果讀取的DocId是緊密相連的,實際上也相當於把隨機讀變成了順序讀了。Random_read(100), Random_read(101), Random_read(102)就相當於Scan(100~102)了。

分佈式計算

分佈式聚合如何做得快?Elasticsearch/Lucene從最底層就支持數據分片,查詢的時候可以自動把不同分片的查詢結果合併起來。Elasticsearch的document都有一個uid,默認策略是按照uid 的 hash把文檔進行分片。

聽說過時序數據庫嗎?

一個Elasticsearch Index相當於一個MySQL裡的表,不同Index的數據是物理上隔離開來的。Elasticsearch的Index會分成多個Shard存儲,一部分Shard是Replica備份。一個Shard是一份本地的存儲(一個本地磁盤上的目錄),也就是一個Lucene的Index。不同的Shard可能會被分配到不同的主機節點上。一個Lucene Index會存儲很多的doc,為了好管理,Lucene把Index再拆成了Segment存儲(子目錄)。Segment內的doc數量上限是1的31次方,這樣doc id就只需要一個int就可以存儲。Segment對應了一些列文件存儲索引(倒排表等)和主存儲(DocValues等),這些文件內部又分為小的Block進行壓縮。

"

時序數據庫

即時間序列數據庫,相信大家都知道mysql之列的關係型數據庫,也瞭解redis等nosql數據庫,可是時間序列數據庫,你瞭解多少呢?

這類時間序列數據庫最多,使用也最廣泛。一般人們談論時間序列數據庫的時候指代的就是這一類存儲。按照底層技術不同可以劃分為三類。

直接基於文件的簡單存儲:RRD Tool,Graphite Whisper。這類工具附屬於監控告警工具,底層沒有一個正規的數據庫引擎。只是簡單的有一個二進制的文件結構。

基於K/V數據庫構建:opentsdb(基於hbase),blueflood,kairosDB(基於cassandra),influxdb,prometheus(基於leveldb)

基於關係型數據庫構建:mysql,postgresql 都可以用來保存時間序列數據

另外一類數據庫其表結構是:

[timestamp] [d1] [d2] .. [dn] [v1] [v2] .. [vn]

其優化的查詢方式不限於查詢原始數據,而是可以組合查詢條件並且做聚合計算,比如:

SELECT d2, sum(v1) / sum(v2) FROM metric WHERE d1 =
“A” AND timestamp >= B AND timestamp < C GROUP BY d2
聽說過時序數據庫嗎?

我們希望時間序列數據庫不僅僅可以提供原始數據的查詢,而且要支持對原始數據的聚合能力。這種聚合可以是在入庫階段完成的,所謂物化視圖。也可以是在查詢階段完成,所謂實時聚合。根據實際情況,可以在這兩種方式中進行取捨。

想要在在查詢階段做數據的聚合和轉換,需要能夠支持以下三點。

  • 用索引檢索出行號:能夠從上億條數據中快速過濾出幾百萬的數據。
  • 從主存儲按行號加載:能夠快速加載這過濾出的幾百萬條數據到內存裡。
  • 分佈式計算:能夠把這些數據按照GROUP BY 和 SELECT 的要求計算出最終的結果集。
聽說過時序數據庫嗎?

要想盡可能快的完成整個查詢過程,需要在三個環節上都有絕招。傳統上說,這三個步驟是三個不同的技術領域。

  • 檢索:這是搜索引擎最擅長的領域。代表產品是Lucene。其核心技術是基於高效率數據結構和算法的倒排索引。
  • 加載:這是分析型數據庫最擅長的領域。代表產品是C-store和Monetdb。其核心技術是按列組織的磁盤存儲結構。
  • 分佈式計算:這是大數據計算引擎最擅長的領域。代表產品是Hadoop和spark。其核心技術是sharding 和 map/reduce等等。

前面提到的時間序列庫(比如opentsdb)有不少從功能上來說是沒有問題。它們都支持過濾,也支持過濾之後的聚合計算。在數據量小的時候勉強是可用的。但是如果要實時從十億條裡取百萬記錄出來,再做聚合運算,對於這樣的數據量可能就勉為其難了。滿足海量數據實時聚合要求的數據庫不多,比較常見的有這麼幾種:

  • 基於Lucene構建的“搜索引擎”:Elasticsearch, Crate.io(雖然是基於Elasticsearch,但是聚合邏輯是自己實現的),Solr;
  • 列式存儲數據庫:Vertica(C-store的後裔)Actian(Monetdb的後裔)等;
  • Druid.io。

其中Elasticsearch是目前市場上比較很少有的,能夠在檢索加載和分佈式計算三個方面都做得一流的數據庫。而且是開源並且免費的。它使用了很多技術來達到飛一般的速度。這些主要的優化措施可以列舉如下。

  • Lucene的inverted index可以比mysql的b-tree檢索更快。
  • 在 Mysql中給兩個字段獨立建立的索引無法聯合起來使用,必須對聯合查詢的場景建立複合索引。而lucene可以任何AND或者OR組合使用索引進行檢索。
  • Elasticsearch支持nested document,可以把一批數據點嵌套存儲為一個document block,減少需要索引的文檔數。
  • Opentsdb不支持二級索引,只有一個基於hbase rowkey的主索引,可以按行的排序順序scan。這使得Opentsdb的tag實現從檢索效率上來說很慢。
  • Mysql 如果經過索引過濾之後仍然要加載很多行的話,出於效率考慮query planner經常會選擇進行全表掃描。所以Mysql的存儲時間序列的最佳實踐是不使用二級索引,只使用clustered index掃描主表。類似於Opentsdb。
  • Lucene 從 4.0 開始支持 DocValues,極大降低了內存的佔用,減少了磁盤上的尺寸並且提高了加載數據到內存計算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分開的數據當成一張表來查詢和聚合。相比之下Mysql如果自己做分庫分表的時候,聯合查詢不方便。
  • Elasticsearch 從1.0開始支持aggregation,基本上有了普通SQL的聚合能力。從 2.0 開始支持 pipeline aggregation,可以支持類似SQL sub query的嵌套聚合的能力。這種聚合能力相比Crate.io,Solr等同門師兄弟要強大得多。

如何快速檢索?

Elasticsearch是通過Lucene的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好,比如年齡在18和30之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型數據庫的b-tree索引快在哪裡?到底為什麼快呢?

籠統的來說,b-tree索引是為寫入優化的索引結構。當我們不需要支持快速的更新的時候,可以用預先排序等方式換取更小的存儲空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下Lucene的倒排索引是怎麼構成的。

聽說過時序數據庫嗎?

這裡有好幾個概念。我們來看一個實際的例子,假設有如下的數據:

聽說過時序數據庫嗎?

這裡每一行是一個document。每個document都有一個docid。那麼給這些document建立的倒排索引就是:

年齡:

聽說過時序數據庫嗎?

性別:

聽說過時序數據庫嗎?

可以看到,倒排索引是per field的,一個字段由一個自己的倒排索引。18,20這些叫做 term,而[1,3]就是posting list。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。那麼什麼是term dictionary 和 term index?

假設我們有很多個term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的term一定很慢,因為term沒有排序,需要全部過濾一遍才能找出特定的term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的term。這個就是 term dictionary。有了term dictionary之後,可以用 logN 次磁盤查找得到目標。但是磁盤的隨機讀操作仍然是非常昂貴的(一次random access大概需要10ms的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裡。但是整個term dictionary本身又太大了,無法完整地放到內存裡。於是就有了term index。term index有點像一本字典的大的章節表。比如:

A開頭的term ……………. Xxx頁

C開頭的term ……………. Xxx頁

E開頭的term ……………. Xxx頁

如果所有的term都是英文字符的話,可能這個term index就真的是26個英文字符表構成的了。但是實際的情況是,term未必都是英文字符,term可以是任意的byte數組。而且26個英文字符也未必是每一個字符都有均等的term,比如x字符開頭的term可能一個都沒有,而s開頭的term又特別多。實際的term index是一棵trie 樹:

聽說過時序數據庫嗎?

例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的term,它包含的是term的一些前綴。通過term index可以快速地定位到term dictionary的某個offset,然後從這個位置再往後順序查找。再加上一些壓縮技術(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的幾十分之一,使得用內存緩存整個term index變成可能。整體上來說就是這樣的效果。

聽說過時序數據庫嗎?

現在我們可以回答“為什麼Elasticsearch/Lucene檢索可以比mysql快了。Mysql只有term dictionary這一層,是以b-tree排序的方式存儲在磁盤上的。檢索一個term需要若干次的random access的磁盤操作。而Lucene在term dictionary的基礎上添加了term index來加速檢索,term index以樹的形式緩存在內存中。從term index查到對應的term dictionary的block位置之後,再去磁盤上找term,大大減少了磁盤的random access次數。

額外值得一提的兩點是:term index在內存中是以FST(finite state transducers)的形式保存的,其特點是非常節省內存。Term dictionary在磁盤上是以分block的方式保存的,一個block內部利用公共前綴壓縮,比如都是Ab開頭的單詞就可以把Ab省去。這樣term dictionary可以比b-tree更節約磁盤空間。

如何聯合索引查詢

所以給定查詢過濾條件 age=18 的過程就是先從term index找到18在term dictionary的大概位置,然後再從term dictionary裡精確地找到18這個term,然後得到一個posting list或者一個指向posting list位置的指針。然後再查詢 gender=女 的過程也是類似的。最後得出 age=18 AND gender=女 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於mysql來說,如果你給age和gender兩個字段都建立了索引,查詢的時候只會選擇其中最selective的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用skip list數據結構。同時遍歷gender和age的posting list,互相skip;
  • 使用bitset數據結構,對gender和age兩個filter分別求出bitset,對兩個bitset做AN操作。

PostgreSQL 從 8.4 版本開始支持通過bitmap聯合使用兩個索引,就是利用了bitset數據結構來做到的。當然一些商業的關係型數據庫也支持類似的聯合索引的功能。Elasticsearch支持以上兩種的聯合索引方式,如果查詢的filter緩存到了內存中(以bitset的形式),那麼合併就是兩個bitset的AND。如果查詢的filter沒有緩存,那麼就用skip list的方式去遍歷兩個on disk的posting list。

利用 Skip List 合併:

聽說過時序數據庫嗎?

以上是三個posting list。我們現在需要把它們用AND的關係合併,得出posting list的交集。首先選擇最短的posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的13的時候,就可以跳過藍色的3了,因為3比13要小。

整個過程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最後得出的交集是[13,98],所需的時間比完整遍歷三個posting list要快得多。但是前提是每個list需要指出Advance這個操作,快速移動指向的位置。什麼樣的list可以這樣Advance往前做蛙跳?skip list:

聽說過時序數據庫嗎?

從概念上來說,對於一個很長的posting list,比如:

[1,3,13,101,105,108,255,256,257]

我們可以把這個list分成三個block:

[1,3,13] [101,105,108] [255,256,257]

然後可以構建出skip list的第二層:

[1,101,255]

1,101,255分別指向自己對應的block。這樣就可以很快地跨block的移動指向位置了。

Lucene自然會對這個block再次進行壓縮。其壓縮方式叫做Frame Of Reference編碼。示例如下:

聽說過時序數據庫嗎?

考慮到頻繁出現的term(所謂low cardinality的值),比如gender裡的男或者女。如果有1百萬個文檔,那麼性別為男的posting list裡就會有50萬個int值。用Frame of Reference編碼進行壓縮可以極大減少磁盤佔用。這個優化對於減少索引尺寸有非常重要的意義。當然mysql b-tree裡也有一個類似的posting list的東西,是未經過這樣壓縮的。

因為這個Frame of Reference的編碼是有解壓縮成本的。利用skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

利用bitset合併

Bitset是一種很直觀的數據結構,對應posting list如:

[1,3,4,7,10]

對應的bitset就是:

[1,0,1,1,0,0,1,0,0,1]

每個文檔按照文檔id排序對應其中的一個bit。Bitset自身就有壓縮的特點,其用一個byte就可以代表8個文檔。所以100萬個文檔只需要12.5萬個byte。但是考慮到文檔可能有數十億之多,在內存裡保存bitset仍然是很奢侈的事情。而且對於個每一個filter都要消耗一個bitset,比如age=18緩存起來的話是一個bitset,18<=age<25是另外一個filter緩存起來也要一個bitset。

所以祕訣就在於需要有一個數據結構:

  • 可以很壓縮地保存上億個bit代表對應的文檔是否匹配filter;
  • 這個壓縮的bitset仍然可以很快地進行AND和 OR的邏輯操作。

Lucene使用的這個數據結構叫做 Roaring Bitmap。

聽說過時序數據庫嗎?

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,然後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch對其性能有詳細的對比(https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps)。簡單的結論是:因為Frame of Reference編碼是如此高效,對於簡單的相等條件的過濾緩存成純內存的bitset還不如需要訪問磁盤的skip list的方式要快。

如何減少文檔數?

一種常見的壓縮存儲時間序列的方式是把多個數據點合併成一行。Opentsdb支持海量數據的一個絕招就是定期把很多行數據合併成一行,這個過程叫compaction。類似的vivdcortext使用mysql存儲的時候,也把一分鐘的很多數據點合併存儲到mysql的一行裡以減少行數。

這個過程可以示例如下:

聽說過時序數據庫嗎?

合併之後就變成了:

聽說過時序數據庫嗎?

可以看到,行變成了列了。每一列可以代表這一分鐘內一秒的數據。

Elasticsearch有一個功能可以實現類似的優化效果,那就是Nested Document。我們可以把一段時間的很多個數據點打包存儲到一個父文檔裡,變成其嵌套的子文檔。示例如下:

{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

這樣可以把數據點公共的維度字段上移到父文檔裡,而不用在每個子文檔裡重複存儲,從而減少索引的尺寸。

聽說過時序數據庫嗎?

在存儲的時候,無論父文檔還是子文檔,對於Lucene來說都是文檔,都會有文檔Id。但是對於嵌套文檔來說,可以保存起子文檔和父文檔的文檔id是連續的,而且父文檔總是最後一個。有這樣一個排序性作為保障,那麼有一個所有父文檔的posting list就可以跟蹤所有的父子關係。也可以很容易地在父子文檔id之間做轉換。把父子關係也理解為一個filter,那麼查詢時檢索的時候不過是又AND了另外一個filter而已。前面我們已經看到了Elasticsearch可以非常高效地處理多filter的情況,充分利用底層的索引。

使用了嵌套文檔之後,對於term的posting list只需要保存父文檔的doc id就可以了,可以比保存所有的數據點的doc id要少很多。如果我們可以在一個父文檔裡塞入50個嵌套文檔,那麼posting list可以變成之前的1/50。

如何利用索引和主存儲,是一種兩難的選擇。

  • 選擇不使用索引,只使用主存儲:除非查詢的字段就是主存儲的排序字段,否則就需要順序掃描整個主存儲。
  • 選擇使用索引,然後用找到的row id去主存儲加載數據:這樣會導致很多碎片化的隨機讀操作。

沒有所謂完美的解決方案。MySQL支持索引,一般索引檢索出來的行數也就是在1~100條之間。如果索引檢索出來很多行,很有可能MySQL會選擇不使用索引而直接掃描主存儲,這就是因為用row id去主存儲裡讀取行的內容是碎片化的隨機讀操作,這在普通磁盤上很慢。

Opentsdb是另外一個極端,它完全沒有索引,只有主存儲。使用Opentsdb可以按照主存儲的排序順序快速地掃描很多條記錄。但是訪問的不是按主存儲的排序順序仍然要面對隨機讀的問題。

Elasticsearch/Lucene的解決辦法是讓主存儲的隨機讀操作變得很快,從而可以充分利用索引,而不用懼怕從主存儲裡隨機讀加載幾百萬行帶來的代價。

Opentsdb 的弱點

Opentsdb沒有索引,主存儲是Hbase。所有的數據點按照時間順序排列存儲在Hbase中。Hbase是一種支持排序的存儲引擎,其排序的方式是根據每個row的rowkey(就是關係數據庫裡的主鍵的概念)。MySQL存儲時間序列的最佳實踐是利用MySQL的Innodb的clustered index特性,使用它去模仿類似Hbase按rowkey排序的效果。所以Opentsdb的弱點也基本適用於MySQL。Opentsdb的rowkey的設計大致如下:

[metric_name][timestamp][tags]

舉例而言:

Proc.load_avg.1m 12:05:00 ip=10.0.0.1
Proc.load_avg.1m 12:05:00 ip=10.0.0.2
Proc.load_avg.1m 12:05:01 ip=10.0.0.1
Proc.load_avg.1m 12:05:01 ip=10.0.0.2
Proc.load_avg.5m 12:05:00 ip=10.0.0.1
Proc.load_avg:5m 12:05:00 ip=10.0.0.2

也就是行是先按照metric_name排序,再按照timestamp排序,再按照tags來排序。

對於這樣的rowkey設計,獲取一個metric在一個時間範圍內的所有數據是很快的,比如Proc.load_avg.1m在12:05到12:10之間的所有數據。先找到Proc.load_avg.1m 12:05:00的行號,然後按順序掃描就可以了。

但是以下兩種情況就麻煩了。

  • 獲取12:05 到 12:10 所有 Proc.load_avg.* 的數據,如果預先知道所有的metric name包括Proc.load_avg.1m,Proc.load_avg.5m,Proc.load_avg.15m。這樣會導致很多的隨機讀。如果不預先知道所有的metric name,就無法知道Proc.load_avg.*代表了什麼。
  • 獲取指定ip的數據。因為ip是做為tags保存的。即便是訪問一個ip的數據,也要把所有其他的ip數據讀取出來再過濾掉。如果ip總數有十多萬個,那麼查詢的效率也會非常低。為了讓這樣的查詢變得更快,需要把ip編碼到metric_name裡去。比如ip.10.0.0.1.Proc.load_avg.1m 這樣。

所以結論是,不用索引是不行的。如果希望支持任意條件的組合查詢,只有主存儲的排序是無法對所有查詢條件進行優化的。但是如果查詢條件是固定的一種,那麼可以像Opentsdb這樣只有一個主存儲,做針對性的優化。

DocValues為什麼快?

DocValues是一種按列組織的存儲格式,這種存儲方式降低了隨機讀的成本。傳統的按行存儲是這樣的:

聽說過時序數據庫嗎?

1和2代表的是docid。顏色代表的是不同的字段。

改成按列存儲是這樣的:

聽說過時序數據庫嗎?

按列存儲的話會把一個文件分成多個文件,每個列一個。對於每個文件,都是按照docid排序的。這樣一來,只要知道docid,就可以計算出這個docid在這個文件裡的偏移量。也就是對於每個docid需要一次隨機讀操作。

那麼這種排列是如何讓隨機讀更快的呢?祕密在於Lucene底層讀取文件的方式是基於memory mapped byte buffer的,也就是mmap。這種文件訪問的方式是由操作系統去緩存這個文件到內存裡。這樣在內存足夠的情況下,訪問文件就相當於訪問內存。那麼隨機讀操作也就不再是磁盤操作了,而是對內存的隨機讀。

那麼為什麼按行存儲不能用mmap的方式呢?因為按行存儲的方式一個文件裡包含了很多列的數據,這個文件尺寸往往很大,超過了操作系統的文件緩存的大小。而按列存儲的方式把不同列分成了很多文件,可以只緩存用到的那些列,而不讓很少使用的列數據浪費內存。

按列存儲之後,一個列的數據和前面的posting list就差不多了。很多應用在posting list上的壓縮技術也可以應用到DocValues上。這不但減少了文件尺寸,而且提高數據加載的速度。因為我們知道從磁盤到內存的帶寬是很小的,普通磁盤也就每秒100MB的讀速度。利用壓縮,我們可以把數據以壓縮的方式讀取出來,然後在內存裡再進行解壓,從而獲得比讀取原始數據更高的效率。

如果內存不夠是不是會使得隨機讀的速度變慢?肯定會的。但是mmap是操作系統實現的API,其內部有預讀取機制。如果讀取offset為100的文件位置,默認會把後面16k的文件內容都預讀取出來都緩存在內存裡。因為DocValues是隻讀,而且順序排序存儲的。相比b-tree等存儲結構,在磁盤上沒有空洞和碎片。而隨機讀的時候也是按照DocId排序的。所以如果讀取的DocId是緊密相連的,實際上也相當於把隨機讀變成了順序讀了。Random_read(100), Random_read(101), Random_read(102)就相當於Scan(100~102)了。

分佈式計算

分佈式聚合如何做得快?Elasticsearch/Lucene從最底層就支持數據分片,查詢的時候可以自動把不同分片的查詢結果合併起來。Elasticsearch的document都有一個uid,默認策略是按照uid 的 hash把文檔進行分片。

聽說過時序數據庫嗎?

一個Elasticsearch Index相當於一個MySQL裡的表,不同Index的數據是物理上隔離開來的。Elasticsearch的Index會分成多個Shard存儲,一部分Shard是Replica備份。一個Shard是一份本地的存儲(一個本地磁盤上的目錄),也就是一個Lucene的Index。不同的Shard可能會被分配到不同的主機節點上。一個Lucene Index會存儲很多的doc,為了好管理,Lucene把Index再拆成了Segment存儲(子目錄)。Segment內的doc數量上限是1的31次方,這樣doc id就只需要一個int就可以存儲。Segment對應了一些列文件存儲索引(倒排表等)和主存儲(DocValues等),這些文件內部又分為小的Block進行壓縮。

聽說過時序數據庫嗎?

時間序列數據一般按照日期分成多個Elasticsearch Index來存儲,比如logstash-2014.08.02。查詢的時候可以指定多個Elasticsearch Index作為查找的範圍,也可以用logstash-*做模糊匹配。

美妙之處在於,雖然數據被拆得七零八落的,在查詢聚合的時候甚至需要分為兩個階段完成。但是對於最終用戶來說,使用起來就好像是一個數據庫表一樣。所有的合併查詢的細節都是隱藏起來的。

對於聚合查詢,其處理是分兩階段完成的:

  • Shard本地的Lucene Index並行計算出局部的聚合結果;
  • 收到所有的Shard的局部聚合結果,聚合出最終的聚合結果。

這種兩階段聚合的架構使得每個shard不用把原數據返回,而只用返回數據量小得多的聚合結果。相比Opentsdb這樣的數據庫設計更合理。Opentsdb其聚合只在最終節點處完成,所有的分片數據要匯聚到一個地方進行計算,這樣帶來大量的網絡帶寬消耗。所以Influxdb等更新的時間序列數據庫選擇把分佈式計算模塊和存儲引擎進行同機部署,以減少網絡帶寬的影響。

除此之外Elasticsearch還有另外一個減少聚合過程中網絡傳輸量的優化,那就是Hyperloglog算法。在計算unique visitor(uv)這樣的場景下,經常需要按用戶id去重之後統計人數。最簡單的實現是用一個hashset保存這些用戶id。但是用set保存所有的用戶id做去重需要消耗大量的內存,同時分佈式聚合的時候也要消耗大量的網絡帶寬。Hyperloglog算法以一定的誤差做為代價,可以用很小的數據量保存這個set,從而減少網絡傳輸消耗。

為什麼時間序列需要更復雜的聚合?

關係型數據庫支持一些很複雜的聚合查詢邏輯,比如:

  • Join兩張表;
  • Group by之後用Having再對聚合結果進行過濾;
  • 用子查詢對聚合結果進行二次聚合。

在使用時間序列數據庫的時候,我們經常會懷念這些SQL的查詢能力。在時間序列裡有一個特別常見的需求就是降頻和降維。舉例如下:

12:05:05 湖南 81
12:05:07 江西 30
12:05:11 湖南 80
12:05:12 江西 32
12:05:16 湖南 80
12:05:16 江西 30

按1分鐘頻率進行max的降頻操作得出的結果是:

12:05 湖南 81
12:05 江西 32

這種按max進行降頻的最常見的場景是採樣點的歸一化。不同的採集器採樣的時間點是不同的,為了避免漏點也會加大采樣率。這樣就可能導致一分鐘內採樣多次,而且採樣點的時間都不對齊。在查詢的時候按max進行降頻可以得出一個統一時間點的數據。

按sum進行降維的結果是:

12:05 113

經常我們需要捨棄掉某些維度進行一個加和的統計。這個統計需要在時間點對齊之後再進行計算。這就導致一個查詢需要做兩次,上面的例子裡:

  • 先按1分鐘,用max做降頻;
  • 再去掉省份維度,用sum做降維。

如果僅僅能做一次聚合,要麼用sum做聚合,要麼用max做聚合。無法滿足業務邏輯的需求。為了避免在一個查詢裡做兩次聚合,大部分的時間序列數據庫都要求數據在入庫的時候已經是整點整分的。這就要求數據不能直接從採集點直接入庫,而要經過一個實時計算管道進行處理。如果能夠在查詢的時候同時完成降頻和降維,那就可以帶來一些使用上的便利。

這個功能看似簡單,其實非常難以實現。很多所謂的支持大數據的數據庫都只支持簡單的一次聚合操作。Elasticsearch 將要發佈的 2.0 版本的最重量級的新特性是Pipeline Aggregation,它支持數據在聚合之後再做聚合。類似SQL的子查詢和Having等功能都將被支持。

"

相關推薦

推薦中...