当我第一次接触到 counter
和 gauge
这两个术语,以及标有“平均值”和“上限 90”的彩色数字图表时,我的反应是回避。就好像我看到了它们,但我并不在意,因为我不理解它们,也不知道它们可能有什么用处。由于我的工作不需要我关注它们,所以它们一直被忽略。
那大约是两年前的事了。随着我职业生涯的发展,我想更多地了解我们的网络应用程序,那时我开始学习指标。
我理解监控的三个阶段(到目前为止)是
- 阶段 1:什么?(看向别处)
- 阶段 2:没有指标,我们真的就像盲人飞行。
- 阶段 3:我们如何避免错误地使用指标?
我目前处于阶段 2,并将分享我到目前为止所学到的知识。我正在逐步向阶段 3 迈进,我将在本文末尾提供一些关于这部分旅程的资源。
让我们开始吧!
软件先决条件
本文讨论的所有演示都可以在 我的 GitHub 仓库 中找到。您需要安装 docker
和 docker-compose
才能使用它们。
我为什么要监控?
监控的主要原因是
- 理解正常和异常的系统和服务行为
- 进行容量规划,向上或向下扩展
- 协助性能故障排除
- 理解软件/硬件变更的影响
- 根据测量结果更改系统行为
- 在系统表现出意外行为时发出警报
指标和指标类型
就我们的目的而言,指标是在给定时间点观察到的某个量的值。博客文章的总点击次数、参加讲座的总人数、在缓存系统中未找到数据的次数、网站上已登录用户的数量——这些都是指标的例子。
它们大致分为三类
计数器
考虑一下您的个人博客。您刚刚发布了一篇文章,想关注它随时间推移获得的点击次数,这个数字只会增加。这是一个 计数器 指标的例子。它的值从 0 开始,并在您的博客文章的生命周期内增加。在图形上,计数器看起来像这样

opensource.com
仪表盘
假设您想跟踪每天或每周的点击次数,而不是博客文章随时间推移的总点击次数。这个指标称为 仪表盘 ,其值可以上升或下降。在图形上,仪表盘看起来像这样

opensource.com
仪表盘的值通常在某个时间窗口内有上限和下限。
直方图和计时器
直方图(Prometheus 称之为)或 计时器 (StatsD 称之为)是跟踪采样观察的指标。与计数器或仪表盘不同,直方图指标的值不一定显示上升或下降的模式。我知道这听起来不太合理,可能看起来与仪表盘没有什么不同。不同之处在于您期望对直方图数据做什么,而不是仪表盘。因此,监控系统需要知道指标是直方图类型,以便允许您执行这些操作。

