Kubernetes监控-Prometheus

参考原文链接:https://www.qikqiak.com/k8strain/monitor/prometheus/

Prometheus

简介

Prometheus 最初是 SoundCloud 构建的开源系统监控和报警工具,是一个独立的开源项目,于2016年加入了 CNCF 基金会,作为继 Kubernetes 之后的第二个托管项目。Prometheus 相比于其他传统监控工具主要有以下几个特点:

  • 具有由 metric 名称和键/值对标识的时间序列数据的多维数据模型
  • 有一个灵活的查询语言
  • 不依赖分布式存储,只和本地磁盘有关
  • 通过 HTTP 的服务拉取时间序列数据
  • 也支持推送的方式来添加时间序列数据
  • 还支持通过服务发现或静态配置发现目标
  • 多种图形和仪表板支持

Prometheus 由多个组件组成,但是其中有些组件是可选的:

  • Prometheus Server:用于抓取指标、存储时间序列数据
  • exporter:暴露指标让任务来抓
  • pushgateway:push 的方式将指标数据推送到该网关
  • alertmanager:处理报警的报警组件 adhoc:用于数据查询

大多数 Prometheus 组件都是用 Go 编写的,因此很容易构建和部署为静态的二进制文件。下图是 Prometheus 官方提供的架构及其一些相关的生态系统组件:

Kubernetes监控-Prometheus

整体流程比较简单,Prometheus 直接接收或者通过中间的 Pushgateway 网关被动获取指标数据,在本地存储所有的获取的指标数据,并对这些数据进行一些规则整理,用来生成一些聚合数据或者报警信息,Grafana 或者其他工具用来可视化这些数据。

安装

由于 Prometheus 是 Golang 编写的程序,所以要安装的话也非常简单,只需要将二进制文件下载下来直接执行即可,前往地址:https://prometheus.io/download 下载最新版本即可。

Prometheus 是通过一个 YAML 配置文件来进行启动的,如果我们使用二进制的方式来启动的话,可以使用下面的命令:

$ ./prometheus --config.file=prometheus.yml

其中 prometheus.yml 文件的基本配置如下:

global:
  scrape_interval:     15s
  evaluation_interval: 15s

rule_files:
  # - "first.rules"
  # - "second.rules"

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['localhost:9090']

上面这个配置文件中包含了3个模块:globalrule_filesscrape_configs

  • global 模块控制 Prometheus Server 的全局配置:
    • scrape_interval:表示 prometheus 抓取指标数据的频率,默认是15s,我们可以覆盖这个值
    • evaluation_interval:用来控制评估规则的频率,prometheus 使用规则产生新的时间序列数据或者产生警报
  • rule_files:指定了报警规则所在的位置,prometheus 可以根据这个配置加载规则,用于生成新的时间序列数据或者报警信息,当前我们没有配置任何报警规则。
  • scrape_configs 用于控制 prometheus 监控哪些资源。

由于 prometheus 通过 HTTP 的方式来暴露的它本身的监控数据,prometheus 也能够监控本身的健康情况。在默认的配置里有一个单独的 job,叫做 prometheus,它采集 prometheus 服务本身的时间序列数据。这个 job 包含了一个单独的、静态配置的目标:监听 localhost 上的 9090 端口。prometheus 默认会通过目标的 /metrics 路径采集 metrics。所以,默认的 job 通过 URL:http://localhost:9090/metrics 采集 metrics。收集到的时间序列包含 prometheus 服务本身的状态和性能。

如果我们还有其他的资源需要监控的话,直接配置在 scrape_configs 模块下面就可以了。

示例应用

比如我们在本地启动一些样例来让 Prometheus 采集。Go 客户端库包含一个示例,该示例为具有不同延迟分布的三个服务暴露 RPC 延迟。

首先确保已经安装了 Go 环境并启用 go modules,下载 Prometheus 的 Go 客户端库并运行这三个示例:

$ git clone https://github.com/prometheus/client_golang.git
$ cd client_golang/examples/random
$ export GO111MODULE=on   
$ export GOPROXY=https://goproxy.cn
$ go build

然后在3个独立的终端里面运行3个服务:

$ ./random -listen-address=:8080
$ ./random -listen-address=:8081
$ ./random -listen-address=:8082

这个时候我们可以得到3个不同的监控接口:http://localhost:8080/metrics、http://localhost:8081/metricshttp://localhost:8082/metrics。

