本文发布于Cylon的收藏册,转载请著名原文链接~


Kubernetes监控架构设计

k8s监控设计背景说明

根据 Kubernetes监控架构 1,Kubernetes 集群中的 metrcis 可以分为 系统指标 (Core Metrics) 和 服务指标 (service metrics) ; 系统指标(System metrics) 是通用的指标,通常可以从每一个被监控的实体中获得(例如,容器和节点的CPU和内存使用情况)。服务指标(Service metrics) 是在应用程序代码中显式定义并暴露的 (例如,API Server 处理的 500 错误数量)。

Kubernetes将系统指标分为两部分:

  • 核心指标 (core metrics) 是 Kubernetes 理解和用于其内部组件和核心工具操作的指标,例如:用于调度的指标 (包括资源估算算法的输入, 初始资源/VPA (vertical autoscaling),集群自动扩缩 (cluster autoscaling),水平Pod自动扩缩 (horizontal pod autoscaling ) 除自定义指标之外的指标);Kube Dashboard 使用的指标,以及 “kubectl top” 命令使用的指标。
  • 非核心指标 (non-core metrics) 是指不被 Kubernetes 解释的指标。我们一般假设这些指标包含核心指标 (但不一定是 Kubernetes 可理解的格式),以及其他额外的指标。

所以,kubernetes monitoring 的架构被设计拥有如下特点:

  • 通过标准的主 API (当前为主监控 API) 提供关于Node, Pod 和容器的核心系统指标,使得核心 Kubernetes 功能不依赖于非核心组件
  • kubelet 只导出有限的指标集,即核心 Kubernetes 组件正常运行所需的指标。

监控管道

Kubernetes 监控管道分为两个:

  • 核心指标管道 (core metrics pipeline) 由 Kubelet、资源估算器, 一个精简版 Heapster (metrics-server),以及 api-server 中 master metrics API 组成。这些指标被核心系统组件使用,例如调度逻辑(如调度器和基于系统指标的HPA)和一些简单 UI 组件(如 kubectl top),这个管道并不打算与第三方监控系统集成。
  • 监控管道:一个用于收集系统中的各种指标并将其暴露给最终用户端,以及通过适配器暴露给 HPA(用于自定义指标) 和 Infrastore 的。用户可以选择多种监控系统供应商(例如 Prometheus, metric-server),也可以完全不使用。

Core Metrics Pipeline

根据 kubernetes 监控设计文档可以得知,核心指标指

  • 使用这组核心指标,由Kubelet收集,并仅供 Kubernetes 系统组件使用,支持"第一类资源隔离和利用特性"。
  • 不设计成面向用户的 API,而是尽可能通用,以支持未来的用户级组件。

核心指标的包含三类:

  • CpuUsage: 记录从创建对象开始的累计CPU使用时间。
  • MemoryUsage: 记录工作集内存使用量。
  • FilesystemUsage: 记录文件系统使用情况,包括已用字节数和已用Inode数。

Monitoring Pipeline

根据 Kubernetes 监控设计文档 1 得知,监控管道用于与核心Kubernetes组件分开的系统,可以更加灵活。并且监控管道可以收集不同类型的指标:

  • Core system metrics
  • Non-core system metrics
  • Service metrics from user application containers
  • Service metrics from Kubernetes infrastructure containers (using Prometheus instrumentation)

监控管道主要用于根据自定义指标进行 HPA,监控管道提供了一个无状态的 API Adapter,用于拉去监控给 HPA

指标API

API类别

根据监控架构设计文档,Kubernetes 定义了两套指标 API,资源指标 API 和 自定义指标 API;Kubernetes 为资源指标 API 提供了两种实现:Heapster 和 metrics-server,而自定义指标 API 由不同的监控供应商实现。下面将详细描述每个 API。

  • 资源指标 API (Resource Metrics API):该 API 允许消费者访问 Pod 和 Node 的资源指标(CPU & Memory)

    • The API is implemented by metrics-server and prometheus-adapter.
  • 自定义指标 API (Custom Metrics API):该 API 允许消费者访问描述 Kubernetes 资源的任意指标。

API的访问

