ClickHouse復制表、分布式表機制與使用方法

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數據庫,還有很多細節等著我們在實際應用中去發掘。

民那晚安晚安。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。