“最初,我们没有预料到 ClickHouse 本身会如此强大,而 ClickHouse Keeper 的性能也同样令人印象深刻!”
“从 ZooKeeper 迁移到 ClickHouse Keeper 后,CPU 和内存使用率降低了 75% 以上。”
“切换到 ClickHouse Keeper 后,性能提升了近 8 倍。”
背景
Bonree ONE 是由 Bonree Data Technology 提供的集成智能可观测平台。
在将所有 Bonree ONE 可观测数据迁移到 ClickHouse 集群后,随着数据量的增长,对 ZooKeeper 的性能和稳定性要求也随之提高。
ZooKeeper 是共享信息存储库和共识系统,用于协调和同步 ClickHouse 中的分布式操作。我们在使用 ZooKeeper 时遇到了一些痛点,如果不能及时解决,将会影响业务运营。
最终,我们决定用 ClickHouse Keeper 替换 ZooKeeper,以解决与写入性能、维护成本和集群管理相关的问题。ClickHouse Keeper 是 ZooKeeper 的快速、更高效且功能丰富的直接替代品,用 C++ 实现。
为了平稳迁移,我们必须找到以下方法:
- 自动化迁移
- 身份验证
- 数据迁移
- 数据验证
本文详细介绍了这些方法。
ZooKeeper 成为瓶颈
随着数据量和数据类型的不断扩展,ClickHouse 集群给 ZooKeeper 带来的压力也随之增加。例如,在需要高实时性能的警报数据存储和查询场景中,使用 ZooKeeper 时遇到了以下问题:
-
容量有限:在我们的云部署中,Keeper 配置为 4C8G(每个节点 4 个 CPU 核心和 8 GB 内存)资源,单个 ZooKeeper 集群(3 个节点)最多可支持 5 个分片(每个分片节点 16 个 CPU 核心和 32 GB 内存,每个分片 2 个副本),每个分片大约 20,000 个 parts,总共约 100,000 个 parts。随着数据量和表数量的增加,集群规模迅速扩大。因此,每次集群扩展 5 个分片时,我们都必须添加另一个 ZooKeeper 集群,导致了巨大的维护成本。
-
性能瓶颈:最初,ClickHouse 存储了 1TB 的数据,总共少于 12,000 个 parts,数据可以在毫秒内成功插入。但是,随着数据量的增加,parts 数量达到 20,000 个,数据插入的响应时间明显变慢,需要几十秒才能成功插入。这导致数据无法及时查询的问题,影响用户体验。
-
资源使用率高:ZooKeeper 消耗大量内存和 I/O 资源,并且成本随着数据量的增长而增加。
在相同的可观测数据环境中的对比测试中,ZooKeeper 的内存消耗是 ClickHouse Keeper 的 4.5 倍,I/O 使用率是 ClickHouse Keeper 的 8 倍。
- 稳定性问题:ZooKeeper 使用 Java 开发,可能会频繁经历 Full GC 周期,导致 ClickHouse 服务中断和性能波动。此外,还发生了
zxid overflow
等问题。当 ClickHouse 在后台执行副本之间的 parts 同步时,它必须首先向 ZooKeeper 注册。ZooKeeper 性能不佳可能会导致后台线程增加,从而导致副本之间的数据同步延迟。在我们的云平台中,使用 ZooKeeper 进行元数据存储导致超过 10,000 个 parts 的延迟队列,严重影响了业务查询的准确性。
基于以上痛点以及写入密集型数据分析场景的特点,我们选择了性能更优的 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 个分片的 parts 元数据存储在每个组的专用 ZooKeeper 集群中(每个集群由 3 个节点组成);表元数据集中存储在另一个 ZooKeeper 集群中(由 3 个节点组成),用于所有组
上图说明了业务 A 和业务 B 需要物理隔离,业务 A 使用 ClickHouse 集群 1(15 个分片,每个分片 2 个副本),业务 B 使用 ClickHouse 集群 2(5 个分片,每个分片 2 个副本)。查询根据集群名称映射到不同物理资源上的 ClickHouse 实例,从而确保资源隔离。ClickHouse 集群 1 中每组 5 个分片的 parts 元数据由专用 ZooKeeper 集群(ZK1
、ZK2
、ZK3
)管理,而 ZK4
管理 ClickHouse 集群 2 的 5 个分片的 parts 元数据。ClickHouse 集群 1 和 ClickHouse 集群 2 的所有表元数据都存储在共享的 ZooKeeper (ZK5
) 集群中,以确保稳定性。维护五个 ZooKeeper 集群(5 次 3 个节点 = 15 个节点)很麻烦,并且性能和稳定性随着时间的推移而下降。
基于 ClickHouse Keeper 的新解决方案
测试验证了使用 ClickHouse Keeper 提高了数据插入速度、ClickHouse 集群的稳定性和副本同步速度。由于 ClickHouse Keeper 的出色性能,我们不再需要多个 ZooKeeper 节点集,而是选择维护一个 ClickHouse Keeper 节点集来支持 15 个以上的分片,从而大大简化了原始解决方案。
迁移过程
迁移前准备
ClickHouse 不断更新 ZooKeeper 中的数据。即使没有数据摄取,后台任务(例如,后台 parts 合并)仍然会更改 ZooKeeper 中的数据,因此很难确保迁移前后数据的一致性。我们彻底研究了导致 ZooKeeper 中数据修改的 ClickHouse 后台任务,并使用命令(例如 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 配置文件不需要修改。如果根本没有加密,即 ClickHouse 在使用 ZooKeeper 时未配置 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。
结果
用 ClickHouse Keeper 替换 ZooKeeper 后,资源节省和写入延迟的改进非常显著,解决了 ZooKeeper 中存储元数据的性能瓶颈,同时也降低了维护门槛。
以前,ZooKeeper 中的 IO 瓶颈会直接影响 ClickHouse 存储的整体写入延迟。在我们的云平台上,随着数万亿事件的摄取,从 ZooKeeper 迁移到 ClickHouse Keeper 后,CPU 和内存使用率节省了 75% 以上。
切换到 ClickHouse Keeper 后,IO 开销降低了 8 倍,性能提升了近 8 倍。
最初,我们没有预料到 ClickHouse 本身会如此强大,而 ClickHouse Keeper 的性能也同样令人印象深刻!
语言 | 资源 | CPU 使用率 | 内存使用率 | 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% |