资源指标,该 API 是在 /apis/metrics.k8s.io/ ,可以使用 kubectl proxy --port 8080 代理后进行访问,

$ kubectl proxy --port=8080
$ curl localhost:8080/apis/metrics.k8s.io/v1beta1/nodes

或者使用 kubectl get --raw 进行获取

$ kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" | jq 

自定义指标,该 API 是在 /apis/custom.metrics.k8s.io/ ,访问的方式相同,用户通过该 PATH 进行访问。

# 查看有哪些指标可用
$ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/" | jq

Prometheus-adapter

通过上一章节介绍了kubernetes监控体系,这已经可以了解到了 prometheus-adapter 的定位;prometheus-adapter 是通过 kubernetes custom-metrics-apiserver 标准实现的一个 custom.metrics.k8s.io API,用于提供给 HPA 的一种指标适配器,可以将任何指标转化为 HPA 可用的指标。他全名为 Kubernetes Custom Metrics Adapter for Prometheus

prometheus-adapter配置文件详解

prometheus-adapter负责确定哪些指标以及如何去发现这些指标,根据这个标准,配置文件分为四个步骤来完成这套 “发现” 规则

每一个指标可以大致分为四个部分,对应在配置文件中:

  • Discovery ,用于指定 adapter 应如何查找此规则的所有Prometheus指标。
  • Association ,用于指定 adapter 应如何确定特定指标与哪些 Kubernetes 资源相关联。
  • Naming ,用于指定 adapter 应如何在自定义指标 API 中公开该指标。
  • Querying ,用于指定如何将针对一个或多个 Kubernetes 对象的特定指标请求转换为对 Prometheus 的查询。

配置文件如下所示,这是官方给出的样板配置文件(文章编写时版本为0.12)

rules:
# Each rule represents a some naming and discovery logic.
# Each rule is executed independently of the others, so
# take care to avoid overlap.  As an optimization, rules
# with the same `seriesQuery` but different
# `name` or `seriesFilters` will use only one query to
# Prometheus for discovery.

# some of these rules are taken from the "default" configuration, which
# can be found in pkg/config/default.go

# this rule matches cumulative cAdvisor metrics measured in seconds
- seriesQuery: '{__name__=~"^container_.*",container!="POD",namespace!="",pod!=""}'
  resources:
    # skip specifying generic resource<->label mappings, and just
    # attach only pod and namespace resources by mapping label names to group-resources
    overrides:
      namespace: {resource: "namespace"}
      pod: {resource: "pod"}
  # specify that the `container_` and `_seconds_total` suffixes should be removed.
  # this also introduces an implicit filter on metric family names
  name:
    # we use the value of the capture group implicitly as the API name
    # we could also explicitly write `as: "$1"`
    matches: "^container_(.*)_seconds_total$"
  # specify how to construct a query to fetch samples for a given series
  # This is a Go template where the `.Series` and `.LabelMatchers` string values
  # are available, and the delimiters are `<<` and `>>` to avoid conflicts with
  # the prometheus query language
  metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container!="POD"}[2m])) by (<<.GroupBy>>)"

# this rule matches cumulative cAdvisor metrics not measured in seconds
- seriesQuery: '{__name__=~"^container_.*_total",container!="POD",namespace!="",pod!=""}'
  resources:
    overrides:
      namespace: {resource: "namespace"}
      pod: {resource: "pod"}
  seriesFilters:
  # since this is a superset of the query above, we introduce an additional filter here
  - isNot: "^container_.*_seconds_total$"
  name: {matches: "^container_(.*)_total$"}
  metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container!="POD"}[2m])) by (<<.GroupBy>>)"

# this rule matches cumulative non-cAdvisor metrics
- seriesQuery: '{namespace!="",__name__!="^container_.*"}'
  name: {matches: "^(.*)_total$"}
  resources:
    # specify an a generic mapping between resources and labels.  This
    # is a template, like the `metricsQuery` template, except with the `.Group`
    # and `.Resource` strings available.  It will also be used to match labels,
    # so avoid using template functions which truncate the group or resource.
    # Group will be converted to a form acceptible for use as a label automatically.
    template: "<<.Resource>>"
    # if we wanted to, we could also specify overrides here
  metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container!="POD"}[2m])) by (<<.GroupBy>>)"

