ClickHouse叢集資料均衡方案分享

禹鼎侯發表於2021-12-11

導語

ClickHouse叢集資料在寫入時,雖然可以通過Distributed引擎的sharding_key指定策略,從而保證一定程度的資料均衡,但這並不是最終解決方案。

比如rand()均衡策略雖然可以保證資料的相對均衡,但是可能會破壞資料的內在業務邏輯。舉個簡單的例子,我們想要將kafka的資料寫入clickhouse叢集,如果採用rand()的策略,則可能將同一個partition的資料拆分到clickhouse叢集不同的shard中,為後續的資料分析等造成了一定的麻煩。

雖然有類似clickhouse-sinker之類的資料匯入工具,可以做到資料匯入時的均衡,但是一旦叢集擴充套件了節點,仍然無法將存量資料均衡到新增加的節點中去。這樣就造成了存量節點的資料仍然很多,新增節點的資料相對較少,並不能起到很好的負載均衡的作用。

資料均衡方案探討

我們在討論資料均衡方案的時候,首先需要明確兩個前提:

  • 針對clickhouse叢集,而不是單點
  • 針對MergeTree家族的引擎資料(其他引擎的資料表由於無法通過分散式表去讀寫,因此也不具備資料均衡的意義)

我們知道,clickhouse儲存資料是完完全全的列式儲存,這也就意味著,在同一個partition下,資料很難再一條條的進行拆分(雖然可以做到,但比較麻煩)。因此,資料均衡最科學的方案是以partition為單位,整個partition進行搬遷。這也就意味著,分割槽的粒度越小,最終的資料越接近均衡。

另一個我們要思考的問題就是,如果其中某一個分割槽我們一直在寫入資料,我們是無法獲取該分割槽的實際大小的(因為一直在變化)。那麼,如果該分割槽資料也引數資料均衡的話,可能參與均衡的partition並不是一個完整的分割槽,就會導致分割槽資料被拆散,從而造成不可預知的問題。所以,我們希望最新的一個分割槽,不參與資料均衡的運算。

如何能獲取到最新的分割槽呢?其實可以通過SQL查詢到:

SELECT argMax(partition, modification_time) FROM system.parts WHERE database='?' AND table='?'

以上SQL查詢出來的,就是指定的資料庫表最新的分割槽。將這個分割槽排除在外,那麼剩下的分割槽就是都可以參與資料均衡的分割槽了。

另一個核心問題是,如何將partition資料在不同節點之間進行移動?我們很容易想到 attachdetach,但attachdetach的前提是,我們必須要設定配置檔案中當前操作使用者allow_drop_detached標誌為1。對於帶副本的叢集,我們總能通過zookeeper的路徑非常方便地將分割槽資料在不同節點間fetch過來。

 -- 在目標節點執行
 ALTER TABLE {{.tbname}} FETCH PARTITION '{{.partition}}' FROM '{{.zoopath}}'
 ALTER TABLE {{.tbname}} ATTACH PARTITION '{{.partition}}'
 
 -- 在原始節點執行
 ALTER TABLE {{.tbname}} DROP PARTITION '{{.partition}}'

但是對於非副本模式的叢集則沒那麼簡單了。因為我們無法知道zoopath,所以不能通過fetch的方式獲取到資料,因此,只能使用物理的方式將資料進行傳輸(比如scp, rsync等)到指定節點上。考慮到傳輸效率,這裡我們使用rsync的方式。

-- 原始節點執行
ALTER TABLE {{.tbname}} DETACH PARTITION '{{.partition}}'
# 原始節點執行
rsync -e "ssh -o StrictHostKeyChecking=false" -avp /{{.datapath}}/clickhouse/data/{{.database}}/{{.table}}/detached dstHost:/{{.datapath}}/clickhouse/data/{{.database}}/{{.table}}/detached
rm -fr /{{.datapath}}/clickhouse/data/{{.database}}/{{.table}}/detached
-- 目標節點執行
ALTER TABLE {{.tbname}} ATTACH PARTITION '{{.partition}}'
-- 原始節點執行
ALTER TABLE {{.tbname}} DROP DETACHED PARTITION '{{.partition}}'

