Replication & Sharding
在ClickHouse文集的第一篇文章中,筆者介紹了ClickHouse高可用集群的配置方法,并且提到:分布式存儲要保證高可用,就必須有數據冗余——即副本(replica)。ClickHouse依靠ReplicatedMergeTree引擎族與ZooKeeper實現了復制表機制,成為其高可用的基礎。
另外,筆者也提到,ClickHouse像ElasticSearch一樣具有數據分片(shard)的概念,這也是分布式存儲的特點之一,即通過并行讀寫提高效率。ClickHouse依靠Distributed引擎實現了分布式表機制,在所有分片(本地表)上建立視圖進行分布式查詢,使用很方便。
在實際操作中,為了最大化性能與穩定性,分片和副本幾乎總是一同使用。
本文就對ClickHouse的復制表、分布式表機制和用法作個介紹。
Replicated Table & ReplicatedMergeTree Engines
ClickHouse的副本機制之所以叫“復制表”,是因為它工作在表級別,而不是集群級別(如HDFS)。也就是說,用戶在創建表時可以通過指定引擎選擇該表是否高可用,每張表的分片與副本都是互相獨立的。
目前支持復制表的引擎是ReplicatedMergeTree引擎族,它與平時最常用的MergeTree引擎族是正交的,如下圖所示。
下面給出ReplicatedMergeTree引擎的完整建表DDL語句。
CREATE TABLE IF NOT EXISTS test.events_local ON CLUSTER '{cluster}' (
ts_date Date,
ts_date_time DateTime,
user_id Int64,
event_type String,
site_id Int64,
groupon_id Int64,
category_id Int64,
merchandise_id Int64,
search_text String
-- A lot more columns...
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/test/events_local','{replica}')
PARTITION BY ts_date
ORDER BY (ts_date_time,site_id,event_type)
SETTINGS index_granularity = 8192;
其中,ON CLUSTER
語法表示分布式DDL,即執行一次就可在集群所有實例上創建同樣的本地表。集群標識符{cluster}
、分片標識符{shard}
和副本標識符{replica}
來自之前提到過的復制表宏配置,即config.xml中<macros>一節的內容,配合ON CLUSTER
語法一同使用,可以避免建表時在每個實例上反復修改這些值。
ReplicatedMergeTree引擎族接收兩個參數:
- ZK中該表相關數據的存儲路徑,ClickHouse官方建議規范化,如上面的格式
/clickhouse/tables/{shard}/[database_name]/[table_name]
。 - 副本名稱,一般用
{replica}
即可。
觀察一下上述ZK路徑下的znode結構與內容。
[zk: localhost:2181(CONNECTED) 0] ls /clickhouse/tables/01/test/events_local
[metadata, temp, mutations, log, leader_election, columns, blocks, nonincrement_block_numbers, replicas, quorum, block_numbers]
[zk: localhost:2181(CONNECTED) 1] get /clickhouse/tables/04/test/events_local/columns
columns format version: 1
9 columns:
`ts_date` Date
`ts_date_time` DateTime
`user_id` Int64
`event_type` String
`site_id` Int64
`groupon_id` Int64
`category_id` Int64
`merchandise_id` Int64
`search_text` String
# ...................
[zk: localhost:2181(CONNECTED) 2] get /clickhouse/tables/07/test/events_local/metadata
metadata format version: 1
date column:
sampling expression:
index granularity: 8192
mode: 0
sign column:
primary key: ts_date_time, site_id, event_type
data format version: 1
partition key: ts_date
granularity bytes: 10485760
# ...................
ReplicatedMergeTree引擎族在ZK中存儲大量數據,包括且不限于表結構信息、元數據、操作日志、副本狀態、數據塊校驗值、數據part merge過程中的選主信息等等。可見,ZK在復制表機制下扮演了元數據存儲、日志框架、分布式協調服務三重角色,任務很重,所以需要額外保證ZK集群的可用性以及資源(尤其是硬盤資源)。
下圖大致示出復制表執行插入操作時的流程(internal_replication
配置項為true)。即先寫入一個副本,再通過config.xml中配置的interserver HTTP port端口(默認是9009)將數據復制到其他實例上去,同時更新ZK集群上記錄的信息。
Distributed Table & Distributed Engine
ClickHouse分布式表的本質并不是一張表,而是一些本地物理表(分片)的分布式視圖,本身并不存儲數據。
支持分布式表的引擎是Distributed,建表DDL語句示例如下,_all
只是分布式表名比較通用的后綴而已。
CREATE TABLE IF NOT EXISTS test.events_all ON CLUSTER sht_ck_cluster_1
AS test.events_local
ENGINE = Distributed(sht_ck_cluster_1,test,events_local,rand());
Distributed引擎需要以下幾個參數:
- 集群標識符
注意不是復制表宏中的標識符,而是<remote_servers>中指定的那個。 - 本地表所在的數據庫名稱
- 本地表名稱
- (可選的)分片鍵(sharding key)
該鍵與config.xml中配置的分片權重(weight)一同決定寫入分布式表時的路由,即數據最終落到哪個物理表上。它可以是表中一列的原始數據(如site_id
),也可以是函數調用的結果,如上面的SQL語句采用了隨機值rand()
。注意該鍵要盡量保證數據均勻分布,另外一個常用的操作是采用區分度較高的列的哈希值,如intHash64(user_id)
。
在分布式表上執行查詢的流程簡圖如下所示。發出查詢后,各個實例之間會交換自己持有的分片的表數據,最終匯總到同一個實例上返回給用戶。
而在寫入時,我們有兩種選擇:一是寫分布式表,二是寫underlying的本地表。孰優孰劣呢?
直接寫分布式表的優點自然是可以讓ClickHouse控制數據到分片的路由,缺點就多一些:
- 數據是先寫到一個分布式表的實例中并緩存起來,再逐漸分發到各個分片上去,實際是雙寫了數據(寫入放大),浪費資源;
- 數據寫入默認是異步的,短時間內可能造成不一致;
- 目標表中會產生較多的小parts,使merge(即compaction)過程壓力增大。
相對而言,直接寫本地表是同步操作,更快,parts的大小也比較合適,但是就要求應用層額外實現sharding和路由邏輯,如輪詢或者隨機等。
應用層路由并不是什么難事,所以如果條件允許,在生產環境中總是推薦寫本地表、讀分布式表。舉個例子,在筆者最近引入的Flink-ClickHouse Sink連接器中,就采用了隨機路由,部分代碼如下。
private Request buildRequest(ClickhouseRequestBlank requestBlank) {
String resultCSV = String.join(" , ", requestBlank.getValues());
String query = String.format("INSERT INTO %s VALUES %s", requestBlank.getTargetTable(), resultCSV);
String host = sinkSettings.getClickhouseClusterSettings().getRandomHostUrl();
BoundRequestBuilder builder = asyncHttpClient
.preparePost(host)
.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=utf-8")
.setBody(query);
if (sinkSettings.getClickhouseClusterSettings().isAuthorizationRequired()) {
builder.setHeader(HttpHeaders.Names.AUTHORIZATION, "Basic " + sinkSettings.getClickhouseClusterSettings().getCredentials());
}
return builder.build();
}
public String getRandomHostUrl() {
currentHostId = ThreadLocalRandom.current().nextInt(hostsWithPorts.size());
return hostsWithPorts.get(currentHostId);
}
The End
ClickHouse確實是一個設計很精妙的OLAP數據庫,還有很多細節等著我們在實際應用中去發掘。
民那晚安晚安。