使用 Grafana 和 Prometheus 进行异常警报监控
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我一直在做一个副业项目,这个项目始于疫情期间,旨在构建一个带有广告和追踪拦截功能的 DoH 服务。此后,它不断发展壮大,目前已部署在 7 个不同的服务器上。我使用 Prometheus 从分布式全球堆栈收集日志、数据和指标,并将其传输到远程服务器,以便使用 Grafana提供所有可观测性的统一视图。同时,我还使用Elixir 和 Phoenix LiveView通过Kafka记录所有 DNS 流量并进行流式传输。
现在,它已成为我日常生活中最关键的服务。任何宕机或性能瓶颈都会降低我的浏览体验,甚至更糟,让我无法上网。最初只是一个副业项目,但在过去几年里,它已经发展成为我的核心开发活动和学习平台。
随着我在手机、平板电脑和台式机上越来越依赖服务的正常运行时间,监控和观察系统的需求也越来越大,对 SLA 和 SLO 的保证也越来越重要。
和往常一样,我首先设置了阈值警报,但很快警报就变得过于嘈杂,因为这些节点的配置都很低,只有 1-2GB 内存和 1 个虚拟 CPU。这项活动的目标之一也是测试低成本 VPS 的性能。目前,每个节点在有限的硬件配置下运行着 13 个服务,这需要仔细的考量和观察。
由于节点容量有限,大部分压力都集中在节点上,警报将针对节点级别的负载、CPU、内存和磁盘,而不是服务级别。
为了克服噪声,更好地了解使用模式并解决服务问题,我开始研究异常检测,并偶然发现了这篇关于“Grafana Prometheus:检测时间序列中的异常”的精彩博文。它很好地解释了“3σ 规则指出,我们所有‘正常’数据都应该在数据平均值的 3 个标准差范围内”。
这可以用数学方式表述为:
基于此,我们制定了以下用于检测 CPU、内存、负载和磁盘异常的 PromQL 查询。总体而言,这些表达式可用于识别当前空闲 CPU 时间、可用内存、可用磁盘空间或 15 分钟平均负载与历史平均值存在显著偏差的情况,这可能表明 CPU 使用率存在异常或不寻常行为。
CPU
(avg_over_time(node_cpu_seconds_total{instance="mark-00-sin", job="node-exporter-mark-00-sin", mode="idle"}[$__rate_interval])-avg_over_time(node_cpu_seconds_total{instance="mark-00-sin", job="node-exporter-mark-00-sin", mode="idle"}[1d]))/stddev_over_time(node_cpu_seconds_total{instance="mark-00-sin", job="node-exporter-mark-00-sin", mode="idle"}[1d])
以下是查询语句的详细分析:
avg_over_time(node_cpu_seconds_total{instance="mark-00-sin", job="node-exporter-mark-00-sin", mode="idle"}[$__rate_interval])- 这部分计算指定时间范围内的平均 CPU 空闲时间
$__rate_interval。 node_cpu_seconds_total该指标表示 CPU 处于各种状态的总秒数。在本例中,我们关注的是“空闲”状态,即 CPU 不执行任何任务的状态。instance、job和mode是用于将指标筛选到空闲模式下 node-exporter 作业的特定实例的标签。
- 这部分计算指定时间范围内的平均 CPU 空闲时间
- avg_over_time(node_cpu_seconds_total{instance="mark-00-sin", job="node-exporter-mark-00-sin", mode="idle"}[1d])- 这部分从上一步计算出的平均值中减去过去一天的平均空闲 CPU 时间。
- 这有助于了解当前平均值与历史平均值的比较情况。
/stddev_over_time(node_cpu_seconds_total{instance="mark-00-sin", job="node-exporter-mark-00-sin", mode="idle"}[1d])- 这部分将步骤 2 的结果除以过去一天 CPU 空闲时间的标准差。
- 标准差衡量一组数据点的离散程度或分散程度。在此语境下,它有助于了解当前值与历史平均值的偏差程度。
Memory
(avg_over_time(node_memory_MemAvailable_bytes{instance="mark-00-sin",job="node-exporter-mark-00-sin"}[$__rate_interval])-avg_over_time(node_memory_MemAvailable_bytes{instance="mark-00-sin",job="node-exporter-mark-00-sin"}[1d]))/(stddev_over_time(node_memory_MemAvailable_bytes{instance="mark-00-sin",job="node-exporter-mark-00-sin"}[1d]))
以下是查询语句的详细分析:
avg_over_time(node_memory_MemAvailable_bytes{instance="mark-00-sin", job="node-exporter-mark-00-sin"}[$__rate_interval])- 这部分计算指定时间范围内的平均可用内存
$__rate_interval。 node_memory_MemAvailable_bytes是表示系统可用内存量的指标。
- 这部分计算指定时间范围内的平均可用内存
-avg_over_time(node_memory_MemAvailable_bytes{instance="mark-00-sin", job="node-exporter-mark-00-sin"}[1d])- 这部分代码从上一步计算出的平均值中减去过去一天的平均可用内存。这有助于了解当前平均值与历史平均值的比较情况。
/(stddev_over_time(node_memory_MemAvailable_bytes{instance="mark-00-sin", job="node-exporter-mark-00-sin"}[1d]))- 这部分将步骤 2 的结果除以过去一天可用内存的标准差。
- 标准差衡量一组数据点的离散程度或分散程度。在此背景下,它有助于了解当前可用内存值与历史平均值的偏差程度。
Load
(avg_over_time(node_load15{instance="mark-00-sin",job="node-exporter-mark-00-sin"}[$__rate_interval]) - avg_over_time(node_load15{instance="mark-00-sin",job="node-exporter-mark-00-sin"}[1d]))/stddev_over_time(node_load15{instance="mark-00-sin",job="node-exporter-mark-00-sin"}[1d])
以下是查询语句的详细分析:
avg_over_time(node_load15{instance="mark-00-sin", job="node-exporter-mark-00-sin"}[$__rate_interval])- 这部分计算指定时间范围内 15 分钟的平均负载
$__rate_interval。 node_load15是代表系统 15 分钟平均负载的指标。
- 这部分计算指定时间范围内 15 分钟的平均负载
- avg_over_time(node_load15{instance="mark-00-sin", job="node-exporter-mark-00-sin"}[1d])- 这部分从上一步计算出的平均值中减去过去一天的平均15分钟负载值。这有助于了解当前平均值与历史平均值的比较情况。
/stddev_over_time(node_load15{instance="mark-00-sin", job="node-exporter-mark-00-sin"}[1d])- 这部分将步骤 2 的结果除以过去一天 15 分钟负荷平均值的标准偏差。
- 标准差衡量一组数据点的离散程度或分散程度。在此语境下,它有助于了解当前值与历史平均值的偏差程度。
Disk
(avg_over_time(node_filesystem_avail_bytes{instance="mark-00-sin",job="node-exporter-mark-00-sin",device="/dev/sda"}[$__rate_interval]) - avg_over_time(node_filesystem_avail_bytes{instance="mark-00-sin",job="node-exporter-mark-00-sin",device="/dev/sda"}[1d]))/stddev_over_time(node_filesystem_avail_bytes{instance="mark-00-sin",job="node-exporter-mark-00-sin",device="/dev/sda"}[1d])
以下是查询语句的详细分析:
avg_over_time(node_filesystem_avail_bytes{instance="mark-00-sin", job="node-exporter-mark-00-sin", device="/dev/sda"}[$__rate_interval])- 此部分计算指定设备上在指定时间范围内的平均可用磁盘空间
$__rate_interval。 node_filesystem_avail_bytes是一个表示文件系统上可用字节数的指标。
- 此部分计算指定设备上在指定时间范围内的平均可用磁盘空间
- avg_over_time(node_filesystem_avail_bytes{instance="mark-00-sin", job="node-exporter-mark-00-sin", device="/dev/sda"}[1d])- 这部分代码从上一步计算出的平均值中减去过去一天的平均可用磁盘空间。这有助于了解当前平均值与历史平均值的比较情况。
/(stddev_over_time(node_filesystem_avail_bytes{instance="mark-00-sin", job="node-exporter-mark-00-sin", device="/dev/sda"}[1d]))- 这部分将步骤 2 的结果除以过去一天可用磁盘空间的标准差。
- 标准差衡量一组数据点的离散程度或分散程度。在此语境下,它有助于了解当前可用磁盘空间值与历史平均值的偏差程度。
这涉及到很多 PromQL 理论,但它真的比阈值警报更好用,并且能让警报机制更合理吗?
以下是基于阈值和基于异常的警报之间的一些比较,以及它们如何提供更好的洞察力并相互补充。
这是内存警报,因为内存已超过阈值而触发。
与此同时,异常警报显示正常,表明使用量在预期范围内。
这里有一个负载阈值和异常同时触发的例子,这很好地表明它们不是独立工作的,而是相互补充的。
节点上只有负载异常警报,没有内存异常警报。
当没有异常警报但阈值警报嘈杂时,这能提供很好的参考信息。要么是阈值设置得太低,要么是资源分配不合理。异常警报和阈值警报应该同时满足要求,才能确保结果的准确性。
另一个基于磁盘异常的例子是,磁盘使用率根据阈值下降,你可能永远不会触发警报,直到使用率上升或下降到超过某个值,但异常表明你的应用程序在写入日志或硬件降级方面可能存在一些问题。
最后再补充一个基于负载的警报:阈值警报保持正常,而异常警报被触发,因为负载突然飙升,但该值仍在阈值范围内,这应该进行调查和观察,因为它可能表明意外流量或长时间运行的僵尸进程消耗资源。

与以往一样,异常检测的有效性取决于所收集数据的质量和一致性,就我而言,我使用的是 Prometheus,您可能需要根据您的具体用例和系统特性调整阈值或使用更高级的技术。













