clickhouse是當下最流行的OLAP產品,我總結其代表能力體現為:
- 存儲數據:與presto等直接讀取外部數據進行計算的方式不同,clickhouse大部分情況是冗余存儲一份數據的,所以clickhouse需要提供多種數據集成的方案與生態。
- 即席查詢:典型ad-hoc產品,海量數據秒出數據,計算能力可以擴充計算節點實現,可以用作實時數倉(其他常見方案是hbase->hive->presto)。
在學習過程中,我發現clickhouse架構的自動化程度比較有限(可能還需要一段時間的發展才能成熟),需要用戶理解的概念比較龐雜,屬於入門門檻挺高的一款產品,所以我會嘗試通過幾篇博客來拎清這些概念之間的關系,幫助大家克服學習困難。
本篇文章我們完成2個任務:
- 搭建只包含1個node的clickhouse集群,雖然只有1個節點但是我們會按照分布式集群的搭建原理來展示這個過程,擴展到多個節點也是毫無問題的。
- 通過1個簡單案例快速理解clickhouse的建表語句、插入語句、查詢語句,重點理解clickhouse本地表與分布式表的架構關系。
安裝clickhouse
根據官方文檔(中文)利用apt/yum安裝即可:https://clickhouse.tech/docs/zh/getting-started/install/,安裝完利用systemctl工具管理它:
(注:apt/yum安裝過程中會讓你輸入clickhouse默認密碼,大家有可能敲錯密碼,沒關系后面會帶大家重置)
安裝zookeeper
clickhouse集群架構方案非常的”原始“,簡單說一個集群邏輯上划分為多個sharding,每個sharding內的數據為做replica副本。

