Clickhouse集羣應用、分片、複製

簡介

通常生產環境我們會用集羣代替單機,主要是解決兩個問題:

  • 效率
  • 穩定

如何提升效率?一個大大大任務,讓一個人幹需要一年,拆解一下讓12個人同時幹,可能只需要1個月。對於數據庫來說,就是數據分片。

如何提升穩定性?所謂穩定就是要保證服務時刻都能用,也常說高可用。這就像團隊裏必須有二把手,老大有事不在,老二要能頂上。對於數據庫來說,就是數據備份。

而集羣是解決這兩個問題的最佳手段。話說,三個臭皮匠,賽過諸葛亮,這就是團隊的力量。

幾乎所有大數據相關的產品,基本都是以這兩個問題爲出發點,Clickhouse也不例外。

不同的是,Hadoop系列的集羣是服務級別的,而Clickhouse的集羣是表上的。例如,一個hdfs集羣,所有文件都會切片、備份;而clickhouse集羣中,建表時也可以自己決定用不用。習慣了其他大數據產品的人,剛轉到clickhouse會感覺這設計太反人類,後文會詳細介紹。

安裝

我們使用三臺機器演示,三個機器分別安裝clickhouse
教程:https://www.jianshu.com/p/5f5ee0904bba

數據

實驗使用到官方提供的OnTime數據集,先下載下來,並按照文檔建表。

教程: https://clickhouse.yandex/docs/en/single/?query=internal_replication#ontime

數據分片

這裏再說明一下,分片是爲了提高效率。

分片,就像是把雞蛋放到多個籃子裏,降低整體風險,結果可能是部分數據不可用,雖然一定程度上起到了「高可用」的作用,但分片的目的是爲了提速。況且,比較嚴格的場景下,部分不可用也是不可用。

clickhouse需要自己動手定義分片。

 

vim /etc/clickhouse-server/config.xml

編輯config.xml文件,搜索remote_servers:

 

<remote_servers incl="clickhouse_remote_servers" >
    ...
</remote_servers>

說明:remote_servers就是集羣配置,可以直接在此處配置,也可以提出來配置到擴展文件中。incl屬性表示可從外部文件中獲取節點名爲clickhouse_remote_servers的配置內容。

我們使用擴展文件,首先,添加外部擴展配置文件:

 

<include_from>/etc/clickhouse-server/metrika.xml</include_from>

然後,編輯配置文件:

 

vim /etc/clickhouse-server/metrika.xml

添加內容:

 

<yandex>
<!-- 集羣配置 -->
<clickhouse_remote_servers>
    <!-- 3分片1備份 -->
    <cluster_3shards_1replicas>
        <!-- 數據分片1  -->
        <shard>
            <replica>
                <host>hadoop1</host>
                <port>9000</port>
            </replica>
        </shard>
        <!-- 數據分片2  -->
        <shard>
            <replica>
                <host>hadoop2</host>
                <port> 9000</port>
            </replica>
        </shard>
        <!-- 數據分片3  -->
        <shard>
            <replica>
                <host>hadoop3</host>
                <port>9000</port>
            </replica>
        </shard>
    </cluster_3shards_1replicas>
</clickhouse_remote_servers>
</yandex>

說明:

  • clickhouse_remote_servers與config.xml中的incl屬性值對應;
  • cluster_3shards_1replicas是集羣名,可以隨便取名;
  • 共設置3個分片,每個分片只有1個副本;

在其他兩臺機器上同樣操作。

打開clickhouse-client,查看集羣:

 

hadoop1 :) select * from system.clusters;

SELECT *
FROM system.clusters

┌─cluster───────────────────┬─shard_num─┬─shard_weight─┬─replica_num─┬─host_name─┬─host_address─┬─port─┬─is_local─┬─user────┬─default_database─┐
│ cluster_3shards_1replicas │         1 │            1 │           1 │ hadoop1   │ 192.168.0.6  │ 9000 │        1 │ default │                  │
│ cluster_3shards_1replicas │         2 │            1 │           1 │ hadoop2   │ 192.168.0.16 │ 9000 │        0 │ default │                  │
│ cluster_3shards_1replicas │         3 │            1 │           1 │ hadoop3   │ 192.168.0.11 │ 9000 │        0 │ default │                  │
└───────────────────────────┴───────────┴──────────────┴─────────────┴───────────┴──────────────┴──────┴──────────┴─────────┴──────────────────┘

