日志记录实战-全-
日志记录实战(全)
原文:Logging in Action
译者:飞龙
前置内容
前言
软件是当今大多数行业的生命线,并且可以成为那些能够快速迭代并在竞争对手之前找到客户价值的公司的差异化因素。一些允许大型组织快速行动的近期趋势包括采用云平台和微服务架构。尽管一些趋势已经演变,但有一点是始终如一的:当事情出错时,我们需要快速了解去哪里查找以解决问题。微服务和短暂的云基础设施(容器等)加剧了这个问题。
我清晰地记得几年前为一个客户解决一个特别棘手的分布式问题,其中一组服务会相互通信以提供某些业务功能。在六天后(几乎精确到点!),这组服务全部崩溃。由此造成的故障给客户造成了重大的收入损失。客户决定在四天后逐个重启所有服务以避免这个问题。
在观察系统几天后,我发现参与调用图的全部服务的内存使用量显著增长,所以我与客户合作,安全地捕获内存和线程转储以了解发生了什么。我确定某个缓冲区正在被填满,但查看代码时很难确定为什么会发生这种情况。系统在各个线程上包含了阻塞和非阻塞代码,这使得工作变得困难。我不得不求助于与分布式系统一起工作的经过验证的基础知识来诊断问题:记录事件。
在花了几天时间勤奋地审查了成千上万条日志行后,我能够看到系统中流动的某些消息组合触发了所有服务的内存泄漏,这最终会在服务中引发“OOM”或内存不足事件。
尽管日志记录在这个努力中起到了显著的作用,但并不容易。日志记录在服务之间不一致,时间戳错误,从机器中提取日志所使用的技术有时会失败、崩溃或损坏日志文件。由于客户不能承受停机,我们在四天后重启服务时也丢失了宝贵的日志数据。如果客户有一个更好的日志和可观察性架构,许多问题都会简化,并且会缩短定位 OOM 问题的耗时。
在这本书中,Phil Wilkins 出色地传达了良好日志模式的原理,并通过使用一种称为 Fluentd 的通用日志收集和聚合技术,用具体的科技和示例进行了展示。Fluentd 用于从各种系统中收集、统一和流式传输日志数据到集中式数据存储,然后可以用于适当的分析。Phil 引导读者构建一个日志系统,考虑到诸如时间戳、结构化可读数据以及更复杂的事物,如路由和日志数据的按摩。
如果你正在构建分布式系统,如微服务架构,你将需要认真考虑你的日志和可观察性架构来支持你的日常运营。这本书将是你踏上旅程的有用伴侣。
—克里斯蒂安·波斯塔,Solo.io 全球现场 CTO
我在七年前开始了 Fluentd 的旅程,将其作为微软 Azure 日志分析 Linux 代理的核心项目进行集成。最初的学习曲线颇具挑战性;然而,我们从不断增长的社区、插件生态系统以及易于扩展性中获得的益处,使得该项目在 Azure 环境中成为最受欢迎的。随后,我跳槽到 Treasure Data,在那里管理该项目,之后加入了 Elastic,在那里我了解了其他日志工具集。在远程欣赏 Fluentd 之后,我终于离开了 Elastic,创立了 Calyptia 公司,这是一家围绕 Fluentd 生态系统建立的公司,并成为了一个项目维护者。
当我开始担任维护者时,我深入社区,调查用户关于他们的痛点以及我们如何改进的地方。社区强调了他们在入门方面的知识差距以及如何找到某些主题的深入解释,并要求提供更多具体的示例。
在一个愉快的巧合中,我在与社区聊天时遇到了 Phil Wilkins,并有幸阅读了他的作品《Logging in Action》。Phil 在解读复杂主题并提供易于理解的视觉和指导方面具有巨大的天赋。《Logging in Action》通过架构细节和深入的分步解释,填补了社区中的许多空白。
对于刚开始接触可观察性领域或已经在生产中运行 Fluentd 的用户来说,通过深入研究 Fluentd 的插件架构和多 worker/multithread 架构,最大化 Fluentd 的性能将非常有价值。所有这些示例都伴随着简单的配置和逐行解释,以便在您的环境中进行定制。
除了基本的入门知识之外,《Logging in Action》还深入探讨了重要的实际用例和商业价值。其中一些我最喜欢的包括减少日志量,这可以降低使用昂贵后端用户的成本,以及如何使用 Fluentd 将数据路由和发送到多个目的地。这两个用例示例在过去几年中会极大地简化我的工作。
随着 Fluentd 进入其发展的第十年,用户每天使用 Docker 部署生态系统项目超过 200 万次,很难找到一个现代的 Kubernetes 服务或云提供商不提及这些基本工具。我强烈推荐使用《Logging in Action》作为入门指南或复习资料,或者作为优化您日志之旅的方法。
——Anurag Gupta,Fluent 维护者及 Calyptia 联合创始人
前言
在某些方面,这本书的开发时间几乎和我从事软件行业的时间一样长。考虑到我的 IT 生涯始于 20 世纪 90 年代初,这听起来可能有些奇怪。我早期就意识到了记录和将错误事件转化为诊断和问题解决的重要性。这些经验来自于担任一个关键产品开发项目的年轻主导开发者,进行全天候的系统测试。如果显示器上没有显示问题,就会认为是演示系统的问题,所以找 Phil——他现在需要解决问题,哪怕是在一些不合适的时间或夜晚。现实情况是,演示子系统很少出问题。错误通常源于众多复杂的后端系统之一,它们发送错误数据或尝试使用错误的接口版本进行通信。我越改进日志记录以帮助显示是否已将数据发送到显示系统,收到的电话就越少。
这些年来,我见证了不断追求交付功能性和特性,而有时却忽略了非功能性方面所需的关注。功能性目标总是优先于非功能性考虑。作为软件开发者,我们在监控和日志记录方面可能会成为自己最糟糕的敌人。当我们的代码运行顺畅并通过所有单元测试时,编写日志事件并不那么令人兴奋。从决策者的角度来看,功能已经完成,并且按照约定的时限完成,那么为什么还要花更多时间在解决方案上呢?
事实上,我们常常收集日志并将它们放在一个阴暗的地方,直到出现问题。日志记录永远不会是一个热门话题,但它至关重要,而且当做得好的时候,它可以使我们做一些聪明的事情。良好的日志记录使得机器学习和人工智能能够用于模式识别或直接通知相关人员解决问题。你甚至可以做到检测日志事件并触发清理过程以避免问题。IBM 曾经将自我保护和自我修复过程的理念称为“自治”——听起来现在更有趣了吗?
我的开源背景大约是在我转向使用 Java(1.4 版本刚刚发布)的同时开始的,从 Log4J 这样的库开始。这逐渐发展到更大的开源解决方案,如 JBoss v3 应用服务器,然后在 Red Hat 收购这些业务之前,与 Fuse(Apache CXF、Camel、ServiceMix 和 ActiveMQ)合作,为这些框架构建和提供服务。真正开源解决方案的伟大之处在于其供应商无关的特性,这意味着它可以有适配器和插件来覆盖各种来源,使其易于集成。一种有利于集成事物的架构将鼓励这样的生态系统蓬勃发展,这正是 Fluentd 所做的。
故事的最后一条线索来自我对知识共享的看法。我看到人们用“知识就是力量”的理念作为保留尽可能多的理由,迫使人们去找那些让自己变得不可或缺的人。我总是几乎完全相反地解释这个理念。我不想变得不可或缺,因为这意味着你将日夜被召唤。最好是与他人分享你的知识;人们更有可能欣赏它,并在将来(在你设定的条件下)回报你。直到我参与 Oracle 中间件及其用户社区,以及后来的合作伙伴社区,我才找到了一群志同道合的人,他们鼓励我写作和分享。我的作者之旅真正开始了。
我最初“认真”接触 Fluentd 是在我审视 CNCF 生态系统,看看孵化器中有哪些解决方案时。CNCF 孵化项目预示着可能的技术未来演变。在 Fluentd 中,我发现了一个工具,它提供的不仅仅是让 Splunk 吸取日志文件的功能。Fluentd 为将日志管理推向激动人心的领域创造了机会,并在混合和多云环境中解决了许多重要的日志管理挑战。然而,我认为它在解释和连贯地展示 Fluentd 的能力和潜力方面没有得到充分的体现,因此这本书应运而生。这可能会让人认为 Fluentd 的文档很糟糕,但事实并非如此。然而,在线文档更像是一本字典,而不是指南。它没有解决在应用配置时应该寻找什么,以及为什么会有这样的问题。
致谢
这本书是我第一次独立写作的冒险,也是我与 Manning 的第一次合作,它让我意识到,一本好书所付出的努力远比表面所见的多。但我希望你会同意,Manning 编辑团队的推动和鼓励意味着这本书会为你带来价值。
我要感谢曼宁出版社的每一个人,尤其是凯蒂·斯波萨托·约翰逊和安德鲁·沃尔德伦,他们一直陪伴我走过这段冒险之旅,还有凯丽·安德鲁斯,她最终使内容变得完美。
在编写这本书的过程中,我们得到了志愿者审稿人和 MEAP 读者的支持。他们的反馈非常有帮助和洞察力。在这个过程中,阿努拉格·古帕塔和埃德华多·席尔瓦·佩雷拉,作为 Fluentd 和 Fluent Bit 的技术和产品负责人,主动联系并抽出时间与我讨论 Fluentd 和 Fluent Bit,并参与到审稿过程中。感谢大家的时间和反馈。
我作为作者的经历如果没有多年的支持和鼓励是不可能开始的。那些参与我成为 Oracle Ace Director(在我的情况下,类似于 Java 摇滚明星或微软 MVP)之旅的人是这段旅程的核心。由此延伸,感谢过去和现在在凯捷和 Oracle 的同事们——一如既往,感谢你们,希望我们有机会再次在旅行安全时一起享受美食和饮料。
最后,也是最重要的一点,如果没有我的妻子凯瑟琳以及我们的两个儿子克里斯托弗和亚伦的支持和理解,这本书根本不可能完成。在我花费晚上的时间和周末在电脑前而不是陪伴他们的时候,所有的爱都给你们。
向所有审稿人致谢:亚历克斯·塞斯、安德烈亚·C·格兰塔、安德烈斯·萨科、克利福德·瑟伯、科诺尔·雷德蒙德、埃利亚斯·兰格尔、乔治·托马斯、乔尔·霍姆斯、约翰·古德里奇、卡纳克·克什特里、肯特·R·斯皮尔纳、凯里·E·科伊茨奇、马里奥-莱安德·雷伊默、迈克尔·布莱特、米哈尔·鲁特卡、雷蒙德·张、萨特杰·库马尔·萨胡、素法·方、西达尔特·马斯达南、西梅恩·莱泽宗、斯蒂芬·赫尔韦格、苏雷什·科亚、特伦特·怀特利、瓦姆西·克里希纳和佐希布·阿因波雷,你们的建议使这本书变得更好。
关于这本书
《Logging in Action》这本书的编写是为了帮助人们充分利用 Fluentd,并思考日志如何让我们的生活变得更简单。是的,这本书专注于 Fluentd,但正如你将看到的,它是最有影响力的日志工具之一。
开发软件所花费的时间只是我们产生的代码生命中的一小部分,这些代码需要保持运行。日志越弱,20 年或 30 年后理解和照顾这些系统就会越困难。考虑一下:在 2020 年,路透社援引报道称,大约有 2200 亿行 COBOL 代码(www.bmc.com/blogs/cobol-trends);Linux 内核于 1991 年发布,所以软件会持续存在。编写良好的日志和最大化日志工具和框架可以为帮助做出巨大的贡献。你不需要是一位超级明星级的热门开发者或精通内核配置黑暗艺术的系统管理员,就能从这本书中受益;作为作者,我也不认为自己是这两者之一。
Fluentd 以及 Prometheus 等各种技术与云原生解决方案紧密相关。但不要因此却步;即使你正在使用主机上的 COBOL 77,你仍然需要了解正在发生的事情。像 Fluentd 这样的工具只是以可以解决云原生所增加的需求(如容器集成、超大规模和高度分布式解决方案跨越数据中心和托管供应商)的方式处理了监控、测量和警报的问题。因此,本书的大部分内容都专注于日志问题,无论位置如何。Fluentd 解决了容器、超大规模等问题,因此我们在本书最先进的部分探讨了这些问题。
每个人都对日志和监控是什么以及应该如何使用都有自己的先入之见。这些先入之见受到我们作为开发者、系统管理员、数据库管理员、安全专家等日常工作的 影响。我希望这本书能帮助你看到你未曾考虑过的其他视角。例如,当我们查看日志时,我们通常考虑的是治疗而不是预防。我希望这本书能让你考虑那些有助于在处理日志事件时采用预防性或至少更响应性方法的观点。
适合阅读本书的人群
《日志实战》面向所有参与开发、配置或运行 IT 解决方案的实践任务的人员,例如那些在支持团队中努力保持一个过时的、未记录的软件运行一整天的人。对于正在考虑降低系统运行成本以释放下一笔资金用于下一个酷炫增强功能的架构师。对于编写代码的开发者,他们不希望凌晨 3 点被叫醒去解决问题,因为日志并不清楚发生了什么错误导致代码失败。对于 IT 行业中的任何人,如果他们认为现在是时候“回馈社会”并尝试减轻理解或预防 IT 问题之痛,这本书都将是你的良伴。最终,如果你想从日志中获得更多,这本书应该能为你提供一些帮助。
本书组织结构
本书分为四部分,共涵盖十一章和五章附录。章节带你了解如何做事,附录提供了大量的参考资料和额外的支持资源及工具。
第一部分
第一部分概述了主要思想,详细介绍了 Fluentd 的架构、Fluentd 可以支持的使用案例和机会,以及部署 Fluentd 的先决条件。我们以经典的动手实践“Hello World”示例结束本节:
-
第一章 从日志统一的电梯简报基础开始,回顾了日志和 Fluentd 背后的背景和基本思想。我们探讨了不同的用例和日志的不同视角,并检查了 ELF 和 EFK 软件堆栈,以及这些事物之间的差异和共性。
-
第二章 讲解了日志事件的构成,时间的重要性(尤其是对于分布式解决方案),Fluentd 的架构以及这些如何影响决策。接下来,我们讨论了部署和运行基本 Fluentd 配置所需的足迹。我们遵循传统,创建 Fluentd 的“Hello World”程序等价物。
第二部分
第二部分深入到与 Fluentd 一起工作的细节,展示了捕获日志事件、路由、过滤和输出事件的机制。我们提供了将日志事件的处理从移动数据转变为使日志更有意义、关键的是可操作和/或可衡量的实际步骤:
-
第三章 主要讲述如何捕获日志事件。我们探讨了最常见的来源,例如日志文件,并阐述了处理此类数据的细微差别。我们如何通过解析器从日志事件中提取更多意义?当然,如果我们正在监控所有其他内容,我们如何监控监控器呢?
-
第四章 探讨了这个问题:捕获事件后,我们将如何处理它们?我们检查了帮助提高 I/O 性能的缓冲区,但最终将日志事件存储在结构化文件或类似 NoSQL 数据库的存储库中。然后我们探讨了支持事件后分析的方法(例如,在 Elasticsearch 中挖掘事件),并探讨了如何更加积极主动,发现重要事件并在它们发生时通过 Slack 通知。
-
第五章 讨论了谁需要日志事件,以及我们如何将事件发送到正确的位置?安全人员希望所有数据都包含在他们专业的数据挖掘工具包中。运维人员不希望日志充斥环境,但他们希望任何有助于他们发现生产问题和将有意义的数据发送给正确人员的工具。
-
第六章 探讨了从日志中获得更多意义。让我们将日志从数据转变为信息。我们需要向日志事件中注入额外信息以提供有价值的上下文吗?如果是这样,我们该如何做?
第三部分
第三部分带我们进入 Fluentd 最先进的部分,探讨了在经典部署场景和容器化环境中部署、性能和扩展。我们解决了构建自己的插件以处理现有插件无法满足我们需求的利基情况:
-
第七章讨论了 Fluentd 如何应用于扩展(无论是静态还是动态),以及它如何在分布式多服务器和集群环境中运行,包括仅限本地、混合和多云因素。我们还探讨了如何将弹性集成到部署中,以便日志事件继续流动。
-
第八章探讨了在 Kubernetes 和 Docker 中配置 Fluentd 以捕获应用程序事件以及监听由这些平台技术本身生成的日志事件。
-
第九章针对那些希望用自己的插件为 Fluentd 社区做出贡献或需要开发一些东西来解决他们自己的特定问题(目前还没有插件可以提供帮助)的人。这是唯一一章,拥有一些开发经验将会很有益处。
第四部分
第四部分探讨了 Fluentd 及其日志处理能力的好坏取决于所创建的日志事件。我们检查了什么构成了好的日志事件。将日志事件输出到文件系统并不是最佳解决方案,因此我们探讨了更有效地将日志事件传递到 Fluentd 的不同方法:
-
第十章描述了日志分类的有效使用,以及可以增加日志事件价值和用途的信息类型,以及我们如何使这些信息可用。这包括考虑记录敏感数据的影响。
-
第十一章探讨了日志框架以及它们如何简化在不同语言中处理日志事件。本章检查了如何使用比日志文件更直接的技术将日志事件发送到 Fluentd。避免这一步骤可以提高我们设置的效率和灵活性。我们参观了众多日志框架的组织方式,并直接将这些框架连接到 Fluentd 而不是通过日志文件,包括如何在应用程序锁定使用 Fluentd 之前完成这一操作。
附录
附录中包含了在使用 Fluentd 时有助于快速参考的内容,以及许多资源,可以帮助您了解更多关于相关主题和有用工具的信息。除了 Fluentd 和 LogSimulator 之外,我们还涵盖了用于演示 Fluentd 各种方面的产品安装。我们将这些内容放在附录中,以避免对书籍流程造成任何干扰:
-
附录 A将指导您安装不同的工具和运行书中所有示例、场景和练习所需的配置,如果您想深入了解。
-
附录 B帮助处理时间和日期以及由于编程语言差异而可能不同的正则表达式。本附录提供了方便的查找,以解决这些问题。
-
附录 C讨论了 Fluentd 存在于插件的世界中,这应该有助于您识别可能有所帮助但尚未在章节中使用过的插件。这不是一个详尽的列表,但它指出了一些可能迟早会很有用的插件。
-
附录 D 讲述了我们如何应用日志管理来为大型组织带来显著改进的故事。为了保护无辜者,我对一些细节进行了模糊处理。然而,如果您试图帮助您的组织采用更好的日志和监控实践,这应该会提供一些想法。
-
附录 E 针对日志和 Fluentd 触及 IT 许多方面的现实情况。我们并没有试图详细涵盖所有内容,因为这会导致一本书变得如此庞大,以至于我们无法拿起它,所以我们已经确定了一系列我们认为可以帮助的外部资源。
关于代码
这本书包含了大量 Fluentd 配置和源代码的示例,无论是编号列表还是与标准文本并列,源代码都以固定宽度字体的形式呈现,以便与普通文本区分。
在大多数情况下,我们限制了本书只展示配置文件的相关部分。所有列表标题都提供了对完整代码或配置的参考。本书不包括配置或代码注释,但我们保留它们在原始源文件中,以使代码紧凑且易于在页面上阅读。在极少数情况下,即使这样也不够,列表中还包括了行续接标记(➥)。
本书示例的源代码可以从出版商的网站www.manning.com/books/logging-in-action或 GitHub 仓库github.com/mp3monster/LoggingInActionWithFluentd下载。我希望随着时间的推移,我们能够将更多示例添加到这个仓库中。
liveBook 讨论论坛
购买《Logging in Action》包括免费访问 Manning 的在线阅读平台 liveBook。使用 liveBook 的独特讨论功能,您可以在全局或特定章节或段落中添加评论。为自己做笔记、提问和回答技术问题,以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问livebook.manning.com/#!/book/logging-in-action/discussion。您还可以在livebook.manning.com/#!/discussion了解更多关于 Manning 论坛和行为准则的信息。
Manning 对我们读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要本书有售,论坛和先前讨论的存档将可通过出版商的网站访问。
关于作者
![]() |
Phil Wilkins 在软件行业工作了超过 30 年,为各种不同的企业和环境工作,从跨国公司到软件初创公司,从雷达到零售,再到商业医疗保健。他最初是一名实时解决方案的开发者,并逐步担任了技术领导角色,主要在基于 Java 的环境中工作。Phil 曾作为咨询架构师和技术倡导者加入 Oracle,此前在 Capgemini 工作,专注于云集成、API 设计和非功能性考虑,如日志记录和监控。他是英国一个多奖获奖的 PaaS 团队的成员,使用供应商特定的和开源技术;他在与知名英国和国际品牌打交道时担任客户 facing 角色,为交付团队提供内部支持。他与交付团队的合作集中在技术专长、开发和发展最佳实践以及领导创新项目。他是 TOGAF 认证的。在日常工作之外,Phil 积极参与以各种方式支持开发者社区,包括作为伦敦 Oracle 开发者 Meetup 的共同组织者、期刊文章和博客的作者,以及在英国和世界各地的会议上的演讲者。自 2019 年以来,Phil 对开源和 PaaS 的贡献得到了 Oracle 的认可,并被认证为 Oracle Ace Director。 |
|---|
关于封面插图
《Logging in Action》封面上的插图是“Fille Bratzke à Udinskoi Ostrog”,或称乌迪诺伊奥斯特罗格的 Bratzke 女孩,出自雅克·格拉塞·德·圣索沃尔 1797 年出版的一本书。每一幅插图都是手工精心绘制和着色的。
在那些日子里,人们通过他们的服饰就能轻易地识别出他们居住的地方以及他们的职业或社会地位。Manning 通过基于几个世纪前丰富的地方文化多样性的封面设计,以及像这一系列这样的图片,来庆祝当今计算机行业的创新精神和主动性。
第一部分:从零到“Hello World”
任何一部优秀的惊悚片都是从介绍其主要角色开始的。他们的动机、背景、以及优点和缺点都会被展示出来。关键角色所运作的环境在前 20 分钟内就会展现出来。
这就是本书的第一部分所讲述的内容。第一章介绍了我们的英雄——Fluentd(以及其兄弟 Fluent Bit);我们通过上下文、用例等来设定场景。如果你还在探索 Fluentd 是什么,或者正在思考如何向同事们推销 Fluentd,这里有很多值得思考的内容。
如果第一章是关于我们的主要角色,那么第二章将探讨 Fluentd 可以运作的环境。我们将通过安装 Fluentd 的第一步实际操作来逐步进行,并遵循 Brian Kernighan 创立的优良传统,第一个解决方案将是“Hello World”。
1 Fluentd 简介
本章涵盖
-
检查日志和日志事件的使用案例
-
确定日志统一的价值
-
区分日志分析和统一日志
-
理解监控概念
-
理解 Fluentd 和 Fluent Bit
在深入了解 Fluentd 之前,我们应该首先关注使用 Fluentd 等工具的动机。日志如何帮助我们?日志分析是什么,为什么日志统一是必要的?这些问题是我们将在本章中努力回答的。我们将强调日志可以帮助或使我们能够实现的活动类型。
让我们退一步,理解一些关于系统如何衡量和监控的当代思考;理解这些想法意味着我们可以更有效地使用我们的工具。毕竟,一个工具的好坏取决于创建配置或生成用于日志事件的用户的技能。
在我们这样做的时候,探索 Fluentd 如何演变以及它为什么在行业中占据其位置是值得的。如果你正在考虑 Fluentd 作为可能的工具,或者想要为其采用做出案例,那么了解它的“起源故事”是有帮助的,因为这将告诉我们 Fluentd 可能被如何看待。
1.1 Fluentd 的简短介绍
由于你在看这本书,我们假设你至少听说过 Fluentd,可能对其有一个模糊的了解。让我们从 Fluentd 和 Fluent Bit 的“简短介绍”开始。
Fluentd 及其兄弟产品 Fluent Bit 的主要目的是从各种可能的来源(如网络交换机、操作系统、自定义应用程序和预构建应用程序,包括平台即服务和软件即服务)捕获日志事件。然后,它将这些事件传递到适当的工具,以便对日志事件进行处理,提取意义和洞察,并可能触发操作。Fluentd 的主要工作不是自己执行详细的日志分析,尽管它可以提取意义,如果需要,更深入的分析可以集成到其配置中。
通过统一影响我们解决方案操作的日志来源的日志事件,我们有看到整体情况的机会。例如,数据库中的错误是否是应用程序返回给用户的错误的原因,或者数据库错误是操作系统无法写入存储的症状?
1.1.1 什么是日志事件?
我们已经用日志事件来描述 Fluentd,那么什么算是日志事件呢?日志事件最好这样描述:
-
日志事件是可读的信息,主要是文本性的。文本信息可以从非结构化到高度结构化。
-
每个日志事件都有一个时间点,由时间戳定义(通常是绝对时间 01:00:00 1 Jan 1970,但可能是相对时间 +0.60),或者时间可以通过日志事件在一系列事件中的位置来推断。
-
每个事件都与一个显式或隐式的位置关联,该位置可以与在物理或逻辑位置上运行的组件关联。
让我们举例说明。任何有编程经验的人都会很可能认出图 1.1 中显示的截图是日志输出的摘录。在这种情况下,输出是由 Fluentd 生成的。如您所见,事件有一个时间戳;一个位置,它来自事件发生的宿主;以及一些额外的半结构化内容。

图 1.1 Fluentd 的日志输出
1.1.2 Fluentd 与中间件的比较
与中间件(例如,Apache Camel、MuleSoft、Oracle SOA Suite)合作过的人会欣赏将 Fluentd 描述为一种专注于日志的企业服务总线这一想法。图 1.2 展示了这一点,其中包含了输入和输出的概念以及路由和转换日志事件的能力。随着本书的深入,这一点将变得更加明显。

图 1.2 展示不同类型的 Fluentd 插件及其与核心关系的插图
备注:如果您想进一步探索这个类比,您可以考虑阅读 Tijs Rademakers 和 Jos Dirksen 所著的《开源企业服务总线实战》(Manning,2008)的 liveBook 版本,网址为mng.bz/Nx6n。
定义 中间件 是一个通用术语,涵盖为软件应用提供服务的软件,这些服务超出了操作系统可提供的服务。通常这涉及到连接不同的软件组件。有时它可以被描述为“软件胶水”。
定义 企业服务总线 是一种特定的中间件类别,用于以近乎实时的方式在软件组件之间传递数据。这通常包括不同软件组件执行顺序的排序。
1.2 我们为什么会产生日志?
我们出于各种原因创建日志条目。日志的一些用例可能只需要很少的时间,但在需要时却非常有价值。我们所能想到的几乎所有用例都将属于以下类别之一:
-
调试 —知道在某个场景中哪些代码部分正在执行,使得隔离错误变得容易。是的,我们有调试器等工具,但通常在日志中添加几行信息来帮助也是同样容易的。其中一些日志消息将被保留,以确保在生产过程中一切运行正常。在其他情况下,日志消息的某些行可能在我们开发或测试软件时被禁用。请注意,我们绝不会建议尝试使用调试器连接到生产环境。允许生产系统记录用于调试的信息应该是在理解可能后果的情况下进行的(本书后面我们将探讨为什么是这样)。
-
意外数据值或异常情况发生 — 当代码遇到超出范围的数据值时,有时最好是标记并继续执行,就像你会在以下情况中看到的那样:
-
在 switch 语句中使用默认条件,当代码应该有一个你在 switch 中允许的值时。但由于其他地方的变化或错误,你的代码需要优雅地处理这种情况并使其为人所知(例如,经典的问题是在表现层[UI]与后端支持的数据值不同):
switch (caseSwitch) { case 1: // do something expected break; case 2: // do something expected break; default: System.Diagnostics.Debug.Write("Unexpected " + caseSwitch); // unexpected path – log this as it may be indicative // of a bug break; } -
应用防御性编程。例如,在使用对象变量之前,检查它是否不为空——这是一个在首次加载配置数据时确保一切如预期的标准操作。
-
当处理连接问题的代码遇到错误,你将回退并再次尝试时,需要报告这种情况。这样我们才能从日志中了解影响用户体验的缓慢响应的原因。
-
-
审计和安全 — 我们生活在一个内部和外部行为者试图获取数据以进行非法使用的世界中。为了帮助我们监视滥用行为,我们需要了解正在发生的事情。事件需要被记录,如果没有报告。有时这是为了寻找异常行为模式,有时是为了表明系统已经按照预期完成了所有操作。我们经常看到这种用法案例被称为取证日志或应用安全监控以及安全信息和事件管理(SIEM)。将可以创建审计跟踪的日志事件汇集在一起是很重要的。单个异常事件可能微不足道。但当你看到同一种事件以不寻常的方式定期发生时,随着时间的推移,它可能指向更可疑的事情。
记录、安全和日志取证
为了深入了解日志取证,本文提供了一些关于使用日志的现实情况的见解:bit.ly/Fluentd-ForensicLogging。此外,这篇 Gartner 文章也为这一领域增添了更多色彩:bit.ly/AppSecurityMonitoring。
美国国家标准与技术研究院(NIST)还在“计算机安全日志管理指南”中提供了关于安全目的日志记录的优秀指南(mng.bz/ExWd)。虽然标题可能暗示内容是为安全专家准备的,但它确实为 IT 行业的任何人提供了进入这种日志应用的良好途径。
-
根本原因分析—有时我们看到一个问题,但原因并不明显。通常这是因为我们只查看一小部分组件的日志。例如,一个基于其日志的应用程序似乎随着时间的推移而变慢,但没有内存泄漏的证据。只有当我们从所有来源汇总日志时,我们才能确定原因,并将其他问题作为副作用分离出来。例如,我们的应用程序可能运行良好。然而,我们在同一服务器上使用另一个服务,该服务从未正确释放 CPU 线程,导致服务器逐渐耗尽资源以运行所有应用程序。但这种情况只有在所有信息都展示在一起时才能被发现。
-
确定性能问题的原因—如Prometheus (
prometheus.io/)和Grafana (grafana.com/)这样的工具因收集指标数据以提供正在运行的软件性能的见解而闻名。虽然数据可能显示正在发生的事情,但它并不一定告诉你原因。是文本日志描述了正在发生的事情——无论是数据库查询日志还是应用程序线程跟踪。 -
异常检测—虽然一个系统在测试解决方案时可能看起来运行得很好并产生预期的结果,但在系统的常规操作中,结果中可能会出现异常。日志记录可以通过在出现异常时帮助找到日志事件之间的相关性,从而促进此类问题的检测,提供原因的指示。
这种情况的例子之一是 20 世纪 90 年代英特尔奔腾 FDIV 错误的发生,其中特定奔腾处理器的设计错误意味着尽管软件运行完美,但在某些特定条件下的一些计算产生了错误的结果。如果我们即使在软件按预期运行时记录事件,如重要计算的结果,那么就更容易发现任何可能的异常,并检查活动以确定异常的来源(更多详情,见
en.wikipedia.org/wiki/Pentium_FDIV_bug))。另一个可以观察到的异常例子是在生产环境中运行我们的应用程序,其中我们与其他进程共享资源。我们的测试环境显示一切正常,但在生产中,我们遇到了内存不足错误。这些场景可能源于测试条件与生产环境略有不同,在生产环境中,我们可能能够使用比生产条件中可用的更多内存。查看正在运行的其他内容以及错误周围的细节可以帮助诊断资源冲突问题。虽然不像芯片缺陷那样引人注目,但仍然是一个可能具有挑战性的隔离问题。
-
运营效率和故障排除—成熟的、高质量的日志事件可以包括错误代码的使用。错误代码可以链接到特定问题,并提供有关如何解决问题的指导。
-
确定何时触发后续操作 —使用日志事件来识别特定的需求并自动启动流程,而不是需要人工干预。
这对于软件和硬件环境脆弱且理解不充分但运营上至关重要的传统状态尤其有帮助;人们会变得对改变持谨慎态度(或者甚至可能无法为现成解决方案实施改变)。因此,为了实施诸如错误预防措施等任务,我们需要在监控的应用程序之外实施解决方案。这可能仅仅是监视报告成功的完成消息,此时可以启动下一个操作或错误预防。
1.3 发展中的理念
关于日志管理和日志应用的理念在过去四五年里发展了很多;这部分是由于容器化的快速进展。Docker 和 Kubernetes 以及单个小型服务(微服务/宏服务/迷你服务)的有效增长,以支持动态和超规模环境,意味着部署的应用程序在本质上更加短暂。其他因素,如DevOps的广泛采用,也在一定程度上得到了发展。最终结果是,发展出了一些值得注意的概念。
1.3.1 四个黄金信号
可观测性可能是现代监控概念中最早发展起来的。关于可观测性的讨论从 2016 年开始逐渐获得主流认可,并出现在了一些参考文本中,如谷歌的网站可靠性工程(SRE)指南(可在landing.google.com/sre/sre-book/toc/找到)。这个想法并不新颖;只是定义得很好。
可观测性本质上是指我们应该跟踪或观察并测量软件正在做什么,以便管理和理解一个系统。行业思维已经将这个前提发展到了跟踪四个特定的信号,通常被称为 SRE 的四个黄金信号:延迟、错误、流量和饱和度。这四个信号有时被称为指标、度量或指标(语言是可互换的;就我个人而言,我认为信号这个词非常二元,而生活很少是那样的)。以下是这些信号的含义:
-
延迟 —处理请求所需的时间。延迟的增长可能表明由于需求的增加或缺乏性能调整,软件或配置可能存在潜在的性能问题。
-
错误 —可能影响服务的问题及其频率,以及它们是否可以自我恢复(例如,无法获取数据库连接意味着回退并重试)。随着我们继续阅读本书,我们将看到 Fluentd 在处理错误方面将发挥其独特的作用。
-
流量 —流量的增加可能表明需求增长或恶意意图,这取决于流量下降时效果的增加或减少。
-
饱和度 —反映系统满载或使用程度(例如,CPU 和磁盘利用率)。一旦系统超过某个饱和阈值,性能下降将会发生,因为操作系统需要投入更多努力来管理其有限的资源。
仅从日志中推导出所有四个信号并不理想(例如,服务降级需要我们持有多个性能指标并在一段时间内进行比较),但半合理的日志记录使用时间戳可以产生所提供的信号。延迟可以通过第一个和最后一个日志事件发生的时间差来推导;例如,吞吐量可以通过日志条目量来表示。
1.3.2 可观测性的三个支柱
另一个在业界变得流行的可观测性视角与我们所监控事物的特性有关。在监控时收集的信息类型可以通过几种定义之一来描述。因此,可观测性由三个支柱或核心思想组成:
-
指标 —通常为数值型,用于量化事物的状态。我们随后会定期采样环境中的数据点(例如,CPU 利用率)。
-
日志 —主要是文本型但基于事件,因此具有时间和描述的特性(例如,简单网络管理协议 [SNMP] 捕获)。
-
跟踪 —追踪执行流程以及事务和子事务执行不同步骤所需的时间。跟踪日志主要是数值型,由代码执行进入和离开解决方案不同部分的时戳组成。为了提供这些时间的上下文,标识符,如事务 ID 和入口和出口点,被识别。
每个人都会熟悉指标,因为我们都有过需要查看 CPU 工作强度或因内存不足或硬盘存储空间不足而遇到限制的经历。
跟踪最强烈地与OpenTracing倡议(opentracing.io/)和云原生计算基金会(CNCF)项目Jaeger(jaegertracing.io/)相关联。OpenTracing 与名为 OpenCensus(opencensus.io/)的项目结合,形成了OpenTelemetry(opentelemetry.io/)。然而,日志可能也会对这个领域做出贡献,因为特定的日志条目可能作为跟踪中的测量点——尤其是在传统解决方案中。存在一种风险,人们会将跟踪与日志的思考合并。通常,人们希望将跟踪性能信息关联回日志,以便日志可以作为确定低性能发生位置的关键诊断工具。然而,每个支柱可用的工具具有不同的差异和优势。我们可以通过考虑 Jaeger 对执行路径(跟踪)的可视化与 Fluentd 解析日志事件并触发操作的能力来看到这一点。虽然这些 CNCF 项目将跟踪推到了前台,但这个想法并不新颖,许多服务总线解决方案(如 Oracle SOA Suite 和 MuleSoft)都有某种跟踪机制。区别在于OpenTracing和OpenTelemetry试图推动标准化。
我们看到这些标准正在被开源实现框架和商业解决方案所采用。这与 Fluentd 有何关联?根据日志输出,它可以代表一种追踪执行(例如,记录交易、标识符、代码库中的执行点和时间)的方式。换句话说,追踪是一种特殊的日志。这种关系以及支持的部署模型使得 Fluentd 和 Fluent Bit 能够成为 OpenTelemetry 解决方案的一部分。因此,OpenTelemetry 协议(OTLP)被纳入 Fluentd。所有这些措施在解决方案的不同层级(从基础设施到业务逻辑)中发挥作用,如图 1.3 所示。

图 1.3 将可观察性三要素应用于解决方案堆栈
各层的定义如下:
-
商业应用监控—这代表的是纯粹的业务应用监控或业务活动监控(BAM),与描述业务流程执行语言(BPEL)等内容的业务任务测量相关。
-
应用监控—这反映了传统应用和中间件/工作流技术(如 Oracle 的 SOA Suite 或 Microsoft 的 BizTalk 支撑 BPEL 实现)的监控。
-
虚拟机/容器监控—这衡量的是共享主机计算服务的引擎是否为虚拟化环境(们)提供了适当的资源级别。它监控以确保虚拟化硬件运行顺畅。
-
主机/基础设施监控—这可以检测硬件问题,例如存储容量、过热 CPU、风扇故障等。
注意:有关 BAM 的更多信息,请参阅 Tijs Rademakers 在《Activiti in Action》的 liveBook 版本(Manning,2012)中的内容mng.bz/DxgR。
在这两个概念中,我认为四个信号更好地被视为衡量标准。通过测量每个信号所描述的数据,信号将指示某事是否正确或错误。更重要的是,接收到的信号变化是否显示出趋势或模式,至少意味着被监控的解决方案不再恶化?理想情况下,我们希望看到一个持续改进的趋势。无论如何,这些信息不会给你关于根本问题的信息。例如,显示系统高度饱和的信号不会告诉你为什么系统会饱和,这可能发生在代码陷入无限循环时。对于这一点,你仍然需要了解软件正在做什么。这并不是说信号是错误的;毫无疑问,它们是提供问题线索的最佳方式。但我相信,通过这三个支柱的视角,我们可以更深入地理解软件行为中的因果关系,从而更好地理解正在发生或没有发生的事情。
你可能已经注意到,在日志记录的原因(用于调试、审计等)中,各种活动将由组织中的不止一两个人处理。一旦组织规模超过一定规模,我们就会有在不同领域工作的专业人士。角色的专业化给不同的工具带来了压力。虽然许多监控工具都有插件功能等,但它们可能不支持每个个体的需求。这可能导致我们在企业 IT 环境中拥有多个工具,在某些组织中,人员和组织政治将进一步复杂化 IT 工具环境。然而,它们都需要来自同一源系统的数据混合。
1.4 日志统一
Fluentd、Logstash 和其他相关工具有时被称为日志统一工具。但这是什么意思,一个统一工具应该具备哪些价值?让我们更深入地看看统一的价值,并将其与其他相关概念区分开来。
剑桥高阶英汉双解词典将统一描述为“将事物或人聚集或结合的行为或过程”(mng.bz/lax2)。这正是我们使用 Fluentd 的目的——从不同的来源收集日志事件,并使用单一工具将它们汇集在一起,以便处理日志事件并将其发送到适当的端点解决方案。
这种能力是必不可少的,因为它提供了许多显著的好处;我们在查看日志应用时已经提到了一些。当我们把这些价值点汇集在一起时,我们可以大致将它们分为日志来源和基于日志的洞察。
日志源的优势包括以下内容:
-
它简化了定位和检索日志和日志事件的任务。通过单一平台,定位相关日志事件变得远更容易。我们可以将日志事件路由到方便的位置/工具,而不是需要访问多个平台,这些平台可能有多个不同的位置和访问日志事件的方式。
-
随着虚拟化、容器化和最近的服务即函数的出现,逻辑的托管变得短暂,因此,在信息丢失之前轻松收集日志信息的方法比以往任何时候都更加关键。使用 Fluentd,我们可以将这些短暂环境中配置轻量级进程,将日志事件推送到持久的位置。
-
单一技术可以将来自不同来源或目标的日志事件汇集在一起。因此,日志事件管理变得更加容易和可访问。我们不必掌握所有不同方式捕获和存储日志事件的方法(例如,Syslog、SNMP、Log4J 以及许多其他日志形式和协议),因为 Fluentd 使这变得更容易。
-
操作系统复杂,由许多离散的过程和应用组成。通常,离散组件会附带自己的日志。我们需要将这些组件汇集在一起,以追踪事件通过不同的组件。一些问题已经通过操作系统和网络设备采用了一小群标准(如 Syslog 和 SNMP 陷阱)来解决。
人们很容易认为 Syslog 和 SNMP 可以满足我们所有的日志需求。但软件不仅仅是使用 SNMP 或 Syslog 的操作系统组件的集合,因此我们需要在另一个统一层面上将这些来源汇集起来。例如,Syslog 主要是一个 Linux 解决方案;它使用 UDP 意味着存在事件丢失的风险,UDP 有大小限制。数据结构和预定义值以基础设施为中心,仅举 Syslog 约束的几个例子。
-
在网络和互联网时代,我们的应用程序通过许多不同的管理设备传递事件,这实际上改变了我们的通信可能被中断的地点数量。在如此大规模的分布中统一日志事件,将问题缩小到可管理的规模。
基于日志的洞察包括以下内容:
-
它更容易创建日志事件的全面视图,使我们更容易看到因果关系。
-
将日志统一到分析平台中,可以利用以下过程来利用数据
-
在一个可访问的位置搜索所有日志
-
在生产环境中识别趋势和模式
-
提取分析数据,以预测未来可能的行为
-
通过观察用户行为来确定系统是否受到滥用或恶意行为的模式
-
-
一个统一化平台为我们提供了一个从被动的事件后分析方法转向识别问题并在它们发生时积极采取行动的机会。这可能会扩展到我们识别预警信号并积极采取行动以避免问题的位置。能够变得积极的能力来自于统一化工具过滤、路由和应用日志事件意义的能力。
-
基础设施即服务(IaaS)和平台即服务(PaaS)带来了全新的动态变化和路由复杂性。因此,日志的统一化减少了跟踪可能影响我们解决方案的因素的挑战规模。
虽然我们已经讨论了日志统一化的原因和内容,但我们还应该将其与其他与处理日志事件相关的概念区分开来,尤其是日志分析。
注意:有关 SNMP 的更多信息,请参阅 Jamie Riedesel(Manning,2021 年)所著的《软件遥测》的 liveBook 版本,链接为livebook.manning.com/book/software-telemetry/chapter-2/155。
1.4.1 日志统一化与日志分析的比较
许多日志空间中的工具都属于日志分析类别,其重点在于应用数据分析技术,如模式搜索,在多个数据记录中使用复杂规则。这种处理通常与大数据和搜索引擎技术相关联。其中最著名的可能是 Splunk,作为一个纯商业产品,以及 Elasticsearch,作为一个开源解决方案,同时提供商业选项。
日志事件需要被摄入到分析引擎中,以便能够执行日志分析。这些分析过程可能包括事件相关性(例如,确定哪些系统或组件产生最多的错误,或者故障频率与一天中特定事件的关系)。如果需要,可以将日志事件手动输入到引擎中。通常,像 Splunk 这样的分析产品具有使用分析引擎中更常见的协议来收集或聚合日志事件的工具。然后,将这些服务部署到多个位置以收集不同的日志源。这是一个简单的聚合行为,因为收集不是智能的;只有在它们进入分析引擎之前,才有可能有效地处理日志事件。收集器通常不具有与统一化工具相同的连接性和配置水平。
区别在于,日志分析引擎的优势在于将搜索和计算科学应用于大量日志,而不是日志事件的收集和路由。而统一化工具的优势在于获取和传递日志事件,它通常具有相对简单的分析能力,例如事件随时间的变化计数。
这两种技术都有一些标准功能,涉及将意义转化为数据(即数据变为可用信息的过程)。没有这些能力,任何解决方案都无法非常有效。这两种技术都有强大的事件过滤能力,但应用方式不同。
定义 日志路由是指 将日志事件提取后通过中间件工具,例如 Fluentd,导向需要这些日志事件的应用程序。
定义 日志聚合 指的是将日志事件提取并发送到一个中央位置进行处理。
1.5 软件栈
自从 2000 年以来,业界一直在讨论软件栈(有些人将这个术语归功于 MySQL 的共同创始人 David Axmark 和 Michael “Monty” Widenius),当时最知名的栈被命名为:LAMP 栈(Linux、Apache、MySQL、PHP)。我们所说的 软件栈 指的是一组标准的产品组合(通常是开源产品),它们一起用于提供软件解决方案。另一个知名的栈是 MEAN(MongoDB、Express、AngularJS、Node.js)。完整的栈列表可以在 en.wikipedia.org/wiki/Solution_stack 找到。
软件栈或解决方案栈
值得注意的是,人们经常将 软件栈 和 解决方案栈 互换使用。在大多数情况下,这是合理的;栈提供了一个完整的解决方案,例如日志管理;我们只需要应用配置。
但在栈提供了构建解决方案所需的所有元素的情况下,这就不成立了;MEAN 栈包含了构建许多解决方案的所有组件,但你必须将你自己的软件添加到 MEAN 栈中才能提供解决方案。
1.5.1 ELK 栈
在软件领域中,用于日志处理的最佳知名栈是 ELK(Elasticsearch、Logstash、Kibana)。这个产品组合提供了使用 Elasticsearch 进行日志分析、通过 Kibana 进行可视化和使用 Logstash 进行日志路由和聚合的能力。ELK 栈之所以能够如此完美地结合在一起,是因为这三个组件(虽然都是开源的)都是由 Elastic(www.elastic.co)开发的,Elastic 与 Red Hat 一样,采用基于开源的商业模式取得了成功。
虽然单一供应商为这些组件提供了整洁的集成和相互补充的功能,但也意味着开发工作可能会受到供应商的商业模式和企业目标的重度影响。对于 Elastic 来说,这是为了向 ELK 栈的不同部分销售更多服务和企业扩展。这个问题可以通过由外部和中立组织(如 Apache、CNCF 或 Linux 基金会)管理的开源产品来解决。但 ELK 并不受此类治理。
不幸的是,作为此堆栈的一部分,Logstash 受到了它偏向于将 Elasticsearch 作为日志事件的最终解决方案(这可能有效也可能无效)的印象的影响。Logstash 确实为除 Elasticsearch 之外的产品提供了插件。然而,可以争辩说,这些插件必须来自想要在 ELK 堆栈中与 Elasticsearch 竞争的供应商,或者 Elastic 必须实施它们以保持竞争力。相比之下,Fluentd 的创始人没有自己的分析产品作为日志事件发送的首选位置。我们还可以考虑 CNCF 对 Fluentd 的采用作为一种隐含的认可,即它不受这些偏见的影响。此外,Fluentd 围绕的社区产生了更多的插件,使其比 Logstash 更灵活。
这导致了被称为 EFK 的变体堆栈,它正在获得关注(Elasticsearch, Fluentd, Kibana)。由于 Fluentd 为 Elasticsearch 和 Kibana 提供了插件,这个替代堆栈被视为具有同等的能力,但具有更大的统一灵活性。例如,OpenShift 采用了 EFK 来管理日志事件(见 mng.bz/YwDj)。
如图 1.4 所示,ELK 和 EFK 都有轻量级、较小的统一能力变体。Beat 与 Logstash 的关系与 Fluent Bit 与 Fluentd 的关系相同(关于 Beats 和 Fluent Bit 的更多内容将在本章后面介绍)。

图 1.4 ELK 与 EFK 软件堆栈对比,展示了堆栈之间的差异以及每个堆栈中涉及的产品
1.5.2 比较 Fluentd 和 Logstash
在表 1.1 中,我们尝试总结了两款产品的不同之处。它们有很多共同点,这也是为什么在堆栈中可以用 Fluentd 替换 Logstash 的原因。然而,也有一些值得强调的差异。
表 1.1 Fluentd 和 Logstash 对比
| 方面 | Fluentd | Logstash |
|---|---|---|
| 主要贡献者和产品治理 | Treasure Data,由 CNCF 管理 | Elastic |
| 商业支持版本 | 是 | 是(更稳健的选项,因为支持可以涵盖整个堆栈) |
| 可用的插件 | ~500 | ~200 |
| 配置风格 | 声明式——使用标签 | 过程式——使用 if-then-else 构造。 |
| 性能 | 与 Logstash 相比,内存占用较低 | 与 Fluentd 相比,内存占用较高 |
| 缓存 | 默认提供高度可配置的缓存选项,包括文件和内存缓存 | 具有固定大小的内存队列 |
| 语言/运行时机器 | CRuby—不需要核心运行时 | JRuby,依赖于 Java 运行时 (JVM) |
1.5.3 Fluentd 和 Fluent Bit 之间的关系
Fluentd 拥有一个基于 C 的小型内核,但大部分产品是用 Ruby 构建的。这带来了一些权衡。与 Ruby 相关的核心权衡是它在解释器上运行(尽管有几个变体使用 Java 虚拟机、Truffle 等代替原始解释器,如 Logstash 使用的 JRuby)。Ruby 使用名为 Gems 的打包工具来提供额外的库甚至应用程序。为了使 Fluentd 能够在物联网(IoT)场景中使用,需要为智能电表或树莓派等设备提供更小的资源占用。创建 Fluentd 最小占用版本的目标导致了 Fluent Bit 的诞生。Fluent Bit 提供了 Fluentd 部分功能,专注于将日志事件路由到更集中的位置。然后可以更有效地处理日志事件(过滤、转换、丰富等),正如您期望 Fluentd 那样。表 1.2 展示了 Fluentd 和 Fluent Bit 之间的差异。
表 1.2 Fluentd 与 Fluent Bit 对比
| 方面 | Fluentd | Fluent Bit |
|---|---|---|
| 开发语言 | 使用 C 和 Ruby 编写 | 使用 C 编写以最小化部署占用空间 |
| 依赖项 | 依赖于 RubyGems | 无依赖(除非自定义) |
| 存储和内存占用 | 内存需求约为 20 MB,具体取决于配置和插件 | 约 150 Kb |
| 可用的插件 | 能够利用大约 300 个预构建和第三方插件 | 限制为内置插件和 30 个其他扩展。输入 |
| 操作系统支持 | 提供适用于各种操作系统预构建安装程序,涵盖大多数 Windows、OS X、Linux 版本 | 基于 CentOS、Debian(及其衍生版本,如 Raspbian)和 Ubuntu 的多种小尺寸 Linux 变体,适用于 x86 和 AArch 处理器。其他基于 BSD 的 Unix 操作系统可能得到支持,但对于插件没有保证。 |
尽管存在这些差异,但 Fluent Bit 和 Fluentd 还是完全能够协同工作,正如我们在本书后面的内容中将会看到的。物联网并不是唯一适合使用 Fluent Bit 的用例。当考虑微服务时,对于某些容器来说,小尺寸和快速启动时间是非常理想的。我们将在本书后面的内容中探讨微服务的部署可能性以及 Fluentd 或 Fluent Bit 的使用。
1.5.4 Logstash 与 Beats 之间的关系
Beats 与 Logstash 之间的关系与 Fluentd 与 Fluent Bit 之间的关系略有不同。首先,Beats 实际上是一套小型组件集合,用于收集特定类型的数据。每个单独的 Beat 解决方案都是基于名为 libbeat 的 Go 库构建的,与 Logstash 使用 Java 相比。Beats 家族包括以下内容:
-
Filebeat—收集日志文件(具有特定模块以处理 Apache、服务器日志等)
-
Packetbeat—收集网络数据包数据(DNS、HTTP、ICMP 等)
-
Metricbeat—收集服务器指标
-
Heartbeat — 提供正常运行时间监控
-
Auditbeat — 收集审计事件,通过 systemd (
mng.bz/6Z9o) 和 Auditd (mng.bz/oa5d) 在 Linux 上监控活动 -
Winlogbeat—集成到 Windows 操作系统,运行 PowerShell 脚本和 Sysmon 等
-
Functionbeat—与无服务器解决方案协同工作,目前仅限于 AWS (Amazon Web Services)
libbeat 库已作为开源软件提供。这使得第三方,包括开源社区,使用该框架构建更多 Beat 解决方案变得更加容易(并提供了代码独立性的保证)。所有 Beats 都使用共享的数据结构定义来传递收集的数据。
1.6 日志路由作为安全手段
随着基础设施越来越多地由配置驱动,而不是物理盒子和电缆,数据可以进入和离开环境的位置可以迅速增加,因为这只是一个配置新点的问题,数据可以进出。最好限制数据在公共网络和私有网络之间传输的点数——这仅仅是拥有后端(或反向)代理的许多原因之一。在纯聚合模型中,日志代理的每个节点都希望直接与聚合点通信。如果解决方案可以容忍网络代理,则可以减轻这种情况。但使用更了解正在路由的内容的代理,如 Fluentd,不是更好吗?
定义 代理 是代表客户端从一台或多台服务器检索资源的服务器。检索到的资源随后返回给请求者,看起来就像是从代理服务器本身发出的。如果部署在执行计算的(通常是轻量级)客户端而不是服务器附近,则代理被描述为后端或反向代理。代理通常通过实现流量缓存和应用通过控制数据进入和离开网络的方式来优化网络负载。
如我们所见,Fluentd 的日志路由功能允许我们使用 Fluentd 节点作为日志的路由器/整合器,这意味着我们可以控制网络暴露,以及考虑其他几个方面。
在 Fluentd 中的安全考虑不仅限于配置路由来控制网络中的日志的入网和出网。Fluentd 支持使用 SSL/TLS 证书,以确保 Fluentd 节点之间或 Fluentd 与其他网络服务(例如 MongoDB)之间传输的数据是安全的。这通过验证真实性和加密数据的能力提高了安全性。今天,安全需要成为我们行动的各个方面的一个方面,而不仅仅是附加的;我们将在适当的地方直接解决这些问题。
1.7 日志事件生命周期
另一个值得考虑的视角是日志事件的周期。当某种类型的软件组件生成日志条目时,为了从中获得价值,它需要通过一个生命周期,如图 1.5 所示。

图 1.5 日志事件的典型生命周期
如图 1.5 所示,我们首先从捕获日志事件(信息源捕获)开始,随着事件的流动,它获得了更多的意义和价值。基于我们之前讨论的内容,任何日志统一工具,包括 Fluentd,在信息源捕获以及结构和路由阶段最为有效。聚合和分析阶段将看到针对单个事件的特性,但将依赖于聚合和分析聚合日志。可视化数据是产品的薄弱环节。鉴于这些工具的路由和连接能力,通过连接合适的服务,通知和警报阶段可以轻松实现。不仅如此,这一阶段还有可能向上移动,因为我们并不总是需要分析产品来决定是否需要通知和警报。
如图中所示,Fluentd 等工具在生命周期上半部分(从捕获到聚合以及部分分析阶段)支持得非常好。下半部分则由日志分析(聚合和分析,可视化数据,通知和警报)得到很好的支持。
1.8 Fluentd 的演变
在本节中,我们将探讨导致 Fluentd 创建及其快速普及的事件。图 1.6 显示了 Fluentd 演变过程中的关键事件的时序图。

图 1.6 影响 Fluentd 的关键事件的时序图
1.8.1 Treasure Data
Fluentd 的起源可以追溯到 2011 年,当时通过使用 Hadoop,大数据正在影响主流 IT。作为硅谷的一家初创公司,Treasure Data 成立是为了围绕基于 Hadoop 的半结构化数据处理创造价值。Treasure Data 发现它需要一个工具来帮助它从多个来源捕获数据并将数据摄取到 Hadoop 数据存储中。因此,它着手构建 Fluentd,并使用 Apache 2 许可证(www.apache.org/licenses/LICENSE2.0)将其作为免费和开源软件(FOSS)发布。这使得构建、扩展和利用工具变得容易。因此,除了为 Treasure Data 工作的开发者之外,其他开发者也对 Fluentd 做出了贡献并进行了扩展。
注意:想了解更多关于 Hadoop 的信息,请查看 John T. Wolohan 所著的《Mastering Large Datasets with Python》的 liveBook 版本(Manning, 2020),链接为mng.bz/do2o。
2013 年,由于 AWS 推荐在其平台上进行数据收集,Fluentd 得到了很大的推动。这进一步得到了谷歌使用 Fluentd 与其 BigQuery 产品相结合,并将其纳入其监控解决方案的帮助。
Treasure Data 背景
Treasure Data 成立于 2011 年,旨在通过使用 Hadoop 等大数据技术来提供商业价值。然而,Sadayuki Furuhashi 和 Treasure Data 的团队发现他们需要一个工具来帮助他们捕获和摄取数据,于是着手构建 Fluentd。它于 2011 年 10 月作为开源软件发布。自 2011 年以来,Treasure Data 已开发出几个专业领域,如客户信息系统和物联网(IoT)。
自那时起,Treasure Data 已被微处理器公司 Arm 收购(随后 Arm 又被 NVIDIA 收购),并从 SoftBank Group Corp.中分离出来成为一个业务。然而,Fluentd 对 Treasure Data 仍然很重要,并且团队继续是 GitHub 上几个重要开源项目(包括 Fluentd 和 Fluent Bit)的非常活跃的贡献者。
1.8.2 CNCF
Fluentd 的下一个重大事件是它被云原生计算基金会(CNCF)采用。CNCF 的存在受到了谷歌和 Linux 基金会的强烈影响,旨在为 Kubernetes 提供一个中立的家园。Kubernetes 旨在在一台或多台服务器上运行多个容器,容器托管一个或多个不同的应用程序。更不用说容器可以根据需要在不同服务器上启动和关闭。从这个角度来看,收集和路由日志数据是一个关键挑战,而 Fluentd 的功能可以很好地解决这个问题。
Fluentd 与 CNCF 的历史
谷歌向 CNCF 捐赠了 Kubernetes,使得竞争企业能够更有效地在 Kubernetes 及其生态系统中协作。像 CNCF 的母公司 Linux Foundation 一样,所有项目都是开源的,并得到了许多贡献者的支持。Fluentd 是 Kubernetes 之后第一个受到 CNCF 治理的项目,因此它是一个早期的毕业生。
1.8.3 与主要云供应商 PaaS/IaaS 的关系
基础设施即服务(IaaS)和平台即服务(PaaS)解决方案受到了 CNCF 项目的启发,同时也影响了 CNCF 项目。这些技术有最好的机会被云平台服务所整合或支持。当涉及到 Fluentd 时,我们已经看到主要供应商(AWS、Azure、Google、Oracle、DigitalOcean、阿里巴巴等)采取了以下行动之一:
-
直接利用 Fluentd 满足自己的需求(例如,谷歌、甲骨文)
-
将其打包为更大服务的一部分(Bitnami、Google Stackdriver)
-
将他们的各种服务暴露为输入或输出,以便他们的服务可以访问(AWS—S3、RDS、CloudWatch、Beanstalk 等)
例如,AWS 为其存储服务提供了输出插件。AWS 的 CloudWatch 解决方案可以接收和发送日志信息到 Fluentd。正如我们所见,谷歌早期就采用了 Fluentd。
除了 IaaS 云服务之外,还有一系列专门针对日志分析的平台即服务(PaaS)服务,从 Loggly(www.loggly.com)到 Datadog(www.datadoghq.com)。这些供应商已经为 Fluentd 提供了插件,因此客户将这些日志数据路由到这些服务变得轻而易举。
1.9 Fluentd 和 Fluent Bit 在哪里可以使用?
Fluentd 和 Fluent Bit 可以用于或适应几乎任何情况,从在容器中运行到部署在物联网设备上,再到大型机解决方案。正如我们所见,Fluent Bit 的体积足够小,可以在广泛的物联网设备上运行,这在某种程度上反映了 Arm 对 Treasure Data 的收购。Fluentd 和 Fluent Bit 一起覆盖了至少 90%今天使用的操作系统平台。正如已经讨论过的,Fluentd 与云服务配合良好,但它并不局限于云,也可以在更传统的虚拟化或专用服务器部署中工作。
更相关的问题应该是,部署 Fluentd 或 Fluent Bit 是否会让你工作更轻松?你应该使用 Fluent Bit 还是 Fluentd?
使用 Fluentd 和 Fluent Bit,从基本的笔记本电脑或台式机,到服务器,无论是物理的还是虚拟的,运行 Windows 和 Linux 操作系统,都可以无忧无虑地进行。这意味着在我们继续本书的其余部分,将 Fluentd 投入实际应用应该是可能的。可能的例外是第八章,当我们运行 Kubernetes 和 Docker 时,这将需要更多的计算能力,但我们仍然在谈论中端桌面或笔记本电脑。但了解 Fluentd 的限制可以帮助我们超越这一点。
1.9.1 平台限制
除了操作系统和硬件之外,平台限制很小。最基本的环境只需要能够运行 Ruby 引擎。Ruby 被多种基于标准的包安装方式支持(yum、Homebrew、apt、Windows 的 RubyInstaller 等)。为所有标准操作系统进行安装很简单,包管理器应有助于解决任何依赖性问题。但对于不太常见的环境,Ruby 也提供了“从源码”安装指南(www.ruby-lang.org/)。如果 Fluentd 本身没有预构建的安装选项——考虑到已涵盖的预构建安装程序(RPM、Deb、MSI 和 RubyGems),那么 Fluentd 网站 (fluentd.org) 提供了如何从源代码进行安装的详细信息。
根据需要设置的配置,Fluentd 可能需要额外的插件。Fluentd 插件通常使用 RubyGems(Ruby 组件的开源包管理器;rubygems.org/)进行部署。如果需要严格的网络控制,可以从本地位置安装 Gems。
如果需要,还有预构建的解决方案可以部署到 Docker 和 Kubernetes。我们将在本书的后面部分讨论 Fluentd 作为 Kubernetes 部署的问题。
最后一个选项——虽然我们认为这是可能的,但我们还没有听说有人尝试过——是通过使用 GraalVM (www.graalvm.org/docs/getting-started/) 创建 Fluentd 的平台原生二进制文件。GraalVM 是一种下一代语言虚拟机,它集成了 Java(JVM)和包括 Ruby 在内的其他几种语言解释器包(github.com/oracle/truffleruby)。但 GraalVM 也可以为 Java 和其他支持的语言创建平台原生二进制文件。
部署 Fluentd 后,它需要读取一个或多个配置文件,这些文件告诉 Fluentd(通常在生产中作为守护进程安装)它应该做什么。
定义 A 守护进程 是一种作为后台进程运行的计算机程序,通常在操作系统启动和关闭时由操作系统启动和停止。这个术语更常与基于 Linux 和 Unix 的操作系统相关联。通常,设计为以这种方式运行的应用程序其名称将以 d 结尾;例如,syslogd 是在 Linux 中实现系统日志(Syslog)的守护进程。在 Windows 操作系统中,这些进程被称为 Windows 服务。
因此,任何部署位置都需要能够读取文件,并且理想情况下允许对 Fluentd 的文件进行更新。Fluent Bit 可以使用配置文件,甚至可以从命令行参数中解释配置。
1.10 基于 Fluentd UI 的编辑
Fluentd 确实有一个浏览器提供的用户界面。图 1.7 展示了 UI 屏幕之一,以展示其外观。
当涉及到使用 Fluentd 的 UI 时,它可以执行一系列任务,例如
-
编辑配置文件
-
从停止和启动的角度管理 Fluentd 实例
-
获取插件修补或安装
-
检查 Fluentd 的日志
我们将直接关注本书的配置文件,因为这有助于探索更复杂的细微差别,并且比 UI 更成熟。一旦完成 Fluentd 的安装,我们将在第二章中简要浏览 UI。

图 1.7 Fluentd UI 的一部分
除了基于 Web 的 UI 之外,还有一个用于 Microsoft 的 Visual Studio Code 的插件,可以帮助在编辑配置文件时进行语法高亮。此插件可用于帮助解决诸如缺少括号等典型问题。此插件可以从 Visual Studio Code 内部或从mng.bz/GOeA下载。其他编辑器,如 Sublime Text,也有开源包/插件来支持 Fluentd 配置文件的语法编辑。
Fluent Bit 的 UI?
考虑到使 Fluent Bit 的占用空间尽可能小,引入 UI 将与这一原则相悖。典型的 Fluent Bit 使用是将日志事件源和转发到聚合点,在那里可以进一步处理事件(例如,Fluentd 或日志分析工具)。这意味着配置应该相对简单,对 UI 的需求有限。集成 UI 的案例将与最小化占用空间的目标相悖。
1.11 插件
如前所述,Fluentd 的插件在覆盖范围和深度上超过了大多数,如果不是所有竞争者。我们无法在以下章节中涵盖每个可能的插件,因此我们将专注于那些有助于说明核心思想和代表大多数 Fluentd 部署可能遇到的情况的插件。
但鉴于插件的范畴,了解可用的内容和可能实现的内容是值得的。Fluentd 插件可以分为以下类别,并且对于每个类别,我们都提供了一些示例:
-
输入
-
文件存储—AWS S3, 文本文件(HTTP 日志文件等)
-
数据源—MongoDB, MySQL, 通用 SQL(适用于所有 ANSI SQL 数据库)
-
事件源—AWS Kinesis, Kafka, AWS CloudWatch, GCP Pub/Sub, RabbitMQ
-
操作系统—系统,HTTP 端点,dstat,SNMP
-
应用服务器—IIS (Internet Information Services), WebSphere, Tomcat
-
-
输出
-
文件存储—AWS S3, Google Cloud Storage, 文件
-
数据库存储—BigQuery, MongoDB, InfluxDB, MySQL, SQL Server
-
事件存储—Kafka, Google Stackdriver, AWS CloudWatch, Prometheus
-
日志分析工具—Splunk, Datadog, Elasticsearch
-
通知—Slack, 邮件, HipChat, Twitter, Twilio, PagerDuty
-
-
日志/事件操作(解析器、过滤器、格式化器)
-
Map—日志格式映射器
-
数值监控器—生成与日志相关的统计数据
-
文本转 JSON
-
键/值解析
-
GeoIP—根据公开信息将 IP 地址转换为地理位置(关于 GeoIP 的更多信息,请参阅 Julien Vehent(Manning, 2018)所著的《Securing DevOps》的 liveBook 版本,详情请见
mng.bz/raJJ) -
JWT—处理 JSON Web Tokens(关于此的更多背景信息可以在 Prabath Siriwardena(Manning, 2022)所著的《OpenID Connect in Action》的 liveBook 版本中找到,详情请见
mng.bz/VlMy) -
红字删除—对敏感数据值进行屏蔽,使得未经授权的人无法看到
-
格式化器—将数据布局成不同的结构以及可能的不同的表示法(例如,XML 到 JSON)
-
-
存储
-
缓存—Redis, Memcached
-
持久化存储—本地文件,SQL 数据库,S3 块存储
-
-
服务发现—配置以查找理解 Fluentd 通信机制的其它节点
注意:可用的 Fluentd 插件完整列表由 www.fluentd.org/plugins/all 管理。
1.12 如何使用 Fluentd 使操作任务更简单
在本章中,我们探讨了 Fluentd 可以帮助解决的一些场景和用例。随着本书的进展,我们将介绍更多场景并观察其复杂性的增加。
1.12.1 可操作的日志事件
而不是等到日志事件收集在一起后再对内容进行处理,可以创建配置,以便在接收到它们时立即进行处理。这种处理可能包括过滤以找到需要立即关注的事件。如果一个系统记录了一个通常只在解决方案失败前很短的时间内发生的事件——例如,操作系统进入恐慌状态(关于内核恐慌的更多信息,请见 wiki.osdev.org/Kernel_Panic)——那么一旦检测到该事件,我们就可以通过近实时通道(如 PagerDuty 或 Slack)向负责处理此类事件的人发送消息(我们将在第四章中说明 Slack 场景)。但可操作的日志事件可以很容易地扩展到更远,例如触发脚本以执行自动修复(例如,清除或存档旧日志文件,以免耗尽存储空间)。
1.12.2 使日志更有意义
可操作的事件也可以扩展,以提供一种使日志事件更具意义的方法。在更大、寿命更长的组织中,有一些仍然是业务关键性的遗留解决方案(它们通常非常大,包含大量逻辑以确保符合很少人理解的要求)。因此,替换成本可能非常高,没有人愿意承担修改的风险,即使是为了改善日志消息以简化支持。但这些问题是可以解决的;那些看似无辜但如果不立即采取补救措施就会带来灾难的日志消息可以被修改,添加诸如错误代码等内容。运维人员可以轻松找到操作协议。
意义的运用可以更进一步;一些日志的结构可能不符合标准格式,如 JSON 和 XML。但 Fluentd 可以用来快速和早期地强加结构,因此,下游的日志事件可以更有效地处理。如果一个应用程序意外地记录了敏感数据,那么越早移除或屏蔽此类信息,就越好。否则,所有下游的日志处理解决方案都必须实施更加严格的设置,因为它们将接收敏感数据,如信用卡数据(PCI 合规性)、个人数据(通用数据保护条例)或受类似立法约束的数据。如果这些问题成为问题,并且日志的来源无法修复,Fluentd 可以过滤或修改日志事件以屏蔽此类内容。
1.12.3 多语言环境
在过去的 10 年里,不同的编程语言数量激增。因此,我们经常讨论多语言环境,在这种环境中,许多不同的语言被用于端到端解决方案;例如,R 或 Python 可能被用来从数据中提取深层含义,而 Web 界面可以用 JavaScript 编写。后端解决方案可能是 Java、Scala、Clojure、.Net (.NET)和 PHP。与相同后端协同工作的厚客户端应用程序可以用 C#、VB.Net 或 Swift 编写。在这些类型的环境中,我们需要一个对应用程序实现语言中立的解决方案。Fluentd 提供了这样的解决方案,但许多语言都有库,允许以优化的方式直接将日志事件传递到 Fluentd。
1.12.4 多个目标
多个目标问题体现了在大型组织中,团队通常专注于特定任务的事实,例如信息安全。不同的团队希望使用不同的工具来支持他们的专业领域——例如,算法在检测表示恶意安全活动的模式方面特别有效。
1.12.5 控制日志数据成本
日志事件,就像任何运营数据一样,需要存储,在移动时消耗网络容量,这会产生成本。当大量未压缩或未过滤的文本从云服务提供商的网络中流出并通过企业的互联网连接进行通信时,这种成本可能会很明显。然而,与此同时,我们又不希望过度节俭地对待日志记录;否则,我们将永远无法理解正在发生的事情。Fluentd 可以通过过滤和存储一些日志事件以本地化方式帮助解决这个问题,其中日志事件的价值有限。但可以进一步帮助的日志信息可以发送到中央位置。不仅如此,传输还可以通过压缩机制(批量日志事件可以高度压缩)进行优化。
1.12.6 日志到指标
之前我们介绍了可观测性的三个支柱(日志、追踪、指标)。在某些情况下,我们希望通过查看日志的生命迹象(即事件是否已被创建)来获取指标,例如日志事件的发生次数,或者哪个进程是活跃的或已死亡的。通过插件,可以生成此类度量并使用 Prometheus 和 Grafana 共享此类数据。
这可以通过 Fluentd 监控其自身已部署节点的可能性来扩展——当涉及到复杂的分布式用例时,这也可能非常理想。毕竟,Fluentd 只是一块软件,因此与其他代码一样容易受到错误的影响。
1.12.7 快速运营整合
公司的合并和收购可以推动对运营资源(如运营团队)整合的需求。这种整合将比整合主要 IT 系统的任何流程都要快。我们可以通过日志统一轻松地将日志数据导向当前运营支持团队工具,以监控和减少将新系统纳入运营组织的时间和精力。
摘要
-
影响现代监控思维的核心理念来源于诸如谷歌的四个黄金信号和可观测性的三个支柱等思想。
-
日志分析不同于日志统一,它侧重于一个平台来挖掘日志数据。相比之下,日志统一是关于将日志汇集在一起并将内容导向必要的工具。
-
Fluentd 和 Fluent Bit 最初是 Treasure Data 的开源倡议,后来在 CNCF 的治理下运营。
-
Fluentd 和 Fluent Bit 并未与任何分析平台对齐。考虑到与 CNCF 的关联,Fluentd 已被 IaaS 和 PaaS 供应商采用,无论是作为监控产品或服务的一部分,还是作为 Fluentd 与其产品之间的支持连接。
-
Fluentd 在微服务领域得到了广泛的应用,但它同样可以很好地适应遗留环境。
-
Fluentd 提供了广泛的插件,并有一个框架,允许在需要时开发自定义插件。
-
Fluent Bit 在高度可插拔性和微小优化的足迹之间进行了权衡。
-
Fluentd 和 Fluent Bit 都可以支持大多数平台,并提供预构建的工件。两者都是开源解决方案;几乎可以在任何可想象的平台之上构建内核和插件。
-
日志记录的应用范围广泛,在整个软件生命周期中都能提供价值。
-
Fluentd 支持广泛的用例,从调试分布式解决方案到运营监控。
-
了解 Fluentd 如何融入 EFK 软件栈,以及 ELK 和 EFK 软件栈之间的区别。
2 Fluentd 的概念、架构和部署
本章节涵盖
-
概述 Fluentd 的架构和核心概念
-
检查 Fluentd、Fluent Bit 和 Fluent UI 的先决条件和部署
-
执行 Fluentd 和 Fluent Bit 的基本配置
-
介绍配置文件结构
第一章探讨了 Fluentd 可以帮助我们的理论、行业趋势和用例。本章讨论了 Fluentd 的工作方式,包括部署和运行最简单的配置以实现传统开发者的“Hello World”。
2.1 架构和核心概念
当你驾驶汽车时,如果你对车辆的动力方式(例如,汽油、柴油、电力、液化石油气)有一些基本的了解,那么驾驶会容易得多。这种理解带来的心智模型意味着我们可以学习预期什么——我们是否可以预期听到发动机的轰鸣声,发动机是否可能熄火,以及齿轮如何工作(如果有)。同样地,在我们开始使用 Fluentd 和 Fluent Bit 之前,花时间了解这些工具的工作原理是值得的。基于此,我们应该熟悉 Fluentd 的一些构建块,这将有助于心智模型的形成。
2.1.1 日志事件的组成
第一章介绍了日志事件的概念。理解 Fluentd 如何定义日志事件是欣赏 Fluentd 工作方式中最关键的事情,因此让我们看看它的组成。每个日志事件都作为一个单独的 JSON 对象进行管理,该对象由三个必需的、不重复的元素组成,如这里所述并在图 2.1 中所示:
-
标签——每个日志事件都与一个标签相关联。标签通常最初通过配置与源相关联,但可以在配置中进一步操作。Fluentd 可以通过使用标签对日志事件应用必要的条件操作(路由、过滤等)。当使用 HTTP 接口时,标签可以在调用中定义,正如我们将会看到的。
-
时间戳——这来自日志信息或由输入插件应用。这确保了事件按顺序保持,这是统一多个日志源和尝试理解组件之间事件序列时的一个基本考虑因素。这些数据以自纪元(1970 年 1 月 1 日 00:00:00 UTC)以来的纳秒数存储。
-
记录——记录是在分离出时间后的核心事件信息。这意味着我们可以在不担心定位事件的时间戳和基本控制所需的标签的情况下处理日志内容,正如我们在本书后面的内容中将会看到的。这提供了即时的好处;每当从 Fluentd 感知适配器传入日志事件时,我们可以避免对时间的初始解析。可以将记录转换为更详细的结构,使其更容易处理。我们将在本书后面的内容中看到如何给数据赋予更多意义。

图 2.1 日志事件的组成
一旦捕获,其他插件就可以与现有的标签一起工作,根据需要对其进行修改、添加和扩展。在处理标签时,可以应用通配符和其他逻辑。例如,如果我们有与一个解决方案(称为子系统 1、2 和 3)相关的几个单独的日志(我们可以称它们为子系统 1、2 和 3),我们可以将每个日志文件标记为 App.Subsystem1、App.Subsystem2 和 App.Subsystem3。然后可以通过使用通配符(例如,App.*)来处理日志的处理。我们可以设置过滤器以更具体地处理特定子系统的日志事件(例如,App.Subsystem2)。
2.1.2 处理时间
考虑到时间戳在日志中的重要性,所有需要协同工作的系统都必须针对一个共同的时钟/时间进行报告。此外,时间不应受夏令时调整的影响。如果没有这一点,每次时钟回拨时,日志就会失去同步。当时钟向前移动时,日志将看到没有记录日志事件的异常期。这可能会触发任何基于时间的分析(例如事件吞吐量分析、错误之间平均时间的测量、监视心跳事件等)的异常。
由于系统可能跨越多个时区协同工作,这一考虑因素更加复杂。因此,所有系统都需要运行在共同同意的时间上。解决这个问题的典型方法是将系统链接到协调世界时(UTC)。然而,当我们需要在多个服务器上对时间戳有毫秒级的精度以确保正确顺序时,需要某种东西来保持它们同步。
时间同步是通过将服务器链接到一个公共时间源,然后使用协议请求时间以进行对齐来处理的。这个协议被称为网络时间协议(NTP)。在配置服务器时,强烈建议确保 NTP 已配置。许多技术和服务提供商提供免费的标准 NTP 服务以进行同步。但这有一个限制;当前时间到达不同服务器的时间可能相差几毫秒或纳秒(取决于 NTP 服务的位置)。这被称为时钟或时间偏移。尽管尽了最大努力,但在跨多个服务器聚合时,日志条目偶尔可能会出现不一致的情况。
NTP 和时钟偏移
关于 NTP 和时钟偏移的更详细信息可以在www.ietf.org/rfc/rfc1305.txt找到。
大多数操作系统都提供了一个可以激活(如果默认不是激活状态)并配置为与 NTP 服务器同步的 NTP 客户端进程(或守护进程)。NTP 服务器越近,时钟偏移的风险就越低。
2.1.3 Fluentd 的架构
Fluentd 的操作由一个配置文件指定(该配置文件可能包含其他配置文件,但这一点将在本书的后续部分进行说明)。配置文件描述了何时以及在某些情况下如何应用插件。Fluentd 的核心中集成了大量插件,因此无需额外安装——例如,类似于 Linux tail -f 命令的 tail 插件。对于那些不太熟悉 Linux/Unix 工具的用户,tail -f 命令提供了在文件被添加时在控制台上查看其内容的手段。
在第一章,我们介绍了插件的概念,并通过一些示例进行了说明。在我们在此基础上进一步探讨并更详细地检查插件类型之前,我们应该明确一个术语问题。如果你阅读 Fluentd 文档,它提到了 directives;这些可以与插件类型重叠。但插件类型与指令之间的关系并非一对一,因为插件可以有支持或辅助关系,因此并非指令。在本章的后续部分,当我们查看“Hello World”示例时,我们将看到指令和插件,以及 Fluentd 如何知道从哪里获取配置文件。
以下列表侧重于核心插件类型及其与我们所识别的指令的映射。除此之外,我们还突出了更常见的插件互相关系:
-
输入—在配置文件的术语中,输入插件将与 源 指令相关联。输入可以利用 解析器 插件,这些插件可以从原始日志文本中提取结构化意义。例如,它们可以从消息文本中提取关键值,如后续处理所需的日志事件分类。输入范围从文件到数据存储到直接 API 集成。
-
输出 ——作为插件类型,这些为我们提供了存储(例如,文件、数据库)或连接到另一个系统(包括另一个 Fluentd 节点)以传递日志事件的手段。输出插件与配置文件中的 match 指令相匹配——这一点在此阶段可能并不明显,但当我们展示 Fluentd 的使用时,将会变得更加明显。输出插件可以利用 格式化器、过滤器、缓冲区 和 服务发现 插件。更通用的输入插件有相应的输出。
-
缓冲区 ——缓冲区插件类型专注于批量收集和临时缓存日志事件,以便优化 I/O 工作量。随着我们继续阅读本书,我们将更深入地探讨这个问题。
-
过滤器 ——这种插件类型通过应用规则来控制日志事件可以流向何处。此插件与 输出 插件相关联。
-
解析器 —此插件的任务是从日志事件中提取关键值,并应用到捕获的内容中。当从日志文件等来源获取内容时,这是关键,这些内容将有效作为单行文本开始。这可以从正则表达式和grok到特定领域的逻辑。
-
格式化器 —当内容输出时,需要以数据可以被消费组件处理的方式生成。例如,结构化内容以便 Prometheus 或 Grafana 可以消费,它们期望特定的结构或用于 PagerDuty 的易于阅读的消息。因此,格式化器插件在match指令中由输出插件使用。
-
存储 —正如我们很快将看到的,Fluentd 的性能和效率是我们处理日志事件的方式的一种权衡。存储日志事件意味着我们可以保留事件(通常是临时性的)直到它们需要被处理。临时存储,如缓存,可以给我们带来性能提升,但存在在故障中丢失事件的风险。因此,一些存储选项因此更具持久性以减轻这种风险。本书中我们将以几种不同的方式使用存储插件。
-
服务发现 —当使用此插件时,它通常与输出插件协同工作。其目的是帮助连接到其他 Fluentd 节点,正如我们将在本书后面探讨的那样。此类插件解决如何在网络中识别/找到目标服务器,从可重载配置中的服务器 IP 列表到使用 DNS 记录的特定部分。
在图 2.2 中,我们展示了 Fluentd 的核心构建块,以及存在以帮助扩展、采用和使用 Fluentd 的支持元素。请注意,图中实现的特定插件只是标准部署中构建的插件的一个子集,以及 Fluentd 可部署和使用的那些插件的一小部分。随着我们继续阅读本书,所有这些构建块都将被深入探讨,从调整引擎的配置到插件基础如何为控制所有插件行为提供基础。但理解不同的构建块及其关系从一开始就会有所帮助。

图 2.2 Fluentd 架构视图,展示了核心构建块和根据您的环境可用的可选支持资源
2.1.4 Fluent 配置执行顺序
- 日志事件在 Fluentd 或 Fluent Bit 实例中仅被消费一次,除非 Fluentd 被明确告知复制日志事件(使用 Fluentd 核心中的功能,我们将在本书后面讨论)。这种排序在图 2.3 中得到了说明。

图 2.3 Fluentd 配置中顺序影响示意图
-
在配置文件中定义操作顺序是重要的。除非事件被复制,否则第一个匹配事件的指令将成为消费者。因此,作为一个一般实践
-
当你希望所有日志事件都执行共同操作时,在配置中尽早定义这些指令,但为后续的定向指令复制它们。
-
通配符指令应在配置的后期定义。
-
目标指令应先于通配符指令。
-
-
Fluentd 默认是单线程的。这有助于确保时间序列不受损害。可以通过更改配置将 Fluentd 配置为并发运行(多进程而不是多线程),我们将在第七章中探讨这一点。这意味着,如果你创建了一系列复杂的日志事件操作,Fluentd 可能无法像事件创建那样快速处理事件。这意味着已经形成了瓶颈。有避免这种情况的策略,但这将进一步复杂整个过程。
单线程与多线程
多线程的挑战多种多样,从运行线程多于处理器核心时的协调开销到互斥线程锁(两个线程互相等待)。当涉及到时间序列事件时,保持顺序或纠正顺序是很重要的。如果不小心应用,多线程可能会创建可能导致事件顺序错乱的竞争条件。为了更好地理解竞争条件,一个很好的资源是devopedia.org/race-condition-software -软件竞争条件。
2.1.5 指令
之前,我们提到了 Fluentd 中的指令,很容易混淆指令和插件。指令提供了一个框架,用于将插件分组以实现逻辑任务,例如将日志事件输出到目的地。你会看到指令的声明方式与 XML 元素相同,通过使用尖括号开始和结束。在元素内,可以提供属性,例如标签过滤,就像match示例那样。在指令中,我们随后识别插件并以其名称-值对的形式提供其配置。随着我们接触到更复杂的示例,你会看到我们可以嵌套事物,包括辅助插件。
如果一个命令或插件必须直接由使 Fluentd 处理日志事件流逻辑调用,那么它就是一个指令。虽然在这个阶段这个概念非常抽象,但随着我们通过本书及其示例的进展,这个想法和微妙之处将变得更加明显。如图 2.4 所示,我们可以可视化配置文件中出现的指令、插件和辅助插件。

图 2.4 在 Fluentd 执行顺序的上下文中,Fluentd 指令(中间列—源、过滤器、匹配)与原生插件(解析器、缓冲区、格式化器)之间的关系
图 2.4 中展示的指令总结在表 2.1 中。我们将在本书的第二部分深入探讨这些指令。
表 2.1 Fluentd 指令
| 指令 | 描述 |
|---|---|
| 源 | 源指令告诉 Fluentd 接收/源日志事件,正如我们刚才看到的。 |
| 匹配 | 这涉及将日志事件与其他操作匹配,包括日志事件的输出。 |
| 过滤器 | 这控制哪些事件应由一个或多个进程处理—通常称为管道。 |
| @include | 这告诉 Fluentd 引入其他配置文件以组装一组完整的操作,就像传统代码中的导入或包含语句一样。 |
| 标签 | 标签为日志事件提供了一种分组机制,这比仅使用标签提供了显著更多的功能。 |
| 系统 | 这告诉 Fluentd 如何配置和内部行为(例如,日志级别的设置)。 |
2.1.6 将时间要求付诸实践
如果你想看看自己到目前为止吸收了多少,试着回答这些问题。答案将跟随这些问题。
-
Fluentd/Fluent Bit 中日志事件的三个关键元素是什么?
-
推荐将时区连接到哪个时间服务器?
答案
-
我们在第 2.1.1 节中介绍了这些内容;日志事件的元素是
-
时间戳 —日志事件发生的表示
-
记录 —日志事件的主体
-
标签—与每个日志条目相关联并用于路由日志事件
-
-
如您所忆,在第 2.1.2 节中,我们建议使用 UTC 链接您的 NTP 服务器。
2.2 Fluentd 的部署
在本节中,我们将部署 Fluentd 和 LogGenerator(有时称为 LogSimulator)等工具,以便我们能够运行“Hello World”场景。
后续的示例和练习。Fluentd 和模拟器的所有配置文件都可以在本书的 GitHub 仓库中找到(mng.bz/Axyo)。在仓库中,每个章节都有自己的文件夹集。请注意,仓库中的配置文件将与书中配置示例中显示的略有不同,因此它们可以包含有用的附加注释。我们假设完整的代码和配置示例将来自 Manning 或通过我们为本书的 GitHub 仓库下载。每个章节文件夹包含代码、配置和解决方案的子文件夹。LogGenerator(稍后将详细介绍)已从 GitHub 下载(github.com/mp3monster/LogGenerator)并复制到章节的根文件夹中(例如,图 2.5 中显示的根文件夹)。

图 2.5 书中用于示例和解决方案的目录结构
注意:由于本书中使用了 Fluentd、Fluent Bit 和 LogSimulator,我们已经在章节中包含了相应的指令。在后续章节中,当我们使用其他工具和产品,可能是一章或两章,我们将在附录 A 中提供指令。
2.2.1 为本书示例部署 Fluentd
我们已经在第一章中确立,Fluentd 和 Fluent Bit 在部署到各种平台方面都非常强大。这给本书提出了一个有趣的挑战。我们是描述将 Fluentd 和 Fluent Bit 部署到最广泛的各种平台,还是只关注其中一种?我们是让你使用 Docker 并将所有内容打包成一个镜像吗?
本书采取的方法是首先支持 Windows;这是基于这样一个事实,即在尝试、原型设计和实验 Fluentd 的过程中,你很可能会使用台式机或笔记本电脑而不是企业服务器。Windows 是台式机和笔记本电脑上最占主导地位的操作系统,因此专注于这个环境是有意义的。
然而,为了让这本书中的指导更容易应用于企业服务器,或者如果你足够幸运拥有 Mac,或者你是一个忠实的 Linux 粉丝并且已经安装了你喜欢的 Linux 操作系统版本,我们已经突出了 Linux 和 Windows 之间的差异。大多数指令将包括 Linux 的等效指令。那些使用 Linux 或 macOS 的人很可能会知道,Linux 只是内核,而在此之上的层,例如 UI 层和安装管理器,在 Linux 各版本之间是不同的。这意味着你可能需要调整提供的命令,以便在你的特定操作系统版本上工作。
Docker 镜像
你也可以从 Docker Hub (hub.docker.com/r/fluent/fluentd/) 或直接从 Fluentd 的 GitHub 网站下载一个准备好的 Docker 镜像 (github.com/fluent/fluentd-docker-image)。对于生产环境,这种方法值得考虑,并在第八章中进一步探讨。在本书的大部分内容中,除非你完全熟悉使用 Docker,否则利用 Docker 只会增加额外的努力。
2.2.2 Fluentd 的部署注意事项
在考虑将 Fluentd 部署到生产环境时,我们需要考虑体积指标——也就是说,需要捕获、过滤、路由和存储的日志数据量。在本书的第三部分,我们将关注 Fluentd 和 Fluent Bit 的可扩展性和分布式能力。但首先,让我们假设我们在一个不需要这种水平扩展的环境中工作。即使在简单的部署中,我们也应该意识到日志处理的计算工作量应该小于核心应用的计算工作量。记住,每次日志事件被存储或传输时,操作都会产生大量的 I/O 活动,这会带来计算开销。如果你熟悉底层计算机操作,你会欣赏到每个进程都伴随着开销:
-
每个网络消息都带有路由、验证和诸如消息大小之类的详细信息。
-
每次文件写入都需要使用硬件来定位一块可用于存储的物理存储空间,记录所使用存储块的详细信息,并机械地将写入设备定位到物理媒体上。
我们能在缓存中分组更多日志事件并将它们作为一个块传输,资源的使用效率就越高。就像生活中的所有事物一样,这里也有一个权衡。我们在写入存储之前进行缓存,这意味着数据到达日志事件处理末尾的速度会变慢。数据在处理过程中停留的时间越长,发生断电或组件故障导致数据丢失的可能性就越大。对于本章,我们只需要确保我们的环境有足够的资源来运行;在性能与数据丢失风险之间的权衡是不必要的。
2.2.3 Fluentd 最小占用
Fluentd 的资源需求在现代机器规格下是最小的(见表 2.2),但在处理小尺寸设置时仍值得注意。
表 2.2 Fluentd 最小硬件占用
| RubyInstaller 大小 | 130 MB |
|---|---|
| Ruby 安装存储需求(含 DevKit) | (基本 Ruby 80 MB,加上 DevKit 的 820 MB,总计 1 GB) |
| 内存需求 | ~20 MB |
| Fluentd 额外存储 | 300 KB |
| Ruby 最低版本 | Ruby 2.x(针对 Fluentd v1.x) |
2.2.4 Ruby 简单部署
要准备运行 Fluentd,我们首先需要安装 Ruby。这最好使用操作系统的最新稳定版本的 Ruby 包框架来完成。不同安装包的链接可以通过www.ruby-lang.org找到。对于 Windows,我们通过访问包含相关链接的下载页面来完成此操作。对于 Windows,我们会转到rubyinstaller.org以获取 RubyInstaller。当我们进入第八章时,我们需要进行一些开发工作,因此我们应该安装 Ruby 的软件开发工具包(SDK)版本(在网站上显示为 Ruby+DevKit)。
下载完成后,运行安装程序;它将引导您定义首选位置,并询问您是否想安装 Mysys——回答是。Mysys 对于具有底层 C 依赖关系的 RubyGems(如与操作系统交互的插件)是必需的。几个与开发相关的工具,如 MinGW,允许 Ruby 开发使用 Windows 本地库。这意味着我们应该有 Mysys,我们建议使用 MinGW 完整安装来支持未来可能出现的任何开发需求。
注意:关于 DevKit 的更多信息可在 Ryan Bigg 等人编写的 liveBook 版本的《Rails4 实战》(Manning,2015)中找到,网址为 mng.bz/ZzAR。
安装程序应将 Ruby 添加到 Windows 的 PATH 环境变量中。(附录 A 提供了关于 PATH 环境变量的详细信息。)在检查时,您需要确认 Ruby 的 bin 文件夹已被包含。如果 Ruby 目录路径不在 PATH 环境变量中,我们需要按照附录 A 中的说明添加完整的 Ruby 路径。一旦设置好,应该可以执行命令 ruby –version,一旦路径被修改,Ruby 将显示已安装的版本。
注意:值得注意的一个名为 Chocolatey 的 Windows 开源软件包管理器(chocolatey.org/),它感觉更像是一个 Linux 软件包管理器。Chocolatey 可以用作安装 Ruby 的替代方法。
对于 Linux 用户,所有主要的 Linux 操作系统都有一个相关的软件包管理器,并安装了最新的稳定版本——从 macOS 的 Homebrew 到 apt、yum、pkg 等。当有选择时,就像 Windows 一样,安装所有内容以支持第九章中进行的开发活动是值得的。与 Windows 一样,我们需要使用附录 A 中的说明确认路径已被正确设置。我们还可以使用相同的命令 ruby –version 验证 Ruby。此外,我们需要验证软件包管理器是否已包含 RubyGems 软件包管理器。通过运行命令 gems help 来检查这一点。这将返回 gems 帮助信息或失败。如果失败,则需要以下步骤(在以下步骤中将 x.y.z 替换为最新的稳定版本):
wget http://production.cf.rubygems.org/rubygems/rubygems-x.y.z.tgz
tar xvf rubygems*
ruby setup.rb
2.2.5 Fluentd 的简单部署
Fluentd 可以通过多种不同的方式进行安装。Treasure Data(在第一章中介绍)为 Fluentd 提供了一个 Windows 安装程序,但需要注意的是,安装程序会在文件和文件夹名称中引入一个 td 前缀。Treasure Data 安装程序还包括了标准安装程序中未包含的额外插件。
使用 RubyGems 安装 Fluentd 及其依赖项有丰富的途径,各有其优势和细微差别。我们将使用以下原因使用 RubyGems 安装 Fluentd:
-
Gems 软件包安装器是平台无关的,因此安装过程对 Linux、Windows 和许多其他环境都是相同的。
-
Gems 是安装 Fluentd 核心未包含的插件的最简单方式。
-
我们已安装 Gems(用于帮助安装 Ruby 依赖项),因此我们可以保持我们的方法一致。
以这种方式安装 Fluentd,我们只需运行以下命令:
gem install fluentd
只要您连接到 rubygems.org/,相关的 Gems(包括依赖项)就会安全地下载和安装。在企业环境中,这些站点可能需要通过代理服务器或本地 gems 服务器访问。可以通过运行以下命令来测试安装:
fluentd –-help
这将显示 Fluentd 的帮助信息。它还应该能够看到在部署位置 lib\ruby\gems\ 2.7.0\gems\(以及其他操作系统的等效路径)中安装的 Fluentd 和其他 gems。
除了核心 Fluentd,安装还提供了一些辅助工具,其中一些我们将贯穿整本书使用。主要提供的工具总结在表 2.3 中。
表 2.3 安装时提供的 Fluentd 支持工具
| Fluentd 工具 | 工具描述 |
|---|---|
fluent-binlog-reader |
Fluentd 可以创建二进制日志文件(提供压缩和性能优势)——例如,在文件缓存时。此实用工具可用于读取文件并生成可读内容。 |
fluent-ca-generate |
这是一个用于创建基本(自签名)证书的实用工具,可用于加密 Fluentd/Fluent Bit 节点之间的通信。 |
fluent-cat |
fluent-cat 工具提供了一种将单个日志消息注入 Fluentd 的方法;它确实需要配置前向插件。例如:`echo '{"message":"hello"}' |
fluent-debug |
这是一个用于远程调试的实用工具,与 Ruby 工具配合使用。 |
fluent-gem |
这实际上是对 Ruby gem 命令的别名,它将列出所有可用的 gems。 |
fluent-plugin-config-format |
这提供了一种查询插件以获取插件将支持的配置参数细节的方法。输出可以被视为一个 README 文档。由于某些插件实现可能支持多种类型的插件(例如,输入和输出),因此需要指定插件类型。例如(在 Windows 和 Linux 上),命令 fluent-plugin-config-format -f txt input tail 将检索 tail 输出插件配置细节的文本格式。此实用工具非常适合包含在自定义构建插件的持续集成管道中,因为它可以生成多种格式的文档。 |
fluent-plugin-generate |
这为插件开发生成代码框架。模板包括 Gem 文件、README、插件的占位 Ruby 代码和一个骨架测试框架。 |
几个操作系统差异
基于 Linux 和 Unix 的操作系统支持一个 中断信号 的框架。这些信号可以发送到应用程序以控制其行为。其中最广为人知的是 SIGHUP。Fluentd 可以使用这些信号来触发操作,如重新加载配置文件,而无需重新启动。表 2.4 总结了基本的中断及其影响。
表 2.4 Linux 信号及其对 Fluentd 的响应
| Linux 信号 | 对 Fluentd 的影响 |
|---|---|
| SIGINT 或 SIGTERM | 这告诉 Fluentd 优雅地关闭,以便清除内存中的所有内容,并将任何文件缓冲区保留在干净的状态。如果另一个进程正在调用 Fluentd,最好先停止该进程,因为它可以防止关闭操作完成。 |
| SIGUSR1 | 这告诉 Fluentd 确保所有缓存的值,包括其日志事件,都被刷新到存储中,然后刷新文件句柄到文件存储。这然后基于一个名为 flush_interval 的系统环境变量重复进行。 |
| SIGUSR2 | 保护和优雅地处理配置的重新加载。它可以被认为是优雅的,因为它确保在重新加载配置之前任何缓存都安全地存储,因此不会丢失任何日志事件。 |
| SIGHUP | 这种中断最著名的是强制配置重新加载。它执行与 SIGUSR2 相同的操作,但还会刷新其内部日志,因此不会丢失任何内部日志信息。 |
| SIGCONT | 此信号将使 Fluentd 记录其内部状态——线程信息、内存分配等。 |
向 Fluentd 进程发送 Linux 杀死命令——例如,kill -s USR1 3699,其中 3699 代表 Fluentd 的进程 ID——将导致 Fluentd 将该信号解释为 SIGUSR1 信号。目前,没有 Windows 相当的发送这些信号的方法,尽管已经向项目提交了几个更改请求以实现这些功能。
文件句柄
在 Linux 文件系统中,任何时刻可以使用的文件句柄数量可以控制,与 Windows 不同,Windows 的这些限制完全由操作系统版本和架构(例如,32 位或 64 位)驱动。此外,Linux 使用文件句柄来表示真实文件,但这些句柄也代表诸如网络连接等事物。默认的文件句柄数量可能对 Fluentd 来说过于限制性。在生产环境中调整保持打开的文件句柄数量并不罕见。可以通过编辑配置文件或使用 Linux 的 ulimit 命令来操作文件句柄限制。更多详细信息可以在 linuxhint.com/linux_ulimit_command/ 找到。文件句柄的数量不应该成为提供的示例和场景的问题,但在生产环境中增加流量时,这是一个需要注意的事项。正确的文件句柄数量取决于正在写入的文件的数量和速度、支持的网络端口的数量等。
2.2.6 部署日志生成器
理想情况下,我们希望验证输入插件的配置,并确认诸如日志轮转等事物的配置。我们希望有一个配置驱动的实用工具,可以持续发送日志事件。我们有一个可用的工具在 github.com/mp3monster/LogGenerator,将在后续章节中使用这个工具。此工具为我们提供了几个有用的功能:
-
取一个现有的日志文件,从现有的日志文件中回放日志事件,用当前的时间戳写入,并按照日志最初编写时的相同时间间隔写入日志。
-
取一个测试日志文件,描述时间间隔和日志正文,并按照事件之间的正确间隔回放。
-
根据模式编写日志文件,这意味着可以生成不同的日志格式。
-
通过 Java 日志框架发送日志以模拟使用日志框架的应用程序。
LogGenerator GitHub 仓库包含了关于如何使用该工具的扩展文档。该实用工具是用 Groovy 编写的,这意味着其核心是 Java,并且使用了标准的 Java 类和库。Groovy 相比 Java 增加了一些便利性。具体来说,它作为脚本执行以保持开发快速便捷,这意味着根据您的需求进行调整很容易;它包含了一些方便的类,使得处理 REST 和 JSON 非常容易。并不是每个人都想安装 Groovy 或修改脚本。因此,我们利用 Groovy 与 Java 的关系,将其编译并打包成 JAR 文件,使得在没有安装 Groovy 的情况下也可以执行。JAR 文件可以从 GitHub 下载。The JAR is available to download from GitHub as well.
Java 安装
要安装 Java,你可以使用包管理器或从www.java.com/en/download/检索并下载。工具的实现已经完成,以便 Java 8 或更高版本可以工作。然而,你需要 Java 开发工具包(JDK)而不是 Java 运行时环境(JRE)。一旦 Java 下载并安装,你需要确保正确的版本已设置在你的PATH环境变量和JAVA_HOME中。我们假设你没有其他应用程序使用 Java,并且依赖于不同的 Java 版本。如果是这种情况,我们建议编写一个脚本,每次你启动一个新的控制台来运行 LogGenerator 时设置这些变量;这种方法在 Groovy 设置中得到了说明。你可以使用命令java –version来检查正在使用的 Java 版本。
Groovy 安装
如果你想要使用已准备好的 JAR 版本的 LogSimulator,你可以跳过这一节,但如果你想要使用 Groovy 版本,了解其工作方式或对其进行修改,你需要以下步骤。在安装了先决条件 Java 之后,我们现在可以安装 Groovy(从groovy.apache.org/download.html下载或使用包管理器安装)。与 Java 一样,你也需要将 Groovy 设置在PATH环境变量和GROOVY_HOME中。你可以使用命令groovy -–version来确认 Groovy 是否已适当地安装。以下是一些示例代码片段,用于确保环境变量已设置。这是 Windows 的设置:
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_221
set PATH=%JAVA_HOME%\bin;%PATH%
echo Set Shell to Java
java -version
set GROOVY_HOME=C:\Program Files\Groovy-3.0.2\
set PATH=%GROOVY_HOME%\bin;%PATH%
echo Set Shell to Groovy
groovy --version
这个脚本的 Linux 版本将是
export JAVA_HOME=/usr/lib/jdk1.8.0_221
export PATH=$JAVA_HOME/bin:$PATH
echo Set Shell to Java
java -version
export GROOVY_HOME=/usr/lib/Groovy-3.0.2
export PATH=$GROOVY_HOME/bin;$PATH
echo Set Shell to Groovy
groovy --version
模拟器使用一个属性文件来控制其行为,并使用一个描述一系列日志条目的文件来重放。我们将在后面的章节中使用这个文件来查看日志轮转和其他行为是如何工作的。每一本书的章节都有一个包含相关属性文件和日志源的文件夹,以帮助理解该章节,如图 2.5 所示。按照之前推荐的方式,将 LogSimulator 复制到下载根文件夹中,然后运行以下命令:
groovy LogSimulator.groovy Chapter2\\SimulatorConfig\\tool.properties
我们可以在图 2.6 中看到一个示例,展示了将 LogSimulator 作为 Groovy 应用程序运行时的控制台输出。

图 2.6 LogSimulator 在详细模式下使用 HelloWorld-Verbose.properties 文件和与相关 HelloWorld.conf 文件一起运行的 Fluentd 的示例输出
将 LogSimulator 作为 JAR 运行
要使用 LogSimulator 的 JAR 版本,需要将 JAR 文件下载到所有章节资源文件夹的父目录中。然后,可以将命令中的 Groovy LogSimulator.groovy 元素替换为java -jar LogSimulator .jar,因此命令将显示为
java -jar LogSimulator.jar Chapter2\\SimulatorConfig\\tool.properties
我们将假设你已经安装了 Groovy,并使用 Groovy 命令运行 LogGenerator,本书的其余部分将如此进行。但正如你所见,唯一的区别是命令中用于 Java 或 Groovy 以及 JAR 或 Groovy 文件的那个部分。如果你希望扩展工具并重新创建 jar 文件,GitHub 仓库包含了所有关于如何生成 JAR 文件的详细信息。
更详细地了解 LogSimulator
如果你想要深入了解正在发生的事情,那么请编辑 tool.properties 文件,将 verbose 属性从 false 更改为 true。这将显示在控制台日志中定义在文件 small-source.txt 中的日志条目。关于模拟器的所有属性都在github.com/mp3 monster/LogGenerator的文档中解释。
2.2.7 安装 Postman
在我们的“Hello World”场景中,需要一个易于使用的工具来发送单个日志事件以练习 Fluentd 配置。虽然可以使用 cURL 等工具,但我们选择使用具有友好 UI 和跨多平台工作能力的 Postman。Postman 是一个广为人知的工具,支持大多数环境(Windows、macOS、Linux 等)。Postman 对个人用户免费,可以从 www.postman.com/downloads/ 获取二进制文件。
对于 Windows,这是一个安装程序,将解决适当的文件位置。对于 Linux,下载的是一个 tarred gzip 文件,需要解压缩(例如,tar -xvf Postman-linux-x64-8.6.2.tar.gz)。一旦 Postman 安装/解压缩完成,请确保它可以启动——对于 Windows,可以使用安装的链接来完成。
2.3 使用 “Hello World” 使 Fluentd 生机勃勃
现在我们已经了解了 Fluentd 的架构并将其部署到环境中,让我们让它变得生动起来。
2.3.1 “Hello World” 场景
“Hello World” 场景非常简单。我们将利用 Fluentd 可以通过 HTTP 接收日志事件的事实,简单地查看控制台记录事件。首先,我们将使用 Postman 推送 HTTP 事件。下一步将是稍微扩展这个操作,使用 LogSimulator 发送日志事件。
2.3.2 “Hello World” 配置
在运行示例之前,让我们快速查看配置文件(见列表 2.1)。正如你所见,我们在配置文件中提供了一些注释。在配置文件中,我们可以通过在前面加上哈希(#)字符来在任何地方添加注释。在 <system> 和 </system> 之间的配置是 Fluentd 内部应该如何工作的指令;在这种情况下,使用 Info 级别日志。然后我们使用一个 source 指令来定义日志事件的来源,使用的是 @type 识别的内置 HTTP 插件功能。接下来的名称-值对被视为该插件的属性或属性。例如,在这里我们定义了使用 port 18080 来接收日志事件。
我们然后使用match指令定义一个输出。在match指令中的星号是一个通配符,告诉match指令任何标签都可以由输出插件处理,在这种情况下,标准输出,它将出现在控制台上。在这个示例中使用的配置文件被简化到最基本的形式,仅定义了每个插件的输入和输出参数以及一些说明性注释。
列表 2.1 Chapter2/Fluentd/HelloWorld.conf
# Hello World configuration will take events received on port 18080 using
# HTTP as a protocol
# set Fluentd's configuration parameters
<system>
Log_Level info ❶
</system>
# define the HTTP source which will provide log events
<source> ❷
@type http ❸
port 18080 ❹
</source>
# accept all log events regardless of tag and write them to the console
<match *> ❺
@type stdout
</match>
❶ 设置 Fluentd 的默认日志级别——因为我们已将级别设置为 info,这并不是严格必要的,因为那是默认值。
❷ 这是一个源指令。
❸ @type 表示插件类型。
❹ 在插件后面的行定义了该插件的配置参数。
❺ 匹配指令定义了哪些日志事件将被允许进入插件。
2.3.3 启动 Fluentd
由于 Fluentd 服务在我们的PATH中,我们可以在任何地方使用命令fluentd启动进程。然而,如果没有参数定义配置位置,工具将根据环境和安装过程在不同的位置查找。对于 Windows 和 Linux,Fluentd 将尝试解析位置/etc/fluent/fluent.conf。对于 Windows,除非在 Linux 子系统内运行命令,否则这将失败。我们不使用默认设置来启动 Fluentd。我们需要在 shell 中导航到您下载配置文件的位置,或者将配置文件的完整路径作为参数包含在内。然后运行以下命令:
fluentd -c HelloWorld.conf
要从下载资源的根目录运行 Fluentd 命令,这将是本书余下的标准,命令将是
fluentd -c ./Chapter2/Fluentd/HelloWorld.conf
这个命令将启动 Fluentd,我们将在控制台上看到启动时显示的信息,包括正在加载和检查的配置。当在 Windows 上运行 Fluentd 或 Fluent Bit 时,根据您的用户账户的权限,您可能会收到如图 2.7 所示的提示。这个提示发生是因为 Fluentd 和 Fluent Bit 默认会向网络公开访问点。


当然,我们应该允许访问。没有它,Fluentd 和 Fluent Bit 都将失败。在 Linux 环境中,等效的安全控制是通过 IPTables 规则和可能的 SELinux 配置来建立的。由于 Linux 环境比 Windows 环境变化更多,因此拥有一个好的 Linux 参考以帮助设置和排除任何限制是值得的。Manning 有几本这样的书籍,例如 David Clinton 的Linux in Motion(www.manning.com/livevideo/linux-in-motion)。
下一步是使用 Postman 发送日志事件。一旦 Postman 启动,我们需要配置它向 Fluentd 发送简单的 JSON 有效负载。图 2.8 显示了标题中的设置。

图 2.8 定义 JSON 有效载荷,使用 Postman 发送到 Fluentd
我们还需要设置 Body 内容,因为我们将要使用 POST 操作。通过在屏幕上选择 Body(以及原始选项),我们就可以键入 body 字段{"Hello" : "World"}。完成这些后,我们现在就可以发送了。我们在图 2.9 中看到了这个配置。

图 2.9 在 Postman 中设置消息体
在 Postman 中点击发送按钮。图 2.10 显示了结果。你可能已经注意到,在 API 调用中,我们没有为日志事件定义时间;因此,Fluentd 实例将应用当前系统时间。

图 2.10 发送 REST 事件后的 Fluentd 输出——注意显示接收事件输出的最后一行
正如俗语所说,“这个配置就像一个巧克力茶壶一样有用”,但它确实说明了 Fluentd 的基本理念——能够接收日志事件并将它们(显式或隐式地)导向输出。让我们通过使用 LogSimulator 创建日志事件流来完成这个说明。
运行 LogSimulator 需要一个新的 shell 窗口。在 shell 中,你需要导航到配置已下载的位置。每个章节文件夹中都有一个名为 SimulatorConfig 的文件夹。根据章节的不同,你将找到一个或多个属性文件。在属性文件中,你会找到一系列键值对,这些键值对将控制 LogSimulator 的行为。这包括引用要回放或测试数据的日志文件。这些引用是相对的,这意味着我们需要在正确的文件夹中——章节的父文件夹中——才能成功启动模拟器。然后我们可以使用以下命令启动 LogSimulator
groovy LogSimulator.groovy Chapter2\SimulatorConfig\HelloWorld.properties
或者,如果你选择使用 JAR 文件
java -jar LogSimulator.jar Chapter2\SimulatorConfig\HelloWorld.properties
记得在 Linux 环境中纠正文件路径中的斜杠。LogSimulator 提供了一个配置,将使用相同的 HTTP 端点通过日志文件源发送日志事件。这将导致每个日志事件都在控制台上显示。
2.4 使用 Fluent Bit 的“Hello World”
如前所述,Fluent Bit 是用 C/C++ 编写的,这使得它的体积非常紧凑。这种做法的缺点是,为您的环境构建 Fluent Bit 需要更多的努力。您需要熟悉 Gnu 编译器集合(GCC)(gcc.gnu.org/),它通常在 Linux 平台上可用,或者跨平台的 C 编译器 Clang (clang.llvm.org/),它可以在 GCC 模式下工作。对于本书,我们不会进一步深入 C/C++ 编译的世界。这意味着下载一个预构建的二进制文件或使用支持的包管理器,如 apt 和 yum。对于 Windows,Treasure Data 提供了 Windows 二进制文件(可在 docs.fluentbit.io/manual/installation /windows 获取)。由于二进制文件由 Treasure Data 提供,创建的工件使用了前缀 td。为了简单起见,并与 Fluent Bit 的基本版本保持一致,我们建议下载 zip 版本。我们已经在我们的示例中使用了 zip 下载方法。
将 zip 文件解压到合适的位置(我们将假设为 C:\td-agent),作为安装位置。为了简化操作,值得将 bin 文件夹(例如,C:\td-agent\bin)添加到 PATH 环境变量中,就像我们之前对 Fluentd 所做的那样。
我们可以使用以下简单的命令来检查 Fluent Bit 是否已部署:
fluent-bit -–help
这将提示 Fluent Bit 在控制台上显示其帮助信息。
2.4.1 启动 Fluent Bit
显而易见的假设是,只要我们将 Fluentd 的配置文件限制在 Fluent Bit 部署中可用的插件范围内,我们就可以使用相同的配置文件。不幸的是并非如此——虽然配置文件相似,但它们并不相同。我们稍后会探讨它们之间的差异。但为了使用我们的“Hello World”示例运行 Fluent Bit,让我们从一个之前准备好的配置文件开始,使用以下命令
fluent-bit -c ./Chapter2/FluentBit/HelloWorld.conf
因此,Fluent Bit 将使用提供的配置启动。与 Fluentd 不同,Fluent Bit 对 HTTP 的支持较新,可能不会包含你想要的所有功能,具体取决于你阅读本书的时间。因此,在我们的发送 JSON 的场景中,我们可以匹配 Fluentd 的 HTTP 功能。如果你遇到 HTTP 功能限制,那么至少可以降级到使用 TCP 插件(HTTP 是 TCP 协议之上的一个层)。Fluent Bit 和 Fluentd 都支持 HTTP 操作以捕获状态信息和 HTTP 转发。在 TCP 层工作唯一的缺点是我们不能使用 Postman 发送调用。你可以使用其他知道如何向 TCP 套接字发送文本内容的工具来创建相同的效果。对于 Linux,tc 等实用程序可以做到这一点。在 Windows 环境中,没有相同的原生工具。你可以使用 PuTTY 等工具创建 Telnet 会话(www.putty.org),而 LogSimulator 包含将文本日志事件发送到 TCP 端口的能力。对于 Fluent Bit,让我们使用 Postman 进行 HTTP,并使用 LogSimulator 进行 TCP。从 TCP 开始,以下命令将启动 LogSimulator,向它提供一个属性文件和一个要发送的日志事件文件。由于我们已经安装了这个工具,我们可以启动它。使用具有正确 Java 和 Groovy 版本的单独壳,我们可以运行以下命令
groovy LogSimulator.groovy Chapter2\SimulatorConfig\fb-HelloWorld.properties.\TestData\small-source.json
我们现在可以期待看到运行 LogSimulator 的壳正在将发送的事件报告到控制台。日志事件将以不同的时间间隔发送(控制台应该看起来像图 2.11 中的截图)。

图 2.11 日志事件传输结束时的模拟器控制台输出
同时,另一个控制台中的 Fluent Bit 将开始报告接收事件,并将接收到的 JSON 有效载荷发送到其控制台。这如图 2.12 所示。

图 2.12 Fluent Bit 控制台输出示例
你可能已经注意到,在模拟器启动和看到 Fluent Bit 显示事件之间存在延迟。这反映了配置选项之一是接收到的日志消息缓存刷新到输出的时间间隔。正如我们将在本书后面发现的那样,这是我们可以在其中调整以帮助性能的领域之一。
现在是 HTTP
TCP 和 HTTP 配置之间的差异很小,因此你可以对 Chapter2/FluentBit/HelloWorld.conf 进行更改,或者使用提供的配置文件 Chapter2/FluentBit/HelloWorld-HTTP.conf。以下是需要应用的变化:
-
在输入部分,将
Name tcp更改为Name http。 -
由于我们一直在 Postman 中使用 18080 端口进行 HTTP,让我们在配置中更正端口,将
port 28080替换为port 18080。
保存应用后的这些更改。要查看 Fluent Bit 现在将如何工作,如果它仍在运行,请停止当前的 Fluent Bit 进程。然后像之前一样重新启动,或者使用提供的更改,从以下命令开始:
fluent-bit -c ./Chapter2/FluentBit/HelloWorld-HTTP.conf
一旦运行,使用与 Fluentd 相同的 Postman 设置发送事件,就像我们之前做的那样。
2.4.2 交替的 Fluent Bit 启动选项
Fluent Bit 也可以完全通过命令行进行配置。这使得配置 Fluent Bit 成为一个有效的方法,因为它简化了部署(不需要映射配置文件)。然而,这确实是以可读性为代价的。例如,我们可以使用以下方式重复相同的 Fluent Bit 配置:
fluent-bit -i tcp://0.0.0.0:28080 -o stdout
如果你使用之前设置的模拟器运行此命令,结果将与之前相同。Fluent Bit,就像 Fluentd 一样,并不局限于与单个日志事件源一起工作。我们可以通过向命令行添加额外的输入定义来展示这一点。在 Windows 环境中运行时,让我们将 winlog 事件添加到我们的输入中。对于 Linux 用户,你可以用 cpu 替换 winlog 源,并通过重复相同的练习,但使用以下命令来让 Fluent Bit 告诉我们更多关于它在做什么的信息:
fluent-bit -i tcp://0.0.0.0:28080 -i winlog -o stdout -vv
这次我们将看到几个不同之处。首先,当 Fluent Bit 启动时,它将给我们提供更多信息,包括清楚地显示正在手动管理的输入和输出。这是由于 -vv(更多内容将在下一节中介绍)。随着日志事件的发生,除了我们的日志模拟器事件外,winlog 信息将被交错。
Fluentd 和 Fluent Bit 内部日志级别
Fluentd 和 Fluent Bit 都支持相同的命令行参数,可以控制它们记录关于其活动多少信息(与接收到的任何与日志事件相关的日志级别信息相对)。除了由命令行控制外,此配置还可以通过配置文件设置。这两个工具都识别五个日志级别,并且当没有参数或配置应用时,中等级别(信息)用作默认日志级别。表 2.5 显示了日志级别、命令行参数和等效的配置设置。记住命令行参数的最简单方法是 -v 代表 详细,-q 代表 安静;更多的字母会增加详细程度或安静程度。
表 2.5 Fluentd 和 Fluent Bit 识别的日志级别
| 日志级别 | 命令行 | 配置设置 |
|---|---|---|
| 跟踪 | -vv |
Log_Level trace |
| 调试 | -v |
Log_Level debug |
| 信息 | Log_Level info |
|
| 警告 | -q |
Log_Level warn |
| 错误 | -qq |
Log_Level error |
注意:只有当 Fluent Bit 编译时设置了 构建标志 以启用跟踪时,才会发生跟踪级别的设置。这可以通过 Fluent Bit 帮助命令(fluent-bit -h 或 fluent-bit -–help)来检查,以显示构建标志及其设置。跟踪级别的日志记录通常只在开发插件时需要。
2.4.3 Fluent Bit 配置文件比较
之前我们提到 Fluentd 和 Fluent Bit 配置不同。为了帮助说明差异,表 2.6 提供了并排的配置。
表 2.6 Fluentd 和 Fluent Bit 配置比较(使用 Fluent Bit 的 HTTP 配置)
| Fluent Bit | Fluentd |
|---|
|
# Hello World configuration will take events received
# on port 18080 using TCP as a protocol
[SERVICE]
Flush 1
Daemon Off
Log_Level info
# define the TCP source which will provide log events
[INPUT]
Name http
Host 0.0.0.0
Port 18080
# accept all log events regardless of tag and write
# them to the console
[OUTPUT]
Name stdout
Match *
|
# Hello World configuration will take events received on port 18080 using
# HTTP as a protocol
# set Fluentd's configuration parameters
<system>
Log_Level info
</system>
# define the HTTP source which will provide log events
<source>
@type http
port 18080
</source> # after a directive
# accept all log events regardless of tag and write them to the console
<match *>
@type stdout
</match>
|
如果你想找出差异,那么你应该已经观察到了以下内容:
-
与通过打开和关闭尖括号 (
<>) 定义指令不同,指令在方括号 ([]) 中,终止是通过后续指令或文件末尾隐式完成的。 -
SERVICE替换了 system 用于定义通用配置。 -
@type被替换为Name属性来定义要使用的插件。 -
Match,而不是指令中带有参数的指令名称,变成了
Output。match 子句随后由属性中的另一个名称-值对定义。 -
旧版本的 Fluent Bit 不支持 HTTP,因此事件需要通过 TCP 发送,但接收的事件仍然可以是 JSON 格式。
当并排查看配置时,细节并不太激进地不同,但它们足够显著,足以让人注意到。
2.4.4 Fluent Bit 配置文件详细说明
仔细查看配置文件和应用的规则,我们刚刚看到有一些相似之处,也有一些不同之处。在下面的列表中,我们突出了一些关键规则。
列表 2.2 第二章/FluentBit/HelloWorld.conf
# Hello World configuration will take events received
# on port 18080 using TCP as a protocol
[SERVICE] ❶
Flush 1 ❷
Daemon Off ❸
Log_Level info ❹
# define the TCP source which will provide log events
[INPUT] ❺
Name tcp
Listen 0.0.0.0
Port 18080
# accept all log events regardless of tag and write
# them to the console
[OUTPUT]
Name stdout
Match * ❻
❶ 所有 Fluent Bit 通用配置值都设置在本节中。
❷ Flush 属性控制 Fluent Bit 多频繁地将日志缓存刷新到输出通道(stdout 和 stderr)。在这种情况下,我们将其设置为 1 秒。
❸ 这告诉 Fluent Bit 启动时进程是否应以守护进程运行。
❹ 在配置文件中,缩进很重要,并且必须一致。建议的缩进是四个空格字符。与 YAML 文件一样,缩进表示父级和子级关系。在这种情况下,所有这些值都是这个输入的从属。
❺ 与源不同,Fluent Bit 配置使用输入和输出的术语。
❻ 在 Fluent Bit 中,控制哪些日志事件通过插件传递不是在输出声明中确定的,如 Fluentd 所示,而是通过一个单独的匹配属性。
与 Fluentd 一样,配置文件中的顺序很重要,尤其是在 match 语句中——例如,如果我们立即在当前的 OUTPUT 声明之前添加以下配置片段:
[OUTPUT]
Name file
Path ./test.out
Match *
假设配置如下所示:
# send all log events to a local file called test.out
[OUTPUT]
Name file
Path ./test.out
Match *
# accept all log events regardless of tag and write
# them to the console
[OUTPUT]
Name stdout
Match *
我们是否应该期待日志出现在日志文件、stdout(即控制台)或两者都出现?答案是事件只会出现在文件中。这是因为我们在两个输出中都匹配了所有事件;然后是配置中的第一个输出定义获取了事件(即带有通配符 match 属性的日志文件;没有日志事件会到达 stdout)。
2.4.5 将 dummy 插件投入实际应用
为了测试一些细节,看看你是否能实现以下配置更改。在 Fluentd 和 Fluent Bit 中都有一个内置的输入插件,称为 dummy。修改相应的 HelloWorld.conf 文件,并包含源,然后依次启动 Fluentd 和 Fluent Bit,看看你得到什么结果。练习的结果包含在本章末尾。
答案
与在页面上填充配置文件不同,答案配置可以在下载的文件夹中找到,位于 Chapter2/ExerciseResults/Fluentd/HelloWorld-Answer.conf 和 Chapter2/ExerciseResults/FluentBit/HelloWorld-Answer.conf。
2.5 使用 Kubernetes 和容器部署 Fluentd
到目前为止,我们已经探讨了 Fluentd 和 Fluent Bit 的部署,你可能只是对主机的工作方式(原生部署、虚拟化和容器化)进行了最少的考虑。我们已经引用了一些机制,这些机制可以让我们进一步自动化或容器化这些工具。正如第一章所讨论的,Fluentd 与容器化和 Kubernetes 的使用有着紧密的联系。我们将简要地看看 Fluentd 在 Kubernetes 上下文中的配置方式;当我们到达本书的第三部分时,我们将深入探讨诸如扩展和容器化等细节。
建立 Kubernetes 环境的部署和容器化需要一本自己的书(我们推荐 Marko Lukša 的《Kubernetes in Action》,第 2 版;www.manning.com/books/kubernetes-in-action-second-edition)。然而,了解其基本操作原理是值得的;在我们接下来的章节中配置 Fluentd 时,你将能够欣赏到配置如何与 Kubernetes 部署相关联。它也可能激发你关于如何以及使用 Fluentd 监控微服务的想法。
2.5.1 Fluentd DaemonSet
Fluentd 是将日志管理集成到 Kubernetes 环境的选项之一。这通常是通过定义配置文件来实现的。Kubernetes 配置文件告诉 Kubernetes 如何在单个或多个工作节点(为 Kubernetes 集群提供计算能力的服务器)上运行 pod(协同工作的容器集合)和容器。在 Kubernetes 中,我们可以描述 pod 部署的不同方式,例如 ReplicaSets、Jobs 和 DaemonSets。例如,可以定义一些事情,使得 Fluentd 容器将在每个工作节点上执行,以收集该节点上所有本地容器运行的日志事件。这种 Kubernetes 中的配置类型被称为 DaemonSet,并且是 Kubernetes 为 Fluentd 常见的配置。正如我们将在本书后面看到的那样,这并不是部署 Fluentd 的唯一方法,我们也不限于一种部署模型。在下一个列表中,我们可以看到一个示例 DaemonSet 配置,用于应用配置文件和将日志事件路由到另一个 Fluentd 节点的参数。
列表 2.3 第二章/开箱即用的 Fluentd DaemonSet,用于转发
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-system
labels:
k8s-app: fluentd-logging
version: v1
spec:
selector:
matchLabels:
k8s-app: fluentd-logging
version: v1
template:
metadata:
labels:
k8s-app: fluentd-logging
version: v1
spec:
tolerations:
- key: node-role.kubernetes.io/master ❶
effect: NoSchedule ❷
containers: ❸
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset
[CA]:v1-debian-forward ❹
env: ❺
- name: FLUENT_FOWARD_HOST
value: "REMOTE_ENDPOINT"
- name: FLUENT_FOWARD_PORT
value: "18080"
resources: ❻
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts: ❼
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracPeriodSeconds: 30
volumes: ❽
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
❶ 告诉 Kubernetes 是否应该在主节点(控制节点)上运行 pod
❷ 告诉 Kubernetes 这是一项必须持续运行的任务,而不是按计划运行
❸ 识别 Kubernetes 将要使用的容器镜像,该镜像将运行 Fluentd
❹ 这是我们开始定义 pod 内部容器的地方。除了这里显示的内容外,每个容器还可以执行一些操作,例如定义启动命令——例如,定制每个 Fluentd 实例。
❺ 还可以通过这些名称-值对在容器实例内设置环境变量。在这种情况下,定义了几个变量,然后在配置文件中引用这些变量以指导转发插件。
❻ 可以定义资源配额,这样 Fluentd 就不会耗尽节点上其他进程的时间。但这可能会有其他后果。
❼ 描述容器内的一个挂载点,可以用来访问外部化存储
❽ 描述容器的外部存储在底层 Kubernetes 基础设施上的位置。在企业场景中,这可能是一个网络存储设备,如 SAN,或者在云中它将被映射到某种形式的块存储。这意味着我们可以将 Fluentd 生成的日志映射到共享位置,并且我们可以指导 Fluentd 实例去获取一个公共配置文件。
备注:DaemonSet 来自 mng.bz/nYea。
应该注意的是,在托管 Kubernetes 节点的基础设施中,可以直接在底层平台上运行 Fluentd 等进程。虽然这消除了 Kubernetes 的抽象层(及其相关开销),但也消除了使用 Kubernetes 管理和监控 Fluentd 运行的机会。我们仅在非常特殊的情况下推荐这样做。
注意:DaemonSets 被定义为在每个工作节点上提供基本操作。这可以是将日志事件直接发送到 Elasticsearch(如第一章中讨论的 EFK 堆栈的一部分)或将日志转发到各种云供应商的日志分析解决方案,例如 AWS CloudWatch。这些可以在 Fluentd GitHub(mng.bz/2jng)中找到。
图 2.13 说明了如何使用 DaemonSet 使用 Kubernetes 配置。通常,DaemonSet 配置会存储在共享配置仓库或文件系统中,然后通过像 kubectl 这样的工具传递给 Kubernetes,kubectl 是标准的 Kubernetes CLI 工具。我们假设 Fluentd 配置位于共享文件系统中,因此由 Fluentd 容器挂载以允许访问。另一种方法是通过 DaemonSet YAML 文件传递配置,或者直接将配置直接连接到 Docker 镜像。在 DaemonSet 中,Fluentd 配置具有的日志消费者可以将日志事件直接发送到 Elasticsearch,或者发送到 Kubernetes 配置使其可访问的集群外部的文件系统。当我们到达扩展 Fluentd 时,我们将进一步探讨这一点。

图 2.13 展示了 Fluentd 在 Kubernetes 中作为 DaemonSet 的部署模型。Kubernetes 集群中的每个独立服务器都有自己的 pod,运行着 Fluentd 容器。
2.5.2 Docker 化 Fluentd
就像几乎所有应用程序一样,除了手动安装或通过像 Ansible (www.ansible.com)这样的工具自动化安装外,还可以使用 Docker 容器引擎部署 Fluentd 或 Fluent Bit。预定义的 Fluentd Docker 文件(即告诉 Docker 如何构建可执行镜像的文件)在 GitHub 仓库(github.com/fluent/fluentd-docker-image)中提供,包括解决不同的主机操作系统因素(例如,Debian 到 Windows)。Fluent Bit 在 GitHub 上也有更小的一组预定义 Docker 文件(github.com/fluent/fluent-bit)。GitHub 仓库包含配置文件和脚本。已实现的镜像存储在 Docker Hub 上,可以在hub.docker.com/u/fluent找到 Fluentd,以及hub.docker.com/r/fluent/fluent-bit找到 Fluent Bit。
2.6 使用 Fluentd UI
我们已经成功安装并运行了 Fluentd 和 Fluent Bit。但在两种情况下,控制都是通过命令行进行的。如果安装了 Web UI,Fluentd 也可以运行。Web UI 由执行 Fluentd 核心逻辑的同一进程提供服务。
2.6.1 使用 UI 安装 Fluentd
安装将触发 Fluentd 下载并安装一系列额外的 gem。这是因为它提供了将多个插件(除了基本提供的插件之外)纳入其中的手段。这意味着安装时间比仅安装 Fluentd 要长。安装 UI 的命令是
gem install -V fluentd-ui
fluentd-ui setup
安装完成后,我们可以使用以下命令启动 UI:
fluentd-ui start
这将启动一个 Fluentd 节点,其中包含一个 Web 服务器。可以通过打开端口 9292 来访问 Web UI(即,将浏览器指向localhost:9292将显示登录界面)。
使用 HTTPS 保护 Fluent-UI
Fluentd UI 通过 HTTP 运行;默认安装中不使用 SSL/TLS 证书。在开发/实验环境中这不太可能成为问题。但是,在生产环境中,不使用 SSL/TLS 和至少基本凭证运行则远非推荐的做法。这可以通过几种方式来解决:
-
在 Fluentd-ui 前面使用 Nginx 或 Apache 服务器实现反向代理——这是一种常见的保护未由 SSL/TLS 证书保护的 Web 内容的方法(有关如何操作的文档可在
mng.bz/Ywne找到)。这也意味着您的环境中将运行一个额外的进程,需要配置网络,以确保反向代理不会被绕过。 -
对于其网络层,Fluentd UI 使用 Ruby on Rails 框架(
ruby onrails.org/)和 Ruby 应用程序服务器 Puma(puma.io)。因此,可以配置 Puma 使用 SSL/TLS 证书。应用配置需要 Ruby 代码更改和启动参数,这将对 Fluent 代码库产生影响。这并不理想,因为任何更新都意味着需要重新应用这些更改。 -
我们不推荐在生产环境中使用 Fluentd UI。这看起来像是回避问题而不是解决问题。然而,这确实有很多优点。对于生产环境,您希望通过 Git 等工具控制 Fluentd 配置文件。这意味着不赋予用户在生产环境中使用 UI 进行配置更改的权限。更好的做法是让用户进行受控的更改,然后可以安全地部署。如果您在微服务或分布式环境中运行 Fluentd,仅允许从受控配置文件进行更改,这提供了驱动环境一致性和减少“配置漂移”机会的手段。
-
再次强调,我们只推荐将 Fluentd UI 用于实验目的,而不是在生产环境中使用。鉴于这一点,以下内容将提供足够的见解,使您能够欣赏 UI 支持的功能。
默认情况下,登录用户名为 admin,密码为 changeme。一旦登录,UI 所展示的界面将类似于图 2.14。由于 UI 具有反应性和响应性,可能会出现差异,导致布局根据查看 UI 的设备进行调整。

图 2.14 Fluentd UI 在没有任何配置的情况下启动
我们需要为 Fluentd 节点提供一些配置值以执行。点击设置 Fluentd 将带我们到一个 UI,通过该 UI 我们可以配置其行为。图 2.15 展示了一些相关的配置需求。

图 2.15 Fluentd UI 设置配置位置
配置字段使用默认值设置。将 Config File 选项切换到指向用于运行 Fluentd 的现有 HelloWorld.conf 文件。您可能还希望提供进程标识符(PID)和日志文件的备用位置。一旦我们在 UI 中点击创建按钮,如果位置和文件可以写入和读取,服务器进程将启动。然后 UI 将切换到不同的主页,如图 2.16 所示。

图 2.16 后端运行后的 Fluentd UI
左侧的导航菜单现在要丰富得多。Fluentd 子菜单提供了用于处理配置文件、访问日志以及任何错误日志的选项。显示的日志与控制台输出相同。导航菜单让我们可以看到已安装插件、推荐插件和更新插件的详细信息。
屏幕的核心部分留给了服务器产生的实时日志,以及用于启动和停止操作以及当前配置的控制。Config File 选项将显示正在使用的配置文件,并允许直接编辑配置文件。如果配置的 UI 选项出现问题,您可以求助于传统的编辑。添加源和输出选项允许使用 UI 作为引导的、基于表单的演示来捕获插件配置,以修改配置值。如图 2.17 所示,UI 为设置插件及其配置值提供了一个良好的逻辑流程。

图 2.17 Fluentd UI 定义输入和输出
点击源、过滤器或输出元素之一将导航到配置该类型插件的 UI。例如,选择文件源将向您提供一个文件选择器 UI(如图 2.18 所示)。

图 2.18 Fluentd UI 文件选择器作为文件插件配置的一部分
摘要
-
日志事件由一个标签、一个时间戳和一个包含核心日志事件的记录组成。
-
在将多个服务器日志合并在一起以确保正确日志顺序时,使用 NTP 进行机器时间同步至关重要。
-
Fluentd 和 Fluent Bit 可以部署在大多数环境中,因为基础设施需求非常小,应用程序依赖性最小。如有必要,您可以编译这些工具以在特定情况下工作。
-
部署 Fluentd 有多种方式,包括部署 Ruby 和 RubyGems,然后作为 gem 获取 Fluentd。
-
将 LogSimulator 部署以快速模拟日志事件源只需要 Java,但若要自定义此工具,则需要 Groovy。
-
Fluentd 可以与 Kubernetes 和 Docker 日志记录一起使用,也可以与传统的环境一起使用。我们可以为 Kubernetes 部署检索标准配置。
-
当部署在 Linux 主机上时,Fluentd 可以响应如 SIGINT 的信号以优雅地关闭,以及 SIGUSR2 信号以重新加载配置文件。
-
Fluentd UI 是 Fluentd 提供的附加工具之一。这提供了一个网页前端来可视化 Fluentd 环境的配置并观察 Fluentd 正在做什么。其他工具包括生成证书和列出可用插件的功能。
-
配置文件中定义配置的顺序很重要。
-
Fluentd 和 Fluent Bit 的自用日志可以配置为不同的日志级别。
-
Fluentd 和 Fluent Bit 的配置相似但并不相同。
第二部分:深入理解 Fluentd
在第一部分,我们介绍了 Fluentd 和 Fluent Bit,探讨了推动这些工具采用的因素,并涵盖了 Fluentd 和 Fluent Bit 最适合解决的难题。我们还花时间了解了 Fluentd 和 Fluent Bit 之间的差异。从现在开始,我们将主要关注 Fluentd。
在以下部分,我们将深入探讨 Fluentd 的功能和配置方法。在这个过程中,我们将处理大多数用户迟早会遇到的一些场景——例如,如何捕获由其他日志应用程序添加的日志文件中的日志事件,包括处理日志轮转等挑战。第三章从捕获日志事件开始,重点关注文件等来源,因为这是日志事件最常见的来源之一。第四章将带我们进入将日志事件存储在不同目的地,如数据库和社交/协作工具的过程。这包括处理事件的解析和格式化。第五章和第六章展示了日志事件路由、重复和日志属性注入的不同方面。
到第二部分结束时,我们将覆盖足够的内容,让您为大多数用例构建配置。这使得我们可以进入第三部分,在那里我们将重点关注 Docker、Kubernetes、扩展、性能以及需要专业插件的用例的挑战。
3 使用 Fluentd 捕获日志事件
本章涵盖
-
为日志文件的输入配置 Fluentd
-
通过 Fluentd 检查文件读取过程中停止和启动的影响
-
使用解析器从日志事件中提取更多意义
-
使用 API 进行 Fluentd 的自监控和外部监控
在建立了概念和架构基础,并运行了一个简单的配置之后,我们准备开始更详细地查看日志事件的捕获。在本章中,我们将重点关注捕获日志事件。但在我们这样做之前,让我们看看我们如何检查我们的 Fluentd 配置是否正确。
设置以跟踪和尝试配置
关于我们在书中如何呈现代码的简要说明。为了避免书籍因代码和 Fluentd 配置文件而变得臃肿,我们只包含了与讨论主题相关的配置和代码部分。但下载和 GitHub 仓库中引用的文件是完整的配置(github.com/mp3monster/LoggingInActionWithFluentd)。
仓库中包含了完整的配置和部分配置文件,这样您可以自行实现配置。此外,还包括了一些场景及其解决方案,这将使您能够尝试检验自己对书中观点的理解。
如果您跳过了前面的章节,您需要确保您已安装并配置了 LogSimulator(详情见第二章)或已按照github.com/mp3monster/LogGenerator中记录的基本设置进行操作,包括任何故障排除技巧。
3.1 干运行以检查配置
在开发 Fluentd 配置时,我们不希望设置一个测试来发现配置错误。正如我们在开发代码时所做的,我们在尝试运行解决方案之前,会使用一种方法来检查代码。当配置或代码变得更加复杂时,这一点变得更加重要。
干运行配置文件使 Fluentd 加载配置并确认它可以根据语法正确性以及属性是否被识别以及提供的值是否有效来执行它。干运行选项是 Fluentd 命令行的一部分。要使用干运行功能,我们只需将 –-dry-run 添加到命令行参数中。任何配置错误都会在控制台输出中报告;例如:
2020-04-17 11:08:51 +0100 [error]: config error file="Chapter3/Fluentd/basic-file-read2.conf" error_class=Fluent::ConfigError error="'path' parameter is required"
这次干运行显示了一个插件缺少一个必需的属性;在这种情况下,失败的插件配置需要一个路径属性。
解决结构错误
如果错误更“结构化”,例如省略了声明性块的开始或结束——例如,当存在<parse>声明时却遗漏了</parse>——那么我们最有可能看到backtrace(堆栈跟踪)错误。Fluentd 将完成对它认为缺失内容的回溯错误。在这些情况下,建议可能是不正确的,而其他地方缺少的语法元素是原因。解决这类问题的最简单方法是确保你已经应用了良好的缩进并开始匹配开始和结束块。
干运行的成功执行将导致 Fluentd 优雅地停止。如果你使用默认的日志级别运行,那么将显示以下类型的消息:
2020-04-17 10:53:24 +0100 [info]: finished dry run mode
由于这被归类为info,如果你已经将 Fluentd 配置为更安静,你可能不会看到消息,只会看到 Fluentd 在没有错误的情况下停止。
Fluent Bit 目前没有类似的功能,部分原因是期望配置更简单。通过命令行通信来提供 Fluent Bit 配置的能力将更具挑战性。如果你的目标是使用命令行向 Fluent Bit 提供配置,我们建议你开始使用文件,直到配置完成且有效。然后删除新行和多余的空白字符——这可以通过在线工具(例如browserling.com)或仅使用 Linux 主机上的 awk 和 sed 来完成。这应该允许你将配置简化为单行,以便使用。
3.1.1 将验证 Fluentd 配置付诸实践
作为指定的团队 Fluentd 专家,你被要求检查几个配置。这是一个尝试使用 dry-run 功能来评估配置是否有效并必要时修复它的机会(如果你需要修复配置,可能值得制作原始配置的副本)。要验证的配置文件如下:
-
Chapter3/Fluentd/basic-file-read.conf -
Chapter3/Fluentd/dry-run.conf
没有人想成为那个为每个人修复 Fluentd 配置的人,所以你可能需要考虑如何与你的同事分享以下问题的答案:
-
你如何知道配置文件是有效的?
-
你如何知道配置有误?
答案
-
你应该已经发现
Chapter3/Fluentd/basic-file-read.conf已经是有效的。我们知道这个配置文件是好的,因为当使用 dry-run 模式时,控制台输出不会报告任何错误消息并且应该干净地终止。进程将以以下消息终止(假设日志级别没有设置高于 info):2020-04-17 10:53:24 +0100 [info]: finsihed dry run mode -
控制台日志输出将报告一个错误,反映为
Chapter3/Fluentd/dry-run.conf识别出的配置问题。我们应该看到以下带有详细信息的消息:2020-04-17 10:54:00 +0100 [error]: config error file="Chapter3/Fluentd/basic-file-read2.conf" error_class=Fluent::ConfigError error="Missing '@type' parameter on <source> directive"
3.2 读取日志文件
当涉及到应用程序时,日志文件是日志事件最常见的来源。虽然效率不高,但文件创建和消耗是数据在进程之间共享(包括事件)的最古老方式。因此,File 插件是核心插件集的一部分。
第一步是在 Fluentd 配置文件中构建一个文件源。这可以通过将以下片段添加到 Chapter3/Fluentd/no-source-config.conf 的副本中(或使用 Chapter3/Fluentd/basic-file-read.conf)来完成:
列表 3.1 Chapter3/Fluentd/basic-file-read.conf 展示文件尾部
<source>
@type tail ❶
path ./Chapter3/basic-file.* ❷
read_lines_limit 5 ❸
tag simpleFile
<parse> ❹
@type none
</parse>
</source>
❶ 文件源插件被称为 tail,因为它在行为上有点像同名的 Linux 命令。
❷ 定义要捕获的文件
❸ 在开始处理事件之前应读取的最大行数
❹ 每个文件处理器都需要知道如何将文本行输入转换为日志事件。Fluentd 提供了几个标准解析器,从预定义格式到表达式处理器。
如配置摘录所示,我们已告诉 Fluentd 使用第三章文件夹中任何以 basic-file 开头的文件,并读取其内容而不进行任何形式的解析。在启动 Fluentd 之前,我们运行模拟器以查看输出内容。这可以通过以下命令完成
groovy LogSimulator.groovy ./Chapter3/SimulatorConfig/basic-log-file.properties ./TestData/small-source.txt
我们应该看到在第三章文件夹中创建了一个名为 basic-log.txt 的文件。该文件夹将只包含我们源文件的消息部分 (TestData/small-source.txt)。
生成日志文件后,我们现在可以启动 Fluentd 来查看会发生什么。这是通过以下命令完成的(记住我们正在使用第二章中解释的相对路径)
fluentd -c ./Chapter3/Fluentd/basic-file-read.conf
当 Fluentd 启动时,我们将看到控制台输出显示配置文件。不久之后,它将检测到文件并将其作为日志事件发送到控制台。
3.2.1 将 Fluentd 配置的适配应用到 Fluent Bit 中
您的团队已决定当前的配置要求足够简单,可以使用 Fluent Bit 而不是 Fluentd。作为在容器中部署解决方案的准备的一部分,您需要复制 Chapter3/FluentBit/no-source-config.conf 文件。然后应用适当的配置更改并运行 Fluent Bit 以测试配置。
答案
使用命令 fluent-bit -c <配置文件> 启动 Fluent Bit。您将生成的配置应该看起来像 Chapter3/ExerciseResults/basic-file-read-FluentBit-Answer.conf 中的配置。
可以使用以下命令启动模拟器
groovy LogSimulator.groovy ./Chapter3/SimulatorConfig/basic-log-file.properties ./TestData/source.txt
日志事件应作为结果显示在控制台上。
3.2.2 重新读取和继续读取日志文件
如果 Fluentd 需要停止(或需要被停止),但应用程序继续写入日志事件,那么当 Fluentd 重新启动时,它将收集它找到的所有日志事件,而不仅仅是 Fluentd 停止后写入的事件。在 Fluentd 与应用程序逻辑位于同一容器内的微服务中,这种行为可能是可接受的,并且由于 Fluentd 停止,Kubernetes pod 被关闭。然后启动一个新的容器实例。但在许多情况下,这还不够。幸运的是,这已经被考虑到了,Fluentd 有方法通过日志文件(们)跟踪其进度。如果配置没有跟踪其位置以从上次停止的地方继续,则会显示如下警告:
2020-04-14 17:07:09 +0100 [warn]: #0 'pos_file PATH' parameter is not set to a 'tail' source.
2020-04-14 17:07:09 +0100 [warn]: #0 this parameter is highly recommended to save the position to resume tailing.
如果你想消除这个警告,因为你不需要从上次停止的地方继续,那么需要在配置中添加一个额外的属性到尾部语句中:
read_from_head true
在许多情况下,尤其是在更传统的部署中,我们肯定会希望从上次读取的最后一个日志事件继续。我们应该引入一个属性,告诉 Fluentd 通过日志文件记录其进度
pos_file ./Chapter3/basic-file.pos
pos_file属性由尾部插件用于指定一个插件可以记录其通过日志文件(们)进度的文件。当 Fluentd 重新启动时,pos_file作为启动过程的一部分被检查,以确定从哪里开始。但仅pos_file本身并不能确保在 Fluentd 第一次启动时收集现有的日志条目。为了确保从开始收集所有日志事件,我们需要使用read_from_head属性并将其设置为true。
3.2.3 跟踪位置配置考虑因素
在使用位置文件时,需要考虑一些设计问题,例如当定义了pos_file时,尾部插件所使用的位置文件。这些考虑因素包括以下内容:
-
避免在不同尾部配置之间共享文件。 共享此类文件存在 I/O 冲突的风险,因为两个不同的线程试图同时写入它们的位置信息,从而导致文件损坏。这也适用于设置多个工作线程(我们将在查看 Fluentd 扩展时详细研究这一点)。
-
你希望
pos_file条目仅存在于日志文件存在期间。 如果删除了日志文件,则需要删除跟踪文件。否则,在重启时,插件无法正确确定文件处理从哪里继续。在容器化环境中,这可能很棘手,因为文件系统可能完全是本地的,因此与容器一样短暂。如果日志文件的部分文件系统映射到容器外的持久存储,则可以克服这一点。
推荐:如果可能的话,在使用位置跟踪文件时,请将它们保存在与它们所跟踪的日志文件相同的文件夹中。这样做可以提高跟踪器和日志文件被一致处理(即保留或临时处理)的机会。当日志文件被清除时,pos_file 同时被清除的机会会更大。
注意:在pos_file的使用方面,Fluentd 和 Fluent Bit 之间存在一些差异。Fluent Bit 没有pos_file属性;相反,它使用DB作为此任务的属性。
尝试重新运行 Fluentd 配置,将上述更改应用于当前配置文件(或者运行带有Chapter3/Fluentd/basic-file -read2.conf的 Fluentd,其中包含位置文件在配置中)。
3.2.4 路径属性中的通配符
你可能已经注意到,在路径声明中配置使用了通配符(即星号,“*”),而不是文件扩展名。Fluentd 将以操作系统相同的方式接受通配符的使用。因此,可以通过单个source指令(或者如果你更喜欢,配置)读取多个文件。
如果你已经运行了之前的案例配置,这可以简单地说明。通过运行该配置,你应该在第三章文件夹中有一个名为basic-file.txt和basic -file-read2.pos_file的文件。删除pos_file,然后将basic-file.txt复制到basic-file.log。使用与上次相同的配置文件重新运行 Fluentd。在日志输出中,你会看到两条记录说#0 following tail of ./Chapter3/basic-file.txt和#0 following the tail of ./Chapter3/basic-file.log。如果你打开pos_file,你会看到每个文件都有一行。
注意:如果你在 Fluentd 仍在运行时尝试删除 pos 文件,Fluentd 仍然会持有该文件的句柄,这会阻止删除。因此,始终先关闭 Fluentd 进程。
这意味着在使用通配符时必须谨慎;如果应用程序需要在同一位置创建多个需要捕获的日志,这可能会带来优势。另一个可以极大帮助的使用案例是,如果你弹性扩展解决方案,例如配置为将日志记录到高性能网络存储设备的 Web 服务器。我们可以将 Fluentd 配置设置为针对单个文件夹位置,而不是为每个服务器的日志文件设置 Fluentd 配置。
在后一种情况下,如果每个 Web 服务器都有自己的日志文件,并且在 Fluentd 启动后启动了新的 Web 服务器,我们需要检测新的日志文件。默认情况下,Fluentd 每 60 秒检查一次匹配path属性的文件。这可以通过使用名为refresh_interval的属性并使用时间表达式来调整;例如,refresh_interval 5s表示每 5 秒检查一次。因此,那些新的 Web 服务器日志文件将在后续扫描中被捕获。这提供了一种简单的方法来适应自动扩展。
3.2.5 表达时间
当将时间定义为间隔,例如需要指定任务频率时,Fluentd 有一个时间数据类型,以及用于设置这些属性值的关联符号。对于时间数据类型属性,我们可以像表 3.1 所示那样表示时间值。
表 3.1 Fluentd 配置中表达时间类型值的符号
| 时间间隔 | 字符 | 示例 |
|---|---|---|
| 秒 | s | 10s → 10 秒 0.1s → 100 毫秒 |
| 分钟 | m | 1m → 1 分钟 0.25m → 15 秒 |
| 小时 | h | 24h → 24 小时 0.25h → 15 分钟 |
| 天 | d | 1d → 1 天 0.5d → 12 小时 |
提供的数字将被视为整数。如果值不是整数,它将被处理为一个表示时间周期分数的浮点数。这种表示法适用于几乎所有用于表达时间间隔的标准 Fluentd 和 Fluent Bit 插件属性。
3.2.6 控制文件名中通配符的影响
正如我们刚才讨论的,通配符的使用可能很强大,但也伴随着风险,例如选择不需要的文件。我们可以应用几种策略来控制通配符的风险。
明确列出
如果所有文件名都提前知道,path 属性可以填充为一个以逗号分隔的文件列表。文件将以与通配符路径匹配多个文件相同的方式进行处理——如果我们在记录读取进度,每个文件将被读取并记录一个 pos 条目。path 属性可能看起来像这样:
path Chapter3/structured-rolling-log.0.log, ChapterN/another-rolling-log.0.log
与许多其他插件一样,可以更改路径中每个条目的分隔符。这是通过设置属性 path_delimiter(例如,path_delimiter = ';')来完成的,这使得我们可以绕过任何奇怪的文件命名问题。
按日期文件夹进行日志排序
一些解决方案允许您配置日志记录,以便文件夹包含特定时间段的所有日志(一天、一个月等的所有日志)。这使得管理覆盖长期的多日志更容易。因此,Fluentd 能够使用日期元素(例如,Chapter3/2020/08/30/app.log)处理此类文件路径的结构。为了实现这种效果,我们需要修改配置,使其看起来像
path Chapter3/%Y/%m/%d/*.log
在附录 B 的“表达日期和时间”部分,有一个表格描述了不同的转义序列,例如 %Y、%m, 和 %d,如前例所示。
配置错误
Fluentd 擅长指示其配置中的错误。通常它们作为警告发布在 Fluentd 输出中,并附有解释。例如,将 %B 添加到文件名中会产生 ./Chapter3/structured-rolling-logMay.0.log not found. Continuing without tailing it。这是有道理的;文件不存在。另一个错误处理的例子是使用转义序列处理不正确的日期属性,导致 Fluentd 将这些值视为普通文本并报告错误。
使用文件排除
另一种方法是使用exclude_path属性提供不应考虑的文件列表来排除文件。此属性的工作方式与path属性类似,因此它可以使用通配符或逗号分隔列表。例如:
exclude_path: ./Chapter3/*.zip, ./Chapter3/threadDump*.txt
此声明将防止从文件夹中收集任何具有.zip 或线程转储文件的文件。这是一种减轻意外选择不应捕获的文件的风险的好方法——例如,如果应用程序的日志也可能生成堆栈转储文件或打算发送给供应商进行分析的文档,并且这些文件位于与正常日志相同的文件位置。
只考虑最近更改的文件
我们可以告诉插件只考虑在一定时间范围内发生变化的文件。因此,我们可以假设在安装或重启后不久生成的/更改的任何文件都包含值得收集的日志事件;这是通过将属性limit_recently_modified设置为时间间隔值来完成的。例如:
limit_recently_modified: 2m
limit_recently_modified属性是时间数据类型的一个实例,因此它可以按照我们刚才描述的方式进行配置。
通过使用更改持续时间来控制要检查哪些日志文件,可以在几个方面有所帮助:
-
如果在相同位置保留了大量的日志文件(如日志轮转的情况——稍后将有更多介绍),那么我们可以控制在处理文件时可以回溯多远。
-
在实时用例中,例如来自制造线设备的日志事件,如果日志事件没有在特定时间范围内被捕获,那么该日志事件就变得冗余,因为什么也不能做。那么为什么还要浪费时间处理实际上已经过时的日志事件呢?
-
如果可用的计算能力较小,并且有大量的日志需要追回,你可以创建一个条件,即你永远不会追赶上当前生成的事件。限制回溯文件处理可以回溯多远,可以降低这种场景的风险。
需要仔细考虑这种配置的使用,以及检查新文件的频率。如果刷新间隔长于limit_recently_modified属性,那么当新文件被识别时,它们可能已经超出了限制的时间范围。
3.2.7 在操作中用分隔列表替换通配符
备份配置文件Chapter3/Fluentd/basic-file-read2.conf。将basic-file.txt复制到名为basic-file.log的文件中,然后再次复制该文件,以便这个副本被称为basic-file.out。修改 Fluentd 配置中的路径,以便通配符不在路径中,并使用逗号表示法添加.txt 和.out 文件。
使用我们之前使用的命令运行 Fluentd 和模拟器
-
groovy LogSimulator.groovy ./Chapter3/SimulatorConfig/basic-log-file.properties ./TestData/source.txt -
fluentd -c Chapter3/Fluentd/basic-file-read2.conf
用分隔列表替换通配符的解决方案
配置文件的更改应导致属性看起来像Chapter3/Fluentd/basic-file-read2-Answer.conf.。与通配符运行一样,输出将包含两个日志文件(你可以从pos_file中看到,也可以通过查看控制台输出)。但额外的第三个文件没有被处理。
3.2.8 处理日志轮转
日志轮转是一种常见的解决方案,允许收集大量日志,同时避免日志文件变得过大,难以处理或无限消耗空间。日志轮转还简化了清除旧内容的过程,而不是修剪文件;你只需删除最旧的日志文件。
tail 输入插件可以处理日志轮转。默认的方法是定义配置,以便路径明确标识轮转中的主文件(例如,path ./Chapter3/structured-rolling-log.0.log)。通过排除包含日志的文件夹中的通配符,轮转意味着当文件轮转时,它们不会被文件夹重新扫描选中。因此,我们添加了rotate_wait属性。此属性规定了一个时间段,在此期间,当前文件(将轮转至./Chapter3/structured-rolling-log.1.log)继续被读取。这是必要的,因为日志写入者可能在新文件创建之前还没有完成将内容刷新到第一个文件,这可能导致错过旧文件的最终内容。因此,了解写入者可能需要多长时间完成是很重要的;如果不知道,你需要留出足够的时间,以便日志不会被生成得太快,以至于你永远无法赶上。我们已经创建了一个设置来展示这种行为。列表 3.2 显示了输入配置。
Rotate_wait: 建议的配置
每个案例都是不同的,并且取决于日志写入机制的工作方式。对于后端服务器,我们通常将 30 秒视为合理的权衡。这是基于不希望日志落后太多,因为追赶可能会在工作负载中产生峰值(因为我们正在追赶活动服务器上的日志事件),并且如果我们遇到节点问题,有很大机会我们会捕捉到灾难性事件之前的事件。
列表 3.2 Chapter3/Fluentd/rotating-file-read.conf
<source>
@type tail
path ./Chapter3/structured-rolling-log.0.log ❶
rotate_wait = 15s ❷
read_lines_limit 5
tag simpleFile
pos_file ./Chapter3/rotating-file-read.pos_file
read_from_head true ❸
<parse>
@type none
</parse>
</source>
❶ 注意没有通配符,尽管我们也可以使用 ./*/structured-rolling-log.0.log,只要其他章节文件夹仍然干净。
❷ 我们使用 3.2.5 节中描述的符号表示法,以合理的时间间隔表示我们的轮转控制。
❸ 确保我们从文件的开头读取。然而,这将是当前轮转的开始。
我们可以从所有章节文件夹下载到的文件夹中运行此配置的命令行。
fluentd -c ./Chapter3/Fluentd/rotating-file-read.conf
在第二个控制台中,运行以下命令:
groovy LogSimulator.groovy ./Chapter3/SimulatorConfig/jul-log-file.properties ./TestData/medium-source.txt
LogSimulator 使用配置利用了标准 Java 实用工具日志框架(Java 核心的一部分)来提供日志轮换行为。Java 的日志工作方式类似于广泛的日志框架(日志框架在第四部分中进一步探讨)。模拟器已被配置为多次遍历数据集。为了便于观察这种行为,模拟器为文件中的每一行添加了一个行计数器。迭代计数器被添加到每条消息的前面。因此,如果您跟踪输出,您将看到所有行都按照从 Fluentd 输出的正确顺序排列。如果您足够快,您还会在控制台上观察到这样的日志消息:
2020-04-24 16:29:42.966831500 +0100 simpleFile: {"message":"2020-04-24--16:29:42 INFO com.demo (36-1) Heres a picture of me with REM. Thats me in the corner"}
2020-04-24 16:29:42 +0100 [info]: #0 detected rotation of ./Chapter3/structured-rolling-log.0.log; waiting 15 seconds
2020-04-24 16:29:42 +0100 [info]: #0 following tail of ./Chapter3/structured-rolling-log.0.log
注意这些行中的第一个值(36-1);这分别反映了从 LogSimulator 配置中得到的行号和迭代号,行号是从源日志的行开始的,迭代号是我们将日志条目传递给 Fluentd 以供其捕获的次数。第一行剩余的部分反映了日志消息。接下来的两行告诉我们 Fluentd 已经意识到轮换文件的变化,但在确保它从新的领先文件中读取之前,它将继续监控当前文件以查找任何需要刷新的最终内容。
警告:如果 Fluentd 必须重新启动,但应用程序继续运行,并且日志在 Fluentd 恢复之前轮换,那么使用read_from_head true将只会读取当前轮换开始的日志事件。Fluentd 停止和最新轮换之间的任何日志都不会被捕获。可以通过确保 Fluentd 在失败时自动重启,并使日志通常填充的时间超过 Fluentd 失败和恢复的时间来减轻这种影响。
如果需要在单个文件夹内使用多个属性使用通配符,则存在另一种方法:
-
refresh_interval——控制应该由通配符检测的文件列表更新的频率。 -
limit_recently_modified——这防止了由于通配符而被捕获的较旧日志文件在它们在定义的期间内未更改的情况下被使用。 -
pos_file_compaction_interval——这是每次访问位置跟踪器文件以整理其条目之间的间隔。根据配置,位置文件可能会积累条目,然后变得冗余。由于位置文件定期读取和更新,文件中的条目越少,对位置文件的处理就越高效。因此,最好定期进行一些清理工作。
这种方法依赖于日志文件定期写入;否则,日志将“中断”。一般来说,如果最后更新时间早于limit_recently_modified值,则日志文件将超出捕获范围。与之前一样,我们有一个包含输入的配置文件,如下所示。
列表 3.3 Chapter3/Fluentd/rotating-file-read-alternate.conf
<source>
@type tail
path ./Chapter3/structured-rolling-*.*.log ❶
read_lines_limit 5
refresh_interval 30s ❷
limit_recently_modified 5s ❸
pos_file_compaction_interval 15s ❹
tag simpleFile
pos_file ./Chapter3/rotating-file-read.pos_file
read_from_head true
<parse>
@type none
</parse>
</source>
❶ 路径现在有通配符。
❷ 如果需要设置日志,检查新日志文件的时间间隔,如 3.2.5 节中所述
❸ 为了避免意外读取旧日志,我们确定了一个必须更改文件的时间段。
❹ 清理界面
与前面的示例一样,Fluentd 实例可以通过以下方式启动
fluentd -c ./Chapter3/Fluentd/rotating-file-read-alternate.conf
对于要跟踪的日志事件,你可以使用上次相同的配置:
groovy LogSimulator.groovy ./Chapter3/SimulatorConfig/jul-log-file.properties ./TestData/medium-source.txt
警告:与处理日志轮换的主要方式一样,可能会丢失日志。此外,如果将 limit_recently_modified 属性设置得太短,新日志文件在当前文件进行最终刷新或文件句柄关闭时被选中,这可能会包括任何要写入存储的最终日志条目。这些最终文件操作可能会影响文件更改时间戳,触发 Fluentd 的文件扫描以检测旧日志文件是否在范围内。这可能会导致日志条目重叠,因为旧日志事件在较新的事件之后被收集。
3.3 自我监控
在前面的章节中,在日志轮换的场景中,可能会有丢失日志事件的外部机会。但是,Fluentd,就像任何好的应用程序一样,会记录其活动的事件。这意味着可以通过跟踪其日志事件来使用 Fluentd 监控其健康状况。除了 Fluentd 的日志之外,还有其他方法可以获得健康信息,我们将在后面看到。
3.3.1 HTTP 接口检查
Fluentd 提供了一个 HTTP 端点,它将提供有关实例设置的详细信息,如下面的列表所示。
列表 3.4 Chapter3/Fluentd/rotating-file-self-check.conf
<source>
@type monitor_agent
bind 0.0.0.0 ❶
port 24220 ❷
</source>
❶ 绑定的地址(即本地服务器)
❷ 用于此服务要使用的端口
使用提供的配置(fluentd -c Chapter3/Fluentd/ rotating-file-self-check.conf)运行 Fluentd,然后像第二章中那样启动 Postman。然后将地址配置为 0.0.0.0:24220/api/plugins.json。正如你在 bind 属性中看到的,与其他插件一样,这关系到主机的 DNS 或 IP,而 port 属性与 URL 的端口号相匹配。接口可以描述为 {bind}:{port}/api/plugins.json。与第二章中的操作不同,那里的操作是 POST,我们需要将操作设置为 GET。完成设置后,点击发送按钮,我们将看到返回的运行配置的 HTTP 表示,如图 3.1 所示。

图 3.1 使用包含 monitor_agent 插件的 Fluentd API 返回结果的 Postman 示例
正如图 3.1 所示,URL 和结果被突出显示。如果你希望结果使用标签分隔值(ltsv)来表示,只需从 URL 中省略 .json。当输出设置为 JSON 时,URL 也可以处理几个额外的参数。例如,向 URL 中添加 ?debug=1 将会提供一系列额外的状态信息(注意 debug 的值并不重要;参数的存在才是重要的)。可用于作为监控代理 URL 部分的所有参数的完整集合在表 3.2 中描述。
表 3.2 可用于调用 monitor_agent API 的 URI 参数
| URI 参数 | 描述 | 示例 |
|---|---|---|
| debug | 将获取额外的插件状态信息以包含在响应中。参数设置的值并不重要。 | ?debug=0 |
| with_ivars | 使用此参数足以使 instance_variables 属性包含在响应中。我们将在本书后面的插件开发部分讨论实例变量。 |
?with_ivars=false |
| with_config | 覆盖默认或显式设置的 include_config。提供的值必须是小写形式的 true;其他所有值都被视为 false。 |
?with_config=true |
| with_retry | 覆盖默认或显式设置的 include_config。提供的值必须是小写形式的 true 或 false;其他所有值都被视为 false。 |
?with_retry=true |
| tag | 这将返回的配置过滤,只返回与提供的标签名称链接的指令。 | ?tag=simpleFile |
| @id | 这将响应过滤到特定的指令。如果配置没有显式的 ID,则值将是任意的。 | ?id= in_monitor_agent |
| @type | 允许通过插件类型过滤结果。 | ?id=tail |
我们还可以通过在源指令中添加 tag 和 emit_interval 属性来获取 Fluentd 定期报告的插件的基本状态。如果我们使用 fluentd -c Chapter3/Fluentd/rotating-file-self-check2.conf(或者你可以尝试编辑并添加这些属性到之前的配置中)来运行配置,我们可以看到这些设置的影响。当 Fluentd 运行时,我们每 10 秒钟将开始看到一些状态信息,如下面的片段所示:
2020-05-01 17:10:04.041641100 +0100 self: {"plugin_id":"in_monitor_agent","plugin_category":"input","type":"monitor_agent","output_plugin":false,"retry_count":null}
由于信息被标记,我们可以将此流量引导到中央监控点,从而节省了需要编写 HTTP 轮询脚本的需求。通过将以下列表中的源包含到配置文件中(在 match 之前),信息会被发送到相同的输出。
列表 3.5 Chapter3/Fluentd/rotating-file-self-check2.conf
<source>
@type monitor_agent
bind 0.0.0.0
port 24220
@id in_monitor_agent
include_config true ❶
emit_interval 10s ❷
</source>
❶ 告诉监控代理在代理的输出中包含配置信息
❷ 监控代理应多久运行一次自我检查和输出
3.4 在日志事件上施加结构
到目前为止,我们只看了最简单的日志文件;然而,很少有日志文件是这样的。它们越是有结构,我们就能更好地为日志事件赋予更多意义,并且使事件在 Fluentd 或下游解决方案中变得更容易操作。这意味着将解析器的类型从 none 更改为使用提供的插件之一。值得注意的是,我们可以将解析器分为两大类:
-
产品特定解析器 --有些产品被广泛使用,以至于为它们专门开发了解析器,而不是使用具有详细配置的通用解析器。Apache 和 Nginx 就是这样的例子。这些解析器的优点是通常配置更简单,性能更高,因为代码被优化以处理特定的日志结构。
-
通用解析器——这些可以根据以下任一进行分组
-
支持特定类型的文件表示法(例如,CSV,LTSV,将在稍后讨论)
-
使用高度可配置的解析器技术(例如,Grok,正则表达式)
这些解析器高度可配置,但同时也更加复杂。解析器的可配置性和灵活性越高,对每个事件的解析效率就越低。
-
默认情况下,有一系列解析器覆盖这些类别。除了透传解析器(称为 none)之外,其他解析器包括 CSV 和 JSON,以及针对网络服务器监控文件的特定解析器。这些与社区(开源)提供的其他解析器相辅相成。第 3.4.1 节描述了核心解析器,它们的工作原理以及它们何时可以提供帮助。第 3.4.2 节继续介绍一些值得了解的社区提供的解析器;这些并不反映所有可能的解析器的总和,但我们认为这些是值得了解的。
在审查了不同的选项后,我们将应用最常用的解析器之一——正则表达式——来处理日志事件。
3.4.1 标准解析器
apache2
Apache 和 Nginx 是目前生产中最主要的两个网络服务器,Apache 自 1990 年代中期以来一直可用。在企业内部应用监控的过程中,你很可能遇到 Apache 服务器,即使它被包含在更大的产品中。这个解析器和 Nginx 解析器被设计用来处理记录请求和响应的标准网络服务器日志。记录的细节包括以下内容:
-
主机
-
用户
-
方法(HTTP POST,GET 等)
-
URI
-
HTTP 请求和响应代码
-
负载大小
-
引用者
-
代理
apache_error
除了核心的 Apache 日志文件外,我们还需要捕获与 CGI 脚本等相关的单独错误以及诊断和调试信息。捕获的信息包括
-
级别(例如,警告,错误)
-
PID:进程标识符
-
与错误关联的客户端(例如,浏览器,应用程序)
-
错误信息
Nginx
此解析器处理捕获 HTTP 调用的标准 Nginx 访问日志。在插件的核心中,它通过应用正则表达式来捕获消息元素。这意味着修改 Nginx 配置将需要更改此解析器以进行正则表达式处理,并相应地调整标准正则表达式。捕获的属性包括
-
远程地址—远程地址
-
用户—远程用户
-
方法—HTTP 动词 post、get 等。
-
路径—Nginx 正在处理的 URL
-
代码—HTTP 状态码
-
大小—缓冲区大小
-
引用者—在调用被转接时提供的引用者身份
-
代理—通常是浏览器类型
-
Http_x_forwarded_for—如果发生了转发,记录转发步骤的 HTTP 头信息由该元素持有。
注意:有关 Nginx 日志的更多信息,可以从mng.bz/1joX获取
CSV
CSV 解析器是一个快速解析器,默认使用逗号作为字段分隔符。解析器默认通过使用 Ruby 自己的 CSV 处理器(见mng.bz/J12o)来工作。或者,可以通过设置parser_type属性为fast来使用一个优化的快速解析器(该解析器限制为识别引号的使用,以便分隔符可以像平常一样使用,并且多个引号作为转义模式[例如,"""])。
一个优化的字符串解析器,用于将意义应用于 CSV 字符串,将始终优于通过执行我们的字符串处理或使用正则表达式解析器来应用意义。
然后,keys属性接受一个字段名称列表。time_key属性标识了哪些键用作日志事件的时间戳。分隔符可以通过分隔符属性从逗号更改为其他内容。
以下示例展示了使用优化选项的 CSV 解析器,而不是默认选项:
<parse>
@type csv
keys message, trans_id, time, host
time_key time
parser_type fast
</parse>
当应用于日志条目时,"my quoted message, to you", 124, 2020/04/15 16:59:04, 192.168.0.1将产生一个内部表示为
time: 1586966344
record:
{
“message” : “my quoted message, to you”,
“trans_id” : “124”,
“host” : “192.168.0.1”
}
JSON
这将接收到的日志事件作为 JSON 有效载荷处理。将日志事件作为 JSON 结构处理以帮助理解,而不在日志文件大小上产生大量开销的趋势正在出现。这种趋势甚至在 Fluentd 中也有所体现;如您所记得,Fluentd 将日志事件视为 JSON 对象。
它将寻找一个名为time的根元素,将其作为日志事件的时间戳应用。与日志事件关联的标签可以在根元素中找到,作为tag。默认情况下,嵌套的 JSON 结构不会被处理。解析器可以轻松处理以下结构,尽管需要额外的注意来使nested1在以下 JSON 片段中作为 JSON 处理:
{
“time” : “”,
“tag” : “myAppTag.Source1”
“field1” : “blah”,
“field2” : “more blah”,
“fieldNested” :
{
"nested1": "nested blah"
}
"fieldn": "enough of the blah"
}
如果需要,可以更改 JSON 解析器的实现以使用替代的 Ruby 实现。然而,默认解析器在多个基准测试中显示出是最高性能的。
TSV
这与 CSV 解析器非常相似。关键区别在于不支持转义和引号值。它假设默认分隔符为制表符字符(或转义序列 \t)。与 CSV 解析器一样,TSV 解析器可以更改分隔符(分隔符 属性)。它还期望 keys 和 time_key 属性定义 JSON 映射和时间戳。它还提供了一个额外的可选属性,称为 null_value_pattern,如果设置,则将找到的任何包含该值的值替换为 JSON 中的空字符串。例如,null_value_pattern '-' 意味着类似 afield\t123\t-\totherField 的行将导致
{
“field1” : ”afield”,
“field2” : “123”,
“field3” : “”,
“field4” :”otherField”
}
LTSV
标签制表符分隔值(LTSV)是制表符分隔值的一种变体。关键区别在于,每个制表符分隔值都由一个标签和一个冒号形式的标签分隔符前缀。这意味着值具有语义意义,值顺序并不重要。因此,不需要大量逗号分隔符来表示空值,正如你在 CSV 文件中看到的那样。可以说,在日志数据方面(例如,没有额外的引号、字符、花括号),这比 JSON 更高效和宽容,并且只预留了三个字符——制表符、标签分隔符和换行符。JSON 中不允许使用其他字符。例如,hostname:localhost/tip:127.0.0.1(注意示例中间的制表符用 /t 表示)有两个标签——hostname 和 ip。LTSV 的详细文档可以在 ltsv.org/ 找到,并包括指向有用工具的链接。
与 TSV 解析器一样,我们可以修改分隔符,以便可以使用除了制表符之外的字符。此外,可以使用 label_delimiter 属性来更改默认(冒号)标签分隔符。
MSGPACK
MessagePack 是一个开源标准和库,它内联描述了有效载荷,允许内容被移除或缩短。该格式在 Fluentd 的多个部分中得到支持(考虑到它是由负责 Fluentd 的同一团队开发的,这并不令人惊讶)。
它通过提供简短的字段和值描述符来实现。因此,可以通过删除冗余字符(如引号、空白等)来实现压缩。
此格式可用于 Fluentd 和 Fluent Bit 节点之间的通信,在跨越网络时尤其值得使用,尤其是在使用基于动态 HTTP 的压缩时(更多信息请参阅 msgpack.org/ 和 www.websiteoptimization.com/speed/tweak/compress/)。例如,JSON 片段 {"Fluentd": 1, "msgPackSupport":true} 将被缩减为十六进制
82 a7 46 6c 75 65 6e 74 64 01 ae 6d 73 67 50 61 63 6b 53 75 70 70 6f 72 74 c3
这占用了 26 字节,压缩率为 68%。
考虑到这种效率,当你在跨广域网、云提供商等共享日志事件时,考虑使用 msgpack 是值得的,因为这些网络将根据数据量收费,并且可能受到带宽和延迟问题的影响。
多行
不幸的是,并非所有日志都像处理堆栈跟踪和堆栈转储时那样优雅,即单行代表一个事件。多行插件通过定义多个正则表达式(Regex)来解决这个问题。正则表达式允许解析日志行并提取所需的日志元素。为了使插件正常工作,它需要一个正则表达式来识别多行日志事件的起始行(以及由此推断的日志条目的结束)。这个正则表达式使用属性名format_firstline指定。在此属性之后,可以定义多达 20 个额外的正则表达式格式,按数字顺序排列。
有关正则表达式的工作方式,请参阅以下内容,但配置遵循模式
@type multiline
format_firstline <regex expression>
format1 <regex expression>
format2 <regex expression>
...
formatN <regex expression>
</parse>
由于这两个插件之间的独特交互,多行插件作为解析器目前仅与tail输入插件一起提供。
如果应用程序的日志框架可以被配置为不产生多行输出(例如,Log4J 2 可以在其模式配置中支持这一点),那么至少值得考虑。这是因为多行解析器并不像大多数单行解析器那样高效。避免使用多行解析器的另一种策略是将像堆栈跟踪这样的日志事件写入单独的文件。单独的文件意味着我们可以在所有日志事件的一个子集上使用多行解析器。
无
这可以在必须定义解析器的地方使用(例如,在 tail 插件中)。但是不应用任何解析,整个日志行形成 Fluentd 记录。读取时间被设置为日志事件的值。
正则表达式
正则表达式解析器可能是可用的最强大的解析器选项,但作为结果,它也是最复杂的。在第 3.4.3 节中,我们将更深入地探讨正则表达式的使用。
系统日志
系统日志通常由操作系统和基础设施进程生成,但没有任何东西阻止应用程序使用该格式。syslog 条目的原始非官方结构被 IETF 正式化为RFC 3164 (tools.ietf.org/html/rfc3164)。然后,在 2009 年被RFC 5424 (tools.ietf.org/html/rfc5424)所取代。由于一些硬件可能需要多年才能更换,Fluentd 可以处理这两个标准。默认情况下,Fluentd 将假设原始标准,但您可以告诉 Fluentd 使用较新的标准,或者通过设置message_format属性为之一(rfc3164,rfc5424,auto)来使用负载来处理它。如果您知道将处理哪种格式,最好在配置中明确定义它。这样做可以消除在解析之前评估每个事件的负担。如果单个端点正在消耗这两种事件类型,您可能必须接受这种开销。
该插件有两个不同的算法用于处理事件--字符串处理逻辑和正则表达式(regexp)。目前,默认是regex,但将来这将被更改为默认使用字符串选项,这比regexp算法更快。如果您想强制使用算法,需要使用parser_type属性设置为regexp或字符串。
3.4.2 第三方解析器
除了核心解析器之外,还有一些第三方解析器。以下只是可能选项的一部分。然而,这些要么经过认证,要么下载量很大,因此它们很可能已经从广泛的使用中受益,并且由于代码是开源的,因此也受益于林纳斯定律。只要有足够的眼睛,所有错误都是浅显的--埃里克·雷蒙德)。
Fluentd 的多格式解析器插件
这尝试按照定义的顺序使用不同的格式模式以获得匹配。这可以从mng.bz/wnoO获取。
Fluentd 的 Grok 解析器
这使用基于 Grok 的方法从日志条目中提取详细信息。它包括多行支持。这可以从github.com/fluent/fluent-plugin-grok-parser获取。使用 Grok 解析器的优点是 Grok 被 Logstash 用作过滤和解析机制;因此,利用 Grok 预定义的模式并在 Logstash 和 Fluentd 之间切换相对容易。
3.4.3 将正则表达式解析器应用于复杂日志
如前所述,正则表达式或常规表达式解析器可能是最强大但最难使用的。在大多数正则表达式的应用中,结果通常是单个结果、子字符串或字符串出现次数的计数。然而,当涉及到 Fluentd 时,我们需要正则表达式产生多个值返回,例如设置日志事件时间,将有效负载分解为 JSON 事件体中元素的第一个级别。让我们取一个正则表达式并将其分解以突出基本要点。但在我们能够这样做之前,我们需要与一个实际的日志条目一起工作。让我们运行日志模拟器配置,使用以下命令
groovy LogSimulator.groovy Chapter3/SimulatorConfig/jul-log-file2.properties ./TestData/medium-source.txt
注意,这与上一个例子略有不同,因此我们可以在 Fluentd 中看到几个可能性。查看生成的输出文件(仍然是structuredrolling-log.0.log),现在发送的有效负载看起来如下
2020-04-30--20:10:52 INFO com.demo (6-1) {"log":"A clean house is the sign of a broken computer"}
在这一输出行中,我们可以看到应用的精确到秒的时间戳,然后是一个空格,接着是日志级别,更多空格,然后是包名,后面跟着编号方案,如前所述。最后,核心日志条目被包装为一个 JSON 结构。当我们解析消息时,我们需要移除该 JSON 符号,因为它不应该出现在我们想要的 JSON 结构中。
目标是在 Fluentd 中结束结构如下,这样我们就可以使用这些值进行未来的操作:
{
"level":"INFO",
"class":"com.demo",
"line":33,
"iteration":1,
"msg":"What is an astronauts favorite place on a computer? The Space bar!"
}
下面是正则表达式,下面添加了字符位置,以便精确引用每个部分:
(?<time>\S+)\s(?<level>[A-Z]*)\s*(?<class>\S+)[^\d]*(?<line>[\d]*)
1234567890123456789012345678901234567890123456789012345678901234567
0 1 2 3 4 5 6
\-(?<iteration>[\d]*)\)[\s]+\{"log":"(?<msg>.*(?="\}))
89012345678901234567890123456789012345678901234567890123
7 8 9 10 11 12
作为表达式的一部分,我们需要使用正则表达式定义文本组的能力。组的范围由开括号和闭括号定义(例如,字符 1 和 12)。为了将一些源文本分配给 JSON 元素,我们需要使用?<name>,其中 name 是将在 JSON 中出现的元素名称。在我们的情况下,这些值应该是level、class、line、iteration和msg。此外,我们还需要使用默认的time值捕获日志事件时间。例如,它可以在字符 2 和 8 之间看到,也可以在 16 和 23 之间看到。紧接着,我们可以使用正则表达式符号来描述要捕获的文本。为此,我们使用\S(字符 9 和 10),这意味着非空白字符;通过添加一个+(字符 11),我们声明这应该发生一次或多次。我们需要向解析器提供额外的配置,因为我们需要声明如何将消息的这一部分分解为特定的时间。
不符合模式的第一字符是时间和日志级别之间的空格--因此我们使用正则表达式表示单个空格(字符 13 和 14),然后开始日志级别的组。
表达式定义了日志级别,我们知道它将由一个或多个大写字母组成。方括号的使用表示值的选项(字符 24 和 28)。我们可以列出方括号内所有可能的字符,但为了可读性,我们选择在A和Z大写字母之间表示;A和Z之间的连字符(字符 27)表示这是一个范围。由于日志级别单词将是多个字符,我们使用星号来表示多个。这样就完成了日志级别。因此,在组外部,我们需要表示多个空白字符--即\s*(从 31 开始)。
我们可以遵循用于日期和时间的相同基本模式来处理路径或类字符串。这可以在字符 34 和 46 之间看到。为了将行传递到后续的有意义字符,我们定义了一个范围,使用正则表达式\d(字符 49 和 50),这意味着一个数字。然而,通过添加圆点(^),我们否定后续的值--在这种情况下,任何非数字字符。这意味着我们将跳过空白和开括号。
线和迭代的组是相同的--需要多个数字,并且两个组之间用连字符分隔。连字符被转义(字符 68),因为它在正则表达式中有特殊意义。我们可以看到在字符 95 处的花括号也有相同的转义字符。
在花括号开始日志细节之后,我们知道文本是什么,因此我们可以将其放入表达式(从字符 97 开始)。这强调了在表达式中精确性的重要性,因为未转义的代码字符将被视为字面量,因此将期望在正则表达式中找到它们出现的位置。如果正则表达式没有按预期找到字面量字符,那么正在处理的字符串将被拒绝。
下一个新正则表达式技巧是使用点(字符 112)。这表示任何字符;当与后面的星号结合时,表达式就变成了任何字符的多次出现。这构成了一个有趣的挑战--我们如何阻止关闭引号和括号被消费到msg组中?这是通过使用子组定义containing ?=(字符 115 和 116)来实现的。这描述了当你找到该序列时,向前查看后续序列,然后停止分配文本到当前组。因此,匹配表达式是`"}",但由于花括号在正则表达式中具有特殊意义,我们必须使用另一个反斜杠来转义它。这意味着此之后的任何字符都将被忽略。
在附录 B 中,我们包含了正则表达式的详细信息,以便您快速参考构建您的表达式。请注意,如果您在其他地方研究正则表达式,虽然实现上高度相似,但您会发现一些细微的差异。请记住,这里的正则表达式是使用 Ruby 语言实现的。
为了完成解析器配置,我们需要告诉解析器哪个命名的分组代表日期和时间(通常简称为 date-time 或 date-time-group [DTG])以及该日期和时间是如何表示的。在我们的例子中,日期和时间可以使用 %Y-%m-%d--%T 的模式来表示。由于时间元素是标准的,我们可以使用附录 B 中描述的短路格式(%T)。最后,让我们将这些信息拼凑在一起,并定义解析器属性(列表 3.6)。
正则表达式处理是一个丰富且复杂的特性
有关这个主题的书籍有很多,还有更多有专门章节的书籍。在这里,我们只是触及了表面,只提供了足够的信息来帮助你理解它在 Fluentd 上下文中的工作方式。投资一本书来帮助可能会有所帮助。此链接也可能有所帮助:www.rubyguides.com/2015/06/ruby-regex/。
注意:当解析器表达式在处理过程中失败时,Fluentd 将生成一个警告日志条目。
列表 3.6 Chapter3/Fluentd/rotating-file-read-regex.conf--parse extract
<parse>
@type regexp ❶
Expression /(?<time>\S+)\s(?<level>[A-Z]*)
\s*(?<class>\S+)[^\d]*(?<line>[\d]*)\-(?<iteration>[\d]*)\)
[\s]+\{"log":"(?<msg>.*(?="\}))/ ❷
time_format %Y-%m-%d--%T ❸
time_key time ❹
</parse>
❶ 我们在这里将解析器类型更改为正则表达式。
❷ 表达式需要在正斜杠之间提供。我们将在后面看到,可以在尾随斜杠之后添加额外的控制。
❸ time_format 允许我们更简洁地定义日期时间格式。
❹ time_key 用于告诉 Fluentd 使用哪个提取值作为时间戳。默认情况下,它将使用名为 time 的值,所以从技术上讲这是多余的。
这个配置可以通过重新启动模拟器来运行,就像我们为了获取示例值所做的那样,然后使用配置启动 Fluentd
fluentd -c Chapter3/Fluentd/rotating-file-read-regex.conf
由于 Fluentd 将处理后的事件流输出到控制台,你会看到如下条目:
2020-04-30 23:29:47.000000000 +0100 simpleFile:
{"level":"","class":"INFO","line":"50","iteration":"1","msg":
"The truth is out there. Anybody got the URL"}
Fluentd 打印到控制台的每一行都将包含日志事件的日期和时间、时区偏移、事件标签和有效载荷,以正确结构的 JSON 格式,省略了时间。注意,所有的纳秒值都是 0。这是因为我们给 Fluentd 提供了一个没有纳秒精度的日志时间;因此,时间戳的这一部分被留在了 0。
更重要的是,你会注意到所有的 JSON 值都被引号包围,因此它们将被视为字符串。这可能不是问题。但既然已经走到这一步,不正确地定义数据类型就太遗憾了。这可能会使下游活动,如推断额外含义,更加有效。定义非字符串数据类型很简单。我们需要在解析器结构中添加属性类型,它包含一个以逗号分隔的列表,每个定义的值都按照 name:type 的格式描述。在我们的用例中,我们希望添加 types line:integer,iteration:integer。支持的类型列表如下
-
string—可以显式定义,但默认类型
-
bool—布尔值
-
integer—表示任何整数(即,没有小数位)
-
浮点数—表示任何十进制数
-
时间—将值转换为 Fluentd 内部表示时间的方式。我们可以扩展此描述时间应该如何转换。例如:
-
日期:时间:%d/%b/%Y:%H:%M—定义表示的格式
-
日期:时间:Unix 时间—从 1970 年 1 月 1 日起的整数格式时间
-
日期:时间:浮点数—相同的纪元点,但数字是浮点数
-
-
数组—相同类型值的序列(例如,所有字符串,所有整数)
处理数组需要值之间有一个分隔符来分隔每个值。默认的分隔符是逗号,但可以通过添加一个冒号和分隔符字符来更改。例如,逗号分隔的数组可以定义为 myList:array。但如果我想用哈希替换分隔符,那么表达式将是 myList:array:#。
最后对 JSON 的操作涉及我们是否希望将日期时间戳包含在 JSON 中;毕竟,它已经在日志事件的主体中了。这可以通过向解析器属性中添加 keep_time_key true 来轻松完成。
我们可以添加所描述的更改(尽管提供的配置已经准备好了这些值但被注释掉了,所以您只需取消注释它们并像以前一样重新运行模拟器和 Fluentd)。这些更改的结果是日志条目将如下所示:
2020-05-01 00:14:25.000000000 +0100 simpleFile:
{"time":"2020-05-01--00:14:25","level":"","class":"INFO","line":75,"iteration":1,"msg":
"I started a band called 999 megabytes we still havent gotten a gig"}
如果现在查看 JSON 主体,我们的数值元素不再带引号,时间戳出现在 JSON 负载中。
评估/检查正则表达式
正则表达式表达式可能具有挑战性;我们最不希望做的就是在整个 Fluentd 环境中运行日志来确定表达式是否完整。为此,Fluentd UI 配置支持 Regex 验证。
此外,还有一个名为 Fluentular 的免费网络工具(fluentular.herokuapp.com/),它允许您开发和测试表达式。
一些 IDE,例如微软的 Visual Studio Code,具有正则表达式工具来帮助可视化正在构建的正则表达式--例如,Regexp Explain (mng.bz/q2YA)。完成的正则表达式可以在以下图中看到。

使用 Regexp Explain 在 Visual Code 中可视化正则表达式(Regex),以帮助您了解解析器应该如何处理日志事件。这也可以通过 regexper.com/ 完成。
如果您仔细观察,您会注意到 ?<元素名称> 缺失;然而,由于核心部分已经被分组,因此很容易看到这些部分需要添加的位置。如果使用分组,则将表达式导入 Fluentd 并添加元素变得容易。
3.4.4 将解析器配置投入实际应用
本练习旨在让您与解析器一起工作。模拟器配置 Chapter3/SimulatorConfig/jul-log-file2-exercise.properties 与之前的示例有所不同。复制用于说明解析器的 Fluentd 配置文件(Chapter3/Fluentd/rotating-file-read-regex.conf)。然后,修改解析器表达式,以确保所有输入值都作为日志事件的 JSON 元素正确表示,而不是像 Fluentd 默认那样仅作为有效载荷。可以使用命令运行日志模拟器配置的变体
groovy LogSimulator.groovy Chapter3/SimulatorConfig/jul-log-file2-exercise.properties ./TestData/medium-source.txt
运行修订后的 Fluentd 配置,并确定您的更改是否有效。
答案
解析器配置应类似于以下列表中所示的代码。
列表 3.7 Chapter3/ExerciseResults/rotating-file-read-regex-Answer.conf
<parse>
@type regexp
Expression /(?<time>\S+)\s(?<level>
[A-Z]*)\s*(?<class>\S+)[^\d]*(?<iteration>[\d]*)\-(?<line>[\d]*)\][\s]+
\{"event":"(?<msg>.*(?="\,))/
time_format %Y-%m-%d--%T
time_key time
</parse>
完整的配置文件在 Chapter3/ExerciseResults/rotating-file-read-regex-Answer.conf 中提供。
摘要
-
可以使用
dry_run选项验证 Fluentd 的配置,而无需启动实际的部署。 -
存储在日志文件中的日志事件格式可能范围很广,从非结构化到完全结构化。Fluentd 消费来自文件的日志事件的能力使其能够适应这种多样性水平。
-
Fluentd 可以处理日志文件复杂性——应用于日志文件的过程可能很复杂,例如处理日志轮转和跟踪 Fluentd 停止和启动后如何恢复的位置。
-
由于 Fluentd 的插件模型以及广泛的社区和供应商支持,可以处理广泛的日志事件源。
-
输入插件可以使用一系列现成的解析器(例如,CSV、Regex、LTSV、Web 服务器标准文件)来为日志事件应用结构和意义。
-
开发正则表达式(regex)配置可能具有挑战性,但存在工具可以简化这一挑战(例如,专为 Fluentd 设计的 Fluentular;Regexp Explain;以及其他工具)。
-
除了使用插件监控其他系统外,Fluentd 还可以配置为通过启用和使用 HTTP 端点来检查 Fluentd 的方法提供被监控的手段。
4 使用 Fluentd 输出日志事件
本章涵盖
-
使用文件、MongoDB 和 Slack 的输出插件
-
使用 Fluentd 应用不同的缓冲选项
-
查看缓冲的优势
-
处理缓冲过载和其他缓冲风险
-
添加格式化器以结构化日志事件
第三章演示了如何捕获日志事件以及辅助插件(如解析器)如何发挥作用。但是,如果我们不能对数据进行有意义的操作,例如将事件交付到格式化后的端点以便使用——例如,将事件存储在日志分析引擎中或将消息发送到操作(Ops)团队进行调查,那么捕获数据就只有价值。本章将展示 Fluentd 如何使我们能够做到这一点。我们将探讨 Fluentd 输出插件如何从文件中使用,以及 Fluentd 如何与 MongoDB 和 Slack 的协作/社交工具配合进行快速通知。
本章将继续使用 LogSimulator,我们还将使用一些其他工具,例如 MongoDB 和 Slack。与之前一样,完整的配置可在 Manning 的下载包中找到,或通过 GitHub 仓库获取,这样我们就可以专注于相关插件(s)的配置。MongoDB 和 Slack 的安装步骤在附录 A 中介绍。
4.1 文件输出插件
与 tail(文件输入)插件相比,我们不太可能使用文件输出插件,因为我们通常希望输出到允许我们查询、分析和可视化事件的工具。当然,对于生产环境中确实需要文件输出的情况,这将是最佳选择之一。然而,它是一个很好的起点,因为我们可以轻松地看到各种插件(如解析器和过滤器)的结果和影响。将重要事件记录到文件也便于在必要时轻松存档日志事件以供将来参考(例如,审计日志事件以支持法律要求)。因此,在继续探讨更复杂的输出之前,我们将查看文件输出。
在文件输出(以及由此扩展的任何涉及直接或间接写入物理存储的输出)中,我们需要考虑几个因素:
-
我们可以在文件系统中写入哪里,这取决于存储容量和权限?
-
那个位置是否有足够的容量(分配的容量和物理容量)?
-
物理硬件可以提供多少 I/O 吞吐量?
-
数据访问是否存在延迟(NAS 和 SAN 设备通过网络访问)?
虽然基础设施性能不太可能影响开发工作,但在预生产(例如,性能测试环境)和生产环境中却极为重要。值得注意的是,设备性能对于文件插件至关重要。其他输出插件可能正在使用包括优化 I/O(例如,数据库缓存、分配文件空间的优化)逻辑的服务。使用输出插件,我们可能已经整合了多个日志事件的来源。因此,我们可能会得到一个配置,其中 Fluentd 将所有输入写入一个文件或位置。可以通过缓冲区(正如我们很快将看到的)和缓存来减轻物理性能方面的考虑。
4.1.1 基本文件输出
让我们从 Fluentd 的一个相对基本的配置开始。在前几章的示例中,我们只看到了写入控制台的内容。现在,而不是控制台,我们应该简单地将所有内容推送到一个文件。为此,我们需要在配置中添加一个新的 match 指令,但我们将继续使用文件源来处理日志事件。
为了说明输出插件可以在配置中处理多个输入,我们除了包含前一章中展示的自监控源配置外,还包含了一个日志文件源。为了控制 Fluentd 的 self_monitor 生成的日志事件的频率,我们可以定义另一个属性,称为 emit_interval,它接受一个持续时间值——例如,10s(10 秒)。emit_interval 提供的值是 Fluentd 生成日志事件之间的时间。自监控可以包括诸如已处理的事件数量、管理的工人数等详细信息。
至少,文件输出插件只需要定义 type 属性,并使用 path 属性指定输出位置。在以下列表中,我们可以看到我们的 Chapter4/Fluentd/rotating-file-read-file-out.conf 文件的相关部分。这个配置的结果可能会让你感到惊讶,但让我们看看会发生什么。
列表 4.1 Chapter4/Fluentd/rotating-file-read-file-out.conf—match extract
<source>
@type monitor_agent
bind 0.0.0.0
port 24220
@id in_monitor_agent
include_config true
tag self
emit_interval 10s
</source>
<match *>
@type file ❶
path ./Chapter4/fluentd-file-output ❷
</match>
❶ 将插件类型更改为文件
❷ 要写入的文件位置
使用此新的 match 指令的结果,如果使用以下命令运行 LogSimulator 和 Fluentd,就可以看到:
-
fluentd -c ./Chapter4/Fluentd/rotating-file-read-file-out.conf -
groovy LogSimulator.groovy ./Chapter4/SimulatorConfig/jul-log-output2.properties./TestData/medium-source.txt
容易的假设是所有内容都写入一个名为fluentd-file-output的文件。然而,实际上发生的是使用路径的最后部分作为其名称创建了一个文件夹(即,fluentd-file-output),您将在该文件夹中看到两个文件。文件将以半随机名称出现(以区分不同的缓冲区文件),并且有一个具有相同基本名称的元数据文件。Fluentd 所做的是隐式地使用缓冲机制。在输出插件中使用默认选项的缓冲区并不罕见;有些插件放弃了缓冲区的使用——例如,stdout插件。
4.1.2 缓冲区的基本原理
缓冲区,如您可能从第一章回忆起来,是 Fluentd 的一个辅助插件。输出插件需要意识到它们可能对 I/O 性能产生的影响。因此,大多数输出插件都使用一个可以同步或异步行为的缓冲区插件。
同步方法意味着当日志事件收集到块中时,一旦块满了,它就被写入存储。异步方法利用一个额外的队列阶段。与输出通道交互的队列阶段在单独的线程中执行,因此块填充不应受到任何 I/O 性能因素的影响。
之前的例子没有明确定义缓冲区;我们看到输出插件应用了默认的文件缓冲区。当你意识到文件输出插件支持使用 gzip 压缩输出文件的能力时,这更有意义,压缩的内容越多,效果越好。
在图 4.1 中,我们已对步骤进行了编号。正如箭头的不同路径所示,生命周期中的步骤可以被绕过。所有日志事件都从步骤 1 开始,但如果未使用缓冲区,则过程立即移动到步骤 5,在那里发生物理 I/O 操作,然后我们继续到步骤 6。如果发生错误,步骤 6 可以将逻辑发送回前面的步骤再次尝试。这非常依赖于插件实现,但这是数据库插件等插件中常见的做法。如果重试失败或插件不支持该概念,一些插件支持指定二级插件的想法。

图 4.1 日志事件通过输出生命周期(例如,斜体步骤仅在出现问题时使用)
二级插件是另一个可以被调用的输出插件(步骤 7)。通常,二级插件会尽可能简单,以最小化出现问题的可能性,以便稍后可以恢复日志事件。例如,假设输出插件从 Fluentd 节点调用远程服务(例如,在不同的网络、单独的服务器集群,甚至数据中心)。在这种情况下,二级插件可以是一个简单的文件输出到本地存储设备。
注意:我们始终建议实现一个具有最少软件和基础设施依赖的二级输出。需要降级到二级插件强烈表明存在更广泛的问题。因此,它越简单,对其他因素的依赖性越少,输出就越不可能被中断。文件很好地支持这种方法。
如果已配置缓冲,则步骤 1 到 3 将会被执行。但接下来的操作将取决于缓冲是否为异步。如果是同步的,则过程将跳转到步骤 5,我们将遵循相同的步骤。对于异步缓冲,日志块将进入一个单独的过程,该过程管理一个待写入的块队列。步骤 4 代表缓冲异步操作。随着块的填充,它们被放入队列结构中,等待输出机制将每个块输出内容。这意味着要处理的下一个日志事件不会被步骤 5 及以后的 I/O 操作所阻塞。
理解 gzip 压缩
Gzip 是 GNU 对 ZLIB 压缩格式的实现,该格式由 IETF RFC 的 1950、1951 和 6713 定义。Zip 文件使用一种称为 Lempel-Ziv 编码(LZ77)的算法来压缩内容。简单来说,该算法通过寻找字符的重复模式来工作;当找到重复时,该字符串会被替换为对前一次出现的引用。因此,识别为重复的字符串越大,引用就越有效,从而提供更多的压缩——文件越大,找到重复的可能性就越高。
默认情况下,Fluentd 提供以下缓冲类型:
-
内存
-
文件
如您所意识到的那样,路径被用作文件夹位置来存储其缓冲内容,并包含内容和元数据文件。使用文件 I/O 作为缓冲在存储设备方面不会带来很大的性能提升,除非您建立了一个 RAM 磁盘(也称为 RAM 驱动器)。基于文件的缓冲仍然提供一些好处;文件的使用方式被优化(保持文件打开等)。它还充当一个临时区域,在应用压缩之前积累内容(如前所述,参与 zip 压缩的数据越多,可能的压缩就越大)。此外,由于某种形式的进程或硬件故障,记录的内容不会丢失,并且当服务器和/或 Fluentd 重新启动时,缓冲可以重新恢复。
注意:RAM 驱动通过为存储分配一块内存,然后告诉操作系统的文件系统它是一个额外的存储设备。使用此存储的应用程序认为它们正在写入一个像磁盘这样的物理设备,但实际上内容是写入内存的。更多信息可以在 www.techopedia.com/definition/2801/ram-disk 找到。
由于许多插件默认或明确地涉及缓冲区,我们应该看看如何开始配置缓冲区行为。我们知道当事件从缓冲区移动到输出目标时,此类动作发生的频率,以及这些配置如何影响性能。图 4.1 中所示的生命周期图提供了配置可能性的线索。
4.1.3 块和缓冲区控制
如图 4.1 所示,缓冲区的核心结构是块的概念。我们配置块的方式,除了同步和异步之外,还会影响性能。块可以通过分配存储空间(允许使用连续的内存或磁盘的一部分)或通过时间段来控制。例如,一个时间段内的所有事件都进入一个块,或者一个块将继续填充事件,直到达到特定的日志事件数量或块达到一定的大小。如果需要为 I/O 提供连接重试,这种分离 I/O 和块填充是有益的,例如在共享服务或网络远程服务(如数据库)的情况下。
在两种方法中,通过配置可以设置属性,使得日志事件不会因为阈值从未完全满足而滞留在缓冲区中。采用哪种方法将受你的日志事件源的行为以及性能与可用资源(例如,内存)之间的权衡以及可接受的延迟量(在日志事件向下移动时)的影响。
个人而言,我倾向于使用大小约束,这提供了可预测的系统行为;这可能反映了我 Java 背景和偏好,不希望过度调整虚拟机。
表 4.1 显示了缓冲区上大多数可能的控制。当控制可以有一些微妙的行为时,我们包括了更详细的说明。
表 4.1 缓冲区配置控制
| 属性 | 描述 |
|---|---|
timekey |
这是指每个块将负责保持的秒数(默认为 1 天)。log_event 的 time 属性然后确定将事件添加到哪个块。例如,如果我们的 timekey 设置为 300(秒)且块从小时开始,那么当 10:00:01 标记的事件到达,并且每 30 秒到达更多事件时,第一个块将保留额外的 9 个事件。下一个块将保留 10:05:00 后开始到达的事件,因此下一个事件将是 10:05:01。 |
timekey |
如果在 10:05 之前有额外的乱序事件到达(例如,带有时间戳 10:03:15 和 10:03:55),但它们直到 10:04:31 才到达,那么它们仍然会被添加到第一个块中。这种行为可以通过 timekey_wait 属性进一步修改。 |
timekey_wait |
这是块存储周期结束后,块写入之前需要等待的秒数。默认为 60 秒。扩展我们的 timekey 示例,如果此值设置为 60s(60 秒),则该块将在 10:06 之前保留在内存中,然后才会刷新。如果收到的时间戳为 10:04:49 的另一个事件在 10:05:21,这将进入我们的第一个块,而不是覆盖接收时间的块。 |
chunk_limit_size |
这定义了块的最大大小,默认为内存中的 8 MB 和文件缓冲区中的 256 MB。提高此阈值的可能性很小,但您可以考虑将其降低以限制容器的最大占用空间或物联网设备的限制。请记住,您可以操作多个块。 |
chunk_limit_records |
这定义了单个块中日志事件的最多数量。如果日志事件的大小波动很大,则需要考虑这一点。大量的大型日志可能会创建一个非常大的块,从而产生内存耗尽和块写入持续时间变化的风险。 |
total_limit_size |
这是所有块允许的存储限制,在新事件接收之前,将丢弃错误事件。默认为内存中的 512 MB 和文件中的 64 GB。 |
chunk_full_阈值` |
一旦缓冲区容量的百分比超过此值,块就被视为已满,并移动到 I/O 阶段。默认为 0.95。如果日志事件相对于分配的内存非常大,您可以考虑降低此阈值以确保更可预测的性能,尤其是如果您限制了队列大小。 |
queued_chunks_limit_size |
这定义了队列中等待按需持久化的块的数量。理想情况下,这个值不应大于 flush_thread_count。默认值是 1。 |
compress |
仅接受 text(默认)或 gzip 的值。当设置为 gzip 时,则应用压缩。如果引入了其他压缩机制,则可用的选项将扩展。 |
flush_at_shutdown |
这告诉缓冲区是否应在允许 Fluentd 优雅地关闭之前将所有内容写入输出。对于内存缓冲区,默认为 true,但对于文件,默认为 false,因为内容可以在启动时恢复。鉴于您可能不知道 Fluentd 将何时重新启动并处理缓存的事件(如果可以的话),我们建议在大多数情况下将其设置为 true。 |
flush_interval |
这是一个持续时间,定义了缓冲内容应该写入输出存储机制的频率。这意味着我们可以配置基于体积或时间间隔的行为。默认为 60 秒(60s)。 |
| flush_mode | 接受的值是
-
default—如果定义了块键,则使用lazy,否则使用interval -
lazy—每次timekey时刷新/写入块一次。 -
interval—通过flush_interval指定的时间间隔刷新/写入块。 -
immediate—在事件被追加到块中后立即刷新/写入块。
|
flush_thread_count |
用于写入块的线程数量。大于 1 的数字将创建并行线程——这取决于输出类型,可能不是所希望的。例如,如果数据库连接池可以处理多个连接,那么超过 1 是值得考虑的。但在文件上超过 1 可能会造成竞争或写入冲突。默认为 1。 |
|---|---|
flush_thread_interval |
清洗线程在检查是否需要刷新之前应该休眠的时间长度。以浮点数格式表示秒数,默认为 1。 |
delayed_commit_timeout |
当使用异步 I/O 时,我们需要设置一个最大时间,允许线程运行,在我们认为它必须遇到错误之前。如果这个时间超过了(默认 60 秒),则停止线程。这需要根据目标系统的响应性进行调整。例如,将大块数据写入远程数据库将比将小块数据写入本地文件系统花费更长的时间。 |
| overflow_action | 如果缓冲区的输入速度超过我们能够从缓冲区中写入内容的速度,我们将遇到溢出条件。此配置允许我们定义如何解决这个问题。选项有
-
throw_exception—抛出一个异常,该异常将作为BufferOverflowError出现在 Fluentd 日志中;这是默认设置。 -
block—阻止输入处理以允许写入事件。 -
interval—通过flush_interval按指定时间刷新/写入块。 -
drop_oldest_chunk—丢弃最旧的块数据以释放一个块供使用。 -
抛出异常在永远不会预期溢出场景时可能是可以接受的,并且潜在的日志事件丢失是一个值得承担的风险。但在更关键的区域,我们建议有意识地选择一个替代方案,例如
interval。
|
我们可以通过理解缓冲区行为(或使用准备好的配置)来修改配置。由于我们建议在关闭时刷新,我们应该将其设置为 true(flush_at_shutdown true)。由于我们希望快速看到更改的影响,让我们将最大记录数设置为 10(chunk_limit_records 10)并将刷新块的最大时间设置为 30 秒(flush_interval 30)。否则,如果缓冲区中有 1 到 9 个日志事件,如果源停止创建日志事件,它们将永远不会被刷新。最后,我们在配置中增加了一层额外的保护,通过强制执行缓冲区写入过程的时间超时。我们可以在以下列表中看到这一点。
列表 4.2 第四章/Fluentd/rotating-file-read-file-out2.conf—匹配提取
<match *>
@type file
path ./Chapter4/fluentd-file-output
<buffer>
flush_at_shutdown true ❶
delayed_commit_timeout 10
chunk_limit_records 10 ❷
flush_interval 30 ❸
</buffer>
</match>
❶ 默认情况下,文件缓冲区在关闭时不会刷新,因为停止 Fluentd 实例不会导致事件丢失。然而,在关闭时看到所有事件完成是可取的。存在这样的风险,即配置更改意味着文件缓冲区在重启时不会被选中,导致日志事件实际上处于悬而未决的状态。
❷ 由于我们理解日志内容并希望事情发生得非常快,我们将使用多个日志而不是控制块大小的能力,这间接影响事件从缓冲区移动到输出目标的速度。
❸ 由于自我监控事件的累积速度将比我们的文件源慢得多,因此强制按时间刷新也将确保我们可以以合理的频率看到这些事件通过输出。
要运行这个场景,让我们重置(删除structured-rolling-log.*和rotating-file-read.pos_file文件以及fluentd-file-output文件夹)并再次运行,在每个单独的 shell 中使用这些命令:
-
fluentd -c Chapter4/Fluentd/rotating-file-read-file-out2.conf -
groovy LogSimulator.groovy Chapter4/SimulatorConfig/jul-log-file2.properties ./TestData/medium-source.txt
一旦日志模拟器完成,不要关闭 Fluentd。我们会看到文件夹fluentd-file-output仍然被创建,并且像之前一样包含缓冲文件。但与此同时,我们还会看到以fluentd-file-output.<date>_<incrementing number>.log(例如,fluentd-file-output.20200505_12.log)命名的文件。打开这些文件中的任何一个,你会看到 10 行日志数据。你会注意到日志数据是以日期时间戳、标签名称,然后是有效负载体格式化的,反映了日志事件的标准化组成。如果你浏览这些文件,你会找到标签不是 simpleFile 而是 self 的事件。这反映了我们保留了源报告在自我监控上,并且与我们的 simpleFile 相匹配,simpleFile 正在跟踪旋转日志文件。
最后,优雅地关闭 Fluentd。在 Windows 中,在 shell 中这样做最简单:按一次CTRL-c(并且只按一次),然后对关闭提示响应 yes(在 Linux 中,可以使用中断事件)。一旦我们可以在控制台中看到 Fluentd 已关闭,查找最后一个日志文件并检查它。这涉及到一些时间因素,但如果检查最后一个文件,它很可能包含少于 10 条记录,因为缓冲区在关闭时将刷新到输出文件中任何日志事件。
缓冲区大小错误
如果你设置缓冲区小于单个日志事件,那么处理该日志事件将失败,出现如下错误
emit transaction failed: error_class=Fluent::Plugin::Buffer
::BufferChunkOverflowError error="a 250bytes record is larger than
buffer chunk limit size" location="C:/Ruby26-x64/lib/ruby/gems/2.6.0/
gems/fluentd-1.9.3-x64-mingw32/lib/fluent/plugin/buffer.rb:711:in `block
in write_step_by_step'"
4.1.4 重试和退避
缓冲区的使用还允许 Fluentd 提供重试机制。在出现如短暂网络中断等问题的情况下,我们可以告诉缓冲区在识别到问题时进行重试,而不是仅仅丢失日志事件。为了使重试工作而不产生新的问题,我们需要定义控制措施,告诉缓冲区在放弃数据之前重试多长时间或多少次。此外,我们还可以定义在重试之前等待多长时间。我们可以规定无限期重试(将retry_forever属性设置为 true),但我们建议非常谨慎地使用此类选项。
使用retry_type属性有两种方式来实现重试:通过固定间隔(periodic)或通过指数回退(exponential_backoff)。指数回退是默认模型,每次重试失败都会导致重试延迟加倍。例如,如果重试间隔是 1 秒,第二次重试将是 2 秒,第三次重试是 4 秒,依此类推。我们可以通过定义retry_wait(使用表示秒数的数值)来控制重试之间的初始或重复等待周期。例如,1 表示 1 秒,60 表示 1 分钟。
除非我们希望无限期地重试,否则我们需要提供一种方法来确定是否继续重试。对于周期性重试模型,我们可以通过数量或时间来控制。这是通过设置重试写入每个块的最大周期(retry_timeout)或最大重试尝试次数(retry_max_attempts)来实现的。
对于回退方法,我们可以规定回退次数(retry_exponential_backoff_base)或回退可以持续的最大时长,在停止之前(retry_max_interval)。
假设我们想要配置缓冲重试为从 3 秒开始的指数回退。在最多 10 次尝试的情况下,我们可能会达到近 26 分钟的重试间隔峰值。我们需要配置的配置属性包括
retry_exponential_backoff_base 3
retry_max_times 10
指数回退的重要之处在于确保你了解可能的总时间。一旦指数曲线开始,时间会迅速延长。在这个例子中,前 5 次重试将在一分钟内发生,但之后间隔会真正开始拉长。
4.1.5 将配置缓冲大小设置付诸实践
你被要求帮助团队更好地理解缓冲机制。有一个共识,即应该修改现有的配置以帮助实现这一点。复制配置文件 /Chapter4/Fluentd/rotating-file-read-file-out2.conf 并修改它,以便缓冲块配置基于 500 字节的尺寸(参见附录 B 了解如何表示存储大小)。
运行修改后的配置以展示对输出文件的影响。
在讨论过程中,出现的一个问题是如果输出插件受到间歇性网络问题的影响,我们有哪些选项可以防止任何日志信息的丢失?
答案
以下列表显示了将包含在结果中的缓冲区配置。
列表 4.3 Chapter4/ExerciseResults/rotating-file-read-file-out2-Answer.conf
<match *>
@type file
@id bufferedFileOut
path ./Chapter4/fluentd-file-output
<buffer> ❶
delayed_commit_timeout 10
flush_at_shutdown true
chunk_limit_size 500 ❷
flush_interval 30
</buffer>
</match>
❶ 注意,我们保留了延迟和刷新时间,所以如果缓冲区停止填充,它仍然会被强制输出。
❷ 基于大小的约束而不是基于时间的
完整的配置文件在Chapter4/ExerciseResults/rotating -file-read-file-out2-Answer.conf中提供。日志文件的变化率可能看起来不同,但内容将是相同的。为了解决减轻日志丢失风险的问题,可以应用几种选项:
-
在缓冲区上配置重试和退避参数,以便在丢失信息之前重试存储事件。
-
使用定义一个二级日志机制的能力,例如本地文件,这样事件就不会丢失。提供一种方法,以便在稍后日期将日志注入到 Kafka 流中。这甚至可以是一个额外的来源。
4.2 输出格式化选项
我们如何结构化日志事件的输出,与我们如何对输入应用结构一样重要。不出所料,格式化插件可以包含在输出插件中。有了格式化插件,合理地期望有几个预构建的格式化器。让我们看看通常遇到的“开箱即用”的格式化器;完整的格式化器集在附录 C 中详细说明。
4.2.1 输出文件
这可能是最简单的格式化器,并且已经被隐式使用。格式化器通过在time、tag和record值之间使用分隔符来工作。默认情况下,*分隔符*是一个制表符字符。这只能通过将属性分隔符的值更改为comma或space来更改,例如,delimiter comma。也可以通过基于布尔值的属性来控制输出哪些字段:
-
output_tag—这个属性接受真或假值来决定是否在行中包含标签(默认情况下,行中的第二个值)。 -
output_time—这个属性接受真或假来定义是否包含时间(默认情况下,行中的第一个值)。如果你已经在核心事件记录中包含了时间,你可能希望省略时间。 -
time_format—这个属性可以用来定义日期和时间在路径中使用的方式。如果未定义且设置了 timekey,则 timekey 将告知格式的结构。具体来说:-
0...60 秒则使用
'%Y%m%d%H%M%S' -
60...3600 秒则使用
'%Y%m%d%H%M' -
3600...86400 秒则使用
'%Y%m%d%H'
-
注意:如果格式化器无法识别设置的属性值,它将忽略提供的值并使用默认值。
4.2.2 JSON
这个格式化器将日志事件记录视为单行上的 JSON 有效负载。与事件关联的时间戳和标签被丢弃。
4.2.3 LTSV
与out_file格式化程序和解析器一样,可以使用*delimiter*(每个标签值)和label_delimiter来更改限制器,以分隔值和标签。例如,如果将分隔符设置为delimiter ;并且设置了label_delimiter =,那么如果记录表示为{"my1stValue":" blah", "secondValue": "more blah", "thirdValue": "you guessed – blah",输出将变为my1st Value=blah; secondValue= more blah; thirdValue=you guessed – blah.。
由于值是标签值,因此减少了对按行分隔记录的需求,因此可以通过将add_newline false(默认值为 true)关闭来停止使用新行来分隔每个记录。
4.2.4 csv
就像 ltsv 和out_file一样,可以通过设置属性delimiter来定义分隔符。该属性有一个默认值,即逗号。此外,csv 输出允许我们使用fields属性定义可以包含在输出中的值。如果我用记录再次说明,如果我的事件是{"my1stValue":" blah","secondValue":" more blah","thirdValue":"you guessed – blah",并且我将字段属性设置为secondValue, thirdValue fields,那么输出将是"more blah","you guessed – blah"。如果需要,可以通过force_quotes的布尔值禁用每个值的引号。
4.2.5 msgpack
与msgpack解析器一样,格式化程序与 MessagePack 框架一起工作,该框架使用核心日志事件记录,并使用 MessagePack 库来压缩内容。通常,我们只期望在期望接收 MessagePack 内容的 HTTP 转发输出插件中使用它。为了获得类似的压缩性能提升,我们可以使用 gzip 进行文件和块存储。
注意:你可能已经注意到,大多数格式化程序(out_file除外)省略了从日志事件中添加时间和键。因此,如果你想保留这些信息,将需要确保它们被包含在日志事件记录中,或者输出插件明确地使用这些值。可以使用*inject*插件将附加数据添加到日志事件有效负载中,该插件可以在*match*或*filter*指令中使用。我们将在第六章讨论过滤时介绍注入功能。
4.2.6 应用格式化程序
我们可以将现有的配置扩展为从当前的隐式配置转换为显式配置。让我们从使用默认的out_file格式化程序开始,使用逗号作为分隔符(delimiter comma)并排除日志事件标签(output_tag false)。我们将继续使用之前相同的源来演示格式化程序的效果。以下列表显示了此out_file格式化程序配置。
列表 4.4 第四章/Fluentd/rotating-file-read-file-out3.conf—格式化程序配置
<match *>
@type file
@id bufferedFileOut
path ./Chapter4/fluentd-file-output
<buffer>
delayed_commit_timeout 10
flush_at_shutdown true
chunk_limit_records 50 ❶
flush_interval 30
flush_mode interval
</buffer>
<format>
@type out_file ❷
delimiter comma ❸
output_tag false ❹
</format>
</match>
❶ 为了减少生成的文件数量,我们已将每个文件中的记录数设置得很大。
❷ 我们明确地将输出格式器定义为 out_file,因为我们想覆盖默认的格式化行为。
❸ 将制表符分隔符替换为逗号。
❹ 从输出中排除标签信息。
假设现有的日志文件和输出已经被移除,我们可以使用以下命令开始示例:
-
fluentd -c Chapter4/Fluentd/rotating-file-read-file-out3.conf -
groovy LogSimulator.groovy Chapter4/SimulatorConfig/jul-log-file2.properties ./TestData/medium-source.txt
4.2.7 将 JSON 格式化配置投入实际应用
您的组织已决定,作为标准做法,所有输出都应使用 JSON 结构完成。这种方法确保了日志事件中任何现有的或应用的结构意义不会丢失。为了支持这一目标,需要修改配置文件 /Chapter4/Fluentd/rotating-file-read-file-out3.conf。
答案
配置文件中的格式声明应减少到类似于以下列表中的片段。
列表 4.5 Chapter4/ExerciseResults/rotating-file-file-out3-Answer.conf
<format>
@type json
</format>
本答案的完整示例配置可以在 /Chapter4/ ExerciseResults/rotating-file-read-file-out3-Answer.conf 中找到。
4.3 将日志事件发送到 MongoDB
虽然将日志事件输出到某种形式的基于文件的存储是一种简单且易于存储日志事件的方法,但它并不适合执行任何分析或数据处理。为了使日志分析成为实际可能,Fluentd 需要与能够执行分析的系统进行交互,例如 SQL 或 NoSQL 数据库引擎、搜索工具如 Elasticsearch,甚至是 SaaS 服务如 Splunk 和 Datadog。
第一章强调了 Fluentd 对任何特定的日志分析引擎或供应商都没有忠诚度,这使得 Fluentd 与许多其他工具区分开来。因此,许多供应商发现 Fluentd 是将日志事件输入其产品或服务的有吸引力的解决方案。为了使采用变得非常容易,供应商已经开发了他们的适配器。
我们选择使用 MongoDB 来帮助说明将事件输入到能够进行日志分析的工具的方法。虽然 MongoDB 不像 Elasticsearch 那样专注于文本搜索,但其功能非常适合具有良好 JSON 结构的日志事件。MongoDB 在入门时非常灵活且要求不高,所以如果你没有使用过 MongoDB,请不要担心。MongoDB 的安装指南可以在附录 A 中找到。
MongoDB 概述
我们不希望过多地偏离 MongoDB 的机制,但总结一些基本概念是值得的。大多数读者都熟悉关系型数据库及其概念。数据库结构在 MongoDB 和关系型数据库中的作用是相似的。在数据库模式中有一组表。在 MongoDB 中,与之最接近的是“集合”。与关系型数据库不同,集合可以包含几乎所有内容,这就是为什么它有时被描述为使用“文档模型”。可以说,集合中的每个条目大致相当于一个包含 DB 分配的 ID 和 BLOB(大型二进制对象)或文本数据类型的表中的记录。通常,MongoDB 的行或文档是一个结构化文本对象,通常是 JSON 格式。MongoDB 可以然后搜索和索引这些结构的一部分,从而提供一种灵活的解决方案。对于 Fluentd 来说,这意味着我们可以存储可能具有不同记录结构的日志事件。
更新版的 MongoDB 引擎提供了验证进入集合的内容结构的手段。这为内容提供了一定的可预测性。如果使用此功能,那么我们可以利用 Fluentd 来结构化必要的有效负载。
除了控制每个文档的内容必须严格遵循结构外,Mongo 还引入了“固定大小”的概念。此功能允许我们限制集合使用的存储量,并且集合作为 FIFO(先进先出)列表运行。
当您想要清空关系型数据库中的表时,通常简单地删除并重新创建表会更简单。MongoDB 的等效操作是删除集合;然而,如果这是数据库中唯一的集合,MongoDB 将会删除整个数据库。有两种选择:仅删除集合的内容并保留集合,或者创建一个空的第二个集合,以保持数据库不为空。
您可以通过阅读 Kyle Banker 等人所著的《MongoDB in Action》一书来发现更多内容(www.manning.com/books/mongodb-in-action-second-edition)。
4.3.1 部署 MongoDB Fluentd 插件
MongoDB 输入插件包含在 Fluentd 的 Treasure Data Agent 构建中,但不包含在标准部署中。如果我们想让 Fluentd 与 MongoDB 一起工作,我们需要安装 RubyGem,如果它尚未安装的话。
为了确定是否需要安装以及执行 gems 的安装,我们可以使用一个利用 RubyGems 工具的包装工具,称为 fluent-gem。要查看 gem 是否已经安装,请在命令行中运行 fluent-gem list 命令。该命令将显示本地安装的 gems,其中包含 Fluentd 及其插件。在此阶段,不应有任何关于 fluent-plugin-mongo gem 的指示。因此,我们可以使用命令 fluent -gem install fluent-plugin-mongo 来执行安装。这将检索并安装该 gem 的最新稳定版本,包括文档和依赖项,如 MongoDB 驱动程序。
4.3.2 配置 Fluentd 的 Mongo 输出插件
在一个匹配规则中,我们需要引用 mongo 插件并设置相关属性。就像连接到任何数据库一样,我们需要提供一个地址(可以通过 host [名称] 和 port [网络上的端口号] 实现)以及用户名和密码,或者通过 connection string(例如,mongodb://127.0.0.1:27017/Fluentd)。在我们的示例配置中,我们采用了前一种方法并避免了凭证问题。需要数据库(模式)和集合(类似于关系型数据库中的表)来确定日志事件放置的位置。
在 MongoDB 输出中,不需要使用格式化工具,因为 MongoDB 插件假设所有内容已经结构化。正如我们所见,如果没有配置,将采用默认的缓冲区。像以前一样,我们将保持缓冲区设置以支持低流量,这样我们可以快速看到变化。我们可以在以下列表中看到结果。
列表 4.6 Chapter4/Fluentd/rotating-file-read-mongo-out.conf—匹配配置
<match *>
@type mongo ❶
@id mongo-output
host localhost ❷
port 27017 ❸
database Fluentd ❹
collection Fluentd ❺
<buffer> ❻
delayed_commit_timeout 10
flush_at_shutdown true
chunk_limit_records 50
flush_interval 30
flush_mode interval
</buffer>
</match>
❶ Mongo 是插件名称。
❷ 识别 MongoDB 主机服务器。在我们的开发设置中,这仅仅是本地机器;这也可以通过使用连接属性来定义。
❸ 与目标服务器通信的端口。我们也可以通过将主机名、端口和数据库组合成一个字符串来表示 URI。
❹ 由于 MongoDB 安装可以支持多个数据库,我们需要命名数据库。
❺ 我们想要添加日志事件的数据库中的集合——类似于基于 SQL 的表
❻ 缓冲区配置以确保日志事件能够快速集成到 MongoDB 中
Mongo 与 Mongo 副本集插件
如果你已经查看过 Fluentd 在线文档,你可能已经注意到两个输出插件,out_mongo和out_mongo_replset。关键区别在于replset(副本集)可以支持 MongoDB 的扩展方法,其中可以定义 Mongo 的额外实例作为主副本。当这种情况发生时,理想的模式是直接将活动写入主节点,但从副本节点读取。在配置差异方面,需要一个以逗号分隔的节点列表,而不是指定单个主机名。每个节点代表副本组中的一个节点(例如,nodes 192.168.0.10:27017, 192.168.0.20:27017, 192.168.0.30:27017)。还需要副本集名称(例如,replica_set myFluentReps)。有关 Mongo 副本集机制的更多信息,请参阅docs.mongodb.com/manual/replication/。
确保 MongoDB 实例正在运行(这可以通过在 shell 窗口中运行命令mongod --version或通过使用 Compass UI 尝试连接到服务器来完成)。如果 MongoDB 服务器没有运行,那么它需要被启动。最简单的方法是运行命令mongod。
当 Mongo 运行时,我们可以在每个 shell 中运行我们的模拟日志和 Fluentd 配置命令:
-
groovy LogSimulator.groovy Chapter4/SimulatorConfig/jul-log-file2-exercise.properties ./TestData/medium-source.txt -
fluentd -c Chapter4/fluentd/rotating-file-read-mongo-out.conf
Mongo 插件启动警告
当 Fluentd 与 MongoDB 插件一起启动时,它将记录以下警告:[mongo-output]。从 v0.8 版本开始,无效记录检测将被移除,因为 mongo 驱动程序 v2.x 和 API 规范不提供它。你可能会丢失无效记录,因此你不应该将此类记录发送到 Mongo 插件。
这实际上意味着正在使用的 MongoDB 驱动程序不对有效负载(集合可能已配置为需要)进行任何结构检查。因此,如果 MongoDB 引擎正在应用强检查,它可能会丢弃更新,但信息不会通过驱动程序返回,因此 Fluentd 将一无所知,导致数据丢失。对于 Fluentd 更常见的应用,最好不对有效负载施加严格的检查。如果这不是一个选项,那么一个过滤器指令可以识别将失败 MongoDB 检查的日志事件。

图 4.2 通过 Compass UI 工具查看 MongoDB,其中包含日志内容。
在 MongoDB 中查看日志事件
要查看 MongoDB 在查询 JSON 日志事件方面的有效性,如果您将表达式{"msg" : {$regex : ".*software.*"}}添加到 FILTER 字段并点击 FIND,我们将得到一些结果。这些结果将显示包含单词 software 的msg的日志事件。查询表达式告诉 MongoDB 在文档中查找,如果它们有一个名为msg的顶级元素,则使用正则表达式评估此值。
如果您检查 MongoDB 中的内容,您将看到存储的内容仅仅是核心日志事件记录,而不是相关的时间或标签。
要在命令行中清空集合以便进一步执行场景,请运行以下命令:
mongo Fluentd –-eval "db.Fluentd.remove({})".
mongo 插件还有一些其他技巧。当配置中包含tag_mapped属性时,标签名称用作集合名称,如果不存在,MongoDB 将创建该集合。这使得将日志事件分离到不同的集合变得极其简单。如果标签名称已经按层次结构使用,则可以删除标签前缀以简化tag_mapped功能。这可以通过remove_tag_prefix属性定义,它接受要删除的前缀名称。
由于集合可以动态建立,可以在配置中定义特性;例如,集合是否应该限制大小。
在此配置中,我们没有正式定义任何用户名或密码。这是因为我们在 MongoDB 配置中没有强制凭证限制。在 Fluentd 配置文件中包含凭证也不是最佳实践。第七章中讨论了在 Fluentd 配置中安全处理凭证的技术,不仅适用于 MongoDB,也适用于需要身份验证的其他系统。
4.3.3 将 MongoDB 连接配置字符串投入实际应用
修改配置以通过连接属性定义连接,而不是使用主机、端口和数据库名。这最好从复制配置文件Chapter4/fluentd/rotating-file-read-mongo-out.conf开始。调整运行命令以使用新的配置。命令看起来可能如下所示:
-
groovy LogSimulator.groovy Chapter4/SimulatorConfig/jul-log-file2-exercise.properties ./TestData/medium-source.txt -
fluentd -c Chapter4/fluentd/my-rotating-file-read-mongo-out.conf
答案
MongoDB 连接的配置应类似于以下列表中所示的配置。
列表 4.7 Chapter4/ExerciseResults/rotating-file-read-mongo-out-Answer.conf
<match *>
@type mongo
@id mongo-output
connection_string mongodb://localhost:27017/Fluentd ❶
collection Fluentd
<buffer>
delayed_commit_timeout 10
flush_at_shutdown true
chunk_limit_records 50
flush_interval 30
flush_mode interval
</buffer>
</match>
❶ 注意,为了这个目的,省略了主机、端口和数据库名属性。您可以使用您主机的特定 IP 地址或 127.0.0.1 而不是 localhost。
示例配置可以在Chapter4/ExerciseResults/rotating-file-read-mongo-out-Answer.conf中查看。
在我们离开 MongoDB 之前,我们关注的是 MongoDB 的输出用途,以便它可以用于查询日志事件。但 MongoDB 也可以作为 Fluentd 的输入,允许新添加到 MongoDB 的记录作为日志事件检索。
4.4 可采取行动的日志事件
在第一章中,我们介绍了使日志事件可采取行动的想法。到目前为止,我们已经看到了统一日志事件的方法。为了使日志事件可采取行动,我们需要几个元素:
-
能够将事件发送到外部系统,该系统能够触发动作,例如 Ops 人员可以看到并对此做出反应的协作/通知平台,或者调用脚本或工具执行动作
-
将需要采取行动的日志事件与仅提供信息但不需要特定行动的事件分开
在本书的后面部分将讨论如何分离或过滤出需要采取行动的事件。但在这里,我们可以看看如何在所有日志都到达分析平台并运行其分析过程之前,如何使事件可采取行动。
4.4.1 通过服务调用实现可采取行动的日志事件
使日志事件可采取行动的一种方法是通过调用一个可以执行必要修复的应用程序,作为接收 API 调用的结果。这可能就像调用 Ansible Tower REST API (mng.bz/Nxm1) 来启动一个执行一些日常维护工作(例如,将日志移动到存档存储或告知 Kubernetes 有关 Pod 的内部状态)的模板作业一样聪明。我们需要控制动作执行的频率;例如,flow_counter 和 notifier 这样的插件可以帮助。为了调用通用网络服务,我们可以使用 Fluentd 核心部分之一的 HTTP 输出插件。为了给出可能的技艺感,以下是此插件支持的一般功能的总结:
-
支持 HTTP post 和 put 操作
-
允许在路由中使用代理
-
配置内容类型,自动设置格式化器(格式化器也可以显式定义)
-
定义头信息,以便可以定义所需的额外头值(例如,API 密钥)
-
配置连接以使用 SSL 和 TLS 证书,包括定义要使用的证书的位置、版本、要使用的加密套件等。
-
当收到非成功的 HTTP 代码响应时,创建日志错误
-
支持基本身份验证(截至编写时,不支持 OAuth)
-
设置超时
-
使用缓冲区插件
4.4.2 通过用户交互工具实现可采取行动
以这种方式自动化问题解决使我们朝着自愈系统迈进,但并非许多组织都为这种高级状态做好了准备,或者必然想要达到这种程度。他们更愿意信任快速的人工干预来确定因果关系,而不是依赖于自动化诊断,在自动化诊断中,高度的确定性可能很难实现。拥有足够知识和适当信息的人可以迅速确定并解决此类问题。因此,Fluentd 有一套丰富的插件,用于社交协作机制。以下只是几个例子:
-
IRC (互联网中继聊天) (
tools.ietf.org/html/rfc2813) -
Twilio (支持许多不同的通讯渠道) (www.twilio.com)
-
Jabber (
xmpp.org) -
Redmine (www.redmine.org)
-
Typetalk (www.typetalk.com)
-
PagerDuty (www.pagerduty.com)
-
Slack (
slack.com/)
显然,我们需要在社交渠道通信中包含相关信息。为了帮助做到这一点,可以采取一系列措施,从清晰的日志事件,这些事件可以链接到解决方案指南,到将 Fluentd 配置为从日志事件中提取相关信息以共享。
4.5 Slack 示例以展示社交输出
Slack 已经成为一个领先的团队消息协作工具,它拥有强大的 API 层和一个免费版本,该版本仅受对话存档大小的限制。作为一个云服务,它是展示 Fluentd 与通过社交平台可操作日志事件交集的绝佳工具。虽然以下步骤是针对 Slack 的,但涉及的原则对 Microsoft Teams、Jabber 以及许多其他协作服务都是相同的。
如果你已经在使用 Slack,可能会倾向于使用现有的群组来运行示例。为了避免在你用测试日志事件填满频道时,其他 Slack 工作空间用户收到通知和消息而感到烦恼,我们建议设置你自己的测试工作空间。如果你没有使用 Slack,那也不是问题;在附录 A 中,我们解释了如何获取 Slack 账户并将其配置为准备使用。确保你在 Slack 配置过程中记下 API 令牌,该令牌以xoxb为前缀。
Slack 提供了一组丰富的配置选项以供交互使用。与 MongoDB 和许多 IaaS- 和 PaaS-级别的插件不同,由于 Slack 是一个 SaaS 服务,因此 Slack 实例的解析既简化了,又通过使用单个 token(无需服务器地址等)隐藏于我们面前。username 并非关于凭证,而是关于如何表示 Fluentd 插件所扮演的机器人;因此,使用一个有意义的名称是值得的。channel 与消息将被显示的 Slack 频道相关。通用频道默认存在,但如果你想在 Slack 中创建一个自定义频道并限制对该频道的访问,以便控制谁可以看到消息,你可能希望这样做。毕竟,你希望企业 Slack 设置中的每个人都看到每个操作消息吗?
message 和 message_keys 属性与消息一起使用,使用 %s 来指示已识别的有效负载元素的值插入的位置。message 中的引用与在 message_keys 中按顺序列出的 JSON 有效负载元素相关。
title 和 title_keys 与 message 和 message_keys 的工作方式类似,但用于 Slack UI 中显示的消息的标题。在我们的案例中,我们只是将使用 tag。最后一部分是 flush 属性;这告诉插件如何快速将 Slack 消息推送给用户。如果周期太长,可以将多个消息分组。为了保持快速流动,让我们每秒刷新一次。
编辑提供的现有配置(Chapter4/Fluentd/rotating-file -read-slack-out.conf),以在 Slack 设置中包含捕获的详细信息。以下列表展示了这一点。
列表 4.8 第四章/Fluentd/rotating-file-read-slack-out.conf—匹配配置
<match *>
@type slack
token xoxb-9999999999999-999999999999-XXXXXXXXXXXXXXXXXXXXXXXX ❶
username UnifiedFluent ❷
icon_emoji :ghost: ❸
channel general ❹
message Tell me if you've heard this before - %s ❺
message_keys msg ❻
title %s ❼
title_keys tag ❽
flush_interval 1s ❾
</match>
❶ 这就是从 Slack 获取的令牌被放置以正确识别工作空间并合法化连接的地方。
❷ 将在 Slack 对话中显示的用户名
❸ 定义与机器人关联的单独的 emoji(图标)
❹ 根据名称确定将消息放置在哪个频道。在我们的演示中,我们只是使用默认频道。
❺ 要显示的消息,按照配置中提供的顺序引用值。消息属性与消息 _keys 属性协同工作。
❻ 为要包含在日志事件中的日志事件的 JSON 元素命名。然后,该命名元素由消息标签获取并插入到消息文本中。
❼ 消息的标题采用 title_keys 的值。使用顺序在配置值之间进行映射。
❽ 消息标题的定义
❾ 定义 Fluentd 获取 Slack 发布日志事件的频率
在运行解决方案之前,我们还需要使用命令 fluent-gem install fluent-plugin-slack 安装 Slack 插件。一旦安装完成,我们就可以使用以下命令启动日志模拟器和 Fluentd:
-
fluentd -c Chapter4/Fluentd/rotating-file-read-slack-out.conf -
groovy LogSimulator.groovy Chapter4/SimulatorConfig/social-logs.properties ./TestData/small-source.txt
一旦启动,如果你在 Web 客户端或应用程序中打开#general频道,你将看到 Fluentd 流过的消息。
Slack 插件的全部细节可以从github.com/sowawa/fluent-plugin-slack获得。我们关于 Slack 使用的说明相对简单(图 4.3)。通过使用几个插件,我们可以快速从源标签路由 Slack 消息到最相关的个人或直接。或者,我们可以为每个应用程序设置不同的频道,并将消息直接发送到这些频道。

图 4.3 在 Slack 中显示的我们的 Fluentd 日志事件
4.5.1 更小心地处理令牌和凭证
很长时间以来,良好的安全实践告诉我们不应该将凭证硬编码到代码和配置文件中,因为任何人都可以查看文件并获取敏感凭证。实际上,如果你将代码提交到 GitHub,它将标记任何包含 GitHub 所知的服务的安全令牌字符串的配置文件或代码。当 Slack 被告知并决定这是一个有效的令牌时,请放心,它将吊销该令牌。
那么,我们如何解决这个问题呢?根据你的情况,有一系列策略;以下是一些选项:
-
使用具有有限会话作用域的环境变量设置敏感凭证。可以通过多种方式配置环境变量,例如使用 Chef 和 Puppet 等工具从密钥库中设置值。
-
在应用程序或配置中嵌入访问密钥库或秘密管理解决方案(如 HashiCorp 的 Vault)的方法。
配置文件可能看起来不像我们在这本书中看到的那样可以基于凭证进行安全保护。但是,我们可以通过在 Fluentd 配置文件中嵌入 Ruby 片段来实现这两种方法来安全地管理凭证,因为 Fluentd 允许我们这样做。这并不意味着我们立即需要学习 Ruby。对于这些方法中的第一种,我们只需要理解几个基本模式。嵌入对 Vault 的调用方法更具挑战性,但可以完成。
token “#{ENV[’slack-token']}”
4.5.2 在实际操作中外部化 Slack 配置属性
挑战在于设置你的环境,以便你有一个名为SlackToken的环境变量,该变量设置为保存你之前获得的令牌。然后自定义Chapter4/Fluentd/rotating-file-read-slack-out.conf以使用环境变量,并使用以下命令重新运行示例设置
-
fluentd -c Chapter4/Fluentd/rotating-file-read-slack-out.conf -
groovy LogSimulator.groovy Chapter4/SimulatorConfig/social-logs.properties ./TestData/small-source.txt
确认日志事件是否已到达 Slack。
答案
通过设置环境变量,您将创建一个看起来像以下之一的命令
set slack-token= xoxb-9999999999999-999999999999-XXXXXXXXXXXXXXXXXXXXXXXX
或者为 Windows 或 Linux
Export slack-token= xoxb-9999999999999-999999999999-XXXXXXXXXXXXXXXXXXXXXXXX
配置现在将改变,看起来像以下列表中的示例。
列表 4.9 第四章/rotating-file-read-slack-out-Answer.conf—匹配配置
<match *>
@type slack
token "{ENV["slack-token"]}"
username UnifiedFluent
icon_emoji :ghost:
channel general
message Tell me if you've heard this before - %s
message_keys msg
title %s
title_keys tag
flush_interval 1s
</match>
4.6 适合工作的正确工具
在第一章中,我们强调了由于各种原因(如下所述)不同的人想要不同的工具的问题:
-
为了执行日志分析,不同的工具具有不同的优势和劣势
-
多云,因此专业团队(以及网络流量的成本考虑)意味着使用不同的云服务提供商工具
-
为了做出影响个人偏好和政策的决策(先前经验等)
正如我们所展示的,Fluentd 可以支持许多社交平台和协议。当然,这不会是日志事件放置的唯一地方。核心目的地类型之一是日志分析工具或平台。Fluentd 拥有大量插件来为日志分析平台提供数据;除了我们之前提到的两个之外,其他可以轻松插入的主要解决方案包括
-
Azure Monitor
-
Graphite
-
Elasticsearch
-
CloudWatch
-
Google Stackdriver
-
Sumo Logic
-
Logz.io
-
Oracle 日志分析
然后,当然,我们可以将日志发送到各种数据存储解决方案以备后用或进行数据分析;例如:
-
Postgres、InfluxDB、MySQL、Couchbase、DynamoDB、Aerospike、SQL Server、Cassandra
-
Kafka,AWS Kinesis(时间序列存储/事件流)
-
存储区域,如 AWS S3、Google Cloud Storage、Google BigQuery、WebHDFS
因此,问题变成了,我的需求是什么,哪些工具最适合?如果我们的需求随时间变化,那么我们就根据需要添加或删除我们的目标。改变技术可能会提出更多关于如何处理我们当前的日志事件的问题,而不是如何将数据放入解决方案。
摘要
-
Fluentd 拥有广泛的支持文件、其他 Fluentd 节点、关系型数据库和文档数据库(如 MongoDB、Elasticsearch 等)的输出插件。
-
插件支持不仅限于分析和存储解决方案,还包括协作和通知工具,如 Slack。这使得 Fluentd 能够对重要的日志事件做出更快的反应。
-
Fluentd 提供了一些强大的辅助插件,包括格式化和缓冲,使得日志事件输出配置非常高效且易于使用。
-
日志事件可以通过分析工具和可视化工具等工具变得易于消费。Fluentd 提供了使用格式化插件(如 out_file 和 json)格式化日志事件的方法。
-
缓冲辅助插件可以根据需要支持不同的生命周期,从简单的同步缓存到完全异步。有了这个,缓冲存储可以根据大小或日志事件的数量进行组织。
-
缓冲区可以被配置为不仅在关闭时刷新其内容,还可以在其他条件下刷新,例如缓冲一段时间的新事件。
5 路由日志事件
本章涵盖
-
将日志事件复制到发送到多个输出
-
使用标签和标签路由日志事件
-
在 Fluentd 中观察过滤器并处理错误
-
应用包含以实现配置的重用
-
将额外的上下文信息注入日志事件
到目前为止,在这本书中,我们已经看到了如何捕获和存储日志事件。但在所有示例中,路由都是简单地所有事件都发送到同一个输出。然而,这可能远非理想。正如第一章所述,我们可能希望日志事件根据日志事件的类型发送到不同的工具。可能希望将日志事件发送到多个位置或一个都不发送。因此,在本章中,我们将探讨我们可以路由事件的不同方式。此外,我们还将查看一些较小的功能,这些功能可以帮助解决路由的挑战,例如在日志事件中添加信息以确保日志事件的来源在传输过程中不会丢失。
路由通常与个人或团队之间如何分配工作相一致。正如我们将看到的,使用包含功能支持多个团队可以各自在其 Fluentd 配置的部分上工作,而不会打扰他人并注入特定的配置值。例如,我们已经看到安全团队需要将日志事件的路由和过滤应用于他们的工具(并排除他们不感兴趣的事件)。相比之下,运维团队需要不同的工具中的日志事件。通过路由和包含功能,我们可以快速实现这一点。
本章不会涉及路由的一个方面是将日志事件转发到其他 Fluentd 节点,因为这最好在我们稍后查看扩展时解决。
5.1 通过复制达到多个输出
将日志事件发送到所有正确输出(s)的一种方法是通过确保所有输出都接收事件,并且每个输出包含一个或多个过滤器以阻止不需要的内容被输出。在本节中,我们将重点关注复制,稍后我们将讨论过滤,因为在过滤事物之前,我们需要将日志事件带到正确的位置。
如第二章所述,日志事件默认情况下由第一个合适的match指令消费,其中包含输出插件。为了允许日志事件在匹配指令内到达多个输出插件,我们需要使用复制插件(@copy)。
每个目的地都包含在匹配指令中定义的store声明中,该声明使用 XML 风格的标签<store>和</store>。虽然store可能并不总是作为一个插件名称看起来直观(许多输出是为了我们不会将其与存储关联的解决方案,如 Grafana),但值得记住的是,Fluentd 的更多插件是针对日志事件的检索和存储,而不是其他任何事情。图 5.1 中的图示说明了指令和插件在逻辑上以及配置文件编写方式上的相互关系。

图 5.1 使用 @copy 和 Store 的匹配指令元素层次结构可视化。从左到右阅读,我们看到配置块越来越详细和专注(即特定插件类型的缓冲区或格式化器)。存储配置块可以在复制插件内部出现一次或多次。
在每个存储配置块中,我们可以配置插件的使用。通常,这将是一个输出插件,但也可以很容易地是一个过滤器插件。存储插件的属性可以像在 match 指令中直接使用时一样进行配置。这包括使用辅助插件,如 buffers。
为了说明这一点,我们将使用文件输入,而不是像第三章中那样将日志事件从一个文件发送到另一个文件,我们将扩展配置以将输出发送到文件和控制台。我们可以在图 5.2 中看到这种表示。

图 5.2 使用存储和复制将日志事件发送到多个目标配置文件的可视化
要实现此功能,我们需要编辑 match 指令。最简单的方法是首先将现有的输出插件属性包裹在 store 标签内,然后添加下一个 store 开始和结束标签。有了 store 开始和结束标签,就可以配置每个输出插件。最后,在 match 指令的开始处引入 @copy。修改后的配置如下所示,其中包含两个 store 块,每个块包含一个输出插件(file 和 stdout)。您还会看到一个具有 null 输出插件类型的第三个 store 块,后面跟着一个 @include 指令。我们将在稍后解释这些。
列表 5.1 Chapter5/Fluentd/file-source-multi-out.conf——复制到多个输出
<match *>
@type copy ❶
<store>
@type null
</store>
<store> ❷
@type stdout
</store>
<store> ❸
@type file
@id bufferedFileOut
tag bufferedFileOut
path ./Chapter5/fluentd-file-output
<buffer>
delayed_commit_timeout 10
flush_at_shutdown true
chunk_limit_records 500
flush_interval 30
flush_mode interval
</buffer>
<format>
@type out_file
delimiter comma
output_tag true
</format>
</store>
@include additionalStore.conf
</match>
❶ 声明要使用的插件
❷ 存储块的开始——每个存储反映要采取的操作。这通常是为了使用插件存储日志事件或将事件转发到另一个 Fluentd 节点。在这种情况下,我们只是写入控制台。
❸ 第三个存储路由到文件
让我们看看配置的结果。由于这使用文件源,我们需要运行 LogSimulator。因此,要运行此示例,需要以下命令:
-
fluentd -c ./Chapter5/Fluentd/file-source-multi-out.conf -
groovy LogSimulator.groovy ./Chapter5/SimulatorConfig/log-source-1.properties
运行这些命令后,日志事件将很快出现在控制台上。一旦缓冲区达到写入点,将出现名为 fluentd-file-output.<date>_<number>.log 的文件。将文件内容与控制台内容进行比较是值得的,因为我们已经将额外的属性包含在有效负载中。
5.1.1 按引用或按值复制
在大多数,也许甚至是所有编程语言中,都存在浅拷贝和深拷贝的概念,有时称为按引用复制(如图 5.3 所示)和按值复制(如图 5.4 所示)。无论您习惯于哪种术语,按引用复制意味着每个副本通过引用同一块内存来达到日志事件的复制。如果日志事件被修改,那么这种变化会影响所有后续使用的所有副本。按值复制意味着获取一个新的内存块,并复制其内容。这意味着如果一个副本被修改,另一个副本不会受到影响,因为它是一个完整的克隆。虽然我们还没有看到需要做任何其他事情的理由,但在下一章中,我们将看到可以操纵日志事件的内容。

图 5.3 按引用复制时对象在内存中的存储方式
如图 5.3 所示,当对象 B 被创建为对象 A 的浅拷贝时,它们都指向包含内部对象(对象 1)的同一内存。因此,如果我们通过对象 B 更新对象 1,那么也会影响对象 A。

图 5.4 按值复制时对象在内存中的存储方式
在复制配置中,我们可以通过copy_mode属性来控制这种行为。复制模式有几种设置,其行为范围从按引用复制到按值复制:
-
no_copy—默认状态,实际上是按引用复制。
-
浅拷贝—这会深度复制第一层值。如果那些对象反过来又引用对象,它们仍然引用与原始对象相同的内存。在底层,这使用了 Ruby 的dup方法。虽然比深度复制快,但使用 dup 需要谨慎;它类似于嵌套对象的no_copy。
-
深拷贝—这是一个正确的按值复制,利用 Ruby 的msgpack gem。如果有疑问,这是我们推荐的方法。
-
Marshal—当 Ruby 的 msgpack 无法使用时,可以使用本地语言的物体序列化。对象被序列化(序列化)成一个字节流表示形式。然后字节流被反序列化(反序列化),并产生一个代表字节流的对象。
复制操作的工作原理
以下将帮助您更好地理解复制行为的工作原理:
-
Ruby dup(浅拷贝):
mng.bz/mxOP -
msgpack-ruby(深拷贝):
rubydoc.info/gems/msgpack -
msgpack 序列化(marshal):
ruby-doc.org/core-2.6.3/Marshal.html
理想情况下,我们不需要担心按值复制,因为日志事件以良好的结构接收,并包含所有必要的状态信息,因此内容操作变得不必要。遗憾的是,世界并不完美;在使用复制功能时,请考虑默认选项是否合适;例如,我们是否需要操作一个目标而不是另一个目标的日志事件?使用标签创建“日志事件处理管道”会增加需要考虑如何复制的机会,正如我们将在本章后面看到的那样。
另一个需要注意的考虑因素是,在为不同的存储复制日志事件时,如果日志事件可以携带敏感数据,我们可能希望在大多数情况下对值进行编辑或屏蔽,但对于发送到安全部门的日志事件则不是这样。如果安全部门不希望受到任何数据屏蔽或编辑的影响,他们需要深度复制。
5.1.2 复制时的错误处理
在我们提供的示例配置中,两个输出都指向相同的本地硬件,并且需要一些独特的情况才会影响一个文件而不影响另一个。然而,假设输出被发送到远程服务,例如数据库或 Elasticsearch。在这种情况下,一个输出出现问题而另一个输出不受影响的可能性显著增加。例如,如果其中一个目标服务已被关闭或网络问题阻止了通信,我们的输出会发生什么?Fluentd 是否会将日志事件发送到可用的存储,或者除非它们全部可用,否则不发送到任何存储?
Fluentd 不会尝试应用XA 事务(也称为两阶段提交),因为它允许全有或全无的行为,因为此类事务的协调是资源密集型的,且协调需要时间。然而,默认情况下,它确实应用了次优方案;在某个输出失败的情况下,后续的输出将被放弃。例如,如果我们复制到三个存储,分别称为 Store A、Store B 和 Store C,它们在配置中按此顺序定义,并且我们未能将日志事件发送到 Store A,那么没有任何存储会收到该事件(参见图 5.5 的第一部分)。如果问题发生在 Store B,那么 Store A 将保留日志事件,但 Store C 将被放弃(参见图 5.5 的第二部分)。

图 5.5 存储错误如何影响包含的复制。每个图底部的条形图表示哪个存储会接收到数据值,哪个存储失败,以及哪个存储未执行操作。例如,在中间示例中,如果 Store B 失败,那么 Store A 将接收到日志事件,Store B 不会接收到事件,而 Store C 则不会进行通信。
但如果你在输出配置中有一个缓冲区,这可能会掩盖问题,因为缓冲区可能异步操作并包括回退和重试等选项。结果,如上所述,放弃重试等错误可能不会影响复制过程。鉴于这种方法,有选项可以按顺序排列复制块以反映输出优先级。
然而,如果你使用带有重试的异步缓冲区,缓冲区将允许执行继续到下一个存储。但如果它随后达到最大重试次数,它将失败该存储,但后续的存储操作可能已经成功。
如何应用优先级/顺序应取决于日志事件的值和输出能力。例如,使用输出插件允许使用二级辅助插件,如secondary_file。如果日志事件至关重要,以至于不能丢失,那么首先优先考虑本地 I/O 选项是最好的。如果日志事件的优先级是快速将其发送到远程中央服务(例如,Kafka 或 Splunk)并且失败,那么这意味着该事件在其他地方(例如,Prometheus 用于指标计算贡献)的帮助很小;因此,最好以最高优先级的目的地开始。
Fluentd 还提供另一种选项来定制此行为。在<store>声明中,可以添加ignore_error(例如,<store ignore_error>)参数。然后,如果该存储块中的输出导致错误,它将阻止错误级联,从而触发后续存储块被放弃。使用我们的示例三个存储,在存储 A 上设置ignore_error意味着无论是否将事件发送到存储 A,我们都会继续尝试使用存储 B。但如果存储 B 失败,那么存储 C 将不会收到该事件。
5.2 通过包含配置重用和扩展
随着 Fluentd 配置的发展、成熟以及可能引入多个日志事件处理路由,我们的 Fluentd 配置文件在大小和复杂性上都会增长。随着这种增长,我们也很可能发现一些配置可以重用(例如,不同的匹配定义想要重用相同的过滤器或格式化器),尤其是在尝试实现DRY(不要重复自己)的理想时。因此,在本节中,让我们探讨如何解决大型配置文件带来的挑战并最大化重用。
我们可以通过确保 Fluentd 配置操作以正确的顺序出现来尝试解决这个问题。使用标签过滤事件可能有效。这种方法可能会相当混乱,并且小的更改可能会以意想不到的方式干扰日志事件的流程。
另一种方法是尝试调整配置文件,以便在不同的上下文中重复使用部分内容。第一步是将需要重复使用的 Fluentd 配置隔离到自己的文件中,然后使用 @include 指令和文件名及路径,在需要该配置的任何地方使用。使用 include 语句,引用的文件将被合并到父配置文件中。这意味着我们可以重复使用配置和包含,这样就不需要操作 Fluentd 配置,指令的顺序也就不是问题。
在 Fluentd 启动期间,配置文件会被解析,并将包含指令替换为所包含文件的副本内容。这样,我们可以在需要的地方包含相同的配置文件。例如,如果我们有一个针对 Elasticsearch 的企业级设置,那么所有不同的 Fluentd 配置都可以引用单个文件来使用企业 Elasticsearch,并且可以针对单个文件应用更改,例如优化连接设置。当配置文件部署时,每个人都会继承这些更改。
包含 不必包含完整的配置;它可以很容易地包含单个属性。一个很好的例子是,当你想要将一些常见的 Ruby 逻辑(例如,检索一些安全凭证)重复使用到 Fluentd 配置中,正如我们稍后将要讨论的。同样,包含文件也可以用来注入配置块,例如 store 块,甚至是一个完整的附加配置文件,该文件也可以独立使用。在列表 5.2 中,我们通过在最后一个 store 标签定义的附加存储配置之后引入 @include additionalStore.conf,添加了几个包含项。这意味着我们可以为所有日志事件定义一个共同的目的地,并在其他配置文件中重复配置,以便在共同的位置记录所有事件,然后允许配置专注于目的地。
列表 5.2 Chapter5/Fluentd/file-source-multi-out2.conf—包含示例
<match *>
@type copy
<store>
@type null
</store>
<store>
@type stdout
</store>
<store>
@type file
@id bufferedFileOut
tag bufferedFileOut
path ./Chapter5/fluentd-file-output
<buffer>
delayed_commit_timeout 10
flush_at_shutdown true
chunk_limit_records 500
flush_interval 30
flush_mode interval
</buffer>
<format>
@type out_file
delimiter comma
output_tag true
</format>
</store>
@include additionalStore.conf ❶
</match>
@include record-origin.conf ❷
❶ 将外部文件整合到配置中,提供额外的存储声明
❷ 提供了一个完整的配置集,如果需要可以单独运行,或者可以重复使用
我们还添加了一个引用文件 record-origin .conf 的包含指令。这说明了当多个团队向单个运行时环境(例如,J2EE 服务器)贡献功能时,而不是所有团队都试图维护单个配置文件并处理冲突,每个团队都有自己的配置文件。但在执行时间,单个配置文件使用包含来将所有内容组合在一起。因此,Fluentd 节点需要在启动时合并所有配置。在 record-origin .conf(如果您查看 record-origin.conf 的内容),我们引入了一些新的插件,我们将在本章后面介绍。
让我们看看配置的结果。由于这使用文件源,我们需要运行 LogSimulator。因此,要运行此示例,需要以下命令:
-
fluentd -c ./Chapter5/Fluentd/file-source-multi-out2.conf -
groovy LogSimulator.groovy ./Chapter5/SimulatorConfig/log-source-1.properties
注意:重要的是要记住,包含的内容可能会影响具有 include 声明的配置。因此,包含的位置和使用必须谨慎处理,因为指令和它们相关插件的最终顺序仍然适用,如第二章中所述。
如果 include 语句中包含文件的路径是相对的,那么参考点是带有 include 指令的文件位置。include 指令可以使用逗号分隔的列表,在这种情况下,列表顺序与插入顺序相关——例如,@include file.conf, file2.conf 表示 file.conf 在 file2.conf 之前被包含。如果 include 指令使用通配符(例如,@include *.conf),则插入顺序是按字母顺序的。
图 5.6 显示了干运行输出,并突出显示了 include 声明已被替换为包含的配置或配置片段内容。

图 5.6 显示了 Fluentd 启动时解析的包含文件(框内高亮显示)
注意:由于这个过程是纯文本替换,这意味着包含可以很容易地是一个空的占位符文件或配置片段。如果包含在文件中的位置不正确,可能会使整个配置无效。
5.2.1 使用空输出进行占位
在列表 5.2 中,提供的附加包含片段 (@include additionalStore .conf) 如列表 5.3 所示。这个 store 定义使用了空输出插件;它简单地丢弃接收到的日志事件。
在一个可能希望将日志事件输出到不同工具的不同团队的环境中工作时,放置空插件允许开发者构建一个服务,在 Fluentd 配置中放置占位符,以便其他团队替换。在许多方面,使用空插件是添加 TODO 代码注释的近似方法。
注意 TODO 是在代码中标记还有事情要做的一个常用标签。
列表 5.3 Chapter5/Fluentd/additionalStore.conf—包含配置片段
<store>
@type null
@id inclusion
</store>
5.2.2 将 MongoDB 输出包含到实际操作中
让我们将一些见解应用到这个场景中。知道在哪里最好地应用努力最好是由分析洞察驱动的。将错误事件直接导向数据库使得获取随时间显示发生错误及其频率的统计数据变得容易。当与对错误影响的欣赏相结合时,可以以最大价值的目标来定位努力。
我们需要将此应用到 Chapter5/Fluentd/file-source-multi-out.conf。为此,可以利用第四章中的工作,当时我们使用了 MongoDB 插件的 Fluentd。我们可以利用它来观察复制错误和使用 ignore_error 选项的影响。为此,创建一个 Chapter5/Fluentd/file-source-multi-out.conf 的副本,可以安全地进行修改。为了简单起见,让我们称这个副本为 Chapter5/Fluentd/file-source-multi-out-exercise.conf。我们需要将 @type null 替换为 MongoDB 输出配置。运行场景所需的命令是
-
fluentd -c ./Chapter5/Fluentd/file-source-multi-out-exercise.conf -
groovy LogSimulator.groovy ./Chapter5/SimulatorConfig/log-source-1.properties
应用更改后,我们应该能够完成以下步骤:
-
使用干运行功能检查配置。这应该会产生一个有效结果。
-
通过启动 MongoDB 并重新运行 LogSimulator 和 Fluentd 来确认修改后的配置产生了预期的结果。
-
如果我们无法连接到 MongoDB,请验证行为是否符合预期,并重复执行运行 LogSimulator 和 Fluentd 的相同操作。
-
前一个步骤应该已经突出了
ignore_error选项的缺失。修改 Fluentd 配置,将ignore_error选项添加到控制台输出配置中。重新运行配置和 LogSimulator。确认现在所需的行为是正确的。
答案
-
修改后的 Fluentd 配置文件现在应该看起来像
Chapter5/ExerciseResults/file-source-multi-out-Answer1.conf,并且能够成功执行干运行。 -
当 MongoDB 运行时,数据库应继续填充反映发送到文件的内容的事件,并且控制台仍然会显示内容。
-
当 MongoDB 停止时,输出插件将开始出现错误,因为没有配置来确保问题不会级联影响其他插件。没有任何输出流会产生日志事件。这是因为默认位置是,一旦发生错误,后续输出插件不应执行。
-
在配置中添加了
ignore_error后,配置现在应类似于Chapter5/ExerciseResults/file-source-multi-out-Answer2.conf。由于 MongoDB 仍然停止,MongoDB 输出将失败,但失败不会停止控制台输出,但会阻止文件输出。
5.3 将上下文注入日志事件
提供更多信息和环境可以帮助我们处理日志事件。为此,我们可能需要操作预定义的日志事件属性并捕获额外的 Fluentd 值。本节将更详细地探讨这一点。
通过将此信息注入日志事件作为可识别的日志事件属性,我们可以在尝试使用过滤器排除指令时显式引用这些值,这将防止日志事件在事件序列中进一步处理。例如,假设与特定主机关联的日志事件被认为是不必要的,可以通过将属性集与主机名进行比较来停止信息传递。在这种情况下,我们可以应用一个带有排除指令的过滤器来阻止信息传递。
注入操作只能与匹配和过滤指令一起使用,这很遗憾,因为我们可能希望在源处应用它。尽管如此,如果需要,这并不是一个重大的挑战,正如我们很快就会看到的。使用我们的示例配置Chapter5/Fluentd/record-origin.conf,我们可以在列表 5.4 中看到注入的工作情况。
在配置时间数据注入时,可以配置时间的不同表示形式。这由time_type属性处理,它接受以下值
-
字符串—允许使用文本表示,并委托给
time_format属性进行表示。time_format使用标准表示法,如附录 A 所述。 -
浮点数—从纪元开始的秒和纳秒(例如,
1510544836.154709804)。 -
Unix 时间—这是从纪元开始的传统的秒表示。
在列表 5.4 中,我们选择了最易读的字符串格式。除了描述时间数据格式外,还可以通过包含localtime和utc属性来指定时间作为本地时间或 UTC 时间,这些属性接受布尔值。尝试设置这两个属性可能是许多问题的来源。
列表 5.4 Chapter5/Fluentd/record-origin.conf—注入声明
<match **>
<inject> ❶
hostname_key hostName ❷
worker_id_key workerId ❸
tag_key tag ❹
time_key fluentdTime ❺
time_type string ❻
localtime true
</inject>
@type stdout
</match>
❶ 在通用匹配中注入声明
❷ 添加 Fluentd 的主机名并按提供的名称(例如,hostName)调用值
❸ 添加 worker_id 并按提供的名称调用它。这有助于当 Fluentd 有支持进程来分担工作时。
❹ 将标签放入记录输出并使用提供的名称
❺ 提供要包含的时间名称
❻ 定义日期时间的表示方式。在这里,我们说的是提供文本表示,但由于我们省略了用于定义格式的 time_format 值,因此使用标准格式。
inject 配置的属性与将已知值(如 hostname、tag 等)映射到日志事件记录中的属性相关。
要看到这个配置的实际效果,我们使用了 monitor_agent 和 stdout,所以我们只需要运行 Fluentd,命令为 fluentd -c ./Chapter5/fluentd/record-origin.conf。结果将出现在控制台,类似于以下内容
2020-05-18 17:42:41.021702900 +0100 self: {"plugin_id":"object:34e82cc",
"plugin_category":"output","type":"stdout","output_plugin":true,"retry_count":0,
"emit_records":4,"emit_count":3,"write_count":0,"rollback_count":0,"slow_flush_count":0,
"flush_time_count":0,"hostName":"Cohen","workerId":0,"tag":"self",
"fluentdTime":"2020-05-18T17:42:41+01:00"}
在这个输出中,您将看到注入的值使用属性定义的名称出现在 JSON 结构的末尾;例如,"hostName": "Cohen",其中 Cohen 是编写这本书所使用的电脑。
5.3.1 值提取
如果我们可以将某些值注入到日志事件的记录中,那么很明显,应该有一个计数器功能来从记录中提取值以设置日志事件的标签和时间戳。这种能力可以通过与源、过滤器和匹配指令一起工作的插件来利用。这为我们提供了一种根据日志事件记录内容动态设置标签的有用手段。动态设置标签使得基于标签的路由非常灵活。例如,如果日志事件有一个名为 source 的属性,而我们想将其用作路由的手段,我们可以使用 extract 操作。例如:
<inject>
tag_key nameOfLogRecordAttribute
</inject>
不幸的是,只有一小部分可用的插件利用了 extract 辅助功能。其中一个是核心插件,它确实包含了我们尚未涉及的 exec, which。因此,当我们探索下一节的基于标签的路由时,我们将使用 exec,并探索它提供的有趣机会。
5.4 基于标签的路由
在到目前为止的所有章节中,我们总是在 match 声明中使用了通配符(例如,<match *>),但我们有机会在不同的阶段定义和更改标签值。我们已经看到标签在从 URI 中获取标签值到在配置中设置标签,甚至是从日志事件记录中提取标签的上下文中被操作。我们可以使用标签来控制哪些指令被执行,这是本节的主题。
我们可以通过更明确地定义匹配值来控制哪些指令将处理和消费日志事件。例如,一个名为 AppA 和 AppB 的两个输入配置包括设置相应标签为 AppA 和 AppB 的 tag 属性。现在,而不是 match *,我们将指令设置为 <match AppA> 和 <match AppB>。随着这一变化,匹配指令将只处理来自相关源的日志事件。
在我们的示例中,为了保持源简单,我们已配置了两个 dummy 源插件的实例以生成日志事件。我们添加了额外的属性来控制在不同频率(rate 属性表示每个生成的日志事件之间的秒数)和不同消息(dummy 属性)上的行为。
在以下列表中,我们展示了配置的关键元素(为了清晰起见,我们删除了一些配置元素;这可以通过省略号 [...] 来看到)。
列表 5.5 第五章/Fluentd/monitor-file-out-tag-match.conf—标签匹配
<source> ❶
@type dummy
dummy {"hello from":"App A"}
auto_increment_key AppACounter
tag AppA
rate 5
</source>
<source> ❷
@type dummy
dummy {"Goodbye from":"App B"}
auto_increment_key AppBIncrement
tag AppB
rate 3
</source>
<match AppB> ❸
@type file ❹
path ./Chapter5/AppB-file-output
@id AppBOut
<buffer> . . . </buffer>
<format> . . . </format>
</match>
<match AppA> ❺
@type file
path ./Chapter5/AppA-file-output
@id AppAOut
<buffer> . . . </buffer>
<format> . . . </format>
</match>
❶ 此配置文件中的两个源定义中的第一个,但请注意端口号不同,以及几个其他配置属性,因此源很容易区分。
❷ 第二个 self_monitor 源配置。最重要的是,注意源之间的标签名称差异。
❸ 两个匹配声明中的第一个。注意我们如何使用通配符字符来定义部分名称匹配。
❹ 将文件输出配置映射到每个匹配的不同输出文件(比较路径属性)
❺ 第二次匹配,这次没有任何通配符
可以使用以下命令运行此设置
fluentd -c ./Chapter5/Fluentd/ monitor-file-out-tag-match.conf
输出文件应反映不同的虚拟消息,因为路由将已从相关源导向。
尽管命名如此,仍然可以使用选择性的通配符与标签一起使用。如果我们通过添加一个额外的源并标记为 AppAPart2 来扩展此示例,我们可以捕获 AppA 和 AppAPart2。这是通过将 <match AppA> 修改为 <match AppA*> 来实现的。从新源捕获的日志事件将被纳入 AppA 输出。
这在列表 5.6 中得到了说明。如果我们不想重新引入通配符的使用,我们也可以在匹配声明中使用逗号分隔的标签列表;例如,<match AppA, AppAPart2>。为了说明通配符的行为,这次我们引入了另一个名为 exec 的源插件。exec 插件允许我们调用 OS 脚本并捕获结果。我们只是在 exec 语句中使用 more 命令(因为它在 Linux 和 Windows 上表现相同)。
列表 5.6 第五章/Fluentd/monitor-file-out-tag-match2.conf—标签匹配
<source> ❶
@type dummy
dummy {"hello from":"App A"}
auto_increment_key AppACounter
tag AppA
rate 5
</source>
<source>
@type exec ❷
command more .\TestData\valuePair.txt
run_interval 7s
tag AppAPart2
</source>
<source> ❸
@type dummy
dummy {"Goodbye from":"App B"}
auto_increment_key AppBIncrement
tag AppB
rate 3
</source>
<match AppB> . . . </match> ❹
<match AppA*> ❺
@type file
path ./Chapter5/AppA-file-output
@id AppAOut
<buffer> . . . </buffer>
<format> . . . </format>
</match>
❶ 原始源,保持未更改
❷ 使用 exec 源插件添加的额外源
❸ 原始 AppB 源,保持不变
❹ AppB 的匹配保持未修改。
❺ AppA 的原始匹配现在已被修改以包含通配符,这意味着 AppA 和 AppAPart2 都将被匹配。这也可以表示为 <match AppA, AppAPart2>。
可以使用以下命令运行此设置
fluentd -c ./Chapter5/Fluentd/ monitor-file-out-tag-match2.conf
输出文件应反映不同的虚拟消息,但 AppA 输出现在应包括在预定义测试数据文件上执行 OS 命令的结果。
标签命名约定
尽管使用通配符字符来帮助选择不同指令的标签,而不考虑其位置,但通常有一个约定。标签命名通常遵循类似于命名空间的层次结构,使用点来分隔层次结构层(例如,AppA.ComponentB.SubComponentC)。现在通配符可以过滤不同的命名空间(例如,AppA.* 或 AppA.ComponentB.*)。例如,如果我们有一个托管多个不同服务的域的 Web 服务器,每个服务可能有一个或多个日志输出,我们可能会在标签约定中看到 webserver.service .outputName 的约定。
5.4.1 使用 exec 输出插件
列表 5.6 中所示的 exec 插件提供了一些有趣的机会。当插件无法帮助我们获取所需的信息时,我们有几种选择:
-
构建一个自定义插件(本书稍后将会探讨)。
-
创建一个独立的实用程序,可以直接通过 HTTP、UDP、转发插件将数据馈送到 Fluentd。
-
生成一个可以由 exec 插件调用的小型脚本。
使用 exec 插件可以轻松检索特定环境的信息或执行类似使用 Wget 和 cURL 等工具抓取网页输出的操作——这是屏幕抓取的现代版本。后者尤其有趣,因为可以从 Web 接口或 Web 端点中提取信息——例如,如果第三方提供了一个微服务(因此必须将其视为黑盒)——并且仍然可以有效地进行监控。如果第三方遵循了提供 /health 端点的最佳实践(更多信息请参阅 mng.bz/5KQz),我们可以运行一个脚本来从对 /health 的 Wget 或 cURL 调用的响应中提取必要的值。
使用 exec 插件确实需要小心谨慎。每个 exec 进程都在自己的线程中执行,这样就不会在其他日志事件被触发时对其他日志事件的消费产生不利影响。然而,如果进程太慢,我们可能会遇到以下情况:
-
exec 插件可能会在最后一个尚未完成之前再次被触发,这可能导致创建出序事件(由于资源如何在线程之间共享,这种情况可能会发生)。
-
线程死亡可能发生,因为存在太多线程需要太多资源(如果缓冲区最终有太多线程,可能会出现此类问题)。
-
事件开始积压,因为逻辑会等待线程完成以分配给另一个 exec。
吸取的教训是思考 exec 在做什么;如果它运行缓慢或计算密集,那么在 Fluentd 中运行它可能是不明智的。我们可以考虑独立运行将结果写入文件的 exec 进程,并且与核心业务流程相比,日志管理应该相对较轻量。
5.4.2 将标签命名约定付诸实践
团队已经决定,日志配置应该反映 domain.service.source 的命名约定。当前的配置没有反映被调用的域为 Demo,服务被命名为 AppA 和 AppB,其中 AppA 有两个组件 Part1 和 Part2。你被要求更新配置文件 monitor-file-out-tag-match2.conf 以符合此约定。更改 AppA 的匹配指令,以便仅在 AppA 文件中捕获 Part1。注意额外的输入,因为 exec 源在输出中尚不需要。
答案
结果应该得到一个修改后的配置,看起来类似于 Chapter5/ExerciseResults/monitor-file-out-tag-match-Answer.conf。注意匹配条件是如何改变的。
5.4.3 将动态标签与提取结合使用
在 5.3.1 节中,我们看到了如何动态设置标签的解释。我们应该改进并重新运行 monitor-file-out-tag-match2.conf,以便 exec 源根据检索到的文件值设置标签。
答案
我们应该得到一个类似于 Chapter5/ExerciseResults/monitor-file-out-tag-match-Answer2.conf 的配置。注意,当我们运行这个配置时,使用 exec 源的日志事件内容将不再达到输出,因为我们已经更改了标签,所以它无法通过匹配条件。
5.5 标签插件
有可用的插件可以进一步帮助使用标签进行路由;让我们看看一些核心 Fluentd 之外的认证插件(表 5.1)。
当插件被描述为“认证”时,这意味着它们来自被 Fluentd 社区认可和信任的贡献者。由于这些插件不是 Fluentd 的核心部分,这意味着要使用这些插件,你需要安装它们,就像我们在第四章中为 MongoDB 所做的那样。
表 5.1 可帮助进行路由的基于标签的附加路由插件
| 插件名称和链接 | 描述 |
|---|---|
rewrite-tag-filtergithub.com/fluent/fluent-plugin-rewrite-tag-filter |
通过 match 指令中的一个或多个规则,插件会对日志事件应用正则表达式。然后,根据结果,标签会更改到指定的值。规则可以设置为可以选择是否将重写应用于正则表达式的真或假结果。如果成功,则使用新标签重新发出日志事件以继续匹配事件之后的过程。 |
routegithub.com/tagomoris/fluent-plugin-route |
路由插件允许标签将日志事件定向到一个或多个操作,例如操纵日志事件并将其复制以通过另一个指令拦截。 |
rewritegithub.com/kentaro/fluent-plugin-rewrite |
这使得可以使用一个或多个规则修改标签,例如如果日志事件记录的属性与正则表达式匹配。因此,根据日志事件执行特定任务变得非常容易。 |
5.6 标签:将标签提升到新的水平
正如我们将在本节中看到的那样,标签指令使用了基于标签的路由的基本思想,并将其提升到了全新的水平。理想情况下,我们应该能够清楚地将一组指令组合在一起,以区分特定的日志事件组,但这可能具有挑战性。标签允许我们克服这一点。它们有两个方面:首先,可以使用@label属性将一个额外的属性链接到日志事件,这与标签链接的方式非常相似(尽管,与标签不同,标签不是日志事件数据结构的一部分)。其次,标签提供了一个指令(<label labelName> ... </label>),我们使用它来分组其他按顺序执行的指令(例如,匹配和过滤器)。实际上,我们正在定义一个动作管道。为了在本书的其余部分区分这两个概念,我们将谈论标签作为日志事件的属性,并将指令作为将一个或多个指令链接在一起作为管道或标签管道的方式。
与标签相比,标签有一个限制。可以创建一个以逗号分隔的标签列表(例如,<match basicFile,basicFILE2>),但标签只能与该管道关联一个标签(例如,<label myLabel>)。你会发现尝试以相同方式匹配多个标签会导致错误——例如,'find_label': common label not found (ArgumentError)。这种情况发生是因为 Fluentd 确实会在启动时检查每个标签声明是否可以执行。
注意:与标签不同,命名约定通常在意义上更具有功能性。
5.6.1 使用 stdout 过滤器查看发生了什么
为了帮助说明这一点,我们将引入一个特殊的过滤器配置。与匹配指令不同,filters with stdout 的重要之处在于,即使事件满足过滤器规则,它也会由插件发出,以便被后续的内容消费。这种过滤器设置有点像开发者的println,有助于在代码开发过程中查看发生了什么。我们将在下一章更详细地探讨过滤器,但现在,让我们看看 stdout 插件在过滤器中的行为。
stdout 插件有效地接受所有事件;因此,以下过滤器将允许所有内容通过并发送详细信息到控制台:
<filter *>
@type stdout
</filter>
此配置通常被称为 filter_stdout。使用此作为额外步骤将帮助我们说明标签管道的行为。这是查看 Fluentd 配置内部发生情况的一种便捷方式。
5.6.2 标签和标签路由的示例
为了说明基于标签的管道,我们创建了一个配置,该配置跟踪两个不同的文件(来自两个不同的日志模拟器)。模拟器输出结果的配置导致两种不同的消息结构(尽管两者都源自相同的数据源)。要观察差异,请在模拟器运行后比较 basic-file.txt 和 basic-file2.txt。
此配置将说明将标签应用于一个源而不是另一个源的使用。然后,在“pipeline”标签内,一个源(文件)将同时受到 stdout 过滤器(如第 5.6.1 节所述)和与另一个文件输出分开的文件输出的影响。以下列表中展示了这一点。与其他较大的配置一样,我们用省略号替换了部分内容,以便更容易阅读配置的相关方面。
列表 5.7 第五章/Fluentd/file-source-file-out-label-pipeline.conf 标签管道
<source>
@type tail
path ./Chapter5/basic-file.txt
read_lines_limit 5
tag basicFile
pos_file ./Chapter5/basic-file-read.pos_file
read_from_head true
<parse> @type none </parse>
@label labelPipeline ❶
</source>
<source> ❷
@type tail
path ./Chapter5/basic-file2.txt
read_lines_limit 5
tag basicFILE2
pos_file ./Chapter5/basic-file-read2.pos_file
read_from_head true
<parse> @type json </parse>
</source>
#### end - tail basic-file2
<label labelPipeline> ❸
<filter *> ❹
@type stdout
</filter>
<match *> ❺
@type file
path ./Chapter5/label-pipeline-file-output
@id otherSelfOut
<buffer> . . . </buffer>
<format> . . . </format>
</match>
<match *> ❻
@type stdout
</match>
</label> ❼
<match basicFILE2> ❽
@type file
path ./Chapter5/alt-file-output
@id basicFILE2Out
<buffer> . . . </buffer>
<format> . . . </format>
</match>
❶ 我们将源附加到它创建的事件上;在这种情况下,标签为 labelPipeline。这意味着对这些事件执行的操作步骤将在
❷ 此源未标记。因此,其日志事件将被下一个可以消耗标签 basicFILE2 的匹配博客拦截。
❸ 在标签块的开始处,任何具有匹配标签的日志事件将通过此操作序列,假设处理允许事件从插件输出。
❹ 使用 stdout 过滤器将日志事件推送到 stdout 并输出到下一个插件。
❺ 使用匹配将内容定向到文件。
❻ 由于前面的匹配将消耗日志事件,因此我们永远不会看到此 stdout 过滤器的任何结果。要将日志事件发送到 stdout 和文件,需要使用复制功能。
❼ 定义事件标签系列的结束
❽ 在标签之外,匹配将应用于所有无标签事件。
要运行此配置,我们需要运行以下命令
-
fluentd -c Chapter5/Fluentd/file-source-file-out-label-pipeline.conf -
groovy LogSimulator.groovy Chapter5/SimulatorConfig/basic-log -file.properties -
groovy LogSimulator.groovy Chapter5/SimulatorConfig/basic-log -file2.properties
当运行此设置时,日志事件可以在 basic-file.txt 和控制台上看到。此外,还将有两个更多文件,因为日志内容输出到 label-pipeline-file-output.*_*.log 和 alt-file-output.*_*.log(通配符代表日期和文件增量编号)。这两个文件都不应混合标签。
虽然定义的匹配表达式在标签管道内继续使用通配符,但仍然可以在管道内的指令上应用标签控制。如果您将配置设置编辑为与
5.6.3 连接管道
随着配置变得更加复杂,您可能需要创建管道并将它们链接在一起。这可以通过使用relabel插件来完成。Relabel 做它所说的;它更改与日志事件关联的标签。由于 relabel 是一个输出插件,日志事件可以更改标签并发出日志事件而不是消费它。例如,您可能有一个包含多个指令的标签,可以将日志事件转换为人类友好的表示并发送到像 Slack 这样的社交平台。但在您使用标签做那之前,您可能希望将日志事件通过一个标记管道的过滤器,排除所有代表常规业务事件的日志事件。
随着我们的 Fluentd 配置结构随着管道变得更加复杂,可视化正在发生的事情很有帮助,如图 5.7 所示。正如您所看到的,我们现在已经将alt-file-output的匹配项改为一个名为common的标记管道。为了说明 relabel 的使用,我们原始的labelPipeline(如我们在列表 5.7 中看到的)已经被修改。我们引入了一个copy插件来确保日志事件同时发送到输出和 relabel(突出显示存储声明不仅可以用于存储插件)。当我们运行此配置时,alt-file-output文件将现在包含两个来源。

图 5.7 标签路由示例,两个标签管道连接(labelPipeline 和 common)
我们可以使用以下命令运行配置
-
fluentd -c Chapter5/Fluentd/file-source-multi-out-label-pipelines.conf -
groovy LogSimulator.groovy Chapter5/SimulatorConfig/basic-log -file.properties -
groovy LogSimulator.groovy Chapter5/SimulatorConfig/basic-log -file2.properties
下面的列表显示了应用relabel的配置。注意再次使用省略号,这样您可以专注于关键元素。
列表 5.8 Chapter5/Fluentd/file-source-multi-out-label-pipelines.conf 中使用 relabel
<source>
@type tail
path ./Chapter5/basic-file.txt
read_lines_limit 5
tag basicFile
pos_file ./Chapter5/basic-file-read.pos_file
read_from_head true
<parse> . . . </parse>
@label labelPipeline ❶
</source>
<source>
@type tail
path ./Chapter5/basic-file2.txt
read_lines_limit 5
tag basicFILE2
pos_file ./Chapter5/basic-file-read2.pos_file
read_from_head true
<parse> . . . </parse>
@label common ❷
</source>
<label labelPipeline>
<filter *>
@type stdout
</filter>
<match *>
@type copy ❸
<store>
@type file ❹
path ./Chapter5/label-pipeline-file-output
<buffer> . . . </buffer>
<format> . . . </format>
</store>
<store> ❺
@type relabel ❻
@label common ❼
</store>
</match>
</label>
<label common> ❽
<match *>
@type file
path ./Chapter5/alt-file-output
<buffer> . . . </buffer>
<format> . . . </format>
</match>
</label>
❶ 这将此来源的日志事件链接到一个标签,就像我们在前面的例子中所做的那样。
❷ 将第二个来源设置为使用 common 标签而不是信任指令捕获日志事件
❸ 在 match 中使用 copy 插件,我们可以使日志事件被多个输出插件消费。
❹ 日志事件将首先被推送到文件输出插件。
❺ 由于使用了 copy 指令,我们可以强制日志事件被进一步的操作处理。
❻ 使用 relabel 插件更改日志事件的标签,直到此操作为'labelPipeline'
❼ 标签现在设置为 common;当我们离开这个匹配和标签块时,事件将被名为 common 的标签指令消费。
❽ 标签 common 开始,现在将接收来自两个来源的日志事件。一个来源的事件直接到达,另一个通过 labelPipeline 到达 common 管道。
5.6.4 标签排序
与标签不同,标签指令的相对位置并不重要,以我们当前在图 5.7 中显示的配置为例。虽然labelPipeline将触发使用common标签的重新标记,但common标签可以在labelPipeline之前声明。标签管道内的步骤在执行时仍然是顺序的。您可以通过提供的配置文件Chapter5/Fluentd/file-source-multi-out-label-pipelines2.conf看到并尝试这一点。在配置中,您可以看到
-
一个曾经用于通用但已更改为使用标签
outOfSequence.的重新标记声明。有了它,我们将过滤器移动到了新的标签部分。 -
outOfSequence标签管道随后重定向到通用,就像我们之前所做的那样,如图 5.8 所示。配置文件的实际顺序反映了从左到右阅读图中的外观(忽略图中显示的流程)。

图 5.8 此配置说明标签不是顺序敏感的。
该场景可以使用以下命令执行
-
fluentd -c Chapter5/Fluentd/file-source-multi-out-label-pipelines2.conf -
groovy LogSimulator.groovy Chapter5/SimulatorConfig/basic-log-file-small.properties -
groovy LogSimulator.groovy Chapter5/SimulatorConfig/basic-log-file2-small.properties
模拟器属性已配置为使用小数据集,以便更容易确认,并且我们没有得到任何意外的循环。
注意:虽然我们已经说明了在不按顺序标记部分是可能的,但我们并不一定提倡这种做法。从某些方面来看,标签可以与goto语句相提并论,因此最好尝试将配置文件结构得尽可能线性。
5.6.5 特殊标签
Fluentd 的标签功能包括一个预定义的@Error标签。这意味着我们可以使用标签指令来定义一个处理错误的管道;这些错误可以在我们的配置中或在执行配置的插件中引发。这确实依赖于插件实现使用特定的 API(emit_error_event)。我们可以确信核心 Fluentd 插件实现将使用此 API,但检查第三方插件是否使用此功能而不是简单地写入 stdout 可能是有价值的。我们将在本书后面查看构建我们自己的插件时看到这一点。
因此,我们可以基于现有的 Fluentd 配置步骤来捕获这些错误。有了这个,我们可以做一些事情,比如重新标记日志事件,使其被常见的管道捕获,或者简单地将其直接发送到其自己的目的地。在下一个示例中,我们向之前的配置中添加了一个新的标签管道,该管道将写入其自己的文件,如下面的列表所示。
列表 5.9 使用@Error 的 Fluentd/file-source-multi-out-label-pipelines-error.conf
<source> . . . </source>
<source> . . . </source>
<label labelPipeline> . . . </label>
<label common> . . . </label>
<label @Error> ❶
<match *> ❷
@type file
path ./Chapter5/error-file-output
<buffer> . . . </buffer>
<format>
@type out_file
delimiter comma
output_tag true
</format>
</match>
</label>
❶ 使用预定义的 @Error 标签开始管道。因此,任何设置此标签的插件都将在此管道中处理其错误。
❷ 匹配所有标签,一旦日志事件被标记为错误,就将其写入文件。但我们可以变得聪明一些,对不同标签关联的错误进行不同的处理。例如,需要更多紧急关注的特定标签错误可以发送到通知或协作工具。
小贴士:创建一个简单的通用管道来处理错误总是很有用的。该管道会引发某种问题票、社交通知(例如 Slack、Teams)或简单地发送电子邮件来通知 Fluentd 配置中的事件。然后,在所有配置中作为标准实践使用 include。因此,如果您没有特定的错误处理配置,则通用的答案将为您启动。
5.6.6 将通用管道投入实际应用
您被要求重构列表 5.8 中引用的配置,使其具有单个管道。这将允许通过包含将其服务纳入主配置。此更改将允许其他团队开发的所有不同日志事件路由更安全地进行。为了帮助这些团队,您应该创建一个包含空输出作为模板的额外管道。
答案
结果实际上涉及三个文件。核心是 Chapter5/ExerciseResults/file-source-multi-out-label-pipelines-Answer.conf。它使用 @include 引入配置 Chapter5/ExerciseResults/label-pipeline-Answer.conf,其中包含重构的逻辑,以及一个包含由 Chapter5/ExerciseResults/label-pipeline-template-Answer.conf 定义的空输出的模板。
摘要
-
Fluentd 提供了一个空输出插件,可以用作输出插件的占位符。该插件将简单地删除接收到的日志事件。
-
Fluentd 提供了一个 exec 插件,允许我们将外部进程的触发(如脚本)纳入配置。外部进程可以使用日志事件的参数来调用。
-
Fluentd 提供了多种机制来通过配置路由日志事件。其中最简单的是使用与每个日志事件关联的标签。
-
对于更复杂的路由,我们希望有一个“管道”式的动作,Fluentd 提供了标签构造。标签的使用还可以帮助我们简化配置文件中的配置值排序的复杂性。
-
Fluentd 通过使用我们可以分组和
namespace标签的约定来提供命名约定。我们可以在配置中使用通配符来分组标签。 -
Fluentd 配置可以通过使用
@include指令从多个文件组装。 -
如果 Fluentd 发生错误(例如,无法连接到 Elasticsearch 的实例),我们可以捕获错误并使用自定义
@Error标签执行操作。
6 过滤和扩展
本章涵盖
-
应用过滤器来控制日志事件
-
实现 record_transformer 过滤器
-
从日志事件中提取信息
-
将环境信息注入到日志事件中
-
隐藏日志事件的元素以维护数据安全
在第五章中,我们提到了使用 filter 指令将日志事件发送到标准输出。虽然这种方式使用过滤器是有帮助的,但它几乎只是指令全部功能的附属品。filter 指令可以帮助我们
-
过滤掉特定的日志事件,以便只有特定的日志事件发送到特定的消费系统
-
过滤掉日志事件消息的特定部分,并允许我们将它们记录为日志事件的独特属性(最终使应用该数据中的逻辑更容易)
-
通过修改标签和时间戳来丰富日志事件,以反映日志事件记录本身的动态内容(例如,调整事件的上游缓存)
-
进一步丰富日志事件;例如
-
使用基于公共 IP(称为 GeoIP)的插件添加地理位置信息
-
通过识别日志事件中的信息来附加错误指导(例如,如果错误代码是通用的,但生成该代码的路径与某些内容匹配,则对日志进行注释,例如“根本原因是数据库连接错误”)
-
添加上下文信息,这有助于你以后进行进一步分析(例如,Fluentd 的 worker_id 和 server_id)
-
-
应用更改以解决安全考虑(例如,匿名化、屏蔽和删除任何敏感数据,这些数据可能已进入日志事件)
-
从日志事件及其上下文中计算或外推新数据(例如,取两个时间戳并计算经过的时间)
-
过滤掉确认一切按预期运行的日志事件
本章将探讨为什么我们可能想要过滤日志事件,以及如何配置过滤器来完成这项工作。由于过滤器可以用来操纵事件日志,我们将探讨如何进行操作,是否应该这样做,以及为什么我们可能想要这样做。
6.1 过滤器的应用
我们刚刚简要概述了使用过滤器可能性的广泛性;让我们深入研究一些这些应用,以更好地理解为什么我们可能想要使用它们。
6.1.1 所有正常事件不需要分发
许多日志信息实际上会向我们表明事情正在按预期运行。获取这些事件很重要;正如著名的管理顾问和作家彼得·德鲁克所说:“你不能管理你无法衡量的东西。”也许更加相关的是,联合国秘书长达格·哈马舍尔德(经济学家)说:“一位好护士的持续关注可能和一位外科医生的重大手术一样重要。”换句话说,我们需要积极观察、量化并评估状态,以确保一切正常。这种持续、稳定的观察将使我们能够进行小的调整,以保持一切良好,而不是需要技能但重大的改变。
但我们不需要与每个人分享每个确认一切正常的日志事件。就像心脏监测器一样,当事情不正常时,所有的警报和信号都会响起,以确保每个人都意识到需要帮助。如果一切都在预期参数内,数据不会超出监控器的显示范围。例如,Elastic Beats 可以生成心跳日志事件,如2017-12-17T19:17:42.667-0500 INFO [metrics] log/log.go:110 Non-zero metrics in the last 30s: beat.info.uptime.ms=30004 beat.memstats.gc_next=5046416。这可能是不需要保留的日志消息,或者如果保留,也不需要分发,而是本地记录一段时间。
如果我们收到表示一切正常且不太可能提供更多洞察力的日志事件,我们是否需要将这些事件传播到下游系统?这意味着将更容易看到重要事件。通过过滤掉日常信息,我们也在控制成本。物理基础设施在需要更多硬件之前可以传输的最大数据量是有限的。我们根据带宽(即数据量)支付公共网络容量,因此消耗带宽分发每个“心跳”日志事件可能会积累成本,而收益却很少——更多的网络硬件、更多的带宽等等。在过去的几年里,观察到从云平台出口数据成本可以影响商业决策。换句话说,将数据从一个云推送到另一个位置需要花钱,而且这种成本可能会变得相当高。但我们不希望切断那百分之一的重要事件,这些事件值得传输。
6.1.2 在 haystack 中找到针
过滤可以用来隔离那些看似无害的事件,这些事件是即将到来的更严重问题的警告。这些情况发生在有人错误地将应该是一个警告日志事件分类为信息性或甚至调试性事件时。当你无法让日志生成更多有用的事件时(例如,现成的软件、无人愿意接触的遗留解决方案),能够识别和标记这类事件的能力很重要。
6.1.3 虚假紧急情况
总有一天,我们会遇到一个警告或错误日志发生的情况,问题会升级到管理层。关于必须优先解决的所有其他优先级的问题开始“大声疾呼”。但最终,问题的后果及其影响并不需要放弃一切;是的,发生了错误,但这并不是世界末日。所检测到的是一个本可以通过日常运营任务处理的问题。通过过滤器,我们可以定义规则,帮助我们区分“世界末日”事件和“请在登录时修复我并相应地指导信息”的事件。
更好的是,如果存在解决已知问题的操作步骤,我们将修复过程的引用添加到日志事件中。因此,当警报被触发时,它们已经与修复信息相关联。避免了不必要的升级,没有采取可能加剧问题的行动,等等。
6.1.4 重新分级
之前的应用场景是当日志事件可以被生成并标记为比其应有的日志级别更高的级别时——例如,使用Error而不是Warning或Info。和之前一样,如果人们无法或不愿意修复这个问题,那么我们可以在日志事件传递过程中修改它。这是通过操作日志事件来改变记录的日志级别到一个不那么令人警觉且更准确的分类来实现的。或者,通过给日志事件添加带有注释的额外属性来标记,这表明这是一个已知的错误日志级别。
6.1.5 未实现的日常维护
只要软件开发存在,业务驱动因素就会优先考虑功能性能力,而不是非功能性能力,如日常维护(例如存档或删除处理过的文件文件夹等)。当这是一个遗留应用程序的特征时,人们害怕改变任何东西以改进系统,比如自己清理,这种情况并不罕见。典型的结果是,常规支持流程是手动完成的,然后我们可以通过脚本自动化,只需在特定情况下运行。过滤掉日志中指示需要执行日常维护任务的指标(例如,Fluentd 捕获与磁盘空间相关的日志事件)是触发执行日常维护任务的小步骤。
6.2 为什么更改日志事件?
一些过滤器允许我们修改日志事件。我们为什么应该考虑这一点,这种能力如何帮助我们?有些人可能会争论,修改日志事件也是在篡改“原始真相”,那么我们甚至应该允许它吗?
6.2.1 更容易处理下游的意义
当我们处理日志事件时,我们通常需要从提供的日志中提取更多意义。日志事件是无结构的、半结构的,甚至是有结构的,但需要重新解析为合适的数据结构(例如,读取 JSON 文本文件)。结构可以帮助过滤、路由、创建新的报告指标,并使用日志事件数据进行度量。一旦我们投入了从日志事件中提取意义的努力,为什么不使其易于在下游重复使用呢?换句话说,应用 DRY(不要重复自己)的原则。因此,如果您已经提取了意义和结构,不要让人们以后再重复做这件事。只需将派生信息与日志事件一起传递。
6.2.2 添加上下文
为了正确处理事件,我们可能需要额外的上下文。当试图诊断为什么应用程序表现不佳时,查看事件周围发生的事情并不罕见——例如,服务器是否运行了大量的线程?有时很容易将此类上下文数据与日志事件关联起来。将额外上下文关联起来的最简单方法是将它添加到日志事件中。
6.2.3 记录我们何时对日志事件做出反应
我们已经提到了由于日志事件而采取某种行动的可能性。回顾起来,了解哪些事件触发了行动可能是有帮助的。向触发日志事件添加信息可能是一种更直接、更可接受的做法,而不是在之后关联单独的日志事件来显示因果关系。
6.2.4 数据编辑/掩码
当我们在开发软件时,在开发阶段记录正在处理的数据对象通常很有帮助。在开发和测试阶段这并不是问题,因为这只是测试数据。但如果数据包含敏感信息,例如可以用来识别个人的数据(PII,个人可识别信息),例如在医疗保健或信用卡使用中,这可能会成为一个挑战。任何处理此类数据的 IT 系统部分都将成为许多法律、立法和合同技术要求的对象。这些要求来自国际、国家和地区的数据法律,例如
-
GDPR(通用数据保护条例)
-
HIPAA(健康保险可携带性和问责制法案)和其他医疗保健立法
-
PCI DSS(支付卡行业数据安全标准)
您可以将此列表扩展到许多公司可能也希望以相同敏感性处理一些财务会计数据。明显的解决方案是修复软件,使其不记录数据或限制此类记录的影响,从而限制需要应用额外极其严格的控制、安全机制和报告的“爆炸半径”。Fluentd 提供了一个很好的方法来解决这个问题:
-
从日志中删除或红字/掩码数据。掩码通常是通过用无意义的值替换敏感值来完成的。红字是通过从通信中删除信息,或者简单地确保日志中不显示信息等方式来移除信息。我们可以看到在支付卡收据上用星号或哈希字符替换你的卡号来掩码数据。只要不能通过掩码恢复原始数据,任何掩码方法都可以使用。
-
将 Fluentd 与日志源协同定位,以限制需要满足提高数据安全要求的基础设施数量。提高安全范围的越小,“攻击面”就越小(即,可能受到恶意攻击尝试获取数据的服务器和软件组件数量就越少,越好)。
-
使用RPC(远程过程调用)技术直接将主应用程序的日志连接到 Fluentd,而不是使用日志文件,这样日志事件就是瞬时的。我们将在第十一章中看到更多关于直接将应用程序连接到 Fluentd 的内容。
安全不是成本
很容易误解这里所说的内容,得出结论认为安全是不受欢迎的成本,避免安全是好的。但现实是,今天,安全应该被视为一种资产,安全的应用是积极的卖点。像 Oracle 这样的 SaaS 解决方案提供商确实将他们的安全作为一项优点。数据丢失的成本影响,尤其是当影响程度没有限制或理解时,很容易超过没有投资于防范风险的节省。但潜在的影响范围越小,越好。如今,安全漏洞(恶意或意外)是“何时”的问题,而不是“是否”的问题。谚语“最坏打算,最好希望”非常恰当。
6.3 应用过滤器和解析器
在本节中,我们将探讨过滤器和解析器的实际配置和使用,以
-
管理日志事件的路由
-
操作日志事件
要操作日志事件,我们可能需要从中提取或赋予它们一些意义。为了提取这种意义,我们需要解析非结构化日志事件内容,因此我们将涉及到解析器的使用。
6.3.1 过滤器插件
作为指令的过滤器就像一个match,因为指令可以在声明中包含标签(例如,<filter myApp>或<filter *>)。区别在于,如果日志事件符合过滤器表达式,而不是日志事件被消费,它可以通过配置的下一部分而不需要复制操作,就像在第三章中用match指令所示。
在 Fluentd 核心中包含以下过滤器插件:
-
record_transformer—这是内置过滤器中最复杂的;同时也提供了一系列选项来操作日志事件。
-
grep—提供了定义关于日志事件属性的规则以从事件流中过滤掉它们的手段。可以提供多个表达式来定义累积规则。
-
filter_parser—结合了解析器插件的功能和过滤器。
-
stdout—我们已看到这个插件的工作。每个事件都可以通过过滤器,但也会写入 stdout。
Fluentd 内置了一组核心过滤器插件;除此之外,还有社区提供的过滤器插件。附录 C 包含了我们认为可能特别有帮助的附加插件详细信息。
6.3.2 应用 grep 过滤器
grep 解析器允许我们定义一个搜索表达式并将其应用于日志事件中的命名属性。例如,我们可以扩展我们的路由,使得具有日志条目的事件明确地引用文本中的计算机。这是以下场景的基础;虽然计算机引用相对无意义,但我们可以很容易地用已编目错误代码的引用替换或扩展它。例如,WebLogic 通知以 BEA-000 开头。
当我们在演示过滤器使用时,让我们使用一个不同的输出插件。第一章介绍了 EFK(Elasticsearch、Fluentd、Kibana),因此我们将把 Elasticsearch 加入到混合中,以展示更多这个堆栈(附录 A 提供了如何安装 Elasticsearch 的说明)。我们将使用的 Fluentd 配置如图 6.1 所示。

图 6.1 过滤器和 Elasticsearch 作为输出应用
我们可以使用 grep 插件来应用过滤器,该插件将执行一个正则表达式,其结果可以以二进制方式处理。结果将决定是否存储日志事件。所有这些操作都是通过将指令设置为 regexp 来完成的。我们需要定义一个键,即要检查的日志事件的元素。在这种情况下,我们想查看名为 msg 的核心日志事件。一旦我们确定了查看的位置,我们需要提供一个模式供正则表达式解析器查找。将此与属性名称结合,我们得到
<regexp>
key msg
pattern /computer/
</regexp>
定义了过滤器后,我们需要将任何匹配的日志事件发送到我们的 Elasticsearch 安装。我们使用 match 指令和一个 @type 值为 elasticsearch 来完成此操作。Elasticsearch 插件具有极高的可配置性,超过 30 个属性,涵盖了从缓存控制到确定日志事件如何在 Elasticsearch 中填充和索引等行为。我们不会涵盖所有这些,因为这将导致一本解释 Elasticsearch 的书,而对于那本书,你最好阅读 Radu Gheorghe 等人所著的 Elasticsearch in Action(www.manning.com/books/elasticsearch-in-action);然而,我们应该简要介绍你可能会遇到的最常见的属性。
与 MongoDB 连接一样,必须提供详细信息以解决服务器(host 和 port 属性)。可能需要访问凭证(user 和 password)。由于我们没有使用开箱即用的部署设置任何此类限制,因此我们不需要提供它们。通信方案或类型,如 http 或 https,将决定是否需要更多信息(例如,可以使用证书的位置);端到端 SSL/TLS 总是良好的安全实践。
一旦定义了连接到 Elasticsearch 的方式,我们需要声明数据在 Elasticsearch 内部的位置(index_name)以及要提供的数据,例如是否将标签值包含在核心日志事件记录中(include_tag_key, tag_key)。记住,我们还设置了正在传递的数据的表示方式。由于 Elasticsearch 和 Logstash 之间的关系,插件允许我们告诉 Fluentd 将日志事件呈现为 Logstash 会将属性 logstash_format 设置为 true 是不足为奇的。
Elasticsearch 插件还利用辅助缓存插件;因此,我们需要考虑这可能会如何影响行为。为了方便和速度,让我们使用内存缓冲区,通过使用属性 flush_interval 5s 每隔 5 秒刷新一次。此配置可以在以下列表中看到。
列表 6.1 Chapter6/Fluentd/file-source-elastic-search-out.conf
<filter *> ❶
@type grep ❷
<regexp> ❸
key msg
pattern /computer/
</regexp>
</filter>
<match *> ❹
@type elasticsearch
host localhost
port 9200
scheme http ❺
reload_on_failure true
index_name fluentd-book ❻
logstash_format false ❼
include_tag_key true ❽
tag_key key
<buffer>
flush_interval 5s
</buffer>
</match>
❶ 允许过滤器处理任何标签
❷ 定义过滤器类型
❸ 定义应用于字段的正则表达式以及表达式,它需要产生二进制结果
❹ 所有通过过滤器的日志事件现在将由配置为写入文件的此匹配处理。
❺ 虽然我们不需要明确设置方案,因为它默认为 http 而不是 https,但包括它来提醒我们正在使用的低安全阈值是值得的。您还可以包括用户名和密码,如注释所示。
❻ 定义要使用的索引;如果未指定,则默认为 Fluentd
❷ 告诉 Elasticsearch 将数据添加到命名索引,而不是使用时间戳名称创建新的索引,就像 Logstash 连接那样
❽ 显示我们正在告诉 Elasticsearch 插件将日志事件标签包含在要存储的数据中,并给它命名为 key,如下一个属性所示
让我们看看配置的结果。由于这使用的是文件源,我们需要同时运行 LogSimulator。假设 Elasticsearch 也正在运行并已准备好,以下命令是运行示例所需的:
-
fluentd -c ./Chapter6/Fluentd/file-source-elastic-search-out.conf -
groovy logSimulator.groovy ./Chapter6/SimulatorConfig/log-source-1.properties
我们可以通过 UI 工具验证 Elasticsearch 中的记录,通过查看索引内容来实现,我们将其配置为fluentd-book。(附录 A 也涵盖了为该目的设置 Elasticvue 的过程。)你应该会发现索引包含了我们发送到 stdout 的相同日志事件。
应该过滤日志事件吗?
我们可以更改日志事件的想法可能是一个有争议的话题。如果您更改原始日志事件,您是否在修改原始真相?用一个电视侦探的类比来说,篡改原始日志事件就像篡改犯罪现场。Fluentd 不应该像处理证据的保管链一样处理日志事件吗?一般来说,我会同意原始日志事件应该保留未做修改。然而,我们经常需要将附加信息关联到某个证据(继续我们的类比,弹道报告将被附加到相关武器上)。与其试图保持细节分离,不如仔细地附加细节可能更有帮助。
在现实世界中,我们使用的指导原则是保留日志事件的副本,不做任何修改,只有一个例外——信息安全。如果您需要屏蔽或删除数据,请考虑在安全的地方保留一份未受污染的副本,以便在必要时追溯。然后,任何经过处理、提取的值都可以与原始值一起保留。您可能考虑采用命名约定,这样当日志事件的这些元素被处理、构建或丰富时,起源就清晰了。
6.3.3 使用 record_transformer 插件更改日志事件
使用过滤器根据日志事件的内容控制哪些日志事件被处理或不被处理,这解决了许多之前描述的场景。修改日志事件以添加额外的上下文信息、派生值,或以更有意义和可用的方式提取和记录日志事件值,有助于解决其他提到的场景。
为了说明这如何工作,我们将在我们的日志事件中添加新的字段,除了标准字段之外,具体如下:
-
一个名为
computer的字段,包含运行 Fluentd 的主机名称。 -
将
processed-前缀应用于标准*message*,以说明现有值的修改。 -
示例日志消息包含一个包含
name属性的 JSON 结构,该属性由firstname和surname组成。这种组合可能会使日志数据对 PII 规则敏感,因为它引用了一个可识别的个人。我们将提取firstname并创建一个新的日志事件属性from,并删除姓氏来解决这个问题。新的属性from可能没有理由;它确实允许我们看到如何复制元素。
我们日志事件的消息是结构化的,当接收时,将看起来像
{"msg": "something about computers",
"name":
{
"firstname": "Computer",
"surname": “AI”
},
"age": 404
}
记录指令
过滤器定义的关键部分是record指令。指令中的每一行代表一个字段名称和字段值。例如,如果我们想添加一个名为myNewField的新字段,其字面值为aValue,那么我们就会按照以下方式配置指令:
<record>
myNewField aValue
</record>
仅包含设计时字面值并不能提供太多价值。为了告诉 Fluentd 它需要处理一个派生值,我们需要在${}内包装表达式。我们可以通过在括号内放置名称来引用日志事件中的其他字段(例如,${msg})。要访问日志事件消息,我们使用record["<字段名称>"]的表示法(例如,record["msg"])。Record是对一个函数的引用,该函数可供使用。
在${}字符内,我们被允许使用 Ruby 对象、函数和操作符的小子集,包括 Fluentd 提供的一些。为了允许这样做,我们可以在过滤器属性enable_ruby中包含一个属性;当它设置为true时,它将允许使用 Ruby 语言的全部功能。这默认为false,因为它会给解析器带来更多的工作,例如确保它可以解决依赖关系等;为了保持效率,最好除非必要,否则不要设置为true。
访问嵌套的 JSON 元素
要获取firstname元素,我们需要在日志事件的“消息”部分中导航 JSON 结构。这可以通过标准record方法完成——例如,record["name"]["firstname"]——这将遍历到firstname作为一个子属性,但需要该属性存在。如果结构的一部分是可选的,这可能会成为一个问题,因为路径中缺失的任何部分都会触发运行时错误。另一种方法是使用由record操作符提供的名为dig的函数。语法非常相似;然而,如果路径不存在,则提供一个 nil 结果而不是错误。dig函数是record.dig("msg", "name", firstname``)。这确实需要将*enable_ruby*设置为工作状态。
删除 JSON 元素
record_transformer 包含几个属性,允许控制日志事件元素的组成。这可以通过在配置中使用可选属性来删除列表元素(remove_keys)或定义哪些元素(除了像标签这样的强制元素)应该保留(keep_keys)来实现。这包括遍历 JSON 结构的表示法(这也在插件的其它部分中工作)。配置中属性的顺序很重要。在我们的例子中,remove_keys 属性需要在 record 指令之后出现;否则,我们可能会发现自己没有元素可以复制。要删除结构中的特定元素,我们使用具有通过对象路径的 remove_keys 属性,例如 $.name.surname。在表示法中,$(美元符号)实际上代表了日志事件的根。然后,使用点表示法跟随属性名称来遍历结构。这确实回到了我们是否可以信任路径存在的先前的点。单个 remove_keys 属性可以通过将其作为逗号分隔的列表来扩展到更多元素;例如,remove_keys $.name.surname, $.name.anotherName, $.somethingElse。
值替换
记录操作符包含一个函数,允许我们替换 JSON 元素中的值。这在掩码数据和纠正值时是必要的,例如在 6.1.4 节中描述的错误消息级别。这是通过引用元素名称然后调用函数 gsub 并跟随包含要替换的值及其替换值的参数来完成的。例如,在我们的数据集中,msg 包含一些 'I' 的出现。使用表达式 ${record["msg"].gsub('I', 'We')},可以将 'I'替换为'We'。在下面的列表中,我们已经包含了这个表达式。我们不是用替换后的字符串替换 msg`,而是添加了一个新属性,以便于比较。
列表 6.2 Chapter6/Fluentd/file-source-transformed-elastic-search-out.conf
<filter *>
@type record_transformer
enable_ruby true ❶
<record>
computer ${hostname} ❷
from ${record.dig("name", "firstname")} ❸
msg processed ${record["msg"]} ❹
msg_gsub ${record["msg"].gsub('I ', 'We ')} ❺
</record>
remove_keys $.name.surname ❻
</filter>
<filter *>
@type stdout
<inject>
worker_id_key ❼
</inject>
</filter>
❶ 启用 Ruby 支持记录.dig 方法来定位值
❷ 使用已知的上下文值添加一个属性
❸ 通过查找子元素并检索其值来创建一个新值
❹ 通过添加文本内容修改 msg 元素
❺ 执行将 'I' 替换为 'We' 的字符串替换。包含一个空白字符以避免意外选中字符,换句话说。
❻ 删除姓氏元素以确保我们不会因为 PII 考虑而处于风险之中
❽ 这里显示的 inject 指令允许将进程的 worker_id 添加到日志事件中。inject 指令允许添加一些有用的值以提供额外的上下文。
让我们看看配置的结果。由于这使用了文件源,我们需要运行 LogSimulator。因此,要运行示例,需要以下命令:
-
fluentd -c ./Chapter6/Fluentd/file-source-transformed-elastic-search-out.conf -
groovy logSimulator.groovy ./Chapter6/SimulatorConfig/log-source-1.properties
在开始 UI 以审查存储在 Elasticsearch 中 fluentd-book-transformed 索引中的日志事件之前,由于 stdout 过滤器,record_transformer 和 inject 指令的更改应该在控制台上可见。
预定义值
record_transformer 还通过提供一些预定义值来帮助,包括
-
主机名—计算机主机的名称
-
时间—当前时间
-
标签—当前日志事件的标签
启用 Ruby 标志后,我们可以通过使用 ${} 扩展获取和设置值的能力,以访问任何公共类方法。例如,在类中使用方法将使用 "${#.}",其中 <class> 是 Ruby 类的名称,而 <method> 是相应的公共类方法。"#{Dir.getwd}" 将检索当前工作目录。
6.3.4 过滤器解析器与记录转换器
record_transformer 插件为我们提供了将日志事件作为 JSON 有效负载工作的手段。如果日志事件只是一个简单的文本块,我们可能需要解析它以获取有意义的值。在第三章中,我们介绍了使用解析器从日志事件中提取意义的方法。我们看到的解析器,如 regexp,也与 filter 指令一起工作。因此,当 regexp 表达式 定义了要捕获为命名值的字符串部分时,解析器的行为被扩展,使得命名元素将成为顶级日志事件属性。
让我们将相同的表达式(包含在以下列表中)放入 filter 指令的上下文中。这里的基本区别是我们需要在解析器定义之前告诉 Fluentd 要处理哪个日志事件属性。这意味着我们可以针对日志事件的一个特定部分。如果我们想的话,我们还可以使用连续的过滤器来分解嵌套结构。在列表 6.3 中,我们期望输出产生额外的属性,称为 time、level、class、line、iteration 和 msg。像 record_transformer 插件一样,我们可以使用 reserve_data 配置元素确定处理的日志事件属性是否保留。我们可以通过添加 remove_key_name_field 并将其设置为 true 来使控制更加细致;如果解析过程成功,Fluentd 将仅删除原始属性。
列表 6.3 Chapter6/Fluentd/rotating-file-read-regex.conf—解析提取
<filter>
@type parser
key_name log ❶
reserve_data true ❷
<parse>
@type regexp
expression /(?<time>\S+)\s(?<level>[A..Z]*)\s*(?<class>\S+)[^\d]*(?<line>[\d]*)\-(?<iteration>[\d]*)\)[\s]+\{"log":"(?<msg>.*(?="\}))/
time_format %Y-%m-%d--%T
time_key time
types line:integer,iteration:integer ❸
keep_time_key true
</parse>
</filter>
❶ 识别要解析的日志事件属性
❷ 告诉 Fluentd 保留现有值,因此如果有更多属性,我们可以在下游检索它们
❸ 告诉 Fluentd 提取的值应该是什么数据类型,这使得进一步的转换更容易
6.4 使用 stdout 展示变更影响
由于生成日志的应用程序已经需要被安全锁定以限制记录此信息的影响,Fluentd 的安装将与源一起部署。由于操作日志事件是不被推荐的,因此已经决定
-
将其添加到 Fluentd 配置中,以便 stdout 输出显示未修改的日志事件,这样它们可以在一个受控但短暂的环境中观察到。
-
允许经过脱敏处理的修改后的日志事件进入 Elasticsearch。
Chapter6/Fluentd/file-source-transformed-elastic-search-out.conf 是进行必要更改的起点。
6.4.1 使用 stdout 展示更改影响的解决方案
您可以将您的配置修改与我们在 Chapter6/ExerciseResults/file-source-transformed-elastic-search-out-Answer.conf 中展示的解决方案实现进行比较。基本更改是带有类型设置为 stdout 的过滤器的位置。
6.5 从日志中提取设置键值
有时我们需要聪明一些,更动态地设置日志事件的次要属性(时间和标签)。这可能是因为我们不希望在标签配置中有一个静态值(我们通常设置为反映源),而是希望它动态地反映日志事件的属性。通过这样做,我们为自己设置了一个更有效地使用匹配表达式的过滤环境。例如,我们希望日志事件的标签反映微服务的名称,但我们从 Kubernetes 级别的 stdout 收集日志事件,因此单个源将反映多个服务。因此,我们需要从日志事件记录中提取作为标签所需的价值。
当涉及到时间戳时,我们可能希望将其调整为日志事件中的时间值,反映事件发生的时间而不是 Fluentd 捕获日志事件的时间。这可能是有必要的,因为事件生成和 Fluentd 获取它之间存在一些延迟。
extract 功能允许我们执行此类任务。与我们的过滤器和解析器不同,extract 指令可以集成到 source、filter 和 match(输出)插件,如 exec。extract 机制在用途上非常灵活,但它仅限于仅操作标签和时间日志事件属性。
extract 参数允许我们声明如何解释时间值。要使用的时间值可以是自纪元(1970 年 1 月 1 日午夜)以来的秒数。例如,1601495341 代表 2020 年 9 月 30 日星期三 19:49:01。另一种可能的格式是 ISO 8601 标准(更多信息请参阅 www.w3.org/TR/NOTE-datetime)。
让我们考虑一个简单的例子。我们应该使用 exec 源插件获取的值来设置标签。就像我们之前对 exec 的使用一样,我们选择了一个简单且易于使用或适应不同操作系统的命令。我们还得到了一个结构化对象,因此无需在检索值之前解析有效负载。source 指令需要将 tag 属性设置为不会来自我们的 exec 插件的值。
为了确保我们可以看到数据变化的影响,让我们将 run_interval 设置为 10 秒。这意味着相同的文件将被捕获作为输入,但给我们时间在执行之间保存对文件的更改。尝试在运行配置时更改文件。我们已经告诉解析器确保 exec 命令被视为一个 JSON 对象。
最后,我们在提取中包含了 tag_key 属性;这告诉 Fluentd 哪个日志事件元素应该被检索并用于设置标签。我们将日志事件元素的副本复制到另一个标签中,以保留原始日志事件记录。此属性称为 keep_tag_key, 我们选择保留捕获的有效负载未做修改。这在下述列表中演示。
列表 6.4 Chapter6/Fluentd/exec-source-extract-stdout.conf
<source>
@type exec
command more TestData\exe-src.json
run_interval 10s ❶
tag exec
<parse>
@type json
</parse>
<extract> ❷
tag_key msg ❸
keep_tag_key true ❹
</extract>
</source>
<match *>
@type stdout
</match>
❶ 保持源捕获迭代,以便我们可以修改有效负载并看到后果
❷ 提取指令
❸ 识别将用作标签的元素名称
❹ 表示我们希望保留检索到的日志事件内容未做修改
要运行此配置,我们只需使用命令 fluentd -c ./Chapter6/Fluentd/exec-source-extract-stdout.conf 运行 Fluentd。一旦 Fluentd 开始运行,更改 TestData\exe-src.json 文件中的值,并查看更改如何影响标签。
6.6 使用 record_transformer 派生新的数据值
通过能够排除后续操作中的日志事件并从日志事件中提取特定值,我们现在可以考虑生成派生值和指标的可能性。例如,我们可能想了解错误发生的频率,或者哪些组件甚至哪些代码库的部分是错误的主要来源。虽然使用 Elasticsearch 或 Splunk 生成此类度量是可能的,但它们是在事件分析之后使用的。如果我们想更加主动,我们需要在事件发生时更动态地计算这些指标。
在第一章中,我们介绍了监控既包括基于文本的内容,也包括数值指标。日志事件和指标通常,但不总是,在基于时间的环境中使用(日志事件按时间顺序显示,指标通常测量诸如在一定时期内的值,如每秒的 CPU 使用情况等)。Fluentd 的核心没有生成基于时间序列的指标的能力。然而,一些由社区编写的插件,包括 Fluentd 核心的贡献者,可以提供一些基本的数值和时间序列度量。正如我们稍后将要看到的,有方法可以处理时间序列数据。
时间序列数据点并不是唯一有价值的数值数据,可能有助于解决问题。例如,警报是如何被触发的,可能是交易价值(单位价值×数量)的函数,或者系统尝试与数据库连接失败的时间或次数(当前时间-原始错误时间戳)。record_transformer可以通过取数据值并执行数学运算来生成数值指标值。
使用我们的示例数据集,我们可以考虑用出生年份替换年龄,因为表达式将是当前年份减去年龄。例如:
<record>
birthYr ${Date.today.year - record['age']}
</record>
虽然这可能不是一个非常贴近现实世界的例子,但它确实展示了可能性的艺术。
注意:引用属性需要谨慎进行。如果使用不当,你可能会遇到一些日志事件属性被找到而其他属性没有被找到的奇怪行为。当使用record['xxxx']方法时,需要使用单引号。使用双引号是必要的,即使用 dig 方法,即record.dig("xxxx")。
6.6.1 将计算结果的合并到日志事件转换中付诸实践
有些人认为采用出生年份比年龄更不具个人色彩。这意味着你被要求修改存储在下游的日志数据。已经提出,应添加出生年份,并删除年龄属性。Chapter6/Fluentd/file-source-transformed-elastic-search-out.conf配置文件已被确定为合并必要更改的起点。可以使用相同的测试源(./Chapter6/SimulatorConfig/log-source -1.properties)来练习配置。为了便于识别此场景的输出,请在匹配配置中更改index_name。
答案
我们对配置的实现可以在Chapter6/ExerciseResults/file-source-transformed-elastic-search-out-Answer2.conf中看到。基本更改是在record指令中包含birthYr ${Date.today.year - record ['age']},并在remove_keys $.age指令之后。结果可以通过使用之前描述的 UI 在 Elasticsearch 的内容中检查。
6.7 生成简单的 Fluentd 指标
Fluentd 在 CNCF 的控制下有一个优秀的合作伙伴项目,即 Prometheus (prometheus.io/),其作用是处理和创建基于指标的度量数据。Prometheus 通常也与 Grafana 相关联,用于可视化这些数据。Prometheus 和 Grafana 与微服务相关联。像 Fluentd 一样,在微服务生态系统之外使用这些工具并没有真正的限制或原因。
Fluentd 和 Prometheus 的合作关系
既然提到了 Prometheus,那么看看 Fluentd 如何与 Prometheus 的架构以及更广泛的指标和监控生态系统相契合是很有价值的。如图所示,Fluentd 在几个点上可以与 Prometheus 相关联。
如图所示,Fluentd 与 Prometheus 有几种可能的关系,包括
-
将数据推送到 Push Gateway 的数据源,Prometheus 可以从中计算指标。
-
以 Prometheus 格式提供的 Fluentd 内部指标流,供服务器处理(无需从 Push Gateway 进行准备步骤)。这是通过 monitor_agent 插件实现的。
-
通过 Alert Mgr 记录指标警报的通道

Prometheus 架构以及 Fluentd 如何与之相关
Fluentd 的 Prometheus 插件(通过 fluent-gem install fluent-plugin-prometheus 安装)提供了几个度量选项。Prometheus 插件允许我们在 filter 和 match 指令中创建度量值。
更多关于 Prometheus 插件的信息可以在 github.com/fluent/fluent-plugin-prometheus 找到,有关 Prometheus 的信息可以在 prometheus.io 找到。还有几本关于这个主题的书,例如 Morgan Bruce 和 Paulo A. Pereira(2018)的 Manning 出版的 Microservices in Action (www.manning.com/books/microservices-in-action),这些书也可以提供帮助。
Prometheus 的价值在于处理事件序列数据,提取并提供度量数据。如果我们能够轻松避免将每个日志事件发送到 Prometheus(或任何其他工具)来计算基本指标,那么显然没有必要这样做。毕竟,为什么要传递所有这些数据呢?如前所述,有社区插件支持时间序列度量。我们认为,对于这些类型的请求,目前可用的插件值得考虑的是
-
fluent-plugin-datacounter (
mng.bz/voX7) -
fluent-plugin-numeric-counter (
mng.bz/4j9w)
这两个插件以类似的方式工作。数据计数器将根据正则表达式匹配来计数日志事件。数字计数器旨在将数值意义应用于值。例如,我们可以使用数字计数器来计数日志事件,如果事件属性有一个在 1 到 10 范围内的值。两者都在定义的周期内计数,并根据出现次数发射日志事件。
例如,在我们之前的过滤示例中,我们隔离了在事件的msg属性中提到单词computer的日志事件。我们可以将其改为记录每分钟包含对computer引用的日志事件数量,而不是过滤这些日志事件。
在列表 6.5 中,我们已修改配置,以便检查日志事件的元素是msg,如count_key属性所指定的。我们只定义了一个表达式,使用pattern1,并使用count_interval按照标准的 Fluentd 表示法定义了计数的时间段——在这种情况下,1 分钟。
列表 6.5 Chapter6/Fluentd/file-source-counted-elastic-search-out.conf
<match *>
@type datacounter ❶
@id counted
tag counted
count_key msg ❷
count_interval 1m ❸
aggregate all
output_messages yes
pattern1 p1 computer ❹
</match>
<match *>
@type elasticsearch
host localhost
port 9200
index_name fluentd-book-counted
scheme http
logstash_format true
reload_on_failure true
include_tag_key true
tag_key tag
<buffer>
flush_interval 5s
</buffer>
</match>
❶ 定义使用数据计数器插件的匹配指令
❷ 告诉插件要检查的日志事件元素
❸ 定义我们计数事件的周期
通过提供一系列数字模式,我们可以包括单个模式。
通常情况下,我们不会期望一个match指令在没有使用复制插件的情况下允许任何事件继续。然而,由于该插件利用了底层的发射辅助插件,它可以消费匹配的日志事件并发射新的事件供下游消费。要运行此配置,我们需要通过执行命令fluent-gem install fluent-plugin-datacounter来安装 fluent-gem。
插件内部处理线程和计时的方式意味着,当有传入的日志事件时,计算出的值不会写入 Elasticsearch。因此,根据时间,你可能不会立即看到写入的度量。
与之前 Elasticsearch 的场景一样,通过更改索引名称更容易看到存储的内容;例如,fluentd-book-counted. 假设 Elasticsearch 已准备好并正在运行,我们可以使用以下命令运行场景:
-
fluentd -c ./Chapter6/Fluentd/file-source-counted-elastic-search-out.conf -
groovy logSimulator.groovy ./Chapter6/SimulatorConfig/log-source-1.properties
6.7.1 将日志事件计数投入实践
LogSimulator 提供了设置和更改日志事件播放速率的手段。尝试更改 Fluentd 配置文件中的count_interval,并修改 LogSimulator 配置以以不同的速度发送日志事件(SimulatorConfig/log-source-1.properties)。向datacounter添加一个模式以定位消息中的Unix出现次数。
答案
改变 LogSimulator 的速度会导致计数的日志事件数量发生变化。通过修改配置文件中的 count_interval 属性来改变计数周期。在 match 指令中定义的第二种模式应类似于 pattern2 p2 Unix。
摘要
-
Fluentd 过滤器可以隔离需要触发操作的特定日志事件,例如执行维护脚本。
-
在过滤器中应用
record_transformation创建了修改事件以添加、删除和屏蔽内容的可能性,包括查看为什么修改日志事件有帮助的案例。 -
将 Fluentd 转换插件应用于日志事件中移除和屏蔽敏感数据,使我们能够限制满足法规要求(从额外的审计到建立更高安全配置的额外工作)的影响。
-
Fluentd 提供了导航 JSON 数据结构的方法,例如转换后的日志事件有效负载。因此,我们可以为事件处理应用更多的智能。例如,如果一个日志事件的客户属性识别出高价值客户,我们除了向 Ops 发出信号外,还可以向 CRM 系统发出信号。
-
事件标签、时间和记录值可以被修改。
extract和inject功能可以确保它们反映有意义的值——例如,将标签更改为反映日志事件记录,以便使用标签进行路由和过滤可以更加动态。 -
操作日志事件有优点也有缺点,从影响事件发生的准确记录或为下游使用提供有意义的日志数据。了解日志的潜在应用可以帮助我们确定最佳方案(例如,在可能的法律行动中使用需要未更改的日志)。
-
Fluentd 在 CNCF 的 Prometheus 部署中可以扮演多个角色,从向 Prometheus 提供特定事件及其事件属性,到捕获 Prometheus 输出数据并协助监控 Prometheus。
第三部分:超越基础
在第二部分,我们探讨了核心功能,从源和匹配指令到过滤、路由以及日志事件的转换和处理。我们还看到了一些常见的源和目标,从日志文件到 Elasticsearch、MongoDB 和 Slack。有了这些知识,我们已经足够开发出满足许多需求的监控解决方案。但最终,我们将会发现自己需要超越基础。
我们在整本书中多次提到了云原生、Docker 和 Kubernetes,但并没有过多地深入到将 Fluentd 配置到这些环境中的具体细节。这主要是因为我们值得欣赏的是,Fluentd 不仅仅是一个 Kubernetes 的实用工具。在我们具体讨论使用 Docker 和 Kubernetes 进行日志记录之前,我们首先应该处理 Fluentd 如何进行扩展的问题,因为这将影响我们如何支持容器化。
当我们审视 Docker 和 Kubernetes 时,我们将讨论 Fluentd 如何支持容器化应用程序,以及我们如何捕获这些技术和它们可能带来的挑战的日志事件。
最后,我们面对的挑战是,当现有的插件无法帮助我们处理那些具有独特或过时应用方式的应用程序或平台时,我们应该怎么办。这可能是一个具有过于复杂的数据结构的程序,或者我们可能需要一个定制的解析器来高效地处理它,而不是使用正则表达式。也许获取日志事件唯一的方法是调用应用程序 API。无论问题是什么,我们都需要开发自己的插件。因此,我们将构建一个自定义插件来了解如何解决此类问题,并揭示 Fluentd 扩展性的核心。
7 性能和扩展
本章涵盖
-
使用工作进程调整 Fluentd 以最大化资源
-
使用扇入和扇出模式部署 Fluentd
-
使用部署模式进行扩展
-
实施高可用性和部署
-
使用微服务模式与 Fluentd 结合
在前面的章节中,我们只使用了一个 Fluentd 实例。然而,我们生活在一个分布式、虚拟化和容器化的世界中,通常需要不止一个实例。除了分布式的考虑之外,我们还需要通过扩展(向服务器添加更多 CPU 或内存以支持更多进程和线程)和扩展(通过负载均衡部署额外的服务器实例以实现工作负载的分布)来支持弹性,以满足波动的需求(以及反向的缩放和缩减)。企业需要弹性来处理故障和灾难场景。为了提供良好的可用性,我们至少应该部署一个活动服务器和一个备用服务器,两个服务器都使用保持同步的配置文件。配置同步使得在第一个实例失败时能够迅速启动备用服务器(主动-被动)。在更严格的情况下,需要使用活动-活动部署,服务器分布在多个数据中心;这作为一种部署模式是非常常见的。在企业空间中,单服务器解决方案是非常罕见的。
本章将探讨可用的技术和功能,让我们可以使用工作进程和资源管理来扩展 Fluentd,并使用多个 Fluentd 节点进行扩展。通过扩展,我们还可以考虑增加弹性选项。由于 Fluentd 只需要很小的占用空间,我们可以在我们的桌面上实现一些技术和功能来扩展 Fluentd。
7.1 使用工作进程进行线程和进程的扩展
我们可以扩展 Fluentd 部署的一种方式是利用其生成额外子进程(工作进程)的能力,以利用现代机器具有多个可用于运行并发进程的 CPU 核心的事实。在配置任何扩展之前,了解 Fluentd 如何受其 Ruby 实现的影响以及 Ruby 如何处理线程至关重要。Ruby 有一个全局解释器锁(GIL),这意味着当一个进程不是 I/O 绑定时,它将阻塞其他任务(有关 GIL 和 Ruby 线程的更多详细信息,请参阅附录 E)。因此,任何计算密集型任务最好在单独的操作系统进程中执行,并使用操作系统来提供更有效的资源共享。一些插件会为你做这件事(例如,在使用 gzip 压缩时使用的 AWS S3 插件),但并非所有插件都这样做,因此我们必须非常注意这一点以进行性能优化。如果没有这种分离,Fluentd 进程将实际上被锁定,直到进程完成或释放线程。通常,Fluentd 作为路由日志事件的工具更有可能受到 I/O 绑定的影响——无论这种 I/O 是基于网络的还是最终是存储(即使是通过某种类型的数据库的物理存储间接地)。
Fluentd 通过启动称为工作进程的独立进程来解决线程锁定限制。默认情况下,Fluentd 有一个工作进程和一个控制器进程,但我们可以配置工作进程的数量。这实际上利用了操作系统通常将进程分配给 CPU 并在进程之间进行交换以给予它们 CPU 计算能力的公平比例的事实。如图 7.1 所示,每个工作进程将根据配置执行source、filter和match指令。
注意:当进程数量多于 CPU 核心数时,处理器将在进程之间进行交换。进程越多,交换就越多。交换活动需要付出少量的努力。如果你有太多的运行进程,你将花费更多的时间在进程交换上,而不是进行任何有意义的工作。

图 7.1 默认部署以及如何管理新工作进程
7.1.1 观察工作进程的实际操作
理解工作进程的行为的最佳方式是配置一个示例并观察实际发生了什么。展示工作进程最直接的方法是创建一个 Hello World 配置的变体。我们将建立多个工作进程,并将 dummy 源插件的应用分配给这些工作进程。使用 dummy 源插件意味着源没有影响行为的 I/O 依赖。相关的match指令然后将输出推送到 stdout。通过使用过滤器,我们可以将涉及该过程的哪个工作进程注入到日志事件中,这基于我们在上一章中学到的内容。
让我们定义我们将使用多少个工作进程,并将其添加到系统指令中,与我们已经设置的 log_level 属性并列。这是通过在 system 指令中设置 workers 属性来完成的。
为了明确定义每个工作进程的功能,我们将指令包裹在 <worker x> 指令中,其中 x 代表将执行指令的数字工作进程 ID。例如,<worker 2> 将使用第三个工作进程(ID 从 0 开始)。如果我们想有效地将更多资源(即工作进程)分配给特定的指令集,我们可以在指令中指定工作进程的范围。例如,<worker 1-3> 将将工作进程 1、2 和 3 分配以执行相同的活动。所有未分配指令的工作进程都将得到处理。因此,在我们的配置中,工作进程 0 将仅处理这些指令。
在列表 7.1 中,我们定义了四个工作进程,并且故意将后续指令留在了工作进程配置之外。结果是每个工作进程都会获取配置。这意味着我们可以共享一个公共输出——但是这需要小心处理,因为它可能产生不希望的结果。这些副作用可能包括丢失事件到存储损坏,例如多个进程尝试写入同一文件的问题。在我们的例子中,我们只是应用一个过滤器来提取worker_id,将其添加到日志事件中,并将其发送到stdout(控制台)。
列表 7.1 Chapter7/Fluentd/dummy-stdout-multiworker.conf—展示工作进程
<system>
log_level info
workers 4 ❶
</system>
<worker 0> ❷
<source>
@type dummy
tag w0
auto_increment_key counter
dummy {"hello":"from worker 0"}
</source>
</worker>
<worker 1-2> ❸
<source>
@type dummy
tag w1
auto_increment_key counter
dummy {"hello":"from worker 1-2"}
</source>
</worker>
<worker 3>
<source>
@type dummy
tag w2
auto_increment_key counter
dummy {"hello":"from worker 3"}
</source>
</worker>
<source> ❹
@type dummy
tag w-any
auto_increment_key counter
dummy {"hello":"from workerless"}
</source>
<filter *>
@type record_transformer
enable_ruby
<record>
worker_id ${ENV['SERVERENGINE_WORKER_ID']} ❺
</record>
</filter>
<match *>
@type stdout
</match>
❶ 声明工作进程的数量
❷ 特定于工作进程 0 的活动
❸ 定义两个工作进程的活动
❹ 定义工作进程之外的外部源——我们应该看到所有工作进程都会获取这个源。
❺ 使用过滤器添加涉及该日志事件的 worker ID
可以使用以下命令启动此配置
fluentd -c Chapter7/Fluentd/dummy-stdout-multiworker.conf
在检查 stdout 控制台之前,了解进程方面的情况是值得的。在 Windows 的命令控制台或 Linux 的 shell 中,应运行以下适当的命令:
Windows: tasklist /fi "IMAGENAME eq Ruby.exe"
Linux: ps -ef | grep ruby
这些命令将显示 Ruby 进程,其中包括 Fluentd 进程。因此,如果 Fluentd 是唯一运行的 Ruby 解决方案,你应该看到列出了五个进程。如果有其他 Ruby 解决方案正在运行,我们可以区分它们,因为 Fluentd 进程将具有相同或几乎相同的启动时间。这些进程由四个工作进程和一个控制器进程组成。
TIP 我们可以通过在 <system> 配置中使用 process_name 属性(例如,process_name Fluentd)来使进程更容易识别。
当 Fluentd 进程运行了一段时间后,我们可能想要关闭它们。现在这要复杂一些,因为我们有多个进程。对于 Windows,可以使用命令行 taskkill /IM Ruby.exe /F 来完成,Linux 的等效命令是 pkill -f ruby,前提是你没有运行其他 Ruby 进程。如果你有其他 Ruby 进程在运行,你必须隔离这些进程并手动逐个关闭它们。
通过查看使用 dummy -stdout-multiworker.conf 配置文件运行 Fluentd 的 stdout 结果,你应该能够看到以下情况发生(但请注意存在一定程度上的任意行为):
-
带有标签
w-any的日志事件将出现在任意的worker_id条目中。 -
与标签
w0链接的日志(包括"hello":"from worker 0")将仅链接到"worker_id":"0"。 -
与标签
w1链接的日志(包括"hello":"from worker 1-2")将仅链接到"worker_id":"1"或"worker_id":"2"。
7.1.2 工作进程限制
当使用工作进程时,需要考虑一些限制条件。这涉及到进程如何共享(或不能共享)资源,例如文件句柄。因此,如果你将多个工作进程分配给一个写入文件输出的 Fluentd 配置,文件需要被分开,因为只有一个工作进程可以正确地使用一个文件。我们可以通过设置文件路径包含 worker_id 来解决这个问题;例如,path "logs/#{worker_id}/${tag}/%Y/%m/%d/logfile.out"。
当插件使用 server 辅助插件或插件能够原生地处理端口的共享时,可以在工作进程之间共享端口。forward 插件是原生端口管理的例子。由于每个进程不能使用相同的端口,需要一个可靠的机制来克服这一点并选择合适的端口。当使用服务器辅助插件时,它将为每个工作进程分配连续的端口。因此,如果我们指定了使用四个工作进程,并定义了使用端口设置为 30000 的 monitor_agent 插件,那么工作进程 0 使用端口 30000,工作进程 1 使用 30001,工作进程 2 使用 30002,依此类推。如果你正在使用工作进程,请确保使用的端口是分开的。分离端口将避免潜在的端口冲突,因为算法会在多个工作进程的不同插件实例之间分配相同的端口。例如,指定端口 30000 然后 30002 给不同的插件,然后引入四个工作进程,将看到端口 30002 和 30003 试图被两个不同的插件使用。
7.1.3 控制输出插件线程
输出插件的线程行为可以通过使用名为 num_threads 的属性来控制。此值默认为 1。增加线程数可能会潜在地提高性能,因为它允许在某个线程被阻塞时在线程之间进行上下文切换。因此,任何进程中的延迟都可以更有效地被利用。但这也无法克服全局解释锁(GIL)的限制。
你可以考虑为输出插件使用此类配置,其中配置将工作负载分配到几个不同的目的地,因为一个线程工作直到结束或因 I/O 而必须停止。然后下一个线程,非 I/O 绑定,将被允许工作。这意味着我们获得了性能——而不是等待 I/O 释放和执行继续,我们交换正在执行的线程到可以进行工作的位置。
调整线程的使用是困难的,因为你必须知道进程的性能才能识别线程等待 I/O 等事物的潜力。由于线程切换的开销,存在一个点,等待 I/O 比交换线程更有效。这也可以通过操作系统级别的进程切换潜在水平而加剧。正确调整线程通常取决于运行现实的工作负载并测量性能,然后比较具有不同线程配置的测试运行,以查看性能实际上开始下降的地方。
7.1.4 内存管理优化
另一个可以调整的区域是 Ruby VM 层。这意味着调整垃圾收集和内存块分配。要在这一级别进行调整,你需要对 Ruby 实现的具体细节有很好的理解,以及帮助您分析配置如何影响性能的工具。在附录 E 中,我们提供了有助于 Ruby 的资源。
7.2 扩展和移动工作负载
第四章探讨了输出插件与缓冲区一起工作的能力,这将为我们提供一种优化每个 I/O 活动性能的手段,尤其是与内存缓冲区相关。除了缓冲区和线程及工作者的调整之外,扩展选项是关于工作负载分配。这可以通过以下方式实现
-
将日志事件馈送到像 Kafka 这样的事件流技术。
-
使用 Redis 或 Memcached 等工具进行大规模缓存。
-
利用 Fluentd 将日志事件传递到其他 Fluentd 节点的能力。这种能力提供了将工作负载移动到专用 Fluentd 节点的机会,要么是如果工作负载需要大量的额外计算能力而向外扩展,要么更可能是向内扩展,将来自许多较小节点的大量日志事件汇总到一两个 Fluentd 实例。
在以下章节中,我们将探讨输入/输出(有时称为集中器或聚合网络)和输出部署,因为它们使用相同的核心插件集实现。
Fluentd 的计算足迹非常小,我们可以运行一些配置来展示在单台机器上的设置。
7.2.1 输入/输出聚合和合并
部署多个 Fluentd 和 Fluent Bit 节点的最可能场景是支持集中器网络(扇入),尤其是在容器化环境中。此模型描述了两个或多个 Fluentd 节点收集日志事件并将事件传递给中央 Fluentd/Fluent Bit 实例。例如,正如我们将在本节后面看到的那样,日志事件可能起源于扇入“脊柱”末端的 Fluentd 节点。根据需要过滤日志事件,然后事件沿着脊柱流向扇子的中心——因此得名扇入或集中器。
让我们首先从更通用的形式开始,即与传统的虚拟化或原生硬件环境相关的日志聚合,这也可以在容器化部署中工作。然后我们将看到这与容器如何变化。
与应用程序架构和部署相关的扇入关系
处理高流量和/或需要高弹性级别的环境将看到应用程序软件分布在多个服务器上。我们可以配置服务器,使单个 Fluentd 实例可以看到每个服务器的日志文件,或者在每个服务器上部署 Fluentd(或 Fluent Bit)实例。打开服务器以便从其他服务器访问文件系统的一部分会带来安全挑战。每个服务器拥有 Fluentd 节点是一个更稳健且更安全的采用模式。更好的安全性来自于数据从 Fluentd 流向它所知道的位置,并且日志事件确定可以共享。这如图 7.2 所示。

图 7.2 展示了使用全栈模型进行扩展的情况,其中每个服务器都部署了所有功能。因此,调用序列更有可能保持在服务器内部。
扩展可以通过以下方式实现:
-
每个服务器都持有完整的解决方案栈(表示层、中间层,有时甚至包括后端存储)。
当这种情况发生时,你可能会在单个服务器的日志中从端到端跟踪单个请求和响应。但是,将单个客户端的多个请求响应链接到同一后端(称为服务器亲和性)可能会使工作负载偏向特定服务器。这可能会影响动态扩展的有效性,因为新节点(s)仅承担新客户端。
-
将解决方案划分为逻辑部分并将部分分配给一个或多个特定服务器。我们经常将此称为具有服务器专门运行某一层的 N 层模型,例如,运行表示层的层;部署业务逻辑层的其他服务器;以及用于持久化层的其他服务器等。我们可以在图 7.3 中看到 N 层或三层部署。每种不同颜色的垂直部分代表一个层——左侧的 UI 或表示层,中间的中层(通常在三层时为业务层),以及在这种情况下,右侧的报告层。用户会话的服务器亲和力可能不太成问题,因此同一服务器可能看到同一用户会话的片段。
![图片]()
图 7.3 在这种情况下,服务根据共同目的进行分组,以更有效地进行扩展。然而,在调用之后,端到端变得更加复杂。
最终,从端到端跟踪用户会话的活动将需要我们将所有日志汇总在一起以查看完整情况。有时,只有在所有日志达到分析平台并定期处理之后,才会处理完整情况。这没问题,但我们已经强调,我们可能希望快速反应或主动触发日志事件处理中的操作。通过一个集中节点传递日志提供了几个好处:
-
专门用于处理工作负载的节点允许为该工作调整资源。
-
当配置变得复杂时,如果逻辑更加集中,则更容易,因为部署改进和细化涉及更少的部署——即使有自动化,通常节点越少越好。
-
与需要凭证和访问凭证存储库(如 Vault)的大量节点相比,我们仅将此类细节的访问权限限制在更小的服务器集。因此,这些细节被滥用的可能性更小。如果存储凭证(或在使用相互传输层安全性 [TLS] 时使用证书)的方式不如 Vault 复杂,这一点至关重要。
-
如果控制数据来源的数量,可以更容易地证明安全性。这尤其适用于日志的最终目的地位于网络之外的情况,因为这意味着需要出站访问的服务器数量受到限制。它还使得在涉及出站代理服务器时更容易处理。
-
图 7.4 阐述了如何使用具有相对较小占用空间的 Fluentd 节点的应用服务器部署此类配置。最外层(顶部)的 Fluentd 实例在将日志事件(可能过滤掉一些低价值/不需要的日志事件)传递给内部节点之前捕获日志事件(内部节点显示在底部),该内部节点由多个 Fluentd 节点提供数据。

图 7.4 一个示例集中器网络部署,多个 Fluentd 实例向专用服务器上的中心 Fluentd 实例提供数据,该服务器执行大部分工作
通常,人们会用一个位于中间的单个服务器来展示扇入配置;然而,这可能是服务器集群,尤其是在考虑超大规模环境时。这个模型作为扇入的特点是日志事件源的数量远大于中心执行核心日志事件处理的数量。
扇入 Fluentd 配置
让我们逐步了解这种集中器网络配置的设置。我们需要两个 Fluentd 配置文件,其中一个将用于作为输出使用forward插件表示的任意数量的源服务器。第二个配置使用forward作为输入以处理并将流量导向最终目的地。为了简化,我们将使用虚拟源插件而不是运行模拟器。为了使源节点易于识别,我们需要在日志事件中包含某些内容。通常我们可以使用节点主机名来做这件事,但由于我们是在单个机器上运行所有内容,这并没有帮助我们。另一种方法是检索环境变量并将其用作标签名称。只要环境变量的作用域限制在启动 Fluentd 实例所使用的 shell 的作用域内,这就会起作用。图 7.5 更详细地说明了配置。

图 7.5 多个运行相同配置的 Fluentd 节点如何向单个实例提供详细视图
为了将环境变量放入有效载荷中,我们在源中添加了一个过滤器,该过滤器获取标签值,并使用 Ruby 命令"#{ENV["NodeName"]}"设置;这检索了NodeName的值。
列表 7.2 Chapter7/Fluentd/dummy-foward1.conf—展示前向输出
<system>
log_level info
</system>
<source>
@type dummy
tag "#{ENV["NodeName"]}" ❶
auto_increment_key counter
dummy {"hello":"world"}
</source>
<filter *>
@type stdout
<inject> ❷
tag_key fluentd_tag
</inject>
</filter>
<match *>
@type forward ❸
buffer_type memory
flush_interval 2s ❹
<server> ❺
host 127.0.0.1
port 28080
</server>
<secondary> ❻
@type stdout
</secondary>
</match>
❶ 在这里,我们正在获取环境变量以使每个实例独特。
❷ 将标签放入日志事件记录中
❸ 声明前向插件输出
❹ 在发送之前缓冲事件;为了方便,我们限制了这个时间。在现实世界中,你可能考虑一个更长的持续时间。
❺ 定义目标服务器以将日志事件定向到
❻ 如果我们无法与中心 Fluentd 实例通信,我们需要将日志事件发送到其他地方。在这个配置中,如果无法处理,我们只是将事件发送到控制台。你可能希望在生产场景中做更稳健的事情,比如将事件写入文件。
在启动 Fluentd 之前,用于运行 Fluentd 的 shell 需要set或export(Windows 或 Linux)NodeName=Node1。每个源节点都有一个新的编号。然后我们可以使用以下命令启动 Fluentd:
fluentd -c Chapter7/Fluentd/dummy-forward1.conf
重复启动 shell、设置环境变量和启动源 Fluentd 节点的步骤,以获得第二个生成日志事件并发送到中央 Fluentd 节点的 Fluentd 节点。
注意:如果环境变量未设置,并且 Fluentd 正在显示其配置(在 info 日志级别),您可以查看值是否已正确插入。如果值不存在,根据属性,您最多会观察到启动错误;最坏的情况下,系统将启动但似乎没有任何动作。这源于默认值可能被定义并被采用。例如,port属性将是0。
我们已经使用过滤器确保标签被捕获到日志事件中。此外,我们还可以利用stdout插件,以便发送者的控制台会显示我们应该在中央节点接收到的日志事件。理想情况下,我们需要运行几个 shell 并相应地设置环境变量。根据中央(消费)节点启动所需的时间,源节点将报告周期性网络错误,因为没有对网络调用的响应。
这将我们引向消费配置,它只是接受转发的事件并将它们推送到控制台。我们之前已经看到很多这样的内容,尽管forward插件的使用是新的。为了使 Fluentd 接收事件,我们需要定义一个 Fluentd 源,它绑定到网络地址和端口。这显然需要与发送者的配置相匹配。我们可以在以下列表中看到所有这些内容。
列表 7.3 第七章/Fluentd/forward-stdout.conf—说明将转发作为源
<system>
log_level info
</system>
<source> ❶
@type forward
port 28080 ❷
bind 127.0.0.1
</source>
<match *> ❸
@type stdout
</match>
❶ 定义了输入转发插件的使用
❷ 绑定的网络地址(DNS 或 IP)—在我们的情况下是 localhost。这需要与发送者匹配。
❸ 显示在控制台上已发送的日志事件
定义了消费 Fluentd 节点后,我们可以启动单个实例(对于更常见的集中器网络)。一旦所有 Fluentd 节点开始通信,我们将在该节点的控制台中看到所有日志事件。因此,让我们使用以下命令启动消费者节点
fluentd -c Chapter7/Fluentd/forward-stdout.conf
当您查看现在生成的控制台输出时,应该看到包含在控制台输出中的节点名称会有所不同。这种变化反映了日志事件来自两个不同的 Fluentd 节点,因为我们已在配置中使标签值动态化。
注意:msgpack 插件的应用将有助于减少网络流量,因为可以将格式化器设置为 msgpack 用于转发插件。接收转发插件可以识别 msgpack 格式的事件并自动解包它们。因此,Fluentd 到 Fluentd 的流量传输非常高效。
7.2.2 扇出和工作负载分配
我们可以看到,通过将与应用工作负载位于同一节点的任务卸载到一个或多个专用 Fluentd 服务器上,我们可以如何增加可供 Fluentd 进程使用的计算工作量,如图 7.6 所示。如果我们只是卸载工作,那么使用 Fluent Bit 作为应用协同日志收集器可能是有意义的。Fluent Bit 更小,如果它能收集日志事件(记住 Fluent Bit 在插件选项上更为受限),它就可以轻松地转发到 Fluentd。然后我们使用下游的 Fluentd 来处理日志事件。回顾第一章以了解 Fluent Bit 与 Fluentd 的不同之处。

![图 7.6 工作分布的部署选项允许将更多计算能力分配给 Fluentd 处理日志事件,而不会影响原始应用程序,因为我们可以将日志事件路由到更多具有专用 Fluentd 容量的服务器]
扇出模式的运用并不常见,至少在我们经验中是这样。如果你发现自己正在使用不寻常的配置,那么审查情况以确保没有更大的问题是很值得的。例如,限制默认资源分配迫使需要扇出,但放宽或移除限制可以消除一些分布复杂性。
扇出配置的 Fluentd
在扇出和高可用性部署中,我们需要有能力将工作负载发送到可能多个节点。在高可用性的上下文中,发送流量到不同的节点将由通信丢失触发,而在扇出中,连接性是由工作负载共享驱动的。让我们检查这两个要求,因为它们在配置上存在一些共性。如图 7.7 所示,这次我们将只部署一个带有虚拟源生成器的节点,但将日志事件路由到多个消费者节点,这些节点将输出到控制台。

![图 7.7 Fluentd 扇出示例配置,一个节点将日志事件传递给多个节点进行处理]
与之前源节点配置的关键区别在于,forward 插件的配置现在需要指定多个服务器。在高可用性中,哪个节点应被视为主节点,哪个节点应作为备用节点必须得到解决。对于扇出,我们可能希望权衡一个节点相对于另一个节点的工作负载。所有这些都可以通过属性在配置中完成。对于多个服务器,如图 7.4 所示,我们可以简单地声明多个连续的服务器辅助插件 <server> 的属性块。由于这是一个基本的扇出,我们已添加一个 weight 属性来建立服务器之间工作负载的比例。在我们的例子中,这个比例是 10:1。如果未指定,则所有节点将应用相同的权重。
列表 7.4 Chapter7/Fluentd/dummy-forward2.conf—展示向多个服务器转发
<source>
@type dummy
tag dummy-fanout-source
auto_increment_key counter
dummy {"hello":"world"}
</source>
<filter *>
@type stdout
<inject>
tag_key fluentd_tag
</inject>
</filter>
<match *>
@type forward
buffer_type memory
flush_interval 2s
<server> ❶
host 127.0.0.1
port 28080
weight 10 ❷
</server>
<server>
host 127.0.0.1
port 38080 ❸
weight 1 ❹
</server>
<secondary>
@type stdout
</secondary>
</match>
❶ 首个服务器定义及其不同的端口,这样我们就可以在同一主机上运行
❷ 定义权重,这将优先选择第一个服务器配置。如果未设置,此值默认为 60。
❸ 定义备用端口
❹ 权重设置为将流量从该服务器偏移
由于我们将在同一台机器上运行所有内容,因此形成扇出侧的 Fluentd 实例需要配置为在不同的网络端口上运行,以避免冲突。类似生产环境的 Fluentd 实例配置为在单独的服务器上运行,但使用相同的网络端口。利用我们在 7.2 列表中看到的命名技巧,我们可以使值配置驱动,避免需要具有不同值的多个配置文件。因此,每个节点都需要一个名为NodePort的环境变量,定义节点配置源侧使用的端口之一,如下所示。
列表 7.5 第七章/Fluentd/forward-stdout2.conf
<source>
@type forward
port "#{ENV["NodePort"]}" ❶
bind 127.0.0.1
</source>
<match *>
@type stdout
</match>
❶ 动态设置端口号允许我们运行相同的配置两次。
让我们看看这种节点配置会发生什么。使用命令启动源节点。
fluentd -c Chapter7/Fluentd/dummy-forward2.conf
然后,我们需要为 Windows 配置一个带有命令set NodePort=28080的 shell,或者在基于 Linux 的环境中配置export NodePort=28080。一旦设置完成,我们就可以使用命令启动 Fluentd 实例。
fluentd -c Chapter7/Fluentd/forward-stdout2.conf
我们然后再次重复这些步骤,在设置/导出步骤中将28080替换为38080。
一切运行起来后,日志事件应该出现在运行dummy-forward2.conf配置的 Fluentd 实例的控制台上。设置比例后,我们应该看到日志大量偏向运行在端口 28080 上的节点。但是,如果你计算一个控制台到另一个控制台的更新数量,你不能保证看到使用 38080 端口的每个输出和另一个使用 10 个,因为比例是在我们想要发送输出时计算的。计算的结果将决定比例哪一侧。
轮询插件
另一种分配工作负载的方法是利用轮询插件。这是一个核心 Fluentd 输出插件,与store辅助插件一起工作。以下列表展示了这一点,其中roundrobin将输出轮询到每个单独识别的服务器。由于这是扇出实现,每个store块将使用一个forward插件,但这不是强制性的。
列表 7.6 第七章/Fluentd/dummy-forward3.conf—展示轮询的使用
<source>
@type dummy
tag dummy-fanout-source
auto_increment_key counter
dummy {"hello":"world"}
</source>
<filter *>
@type stdout
<inject>
tag_key fluentd_tag_roundrobin
</inject>
</filter>
<match *>
@type roundrobin ❶
<store> ❷
@type forward
buffer_type memory
flush_interval 1s
<server> ❸
host 127.0.0.1
port 28080
</server>
</store>
<store>
@type forward
buffer_type memory
flush_interval 1s
<server>
host 127.0.0.1
port 38080 ❹
</server>
</store>
<secondary>
@type stdout
</secondary>
</match>
❶ 要获得轮询行为,我们需要将其用作输出插件。然后,它将依次使用每个存储辅助插件,就像复制插件使用所有存储辅助插件一样。
❷ 声明存储配置,但由于我们希望轮询平均使用每个目标,存储的配置只能有一个服务器。
❸ 目标服务器的定义
❹ 配置为使用不同端口的第二个服务器
让我们看看roundrobin与权重的行为对比。我们需要像之前一样启动;如果两个扇出节点的控制台没有为NodePort设置变量,我们需要重新建立设置。然后我们使用以下命令启动事件源 Fluentd 实例:
fluentd -c Chapter7/Fluentd/dummy-forward3.conf
然后使用相同的命令启动两个扇出节点实例:
fluentd -c Chapter7/Fluentd/forward-stdout2.conf
这次输出将始终一致地发送到备用控制台输出,因为roundrobin故意确保分配始终均匀。也可以应用weight属性,但这会破坏roundrobin的行为。
7.2.3 高可用性
高可用性配置与扇出配置并没有太大的区别。我们不是使用weight属性来分配工作负载,而是使用standby属性,并将一个节点设置为 true,另一个节点设置为 false。这里可以看到一个匹配插件服务器部分的示例:
<server>
name myserver1
host 127.0.0.1
port 28080
standby false
</server>
<server>
name myserver2
host 127.0.0.1
port 38080
standby true
</server>
如片段所示,我们定义了两个服务器;例如,使用转发输出插件将是两个 Fluentd 实例,将日志事件发送到。当具有此配置的 Fluentd 实例启动时,它将尝试使用名为myserver1的服务器发送日志事件,因为它被标记为不是备用服务器。然而,如果此 Fluentd 实例与myserver1出现通信问题,它将把日志事件发送到名为myserver2的备用服务器。
在这个片段中,我们使用了name属性。通常,名称仅用于 Fluentd 日志记录和证书验证。但如您所见,使用name属性也可以帮助您确定哪个服务器是哪个,尤其是在使用 IP 地址而不是有意义的 DNS 名称时。
7.2.4 将高可用性比较付诸实践
您的客户想了解高可用性配置在设置和行为上的差异。您的团队已同意将配置文件Chapter7/Fluentd/dummy-forward2.conf和Chapter7/Fluentd/forward-stdout2.conf重构,以提供比较。
一旦配置被重构,运行这两个配置并关闭Chapter7/Fluentd/forward-stdout2.conf的单个实例。注意结果行为,以向客户展示差异。
答案
基于配置文件Chapter7/Fluentd/dummy-forward2.conf和Chapter7/Fluentd/forward-stdout2.conf的高可用性配置示例可以在Chapter7/ExerciseResults/dummy-forward2-Answer.conf和Chapter7/ExerciseResults/forward-stdout2-Answer.conf中找到。
配置中唯一重要的更改是移除weight属性,并在相关服务器配置中引入设置为true或false的standby属性。差异可以在节点启动后立即观察到(最好使用端口 38080 启动dummy-forward2-Answer.conf节点,这样它不会立即认为主目标节点已关闭并切换到备用)。控制台输出将只出现在监听端口 28080 的节点上。然而,当此节点关闭时,日志事件将传递到在端口 38080 上工作的 Fluentd 实例。
7.3 容器、本地和虚拟环境中的 Fluentd 扩展
到目前为止,我们已经探讨了如何从纯 Fluentd 节点到节点的角度扩展 Fluentd。在大多数你工作在虚拟化或本地硬件环境的情况下,你可以使用 Fluentd 或 Fluent Bit 实例部署的配置。这些部署可以描述为 Fluentd 与应用程序一起运行在虚拟机或服务器上。每个节点都在捕获应用程序日志事件以及来自宿主操作系统的日志事件。因此,扩展虚拟机或本地服务器将驱动 Fluentd 的扩展。
对于 Kubernetes 等容器化环境,我们有更多的选项和考虑因素,因为容器通常更细粒度(因此,需要一个完整的解决方案需要更多的容器)。我们有一个额外的抽象层,即 pods,编排要复杂得多。虽然我们将专注于 Kubernetes,但原则对于 OpenShift 和其他相关产品并没有很大不同。
7.3.1 Kubernetes 工作节点配置
不仅你的应用程序需要记录内容,编排层,如 Kubernetes、Apache Mesos、Docker Swarm 等(包括容器引擎本身)也需要。因此,Kubernetes 引擎在每个工作节点上创建了特殊的服务,它使用这些服务。部署将看起来如图 7.8 所示。所有单个容器中的日志事件都必须指向 stdout,以便此部署能够工作。

图 7.8 Fluentd 作为守护进程服务在 Kubernetes 环境中的部署示意图。Fluentd 收集所有 Pod 的 stdout 和 stderr 输出。
7.3.2 集群配置
每个集群模型看起来很像工作节点配置,但结构更严谨。这种结构是由于容器通过映射写入定义的位置,而不是简单地信任 stdout 和 stderr,并希望有人正在监听。这也使得对不同类型的日志进行分段并从中推断出一些意义变得更容易。现在容器只需要挂载集群范围内的文件系统,并将它们的日志写入文件,就像它们在本地运行时一样(将本地文件系统映射到共享存储是 Kubernetes 配置问题)。
当日志被写入文件系统时,带有 Fluentd 容器的 pod 只需使用 tail 插件(s)来捕获和处理日志文件。通过良好的目录和/或文件命名,我们可以定义关于日志文件格式的具体细节。了解日志文件来源可以确定特别重要的事情。这种做法在图 7.9 中得到了说明。

图 7.9 Kubernetes 集群共享日志捕获,其中 pod 将数据写入文件系统,然后 Fluentd pod 收集文件并进行处理
在图 7.9 中,我们提到了存储附加网络(SAN),这对于本地部署来说是非常理想的。它将提供磁盘冗余,并且通常情况下,存储会分配物理磁盘,从而提供高性能。在云环境中,您将使用块或文件存储样式来实现,并信任云提供商提供的质量服务和性能控制。
7.3.3 容器作为虚拟化
这反映了将现有环境作为现有环境并配置在容器中的简单想法,将具有自己操作系统的虚拟机转换为将操作系统工作委托给共享主机的容器。因此,逻辑部署可能看起来像图 7.10,其中每个容器托管应用程序和 Fluentd,或者更理想的是 Fluent Bit,如果它具有适当的适配器和更小的占用空间。

图 7.10 容器中的 Fluentd,正如您在虚拟化应用程序部署中所做的那样
7.3.4 边车模式
基于容器的技术,如 Kubernetes,有 pod 设计模式,如边车 (mng.bz/nYNK)。边车模式的想法是在容器 pod 内部添加容器以提供支持服务;这可以包括提供安全性的代理层;这意味着将存在一个带有 Fluentd 或 Fluent Bit 的容器,以支持 pod 内的所有其他容器,如图 7.11 所示。这是最灵活的,对于 Fluentd 来说,配置也最容易,但需要容器和 pod 配置的更多复杂性。

图 7.11 使用此处所示的边车模式,使 Fluentd 对所有 pod 中的容器可用。
7.3.5 选项比较
在查看不同的部署模型后,我们应该花时间了解不同方法的优缺点。在表 7.1 中,我们提取了所描述的每个模式的优缺点,包括 Fluentd 和 Fluent Bit。
表 7.1 容器化环境中的 Fluentd 部署选项
| 方法 | 优点 | 缺点 |
|---|---|---|
| Fluentd 作为工作节点的一部分 | 在部署方面最简单,因为它只涉及工作节点。无需更改 pod 或容器即可部署 Fluentd 的补丁。 | 将日志转换回更有意义的结构需要更多的工作。要为日志事件赋予意义需要一些上下文,例如理解应用程序或服务。缺点是应用程序上下文位于应用程序域(使用的服务)之外。Fluentd 补丁配置更改会影响整个工作节点。 |
| 应用容器中的 Fluentd | 将配置隔离到最小的组件。 | 更大的计算占用空间,运行大量 Fluentd 实例——这使得 Fluent Bit 成为一个更好的选择。 |
| 应用容器中的 Fluent Bit | 将配置隔离到最小的组件。比 Fluentd 占用更小的空间。 | 与其他模型相比,总计算工作量增加。但比 Fluentd 小。在可消费的输入类型方面存在限制,因为插件选项较少。Fluent Bit 在可用的插件方面不如 Fluentd 丰富,限制了在日志事件上执行的过程。 |
| Fluentd 作为边车 | 最小化 pod 内 Fluentd 实例的数量。服务意识与应用程序的 pod 相关联(例如,拦截特定的日志事件)。有可能使用通用的 Fluentd 容器并利用配置动态检索配置。 | 更复杂的 pod 配置。Fluentd 补丁过程更复杂,因为它会影响 pod。容器将比 Fluent Bit 变体稍大。 |
| Fluent Bit 作为边车 | 最小化 pod 内 Fluentd 实例的数量。服务意识与应用程序的 pod 定义相关联(例如,拦截特定的日志事件)。比 Fluentd 占用更小的空间。日志事件处理在应用程序上下文中进行。有可能使用通用的 Fluentd 容器并利用配置动态检索配置。 | 更复杂的 pod 配置。Fluentd 补丁过程更复杂,因为它会影响每个 pod。 |
7.4 保护 Fluentd 节点之间的流量
当在 Fluentd 节点之间通信时,你可能希望提供一定级别的安全性。使用未加密的网络流量可能导致凭证泄露,这不仅会影响 Fluentd 节点之间的身份验证,还会影响 Fluentd 与源或目标(如 Elasticsearch)之间的通信。我们已使获取凭证变得非常容易,当监听网络流量时。不仅凭证会泄露,而且传递的日志信息还会为攻击者提供了解决方案,了解你的解决方案可能如何工作,如果日志事件用于审计,则可能收集敏感数据等。使用 HTTPS 加密和 TLS(安全套接字层)的继任者可以缓解这些问题。
如果您的日志事件包含 PII 数据,则在传输和存储期间,应用程序和日志事件需要采取积极的安全配置。除了日志事件可能在云和数据中心之间通过不安全的网络(即互联网)进行通信的可能性之外,您还需要考虑所有这些因素。但安全性不应仅仅局限于采用 TLS,或者更好的是,相互 TLS(mTLS),正如我们很快将看到的。
设置 TLS 已不再是曾经那样令人畏惧的神秘技艺,部分原因可能是因为我们已经超越了在网络安全边缘终止 SSL/TLS 的想法,加密和解密的计算开销现在不再被视为如此繁重。但配置 SSL/TLS 仍然相当依赖于上下文,并且确实需要一些对 TLS 概念的基本理解(这是在其他书籍中深入探讨的主题,例如 Julien Vehent 的《Securing DevOps》,可在 www.manning.com/books/securing-devops 购买)。因此,我们不会详细讨论适用于每个人的 TLS 配置过程,而是简要地看一下提供的支持。(附录 E 提供了各种资源的链接,可以帮助您自己应用 TLS。)随着当代安全方法采用“信任无一人”的立场,投入时间建立 TLS 安全性是值得的。
7.4.1 TLS 配置
当涉及到配置网络传输时,我们可以提供一系列传输配置。一些适配器直接利用辅助插件,因此有时会提供略微不同的属性名称。例如,secure_forward 在其配置的服务器部分使用 tls_version。相比之下,当直接使用传输助手时(这可以在源、过滤器和匹配指令中提供),该属性被称为 version。传输结构在配置中以 XML 标签表示,并包括一个指示传输类型(udp、tcp、tls)的元素。例如:
<transport tls>
. . .
</transport>
虽然我们一直专注于 TLS 以及抽象更多网络功能的插件,但我们也可以处理 TCP(传输控制协议)或 UDP(用户数据报协议)流量。然而,这些协议的使用需要更多的配置工作。
关于 TCP 和 UDP 的更多信息
关于如何使用这些协议的更多信息,以下资源将有所帮助:
TLS 版本和算法
可以通过 Fluentd 配置来控制可以支持的 TLS 版本。TLS 1.3(于 2018 年 8 月发布为 RFC 8446;tools.ietf.org/html/rfc8446)是目前发布的最新标准版本。目前,Fluentd 默认使用的版本是 TLS 1.2,这反映了 TLS 1.2 是最广泛采用的。行业惯例建议使用可能的最新版本的 TLS(因为它是最安全的),并且仅在必要时适应较低版本。TLS 兼容性和加密选项可以通过version和ciphers属性来管理。
专门的前向插件用于 SSL/TLS
前向插件的安全版本适用于入站和出站操作,如前所述。这可以使用 gem 部署,就像所有插件一样。例如:
gem install fluent-plugin-secure-forward
此版本的插件仍然需要证书,但简化了配置并隐藏了传输层配置部分。
7.4.2 TLS 不仅仅是加密
使用 TLS 不仅仅是提供加密密钥,而且可以也应该验证为使用该证书的证书颁发机构(CA)提供的真实证书。通常,握手的一部分是客户端和服务器连接时。如果与证书颁发机构确认证书真实性的努力正在损害延迟,那么如果你完全处于一个受信任的网络环境中(例如,一个物理私有数据中心网络,但不是云托管网络),你可能考虑禁用检查。关闭此类检查与深度安全概念相违背,因此请考虑这可能会带来什么风险。如果你使用自签名证书,你需要走得更远,因为没有 CA 参与。需要额外的属性——tls_insure_mode和tls_allow_self_signed_cert——以防止 Fluentd 与 CA 检查证书。
7.4.3 证书和私钥存储
要使用证书,我们显然需要能够存储它和私钥。这些信息被定义为几个属性,包括适应 Windows 存储选项(更多信息,请参阅mng.bz/vo6M)。
无论证书存储在哪里,我们都需要通过cert_path(例如,cert_path ./myFluentd.crt)告诉 Fluentd 证书的位置,以及私钥的位置,通过private_key_path(例如,private_key_path ./myFluentd.key)。理想情况下,证书由公共或私有 CA 提供,可以联系以确认正在使用的证书的真实性。我们可以通过client_cert_auth属性(true或false)告诉 Fluentd 是否应该进行该验证。在自签名设置中,这必须是 false。
7.4.4 安全不仅仅是证书
确保通信安全不仅仅是应用 TLS。一些组织可能需要更多,例如传递用户名、密码和令牌。使用此类属性和令牌 ID 可以提供额外的保证。如果我们打算传递这样的敏感值,那么使用 TLS 应被视为强制性的。
7.5 凭证管理
我们面临挑战,Fluentd 配置文件没有在配置文件中加密和解密凭证的手段。因此,当 Fluentd 以明文形式启动时,用户名和密码需要出现在配置中。任何了解包含明文凭证的文件的系统管理员(sysadmin)都不会高兴,如果您与 IT 安全官员合作,他们将会更加关注。有一些策略可以限制这种风险;这些是我们看到或采用的策略。列表按照安全性的增强顺序排列:
-
限制 Fluentd 文件的访问权限,使其非常严格。记住,这也意味着阻止或限制 Fluentd UI(如图 2 所示)的使用,以及 UI 的凭证。这种方法实际上是最基本的,如果涉及敏感数据如 PII,可能被认为不可接受。
这可能需要设置和运行 Fluentd 或 Fluent Bit 的本地主机用户。这样的设置会带来一系列其他管理考虑因素。
-
使用包含来分离核心配置和凭证。然后只需要将包含文件提交给严格的文件访问控制。这是一个改进,因为它允许您更自由地处理配置。但如果有很多凭证要处理,这可能会变得很麻烦,如果涉及 PII 数据,可能被认为不可接受。
-
使用脚本包装 Fluentd 启动,在启动 Fluentd 之前,将凭证加载到 OS 会话中的环境变量中,然后启动 Fluentd。然后 Fluentd 配置将包含对环境变量的访问,正如我们之前所展示的。因此,配置文件在 Fluentd 解析文件之前没有敏感值。但我们可以将获取和解密环境变量的方法纳入脚本中。这允许您利用标准的 OS 安全功能。在容器化环境中,这可能会变得复杂,在多种 OS 类型的世界上,这意味着可能需要不同的配置。确实,不同的脚本将所需的凭证加载到内存中。
-
另一个通常与更云原生方法相关联的组件,同样可以应用于传统部署环境,是使用 HashiCorp 的 Vault (www.vaultproject.io)。Vault 提供了免费(开源)版本和带有额外功能的企业版(同步分布式密钥库)。然后我们可以将其嵌入到配置文件中,并使用 Vault CLI 或 API 调用 Vault 来检索所需的凭据。这解决了之前需要加载到操作系统环境中的问题。我们不会深入探讨如何将应用程序角色与 Vault 中可用的凭据对齐的详细具体信息,因为文档在 www.vaultproject.io/docs/auth/approle 提供了出色的解释。
如果你在一个 Kubernetes 环境中工作,那么当然,你还有使用 Kubernetes 机密(更多关于此的信息在 mng.bz/4j4V)的额外选项。Vault 有许多插件可以与其他原生凭据框架一起工作,例如 Kubernetes 的 Secrets、云供应商的凭据,或较旧的标准如 LDAP(轻量级目录访问协议)。
7.5.1 简单凭据使用案例
我们可以将用户名和密码凭据定义为 Fluentd 节点之间安全配置的一部分。这允许接收转发日志事件的 Fluentd 节点拥有更高的信任级别。
凭据显然与服务器相关联,因此在转发输出配置中,我们在 server 属性集中提供了 username 和 password。在下面的列表中,我们将 dummy-forward.conf 扩展以包含凭据。
列表 7.7 第七章/Fluentd/dummy-user-forward1.conf 使用用户凭据
<source>
@type dummy
tag "#{ENV["NodeName"]}"
auto_increment_key counter
dummy {"hello":"world"}
</source>
<filter *>
@type stdout
<inject>
tag_key fluentd_tag
</inject>
</filter>
<match *>
@type forward
buffer_type memory
flush_interval 2s
compress gzip
<security> ❶
shared_key hello
self_hostname source_host
</security>
<server>
host 127.0.0.1
port 28080
username hello-this-is-a-long-username ❷
password world-of-security-likes-long-passwords
</server>
<secondary>
@type stdout
</secondary>
</match>
❶ 为了安全起见,必须提供一些必需的属性,包括逻辑名称和通用密钥。
❷ 提供用户凭据
基于 forward-stdout.conf,消费者端也需要相同的凭据来验证。在列表 7.8 中,我们展示了涉及的附加属性。消费者端将需要指定的用户名和密码,并在 security 结构中使用 user_auth 属性进行明确的指示。服务器逻辑名称应期望使用 self_hostname 属性定义转发日志事件,以及必需的安全属性 shared_key。
列表 7.8 第七章/Fluentd/forward-user-stdout.conf 使用凭据接收
<source>
@type forward
port 28080
bind 127.0.0.1
<security> ❶
user_auth true ❷
self_hostname destination_host ❸
shared_key hello
<user> ❹
username hello-this-is-a-long-username
password world-of-security-likes-long-passwords
</user>
</security>
</source>
<label @FLUENT_LOG>
<match fluent.*>
@type stdout
</match>
</label>
<match *>
@type stdout
</match>
❶ 启动安全配置
❷ 告知 Fluentd 我们必须应用用户身份验证
❸ 声明客户端如何识别此节点
❹ 声明预期到达的凭据
我们可以使用一个 shell 运行此配置:
fluentd -c Chapter7/Fluentd/forward-user-stdout.conf
此外,我们还需要运行另一个 Fluentd。在启动 Fluentd 之前,用于运行 Fluentd 的 shell 需要set或export(Windows 或 Linux)NodeName =Node1。每个源节点都有一个新的编号。然后我们可以启动 Fluentd:
fluentd -c Chapter7/Fluentd/dummy-user-forward1.conf
一切都应该像我们没有运行用户凭证时一样运行。然而,我们在消费者端验证凭证。停止客户端,更改密码,然后重新启动该 Fluentd 实例。现在这将因报告的密码问题而失败。
7.5.2 将证书投入实际应用
您的公司需要 Fluentd 部署跨越多个数据中心,以便安全团队能够在 WAN 上使用他们偏好的监控工具。您的首席安全官(CSO)对在节点间通信中应用了安全元素感到高兴。但他们不高兴的是凭证可能会以明文形式传输。CSO 批准了使用跨越公司网络的 Fluentd 节点,只要您能提供 SSL/TLS 配置来加密流量。数据中心没有直接互联网连接,无法从公共 CA 验证和直接分发证书。目前没有内部 CA,尽管未来有关于建立 CA 的讨论。基础设施团队表示,他们将为您分发自签名证书。因此,我们需要使用自签名证书来配置 Fluentd。为了证明基础设施团队能够满足证书要求并且他们理解需要什么,已经同意修改dummy-user-forward1.conf和forward-user-stdout.conf以包含使用自签名证书的过程,以证明这个过程。
答案
通过用虚拟文件替换证书或密钥文件来运行 Fluentd 节点,可以实现证明解决方案可行的过程。这应该会导致数据交换失败。
示例配置可以在配置文件Chapter7/ExerciseResults/dummy-user-forward1-Answer.conf和Chapter7/ExerciseResults/forward-user-stdout1-Answer.conf中找到。我们在 Fluentd 配置中引用了虚拟证书文件(如果使用,这将触发失败)。为了使其工作,您需要用适当的证书替换这些文件。由于证书包含详细信息并且有定义的有效期,您应该创建自己的证书并用您生成的证书替换虚拟文件。这是因为证书可以与身份相关联并且有定义的有效期。关于如何使用 OpenSSL(www.openssl.org)进行此操作的指南可以在 Justin Richer 和 Antonio Sanso 的《理解 API 安全》的 liveBook 版本中找到(Manning,2017)在mng.bz/QWvj。
另一种方法是采用 Let’s Encrypt,它将提供一个自动化的机制来更新证书(letsencrypt.org/)。
在配置中,你会注意到我们选择从标准转发插件切换到安全转发插件,因此我们不需要显式设置传输层属性。我们还假设用于创建密钥和证书的密码短语是your_secret。要更改配置中保留的密码短语以与所使用的密码短语一致,你需要修改包含名为ca_private_key_passphrase属性的forward-user-stdout1 -Answer.conf,该属性需要正确的值。
要运行配置,我们需要使用以下命令启动 Fluentd 节点
fluentd -c Chapter7/ExerciseResults/forward-user-stdout1-Answer.conf
fluentd -c Chapter7/ ExerciseResults /dummy-user-forward1-Answer.conf
正如我们所看到的,Fluentd 在实现扩展、分配和弹性方面非常灵活。但这也意味着需要使用网络连接。我们应该尽可能保护我们的网络流量,就像我们努力保护单个服务器或容器一样。这意味着我们需要处理用于认证和加密的证书。证书的使用可能会使事情更具挑战性,但如果采用周密的策略,这些问题将会变得容易得多,这不仅适用于监控,也适用于应用程序通信。
摘要
-
Fluentd 的性能可以通过使用运行单个 CPU 进程的工作者或通过受 Ruby 工作方式约束的线程管理来调整。
-
工作者确实需要仔细考虑,以避免像将日志事件顺序错乱这样的错误。有一些策略可以帮助确定如何配置工作者,以便他们不会引入新的问题。
-
可以使用扇出和扇入模式来分配工作负载,以分散或集中处理日志事件。
-
可以通过 Fluentd 节点的分布式部署来实现高可用性。
-
相同的基本分配原则可以在微服务环境中应用。使用 Kubernetes 允许以多种不同的方式部署和使用 Fluentd。
-
不同 Fluentd 和 Fluent Bit 实例之间的通信应通过使用 SSL/TLS 证书来确保安全,并且应进一步通过使用凭证或令牌来增强。
-
安全性不仅应该解决 Fluentd 节点之间的通信问题,还应该扩展到向其他服务发送和检索日志事件,例如 Mongo 数据库或 Elasticsearch。
8 使用 Docker 和 Kubernetes 驱动日志
本章节涵盖
-
设置 Docker 以使用 Fluentd 作为其日志驱动程序
-
理解用于 Kubernetes 日志的组件
-
优化 Kubernetes DaemonSets 以适应 Fluentd
-
配置 Fluentd 以收集 Kubernetes 组件日志事件
-
发现 Kubernetes 节点监控的工作原理
前几章提到了 Fluentd 与 Docker 和 Kubernetes 的关系,但我们专注于独立于这些技术运行 Fluentd 以最小化复杂性。这有助于支持观点,尽管与 CNCF 有关联,Fluentd 绝对不仅限于云原生用例。
在本章中,我们将探讨如何使用 Fluentd 与 Docker 和 Kubernetes 结合。我们应该认识到,Docker 和 Kubernetes 的更高级配置并非易事;这两种技术都值得拥有许多专门的书籍。我们可以将这些不同技术视为“蛋糕”的层,这些层构成了云原生微服务开发平台——每一层都增加了更多的复杂性、抽象和扩展。通常,每一层都假设对前一层的理解。操作系统提供了一个坚实的基础,容器通过 Docker 提供第一层。下一层是容器编排——对我们来说就是 Kubernetes(但其他如 Mesos 和 OpenShift 也存在)。可以添加另一层来提供像 Istio 或 Linkerd 这样的服务网格。然而,由于它们带来了从遥感到互信 TLS 的另一层组件,我们选择不涉及这一点。
然而,我们想要通过这些层的小部分来了解日志如何逐层适应每种技术。为了获得这个视角,我们假设您对 Docker 和 Kubernetes 有一个基本的概念理解。Docker 和 Kubernetes 的解释将仅限于高层次,因为我们旨在提供关于如何应用 Fluentd 和预构建解决方案以支持日志的见解。我们将尽可能保持设置和不同点的说明最小化,以便方法和考虑不需要对每一层有深入的实际经验。到本章结束时,您将掌握这些概念,并看到如何部署 Fluentd 与 Docker 和 Kubernetes 一起工作。如果您想了解更多关于这些技术的信息,附录 E 提供了额外的书籍资源推荐。
8.1 从 Docker Hub 获取开箱即用的 Fluentd
上一章展示了各种部署配置,包括适用于 Kubernetes 环境的模式。这些用例可以直接使用 Fluentd 和其他提供并由中央 Docker Hub 仓库发布的预定义容器来解决(hub.docker.com/r/fluent/fluentd/)。容器已配置,以便可以传递一个位置来写入输出日志文件——这允许使用适当的挂载点,并允许从容器外部访问日志,从而避免容器终止时丢失日志的问题。除了日志文件的位置外,我们还可以传递自己的自定义 Fluentd 配置,如果默认设置不足。默认设置包括以下内容:
-
端口 24224 用于接收使用转发插件的日志。
-
标记为
Docker.**的日志被写入到/fluentd/log/docker.log.。 -
所有其他日志都发送到
/fluentd/log/data.*.log.。
8.1.1 官方 Docker 镜像
根据 Docker Hub,Fluentd 有一个官方镜像。官方镜像意味着我们可以确信该镜像正在得到维护,并且可以在 hub.docker.com/_/fluentd 找到(你需要至少一个免费的 Docker Hub 账户才能访问此链接)。这不是唯一可用的 Fluentd 提供的 Docker 镜像,但其他镜像并不提供相同的保证。除了官方镜像外,另一个特别感兴趣的镜像是由 DaemonSet 提供的,这在第二章中首次提到。你可能还记得,DaemonSet 提供了一种确保每个 Kubernetes 工作节点(主机机器)运行一个提供基础服务的 pod 的方法,例如日志记录和基础设施健康监控。如果你想将它们作为起点使用,Docker 文件可以在 Fluentd GitHub 仓库中找到。
如果你搜索 Docker Hub 上的 Fluentd,你会找到数百个条目。这是因为许多组织(包括许多希望让你轻松将日志事件发送到他们产品的供应商)使用 Fluentd 并有自己的镜像配置。值得记住的是,官方 Docker 镜像仅包括核心插件。要使用你自定义的或社区贡献的插件,需要修改 Docker 镜像以检索该插件并安装它,以及任何依赖项。值得考虑的是你选择使用的 Docker 镜像的来源。这样做将使我们能够跟踪镜像提供者是否维护最新的补丁和发布到操作系统和软件中,包括镜像中的 Fluentd。
8.1.2 Docker 日志驱动程序
日志驱动程序的目的在于捕获 stdin、stdout 和 stderr(即你会在控制台上看到的内容)的输出流,并将它们导向一个合适的目的地;否则,这些信息将会“消失在虚空中。”Docker 默认提供了一些捆绑的日志驱动程序,包括
-
Fluentd——与主机机器上的 Fluentd 前向端点通信。
-
JSON 文件——默认设置;使用 JSON 格式将事件存储在文件中。
-
local——基于文件存储,专为 Docker 操作优化。
-
Syslog——与 Syslog 产品集成。
-
journald——一个使用与 Syslog 相同 API 但产生更结构化文件的守护程序服务。它随 systemd 一起提供,systemd 提供了除 Linux 内核之外的一系列操作系统服务(
mng.bz/XWZ6)。 -
GELF——Graylog 扩展日志格式;被多个日志框架如 Graylog 和 Logstash 采用(
docs.graylog.org/en/4.0/pages/gelf.html)。 -
ETW 日志——Windows 日志事件(
mng.bz/y4vq)。 -
Google Cloud Platform、AWS CloudWatch、Rapid7、Splunk——一些为他们的服务提供日志驱动程序的供应商和平台。
除了这些 Docker 提供的日志驱动程序之外,你还可以构建自己的。但除非你想要将 Docker 日志与某个产品或平台紧密耦合,否则有很多选项无需进行开发。
8.1.3 为 Docker 日志驱动程序做好准备
要使用 Fluentd 日志驱动程序,我们需要安装 Docker(以及本章后续部分所需的 Kubernetes)。由于这些技术在 Windows 和 Linux 之间存在显著差异,我们将调整我们的方法以适应这两个平台(这种做法承认了许多人在 Windows 机器上工作,但在生产中经常使用 Linux)。微软和 Docker 已经做出了几个重大进步,允许 Linux 容器在 Windows 服务器上运行。这是通过使用 Windows Linux 子系统(WSL)实现的,但并非所有版本的 Windows 操作系统都提供 WSL(但如果你有使用 WSL 的条件,这是一个很好的前进方式)。在本章中,我们将专注于 Linux 容器。这意味着 Windows 用户需要与 WSL、Hyper-V 或 VirtualBox 合作。在附录 A 中,我们提供了帮助你设置的资源。
8.2 使用 Docker 日志驱动程序
Docker 提供了控制日志发生情况的方法。默认情况下,Docker 使用一个 JSON 日志驱动程序,它将日志写入stdout和stderr(即我们的控制台,除非你已更改了这些输出的路由)。有两种方法可以控制日志驱动程序,要么在 Docker 运行命令中添加额外的参数,要么修改 Docker 配置。区别在于,命令行方法意味着你可以为特定的 Docker 容器使用替代配置。命令行方法的缺点是每次都需要提供参数。
8.2.1 通过命令行使用 Docker 驱动程序
对于我们第一次使用日志驱动程序,我们将使用命令行方法。这是调整日志驱动程序行为最不具侵入性的方法;因此,实验配置控制涉及的最小破坏性更改。
我们将继续在我们的主机计算机上运行 Fluentd 配置来接收和输出日志事件。首先,我们将按照附录 A 中的指导,在 Linux 虚拟机(虚拟机)内部运行Hello-World Docker 镜像。如果你的主机操作系统是 Linux,这可能会显得有些奇怪,但这种方法有以下优点:
-
网络层的清晰分离,因为虚拟化层将提供除 Docker 层网络抽象之外的单独网络层。
-
保持所需的虚拟机数量和虚拟化产生的资源开销最低。
-
无论主机操作系统如何,结果都将相同。如果你的主机是 Windows,这尤其有益,因为它有助于强调 Fluentd 是平台无关的。
个人来说,这将在我的 Windows 10 Pro 主机上运行,该主机运行 Hyper-V 和 Ubuntu 18 LTS 虚拟机。这意味着我们将使用 Ubuntu 来运行 Docker 容器。我们可以将部署可视化如图 8.1 所示。

图 8.1 操作系统层、虚拟化和容器化层的使用,以确保我们的主机环境不会因为仅使用 Docker 而受到干扰
8.2.2 快速检查网络连接
获取正确的网络配置是使用 Docker、Kubernetes 和虚拟机时的重要考虑因素。这意味着始终值得进行快速简单的检查,以确保网络连接按预期工作,例如使用 curl 或 Postman 将 HTTP 日志事件发送到 Fluentd。为了帮助实现这一点并使用 Fluentd 日志驱动程序,我们已准备了一个简单的 Fluentd 配置,将接收到的任何内容发送到stdout。我们可以像以前多次做的那样,使用以下命令启动 Fluentd:
fluentd -c Chapter8/Fluentd/forwardstdout.conf
一旦 Fluentd 在 Linux 环境中运行,我们可以执行第二章中使用的“Hello World”测试的变体。在以下配置和命令中,我们需要将w.x.y.z替换为 Linux 虚拟机看到的宿主机的 IP 地址。您可以在 Windows 上使用ipconfig命令和在 Linux 主机上使用ip addr show命令(ifconfig也可能工作,但已弃用)来获取机器的 IP 地址。我们的测试命令在 Linux 虚拟机或容器上必须是
curl -X POST -H "Content-Type: application/json" -d '{"foo":"bar"}' http://w.x.y.z:18080/test
这应该在 Fluentd 在主机上运行的控制台上显示 JSON 详细信息{"foo":"bar"}。
严格的绑定控制
严格的绑定控制可以是一件非常好的事情。它们允许我们在处理可能位于具有多个网络连接的机器上的组件时应用安全控制,无论这些连接是物理的还是虚拟的(如 Docker 和 Kubernetes 环境中的情况)。对于forward等输入插件,绑定配置属性将确保 Fluentd 调用将通过相关网络进行。但是,当 Docker 和 Kubernetes 创建网络地址时,我们必须更加警觉。当连接失败时,很容易开始检查主机防火墙、网络配置等等。实际上,目标系统因为只监听一个特定的网络连接而存在故障。
8.2.3 运行 Docker 命令行
在设置和检查了我们的部署,特别是网络之后,我们可以继续使用 Docker 守护进程。我们不会构建自己的 Docker 镜像,而是从 Docker Hub 网站检索传统的“Hello World”镜像。hello-world Docker 镜像很简单,当人们在使用 Docker 时,这是一个好的起点。该镜像的详细信息可在hub.docker.com/_/hello-world找到。
我们可以通过使用标签来指定特定的版本。标签添加在名称之后,用冒号分隔。由于hello-world Docker 镜像已经按照使用latest标签为最新稳定版本的习惯进行了标记,我们可以在命令中添加:latest。这可以通过在虚拟机上运行以下 Docker CLI 命令来完成
docker pull hello-world:latest
我们没有对 Docker 配置进行任何更改,这意味着当请求 Docker 守护进程运行我们的镜像时,我们将看到标准的 Docker 日志驱动程序行为。虽然 Docker 日志的位置可能不同,但通常我们应该在/var/lib/docker文件夹中找到它们,在那里我们将看到一个名为containers的文件夹。我们可以将其视为图 8.2 的第一部分中突出显示的内容,每个容器实例都使用其唯一的 ID 创建了自己的文件夹。当然,最初不会有任何容器。现在有了镜像的本地副本,我们应该通过 CLI 命令告诉 Docker 守护进程运行hello-world镜像
docker run hello-world
如果我们现在刷新对文件夹 /var/lib/docker/containers 的视图,该文件夹将新增一个条目,在图 8.2 的第二部分中被突出显示。在新容器的文件夹结构中,我们将看到一个具有长名称的日志文件(例如,Docker 镜像实例;例如,b361e69a1 . . .)。进入容器的文件夹,我们将看到该 Docker 实例的资源,包括一个名为local-logs的文件夹(在图 8.2 的第三部分中被突出显示)。最后,进入local-logs文件夹,我们可以看到名为container.log的容器日志文件(在图 8.2 的第四部分中被突出显示)。由于日志文件以自己的自定义格式存储(图 8.2 的第五部分),其内容将无法阅读。

图 8.2 显示了包含 Docker 和每个容器实例的文件夹结构,随后是容器及其以自定义方式编码的日志的列表(屏幕截图中的数字在前面文本中已解释)
为了使事情更加实用,我们希望配置 Docker 使用更易于消费的格式来记录日志。我们可以覆盖默认设置,使 Docker 使用 Fluentd 日志驱动程序。这是通过告诉 Docker 守护进程使用带有参数-–log-driver=fluentd的替代方案来完成的。由于 Fluentd 驱动程序包含在 Docker 的部署中,我们不需要做更多的事情。我们还需要告诉驱动程序在哪里可以找到我们的 Fluentd 节点以接收日志事件。这和其他配置选项是通过参数-–log-opt来提供的,后面跟着一个由等号(=)分隔的名称-值对。在我们的情况下,我们需要提供主机机的 Fluentd 的地址(就像之前的 curl 命令一样)。由于 Docker 日志驱动程序可以使用转发插件(并从 msgpack 提供的压缩中受益),我们需要确保包括端口号在内的网络地址被提供。这导致运行hello-world的命令如下所示:
docker run –-log-driver=fluentd --log-opt fluentd-address=w.x.y.z:28080 hello-world
执行该语句的结果将是看到 Docker 镜像的日志事件在 Fluentd 控制台上输出。如果 Docker 命令返回错误消息,例如
docker: Error response from daemon: failed to initialize logging driver: dial tcp w.x.y.z:28080: connect: connection refused.
那么网络或 Fluentd(例如,它没有绑定到正确的网络)存在问题。启动 Docker 镜像和目标 Fluentd 节点时的顺序也应该被注意。当迁移到与 Kubernetes 的容器编排时,这一点尤其重要,因为 Kubernetes 管理 pods 启动的顺序。在出现此类问题时,我们建议检查 Docker 配置的网络端口值,以确保允许容器外的网络流量。如果发生任何端口号映射,那么这是可以的。
Fluentd 驱动程序可以使用 Fluentd 提供的任何标准功能,例如使通信异步(即利用内存缓冲区功能;更多内容请参阅第九章)。但当我们转向完整的配置时,我们将探讨更多这些功能。
在图 8.3 中,我们可以看到运行我们的命令所生成的输出。注意日志事件中包含以下属性:
-
container_id—容器的完整 64 字符 ID,唯一标识单个容器。 -
container_name—容器启动时的容器名称。启动后的任何重命名操作都不会反映出来,直到重启。 -
source—详细说明日志是否来自stdout等。 -
log—来自源的内容(例如,stdout的一行)。

图 8.3 从 Docker 执行hello-world容器收到的 Fluentd 控制台输出
8.2.4 通过配置文件切换到驱动程序配置
证明参数化解决方案后,我们可以以更可读的方式推进配置,并添加相关的进一步选项。考虑到所有可能的配置选项,使用命令行进行高级配置将是一项具有挑战性的维护任务。默认情况下,更改 Docker 守护进程配置文件将影响所有正在运行的 Docker 镜像。Docker 命令行还允许我们使用参数–-config指向配置文件,后跟备用配置文件的名称。
Docker 守护进程将其配置(包括日志驱动程序配置)保存在一个名为daemon.json的文件中。对于 Linux 设置,文件的默认位置是/etc/docker/。如果你在 Windows 上使用 Docker 实例(而不是我们选择的间接方法),位置是ProgramData\docker\ config\(ProgramData通常位于 C 驱动器的根目录)。如果 Docker 设置完全运行在默认值上,则该文件可能不存在。
在守护进程配置文件中,我们明确希望包括日志驱动程序类型和连接到我们的 Fluentd 实例的设置。为此,我们在 JSON 文件中包含命令行参数的配置版本"log-driver": "fluentd"。在命令行中,我们还提供了fluentd-address属性。当涉及到fluentd-address时,我们可以提供地址为tcp://w.x.y.z:28080或作为对相关套接字文件的显式路径引用(例如,unix:///usr/var/fluentd/fluent.sock)。
除了地址之外,我们还应该直接引入与日志驱动程序和其他与日志相关的通用参数相关的几个附加参数。我们包括的一般设置是
-
raw-logs—应设置为true或false。如果指定为false,则应用完整的 ANSI 时间戳(例如,YYYY-MM-DD HH:MM:SS),并通过使用转义码关闭日志文本的着色。 -
log-driver—如用于设置日志驱动程序的命令行示例所示。 -
log-level—应用于 Docker 守护进程的日志过滤器阈值。接受的级别是debug、info、warn、error和fatal,默认为info。
在配置文件中,我们可以启动一个名为 log-opts 的内部属性组;这些特定的日志选项包括
-
env—我们可以要求驱动程序捕获并包含特定的环境变量。这通过定义一个以逗号分隔的列表来完成。就我们的目的而言,我们可以使用"os, customer"。这假设已经设置了这样的值。也可以通过使用属性env-regex定义这个的正则表达式版本。 -
labels—这与env非常相似,在某种程度上,可以指定标签列表(Docker 元数据名称-值对),或者可以通过labels-regex提供正则表达式。 -
fluentd-retry-wait—每次连接失败后,在再次尝试之前应用一个等待期。该值需要包括持续时间类型(例如,s表示秒,h表示小时)。 -
fluentd-max-retries—在放弃之前尝试连接的最大次数。默认值为4294967295——即(2**32 - 1)。我们不希望事情因为那么多次重试而挂起。鉴于我们已经将重试设置为每秒一次,最多 10 分钟的重试就足够了,这意味着值为600。 -
fluentd-subsecond-precision—允许我们在硬件支持的情况下,将时间戳精度设置为毫秒级。虽然默认值是false,但明确设置它是有意义的,即使它只是默认值。通过明确设置值,我们会提醒自己我们不会拥有这样的精度。 -
tag—与日志事件记录关联的标签。这可以使用 Docker 定义的符号(附录 A 中有完整列表)构建。在我们的情况下,让我们使用以下表示法定义标签:{{.ID}}-{{.ImageID}}。 -
fluentd-address—与命令行配置一样,这是与 Fluentd 服务器通信的位置。这与参数方法一样,需要根据 Fluentd 实例的主机 IP 地址进行定制。
满足这些其他需求的结果意味着我们到达了列表 8.1 中所示的代码。以调试模式运行 Docker 守护进程是确保配置文件正确处理的最简单方法。这意味着这是一个守护进程服务,我们需要使用以下命令停止当前进程
sudo service docker stop
列表 8.1 第八章/Docker/daemon.json 配置文件,用于 Docker Fluentd 日志驱动程序
{
"log-driver" : "fluentd", ❶
"log-level": "debug",
"raw-logs": true, ❷
"log-opts": {
"env": "os,customer",
"labels": "production_status,dev",
"fluentd-retry-wait": "1s",
"fluentd-max-retries": "600",
"fluentd-sub-second-precision": "false",
"tag": "{{.ID}}-{{.ImageID}}", ❸
"fluentd-address": "w.x.y.z:28080" ❹
}
}
❶ 这告诉 Fluentd 使用 Fluentd 版本的日志驱动程序。
❷ 这设置 Docker 使用原始日志,因此不使用格式化,并应用 ANSI 时间戳。
❸ 这定制了用于日志事件的标签。
❹ 这指定了日志驱动程序中 Fluentd 服务器的位置。
一旦服务停止,我们需要将修改后的守护进程配置文件复制到默认位置 /etc/docker/。然后我们可以使用以下命令手动启动进程
sudo dockerd -D
这将启动 Docker 以调试模式,从默认位置获取配置。如果配置文件有任何问题,Docker 守护进程将几乎立即停止或生成有关无法解析配置的警告。信息将显示在控制台上,例如
unable to configure the Docker daemon with file /etc/docker/daemon.json: invalid character '\n' in string literal
一旦文件读取正常,Docker 守护进程将日志事件直接发送到我们的 Fluentd 实例,包括运行 Hello-World Docker 镜像时的输出。由于我们之前的命令已在前台启动了 Docker 守护进程,我们需要使用另一个 shell 来运行 Docker 镜像。我们可以使用之前的相同命令:
docker run hello-world
如果你感到勇敢,可以直接跳转到再次以服务形式运行 Docker。这意味着终止当前以调试模式执行的 Docker 守护进程。然后执行以下命令
sudo service docker start
当你对配置文件(daemon.json)的任何进一步更改有信心时,而不是手动运行 Docker 守护进程,我们可以采用简单地重启守护进程以强制它获取最新配置的方法。这是通过将 start 命令替换为 restart 来实现的。例如:
sudo service docker restart
假设你想验证配置属性是否已被 Docker 守护进程接受。在这种情况下,你可以运行命令 docker --info,这将显示所有正在使用的设置,包括控制台上的默认值。
8.3 Kubernetes 组件日志记录和 Fluentd 的使用
Kubernetes 的本质及其高度可插拔的模型意味着其生态系统可能会变得复杂。为了说明这一点,如果我们看看 Kubernetes 的容器化方面,Docker 可能是今天最占主导地位的容器技术。然而,通过 API 模型,Kubernetes 允许我们使用其他容器技术,如 containerd (containerd.io/) 和 cri-o (cri-o.io/),这两者都受 CNCF 管理。部分复杂性通过 Open Container Initiative (opencontainers.org/) 得到解决,它也受 CNCF 管理,有助于抽象容器实现与 Kubernetes 容器编排之间的交互。这里的基本问题是这如何影响我们以及 Fluentd 的使用?
这里重要的是,正如我们所看到的,我们可以配置 Docker 以捕获通过 stdout 和 stderr 传播的事件;因此,其他容器是否支持这种功能?并非所有容器在日志记录方面都像 Docker 那样成熟。许多容器只是与 Kubernetes 内部日志框架 klog (github.com/kubernetes/klog) 保持一致,该框架在部署时采用 journald 的日志方法,否则将日志记录到默认文件位置。
Klog 的演变
Klog 追溯到 Google C++ 库(github.com/google/glog)。由于 Kubernetes 是用 Go 实现的,C++ 库不是一种选择,因此在这个过程中,开发了一个 Go 实现(github.com/golang/glog)。从那时起,Kubernetes 开发者确定 glog 在容器化方面存在一些挑战,因此分叉了代码库,从而产生了 klog。API 基本上保持不变。在所有情况下,日志机制都经过优化以实现最佳性能;因此,插入和配置日志很大程度上取决于使用该库的应用程序提供的命令行选项,而不是配置文件。
8.3.1 Kubernetes 组件和结构化日志
在 Kubernetes 组件中,今天结构化日志的应用是一个不断发展的旅程。Kubernetes 中的所有组件尚未全部采用结构化日志(尽管这一状况正在改变)。我们应该准备好应对未来可能出现的任何额外系统组件或扩展可能不应用结构化日志的可能性。这强化了这样的建议:最好是积极采用第七章中概述的日志和部署模式(如 Fluentd 作为边车模式、与应用程序内嵌等),而不是试图从 Kubernetes 中提取日志。
8.3.2 Kubernetes 默认日志保留和日志轮转
当日志由于容器配置从容器传入 Kubernetes 时,Kubernetes 将将日志条目推送到每个容器实例的日志文件中。为了管理大小和日志轮转,我们的责任是建立一个日志轮转工具,它可以控制日志文件的数量以及它们轮转的频率。
Kubernetes 没有自己的日志轮转器;处理日志轮转挑战是 Kubernetes 工作节点部署者的责任。话虽如此,如果工作节点是使用 Kubernetes 提供的脚本(kube-up.sh,mng.bz/M25n)设置的,它将部署开源工具 logrotate(github.com/logrotate/logrotate)。logrotate 可以配置为保留指定数量的文件。某些 Linux 发行版已经部署了 logrotate,因此这是一个额外的配置问题。logrotate 的设置可能因 Linux 发行版而异,这仅仅是因为 Linux 配置的应用方式不同。某些发行版使用 systemd,而 logrotate 是其一部分。在 logrotate 还未部署的地方,通常可以通过选择 Linux 发行版的包管理器独立安装。
Logrotate 作为解决方案并不是跨平台的,因此,在 Windows 上运行 Kubernetes 需要另一个答案来实现日志轮转,这在查看 Kubernetes 关于此主题的讨论时并不明显,而且更具挑战性。无论日志轮转如何,klog 生成的日志在达到 1.8 GB 时会自动截断。因此,任何日志轮转都需要在达到该阈值之前建立。
当容器被移除时,Kubernetes 会自动删除所有除了当前日志文件之外的所有文件。如果捕获此类日志事件的进程落后太多,可能会丢失日志事件——在建立日志捕获时需要考虑的问题。
从这个例子中我们可以看出,在 Kubernetes 层面管理日志会面临挑战,这些挑战可能基于部署方法和基础设施设置的差异。因此,我们倾向于通过专注于我们能够看到更多一致性和更多控制手段的层中的日志捕获来最小化问题。我们无法完全忽视 Kubernetes 日志,但在其他地方拦截日志事件意味着 Kubernetes 日志事件的丢失并不那么关键。
Kubernetes 部署的简化
对于解决方案来说,简化 Kubernetes 部署是必要的。在 Kubernetes 生态系统中,已经开发了几种工具,如 Helm (helm.sh) 和 Rancher (rancher.com),以简化挑战。Helm(更占主导地位的解决方案)甚至将自己称为 Kubernetes 的包管理器。鉴于 Helm 的主导地位,Fluentd 的贡献者已经开发了 Helm 配置文件(称为 charts)以支持 Fluentd 部署。这些 charts 整合并定义了特定于部署的独特配置细节,然后 Helm 使用模板和脚本完成剩余部分。GitHub Fluentd 存储库中包含的 DaemonSet chart (github.com/fluent/helm-charts) 提供了一个基本起点,让您只需为特定需求应用配置。如果您参与 Kubernetes 部署的常规开发,我们建议调查 Helm 并利用 Fluentd charts。
8.3.3 使用 kubectl 进行日志记录
如您可能已经知道,kubectl 是与 Kubernetes 交互的主要 CLI 工具。当 Kubernetes 理解日志被写入的位置时,我们可以利用 kubectl 执行各种任务,例如跟踪一个或多个日志文件、将日志转发到不同的端口,以及支持日常的日志文件活动。而不是描述 kubectl 日志命令,所有详细信息都可以在 kubectl 命令参考中找到,请参阅mng.bz/aDJB。
8.4 使用 Kubernetes 展示日志
我们需要收集 Kubernetes 进程日志,并了解内部容器进程,如 Kubelet 是否在记录错误。Kubernetes 有许多机制可以帮助我们检查容器的健康状况。然而,了解 Kubernetes 中一切运行正常,对于知道容器是否得到妥善照顾,或者节点是否缓慢失败至关重要。如果一个应用程序正在控制台记录日志,事件流向 Kubernetes,那么我们在哪里检索这些事件?
为了解决这个问题,我们将部署一个预构建的包含 LogSimulator 的 pod,该 pod 配置为将日志事件定向到stdout。日志事件将通过容器机制传播,并让 Fluentd 在 Kubernetes 层拦截它们,因此我们将捕获 Kubernetes 和容器内部日志。这可能会反映十二要素应用中描述的推荐设置(12factor.net/logs)。但在许多方面,它代表了一个最坏的情况,因为我们必须投入精力来推导上下文(在stdout中分离可能来自平台或容器而不是应用程序的多个日志事件等)并重新结构日志事件。
在这一点上,如果你还没有按照附录 A 安装 minikube 作为我们的 Kubernetes 实现,那么现在正是理想的时间。一旦完成,你的环境将看起来像图 8.4 中所示的结构。

图 8.4 操作系统层、虚拟化和容器化使用的层次结构,以确保我们的宿主环境不会被 minikube 打扰
8.4.1 Kubernetes 设置
为了展示保持配置简洁的 Kubernetes 配置,我们将使用 minikube。Minikube 是 Kubernetes 的一个版本,经过精简以保持尽可能小的足迹。如果你还没有按照附录 A 中的说明进行操作,那么在 Linux 虚拟机上执行的第一步就是这一步。这也恰好是 Marko Lukša 在《Kubernetes 实战》一书中使用的 Kubernetes 实现(mng.bz/g4wE)。一旦 minikube 安装完成,让我们启动它并使用 Kubernetes 仪表板查看初始状态。我们使用以下命令在 Windows 上执行此操作:
minikube start --vm-driver hyperv --hyperv-virtual-switch "Primary Virtual Switch"
Linux 的等效版本是
minikube start --vm-driver docker
这将建立一个单节点“集群”的 Kubernetes,简化到最小。本章的下载包包含 Linux shell 和 Windows 批处理脚本,这些脚本将执行此命令(使每次记住或复制命令变得容易得多)。然后我们可以在 Windows 或 Linux 上使用以下命令启动仪表板:
minikube dashboard
此命令启动一个前台进程,该进程将部署必要的 pod 以运行仪表板 UI 并提供仪表板页面 URL。当仪表板页面打开时,我们可以使用 UI 的左侧菜单进行导航,以查看当前部署了哪些守护进程集、部署和 pod(作为菜单的工作负载部分)。如图 8.5 所示,目前没有部署任何守护进程集。您将找到基本的 hello-minikube 部署及其关联的 pod 正在运行。

图 8.5 在 minikube 上运行的 Kubernetes 仪表板,目前只显示默认命名空间,而不是 kube 系统,或者大多数守护进程集将运行的地方。
使用所有命名空间简化导航
通过将 Kubernetes 标志旁边的下拉菜单设置为“所有命名空间”而不是默认(如图 8.5 所示)来简化 UI 导航,这将使查看详细信息更容易。否则,您在导航 UI 时可能会遇到障碍,想知道为什么看不到预期的信息,例如 Fluentd 守护进程集。在我们的环境中,显示所有内容(即,所有命名空间)不会有问题,尽管在生产设置中,我们不会推荐这样做。
8.4.2 创建日志以捕获
我们首先需要一个应用程序来生成日志事件,这样我们就可以观察一个日志守护进程集从 Kubernetes 和不直接将日志发送到端点(即,它们只是将日志发送到 stdout 和 stderr)的应用程序收集事件。为此,我们可以使用 LogSimulator 的容器化版本。默认情况下,该工具的容器化版本配置为循环遍历一个简单的数据集多次,然后停止。每个日志事件都简单地写入 stdout;因此,日志事件将被 Kubernetes 收集。当 LogSimulator 容器完成其运行时,它将停止容器,此时 Kubernetes 将介入以重启部署。LogSimulator Docker 镜像已经在 Docker Hub 中存在。以下列出的是在 pod 中使用此 Docker 镜像的 Kubernetes 配置,可以从 mng.bz/5KQB 获取,该配置显示如下。由于不需要任何配置或外部端点,YAML 配置非常简单。
列表 8.2 第八章/LogGenerator/Kubernetes/log-simulator-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: log-simulator
labels:
app: log-simulator
spec:
replicas: 1
selector:
matchLabels:
app: log-simulator
template:
metadata:
labels:
app: log-simulator
spec:
containers:
- name: log-simulator
image: mp3monster/log-simulator:v1 ❶
❶ 这是 Docker Hub 镜像的引用,当部署时,将会被下载。如果需要部署此 pod 的新版本,必须在名称末尾的版本引用(即,:v1)进行更新。如果没有这样做,Kubernetes 将忽略请求,因为它已经有了该版本的 LogSimulator。
要部署此 pod,您需要确保环境变量 LogSimulatorHome 已定义,它引用了之前已安装的 LogGenerator 的根文件夹。或者,编辑提供的脚本(deploy-log-sim-k8.bat 或 deploy-log-sim-k8.sh),将环境变量引用替换为绝对路径。如果您使用脚本,它将始终尝试先删除任何可能存在的 pod 部署以确保安全。这意味着如果您想重新部署,则只需使用脚本。然后,在 shell 中,输入以下语句以自行发出部署命令:
minikube kubectl -- apply -f %LogSimulatorHome%\Kubernetes\log-simulator-deployment.yaml --namespace=default
Minikube 应该会确认部署为成功。
minikube CLI 和 kubectl 之间的差异
kubectl 和 minikube 命令之间的差异很小。Minikube 包装了 kubectl 的使用,以便 minikube 命令可以提供额外的命令和 kubectl 命令。如果您已安装 kubectl,则可以配置它将指令直接指向 Kubernetes 的 minikube 实例。然后,您可以将命令的前一部分,即显示为 minikube kubectl –- 的部分,替换为仅 kubectl。对于 Linux 主机,此方法的替代方法是向 Linux 环境中引入别名。这是通过使用命令 - alias kubectl="minikube kubectl --" 来完成的。现在,当您使用命令 kubectl 时,Linux 将将其替换为完整表达式。如果您发现自己需要经常在调用前加上 sudo 以确保正确的权限,则可以将此也包含到别名中。
理解 LogSimulator 的视图
在我们继续查看 DaemonSet 之前,值得“稍微窥视一下引擎盖下”看看正在发生的事情。因为我们之前已经启动了 Kubernetes 仪表板,我们可以使用它来帮助我们。我们需要访问 pod 列表(左侧菜单选项);因此,我们将看到类似于图 8.6 中显示的列表。我们需要访问 log-simulator pod 实例,这可以通过点击以 log-simulator 开头的名称来完成。

图 8.6 Kubernetes 仪表板显示包含我们的 LogSimulator 和 Fluentd 的 pod 实例
这将显示有关特定 pod 的详细信息,屏幕顶部将类似于图 8.7 中显示的详细信息。

图 8.7 显示特定 log-simulator 实例的 Kubernetes 仪表板
如您在图 8.7 中所注意到的,图像右上角有四个图标。点击第一个图标将显示类似于图 8.8 的视图。该图显示了 LogGenerator 生成的 stdout。

图 8.8 我们的 LogGenerator 实例的控制台(stdout),其简单配置生成要由我们的 Fluentd 设置收集的事件
虽然这很有用,并确认容器按预期运行,但我们还需要知道 Kubernetes 将输出推送到哪个文件,因为我们需要针对该文件设置一个 tail 输入插件。回到屏幕上,我们在图 8.7 中看到我们想要使用基于箭头的图标(从左数第二个),因为这将为我们提供正在执行的容器的 shell 视图。
值得注意的是,登录到由这个容器镜像提供的 shell 中,因为你会看到与应用程序相同的环境。任何容器探索都需要迅速完成——一旦 LogGenerator 完成生成日志事件,它就会停止。结果,我们的容器会死亡,带着我们的会话一起结束。如果你试图查看存在的日志,你不应该看到任何东西,因为日志事件将被捕获在运行 Kubernetes 的主机上,而不是在容器中。通过这样做,我们清楚地确立了我们的 Kubernetes 容器第一个要求的事实——即需要访问主机文件系统来收集生成的日志。
8.4.3 理解 Fluentd DaemonSet 的构建方式
我们第一次接触 Kubernetes 是在第二章,我们简要地查看了一下使用 Fluentd 提供的 DaemonSet。我们说过 Kubernetes 配置会变得复杂。然而,考虑到我们为 LogGenerator 使用的Kubernetes.yaml,这个问题很容易被质疑。让我们花点时间来了解一下 Fluentd 的 Kubernetes 和 Docker 资源所涉及的内容。与此相关的有几个关键仓库,具体如下:
-
Kubernetes DaemonSet in GitHub—这是大多数必要的实现细节所在 (
mng.bz/6ZXo)。 -
Docker 文件基础镜像—(
github.com/fluent/fluentd-docker-image)。这些是 Docker 基础镜像,包括用于帮助生成不同操作系统变体的模板机制。 -
Docker Hub 仓库—这是从 Docker 镜像中拉取 Kubernetes 配置的地方 (
hub.docker.com/u/fluent)。一个或多个 Docker 镜像根据提供的配置形成一个 pod。 -
元数据过滤器—这被集成到 DaemonSet 中 (
mng.bz/oa2d)。元数据过滤器通过添加额外的上下文来丰富 Kubernetes 日志记录,帮助你更好地理解正在发生的事情。
当你访问 Fluentd 的 DaemonSet GitHub 仓库时,你会看到一系列 YAML 文件。Fluentd 社区提供了一系列标准化的配置,用于捕获 Kubernetes 日志并将内容发送到单个目的地。这些配置包括将内容转发到另一个 Fluentd 节点,发送到 AWS、Azure 和 Google 提供的各种云原生服务,以及像 Graylog、Loggly 这样的专用服务,以及更常见的目标 Elasticsearch 和 Syslog。
当检查 Kubernetes YAML 配置时,你会发现它们在本质上都非常相似,具有以下特点:
-
设置镜像,使其将在 kube-system 命名空间中部署
-
引用合适的容器镜像
-
定义可以在相关 Fluentd 配置文件中使用以连接外部服务的环境变量——通常是目标解决方案的主机和端口等详细信息
-
指定应分配给容器的资源数量
-
定义需要在容器内可见的主机文件位置——特别是
/var/log和/var/lib/docker/containers——以及路径在容器内应该如何显示。
配置中没有显示的是,可以设置和传递一些额外的环境变量,从而进一步改变容器的行为;例如,是否尝试与 systemd 交互。但我们很快就会看到这一点。我们可以假设“真正的魔法”发生在容器中,因此也在 Docker 文件中。
Kubernetes Docker 镜像
如果你查看存储库根目录下的 README,你会看到一个 Docker 拉取命令列表,其中每个守护进程类型都有一个或多个引用。查看列表时,你会注意到它们已经被分为两大组:x86_64 镜像和arm64_images。这种需求可能直到我们想起 Docker 文件最终必须引用特定于计算机硬件的二进制文件时才变得明显。这是在虚拟化或容器化解决方案中使用更通用的包管理器的一个缺点。这意味着我们有很多 Docker 镜像需要维护。
Kubernetes Docker 镜像也是使用模板生成的,但我们可以将活动描述为以下事情:
-
建立对相关 Docker 镜像的依赖
-
设置 Ruby 和 Gem,包括将环境变量定义到适当的位置
-
安装各种 gem 文件
-
配置涵盖 Fluentd、systemd、Kubernetes、Prometheus 的文件
以github.com/fluent/fluentd-kubernetes-daemonset/tree/master/docker-image/v1.12/arm64/debian-forward/conf,为例,我们可以详细检查配置和文件关系。图 8.9 也提供了文件关系的视觉表示:
-
Fluent.conf位于根容器中,并使用include机制引入其他配置文件的内容,正如我们在第五章中看到的。此配置文件还有一个匹配项,在转发守护进程集的情况下,匹配所有日志事件并将它们发送到目标服务器。值得注意的是,转发配置不包括任何安全措施(没有 TLS 等);如果日志不敏感,这不是问题。但如果它们是敏感的,那么您需要用包含必要配置的配置文件替换它们。我们将在本章后面看到如何做到这一点。systemd 和 Prometheus 的配置受环境变量控制,具体来说,存在
FLUENTD_SYSTEMD_CONF和FLUENTD_PROMETHEUS_CONF的设置。需要
Kubernetes.conf,因此它被包含在内。最后,conf.d文件夹中的任何配置都被包含,因此可以通过任何特定的自定义来扩展配置。![图片]()
图 8.9 配置文件之间的关系及其受环境变量影响的表现
-
Prometheus.conf文件很简单。它定义了 Prometheus 输入插件的使用以及Prometheus_output_monitor用于监控 Prometheus。环境变量定义了服务器的地址,包括bind、port以及如果指标 URI 不同,则使用变量FLUENTD_PROMETHEUS_BIND、FLUENTD_PROMETHEUS_PORT和FLUENTD_PROMETHEUS_PATH分别定义path。 -
Systemd.conf文件定义了 Docker、Kubelet(使用 systemd 源插件)的来源(Kubernetes 节点控制器和 bootkube 服务)。值得注意的是,此插件独立于 Fluent Git 仓库,并且是独立编写的(详细信息请参阅github.com/fluent-plugin-systemd)。 -
Kubernetes.conf文件是配置中包含的最有趣的部分。像 systemd 一样,它也使用外部插件,这次是一个名为kubernetes_metadata的过滤器(详细信息可在github.com/fabric8io/fluent-plugin-kubernetes_metadata_filter找到)。过滤器的任务是将额外的元数据合并到日志事件中或从日志事件中排除。这是通过使用环境变量FLUENT_FILTER_KUBERNETES_URL或结合KUBERNETES_SERVICE_HOST和KUBERNETES_SERVICE_PORT与 Kubernetes API 端点通信来完成的。这需要从日志事件中获取元数据以获取从 Kubernetes API 获取信息的基本上下文。信息可以来自 journald,如果正在使用它,或者可能来自日志文件名。与插件一起使用的某些属性要么假定默认值,要么直接嵌入到容器中。可以配置的控制映射到插件属性如下:-
KUBERNETES_VERIFY_SSL—verify_ssl设置一个标志,指示是否应该检查 SSL/TLS 证书。如果你的环境有用于证书的证书颁发机构,我们建议将其设置为是。 -
KUBERNETES_CA_FILE—此属性提供了 Kubernetes 服务器证书验证的 CA 文件路径。 -
FLUENT_KUBERNETES_METADATA_SKIP_LABELS—如果设置为true,则不要从元数据中检索标签。 -
FLUENT_KUBERNETES_METADATA_SKIP_CONTAINER_METADATA—如果设置为true,则不会包含与容器镜像和image_id相关的元数据。 -
FLUENT_KUBERNETES_METADATA_SKIP_MASTER_URL—如果设置为true,则不会包含master_url元数据。 -
FLUENT_KUBERNETES_METADATA_SKIP_NAMESPACE_METADATA—如果设置为true,则不会包含如namespace_id这样的元数据。 -
FLUENT_KUBERNETES_WATCH—当设置为true时,它告诉插件监视 Kubernetes API 服务器持有的 pod 元数据的变化。
-
为了让过滤器执行任何有意义的事情,配置需要包含源。在这种情况下,使用了多次 tail 源插件来捕获在文件夹 /var/log/containers/*.log、/var/log/salt/minion、/var/log/startupscript.log、/var/log/docker.log、/var/log/etcd.log、/var/log/kubelet.log、/var/log/kube-apiserver.log、/var/log/kube-controller-manager.log、/var/log/kube-scheduler.log、/var/log/rescheduler.log、/var/log/glbc.log、/var/log/cluster-autoscaler.log 和 /var/log/kubernetes/kube-apiserver-audit.log 中生成的任何日志。你可能已经认出了这些是核心 Kubernetes 进程的日志文件。基于此,我们应该看到日志事件被捕获,只要我们的容器日志事件被写入 Kubernetes 主机上的 /var/log/containers/ 文件夹中的某个位置。
Kubernetes 中的 Fluent Bit
我们主要关注 Fluentd 与 Docker 和 Kubernetes 结合作为主要方式,以提供一种灵活的方式来捕获日志事件。但在容器化环境中,考虑到 Fluent Bit 具有更小的占用空间,应该考虑使用它,尤其是如果目标是将日志事件推送到专门的 Fluentd 节点或日志分析平台,如 Elasticsearch,并使用这些部分来执行“重负载”。值得注意的是,Fluent Bit 在 GitHub 上有自己的项目,提供 Docker 基础设置,并扩展不同环境和操作系统(如 Debian、CentOS、Raspbian 和 Amazon Linux)的 Docker 镜像。这些 Docker 镜像支持一些可能的目标,以及作为 Kubernetes 中的 DaemonSet 部署的 Fluent Bit 配置。
8.5 查看主机日志
在本章早期,我们窥视了 LogGenerator 容器可以看到的环境,并确定它看不到主机上的任何部分,因此看不到任何日志。这是因为我们没有配置容器挂载文件系统。文件夹挂载不是必需的,因为我们信任容器捕获stdout并将内容放在合适的位置。然而,当我们演示这种行为时,我们控制着 Docker。现在 Docker 将由 Kubernetes 管理。此外,我们需要考虑如何监控 Kubernetes 本身。对预构建的 Fluentd 资源的审查表明日志内容位于/var/log。Minikube 提供了一个方便的工具,允许轻松访问主机环境。一旦我们可以访问主机,我们就可以检查环境以定位相关的日志文件并了解需要捕获的内容。使用新的 shell(Windows 或 Linux),我们可以使用以下命令
minikube ssh
这将为我们提供一个安全的 shell 进入主机环境。让我们使用以下命令查看当前配置中看到的文件夹
ls -al /var/log/containers
结果可能并不像预期的那样,因为这是一个指向/var/log/pods中文件的符号链接文件夹,如图 8.10 所示。幸运的是,每个人都可以看到这些链接。

图 8.10 查看文件夹/var/log/containers的结果——你可以看到列出的文件作为 pods 文件夹中另一个文件的符号链接
如果我们跟随链接到/var/log/pods,我们会看到每个链接解析到一个文件夹,该文件夹反映了 pods 的实例,如图 8.11 所示。

图 8.11 查看文件夹/var/log/pods的结果,结果竟然是目录
检查一个之前部署到默认命名空间(因此有default_前缀)的 pod 文件夹,例如log-simulator,我们看到一个包含递增日志文件编号的文件夹和另一层符号链接,如图 8.12 所示。

图 8.12 /var/log/pods中的一个 pod 文件夹的内容,这些文件夹再次是文件系统另一部分的符号链接
通过链接进入 /var/lib/docker/containers 会带来一个新的挑战——权限被大大限制,我们需要使用 sudo 命令来列出文件夹的内容,如图 8.13 所示。

图 8.13 这里可以看到 /var/lib/docker/containers 受限的内容;请注意非常受限的权限。
如果我们查看这些文件夹中的一个,我们会发现图 8.14 中显示的日志文件。但最后几个步骤只有在提升权限的情况下才有效。这也帮助我们理解了预定义配置中使用的不同文件路径。这意味着在 YAML 文件中,我们需要确保挂载具有适当的权限。这也确认了容器内部的路径与主机相同。

图 8.14 在克服了限制之后,我们可以看到 /var/lib/docker/containers 的内容,其中包括一个真正的日志文件而不是另一个符号链接。
我们还应该注意,minikube 的日志访问安全性非常粗粒度,因此,如果你可以看到一个日志,那么如果你与主机交互,你将能够访问所有日志。在生产环境中,从安全角度来看,这并不是非常理想。结论是,如果你对容器化环境中的日志敏感,请直接使用如第七章中讨论的 sidecar 等模式来控制可见性。更积极地控制你容器的日志事件意味着你不会受到 Kubernetes 中访问控制管理方式的影响。
在文件系统中导航可以清楚地看出,Kubernetes 的配置并非易事。这让我们回到了这样一个观点:我们能够在容器和应用程序层面进行更多监控,事情就会变得更容易。
8.6 配置 Kubernetes 日志 DaemonSet
根据概述,我们可以合理地假设我们可以复制配置 YAML 来建立用于日志记录的 Kubernetes DaemonSet。我们可以利用 Fluentd 在我们的配置中提供的现有 Docker 镜像。YAML 配置需要提供特定的环境变量值并将文件系统的正确部分挂载。如果我们想让 DaemonSet 也应用一些自定义配置,我们需要将额外的配置文件映射到系统中。
而不是设置很多环境值来控制当前的 Fluentd 配置,我们可以看看如何将 Fluentd 指向一个替代配置文件并注入修改后的配置。这也给了我们机会来解决 Docker 文件中的内容布局问题,这可能会影响下游应用程序。
8.6.1 准备 Fluentd 配置以供使用
使用定制的 Fluentd 配置,我们可以通过修改 Docker 构建来部署它。然而,一种更优雅的方法是利用 Kubernetes 配置的功能。这意味着如果我们想更改配置,我们只需要重新部署配置更改,而不是更改 Docker 镜像及其重新部署的后续步骤。这是可能的,因为 Fluentd Docker 文件通过环境变量和适当放置的附加配置文件来配置 Fluentd,这些配置文件可以通过 include 语句获取。
在建立 Fluentd 配置后,我们需要使其准备好被容器消费。我们将使用 Kubernetes 配置映射来完成此操作,我们将在稍后对其进行更多解释。我们需要首先将配置映射部署到 Kubernetes 中,以便进行引用。配置映射可以包含在我们的核心 Kubernetes YAML 文件中,或者我们可以使用 Fluentd 文件,将其转换为合适的格式,并单独部署配置。后一种方法更可取,因为我们可以使用第三章中看到的 Fluentd dry-run 功能来验证配置,然后再部署。如果配置嵌入在更大的配置文件中,验证步骤将无法进行。
Fluentd 配置映射与 kube-system 命名空间相关联,以匹配标准 Fluentd 守护进程集部署到该命名空间的事实。使用此命名空间是有意义的;在这种情况下,我们正在配置和部署一个 Kubernetes 全局服务。列表 8.3 显示了我们要引入的 Fluentd 配置。如您所见,它从容器和 Pod 中收集日志并将日志事件发送到可配置的目标。我们还需要注意配置映射的名称(fluentd-conf),因为它将在 YAML 文件中引用。与 LogSimulator 部署一样,我们捆绑了一个批处理和 shell 脚本,移除了任何以前的配置(deploy-config.bat和.sh)。
要部署配置文件,我们需要使用 minikube 命令:
minikube kubectl -- create configmap fluentd-conf –from-file=Fluentd/custom.conf --namespace=kube-system
如果您想确认部署,请使用仪表板查看配置并选择配置映射(从左侧菜单)。然后,在仪表板的中心部分,您将看到所有的配置映射,包括我们的fluentd-conf。通过点击我们的配置映射名称,可以显示配置映射的内容。您可能会看到每行以\r;结尾,这是 Linux 对回车符的编码,在文件处理时不会出现任何问题。
列表 8.3 第八章/Fluentd/custom.conf 覆盖 Kubernetes 的配置
<system>
Log_Level debug
</system>
<source>
@type tail
path /var/log/containers/*.log
read_from_head true
read_lines_limit 25
tag deamonset
path_key sourcePath
emit_unmatched_lines true
<parse>
@type none
</parse>
</source>
<source>
@type tail
path /var/log/pods/*/*.log
read_from_head true
read_lines_limit 25
tag deamonset2
path_key sourcePath
emit_unmatched_lines true
<parse>
@type none
</parse>
</source>
<source> ❶
@type tail
path /var/lib/docker/containers/*.log
read_from_head true
read_lines_limit 25
tag deamonset3
path_key sourcePath
emit_unmatched_lines true
<parse>
@type none
</parse>
</source>
<match *>
@type forward
<buffer> ❷
buffer_type memory
flush_interval 2s
</buffer>
<server>
host "#{ENV['FLUENT_FOWARD_HOST']}" ❸
port "#{ENV['FLUENT_FOWARD_PORT']}"
</server>
</match>
❶ 此源是安全文件系统的一部分,需要设置权限以允许 Fluentd 读取。
❷ 我们设置了一个非常短暂的缓冲区,以便我们可以快速看到事件流过;鉴于我们的部署,网络开销不是问题。
❸ 允许通过 Kubernetes 配置文件驱动对服务器的寻址
通过 Kubernetes 将内容传递到容器
Kubernetes 提供了多种方式将内容作为挂载路径共享到容器中。选项的数量如此之多,以至于《Kubernetes in Action》有数章专门讨论这个主题。本质上,不同的技术可能会影响容器是否可以修改文件系统,存储是否在容器生命周期之后持久化等问题。ConfigMap 是不可变的(只读),这对于我们的场景是理想的。根据使用的选项,ConfigMap 的内容可以通过环境变量、命令行值和文件来消费。然而,它们的大小有限,所以如果希望传递一个要重放的日志文件,可能不适合使用。
当将文件共享到由 Kubernetes 管理的容器中时,我们必须注意,接收共享文件夹的容器中已经存在的任何内容将被共享内容覆盖。因此,将新配置推送到容器需要谨慎操作。例如,在标准的 Docker 设置中仅替换 Kubernetes.conf 文件是不明智的,因为它与所有配置文件共享一个公共文件夹。通过采用在容器中使用的将新配置文件放入不同位置并修改 Fluentd 通过环境变量获取的配置文件路径的方法,我们可以避免此类问题。这意味着如果我们愿意,我们可以包括标准的 Kubernetes 和 Prometheus 配置,并在必要时替换它们。
8.6.2 创建我们的 Kubernetes 部署配置
让我们调整并部署标准 Fluentd 仓库的 DaemonSet 到我们的 minikube 环境中。为此,我们需要本地下载文件,因为我们需要做一些调整。这可以通过使用原始视图中的 wget 命令(mng.bz/OGpE)或使用 git clone 命令来完成。我的首选是 wget(这是在 Docker 文件等中检索大量内容的最简单方法),其命令如下:
wget https://raw.githubusercontent.com/fluent/fluentd-kubernetes-daemonset/master/fluentd-daemonset-forward.yaml
我们需要添加以下内容并进行以下修改:
-
设置我们想要将日志事件转发到的 Fluentd 节点的位置值。这意味着我们应该将文本
REMOTE_ENDPOINT替换为主机机的地址或 IP(例如,192.168.1.2)。FLUENT_FOWARD_PORT的值也需要从18080更改为28080。这是为了反映我们在 Fluentd 节点配置中使用的端口。在生产环境中,我们始终建议使用 DNS 地址。这样,环境中的任何变化或通过负载均衡扩展 Fluentd 都会从配置中隐藏。这需要了解 Kubernetes 中 DNS 的处理方式,但这不是本书的主题。
-
在 YAML 的
env部分中添加环境变量FLUENTD_SYSTEMD_CONF,并将其值设置为"FALSE"。 -
在同一部分覆盖
FLUENTD_CONF环境变量,使其指向我们通过 ConfigMap 提供的custom.conf文件。 -
在 YAML 文件的容器部分添加一个
securityContext部分,将privileged属性设置为true以克服之前确定的权限挑战。 -
为我们的 ConfigMap 添加
volumeMount条目,以便定义路径。这意味着在volumeMounts部分添加一个额外的名称config-volume,mountPath为/fluentd/etc/custom。 -
我们接着将名为
config-volume的卷部分(链接卷和VolumeMounts)作为一个附加条目引用,其属性为configMap,然后通过name引用,这是我们之前设置的fluentd-conf。
由于我们对现有配置进行了定制,您仍然会参考 Docker 文件中定义的 Docker 镜像,网址为 mng.bz/p2dz。实际的 Docker 镜像将来自 Docker Hub,网址为 mng.bz/QWvG。定制 Kubernetes YAML 文件的结果可以在下面的列表中看到。
列表 8.4:针对我们的需求修改的 Chapter8/Kubernetes/fluentd-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-system
labels:
k8s-app: fluentd-logging
version: v1
spec:
selector:
matchLabels:
k8s-app: fluentd-logging
version: v1
template:
metadata:
labels:
k8s-app: fluentd-logging
version: v1
spec:
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1-debian-forward ❶
env:
- name: FLUENT_FOWARD_HOST ❷
value: "192.168.1.2"
- name: FLUENT_FOWARD_PORT
value: "28080"
- name: FLUENTD_SYSTEMD_CONF
value: "FALSE"
- name: FLUENTD_CONF ❸
value: "custom/custom.conf"
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: config-volume ❹
mountPath: /fluentd/etc/custom/
securityContext: ❺
privileged: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: config-volume ❻
configMap:
name: fluentd-conf
❶ 我们继续参考预构建的 Fluentd Docker 镜像设置,用于管理 Kubernetes 的日志。
❷ 通过环境变量设置要转发日志事件的 IP 和端口
❸ 覆盖 Fluentd 启动时使用的配置文件的位置
❹ 定义了容器将看到的挂载位置。然后,此路径映射到 Kubernetes 外部的文件系统,确保日志可以安全保留。
❺ 为了使容器能够访问在探索宿主服务器时看到的受限制的文件夹和文件,设置了额外的安全设置。
❻ 将卷映射到之前加载到 Kubernetes 中的 ConfigMap
8.6.3 将 Fluentd for Kubernetes 的实现付诸实践
利用第三章和第七章中获得的理解,并利用现有的 Fluentd 配置文件为 DaemonSet(见 mng.bz/XWZv)构建一个简单的单个配置文件,该文件可以跟踪 Kubernetes 集群中的任何相关文件。随着日志事件的收集,将它们转发到之前使用 Docker 日志驱动配置的 Fluentd 节点。
答案
下载包中提供了一个简单的 Fluentd 配置,如列表 8.4 所示,我们将利用它进行以下步骤。但您也可以将您的配置替换到下一步中。
8.6.4 在 minikube 上部署
在部署 DaemonSet 之前,我们应该启动本地 Fluentd 实例,准备接收日志事件。这与我们之前多次使用的命令相同。
fluentd -c Chapter8/Fluentd/forwardstdout.conf
下一步是将我们的 DaemonSet 部署到 Kubernetes:
minikube kubectl -- apply -f Kubernetes/fluentd-daemonset.yaml --namespace=kube-system
再次,我们在下载包中创建了一个批处理和 shell 脚本,名为 deploy -deamonset.bat 或 .sh,它将删除任何现有的部署并将当前配置推送到 Kubernetes。你可以通过选择菜单选项“守护集”(在左侧导航菜单下的“工作负载”下)使用仪表板确认其他资产的守护集部署;这应该列出 fluentd。点击标题将显示包含我们的容器的 pod 的详细信息。
如果 Kubernetes 处于稳定的空闲状态,我们可能立即看不到任何日志。我们可以通过重新部署我们的 LogSimulator 配置来解决此问题,就像我们在本章前面使用脚本 deploy-log-sim-k8 所做的那样。这将迅速导致 Kubernetes 发送各种事件,包括来自 LogSimulator 容器的 stdout 日志事件。我们可以在图 8.15 中看到输出示例。

图 8.15 接收到的日志事件在我们的接收 Fluentd 节点
8.6.5 整理
在运行 Kubernetes 并检索各种 Docker 镜像和 minikube 资产后,你会达到想要清除或刷新环境的地步。其中一个简单但出色的功能是 minikube 可以通过单个命令完全清除环境以释放资源,因为你已经完成,或者如果你想再次验证一切,可以重置并重新开始。这是通过以下命令完成的
minikube delete
8.7 Kubernetes 配置实战
我们已经建立了一个基本配置,使我们能够看到 Kubernetes 环境中的日志事件。然而,该配置尚未达到企业级。为了达到企业级,我们需要改进该配置。你的挑战是确定必要的更改,并按照提供的配置进行必要的修改。
8.7.1 答案
你已识别的更改应包括以下要点:
-
尾部源没有在文件中记录其跟踪位置;因此,重启可能会重复日志事件。
-
记录的
pos_file需要映射到一个挂载点,这样如果 pod 重新启动,位置信息就不会丢失。 -
尾部配置需要解决日志轮转管理的问题。
-
应该提供额外的 Kubernetes 指标和 Prometheus 信息,并通过配置进行控制。
-
Kubernetes 核心组件(在 kube-system 命名空间中)的日志应该单独标记为托管应用程序和接收端,以分离标签。
-
利用 Kubernetes 插件为日志事件添加额外的元数据。
-
调整 Fluentd 中的缓存以更符合生产环境,并考虑 Kubernetes 提供的资源。
-
应该将日志级别从调试移动到信息。
8.8 需要监控和记录的更多 Kubernetes
我们已经解决了 Kubernetes 的核心日志考虑因素,但还有一些额外的领域您应该了解,可能需要进一步考虑。Kubernetes 正在持续发展和快速扩展。因此,某些功能可能不会提供,因为运行的部署不是最新版本,或者如果您使用的是托管服务,服务提供商可能根据不同的能力以不同的方式实现了某些功能。当然,您可能希望将 Kubernetes 与 Istio 或 Linkerd 等网格框架叠加,这将有自己的日志。我们认为以下领域是跟踪核心 Kubernetes 最有价值的领域。
8.8.1 节点监控
到目前为止,我们关注的是与我们的容器相关的核心日志。但您可能还希望监控 Kubernetes 节点基础结构的健康状态。在这方面有各种选择,包括在本地节点上使用 Fluentd 或 Fluent Bit 并监控服务器的原始统计信息。然而,在某些环境中可能不允许这样做。Kubernetes 还提供了一个名为 Node Problem Detector 的额外 DaemonSet 服务。
节点问题 DaemonSet 是 minikube 的一个可选附加组件,以及一些云供应商和其他人提供的预构建 Kubernetes 集群也采用这种方法。因此,需要启用 DaemonSet。节点问题检测器监控内核日志文件,并根据配置报告特定问题,这些配置可以通过 ConfigMap 覆盖,就像我们修改 Fluentd 配置一样。检测器包括一个导出元素,将信息发送到不同的端点,包括 Kubernetes API 服务器和 Stackdriver,后者与我们的 Fluentd Daemon 集成。
更多关于此服务的信息可以在 github.com/kubernetes/node-problem-detector 找到。
8.8.2 终止消息
在 pod 的配置中,可以配置与 pod 终止相关的信息记录。因此,在发生异常终止的情况下,可以进行事后诊断。在容器配置中,可以通过 terminationMessagePath 属性(默认为 /dev/termination-log)为 Kubernetes 指定容器中终止消息的路径。我们需要验证 Kubernetes 配置确保日志事件被导向 Fluentd 可以抓取的位置,或者 Fluentd 知道如何从 Kubernetes 获取这些信息。更多相关信息可以在 mng.bz/y4aB 找到。
摘要
-
默认的 Docker 日志驱动程序以这种方式工作,即无法通过标准的 Fluentd 插件直接跟踪其日志文件(例如,使用压缩)。
-
Fluentd 可以用作 Docker 的日志驱动程序,这使得访问和使用 Docker 日志事件变得更加容易。
-
Fluentd GitHub 仓库包括预定义的 DaemonSet 配置。为了适应操作系统差异以及将日志直接路由到如 Elasticsearch 等服务的可能性,预构建镜像中也提供了替代的 Fluentd 配置。
-
Kubernetes 配置,如 minikube,相当复杂,通过符号链接的间接层次使得在想要监控的情况下确定哪些文件是真实日志变得困难。
-
利用 Kubernetes 的 ConfigMaps 功能,可以定制或扩展开箱即用的 Fluentd 配置。因此,预构建的 Fluentd Docker 镜像可以捕获日志事件并将它们发送到不同的 Fluentd 节点。
9 创建自定义插件
本章涵盖了
-
为 Redis 开发自定义 Fluentd 插件
-
使用 Fluentd 实用工具加速开发
-
实现 Fluentd 插件的生命周期方法
-
测试和打包自定义 Fluentd 插件
-
为自定义插件创建文档
在本书的各个部分,我们提到了 Fluentd 对插件开发的支持,这些插件不仅限于核心产品提供的那些。Fluentd 的可扩展性导致了强大的第三方插件生态系统,这使得捕获、过滤、操作并将它们发送到许多不同的系统和数据存储变得容易。我们还讨论了当现有插件无法轻松或高效地完成任务时,自定义插件如何连接和监控一些特殊和遗留解决方案。
在本章中,我们将详细介绍创建一个利用 Redis 列表功能的输入和输出插件的过程。我们将更深入地了解 Redis 及其使用理由。
开发 Ruby
本章确实需要一些开发经验。尽管如此,您不需要成为核心 Ruby 开发者就能将这里展示的信息付诸实践。像大多数语言一样,一旦您在一种语言上有所开发经验,您就可以利用这种理解来开始掌握其他语言。我把自己归入这一类,因为我已经通过 Ada、C、C++编程生涯,然后在过去的 15 年或更长时间里使用 Java。我们不会再写另一本 Ruby 书;其他人已经在这方面做得非常出色,例如 David A. Black 和 Joseph Leo III 的《The Well-Grounded Rubyist》,第 3 版(Manning,2017)。我试图提供足够的细节,让您在没有先学习语言的情况下就能理解我们用 Ruby 和代码所做的工作。毕竟,我们的目标是帮助您理解 Fluentd 中发生的事情。在附录 E 中,我们包括了链接到资源,以帮助您学习 Ruby 的基础知识,并更好地理解使用的工具,或者如果您接受练习,您可能想要使用它们。
9.1 插件源代码
这里开发的插件代码包含在本书的下载中,或可从我们的 GitHub 仓库中获取(mng.bz/M20W)。如果您想在此基础上进行开发,我们鼓励您 fork GitHub 仓库,并按您的意愿进行开发;您可能想要考虑的增强机会包括
-
转向使用 RedisTimeSeries 功能(稍后将有更多介绍)
-
开发支持 Fluentd 基于块大小的缓冲
-
增强与 Redis 连接的安全性(例如,使用 SSL/TLS 连接和凭证,如用户名和密码)
无论您采取何种方法,我们唯一的要求是您承认这本书是代码的起点。
9.2 什么是 Redis,为什么需要使用具有 Redis 列表功能的插件?
Redis 是一个围绕名称-值对构建的开源、可扩展的内存存储解决方案。它能够定义数据元素上诸如生存时间(TTL;这意味着定义了保留数据的时间,之后它将自动删除)等详细信息,使其成为一款卓越的缓存工具。(在附录 E 中,我们提供了关于 Redis 的许多支持性参考,除了 Ruby 的之外。)
除了使用 Redis 来帮助展示插件开发之外,开发此类插件还有一些潜在的实际好处:
-
我们可以使用一个高度弹性的开源选项,而不是使用小型嵌入式内存缓存,这可以在扩展和跨多个服务器复制缓存数据方面更加有效(有关缓存使用的更多信息,请参阅
techterms.com/definition/cache)。 -
它为启用 Fluentd 节点高效协作提供了额外的选项。
-
它与 Redis 列表创建集成,例如 Ruby Resque(Ruby 队列实现;
github.com/resque),提供了支持或使用额外服务的机会。 -
它提供了一种保持事件顺序的方法,因为 Redis 列表支持先进先出(FIFO)模式;这允许我们按顺序(即按时间序列)保持日志事件。
如 Redis 这样的缓存解决方案通过在内存数据结构中存储数据而不是像传统数据库那样的长期存储来运行。这些结构将数据作为键值对处理。内部结构可以非常复杂,以便缓存能够快速定位正确的内存块。此外,数据还可以作为事件的时间序列来处理。存储的数据也可以在自动从存储中删除之前有一个 TTL(生存时间)。
9.2.1 Redis 列表与 RedisTimeSeries
在撰写本文时,Redis Labs 开发了高性能的 RedisTimeSeries,它以由两个 64 位结构组成的时间序列格式处理数据,分别代表时间和值(更多信息请参阅 oss.redislabs.com/redistimeseries)。使用它带来了一些挑战:
-
没有支持 Windows 原生选项,我们希望尽可能保持简单,无需通过不同的 Linux 虚拟化方法进行操作。
-
RedisTimeSeries 使用的数据库结构无法在 128 位中存储日志事件,这意味着为了使用此结构,值部分必须充当另一个数据结构的“外键”。将示例解决方案扩展到企业级能力可能是有价值的,但这会增加与 Redis 相关的复杂性,而不是 Fluentd 的,因此不会对本书有益。
考虑到这些点,我们将坚持使用纯 Redis 功能。
9.3 使用 Redis CLI 展示我们的目标
在我们开始查看任何开发活动之前,让我们模拟我们想要通过插件实现的行为。模拟行为将使我们将开发活动与解决方案联系起来变得更容易。为此,我们首先需要安装 Redis,使用附录 A 中提供的信息。一旦 Redis 安装完成,我们将使用其命令行界面(CLI)来模拟我们将要构建的插件的效果。
Redis 服务器需要在它自己的 shell 中启动。这可以通过以下命令完成
redis-server
一旦我们看到该过程运行的证据(Redis 向控制台报告),下一步就是在新的控制台窗口中使用命令redis-cli启动 Redis CLI。我们可以通过命令提示符的变化来识别命令是否成功执行,命令提示符将看起来像
redis 127.0.0.1:6379 >
我们可以通过输入命令 info 来确认我们有一个合适的 Redis 服务器连接。这将导致显示大量的设置信息,包括在控制台中显示的 Redis 版本。当有一个redis-cli运行时,我们需要在另一个 shell 中重复此过程来模拟两个不同进程与缓存交互的效果。在本节的其余部分,我们将将其称为 CLI 1 和 CLI 2。随着我们进行模拟,想象 CLI 1 作为 Fluentd 输出插件,CLI 2 作为 Fluentd 输入插件。在 CLI 1 中,我们想要运行以下列表推送(lpush)命令:
lpush fluentd '{"tag":"demo", "timestamp" : 1606076261, "record" : {"blah" : "blah 1"}}'
lpush fluentd '{"tag":"demo", "timestamp" : 1606076263, "record" : {"blah" : "blah 2"}}'
lpush fluentd '{"tag":"demo", "timestamp" : 1606076267, "record" : {"blah" : "blah 3"}}'
每次发出命令时,你将得到一个看起来像(integer)1 的响应。一开始,我们将看到这个响应随着列表深度的变化而变化。我们可以使用列表长度(llen)命令
llen fluentd
以找出列表中有多少条目。这些命令中对fluentd的引用是我们正在使用的列表的名称。注意我们如何在记录的开始和结束处使用单引号,这样我们就可以在 JSON 中使用双引号。现在,在 CLI 2 中,发出列表弹出(lpop)命令:
lpop fluentd
因此,你将看到 CLI 1 中提供的第一个条目在 CLI 2 控制台上显示。重复命令lpop fluentd,响应将包括添加的第二个记录。在 CLI 1 上添加更多条目,并在 CLI 2 上继续弹出,直到你看到 nil 响应。这意味着列表已被清空。
CLI 1 实际上是我们在模拟中的输出插件行为,CLI 2 是我们的源插件行为。这可以帮助我们的 Fluentd 设置,通过平滑活动中的峰值。Fluentd 输入和输出节点本质上是瞬时的(在容器化环境中可能也是如此)。在这种情况下,我们有高效的内存存储日志事件(在内存中保留事件不会遭受存储到磁盘的相同 I/O 性能影响)。
9.4 开发准备
在我们开始任何开发之前,需要进行一些基本的准备。你需要有
-
准备了 Fluentd 和 Ruby 的安装,或者继续使用与前面章节一起使用的现有安装(如果您想从头开始,所有必要的步骤都详细说明在附录 A 中)。
-
如果您还没有这样做,请准备一个简单的 Redis 安装(有关此信息的详细信息请参阅附录 A)。
-
选择了一个可以支持 Ruby 的 IDE(集成开发环境),并安装了 IDE 和任何相关的扩展(我们将使用 Visual Studio Code)。由于这通常是个人选择,我们将选择和安装的决定权留给你。
-
安装了 Redis Gem 以用 Ruby 代码与 Redis 通信(详细信息请参阅附录 A)。
-
为实现我们的插件建立了一个文件夹;这可能与你检索本书所有支持文件时创建的文件夹结构相一致。
我们不希望我们的开发工作无意中污染 Ruby 的使用和 gem 的目录。实现这一目标的一种方法是在启动时允许我们的开发代码被 Fluentd 获取,而不是要求它被打包成 gem 并部署到测试代码中。也有方法将开发代码与 gem 文件系统混合,但我们不推荐这样做。
在本章的剩余部分,我们将假设路径以c:\myDev\GitHub|LoggingInAction\Chapter9开始。一旦这些组件就位,我们就可以开始工作了。
9.5 插件框架
正如你在整本书中看到的,Fluentd 与不同类型的插件(输入、输出、过滤器、解析器和格式化器)的强大基础一起工作。Fluentd 需要强制实施一些通用机制,包括命名约定和文件夹结构,以便插件能够工作,并告诉 Fluentd 核心已安装的不同插件。为了帮助实现这一点,Fluentd 包括一些工具来确保插件开发符合所需的约定。插件有一个类层次结构,每个插件类型都有一个可以构建的类。图 9.1 展示了这个类层次结构。

图 9.1 我们可以构建插件的基础类,所有这些类都位于源树的 lib/fluent/plugin 文件夹中
9.5.1 创建骨架插件
Fluentd 提供了一个用于构建我们插件骨架框架的工具。在执行此操作之前,您需要位于您想要用于 Fluentd 开发的正确位置(例如,您自己的 GitHub 项目的根文件夹),因为它将使用该文件夹作为所有工件的开端。一旦到达正确的位置,我们就可以使用命令fluent-plugin-generate <plugin type> <plugin name>。插件类型表示插件类型(输入、输出等),以及插件名称。我们将从输出插件开始。在我们的情况下,这使完整的调用:
fluent-plugin-generate output redislist
我们选择了名称redislist,因为该插件专门与 Redis 的列表功能一起工作。与大多数数据存储一样,已经存在一个通用目的的 Redis 插件(mng.bz/aDP7);你可能还想构建一个插件来监控其健康状态。
使用该实用程序的结果,我们得到了如图 9.2 所示的目录结构,其中包含一些骨架配置和代码文件。随着我们进入本章,我们将逐一处理每个文件。

图 9.2 当我们运行 fluent-plugin-generate 实用程序时生成的目录结构和文件。颜色表示需要修改以完成插件的内容,如键所示。
我们将在插件代码中展示的代码位于/lib/fluent/plugin文件夹中,如图 9.2 所示,Ruby 文件基于插件名称(例如,out_redislist.rb)。我们将在接下来的几节中开发的代码将放入此文件。
9.5.2 插件生命周期
插件会经历一个生命周期,每个阶段都有一个方法,如果需要实现生命周期特定阶段逻辑,则可以覆盖该方法。正如我们在开发输入和输出插件时将看到的,我们不必使用每个阶段,但几乎所有情况下某些阶段是必不可少的——例如,配置、启动和关闭。每个状态还有一个查询函数,用于确定当前状态是否为该状态——例如,after_shutdown? 图 9.3 展示了插件支持的完整生命周期以及每个阶段的目标。

图 9.3 插件通过这个生命周期,使用骨架中提供的方法进行覆盖,并在每个生命周期阶段实现所需的特定逻辑。并非所有状态都需要覆盖。
9.6 实现插件核心
我们已经生成了骨架,并花了一点时间查看生命周期阶段;现在插件可以注入任何所需的具体行为。我们现在可以着手编写处理配置和执行日志事件处理的插件代码,我们还将涵盖与 Redis 的连接和断开连接。
9.6.1 配置属性如何工作
插件结构化后,第一步是定义它将使用的配置属性。这是通过使用config_param对象来实现的,它接受以下值:
-
在配置文件中使用的属性名称。
-
属性的数据类型,可以是字符串、整数等。
-
如果可以指定,则默认值。
-
当 Fluentd 执行干运行或其配置的启动输出时,值是否应该保密。默认情况下,这是 false,因此不需要。但为了说明行为,我们已包含其使用。
-
定义属性名称别名的功能。别名在插件随时间变化时可能很有帮助。
我们需要捕获端口号、主机地址(一个 DNS 名称或 IP 地址),以及 Redis 持有的列表名称。为此,我们需要在配置文件中定义几个配置属性。代码是一系列desc和config_param语句的序列,其中desc提供了代码中出现的后续config_param的描述。我们知道数据类型和潜在的默认值意味着我们应该利用可用的属性来简化数据验证。这将使用户体验尽可能简单。因此,我们最终应该得到如下所示的代码。
列表 9.1 第九章/fluent-plugin-out-redislist/lib/fluent/out_redislist.rb
desc "specifies the port to connect to Redis with, will
➥ default to 6379 if not specified" ❶
config_param :port, :integer, default: 6379, secret: false, alias:
➥:portNo ❷
desc "Defines the host address for Redis, if not defined
➥ 127.0.0.1"
config_param :hostaddr, :string, default: "127.0.0.1",
➥ secret: false ❸
desc "Defines the name of the list to be used in Redis, by
➥ default this is Fluentd"
config_param :listname, :string, default: "fluentd"
desc "Defines the number of reconnection attempts before giving
➥ up on connecting to the Redis server."
config_param :reconnect_attempts, :integer, default: 2
desc "Defines the number of seconds before timing out a connection
➥ to the Redis server"
config_param :connection_timeout, :integer, default: 5
desc "Defines the number of log events in a chunk"
config_param :chunksize, :integer, default: 20
❶ 在每个 config_param 之前提供 desc 条目意味着可以使用工具生成插件配置的文档,并提供内部文档。
❷ 这是一个设置整型配置值并设置默认值的示例。我们还可以指示 Fluentd 在启动时生成配置摘要并输出它;我们可以通过秘密参数告诉 Fluentd 是否应包含该值。我们还告诉 Fluentd,如果它接收到 portNo 作为配置值,则将其用作端口号源。
❸ 与前面的示例一样,我们正在设置一个默认值。这次我们提供一个字符串。
一旦定义,我们就可以将值作为类级别元素进行引用,并在代码的其余部分使用 Ruby 的@符号(例如,@port)。
我们还应该将 Redis 连接作为类成员持有,这样我们就不需要在每次与 Redis 交互时重新创建连接。有了配置细节,我们也可以尝试现在创建 Redis 连接。然而,根据生命周期定义,这样做是不正确的,如果另一个插件配置失败,Fluentd 可能选择不启动我们的插件。因此,我们会消耗不必要的资源,例如网络连接。但是,我们需要通过在文件顶部使用require "redis"声明将 Redis 依赖项合并到代码中。
注意:在编码风格方面,我在插件中创建必要的步骤作为小函数。这可能会显得效率低下,但编译器、解释器和语言虚拟机中的优化器通常可以优化掉这些开销。这种方法确实使得测试更容易,并且可以单独检查每个步骤。
框架提供了一个 configure 函数,如生命周期中所示(见图 9.3)。这给了我们实现任何额外自定义验证的机会。我们还可以使用此方法来定义我们自己的类级别变量。为了说明这种能力,我们将实现代码来检查定义的网络端口是否是默认的 Redis 端口。如果不是默认端口,记录一条警告信息提醒开发者他们需要确保端口不会冲突。此代码如下所示,包括我们的检查函数和配置函数的实现,该函数使用了任何继承的行为。
列表 9.2 Chapter9/fluent-plugin-out-redislist/lib/fluent/out_redislist.rb
def check_port(conf)
log.trace "checkport invoked"
port = conf['port'] ❶
if (port != RedislistOutput::DefaultPort) ❷
log.info ("Default Redis port in use")
else
log.warn ("Non standard Redis port in use - ensure ports are deconflicted")
end
end
def configure(conf) ❸
super ❹
checkPort (conf ❺
end
❶ 使用定义的名称从配置属性列表中检索配置值
❷ 我们在类中定义了一个用于默认端口的常量。
❸ 这是作为插件生命周期的一部分被调用,并触发我们的 check_port 方法的方法。
❹ 确保首先执行任何继承的逻辑
❺ 调用我们的方法,将所有配置数据传递过去
9.6.2 启动和关闭
在配置之后,生命周期中最关键的功能将是大多数插件的启动和关闭函数。这些是创建或关闭连接的理想时刻,例如与 Redis 的连接。由于建立到存储解决方案的连接通常相对较慢,我们希望在积极与远程解决方案通信之前执行此任务。Redis 允许我们定义连接的超时时间和我们可以尝试重新连接的次数。我们将把这些设置为配置值,但现在让我们将它们硬编码。如果连接建立失败,我们应该在插件生命周期中使其易于识别。我们可以通过将连接器设置为 nil 并记录问题来实现这一点。
我们需要将以下列表中的启动和关闭函数整合到我们的插件和辅助函数中,以确保实现所需的行为。
列表 9.3 Chapter9/fluent-plugin-out-redislist/lib/fluent/plugin/out_redislist.rb
def connect_redis()
log.trace "connect_redis - Create connection if non existant"
if !@redis
begin
@redis=Redis.new(host:@hostaddr,port:@port,connect_timeout:@connection_
➥ timeout,reconnect_attempts:@reconnect_attempts) ❶
log.debug "Connected to Redis "+@redis.connected?.to_s
rescue Redis::BaseConnectionError, Redis::CannotConnectError => conn_err ❷
log.error "Connection error - ", conn_err.message, "\n connection
➥ timeout=", @connection_timeout, "\n connection attempts=",
➥ @reconnect_attempts
@redis = nil
return nil
rescue => err
log.error "Error connecting to redis - ", err.message,
➥ "|",err.class.to_s
➥ @redis = nil
return nil
end
end
end
def start
super
log.trace "starting redis plugin\n"
connect_redis() ❸
end
def shutdown
super ❹
log.trace "shutdown"
if @redis ❺
begin
@redis.disconnect!
log.debug "disconnecting from redis\n"
@redis = nil
rescue
log.error "Error closing Redis connection"
end
end
end
❶ 建立 Redis 连接,从配置属性和定义的常量中检索连接参数
❷ 将 Redis 连接错误单独处理,不同于一般的通用捕获,因为我们可以更有效地引导用户处理这类问题
❸ 在所有继承的活动完成后,作为启动过程的一部分建立与 Redis 的连接
❹ 确保任何继承的任务都已完成
❺ 如果我们仍然有 Redis 连接,那么开始断开连接的过程。
9.6.3 使插件与我们的 Fluentd 安装一起工作
有足够的代码来证明我们可以让我们的插件至少启动和停止,我们就可以运行一个简单的测试,使用完整的 Fluentd。Fluentd 工具包括扩展,可以帮助进行单元测试,但看到代码作为更大系统的一部分运行总是令人欣慰的,尤其是当启动速度快的时候。为了做到这一点,我们需要确保 Fluentd 能够识别我们的插件代码。
我们需要一个测试配置来运行 Fluentd。我们已经为这项工作准备了一个,可以从Chapter9/Fluentd/dummy-plugin.conf获取。当然,你可能希望根据你在书中所学的一切选择开发自己的配置。
我们已经重复使用模拟源 Fluentd 插件来生成日志事件,因为内容并不重要。关键元素是匹配配置,如下面的列表所示。
列表 9.4 Chapter9/Fluentd/dummy_plugin.conf
<match *>
@type redislist ❶
portno 6379
#<buffer> ❷
# flush_interval 120
#</buffer>
</match>
❶ 这是使用我们的插件的声明。
❷ 由于我们基于基类进行构建,如果定义了使用缓冲区属性,则内存辅助插件可用。目前,我们已将缓冲区配置注释掉。
在启动 Fluentd 之前,我们需要像本章前面所做的那样启动 Redis 服务器。最终,我们希望像其他任何 Fluentd 插件一样使用 gem 工具打包和部署我们的插件。但一开始,我们不希望每次我们做出更改时都额外努力部署和卸载 gem。为了避免 gem 部署问题,我们可以在命令行中添加参数,告诉 Fluentd 从 Ruby 文件中获取我们插件的源代码。例如,我的插件目录结构从以下位置开始:
c:\myDev\GitHub|UnifiedLoggingWithFluentd\Chapter9\
➥ fluent-plugin-out-redislist
然后,扩展的 Fluentd 命令将如下所示:
fluentd -c Chapter9\Fluentd\dummy-plugin-out.conf -p
➥ c:\myDev\GitHub|UnifiedLoggingWithFluentd\Chapter9\fluent-plugin-out-
➥ redislist\lib\fluent\plugin -vv
今后,我们将显示路径为<plugin absolute path>\Chapter9\ fluent-plugin-out-redislist\lib\fluent\plugin,其中你需要相应地替换<plugin absolute path>。
将配置文件和扩展路径以收集我们的插件结合起来,意味着我们启动 Fluentd 的命令将导致 Fluentd 在启动时显示配置。当 Redis 服务器在命令行上运行时,我们将能够看到 Redis 在启动和停止 Fluentd 时记录其连接数。
9.6.4 将额外的配置验证付诸实践
你的目标是重启 Redis 服务器到不同的端口,并创建一个替代的 Fluentd 配置来连接到不同端口的 Redis。你需要确认我们的配置检查是否正确执行。要重启 Redis 到不同的端口,请添加--port nnnn,其中nnnn代表启动命令中使用的端口号。
答案
修改后的配置文件解决方案可以在Chapter9\ExerciseResults\dummy-plugin-Answer.conf中找到,我们将端口从6379更改为16379。我们还修改了模拟输出插件生成的日志消息,尽管我们目前还看不到。
当 Redis 服务器启动时,我们需要添加--port 16379来覆盖默认值。我们的 Fluentd 启动命令现在变为
fluentd -c Chapter9\ExerciseResults\Fluentd\dummy-plugin-Answer.conf -p
➥ <plugin absolute path>\Chapter9\
➥ fluent-plugin-out-redislist\lib\fluent\plugin
当 Fluentd 启动时,我们应该在 Fluentd 的日志输出中看到关于使用非标准端口的警告。但 Redis 服务器应该报告连接。
9.6.5 实现 Redis 输出逻辑
在证明我们可以配置、启动和关闭插件之后,我们可以进入实现将事件发送到 Redis 的逻辑的下一步。逻辑可以以几种方式执行:
-
同步—通过实现
def process (tag, es)方法处理每个事件。这是最直接的方法,但执行效率最低,因为它没有使用任何缓冲。 -
同步缓冲—输出通过
def write (chunk)方法实现。 -
异步缓冲—输出通过
def try_write (chunk)方法实现。
使用的实现方法取决于配置是否包含<buffer>部分,除非我们配置了一些覆盖标准行为。对于第一个实现,我们将保持同步模型简单直接。
我们的实现过程需要标记传递给(es)的事件流,并遍历事件。由于流可能包含多个事件,我们可以通过告诉 Redis 批量执行事件的插入来使这个过程更高效。这是通过使用redis.multi命令告诉 Redis 它将接收多个事务调用来完成的。一旦我们遍历了事件,我们就可以通过调用redis.exec告诉 Redis 它可以执行事务。当我们遍历日志事件时,我们需要执行以下操作:
-
构建日志事件(的)JSON 表示。如果您已经审查了输出接口,您会注意到有一个预定义的格式化函数。我们选择不覆盖或使用它,因为我们不希望影响插件基类中此方法的其它应用;因此,我们可以以任何期望的方式格式化表示,例如使用 msgpack。
-
执行 Redis 列表推送功能。
我们应该在代码中采取防御性措施来处理在所有事件都提交到 Redis 之前丢失 Redis 连接的场景。
我们需要将日志事件转换为 JSON,可能用于三个不同的功能;我们应该只编写一次逻辑,并从涉及的不同插件方法中调用它。结果是以下列表中的两个方法。
列表 9.5 第九章/fluent-plugin-out-redislist/lib/fluent/pluginout_redislist.rb
def redisFormat(tag,time,record) ❶
redis_entry = Hash.new
redis_entry.store(RedislistOutput::TagAttributeLabel,
➥ tag.to_s) ❷
redis_entry.store(RedislistOutput::TimeAttributeLabel,
➥ time.to_i)
redis_entry.store(RedislistOutput::RecordAttributeLabel,
➥ record.to_s)
redis_out = JSON.generate(redis_entry) ❸
return redis_out
end
def write(chunk) ❹
log.trace "write:", chunk
@redis.multi ❺
chunk.each do |time, record|
log.debug "write sync redis push ", chunk.metadata.tag,
➥ time, record, @listname @redis.lpush(@listname,redisFormat
➥ (chunk.metadata.tag, time, record))
end
@redis.exec ❻
end
❶ 这是一个将日志事件转换为 JSON 表示的函数。我们需要构建自己的 JSON 表示,因为我们需要捕获所有日志事件属性。
❷ 通过使用预定义的常量,这些可以与输入插件共享。
❸ 由于记录、事件时间和标签代表一个平面结构,我们可以构建一个简单的哈希结构,然后利用预构建的操作将其转换为 JSON。请注意,我们不使用格式化方法,因为这在 Fluentd 的其他部分中使用,例如缓冲区,我们不希望用这种变体表示混淆逻辑。
❹ 这是由输出插件使用的一个标准功能之一。
❺ 这告诉 Redis 接受多个应该在一个操作中执行的状态语句。
❻ 这将释放 Redis 库将所有语句作为一个单独的块发送到 Redis 服务器。
9.6.6 将同步输出测试付诸实践
我们已经达到了可以确认我们可以写入 Redis 的状态。使用之前展示的方法,重新启动 Fluentd 并使用展示的 Redis 命令来检查 Redis 中的列表并弹出列表中的条目。
答案
在重复 Fluentd 测试时,现在写入方法已经就位,你应该期望 Redis 命令显示列表结构随着 JSON 内容增长,看起来像这样:
{"tag" : "dummy", "time" : "2014-12-14 23:23:38", "record" :
➥ {"hello" : "world", "counter":1}}
由于日志事件是按生成顺序增加计数器值的,这意味着计数器属性值和 Redis 列表长度之间应该存在关联。这可以通过 Redis CLI 中的llen命令来确认。你还可以使用lpop fluentd命令从列表中弹出条目,因为配置允许使用默认的列表名称。
9.7 实现 Redis 输入插件
在实现其他写入方法之前,让我们先完成输入插件的部分电路。我们可以使用生成输出插件的相同工具来生成输入侧的骨架文件夹和文件。一切与之前重新运行工具时相同,只是我们指定插件为输入,而不是输出。这将导致生成的代码扩展不同的基类(如图 9.2 所示);因此,我们需要实现一些不同的函数。
对于 Redis 来说,输入插件实际上是一种轮询活动,因为大多数解决方案都不支持回调或 webhooks(值得注意的是,Redis 确实有 webhook 的概念)。这意味着我们需要一个配置值来指定插件需要多快轮询 Redis。与输出插件一样,我们需要连接到 Redis 所需的信息。对于这个后者的任务,我们可以复制为输出插件编写的代码。虽然这并不支持 DRY(不要重复自己)的优秀编码原则,但以后有很多机会改进我们的代码。
尽管我们有一些额外的值需要考虑,但输入插件处理配置属性的方式与输出插件相同。在输入插件上需要实现的两个关键功能是处理run命令和emit函数(如列表 9.6 所示)。run方法将负责启动我们的调度线程。emit函数处理调用 Redis 并将日志事件输出到 Fluentd 配置文件定义的下一个流程。
作为源插件,框架将在事件上设置标签和时间戳值,以反映当前时间和标签默认行为。这些值有意义吗?因为它们并不真正反映原始事件发生的时间。为了解决这个问题,我们提供了确定原始标签和时间是否添加到事件记录或应在核心日志事件中使用的手段。最好允许配置插件的人决定应该替换什么。如果这些值应该插入到日志事件中,我们必须确定要使用的属性名称。我们可以使用捕获插件配置属性已使用的相同机制来解决这个问题。
列表 9.6 第九章/fluent-plugin-redislist/lib/fluent/plugin/in_redislist.rb
def emit
log.trace "emit triggered"
if !@redis ❶
log.debug "reconnecting Redis ",@hostaddr,":",@port
connect_redis()
end
if @redis
keep_popping = true ❷
while keep_popping
if (@fifo) ❸
popped = @redis.rpop(@listname)
else
popped = @redis.lpop(@listname)
end
log.debug "Popped",@listname, ": ", popped
if popped
data = JSON.parse(popped)
if (@use_original_time)
time = data[TimeAttributeLabel]
else
time = Fluent::EventTime.now
end
if (@use_original_tag)
tag = data[RedislistInput::TagAttributeLabel]
else
tag = @tag
end
data_record = data.fetch(RecordAttributeLabel).to_s
log.debug "original data record=>",data_record
if (@add_original_time && !(data_record.include?
➥ '"'+@add_original_time_name+'"'))
data_record= inject_original_value(data,data_record,
➥ RedislistInput::TimeAttributeLabel, @add_original_time_name) ❹
end
if @add_original_tag &&
➥ !(data_record.include? '"'+@add_original_tag_name+'"')
data_record = inject_original_value(data,data_record,
➥ RedislistInput::TagAttributeLabel,@add_original_tag_name)
end
log.debug "Emitting -->", tag," ", time, " ", data_record
router.emit(tag, time, data_record) ❺
else
keep_popping = false
end
end
else
log.warn "No Redis - ", @redis
end
end
def run
log.trace ("run triggered")
while thread_current_running? ❻
current_time = Time.now.to_i
emit() if thread_current_running?
while thread_current_running? && Time.now.to_i <= current_time
sleep @run_interval ❼
end
end
end
❶ 确定是否需要新的连接
❷ 设置循环控制器,以便我们持续调用 Fluentd,直到关闭流程改变此标志的状态
❸ Redis 允许我们将列表视为先进先出(FIFO)或后进先出(LIFO)。因此,我们可以使用此配置来控制我们是否希望以 FIFO 或 LIFO 方式操作列表。
❹ 确定标签和日期时间值是否应该替换新的日志事件,或者只是简单地纳入其日志事件记录中。
❺ 这时,我们告诉 Fluentd 根据配置定义将日志事件传递到处理流程的下一步。
❻ 运行方法的线程处理,以及线程允许运行的时间
❼ 一旦我们决定线程自上次循环以来确实被唤醒,我们就可以使用emit发送所有日志事件。
9.7.1 测试输入和输出插件执行
在实现我们的输入插件后,我们可以进行简单的测试。我们可以轻松地将输入和输出插件合并到单个配置中(参见Chapter9/fluentd/dummy-plugin.conf);问题在于输入和输出的日志信息会混合。我们需要扩展插件路径参数以包括这两个插件,如下所示:
fluentd -c Chapter9\fluentd\dummy-plugin.conf -p <plugin absolute
➥ path>\Chapter9\fluent-plugin-out-redislist\lib\fluent\plugin -p <plugin
➥ absolute path>\Chapter9\fluent-plugin-redislist\lib\fluent\plugin
或者,我们可以启动两个 Fluentd 实例,每个实例使用自己的配置,这样每个进程就有一个输入。这将使观察发生的事情变得容易得多。为此,重复之前用于查看输出插件工作的步骤。然后,在另一个控制台窗口中,我们可以调整命令以引用我们的输入插件路径和已经准备好的 Chapter9\Fluentd\dummy-plugin-in.conf。结果应该看起来像这样:
fluentd -c Chapter9\Fluentd\dummy-plugin-in.conf -p <plugin absolute
➥ path>\Chapter9\fluent-plugin-redislist\lib\fluent\plugin
当一切运行正常时,你应该在一个控制台中看到显示正在添加的日志事件的日志消息。另一个控制台显示它们正在被移除,Redis 控制台则显示了添加和从列表中移除的两个交互。
现在有了使用 Redis 列表写入和消费日志事件的能力,让我们回到写入逻辑,并通过使用缓冲等不同的输出逻辑工作方式扩展实现。
9.8 使用缓冲扩展输出
正如我们在第四章中看到的,如果插件支持缓冲,I/O 过程可以被优化。我们已经通过配置同步过程将 Redis 推送操作组合在一起进行了一些性能优化,因此如果收到多个日志事件,Redis 连接器会一次性执行它们。但我们可以通过使用缓冲将更大的组处理成一个单独的 Redis 事务来进一步加速这个过程。
正如我们在第四章中看到的,对于 Fluentd 来说,默认情况下有两种类型的缓冲,可以使用临时文件或内存。当我们的目标是提供比使用物理存储(如磁盘)更好的性能的内存解决方案时,支持文件实现是没有意义的。这意味着我们的解决方案应该只允许使用内存缓冲。
在图 9.4 中,我们可以看到日志事件如何通过基础 Fluentd 输出类的不同路径以及需要实现的功能,这取决于是否使用缓冲以及缓冲是同步的还是异步的。

图 9.4 根据缓冲的使用和类型,从输出插件输出日志事件的不同方法
如图 9.4 所示,为了保持简单,一旦我们有了输入和输出插件,我们将首先实现同步缓冲路径;然后我们将重新访问以扩展插件的缓冲使用案例。
异步路径在如何与 Fluentd 交互方面与同步路径大致相同。但我们必须管理额外的逻辑,这些逻辑使得行为异步并使用缓冲块数据结构。有了缓冲,我们可以提供处理缓冲块的方法,包括以下选项:
-
每个数据块通过它可以包含的日志事件数量来控制。 这是一个简单的机制,当日志事件大小一致时,非常高效且可预测。但如果日志事件大小可变,在填满数据块之前,您可能会耗尽可用内存。
-
每个数据块通过分配相同数量的内存来控制。 在大小模型中,我们有责任实现围绕计算日志事件大小的逻辑,确定日志事件是否可以放入当前数据块或需要放入另一个数据块。此外,我们必须决定如果日志事件的大小超过数据块大小限制时应该做什么。
这种模型的好处是,如果日志事件的大小是可变的,我们不会面临内存耗尽的情况,因为我们已经限制了将要使用的资源数量。这里的额外代码复杂性并不在于 Fluentd 插件的编写方式,而更多在于理解 Ruby 以及它是如何与数据结构协同工作的。
在“保持简单愚蠢”(KISS)的精神和专注于 Fluentd 的基础上,我们将说明每个数据块中日志事件的数目模型。理解日志事件并避免内存耗尽风险的责任在于用户。通过我们的方法,我们应该默认一些缓冲设置,以便用户使用起来更加方便。因此,我们可以将以下代码片段纳入配置中:
config_section :buffer do
config_set_default :@type, 'memory'
config_set_default :chunk_keys, ["tag"]
config_set_default :chunk_limit_records, @chunksize
end
如您所见,我们定义了一个数据块大小配置属性,它本身已经默认设置。因此,除非明确覆盖,否则我们将默认使用缓冲方法而不需要任何配置值。我们还应该扩展 configure 函数,以考虑提供我们不推荐或不支持配置值的可能性,因为它们是为缓冲的数据块大小模型设计的。
下一步是实现写入函数,它接受一个完整的缓冲数据块,并将构建 Redis 推送调用的数据块传递出去。我们可以利用之前的逻辑来生成我们希望在 Redis 中使用的表示。就像在同步无缓冲路径中一样,我们希望用 Redis 连接器的指令来包围数据块循环,以便一次性使用 redis.multi 和 redis.exec 函数执行所有 Redis 命令。新的写入方法如下所示。
列表 9.7 第九章/fluent-plugin-out-redislist/lib/fluent/out_redislist.rb
def write(chunk) ❶
log.trace "write:", chunk
@redis.multi ❷
chunk.each do |time, record| ❸
log.debug "write sync redis push ", chunk.metadata.tag, time,
➥ record, @listname
@redis.lpush(@listname,redisFormat(chunk.metadata.tag,time,record)) ❹
end
@redis.exec ❺
end
❶ 函数是从基类逻辑中调用的。我们已经实现了自己的版本。与 configure 等函数不同,调用 super 会触发未实现异常。
❷ 指示 Redis 连接器将随后的所有 Redis 语句分组
❸ 数据块的结构与未分批路径中提供的流结构不同,因此我们需要不同的循环。这将遍历每个数据块条目
❹ 如前所述,我们需要获取日志事件、时间和标签来构建用于 Redis 的表示。
❺ 将 Redis 连接错误与一般的捕获所有错误分开处理,因为我们可以更有效地引导用户解决这类问题
我们可以使用略微不同的配置重新运行我们的测试,以确保我们使用这种方法引入的缓冲行为。这可以通过以下命令完成:
fluentd -c Chapter9\Fluentd\dummy-plugin.conf -p <plugin absolute
➥ path>Chapter9\fluent-plugin-out-redislist\lib\fluent\plugin -vv
使用提供的测试配置,我们应该看到写入方法跟踪语句定期发生。内部跟踪以短促的爆发将日志事件写入stdout,因为写入函数在调用 Redis 连接器时记录详细信息。
9.8.1 通过实施可维护性来改进我们的场景
我们对 Redis 列表插件的初步实现展示了我们如何交付一个新的插件。在开发过程中,输入和输出代码之间存在一些共性。因此,我们得到了改进的批准。因此,第一个目标是重构输入和输出以使用一个共同的基类。
9.9 单元测试
我们迄今为止所进行的测试是一个手动过程,我们都知道这不是最好的。在现实世界中,我们会从单元测试开始,并在此基础上构建。理想情况下,代码的更改应触发持续集成和持续交付过程,自动运行单元测试和端到端测试。
我们在这里不会深入探讨,因为单元测试主要是 Ruby 开发的一个方面,而不是 Fluentd。Fluentd 团队构建了一些支持库,可以与任何主要的单元测试框架一起使用,包括 test-unit (test-unit.github.io/)、RSpec (rspec.info/) 和 minitest。我们的示例使用 test-unit,因为它是一个广泛采用的框架,感觉像许多其他主要的单元测试框架,如 NUnit、JUnit 等等。
当我们使用生成插件骨架的实用程序并生成主 Ruby 代码时,该工具还在基本插件文件夹中生成了一个测试文件夹结构。这包括提供一个骨架类来帮助我们开始。测试类与插件同名,但前面有一个test_前缀。这是测试路径基础目录中的一小段辅助代码(helper.rb),它将框架的辅助代码加载到测试单元工具中。
为了说明可能性,我们为输出插件构建了一些测试,如列表 9.8 所示。这些测试侧重于验证驱动我们插件行为的配置相关逻辑。这是通过 Fluentd 测试框架的一部分实现的,该框架实现了不同类型的驱动程序。所需的驱动程序类型由插件类型决定,并模仿 Fluentd 的核心。我们可以使用驱动程序触发必要的操作,包括向插件提供日志事件。驱动程序还提供了检索和评估结果的方法,例如事件通过特定阶段(例如,emit、write)的次数。评估可以通过驱动程序访问和检查处理的事件来完成,例如同步和异步写入操作的输出,以及简单地知道处理了多少事件。
驱动能力可以扩展以处理计划活动的影响,例如事件在缓冲区中积累。测试实用程序也可以捕获标准输出并处理文本。捕获此类输出允许您验证日志事件的处理是否按预期生成。在我们的高级配置测试中,我们应用了这项技术。如果日志事件不会输出到stdout,而是输出到 Fluentd 的日志文件,也可以进行查询,查看生成了多少日志信息以及是否发生了特定的日志事件。
列表 9.8 Chapter9/fluent-plugin-out-redislist/test/plugin/test_out_redislist.rb
test 'advanced config' do ❶
conf = %[ ❷
host 127.0.0.1
port 24229
]
captured_string = capture_stdout do ❸
d = create_driver(conf) ❹
assert_equal 24229, d.instance.port ❺
assert_equal '127.0.0.1', d.instance.hostaddr
end
assert_true (captured_string.include? "Non standard Redis port in use") ❻
d.shutdown ❼
end
❶ 声明单元测试
❷ 创建要传递的一组配置值
❸ 定义一个变量以捕获以下语句块中生成的任何标准输出
❹ 使用提供的测试配置创建驱动程序
❺ 测试插件中设置的端口号值
❻ 评估是否生成了关于没有标准端口的警告
❼ 清洁地关闭一切
要执行单元测试,我们只需遵循单元测试框架的指导。在 test-unit 的情况下,这归结为使用 Ruby 执行单元测试文件。例如:
ruby Chapter9/fluent-plugin-out-redislist/test/plugin/test_out_redislist.rb
9.10 将单元测试的开发付诸实践
之前,我们确定了通过运行 Fluentd 使用不同的配置文件来测试不同配置的需求。这些应该被单元测试所取代。由于输出插件限制了我们可以使用的缓冲方式,我们需要进一步测试配置处理。这最好通过为configure和write函数进行单元测试来完成。
9.10.1 答案
在Chapter9/ExerciseResults目录中,我们包含了两个子目录,分别称为test-out和test-in。这些包含目录结构和文件,其中包含额外的单元测试,覆盖了配置和其他操作,您可以进行比较。
9.11 打包和部署
完成测试后,我们可以考虑打包和部署我们的插件。这包括准备元数据文件和文档。
9.11.1 文档
打包解决方案的一部分包括提供插件的许可信息和文档。模板工具将提供标准的许可文档和基本的 README。这里最重要的是确保 README 清晰完整。像任何好的产品一样,插件能否成功使用取决于人们是否了解如何使用它,因此良好的文档将产生重大影响。在下载包(和 GitHub 仓库)中,我们提供了一个单独的完成后的 README,以便可以比较初始生成的状态(readme.md)和最终状态(readme-final.md)。您可能会在 README 内容中注意到一些帮助完成 Gemfile 的说明,我们将在稍后讨论。
使用另一个 Fluentd 提供的工具,生成插件的文档任务得到了极大的简化。fluent-plugin-config-format 工具接受插件类型(与为我们创建骨架的工具相同的方式)并为插件的参数命名。然后我们可以告诉工具我们希望如何生成文档。在下面的示例中,我们使用了 markdown(它使得 GitHub 和其他 Git 类型的仓库能够很好地渲染,但其他选项包括纯文本和 JSON)。由于我们希望从源代码生成文档,我们需要提供 Ruby 插件代码的路径。使用此工具并以下列参数将生成关于插件配置信息的文档:
fluent-plugin-config-format output redislist -f markdown -p lib/fluent/plugin/
工具的结果被发送到控制台而不是文件中,因此我们需要将输出剪切并粘贴到我们的 readme.md 文件中(或者使用一些 shell/console 技巧将输出管道到文件中)。我们已经将输出整合到了 readme - final.md 文件中。
使用 RDOC 或 YARD
在图 9.2 所示的目录结构中,我们有一个 doc 文件夹,它不是由 Fluentd 工具生成的。这是通过在代码上运行 RDoc 或 YARD 得到的,从而生成了开发者级别的文档。在这种情况下,我们选择使用 YARD,因为它在标准 RDoc 之上提供了一些额外的便捷功能。要了解更多关于 YARD 的信息以及如何安装它,请参阅附录 E。请注意,如果您更喜欢坚持使用 RDoc,YARD 使用的元数据标签将出现在生成的输出中。
安装 YARD 后,维护此文档的工作就归结为关注代码、注释以及在插件的根目录中运行命令:
yard doc lib/fluent/plugin/out_redislist.rb
yard doc lib/fluent/plugin/in_redislist.rb
这将为您更新文档,但它主要关注通用 Ruby 代码和注释,而不是与 Fluentd 相关的特定内容,例如配置值及其参数。
9.11.2 完整元数据即清单
需要根据 README 中默认内容中的指示更新 Gemfile。因此,我们需要将我们要创建的 gem 的名称添加到 gemspec 文件中。它采用 gem <gem 名称> 的形式——例如,gem "fluent-plugin-out-redislist"——用于我们的输入插件。
gemspec 文件还需要补充额外的信息,包括摘要、描述、主页、许可证、联系详情和版本信息(预期使用语义版本控制格式;semver.org)。这还需要包括任何依赖项的详细信息。当设置好时,我们安装了几个额外的 gem,例如 Redis 连接器。这意味着我们需要将依赖项添加到 gemspec 文件中,以便在安装此插件时检索它们:
spec.add_runtime_dependency "redis", "~>4.0"
这表明需要 Redis gem 的版本为 4.0 或更高版本。gemspec 标准确实允许定义复杂的规则,以确定可以使用哪些版本。
9.11.3 构建 gem 包
一旦完成,我们可以使用之前安装的 RubyGem 工具来完成此操作。这意味着我们可以使用 gemspec 工具创建最终的包。但请注意执行命令的位置,因为 gemspec 文件包含脚本,它使用相对路径来定位所有需要包含的文件。我们可以使用命令的 -V 参数来开启详细模式,以便更容易地看到正在发生的事情。为了完成任务,我们使用以下命令创建 gem 文件:
gem build in-redislist.gem --config-file ./fluent-plugin-redistlist.gemspec -V
当 gem 文件创建完成后,我们需要将其安装到我们的本地 gem 库中。一旦完成,我们就可以使用该 gem,而无需提供实际 Ruby 代码的路径。这可以通过以下命令完成:
gem install ./fluent-plugin-out-redislist-0.1.0.gem
我们可以通过执行以下命令来确认 gem 已安装:
gem search -l redis
这应该会生成一个包括我们的 gem 的列表。
9.11.4 无插件路径重新运行
在构建了插件并创建了安装了 gem 之后,我们可以重新运行我们的测试场景。这次测试可以在不直接引用 Ruby 代码的情况下运行,因为我们已经通过我们创建的 gem 提供了代码,以确保我们不会意外地在 Fluentd 命令行中使用 -p 参数。因此,我们可以看到我们的插件正在工作,但执行命令反映了在生产中会看到的传统工作方式。
9.12 扩展为成为企业级解决方案
插件开发已成功,并满足了我们所有的要求。但它还没有达到我们可以称之为适合企业级用例的水平的程度。为了达到合适的标准,我们建议进行一些更改:
-
在 Redis 连接上使用的凭证——配置插件以可选地使用带有 Redis 的认证连接。这样做是为了允许凭证从安全源注入,例如 Vault。
-
使用经过身份验证的 Redis 连接意味着传递凭证。我们真的不希望在明文凭证的 HTTP 连接中这样做。因此,Redis 连接应该通过 SSL/TLS 证书安全实现。
-
Redis 中的一个特性,也是使其成为优秀缓存解决方案的原因之一,是包含 TTL(生存时间)。通过包含 TTL,我们可以控制 Redis 缓冲区的大小,如果我们无法跟上,那么事件将简单地过期。
-
在处理事件时,可以生成一些基本统计信息,这些信息可能会被 Prometheus 消费。
-
扩展和增强单元测试,以达到超过 60% 的覆盖率目标。
-
开发预构建的 Rakefile 以包含并执行所有单元测试。同时,结合以下活动:
-
代码分析。
-
使用 RDoc 或 YARD 维护额外的文档。目前,doc 文件夹是使用 YARD 生成的。
-
生成
fluent-plugin-config-format输出,并确定自上次构建以来是否有任何更改。
-
-
将该流程集成到持续集成工具套件中。
-
将
readme.md文档合并,而不是使用readme- final.md。
如果你想提高你的插件开发技能或将其作为实际机会来提高你的 Ruby 开发技能,我们鼓励你尝试实现这些建议的改进。由于你在实现这些功能时不会遵循我们的步骤,我们建议你采用你喜欢的开发工具(们)。我们没有参考解决方案,但运行测试场景以确认你实现此练习的解决方案的功能性成功。
摘要
-
Fluentd 插件工具提供了创建输入和输出插件骨架代码的手段。
-
Fluentd 的框架支持异步和同步缓冲输出插件功能。
-
Fluentd 输入插件需要能够设置时间和标签细节。输入插件实现了这一逻辑。
-
插件框架包括定义、创建和默认配置从配置文件加载的插件的配置属性。
-
Fluentd 插件通常作为 RubyGems 提供。Ruby 和 Fluentd 提供的实用工具使得实现这一点变得简单。
第四部分:良好的日志实践和框架以最大化日志价值
软件的质量取决于其处理的数据。同样,日志处理的质量取决于生成的日志。或许我们可以这样看待:良好的信息能够带来良好的洞察力和有效的决策。为了从捕获、操作和路由日志事件中获益,我们需要清晰且意义明确的日志事件。软件能够越直接地将日志事件传递给 Fluentd,其开销就越低,监控中产生歧义的机会也就越少。
在接下来的章节中,我们将专注于确保日志内容具有最大价值和意义。简而言之,什么因素使一个日志事件变得良好?何时日志过度分享或分享不足?日志事件的上下文是否清晰?
如今,许多应用程序要么将日志框架作为语言的一部分,要么从第三方获取。我们应该从这样的框架中寻求什么?在选择框架时我们应该寻找什么?如果没有框架或特定的连接到 Fluentd 的方法,有哪些选项存在?Fluentd 提供了一系列额外的实用工具和库,可以帮助我们,那么有哪些可用?这些都是我们在本书最后一部分将要探讨的问题。
10 日志最佳实践
本章涵盖
-
将日志级别应用于过滤和优先处理操作
-
识别良好日志的特征
-
利用良好的日志简化操作活动
-
理解立法对日志的影响
-
提高日志记录的编码实践
使用的这项技术仅与日志事件本身一样好,无论日志条目是如何生成的,无论是应用程序写入 stdout、stderr、OS 事件框架还是日志框架。为了最大化技术投资,我们需要使日志事件及其创建尽可能有效。
我们已经深入探讨了技术,因此我们也需要对日志事件做同样的事情。本章将探讨在业务数据方面应该记录什么,不应该记录什么,并检查哪些信息可以使日志事件更有帮助。有了这些,我们将确定一些实践来帮助从日志事件中获得价值。我们系统处理的业务数据可能受到各种合同和法律要求的影响。因此,我们将探讨一些更知名的立法需求,一些减轻其影响的选项,以及可以帮助我们识别可能影响日志使用其他立法需求的来源。
10.1 审计事件与日志事件对比
何时一个事件是审计事件,何时是日志事件?让我们先定义这两个事件是什么(图 10.1):
-
审计事件——这些通常是记录一个动作、事件或数据状态,这些状态需要保留以提供可能在未来某个时刻需要解决合规性问题(如会计流程或安全)的正式记录。许多这些动作将由用户触发。
-
日志事件——这些记录了已经发生的事情;日志事件将出于技术原因提供,范围从显示交易是如何处理的到报告意外情况以显示代码是如何执行的。

图 10.1 展示日志和审计之间关系的维恩图
从描述中,你可以看到相当程度的重叠。这种重叠源于日志作为一种技术机制可以用来满足审计要求,并且两者都有一个共同的数据核心。审计事件通常由结构良好的内容组成,偏向于与安全相关的事件,如登录和注销。问题是关于事件路由到的工具是否适合审计相关任务。所有这些都指向这样一个事实,即像 Fluentd 这样的日志统一工具可以支持审计需求;我们需要专注于确保事件中的信息是合适的,并且将其发送到合适的工具进行分析。
10.2 日志级别和严重性
回到第一章,我们讨论了日志消息将如何在不同时间支持不同的角色,无论你是工作在 DevOps 组织(开发团队也负责运营)还是在经典分离的运营和开发团队中。一些日志有助于开发和测试,其他日志有助于故障排除,还有一些日志有助于审计、安全和性能跟踪。最简单的事情就是给每个日志分配一个反映日志事件代表影响的日志级别或严重性。典型的日志级别有追踪、调试、信息、警告、错误和致命。事件严重性和这些严重级别可以追溯到 20 世纪 80 年代和 Syslog 的开发。Syslog 如何做的许多方面已经随着 IETF(互联网工程任务组;tools.ietf.org/html/rfc5424)而成为标准。但正如我们稍后将看到的,这些级别与相关活动之间存在关联。
注意:是否应该使用诸如严重性或日志级别之类的术语来描述日志事件?IETF 讨论了严重性;然而,这带有暗示日志信息与不良事物相关的含义。然而,日志应该合法地生成以指示一切按预期运行。因此,包括我自己在内,许多人使用日志级别这个术语来避免与严重性一词相关的含义。
当然,关键在于对日志级别所代表内容的共同理解。日志条目的误分类是日志事件中常见的错误,这就是为什么我们在前面的章节中提到了使用 Fluentd 纠正这些问题的可能性。但这也意味着我们应该明确,并且团队就级别的含义达成一致。以下各节提供了一个共同的日志级别定义集。
10.2.1 追踪
主要用于开发活动,执行可以写入日志的简单事件,以指示正在执行的方法。这使我们能够确认/验证执行路径是否符合预期。
当谈到开放追踪和开放遥测的概念时,我们确实需要将这些技术与分类区分开来。这些技术收集显示“事务”如何在我们环境中流动的“追踪”信息。开放追踪依赖于所实现追踪的粒度。如果追踪反映了事务流程中执行的技术步骤,例如,进入和离开组件、函数等,那么我们将看到一个细粒度和全面的追踪,并且应该使用追踪分类进行记录。如果追踪反映了业务视角(例如,执行与将商品运入仓库相关的所有操作),那么我们将看到粗粒度细节,这最好在信息级别进行记录。
10.2.2 调试
此级别旨在共享日志数据以支持任何开发和调试活动。此日志级别应信息丰富,以便轻松进行故障排除或重新创建运营问题,因为它将提供对软件正在做什么的最深入了解。此类信息的生成应涉及开发人员和参与更详细故障排除的人员。我们经常看到此级别的日志记录被使用,而不是试图使用 IDE 调试器并将其附加到正在运行的软件上。
这些日志消息最容易被意外记录过多信息(例如,个人或财务数据),因为整个数据对象可以轻松地被记录。在非生产环境中,这通常不会成为问题,因为数据很可能是合成的。这也意味着很容易忽视生产中的风险。
通过以下方式可以解决记录敏感数据的风险
-
制定包括如何记录或不记录数据在内的详细标准的规范,并测试流程以确保不会记录合成事件数据。
-
在生产环境中全面禁止启用调试级别的日志记录(生产永远不应该有调试级别的日志记录)。在出现严重的运营问题时,帮助诊断重大运营问题的诱惑会很高,而设置调试级别日志的后果直到太晚才会被忽视。
10.2.3 信息(信息)
这是日常操作环境中日志的典型阈值。它应该提供足够的日志信息,以便在系统似乎行为不正确时进行诊断任务。记录的信息应包括如下细节
-
在启动和部署期间记录的软件版本等。
-
审计日志,如什么和谁,通过如会话 ID 等细节(这涉及到个人数据安全的问题,我们将在稍后更深入地讨论)。
-
影响决策逻辑的数据值。
-
与源和目标交互,例如其他服务的 URI 或数据库。
10.2.4 警告
当事情没有按预期进行时,存在出现错误的风险,但软件可以继续执行。例如,数据库连接失败,代码支持回滚或成功的重试操作。这可能会导致一个警告,说明它未能连接,然后在重试时出现错误,或者完全回滚并放弃事务。
警告不应需要立即干预,但应表明可能需要补救。这应包括处理意外路径,并因此采取行动。
警告理想情况下应与操作指导文档相关联,例如建议维护流程可能需要比维护计划预期的更早执行,或者已经自动采取了缓解措施,例如增加资源。其他警告操作可能包括审查交易是如何完成的,因为系统没有按常规处理它,或者代码假设了某些不正确的事情。
我们还应该考虑我们的解决方案具有防御性,检查事物是否接近危险阈值,并创建警告。例如,这可能包括确保有足够的磁盘容量来应对当前的数据增长速度。其他防御措施应包括验证接收到的数据,即使它来自可信的来源。
10.2.5 错误
这用于记录需要干预的事件;例如,对一个假设始终有值的空数据结构执行操作可能会触发空指针异常。这可能会创建一个进程无法干净完成的情况,因此需要反映为错误。为了帮助解决错误,日志事件需要阐明错误原因。错误发生的代码位置对于有效地应用改进至关重要。这意味着开发人员需要在错误日志事件中获取可消费的信息以帮助实施改进,而运维人员需要详细信息以确定需要哪些修复措施。
当发生错误时,我们不仅需要一个解决方案,还需要对数据进行操作上的纠正(例如,除以零导致计算值未更新)。因此,信息必须也被运维人员和发展/支持团队清楚地理解。错误代码可能有益,因为修复步骤可以在不淹没代码大量文本的情况下进行记录。
错误应尽量优雅地失败——也就是说,它们被处理并尽量减少干扰(例如,记录请求的交易然后允许后续交易在没有被污染的情况下处理)。
10.2.6 致命
这种错误仅在例外情况下使用,例如当应用程序必须意外终止时。终止可能是不优雅的。与错误一样,信息需要尽可能全面。然而,对于致命错误,可能存在可以收集的信息的限制——例如,由于文件系统失败而导致的致命错误将限制获取可能影响问题原因的相关数据值的能力。
再次强调,错误代码可以是有帮助的,既可以指导恢复任务,又可以通过提供不依赖于构建良好错误消息的底层原因的指示。
10.2.7 扩展或创建自己的日志级别
这些定义并不意味着你不能制定自己的级别,但它们在使用框架和确保共同理解方面具有重大影响。例如,我时常在想错误级别是否应该被拆分——一些错误需要立即干预,因为如果不干预,它们可能是致命事件的先兆。而一些错误意味着不良的结果,但它们可以等到正常工作时间再解决。考虑一个夜间的工资发放——由于公式不允许某人在一个月内获得 0 小时工资,导致了一个除以零的错误。虽然这是一个错误并且需要解决,但它是否应该停止所有人的工资发放?
日志级别具有严重性的层次结构,随之而来的是发生频率。跟踪日志可能非常普遍,但致命的日志事件应该非常罕见。我们可以在图 10.2 中看到这一点。如果我们添加一个新的日志级别,它将如何融入这个层次结构?

图 10.2 日志级别作为严重性和发生频率的层次结构
虽然创建额外的或不同的日志级别是可能的,但我得出结论,改变日志级别与行业规范相悖,就像逆流游泳一样,我决定采用清晰定义的日志级别。如果你正在考虑定制日志级别,底线是要准备好投入相当多的努力,从传达日志级别细节到弄清楚所有受影响的内容,再到确定日志框架设置。
考虑到之前描述的重叠,审计事件应该使用日志级别吗?通常,审计事件应该是良性的,因此应该记录在 info 级别,如第 10.2.3 节所示。然而,如果你的审计包括财务考虑,那么像账户余额的借记/贷记失败交易这样的失败交易应该成为审计跟踪的一部分,并反映事件的重大性。因此,事件可能需要人工干预,这将创建后续的审计事件,显示可能的干预。值得考虑将日志事件链接起来以获得更好的洞察。
10.3 清晰的语言
编写日志条目有时会感觉枯燥乏味,搜索日志文件也不吸引人。在消息中包含笑话(或者更糟)以使日志更有趣,或者添加当我们的代码而不是简单直接的语言时容易搜索的随机关键词,这种诱惑是存在的。问题是,你将不再是查看这些消息的人。有相当大的可能性,需要与日志一起工作的人可能不使用你使用的语言作为他们的第一语言,因此消息的含义将不会被理解。在记录异常时使用像Geronimo ...或“代码的一大步”这样的轻松话语,对大多数人来说可能是轻松的,但它可能引起混淆,或者更糟,将来可能冒犯到某人——所以,抱歉,现在是时候变得无聊、简单和直接了。使用措辞使日志搜索更容易是好的,但不能以牺牲理解的风险为代价。
与此类似,我们应该仔细选择我们在日志消息中使用的词语。这很重要,因为我们确实经常使词语的含义超载。例如,当使用 API 时,有些人将术语视为仅仅是接口规范,就像你在 OpenAPI、Blueprint 和 Swagger 等技术中看到的那样。其他人实际上是指实现 Swagger、Open API 或 Blueprint 定义背后逻辑的代码。这个问题不仅仅是一个 IT 问题;它发生在我们使用的业务领域或编写软件的过程中。
10.4 人类和机器可读性
强调日志需要既易于人类阅读又易于机器读取的需求似乎非常明显。但当我们编写日志事件条目时,很容易专注于对我们人类来说读起来好的内容,而忘记我们可能希望使日志事件可操作,这意味着我们应该确保日志事件尽可能结构化。例如,如果我有一个日志条目说“收到了过去的时间戳 12345678”,它可以变成{"IncorrectData": "12345678", "AttributeImpacted": "schedule", "DataType": "date-time", "Reason": "date past"}。阅读日志消息可能需要一点更多的认知努力,但后一种格式在处理上可以有很多可能性,从错误原因到我们表示值的方式。
10.5 上下文是关键
理解任何日志事件都需要上下文。当我们开发和使用跟踪和调试日志时,上下文在一定程度上是我们所知的,可能是隐含的,因为代码的位置将是上下文的一部分,或者正在运行的测试场景将是上下文。但当我们进入生产环境时,上下文可能不会是隐含的,因此我们需要使其明确。上下文的关键在于我们如何回答以下问题:
-
什么——正在报告什么,是一个错误还是仅仅是一个跟踪?
-
何时——日期和时间。如果你使用某种形式的日志框架,这部分是容易的。
-
位置——日志事件源在代码和基础设施中的位置。
-
原因——当涉及到信息和更高日志级别的日志级别时,我们提供信息的原因是至关重要的。是即将发生问题,还是我们在报告你想跟踪的事情,比如登录操作?
-
谁——谁触发了操作?谁的数据可能会受到影响?
让我们更详细地探讨这些点。
10.5.1 上下文:内容
日志事件的“内容”部分通过在事件中包含日志级别来部分解决。对于跟踪日志事件,当结合位置信息时,事件被记录的事实可能就足够了。对于信息日志级别及以上,我们将提供一些额外的细节:这个信息记录是为了审计目的吗?发生了什么类型的错误?警告与什么相关(例如,存储空间不足)?最佳地支持“内容”的是允许识别事务的细节,包括事务类型。事务数据或代理,如事务的唯一 ID(这样我们就可以查找实际的事务数据),应该提供足够的洞察力;例如,如果事务缺少对相关数据的引用,我们需要看到该值没有被设置。
10.5.2 上下文:何时
日志框架为您解决了大部分问题,无需任何努力,但很可能会回到服务器的系统时钟。在第二章中,我们强调了时区、时钟偏差等问题的影响。如果你在寻找一个运行在采用夏令时的时区(因为有人查看时间戳,它看起来不正确,因为他们正在应用夏令时,但日志没有)或全球分布式的解决方案时,这些问题可能会让你陷入困境。因此,你需要知道服务器所在的时区。一个选择是将时区配置到日志框架中,但更好的做法是将所有服务器对齐到协调世界时(UTC)。
当尝试将日志分析与用户错误报告对齐时,你需要明确用户正在工作的时区以及错误报告是按照他们的时间还是系统时间记录的。
10.5.3 上下文:位置
命名代码位置需要了解代码是如何处理的。当代码被用于商业解决方案,并且可能使用混淆和最小化工具时,这一点尤为重要,尤其是在基于脚本的解决方案,如 JavaScript 中。因此,依赖于反射来获取代码位置的细节可能是不有帮助的。尽管有一些工具,但一些混淆提供者会包括映射信息,以便根据正确信息识别原始代码。
注意:有关代码最小化和混淆的更多信息,请参阅以下资源:
-
Jeremy L. Wagner(Manning,2016 年)所著的《Web Performance in Action》的 liveBook 版本,可在
mng.bz/g4RV找到
应用程序通常是多线程的(例如,Java)或在 I/O 等待时进行上下文切换(例如,Node.js),并且它们是单线程的。上下文切换意味着我们一次不处理一个事务,因此理解感兴趣事件之前或之后的事件是否相关可能变得具有挑战性。这可以通过包含事务 ID 或会话 ID,或者利用日志事件的一部分开放跟踪或开放遥测 ID 来克服。一些日志框架会在它们的配置中帮助您捕获线程或进程 ID。例如,在 Fluentd 中,我们可以利用日志文件输出中的WorkerId。
“Where”也可能受到软件版本的影响。在生产环境中,我们可能同时拥有同一逻辑的多个版本,以支持诸如
-
运行 A/B 部署以帮助评估某个实现是否改善了用户体验
-
以高可用性运行,因此软件更新需要滚动更新来发生
这里还有另一种看待它的方法:你注意到这本书中有一张图像渲染得不是很好。你联系了 Manning。为了帮助你,我们需要知道哪个图是错误的。如果这个问题之前已经出现过,并在新版本的书中被修复了呢?这并不是说每个日志事件都需要发布每个版本信息方面,但我们确实需要让它容易提供足够的信息。也许当我们记录错误或更糟的情况时,这些信息会被写入日志。这是一个在日志事件中注入信息可能有所帮助的领域。如果日志事件被识别为软件中异常的重新发布,例如错误,那么 Fluentd 可以检索正在运行的软件版本并将其注入到日志中供将来参考。
10.5.4 上下文:为什么
这归结于事件发生的原因——是错误还是仅仅是一个显示代码位置(跟踪)或应用程序当前状态(调试)的信号?随着我们向更高层次(警告、错误和致命在我们的分类中)移动,“为什么”变得更加重要,并且仅从日志级别来看不那么明显。信息级别的日志事件可能是一个审计或系统当前状态的定期快照,无论事情是好是坏——例如,记录消息队列的深度。然而,日志事件消费者需要能够理解事件生成的原因。经过一些思考,这很容易解决。
可以包含一个简单的属性,例如“当前状态”或“审计操作”,以及共享数据。实际上,我们在许多情况下为日志事件提供了一个二级分类。鉴于我们提供了额外的元数据,我们可能会在开发组织内部保持一致性的情况下对其进行结构化。
当涉及到报告警告和错误时,原因回到了触发警告或错误的原因。这是主要错误,还是之前问题产生的副作用?试图表明错误是原因还是效果是困难的。如果我们能确定,我们应该明确指出;如果不能,我们可能给日志事件消费者一些关于可能性的提示。编码此类信息可能很复杂且难以测试。但很容易链接到错误代码并提供确认原因或效果的步骤。
我们通过事件生成的记录需要清楚地提供信息,以帮助进行诊断,不仅限于操作层面,还包括代码中是否可能需要更多防御性代码或更好的数据验证。由于解决方案目前处于不愉快的路径上,我们不应害怕提供更多信息——只要它不会引发敏感问题,我们将在稍后讨论。对于错误处理路径,我们处于一个性能不应成为考虑因素的位置,因为这部分代码库应该很少执行。一般来说,信息过少比信息过多要糟糕得多。
10.5.5 上下文:谁
记录“谁”可能会很棘手。正如我们稍后将看到的,记录可识别为个人的信息将使我们的日志处理受到立法、合同和商业要求的约束。尽管如此,我们将在本章稍后更深入地探讨这一点。重要的是要考虑何时需要“谁”,以及我们是否可以安全地使用其他数据作为真实身份的代理。例如,也许“谁”仅在登录会话期间相关,因此我们只需要携带会话 ID 并使用它。如果我们需要将会话中的操作归因于特定个人,我们将以安全的方式单独记录。那个会话 ID 同样可以是交易或订单 ID 等。
当记录诸如失败的登录或不需要特定个人的应用程序交互等事件时,我们可能仍然需要一个“谁”的值,例如原始 IP 地址。例如,单个服务器的 ping 可能无害(活跃服务报告可能只是这样做),但来自同一位置的真正快速重复发生则不好。然而,拥有那个 IP 意味着可以确定是同一系统在调用,因此可以确定要阻止的对象。
“谁”上下文的应用
与客户的 DevOps 团队合作,我们发现客户的安全团队雇佣了一家第三方组织定期对所有面向互联网的服务器进行探测。我们弄清楚了发生了什么,因为我们看到我们的 API 网关服务器定期报告来自几个 IP 地址的非法请求。一旦我们确定了模式和记录的详细信息,如 IP 来源、时间和 HTTP 请求,我们就向安全团队提出了怀疑,他们确认了第三方组织的使用。
不要忘记,“谁”可能是一个系统或应用程序组件。例如,在处理工资单时,该活动是由调度器触发的。因此,了解哪个调度器或调度器触发了该过程是有帮助的。
10.5.6 捕获上下文的一个实用清单
解决什么、何时、何地、为什么以及谁的问题可能有些抽象。我个人尝试通过以下问题来解决这个问题:
-
事件来自代码的哪个位置?
-
如果你的代码在生产中有多个版本的可能性?如果是这样,那么哪个版本就变得很重要。
-
事务是如何处理的?如果需要修复问题的数据影响,这一点尤为重要。
-
哪个服务器、进程或线程遇到了问题?如果问题是与基础设施相关的,你需要知道它关联的是哪个服务器、虚拟机或容器。
-
错误的原因是否可以识别(例如,除以零作为一个错误标识了参与错误的数据值)?错误日志能否追踪回代码中的某个位置?至少,表达错误的性质,如果可行的话,相关的数据值(例如,除以零错误——说明被除以零的内容以及相关的值)。
-
在特定路径中执行是由哪些数据值引起的?
10.6 错误代码
作为开发者,我们倾向于从自己的角度来编写日志细节。在一个 DevOps 组织中,开发团队也处理操作,这是可以的。但许多大型组织可能会选择采用*信息技术基础设施库(ITIL**)的方法来处理错误和问题管理,或者你可能有一个人们在你无法触及的地方部署的产品;我们需要考虑得更长远一些。对我们来说,ITIL 的一个重要方面是其对已知错误的定义:
已知错误是一个有记录的根本原因和解决方案的问题。已知错误在其生命周期内通过问题管理流程进行管理。每个已知错误的详细信息都记录在已知错误记录中,这些记录存储在已知错误数据库(KEDB)中。通常,已知错误由问题管理识别,但已知错误也可能由其他服务管理学科提出,例如事件管理。
更多信息可以在mng.bz/enpQ找到
简而言之,一个组织将记录错误及其解决方案。这就是我们为什么分配错误代码的原因。错误代码允许我们提供一个简单的查找,以便将错误与适当的文档相关联。文档应描述错误并提供详细信息,包括要执行的一组补救措施(如果这涉及到将系统恢复到最佳状态而不损坏或丢失数据,这是必不可少的)。当然,如果日志事件在真正出错之前被记录并采取行动,那么这些行动可能是预防性的。
错误的原因可能是用户操作或已捕获的应用程序中的错误;无论哪种情况,我们都应该在日志信息中添加错误代码。最好不要在 UI 中弹出错误代码,因为这可能会削弱用户对产品的信心。但这不应阻止你在用户操作触发问题时将错误代码链接到适合用户的消息。
错误代码使得客户能够轻松地查找错误代码、描述和推荐响应,以便将其纳入 KEDB(已知错误数据库)。构建这样的错误代码内容可能看起来要求很高;但这远非如此。在开发软件时,最简单的解决方案是使用一个协作的电子表格来分配错误 ID,确保 ID 是唯一的。然后,从开发人员那里获取预期的简要描述。构建解决方案文档可以稍后进行。
使用错误代码的一个好处是,它使得错误文档的标准化和国际化变得相对容易。错误代码与语言和地区无关;一旦你有了代码,你就可以在适当的语言中查找代码文档。
你可以在软件开发过程中融入各种额外的技巧,例如将文档包含到代码管理工具中。因此,你可以在代码中发布文档,以便细节与你的发布流程相关联。代码质量工具可以查找错误或致命日志条目,并应用正则表达式来查看是否有错误代码与之相关联,等等。
错误代码编号
创建错误代码编号的一些建议:
-
如果没有以字母字符为前缀,不要在数字前使用前导零——这可能会导致数字处理时被截断,或者为格式化(例如,指定长度和前缀字符的数字字符串)增加额外的工作。然后,如果将该数字转换回字符串,它将不再匹配作为信息查找的关键字。
-
不要从 1 开始;最好从全范围的最低位开始(例如,1000)。
-
使用搜索工具(例如,1000)使错误代码易于查找,因为代码不太可能被索引,而 AppErr1000 则更有可能被索引。例如,Oracle 将它们的数据库错误代码以 ORA 为前缀,而 WebLogic 以 BEA 开头;因此,它们更有可能被索引,在搜索中更独特。
-
很有诱惑力简单地在一个代码块(类、接口、头文件,取决于你的语言)中记录所有错误代码;毕竟,有一个地方可以查找,并且可以从代码生成文档。但这是不被推荐的。你最终会得到一个所有内容都会依赖且不断变化的代码,而系统正在开发中。每个错误代码的增加都会导致代码变更,并产生巨大的依赖影响。这种影响巨大的代码变更将引发担忧(即使它们没有正当理由)并产生对变更的阻力。
最好在共享知识库中编译详细信息,例如维基或协作知识库,这样每个人都可以维护而无需担心。请注意,在代码中定义错误代码的本地子集是可以的——这应用了 DRY 原则。例如,与特定模块相关的错误代码可以一起定义,因为添加错误代码很可能会与模块开发同时进行。
-
将错误代码分组为家族,就像在 HTTP RFC 中看到的那样,但对此要实际;一个错误代码可能在逻辑上属于两个分组(例如,与数据库连接错误——这是数据库问题还是网络连接问题?)。
-
这并不一定意味着要预留数字范围。然而,代码可以前缀或后缀一个简短代码来在产品或子系统级别提供范围;例如,BEA-00000 和 ORA-00000 表示两个不同 Oracle 产品(WebLogic 和 Oracle 数据库)的错误代码。
-
如果你从不同的异常中使用了相同的错误代码,尝试在辅助信息中区分其来源点。
-
考虑错误代码的消费者;一个错误可能来自单一位置,但有不同的原因,因此为原因使用不同的错误代码。这将使支持团队的工作或修复变得更加容易。
10.6.1 使用标准错误
一些技术提供代码来表示成功和错误,这些代码已经得到了很好的记录,例如 HTTP(RFC tools.ietf.org/html/rfc7231#section-6);其他包括 SMTP(电子邮件服务)、Oracle WebLogic 服务器等。在我们的日志中使用这些代码有助于通过共同的意义和理解提供更多上下文,只要它们被正确使用。例如,简单地使用标准 HTTP 200 或 400 代码的做法并不帮助。使用 HTTP 413 代码告诉请求者他们发送了过多的数据要有效得多,更有意义,而且这还会在日志中显示任何网络路由设备。
预定义错误代码的使用确实需要谨慎判断,因为软件中的异常类可以被视为错误代码的特殊形式。但是,正如我们将在本章后面看到的,这些情况可能导致清晰度下降。
10.6.2 代码不仅可以用于错误
错误代码是最重要的唯一可识别的消息。将代码与操作流程文档(而不是用户流程)关联的原则意味着你可以将事件回溯到特定的操作建议,这些建议可能包括执行数据库优化过程到存档日志文件。
10.7 过少记录或过多记录?
将什么内容放入日志可能会很棘手。有时很容易将整个数据结构/对象整个放入日志事件中,假设它都是相关的。这种方法提出了两个挑战:
-
因此,记录的数据量可能会变得非常大,过度增加计算和存储的工作量。
-
很可能你最终会记录敏感细节。
当处理日志需要付出过多努力时,代码应用条件控制并没有什么可以阻止的,因此当我们需要大量信息时,我们可以获取到。更糟糕的是——是配置控制记录的信息还是修改代码以获取足够的信息?底线是调整日志框架配置以避免过多的日志输出,这比修改代码更可取,至少从配置文件变更周期可能更快这一点来看是这样的。软件变更治理控制可能会在发布时要求更高的谨慎,从而减慢日志文件将支持的任务。
问题在于记录整个数据结构可能导致日志包含敏感数据,如个人或信用卡数据,这两者都有严格的规则来保护包含此类数据的任何内容。重要的是在日志中提供足够的信息上下文,而不写入任何敏感数据。如果事件被记录到安全的存储中,例如数据库,我们就有可能为该事件分配一个 ID。然后,其余的日志可以通过记录记录的事件 ID 来实现。如果需要,这允许你在不分散日志值的情况下回到数据上下文。
这部分问题不仅涉及到我们如何编写应用程序日志,还涉及到我们解决方案的设计。最好的例子是处理 HTTP 调用。在我们实施 Web 应用程序时,在 HTTP 调用到达我们的应用服务器之前,HTTP 流量将穿过防火墙、负载均衡器、代理、网络路由器和其他基础设施元素。即使你有史上最好的 HTTPS 配置,头部信息也必须可读,以便将流量路由到目的地。通常,这些组件会记录 URI 和通常所有的 HTTP 头部。头部可能包含有关处理请求和响应的详细信息(例如,头部包含指示基础设施是否可以缓存内容的属性)。最终结果是,如果你将敏感值放入有效载荷 URI 或头部,敏感信息可能会意外地被记录。
如果你有一个捕获敏感数据的本地日志文件,并且没有修复代码的方法,我们需要控制由此产生的问题。一种方法是使用 Fluentd 通过一些逻辑处理日志事件,以去除敏感数据。在将日志发送到或简单地写入单独的本地文件之前实施这种逻辑可以帮助控制影响(有些人可能会说是敏感数据被记录的“爆炸半径”)。通过配置应用程序的日志,使其尽可能短暂,并且原始日志永远不会备份或复制到任何地方,这种策略可以得到进一步的帮助。
10.7.1 什么算是敏感信息?
决定什么数据是敏感的可能会很棘手,因为它可能由许多因素驱动:
-
数据的商业估值
-
立法要求
-
信息公开领域的后果
许多复杂性来自立法的拼凑,不仅在国际上,而且在各国国内。例如,在欧洲,所有国家都批准了GDPR(通用数据保护条例),越来越多的国家采用了类似的立法(例如,澳大利亚)。但在欧盟内部,一些国家有额外的立法,因此仅遵守 GDPR 可能是不够的。在美国,控制措施既有联邦的也有州驱动的,加利福尼亚州领先并采用了类似 GDPR 的立法,但并非所有州都效仿。
由于 GDPR 似乎是许多人的起点,因此值得探讨它试图实现的目标以及它可能产生的影响。核心原则如下:
-
合法性、公平性和透明度原则
-
目的限定原则
-
数据最小化原则
-
准确性原则
-
储存限制原则
-
完整性和保密性原则
-
责任原则
这些原则向下延伸,为那些我们保留数据的人赋予了几项权利:
-
个人有权知道为什么保留他们的个人数据以及这些数据将用于什么目的。
-
个人有权要求获取关于他们的存储信息。
-
个人可以要求更正数据中的任何不准确之处。
-
个人可以行使“被遗忘”的权利,这意味着所有数据都将被删除。
-
个人可以要求限制其个人数据的用途。
此外,组织必须能够向负责监督 GDPR 合规性的机构(例如,英国的信息专员办公室)证明其行动的合理性;例如:
-
数据存储的时间长度。
-
证明采取了确保完整性和保密性的行动。
正如您所看到的,这有一些深远的影响。例如,如果您的系统正在处理工资数据,那么当涉及某人工资的数据被记录时,日志文件就会成为大量安全要求的对象。如果这是您自己的个人数据,您希望它得到与应用程序中副本相同的安全处理。在一些国家,有法律权利被遗忘(即,从所有系统中删除个人的所有记录)。这不仅会创建从应用程序中删除数据的任务——这是容易的部分——而且可能还需要定位和删除任何可能执行处理的服务器上的个人日志条目,以及备份等。仅找到这样的细节本身就非常耗时。这还包括确保日志文件安全以及确保对日志文件访问可防御的要求。
所有这些导致了一个论点:虽然日志数据对于问题诊断、审计等等很重要,但我们应尽可能减少放入日志中的敏感数据。控制已记录的数据,我们就可以消除这些日志需要遵守所有规则的需求。
如果需要保留敏感数据,那么如果可能的话,请将其单独保存。当您无法将其单独保存时,不要在日志数据源之间使用临时日志,并确保最终目的地安全。在 Fluentd 方面,这意味着通过以安全方式配置的前向插件来确保“数据在传输中”的安全(例如,实施 TLS,控制密钥和证书的访问)。如果数据存储在临时或临时文件中,那么为临时文件的安全设置(“数据静止”)就更加复杂。这包括诸如文件系统的访问控制、对文件应用适当的加密以及创建和管理加密密钥的所有工作。潜在的挑战可能包括需要实施正式流程来管理存储数据的物理驱动器的处置,即使它是临时的。别忘了,这也适用于在文件缓冲区使用时创建的文件。
回到日志分析和日志统一的思想,其基本原理是在必要时才记录敏感数据,并且只将其保存在足够安全的位置。最小化日志事件传输中的“接触”点。对待日志就像对待实际数据一样。
注意:可以从全球律师事务所 DLA Piper(www.dlapiperdataprotection.com)免费查看关于数据保护法的全球视角。附录 E 包含多个链接,可以帮助您快速找到可能影响您日志(或应用程序)的立法。
10.7.2 GDPR 只是开始
国家立法并不总是关于数据处理的地点或数据代表的公司或公民的国籍。它可能来自其他来源;另一个众所周知的安全要求来源是支付卡行业(PCI)数据安全标准(DSS)。PCI 专注于处理支付卡,如信用卡。所需的安全级别基于处理的交易总额,有具体、详细的要求,涵盖基础设施、软件和运营流程。如果卡数据被记录在日志中,毫无疑问,日志将需要遵守 PCI 规则。像个人数据一样,这些规则也适用于存储日志的硬件、处理日志的应用程序以及日志信息对用户的可见性(即开发人员和运维人员)。
许多组织已经采取措施来定义敏感数据以及他们认为可接受的使用的声明(合规性的一部分)。这对于特定环境来说是一个很好的参考点。在提供服务的地方,服务条款和条件也可能决定什么被认为是敏感的。但作为一个经验法则,以下被认为是敏感的:
-
任何使个人唯一可识别的数据,例如个人地址或社会保障号码(即个人可识别信息,或PII)。
-
任何可能对组织或个人产生财务影响的数据,因为信息泄露可能会造成严重损害。这包括诸如收费和信用卡、银行账户和财务报告(此类信息提前泄露可能导致在证券交易所上市公司的内部交易)等数据。
-
任何与个人相关的临床数据。
到目前为止,我们已从试图最小化其安全影响的角度来查看日志。当日志得到良好定义和管理时,可以有助于证明符合商业和法律要求——例如,记录谁何时访问了信息,以及,可能更重要的是,何时拒绝了访问。这些信息可用于证明正确的控制措施,并且可以使其具有操作上的可执行性。
为了说明这一点,以下是一些其他立法或标准的例子,其中日志事件的用途可以提供审计轨迹以解决对立法要求的合规性问题:
-
萨班斯-奥克斯利法案(SOX)(
mng.bz/Bxl8)及其变体,如 J-SOX(日本)、C-SOX(加拿大)和 TC-SOX(土耳其)。 -
健康保险可携带性和问责法案(HIPAA)(www.hhs.gov/hipaa/index.html)。
-
ISO/IEC 27001。虽然这不是由立法驱动的规则集,但这是一套最佳实践标准,可以认证(www.iso.org/isoiec-27001-information-security.html)。
数据风险:一个类比
处理敏感数据的风险有点抽象,所以让我们看看一个类比。想象它是有毒的;数据有多敏感反映了毒液有多危险。如果你喜欢可视化,考虑一个无毒蛇作为低风险数据项,而非常敏感的数据,如政府身份证,就像水母。然而,有了合适的设备、环境和专业知识,被咬或蛰的风险很小。无毒蛇的咬伤可能很痛苦,如果未得到治疗,可能成为感染源;更有毒的蛰或咬伤是严重的,但如果准备充分并且有抗毒血清,你会在及时治疗下存活。问题不仅仅是数据有多有毒;而是有多少毒液(有多少咬或蛰)——换句话说,数据的数量。我们可以限制日志文件中处理的数据的数量和敏感性;风险越低,必要的预防措施就越简单。
10.8 日志结构和格式
通过对消息应用结构,可以使信息更具可操作性,因为处理日志的逻辑可以从数据中提取意义。假设我们遇到了数据库连接错误,这产生了结构化的日志事件。实现一个解析表达式来检索数据库连接器错误代码和数据库详细信息并不困难。这可以通过 Fluentd 完成,因此可以向相关的数据库团队发送信号。日志分析工具可以对相同的数据采取行动,但警报会晚些时候发出,可能已经发生了更多问题。但是,日志分析可以通过检查历史记录来确定问题是否重复发生,如果是的话,频率是多少,以及是否有注册问题的节点(s)的共性。
日志的结构化不仅限于此,我们还需要在时间戳、日志级别、位置、线程 ID 等细节周围有一个结构,这些细节有助于提供上下文。有一些行业公认的格式。图 10.3 提供了与应用逻辑通常相关的一些格式的详细信息。

图 10.3 当结合使用时,将提供优秀的日志事件和强大的机制,以便在必要时使用
10.8.1 将制作日志条目以准备应用程序发布投入实际应用
作为开发团队的一部分,你的基于服务器的应用程序已经具备了足够的特性。Beta 测试和早期采用者客户正在成功使用你的软件。管理层认识到,为了提供成本效益的支持,需要提供文档以避免系统管理员感到沮丧和不必要的支持电话。此外,良好的支持文档将有助于防止支持请求返回到开发团队。你被要求确定需要实现什么,以及实施所需的时间,以及是否可以采取措施来最小化销售活动启动前的等待时间。
答案
有很多事情可以做,没有单一的正确答案,除了在本章讨论的基础上进行构建。在估计这样的任务时,最简单的事情是搜索当前活动的代码库中的日志事件,并记录事件类型。然后估计评估日志事件与图 10.3 中所示的不同因素的努力。务实地说,如果估计被压缩,最好是采用自上而下的方法(见图 10.2)。
在处理代码检查和修改日志事件所估计的努力之外,你还需要估计生成支持文档的努力。如果你通过网站等渠道提供文档,而不是将其嵌入到代码中,那么在文档完成之前,软件发布就可以开始了。显然,有一种诱惑是不完成这项任务,而是专注于下一个产品版本。相反,不解决这个问题将直接影响到支持电话的数量,包括那些被升级回开发者的电话,即使在更传统的组织安排中也是如此。
10.9 如果可能,请使用框架
大多数编程语言都提供日志框架,要么作为基础语言的一部分,要么作为库(我们将在第十一章中更深入地解释这些)。采用日志框架可以帮助以多种方式改进你的日志。潜在的好处包括以下内容:
-
日志数据的统一性。
-
日志级别的统一性
-
日志条目的结构
-
-
为你管理的附加上下文信息(例如,如果框架理解 OpenTracing,它可以拉取跟踪上下文值)。
-
框架可以让我们通过配置来控制查看的内容,这使得确定需要多少或多少日志变得更加容易。
-
重要的是,在容器化环境中登录时,如果我们不需要重新解析文本输出以应用上下文和意义,我们可以节省大量的努力。这节省了处理能力,许多日志框架允许我们直接输出机制,从而有助于避免这种开销。
使用许多第三方或语言原生解决方案之一是首选的,因为它们已被验证并证明有效。即使是帮助你驱动一致性的简化代码片段也是很有价值的。我在职业生涯开始时工作的关键任务系统就属于这一类别。如果你选择使用 Homebrew,我们强烈建议采用行业标准格式,以便更容易地将数据导入其他工具。
10.10 开发实践
我们已经看到了一些可以积极改善情况的方法。但是,有些常见的开发实践在本质上可能是负面的,即使意图是积极表达的。
10.10.1 重新抛出异常
捕获并重新抛出异常(在代码中拥有捕获块并使用 throw 语句再次引发异常的行为)是一种不良实践,可能会对日志产生不希望的影响,因为通常情况下,当捕获到异常时,它会被记录。这意味着如果你捕获并重新抛出异常,你很可能会为同一个问题产生多个日志事件。当分析问题所在时,你增加了确定哪个日志事件是问题首次实际发生的负担,并且增加了警报的数量,这使你更接近于“通知风暴”。
通知或警报风暴
如果你将日志事件链接到通知机制,如电子邮件或 Slack,那么通知或警报风暴是需要警惕的。如果你不断收到相同的错误,例如当逻辑陷入无限循环中尝试做某事(例如,尝试向没有存储空间的文件写入),那么你最终会在通知渠道中饱和相同的消息。最终结果是每个人都关闭并取消订阅通知;更糟糕的是,系统认为你的应用程序生成垃圾邮件,并将其阻止。幸运的是,有一些技术可以抑制此类场景,例如 Fluentd 中的过滤器(mng.bz/OG6E)或日志框架本身。例如,Log4j2 提供了一个 BurstFilter(mng.bz/YgKA),而 Log4Net 有一个扩展,可以执行类似的功能(mng.bz/GG1O)。
10.10.2 使用标准异常和错误结构
我对使用编程语言的标准异常的看法可能是一个更有争议的点,因为我并不完全同意标准异常应该被抛出的说法。例如,Joshua Bloch 的《Effective Java》(Addison-Wesley Professional;第 3 版,2017 年)提倡,如果你有防御性代码,并且你接收到的值是 null(在 Ruby 中是 nil)但不应该是,那么你的代码应该使用语言自己的NullPointerException或IllegalArgumentException。提出的论点是你可以从代码重用中受益,而且阅读你的 API 的人会更容易理解 API。虽然重用考虑可能有其价值,但使用语言预定义的异常类,因为它将有助于可理解的定义,这更多是关于良好的命名约定,而不是对代码的洞察。
真正的问题出现在查看日志事件时;很难确定这个异常是由于防御性编码还是潜在的 bug 导致的。区别在于防御性编码指向一个可能的上游问题。如果有人添加了防御性检查,那么很可能有人考虑了可能出现的问题以及如何使事物处于可恢复状态。
虽然我的例子和最佳实践的引用主要集中在 Java 上,但基础原则适用于支持异常框架的语言,Python 和 Ruby 是两个例子。其他语言,如 Go,有错误结构和处理不同结构类型返回的能力。所以,这个问题亟待解决,答案是什么?
回到 Java,从我的角度来看,创建一个简单的单行类,该类扩展了一个具有清晰、有意义的名称的基础类(例如,class IllegalBufferConfigurationException extends IllegalArgumentException)并没有什么问题。毕竟,这就是语言如何应用继承来处理 Java 中的原生异常(例如,NullPointerException扩展了Exception类)。Ruby 的大致对应物是ArgumentError,它扩展了StandardError,而StandardError又扩展了Exception。如果一个异常名称很好,那么它将清楚地说明为什么抛出异常。它还会告诉读者正在防御的具体场景以及 API 调用者应该或不应该做什么。所以当我们看到像NullPointerException这样的通用异常时,我们很可能会看到一个更基本的问题,一个尚未考虑过的问题。如果我们已经考虑了一个问题,我们可能知道补救措施可能是什么。
10.10.3 字符串构造作为不记录日志的理由
在日志记录时,我们有时需要通过组合几个元素来构建日志消息,以产生实用的信息级别。将数据转换为字符串并将它们连接起来需要一点 CPU 努力时间。让我们假设一下,我们正在创建一个 info 级别的日志消息,因此如果有人将日志过滤器阈值设置为警告,那么日志消息可能会被过滤掉。在这种情况下,构建日志消息的 CPU 努力实际上是浪费的。这已经被用作不费心在代码中实现日志记录的论据,因为日志消息的构建消耗了处理努力而没有获得任何收益。
这个论点试图从我的角度来看合理化不投资评估日志将如何帮助实现适当的代码。我们有技术手段可以最小化成本。但也许更重要的是,与开发者尝试调查和理解他人代码的成本相比,少量 CPU 循环的成本确实有利于帮助开发者。我并不是提倡编写效率低下的代码。然而,为了支持性和可维护的代码(包括合理的日志记录)而增加的计算周期成本远远小于节省的开发者努力成本。
回到实际的技术手段以避免浪费,我所知道的每个日志框架都提供了查询当前设置的日志级别的手段,允许代码决定是否构建日志负载有任何价值。这些有时被称为“守卫”函数,可以像这样应用:
Logger.ifDebug
{
myLogMessage = '{"attribute:" + aStringValue + ","
➥ + aArrayOfKeyValues.toJSON + "}"
Logger.debug (myLogMessage)
}
显然,精确的代码将根据所使用的框架和特定语言的语法而有所不同,但您应该明白这个要点。
在过去 5 到 10 年中,我们看到大多数主流语言都发展出了支持 Lambda 或惰性执行的能力。这意味着我们现在可以编写代码,守卫是隐式的,如果隐式条件得到解决,那么后续的表达式才会被评估和执行。例如:
LOGGER.atDebug().log('{"attribute:" + aStringValue + "," + aArrayOfKeyValues.toJSON + "}")
结果是可忽略的计算成本和优化,同时不丢失日志代码的存在。再加上编译器、虚拟机和解释器所看到的性能改进。我们在获得性能——以 GraalVM 和 Quarkus 作为例子。当你考虑这一点时,我们正在看到更多的效率提升,这远远超过了不编写日志语句所带来的提升。
更多关于 Quarkus 和 GraalVM 的信息
-
Quarkus 和微服务开发:请参阅 John Clingan 和 Ken Finnigan 著的《Kubernetes 原生微服务》的 liveBook 版本,
mng.bz/zQ5Q。 -
GraalVM 简介:请参阅 Benjamin Evans 等人著的《扎实的 Java 开发者》(第 2 版)的 liveBook 版本,
mng.bz/0w96。 -
GraalVM 主页:www.graalvm.org/
-
Quarkus 主页:
quarkus.io/
摘要
-
使用清晰、简单的语言,并在适当的情况下,将错误代码与日志事件关联,将显著简化对日志的理解,并基于日志事件采取任何必要的行动。
-
良好的日志事件将包括超出日志级别的上下文信息,包括诸如指示相关进程在哪个服务器上运行等详细信息。
-
立法可能会影响日志事件,尤其是当日志生成包括诸如 PII 或信用卡数据等内容时。因此,对日志数据施加了广泛的额外安全控制和限制。
-
一些组织还会将内部数据如商业价值(例如,产品或服务的利润率)进行分类。在创建或传输日志事件时,理解组织的敏感性非常重要,以确保遵守与记录此类数据相关的组织要求。
-
使用错误代码与日志事件结合使用的价值是显著的,从确定问题起源于系统的哪个部分到使修复说明易于识别。
-
将行业标准应用于记录的内容可以加速对数据和其含义的理解。
11 日志框架
本章涵盖
-
检查日志框架的特性
-
为应用程序选择日志框架
-
直接从日志框架调用 Fluentd
-
从没有框架的应用程序中调用 Fluentd
在上一章中,我们探讨了如何创建具有最大意义和价值的日志事件。我们还可以通过使用日志框架来轻松地从日志中获取更多价值。如今,大多数编程语言都能够使用日志框架。在某些情况下,第三方日志框架比语言原生功能出现得更早,并成为了一种事实上的标准。其他日志框架则作为应用程序容器或平台的一部分出现,以解决原生解决方案中感知或证明的弱点。
本章将探讨日志框架的格局,因为它们在能力和设计上存在许多共同点。对这一点的理解将帮助我们欣赏“可能的技艺”,并在选择框架时做出明智的决定。我们还将探讨不同语言的更主导框架是否能够支持直接连接到 Fluentd 的能力。Fluentd 也通过为多种语言提供日志库来帮助我们在这个领域,因此我们将研究这些库,以了解它们如何适应我们拥有的选项。
如果框架或 Fluentd 库不是选项,我们显然可以让我们的应用程序写入文件。我们已经看到 Fluentd 可以消费此类信息。但是通过文件连接比直接连接应用程序效率低。如果你在与 AWS Lambda 和 Oracle Cloud Functions、Microsoft Azure Functions、Google Functions 或通过 Fn Project(fnproject.io/)自托管函数一起工作,你会认识到这些服务非常短暂。因此,这些非常短暂的服务在高效日志记录方面更具挑战性。尝试连接到存储可能配置更复杂,连接速度更慢,因此更适合基于网络的日志记录。因此,我们将探讨如何更直接地与 Fluentd 通信。
11.1 日志框架的价值
无论日志框架的起源如何,它们都在一定程度上解决了以下关键主题:
-
提供一种使用日志级别分类轻松输出日志事件的方法
-
允许通过配置控制发送的日志事件
-
将日志事件定向到不同的输出形式,例如文件、stdout、HTTP 等。
虽然日志级别可以追溯到 Syslog 标准(RFC 5424,tools.ietf.org/html/rfc5424)以用于应用程序开发(与导致 RFC 5424 定义的操作系统级别工具相比),但影响日志库的最强因素之一是 Apache Log4J。这种影响可以归因于 Apache 软件基金会将该设计和实现移植到多种不同的语言。但它的作用远不止于此。尽管基于相同的需求可以得出非常相似甚至相同的答案,但你可以在许多其他语言的日志框架中看到非常相似甚至相同的 API 和功能。一些与 Apache 软件基金会无关的日志框架公开承认借鉴了 Log4J 的设计原则。为了保持开放和透明,我进入开源领域是在我开始使用 Java 1.2 时,因此我的观点可能有些偏见。
跟随 Log4J 路线的美丽之处在于第三方可以实施框架的某些部分,因此应用程序不会看到任何差异。然而,配置可能会改变行为,例如日志的存储方式,从平面文件到数据库。我们将在下一两个部分中更详细地了解这一点。
注意:对 Log4J 的引用可能会引起一些混淆,因为有两个版本——Log4J 和 Log4J2。当今天提到 Log4J 时,可以假设它指的是版本 2。版本 1 在 2015 年被宣布已结束其生命周期。版本 1 和 2 在理念上并没有根本的不同。但版本 2 是为了解决版本 1 实现的一些弱点而重写的;这意味着实现可以编写以利用新的语言特性。
11.2 日志框架的典型结构
由于 Log4J 在许多日志框架和语言中的影响,最好从检查 Log4J 结构开始。我们可以轻松理解和掌握其他框架。图 11.1 展示了这种结构以及与不同类的关系(我们使用了经过一些调整的 UML 类符号,如图所示;www.omg.org/spec/UML/)。我们可以看到涉及的类或模块是 Logger Context、Configuration、Filter、Logger、Logger Config、Formatter 和 Appender。

图 11.1 使用 UML 类符号表示的常见日志结构,包括表示关系中的数量,如 0 或 1 到多
在接下来的部分中,我们将描述每个组件所扮演的角色。我们根据它们的逻辑对日志框架的使用和行为影响程度对组件进行了排序。
11.2.1 日志上下文
这是应用程序中框架的基础。它负责保留对特定日志记录器对象的引用。它将处理任何配置文件,根据需要创建必要的日志记录器对象。
日志记录器上下文通常是一个“一站式”商店,用于处理所有日志元素;在应用程序中,这个类用于检索一个将处理相关日志事件的对象(由日志记录器对象的实例表示)。当对日志记录器上下文请求一个日志记录器对象时,它可以推导或使用参数来确定提供哪个日志记录器对象。如果没有与提供的标识符(通常是一个逻辑名称或类路径)关联的特定日志记录配置,则将提供默认的日志记录行为。
根据实现,它还可能协调任何细节,例如连接池等。这是唯一一个可以确定只有一个对象的地方,使其成为所有 Log4J 配置值的根。
11.2.2 追加器
追加器的任务是最容易关联的,也是处理日志事件的关键。根据具体的日志记录框架实现,追加器可能被称为适配器或传输器,因为这一层负责接收日志事件并将它们发送到适当的目的地。例如:
-
使用 TCP/IP 消息等技术进行传输
-
使用对 Logstash 等服务进行 API 调用的方式
-
将日志事件写入或追加到文件的末尾(因此得名)
每个追加器将使用过滤器来控制它可能需要追加哪些日志事件。追加器还可以使用格式化程序将事件的内部表示转换为输出方式;这可以从 JSON 到制表符分隔的行。某些类型的追加器只能以特定方式发出日志事件;这种关系有时可以简化并合并为单个类或模块。
在日志记录框架的配置中,可以看到配置了多个不同的追加器,以将某些事件发送到多个目的地,并使用不同的日志级别。
11.2.3 日志记录器
可以定义多个日志记录器(或仅使用上下文默认值),以便不同的应用程序部分可以以不同的方式使用日志记录。例如,为记录官方审计事件和通用应用程序审计跟踪使用单独的日志记录器。官方审计事件可能需要发送到数据库,并且所有事件,包括审计,都应该发送到日志记录框架。然后可以在代码中选择这些日志记录器。将会有不同的配置与不同的日志记录器相关联,例如使用哪个追加器、应用哪些过滤器等。
通过拥有多个日志记录器,我们可以从为代码库的不同部分调整配置中受益,甚至可以为应用程序的部分拥有多个配置(例如,将错误记录到stdout并将所有内容记录到文件中)。
11.2.4 过滤器
过滤器确定哪些日志事件应该被发出,主要是通过确定日志事件是否在配置中设置的阈值之上或之下。由于过滤器与附加器相关联,可以配置不同的日志级别到不同的日志目的地。例如,我可以将控制台附加器的日志级别设置为 Warning,并将文件附加器的日志级别设置为 Info。结果是只有 Warning 和 Error 事件会发送到控制台,但文件中包含了更多细节。
11.2.5 格式化器
如附加器所述,格式化器的任务是构建附加器输出,以便日志条目以所需或需要的方式呈现(例如,以 12 小时或 24 小时格式显示时间)。一些附加器将允许灵活性(例如,文件附加器)。
11.2.6 配置
通常,我们希望通过配置而不是代码来驱动应用程序的日志记录,因为这样可以在不必要进行侵入性代码更改的情况下配置日志。这也使得配置的验证过程更加迅速。它允许我们根据部署环境来改变日志的处理方式。例如,我们可能为开发机器配置一个将所有内容发送到 stdout 的配置。然而,在测试和生产环境中,配置被设置为将日志发送到 Elasticsearch。
11.2.7 日志记录器配置
日志记录器配置是特定日志记录器总日志配置的一个子集(参见第 11.2.3 节)。这将跟踪相关的配置部分并将其转换为代码中的正确对象。这可能包括使用工厂设计模式等。
11.3 附加器结构
通常,通过继承或封装的层次结构来构建附加器,以便每一层复杂性都可以利用更简单的操作。最终,这将取决于标准接口定义,以便无论附加器如何,它们都以相同的方式进行编排,就像 Fluentd 对其插件所做的那样。在图 11.2 中,我们可以看到 Log4J 是如何通过从实现接口并提供公共逻辑的基本类进行继承来组织其附加器的。然后从这个派生层扩展,提供一组基本附加器,例如控制台附加器。从这个派生层,我们看到层叠构建了专业化的增加。这最显著的是 AbstractOutputStreamAppender,它随后用于一般的套接字用例,并进一步专门化以将日志发送到符合 Syslog 的解决方案。

图 11.2 Log4J 的一些附加器关系的 UML 表示
11.4 日志框架概览
日志框架的数量相当多,大多数语言都有本地的能力以及开源框架。在附录 E 中,我们收集了一系列常用语言的日志框架列表。我们还提供了一些关于主导框架的详细信息,其中一些是语言本地的选项,以及如何获取更多信息。
除了日志记录框架之外,一些库提供了程序化接口以及 API 与几个不同流行框架之间的映射。熟悉 Log4J 的人可能听说过SL4J (www.slf4j.org),它抽象了 Java 本地的日志框架 Log4J,另一个叫做 Logback。因此,可以透明地切换日志框架。有了这些抽象,需要一种实例化所需日志框架的方法。这可以通过实现工厂(mng.bz/KB00)或依赖注入(mng.bz/DxZw)模式来实现。这种抽象的另一个例子是.NET 本地的日志记录(更多详情请见mng.bz/9KV1)。
11.5 选择框架
当评估要采用的日志框架时,应考虑一些因素以帮助选择最合适的框架。我们开发了一套问题,这些问题将帮助你评估需求并选择满足这些需求的框架。通过审查这些问题,将有助于确定你在日志框架方面的优先级。一旦这些问题被赋予了某种形式的优先级,评估框架与问题的匹配程度将更容易,以查看它们如何满足你的需求。问题如下:
-
可用的追加器有哪些?它们是否仅限于一种类型的追加器,例如文件?是否有现成的追加器可以与你的日志统一解决方案一起工作,例如 Fluentd 或 Logstash?
-
是否可以定制或优化追加器的行为?例如,日志轮转或网络端口和地址是否可配置?
-
是否可以根据应用程序的不同部分定制日志事件的输出?例如,应用程序框架(如 Spring 或 Core .Net)的日志阈值设置为
Warning和Error,但你的自定义逻辑可以设置阈值为Info。 -
定制日志配置(不使用代码)有多容易?你可能希望调整日志记录,如果存在操作问题,理想情况下可以更新或覆盖默认的日志配置,以选择性地获取更多信息。
-
框架为你提供了多少信息(例如,为跟踪点提供方法和类名)以及正确结构的时戳?
-
是否可以定制日志输出格式(例如,JSON,XML)?这个问题反映了前一章中提到的最佳日志具有结构,允许日志事件既可读性又可机器读取。
-
脚印有多紧凑(这在物联网用例中很重要)?对于物联网和移动解决方案,我们需要有一个紧凑的脚印来限制资源使用。
-
您能否使日志输出安全——使用 TLS、加密文件等?安全性是否足够处理所处理的数据?
-
框架会对我的应用程序的吞吐量/性能产生重大影响吗,尤其是在最终的 I/O 阶段?日志是否会变成一个线程阻塞机制?
-
日志框架的 API 是否易于使用?如果应用程序代码中的调用难以使用,开发者可能会避免创建日志事件。理想情况下,接口将是直观的,但拥有良好的支持文档进行参考可能非常有价值,尤其是对于刚开始他们职业生涯的开发者。
而不是评估所有可能的选择,尝试缩小选择范围是值得的。附录 E 中的 E.11 表格中的细节可以在这里有所帮助,因为它们反映了我们认为最重要的和/或最占主导地位的日志框架。
11.5.1 将优化应用程序日志投入实际应用
您组织中的 Fluentd 采用情况良好,您被要求确定当前使用的日志框架是否能够满足未来的需求,或者 Fluentd 的成功是否允许支持更改日志框架的情况。使用所描述的因素,评估您的开发团队正在使用的当前解决方案。将其与一个替代方案进行比较(查看附录 E,看看是否提供了替代选项)。
答案
由于我们显然无法为这个练习提供特定的解决方案,我们希望您已经发现您已经在使用一个日志框架,并且它与您的需求很好地匹配。如果您的日志框架并不非常适合,您可能已经发现了问题。如果您还没有确定当前框架的问题,这份考虑事项列表应该有助于确定问题。
11.6 Fluentd 自身的日志和追加器
如果正在使用的日志框架没有提供针对 Fluentd 的特定支持,会发生什么?有几种方法可以克服这个问题。一种方法是通过使用 Fluentd 提供的库。
根据语言的不同,Fluentd 日志库的实现可能包含之前描述的一些或所有结构,例如追加器和过滤器。这些实现可能与本地语言的日志库一起工作并扩展它,例如 Python 和 Ruby。在其他情况下,Fluentd 库与框架不匹配,通常在没有本地语言日志库或已确立的占主导地位解决方案的情况下。在这些情况下,库将更加直接,并且需要直接从您的代码中使用。
您可能已经建立了一个没有连接到 Fluentd 功能的日志框架,并且 Fluentd 提供的库不会自动集成到框架中。在这些情况下,可能可以找到额外的开源软件来包装或扩展 Fluentd 库,以便在框架结构中使用。我们可以仅通过配置来更改日志事件的处理方式,而不会影响应用程序代码。当然,您也可以创建代码来填补这个差距。根据语言、Fluentd 库和框架的组合,这最棘手的部分很可能是如何向 Fluentd 库提供配置值。
在表 11.1 中,我们可以看到 Fluentd 如何通过库支持不同的编程语言。我们还建议了其他开源选项,这些选项将允许日志代码与 Fluentd 通信。
表 11.1 Fluentd 可以直接或间接集成到本地或常用框架中的位置
| 语言 | 是否有本地日志库 | Fluentd 日志库 | 其他开源解决方案 |
|---|---|---|---|
| Erlang | Y | github.com/fluent/fluent-logger-erlang |
|
| Go | N | github.com/fluent/fluent-logger-golang |
|
| Java | N | github.com/fluent/fluent-logger-java |
Log4J: github.com/tuxetuxe/fluentd4log4j |
| Node.js | N | github.com/fluent/fluent-logger-node |
直接与 Log4JS 集成 |
| OCaml | N | github.com/fluent/fluent-logger-ocaml |
|
| Perl | N | github.com/fluent/fluent-logger-perl |
Log4perl: metacpan.org/pod/Log::Log4perl::Appender::Fluent |
| PHP | N | github.com/fluent/fluent-logger-php |
github.com/Seldaek/monolog |
| Python | Y | github.com/fluent/fluent-logger-python |
|
| Ruby | Y | github.com/fluent/fluent-logger-ruby |
|
| Scala | N | github.com/fluent/fluent-logger-scala |
通过 Logback 与 SLF4S 的兼容性 |
如果您没有合适的日志框架或包装层,那么您可以直接在核心应用程序代码中使用 Fluentd 日志库。与所有事情一样,这种方法也有其优缺点。因此,在表 11.2 中,我们列出了直接使用库的优缺点,以帮助您做出明智的决定。
表 11.2 使用 Fluentd 自带的日志框架的优缺点
| 优点 | 缺点 |
|---|---|
| 脚本占用小,因为它只为输出到 Fluentd 提供支持 | 锁定使用 Fluentd。对于打包解决方案,你最好不要尝试强制其日志工作方式与它提供的选项不同。这可能更适合考虑一个自定义插件或找到一个折衷的配置。 |
| Fluentd 日志库提供了与其他框架相同的程序员接口,提供了可比的开发体验。但 Fluentd 服务器在处理日志事件方面提供了比日志框架更复杂的处理能力。 | |
| 与 Fluentd 的通信是通过网络进行的。使用 msgpack 压缩意味着高效的通信,并可以限制托管复杂性(例如,容器的外部存储,FaaS 的存储复杂性)。 |
将日志直接发送到 Fluentd 的最终可能选项是利用框架的插件通过 TCP/IP 或 HTTP(s)进行通信,并使用这些协议发送日志事件。这些路径意味着你没有任何库依赖(假设你的编程语言可以提供基本的网络功能)。
11.7 直接将应用程序日志记录到 Fluentd 的示例
现在我们已经了解了日志框架的结构,选择日志框架时需要考虑的因素,以及在没有框架的情况下直接将日志发送到 Fluentd 的可能性,让我们看看不同的直接日志记录方法在现实中是如何表现的。每个示例都将进一步偏离理想的抽象连接到 Fluentd,但将展示如何实现直接通信。在每种情况下,我们将传输一个简单的日志消息到 Fluentd。
对于这些示例,我们选择使用 Python,因为它
-
支持使用 Fluentd 库与其本地的日志框架一起使用,以展示最理想的选项
-
是一种广泛使用的语言,其语言结构易于阅读并映射到其他语言
-
是一种脚本语言,因此不需要额外的努力来设置和运行编译过程(与 Java、C#等相比)
-
与 Fluentd 的实现语言不同,因此有助于说明与 Fluentd 合作的语言无关性
11.7.1 使用日志框架的 Python:使用 Fluentd 库
在大多数情况下,将 Fluentd 库直接插入到日志框架中是理想的,因为我们可以在不更改任何代码的情况下配置不同的日志记录方式。让我们从创建由配置文件驱动的日志框架的代码开始;然后我们的应用程序使用该框架记录日志事件。为了实现这个示例,我们需要建立一些代码和配置,我们将在接下来的列表中回顾:
-
一个简单的 Python 测试应用程序,用于使用日志框架并生成日志事件
-
一个配置文件,用于告知日志框架如何以及记录什么
-
一个 Fluentd 服务器和配置,以便它可以接收日志事件
在第 11.1 节中显示的代码中,我们可以看到 Python 测试应用程序,它从配置文件中创建一个配置对象,并将其传递到日志上下文中,然后请求一个日志记录器对象。有了日志对象,我们可以多次使用该对象。在我们的示例中,我们然后在日志消息中构建内容——这里,日期时间的字符串表示。然后日志框架被调用两次,一次作为文本消息,再次作为 JSON 结构。当您查看代码时,请注意 Fluentd 引用的完全缺失。这一切都是根据配置由日志框架为我们处理的。
列表 11.1 第十一章/clients/log-conf.py—测试 Python 客户端—配置信息
import logging
import logging.config
import yaml
with open('logging.yaml') as fd: ❶
conf = yaml.load(fd, Loader=yaml.FullLoader)
logging.config.dictConfig(conf['logging'])
log = logging.getLogger() ❷
now = datetime.datetime.now().strftime("%d-%m-%Y %H-%M-%S")
log.warning ('from log-conf.py at ' + now) ❸
log.info ('{"from": "log-conf.py", "now": '+now+'"}') ❹
❶ 加载配置文件,该文件将描述所需的日志设置
❷ 获取正确的日志记录器对象;由于没有提供特定的名称,我们将得到默认设置
❸ 生成日志事件
❹ 对于传递给库的日志事件,最好是创建 JSON。但首先构建 JSON 对象会分散我们试图说明的重点。
第 11.2 节中显示的配置是应用程序中加载的详细信息,并由日志框架解释以建立所需的日志方式。只有配置将驱动日志通过定义 处理器(或使用我们之前描述的命名方式称为附加器)直接与 Fluentd 通信。注意配置实体如何关联回图 11.1 中所示的结构,其中日志记录器通过名称引用处理器(附加器),处理器引用用于与 Fluentd 一起工作的格式化程序。在这里,过滤器没有解耦,而是在日志记录器和处理器中使用 level 属性指定。
列表 11.2 第十一章/clients/logging.yaml—测试 Python 客户端:配置
logging:
version: 1
formatters: ❶
fluent_fmt:
'()': fluent.handler.FluentRecordFormatter ❷
format:
level: '%(levelname)s'
hostname: '%(hostname)s'
where: '%(module)s.%(funcName)s'
handlers: ❸
fluent:
class: fluent.handler.FluentHandler ❹
host: localhost
tag: test
port: 18090
level: DEBUG
formatter: fluent_fmt
loggers: ❺
'': # root logger
handlers: [fluent]
level: DEBUG
propagate: False
❶ 定义日志事件将如何表示
❷ 我们仍然需要告诉日志框架哪个类将实现格式化器接口。在这里,我们有一个用于 Fluentd 的自定义格式化器,因此服务器将接收到正确结构的日志事件。
❸ 定义处理器(附加器)并提供与我们的 Fluentd 节点通信所需的配置
❹ 识别知道如何实际与 Fluentd 服务器通信的类
❺ 定义默认日志记录器对象和默认设置,并将默认日志配置链接到相关处理器
为了使 Fluentd 能够与示例一起工作,我们提供了一个配置文件。如果您查看第 11.3 节中的配置文件,您将看到配置了两个来源。使用 forward 输入插件的来源将接收日志事件。您可以通过比较配置文件中的端口号和日志记录器 YAML 文件来确认这一点;我们将在稍后使用另一个来源。
列表 11.3 第十一章/fluentd/http.conf—简单的 HTTP 来源
<system>
Log_Level debug ❶
</system>
<source>
@type http
port 18080
<parse>
@type none ❷
</parse>
</source>
<source>
@type http
port 18085
<parse>
@type json ❸
</parse>
</source>
<source> ❹
@type forward
port 18090
</source>
<match *>
<format>
@type json
</format>
@type stdout
</match>
❶ 将日志设置为调试以帮助我们理解正在发生的事情。鉴于 HTTP 的行为高度可配置,让我们确保假设的配置是兼容的。
❷ 在这个阶段,我们不必担心预期的结构,将日志事件作为通过 HTTP 接收的单个字符串处理。
使用不同的端口,我们可以通过 HTTP 以 JSON 结构获取日志事件。
使用转发插件,我们可以以 JSON 文本或使用 msgpack 压缩的形式接收有效负载。
要启动,我们需要两个 shell 窗口;在导航到包含所有提供资源的文件夹后,可以使用以下命令启动 Fluentd:
fluentd -c Chapter11/Fluentd/http.conf
这之后,导航到Chapter11/clients文件夹并执行以下命令
python log-conf.py
执行后,您将看到 Fluentd 服务器将以 JSON 格式将生成的日志事件写入控制台。
11.7.2 直接调用 Fluentd 附加器
现在,让我们看看如何使用 Fluentd Python 库直接从我们的应用程序而不是日志框架中查看代码可能的样子。虽然这是 Python 实现,但大多数支持的语言的日志库工作方式相似。显然,由于编程语言的工作方式限制,每种实现可能都有所不同。例如,Go 没有像 Python 和 Java 那样的类和继承,而是有模块和类型。
为了便于比较直接调用 Fluentd 日志库的方法与使用日志框架的方法,我们创建了一个新的 Python 测试客户端,如列表 11.4 所示。第一个明显的区别是我们需要显式地将 Fluentd 库导入到我们的代码中。我们的代码不再建立日志上下文和日志对象,而是与一个发送者交互,这是一个特定的实现,即附加器(或 Python 所说的处理器)。发送者对象使用连接到 Fluentd 所需的配置来构建(当然,您也可以从通用配置文件中检索这些数据)。与之前一样,我们正在构建要放入日志事件消息中的时间。然后,最后,我们可以使用 Fluent 库的emit或emit_with_timestamp函数来传输日志事件。emit 函数需要将有效负载表示为哈希表(或使用 Python 的命名,即字典)。
列表 11.4 Chapter11/clients/log-fluent.py—直接使用 Fluentd 库
import datetime, time
from fluent import handler, sender ❶
fluentSender =
sender.FluentSender('test', host='localhost', port=18090) ❷
now = datetime.datetime.now().strftime("%d-%m-%Y %H-%M-%S")
fluentSender.emit_with_time('', int(time.time()),
➥{'from': 'log-fluent','at': now}) ❸
直接依赖 Fluentd 库的导入
创建 Fluentd 处理器的实例
发送日志事件
要看到此场景运行,需要重新启动 Fluentd,就像我们在 11.7.1 节中所做的那样。这意味着当收到日志事件时,它们将在 Fluentd 的控制台会话中显示。然后,在第二个 shell 中,我们需要在Chapter11/clients文件夹中运行以下命令
python log-fluent.py
11.7.3 仅使用 Python 的日志库的示例
在之前的示例中,日志记录直接使用了 Fluentd 日志库(使用其发送对象)和间接使用(使用 Python 日志框架和配置)。这次,我们将探讨在不使用 Fluentd 库的情况下如何工作。如果您检查 Fluentd 库的代码,您会发现该库使用我们在第三章和第四章中遇到的 msgpack 压缩机制。Msgpack 是 Fluentd 的一部分,而不是 Python 日志本身的本地部分。因此,当仅使用本地层工作时,我们无法从 msgpack 提供的压缩中受益。克服这一点的唯一方法是我们实现自己的格式化代码,该代码使用 msgpack。
在不开发自己版本的 Fluentd 库的情况下,下一个选项是使用预构建的 Python 日志处理器(或我们称之为附加器)直接与 Fluentd 通信。这种方法对 Python 来说价值不大。但如果您想在另一种语言中使用比较方法,这可能是有必要的。
由于并非所有语言都能从 Fluentd 库中受益,让我们看看在没有这种帮助的情况下需要如何实现。在这种情况下,我们将利用预构建的 HTTPHandler(大多数语言都有类似的功能)。与前面的示例一样,我们提供了另一个客户端实现,如列表 11.5 所示。为了使其工作,我们实例化一个 Python HTTPHandler 用于日志记录,并包含必要的连接细节。请注意,在连接中,我们分别提供了服务器地址和 URL 路径。Fluentd 预期的是一个路径,而不是尝试与根地址通信。我们提供了一个自定义格式化程序并将其附加到处理器上。然后我们通过相同的格式化过程来形成日志事件的一部分,并使用日志事件字符串调用日志记录器对象。
使用预构建的 HTTPHandler 意味着 Fluentd 配置需要包含一个 HTTP 源插件,这我们已经完成了。
列表 11.5 Chapter11/clients/log-simple.py—直接使用 Fluentd 库
import logging, logging.handlers ❶
import datetime
testHandler = logging.handlers.HTTPHandler('localhost:18080',
➥ '/test', method='POST') ❷
custom_format = {
'host': '%(hostname)s',
'where': '%(module)s.%(funcName)s',
'type': '%(levelname)s',
'stack_trace': '%(exc_text)s'
}
myFormatter = logging.Formatter(custom_format)
testHandler.setFormatter(myFormatter)
log = logging.getLogger("test") ❸
log.addHandler(testHandler) ❹
now = datetime.datetime.now().strftime("%d-%m-%Y %H-%M-%S")
log.warn ('{"from":"log-simple", "at" :'+now+'"}') ❺
❶ 需要导入日志和处理器所需的核心类
❷ 创建 HTTPHandler 并提供详细信息;在受保护的生产环境中,这还包括使用证书。
❸ 如果尚未存在,则创建要使用的记录器
❹ 我们将创建的日志处理器添加到根日志对象中,以便使用。
❺ 调用处理器。在实现特定语言的代码版本时,理想情况下您会使用库来生成 JSON,而不是手动注入。
要运行此示例,请像之前一样打开两个外壳。导航到根文件夹,然后启动 Fluentd。一旦运行,请在每个外壳中使用以下命令执行 Python 脚本(对于 Python 脚本,请从 Chapter11/clients 文件夹执行):
fluentd -c Chapter11/Fluentd/http.conf
python log-simple.py
11.7.4 无 Python 的 logging 或 Fluentd 库的示例
虽然在现实世界中没有停止使用 Python 日志框架的理由,但在其他语言中可能没有这样的选择,所以让我们看看这可能会是什么样子。为了保持连贯性和便于比较,我们将使用 Python 来演示这一点。大多数语言都提供了与 HTTP 服务交互的手段,而不需要任何依赖。我们可以与 Fluentd HTTP 源插件交互,因为我们已经消除了 Fluentd 的日志库。但现在我们负责构建所有的 HTTP 头部,处理 HTTP 连接以保持其开启,以及关闭连接,如列表 11.6 所示。这个列表遵循了之前客户端文件的相同模式,以便于进行并排比较。
如您所见,代码通过填充诸如内容类型和内容长度等详细信息来填充头部。这在很多方面应该感觉熟悉,因为这些几行代码与我们第二章中“Hello World”场景中配置 Postman 的方式完全相同。由于这是使用 HTTP 连接,我们再次没有从 msgpack 压缩中受益。
列表 11.6 Chapter11/clients/log.py—无任何支持的日志记录
import httplib, urllib
import datetime
message = '{"from":"log.py", "at":"'+datetime.datetime.now().strftime
➥ ("%d-%m-%Y %H-%M-%S")+'"}'
headers = {"Content-Type": "application/JSON", "Accept": "text/plain",
➥ "Content-Length":len(message)} ❶
conn = httplib.HTTPConnection("localhost:18085") ❷
conn.request("POST", "/test", message, headers) ❸
response = conn.getresponse()
print response.status, response.reason
conn.close() ❹
❶ 手动填充 HTTP 头部属性
❷ 创建连接
❸ 发送日志事件
❹ 我们负责关闭资源。
假设 Fluentd 服务器仍然在运行之前的示例中,那么我们只需要运行命令(从 Chapter11/clients 文件夹)
python log.py
如前所述,我们应该期待看到日志事件被写入 Fluentd 服务器控制台。
11.7.5 将 Fluentd 调用端口到另一种语言的实现
您所在的公司正在其制造设施中试用一些具有自定义功能的智能设备。目前已知这些试用智能设备支持包括 Java、Python 和 Ruby 在内的几种核心语言。已经提出了一种观点,即智能设备在需要或必须发送数据时会调用中央服务器。这样做可以通过在需要时才开启无线电源来节省电池电量。这个原则可以应用于记录智能设备遇到的问题。为了尽可能保持软件占用空间最小,您被要求不要添加任何额外的库。您被要求提供一个概念证明,以证明设备是否可以直接与您当前的 Fluentd 基础设施通信,而不是需要一个作为智能设备和 Fluentd 之间代理的定制解决方案。
答案
我们的主要语言是 Java 和 Groovy(Groovy 运行在 Java 虚拟机上)。我们已经使用原生 HTTP 调用创建了一个小的 Groovy 示例,用于连接 Fluentd 服务器。这个示例可以在 Chapter11/ExerciseResults/native-fluentd .groovy 中找到。Groovy 虽然带来了一些开销,但允许我们快速地生成证明,因为我们不需要设置构建和打包环境(我们已经在书中介绍了 Groovy 的设置)。
你应该已经使用你喜欢的语言产生了类似的结果,并使用我们简单的 Fluentd 配置或你自己的配置来展示结果。
11.7.6 使用通用 appender:要点
正如你所见,使用通用协议是可能的,但这确实增加了开发工作量。此外,如果不付出更多努力,你将无法获得 msgpack 的好处,也不知道库已经被验证。因此,如果你不能使用预构建的 Fluentd 库,考虑寻找或开发一个包装器,该包装器将翻译日志框架与 Fluentd 库提供的接口之间的工作方式。
摘要
-
如果事件日志可以直接从应用程序发送,而不必求助于使用中介,如文件,那么可以从中获得效率。
-
许多日志框架具有一组共同的特征,尽管它们通常被称作不同的名称。这些特征包括一个抽象的机制,用于将日志发送到一种消费者类型(称为appender、handler或sender)。另一个共同元素是一个解耦的格式化器,它将待表示的日志事件转换为下游可以理解的形式。
-
在审查日志框架时,需要就框架提出一些问题,以确定其适用性:它是否有为 Fluentd 或其他组件提供的本地 appender?是否可以回退到写入文件?是否可以通过 HTTP(S)或 TCP/IP 传输日志事件?
-
Fluentd 提供了一系列支持各种编程语言的日志库,包括 Ruby、Java、Python 等。在某些情况下,这些库可以与语言的本地日志框架集成。
附录 A. 安装额外的工具和服务
A.1 工具安装概述
本书除了运行 Fluentd 所需的组件和工具外,还使用了其他组件和工具。这些有助于说明 Fluentd 如何工作以及如何与其他功能集成,例如 MongoDB 和 Elasticsearch。第二章探讨了 Fluentd 和 Fluent Bit 的安装和配置,因为它们是本书的核心,包括 Fluentd 对 Ruby 的依赖以及 LogGenerator 的使用。如果您想尝试书中描述的场景,您将不得不下载并安装附录中描述的工具。
以下章节提供了足够的细节,以安装支持本书中示例的工具。如果没有指定安装值,则应假定默认值。要将安装转换为生产级部署,您需要查阅额外的资源,其中许多资源可在附录 E 中找到。
尽管我们已经努力确保涵盖 Windows 和 Linux 的安装,但与它们相关的 Linux 版本和包管理器(yum、apt 等)很多,这使得事情变得复杂。为了测试 Linux 说明,我们使用了 Ubuntu 18 LTE。这意味着您可能需要调整特定 Linux 版本的操作步骤。如果您可以访问社交媒体或在线 Manning 论坛,请随时分享这些调整。
注意:正如您可能知道的,Linux 和 Windows 的目录和文件路径在使用正斜杠和反斜杠时有所不同。本书中使用的许多工具无论在哪个操作系统上都可以使用相同的命令。因此,我们没有为这两种类型的操作系统提供相同的命令。我们假设您将能够识别何时根据您的平台反转反斜杠。
A.2 创建环境变量和修改 PATH
设置环境变量和扩展PATH环境变量是一个常见的需求。让我们以设置 JAVA 为例来总结如何完成这项工作。后续安装将参考本节。要检查PATH是否可能需要修改,我们可以在 Windows 上使用echo %PATH%和在 Linux 上使用echo $PATH来查看其值。
A.2.1 Windows
以下操作需要谨慎进行,因为它代表系统级更改。在以下命令中,将<path addition>替换为新路径——例如,c:\java\bin:
setx path "%path%;<path addition>"
关于setx的更多文档,请参阅mng.bz/jyAP。
在 Windows 中,您可以通过设置 UI 和搜索环境变量来进行更改。这将定位用于操作PATH和其他环境变量的 UI 元素。具体步骤在不同版本的 Windows 之间略有不同。如果您使用这种方法,任何 shell 窗口都需要重新启动才能看到更改。
A.2.2 Linux
根据你的 Linux 版本,如果你安装了桌面 UI,它很可能提供基于 UI 的更新PATH的解决方案。然而,鉴于可用的 UI 多样性,没有标准答案,但这是一个常见需求,因此网络搜索应该会给出答案。
在以下命令中,将 <path addition> 替换为相关路径(例如,/etc/java/bin)。这将把该目录添加到执行命令的 shell 的路径中:
export PATH="$PATH:<path addition>"
要使更改在系统范围内生效,将之前的命令添加到 shell 脚本中(例如,setup-for-fluentd.sh),并将 shell 脚本保存在/etc/profile.d文件夹中。确保 shell 脚本可执行(如果需要,运行chmod a+x setup-for-fluentd.sh)。有关在 Linux 中操作PATH的更多信息,请参阅bit.ly/SetLinuxPath。
A.3 Java 和 Groovy
这些两个元素的核心细节在第二章中有所介绍,因为它们是使用 LogSimulator 的基本要素。如果你希望在包管理器之外工作,可以在此处检索适当的安装资源:
如果你希望将 LogSimulator 用作 Groovy 实用工具,那么建议你注意与 Java 的兼容性细节,尤其是考虑到 Java 发布周期更快。并非所有版本都是长期版本,因此需要测试兼容性。
A.4 Postman
在第二章中,我们使用了 Postman 来发送基本的 HTTP 日志事件。可以通过访问identity.getpostman.com/login以简化浏览器的方式使用 Postman;它确实需要一个免费账户来设置。或者,可以从www.postman.com/downloads安装桌面解决方案。
A.5 Elasticsearch
本节探讨了 Elasticsearch 的基本设置。Elasticsearch 是一个常用的日志分析活动存储库,因此是 Fluentd 的重要目标。我们只涵盖足够的内容,以便我们能够使用 Fluentd,因此,要更全面地了解 Elasticsearch,请参阅 Radu Gheorghe 等人撰写的《Elasticsearch In Action》(Manning,2015)一书,可在www.manning.com/books/elasticsearch-in-action找到。
A.5.1 核心 Elasticsearch 安装
可以下载 Elasticsearch 的二进制文件,这些文件可以从 www.elastic.co/downloads/elasticsearch 获取。这包括针对不同 Linux 版本的包管理器版本,以及 zip 和 tar 格式。为此,我们将使用 zip 或 gzip/tar,具体取决于平台。下载后,解压缩压缩文件。对于 Windows,我们将假设 c:\dev\elasticsearch 为目标,以及类似 /usr/bin/elasticsearch 的路径。Windows 安装需要安装 Microsoft Universal C Runtime Library(可从 mng.bz/W7M1 下载)。
为了方便起见,将 Elasticsearch 的 bin 文件夹(例如,C:\dev\elasticsearch-x.y.z\bin,其中 x.y.z 是版本号)添加到 PATH 中,如 A.2 节中所述。如果您遇到任何问题,Elasticsearch 提供了一套全面的说明,可在 mng.bz/8l2w 找到。
要启动 Elasticsearch,我们可以运行 shell 脚本 elasticsearch bat 或 bash 脚本。或者,可以使用 elasticsearch-service 脚本将其添加、移除和控制为服务,以下是一些参数:
-
安装
-
移除
-
启动
-
停止
Linux 中守护进程服务的等价物是 elasticsearch -d -p pid,其中 pid 代表一个用于存储服务详细信息的文件。可以使用 Postman 在 localhost:9200 上执行 GET 请求来验证 Elasticsearch 的安装和启动。成功的结果将返回一个 JSON 有效负载,反映 Elasticsearch 服务器的状态。这如图 A.1 所示。

图 A.1 使用 Postman 验证 Elasticsearch 安装
A.5.2 Elasticsearch UI 安装
要查看 Elasticsearch 的内容,而不是使用 Postman 构建表达式,我们可以利用可用的 UI 之一。为了本书的目的,我们发现 Elasticvue 既有易用性,又非常简单易安装。Elasticvue 可以从 elasticvue.com 获取,并提供作为 Chrome、Firefox、Microsoft Edge 的浏览器扩展、Web 应用或 Docker 镜像的安装。我们采用了 Chrome 扩展,因为它无需配置,并且本地运行以适应 localhost 地址。此安装需要您从 Elasticvue 网站链接到 Chrome 商店,然后点击“添加到 Chrome”按钮。
一旦启动了 Elasticsearch 并打开了扩展,您只需确认要连接的服务器(http:/./localhost:9200)。一旦连接,Elasticvue 将提供有关服务器的统计信息,选择索引菜单将显示索引。这包括在运行示例后我们将使用的那些索引。
A.5.3 Fluentd 插件用于 Elasticsearch
Fluentd 的td_agent版本包含 Elasticsearch 插件out_elasticsearch。然而,未经修改的 Fluentd 版本并不包含它。因此,有必要使用命令fluent-gem install fluent-plugin-elasticsearch手动安装插件。
A.6 Mongo 数据库
我们首先在第四章中使用了 MongoDB 作为备选输出目标。MongoDB 在企业和社区版中都可用。我们将坚持使用社区版,因为它提供了我们需要的功能,并且不受商业许可的约束。除了核心 MongoDB,我们还想拥有 MongoDB 图形用户界面(GUI)Compass,它也有不同的版本。我们将使用免费版。
A.6.1 Mongo DB 安装
MongoDB 为 Windows 提供 MSI 安装程序,并为 Linux 和 macOS(RPM、DMG)等提供其他包管理工具。您可以通过访问www.mongodb.com/download-center/community下载当前版本。
注意:RPM 代表Red Hat Package Manager,DMG 代表磁盘镜像。
我们将假设使用 MSI 安装的最新稳定版本,因为它简单快捷,非常适合在开发环境中设置。在生产环境中,您更有可能根据您的基础设施和用例采用不同的安装策略——例如,在微服务环境中使用 Docker 或检索本地二进制文件和手动编写的配置文件以利用网络存储解决方案,如 SANs(存储区域网络)。
根据您的操作系统权限,您可能需要在 Windows 中以本地管理员身份运行 MongoDB 或在 Linux 中使用基于 root 的权限(使用sudo)。在标准的操作系统配置中,这无疑是必要的。
下载 MSI 后,开始安装。安装程序是自我解释的;请确保您选择推荐的完整设置。几个进一步的步骤后,安装程序将提供安装 Compass 的选项;请确保此选项被勾选。
A.6.2 MongoDB 配置
安装完成后,应启动 Compass,它将提供一个连接按钮(如果您的包管理器安装没有启动 Compass,请手动启动)。Compass 将显示一个带有基本设置的屏幕。点击图 A.2 所示的连接按钮。

图 A.2 通过 Compass UI 工具查看的 MongoDB 数据库
最后一步是建立一个用于存储日志事件的数据库。点击图 A.2 所示的创建数据库按钮。此操作将带我们到一个弹出窗口,该窗口将要求输入数据库和集合名称。为了我们的目的,我们将它们都称为Fluentd,如图 A.3 所示。

图 A.3 MongoDB Compass 创建数据库弹出窗口——为我们的示例定义数据库的最简单方法
我们不想指定任何 MongoDB 限制(固定集合)或对集合的定制。这意味着确保复选框没有被勾选。有了这个,我们就完成了 MongoDB 的设置,你可以点击创建数据库。
完成后,你将在列表中看到 Fluentd 数据库的添加,以及作为安装部分提供的默认数据库(admin、config 和 local),如图 A1 所示。如果你点击 Fluentd 数据库,你将看到数据库集合的视图。
最后一步是将 MongoDB 二进制文件添加到 PATH 变量中,以便运行一些简单的命令来快速刷新环境。确切的路径将取决于安装的版本以及任何从安装默认值中更改的安装配置值。一个典型的位置是 C:\Program Files\MongoDB\Server\4.2\bin, 其中 4.2 是 MongoDB 的版本号。应用此更改的步骤在 A.2 节中有详细说明。在 Linux 上设置 PATH 需要指向 MongoDB 二进制文件安装位置的路径,该路径将位于以下形式的文件夹中:mongodb-linux-x86_64-4.2.0/bin。这个文件夹的位置将更依赖于包管理器,但 /usr/bin 将是常规做法。
A.7 Slack
Slack (slack.com) 云服务有一个免费使用层,可以用作你的私有 Slack 工作空间。我们建议你为使用 Fluentd 的工作设置自己的空间,以保持事情简单。这也意味着,如果你尝试使用与工作相关的 Slack 工作空间,在设置应用程序令牌时遇到权限限制的问题,你将不会有任何问题。
如果你刚开始使用 Slack,你会发现网页界面已经足够,但将应用安装到设备(手机、平板或桌面)上会提供更好的体验(slack.com/downloads),尤其是当需要将应用置于后台而不是保持另一个浏览器标签页打开时。
下一步将是设置 Fluentd 所需的令牌。使用 Fluentd 网页界面选项“添加功能”,选择“入站 Web 钩子”>“机器人”>“权限”,然后点击保存更改。
在个人 Slack 工作空间中,我们需要使用 Slack 管理界面来配置一个应用以获取应用令牌。最简单的方法是在浏览器中登录 Slack 后直接跳转到 api.slack.com/apps。点击创建新应用的按钮,如图 A.4 所示。我们需要提供一个应用名称(我们建议您也将其用于插件配置中的 username 属性)并确保您的工作空间已选择为“开发 Slack 工作空间”选项;然后点击创建应用。UI 将提示您启用 webhooks。一旦完成,使用左侧菜单,我们需要选择 OAuth 和权限。屏幕底部是作用域的配置。我们特别想要修改 Bot Tokens 的作用域。可用的 API 作用域列表添加了 chat:write 和 chat:write:public,并确保 incoming-webhook 已经包含在内。完成这些后,您将在页面顶部看到两个令牌。我们需要复制 Bot User OAuth Access Token,因为当与 Fluentd 一起工作时需要它。然后点击重新安装应用按钮。现在 Slack 的配置已经完成。

图 A.4 Slack UI 创建“应用”界面,以及相应的表单,其中所需值已突出显示
A.8 设置 Docker 和 Kubernetes
本节涵盖了 Docker 和 Kubernetes 的安装,以支持第八章。本节有点棘手,因为 Linux 操作系统在软件包管理和已安装的软件方面有所不同。为了本节的目的,当涉及到 Linux 时,我们将假设使用 Ubuntu 18.04 或更高版本。这意味着 Windows 用户需要一些先决步骤。
如果您的 Linux 版本不是 Ubuntu(例如,Fedora、Debian),由于软件包管理器等原因,步骤可能会有所不同。在这种情况下,有几个选项:
-
使用 Docker 等工具设置 Ubuntu 容器实例,并连接到容器,这在很多方面可能和 Ubuntu 一样。
-
根据您的 Linux 发行版自行调整命令。
-
按照虚拟机的 Linux 版本设置,这将在后面进行解释。
第八章使用了虚拟机来仅与 Docker 一起工作,并使用了 minikube,这是一个 CNCF 支持的小型 Kubernetes 部署,用于运行本章的 Kubernetes 相关内容。通过在虚拟化环境中执行 Docker 相关活动,我们不会意外地污染 Kubernetes 设置或您可能已经使用 Docker 进行的工作。
采用 minikube 也意味着我们有了与 Marko Lukša(Manning Publications,2017 年)的书籍《Kubernetes in Action》中相同的本地部署方法。根据您的环境和部署方法,如果您的平台是 Windows,minikube 将使用 VirtualBox 或 Hyper-V;如果是在原生 Linux 环境中运行,则使用 Docker。
A.8.1 Windows 环境准备
预先条件将取决于正在运行的 Windows 版本。对于 Windows 10 和 11 家庭版或更旧的 Windows 版本,你的选择有限,这意味着需要按照下一节所述安装和使用 VirtualBox。Windows 10 专业版或更高版本提供了一些选项:
-
使用 Windows Hyper-V 管理器创建一个 Ubuntu 虚拟机;有关如何进行此操作的 Microsoft 文档可在
docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/quick-create-virtual-machine找到。如果采用这种方法,你可能还需要采取额外的步骤以确保可以在 Windows 主机和 Ubuntu 虚拟机之间复制和粘贴。这个解决方案的详细信息可以在
docs.microsoft.com/en-us/troubleshoot/windows-server/virtualization/copy-paste-not-work-hyper-v-vm-vmconnect-enhanced-session-mode找到。 -
替代方案(也是我的首选)是利用 Windows Subsystem for Linux (WSL) 2。这提供了一个在 Windows 原生和 Linux 之间切换的更简单的方法。设置这些步骤更为复杂,因为你需要安装 Docker Desktop (www.docker.com/products/docker-desktop) 并从 Microsoft 下载和安装几个额外的组件。这些步骤的详细信息可以在
docs.microsoft.com/en-gb/windows/wsl/install-win10找到。当设置 WSL 和 Ubuntu 发行版(我们的推荐)时,我们观察到在运行 Docker 命令,如
docker -–version以验证安装时,并非所有内容都报告正确。据报道,使用 Docker Desktop 的配置可能不正确,但在 UI 中看起来是正确的。在这种情况下,我们在 Docker Desktop 中禁用了 WSL2 选项,保存了更改,然后在没有 Ubuntu 运行的情况下,返回 Docker Desktop 设置并重新启用了 WSL2。接下来,我们启动了我们的 Ubuntu 实例;一旦运行,问题就自行解决了。
A.8.2 VirtualBox 方法
VirtualBox (www.virtualbox.org) 是一个 Oracle 支持的开源项目,旨在为桌面提供虚拟化,覆盖 Windows、macOS 和 Linux。它确实需要主机具有 AMD 或 Intel 芯片组。如果你使用 Hyper-V 虚拟化,VirtualBox 在 Windows 上将无法工作,并且它可能对 BIOS/UEFI 设置敏感。关于这个主题的更多信息可以在 www.howtogeek.com/213795/how-to-enable-intel-vt-x-in-your-computers-bios-or-uefi-firmware 找到。
第一步是从下载页面(www.virtualbox.org/wiki/Downloads)下载 VirtualBox,提供 Windows 可执行文件作为安装程序或适用于您 Linux 版本的相关软件包。下载完成后,运行安装过程以设置好。一旦安装了 VirtualBox 的核心,建议您安装扩展包,该扩展包可在下载页面上找到。
下一步是获取 Ubuntu 虚拟操作系统的构建。这可以通过以下任一方式完成:
-
从 VirtualBox 网站下载预构建的虚拟镜像,尽管提供的 Ubuntu 镜像已经过时。或者,使用像 OSBoxes(www.osboxes.org/ubuntu-server/)这样的网站,它提供可以导入到 VirtualBox 中的预构建镜像。使用此方法意味着您需要确保有 root 权限用户的凭据。
-
使用 VirtualBox 创建虚拟镜像的详细信息请参阅
mng.bz/Ex0O。
A.8.3 为与 Docker 一起使用准备 Ubuntu 镜像
镜像组织好后,建议您登录到 Ubuntu 镜像并运行以下命令,以确保包管理器是最新的:
sudo apt-get update
sudo apt-get upgrade
接下来,让我们使用以下命令验证curl实用程序是否可用:
curl -–version
如果响应返回正面并带有版本信息,那么我们就完成了。如果没有,则需要以下命令来安装curl:
sudo apt-get install curl
在我们的安装需求中,下一个是 Docker。这可以通过 shell 命令完成:
sudo apt-get install Docker.io
如果您有任何首选的实用程序、Linux 快捷键等,现在是配置它们的最佳时机。
在这个阶段,我们建议您停止 VirtualBox 虚拟机并导出 VM,以便在需要回到干净状态时有一个可用的起始状态。这意味着如果您想放弃当前 VM 并重新开始,只需停止您正在使用的 VM,删除它,并导入导出的镜像即可。
A.8.4 Kubernetes 安装
此安装过程在您开始使用 Kubernetes 之前并不需要,但需要提前执行 Docker 步骤,因为它们建立了一些先决条件。在计算能力方面,您至少需要 2 个 CPU、2GB 的 RAM 和 20GB 的存储空间。您能提供给 minikube 的资源越多,体验就越好。
我们曾考虑在虚拟机内部运行 minikube 的可能性,因此步骤将与主机是 Windows 还是 Linux 无关。但对于大多数不太可能需要它的用户来说,这会是一个更复杂的设置。
Windows 上的 Minikube
如果您打算在 Windows 平台上部署 minikube,我们建议使用Chocolatey 包管理器,这就是我们将部署 minikube 的方式。其他方法在 minikube 网站上详细说明,请参阅minikube.sigs.k8s.io/docs/start/。
注意:Chocolatey (chocolatey.org/) 为 Windows 提供了类似 Linux 软件包管理器的体验。对于不绑定到正式 Microsoft 安装体验的应用程序(如 MSIs),使用它来处理设置环境变量、处理依赖关系和清理等工作是值得的,这比尝试记住手动安装步骤来反转要好得多。安装 Chocolatey 的过程在 chocolatey.org/install 上有明确的说明。
需要使用以管理员身份运行的 cmd 命令行(在开始菜单中的 cmd 选项上右键单击以获取管理员选项)来执行这些步骤。在 cmd 命令行中,我们需要使用以下命令安装 minikube 的核心:
choco install minikube
接下来,我们需要 Kubernetes 命令行界面(CLI),称为 kubectl,可以使用以下命令进行安装:
choco install kubernetes-cli
这两个步骤已经将 minikube 建立到环境中。由于 minikube 在 Windows 主机上使用 Hyper-V,您可能希望在 Hyper-V 中设置一个额外的虚拟网络,以避免与现有任何其他虚拟化设置发生网络冲突。为此,可以从开始菜单或使用命令行通过以下命令启动 Hyper-V 管理器:
Virtmgmt.msc
我们需要在 Hyper-V 管理器 UI 的右侧单击虚拟交换机管理器选项,如图 A.5 所示。

图 A.5 带有虚拟交换机管理器选项的 Hyper-V 管理 UI
当虚拟交换机管理器 UI 显示时,我们使用列表顶部的“新建虚拟网络交换机”选项来创建一个新的交换机。新的交换机设置显示在右侧。交换机需要设置为“内部”选项。一旦创建名为“主虚拟交换机”的交换机(名称至关重要,因为当您创建 minikube 集群时,我们将引用此名称),配置应类似于图 A.6 中显示的详细信息。

图 A.6 虚拟交换机配置
Linux 版本的 Minikube
对于 Linux,操作系统需要安装适当的虚拟机管理程序,例如 KVM(基于内核的虚拟机)。由于这个过程可能因操作系统版本的不同而有所变化,我们建议您查阅您的操作系统文档以获取详细信息。以下描述了 Ubuntu 的步骤,这些步骤适用于许多 Linux 版本。
首先,我们需要检查硬件是否支持虚拟化;这可以通过以下命令完成:
egrep -c '(vmx|svm)' /proc/cpuinfo
结果为 1 或更大表示计算机硬件可以支持虚拟化。你可能希望通过检查操作系统是 32 位还是 64 位以及有多少资源可用来确定主机是否能够良好运行。我们将假设这不是问题。下一步是安装虚拟化功能。这些功能在 Linux 版本之间可能略有不同,但对于 Ubuntu 18.10 或更高版本,命令是
sudo apt-get install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils
下一步是确保你有使用虚拟化的权限;这是通过以下命令完成的:
sudo adduser 'id -un' libvirtd
sudo adduser 'id -un' kvm
每个这些命令都将确认你的用户添加。下一步是再次登录以使更改生效。可以使用以下命令检查安装和配置,这些命令将指示能够连接到虚拟机管理程序并访问libvirt-sock:
virsh list --all
sudo ls -la /var/run/libvirt/libvirt-sock
我们需要确保文件夹/dev/kvm的所有权和权限正确。这是通过以下命令完成的
sudo chown root:libvirtd /dev/kvm
再次,需要登录循环才能使更改生效。最后一步取决于你是否想使用虚拟化 GUI;这可以通过以下命令安装
sudo apt-get install virt-manager
在虚拟化成功设置之后,这个过程变得稍微简单一些,因为它只使用 Docker,并且不会创建额外的虚拟化层。第一步是使用curl获取 Debian 软件包,然后使用以下命令安装下载的软件包
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.debsudo dpkg -i minikube_latest_amd64.deb
我们需要安装conntrack服务并确保安装了支持 Linux OS 内部连接管理的apt-transport-housings软件包。这可以通过以下命令完成
sudo apt-get install conntrack
sudo apt-get install apt-transport-https
将 Kubernetes 键命令行工具以kubectl的形式拥有是有帮助的。我们可以使用以下命令安装它:
curl -LO "https://dl.k8s.io/release/$(curl -L -s \
https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
最后一步是安装 minikube。这可以通过以下命令完成:
wget https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo cp minikube-linux-amd64 /usr/local/bin/minikube
sudo chmod +x /usr/local/bin/minikube
我们应该可以使用以下命令验证 minikube 的安装:
minikube version
A.9 支持 Ruby 开发库和工具
对于第八章,其中我们实现自定义插件,我们建议安装额外的 RubyGems 以帮助开发。可以使用以下命令添加 RubyGems
gem install <name of ruby gem>
你只需要将<name of ruby gem>替换为以下推荐添加的内容:
-
test-unit -
ruby-lint
如果你想了解更多关于这些工具的信息,链接在附录 E 中提供,以及所有其他有用的资源信息。
A.10 Redis
第八章使用了 Redis 来演示自定义插件的开发。它是领先的内存缓存解决方案之一。根据我们的需求,安装是最小的。Redis 可以在 Linux 平台上以几种不同的方式安装:
-
按照 Redis 快速入门指南(
redis.io/topics/quickstart),它将带你通过下载源代码并运行 make 工具生成合适的可执行文件的过程。 -
或者按照
redis.io/download中的步骤获取包管理器解决方案。在两种情况下,您都希望使用最新的稳定版本。作为替代,Redis 可以使用 apt-get 包管理器(例如 Ubuntu)或 Snapcraft (snapcraft.io/) 安装。
为了让 Windows 采用这种方法,您需要使用带有 WSL 的 Windows 10 Pro。可以通过 github.com/ServiceStack/redis-windows 获取一个纯 Windows 版本。还有一个流行的社区版本可在 github.com/dmajkic/redis/downloads 获取;虽然这个版本非常陈旧,但它具有满足我们需求的特性。另一种选择是运行预打包的 Docker 解决方案,这将使一些必要的步骤变得更加复杂,因为我们需要使用 Redis CLI。
一旦下载并构建/安装,您需要将二进制文件的路径添加到您的路径中,如 A.2 节中所述。这应该会同时获取服务器和 CLI。
A.10.1 Redis gem
除了核心 Redis 服务器外,我们还需要一个可以与 Redis 通信的 Ruby 库。这可以通过以下命令快速获取和安装:
gem install Redis
这将下载并安装最新的稳定 gem 到您的 gem 库中,以便使用。如果您想了解更多信息,这个 gem 的文档可以在 github.com/redis/redis-rb 找到。
A.11 Python
在第十一章,为了说明如何使用默认适配器,我们使用了 Python。Python 的安装取决于您的操作系统。对于 Windows,最直接的方法是从 Python.org 下载 MSI 文件。对于 Linux,使用操作系统包管理器将是安装的最佳方法。
当涉及到使用 Python 2.x 或 3.x 时,许多人仍在使用 Python 2.x,尽管它已经达到了其生命周期的终点。我们提倡采用 Python 3;我们已经检查了使用这两个版本的小型应用程序。对于 Windows,我们建议使用 Python Switcher 工具(更多详情请见 mng.bz/Nx51)来允许在不同 Python 版本之间切换。对于 Linux,如果需要,最佳选择是利用 Alternatives 功能(Linux 通用描述可以在 www.lifewire.com/alternatives-linux-command-4091710 找到,Ubuntu 的详细说明可以在 mng.bz/DxDw 找到)。
在安装了 Python 之后,还需要两个额外的库,可以使用 Python 的包安装器(PIP)来安装,使用以下命令:
pip install pyyaml
pip install fluent-logger
根据您查看文件的首选方式,您可能希望为您的首选 IDE 安装或添加一个插件,以便更容易地处理 Python 和 YAML 文件。
A.12 密钥库
在本书中,我们提到了 HashiCorp 的 Vault 的使用。虽然这不是必需的,但您可以考虑尝试使用 Fluentd 来使用它。HashiCorp 制作了一份出色的安装指南,将整个过程分解为几个相当简单的步骤。详细信息可以在 mng.bz/laJ8 找到。由于步骤相对简单,我们将在这里总结它们,如果您需要更多信息,请参考 Vault 教程中的开发模式设置部分:
-
从 www.vaultproject.io/downloads 下载适用于您的操作系统的正确二进制文件。
-
将 zip 文件解压到合适的位置(例如,
c:\vault或/usr/bin/vault)。 -
将安装位置添加到
PATH环境变量中(参见章节 A.2)。 -
运行命令
vault -install-autocomplete;这将添加它能够添加到您环境中的任何命令行实用程序。 -
使用命令
vault server-dev以开发模式启动 Vault 服务器。请注意,命令执行开始附近输出的内容将在控制台上显示一个密钥和令牌。您需要记住这些信息,因为根令牌将在一分钟内需要。 -
使用 Windows 的 set 命令或 Linux 的 export 命令设置两个环境变量:
-
VAULT_ADDR=http://127.0.0.1:8200 -
VAULT_TOKEN=<Root Token>
-
完成这些步骤后,您可以使用命令 vault status 验证设置的状态。请注意,每次 Vault 重新启动时,令牌都会更改,并且所持有的秘密将为空。这种行为发生是因为 Vault 正在以开发模式运行,其中开发模式是非持久的。
附录 B.处理时间和日期、正则表达式以及其他配置值
B.1 表达相对时间
Fluentd 的一些配置属性要求我们以相对方式表达时间(即,从现在起有多长时间)。以可读形式表示这些值最好使用简单的整数和时间类型——秒、分钟和小时。以下表格显示了在 Fluentd 中如何实现这一点。
| Interval | 字符 | 示例 |
|---|---|---|
| Seconds | s | 10s → 10 秒 0.1s → 100 毫秒 |
| Minutes | m | 1m → 1 分钟 0.25m → 15 秒 |
| Hours | h | 24h → 24 小时 0.25h → 15 分钟 |
| Days | d | 1d → 1 天 0.5d → 12 小时 |
B.2 表达日期和时间
输入和输出的日期表示由 Ruby 如何解析自定义格式的日期时间驱动。以下表格显示了字符代码到它们解析的值的映射。在几个案例中,相同字符的大小写表示相同值的截断和完整版本(例如,a% 和 %A)。小写形式表示截断格式。我们将这些案例放在同一行中,以便更容易发现。
| Code | 日期元素描述 | 示例 |
|---|---|---|
%a %A |
一周中天数的三位字母缩写。一周中天数的完整名称。 | Mon 星期一 |
%b 或 %h %B |
月份名称的缩写版本。完整月份名称。 | Feb 二月 |
%c |
本地默认表示法的快捷方式。 | %a %b %e %T %Y 精确格式由区域设置驱动。 |
%C |
以数字形式表示的世纪。2021 年将结果为 20(Ruby 通过将年份除以 100 并向下取整来得到这个值)。 | 2021 |
%d |
月份中的天数,以两位数表示 01 ... 31。 | 06 |
%D |
%m/%d/%y(月/日/年)的快捷方式。 |
01/01/21 |
%e |
月份中的天数,但小于 10 的天数以一位数表示。 | 6 |
%F |
%Y-%m-%d 的快捷方式,与 ISO 8601 格式对齐(有关 ISO 格式标准的更多信息,请参阅 www.iso.org/iso-8601-date-and-time-format.html)。通常记录为 YYYY-MM-DD。 |
2021-12-31 |
%H |
使用 24 小时制表示一天中的小时,以两位数形式表示。 | 02 |
%I (大写 I) |
使用两位数 12 小时制表示一天中的小时,因此 01 可以是上午 1 点或下午 1 点。另一种形式是 %l(小写 L)。 |
01 |
%j |
从 001 开始的一年中的天数的三位数表示。 | 211 |
%k |
一天中的小时(24 小时制 0 ... 23);个位数表示上午小时 | 3 |
%l(小写 L) |
使用一位数表示小时数小于 10,对于剩余的小时使用两位数。这使用 12 小时制,因此 1 可以是上午 1 点或下午 1 点。另一种形式是 %I。 |
1 |
%L |
时间中的毫秒部分,以三位数形式表示。 | 021 |
%m |
年份中的月份,以两位数形式表示。 | 08 |
%M |
小时中的分钟,以两位数形式表示。 | 09 |
%N 或 %9N 或 %6N 或 %3N |
秒的分数以纳秒(9 位)表示。秒的分数以微秒(6 位)表示。秒的分数以毫秒(3 位)表示。 | 012345678012345012 |
%p 或 %P |
子午线指示符:上午或下午(大写)。子午线指示符:am 或 pm(小写)。 | PMp.m. |
%r |
使用 12 小时制的时钟表示时间的快捷方式——等同于使用 %I:%M:%S %p。 |
01:02:06 PM |
%R |
使用 24 小时制的时钟表示时间的快捷方式——等同于 %H:%M。 |
15:01 |
%s |
自纪元(即 1970-01-01 00:00:00 UTC)以来的秒数;也称为 POSIX 时间或 Unix 时间。 | 1588115129 |
%S |
分中的秒数,以两位数字表示。 | 05 |
%T |
使用 24 小时制的时钟表示时间的快捷方式——到秒的精度使用 24 小时制。 | 01:59:11 |
%u |
周的星期几的数字表示形式,星期一为 1。 | 7 |
%U |
当前年份的周数,以两位数字格式表示,第一个星期天算作第一周的第一天:00 . . . 53 | 52 |
%V |
根据 ISO 8601 定义的周数( %U 的替代)。 |
01 |
%W |
从第一个星期一开始计算的周数。 | 00 |
%w |
周的星期几的另一种数字表示形式,其中第一天是星期日(0)。 | 6 |
%x |
%D 的别名。 |
01/01/21 |
%X |
%T 的别名。 |
01:59:11 |
%y 或 %Y |
年份以两位数字表示(即省略世纪)。包括世纪的年份。 | 212021 |
%z |
时区偏移量以相对于 UTC 的正负四位数字表示。例如,纽约比 UTC 晚 4 小时,因此是 -0400。前两位数字代表小时;第二位代表分钟。例如,澳大利亚中央标准时间比 UTC 快 9.5 小时。 | +0930 |
%Z |
时间区以名称表示。可以找到完整的时间区和它们的代码列表,请参阅 www.timeanddate.com/time/zones/。 | BST |
B.3 表达大小
一些属性允许您以字节为单位表达数据大小,最高可达太字节。然而,我们不推荐您使用太字节大小。
| 大小 | 字符 | 示例 |
|---|---|---|
| 字节 | - | 100 → 100 字节 |
| 千字节 | k | 12k → 12 千字节 (Kb) |
| 兆字节 | m | 5m → 5 兆字节 (MB) |
| 吉字节 | g | 3h → 3 吉字节 (GB) |
| 太字节 | T | 1t → 1 太字节 (TB) |
B.4 正则表达式
正则表达式是我们处理文本字符串以查找特定模式或将字符串分解成部分的一种常用方法。不幸的是,正则表达式(通常简称为 REGEX)在不同的实现之间可能略有差异。为了参考,以下部分突出了 Ruby(以及 Fluentd 使用 Ruby 实现的方式)和 Fluentd 中实现的正则表达式的最有用方面。
B.4.1 转义码
转义码提供了使用预定义字符组的手段。
| 正则表达式代码 | 说明 |
|---|---|
. |
任何字符(除非在解析器中启用了多行选项,则不包括换行符) |
\d |
一个数字字符 ([0-9]) |
\D |
一个非数字字符 ([⁰-9]) |
\h |
一个十六进制数字字符 ([0-9a-fA-F]) |
\H |
一个非十六进制字符 ([⁰-9a-fA-F]) |
\s |
一个空白字符(即空格、制表符、回车、换行、垂直制表符或换页符) |
\S |
一个非空白字符(即 ^ \t\r\n\f\v 的逆) |
\w |
一个单词字符 ([a-zA-Z0-9_]) |
\W |
任何非单词字符(即 \w 的逆) |
B.4.2 重复/选择
这些字符允许我们定义不同的重复模式并定义可接受值的范围。
| 正则表达式代码 | 说明 |
|---|---|
* |
零次或多次 |
? |
零次或一次(可选) |
[n,m] |
定义接受的值的选项 |
[n..m] |
定义值范围——内容受 ASCII 编码影响,可以通过重复 n . . . m 包含多个范围(例如,[a..zA..Z0..9]) |
^ |
在范围内 |
{,m} |
m 次或更少 |
{n,} |
n 次或更多 |
{n,m} |
至少 n 次且最多 m 次 |
{n} |
精确 n 次 |
+ |
一次或多次 |
B.4.3 锚点、组和备选方案
锚点是元字符,用于匹配字符之间的零宽度位置,锚定匹配到特定位置。组允许我们定义原子分组,这可以看作是数学中括号的使用。
| 正则表达式代码 | 说明 |
|---|---|
| ` | 正则表达式代码 |
| --- | --- |
| 匹配行的结束。 | |
( |
开始分组。 |
(? |
开始一个不捕获内容的组。 |
(?<NAME> |
使用名称定义捕获组。NAME 被替换为选择的名称。 |
) |
分组的结束,无论是捕获组还是非捕获组。 |
\A |
匹配字符串的开始。 |
\G |
匹配定义字符的第一个出现。 |
\k<NAME> |
允许命名组进行回引用(即,一旦定义即可引用)。 |
\Z |
匹配字符串的结束。如果行的结束是一个换行符,则忽略。 |
\z |
匹配字符串的结束。 |
^ |
匹配行的开始。 |
| |
分隔两个值或表达式,可以像这样或这样处理——例如,对于 (a|b),值可以是 a 或 b。 |
B.5 Docker 标签定制
在 Docker 日志驱动程序的 log-opts 配置中,可以定制其输出的标签元素。这是通过将 log-opts 的标签部分设置为等于使用预定义模板标记的配置字符串来完成的,如下表所示。
| 标记 | 描述 |
|---|---|
{{.ID}} |
容器 ID 的前 12 个字符 |
{{.FullID}} |
完整的 64 位容器 ID |
{{.Name}} |
启动时的容器名称 |
{{.ImageID}} |
镜像 ID 的前 12 个字符 |
{{.ImageFullID}} |
镜像的完整 ID |
{{.ImageName}} |
容器使用的镜像名称 |
{{.DaemonName}} |
运行容器的软件守护进程名称(例如,Docker) |
使用这张表格,如果我们提供了额外的配置,例如 –log-opt tag="{{.ID}}-{{.ImageID}}",那么标签看起来可能就像 "myid12xxeerr-mynamefluent" 这样,连字符来自我们在配置中指定的两个标签部分的分隔。
附录 C. 插件总结
我们在书中探讨了多种插件类型,但并未涵盖所有开箱即用的插件。本附录将对此进行说明。我们提供了一份所有核心插件和一些值得关注的开源插件的总结。
C.1 格式化器插件
| 插件名称 | 概述 | Fluentd 核心 |
|---|---|---|
csv |
在第四章中有介绍。基本的逗号分隔值,尽管可以通过配置属性更改分隔符。 | 是 |
hash |
这将日志事件记录转换为 Ruby 可以处理的哈希格式表示。可以在某些插件配置中嵌入 Ruby 代码片段,因此它被包含在内。 | 是 |
json |
在第四章中有介绍。允许我们将日志事件内容以 JSON 格式输出。有关 JSON 格式的更多信息,请参阅 www.json.org。 | 是 |
ltsv |
在第四章中有介绍。标签制表符(字符)分隔值——而不是依赖于列表中的位置来获取正确的值含义,我们可以包含一个标签。更多信息请参阅 ltsv.org/。 |
是 |
msgpack |
在第四章中有介绍。一个理想的格式化器,用于压缩日志事件以在 Fluentd 和 Fluent Bit 之间通信。有关 msgpack 的更多信息,请参阅 msgpack.org/。 |
是 |
out_file |
在第四章中有介绍。这个格式化器会打印出命名的日志事件属性,可以使用分隔符进行列表。 | 是 |
single_value |
single_value 格式化器有点类似于 CSV 格式化器,因为它可以从日志事件记录中选择内容并输出。然而,在这种情况下,只能通过使用 message_key 属性来识别日志事件记录的一部分。 |
是 |
C.2 提取和注入插件支持
开箱即用的来源和匹配插件支持提取和注入如下:
| 来源 | 注入 | 提取 |
|---|---|---|
dummy |
否 | 否 |
exec |
否 | 是 |
forward |
否 | 否 |
http |
否 | 否 |
monitor_agent |
否 | 否 |
syslog |
否 | 否 |
tail |
否 | 否 |
tcp |
否 | 是 |
udp |
否 | 是 |
unix |
否 | 否 |
windows_eventlog |
否 | 否 |
| 匹配 | 注入 | 提取 |
copy |
是 | 否 |
exec |
否 | 否 |
exec_filter |
是 | 是 |
file |
是 | 否 |
forward |
否 | 否 |
http |
否 | 否 |
null |
否 | 否 |
relabel |
否 | 否 |
round_robin |
否 | 否 |
secondary_file |
否 | 否 |
stdout |
是 | 否 |
C.3 过滤器插件
| 插件名称 | 概述 | Fluentd 核心 |
|---|---|---|
add |
很有趣,因为它提供了一种无需努力即可添加 UUID(通用唯一标识符)和其他附加名称值对的方法。唯一标识符在日志事件通过多个 Fluentd 节点时非常有用。更多信息可以在 github.com/yu-yamada/fluent-plugin-add 找到。 |
否 |
anonymizer |
匿名化器可以配置为使用所选算法对日志事件中元素的 内容进行单向哈希。这对于屏蔽敏感数据非常理想。更多信息可以在 github.com/y-ken/fluent-plugin-anonymizer 找到。 |
N |
autotype |
根据分析有效载荷对日志事件属性应用类型。对于处理数值,因为它消除了手动类型转换的需要,所以非常适合。 | N |
filter_parser |
结合了解析插件的功能和过滤器。 | Y |
fluent-plugin-fields-autotype |
此插件非常适合解析日志事件结构并选择正确的数据类型来评估属性。这也是 fluent-plugin-auto-typ 的一个变体。更多信息可以在 mng.bz/AxlK. 找到。 |
N |
| geoip | 此插件利用了互联网服务提供商发布它们分配的互联网协议(IP)地址以及这些 IP 连接到的物理位置的事实。这些信息随后由几个组织汇总。通过知道请求的 IP,可以查找位置。这很有用,因为它允许数据更有效地路由和过滤。例如,您可能有一个全球的 Fluentd 服务器网络。使用 GeoIP 将使我们能够将日志定向到最近的 Fluentd 服务器以聚合日志事件。这在处理分布式用例中的大量日志时非常有帮助,例如
-
物联网(IoT)部署
-
全球多区域和多云解决方案
更多信息可以在 github.com/y-ken/fluent-plugin-geoip 找到。 | N |
grep |
提供了定义关于日志事件属性规则的手段,以将它们从事件流中过滤出来。可以指定多个表达式以创建累积规则。 | Y |
|---|---|---|
record_modifier |
record_transformer 的一个变体已被修改以提高插件效率:mng.bz/ZzoO。 |
N |
record_transformer |
最复杂的内置过滤器,提供了一组用于操作日志事件的选项。 | Y |
stdout |
将所有事件发送到 stdout,而不从流中删除事件。 |
Y |
C.4 标签操作插件
| 插件名称 | 概述 | Fluentd 核心库 |
|---|---|---|
rewritemng.bz/2jad |
这使得可以使用一个或多个规则修改标签,例如如果日志事件记录的属性与正则表达式匹配。因此,根据日志事件执行特定任务变得非常容易。 | N |
rewrite-tag-filtermng.bz/1jMV |
匹配指令中有一个或多个规则时,将对日志事件应用正则表达式。然后,根据结果,将标签更改为指定的值。规则可以设置为选择是否将重写应用于正则表达式的真或假结果。如果成功,则使用新标签重新发出日志事件以继续匹配事件之后。 | N |
routemng.bz/PWx9 |
路由插件允许标签将日志事件定向到一个或多个操作,例如操作日志事件并复制它以通过另一个指令拦截。 | N |
C.5 防止警报风暴
如果生成连续的错误流,一些资源可以帮助控制或防止可能的警报风暴。
| 插件名称 | 摘要 |
|---|---|
日志抑制过滤器mng.bz/J1l0 |
如果在特定时间段内重复出现超过定义次数,Fluentd 插件将基于命名的日志事件属性保留之前日志条目的列表。 |
Log4J2 插件mng.bz/wnPq |
在 Log4J2 框架内抑制日志事件,以防止发出过多匹配一组规则的日志事件。 |
Log4Netmng.bz/7W19 |
Log4J2 解决方案的 .Net 变体。 |
C.6 分析和指标插件
一些插件可以帮助在 Fluentd 中创建分析或指标值,或使用工具帮助生成指标。
| 插件名称 | 摘要 |
|---|
| Fluent-plugin-prometheusmng.bz/mxJr | 将六个插件捆绑在一起,包括以下内容:
-
prometheus输入插件提供了一个指标 HTTP 端点,由 Prometheus 服务器在 24231/tcp(默认)上抓取。 -
prometheus_input插件收集 Fluentd 中的内部指标,有点像监控代理。 -
prometheus_output_monitor插件收集 Fluentd 中输出插件的内部指标。 -
prometheus_tail_monitor插件收集 Fluentd 中尾插件的内部指标。这使我们能够确保尾插件按预期运行。 -
输出和过滤器插件使用额外的指标来装备日志事件,例如基于配置属性的值出现的次数。
|
Fluentd Elasticsearch 插件mng.bz/5K1B |
将日志事件发送到 Elasticsearch 的插件。该插件提供了一套非常丰富的配置属性。 |
|---|
| Fluentd 数据计数器mng.bz/6Z1o | 计算在指定属性中匹配任何指定正则表达式模式的日志事件:
-
每分钟/每小时/每天的计数
-
每秒计数(平均每分钟/每小时/每天)
-
每种模式在消息总数中的百分比
DataCounterOutput 发出包含结果数据的消息,因此您可以将这些消息(默认情况下带有 datacount 标签)输出到您想要的任何输出。|
| Fluentd 数字计数器mng.bz/oaJd | 插件用于计算匹配数字范围模式的日志事件,并输出其结果(类似于fluent-plugin-datacounter):
-
每分钟/每小时/每天的计数
-
每秒计数(平均每分钟/每小时/每天)
-
每个数字模式在日志事件总数中的百分比
|
C.7 插件接口
以下表格总结了在开发自己的插件时可以或应该实现的功能。您可以通过查看 Fluentd 代码以及 https://github.com/fluent/fluentd/blob/master/lib/fluent/plugin 来查看详细信息,但我们希望这些表格将使生活更加容易。
输入
| 函数原型 | 描述 |
|---|---|
def emit |
这为我们的需求重载了 Fluent::Input 类的方法。 |
def run |
设置循环线程,如果没有要输出的内容,则进入睡眠状态。 |
def configure(conf) |
此方法用于处理配置值,以便它们可以被验证,特别是如果值可能冲突。 |
def start |
一个生命周期事件,用于开始创建连接。启动计时器以推动逻辑查看是否需要任何 I/O。 |
def shutdown |
开始释放资产的过程,例如释放到资源的连接。 |
输出
| 函数原型 | 描述 |
|---|---|
commit_write(chunk_id) |
一旦一个块可以写入,此方法告诉我们的实现哪个块 ID 要写入。返回到 Fluentd 后,该块将被清除。 |
def configure(conf) |
此方法用于处理配置值,以便它们可以被验证,特别是如果值可能冲突。 |
def format(tag, time, record) |
将代码序列化以存储在块中。tag、time和record代表日志事件的三个部分,时间是从纪元以来的秒数。此阶段的记录是一个哈希表,允许 JSON 操作。如果没有重载,将抛出NotImplemented错误。 |
def formatted_to_msgpack_binary? |
用于指示自定义格式方法(#format),返回 msgpack 到二进制或否。如果#format 返回 msgpack 二进制,则覆盖此方法以返回true。默认情况下,它返回false。 |
def multi_workers_ready? |
False |
| def prefer_buffered_processing | 仅当以下所有条件都为真时,覆盖此方法以返回false:
-
该插件提供了缓冲和非缓冲方法的实现。
-
如果没有指定
<buffer>部分为true,则插件预期作为非缓冲插件工作。
|
def prefer_delayed_commit |
覆盖此方法以决定使用write还是try_write,如果两者都实现了。 |
|---|---|
def shutdown |
关闭操作是启动事件的相反。在这个阶段,我们应该释放资源,例如网络连接。 |
def start |
这是重要的生命周期事件之一;我们应该在收到此事件时开始消费或允许发送事件。 |
def try_write(chunk) |
此方法用于实现异步 I/O。如果未重载,基类将抛出 NotImplemented 错误,但预期需要实现。 |
def write(chunk) |
此方法接收一个块并将其同步写入。如果未重载,基类将抛出 NotImplemented 错误,但预期可以工作。 |
extract_placeholders(str, chunk) |
用于使用块信息从 str 中提取一个字符串值。返回一个字符串。 |
process(tag, es) |
此方法用于同步输出。如果没有重载,将抛出 NotImplemented 错误。tag 是应用于 es 结构中所有事件的标签。es 参数表示一个或多个要写入的日志事件。 |
rollback_write(chunk_id) |
插件可以使用此方法控制块写入的回滚和重试。 |
附录 D. 现实世界用例
D.1 在现实世界用例中使用 Fluentd
在整本书中,我们专注于解释如何使用 Fluentd。解释主要集中在特定领域,而不是提供整体视角。本附录试图纠正这一点,通过提供一个更全面的图景。
本附录选取了几个类似的现实世界场景并将它们融合在一起,以避免泄露任何客户信息。将这些案例结合起来,我们还能够融入更多的经验教训。我们不会深入具体配置细节,因为它们已在本书的核心部分有所涵盖,但这应该有助于您理解我们为什么重视某些功能。
D.2 设置场景
我们的组织大约 30 年前作为一个单一国家的零售商开始,并迅速增长。这种增长是由以下因素驱动的:
-
获得自己的制造能力,因为其产品针对每个客户都是定制的。
-
扩大其零售范围至反映市场变化的关联产品线。
-
在商业上,零售单位作为独立的企业建立,要么由主要公司的子公司全资拥有,要么与门店经理共同拥有;还有一些传统的特许经营商。这种结构赋予了商店一定程度的自主性和个人责任。这意味着独立零售商可以成为集团的一部分,外部投资(共同拥有或特许经营权的股份)有助于增长。
-
技术采用是由简化流程和因此提高零售吞吐量的目标所驱动的。这意味着移动设备采用的增长,以及与基于互联网的销售渠道的缓慢互动。
-
在过去 20 年中,国际扩张在欧洲、亚太地区(包括中国)和北美进行。
在内部,组织在地理业务和运营方面是分割的。
-
零售
-
制造和分销
-
企业(涵盖营销、会计、外部供应链采购等)
一个中央 IT 部门支持企业系统、在区域或制造站点级别使用的核心解决方案,以及零售 IT 解决方案。尽管如此,垂直部门所拥有的自主性创造了存在影子或灰色 IT 的条件。这一点进一步由区域运营拥有自己的与区域业务对齐的 IT 团队而加剧。企业总部和主要的 IT 运营,包括软件开发,位于欧洲。
一些 IT 解决方案反映了组织的年龄和增长;一些企业解决方案已经经历了多年的累积扩展、修补和定制,显然已经过时且脆弱。这导致了人们不愿进行变革,并且已经启动了一些重大计划,用商业现货(COTS)或 SaaS 方法来替换一些遗留系统。影子 IT 导致了本地 SaaS 和“集成”的局部涌现。欧洲 IT 主要由其自身的本地数据中心主导。然而,较新的地理区域更快速地采用云计算,而不是寻求为运营 IT 和支持中心投入资本。目前,零售是一个本地化方案,因为广域网通信被认为不够稳健,无法允许零售地点在出现故障时继续运营。在世界的某些部分,商店的网络质量、可靠性和带宽不足以支持远程托管解决方案的所有需求。
除了商业现货(COTS)之外,内部开发的零售和制造解决方案通常包含企业知识产权或简化业务流程。组织在制造和零售空间中看到了越来越多的互联设备。这种互联性要么是由于自然的信息技术演变,要么是因为组织委托第三方构建解决方案或对标准商业产品进行独特的定制化,这些产品通过平板电脑提供。
大部分系统集成是通过消息传递、共享数据库或 FTP 实现的。零售和制造能力最依赖于消息传递,因为商店订单到制造的时间敏感性强,反映了动态库存分配(这些解决方案是定制构建的)。
软件变更的推出往往比较缓慢,这通常源于对脆弱的遗留解决方案的担忧。在非欧洲地理区域外的延误,由于对软件变更和运营支持挑战缺乏理解,使得人们抵制变革,这一问题变得更加严重。讽刺的是,这反而加剧了问题,因为来自欧洲的压力要求推出升级,意味着推出往往涉及大量的变更。
现有的运营和监控成熟度取决于地理位置,欧洲是最成熟和发达的。然而,监控主要是基础设施层面的参与。应用层面的监控较弱。业务层面的监控是通过经典的零售和供应链报告指标(如销售额和订单)而不是从更现代的视角来衡量任务在其流程的不同阶段。
一些问题来自地理和市场领导力(市场份额、创新、法规变化)的持续增长驱动。有时,这些压力并不有助于跟上非功能性考虑因素,因为它们随着增长和技术债务而演变。这对许多成熟组织来说并不罕见,但嵌入到组织 DNA 中的持续增长增加了压力。IT 动量通常通过新的功能性能力来衡量,当然在解决债务直到债务影响交付时也会带来一些挑战。
正如你所见,这不是一个云原生组织,但它开始摸索进入云,需要了解如何解决监控,无论是本地还是处理云。
D.2.1 运营挑战
值得关注组织在运营中遇到的一些问题。解决这些问题将通过监控改进的交付带来业务价值,包括采用 Fluentd。
核心系统的第三线和第四线支持来自欧洲的 IT 发展,这些地区不提供 24/7 的支持。在不同时区工作的区域运营必须为自己的区域零售提供自己的支持。中央支持通常归结为组织政治和员工提供加班支持的好意。
欧洲开发,能够有效地使用基本的 DevOps 实践支持本地地理,但对其他地区的需求认识不足。DevOps 往往是避免写下来的原因,因此本地理解很大程度上是通过口头和协作行为。不同的地理区域不是开发过程的积极参与者,也没有从协作知识转移中受益,这意味着他们缺乏对内部解决方案的了解。这导致了人们以他们未预期的方式使用解决方案的问题。快速而简陋的流程来恢复问题,导致对运营失败缺乏洞察力(没有保留日志或环境镜像/快照),这使得欧洲 IT 难以调查和制定预防措施。
D.3 引入监控
IT 系统有效地形成了一系列从店铺到区域运营中心和从区域中心到企业中心的星形结构。这可以在图 D.1 中看到。制造和分销也直接连接到中心和全球中心。这使其适合采用监控作为一系列集中器网络。这也非常适合,因为大多数中心都有最大量的事件/交易通过,这意味着这些区域的问题具有最大的商业影响。

图 D.1 商业中的关键“参与者”及其在组织内(以及信息流)的关系
第一个目标是让区域中心系统聚合和统一其日志数据。根据 80/20 原则,我们寻找标准技术和通用系统(通常是源自中央 IT 能力的系统)来创建标准化的日志事件捕获配置。还实施了简单的阈值警报。警报反映了重大阈值违规(在它们变得关键之前)并生成了日志事件。在通过日志调查问题时,我们可以看到历史上可能存在某种性能问题(通常是磁盘存储耗尽最为严重)。
在将基础设施日志与应用程序日志对齐本可以显示这些措施的情况下,想要结合基础设施监控导致了冲突的政治,因为基础设施团队认为他们的监控已经成熟。他们不是问题的来源。IaaS 活动没有看到基础设施团队参与的事实也没有帮助。
我们敦促开发团队编制异常情况清单,并使用错误代码作为构建更好的支持文档的第一步。这为区域团队提供了附加和共享他们自己的运营流程的途径。随着时间的推移,我们实现了 CI/CD 代码质量检查以检测错误代码的分配,并使采用游戏化。
在统一日志方面取得进展,我们更好地理解了不同部署中可能发生的情况——一些问题的警告信号已被识别,但直到太晚才被认识到。因此,通过使用分组电子邮件为众所周知的指标提供警报。
其他候选指标开始受到关注,这触发了收集更多诊断信息的脚本,例如消息队列深度,捕获对队列的最后访问时间。
随着对监控产生结果的信心增强,无论是本地运营团队还是中央 IT 资源都能更好地了解正在发生的事情,扩展监控的接受度得到了实施。将特定事件转发到中央 IT 团队也有助于确认当企业中心的数据分发过程成为问题来源时。通过检查日志以确定预期发生的事情,一些问题开始被识别。随着能力的整合,标志包括
-
清理工作没有按时进行。
-
软件部署并不总是遵循中央团队的建议。因此,当预期或需要时,自动化流程并没有触发。
-
手动清理工作并不始终得到一致执行,通常是因为额外的清理步骤通过电子邮件传递,没有达到每个人,或者没有作为更结构化的流程添加到文档中。
-
一些业务流程在不同地理区域比预期花费了更长的时间。结果,预演数据积累超过了预期,产生了问题。
-
当需要处理警告指示时,并未得到解决。
-
一些应用程序逻辑不够防御性,允许用户执行企业其他部分不期望的操作——例如,当通信基础设施假定此类数据只会是几个 Kb 大小时,产品图片的大小为 GB。
其中一些发现肯定需要非常小心地处理,因为错误和沟通问题为任何有组织议程的人提供了燃料,并且经常会发生大量的指责。
D.3.1 监控的扩展
在地理枢纽和实验室取得良好进展后,相同的做法被重复用于零售运营。这必须更加小心地进行,因为任何被认为会影响零售运营的事情都是一个敏感问题。一个主要挑战是向枢纽发送足够的信息以提供合适的价值,同时不消耗宝贵的带宽。
日志聚合主要集中在商店的服务器端解决方案上,而不是具有设备原生应用的移动平台。为了获得有意义的见解而理解原生应用最终成为一个与核心监控努力分离的问题。由于商店应用程序处理 PII 数据,商店服务器被紧密锁定,偶尔会出现错误的数据条目进入不应存在的 UI,导致 PII 数据偶尔出现在日志条目中。随后对进一步过滤日志事件的需求导致对日志事件应用数据掩码,甚至是在本地。
D.3.2 监控的精细化
随着运营洞察开始反馈给中央 IT 团队,一些开发团队从使用 Java 日志记录到文件转变为直接记录到 Fluentd 代理。在这些地方采用此方法时,我们在开发期间使用了一种配置,确保日志事件被写入文件而不是中央日志存储能力。日志集中化程度取决于是否愿意部署额外的软件以及由谁部署。这意味着开发者并没有将 Fluentd 的引入视为过程中的干扰,因为他们习惯于查看日志文件以帮助确认代码按预期工作。但这也有助于促进日志记录的改进,因为我们仍然可以设置过滤器来突出潜在问题,例如意外记录敏感数据项。
D.4 云困境
虽然云的影响在很大程度上是 SaaS 或 IaaS 的问题,但我们在成熟的 PaaS 中看到的挑战越来越多,是使用云原生服务,而不是“低代码”风格的 PaaS,例如 MuleSoft、Dell Boomi 和 Oracle 的集成云,这些是程度不同的封闭生态系统。AWS、Azure、Oracle 和 Google 都提供了云原生服务,为他们的 Kubernetes 服务提供了类似 Fluentd 的能力。因此,当使用 AWS 的 Kubernetes 或 Oracle 的区块链时,我们应该使用供应商的实现,还是明确实例化 Fluentd 并配置 Kubernetes 使用我们的 Fluentd,而不是开箱即用的日志和监控?我们可以在图 D.2 中清楚地看到这一点,其中使用云主机自然解决方案利用云原生技术和云主机对齐的方法。

图 D.2 这说明了使用云原生解决方案进行日志记录的云中立方法(顶部)与特定供应商模型之间的差异,在特定供应商模型中,所有日志事件都将进入由云服务提供商处理的透明监控层,而在云中立方法中,你需要在基础设施即代码中适应网络需求。
这并不是一个容易回答的问题。如果我们使用提供的服务并从集成、自动扩展、自动修补等中受益,我们必须接受随之而来的供应商锁定。或者我们必须采取 IaaS 方法并自行部署 Fluentd,这意味着可以使用任何 IaaS 云服务提供商,但我们必须自行修补和扩展。我们已经决定通过几个问题来回答这个问题:
-
通过成为云服务提供商无关,你获得了哪些价值?
-
对于软件供应商来说,好处是显而易见的——在消费者基础方面的最大潜力。但现实是,你最终可能会将适配器集成到你支持最多的云中,因为这有利于客户。有关于 Zoom 能够快速切换云服务提供商以实现实质性节省的高知名度故事,以及 Dropbox 决定离开 AWS,因为他们达到了一个规模,这意味着私有数据中心提供了更好的经济性。
-
你是否可能需要多个云服务提供商或以混合方式运营?
-
对于真正全球性的解决方案,今天的现实是,你很可能会需要部署到多个云中。这仅仅是因为并非每个供应商在每个地理区域都有实质性存在。你可能会遇到特定的国家数据限制,这可能意味着使用特定的供应商(例如,撰写本文时,Azure 在东欧没有存在,而 Google 在非洲没有存在)。
-
此外,除非你是拥有深厚资金的大客户,否则你不会在数据中心解决方案中使用云,例如 AWS 的 Outpost 或 Oracle 的云客户,整个云平台部署到位于你数据中心的服务器机架上。这意味着存在多个实施和配置问题。这可能不是在中国运营的选项,因为许多 IT 供应商对提供的产品和地点有限制。
-
你的 IT 团队有多大,能力如何?
-
解决相同问题的方法越多变,IT 团队或部门需要的技能就越多。假设你的 IT 组织在人员方面需要非常精简。在这种情况下,一套标准的技术有很多优势。团队可以使用更小、更专注的工具集来发展他们的专业知识。更深入地掌握这些工具,并最大化投资。我们并不是说所有监控问题都可以通过 Fluentd 来解决,但为什么需要掌握三四个日志统一工具,如果有一个就足够了呢?
-
你的监控需求或策略是否可能让你进入专业领域?
-
对于那些可能需要大量调整或你正在推动标准工具可能性的边界的情况,云服务提供商的服务模式可能比解决方案造成更多问题。你将无法像完全控制代码和部署时那样,根据你独特的环境定制他们的平台。
探索这些问题无疑会揭示采用 Fluentd 或类似 ELK 堆栈是否是正确的选择。
D.5 解决方案
在区域枢纽级别达成的解决方案看起来与图 D.3 中的图表相似。企业监控看起来会很相似,尽管随着时间的推移,源的数量显著增加,因为监控已成为更标准的实践。

图 D.3 监控概述解决方案与日志事件生命周期的对比
仓库和制造中心会有一些相似之处,尽管比枢纽简单得多,因为它们没有使用容器化,并且正在运行的系统套件较小。
D.6 结论
希望你能从这一点看出,我们在书中涵盖的 Fluentd 技术中的几个帮助了一个面临挑战的组织实现价值。你会注意到,Fluentd 的应用并不局限于任何特定类型的平台或技术。虽然 Fluentd 受到 CNCF 的监管,但这反映的是它可以帮助解决现代云挑战的事实,而不是它专门为此背景设计。
从本课程中我们最大的收获之一是,实施监控能力的组织需要确保其文化和实施监控的方法已经准备好并能够接受必要的变革。虽然硬件监控被视为常态(毕竟,你已经投入了大量的资本,所以你希望确保它能够自我回报),但应用监控和日志的使用往往没有得到同样的重视。在形成这个例子的案例中,问题必须变得非常严重,才会在解决这些问题上做出重大承诺。
附录 E. 有用资源
E.1 有用的 Fluentd 资源
这些是与 Fluentd 直接相关并由 Fluentd 社区支持的资源。
| 名称 | 网址 | 描述 |
|---|---|---|
| Fluentd 官方文档 | docs.fluentd.org/ |
Fluentd 的官方 GitHub 文档 |
| Slack | slack.fluentd.org |
Fluentd 社区在 Slack 上 |
| Fluentd 的 Stack Overflow | stackoverflow.com/questions/tagged/fluentd |
与 Fluentd 相关的 Stack Overflow 内容 |
E.2 有用的 Fluentd 第三方工具
Fluentd 是用 Ruby 编写的,如果您正在编写 Fluentd 插件,这些资源将非常有帮助。
| 名称 | 网址 | 描述 |
|---|---|---|
| Fluentular | fluentular.herokuapp.com/ |
用于帮助验证 Fluentd 配置中的正则表达式的实用工具。 |
| Grok 解析器 | mng.bz/nYJa |
这使用基于 Grok 的方法从日志条目中提取详细信息。它包括多行支持。 |
| 微软的 Visual Studio Code | code.visualstudio.com/ |
通过插件框架支持大量语言和语法的免费 IDE。 |
| 多格式解析器 | mng.bz/vo17 |
尝试按照定义的顺序使用不同的格式模式以获得匹配。 |
| VS Code—Fluentd 插件 | github.com/msysyamamoto/vscode-fluentd |
在 Visual Studio Code 中搜索 msysyamamoto.vscode-fluentd。 |
| VS Code 插件的网站版本 | regexper.com/ |
提供正则表达式的可视化表示。有助于解决正确的分组等问题。 |
E.3 有用的日志记录实践资源
有效使用日志的关键是良好的日志记录。我们提供了许多关于推荐实践的见解。但如果您想了解他人的看法,那么这些资源可能会有所帮助。
| 描述 | 网址 |
|---|---|
| Logz.io 的日志最佳实践。 | logz.io/blog/logging-best-practices/ |
| Loggly 日志指南—涵盖多种语言视角。 | www.loggly.com/ultimate-guide/ |
| Loggly 的《实用日志手册》。 | mng.bz/4j1w |
| 国家标准与技术研究院(NIST)计算机安全日志管理指南 | mng.bz/QW8G |
| 这定义了 Syslog 的标准;除了帮助您更好地理解 Syslog 之外,它还包含了一些关于日志记录实践的好想法。 | tools.ietf.org/html/rfc5424 |
E.4 常见日志格式及描述
行业已经为日志文件结构开发了几个事实上的或正式化的行业标准。以下是对这些行业规范的参考。
| 日志格式 | 定义参考 |
|---|---|
| Apache HTTP 日志 | httpd.apache.org/docs/2.4/logs.html |
| 通用事件格式 (CEF) | mng.bz/XW5v |
| Graylog 扩展日志格式 (GELF) | mng.bz/y4QB |
| Nginx 日志 | mng.bz/M2BW |
| Syslog | tools.ietf.org/html/rfc5424 |
| Systemd 日志 | systemd.io/JOURNAL_FILE_FORMAT/ |
| W3C 扩展日志文件格式 (ELF) | www.w3.org/TR/WD-logfile.html |
| WinLoG(Windows 本地日志) | mng.bz/aD17 |
E.5 有用的 Ruby 资源
Fluentd 的主体是用 Ruby 构建的,如果你正在编写 Fluentd 插件,你可能需要这些资源。
| 名称 | URL | 描述 |
|---|---|---|
| 全局解释器锁 (GIL) 的解释 | thoughtbot.com/blog/untangling-ruby-threads |
Ruby 使用全局解释器锁,这影响了线程的管理方式以及如何调整 Fluentd。 |
| Gem 规范 | mng.bz/g4BV |
定义了打包自定义开发的插件所需的 gemspec 文件细节。 |
| Gemspec 与 Gemfile 的比较 | mng.bz/5Kwa |
解释了 Gemspec 和 Gemfile 之间的区别。 |
| Minitest——Ruby 单元测试框架 | github.com/seattlerb/minitest |
另一个流行的单元测试框架。此资源包括帮助区分它与其他常用框架的信息。 |
| Rake | github.com/ruby/rake |
提供了一个可以集成到 CI/CD 管道中的构建过程。 |
| rbenv | github.com/rbenv/rbenv |
这个开源工具使得确保不同的应用程序可以使用不同的 Ruby 版本进行工作变得更加容易。 |
| RDoc | github.com/ruby/rdoc |
标准的 Ruby-Doc 生成工具。如果你需要生成文档但不想使用 Yard 的扩展,那么 RDoc 是标准的。 |
| RSpec Ruby 单元测试框架 | rspec.info/ |
RSpec 是 Ruby 单元测试的替代品,它支持测试驱动开发 (TDD) 的开发方法。 |
| RuboCop | docs.rubocop.org/rubocop/ installation.html |
Ruby 的 Lint 工具。如果你在开发自定义插件,那么它是有价值的。 |
| Ruby API | rubyapi.org/ |
Ruby 文档工具。 |
| RubyGems | rubygems.org/ |
RubyGems 目录以及 RubyGems 管理器。 |
| RubyGuides | www.rubyguides.com | 一套使用 Ruby 实现特定功能的在线指南。 |
| Ruby in Twenty Minutes | www.ruby-lang.org/en/documentation/quickstart/ | 使用 Hello World 介绍 Ruby 的优秀入门指南。 |
| Ruby in Practice | www.manning.com/books/ruby-in-practice | Ruby in Practice 是来自 Manning Publications 的第二本书,采用不同的方法教授 Ruby 开发。 |
| RubyInstaller | rubyinstaller.org/ |
Ruby 的 Windows 安装程序。 |
| Ruby 语言规范和文档 | www.ruby-lang.org/en/documentation/ | 包括 Ruby 与其他语言(如 Java)不同的摘要页面。 |
| ruby-lint | rubygems.org/gems/ruby-lint/ |
在开发自定义插件时,lint 工具将帮助保持代码整洁,并且重要的是,帮助发现任何潜在的错误。 |
| Ruby 线程 | thoughtbot.com/blog/untangling-ruby -threads |
对 Ruby 线程的更详细探讨。 |
| Ruby 单元测试框架 | test-unit.github.io/ |
Fluentd 提供了额外的支持资源,使插件能够使用 test-unit 工具轻松进行测试。 |
| test-unit | github.com/test-unit/test-unit |
基于 xUnit 的 Ruby 单元测试解决方案。 |
| The Well-Grounded Rubyist, third edition | www.manning.com/books/the-well-grounded -rubyist-third-edition | Manning 关于 Ruby 开发的圣经。 |
| VSCode Ruby 插件 | marketplace.visualstudio.com/items?itemName=wingrunr21.vscode-ruby |
在 Microsoft 的 Visual Studio Code 中为 Ruby 提供语法感知高亮显示。 |
| YARD | yardoc.org/ |
符合 RubyDoc 规范的文档生成器,同时也支持标签符号,有助于提供更全面的元数据。 |
E.6 Docker 和 Kubernetes
Docker 是最常用的容器化技术,通常与 Kubernetes 一起使用。Kubernetes 和 CNCF 的采用对 Fluentd 的采用产生了重大影响。如果您想了解更多关于 Kubernetes 的信息,以下资源将有所帮助。
| 名称 | 网址 | 描述 |
|---|---|---|
| Docker | www.docker.com | Docker 生态系统的家园。 |
| Docker Hub | hub.docker.com/ |
Docker 镜像仓库,包括 Fluentd 镜像和本书使用的其他镜像。 |
| Visual Studio Code 的 Docker 插件 | mng.bz/6ZDA |
帮助理解 Docker 文件和文件语法的插件。 |
| Kubernetes 的家园 | kubernetes.io |
官方的 Kubernetes 网站。 |
| Kubernetes 日志(klog) | github.com/kubernetes/klog |
Kubernetes klog 组件的实现和文档。Klog 正在被 Kubernetes 采用作为默认的本地记录器。 |
| logrotate | github.com/logrotate/logrotate |
Logrotate 是一个开源工具,用于处理日志轮转。根据 Kubernetes 的配置方式,它被部署来管理 Kubernetes 的日志轮转。 |
| Kubernetes in Action 第二版 | mng.bz/oa1p |
Manning 的 Kubernetes 官方指南。 |
| Kubernetes 密钥 | mng.bz/nYW2 |
Kubernetes 与 pods 和容器凭证共享的方法。 |
| 开放容器 | opencontainers.org/ |
Docker 和其他几个容器开发组织支持的标准,用于标准化 Kubernetes 与容器交互的方式。 |
| Docker 的替代容器 | containerd.io/ cri-o.io/ |
几个替代的容器化开发倡议,包括来自英特尔、IBM、红帽和其他 Linux 供应商(如 SUSE)等组织的贡献。 |
| 部署工具 | rancher.com helm.sh |
将 pods 和配置部署到 Kubernetes 可能很复杂。已经开发出几种解决方案,如 Helm 和 Rancher,代表了这一领域的领先解决方案。 |
E.7 Elasticsearch
Elasticsearch 是日志聚合的常见目标之一。Elasticsearch 插件不是 Fluentd 开源部署的标准插件集的一部分(尽管它是预构建 Treasure Data 代理版本的一部分)。对于纯 Fluentd,需要安装代理。
| 名称 | URL | 描述 |
|---|---|---|
| Elasticsearch 堆栈 | www.elastic.co/elastic-stack | ELK 堆栈(Elasticsearch、Logstash、Kibana)的详细信息。虽然 Logstash 可以被视为 Fluentd 的竞争对手,但 Fluentd 通常与 Elasticsearch 和 Kibana 一起使用。 |
| Elasticvue | elasticvue.com |
用于查看 Elasticsearch 内容的 UI 工具。 |
| Fluentd Elasticsearch 插件 | docs.fluentd.org/output/elasticsearch |
允许您集成 Elasticsearch 的插件。 |
| Elasticsearch 插件源码 | mng.bz/von4 |
Fluentd Elasticsearch 插件的仓库。 |
| Manning, Elasticsearch in Action | www.manning.com/books/ elasticsearch-in-action | 虽然 Elasticsearch in Action 超出了我们所需了解的缓存知识,但这份 Redis 指南很有帮助。 |
E.8 Redis
Redis 可以被 Fluentd 用来提供内存缓存解决方案。以下资源将提供更多详细信息、下载等。
| 名称 | 网址 | 描述 |
|---|---|---|
| Redis 的家园 | redis.io/ |
提供 Redis 的基本文档和下载。除了商业产品外,我们还将使用 Redis 进行自定义插件开发。 |
| Manning, Redis in Action | www.manning.com/books/redis-in-action | 虽然 Redis in Action 超出了我们所需了解的缓存知识,但这份 Redis 指南很有帮助。 |
| Redis RubyGem | github.com/redis/redis-rb |
这是连接和使用 Redis 操作的 Ruby 库。我们将在构建自定义插件时使用它。 |
E.9 SSL/TLS 和安全
在分布式操作中使用 SSL/TLS 的能力是必不可少的。以下是一些关于使用 SSL/TLS 的有用链接。
| 名称 | 网址 | 描述 |
|---|---|---|
| 证书颁发机构 | jamielinux.com/docs/ openssl-certificate-authority/ |
了解如何建立自己的证书颁发机构。 |
| TLS 简介 | www.internetsociety.org/ deploy360/tls/basics/ | TLS 简介。 |
| Let’s Encrypt | letsencrypt.org/ |
Let’s Encrypt 是由 Linux 基金会开发的一项服务,提供有效期短的免费证书。该服务包括一些自动化功能,用于自动化证书续签。 |
| OWASP TLS 技巧表 | cheatsheetseries.owasp.org/ cheatsheets/ Transport_Layer_Protection _Cheat_Sheet.html |
提供了来自开放网络应用安全项目的关于 TLS 及其工作原理的实用、有帮助的信息。 |
| 自签名证书 | dzone.com/articles/ creating-self-signed-certificate |
解释了如何创建自签名证书。 |
| SSL 和 TLS | www.hostingadvice.com/how-to/tls-vs-ssl/ | 该网站解释了 SSL 和 TLS 之间的区别。 |
| 保险库 | www.vaultproject.io | Vault 是一个用于管理秘密的工具,包括用户名和密码等详细信息。它包括从主节点安全分发信息的方法。 |
| OpenSSL | www.openssl.org | 开源的全功能工具包,用于传输层安全(TLS)和安全的套接字层(SSL)协议。被许多产品采用,包括 Fluentd。 |
E.10 环境设置
以下资源是设置 Fluentd 环境的有用额外信息来源。
| 名称 | 网址 | 描述 |
|---|---|---|
| Browserling 工具 | www.browserling.com/tools | 一套在线工具,帮助进行格式转换等。 |
| Chocolatey | chocolatey.org/ |
Chocolatey 是 Windows 的包管理器。它提供了一个类似于 Linux 的包管理器用户体验,如 rpm、yum 等。 |
| Clang | clang.llvm.org/ |
支持编译平台原生元素的库。 |
| GCC | gcc.gnu.org/ |
GCC 编译工具,如果你需要为源代码构建操作系统原生功能,则需要这些工具。 |
| GraalVM | www.graalvm.org/ | 支持 Java 和其他语言的下一代多语言虚拟机。它能够创建平台原生二进制文件。 |
| Linux 包管理器摘要 | mng.bz/4jDj |
提供有关不同包管理器和根据您的包管理器使用相应命令的信息。 |
| NTP(网络时间协议) | doc.ntp.org/ |
NTP 定义的官方网站;包括有关该主题的软件和白皮书。 |
| Quarkus | quarkus.io/ |
使用 OpenJDK 的基于 Java 的堆栈,能够创建 Kubernetes 原生解决方案。 |
| 语义化版本控制 | semver.org/ |
艺术品的版本标准形式。 |
E.11 日志框架
以下不是所有日志框架的详尽总结,因为维护此类列表本身就是一项全职工作。此列表确实提供了涵盖,包括链接到语言原生日志框架和我们认为活跃的开源框架。
| 名称 | 语言 | 网址 | 描述 |
|---|---|---|---|
| N/A | PHP | www.php-fig.org/psr/psr-3/ |
PHP 社区已经开发了各种标准(PSR),包括日志框架(PSR-3)。PSR 定义了日志的接口标准,因此任何支持此标准的框架都将兼容。从 Zend 到 Drupal,广泛的 PHP 框架支持此规范。例如,Magneto 这样的框架提供了符合 PSR 的日志,支持与 Fluentd 的通信。 |
| Django | Python | docs.djangoproject.com/en/3.1/ |
Django 是一个利用原生日志并提供一些附加功能的 Web 应用程序框架。这意味着 Fluentd 扩展可以被集成。 |
| 语言原生 | Java | mng.bz/QWPv |
这是 Java 语言的一部分;然而,它并不是最终的日志解决方案。许多人仍然更喜欢使用其他开源解决方案。 |
| 语言原生 | C#—语言原生库 | mng.bz/XWNa |
这是微软提供的 C# 原生框架。它支持 JSON 配置文件控制和从配置中驱动的日志记录器注入。 |
| 语言原生 | VB.Net—语言原生库 | mng.bz/y4Qd |
这涵盖了 Microsoft 为 Visual Basic 在 .Net 中提供的日志记录功能。 |
| 语言原生 | Ruby—语言原生库 | docs.ruby-lang.org/en/2.4.0/Logger.html |
Ruby 提供了一个具有日志级别的原生日志类。输出选项有限。语言基础可以扩展以支持更多目的地。 |
| 语言原生 | Python—语言原生库 | docs.python.org/3/library/logging.html |
Python 的原生语言特性是几个扩展功能的日志框架的基础,包括一些 Python 框架,它们通过添加追加器增加了价值。这包括 Fluentd Python 库。 |
| 语言原生 | Go—原生日志 | golang.org/pkg/log/ |
这是 Go 的原生日志包。与许多其他日志机制相比,这相当简单,最好将其视为一组辅助方法,用于以更结构化的方式将日志事件发送到 stderr。 |
| lgr | R | mng.bz/M2BB |
这是一个为 R 语言设计的日志框架,其设计原则来源于 Log4J。提供的追加器数量较少,专注于数据库(考虑到这是一个以数据分析为重点的语言,这是可以理解的)。 |
| Log4cplus | C++ | sourceforge.net/p/log4cplus/wiki/Home/ |
基于 Log4J 框架。 |
| Log4Cxx | C++ | logging.apache.org/log4cxx/latest_stable/ |
作为 Apache Log4J 家族的一部分,Log4Cxx 是 Log4J 的端口。 |
| Log4J2 | Java | logging.apache.org/log4j/2.x/index.html |
Apache 还为 Kotlin 和 Scala 提供了几个 JVM 相关的语言封装。 |
| Log4Net | C# & VB 和其他在 .Net 框架上支持的语言 | logging.apache.org/log4net/ |
Log4Net 是 Apache 基金会移植的 Log4J 框架,该基金会开发了原始框架。 |
| Logrus | Go | github.com/Sirupsen/logrus |
Logrus 目前处于仅维护状态,因为开发者认为它已经达到了可扩展性的极限,同时没有破坏兼容性。然而,它因其常用性而被引用。 |
| Monolog | PHP | github.com/Seldaek/monolog |
Monolog 只是一个支持 PSR-3 规范的日志框架。它包括一个用于 Fluentd 的格式化器,可以与用于套接字级别通信的处理程序结合使用。 |
| NLog | C# & VB 和其他在 .Net 框架上支持的语言 | nlog-project.org/ |
框架扩展中包含的 HTTP 追加器。 |
| Pino | Node JS | getpino.io/ |
Pino 适合集成到各种 Node.JS 框架中,例如 Express。它使用传输的概念将日志发送到其他系统,包括一些本机云服务提供商的解决方案。它支持套接字和 HTTP 端点,因此可以使用这些来与 Fluentd 进行通信。 |
| Serilog | C# & VB 和其他在 .Net 框架上支持的语言 | serilog.net/ |
Serilog 是一个开源框架,它促进了更强的结构化日志记录。它提供了广泛的输出目的地,包括 Fluentd。 |
| SLF4J | Java | www.slf4j.org/ |
Simple Logging Facade for Java (SLF4J) 不是一个框架,而是一个抽象层,使得不同的日志框架可以使用相同的底层基础;例如,Logback 和 Log4J2。 |
| Twisted | Python | twistedmatrix.com/trac/ |
这又是一个具有事件驱动模型的 Python 框架。它提供了一种集成到更广泛生态系统的日志机制。从那里,Twisted 可以用来将日志事件发送到一组本机发布者,或者通过 Python 的原生日志功能发送日志事件。 |
| Winston JS | Node JS | github.com/winstonjs/winston |
Winston 具有许多日志框架的特征,具有日志级别和追加器(在 Winston 中称为传输)等功能。与一些其他框架相比,传输的范围有限,但提供了一个构建自己框架的框架。 |
E.12 立法信息门户
一旦我们记录敏感信息,例如可识别的个人、信用卡以及许多其他事物,我们的日志和日志存储就可能受到一系列法律法规的约束。以下是我们过去查看的一些资源,以帮助我们进一步理解。
| 常见名称 | URL | 描述 |
|---|---|---|
| DLA Piper 数据保护 | www.dlapiperdataprotection.com | DLA Piper 是一家全球律师事务所,它开发和维护了一个网站,提供了关于各国在数据保护方面定位的良好见解。 |
| GDPR |
| 欧洲联盟(EU)制定的通用数据保护条例(GDPR)旨在加强个人数据保护。所有欧盟国家都已批准了这项立法,以及一些基于其构建的国家立法。许多国家和美国各州都制定了他们自己的衍生立法;例如,加利福尼亚州的《加州消费者隐私法案》(CCPA)。 |
|---|
| 健康保险可携带性和问责制法案(HIPAA) |
| ISO/IEC 27001 |
| PCI DSS(支付卡行业数据安全标准) |
| 萨班斯-奥克斯利法案(SOX) |
| 联合国贸易和发展会议(UNCTAD) |
E.13 其他有用的信息来源
| 名称 | URL | 描述 |
|---|---|---|
| 转换不同的时间表示 | www.epochconverter.com/ | 将时间戳转换为 Linux/Unix 系统和 Java 等语言使用的秒或毫秒纪元表示形式。 |
| 记录错误代码的示例 |
| 优秀的错误代码文档示例。涵盖 IETF 和 WebLogic 应用程序服务器的 HTTP 和电子邮件。 |
|---|
| ISO 8601 日期时间标准 |
| ITIL(信息技术基础设施库) |
| 支付卡行业(PCI)安全标准委员会(SSC) |
| 正则表达式开发 |
| N 层架构 |
| TCP 和 UDP 网络协议 |
E.14 支持 Fluentd 资源
以下表格提供了 Fluentd 社区提供的与将日志事件发送到 Fluentd 相关的额外资源的链接。
| 名称 | URL | 描述 |
|---|---|---|
| Fluentd 提供的日志库 | github.com/fluent/fluent-logger-java |
Fluentd 为 Java 直接日志记录提供了一个库。 |
github.com/fluent/fluent-logger-ruby |
Fluentd 为 Ruby 直接日志记录提供了一个库。 | |
github.com/fluent/fluent-logger-python |
Fluentd 为 Python 直接日志记录提供了一个库。 | |
github.com/fluent/fluent-logger-perl |
Fluentd 为 Perl 直接日志记录提供了一个库。 | |
github.com/fluent/fluent-logger-php |
Fluentd 为 PHP 直接日志记录提供了一个库。 | |
github.com/fluent/fluent-logger-node |
Fluentd 为 NodeJS 直接日志记录提供了一个库。 | |
github.com/fluent/fluent-logger-scala |
Fluentd 为 Scala 直接日志记录提供了一个库。 | |
github.com/fluent/fluent-logger-golang |
Fluentd 提供了一个从 Go 直接进行日志记录的库。 | |
github.com/fluent/fluent-logger-erlang |
Fluentd 提供了一个从 Erlang 直接进行日志记录的库。 | |
github.com/fluent/fluent-logger-ocaml |
Fluentd 提供了一个从 OCaml 直接进行日志记录的库。 | |
| msgpack | msgpack.org/ |
这是一个在多种语言中实现的压缩库,Fluentd 可以利用它。在转发插件发送或接收事件时使用。 |
E.15 相关阅读
Fluentd,正如你所观察到的,在其潜在应用中跨越了许多边界。下表反映了这一点。列出的书籍可能有助于你扩展和利用那些相关服务。
| 名称 | 网址 | 描述 |
|---|---|---|
| Core Kubernetes | www.manning.com/books/core-kubernetes |
Fluentd 通常在 Kubernetes 的上下文中使用,并且只是 Kubernetes 设置的一个方面。这本书和Kubernetes in Action将涵盖你需要的大部分内容。 |
| Design Patterns | refactoring.guru/design-patterns/catalog |
这是首先由四人帮(GoF)描述的核心模式及其书籍《设计模式:可重用面向对象软件元素》的详细信息。四人帮是 Erich Gamma、John Vlissides、Richard Helm 和 Ralph Johnson。我们为每个模式提供了一个简要指南的链接。 |
| Docker in Action | mng.bz/g4Bv |
Docker 是实现容器的典型技术。当我们不需要 Kubernetes 的复杂性时,我们会更直接地使用 Docker。这本书涵盖了构建和运行容器的核心。 |
| Effective Unit Testing | www.manning.com/books/effective-unit-testing | 在第九章中,当我们实现自定义插件时,我们探讨了单元测试。理想情况下,为生产使用构建的单元测试应该是广泛的。虽然这本书专注于 Java,但它将提供最佳实践的见解。 |
| Elasticsearch in Action | www.manning.com/books/elasticsearch-in-action | 当与 Elasticsearch 和 Fluentd 一起工作时,这本书可能会很有用。 |
| Groovy in Action, 第二版 | mng.bz/en1V |
我们使用的日志模拟器是用 Groovy 构建的,这使得它非常容易添加增强功能,以有效地模拟不同的来源。如果你对深入了解 Groovy 感兴趣,这本书将有所帮助。 |
| Kubernetes in Action | mng.bz/p2PK |
本书涵盖了 Kubernetes 及其工作方式的大量细节,为容器编排提供了更多见解。 |
| MongoDB in Action, Second Edition | mng.bz/OGxw |
我们使用 MongoDB 作为输出目标。这将提供有关 MongoDB 可能需要的所有信息。 |
| Operations Anti-Patterns, DevOps Solutions | mng.bz/Yg1z |
获得您的操作流程和日志是实现 DevOps 工作方式的一个关键部分。本书探讨了您可能最终面临的潜在陷阱(或反模式)。 |
| Redis in Action | www.manning.com/books/redis-in-action | 我们在构建自定义插件的示例中使用了 Redis。这本书将更深入地介绍 Redis。 |
| Ruby in Practice | www.manning.com/books/ruby-in-practice | 提供更多关于使用 Ruby 的实际指导。 |
| Software Telemetry | www.manning.com/books/software-telemetry | Software Telemetry 讲述了获取指标、日志和某些类型的业务应用状态数据,并使用这些数据为您提供健康视角的想法。Fluentd 可以是遥测解决方案的关键部分。 |
| The Well-Grounded Rubyist, Third Edition | mng.bz/GGyD |
如果您正在考虑深入了解 Fluentd 或开发自己的自定义插件,那么这是一本很好的读物。 |
| Securing DevOps | www.manning.com/books/securing-devops | 探讨了保护云环境的需求和技术。 |
| YAML | yaml.org/ |
YAML 的官方站点包含了其语法的详细信息。 |





浙公网安备 33010602011771号