zookeeper在集群中做2件事情:
- 將DDL(create/alter…)操作先寫入到ZK,然后通過ZK觸發生效到所有Node。
- 以Shard0為例,寫入可以發生在Replica0和Replica1的任意實例上,承載客戶端寫入的node會向ZK寫入新數據塊的信息,從而通知sharding內其他replica根據ZK信息來同步這份數據。
DDL與ZK的關系我們展開解釋:
- 本地DDL:用客戶端直連某個node提交DDL建表語句,默認情況下這個表只在這個node可見,其他Node不可見,這意味着其實clickhouse沒有全局的元數據管理,這是很奇怪的一點。
- 分布式DDL:為了讓某個DDL操作生效到所有node,執行DDL時需要指定為分布式類型,則DDL語句先寫入ZK,通過ZK觸發集群中所有Node在自己的本地執行DDL。
Replica與ZK的關系我們展開解釋:
- 同Shard內的多個Replica沒有主次關系,每一個都可以寫入,數據互相同步。
- 同步是依靠ZK通知的,因此每次寫入數據都要同時寫ZK,所以ZK會成為高頻寫入的瓶頸,這也是clickhouse要求應用批量寫入的重要原因。(另外,clickhouse允許為不同的表使用不同的ZK集群進行寫入,這樣可以解決ZK瓶頸問題)
我們下載ZK:https://zookeeper.apache.org/releases.html。
拷貝conf/zoo_sample.cfg到conf/zoo.cfg,修改一下里面的zk數據存儲目錄即可(具體路徑大家自行指定):
dataDir=/root/clickhouse/apache-zookeeper-3.7.0-bin/data
然后啟動ZK:
bin/zkServer.sh start
配置clickhouse
clickhouse配置文件所在位置:
|
1
2
3
4
5
6
|
root@debian:~/clickhouse/apache-zookeeper-3.7.0-bin# ll /etc/clickhouse-server/
總用量
72
dr-x------ 2 clickhouse clickhouse 4096 7月 1 10:37 config.d
-r-------- 1 clickhouse clickhouse 55925 6月 30 11:03 config.xml
dr-x------ 2 clickhouse clickhouse 4096 7月 1 11:03 users.d
-r-------- 1 clickhouse clickhouse 6148 7月 1 10:53 users.xml
|
config.xml是配置主文件,config.d下面的xml會被合並到config.xml結構中,我們應該去config.d配置xml片段,而不應直接修改config.xml,這樣管理起來更加清晰方便。
users.xml是用戶配置文件,users.d下面的xml會被合並到users.xml中,我們同樣應該去users.d下面配置xml片段。
配置default用戶密碼
默認clickhouse用default用戶登陸,密碼在apt/yum安裝中可能誤敲,所以這里我們重置一下。
編輯users.d/default-password.xml,設置上自己的密碼為123:
|
1
2
3
4
5
6
7
|
<yandex>
<users>
<default>
<password>123</password>
</default>
</users>
</yandex>
|
default-password.xml中的XML結構會合並到users.xml中,大家自己看一下就懂了。
配置zookeeper地址
編輯config.d/zk.xml,填入zk地址:
|
1
2
3
4
5
6
7
8
|
<yandex>
<zookeeper>
<node>
<host>localhost</host>
<port>2181</port>
</node>
</zookeeper>
</yandex>
|
這個XML結構會合並到config.xml的XML結構中。
配置cluster集群
clickhouse的集群概念比較”原始“,我們需要區分看待”物理集群“和”邏輯集群“的差異。
- 首先clickhouse是若干等價的物理機組成的”物理集群“,這些物理機之間彼此並不認識。
- 其次clickhouse在”物理集群“之上,可以通過config.xml配置出一個”邏輯集群“,讓這些節點彼此認識對方。
一套”物理集群“理論上可以配置N套”邏輯集群“,每套”邏輯集群“可以由不同的node組成,選擇不同的sharding和replica策略,比如:
- 配置cluster1,它用node1和node2兩個節點組成1個shard的2個replica。
- 配置cluster2,它用node1和node2成為shard0,用node3和node4成為shard1,然后node1和node2互為replica,node3和node4互為replica。
有了多個可選的”邏輯集群“,我們在建庫建表的時候就可以指定采用哪個cluster,從而決定分片個數和副本個數,滿足不同的可靠性和性能需求。
出於簡單考慮,我們就做一套標准的邏輯集群即可,編輯config.d/cluster.xml:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<yandex>
<remote_servers>
<mycluster>
<shard>
<replica>
<host>127.0.0.1</host>
<port>9000</port>
</replica>
</shard>
</mycluster>
</remote_servers>
</yandex>
~
|
這里配置了一個mycluster邏輯集群,因為我們就1個node,所以就配置成1個shard的單replica集群了,9000端口是clickhouse的內部通訊端口。
如果我們有多台node,這樣的cluster.xml配置文件需要分發到所有node,讓大家互相認識彼此。
配置macro宏
除了用cluster.xml來配置邏輯集群的構成結構之外,我們還需要為每個node分發不同的身份標識,這對於clickhouse進行數據分片和副本復制是至關重要的。
編輯config.d/macro.xml,為我們唯一的node寫入它在邏輯集群mycluster中的身份:
|
1
2
3
4
5
6
|
<yandex>
<macros>
<shard>shard-0</shard>
<replica>replica-0</replica>
</macros>
</yandex>
|
連接clickhouse
現在重啟clickhouse讓上述配置生效:
systemctl restart clickhouse-server
然后命令行訪問:
clickhouse-client –host 127.0.0.1 –password 123
大部分命令和mysql一樣,大家可以自行體驗一下show database;use等語句。
實踐clickhouse分布式表
clickhouse的SQL語句默認情況下都是單機本地的,除非我們顯式指定生效到cluster的所有Node,否則是不會通過ZK通知到集群所有Node的。
大部分情況我們肯定希望庫表是分布式的,具備分片並行能力,具備分片內的多replica冗余能力,否則單點故障數據丟了誰也受不起。
因此,下面我們就來創建一套分布式的數據庫表,並完成分布式的寫入和查詢(意味着數據自動分布到多sharding進行寫入和計算)。
創建database
CREATE DATABASE IF NOT EXISTS db1 ON CLUSTER mycluster
這是一條DDL操作,因為我們帶了ON CLUSTER mycluster,所以會通過ZK下發到mycluster中的每一個node(雖然我們只部署了1個node),這叫做一次分布式DDL:

返回的信息包含了這次分布式DDL影響到的所有node,這里127.0.0.1:9000就是我們當前客戶端所連接的node。
我們在節點127.0.0.1:9000上執行show databases,看到的是本地的database列表,因為剛才是分布式DDL所以本地也有了這個database,分布式DDL和本地database出現之間的關系需要大家理解明白,clickhouse沒有全局的元數據管理,只是將操作扇出到所有node都做了一遍而已,元數據在每台node自己本地。