3 rows in set. Elapsed: 0.003 sec.

可以看到cluster_3shards_1replicas就是我們定義的集羣名稱,一共有三個分片,每個分片有一份數據。

建數據表

在三個節點上分別建表:ontime_local

 

CREATE TABLE `ontime_local` (
  `Year` UInt16,
  `Quarter` UInt8,
  `Month` UInt8,
  `DayofMonth` UInt8,
  `DayOfWeek` UInt8,
  `FlightDate` Date,
  `UniqueCarrier` FixedString(7),
  `AirlineID` Int32,
  `Carrier` FixedString(2),
  `TailNum` String,
  `FlightNum` String,
  `OriginAirportID` Int32,
  `OriginAirportSeqID` Int32,
  `OriginCityMarketID` Int32,
  `Origin` FixedString(5),
  `OriginCityName` String,
  `OriginState` FixedString(2),
  `OriginStateFips` String,
  `OriginStateName` String,
  `OriginWac` Int32,
  `DestAirportID` Int32,
  `DestAirportSeqID` Int32,
  `DestCityMarketID` Int32,
  `Dest` FixedString(5),
  `DestCityName` String,
  `DestState` FixedString(2),
  `DestStateFips` String,
  `DestStateName` String,
  `DestWac` Int32,
  `CRSDepTime` Int32,
  `DepTime` Int32,
  `DepDelay` Int32,
  `DepDelayMinutes` Int32,
  `DepDel15` Int32,
  `DepartureDelayGroups` String,
  `DepTimeBlk` String,
  `TaxiOut` Int32,
  `WheelsOff` Int32,
  `WheelsOn` Int32,
  `TaxiIn` Int32,
  `CRSArrTime` Int32,
  `ArrTime` Int32,
  `ArrDelay` Int32,
  `ArrDelayMinutes` Int32,
  `ArrDel15` Int32,
  `ArrivalDelayGroups` Int32,
  `ArrTimeBlk` String,
  `Cancelled` UInt8,
  `CancellationCode` FixedString(1),
  `Diverted` UInt8,
  `CRSElapsedTime` Int32,
  `ActualElapsedTime` Int32,
  `AirTime` Int32,
  `Flights` Int32,
  `Distance` Int32,
  `DistanceGroup` UInt8,
  `CarrierDelay` Int32,
  `WeatherDelay` Int32,
  `NASDelay` Int32,
  `SecurityDelay` Int32,
  `LateAircraftDelay` Int32,
  `FirstDepTime` String,
  `TotalAddGTime` String,
  `LongestAddGTime` String,
  `DivAirportLandings` String,
  `DivReachedDest` String,
  `DivActualElapsedTime` String,
  `DivArrDelay` String,
  `DivDistance` String,
  `Div1Airport` String,
  `Div1AirportID` Int32,
  `Div1AirportSeqID` Int32,
  `Div1WheelsOn` String,
  `Div1TotalGTime` String,
  `Div1LongestGTime` String,
  `Div1WheelsOff` String,
  `Div1TailNum` String,
  `Div2Airport` String,
  `Div2AirportID` Int32,
  `Div2AirportSeqID` Int32,
  `Div2WheelsOn` String,
  `Div2TotalGTime` String,
  `Div2LongestGTime` String,
  `Div2WheelsOff` String,
  `Div2TailNum` String,
  `Div3Airport` String,
  `Div3AirportID` Int32,
  `Div3AirportSeqID` Int32,
  `Div3WheelsOn` String,
  `Div3TotalGTime` String,
  `Div3LongestGTime` String,
  `Div3WheelsOff` String,
  `Div3TailNum` String,
  `Div4Airport` String,
  `Div4AirportID` Int32,
  `Div4AirportSeqID` Int32,
  `Div4WheelsOn` String,
  `Div4TotalGTime` String,
  `Div4LongestGTime` String,
  `Div4WheelsOff` String,
  `Div4TailNum` String,
  `Div5Airport` String,
  `Div5AirportID` Int32,
  `Div5AirportSeqID` Int32,
  `Div5WheelsOn` String,
  `Div5TotalGTime` String,
  `Div5LongestGTime` String,
  `Div5WheelsOff` String,
  `Div5TailNum` String
) ENGINE = MergeTree(FlightDate, (Year, FlightDate), 8192)

