请求速率监控实例

72 阅读12分钟

我们已经在之前的博文中介绍了基于SLO的请求延迟(持续时间)和请求错误率的预警细节。 然而,如果我们看一下谷歌的《网站可靠性工程》一书,我们会发现它提到了四个黄金信号,可以作为任何系统的预警策略的基础。

  • 延迟(或持续时间)。为一个请求提供服务所需的时间。
  • 错误。请求失败的比率,要么是显性的(例如HTTP 500),要么是隐性的(例如HTTP 200成功响应,但是加上了错误的内容)。
  • 流量(或速率)。衡量对你的系统有多少需求,以高层次的系统特定指标来衡量。对于网络服务,这种测量通常是每秒的HTTP请求。
  • 饱和度。你的服务有多 "满"。对你的系统部分的衡量,强调最受限制的资源(例如,在一个内存受限的系统中,显示内存;在一个I/O受限的系统中,显示I/O)。

在这篇博文中,我们将讨论如何实现流量(或速率)监控。为了简单起见,我们将依靠Backyards,我们的企业级Istio发行版,因为它为其部署的Istio服务网提供了一个预先集成的基于Prometheus的监控解决方案。

它还允许你在你的计算机或集群上尝试我们将使用的例子。要做到这一点,请查看Backyards的快速入门指南或关于使用Backyards开始使用Istio的博文。

环境 🔗︎

要设置基于请求率指标的警报,我们需要首先测量我们的系统如何表现。鉴于正确配置的基于Istio的服务网可以提供所需的指标,我们将假设存在这样一个环境。

如果你的生产环境没有利用服务网,请随时查看我们的博文《使用Backyards定义应用级SLO》。这篇文章详细介绍了通过依靠Prometheus记录规则来抽象实现细节,从应用层面导出这些指标的最佳实践。

在下面的例子中,我们将依赖一个名为istio_requests_total 的普罗米修斯指标。这个指标是由Istio(更确切地说,是Envoy代理)提供的,是一个计数器,包含了一个给定的istio-proxy 容器处理的请求总数。

在这篇文章中,指标上最重要的标签是destination_service_name ,它包含了请求所指向的服务名称,以及destination_service_namespace ,它包含了目标服务所在的命名空间的名称。

Istio Architecture

最后,还有reporter 标签。它的值可以是source ,也可以是destination ,这取决于测量是在调用方还是在Proxy的调用方进行的。在本指南中,我们将使用source-side作为例子,因为它不仅包含了微服务处理请求的时间,还包含了网络传输和连接的开销。这样一来,source-side的测量结果就能更好地代表用户遇到的行为(因为它包括网络延迟等)。

警告:不是所有的服务都有source-side度量。如果有些东西是从服务网外调用的,那么它将只有目标方的报告,这意味着监控将需要依赖destination-side度量。

在我们的例子中,我们将使用Backyards的演示应用程序来展示个别警报。它有以下架构。

Backyards’ demo application’s architectureBackyards’ demo application’s architecture

警报定义的例子将在movies 服务上创建,其命名空间为backyards-demo 。如果你想重复使用这些例子,请用适当的服务名称替换这些常量。

服务水平目标与请求率 🔗︎

自然产生的第一个问题是:"在入口端测量传入的流量就够了吗?"如果我们的目标是确保我们的外部连接是按预期工作的,并且在传入的流量中没有分歧的模式,那就够了。

从上图中可以看出,movies 服务处于我们演示应用的呼叫链的中间位置。然而,我们将这些例子建立在该服务的基础上。

这个决定的原因是,我们的演示应用程序中的每个服务都有自己的服务级别目标和相关的警报集。例如,当考虑以下服务级别目标(SLO)时,它被设置在movies 服务上。

movies 服务应该在30天的滚动期内,在99.9%的情况下,在250毫秒内回答一个请求。

问题是,只有当服务有稳定的流量时,这个SLO才能完美地工作。在服务没有得到任何流量的情况下,SLO会报告说系统正在按预期工作,但系统的可用性仍然可能受到影响。

这让我们看到了在为任何系统实施警报时,应该解决的第一个故障案例。

应该有一种方法来检测流量的完全损失,因为其他警报规则假定流量是稳定的。

对流量损失进行报警 🔗︎

解决这个问题的最明显的办法是,当某个服务在很长一段时间内没有流量时,就发出警报。

对于请求率监控有更好的实现,但是,随着更复杂的算法被讨论,它们往往开始要求流量本身的特定行为。如果服务有一个流量模式,例如,几分钟内,它没有得到任何流量(这被认为是正常的),或者如果服务是一个新的服务,事先没有对流量模式的了解,这个实现通过确保延迟和错误率警报仍然有效来提供一个安全网。