现在我们配置 Prometheus 来采集这些新的目标,让我们将这三个目标分组到一个名为 example-random 的任务。假设前两个端点(即:http://localhost:8080/metrics、http://localhost:8081/metrics )都是生产级目标应用,第三个端点(即:http://localhost:8082/metrics )为金丝雀实例。

要在 Prometheus 中对此进行建模,我们可以将多组端点添加到单个任务中,为每组目标添加额外的标签。

在此示例中,我们将 group =“production” 标签添加到第一组目标,同时将 group=“canary”添加到第二组。将以下配置添加到 prometheus.yml 中的 scrape_configs 部分,然后重新启动 Prometheus 实例:

scrape_configs:
  - job_name: 'example-random'
    scrape_interval: 5s
    static_configs:
      - targets: ['localhost:8080', 'localhost:8081']
        labels:
          group: 'production'
      - targets: ['localhost:8082']
        labels:
          group: 'canary'

这就是 Prometheus 添加监控配置最基本的配置方式了,非常简单,只需要提供一个符合 metrics 格式的可访问的接口配置给 Prometheus 就可以了。

但是由于我们这里是要运行在 Kubernetes 系统中,所以我们直接用 Docker 镜像的方式运行。

为了能够方便的管理配置文件,我们这里将 prometheus.yml 文件用 ConfigMap 的形式进行管理:

apiVersion: v1
kind: Namespace
metadata:
  name: kube-mon
  
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: kube-mon
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
      scrape_timeout: 15s
    scrape_configs:
    - job_name: 'prometheus'
      static_configs:
      - targets: ['localhost:9090']

我们这里暂时只配置了对 prometheus 本身的监控,直接创建该资源对象,配置文件创建完成了,以后如果我们有新的资源需要被监控,我们只需要将上面的 ConfigMap 对象更新即可。

由于 prometheus 可以访问 Kubernetes 的一些资源对象,所以需要配置 rbac 相关认证,这里我们使用了一个名为 prometheus 的 serviceAccount 对象(prometheus-rbac.yaml):

apiVersion: v1
kind: ServiceAccount
metadata:
  name: prometheus
  namespace: kube-mon
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prometheus
rules:
- apiGroups:
  - ""
  resources:
  - nodes
  - services
  - endpoints
  - pods
  - nodes/proxy
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - "extensions"
  resources:
    - ingresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - configmaps
  - nodes/metrics
  verbs:
  - get
- nonResourceURLs:
  - /metrics
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: prometheus
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus
subjects:
- kind: ServiceAccount
  name: prometheus
  namespace: kube-mon

由于我们要获取的资源信息,在每一个 namespace 下面都有可能存在,所以我们这里使用的是 ClusterRole 的资源对象,值得一提的是我们这里的权限规则声明中有一个 nonResourceURLs 的属性,是用来对非资源型 metrics 进行操作的权限声明,这个在以前我们很少遇到过,然后直接创建上面的资源对象即可:

$ kubectl apply -f prometheus-rbac.yaml 
serviceaccount/prometheus created
clusterrole.rbac.authorization.k8s.io/prometheus created
clusterrolebinding.rbac.authorization.k8s.io/prometheus created

为了防止Pod漂移,这里准备把Pod固定在一个具有monitor=prometheus标签的节点上,这里我们为node1打上标签:

$ kubectl label node node1 monitor=prometheus

现在我们来创建 prometheus 的 Pod 资源:(prometheus-deploy.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
  namespace: kube-mon
  labels:
    app: prometheus
spec:
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      serviceAccountName: prometheus
      nodeSelector:
        monitor: prometheus
      containers:
      - image: prom/prometheus:v2.14.0
        name: prometheus
        args:
        - "--config.file=/etc/prometheus/prometheus.yml"
        - "--storage.tsdb.path=/prometheus"  # 指定tsdb数据路径
        - "--storage.tsdb.retention.time=24h"
        - "--web.enable-admin-api"  # 控制对admin HTTP API的访问,其中包括删除时间序列等功能
        - "--web.enable-lifecycle"  # 支持热更新,直接执行localhost:9090/-/reload立即生效
        - "--web.console.libraries=/usr/share/prometheus/console_libraries"
        - "--web.console.templates=/usr/share/prometheus/consoles"
        ports:
        - containerPort: 9090
          name: http
        volumeMounts:
        - mountPath: "/etc/prometheus"
          name: config-volume
        - mountPath: "/prometheus"
          name: data
        resources:
          requests:
            cpu: 100m
            memory: 512Mi
          limits:
            cpu: 100m
            memory: 512Mi
      volumes:
      - name: data
        hostPath:
          path: /data/prometheus/
      - configMap:
          name: prometheus-config
        name: config-volume

另外为了 prometheus 的性能和数据持久化我们这里是直接将通过 hostPath 的方式来进行数据持久化的,通过 --storage.tsdb.path=/prometheus 指定数据目录,然后将该目录声明挂载到 /data/prometheus 这个主机目录下面。

现在我们就可以添加 promethues 的资源对象了:

$ kubectl apply -f prometheus-deploy.yaml 
deployment.apps/prometheus created

$ kubectl get pod -n kube-mon
NAME                          READY   STATUS              RESTARTS   AGE
prometheus-7bb94b64f7-dkl6c   0/1     ContainerCreating   0          13s

$ kubectl logs -f  prometheus-7bb94b64f7-dkl6c -n kube-mon
level=info ts=2022-01-15T07:29:46.985Z caller=main.go:332 msg="Starting Prometheus" version="(version=2.14.0, branch=HEAD, revision=edeb7a44cbf745f1d8be4ea6f215e79e651bfe19)"
level=info ts=2022-01-15T07:29:46.985Z caller=main.go:333 build_context="(go=go1.13.4, user=root@df2327081015, date=20191111-14:27:12)"
level=info ts=2022-01-15T07:29:46.985Z caller=main.go:334 host_details="(Linux 5.15.5-2.el7.x86_64 #1 SMP Tue Nov 30 07:09:55 UTC 2021 x86_64 prometheus-7bb94b64f7-dkl6c (none))"
level=info ts=2022-01-15T07:29:46.985Z caller=main.go:335 fd_limits="(soft=1048576, hard=1048576)"
level=info ts=2022-01-15T07:29:46.985Z caller=main.go:336 vm_limits="(soft=unlimited, hard=unlimited)"
level=error ts=2022-01-15T07:29:46.985Z caller=query_logger.go:85 component=activeQueryTracker msg="Error opening query log file" file=/prometheus/queries.active err="open /prometheus/queries.active: permission denied"
panic: Unable to create mmap-ed active query log

goroutine 1 [running]:
github.com/prometheus/prometheus/promql.NewActiveQueryTracker(0x7ffce1d23d89, 0xb, 0x14, 0x2b4f400, 0xc000113470, 0x2b4f400)
	/app/promql/query_logger.go:115 +0x48c
main.main()
	/app/cmd/prometheus/main.go:364 +0x5229

创建 Pod 后,我们可以看到并没有成功运行,出现了 open /prometheus/queries.active: permission denied 这样的错误信息,这是因为我们的 prometheus 的镜像中是使用的 nobody 这个用户,然后现在我们通过 hostPath 挂载到宿主机上面的目录的 ownership 却是 root

$ ls -la
total 0
drwxr-xr-x   3 root root  24 Jan 15 15:35 .
dr-xr-xr-x. 18 root root 256 Jan 15 15:33 ..
drwxr-xr-x   2 root root   6 Jan 15 15:35 prometheus

以当然会出现操作权限问题了,这个时候我们就可以通过 securityContext 来为 Pod 设置下 volumes 的权限,通过设置 runAsUser=0 指定运行的用户为 root:

......
securityContext:
  runAsUser: 0
volumes:
- name: data
  hostPath:
    path: /data/prometheus/
- configMap:
    name: prometheus-config
  name: config-volume

这时候重新更新下prometheus:

$ kubectl get pod -n kube-mon
NAME                          READY   STATUS    RESTARTS   AGE
prometheus-56ff59cf74-vdngs   1/1     Running   0          12s

$ kubectl logs prometheus-56ff59cf74-vdngs -n kube-mon
level=info ts=2022-01-15T07:37:50.588Z caller=main.go:332 msg="Starting Prometheus" version="(version=2.14.0, branch=HEAD, revision=edeb7a44cbf745f1d8be4ea6f215e79e651bfe19)"
level=info ts=2022-01-15T07:37:50.588Z caller=main.go:333 build_context="(go=go1.13.4, user=root@df2327081015, date=20191111-14:27:12)"
level=info ts=2022-01-15T07:37:50.588Z caller=main.go:334 host_details="(Linux 5.15.5-2.el7.x86_64 #1 SMP Tue Nov 30 07:09:55 UTC 2021 x86_64 prometheus-56ff59cf74-vdngs (none))"
level=info ts=2022-01-15T07:37:50.588Z caller=main.go:335 fd_limits="(soft=1048576, hard=1048576)"
level=info ts=2022-01-15T07:37:50.588Z caller=main.go:336 vm_limits="(soft=unlimited, hard=unlimited)"
level=info ts=2022-01-15T07:37:50.682Z caller=main.go:657 msg="Starting TSDB ..."
level=info ts=2022-01-15T07:37:50.682Z caller=web.go:496 component=web msg="Start listening for connections" address=0.0.0.0:9090
level=info ts=2022-01-15T07:37:50.784Z caller=head.go:535 component=tsdb msg="replaying WAL, this may take awhile"
level=info ts=2022-01-15T07:37:50.785Z caller=head.go:583 component=tsdb msg="WAL segment loaded" segment=0 maxSegment=0
level=info ts=2022-01-15T07:37:50.786Z caller=main.go:672 fs_type=XFS_SUPER_MAGIC
level=info ts=2022-01-15T07:37:50.786Z caller=main.go:673 msg="TSDB started"
level=info ts=2022-01-15T07:37:50.786Z caller=main.go:743 msg="Loading configuration file" filename=/etc/prometheus/prometheus.yml
level=info ts=2022-01-15T07:37:50.787Z caller=main.go:771 msg="Completed loading of configuration file" filename=/etc/prometheus/prometheus.yml
level=info ts=2022-01-15T07:37:50.787Z caller=main.go:626 msg="Server is ready to receive web requests."

Pod 创建成功后,为了能够在外部访问到 prometheus 的 webui 服务,我们还需要创建一个 Service 对象:(prometheus-svc.yaml)

apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: kube-mon
  labels:
    app: prometheus
spec:
  selector:
    app: prometheus
  type: NodePort
  ports:
    - name: web
      port: 9090
      targetPort: http

当然这时候我们也可以创建一个Ingress对象,通过域名进行访问:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: prometheus
  namespace: kube-mon
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
    - host: k8s.negan.com  # 将域名映射到 prometheus 服务
      http:
        paths:
          - path: /
            backend:
              serviceName: prometheus  
              servicePort: 9090     

现在我们就可以通过IP:port的方式或者通过域名的方式直接访问我们的prometheus 的webui服务了:

$ kubectl get svc,ingress -n kube-mon
NAME                 TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
service/prometheus   NodePort   10.101.4.94   <none>        9090:30050/TCP   8m49s

NAME                            CLASS    HOSTS           ADDRESS   PORTS   AGE
ingress.extensions/prometheus   <none>   k8s.negan.com             80      5m25s

Kubernetes监控-Prometheus

现在我们可以查看当前监控系统中的一些监控目标(Status -> Targets):

Kubernetes监控-Prometheus

由于我们现在还没有配置任何的报警信息,所以 Alerts 菜单下面现在没有任何数据,隔一会儿,我们可以去 Graph 菜单下面查看我们抓取的 prometheus 本身的一些监控数据了,其中- insert metrics at cursor -下面就有我们搜集到的一些监控指标数据:

Kubernetes监控-Prometheus

比如我们这里就选择 scrape_duration_seconds 这个指标,然后点击 Execute,就可以看到类似于下面的图表数据了:

Kubernetes监控-Prometheus

除了简单的直接使用采集到的一些监控指标数据之外,这个时候也可以使用强大的 PromQL 工具,PromQL 其实就是 prometheus 便于数据聚合展示开发的一套 ad hoc 查询语言的,你想要查什么找对应函数取你的数据好了。

应用监控

Prometheus 的数据指标是通过一个公开的 HTTP(S) 数据接口获取到的,我们不需要单独安装监控的 agent,只需要暴露一个 metrics 接口,Prometheus 就会定期去拉取数据;对于一些普通的 HTTP 服务,我们完全可以直接复用这个服务,添加一个 /metrics 接口暴露给 Prometheus;而且获取到的指标数据格式是非常易懂的,不需要太高的学习成本。

现在很多服务从一开始就内置了一个 /metrics 接口,比如 Kubernetes 的各个组件、istio 服务网格都直接提供了数据指标接口。有一些服务即使没有原生集成该接口,也完全可以使用一些 exporter 来获取到指标数据,比如 mysqld_exporternode_exporter,这些 exporter 就有点类似于传统监控服务中的 agent,作为服务一直存在,用来收集目标服务的指标数据然后直接暴露给 Prometheus。

普通应用

对于普通应用只需要能够提供一个满足 prometheus 格式要求的 /metrics 接口就可以让 Prometheus 来接管监控,比如 Kubernetes 集群中非常重要的 CoreDNS 插件,一般默认情况下就开启了 /metrics 接口:

$ kubectl get cm coredns -n kube-system -o yaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
kind: ConfigMap
metadata:
  creationTimestamp: "2022-01-13T02:57:11Z"
  name: coredns
  namespace: kube-system
  resourceVersion: "197"
  selfLink: /api/v1/namespaces/kube-system/configmaps/coredns
  uid: 18f3bc25-9f66-4faa-b4f4-1e9104fa49ca

上面 ConfigMap 中 prometheus :9153 就是开启 prometheus 的插件:

$ kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide
NAME                       READY   STATUS    RESTARTS   AGE    IP            NODE    NOMINATED NODE   READINESS GATES
coredns-7ff77c879f-2wlp9   1/1     Running   2          2d5h   10.244.1.15   node1   <none>           <none>
coredns-7ff77c879f-bpvjp   1/1     Running   2          2d5h   10.244.1.13   node1   <none>           <none>

我们可以先尝试手动访问下 /metrics 接口,如果能够手动访问到那证明接口是没有任何问题的:

$ curl http://10.244.1.15:9153/metrics
# HELP coredns_build_info A metric with a constant '1' value labeled by version, revision, and goversion from which CoreDNS was built.
# TYPE coredns_build_info gauge
coredns_build_info{goversion="go1.13.6",revision="da7f65b",version="1.6.7"} 1
# HELP coredns_cache_misses_total The count of cache misses.
# TYPE coredns_cache_misses_total counter
coredns_cache_misses_total{server="dns://:53"} 10
# HELP coredns_dns_request_count_total Counter of DNS requests made per zone, protocol and family.
# TYPE coredns_dns_request_count_total counter
coredns_dns_request_count_total{family="1",proto="udp",server="dns://:53",zone="."} 10
# HELP coredns_dns_request_duration_seconds Histogram of the time (in seconds) each request took.
......

我们可以看到可以正常访问到,name CoreDNS 的监控数据接口是正常的了,我们就可以将这个 /metrics 接口配置到 prometheus.yml 中去了,直接加到默认的 prometheus 这个 job 下面:(prometheus-cm.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: kube-mon
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
      scrape_timeout: 15s
    scrape_configs:
    - job_name: 'prometheus'
      static_configs:
      - targets: ['localhost:9090']
    - job_name: "coredns"
      static_configs:
      - targets: ["10.244.1.15:9153","10.244.1.13:9153"]

当然,我们这里只是一个很简单的配置,scrape_configs 下面可以支持很多参数,例如:

  • basic_authbearer_token:比如我们提供的 /metrics 接口需要 basic 认证的时候,通过传统的用户名/密码或者在请求的 header 中添加对应的 token 都可以支持
  • kubernetes_sd_configsconsul_sd_configs:可以用来自动发现一些应用的监控数据

现在 Prometheus 的配置文件内容已经更改了,隔一会儿被挂载到 Pod 中的 prometheus.yml 文件也会更新,由于我们之前的 Prometheus 启动参数中添加了 --web.enable-lifecycle 参数,所以现在我们只需要执行一个 reload 命令即可让配置生效:

$ kubectl get pod -n kube-mon -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
prometheus-56ff59cf74-vdngs   1/1     Running   0          73m   10.244.1.18   node1   <none>           <none>

$ curl -X POST "http://10.244.1.18:9090/-/reload"

这个时候我们再去看 Prometheus 的 Dashboard 中查看采集的目标数据:

Kubernetes监控-Prometheus

可以看到我们刚刚添加的 coredns 这个任务已经出现了,然后同样的我们可以切换到 Graph 下面去,我们可以找到一些 CoreDNS 的指标数据,至于这些指标数据代表什么意义,一般情况下,我们可以去查看对应的 /metrics 接口,里面一般情况下都会有对应的注释。

到这里我们就在 Prometheus 上配置了第一个 Kubernetes 应用。

使用exporter监控

有一些应用可能没有自带 /metrics 接口供 Prometheus 使用,在这种情况下,我们就需要利用 exporter 服务来为 Prometheus 提供指标数据了。Prometheus 官方为许多应用就提供了对应的 exporter 应用,也有许多第三方的实现,我们可以前往官方网站进行查看:exporters,当然如果你的应用本身也没有 exporter 实现,那么就要我们自己想办法去实现一个 /metrics 接口了,只要你能提供一个合法的 /metrics 接口,Prometheus 就可以监控你的应用。

比如我们这里通过一个 redis-exporter 的服务来监控 redis 服务,对于这类应用,我们一般会以 sidecar 的形式和主应用部署在同一个 Pod 中,比如我们这里来部署一个 redis 应用,并用 redis-exporter 的方式来采集监控数据供 Prometheus 使用,如下资源清单文件:(prometheus-redis.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: kube-mon
spec:
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9121"
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:4
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 6379
      - name: redis-exporter
        image: oliver006/redis_exporter:latest
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 9121
---
kind: Service
apiVersion: v1
metadata:
  name: redis
  namespace: kube-mon
spec:
  selector:
    app: redis
  ports:
  - name: redis
    port: 6379
    targetPort: 6379
  - name: prom
    port: 9121
    targetPort: 9121

可以看到上面我们在 redis 这个 Pod 中包含了两个容器,一个就是 redis 本身的主应用,另外一个容器就是 redis_exporter。创建完成之后,我们就可以看到redis的Pod包含两个容器:

$ kubectl get pod -n kube-mon
NAME                          READY   STATUS    RESTARTS   AGE
prometheus-56ff59cf74-vdngs   1/1     Running   0          8h
redis-7479d9b867-9pqtx        2/2     Running   0          4m53s

$ kubectl get svc -n kube-mon
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
prometheus   NodePort    10.101.4.94     <none>        9090:30050/TCP      7h29m
redis        ClusterIP   10.104.17.136   <none>        6379/TCP,9121/TCP   5m21s

我们可以通过 9121 端口来校验是否能够采集到数据:

$ curl 10.104.17.136:9121/metrics
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
.....

同样的,现在我们只需要更新 Prometheus 的配置文件:

- job_name: 'redis'
  static_configs:
  - targets: ['redis:9121']

由于我们这里是通过 Service 去配置的 redis 服务,这里直接使用servicename,直接配置 Pod IP 也是可以的,因为和 Prometheus 处于同一个 namespace。

配置文件更后重新加载:

$ curl -X POST "http://10.244.1.18:9090/-/reload"

这个时候我们再去看 Prometheus 的 Dashboard 中查看采集的目标数据:

Kubernetes监控-Prometheus

可以看到配置的 redis 这个 job 已经生效了。切换到 Graph 下面可以看到很多关于 redis 的指标数据,我们选择任意一个指标,比如 redis_exporter_scrapes_total,然后点击执行就可以看到对应的数据图表了:

Kubernetes监控-Prometheus

集群节点

我们已经知道怎样用 Promethues 来监控 Kubernetes 集群中的应用,但是对于 Kubernetes 集群本身的监控也是非常重要的,我们需要时时刻刻了解集群的运行状态。

对于集群的监控一般我们需要考虑以下几个方面:

​ 1、Kubernetes 节点的监控:比如节点的 cpu、load、disk、memory 等指标

​ 2、内部系统组件的状态:比如 kube-scheduler、kube-controller-manager、kubedns/coredns 等组件的详细运行状态

​ 3、编排级的 metrics:比如 Deployment 的状态、资源请求、调度和 API 延迟等数据指标

Kubernetes 集群的监控方案目前主要有以下几种方案:

  • Heapster:Heapster 是一个集群范围的监控和数据聚合工具,以 Pod 的形式运行在集群中。 heapster 除了 Kubelet/cAdvisor 之外,我们还可以向 Heapster 添加其他指标源数据,比如 kube-state-metrics,需要注意的是 Heapster 已经被废弃了,后续版本中会使用 metrics-server 代替。
  • cAdvisor:cAdvisor 是 Google 开源的容器资源监控和性能分析工具,它是专门为容器而生,本身也支持 Docker 容器。
  • kube-state-metrics:kube-state-metrics 通过监听 API Server 生成有关资源对象的状态指标,比如 Deployment、Node、Pod,需要注意的是 kube-state-metrics 只是简单提供一个 metrics 数据,并不会存储这些指标数据,所以我们可以使用 Prometheus 来抓取这些数据然后存储。
  • metrics-server:metrics-server 也是一个集群范围内的资源数据聚合工具,是 Heapster 的替代品,同样的,metrics-server 也只是显示数据,并不提供数据存储服务。

不过 kube-state-metrics 和 metrics-server 之间还是有很大不同的,二者的主要区别如下:

1、kube-state-metrics 主要关注的是业务相关的一些元数据,比如 Deployment、Pod、副本状态等

​ 2、metrics-server 主要关注的是资源度量 API 的实现,比如 CPU、文件描述符、内存、请求延时等指标。

监控集群节点

要监控节点其实已经有很多非常成熟的方案了,比如 Nagios、zabbix,甚至我们自己来收集数据也可以,我们这里通过 Prometheus 来采集节点的监控指标数据,可以通过 node_exporter 来获取,顾名思义,node_exporter 就是抓取用于采集服务器节点的各种运行指标,目前 node_exporter 支持几乎所有常见的监控点,比如 conntrack,cpu,diskstats,filesystem,loadavg,meminfo,netstat 等,详细的监控点列表可以参考其 Github 仓库

我们可以通过 DaemonSet 控制器来部署该服务,这样每一个节点都会自动运行一个这样的 Pod,如果我们从集群中删除或者添加节点后,也会进行自动扩展。

在部署 node-exporter 的时候有一些细节需要注意,如下资源清单文件:(prome-node-exporter.yaml)

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: kube-mon
  labels:
    app: node-exporter
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      hostPID: true
      hostIPC: true
      hostNetwork: true
      nodeSelector:
        kubernetes.io/os: linux
      containers:
      - name: node-exporter
        image: prom/node-exporter
        args:
        - --web.listen-address=$(HOSTIP):9100
        - --path.procfs=/host/proc
        - --path.sysfs=/host/sys
        - --path.rootfs=/host/root
        - --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/)
        - --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$
        ports:
        - containerPort: 9100
        env:
        - name: HOSTIP
          valueFrom:
            fieldRef:
              fieldPath: status.hostIP
        resources:
          requests:
            cpu: 150m
            memory: 180Mi
          limits:
            cpu: 150m
            memory: 180Mi
        securityContext:
          runAsNonRoot: true
          runAsUser: 65534
        volumeMounts:
        - name: proc
          mountPath: /host/proc
        - name: sys
          mountPath: /host/sys
        - name: root
          mountPath: /host/root
          mountPropagation: HostToContainer
          readOnly: true
      tolerations:
      - operator: "Exists"
      volumes:
      - name: proc
        hostPath:
          path: /proc
      - name: dev
        hostPath:
          path: /dev
      - name: sys
        hostPath:
          path: /sys
      - name: root
        hostPath:
          path: /

由于我们要获取到的数据是主机的监控指标数据,而我们的 node-exporter 是运行在容器中的,所以我们在 Pod 中需要配置一些 Pod 的安全策略,这里我们就添加了 hostPID: truehostIPC: truehostNetwork: true 3个策略,用来使用主机的 PID namespaceIPC namespace 以及主机网络,这些 namespace 就是用于容器隔离的关键技术,要注意这里的 namespace 和集群中的 namespace 是两个完全不相同的概念。

另外我们还将主机的 /dev/proc/sys这些目录挂载到容器中,这些因为我们采集的很多节点数据都是通过这些文件夹下面的文件来获取到的,比如我们在使用 top 命令可以查看当前 cpu 使用情况,数据就来源于文件 /proc/stat,使用 free 命令可以查看当前内存使用情况,其数据来源是来自 /proc/meminfo 文件。

另外由于我们集群使用的是 kubeadm 搭建的,所以如果希望 master 节点也一起被监控,则需要添加相应的容忍,然后直接创建上面的资源对象:

$ kubectl get pods -n kube-mon -l app=node-exporter -o wide
NAME                  READY   STATUS    RESTARTS   AGE     IP               NODE     NOMINATED NODE   READINESS GATES
node-exporter-d55w6   1/1     Running   0          2m12s   192.168.47.135   node2    <none>           <none>
node-exporter-j892n   1/1     Running   0          2m12s   192.168.47.133   master   <none>           <none>
node-exporter-nwjvr   1/1     Running   0          2m12s   192.168.47.134   node1    <none>           <none>

部署完成后,我们可以看到在3个节点上都运行了一个 Pod,由于我们指定了 hostNetwork=true,所以在每个节点上就会绑定一个端口 9100,我们可以通过这个端口去获取到监控指标数据:

$ curl 192.168.47.133:9100/metrics

当然如果你觉得上面的手动安装方式比较麻烦,我们也可以使用 Helm 的方式来安装:

$ helm upgrade --install node-exporter --namespace kube-mon stable/prometheus-node-exporter

服务发现

由于我们这里每个节点上面都运行了 node-exporter 程序,如果我们通过一个 Service 来将数据收集到一起用静态配置的方式配置到 Prometheus 去中,就只会显示一条数据,我们得自己在指标数据中去过滤每个节点的数据,当然我们也可以手动的把所有节点用静态的方式配置到 Prometheus 中去,但是以后要新增或者去掉节点的时候就还得手动去配置,那么有没有一种方式可以让 Prometheus 去自动发现我们节点的 node-exporter 程序,并且按节点进行分组呢?这就是 Prometheus 里面非常重要的服务发现功能了。

在 Kubernetes 下,Promethues 通过与 Kubernetes API 集成,主要支持5中服务发现模式,分别是:NodeServicePodEndpointsIngress

我们通过 kubectl 命令可以很方便的获取到当前集群中的所有节点信息:

$ kubectl get node
NAME     STATUS   ROLES    AGE     VERSION
master   Ready    master   2d13h   v1.18.0
node1    Ready    <none>   2d13h   v1.18.0
node2    Ready    <none>   9h      v1.18.0

但是要让 Prometheus 也能够获取到当前集群中的所有节点信息的话,我们就需要利用 Node 的服务发现模式,同样的,在 prometheus.yml 文件中配置如下的 job 任务即可:

- job_name: 'kubernetes-nodes'
  kubernetes_sd_configs:
  - role: node

通过指定 kubernetes_sd_configs 的模式为node,Prometheus 就会自动从 Kubernetes 中发现所有的 node 节点并作为当前 job 监控的目标实例,发现的节点 /metrics 接口是默认的 kubelet 的 HTTP 接口。

Prometheus 的 ConfigMap 更新完成后,同样的我们执行 reload 操作,让配置生效:

$ curl -X POST "http://10.244.1.18:9090/-/reload"

配置生效后,我们再去 prometheus 的 dashboard 中查看 Targets 是否能够正常抓取数据。

Kubernetes监控-Prometheus

我们可以看到上面的 kubernetes-nodes 这个 job 任务已经自动发现了我们3个 node 节点,但是在获取数据的时候失败了,出现了类似于下面的错误信息:

server returned HTTP status 400 Bad Request

这个是因为 prometheus 去发现 Node 模式的服务的时候,访问的端口默认是10250,而默认是需要认证的 https 协议才有权访问的,但实际上我们并不是希望让去访问10250端口的 /metrics 接口,而是 node-exporter 绑定到节点的 9100 端口,所以我们应该将这里的 10250 替换成 9100,但是应该怎样替换呢?

这里我们就需要使用到 Prometheus 提供的 relabel_configs 中的 replace 能力了,relabel 可以在 Prometheus 采集数据之前,通过 Target 实例的 Metadata 信息,动态重新写入 Label 的值。

除此之外,我们还能根据 Target 实例的 Metadata 信息选择是否采集或者忽略该 Target 实例。比如我们这里就可以去匹配 __address__ 这个 Label 标签,然后替换掉其中的端口,如果你不知道有哪些 Label 标签可以操作的话,可以将鼠标移动到 Targets 的标签区域,其中显示的 Before relabeling 区域都是我们可以操作的标签:

Kubernetes监控-Prometheus

现在我们来替换掉端口,修改 ConfigMap:

- job_name: 'kubernetes-nodes'
  kubernetes_sd_configs:
  - role: node
  relabel_configs:
  - source_labels: [__address__]
    regex: '(.*):10250'
    replacement: '${1}:9100'
    target_label: __address__
    action: replace

这里就是一个正则表达式,去匹配 __address__ 这个标签,然后将 host 部分保留下来,port 替换成了 9100,现在我们重新更新配置文件,执行 reload 操作,然后再去看 Prometheus 的 Dashboard 的 Targets 路径下面 kubernetes-nodes 这个 job 任务是否正常了:

Kubernetes监控-Prometheus

我们可以看到现在已经正常了,但是还有一个问题就是我们采集的指标数据 Label 标签就只有一个节点的 hostname,这对于我们在进行监控分组分类查询的时候带来了很多不方便的地方,要是我们能够将集群中 Node 节点的 Label 标签也能获取到就很好了。这里我们可以通过 labelmap 这个属性来将 Kubernetes 的 Label 标签添加为 Prometheus 的指标数据的标签:

- job_name: 'kubernetes-nodes'
  kubernetes_sd_configs:
  - role: node
  relabel_configs:
  - source_labels: [__address__]
    regex: '(.*):10250'
    replacement: '${1}:9100'
    target_label: __address__
    action: replace
  - action: labelmap
    regex: __meta_kubernetes_node_label_(.+)

添加了一个 action 为 labelmap,正则表达式是 __meta_kubernetes_node_label_(.+) 的配置,这里的意思就是表达式中匹配都的数据也添加到指标数据的 Label 标签中去。

对于 kubernetes_sd_configs 下面可用的元信息标签如下:

  • __meta_kubernetes_node_name:节点对象的名称
  • _meta_kubernetes_node_label:节点对象中的每个标签
  • _meta_kubernetes_node_annotation:来自节点对象的每个注释
  • _meta_kubernetes_node_address:每个节点地址类型的第一个地址(如果存在)

关于 kubernets_sd_configs 更多信息可以查看官方文档:kubernetes_sd_config

另外由于 kubelet 也自带了一些监控指标数据,就上面我们提到的 10250 端口,所以我们这里也把 kubelet 的监控任务也一并配置上:

- job_name: 'kubernetes-kubelet'
  kubernetes_sd_configs:
  - role: node
  scheme: https
  tls_config:
    ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
    insecure_skip_verify: true
  bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
  relabel_configs:
  - action: labelmap
    regex: __meta_kubernetes_node_label_(.+)

这里需要特别注意的是这里必须使用 https 协议访问,这样就必然需要提供证书,我们这里是通过配置 insecure_skip_verify: true 来跳过了证书校验,但是除此之外,要访问集群的资源,还必须要有对应的权限才可以,也就是对应的 ServiceAccount 权限允许才可以,这里我们只需将 Pod 启动后自动注入的 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt/var/run/secrets/kubernetes.io/serviceaccount/token 文件配置上,就可以获取到对应的权限了。

现在我们再去更新下配置文件,执行 reload 操作,让配置生效,然后访问 Prometheus 的 Dashboard 查看 Targets 路径:

Kubernetes监控-Prometheus

现在我们就可以切换到 Graph 路径下面查看采集的一些指标数据了,比如查询 node_load1 指标:

Kubernetes监控-Prometheus

我们可以看到将3个节点对应的 node_load1 指标数据都查询出来了,同样的,我们还可以使用 PromQL 语句来进行更复杂的一些聚合查询操作,还可以根据我们的 Labels 标签对指标数据进行聚合,比如我们这里只查询 mster 节点的数据,可以使用表达式 node_load1{instance="master"} 来进行查询:

Kubernetes监控-Prometheus

容器监控

cAdvisor已经内置在了 kubelet 组件之中,所以我们不需要单独去安装,cAdvisor 的数据路径为 /api/v1/nodes/<node>/proxy/metrics,同样我们这里使用 node 的服务发现模式,因为每一个节点下面都有 kubelet,自然都有 cAdvisor 采集到的数据指标,配置如下:

- job_name: 'kubernetes-cadvisor'
  kubernetes_sd_configs:
  - role: node
  scheme: https
  tls_config:
    ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
  relabel_configs:
  - action: labelmap
    regex: __meta_kubernetes_node_label_(.+)
  - target_label: __address__
    replacement: kubernetes.default.svc:443
  - source_labels: [__meta_kubernetes_node_name]
    regex: (.+)
    target_label: __metrics_path__
    replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor

上面的配置和我们之前配置 node-exporter 的时候几乎是一样的,区别是我们这里使用了 https 的协议(和上面的kubelet一样),通过ca.crttoken两个文件我们可以在 Pod 中访问 apiserver,比如我们这里的 __address__ 不再是 nodeip 了,而是 kubernetes 在集群中的服务地址,然后加上__metrics_path__ 的访问路径 /api/v1/nodes/${1}/proxy/metrics/cadvisor,因为我们现在是通过 kubernetes 的 apiserver 地址去进行访问的,现在同样更新下配置,然后查看 Targets 路径:

Kubernetes监控-Prometheus

我们可以切换到 Graph 路径下面查询容器相关数据,比如我们这里来查询集群中所有 Pod 的 CPU 使用情况,kubelet 中的 cAdvisor 采集的指标和含义,可以查看 Monitoring cAdvisor with Prometheus 说明,其中有一项:

container_cpu_usage_seconds_total   Counter     Cumulative cpu time consumed    seconds

container_cpu_usage_seconds_total 是容器累计使用的 CPU 时间,用它除以 CPU 的总时间,就可以得到容器的 CPU 使用率了:

首先计算容器的 CPU 占用时间,由于节点上的 CPU 有多个,所以需要将容器在每个 CPU 上占用的时间累加起来,Pod 在 1m 内累积使用的 CPU 时间为:(根据 pod 和 namespace 进行分组查询)

sum(rate(container_cpu_usage_seconds_total{image!="",pod!=""}[1m])) by (namespace, pod)

然后计算 CPU 的总时间,这里的 CPU 数量是容器分配到的 CPU 数量,container_spec_cpu_quota 是容器的 CPU 配额,它的值是容器指定的 CPU 个数 * 100000,所以 Pod 在 1s 内 CPU 的总时间为:Pod 的 CPU 核数 * 1s:

sum(container_spec_cpu_quota{image!="", pod!=""}) by(namespace, pod) / 100000

CPU配额

由于 container_spec_cpu_quota 是容器的 CPU 配额,所以只有配置了 resource-limit CPU 的 Pod 才可以获得该指标数据。

将上面这两个语句的结果相除,就得到了容器的 CPU 使用率:

(sum(rate(container_cpu_usage_seconds_total{image!="",pod!=""}[1m])) by (namespace, pod))
/
(sum(container_spec_cpu_quota{image!="", pod!=""}) by(namespace, pod) / 100000) * 100

在 promethues 里面执行上面的 promQL 语句可以得到下面的结果:

Kubernetes监控-Prometheus

监控 apiserver

apiserver 作为 Kubernetes 最核心的组件,当然它的监控也是非常有必要的,对于 apiserver 的监控我们可以直接通过 kubernetes 的 Service 来获取:

$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   3d6h

上面这个 Service 就是我们集群的 apiserver 在集群内部的 Service 地址,要自动发现 Service 类型的服务,我们就需要用到 role 为 Endpoints 的 kubernetes_sd_configs,我们可以在 ConfigMap 对象中添加上一个 Endpoints 类型的服务的监控任务:

- job_name: 'kubernetes-apiservers'
  kubernetes_sd_configs:
  - role: endpoints

面这个任务是定义的一个类型为 endpoints 的 kubernetes_sd_configs ,添加到 Prometheus 的 ConfigMap 的配置文件中,然后更新配置,更新完成后,我们再去查看 Prometheus 的 Dashboard 的 target 页面:

Kubernetes监控-Prometheus

可以看到 kubernetes-apiservers 下面出现了很多实例,这是因为这里使用的是 Endpoints 类型的服务发现,所以 Prometheus 会把所有的 Endpoints 服务都抓取过来了,同样的,上面我们需要的服务名为 kubernetes 这个 apiserver 的服务也在这个列表之中,同样可以使用relabel_configs这个配置,只是这里不是使用 replace 这个动作了,而是 keep,就是只把符合我们要求的给保留下来。

哪些才是符合我们要求的呢?同样我们可以把鼠标放置在任意一个 target 上,可以查看到Before relabeling里面所有的元数据,比如要过滤的服务是 default 这个 namespace 下面,服务名为 kubernetes 的元数据,所以这里就可以根据对应的 __meta_kubernetes_namespace__meta_kubernetes_service_name 这两个元数据来 relabel,另外由于 kubernetes 这个服务对应的端口是 443,需要使用 https 协议,所以这里我们需要使用 https 的协议,对应的就需要将 ca 证书配置上,如下所示:

- job_name: 'kubernetes-apiservers'
  kubernetes_sd_configs:
  - role: endpoints
  scheme: https
  tls_config:
    ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
  relabel_configs:
  - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
    action: keep
    regex: default;kubernetes;https

现在重新更新配置文件、重新加载 Prometheus,切换到 Prometheus 的 Targets 路径下查看:

Kubernetes监控-Prometheus

现在可以看到 kubernetes-apiserver 这个任务下面只有 apiserver 这一个实例了,证明我们的 relabel 是成功的。

监控Pod

上面的 apiserver 实际上就是一种特殊的 Endpoints,现在我们同样来配置一个任务用来专门发现普通类型的 Endpoint,其实就是 Service 关联的 Pod 列表:

- job_name: 'kubernetes-endpoints'
  kubernetes_sd_configs:
  - role: endpoints
  relabel_configs:
  - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
    action: keep
    regex: true
  - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
    action: replace
    target_label: __scheme__
    regex: (https?)
  - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
    action: replace
    target_label: __metrics_path__
    regex: (.+)
  - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
    action: replace
    target_label: __address__
    regex: ([^:]+)(?::\d+)?;(\d+)
    replacement: $1:$2
  - action: labelmap
    regex: __meta_kubernetes_service_label_(.+)
  - source_labels: [__meta_kubernetes_namespace]
    action: replace
    target_label: kubernetes_namespace
  - source_labels: [__meta_kubernetes_service_name]
    action: replace
    target_label: kubernetes_name
  - source_labels: [__meta_kubernetes_pod_name]
    action: replace
    target_label: kubernetes_pod_name

注意这里在 relabel_configs 区域做了大量的配置,特别是第一个保留__meta_kubernetes_service_annotation_prometheus_io_scrape 为 true 的才保留下来,这就是说要想自动发现集群中的 Endpoint,就需要在 Service 的 annotation 区域添加 prometheus.io/scrape=true 的声明,现在我们先将上面的配置更新,查看下效果:

Kubernetes监控-Prometheus

我们可以看到 kubernetes-endpoints 这一个任务下面只发现了两个服务,这是因为我们在 relabel_configs 中过滤了 annotationprometheus.io/scrape=true 的 Service,而现在我们系统中只有这样一个 kube-dns 服务符合要求,该 Service 下面有两个实例,所以出现了两个实例:

$ kubectl get svc kube-dns -n kube-system -o yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    prometheus.io/port: "9153"  # metrics接口的端口
    prometheus.io/scrape: "true"  # 可以让prometheus自动发现
  creationTimestamp: "2022-01-13T02:57:12Z"
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: KubeDNS
......

现在我们在之前创建的 redis 这个 Service 中添加上 prometheus.io/scrape=true 这个 annotation,于 redis 服务的 metrics 接口在 9121 这个 redis-exporter 服务上面,所以我们还需要添加一个 prometheus.io/port=9121 这样的 annotations:

apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: kube-mon
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "9121"
spec:
  selector:
    app: redis
  ports:
  - name: redis
    port: 6379
    targetPort: 6379
  - name: prom
    port: 9121
    targetPort: 9121

更新完成后,去 Prometheus 查看 Targets 路径,可以看到 redis 服务自动出现在了 kubernetes-endpoints 这个任务下面:

Kubernetes监控-Prometheus

这样以后我们有了新的服务,服务本身提供了 /metrics 接口,我们就完全不需要用静态的方式去配置了,到这里我们就可以将之前配置的 redis 的静态配置去掉了。

PromQL

Prometheus 通过指标名称(metrics name)以及对应的一组标签(label)定义一条唯一时间序列。指标名称反映了监控样本的基本标识,而 label 则在这个基本特征上为采集到的数据提供了多种特征维度。用户可以基于这些特征维度过滤、聚合、统计从而产生新的计算后的一条时间序列。

PromQL 是 Prometheus 内置的数据查询语言,其提供对时间序列数据丰富的查询,聚合以及逻辑运算能力的支持。并且被广泛应用在 Prometheus 的日常应用当中,包括对数据查询、可视化、告警处理。可以这么说,PromQL 是 Prometheus 所有应用场景的基础,理解和掌握 PromQL 是我们使用 Prometheus 必备的技能。

时间序列

前面我们通过 node-exporter 暴露的 metrics 服务,Prometheus 可以采集到当前主机所有监控指标的样本数据。例如:

# HELP node_cpu_seconds_total Seconds the cpus spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="0",mode="idle"} 39835.07
# HELP node_load1 1m load average.
# TYPE node_load1 gauge
node_load1 0.44

其中非 # 开头的每一行表示当前 node-exporter 采集到的一个监控样本:node_cpu_seconds_totalnode_load1 表明了当前指标的名称、大括号中的标签则反映了当前样本的一些特征和维度、浮点数则是该监控样本的具体值。

Prometheus 会将所有采集到的样本数据以时间序列的方式保存在内存数据库中,并且定时保存到硬盘上。时间序列是按照时间戳和值的序列顺序存放的,我们称之为向量(vector),每条时间序列通过指标名称(metrics name)和一组标签集(labelset)命名。如下所示,可以将时间序列理解为一个以时间为 X 轴的数字矩阵:

  ^
  │   . . . . . . . . . .   . .     node_cpu_seconds_total{cpu="cpu0",mode="idle"}
  │     . . . . . . . . . . . .   	node_cpu_seconds_total{cpu="cpu0",mode="system"}
  │     . . . . . . . . . . . .     node_load1{}
  │     . . . . . . . . . . . .  
  v
    <-------- 时间 ------------->

在时间序列中的每一个点称为一个样本(sample),样本由以下三部分组成:

  • 指标(metric):metric name 和描述当前样本特征的 labelsets
  • 时间戳(timestamp):一个精确到毫秒的时间戳
  • 样本值(value): 一个 float64 的浮点型数据表示当前样本的值

如下所示:

<--------------- metric ---------------------><-timestamp -><-value->
http_request_total{status="200", method="GET"}@1434417560938 => 94355
http_request_total{status="200", method="GET"}@1434417561287 => 94334

http_request_total{status="404", method="GET"}@1434417560938 => 38473
http_request_total{status="404", method="GET"}@1434417561287 => 38544

http_request_total{status="200", method="POST"}@1434417560938 => 4748
http_request_total{status="200", method="POST"}@1434417561287 => 4785

在形式上,所有的指标(Metric)都通过如下格式表示:

<metric name>{<label name> = <label value>, ...}
  • 指标的名称(metric name)可以反映被监控样本的含义(比如,http_request_total - 表示当前系统接收到的 HTTP 请求总量)。指标名称只能由 ASCII 字符、数字、下划线以及冒号组成并必须符合正则表达式[a-zA-Z_:][a-zA-Z0-9_:]*
  • 标签(label)反映了当前样本的特征维度,通过这些维度 Prometheus 可以对样本数据进行过滤,聚合等。标签的名称只能由 ASCII 字符、数字以及下划线组成并满足正则表达式 [a-zA-Z_][a-zA-Z0-9_]*

每个不同的 metric_namelabel 组合都称为时间序列,在 Prometheus 的表达式语言中,表达式或子表达式包括以下四种类型之一:

  • 瞬时向量(Instant vector):一组时间序列,每个时间序列包含单个样本,它们共享相同的时间戳。也就是说,表达式的返回值中只会包含该时间序列中的最新的一个样本值。而相应的这样的表达式称之为瞬时向量表达式。
  • 区间向量(Range vector):一组时间序列,每个时间序列包含一段时间范围内的样本数据,这些是通过将时间选择器附加到方括号中的瞬时向量(例如[5m]5分钟)而生成的。
  • 标量(Scalar):一个简单的数字浮点值。
  • 字符串(String):一个简单的字符串值。

所有这些指标都是 Prometheus 定期从 metrics 接口那里采集过来的。采集的间隔时间的设置由 prometheus.yaml 配置中的 scrape_interval 指定。最多抓取间隔为30秒,这意味着至少每30秒就会有一个带有新时间戳记录的新数据点,这个值可能会更改,也可能不会更改,但是每隔 scrape_interval 都会产生一个新的数据点。

指标类型

从存储上来讲所有的监控指标 metric 都是相同的,但是在不同的场景下这些 metric 又有一些细微的差异。 例如,在 Node Exporter 返回的样本中指标 node_load1 反应的是当前系统的负载状态,随着时间的变化这个指标返回的样本数据是在不断变化的。而指标 node_cpu_seconds_total 所获取到的样本数据却不同,它是一个持续增大的值,因为其反应的是 CPU 的累计使用时间,从理论上讲只要系统不关机,这个值是会一直变大。

为了能够帮助用户理解和区分这些不同监控指标之间的差异,Prometheus 定义了4种不同的指标类型:Counter(计数器)、Gauge(仪表盘)、Histogram(直方图)、Summary(摘要)。

Counter

Counter (只增不减的计数器) 类型的指标其工作方式和计数器一样,只增不减。常见的监控指标,如 http_requests_totalnode_cpu_seconds_total 都是 Counter 类型的监控指标。

Counter 是一个简单但又强大的工具,例如我们可以在应用程序中记录某些事件发生的次数,通过以时间序列的形式存储这些数据,我们可以轻松的了解该事件产生的速率变化。PromQL 内置的聚合操作和函数可以让用户对这些数据进行进一步的分析,例如,通过 rate() 函数获取 HTTP 请求量的增长率:

rate(http_requests_total[5m])

查询当前系统中,访问量前 10 的 HTTP 请求:

topk(10, http_requests_total)

Gauge

Counter 不同,Gauge(可增可减的仪表盘)类型的指标侧重于反应系统的当前状态。因此这类指标的样本数据可增可减。常见指标如:node_memory_MemFree_bytes(主机当前空闲的内存大小)、node_memory_MemAvailable_bytes(可用内存大小)都是 Gauge 类型的监控指标。通过 Gauge 指标,用户可以直接查看系统的当前状态:

node_memory_MemFree_bytes

对于 Gauge 类型的监控指标,通过 PromQL 内置函数 delta() 可以获取样本在一段时间范围内的变化情况。例如,计算 CPU 温度在两个小时内的差异:

delta(cpu_temp_celsius{host="zeus"}[2h])

还可以直接使用 predict_linear() 对数据的变化趋势进行预测。例如,预测系统磁盘空间在4个小时之后的剩余情况:

predict_linear(node_filesystem_free_bytes[1h], 4 * 3600)

Histogram 和 Summary

HistogramSummary 主用用于统计和分析样本的分布情况。

在大多数情况下人们都倾向于使用某些量化指标的平均值,例如 CPU 的平均使用率、页面的平均响应时间,这种方式也有很明显的问题,以系统 API 调用的平均响应时间为例:如果大多数 API 请求都维持在 100ms 的响应时间范围内,而个别请求的响应时间需要 5s,那么就会导致某些 WEB 页面的响应时间落到中位数上,而这种现象被称为长尾问题

为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在 0~10ms 之间的请求数有多少而 10~20ms 之间的请求数又有多少。通过这种方式可以快速分析系统慢的原因。HistogramSummary 都是为了能够解决这样的问题存在的,通过 HistogramSummary 类型的监控指标,我们可以快速了解监控样本的分布情况。

例如,指标 prometheus_tsdb_wal_fsync_duration_seconds 的指标类型为 Summary。它记录了 Prometheus Server 中 wal_fsync 的处理时间,通过访问 Prometheus Server 的 /metrics 地址,可以获取到以下监控样本数据:

# HELP prometheus_tsdb_wal_fsync_duration_seconds Duration of WAL fsync.
# TYPE prometheus_tsdb_wal_fsync_duration_seconds summary
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.5"} 0.012352463
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.9"} 0.014458005
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.99"} 0.017316173
prometheus_tsdb_wal_fsync_duration_seconds_sum 2.888716127000002
prometheus_tsdb_wal_fsync_duration_seconds_count 216

从上面的样本中可以得知当前 Prometheus Server 进行 wal_fsync 操作的总次数为216次,耗时2.888716127000002s。其中中位数(quantile=0.5)的耗时为0.012352463,9分位数(quantile=0.9)的耗时为0.014458005s。

在 Prometheus Server 自身返回的样本数据中,我们还能找到类型为 Histogram 的监控指标prometheus_tsdb_compaction_chunk_range_seconds_bucket

# HELP prometheus_tsdb_compaction_chunk_range_seconds Final time range of chunks on their first compaction
# TYPE prometheus_tsdb_compaction_chunk_range_seconds histogram
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="100"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="400"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="1600"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="6400"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="25600"} 405
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="102400"} 25690
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="409600"} 71863
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="1.6384e+06"} 115928
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="6.5536e+06"} 2.5687892e+07
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="2.62144e+07"} 2.5687896e+07
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="+Inf"} 2.5687896e+07
prometheus_tsdb_compaction_chunk_range_seconds_sum 4.7728699529576e+13
prometheus_tsdb_compaction_chunk_range_seconds_count 2.5687896e+07