表結構與ontime完全一樣。

建分佈表

 

CREATE TABLE ontime_all AS ontime_local
ENGINE = Distributed(cluster_3shards_1replicas, default, ontime_local, rand())

分佈表(Distributed)本身不存儲數據,相當於路由,需要指定集羣名、數據庫名、數據表名、分片KEY,這裏分片用rand()函數,表示隨機分片。查詢分佈表,會根據集羣配置信息,路由到具體的數據表,再把結果進行合併。

ontime_all與ontime在同一個節點上,方便插入數據。

插入數據

 

INSERT INTO ontime_all SELECT * FROM ontime;

把ontime的數據插入到ontime_all,ontime_all會隨機插入到三個節點的ontime_local裏。

表ontime需要提前建好,並導入數據。

導入完成後,查看總數據量:

 

hadoop1 :) select count(1) from ontime_all;

SELECT count(1)
FROM ontime_all

┌──count(1)─┐
│ 177920306 │
└───────────┘

再看下每個節點的數據:

 

hadoop1 :) select count(1) from ontime_local;

SELECT count(1)
FROM ontime_local

┌─count(1)─┐
│ 59314494 │
└──────────┘

可以看到,每個節點大概有1/3的數據。

性能對比

對比一下分片與不分片的性能差異。

不分片:

 

hadoop1 :) select Carrier, count() as c, round(quantileTDigest(0.99)(DepDelay), 2) as q from ontime group by Carrier order by q desc limit 5;

SELECT
    Carrier,
    count() AS c,
    round(quantileTDigest(0.99)(DepDelay), 2) AS q
FROM ontime
GROUP BY Carrier
ORDER BY q DESC
LIMIT 5

┌─Carrier─┬───────c─┬──────q─┐
│ B6      │ 2991782 │ 191.22 │
│ NK      │  412396 │ 190.97 │
│ EV      │ 6222018 │ 187.17 │
│ XE      │ 2145095 │ 179.55 │
│ VX      │  371390 │  178.3 │
└─────────┴─────────┴────────┘

5 rows in set. Elapsed: 1.951 sec. Processed 177.92 million rows, 1.07 GB (91.18 million rows/s., 547.11 MB/s.)

用時1.951秒。

分片:

 

hadoop1 :) select Carrier, count() as c, round(quantileTDigest(0.99)(DepDelay), 2) as q from ontime_all group by Carrier order by q desc limit 5;

SELECT
    Carrier,
    count() AS c,
    round(quantileTDigest(0.99)(DepDelay), 2) AS q
FROM ontime_all
GROUP BY Carrier
ORDER BY q DESC
LIMIT 5

┌─Carrier─┬───────c─┬──────q─┐
│ B6      │ 2991782 │  191.2 │
│ NK      │  412396 │ 191.06 │
│ EV      │ 6222018 │ 187.22 │
│ XE      │ 2145095 │ 179.42 │
│ VX      │  371390 │ 178.33 │
└─────────┴─────────┴────────┘

5 rows in set. Elapsed: 0.960 sec. Processed 177.92 million rows, 1.07 GB (185.37 million rows/s., 1.11 GB/s.)

用時0.96秒,速度大約提升2倍。

現在,停掉一個節點,會是神馬情況?

 

Received exception from server (version 18.10.3):
Code: 279. DB::Exception: Received from localhost:9000, 127.0.0.1. DB::NetException. DB::NetException: All connection tries failed. Log:

Code: 32, e.displayText() = DB::Exception: Attempt to read after eof, e.what() = DB::Exception
Code: 210, e.displayText() = DB::NetException: Connection refused: (hadoop2:9000, 192.168.0.16), e.what() = DB::NetException
Code: 210, e.displayText() = DB::NetException: Connection refused: (hadoop2:9000, 192.168.0.16), e.what() = DB::NetException

