使用 Backyards定义应用层面的SLOs的方法

108 阅读14分钟

Backyards(现在的Cisco Service Mesh Manager)提供了开箱即用的、基于服务等级指标的监控和警报解决方案,用于监控基于HTTP/GRPC的微服务延迟和错误率特征。之所以决定只提供这些服务水平指标作为基本安装的一部分,是因为只有Istio本身能够提供它们所需的指标。另一方面,正如我们在跟踪和执行SLO网络研讨会上简要讨论的那样,基于SLO的系统测量不能也不应该局限于这三种测量。

为了解决这个问题,Backyards的SLO实施是在保持可扩展性的前提下创建的。在这篇博文中,我们将介绍如何利用Backyards进行基于SLO的警报,只使用预先存在的Prometheus实例和运行应用程序所提供的指标。

服务水平目标 🔗︎

正如我们在之前的博文《跟踪和执行SLO》中所详述的,SLO是一个服务水平目标:一个服务水平的目标值或值范围,由服务水平指标(SLI)来衡量。这个定义意味着,定义你自己的SLO主要是为你的工作负载提供正确的SLI的问题。

从本质上讲,SLI就是你用来衡量服务水平的东西。例如,一个SLI可以是。

  • HTTP请求的成功率。
  • 低于某个延迟阈值的请求的百分比。
  • 一个服务的可用时间,或
  • 任何其他以某种方式定量描述服务状态的指标。

在Backyards中,我们将SLI表述为两个数字的比率:好的事件除以总事件。这样,SLI值将在0和1之间(或0%和100%),它很容易与SLO值相匹配,后者通常被定义为特定时间段内的目标百分比。前面的例子都是按照这种做法。

服务水平指标(模板) 🔗︎

无论现代基于微服务的架构在涉及到编程语言时是相当多样化的,还是开发人员倾向于使用框架并倾向于更多经过验证的、经过测试的解决方案,不同的框架和语言的数量往往不会随着时间而大量增加。

目前大多数编程语言和(后端)开发框架都提供了自己的Prometheus度量输出器的实现。即使这些实现产生了不同的度量,由于框架的差异较小,多个服务将可能共享相同的度量结构。

为了利用这一事实,并最大限度地提高监控堆栈中的可重用性,Backyards依赖于一个名为ServiceLevelIndicatorTemplate的自定义资源。

让我们看一下Backyards中预定义的http-requests-success-rate 服务水平指标。在仪表板上,可以找到以下SLO表格。

<code>http-requests-success-rate</code> on the user interface<code>http-requests-success-rate</code> on the user interface

在后台,这个用户界面是由这个自定义资源构建的。

apiVersion: sre.banzaicloud.io/v1alpha1
kind: ServiceLevelIndicatorTemplate
metadata:
  name: http-requests-success-rate
  namespace: backyards-system
spec:
  goodEvents: |
    sum(rate(
      istio_requests_total{reporter="{{ .Params.reporter }}", destination_service_namespace="{{ .Service.Namespace }}", destination_service_name="{{ .Service.Name }}",response_code!~"5[0-9]{2}|0"}[{{ .SLO.Period }}]
    ))
  totalEvents: |
    sum(rate(
      istio_requests_total{reporter="{{ .Params.reporter }}", destination_service_namespace="{{ .Service.Namespace }}", destination_service_name="{{ .Service.Name }}"}[{{ .SLO.Period }}]
    ))
  kind: availability
  description: |
    Indicates the percentages of successful (non 5xx) HTTP responses compared to all requests
  parameters:
  - default: source
    description: the Envoy proxy that reports the metric (source | destination)
    name: reporter

该示例资源描述了使用Go模板字符串计算goodEventstotalEvents 的数量所需的PromQL查询,允许Backyards在不同的情况下重复使用SLI(如计算SLO的合规性值或计算用于警报的预测错误率)。

为了实现代码重用,服务水平目标的自定义资源只引用这些模板,从而在应用端产生更简洁的描述。例如,Backyards的演示应用程序中包含的Frontpage将这个ServiceLevelObjective自定义资源定义为。