Summary 类型的指标相似之处在于 Histogram 类型的样本同样会反应当前指标的记录的总数(以 _count 作为后缀)以及其值的总量(以 _sum 作为后缀)。不同在于 Histogram 指标直接反应了在不同区间内样本的个数,区间通过标签 le 进行定义。

查询

当 Prometheus 采集到监控指标样本数据后,我们就可以通过 PromQL 对监控样本数据进行查询。基本的 Prometheus 查询的结构非常类似于一个 metric 指标,以指标名称开始。

查询结构

比如只查询 node_cpu_seconds_total 则会返回所有采集节点的所有类型的 CPU 时长数据,当然如果数据量特别特别大的时候,直接在 Grafana 执行该查询操作的时候,则可能导致浏览器崩溃,因为它同时需要渲染的数据点太多。

接下来,可以使用标签进行过滤查询,标签过滤器支持4种运算符:

  • = 等于
  • != 不等于
  • =~ 匹配正则表达式
  • !~ 与正则表达式不匹配

标签过滤器都位于指标名称后面的{}内,比如过滤 master 节点的 CPU 使用数据可用如下查询语句:

node_cpu_seconds_total{instance="master"}

此外我们还可以使用多个标签过滤器,以逗号分隔。多个标签过滤器之间是 AND 的关系,所以使用多个标签进行过滤,返回的指标数据必须和所有标签过滤器匹配。

