Prometheus-基础设施监控实用指南-全-

Prometheus 基础设施监控实用指南(全)

原文:annas-archive.org/md5/eed0067609d974db930559c34e6f291f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书与技术简介

本书介绍了 Prometheus,第二个从 云原生计算基金会 (CNCF) 毕业的项目,将帮助你巩固监控的核心基础知识,以及确保所需基础设施可见性的可用方法。它通过实践示例、使用测试环境和图表的方式,以易于消化的方式传达知识。

本书的内容设计旨在确保涵盖所有重要的 Prometheus 堆栈概念。我们在编写过程中的主要目标是以我们过去的自己为目标,确保他们可以在本书中找到关于这项技术的所有必要知识。

从运行一个 Prometheus 服务器,到可用的扩展选项,从创建和测试告警规则,到模板化 Slack 通知;以及从有用的仪表盘,到自动化目标发现;许多其他主题将被解释,以确保在使用 Prometheus 作为基石的基础设施监控中建立完整的知识体系。

本书适合的人群

如果你是软件开发人员、云计算专家、站点可靠性工程师、DevOps 爱好者或系统管理员,想要搭建一个可靠的监控和告警系统来维持基础设施的安全性和性能,那么本书适合你。基本的网络和基础设施监控知识将帮助你理解本书中涉及的概念。

本书内容概述

第一章,监控基础知识,奠定了本书中多个关键概念的基础。本章还探讨了 Prometheus 在度量收集方面的方式,以及为何一些有争议的决策对其架构设计至关重要。

第二章,Prometheus 生态系统概述,提供了整个 Prometheus 生态系统的高级概述,介绍了各个组件的职能,以及它们如何在逻辑上协同工作。

第三章,设置测试环境,介绍了如何使用本书提供的测试环境的基础知识,以及如何操作这些环境来验证不同的配置。

第四章,Prometheus 度量基础,探讨了度量,这是 Prometheus 的核心资源。正确理解这些度量对于充分利用、管理甚至扩展 Prometheus 堆栈至关重要。

第五章,运行 Prometheus 服务器,聚焦于 Prometheus 服务器,提供了常见的使用模式和虚拟机与容器的完整设置过程场景。

第六章,导出器与集成,介绍了一些最有用的导出器,并提供了如何使用它们的示例。

第七章,Prometheus 查询语言–PromQL,深入讲解了强大而灵活的 Prometheus 查询语言,利用其多维数据模型,支持临时聚合和时间序列的组合。

第八章,故障排除与验证,提供了有关如何快速检测和修复问题的实用指南。还介绍了暴露关键信息的有用端点,并探讨了promtool,即 Prometheus 的命令行接口和验证工具。

第九章,定义告警和记录规则,介绍了记录和告警规则的使用与测试,并提供了相关的示例。

第十章,发现与创建 Grafana 仪表板,深入探讨了 Prometheus 栈中的可视化组件,涵盖了内置控制台功能,同时也探索了 Grafana,及如何构建、共享和重用仪表板。

第十一章,理解与扩展 Alertmanager,介绍了栈中的告警组件,展示了如何将其与多种告警提供者集成,并讲解了如何正确设置集群以实现高可用性和告警去重。

第十二章,选择合适的服务发现,探讨了多种服务发现集成方式,并提供了构建自定义集成所需的要求和知识。

第十三章,扩展和联邦 Prometheus,探讨了 Prometheus 栈的扩展问题,并介绍了分片和全局视图等概念,同时提供了背景信息并对其进行了说明。

第十四章,将长期存储与 Prometheus 集成,涵盖了 Prometheus 读取和写入端点的概念。接着深入探讨了外部和长期度量存储的考虑因素。最后,通过 Thanos 提供了一个端到端的示例。

为了最大程度地利用本书

基本的监控、网络和容器知识是有用的,但不是必需的。

所有测试环境均使用 macOS 和 Linux 进行验证,并强制使用特定的软件版本以提高兼容性,因此这些是最适合确保你不会遇到问题的环境。第三章,设置测试环境,提供了与此主题相关的所有技术信息。

下载软件依赖项需要良好的互联网连接,同时需要现代浏览器来确保访问书中呈现的所有 Web 界面。

下载示例代码文件

你可以从www.packt.com的账户下载本书的示例代码文件。如果你从其他地方购买了本书,可以访问www.packt.com/support,并注册让文件直接发送到你的邮箱。

你可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误表”。

  4. 在搜索框中输入书籍名称,并按照屏幕上的指示操作。

下载文件后,请确保使用以下最新版本的解压或提取工具:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Hands-On-Infrastructure-Monitoring-with-Prometheus。如果代码有更新,它将在现有的 GitHub 仓库中进行更新。

我们还在我们的丰富书籍和视频目录中提供了其他代码包,访问github.com/PacktPublishing/查看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色版本。你可以在此下载:www.packtpub.com/sites/default/files/downloads/9781789612349_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户。例如:“现在,你可以运行vagrant status。”

代码块按以下方式设置:

 annotations:
     description: "Node exporter {{ .Labels.instance }} is down."
     link: "https://example.com"

当我们希望特别提醒你注意代码块中的某一部分时,相关的行或项目会以粗体显示:

 annotations:
     description: "Node exporter {{ .Labels.instance }} is down."
     link: "https://example.com"

任何命令行输入或输出都按以下方式编写:

vagrant up

粗体:表示新术语、重要词汇或在屏幕上看到的文字。例如,菜单或对话框中的文字在文本中会这样显示。以下是一个示例:“你可以通过进入顶部栏中的状态 | 规则找到此页面。”

警告或重要说明如下所示。

提示和技巧如下所示。

联系我们

我们非常欢迎读者的反馈。

一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中注明书名,并通过customercare@packtpub.com联系我们。

勘误表:尽管我们已尽力确保内容的准确性,但难免会有错误。如果你在本书中发现错误,我们将非常感激你向我们报告。请访问www.packt.com/submit-errata,选择你的书籍,点击勘误表提交链接,输入相关细节。

盗版:如果您在互联网上发现我们作品的任何非法复制版本,我们将非常感激您提供相关的地址或网站名称。请通过copyright@packt.com与我们联系,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣撰写或参与书籍的创作,请访问authors.packtpub.com

评价

请留下您的评价。阅读并使用本书后,为什么不在您购买书籍的站点上留下评价呢?潜在读者可以根据您的公正意见做出购买决策,我们在 Packt 能够了解您对我们产品的看法,我们的作者也可以看到您对他们书籍的反馈。谢谢!

欲了解有关 Packt 的更多信息,请访问packt.com

第一部分:简介

完成本节后,读者将掌握基本知识,能够深入探索 Prometheus 堆栈。

本节包括以下章节:

  • 第一章,监控基础

  • 第二章,Prometheus 生态系统概览

  • 第三章,设置测试环境

第一章:监控基础

本章为本书中将要使用的几个关键概念奠定了基础。从监控的定义开始,我们将探讨不同的视角和因素,强调为何系统化分析在不同层面上具有重要性,并对组织产生影响。你将了解不同监控机制的优缺点,深入了解 Prometheus 在收集指标方面的做法。最后,我们将讨论一些在 Prometheus 堆栈的设计和架构中至关重要的有争议的决策,及其在设计你自己的监控系统时应当考虑的原因。

本章将涵盖以下主题:

  • 监控的定义

  • 白盒监控与黑盒监控

  • 理解指标收集

监控的定义

监控的共识定义很难达成,因为它很快就会在行业甚至职位特定的上下文中发生变化。观点的多样性、监控系统的组成组件,甚至数据的收集或使用方式,都是导致难以形成清晰定义的因素。

如果没有共同的基础,很难维持有效的讨论,通常期望会不一致。因此,在接下来的主题中,我们将概述一个基础框架,旨在获得一个监控的定义,帮助我们在本书中进行导航。

监控的价值

随着基础设施复杂度的增加,特别是由于微服务架构的采用,获取基础设施各个组成部分的全局视图变得至关重要。手动验证每个实例、缓存服务、数据库或负载均衡器的健康状态几乎是不可想象的。移动的组件太多,数目庞大——更不用说还要紧密关注它们了。

如今,监控被期望能够跟踪这些组件的数据。然而,数据可能有多种形式,这使得它可以用于不同的目的。

报警是监控数据的标准应用之一,但这些数据的应用可以远远超出报警。你可能需要历史信息来帮助进行容量规划或事件调查,或者你可能需要更高的分辨率来深入挖掘问题,甚至需要更高的时效性以减少故障期间的平均恢复时间。

你可以将监控视为维持系统健康的一个信息来源,既包括生产系统,也包括业务系统。

组织背景

从组织的角度来看,系统管理员、质量保证工程师、站点可靠性工程师SRE)或产品负责人等角色对监控有不同的期望。了解每个角色的需求有助于更容易理解为什么在讨论监控时上下文如此重要。我们可以扩展以下几个陈述,并举些例子:

  • 系统管理员关注的是高分辨率、低延迟和高多样性的数据。对于系统管理员来说,监控的主要目标是获取基础设施的可视化,管理从 CPU 使用率到超文本传输协议HTTP)请求率等数据,以便快速发现问题并尽早识别根本原因。在这种方法中,呈现高分辨率的监控数据至关重要,这样才能深入分析受影响的系统。如果出现问题,你不能等待几个小时才能看到下一个数据点,因此数据必须接近实时提供,换句话说,具有低延迟。最后,由于没有简单的方法可以识别或预测哪些系统容易受到影响,我们需要从所有系统收集尽可能多的数据,即需要高度多样化的数据。

  • 质量保证工程师关注的是高分辨率、高延迟和高多样性的数据。除了收集高分辨率的监控数据,这对于质量保证工程师深入了解影响因素至关重要外,延迟对他们来说并不像对系统管理员那么关键。在这种情况下,历史数据对于比对软件版本之间的差异比数据的新鲜度更为关键。由于我们无法完全预测新版本发布的后果,因此需要将可用数据分布到尽可能多的基础设施中,触及每一个可能使用、调用或与软件版本互动的系统(直接或间接),以确保我们能够获得尽可能多的数据。

  • 专注于容量规划的 SRE 关注的是低分辨率、高延迟和高多样性的数据。在这种情况下,历史数据对 SRE 来说比数据呈现的分辨率更为重要。例如,在预测基础设施增长时,对于 SRE 来说,知道几个月前凌晨 4 点某个节点在 10 秒内出现了 CPU 使用率达到 100%的峰值并不至关重要,但理解节点负载的趋势,以推测为应对新的规模需求所需的节点数量,则非常有用。因此,对于 SRE 来说,广泛地了解受这些需求影响的基础设施各个部分也很重要,比如预测日志存储量、网络带宽增长等,这使得高多样性的监控数据变得至关重要。

  • 产品负责人关注的是低分辨率、高延迟和低多样性的数据。在产品负责人的关注点中,监控数据通常不再局限于基础设施,而是转向业务领域。产品负责人力图理解特定软件产品的趋势,其中历史数据至关重要,分辨率并非那么关键。考虑到评估软件发布对客户的影响,延迟对他们来说不如对系统管理员那么重要。产品负责人管理的是一组特定的产品,因此预期监控数据的多样性较低,主要由业务度量指标组成。

下表将之前的示例以更简洁的形式总结:

数据分辨率 数据延迟 数据多样性
基础设施警报
软件发布视角
容量规划
产品/业务视角

监控组件

就像监控的定义在不同上下文中有所变化一样,其组件也面临着相同的困境。根据你希望定义的范围,我们可以在以下话题中找到这些组件中的某些或全部:

  • 度量:这表示特定系统资源、应用程序操作或业务特征在某一时刻的值。该信息以汇总形式获得;例如,你可以了解到每秒处理了多少请求,但无法得知特定请求的确切时间,且没有上下文,你也不知道请求的 ID。

  • 日志记录:包含比度量更多的数据,它表现为系统或应用程序的一个事件,包含该事件产生的所有信息。这些信息未经过汇总,且具有完整的上下文。

  • 追踪:这是日志记录的一种特殊情况,其中请求被赋予唯一标识符,以便在整个生命周期中追踪每个系统中的请求。由于请求数量的增加会使数据集变大,因此使用样本而不是追踪所有请求是一个好主意。

  • 警报:这是对度量或日志的持续阈值验证,当超出指定阈值时,触发动作或通知。

  • 可视化:这是度量、日志或追踪的图形化表示。

最近,“监控”这一术语被一个超集可观测性所取代,这被视为监控的演变,或者是为了制造炒作并复兴这一概念的不同包装(就像 DevOps*一样)。从目前的角度来看,可观测性确实包含了我们在此描述的所有组件。

在本书中,我们的监控定义包括度量、警报和可视化。

监控是具有相关警报和可视化的度量。

白盒监控与黑盒监控

我们可以采用多种方式进行监控,但它们大体上可以分为两大类:即黑盒监控和白盒监控。

在黑盒监控中,应用程序或主机从外部被观察,因此,这种方法可能相对有限。检查是否系统响应探测请求,并以已知的方式作出反应:

  • 主机是否响应互联网控制消息协议ICMP)回显请求(通常称为 ping)?

  • 给定的 TCP 端口是否开放?

  • 当应用程序收到特定的 HTTP 请求时,它是否以正确的数据和状态码作出响应?

  • 特定应用程序的进程是否在其主机上运行?

另一方面,在白盒监控中,受监控的系统会展示其内部状态和关键部分的性能数据。这种类型的内省非常强大,因为它暴露了操作遥测数据,从而可以了解不同内部组件的健康状况,否则这些信息可能很难甚至无法得知。通常,这些遥测数据以以下方式处理:

  • 通过日志导出:这是迄今为止最常见的情况,也是应用程序在仪表化库普及之前暴露其内部工作方式的方式。例如,可以处理 HTTP 服务器的访问日志,以监控请求率、延迟和错误百分比。

  • 作为结构化事件发出:这种方法类似于日志记录,但数据不是写入磁盘,而是直接发送到处理系统进行分析和聚合。

  • 以聚合形式存储在内存中:这种格式的数据可以托管在端点中,也可以直接通过命令行工具读取。例如,*/*metrics 用于 Prometheus 指标,HAProxy 的统计页面,或者 varnishstats 命令行工具。

并非所有软件都经过仪表化,并且准备好暴露其内部状态以进行指标收集。例如,它可能是一个第三方的闭源应用程序,无法展示其内部工作原理。在这些情况下,外部探测可能是收集被认为对于适当服务状态验证至关重要的数据的可行选择。

无论如何,黑盒监控不仅对第三方应用程序有益。它还可以从客户端的角度验证您的应用程序,例如,通过负载均衡器和防火墙。探测可以是您的最后一道防线——如果其他方法失败,您可以依赖黑盒监控来评估可用性。

理解指标收集

监控系统采集指标的过程通常可以分为两种方法——推送和拉取。正如我们将在接下来的主题中看到的,两种方法都是有效的,并且各有优缺点,我们将详细讨论。然而,了解它们的差异对于理解和充分利用 Prometheus 至关重要。在了解了指标采集的工作原理之后,我们将深入探讨应该采集哪些内容。实现这一目标有几种经过验证的方法,我们将对每种方法进行概述。

两种采集方法概述

在基于推送的监控系统中,生成的指标或事件会直接从生成应用程序或本地代理发送到采集服务,如下所示:

图 1.1:基于拉取的监控系统

处理原始事件数据的系统通常更倾向于使用推送,因为事件生成的频率非常高——每个实例每秒钟生成数百、数千甚至数万次事件——这使得轮询数据变得不切实际且复杂。为了在轮询之间保持事件的生成,需要某种缓冲机制,且与直接推送数据相比,事件的新鲜度仍然是一个问题。使用这种方法的一些例子包括 Riemann、StatsD,以及ElasticsearchLogstashKibanaELK)堆栈。

这并不意味着只有这些类型的系统使用推送。一些监控系统,如 Graphite、OpenTSDB,以及TelegrafInfluxDBChronographKapacitorTICK)堆栈,都是采用这种方式设计的。即便是老牌的 Nagios,也通过Nagios Service Check AcceptorNSCA),也就是通常所说的被动检查,支持推送:

图 1.2:基于推送的监控系统

相比之下,基于拉取的监控系统直接从应用程序或通过代理进程收集指标,这些代理进程将指标提供给系统。一些使用拉取方法的知名监控软件包括 Nagios 及其类似系统(如 Icinga、Zabbix、Zenoss 和 Sensu 等)。Prometheus 也是采用拉取方法的其中之一,并且在这一点上有着明确的立场。

推送与拉取

在监控社区中,关于这些设计决策的优劣一直存在着许多争议。主要的争论点通常围绕目标发现展开,我们将在接下来的段落中讨论这一点。

在基于推送的系统中,被监控的主机和服务通过向监控系统报告来向系统表明自己的存在。这里的优势是,不需要预先了解新的系统即可将它们纳入监控。然而,这意味着监控服务的位置信息需要传播到所有目标,通常通过某种配置管理方式来实现。陈旧性是这种方法的一个大缺点:如果一个系统长时间没有报告,这是否意味着它出了问题,还是它被故意停用?

此外,当你管理一个分布式的主机和服务队列,且它们将数据推送到一个中心点时,"雷霆般的群体"(由于许多连接同时到达而导致的过载)或误配置导致的数据洪流的风险变得更加复杂,需要花费更多的时间和精力来缓解。

在基于拉取的监控中,系统需要一个明确的主机和服务列表,以便监控其度量指标并将其导入。拥有一个集中式的真实数据源可以提供一定程度的保证,确保一切按预期运行,但缺点是需要维护该数据源并保持其随时更新。由于当今基础设施的变化速度极快,因此需要某种形式的自动发现机制来跟上全局变化。拥有一个集中的配置点,可以在出现问题或误配置时提供更快速的响应。

最终,每种方法的缺点都可以通过巧妙的设计和自动化来减少或有效解决。在选择监控工具时,还有其他更重要的因素,例如灵活性、自动化的便捷性、可维护性,或对所使用技术的广泛支持。

尽管 Prometheus 是基于拉取的监控系统,它也提供了一种通过使用网关将推送转为拉取的方式来获取推送的度量。这对于监控一些非常狭窄的进程类非常有用,稍后我们将在本书中看到。

应该测量什么

在规划度量收集时,必定会遇到一个问题,即定义要观察的度量指标。为了回答这个问题,我们应该参考当前的最佳实践和方法论。在接下来的主题中,我们将概述一些最具影响力和最受推崇的方法,帮助减少噪声并提高性能和整体可靠性问题的可见性。

谷歌的四个黄金信号

谷歌关于监控的理由非常简单。它直接说明,跟踪的四个最重要的度量指标如下:

  • 延迟:处理请求所需的时间

  • 流量:发起的请求数量

  • 错误:失败请求的比例

  • 饱和度:未被处理的工作量,通常是排队等待的

Brendan Gregg 的 USE 方法

Brendan 的方法更侧重于机器,它指出对于每个资源(CPU、磁盘、网络接口等),应该监控以下指标:

  • 利用率:以资源忙碌的百分比来衡量

  • 饱和度:资源未能处理的工作量,通常以排队的方式存在

  • 错误:发生的错误数量

Tom Wilkie 的 RED 方法

RED 方法更侧重于服务层面的监控,而不是底层系统本身。当然,作为监控服务的有用方法,这一策略也对预测外部客户的体验具有价值。如果一个服务的错误率增加,可以合理地假设这些错误将直接或间接地影响客户的体验。以下是需要关注的指标:

  • 速率:翻译为每秒请求数

  • 错误:每秒失败请求的数量

  • 持续时间:这些请求所花费的时间

总结

在本章中,我们有机会理解监控的真正价值以及如何在特定上下文中处理这一术语,包括本书中使用的上下文。这将帮助你避免任何误解,并确保你清楚地了解本书在这一主题上的立场。我们还讨论了监控的不同方面,如指标、日志记录、追踪、告警和可视化,同时介绍了可观测性及其带来的好处。白盒和黑盒监控被提及,为理解使用指标的好处提供了基础。掌握了关于指标的知识后,我们深入探讨了推拉机制以及各自的相关论点,最后总结了你所管理的系统中应跟踪的指标。

在下一章中,我们将概述 Prometheus 生态系统,并讨论其几个组件。

问题

  1. 为什么监控的定义如此难以清晰界定?

  2. 指标的高延迟是否会影响专注于修复实时事件的系统管理员的工作?

  3. 为了正确进行容量规划,监控的要求是什么?

  4. 日志记录是否算作监控?

  5. 关于可用的指标收集策略,使用基于推送的方法有什么缺点?

  6. 如果你必须选择三个来自通用 Web 服务的基本指标来关注,它们会是哪三个?

  7. 当一个检查通过列出主机中正在运行的进程来验证给定进程是否在主机上运行时,这属于白盒监控还是黑盒监控?

进一步阅读

第二章:Prometheus 生态系统概述

在可用的如此庞大的组件集合中,选择所需的组件来解决特定的监控需求可能令人望而生畏。本章将介绍 Prometheus 生态系统,各个组件执行什么任务,并了解它们如何在逻辑上协同工作。

追求简洁并清楚理解 Prometheus 堆栈的所有组成部分,对于保持系统的可管理性和可靠性至关重要。

简而言之,本章将涉及以下主题:

  • 使用 Prometheus 进行度量指标收集

  • 通过 exporters 暴露内部状态

  • 使用 Alertmanager 进行警报路由和管理

  • 可视化你的数据

使用 Prometheus 进行度量指标收集

Prometheus 是一个基于时间序列的开源监控系统。它通过向主机和服务的度量端点发送 HTTP 请求来收集数据,然后使用强大的查询语言使其可用于分析和警报。

尽管 Prometheus 通过证明其稳定性、成熟性和稳固的治理,已经获得了云原生计算基金会CNCF)的认证,它仍在以非常快速的速度发展。写作时,Prometheus 的当前稳定版本是 2.9.2,本书中讨论的所有组件或特性将基于此版本。虽然在 2 版本内不应有重大架构变化,但在将本书中学习到的特定配置应用于早期或甚至后续版本时,仍需谨慎。

Prometheus 架构的高级概览

Prometheus 生态系统由多个组件组成,每个组件都有自己的责任和明确的范围。Prometheus 本身至关重要,因为它位于大多数交互的核心,但许多组件实际上是可选的,这取决于你的监控需求。

如下图所示,Prometheus 生态系统中的主要组件如下:

  • Prometheus 服务器收集时间序列数据,存储数据,提供查询接口,并根据数据发送警报。

  • Alertmanager 从 Prometheus 接收警报触发器,并处理警报的路由和派发。

  • Pushgateway 处理来自短生命周期作业(如 cron 或批处理作业)推送的度量指标的公开。

  • 支持 Prometheus 暴露格式的应用程序通过 HTTP 端点公开内部状态。

  • 社区驱动的 exporters 暴露来自不原生支持 Prometheus 的应用程序的度量指标。

  • 第一方和第三方仪表板解决方案提供了收集数据的可视化。

本书后续将深入探讨每个组件:

图 2.1:Prometheus 生态系统中主要组件的高级概览

Prometheus 服务器有其自身的内部进程,如记录规则和服务发现,这些内容分别在第九章,定义告警和记录规则,以及第十二章,选择正确的服务发现中有详细解释。

Prometheus 最初由 Matt T. Proud 和 Julius Volz 在 SoundCloud 工作时创建。它的灵感来源于 Google 的 Borgmon,Borgmon 对其早期设计产生了很大影响:从度量端点抓取纯文本;导出器作为度量收集的代理;将时间序列视为多维向量,然后可以进行转换和过滤;以及使用规则集进行记录和告警等功能。

你可能会想尝试将 Prometheus 适配为基于推送的度量收集模型,但这是不建议的。Prometheus 的核心设计围绕拉取展开,因此从推送转为拉取时,很多假设会被打破。当我们介绍 Pushgateway 时会进一步解释这个问题。

Prometheus 的一个独特特点是,它毫不掩饰地不尝试做任何类型的集群。通过不依赖网络进行协调和存储(尽管远程写入是可能的,正如我们在本书结尾部分将看到的那样),它为可靠性和易用性提供了有力的论据。只需选择合适的 Prometheus 二进制分发版并在本地计算机上运行,它便能轻松地处理成千上万的抓取目标和每秒数百万个样本的摄取,即使是服务器硬件。

使用导出器暴露内部状态

不是所有的应用程序都使用 Prometheus 兼容的监控工具。有时根本不会暴露任何度量。在这些情况下,我们可以依赖导出器。以下图示展示了它们的工作原理:

图 2.2:导出器的高层次概述

导出器不过是一个收集服务或应用程序数据并通过 HTTP 以 Prometheus 格式暴露数据的软件。每个导出器通常针对特定的服务或应用,因此它们的部署反映了这种一对一的协同关系。

如今,你几乎可以找到任何你需要的服务的导出器,如果某个特定的第三方服务没有可用的导出器,自己构建一个也非常简单。

导出器基础知识

当导出程序启动时,它会绑定到一个配置的端口,并通过你选择的 HTTP 端点暴露所收集的内部状态(默认端点为 /metrics)。当发出 HTTP GET 请求到配置的端点时,就会收集到仪表数据。例如,node exporter 是最常用的导出程序之一,它依赖于多个内核统计信息来展示诸如磁盘 I/O、CPU、内存、网络、文件系统使用情况等数据。每次抓取该端点时,信息都会迅速收集并同步暴露出来。

Prometheus 服务器向被监控系统发出的用于指标收集的 HTTP GET 请求被称为 抓取(scrape)。

如果你是编写服务的人,最佳选择是直接使用 Prometheus 客户端库来进行代码仪表化。以下编程语言有官方的客户端库:

  • Go

  • Java/JVM

  • Python

  • Ruby

以下编程语言有社区驱动的客户端库:

  • Bash

  • C++

  • Common Lisp

  • Elixir

  • Erlang

  • Haskell

  • Lua for NGINX

  • Lua for Tarantool

  • .NET

  • C#

  • Node.js

  • Perl

  • PHP

  • Rust

由于围绕 Prometheus 的社区不断壮大,这个列表也在不断扩展。

通常情况下,导出程序非常轻量,性能开销通常可以忽略不计,但像往常一样,也有一些例外,我们将在本书的后面详细讲解这些例外。

使用 Alertmanager 进行警报路由和管理

Alertmanager 是 Prometheus 生态系统中的一个组件,负责处理由 Prometheus 服务器生成的警报触发的通知。因此,它的可用性至关重要,设计选择也反映了这一需求。它是唯一真正为高可用集群设置而设计的组件,并使用 gossip 作为通信协议:

图 2.3:Alertmanager 高级概览

从高层次来看,Alertmanager 是一个通过其 API 从 Prometheus 服务器接收 HTTP POST 请求的服务,然后它会去重并根据预定义的路由集进行处理。

Alertmanager 还暴露了一个 Web 界面,用于例如可视化和静音触发的警报,或为它们应用抑制规则。

核心设计选择之一是重视交付而非去重。这意味着,如果在一组 Alertmanager 实例之间发生网络分区,通知将从分区的两侧发送。

警报路由

本质上,路由可以看作是一种树状结构。如果一个传入的警报具有特定的有效负载并触发某个特定的路由(分支),则会调用预定义的集成。

针对最常见的用例,已有多个开箱即用的集成可供使用,举例如下:

  • Email

  • Hipchat

  • PagerDuty

  • Slack

  • Opsgenie

  • VictorOps

  • 微信

还有 Webhook 集成,它会向您选择的端点发送带有触发警报的 JSON 负载的 HTTP POST 请求,打开了自定义集成的无限可能性。

可视化您的数据

数据可视化是产生或消费信息的最简单方式之一。Prometheus 提供了一个定义良好的 API,在该 API 中,PromQL 查询可以生成用于可视化的原始数据。

目前,最好的外部可视化软件是 Grafana,我们将在第十章中详细解释,发现和创建 Grafana 仪表板。Grafana 团队已经使其与 Prometheus 的集成无缝,结果是令人愉快的用户体验。

Prometheus 服务器还附带两个内部可视化组件:

  • 表达式浏览器:在这里,您可以直接运行 PromQL 快速查询数据并即时可视化:

图 2.4:Prometheus 表达式浏览器界面

  • 控制台:这些是使用 Golang 模板语言构建的网页,由 Prometheus 服务器本身提供。这种方法使您能够拥有预定义的数据可视化界面,而无需不断键入 PromQL:

图 2.5:Prometheus 控制台界面

总结

为了更好地理解 Prometheus 的理念,必须深入了解 Prometheus 生态系统的主要组件——从通过导出器收集数据到使用 Alertmanager 进行可靠的警报,以及可用的可视化选项。我们在这一章中已经涵盖了这些内容。

在下一章,我们将开始构建一个测试环境,这样我们迄今为止讨论的所有概念就能开始实现。

问题

  1. Prometheus 生态系统的主要组成部分是什么?

  2. 在 Prometheus 部署中,哪些组件是必需的,哪些是可选的?

  3. 为什么需要外部导出器?

  4. 当 HTTP GET 请求访问导出器的指标端点时,会发生什么?

  5. 如果网络分区发生,在 Alertmanager 集群中触发的警报会发生什么情况?

  6. 您意识到需要将 Alertmanager 与定制的 API 集成。最快的选择是什么?

  7. 标准 Prometheus 服务器安装中包括哪些可视化选项?

深入阅读

第三章:设置测试环境

最好的学习方式是通过实践。本章节将帮助你快速启动测试环境,让你可以安全地进行实验而无需过多担心。它将提供几个配置示例,并提供如何运行的提示。本书中将多次使用这种环境,适用于不同的场景。

简而言之,本章节将涵盖以下主题:

  • 代码组织

  • 机器要求

  • 启动新环境

代码组织

本书中的示例和代码列表可以直接使用而无需任何支持材料,但也提供了一个配套的 Git 仓库,以帮助你设置和自动化测试环境,从而便于你跟随学习。

在本节中,我们将探讨该仓库的组织结构,解释在自动化测试环境时所做的一些选择,并提供一些关于如何自定义测试环境的建议:

.
├── Makefile
├── README.md
├── cache/
├── chapter03/
├── ...
├── chapter14/
└── utils/

这里展示的仓库根目录结构应该很容易理解:

  • 每个章节都有一个单独的目录,用于其独立的测试环境(目录名为chapter,后跟章节号)

  • 一个cache目录,用于存放下载的包,以便重建测试环境时尽可能加速

  • 一个utils目录,存放测试环境的默认版本和参数(如果需要,可以修改),以及一些辅助函数

接下来,我们将深入探讨每个部分,具体如下:

.
├── ...
└── utils/
    ├── defaults.sh
    ├── helpers.sh
    └── vagrant_defaults.rb

utils目录下,可以找到以下文件:

  • defaults.sh:在这里,可以找到将在测试环境中使用的 Prometheus 堆栈中每个组件的版本(例如 Prometheus 本身、exporters、Alertmanager 等)。

  • vagrant_defaults.rb:此文件控制运行测试环境的虚拟机的一些可调参数,如每台虚拟机的内存大小、使用的基础镜像以及环境的内部网络结构。

  • helpers.sh:这是一个 shell 库,供配置脚本使用,包含一些帮助函数来管理归档文件的下载和缓存:

.
├── ...
├── chapter03/
│   ├── Vagrantfile
│   ├── configs/
│   └── provision/
└── ...

虽然每个测试环境在各章节之间可能有所不同,但基本结构将保持一致:

  • 一个Vagrantfile,用于描述测试环境所需的虚拟机数量,以及如何配置和配置它们

  • 一个configs目录,存放将在配置步骤中使用的配置文件

  • 一个provision目录,包含用于下载、安装和配置当前测试环境所需的 Prometheus 组件的脚本

我们可以通过查看本章节的目录结构来看到一个示例:

.
├── ...
├── chapter03/
│   ├── Vagrantfile
│   ├── configs/
│   │   ├── alertmanager/
│   │   ├── grafana/
│   │   ├── node_exporter/
│   │   └── prometheus/
│   └── provision/
│       ├── alertmanager.sh*
│       ├── grafana.sh*
│       ├── hosts.sh*
│       ├── node_exporter.sh*
│       └── prometheus.sh*
└── ...

configs目录包含本章节中使用的各个组件的子目录。provision目录遵循相同的模式,并增加了hosts.sh脚本,以自动化管理来宾机上的/etc/hosts文件。

到现在为止,可能有人会产生为什么不直接使用配置管理?这个问题。所有的自动化配置工作都是通过 shell 完成的,原因有几个:

  • 这是有意识的努力,旨在暴露每个细节,而非将其抽象化。

  • Shell 脚本是类 Unix 系统中自动化的最基本手段。

  • 本书的目的是聚焦于 Prometheus 的内部工作原理,而不是某个特定配置管理工具的实现。

机器要求

该设置的机器要求可以在现代笔记本电脑上舒适运行,只要其启用了 CPU 虚拟化扩展,并且操作系统与软件要求兼容。这里涉及的所有软件要求都经过深思熟虑。我们将使用免费且开源的软件,因此在尝试测试环境时不需要额外费用。

硬件要求

部署提供示例的主机最低要求如下:

  • 至少 2 个 CPU 核心

  • 至少 4 GB 内存

  • 至少 20 GB 可用磁盘空间

按照这些规格,你应该能够顺利启动测试环境,且不会遇到任何问题。

关于连接性,主机应该具有互联网访问权限,并能够解析外部 DNS 记录。提供的脚本在执行过程中需要下载依赖项,但大部分依赖项会被缓存到本地,以避免每次部署时都下载。

示例环境的默认网络为192.168.42.0/24,下图展示了运行本章节示例时的配置:

图 3.1:虚拟网络配置

启动测试环境时,将使用子网192.168.42.0/24。每个环境属于特定的章节,在切换到新环境之前,应该销毁当前环境。如果你的本地地址空间发生冲突,可以通过编辑提供的./utils/vagrant_defaults.rb文件中的 NETWORK 选项来更改测试环境的子网。

推荐软件

环境使用以下软件进行测试,因此适用标准免责声明:尽管相同主版本的其他版本可能无需额外修改即可工作,但在使用与我们推荐版本不同的版本时应谨慎:

软件 版本
VirtualBox 6.0.4
Vagrant 2.2.4
Minikube 1.0.1
kubectl 1.14.1

关于支持的操作系统,所有的测试都是在以下版本的 Linux 和 macOS 上进行的:

  • Ubuntu 18.04 LTS(Bionic Beaver)

  • macOS 10.14.3 (Mojave)

其他操作系统/发行版可能能够运行测试环境,尽管无法保证。

VirtualBox

Oracle VirtualBox 是一款免费的开源虚拟机管理程序,支持所有主流操作系统(macOS、Linux 和 Windows)。它不仅允许启动虚拟机镜像,还可以创建虚拟网络,并将主机文件系统路径挂载到客户机中,具备其他功能。此软件要求启用硬件虚拟化。

你可以在www.virtualbox.org/wiki/Downloads找到 VirtualBox 的所有安装文件。

Vagrant

HashiCorp Vagrant 允许创建可移植的环境。在本书的上下文中,它将成为与 VirtualBox 的接口,允许启动和配置虚拟机。在我们的示例中,我们选择使用 Chef Bento 作为虚拟机镜像,这是 HashiCorp 推荐的。

你可以在www.vagrantup.com/downloads.html找到 Vagrant 的所有安装文件。

Minikube

Minikube 是测试 Kubernetes 本地环境的最简单方法。我们将结合使用 Minikube 和 VirtualBox,确保本书中的示例在不同操作系统上表现一致。

你可以在kubernetes.io/docs/tasks/tools/install-minikube/#install-minikube找到有关 Minikube 的所有安装信息。

kubectl

与 Kubernetes API 交互的客户端工具叫做 kubectl,它是本书中一些示例的基础。

你可以在kubernetes.io/docs/tasks/tools/install-minikube/#install-kubectl找到有关 kubectl 的所有安装信息。

启动一个新的环境

确保主机上已经安装所有必需的软件后,你可以继续进行以下一个或两个指南。

自动化部署指南

此方法将抽象化所有部署和配置细节,允许你仅通过几个命令便能拥有一个完全运行的测试环境。你仍然可以连接到每个客户机实例并更改配置。

启动环境的步骤如下:

  1. 克隆本书的代码仓库:
git clone https://github.com/PacktPublishing/Hands-On-Infrastructure-Monitoring-with-Prometheus.git
  1. 进入新创建的目录并进入章节编号:
cd Hands-On-Infrastructure-Monitoring-with-Prometheus/chapter03
  1. 启动本章的测试环境:
vagrant up

第一次运行会花费几分钟,因为需要下载 Vagrant 镜像和一些软件依赖项。完成设置过程后,后续的运行会更快,因为所有这些资源将被缓存。

  1. 现在,你可以运行 vagrant status。你将看到以下输出:
Current machine states:

prometheus running (virtualbox)
grafana running (virtualbox)
alertmanager running (virtualbox)

This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.

Prometheus

你可以在 http://192.168.42.10:9090/targets 找到 Prometheus HTTP 端点:

图 3.2:Prometheus HTTP 端点 —— 显示所有已配置的目标

Grafana

你可以在 http://192.168.42.11:3000 找到 Grafana HTTP 端点。

Grafana 的默认凭证如下:

用户名 密码
admin admin

你将看到两个自动配置的仪表板。我们将在本书后续的章节中专门讨论 Grafana 和仪表板,参考 第十章,发现和创建 Grafana 仪表板

图 3.3:自动配置的 Grafana 仪表板

警报管理器

你可以在 http://192.168.42.12:9093 找到警报管理器 HTTP 端点。

我们还在 Prometheus 上配置了一个始终触发的警报,并通过 Webhook 集成了自定义的警报管理器,这样你就可以开始了解它们之间的关系。我们将在其他章节详细介绍警报管理器,但现在,你可以查看示例警报在代码库根目录下生成的日志,路径为 ./cache/alerting.log

图 3.4:警报管理器 —— 触发一个示例警报

清理

测试完成后,只需确保你在 chapter03 目录下并执行以下命令:

vagrant destroy -f

不用担心 —— 如果你愿意,环境可以轻松重新启动。

高级部署演练

使用此方法,来宾虚拟机将被启动,但不会进行任何配置,因此你需要亲自通过实践方式来设置环境。我们不会深入解释可用的配置文件和命令行参数 —— 这些内容将在接下来的章节中详细探讨。总体来说,对于每个软件组件,我们将执行以下操作:

  • 在环境中的虚拟机之间设置基本的网络连接

  • 创建一个独立的系统用户

  • 下载并安装软件

  • 创建支持文件和目录

  • 启动守护进程

首先,克隆本书的代码库:

git clone https://github.com/PacktPublishing/Hands-On-Infrastructure-Monitoring-with-Prometheus.git

进入新创建的目录并输入章节号,运行 Vagrant,不进行来宾实例的配置。这样你就会得到即开即用的虚拟机:

cd Hands-On-Infrastructure-Monitoring-with-Prometheus/chapter03
vagrant up --no-provision

所有来宾启动完成后,我们将继续一一配置我们的实例。

Prometheus

执行以下步骤:

  1. 登录到 Prometheus 来宾实例:
vagrant ssh prometheus
  1. 进入来宾实例,切换到 root 用户:
sudo -i
  1. 将所有访客的地址添加到实例主机的文件中:
cat <<EOF >/etc/hosts
127.0.0.1       localhost
192.168.42.10   prometheus.prom.inet    prometheus
192.168.42.11   grafana.prom.inet       grafana
192.168.42.12   alertmanager.prom.inet  alertmanager

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
  1. 创建一个新的系统用户:
useradd --system prometheus
  1. 进入 /tmp 并下载 Prometheus 压缩包:
cd /tmp
curl -sLO "https://github.com/prometheus/prometheus/releases/download/v2.9.2/prometheus-2.9.2.linux-amd64.tar.gz"
  1. 解压该压缩包:
tar zxvf prometheus-2.9.2.linux-amd64.tar.gz
  1. 将每个文件放在正确的位置:
install -m 0644 -D -t /usr/share/prometheus/consoles prometheus-2.9.2.linux-amd64/consoles/*

install -m 0644 -D -t /usr/share/prometheus/console_libraries prometheus-2.9.2.linux-amd64/console_libraries/*

install -m 0755 prometheus-2.9.2.linux-amd64/prometheus prometheus-2.9.2.linux-amd64/promtool /usr/bin/

install -d -o prometheus -g prometheus /var/lib/prometheus 

install -m 0644 -D /vagrant/chapter03/configs/prometheus/prometheus.yml /etc/prometheus/prometheus.yml

install -m 0644 -D /vagrant/chapter03/configs/prometheus/first_rules.yml /etc/prometheus/first_rules.yml
  1. 为 Prometheus 服务添加 systemd 单元文件:
install -m 0644 /vagrant/chapter03/configs/prometheus/prometheus.service /etc/systemd/system/

systemctl daemon-reload
  1. 启用并启动 Prometheus 服务:
systemctl enable prometheus
systemctl start prometheus

你现在应该可以在主机上访问 Prometheus HTTP 端点了。

  1. 退出 root 账户,再退出 Vagrant 用户账户:
exit

exit

Grafana

执行以下步骤:

  1. 登录到 Grafana 来宾实例:
vagrant ssh grafana
  1. 在客机实例内,切换到 root 用户:
sudo -i
  1. 将所有来宾的地址添加到实例主机的文件中:
cat <<EOF >/etc/hosts
127.0.0.1       localhost
192.168.42.10   prometheus.prom.inet    prometheus
192.168.42.11   grafana.prom.inet       grafana
192.168.42.12   alertmanager.prom.inet  alertmanager

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
  1. 进入 /tmp 并下载 Grafana 包:
cd /tmp
curl -sLO "https://dl.grafana.com/oss/release/grafana_6.1.6_amd64.deb"
  1. 安装包和所有依赖项:
DEBIAN_FRONTEND=noninteractive apt-get install -y libfontconfig

dpkg -i "grafana_6.1.6_amd64.deb"
  1. 将所有提供的配置文件放到正确的位置:
rsync -ru /vagrant/chapter03/configs/grafana/{dashboards,provisioning} /etc/grafana/
  1. 启用并启动 Grafana 服务:
systemctl daemon-reload
systemctl enable grafana-server
systemctl start grafana-server

你现在应该可以在主机上访问到 Grafana HTTP 端点。

  1. 退出 root 账户,然后退出 Vagrant 用户账户:
exit

exit

Alertmanager

执行以下步骤:

  1. 登录到 Alertmanager 客机实例:
vagrant ssh alertmanager
  1. 在客机实例内,切换到 root 用户:
sudo -i
  1. 将所有来宾的地址添加到实例主机的文件中:
cat <<EOF >/etc/hosts
127.0.0.1       localhost
192.168.42.10   prometheus.prom.inet    prometheus
192.168.42.11   grafana.prom.inet       grafana
192.168.42.12   alertmanager.prom.inet  alertmanager

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
  1. 创建一个新的系统用户:
useradd --system alertmanager
  1. 进入 /tmp 并下载 Alertmanager 归档文件:
cd /tmp
curl -sLO "https://github.com/prometheus/alertmanager/releases/download/v0.17.0/alertmanager-0.17.0.linux-amd64.tar.gz"
  1. 解压归档文件:
tar zxvf alertmanager-0.17.0.linux-amd64.tar.gz
  1. 将每个文件放到正确的位置:
install -m 0755 alertmanager-0.17.0.linux-amd64/{alertmanager,amtool} /vagrant/chapter03/configs/alertmanager/alertdump /usr/bin/

install -d -o alertmanager -g alertmanager /var/lib/alertmanager

install -m 0644 -D /vagrant/chapter03/configs/alertmanager/alertmanager.yml /etc/alertmanager/alertmanager.yml
  1. 为 Alertmanager 服务添加一个 systemd 单元文件:
install -m 0644 /vagrant/chapter03/configs/alertmanager/alertmanager.service /etc/systemd/system/

systemctl daemon-reload
  1. 启用并启动 Alertmanager 服务:
systemctl enable alertmanager
systemctl start alertmanager

你现在应该可以在主机上访问到 Alertmanager HTTP 端点。

  1. 退出 root 账户,然后退出 Vagrant 用户账户:
exit

exit

Node Exporter

为确保收集系统级别的指标,必须在所有三个虚拟机中安装 Node Exporter。要登录到每个虚拟机,请使用我们在前面章节中探讨的命令:

  1. 在客机实例内,切换到 root 用户:
sudo -i
  1. 创建一个新的系统用户:
useradd --system node_exporter
  1. 进入 /tmp 并下载 Node Exporter 归档文件:
cd /tmp
curl -sLO "https://github.com/prometheus/node_exporter/releases/download/v0.17.0/node_exporter-0.17.0.linux-amd64.tar.gz"
  1. 将每个文件放到正确的位置:
tar zxvf "node_exporter-0.17.0.linux-amd64.tar.gz" -C /usr/bin --strip-components=1 --wildcards */node_exporter
  1. 为 Node Exporter 服务添加一个 systemd 单元文件:
install -m 0644 /vagrant/chapter03/configs/node_exporter/node-exporter.service /etc/systemd/system/

systemctl daemon-reload
  1. 启用并启动 Node Exporter 服务:
systemctl enable node-exporter
systemctl start node-exporter
  1. 退出 root 账户,然后退出 Vagrant 用户账户:
exit

exit

验证你的测试环境

完成这些步骤后,你可以使用以下端点验证你的环境:

服务 端点
Prometheus http://192.168.42.10:9090
Grafana http://192.168.42.11:3000
Alertmanager http://192.168.42.12:9093

总结

有了测试环境后,你可以在不担心破坏系统的情况下检查、修改和验证配置。全书将广泛使用这种测试方法,因为没有什么比实验更能帮助学习新技能。

在下一章中,我们将介绍 Prometheus 指标的基本知识。我们刚刚搭建的测试环境将有助于展示这些指标。

问题

  1. 设置可重现的测试环境的推荐工具是什么?

  2. 如何更改测试环境中 Prometheus 组件的默认版本?

  3. 所有示例中使用的默认子网是什么?

  4. 从高层次来看,启动 Prometheus 实例的步骤有哪些?

  5. 每个来宾实例上都安装了 Node Exporter。你如何快速验证它们是否正确暴露指标?

  6. 在我们的测试环境中,在哪里可以找到警报日志?

  7. 如何从零开始创建一个干净的测试环境?

进一步阅读

第二部分:开始使用 Prometheus

在测试环境搭建完成后,接下来就可以深入探讨 Prometheus 堆栈。本节将介绍指标、配置和最佳实践。

本节包含以下章节:

  • 第四章,Prometheus 指标基础

  • 第五章,运行 Prometheus 服务器

  • 第六章,导出器和集成

  • 第七章,Prometheus 查询语言 – PromQL

  • 第八章,故障排除与验证

第四章:Prometheus 指标基础

指标是 Prometheus 堆栈获取并提供有用信息的核心资源。正确理解它们对于充分利用、管理甚至扩展该堆栈的可能性领域至关重要。从数据到信息,最终到知识,指标在这里帮助你。

简而言之,本章将涵盖以下主题:

  • 理解 Prometheus 数据模型

  • 四种核心指标类型的介绍

  • 横向和纵向聚合

理解 Prometheus 数据模型

为了理解 Prometheus 数据模型,我们需要了解什么构成了时间序列以及如何存储这种数据。这些概念在本书的学习过程中将非常宝贵。

时间序列数据

时间序列数据通常可以定义为从相同来源按时间顺序索引的数值数据点。在 Prometheus 的范围内,这些数据点是按照固定时间间隔收集的。因此,当以图形形式呈现这种数据时,最常见的做法是绘制数据随时间的变化,x 轴表示时间,y 轴表示数据值。

时间序列数据库

一切都始于需要收集、存储并查询随时间变化的度量。当处理来自收集器和传感器的大量数据(例如物联网组成部分)时,如果数据库没有为该用例设计,查询结果数据集将非常慢。虽然你可以使用标准的关系型或 NoSQL 数据库来存储时间序列数据,但性能损失和可扩展性问题应该让你在做出决定时深思。Prometheus 选择实现一个专门为其独特问题空间量身定制的时间序列数据库。

除了这些类型的数据库具有写入负载重的特点,这也意味着存储了大量的测量数据外,还需要理解的是,简单的查询可能跨越数小时、数天甚至数月,返回大量的数据点,但仍然期望能够合理快速地返回数据。

因此,现代时间序列数据库存储以下组件:

  • 时间戳

  • 一个值

  • 关于值的一些背景,编码在指标名称或关联的键/值对中

一个符合此时间序列数据库规范的抽象数据示例如下:

timestamp=1544978108, company=ACME, location=headquarters, beverage=coffee, value=40172

如你所见,这种数据可以很容易地存储在数据库中的单一表格里:

timestamp company location beverage value
1544978108 ACME headquarters coffee 40172

在这个简单的例子中,我们可以查看位于 ACME 公司总部的自动售货机提供的咖啡杯数。如果通过时间持续测量,这个例子就拥有时间序列所需的所有组成部分。

这个例子并没有直接映射到 Prometheus 数据模型,因为它还需要一个度量名称,但它展示了我们希望解决的逻辑。

Prometheus 本地存储

本地存储是 Prometheus 存储数据的标准方法,因此,我们必须理解它的基础知识。总体来看,Prometheus 存储设计结合了使用发布列表(posting lists)为当前存储的所有标签及其值实现索引的方式,以及它自己的时间序列数据格式。

数据流

Prometheus 存储收集数据的方式可以看作是一个三部分的过程。以下主题描述了数据在成功持久化之前所经历的各个阶段。

内存

最新一批数据会保存在内存中,最长可达两小时。这包括在两小时时间窗口内收集到的一个或多个数据块。这种方法大大减少了磁盘 I/O,最多可以减少两倍;最近的数据保存在内存中,查询非常快速;并且数据块是在内存中创建的,避免了持续的磁盘写入。

提前写日志

在内存中时,数据并未持久化,如果进程异常终止,数据可能会丢失。为了防止这种情况发生,磁盘上的 写前日志 (WAL) 会保存内存中数据的状态,以便在 Prometheus 因任何原因崩溃或重启时能够重放这些数据。

磁盘

在两小时的时间窗口结束后,这些数据块将被写入磁盘。这些数据块是不可变的,尽管数据可以被删除,但这不是一个原子操作。相反,墓碑文件(tombstone)会被创建,记录不再需要的数据。

布局

如我们在下面的示例中所看到的,数据在 Prometheus 中的存储方式被组织成一系列包含数据块、该数据的 LevelDB 索引、一个带有人类可读信息的 meta.json 文件以及记录不再需要数据的墓碑文件的目录(块)。每一个这样的块代表一个数据库。

在最顶层,你还可以看到那些尚未刷新到自己数据块中的数据的 WAL(写前日志):

...
├── 01CZMVW4CB6DCKK8Q33XY5ESQH
│   ├── chunks
│   │ └── 000001
│   ├── index
│   ├── meta.json
│   └── tombstones
├── 01CZNGF9G10R2P56R9G39NTSJE
│   ├── chunks
│   │ └── 000001
│   ├── index
│   ├── meta.json
│   └── tombstones
├── 01CZNGF9ST4ZNKNSZ4VTDVW8DH
│   ├── chunks
│   │ └── 000001
│   ├── index
│   ├── meta.json
│   └── tombstones
├── lock
└── wal
    ├── 00000114
    ├── 00000115
    ├── 00000116
    ├── 00000117
    └── checkpoint.000113
        └── 00000000

Prometheus 数据模型

正如我们目前所看到的,Prometheus 将数据存储为时间序列,这包括被称为标签的键值对、时间戳和最终的值。接下来的主题将扩展这些组件,并提供每个组件的基础知识,我们将在第七章,Prometheus 查询语言 - PromQL中深入使用这些知识。

标注

Prometheus 中的时间序列表示如下:

<metric_name>[{<label_1="value_1">,<label_N="value_N">}] <datapoint_numerical_value>

如你所见,数据表示为一个度量名称,后面可选地跟着一个或多个带有标签名称/值的集合,并且标签值放在大括号中,最后是度量的值。此外,一个样本也会带有精确到毫秒的时间戳。

度量名称

尽管这是一个实现细节,但指标名称实际上就是一个特殊标签的值,叫做"__name__"。因此,如果你有一个名为 "beverages_total" 的指标,内部表示为 "__name__=beverages_total"。请记住,被 "__" 包围的标签是 Prometheus 的内部标签,任何以 "__" 为前缀的标签仅在某些阶段的指标收集周期中可用。

标签(键/值)和指标名称的组合定义了时间序列的身份。

Prometheus 中的每个指标名称必须匹配以下正则表达式:

"[a-zA-Z_:][a-zA-Z0-9_:]*"

通俗来说,这意味着指标名称只允许使用英文字母的大小写(a-z)、下划线(_)、冒号(:)和阿拉伯数字(0-9),但首字符不能是数字。

冒号是保留给一种特殊类型的指标指定的记录规则的。我们将在另一章中详细展开这个话题。

指标标签

标签,或者与特定指标相关的键/值对,为指标添加了维度。这是 Prometheus 在切片和处理时间序列数据时如此出色的一个关键部分,正如我们在第七章中所见的,Prometheus 查询语言 – PromQL

虽然标签值可以是完整的 UTF-8 字符串,但标签名称必须符合正则表达式才能被认为是有效的;例如,"[a-zA-Z0-9_]*"

与指标名称相比,它们的主要区别在于标签名称不允许使用冒号(:)。

样本

样本是收集到的数据点,它们表示时间序列数据的数值。定义一个样本所需的组件是一个 float64 值和一个具有毫秒精度的时间戳。需要记住的是,顺序错乱的样本会被 Prometheus 丢弃。同样,具有相同指标身份和不同样本值的样本也会被丢弃。

基数

根据分配给 Prometheus 实例的计算资源(即 CPU、内存、磁盘空间和 IOPS),它将优雅地处理一定数量的时间序列。这个数字可以被视为该实例容量的主要指标,它将影响你的抓取决策:你会有成千上万个目标,每个目标的指标较少,还是较少的目标,每个目标有成千上万个指标,或者介于两者之间?最终,Prometheus 将仅能处理一定数量的时间序列,而不会导致性能下降。

在这个背景下,基数的概念出现了。这个术语通常用来表示由指标名称和其关联的标签名称/值的组合所产生的独特时间序列的数量。举个例子,一个没有额外维度(如标签)的指标,如果来自一个拥有一百个实例的应用程序,自然意味着 Prometheus 将存储 100 个时间序列,每个实例一个(这里的实例是应用程序外部添加的维度);另一个来自该应用程序的指标,如果有一个标签有十个可能的值,则会转化为 1,000 个时间序列(每个实例 10 个时间序列,乘以 100 个实例)。这表明基数是乘法性的——每增加一个维度,现有维度的每个值都会与新维度的每个值重复,从而增加产生的时间序列数。拥有多个维度且每个维度具有大量可能值的指标将导致 Prometheus 中所称的基数爆炸,即创建大量时间序列。

当标签值没有明确的限制,且可能会无限增加或有数百个可能值时,你也会遇到基数问题。这些指标可能更适合在基于日志的系统中处理。

以下是一些具有高或无限基数的数据示例,这些数据不应作为标签值(或在指标名称中使用):

  • 电子邮件地址

  • 用户名

  • 请求/进程/订单/交易 ID

四种核心指标类型的概述

Prometheus 指标分为四种主要类型:计数器、计量器、直方图和摘要。深入理解它们非常重要,因为 Prometheus 提供的大多数功能只有在特定数据类型下才能正常工作。因此,以下是对每种类型的概述。

计数器

这是一个严格的累积性指标,其值只能增加。唯一的例外是当指标被重置时,它会恢复为零。

这是最有用的指标类型之一,因为即使抓取失败,数据的累积增加也不会丢失,下次抓取时仍然可以获取到。需要明确的是,在抓取失败的情况下,由于保存的点较少,粒度会丢失。

为了帮助可视化这种类型的指标,以下是一些计数器及其基于我们在上一章创建的测试环境的图形表示示例:

  • Prometheus 实例接收到的总数据包数量:

  • Grafana 实例写入磁盘的总字节数——注意由于实例重启而导致的中间空隙,迫使计数器重置:

计量器

计量器是一种在收集时对给定测量值进行快照的指标,其值可以增加或减少(例如温度、磁盘空间和内存使用情况)。

如果抓取失败,你将丢失该样本,因为下次抓取可能会遇到不同值的度量(更高或更低)。

为了帮助可视化这种类型的度量,以下是基于我们在上一章创建的测试环境的一些仪表和其图形表示示例:

  • Alertmanager 实例上建立的 TCP 连接数:

  • Grafana 实例上的空闲内存量——注意由于实例重启导致的中间间隙,这使得在该期间无法对可能的值做出任何假设:

直方图

记录系统中每个事件固有的数值数据可能会很昂贵,因此通常需要某种形式的预聚合,以至少保留有关发生了什么的部分信息。然而,通过在每个实例上预计算聚合(例如,自进程启动以来的平均值、滚动窗口、指数加权等),会丢失大量的粒度,而且某些计算可能在计算上很昂贵。更糟糕的是,许多预聚合通常不能重新聚合,否则会失去其意义——例如,计算一千个预先计算的第 95 百分位数的平均值是没有统计意义的。类似地,从给定集群的每个实例收集的请求延迟的第 99 百分位数(例如)并不能指示整个集群的第 99 百分位数,也无法准确计算它。

直方图允许你通过将事件计入客户端可配置的桶中来保持一定的粒度,同时还提供所有观察值的总和。Prometheus 直方图为每个配置的桶生成一个时间序列,并额外生成两个时间序列,分别跟踪观察到的事件的总和和计数。此外,Prometheus 中的直方图是累积的,这意味着每个桶将包含前一个桶的值,加上其自身事件的数量。这样做是为了在不丢失使用直方图的整体能力的情况下,可以出于性能或存储的原因丢弃一些桶。

使用直方图的缺点是所选的桶需要适应预期收集的值的范围和分布。分位数计算的误差范围将与此适配直接相关:选择太少或不当的桶将增加分位数计算的误差范围。

这种类型的度量特别适用于跟踪分桶延迟和大小(例如,请求持续时间或响应大小),因为它可以在不同维度之间自由聚合。另一个很好的应用是生成热图(直方图随时间的演变)。

为了帮助可视化这种类型的度量,以下是基于我们在上一章创建的测试环境的直方图及其图形表示示例:

  • Prometheus HTTP 请求持续时间(单位:秒),按桶划分。这在 Grafana 热力图中显示,以更好地说明桶的概念:

摘要

摘要在某些方面类似于直方图,但呈现了不同的权衡,并且通常不那么有用。它们也用于跟踪大小和延迟,并提供观察到的事件的总和和计数。此外(如果使用的客户端库支持),摘要还可以提供在预定滑动时间窗口上预先计算的分位数。使用摘要分位数的主要原因是,当需要准确的分位数估算时,无论观察到的事件的分布和范围如何。

Prometheus 中的分位数被称为 φ-分位数,其中 0 ≤ φ ≤ 1。

分位数和滑动窗口大小都在仪表代码中定义,因此无法根据需要计算其他分位数或窗口大小。在客户端进行这些计算也意味着仪表和计算成本会更高。最后需要提到的缺点是,计算出来的分位数是不可聚合的,因此其实用性有限。

摘要的一个好处是,在没有分位数的情况下,它们生成、收集和存储的成本相对较低。

为了帮助可视化这种类型的度量,以下是一个摘要及其图形表示,基于我们在上一章创建的测试环境:

  • Prometheus 规则组的最大持续时间(单位:秒)按分位数划分:

纵向和横向聚合

在思考时间序列时,最后一个需要掌握的概念是聚合如何在抽象层面上工作。Prometheus 的核心优势之一是,它使得时间序列数据的操作变得简单,这种对数据的切片与切分通常归结为两种聚合,且通常是一起使用的:纵向聚合和横向聚合。

在时间序列的背景下,聚合是一个减少或总结原始数据的过程,也就是说,它接收一组数据点作为输入,并输出一个较小的集合(通常是一个单一元素)。在时间序列数据库中,一些最常见的聚合函数包括最小值、最大值、平均值、计数和总和。

为了更好地理解这些聚合是如何工作的,我们来看看一些数据,使用本章前面介绍的示例时间序列。需要明确的是,接下来的几节将解释这些聚合是如何在抽象层面上工作的,并暗示它们在 Prometheus 中的对应关系,但这些内容并不打算与 PromQL(我们将在第七章中深入探讨,Prometheus 查询语言 – PromQL)一一对应。

假设我们选择了 {company=ACME, beverage=coffee},现在我们正在查看每个位置上随时间变化的原始计数器。数据大概是这样的:

位置/时间 t=0 t=1 t=2 t=3 t=4 t=5 t=6
工厂 1,045 1 2 3 4 5 6
仓库 223 223 223 223 224 224 224
总部 40,160 40,162 40,164 40,166 40,168 40,170 40,172

实际数据不会完全像这样,因为每个时间序列是略微不同时间点收集的。数据点会有各自的时间戳,这意味着它们会错开对齐。这反过来会影响聚合的结果,因为会应用某种插值方法来对齐数据点。

假设每分钟收集一次样本。度量类型可能是计数器,因为它是单调递增的,除了在 t=1 时重置的计数器(对于 location=factory)。

横向聚合

横向聚合是最容易理解的。正如我们在以下数据表示中看到的那样,我们从一列数据中应用聚合函数:

位置/时间 t=0 t=1 t=2 t=3 t=4 t=5 t=6
工厂 1,045 1 2 3 4 5 6
仓库 223 223 223 223 224 224 224
总部 40160 40,162 40,164 40,166 40,168 40,170 40,172

如果我们应用 max() 聚合,我们可以找出报告更多咖啡分发量的地点——在这种情况下,结果将是 40,172。应用 count() 将告诉我们报告数据的办公室数量,针对所选择的维度({company=ACME, beverage=coffee}):3。

一般来说,应用 max() 到计数器上是不合理的,正如我们在第七章中看到的,Prometheus 查询语言 – PromQL。这是一个简单的抽象示例,帮助你理解时间序列的基础。

这种类型的聚合通常应用于请求集中最后的数据点。唯一通常例外的情况是当绘制时间序列聚合图时,因为它需要对图中的每个点进行计算。

你会注意到,所选数据类似于线性代数中的传统列向量。正如我们在第七章中看到的,Prometheus 查询语言 – PromQL,专门讨论 PromQL,这些将被称为瞬时向量。

纵向聚合

纵向聚合的使用较为复杂,因为你需要选择一个时间窗口来进行聚合。这意味着它们是按行工作的,正如我们在以下表示中看到的那样:

位置/时间 t=0 t=1 t=2 t=3 t=4 t=5 t=6
工厂 1,045 1 2 3 4 5 6
仓库 223 223 223 223 224 224 224
总部 40,160 40,162 40,164 40,166 40,168 40,170 40,172

由于当前我们使用的选择器返回三行数据,这意味着应用纵向聚合时,我们将得到三个结果。在这个例子中,我们选择了最后三分钟的数据进行聚合(正如前面所提到的,我们考虑的是 1 分钟的样本间隔)。如果我们对时间应用 max() 聚合,因为这些是计数器且在选择的时间窗口内没有重置,我们将得到所选集合中的最新值:location=factory 为 6,location=warehouse224location=headquarters40,172count() 将返回在指定时间范围内选择的点数——在这个例子中,由于收集发生在每分钟一次,我们请求了三分钟的数据,它将为每个位置返回 3

之前未提到的一种更有趣的聚合方法是 rate()。这是一个特别适用于计数器的聚合方法,因为你可以计算每单位时间的变化速率——我们将在本书后面详细探讨这一点。在这个例子中,rate() 将分别返回每个位置的 1、0 和 2。

我们再次指出,所选数据与数学中传统矩阵的相似性。这类选择将被称为 PromQL 中的范围向量(range vectors)。

总结

在本章中,我们了解了什么是时间序列数据,并概述了现代时间序列数据库(如 Prometheus)如何工作,不仅是逻辑上,还有物理层面。我们回顾了 Prometheus 的度量符号,度量名称和标签之间的关系,并讨论了什么定义了一个样本。Prometheus 的度量有四种类型,我们有机会逐一了解并提供了一些有用的示例。最后,我们深入探讨了纵向和横截面聚合如何工作,这是充分利用 Prometheus 查询语言的关键。

在下一章中,我们将回到更实用的方法,探讨 Prometheus 服务器的配置,以及如何在虚拟机和 Kubernetes 上管理它。

问题

  1. 任何图形化时间序列表示的强制要求是什么?

  2. 一个数据点要被视为时间序列数据,它的组成部分是什么?

  3. 当 Prometheus 服务器崩溃时,是什么防止它丢失数据?

  4. Prometheus 内存数据库存储数据的时间窗口是多少?

  5. Prometheus 样本的组成部分是什么?

  6. 直方图和摘要的常见用例是什么?

  7. 横截面聚合和纵向聚合有什么区别?

进一步阅读

第五章:运行 Prometheus 服务器

现在是时候动手配置一些 Prometheus 配置了。本章将探索这一堆栈的核心组件,你将了解常见的使用模式和在虚拟机及容器下的完整设置过程。这将帮助你真正验证你迄今为止所学的知识,并为你提供实际的示例来测试你的知识。

简要来说,本章将涵盖以下主题:

  • 深入探索 Prometheus 配置

  • 在独立服务器上管理 Prometheus

  • 在 Kubernetes 中管理 Prometheus

深入探索 Prometheus 配置

Prometheus 的一个关键特性是,得益于其极为合理的默认配置,它可以从本地计算机上运行的快速测试,扩展到生产级实例,处理每秒数百万个样本,而几乎不需要触动它的任何一个设置选项。话虽如此,了解有哪些配置选项可用,能够帮助你充分利用 Prometheus 的功能,这一点非常有用。

Prometheus 服务器有两种主要的配置类型——命令行标志和通过配置文件提供的操作逻辑。命令行标志控制那些无法在运行时更改的参数,例如存储路径或绑定的 TCP 端口,并且需要完全重启服务器才能应用此级别所做的任何更改。配置文件控制运行时配置,例如抓取作业定义、规则文件位置或远程存储设置。在接下来的章节中,我们将深入探讨这两种配置类型。

Prometheus 启动配置

虽然在没有启动配置的情况下运行 Prometheus 服务器对于本地实例可能足够,但对于任何严肃的部署,建议配置一些基本的命令行标志。

在撰写本文时,Prometheus 大约有 30 个命令行标志,用于调整其操作配置的多个方面,这些标志按以下命名空间分组:configwebstoragerulesalertmanagerquerylog--help 标志在描述大部分选项时做得很好,但有些地方可能简短一些,因此我们将重点介绍那些对于任何部署都很重要或其功能不太显而易见的选项。

配置部分

通常首先需要设置的重要内容是 Prometheus 配置文件的路径,可以通过 --config.file 标志来指定。默认情况下,Prometheus 会在当前工作目录中查找名为 prometheus.yml 的文件。虽然这对于本地测试很方便,但生产环境的部署通常会将服务器二进制文件和配置文件放置在各自的路径中,因此通常需要使用这个标志。顺便提一下,配置文件和存储目录是启动 Prometheus 服务器的唯一硬性要求;如果没有配置文件,Prometheus 会拒绝启动。

存储部分

按照上一部分相同的逻辑,--storage.tsdb.path 标志应该被设置来配置数据存储位置的基础路径。默认情况下,它指向当前工作目录中的 data/,因此建议将其指向更合适的路径——可能是不同的驱动器/卷,在那里数据可以安全地持久化并且减少 I/O 竞争。需要注意的是,不支持 NFS(包括 AWS EFS),因为它不支持进行安全数据库文件管理所需的 POSIX 锁定原语。将 Prometheus 数据存储目录放在网络共享上也是不推荐的,因为暂时的网络故障会影响监控系统的正常运行——尤其是在你最需要它的时候。

Prometheus 本地存储一次只能由一个 Prometheus 实例进行写入。为了确保这一点,它在数据目录中使用锁文件。在启动时,它会尝试使用操作系统特定的系统调用来锁定此文件,如果文件已被另一个进程锁定,则会拒绝启动。

这种行为可能存在一个边缘情况;当使用持久化卷存储数据目录时,如果通过相同的卷重新启动 Prometheus 实例作为另一个容器实例,可能会出现前一个实例没有解锁数据库的情况。这种问题会使这种配置容易受到竞争条件的影响。幸运的是,存在 --storage.tsdb.no-lockfile 标志,可以在这种情况下使用。不过需要警告的是,一般来说(特别是在大多数 Prometheus 部署中),禁用锁文件是个坏主意,因为这样会更容易导致意外的数据损坏。

Web 部分

下一步是配置用户将使用什么地址来访问 Prometheus 服务器。--web.external-url 标志设置此基础 URL,以便在 web 用户界面和外发警报中生成的 web 链接能够正确地指向 Prometheus 服务器或多个服务器。这可能是负载均衡器/反向代理的 DNS 名称,一个 Kubernetes 服务,或者在最简单的部署中,运行服务器的主机的公共可访问完全合格域名。为了完整性,并且如官方文档中所述,当 Prometheus 位于某个具有内容切换的七层反向代理后面时(也称为基于位置的切换或 URL 前缀路由),也可以在此提供一个 URL 路径。

Prometheus 服务器像传统的*nix守护进程一样,当收到SIGHUP信号时,会重新加载其配置文件(以及规则文件)。然而,有些情况下发送此信号并不方便(例如,在 Kubernetes 等容器编排系统中运行,或者使用自定义构建的自动化工具),甚至可能不可能(例如,在 Windows 上运行 Prometheus 时)。在这些情况下,可以使用--web.enable-lifecycle标志来启用/-/reload/-/quit这两个 HTTP 端点,用于控制、重新加载和关闭 Prometheus。为了防止意外触发这些端点,并且因为GET请求在语义上不合适,必须使用POST请求。此标志默认关闭,因为对这些端点的无限制访问可能带来安全隐患。

同样,--web.enable-admin-api标志默认也是关闭的,原因相同。此标志启用提供一些高级管理操作的 HTTP 端点,例如创建数据快照、删除时间序列和清理墓碑数据。

正如你在第三章《设置测试环境》中可能已经注意到,官方的 Prometheus tarball 还带有两个额外的目录,consolesconsole_libraries。这些是启用 Prometheus 本地仪表盘功能所需的,然而这一点常常被忽视。这些目录包含一些预配置的仪表盘(称为控制台)和支持模板库,这些都是用 Go 模板语言编写的。可以通过使用--web.console.templates--web.console.libraries标志来配置 Prometheus 加载这些内容。之后,这些仪表盘将可在/consoles端点访问(如果存在 index.html 文件,主 Web 界面中将提供一个链接)。

查询部分

本节内容主要讲解如何调整查询引擎的内部工作机制。某些调整比较简单易懂,例如指定查询在被终止之前能够运行多长时间(--query.timeout),或者能够同时运行多少个查询(--query.max-concurrency)。

然而,有两个参数设置的限制可能会产生不明显的后果。第一个是--query.max-samples,它在 Prometheus 2.5.0 版本中引入,用于设置可以加载到内存中的最大样本数量。这个设置是为了限制查询子系统能够使用的最大内存(与--query.max-concurrency一起使用),以避免出现致命查询——一个将大量数据加载到内存中,导致 Prometheus 达到内存限制并终止进程的查询。2.5.0 版本之后的行为是,如果某个查询触及了该标志所设置的限制(默认是 50,000,000 个样本),查询会直接失败。

第二个配置项是--query.lookback-delta。在不详细解释 PromQL 内部工作原理的前提下,这个标志设置了 Prometheus 在考虑数据点过时之前,回溯查询时间序列数据的时间限制。这意味着,如果你的数据收集间隔比此处设置的更长(默认值为五分钟),你将在警报和图表中得到不一致的结果,因此,允许失败的最大合理值为两分钟。

Prometheus 配置文件解析

我们在前一节中提到的配置文件声明了 Prometheus 实例的运行时配置。如我们所见,所有与抓取任务、规则评估以及远程读写配置相关的内容都在此定义。正如前面提到的,这些配置可以在不关闭 Prometheus 服务器的情况下重新加载,可以通过向进程发送SIGHUP信号,或者向/-/reload端点发送 HTTP POST 请求来实现(当启动时使用了--web.enable-lifecycle)。

从高层次看,我们可以将配置文件划分为以下几个部分:

  • global

  • scrape_configs

  • alerting

  • rule_files

  • remote_read

  • remote_write

再次提醒,官方的 Prometheus 文档包括了该文件的架构,该文件采用 YAML 格式。在本章中,我们将介绍一个示例配置供大家分析,但只详细讲解globalscrape_configs部分。警报和rule_files在第九章《定义警报和记录规则》中有详细讨论,而remote_readremote_write则在第十四章《将长期存储与 Prometheus 集成》中有解释。

一个包含最全配置选项的配置文件可以在 Prometheus 项目的 GitHub 仓库中找到,地址为:github.com/prometheus/prometheus/blob/v2.9.2/config/testdata/conf.good.yml

我们的示例配置如下所示:

global:
  scrape_interval: 1m
...
scrape_configs:
  - job_name: 'prometheus'
    scrape_interval: 15s
    scrape_timeout: 5s
    sample_limit: 1000
    static_configs:
      - targets: ['localhost:9090']
    metric_relabel_configs:
      - source_labels: [ __name__ ]
        regex: expensive_metric_.+
        action: drop

初看之下,它可能显得有些复杂,但为了清晰起见,我们正在进行一些配置,这些配置的默认值通常不需要修改。

让我们逐个详细查看每个部分。

全局配置

global配置定义了每个其他配置部分的默认参数,同时概述了应该添加到外部系统度量值的标签,如下所示的代码块所示:

global:
  scrape_interval: 1m
  scrape_timeout: 10s
  evaluation_interval: 1m
  external_labels:
    dc: dc1
    prom: prom1

持续时间只能是整数值,并且只能有一个单位。这意味着,如果试图使用 0.5 分钟而不是 30 秒,或用一分钟 30 秒而不是 90 秒,将被视为配置错误。

scrape_interval 设置目标被抓取的默认频率。通常在 10 秒到 1 分钟之间,默认的 1m 是一个很好的保守值。较长的间隔不建议使用,因为丧失的粒度(尤其是在仪表类指标中)开始影响正确告警的能力,并且查询会变得麻烦,因为你需要意识到某些较短的间隔可能不会返回数据。此外,考虑到默认的 5 分钟回环差值(在命令行标志中提到),任何大于 150 秒(2 分钟 30 秒)的 scrape_interval 都意味着如果一次抓取失败,那么给定目标的每个时间序列都会被视为过期。

scrape_timeout 定义了 Prometheus 默认等待目标响应的时间,超过该时间则关闭连接并将抓取标记为失败(如果未声明,则默认为 10 秒)。请记住,尽管预期目标能快速响应抓取请求,但关于指标暴露的指导原则要求在抓取时进行数据收集,而非使用缓存,这意味着某些导出程序可能需要稍长时间来响应。

类似于 scrape_intervalevaluation_interval 设置记录和告警规则评估的默认频率。为了确保一致性,两者应当相同。关于这一点将在第九章《定义告警和记录规则》中详细讨论:

图 5.1:Prometheus 中抓取间隔和评估间隔的表示

最后,external_labels 允许你设置标签名/值对,这些标签会添加到发送到外部系统的时间序列或警报中,例如 Alertmanager、远程读写基础设施,甚至是通过联合(federation)连接的其他 Prometheus 实例。此功能通常用于唯一标识给定警报或时间序列的来源;因此,通常会标识区域、数据中心、分片,甚至是 Prometheus 服务器的实例标识符。

根据官方文档,Prometheus 的复数形式是 Prometheisprometheus.io/docs/introduction/faq/#what-is-the-plural-of-prometheus

抓取配置

尽管 Prometheus 将空文件视为有效的配置文件,但最基本的有效配置需要一个 scrape_configs 部分。在这里,我们定义用于指标收集的目标,以及在实际数据摄取之前是否需要进行一些抓取后处理。

在之前介绍的配置示例中,我们定义了两个抓取任务:prometheusblackbox。在 Prometheus 的术语中,抓取是指通过 HTTP 请求从目标实例收集指标,解析响应,并将收集的样本存入存储。Prometheus 生态系统中用于指标收集的默认 HTTP 端点恰当地命名为 /metrics

这样的实例集合被称为任务。任务中的实例通常是相同服务的多个运行副本,因此通常会为每种被监控的软件定义一个任务,即使在使用服务发现时也可能有所不同,正如我们在第十二章《选择正确的服务发现》中将看到的那样。实例和任务的组合标识了收集样本的来源,因此这些会自动作为标签添加到摄取的数据中,如下代码块所示:

scrape_configs:
 - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']
...

  - job_name: 'blackbox'
    static_configs:
      - targets:
        - http://example.com
        - https://example.com:443
...

一个抓取任务定义至少需要一个 job_name 和一组目标。在此示例中,static_configs 被用来声明两个抓取任务的目标列表。虽然 Prometheus 支持多种动态定义该列表的方法,但 static_configs 是最简单和直接的方法:

scrape_configs:
 - job_name: 'prometheus'
    scrape_interval: 15s
    scrape_timeout: 5s
    sample_limit: 1000
    static_configs:
      - targets: ['localhost:9090']
    metric_relabel_configs:
      - source_labels: [ __name__ ]
        regex: expensive_metric_.+
        action: drop

详细分析 prometheus 抓取任务后,我们可以看到 scrape_intervalscrape_timeout 都可以在任务级别重新声明,从而覆盖全局值。如前所述,不推荐使用不同的抓取间隔,因此仅在绝对必要时使用此功能。

通过设置 sample_limit,Prometheus 将确保不论设置了什么值,它都会在每次抓取时检查,如果样本数量超过限制,则不摄取这些样本,并将该次抓取标记为失败。这是一个很好的安全机制,可以防止来自不可控目标的基数爆炸影响监控系统。

这里最后一个相关的配置是 metric_relabel_configs。这是一个强大的重写引擎,允许在保存到存储之前转换或丢弃收集到的指标的身份。此功能最常见的使用场景是将一组表现不佳的指标列入黑名单,丢弃标签而不影响指标的身份,或者更改标签以更好地符合 Prometheus 的语义。理想情况下,metric_relabel_configs 应该作为在源头问题未解决之前的临时解决方法,因此频繁使用它可能是一个警示信号。前面的示例使用 metric_relabel_configs 来丢弃每个以 expensive_metric_ 开头的指标:

  - job_name: 'blackbox'
    metrics_path: /probe
    scheme: http
    params:
      module: [http_2xx]
    static_configs:
      - targets:
        - http://example.com
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 127.0.0.1:9115

虽然我们将在下一章深入探讨 blackbox exporter,但这里的配置用于帮助解释以下重要配置:

  • metrics_path 用于更改 Prometheus 应该抓取的端点

  • scheme 定义了连接目标时是使用 HTTP 还是 HTTPS

  • params 允许你定义一组可选的 HTTP 参数

然而,最重要且最有用的配置是 relabel_configs。它提供与 metric_relabel_configs 相同的强大语义,但功能完全不同;relabel_configs 用于操作抓取任务的目标列表。重新标记操作是按顺序执行的,因此可以创建或修改标签,然后在下一个操作中使用这些标签。默认情况下,目标将具有几个自动生成的标签,这些标签将可供重新标记使用:job 标签将设置为 job_name 配置,__address__ 标签将使用目标的主机和端口生成,__scheme____metrics_path__ 标签将设置为各自的配置(schememetrics_path),并且会为 params 配置中定义的每个参数创建一个 __param_<name> 标签。此外,当使用服务发现机制时,__meta_ 标签将可用,正如我们在第十二章 选择合适的服务发现中看到的那样。如果在重新标记阶段结束时 instance 标签未设置,则将使用 __address__ 来设置它。以两个下划线开头的标签(__)将在重新标记阶段结束时被删除。最后,如果在重新标记过程中需要临时标签,始终使用 __tmp 前缀,因为它保证不会与 Prometheus 内部标签重叠。

对于黑盒导出器,这个功能非常有用,因为我们需要将探测请求发送到导出器,导出器将使用 targetGET 参数来执行其工作。因此,通过这个示例,对于 static_configs 中指定的每个目标,此配置执行以下操作:

  • 将目标的地址复制到 __param_target 标签中,该标签将用于设置抓取中的 target GET 参数。

  • 将新创建的标签的内容复制到 instance 标签中,以便明确设置它,绕过基于 __address__ 的自动生成。

  • 用黑盒导出器的地址替换 __address__ 标签,以便抓取任务被定向到导出器,而不是直接定向到我们在 `static_configs` 中指定的目标。

relabel_configs 用于重写目标列表(它在抓取之前运行),而 metric_relabel_configs 用于重写标签或删除样本(它在抓取之后运行)。

本节中使用的示例配置仅用于演示目的。例如,通常不需要在 Prometheus 本身上设置 sample_limit,或者在没有明确理由的情况下删除度量指标。

一个非常有用的度量是 up 度量。这个度量暴露了抓取任务的状态。它至少包括一个与相应任务名称的标签和另一个与目标实例的标签。在其样本中,成功抓取时我们可以看到值 1,而抓取失败时则为 0

接下来,我们将开始在不同的部署环境中管理 Prometheus。

在独立服务器上管理 Prometheus

由于我们之前讨论了多个配置定义,现在我们准备通过管理 Prometheus 的独立实例来将它们付诸实践。在这些示例中,我们将展示多个配置,同时提供一个环境来验证它们。

服务器部署

要创建一个新的 Prometheus 实例,请移动到正确的仓库路径,如下所示:

cd chapter05/

确保没有其他测试环境正在运行,然后启动本章的环境,如下所示:

vagrant global-status
vagrant up

几秒钟后,新的实例将可供检查,并且 Prometheus Web 界面可以通过http://192.168.42.10:9090访问。

配置检查

创建新的实例并运行后,使用以下命令登录:

vagrant ssh prometheus

我们可以通过以下命令查看systemd单元文件来验证当前使用的启动配置:

cat /etc/systemd/system/prometheus.service

以下摘录显示了当前的标志设置:

ExecStart=/usr/bin/prometheus \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.path=/var/lib/prometheus/data \
    --web.console.templates=/usr/share/prometheus/consoles \
    --web.console.libraries=/usr/share/prometheus/console_libraries

Prometheus 本身的配置文件,如--config.file标志所定义,可以按如下方式查看:

cat /etc/prometheus/prometheus.yml

如我们所见,当前使用的配置与之前在 Prometheus 配置文件演示中展示的配置类似。我们现在可以验证之前提到的一些概念。

由于prometheus任务中的metric_relabel_configs,我们可以使用 Prometheus 的两个按抓取的指标来确定我们的配置丢弃的样本数量,如下所示:

  • scrape_samples_scraped:此指标提供已收集样本的总数

  • scrape_samples_post_metric_relabeling:此指标提供在指标重标记发生后可用的样本总数

如果我们减去这两个指标,我们就可以得到丢弃的样本数量(在我们的示例中,这些是所有以go_开头的指标名称):

图 5.2:丢弃的指标数量

我们可以确认配置重标记的结果,在我们的示例中,它生成了blackbox任务下的实例标签:

图 5.3:由relabel_configs生成的实例标签

您可以通过使用提供的工具promtool来验证 Prometheus 配置,它将在第八章中详细分析,故障排除与验证。当使用新配置重新加载 Prometheus 时,您还可以查看prometheus_config_last_reload_successful指标,评估配置是否已成功解析并应用。

清理

完成测试后,只需确保您位于chapter05/路径下并执行以下命令:

vagrant destroy -f

不必过于担心——如果需要,您可以轻松重新启动环境。

在 Kubernetes 中管理 Prometheus

Kubernetes 是首个从 CNCF 毕业的项目,目前是容器编排的事实标准。早期,Heapster 被广泛用作与 Kubernetes 一起开箱即用的监控解决方案。最初,它是一个将监控数据发送到外部系统的工具,但后来它发展成了一个监控系统。然而,Prometheus 很快成为 Kubernetes 集群的事实标准监控系统。如今,构成 Kubernetes 集群的大多数组件都内置了 Prometheus 监控功能。

在接下来的部分中,我们将探讨如何通过基于 Kubernetes 项目和 Prometheus Operator 项目的示例,将 Prometheus 集成到 Kubernetes 环境中。

你可以分别在以下地址找到 Kubernetes 项目和 Prometheus Operator 的完整源代码:

github.com/kubernetes/kubernetesgithub.com/coreos/prometheus-operator

确保你已经具备第三章中定义的所有软件要求,设置测试环境,并确保它们是特定版本,特别是以下内容:

  • Minikube

  • kubectl

静态配置

尽管这种方法远非推荐,但它为更好地理解和排除 Kubernetes 中运行的 Prometheus 服务器的故障提供了基础。在此示例中,我们将创建一个 Prometheus 部署,使用 ConfigMap 定义服务器配置。

Kubernetes 环境

确保没有minikube实例在运行以下命令:

minikube status
minikube delete

minikube delete是一个破坏性指令,所以在继续之前,请确保保存你的工作。

使用以下规格启动一个新的minikube实例:

minikube start \
 --cpus=2 \
 --memory=2048 \
 --kubernetes-version="v1.14.0" \
 --vm-driver=virtualbox

当上一个命令完成后,一个新的 Kubernetes 环境应该已经准备好使用。你可以通过以下命令访问其仪表盘,该命令将在默认浏览器中打开 Kubernetes 仪表盘地址:

minikube dashboard

要继续我们的示例,请确保进入正确的仓库路径,如下所示:

cd chapter05/provision/kubernetes/static

为了组织结构,我们将使用kubectl帮助创建一个新的命名空间,命名为monitoring,并使用以下清单:

apiVersion: v1
kind: Namespace
metadata:
 name: monitoring

使用以下命令应用上面的清单:

kubectl apply -f monitoring-namespace.yaml

我们可以在 Kubernetes 仪表盘上验证命名空间的成功创建:

图 5.4:Kubernetes 仪表盘 - 监控命名空间

Prometheus 服务器部署

在我们的新命名空间可用之后,是时候创建一个非常简单的 Prometheus 配置,并使用以下清单将其保存到 ConfigMap 中:

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring 
data:
  prometheus.yml: |
    scrape_configs:
    - job_name: prometheus
      static_configs:
      - targets:
        - localhost:9090

使用以下命令应用上面的清单:

kubectl apply -f prometheus-configmap.yaml

现在,是时候启动 Prometheus 的新部署了,确保我们将之前配置的 ConfigMap 挂载到我们正在部署的 pod 中。Deployment 对象配置如下元数据:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-deployment
  namespace: monitoring
  labels:
    app: prometheus

Prometheus 容器将根据其配置文件和来自卷挂载的数据目录启动,如下所示:

        args:
          - --config.file=/etc/config/prometheus.yml
          - --storage.tsdb.path=/data
        volumeMounts:
          - name: config-volume
            mountPath: /etc/config/prometheus.yml
            subPath: prometheus.yml
          - name: prometheus-data
            mountPath: /data
            subPath: ""

config-volume 卷是从 ConfigMap 创建的,而 prometheus-data 卷是通过空目录创建的。这可以从以下代码片段中看到:

      volumes:
        - name: config-volume
          configMap:
           name: prometheus-config
        - name: prometheus-data
          emptyDir: {}

使用以下命令应用之前的清单:

kubectl apply -f prometheus-deployment.yaml

我们可以使用这个代码片段跟踪部署状态:

kubectl rollout status deployment/prometheus-deployment -n monitoring

我们应该使用以下命令查看 Prometheus 实例的日志:

kubectl logs --tail=20 -n monitoring -l app=prometheus

部署成功后,我们准备好为我们的实例分配一个新服务,选择 NodePort,这样我们就可以无需端口转发直接访问它,如下所示:

kind: Service
apiVersion: v1
metadata:
  name: prometheus-service
  namespace: monitoring
spec:
  selector:
    app: prometheus
  type: NodePort
  ports:
  - name: prometheus-port
    protocol: TCP
    port: 9090
    targetPort: 9090

使用以下命令应用之前的清单:

kubectl apply -f prometheus-service.yaml

然后你可以使用以下代码片段检查你的新 Prometheus 服务:

minikube service prometheus-service -n monitoring

这将打开浏览器并指向 Prometheus 服务端点。你现在可以通过 Prometheus 的 web 界面查看正在运行的配置和目标:

图 5.5: Prometheus 初始配置

现在我们已经在 Kubernetes 中运行了 Prometheus,可以开始为其添加目标进行抓取。在接下来的章节中,我们将看看如何实现这一目标。

将目标添加到 Prometheus

为了举例说明,我们将部署另一个服务,并逐步演示如何将其添加到我们的 Prometheus 服务器中。我们将使用一个小型的 Hello World 类型应用程序,名为 Hey,来进行设置。

Hey 应用程序的代码可以在 github.com/kintoandar/hey 上查看。

这些步骤与 Prometheus 服务器的部署非常相似。首先使用以下清单创建一个新的 Hey 部署:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hey-deployment
  namespace: monitoring
  labels:
    app: hey
...
      - name: hey
        image: kintoandar/hey:v1.0.1
...
        - name: http
          containerPort: 8000
...

使用以下命令应用之前的清单:

kubectl apply -f hey-deployment.yaml

我们可以使用这个代码片段跟踪部署状态:

kubectl rollout status deployment/hey-deployment -n monitoring

我们可以使用以下命令验证 Hey 实例的日志:

kubectl logs --tail=20 -n monitoring -l app=hey

部署成功后,我们准备好为我们的实例分配一个新服务,选择 NodePort,这样我们就可以无需端口转发直接访问它,如下所示:

kind: Service
apiVersion: v1
metadata:
  name: hey-service
  namespace: monitoring
spec:
  selector:
    app: hey
  type: NodePort
  ports:
  - name: hey-port
    protocol: TCP
    port: 8000
    targetPort: 8000

使用以下命令应用之前的清单:

kubectl apply -f hey-service.yaml

现在,你可以这样检查你的新 Hey 服务:

minikube service hey-service -n monitoring

由于在我们的示例中 Prometheus 是静态管理的,我们需要为新的 Hey 目标添加指标收集。这意味着我们需要更改 Prometheus 的 ConfigMap 以反映新增的服务,如下所示:

kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring 
data:
  prometheus.yml: |
    scrape_configs:
    - job_name: prometheus
      static_configs:
      - targets:
        - localhost:9090
    - job_name: hey
      static_configs:
      - targets:
        - hey-service.monitoring.svc:8000

使用以下命令应用之前的清单:

kubectl create -f prometheus-configmap-update.yaml -o yaml --dry-run | kubectl apply -f -

如果你检查正在运行的 Prometheus 配置,你会发现没有变化;这是因为没有触发新的部署。为了使部署生效,需要更改部署定义,因此我们只需更改版本注释并应用新的清单,如下所示:

kubectl apply -f prometheus-deployment-update.yaml

我们可以使用以下命令跟踪部署状态:

kubectl rollout status deployment/prometheus-deployment -n monitoring

片刻后,将会进行一次新的部署,改变 Prometheus 配置并出现一个新的目标,你可以在 Prometheus 的 Web 用户界面中验证这一点:

图 5.6:Prometheus 定位到 Hey 应用

你可能已经注意到,在这个示例中并未要求配置 基于角色的访问控制 (RBAC)。这是因为所有 pod 都运行在同一个命名空间,且 Prometheus 尚未需要访问 Kubernetes API。我们坚信,RBAC 是安全的 Kubernetes 设置中的基础。

动态配置 – Prometheus Operator

CoreOS 是构建名为 Operator 的模式的先驱,Operator 抽象了 Kubernetes 应用的打包、部署和管理的复杂性。Operator 将应用所需的操作知识(如配置和部署逻辑)合成到 Kubernetes 自定义资源和自定义控制器中。

自定义资源是扩展 Kubernetes API 的对象,允许自定义 API 定义。自定义控制器旨在实现资源的用户所需状态,持续保持该状态。

将 Kubernetes 自定义资源和自定义控制器结合成模式,就是让 Operator 定义得以实现的关键。

在实现这种模式时,用户不需要为每个示例定义持久化存储和特定环境配置,而是直接请求该应用程序的一个实例,Operator 会抽象出所有所需的依赖项,并自动提供最终结果。

在我们的例子中,除了管理部署,包括 Prometheus 服务器的 pod 数量和持久化卷外,Prometheus Operator 还将使用 ServiceMonitor 的概念动态更新配置,ServiceMonitor 会针对运行容器的标签匹配规则来定位服务:

图 5.7:Prometheus Operator 逻辑图

掌握这些知识后,我们将提供一个示例,展示如何使用 Prometheus Operator 部署和配置 Prometheus,包括从应用程序收集指标,这次我们将在不同的命名空间中运行应用。

Kubernetes 环境

确保没有 minikube 实例在运行,如下所示:

minikube status
minikube delete

使用以下规格启动一个新的 minikube 实例:

minikube start \
 --cpus=2 \
 --memory=2048 \
 --kubernetes-version="v1.14.0" \
 --vm-driver=virtualbox

当前一个命令完成后,一个新的 Kubernetes 环境应该准备好使用。你可以使用以下命令访问其仪表盘,命令会在默认浏览器中打开 Kubernetes 仪表盘地址:

minikube dashboard

要继续部署我们的示例,请确保进入正确的仓库路径,如下所示:

cd chapter05/provision/kubernetes/operator

像之前的例子一样,我们将使用kubectl创建一个新的命名空间,命名为monitoring,如下所示:

kubectl apply -f monitoring-namespace.yaml

Prometheus Operator 部署

创建好新的命名空间后,接下来需要确保 Prometheus Operator 的所有访问权限已经到位,如接下来的几个配置片段所示。第一个片段定义了ClusterRole

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prometheus-operator
rules:
- apiGroups: [apiextensions.k8s.io]
  verbs: ['*']
  resources: [customresourcedefinitions]
- apiGroups: [monitoring.coreos.com]
  verbs: ['*']
  resources: 
  - alertmanagers
  - prometheuses
  - servicemonitors
...

然后,我们将ClusterRole应用到ClusterRoleBinding

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: prometheus-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus-operator
subjects:
- kind: ServiceAccount
  name: prometheus-operator
  namespace: monitoring

最后,我们为ClusterRoleBinding创建一个ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: prometheus-operator
  namespace: monitoring

使用以下命令应用包含前面片段的清单:

kubectl apply -f prometheus-operator-rbac.yaml

配置好新的服务账户后,我们准备好部署 Operator 本身,如下所示:

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  labels:
    k8s-app: prometheus-operator
  name: prometheus-operator
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      k8s-app: prometheus-operator
...
      serviceAccountName: prometheus-operator

使用以下命令应用前面的清单:

kubectl apply -f prometheus-operator-deployment.yaml

我们可以使用以下代码片段跟踪部署状态:

kubectl rollout status deployment/prometheus-operator -n monitoring

Operator 部署完成后,我们现在可以使用它来部署和管理 Prometheus 实例。

Prometheus 服务器部署

在继续设置 Prometheus 之前,我们需要为其实例授予正确的访问控制权限。以下是 Prometheus RBAC 清单中的片段,完成了这一任务。首先,我们需要创建一个ClusterRole,允许 Prometheus 通过 GET 请求访问/metrics

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prometheus-k8s
rules:
...
- nonResourceURLs:
  - /metrics
  verbs:
  - get

接下来,我们创建一个ClusterRoleBinding,将前面提到的ClusterRole权限授予用户,在我们的例子中是ServiceAccount

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: prometheus-k8s
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus-k8s
subjects:
- kind: ServiceAccount
  name: prometheus-k8s
  namespace: monitoring

最后,我们为 Prometheus 创建一个ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: prometheus-k8s
  namespace: monitoring

使用以下命令应用包含前面片段的清单:

kubectl apply -f prometheus-rbac.yaml

配置好服务账户后,我们可以使用 Prometheus Operator 通过以下清单部署 Prometheus 服务器:

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  labels:
    prometheus: k8s
  name: k8s
  namespace: monitoring
spec:
  baseImage: quay.io/prometheus/prometheus
  version: v2.9.2
  replicas: 2
...
  serviceAccountName: prometheus-k8s
  serviceMonitorNamespaceSelector: {}
  serviceMonitorSelector: {}

使用以下命令应用前面的清单:

kubectl apply -f prometheus-server.yaml

我们可以使用以下命令跟踪部署进度:

kubectl rollout status statefulset/prometheus-k8s -n monitoring

部署完成后,我们将准备好为 Prometheus 服务器创建一个新的服务,并启动 Web 界面来验证当前设置,如下所示:

kubectl apply -f prometheus-service.yaml

minikube service prometheus-service -n monitoring 

以下是 Prometheus Operator 创建的 Prometheus 默认配置:

图 5.8:Prometheus Operator 创建的 Prometheus 默认配置

向 Prometheus 添加目标

到目前为止,我们已经部署了 Operator 并使用它部署了 Prometheus。现在,我们准备添加目标并探讨生成目标的逻辑。

在继续之前,我们还将部署一个应用程序,以增加可用目标的数量。为此,我们将再次使用Hey应用程序,这次使用默认命名空间:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hey-deployment
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hey

请特别注意标签和端口名称,如以下代码块所示;它们将被服务监视器使用:

  template:
    metadata:
      labels:
        app: hey
    spec:
      containers:
      - name: hey
        image: kintoandar/hey:v1.0.1
        ports:
        - name: hey-port
          containerPort: 8000
...

使用以下命令应用包含前面片段的清单:

kubectl apply -f hey-deployment.yaml

我们可以使用以下命令跟踪部署状态:

kubectl rollout status deployment/hey-deployment -n default

部署完成后,我们将创建一个新的服务,如以下代码块所示。请特别注意服务监视器将用于定位此服务的标签:

kind: Service
metadata:
  labels:
    squad: frontend
  name: hey-service
  namespace: default
spec:
  selector:
    app: hey
  type: NodePort
  ports:
  - name: hey-port
    protocol: TCP
    port: 8000
    targetPort: hey-port

使用以下命令应用之前的清单:

kubectl apply -f hey-service.yaml minikube service hey-service -n default

最后,我们将为 Prometheus 实例和 Hey 应用程序创建服务监视器,指示 Operator 配置 Prometheus,并添加所需的目标。请注意选择器配置——它将用于匹配我们之前创建的服务。

以下是 Prometheus 的服务监视器:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    k8s-app: prometheus
  name: prometheus
  namespace: monitoring
spec:
  endpoints:
  - interval: 30s
    port: web
  selector:
    matchLabels:
      prometheus: k8s

Hey 应用程序的服务监视器如下:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    app: hey
  name: hey-metrics
  namespace: default
spec:
  endpoints:
  - interval: 30s
    port: hey-port
  selector:
    matchLabels:
      squad: frontend

使用以下命令应用之前的清单:

kubectl apply -f prometheus-servicemonitor.yaml

kubectl apply -f hey-servicemonitor.yaml

你可以使用以下命令验证服务监视器的成功部署:

kubectl get servicemonitors --all-namespaces

在 Operator 重新配置 Prometheus 后,可能需要几秒钟,新增的目标应该会出现在 Prometheus 的网页界面上:

图 5.9:服务监视器配置后的 Prometheus 目标

ServiceMonitors 是使用 Prometheus Operator 时的核心构件。你可以配置任何进入抓取作业的内容,如抓取和超时间隔、抓取的指标端点、HTTP 查询参数等。你可以在 github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint 找到这些配置的文档。

总结

在本章中,我们介绍了设置 Prometheus 服务器的一些重要配置概念。这些知识对定制 Prometheus 以适应特定场景至关重要。从启动标志到配置文件,我们还启动了一个实例来实验并验证我们获得的知识。

随着越来越多的工作负载过渡到容器,特别是 Kubernetes 环境,我们探讨了如何在这种环境中设置和管理 Prometheus。我们开始通过静态配置进行实验,作为理解更强大方法——Prometheus Operator 的基础。

在下一章中,我们将深入探讨最常见的导出器,并在此基础上进行构建,以便我们能够成功地从 Prometheus 收集来自不同来源的数据。

问题

  1. 如果没有显式声明 scrape_timeout 会发生什么?

  2. 如何让 Prometheus 重新加载其配置文件?

  3. Prometheus 会回溯多长时间来判断一个时间序列是否过时?

  4. relabel_configsmetric_relabel_configs 有什么区别?

  5. 在静态部署示例中,我们将 Hey 应用程序的 Kubernetes 服务作为目标添加到 Prometheus。如果我们增加 Hey pod 的数量,会出现什么问题?

  6. 在 Kubernetes 环境中,静态 Prometheus 配置是否合理?为什么?

  7. Prometheus Operator 依赖哪些 Kubernetes 组件来实现其目标?

进一步阅读

第六章:导出器和集成

即使第一方导出器已经很好地覆盖了基本内容,Prometheus 生态系统仍然提供了多种第三方导出器,涵盖了其他所有内容。在本章中,我们将介绍一些最有用的导出器——从操作系统OS)指标和互联网控制消息协议ICMP)探测,到从日志生成指标,或者如何从短生命周期的进程(如批处理作业)中收集信息。

简要来说,本章节将涵盖以下主题:

  • 本章的测试环境

  • 操作系统导出器

  • 容器导出器

  • 从日志到指标

  • 黑盒监控

  • 推送指标

  • 更多导出器

本章的测试环境

在本章中,我们将使用两个测试环境:一个基于虚拟机VMs)模拟传统的静态基础设施,另一个基于 Kubernetes,用于现代工作流。以下主题将指导你完成这两种环境的自动化设置过程,但会略过每个导出器的详细内容——这些将在各自的章节中详细讲解。

静态基础设施测试环境

这种方法将抽象化所有部署和配置细节,只需几条命令,你就能拥有一个完全配置的测试环境。你仍然可以连接到每个访客实例,并调整示例配置。

要启动新的测试环境,请进入相对于仓库根目录的以下章节路径:

cd ./chapter06/

确保没有其他测试环境正在运行,并启动本章的环境:

vagrant global-status
vagrant up

你可以使用以下命令验证测试环境是否成功部署:

vagrant status

该指令将输出以下内容:

Current machine states:

prometheus                running (virtualbox)
target01                  running (virtualbox)

This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.

最终结果将是如下图所示的环境:

图 6.1:静态基础设施测试环境示意图

要连接到target01实例,只需运行以下命令:

vagrant ssh target01

要连接到 Prometheus 实例,使用以下代码片段:

vagrant ssh prometheus

当你完成当前环境后,请进入相对于仓库根目录的以下章节路径:

cd ./chapter06/

然后执行以下指令:

vagrant destroy -f

Kubernetes 测试环境

要启动 Kubernetes 测试环境,首先必须确保没有正在运行的minikube实例,如下所示:

minikube status
minikube delete

使用以下规格启动一个新的minikube实例:

minikube start \
 --cpus=2 \
 --memory=3072 \
 --kubernetes-version="v1.14.0" \
 --vm-driver=virtualbox

当前面的命令执行完毕后,一个新的 Kubernetes 环境应该已经准备好可以使用。

对于我们的 Kubernetes 测试环境,我们将基于在第五章中学到的经验,运行 Prometheus 服务器,并在我们的工作流中使用 Prometheus Operator。由于我们已经讲解了 Prometheus Operator 的设置,我们将直接部署所有必要的组件,而不逐一讲解。

进入以下章节编号:

cd ./chapter06/

部署 Prometheus Operator 并验证成功部署,方法如下:

kubectl apply -f ./provision/kubernetes/operator/bootstrap/

kubectl rollout status deployment/prometheus-operator -n monitoring

使用 Prometheus Operator 部署 Prometheus 并确保部署成功,如下所示:

kubectl apply -f ./provision/kubernetes/operator/deploy/

kubectl rollout status statefulset/prometheus-k8s -n monitoring

按照以下代码添加 ServiceMonitors,这将配置 Prometheus 作业:

kubectl apply -f ./provision/kubernetes/operator/monitor/

kubectl get servicemonitors --all-namespaces

稍等片刻后,您应该能够访问 Prometheus 并准备就绪;以下指令将提供其 Web 界面:

minikube service prometheus-service -n monitoring

您可以使用以下指令验证 Kubernetes 的 Prometheus StatefulSet,这将打开 Kubernetes 仪表盘:

minikube dashboard

有关 Kubernetes 对象(包括 StatefulSet 控制器)的更多信息,请访问 kubernetes.io/docs/concepts/

以下截图展示了 Prometheus StatefulSet 的正确部署:

图 6.2 - Kubernetes 仪表盘展示 Prometheus StatefulSet

操作系统 exporter

在监控基础设施时,最常见的起点是操作系统层面。有关资源的指标,如 CPU、内存和存储设备,以及内核操作计数器和统计信息,为评估系统性能特征提供了宝贵的见解。为了让 Prometheus 服务器收集这些类型的指标,目标主机上需要有一个操作系统级别的 exporter 来将这些数据暴露在 HTTP 端点上。Prometheus 项目提供了一个支持类 Unix 系统的 exporter,叫做 Node Exporter,社区也维护了一个等效的 exporter,专门用于 Microsoft Windows 系统,称为 WMI exporter。

Node Exporter

Node Exporter 是最著名的 Prometheus exporter,且有充分的理由。它为操作系统的不同领域提供了超过 40 个收集器,并且可以暴露关于 cron 作业的本地指标和主机的静态信息。与其他 Prometheus 生态系统组件一样,Node Exporter 提供了一个合理的默认配置,并具备智能识别可以收集哪些数据的功能,因此无需过多调整,直接运行是完全合理的。

在此上下文中,node 指的是计算机节点或主机,与 Node.js 无关。

尽管该 exporter 被设计为以非特权用户身份运行,但它确实需要访问内核和进程统计信息,这些信息在容器内通常无法获取。这并不意味着它在容器中无法工作——每个 Prometheus 组件都可以在容器中运行——只是需要额外的配置才能使其正常工作。因此,推荐尽可能直接在主机上以系统守护进程的方式运行 Node Exporter。

Node Exporter 的收集器可能会根据运行的系统收集不同的指标,因为操作系统内核在暴露内部状态和提供的细节上有所不同。例如,node_exporter 在 macOS 上暴露的指标将与 Linux 上的指标有很大不同。这意味着,尽管 Node Exporter 支持 Linux、Darwin(macOS)、FreeBSD、OpenBSD、NetBSD、DragonFly BSD 和 Solaris,但 Node Exporter 中的每个收集器都会有自己的兼容性矩阵,其中 Linux 是支持最多的内核。

Node Exporter 在 0.16.0 版本中更改了暴露的指标名称,这是因为 Prometheus 项目中的标准化工作。这是一个破坏性变更,意味着为早期版本的出口商制作的仪表板和教程不能直接使用。升级指南(github.com/prometheus/node_exporter/blob/v0.17.0/docs/V0_16_UPGRADE_GUIDE.md)可以在 Node Exporter 的代码库中找到。

Node Exporter 的源代码和安装文件可以在 github.com/prometheus/node_exporter 上找到。

根据设计,这个出口商只会生成关于进程的汇总指标(例如,正在运行的进程数等),而不是每个进程的独立指标。在 Prometheus 模型中,每个相关进程都需要暴露其自己的指标,或者有一个配套的出口商来完成这项工作。这也是在大多数情况下不建议在没有明确白名单的情况下运行通用进程出口商的原因之一。

配置

Prometheus 生态系统中的出口商通常会从给定的进程中收集一组特定的指标。Node Exporter 与大多数其他出口商不同,因为机器级别的指标涵盖了广泛的子系统,因此它被设计为提供单独的收集器,可以根据仪表需求启用或禁用。启用默认关闭的收集器可以通过 --collector.<name> 一组标志来完成;已启用的收集器可以通过使用 --no-collector.<name> 标志变体来禁用。

在默认启用的所有收集器中,有一个需要特别指出,因为它非常有用且需要配置才能正常工作。textfile 收集器通过监视一个目录中的 .prom 扩展名文件来启用自定义指标的展示,这些文件包含 Prometheus 展示格式的指标。--collector.textfile.directory 标志默认是空的,因此需要设置为一个目录路径,以便收集器完成其任务。通过这种方法,预计只会导出特定实例的指标,例如:

  • 本地的 cron 作业可以通过一个指标报告它们的退出状态(完成时间戳并不需要记录,因为指标文件的修改时间戳已经作为指标导出)

  • 信息性指标(仅存在于它们提供的标签中),如虚拟机类型、大小或分配的角色

  • 有多少个软件包升级待处理,是否需要重启

  • 任何其他未由内建收集器覆盖的内容

部署

本章的静态基础设施测试环境应该已经通过自动化配置使node_exporter启动并运行。不过,我们可以通过连接到target01虚拟机来检查它,例如如下所示:

cd ./chapter06/
vagrant ssh target01

然后检查提供的systemd单元文件配置,如下所示:

vagrant@target01:~$ systemctl cat node-exporter

在这个代码片段中,我们可以看到设置了textfile收集器目录,以便可以导出自定义指标:

...
ExecStart=/usr/bin/node_exporter --collector.textfile.directory=/var/lib/node_exporter
...

让我们尝试创建一个自定义指标。为此,我们只需要将指标写入textfile收集器目录中的一个.prom扩展名的文件,如下所示:

vagrant@target01:~$ echo test_metric 1 | sudo tee /var/lib/node_exporter/test.prom

在实际场景中,你需要确保文件是原子性地写入的,以防node_exporter看到一个半写的(因此损坏的)文件。你可以将文件先写入一个临时文件,然后使用mv将其移到正确位置(小心不要跨越挂载点边界),或者使用 sponge工具,通常在moreutils包中可以找到。

然后我们可以请求/metrics端点并搜索我们的测试指标,如下所示:

vagrant@target01:~$ curl -qs 0:9100/metrics | grep test_metric

输出应该类似于以下内容:

# HELP test_metric Metric read from /var/lib/node_exporter/test.prom
# TYPE test_metric untyped
test_metric 1

这个导出程序会生成大量的指标,具体取决于启用了哪些收集器。node_exporter提供的一些更有趣的指标如下:

  • node_cpu_seconds_total,它提供每个核心累计使用的秒数,适用于所有可用的 CPU 模式,非常有助于理解 CPU 的利用率

  • node_memory_MemTotal_bytesnode_memory_MemAvailable_bytes,这可以用来计算可用内存的比率

  • node_filesystem_size_bytesnode_filesystem_avail_bytes,这可以用来计算文件系统的利用率

  • node_textfile_scrape_error,它会告诉你在启用此收集器时,textfile收集器是否无法解析textfile目录中的任何指标文件

容器导出程序

在追求工作负载隔离和资源优化的过程中,我们见证了从物理机到虚拟机的迁移,使用了虚拟化技术。使用虚拟化意味着存在一定程度的资源使用低效,因为无论虚拟机是否使用它们,存储、CPU 和内存都需要分配给每个正在运行的虚拟机。虽然在这个领域已经做了很多工作来缓解这些低效问题,但最终,完全利用系统资源仍然是一个难题。

随着 Linux 操作系统级虚拟化(即使用容器)的兴起,思维方式发生了变化。我们不再需要为每个工作负载提供操作系统的完整副本,而只需要正确隔离的进程来完成所需的工作。为了实现这一点,特别是在 Linux 容器方面,一组负责隔离硬件资源(称为 cgroups 或控制组)和内核资源(称为命名空间)的内核特性被引入。cgroups 管理的资源如下:

  • CPU

  • 内存

  • 磁盘 I/O

  • 网络

这些内核特性允许用户对给定工作负载可用的资源进行精细控制,从而优化资源的使用。Cgroups 指标对于任何现代监控系统都是无价的。

cAdvisor

容器顾问 (cAdvisor) 是一个由 Google 开发的项目,负责收集、汇总、分析并暴露运行中的容器的数据。可用的数据几乎涵盖了你可能需要的任何内容,从内存限制到 GPU 指标,所有这些都可以按容器和/或主机进行分类展示。

cAdvisor 并不依赖于 Docker 容器,但它通常作为容器进行部署。数据通过容器守护进程和 Linux cgroups 收集,使得容器的发现变得透明且完全自动化。它还会在达到限制时暴露进程限制和节流事件,这是最大化基础设施资源使用而不对工作负载产生负面影响的重要信息。

除了以 Prometheus 格式暴露指标外,cAdvisor 还提供了一个有用的 Web 界面,允许即时可视化主机及其容器的状态。

cAdvisor 的源代码和安装文件可以在 github.com/google/cadvisor 找到。

配置

当以容器形式启动 cAdvisor 时,某些主机路径需要以只读模式可用。这将允许,例如,收集内核、进程和容器的数据。

有很多运行时标志,我们将以下表格中列出一些与我们测试案例最相关的标志:

标志 描述
--docker Docker 端点,默认为 unix:///var/run/docker.sock
--docker_only 仅报告容器信息,外加根统计数据
--listen_ip 要绑定的 IP,默认为 0.0.0.0
--port 监听端口,默认为 8080
--storage_duration 数据存储时长,默认为 2m0s

你可以使用以下地址查看可用的运行时配置:github.com/google/cadvisor/blob/release-v0.33/docs/runtime_options.md

部署

尽管历史上 cAdvisor 代码被嵌入在 Kubelet 二进制文件中,但它目前计划在该位置被弃用。因此,我们将以 DaemonSet 的方式启动 cAdvisor,以便为这个示例做好未来兼容性,同时公开其配置,并将其 Web 界面作为 Kubernetes 服务进行探索。

确保你进入正确的仓库路径,如下所示:

cd ./chapter06/provision/kubernetes/

接下来,我们必须创建一个 DaemonSet,因为我们希望在每个节点上都运行 cAdvisor:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: cadvisor
  namespace: monitoring
...

请注意,所有的卷挂载允许从 Docker 守护进程和各种 Linux 资源收集数据,如下所示:

...
    spec:
     containers:
      - name: cadvisor
        volumeMounts:
        - {name: rootfs, mountPath: /rootfs, readOnly: true}
        - {name: var-run, mountPath: /var/run, readOnly: true}
        - {name: sys, mountPath: /sys, readOnly: true}
        - {name: docker, mountPath: /var/lib/docker, readOnly: true}
        - {name: disk, mountPath: /dev/disk, readOnly: true}
...

使用以下指令应用之前的清单:

kubectl apply -f ./cadvisor/cadvisor-daemonset.yaml

我们可以通过以下方式跟踪部署状态:

kubectl rollout status daemonset/cadvisor -n monitoring

部署完成后,是时候添加一个新的服务了。注意将在 ServiceMonitor 中使用的端口名称。以下是我们将使用的清单:

apiVersion: v1
kind: Service
metadata:
  labels:
    p8s-app: cadvisor
  name: cadvisor-service
  namespace: monitoring
spec:
  selector:
    p8s-app: cadvisor
  type: NodePort
  ports:
  - {name: http, protocol: TCP, port: 8080, targetPort: http}

可以使用以下命令应用清单:

kubectl apply -f ./cadvisor/cadvisor-service.yaml

现在我们可以使用以下指令连接到 cAdvisor Web 界面:

minikube service cadvisor-service -n monitoring

这将打开一个浏览器窗口,界面与下图类似:

图 6.3:cAdvisor 网络界面

现在是时候将 cAdvisor 导出器添加为 Prometheus 的新目标了。为此,我们将使用下一个 ServiceMonitor 清单,如下所示:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    p8s-app: cadvisor
  name: cadvisor-metrics
  namespace: monitoring
spec:
  endpoints:
  - interval: 30s
    port: http
  selector:
    matchLabels:
      p8s-app: cadvisor

可以使用以下命令应用之前的清单:

kubectl apply -f ./cadvisor/cadvisor-servicemonitor.yaml

稍等片刻后,你可以通过以下指令打开 Prometheus Web 界面,检查新添加的目标:

minikube service prometheus-service -n monitoring

下图展示了 Prometheus /targets 端点,显示 cAdvisor 目标:

图 6.4:Prometheus /targets 端点显示 cAdvisor 目标

通过这个,我们现在拥有了容器级别的指标。请注意,cAdvisor 会为每个容器导出大量样本,这可能会导致每次抓取时导出的指标数量达到几千个样本,进而可能引发 Prometheus 在抓取时的基数问题。

你可以在 cAdvisor 的 Prometheus 文档中找到每个导出的指标:github.com/google/cadvisor/blob/release-v0.33/docs/storage/prometheus.md

从 cAdvisor 导出的成千上万的指标中,这些通常是用来监控问题的有用指标:

  • container_last_seen,它跟踪容器最后一次被看到作为正在运行的时间戳

  • container_cpu_usage_seconds_total,它提供了每个容器每个核心使用的 CPU 秒数的计数器

  • container_memory_usage_bytescontainer_memory_working_set_bytes,分别用于跟踪容器内存使用情况(包括缓存和缓冲区)和仅容器的活跃内存

  • container_network_receive_bytes_totalcontainer_network_transmit_bytes_total,它们分别显示容器接收和发送的流量

在 Kubernetes 上运行时,cAdvisor 无法提供有关集群运行状态的洞察力 —— Kubernetes 本身的应用级别指标。因此,我们需要另一个导出器:kube-state-metrics。

kube-state-metrics

kube-state-metrics 不导出容器级别数据,因为这不是它的功能。它在更高级别运行,公开 Kubernetes 状态,提供有关 API 内部对象(如 pod、service 或 deployment)的指标。当使用此导出器时,当前可用的对象指标组包括以下内容:

  • CronJob 指标

  • DaemonSet 指标

  • 部署指标

  • 作业指标

  • LimitRange 指标

  • 节点指标

  • 持久卷指标

  • 持久卷声明指标

  • Pod 指标

  • Pod 稳定性预算指标

  • 副本集指标

  • 复制控制器指标

  • 资源配额指标

  • 服务指标

  • StatefulSet 指标

  • 命名空间指标

  • 水平 Pod 自动缩放器指标

  • 端点指标

  • Secret 指标

  • ConfigMap 指标

kube-state-metrics 公开两个端点:一个提供 API 对象的指标,另一个展示导出器本身的内部指标。

kube-state-metrics 的源代码和安装文件可在 github.com/kubernetes/kube-state-metrics 找到。

配置

在配置 kube-state-metrics 时,除了所有必需的 RBAC 权限外,还需注意几个运行时标志。我们将在下表中提供我们测试案例中更相关的概述。

标志 描述
--host 绑定和公开 Kubernetes 指标的 IP,默认为 0.0.0.0
--port 用于公开 Kubernetes 指标的端口,默认为 80
--telemetry-host 用于公开内部指标的 IP,默认为 0.0.0.0
--telemetry-port 用于公开内部指标的端口,默认为 80
--collectors 用于启用的度量指标组的逗号分隔列表,默认为 ConfigMap、CronJobs、DaemonSets、Deployments、endpoints、horizontalpodautoscalers、Jobs、LimitRanges、namespaces、Nodes、PersistentVolumeClaims、PersistentVolumes、PodDisruptionBudgets、pods、ReplicaSets、ReplicationControllers、resource quotas、Secrets、services、StatefulSets
--metric-blacklist 禁用的度量指标的逗号分隔列表,与白名单互斥
--metric-whitelist 启用的度量指标的逗号分隔列表,与黑名单互斥

由于需要导出的对象数量不可预测,这些对象与集群大小成正比,部署 kube-state-metrics 的常见模式是使用一个名为 addon-resizer 的特殊容器,可以动态垂直调整导出器 Pod 的大小。有关 addon-resizer 的信息可在 github.com/kubernetes/autoscaler/tree/addon-resizer-release-1.8 找到。

部署

我们将在之前启动的 Kubernetes 测试环境基础上进行构建。要开始部署,请确保切换到正确的仓库路径,相对于仓库根目录,如下所示:

cd ./chapter06/provision/kubernetes/

由于需要访问 Kubernetes API,这次部署的基于角色的访问控制RBAC)配置相当广泛,包括 Role、RoleBinding、ClusterRole、ClusterRoleBinding 和 ServiceAccount。该清单位于./kube-state-metrics/kube-state-metrics-rbac.yaml

应该使用以下命令来应用:

kubectl apply -f ./kube-state-metrics/kube-state-metrics-rbac.yaml

我们将为 kube-state-metrics 创建一个仅有一个实例的部署,因为在这种情况下,不需要集群或特殊的部署要求:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kube-state-metrics
  namespace: monitoring 
spec:
  selector:
    matchLabels:
      k8s-app: kube-state-metrics
  replicas: 1
...

这个部署将运行一个kube-state-metrics导出器实例,并搭配addon-resizer动态扩展导出器:

...
  template:
    spec:
      serviceAccountName: kube-state-metrics
      containers:
      - name: kube-state-metrics
...
      - name: addon-resizer
...

可以使用以下指令应用此操作:

kubectl apply -f ./kube-state-metrics/kube-state-metrics-deployment.yaml

我们可以使用以下命令来跟踪部署状态:

kubectl rollout status deployment/kube-state-metrics -n monitoring

部署成功后,我们将为此导出器创建一个服务,这次使用两个端口:一个用于 Kubernetes API 对象的度量,另一个用于导出器的内部度量:

apiVersion: v1
kind: Service
metadata:
  name: kube-state-metrics
  namespace: monitoring
  labels:
    k8s-app: kube-state-metrics
  annotations:
    prometheus.io/scrape: 'true'
spec:
  type: NodePort
  ports:
  - {name: http-metrics, port: 8080, targetPort: http-metrics, protocol: TCP}
  - {name: telemetry, port: 8081, targetPort: telemetry, protocol: TCP}
  selector:
    k8s-app: kube-state-metrics

上述清单可以按照以下方式应用:

kubectl apply -f ./kube-state-metrics/kube-state-metrics-service.yaml

有了服务后,我们可以使用以下命令验证两个度量端点:

minikube service kube-state-metrics -n monitoring

这将打开两个不同的浏览器标签页,每个标签页对应一个度量端点:

图 6.5:kube-state-metrics 的 Web 界面

最后,是时候配置 Prometheus 来抓取这两个端点,使用如下所示的ServiceMonitor清单:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    k8s-app: kube-state-metrics
  name: kube-state-metrics
  namespace: monitoring
spec:
  endpoints:
  - interval: 30s
    port: http-metrics
  - interval: 30s
    port: telemetry
  selector:
    matchLabels:
      k8s-app: kube-state-metrics

现在可以使用以下命令应用它:

kubectl apply -f ./kube-state-metrics/kube-state-metrics-servicemonitor.yaml

我们现在可以通过以下指令验证 Prometheus 中抓取目标的正确配置,打开其 Web 界面:

minikube service prometheus-service -n monitoring

图 6.6:Prometheus /targets 端点,显示 kube-state-metrics 的度量和遥测目标

来自 kube-state-metrics 的一些有趣度量,可以用来监控你的 Kubernetes 集群:

  • kube_pod_container_status_restarts_total,可以告诉你某个特定的 Pod 是否处于重启循环中;

  • kube_pod_status_phase,可以用来告警那些长时间处于非就绪状态的 Pods;

  • kube_<object>_status_observed_generationkube_<object>_metadata_generation进行比较,可以帮助你了解何时某个对象失败但还没有回滚

从日志到度量

在理想的情况下,所有应用程序和服务都应该已经正确地进行了监控,我们只需要收集度量数据以获得可视性。外部导出器是一种权宜之计,简化了我们的工作,但并不是每个服务都通过简洁的 API 暴露其内部状态。较旧的守护进程软件,如 Postfix 或 ntpd,利用日志来传达其内部工作原理。在这些情况下,我们只有两个选择:要么自己为服务添加监控(对于闭源软件来说不可行),要么依赖日志来收集所需的度量数据。接下来的内容将介绍从日志中提取度量数据的可用选项。

mtail

由 Google 开发的 mtail 是一个非常轻量级的日志处理工具,能够运行具有模式匹配逻辑的程序,从而从日志中提取度量数据。它支持多种导出格式,例如 Prometheus、StatsD、Graphite 等。

除了 /metrics 端点,mtail 服务的 / 端点还暴露了有价值的调试信息。此端点在静态基础设施测试环境中可通过 http://192.168.42.11:9197 访问:

图 6.7:mtail 网页界面

mtail 的源代码和安装文件可以在 github.com/google/mtail 找到。

配置

要配置 mtail,我们需要一个具有模式匹配逻辑的程序。让我们看一个官方仓库中提供的非常简单的示例:

# simple line counter
counter line_count
/$/ {
  line_count++
}

该程序定义了一个类型为 counterline_count 度量,使用一个 RE2 兼容的表达式 /$/ 来匹配行尾,最后,使用 { } 包裹的动作来增加 line_count 计数器。

要运行此程序,我们只需要通过命令行标志启动 mtail,指向我们想要监控的程序和日志。以下是我们测试用例中最常用的一些标志:

-address 要绑定的主机或 IP
-port 监听端口,默认为 3903
-progs 程序路径
-logs 以逗号分隔的文件列表,用于监控(此标志可以多次设置)

您可以在 github.com/google/mtail/blob/master/docs/Programming-Guide.md 找到 mtail 编程指南,在 github.com/google/re2/wiki/Syntax 找到 RE2 语法。

部署

在我们的静态基础设施测试环境中,我们可以通过连接到 target01 实例来验证 mtail 的配置,如下所示:

cd ./chapter06/

vagrant ssh target01

然后检查所提供的 systemd 单元文件的配置,如以下命令所示:

vagrant@target01:~$ systemctl cat mtail-exporter

如本示例所示,mtail正在计算syslog文件的行数,因此它需要具有适当的权限来访问系统日志,因此我们使用Group=adm运行mtail守护进程以使其正常工作。我们可以在以下单元文件的代码片段中看到mtail服务所需的所有参数,包括行计数程序的路径:

...
Group=adm
ExecStart=/usr/bin/mtail -address 0.0.0.0 -port 9197 -progs /etc/mtail_exporter/line_count.mtail -logs /var/log/syslog
...

在 Prometheus 实例上,我们添加了以下任务:

  - job_name: 'mtail'
    scrape_interval: 15s
    scrape_timeout: 5s
    static_configs:
      - targets: ['target01:9197']

在实际场景中,你可以将抓取任务命名为 mtail 正在监控的守护进程的名称,比如 ntpd 或 Postfix。

使用 Prometheus 表达式浏览器,网址为http://192.168.42.10:9090,我们不仅可以通过up指标验证抓取是否成功,还可以验证我们的指标是否可用:

图 6.8:mtail line_count 指标

一些来自 mtail 的有趣指标,可以用来监控这个导出器:

  • mtail_log_watcher_error_count,它计算从fsnotify(基于内核的文件系统事件通知系统)接收到的错误数量

  • mtail_vm_line_processing_duration_milliseconds_bucket,这是一个直方图,用于提供每个 mtail 程序处理行时的持续时间分布(以毫秒为单位)

Grok 导出器

类似于mtailgrok_exporter解析非结构化日志数据并从中生成指标。然而,正如其名称所示,主要的区别在于该导出的领域特定语言是基于 Logstash 模式语言(Grok)建模的,这使得你可以重用你可能已经构建的模式。

grok_exporter的源代码和安装文件可在github.com/fstab/grok_exporter找到。

配置

该导出器需要一个配置文件来进行设置。配置文件中有五个主要部分,我们可以在以下从导出器配置文件中提取的片段中进行详细分析,这些文件部署在我们的静态基础设施测试环境中。global部分设置配置格式版本。目前,版本 2 是标准配置版本,因此我们在此设置它:

global:
    config_version: 2

输入部分定义了要解析的日志位置。如果readall设置为true,文件将在等待新行之前完全解析;正如我们在示例中看到的那样,我们并没有这么做:

input:
    type: file
    path: /var/log/syslog
    readall: false

grok部分加载用于解析的模式。这些模式在一个单独的位置进行配置,如这里所示:

grok:
    patterns_dir: /etc/grok_exporter/patterns

metrics部分是“魔法”发生的地方。它定义了从解析的日志中提取哪些指标。每种 Prometheus 指标类型在此导出器中都原生支持。每个type的配置可能会有所不同,因此你应该查阅其文档。然而,我们将在此提供一个概览,展示它们之间的共同配置:

  • match配置定义了数据提取的正则表达式;在我们的示例中,LOGLEVEL是一个预定义的模式,用于匹配日志级别。

  • labels 配置能够使用 Go 的模板语法输出从匹配定义中提取的内容;在这种情况下,我们在匹配模式中使用了 level 作为变量,因此它可以在模板中作为 .level 使用:

metrics:
    - type: counter
      name: grok_loglevel_example_total
      help: Total log level events triggered.
      match: '.*\(echo %{LOGLEVEL:level}\)$' 
      labels:
          level: '{{.level}}'

完整的配置文档可以在 github.com/fstab/grok_exporter/blob/v0.2.7/CONFIG.md 获取

最后,server 部分定义了导出程序的绑定地址和端口:

server:
    host: 0.0.0.0
    port: 9144

现在我们更好地理解了配置文件中的内容,是时候在我们的测试环境中尝试这个导出程序了。

部署

在我们的静态基础设施测试环境中,我们可以通过连接到 target01 实例来验证 grok_exporter 的配置,如下所示:

cd ./chapter06/

vagrant ssh target01

输出提供的 systemd 单元文件的配置如下所示:

vagrant@target01:~$ systemctl cat grok-exporter

就像 mtail 导出程序一样,我们需要以 Group=adm 运行 grok_exporter,以便它能够访问 syslog,而不需要以特权用户身份运行。我们可以在以下单元文件的代码片段中看到 grok_exporter 服务的所有必需参数,包括前面提到的配置文件路径:

...
Group=adm
ExecStart=/usr/bin/grok_exporter -config /etc/grok_exporter/config.yml
...

在 Prometheus 实例中,我们添加了以下作业:

  - job_name: 'grok'
    scrape_interval: 15s
    scrape_timeout: 5s
    static_configs:
      - targets: ['target01:9144']

使用 Prometheus 表达式浏览器,访问 http://192.168.42.10:9090,我们不仅可以验证抓取是否成功,还可以验证我们的指标是否可用:

图 6.9:grok_exporter 示例指标

grok_exporter 获取的一些有趣的指标可以用来监控这个导出程序:

  • grok_exporter_line_buffer_peak_load,一个汇总,提供从日志文件中读取并等待处理的行数

  • grok_exporter_line_processing_errors_total,暴露每个定义的指标的处理错误总数

黑盒监控

内省对于收集系统数据非常宝贵,但有时我们需要从该系统用户的角度来进行测量。在这种情况下,探测是一种不错的选择,可以模拟用户交互。由于探测是从外部进行的,并且不了解系统的内部工作原理,因此它被归类为黑盒监控,正如在第一章《监控基础》一节中所讨论的那样,监控基础知识

黑盒导出程序

blackbox_exporter 是目前 Prometheus 生态系统中最特殊的导出程序之一。它的使用模式非常巧妙,通常,初学者对此感到困惑。我们将深入探讨这个导出程序,希望能让它的使用尽可能简单明了。

blackbox_exporter 服务暴露了两个主要端点:

  • /metrics:暴露它自己的指标

  • /probe:这是启用 blackbox 探测的查询端点,返回 Prometheus 展示格式的结果。

除了之前提到的两个端点外,服务的 / 端点还提供了有价值的信息,包括已执行探测的日志。这个端点可以在静态基础设施测试环境 http://192.168.42.11:9115 中访问。

blackbox exporter 本身支持通过多种协议对端点进行探测,如 TCP、ICMP、DNS、HTTP(版本 1 和 2),以及大多数探测的 TLS。此外,它还支持通过 TCP 连接并配置要发送的消息及预期响应的脚本化文本协议,如 IRC、IMAP 或 SMTP;甚至普通的 HTTP 也可以进行脚本化,但由于 HTTP 探测是一个常见的用例,它已经内置于此。

尽管如此,这个 exporter 并不涵盖所有黑盒式监控的需求。对于这些情况,可能需要编写自己的 exporter。例如,你不能使用 blackbox_exporter 来测试 Kafka 主题的端到端功能,因此你可能需要寻找一个能够生成消息并将其消费回 Kafka 的 exporter:

图 6.10:blackbox_exporter 网页界面

/probe 端点,在接收到带有 moduletarget 参数的 HTTP GET 请求时,会执行指定的 prober 模块来探测定义的目标,并将结果以 Prometheus 指标的形式展示:

图 6.11:blackbox_exporter 高级工作流程

例如,像 http://192.168.42.11:9115/probe?module=http_2xx&target=example.com 这样的请求将返回类似以下的内容(为了简洁,省略了一些指标):

# HELP probe_duration_seconds Returns how long the probe took to complete in seconds
# TYPE probe_duration_seconds gauge
probe_duration_seconds 0.454460181
# HELP probe_http_ssl Indicates if SSL was used for the final redirect
# TYPE probe_http_ssl gauge
probe_http_ssl 0
# HELP probe_http_status_code Response HTTP status code
# TYPE probe_http_status_code gauge
probe_http_status_code 200
# HELP probe_ip_protocol Specifies whether probe ip protocol is IP4 or IP6
# TYPE probe_ip_protocol gauge
probe_ip_protocol 4
# HELP probe_success Displays whether or not the probe was a success
# TYPE probe_success gauge
probe_success 1

在调试探测时,你可以在 HTTP GET URL 后附加 &debug=true 来启用调试信息。

blackbox_exporter 的源代码和安装文件可以在 github.com/prometheus/blackbox_exporter 获取。

使用 blackbox_exporter 时需要注意的一个问题是,up 指标并不反映探测的状态,而仅仅表示 Prometheus 能够访问到 exporter。如前面指标输出所示,存在一个 probe_success 指标,它代表了探测本身的状态。这意味着,up 指标显示为正常的情况很常见,但探测可能会失败,这也是常见的困惑来源。

配置

blackbox 探测的抓取任务配置比较特殊,因为 prober 模块和目标列表(无论是静态的还是动态发现的)都需要作为 HTTP GET 参数传递给 /probe 端点。为了使其工作,需要使用一些 relabel_configs 魔法,如 第五章《运行 Prometheus 服务器》中所述,运行 Prometheus 服务器

使用以下 Prometheus 配置片段作为示例,我们设置一个针对 Prometheus 实例的 ICMP 探测,同时 blackbox_exportertarget01 上运行:

  - job_name: 'blackbox-icmp'
    metrics_path: /probe
    params:
      module: [icmp]
    static_configs:
      - targets:
       - prometheus.prom.inet
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: target01:9115

由于 ICMP 探测的性质,它需要提升的权限才能运行。在我们的环境中,我们通过设置原始套接字的能力(setcap cap_net_raw+ep /usr/bin/blackbox_exporter)来确保具备此类权限。

目标是用 blackbox_exporter 的地址替换目标地址,确保内部的 __param_target 保留目标的地址。重点是如何处理 relabel_configs,以下是处理过程:

  • __address__ 值(包含目标地址)存储在 __param_target 中。

  • __param_target 值随后被存储到实例标签中。

  • blackbox_exporter 主机随后被应用到 __address__

这使得 Prometheus 可以查询 blackbox_exporter(使用 __address__),保留带有目标定义的实例标签,并将模块和目标参数(通过内部的 __param_target)传递到 /probe 端点,返回指标数据。

部署

在我们的静态基础设施测试环境中,我们可以通过连接到 target01 实例来验证 blackbox_exporter 的配置,如下所示:

cd ./chapter06/

vagrant ssh target01

然后通过以下命令检查提供的 systemd 单元文件配置:

vagrant@target01:~$ systemctl cat blackbox-exporter

可以通过向 /-/reload 端点发送 HTTP POST 请求或向 blackbox_exporter 进程发送 SIGHUP 来在运行时重新加载配置。如果配置有错误,将不会应用。

我们可以在下面的单元文件片段中看到 blackbox_exporter 服务所需的所有参数,包括配置文件的路径:

...
ExecStart=/usr/bin/blackbox_exporter --config.file=/etc/blackbox_exporter/blackbox.yml
...

我们为示例量身定制的配置可以在以下片段中找到:

modules:
  http_2xx:
    prober: http
    http:
      preferred_ip_protocol: ip4

  icmp:
    prober: icmp
    icmp:
      preferred_ip_protocol: ip4

请注意使用了 preferred_ip_protocol: ip4,因为 blackbox_exporter 偏好使用 ipv6,但我们在探测中强制使用 ipv4

在 Prometheus 实例上,我们添加了以下作业:

- job_name: 'blackbox-http'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets: [ 'http://example.com', 'https://example.com:443' ]
...
  - job_name: 'blackbox-icmp'
    metrics_path: /probe
    params:
      module: [icmp]
    static_configs:
      - targets:
         - prometheus
...

通过 Prometheus 网络界面(位于 http://192.168.42.10:9090/targets),我们可以验证抓取是否成功(不管探测返回的状态如何):

图 6.12:Prometheus /targets 端点显示 blackbox_exporter 目标

如前所述,/targets 页面不会告诉你探测是否成功。这需要通过查询 probe_success 指标在表达式浏览器中进行验证:

图 6.13:Prometheus 表达式浏览器显示 probe_success 查询结果

可以从 blackbox_exporter(包括关于导出器本身和探测器的数据)收集的一些有趣的指标是:

  • blackbox_exporter_config_last_reload_successful,该指标表示在 SIGHUP 后导出器的配置文件是否成功重新加载

  • probe_http_status_code,它允许您了解在使用 HTTP prober模块时返回了什么 HTTP 状态码。

  • probe_ssl_earliest_cert_expiry,它返回 SSL 探测中证书链由于链中的某个证书过期而失效时的时间戳。

推送度量

尽管关于推送与拉取的激烈辩论以及在 Prometheus 服务器设计中故意选择使用拉取方式,依然有一些合理的情况,推送方式更为合适。

其中一种情况是批处理作业,不过,为了让这个说法真正有意义,我们需要清楚地定义什么是批处理作业。在这个范围内,服务级批处理作业是一个不依赖于特定实例的处理工作负载,通常不频繁执行或者按计划执行,因此并不是一直运行的。这种作业如果被监控,很难生成成功的抓取结果,正如在第五章《运行 Prometheus 服务器》中所讨论的那样,即便长时间运行,偶尔抓取也会导致度量的陈旧性

也有其他不依赖于推送度量的替代方法;例如,之前所述的通过使用node_exporter的 textfile collector。然而,这个选项并非没有缺点。如果工作负载不是特定于某个实例的,您最终将会得到多个时间序列,还需要处理 textfile collector 文件的清理逻辑,除非度量的生命周期与实例的生命周期匹配,这样在实际操作中可以更好地工作。

作为最后的手段,您可以使用 Pushgateway,接下来我们将详细介绍。

Pushgateway

正如之前所述,此导出器应该仅在非常特定的使用场景中使用,并且我们应当注意一些常见的陷阱。一个可能的问题是缺乏高可用性,使其成为单点故障。这也影响了可扩展性,因为容纳更多度量/客户端的唯一方法是垂直扩展实例(增加更多资源)或分片(为不同的逻辑组拥有不同的 Pushgateway 实例)。通过使用 Pushgateway,Prometheus 不会直接抓取应用实例,这就避免了将up度量作为健康监测的代理。此外,类似于node_exporter的 textfile collector,度量需要通过 Pushgateway 的 API 手动删除,否则它们将永远暴露给 Prometheus。

要推送一个度量指标,您需要向 Pushgateway 端点发送一个 HTTP POST 请求,使用以下的 URL 路径定义。接下来的部署部分将展示如何操作:

http://<pushgateway_address>:<push_port>/metrics/job/<job_name>/[<label_name1>/<label_value1>]/[<label_nameN>/<label_valueN>]

在这里,<job_name>将成为推送度量的标签 job 的值,<label_name>/<label_value>对将成为附加的标签/值对。请记住,度量将一直可用,直到被手动删除,或者在重新启动的情况下,如果没有配置持久化,度量将被丢失。

Pushgateway 的源代码和安装文件可以在github.com/prometheus/pushgateway找到。

配置

由于 Pushgateway 是一个集中式的点,实例会将它们的指标推送到这里,当 Prometheus 执行抓取时,每个暴露的指标的标签实例会自动设置为 Pushgateway 服务器的地址/端口,标签作业会设置为 Prometheus 抓取作业定义中所指定的名称。如果标签发生冲突,Prometheus 会将原始标签重命名为exported_instanceexported_job。为了避免这种情况,应在抓取作业定义中使用honor_labels: true,以确保最终使用的标签是来自 Pushgateway 的标签。

我们测试用例中的重要运行时配置如下:

标志 描述
--web.listen-address 绑定地址,默认为0.0.0.0:9091
--persistence.file 持久化文件位置,如果为空,指标仅保存在内存中
--persistence.interval 写入持久化文件的时间间隔,默认为 5 分钟

部署

我们将在之前启动的 Kubernetes 测试环境上进行构建。在这个特定的场景中,我们将部署一个 Pushgateway 实例,并将其作为目标添加到 Prometheus 中。为了验证我们的设置是否正确,我们将创建一个 Kubernetes CronJob 来模拟批处理工作负载,并将其指标推送到 Pushgateway 服务,以确保 Prometheus 能够收集我们的数据。

要开始部署,确保切换到正确的仓库路径,相对于代码仓库的根目录:

cd ./chapter06/provision/kubernetes/

要部署一个 Pushgateway 实例,您可以使用以下清单。请记住,这个服务不支持高可用性或集群配置:

apiVersion: apps/v1
kind: Deployment
metadata: {name: pushgateway, namespace: monitoring}
spec:
  selector:
    matchLabels: {p8s-app: pushgateway}
  replicas: 1
  template:
    metadata:
      labels: {p8s-app: pushgateway}
    spec:
      containers:
      - name: pushgateway
...

通过执行以下命令应用清单:

kubectl apply -f ./pushgateway/pushgateway-deployment.yaml

并使用以下命令继续部署:

kubectl rollout status deployment/pushgateway -n monitoring

部署成功后,接下来需要为我们的新实例提供一个Service,使用以下清单:

apiVersion: v1
kind: Service
metadata:
  name: pushgateway-service
  namespace: monitoring
  labels:
    p8s-app: pushgateway
spec:
  type: NodePort
  ports:
  - {name: push-port, port: 9091, targetPort: push-port, protocol: TCP}
  selector:
    p8s-app: pushgateway

以下指令适用于之前的清单:

kubectl apply -f ./pushgateway/pushgateway-service.yaml 

现在,您可以使用以下命令验证 Pushgateway 的 Web 界面:

minikube service pushgateway-service -n monitoring

这将打开一个新的浏览器标签页,指向新创建的 Pushgateway 实例的 Web 界面,界面应如下图所示:

图 6.14:没有任何指标被推送的 Pushgateway Web 界面

现在,我们需要指示 Prometheus 抓取 Pushgateway。这可以通过一个新的ServiceMonitor清单来实现,如下所示:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    p8s-app: pushgateway
  name: pushgateway
  namespace: monitoring
spec:
  endpoints:
  - interval: 30s
    port: push-port
    honorLabels: true
  selector:
    matchLabels:
      p8s-app: pushgateway

要应用此 ServiceMonitor,我们只需输入以下命令:

kubectl apply -f ./pushgateway/pushgateway-servicemonitor.yaml

现在我们已经建立了监控基础设施,我们需要模拟一个批处理作业来验证我们的设置。

我们可以依赖以下清单,通过手动构造的curl负载将一个虚拟的batchjob_example指标和多个标签推送到 Pushgateway 服务端点:

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: batchjob
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: batchjob
            image: kintoandar/curl:7.61.1
            args:
            - -c
            - 'echo "batchjob_example $(date +%s)" | curl -s --data-binary @- http://pushgateway-service.monitoring.svc.cluster.local:9091/metrics/job/batchjob/app/example/squad/yellow'
          restartPolicy: OnFailure

要应用之前的清单,请使用以下命令:

kubectl apply -f ./pushgateway/batchjob-cronjob.yaml

一分钟后,Pushgateway 的 Web 界面将类似于这张截图:

图 6.15:Pushgateway Web 界面展示 batchjob_example 指标

我们现在可以使用 Prometheus 表达式浏览器来验证指标是否已从 Pushgateway 被抓取:

图 6.16:Prometheus 表达式浏览器显示 batchjob_example 指标

由于 Pushgateway 的工作是从其他来源代理指标,它自己提供的指标非常少——只有标准的 Go 运行时指标、进程指标、HTTP 处理器指标和构建信息。然而,有一个应用程序指标需要注意,那就是 push_time_seconds。它会告诉你最后一次看到特定组(推送时使用的标签组合)是什么时候。这可以用来检测缺失或延迟的任务。

更多导出器

Prometheus 社区已经为几乎所有你可能需要的东西制作了大量的导出器。然而,在你的基础设施中部署一款新的软件需要做出一个有意识的选择,而这背后有着一定的隐性成本。这个成本转化为需要编写的部署自动化代码、打包工作、需要收集的指标、创建的告警、日志配置、安全问题、升级等,以及其他一些我们有时理所当然认为已经解决的问题。在选择开源导出器或任何其他开源项目时,有几个指标需要记住。

我们应该验证项目背后的社区,贡献的整体健康状况,是否有问题得到解决,拉取请求是否及时管理,维护者是否愿意与社区讨论和互动。从技术角度来看,我们还应检查该项目是否使用了官方的 Prometheus 客户端库。话虽如此,我们将介绍一些值得注意的导出器。

JMX 导出器

Java 虚拟机JVM)是核心基础设施服务的热门选择,诸如 Kafka、ZooKeeper 和 Cassandra 等服务都使用它。像许多其他服务一样,它们本身并不提供 Prometheus 展示格式的指标,且对这些应用进行监控远非一项简单的任务。在这种情况下,我们可以依赖 Java 管理扩展JMX)通过 管理 BeanMBeans)来暴露应用程序的内部状态。JMX 导出器从暴露的 MBeans 中提取数字数据并将其转换为 Prometheus 指标,通过 HTTP 端点进行暴露,供 Prometheus 吸收。

导出器有以下两种形式:

  • Java 代理:在这种模式下,导出器被加载到本地 JVM 中,目标应用程序正在运行,并暴露一个新的 HTTP 端点。

  • 独立 HTTP 服务器:在这种模式下,使用单独的 JVM 实例运行导出器,该实例通过 JMX 连接到目标 JVM,并在其自己的 HTTP 服务器上公开收集到的指标。

文档强烈建议使用 Java 代理部署导出器,理由很充分;与独立导出器相比,代理可以产生更丰富的指标集,因为它能够访问被检测的整个 JVM。然而,两者都有各自的权衡点,了解这些权衡非常重要,以便选择合适的工具来完成任务。

尽管独立服务器无法访问特定于 JVM 的指标,如垃圾回收统计信息或进程内存/CPU 使用情况,但当 Java 应用程序已经启用 JMX 并且是长时间运行的进程时,独立服务器更易于在静态基础设施上部署和管理,而不必频繁干预这些应用程序。此外,导出器的升级周期与应用程序生命周期解耦,尽管新版本发布较为稀少。

另一方面,Java 代理提供 JVM 中所有可用指标的完整范围,但需要在目标应用程序启动时加载。这在定期部署的应用程序或这些应用程序运行在容器中时可能会更为简单。

运行代理的另一个好处是,目标 JVM 还负责提供其自己的指标,因此从抓取任务中得到的up指标可以无歧义地表示进程状态。

两种选项都需要一个配置文件,该文件可以对 MBeans 中的指标进行白名单、黑名单和/或重新标记,并将其转换为 Prometheus 格式。一个重要的性能考虑是尽可能使用白名单。某些应用程序会暴露大量的 MBeans(如 Kafka 或 Cassandra),频繁的抓取会对性能产生显著影响。

你可以在github.com/prometheus/jmx_exporter/tree/master/example_configs找到适用于大多数常用应用程序的配置文件示例。

jmx_exporter的源代码可以在github.com/prometheus/jmx_exporter找到。

HAProxy 导出器

HAProxy,一款著名的负载均衡解决方案,在撰写本文时并未原生支持 Prometheus 指标。幸运的是,它有一个由 Prometheus 维护者开发的导出器,确保可以收集其指标,该导出器名为haproxy_exporter。HAProxy 原生通过可配置的 HTTP 端点以逗号分隔值CSV)格式公开其指标,使用的配置是stats enablehaproxy_exporter作为一个独立的守护进程运行,能够连接到 HAProxy 的统计端点,读取 CSV 内容,并将其转换为 Prometheus 指标格式,在触发抓取时以同步方式公开这些指标。

在负载均衡器层进行监控可以非常有用,特别是当后端池中的应用程序没有正确地进行监控,因此没有暴露访问指标时。例如,可以为 HTTP 错误率或后端可用性创建仪表板和警报,而无需应用程序方面的开发工作。这并不是长期解决方案,但可以帮助从传统监控系统过渡到 Prometheus。

你可以在 github.com/prometheus/haproxy_exporter 找到 haproxy_exporter 的源代码和安装文件。

总结

在本章中,我们有机会了解一些最常用的 Prometheus 导出工具。通过测试环境,我们能够与运行在虚拟机上的操作系统级导出工具以及在 Kubernetes 上运行的容器特定导出工具进行互动。我们发现,有时需要依赖日志来获取指标,并回顾了当前最佳的实现方式。接着,我们通过 blackbox_exporter 探索了黑盒探测,并验证了其独特的工作流程。我们还实验了推送指标,而不是使用 Prometheus 的标准拉取方法,并阐明了为什么有时这种方法确实是有意义的。

所有这些导出工具使你能够在不本地化代码的情况下获取可见性,而这往往比依赖社区驱动的导出工具更加昂贵。

有了这么多的指标源,现在是时候理解如何从它们的数据中提取有用的信息了。在下一章中,我们将深入讲解 PromQL,并讨论如何最好地利用它。

问题

  1. 如何通过 Node Exporter 收集自定义指标?

  2. cAdvisor 获取哪些资源来生成指标?

  3. kube-state-metrics 暴露了大量的 API 对象。有办法限制这个数量吗?

  4. 如何调试 blackbox_exporter 探针?

  5. 如果一个应用程序不暴露指标(无论是 Prometheus 格式还是其他格式),有哪些监控选项?

  6. 使用 Pushgateway 的缺点是什么?

  7. 如果某个特定的批处理任务是针对特定主机的,是否有替代 Pushgateway 的方法?

进一步阅读

第七章:Prometheus 查询语言 - PromQL

Prometheus 提供了一种强大且灵活的查询语言,利用其多维数据模型进行临时聚合和时间序列数据的组合。在本章中,我们将介绍 PromQL 及其语法和语义。掌握了这门语言的知识和功能后,我们将能够解锁 Prometheus 的真正潜力。

简而言之,本章将涵盖以下内容:

  • 本章的测试环境

  • 了解 PromQL 的基础知识

  • 常见模式与陷阱

  • 进入更复杂的查询

本章的测试环境

在本章中,我们将使用基于 Kubernetes 的环境来生成我们需要的所有指标,以测试本章中涵盖的 PromQL 示例。通过使用 Prometheus Operator,设置这个环境相当简单;按照以下步骤操作即可启动并运行:

  1. 为了启动 Kubernetes 测试环境,我们首先必须确保没有正在运行的 minikube 实例:
minikube status
minikube delete
  1. 启动一个新的 minikube 实例,具体规格如下:
minikube start \
  --cpus=2 \
  --memory=3072 \
  --kubernetes-version="v1.14.0" \
  --vm-driver=virtualbox

当前一个命令执行完成后,一个新的 Kubernetes 环境应该可以开始使用了。

对于我们的 Kubernetes 测试环境,我们将基于第五章《运行 Prometheus 服务器》中学到的知识,并在我们的工作流中使用 Prometheus Operator。由于我们已经覆盖了 Prometheus Operator 的设置,接下来我们将部署所有必需的组件,而不再逐一讲解它们。

  1. 进入本章对应的代码仓库根路径:
cd ./chapter07/
  1. 部署 Prometheus Operator 并验证成功部署:
kubectl apply -f ./provision/kubernetes/bootstrap/
kubectl rollout status deployment/prometheus-operator -n monitoring
  1. 等待几秒钟,直到 Prometheus Operator 能够执行请求并部署 Prometheus 服务器:
kubectl apply -f ./provision/kubernetes/prometheus/
kubectl rollout status statefulset/prometheus-k8s -n monitoring

为了让我们有一些指标提供者,我们将部署在第六章《导出器与集成》中介绍的部分导出器,特别是以下这些:

  • Node Exporter

  • cAdvisor

  • kube-state-metrics

我们还将部署一种Hello World 应用,Hey,这也是我们在第五章《运行 Prometheus 服务器》中介绍过的,这样 Prometheus 也可以收集 Web 应用的指标。

为了简化所有组件和配置的部署工作,以下命令将抽象出所有必需的步骤,我们在之前的章节中也走过这些步骤:

kubectl apply -f ./provision/kubernetes/services/
kubectl get servicemonitors --all-namespaces

几秒钟后,你应该可以看到 Prometheus 和所有服务已经准备好并可用。以下指令将在默认的 web 浏览器中打开 Prometheus 的 web 界面:

minikube service prometheus-service -n monitoring

如果你浏览 /targets 端点,你将看到类似以下的内容:

图 7.1:Prometheus /targets 端点显示所有已配置的目标

现在你可以在此新创建的测试环境中跟随本章的示例。

了解 PromQL 基础

理解 Prometheus 查询语言对于能够执行深入的仪表盘分析、容量规划和告警非常重要。但要做到这一点,我们需要从了解基础开始。以下主题将介绍构建查询时可用的组件,并探讨它们如何一起运作。

选择器

Prometheus 旨在处理数十万条时间序列。每个度量名称可能具有多个不同的时间序列,这取决于标签的组合;当来自不同作业的相似名称的度量混合在一起时,查询正确的数据可能看起来很困难,甚至令人困惑。在 Prometheus 中,选择器指的是一组标签匹配器。度量名称也包含在此定义中,因为从技术上讲,它的内部表示也是一个标签,尽管是一个特殊的标签:__name__。选择器中的每一对标签名称/值被称为标签匹配器,可以使用多个匹配器进一步过滤选择器匹配的时间序列。标签匹配器被包含在大括号中。如果不需要匹配器,大括号可以省略。选择器可以返回瞬时或区间向量。以下是一个选择器示例:

prometheus_build_info{version="2.9.2"}

这个选择器等同于以下内容:

{__name__="prometheus_build_info", version="2.9.2"}

现在,让我们来看一下标签匹配器是如何工作的。

标签匹配器

匹配器用于限制查询搜索到特定的标签值集。我们将使用 node_cpu_seconds_total 度量来示范四个可用的标签匹配器操作符:=, !=, =~!~。如果没有任何匹配规范,仅使用此度量会返回一个瞬时向量,其中包含所有包含该度量名称的时间序列,以及所有 CPU 核心编号(cpu="0"cpu="1")和 CPU 模式(mode="idle"mode="iowait"mode="irq"mode="nice"mode="softirq"mode="steal"mode="user"mode="system")的组合,总共返回 16 个时间序列,如以下截图所示:

图 7.2:node_cpu_seconds_total 查询结果返回 16 个时间序列

现在,让我们使用四个可用的标签匹配器(=, !=, =~, !~)来不同地限制查询,并分析产生的结果。

使用 =,我们可以对标签值进行精确匹配。例如,如果我们只匹配 CPU 核心 0,它将返回一个瞬时向量,其中包含前一个查询中一半的时间序列:

图 7.3:仅查询 CPU 核心 0 上的 node_cpu_seconds_total

我们还可以通过使用 != 匹配器来否定匹配,获取所有剩余的时间序列。应用到我们的示例时,它将仅返回剩余的八个时间序列,如下所示:

图 7.4:查询 node_cpu_seconds*_total* 的所有时间序列,排除核心 0

在选择时间序列时,除了仅依赖于精确匹配外,能够应用正则表达式也是非常重要的。因此,=~!~ 是 PromQL 用于此操作的匹配符,它们都接受 RE2 类型的正则表达式语法。请记住,在使用这些匹配符时,正则表达式是有锚点的。这意味着它们需要匹配完整的标签值。你可以通过在正则表达式的开始和结束处添加 .* 来解除锚点。

RE2 接受的正则表达式语法可以在以下链接找到:github.com/google/re2/wiki/Syntax

通过查看我们的示例,如果我们只对两个 CPU 模式感兴趣,mode="user"mode="system",我们可以轻松地执行如下查询,从而有效地选择我们需要的模式:

图 7.5:仅查询 mode="user" 和 mode="system" 的 node_cpu_seconds_total

考虑到 RE2 不支持负向前瞻,且类似于否定匹配符,Prometheus 提供了一种方式来否定正则表达式匹配符,方法是使用 !~。该匹配符排除与表达式匹配的结果,并允许所有其余的时间序列。以下是一个示例:

图 7.6:查询所有时间序列,除了 mode="user" 和 mode="system"

现在我们已经对标签匹配器的工作原理有了很好的理解,让我们来看看即时向量。

即时向量

即时向量选择器之所以这样命名,是因为它们返回与查询评估时间相关的、匹配时间序列的样本列表。这个列表被称为即时向量,因为它是某一时刻的结果。样本是时间序列中的数据点,由一个值和一个时间戳组成。这个时间戳在大多数情况下反映了抓取发生的时间以及该值被摄取的时间,除非是推送到 Pushgateway 的度量,因为它们的性质决定了它们永远没有时间戳。然而,如果对时间序列应用了函数或执行了操作,则即时向量样本的时间戳将反映查询时间,而不是摄取时间。

即时向量的工作方式——只返回与查询时间相关的、与选择器匹配的最新样本——意味着 Prometheus 不会返回被视为过时的时间序列(正如我们在第五章中提到的,运行 Prometheus 服务器)。当源目标从发现机制中消失,或在上次成功的抓取后未出现在抓取数据中时,会插入一个过时标记(一个特殊的样本,用来标记该时间序列为过时)。当时间序列的最后一个样本是过时标记时,使用即时向量选择器时该时间序列将不会被返回。

标签匹配器 部分中的每个示例都是瞬时向量选择器,因此每个结果都是瞬时向量。

范围向量

范围向量选择器类似于瞬时向量选择器,但它返回一组样本,适用于每个匹配的时间序列,并且在给定的时间范围内。请记住,给定值的时间戳可能不会完全与不同目标的抓取时间对齐,因为 Prometheus 会将抓取分散到其定义的时间间隔内,从而减少同一时刻的重叠抓取。

要定义范围向量选择器查询,必须设置一个瞬时向量选择器,并使用方括号 [ ] 添加一个范围。

下表详细说明了用于定义时间范围的可用时间单位:

缩写 单位
s
m 分钟
h 小时
d
w
y

如在第五章《运行 Prometheus 服务器》中所述,时间范围总是一个带有单一单位的整数值。例如,1.5d 和 1d12h 被视为错误,应表示为 36h。持续时间忽略闰秒和闰日:一周始终恰好是 7 天,一年始终是 365 天。

让我们来实际操作一下。以 Hey 应用程序为例,我们将检查过去两分钟内收集的 HTTP 状态码 200 的样本:

http_requests_total{code="200"}[2m]

以下是前面代码的输出:

图 7.7:HTTP 状态码 200 的 http_requests_total 指标过去两分钟的样本

正如我们在前面的截图中看到的,返回的每个 Hey 应用程序实例的样本(由 30 秒抓取间隔定义)有四个可用样本,来自我们的范围向量选择器。

偏移修饰符

offset 修饰符允许你查询过去的数据。这意味着我们可以相对于当前时间对瞬时或范围向量选择器的查询时间进行偏移。它是按选择器逐一应用的,这意味着只偏移一个选择器而不偏移另一个选择器,实际上解锁了将当前行为与过去行为进行比较的能力,适用于每个匹配的时间序列。使用此修饰符时,我们需要将其紧跟在选择器后面,并添加偏移时间;例如:

图 7.8:HTTP 状态码 200 的 http_requests_total 指标过去一小时内的两分钟样本

尽管与 PromQL 没有直接关系,但了解 Prometheus 表达式浏览器的时刻功能仍然很重要。此功能改变了查询时刻,就像我们回到了特定的日期和时间。时刻选择器与使用偏移量的主要区别在于,前者是绝对时间,而后者是相对时间偏移。

子查询

在 Prometheus 2.7.0 引入子查询选择器之前,并没有直接的方法将返回瞬时向量的函数的输出传递给范围向量函数。为了做到这一点,你需要将产生所需瞬时向量的表达式记录为一个新的时间序列,这也叫做记录规则——我们将在第九章中深入讨论,定义告警和记录规则——等待它有足够的数据,然后使用适当的范围向量选择器将记录的序列传递给范围向量函数。子查询选择器通过允许对返回瞬时向量的函数进行时间上的评估,并将结果作为范围向量返回,从而简化了这个过程,而无需等待记录规则捕获足够的数据。子查询语法与范围向量类似,唯一的区别是可以指定采样频率,正如我们很快将看到的那样。

我们将使用以下查询示例来解释其语法:

max_over_time(rate(http_requests_total{handler="/health", instance="172.17.0.9:8000"}[5m])[1h:1m])

将查询拆解成其组件后,我们可以看到以下内容:

组件 描述
rate(http_requests_total{handler="/health", instance="172.17.0.9:8000"}[5m]) 要执行的内部查询,在这种情况下,它将五分钟的数据聚合成一个瞬时向量。
[1h 就像一个范围向量选择器一样,这定义了相对于查询评估时间的范围大小。
:1m] 要使用的分辨率步长。如果没有定义,则默认为全局评估间隔。
max_over_time 子查询返回一个范围向量,这现在可以作为此聚合操作在时间上的参数。

这是一个常见的用例,因为尽可能公开计数器是一个好的实践(显然,像当前内存占用这类天生是仪表的内容除外),然后对其进行速率计算,以增强对抓取失败的抵抗力,但大多数有趣的函数会采用范围内的仪表值。

子查询的评估开销相对较高,因此强烈不建议在仪表板中使用它们,因为记录规则在足够的时间后会产生相同的结果。同样,出于同样的原因,它们也不应在记录规则中使用。子查询最适合用于探索性查询,其中无法提前知道需要查看哪些聚合。

运算符

PromQL 允许使用二元操作符、向量匹配和聚合操作符。在接下来的章节中,我们将逐一介绍每个操作符,并提供如何以及何时使用它们的示例。

二元操作符

除了瞬时向量和范围向量,Prometheus 还支持标量类型的值,它们是没有任何维度的单一数字。

在接下来的子章节中,我们将探讨每个二元操作符:算术操作符和比较操作符。

算术

算术操作符提供两个操作数之间的基本数学运算。

有三种可用的操作数组合。最简单的组合是两个标量之间的运算,这将返回一个标量结果。我们还可以将一个即时向量与标量相结合,这将在标量和即时向量的每个样本之间应用选定的算术运算符,最终返回更新样本的相同即时向量。最后一种组合是两个即时向量之间的运算。在这种情况下,算术运算符将应用于左侧向量和右侧向量中匹配的元素,而度量名称会被省略。如果没有匹配项,这些样本将不会出现在结果中。这个案例将在向量匹配部分进一步解释。

作为参考,以下是可用的算术运算符:

操作符 描述
+ 加法
- 减法
* 乘法
/ 除法
% 取模
^ 幂运算

比较

以下表格显示了比较运算符,这些运算符对于过滤结果非常有用:

操作符 描述
== 等于
!= 不等于
> 大于
< 小于
>= 大于或等于
<= 小于或等于

比如,我们有以下即时向量:

process_open_fds{instance="172.17.0.10:8000", job="hey-service"} 8
process_open_fds{instance="172.17.0.11:8000", job="hey-service"} 23

我们可以应用如下的比较运算符:

process_open_fds{job="hey-service"} > 10

结果将如下所示:

process_open_fds{instance="172.17.0.11:8000", job="hey-service"} 23

该操作表明我们已经有效地过滤了即时向量的结果,这对于警报非常重要,正如我们稍后将在第九章 定义警报和记录规则部分中讨论的那样。

此外,我们可以使用 bool 修饰符,不仅返回所有匹配的时间序列,还可以将每个返回的样本修改为 1 或 0,具体取决于该样本是否会被比较运算符保留或丢弃。

使用 bool 修饰符是比较标量的唯一方法;例如,42 == bool 42

因此,我们可以将带有 bool 修饰符的相同查询应用于我们之前的示例:

process_open_fds{job="hey-service"} > bool 10

这将返回以下内容:

process_open_fds{instance="172.17.0.10:8000", job="hey-service"} 0
process_open_fds{instance="172.17.0.11:8000", job="hey-service"} 1

向量匹配

向量匹配,顾名思义,是一种仅在向量之间可用的操作。到目前为止,我们已经了解了当我们有一个标量和一个即时向量时,标量会应用于即时向量的每个样本。然而,当我们有两个即时向量时,如何匹配它们的样本呢?我们将在接下来的子部分中探讨这个问题。

一一对应

由于二元运算符需要两个操作数,正如我们之前所描述的,当同样大小和标签集的向量位于一个运算符的两侧时,也就是一一对应的情况下,具有完全相同标签/值对的样本会被匹配在一起,而度量名称以及所有不匹配的元素将被丢弃。

让我们考虑一个示例。我们将从使用以下即时向量开始:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 100397019136
node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 14120038400
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 250685575168
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 17293533184

然后我们将应用以下操作:

node_filesystem_avail_bytes{} / node_filesystem_size_bytes{} * 100

这将返回结果即时向量:

{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 40.0489813060515
{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 81.64923991971679

在某些情况下,可能需要聚合标签不匹配的向量。在这些情况下,你可以在二元运算符后应用 ignoring 关键字来忽略指定的标签。此外,也可以使用 on 关键字来限制用于匹配的左右两侧的标签。

多对一和一对多

偶尔,你需要执行一种操作,其中一侧的元素与另一侧的多个元素匹配。当这种情况发生时,你需要为 Prometheus 提供解释这种操作的方法。如果操作的左侧具有更高的基数,你可以在 onignoring 后使用 group_left 修饰符;如果更高的基数在右侧,那么应使用 group_rightgroup_left 操作通常用于其能够将标签从表达式的右侧复制过来的能力,稍后的实践示例中将展示这一点。

逻辑运算符

逻辑运算符最容易理解的是它们的集合论对应物,如下表所示。这些运算符是 PromQL 中唯一能实现多对多的运算符。可以在表达式之间使用三种逻辑运算符:

运算符 描述
and 交集
or 并集
unless 补集

and 逻辑运算符通过仅返回左侧的匹配项来工作,前提是右侧的表达式有与之匹配的标签键/值对。所有左侧没有在右侧匹配的时间序列将被丢弃。结果的时间序列将保留来自左侧操作数的名称。这就是它也被称为交集运算符的原因。and 运算符通常像 if 语句一样使用:通过将右侧的表达式作为条件来返回左侧的表达式。

以以下瞬时向量为例,我们将验证之前的说法:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 1003970
node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 141200
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 2506855
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 172935

我们将应用以下表达式:

node_filesystem_size_bytes and node_filesystem_size_bytes < 200000

这将返回以下结果:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 141200
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 172935

并集逻辑运算符 or 通过返回左侧的元素来工作,除非没有匹配项,否则它将返回右侧的元素。再次强调,左右两侧的标签名称/值必须匹配。

我们可以重用之前的数据样本,并应用以下表达式:

node_filesystem_avail_bytes > 200000 or node_filesystem_avail_bytes < 2500000

结果将如下所示:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 1003970

最后,unless 逻辑运算符将返回来自第一个表达式中不与第二个表达式的标签名称/值对匹配的元素。在集合理论中,这称为补集。实际使用中,这个运算符的工作方式与 and 相反,这意味着它也可以作为 if not 语句使用。

再次,我们将使用之前使用的相同样本数据,并应用以下表达式:

node_filesystem_avail_bytes unless node_filesystem_avail_bytes < 200000

这反过来会给我们提供以下结果:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 1003970
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 2506855

聚合运算符

通过使用聚合运算符,我们可以对即时向量进行聚合,结果是一个新的即时向量,通常包含更少的元素。每次对即时向量的聚合都按我们在第四章“Prometheus 指标基础”一节中描述的方式进行。

可用的聚合运算符如下:

运算符 描述 要求
sum 求元素的和
min 选择最小元素
max 选择最大元素
avg 计算元素的平均值
stddev 计算元素的标准偏差
stdvar 计算元素的标准方差
count 统计元素的个数
count_values 统计具有相同值的元素个数
bottomk 按样本选择最小的 k 个元素 需要元素数量(k)作为标量
topq 按样本值选择最上面的 k 个元素 需要元素数量(k)作为标量
quantile 计算元素的分位数 需要分位数(0 ≤ φ ≤ 1)定义作为标量

需要参数的运算符(如 count_valuesbottomktopkquantile)需要在向量表达式之前指定参数。与聚合运算符一起使用时,有两个可用的修饰符可以与标签名列表结合使用:without 允许你定义要聚合掉的标签,从而将这些标签从结果向量中去除,而 by 则完全相反;即,它允许你指定哪些标签不被聚合。每个聚合运算符只能使用一个修饰符。这些修饰符会影响运算符聚合的维度。

例如,假设我们使用以下查询的一些示例数据:

rate(http_requests_total[5m])

这将生成类似以下的片段:

{code="200",endpoint="hey-port",handler="/",instance="172.17.0.10:8000",job="hey-service",method="get"} 5.891716069444445

{code="200",endpoint="hey-port",handler="/",instance="172.17.0.11:8000",job="hey-service",method="get"} 5.9669884444444445

{code="200",endpoint="hey-port",handler="/",instance="172.17.0.9:8000",job="hey-service",method="get"} 11.1336484826487

{code="200",endpoint="hey-port",handler="/health",instance="172.17.0.10:8000",job="hey-service",method="get"} 0.1

{code="200",endpoint="hey-port",handler="/health",instance="172.17.0.11:8000",job="hey-service",method="get"} 0.1

{code="200",endpoint="hey-port",handler="/health",instance="172.17.0.9:8000",job="hey-service",method="get"} 0.1000003703717421 

如果我们想知道所有请求的聚合,可以应用以下表达式:

sum(rate(http_requests_total[5m]))

这将返回以下内容:

{} 23.292353366909335

现在,如果我们添加 by 运算符,就可以按处理程序端点进行聚合:

sum by (handler) (rate(http_requests_total[5m]))

这将返回以下内容:

{handler="/"} 22.99235299653759
{handler="/health"} 0.3000003703717421

这个简单的示例展示了如何轻松地聚合数据。

二元运算符优先级

当 PromQL 查询被评估时,二元运算符的应用顺序由运算符的优先级决定。下表展示了优先级顺序,从高到低:

优先级 运算符 描述
1 ^ 从右到左求值,例如,1 ^ 2 ^ 3 按照 1 ^ (2 ^ 3) 计算
2 *, /, % 从左到右求值,例如,1 / 2 * 3 按照 (1 / 2) * 3 计算
3 +, - 从左到右求值
4 ==, !=, <=, <, >=, > 从左到右求值
5 and, unless 从左到右求值
6 or 从左到右求值

函数

PromQL 有近 50 种不同的函数,适用于各种用例,如数学运算、排序、计数器、仪表和直方图操作、标签转换、时间上的聚合、类型转换以及日期和时间函数。在接下来的章节中,我们将介绍一些最常用的函数,并提供它们为何如此相关的示例。

所有函数的全面概述可以在prometheus.io/docs/prometheus/latest/querying/functions/中查看。

absent()

absent()函数接受一个瞬时向量作为参数,并返回以下内容:

  • 如果参数有结果,则返回一个空向量

  • 1 元素向量,示例值为 1,在非冲突相等匹配器的情况下包含指定参数中的标签

正如名称所示,这个函数对告警非常有用,特别是缺失时间序列时。

例如,假设瞬时向量存在,我们执行以下表达式:

absent(http_requests_total{method="get"})

这将返回以下内容:

no data

假设我们使用带有不存在标签值的标签匹配器的表达式,如下所示:

absent(http_requests_total{method="nonexistent_dummy_label"})

这将生成一个包含不存在标签值的瞬时向量:

{method="nonexistent_dummy_label"} 1

让我们将absent应用于一个不存在的指标,如下所示:

absent(nonexistent_dummy_name)

这将转化为以下输出:

{} 1

最后,假设我们在一个不存在的指标和不存在的标签/值对上使用absent,如以下代码片段所示:

absent(nonexistent_dummy_name{method="nonexistent_dummy_label"})

结果可以在以下代码片段中看到:

{method="nonexistent_dummy_label"} 1

label_join() 和 label_replace()

这些函数用于操作标签——它们允许你将标签连接到其他标签、提取标签值的一部分,甚至删除标签(尽管这个操作通过标准聚合操作符更简单、更符合人机工程学)。在这两个函数中,如果定义的目标标签是新的,它将被添加到标签集合中;如果是现有标签,它将被替换。

使用label_join时,您需要提供一个瞬时向量,定义一个结果标签,确定结果连接的分隔符,并建立要连接的标签,如以下语法所示:

label_join(<vector>, <resulting_label>, <separator>, source_label1, source_labelN)

例如,假设我们使用以下示例数据:

http_requests_total{code="200",endpoint="hey-port", handler="/",instance="172.17.0.10:8000",job="hey-service",method="get"} 1366
http_requests_total{code="200",endpoint="hey-port", handler="/health",instance="172.17.0.10:8000",job="hey-service",method="get"} 942

然后我们应用以下表达式:

label_join(http_requests_total{instance="172.17.0.10:8000"}, "url", "", "instance", "handler")

最终,我们得到以下瞬时向量:

http_requests_total{code="200",endpoint="hey-port", handler="/",instance="172.17.0.10:8000",job="hey-service", method="get",url="172.17.0.10:8000/"} 1366
http_requests_total{code="200",endpoint="hey-port", handler="/health",instance="172.17.0.10:8000",job="hey-service", method="get",url="172.17.0.10:8000/health"} 942

当你需要任意操作标签时,label_replace是要使用的函数。它的工作方式是对所选源标签的值应用正则表达式,并将匹配的捕获组存储到目标标签中。源标签和目标标签可以是相同的标签,从而有效地替换其值。这听起来复杂,但实际上并不复杂;让我们看看label_replace的语法:

label_replace(<vector>, <destination_label>, <regex_match_result>, <source_label>, <regex>)

假设我们采用前面的示例数据并应用以下表达式:

label_replace(http_requests_total{instance="172.17.0.10:8000"}, "port", "$1", "instance", ".*:(.*)")

结果将是带有新标签port的匹配元素:

http_requests_total{code="200",endpoint="hey-port",handler="/", instance="172.17.0.10:8000", job="hey-service",method="get",port="8000"} 1366
http_requests_total{code="200",endpoint="hey-port",handler="/health", instance="172.17.0.10:8000", job="hey-service",method="get",port="8000"} 942

使用 label_replace 时,如果正则表达式没有匹配到标签值,则原始时间序列将保持不变。

predict_linear()

该函数接收一个范围向量和一个标量时间值作为参数。它会根据范围向量中的数据趋势,将查询评估时间到未来指定秒数的时间序列值进行外推。它使用线性回归来实现这样的预测,这意味着后台没有复杂的算法预测过程。此函数仅应与仪表(gauge)类型一起使用。

我们将应用以下表达式,使用 predict_linear,并利用一小时的数据范围,外推四小时后的样本值(60(秒)* 60(分钟)* 4):

predict_linear(node_filesystem_free_bytes{mountpoint="/data"}[1h], 60 * 60 * 4)
{device="/dev/sda1", endpoint="node-exporter",fstype="ext4",instance="10.0.2.15:9100", job="node-exporter-service",mountpoint="/data", namespace="monitoring", pod="node-exporter-r88r6", service="node-exporter-service"} 15578514805.533087

rate() 和 irate()

这两个函数允许您计算给定计数器的增长率。它们都会自动调整计数器重置,并将范围向量作为参数。

rate() 函数通过使用范围内的第一个和最后一个值来提供指定区间内的每秒 平均变化率,而 irate() 函数则使用范围内的最后两个值进行计算,从而产生瞬时变化率。

了解在什么场景下使用哪一个函数更为合适非常重要。例如,在创建仪表板等可视化时,我们可能希望更好地识别潜在的尖峰;此时,irate 符合这个要求。请注意,由于 irate() 使用的是范围内的最后两个值,因此它适合步进下采样(step downsampling),只能在完全放大时使用。当构建告警表达式时,我们更关心获取平滑的趋势,以避免偶发的尖峰重置 for 计时器(如我们将在 第九章《定义告警和记录规则》中看到的那样);在这种情况下,rate 是更合适的函数。始终确保范围向量中至少有四个样本,以便 rate() 可以可靠地工作。

为了展示这两个函数之间的区别,以下截图展示了相同的指标,在相同的时间范围内,一个使用了 rate(),另一个使用了 irate()

图 7.9:node_network_receive_bytes_total 的 rate(),时间范围为 1 分钟

图 7.10:node_network_receive_bytes_total 的 irate(),时间范围为 1 分钟

如我们所见,irate 对基础计数器的变化更为敏感,而 rate 通常会产生更平滑的值。

histogram_quantile()

该函数接受一个浮动值,定义所需的分位数(0 ≤ φ ≤ 1),以及一个量表类型的即时向量作为参数。每个时间序列必须具有一个 le 标签(表示小于或等于),用来表示桶的上界。该函数还要求所选时间序列之一具有一个名为 +Inf 的桶,它作为捕获所有其他值的“最后桶”,即累计直方图的最后一个桶。由于 Prometheus 客户端库生成的直方图对每个桶使用计数器,rate() 需要被应用来将它们转换为量表,以便此函数能正常工作。为时间范围选择的时间将对应于分位数计算的窗口。虽然不常见,但一些第三方软件生成的直方图可能没有对其桶使用计数器,因此只要它们满足标签要求,就可以直接在 histogram_quantile 中使用。

例如,执行以下表达式:

histogram_quantile(0.75, sum(rate(prometheus_http_request_duration_seconds_bucket[5m])) by (handler, le)) > 0

我们将会看到类似于以下的结果:

{handler="/"} 0.07500000000000001
{handler="/api/v1/label/:name/values"} 0.07500000000000001
{handler="/static/*filepath"} 0.07500000000000001
{handler="/-/healthy"} 0.07500000000000001
{handler="/api/v1/query"} 0.07500000000000001
{handler="/graph"} 0.07500000000000001
{handler="/-/ready"} 0.07500000000000001
{handler="/targets"} 0.7028607713983935
{handler="/metrics"} 0.07500000000000001

这提供了一个内部 Prometheus 直方图输出的示例。

sort() 和 sort_desc()

正如它们的名字所示,sort 接收一个向量,并根据样本值按升序对其进行排序,而 sort_desc 做的是相同的操作,只不过是降序排序。

topkbottomk 聚合操作符默认会对其结果进行排序。

vector()

vector() 函数接收一个标量值作为参数,并返回一个没有标签的向量,该向量的值为指定的标量参数。

例如,查询以下表达式:

vector(42)

它将返回以下内容:

{} 42

这通常用作确保表达式总是至少有一个结果的一种方式,通过将一个向量表达式与其组合,如以下代码所示:

http_requests_total{handler="/"} or vector(0)

由于 or 操作符返回两边的结果,因此值为 0 的样本将始终存在。

聚合操作(按时间)

我们之前讨论的聚合操作总是应用于即时向量。当我们需要在范围向量上执行这些聚合时,PromQL 提供了 *_over_time 系列函数,按 第四章《Prometheus 指标基础》中的水平聚合部分所述工作。它们都接受一个范围向量并输出一个即时向量。以下是这些操作的描述:

操作 描述
avg_over_time() 范围内所有样本的平均值。
count_over_time() 范围内所有样本的计数。
max_over_time() 范围内所有样本的最大值。
min_over_time() 范围内所有样本的最小值。
quantile_over_time() 计算范围内所有样本的分位数。它需要两个参数,第一个参数是所需分位数 φ 的定义,作为一个标量,其中 0 ≤ φ ≤ 1,第二个参数是一个范围向量。
stddev_over_time() 范围内样本值的标准差。
stdvar_over_time() 范围内样本值的标准方差。
sum_over_time() 在范围内所有样本值的总和。

时间函数

Prometheus 提供了许多函数来帮助处理时间数据。这些函数在几种场景中非常有用,例如检查一个进程或批处理作业上次出现的时间,只有在特定时间触发警报,或者在某些天完全不触发警报。Prometheus 中的每个时间函数都假定使用协调世界时(UTC)。

time 函数返回一个即时向量,表示当前时间的 UNIX 纪元格式(通常称为 UNIX 时间戳):自 1970 年 1 月 1 日 UTC 起经过的秒数。

timestamp 函数返回一个即时向量,其中包含由提供的选择器返回的样本的 UNIX 时间戳。

minutehourmonthyear 函数的工作方式相同:它们接受一个带有一个或多个时间戳的即时向量,并返回一个即时向量,表示相应的时间组件。这些函数的默认输入是 time 函数,因此,如果没有提供参数,它们将分别返回当前的分钟、小时、月份或年份。

days_in_month 函数接受一个带有时间戳的即时向量作为参数,并返回每个时间戳所在月份的天数。与之前的函数一样,默认输入参数是 time 函数。结果显然会在 28 到 31 之间变化。

最后,类似于之前的函数,day_of_weekday_of_month 函数期望一个带有时间戳的即时向量作为输入,分别返回对应的星期几(星期天为 0,星期一为 1,以此类推)和月份中的天数(从 1 到 31)。默认输入是 time 函数。

信息与枚举

还有两种度量类型尚未提及,分别是 info 和 enum。它们是相对较新的类型,但它们带来的便利性非常值得赞赏。info 类型的度量名称以 _info 结尾,是具有一个可能值 1 的常规仪表。这种特殊类型的度量被设计为存储随时间变化的标签的地方,例如版本(例如,导出器版本、语言版本和内核版本)、分配的角色或虚拟机元数据等;如果这些标签每次都导出到时间序列中,当它们变化时,会导致断续性问题,因为度量标识(即度量名称和标签组合)会发生变化。这也会污染所有受影响的时间序列的标签,因为这些新的标签会出现在每个度量中。要使用这种类型的度量,我们需要结合我们希望增强的度量,使用乘法运算符—由于 info 度量值为 1,乘法不会改变其他度量的值—而 group_left/group_right 修饰符允许我们在结果向量中添加所需的标签。

这里是一个使用 info 度量的查询示例:

node_uname_info{instance=”10.0.2.15:9100”}

我们可以在以下代码片段中看到上一个查询结果:

node_uname_info{domainname="(none)",endpoint="node-exporter", instance="10.0.2.15:9100",job="node-exporter-service",machine="x86_64", namespace="monitoring",nodename="minikube",pod="node-exporter-r88r6", release="4.15.0",service="node-exporter-service",sysname="Linux", version="#1 SMP Fri Dec 21 23:51:58 UTC 2018"} 1

枚举度量类型也是一种仪表,和 info 类型类似。它的目的是提供一种暴露需要跟踪状态的事物的方法,例如状态机的当前状态。此类度量的最常见用例是暴露守护进程的状态(启动、启动中、停止、停止中、失败等)。这种跟踪是通过在标签上维护状态信息来完成的,标签命名为 state,并将度量值设置为 1,表示当前状态,否则为 0。

这里是一个使用枚举度量的即时向量选择器查询示例:

node_systemd_unit_state{name="kubelet.service"}

上一个查询结果如下所示:

node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="activating"} 0
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="active"} 1
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="deactivating"} 0
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="failed"} 0
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2"", state="inactive"} 0

现在我们已经了解了 PromQL 的基础知识,让我们深入探讨在编写表达式时一些常见的模式和可以避免的陷阱。

常见的模式和陷阱

由于 PromQL 是一个功能强大的语言,面对如此多的选项,你很容易感到不知所措。在接下来的章节中,我们将提供一些常见的模式和陷阱,确保在每种情况中正确使用 PromQL,通过这种方式进一步巩固我们至今为你提供的知识。

模式

尽管 PromQL 的强大功能和灵活性为信息提取提供了无尽的可能性,但有一些查询模式能够使一组常见问题变得更容易理解,并帮助提高对被监控服务的洞察力。在接下来的主题中,我们将介绍一些我们最喜欢的模式,希望它们对你来说和对我们一样有用。

服务级指标

在第一章,《监控基础》一章中,我们介绍了测量什么的概念,讨论了Google 的四大黄金信号,以及 USE 和 RED 方法论。在此基础上,我们可以开始定义服务水平指标SLI),这些指标反映了特定服务的性能和可用性。构建查询来生成 SLI 是 PromQL 使用的常见模式,也是最有用的之一。

我们来看一个 SLI 的示例:其典型定义是“良好事件的数量”除以“有效事件的数量”;在这个例子中,我们希望了解 Prometheus 处理的请求是否在 100 毫秒以内,这使得它成为一个延迟 SLI。首先,我们需要收集有关在该阈值下处理的请求数的信息;幸运的是,我们可以依赖一个已存在的直方图类型指标prometheus_http_request_duration_seconds_bucket。正如我们已经知道的,这种类型的指标有由le标签(小于或等于)表示的桶,所以我们只需匹配 100 毫秒(0.1 秒)以内的元素,如下所示:

prometheus_http_request_duration_seconds_bucket{le="0.1"} 

虽然比率通常是此类计算的基本单位,但在这个例子中,我们需要一个百分比,因此我们必须将匹配的元素除以总请求数(prometheus_http_request_duration_seconds_count),然后将结果乘以 100。这两个即时向量不能直接相除,因为le标签不匹配,因此我们必须忽略它,如下所示:

prometheus_http_request_duration_seconds_bucket{le="0.1"} / ignoring (le) prometheus_http_request_duration_seconds_count * 100

这将给我们一个按端点和实例划分的即时向量,设置样本值为自服务启动以来每个实例中,低于 100 毫秒的请求百分比(记住,_bucket是一个计数器)。这算是一个好的开始,但还不是我们想要的,因为我们希望的是整个服务的 SLI,而不是每个实例或每个端点的 SLI。用一个滚动窗口来计算它比对不确定数量的数据求平均值更有用;随着数据的收集,平均值会变得更加平滑,且更难以改变。因此,为了修复这些问题,我们需要在时间窗口内对计数器进行速率计算,以获得一个固定的滚动平均值,然后使用sum()聚合掉实例和端点。这样,我们也不需要忽略le标签,因为它在聚合过程中也会被丢弃。我们把这些全部放在一起:

sum by (job, namespace, service) (
  rate(prometheus_http_request_duration_seconds_bucket{le="0.1"}[5m])
) / 
sum by (job, namespace, service) (
  rate(prometheus_http_request_duration_seconds_count[5m])
) * 100

构建一个服务水平目标SLO)对我们的服务来说变得简单,因为我们只需要通过比较操作符来指定我们希望达到的百分比。这为定义一个告警条件提供了一个很好的基础。

百分位

我们刚刚学习了如何提取在给定延迟下处理的请求百分比,但如果我们需要了解在某个百分位的请求延迟呢?

例如,要获得 95 百分位数,我们可以使用histogram_quantile函数,通过定义分位数(在此案例中为0.95),然后将表示我们感兴趣数据集的查询表达式传递给它——即请求持续时间直方图中每个桶的平均增长率,且该直方图是在滚动时间窗口内计算的。如果我们想要查看服务的全局延迟,而非每个实例/Pod/处理器的延迟,我们需要应用sum()聚合:

histogram_quantile(0.95, sum without (instance, pod, handler) (rate(prometheus_http_request_duration_seconds_bucket[5m])))

这个表达式会生成一个值,表示 95%的请求会在该值或该值以下。

抓取任务的健康状态

对于每个定义的抓取任务,Prometheus 会生成一个名为up的自动指标,反映该任务的健康状况——抓取成功时为 1,失败时为 0。我们可以利用这个指标快速可视化整个基础设施中所有被抓取的 exporters 和/或应用程序的健康状态。

让我们来看一下所有成功的抓取任务的概况:

sum by (job) (up)
{job="hey-service"} 3
{job="cadvisor-service"} 1
{job="kube-state-metrics"} 2
{job="node-exporter-service"} 1
{job="prometheus-service"} 2

陷阱

PromQL 的强大功能和灵活性使得我们能够对时间序列数据进行一些令人印象深刻的切割和分析,但也可能是造成挫折、意外结果,甚至严重性能问题的源头。尽管 Prometheus 的最新版本一直在持续引入功能以解决其中一些陷阱,但了解这些问题可以帮助你最大限度地利用 PromQL,同时节省时间和计算资源。

为数据类型选择正确的函数

在开始使用 PromQL 时,最常见的陷阱是没有为数据类型(如计数器、仪表或直方图)或向量类型选择正确的函数。尽管 Prometheus 文档中有指出这一点,但由于有些函数和聚合器的命名概念上相似,仍然可能会让人感到困惑:如ratederivincreasedeltaquantilehistogram_quantilesumsum_over_time等。幸运的是,在存在向量类型不匹配的情况下,表达式计算会失败,并且会告诉你哪里出了问题;但如果数据类型不匹配,例如将一个计数器传递给期望仪表的函数时,表达式可能会成功计算,但返回的结果却是错误的或具有误导性的。

总和速率与速率总和

前一点可能看起来很显而易见,但当构建的查询复杂性开始增加时,很容易犯错。一个常见的例子是尝试对计数器的总和进行速率计算,而不是对速率进行求和。rate 函数期望一个计数器,但计数器的总和实际上是一个仪表(gauge),因为当其中一个计数器重置时,仪表值可能会下降;这会在图形上显示出看似随机的尖峰,因为 rate 会将任何下降视为计数器重置,而其他计数器的总和则会被视为从零到当前值的巨大变化。在下图中,我们可以看到这种情况:两个计数器(G1G2),其中一个计数器(G2)发生了重置;G3 显示了通过对每个计数器的速率求和得到的期望汇总结果;G4 显示了计数器 1 和 2 的总和;G5 表示 rate 函数如何将 G4 视为计数器(突如其来的增加是 0 与下降发生点之间的差异);最后,G6 显示了对计数器总和进行速率计算的结果,错误的尖峰出现在 G2 的计数器重置点:

图 7.11:总和的速率和速率的总和的近似图

在 PromQL 中如何正确做这个的一个例子可能是:

sum(rate(http_requests_total[5m]))

在过去的 Prometheus 版本中,犯这个错误有点困难,因为要给 rate 提供一个总和的范围向量,我们需要一个录制规则或手动求和范围向量。不幸的是,从 Prometheus 2.7.0 开始,现在可以请求在一个时间窗口内对计数器求和,从而有效地从该结果创建一个范围向量。这是一个错误,应该避免这样做。因此,总结一下,永远在计算速率后应用聚合操作,而不是反过来。

拥有足够的数据进行工作

函数组 rateirateincreasederivdeltaidelta 需要在提供的范围向量中至少有两个样本才能正常工作。这意味着,接近 scrape_interval 的时间范围可能无法产生预期结果,因为一次抓取失败或窗口对齐问题(抓取并不会按精确的间隔发生,可能会有延迟)会导致范围内只包含一个样本。因此,建议使用 4 倍(或更多)scrape_interval 的时间,以确保足够的样本返回以支持计算。以下图显示了由于抓取失败,导致给定范围内样本趋势发生变化:

图 7.12:抓取失败改变给定范围内样本的趋势

使用 increase 时出现意外结果

与此相关的一个常见问题是,为什么像increasedelta这样的函数会产生非整数结果。文档中简要解释了这一点,但值得进一步展开。由于 Prometheus 定期收集数据(由scrape_interval配置定义),当查询请求一个样本范围时,该范围的窗口限制通常不会与返回数据的时间戳完全对齐。这些函数通过推测数据点如果与时间窗口匹配时会是什么样子,来处理此问题。它们通过计算提供样本的精确结果,然后将该结果乘以时间窗口与第一个和最后一个数据点之间间隔的比率,从而有效地将结果缩放到请求的范围。

未使用足够的匹配器来选择时间序列

编写 PromQL 表达式时的另一个常见陷阱,无论是用于仪表盘还是告警,是没有使用足够的匹配器,确保返回的样本来自预期的时间序列。在 Prometheus 社区中,将度量名称命名为应用程序名称被视为一种反模式,尤其是在该度量并非特定于某个软件时,即便是特定于该软件的,也可能会出现命名冲突的情况;因此,最好实践是使选择器具有范围,以便在尝试提取有关某个特定软件的信息时,job始终明确选择。例如,我们可以看看由官方 Prometheus Go 客户端库收集的go_goroutines度量:由于 Prometheus 生态系统的大部分内容都是用 Go 编写的,并且使用此客户端库进行监控,因此通常会在许多抓取作业中看到该度量。这意味着,如果我们想调查某个特定软件的聚合 Go 协程行为,若所用的选择器不够精确,可能会得到错误的结果。

失去统计意义

一个与 PromQL 并无直接关系的错误,但由于语言的灵活性而容易犯的错误,是对聚合值应用变换,从而失去其统计意义。例如,你可能会想对来自一组实例的汇总中的预先计算的分位数进行平均,以了解集群的情况,但从统计学角度来看,这些数据不能进一步操作——这项操作的结果将无法代表整个集群的相应分位数。然而,对于直方图是可以做到的,因为每个实例的桶可以在计算近似分位数之前进行全集群求和。另一个常见的例子是对平均值进行平均。

在构建复杂查询时,知道该期望什么

使用 PromQL 时需要注意的一个有趣细节是,当在向量之间使用比较运算符时,返回的结果将来自比较的左侧部分。这意味着,在进行当前值与阈值的比较时,你应该按此顺序进行(例如,current_value < threshold),因为你可能希望返回的是当前值,而不是阈值:

node_filesystem_avail_bytes < node_filesystem_size_bytes * 0.2

此外,在使用 and 连接不同的比较时,结果仍然会是第一个比较的左侧部分。以下示例返回的是文件系统中剩余空间的百分比,且该空间低于 20%,并且根据过去 6 小时的填充速率,预测在 4 小时内将满。请注意,它不是只读模式:

node_filesystem_avail_bytes/node_filesystem_size_bytes * 100 < 20
and
predict_linear(node_filesystem_avail_bytes[6h], 4*60*60) <= 0
and
node_filesystem_readonly == 0

如果将第一个比较和第二个比较的位置交换,仍然会得到相同数量的结果,但也会展示预测的 4 小时后的可用字节数。这个结果就不那么有用了,因为知道预测的负字节数实际上只传达了它们在现实中会变成 0 的事实。尽管如此,这两个表达式都可以用于告警,因为表达式的结果不会在告警通知中发送。

死亡查询

最后,在构建包含过于宽泛选择器和内存密集型聚合的查询时,需要小心。虽然 Prometheus 默认实现了检查和边界,以避免无限制的内存使用(如第五章《运行 Prometheus 服务器》所讨论),但仍然可能会遇到内存限制不足的情况——无论是容器限制,还是实际的系统内存不足,这会导致操作系统无情地终止 Prometheus 服务器。更糟糕的是,确定哪些查询使用了最多的资源是非常困难的,特别是在你对发送到服务器的查询几乎没有控制权的环境中;由于没有内建的慢查询日志功能,启用此功能将涉及一些会影响性能和可管理性的权衡。实际上,尽管如此,不断改进资源利用率限制(特别是在内存方面)使得在良好配置的环境中,这个问题的发生变得更加困难。

处理更复杂的查询

利用目前提供的信息,我们可以继续处理更复杂的查询,理解它们的构建方式,以及如何预期它们的结果。在接下来的部分中,我们将讨论一些复杂的场景,这些场景需要使用 PromQL 来探索并巩固我们目前已经学习的概念。

Node Exporter 运行在哪个节点上?

这个场景旨在帮助你理解诸如 info 指标和 group_left 修饰符等概念。

场景背景

在 Kubernetes 上运行时,你可能需要排查 Node Exporter pod 的问题,为此你需要知道它运行在哪个主机上。Node Exporter 指标查看的是 pod 而不是主机,因此你无法在生成的指标中获取主机名。在这种情况下,我们需要将缺失的信息添加到原本没有该标签的指标中。另一种替代方案是通过重新标签化将所需信息放到实例标签中。

PromQL 方法

以下查询允许我们通过 group_left 将另一个名为 nodename 的标签添加到 node_exporter_build_info 指标中,该标签包含关于运行 Node Exporter pod 的主机名的信息。

在这个例子中,我们有以下瞬时向量:

node_exporter_build_info

这将生成以下结果:

node_exporter_build_info{branch="HEAD",endpoint="node-exporter",goversion="go1.11.2", instance="10.0.2.15:9100",job="node-exporter-service",namespace="monitoring", pod="node-exporter-r88r6",revision="f6f6194a436b9a63d0439abc585c76b19a206b21", service="node-exporter-service",version="0.17.0"} 1

我们还有 node_uname_info,它包含 nodename 标签:

node_uname_info

这将转化为以下代码:

node_uname_info{domainname="(none)",endpoint="node-exporter", instance="10.0.2.15:9100",job="node-exporter-service",machine="x86_64", namespace="monitoring",nodename="minikube",pod="node-exporter-r88r6", release="4.15.0",service="node-exporter-service",sysname="Linux", version="#1 SMP Fri Dec 21 23:51:58 UTC 2018"} 1

借助我们之前描述的 info 类型指标,我们将使用以下表达式,通过 group_leftnode_uname_info info 类型指标将 nodename 标签添加到 node_exporter_build_info 指标中:

node_exporter_build_info * on (pod, namespace) group_left (nodename) node_uname_info

结果可以通过以下代码片段进行检查:


{branch="HEAD",endpoint="node-exporter",goversion="go1.11.2", instance="10.0.2.15:9100",job="node-exporter-service",namespace="monitoring", nodename="minikube",pod="node-exporter-r88r6", revision="f6f6194a436b9a63d0439abc585c76b19a206b21", service="node-exporter-service",version="0.17.0"} 1

比较不同版本的 CPU 使用情况

这个场景类似于之前的场景,但通过结合来自不同来源的指标,并使它们共同工作,进一步扩展了之前的思路。

场景理由

你可能想观察不同软件版本在吞吐量或资源使用上的表现。这可以通过在升级前后以清晰的方式绘制图表来更容易地分析。在这个具体示例中,我们将查看 node_exporter 升级前后的容器 CPU 使用情况。

请记住几点考虑事项:为了这个示例,node_exporter 作为容器运行,在实际场景中不推荐这样做。此外,我们将使用来自 cAdvisor 的 container_cpu_usage_seconds_total,而不是直接从原生检测应用程序收集的 process_cpu_seconds_total,这样这种方法就可以应用于任何类型的容器化进程,同时整合 cAdvisor 指标的使用。

PromQL 方法

container_cpu_usage_seconds_total 指标告诉我们每个容器运行时消耗的 CPU 秒数,来自 cadvisor 导出器。node_exporter 版本可以在 node_exporter_build_info 指标中找到。为了增加难度,由于容器指标来自 cadvisor,因此这些指标中注册的容器和 pod 是 cadvisor 的,而不是目标 pod 的;但是,我们可以在 container_label_io_kubernetes_container_namecontainer_label_io_kubernetes_pod_name 标签中分别找到原始的容器名称和 pod 名称。

我们需要做的第一件事是获取每个 pod 在一分钟滚动窗口内使用的平均 CPU 秒数:

sum by (container_label_io_kubernetes_pod_name) (
rate(container_cpu_usage_seconds_total{container_label_io_kubernetes_container_name="node-exporter"}[1m])

接下来,我们需要在node_exporter_build_info中创建一个新的标签,以便匹配按预期工作。对于这种用例,我们可以使用label_joinlabel_replace,因为我们只是读取一个标签并将其内容逐字写入另一个标签中:

label_join(node_exporter_build_info, "container_label_io_kubernetes_pod_name", "", "pod")

或者,我们可以使用以下代码:

label_replace(node_exporter_build_info, "container_label_io_kubernetes_pod_name", "$1", "pod", "(.+)")

最后,我们只需要通过它们的共同标签container_label_io_kubernetes_pod_name来匹配这两个指标,使用on(),然后请求将版本标签连接到 CPU 表达式的标签集,方法是使用group_left()。让我们将这些内容合起来:

sum by (container_label_io_kubernetes_pod_name) (
rate(container_cpu_usage_seconds_total{container_label_io_kubernetes_container_name="node-exporter"}[1m])
) 
* on (container_label_io_kubernetes_pod_name) 
 group_left (version)
 label_replace(node_exporter_build_info, "container_label_io_kubernetes_pod_name", "$1", "pod", "(.+)")

图 7.13: Node exporter 版本升级对 CPU 使用率的影响

所有这些可能一开始看起来很复杂,但在你自己尝试这些概念之后,它们很快就会变得容易理解、应用并推理。

总结

在本章中,我们学习了 PromQL 的基础知识,从选择器到函数,涵盖了二元运算符、向量匹配和聚合等概念。通过常见的模式和陷阱,我们介绍了这个语言如何不仅仅用于简单的查询,它已经成为一项重要的基础设施工具,帮助设计和管理 SLI 和 SLO。我们还展示了 PromQL 发挥作用的几种场景,以及看似复杂的查询其实并没有那么复杂。

在下一章,第八章,故障排除与验证,我们将深入探讨如何验证一个健康的 Prometheus 设置,并学习如何快速排除问题,确保监控堆栈的稳定性。

问题

  1. PromQL 中有哪些六种可用的比较运算符?

  2. 何时应该使用group_right修饰符而不是group_left修饰符?

  3. 为什么在应用topk聚合操作符时不应该使用sort()函数?

  4. rate()irate()之间的主要区别是什么?

  5. 哪种类型的指标具有_info后缀,它的用途是什么?

  6. 你应该先求和再应用rate,还是先应用rate再求和?

  7. 如何获取过去五分钟的平均 CPU 使用率(以百分比表示)?

深入阅读

第八章:故障排除与验证

故障排除本身就是一种艺术,在本章中,我们将提供一些有用的指南,教你如何快速发现和修复问题。你将发现一些有用的端点,它们展示了关键信息,了解 promtool,Prometheus 的命令行界面和验证工具,以及如何将其集成到日常工作流程中。最后,我们将探讨 Prometheus 数据库,并收集关于其使用的深刻信息。

简而言之,本章将涵盖以下主题:

  • 本章的测试环境

  • 探索 promtool

  • 日志和端点验证

  • 分析时间序列数据库

本章的测试环境

在本章中,我们将重点关注 Prometheus 服务器,并将部署一个新的实例,以便在新的测试环境中应用本章所讲的概念。

部署

要创建一个新的 Prometheus 实例,请进入正确的仓库路径:

cd chapter08/

确保没有其他测试环境在运行,然后启动本章的环境:

vagrant global-status
vagrant up

你可以使用以下命令验证测试环境是否成功部署:

vagrant status

这将输出以下内容:

Current machine states:

prometheus running (virtualbox)

The VM is running. To stop this VM, you can run `vagrant halt` to shut it down forcefully, or you can run `vagrant suspend` to simply suspend the virtual machine. In either case, to restart it again, simply run `vagrant up`.

新实例将可以进行检查,且 Prometheus 的 Web 界面可以通过 http://192.168.42.10:9090 访问。

你现在可以通过执行以下命令访问 Prometheus 实例:

vagrant ssh prometheus

现在你已经连接到 Prometheus 实例,你可以验证本章中描述的指令。

清理

测试完成后,只需确保你在 chapter08/ 目录下,并执行以下命令:

vagrant destroy -f

不用太担心——如果需要,你可以轻松重新启动环境。

探索 promtool

Prometheus 自带一个非常有用的命令行工具,叫做 promtool。这个小型的 Golang 二进制文件可以用来快速执行多个故障排除操作,并且内置了许多有用的子命令。

可用的功能可以分为四个类别,我们将在接下来的内容中讨论。

检查

属于此类别的子命令允许用户检查和验证 Prometheus 服务器的多个配置方面以及指标标准的合规性。以下部分将展示它们的使用方法。

检查配置

promtool 提供了几种类型的检查。其中最有价值的一种是检查 Prometheus 服务器的主配置文件。

check config 需要提供 Prometheus 主配置文件的路径,并输出对配置有效性的评估。当配置有问题时,该子命令可以告知用户问题所在:如果是非破坏性的问题,比如空的发现文件,它会输出警告并允许 promtool 成功退出;当遇到破坏性错误,例如语法错误时,它会输出错误并将检查标记为失败。通过工具返回的退出代码 – 0 表示成功,1 表示失败 – 是确保配置更改在 Prometheus 重启时不会导致崩溃的好方法,应作为预飞行检查使用。除了主配置文件外,该选项还会递归检查任何引用的文件,例如规则文件。

以下示例演示了它的用法:

vagrant@prometheus:~$ promtool check config /etc/prometheus/prometheus.yml 
Checking /etc/prometheus/prometheus.yml
 SUCCESS: 1 rule files found

Checking /etc/prometheus/first_rules.yml
 SUCCESS: 1 rules found

检查规则

check rules 分析并定位规则配置文件中的配置错误。它允许直接指定特定的规则文件,从而测试尚未在主 Prometheus 配置中引用的文件。这一功能在规则文件的开发周期中非常有用,也能帮助在使用配置管理时验证文件中的自动更改。我们将在第九章中深入讨论这些概念,定义告警和记录规则

以下是检查规则文件时的预期输出:

vagrant@prometheus:~$ promtool check rules /etc/prometheus/first_rules.yml 
Checking /etc/prometheus/first_rules.yml
 SUCCESS: 1 rules found

检查度量

check metrics 子命令验证传递给它的度量指标是否遵循 Prometheus 在一致性和正确性方面的标准。这在开发周期中非常有用,确保新的仪表化代码符合标准,也可用于自动化管理,如果你能控制新作业是否遵循相同规则。它通过 STDIN 接收度量数据作为输入,因此你可以直接将文件或 curl 输出传输给它。为了说明这个例子,我们暴露了 Prometheus 在版本 2.8.0 之前发生的问题:

~$ curl -s http://prometheus:9090/metrics | promtool check metrics
prometheus_tsdb_storage_blocks_bytes_total non-counter metrics should not have "_total" suffix

如你所见,prometheus_tsdb_storage_blocks_bytes_total 度量指标似乎存在问题。让我们查看这个特定的度量指标,排查错误:

~$ curl -s http://prometheus:9090/metrics | grep prometheus_tsdb_storage_blocks_bytes_total
# HELP prometheus_tsdb_storage_blocks_bytes_total The number of bytes that are currently used for local storage by all blocks.
# TYPE prometheus_tsdb_storage_blocks_bytes_total gauge
prometheus_tsdb_storage_blocks_bytes_total 0

在这些较旧版本的 Prometheus 中,似乎该度量被声明为仪表值,但却带有 _total 后缀,而该后缀应该仅用于计数器。

查询

属于这一类别的子命令使得可以直接在命令行中执行 PromQL 表达式。这些查询依赖于 Prometheus 公共 HTTP API。以下主题展示了如何使用它们。

查询即时数据

query instant 子命令允许通过命令行直接查询 Prometheus 服务器,基于当前时间进行查询。要使其工作,必须提供 Prometheus 服务器的 URL 作为参数,以及要执行的查询,如下所示:

vagrant@prometheus:~$ promtool query instant 'http://prometheus:9090' 'up == 1'
up{instance="prometheus:9090", job="prometheus"} => 1 @[1550609854.042]
up{instance="prometheus:9100", job="node"} => 1 @[1550609854.042]

查询时间范围

与前面的子命令类似,query range 能够显示指定时间范围内的结果。因此,我们必须提供开始和结束的 Unix 格式时间戳,以及查询和 Prometheus 服务器端点。

例如,我们将使用 date 命令来定义开始和结束时间戳,生成五分钟前和现在的 Unix 格式时间戳。我们还可以使用 --step 标志来指定查询的分辨率,在我们的示例中是一分钟。最后,我们放置要执行的 PromQL 表达式,最终得到类似以下的指令:

vagrant@prometheus:~$ promtool query range --start=$(date -d '5 minutes ago' +'%s') --end=$(date -d 'now' +'%s') --step=1m 'http://prometheus:9090' 'node_network_transmit_bytes_total{device="eth0",instance="prometheus:9100",job="node"}'
node_network_transmit_bytes_total{device="eth0", instance="prometheus:9100", job="node"} =>
139109 @[1551019990]
139251 @[1551020050]
139401 @[1551020110]
139543 @[1551020170]
139693 @[1551020230]
140571 @[1551020290]

在我们的测试环境中,可用的 date 命令来自 GNU coreutils,与 macOS 上基于 BSD 的命令不同。两者的语法可能不直接兼容。

查询系列

使用 query series 子命令,您可以搜索匹配一组度量名称和标签的所有时间序列。以下是如何使用它:

vagrant@prometheus:~$ promtool query series 'http://prometheus:9090' --match='up' --match='go_info{job="prometheus"}'
{__name__="go_info", instance="prometheus:9090", job="prometheus", version="go1.11.5"}
{__name__="up", instance="prometheus:9090", job="prometheus"}
{__name__="up", instance="prometheus:9100", job="node"}

查询标签

使用 query labels,您可以在所有可用的度量标准中搜索特定标签,并返回附加到它的所有可能值;例如:

vagrant@prometheus:~$ promtool query labels 'http://prometheus:9090' 'mountpoint'
/
/run
/run/lock
/run/user/1000
/vagrant
/var/lib/lxcfs

调试

属于此类别的子命令允许从运行中的 Prometheus 服务器中提取调试数据以便分析。接下来我们将演示如何使用它们。

调试 pprof

Prometheus 服务器,像大多数用 Go 语言编写的严肃软件一样,使用标准库中名为 pprof 的包进行仪表化,该包使用特定格式提供运行时分析信息。生成的这种格式的文件可以由同名的命令行工具 pprof 读取,后者用于生成分析数据的报告和可视化。promtool 提供 debug pprof 子命令,我们可以在以下代码片段中看到它的操作:

vagrant@prometheus:~$ promtool debug pprof 'http://prometheus:9090'
collecting: http://prometheus:9090/debug/pprof/profile?seconds=30
collecting: http://prometheus:9090/debug/pprof/block
collecting: http://prometheus:9090/debug/pprof/goroutine
collecting: http://prometheus:9090/debug/pprof/heap
collecting: http://prometheus:9090/debug/pprof/mutex
collecting: http://prometheus:9090/debug/pprof/threadcreate
collecting: http://prometheus:9090/debug/pprof/trace?seconds=30
Compiling debug information complete, all files written in "debug.tar.gz".

当我们提取前一个命令生成的存档时,可以看到一些文件:

vagrant@prometheus:~$ tar xzvf debug.tar.gz 
cpu.pb
block.pb
goroutine.pb
heap.pb
mutex.pb
threadcreate.pb
trace.pb

使用 pprof,我们可以生成一个转储的图像,正如我们在下一个片段中可以观察到的:

vagrant@prometheus:~$ pprof -svg heap.pb > /vagrant/cache/heap.svg

测试环境已准备好使用的 pprof 命令行工具。有关如何构建和部署它的更多信息,请访问 github.com/google/pprof

在您的主机机器上,在代码库的 ./cache/ 路径(相对于库的根目录)下,您现在应该有一个名为 heap.svg 的可扩展矢量图像文件,可以通过浏览器进行检查。以下截图显示了在前面示例生成的文件时可能看到的内容:

Figure 8.1: 由 pprof 生成的热图示例

调试度量

此子命令下载由提供的 Prometheus 实例公开的度量数据,并以压缩档案的形式保存。debug metrics 通常不常用,因为 /metrics Prometheus 端点对任何能够运行此命令的人开放;它的存在是为了在需要时更容易向外部支持人员(例如 Prometheus 维护者)提供 Prometheus 实例的当前状态。此子命令可以如下使用:

vagrant@prometheus:~$ promtool debug metrics 'http://prometheus:9090'
collecting: http://prometheus:9090/metrics
Compiling debug information complete, all files written in "debug.tar.gz".

vagrant@prometheus:~$ tar xzvf debug.tar.gz 
metrics.txt

vagrant@prometheus:~$ tail -n 5 metrics.txt 
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 284
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

调试全部

此选项将之前的调试子命令聚合为一个指令,如以下示例所示:

vagrant@prometheus:~$ promtool debug all 'http://prometheus:9090'
collecting: http://prometheus:9090/debug/pprof/threadcreate
collecting: http://prometheus:9090/debug/pprof/profile?seconds=30
collecting: http://prometheus:9090/debug/pprof/block
collecting: http://prometheus:9090/debug/pprof/goroutine
collecting: http://prometheus:9090/debug/pprof/heap
collecting: http://prometheus:9090/debug/pprof/mutex
collecting: http://prometheus:9090/debug/pprof/trace?seconds=30
collecting: http://prometheus:9090/metrics
Compiling debug information complete, all files written in "debug.tar.gz".

测试

promtool 最近获得了运行单元测试的能力,测试已定义的记录和告警规则。在你可能需要检查某个表达式是否匹配某些从未发生过的条件的情况下,这个功能非常有用,因此很难确定它们在关键时刻是否能正常工作。这个子命令被称为 test rules,并且需要一个或多个测试文件作为参数。稍后我们将在第九章《定义告警和记录规则》中深入探讨如何最好地利用规则。

日志和端点验证

在接下来的部分,我们将介绍一些有用的 HTTP 端点和服务日志,这些对于排查 Prometheus 实例的问题至关重要。

端点

检查 Prometheus 是否正常运行通常非常简单,因为它遵循大多数云原生应用程序用于服务健康检查的约定:一个端点用于检查服务是否健康,另一个用于检查服务是否准备好处理传入请求。对于那些使用过或曾经使用过 Kubernetes 的人来说,这些概念可能会感觉很熟悉;事实上,Kubernetes 也使用这些约定来评估容器是否需要重启(例如,如果应用程序死锁并停止响应健康检查)以及是否可以开始向容器发送流量。在 Prometheus 中,这些就是 /-/healthy/-/ready 端点。

你可以通过在测试环境中运行以下命令并检查其输出以及 HTTP 状态码,自己尝试这些端点:

vagrant@prometheus:~$ curl -w "%{http_code}\n" http://localhost:9090/-/healthy
Prometheus is Healthy.
200

vagrant@prometheus:~$ curl -w "%{http_code}\n" http://localhost:9090/-/ready
Prometheus is Ready.
200

在传统的基础设施中,通常使用就绪端点作为负载均衡器前面一组 Prometheus 实例的后端健康检查,因为只能配置一个健康检查。通过使用就绪端点,流量仅会被路由到准备好接收流量的实例。

此外,Prometheus 还暴露了一个 /debug/pprof/ 端点,它由 promtool debug pprof 命令使用,如前节所示。此端点还暴露了一个可导航的 Web UI,用户可以查看 pprof 调试信息,如当前的 goroutine 及其堆栈跟踪、堆分配、内存分配等:

图 8.2:Prometheus 服务器/debug/pprof 端点可用的信息

日志

与大多数当前软件相比,Prometheus 的日志非常简洁。这是 Prometheus 维护人员的有意为之,因为过多的日志记录可能会导致性能问题。此外,支持不同的日志流(例如应用日志、访问日志和慢查询日志),而不是将所有日志都写入标准输出——从而使应用日志被其他类型的日志淹没——将迫使 Prometheus 明确支持写入文件,这在云原生环境中是不受欢迎的。话虽如此,你可以通过设置--log.level标志来配置 Prometheus 增加应用日志的详细级别。例如,失败的抓取被认为是正常的操作行为,因此不会出现在日志中;但是,通过将日志详细级别增加到debug,它们是可以被记录的。

本章测试环境中的 Prometheus 实例已配置为调试级别日志。你可以通过运行以下命令来确认这一点:

vagrant@prometheus:~$ sudo systemctl cat prometheus.service

相关部分应该设置如下标志:

ExecStart=/usr/bin/prometheus \
    --log.level=debug \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.path=/var/lib/prometheus/data \
    --web.console.templates=/usr/share/prometheus/consoles \
    --web.console.libraries=/usr/share/prometheus/console_libraries

现在我们可以看到当抓取失败时会发生什么。为了实现这一点,我们可以在测试环境中停止node_exporter服务,然后查看 Prometheus 的日志:

vagrant@prometheus:~$ sudo service node-exporter stop
vagrant@prometheus:~$ sudo journalctl -fu prometheus | grep debug
Feb 23 15:28:14 prometheus prometheus[1438]: level=debug ts=2019-02-23T15:28:14.44856006Z caller=scrape.go:825 component="scrape manager" scrape_pool=node target=http://prometheus:9100/metrics msg="Scrape failed" err="Get http://prometheus:9100/metrics: dial tcp 192.168.42.10:9100: connect: connection refused"
Feb 23 15:28:29 prometheus prometheus[1438]: level=debug ts=2019-02-23T15:28:29.448826505Z caller=scrape.go:825 component="scrape manager" scrape_pool=node target=http://prometheus:9100/metrics msg="Scrape failed" err="Get http://prometheus:9100/metrics: dial tcp 192.168.42.10:9100: connect: connection refused"

分析时间序列数据库

Prometheus 服务器的一个关键组件是其时间序列数据库。能够分析该数据库的使用情况对于检测序列更替和基数问题至关重要。在此背景下,更替(Churn)指的是时间序列变得陈旧(例如,源目标停止收集数据或某个序列从一次抓取到下一次抓取之间消失),然后开始收集一个身份略有不同的新序列。一个常见的更替示例与 Kubernetes 应用部署相关,其中 Pod 实例的 IP 地址发生变化,导致之前的时间序列过时,取而代之的是一个新的序列。这会影响查询性能,因为可能会返回一些完全无关的样本。

幸运的是,Prometheus 数据库的源代码中有一个不太为人知的工具,可以分析其数据,这个工具名为tsdb

你可以在github.com/prometheus/tsdb/找到tsdb工具的源代码。通过在安装了正确 Go 工具链的系统上运行go get github.com/prometheus/tsdb/cmd/tsdb,它可以很容易地构建。

使用 tsdb 工具

tsdb工具可以针对 Prometheus 的整个数据库或仅针对某个数据块运行,并输出有关该数据健康状况的有用信息。为了运行此工具,我们必须确保 Prometheus 服务器已停止:

vagrant@prometheus:~$ sudo systemctl stop prometheus

我们将运行 tsdb 工具,目标是 Prometheus 数据库路径。为了简洁起见,我们将每个部分的输出限制为三条。如果没有指定块名称作为参数,将使用最后一个可用的块:

vagrant@prometheus:~$ sudo tsdb analyze --limit=3 /var/lib/prometheus/data/

输出被分为几个部分。在标题中,我们可以找到关于该块的摘要,包含以下内容:

  • 其完整路径位置

  • 块持续时间跨度,标准的 Prometheus 部署默认为两小时

  • 块中包含的系列和标签名称的数量

  • 有关索引条目数量的统计信息

在这里,我们可以看到前述指令生成的输出:

Block path: /var/lib/prometheus/data/01D48RFXXF27F91FVNGZ107JCK
Duration: 2h0m0s
Series: 819
Label names: 40
Postings (unique label pairs): 592
Postings entries (total label pairs): 3164

虽然在我们的测试环境中更替并不是问题,但我们可以看到哪些标签对在产生更替方面有最高参与度:

Label pairs most involved in churning:
112 job=node
112 instance=prometheus:9100
111 instance=prometheus:9090

接下来,我们可以看到基数最高的标签名称:

Label names most involved in churning:
224 instance
224 __name__
224 job

在标签名称更替之后,我们展示了最常见的标签对:

Most common label pairs:
413 job=node
413 instance=prometheus:9100
406 instance=prometheus:9090

最后,我们进入高基数部分,从标签开始:

Highest cardinality labels:
394 __name__
66 le
30 collector

__name__ 是存储指标名称的内部标签,因此,在健康的 Prometheus 系统中,它通常被认为是基数最高的标签。请记住,这并不意味着指标名称不能被错误地用作标签(例如,给指标名称加上 ID 后缀),因此需要关注基数的突然增加。

最后,我们找到了关于指标名称的统计信息:

Highest cardinality metric names:
30 node_scrape_collector_duration_seconds
30 node_scrape_collector_success
20 prometheus_http_request_duration_seconds_bucket

上述统计信息是从一个两小时的块中收集的。不过,也可以通过表达式浏览器查询特定时刻的数据,使用类似 topk(3, count({__name__=~".+"}) by (__name__)) 的查询。

如前所述,您可以选择不同的块进行分析:

vagrant@prometheus:~$ ls -l /var/lib/prometheus/data/
total 28
drwxr-xr-x 3 prometheus prometheus 4096 Feb 21 21:15 01D45Z3QCP8D6135QNS4MEPJEK/
drwxr-xr-x 3 prometheus prometheus 4096 Feb 21 21:15 01D486GRJTNYJH1RM0F2F4Q9TR/
drwxr-xr-x 4 prometheus prometheus 4096 Feb 21 21:15 01D48942G83N129W5FKQ5B3XCH/
drwxr-xr-x 4 prometheus prometheus 4096 Feb 21 21:15 01D48G04625Y6AKQ3Z63YJVNTQ/
drwxr-xr-x 3 prometheus prometheus 4096 Feb 21 21:15 01D48G048ECAR9GZ7QY1Q8SQ6Z/
drwxr-xr-x 4 prometheus prometheus 4096 Feb 21 21:15 01D48RFXXF27F91FVNGZ107JCK/
-rw-r--r-- 1 prometheus prometheus 0 Feb 19 23:16 lock
drwxr-xr-x 2 prometheus prometheus 4096 Feb 19 23:16 wal/

vagrant@prometheus:~$ sudo tsdb analyze /var/lib/prometheus/data 01D486GRJTNYJH1RM0F2F4Q9TR

tsdb 报告的好处在于,它提供了对指标和标签使用方式的更深入理解,并能够精确找出需要探索并验证其目标的良好候选项。

总结

在本章中,我们有机会实验一些有用的工具,用于排除故障和分析 Prometheus 配置问题和性能。我们从 promtool 开始,了解了它的所有可用选项;然后,我们使用了多个端点和日志,以确保一切正常工作。最后,我们描述了 tsdb 工具及其如何用于排查和定位 Prometheus 数据库中与基数和指标、标签的更替有关的问题。

现在我们可以进入记录和告警规则,这将在下一章进行讲解。

问题

  1. 如何验证主要的 Prometheus 配置文件是否有问题?

  2. 如何评估目标暴露的指标是否符合 Prometheus 标准?

  3. 使用 promtool,您如何执行即时查询?

  4. 如何查找所有使用的标签值?

  5. 如何在 Prometheus 服务器上启用调试日志?

  6. “准备好”端点和“健康”端点有什么区别?

  7. 如何查找 Prometheus 数据中旧块的标签更替情况?

进一步阅读

第三部分:仪表板和告警

本节结束后,读者将能够在 Prometheus 中创建有意义的告警和记录规则,充分利用 Grafana,并在 Alertmanager 中设置复杂的告警路由。

本节包括以下章节:

  • 第九章,定义告警和记录规则

  • 第十章,发现和创建 Grafana 仪表板

  • 第十一章,理解和扩展 Alertmanager

第九章:定义告警规则和记录规则

记录规则是 Prometheus 中一个非常有用的概念。它们使你能够加速繁重的查询,并在 PromQL 中启用本来非常昂贵的子查询。告警规则类似于记录规则,但具有告警特定的语义。由于测试是任何系统中的基础部分,本章将提供学习如何确保记录和告警规则在部署前按预期工作的机会。理解这些结构将有助于提升 Prometheus 的性能和鲁棒性,并启用其告警能力。

本章将涵盖以下主题:

  • 创建测试环境

  • 规则评估是如何工作的?

  • 在 Prometheus 中设置告警

  • 测试你的规则

创建测试环境

在本章中,我们将重点关注 Prometheus 服务器,并且将部署一个新的实例,以便应用所覆盖的概念。

部署

我们从创建一个新的 Prometheus 实例并将其部署到服务器上开始:

  1. 要创建一个新的 Prometheus 实例,请进入正确的仓库路径:
cd chapter09/
  1. 确保没有其他测试环境正在运行,然后启动本章的环境:
vagrant global-status
vagrant up
  1. 使用以下代码验证测试环境是否成功部署:
vagrant status

这将输出以下内容:

Current machine states:

prometheus                   running (virtualbox)

The VM is running. To stop this VM, you can run `vagrant halt` to
shut it down forcefully, or you can run `vagrant suspend` to simply
suspend the virtual machine. In either case, to restart it again,
simply run `vagrant up`.

新实例将可供检查,Prometheus 的 Web 界面将可以通过 http://192.168.42.10:9090 访问。

现在你可以通过执行以下命令访问 prometheus 实例:

vagrant ssh prometheus

现在你已经连接到 prometheus 实例,你可以验证本章中描述的指令。

清理

测试完成后,请确保你位于 chapter09/ 目录内,并执行以下命令:

vagrant destroy -f

不用太担心,如果需要,你可以轻松地重新启动环境。

了解规则评估是如何工作的

Prometheus 允许周期性地评估 PromQL 表达式,并存储由这些表达式生成的时间序列;这些被称为 规则。本章中我们将看到这两种规则类型。它们分别是 记录规则和告警规则。它们共享相同的评估引擎,但在目的上有所不同,我们将在接下来的内容中详细探讨。

记录规则的评估结果将作为样本保存到 Prometheus 数据库中,针对配置中指定的时间序列。这类规则可以通过预计算昂贵的查询,将原始数据聚合为时间序列来减轻重型仪表板的负担,随后可以将其导出到外部系统(例如通过联邦机制导出到更高层次的 Prometheus 实例,具体内容请见 第十三章,扩展与联邦化 Prometheus),并且有助于创建复合的范围向量查询(虽然记录规则过去是唯一的实现方式,但新的子查询语法使得这类用例的探索成为可能)。

当规则中评估的 PromQL 表达式产生非空结果时,警报规则会触发。它们是 Prometheus 中进行时间序列警报的机制。警报规则触发时也会生成新的时间序列,但不会将评估结果作为样本;相反,它们会创建一个ALERTS度量,其中包含警报名称和状态作为标签,以及配置中定义的任何其他标签。下一节将进一步分析这一点。

使用记录规则

规则与主 Prometheus 配置文件分开定义,并通过rule_files顶级配置键包含在主配置文件中。它们会定期评估,这个间隔可以通过global中的evaluation_interval全局定义(默认为一分钟)。

我们可以通过查看测试环境中提供的配置来看到这一点:

vagrant@prometheus:~$ cat /etc/prometheus/prometheus.yml
global:
...
 evaluation_interval: 1m
...

rule_files:
 - "recording_rules.yml"
...

rule_files接受一个路径列表,这些路径可以是相对于主 Prometheus 配置的相对路径或绝对路径。此外,可以使用通配符匹配文件名(而不是目录);例如,/etc/prometheus/rules/*.yml。规则文件中的更改不会被 Prometheus 自动检测到,因此需要重新加载(如第五章《运行 Prometheus 服务器》中所述,Running a Prometheus Server)。如果规则文件中发现错误,Prometheus 将无法重新加载,并将继续使用先前的配置运行。然而,如果服务器被重启,它将无法启动。为了确保这种情况不发生,可以使用promtool提前测试错误(如第八章《故障排除与验证》中所述,Troubleshooting and Validation)– 在使用自动化部署规则时,强烈建议这样做。

就像prometheus.yml配置文件一样,rules文件也是以 YAML 格式定义的。实际格式非常容易理解:

groups:
- name: <group_name_1>
  interval: <evaluation_interval>
  rules:
  - record: <rule_name_1>
    expr: <promql_expression_1>
    labels:
 <label_name>: <label_value>
  ...
  - record: <rule_name_N>
    expr: <promql_expression_N>
...
- name: <group_name_N>
  ...

每个文件在groups键下定义一个或多个规则组。每个组都有一个name,一个可选的评估interval(默认为在主 Prometheus 配置文件中定义的全局评估间隔),以及一个rules列表。每个规则指示 Prometheus 将评估在expr中定义的 PromQL 表达式的结果记录到指定的度量名称中,并可以通过在labels中设置它们来选择性地添加或覆盖存储结果之前设置的系列标签。每个组中的规则按声明的顺序依次评估,这意味着由某个规则生成的时间序列可以安全地在同一组内的后续规则中使用。由规则生成的样本将具有与规则组评估时间相对应的时间戳。下图说明了前述过程:

图 9.1:规则管理器是 Prometheus 的内部子系统,负责根据规则组的评估间隔定期评估规则,并管理警报生命周期。

让我们来看一下在本章测试环境中可用的录制规则:

vagrant@prometheus:~$ cat /etc/prometheus/recording_rules.yml

该文件有两个规则组,分别命名为recording_rulesdifferent_eval_interval

...
- name: recording_rules
  rules:
  - record: instance:node_cpu:count
    expr: count without (cpu) (count without (mode) (node_cpu_seconds_total))
...

第一个规则组由一个录制规则组成,使用全局评估间隔,采用node_cpu_seconds_total指标,从 Node Exporter 获取该指标,以计算虚拟机VM)中可用的 CPU 核心数,并将结果录制到一个新的时间序列instance:node_cpu:count中。

第二个规则组更为复杂;它为该组显示了自定义评估间隔,并使用该组中先前规则生成的时间序列作为录制规则。我们不会深入讨论这些规则的具体作用,因为它们将作为接下来规则命名约定部分的示例,但可以看到这里的评估间隔:

...
- name: different_eval_interval
  interval: 5s
...

通过在第二个规则组中声明评估间隔,我们正在覆盖prometheus.ymlglobal部分中的配置——此组中的规则将在指定的频率下生成样本。这样做仅用于演示目的;通常不建议设置不同的间隔,原因与抓取作业相同:当使用具有不同采样率的系列时,查询可能会产生错误的结果,而且需要定期跟踪每个系列的不同采样率变得不可管理。

Prometheus 提供了一个状态页面,在 Web用户界面UI)中,用户可以查看已加载的规则组及其包含的录制规则、录制状态、每个规则的最后一次评估所需时间,以及它们上次运行的时间。您可以通过进入顶部栏的状态 | 规则来找到这个页面:

图 9.2:Prometheus Web 界面显示/rules 端点

有了这些信息,我们现在掌握了如何创建录制规则的基本知识。接下来,我们将探讨 Prometheus 社区为录制规则约定的命名规则。

录制规则的命名约定

录制规则名称的验证遵循与度量名称相同的正则表达式,因此,规则技术上可以与任何其他度量名称相同。然而,制定清晰的命名标准能使得在抓取的度量中更容易识别录制规则,知道它们来源于哪些度量,以及理解应用了哪些聚合。

Prometheus 社区已经趋向于为录制规则制定一个明确的命名约定。这是基于多年的经验,在大规模运行 Prometheus 时积累的。这能在正确使用时提供所有上述优势。

记录规则命名的推荐约定由三个部分组成,使用冒号分隔,格式如下:level:metric:operations。第一部分表示规则的聚合级别,意味着它将列出相关的标签/维度(通常以下划线分隔);第二部分是作为规则基础的指标名称;第三部分列出了应用于指标的聚合操作。

本章介绍的记录规则都遵循这一约定,因此让我们看一下在测试环境中可用的第二条规则组:

- record: handler_instance:prometheus_http_request_duration_seconds_sum:rate5m
  expr: >
    rate(prometheus_http_request_duration_seconds_sum[5m])

- record: handler_instance:prometheus_http_request_duration_seconds_count:rate5m
  expr: >
    rate(prometheus_http_request_duration_seconds_count[5m])

- record: handler:prometheus_http_request_duration_seconds:mean5m
  expr: >
    sum without (instance) (
      handler_instance:prometheus_http_request_duration_seconds_sum:rate5m
    )
    /
    sum without (instance) (
      handler_instance:prometheus_http_request_duration_seconds_count:rate5m
    )

从第一条规则的命名来看,我们可以轻松理解该规则基于prometheus_http_request_duration_seconds_sum指标;rate5m表示对五分钟的时间范围应用了rate()函数,且有两个有趣的标签:handlerinstance

第二条规则应用了相同的逻辑,但这次使用的是prometheus_http_request_duration_seconds_count指标。第三条规则则稍微复杂一些;因为它是将_sum除以延迟事件的_count,它实际上代表了五分钟的延迟平均值,在这种情况下是 Prometheus 提供的 HTTP 请求的延迟。由于我们将instance标签聚合掉了,level部分通过仅保留handler作为相关维度来反映这一点。最后需要注意的是,这条规则的指标名称现在是prometheus_http_request_duration_seconds,因为它既不代表总和也不代表计数,但它仍然可以清楚地理解该规则所基于的指标。

命名记录规则可能是一项困难的任务,需要在精确表示所有相关因素和简洁管理指标名称之间取得平衡。当你遇到这种情况——无法立即清楚如何根据表达式命名记录规则时,一个好的经验法则是确保另一个熟悉这种命名约定的人可以将该规则与使用的指标、应该存在的标签/维度以及应用的变换联系起来。

在 Prometheus 中设置告警

到目前为止,我们已经讨论了 PromQL 如何在查询收集到的数据时发挥重要作用,但当我们需要一个表达式持续评估,以便在满足定义的条件时触发事件时,我们便进入了告警部分。我们在第一章中解释了告警是监控组件之一,监控基础知识。需要明确的是,Prometheus 并不负责发送电子邮件、Slack 或其他形式的通知;这通常是另一个服务的责任。这个服务通常是 Alertmanager,我们将在第十一章中讨论,理解与扩展 Alertmanager。Prometheus 利用告警规则的强大功能来推送告警,接下来我们将讨论这个话题。

什么是告警规则?

告警规则就像是记录规则,只是有一些额外的定义;它们甚至可以共享同一个规则组而不产生任何问题。最大的区别是,当触发时,它们会通过 HTTP POST 和 JSON 有效负载发送到外部端点,进行进一步处理。扩展一下“激活”这个术语,在这个上下文中,我们指的是当前状态与期望状态不同的情况,简而言之,就是当表达式返回一个或多个样本时。

告警规则,例如记录规则,依赖于在定义的间隔内评估的 PromQL 表达式。这个间隔可以是全局配置的,也可以是特定规则组的本地配置。在每次间隔迭代中,触发的告警会被验证,以确保它们仍然有效;如果不再有效,则认为它们已被解决。

对于每个由我们的表达式返回的样本,它都会触发一个告警。记住这一点非常重要,因为宽松的 PromQL 表达式可能会生成大量的告警,所以尽量将它们聚合在一起。

配置告警规则

为了演示如何创建和理解告警规则,我们将引导您完成整个过程。这不仅涉及到主要的 Prometheus 配置文件,还涉及规则文件以及服务器的 Web 界面在处理告警时的表现。

Prometheus 服务器配置文件

在本章的测试环境中,我们可以找到以下 Prometheus 配置:

vagrant@prometheus:~$ cat /etc/prometheus/prometheus.yml 
global:
...
 evaluation_interval: 1m
...
rule_files:
 - "recording_rules.yml"
 - "alerting_rules.yml"
alerting:
 alertmanagers:
 - static_configs:
 - targets:
 - “prometheus:5001”
...

在这个配置中,有三个组件需要注意:

  • evaluation_interval:它负责定义用于记录和告警规则的全局评估间隔,可以在规则组层级通过使用interval关键字进行覆盖。

  • rule_files:这是 Prometheus 读取已配置的记录和/或告警规则的文件位置。

  • alerting:这是 Prometheus 发送告警进行进一步处理的端点。

在告警部分,我们配置了 “prometheus:5001”。在这个端点背后,实际上只是一个名为 alertdump 的小服务,它在 5001 端口上监听 HTTP POST 请求,并简单地将其有效负载转储到日志文件中。这将有助于分析 Prometheus 在告警触发时发送的内容。

规则文件配置

之前,我们查看了 Prometheus 配置文件;接下来,我们将转到提供的告警规则示例,以下是相应代码片段:

vagrant@prometheus:~$ cat /etc/prometheus/alerting_rules.yml 
groups:
- name: alerting_rules
 rules:
 - alert: NodeExporterDown
 expr: up{job="node"} != 1
 for: 1m
 labels:
 severity: "critical"
 annotations:
 description: "Node exporter {{ $labels.instance }} is down."
 link: "https://example.com"

让我们更详细地查看 NodeExporterDown 告警定义。我们可以将配置分为五个不同的部分:alertexprforlabelsannotations。接下来,我们将在下表中逐一介绍这些部分:

部分 描述 是否必需
alert 使用的告警名称
expr 要评估的 PromQL 表达式
for 发送告警前,确保告警已触发的时间,默认为 0
labels 用户定义的键值对
annotations 用户定义的键值对

Prometheus 社区通常使用驼峰命名法(CamelCase)来命名告警。

Prometheus 不会执行验证来检查告警名称是否已被使用,因此可能会有两个或多个告警共享相同的名称,但评估不同的表达式。这可能会导致问题,例如追踪哪个特定的告警触发,或编写告警测试。

NodeExporterDown 规则仅在 job="node" 选择器的 up 指标超过 1 分钟不为 1 时触发,我们现在通过停止 Node Exporter 服务来测试这一点:

vagrant@prometheus:~$ sudo systemctl stop node-exporter
vagrant@prometheus:~$ sudo systemctl status node-exporter

...
Mar 05 20:49:40 prometheus systemd[1]: Stopping Node Exporter...
Mar 05 20:49:40 prometheus systemd[1]: Stopped Node Exporter.

我们现在强制使告警变为活动状态。这将迫使告警经历三个不同的状态:

顺序 状态 描述
1 非活动 尚未进入待处理或触发状态
2 待处理 尚未足够长时间以变为触发状态
3 触发 活跃时间超过定义的 for 子句阈值

进入 Prometheus 服务器网页界面的 /alerts 端点,我们可以查看 NodeExporterDown 告警的三种不同状态。首先,告警处于非活动状态,正如我们在下图中看到的:

图 9.3:NodeExporterDown 告警处于非活动状态

然后,我们可以看到告警处于待处理状态。这意味着,尽管告警条件已经触发,Prometheus 会继续检查该条件是否在每个评估周期中持续触发,直到for时间段过去。下图展示了待处理状态;请注意,显示注释复选框已被选中,这会展开告警注释:

图 9.4:NodeExporterDown 告警处于待处理状态

最后,我们可以看到告警变为触发状态。这意味着告警已活跃超过 for 子句定义的持续时间——在这种情况下是 1 分钟:

图 9.5:NodeExporterDown 告警正在触发

当告警变为触发状态时,Prometheus 会向配置的告警服务端点发送 JSON 负载,在我们的案例中,端点是 alertdump 服务,该服务配置为将日志记录到 /vagrant/cache/alerting.log 文件。这使得我们很容易理解正在发送什么样的信息,并且可以进行如下验证:

vagrant@prometheus:~$ cat /vagrant/cache/alerting.log
[
   {
       "labels": {
           "alertname": "NodeExporterDown",
           "dc": "dc1",
           "instance": "prometheus:9100",
           "job": "node",
           "prom": "prom1",
           "severity": "critical"
       },
       "annotations": {
           "description": "Node exporter prometheus:9100 is down.",
           "link": "https://example.com"
       },
       "startsAt": "2019-03-04T21:51:15.04754979Z",
       "endsAt": "2019-03-04T21:58:15.04754979Z",
       "generatorURL": "http://prometheus:9090/graph?g0.expr=up%7Bjob%3D%22node%22%7D+%21%3D+1&g0.tab=1"
   }
]

现在我们已经了解了如何配置一些告警规则,并验证了 Prometheus 向配置的告警系统发送的内容,接下来让我们探索如何通过使用标签和注释来丰富这些告警的上下文信息。

标签和注释

在告警规则定义中,有两个可选部分:标签和注释。标签定义了告警的身份,并且可以根据它们所在的评估周期发生变化;如果发生变化,将会改变告警的身份。为了说明这一点,我们将介绍 ALERTS 指标,它跟踪所有活动告警及其标签。正如我们在下面的图示中看到的,我们有一个名为 alertstate 的标签,它跟踪告警的状态,并且会从 pending 状态过渡到 firing 状态:

图 9.6:ALERTS 指标

需要注意的是,标签中使用示例值的问题。虽然技术上是可行的,但这是一个非常糟糕的主意。这样做每次值发生变化时都会改变告警的身份,从而始终会重置定义的 for 倒计时,导致告警永远不会进入 firing 状态。

另一方面,注释不属于告警的身份,因此它们不会存储在 ALERTS 指标中。这些注释对于丰富告警的上下文信息非常有用。注释也使用 Go 模板语言进行模板化,就像我们在示例中看到的那样。通过使用 {{ .Labels.instance }} 模板语法,我们可以访问可用的告警标签,选择 instance 标签,并在注释的 description 字段中使用其值。如果需要,还可以通过 {{ .Value }} 来获取触发样本的值。

Golang 模板中的 .Labels.Value 变量也可以作为 $labels$value 使用,以方便操作。

以下代码段展示了我们示例中的告警规则:

   annotations:
     description: "Node exporter {{ .Labels.instance }} is down."
     link: "https://example.com"

当告警处于 firing 状态时,将产生以下渲染结果:

       "annotations": {
           "description": "Node exporter prometheus:9100 is down.",
           "link": "https://example.com"
       },

您可以在 golang.org/pkg/text/template/ 获得更多关于 Golang 模板的信息。

告警延迟

在前面的主题中,我们讨论了警报会经历的三个状态;但是在计算警报变为触发状态所需的总时间时,还需要考虑其他因素。首先是抓取间隔(在我们的示例中是 30 秒,虽然通常抓取和评估间隔应该相同以便于理解),然后是规则评估间隔(在我们的例子中全局定义为 1 分钟),最后是警报规则中 for 子句定义的 1 分钟。如果将这些变量都考虑在内,警报被认为是 firing 的时间,在最坏情况下可能需要 2 分钟 30 秒。下图展示了这个示例情况:

图 9.7:警报延迟可视化

所有这些延迟仅发生在 Prometheus 端。处理发送警报的外部服务可能会有其他约束,这可能导致从发送通知到最终发送的全球延迟更长。

在 Prometheus 2.4.0 之前,pendingfiring 状态在重启后不会持久化,这可能会进一步延长警报的延迟。通过实现一个新的度量指标 ALERTS_FOR_STATE,来存储警报状态,从而解决了这个问题。你可以在 github.com/prometheus/prometheus/releases/tag/v2.4.0 查阅 Prometheus 2.4.0 的发布说明。

测试你的规则

在第八章《故障排除与验证》中,我们介绍了 promtool 的一些功能,测试功能除外。test rules 子命令可以模拟多个时间序列的周期性样本摄取,利用这些时间序列评估记录和警报规则,然后测试记录的系列是否与配置的预期结果匹配。现在我们已经了解了记录和警报规则,接下来将通过创建单元测试并使用 promtool 来验证我们的规则,确保它们按预期工作。

记录规则测试

Prometheus 二进制分发包中包含的 promtool 工具允许我们定义测试用例来验证我们编写的规则是否按预期工作。本章的测试环境还提供了一套针对我们到目前为止探索的规则的预构建测试。你可以在这里查看配置:

vagrant@prometheus:~$ cat /etc/prometheus/tests.yml

该文件包含本章介绍的所有记录和告警规则的测试。虽然你不需要在一个文件中定义所有的测试(实际上,为了保持组织性,每个规则组使用单独的测试文件更为整洁),但为了简便,这里统一在一个文件中进行了定义。现在,我们只分析记录规则,因为它们更容易理解。测试文件的顶层配置键定义了要加载的规则文件以及测试的默认评估间隔,它决定了当规则没有明确指定自己的评估周期时,记录和告警规则评估的周期性:

rule_files:
  - /etc/prometheus/recording_rules.yml
...
evaluation_interval: 1m

虽然测试文件中的 rule_files 配置键看起来与主 Prometheus 配置文件中的相同,但它不支持通配符(使用文件名通配符)。

在这些全局配置之后,紧接着是 tests 键下的测试用例定义。你可以定义多个测试包,每个包有自己独立的模拟抓取间隔、采集的系列和待测规则。让我们看看文件中定义的第一个测试,我们在其中添加了一些注释以便于理解:

tests:

interval 设置了在我们的模拟时间序列中生成间隔样本的时间:

- interval: 15s

input_series 的列表定义了生成哪些时间序列以及在每次模拟采集间隔的迭代中生成哪些值:

    input_series:
      - series: 'node_cpu_seconds_total{cpu="0",instance="prometheus:9100",job="node",mode="user"}'
        values: '1 1 1 1 1 1 1 1 1 1'
      - series: 'node_cpu_seconds_total{cpu="1",instance="prometheus:9100",job="node",mode="user"}'
        values: '1 1 1 1 1 1 1 1 1 1'
      - series: 'node_cpu_seconds_total{cpu="0",instance="example:9100",job="node",mode="idle"}'
        values: '1 1 1 1 1 1 1 1 1 1'
      - series: 'node_cpu_seconds_total{cpu="0",instance="example:9100",job="node",mode="system"}'
        values: '1 1 1 1 1 1 1 1 1 1'

用于测试的 PromQL 表达式列表被定义为 promql_expr_test

promql_expr_test:

每个 expr 定义了一个特定的表达式:

- expr: instance:node_cpu:count

设置 eval_time 来确定该表达式运行的时间点,预期的样本应该通过运行该表达式返回,称为 exp_samples

        eval_time: 1m
        exp_samples:
          - labels: 'instance:node_cpu:count{instance="prometheus:9100", job="node"}'
            value: 2
          - labels: 'instance:node_cpu:count{instance="example:9100", job="node"}'
            value: 1

在这个测试包中,我们可以看到每 15 秒生成四个时间序列,都是针对相同的指标 node_cpu_seconds_total。由于这些序列的实际值对该记录规则并不重要(它仅计算每个实例的 CPU 数量),因此每个样本的值都设置为 1。请注意标签的变化,具体来说,prometheus:9100 实例报告的是两个 CPU 的指标,而 example:9100 实例报告的是一个 CPU。实际测试只是验证,当 instance:node_cpu:count 表达式在 t=1m 时评估(假设从生成的采集开始经过了 1 分钟),返回的样本应显示每个实例的正确 CPU 数量。

我们现在可以使用以下指令来执行测试:

vagrant@prometheus:/etc/prometheus$ promtool test rules tests.yml 
Unit Testing: tests.yml
 SUCCESS

这确保了配置的记录规则按我们预期的方式运行。你可以通过从 prometheus:9100 实例的 instance:node_cpu:count 测试包中移除一个输入序列来尝试破坏测试。当你重新运行测试时,以下内容将显示出来,因为其中一个测试现在失败了:

vagrant@prometheus:/etc/prometheus$ promtool test rules tests.yml 
Unit Testing: tests.yml
  FAILED:
    expr:'instance:node_cpu:count', time:1m0s, 
        exp:"{__name__=\"instance:node_cpu:count\", instance=\"example:9100\", job=\"node\"} 1E+00, {__name__=\"instance:node_cpu:count\", instance=\"example:9100\", job=\"node\"} 1E+00, {__name__=\"instance:node_cpu:count\", instance=\"prometheus:9100\", job=\"node\"} 2E+00", 
        got:"{__name__=\"instance:node_cpu:count\", instance=\"example:9100\", job=\"node\"} 1E+00, {__name__=\"instance:node_cpu:count\", instance=\"example:9100\", job=\"node\"} 1E+00, {__name__=\"instance:node_cpu:count\", instance=\"prometheus:9100\", job=\"node\"} 1E+00"

这个输出告诉我们,promtool 期待的是已定义的样本集,但返回了一个不同的样本集。你可以看到,正如我们配置的那样,记录规则现在只报告了 prometheus:9100 实例的一个 CPU。这让我们对规则的行为是否如我们所愿充满信心。

第二组记录规则的测试大部分相同,但它们展示了生成更丰富输入序列的强大表示法:

  - interval: 5s
    input_series:
      - series: 'prometheus_http_request_duration_seconds_count{handler="/",instance="localhost:9090",job="prometheus"}'
        values: '0+5x60'
      - series: 'prometheus_http_request_duration_seconds_sum{handler="/",instance="localhost:9090",job="prometheus"}'
        values: '0+1x60'

这称为展开表示法。这是一种紧凑的方式,用于声明生成时间序列值的公式。它的形式可以是A+BxCA-BxC,其中 A 是起始值,B 是每次迭代中序列值应增加(前有 +)或减少(前有 -)的量,C 是这种增加或减少应应用多少次。

回到我们的示例,0+5x60 将展开成以下系列:

0 5 10 15 20 … 290 295 300

在声明输入时间序列的值时,您可以将字面值与展开表示法混合使用。这使得您可以轻松创建复杂的行为。以下是一个例子:

0 1 1 0 1+0x3 0 1 1 0 1 1 0 0 0 1 1 0+0x3 1

这将展开成如下所示:

0 1 1 0 1 1 1 1 0 1 1 0 1 1 0 0 0 1 1 0 0 0 0 1

测试对于避免不可预见的问题至关重要,凭借目前所涵盖的信息,您现在可以为记录规则生成自己的单元测试。接下来,我们将继续处理单元测试,但这次特别针对警报规则。

警报规则测试

警报规则的单元测试与记录规则的单元测试非常相似。我们将使用本章之前提供的示例警报来演示如何配置警报测试以及如何验证它们。如前所述,本章的测试环境包含了一套用于测试此处呈现规则的测试,包括我们感兴趣的警报规则。再次提醒,您可以使用以下命令查看测试文件:

vagrant@prometheus:~$ cat /etc/prometheus/tests.yml

专注于警报组件,我们可以看到我们首先定义了警报规则的位置:

rule_files:
  - /etc/prometheus/alerting_rules.yml

默认的规则评估间隔在同一文件中的记录规则和警报规则之间共享:

evaluation_interval: 1m

警报测试便捷地位于自己的测试组中,让我们来看一下它的完整定义:

  - interval: 1m
    input_series:
      - series: 'up{job="node",instance="prometheus:9100"}'
        values: '1 1 1 0 0 0 0 0 0 0'
      - series: 'up{job="prometheus",instance="prometheus:9090"}'
        values: '1 0 1 1 1 0 1 1 1 1'

测试组定义与之前解释的相同,唯一的例外是 alert_rule_test 部分,这里我们定义了警报测试。需要注意的是,在这个示例中,第二个输入序列永远不应被我们的测试规则匹配,因为已定义的警报特别匹配 job="node"

    alert_rule_test:
      - alertname: NodeExporterDown
        eval_time: 3m
      - alertname: NodeExporterDown
        eval_time: 4m
        exp_alerts:
          - exp_labels:
              instance: "prometheus:9100"
              job: "node"
              severity: "critical"
            exp_annotations:
              description: "Node exporter prometheus:9100 is down."
              link: "https://example.com"

alert_rule_testpromql_expr_test 不必放在单独的测试块中;当记录规则和警报规则使用相同的输入时间序列并且具有相同的评估间隔时,您可以将两者放在同一测试组中。

alert_rule_test部分列出了应在模拟测试运行开始时(eval_time)评估哪些警报(alertname)。如果预计该警报会在该时刻触发,则应定义一个额外的exp_alerts部分,列出每个警报实例应包含的预期标签(exp_labels)和注释(exp_annotations)。如果exp_alerts部分为空,意味着该警报在指定的时间不应触发。

第一个警报测试将在第三分钟执行,由于我们之前提供的匹配系列在该时刻返回值1,因此在alerting_rules.yml中定义的警报表达式不会触发——这意味着警报中定义的表达式没有返回数据。

第二个警报规则将在第四分钟执行,并返回数据,因为我们提供的匹配系列在该特定时刻的样本值为0。警报规则返回的所有标签需要明确检查。测试还必须检查警报返回的所有描述,并完全展开任何模板变量。

我们现在可以使用以下指令运行测试:

vagrant@prometheus:~$ promtool test rules /etc/prometheus/tests.yml 
Unit Testing: /etc/prometheus/tests.yml
 SUCCESS

作为额外步骤,尝试将第二个警报的描述从prometheus:9100更改为类似prometheus:9999的内容,然后重新运行测试。你应该会得到以下输出:

vagrant@prometheus:~$ promtool test rules /etc/prometheus/tests.yml 
Unit Testing: /etc/prometheus/tests.yml
  FAILED:
    alertname:NodeExporterDown, time:4m0s, 
        exp:"[Labels:{alertname=\"NodeExporterDown\", instance=\"prometheus:9100\", job=\"node\", severity=\"critical\"} Annotations:{description=\"Node exporter prometheus:9999 is down.\", link=\"https://example.com\"}]", 
        got:"[Labels:{alertname=\"NodeExporterDown\", instance=\"prometheus:9100\", job=\"node\", severity=\"critical\"} Annotations:{description=\"Node exporter prometheus:9100 is down.\", link=\"https://example.com\"}]"

尽管这个警报非常简单,容易确定在何种条件下会触发,但警报规则的测试能够确保在你无法合理重现的环境条件下,警报会触发。

总结

在本章中,我们有机会观察另一种生成衍生时间序列的方法。记录规则通过将重复的重查询预先计算成新的时间序列,帮助提高监控系统的稳定性和性能,这些新时间序列相对便宜,便于查询。警报规则将 PromQL 的强大功能和灵活性带入警报中;它们使得可以为复杂且动态的阈值触发警报,并且使用单个警报规则针对多个实例甚至不同应用进行目标定位。现在,了解警报中如何引入延迟将帮助你根据需求调整它们,但记住,稍微的延迟总比产生噪音的警报要好。最后,我们探索了如何为规则创建单元测试,并在 Prometheus 服务器运行之前验证它们。

下一章将进入监控的另一个组件:可视化。我们将深入探讨 Grafana,这是社区首选的 Prometheus 驱动仪表板工具。

问题

  1. 记录规则的主要用途是什么?

  2. 为什么应避免在规则组中设置不同的评估间隔?

  3. 如果你看到instance_job:latency_seconds_bucket:rate30s指标,你期望看到哪些标签,并且会使用什么表达式来记录它?

  4. 为什么在告警标签中使用告警的样本值是一个不好的主意?

  5. 警报的待处理状态是什么?

  6. 当没有指定 for 子句时,警报从触发到过渡到 firing 状态之间会等待多长时间?

  7. 如何在不使用 Prometheus 的情况下测试你的规则?

深入阅读

第十章:发现并创建 Grafana 仪表板

Prometheus 表达式浏览器非常适合执行探索性查询,但有时我们需要预先构建的可视化来帮助我们快速调试问题。在本章中,我们将深入了解 Grafana,这是 Prometheus 项目推荐的用于构建仪表板的工具。Grafana 社区不断壮大,并通过托管大量现成的仪表板,使得重用它们、贡献社区并改进生态系统变得更加容易。在本章中,我们将学习如何查找和使用社区提供的仪表板,以及如何编写自己的仪表板并为社区做出贡献。最后,我们还会简要介绍控制台,这是一种内建于 Prometheus 中的仪表板解决方案,适用于高级用例。

简而言之,本章将涵盖以下主题:

  • 本章的测试环境

  • 如何将 Grafana 与 Prometheus 一起使用

  • 构建你自己的仪表板

  • 发现现成的仪表板

  • 默认 Prometheus 可视化

本章的测试环境

为了提供一个动手操作的方法,我们将为本章创建一个新的测试环境。我们将使用的设置类似于以下图示:

图 10.1:测试环境网络

部署

要生成本章的虚拟机VM)基础测试环境,前往代码库根目录下的正确存储库路径:

cd ./chapter10/

确保没有其他测试环境在运行,并启动本章的环境,如下所示:

vagrant global-status
vagrant up

你可以使用以下代码验证测试环境的成功部署:

vagrant status

这将产生以下输出:

Current machine states:

prometheus                 running (virtualbox)
grafana                    running (virtualbox)

This environment represents multiple VMs. The VMs are all listed above with their current state. For more information about a specific VM, run `vagrant status NAME`.

当部署任务完成后,你将能够使用你喜欢的支持 JavaScript 的网页浏览器,验证主机上的以下端点:

服务 端点
Prometheus http://192.168.42.10:9090
Grafana http://192.168.42.11:3000

你应该能够通过以下命令之一访问所需的实例:

实例 命令
Prometheus vagrant ssh prometheus
Grafana vagrant ssh grafana

清理

当你完成测试后,只需确保你在 chapter10/ 目录下并执行以下命令:

vagrant destroy -f

不用太担心——如果需要,你可以轻松地重新启动环境。

如何将 Grafana 与 Prometheus 一起使用

Grafana 是最为人所知的开源仪表板项目。它有数据源的概念,数据源不过是与数据后端的集成。截止目前,以下是可用的数据源:

  • Prometheus

  • 石墨

  • InfluxDB

  • Elasticsearch

  • Google Stackdriver

  • AWS CloudWatch

  • Azure Monitor

  • Loki(日志可视化)

  • MySQL

  • PostgreSQL

  • Microsoft SQL Server

  • OpenTSDB

  • TestData(用于生成测试的虚拟数据)

已经做了很多努力来提升 Prometheus 在 Grafana 中的集成性——例如,PromQL 自动补全。目前,Grafana 是任何想要可视化 Prometheus 数据的人的首选仪表板解决方案。不过,前面的说法并不完全准确,因为我们知道,对于探索性查询来说,没有什么比 Prometheus 表达式浏览器更好的了。然而,最近在 6.0.0 版本发布后,Grafana 引入了一项名为 Explore 的功能,作为替代的表达式浏览器。

你可以在 grafana.com/grafana/download 下载多个操作系统和发行版的安装文件。

Grafana 在构建时就考虑到了自动化。以下示例将展示你如何在不触碰主要配置文件的情况下几乎完成环境的设置。一个值得注意的好处是,Grafana 本身就已经集成了 Prometheus 的度量指标。

登录界面

在测试环境运行时,你可以通过 http://192.168.42.11:3000 网址访问 Grafana。用户将看到一个简单的登录界面,如下所示:

图 10.2:Grafana 登录界面

默认的身份验证凭证如下:

用户名 密码
admin admin

登录成功后,我们将进入 Grafana 的首页,首页会显示一个设置向导。接下来,我们将解释每个配置步骤。以下截图展示了向导,其中一些步骤已完成配置:

图 10.3:Grafana 首页

为了提高可读性,我们将默认的 Grafana 主题改为 Light,而不是 Dark。这可以在 首选项 菜单中的 配置 标签(左侧的小齿轮图标)轻松配置。

数据源

为了能够查询数据,我们必须配置一个数据源。在我们的案例中,我们将把 Prometheus 实例添加为默认的数据源。为此,我们需要指明数据源的位置、所需的身份验证/授权信息,以及任何其他特定的数据源配置。

配置数据源有两种方式。一种方式是通过在 Grafana 配置路径中添加一个包含所需配置的 YAML 文件,服务在启动时会自动读取并配置。这就是我们在本章的测试环境中所做的,因为它是自动化部署的更好解决方案。当连接到测试环境中的 grafana 实例时,你可以通过查看默认配置路径,看到我们正在使用的配置,路径如下所示:

vagrant@grafana:~$ cat /etc/grafana/provisioning/datasources/prometheus.yaml 
apiVersion: 1

datasources:
- name: prometheus
 type: prometheus
 access: proxy
 orgId: 1
 url: http://prometheus:9090
 isDefault: True
 version: 1
 editable: True

另一种选择是通过进入配置(左侧的小齿轮图标)| 数据源,手动添加设置选项。在点击保存并测试后,Grafana 将验证设置,并告知你是否存在任何问题。Grafana 提供了两种访问提供 HTTP 基础 API(如 Prometheus)的数据源的选项:是否代理请求。当代理请求时,从仪表板面板或通过探索表达式浏览器发出的每一个查询都会通过 Grafana 后端代理到数据源。尽管这样可以集中管理数据源凭证,并关闭除了受信任的客户端之外的所有直接网络访问,但它会增加 Grafana 实例的负载,因为需要处理更多的流量。不代理请求意味着客户端浏览器每次请求都会直接访问数据源。此配置要求访问 Grafana 的用户也能直接访问使用的数据源,并且该数据源的安全设置允许来自不同来源的请求。在我们所有的示例中,Grafana 都会设置为代理查询。

以下截图展示了我们在测试环境中使用的配置:

图 10.4:数据源配置界面

请注意,访问选项设置为服务器(默认)。这意味着所有的数据源请求将通过 Grafana 实例进行代理。

探索

这个功能是在 Grafana 6 中引入的,开发人员持续改进其与 Prometheus 的紧密集成。在探索模式之前,每次你想执行探索查询时,都需要跳转到 Prometheus 表达式浏览器。

除了这个便利性外,还有一些值得注意的功能,使得探索模式独具特色,如下所示:

  • 指标列表:在左上角,我们可以找到一个名为指标的组合框。它以层次结构的形式列出了指标,按前缀分组,并且在符合双冒号命名规范时,能够自动检测和分组记录规则。

  • 查询字段:除了建议和自动完成指标外,查询字段还会显示有关 PromQL 函数的有用工具提示,甚至可以展开在原始表达式中检测到的记录规则。

  • 上下文菜单:你可以选择直接在探索页面中打开来自任何仪表板面板的查询。

以下截图展示了探索界面,同时显示了正在使用的 PromQL 函数的工具提示:

图 10.5:Grafana 探索页面,显示 label_replace 函数的工具提示

通常可以通过点击左侧的小指南针图标找到探索模式。

仪表盘

类似于管理数据源,添加仪表板有几种方法,列举如下:

  • 通过手动构建你自己的

  • 通过导入 grafana.com 社区驱动的仪表板

  • 通过自动配置之前存储的仪表板

我们现在将处理最后一种方法,因为测试环境正在使用这种方法。我们将在后续章节中关注另外两种方法。

仪表板文件是仪表板的声明性表示,包含所有必要的设置,并使用 JSON 格式。如果你将其放置在预期的配置路径中,Grafana 服务将在启动时加载它。在我们的示例中,我们使用了默认路径,正如以下代码片段所示:

vagrant@grafana:~$ ls /etc/grafana/dashboards/
node_exporter_basics.json

你可以通过进入页面左上角的 首页 菜单并选择 node_exporter_basics 来找到这个仪表板。它在视觉上对应于以下屏幕截图:

图 10.6:已自动配置的示例仪表板

在 Kubernetes 上运行的 Grafana

在 Kubernetes 上部署 Grafana 基本上与在虚拟机上部署的方式相同,因此我们只关注一些操作员应当注意的细节。用于在我们的 Kubernetes 测试环境中启动 Grafana 和 Prometheus 的 Kubernetes 清单可以在代码库根路径的以下位置找到:

cd ./chapter10/provision/kubernetes/

由于 Kubernetes 部署过程与前几章相同(引导 Prometheus Operator,使用 Operator 部署 Prometheus,并部署导出器及其相应的 ServiceMonitor),此处将不再详细介绍。如果需要更多上下文,请随时查看前几章中的测试环境操作说明,例如 第七章,Prometheus 查询语言 - PromQL

以下步骤将确保创建一个新的 Kubernetes 环境,并预配置所有必要的软件,从而使我们能够专注于 Grafana 组件。

  1. 验证没有其他环境正在运行:
minikube status
minikube delete
  1. 使用以下命令启动一个空的 Kubernetes 环境:
minikube start \
  --cpus=2 \
  --memory=3072 \
  --kubernetes-version="v1.14.0" \
  --vm-driver=virtualbox
  1. 添加 Prometheus Operator 组件并按如下方式进行部署:
kubectl apply -f ./bootstrap/
kubectl rollout status deployment/prometheus-operator -n monitoring
  1. 使用以下命令添加新的 Prometheus 集群,并确保其成功:
kubectl apply -f ./prometheus/
kubectl rollout status statefulset/prometheus-k8s -n monitoring
  1. 将所有目标添加到 Prometheus,并使用以下命令列出它们:
kubectl apply -f ./services/
kubectl get servicemonitors --all-namespaces

现在 Kubernetes 环境已经运行,我们可以继续进行 Grafana 特定的配置。类似于专注于虚拟机的测试环境,我们不仅需要配置 Grafana 本身,还需要配置数据源和仪表板。

对于数据源,由于我们可能将来需要添加敏感信息(如身份验证),我们将使用 Kubernetes 秘密。这也意味着应该有一个 ServiceAccount 用于访问该秘密。

我们可以通过应用以下清单来创建 ServiceAccount:

kubectl apply -f ./grafana/grafana-serviceaccount.yaml

由于我们使用的是秘密,因此数据源配置需要进行 base64 编码。至于配置本身,与虚拟机部署中的配置相同,但我们将使用由服务管理的 Kubernetes 等效 URL 来替换 Prometheus URL。以下是编码前的片段:

...
datasources:
- name: prometheus
...
 url: http://prometheus-service.monitoring.svc:9090
...

应用以下清单后,将会有一个新的秘密,包含所需的 Grafana 数据源:

kubectl apply -f ./grafana/grafana-datasources-provision.yaml

现在,是时候将我们的示例仪表板添加到 Grafana 中了。为此,我们需要向 Grafana 提供一个配置文件,告诉它在哪里查找仪表板定义,然后将我们的示例仪表板定义放到该路径中。这些将作为 ConfigMap 可用于 Grafana 部署。显示仪表板位置配置的相关片段如下:

...
data:
  dashboards.yaml: |-
    {
        "apiVersion": 1,
        "providers": [{
           "folder": "",
           "name": "default",
           "options": {
             "path": "/etc/grafana/dashboards" 
           },
           "orgId": 1,
           "type": "file"
         }]
    }
kind: ConfigMap
...

另一个ConfigMap包含我们的示例仪表板,具体如下所示:

...
data:
  node_exporter_basics.json: |-
    {
...
    }
kind: ConfigMap
...

两个清单可以使用以下命令在 Kubernetes 测试环境中部署:

kubectl apply -f ./grafana/grafana-dashboards-provision.yaml

kubectl apply -f ./grafana/grafana-dashboards.yaml

现在是部署 Grafana 并利用之前所有配置的时候,具体操作如下:

kubectl apply -f ./grafana/grafana-deployment.yaml

这个部署将所有内容整合在一起:它挂载了数据源的秘密,并且将仪表板配置和仪表板 ConfigMap 放置在与虚拟机测试环境相同的位置,如下所示:

... 
        volumeMounts:
        - name: grafana-datasources-provision
          mountPath: /etc/grafana/provisioning/datasources
        - name: grafana-dashboards-provision
          mountPath: /etc/grafana/provisioning/dashboards
        - name: grafana-dashboards
          mountPath: /etc/grafana/dashboards
...

您可以通过以下指令跟踪部署状态:

kubectl rollout status deployment/grafana -n monitoring

最后,我们可以添加一个服务,以便访问新启动的 Grafana 实例,并添加一个 ServiceMonitor,使 Prometheus Operator 配置 Prometheus 来收集指标:

kubectl apply -f ./grafana/grafana-service.yaml

kubectl apply -f ./grafana/grafana-servicemonitor.yaml

现在,您可以使用以下命令访问 Grafana 界面:

minikube service grafana -n monitoring

测试完成后,您可以通过执行以下命令删除这个基于 Kubernetes 的测试环境:

minikube delete

这个设置为您提供了一个关于如何在 Kubernetes 上集成 Grafana 和 Prometheus 的快速概览。它与虚拟机测试环境没有太大区别,但这里展示的细节希望能避免您需要到处搜索如何实现的资料。

创建您自己的仪表板

在提供的虚拟机测试环境中,您有机会尝试捆绑的仪表板。现在,您应该学习如何创建自己的仪表板,为此,您需要掌握一些概念。在本节中,我们将引导您完成创建仪表板的过程。

仪表板基础

仪表板由多个组件组成。我们将在接下来的章节中介绍最重要的概念,包括面板、它们支持的可视化、如何模板化变量,以及如何更改显示数据的时间范围。

面板

面板是仪表板可视化区域中的一个矩形插槽。以下截图展示了一个示例。它可以通过拖放调整其各个维度的大小和位置。你还可以将多个面板放置在一个行中,行只是这些面板的逻辑分组。行可以展开或折叠,以显示或隐藏其中的面板:

图 10.7:新面板

一个面板,除了能够查询选定的数据源外,还提供多种可视化选项供选择。这些可视化选项可以以多种方式展示数据,如简单的单值面板、条形图、折线图、表格,甚至热力图。下图展示了可用的内置可视化选项:

图 10.8:内置可视化选项

在前面的截图中,我们可以看到几种面板类型。以下是四种最常用的面板类型:

  • Graph:这是 Grafana 的主面板。它提供了创建富有表现力的二维图形的工具,这些图形由一个或多个 PromQL 表达式支持。

  • Singlestat:这是一个多用途的单值显示器。因此,PromQL 查询必须返回一个只有一个样本的瞬时向量。

  • Gauge:使用阈值,这个选项表示当前值相对于定义的上下限位置。像 Singlestat 可视化一样,这个选项需要一个只有一个样本的单一瞬时向量。

  • Table:这是以表格格式显示 PromQL 表达式结果的面板,每个标签都在自己的列中,并显示相应的值和时间戳。

对于每种可用的可视化选项,都有大量的设置选项,允许对每个面板的外观进行极为详细的自定义。官方的 Grafana 文档详细解释了每个选项,因此我们在这里将重点介绍最相关的选项。

变量

变量功能非常强大。它允许仪表板配置占位符,可以在表达式中使用,这些占位符可以通过静态或动态列表填充,通常以下拉菜单的形式呈现给仪表板用户。每当选定的值发生变化时,Grafana 将自动更新使用该特定变量的面板中的查询。在我们的示例仪表板中,我们使用此功能让用户选择要展示的节点实例。除了常用于查询之外,它们也可用于面板标题等其他地方。

此功能可以在 仪表板设置 中找到,通过点击右上角的齿轮图标进入,这个图标在任何仪表板内都可以找到。下图展示了 设置 菜单中的 node_exporter_basics 仪表板的 Variables 选项:

图 10.9:仪表板变量

如你所见,我们使用了一个 PromQL 查询来动态获取$instance变量的可能值。

当视口不够大时,Grafana 的响应式设计会隐藏一些右上角的图标。

时间选择器

时间选择器功能在任何仪表板的右上角都有一个包含时钟图标的按钮。界面分为两个主要区域:快速范围,即预定义的时间范围(大多数是相对于当前时间的),或自定义范围,允许你指定用于所有仪表板面板的确切时间跨度。顾名思义,每隔:选项将使仪表板面板在指定的时间间隔内自动重新加载。将其与相对时间范围结合使用时,这对于查看新数据非常有用。

以下截图显示了一系列的快速范围:

图 10.10:时间选择器

创建一个基本的仪表板

我们将通过引导你完成创建一个基础仪表板的过程来亲自体验 Grafana。你可以通过点击左侧的加号图标 | 仪表板来开始。这将打开一个新的空白仪表板,并且已经有一个新面板,准备进行编辑。因为我们想要一个动态仪表板,所以我们将创建一个新的变量,这个变量将扩展为我们 Prometheus 服务器中可用的 Node Exporter 实例的列表。

使用Shift + ?的快捷键组合将显示所有可用快捷键的帮助提示。

为了实现我们的目标,我们必须点击右上角的齿轮图标,打开仪表板设置,然后选择变量。下图展示了在创建此类变量时可用的选项:

图 10.11:变量界面

上述截图中显示的预览值展示了基于虚拟机的测试环境中的 Node Exporter 目标。如果你跟随使用 Kubernetes 测试环境,你将看到不同的预览。

在这个示例中,我们正在创建一个名为instance的变量,使用查询类型,这意味着它将从查询数据源的结果中填充其值。我们将数据源指定为prometheus,即在配置时给定的标识符,并且我们希望只有在仪表板加载时才刷新该变量。

现在进入有趣的部分:因为我们有兴趣收集 Node Exporter 实例,我们在查询字段中使用了一个指标,确保能够返回我们所需要的实例,node_exporter_build_infolabel_values()函数实际上不是有效的 PromQL 语法,但它是 Grafana 中的 Prometheus 数据源插件提供的,用于在该字段中使用,以便支持这种表达式。

Regex 字段用于匹配我们想要用于填充变量的查询结果部分。在我们的示例中,我们希望获取实例标签的完整内容,因此我们在正则表达式捕获组 (.+) 中匹配所有内容。我们可以在屏幕底部的 值预览 部分看到匹配是否生效。点击 添加 并保存此仪表板为 example 后,我们现在可以看到包含 instance 变量值的下拉菜单:

图 10.11:实例值

现在是创建我们第一个面板的时候了。点击右上角的图表图标,在新面板中点击 添加查询。以下截图展示了查询接口:

图 10.12:查询接口

在这里,我们可以指定要在所需数据源上执行的 PromQL 查询(一个或多个,取决于可视化类型)。在我们的示例中,我们将创建一个按模式划分的 CPU 使用率图,并希望将查询模板化,使其使用我们之前创建的 instance 变量。请注意 $instance,在查询时它将被替换为 instance 下拉框中选择的值。完整表达式如下:

label_replace(avg by (mode, instance) (irate(node_cpu_seconds_total{instance="$instance", mode!="idle"}[5m])), "instance", "$1", "instance", "([^:]+):.+")

label_replace() 函数允许我们从实例值中移除端口,这将在 图例 字段中使用。此字段允许用其中设置的指标标签的值替换 {{ }} 模板标记。这样,在图表图例中将反映出在 查询 菜单之前的内容。以下截图中,我们可以看到应用于仪表板的多个视觉选项,我们将逐一介绍:

图 10.13:可视化接口

部分,我们选择启用哪个图表轴;右侧 Y 配置选项没有产生任何变化,因为该轴未被使用。在 左侧 Y 配置中,我们可以指定 单位。在我们的例子中,我们想要百分比;我们本可以简单地将表达式乘以 100,但不这么做却展示了 Grafana 的一个实用功能。如我们所知,值的范围是从 0 到 1;这种 单位 类型将会把 0-1 范围内的值自动转换为百分比(从 0 到 100)。我们还确保 Y-最小值 设置为 0,这样图表在视觉上更容易理解,因为如果没有此设置,图表的 y 轴会根据查询结果中的最小 Y 值进行适应。此外,为了便于本示例,我们希望 y 轴刻度显示三位小数,因此我们通过 小数位数 字段设置了这一点。在 X 轴 中,我们没有做任何更改,因为我们希望显示时间。

图例部分,我们控制图例的显示方式以及它在面板中的位置。在我们的案例中,我们希望它作为一个表格,放置在图表的右侧,并且我们希望它显示Y的平均值和当前值。

要完成我们的面板,我们需要进入常规菜单,如下图所示,在这里我们可以命名面板并添加描述。描述将在面板的左上角以小i图标形式显示:

图 10.14:面板的常规菜单

要保存你的新仪表板,只需点击右上角的小软盘图标。现在,你已经从头创建了一个简单的仪表板!你可以继续添加面板和可视化内容,但主要概念基本相同。你可以在测试环境中探索提供的仪表板,查看更多如何使用不同可视化选项的例子。

创建仪表板时需要记住的一点是避免不必要的杂乱。通常可以看到仪表板上有几十个面板,显示着大量的数据。尽量保持信息的适量,这样例如故障排除时可以快速且无痛。焦点是这里的关键:如果仪表板内有不相关的面板,可能将它们分成单独的仪表板会是一个好主意。

导出仪表板

Grafana 使得导出仪表板变得简单。要进行导出,只需打开你想导出的仪表板,点击右上角面板中的小带箭头的方框图标,靠近软盘图标。

将会打开以下表单:

图 10.15:Grafana 仪表板导出

在这里,你会看到几个选项:

  • 外部共享导出:启用数据源名称的模板化,这对公开共享仪表板非常有帮助。为了在grafana.com网站上发布仪表板,这是强制要求的。

  • 查看 JSON:允许你查看仪表板的代码。

  • 保存到文件:将仪表板保存为 JSON 文件。

接下来,我们将看看如何从 Grafana 仪表板画廊下载仪表板,以及如何将自己的仪表板贡献到其中。

发现现成的仪表板

由于 Grafana 被广泛使用,并且拥有庞大的社区支持,显然有大量仪表板是由该社区创建的。Grafana 的开发团队提供了一项服务,注册用户可以将他们的仪表板发布到画廊中,任何人都可以下载并在自己的 Grafana 实例中安装它们。在接下来的部分,我们将概述这两项操作,不仅包括如何使用社区制作的仪表板,还包括如何发布自己的仪表板。

Grafana 仪表板画廊

社区驱动和官方的仪表盘可以在 grafana.com/dashboards 上找到,正如预期的那样,有很多选择。由于我们对 Prometheus 特定的仪表盘感兴趣,因此在搜索站点时,应该将搜索结果限制为该数据源。通过应用额外的筛选器,我们继续缩小结果范围,正如你在以下截图中看到的那样:

10.16:Grafana.com 过滤后的仪表盘结果

然后我们可以选择感兴趣的仪表盘,接着会打开一个屏幕,如下图所示:

图 10.17:Grafana.com 选定的仪表盘信息

在这里,我们可以看到一些关于仪表盘的信息和它的截图。请注意右侧的 ID 9916;它是 Grafana 库中该特定仪表盘的唯一标识符。我们可以通过进入 Grafana 实例(如我们测试环境中的实例),点击左侧主菜单中的加号,选择子菜单中的导入,并将其粘贴到相应的文本框中,如下图所示:

图 10.18:导入仪表盘界面

在粘贴 ID 后,会弹出一个新菜单,要求填写该仪表盘的名称、它应放置在哪个文件夹中以及应使用的数据源。如果与已有的仪表盘(例如同名仪表盘)发生冲突,系统会要求你先解决冲突,才能完成导入过程。

发布你的仪表盘

发布新创建的仪表盘非常简单。首先,确保你有一个 Grafana 网站的账户,可以通过 grafana.com/signup 上的注册表单进行注册。成功注册后,可以通过个人 | 我的仪表盘进入你的个人资料,或者使用 https://grafana.com/orgs/<user>/dashboards 链接,替换 <user> 为你的 Grafana 用户名。

我的仪表盘中,你现在可以点击上传仪表盘按钮。此操作将打开一个上传表单,要求上传仪表盘。请记住,只有导出时启用了导出以供外部分享选项的仪表盘才能被接受:

图 10.19:仪表盘上传表单

完成!现在你将获得一个仪表盘的数字 ID,你可以开始使用它或与全球分享。如果你愿意,还可以更新发布的仪表盘,因为这不会改变生成的 ID。相反,它会创建该仪表盘的另一个版本,用户将始终下载最新的版本。

默认 Prometheus 可视化

历史上,Prometheus 曾维护自己的工具来创建仪表盘,叫做 PromDash。随着时间的推移,由于 Grafana 增强了对 Prometheus 作为数据源的原生支持,社区开始倾向于使用 Grafana 作为主要的可视化解决方案,以至于 PromDash 被维护 Prometheus 的人弃用,转而支持 Grafana。

你可以在 github.com/prometheus-junkyard/promdash 找到 PromDash 的源代码。

尽管 Grafana 是大多数人推荐的可视化解决方案,Prometheus 也附带了一个名为控制台模板的内部仪表盘功能。这些控制台模板是用原始 HTML/CSS/JavaScript 编写的,并利用 Go 模板语言的强大功能生成由 Prometheus 服务器本身提供的仪表盘(称为控制台)。这使得它们既极其快速,又具有无限的可定制性。控制台模板既强大又复杂。我们将在下一节中通过简要概述如何使用和构建控制台模板来介绍这一功能。

开箱即用的控制台模板

当你解压 Prometheus 发布包时,除了服务器和 promtool 的二进制文件外,还可以找到一些现成的控制台模板。为了更清楚地说明,我们可以查看在测试环境中解压这些模板到系统路径中的内容,如下所示:

vagrant@prometheus:/usr/share/prometheus$ systemctl cat prometheus
...
ExecStart=/usr/bin/prometheus \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.path=/var/lib/prometheus/data \
 --web.console.templates=/usr/share/prometheus/consoles \
 --web.console.libraries=/usr/share/prometheus/console_libraries
...

这两个目录需要正确配置才能使控制台正常工作。控制台库定义了辅助函数,这些函数随后在控制台模板中使用,以减少重复。我们将在下一节中仔细查看这些库,当我们构建自己的模板时。

目前,以下是随 Prometheus 一起提供的控制台模板:

vagrant@prometheus:~$ ls -lh /usr/share/prometheus/consoles 
total 36K
-rw-r--r-- 1 root root 623 Mar 10 16:28 index.html.example
-rw-r--r-- 1 root root 2.7K Mar 10 16:28 node-cpu.html
-rw-r--r-- 1 root root 3.5K Mar 10 16:28 node-disk.html
-rw-r--r-- 1 root root 1.5K Mar 10 16:28 node.html
-rw-r--r-- 1 root root 5.7K Mar 10 16:28 node-overview.html
-rw-r--r-- 1 root root 1.4K Mar 10 16:28 prometheus.html
-rw-r--r-- 1 root root 4.1K Mar 10 16:28 prometheus-overview.html

正如在 index.html.example 中所看到的,这些模板期望 Prometheus 和 Node Exporter 的抓取作业分别命名为 prometheusnode,因此它们可能无法直接在你的 Prometheus 配置中使用。

我们可以通过使用 http://192.168.42.10:9090/consoles/index.html.example 的 Web 界面 URL 访问它,并探索可用的控制台。以下截图展示了 Prometheus 实例的节点 CPU 控制台:

图 10.20:Prometheus 的节点 CPU

控制台模板基础

从零开始创建控制台模板有一个陡峭的学习曲线。与 Grafana 不同,控制台模板直接用 HTML 和 JavaScript 编写,并且混合了相当多的 Go 模板语言。这意味着控制台在技术上可以采用任何形式,但为了简化,我们将坚持使用内置控制台库提供的结构。

支撑示例控制台模板的库定义了控制台的框架。它们处理的内容包括构建 HTML 结构、包含必要的 CSS 和 JavaScript 以及围绕主控制台内容建模四个部分:顶部的导航栏、左侧的菜单、底部的控制台时间控制和右侧显示摘要统计的表格。让我们通过查看以下代码,看看如何利用它们来构建一个简单的控制台模板:

{{template "head" .}}

{{template "prom_content_head" .}}

head模板展开为定义包含 CSS 和 JavaScript、顶部导航栏以及菜单的 HTML;而prom_content_head模板则定义了时间控制,如以下代码所示:

<h1>Grafana</h1>

<h3>Requests by endpoint</h3>
<div id="queryGraph"></div>
<script>
new PromConsole.Graph({
  node: document.querySelector("#queryGraph"),
  expr: "sum(rate(http_request_total{job='grafana'}[5m])) by (handler)",
  name: '[[ handler ]]',
  yAxisFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
  yHoverFormatter: PromConsole.NumberFormatter.humanizeNoSmallPrefix,
  yUnits: "/s",
  yTitle: "Requests"
})
</script>

本节定义了控制台本身。queryGraph元素用作占位符,图形 JavaScript 库将使用它来生成图表。而 JavaScript 代码片段则配置了图表所使用的选择器(node)、要绘制的表达式(expr)、图例中使用的内容(name)以及几个y轴配置,如以下代码所示:

{{template "prom_content_tail" .}}

{{template "tail"}}

最后两个模板关闭了第一个模板所打开的部分。它们是必需的,以确保生成的 HTML 结构良好。

结果控制台可以在本章的测试环境中访问,可以通过http://192.168.42.10:9090/consoles/grafana.html查看。以下是它应该呈现的截图:

图 10.21:Grafana 每秒请求示例控制台

请注意,左侧的菜单中没有链接到我们新创建的控制台模板。这是因为包含的menu.lib仅支持随 Prometheus 一起提供的示例控制台模板。在部署实际的自定义控制台模板时,您需要用自己的库替换此库。这将使您能够在顶部的导航栏中添加指向其他内部系统的链接,并列出哪些控制台应出现在左侧的导航菜单中。通过利用您可以在模板中执行 PromQL 查询的事实,您应该能够找出该 Prometheus 实例抓取了哪些作业,并生成类似名称控制台的链接。

总结

在本章中,我们探讨了 Prometheus 的标准可视化工具:Grafana。我们学习了如何配置不仅是数据源,还有仪表盘。在了解仪表盘的构建模块后,我们从零开始创建了一个简单的仪表盘,逐步学习了其中的各个环节。我们还学习了如何利用蓬勃发展的社区构建的仪表盘库。回馈社区始终很重要,因此我们经历了导出和发布仪表盘的过程。最后,我们了解了 Prometheus 默认的可视化——控制台——尽管它们有陡峭的学习曲线,但非常强大。

在下一章中,我们将探讨 Alertmanager,如何充分利用其功能,并与 Prometheus 集成。

问题

  1. 如何在 Grafana 中自动配置数据源?

  2. 从 Grafana 图库导入仪表盘的步骤是什么?

  3. Grafana 仪表盘变量是如何工作的?

  4. 仪表盘的构建块是什么?

  5. 当你更新发布到 grafana.com 的仪表盘时,是否会更改其 ID?

  6. 在 Prometheus 中,控制台是什么?

  7. 为什么要使用 Prometheus 控制台模板?

进一步阅读

第十一章:理解与扩展 Alertmanager

警报是任何监控堆栈中的关键组件。在 Prometheus 生态系统中,警报及其后续通知是解耦的。Alertmanager 是处理这些警报的组件。在本章中,我们将专注于使用 Alertmanager 将警报转换为有用的通知。从可靠性到自定义,我们将深入探讨 Alertmanager 服务的内部工作原理,提供所需的知识,以配置、排除故障并自定义所有可用选项。我们将确保警报路由、静默和抑制等概念清晰,以便你能够决定如何在自己的堆栈中实现它们。

由于 Alertmanager 是一个关键组件,我们还将探讨高可用性,并解释 Prometheus 与 Alertmanager 之间的关系。我们将自定义通知,并学习如何构建和使用可重用的模板,以确保通知在到达目的地时是适当的,并且传递准确的信息。本章的最后,我们将学习如何监控监控系统,更重要的是,学习当系统部分或完全宕机时如何收到警报。

在本章中,我们将探讨以下主题:

  • 本章的测试环境。

  • Alertmanager 基础知识。

  • Alertmanager 配置。

  • 常见的 Alertmanager 通知集成。

  • 自定义你的警报通知。

  • 谁来监视“监视者”?

设置测试环境

为了使用 Alertmanager,我们将新增三个实例来模拟一个高可用的设置。这种方法不仅能让我们暴露所需的配置,还能验证一切如何协同工作。

我们将使用的设置类似于以下图示:

图 11.1:测试环境

部署

让我们从部署 Alertmanager 测试环境开始:

  1. 要启动一个新的测试环境,请进入本章相对于仓库根目录的路径:
cd ./chapter11/
  1. 确保没有其他测试环境在运行,然后启动本章的环境:
vagrant global-status
vagrant up
  1. 你可以通过以下命令验证测试环境的成功部署:
vagrant status

你将收到以下输出:

Current machine states:

prometheus running (virtualbox)
alertmanager01 running (virtualbox)
alertmanager02 running (virtualbox)
alertmanager03 running (virtualbox)

This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.

当部署任务完成后,你将能够使用你喜欢的支持 JavaScript 的网页浏览器,在主机上验证以下端点:

服务 端点
Prometheus http://192.168.42.10:9090
Alertmanager01 http://192.168.42.11:9093
Alertmanager02 http://192.168.42.12:9093
Alertmanager03 http://192.168.42.13:9093

你应该能够通过以下命令之一访问所需的实例:

实例 命令
Prometheus vagrant ssh prometheus
Alertmanager01 vagrant ssh alertmanager01
Alertmanager02 vagrant ssh alertmanager02
Alertmanager03 vagrant ssh alertmanager03

清理

测试完成后,只需确保你位于 ./chapter11/ 目录下,并执行以下命令:

vagrant destroy -f

不用太担心——如果需要,你可以轻松地再次启动环境。

Alertmanager 基础知识

我们在第九章中讲解了 Prometheus 中告警规则的工作原理,定义告警和记录规则,但仅仅这些规则本身并不十分有用。如我们之前提到的,Prometheus 通过 Webhook 风格的 HTTP 接口将通知处理和路由委托给外部系统。这就是 Alertmanager 的作用所在。

Alertmanager 负责接收由 Prometheus 告警规则生成的告警,并将它们转换为通知。后者可以采取任何形式,例如电子邮件、聊天消息、页面通知,甚至是 Webhook,这些 Webhook 会触发自定义操作,如将告警记录到数据存储中或创建/更新工单。Alertmanager 还是官方栈中唯一一个将其状态分发到多个实例的组件,这样它就能跟踪哪些告警已发送,哪些被静默。

通知管道

以下图表受到 Alertmanager 架构图的启发,提供了一个概述,展示了告警经过的各个步骤,直到它成功地作为通知发送出去:

图 11.2:通知管道概览

前面的图表包含了很多内容,因此我们将在接下来的几节中逐一讲解这些步骤。了解告警管道的工作原理将帮助你理解各种配置选项,如何排查丢失的告警,并全面利用 Alertmanager 提供的所有功能。

将告警组派发到通知管道

每当一个告警规则被触发时,Prometheus 会以 JSON 负载的形式将告警发送到 Alertmanager API,并且它会在该规则的每个评估间隔或每分钟(通过 --rules.alert.resend-delay 标志可配置)内继续发送更新,以较长者为准。当告警被 Alertmanager 接收时,它们会经过调度步骤,在此步骤中,告警会根据一个或多个告警标签进行分组,例如 alertname。我们将在本章后面的Alertmanager 配置部分进一步讨论。这样做可以将告警分类,从而减少多个告警合并为一个通知发送的次数,这些合并后的告警会触发通知管道:

图 11.3:Alertmanager 界面通过 alertname 分组告警

当运行多个具有相同配置的 Prometheus 实例时(追求高可用性/冗余性时的常见设置),相同条件的警报规则不一定会在完全相同的时间触发。Alertmanager 通过具有可配置时间间隔来解决这种情况。它会等待其他任何操作,以便将相似的警报分组在一起,从而避免为单一类型的问题发送多个通知。

这种分组是在用户指定的所有标准并行进行的。然后,每个组将触发通知管道,接下来我们将详细介绍它。

抑制

一个很好的例子可以帮助我们理解什么是警报抑制,就像想象一个服务器机架,如果顶部交换机失效会发生什么。在这种情况下,该机架中的所有服务器和服务都将开始触发警报,因为它们突然无法访问。为了避免这个问题,我们可以使用顶部交换机的警报,如果触发了,将阻止该机架中所有其他警报的通知发送出去。这有助于操作员更专注于真正的问题,避免洪水般的无法采取行动的警报。

简言之,抑制允许您映射警报之间的依赖关系,因此阻止依赖警报的通知继续在通知管道中传递。这是在 Alertmanager 配置文件中设置的,这意味着如果更改,则需要重新加载服务。

如果在抑制阶段未匹配警报,则会进入静音步骤。

静音

在监控/警报系统中,静音是一个常见概念;它可以在有时间限制的情况下避免警报通知的发送。通常用于在维护窗口期间禁用通知,或在事故期间临时抑制较低重要性的警报。Alertmanager 通过利用警报通常具有一个或多个不同标签的事实来加强这一概念:来自原始警报规则表达式、警报名称、警报的标签字段、alert_relabel_configs,以及 Prometheus external_labels。这意味着可以使用这些标签中的任何一个(或它们的组合),通过直接匹配或正则表达式匹配来临时禁用通知:

图 11.4:创建匹配 alertname=NodeExporterDown 的静音

在使用正则表达式匹配时要小心,因为可能会意外地静音超出预期的内容。Alertmanager Web UI 可以帮助预防此类问题,因为在创建新的静音时,它会显示将被抑制的触发警报的预览。

静默是在运行时定义的。可以通过 Alertmanager Web 界面、amtool(稍后将介绍的 Alertmanager 命令行界面)或直接通过 API 设置静默。可以在警报触发时设置静默,比如在事故处理中,或者提前设置以避免计划中的维护干扰值班人员。它不应成为一个永久的解决方案,仅是一个临时措施;因此,创建静默时需要设置到期日期,并且 Web UI 仅识别最长为“天”的持续时间。

由于静默步骤位于抑制之后,如果您对触发抑制规则的警报进行静默,它将继续抑制其他警报。

如果警报未与任何静默匹配,它将进入通知管道的下一步骤,即路由阶段。

路由

当警报批次到达此阶段时,Alertmanager 需要决定将其发送到哪里。由于最常见的使用场景是不同的人对不同的警报感兴趣、对不同严重性的警报使用不同的通知方式,或者这两者的组合,因此此步骤通过路由树实现了这一点。它由多个路由(如果有的话)组成,每个路由指定了一个或多个标签的匹配条件和接收器,以及一个根节点,根节点定义了一个“捕获所有”接收器,以防没有子路由与传递的警报组匹配。子路由可以有自己的路由,从而形成多层树结构。匹配按声明路由的顺序进行,当一个路由匹配时,会进入已定义的子路由,而最深的匹配将决定使用哪个接收器。我们在实际应用中使用 Alertmanager 配置时,这一点会更加清晰。

接收器和通知器的工作方式类似于通讯录联系人。接收器是命名的联系人,可以有一个或多个通知器,通知器类似于联系信息。Alertmanager 支持许多不同的通知器,通常属于以下类别之一:电子邮件、聊天(Slack、微信、HipChat)和页面(PagerDuty、Opsgenie、VictorOps、Pushover)。此外,它还支持 Webhook 通知器,这是一个通用的集成点,可用于支持所有未内置于 Alertmanager 的其他通知系统。

在此路由阶段将警报批次与接收器连接后,Alertmanager 会为该接收器中指定的每个通知器运行通知任务。这个任务负责去重、发送和重试通知。对于去重,它首先检查通知日志(稍后将在本章中讨论),确保该通知尚未发送;如果已经存在,则不执行任何操作。接下来,它会尝试发送通知,如果成功,通知将被记录在通知日志中。如果通知发送失败(例如 API 错误、连接超时等),该任务会再次尝试。

现在我们已经了解了通知管道的基本知识,接下来我们来看看当有多个 Alertmanager 实例时,会发生什么情况,以及如何在它们之间共享警报状态。

Alertmanager 集群

通知管道的概述没有涉及 Alertmanager 的高可用性组件。高可用性是通过依赖 gossip(基于 HashiCorp 的 memberlist,github.com/hashicorp/memberlist)来实现的,而不是使用基于共识的协议;这意味着选择集群中实例的奇数数量没有实际意义。通过 gossip,集群在所有 Alertmanager 实例之间共享通知日志nflog),从而让每个实例都能了解到集群在通知方面的整体状态。如果发生网络分区,通知将会从每个分区的一方发送,因为从逻辑上讲,接收更多通知总比完全无法通知要好。

如我们所知,抑制是在配置文件级别设置的,因此它应该在所有 Alertmanager 实例中保持一致。然而,静默也需要在集群中进行 gossip,因为它们是在单个 Alertmanager 实例的运行时设置的。这是验证集群是否按预期工作的一个好方法——确认配置的静默是否在所有实例中都能显示。

Alertmanager /#/status 页面显示了 gossip 集群的状态,以及已知的对等节点。你可以在我们的测试环境中查看这个端点,例如打开 http://192.168.42.11:9093/#/status

图 11.5:Alertmanager 集群状态

在 Alertmanager 中,集群工作方式如下:每个 Prometheus 实例将告警发送到它们所知道的所有 Alertmanager 实例。这些实例假设它们都在同一个 HA 集群中,会自己排序,并且排在第一位的实例将处理告警通知。该实例通过传播协议分发通知日志,通知日志会列出已成功发送的通知。其余的 Alertmanager 实例将根据它们在排序中的位置,增加等待通知日志更新的延迟。通知日志中的告警不会被这些实例重新发送——如果通知日志没有说明某个通知在传播延迟结束时已被处理,那么第二个 Alertmanager 实例将处理它,以此类推:

图 11.6:Alertmanager 集群概览

Prometheus 实例直接与所有 Alertmanager 实例通信,因为集群成员将负责彼此之间的去重。这意味着不应在 Prometheus 和 Alertmanager 之间放置负载均衡器。

Alertmanager 集群假设每个实例都使用相同的配置文件运行。然而,如果没有做到这一点,只会影响其去重通知的能力。

Alertmanager 配置

在第九章,定义告警和记录规则中,我们讨论了 Prometheus 如何生成并推送告警。在已经明确区分告警和通知之后,现在是时候使用 Alertmanager 来处理 Prometheus 发送的告警并将其转化为通知了。

接下来,我们将介绍在 Prometheus 中所需的配置,以及在 Alertmanager 中可用的配置选项,以便我们能够从监控栈中发送通知。

Prometheus 配置

在 Prometheus 中需要做几个配置,以便我们能够开始使用 Alertmanager。首先需要配置外部标签,这些标签会在与外部系统(包括但不限于 Alertmanager)通信时,添加到时间序列数据中(如果数据中没有这些标签)。这些标签用于唯一标识指标的来源,比如 regiondatacenterenvironment 等。一般来说,如果你有将相同的标签名/值添加到每个抓取和记录规则中的冲动,那么该标签更适合做为外部标签,因为它不会在本地的 Prometheus 实例中引入新的维度,但在更高级的系统(例如联合或长期指标存储)中可能会非常有用,正如我们将在接下来的章节中看到的那样。正如我们将在以下示例中看到的,外部标签是在 Prometheus 主配置文件的顶级 global 键内进行配置的。

第二步是配置 Prometheus,使其能够将警报发送到 Alertmanager。正如我们之前在Alertmanager 集群一节中讨论的那样,Prometheus 实例是必需的,以便你可以了解并将警报分别发送到所有 Alertmanager 集群成员。这个配置在 Prometheus 配置文件中,位于一个名为 alerting 的顶级部分中。这个配置的示例可以在我们的测试环境中找到,具体如下:

vagrant@prometheus:~$ cat /etc/prometheus/prometheus.yml
global:
 external_labels:
 dc: dc1
alerting:
 alertmanagers:
 - static_configs:
 - targets:
 - alertmanager01:9093
 - alertmanager02:9093
 - alertmanager03:9093
...

alerting 部分,我们还可以使用 alert_relabel_configs,它的配置语法与 relabel_configsmetric_relabel_configs 相同,正如在第五章《运行 Prometheus 服务器》中所解释的那样,但在这种情况下,它仅适用于传出的警报。使用重标记在这里是有用的,可以防止某些警报完全到达 Alertmanager,修改或丢弃标签以便于分组,甚至可以添加某些警报特有的标签,这些标签在 external_labels 中可能没有意义。由于 alert_relabel_configs 在我们发送警报之前运行,因此外部标签会出现在这些警报中,并因此可以进行操作。以下是一个示例,防止具有名为 environment 且匹配值为 development 的标签的警报被推送到 Alertmanager:

  alert_relabel_configs:
  - source_labels: [environment]
    regex: development
    action: drop

虽然前面的示例说明了如何丢弃警报,但不应将其作为永久解决方案,因为更好的解决方案可能是根本不创建这些警报。

接下来,我们将介绍 Alertmanager 配置文件及其主要部分,并指出一些有用的信息,帮助你入门。

配置文件概述

Alertmanager 通过一个单一的配置文件进行配置,并且可以像 Prometheus 一样在运行时重新加载,而无需重启:可以通过向进程发送 SIGHUP 或发送 HTTP POST 请求到 /-/reload 端点来实现。与 Prometheus 一样,格式错误的配置将不会被应用——系统会记录错误信息,并且在其 /metrics 端点中找到的 alertmanager_config_last_reload_successful 指标将被设置为 0

配置文件分为五个顶级部分:globalrouteinhibit_rulesreceiverstemplates。在接下来的章节中,我们将逐一探讨每个部分。

global

全局部分收集了在文件的其他部分有效的所有配置选项,并作为这些选项的默认设置。由于这些参数可以在其他部分被覆盖,使用它们是保持配置文件尽可能简洁、避免重复的好方法。在这一部分的所有可用参数中,大多数与凭证和令牌作为通知器相关。这里有一个值得注意的参数,叫做resolve_timeout。当 Prometheus 触发警报规则时,它将在每个评估间隔发送产生的警报,并更新 JSON 负载中的EndTime字段。当这些警报被解决时,Alertmanager 会通过更新EndTime来通知这些解决。如果由于某些原因,警报停止定期更新(例如,发送该警报的 Prometheus 实例崩溃并且仍在恢复过程中),Alertmanager 将使用最后接收到的EndTime来解决该警报。resolve_timeout配置用于解决由非 Prometheus 系统创建的警报,这些系统不使用EndTime。需要明确的是,这是一个你不应该修改的设置,因为它与 Prometheus-Alertmanager 警报协议相关;在此解释只是为了完整性。

作为示例,我们测试环境中 Alertmanager 配置的全局部分如下所示:

global:
  smtp_smarthost: 'mail.example.com:25'
  smtp_from: 'example@example.com'
...

这个示例配置为每个使用电子邮件(SMTP)通知器的接收器设置默认的电子邮件smarthostfrom地址。

路由

这无疑是 Alertmanager 最重要的配置部分。在这一部分,我们将定义如何根据标签(group_by)对警报进行分组,等待多长时间才能发送更多通知(group_interval),以及如何重复它们(repeat_interval),但最重要的是,为每个警报批次触发哪些接收器(receiver)。由于每个路由都可以有自己的子路由,这就形成了一棵路由树。顶级路由不能有任何匹配规则,因为它像一个兜底规则,处理所有不匹配任何子路由的警报。除了continue,路由上的每个设置都会以级联方式传递给其子路由。尽管默认行为是在找到最具体的匹配项时停止搜索接收器,但可以将continue设置为true,使匹配过程继续,从而允许触发多个接收器。

你可以在我们的测试环境中找到以下示例路由配置:

route:
  receiver: operations
  group_by: ['alertname', 'job']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

  routes:
  - match_re:
      job: (checkoutService|paymentService)
    receiver: yellow-squad-email
    routes:
    - match:
        severity: pager
      receiver: yellow-squad-pager
...

上述示例中的主路由执行以下操作:

  • 定义operations接收器为默认路由,当没有其他子路由匹配时使用

  • 根据alertnamejob对传入的警报进行分组

  • 在发送第一次通知之前,等待 30 秒钟以便更多警报到达,从而减少相同问题的通知次数

  • 在将新警报添加到批次后,等待五分钟再发送额外的通知

  • 每四小时重新发送一次包含当前触发告警的告警批次的通知

此外,还为 job 标签匹配 checkoutServicepaymentService 的告警设置了一个子路由,指定其接收者为 yellow-squad-email。该子路由反过来定义了一个自己的子路由,如果严重性标签匹配 pager,则应使用 yellow-squad-pager receiver

官方 Prometheus 网站提供了一个路由树编辑器和可视化工具,链接:prometheus.io/webtools/alerting/routing-tree-editor/

group_by 子句也可以采用唯一值 ...,这将指示 Alertmanager 不对传入的告警进行任何分组。这种情况很少使用,因为分组的目的是减少通知的数量,从而提高信噪比。此功能的一个可能用途是将每个告警原样发送到另一个系统,供该系统处理。

inhibit_rules

在本节中,我们将添加规则来抑制告警。其工作原理是通过在源和目标告警上使用匹配器来匹配源告警并静音目标告警。唯一的要求是目标和源的标签在标签名称和值上都要匹配,例如:

inhibit_rules:
  - source_match:
      job: 'icmp'
    target_match_re:
      alertname: (AlertmanagerDown|NodeExporterDown)
    equal: ['base_instance']

在这个示例中,我们可以读取到以下内容:如果有一个 job 标签设置为 icmp 的告警,且在所有匹配的告警中,base_instance 相同,则会静音所有 alertname 匹配 AlertmanagerDownNodeExporterDown 的告警。换句话说,如果运行 Alertmanager 和 Node Exporter 的实例宕机,则跳过关于这些服务的告警,只发送关于实例本身的告警,让运维人员专注于真正的问题。

如果等式子句中的任何标签在源和目标告警中都不存在,则视为匹配,从而启用抑制。

如果一个告警同时匹配抑制规则定义中的源和目标,该告警将不会被抑制——这是为了防止告警抑制自身。

我们可以在以下截图中看到,当发生此情况时,Alertmanager 的界面:

图 11.7:Alertmanager 界面,仅显示触发的通知

在下面的截图中,我们可以通过选择右上角的抑制选项,看到所有未出现在前一张截图中的被抑制告警:

图 11.8:Alertmanager 界面,显示包括被抑制告警在内的通知

Alertmanager 界面让你可以俯瞰所有警报,不仅是那些处于激活状态的警报,还有被抑制的警报。默认情况下不会显示被抑制的警报,以减少视觉杂乱;但是,正如我们在前面的截图中所看到的,你可以通过选择抑制复选框来轻松启用显示这些警报,位于筛选器/分组框的右上角。

receiver

当路由被匹配时,它将调用一个接收器。接收器包含通知器,我们将在后续章节中更深入地探讨这一点。基本上,接收器是可用集成的命名配置。

在我们的测试环境中,我们可以找到一个使用 Webhook 通知器的接收器示例,具体如下:

route:
  receiver: operations
...
receivers:
- name: 'operations'
  webhook_configs:
  - url: 'http://127.0.0.1:5001'
...

顶级路由,也称为捕获所有或回退路由,将在其他子路由没有匹配到来警报时触发名为operations的接收器。operations接收器是使用单一通知器配置的,该通知器是 Webhook 通知器。这意味着发送到此接收器的警报会被发送到url配置键中指定的 URL。Webhook 通知器将在本章稍后进一步分析。

templates

本节是定义指向可用通知器的多个自定义通知模板的路径列表的地方。与 Prometheus 中的其他文件路径配置类似,每个路径定义允许在最后一个组件上使用 glob 模式,并可以如下定义:

templates:
  - /etc/alertmanager/templates/*.tmpl

我们将在自定义警报通知部分使用这一部分来定义我们的自定义模板。

amtool 命令行工具

类似于promtoolamtool是一个易于使用的命令行工具,由 Alertmanager HTTP API 提供支持。除了用于验证 Alertmanager 配置文件的正确性外,它还允许你查询服务器中当前触发的警报,并执行诸如静默警报或创建新警报等操作。amtool的子命令分为四组——alertsilencecheck-configconfig——我们将使用测试环境对每一组进行概述。

为了跟随本节中的示例,确保你连接到其中一个 Alertmanager 实例。由于它们是集群化的,任何一个实例都可以,举例如下:

vagrant ssh alertmanager02

登录后,你可以作为默认用户运行amtool,因为与 Alertmanager API 交互不需要管理员权限。此外,你甚至可以使用amtool连接到任何 Alertmanager 实例,而不仅仅是本地实例,因为与 HTTP API 交互的大多数命令要求你指定实例的 URL。

alert

这个子命令允许你查询 Alertmanager 集群中当前触发的警报,可以通过如下方式实现:

vagrant@alertmanager02:~$ amtool alert --alertmanager.url http://alertmanager02:9093
Alertname Starts At Summary 
deadmanswitch 2019-03-31 14:49:45 UTC 
InstanceDown 2019-03-31 15:48:30 UTC

alert子命令的默认操作是query。与之前的示例等效的命令是amtool alert query --alertmanager.url http://alertmanager02:9093

此子命令的另一个功能是按需创建警报。这对于测试目的非常有用。例如,让我们创建一个名为 ExampleAlert 的新警报,标签为 example="amtool"

vagrant@alertmanager02:~$ amtool alert add alertname="ExampleAlert" example="amtool" --alertmanager.url http://alertmanager02:9093

add 操作期望每个命令参数都有一个标签名称/值对,正如我们在前面的代码中所看到的那样。此操作还会考虑第一个参数,因为警报名称没有标签名称。如果我们省略 alertname 标签,警报也可以创建,但这可能会在 amtool 和 Alertmanager Web UI 中引发一些奇怪的行为,因此对此需要保持谨慎。

我们可以稍等一会儿(在测试环境中,group_wait 定义为 30 秒),然后重新查询当前的警报,来检查它是否已正确添加:

vagrant@alertmanager02:~$ amtool alert --alertmanager.url http://alertmanager02:9093
Alertname Starts At Summary 
deadmanswitch 2019-03-31 14:49:45 UTC 
InstanceDown 2019-03-31 15:48:30 UTC 
ExampleAlert 2019-03-31 15:55:00 UTC 

此操作还允许你指定其他警报字段,例如结束时间、生成器 URL 和注解。你可以通过使用 help 标志来查看 add 操作的命令行界面(参数和选项):

vagrant@alertmanager01:~$ amtool alert add --help

请记住,这个新创建的警报将在五分钟的非活动后被认为已解决(resolve_timeout 的默认值),因此,如果你需要更多时间进行测试,请确保通过运行 add 操作添加此警报的新实例,以保持警报继续存在。

接下来,我们将使用这个新警报作为静默的目标。

silence

使用此子命令,我们可以管理静默。首先,我们可以尝试使用以下指令查询集群中可用的静默:

vagrant@alertmanager02:~$ amtool silence --alertmanager.url http://alertmanager02:9093
ID Matchers Ends At Created By Comment

silence 子命令的默认操作是 query。与前面示例等效的命令是 amtool silence query --alertmanager.url http://alertmanager02:9093

如我们所见,目前没有任何静默被执行。让我们通过匹配其标签 example="amtool" 来为之前生成的警报创建一个新的静默,并再次检查静默:

vagrant@alertmanager02:~$ amtool silence add 'example="amtool"' --comment "ups" --alertmanager.url http://alertmanager02:9093
1afa55af-306a-408e-b85c-95b1af0d7169

vagrant@alertmanager02:~$ amtool silence --alertmanager.url http://alertmanager02:9093
ID Matchers Ends At Created By Comment 
1afa55af-306a-408e-b85c-95b1af0d7169 example=amtool 2019-03-31 16:58:08 UTC vagrant ups

我们现在可以看到新静默已被添加。为了验证它是否已生效,我们可以使用 alert 子命令并检查 ExampleAlert 是否已从当前警报列表中消失:

vagrant@alertmanager02:~$ amtool alert --alertmanager.url http://alertmanager02:9093
Alertname Starts At Summary 
deadmanswitch 2019-03-31 14:49:45 UTC 
InstanceDown 2019-03-31 15:48:30 UTC

让我们通过使用 expire 操作来移除刚刚创建的静默。为此,我们需要静默标识符,可以在列出当前静默时,在 ID 列中看到它:

vagrant@alertmanager02:~$ amtool silence expire 1afa55af-306a-408e-b85c-95b1af0d7169 --alertmanager.url http://alertmanager02:9093

vagrant@alertmanager02:~$ amtool silence --alertmanager.url http://alertmanager02:9093
ID Matchers Ends At Created By Comment

如果我们再次查询当前的警报列表,我们将看到我们的 ExampleAlert 又出现了。

这些是静默功能最常见的使用场景。还有其他可用的操作,比如批量导入静默(在迁移到新集群时很有用)或在需要时更新现有的静默。像往常一样,--help 标志将为你提供如何使用这些操作的指导。

check-config

这可能是 amtool 最有用的功能:验证我们 Alertmanager 配置文件及引用的模板文件的语法和模式。你可以通过以下示例来测试 check-config 子命令:

vagrant@alertmanager02:~$ amtool check-config /etc/alertmanager/alertmanager.yml 
Checking '/etc/alertmanager/alertmanager.yml' SUCCESS
Found:
 - global config
 - route
 - 1 inhibit rules
 - 8 receivers
 - 1 templates
  SUCCESS

这种类型的验证非常容易自动化,且应该在任何配置更改后进行,但在重新加载 Alertmanager 实例之前进行,以防止大多数类型的配置问题。

config

使用config子命令,我们可以查询运行中的 Alertmanager 实例的内部配置,包括所有可配置字段,甚至是配置文件中未明确列出的字段。你可以通过执行以下命令来检查:

vagrant@alertmanager02:~$ amtool config --alertmanager.url http://alertmanager02:9093
global:
  resolve_timeout: 5m
  http_config: {}
  smtp_from: example@example.com
  smtp_hello: localhost
  smtp_smarthost: example.com:25
  smtp_require_tls: true
  slack_api_url: <secret>
...

配置文件中未指定的配置字段将显示其默认值,而涉及秘密信息的字段(如密码和令牌)将被自动屏蔽。

显示了config子命令的默认操作。与前一个示例等效的命令是amtool config show --alertmanager.url http://alertmanager02:9093

下一个子命令操作routes会生成配置的路由树的文本可视化。这个命令可以在运行中的 Alertmanager 实例或本地配置文件上执行。其语法和输出如下:

vagrant@alertmanager02:~$ amtool config routes --alertmanager.url http://alertmanager02:9093
Routing tree:
.
└── default-route receiver: operations
    ├── {job=~"^(?:^(?:(checkoutService|paymentService))$)$"} receiver: yellow-squad-email
    │ └── {severity="pager"} receiver: yellow-squad-pager
    ├── {job="firewall"} receiver: purple-squad-email
    │ ├── {severity="slack"} receiver: purple-squad-slack
    │ └── {severity="pager"} receiver: purple-squad-pager
    └── {alertname=~"^(?:^(?:(AlertmanagerDown|NodeExporterDown))$)$"} receiver: violet-squad-slack
        └── {severity="pager"} receiver: violet-squad-pager

你甚至可以通过为routes test操作提供标签来验证路由树,并检查哪条路由会被触发。在以下示例中,我们可以看到当警报带有job="checkoutService"标签时,触发的接收器是否确实是yellow-squad-email

vagrant@alertmanager02:~$ amtool config routes test 'job="checkoutService"' --config.file /etc/alertmanager/alertmanager.yml 
yellow-squad-email

拥有这个命令行工具可以帮助你简化复杂路由规则的开发,并且无需本地运行 Alertmanager 实例即可验证生成的配置。

Kubernetes Prometheus Operator 和 Alertmanager

在第五章,运行 Prometheus 服务器,我们有机会尝试了 Prometheus Operator。由于 Alertmanager 是 Prometheus 栈的一个核心组件,因此 Operator 也能够管理其实例。除了负责管理 Alertmanager 集群外,Operator 还负责管理记录和告警规则的配置。

为了提供一些如何使用 Operator 管理 Alertmanager 集群的见解,我们将提供一个完整的示例供你尝试。我们 Kubernetes 测试环境中使 Alertmanager 和 Prometheus 运行的 Kubernetes 清单可以在相对于仓库根路径的以下路径中找到:

cd ./chapter11/provision/kubernetes/

以下步骤将确保已经配置好一个新的 Kubernetes 环境,并安装所有必需的软件,以便我们接下来可以专注于 Alertmanager 组件:

  1. 验证没有其他 Kubernetes 环境在运行:
minikube status
minikube delete
  1. 启动一个空的 Kubernetes 环境:
minikube start \
  --cpus=2 \
  --memory=3072 \
  --kubernetes-version="v1.14.0" \
  --vm-driver=virtualbox
  1. 添加 Prometheus Operator 组件,并按照其部署步骤进行操作:
kubectl apply -f ./bootstrap/

kubectl rollout status deployment/prometheus-operator -n monitoring
  1. 添加新的 Prometheus 集群,确保其成功:
kubectl apply -f ./prometheus/

kubectl rollout status statefulset/prometheus-k8s -n monitoring
  1. 将所有目标添加到 Prometheus 并列出它们:
kubectl apply -f ./services/

kubectl get servicemonitors --all-namespaces

在 Kubernetes 测试环境启动后,我们可以继续进行 Alertmanager 特定的配置。与基于虚拟机的测试环境类似,我们不仅需要配置 Alertmanager 本身,还需要为 Prometheus 提供报警规则。

对于 Alertmanager 配置,由于我们可能需要添加一些敏感信息,比如电子邮件凭证或呼叫令牌,我们将使用 Kubernetes secret。这也意味着应该有一个 ServiceAccount 来访问该 secret。

我们可以通过应用以下清单来创建 ServiceAccount:

kubectl apply -f ./alertmanager/alertmanager-serviceaccount.yaml

由于我们使用了 secret,Alertmanager 配置需要编码为 base64。提供了一个最小配置,可以通过执行以下命令进行部署:

kubectl apply -f ./alertmanager/alertmanager-configuration.yaml

作为参考,编码在 secret 中的最小配置如下:

global:

route:
  receiver: "null"
  group_by:
    - job
  group_interval: 3m
  repeat_interval: 3h
  routes:
    - match:
        alertname: deadmanswitch
      receiver: "null"

receivers:
  - name: "null"

现在,我们可以继续部署,并让 Operator 为我们处理繁重的工作。它会抽象出 StatefulSet 的创建,并让集群正常运行。为此,我们需要应用以下清单:

kubectl apply -f ./alertmanager/alertmanager-deploy.yaml

上述清单中的重要部分可以在以下代码片段中看到:

...
kind: Alertmanager
...
spec:
  baseImage: quay.io/prometheus/alertmanager
...
  replicas: 3
...

我们可以通过执行以下指令来跟踪部署状态:

kubectl rollout status statefulset/alertmanager-k8s -n monitoring

为了确保 Prometheus 实例能够从新创建的 Alertmanager 收集指标,我们将添加一个新的 Service 和 ServiceMonitor。为此,我们需要应用以下清单:

kubectl apply -f ./alertmanager/alertmanager-service.yaml

kubectl apply -f ./alertmanager/alertmanager-servicemonitor.yaml

现在是时候添加报警规则了。为此,你只需要应用以下清单:

kubectl apply -f ./alertmanager/alerting-rules.yaml

如果你打开之前的清单,你将看到几个规则。以下代码片段展示了第一个规则:

...
kind: PrometheusRule
...
spec:
  groups:
  - name: exporter-down
    rules:
    - alert: AlertmanagerDown
      annotations:
        description: Alertmanager is not being scraped.
        troubleshooting: https://github.com/kubernetes-monitoring/kubernetes-mixin/blob/master/runbook.md
      expr: |
        absent(up{job="alertmanager-service",namespace="monitoring"} == 1)
      for: 5m
      labels:
        severity: page
...

这些规则将被添加到 Prometheus 实例中,Operator 会处理重新加载配置,而不会导致服务停机。

最后,你可以访问 Prometheus 和 Alertmanager 的 web 界面,并通过执行以下指令验证到目前为止所做的所有配置,这将打开几个浏览器标签:

minikube service alertmanager-service -n monitoring

minikube service prometheus-service -n monitoring

测试完成后,你可以通过执行以下命令删除这个基于 Kubernetes 的环境:

minikube delete

这个设置为你提供了一个快速概览,说明如何在 Kubernetes 上将 Alertmanager 集成到 Prometheus 中。再说一遍,Prometheus Operator 抽象了大部分复杂性,让你可以专注于最重要的部分。

常见的 Alertmanager 通知集成

用户和/或组织对通知方式有不同的需求;有些可能使用 HipChat 作为通讯工具,而其他则依赖于电子邮件,值班通常需要使用类似 PagerDuty 或 VictorOps 的寻呼系统,等等。幸运的是,Alertmanager 提供了多种开箱即用的集成选项,能够满足大部分的通知需求。如果没有,始终可以使用 Webhook 通知器,它支持与自定义通知方法的集成。接下来,我们将探索最常见的集成方式以及如何配置它们,并提供基本的示例帮助你入门。

在考虑与聊天系统集成时,需要记住的是,这些系统是为人类设计的,对于低优先级警报,建议使用工单系统。当创建警报的过程变得简单且自助时,管理警报可能很快失控。工单确保责任归属:与在聊天频道中发送警报相比,使用工单的主要优势是,它们可以跟踪、优先排序,并进行适当的跟进,以确保警报问题不再发生。这种方法还隐式确保了通知的归属,并避免了常见的 谁是这个警报的负责人? 问题。归属感使服务维护人员能够筛选他们收到的警报,并且作为副作用,也有助于减少警报疲劳。

如果你正在使用 JIRA 进行任务跟踪,可以通过 Webhook 通知器实现一个名为 JIRAlert 的自定义集成,具体内容请参考 github.com/free/jiralert

所有通知器都有一个共同的配置项,叫做 send_resolved。它接受一个布尔值(true 或 false),用于声明是否在警报被解决时发送通知。默认情况下,PagerDuty、Opsgenie、VictorOps、Pushover 和 Webhook 集成启用了此选项,但其他通知器则禁用了它,这是你应该防止不必要垃圾邮件的主要原因。

电子邮件

电子邮件是大多数组织的标准通讯方式,因此 Alertmanager 支持电子邮件也就不足为奇了。在配置方面,设置相当简单;然而,由于 Alertmanager 本身并不直接发送电子邮件,它需要使用一个实际的电子邮件中继。我们以一个现实世界的例子为例,适用于快速测试和低预算设置,这就是使用一个电子邮件提供商(在此例中是 Google 的 Gmail)提供的 SMTP 服务:

global:
  smtp_smarthost: 'smtp.gmail.com:587'
  smtp_from: 'alertmanager@example.com'
  smtp_auth_username: 'alertmanager@example.com'
  smtp_auth_identity: 'alertmanager@example.com'
  smtp_auth_password: '<generated_token>'

route: 
  receiver: 'default'

receivers:
- name: 'default'
  email_configs:
  - to: 'squad@example.com'

在这个特定的示例中,由于直接使用你的主 Gmail 密码在网络安全方面不太安全,你需要一个启用了双因素认证的账户,并生成一个应用密码来使用 smtp_auth_password 字段。

你可以在 使用应用密码登录 支持页面找到如何为 Gmail 帐号生成应用密码,页面地址是 support.google.com/accounts/answer/185833

聊天

在撰写时,有三种聊天服务的集成:Slack、WeChat 和 HipChat。以下示例表示 Slack 集成的配置;本章稍后我们将提供更多关于这种集成的定制化概述:

global:
  slack_api_url: 'https://hooks.slack.com/services/TOKEN'

route:
  receiver: 'default'

receivers:
- name: 'default'
  slack_configs:
  - channel: '#alerting'

slack_api_url 应指向 Slack Incoming Webhooks 的 URL。你可以通过访问他们的文档了解更多内容,网址是 api.slack.com/incoming-webhooks。由于 slack_configs 是一个列表,你可以在单个接收器中指定多个频道。

呼叫器

值班通常意味着携带一个呼叫器,无论是物理的还是虚拟的。在撰写时,Alertmanager 支持四种呼叫器样式的服务集成:PagerDuty、Opsgenie、VictorOps 和 Pushover。这些服务的配置相对简单,主要涉及 API URL 和身份验证令牌。然而,它们也支持更深层次的定制,比如添加图片、链接以及配置服务特定的字段,如严重性。这些高级配置选项已在 Alertmanager 的官方文档中描述,因此这里不再赘述。以下示例展示了 PagerDuty 的基本配置:

global:
  pagerduty_url: 'https://events.pagerduty.com/v2/enqueue'

route:
  receiver: 'default'

receivers:
- name: 'default'
  pagerduty_configs:
  - service_key: 'PAGERDUTYSQUADTOKENEXAMPLE'

与之前的通知器类似,由于 pagerduty_configs 配置是一个列表,你可以在单个接收器中触发多个服务路由。你可以在这里了解更多有关 PagerDuty 与 Alertmanager 集成的内容:www.pagerduty.com/docs/guides/prometheus-integration-guide/

Webhook

Webhook 集成为自定义集成提供了广阔的可能性。此功能允许 Alertmanager 发出带有通知 JSON 负载的 HTTP POST 请求到你选择的端点。请记住,URL 不能被模板化,且目标端点必须设计为处理 JSON 负载。例如,它可以用于将所有通知推送到日志系统(如 Elasticsearch),以便你进行报告和统计分析。如果你的团队使用 IRC,这也可以是与其集成的解决方案。另一个示例是我们为本书创建的 alertdump 工具。它以前在 第九章中,定义警报和记录规则,用于展示 Prometheus 在触发警报规则时发送的内容,但它也可以用来展示 Alertmanager 发送的通知负载。

可以在以下代码中看到一个简单的配置:

global:

route:
  receiver: 'default'

receivers:
- name: 'default'
  webhook_configs:
  - url: 'http://127.0.0.1:5001'

该配置会将 Alertmanager 接收到的每个警报原封不动地发送到 alertdump,后者会将有效负载追加到以 alertdump 运行所在主机命名的日志文件中。该日志文件位于一个路径中,该路径在我们测试环境中的每个虚拟机内均可访问(/vagrant/cache/alertmanager*.log),同时在外部(相对于仓库根目录的 ./cache/alertmanager*.log)也可访问。

null

这本身不是一个通知器,而是一个常用的模式,用于丢弃通知。其配置方式是指定一个接收者而不指定通知器,这样就会导致通知被丢弃。以下示例确保永远不会发送任何通知:

global:

route:
  receiver: 'null'

receivers:
- name: 'null'

这有时对于演示目的很有用,但除此之外并无太多作用;不应触发通知的警报应该在源头被丢弃,而不是在 Alertmanager 中丢弃,唯一的例外是用作禁止的源头的警报。

始终需要关注的是 alertmanager_notifications_failed_total 这个 Alertmanager 指标,因为它跟踪每个集成的通知发送失败次数。

现在我们了解了 Alertmanager 通知器的基本知识,接下来可以学习如何自定义告警通知,以便将最重要的信息适当呈现。

自定义你的警报通知

对于每个可用的集成,Alertmanager 已经包含了内置的通知模板。然而,这些模板可以根据用户和/或组织的具体需求进行定制。类似于我们在第九章《定义告警和记录规则》中探讨的告警规则注解,告警通知使用 Go 模板语言进行模板化。我们以 Slack 集成为例,了解消息是如何构建的,以便根据你的需求进行定制。

默认消息格式

为了了解没有任何自定义的通知是什么样子,我们将使用一个非常简单的示例。以我们在 Prometheus 实例中定义的以下告警规则为例:

  - alert: deadmanswitch
    expr: vector(42)

一旦此警报开始触发,警报有效负载将被发送到 Alertmanager。以下代码片段演示了有效负载的发送。注意其中的标签,包括来自 Prometheus 实例的 alertnameexternal_labels

    {
        "labels": {
            "alertname": "deadmanswitch",
            "dc": "dc1"
        },
        "annotations": {},
        "startsAt": "2019-04-02T19:11:30.04754979Z",
        "endsAt": "2019-04-02T19:14:30.04754979Z",
        "generatorURL": "http://prometheus:9090/graph?g0.expr=vector%2842%29&g0.tab=1"
    }

在 Alertmanager 端,我们将有这个最小化配置,确保能够发送 Slack 通知(将 TOKEN 替换为实际的 Slack token):

global:
  slack_api_url: 'https://hooks.slack.com/services/TOKEN'

route:
  receiver: 'default'

receivers:
- name: 'default'
  slack_configs:
  - channel: '#alerting'

最终结果会是一个类似于以下的 Slack 消息:

图 11.9:Slack 默认通知格式

如我们所见,默认的通知格式包含了大量信息。但问题仍然存在,这到底是如何生成的?为了回答这个问题,我们可以查看由我们基础的 Alertmanager 配置生成的 default 接收器的运行时配置,以下片段展示了这一点:

...
receivers:
- name: default
  slack_configs:
  - send_resolved: false
    http_config: {}
    api_url: <secret>
    channel: '#alerting'
    username: '{{ template "slack.default.username" . }}'
    color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'
    title: '{{ template "slack.default.title" . }}'
    title_link: '{{ template "slack.default.titlelink" . }}'
    pretext: '{{ template "slack.default.pretext" . }}'
    text: '{{ template "slack.default.text" . }}'
...

可以通过使用amtool config或访问 Alertmanager Web 界面的/#/status端点来检查 Alertmanager 的运行时配置。

如我们所见,每个可自定义的字段都是使用 Go 模板配置的。我们将以username字段为例,说明 Slack 消息中的用户名是如何生成的,因为这是一个相当简单的模板。所有其他使用的模板都遵循相同的逻辑,复杂度有所不同。

Alertmanager 使用的默认模板无法在测试环境实例中本地查看,因为它们已经被编译并包含在 Alertmanager 的二进制文件中。不过,我们可以通过查看 Alertmanager 代码库中的 templates/default.tmpl 文件来查阅所有默认的通知集成模板。本文撰写时,当前版本是 0.16.2,因此,为了方便起见,我们在此提供了链接:github.com/prometheus/alertmanager/blob/v0.16.2/template/default.tmpl

如果我们查看 default.tmpl 文件,我们将找到 slack.default.username 模板的定义:

{{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }}

如我们所见,该模板使用了另一个模板作为其定义。因此,如果我们查找 __alertmanager 模板的定义,我们会发现以下内容:

{{ define "__alertmanager" }}AlertManager{{ end }}

现在,你明白了 AlertManager 是如何出现在 Slack 通知中的。其余各个模板的追踪任务留给你作为练习。在下一部分中,我们将学习如何创建自己的模板,并将它们用于自定义我们的警报通知。

创建新模板

在深入创建模板之前,我们首先需要了解发送到通知模板的数据结构。下表显示了可以使用的变量:

变量 描述
Alerts 一个警报结构列表,每个警报都有其自己的状态、标签、注解、StartsAt、EndsAt 和 GeneratorURL
CommonAnnotations 所有警报共有的注解
CommonLabels 所有警报共有的标签
ExternalURL 发送警报的 Alertmanager 的 URL
GroupLabels 用于警报分组的标签
receiver 将处理通知的接收器
status 只要警报处于该状态,就会触发该变量,或者当警报解决时,它会变为已解决

所有可用的数据结构和函数的全面参考可以在prometheus.io/docs/alerting/notifications/找到。

演示如何构建模板的最佳方法是提供一个示例。我们构建了以下模板,它已经在测试环境中准备好,目的正是如此。我们将在详细讲解每一部分的同时,将 Alertmanager 配置精简为只包含重要的部分。

我们希望创建的最终结果可以在以下截图中看到:

图 11.10:示例 Slack 通知模板

现在,我们将解析示例配置,该配置将生成类似于前面截图所示的通知:

route:
  group_by: [alertname, job]
  receiver: null
  routes:
  - match:
      severity: slack
    receiver: purple-squad-slack

出于这个示例的考虑,我们将警报按alertnamejob进行分组。这一点很重要,因为它将影响CommonAnnotationsCommonLabels,正如我们稍后会看到的那样:

receivers:
- name: null
- name: purple-squad-slack
  slack_configs:
  - api_url: 'https://hooks.slack.com/TOKEN'
    channel: '#alerting'
    title: >
      [{{ .Alerts | len }}x ALERT{{ if gt (len .Alerts.Firing) 1 }}S{{end}}] {{ .CommonLabels.alertname }}

正如我们在前面的表格中看到的,.Alerts 是所有警报的列表,因此我们希望获取该列表的长度(len)来为消息创建一个标题,从触发的警报数量开始。注意if语句,它确保在有多个警报时使用复数形式。最后,由于我们按alertname分组警报,我们会在方括号后面打印alertname

    text: >
      :link: <{{ .CommonAnnotations.troubleshooting }}/{{ .CommonLabels.alertname }}|Troubleshooting Guide>

对于消息正文,我们希望生成一个指向此类警报故障排除指南的链接。我们的警报发送了一个名为troubleshooting的注解,包含一个基本 URL。如果我们遵循惯例,使得指南名称与警报名称匹配,我们就可以使用这两个字段轻松生成链接。

为了提供更多关于触发警报的上下文,我们将把所有可用的警报标签添加到消息中。为了实现这个目标,我们必须遍历列表中的每个警报:

      {{ range .Alerts }}

对于每个警报,我们将打印该警报可用的描述,它作为该警报的注解存在:

       *Description:* {{ .Annotations.description }}
      *Details:*

我们还将打印每个警报的标签/值对。为此,我们将遍历SortedPairs的结果,它返回一个排序后的标签/值对列表:

        {{- range .Labels.SortedPairs }}

{{- 代码会修剪掉前面的文本中的所有尾随空格。更多信息可以参见tip.golang.org/pkg/text/template/#hdr-Text_and_spaces

我们将严重性标签用作路由键,以选择通知器(呼叫器、电子邮件或 Slack),因此我们不希望在警报消息中暴露它。我们可以通过添加if语句来实现这一点,从而避免打印该特定标签/值:

          {{- if ne .Name "severity"}}
        • *{{ .Name }}:* `{{ .Value }}`
          {{- end}}
      {{- end }}
      {{ end }}

就是这样。你甚至可以通过将此模板从 Alertmanager 配置中提取出来并放入自己的模板文件中,使其更加易于管理。我们在测试环境中已经这么做了,其中接收器配置仅如下所示:

- name: violet-squad-slack
  slack_configs:
  - channel: '#violet-squad-alerts'
    title: '{{ template "slack.example.title" . }}'
    text: '{{ template "slack.example.text" . }}'

所有模板定义都可以在 Alertmanager 实例中的以下路径找到:

/etc/alertmanager/templates/example.tmpl

通知模板化一开始是相当难以理解和构建的。幸运的是,Prometheus 的联合创始人之一和核心维护者 Julius Volz 创建了一个工具,可以帮助你快速迭代 Slack 通知模板。这是理解它们如何工作及如何生成它们的最佳方式。你可以在juliusv.com/promslack/找到它。

谁来监视监视者?

监控系统是任何基础设施的关键组件。我们依赖它来监视一切——从服务器和网络设备到服务和应用程序——并期待在出现问题时收到通知。然而,当问题出现在监控堆栈本身,或者甚至是通知提供者上,导致警报被生成但未能到达我们时,作为运维人员,我们该如何得知?

保证监控堆栈正常运行,并确保通知能够到达接收者,是一个常常被忽视的任务。在本节中,我们将深入探讨如何采取措施来减轻风险因素,并提高对监控系统的整体信任度。

元监控与交叉监控

从广义上讲,你不能让监控系统监控自己;如果系统遭遇严重故障,它将无法发出相关通知。虽然让 Prometheus 自己抓取数据是常见做法(你可能会在大多数教程中看到),但显然不能依赖它来发出关于自己的警报。这时,元监控就派上用场了:它是指监控系统被监控的过程。

你应该考虑的第一个选项来缓解这个问题,是拥有一组 Prometheus 实例,监控它们数据中心/区域中的所有其他 Prometheus 实例。由于 Prometheus 自身生成的指标相对较少,这将转化为一个相当轻量的抓取任务,进行元监控的实例甚至不需要专门致力于此:

图 11.11:元监控——Prometheus 组互相监控

然而,你可能会想知道,这些实例该如何被监控。我们可以不断添加更高层次的实例,以分层方式进行元监控——先在数据中心级别,再在区域级别,最后在全球级别——但我们仍然会面临一组没有被监控的服务器。

一种补充的技术来缓解这一不足被称为交叉监控。这种方法涉及让同一责任层级的 Prometheus 实例相互监控。这样,每个实例至少会有另一个 Prometheus 在监视它,并在其失败时生成警报:

图 11.12:Prometheus 组自我监控

那么,如果问题出现在 Alertmanager 集群中会怎样?或者如果外部连接问题阻止了通知到达通知提供者呢?甚至如果通知提供者本身发生故障呢?在接下来的部分,我们将提供这些问题的可能解决方案。

死人开关警报

想象一下,你有一组 Prometheus 实例,使用 Alertmanager 集群进行警报。当某种原因导致这两个服务之间发生网络分区时,即使每个 Prometheus 实例都检测到它无法再连接任何 Alertmanager 实例,它们也没有办法发送通知。

在这种情况下,关于问题的任何警报都不会被发送:

图 11.13:Prometheus 与 Alertmanager 之间的网络分区

死人开关的原始概念指的是一个机制,它在停止触发/按下时会被激活。这个概念在软件世界中以多种方式得到了应用;就我们的目的而言,我们可以通过创建一个应该始终触发的警报来实现——从而持续发送通知——然后检查它是否停止触发。通过这种方式,我们可以全面测试从 Prometheus、通过 Alertmanager,到通知提供者,最终到达通知接收者的警报路径,以确保端到端的连接性和服务可用性。当然,这与我们了解的警报疲劳背道而驰,我们不希望一直收到关于总是触发的警报的页面或电子邮件。你可以实现自己的自定义服务,使用看门狗定时器,但那样你也需要监控这个服务。理想情况下,你应该利用第三方服务,这样可以减轻该服务遭遇与阻止通知发送的故障相同问题的风险。

为此,有一个围绕死人的开关类型警报构建的服务,名字也非常有趣,叫做Dead Man's Snitchdeadmanssnitch.com)。这是一个第三方服务,位于你的基础设施之外,负责通过电子邮件或 Webhook 接收你的始终触发的通知,并在该通知停止接收超过可配置时间后,发出页面、Slack 消息或 Webhook。这个设置减轻了我们之前提出的问题——即使整个数据中心起火,你仍然会收到警报!

Dead Man's Snitch 与 VictorOps 和 PagerDuty 集成的完整配置指南可以在help.victorops.com/knowledge-base/victorops-dead-mans-snitch-integration/www.pagerduty.com/docs/guides/dead-mans-snitch-integration-guide/找到。

摘要

在本章中,我们深入探讨了 Prometheus 堆栈中的告警组件——Alertmanager。这个服务的设计考虑了高可用性,我们有机会了解它的工作原理,从生成更好的通知到避免被无用的通知淹没。通知管道是理解 Alertmanager 内部工作原理的一个很好的起点,但我们也了解了它的配置,并通过示例更好地巩固了这些知识。我们还介绍了 amtool 及其提供的所有功能,如通过命令行直接添加、删除和更新静默。

Alertmanager 提供了多种通知集成方式,我们已经涵盖了所有这些方式,帮助你挑选出感兴趣的部分。既然我们都希望获得更好的通知,我们深入探讨了如何自定义默认通知,使用 Slack 作为示例。解决的一个难题是如何监控监控系统;在本章中,我们学习了如何确保在通知未正常发送时能够及时告警。

在一个不断变化的基础设施中,追踪哪些服务在运行以及它们运行在哪里并非易事。在下一章中,我们将深入探讨 Prometheus 如何处理服务发现,并为你自动化这些任务。

问题

  1. 如果在同一集群中 Alertmanager 实例之间发生网络分区,通知会发生什么?

  2. 一个告警可以触发多个接收者吗?为此需要满足什么条件?

  3. group_intervalrepeat_interval 有什么区别?

  4. 如果告警与任何配置的路由都不匹配,会发生什么?

  5. 如果 Alertmanager 原生不支持你需要的通知提供者,你该如何使用它?

  6. 在编写自定义通知时,CommonLabelsCommonAnnotations 是如何填充的?

  7. 你能做些什么来确保整个告警路径从头到尾都正常工作?

进一步阅读

第四部分:可扩展性、弹性和可维护性

伟大的事物往往从小事开始,但随着时间的推移,它们很快会变得难以控制。本节将介绍通过动态设置目标、分片和联邦来处理规模的几种选项。

以下章节包含在本节中:

  • 第十二章,选择合适的服务发现

  • 第十三章,扩展与联邦化 Prometheus

  • 第十四章,将长期存储与 Prometheus 集成

第十二章:选择合适的服务发现方式

在处理动态环境时,手动维护目标文件是不可行的。服务发现会为您处理复杂的不断变化的基础设施,确保没有任何服务或主机被遗漏。本章重点介绍如何利用 Prometheus 服务发现来减少在应对不断变化的基础设施管理工作中的繁琐工作。

简而言之,本章将涵盖以下主题:

  • 本章节的测试环境

  • 运行服务发现选项

  • 使用内建的服务发现

  • 构建自定义服务发现

本章节的测试环境

本章将重点介绍服务发现。为此,我们将部署两个新的实例,以模拟一个场景,其中 Prometheus 使用流行的服务发现软件动态生成目标。这个方法不仅能暴露所需的配置,还能验证所有内容如何协同工作。

我们将使用的设置类似于以下图示:

图 12.1:本章节的测试环境

Consul 的常见部署模式是在基础设施的每个节点上以客户端模式运行一个代理,然后与以服务器模式运行的 Consul 实例进行通信。此外,客户端实例充当 API 代理,因此 Prometheus Consul 服务发现通常会使用本地主机进行配置。然而,为了使各自的职责更加明确,我们在测试环境中选择仅在一台虚拟机上运行 Prometheus 实例,并在另一台虚拟机上运行作为服务器的 Consul。

在接下来的章节中,我们将解释如何启动并运行测试环境。

部署

要启动一个新的测试环境,请进入本章节的路径,相对于仓库根目录:

cd ./chapter12/

确保没有其他测试环境在运行,并启动本章节的环境:

vagrant global-status
vagrant up

您可以使用以下命令验证测试环境的成功部署:

vagrant status

这将为您提供以下输出:

Current machine states:

prometheus                running (virtualbox)
consul                    running (virtualbox)

This environment represents multiple VMs. The VMs are all listed above with their current state. For more information about a specific VM, run `vagrant status NAME`.

部署任务结束后,您将能够使用您最喜欢的启用了 JavaScript 的网页浏览器,在主机机器上验证以下端点:

服务 端点
Prometheus http://192.168.42.10:9090
Consul http://192.168.42.11:8500

您应该能够通过以下命令之一访问所需的实例:

实例 命令
Prometheus vagrant ssh prometheus
Consul vagrant ssh consul

清理

测试完成后,只需确保您位于 ./chapter12/ 目录中并执行以下命令:

vagrant destroy -f

不用太担心——如果需要,您可以轻松地重新启动环境。

运行服务发现选项

Prometheus 开箱即用地提供了多个发现集成。这些集成覆盖了大多数主流数据源,用于应用和机器清单,例如公共和私有云计算 API、虚拟机和容器编排系统、独立的服务注册和发现系统等。对于 Prometheus 不直接支持的发现机制,可以通过通用的发现系统进行集成,利用文件系统和一些粘合代码,就像我们在本章稍后看到的那样。

每个集成都以相同的方式工作——通过将所有发现的地址作为目标,并将其相关的元数据作为临时标签(在没有某些重新标签的情况下,这些标签不会持久化)。对于每个被发现的目标,__address__ 标签通常被设置为服务地址和端口。这一点很重要,因为这个标签是 Prometheus 用来连接抓取目标的标签;instance 标签在没有显式定义时默认使用 __address__ 的值,但它可以设置为任何其他便于识别目标的值。

服务发现集成提供的元数据标签遵循 __meta_<service discovery name>_<key> 的模式。还有一些标签是 Prometheus 添加的,例如 __scheme____metrics_path__,它们分别定义是否应使用 HTTP 或 HTTPS 执行抓取,以及配置的抓取端点。

metrics_path 抓取配置中不支持 URL 参数。相反,这些参数需要在 params 配置中设置。相关内容将在 第五章 中介绍,运行 Prometheus 服务器

以下章节提供了可用的发现选项概述,并展示了如何配置它们的一些示例,附带生成数据的截图。

云提供商

随着云基础设施的崛起,在这些环境中运行工作负载变得越来越常见。这带来了新的挑战,例如短暂且高度动态的基础设施配置。可扩展性易于实现也是需要考虑的因素:过去,可能需要几个月的时间来协商、达成支持合同、购买、部署和配置新硬件;而现在,只需几秒钟即可启动并运行一组新的实例。随着像自动扩展这样的技术出现,它可以在你甚至不知道的情况下部署新实例,变化的速度让人难以跟上。为了减轻对这种云原生动态基础设施的监控负担,Prometheus 开箱即用地与一些 基础设施即服务 (IaaS) 市场中的大玩家进行了集成,例如 Amazon Web Services、Microsoft Azure Cloud、Google Cloud Platform、OpenStack 和 Joyent。

以 Amazon 弹性计算 (EC2) 为虚拟机发现的例子,抓取任务的配置可以像下面这样简单:

scrape_configs:
 - job_name: ec2_sd
   ec2_sd_configs:
    - region: eu-west-1
      access_key: ACCESSKEYTOKEN
      secret_key: 'SecREtKeySecREtKey+SecREtKey+SecREtKey'

其他云服务提供商的设置可能有所不同,但逻辑基本相同。基本上,我们需要设置适当的凭据级别来查询云服务提供商的 API,以便 Prometheus 发现集成能够获取生成目标所需的所有数据以及其相关的元数据。以下截图演示了如何将类似于之前列出的配置但带有实际凭据的设置转化为一组目标:

图 12.2:Prometheus /service-discovery 端点,展示 ec2_sd 数据

正如我们在前面的截图中看到的,EC2 发现将相当多的元数据标签附加到每个发现的目标。这些标签在重新标记阶段可用,您可以利用它们只抓取正在运行的目标,将抓取地址从私有 IP 地址更改为公共 IP 地址,或者将实例标签重命名为更友好的名称。

从发现过程中收集的信息要么是定期刷新(刷新间隔可以在服务发现级别进行配置),要么通过监视自动刷新,使得 Prometheus 能够意识到目标的创建或删除。

容器编排器

容器编排器是提取正在运行的服务及其位置的完美场所,因为它们的工作正是管理这些信息。因此,Prometheus 的发现机制支持一些最广泛使用的容器编排平台,如 Kubernetes 和 Marathon,这是 Mesos 和 DC/OS 的容器编排平台。由于我们在本书的大部分示例中使用了 Kubernetes,因此我们将重点介绍该平台,以解释这些系统是如何工作的。

和 Prometheus 一样,Kubernetes 也是云原生计算基金会CNCF)的一个毕业项目。虽然这并不意味着一个是专门为了与另一个一起工作的,但二者之间的联系是不可否认的。Google 的容器编排和监控系统 Borg 和 Borgmon,分别是 Kubernetes 和 Prometheus 的灵感来源。为了应对 Kubernetes 等云原生平台的监控,其中变化的速度几乎令人难以承受,必须具备一套特别的功能。Prometheus 符合这些需求,例如高效处理容器的短暂性。

Prometheus 服务发现集成通过 Kubernetes API 检索所有所需的数据,保持与集群状态的同步。由于可查询的 API 对象数量庞大,Prometheus 的发现配置中引入了角色(role)概念,这些角色可以是 nodeservicepodendpointingress。虽然解释 Kubernetes 核心概念超出了本书的范围,但我们可以快速了解每个角色的用途:node 用于收集形成 Kubernetes 集群的实际节点(例如,运行 kubelet 代理的虚拟机),因此可以用来监控集群本身以及其底层基础设施;Kubernetes 中的服务对象充当负载均衡器,service 将提供每个配置服务的每个端口的单个端点,无论它是由一个还是多个应用实例支持——并且仅用于黑盒监控;pod 用于发现独立的 Pod,无论它们是否属于某个服务;endpoint 发现支持特定服务的 Pod 中的主要进程;最后,ingress 类似于 service,返回一个应用实例集合的外部负载均衡器,因此仅应在端到端探测中使用。

以下代码片段提供了一个查询 Pods 的示例,匹配具有标签 app 且值为 hey 的 Pods:

scrape_configs:
  - job_name: kubernetes_sd
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - action: keep
        regex: hey
        source_labels:
          - __meta_kubernetes_pod_label_app

前面的配置生成了以下截图中所示的数据,在其中我们可以看到通过 Kubernetes API 收集的所有元数据:

图 12.3:Prometheus /service-discovery 端点,展示 kubernetes_sd 数据

这是一个可以完成的非常小的示例。使用 Kubernetes 服务发现的配置通常广泛使用 relabel_configs 来过滤目标,重写 job 标签以匹配容器名称,并且通常基于 Kubernetes 注解的约定进行巧妙的自动配置。

服务发现系统

随着服务数量的增加,越来越难以将所有内容联系在一起——无论是在服务正确配置以相互通信方面,还是在操作员能看到系统行为的方面。解决这些问题的常见方法是实现一个服务发现系统,它充当注册表,软件客户端以及监控系统可以查询该注册表。

Prometheus 无缝集成了几个主流的服务发现系统,目前支持 Consul、Nerve 和 ServerSets。直接与发现服务集成使 Prometheus 始终能够获取当前运行的服务及其位置,从而使服务实例在创建后能够被自动监控,直到它们被销毁。

Consul 目前是最受欢迎的,它提供了一整套功能来实现服务发现,并且拥有强大且易于使用的命令行工具和 API,且易于扩展。我们以以下内容作为示例:

scrape_configs:
  - job_name: 'consul_sd'
    consul_sd_configs:
      - server: http://consul.prom.inet:8500
        datacenter: dc1
    relabel_configs:
      - source_labels: [__meta_consul_service]
        target_label: job
      - source_labels: [job, __address__]
        regex: "consul;([^:]+):.+"
        target_label: __address__
        replacement: ${1}:9107

上面的示例转换成如下截图,我们可以看到不仅是生成的标签,还有目标的定义:

图 12.4:Prometheus /service-discovery 端点展示 consul_sd 数据

上面的示例展示了一个工作配置,用于从 Consul 服务器中所有可用服务收集数据,使用 relabel_configs 将目标的 job 标签重写为服务名称,而不是 job_name。这意味着在 Consul 中注册的每个应用实例都会被自动拾取为抓取目标,并正确分配适当的作业名称。此外,最后的重标签规则会在服务名为 Consul 时将目标端口更改为 9107,从而将目标从 Consul 本身更改为其导出器。

基于 DNS 的服务发现

这种类型的服务发现依赖于 DNS 来收集数据。它通过定义一系列需要定期查询的域名来获取目标。用于解析的名称服务器会在 /etc/resolv.conf 中查找。除了支持 A 和 AAAA DNS 记录外,该发现集成还能够查询 SRV 记录,SRV 记录还提供服务的端口:

~$ dig SRV hey.service.example.inet
...
;; QUESTION SECTION:
;hey.service.example.inet. IN SRV

;; ANSWER SECTION:
hey.service.example.inet. 0 IN SRV 1 1 8080 server01.node.example.inet.

;; ADDITIONAL SECTION:
server01.node.example.inet. 0 IN A 192.168.42.11
server01.node.example.inet. 0 IN TXT "squad=purple"
...

我们可以看到,通过查询 hey.service.example.inet 的 SRV 记录,在此示例中,我们得到服务位置 server01.node.example.inet 和端口 8080。我们还得到了包含服务 IP 地址的 A 记录和包含一些元数据的 TXT 记录。

以下代码片段展示了使用此 DNS 服务发现集成的示例抓取配置。它通过使用之前提到的域名 hey.service.example.inet 来实现:

scrape_configs:
  - job_name: 'dns_sd'
    dns_sd_configs:
      - names:
        - hey.service.example.inet

返回的 SRV 记录将转换为新的目标。Prometheus 不支持 RFC 6763 中指定的高级 DNS-SD,它允许通过相关的 TXT 记录传输元数据(如之前在 dig 命令中看到的)。这意味着只能使用此方法发现服务地址和端口。我们可以在以下截图中看到发现的标签:

图 12.5:Prometheus /service-discovery 端点展示 dns_sd 数据

在所有发现集成中,这是提供的元数据最少的一个。此外,使用 DNS 进行服务发现很难做到正确——需要规划慢速收敛,考虑多个不同的缓存层,这些缓存层可能会或可能不会尊重记录的 TTL 等问题。这种方法应该仅在高级用例中考虑。

基于文件的服务发现

类似于 webhook 通知器为集成不受支持的通知系统提供解决方案(如 第十一章《理解与扩展 Alertmanager》中所解释的),基于文件的集成为服务发现提供了相同类型的解决方案。它的工作原理是加载有效的 JSON 或 YAML 文件列表,这些文件用于生成所需的目标及其标签。文件发现更改后不需要重载或重启 Prometheus,因为它们会被监控并自动重新读取,具体取决于操作系统。此外,作为回退,发现文件也会按照预定计划(默认每 5 分钟)被读取。

以下 JSON 片段展示了一个有效的 Prometheus 服务发现文件。正如我们所看到的,它包含了标签列表和一个目标数组,这些标签适用于目标:

[
    {
        "labels": {
            "job": "node"
        },
        "targets": [
            "192.168.42.11:9100"
        ]
    }
]

以下抓取配置使用了 file_sd 服务发现,它加载了之前展示的 file_sd.json 文件:

scrape_configs:
  - job_name: 'file_sd'
    file_sd_configs:
      - files:
        - file_sd.json

files 列表还允许对路径的最后一个元素进行 glob 匹配,即对文件级别进行匹配。

从该配置中发现的目标可以在下图中看到,我们可以检查文件提供的元数据以及 Prometheus 自动生成的标签:

图 12.6:Prometheus /service-discovery 端点展示 file_sd 数据

很容易看出,这种集成打开了无数可能性:这些文件可以通过一个常驻的守护进程或者一个定时任务(cron job)创建,使用 shell 脚本(甚至是简单的 wget)或完备的编程语言,或者通过配置管理工具来部署。我们将在本章后面讨论如何构建自定义服务发现时,深入探讨这个话题。

使用内置的服务发现

为了理解 Prometheus 和服务发现提供者之间的集成方式,我们将依赖于我们的测试环境。进一步来说,我们将提供一个在 Kubernetes 中运行的 Prometheus 工作示例,依赖于该平台的原生服务发现。这些动手示例将展示如何将一切结合起来,帮助你不仅理解其优点,更重要的是理解这些机制的简单性。

使用 Consul 服务发现

在本章中,我们将 Consul 配置为虚拟机基础的测试环境中的示例服务发现系统——Consul 的设置非常简单,这使得它非常适合我们的示例。它的工作方式是在每个节点上运行一个客户端模式的代理,以及在服务器模式下运行的一个奇数个代理,后者维护服务目录。客户端节点上可用的服务会直接传递给服务器节点,而集群成员身份则通过集群中每个节点之间使用的 gossip 协议(随机点对点消息传递)进行传播。由于我们的主要目标是展示使用 Consul 进行 Prometheus 服务发现,我们将测试环境配置为在开发模式下运行的代理,从而启用一个内存中的服务器来进行实验。当然,这完全忽视了安全性、可扩展性、数据安全性和弹性;关于如何正确配置 Consul 的文档可以在learn.hashicorp.com/consul/找到,在部署和维护 Consul 于生产环境中时应予以考虑。

要查看测试环境中如何配置此项,我们需要连接到运行 Consul 的实例:

vagrant ssh consul

从这里,我们可以开始探索 Consul 是如何配置的。例如,以下片段展示了正在使用的 systemd 单元文件,在其中我们可以看到正在使用的配置标志——它被配置为在开发模式下以代理身份运行,并且必须将其端口绑定到实例的外部 IP 地址:

vagrant@consul:~$ systemctl cat consul.service 
...
[Service]
User=consul
ExecStart=/usr/bin/consul agent \
 -dev \
 -bind=192.168.42.11 \
 -client=192.168.42.11 \
 -advertise=192.168.42.11
...

如果我们运行ss并过滤其输出,只显示属于 Consul 的行,我们可以找到它正在使用的所有端口:

vagrant@consul:~$ sudo /bin/ss -lnp | grep consul
udp UNCONN 0 0 192.168.42.11:8301 0.0.0.0:* users:(("consul",pid=581,fd=8))
udp UNCONN 0 0 192.168.42.11:8302 0.0.0.0:* users:(("consul",pid=581,fd=6))
udp UNCONN 0 0 192.168.42.11:8600 0.0.0.0:* users:(("consul",pid=581,fd=9))
tcp LISTEN 0 128 192.168.42.11:8300 0.0.0.0:* users:(("consul",pid=581,fd=3))
tcp LISTEN 0 128 192.168.42.11:8301 0.0.0.0:* users:(("consul",pid=581,fd=7))
tcp LISTEN 0 128 192.168.42.11:8302 0.0.0.0:* users:(("consul",pid=581,fd=5))
tcp LISTEN 0 128 192.168.42.11:8500 0.0.0.0:* users:(("consul",pid=581,fd=11))
tcp LISTEN 0 128 192.168.42.11:8502 0.0.0.0:* users:(("consul",pid=581,fd=12))
tcp LISTEN 0 128 192.168.42.11:8600 0.0.0.0:* users:(("consul",pid=581,fd=10))

如我们所见,Consul 监听了多个端口,包括 TCP 和 UDP。我们关心的端口是提供 HTTP API 的端口,默认是 TCP 端口8500。如果我们在浏览器中访问http://192.168.42.11:8500,我们将看到类似以下内容:

图 12.7:Consul Web 界面显示其默认配置

默认配置了一个服务,即 Consul 服务本身。

为了让这个示例更加有趣,我们还在consul实例中部署了consul_exporter(一个由 Prometheus 项目提供的导出器)。这个导出器无需在 Consul 端进行任何额外配置,因此应该能够直接工作。我们可以在 systemd 单元文件中找到运行该服务所使用的配置,像这样:

vagrant@consul:~$ systemctl cat consul-exporter.service 
...
[Service]
User=consul_exporter
ExecStart=/usr/bin/consul_exporter --consul.server=consul:8500
...

consul_exporter的源代码和安装文件可在github.com/prometheus/consul_exporter找到。

为了验证导出器是否正确地联系到 Consul 并解析其指标,我们可以运行以下指令:

vagrant@consul:~$ curl -qs localhost:9107/metrics | grep "^consul"
consul_catalog_service_node_healthy{node="consul",service_id="consul",service_name="consul"} 1
consul_catalog_services 1
consul_exporter_build_info{branch="HEAD",goversion="go1.10.3",revision="75f02d80bbe2191cd0af297bbf200a81cbe7aeb0",version="0.4.0"} 1
consul_health_node_status{check="serfHealth",node="consul",status="critical"} 0
consul_health_node_status{check="serfHealth",node="consul",status="maintenance"} 0
consul_health_node_status{check="serfHealth",node="consul",status="passing"} 1
consul_health_node_status{check="serfHealth",node="consul",status="warning"} 0
consul_raft_leader 1
consul_raft_peers 1
consul_serf_lan_members 1
consul_up 1

当 exporter 成功连接并从 Consul 收集指标时,它会将 consul_up 指标设置为 1。我们还可以看到 consul_catalog_services 指标,它告诉我们 Consul 知道有一个服务,与我们在 Web 界面中看到的内容一致。

现在我们可以断开与 consul 实例的连接,并使用以下命令连接到 prometheus 实例:

exit
vagrant ssh prometheus

如果我们查看 Prometheus 服务器配置,我们将发现以下内容:

vagrant@prometheus:~$ cat /etc/prometheus/prometheus.yml 
...

 - job_name: 'consul_sd'
 consul_sd_configs:
 - server: http://consul:8500
 datacenter: dc1
 relabel_configs:
 - source_labels: [__meta_consul_service]
 target_label: job
...

该配置允许 Prometheus 连接到 Consul API 地址(可通过 http://192.168.42.11:8500 访问),并通过 relabel_configs 重写 job 标签,使其与服务名称(如 __meta_consul_service 标签中所示)匹配。如果我们检查 Prometheus 的 Web 界面,我们可以看到以下信息:

图 12.8:Prometheus /service-discovery 端点显示 Consul 默认服务

现在,到了有趣的部分:让我们通过将 consul_exporter 定义为 Consul 中的一个服务,自动添加一个抓取目标。该章节的资源中提供了一个包含 Consul 服务配置的 JSON 有效负载,我们可以通过 Consul API 将其添加。有效负载位于以下路径:

vagrant@prometheus:~$ cat /vagrant/chapter12/configs/consul_exporter/payload.json 
{
 "ID": "consul-exporter01",
 "Name": "consul-exporter",
 "Tags": [
 "consul",
 "exporter",
 "prometheus"
 ],
 "Address": "consul",
 "Port": 9107
}

使用以下指令,我们将通过 HTTP API 将此新服务添加到 Consul 的服务目录中:

vagrant@prometheus:~$ curl --request PUT \
--data @/vagrant/chapter12/configs/consul_exporter/payload.json \
http://consul:8500/v1/agent/service/register

运行此命令后,我们可以通过查看 Consul Web 界面来验证新服务是否已被添加,该界面将显示类似如下内容:

图 12.9:Consul Web 界面显示 consul-exporter 服务

最后,我们可以检查 Prometheus /service-discovery 端点,查看是否有一个新目标,从而证明 Consul 服务发现功能正常:

图 12.10:Prometheus /service-discovery 端点显示 consul-exporter 目标

如果我们再次查看 consul_catalog_services 指标,我们会发现它已经变为 2。由于我们现在正在 Prometheus 中收集 consul_exporter 指标,我们可以使用 promtool 查询其当前值:

vagrant@prometheus:~$ promtool query instant http://localhost:9090 'consul_catalog_services'
consul_catalog_services{instance="consul:9107", job="consul-exporter"} => 2 @[1555252393.681]

Consul 标签可以用于通过 relabel_configs 配置抓取任务,适用于有不同要求的服务,例如在某个标签存在时更改指标路径,或者使用一个标签来标记是否使用 HTTPS 进行抓取。__meta_consul_tags 标签的值在开始和结束时有逗号分隔符,以便更容易进行匹配;这样,你就不需要根据你尝试匹配的标签在字符串中的位置来特别处理正则表达式。一个实际的例子如下:

...
    relabel_configs: 
      - source_labels: [__meta_consul_tags]
        regex: .*,exporter,.*
        action: keep
...

这将仅保留在 Consul 中注册的带有 exporter 标签的服务,丢弃其他所有服务。

使用 Kubernetes 服务发现

在这个例子中,我们将不再使用前几章中的 Prometheus Kubernetes Operator,而是将重点放在 Prometheus 原生服务发现集成上,以便与这个容器编排平台配合使用。用于在 Kubernetes 测试环境中启动 Prometheus 的清单文件可以在以下路径中找到,相对于代码仓库的根路径:

cd ./chapter12/provision/kubernetes/

以下步骤将确保在新 Kubernetes 环境中配置所有所需的软件,从而使我们能够专注于服务发现组件。

验证没有其他环境在运行:

minikube status
minikube delete

启动一个空的 Kubernetes 环境:

minikube start \
  --cpus=2 \
  --memory=3072 \
  --kubernetes-version="v1.14.0" \
  --vm-driver=virtualbox \
  --extra-config=kubelet.authentication-token-webhook=true \
  --extra-config=kubelet.authorization-mode=Webhook

我们提供给 minikube 的额外配置是必需的,以便 Prometheus 能够通过服务帐户令牌与 kubelets 进行交互。当前一个命令完成时,一个新的 Kubernetes 环境将准备就绪。然后我们可以按照以下说明部署我们的配置:

kubectl apply -f ./bootstrap/
kubectl rollout status deployment/prometheus-deployment -n monitoring

前面的命令应用了几个清单文件,其中包括创建一个名为 monitoring 的命名空间、一个 ServiceAccount,以及所有所需的 RBAC 配置,以便 Prometheus 可以查询 Kubernetes API。还包括一个包含 Prometheus 服务器配置的 ConfigMap,该配置可以在 bootstrap/03_prometheus-configmap.yaml 中找到。它定义了多个 Kubernetes 组件的抓取任务,这些任务通过服务发现功能进行目标定位,正如以下片段所示:

$ cat bootstrap/03_prometheus-configmap.yaml
...
data:
 prometheus.yml: |
 scrape_configs:
...
 - job_name: kubernetes-pods
 kubernetes_sd_configs:
 - role: pod
 relabel_configs:
 - action: keep
 regex: hey
 source_labels:
 - __meta_kubernetes_pod_label_app
...

我们可以通过执行以下命令打开 Prometheus Web 界面:

minikube service prometheus-service -n monitoring

通过进入 /service-discovery 端点的服务发现部分,我们可以看到,尽管发现了几个 pod,但没有一个匹配 app 标签的 hey 标签值,因此它们被丢弃:

图 12.11:Prometheus /service-discovery 端点显示 kubernetes-pods 任务丢弃的目标

现在是时候添加一些带有正确标签/值对的新 pod,以触发我们的服务发现配置。我们可以通过运行以下命令来继续部署 hey 应用程序,然后跟踪部署状态:

kubectl apply -f ./services/
kubectl rollout status deployment/hey-deployment -n default

在成功部署后,我们可以再次访问 Prometheus Web 界面,在 /service-discovery 端点查看,发现现在在 kubernetes-pods 抓取任务中有三个活动目标。以下截图展示了其中一个目标及其所有由 Kubernetes API 提供的标签:

图 12.12:Prometheus /service-discovery 端点显示为 kubernetes-pods 任务发现的目标

测试完成后,您可以通过执行以下命令删除此 Kubernetes 环境:

minikube delete

这种服务发现方式使我们能够自动跟踪多个 Kubernetes 对象,而无需手动更改 Prometheus 配置。该环境使我们能够测试各种设置,并为定制 Kubernetes 服务发现以满足我们的特定需求提供了基础。

构建自定义服务发现

即便有许多现成的服务发现选项,仍有许多其他系统/提供者不被默认支持。在这些情况下,我们有几种选择:

  • 提交一个功能请求,请求 Prometheus 支持该特定的服务发现,并依赖社区和/或维护者来实现它。

  • 在 Prometheus 中自行实现服务发现集成,并维护一个分支或将其贡献回项目。

  • 找到一种方法,以最少的维护工作和时间成本将所需的目标导入 Prometheus 实例,并且无需依赖 Prometheus 路线图来完成这项工作。

前两种选项不太理想,因为它们要么超出了我们的控制范围,要么维护起来很麻烦。此外,在没有较大兴趣和支持社区的情况下将额外的服务发现集成到 Prometheus 中,会给维护者带来过多的支持负担,而维护者目前并未接受任何新的集成。幸运的是,有一种方法可以轻松与任何类型的服务或实例目录进行集成,而无需维护昂贵的分支或创造性的黑客解决方案。在接下来的部分,我们将探讨如何将我们自己的服务发现与 Prometheus 集成。

自定义服务发现基础

集成自定义服务发现的推荐方式是依赖基于文件的服务发现file_sd。集成实现的方式是通过一个进程(本地或远程、定时或持续运行)查询数据源(目录/API/数据库/配置管理数据库CMDB)),然后将所有目标及其相应标签写入一个 Prometheus 可访问的路径上的 JSON 或 YAML 格式文件。然后,Prometheus 会通过磁盘监控或定时任务自动读取该文件,从而使你能够动态更新可供抓取的目标。

以下图表展示了上述工作流程:

图 12.13:自定义服务发现流程

这种方法足够通用,可以满足大多数(如果不是全部)所需的用例,使得以简单明了的方式构建自定义服务发现机制成为可能。

社区驱动的file_sd集成可以在 prometheus.io/docs/operating/integrations/#file-service-discovery 找到。

现在我们已经知道了这种集成方式的工作原理,接下来让我们直接动手,开始构建我们自己的服务发现。

推荐方式

如前所述,构建自定义服务发现似乎是一个可管理的任务。我们需要查询数据并将数据按照标准格式写入文件。为了简化我们的生活,Prometheus 团队提供了一个适配器,可以消除创建新服务发现集成的大部分样板代码。这个适配器仅适用于 Go 编程语言,因为它重用了来自 Prometheus 本身的一些代码。这种设计使得一些维护较少的服务发现集成可以不太费力地迁移到独立服务中,同时也简化了使用适配器构建的外部发现集成迁移到主 Prometheus 二进制文件中的过程,这些集成都已经被证明是有效的。请注意,并没有阻止您使用您选择的语言来构建这样的集成,但出于遵循推荐方法的考虑,我们将坚持使用 Go 和这个发现适配器。关于如何在 Go 中编程的详细说明超出了本书的范围。

在主 Prometheus 代码库中,我们可以找到适配器的代码,以及一个使用 Consul 的示例,有趣的是,我们已经在我们的测试环境中设置了 Consul。正如我们现在所知,Consul 集成在 Prometheus 中得到了原生支持;然而,假装它没有得到支持,我们需要与之集成。在接下来的主题中,我们将详细讨论如何将所有内容整合在一起并构建一个自定义的服务发现。

自定义服务发现示例的代码可在github.com/prometheus/prometheus/tree/v2.9.1/documentation/examples/custom-sd找到。

服务发现适配器

作为高层次概述,适配器负责启动和管理我们自定义服务发现代码,消费它生成的目标组,并将它们转换为file_sd格式,并确保在需要时将 JSON 数据写入文件。使用这个适配器编写服务发现集成时,不需要修改其代码,因此它可以作为一个库直接导入。为了更清楚地解释适配器的行为,我们将解释一些低级细节,以便在实现我们自己的发现时其行为变得清晰。

以下代码片段说明了我们需要从我们的代码中调用的适配器的Run函数。这个函数将负责在它自己的 goroutine 中启动discovery.Managera.manager.Run),指示它运行我们的发现实现(a.disc),最后在另一个 goroutine 中运行适配器本身(a.runCustomSD):

// Run starts a Discovery Manager and the custom service discovery implementation.
func (a *Adapter) Run() {
    go a.manager.Run()
    a.manager.StartCustomProvider(a.ctx, a.name, a.disc)
    go a.runCustomSD(a.ctx)
}

启动后,适配器从 Manager 提供的通道消费,更新我们的代码将生成的目标组。当有更新时,它将把目标组转换为 file_sd 格式,并验证自上次更新以来是否有任何更改。如果有更改,它将保存新的目标组以备将来比较,并将它们以 JSON 格式写入输出文件。这意味着应该在每次更新中发送完整的目标组列表;没有通过通道发送的组将从生成的发现文件中删除。

file_sd 适配器源代码可在 github.com/prometheus/prometheus/blob/v2.9.2/documentation/examples/custom-sd/adapter/adapter.go 找到。

自定义服务发现示例

现在我们已经了解了适配器的工作原理,让我们看看我们需要实现哪些内容来使我们的自定义服务发现工作。正如我们之前看到的,适配器使用了 discovery.Manager,因此我们需要提供一个 Discoverer 接口的实现,以便它可以运行我们的发现。该接口的样式如下:

type Discoverer interface {
    Run(ctx context.Context, up chan<- []*targetgroup.Group)
}

Discoverer 接口文档可在 godoc.org/github.com/prometheus/prometheus/discovery#Discoverer 找到。

这意味着我们只需要实现 Run 函数,在其中我们将在循环中运行我们发现的逻辑,生成适当的目标组,并将它们通过 up 通道发送到适配器。ctx 上下文存在是为了让我们知道何时需要停止。我们实现的代码将定期收集来自我们数据源的所有可用目标/元数据。在本例中,我们使用的是 Consul,它要求我们首先获取服务列表,然后针对每个服务查询支持它的实例及其元数据以生成标签。如果发生故障,我们不会通过通道发送任何更新,因为提供过时的数据比提供不完整或不正确的数据更好。

最后,在我们的 main 函数中,我们只需要实例化一个新的适配器,并提供一个后台上下文、输出文件的名称、我们发现实现的名称、实现 Discoverer 接口的发现对象,以及一个 log.Logger 实例:

func main() {
...
  sdAdapter := adapter.NewAdapter(ctx, *outputFile, "exampleSD", disc, logger)
  sdAdapter.Run()
...
}

此适配器实现的工作示例可在 github.com/prometheus/prometheus/blob/v2.9.2/documentation/examples/custom-sd/adapter-usage/main.go 找到。

下一步是部署并集成这个新创建的服务发现提供者与 Prometheus,这是我们将在接下来的部分中完成的。

使用自定义服务发现

为了亲自观察自定义服务发现的表现,我们将依赖于我们的测试环境。custom-sd 二进制文件作为自定义服务发现的示例,重新创建了 Consul 服务发现集成,已经与 Prometheus 一起部署并准备使用。加上 Consul 部署,我们在测试环境中具备了所有必要的组件,可以看到一切如何协同工作。

custom-sd 可以在配置了 Go 开发环境的机器上通过执行以下命令构建:go get github.com/prometheus/prometheus/documentation/examples/custom-sd/adapter-usage

首先,我们需要确保我们已连接到 prometheus 实例。我们可以使用以下命令:

vagrant ssh prometheus

接下来,我们可以修改 Prometheus 配置,使用 file_sd 作为我们的集成方式。为此,我们必须将原本配置为使用 consul_sd 的抓取任务替换为新的任务。为了简化操作,我们已经将这个更改写入了 /etc/prometheus/ 下的配置文件。要使用它,你只需要将当前配置替换为新的配置:

vagrant@prometheus:~$ sudo mv /etc/prometheus/prometheus_file_sd.yml /etc/prometheus/prometheus.yml

我们感兴趣的抓取任务如下:

- job_name: 'file_sd'
    file_sd_configs:
      - files:
        - custom_file_sd.json

为了让 Prometheus 知道这些更改,我们必须重新加载它:

vagrant@prometheus:~$ sudo systemctl reload prometheus

我们还应该确保 Consul 服务器已配置了我们之前添加的 consul-exporter。如果你错过了这一步,可以通过运行以下代码来添加:

vagrant@prometheus:~$ curl --request PUT \
--data @/vagrant/chapter12/configs/consul_exporter/payload.json \
http://consul:8500/v1/agent/service/register

如果我们查看 Prometheus 的 Web 界面,我们会看到类似如下的内容:

12.14:Prometheus /service-discovery 端点没有任何 file_sd 目标

现在我们可以开始尝试 custom-sd 应用程序了。我们需要指定 Consul API 地址和 Prometheus 配置为读取的输出文件路径。以下命令将完成这项工作,并确保正确的用户创建文件,以便 Prometheus 进程能够访问它:

vagrant@prometheus:~$ sudo -u prometheus -- custom-sd --output.file="/etc/prometheus/custom_file_sd.json" --listen.address="consul:8500"

现在我们已经启动了自定义服务发现。如果我们返回到 Prometheus 的 Web 界面,并访问 /service-discovery 端点,我们将能够看到已发现的目标:

12.15:Prometheus /service-discovery 端点显示已发现的目标

我们还可以检查由 custom-sd 创建的文件,并验证其内容,如下所示(输出已被压缩以节省空间):

vagrant@prometheus:~$ sudo cat /etc/prometheus/custom_file_sd.json 
[
 {
 "targets": ["consul:9107"],
 "labels": {
 "__address__": "consul:9107",
 "__meta_consul_address": "192.168.42.11",
 "__meta_consul_network_segment": "",
 "__meta_consul_node": "consul",
 "__meta_consul_service_address": "consul",
 "__meta_consul_service_id": "consul-exporter01",
 "__meta_consul_service_port": "9107",
 "__meta_consul_tags": ",consul,exporter,prometheus,"
 }}]

就这样!你现在已经成功搭建了一个自定义服务发现系统,并且与 Prometheus 完全集成,使用基于文件的服务发现机制。更为严谨的部署方式是将 custom-sd 服务作为守护进程运行。如果你更喜欢使用脚本语言,可以选择编写一个生成发现文件并退出的服务发现脚本,在这种情况下,将其作为定时任务运行是一个可行的选择。最后的建议是,你可以让你的配置管理软件按计划动态生成发现文件。

总结

在本章中,我们有机会了解为什么服务发现对于以合理的方式管理日益增长的基础设施至关重要。Prometheus 提供了几种开箱即用的服务发现选项,这些选项可以帮助你以非常快速和友好的方式开始使用。我们介绍了 Prometheus 提供的可用服务发现选项,并向你展示了从中可以期待的内容。然后,我们通过使用 Consul 和 Kubernetes 的几个例子,具体化了之前介绍的概念。最后,我们介绍了如何通过使用推荐的方法和依赖 file_sd 将自定义服务发现与 Prometheus 集成。

在下一章中,我们将介绍如何扩展和联邦 Prometheus。

问题

  1. 为什么在 Prometheus 中使用服务发现机制?

  2. 当你使用云服务提供商的服务发现时,设置集成的主要要求是什么?

  3. 基于 DNS 的服务发现集成支持哪些类型的记录?

  4. 在 Kubernetes 服务发现集成中,角色概念的作用是什么?

  5. 当你构建自定义服务发现时,你将依赖哪些可用的集成?

  6. file_sd 中配置的目标文件更新时,你需要重新加载 Prometheus 吗?

  7. 构建自定义服务发现的推荐方式是什么?

进一步阅读

第十三章:扩展和联邦 Prometheus

Prometheus 被设计为单一服务器运行。这种方法可以让你处理成千上万的目标和数百万个时间序列,但随着扩展,你可能会发现这种方法不足以应对。在这一章中,我们将讨论这一需求,并阐明如何通过分片扩展 Prometheus。然而,分片使得获得全局视图变得更加困难。为了解决这个问题,我们还将介绍分片的优缺点、联邦是如何介入的,最后,我们将介绍 Thanos,这是由 Prometheus 社区创建的一个组件,旨在解决一些提出的问题。

简而言之,本章将涵盖以下主题:

  • 本章的测试环境

  • 利用分片扩展

  • 通过联邦实现全局视图

  • 使用 Thanos 来缓解 Prometheus 在大规模部署中的不足

本章的测试环境

在本章中,我们将重点讨论如何扩展和联合 Prometheus。为此,我们将部署三个实例,模拟一个全局 Prometheus 实例从另外两个实例收集度量数据的场景。这种方法不仅可以帮助我们探索所需的配置,还可以理解各个组件是如何协同工作的。

我们将使用的设置如下面的图表所示:

图 13.1:本章的测试环境

在下一节中,我们将解释如何启动和运行测试环境。

部署

要启动新的测试环境,请进入以下章节路径,该路径相对于代码库的根目录:

cd ./chapter13/

确保没有其他测试环境正在运行,并使用以下命令启动本章的环境:

vagrant global-status
vagrant up

你可以使用以下命令验证测试环境是否成功部署:

vagrant status

这将输出以下内容:

Current machine states:

shard01                   running (virtualbox)
shard02                   running (virtualbox)
global                    running (virtualbox)

This environment represents multiple VMs. The VMs are all listed above with their current state. For more information about a specific VM, run `vagrant status NAME`.

部署任务完成后,你可以使用你喜欢的支持 JavaScript 的网页浏览器,在主机上验证以下端点:

服务 端点
Shard01 Prometheus http://192.168.42.10:9090
Shard02 Prometheus http://192.168.42.11:9090
全局 Prometheus http://192.168.42.12:9090
全局 Thanos 查询器 http://192.168.42.12:10902

你应该能够使用相应的命令访问这些实例:

实例 命令
Shard01 vagrant ssh shard01
Shard02 vagrant ssh shard02
全局 vagrant ssh global

清理

测试完成后,只需确保你处于./chapter13/目录下,并执行以下命令:

vagrant destroy -f

不用太担心:如果需要,你可以很容易地再次启动环境。

利用分片扩展

随着业务增长,团队、基础设施和应用的增多,单一 Prometheus 服务器的运行开始变得不可行:记录/告警规则和抓取任务的变化变得更加频繁(因此需要重新加载,具体时间取决于配置的抓取间隔,可能需要几分钟),Prometheus 在处理大量数据时可能会错过一些抓取,或者负责该实例的人员或团队可能会成为组织流程中的瓶颈。出现这种情况时,我们需要重新思考解决方案的架构,以便其能够按需扩展。幸运的是,这是社区反复解决过的问题,因此已经有了一些关于如何处理此问题的建议。这些建议围绕着分片展开。

在这个背景下,分片意味着将抓取目标列表分配到两个或更多 Prometheus 实例中。这可以通过两种方式实现:垂直分片或水平分片。垂直分片是最常用的方法,它通过将抓取任务按照逻辑(例如按范围、技术、组织边界等)分配到不同的 Prometheus 实例中来实现,其中分片的单位是抓取任务。相对而言,水平分片是在抓取任务层级进行的,它意味着为每个任务配置多个 Prometheus 实例,每个实例抓取目标列表的一个子集。水平分片很少使用,因为抓取任务通常不会大到超出单个 Prometheus 实例的处理能力。

此外,我们并未考虑为每个数据中心/环境配置一个 Prometheus 实例作为分片;Prometheus 应该与其监控的系统/服务一起运行,以便减少带宽和延迟问题,同时提高系统的弹性(更不容易受到网络故障的影响)。

作业的逻辑分组

当单个 Prometheus 实例无法满足扩展需求时,一个好的起点是将抓取任务分为逻辑组,并将这些组分配到不同的 Prometheus 实例。这就是垂直分片。可以根据任何对你有意义的方式来分组:按架构/范围(前端、后端、数据库)、按层次(操作系统级指标、基础设施服务、应用程序)、按内部组织结构、按团队、按网络安全边界(以避免抓取跨越防火墙),甚至按应用集群分组。

以下图示例展示了这种垂直分片的实现方式:

图 13.2:展示垂直分片的示意图

这种分片方式还可以实现 Prometheus 实例之间的隔离,意味着从一组目标中提取的高频使用的部分指标可以被拆分到多个实例中,可能会有更多的资源。这样,任何由于高负载使用导致的负面副作用都能被限定在一定范围内,不会影响整体监控平台的稳定性。

此外,为每个团队执行垂直分片可以带来一些组织上的好处:

  • 它使服务拥有者能更清楚地看到基数(cardinality)问题。

  • 团队可以感到既有权力也有责任监控自己的服务。

  • 它可以在不影响其他人的情况下,促进规则和仪表盘的更多实验。

单一作业规模问题

我们已经探讨了几种垂直分片 Prometheus 的策略,但还有一个问题我们尚未解决:与单一作业相关的扩展需求。假设你有一个作业,其中包含成千上万的抓取目标,且这些目标都位于同一个数据中心,并且没有合适的方式再进行进一步拆分。在这种情况下,最好的选择是进行水平分片,将相同的作业分布到多个 Prometheus 服务器上。以下示意图展示了这种类型的分片示例:

图 13.3:展示水平分片的示意图

为了实现这一点,我们必须依赖 hashmod 重标记操作。hashmod 的工作方式是通过将 target_label 设置为 source_labels 连接后哈希值的 modulus,然后将其放置在 Prometheus 服务器中。我们可以在我们的测试环境中看到这个配置在 shard01shard02 中的实际应用,有效地将节点作业进行了分片。让我们查看以下配置,它可以在 /etc/prometheus/prometheus.yml 中找到:

...
scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['shard01:9100', 'shard02:9100', 'global:9100']
    relabel_configs:
      - source_labels: [__address__]
        modulus: 2 # Because we're using 2 shards
        target_label: __tmp_shard
        action: hashmod
      - source_labels: [__tmp_shard]
        regex: 0 # Starts at 0, so this is the first
       action: keep
...

使用临时标签时,像前面例子中那样,始终使用 __tmp 前缀,因为该前缀保证不会被 Prometheus 内部使用。

在以下截图中,我们可以看到 shard01shard02 Prometheus 实例并排显示的 /service-discovery 端点。hashmod 操作的结果使我们能够将节点导出器作业分配到两个实例,如图所示:

图 13.4:shard01shard02 /service-discovery 端点展示 hashmod 结果

很少有人达到需要这种类型分片的规模,但很高兴知道 Prometheus 原生支持这一功能。

进行分片时需要考虑的事项

无论是垂直分片还是水平分片,分片策略在某些情况下是必需的,但不应轻视。管理多个 Prometheus 实例的多个配置的复杂性会迅速增加,如果没有相应地规划所需的自动化,这将使你的工作更加困难。诸如外部标签、外部 URL、用户控制台和抓取作业等内容可以通过编程设置,以减少维护分片的团队的运维工作。

拥有全球视角也会成为一个问题,因为每个 Prometheus 实例都会有自己的数据子集。这可能会使得仪表板的制作变得更加困难,因为数据的位置可能不会立即清晰(例如,当在 Grafana 中使用多个数据源时),还可能阻止一些查询聚合跨多个分片的服务。这个问题可以通过几种技术来缓解,我们将在本章稍后进行探讨。

最后,一些录制和告警规则可能变得不切实际,如果所需的时间序列不位于同一个分片中。例如,假设我们有一个包含操作系统级别指标的分片和另一个包含应用程序指标的分片。需要关联两个分片的指标的告警将会成为问题。这个问题可以通过仔细规划每个分片包含的内容来缓解,或者通过使用联邦使所需的指标在需要的地方可用,或者通过使用远程写入将数据写入外部系统,这些系统可以在 Prometheus 之外执行此操作(正如我们将在第十四章中看到的,将长时存储与 Prometheus 集成)。

分片的替代方案

如本章开头所述,单个 Prometheus 实例如果配置和使用得当,可以带来很大的帮助。避免高基数(high cardinality)指标应当是首要关注点,并且要尽量减少启动分片的需求。一种有助于保护 Prometheus 实例不被抓取产生不合理数量指标的目标的方法是定义每次抓取作业的最大样本摄取量。为此,你只需要在抓取作业中添加 sample_limit;如果在 metric_relabel_configs 后的样本数量超过配置的限制,抓取将会完全丢弃。以下是一个配置示例:

scrape_configs:
  - job_name: 'massive_job'
    sample_limit: 5000
    static_configs:
      - targets:
        - 'shard01:9999'
        - 'shard02:9999'

下图展示了当抓取达到配置的 sample_limit 时会发生什么:

图 13.5:Prometheus 目标端点显示由于超过样本限制而丢弃的抓取作业

在使用这个限制器时,你应该留意通过 prometheus_target_scrapes_exceeded_sample_limit_total 指标与 up 指标一起监控抓取是否被丢弃。后者告诉你 Prometheus 无法抓取目标,而前者则会告诉你原因。

如果丢弃抓取是不可接受的,并且你能够接受分辨率的损失,另一种选择是增加抓取间隔。请记住,你不应该将其增加到超过两分钟,因为这样做会增加由于单次抓取失败而导致指标过时的风险,正如我们在第五章中所解释的,运行 Prometheus 服务器

使用联邦(federation)实现全球视角

当你有多个 Prometheus 服务器时,确定查询哪个服务器来获取特定的指标可能会变得相当繁琐。另一个很快出现的问题是如何从多个实例中汇总数据,可能还分布在多个数据中心。这时,联邦机制就派上了用场。联邦机制允许你拥有一个 Prometheus 实例,从其他实例中抓取选定的时间序列,实际上充当一个更高级别的汇总实例。这可以以层级的方式进行,每一层将下级实例的指标聚合成更大范围的时间序列,或者以跨服务的模式进行,在同一级别的实例中选择一些指标进行联邦,以便某些记录和告警规则成为可能。例如,你可以收集每个分片的服务吞吐量或延迟数据,然后跨所有分片进行聚合,得到一个全局值。

让我们来看看设置 Prometheus 联邦所需的内容,然后再深入讨论前述的每种联邦模式。

联邦配置

一个运行中的 Prometheus 服务器会暴露一个特别的端点 /federate。这个端点允许检索与一个或多个瞬时向量选择器匹配的时间序列(我们在第七章中讨论过,Prometheus 查询语言 – PromQL)。为了让这些机制更清晰,我们在测试环境中提供了一个非常简单的示例。每个分片实例都有一个记录规则,生成表示 HTTP 请求次数的汇总指标,示例如下代码块:

vagrant@shard01:~$ cat /etc/prometheus/rules.yml
groups:
  - name: recording_rules
    rules:
      - record: job:promhttp_metric_handler_requests:sum_rate1m
        expr: sum by (job) (rate(promhttp_metric_handler_requests_total[1m]))

为了提供全球视图,我们可以在测试环境中使用 global 实例,抓取两个分片的联邦端点,只请求那些汇总指标(所有以 job: 开头的指标),如下片段所示:

vagrant@global:~$ cat /etc/prometheus/prometheus.yml 
...
scrape_configs:
  - job_name: shards
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        - '{__name__=~"job:.+"}'
    static_configs:
      - targets:
        - shard01:9090
        - shard02:9090
...

在这个片段中有几点需要注意。联邦使用与常规抓取任务相同的机制,但需要配置一个不同的抓取端点,以及一个 HTTP 参数来指定我们想要收集哪些指标。此外,设置 honor_labels: true 会确保所有原始标签值保持不变,永远不会被覆盖;这一点需要配置,否则 Prometheus 会将诸如 instancejob 等标签设置为抓取任务的值。

你可以在我们的测试环境中,通过访问 http://192.168.42.12:9090/targets 端点,检查聚合 Prometheus 实例中联邦目标的状态,如下所示:

图 13.6:Prometheus targets 端点,显示联邦服务器

我们还可以在全球 Prometheus Web 界面上测试度量指标的联邦:即使该实例没有抓取任何导出器,也没有录制规则,我们仍然可以从job:promhttp_metric_handler_requests:sum_rate1m指标中获取每个时间序列,这些指标最初是在每个分片实例中生成的。请注意,返回的job标签来自原始的作业,而不是联邦抓取作业。此外,我们可以看到我们在实例中配置为外部标签的shard标签也出现在这里;在external_labels部分定义的标签会自动添加到从/federate端点返回的指标中,具体如下:

图 13.7:全局 Prometheus 视图上的聚合指标

现在我们了解了联邦的工作原理,可以继续探讨联邦的常见模式和最佳实践。

联邦模式

在开始实现 Prometheus 联邦时,首先要注意的是,联邦指标集应该是预先聚合或手动选择的。试图通过联邦将大量数据或甚至每个指标从一个 Prometheus 实例导入到另一个实例通常是一个坏主意,原因有几个:

  • 由于每次抓取时收集的数据量庞大,这将对生成指标的实例和消费指标的实例都产生负面性能影响。

  • 由于抓取数据的摄取是原子性的,但不是隔离的,因此目标 Prometheus 实例可能会因为竞争条件而呈现出其时间序列的一个不完整快照:它可能正处于处理抓取作业的过程中,并会返回当时已处理的内容。这对于多系列的指标类型(如直方图和摘要)尤其相关。

  • 更大的抓取更容易受到超时的影响,这将导致在进行联邦抓取的 Prometheus 实例中出现数据缺口。

  • 试图将一个 Prometheus 实例的所有时间序列导入到另一个实例中,实际上违背了联邦的意义。如果问题只是代理指标,实际的代理服务器可能会是更好的选择。不过,最佳实践仍然是在你要监控的对象附近运行 Prometheus。

Prometheus 时间序列联邦的实现有两种主要方式:层次化和跨服务。如我们所见,这两种模式是互补的,可以一起使用。

层次化

拥有多个分片,甚至仅仅是多个数据中心,意味着时间序列数据现在分布在不同的 Prometheus 实例中。分层联合的目标是通过一个或多个 Prometheus 服务器收集来自其他 Prometheus 实例的高层次聚合时间序列来解决这个问题。你可以拥有超过两级的联合,尽管这需要较大的规模。这使得更高层次的 Prometheus 能够更广泛地查看基础设施及其应用程序。然而,由于只有聚合的度量数据应被联合,那些具有更多上下文和细节的度量数据仍然会保留在较低层次。下图展示了这一工作的方式:

图 13.8:分层联合示例图

例如,你可能希望查看跨多个数据中心的延迟。为满足这一需求,三层 Prometheus 层级结构可能如下所示:

  • 一些垂直分片抓取多个作业

  • 一个数据中心实例抓取这些分片,聚合在作业级别的时间序列(__name__=~"job:.+"

  • 一个全局 Prometheus,它抓取数据中心实例的时间序列,这些时间序列是在数据中心级别聚合的(__name__=~"dc:.+"

当使用具有这种布局的监控平台时,通常从最高层开始,然后逐步深入到下一级。这可以通过 Grafana 很容易地实现,因为你可以将联合层级中的每一层配置为数据源。

告警规则应尽可能在它们使用的时间序列的源头附近执行,因为每个需要穿越的联合层都会在告警的关键路径中引入新的故障点。这一点在跨数据中心进行聚合时尤其如此,因为抓取可能需要通过不太可靠的网络,甚至是通过互联网连接。

跨服务

当你需要从另一个 Prometheus 实例本地获取一些特定的时间序列,用于记录或告警规则时,这种类型的联合非常有用。回到之前的示例场景,其中有一个 Prometheus 实例负责抓取 Node Exporters,另一个实例用于应用程序,这种模式可以让你联合特定的操作系统级别的度量数据,然后在应用程序的告警中使用,如下图所示:

图 13.9:跨服务联合示例图

跨服务联合的配置与之前的模式基本相同,但在这种情况下,抓取的 Prometheus 位于同一逻辑层级,所使用的选择器应匹配特定的度量数据,而不是聚合数据。

在接下来的章节中,我们将介绍一个在 Prometheus 社区中逐渐受到关注的新组件,它通过一种新颖有趣的方式解决了全局视图的问题。

使用 Thanos 来缓解 Prometheus 在大规模时的不足

当你开始扩展 Prometheus 时,你很快会遇到跨分片可见性的问题。事实上,Grafana 可以提供帮助,因为你可以在同一个仪表板面板中添加多个数据源,但这变得越来越难以维护,尤其是当多个团队有不同的需求时。在没有明确界定边界的情况下,追踪哪个分片包含哪个指标可能并不简单——当每个团队只关心自己的指标时,如果每个团队都有一个分片,这可能不是问题,但当多个分片由同一个团队维护并作为服务向组织公开时,就会出现问题。

此外,通常的做法是运行两个相同的 Prometheus 实例,以防止告警路径中的单点故障(SPOF)——这种做法被称为 HA(高可用性)对。这进一步复杂化了仪表板的使用,因为每个实例会有略微不同的数据(尤其是在计量指标方面),并且负载均衡器分配查询时,会导致仪表板数据刷新时出现不一致的结果。

幸运的是,一个项目已经启动,旨在解决这个具体问题——这个项目被称为 Thanos。它在 Improbable I/O 开发,并由 Fabian Reinartz(Prometheus 2.x 中新存储引擎/时间序列数据库的作者)合作开发。

Prometheus 有一个明确的作用范围;例如,它不是为集群设计的,到目前为止所做的所有决策始终以可靠性和性能为首要目标。这些设计选择是 Prometheus 成功的基石之一,使它能够从处理少量目标的简单部署扩展到每秒处理百万级采样的大型实例,但这些选择几乎总是伴随着权衡。虽然 Prometheus 确实提供了一些变通方法来实现功能而不依赖共享状态(如我们之前看到的通过联合),但这样做也有限制,比如必须选择要联合的指标。在这些情况下,出现了一些创意解决方案,试图克服这些限制。Thanos 就是一个优雅的例子,正如我们稍后会看到的那样。

我们将在第十四章中讨论更多 Thanos 的功能,将长期存储与 Prometheus 集成,但目前我们将重点关注这个项目的全局查询方面。

Thanos 的全局视图组件

要使用 Thanos 实现全局视图,我们必须首先了解一些其组件。像 Prometheus 一样,Thanos 是用 Go 编写的,并且提供一个单一的二进制文件(针对每个平台/架构),其行为会根据执行时提供的子命令不同而有所不同。在我们的案例中,我们将扩展讨论 sidecar 和 Query 组件。

你可以在github.com/improbable-eng/thanos上找到 Thanos 的所有源代码和安装文件。

简而言之,sidecar 使 Prometheus 实例中的数据对其他 Thanos 组件可用,而 Query 组件则是一个 API 兼容的 Prometheus 替代品,将它收到的查询分发给其他 Thanos 组件,如 sidecar 或其他 Query 实例。从概念上讲,Thanos 所采用的全局视图方法类似于以下示意图:

图 13.10:带有 Thanos 示例图的全局视图

可以返回查询结果的 Thanos 组件实现了所谓的 store API。当请求命中 Thanos 查询器时,它将分发到为其配置的所有存储 API 节点,在我们的示例中就是使各自 Prometheus 服务器中的时间序列可用的 Thanos sidecars。查询器将整理这些响应(能够聚合分散的数据或去重数据),然后对数据集执行所需的 PromQL 查询。去重功能在使用 Prometheus 服务器对提高可用性时特别有用。

现在,让我们深入了解这些组件,深入研究它们如何工作以及如何设置它们。

Sidecar

sidecar 组件旨在与 Prometheus 一起本地部署,并通过其远程读取 API 与 Prometheus 连接。Prometheus 的远程读取 API 允许与其他系统集成,以便它们可以像查询本地可用的样本一样访问样本。这显然在查询路径中引入了网络,可能会导致带宽相关问题。sidecar 利用这一点将 Prometheus 中的数据提供给其他 Thanos 组件。它将存储 API 作为 gRPC 端点暴露(默认绑定到端口 10901),然后 Thanos 查询组件将使用这个端点,从查询者的角度来看,sidecar 变成了一个数据存储。sidecar 还在端口 10902 上暴露了一个 HTTP 端点,并提供了一个用于 /metrics 的处理程序,以便您可以在 Prometheus 中收集其内部指标。

sidecar 附加到的 Prometheus 实例必须设置 external_labels,以便每个实例都有唯一标识。这对 Thanos 过滤出要查询的存储 API 和去重功能至关重要。

不幸的是,具有唯一外部标签会在使用一对 Prometheus 实例以提高可用性时破坏 Alertmanager 的去重功能。您应在 alerting 部分使用 alert_relabel_configs 来删除每个 Prometheus 实例特有的标签。

在我们的测试环境中,我们可以发现每个可用的分片中都有一个 Thanos sidecar 正在运行。为了快速验证正在使用的配置,我们可以在任何一个分片实例中运行以下指令:

vagrant@shard01:~$ systemctl cat thanos-sidecar
...
ExecStart=/usr/bin/thanos sidecar \
           --prometheus.url "http://localhost:9090"
...

上面的代码片段表明,sidecar 正在连接到本地的 Prometheus 实例。sidecar 提供了更多的功能,正如我们将在下一章中看到的那样,但为了实现全局视图,这个配置已经足够。

Query

查询组件实现了 Prometheus HTTP API,使得 PromQL 表达式可以在所有配置的 Thanos 存储 API 上运行。它还包括一个查询的 web 界面,基于 Prometheus 自己的 UI,并做了一些小的改动,让用户感觉更加熟悉。该组件是完全无状态的,可以水平扩展。由于兼容 Prometheus API,它可以直接作为 Prometheus 类型的数据源在 Grafana 中使用,实现对 Prometheus 查询的无缝替代。

这个 Thanos 组件也在我们的测试环境中运行,特别是在 global 实例中,其配置可以通过运行以下指令查看:

vagrant@global:~$ systemctl cat thanos-query
...
ExecStart=/usr/bin/thanos query \
            --query.replica-label "shard" \
            --store "shard01:10901" \
            --store "shard02:10901"
...

如前面的代码片段所示,我们需要指定所有包含存储 API 的组件,才能通过查询器提供它们。由于我们大多数使用的是该组件的默认值,web 界面可在端口 10902 上访问,我们可以通过将浏览器指向 http://192.168.42.12:10902/stores 来验证,正如以下截图所示:

图 13.11: Thanos 查询 web 界面显示 /stores 端点

查询器的 HTTP 端口还提供 Prometheus 指标收集的 /metrics 端点。

--query.replica-label 标志允许使用特定的 Prometheus 外部标签进行指标去重。例如,我们在 shard01shard02 上有完全相同的 icmp 作业,并且这两个作业都有一个 shard 外部标签来唯一标识它们。如果没有去重,我们在进行查询时将看到每个指标有两个结果,因为这两个 sidecar 都有相关数据。通过将 shard 标记为标识副本的标签,查询器可以选择其中一个结果。我们可以通过应用程序编程接口(在 GET 参数中发送 dedup=true)或 web 界面(选择 去重 选项)来切换去重功能,这取决于我们是否希望包括所有存储 API 的指标,还是只获取单一结果,仿佛只有一个 Prometheus 实例拥有该数据。以下截图展示了这一差异:

图 13.12: Thanos 查询去重功能禁用和启用

默认启用去重功能,使得查询器可以无缝地替代 Prometheus 进行查询服务。这样,上游系统,如 Grafana,可以继续正常工作,而不会意识到查询层已经发生变化。

概述

在本章中,我们解决了在大规模环境中运行 Prometheus 时的问题。尽管单个 Prometheus 实例可以让你走得很远,但如果有需要,具备扩展知识是一个好主意。我们学习了垂直和水平切片的工作原理,何时使用切片,以及切片带来的好处和问题。我们还了解了在联邦 Prometheus 时常见的模式(层次型或跨服务型),以及如何根据我们的需求选择它们。由于有时我们需要的不仅仅是开箱即用的联邦功能,我们还介绍了 Thanos 项目及其如何解决全局视图问题。

在下一章中,我们将探讨另一个常见需求,这个需求并不是 Prometheus 项目的核心问题,那就是时间序列的长期存储。

问题

  1. 什么时候应该考虑切片 Prometheus?

  2. 垂直切片和水平切片有什么区别?

  3. 在选择切片策略之前,你可以做些什么?

  4. 哪种类型的指标最适合以层次型模式进行联邦?

  5. 为什么你可能需要跨服务联邦?

  6. Thanos 查询器与 sidecar 之间使用什么协议?

  7. 如果 Thanos 查询器中没有设置副本标签,而该查询器配置了与 Prometheus HA 对一起运行的 sidecar,那么执行的查询结果会发生什么?

进一步阅读

第十四章:将长期存储与 Prometheus 集成

Prometheus 的单实例设计使其不适合维护大量历史数据集,因为它受限于本地可用的存储空间。拥有跨越较长时间段的时间序列可以进行季节性趋势分析和容量规划,因此,当数据集无法适应本地存储时,Prometheus 通过将数据推送到第三方集群存储系统来解决这一问题。在本章中,我们将探讨远程读取和写入 API,以及如何借助 Thanos 将指标发送到对象存储。这将提供多种处理这一需求的选项,支持多种架构选择。

简而言之,本章将涵盖以下主题:

  • 本章的测试环境

  • 远程写入与远程读取

  • 指标存储选项

  • Thanos 远程存储与生态系统

本章的测试环境

本章将重点讨论集群存储。为此,我们将部署三个实例,模拟一个 Prometheus 生成指标的场景,然后我们将讨论如何将它们存储到对象存储解决方案中。这种方法不仅能帮助我们探索所需的配置,还能了解各部分如何协同工作。

我们将使用的设置类似于以下图示:

图 14.1:本章的测试环境

在下一节中,我们将解释如何启动和运行测试环境。

部署

要启动一个新的测试环境,请进入以下路径,相对于仓库根目录,如下所示:

cd ./chapter14/

确保没有其他测试环境正在运行,并启动本章的环境,展示如下:

vagrant global-status
vagrant up

你可以使用以下命令验证测试环境是否成功部署:

vagrant status

这将输出以下内容:

Current machine states:

prometheus                running (virtualbox)
storage                   running (virtualbox)
thanos                    running (virtualbox)

This environment represents multiple VMs. The VMs are all listed above with their current state. For more information about a specific VM, run `vagrant status NAME`.

部署任务完成后,你将能够通过你最喜欢的支持 JavaScript 的网页浏览器,在你的主机上验证以下端点:

服务 端点
Prometheus http://192.168.42.10:9090
Thanos sidecar http://192.168.42.10:10902
对象存储访问密钥: strongACCESSkey``密钥: strongSECRETkey http://192.168.42.11:9000
Thanos 查询器 http://192.168.42.12:10902

你应该能够使用以下命令之一访问所需的实例:

实例 命令
Prometheus vagrant ssh prometheus
存储 vagrant ssh storage
Thanos vagrant ssh thanos

清理

测试完成后,确保你在./chapter14/目录内并执行以下命令:

vagrant destroy -f

不用太担心;如果需要的话,你可以轻松地重新启动环境。

远程写入与远程读取

远程写入和远程读取分别允许 Prometheus 推送和拉取样本:远程写入通常用于实现远程存储策略,而远程读取则允许 PromQL 查询透明地针对远程数据。在接下来的主题中,我们将详细介绍这些功能,并展示它们可以应用的一些示例。

远程写入

远程写入是 Prometheus 非常需要的一个功能。它最初作为对 openTSDB、InfluxDB 和 Graphite 数据格式的样本发送的原生支持实现。然而,很快就做出了决定,不再支持每一个可能的远程系统,而是提供一个通用的写入机制,适用于构建自定义适配器。这使得与 Prometheus 开发路线图解耦的自定义集成成为可能,同时也为这些桥接中支持读取路径打开了可能性。远程写入的系统特定实现从 Prometheus 二进制文件中移除,并作为示例转化为独立的适配器。依赖适配器并赋能社区以构建所需的任何集成的逻辑,遵循了我们在第十二章中讨论的哲学——选择合适的服务发现,用于构建自定义服务发现集成。

自定义远程存储适配器的官方示例可以在github.com/prometheus/prometheus/tree/master/documentation/examples/remote_storage/remote_storage_adapter找到。

Prometheus 将单个样本发送到远程写入端点,使用一种非常简单的格式,这种格式与 Prometheus 的内部实现无关。另一端的系统可能甚至不是一个存储系统,而是一个流处理器,如 Kafka 或 Riemann。当定义远程写入设计时,这是一个艰难的决定,因为 Prometheus 已经知道如何创建高效的块并可以直接将其发送。块会使支持流系统变得不切实际,而发送样本在适配器方面既更易于理解,也更易于实现。

远程写操作在 Prometheus 2.8 发布时得到了重大增强。此前,当指标未能成功传送到远程写入端点(由于网络或服务问题)时,只有一个小缓冲区用于存储数据。如果缓冲区被填满,指标将被丢弃,并且永久丢失在那些远程系统中。更糟糕的是,缓冲区可能会造成背压,导致 Prometheus 服务器由于 内存溢出 (OOM) 错误而崩溃。自从远程写 API 开始依赖 预写日志 (WAL) 来进行账务处理后,这种情况不再发生。远程写操作现在直接从 WAL 中读取数据,WAL 包含所有正在进行的事务和抓取的样本。在远程写子系统中使用 WAL 使 Prometheus 的内存使用更加可预测,并允许它在与远程系统的连接中断后从中断点继续。

配置方面,以下代码片段展示了在 Prometheus 中设置远程写入端点所需的最小代码:

remote_write:
  - url: http://example.com:8000/write

由于远程写操作是与外部系统交互的另一种形式,external_labels 也会在发送数据之前应用于样本。这也可以防止在使用多个 Prometheus 服务器将数据推送到相同位置时,远端发生指标冲突。远程写操作还支持 write_relabel_configs,允许您控制哪些指标被发送,哪些被丢弃。此重新标签化操作在应用外部标签之后进行。

本章稍后我们将讨论一个相对较新(且实验性的)Thanos 组件,称为 receiver,它作为远程写操作的实际应用示例。

远程读取

在远程写功能推出后,远程读取的请求开始增加。试想一下,将 Prometheus 数据发送到远程端点,然后不得不学习一种新的查询语言,如 InfluxQL(InfluxDB 查询语言),以访问上述数据。这个功能的引入使得可以透明地使用 PromQL 来查询存储在 Prometheus 服务器外部的数据,就像它是本地可用的那样。

针对远程数据执行的查询是集中评估的。这意味着远程端点只会发送请求的匹配器和时间范围的数据,PromQL 会在发起查询的 Prometheus 实例中应用。再次强调,选择集中式(而非分布式)查询评估是设计 API 时的一个关键决策。分布式评估本可以分担每个查询的负载,但会迫使远程系统理解和评估 PromQL,并处理数据非互斥时的各种边界情况,极大地增加了前述系统的实现复杂性。集中评估还允许远程系统对请求的数据进行下采样,从而显著提高了处理非常长时间范围查询的效率。

远程读取有一个典型的应用场景,即帮助在 Prometheus 的主要版本之间迁移,比如从 Prometheus v1 迁移到 Prometheus v2。后者可以配置为从前者进行远程读取,从而使旧实例成为只读(不配置抓取任务)。这将 v1 实例作为一种强化的远程存储,直到它的指标不再有用。实施这一策略时,一个常见的陷阱是,配置了远程读取的 Prometheus 实例的 external_labels 需要与被读取的 Prometheus 实例的 external_labels 匹配。

另一方面,在上一章(第十三章,扩展与联邦化 Prometheus)中,已经展示了 Prometheus 自身的远程读取端点示例:Thanos 侧车使用本地 Prometheus 实例的远程读取 API 来获取 Thanos 查询器请求的时间序列数据。

在配置方面,设置 Prometheus 进行远程读取非常简单。以下代码段展示了配置文件中需要的部分:

remote_read:
  - url: http://example.com:8000/read

remote_read 部分还允许你使用 required_matchers 指定一个匹配器列表,这些匹配器需要出现在选择器中,以查询给定的端点。这对于不是存储的远程系统或仅将部分指标写入远程存储时非常有用,因此需要限制远程读取到这些指标。

指标存储的选项

默认情况下,Prometheus 很好地管理着本地存储的指标,使用它自己的 TSDB。但也有一些情况是它无法满足的:本地存储受限于 Prometheus 实例的本地磁盘空间,对于较长的保存周期(如多年)以及超出实例附加磁盘空间容量的大量数据来说,这并不理想。在接下来的部分中,我们将讨论本地存储方法,以及当前可用的远程存储选项。

本地存储

Prometheus 的开箱即用时间序列数据存储解决方案就是本地存储。它更易于理解和管理:数据库存在于一个单独的目录中,便于备份、恢复或在需要时销毁。通过避免集群化,Prometheus 确保在面临网络分区时仍能保持正常行为;你不希望在最需要的时候,监控系统突然崩溃。高可用性通常通过简单地运行两个配置相同、各自拥有独立数据库的 Prometheus 实例来实现。然而,这种存储解决方案并不能涵盖所有使用场景,且存在一些不足之处:

  • 它不具备持久性——在容器编排部署中,如果没有使用持久化卷,收集的数据会在容器重新调度时消失(因为之前的数据被销毁,当前数据会重新开始),而在虚拟机部署中,数据的持久性与本地磁盘一样。

  • 它不能水平扩展——使用本地存储意味着你的数据集只能大到你为实例提供的磁盘空间。

  • 它并不是为了长期存储设计的,尽管在适当的度量标准和基数控制下,商品存储能够走得很远。

这些不足之处是为了确保小型和中型部署(这些部署是最常见的使用场景)能够良好运行,同时也能使得高级和大规模的使用场景成为可能,而做出的折中。告警和仪表盘,在日常操作监控或排查正在进行的事件时,只需要最多几周的数据。

在全力投入到一个远程度量存储系统用于长期存储之前,我们可以考虑通过使用 TSDB 管理 API 端点来管理本地存储,也就是 snapshot 和 delete_series。这些端点有助于保持本地存储的可控性。正如我们在第五章《运行 Prometheus 服务器》中提到的,TSDB 管理 API 默认情况下不可用;Prometheus 需要启动时带上 --web.enable-admin-api 标志才能启用该 API。

在本章的测试环境中,你可以尝试使用这些端点,并评估它们的目标。通过连接到 prometheus 实例,我们可以验证 TSDB 管理 API 是否已经启用,并使用以下命令查找本地存储路径:

vagrant@prometheus:~$ systemctl cat prometheus
...
    --storage.tsdb.path=/var/lib/prometheus/data \
    --web.enable-admin-api \
...

向 /api/v1/admin/tsdb/snapshot 端点发出 HTTP POST 请求将触发一个新的快照,快照会将可用的块存储在一个快照目录中。快照是使用硬链接创建的,这使得它们在 Prometheus 仍然保留这些块的情况下非常节省空间。以下说明展示了所有过程是如何处理的:

vagrant@prometheus:~$ curl -X POST http://localhost:9090/api/v1/admin/tsdb/snapshot
{"status":"success","data":{"name":"20190501T155805Z-55d3ca981623fa5b"}}

vagrant@prometheus:~$ ls /var/lib/prometheus/data/snapshots/
20190501T155805Z-55d3ca981623fa5b

然后,你可以备份快照目录,当需要查询历史数据时,可以通过 --storage.tsdb.path 将其用作另一个 Prometheus 实例的 TSDB 存储路径。请注意,可能需要根据你的数据存储时间调整 --storage.tsdb.retention.time,因为 Prometheus 可能会开始删除超出保留期的块。

当然,这并不会阻止 TSDB 的增长。为了管理这一方面,我们可以使用/api/v1/admin/tsdb/delete_series端点,这对每周甚至每日的维护都很有用。它通过 HTTP POST 请求操作,并携带一组匹配选择器,标记所有符合条件的时间序列以供删除,如果发送了时间范围,还可以选择将删除限制在给定的时间窗口内。下表提供了相关 URL 参数的概述:

URL 参数 描述
match[]=<selector> 一个或多个匹配选择器,例如match[]={__name__=~"go_.*"}(删除所有名称以go_开头的度量)
start=<unix_timestamp> 删除的开始时间,采用 RFC 3339 或 Unix 格式(可选,默认值为最早时间)
end=<unix_timestamp> 删除的结束时间,采用 RFC 3339 或 Unix 格式(可选,默认值为最新时间)

在执行POST请求后,会返回 HTTP 204。这并不会立即释放磁盘空间,因为它需要等到下一个 Prometheus 压缩事件。你可以通过请求clean_tombstones端点强制进行清理,以下是相关的操作示例:

vagrant@prometheus:~$ curl -X POST -w "%{http_code}\n" --globoff 'http://localhost:9090/api/v1/admin/tsdb/delete_series?match[]={__name__=~"go_.*"}'
204

vagrant@prometheus:~$ curl -X POST -w "%{http_code}\n" http://localhost:9090/api/v1/admin/tsdb/clean_tombstones
204

这些知识可能帮助你控制本地存储,避免在关注点主要是可扩展性时进入复杂且耗时的替代方案。

远程存储集成

选择远程度量存储不应轻视,因为它有多个影响因素。选择远程存储解决方案时需要考虑的一些因素如下:

  • 成熟度:一些存储解决方案比其他的更成熟且维护得更好。

  • 控制:有些解决方案是由你自己运行实例的,而另一些则是 SaaS 服务。

  • 可用性、可靠性与可扩展性:如果选择内部管理存储解决方案,你需要考虑这些方面。

  • 可维护性:一些选项的部署和/或维护非常复杂。

  • 远程读写:你是否真的需要同时支持读写,还是仅写操作足以满足你的用例?

  • 成本:这一切可能归结于此;我们定义成本不仅仅是指金钱,还包括学习、测试和操作解决方案所需的时间。

另一个关键因素与告警相关。为了确保可靠性,规则应该只查询本地数据;这样可以防止网络层的瞬时故障对规则评估产生负面影响。因此,远程存储系统中的数据不应用于关键告警,至少你应当能够容忍它们的缺失。

如果你所处理的规模要求可扩展的存储,或者历史数据对你的用例至关重要,例如用于容量规划,那么有一些可用的选项。官方 Prometheus 文档提供了一个详尽的已知远程存储集成列表,网址为prometheus.io/docs/operating/integrations/#remote-endpoints-and-storage。这个列表涵盖了多种不同的用例:SaaS 服务(如 SignalFX 和 Splunk);流处理系统(Kafka);不同的时序数据库——包括付费(IRONdb)和开源(InfluxDB、Cortex、TimescaleDB、M3DB 等);其他监控系统(OpenTSDB、Graphite);甚至是通用数据存储(如 Elasticsearch 和 TiKV)。其中很多支持远程读写功能。有些甚至值得单独写一本书来介绍。

有趣的是,我们将深入探讨的解决方案在写作时并不在上述列表中,因为它采用了与我们处理问题的方式完全不同的策略。事实上,Prometheus 根本不需要了解它,因为它像一个覆盖层一样工作。我们将专注于最有前景的长期存储解决方案,它在复杂性、成本和功能集之间达到了优美的平衡:Thanos。

Thanos 远程存储和生态系统

在第十三章《扩展和联邦 Prometheus》中,我们介绍了 Thanos,这是一个开源项目,旨在改进 Prometheus 在大规模使用中的一些不足之处。具体来说,我们探讨了 Thanos 如何使用 Thanos sidecar 和 querier 组件解决多个 Prometheus 实例的全局视图问题。现在是时候了解其他 Thanos 组件,并探索它们如何协同工作,以通过对象存储实现廉价的长期数据保留。请记住,在走这条路径时,复杂性会增加,因此请验证你的需求,并考虑全局视图方法和本地存储是否足以满足你的特定用例。

Thanos 生态系统

除了我们之前介绍的 Thanos querier 和 sidecar,Thanos 生态系统中还有其他一些组件。所有这些组件共存在同一个二进制文件中,并通过调用不同的子命令运行,稍后我们会列举它们:

  • query:通常被称为 querier,它是一个守护进程,负责将查询分发出去并对配置的 StoreAPI 端点返回的结果进行去重。

  • sidecar:一个守护进程,暴露一个 StoreAPI 端点,用于访问本地 Prometheus 实例中的数据,并将前述实例的 TSDB 数据块传输到对象存储。

  • store:一个守护进程,充当远程存储的网关,暴露一个 StoreAPI 端点。

  • compact:通常被称为 compactor,这个守护进程负责压缩对象存储中可用的块,并创建新的下采样时序数据。

  • bucket:一个命令行工具,用于验证、恢复和检查存储在对象存储中的数据。

  • receive:也叫receiver,它是一个守护进程,接受来自 Prometheus 实例的远程写入,通过 StoreAPI 端点暴露推送的数据,并将数据块发送到对象存储。

  • rule:通常称为ruler,它是一个守护进程,用于评估 Prometheus 规则(记录和告警),并通过远程 StoreAPI 端点进行评估,暴露自己的 StoreAPI 以便查询评估结果,将结果发送到对象存储,并连接到 Alertmanager 集群以发送警报。

你可以在github.com/improbable-eng/thanos找到所有 Thanos 的源代码和安装文件。

以下所有组件协同工作,解决了多个挑战:

  • 全局视图:从同一个地方查询所有 Prometheus 实例,同时对返回的时间序列进行聚合和去重。

  • 降采样:如果样本以完整分辨率返回,查询数月甚至数年的数据会成为问题;通过自动创建降采样数据,跨越长时间段的查询变得可行。

  • 规则:支持创建全局警报和记录规则,能够混合来自不同 Prometheus 分片的度量数据。

  • 长期保存:通过利用对象存储,将存储的持久性、可靠性和可扩展性问题委托给监控栈之外的系统。

虽然我们将提供如何使用 Thanos 应对这些挑战的简要介绍,但我们的主要关注点将是 Thanos 的长期存储方面。

从存储角度来看,Thanos 项目选择了对象存储来进行长期数据保存。大多数云服务提供商都提供这一服务,且还额外确保服务水平协议SLA)。对象存储通常在任何顶级云服务商上都有 99.999999999%的持久性和 99.99%的可用性。如果你有自己的本地基础设施,也有一些可用的选择:使用 Swift(提供对象存储 API 的 OpenStack 组件),或者甚至是 MinIO 项目,这是我们在本章测试环境中使用的。大多数这些本地对象存储解决方案都具有相同的特点:它们提供的 API 模仿了广为人知的 AWS S3,因为有许多工具都支持它。此外,云服务提供商的对象存储通常是一个非常具成本效益的解决方案。

以下图表提供了一个简单的概览,展示了使用 Thanos 实现 Prometheus 时间序列数据长期保存所需的核心组件:

图 14.2:Thanos 长期存储架构概览

正如我们在前面的图表中看到的,我们只需要一些 Thanos 组件来应对这个挑战。在接下来的主题中,我们将详细介绍每个组件,并扩展它在整体设计中的作用。

Thanos 组件

现在我们已经看到 Thanos 长期存储架构的概述,是时候了解所有可用的 Thanos 组件了。除了介绍每个组件之外,我们还将强调那些在测试环境中可用的组件,详细说明它们在生态系统中的作用及其配置。

你可以在github.com/improbable-eng/thanos/tree/master/examples找到一些社区驱动的警报和仪表盘。

测试环境细节

正如本书开头所述,使用提供的测试环境不会产生任何费用。因此,由于我们需要一个对象存储桶来进行示例,我们依赖于一个名为 MinIO 的项目,它提供了一个兼容 S3 的 API;你可以在min.io/找到更多关于它的信息。在配置方面,存储端点应该在存储实例中以以下设置可用:

vagrant@storage:~$ systemctl cat minio
...
EnvironmentFile=/etc/default/minio
ExecStart=/usr/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
...

上述的systemd单元文件加载了以下环境变量:

vagrant@storage:~$ sudo cat /etc/default/minio 
MINIO_VOLUMES='/var/lib/minio/data'
MINIO_OPTS='--address :9000'
MINIO_ACCESS_KEY='strongACCESSkey'
MINIO_SECRET_KEY='strongSECRETkey'

为了确保你不必等两个小时才能将块数据传输到对象存储,我们的测试环境中的 Prometheus 服务器已设置如下:

vagrant@prometheus:~$ systemctl cat prometheus
...
    --storage.tsdb.max-block-duration=10m \
    --storage.tsdb.min-block-duration=10m
...

此配置更改了块的持续时间,将它们刷写到磁盘的间隔从默认的两个小时更改为十分钟。虽然设置如此低的值对测试此特性非常有用,但对于其他任何情况,这种做法都是完全不建议的。为了让这一点更加明确,除了测试外,没有任何理由将这些值更改为其他任何值,除了两个小时。

了解了本章测试环境的细节后,我们可以继续介绍每个单独的 Thanos 组件。

Thanos 查询

在第十三章,扩展和联邦 Prometheus中,我们有机会探索 Thanos 查询器和 sidecar 来解决全局视图问题。对于这个组件,我们当时介绍的功能将在本章中继续使用。我们将继续使用查询器查询多个 StoreAPI 端点,利用它提供的去重功能,并通过其 Web 界面使用查询 API。

我们在测试环境中可用的配置非常简单,正如我们在以下代码片段中看到的,这来自thanos实例:

vagrant@thanos:~$ systemctl cat thanos-query
...
ExecStart=/usr/bin/thanos query \
            --query.replica-label replica \
            --store "prometheus:10901" \
            --store "thanos:11901" \
            --store "thanos:12901"
...

正如你在之前的代码片段中看到的,我们指定了多个--store端点。为了了解每个端点的具体作用,我们可以将浏览器指向 Thanos 查询器的 Web 界面,地址是192.168.42.12:10902/stores,在界面上查看可用的存储,如下图所示:

图 14.3:Thanos 查询器 /stores 端点

之前的截图展示了我们测试环境中所有可用的存储 API。这些将是你在 Thanos 查询器中执行查询时使用的数据源。

Thanos sidecar

除了之前讨论的 Thanos sidecar 暴露的 Store API,该组件还能够从磁盘收集 TSDB 块并将其传输到对象存储桶中。这使得你可以通过将历史数据保存在持久介质中来减少 Prometheus 服务器的保留数据。为了使用 sidecar 中的块上传功能,--storage.tsdb.min-block-duration--storage.tsdb.max-block-duration 标志需要设置为相同的值(两个小时,以匹配默认行为),以便禁用 Prometheus 的本地压缩。

当前使用的配置文件可以在 prometheus 实例中查看,并可以通过执行以下命令进行检查:

vagrant@prometheus:~$ systemctl cat thanos-sidecar
...
ExecStart=/usr/bin/thanos sidecar \
           --log.level debug \
           --prometheus.url "http://localhost:9090" \
           --tsdb.path "/var/lib/prometheus/data" \
           --objstore.config-file "/etc/thanos/storage.yml"
...

如我们所见,--objstore.config-file 标志加载了所有必需的配置文件,以便将 TSDB 块传输到对象存储桶中,例如桶名称(在我们的案例中是 thanos)、存储端点和访问凭证。以下是该文件的内容:

vagrant@prometheus:~$ sudo cat /etc/thanos/storage.yml
type: S3
config:
  bucket: 'thanos'
  endpoint: 'storage:9000'
  insecure: true
  signature_version2: true
  access_key: 'strongACCESSkey'
  secret_key: 'strongSECRETkey'

在我们的 Prometheus 测试实例中,每 10 分钟会生成一个新的 TSDB 块,Thanos sidecar 会负责将其传输到对象存储端点。我们可以通过使用 MinIO 的 Web 界面来查看存储桶中可用的块,界面地址为 192.168.42.11:9000/minio/thanos/。登录时使用前面代码片段中显示的 access_keysecret_key,你将看到类似以下截图的界面:

图 14.4:MinIO 对象存储 Web 界面

现在我们应该有一些历史数据可供测试。我们需要一种查询这些数据的方式。此时,Thanos 存储网关就发挥作用了。

Thanos 存储网关

Thanos 存储网关的主要功能是通过 StoreAPI 端点提供对历史时间序列数据的访问,这些数据存储在被传输到对象存储的块中。这意味着它实际上充当了一个 API 网关。Thanos 存储中的所有主要对象存储集成(Google Cloud Storage、AWS S3、Azure Storage)都被认为足够稳定,可以在生产环境中运行。它使用一个相对较小的本地缓存来存储所有块的元数据,并将其与存储桶保持同步。

在我们的测试环境中的 Thanos 实例中,可以找到此组件的最小配置。以下是其中的一段代码:

vagrant@thanos:~$ systemctl cat thanos-store
...
ExecStart=/usr/bin/thanos store \
           --data-dir "/var/lib/thanos/store" \
           --objstore.config-file "/etc/thanos/storage.yml" \
           --grpc-address "0.0.0.0:11901" \
           --http-address "0.0.0.0:11902"
...

如我们所见,对象存储配置在其自己的配置文件中进行。与大多数其他组件一样,store 绑定了一个 StoreAPI GRPC 端口用于接收查询,以及一个 HTTP 端口以便其指标能够被 Prometheus 收集。

Thanos 压缩

由于必须关闭 Prometheus 块压缩才能使 Thanos sidecar 上传功能可靠工作,因此这项工作被委托给一个不同的组件:Thanos 压缩。它设计用于采用与 Prometheus 存储引擎相同的压缩策略,但应用于对象存储中的块。由于不能直接在对象存储中进行压缩,因此该组件需要相当多的可用空间(几百 GB,具体取决于远程存储的数据量)来处理这些块。

Thanos 压缩的另一个重要功能是创建下采样样本。下采样的最大优点是可以可靠地查询较大的时间范围,而无需拉取大量数据。当使用下采样数据时,强烈建议使用*_over_time函数(如在第七章,* Prometheus 查询语言 – PromQL* 中讨论的那样)。这是因为用于下采样的方法不仅仅是删除样本,而是使用五种不同的聚合函数对其进行预聚合。这意味着每个原始时间序列会生成五个新的时间序列。需要特别注意的是,完整分辨率数据只有在 40 小时后才会被下采样为五分钟分辨率。同样,使用先前已下采样的五分钟分辨率数据作为来源,经过 10 天后才会生成一小时的下采样数据。保留原始数据可能对缩放到特定时间事件有帮助,而仅仅依靠下采样数据则无法做到这一点。以下是管理数据保留(即保存多长时间)在原始数据、五分钟分辨率和一小时分辨率形式中的三种标志,如下表所示:

标志 持续时间
--retention.resolution-raw 保持数据以原始分辨率在对象存储桶中保存的持续时间,例如 365d(默认为 0d,即永久保存)
--retention.resolution-5m 保持数据在对象存储桶中以 5 分钟分辨率保存的时间,例如 365d(默认为 0d,即永久保存)
--retention.resolution-1h 保持数据在对象存储桶中以 1 小时分辨率保存的时间,例如 365d(默认为 0d,即永久保存)

每个存储桶应该只关联一个 Thanos 压缩器,因为它并不设计为支持并发运行。

在考虑保留策略时,请记住,由于第一步下采样会将五分钟的数据聚合起来,并且该聚合会生成五个新的时间序列,因此您需要拥有低于一分钟的抓取间隔,才能真正节省空间(间隔中的样本数需要大于聚合步骤生成的样本数)。

压缩器可以作为守护进程运行,必要时启动,或者作为单次任务运行,任务完成后退出。在我们的测试环境中,我们在 thanos 实例中运行 Thanos 压缩器来管理我们的对象存储桶。它作为一个服务运行(使用 --wait 标志),使测试环境更简洁。正在使用的配置如下所示:

vagrant@thanos:~$ systemctl cat thanos-compact
...
ExecStart=/usr/bin/thanos compact \
           --data-dir "/var/lib/thanos/compact" \
           --objstore.config-file "/etc/thanos/storage.yml" \
           --http-address "0.0.0.0:13902" \
           --wait \
           --retention.resolution-raw 0d \
           --retention.resolution-5m 0d \
           --retention.resolution-1h 0d
...

和其他组件一样,HTTP 端点对于抓取指标非常有用。正如 retention.* 标志所示,我们将数据保存在所有可用的分辨率中,永久保存。接下来我们将讨论 Thanos 桶,这是一个调试工具,用于检查 Thanos 管理的存储桶。

Thanos 桶

Thanos 生态系统的这个组件负责验证、修复、列出和检查对象存储中的数据块。与其他组件不同,这个组件作为命令行工具运行,而不是守护进程。

你可以尝试以下示例,列出我们对象存储桶中可用的数据块:

vagrant@thanos:~$ sudo thanos bucket ls -o wide --objstore.config-file=/etc/thanos/storage.yml
01D9SN3KEBNCB2MHASYXSDF1DE -- 2019-05-01 12:00 - 2019-05-01 12:10 Diff: 10m0s, Compaction: 1, Downsample: 0, Source: sidecar
01D9SNNXCAXWKZ0EH6118FTHSS -- 2019-05-01 12:10 - 2019-05-01 12:20 Diff: 10m0s, Compaction: 1, Downsample: 0, Source: sidecar
01D9SP87A9NZ9DE35TC2QNS7ZZ -- 2019-05-01 12:20 - 2019-05-01 12:30 Diff: 10m0s, Compaction: 1, Downsample: 0, Source: sidecar
01D9SPTH88G9TR4503C4763TDN -- 2019-05-01 12:30 - 2019-05-01 12:40 Diff: 10m0s, Compaction: 1, Downsample: 0, Source: sidecar
01D9SQCV68KVE7CXK4QDW9RWM1 -- 2019-05-01 12:40 - 2019-05-01 12:50 Diff: 10m0s, Compaction: 1, Downsample: 0, Source: sidecar
01D9SQN6TVJ97NP4K82APW7YH9 -- 2019-05-01 10:51 - 2019-05-01 11:50 Diff: 58m41s, Compaction: 2, Downsample: 0, Source: compactor

这个工具对于故障排查非常有用,能够快速了解存储桶中数据块的状态。

Thanos 接收

这个组件在写作时仍然是非常实验性的。不过,正如本章远程写入和读取部分所承诺的,这个组件是远程写入的一个极好示例,因此我们决定展示它的功能。它作为 Prometheus 远程写入请求的目标,并将接收到的样本存储在本地。接收器还实现了 StoreAPI 端点,因此它具有作为存储节点的能力。最后,它也可以像 sidecar 一样将数据块传输到对象存储中。

为了更好地理解这一切,我们来探讨两种场景。

默认情况下,Prometheus 服务器每两小时生成一个数据块。即使使用 Thanos sidecar 进行数据块传输,Thanos 查询器也无法直接访问仍在 WAL 中的数据,除非 sidecar 通过远程读取 API 向 Prometheus 服务器请求数据。当你达到一定规模时,Grafana 或其他 API 客户端可能会对 Prometheus 产生巨大的请求负载,最终可能影响 Prometheus 的性能,甚至影响告警功能,尽管如我们之前所见,Prometheus 已有一些保护机制。通过使用 Thanos 接收器,你可以将客户端查询简单地转发给它,确保 Prometheus 服务器的主要任务是抓取数据和评估规则。简而言之,你实际上是将 Prometheus 服务器的读取与写入操作分离了。你可以继续使用 Thanos sidecar 仅用于传输数据块,而使用 Thanos 接收器来回答所有 Thanos 查询器的查询,就像一个新的缓存一样,保护 Prometheus 的写入路径。

Thanos 接收器每两小时生成一个数据块,与 Prometheus 不同,这个值是按设计硬编码的。

假设另一个场景,其中有多个租户/团队使用你正在管理的基础设施。如果他们技术足够精通,他们最终会想要管理自己的 Prometheus 服务器。你可以尝试提供所有管理他们服务器所需的自动化,但你很快会遇到瓶颈。一个选择是给他们一个 Prometheus 服务器来管理,一个 Thanos 接收端点,以及一个 Thanos 存储端点。Thanos 接收方将负责将块发送到对象存储,Thanos 存储将提供一种访问这些数据的方式,从而将远程存储的复杂性完全抽象化。 这将只是为你的基础设施提供长期存储作为服务的第一步。

在我们的测试环境中,在 thanos 实例中,我们运行了一个 Thanos 接收方。在下面的代码片段中,我们可以看到它的配置:

vagrant@thanos:~$ systemctl cat thanos-receive
...
ExecStart=/usr/bin/thanos receive \
           --tsdb.path "/var/lib/thanos/receive" \
           --tsdb.retention 6h \
           --labels "store=\"receiver\"" \
           --remote-write.address "0.0.0.0:19291" \
           --grpc-address "0.0.0.0:12901" \
           --http-address "0.0.0.0:12902"
...

由于我们已经在 Prometheus 服务器旁边运行了一个 Thanos sidecar,它将 TSDB 块发送到对象存储,我们通过不添加 --objstore.config-file 标志禁用了接收方的发送功能。注意 --labels 标志,它允许我们指定一个标签,添加到通过该接收方的 StoreAPI 暴露的所有时间序列中;这实际上是一种配置外部标签的方式。另一个值得注意的标志是 --remote-write.address,它用于提供远程写入端点。如果我们查看 prometheus 实例,我们会看到以下配置,这利用了前述标志:

vagrant@prometheus:~$ cat /etc/prometheus/prometheus.yml 
...
remote_write:
  - url: http://thanos:19291/api/v1/receive
...

为了测试这一切,我们可以简单地停止 prometheus 实例中的 Thanos sidecar,方法如下:

vagrant@prometheus:~$ sudo systemctl stop thanos-sidecar

完成此操作后,Thanos 查询器将无法访问 Thanos sidecar,并且最近的块信息将不会刷新到磁盘。通过这种方式,我们可以验证接收方是否会提供这些数据。如果我们访问 192.168.42.12:10902/graph 的 Thanos 查询器 Web 界面,并运行像 up{instance=~"prometheus.+"} 这样的即时查询,我们将看到以下输出:

图 14.5:即使 Thanos sidecar 停止,Thanos 接收方仍显示度量

请注意 store 标签,表示 Thanos 接收方正在提供数据,同时也告诉我们 Thanos sidecar 当前已停止。这证明了我们可以通过 Prometheus 的远程写入 API 查询到最近的数据。

使用此组件的决策不应轻率作出,因为它存在一些缺点:除了它被明确标记为实验性之外,它实际上是一个基于推送的系统,就像 Graphite 一样。这意味着所有推送式方法的缺点都适用,即管理滥用/恶意客户端的困难。

Thanos 规则

这个组件允许你在远程查询端点(如 Thanos querier 或 sidecar)上运行 Prometheus 兼容的规则(记录/告警)。它公开一个 StoreAPI 端点,以便将规则评估结果提供出来,结果会存储在本地块中,也可以将这些 TSDB 块发送到对象存储。可以想象将此组件用作规则的集中管理解决方案,而不是将它们分散在多个 Prometheus 实例中,但这不是其目的,而且不建议这么做。在本书中,我们强调了规则的重要性,尤其是告警规则。我们还强调了在具有所需指标的 Prometheus 实例中本地运行这些规则的重要性。我们提供了一些替代方案,如层级或跨服务联合,用于从不同的 Prometheus 实例收集指标。如果使用 Thanos ruler 进行告警,你将向关键路径添加多个故障点:其他 Thanos 组件、网络以及在最坏的情况下,对象存储。告警需要是可预测和可靠的,这样你才可以在最需要时确信它能正常工作。尽管 Thanos ruler 有一系列合法的使用场景,但它不应被视为大多数告警需求的解决方案。尽管如此,承认其存在仍然很重要。

有关 Thanos ruler 的更多信息,请访问 thanos.io/components/rule.md

现在我们已经全面了解了 Thanos 生态系统以及它在测试环境中的配置。我们邀请你在评估其行为时,尝试所有组件:例如,停止除 Thanos store 以外的所有 store API,或使用 Thanos 存储桶来了解对象存储桶中可用的数据。

总结

在本章中,我们介绍了远程读取和远程写入端点。我们了解了使用 WAL 的远程写入策略对 Prometheus 的全球性能和可用性如此重要。接着,我们探讨了一些保持 Prometheus 本地存储可控的替代方案,同时解释了选择长期存储解决方案的影响。最后,我们深入探讨了 Thanos,揭示了一些设计决策并介绍了完整的组件生态系统,提供了实际示例,展示了所有不同组件如何协同工作。通过这些,我们现在可以根据需要为 Prometheus 构建长期存储解决方案。

问题

  1. 基于 WAL 的远程写入的主要优点是什么?

  2. 如何对正在运行的 Prometheus 服务器执行备份?

  3. 能否在运行时释放 Prometheus 服务器的磁盘空间?如果可以,应该如何操作?

  4. Thanos 使用对象存储的主要优点是什么?

  5. 保留所有可用分辨率的数据是否有意义?

  6. Thanos store 的作用是什么?

  7. 如何使用 Thanos 检查对象存储中可用的数据?

深入阅读

第十五章:评估

第一章,监控基础

  1. 对于监控的共识定义很难达成,因为它在行业甚至是特定工作领域的背景下会迅速发生变化。观点的多样性、构成监控系统的组件,甚至数据如何收集或使用,都是导致很难达成清晰定义的因素。

  2. 系统管理员关注的是高分辨率、低延迟和高多样性的数据。在这个范围内,监控的主要目标是尽早发现问题,并尽快找出根本原因。

  3. 低分辨率、高延迟和高多样性的数据。

  4. 这取决于你希望监控定义的范围有多广。在本书的范围内,日志记录不被视为监控。

  5. 监控服务的位置需要传播到所有目标。时效性是这种方法的一个大缺点:如果系统一段时间没有报告,是否意味着它有问题,还是被故意停用?此外,当你管理一个分布式的主机和服务集群,数据推送到中央点时,成群涌来的请求(由于大量同时到来的连接导致的超载)或配置错误导致的数据洪流的风险变得更加复杂和耗时,需要缓解。

  6. RED 方法是一个非常好的起点,选择速率、错误和持续时间指标。

  7. 这是一种黑箱式的监控方法,应该依赖于直接或通过 exporter 仪表化该过程。

第二章,Prometheus 生态系统概述

  1. 主要组件包括 Prometheus、Alertmanager、Pushgateway、原生仪表化应用程序、Exporters 和可视化解决方案。

  2. 仅 Prometheus 和抓取目标(无论它们是原生仪表化的还是使用 exporters)是 Prometheus 部署所必需的。然而,为了实现警报路由和管理,你还需要 Alertmanager;Pushgateway 仅在某些特定用例中需要,比如批处理作业;虽然 Prometheus 自带基本的仪表盘功能,但可以将 Grafana 添加到堆栈中作为可视化选项。

  3. 并非所有应用程序都采用 Prometheus 兼容的仪表化。某些情况下,根本没有暴露任何指标。在这些情况下,我们可以依赖 exporters。

  4. 信息应该迅速收集并以同步操作方式暴露。

  5. 如果可能,警报将从分区的两侧发送。

  6. 最快捷的选项是使用 webhook 集成。

  7. Prometheus 服务器自带表达式浏览器和控制台。

第三章,设置测试环境

  1. 虽然 Prometheus 堆栈几乎可以在所有主流操作系统中部署,因此它很可能在你的桌面环境中运行,但使用基于 Vagrant 的测试环境来模拟机器部署,以及使用 minikube 来模拟基于 Kubernetes 的生产环境,会更具可重复性。

  2. 位于 utils 目录下的 defaults.sh 文件允许为基于虚拟机的示例更改软件版本。

  3. 所有基于虚拟机的示例中的默认子网是 192.168.42.0/24

  4. 启动 Prometheus 实例的步骤如下:

    1. 确保软件版本与推荐版本匹配。

    2. 克隆提供的代码仓库。

    3. 进入章节目录。

    4. 运行 vagrant up

    5. 完成后,运行 vagrant destroy -f

  5. 该信息可以在 Prometheus Web 界面的 /targets 下查看。

  6. 位于 ./cache/alerting.log

  7. 在任何章节中,当你完成测试环境后,只需在该章节的目录下运行 vagrant destroy -f

第四章,Prometheus 指标基础

  1. 时间序列数据可以定义为按时间顺序从同一来源收集的数值数据点序列——通常是以固定的间隔收集。因此,这种数据在图形化表示时,会描绘数据随时间的演变,其中 x 轴是时间,y 轴是数据值。

  2. 一个时间戳,一个值,以及标签/标签。

  3. 预写日志WAL)。

  4. 默认值是 2 小时,不应更改。

  5. 一个 float64 值和具有毫秒精度的时间戳。

  6. 直方图对于跟踪分桶的延迟和大小(例如,请求持续时间或响应大小)特别有用,因为它们可以跨不同维度自由聚合。另一个很好的用途是生成热图(直方图随时间的演变)。

    不带分位数的摘要生成、收集和存储的成本非常低。使用摘要分位数的主要原因是在需要准确的分位数估计时,无论观察到的事件的分布和范围如何。

  7. 横向聚合通过聚合维度将多个时间序列合并为一个;纵向聚合则将来自单一时间序列的样本在时间范围内合并为一个数据点。

第五章,运行 Prometheus 服务器

  1. 然后,scrape_timeout 将被设置为其默认值——10 秒。

  2. 除了重启外,配置文件还可以通过向 Prometheus 进程发送 SIGHUP 信号或如果启动时使用了 --web.enable-lifecycle,通过向 /-/reload 端点发送 HTTP POST 请求来重新加载。

  3. Prometheus 默认会回溯最多五分钟,除非发现过时标记,在这种情况下,它会立即将该系列视为过时。

  4. relabel_configs 用于在抓取前重写目标列表时,metric_relabel_configs 用于在抓取后重写标签或丢弃样本。

  5. 由于我们是通过 Kubernetes 服务(其功能类似于负载均衡器)进行抓取的,因此每次抓取仅会命中 Hey 应用程序的一个实例。

  6. 由于 Kubernetes pod 的短暂性质,几乎不可能在没有额外自动化的情况下使用静态配置准确管理抓取目标。

  7. Prometheus Operator 利用 Kubernetes 自定义资源和自定义控制器声明特定领域的定义,这些定义可以用来自动管理 Prometheus 堆栈及其抓取任务。

第六章,导出器和集成

  1. 文本文件收集器通过监视目录中的.prom扩展名文件,启用自定义指标的展示,这些文件包含 Prometheus 展示格式的指标。

  2. 数据是从容器运行时守护进程和 Linux cgroups 中收集的。

  3. 你可以限制启用的收集器数量(--collectors),或者使用指标白名单(--metric-whitelist)或黑名单(--metric-blacklist)标志。

  4. 在调试探针时,你可以将 &debug=true 附加到 HTTP GET URL,以启用调试信息。

  5. 我们可以使用 mtailgrok_exporter 从应用程序日志中提取指标。

  6. 一个可能的问题是缺乏高可用性,导致其成为单点故障。这也影响了可扩展性,因为唯一的扩展方式是垂直扩展或分片。通过使用 Pushgateway,Prometheus 不直接抓取实例,这避免了将 up 指标作为健康监控的代理。此外,和 node_exporter 的文本文件收集器类似,指标需要通过 Pushgateway 的 API 手动删除,否则它们将永远暴露给 Prometheus。

  7. 在这种特殊情况下,Node Exporter 的文本文件收集器可以是一个有效的解决方案,特别是当生成的指标的生命周期与实例的生命周期匹配时。

第七章,Prometheus 查询语言 - PromQL

  1. 比较运算符包括 <(小于)、>(大于)、==(等于)、!=(不等于)、=>(大于或等于)和 <=(小于或等于)。

  2. 当你想要丰富的时间序列位于 PromQL 表达式的右侧时。

  3. topk 已经对结果进行了排序。

  4. 虽然 rate() 函数通过使用范围内的第一个和最后一个值,并按范围窗口缩放来提供指定时间间隔内每秒的平均变化率,但 irate() 函数则使用范围内的最后两个值进行计算,从而得出瞬时变化率。

  5. 类型为 info 的指标名称以 _info 结尾,是常规的仪表,只有一个可能的值 1。这种特殊类型的指标被设计用来存储那些随着时间变化的标签值,例如版本(例如,导出器版本、语言版本和内核版本)、分配的角色或虚拟机元数据等信息。

  6. rate 函数期望一个计数器,但计数器的总和实际上是一个仪表,因为当计数器之一重置时,它可以下降;这在绘制图表时会导致看似随机的尖峰,因为 rate 会将任何下降视为计数器重置,但其他计数器的总和会被认为是从零到当前值的巨大增量。

  7. 当 CPU 核心使用 100%时,它使用 1 个 CPU 秒。相反,当它处于空闲状态时,它将使用 0 个 CPU 秒。通过直接利用 CPU 秒,我们可以轻松计算使用百分比。虚拟机可能拥有多个核心,这意味着每秒可能使用超过 1 个 CPU 秒。以下表达式计算每个核心在过去 5 分钟内每秒空闲了多少 CPU 秒:

rate(node_cpu_seconds_total{job="node",mode="idle"}[5m])

计算过去五分钟内每个核心每秒的平均 CPU 空闲秒数的简单方法是对每个核心的值进行平均:

avg without (cpu, mode) (rate(node_cpu_seconds_total{job="node",mode="idle"}[5m]))

由于使用的 CPU 秒数加上空闲的 CPU 秒数应总计为每核心每秒1个 CPU 秒,为了获得 CPU 使用率,我们执行以下操作:

avg without (cpu, mode) (1 - rate(node_cpu_seconds_total{job="node",mode="idle"}[5m]))

为了获得百分比,我们只需将其乘以100

avg without (cpu, mode) (1 - rate(node_cpu_seconds_total{job="node",mode="idle"}[5m])) * 100

第八章,故障排除与验证

  1. Prometheus 随附promtool,该工具具有其他功能,其中之一是检查配置文件中的问题:
promtool check config /etc/prometheus/prometheus.yml
  1. promtool工具还可以从stdin读取 Prometheus 展示格式的度量,并根据当前的 Prometheus 标准进行验证:
curl -s http://prometheus:9090/metrics | promtool check metrics
  1. promtool工具可以用来对 Prometheus 实例执行即时查询:
promtool query instant 'http://prometheus:9090' 'up == 1'
  1. 你可以使用promtool查找给定标签名称的每个标签值。一个例子如下:
promtool query labels 'http://prometheus:9090' 'mountpoint'
  1. 通过将--log.level=debug添加到启动参数中。

  2. /-/healthy端点将告诉你(或编排系统)实例是否有问题,需要重新部署,而/-/ready端点将告诉你(或实例的负载均衡器)是否准备好接收流量。

  3. 当 Prometheus 数据库处于解锁状态时(例如,当没有 Prometheus 使用该目录时),你可以运行tsdb工具来分析特定的数据块,检查指标和标签的变化:

tsdb analyze /var/lib/prometheus/data 01D486GRJTNYJH1RM0F2F4Q9TR

第九章,定义告警与记录规则

  1. 这种规则可以通过预先计算昂贵的查询、将原始数据聚合为时间序列并导出到外部系统,帮助减轻重型仪表板的负担,并协助创建复合范围向量查询。

  2. instance_job:latency_seconds_bucket:rate30s需要至少具有instancejob标签。它是通过对latency_seconds_bucket_total度量应用速率计算得出的,使用了 30 秒的范围向量。因此,源表达式可能是以下形式:

rate(latency_seconds_bucket_total[30s])
  1. 随着该标签值的变化,警报的身份也会发生变化。

  2. 当警报开始触发(其表达式开始返回结果)时,它进入待处理状态,但for时间间隔尚未过去,因此不会被视为触发

  3. 这将是即时的。当没有指定for子句时,只要表达式产生结果,警报就会被视为触发。

  4. promtool工具具有一个test子命令,可以为记录和告警规则运行单元测试。

第十章,发现与创建 Grafana 仪表板

  1. Grafana 支持通过在启动时从配置路径读取 YAML 定义来自动配置数据源。

  2. 从 Grafana 画廊导入仪表盘的步骤如下:

    1. grafana.com画廊中选择一个仪表盘 ID。

    2. 在目标 Grafana 实例中,点击左侧主菜单中的加号,然后从子菜单中选择导入

    3. 将选定的 ID 粘贴到适当的文本字段中。

  3. 变量允许仪表盘配置占位符,这些占位符可以用于表达式和标题字符串,并且这些占位符可以从静态或动态列表中填充值,通常以下拉菜单的形式展示给仪表盘用户。每当选择的值发生变化时,Grafana 会自动更新使用该变量的面板查询和标题字符串。

  4. 在 Grafana 中,构建块是面板。

  5. 不,实际上不会。仪表盘 ID 保持不变,但迭代次数会递增。

  6. 控制台是直接从 Prometheus 实例提供的自定义仪表盘。

  7. 它们是从控制台模板生成的,这些模板用原始的 HTML/CSS/JavaScript 编写,并利用 Go 模板语言的强大功能,使得它们具有无限的可定制性。由于模板运行在 Prometheus 内部,它可以直接访问 TSDB,而无需通过 HTTP API,这使得控制台生成速度惊人地快。

第十一章,理解和扩展 Alertmanager

  1. 在网络分区的情况下,分区的每一方都会发送它们已知的告警通知:在集群故障场景下,收到重复的告警通知总比完全没有告警好。

  2. 通过在路由上将continue设置为true,它会使匹配过程继续遍历路由树,直到找到下一个匹配,从而允许触发多个接收器。

  3. group_interval配置定义了在接收到新告警时,等待额外告警加入给定告警组(由group_by定义)多长时间,才会发送更新通知;repeat_interval定义了在没有变化的情况下,等待多长时间才会重新发送给定告警组的通知。

  4. 顶级路由,也称为捕获所有路由或后备路由,在其他子路由未匹配到传入的告警时,会触发默认的接收器。

  5. Webhook 集成允许 Alertmanager 向一个可配置的端点发送 HTTP POST 请求,并带上通知的 JSON 有效负载。这允许你运行一个桥接程序,将 Alertmanager 的通知转换为你选择的通知提供商格式,然后转发给它。

  6. CommonLabels字段包含了所有通知中通用的告警标签。CommonAnnotations字段的作用完全相同,但适用于注释。

  7. 一个好的方法是使用“死人开关”警报:创建一个始终会触发的警报,然后配置 Alertmanager 将该警报路由到一个(希望是)外部系统,该系统将负责通知你该警报是否停止接收通知。

第十二章,选择合适的服务发现

  1. 在一个高度动态的环境中管理抓取目标,若没有自动发现机制,会变得十分艰巨。

  2. 拥有一组具有足够权限的访问凭证,可以通过其 API 列出所有所需资源。

  3. 它支持 A、AAAA 和 SRV DNS 记录。

  4. 由于可查询的 API 对象数量庞大,Prometheus 的 Kubernetes 发现配置引入了角色的概念,角色可以是nodeservicepodendpointingress。每个角色将提供相应的对象集以供目标发现。

  5. 实现自定义服务发现的最佳机制是使用基于文件的发现集成,将目标注入到 Prometheus 中。

  6. 不是的。Prometheus 会尝试使用文件系统监视自动检测何时发生变化,然后重新加载目标列表,如果监视不可用,则会回退到定期重新读取目标文件。

  7. 推荐使用在 Prometheus 代码库中提供的适配器代码,因为它抽象了实现发现机制所需的大部分样板代码。此外,如果你打算将自定义的服务发现功能贡献给项目,适配器使得将服务发现代码轻松地集成到主 Prometheus 二进制文件中,假如它能够获得广泛支持和社区支持的话。

第十三章,Prometheus 的扩展与联邦

  1. 当你确认单个实例不足以处理负载,而且无法通过增加资源来运行时,你应该考虑分片。

  2. 垂直分片用于根据职责(例如按功能或团队)拆分抓取工作负载,每个 Prometheus 分片抓取不同的任务。水平分片则将单个抓取任务的负载拆分到多个 Prometheus 实例中。

  3. 为了减少 Prometheus 实例的接收负载,你应该考虑通过使用metric_relabel_configs规则来丢弃不必要的指标,或者通过增加抓取间隔来减少总体抓取样本数量。

  4. 实例级的 Prometheus 服务器应该联合作业级的聚合指标。作业级的 Prometheus 服务器应该联合数据中心级的聚合指标。

  5. 你可能需要在记录和警报规则中使用其他 Prometheus 实例中才有的指标。

  6. 使用的协议是 gRPC。

  7. 你将失去使用 Thanos 去重功能的能力。

第十四章,Prometheus 与长期存储的集成

  1. 将远程写入功能基于 WAL 的主要优点是:它使得指标流式传输成为可能,具有更小的内存占用,并且对崩溃更具恢复力。

  2. 你可以通过使用/api/v1/admin/tsdb/snapshot API 端点请求 Prometheus 生成 TSDB 快照(仅在启用--web.enable-admin-api标志时可用),然后备份该快照。

  3. 你可以通过使用/api/v1/admin/tsdb/delete_series API 端点从 TSDB 中删除时间序列,然后使用/api/v1/admin/tsdb/clean_tombstones来让 Prometheus 清理已删除的序列(这些端点仅在启用--web.enable-admin-api标志时可用)。

  4. 对象存储通常提供 99.999999999% 的耐久性和 99.99% 的可用性服务水平协议,与块存储相比,它的费用非常低廉。

  5. 是的。例如,保留原始数据对于缩放到过去的短时间范围非常有用。

  6. Thanos store 提供了 Thanos Querier 与对象存储之间的 API 网关。

  7. 可以使用thanos bucket子命令检查对象存储中的数据,该命令还支持验证、修复、列出和检查存储桶。

posted @ 2025-06-27 17:08  绝不原创的飞龙  阅读(114)  评论(0)    收藏  举报