ElasticSearch 2 (35) - 信息聚合系列之近似聚合


ElasticSearch 2 (35) - 信息聚合系列之近似聚合

摘要

如果所有的數據都在一台機器上,那么生活會容易許多,CS201 課商教的經典算法就足夠應付這些問題。但如果所有的數據都在一台機器上,那么就不需要像 Elasticsearch 這樣的分布式軟件了。不過一旦我們開始分布式數據存儲,算法的選擇就需務必小心。

版本

elasticsearch版本: elasticsearch-2.x

內容

如果所有的數據都在一台機器上,那么生活會容易許多,CS201 課商教的經典算法就足夠應付這些問題。但如果所有的數據都在一台機器上,那么就不需要像 Elasticsearch 這樣的分布式軟件了。不過一旦我們開始分布式數據存儲,算法的選擇就需務必小心。

有些算法可以分布執行,到目前為止討論過的所有聚合都是單次請求獲得精確結果的。這些類型的算法通常是高度並行的,因為它們無須任何額外代價,就能在多台機器上並行執行。比如當計算 max 度量時,以下的算法就非常簡單:

  1. 請求廣播到所有分片。
  2. 查看每個文檔的 price 字段。如果 price > current_max,將 current_max 替換成 price
  3. 返回所有分片的最大 price 並傳給協調節點。
  4. 找到從所有分片返回的最大 price 。這是最終的最大值。

這個算法可以隨着機器數的線性增長而橫向擴展,無須任何協調操作(機器之間不需要討論中間結果),而且內存消耗很小(一個整型就能代表最大值)。

不幸的是,不是所有的算法都像獲取最大值這樣簡單。更加復雜的操作則需要在算法的性能和內存使用上做出權衡。對於這個問題,我們有個三角因子模型:大數據、精確性和實時性。

我們需要選擇其中兩項:

  • 精確 + 實時

    數據可以存入單台機器的內存之中,我們可以隨心所欲,使用任何想用的算法。結果會 100% 精確,響應會相對快速。

  • 大數據 + 精確

    傳統的 Hadoop。可以處理 PB 級的數據並且為我們提供精確的答案,但它可能需要幾周的時間才能為我們提供這個答案。

  • 大數據 + 實時

    近似算法為我們提供准確但不精確的結果。

Elasticsearch 目前支持兩種近似算法(cardinality(基數)percentiles(百分位數))。它們會提供准確但不是 100% 精確的結果。以犧牲一點小小的估算錯誤為代價,這些算法可以為我們換來高速的執行效率和極小的內存消耗。

對於大多數應用領域,能夠實時返回高度准確的結果要比 100% 精確結果重要得多。乍一看這可能是天方夜譚。有人會叫“我們需要精確的答案!”。但仔細考慮 0.5% 錯誤所帶來的影響:

  • 99% 的網站延時都在 132ms 以下。
  • 0.5% 的誤差對以上延時的影響在正負 0.66ms 。
  • 近似計算會在毫秒內放回結果,而“完全正確”的結果就可能需要幾秒,甚至無法返回。

只要簡單的查看網站的延時情況,難道我們會在意近似結果是在 132.66ms 內返回而不是 132ms?當然,不是所有的領域都能容忍這種近似結果,但對於絕大多數來說是沒有問題的。接受近似結果更多的是一種文化觀念上的壁壘而不是商業或技術上的需要。

查找唯一值的數目(Finding Distinct Counts)

Elasticsearch 提供的首個近似聚合是基數度量。它提供一個字段的基數,即該字段的唯一值的數目。可能會對 SQL 形式比較熟悉:

  SELECT DISTINCT(color)
  FROM cars

Distinct 計數是一個普通的操作,可以回答很多基本的商業問題:

  • 網站的獨立訪問用戶(UVs)是多少?
  • 賣了多少種汽車?
  • 每月有多少獨立用戶購買了商品?

我們可以用基數度量確定經銷商銷售汽車顏色的種類:

  GET /cars/transactions/_search
  {
      "size" : 0,
      "aggs" : {
          "distinct_colors" : {
              "cardinality" : {
                "field" : "color"
              }
          }
      }
  }

