Shopee ClickHouse 冷熱數據分離存儲架構與實踐

摘要

Shopee ClickHouse 是一款基於開源數據庫 ClickHouse 做二次開發、架構演進的高可用分佈式分析型數據庫。本文將主要介紹 Shopee ClickHouse 的冷熱分離存儲架構和支持公司業務的實踐。

Shopee ClickHouse 的冷熱分離存儲架構使用 JuiceFS 客戶端 mount 遠端對象存儲到本地機器路徑,通過編寫 ClickHouse 的存儲策略,如同使用多卷存儲一樣使用遠端對象存儲。因爲我們用同一個 ClickHouse DB 集羣支持多個團隊的業務,不同團隊甚至相同團隊的不同業務之間對數據的冷熱劃分基準可能都不同,所以在做冷熱分離時策略需要做到 ClickHouse 的表級別。

爲了做到表級別的冷熱分離,我們依照提前編輯好的存儲策略,針對存量需要做冷熱隔離的業務表,修改表的存儲策略。對於新的需要做冷熱分離的業務表,建表時指明使用支持數據落在遠端存儲的存儲策略,再通過細化 TTL 表達式判斷數據應該落在本地還是遠端。

冷熱分離存儲架構上線後,我們遇到了一些問題和挑戰,比如:juicefs object request error、Redis 內存增長異常、suspicious broken parts 等。本文會針對其中一些問題,結合場景上下文,並通過源碼分析來給出解決方案。

總的來說 Shopee ClickHouse 冷熱存儲架構的整體設計思想是:本地 SSD 存儲查詢熱數據,遠端存儲查詢相對不那麼頻繁的數據,從而節約存儲成本,支持更多的數據存儲需求。

1. Shopee ClickHouse 集羣總架構

ClickHouse 是一款開源的列存 OLAP(在線分析查詢)型數據庫,實現了向量化執行引擎,具有優秀的 AP 查詢性能。Shopee ClickHouse 則是基於 ClickHouse 持續做二次迭代開發和產品架構演進的分析型數據庫。

下圖展示了 Shopee ClickHouse DB 集羣的架構:

從上到下依次是用戶請求介入 SLB、Proxy 層、ClickHouse DB 集羣層,最下方是遠端對象存儲,這裏我們用的是 Shopee STO 團隊提供的 S3。

其中,SLB 提供用戶請求路由Proxy 層提供了查詢路由,請求會根據用戶連接串中的集羣名,路由到對應的集羣中,也提供了部分寫入 balance 和查詢路由的能力;ClickHouse DB 集羣層是由 Shopee ClickHouse 數據庫組成的分佈式集羣,目前有以 SSD 磁盤作爲熱數據存儲介質的計算型分佈式集羣,和計算型單節點集羣,還有以 SATA Disk 作爲存儲介質的存儲型分佈式集羣;最下方的遠端存儲則用作冷數據存儲介質

2. 冷熱分離存儲架構方案

用戶希望數據可以存儲得更多更久,查詢速度更快。但是通常數據存儲得越多,在相同查詢條件下,返回延時就會越高。

從資源利用率上來說,我們希望存儲在 Shopee ClickHouse 上的數據可以被更多地訪問和利用,爲業務提供更廣泛的支持。所以,起初我們要求業務方存儲到 Shopee ClickHouse 數據庫中的數據是用戶的業務熱數據。

但是這樣也帶來了一些問題,比如:用戶有時候需要查詢時間相對久一點的數據做分析,這樣就得把那部分不在 ClickHouse 的數據導入後再做分析,分析結束後還要刪除這部分數據。再比如:一些通過日誌服務做聚合分析和檢索分析的業務,也需要相對久一點的日誌服務數據來幫助監管和分析日常業務。

基於此類需求,我們一方面希望資源的最大化利用,一方面希望支持更多的數據存儲量,同時不影響用戶熱數據的查詢速度,所以使用冷熱數據分離的存儲架構就是一個很好的選擇。

通常,冷熱分離方案的設計需要考慮以下幾個問題:

而冷數據存儲介質的選擇一般通過以下幾個要點做對比分析:

2.1 冷存介質的選擇和 JuiceFS

可以用作冷存儲的介質一般有 S3、Ozone、HDFS、SATA Disk。其中,SATA Disk 受限於機器硬件,不易擴展,可以先淘汰。而 HDFS、Ozone 和 S3 都是比較好的冷存介質。

同時,爲了高效簡單地使用冷存介質,我們把目光鎖定在了 JuiceFS 上。JuiceFS 是一種基於 Redis 和雲對象存儲構建的開源 POSIX 文件系統,可以使我們更加便捷和高效地訪問遠端對象存儲。

JuiceFS 使用公有云中已有的對象存儲,如 S3、GCS、OSS 等。用 JuiceFS 做存儲,數據實際上存儲在遠端,而 JuiceFS 重點關注這些存儲在遠端的數據文件的元數據管理。JuiceFS 選擇 Redis 作爲存儲元數據的引擎,這是因爲 Redis 存儲都在內存中,可以滿足元數據讀寫的低延時和高 IOPS,支持樂觀事務,滿足文件系統對元數據操作的原子性 [1]。

JuiceFS 提供了一種高效便捷的遠端存儲訪問方式,只需要通過 JuiceFS 的客戶端,使用 formatmount 命令,就可以將遠端存儲 mount 到本地路徑。我們 ClickHouse 數據庫訪問遠端存儲就可以如同訪問本地路徑一樣訪問。

選擇了 JuiceFS 後,我們再把目光轉回冷數據存儲介質的篩選。由於 JuiceFS 主要支持的後臺存儲層爲對象存儲類別,餘下的選項變成了 S3 和 Ozone。我們設計了一個如下的 benchmark , 使用 ClickHouse TPCH Star Schema Benchmark 1000s(benchmark 詳細信息可以參照 ClickHouse 社區文檔 [2])作爲測試數據,分別測試 S3 和 Ozone 的 Insert 性能,並使用 Star Schema Benchmark 的 select 語句做查詢性能對比。

查詢的數據處於以下三種存儲狀態:

以下是我們的測試抽樣結果:

(1)Insert 性能抽樣結果

Insert Lineorder 表數據到 Ozone:

Insert Lineorder 表數據到 S3:

可以看出,S3 的 Insert 性能稍微強勢一點

(2)查詢性能抽樣結果

依照 ClickHouse Star Schema Benchmark,在導入完畢 Customer、Lineorder、Part、Supplier 表後,需要根據四張表的數據創建一個打平的寬表。

CREATE TABLE lineorder_flat  
ENGINE = MergeTree  
PARTITION BY toYear(LO_ORDERDATE)  
ORDER BY (LO_ORDERDATE, LO_ORDERKEY)  
AS  
SELECT  
l.LO_ORDERKEY AS LO_ORDERKEY,  
l.LO_LINENUMBER AS LO_LINENUMBER,  
l.LO_CUSTKEY AS LO_CUSTKEY,  
l.LO_PARTKEY AS LO_PARTKEY,  
l.LO_SUPPKEY AS LO_SUPPKEY,  
l.LO_ORDERDATE AS LO_ORDERDATE,  
l.LO_ORDERPRIORITY AS LO_ORDERPRIORITY,  
l.LO_SHIPPRIORITY AS LO_SHIPPRIORITY,  
l.LO_QUANTITY AS LO_QUANTITY,  
l.LO_EXTENDEDPRICE AS LO_EXTENDEDPRICE,  
l.LO_ORDTOTALPRICE AS LO_ORDTOTALPRICE,  
l.LO_DISCOUNT AS LO_DISCOUNT,  
l.LO_REVENUE AS LO_REVENUE,  
l.LO_SUPPLYCOST AS LO_SUPPLYCOST,  
l.LO_TAX AS LO_TAX,  
l.LO_COMMITDATE AS LO_COMMITDATE,  
l.LO_SHIPMODE AS LO_SHIPMODE,  
c.C_NAME AS C_NAME,  
c.C_ADDRESS AS C_ADDRESS,  
c.C_CITY AS C_CITY,  
c.C_NATION AS C_NATION,  
c.C_REGION AS C_REGION,  
c.C_PHONE AS C_PHONE,  
c.C_MKTSEGMENT AS C_MKTSEGMENT,  
s.S_NAME AS S_NAME,  
s.S_ADDRESS AS S_ADDRESS,  
s.S_CITY AS S_CITY,  
s.S_NATION AS S_NATION,  
s.S_REGION AS S_REGION,  
s.S_PHONE AS S_PHONE,  
p.P_NAME AS P_NAME,  
p.P_MFGR AS P_MFGR,  
p.P_CATEGORY AS P_CATEGORY,  
p.P_BRAND AS P_BRAND,  
p.P_COLOR AS P_COLOR,  
p.P_TYPE AS P_TYPE,  
p.P_SIZE AS P_SIZE,  
p.P_CONTAINER AS P_CONTAINER  
FROM lineorder AS l  
INNER JOIN customer AS c ON c.C_CUSTKEY = l.LO_CUSTKEY  
INNER JOIN supplier AS s ON s.S_SUPPKEY = l.LO_SUPPKEY  
INNER JOIN part AS p ON p.P_PARTKEY = l.LO_PARTKEY