我們看到database的engine叫做atomic,其大概含義就是能夠支持對該database下面的table進行原子性的rename之類的操作,是默認的database引擎,了解即可。
創建本地table(向集群中每個node)
接下來,我們要在mycluster的所有node上創建同樣的table,並且讓同一個sharding內的replica之間互相復制,這個就要求我們創建一個帶復制能力的table:
|
1
2
3
4
5
6
|
CREATE TABLE user ON CLUSTER mycluster
(
id Int64,
name String
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/db1/user', '{replica}')
ORDER BY (id);
|
ReplicatedMergeTree是帶有復制能力的MergeTree(LSM樹)引擎,兩個參數是指定ZK中的路徑用途。
填充符{shard}和{replica}是每個node上marco.xml決定的,因此這個分布式DDL建表語句通過ZK推到各個node之后,各個node本地執行時都會用各自的值填充進去。
總結一下,每個node都有自己獨立的ZK路徑可以寫入,當向user表某個node寫入數據時,該node查看本地表中/clickhouse/tables/{shard}/db1/user和{replica}的實際值,然后向ZK的/clickhouse/tables/{shard}/db1/user/replicas/{replica}下面寫入自己的新數據塊信息,這樣同shard下的其他replica就會監聽到該路徑的變化,取出新數據塊的元信息,然后調用寫入node接口來拉取新數據塊本地,這就完成了數據復制。
我們可以看一下ZK的路徑關系就懂了:

至於ORDER BY(id)是指user表的數據文件中按id字段排序存儲並生成索引文件,這樣如果查找id的話走索引二分查找就會很快,其他查詢條件則掃表計算,這是clickhouse主要的查詢原理。(可以把ORDER BY暫時理解為主鍵PRIMARY KEY,但其實只是為了加速查詢用的,並沒有去重功能)
創建分布式table(向集群中每個node)
當集群中每個node都有了這個table定義之后,我們希望插入數據時能負載均衡數據到各個node,查詢時能自動從各個node聚合結果,這時候就需要配置一個分布式table,就有點像反向代理一樣。
分布式表的特性是:
- 寫入時會按某字段打散,這樣數據可以均衡到各個shard。
- 讀取時會從所有shard讀取,然后聚合結果。
因為讀取是所有shard都讀,所以寫入時即便是隨機打散也沒有關系,clickhouse分布式表會完成多shard的二次聚合,保證統計結果正確。
所以,我們就建一個隨機打散的分布式表,這樣數據最均衡:
CREATE TABLE db1.dis_user ON CLUSTER mycluster AS db1.user ENGINE = Distributed(mycluster, db1, user, rand());
這個語句的意思是創建一個Distributed引擎的表,這個表通過ON CLUSTER mycluster語法會推送到所有node上創建,因此后續我們無論訪問哪個node都可以訪問到這個分布式表dis_user。
AS db1.user表示和db1.user表結構一致。
同時,Distributed(mycluster, db1, user, rand())指明了這個分布式表背后的本地表是mycluster集群的db1數據庫的user表,寫入時通過rand()隨機打散寫入到各個shard。

創建成功后列出了其背后各個node,我們只有1台。
插入數據到分布式表
連接任意clickhouse節點,執行SQL:
insert into db1.dis_user values(1,’hello’);
insert into dis_user values(2,’goodbye’);
然后查詢:
select * from dis_user;
可以看到數據:

查詢分布式表
執行SQL:
select count(*) from dis_user;
返回:

再次執行:
insert into dis_user values(2,’building’);
你會發現插入成功了,也就是說雖然id是主鍵,但是id=2的重復記錄還是能插入成功:

實際上clickhouse的MergeTree引擎是類似LSM的數據庫,滿足高吞吐寫入,但根據網上說法clickhouse的刪除和更新操作並不能通過LSM追加的方式實現,具體原因沒細看。
總之,
- clickhouse默認MergeTree引擎是沒有主鍵去重功能的,主鍵只是為了加速查詢索引用的。
- 不建議使用UPDATE更新和DELETE刪除,因為這些操作對clickhouse實現來說性能很差,應該通過追加新記錄的方式實現偽更新和偽刪除,在這篇博客就不展開了,免得大家糊塗,在下一篇博客我會通過一個案例講一下clickhouse的這塊思想。
總結
本文關鍵是理解如何通過ON CLUSTER關鍵字完成分布式DDL,記住clickhouse沒有全局元信息,因此所有的DDL操作都應該分發到每個Node上執行。
通過在所有node上創建一份分布式表的方式,可以確保客戶端訪問任意節點均能訪問到該分布式表,並且該分布式表能夠代理完成對所有node上本地表的查詢和寫入負載。
最后,我們應該也意識到了clickhouse在分布式架構上的簡陋性,對其底層LSM模型有個基本的了解。