返回的結果表明已經售賣了三種不同顏色的汽車:

  ...
  "aggregations": {
    "distinct_colors": {
       "value": 3
    }
  }
  ...

可以讓我們的例子變得更有用:每月有多少顏色的車被售出?為了得到這個度量,我們只需要將一個 cardinality 度量嵌入一個 date_histogram

  GET /cars/transactions/_search
  {
    "size" : 0,
    "aggs" : {
        "months" : {
          "date_histogram": {
            "field": "sold",
            "interval": "month"
          },
          "aggs": {
            "distinct_colors" : {
                "cardinality" : {
                  "field" : "color"
                }
            }
          }
        }
    }
  }

學會權衡(Understanding the Trade-offs)

正如我們本章開頭提到的,cardinality 度量是一個近似算法。它是基於 HyperLogLog++(HLL)算法的,HLL 會先對我們的輸入作哈希運算,然后根據哈希運算的結果中的 bits 做概率估算從而得到基數。

我們不需要理解技術細節(如果確實感興趣,可以閱讀這篇論文),但我們最好應該關注一下這個算法的特性:

  • 可配置的精度,用來控制內存的使用(更精確 = 更多內存)。
  • 對於低基數集能夠達到高准確度。
  • 固定的內存使用。即使有幾千或甚至上百億的唯一值,內存的使用也只是依賴於配置里的精度要求。

要配置精度,我們必須指定 precision_threshold 參數的值。這個閥值定義了在何種基數水平下我們希望得到一個近乎精確的結果。參考以下示例:

  GET /cars/transactions/_search
  {
      "size" : 0,
      "aggs" : {
          "distinct_colors" : {
              "cardinality" : {
                "field" : "color",
                "precision_threshold" : 100 #1
              }
          }
      }
  }

#1 precision_threshold 接受 0–40,000 之間的數字,更大的值還是會被當作 40,000 來處理。

示例會確保當字段唯一值在 100 以內時會得到非常准確的結果。盡管算法是無法保證這點的,但如果基數在閥值以下,幾乎總是 100% 正確的。高於閥值的基數會開始節省內存而犧牲准確度,同時也會對度量結果帶入誤差。

對於指定的閥值,HLL 的數據結構會大概使用內存 precision_threshold * 8 字節,所以就必須在犧牲內存和獲得額外的准確度間做平衡。

在實際應用中,100 的閥值可以在唯一值為百萬的情況下仍然將誤差維持 5% 以內。

速度優化(Optimizing for Speed)

如果想要獲得唯一數目的值,通常需要查詢整個數據集合(或幾乎所有數據)。所有基於所有數據的操作都必須迅速,原因是顯然的。HyperLogLog 的速度已經很快了,它只是簡單的對數據做哈希以及一些位操作。

但如果速度對我們至關重要,可以做進一步的優化,因為 HLL 只需要字段內容的哈希值,我們可以在索引時就預先計算好。就能在查詢時跳過哈希計算然后將數據信息直接加載出來。

注意

預先計算哈希值只對內容很長或者基數很高的字段有用,計算這些字段的哈希值的消耗在查詢時是無法忽略的。

盡管數值字段的哈希計算是非常快速的,存儲它們的原始值通常需要同樣(或更少)的內存空間。這對低基數的字符串字段同樣適用,Elasticsearch 的內部優化能夠保證每個唯一值只計算一次哈希。

基本上說,預先計算並不能保證所有的字段都快,它只對那些具有高基數和內容很長的字符串字段有作用。需要記住的是預先計算也只是簡單地將代價轉到索引時,代價就在那里,不增不減。