再執行這條 SQL 語句,當數據全部在 Ozone 上時,發生瞭如下 Error:

Code: 246. DB::Exception: Received from localhost:9000. DB::Exception: Bad size of marks file '/mnt/jfs/data/tpch1000s_juice/customer/all_19_24_1/C_CUSTKEY.mrk2': 0, must be: 18480

Select 數據一部分在 Ozone,並且此過程中發生了數據從 SSD 磁盤下沉到 Ozone 的情況。

結果:Hang 住,無法查詢。

做這個測試時,我們使用的 Ozone 是社區版本 1.1.0-SNAPSHOT,此次測試結果僅說明 Ozone 1.1.0-SNAPSHOT 不是很適合我們的使用場景。

由於 Ozone 1.1.0-SNAPSHOT 在我們的使用場景中有功能性的缺點,所以後續的 Star Schema Benchmark 的性能測試報告重點放在 SSD 和 S3 的性能對比上(詳細 Query SQL 語句可以從 ClickHouse 社區文檔獲取)。

3ZhnNp

最終,在各個方面的對比下,我們選擇 S3 作爲冷存介質

因此,冷熱存儲分離的方案採用 JuiceFS+S3 實現,下文將簡述實現過程。

2.2 冷熱數據存儲分離的實現

首先,我們通過使用 JuiceFS 客戶端,mount S3 bucket 到本地存儲路徑 /mnt/jfs,然後編輯 ClickHouse 存儲策略配置 ../config.d/storage.xml 文件。編寫存儲策略配置文件時要注意,不要影響到歷史用戶存儲(即保留之前的存儲策略)。在這裏,default 就是我們的歷史存儲策略,hcs_ck 是冷熱分離的存儲策略。

詳細信息可以參照下圖:

有需要冷熱分離存儲的業務,只需要在建表 Statement 裏面寫明存儲策略爲 hcs_ck,然後通過 TTL 的表達式來控制冷數據下沉策略。

下面通過一個例子說明使用方式和數據分離過程。表 hcs_table_name 是一個需要冷熱存儲分離的業務日誌數據表,以下是建表語句:

CREATE TABLE db_name.hcs_table_name  
(  
    .....  
    `log_time` DateTime64(3),  
    `log_level` String,  
    .....  
    `create_time` DateTime DEFAULT now()  
)  
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/db_name.hcs_table_name  
''{replica}')  
PARTITION BY toYYYYMMDD(log_time)  
ORDER BY (ugi, ip)  
TTL toDateTime(log_time) TO VOLUME 'v_ssd',  
        toDateTime(log_time) + toIntervalDay(7) TO VOLUME 'v_cold',  
        toDateTime(log_time) + toIntervalDay(14)  
SETTINGS index_granularity = 16384,  
                   storage_policy = 'hcs_ck',   
                    parts_to_throw_insert = 1600