# this rule matches only a single metric, explicitly naming it something else
# It's series query *must* return only a single metric family
- seriesQuery: 'cheddar{sharp="true"}'
  # this metric will appear as "cheesy_goodness" in the custom metrics API
  name: {as: "cheesy_goodness"}
  resources:
    overrides:
      # this should still resolve in our cluster
      brand: {group: "cheese.io", resource: "brand"}
  metricsQuery: 'count(cheddar{sharp="true"})'

# external rules are not tied to a Kubernetes resource and can reference any metric
# https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-metrics-not-related-to-kubernetes-objects
externalRules:
- seriesQuery: '{__name__="queue_consumer_lag",name!=""}'
  metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>}) by (name)
- seriesQuery: '{__name__="queue_depth",topic!=""}'
  metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>}) by (name)
  # Kubernetes metric queries include a namespace in the query by default
  # but you can explicitly disable namespaces if needed with "namespaced: false"
  # this is useful if you have an HPA with an external metric in namespace A
  # but want to query for metrics from namespace B
  resources:
    namespaced: false

# TODO: should we be able to map to a constant instance of a resource
# (e.g. `resources: {constant: [{resource: "namespace", name: "kube-system"}}]`)?

Discovery

Discovery 部分控制了查找要在自定义指标 API 中公开的指标的过程。其中有两个关键字段:seriesQueryseriesFilters

seriesQuery 指定了用于查找某些 Prometheus series 的 Prometheus series 查询(作为传递给 Prometheus /api/v1/series)。适配器将从这些系列中剥离标签值,然后在后续步骤中使用得到的“指标名称—标签名称”的组合。

在许多情况下,seriesQuery 就足以缩小 Prometheus series 的列表。但有时(特别是当两个规则可能重叠时),对指标名称进行额外的过滤是很有用的。在这种情况下,可以使用 seriesFilters。在从 seriesQuery 返回 series 列表后,每个 series 的指标名称都会通过指定的任何过滤器进行过滤。

过滤器可以是以下两种形式之一:

  • is: ,匹配名称符合指定正则表达式的任何序列。
  • isNot: ,匹配名称不符合指定正则表达式的任何序列。

例如

# match all cAdvisor metrics that aren't measured in seconds
seriesQuery: '{__name__=~"^container_.*_total",container!="POD",namespace!="",pod!=""}'
seriesFilters:
  - isNot: "^container_.*_seconds_total"

Association

Association 部分控制了确定序列指标可以附加到哪些 Kubernetes 资源的过程。resources 字段控制了这个过程。

有两种方式来关联资源与特定指标。在这两种情况下,标签的值都会成为特定对象的名称。

一种方式是指定,任何符合某个特定模式的标签名称都指向基于标签名称的某个“group_resource”。这可以使用 template 字段来完成。pattern 被指定为一个 Go 模板,其中 Group 和 Resource 字段分别代表“组”和“资源”。

# any label `kube_<group>_<resource>` becomes <group>.<resource> in Kubernetes
resources:
  template: "kube_<<.Group>>_<<.Resource>>"

另一种方式是指定某个特定标签代表某个特定的 Kubernetes 资源。这可以使用 overrides 字段来完成。每个 override 将一个 Prometheus 标签映射到一个 Kubernetes group-resource。例如:

# the microservice label corresponds to the apps.deployment resource
resources:
  overrides:
    microservice: 
      group: "apps"
      resource: "deployment"

Association 部分提供了两种关联 Prometheus 指标和 Kubernetes 资源的方式,可以根据需要灵活地组合使用。这是实现自定义指标 API 的关键一环。

Naming

Naming 部分控制了将 Prometheus 指标名称转换为自定义指标 API 中的指标,这是通过 name 字段来实现的。

Naming 的控制通过指定一个从 Prometheus 名称中提取 API 名称的模式,以及对提取值进行的可选转换来实现。

模式由 matches 字段指定,这是一个正则表达式。如果没有指定,它默认为 .* 。