報錯了,看來clickhouse的處理很嚴格,如果一個分片不可用,就整個分佈式表都不可用了。

當然,此時如果查本地表ontime_local還是可以的。

那麼,如何解決整個問題呢?這就是前文所說的穩定性問題了,解決方案是:數據備份!

數據備份

說明一點,數據備份與分片沒有必然聯繫,這是兩個方面的問題。但在clickhouse中,replica是掛在shard上的,因此要用多副本,必須先定義shard。

最簡單的情況:1個分片多個副本。

添加集羣

像之前一樣,再配置一個集羣,叫做cluster_1shards_2replicas,表示1分片2副本,配置信息如下:

 

<yandex>
        <!-- 1分片2備份 -->
        <cluster_1shards_2replicas>
            <shard>
                <internal_replication>false</internal_replication>
                <replica>
                    <host>hadoop1</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>hadoop2</host>
                    <port>9000</port>
                </replica>
            </shard>
        </cluster_1shards_2replicas>
</yandex>

注意,如果配置文件沒有問題,是不用重啓clickhouse-server的,會自動加載!

建數據表

 

CREATE TABLE `ontime_local_2` (
  ...
) ENGINE = MergeTree(FlightDate, (Year, FlightDate), 8192)

表名爲ontime_local_2,在三臺機器分別執行。

建分佈表

 

CREATE TABLE ontime_all_2 AS ontime_local_2
ENGINE = Distributed(cluster_1shards_2replicas, default, ontime_local_2, rand())

表名是ontime_all_2,使用cluster_1shards_2replicas集羣,數據爲ontime_local_2。

導入數據

 

INSERT INTO ontime_all_2 SELECT * FROM ontime

查詢

 

hadoop1 :) select count(1) from ontime_all_2;

SELECT count(1)
FROM ontime_all_2

┌──count(1)─┐
│ 177920306 │
└───────────┘

查詢ontime_local_2,兩個節點都有全量數據。

關掉一個服務器,仍能查詢全量數據,數據副本已經生效。

一致性

既然有多副本,就有個一致性的問題:加入寫入數據時,掛掉一臺機器,會怎樣?

我們來模擬一下:

  1. 停掉hadoop2服務

 

service clickhouse-server stop
  1. 通過ontime_all_2插入幾條數據

 

hadoop1 :) insert into ontime_all_2 select * from ontime limit 10;
  1. 啓動hadoop2服務

 

service clickhouse-server start
  1. 查詢驗證
    查看兩個機器的ontime_local_2、以及ontime_all_2,發現都是總數據量都增加了10條,說明這種情況下,集羣節點之間能夠自動同步

再模擬一個複雜點的:

  1. 停掉hadoop2;
  2. 通過ontime_all_2插入10條數據;
  3. 查詢ontime_all_2,此時數據增加了10條;
  4. 停掉hadoop1;
  5. 啓動hadoop2,此時,整個集羣不可用;
  6. 查詢ontime_all_2,此時,集羣恢復可用,但數據少了10條;
  7. 啓動hadoop1,查詢ontime_all_2、ontime_local_2,數據自動同步;

上邊都是通過ontime_all_2表插入數據的,如果通過ontime_local_2表插入數據,還能同步嗎?

  1. hadoop1上往ontime_local_2插入10條數據;
  2. 查詢hadoop2,ontime_local_2數據沒有同步

綜上,通過分佈表寫入數據,會自動同步數據;而通過數據表寫入數據,不會同步;正常情況沒什麼大問題。

更復雜的情況沒有模擬出來,但是可能會存在數據不一致的問題,官方文檔描述如下:

Each shard can have the 'internal_replication' parameter defined in the config file.

If this parameter is set to 'true', the write operation selects the first healthy replica and writes data to it. Use this alternative if the Distributed table "looks at" replicated tables. In other words, if the table where data will be written is going to replicate them itself.

If it is set to 'false' (the default), data is written to all replicas. In essence, this means that the Distributed table replicates data itself. This is worse than using replicated tables, because the consistency of replicas is not checked, and over time they will contain slightly different data.