但是,通過rsync的方式需要有前提,那就是首先必須在各個節點上已經安裝過rsync工具了,如果沒有安裝,可通過下面的命令安裝:

yum install -y rsync

其次,需要配置各節點之間的互信(主要是moveout的節點到movein節點之間的互信,但其實我們並不知道資料在節點間數如何移動的,因此最好全部配置)。

以上問題解決後,那麼就剩下最核心的一個問題了。資料如何均衡?

這裡需要說明的是,由於是整個partition的移動,因此,無法做到絕對的均衡,而是隻能做到相對的資料均衡。partition的粒度越小,均衡越精確。

一種比較科學的方案是,將各個節點的分割槽資料按大小排列之後,將最大的節點資料移動到最小的節點中去,次大的節點移到次小的節點,以此類推,不斷向中間靠攏,直到滿足某一個閾值,則不再移動。

這一段的程式碼實現我們已經通過ckman專案開源出來了,如果感興趣的朋友可以通過下面的連結閱讀原始碼:ckman:rebalancer

因此,不難發現,資料均衡的過程中,分割槽資料由於可能已經被detach,但是還沒來得及在新的節點上attach,這時候去做查詢,可能存在一定機率的不準確。

所以,在做資料均衡的過程中,最好不要有查詢操作。

插入操作反而不受影響,因為我們已經排除了最新的分割槽不參與均衡運算。

ckman如何實現資料均衡

ckman作為一款管理和監控ClickHouse叢集的視覺化工具,天然整合了資料均衡的功能。只需要點選叢集管理頁面的"均衡叢集"按鈕,即可實現資料均衡的操作。
圖片.png

與此同時,ckman還提供了命令列方式的資料均衡工具rebalancer, 其引數如下:

  • -ch-data-dir

    • clickhouse叢集資料目錄
  • -ch-hosts

    • 節點列表(每個shard只需列出一個,如果shard有多個副本,無需全部列出)
  • -ch-password

    • clickhouse使用者密碼
  • -ch-port

    • clickhouseTCP埠,預設9000
  • -ch-user

    • clickhouse的使用者,介面操作時,使用default使用者
  • -os-password

    • 節點的ssh登入密碼(非副本模式時需要)
  • -os-port

    • 節點的ssh埠,預設22(非副本模式時需要)
  • -os-user

    • 節點的ssh使用者(非副本模式時需要)

如:

rebalancer -ch-data-dir=/var/lib/ --ch-hosts=192.168.0.1,192.168.0.2,192.168.0.3 --ch-password=123123 --ch-port=9000 --ch-user=default --os-password=123456 --os-port=22 --os-user=root

實操案例

我們在ckman中準備了一個名為eoi的叢集,該叢集有三個節點,分別為192.168.21.73,192.168.21.74,192.168.21.75,叢集為非副本模式。

圖片.png

我們從官方文件給出的資料集中匯入如下資料:https://clickhouse.com/docs/e...

該資料是從2019年1月到2021年5月,共計30個月的航空資料,為了更直觀地展示資料均衡,本文將官方的建表語句做了微調,按照月進行分割槽,並且在叢集各個節點都建立表:

CREATE TABLE opensky ON CLUSTER eoi
(
    callsign String,
    number String,
    icao24 String,
    registration String,
    typecode String,
    origin String,
    destination String,
    firstseen DateTime,
    lastseen DateTime,
    day DateTime,
    latitude_1 Float64,
    longitude_1 Float64,
    altitude_1 Float64,
    latitude_2 Float64,
    longitude_2 Float64,
    altitude_2 Float64
) ENGINE = MergeTree 
PARTITION BY toYYYYMM(day)
ORDER BY (origin, destination, callsign);

並建立分散式表:

CREATE TABLE dist_opensky ON CLUSTER eoi AS opensky
ENGINE = Distributed(eoi, default, opensky, rand())

