"最初,我们并没有预料到 ClickHouse 本身会如此强大,并且**ClickHouse Keeper 的性能同样令人印象深刻!**"
"从 ZooKeeper 迁移到 ClickHouse Keeper 使**CPU 和内存使用量减少了 75% 以上**。"
"切换到 ClickHouse Keeper 后,**性能提升了近 8 倍**。"
背景
Bonree ONE 是由Bonree 数据科技 提供的一体化智能可观测性平台。
将所有 Bonree ONE 可观测性数据迁移到 ClickHouse 集群后,随着数据量的增长,对 ZooKeeper 的性能和稳定性要求也随之提高。
ZooKeeper 是用于协调和同步 ClickHouse 中分布式操作的共享信息存储库和一致性系统。我们在使用 ZooKeeper 时遇到了一些痛点,如果不及时解决,可能会影响业务运营。
最终,我们决定用ClickHouse Keeper 替换 ZooKeeper,以解决写入性能、维护成本和集群管理相关的问题。ClickHouse Keeper 是 ZooKeeper 的快速、更节省资源且功能丰富的直接替代方案,用 C++ 实现。
为了实现平滑迁移,我们必须找到以下方法:
- 自动化迁移
- 身份验证
- 数据迁移
- 数据验证
本文将详细介绍这些方法。
ZooKeeper 成为瓶颈
随着数据量和数据类型的不断扩展,ClickHouse 集群对 ZooKeeper 的压力也随之增加。例如,在需要为告警数据存储和查询提供高实时性能的场景中,我们遇到了以下 ZooKeeper 相关问题:
-
**容量有限**:在我们的云部署中,Keeper 配置了 4C8G(每个节点 4 个 CPU 核心和 8GB RAM)资源,单个 ZooKeeper 集群(3 个节点)最多可以支持 5 个分片(每个分片节点 16 个 CPU 核心和 32GB RAM,每个分片 2 个副本),每个分片大约有 20,000 个 Part,总计约 100,000 个 Part。随着数据量和表数量的增加,集群规模迅速扩大。因此,每次集群扩展 5 个分片时,我们都必须添加另一个 ZooKeeper 集群,导致维护成本显著增加。
-
**性能瓶颈**:最初,当 ClickHouse 存储 1TB 数据且总共少于 12,000 个 Part 时,数据可以在几毫秒内成功插入。但是,随着数据量的增加和 Part 数量达到 20,000 时,数据插入的响应时间变得明显变慢,需要数十秒才能成功插入。这导致数据无法及时查询的问题,影响用户体验。
-
**高资源占用**:ZooKeeper 消耗了大量的内存和 I/O 资源,并且随着数据量的增长,成本也随之增加。
在相同可观测性数据环境下的对比测试中,ZooKeeper 的内存消耗高出 4.5 倍,I/O 使用量高出 8 倍。
- **稳定性问题**:ZooKeeper 使用 Java 开发,可能出现频繁的 Full GC 周期,导致 ClickHouse 服务中断和性能波动。此外,还出现了
zxid overflow
等问题。当 ClickHouse 在后台执行副本之间的数据同步时,它必须先向 ZooKeeper 注册。ZooKeeper 性能低下会导致后台线程增加,从而导致副本之间的数据同步延迟。在我们的云平台上,使用 ZooKeeper 存储元数据导致超过 10,000 个 Part 的延迟队列,严重影响业务查询的准确性。
基于上述痛点以及写入密集型数据分析场景的特点,我们选择了 ClickHouse Keeper,它作为 ZooKeeper 的替代方案表现更出色。主要考虑因素包括:
-
**兼容性**:ClickHouse Keeper 与 ZooKeeper 客户端协议兼容。任何标准 ZooKeeper 客户端都可以与 ClickHouse Keeper 交互,支持使用 ClickHouse 客户端命令。这意味着无需修改服务器端即可连接到 ClickHouse Keeper。
-
**历史数据迁移**:我们可以使用clickHouse-keeper-converter 工具将 ZooKeeper 中的历史数据转换为 ClickHouse Keeper 集群可以导入的快照文件。通过选择数百万条路径的五分钟块来控制转换效率,从而减少整体集群停止写入时间,并将对在线产品的影响降到最低。
-
**服务稳定性**:ClickHouse-Keeper 使用 C++ 开发,通过从根本上解决 ZooKeeper 引起的 Full GC 问题,并通过调整服务器参数来提高服务性能,从而增强整体服务稳定性。
-
**资源和性能**:元数据的压缩存储减少了资源使用。它还可以更有效地使用 CPU、内存和磁盘 I/O。通过更有效地使用资源,ClickHouse Keeper 的性能更出色。
方案演进
基于 ZooKeeper 的原始方案
随着我们的可观测性平台业务的扩展,平台数据被划分为关键数据和普通数据两类。关键数据的性能和查询需求优先级更高,需要物理资源隔离。对于大型集群,逻辑资源隔离比较繁琐,因为引入新的类别数据需要重新分配和调整资源。物理隔离不同的资源更有效率,使运维工作能够专注于某一特定领域的资源支持。
最初,我们使用多个通过 ZooKeeper 协调的 ClickHouse 集群来处理不同数据类别的复杂性和不同的服务等级协议 (SLA)。但是,随着 ClickHouse 的扩展,ZooKeeper 的压力也随之增加,导致延迟和任务积压。为了解决这个问题,我们使用了官方 ClickHouse 多 ZooKeeper 架构:每组 5 个分片的 Part 元数据存储在一个专用的 ZooKeeper 集群(每个集群包含 3 个节点)中;表元数据集中存储在另一个 ZooKeeper 集群(包含 3 个节点)中,用于所有组。
上图说明业务 A 和业务 B 需要物理隔离,业务 A 使用 ClickHouse 集群 1(15 个分片,每个分片 2 个副本),业务 B 使用 ClickHouse 集群 2(5 个分片,每个分片 2 个副本)。查询根据集群名称映射到不同物理资源上的 ClickHouse 实例,确保资源隔离。ClickHouse 集群 1 中每组 5 个分片的 Part 元数据由一个专用的 ZooKeeper 集群(ZK1
、ZK2
、ZK3
)管理,而 ZK4
管理 ClickHouse 集群 2 的 5 个分片 Part 元数据。ClickHouse 集群 1 和 ClickHouse 集群 2 的所有表元数据都存储在一个共享的 ZooKeeper(ZK5
)集群中,以保证稳定性。维护五个 ZooKeeper 集群(5 次 3 个节点 = 15 个节点)非常繁琐,并且性能和稳定性随着时间的推移而下降。
基于 ClickHouse Keeper 的新方案
测试验证了使用 ClickHouse Keeper 可以提高数据插入速度、ClickHouse 集群稳定性和副本同步速度。由于 ClickHouse Keeper 拥有出色的性能,我们不再需要多个 ZooKeeper 节点集,而是选择只维护一个 ClickHouse Keeper 节点集来支持超过 15 个分片,从而大大简化了原始方案。
迁移过程
迁移前准备
ClickHouse 持续更新 ZooKeeper 中的数据。即使没有数据导入,后台任务(例如后台数据合并)也会更改 ZooKeeper 中的数据,这使得在迁移前后难以保证数据一致性。我们深入研究了 ClickHouse 中导致 ZooKeeper 数据修改的后台任务,并使用命令(例如 SYSTEM STOP MERGES
)停止了这些任务。这确保了迁移前后 ZooKeeper 和 ClickHouse Keeper 之间的数据一致性,方便了数据比较和验证。自动化迁移流程确保了更改在 30 分钟内完成,而不会严重影响业务。
自动化迁移
迁移过程涉及在多台机器上运行多个命令并评估结果,存在人为错误风险并增加迁移时间。为了解决这个问题,我们开发了一个基于 Ansible 的自动化迁移工具,Ansible 是一种用于配置管理、软件部署和高级任务编排(如无缝滚动更新)的 IT 自动化工具。迁移步骤如下:
- 停止数据导入。
- 停止 ClickHouse 的合并和其他后台任务。
- 记录比较指标。
- 重新启动每个 ZooKeeper 集群以获取最新的快照文件。
- 将快照文件从 ZooKeeper 复制并转换为 ClickHouse Keeper。
- 将快照文件加载到 ClickHouse Keeper 中,并对 ZooKeeper 和 ClickHouse-Keeper 节点内容进行抽样比较。
- 将 ClickHouse 元数据存储从 ZooKeeper 切换到 ClickHouse Keeper。
- 将指标与步骤 3 中的指标进行比较。
- 启动 ClickHouse 的合并和后台任务以及数据导入。
自动化迁移避免了操作错误,将迁移时间从手动操作的 2-3 小时显著缩短到几分钟,最大程度地减少了迁移期间对业务查询的影响。核心自动化流程包括:
// Stop ClickHouse schema creation and table operations
stopClickHouseManagers()
// Stop ClickHouse data write operations
stopClickHouseConsumers()
// Stop ClickHouse merge and other background tasks
ClickHouseStopMerges()
// Obtain the latest snapshot information from ZooKeeper clusters
getNewZookeeperSnap()
// Convert ZooKeeper snapshots to ClickHouse-Keeper snapshots
createAndExecConvertShell(housekeeperClusterName)
// Start ClickHouse clusters
startClickHouses()
// Compare data before and after the migration
checkClickHouseSelectData()
挑战
上面描述的迁移过程非常基础。然而,环境更加复杂,涉及多个集群、各种 ZooKeeper 节点集以及诸如多 ZooKeeper 到单个 ClickHouse-Keeper 的转换(社区版本仅支持一对一)、加密和身份验证问题以及数据验证效率挑战等问题。
将多个 ZooKeeper 集群迁移到单个 ClickHouse Keeper 集群
ClickHouse Keeper 在性能测试中显著优于 ZooKeeper,并且需要的资源少得多。因此,必须将多个 ZooKeeper 集群减少到单个集群以避免资源浪费。官方的 clickHouse-keeper-converter
工具仅支持 ZooKeeper 到 ClickHouse Keeper 的一对一转换。我们通过以下方式解决了这个问题:
- 在迁移之前对一些 ZooKeeper 节点进行采样,保存此信息以便在迁移后进行比较,确保迁移前后数据的一致性。
- 修改
clickHouse-keeper-converter
源代码以支持将来自多个 ZooKeeper 节点集的快照合并到一个 ClickHouse Keeper 节点集中。例如:
// Deserialize all snapshot files in a loop
for (const auto &item : existing_snapshots) {
deserializeKeeperStorageFromSnapshot(storage item.second log);
}
// Modify numChildren property acquisition method to avoid using incremental IDs
storage.container.updateValue(parent_path [path = itr.key] (KeeperStorage::Node &value) {
value.addChild(getBaseName(path));
value.stat.numChildren = static_cast<int32_t>(value.getChildren().size());
});
// File output stream when creating the conversion snapshot file, for quick reading by the subsequent diff tool
int flags = (O_APPEND | O_CREAT | O_WRONLY);
std::unique_ptr<WriteBufferFromFile> out = std::make_unique<WriteBufferFromFile>("pathLog", DBMS_DEFAULT_BUFFER_SIZE, flags);
加密身份验证
Bonree 拥有许多私有的 B2B(企业对企业)客户,其中一些客户历史上需要对 ZooKeeper 内容进行加密身份验证,而另一些则不需要,导致情况各不相同。必须考虑所有场景以确保迁移顺利进行。ZooKeeper 可以执行 ACL(访问控制列表)加密。以下是如何根据不同的策略处理已加密的 ZooKeeper 集群、部分加密的 ZooKeeper 集群和未加密的 ZooKeeper 集群的转换:
-
**完全加密或完全未加密:**如果所有内容都已加密并且加密信息一致,这意味着 ZooKeeper 集群中的每个节点都使用相同的 ACL 策略并且策略内容相同,并且 ClickHouse Keeper 的 ACL 与 ZooKeeper 的 ACL 兼容,我们可以保留原始加密信息并直接执行转换。ClickHouse 配置文件不需要修改。如果没有加密,即在使用 ZooKeeper 时 ClickHouse 没有配置 ACL 信息,这种情况也可以直接转换,ClickHouse 配置文件也不需要修改。
-
**部分加密或加密信息不一致:**在这种情况下,我们需要删除原始加密信息并修改 ClickHouse 配置文件以确保加密方法一致。删除 ZooKeeper 加密的步骤如下:
-
添加超级管理员帐户。在启动 ZooKeeper 集群时,在 JAVA 启动命令中添加以下内容:
Dzookeeper.DigestAuthenticationProvider.superDigest=zookeeper:{XXXXXX}
-
重新启动 ZooKeeper 集群。
-
使用 ZooKeeper 客户端进入集群:
zkCli.sh -server ${zookeeper_cluster_address, format as ip1:port,ip2:port}
-
使用超级管理员帐户登录:
addauth digest zookeeper:#{XXXXXX}
-
执行命令删除节点身份验证(目标路径下的节点越多,所需时间越长):
setAcl -R ${zookeeper_znode_path} world:anyone:cdrwa
-
删除步骤 (1) 中添加的超级管理员帐户并重新启动 ZooKeeper 集群。
-
迁移后,决定是否需要在 ClickHouse-Keeper 中重新启用加密。
-
验证过程
我们在验证过程中遇到了三个重大挑战,必须解决这些挑战才能确保迁移成功。
- **数据检索限制:**目前无法使用单个命令检索存储在 ZooKeeper 中的所有现有路径。
- **集群合并:**我们将多个 ZooKeeper 集群合并到一个 ClickHouse Keeper 集群中。
- **数据比较准确性和速度:**在自动化迁移过程中,快速准确地执行数据比较至关重要。如果未能做到这一点,可能会成倍地增加迁移持续时间,从而增加误判和潜在迁移失败的风险。
为了快速检索和比较路径,我们实施了以下策略:
-
在将 ZooKeeper 快照转换为 ClickHouse Keeper 快照时,我们将所有转换的路径打印到一个 pathlog 目标文件中。在比较过程中,我们对该文件的内容进行采样,将 ZooKeeper 查询转换为文件读取。最初,读取 900 万个 ZooKeeper 路径大约需要 9 个小时,但通过优化,现在只需几秒钟(例如通过将 pathlog 加载到内存中以进行读取)。
-
我们根据迁移关系依次比较 znode 路径。我们确保在比较之前关闭合并等任务,进一步确保不会出现临时目录,从而保证比较的有效性。比较分为两种情况:
- **差异路径:**这些路径仅存在于
/clickhouse/tables
路径下,并且仅在一个 ZooKeeper 集群中存在。如果它们通过比较验证,则验证成功。相反,如果它们不存在于多个 ZooKeeper 集群中或存在于两个或多个集群中,则验证失败。这确保了差异路径数据的正确性。 - **公共路径:**这些路径的内容在所有 ZooKeeper 集群中都相同,通过比较验证。这确保了公共路径数据的正确性。
- **差异路径:**这些路径仅存在于
调优
我们对 ClickHouse Keeper 应用了以下参数调优:
-
**max_requests_batch_size:**这表示在发送到 raft 之前批量请求的最大大小。集群越大,此值应向上调整得越多以促进批量处理和性能提升。在我们的场景中,我们将该值设置为 10,000。
-
**force_sync:**指示请求是否同步写入日志。优化的设置为 false。
-
**compress_logs:**指示日志是否压缩。日志文件大小会影响启动速度和磁盘使用情况。优化的设置为 true。
-
**compress_snapshots_with_zstd_format:**指示快照是否压缩。日志文件大小会影响启动速度和磁盘使用情况。优化的设置为 true。
结果
替换 ZooKeeper 为 ClickHouse Keeper 后,资源节省和写入延迟改进非常显著,解决了在 ZooKeeper 中存储元数据的性能瓶颈,同时降低了维护难度。
以前,ZooKeeper 中的 IO 瓶颈会直接影响 ClickHouse 存储的整体写入延迟。在我们的云平台中,每天处理数万亿个事件,从 ZooKeeper 迁移到 ClickHouse Keeper 使 CPU 和内存使用量节省了 75% 以上。
切换到 ClickHouse Keeper 后,IO 开销降低了 8 倍,性能提高了近 8 倍。
最初,我们没有预料到 ClickHouse 本身会如此强大,ClickHouse-Keeper 的性能同样令人印象深刻!
语言 | 资源 | CPU 利用率 | RAM 利用率 | I/O 利用率 | 导入持续时间 | 错误率 | |
---|---|---|---|---|---|---|---|
ZooKeeper | Java | 12 个节点 | 36 个核心 | 81.6 GB | 4% | P99=15 秒 | 高 |
ClickHouse Keeper | C++ | 3 个节点 | 9 个核心 | 18 GB | <0.5% | P99=2 秒 | 几乎为零 |
节省 | 75% | 75% | 78% | 87% | 86% | >90% |