apiVersion: sre.banzaicloud.io/v1alpha1
kind: ServiceLevelObjective
metadata:
  name: frontpage-30d-rolling-availability
  namespace: backyards-demo
spec:
  description: HTTP request success rate should be above 99.9% for a rolling period of 30 days
  selector:
    name: frontpage
    namespace: backyards-demo
  sli:
    parameters:
      reporter: destination
    templateRef:
      name: http-requests-success-rate
      namespace: backyards-system
  slo:
    goal: "99.9"
    rolling:
      length: 720h

正如你所看到的,.spec.slo 定义了SLO的窗口大小和目标,使我们能够轻松了解SLO的属性,而不需要进行大量的PromQL查询。.spec.selector 指定了这个SLO指的是哪个Kubernetes服务,而.spec.sli 包含了从模板本身实例化SLI所需的信息。

要了解良好事件的数量是如何计算的,首先需要检查spec.sli.templateRef :该值指定了要使用哪个SLI模板。

良好事件的PromQL模板看起来像这样。

sum(rate(
      istio_requests_total{reporter="{{ .Params.reporter }}", destination_service_namespace="{{ .Service.Namespace }}", destination_service_name="{{ .Service.Name }}",response_code!~"5[0-9]{2}|0"}[{{ .SLO.Period }}]
    ))

模板参数是使用SLO自定义资源计算的。在前面的例子中,这些值的评估方式如下。

变量意义例子中的值
.Service.Namespace此SLI测量的服务的命名空间backyards-demo
.Service.Name该SLI测量的服务名称frontpage
.SLO.Period计算SLI的时间范围基于用量的变量
.Params.reporter来自ServiceLevelObjective自定义资源的参数值destination

parameters 功能还允许在 SLI 需要额外的配置选项(例如延迟阈值,或者像本例中那样,报告人特使代理的名称)时扩展 UI。

例子。一个测试应用

为了演示如何定义一个自定义的服务水平指标模板,让我们假设我们有一个支付系统的工人池应用程序,它处理那些与支付流程相关的后台活动(如发送欢迎邮件,在后台确认交易,或尝试更新用户的许可证)。

这个应用程序是用Python写的,并使用Celery作为处理作业的框架。我们想实现一个SLO,确保这些后台处理任务有99.9%的运行成功。

Celery带有一个提供Prometheus度量的模块,叫做celery-exporter。这个导出器以下列格式公开了队列的成功统计。

celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="RECEIVED"} 0.0
celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="PENDING"} 2.0
celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="STARTED"} 0.0
celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="RETRY"} 0.0
celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="FAILURE"} 5.0
celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="REVOKED"} 0.0
celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="SUCCESS"} 13.0

这些是应用程序暴露的 "原始 "度量。构建SLI模板的第一步是了解我们如何区分属于这个服务的celery_tasks_total 和可能正在运行的其他服务。

普罗米修斯的工作方式是,当从一个给定的服务中获取指标时(刮取服务),它可以通过添加标签来丰富返回的指标。普罗米修斯操作员进一步提供了一个有主见的标签列表,以应用于这些刮削的度量。

让我们说,该应用是一个简单的基于Kubernetes的应用,前面有一个基本的deployment ,还有一个service

apiVersion: v1
kind: Service
metadata:
  name: payments-worker
  namespace: payments
spec:
  selector:
    app: payments-worker
  ports:
    - protocol: TCP
      port: 8080
      name: metrics
      targetPort: 8080

首先,普罗米修斯操作员应该被配置为刮取服务。这可以通过定义一个新的ServiceMonitor来完成,该ServiceMonitor指示Prometheus开始刮取特定服务背后的Pod。

apiVersion: v1/monitoring.coreos.com
kind: ServiceMonitor
metadata:
  name: payments-worker
  namespace: payments
spec:
  endpoints:
  - port: metrics
    path: /metrics
  namespaceSelector:
    matchNames: [ "payments" ]
  selector:
    matchLabels:
      app: payments-worker

