原作者/ BURAK YUCESOY
翻譯&編輯 / 馬叔
在資料庫上執行 SELECT COUNT(DISTINCT)
是非常常見的。在應用程式中,通常有一些分析大屏可以突出顯示去重項的數量,例如去重的使用者,去重的產品或去重的訪問。雖然傳統的 SELECT COUNT(DISTINCT)
查詢在單機設定中執行良好,但在分散式系統中卻很難解決。當你有這種型別的查詢時,你不能只是將查詢推送到從屬節點然後把結果加起來,因為很可能,在不同的從節點中存在重疊的記錄。相反,你可以這樣做:
將所有不同的資料拉到一臺機器上並在那裡計數。(伸縮性差)
做 map / reduce。(有伸縮性,但很慢)
這就是可以用到近似演算法或草算(sketches)的地方。草算(sketches)是概率演算法,可以在數學可證明的誤差範圍內有效地生成近似結果。這樣的草圖有很多,但今天我們只專門講一個:HyperLogLog (HLL)。HLL 非常擅於估算列表中的去重元素計數。首先,我們來看一下 HLL 的內部結構,來幫助我們瞭解為什麼 HLL 演算法可以伸縮地解決去重計數問題,並解決如何將其以分散式的方式應用。然後,我們來看一些使用 HLL 的例子。
HLL 在後面都在做什麼?
對所有元素求雜湊(雜湊)
HLL 和幾乎所有其他概率計數演算法都依賴於資料的均勻分佈。但是,因為在現實世界中,我們的資料一般不是均勻分佈的,所以 HLL 首先就會對每個元素進行雜湊,使資料分佈更均勻。這裡說到的均勻分佈,其意思是:元素的每一位都有 0.5 的概率成為 0 或 1.我們很快就會看到為什麼這是有用的。除了均勻性,雜湊讓 HLL 可以用相同的方式處理所有資料型別。只要你的資料型別有雜湊函式(雜湊函式),你就可以使用 HLL 進行基數估算。
觀察罕見的資料模式
在雜湊了所有元素後,HLL 將查詢每個雜湊元素的二進位制形式。它主要是看這裡是否存在不太可能發生的位元模式(bit patterns)。如果存在這種罕見的模式,就意味著我們正在處理大資料集。
為此,HLL 在每個元素的雜湊值中找首位零位元,並找到首位零位元的長度。基本上,為了能夠觀察到 k 個首位零,我們需要 2k + 1 次試驗(即,雜湊數)。因此,如果資料集中,首位零的最大數目為 k,HLL 就得出結論,這裡存在大約 2k + 1 個不同的元素。
這是非常直接、簡單的估算方法。然而,它具有一些重要的特性,在分散式環境中尤其明顯。
HLL 的記憶體佔用非常低。對於最大數 n,我們只需要儲存 log log n 位元。例如,如果我們將元素雜湊成 64 位元整數,我們只需要儲存 6 位元來進行估算。這比起樸素法(需要記住所有值),大量節省了記憶體。
我們只需要對資料進行一次遍歷(掃描),找到首位零的最大值。
我們可以使用流資料。在計算了首位零的最大值之後,如果這時來了一些新的資料,我們就可以不用再過一遍整個資料集,可以直接將它們包含進去進行計算。我們只需要查詢每個新元素的首位零的數,將它們與整個資料集裡首位零的最大數進行比較,如果需要的話,就可以更新首位零的最大值。
我們可以有效地合併兩個單獨資料集的估算結果。我們只需要選擇首位零數值更大的那個作為合併資料集首位零的最大值。這就讓我們可以將資料分片,估算它們的基數,以及合併結果。這被稱為可加性,可加性讓我們可以在分散式系統中使用 HLL。
隨機平均
如果你認為上述的估算還不是很好,你是對的。首先,我們的預測總是以 2k 的形式。其次,如果資料分佈沒有足夠統一,我們可能會得出偏差特別大的估算。
一個可能解決這些問題的方案是,用不同的雜湊函式來重複這個過程,然後取平均值。這應該是可行的,但是,對所有的資料進行多次雜湊十分昂貴。 HLL 解決了這個問題,這個方法叫做隨機平均。基本上就是,我們將資料分成桶,並分別對每個桶使用上述演算法。然後我們就取這些結果的平均值。我們用雜湊值的前幾個位元來確定某個元素屬於哪個儲存桶,然後使用剩餘的位元來計算首位零的最大值。
此外,我們還可以選擇一些桶來劃分資料,以此來定製 / 調整精度。我們需要為每個儲存桶儲存 log log n 個位元。由於我們可以將 log log n 位元中的每個估算值都儲存起來,所以就算我們建立大量的儲存桶,最終也只會使用非常少的記憶體。在進行大規模資料操作時,這麼小的記憶體佔用十分重要。要合併兩個估算,我們會合並每個桶,然後取平均值。因此,如果我們打算進行合併操作,我們應該保留每個資料桶中首位零的最大值。
還有什麼?
為了提高估算的準確性,HLL 還做了一些其他的事情,不過觀察位元模式和隨機平均仍然是 HLL 的關鍵點。在這些優化之後,HLL 可以使用 1.5 kB 的記憶體來估算資料集的基數,其典型錯誤率為2%。當然,如果用更多的記憶體,可以提高準確度。我們不會詳細介紹其他步驟,網上有關 HLL 的內容有很多很多。
分散式系統中的HLL
正如我們提到的,HLL 具有可加性的特質,這意味著,你可以將資料集分為幾個部分,使用 HLL 演算法對其分別操作,查詢每個部分的去重元素數量。然後,不用回顧原始資料,你也可以有效地合併中間的 HLL 結果、查詢所有資料的去重元素數量。
如果處理大規模資料,並將資料儲存在不同的物理機器中,那麼,無需將整個資料拖放到一個位置,你就可以使用 HLL 來計算所有資料的去重計數。實際上,Citus 可以幫你做這個操作。有一個為 PostgreSQL 開發的 HLL 擴充套件包,它與 Citus 完全相容。如果你已經安裝了 HLL 擴充套件包,並且想在分散式資料表上執行 COUNT(DISTINCT)查詢,Citus 會自動啟用 HLL。配置後,你不需要額外做任何事情。
使用 HLL
建立
要使用 HLL,我們將使用 Citus Cloud 和 GitHub 事件資料集。你可以從這裡看到並瞭解更多有關 Citus Cloud 的資訊。假設你建立了你的 Citus Cloud 例項,並通過 psql 與它連線,你可以通過下面這個建立 HLL 擴充套件:
CREATE EXTENSION hll;複製程式碼
你應該在主節點和從節點建立擴充套件。 然後通過設定 citus.count_distinct_error_rate 配置值來啟用計數不同的近似值。當配置值設定的較低時,可以提供更準確的結果,但需要更長的時間和更多的記憶體進行計算。 我們建議將其設定為0.005。
SET citus.count_distinct_error_rate TO 0.005;複製程式碼
與之前在部落格中用到的不同,我們將只使用 github_events 表 和 large_events.csv 資料集;
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
SELECT create_distributed_table('github_events', 'user_id');
\COPY github_events FROM large_events.csv CSV複製程式碼
例子
在分發了資料表格後,我們可以使用常規的 COUNT(DISTINCT)查詢來找出多少去重使用者建立了事件:
SELECT
COUNT(DISTINCT user_id)
FROM
github_events;複製程式碼
它應該返回類似這樣的東西:
count
--------
264227
(1 row)複製程式碼
看起來,這個查詢與 HLL 沒有任何關係。 但是,如果你將 citus.count_distinct_error_rate 設定為大於0,併發出 COUNT(DISTINCT)查詢,Citus 就會自動使用 HLL。對於這種簡單的用例,你甚至不需要更改查詢。建立事件的使用者,準確的去重計數是 264198,所以我們的錯誤率略大於 0.0001。
我們也可以使用約束條件來過濾掉一些結果。 例如,我們可以查詢建立 PushEvent 的去重使用者數量:
SELECT
COUNT(DISTINCT user_id)
FROM
github_events
WHERE
event_type = ‘PushEvent'::text;複製程式碼
它會返回:
count
--------
157471
(1 row)複製程式碼
類似地,該查詢的準確去重計數是157154,我們的錯誤率略大於0.002。
結論
如果在 Postgres 中,你有關於count (distinct)
伸縮性的問題,可以看一下 HLL,如果足夠近似的計數對你來說可行,這就可能會很有用。 關於“使用 Citus 進一步擴充套件計數事件”,如果你有任何問題,請與我們聯絡。
翻譯如有不當之處,歡迎指正。歡迎探討技術問題。
原文連結:citusdata