转换由 as 字段指定。你可以使用 matches 字段中定义的任何捕获组。如果 matches 字段没有捕获组,as 字段默认为 $0 。如果只包含一个捕获组,as 字段默认为 $1 。否则,如果没有指定 as 字段就会出错。例如

# match turn any name <name>_total to <name>_per_second
# e.g. http_requests_total becomes http_requests_per_second
name:
  matches: "^(.*)_total$"
  as: "${1}_per_second"

Querying

Querying 部分控制了实际获取特定指标值的过程。它由 metricsQuery 字段来控制。

metricsQuery 字段是一个 Go 模板,它会被转换成一个 Prometheus 查询,使用从特定的自定义指标 API 调用获取的输入数据。对自定义指标 API 的一次调用会被简化为一个指标名称、一个 “group-resource” 和一个或多个该 “group-resource” 的对象。这些会被转换成模板中的以下字段:

  • Series: 指标名称
  • LabelMatchers: 一个逗号分隔的标签匹配器列表,匹配给定的对象。当前包括特定的 “group-resource” 标签,以及 namespace 标。
  • GroupBy: 一个逗号分隔的用于分组的标签列表。当前包括用于 LabelMatchers 的组-资源标签。

例如,假设我们有一个 http_requests_total 序列 (在 API 中公开为 http_requests_per_second ),具有 service、pod、ingress、namespace 和 verb 标签。前四个对应于 Kubernetes 资源。那么,如果有人请求了 pods/http_request_per_second 指标,那么针对 somens 命名空间中的 pod1 和 pod2,我们会有:

  • Series: “http_requests_total”
  • LabelMatchers: "pod=~"pod1|pod2",namespace="somens""
  • GroupBy: pod

对应 prometheus promql 如下所示

sum(http_requests_total{pod=~"pod1|pod2",namespace="somens"}) by (pod)

此外,还有两个高级字段是其他字段的"原始"形式:

  • LabelValuesByName: 映射。将 LabelMatchers 字段中的标签和值对应起来。值是用 | 预先连接的 (用于在 Prometheus 中使用 =~ 匹配器)。
  • GroupBySlice: GroupBy 字段的切片形式。

通常,我们可能会想使用 Series、LabelMatchers 和 GroupBy 字段。其他两个是用于高级用法的。

Querying 预计会为每个请求的对象返回一个值。适配器会使用返回的系列上的标签,将给定的系列关联回其相应的对象。例如:

# convert cumulative cAdvisor metrics into rates calculated over 2 minutes
metricsQuery: "sum(rate(<<.Series>>{<<.LabelMatchers>>,container!="POD"}[2m])) by (<<.GroupBy>>)"

完整的配置文件实例

例如,我们想使用 springboot 的 actuator 提供的 jvm_memory_used_bytesjvm_memory_max_bytes 计算内存使用率,如下所式

rules:
- seriesQuery: 'jvm_memory_used_bytes'
  resources:
    overrides:
      namespace:
        resource: "namespace"
      pod:
        resource: "pod"
  name:
    matches: 'jvm_memory_used_bytes'
    as: memory_percent
  metricsQuery: 'sum(jvm_memory_used_bytes{<<.LabelMatchers>>}) by (<<.GroupBy>>) / sum(jvm_memory_max_bytes{<<.LabelMatchers>>}) by (<<.GroupBy>>) * 100'
- seriesQuery: 'process_cpu_usage'
  resources:
    overrides:
      namespace:
        resource: "namespace"
      pod:
        resource: "pod"  
  name:
    matches: 'process_cpu_usage'
    as: process_cpu_percent
  metricsQuery: 'sum(avg_over_time(process_cpu_usage{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)'

这里用到了一个技巧,就是使用查询多个指标,这里参考了 prometheus-adapter 的说明 4

这很好理解,虽然一开始可能看起来不太明显。

基本上,你只需要选择一个指标作为 “Discovery” 和 “naming” 指标,然后使用它来配置配置中的 “discovery” 和 “naming” 部分。之后,你就可以在 metricsQuery 中写任何你想要的指标了! ==Querying 的序列可以包含任何你想要的指标,只要它们有正确的标签集合即可==。

