工程

“写时模式”与“读时模式”之间的对比

Elastic Stack(或者通常所说的 ELK Stack)是一种存储日志的热门工具。

很多用户开始存储日志时,仅会解析出时间戳,可能还会添加一些简单标签以便筛选,此外就没有其他结构了。Filebeat 默认就是这样做的:尽快对日志进行 tail 操作并将其发送到 Elasticsearch,而不会提取任何其他结构。Kibana 的 Logs UI 也不考虑日志结构:包括“@timestamp”和“message”的简单模式就足够了。我们将这种日志处理方法称为最简模式。这种方法固然可以节省磁盘空间,但只能用于简单的关键字搜索和基于标签的筛选,除此之外便没有多大用处了。

minimal-schema.jpg

最简模式

随着对日志越来越熟悉,您通常希望对日志进行更多操作。如果发现日志中的数字与状态代码有关联,您可能希望对这些数字进行计数,从而了解在过去一小时内出现了多少次 5xx 级别的状态代码。Kibana 脚本字段能够让您在搜索时对日志应用模式,从而提取出这些状态代码并针对这些状态代码进行聚合、可视化以及其他类型的操作。我们通常将这种处理日志的方法称为读时模式

schema-on-read.jpg

读时模式

尽管这种方法十分便于开展临时性探索,但却有一大缺点,那就是:如果采用这种方法制作长期报告和仪表板,您每次执行搜索或者对可视化进行重新渲染时都需要重复运行字段提取工作。相反,一旦您固定下来所需的结构化字段,便可在后台启动一个 reindex 进程,以“持续”将这些脚本字段添加到永久性 Elasticsearch 索引中的结构化字段内。而且,对于流式传输到 Elasticsearch 的数据,您可以设置一个 Logstash 或者采集节点管道来通过 dissectGrok 处理器主动提取这些字段。

这就为我们引出了第三种方法,也就是在写入时对日志进行解析,以主动提取出上述字段。这些结构化日志能够为分析师提供很多额外好处,让他们无需在事件之后确定如何提取字段,还可提高查询速度,并且大幅提高他们从日志数据中获得的价值。这种适用于集中式日志分析工作的“写时模式”方法受到了很多 ELK 用户的欢迎。

schema-on-write.jpg

写时模式

在这篇博文中,我会详细讲解这些方法的利弊,以及如何从规划角度思考这个问题。我还会说明为何在前期解析日志结构有内在价值,以及我为什么认为随着您的集中式日志部署越来越成熟,即使您在前期基本没有结构,“写时模式”都是一个发展方向。

探索“写时模式”的优势(并消除误解)

首先说一下,您为什么要在将日志写入集中式日志集群时便解析日志结构。

更佳的查询体验。 当您从日志中搜索宝贵信息时,起初时会很自然地单纯搜索关键字,例如“error”(错误)。要想返回此类查询的结果,可以通过下面的方法来实现:将每个日志行都视作倒排索引中的一个文档,然后执行全文本搜索。然而,如果您想问一些更复杂的问题,例如“向我提供 my_field 等于 N 的所有日志行”,结果会怎么样呢? 如果并未定义 my_field 字段,您便不能直接问这个问题(不能使用 autocomplete(自动完成)功能)。而且,即使您知道自己的日志中包括此信息,为了将其与预期值进行比较,您在查询时也需要编写解析规则以将这个字段提取出来。在 Elastic Stack 中,如果您在前期解析日志结构,Kibana 的 autocomplete(自动完成)功能会自动提示字段和值来帮助您构建查询。这能够大大提高分析师的工作效率!现在,您和您的同事能够直接提问,无需你们每个人费心琢磨有哪些字段,也无需你们编写搜索时用来提取字段的复杂解析规则。

更快地完成历史数据的查询和聚合。 在 Elastic Stack 中对结构化字段进行查询时,即使涉及大量的历史数据,也可在几毫秒内返回结果。相比之下,如果使用通常的“读时模式”系统,则需要数分钟甚至数小时才能返回答案。之所以会有这样的差别,是因为与对每个日志行运行正则表达式来提取字段并进行数学运算相比,对前期已提取并索引的结构化字段进行筛选和运行统计聚合操作要快得多。“读时模式”对临时查询而言尤为重要,因为您不知道在调查时要运行哪个查询,所以前期工作并不能加快此类查询的速度。

从日志到指标。 这一点与上一点相关,从结构化日志中提取数值后得到的结果与数值型时序数据或者指标出奇地类似。从运营角度来说,对这些宝贵的数据点运行聚合操作能够产生巨大价值。如果数据量大,通过结构化字段,您能够将日志中的数值型数据点视作指标。