这个ServiceMonitor自定义资源确保附属于payments-worker 服务的Pod(作为与服务本身的标签相匹配的.spec.selector.matchLabels )将被刮取。当这个刮削配置被Prometheus操作员处理时,前面提到的度量将被一些额外的标签所充实,如namespaceservicepod ,这些标签包含了属于特定度量行的命名空间、服务名称和pod的名称。因此,上述指标--当从Prometheus方面查询时--将看起来像这样。

celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="SUCCESS",service="payments-worker", namespace="payments", "pod"="payments-worker-9x2wb"} 0.0
celery_tasks_total{name="my_app.tasks.calculate_something",namespace="celery",queue="celery",state="FAILURE",service="payments-worker", namespace="payments", "pod"="payments-worker-9x2wb"} 0.0

要定义一个新的服务水平指标,我们需要有两个指标:总事件数和良好事件数。下面的ServiceLevelIndicatorTemplate 自定义资源规定了这一点。

apiVersion: sre.banzaicloud.io/v1alpha1
kind: ServiceLevelIndicatorTemplate
metadata:
  name: celery-success-rate
  namespace: payments
spec:
  goodEvents: |
    sum(celery_tasks_total{service="{{ .Service.Name }}", namespace="{{ .Service.Namespace }}", state="SUCCESS"})
  totalEvents: |
    sum(celery_tasks_total{service="{{ .Service.Name }}", namespace="{{ .Service.Namespace }}", state=~"SUCCESS|FAILURE"})
  kind: availability
  description: |
    Indicates the percentages of successfully executed Celery jobs
  parameters: []

应用该自定义资源后,SLI模板在Backyards Dashboard上变得可用。

<code>celery-success-rate</code> is now available on the user interface<code>celery-success-rate</code> is now available on the user interface

鉴于该指标仅在集群的Prometheus运营商上可用,我们需要确保Backyards能够访问该指标。但在关注这个问题之前,让我们快速绕道而行:通常,有多个工人队列服务组成了这样一个复杂的服务,可能使用不同的框架实现。

普罗米修斯记录规则作为抽象层 🔗︎

前面例子中的celery-success-rate SLI模板只在使用Celery时有效。如果有多个框架在使用,最好有一个统一的SLI模板,可以在多个服务之间重复使用。

大体上,Prometheus提供了一个叫做记录规则的功能,它允许定义一组查询,其结果将被保存为一个不同的指标,可供以后处理。

celery_tasks_total基于之前定义一个记录规则的例子,该规则将产生来自worker_successful_jobs-metric,我们在celery-exporter的基础上创建一个抽象。这允许我们创建一个SLI模板,可以重复用于Celery以外的工作引擎。

这可以通过添加下面的记录规则来实现。

apiVersion: v1/monitoring.coreos.com
kind: PrometheusRule
metadata:
  name: celery-workers
  namespace: prometheus
spec:
 groups:
 - name: celery.rules
   rules:
   - expr: sum(celery_tasks_total{state="SUCCESS"}) by (pod, service, namespace)
     record: worker_successful_jobs
     labels:
       engine: "celery"
   - expr: sum(celery_tasks_total{state~="SUCCESS|FAILURE"}) by (pod, service, namespace)
     record: worker_total_jobs
     labels:
       engine: "celery"

这个Prometheus规则的结果是worker_total_jobsworker_successful_jobs 指标,其中包含了所有基于Celery的指标,直到Pod级别。现在我们可以重写之前显示的SLI模板,如下。

apiVersion: sre.banzaicloud.io/v1alpha1
kind: ServiceLevelIndicatorTemplate
metadata:
  name: worker-success-rate
  namespace: payments
spec:
  goodEvents: |
    sum(worker_successful_jobs{service="{{ Service.Name }}", namespace="{{ Service.Namespace }}"})
  totalEvents: |
    sum(worker_total_jobs{service="{{ Service.Name }}", namespace="{{ Service.Namespace }}"})
  kind: availability
  description: |
    Indicates the percentages of successfully executed Celery jobs
  parameters: []

如果你要在系统中引入另一个工作者框架,所有现有的SLI模板都可以为这些服务重用,只需实现一个类似的记录规则集。这开启了更多的可能性,如集群内所有工人服务的统一仪表板。