opensource.com
演示 1:计算和报告指标
演示 1 是一个使用 Flask 框架编写的基本 Web 应用程序。它演示了我们如何计算和报告指标。
src
目录中的 app.py
包含应用程序,而 src/helpers/middleware.py
包含以下内容
from flask import request
import csv
import time
def start_timer():
request.start_time = time.time()
def stop_timer(response):
# convert this into milliseconds for statsd
resp_time = (time.time() - request.start_time)*1000
with open('metrics.csv', 'a', newline='') as f:
csvwriter = csv.writer(f)
csvwriter.writerow([str(int(time.time())), str(resp_time)])
return response
def setup_metrics(app):
app.before_request(start_timer)
app.after_request(stop_timer)
当从应用程序调用 setup_metrics()
时,它配置 start_timer()
函数在处理请求之前调用,stop_timer()
函数在处理请求之后但在发送响应之前调用。在上面的函数中,我们写入 timestamp
和处理请求所花费的时间(以毫秒为单位)。
当我们在 demo1
目录中运行 docker-compose up
时,它会启动 Web 应用程序,然后启动一个客户端容器,该容器向 Web 应用程序发出大量请求。您将看到已创建的 src/metrics.csv
文件,其中包含两列:timestamp
和 request_latency
。
查看此文件,我们可以推断出两件事
- 已生成大量数据
- 指标的任何观察结果都没有与之相关的任何特征
如果没有与指标观察相关的特征,我们就无法说出此指标与哪个 HTTP 端点相关联,或者此指标是从应用程序的哪个节点生成的。因此,我们需要使用适当的元数据来限定每个指标观察结果。
统计学 101
如果我们回顾高中数学,我们应该都记得一些统计学术语,即使只是模糊地记得,包括:平均值、中位数、百分位数和直方图。让我们简要回顾一下它们,不评判它们的用处,就像在高中时一样。
平均值
平均值,或数字列表的平均值,是数字的总和除以列表的基数。3、2 和 10 的平均值是 (3+2+10)/3 = 5
。
中位数
中位数是另一种平均值,但它的计算方式不同;它是按从小到大(或反之亦然)排序的数字列表中的中心数字。在上面的列表 (2, 3, 10) 中,中位数是 3。计算不是很直接;它取决于列表中的项目数。
百分位数
百分位数是一种度量,它给出了一个度量,低于该度量,数字的某个 (k
) 百分比位于其下方。在某种意义上,它让我们了解这个度量相对于我们 k
百分比的数据表现如何。例如,上述列表的第 95 个百分位数得分是 9.29999。百分位数度量范围从 0 到 100(不包括 100)。第零个百分位数是一组数字中的最低分数。你们中的一些人可能还记得,中位数是第 50 个百分位数,结果是 3。
一些监控系统将百分位数度量称为 upper_X
,其中 X 是百分位数;上限 90 指的是第 90 个百分位数的值。
分位数
q-分位数是一种度量,它在 N 个数字的集合中对 qN 进行排名。q 的值介于 0 和 1 之间(包括 0 和 1)。当 q 为 0.5 时,该值是中位数。分位数和百分位数之间的关系是,q 分位数处的度量等效于 100q 百分位数处的度量。
直方图
我们之前学到的指标 直方图 是监控系统的实现细节。在统计学中,直方图是将数据分组到桶中的图形。让我们考虑一个不同的、人为的例子:阅读您博客的人的年龄。如果您获得了一些数据,并且想粗略了解读者按年龄组划分的情况,那么绘制直方图将向您显示如下图形

opensource.com
累积直方图
累积直方图是一种直方图,其中每个桶的计数都包括前一个桶的计数,因此得名累积。上述数据集的累积直方图如下所示