例如如下查询语句将返回所有以 node为前缀的节点的并且是 idle 模式下面的节点 CPU 使用时长指标:

node_cpu_seconds_total{instance=~"node.*", mode="idle"}

范围选择器

我们可以通过将时间范围选择器([])附加到查询语句中,指定为每个返回的区间向量样本值中提取多长的时间范围。每个时间戳的值都是按时间倒序记录在时间序列中的,该值是从时间范围内的时间戳获取的对应的值。

时间范围通过数字来表示,单位可以使用以下其中之一的时间单位:

  • s - 秒
  • m - 分钟
  • h - 小时
  • d - 天
  • w - 周
  • y - 年

比如 node_cpu_seconds_total{instance="master", mode="idle"} 这个查询语句,如果添加上 [1m] 这个时间范围选择器,则我们可以得到如下所示的信息:

Kubernetes监控-Prometheus

可以看到上面的五个时间序列都有4个值,这是因为我们 Prometheus 中配置的抓取间隔是15秒,所以,我们从图中的 @ 符号后面的时间戳可以看出,它们之间的间隔基本上就是15秒。

但是现在如果我们在 Prometheus 的页面中查询上面的语句,然后切换到 Graph 选项卡的时候,则会出现如下所示的错误信息:

Kubernetes监控-Prometheus