下面的Prometheus查询将触发警报,如果movies 服务在5分钟内的每秒请求数为0。当然,这个时间段需要根据流量特性,对每项服务进行调整。

sum(rate(istio_requests_total{
    reporter="source",
    destination_service_name="movies",
    destination_service_namespace="backyards-demo"
}[5m])) == 0

对丢失吊舱导致的流量损失发出警报 🔗︎

鉴于这些警报是针对新服务的,值得一提的是,还有一种情况是传入速率变为0,也就是没有运行这个特定工作负载的pod(或者pod间通信有问题)。

这种情况可能是由于相当多的原因引起的,比如。

  • 所有的Pod都被OOMKilled了
  • 集群中没有足够的资源来催生一个新的Pod
  • 工人节点的大规模中断(如现货价格变化)
  • 网络问题使Istio sidecar无法提供流量服务
  • 部署策略有问题,有的时候没有活跃的pod

这个列表,绝非详尽无遗,只包含了那些我可以快速列举的问题。从技术上讲,为每一个这样的问题创建警报是可能的,但对我们看到的症状发出警报要容易得多

豆荚没有暴露它们的使用统计数据。

鉴于这个问题可能意味着从单一的错误配置到集群范围内的故障,它是值得提醒的。

在基于Prometheus的警报中处理丢失的数据点 🔗︎

当我们考虑到没有pods服务这个工作负载的可能性时,这也意味着Envoy代理(istio-proxy sidecar)没有运行。普罗米修斯将从该sidecar获取istio_requests_total ,这意味着该指标将不可用。

为了理解为什么这是一个特殊情况,让我们首先尝试理解前面表达式的含义。istio_requests_total{...}[5m] 将产生一个范围向量,它包含了普罗米修斯从所有的pod中刮取数据的最后5分钟的测量值。

标签行内容
{pod="a", reporter="source", ...}。[3.5, 3.3, ...]过去一分钟内所有关于pod所提供的请求数量的时间点测量值a
{pod="b", reporter="source", ...}。[2.5, 2.2, ...]最近一分钟内所有关于pod所提供的请求数的时间点测量值b

rate 函数接收这个范围向量,并对输入向量中的每一行执行速率计算算法。

rate(istio_requests_total{}[5m]) 表达式将产生一个即时向量,它将包含一组标签和与这些标签相关的每一行的值。

标签行内容
{pod="a", reporter="source", ...}。3.5荚的RPS值a
{pod="b", reporter="source", ...}.2.5荚的RPS值b

sum 函数,总结了所有原始数据的价值,并返回一个没有标签的向量,并将总结的价值作为价值。

标签行内容
{}3该服务的RPS值

考虑到这一点,让我们看看这个定义中的条件== 0

sum(rate(istio_requests_total{
    reporter="source",
    destination_service_name="movies",
    destination_service_namespace="backyards-demo"
}[5m])) == 0

如果你对任何编程语言有一点熟悉,你可能会认为这意味着最终结果将是一个布尔值(真或假的值)。但实际上,普罗米修斯的比较运算符是向量运算,也就是说,。

在一个瞬时向量(这里是sum(rate()) 表达式)和一个标量之间,这些运算符被应用于向量中每个数据样本的值。比较结果为假的向量元素,会从结果向量中删除

警报是这样定义的:如果负责警报的表达式被评估为一个非空的即时向量,那么警报就会被触发。在前面的例子中,如果有一个由sum(rate(istio_requests_total)) 表达式返回的0值,== 0 操作员将重新训练0值,警报将被触发。

同样,如果只有一个非零值,== 0 条件将把它从向量中删除,导致一个空向量,因此不会发生警报。

让我们回到我们之前提到的情况:如果没有数据作为给定服务的istio_requests_total 度量的一部分,前面的表达式将不会触发,因为在空向量上调用ratesum 会产生空向量。普罗米修斯会把这种情况解释为一切都完全正常。

为了解决这个问题,除了前面的表达式外,还需要一个表达式,以确保这些值存在于Prometheus中。

absent(
    sum(rate(
        istio_requests_total{
            reporter="source",
            destination_service_name="movies",
            destination_service_namespace="backyards-demo"
        })
    )
)

absent 其工作方式是,如果其参数是一个空向量,它将产生一个包含1值的向量,而如果向量不是空的,它将产生一个空向量。

使用 "静态 "下限阈值发出警报 🔗︎

当然,如果一个服务是日常使用的,仅仅监测总流量损失是不够的。例如,为软件即服务解决方案提供的流量可能与公司的收入额成正比。在这种情况下,瞬间损失50%的流量,如果不被注意,可能会产生灾难性的结果。