时间准确。 如果需要处理诸如“IP 地址至主机名”之类的字段,您必须在索引时应用模式,而不能于后期查询时再应用模式,这是因为如果后期处理的话,可能对之前的事务根本是无效的(一周后,该 IP 地址可能会链接至完全不同的主机名)。这一点适用于对仅提供映射关系最近快照的外部来源进行的查询,例如基于身份管理系统处理用户名,基于 CMDB 处理资产标签,等等。

实时异常检测和警报。 与聚合类似,数据量大时,如果使用结构化字段,实时异常检测和警报的效率最高。如不使用结构化字段,对集群长期处理能力的要求将是一项十分繁重的任务。我们与很多对创建警告和异常检测作业持犹豫态度的客户交谈之后,发现他们之所以止步不前,是因为对他们所需的警报数量而言,搜索时提取字段这种方法完全不具有可扩展性。这意味着他们收集的日志数据基本只适用于被动用例,进而限制了他们在项目上的投资回报率。

可观察性计划中的日志。 如果正在开展可观察性计划,您肯定知道仅简单地采集和搜索日志并不能满足需求。理想状况下,日志数据应该与指标(例如资源使用情况)和应用程序痕迹进行关联,从而让运营人员无论数据点来自哪里,都能全面了解服务的运行状况。进行关联时,结构化字段的效果最佳;否则查询速度会非常慢,而且对于拥有大规模数据量的实际情形,分析结果根本无法使用。

数据质量。 如果对事件进行前期处理的话,您将有机会检查无效、重复或缺失的数据,并纠正这些问题。如果依靠“读时模式”方法,由于前期并未验证您的数据的有效性和完整性,所以您并不知道返回的结果是否准确。这可能导致结果不准确,并致使您基于返回的数据得出错误结论。

细粒度访问控制。 对非结构化数据应用细粒度安全规则(例如字段级限制)是件极富挑战性的事。某些筛选条件能够在搜索时限制数据访问,这种方法虽有一定帮助,但却面临极大的限制,例如无法返回包含字段子集的部分匹配结果。在 Elastic Stack 中,字段级安全功能可让拥有较低权限集合的用户看到部分字段,而看不到整个数据集中的其他字段。所以要想保护日志中的 PII(个人身份信息)数据,同时让规模更大的用户集合针对其他信息进行操作,字段级安全功能可让您极其轻松、无比灵活地实现这一目标。

硬件要求

关于“写时模式”的一个常见误解是,这直接意味着您的集群需要更多资源来解析日志并同时存储未解析和已解析(或“已索引”)格式的日志。我们来看一下针对自己的具体用例您应该权衡的几个利弊点,因为答案实际上取决于多项因素。

一次性解析 vs 持续性字段提取。 解析日志并以结构化格式进行存储的确会在采集端消耗处理能力。然而,如果针对非结构化日志运行重复查询,并且此类查询会执行复杂的正则表达式语句来提取字段,那么长远来看这种做法消耗的 RAM 和 CPU 资源会多得多。如果您预计自己日志的常见用例仅仅是满足偶尔的搜索需求,则在前期解析日志结构可能的确有些小题大做。但如果您预计会对日志进行主动查询并对日志数据运行聚合操作,则采集时的一次性成本可能会低于在查询时重复运行同一操作的长期成本。

采集要求。 由于前期需要进行额外处理,所以如果不采取其他措施的话,您的采集通量可能会比正常情况稍慢一些。您可以引入额外的采集基础设施来处理这一负荷,只需单独扩展您的 Elasticsearch 采集节点或者 Logstash 实例即可。关于如何完成这一操作,有一些很好的资源和博客文章可供参考,如果您正在使用 Elastic Cloud 上的 Elasticsearch Service,扩展采集节点的工作无比轻松,只需添加更多“能够采集”的节点即可。

存储要求。 尽管与人们的正常认知相违背,但实际情况却的确如此:如果您在前期做一些额外工作来理解自己日志的结构,存储要求反而会较低。日志可能会十分冗长,并且包含很多无用内容。如果在前期对它们进行研究(即使不全部解析每个字段),您便能够决定在自己的集中式日志集群中在线存储哪些用于搜索的日志行和已提取字段,哪些内容可以立即进行归档。存储冗长且包含无用信息的日志时,这一方法能够降低整体磁盘要求。Filebeat 提供轻量型 dissectdrop 处理器,正好可以实现这一目的。

即使由于法规要求您必须存储每个日志行,也仍有方法来优化与“写时模式”相关的存储成本。首先,控制权在您;您并非必须解析全部日志结构:如果用例允许的话,仅添加几项重要的结构化元数据即可,而无需解析其他日志行。另一方面,如果完整解析日志结构的话,即所有重要信息都以结构化方式加以存储,则您没有必要在存储已索引日志的同一集群中保留“源”字段,可以将它们归档到费用较低的存储选项。