将数据提供给Backyards 🔗︎

Backyards有自己的Prometheus和Thanos部署(当以高可用模式部署时),服务水平目标相关功能预计将通过我们的Prometheus部署提供数据。

依靠Backyards的Prometheus来存储应用级指标可能很诱人,但我们认为在生产环境中拥有单独的Prometheus实例来存储应用级指标是最佳做法。这样做的原因有两个:首先,它有助于隔离领域的故障。人们普遍认为,监控系统应该是任何基础设施中最可用的部分。而且,如果应用指标对Prometheus的负荷过重,基于Istio的警报仍将发挥作用,在故障的Prometheus实例被修复时继续发送警报。

第二个原因与Prometheus的扩展特性有关:它应该通过分片(更多的Prometheus实例刮取你系统的不同部分)来扩展,或者通过增加Prometheus可支配的存储、CPU和内存资源来垂直扩展。鉴于在使用现代云供应商时,你是计算和存储资源方面几乎无限弹性的受益者,垂直扩展似乎很诱人。然而,采用这种方法,问题就变成了Prometheus的启动时间:当重新启动时,它需要加载其数据库的某些部分,还需要读取Write Ahead Log(它存储了两个小时的数据,以提高写入性能)。在摄入量大的Prometheus实例上,这可能转化为读取多个千兆字节的数据。更糟糕的是,在这个过程中,Prometheus无法刮取或摄取任何数据,使Kubernetes集群在高可用部署中只有一个工作的Prometheus实例。

为了实现分片设置,我们首先建议你了解Backyards的监控架构(在高可用模式下部署时)。

通常,高可用普罗米修斯设置的第一个实现是两个普罗米修斯实例刮取相同的服务,每个实例维护自己的时间序列数据库。当发出警报时,两个Prometheus实例(如果可能的话)将向一个警报管理器发出警报,该管理器将在向上游发送警报之前对警报进行重复计算。

这种方法的问题(从可靠性的角度看是个好处)是这两个普罗米修斯实例不共享一个共同的状态。让我们看看如果一个普罗米修斯短暂停机(几分钟),而另一个普罗米修斯正在处理警报任务会发生什么。从表面上看,一切都会像预期的那样工作,但这只是这种设置的结果,它使用警报来回顾过去几分钟的情况来决定系统行为。例如,看看这个警报定义。

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: example
  namespace: backyards-demo
spec:
  groups:
  - name: example_latency_alert
    rules:
    - alert: example-alert
      expr: histogram_quantile(0.9, rate(demo_api_request_duration_seconds_bucket{job="demo"}[5m])) > 0.05

