1.分布式表与集群配置
- 分布式表基于Distributed引擎创建,在多个分片上运行分布式查询。
- 读取是自动并行化的,可使用远程服务器上的索引(如果有)。
- 数据在请求的本地服务器上尽可能地被部分处理。例如,对于GROUP BY查询,数据将在远程服务器 上聚合,聚合函数的中间状态将发送到请求服务器,然后数据将进一步聚合。
创建分布式表:
ENGINE = Distributed(cluster_name, db_name, table_name[, sharding_key[, policy_name]])
参数:
- cluster_name:集群名称。
- db_name:数据库名称,可使用常量表达式:currentDatabase()。
- table_name: 各分片上的表名称。
- sharding_key: (可选)分片的key,可设置为rand()。
- policy_name: (可选)策略名称,用于存储异步发送的临时文件。
例如下面的/etc/metrika.xml的一部分内容:
<remote_servers> <logs> <shard> <weight>1</weight> <internal_replication>false</internal_replication> <replica> <host>example01-01-1</host> <port>9000</port> </replica> <replica> <host>example01-01-2</host> <port>9000</port> </replica> </shard> <shard> <weight>2</weight> <internal_replication>false</internal_replication> <replica> <host>example01-02-1</host> <port>9000</port> </replica> <replica> <host>example01-02-2</host> <secure>1</secure> <port>9000</port> </replica> </shard> </logs> </remote_servers>
这里定义了一个名为logs的集群名称,它有两个分片(shard)组成,每个分片包含两个副本(replica)。
分片是包含数据的不同服务器(要读取所有数据,必须访问所有分片)。
副本是存储复制数据的服务器(要读取所有数据,访问该分片上的任意一个副本上的数据即可)。
1.weight : 可选,写入数据时分片的权重,建议忽略该配置。
2.internal_repliacation : 可选,同一时刻是否只将数据写入其中一个副本。默认值:false(将数据写入所有副本),建议设置为true。写一个即可。避免重复写。
3.副本配置:配置每个Server的信息,必须参数:host和port,可选参数:user、password、secure和compression。
(1)、host : 远程服务器地址。支持IPv4和IPv6。也可指定域名,更改域名解析需 重启服务。
(2)、port : 消息传递的TCP端口。配置文件的tcp_port指定的端口,通常设置为 9000。
(3)、user : 用于连接到服务的用户名称。默认值:true。在users.xml文件中配置 了访问权限。
(4)、password:用于连接到远程服务的密码。默认值:空字符串。
(5)、secure : 使用ssl进行连接,通常还应该定义port=9440。
(6)、compression : 使用数据压缩。默认值:true。
2.数据副本与复制表
- 只有MergeTree系列引擎支持数据副本,支持副本的引擎是在MergeTree引擎名称的前面加上前缀 Replicated。
- 副本是表级别的而不是整个服务器级别的,因此服务器可以同时存储复制表和非复制表。
- 副本不依赖于分片,每个分片都有自己独立的副本。
副本表如:
- ReplicatedMergeTree
- ReplicatedSummingMergeTree
- ReplicatedReplacingMergeTree
- ReplicatedAggregatingMergeTree
- ReplicatedCollapsingMergeTree
- ReplicatedVersionedCollapsingMergeTree
- ReplicatedGraphiteMergeTree
3.ZooKeeper整合
ClickHouse使用Apache ZooKeeper来存储副本元信息, 在配置文件设置 zookeeper相关的参数。
ClickHouse在创建复制表的时候指定Zookeeper的目录,指定的目录会在建 表时自动创建。
如果ClickHouse的配置文件未配置ZooKeeper, 则无法创建复制表, 并且 任何存量的复制表都将是只读的。
对本地复制表的查询,不会使用ZooKeeper, 其查询速度和非复制表一样快。本地复制表的数据插入,针对每个数据块(一个块最多有 max_insert_block_size = 1048576条记录),会通过几个事务将大约十个条目添加到Zookeeper。因此,与非复制表相比, 复制表的INSERT操作等 待时间稍长。
<zookeeper> <node index="1"> <host>example1</host> <port>2181</port> </node> <node index="2"> <host>example2</host> <port>2181</port> </node> <node index="3"> <host>example3</host> <port>2181</port> </node> </zookeeper>
4.创建复制表
复制表的引擎要以Replicated为前缀,例如:ReplicatedMergeTree。
CREATE TABLE table_name ( EventDate DateTime, CounterID UInt32, UserID UInt32 ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/table_name', '{replica}') PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID);
引擎参数包含了变量,这些变量是在配置文件的”macros”部分配置的,例如:
<macros> <layer>05</layer> <shard>02</shard> <replica>clickhouse1</replica> </macros>
Replicated*MergeTree引擎参数:
zoo_path : ZooKeeper中表的路径。
replica_name : ZooKeeper中的副本名称。1.第一个参数ZooKeeper路径组成:
(1)、通用前缀:/clickhouse/tables/,建议复制表都使用类似这样的前缀。
(2)、分片标识符:{layer}-{shard},在本示例中,分片标识符有两部分组成,只要保证分片标识符能唯一标识一个分片即可。
(3)、ZooKeeper节点名称:table_name。节点名称最好与表名相同,节点名称在定义后不会更改,即使执行表的重命名操作。
2.第二个参数是副本名称,用于标识同一个分片的不同副本。副本名称只需要在每个shard中唯一即可。上面的示例中,复制引擎的参数使用了变量替换。ClickHouse也支持使用显示的参数。在这种情况下,不能使用分布式的DDL查询(ON CLUSTER)。建议使用变量替换的方式传入参数,
降低出错概率。
在每个副本服务器上运行CREATE TABLE语句,如果该分片的表在其他节点已经创建且有数据,则该新副本自动同步其他副本的数据。
5.副本同步机制
- 复制是多主异步的。
- INSERT语句(以及ALTER)可在任意可用的服务器上执行。数据首先插入到本地的服务器 (即运行查询的服务器),然后数据被复制到其他服务器。
- 由于复制是异步的,所以最近插入的数据出现在其他副本上会有一定的延迟。
- 如果部分副本不可用,则在它们可用时写入数据。
- 如果副本可用, 则等待的时间是通过网络传输压缩数据块所耗费的时间。
- 默认情况下, INSERT操作只需等待一个副本写入成功后返回。如果仅将数据成功写入一个 副本,并且该副本的服务器不再存在, 则存储的数据将丢失。要启动来自多个副本的写入确 认机制,使用insert_quorum选项。
6.数据原子写入与去重
- INSERT查询按照数据块插入数据,每个数据块最多max_insert_block_size(默认 max_insert_block_size = 1048576)条记录。换言之, 如果INSERT插入少于1048576条记 录,则插入操作是原子的。单个数据块的写入是原子的。
- 数据块是去重的。 对于同一数据块的多次写入(相同大小的的数据块,包含相同的行以及相 同的顺序),该块仅写入一次。在出现网口故障等异常情况下, 客户端应用程序不知道数据 是否已将数据成功写入数据库,因此可以简单地重复执行INSERT查询。相同的数据发送到哪 个副本进行插入并不重要,INSERT是幂等的。数据去重可通过参数 insert_deduplicate控 制,默认为0(开启去重)。
- 在复制过程中, 只有插入的源数据通过网络传输。进一步的数据转换(合并)会在所有副本 上以相同的方式进行处理。 这样可以最大限度减少网络带宽占用,这意味着当副本位于不同 的数据中心时,复制的效果也很好。
- ClickHouse内部监控副本的数据同步,并能够在发生故障后恢复。故障转义是自动的(对于数据的微小差异)或半自动的(当数据的差异太大时,这可能表示配置错误)。
- ClickHouse内部监控副本上的数据同步,并能够在发生故障后恢复。故障转移是自动的(对于数据的微小差异)或半自动的(当数据差异太大时,这可能表示配置错误)。
7.负载平衡策略
执行分布式查询时,首先计算分片的每个副本的错误数,然后将查询发送至最少错误的副本。如果没有错误或者错误数相同,则按如下的策略查询数据:
1.random(默认) : 将查询发送至任意一个副本。
2.nearest_hostname : 将查询发送至主机名最相似的副本。
3.in_order : 将查询按配置文件中的配置顺序发送至副本。
4.first_or_random : 选择第一个副本,如果第一个副本不可用,随机选择一个可用的副本。
设置策略的方式:
set load_balancing = 'first_or_random';
8.案例
1.在所有节点执行如下语句:
创建本地复制表:
CREATE TABLE table_local on cluster mycluster ( EventDate DateTime, CounterID UInt32, UserID UInt32 ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/table_local', '{replica}') PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID);
执行效果图:
在docker01-node节点执行的效果图如下:
![]()
在docker02-node上执行后的效果如下(提示已经存在了):
docker02-node :) CREATE TABLE table_local on cluster mycluster :-] ( :-] EventDate DateTime, :-] CounterID UInt32, :-] UserID UInt32 :-] ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/table_local', '{replica}') :-] PARTITION BY toYYYYMM(EventDate) :-] ORDER BY (CounterID, EventDate, intHash32(UserID)) :-] SAMPLE BY intHash32(UserID); CREATE TABLE table_local ON CLUSTER mycluster ( `EventDate` DateTime, `CounterID` UInt32, `UserID` UInt32 ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/table_local', '{replica}') PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID) ┌─host──────────┬─port─┬─status─┬─error─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─num_hosts_remaining─┬─num_hosts_active─┐ │ 192.168.56.13 │ 9000 │ 57 │ Code: 57, e.displayText() = DB::Exception: Table default.table_local already exists. (version 20.1.4.14 (official build)) │ 3 │ 1 │ │ 192.168.56.11 │ 9000 │ 57 │ Code: 57, e.displayText() = DB::Exception: Table default.table_local already exists. (version 20.1.4.14 (official build)) │ 2 │ 1 │ │ 192.168.56.10 │ 9000 │ 57 │ Code: 57, e.displayText() = DB::Exception: Table default.table_local already exists. (version 20.1.4.14 (official build)) │ 1 │ 1 │ └───────────────┴──────┴────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────────┴──────────────────┘ ┌─host──────────┬─port─┬─status─┬─error─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─num_hosts_remaining─┬─num_hosts_active─┐ │ 192.168.56.12 │ 9000 │ 57 │ Code: 57, e.displayText() = DB::Exception: Table default.table_local already exists. (version 20.1.4.14 (official build)) │ 0 │ 0 │ └───────────────┴──────┴────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────────┴──────────────────┘ ↓ Progress: 0.00 rows, 0.00 B (0.00 rows/s., 0.00 B/s.) 0%Received exception from server (version 20.1.4): Code: 57. DB::Exception: Received from 192.168.56.11:9000. DB::Exception: There was an error on [192.168.56.13:9000]: Code: 57, e.displayText() = DB::Exception: Table default.table_local already exists. (version 20.1.4.14 (official build)). 4 rows in set. Elapsed: 0.617 sec. docker02-node :)
通过上面的案例可以知道,只要在一个节点上创建了副本表之后,在其它节点上也已经存在了。
创建分布式表(每个节点都要创建):
CREATE TABLE table_distributed as table_local ENGINE = Distributed(mycluster, default, table_local, rand());
2.验证副本的复制在clickhouse1上,对本地表操作。
在docker02-node上(docker02-node的分片副本节点)上,验证数据是否同步:
在docker03-node和docker04-node上执行(即shard2上)。发现查询不到结果,效果如下:
3.验证集群的功能
在任意节点查看分布式表的数据(都将出现下面的效果)。
在任意一个节点往分布式表里面插入5条数据:
insert into table_distributed values('2020-03-11 12:12:31', 21, 1); docker04-node上执行 insert into table_distributed values('2020-03-12 12:12:32', 22, 2); docker04-node上执行 insert into table_distributed values('2020-03-13 12:12:33', 23, 3); docker03-node上执行 insert into table_distributed values('2020-03-14 12:12:34', 24, 4); docker03-node上执行 insert into table_distributed values('2020-03-15 12:12:35', 25, 5); docker02-node上执行
然后在任意一台机器上执行:
select * from table_distributed;
都可以看到:
docker02-node :) select * from table_distributed; SELECT * FROM table_distributed ┌───────────EventDate─┬─CounterID─┬─UserID─┐ │ 2020-03-11 12:12:33 │ 22 │ 37 │ └─────────────────────┴───────────┴────────┘ ┌───────────EventDate─┬─CounterID─┬─UserID─┐ │ 2020-03-12 12:12:32 │ 22 │ 2 │ └─────────────────────┴───────────┴────────┘ ┌───────────EventDate─┬─CounterID─┬─UserID─┐ │ 2020-03-11 12:12:31 │ 21 │ 1 │ └─────────────────────┴───────────┴────────┘ ┌───────────EventDate─┬─CounterID─┬─UserID─┐ │ 2020-03-14 12:12:34 │ 24 │ 4 │ └─────────────────────┴───────────┴────────┘ ┌───────────EventDate─┬─CounterID─┬─UserID─┐ │ 2020-03-13 12:12:33 │ 23 │ 3 │ └─────────────────────┴───────────┴────────┘ ┌───────────EventDate─┬─CounterID─┬─UserID─┐ │ 2020-03-15 12:12:35 │ 25 │ 5 │ └─────────────────────┴───────────┴────────┘ 6 rows in set. Elapsed: 0.007 sec. docker02-node :)
然后,分别在两个分片的主机上查询本地表:
在docker01-node上(shard1)发现的效果是:
在docker03-node上(shard2)发现的效果是:
可以看到,使用分布式表插入数据,数据分散到不同分片(shard)的本地表。