To do this, we need to add a new multifield to our data. We’ll delete our index, add a new mapping that includes the hashed field, and then reindex:
要想這么做,我們需要為數據增加一個新的多值字段。我們先刪除索引,再增加一個包括哈希值字段的映射,然后重新索引:

  DELETE /cars/

  PUT /cars/
  {
    "mappings": {
      "transactions": {
        "properties": {
          "color": {
            "type": "string",
            "fields": {
              "hash": {
                "type": "murmur3" #1
              }
            }
          }
        }
      }
    }
  }

  POST /cars/transactions/_bulk
  { "index": {}}
  { "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" }
  { "index": {}}
  { "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
  { "index": {}}
  { "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" }
  { "index": {}}
  { "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" }
  { "index": {}}
  { "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" }
  { "index": {}}
  { "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
  { "index": {}}
  { "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" }
  { "index": {}}
  { "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" }

#1 多值字段的類型是 murmur3,這是一個哈希函數。

現在當我們執行聚合時,我們使用 color.hash 字段而不是 color 字段:

  GET /cars/transactions/_search
  {
      "size" : 0,
      "aggs" : {
          "distinct_colors" : {
              "cardinality" : {
                "field" : "color.hash" #1
              }
          }
      }
  }

#1 注意我們指定的是哈希過的多值字段,而不是原始字段。

現在 cardinality 度量會讀取 "color.hash" 里的值(預先計算的哈希值),並將它們作為原始值的動態哈希值。

每個文檔節省的時間有限,但如果哈希每個字段需要 10 納秒而我們的聚合需要訪問一億文檔,那么每個查詢就需要多花 1 秒鍾的時間。如果我們發現自己在很多文檔都會使用 cardinality 基數,可以做些性能分析看是否有必要在我們部署的應用中采用預先計算哈希的方式。

百分計算(Calculating Percentiles)

Elasticsearch 提供的另外一個近似度量就是 percentiles 百分位數度量。百分位數展現某以具體百分比下觀察到的數值。例如,第95個百分位上的數值,是高於 95% 的數據總和。

百分位數通常用來找出異常。在(統計學)的正態分布下,第 0.13 和 第 99.87 的百分位數代表與均值距離三倍標准差的值。任何處於三倍標准差之外的數據通常被認為是不尋常的,因為它與平均值相差太大。

更具體的說,假設我們正運行一個龐大的網站,而我們的任務是保證用戶請求能得到快速響應,因此我們就需要監控網站的延時來判斷響應是否能達到目標。

在此場景下,一個常用的度量方法就是平均響應延時,但這是一個不好的選擇(盡管很常用),因為平均數通常會隱藏那些異常值,中位數有着同樣的問題。我們可以嘗試最大值,但這個度量會輕而易舉的被單個異常值破壞。

在圖 Figure 40, “Average request latency over time” 查看問題。如果我們倚靠如平均值或中位數這樣的簡單度量,就會得到像這樣一幅圖 Figure 40, “Average request latency over time”.

Figure 40. Average request latency over time

一切正常。圖上有輕微的波動,但沒有什么值得關注的。但如果我們加載 99 百分位數時(這個值代表最慢的 1% 的延時),我們看到了完全不同的一幅畫面,如圖Figure 41, “Average request latency with 99th percentile over time” 所示。

Figure 41. Average request latency with 99th percentile over time

令人吃驚!在上午九點半時,均值只有 75ms。如果作為一個系統管理員,我們都不會看他第二眼。一切正常!但 99 百分位告訴我們有 1% 的用戶碰到的延時超過 850ms,這是另外一幅場景。在上午4點48時也有一個小波動,這甚至無法從平均值和中位數曲線上觀察到。

這只是百分位的一個應用場景,百分位還可以被用來快速用肉眼觀察數據的分布,檢查是否有數據傾斜或雙峰甚至更多。

百分位度量(Percentile Metric)

讓我加載一個新的數據集(汽車的數據不太適用於百分位)。我們要索引一系列網站延時數據然后運行一些百分位操作進行查看:

  POST /website/logs/_bulk
  { "index": {}}
  { "latency" : 100, "zone" : "US", "timestamp" : "2014-10-28" }
  { "index": {}}
  { "latency" : 80, "zone" : "US", "timestamp" : "2014-10-29" }
  { "index": {}}
  { "latency" : 99, "zone" : "US", "timestamp" : "2014-10-29" }
  { "index": {}}
  { "latency" : 102, "zone" : "US", "timestamp" : "2014-10-28" }
  { "index": {}}
  { "latency" : 75, "zone" : "US", "timestamp" : "2014-10-28" }
  { "index": {}}
  { "latency" : 82, "zone" : "US", "timestamp" : "2014-10-29" }
  { "index": {}}
  { "latency" : 100, "zone" : "EU", "timestamp" : "2014-10-28" }
  { "index": {}}
  { "latency" : 280, "zone" : "EU", "timestamp" : "2014-10-29" }
  { "index": {}}
  { "latency" : 155, "zone" : "EU", "timestamp" : "2014-10-29" }
  { "index": {}}
  { "latency" : 623, "zone" : "EU", "timestamp" : "2014-10-28" }
  { "index": {}}
  { "latency" : 380, "zone" : "EU", "timestamp" : "2014-10-28" }
  { "index": {}}
  { "latency" : 319, "zone" : "EU", "timestamp" : "2014-10-29" }

數據有三個值:延時、數據中心的區域以及時間戳。讓我們對數據全集執行百分位操作以獲得數據分布情況的直觀感受:

  GET /website/logs/_search
  {
      "size" : 0,
      "aggs" : {
          "load_times" : {
              "percentiles" : {
                  "field" : "latency" #1
              }
          },
          "avg_load_time" : {
              "avg" : {
                  "field" : "latency" #2
              }
          }
      }
  }

#1 percentiles 度量被應用到 latency 延時字段。

#2 為了比較,我們對相同字段使用 avg 度量。

默認情況下,percentiles 度量會返回一組預定義的百分位數值:[1, 5, 25, 50, 75, 95, 99]。它們表示了人們感興趣的常用百分位數值,極端的百分位數在范圍的兩邊,其他的一些處於中部。在返回的響應中,我們可以看到最小延時在 75ms 左右,而最大延時差不多有 600ms。與之形成對比的是,平均延時在 200ms 左右,信息並不是很多:

  ...
  "aggregations": {
    "load_times": {
       "values": {
          "1.0": 75.55,
          "5.0": 77.75,
          "25.0": 94.75,
          "50.0": 101,
          "75.0": 289.75,
          "95.0": 489.34999999999985,
          "99.0": 596.2700000000002
       }
    },
    "avg_load_time": {
       "value": 199.58333333333334
    }
  }

所以顯然延時的分布很廣,然我們看看它們是否與數據中心的地理區域有關:

  GET /website/logs/_search
  {
      "size" : 0,
      "aggs" : {
          "zones" : {
              "terms" : {
                  "field" : "zone" #1
              },
              "aggs" : {
                  "load_times" : {
                      "percentiles" : { #2
                        "field" : "latency",
                        "percents" : [50, 95.0, 99.0] #3
                      }
                  },
                  "load_avg" : {
                      "avg" : {
                          "field" : "latency"
                      }
                  }
              }
          }
      }
  }

#1 首先根據區域我們將延時分到不同的桶中。

#2 再計算每個區域的百分位數值。

#3 percents 參數接受了我們想返回的一組百分位數,因為我們只對長的延時感興趣。

在響應結果中,我們發現歐洲區域(EU)要比美國區域(US)慢很多,在美國區域(US),50 百分位與 99 百分位十分接近,它們都接近均值。

與之形成對比的是,歐洲區域(EU)在 50 和 99 百分位有較大區分。現在,顯然可以發現是歐洲區域(EU)拉低了延時的統計信息,我們知道歐洲區域的 50% 延時都在 300ms+。

  ...
  "aggregations": {
    "zones": {
       "buckets": [
          {
             "key": "eu",
             "doc_count": 6,
             "load_times": {
                "values": {
                   "50.0": 299.5,
                   "95.0": 562.25,
                   "99.0": 610.85
                }
             },
             "load_avg": {
                "value": 309.5
             }
          },
          {
             "key": "us",
             "doc_count": 6,
             "load_times": {
                "values": {
                   "50.0": 90.5,
                   "95.0": 101.5,
                   "99.0": 101.9
                }
             },
             "load_avg": {
                "value": 89.66666666666667
             }
          }
       ]
    }
  }
  ...

百分位等級(Percentile Ranks)

這里有另外一個緊密相關的度量叫 percentile_ranks。百分位度量告訴我們落在某個百分比以下的所有文檔的最小值。例如,如果 50 百分位是 119ms,那么有 50% 的文檔數值都不超過 119ms。percentile_ranks 告訴我們某個具體值屬於哪個百分位。119ms 的 percentile_ranks 是在 50 百分位。這基本是個雙向關系,例如:

  • 50 百分位是 119ms。
  • 119ms 百分位等級是 50 百分位。

所以假設我們網站必須維持的服務等級協議(SLA)是響應時間低於 210ms。然后,開個玩笑,我們老板警告我們如果響應時間超過 800ms 會把我開除。可以理解的是,我們希望知道有多少百分比的請求可以滿足 SLA 的要求(並期望至少在 800ms 以下!)。

為了做到這點,我們可以應用 percentile_ranks 度量而不是 percentiles 度量:

  GET /website/logs/_search
  {
      "size" : 0,
      "aggs" : {
          "zones" : {
              "terms" : {
                  "field" : "zone"
              },
              "aggs" : {
                  "load_times" : {
                      "percentile_ranks" : {
                        "field" : "latency",
                        "values" : [210, 800] #1
                      }
                  }
              }
          }
      }
  }

#1 percentile_ranks 度量接受一組我們希望分級的數值。

在聚合運行后,我們能得到兩個值:

  "aggregations": {
    "zones": {
       "buckets": [
          {
             "key": "eu",
             "doc_count": 6,
             "load_times": {
                "values": {
                   "210.0": 31.944444444444443,
                   "800.0": 100
                }
             }
          },
          {
             "key": "us",
             "doc_count": 6,
             "load_times": {
                "values": {
                   "210.0": 100,
                   "800.0": 100
                }
             }
          }
       ]
    }
  }

這告訴我們三點重要的信息:

  • 在歐洲(EU),210ms 的百分位等級是 31.94% 。
  • 在美國(US),210ms 的百分位等級是 100% 。
  • 在歐洲(EU)和美國(US),800ms 的百分位等級是 100% 。

通俗的說,在歐洲區域(EU)只有 32% 的響應時間滿足服務等級協議(SLA),而美國區域(US)始終滿足服務等級協議的。但幸運的是,兩個區域所有響應時間都在 800ms 以下,所有我們還不會被炒魷魚(至少目前不會)。

percentile_ranks 度量提供了與 percentiles 相同的信息,但它以不同方式呈現,如果我們對某個具體數值更關心,使用它會更方便。

學會權衡(Understanding the Trade-offs)

和基數一樣,計算百分位需要一個近似算法。朴素的實現會維護一個所有值的有序列表,但當我們有幾十億數據分布在幾十個節點時,這幾乎是不可能的。

取而代之的是 percentiles 使用一個 TDigest 算法(由 Ted Dunning 在 Computing Extremely Accurate Quantiles Using T-Digests 里面提出的)。有了 HyperLogLog,就不需要理解完整的技術細節,但有必要了解算法的特性:

  • 百分位的准確度與百分位的極端程度相關,也就是說 1 或 99 的百分位要比 50 百分位要准確。這只是數據結構內部機制的一種特性,但這是一個好的特性,因為多數人只關心極端的百分位。
  • 對於數值集合較小的情況,百分位非常准確。如果數據集足夠小,百分位可能 100% 精確。
  • 隨着桶里數值的增長,算法會開始對百分位進行估算。它能有效在准確度和內存節省之間做出權衡。不准確的程度比較難以總結,因為它依賴於聚合時數據的分布以及數據量的大小。

與基數類似,我們可以通過修改參數 compression 來控制內存與准確度之間的比值。

TDigest 算法用節點近似計算百分比:節點越多,准確度越高(同時內存消耗也越大),這都與數據量成正比。compression 參數限制節點的最大數目為 20 * compression

因此,通過增加壓縮比值,我們可以提高准確度同時也消耗更多內存。更大的壓縮比值會使算法運行更慢,因為底層的樹形數據結構的存儲也會增長,也導致操作的代價更高。默認的壓縮比值是 100

一個節點粗略計算使用 32 字節的內存,所以在最壞的情況下(例如,大量數據有序存入),默認設置會生成一個大小約為 64KB 的 TDigest。在實際應用中,數據會更隨機,所以 TDigest 使用的內存會更少。

參考

elastic.co:
Approximate Aggregations


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM