1. 复制模型概览
ClickHouse 的复制是表级的:只有使用 ReplicatedMergeTree 家族引擎(ReplicatedMergeTree、ReplicatedSummingMergeTree 等)的表才有副本。复制依赖一个协调服务(ZooKeeper 或 ClickHouse Keeper)记录日志,让每个副本都朝同一状态收敛。
┌────────────┐ 写入 ┌─────────────────────┐
│ 应用写入 │ ────────▶ │ ReplicatedMergeTree │
└────────────┘ │ (每个副本独立接收) │
└─────────┬───────────┘
│ 通过 Keeper 复制日志同步
┌────────────┬────────────┼────────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ replica1 │ │ replica2 │ │ replica3 │ │ replicaN │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
1.1 关键特性
| 特性 | 说明 |
|---|---|
| 多主写入 | 每个副本都可独立接收写入,无单一写入点 |
| 最终一致 | 所有副本最终收敛到同一数据状态 |
| 无锁合并 | 合并(merge)在副本间协调,避免重复 |
| 依赖协调者 | Keeper 或 ZooKeeper 是复制中枢 |
1.2 创建复制表
-- 副本 1
CREATE TABLE events (
ts DateTime,
user_id UInt64,
event String
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/events', -- 副本路径(Keeper 中)
'{replica}' -- 副本名
)
ORDER BY (user_id, ts);
-- 副本 2 使用同一路径、不同副本名
-- /clickhouse/tables/{shard}/events + replica2
一句话总结:复制在表级别生效,靠 Keeper 日志协调多副本朝同一状态收敛——理解"多主 + 最终一致"是理解复制表一切行为的前提。
2. 写入路径与日志复制
2.1 一次写入的完整路径
应用写入 replica1
→ replica1 落盘 Part(不可变数据块)
→ 向 Keeper 提交"新 Part 记录"(复制日志条目)
→ Keeper 分发日志到 replica2/replica3
→ 各副本拉取 Part 或元数据 → 本地落地
→ 各副本确认 → 写入完成
2.2 读写可用性
| 场景 | 表现 |
|---|---|
| 写入 | 任意存活副本可写(多主) |
| 读取 | 从任意副本读(分布式表自动路由) |
| 单副本宕机 | 其他副本照常读写 |
| Keeper 宕机 | 无法协调,副本间暂停复制(本地仍可读写) |
2.3 同步延迟指标
-- 查看各副本落后主副本的延迟
SELECT
database, table, replica_name,
absolute_delay, -- 绝对延迟(秒)
queued_queries
FROM system.replicas
ORDER BY absolute_delay DESC;
一句话总结:写入 = 落盘 Part + 向 Keeper 记日志 + 各副本拉取确认;复制延迟是排障的第一观察指标。
3. ZooKeeper 与 ClickHouse Keeper
3.1 为什么需要协调服务
复制表需要协调:合并任务的分配、Part 记录的广播、副本状态登记。ZooKeeper 曾是唯一选择,但存在运维复杂、可用性敏感等问题。ClickHouse Keeper 是原生替代(C++ 实现、协议兼容 ZooKeeper)。
3.2 对比
| 维度 | ZooKeeper | ClickHouse Keeper |
|---|---|---|
| 实现语言 | Java | C++ |
| 内存占用 | 高(JVM) | 低(~1GB/节点) |
| 部署方式 | 独立集群 | 可内嵌 clickhouse-server |
| 协议 | ZK 原生 | 兼容 ZK |
| 适用规模 | 中大规模 | 与 ClickHouse 同生共长 |
| 运维复杂度 | 较高 | 较低 |
3.3 Keeper 配置要点
<clickhouse>
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id>
<log_storage_path>/var/lib/clickhouse/coordination/log</log_storage_path>
<snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
<coordination_settings>
<operation_timeout_ms>10000</operation_timeout_ms>
<session_timeout_ms>30000</session_timeout_ms>
</coordination_settings>
<raft_configuration>
<server>
<id>1</id>
<hostname>keeper1</hostname>
<port>9234</port>
</server>
<server>
<id>2</id>
<hostname>keeper2</hostname>
<port>9234</port>
</server>
<server>
<id>3</id>
<hostname>keeper3</hostname>
<port>9234</port>
</server>
</raft_configuration>
</keeper_server>
</clickhouse>
一句话总结:协调服务是复制的"仲裁者";ClickHouse Keeper 用更低成本提供了与 ZooKeeper 兼容的协调能力,是新建集群的首选。
4. 跨机房容灾拓扑
4.1 同城双活
机房 A ────────────── 机房 B
├ replica1 ├ replica2
├ replica3 ├ replica4
└ Keeper1 └ Keeper2
Keeper3(第三地/仲裁)
- 数据在双机房各持副本,任意机房故障可继续服务
- 关键:Keeper 仲裁放第三地,避免双机房各半失联
4.2 异地多活
数据中心1(主) 数据中心2(备) 数据中心3(备)
replica×2 replica×2 replica×2
写入热点 低频写入/只读 低频写入/只读
- 异地延迟高,适合"主写 + 异地只读容灾"
- 主动-被动模式:故障后提升备机房为主
4.3 拓扑决策表
| 场景 | 副本数 | Keeper 布局 | 写入模型 |
|---|---|---|---|
| 单机房高可用 | 2-3 | 单机房 3 节点 | 任意副本 |
| 同城双活 | 每机房 2 | 双机房 + 第三地仲裁 | 任意副本 |
| 异地容灾 | 主 2 + 备 2 | 主机房为主 | 主写备读 |
| 多地多活 | 每地 2 | 跨地域 raft | 就近写入 |
一句话总结:跨机房拓扑的核心不是"多几份数据",而是 Keeper 仲裁怎么放、写入点怎么定——仲裁位置的错误选择比数据丢失更致命。
5. 脑裂(Split Brain)与防护
5.1 脑裂成因
当 Keeper 集群分裂成两个无法互通的组,且两组各自选主时,副本们可能朝两个不同状态收敛——这就是脑裂。
场景:Keeper 3 节点,机房间网络中断
组 A:Keeper1 + Keeper2(多数派)
组 B:Keeper3(少数派)
→ 多数派仍可服务(raft quorum)
→ 少数派失联,不再参与 → 通常不会双写
真正风险:配置错误 / 人为 failover 命令
5.2 防护措施
| 防护 | 做法 |
|---|---|
| 奇数节点 | Keeper 用 3/5 奇数,避免平票 |
| 严格 quorum | 写入需要多数派确认(raft 天然保证) |
| 勿手动主备切换 | 依赖 raft 自动选主 |
| 网络分区预案 | 监控 Keeper 成员可达性 |
| 心跳超时调优 | session_timeout 合理设置 |
5.3 双主误判的检测
-- 检查副本间数据偏差
SELECT
replica_name,
parts_count,
total_rows
FROM system.replicas;
-- 用哈希校验分布一致性
SELECT
database, table,
count() AS shards,
uniqExact(pairwiseHash) AS distinct_hashes
FROM (
SELECT
database, table,
cityHash64(groupArray(tuple(part_name))) AS pairwiseHash
FROM system.parts
GROUP BY database, table, replica
)
GROUP BY database, table;
一句话总结:脑裂的根子多在仲裁配置而非网络;用奇数 Keeper + 依赖 raft quorum,把"人为双活"变成不可能,是防脑裂的第一原则。
6. 故障切换演练
6.1 演练清单
1. 单副本宕机演练
- 停止 replica1 → 写入仍成功(走 replica2/3)
- 查询仍可用 → 检查 absolute_delay
- 恢复 replica1 → 自动追赶复制日志
2. 机房级故障演练
- 切断机房 A → 机房 B 独立服务
- 验证 Keeper 仲裁仍存活
- 验证写入/查询的 P99 延迟
3. 数据一致性验证
- 双机房对拍 sum/hash → 确认收敛
4. 恢复后回切
- 机房 A 恢复 → 自动补同步 → 切回正常拓扑
6.2 演练工具
# 模拟副本故障:暂停服务进程
systemctl stop clickhouse-server
# 观察复制状态
clickhouse-client --query "SELECT * FROM system.replicas FORMAT Pretty"
# 手动触发数据一致性校验
clickhouse-client --query "SYSTEM SYNC REPLICA"
6.3 RTO / RPO 目标
| 指标 | 目标 |
|---|---|
| RPO(数据丢失容忍) | 取决于写入确认策略;多主写入时损失窗口 ≈ 同步延迟 |
| RTO(恢复时间) | 秒级(副本自动接管);机房级看拓扑 |
一句话总结:容灾不是"买了副本就行",而是"演练出来的"——周期性故障注入是验证拓扑正确性的唯一可靠手段。
7. 一致性校验与数据对账
7.1 常见校验手段
-- 按表聚合对比
SELECT 'events' AS table,
sum(total_rows) AS rows,
sum(total_bytes) AS bytes
FROM system.parts
WHERE database = 'default' AND active;
-- 按分区校验
SELECT partition,
count() AS parts,
sum(rows) AS rows
FROM system.parts
WHERE table = 'events'
GROUP BY partition
ORDER BY partition;
7.2 深校验:抽样哈希
-- 抽 10000 行对比关键字段指纹
SELECT
count(),
uniqExact(concat(toString(user_id), '_', toString(ts)))
FROM events
SAMPLE 0.001
7.3 对账自动化
调度任务(cron / 数据质量平台)
→ 对每张复制表做 count/hash 对拍
→ 偏差超过阈值 → 告警
→ 定位差异分区 → 用备份回补或重建副本
一句话总结:副本的正确性靠"持续对账"而非"一次验证"——把 count/hash 对拍纳入日常监控,才能保证容灾体系真正可用。
8. 生产实践清单
| 主题 | 核心结论 |
|---|---|
| 复制模型 | 表级复制,多主写入,最终一致 |
| 协调服务 | 优先 ClickHouse Keeper(3/5 奇数) |
| 跨机房 | 同城双活 + 第三地仲裁;异地主备 |
| 脑裂防护 | 奇数节点 + raft quorum + 不手动切主 |
| 容灾演练 | 周期注入故障,验证 RTO/RPO |
| 一致性 | count/hash 对拍纳入日常监控 |
| 延迟监控 | 盯 system.replicas 的 absolute_delay |
跨机房容灾不是"多放几份数据",而是"协调层怎么放、写入怎么定、故障怎么切、一致性怎么证"四件事的完整闭环。把这四件事做扎实,ClickHouse 就能扛住机房级故障。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。