原作:Monika Singh & Pradeep Chhetri
这是我们在 Monitorama 2022 上发表的演讲的改编稿。您可以在此处找到包含演讲者笔记的幻灯片和此处的视频。
当 Cloudflare 的请求抛出错误时,信息会记录在我们的 requests_error 管道中。错误日志用于帮助解决特定于客户或网络范围的问题。
我们,站点可靠性工程师 (SRE),负责管理日志平台。我们已经运行Elasticsearch集群很多年了,这些年来日志量急剧增加。随着日志量的增加,我们开始面临一些问题。查询性能慢、资源消耗高等。我们的目标是通过提高查询性能并提供经济高效的日志存储解决方案来改善日志消费者的体验。这篇博文讨论了日志记录管道的挑战以及我们如何设计新架构以使其更快且更具成本效益。
在我们深入探讨维护日志管道的挑战之前,让我们先了解一下日志的特征。
日志的特征
不可预测:当今世界,微服务数量众多,集中式日志系统将收到的日志量非常难以预测。日志体量估算如此困难的原因有多种。主要是因为新应用程序不断部署到生产中,现有应用程序会自动扩展或缩小以满足业务需求,或者有时应用程序所有者启用调试日志级别并忘记将其关闭。
上下文:对于调试问题,通常需要上下文信息,即事件发生之前和之后的日志。单个日志行几乎没有帮助,通常,是一组日志行有助于构建上下文。此外,我们经常需要将多个应用程序的日志关联起来以绘制全貌。因此,必须保留日志在数据源处填充的顺序。
写入密集型:任何集中式日志系统都是写入密集型的。超过 99% 的已写入日志从未被读取。它们占用空间一段时间,并最终被保留策略清除。剩下的不到1%的被读取的日志非常重要,我们不能错过它们。
日志管道
与大多数其他公司一样,我们的日志记录管道由生产者、路由转发器、队列、消费者和存储组成。
在 Cloudflare 全球网络上运行的应用程序(生产者)生成日志。这些日志以 Cap'n Proto 序列化格式在本地写入。 Shipper(内部解决方案)通过流将 Cap'n Proto 序列化日志推送到 Kafka(队列)进行处理。我们运行 Logstash(消费者),它从 Kafka 消费并将日志写入 ElasticSearch(数据存储)。然后使用 Kibana 或 Grafana 可视化数据。我们在 Kibana 和 Grafana 中内置了多个仪表板来可视化数据。
Cloudflare 的 Elasticsearch 瓶颈
在 Cloudflare,我们多年来一直运行 Elasticsearch 集群。多年来,日志量急剧增加,在优化 Elasticsearch 集群以处理此类量时,我们发现了一些限制。
Mapping 爆炸
Mapping 爆炸是 Elasticsearch 众所周知的局限性之一。 Elasticsearch 维护一个映射,决定如何存储和索引新文档及其字段。当此映射中的键太多时,可能会占用大量内存,从而导致频繁的垃圾回收。防止这种情况的一种方法是使 schema 严格,这意味着任何不遵循此严格 schema 的日志行最终都会被删除。另一种方法是使其成为半严格的,这意味着不属于此映射的任何字段都将不可搜索。
多租户支持
Elasticsearch 没有很好的多租户支持。一个坏用户很容易影响集群性能。无法限制查询可以读取的文档或索引的最大数量或 Elasticsearch 查询可以占用的内存量。错误的查询很容易降低集群性能,即使查询完成后,它仍然会留下影响。
集群维护工作
管理Elasticsearch集群并不容易,尤其是多租户集群。一旦集群降级,就需要花费大量时间才能使集群恢复到完全健康的状态。在Elasticsearch中,更新索引模板意味着重新索引数据,这是一个相当大的开销。我们使用冷热分层存储,即最近的数据存储在 SSD 中,较旧的数据存储在机械硬盘中。虽然Elasticsearch每天都会将数据从热存储移动到冷存储,但它会影响集群的读写性能。
垃圾回收
Elasticsearch 使用 Java 开发并在 Java 虚拟机 (JVM) 上运行。它执行垃圾收集以回收由程序分配但不再引用的内存。Elasticsearch 需要垃圾收集调整。最新的 JVM 中默认的垃圾回收是 G1GC。我们尝试了其他 GC,例如 ZGC,这有助于减少 GC 暂停,但在读写吞吐量方面并没有给我们带来太多性能优势。
Elasticsearch 是一个很好的全文搜索工具,这些限制对于小型集群来说并不重要,但在 Cloudflare 中,我们每秒处理超过 35 到 4500 万个 HTTP 请求,其中每秒有超过 500K-800K 的请求失败。这些失败可能是由于不正确的请求、源服务器错误、用户配置错误、网络问题和各种其他原因造成的。
我们的客户支持团队使用这些错误日志作为定位客户问题的起点。错误日志包含有关 HTTP 请求所经过的各种 Cloudflare 产品的许多字段元数据。我们将这些错误日志存储在 Elasticsearch 中。我们对它们进行了大量采样,因为存储所有内容需要花费数百 TB 的空间,超出了我们的资源分配预算。此外,基于它构建的仪表板非常慢,因为它们需要对各个字段进行大量聚合。根据调试要求,我们需要将这些日志保留几周。
建议的解决方案
我们希望完全取消采样,即存储保留期内的每条日志行,为如此庞大的数据量提供快速查询支持,并在不增加成本的情况下实现这一切。为了解决所有这些问题,我们决定进行概念验证,看看是否可以使用 ClickHouse 来满足我们的要求。
Cloudflare 是 ClickHouse 的早期采用者,我们多年来一直在管理 ClickHouse 集群。我们已经拥有许多内部工具和库,用于将数据插入 ClickHouse,这使我们可以轻松进行概念验证。让我们看一下 ClickHouse 的一些功能,这些功能使其非常适合存储日志,并使我们能够构建新的日志管道。
ClickHouse 是一个面向列的数据库,这意味着与特定列相关的所有数据在物理上彼此相邻存储。即使在普通商用硬件上,这种数据布局也有助于快速顺序扫描。这使我们能够从老一代硬件中获得最大性能。
ClickHouse 专为分析工作负载而设计,数据可以有很多列。我们能够设计具有大量列的新 ClickHouse 表,而不会牺牲性能。
ClickHouse 索引的工作方式与关系数据库中的索引不同。在关系数据库中,主索引非常密集,并且每个表行包含一个条目。因此,如果表中有 100 万行,主索引也将有 100 万个条目。而在 ClickHouse 中,索引是稀疏的,这意味着每几千行只有一个索引条目。ClickHouse 索引使我们能够动态添加新索引。
ClickHouse 默认使用 LZ4 压缩所有内容。高效的压缩不仅有助于最大限度地减少存储需求,还可以让 ClickHouse 有效地使用页面缓存。
ClickHouse 的一项很酷的功能是可以按列配置压缩编解码器。我们决定为所有列保留默认的 LZ4 压缩。我们对 DateTime 列使用了 Double-Delta,对 Float 列使用了 Gorilla,对固定大小的 String 列使用了 LowCardinality。
ClickHouse是线性可扩展的;也就是说,写入可以通过添加新分片来扩展,读取可以通过添加新副本来扩展。ClickHouse 集群中的每个节点都是相同的。没有任何特殊节点有助于轻松扩展集群。
让我们看一下我们用来提供更快的读/写吞吐量和更好的日志数据压缩的一些优化。
Inserter
拥有高效的插入器与拥有高效的数据存储一样重要。在 Cloudflare,我们一直在运行相当多的分析管道,在编写新的插入器时我们借用了大部分概念。我们使用 Cap'n Proto 消息作为传输数据格式,因为它提供快速的数据编码和解码。扩展插入器很容易,可以通过添加更多 Kafka 分区并生成新的插入器 Pod 来完成。
Batch Size
将数据插入 ClickHouse 时的关键性能因素之一是批量大小。当批量较小时,ClickHouse 会创建许多小分区,然后将其合并为更大的分区。因此,较小的批量大小会给 ClickHouse 在后台带来额外的工作,从而降低 ClickHouse 的性能。因此,将其设置得足够大,以便 ClickHouse 可以愉快地接收数据批次,而不会达到内存限制,这一点至关重要。
数据模型
ClickHouse 提供内置的分片和复制,无需任何外部依赖。ClickHouse 的早期版本依赖于 ZooKeeper 来存储复制信息,但最新版本通过添加 clickhouse-keeper 消除了对 ZooKeeper 的依赖。
为了跨多个分片读取数据,我们使用分布式表,一种特殊的表。这些表本身不存储任何数据,而是充当存储实际数据的多个基础表的代理。
与任何其他数据库一样,选择正确的表 schema 非常重要,因为它将直接影响性能和存储利用率。我们想讨论将日志数据存储到 ClickHouse 中的三种方法。
第一个是最简单且最严格的表模式,您可以在其中指定每个列名称和数据类型。任何具有此预定义 schema 之外的字段的日志行都将被删除。根据我们的经验,此架构将为您提供最快的查询性能。如果您已经知道前面所有可能字段的列表,我们建议使用它。您始终可以通过运行 ALTER TABLE 查询来添加或删除列。
第二种模式使用 ClickHouse 的一个非常新的功能,它完成了大部分繁重的工作。您可以将日志作为 JSON 对象插入,在幕后,ClickHouse 将了解您的日志架构并动态添加具有适当数据类型和压缩的新列。仅当您可以很好地控制日志架构并且总字段数小于 1,000 时,才应使用此架构。一方面,它提供了自动添加新列作为新日志字段的灵活性,但与此同时,一个糟糕的应用程序可以轻松地破坏 ClickHouse 集群。
第三种模式将相同数据类型的所有字段存储在一个数组中,然后使用 ClickHouse 内置数组函数来查询这些字段。即使字段超过 1,000 个,此架构也能很好地扩展,因为列数取决于日志中使用的数据类型。如果某个数组元素被频繁访问,可以利用ClickHouse的物化列功能将其取出作为专用列。我们建议采用此模式,因为它可以防止应用程序记录过多字段。
数据分区
分区是 ClickHouse 数据的一个单位。 ClickHouse 用户常犯的一个错误是分区键过于细化,导致分区过多。由于我们的日志管道每天都会生成 TB 级的数据,因此我们创建了使用toStartOfHour(dateTime)
分区的表。通过这种分区逻辑,当查询在 WHERE 子句中带有时间戳时,ClickHouse 就会知道分区并快速检索它。它还有助于根据数据保留策略设计有效的数据清除规则。
主键选择
ClickHouse 将数据按主键排序存储在磁盘上。因此,选择主键会影响查询性能并有助于更好的数据压缩。与关系数据库不同,ClickHouse 不需要每行都有唯一的主键,我们可以插入具有相同主键的多行。拥有多个主键会对插入性能产生负面影响。ClickHouse 的重要限制之一是,一旦创建表,主键就无法更新。
Data skipping indexes
ClickHouse 查询性能与评估 WHERE 子句时是否可以使用主键成正比。我们有很多列,所有这些列都不能成为主键的一部分。因此,对这些列的查询将必须进行全面扫描,从而导致查询速度变慢。在传统数据库中,可以添加二级索引来处理这种情况。在 ClickHouse 中,我们可以添加另一类索引,称为数据跳过索引,它使用布隆过滤器并跳过读取保证不匹配的重要数据块。
ABR
我们在 requests_error 日志上构建了多个仪表板。加载这些仪表板通常会达到 ClickHouse 中为单个查询/用户设置的内存限制。
基于这些日志构建的仪表板主要用于识别异常情况。为了直观地识别指标中的异常情况,不需要确切的数字,但可以提供近似的数字。例如,要了解数据中心中错误的增加,我们不需要确切的错误数量。因此,我们决定使用围绕 ABR 概念构建的内部库和工具。
ABR 代表"自适应比特率" - 术语 ABR 主要用于视频流服务,其中服务器选择视频流的最佳分辨率以匹配客户端和网络连接。博客文章《解释 Cloudflare 的 ABR 分析》对此进行了详细描述。
换句话说,数据以多种分辨率或采样间隔存储,并为每个查询选择最佳解决方案。
ABR的工作方式是在向ClickHouse写入请求时,它将数据写入多个具有不同采样间隔的表中。例如table_1存储100%的数据,table_10存储10%的数据,table_100存储1%的数据,table_1000存储0.1%的数据等等。表之间的数据是重复的。 Table_10 将是 table_1 的子集。
Demo
在 Cloudflare 中,我们使用内部库和工具将数据插入 ClickHouse,但这可以通过使用开源工具 - vector.dev 来实现。如果您想测试 ClickHouse 的日志摄取是如何工作的,您可以参考或使用https://github.com/cloudflare/cloudflare-blog/tree/master/2022-08-log-analytics的演示。
确保您已安装 docker 并运行docker compose up
即可开始。这将打开三个容器,Vector.dev 用于生成矢量演示日志,将其写入 ClickHouse,ClickHouse 容器用于存储日志,Grafana 实例用于可视化日志。当容器启动后,访问 http://localhost:3000/dashboards
来使用预构建的演示仪表板。
总结
日志本质上应该是不可变的,而 ClickHouse 最适合处理不可变的数据。我们能够将关键且重要的日志生成应用程序之一从 Elasticsearch 迁移到更小的 ClickHouse 集群。
inserter 端的 CPU 和内存消耗减少了八倍。每个使用 600 字节的 Elasticsearch 文档在 ClickHouse 中减少到每行 60 字节。这种存储增益使我们能够在较新的集群中存储 100% 的事件。在查询方面,99分位的查询延迟也显著改善。
Elasticsearch 非常适合全文搜索,ClickHouse 非常适合分析!
不管是日志分析还是指标体系,都少不了监控告警。很多公司都会同时使用多个监控系统(云上的、云下的),导致监控事件散落各处,人员维护多份,缺少了告警聚合降噪、排班协同的能力。我们团队做了9年开源监控系统,深知大家的痛点,特推出 FlashDuty 事件 OnCall 中心的产品,一站式解决告警难题。
🛎️ 中心化告警处理,在正确的时间通知正确的人
💸 每一分钟都很关键,降低故障时间,就是赚钱
🖇️ 您常用的监控系统,我们都可以集成
告警事件的及时处理,对于线上稳定性保障至关重要。一款中心式的告警事件 OnCall 中心,去除告警风暴,确保告警不遗漏,还能分析故障处理的MTTA、MTTR等效率指标,您的团队值得拥有,快来免费体验起来吧:FlashDuty - 一站式告警响应平台。