例如,假设你有两个指标 foo_total 和 foo_count,它们都有一个标签 system_name,用于表示节点资源,那么如下配置所示

rules:
- seriesQuery: 'foo_total'
  resources: {overrides: {system_name: {resource: "node"}}}
  name:
    matches: 'foo_total'
    as: 'foo'
  metricsQuery: 'sum(foo_total{<<.LabelMatchers>>}) by (<<.GroupBy>>) / sum(foo_count{<<.LabelMatchers>>}) by (<<.GroupBy>>)'

由于我们使用了 jvm_memory_used_bytesjvm_memory_max_bytes ,那么我们可以在 “discovery” 和 “naming” 部分写任意指标,在 ”quering“ 中使用真是的指标进行替换,就可以完成

查询 kubernetes 的指标

完成配置后,可以使用下面命令进行查询

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1"|jq
{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "custom.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "pods/process_cpu_percent",
      "singularName": "",
      "namespaced": true,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "pods/memory_percent",
      "singularName": "",
      "namespaced": true,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "namespaces/memory_percent",
      "singularName": "",
      "namespaced": false,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "namespaces/process_cpu_percent",
      "singularName": "",
      "namespaced": false,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    }
  ]
}

可以通过 custom API 进程查询具体获取的值,如下所示

$ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/public/pods/*/process_cpu_percent"|jq
{
  "kind": "MetricValueList",
  "apiVersion": "custom.metrics.k8s.io/v1beta1",
  "metadata": {},
  "items": [
    {
      "describedObject": {
        "kind": "Pod",
        "namespace": "msg",
        "name": "message-gateway-api-78c4d5cdbf-9k2g7",
        "apiVersion": "/v1"
      },
      "metricName": "process_cpu_percent",
      "timestamp": "2024-05-31T11:40:25Z",
      "value": "404m",
      "selector": null
    },
    
    ...
    
    {
      "describedObject": {
        "kind": "Pod",
        "namespace": "msg",
        "name": "message-core-79fdc6fdd-lkpdm",
        "apiVersion": "/v1"
      },
      "metricName": "process_cpu_percent",
      "timestamp": "2024-05-31T11:40:25Z",
      "value": "31m",
      "selector": null
    },
    {
      "describedObject": {
        "kind": "Pod",
        "namespace": "msg",
        "name": "message-push-admin-554f5d96fd-xlnhj",
        "apiVersion": "/v1"
      },
      "metricName": "process_cpu_percent",
      "timestamp": "2024-05-31T11:40:25Z",
      "value": "487m",
      "selector": null
    }
  ]
}

我们可以看到,返回值是带有 ”m“ 的单位,这里 issue 是这样回答的

The m-suffix means milli, Quantity Values are explained here: https://github.com/kubernetes-sigs/prometheus-adapter/blob/master/docs/walkthrough.md#quantity-values 5

在指标 API 中最常见的是 m 后缀,它表示毫单位,即单位的千分之一;由于我们返回值是一个百分比,例如 4.87%,那么实际值是 0.0487,那么他的毫单位为就是 “487m” ,和上面返回值一样。

Prometheus-adapter的安装

在这里采用 helm 方式进行安装,只需要修改对应参数即可

helm install prometheus-adapter -n monitoring  prometheus-community/prometheus-adapter \
	--set prometheus.url=http://prometheus.default.svc \
	--set logLevel=2 \
	--set rules.external=xxx # 如果使用外部规则替换默认的config.yaml,则需要提前创建一个configmap,然后这里指定这个名称

Reference

[1] Kubernetes monitoring architecture

[2] core-metrics-pipeline

[3] kubernetes/metrics

[4] my-query-contains-multiple-metrics-how-do-i-make-that-work

[5] why i request rest-api, returned requeslt for item value has ’m’ unit!! #376

[6] Guide to Kubernetes Metrics

[7] Kubernetes 监控架构(译)

本文发布于Cylon的收藏册,转载请著名原文链接~

链接:https://www.oomkill.com/2024/05/prometheus-adapter-intro/

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」 许可协议进行许可。