我们欢迎 Contentsquare 作为嘉宾来到我们的博客。继续阅读,了解他们为什么使用 ClickHouse 以及他们从 Elastic 迁移到 ClickHouse 的历程。
在 Contentsquare,我们过去在 Elasticsearch 之上运行我们的主要 SaaS 应用程序。
5 年前,我们启动了一个迁移过程,将我们所有的分析应用程序迁移到 ClickHouse 之上运行。我们希望通过迁移来提高水平可扩展性、系统的稳定性和整体效率(查询时间和成本)。
在这篇博文中,我们将详细介绍迁移过程以及在此过程中学到的经验教训。
我们为什么决定迁移?
我们在生产环境中有 14 个 Elasticsearch 集群。每个集群有 30 个节点(3 个主节点)。我们使用 m5.4xlarge
和网络附加磁盘。当时,我们在水平可扩展性方面遇到了困难,因为我们无法将更大的集群组合在一起,并保持它们在我们工作负载下的稳定运行。
鉴于我们的集群在规模上受到限制,我们不可能处理任何无法容纳在单个集群中的租户。 这严重限制了我们公司的发展能力。 我们可以处理的流量数量有一个上限,这意味着我们公司的增长因技术原因而放缓。 这是我们无法接受的。
为了解除这种技术限制并支持公司的增长,我们剩下两种选择
- 找出一种在多集群设置中有效托管每个租户的方法。
- 迁移到更具可扩展性的技术。
我们选择了第二种方案,并开始研究符合我们要求的 OLAP 数据库引擎
- 查询的最小延迟。
- 丰富的查询语言。
- 使用旋转磁盘快速高效。
- 易于部署和操作。
在进行了广泛的工程研究并考察了市场上大多数 OLAP 数据库和处理系统之后,我们发现 ClickHouse 完全符合我们的要求,于是我们开始规划迁移。
我们的迁移策略
迁移一个大型代码库,该代码库被成千上万的生产客户使用,说起来容易做起来难。我们将迁移工作分为 3 个阶段
- 熟悉 ClickHouse 并使用它构建新产品
- 使用自定义工具镜像所有现有功能,以确保我们没有任何回归。
- 逐个迁移我们的客户。
第一阶段:构建新产品以熟悉该技术
我们没有迁移现有产品,而是通过在 ClickHouse 之上构建新产品来启动我们的迁移。我们希望首先在一个更安全的设置中熟悉这项技术并在生产环境中运行它。第一个里程碑使我们能够
- 熟悉该技术并学习如何使用它
- 为 ClickHouse 部署构建自动化和 CI/CD 工具
- 设置警报和监控
发现一项新技术、调整它并构建所需的工具花费了我们大约 4 个月的时间。这个阶段对于提升团队水平并在更大规模上舒适地部署 ClickHouse 来说是无价的。
第二阶段:迁移现有产品
一旦第一个里程碑成功实现,我们就将注意力转向我们的主要产品。我们将团队分成两部分:一部分将维护和改进当前堆栈,另一部分将把所有现有功能移植到 ClickHouse。
我们以迭代方式迁移了我们的主要产品。我们逐个获取每个现有的 API 端点并重写它们,以便它们使用 ClickHouse 而不是 Elasticsearch。我们监听每个发送到旧端点的查询,并在新端点上也重放它。这使我们能够使用真实的生产使用情况,比较两个端点的结果,识别错误并迭代地修复它们。一旦我们认为此端点稳定,我们就继续重写下一个端点。
在所有这些时间里,所有生产查询仍然由旧的 Elasticsearch 处理。新的 ClickHouse 基础设施(尚未)被任何人在生产中使用。
第三阶段:迁移客户
一旦我们将所有端点迁移并测试到 ClickHouse,我们就可以放心地将客户迁移到我们的新基础设施。我们再次非常小心,不要一次移动所有客户,以便有足够的时间和资源来识别潜在的问题。
我们最初移动了一个客户。然后又一个。然后更多。在 6 个月的时间里,我们所有的客户都迁移了,我们的 Elasticsearch 集群可以关闭了。我们很自豪地说,这种周密的计划使我们在迁移过程中完全没有出现回归。
自迁移以来的经验
事实证明,ClickHouse 的成本降低了 11 倍(在基础设施成本方面),并且使我们的 p99 查询性能提高了 10 倍。因此,我们能够让我们的客户查询长达 3 个月的历史数据,而不是我们之前拥有的 1 个月。我们还将保留期限延长至 13 个月,因为现在从技术上讲这样做是可行的。
我们的 ClickHouse 设置
在迁移到 ClickHouse 的过程中,我们对我们的架构进行了两项重大调整,以确保我们能够充分利用 ClickHouse 提供的功能。
首先,我们设计了一个自定义的摄取组件,以降低主 ClickHouse 集群上插入操作的开销。其次,我们决定将我们的查询表示为抽象语法树。这使我们能够构建一个查询优化器,该优化器将利用我们的数据模型所隐含的一些假设。
摄取管道
我们在每个分片中单独插入数据,但我们确保以与分布式表中定义的分片键兼容的方式进行插入。这样做减少了集群为管理插入操作而必须执行的 I/O 量。
因此,我们不得不构建一个名为 clickin
的专用组件,该组件为我们处理插入操作。clickin
从 Kafka 主题读取;Kafka 主题中的数据使用 ClickHouse 中表的分片键进行分区。因此,分区分配是静态的。
鉴于我们已经构建了一个组件来最大限度地减少插入操作的 I/O 开销,我们还借此机会实现了另一个优化。clickin
获取输入数据,并使用 clickhouse-local
实例将其转换为 ClickHouse 本地格式,然后再将其发送到集群。这使我们能够在集群上节省一些 CPU,因为数据以 ClickHouse 最有效的格式到达。
查询优化器
我们从一开始就选择构建一个库,该库允许我们构建和操作 ClickHouse 查询作为抽象语法树。我们需要采用这种方法,因为我们的大多数查询都是动态的,并且是使用我们的用户选择的构建块动态组成的。这种设计选择使我们能够构建一个查询优化器,该优化器为我们执行几个重要的转换
- 它将分区键和排序键条件传播到所有子查询
- 当适用时,它将
distributed_group_by_no_merge
设置传播到嵌套子查询 - 它在适用时将子查询合并在一起
- 它在生成查询之前简化了一些冗余/无用的代数表达式。
这些优化使我们 5% 最慢的查询速度提高了 10 倍。
经验教训:平稳迁移的要点
- 不要将迁移视为修复功能性错误的机会。这将使您的非回归测试成为一场噩梦并拖慢您的速度
- 在非回归测试方面投入自动化
- 备份尚不完善,我们不得不构建一个小工具来执行备份。我们利用了此处描述的技术。
- ClickHouse 非常非常快,但为您做的查询优化很少。
- 投入尽可能多的时间来理解 MergeTree 引擎以及查询是如何执行的。
- 确保有关您的实体的所有数据都在单个分片中。这将使您可以使用
distributed_group_by_no_merge = 1
并减少网络 I/O。 - 确保您可以轻松关闭所有写入表的进程,在进行架构更改之前,您需要这样做。
从 Elasticsearch 迁移到 ClickHouse 是一段漫长的旅程,但这是我们做出的最好的技术决策之一。我们不后悔,这释放了新功能、增长和更容易扩展的潜力。