这是因为现在每一个时间序列中都有多个时间戳多个值,所以没办法渲染,必须是标量或者瞬时向量才可以绘制图形。

不过通常区间向量都会应用一个函数后变成可以绘制的瞬时向量,Prometheus 中对瞬时向量和区间向量有很多操作的函数,不过对于区间向量来说最常用的函数并不多,使用最频繁的有如下几个函数:

  • rate(): 计算整个时间范围内区间向量中时间序列的每秒平均增长率
  • irate(): 仅使用时间范围中的最后两个数据点来计算区间向量中时间序列的每秒平均增长率,irate 只能用于绘制快速变化的序列,在长期趋势分析或者告警中更推荐使用 rate 函数
  • increase(): 计算所选时间范围内时间序列的增量,它基本上是速率乘以时间范围选择器中的秒数

我们选择的时间范围持续时间将确定图表的粒度,比如,持续时间 [1m] 会给出非常尖锐的图表,从而很难直观的显示出趋势来,看起来像这样:

Kubernetes监控-Prometheus

对于一个一小时的图表,[5m] 显示的图表看上去要更加合适一些,更能显示出 CPU 使用的趋势:

Kubernetes监控-Prometheus

对于更长的时间跨度,可能需要设置更长的持续时间,以便消除波峰并获得更多的长期趋势图表。