翻譯下:

分片可以設置internal_replication屬性,這個屬性是true或者false,默認是false。

如果設置爲true,則往本地表寫入數據時,總是寫入到完整健康的副本里,然後由表自身完成複製,這就要求本地表是能自我複製的。

如果設置爲false,則寫入數據時,是寫入到所有副本中。這時,是無法保證一致性的。

舉個栗子,一條數據要insert到ontime_all_2中,假設經過rand()實際是要寫入到hadoop1的ontime_local表中,此時ontime_local配置了兩個副本。
如果internal_replication是false,那麼就會分別往兩個副本中插入這條數據。注意!!!分別插入,可能一個成功,一個失敗,插入結果不檢驗!這就導致了不一致性;
而如果internal_replication是true,則只往1個副本里寫數據,其他副本則是由ontime_local自己進行同步,這樣就解決了寫入一致性問題。

雖然沒有模擬出數據不一致的情況,實際中可能會遇到,所以官方建議使用表自動同步的方式,也就是internal_replication爲true。

具體怎麼用,下邊具體介紹。

自動數據備份

自動數據備份,是表的行爲,ReplicatedXXX的表支持自動同步。

Replicated前綴只用於MergeTree系列(MergeTree是最常用的引擎),即clickhouse支持以下幾種自動備份的引擎:

ReplicatedMergeTree
ReplicatedSummingMergeTree
ReplicatedReplacingMergeTree
ReplicatedAggregatingMergeTree
ReplicatedCollapsingMergeTree
ReplicatedGraphiteMergeTree

再強調一遍,Replicated表自動同步與之前的集羣自動同步不同,是表的行爲,與clickhouse_remote_servers配置沒有關係,只要有zookeeper配置就行了。

爲了說明這個問題,先不配置clickhouse_remote_servers,只添加zookeeper配置:

 

<yandex>
    ...
    <zookeeper-servers>
        <node index="1">
            <host>hadoop1</host>
            <port>2181</port>
        </node>
        <node index="2">
            <host>hadoop2</host>
            <port>2181</port>
        </node>
        <node index="3">
            <host>hadoop3</host>
            <port>2181</port>
        </node>
    </zookeeper-servers>
    ...
</yandex>

建數據表

 

