我们欢迎 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
我们尝试了一些数据存储,以找到 APM 的 Elasticsearch 替代方案,我们发现了 ClickHouse。我们开始尝试 ClickHouse,了解其功能,我们用 ClickHouse 编写了 Elasticsearch 查询,并开始进行性能测试活动,我们意识到 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 亿个文档,占用 50GB 的磁盘空间。现在,相同的索引但在 ClickHouse 表中大约有 305 亿行,占用高达 350GB 的空间,通过简单的线性插值,Elasticsearch 将需要大约 500GB 的空间来存储,这至少节省了 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 支持用于写入的缓冲表,事实证明这对于处理我们非常高的写入吞吐量非常有用。
结论和一般建议
迁移到全新的数据存储可能是一项非常艰巨的任务,充满了挑战。对我们而言,成功迁移的关键是彻底的研究和通用的代码和基础设施。彻底的研究可以帮助您做出明智的决策,并了解您正在使用的数据存储以及您正在迁移到的数据存储的功能和局限性。通用的代码和基础设施对于实验和逐步推广非常有用,这在敏感的生产环境中非常重要。
我们从一个成为我们产品开发瓶颈的数据存储开始进行迁移,但通过周密的计划,我们最终获得了一个数据存储,该数据存储已经为我们服务了近一年,并且将在未来几年继续为我们提供良好的服务。