通过查看过去几周的流量模式,采取每秒最低请求数,然后降低一点(例如最低请求数的80%),应该可以为我们提供一个相当好的警报阈值。

    threshold = 0.8 * minRPSOverTwoWeeks

在这种情况下,用于警报的表达式就变成了这样。

sum(rate(istio_requests_total{
    reporter="source",
    destination_service_name="movies",
    destination_service_namespace="backyards-demo"
}[5m])) < 13.1

除了我们已经定义的阈值(本例中为13.1 ),该表达式还依赖于一个5m 的回看窗口。这个回看窗口可以增加或减少,以微调这个警报的敏感性。一个较长的窗口意味着在警报触发之前,停电时间需要更长。

这种方法最大的缺点是,随着负载特性的变化,阈值和窗口大小需要定期调整,以适应给定的服务。

使用自动阈值设置进行告警 🔗︎

由于我们刚才提到的设置需要手动调整,让我们尝试一下自动设置。要创建一个自动调整阈值的Prometheus查询,首先我们需要创建一个记录规则,将最后5分钟的请求率作为一个新的指标公开。

apiVersion: monitoring.backyards.banzaicloud.io/v1
kind: PrometheusRule
metadata:
  labels:
    backyards.banzaicloud.io/monitor: "true"
  name: health-statistics
  namespace: backyards-system
spec:
  groups:
  - name: service-source-request-rate
    rules:
    - expr: |
        avg
           sum
           by (destination_service_name, destination_service_namespace)
           (rate(istio_requests_total{reporter="source"}[5m]))
      record: rate:service_request_rate

这个指标将包含诸如以下的行。

标签行内容
{destination_service_name="movies", destination_service_name="backyards-demo" }3.5movies 服务在过去5分钟内测量的源端RPS值。
{destination_service_name="bookings", destination_service_name="backyards-demo" }5bookings 服务在过去5分钟内源端测量的RPS值。

使用这个指标,我们可以计算出该服务上周收到的最小流量(并将最后一小时的流量排除在计算之外)。

min_over_time(
    rate:service_request_rate{
        destination_service_name="movies",
        destination_service_namespace="movies"}[1w:5m] offset 1h)

通过简单地将阈值定义替换成前面例子中的定义,我们就可以看到这个警报表达式。

 sum(rate(
     istio_requests_total{
        reporter="source",
        destination_service_name="movies",
        destination_service_namespace="backyards-demo"
    }[5m])) < 0.8 * min_over_time(
                        rate:service_request_rate{
                            destination_service_name="movies",
                            destination_service_namespace="movies"}[1w:5m] offset 1h)

它所做的是计算过去5分钟的请求率,并将其与上周的最低值进行比较。如果该值低于每周最低值的80%,那么就会触发警报。

在这里,我们使用了一个1小时的偏移量,这样我们就没有把当前的最小值包括在控制样本中,但是让我们看看在一个完整的流量中断之后,阈值会发生什么。

minimum over timeminimum over time

从这张图上可以看出,这种方法有一个严重的缺点:一旦有足够的时间把新的最小值纳入回视窗口(1小时偏移),它就会降低阈值(绿线)。这意味着,在全流量损失持续一周的情况下,新的流量损失将不会被发现。

为了解决这个问题,可以依靠不同的函数来寻找较低的阈值。例如,当使用quantile_over_time 函数而不是min_over_time ,最小值开始表现为这样(绿线显示函数观察到的最小值)。

quantile over timequantile over time

绿线显示了这个阈值函数的行为方式。

quantile_over_time(0.1,
    rate:service_request_rate{
        destination_service_name="movies",
        destination_service_namespace="movies"}[1w:5m] offset 1h)

quantile_over_time 将返回一个仅大于所见样本的10%的阈值。这样,如果系统在无流量的情况下没有花费超过10%的正常运行时间,我们就会看到一个非零的阈值。

缺点是这增加了最小的阈值(因为它排除了10%的最小数据点),因此可能需要调整阈值上的80%修改器。

为什么在高流量条件下发出警报? 🔗︎

在继续讨论寻找这些最小值的更好方法之前,让我们简单谈谈高流量警报的必要性。基于之前的假设,即流量推动了(一些)SaaS公司的利润,人们可能会认为,有更多的流量是好事。

一般来说,有两种情况下高流量是没有帮助的。第一种情况是,如果系统受到DoS或DDoS攻击(要么是恶意的结果,要么是来自有漏洞的客户端或前端代码),这意味着为流量服务所需的额外成本是一种浪费。在这种情况下设置警报,意味着这类问题不会只是幸运地被发现,或在系统过载时被发现,而是及时地被发现。