hadoop1:
CREATE TABLE `ontime_replica` (
  ...
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/ontime', 'replica1', FlightDate, (Year, FlightDate), 8192);

hadoop2:
CREATE TABLE `ontime_replica` (
  ...
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/ontime', 'replica2', FlightDate, (Year, FlightDate), 8192);

hadoop3:
CREATE TABLE `ontime_replica` (
  ...
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/ontime', 'replica3', FlightDate, (Year, FlightDate), 8192);

查看zk信息:

 

[zk: localhost:2181(CONNECTED) 0] ls /clickhouse/tables/ontime/replicas
[replica2, replica3, replica1]

可以看到,zk中已經有了對應的路徑和副本信息。

插入數據

 

hadoop1:
INSERT INTO ontime_replica SELECT * FROM ontime

注意!只在一個機器上執行插入操作。

查看數據

分別在三個機器上查詢ontime_replica,都有數據,且數據完全一樣。

可以看到,這種方式與之前方式的區別,直接寫入一個節點,其他節點自動同步,完全是表的行爲;而之前的方式必須創建Distributed表,並通過Distributed表寫入數據才能同步(目前我們還沒有給ontime_replica創建對應的Distributed表)。

配置集羣

 

...
        <!-- 1分片3備份:使用表備份-->
        <cluster_1shards_3replicas>
            <shard>
                <internal_replication>true</internal_replication>
                <replica>
                    <host>hadoop1</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>hadoop2</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>hadoop3</host>
                    <port>9000</port>
                </replica>
            </shard>
        </cluster_1shards_3replicas>
...

集羣名字爲cluster_1shards_3replicas,1個分片,3個副本;與之前不同,這次設置internal_replication爲true,表示要用表自我複製功能,而不用集羣的複製功能。

建分佈表

 

CREATE TABLE ontime_replica_all AS ontime_replica
ENGINE = Distributed(cluster_1shards_3replicas, default, ontime_replica, rand())

表名爲ontime_replica_all,使用cluster_1shards_3replicas集羣,數據表爲ontime_replica。

查詢分佈表:

 

hadoop1 :) select count(1) from ontime_replica_all;

SELECT count(1)
FROM ontime_replica_all

┌──count(1)─┐
│ 177920306 │
└───────────┘

分佈表寫入

前邊說了,一個節點ontime_replica寫入數據時,其他節點自動同步;那如果通過分佈表ontime_replica_all寫入數據會如何呢?

其實,前文已經提到過,internal_replication爲true,則通過分佈表寫入數據時,會自動找到“最健康”的副本寫入,然後其他副本通過表自身的複製功能同步數據,最終達到數據一致。

分片 + 備份

分片,是爲了突破單機上限(存儲、計算等上限),備份是爲了高可用。

只分片,提升了性能,但是一個分片掛掉,整個服務不可用;只備份,確實高可用了,但是整體還是受限於單機瓶頸(備份1000份與備份2份沒什麼區別,除了更浪費機器)。

所以,生產中這兩方面需要同時滿足。

其實,只要把之前的分片和備份整合起來就行了,例如,3分片2備份的配置如下:

 

...
        <!-- 3分片2備份:使用表備份 -->
        <cluster_3shards_2replicas>
            <shard>
                <internal_replication>true</internal_replication>
                <replica>
                    <host>hadoop1</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>hadoop2</host>
                    <port>9000</port>
                </replica>
            </shard>
            <shard>
                <internal_replication>true</internal_replication>
                <replica>
                    <host>hadoop3</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>hadoop4</host>
                    <port>9000</port>
                </replica>
            </shard>
            <shard>
                <internal_replication>true</internal_replication>
                <replica>
                    <host>hadoop5</host>
                    <port>9000</port>
                </replica>
                <replica>
                    <host>hadoop6</host>
                    <port>9000</port>
                </replica>
            </shard>
        </cluster_3shards_2replicas>
...

這裏一共需要建6個數據表:

 

CREATE TABLE `ontime_replica` (
  ...
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/ontime/{shard}', '{replica}', FlightDate, (Year, FlightDate), 8192);

其中,{shard}和{replica}是macros配置(相當於環境變量),修改配置文件:

 

vi /etc/clickhose-server/metrika.xml

添加內容:

 

...
    <macros>
        <shard>01</shard>
        <replica>01</replica>
    </macros>
...

每臺機器的shard和replica值根據具體情況設置,例如,這裏是3分片2副本,則配置如下:

 

hadoop1: shard=01, replica=01
hadoop2: shard=01, replica=02
hadoop3: shard=02, replica=01
hadoop4: shard=02, replica=02
hadoop5: shard=03, replica=01
hadoop6: shard=03, replica=02

使用macros只是爲了建表方便(每個機器可以使用同樣的建表語句),不是必須的,只要ReplicatedMergeTree指定zk路徑和replica值即可。

由於資源有限,這裏不實驗了。

需要提醒一下,每個clickhouse-server實例只能放一個分片的一個備份,也就是3分片2備份需要6臺機器(6個不同的clickhouse-server)。

之前爲了節省資源,打算循環使用,把shard1的兩個副本放到hadoop1、hadoop2兩個機器上,shard2的兩個副本放到hadoop2、hadoop3上,shard3的兩個副本放到hadoop3、hadoop1上,結果是不行的。

原因是shard+replica對應一個數據表,Distributed查詢規則是每個shard裏找一個replica,把結果合併。

假如按照以上設置,可能一個查詢解析結果爲:
取shard1的replica1,對應hadoop1的ontime_replica;
取shard2的replica2,對應hadoop3的ontime_replica;
取shard3的replica2,對應hadoop1的ontime_replica;

最後,得到的結果是hadoop1的ontime_replica查詢兩次+hadoop3的ontime_replica查詢一次,結果是不正確的。

由於之前用elasticsearch集羣,可以用3臺服務器來創建5分片2備份的索引,所以想當然的認爲clickhouse也可以,結果坑了兩天。




鏈接:https://www.jianshu.com/p/20639fdfdc99
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章