【ClickHouse 技术系列】- 在 ClickHouse 中处理实时更新
簡介:本文翻譯自 Altinity 針對 ClickHouse 的系列技術文章。面向聯機分析處理(OLAP)的開源分析引擎 ClickHouse,因其優良的查詢性能,PB級的數據規模,簡單的架構,被國內外公司廣泛采用。本系列技術文章,將詳細展開介紹 ClickHouse。
前言
本文翻譯自 Altinity 針對 ClickHouse 的系列技術文章。面向聯機分析處理(OLAP)的開源分析引擎 ClickHouse,因其優良的查詢性能,PB 級的數據規模,簡單的架構,被國內外公司廣泛采用。
阿里云 EMR-OLAP 團隊,基于開源 ClickHouse 進行了系列優化,提供了開源 OLAP 分析引擎 ClickHouse 的云上托管服務。EMR ClickHouse 完全兼容開源版本的產品特性,同時提供集群快速部署、集群管理、擴容、縮容和監控告警等云上產品功能,并且在開源的基礎上優化了 ClickHouse 的讀寫性能,提升了 ClickHouse 與 EMR 其他組件快速集成的能力。訪問 ClickHouse - E-MapReduce - 阿里云 了解詳情。
譯者:何源(荊杭),阿里云計算平臺事業部高級產品專家
(圖源Altinity,侵刪)
在 ClickHouse 中處理實時更新
目錄
- ClickHouse 更新的簡短歷史
- 用例
- 實現更新
- 結論
- 后續
在 OLAP 數據庫中,可變數據通常不受歡迎。ClickHouse 也不歡迎可變數據。像其他一些 OLAP 產品一樣,ClickHouse 最初甚至不支持更新。后來添加了更新功能,但是像其他許多功能一樣,都是以“ClickHouse 方式”添加的。
即使是現在,ClickHouse 更新也是異步的,因此很難在交互式應用程序中使用。盡管如此,在許多用例中,用戶需要對現有數據進行修改,并期望立即看到效果。ClickHouse 能做到嗎?當然可以。
ClickHouse 更新的簡短歷史
早在 2016 年,ClickHouse 團隊就發布了一篇題為“如何在 ClickHouse 中更新數據”的文章。當時 ClickHouse 并不支持數據修改,只能使用特殊的插入結構來模擬更新,并且數據必須按分區丟棄。
為滿足 GDPR 的要求,ClickHouse 團隊在 2018 年提供了 UPDATE 和 DELETE。后續文章ClickHouse 中的更新和刪除目前仍然是 Altinity 博客中閱讀量最多的文章之一。這種異步、非原子性的更新以 ALTER TABLE UPDATE 語句的形式實現,并且可能會打亂大量數據。這對于批量操作和不頻繁的更新是很有用的,因為它們不需要即時的結果。盡管“正常”的 SQL 更新每年都妥妥地出現在路線圖中,但依然沒能在 ClickHouse 中實現。如果需要實時更新行為,我們必須使用其他方法。讓我們考慮一個實際的用例,并比較在 ClickHouse 中的不同實現方法。
用例
考慮一個生成各種報警的系統。用戶或機器學習算法會不時查詢數據庫,以查看新的報警并進行確認。確認操作需要修改數據庫中的報警記錄。一旦得到確認,報警將從用戶的視圖中消失。這看起來像是一個 OLTP 操作,與 ClickHouse 格格不入。
由于我們無法使用更新,因此只能轉而插入修改后的記錄。一旦數據庫中有兩條記錄,我們就需要一種有效的方法來獲取最新的記錄。為此,我們將嘗試 3 種不同的方法:
- ReplacingMergeTree
- 聚合函數
- AggregatingMergeTree
ReplacingMergeTree
我們首先創建一個用來存儲報警的表。
CREATE TABLE alerts(tenant_id UInt32,alert_id String,timestamp DateTime Codec(Delta, LZ4),alert_data String,acked UInt8 DEFAULT 0,ack_time DateTime DEFAULT toDateTime(0),ack_user LowCardinality(String) DEFAULT '' ) ENGINE = ReplacingMergeTree(ack_time) PARTITION BY tuple() ORDER BY (tenant_id, timestamp, alert_id);為簡單起見,將所有報警特定列都打包到一個通用的“alert_data”列中。但是可以想象到,報警可能包含數十甚至數百列。此外,在我們的示例中,“alert_id”是一個隨機字符串。
請注意 ReplacingMergeTree 引擎。ReplacingMergeTee 是一個特殊的表引擎,它借助 ORDER BY ?語句按主鍵替換數據——具有相同鍵值的新版本行將替換舊版本行。在我們的用例中,“行數據的新舊程度”由“ack_time”列確定。替換是在后臺合并操作中進行的,它不會立即發生,也不能保證會發生,因此查詢結果的一致性是個問題。不過,ClickHouse 有一種特殊的語法來處理這樣的表,我們在下面的查詢中就會用到該語法。
在運行查詢之前,我們先用一些數據填充這個表。我們為 1000 個租戶生成 1000 萬個報警:
INSERT INTO alerts(tenant_id, alert_id, timestamp, alert_data) SELECTtoUInt32(rand(1)%1000+1) AS tenant_id,randomPrintableASCII(64) as alert_id,toDateTime('2020-01-01 00:00:00') + rand(2)%(3600*24*30) as timestamp,randomPrintableASCII(1024) as alert_data FROM numbers(10000000);接下來,我們確認 99% 的報警,為“acked”、“ack_user”和“ack_time”列提供新值。我們只是插入一個新行,而不是更新。
INSERT INTO alerts (tenant_id, alert_id, timestamp, alert_data, acked, ack_user, ack_time) SELECT tenant_id, alert_id, timestamp, alert_data, 1 as acked, concat('user', toString(rand()%1000)) as ack_user, now() as ack_time FROM alerts WHERE cityHash64(alert_id) % 99 != 0;如果我們現在查詢這個表,會看到如下結果:
SELECT count() FROM alerts┌──count()─┐ │ 19898060 │ └──────────┘1 rows in set. Elapsed: 0.008 sec.表中顯然既有已確認的行,也有未確認的行。所以替換還沒有發生。為了查看“真實”數據,我們必須添加 FINAL 關鍵字。
SELECT count() FROM alerts FINAL┌──count()─┐ │ 10000000 │ └──────────┘1 rows in set. Elapsed: 3.693 sec. Processed 19.90 million rows, 1.71 GB (5.39 million rows/s., 463.39 MB/s.)現在計數是正確了,但是看看查詢時間增加了多少!使用 FINAL 后,ClickHouse 執行查詢時必須掃描所有的行,并按主鍵合并它們。這樣能得到正確答案,但造成了大量開銷。讓我們看看,只篩選未確認的行會不會有更好的效果。
SELECT count() FROM alerts FINAL WHERE NOT acked┌─count()─┐ │ 101940 │ └─────────┘1 rows in set. Elapsed: 3.570 sec. Processed 19.07 million rows, 1.64 GB (5.34 million rows/s., 459.38 MB/s.)盡管計數顯著減少,但查詢時間和處理的數據量還是一樣。篩選無助于加快查詢速度。隨著表增大,成本可能會更加巨大。它不能擴展。
注:為了提高可讀性,所有查詢和查詢時間都像在“clickhouse-client”中運行一樣顯示。實際上,我們嘗試了多次查詢,以確保結果一致,并使用“clickhouse-benchmark”實用程序進行確認。
好吧,查詢整個表沒什么幫助。我們的用例還能使用 ReplacingMergeTree 嗎?讓我們隨機選擇一個 tenant_id,然后選擇所有未確認的記錄——想象用戶正在查看監控視圖。我喜歡 Ray Bradbury,那就選 451 好了。由于“alert_data”的值只是隨機生成的,因此我們將計算一個校驗和,用來確認多種方法的結果相同:
SELECT count(), sum(cityHash64(*)) AS data FROM alerts FINAL WHERE (tenant_id = 451) AND (NOT acked)┌─count()─┬─────────────────data─┐ │ 90 │ 18441617166277032220 │ └─────────┴──────────────────────┘1 rows in set. Elapsed: 0.278 sec. Processed 106.50 thousand rows, 119.52 MB (383.45 thousand rows/s., 430.33 MB/s.)太快了!我們只用了 278 毫秒就查詢了所有未確認的數據。為什么這次很快?區別就在于篩選條件。“tenant_id”是某個主鍵的一部分,所以 ClickHouse 可以在 FINAL 之前篩選數據。在這種情況下,ReplacingMergeTree 就變得高效了。
我們也試試用戶篩選器,并查詢由特定用戶確認的報警數量。列的基數是相同的——我們有 1000 個用戶,可以試試 user451。
SELECT count() FROM alerts FINAL WHERE (ack_user = 'user451') AND acked┌─count()─┐ │ 9725 │ └─────────┘1 rows in set. Elapsed: 4.778 sec. Processed 19.04 million rows, 1.69 GB (3.98 million rows/s., 353.21 MB/s.)這個速度非常慢,因為沒有使用索引。ClickHouse 掃描了全部 1904 萬行。請注意,我們不能將“ack_user”添加到索引,因為它將破壞 ReplacingMergeTree 語義。不過,我們可以用 PREWHERE 進行一個巧妙的處理:
SELECT count() FROM alerts FINAL PREWHERE (ack_user = 'user451') AND acked┌─count()─┐ │ 9725 │ └─────────┘1 rows in set. Elapsed: 0.639 sec. Processed 19.04 million rows, 942.40 MB (29.80 million rows/s., 1.48 GB/s.)PREWHERE 是一個特別的妙招,能讓 ClickHouse 以不同方式應用篩選器。通常情況下 ClickHouse 是足夠智能的,可以自動將條件移動到 PREWHERE,因此用戶不必在意。這次沒有發生,幸好我們檢查過了。
聚合函數
ClickHouse 因支持各種聚合函數而聞名,最新版本可支持 100 多種。結合 9 個聚合函數組合子(參見 Combinators | ClickHouse Documentation),這為有經驗的用戶提供了很高的靈活性。對于此用例,我們不需要任何高級函數,僅使用以下 3 個函數:“argMax”、“max”和“any”。
可以使用“argMax”聚合函數執行針對第 451 個租戶的相同查詢,如下所示:
SELECT count(), sum(cityHash64(*)) data FROM (SELECT tenant_id, alert_id, timestamp, argMax(alert_data, ack_time) alert_data, argMax(acked, ack_time) acked,max(ack_time) ack_time_,argMax(ack_user, ack_time) ack_userFROM alerts GROUP BY tenant_id, alert_id, timestamp ) WHERE tenant_id=451 AND NOT acked;┌─count()─┬─────────────────data─┐ │ 90 │ 18441617166277032220 │ └─────────┴──────────────────────┘1 rows in set. Elapsed: 0.059 sec. Processed 73.73 thousand rows, 82.74 MB (1.25 million rows/s., 1.40 GB/s.)同樣的結果,同樣的行數,但性能是之前的 4 倍!這就是 ClickHouse 聚合的效率。缺點在于,查詢變得更加復雜。但是我們可以讓它變得更簡單。
請注意,當確認報警時,我們只更新以下 3 列:
- acked: 0 => 1
- ack_time: 0 => now()
- ack_user: ‘’ => ‘user1’
在所有 3 種情況下,列值都會增加!因此,我們可以使用“max”代替略顯臃腫的“argMax”。由于我們不更改“alert_data”,因此不需要對此列進行任何實際聚合。ClickHouse 有一個很好用的“any”聚合函數,可以實現這一點。它可以在沒有額外開銷的情況下選取任何值:
SELECT count(), sum(cityHash64(*)) data FROM (SELECT tenant_id, alert_id, timestamp, any(alert_data) alert_data, max(acked) acked, max(ack_time) ack_time,max(ack_user) ack_userFROM alertsGROUP BY tenant_id, alert_id, timestamp ) WHERE tenant_id=451 AND NOT acked;┌─count()─┬─────────────────data─┐ │ 90 │ 18441617166277032220 │ └─────────┴──────────────────────┘1 rows in set. Elapsed: 0.055 sec. Processed 73.73 thousand rows, 82.74 MB (1.34 million rows/s., 1.50 GB/s.)查詢變簡單了,而且更快了一點!原因就在于使用“any”函數后,ClickHouse 不需要對“alert_data”列計算“max”!
AggregatingMergeTree
AggregatingMergeTree 是 ClickHouse 最強大的功能之一。與物化視圖結合使用時,它可以實現實時數據聚合。既然我們在之前的方法中使用了聚合函數,那么能否用 AggregatingMergeTree 使其更加完善呢?實際上,這并沒有什么改善。
我們一次只更新一行,所以一個組只有兩行要聚合。對于這種情況,AggregatingMergeTree 不是最好的選擇。不過我們有個小技巧。我們知道,報警總是先以非確認狀態插入,然后再變成確認狀態。用戶確認報警后,只有 3 列需要修改。如果我們不重復其他列的數據,可以節省磁盤空間并提高性能嗎?
讓我們創建一個使用“max”聚合函數來實現聚合的表。我們也可以用“any”代替“max”,但列必須是可以設置為空的——“any”會選擇一個非空值。
DROP TABLE alerts_amt_max;CREATE TABLE alerts_amt_max (tenant_id UInt32,alert_id String,timestamp DateTime Codec(Delta, LZ4),alert_data SimpleAggregateFunction(max, String),acked SimpleAggregateFunction(max, UInt8),ack_time SimpleAggregateFunction(max, DateTime),ack_user SimpleAggregateFunction(max, LowCardinality(String)) ) Engine = AggregatingMergeTree() ORDER BY (tenant_id, timestamp, alert_id);由于原始數據是隨機的,因此我們將使用“alerts”中的現有數據填充新表。我們將像之前一樣分兩次插入,一次是未確認的報警,另一次是已確認的報警:
INSERT INTO alerts_amt_max SELECT * FROM alerts WHERE NOT acked;INSERT INTO alerts_amt_max SELECT tenant_id, alert_id, timestamp,'' as alert_data, acked, ack_time, ack_user FROM alerts WHERE acked;請注意,對于已確認的事件,我們會插入一個空字符串,而不是“alert_data”。我們知道數據不會改變,我們只能存儲一次!聚合函數將填補空白。在實際應用中,我們可以跳過所有不變的列,讓它們獲得默認值。
有了數據后,我們先檢查數據大小:
SELECT table, sum(rows) AS r, sum(data_compressed_bytes) AS c, sum(data_uncompressed_bytes) AS uc, uc / c AS ratio FROM system.parts WHERE active AND (database = 'last_state') GROUP BY table┌─table──────────┬────────r─┬───────────c─┬──────────uc─┬──────────────ratio─┐ │ alerts │ 19039439 │ 20926009562 │ 21049307710 │ 1.0058921003373666 │ │ alerts_amt_max │ 19039439 │ 10723636061 │ 10902048178 │ 1.0166372782501314 │ └────────────────┴──────────┴─────────────┴─────────────┴────────────────────┘好吧,由于有隨機字符串,我們幾乎沒有壓縮。但是,由于我們不必存儲“alerts_data”兩次,所以相較于不聚合,聚合后數據規模可以縮小一半。
現在我們試試對聚合表進行查詢:
SELECT count(), sum(cityHash64(*)) data FROM (SELECT tenant_id, alert_id, timestamp, max(alert_data) alert_data, max(acked) acked, max(ack_time) ack_time,max(ack_user) ack_userFROM alerts_amt_maxGROUP BY tenant_id, alert_id, timestamp ) WHERE tenant_id=451 AND NOT acked;┌─count()─┬─────────────────data─┐ │ 90 │ 18441617166277032220 │ └─────────┴──────────────────────┘1 rows in set. Elapsed: 0.036 sec. Processed 73.73 thousand rows, 40.75 MB (2.04 million rows/s., 1.13 GB/s.)多虧了 AggregatingMergeTree,我們處理的數據更少(之前是 82MB,現在是 40MB),效率更高。
實現更新
ClickHouse 會盡最大努力在后臺合并數據,從而刪除重復的行并執行聚合。然而,有時強制合并是有意義的,例如為了釋放磁盤空間。這可以通過 OPTIMIZE FINAL 語句來實現。OPTIMIZE 操作速度慢、代價高,因此不能頻繁執行。讓我們看看它對查詢性能有什么影響。
OPTIMIZE TABLE alerts FINAL Ok. 0 rows in set. Elapsed: 105.675 sec.OPTIMIZE TABLE alerts_amt_max FINAL Ok. 0 rows in set. Elapsed: 70.121 sec.執行 OPTIMIZE FINAL 后,兩個表的行數相同,數據也相同。
┌─table──────────┬────────r─┬───────────c─┬──────────uc─┬────────────ratio─┐ │ alerts │ 10000000 │ 10616223201 │ 10859490300 │ 1.02291465565429 │ │ alerts_amt_max │ 10000000 │ 10616223201 │ 10859490300 │ 1.02291465565429 │ └────────────────┴──────────┴─────────────┴─────────────┴──────────────────┘不同方法之間的性能差異變得不那么明顯了。匯總表如下:
結論
ClickHouse 提供了豐富的工具集來處理實時更新,如 ReplacingMergeTree、CollapsingMergeTree(本文未提及)、AggregatingMergeTree 和聚合函數。所有這些方法都具有以下三個共性:
- 通過插入新版本來“修改”數據。ClickHouse 中的插入速度非常快。
- 有一些有效的方法來模擬類似于 OLTP 數據庫的更新語義。
- 然而,實際的修改并不會立即發生。
具體方法的選擇取決于應用程序的用例。對用戶來說,ReplacingMergeTree 是直截了當的,也是最方便的方法,但只適用于中小型的表,或者數據總是按主鍵查詢的情況。使用聚合函數可以提供更高的靈活性和性能,但需要大量的查詢重寫。最后,AggregatingMergeTree 可以節約存儲空間,只保留修改過的列。這些都是 ClickHouse DB 設計人員的好工具,可根據具體需要來應用。
后續
您已經了解了在 ClickHouse 中處理實時更新相關內容,本系列還包括其他內容:
- 使用新的 TTL move,將數據存儲在合適的地方
- 在 ClickHouse 物化視圖中使用 Join
- ClickHouse 聚合函數和聚合狀態
- ClickHouse 中的嵌套數據結構
原文鏈接
本文為阿里云原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的【ClickHouse 技术系列】- 在 ClickHouse 中处理实时更新的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “不服跑个分?” 是噱头还是实力?
- 下一篇: Cloudera Manager 术语和