我们很高兴邀请 Instabug 后端负责人 Sayed Alesawy 作为嘉宾分享他的见解。
Instabug 是什么?
Instabug 是一款 SDK,能够帮助用户在整个移动应用开发生命周期中监控、优先处理和调试性能和稳定性问题。Instabug SDK 提供了一套产品,主要包括崩溃报告和应用性能监控 (APM),使您可以监控应用性能的各个方面,例如崩溃、已处理的异常、网络故障、UI 停顿、启动和屏幕加载延迟,以及设置自定义跟踪以监控关键代码部分的能力。Instabug 还允许您通过规则和警报引擎来自动化工作流程,该引擎与 Jira、Opsgenie、Zendesk、Slack、Trello 等其他项目和事件管理工具进行了许多有用的集成。
性能监控的挑战
在处理性能指标时,主要面临两个挑战。
1. 性能指标很大程度上依赖于自然情况下非常频繁且庞大的事件,这给接收和有效存储这些事件带来了挑战。
2. 性能事件的原始格式没有太大用处,用户不想浏览数百万个事件来提取洞察。他们需要可视化和聚合来帮助他们理解这些数据,这几乎总是需要执行一些非常繁重的业务逻辑,这涉及大量查询和数据可视化。
总而言之,Instabug 的后端非常大规模。简单来说,Instabug 在微服务架构中运行多个 Kubernetes 集群,包含数十个微服务和数据存储。我们的 API 平均每分钟处理约 200 万个请求,每天有数 TB 的数据进出我们的服务。
我们在哪里使用 ClickHouse?
在我们大多数产品中,我们遵循一个模式,其中 MySQL 是主要数据存储,作为所有写入操作和一些轻量级读取操作(主要是点查询)的单一数据源,我们将数据索引到 Elasticsearch 集群,用于更复杂的读取操作,例如过滤、排序和聚合数据。
在构建 APM 时,我们意识到它将成为我们数据量最大的产品。简单来说,APM 每天存储约 30 亿个事件(行),每分钟约 200 万个事件。我们还必须提供一些非常复杂的数据可视化功能,这些功能严重依赖于对大量数据的过滤和在合理的时间范围内计算复杂的聚合,以确保良好的用户体验。
以下是一些我们为用户在 APM 仪表板中提供的可视化数据示例。
- 在这个数据可视化中,用户可以全面查看所有网络组,以检测有问题的组以便进一步检查。在这种可视化中,我们必须计算每个组发生的事件的聚合数据,例如计数、失败率、响应时间百分位数、Apdex 分数等。该数据可视化可以按任何列进行过滤和排序。
- 当用户希望深入研究某个特定组以调试问题时,例如潜在的应用启动问题,以下数据可视化将显示冷应用启动的详细信息。在这种可视化中,我们需要计算直方图,其中每个数据点都包含应用启动每个阶段的延迟的细分,同时生成一个包含这些细分总计的表格。
- 与第一个数据可视化不同,有时用户希望按特定属性细分分数,例如按设备类型测量的失败率,因此我们按数据实体中某个字段的不同值进行分组,并对第一个可视化进行类似的聚合。
- 虽然所有以前视图都可以在整个数据集上生成,但有时我们的用户希望知道数据集是否存在任何异常值,而无需手动浏览所有数据。在以下数据可视化中,我们运行了可能是我们迄今为止最复杂和最昂贵的查询,它用于检测数据集是否存在任何异常值。
为什么 Elasticsearch 不适合我们?
在一开始,我们使用与所有其他产品相同的方式设计了 APM。但我们遇到了 Elasticsearch 的性能问题,主要是读取操作,但写入操作的速度也不足以满足我们的负载。我们尝试了很多解决方案,例如尝试不同的节点/分片配置,运行多个 Elasticsearch 集群,创建每日索引以减少最近日期过滤器范围需要遍历的数据量等,这些措施让 Elasticsearch 坚持了一段时间……但我们意识到它无法扩展,我们需要找到另一种解决方案。
ClickHouse 的登场
我们尝试了几种数据存储来寻找 Elasticsearch 的替代方案用于 APM,然后我们发现了 ClickHouse。我们开始尝试使用 ClickHouse,了解其功能,我们将 Elasticsearch 查询写入 ClickHouse 并开始进行性能测试,我们发现 ClickHouse 的读取和写入操作性能都明显更好,因此我们决定开始迁移到 ClickHouse。
在一开始,我们不能仅仅冻结产品本身的工作来迁移到新的数据存储。我们也没有操作 ClickHouse 的经验,因此我们不想一次性将其部署到生产环境。因此,我们必须想出一些方法来使我们的代码和基础设施足够灵活,以允许进行增量部署和实验。我们重构了代码以抽象出与数据存储的交互,因此代码不再与我们使用的任何数据存储耦合,现在它可以根据一些动态提供的配置读取和写入不同的数据存储,这使我们能够:
- 将所有新数据写入 ClickHouse 和 Elasticsearch,以最大程度地减少迁移工作量。
- 让特定用户从 ClickHouse 写入/读取数据,而所有其他用户从 Elasticsearch 写入/读取数据,这对通过增量部署来获得信心至关重要。
- 能够为 Elasticsearch 和 ClickHouse 仍然添加新功能,并在需要时将其提供给实验组和主要生产环境。
我们花了大约 5 个月的时间才完全迁移到 ClickHouse,因此拥有可配置且灵活的基础设施对于帮助我们在积极迁移核心基础设施(例如主要数据存储)的同时不断改进产品至关重要。
我们在迁移过程中构建的配置基础设施实际上仍在使用中,它使我们能够运行多个集群,甚至根据需要将不同的事件指标数据托管到不同的数据库或集群中。
使用 ClickHouse 的当前架构
以下图表显示了 APM 的总体架构。
我们从 SDK 收集事件数据,速度非常快,因此我们创建了一个轻量级且快速的网关服务来调节此高速率,将其转化为持续的后台作业流。然后,我们的事件处理器工作器处理来自队列系统的这些后台作业,并执行业务逻辑,包括验证、分类和将这些事件分组到父组中,这些父组稍后将插入 MySQL。最后,事件数据本身被索引到我们的 ClickHouse 集群中。
我们的 ClickHouse 数据模式如下:
- 我们有多个表,它们按多个列进行分区和排序(通常您选择在 `WHERE` 子句中经常出现的列)。例如,我们的分区键之一是 `date`,这使我们能够根据所需的任何日期范围对数据进行分区。
- 我们还在 `TO` 形式中使用物化视图,这些物化视图由数据表支持。对于我们的每个表,我们都有多个物化视图来满足各种数据可视化和过滤用例。
- 我们运行保留 cron 作业,在保留期到期时删除旧数据。目前,我们设计保留作业以删除分区,而不是使用 `ALTER TABLE DELETE`,后者在 ClickHouse 中众所周知效率低下。
我们的 ClickHouse 基础设施由多个集群组成,每个集群都以主副本配置运行,复制由 ZooKeeper 管理。在这里,我们尝试使用适合任务的实例类型;例如,对于数据库节点,使用针对 CPU、内存、存储和网络进行优化的实例类型是有意义的,而对于 ZooKeeper 节点,则使用适合内存缓存的实例类型。
为什么 ClickHouse 对我们如此有效?
ClickHouse 在帮助我们扩展 APM 方面做得很好,因为:
- ClickHouse 是一个专为大型分析而设计的列式数据库,这与我们的用例非常匹配。与 Elasticsearch 相比,我们能够使用 ClickHouse 实现更好的响应时间。
- ClickHouse 支持物化视图 (MV),这是我们 ClickHouse 数据管道中非常重要的组成部分。事实上,我们从不查询原始原始表,我们只是使用它们作为单一的事实来源来恢复数据或在需要时重新填充 MV,相反,我们始终从 MV 读取,因为它们的大小要小得多,并且它们已经聚合了我们正在寻找的数据。使用 MV 是实现良好响应时间的最重要部分(设计良好的 MV 需要付出大量的努力)。
- ClickHouse 支持一系列 MergeTree 表引擎,这些引擎是 LSM 树(日志结构化合并树),众所周知写入吞吐量很高(写入仅仅是对内部排序的数据结构的追加,例如红黑树,合并是在后台完成的)。这对我们来说也很重要,因为我们的写入吞吐量很高(每分钟约 200 万个事件),因此我们使用 ReplicatedMergeTree 引擎。ClickHouse 写入在数据以大批量插入时效果最佳,因此我们也构建了索引器来优化这一点。
- ClickHouse 支持数据压缩,因为它压缩磁盘上的数据以节省空间。例如,当我们运行 Elasticsearch 时,我们有一个索引包含大约 3.07 亿个文档,占用了 50 GB 的磁盘空间。现在,同一个索引,但在 ClickHouse 表中,包含大约 305 亿行,占用 350 GB 空间,通过简单的线性插值,Elasticsearch 将需要大约 500 GB 来存储,这节省了至少 30% 的磁盘空间。
- ClickHouse 运行起来便宜得多,我们能够显著减少使用 Elasticsearch 的机器数量。值得一提的是,自我们从 Elasticsearch 迁移以来,我们的数据规模已经增加了两倍以上,因此现在的成本节省更大。
- ClickHouse 提供了一种非常类似 SQL 的查询界面,每个人都熟悉它,这使得将我们的 Elasticsearch 查询重写为 ClickHouse 查询变得更容易。
- ClickHouse 提供了许多聚合函数,这也帮助我们在 ClickHouse 中编写新功能。
经验教训和挑战
迁移到 ClickHouse 时,我们遇到了必须解决的一些挑战。其中一些挑战是:
- 这是我们第一次操作非托管数据存储,我们所有数据存储(如 MySQL、Elasticsearch 和 Redis)都是 AWS 托管云服务的一部分,这些服务提供了许多有用的操作功能,例如扩展、复制、故障转移、监控、自动快照和版本升级等。ClickHouse 不属于此范围,我们必须在裸机 EC2 上运行它并自行管理,这很有挑战性。
- ClickHouse 没有像 ActiveRecord 那样的 ORM,因此我们必须构建一个查询引擎来抽象一些裸机 ClickHouse 代码,最重要的是,以安全的方式编写它,使其不易受到 SQL 注入等攻击(ORM 通常默认情况下会为您处理这一点)。
- 我们的 ClickHouse 模式会随着业务需求的变化而发展,因此我们几乎总是需要添加新列或更改 MV 的定义,甚至创建新的 MV。我们必须找出有效编写这些迁移的方法,特别是在重新填充 MV 时。我们还必须为我们运行的每个集群多次运行相同的迁移,因此我们必须找出自动执行此操作以节省时间的方法。自动执行这些迁移有助于我们制定一个操作标准,这有助于避免重复过去的错误。
- 我们过去在使用 Elasticsearch 时依赖的一些聚合是它能够以开箱即用的方式构建不同桶的数据直方图。ClickHouse 不支持此功能,因此我们必须在迁移过程中自己构建此代码。
- 访问 ClickHouse 的代码库使用 Ruby on Rails 和 Golang 作为编程语言,事实证明,Ruby 和 Go 的适配器在它们用于与 ClickHouse 通信的协议方面有所不同,因为其中一个使用 HTTP,而另一个使用 TCP,因此我们必须以可配置的方式对此进行处理。
我们还吸取了一些教训,例如:
- ClickHouse 提供了许多数据类型,它们会影响其性能,因此我们必须为每列仔细选择正确的数据类型,并避免使用 `NULL`。
- ClickHouse 中的表分区键对于良好的性能至关重要,我们不能有太多分区,因此我们必须谨慎选择这些键。我们为此进行了多次迭代,以找到最适合我们用例的方法。
- ClickHouse 具有主键,但它们不是唯一的,尝试使用唯一键,特别是如果它们是随机的,例如 UUID,一定会破坏压缩,并导致非常大的数据集。我们为此付出了惨痛的代价。
- ClickHouse 是一个分析数据库,因此它主要针对不可变数据进行设计,它非常适合读取,但对更新和删除的性能很差(因为它必须重新排序数据部分,这是很昂贵的)。因此,我们学会了选择在 ClickHouse 中存储什么,以及在 MySQL 中存储什么,我们还必须以支持通过删除部分而不是使用删除来删除数据的方式设计数据部分。
- ClickHouse 物化视图非常适合读取性能,因此我们必须学习如何以一种方式设计物化视图,从而使 MV 相比于原始表的大小更小,以获得更好的查询响应时间。这包括努力通过对某些字段进行舍入等方式来降低 MV 的基数。
- ClickHouse 支持缓冲表以进行写入,事实证明,这对于处理我们非常高的写入吞吐量非常有用。
结论和一般建议
迁移到全新的数据存储可能是一项非常艰巨的任务,充满了挑战。对于我们来说,成功迁移的关键是彻底的研究和通用的代码和基础设施。彻底的研究可以帮助您做出明智的决定,并了解您正在使用的数据存储和您要迁移到的数据存储的功能和局限性。通用的代码和基础设施对于实验和逐步推出非常有用,这在敏感的生产环境中非常重要。
我们开始了迁移,我们的数据存储是产品开发的瓶颈,但通过精心计划,我们最终得到了一个数据存储,它已经为我们服务了将近一年,并且将在未来几年继续为我们服务。