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