正如你所看到的,警报规则取决于最后5分钟的数据(rate(demo_api_request_duration_seconds_bucket{job="demo"}[5m])。每当这个时间过去,并且两个Prometheus实例都有可用的数据,警报就会在两个Prometheus实例上恢复相同的行为。

当涉及到基于SLO的警报时,使用跨越多个小时的回望窗口是很常见的。而对于基于SLO的决策,我们可能需要以稳定和可靠的方式计算几个月的服务水平指标。这意味着,对于Backyards监控架构,我们将依靠Thanos Query来重复和同步Prometheus实例的状态,确保这种一致的行为。

Backyards’ Monitoring ArchitectureBackyards’ Monitoring Architecture

正如你在这个架构图上看到的,Backyards的工作方式是这样的:它总是使用Thanos Query来稳定度量,并确保没有数据点丢失。

例子:使用来自外部普罗米修斯的数据 🔗︎

如前所述,用户界面已经清楚地告诉我们,Backyards并没有收到我们已经从服务中导出的数据。

因为Backyards使用Thanos Query来规范数据,在这篇博文中,我们还将利用它的能力来查询多个Prometheus实例:我们将添加一个由Prometheus操作员管理的 "库存 "Prometheus实例,作为Backyards的Thanos Query的存储。这样做的好处是,我们将不需要在两个地方存储相关数据,Thanos Query将简单地从正确的Prometheus实例中查询数据。

在这篇博文中,我们将使用一个新的安装,并使用kube-prometheus-stackHelm图表来展示这个过程。如果你的系统已经在运行该图表的安装,你唯一需要做的就是为Helm版本设置prometheus.prometheusSpec.thanos.listenLocal=false 。禁用这个设置将允许Thanos Query of Backyards访问你的Prometheus部署上的Thanos Sidecar组件来进行查询流量。

要安装一个新的Prometheus堆栈进行实验,请执行这些命令。

kubectl create ns prometheus  # Let's create a namespace for the new Prometheus deployment
kubectl config set-context $(kubectl config current-context) --namespace prometheus # switch to the freshly created namespace
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus-stack prometheus-community/kube-prometheus-stack --set 'prometheus.prometheusSpec.thanos.listenLocal=false' # install Prometheus operator with the default settings

对于这篇博文(因为Prometheus命名空间没有istio.io/rev 标签),Prometheus实例将不在网状结构内,因此这个设置只有在你没有在集群上执行MTLS的情况下才会有效。

为了验证这个新部署的Prometheus实例是否在工作,让我们快速检查一下警报管理器的指标是否可用。

$ k port-forward prometheus-prometheus-stack-kube-prom-prometheus-0 9090 &; PF_PID=$!
$ curl "http://127.0.0.1:9090/api/v1/query?query=alertmanager_alerts" | jq .
{
  "status": "success",
  "data": {
    "resultType": "vector",
    "result": [
      {
        "metric": {
          "__name__": "alertmanager_alerts",
          "endpoint": "web",
          "instance": "10.20.6.186:9093",
          "job": "prometheus-stack-kube-prom-alertmanager",
          "namespace": "prometheus",
          "pod": "alertmanager-prometheus-stack-kube-prom-alertmanager-0",
          "service": "prometheus-stack-kube-prom-alertmanager",
          "state": "suppressed"
        },
        "value": [
          1601391763.841,
          "0"
        ]
      }
    ]
  }
}
$ kill $PF_PID

正如你所看到的,工作prometheus-stack-kube-prom-alertmanager ,已经为正在安装在prometheus 命名空间中的alertmanager提供了一个数据点。

最后一步是通过改变Backyards的controlplane 配置资源,确保Backyards能够利用这个Prometheus实例作为数据源。

$ cat > controlPlanePatch.yaml <<EOF
spec:
  backyards:
    prometheus:
      thanos:
        query:
         additionalStores:
         - prometheus-prometheus-stack-kube-prom-prometheus-0.prometheus-operated.prometheus.svc.cluster.local:10901
EOF
$ kubectl patch controlplane backyards -p "$(cat controlPlanePatch.yaml)" --type=merge

在应用补丁后,应调用backyards CLI来更新被改变的Kubernetes资源。

$ backyards operator reconcile

核对完成后,你应该能够使用此命令从backyards查询相同的度量(假设你的backyards dashboard 命令正在运行)。

$ curl "http://127.0.0.1:50500/prometheus/api/v1/query?query=alertmanager_alerts" | jq .
{
  "status": "success",
  "data": {
    "resultType": "vector",
    "result": [
      {
        "metric": {
          "__name__": "alertmanager_alerts",
          "endpoint": "web",
          "instance": "10.20.6.186:9093",
          "job": "prometheus-stack-kube-prom-alertmanager",
          "namespace": "prometheus",
          "pod": "alertmanager-prometheus-stack-kube-prom-alertmanager-0",
          "service": "prometheus-stack-kube-prom-alertmanager",
          "state": "suppressed"
        },
        "value": [
          1601391763.841,
          "0"
        ]
      }
    ]
  }
}

你也可以通过检查Backyards的仪表板中的拓扑视图来验证连接是否正常。

monitoring topology with external Prometheusmonitoring topology with external Prometheus

结论 🔗︎

如上所示,Backyards的服务水平目标功能不仅为基于Istio的警报提供了开箱即用的支持,而且,如果实施得当,可以作为任何基于SLO的监控工作的支柱,并以这样一种方式来执行Prometheus本身的最佳实践。

为了更容易适应,它可以很容易地与您现有的监控解决方案对接,并提供一个灵活的框架来定义为您的应用量身定做的定制服务水平指标。