Clickhouse集群实践指南

目录
作者声明:内容由AI辅助创作
本文使用 ChatGpt 辅助进行文章结构规划、配置及示例生成,由 Google gemini 辅助技术细节解释优化 所有配置均经过实际测试验证,但其中某些概念可能存疑,请自行判断。
信息

当前测试环境:

  • Ubuntu 24.04
  • ClickHouse server version 25.11.2.24 (official build)

1. 写在前面:这篇文章适合谁?

  • ✔ 第一次接触 ClickHouse 集群
  • ✔ 想从一开始就用“正确姿势”
  • ✔ 不想以后因为早期决策返工
  • ✔ 需要一份 可长期复用的模板

不适合

  • 只想临时跑个 demo
  • 不关心扩容 / 高可用 / 运维

2. 官方安装指南

https://clickhouse.com/docs/zh/install

3. ClickHouse 集群的整体认知

ClickHouse 中,“集群”不是一个开关,而是一整套能力的组合

flowchart TB
    %% ================= 客户端 =================
    Client["客户端
INSERT / SELECT"] style Client fill:#4A90E2,color:#fff,stroke:#2C5AA0,stroke-width:2px %% ================= Distributed 表 ================= DT["Distributed 表
统一访问入口 / 路由层"] style DT fill:#9B59B6,color:#fff,stroke:#6C3483,stroke-width:2px %% 客户端请求 Client -->|写入请求 INSERT
查询请求 SELECT| DT %% ================= 分片层 ================= subgraph SHARDS["ClickHouse 分片层"] direction LR style SHARDS fill:#F4F6F7,stroke:#95A5A6,stroke-width:2px subgraph S1["Shard 1"] R11["副本 1-1
ReplicatedMergeTree"] R12["副本 1-2
ReplicatedMergeTree"] end subgraph S2["Shard 2"] R21["副本 2-1
ReplicatedMergeTree"] R22["副本 2-2
ReplicatedMergeTree"] end subgraph S3["Shard 3"] R31["副本 3-1
ReplicatedMergeTree"] R32["副本 3-2
ReplicatedMergeTree"] end end %% 副本样式 style R11 fill:#2ECC71,color:#fff,stroke:#1E8449 style R12 fill:#2ECC71,color:#fff,stroke:#1E8449 style R21 fill:#2ECC71,color:#fff,stroke:#1E8449 style R22 fill:#2ECC71,color:#fff,stroke:#1E8449 style R31 fill:#2ECC71,color:#fff,stroke:#1E8449 style R32 fill:#2ECC71,color:#fff,stroke:#1E8449 %% 写入路由 DT -->|按分片键路由写入数据| R11 DT -->|按分片键路由写入数据| R21 DT -->|按分片键路由写入数据| R31 %% 副本复制 R11 -->|异步复制(通过 Keeper 拉取增量 parts)| R12 R21 -->|异步复制(通过 Keeper 拉取增量 parts)| R22 R31 -->|异步复制(通过 Keeper 拉取增量 parts)| R32 %% ================= Keeper ================= subgraph KEEPERS["ClickHouse Keeper 集群"] direction LR K1["Keeper 节点 1
元数据 / 复制日志"] K2["Keeper 节点 2
元数据 / 复制日志"] end style K1 fill:#F39C12,color:#fff,stroke:#B9770E,stroke-width:1px style K2 fill:#F39C12,color:#fff,stroke:#B9770E,stroke-width:1px %% Keeper 协调 R11 <-->|注册副本 / 拉取复制日志| K1 R12 <-->|注册副本 / 拉取复制日志| K1 R21 <-->|注册副本 / 拉取复制日志| K2 R22 <-->|注册副本 / 拉取复制日志| K2 R31 <-->|注册副本 / 拉取复制日志| K1 R32 <-->|注册副本 / 拉取复制日志| K2 %% 查询返回 R11 -.->|返回本 Shard 查询结果| DT R21 -.->|返回本 Shard 查询结果| DT R31 -.->|返回本 Shard 查询结果| DT DT -.->|汇总所有 Shard 结果并返回| Client %% ================= 线路颜色 ================= %% 写入链路(红色) linkStyle 0 stroke:#E74C3C,stroke-width:2px linkStyle 1 stroke:#E74C3C,stroke-width:2px linkStyle 2 stroke:#E74C3C,stroke-width:2px linkStyle 3 stroke:#E74C3C,stroke-width:2px %% 副本复制(绿色) linkStyle 4 stroke:#27AE60,stroke-width:2px linkStyle 5 stroke:#27AE60,stroke-width:2px linkStyle 6 stroke:#27AE60,stroke-width:2px %% Keeper 协调(橙色) linkStyle 7 stroke:#F39C12,stroke-width:2px linkStyle 8 stroke:#F39C12,stroke-width:2px linkStyle 9 stroke:#F39C12,stroke-width:2px linkStyle 10 stroke:#F39C12,stroke-width:2px linkStyle 11 stroke:#F39C12,stroke-width:2px linkStyle 12 stroke:#F39C12,stroke-width:2px %% 查询链路(蓝色虚线) linkStyle 13 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 14 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 15 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 16 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5
flowchart TB
    %% ================= 客户端 =================
    Client["客户端
INSERT / SELECT"] style Client fill:#4A90E2,color:#fff,stroke:#2C5AA0,stroke-width:2px %% ================= Distributed 表 ================= DT["Distributed 表
统一访问入口 / 路由层"] style DT fill:#9B59B6,color:#fff,stroke:#6C3483,stroke-width:2px %% 客户端请求 Client -->|写入请求 INSERT
查询请求 SELECT| DT %% ================= 分片层 ================= subgraph SHARDS["ClickHouse 分片层"] direction LR style SHARDS fill:#F4F6F7,stroke:#95A5A6,stroke-width:2px subgraph S1["Shard 1"] R11["副本 1-1
ReplicatedMergeTree"] R12["副本 1-2
ReplicatedMergeTree"] end subgraph S2["Shard 2"] R21["副本 2-1
ReplicatedMergeTree"] R22["副本 2-2
ReplicatedMergeTree"] end subgraph S3["Shard 3"] R31["副本 3-1
ReplicatedMergeTree"] R32["副本 3-2
ReplicatedMergeTree"] end end %% 副本样式 style R11 fill:#2ECC71,color:#fff,stroke:#1E8449 style R12 fill:#2ECC71,color:#fff,stroke:#1E8449 style R21 fill:#2ECC71,color:#fff,stroke:#1E8449 style R22 fill:#2ECC71,color:#fff,stroke:#1E8449 style R31 fill:#2ECC71,color:#fff,stroke:#1E8449 style R32 fill:#2ECC71,color:#fff,stroke:#1E8449 %% 写入路由 DT -->|按分片键路由写入数据| R11 DT -->|按分片键路由写入数据| R21 DT -->|按分片键路由写入数据| R31 %% 副本复制 R11 -->|异步复制(通过 Keeper 拉取增量 parts)| R12 R21 -->|异步复制(通过 Keeper 拉取增量 parts)| R22 R31 -->|异步复制(通过 Keeper 拉取增量 parts)| R32 %% ================= Keeper ================= subgraph KEEPERS["ClickHouse Keeper 集群"] direction LR K1["Keeper 节点 1
元数据 / 复制日志"] K2["Keeper 节点 2
元数据 / 复制日志"] end style K1 fill:#F39C12,color:#fff,stroke:#B9770E,stroke-width:1px style K2 fill:#F39C12,color:#fff,stroke:#B9770E,stroke-width:1px %% Keeper 协调 R11 <-->|注册副本 / 拉取复制日志| K1 R12 <-->|注册副本 / 拉取复制日志| K1 R21 <-->|注册副本 / 拉取复制日志| K2 R22 <-->|注册副本 / 拉取复制日志| K2 R31 <-->|注册副本 / 拉取复制日志| K1 R32 <-->|注册副本 / 拉取复制日志| K2 %% 查询返回 R11 -.->|返回本 Shard 查询结果| DT R21 -.->|返回本 Shard 查询结果| DT R31 -.->|返回本 Shard 查询结果| DT DT -.->|汇总所有 Shard 结果并返回| Client %% ================= 线路颜色 ================= %% 写入链路(红色) linkStyle 0 stroke:#E74C3C,stroke-width:2px linkStyle 1 stroke:#E74C3C,stroke-width:2px linkStyle 2 stroke:#E74C3C,stroke-width:2px linkStyle 3 stroke:#E74C3C,stroke-width:2px %% 副本复制(绿色) linkStyle 4 stroke:#27AE60,stroke-width:2px linkStyle 5 stroke:#27AE60,stroke-width:2px linkStyle 6 stroke:#27AE60,stroke-width:2px %% Keeper 协调(橙色) linkStyle 7 stroke:#F39C12,stroke-width:2px linkStyle 8 stroke:#F39C12,stroke-width:2px linkStyle 9 stroke:#F39C12,stroke-width:2px linkStyle 10 stroke:#F39C12,stroke-width:2px linkStyle 11 stroke:#F39C12,stroke-width:2px linkStyle 12 stroke:#F39C12,stroke-width:2px %% 查询链路(蓝色虚线) linkStyle 13 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 14 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 15 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 16 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5
flowchart TB
    %% ================= 客户端 =================
    Client["客户端
INSERT / SELECT"] style Client fill:#4A90E2,color:#fff,stroke:#2C5AA0,stroke-width:2px %% ================= Distributed 表 ================= DT["Distributed 表
统一访问入口 / 路由层"] style DT fill:#9B59B6,color:#fff,stroke:#6C3483,stroke-width:2px %% 客户端请求 Client -->|写入请求 INSERT
查询请求 SELECT| DT %% ================= 分片层 ================= subgraph SHARDS["ClickHouse 分片层"] direction LR style SHARDS fill:#F4F6F7,stroke:#95A5A6,stroke-width:2px subgraph S1["Shard 1"] R11["副本 1-1
ReplicatedMergeTree"] R12["副本 1-2
ReplicatedMergeTree"] end subgraph S2["Shard 2"] R21["副本 2-1
ReplicatedMergeTree"] R22["副本 2-2
ReplicatedMergeTree"] end subgraph S3["Shard 3"] R31["副本 3-1
ReplicatedMergeTree"] R32["副本 3-2
ReplicatedMergeTree"] end end %% 副本样式 style R11 fill:#2ECC71,color:#fff,stroke:#1E8449 style R12 fill:#2ECC71,color:#fff,stroke:#1E8449 style R21 fill:#2ECC71,color:#fff,stroke:#1E8449 style R22 fill:#2ECC71,color:#fff,stroke:#1E8449 style R31 fill:#2ECC71,color:#fff,stroke:#1E8449 style R32 fill:#2ECC71,color:#fff,stroke:#1E8449 %% 写入路由 DT -->|按分片键路由写入数据| R11 DT -->|按分片键路由写入数据| R21 DT -->|按分片键路由写入数据| R31 %% 副本复制 R11 -->|异步复制(通过 Keeper 拉取增量 parts)| R12 R21 -->|异步复制(通过 Keeper 拉取增量 parts)| R22 R31 -->|异步复制(通过 Keeper 拉取增量 parts)| R32 %% ================= Keeper ================= subgraph KEEPERS["ClickHouse Keeper 集群"] direction LR K1["Keeper 节点 1
元数据 / 复制日志"] K2["Keeper 节点 2
元数据 / 复制日志"] end style K1 fill:#F39C12,color:#fff,stroke:#B9770E,stroke-width:1px style K2 fill:#F39C12,color:#fff,stroke:#B9770E,stroke-width:1px %% Keeper 协调 R11 <-->|注册副本 / 拉取复制日志| K1 R12 <-->|注册副本 / 拉取复制日志| K1 R21 <-->|注册副本 / 拉取复制日志| K2 R22 <-->|注册副本 / 拉取复制日志| K2 R31 <-->|注册副本 / 拉取复制日志| K1 R32 <-->|注册副本 / 拉取复制日志| K2 %% 查询返回 R11 -.->|返回本 Shard 查询结果| DT R21 -.->|返回本 Shard 查询结果| DT R31 -.->|返回本 Shard 查询结果| DT DT -.->|汇总所有 Shard 结果并返回| Client %% ================= 线路颜色 ================= %% 写入链路(红色) linkStyle 0 stroke:#E74C3C,stroke-width:2px linkStyle 1 stroke:#E74C3C,stroke-width:2px linkStyle 2 stroke:#E74C3C,stroke-width:2px linkStyle 3 stroke:#E74C3C,stroke-width:2px %% 副本复制(绿色) linkStyle 4 stroke:#27AE60,stroke-width:2px linkStyle 5 stroke:#27AE60,stroke-width:2px linkStyle 6 stroke:#27AE60,stroke-width:2px %% Keeper 协调(橙色) linkStyle 7 stroke:#F39C12,stroke-width:2px linkStyle 8 stroke:#F39C12,stroke-width:2px linkStyle 9 stroke:#F39C12,stroke-width:2px linkStyle 10 stroke:#F39C12,stroke-width:2px linkStyle 11 stroke:#F39C12,stroke-width:2px linkStyle 12 stroke:#F39C12,stroke-width:2px %% 查询链路(蓝色虚线) linkStyle 13 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 14 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 15 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5 linkStyle 16 stroke:#3498DB,stroke-width:2px,stroke-dasharray:5 5

记住一句话

ClickHouse 的高可用 = 数据可复制 + 元数据不丢 + 访问不中断


4. 为什么“单节点也要用集群模式初始化”

4.1. 常见误区

“现在只有一台机器,用单机模式更简单吧?”

这是 ClickHouse 中最容易留下技术债的决定。


4.2. 正确做法(推荐)

即使只有 1 台机器,也要:

原因:

好处解释
表结构不返工以后加节点不用重建
用法统一单机 / 集群 SQL 一样
符合 25.x 设计ON CLUSTER 强依赖 Keeper

5. 集群配置:cluster.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- /etc/clickhouse-server/config.d/cluster.xml -->
<clickhouse>
    <!-- 强制指定 ClickHouse 使用IP进行注册 -->
    <interserver_http_host>172.31.10.12</interserver_http_host>
    <remote_servers>
        <!-- 集群名称,后续 SQL 会用到 -->
        <default_cluster>
            <!-- shard:数据分片
                shard 数量 = 数据水平切分的数量
                一个 shard 可以包含多个 replica(副本节点) -->
            <shard>
                <!-- 决定数据是如何在副本之间"流动", 配合 ReplicatedMergeTree 的时候必须设置为 true -->
                <internal_replication>true</internal_replication>
                <!-- replica:副本 -->
                <replica>
                    <host>172.31.10.12</host>
                    <port>9000</port>
                </replica>
                <!-- <replica>
                    <host>172.31.10.12</host>
                    <port>9000</port>
                </replica> -->
            </shard>
        </default_cluster>
    </remote_servers>
</clickhouse>

关键说明:

  • cluster 名字非常重要 后续所有 ON CLUSTER xxx 都依赖它
  • shard ≠ replica
    • shard:数据水平切分
    • replica:同一数据的副本

6. Keeper:为什么必须有 ? & 怎么配 ?

6.1. Keeper 是干什么的?

Keeper 负责:

  • 分布式 DDL(ON CLUSTER
  • 副本元数据
  • 表锁
  • 副本状态协调

没有 Keeper,集群会“看起来存在,但不能用”


6.2. keeper.xml(最小可用,单节点)

  • /etc/clickhouse-server/config.d/keeper.xml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    <clickhouse>
        <!-- Keeper 的配置,只要保持各个节点在 raft_configuration 和 zookeeper 中即可,无关顺序节点类型 -->
        <!-- Keeper 服务本身 -->
        <keeper_server>
            <!-- Keeper 对外端口 -->
            <tcp_port>9181</tcp_port>
            <!-- 每台机器唯一,不能重复, 需要与 raft_configuration - server - id 保持一致 -->
            <server_id>1</server_id>
            <coordination_settings>
                <operation_timeout_ms>10000</operation_timeout_ms>
                <session_timeout_ms>30000</session_timeout_ms>
            </coordination_settings>
    
            <!-- Raft 成员(单节点也要写) -->
            <raft_configuration>
                <!-- 多少个节点(成员), 多少个 server , 其中id与对应的 server_id 保持一致 -->
                <server>
                    <id>1</id>
                    <hostname>172.31.10.12</hostname>
                    <!-- 9234 是 Keeper 节点之间 Raft 通信端口 -->
                    <port>9234</port>
                </server>
                <!-- <server>
                    <id>2</id>
                    <hostname>172.31.10.12</hostname>
                    <port>9234</port>
                </server> -->
            </raft_configuration>
        </keeper_server>
        <!-- ClickHouse Server 通过这里访问 Keeper -->
        <zookeeper>
            <!-- 同样多少个节点,多少个node -->
            <node>
                <host>172.31.10.12</host>
                <!-- 9181 是 ClickHouse Server 连接 Keeper 的端口 -->
                <port>9181</port>
            </node>
            <!-- <node>
                <host>172.31.10.12</host>
                <port>9181</port>
            </node> -->
        </zookeeper>
    </clickhouse>
  • 易错点

  • server_id 每台必须不同
  • Keeper 的目录 不要和数据目录混在一起
  • 即使单节点,也必须写 raft_configuration

7. 多磁盘 / 冷热分层(初始化就该做)

7.1. 为什么要一开始就配置?

  • 冷热分层是 表级能力
  • 表一旦建好,后面再补策略会很痛苦
  • Disk 在 ClickHouse 中只是目录抽象

👉 所以:可以先“假装有多块盘”


7.2. storage.xml

  • /etc/clickhouse-server/config.d/storage.xml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    
    <clickhouse>
        <!-- ClickHouse 数据根目录 -->
        <!-- 所有表、元数据都在这里 -->
        <path>/data/_clickhouse/</path>
    
        <!-- 
            sudo mkdir -p /data/_clickhouse/coordination/{log,snapshots}
            sudo chown -R clickhouse:clickhouse /data/_clickhouse
        -->
    
        <!-- 访问控制与权限存储路径 -->
        <!-- 此项设定实际上已过时(低版本兼容可用),交由 user_directories.local_directory 进行控制 -->
        <!-- <access_control_path>/data/_clickhouse/access/</access_control_path> -->
    
        <!-- 用户文件沙盒路径 -->
        <user_files_path>/data/_clickhouse/user_files/</user_files_path>
    
        <!-- 格式结构定义路径 -->
        <format_schema_path>/data/_clickhouse/format_schemas/</format_schema_path>
    
        <!-- Keeper 服务数据存储路径 -->
        <keeper_server>
            <!-- 存放 Keeper 的 Raft 事务日志 -->
            <log_storage_path>/data/_clickhouse/coordination/log</log_storage_path>
            <!-- 存放 Keeper 的 状态快照 -->
            <snapshot_storage_path>/data/_clickhouse/coordination/snapshots</snapshot_storage_path>
        </keeper_server>
    
        <!-- 多磁盘配置 -->
        <storage_configuration>
            <!-- 定义磁盘(逻辑) -->
            <disks>
                <!-- 热数据盘(模拟 SSD) -->
                <hot>
                    <type>local</type>
                    <path>/data/_clickhouse/hot/</path>
                </hot>
    
                <!-- 冷数据盘(模拟 HDD) -->
                <cold>
                    <type>local</type>
                    <path>/data/_clickhouse/cold/</path>
                </cold>
    
            </disks>
    
            <!-- 存储策略 -->
            <policies>
                <!-- 冷热分层策略名称 -->
                <hot_to_cold>
                    <volumes>
                        <!-- 新数据优先写这里 -->
                        <hot_volume>
                            <disk>hot</disk>
                        </hot_volume>
                        <!-- 冷数据移动到这里 -->
                        <cold_volume>
                            <disk>cold</disk>
                        </cold_volume>
                    </volumes>
                    <!-- hot 使用率 > 80% 时允许 move -->
                    <move_factor>0.2</move_factor>
                </hot_to_cold>
            </policies>
        </storage_configuration>
    </clickhouse>
  • 注意

  • 所有节点的 disk / policy 名称必须一致
  • 后续换真实 SSD / HDD 只改 path

8. 标准建表模板(冷热分层 + 可扩展)

8.1. 单副本(入门 / 非 HA)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CREATE TABLE dbtest.test_table
-- ON CLUSTER 在创建数据库的时候如果已指定,这儿就不需要了,否则会报告错误
-- ON CLUSTER 的作用是"广播执行", 不是"持续复制"
ON CLUSTER default_cluster
(
    event_time DateTime,
    user_id UInt64,
    event_type String,
    payload String
)
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, user_id)
TTL event_time + INTERVAL 7 DAY TO DISK 'cold'
SETTINGS
    storage_policy = 'hot_to_cold';

8.2. 高可用版本(推荐,生产)

更多内容查看«集群架构设计与元数据管理»

1
2
3
4
5
6
7
8
9
<!-- /etc/clickhouse-server/config.d: macros.xml -->
<clickhouse>
    <macros>
        <!-- 分片标识,用于分布式查询,同一分片需要保持一致 -->
        <shard>01</shard>
        <!-- 副本标识符(在同一分片的副本中必须唯一) -->
        <replica>01_01</replica>
    </macros>
</clickhouse>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE dbtest.test_table
-- ON CLUSTER 在创建数据库的时候如果已指定,这儿就不需要了,否则会报告错误
-- ON CLUSTER 的作用是"广播执行", 不是"持续复制"
ON CLUSTER default_cluster
(
    event_time DateTime,
    user_id UInt64,
    event_type String,
    payload String
)
-- ReplicatedMergeTree 定义路径在非多租户的情况下,下列模板应是一个最优解
-- '/clickhouse/tables/{shard}/<db_name>/<table_name>','{replica}'
-- 为什么要定义路径而不默认由系统自行判定呢?
-- 注: 当前为手动模式情况下,ZK 路径指定只能在建表或者建库当中的一个指定,当前介绍的是在建表时指定的情况(传统手动模式)
-- 因为系统默认行为通常会使用类似 /clickhouse/tables/{uuid}/{shard}, 其中uuid
-- 是数据库自动生成的唯一 ID, 这会导致新节点加入时非常很难"自动对接"。 因为新节点在手动创建表时,系统也会生成一个新的随机 uuid。
-- 这意味着新节点生成的自动路径会和旧节点不一致。路径不一致,数据就永远无法自动同步,除非你自己一个个去查询主节点然后手动固定路径,非常痛苦。
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/dbtest/test_table', '{replica}')
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, user_id)
TTL event_time + INTERVAL 7 DAY TO DISK 'cold'
SETTINGS
    storage_policy = 'hot_to_cold';
  • 为什么一定要 ReplicatedMergeTree?
  • 扩容后自动补数据
  • 节点挂掉不丢数据
  • 支持滚动升级

注: 根据已搜集到的信息说明,单节点和高可用模式尽量不要混用,否则容易后期的数据同步异常(未测试)


9. Distributed 表(统一读写入口)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
CREATE TABLE dbtest.test_table_all
ON CLUSTER default_cluster
AS dbtest.test_table
ENGINE = Distributed(
    --  指定该表要往哪个集群里分发数据
    default_cluster,
    -- 指定本地表所在的数据库名
    dbtest,
    -- 指定本地表的名称(数据真正存储的地方)
    test_table,
    -- 分流策略,用于决定数据流向哪个分片
    -- 随机分发: rand()
    -- 固定值分发: 1
    -- 字段直接分发: user_id
    -- 业务逻辑分发: cityHash64(user_id,...)
    -- 权重分发: 在cluster 中去定义权重
    cityHash64(user_id)
);

使用原则

  • 写 → test_table_all
  • 查 → test_table_all
  • 不直接访问本地表

10. 表命名规范

10.1. 行业级数仓表命名总规范(核心)

  1. 统一命名模型(强烈推荐)
    1
    
    <数据层>_<来源系统>_<业务域>_<业务实体>_<加工类型>_<时间粒度>

表名本身就是一份“元数据”

  1. 字段含义说明
    组成含义示例
    数据层数仓分层ods / dwd / dws / dim / ads
    来源系统数据来源系统crm / erp / app / pay / log
    业务域一级业务域trade / user / content
    业务实体核心业务对象order / user / sku
    加工类型明细/汇总/拉链等detail / snapshot / acc
    时间粒度数据粒度day / hour / month / full

10.2. ClickHouse 表类型命名规范

  1. ReplicatedMergeTree(本地表)
  • 命名规则

    1
    
    <数据层>_<来源系统>_<业务域>_<业务实体>_<加工类型>_<时间粒度>_local
  • 示例

    场景表名
    订单明细事实表dwd_erp_trade_order_detail_day_local
    用户行为日志ods_app_log_user_action_detail_local
    商品维度表dim_erp_product_sku_snapshot_full_local
    用户日汇总dws_app_user_login_acc_day_local
  1. Distributed(分布式表)
  • 命名规则
    1
    
    <数据层>_<来源系统>_<业务域>_<业务实体>_<加工类型>_<时间粒度>_(all|dist)
  • 示例
    Distributed 表对应 Local
    dwd_erp_trade_order_detail_day_alldwd_erp_trade_order_detail_day_local
    ods_app_log_user_action_detail_allods_app_log_user_action_detail_local
    dim_erp_product_sku_snapshot_full_alldim_erp_product_sku_snapshot_full_local

_all 代表全量数据(跨shard)
_dist 分布式路由

  • 如何抉择 _all/_dist ?
    • 数仓 / 业务 / BI / 数据服务 应该只认_all
    • 工程 / 运维 / 底层路由 可以存在 _dist, 但不应用于业务使用。

10.3. 数据层级命名(行业标准)

层级含义
ods贴源数据(准实时/离线)
dwd明细事实(业务语义清洗)
dws轻度汇总(公共指标)
dim维度表
ads应用层 / 报表层
  • ClickHouse 通常以 dwd / dws / ads 为主

10.4. 来源系统命名规范

  • 命名原则

    • 使用 稳定系统标识
    • 不使用部署名、环境名
    • 多源不混用
  • 示例

    系统命名
    App 埋点app
    ERPerp
    CRMcrm
    支付pay
    日志平台log
    第三方third

10.5 业务域 & 业务实体规范

  • 业务域(Domain)

    1
    
    trade / user / content / finance / risk
  • 业务实体(Entity)

    1
    
    order / refund / user / sku / coupon
  • 实体名必须是“名词”

10.5. 加工类型(行业强约定)

类型含义说明
detail明细事实一行一业务事件
snapshot快照当前状态
acc累计期间累计
delta增量变更数据
zipper拉链表状态变更历史

10.7. 时间粒度命名

粒度示例
明细full / detail
小时hour
day
month

10.8. ZooKeeper 路径命名规范(ClickHouse 特有)

1
/clickhouse/tables/{shard}/<db_name>/<table_name>
  • Distributed 表同名
  • 不带 _local

10.9. 完整示例

  • ReplicatedMergeTree(本地表)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    CREATE TABLE dbtest.dwd_erp_trade_order_detail_day_local
    (
        order_id UInt64,
        user_id UInt64,
        order_amount Decimal(18,2),
        order_time DateTime
    )
    ENGINE = ReplicatedMergeTree(
        '/clickhouse/tables/{shard}/dbtest/dwd_erp_trade_order_detail_day',
        '{replica}'
    )
    PARTITION BY toYYYYMM(order_time)
    ORDER BY (order_id);
  • Distributed 表

    1
    2
    3
    4
    5
    6
    7
    8
    
    CREATE TABLE dbtest.dwd_erp_trade_order_detail_day_dist
    AS dwd_erp_trade_order_detail_day_local
    ENGINE = Distributed(
        ck_cluster,
        default,
        dwd_erp_trade_order_detail_day_local,
        cityHash64(order_id)
    );

10.10. 命名反例(行业强禁止)

  • order_detail(缺少层级、来源)
  • ods_trade_order_v2(版本号禁止)
  • user_order_local(语义不完整)
  • trade_order_dist(表类型不进表名)
  • prod_dwd_order(环境不进表名)

10.11. 推荐最终对照表

类型表名
本地表dwd_erp_trade_order_detail_day_local
分布式表dwd_erp_trade_order_detail_day
ZK Path/clickhouse/tables/{shard}/<db_name>/<table_name>

11. 备份与恢复(未测试)

11.1. 推荐工具:clickhouse-backup

部署位置: 👉 每个 ClickHouse 节点(或运维机)


11.2. 创建备份

包含:

  • 表结构
  • 数据(支持增量)
1
clickhouse-backup create daily_2025_12_18

11.3. 恢复示例(单表)

1
clickhouse-backup restore --table dbtest.test_table daily_2025_12_18

11.4. 恢复整个数据库

1
clickhouse-backup restore daily_2025_12_18

11.5. 其他

  • 备份 /var/lib/clickhouse 目录(最后防线,但可能不靠谱)

12. 升级和迁移(未测试)

  • 要求:
  • 副本 ≥ 2
  • Keeper 存活

12.1. 正确姿势:滚动升级

1
2
node1 升级 → OK
node2 升级 → OK

12.2. 升级前必做:

1
2
-- 强制同步
SYSTEM SYNC REPLICA;

12.3. 迁移(换机器 / 扩容)

正确方式:

  • 新节点上线
  • 加入 cluster.xml
  • Replicated 表自动同步
  • 移除旧节点

13. 高可用(未测试)

13.1. 自动重试 9000 端口

13.2. HAProxy

  • 支持 TCP
  • 健康检查好
  • 性能稳定
1
2
3
4
5
6
7
8
frontend clickhouse
    bind *:9000
    default_backend ch_nodes

backend ch_nodes
    balance roundrobin
    server n1 172.31.10.11:9000 check
    server n2 172.31.10.110:9000 check

13.3. 不推荐 Nginx(除非 HTTP)

Nginx

  • 更适合 8123
  • 不适合高并发 TCP 查询

14. 扩容与自动复制是如何发生的?

14.1. 如何新增节点?

所有需要调整的配置项,在配置文件模板中均有定义,可以自行参考阅读

  1. 主节点和新增节点的 cluster.xmlkeeper.xmlmacros.xmlstorage.xml 配置文件需要保持高度一致,以下是需要修改的地方

    • cluster.xml

      • interserver_http_host 项, 值对应当前节点的 IP
      • shard 项, 在下面添加一个 replica 副本节点(注: 若是分片就添加一个shard)
    • keeper.xml, keeper.xml 配置文件对于无论对于哪一个节点来说,除了 server_id 不一样外,其他应完全一样

      • server_id 项, 各个节点保持唯一
      • raft_configuration 项, 在下面添加一个server节点, 值为对应节点的 IPserver_id
      • zookeeper 项, 在下面添加一个 node 节点
    • macros.xml

      • replica 项,保持设置节点的唯一性
    • storage.xml

      • 此配置每个节点完全一致(仅测试时候,实际应用中可能由于服务器环境不同,需要根据实际情况修改)
  2. 复制主节点自定义配置文件到新节点的 /etc/clickhouse-server/config.d, 修改上述内容

  3. 配置同步后,重启主节点, 然后重启新节点(这个重启顺序应该没什么关系,实际测试时候是先重启主节点、在重新的新节点)

  4. 登陆到新节点,等待同步完成,如果没有进行同步那么就需要手动创建与主节点相同的数据库、相同的表结构(注: 建表和建库的时候,需要保持与主节点的ZK路径完全一致,否则难以自动同步数据,这也是为什么在见表的时候要求手动指定ZK路径的原因)

14.2. 如何自动复制?

  • 当你有一个 ReplicatedMergeTree 表时:
    1. 每个副本在 ZooKeeper 注册自己
      • /clickhouse/tables/{shard}/dbtest/test_table
    2. 写入数据
      • 客户端写到任意副本
      • 副本会生成 mutation / insert log 放入 ZooKeeper 队列
    3. 其他副本监听 ZooKeeper
      • 自动检测到新的 insert
      • 拉取对应数据块(parts)并写入本地存储
    4. Merge 操作
      • 每个副本独立进行 Merge
      • 保持本地数据块高效存储

核心点:所有副本最终一致,但可以临时存在延迟(异步复制)

14.3. 节点扩容时发生什么?

  • 新 shard / 新副本加入
    1. 新副本在 ZooKeeper 注册
    2. 系统会识别它缺少哪些数据块(parts)
    3. 旧副本会将已有数据块复制到新副本
    4. 新副本完成数据同步后,自动参与 Merge 和查询

注意:这个过程是增量拉取,不会重复写入全量数据。

14.4. 新 shard 加入(分片扩容)

  • 分片扩容相对复杂
  • ClickHouse 原生不支持自动重分片
  • 操作步骤:
    • 创建新 shardlocal
    • 根据分片 key 手动迁移部分数据(或者使用 INSERT SELECT
    • 更新 Distributed 表的 shard 配置
  • 所以 shard 扩容通常是 人工触发的迁移 + 配置更新
  • 副本扩容则是 自动的

总结:副本复制自动,分片迁移手动


15. 扩容节点时候如何选择是分片还是副本?

15.1. 场景 1:读压力过大(查询多、查询慢)

  • 症状:

    • 大量并发查询请求
    • CPU 使用率高
    • 查询排队等待
  • 解决方案:增加副本(Replica)

    1
    2
    3
    4
    5
    6
    7
    
    <!-- 1分片3副本 -->
    <shard>
        <internal_replication>true</internal_replication>
        <replica><host>node1</host></replica>
        <replica><host>node2</host></replica>
        <replica><host>node3</host></replica>  <!-- 新增副本 -->
    </shard>
  • 效果:

    • 读请求可以分散到多个副本
    • 每个副本都有完整数据,可独立处理查询
    • 提高查询吞吐量
    • 不增加存储容量(每个副本存相同数据)

15.2. 场景 2:存储容量不足(数据量太大)

  • 症状:

    • 磁盘空间不足
    • 单表数据量达到 TB 级
    • 单节点存储瓶颈
  • 解决方案:增加分片(Shard)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    <!-- 2分片,每分片2副本 = 4节点 -->
    <shard>
        <internal_replication>true</internal_replication>
        <replica><host>node1</host></replica>
        <replica><host>node2</host></replica>
    </shard>
    <shard>
        <internal_replication>true</internal_replication>
        <replica><host>node3</host></replica>
        <replica><host>node4</host></replica>
    </shard>
  • 效果:

    • 数据分散存储,每个分片存一部分
    • 存储容量翻倍(2分片 = 2倍容量)
    • 写入压力分散
    • 并行查询性能提升(多分片并行处理)

15.3. 场景 3:写入压力过大(插入速度慢)

  • 症状:

    • 大量数据写入
    • 写入队列积压
    • Merge 操作频繁
  • 解决方案:增加分片(Shard)

    • 原因:
      • 副本之间需要数据同步,写入不会更快
      • 分片可以并行写入,每个分片独立处理

15.4. 场景 4:既有读压力又有存储压力

  • 解决方案:增加分片 + 每个分片有多个副本
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    <!-- 2分片,每分片3副本 = 6节点 -->
    <shard>
        <replica><host>node1</host></replica>
        <replica><host>node2</host></replica>
        <replica><host>node3</host></replica>
    </shard>
    <shard>
        <replica><host>node4</host></replica>
        <replica><host>node5</host></replica>
        <replica><host>node6</host></replica>
    </shard>

15.5. 决策流程图

graph TD
    A[单节点压力过大] --> B[判断压力类型]
    
    B --> C[读压力
查询慢、并发高] B --> D[存储/写入压力
容量不足、写入慢] C --> E[增加副本
同分片多副本] D --> F[增加分片
多分片多副本] E --> G1[1分片2副本] E --> G2[1分片3副本] F --> H1[2分片2副本] F --> H2[3分片2副本] style A fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff style B fill:#4dabf7,stroke:#1971c2,stroke-width:2px,color:#fff style C fill:#51cf66,stroke:#2f9e44,stroke-width:2px,color:#fff style D fill:#ffd43b,stroke:#f59f00,stroke-width:2px,color:#000 style E fill:#a9e34b,stroke:#5c940d,stroke-width:2px,color:#000 style F fill:#ff922b,stroke:#d9480f,stroke-width:2px,color:#fff style G1 fill:#d0ebff,stroke:#1971c2,stroke-width:2px,color:#000 style G2 fill:#d0ebff,stroke:#1971c2,stroke-width:2px,color:#000 style H1 fill:#ffe8cc,stroke:#d9480f,stroke-width:2px,color:#000 style H2 fill:#ffe8cc,stroke:#d9480f,stroke-width:2px,color:#000
graph TD
    A[单节点压力过大] --> B[判断压力类型]
    
    B --> C[读压力
查询慢、并发高] B --> D[存储/写入压力
容量不足、写入慢] C --> E[增加副本
同分片多副本] D --> F[增加分片
多分片多副本] E --> G1[1分片2副本] E --> G2[1分片3副本] F --> H1[2分片2副本] F --> H2[3分片2副本] style A fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff style B fill:#4dabf7,stroke:#1971c2,stroke-width:2px,color:#fff style C fill:#51cf66,stroke:#2f9e44,stroke-width:2px,color:#fff style D fill:#ffd43b,stroke:#f59f00,stroke-width:2px,color:#000 style E fill:#a9e34b,stroke:#5c940d,stroke-width:2px,color:#000 style F fill:#ff922b,stroke:#d9480f,stroke-width:2px,color:#fff style G1 fill:#d0ebff,stroke:#1971c2,stroke-width:2px,color:#000 style G2 fill:#d0ebff,stroke:#1971c2,stroke-width:2px,color:#000 style H1 fill:#ffe8cc,stroke:#d9480f,stroke-width:2px,color:#000 style H2 fill:#ffe8cc,stroke:#d9480f,stroke-width:2px,color:#000
graph TD
    A[单节点压力过大] --> B[判断压力类型]
    
    B --> C[读压力
查询慢、并发高] B --> D[存储/写入压力
容量不足、写入慢] C --> E[增加副本
同分片多副本] D --> F[增加分片
多分片多副本] E --> G1[1分片2副本] E --> G2[1分片3副本] F --> H1[2分片2副本] F --> H2[3分片2副本] style A fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff style B fill:#4dabf7,stroke:#1971c2,stroke-width:2px,color:#fff style C fill:#51cf66,stroke:#2f9e44,stroke-width:2px,color:#fff style D fill:#ffd43b,stroke:#f59f00,stroke-width:2px,color:#000 style E fill:#a9e34b,stroke:#5c940d,stroke-width:2px,color:#000 style F fill:#ff922b,stroke:#d9480f,stroke-width:2px,color:#fff style G1 fill:#d0ebff,stroke:#1971c2,stroke-width:2px,color:#000 style G2 fill:#d0ebff,stroke:#1971c2,stroke-width:2px,color:#000 style H1 fill:#ffe8cc,stroke:#d9480f,stroke-width:2px,color:#000 style H2 fill:#ffe8cc,stroke:#d9480f,stroke-width:2px,color:#000

15.6. 实际扩展示例

15.6.1. 当前状态:1节点

1
2
分片1
└─ 副本1 (node1) - 100% 数据,100% 压力

15.6.2. 扩展方案 A:增加副本(应对读压力)

1
2
3
4
5
6
7
分片1
├─ 副本1 (node1) - 100% 数据,50% 读压力
└─ 副本2 (node2) - 100% 数据,50% 读压力

存储: 无增加 (2 × 100% = 200% 冗余)
读能力: 翻倍
写能力: 不变(需同步到所有副本)

15.6.3. 扩展方案 B:增加分片(应对存储/写入压力)

1
2
3
4
5
6
7
分片1                    分片2
├─ 副本1 (node1) - 50%   ├─ 副本1 (node3) - 50%
└─ 副本2 (node2) - 50%   └─ 副本2 (node4) - 50%

存储: 翻倍 (每分片存一半)
读能力: 大幅提升(并行查询)
写能力: 翻倍(并行写入)

16. 一些常用的sql

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
-- 查看集群状态
SELECT * FROM system.clusters;

-- 查看 Keeper 中的 ZK 路径数据
SELECT * FROM system.zookeeper;

-- 查看副本状态
SELECT * FROM system.replicas;

-- 查看同步队列状态
SELECT * FROM system.replication_queue;

-- 验证集群是否生效(在任意一台执行)
SELECT * FROM system.clusters WHERE cluster = 'default_cluster';

-- 集群拓扑
SELECT
    cluster,
    shard_num,
    replica_num,
    host_name,
    host_address,
    port
FROM system.clusters
ORDER BY shard_num, replica_num;

--当前节点身份(宏是否生效)
SELECT getMacro('shard')  AS shard, getMacro('replica') AS replica;

--副本整体状态
--健康参考: 
--is_readonly = 0
--queue_size ≈ 0
--inserts_in_queue ≈ 0
SELECT database, table,is_leader,is_readonly,future_parts,parts_to_check,queue_size,inserts_in_queue FROM system.replicas;

--复制延迟 / 堆积详情
--判断是否存在副本 lag
--升级 / 扩容前必须检查

SELECT database,table,absolute_delay,log_max_index,log_pointer FROM system.replicas;

-- -- 强制修复常用命令
--使用建议: 
--1. 先 SYNC
--2. 再 RESTART
--3. 不要随便 DROP REPLICA

-- SYSTEM SYNC REPLICA db.table;
-- SYSTEM RESTART REPLICA db.table;



--磁盘状态
--用途: 
-- - 验证 hot / cold 是否加载
-- - 排查磁盘空间问题

SELECT name, path, free_space, total_space FROM system.disks;

-- 存储策略
SELECT * FROM system.storage_policies;

-- 表实际使用的磁盘
-- - 用途: 
-- - 验证 TTL 是否真的 move 到 cold
-- - 冷热分层是否生效
-- -- disk_name: 如果配置了多盘存储,这里会显示具体的物理盘别名(如 default, hdd, ssd)
-- -- count() AS parts: 正常情况下,一个表的 Part 数量不应过多(建议单表保持在几百个以内)。如果这个值过大,说明写入频率过高或合并(Merge)太慢。
-- readable_bytes: 反映了数据在磁盘上的实际物理占用(经过压缩后的)。
SELECT 
    database, 
    table, 
    disk_name, 
    count() AS parts, 
    formatReadableSize(sum(bytes_on_disk)) AS readable_bytes
FROM system.parts 
WHERE active -- 只统计活跃的分区,排除正在合并或过期的
GROUP BY database, table, disk_name 
ORDER BY sum(bytes_on_disk) DESC;

-- -- 监控合并队列 查看后台合并任务是否卡住
-- SELECT * FROM system.merges;


--当前正在执行的查询
SELECT query_id,user,elapsed,memory_usage,query FROM system.processes;


-- 最近慢查询
SELECT
    query_duration_ms,
    read_rows,
    result_rows,
    query
FROM system.query_log
ORDER BY query_duration_ms DESC
LIMIT 10;

--数据库级容量
SELECT
    database,
    sum(bytes_on_disk) AS size
FROM system.parts
WHERE active
GROUP BY database
ORDER BY size DESC;


-- 1. 检查集群配置是否正确(新节点)
SELECT 
   cluster,
   shard_num,
   replica_num,
   host_name,
   host_address,
   port
FROM system.clusters 
WHERE cluster = 'default_cluster'
ORDER BY shard_num, replica_num;
-- 期望结果:
-- default_cluster | 1 | 1 | 172.31.10.12 | ...
-- default_cluster | 1 | 2 | 172.31.10.12 | ...
-- 注意:shard_num 必须都是 1(同一个分片) 


-- 查看 SQL 执行错误时候的具体报错
SELECT 
    event_time, 
    query, 
    exception_code, 
    exception,  -- 具体的错误简述
    stack_trace -- 详细的错误堆栈
FROM system.query_log 
WHERE type != 'QueryFinish' -- 过滤掉成功的查询
AND type != 'QueryStart'
AND event_time > now() - INTERVAL 1 HOUR -- 查看最近一小时
ORDER BY event_time DESC 
LIMIT 10;

目录