通過上面的 TTL 表達式可以看到,hcs_table_name 這個表指明最近 7 天的數據存儲在本地 SSD 磁盤,第 8 到 14 天的數據存儲在遠端 S3,超過 14 天的數據過期刪除。

大體流程如下圖所示:

hcs_table_name 的 data parts(ClickHouse 的數據存儲以 data part 爲基本處理單位)會被後臺任務調度,後臺任務由線程 BgMoveProcPool 執行,這個線程來自 back_ground_move_pool(注意和 back_ground_pool 不是同一個)。

std::optional<BackgroundProcessingPool> background_move_pool; /// The thread pool for the background moves performed by the tables.

後臺任務調度會判斷 data parts 是否需要 move(數據是否需要下沉移動到遠端存儲上)和是否可以 move。

如果需要執行 move,後臺 move_pool 會創建一個 move 的 task。這個 task 的核心邏輯是:首先選擇需要 move 的 data parts,然後再 move 這些 data parts 到目的存儲。

在接口:

MergeTreePartsMover::selectPartsForMove

中根據 TTL 表達式獲取 ttl_entry,然後根據 data parts 中的 ttl_move 信息,選出需要 move 的 data parts,存儲 data parts 的 move_entry(包含 IMergeTreeDataPart 指針和需要預留的存儲空間大小)到 vector 中。之後會調用接口:

MergeTreeData::moveParts

實現 move 操作,move 的過程簡單來說就是 clone SSD 磁盤上的 data parts 到遠端存儲 S3 上 hcs_table_name 表的 detach 目錄下,然後再從 detach 目錄下把 data parts 移出來,最後這些在 SSD 磁盤上的 data parts 會在 IMergeTreeDataPart 的析構函數中被清除。

所以整個 move 過程中,表一直是可查的,因爲是 clone 操作,同一時刻下 move 的 data parts 要麼在 SSD 磁盤上爲 active,要麼在遠端存儲上爲 active。

關於表 data parts 的 move 信息,也可以查詢系統表 system.parts 的以下三個字段:

move_ttl_info.expression;
move_ttl_info.min; 
move_ttl_info.max;

3. 實踐分享

在 Shopee ClickHouse 冷熱數據分離存儲架構上線後,我們總結了一些實踐中遇到的問題。

3.1 Redis 內存增長異常

S3 上的數據存儲量並沒有增加太多,Redis 內存卻持續高速增長。

JuiceFS 使用 Redis 存儲 S3 上的數據文件的元數據,所以正常情況下,S3 上的數據文件越多,Redis 存儲使用量也就越多。一般這種異常情況是因爲目標表有很多小文件沒有 merge 而直接下沉,很容易打滿 Redis。

這也會引入另一個問題:一旦 Redis 內存打滿,JuiceFS 就不能再成功寫數據到 S3 上,如果 unmount 掉 JuiceFS 客戶端,也無法再次成功 mount 上去,再次 mount 的時候會拋 Error:

Meta: create session: OOM command not allowed when used memory > 'maxmemory'.

要避免這種問題發生,首先應該做好 ClickHouse merge 狀態的監控。clickhouse-exporter 會採集一個 merge 指標 clickhouse_merge,這個指標會採集到當前正在觸發的 merge 個數(通過查詢 system.metrics 表 metric=‘merge’),每觸發一次 merge 會有一個表的多個 data parts 做合併操作。按照我們的經驗來看,若每三個小時 merge 的平均次數小於 0.5,那麼很有可能是這臺機器的 merge 出現了問題。

而 merge 異常的原因可能有很多(例如 HTTPHandler 線程、ZooKeeperRecv 線程持續佔據了大量 CPU 資源等), 這個不是本文的介紹重點,在此不再展開。所以可以設置告警規則,如果三小時內 merge 次數小於 0.5 次,告警給 ClickHouse 的開發運維團隊同學,避免大量小文件產生。

如果已經有大量小文件下沉到 S3 應該怎麼辦

首先要阻止數據繼續下沉,可以通過兩種方式找到有大量小文件下沉的用戶業務表。

第一種方式:查看 ClickHouse 的 Error Log,找到拋 too many parts 的表,再進一步判斷拋 Error 的表是否有冷熱存儲。