opensource.com
我们为什么需要统计学?
在上面的演示 1 中,我们观察到,当我们报告指标时,会生成大量数据。当处理指标时,我们需要统计学,因为指标太多了。我们不关心单个值,而是关心整体行为。我们期望这些值表现出的行为是正在观察的系统行为的代理。
演示 2:向指标添加特征
在我们上面的演示 1 应用程序中,当我们计算和报告请求延迟时,它指的是由一些特征唯一标识的特定请求。其中一些是
- HTTP 端点
- HTTP 方法
- 运行它的主机/节点的标识符
如果我们将这些特征附加到指标观察结果,我们就可以获得围绕每个指标的更多上下文。让我们在 演示 2 中探索向我们的指标添加特征。
现在,src/helpers/middleware.py
文件在写入指标时,会将多列写入 CSV 文件
node_ids = ['10.0.1.1', '10.1.3.4']
def start_timer():
request.start_time = time.time()
def stop_timer(response):
# convert this into milliseconds for statsd
resp_time = (time.time() - request.start_time)*1000
node_id = node_ids[random.choice(range(len(node_ids)))]
with open('metrics.csv', 'a', newline='') as f:
csvwriter = csv.writer(f)
csvwriter.writerow([
str(int(time.time())), 'webapp1', node_id,
request.endpoint, request.method, str(response.status_code),
str(resp_time)
])
return response
由于这是一个演示,因此我在报告指标时,擅自报告了随机 IP 作为节点 ID。当我们在 demo2
目录中运行 docker-compose up
时,它将生成一个包含多列的 CSV 文件。
使用 pandas
分析指标
我们现在将使用 pandas 分析此 CSV 文件。运行 docker-compose up
将打印一个 URL,我们将使用该 URL 打开 Jupyter 会话。一旦我们将 Analysis.ipynb
笔记本上传到会话中,我们就可以将 CSV 文件读入 pandas DataFrame
import pandas as pd
metrics = pd.read_csv('/data/metrics.csv', index_col=0)
index_col
指定我们想使用 timestamp
作为索引。
由于我们添加的每个特征都是 DataFrame 中的一列,因此我们可以根据这些列执行分组和聚合
import numpy as np
metrics.groupby(['node_id', 'http_status']).latency.aggregate(np.percentile, 99.999)
有关数据的更多示例分析,请参阅 Jupyter 笔记本。
我应该监控什么?
软件系统有许多变量,它们的值在其生命周期内会发生变化。该软件在某种操作系统中运行,操作系统变量也会发生变化。在我看来,当出现问题时,数据越多越好。
我建议监控的关键操作系统指标是
- CPU 使用率
- 系统内存使用率
- 文件描述符使用率
- 磁盘使用率
其他要监控的关键指标将因您的软件应用程序而异。
网络应用程序
如果您的软件是一个网络应用程序,它监听并服务于客户端请求,则要衡量的关键指标是
- 传入请求数(计数器)
- 未处理的错误(计数器)
- 请求延迟(直方图/计时器)
- 排队时间,如果您的应用程序中有队列(直方图/计时器)
- 队列大小,如果您的应用程序中有队列(仪表盘)
- 工作进程/线程使用率(仪表盘)
如果您的网络应用程序在满足客户端请求的上下文中向其他服务发出请求,则它应该具有记录与这些服务通信行为的指标。要监控的关键指标包括请求数、请求延迟和响应状态。
HTTP Web 应用程序后端
HTTP 应用程序应监控以上所有内容。此外,它们应保留关于非 200 HTTP 状态计数的粒度数据,按所有其他 HTTP 状态代码分组。如果您的 Web 应用程序具有用户注册和登录功能,则它也应该具有这些功能的指标。
长时间运行的进程
长时间运行的进程,如 Rabbit MQ 消费者或任务队列工作程序,虽然不是网络服务器,但其工作模式是拾取任务并处理它。因此,我们应该监控这些进程已处理的请求数和请求延迟。
无论应用程序类型如何,每个指标都应具有与之关联的适当元数据。
在 Python 应用程序中集成监控
在 Python 应用程序中集成监控涉及两个组件
- 更新您的应用程序以计算和报告指标
- 设置监控基础设施以容纳应用程序的指标,并允许对它们进行查询
记录和报告指标的基本思想是
def work():
requests += 1
# report counter
start_time = time.time()
# < do the work >
# calculate and report latency
work_latency = time.time() - start_time
...
考虑到上述模式,我们经常利用装饰器、上下文管理器和中间件(对于网络应用程序)来计算和报告指标。在演示 1 和演示 2 中,我们在 Flask 应用程序中使用了装饰器。
指标报告的拉取和推送模型
本质上,从 Python 应用程序报告指标有两种模式。在拉取模型中,监控系统在预定义的 HTTP 端点“抓取”应用程序。在推送模型中,应用程序将数据发送到监控系统。