如果存储空间仍然是一个大顾虑,还有很多方式可以优化 Elasticsearch 的默认配置;您只需进行些许调整,即可将压缩率降下来。对于保留期较长但访问不太频繁的数据,您还可以使用热温架构冻结索引来最充分利用自己的存储空间。然而,务必记住,对于频繁使用的数据,相较于在最需要的时候不得不等待数分钟来获得查询结果,存储费用还是要相对低一些的。

在前期定义结构

我们听到的另一个误解是:在存储之前就解析日志结构操作起来很困难。我们接下来打破这一误解。

结构化日志。 很多日志在生成时便采用结构化格式。大部分常见应用程序都支持直接生成 JSON 格式的日志。这意味着,您开始时便可直接将日志采集到 Elasticsearch 中,无需进行解析便可将它们以结构化格式进行存储。

预构建解析规则。 目前有数十个 Elastic 正式支持的预构建解析规则。举例说明,Filebeat 模块会为您解析已知供应商日志的结构,而 Logstash 则包含大量的 Grok 模式库。社区内还有更多预构建解析规则。

自动生成的解析规则。 可以使用诸如 Kibana Data Visualizer(Kibana 数据可视化工具,可以自动提示如何对日志进行解析)等工具,来帮助定义从自定义日志中提取字段时所用的规则。只需将一段日志样本粘贴过去,然后便能获得可在采集节点或者 Logstash 中使用的 Grok 模式。

如果日志格式发生变化呢

我们听到的最后一项误解是,如果采用“写时模式”方法的话,会更加难以应对日志格式发生变化这种情况。这种说法是完全错误的:无论您采用哪种方法(前期或发生之后)从日志中提取情报,总归要有人应对日志格式发生变化这种情况,当然前提是您的需求不仅限于“全文本”搜索。无论您使用采集节点管道在索引时对日志进行 Grok 解析,还是使用 Kibana 脚本字段在搜索时完成同样的任务,当日志格式发生变化时,您都需要对字段提取逻辑进行修改。请注意,对于我们维护的 Filebeat 模块,我们会跟踪上游日志供应商何时推出新版本,而且会在测试之后更新兼容性。

可以通过多种方法应对写入时日志结构发生变化这种情况。

提前修改解析逻辑。 如果知道日志格式即将发生变化,则您可以创建平行的处理管道,并在过渡期间同时支持两种日志版本。这通常适用于您在公司内部可以控制的日志格式。

在解析失败时以最简模式写入。 并非所有变更都会提前通知,有时您控制范围之外的日志会不予通知便发生变更。您可以从一开始的时候便在日志管道中将这种可能性考虑在内。如果 Grok 解析失败,则以最简模式写入(仅包括时间戳和未解析消息),并向运营人员发送警报。接下来,运营人员便可针对新日志格式创建脚本字段,从而避免分析工作流发生中断,然后修改之后的管道,并考虑对短暂的解析逻辑中断期间的日志重新索引字段。

解析失败时推迟编写事件。 如果以最简模式写入起不到帮助作用的话,则您可以在解析逻辑失败时不写入日志行,而是将事件存储到“dead letter queue”(未处理消息队列,Logstash 提供这一开箱即用的功能),并向运营人员发送警报,运营人员然后便可修复逻辑问题并通过新的解析管道从“dead letter queue”重新运行事件。这会造成分析中断,但是您无需处理脚本字段,亦无需进行重新索引。

一个极为恰当的类比

这是一篇特别长的博文,如果您读到这里的话,我要给您一个赞!恰当的类比可以很好地帮助我理解深奥概念。和 Elastic 的安全专家 Neil Desai 交谈之后,我最近听到了一个关于“读时模式”和“写时模式”之间对比的最恰当类比,希望也能帮助到您。

结语:需要您自行定夺

如我在最开始所说的那样,对于在“写时模式”和“读时模式”之间如何选择,并没有适用于每个集中式日志部署的统一答案。实际上,我们看到的大部分部署都介于两者之间:它们会详细解析某些日志的结构,而会将其他日志保留为最基本模式(@timestamp 和 message)。这完全取决于您要用日志实现什么目的,以及您更看重结构化查询的速度和效率,还是更希望无需操心前期工作便将数据尽快写入磁盘中。Elastic Stack 同时支持这两种方法。

如要开始使用 Elastic Stack 处理日志,您既可以通过 Elasticsearch Service 快速部署一个集群,也可以进行本地下载。同时,欢迎试用我们 Kibana 中的全新 Logs 应用,该应用能够优化您处理日志时的工作流,无论日志是何形式或格式,也无论是结构化还是非结构化日志,都不在话下。