有的时候可能想要查看5分钟前或者昨天一天的区间内的样本数据,这个时候我们就需要用到位移操作了,位移操作的关键字是 offset,比如我们可以查询30分钟之前的 master 节点 CPU 的空闲指标数据:

node_cpu_seconds_total{instance="master", mode="idle"} offset 30m

需要注意的是 offset 关键字需要紧跟在选择器({})后面。

同样位移操作也适用于区间向量,比如我们要查询昨天的前5分钟的 CPU 空闲增长率:

sum(rate(node_cpu_seconds_total{instance="master", mode="idle"}[5m] offset 1d))

关联查询

Prometheus 没有提供类似与 SQL 语句的关联查询的概念,但是我们可以通过在 Prometheus 上使用 运算符 来组合时间序列,可以应用于多个时间序列或标量值的常规计算、比较和逻辑运算。

如果将运算符应用于两个瞬时向量,则它将仅应用于匹配的时间序列,当且仅当时间序列具有完全相同的标签集的时候,才认为是匹配的。当表达式左侧的每个序列和右侧的一个序列完全匹配的时候,在序列上使用这些运算符才可以实现一对一匹配。

比如如下的两个瞬时向量:

node_cpu_seconds_total{instance="master", cpu="0", mode="idle"}

node_cpu_seconds_total{instance="node1", cpu="0", mode="idle"}