opensource.com
Prometheus 是在拉取模型中工作的监控系统的示例。StatsD 是监控系统的示例,其中应用程序将指标推送到系统。
集成 StatsD
要将 StatsD 集成到 Python 应用程序中,我们将使用 StatsD Python 客户端,然后更新我们的指标报告代码,以使用适当的库调用将数据推送到 StatsD 中。
首先,我们需要创建一个 client
实例
statsd = statsd.StatsClient(host='statsd', port=8125, prefix='webapp1')
prefix
关键字参数会将指定的 prefix
添加到通过此客户端报告的所有指标。
一旦我们有了客户端,我们就可以使用以下命令报告 timer
的值
statsd.timing(key, resp_time)
要递增计数器
statsd.incr(key)
要将元数据与指标关联,键定义为 metadata1.metadata2.metric
,其中每个 metadataX
都是允许聚合和分组的字段。
演示应用程序 StatsD 是将 Python Flask 应用程序与 statsd
集成的完整示例。
集成 Prometheus
要使用 Prometheus 监控系统,我们将使用 Promethius Python 客户端。我们将首先创建适当指标类的对象
REQUEST_LATENCY = Histogram('request_latency_seconds', 'Request latency',
['app_name', 'endpoint']
)
上面语句中的第三个参数是与指标关联的 labels
。这些 labels
定义了与单个指标值关联的元数据。
要记录特定的指标观察结果
REQUEST_LATENCY.labels('webapp', request.path).observe(resp_time)
下一步是在我们的应用程序中定义 Prometheus 可以抓取的 HTTP 端点。这通常是一个名为 /metrics
的端点
@app.route('/metrics')
def metrics():
return Response(prometheus_client.generate_latest(), mimetype=CONTENT_TYPE_LATEST)
演示应用程序 Prometheus 是将 Python Flask 应用程序与 prometheus
集成的完整示例。
哪个更好:StatsD 还是 Prometheus?
自然的下一个问题是:我应该使用 StatsD 还是 Prometheus?我已经写了一些关于这个主题的文章,您可能会发现它们很有用
- 使用 Prometheus 监控多进程 Python 应用程序的选项
- 使用 Prometheus 监控同步 Python Web 应用程序
- 使用 Prometheus 监控异步 Python Web 应用程序
使用指标的方法
我们已经了解了一些关于为什么我们要在应用程序中设置监控的信息,但现在让我们更深入地了解其中的两个:警报和自动缩放。
使用指标进行警报
指标的关键用途是创建警报。例如,您可能希望在过去五分钟内 HTTP 500 的数量增加时,向相关人员发送电子邮件或寻呼通知。我们用于设置警报的内容取决于我们的监控设置。对于 Prometheus,我们可以使用 Alertmanager,对于 StatsD,我们使用 Nagios。
使用指标进行自动缩放
指标不仅可以让我们了解我们当前的基础设施是过度配置还是配置不足,它们还可以帮助在云基础设施中实施自动缩放策略。例如,如果过去五分钟内我们服务器上的工作进程使用率经常达到 90%,我们可能需要横向扩展。我们将如何实施缩放取决于云基础设施。默认情况下,AWS Auto Scaling 允许基于系统 CPU 使用率、网络流量和其他因素的缩放策略。但是,要使用应用程序指标进行向上或向下扩展,我们必须发布 自定义 CloudWatch 指标。
多服务架构中的应用程序监控
当我们超越单个应用程序架构时,例如客户端请求可以触发对多个服务的调用,然后才发回响应,我们需要从我们的指标中获得更多信息。我们需要延迟指标的统一视图,以便我们可以查看每个服务响应请求花费了多少时间。这可以通过 分布式追踪 实现。
您可以在我的博客文章 通过 Zipkin 在您的 Python 应用程序中引入分布式追踪 中看到 Python 中分布式追踪的示例。
要记住的要点
总而言之,请务必牢记以下几点
- 了解指标类型在您的监控系统中的含义
- 了解监控系统希望您的数据使用什么计量单位
- 监控应用程序的最关键组件
- 监控应用程序在其最关键阶段的行为
以上假设您不必管理您的监控系统。如果那是您工作的一部分,您还有很多事情要考虑!
其他资源
以下是我在监控教育旅程中发现非常有用的资源
通用
StatsD/Graphite
Prometheus
- Prometheus 指标类型
- Prometheus 仪表盘如何工作?
- 为什么 Prometheus 直方图是累积的?
- 监控 Python 中的批处理作业
- Prometheus:SoundCloud 的监控
避免错误(即,阶段 3 的学习)
当我们学习监控的基础知识时,重要的是要关注我们不想犯的错误。以下是我遇到的一些有见地的资源
- 如何不测量延迟
- Prometheus 的直方图:一个悲伤的故事
- 为什么平均值很糟糕而百分位数很棒
- 您对延迟的所有了解都是错误的
- 谁移动了我的第 99 个百分位延迟?
- 日志、指标和图表
- HdrHistogram:一种更好的延迟捕获方法
要了解更多信息,请参加 Amit Saha 在 PyCon Cleveland 2018 上的演讲 计数器、仪表盘、上限 90——天哪!。
2 条评论