下載資料:

wget -O- https://zenodo.org/record/5092942 | grep -oP 'https://zenodo.org/record/5092942/files/flightlist_\d+_\d+\.csv\.gz' | xargs wget

資料下載完成大約4.3G

圖片.png

使用下面的指令碼將資料匯入到其中一個節點:

for file in flightlist_*.csv.gz; do gzip -c -d "$file" | clickhouse-client --password 123123 --date_time_input_format best_effort --query "INSERT INTO opensky FORMAT CSVWithNames"; done

匯入完成後,分別檢視各節點資料如下:

-- 總資料
master :) select  count() from dist_opensky;

SELECT count()
FROM dist_opensky

Query id: b7bf794b-086b-4986-b616-aef1d40963e3

┌──count()─┐
│ 66010819 │
└──────────┘

1 rows in set. Elapsed: 0.024 sec. 

-- node 21.73
master :) select  count() from opensky;

SELECT count()
FROM opensky

Query id: 5339e93c-b2ed-4085-9f58-da099a641f8f

┌──count()─┐
│ 66010819 │
└──────────┘

1 rows in set. Elapsed: 0.002 sec. 


-- node 21.74
worker-1 :) select  count() from opensky;

SELECT count()
FROM opensky

Query id: 60155715-064e-4c4a-9103-4fd6bf9b7667

┌─count()─┐
│       0 │
└─────────┘

1 rows in set. Elapsed: 0.002 sec. 

-- node 21.75
worker-2 :) select count() from opensky;

SELECT count()
FROM opensky

Query id: d04f42df-d1a4-4d90-ad47-f944b7a32a3d

┌─count()─┐
│       0 │
└─────────┘

1 rows in set. Elapsed: 0.002 sec. 

從以上資訊,我們可以知道,原始資料6600萬條全部在21.73這個節點上,另外兩個節點21.7421.75沒有資料。

ckman介面可以看到如下資訊:

圖片.png

然後點選資料均衡,等待一段時間後,會看到介面提示資料均衡成功,再次檢視各節點資料:

-- 總資料
master :) select  count() from dist_opensky;

SELECT count()
FROM dist_opensky

Query id: bc4d27a9-12bf-4993-b37c-9f332ed958c9

┌──count()─┐
│ 66010819 │
└──────────┘

1 rows in set. Elapsed: 0.006 sec. 


-- node 21.73
master :) select  count() from opensky;

SELECT count()
FROM opensky

Query id: a4da9246-190c-4663-8091-d09b2a9a2ea3

┌──count()─┐
│ 24304792 │
└──────────┘

1 rows in set. Elapsed: 0.002 sec.

-- node 21.74
worker-1 :) select  count() from opensky;

SELECT count()
FROM opensky

Query id: 5f6a8c89-c21a-4ae1-b69f-2755246ca5d7

┌──count()─┐
│ 20529143 │
└──────────┘

1 rows in set. Elapsed: 0.002 sec. 

-- node 21.75
worker-2 :) select count() from opensky;

SELECT count()
FROM opensky

Query id: 569d7c63-5279-48ad-a296-013dc1df6756

┌──count()─┐
│ 21176884 │
└──────────┘

1 rows in set. Elapsed: 0.002 sec.

通過上述操作,簡單演示了資料均衡在ckman中的實現,原始資料6600萬條全部在node1,通過均衡之後,其中node1資料為2400萬條,node22000萬條,node32100萬條,實現了大致的資料均衡。

結語

雖然我們可以通過ckman之類的工具可以實現資料的大致均衡,大大改善了操作的便利性,但資料均衡本身就是一個非常複雜的命題,一旦涉及到儲存策略(如資料儲存在遠端HDFS上),那麼又會增加資料均衡的複雜性,這些都是ckman目前所不能支援的操作(遠端資料做資料均衡沒有意義,但是可以均衡其後設資料,這樣可以在查詢時充分利用各節點的CPU效能)。因此,資料均衡想要做得科學而精確,仍然需要更多的努力。

相關文章