在基于 clickhouse 做類數(shù)倉建模時通常的做法是在本地創(chuàng)建物化視圖,然后使用分布式表做代理對外提供服務(wù)。我們知道 clickhouse 對于 DQL 內(nèi)部實現(xiàn)了分布式,而對于 DDL 則需要我們自動實現(xiàn)比如:
drop table table_name on cluster cluster_name;
來實現(xiàn)分布式 DDL,但對于
select count() from distributed_table_name;
此類分布式表的查詢會自動執(zhí)行分布式查詢并在查詢?nèi)肟诠?jié)點匯總最終數(shù)據(jù),即 MPP 架構(gòu)。
在 clickhouse 中本地表通常以 local 結(jié)尾,而代理它們的分布式表通常以 all 結(jié)尾(只是規(guī)范)
但在一些特殊情況下,上述的分布式查詢會降級為本地查詢,此時對分布式表的查詢會隨機路由到一張代理的本地表中導(dǎo)致該 SQL 的返回結(jié)果在反復(fù)橫跳即為詭異
一、問題復(fù)現(xiàn)
1.1 知識點復(fù)習(xí)
這里先復(fù)習(xí)一下本文所涉及到的知識點或概念:副本和分片
在 clickhouse 中用戶可以將所有節(jié)點按照自己的需求組成不同應(yīng)用場景的集群,可以將所有節(jié)點組成一個單一集群也可以劃分成多個小集群。副本和分片的區(qū)別在于:
- 從功能層面說:副本是防止數(shù)據(jù)丟失,增加數(shù)據(jù)冗余;分片是實現(xiàn)數(shù)據(jù)的水平切分,提高查詢效率
- 從數(shù)據(jù)層面說:副本之間的數(shù)據(jù)是完全相同的;分片之間的數(shù)據(jù)是完全不同的
clickhouse 目前支持副本的表引擎只有 MergeTree Family,其格式為 Replicated*MergeTree,同時副本表需要依賴 zookeeper 實現(xiàn)個節(jié)點數(shù)據(jù)同步的協(xié)同工作,這里配置集群、zookeeper以及復(fù)制表是如何工作的就不做過多介紹。集群的信息通過下面的命令查看
select * from system.clusters;
結(jié)果如下
創(chuàng)建復(fù)制表需要在原 MergeTree 表的基礎(chǔ)上額外指定 zookeeper 路徑(分片信息)和副本信息
engine = ReplicatedMergeTree(path:String, replica:String [, columns:any])
path一致表示它們是同一個分片,后面的 replica 標(biāo)記為不同的副本,通常 replica 填寫本機 ip 或主機名
下面創(chuàng)建一個 3 分片 2 副本的 MergeTree 表作為本次問題復(fù)現(xiàn)的 ODS 表
create table event_local on cluster cluster
(
ldtime Datetime,
ip IPv4
) engine = ReplicatedMergeTree('/clickhouse/tables/default/{shard}/event_local', '{replica}')
order by ldtime
partition by toYYYYMMDD(ldtime);
說明:
{shard}
和{replica}
是 clickhouse 的宏,在配置文件中指定的可以通過下面的 sql 查看
select *
from system.macros;
結(jié)果如下
同時 path 也存在一定的約束,通常是/clickhouse/tables/數(shù)據(jù)庫名稱/分片編號/表名
on cluster cluster
是標(biāo)記該 DDL 為分布式 DDL 且最后的 cluster 為集群名稱上面截圖所示,因此該 DDL 語句會被發(fā)送到 cluster 的集群中所有節(jié)點去執(zhí)行,省去了我們手動去各個節(jié)點執(zhí)行的步驟。同時該分布式 DDL 也會展示所有節(jié)點的響應(yīng),執(zhí)行 SQL 的入口節(jié)點會等待所有節(jié)點創(chuàng)建完將信息返回用戶(默認最大等待時間為 180s,超過后會轉(zhuǎn)入后臺進行)
當(dāng)然也可以使用 remote 查詢所有節(jié)點宏信息
select *
from remote('chi-settings-01-cluster-0-0', 'system', 'macros', 'username', 'password')
where macro in ('replica', 'shard')
union all
select *
from remote('chi-settings-01-cluster-0-1', 'system', 'macros', 'username', 'password')
where macro in ('replica', 'shard')
union all
select *
from remote('chi-settings-01-cluster-1-0', 'system', 'macros', 'username', 'password')
where macro in ('replica', 'shard')
union all
select *
from remote('chi-settings-01-cluster-1-1', 'system', 'macros', 'username', 'password')
where macro in ('replica', 'shard')
union all
select *
from remote('chi-settings-01-cluster-2-0', 'system', 'macros', 'username', 'password')
where macro in ('replica', 'shard')
union all
select *
from remote('chi-settings-01-cluster-2-1', 'system', 'macros', 'username', 'password')
where macro in ('replica', 'shard');
隨機向三個分片中寫入一定量的數(shù)據(jù),可以觀察副本數(shù)據(jù)的同步
# chi-settings-01-cluster-0-0
insert into event_local
values (toDateTime('2023-07-01 00:00:00'), '192.168.0.1'),
(toDateTime('2023-07-01 00:00:00'), '192.168.0.1'),
(toDateTime('2023-07-01 00:00:00'), '192.168.0.1'),
(toDateTime('2023-07-02 00:00:00'), '192.168.0.2');
# chi-settings-01-cluster-1-1
insert into event_local
values (toDateTime('2023-07-03 00:00:00'), '192.168.0.3'),
(toDateTime('2023-07-03 00:00:00'), '192.168.0.3'),
(toDateTime('2023-07-04 00:00:00'), '192.168.0.4');
# chi-settings-01-cluster-2-0
insert into event_local
values (toDateTime('2023-07-05 00:00:00'), '192.168.0.5'),
(toDateTime('2023-07-06 00:00:00'), '192.168.0.6');
根據(jù)分片的特性 event 全量數(shù)據(jù)應(yīng)該是各個分片的 compact 因此我們需要一張分布式表來代理所有的副本分片表
create table event_all
(
ldtime Datetime,
ip IPv4
) engine = Distributed('{cluster}', 'default', 'event_local', rand());
rand() 為分片健,當(dāng)數(shù)據(jù)通過分布式表寫入時,會根據(jù)分片健將數(shù)據(jù)寫入不同的分片中。分片健要求是一個整型數(shù)值,可以是表字段或返回整型的函數(shù)。
注:生產(chǎn)中分布式表通常只做查請求,不做寫請求。因為如果對分布式表執(zhí)行寫請求每條數(shù)據(jù)都需要在 clickhouse 中計算所屬分片效率不高,建議的做法是業(yè)務(wù)測做”分庫分表“將數(shù)據(jù)直接寫入對應(yīng)的本地表不讓 clickhouse 來做自動分片。
1.2 復(fù)現(xiàn)問題
對 event_all 查詢結(jié)果如下
select * from event_all order by ldtime;
2023-07-01 00:00:00,192.168.0.1
2023-07-01 00:00:00,192.168.0.1
2023-07-01 00:00:00,192.168.0.1
2023-07-02 00:00:00,192.168.0.2
2023-07-03 00:00:00,192.168.0.3
2023-07-03 00:00:00,192.168.0.3
2023-07-04 00:00:00,192.168.0.4
2023-07-05 00:00:00,192.168.0.5
2023-07-06 00:00:00,192.168.0.6
該 SQL 是執(zhí)行了分布式查詢,對于副本表 clickhouse 會按照一定策略讀取其中一個副本讀取數(shù)據(jù),具體的副本表讀寫流程不是這篇文章重點。下面開始復(fù)現(xiàn)問題
簡化一下需求,實時統(tǒng)計每個 ip 出現(xiàn)的個數(shù)。正常數(shù)倉可能會這么做
create materialized view mv_ip_count
engine = SummingMergeTree(num)
order by ip
populate
as
select ip, count() as num
from event_all
group by ip;
192.168.0.1,3
192.168.0.2,1
192.168.0.3,2
192.168.0.4,1
192.168.0.5,1
192.168.0.6,1
這樣的結(jié)果是沒有問題的,但是生產(chǎn)中不是所有的需求都可以這么做。因為之所以創(chuàng)建分布式表是因為數(shù)據(jù)量很大,需要做分片。那么對于處理后的數(shù)據(jù)量依然很大的需求我們就不能通過查詢分布式表來創(chuàng)建物化視圖,因為這樣做該物化視圖所有的數(shù)據(jù)都會存儲在該節(jié)點上。
為了解決這個問題就需要各個節(jié)點去統(tǒng)計自己存儲在本地的數(shù)據(jù),然后再創(chuàng)建一張分布式表對外提供統(tǒng)一的服務(wù),sql 如下
create materialized view mv_ip_count_local on cluster cluster
engine = SummingMergeTree(num)
order by ip
populate
as
select ip, count() as num
from event_local
group by ip;
create table mv_ip_count_all
(
ip IPv4,
num UInt64
) engine = Distributed('{cluster}', 'default', 'mv_ip_count_local', rand());
當(dāng)我們用 mv_ip_count_all 進行二次聚合時
select sum(num) from mv_ip_count_all;
其值在 4、3、2 間反復(fù)橫跳,正確答案應(yīng)該是 9。這就很納悶了
二、問題解決
這種情況定位出來就是觸發(fā)了本地查詢,反復(fù)橫跳的數(shù)據(jù)是各個節(jié)點本地查詢結(jié)果。產(chǎn)生這個問題的原因是下面這個 sql
create materialized view mv_ip_count_local on cluster cluster
engine = SummingMergeTree(num)
order by ip
populate
as
select ip, count() as num
from event_local
group by ip;
首先這個 sql 會被分布式執(zhí)行,但是 event_local 它是一個副本表,該 sql 執(zhí)行結(jié)束后每個節(jié)點都會有一張毫無關(guān)聯(lián)的 SummingMergeTree 表,此時我們又創(chuàng)建了分布式表來代理,造成的后果就是分布式表的數(shù)據(jù)量暴增 n 倍,n 為副本個數(shù)。
當(dāng)我們基于分布式表做二次聚合時,clickhouse 或許也發(fā)現(xiàn)了這個問題,如果按照正常的 mpp 架構(gòu)來執(zhí)行這個 sql 得到的結(jié)果就是正確個數(shù)的 n 倍,顯然這樣錯誤的結(jié)果是 clickhouse 無法接受的,因此它將分布式 sql 降級為本地 sql 如文章開頭的圖二流程。
或許 clickhouse 覺得這樣的錯誤結(jié)果比 mpp 流程得到的結(jié)果要更容易接受吧,雖然這兩個結(jié)果都存在問題
分析到這里解決方案也就浮出水面了,就是讓 SummingMergeTree 保持和 event_local 一樣的副本分片關(guān)系即可,即創(chuàng)建具有副本功能的 SummingMergeTree 表
create materialized view mv_ip_count_local
on cluster cluster
engine = ReplicatedSummingMergeTree('/clickhouse/tables/default/{shard}/mv_ip_count_local', '{replica}', num)
order by ip
as
select ip, count() as num
from event_local
group by ip;
分布式表正常創(chuàng)建即可,最終問題解決。文章來源:http://www.zghlxwxcb.cn/news/detail-621773.html
三、總結(jié)
出現(xiàn)這個問題本質(zhì)上是對 clickhouse 的副本分片表、分布式表不熟練,同時 clickhouse 在賦予用戶極高自由度的同時也給用戶帶來了很多心智負擔(dān)。clickhouse 分布式表還存在很多錯誤的使用方式,例如基于分布式表做聚合或連接操作時存在大坑,這點后面有時間可以單獨出一篇博客來說明。雖然在使用過程中產(chǎn)生過很多匪夷所思的結(jié)果,但當(dāng)分析完原因并成功解決后又會覺得 clickhouse 這么做很合理,它依然是一款極其優(yōu)秀且強大的 olap 數(shù)據(jù)庫文章來源地址http://www.zghlxwxcb.cn/news/detail-621773.html
到了這里,關(guān)于clickhouse分布式查詢降級為本地查詢的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!