第二種方式:通過查詢 system.parts 表,找出 active parts 明顯過多,並且 disk_name 等於冷存的別名的。定位到產生大量小文件的表後,通過 ClickHouse 系統命令 SQL 停止數據下沉,避免 Redis 內存打滿。

SYSTEM STOP MOVES [[db.]merge_tree_family_table_name]

如果表比較小,比如壓縮後小於 1TB(這裏的 1TB 是一個經驗值,我們曾經使用 insert into … select * from … 方式導表數據,如果大於 1TB,導入時間會很久,還有一定的可能性在導入中途失敗),在確認 merge 功能恢復正常後,可以選擇創建 temp table > insert into this temp table > select * from org table,然後 drop org table > rename temp table to org table。

如果表比較大,確認 merge 功能恢復正常後,嘗試通過系統命令 SQL 喚醒 merge 線程:

SYSTEM START MERGES [[db.]merge_tree_family_table_name]

如果 merge 進行緩慢,可以查詢 system.parts 表,找到已經落在 S3 上的 data parts,然後手動執行 Query 將落在 S3 上的小文件移回到 SSD 上:

ALTER TABLE table_source MOVE PART/PARTITION partition_expr TO volume 'ssd_volume'

因爲 SSD 的 IOPS 比 S3 要高很多(即使是通過 JuiceFS 訪問加速後),這樣一方面加快 merge 過程,一方面因爲文件移出 S3,會釋放 Redis 內存。

3.2 JuiceFS 讀寫 S3 失敗

數據下沉失敗,通過 JuiceFS 訪問 S3,無法對 S3 進行讀寫操作,這個時候用戶查詢如果覆蓋到數據在 S3 上的,那麼查詢會拋 S3 mount 的本地路徑上的數據文件無法訪問的錯誤。遇到這個問題可以查詢 JuiceFS 的日誌。

JuiceFS 的日誌在 Linux CentOS 中存儲在 syslog 上,查詢日誌可以用方法 cat/var/log/messages|grep 'juicefs',不同操作系統對應的日誌目錄可以參照 JuiceFS 社區文檔 [3]。

我們遇到的問題是 send request to S3 host name certificate expired。後來通過聯繫 S3 的開發運維團隊,解決了訪問問題。

那麼如何監控這類 JuiceFS 讀寫 S3 失敗的情況呢?可以通過 JuiceFS 提供的指標 juicefs_object_request_errors 監控,如果出現 Error 就告警團隊成員,及時查詢日誌定位問題。

3.3 clickhouse-server 啓動失敗

對歷史表需要做冷熱數據存儲分離的複製表(表引擎含有 Replicated 前綴)修改 TTL 時,clickhouse-server 本地 .sql 文件元數據中的 TTL 表達式和 ZooKeeper 上存儲的 TTL 表達式不一致。這個是我們在測試過程中遇到的問題,如果沒有解決這個問題而重啓 clickhouse-server 的話,會因爲表結構沒有對齊而使 clickhouse-server 啓動失敗。

這是因爲對複製表的 TTL 的修改是先修改 ZooKeeper 內的 TTL,然後纔會修改同一個節點下的機器上表的 TTL。所以如果在修改 TTL 後,本地機器 TTL 還沒有修改成功,而重啓了 clickhouse-server,就會發生上述問題。

3.4 suspicious_broken_parts

重啓 clickhouse-server 失敗,拋出 Error:

DB::Exception: Suspiciously many broken parts to remove

這是因爲 ClickHouse 在重啓服務的時候,會重新加載 MergeTree 表引擎數據,主要代碼接口爲:

MergeTreeData::loadDataParts(bool skip_sanity_checks)

在這個接口中會獲取到每一個表的 data parts,判斷 data part 文件夾下是否有 #DELETE_ON_DESTROY_MARKER_PATH 也就是 delete-on-destroy.txt 文件存在。如果有,將該 part 加入到 broken_parts_to_detach,並將 suspicious_broken_parts 統計個數加 1。

那麼在冷熱數據存儲分離的場景下,data parts 通過 TTL 做下沉的時候,在覈心接口 move 操作的函數中會有如下的代碼調用關係:

MergeTreeData::moveParts->MergeTreePartsMover::swapClonedPart->MergeTreeData::swapActivePart

在最後一個函數中交換 active parts 的路徑指向,也就是上文說的,data parts 在 move 過程中,數據是可查的,要麼在 SSD 爲 active,要麼在 S3 爲 active。

void MergeTreeData::swapActivePart(MergeTreeData::DataPartPtr part_copy)  
{  
    auto lock = lockParts();  
    for (auto original_active_part : getDataPartsStateRange(DataPartState::Committed)) // NOLINT (copy is intended)  
    {  
        if (part_copy->name == original_active_part->name)  
        {  
            .....  
            String marker_path = original_active_part->getFullRelativePath() + DELETE_ON_DESTROY_MARKER_PATH;  
            try  
            {  
                disk->createFile(marker_path);  
            }  
            catch (Poco::Exception & e)  
            ...  
}

在這個接口中,舊的 active parts(也就是 replacing parts)內會創建 #DELETE_ON_DESTROY_MARKER_PATH 文件來把 state 修改爲 DeleteOnDestory,用於後期 IMergeTreeDataPart 析構時刪除該 state 的 data parts。

這也就是在我們的使用場景下會出現 suspicious_broken_parts 的原因,這個值超過默認閾值 10 的時候就會影響 ClickHouse 服務啓動。

解決方案有兩種:第一種,刪除這個機器上拋出該錯誤的表的元數據 .sql 文件、存儲數據、ZooKeeper 上的元數據,重啓機器後重新建表,數據會從備份機器上同步過來。第二種,在 ClickHouse /flags 路徑下用 clickhouse-server 進程的運行用戶創建 force_restore_data flag,然後重啓即可。

從上述問題中可以看到,使用 JuiceFS+S3 實現了冷熱數據分離存儲架構後,引入了新的組件(JuiceFS+Redis+S3),數據庫的使用場景更加靈活,相應地,各個方面的監控信息也要做好。這裏分享幾個比較重要的監控指標:

4. 冷熱存儲架構收益總述

冷熱數據存儲分離後,我們更好地支持了用戶的數據業務,提高了整體集羣的數據存儲能力,緩解了各個機器的本地存儲壓力,對業務數據的管理也更加靈活。

冷熱數據分離架構上線前,我們的集羣機器平均磁盤使用率接近 85%。上線後,通過修改業務用戶表 TTL,這一數據下降到了 75%。並且整體集羣在原有的業務量基礎上,又支持了兩個新的數據業務。如果沒有上線冷熱隔離,我們的集羣在擴容前就會因爲磁盤用量不足而無法承接新的項目。當前我們下沉到遠端 S3 的數據量大於 90TB(壓縮後)。

未來 Shopee ClickHouse 會持續開發更多有用的 feature,也會持續演進產品架構。目前 JuiceFS 在我們生產環境中的使用非常穩定,我們後續會進一步使用 JuiceFS 訪問 HDFS,進而實現 Shopee ClickHouse 存儲計算分離架構。

本文提到的各個產品組件版本信息如下:

  • Shopee ClickHouse:當前基於社區版 ClickHouse 20.8.12.2-LTS version

  • JuiceFS:v0.14.2

  • Redis:v6.2.2,sentinel model,開啓 AOF(策略爲 Every Secs),開啓 RDB(策略爲一天一備份)

  • S3:由 Shopee STO 團隊提供

  • Ozone:1.1.0-SNAPSHOT

相關鏈接

[1] JuiceFS: https://github.com/juicedata/juicefs/blob/main/docs/en/redis_best_practices.md

[2] ClickHouse 社區文檔: https://clickhouse.tech/docs/en/getting-started/example-datasets/star-schema/

[3] JuiceFS 社區文檔: https://github.com/juicedata/juicefs/blob/main/docs/zh_cn/fault_diagnosis_and_analysis.md

本文作者

Teng,畢業於新加坡國立大學,來自 Shopee Data Infra 團隊。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/eELcw0v2U9UXaHnfADj3tQ