如果我们对这两个序列做加法运算来尝试获取 master 和 node1 节点的总的空闲 CPU 时长,则不会返回任何内容了:

Kubernetes监控-Prometheus

这是因为这两个时间序列没有完全匹配标签。我们可以使用 on 关键字指定只希望在 mode 标签上进行匹配,就可以计算出结果来:

node_cpu_seconds_total{instance="master", cpu="0", mode="idle"} + on  (mode) node_cpu_seconds_total{instance="node1", cpu="0", mode="idle"}

Kubernetes监控-Prometheus

需要注意的是新的瞬时向量包含单个序列,其中仅包含 on 关键字中指定的标签。

不过在 Prometheus 中还有很多 聚合操作,所以,如果我们真的想要获取节点的 CPU 总时长,我们完全不用这么操作,使用 sum 操作要简单得多:

sum(node_cpu_seconds_total{mode="idle"}) by (instance)

on 关键字只能用于一对一的匹配中,如果是多对一或者一对多的匹配情况下,就不行了,比如我们可以通过 kube-state-metrics 这个工具来获取 Kubernetes 集群的各种状态指标,包括 Pod 的基本信息,比如我们执行如下所示的查询语句:

container_cpu_user_seconds_total{namespace="kube-system"} * on (pod) kube_pod_info

