什么是 prometheus-operator

其实 prometheus-operator 诞生得非常早,当年大家都还在如火如荼地将业务推上 K8s 的时候,prometheus-operator 就已经被广为人知,并一度作为 Operator 模式的开发典范。发展至今,prometheus-operator 所定义的 CRD 则成为了 Prometheus 生态的事实标准。

prometheus-operator 的主要作用:让服务利用 Prometheus 监控变得 K8s 化。用户只需要通过编写 prometheus-operator 所定义的 CR 就能实现 Prometheus 的部署以及服务的监控和告警。

之前光顾着使用 prometheus-operator,并没有特别了解其内部的实现机制,本文就粗线条地就此做点笔记。

部署与使用

备注:大多数用户在生产环境中部署 Prometheus 全家桶都比较喜欢使用 kube-prometheus-stack,这个 chart 比较臃肿,不是特别喜欢,本文就换一种更直接简单的方式。

部署 prometheus-operator

参考自 prometheus-operator.dev,我们采用最直接的 YAML 来进行部署:

1
2
LATEST=$(curl -s https://api.github.com/repos/prometheus-operator/prometheus-operator/releases/latest | jq -cr .tag_name)
curl -sL https://github.com/prometheus-operator/prometheus-operator/releases/download/${LATEST}/bundle.yaml | kubectl create -f -

以上操作将会在部署:

  1. prometheus-operator 所必须的 CRDs

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    alertmanagerconfigs.monitoring.coreos.com   2024-08-08T07:08:18Z
    alertmanagers.monitoring.coreos.com         2024-08-08T07:08:35Z
    podmonitors.monitoring.coreos.com           2024-08-08T07:08:18Z
    probes.monitoring.coreos.com                2024-08-08T07:08:18Z
    prometheusagents.monitoring.coreos.com      2024-08-08T07:08:36Z
    prometheuses.monitoring.coreos.com          2024-08-08T07:08:36Z
    prometheusrules.monitoring.coreos.com       2024-08-08T07:08:19Z
    scrapeconfigs.monitoring.coreos.com         2024-08-08T07:08:19Z
    servicemonitors.monitoring.coreos.com       2024-08-08T07:08:19Z
    thanosrulers.monitoring.coreos.com          2024-08-08T07:08:37Z
    

    我们实际使用的时候重点关注 prometheuses / servicemonitors / podmonitors

  2. prometheus-operator 服务

    • 定义 ClusterRole 和 ClusterRoleBinding 来定义 ServiceAccount 所需的 Cluster 层 RBAC;

    • 在 prometheus-operator 所在的 namespace 中定义了一个 ServiceAccount 用以关联相应的 ClusterRole;

    • 定义 prometheus-operator 的 Deployments 并暴露一个 Service 来暴露 Metrics;

开启监控

部署 Prometheus

我们需要使用 prometheuses CR 来部署 Prometheus 服务,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
apiVersion: v1
kind: ServiceAccount
metadata:
  name: prometheus
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prometheus
rules:
  - apiGroups: [""]
    resources:
      - nodes
      - nodes/metrics
      - services
      - endpoints
      - pods
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources:
      - configmaps
    verbs: ["get"]
  - apiGroups:
      - discovery.k8s.io
    resources:
      - endpointslices
    verbs: ["get", "list", "watch"]
  - apiGroups:
      - networking.k8s.io
    resources:
      - ingresses
    verbs: ["get", "list", "watch"]
  - nonResourceURLs: ["/metrics"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: prometheus
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus
subjects:
  - kind: ServiceAccount
    name: prometheus
    namespace: default
---
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
spec:
  image: quay.io/prometheus/prometheus:v2.53.0
  serviceAccountName: prometheus
  serviceMonitorSelector:
    matchLabels:
      team: frontend
  podMonitorSelector:
    matchLabels:
      team: frontend
  resources:
    requests:
      memory: 400Mi
  enableAdminAPI: false

上面的 YAML 表达的是:

  1. 定义了 ClusterRole / ClusterRoleBinding / ServiceAccount 来赋予 Prometheus 实例读取 K8s 集群资源的权限,用于 Prometheus 的服务发现

  2. prometheuses CR 定义了一个单实例 Prometheus(底层以 StatefulSet 运行),这里要注意

    • serviceMonitorSelector 定义了必须有 team=frontend label 的 ServiceMonitor 才会被这个 Prometheus 实例抓取数据;

    • podMonitorSelector 定义了必须有 team=frontend label 的 PodMonitor 才会被这个 Prometheus 实例抓取数据;

部署 Example App 和 ServiceMonitor

部署一个 example-app 的 Deployment 和相应的 Service(每一个 Pod 的 <ip>:8080/metrics 都是采集点):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: example-app
  template:
    metadata:
      labels:
        app: example-app
    spec:
      containers:
        - name: example-app
          image: quay.io/brancz/prometheus-example-app:v0.5.0
          ports:
            - name: web
              containerPort: 8080
---
kind: Service
apiVersion: v1
metadata:
  name: example-app
  labels:
    app: example-app
spec:
  selector:
    app: example-app
  ports:
    - name: web
      port: 8080

接着使用 ServiceMonitor 来开启监控(注意 metadata 上必须加上 team=frontend):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: example-app
  labels:
    team: frontend
spec:
  selector:
    matchLabels:
      app: example-app
  endpoints:
    - port: web

此时我们可以观察 Prometheus 的 WebUI( kubectl port-forward svc/prometheus-operated 9090:9090),即可以发现已经有被监控的 3 个实例:

其实我们还可以通过观察 Promtheus 的配置文件来发现监控是否生效:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ kubectl exec prometheus-prometheus-0 -- cat /etc/prometheus/config_out/prometheus.env.yaml
...
scrape_configs:
- job_name: serviceMonitor/default/example-app/0
  honor_labels: false
  kubernetes_sd_configs:
  - role: endpoints
    namespaces:
      names:
      - default
  relabel_configs:
  - source_labels:
    - job
    target_label: __tmp_prometheus_job_name
  - action: keep
    source_labels:
    - __meta_kubernetes_service_label_app
    - __meta_kubernetes_service_labelpresent_app
    regex: (example-app);true
...

可以观察到出现了名为 serviceMonitor/default/example-app/0 的 Scrape Job。

ServiceMonitor 和 PodMonitor 的选择

上面的例子同样可以用 PodMonitor,效果是等价的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: example-app
  labels:
    team: frontend
spec:
  selector:
    matchLabels:
      app: example-app
  podMetricsEndpoints:
  - port: web

一般而言:

  • ServiceMonitor 用于监控对应 Service 背后的 Pod 的 Metrics,比较适合被监控 Pod 有一致的 Service 的场景;
  • PodMonitor 用于监控对应 Labels 下背后 Pod 的 Metrics,比较适合被监控 Pod 没有 Service 且多个 Pod 部署规则并不统一的场景;

* 部署 Grafana

我们还可以部署 Grafana 来做更多可视化看板:

  1. 添加 chart 源

    1
    2
    
    helm repo add grafana https://grafana.github.io/helm-charts
    helm repo update
    
  2. 部署 Grafana

    1
    2
    3
    4
    5
    
    helm upgrade \
      --install grafana grafana/grafana \
      --set image.registry=docker.io \
      --create-namespace \
      -n grafana
    
  3. 获取默认的 admin 密码

    1
    2
    3
    
    kubectl get secret \
      -n grafana grafana \
      -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
    

基本设计

prometheus-operator 的控制链路其实非常简单,我们只需要知道以下基本原理就行:

  • PodMonitor 和 ServiceMonitor 最终是用于生成 Prometheus 配置文件中的 srape_config
  • Prometheus 可以使用一个 HTTP Post 请求 /-/reload 来在运行时重新加载配置文件使新的 scrape_config 生效;

以上文为例,prometheus-operator 的控制链路是:

  1. Prometheus / PrometheusAgent

    prometheus-operator 监听 Promtheus 资源,当有 Add Event 发生时,prometheus-operator 将以 StatefulSet 的形式部署 Prometheus 实例。每一个 Prometheus Pod 里有两个容器:

    • prometheus 容器:主容器,使用 /etc/prometheus/config_out/prometheus.env.yaml 作为主要的配置文件

      1
      2
      3
      
      ...
      - --config.file=/etc/prometheus/config_out/prometheus.env.yaml
      ...
      
    • prometheus-config-reloader 容器:辅助容器,用于监听上游配置文件的变化并调用主容器的 reload 接口重新加载配置;

    当我们观察这个 Pod 的 volumes 时候,有两个 volume 可以重点关注:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    ...
    volumes:
      - name: config
        secret:
          defaultMode: 420
          secretName: prom-agent-prometheus-agent
      - name: config-out
        emptyDir:
          medium: Memory
    ...
    
    • config-out:EmptyDir 类型的卷,主要是用于 prometheus 容器与 prometheus-config-reloader 容器的数据共享,同时挂载于两个容器的 /etc/prometheus/config_out/ 中;

    • config:底层是一个 Secret,这个 config 将被挂载为 prometheus-config-reloader 容器中的 /etc/prometheus/config/prometheus.yaml.gzprometheus-config-reloader 会监听这个文件的变化,一旦有变化,将基于新的文件内容生成新的配置文件 /etc/prometheus/config_out/prometheus.env.yamlprometheus-config-reloader 将调用主容器的 reload API 来重新加载配置文件;

  2. PodMonitor / ServiceMonitor

    prometheus-operator 监听 PodMonitor 和 ServiceMonitor 的变化。一旦对应资源发生了变化,prometheus-operator 将基于新的资源生成新的配置文件并将其更新到对应的 Secrets 中。由于对应 Secrets 以文件形式挂载于 prometheus-config-reloader 容器中并被监听文件变化,所以当底层 Secrets 发生了变化,kubelet 将分钟级传播变化到对应容器内部,从而触发 prometheus-config-reloader 容器内的监听逻辑。

    Secrets 内部是一个 Base64 之后的 gz 格式的 prometheus.yaml,比如我们可以用下面这种方式解码出 prometheus.yaml

    1
    
    kubectl get secrets prom-agent-prometheus-agent -o jsonpath="{.data.prometheus\.yaml\.gz}" | base64 -d | gzip -d > prometheus.yaml
    

Prometheus Agent Mode

为什么需要 Prometheus Agent

Prometheus Agent 其实只是 Prometheus 的一种特殊运行状态,在 prometheus-operator 中以 PrometheusAgent 这个 CRD 体现,但其内部控制逻辑与 Prometheus CRD 一致。

之所以需要 Prometheus Agent,我们其实可以从 Prometheus 的这篇博客一窥究竟。Prometheus Agent 本质上就是将时序数据库能力从 Prometheus 中剥离,并优化 Remote Write 性能,从而让其成为了一个支持 Prometheus 采集语义的高性能 Agent。这样一来,Prometheus Agent 还可以部署在一些资源受限的边缘场景进行数据采集。

“众所周知”,Prometheus 作为数据库而言,查询性能和可扩展性相对较弱,这也是为什么 Remote Write 会如此流行以至于又成为了一个事实上的标准:因为大家都希望将数据转存在性能更高的数据库上但又希望继续兼容 Prometheus 的采集逻辑(因为很好用)。Agent 模式其实如大家所意,禁用了查询、报警和本地存储功能,并用了一个特殊的 TSDB WAL 来临时存储数据,从而整体架构如下所示:

这种架构某种程度是推拉结合的模式。Metrics 的采集采用 Pull 模式,而其存储则采用 Push 模式。对于高吞吐的写入,Push 模式其实对写入更友好。因为我们总是可以以 Batch 模式来集中向远端写入大批数据。这种模式下的 Prometheus 其实是无状态,更便于部署和 Scrape Job 的分片。

其实,这类兼容 Prometheus 采集语义的 Agent 社区有不少可供选择,比如 vmagentvector。VictoriaMetrics 还曾经对 Prometheus Agent, vmagant 和 Grafana Agant 做过一个性能报告。不过很快,Grafana Agent 就停止开发并转成维护模式。Grafana 又造了另一个项目 Alloy,重点支持 OpenTelemetry,当然又造了一个与 Terraform 语法酷似的配置语言的 DSL。

从长期技术演技来看,Agent 总是兵家必争之地,因为可以守住数据入口可以做的事情比较多。大家总是希望 Agent 能:

  • 具有极低的 CPU 和 Memory footprint,因为它们通常会以 sidecar 或者 daemonset 的形式进行部署,资源极度受限;
  • 兼容更多的前端采集协议和后端写入逻辑
  • 具备一定的数据的编排能力(或者称为 pipeline ?),即采集后的数据能以一定的规则进行改写和转换;
  • 技术中立

感觉这块也有不少东西可以聊的,后面可以找时间再分析分析。

与 GreptimeDB 的集成

GreptimeDB 作为一个新款的开源 TSDB 很早就支持了 Prometheus Remote Write。我们其实可以直接使用 PrometheusAgent 这个 CRD 来定义基于 GreptimeDB Remote Write 的 Prometheus Agent。这样以来,用户其实无需做过多 CR 的改动就能直接将数据接入到 GreptimeDB 中

  1. 部署 greptimedb-operator

    1
    2
    3
    4
    5
    6
    7
    8
    
    helm repo add greptime https://greptimeteam.github.io/helm-charts/
    helm repo update
    
    helm upgrade \
      --install \
      --create-namespace \
      greptimedb-operator greptime/greptimedb-operator \
      -n greptimedb-admin
    

    greptimedb-operator 同时支持管理 GreptimeDB Standalone 和 Cluster 模式,用户可以根据自己需要创建相应的 CR。

  2. 快速启动一个 Standalone 模式下的 GreptimeDB

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    apiVersion: greptime.io/v1alpha1
    kind: GreptimeDBStandalone
    metadata:
      name: my-greptimedb
      namespace: default
    spec:
      base:
        main:
          image: greptime/greptimedb:latest
    

    我们可以通过观察 GreptimeDBStandalone 的状态来判断其是否启动成功:

    1
    2
    3
    
    $ kubectl get gts my-greptimedb
    NAME            PHASE     VERSION   AGE
    my-greptimedb   Running   latest    21s
    
  3. 创建 Promethus Agent 实例并将 Remote Write 设置为 GreptimeDB

    步骤 2 中创建的 Standalone 模式的 GreptimeDB 模式将暴露名为 ${dbname}-standalone.${namespace} 的 Service,由此我们可以确定其 Remote Write 的 URL 为 http://my-greptimedb-frontend.default:4000/v1/prometheus/write?db=public

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: prometheus-agent
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: prometheus-agent
    rules:
      - apiGroups: [""]
        resources:
          - nodes
          - nodes/metrics
          - services
          - endpoints
          - pods
        verbs: ["get", "list", "watch"]
      - apiGroups: [""]
        resources:
          - configmaps
        verbs: ["get"]
      - apiGroups:
          - discovery.k8s.io
        resources:
          - endpointslices
        verbs: ["get", "list", "watch"]
      - apiGroups:
          - networking.k8s.io
        resources:
          - ingresses
        verbs: ["get", "list", "watch"]
      - nonResourceURLs: ["/metrics"]
        verbs: ["get"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: prometheus-agent
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: prometheus-agent
    subjects:
      - kind: ServiceAccount
        name: prometheus-agent
        namespace: default
    ---
    apiVersion: monitoring.coreos.com/v1alpha1
    kind: PrometheusAgent
    metadata:
      name: prometheus-agent
    spec:
      image: quay.io/prometheus/prometheus:v2.53.0
      replicas: 1
      serviceAccountName: prometheus-agent
      remoteWrite:
        - url: http://my-greptimedb-frontend.default:4000/v1/prometheus/write?db=public
      serviceMonitorSelector:
        matchLabels:
          team: frontend
    
  4. 继续保持上文中的 Example App,让我们用 MySQL 协议查看数据是否成功写入

    先用 kubectl port-forward 命令打开 4002 的 MySQL 协议端口:

    1
    
    kubectl -n default port-forward svc/my-greptimedb-standalone 4002:4002
    

    使用 MySQL 协议进行连接查看数据:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    mysql> show tables;
    +---------------------------------------+
    | Tables                                |
    +---------------------------------------+
    | greptime_physical_table               |
    | numbers                               |
    | scrape_duration_seconds               |
    | scrape_samples_post_metric_relabeling |
    | scrape_samples_scraped                |
    | scrape_series_added                   |
    | up                                    |
    | version                               |
    +---------------------------------------+
    8 rows in set (0.01 sec)
    
    mysql> select * from version limit 10;
    +-------------+----------+----------------------------+----------------+-----------------+-------------+-----------+------------------------------+--------------------------+-------------------------------+-------------+---------+
    | container   | endpoint | greptime_timestamp         | greptime_value | instance        | job         | namespace | pod                          | prometheus               | prometheus_replica            | service     | version |
    +-------------+----------+----------------------------+----------------+-----------------+-------------+-----------+------------------------------+--------------------------+-------------------------------+-------------+---------+
    | example-app | web      | 2024-08-11 13:51:11.897000 |              1 | 10.244.1.6:8080 | example-app | default   | example-app-787b775559-x4bsf | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:51:41.897000 |              1 | 10.244.1.6:8080 | example-app | default   | example-app-787b775559-x4bsf | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:52:11.897000 |              1 | 10.244.1.6:8080 | example-app | default   | example-app-787b775559-x4bsf | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:52:41.897000 |              1 | 10.244.1.6:8080 | example-app | default   | example-app-787b775559-x4bsf | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:51:06.268000 |              1 | 10.244.4.5:8080 | example-app | default   | example-app-787b775559-n6sqc | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:51:36.268000 |              1 | 10.244.4.5:8080 | example-app | default   | example-app-787b775559-n6sqc | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:52:06.268000 |              1 | 10.244.4.5:8080 | example-app | default   | example-app-787b775559-n6sqc | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:52:36.268000 |              1 | 10.244.4.5:8080 | example-app | default   | example-app-787b775559-n6sqc | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:53:06.268000 |              1 | 10.244.4.5:8080 | example-app | default   | example-app-787b775559-n6sqc | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    | example-app | web      | 2024-08-11 13:51:20.090000 |              1 | 10.244.2.5:8080 | example-app | default   | example-app-787b775559-6bvz9 | default/prometheus-agent | prom-agent-prometheus-agent-0 | example-app | v0.5.0  |
    +-------------+----------+----------------------------+----------------+-----------------+-------------+-----------+------------------------------+--------------------------+-------------------------------+-------------+---------+
    10 rows in set (0.01 sec)
    

    数据写入成功

  5. *使用 Grafana 渲染数据

    我们可以直接跳过 Remote Read 直接对接 GreptimeDB,将其作为 Prometheus Datasource,设置对应的 URL 为:http://my-greptimedb-standalone.default:4000/v1/prometheus/