另一种情况是,即使是现代的Kubernetes服务也只能快速扩展到一定程度。部署可能达到其最大的Horizontal Pod Autoscaler容量,集群可能无法分配更多的节点。在这种情况下,主动检测此类问题可以防止中断,或减少平均恢复时间(MTTR)。

当然,在这种情况下,由系统的所有者决定是否要发布寻呼机,或者在Slack上发出简单的警告,或者在票务系统中发出票据就足够了(因为在工作时间检查出这样的问题可能完全没有问题)。

使用度量统计进行警告 🔗︎

正如我们在网络研讨会上所讨论的,基于静态阈值的警报往往有足够严格的阈值来检测可能需要SRE注意的小偏差。另外,正如你所看到的,计算一个绝对的最小或最大阈值是相当棘手的。

最好的选择是创建某种警报规则,根据它看到的流量模式动态地调整自己。在使用机器学习模型完全疯狂之前,让我们首先尝试一下基于统计分析的方法。

如果我们假设某项服务的请求率形成一个正态分布,我们就可以根据这个分布测试当前的数值。(当然,在现实中,请求率不会是一个完美的正态分布,然而,通过设置好的阈值,它将提供一个良好的近似值)。

正态分布的特点是有两个参数。第一个是其数值的平均值。第二个参数叫做标准差,它是对单个数值偏离平均值的趋势的测量。

在实践中,标准差是衡量测量值离时间序列的平均值有多远。在这个意义上,它们可以被看作是对服务所收到的流量的零星程度的一种测量。这意味着,当使用正态分布对流量模式发出警报时,计算也会考虑到传入流量的突发性,而在使用静态阈值时,情况并非如此。

正态分布非常适合计算这些阈值的原因是,它们有一个属性,即99.7%的测量值(如果它们来自有关的正态分布)将在3*StdandardDeviation - MeanMean + 3*StandardDeviation

由于Prometheus为任何时间序列提供了stddevavg 函数,我们可以很容易地创建一个警报规则,如果流量与昨天的数据相比增加了40%以上,就会触发警报。

rate:service_request_rate{...} >
    1.4 * (
        3*stddev_over_time(rate:service_request_rate{...}[1d:5m] offset 1h) + avg_over_time(rate:service_request_rate{...}[1d:5m] offset 1h))

注意:我们还是加入了1小时的偏移量,这样我们检查的数值就不会干扰到我们测量的分布。

由于实际测量结果不能保证形成正态分布,实际分布将只是一个近似值,在极端情况下甚至可以提供一个负的下限阈值,所以除了基于阈值的报警方案外,这种报警也是至关重要的。

另外,值得一提的是,基于服务的流量周期性,额外的每周规则可能是有意义的。

高级模型 🔗︎

可以使用算法来预测流量率的趋势。这种预测库的一个很好的例子是Facebook的Prophet库

该库不涉及机器学习,相反,它使用多种统计模型来估计每日、每周和每年的周期性对数据系列的影响。

即使依靠这样的解决方案来发出警报似乎很诱人,但主要问题是反应时间。训练模型和获取单个服务的速率行为的预测,在一个CPU核心上需要一分钟,而且随着时间的推移,预测开始变得越来越不确定。

Prophet showcasing an uncertainity in trends. Source: <a href="https://facebook.github.io/prophet/docs/uncertainty_intervals.html#uncertainty-in-the-trend">Prophet Python Guide</a>Prophet showcasing an uncertainity in trends. Source: <a href="https://facebook.github.io/prophet/docs/uncertainty_intervals.html#uncertainty-in-the-trend">Prophet Python Guide</a>

当你考虑到,由于Prometheus的存在,Backyards在5秒内就能解决这样的指标,仅仅训练一个服务就需要1分钟,这似乎太多。

此外,这类系统需要历史数据来学习,这意味着如果没有几天的数据,警报将是不可靠的。这些数据还应该没有任何不正常的现象(如停电),这样就不会被当作正常行为。

这样一来,这种系统只应作为前面所介绍的方法的延伸,而且只有在这些警报策略按预期工作时才可使用。

结论 🔗︎

正如本博客所详述的,一个正确实施的请求率监控解决方案对于保证延迟和基于错误率的SLO的正确性至关重要。这样的系统应该由多个警报组成,彼此分层,以解决不同的情况。

一个最低限度的方法将包括对我们讨论的每个流量损失情况的警报,与更严格的基于阈值的警报解决方案(静态阈值、动态阈值、基于统计的阈值)相搭配。

复杂的解决方案,如预测,或(至少在最初)基于统计的阈值,更适合用于检测外围的流量模式,而不是日常的警报做法。