就会出现 Error executing query: multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) 这样的错误提示,这是因为左侧的序列数据在同一个 Pod 上面有可能会有多条时间序列,所以不能简单通过 on (pod) 来进行查询。

要解决这个问题,我们可以使用 group_leftgroup_right 关键字。这两个关键字将匹配分别转换为多对一一对多匹配。左侧和右侧表示基数较高的一侧。因此,group_left 意味着左侧的多个序列可以与右侧的单个序列匹配。结果是,返回的瞬时向量包含基数较高的一侧的所有标签,即使它们与右侧的任何标签都不匹配。

例如如下所示的查询语句就可以正常获取到结果,而且获取到的时间序列数据包含所有的标签:

container_cpu_user_seconds_total{namespace="kube-system"} * on (pod) group_left() kube_pod_info

瞬时向量和标量结合

此外我们还可以将瞬时向量和标量值相结合,这个很简单,就是简单的数学计算,比如:

node_cpu_seconds_total{instance="master"} * 10

会为瞬时向量中每个序列的每个值都剩以10。这对于计算比率和百分比得时候非常有用。

  • 除了 * 之外,其他常用的算数运算符当然也支持:+-*/%^
  • 还有其他的比较运算符:==!=><>=<=
  • 逻辑运算符:andorunless,不过逻辑运算符只能用于瞬时向量之间。

除了这些关于 PromQL 最基本的知识点之外,还有很多相关的使用方法,可以参考官网相关介绍:https://prometheus.io/docs/prometheus/latest/querying/basics/。

上一篇:递归查找树中的某个节点


下一篇:ES--HTTP--Postman--索引