promethus高可用

1. 高可用性

单台的 Prometheus 存在单点故障的风险,随着监控规模的扩大,Prometheus 产生的数据量也会非常大,性能和存储都会面临问题。毋庸置疑,我们需要一套高可用的 Prometheus 集群。

1. 可用性

我们知道 Prometheus 是采用的 Pull 机制获取监控数据,即使使用 Push Gateway 对于 Prometheus 也是 Pull,为了确保 Prometheus 服务的可用性,我们只需要部署多个 Prometheus 实例,然后采集相同的 metrics 数据即可,如下图所示:

prometheus ha1

这个方式来满足服务的可用性应该是平时我们使用得最多的一种方式,当一个实例挂掉后从 LB 里面自动剔除掉,而且还有负载均衡的作用,可以降低一个 Prometheus 的压力,但这种模式缺点也是非常明显的,就是不满足数据一致性以及持久化问题,因为 Prometheus 是 Pull 的方式,即使多个实例抓取的是相同的监控指标,也不能保证抓取过来的值就是一致的,更何况在实际的使用过程中还会遇到一些网络延迟问题,所以会造成数据不一致的问题,不过对于监控报警这个场景来说,一般也不会要求数据强一致性,所以这种方式从业务上来说是可以接受的,因为这种数据不一致性影响基本上没什么影响。这种场景适合监控规模不大,只需要保存短周期监控数据的场景。

2. 数据持久化

使用上面的基本 HA 的模式基本上是可以满足监控这个场景,但是还有一个数据持久化的问题,如果其中一个实例数据丢了就没办法呢恢复回来了,这个时候我们就可以为 Prometheus 添加远程存储来保证数据持久化。

prometheus ha2

在给 Prometheus 配置上远程存储过后,我们就不用担心数据丢失的问题了,即使当一个 Prometheus 实例宕机或者数据丢失过后,也可以通过远程存储的数据进行恢复。

3. 通过锁获取 Leader

其实上面的基本 HA 加上远程存储的方式基本上可以满足 Prometheus 的高可用了,这种方式的多个 Prometheus 实例都会去定时拉取监控指标数据,然后将热数据存储在本地,然后冷数据同步到远程存储中去,对于大型集群来说频繁的去拉取指标数据势必会对网络造成更大的压力。所以我们也通过服务注册的方式来实现 Prometheus 的高可用性,集群启动的时候每个节点都尝试去获取锁,获取成功的节点成为 Leader 执行任务,若主节点宕机,从节点获取锁成为 Leader 并接管服务。

prometheus ha3

不过这种方案需要我们通过去写代码进行改造,如果在 Kubernetes 中我们完全可以使用自带的 Lease 对象来获取分布式锁 🔒,这不是很困难,只是以后要更新版本稍微麻烦点。

上面的几种方案基本上都可以满足基本的 Prometheus 高可用,但是对于大型集群来说,一个 Prometheus 实例的压力始终非常大。

4. 联邦集群

当单个 Prometheus 实例无法处理大量的采集任务时,这个时候我们就可以使用基于 Prometheus 联邦集群的方式来将监控任务划分到不同的 Prometheus 实例中去。

prometheus ha4

我们可以将不同类型的采集任务划分到不同的 Prometheus 实例中去执行,进行功能分片,比如一个 Prometheus 负责采集节点的指标数据,另外一个 Prometheus 负责采集应用业务相关的监控指标数据,最后在上层通过一个 Prometheus 对数据进行汇总。

具体的采集任务如何去进行分区也没有固定的标准,需要结合实际的业务进行考虑,除了上面的方式之外,还有一种情况就是单个的采集数据量就非常非常大,比如我们要采集上万个节点的监控指标数据,这种情况即使我们已经进行了分区,但是对于单个 Prometheus 来说压力也是非常大的,这个时候我们就需要按照任务的不同实例进行划分,我们通过 Prometheus 的 relabel 功能,通过 hash 取模的方式可以确保当前 Prometheus 只采集当前任务的一部分实例的监控指标。

# 省略其他配置......
relabel_configs:
- source_labels: [__address__]
modulus: 4 # 将节点分片成 4 个组
target_label: __tmp_hash
action: hashmod
- source_labels: [__tmp_hash]
regex: ^1$ # 只抓第2个组中节点的数据(序号0为第1个组)
action: keep
 

到这里我们基本上就完成了 Prometheus 高可用的改造。对于小规模集群和大规模集群可以采用不同的方案,但是其中有一个非常重要的部分就是远程存储,我们需要保证数据的持久化就必须使用远程存储。所以下面我们将重点介绍下远程存储的使用,这里我们先讲解目前比较流行的方案:Thanos,它完全兼容 Prometheus API,提供统一查询聚合分布式部署 Prometheus 数据的能力,同时也支持数据长期存储到各种对象存储(比如 S3、阿里云 OSS 等)以及降低采样率来加速大时间范围的数据查询。

 

2. Thanos 架构

Thanos 是一个基于 Prometheus 实现的监控方案,其主要设计目的是解决原生 Prometheus 上的痛点,并且做进一步的提升,主要的特性有:全局查询,高可用,动态拓展,长期存储。

 

1. 架构

Thanos 主要由如下几个特定功能的组件组成:

  • 边车组件(Sidecar):连接到 Prometheus,并把 Prometheus 暴露给查询网关(Querier/Query),以供实时查询,并且可以上传 Prometheus 数据到云存储,以供长期保存
  • 查询网关(Querier):实现 Prometheus API 以聚合来自底层组件(如边车组件 Sidecar,或是存储网关 Store Gateway)的数据
  • 存储网关(Store Gateway):将云存储中的数据内容暴露出来
  • 压缩器(Compactor):将云存储中的数据进行压缩和下采样和保留
  • 接收器(Receiver):从 Prometheus 的远程写入 WAL 接收数据,将其暴露出去或者上传到云存储
  • 规则组件(Ruler):根据 Thanos 中的数据评估记录和警报规则
  • 查询前端:实现 Prometheus 的 API,将其代理给 Query,同时缓存响应

从使用角度来看有两种方式去使用 Thanos,sidecar 模式和 receiver 模式。

sidecar 架构模式

Thanos Sidecar 组件需要和 Pormetheus 实例一起部署,用于代理 Thanos Querier 组件对本地 Prometheus 的数据读取,允许 Querier 使用通用、高效的 StoreAPI 查询 Prometheus 数据。第二是将 Prometheus 本地监控数据通过对象存储接口上传到对象存储中,这个上传是实时的,只要发现有新的监控数据保存到磁盘,会将这些监控数据上传至对象存储。

sidecar

Thanos Sidecar 组件在 Prometheus 的远程读 API 之上实现了 Thanos 的 Store API,这使得 Querier 可以将 Prometheus 服务器视为时间序列数据的另一个来源,而无需直接与它的 API 进行交互。

因为 Prometheus 每 2 小时生成一个时序数据块,Thanos Sidecar 会每隔 2 小时将这个块上传到一个对象存储桶中。这样 Prometheus 服务器就可以以相对较低的存储空间运行,同时通过对象存储提供历史数据,使得监控数据具有持久性和可查询性。但是这样并不意味着 Prometheus 可以完全无状态,因为如果 Prometheus 崩溃并重新启动,我们将失去大约 2 个小时的指标数据,所以 Prometheus 在实际运行中还是需要持久性磁盘的。

receiver 架构模式

Thanos Receiver 实现了 Prometheus 远程写 API,它构建在现有的 Prometheus TSDB 之上,并保持其实用性,同时通过长期存储、水平可伸缩性和下采样扩展其功能。Prometheus 实例被配置为连续地向它写入指标,然后 Thanos Receiver 默认每 2 小时将时间序列格式的监控数据块上传到一个对象存储的桶中。Thanos Receiver 同样暴露了 Store API,以便 Thanos Querier 可以实时查询接收到的指标。

receiver

这两种模式有各种的优缺点,具体使用哪种模式需要结合生产环境综合考虑。

2. 工作流程

Thanos 是同时支持 Prometheus 读和写的远程存储方案,首先我们先看下 sidecar 模式下指标写入的整个流程:

  • 首先 Prometheus 从所采集服务的 metrics 接口抓取指标数据,同时根据自身所配置的 recording rules 定期对抓取到的指标数据进行评估,将结果以 TSDB 格式分块存储到本地,每个数据块的存储时长为 2 小时,且默认禁用了压缩功能。
  • 然后 sidecar 嗅探到 Prometheus 的数据存储目录生成了新的只读数据块时,会将该数据块上传到对象存储桶中做为长期历史数据保存,在上传时会将数据块中的 meta.json 进行修改添加 thanos 相关的字段,如 external_labels
  • rule 根据所配置的 recording rules 定期地向 query 发起查询获取评估所需的指标值,并将结果以 TSDB 格式分块存储到本地。每个数据块的存储时长为 2 小时,且默认禁用了压缩功能,每个数据块的 meta.json 也附带了 thanos 拓展的 external_lables 字段。当本地生成了新的只读数据块时,其自身会将该数据块上传到远端对象存储桶中做为长期历史数据保存。
  • compact 定期将对象存储中地数据块进行压缩和降准采样,进行压缩时数据块中的 truck 会进行合并,对应的 meta.json 中的 level 也会一同增长,每次压缩累加 1,初始值为 1。在进行降准采样时会创建新的数据块,根据采样步长从原有的数据块中抽取值存储到新的数据块中,在 meta.json 中记录 resolution 为采样步长。

读取指标的流程为:

  • 首先客户端通过 query API 向 query 发起查询,query 将请求转换成 StoreAPI 发送到其他的 querysidecarrule 和 store 上。
  • sidecar 接收到来自于 query 发起的查询请求后将其转换成 query API 请求,发送给其绑定的 Prometheus,由 Prometheus 从本地读取数据并响应,返回短期的本地采集和评估数据。
  • rule 接收到来自于 query 发起的查询请求后直接从本地读取数据并响应,返回短期的本地评估数据。
  • store 接收到来自于 query 发起的查询请求后首先从对象存储桶中遍历数据块的 meta.json,根据其中记录的时间范围和标签先进行一次过滤。接下来从对象存储桶中读取数据块的 index 和 chunks 进行查询,部分查询频率较高的index 会被缓存下来,下次查询使用到时可以直接读取。最终返回长期的历史采集和评估指标。

对于发送报警的流程如下所示:

  • Prometheus 根据自身配置的 alerting 规则定期地对自身采集的指标进行评估,当告警条件满足的情况下发起告警到 Alertmanager 上。
  • rule 根据自身配置的 alerting 规则定期的向 query 发起查询请求获取评估所需的指标,当告警条件满足的情况下发起告警到 Alertmanager 上。
  • Alertmanager 接收到来自于 Prometheus 和 rule 的告警消息后进行分组合并后发出告警通知。

3. 特性

Thanos 相比起原生的 Prometheus 具有以下的一些优势:

  • 统一查询入口——以 Querier 作为统一的查询入口,其自身实现了 Prometheus 的查询接口和 StoreAPI,可为其他的 Querier 提供查询服务,在查询时会从每个 Prometheus 实例的 Sidecar 和 Store Gateway 获取到指标数据。
  • 查询去重——每个数据块都会带有特定的集群标签, Querier 在做查询时会去除集群标签,将指标名称和标签一致的序列根据时间排序合并。虽然指标数据来自不同的采集源,但是只会响应一份结果而不是多份重复的结果。
  • 高空间利用率——每个 Prometheus 本身不存储长时间的数据,Sidecar 会将 Prometheus 已经持久化的数据块上传到对象存储中。Compactor 会定时将远端对象存储中的长期数据进行压缩,并且根据采样时长做清理,节约存储空间。
  • 高可用——Querier 是无状态服务,天生支持水平拓展和高可用。StoreRule 和 Sidecar 是有状态服务,在多副本部署的情况下也支持高可用,不过会产生数据冗余,需要牺牲存储空间。
  • 存储长期数据——Prometheus 实例的 Sidecar 会将本地数据上传到远端对象存储中作为长期数据
  • 横向拓展——当 Prometheus 的指标采集压力过大时,可以创建新的 Prometheus 实例,将 scrape job 拆分给多个 Prometheus,Querier 从多个 Prometheus 查询汇聚结果,降低单个 Prometheus 的压力
  • 跨集群查询——需要合并多个集群的查询结果时,仅需要在每个集群的 Querier 之上再添加一层 Querier 即可,这样的层层嵌套,可以使得集群规模无限制拓展。
 

3. Sidecar 组件

1. 对应的 RBAC 权限声明:

复制代码
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/v1
kind: ClusterRoleBinding
metadata:
  name: prometheus
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus
subjects:
  - kind: ServiceAccount
    name: prometheus
    namespace: kube-mon
View Code
复制代码

2. 然后需要部署 Prometheus 的配置文件,下面的资源对象是创建 Prometheus 配置文件的模板,该模板将由 Thanos sidecar 组件进行读取,最终会通过该模板生成实际的配置文件,在同一个 Pod 中的 Prometheus 容器将读取最终的配置文件,在配置文件中添加 external_labels 标签是非常重要的,以便让 Queirer 可以基于这些标签对数据进行去重处理:

复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: kube-mon
data:
  prometheus.yaml.tmpl: | # 注意这里的名称是 prometheus.yaml.tmpl
    global:
      scrape_interval: 15s
      scrape_timeout: 15s
      external_labels:
        cluster: ydzs-test
        replica: $(POD_NAME)  # 每个 Prometheus 有一个唯一的标签
    scrape_configs:
    - job_name: 'prometheus'
      static_configs:
      - targets: ['localhost:9090']


    rule_files:  # 报警规则文件配置
    - /etc/prometheus/rules/*rules.yaml
View Code
复制代码

3. 上面配置了报警规则文件,由于这里配置文件太大了,所以为了更加清晰,我们将报警规则文件拆分到另外的 ConfigMap 对象中来,下面我们配置了两个报警规则:# prometheus-rules.yaml

复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-rules
  namespace: kube-mon
data:
  alert-rules.yaml: |-
    groups:
      - name: K8sObjects_Alerts
        rules:
          - alert: Deployment_Replicas_0
            expr: |
              sum(kube_deployment_status_replicas) by (deployment, namespace) < 1
            for: 1m
            labels:
              severity: warning
            annotations:
              summary: Deployment {{$labels.deployment}} of {{$labels.namespace}} is currently having no pods running
              description: Has no pods running in Deployment {{$labels.deployment}} of {{$labels.namespace}}, you can describe to get events, or get replicas status.
View Code
复制代码

Thanos 通过 Sidecar 和现有的 Prometheus 进行集成,将 Prometheus 的数据备份到对象存储中,所以首先我们需要将 Prometheus 和 Sidecar 部署在同一个 Pod 中,另外 Prometheus 中一定要开启下面两个参数:

  • --web.enable-admin-api 允许 Thanos 的 Sidecar 从 Prometheus 获取元数据。
  • --web.enable-lifecycle 允许 Thanos 的 Sidecar 重新加载 Prometheus 的配置和规则文件。

由于 Prometheus 默认每2h生成一个 TSDB 数据块,所以仍然并不意味着 Prometheus 可以是完全无状态的,因为如果它崩溃并重新启动,我们将丢失〜2 个小时的指标,因此强烈建议依然对 Prometheus 做数据持久化,所以我们这里使用了 StatefulSet 来管理这个应用,添加 volumeClaimTemplates 来声明了数据持久化的 PVC 模板:

复制代码
# local-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
View Code
复制代码

创建pv:

复制代码
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /data/k8s/prometheus-ha  # node1节点上的目录
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node01

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local2
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /data/k8s/prometheus-ha2  # node1节点上的目录
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node01
View Code
复制代码

创建thanos-sidecar

复制代码
# thanos-sidecar.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: prometheus
  namespace: kube-mon
  labels:
    app: prometheus
spec:
  serviceName: prometheus
  replicas: 2
  selector:
    matchLabels:
      app: prometheus
      thanos-store-api: "true"
  template:
    metadata:
      labels:
        app: prometheus
        thanos-store-api: "true"
    spec:
      serviceAccountName: prometheus
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - prometheus
      volumes:
        - name: prometheus-config
          configMap:
            name: prometheus-config
        - name: prometheus-rules
          configMap:
            name: prometheus-rules
        - name: prometheus-config-shared
          emptyDir: {}
      initContainers:
        - name: fix-permissions
          image: busybox:stable
          command: [chown, -R, "nobody:nobody", /prometheus]
          volumeMounts:
            - name: data
              mountPath: /prometheus
      containers:
        - name: prometheus
          image: prom/prometheus:v2.34.0
          imagePullPolicy: IfNotPresent
          args:
            - "--config.file=/etc/prometheus-shared/prometheus.yaml"
            - "--storage.tsdb.path=/prometheus"
            - "--storage.tsdb.retention.time=6h"
            - "--storage.tsdb.no-lockfile"
            - "--storage.tsdb.min-block-duration=2h" # Thanos处理数据压缩
            - "--storage.tsdb.max-block-duration=2h"
            - "--web.enable-admin-api" # 通过一些命令去管理数据
            - "--web.enable-lifecycle" # 支持热更新  localhost:9090/-/reload 加载
          ports:
            - name: http
              containerPort: 9090
          resources:
            requests:
              memory: 500Mi
              cpu: 256m
            limits:
              memory: 500Mi
              cpu: 256m
          volumeMounts:
            - name: prometheus-config-shared
              mountPath: /etc/prometheus-shared/
            - name: prometheus-rules
              mountPath: /etc/prometheus/rules
            - name: data
              mountPath: /prometheus
        - name: thanos
          image: thanosio/thanos:v0.25.1
          imagePullPolicy: IfNotPresent
          args:
            - sidecar
            - --log.level=debug
            - --tsdb.path=/prometheus
            - --prometheus.url=http://localhost:9090
            - --reloader.config-file=/etc/prometheus/prometheus.yaml.tmpl
            - --reloader.config-envsubst-file=/etc/prometheus-shared/prometheus.yaml
            - --reloader.rule-dir=/etc/prometheus/rules/
          ports:
            - name: http-sidecar
              containerPort: 10902
            - name: grpc
              containerPort: 10901
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          resources:
            requests:
              memory: 500Mi
              cpu: 256m
            limits:
              memory: 500Mi
              cpu: 256m
          volumeMounts:
            - name: prometheus-config-shared
              mountPath: /etc/prometheus-shared/
            - name: prometheus-config
              mountPath: /etc/prometheus
            - name: prometheus-rules
              mountPath: /etc/prometheus/rules
            - name: data
              mountPath: /prometheus
  volumeClaimTemplates: # 由于prometheus每2h生成一个TSDB数据块,所以还是需要保存本地的数据
    - metadata:
        name: data
        labels:
          app: prometheus
      spec:
        storageClassName: local-storage # 不要用nfs存储
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 5Gi
复制代码

由于 Prometheus 和 Thanos 的 Sidecar 在同一个 Pod 中了,所以我们完全可以用 localhost 就可以访问到了,然后将数据目录做了声明挂载,所以同样可以在两个容器中共享数据目录了,一定要注意几个配置文件的挂载方式。此外在上面的配置文件中我们通过 POD_NAME 这个环境变量作为 external 标签附加到了 Prometheus 实例上,这里我们通过 Downward API 去设置该环境变量。

由于现在使用的是 StatefulSet 控制器,所以需要创建一个 Headless Service,而且后面的 Thanos Query 还将使用该无头服务来查询所有 Prometheus 实例中的数据,当然我们也可以为每一个 Prometheus 实例去创建一个 Service 对象便于调试,当然这个不是必须的: 

复制代码
# prometheus-headless.yaml
# 该服务为 querier 创建 srv 记录,以便查找 store-api 的信息
apiVersion: v1
kind: Service
metadata:
  name: thanos-store-gateway
  namespace: kube-mon
spec:
  type: ClusterIP
  clusterIP: None
  ports:
    - name: grpc
      port: 10901
      targetPort: grpc
  selector:
    thanos-store-api: "true"
View Code
复制代码

然后我们就可以使用上面的这些资源对象来创建带有 Thanos Sidecar 容器的高可用 Prometheus 应用了:

复制代码
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/prometheus-rbac.yaml
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/prometheus-config.yaml
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/prometheus-rules.yaml
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/prometheus-headless.yaml
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-sidecar.yaml
☸ ➜ kubectl get pods -n kube-mon -l app=prometheus
NAME           READY   STATUS    RESTARTS      AGE
prometheus-0   2/2     Running   1 (78s ago)   106s
prometheus-1   2/2     Running   1 (47s ago)   75s
View Code
复制代码

创建成功后可以看到 Prometheus 中包含两个容器,其中的 Sidecar 容器启动的时候有两个非常重要的参数 --reloader.config-file 与 --reloader.config-envsubst-file,第一个参数是指定 Prometheus 配置文件的模板文件,然后通过渲染配置模板文件,这里就是将 external_labels.replica: $(POD_NAME) 的标签值用环境变量 POD_NAME 进行替换,然后将渲染后的模板文件放到 config-envsubst-file 指定的路径,也就是 /etc/prometheus-shared/prometheus.yaml,所以应用主容器也通过 --config.file 来指定的该配置文件路径。我们也可以查看 Sidecar 容器的相关日志来验证:

kubectl logs -f prometheus-0 -n kube-mon -c thanos

由于在 Sidecar 中我们并没有配置对象存储相关参数,所以出现了 no supported bucket was configured, uploads will be disabled 的警告信息,也就是现在并不会上传我们的指标数据,到这里我们就将 Thanos Sidecar 组件成功部署上了。

 4. Store 组件

上面我们安装了 Thanos 的 Sidecar 和 Querier 组件,已经可以做到 Prometheus 的高可用,通过 Querier 提供一个统一的入口来查询监控数据,而且还可以对监控数据自动去重,但是还有一个非常重要的地方是还没有配置对象存储,如果想要查看历史监控数据就不行了,这个时候我们就需要去配置 Thanos Store 组件,将历史监控指标存储在对象存储中去。

目前 Thanos 支持的对象存储有:

Thanos Store Object Store

要在生产环境使用最好使用 Stable 状态的,比如 S3 或者兼容 S3 的服务,比如 Ceph、Minio 等等。

对于国内用户当然最方便的还是直接使用阿里云 OSS 或者腾讯云 COS 这样的服务,但是很多时候可能我们的服务并不是跑在公有云上面的,所以这里我们用 Minio 来部署一个兼容 S3 协议的对象存储服务。

安装 Minio

MinIO 是一个基于 Apache License v2.0 开源协议的高性能分布式对象存储服务,为大规模私有云基础设施而设计。它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几 kb 到最大 5T 不等。

要安装 Minio 非常容易的,同样我们这里将 Minio 安装到 Kubernetes 集群中,可以直接参考官方文档 使用 Kubernetes 部署 MinIO,在 Kubernetes 集群下面可以部署独立、分布式或共享几种模式,可以根据实际情况部署,我们这里只是单纯测试用最简单的独立模式即可。

直接使用如下所示的 Deployment 来管理 Minio 的服务:

复制代码
# minio-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
spec:
  selector:
    matchLabels:
      app: minio
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: minio
    spec:
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: minio-pvc
      containers:
        - name: minio
          volumeMounts:
            - name: data
              mountPath: "/data"
          image: minio/minio:latest
          args: ["server", "--console-address", ":9001", "/data"]
          env:
            - name: MINIO_ACCESS_KEY
              value: "minio"
            - name: MINIO_SECRET_KEY
              value: "minio123"
          ports:
            - containerPort: 9000
            - containerPort: 9001
          readinessProbe:
            httpGet:
              path: /minio/health/ready
              port: 9000
            initialDelaySeconds: 10
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /minio/health/live
              port: 9000
            initialDelaySeconds: 10
            periodSeconds: 10
View Code
复制代码

由于新版本的镜像区分了 Console 和 API 两个服务的端口,所以在启动的时候我们需要通过 --console-address 参数来指定 Console 服务的端口,默认的 API 服务在 9000 端口上。

然后通过一个名为 minio-pvc 的 PVC 对象将数据持久化:

复制代码
# minio-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: minio-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10G
  storageClassName: longhorn # 最好使用LocalPV
复制代码

最后我们可以通过 NodePort 类型的 Service 服务将 Minio 暴露给外部用户使用:

复制代码
# minio-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: minio
spec:
  ports:
    - name: console
      port: 9001
      targetPort: 9001
      nodePort: 30091
    - name: api
      port: 9000
      targetPort: 9000
  selector:
    app: minio
  type: NodePort
View Code 
复制代码

直接创建上面的资源对象即可:

 kubectl apply -f https://p8s.io/docs/thanos/manifests/minio.yaml
 

部署成功后,可以在浏览器中访问 http://<nodeip>:30091 访问到 MinIO Console 服务,通过上面定义的 MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY 即可登录:

MinIO

然后创建一个名为 thanos 的 bucket 桶:

Bucket

安装 Thanos Store

现在对象存储准备好了,我们就可以来部署 Store 组件了,该组件会从对象存储给 Querier 提供 metrics 数据。

根据上面创建的 Minio 创建一个如下所示的对象存储配置文件

复制代码
# thanos-storage-minio.yaml
type: s3
config:
  bucket: thanos
  endpoint: minio.default.svc.cluster.local:9000
  access_key: minio
  secret_key: minio123
  insecure: true
  signature_version2: false
复制代码

腾讯如下

复制代码
type: COS
config:
  bucket: "promethus-data"
  region: "ap-beijing"
  app_id: ""
  secret_key: ""
  secret_id: ""

# 创建
# kubectl create secret generic thanos-objectstorage --from-file=thanos.yaml=thanos-storage-tencent.yaml -n kube-mon
复制代码

使用上面的配置文件来创建一个 Secret 对象:

kubectl create secret generic thanos-objectstorage --from-file=thanos.yaml=thanos-storage-minio.yaml -n kube-mon

然后创建 Store 组件的资源清单文件,这里有一个需要注意的地方是需要添加一个 thanos-store-api: "true" 的标签,这样前面我们创建的 thanos-store-gateway 这个 Headless Service 就可以自动发现到这个服务,Querier 组件查询数据的时候除了可以通过 Sidecar 去获取数据也可以通过这个 Store 组件去对象存储里面获取数据了。将上面的 Secret 对象通过 Volume 形式挂载到容器中的 /etc/secret 目录下,通过 objstore.config-file 参数指定即可:

复制代码
# thanos-store.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: thanos-store-gateway
  namespace: kube-mon
  labels:
    app: thanos-store-gateway
spec:
  replicas: 2
  selector:
    matchLabels:
      app: thanos-store-gateway
  serviceName: thanos-store-gateway
  template:
    metadata:
      labels:
        app: thanos-store-gateway
        thanos-store-api: "true"
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - thanos-store-gateway
      containers:
        - name: thanos
          image: thanosio/thanos:v0.25.1
          args:
            - "store"
            - "--log.level=debug"
            - "--data-dir=/data"
            - "--objstore.config-file=/etc/secret/thanos.yaml"
            - "--index-cache-size=500MB"
            - "--chunk-pool-size=500MB"
          ports:
            - name: http
              containerPort: 10902
            - name: grpc
              containerPort: 10901
          livenessProbe:
            httpGet:
              port: 10902
              path: /-/healthy
          readinessProbe:
            httpGet:
              port: 10902
              path: /-/ready
          volumeMounts:
            - name: object-storage-config
              mountPath: /etc/secret
              readOnly: false
            - mountPath: /data
              name: data
      volumes:
        - name: object-storage-config
          secret:
            secretName: thanos-objectstorage
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: longhorn
        resources:
          requests:
            storage: 1Gi
View Code 
复制代码

Store Gateway 实际也可以做到一定程度的无状态,它会需要一点磁盘空间来对对象存储做索引以加速查询,但数据不那么重要,是可以删除的,删除后会自动去拉对象存储查数据重新建立索引,这里为了避免每次重启都重新建立索引,所以用 StatefulSet 部署 Store Gateway,挂载一个小容量的 PV。部署两个副本,可以实现 Store Gateway 的高可用。

然后直接创建上面的资源对象即可:

☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-store.yaml
☸ ➜ kubectl get pods -n kube-mon -l thanos-store-api=true
NAME READY STATUS RESTARTS AGE
prometheus-0 2/2 Running 0 20m
prometheus-1 2/2 Running 0 21m
thanos-store-gateway-0 1/1 Running 0 24m
thanos-store-gateway-1 1/1 Running 0 24m
 

部署成功后这个时候去 Thano 的 Querier 页面上查看 Store 信息就可以发现这里我们配置的 Store 组件了:

Thano Store

到这里证明我们的 Store 组件也配置成功了。但是还有一个明显的问题是这里我们只是配置去对象存储中查询数据的,那什么地方往对象存储中写入数据呢?当然还是在 Sidecar 组件里面了。所以同样我们需要把 objstore.config-file 参数和 Secret 对象也要配置到 Sidecar 组件中去:

复制代码
......
volumes:
- name: object-storage-config
  secret:
    secretName: thanos-objectstorage
......
args:
- sidecar
- --log.level=debug
- --tsdb.path=/prometheus
- --prometheus.url=http://localhost:9090
- --reloader.config-file=/etc/prometheus/prometheus.yaml.tmpl
- --reloader.config-envsubst-file=/etc/prometheus-shared/prometheus.yaml
- --reloader.rule-dir=/etc/prometheus/rules/
- --objstore.config-file=/etc/secret/thanos.yaml
......
volumeMounts:
- name: object-storage-config
  mountPath: /etc/secret
  readOnly: false
......
View Code
复制代码

 

5. Compactor 组件

现在历史监控数据已经上传到对象存储中去了,但是由于监控数据量非常庞大,所以一般情况下我们会去安装一个 Thanos 的 Compactor 组件,用来将对象存储中的数据进行压缩。Compactor 组件只与对象存储交互,是唯一拥有删除对象存储数据权限的组件,主要有两个作用:压缩 block(将多个 block 合并成一个)、降采样(可禁用,5m/1h 采样数据)。可设置数据保留时长,原始数据、5m/1h 降采样数据可分别设置保留时长。

 1. 下采样

Compactor 用于定时对远端对象存储中的历史数据块进行下采样,Compactor 会将小的存储块合并为大的存储块,提升在做大时间跨度查询时的速度。

下采样有三个主要的配置参数:

  • --retention.resolution-raw(单位:d,默认 0d)
  • --retention.resolution-5m(单位:d,默认 0d)
  • --retention.resolution-1h(单位:d,默认 0d)

当开启 raw 之后,原有的历史数据会以该项所配置的时间长度保留于远端对象存储中,超过该时间的数据会被清理。5m 开启后会为每个存储时长大于 40 小时的块中开辟新的存储区域,将历史数据以 5 分钟为精度进行下采样,以该项所配置的时间长度存储于远端对象存储中。1h 开启后会为每个存储时长大于 10 天的块中开辟新的存储区域,将历史数据以 1 小时为间隔进行下采样,以该项所配置的时间长度存储于远端对象存储中。

下采样的实现方式是以外部标签为分组,以数据块为单位,以采样精度为取值区间在原有的数据块中取指标值保存到新建的下采样数据块中。这三种采样的数据都是独立存储,相当于存了三份数据,因此并不能起到压缩存储空间的作用。默认情况下值为 0d 表示永久保留下采样数据。如果需要关闭下采样,也可以在启动时附加 --debug.disable-downsampling 参数。

2. 安装

由于 Compactor 的设计是非并发安全的,因此只能单例部署,一个 Bucket 也只允许运行一个 Compactor,每一个 Store Gateway 都需要配置一个 Bucket 桶,而一个 Bucket 只允许一个 Compactor,可以根据以下维度去划分 Bucket:

  • 结算方式
  • 所在区域
  • 所属业务
  • 基础设施层级
  • 单指标的横向拆分

Compactor 组件的部署和 Store 非常类似,指定对象存储的配置文件即可,如下所示的资源清单文件:

复制代码
# thanos-compactor.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: thanos-compactor
  namespace: kube-mon
  labels:
    app: thanos-compactor
spec:
  replicas: 1
  selector:
    matchLabels:
      app: thanos-compactor
  serviceName: thanos-compactor
  template:
    metadata:
      labels:
        app: thanos-compactor
    spec:
      containers:
        - name: thanos
          image: thanosio/thanos:v0.25.1
          args:
            - "compact"
            - "--log.level=debug"
            - "--data-dir=/data"
            - "--objstore.config-file=/etc/secret/thanos.yaml"
            - "--wait"
          ports:
            - name: http
              containerPort: 10902
          livenessProbe:
            httpGet:
              port: 10902
              path: /-/healthy
            initialDelaySeconds: 10
          readinessProbe:
            httpGet:
              port: 10902
              path: /-/ready
            initialDelaySeconds: 15
          volumeMounts:
            - name: object-storage-config
              mountPath: /etc/secret
              readOnly: false
      volumes:
        - name: object-storage-config
          secret:
            secretName: thanos-objectstorage
---
apiVersion: v1
kind: Service
metadata:
  name: thanos-compactor
  namespace: kube-mon
  labels:
    app: thanos-compactor
spec:
  ports:
    - port: 10902
      targetPort: http
      name: http
  selector:
    app: thanos-compactor
  type: NodePort
View Code
复制代码

最重要的还是提供对象存储的配置文件,然后直接创建上面的资源清单文件:

复制代码
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-compactor.yaml
☸ ➜ kubectl get pods -n kube-mon -l app=thanos-compactor
NAME                 READY   STATUS    RESTARTS   AGE
thanos-compactor-0   1/1     Running   0          68s
☸ ➜ kubectl get svc -n kube-mon -l app=thanos-compactor
NAME                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)           AGE
thanos-compactor       NodePort    10.97.151.170    <none>        10902:32051/TCP   71s
View Code
复制代码

Compactor 同样也提供了一个 Web 界面,这里我们可以通过 NodePort 进行访问:

thanos-compactor

在页面中还可以对存储块进行管理,在右下角,我们可以标记删除一个存储块,也可以选择不对其进行降采样。

thanos-compactor

 

配置完成后重新更新 Sidecar 组件即可,配置生效过后正常的话就会有数据传入到 MinIO 里面去了(本地有超过两小时的 TSDB 块数据),我们可以去 MinIO 的页面上查看

6. Query Frontend

前面我们了解到 querier 这个组件可以提供一个统一的查询入口,所以当查询的数据规模较大的时候,对 querier 组件也会有很大的压力,为此 Thanos 也提供了一个 Query Frontend 的组件来提升性能,当然该组件是可选的。

Thanos Query Frontend 是 Thanos Query 的前端,它的目标是将大型查询拆分为多个较小的查询,并缓存查询结果来提升性能。

1. 概述

Thanos Query Frontend 组件通过 thanos query-frontend 命令实现了一个放在 querier 前面的服务,以改进读取路径。它基于 Cortex Query Frontend 组件,所以你可以找到一些 Cortex 常见的特性,如查询拆分和结果缓存。可以使用下列命令来启动 Thanos Query Frontend:

thanos query-frontend \
    --http-address     "0.0.0.0:9090" \
    --query-frontend.downstream-url="<thanos-querier>:<querier-http-port>" 

目前只有范围查询(/api/v1/query_range 接口调用)实际上是通过 Query Frontend 处理的。所有其他 API 调用只是直接转到下游查询器,这意味着只有范围查询被拆分和缓存,但我们也计划支持即时查询。

2.特性

查询拆分

query frontend 会将多天的的查询拆分为多个单天的查询,游下游的 querier 去并行处理这些已拆分的查询。返回的查询结果由 query frontend 进行汇聚。这样可以防止大时间跨度的查询导致 queier 发生 OOM,并且能够更快的执行查询以及更好的查询负载均衡。查询前端根据配置的 --query-range.split-interval 标志将长查询拆分为多个短查询,--query-range.split-interval 的默认值为 24 小时,启用缓存时,它应该大于 0。

重试机制

query frontend 支持在 HTTP 请求失败时重试查询的重试机制,有一个 --query-range.max-retries-per-request 标志来限制最大重试次数。

查询缓存

query frontend 支持将查询结果进行缓存用以加速后续的查询。当缓存的结果不够完整时,query frontend 会计算出所需要的子查询并分配给下游的 querier 并行执行,子查询的步长会对齐以提升查询结果的可缓存性。当前支持的缓存后端有:memcached,redis 和内存,下面是这些缓存后端的一些配置。

内存缓存

复制代码
type: IN-MEMORY
config:
  max_size: ""
  max_size_items: 0
  validity: 0s
View Code
复制代码

max_size 表示在内存中缓存的最大尺寸,单位可以是 KB、MB、GB。如果 max_size 和 max_size_items 都没有设置,就不会创建缓存。如果只设置 max_size 或 max_size_items 中的任意一个,则对其他字段没有限制。

memcached

复制代码
type: MEMCACHED
config:
  addresses: [your-memcached-addresses]
  timeout: 500ms
  max_idle_connections: 100
  max_item_size: 1MiB
  max_async_concurrency: 10
  max_async_buffer_size: 10000
  max_get_multi_concurrency: 100
  max_get_multi_batch_size: 0
  dns_provider_update_interval: 10s
  expiration: 24h
 
复制代码

expiration 表示指定 memcached 缓存有效时间,如果设置为 0,则使用默认的 24 小时过期时间,上面的配置是默认的 memcached 配置。

Redis

默认的 Redis 配置如下:

复制代码
type: REDIS
config:
  addr: ""
  username: ""
  password: ""
  db: 0
  dial_timeout: 5s
  read_timeout: 3s
  write_timeout: 3s
  pool_size: 100
  min_idle_conns: 10
  idle_timeout: 5m0s
  max_conn_age: 0s
  max_get_multi_concurrency: 100
  get_multi_batch_size: 100
  max_set_multi_concurrency: 100
  set_multi_batch_size: 100
  expiration: 24h0m0s
View Code
复制代码

expiration 表示指定 Redis 缓存有效时间,如果设置为 0,则使用默认的 24 小时过期时间。

慢查询日志

query frontend 还支持通过配置 --query-frontend.log-queries-longer-than 标志来记录运行时间超过某个持续时间的查询。

安装

安装 query frontend 最重要的就是通过 -query-frontend.downstream-url 指定下游的 querier,如下所示:

复制代码
# thanos-query-frontend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: thanos-query-frontend
  namespace: kube-mon
  labels:
    app: thanos-query-frontend
spec:
  selector:
    matchLabels:
      app: thanos-query-frontend
  template:
    metadata:
      labels:
        app: thanos-query-frontend
    spec:
      containers:
        - name: thanos
          image: thanosio/thanos:v0.25.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 9090
              name: http
          args:
            - query-frontend
            - --log.level=info
            - --log.format=logfmt
            - --query-frontend.compress-responses
            - --http-address=0.0.0.0:9090
            - --query-frontend.downstream-url=http://thanos-querier.kube-mon.svc.cluster.local:9090
            - --query-range.split-interval=12h
            - --query-range.max-retries-per-request=10
            - --query-frontend.log-queries-longer-than=10s
            - --labels.split-interval=12h
            - --labels.max-retries-per-request=10
            - |-
              --query-range.response-cache-config="config":
                max_size: "200MB"
                max_size_items: 0
                validity: 0s
              type: IN-MEMORY
            - |-
              --labels.response-cache-config="config":
                max_size: "200MB"
                max_size_items: 0
                validity: 0s
              type: IN-MEMORY
          env:
            - name: HOST_IP_ADDRESS
              valueFrom:
                fieldRef:
                  fieldPath: status.hostIP
          livenessProbe:
            failureThreshold: 4
            httpGet:
              path: /-/healthy
              port: 9090
              scheme: HTTP
            periodSeconds: 30
          readinessProbe:
            failureThreshold: 20
            httpGet:
              path: /-/ready
              port: 9090
              scheme: HTTP
            periodSeconds: 5
          resources:
            requests:
              memory: 512Mi
              cpu: 500m
            limits:
              memory: 512Mi
              cpu: 500m
---
apiVersion: v1
kind: Service
metadata:
name: thanos-query-frontend
namespace: kube-mon
labels:
app: thanos-query-frontend
spec:
ports:
-name: http
port:9090
targetPort:9090
selector:
app: thanos-query-frontend
type: NodePort
View Code
复制代码

这里我们开启了缓存,使用内存缓存,直接通过 -query-range.response-cache-config 参数来配置缓存配置,也可以通过 -query-range.response-cache-config-file 指定缓存配置文件,两种方式均可,为了验证结果,这里创建了一个 NodePort 类型的 Service,直接创建上面的资源对象即可:

复制代码
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-query-frontend.yaml
☸ ➜ kubectl get pods -n kube-mon -l app=thanos-query-frontend
NAME                                     READY   STATUS    RESTARTS   AGE
thanos-query-frontend-78954bc857-nkxkk   1/1     Running   0          151m
☸ ➜ kubectl get svc -n kube-mon -l app=thanos-query-frontend
NAME                    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
thanos-query-frontend   NodePort   10.97.182.150   <none>        9090:30514/TCP   152m
View Code
复制代码

然后可以通过任意节点的 30514 端口访问到查询前端:

查询前端

该前端页面和 querier 组件是一致的,但是在 querier 前面缓存包装了一层。

 7. Ruler 组件

ruler 组件是用于评估 Prometheus 的记录规则和报警规则的组件,其本身不会抓取 metrics 接口数据,而是通过 Query API 从 query 组件定期地获取指标数据,如果配置了多个 query 地址,则会采用轮询方式获取。

其中记录规则评估生成的数据会保存在本地,并且定期地扫描本地生成的 TSDB 数据块上传到对象存储桶中做为历史数据长期保存。同时也实现了 Store API 可用于查询本地保存的数据。与 Prometheus 节点类似,每个 ruler 节点都使用独立的存储,可以同时运行多个副本,而且需要为每个副本实例分配不同的标签以作区分,因为 store 组件在查询对象存储中的历史数据时是以该标签进行分组查询的。

安装

由于 ruler 组件也实现了 Store API,所以我们也可以直接将该组件对接到 store 组件中去,只需要给创建的 Pod 带上 thanos-store-api: "true" 这个标签即可(Service 会进行自动关联)被 query 组件服务发现。

整体上我们可以把 ruler 节点理解为一个简单的 Prometheus 节点,只是不需要 thanos sidecar,不抓取指标数据,只负责执行 PromQL 查询,由于本身会保留独立的存储,所以同样这里我们需要做数据的持久化。

然后可以通过部署两个副本来实现高可用,这里我们添加了一个 --label=rule_replica 标签来给数据添加一个 rule_replica 的标签, 同时指定 --alert.label-drop 为 rule_replica,这样在触发告警发送通知给 AlertManager 时可以去掉这个 label,以便让 AlertManager 自动去重,可以避免重复告警。

然后通过 --query 参数指定 query 组件地址,我们这里还是使用 DNS SRV 来做服务发现,这样就可以从查询组件中获取指标数据了。

ruler 同样也需要对象存储的配置,用于上传计算出的数据到对象存储,所以要挂载对象存储的配置文件。--rule-file 参数可以用来指定挂载的 rule 配置,ruler 组件会根据配置来生成数据和触发报警。

完整的资源清单文件如下所示:

复制代码
# thanos-ruler.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: thanos-ruler
  namespace: kube-mon
  labels:
    app: thanos-ruler
spec:
  replicas: 2
  selector:
    matchLabels:
      app: thanos-ruler
  serviceName: thanos-rule
  podManagementPolicy: Parallel
  template:
    metadata:
      labels:
        app: thanos-ruler
        thanos-store-api: "true"
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - thanos-ruler
      containers:
      - name: thanos-ruler
        image: thanosio/thanos:v0.25.1
        args:
        - rule
        - --grpc-address=0.0.0.0:10901
        - --http-address=0.0.0.0:10902
        - --rule-file=/etc/thanos/rules/*rules.yaml
        - --objstore.config-file=/etc/secret/thanos.yaml
        - --data-dir=/var/thanos/rule
        - --label=rule_replica="$(NAME)"
        - --alert.label-drop=rule_replica
        - --query=dnssrv+_http._tcp.thanos-querier.kube-mon.svc.cluster.local
        ports:
        - containerPort: 10901
          name: grpc
        - containerPort: 10902
          name: http
        env:
        - name: NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        livenessProbe:
          httpGet:
            path: /-/healthy
            port: 10902
            scheme: HTTP
        readinessProbe:
          httpGet:
            path: /-/ready
            port: 10902
            scheme: HTTP
        volumeMounts:
        - mountPath: /var/thanos/rule
          name: data
          readOnly: false
        - name: object-storage-config
          mountPath: /etc/secret
          readOnly: false
        - name: thanos-rules
          mountPath: /etc/thanos/rules
      volumes:
      - name: object-storage-config
        secret:
          secretName: thanos-objectstorage
      - name: thanos-rules
        configMap:
          name: thanos-rules
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
      storageClassName: longhorn
      resources:
        requests:
          storage: 1Gi
View Code
复制代码

要注意上面挂载的对象存储配置的 Secret,另外还需要通过一个 ConfigMap 来配置 rules 规则:

复制代码
# thanos-rules-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: thanos-rules
  namespace: kube-mon
data:
  record.rules.yaml: |-
    groups:
    - name: k8s.rules
      rules:
      - expr: |
          sum(rate(container_cpu_usage_seconds_total{job="cadvisor", image!="", container!=""}[5m])) by (namespace)
        record: namespace:container_cpu_usage_seconds_total:sum_rate
      - expr: |
          sum(container_memory_usage_bytes{job="cadvisor", image!="", container!=""}) by (namespace)
        record: namespace:container_memory_usage_bytes:sum
      - expr: |
          sum by (namespace, pod, container) (
            rate(container_cpu_usage_seconds_total{job="cadvisor", image!="", container!=""}[5m])
          )
        record: namespace_pod_container:container_cpu_usage_seconds_total:sum_rate
View Code
复制代码

这里我们简单配置了几个记录规则,配置方式和之前的规则一样的。然后直接创建上面的资源对象即可:

复制代码
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-rules-config-0.yaml 
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-ruler-0.yaml 
☸ ➜ kubectl get pods -n kube-mon -l app=thanos-ruler
NAME             READY   STATUS    RESTARTS   AGE
thanos-ruler-0   1/1     Running   0          16m
thanos-ruler-1   1/1     Running   0          16m
View Code
复制代码

部署完成后我们可以去查看 query 组件页面的 store 信息是否包含上面的 ruler 实例:

ruler节点

同样在 rules 页面可以看到我们定义的记录规则信息:

rules信息

现在我们可以尝试去查询下上面的记录规则,比如查询 namespace:container_cpu_usage_seconds_total:sum_rate

记录规则

可以看到可以正常获取到这条记录规则的数据。

 

对接告警

如果要进行报警,首先我们需要通过启动参数 --alertmanagers.url 来指定 Alertmanager 的地址,如果需要更高级的配置,可以通过启动参数 --alertmanagers.config 或者 --alertmanagers.config-file 来指定对接 Alertmanager 的配置,格式如下所示:

复制代码
alertmanagers:
- http_config:
    basic_auth:
      username: ""
      password: ""
      password_file: ""
    bearer_token: ""
    bearer_token_file: ""
    proxy_url: ""
    tls_config:
      ca_file: ""
      cert_file: ""
      key_file: ""
      server_name: ""
      insecure_skip_verify: false
  static_configs: []
  file_sd_configs:
  - files: []
    refresh_interval: 0s
  scheme: http
  path_prefix: ""
  timeout: 10s
  api_version: v1
View Code
复制代码

比如我们这里对接前面章节中的 Alertmanager,则直接这上面的资源对象容器启动参数中增加 - --alertmanagers.url=http://alertmanager:9093 即可。然后在上面的 thanos-rules 的 ConfigMap 中新增一个 alert.rules.yaml 的配置,用来配置报警规则,如下所示:

复制代码
# thanos-rules-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: thanos-rules
  namespace: kube-mon
data:
  record.rules.yaml: |-
  # ......
  alert.rules.yaml: |-
    groups:
    - name: test-node-mem
      rules:  
      - alert: NodeMemoryUsage  
        expr: (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / node_memory_MemTotal_bytes * 100 > 30
        for: 1m
        labels:
          team: node
          severity: critical
        annotations:
          summary: "{{$labels.instance}}: High Memory usage detected"
          description: "{{$labels.instance}}: Memory usage is above 30% (current value is: {{ $value }})"
View Code
复制代码

直接更新上面的两个资源对象即可:

☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-rules-config.yaml 
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-ruler.yaml

更新完成后这 query 的 rules 页面也可以看到上面新增的报警规则了,因为我们部署的是两个副本,所以能看到两条一样的规则:

报警规则

由于我们这 ruler 组件启动参数中配置了参数 - --alert.label-drop=rule_replica,所以 Alertmanager 中不会收到重复报警,不过需要注意的是这里的 rule_replica 不能加引号,加上引号会去重失效,我们可以前往 Alertmanager 查看触发的报警信息:

触发报警

由于 ruler 组件获取评估数据的路径是 ruler --> query --> sidecar --> prometheus,需要经整个查询链条,这也提升了发生故障的风险,而且评估原本就可以在 Prometheus 中进行,所以在非必要的情况下更加推荐使用原本的 Prometheus 方式来做报警和记录规则的评估。

 8. Receiver

前面我们提到 Thanos 有 Sidecar 和 Receiver 两种不同的架构模式,前面的章节我们已经学习了 Sidecar 模式的是呀,接下来我们再来了解下 Receiver 模式是如何工作的。

我们知道 Sidecar 是在每一个 Prometheus 的实例旁边添加一个 sidecar 组件来上传数据,但是数据上传并不是实时的,而是每 2h 上传一个数据块,所以远程存储的数据并不是实时的,Prometheus 需要各自持久化部分数据,这也是现在使用的 Sidecar 模式的弊端,但这并非是 Thanos 团队引入 Receiver 的决定性因素。

Receiver is only recommended for uses for whom pushing is the only viable solution, for example, analytics use cases or cases where the data ingestion must be client initiated, such as software as a service type environments.

按照文档中的说法,Receiver 只推荐用于多租户以及 Prometheus 配置受限的场景下,比如:

  • 租户使用某些 SaaS 服务对集群进行监控,如 Openshift Operator 部署的 Prometheus
  • 由于安全或权限问题,监控团队无法在被监控集群中配置 Sidecar
  • 被监控集群部署的是非容器化部署的 Prometheus

这是因为 Receiver 会暂存多个 Prometheus 实例的 TSDB,当数据量较大时可能会发生 OOM,另外根据 官方文档,开启远程写入还将增加 Prometheus 约 25% 的内存使用。我们并非一定要在 Receiver 和 Sidecar 之间做出抉择,比如 Lastpass 就采用 Sidecar 与 Receiver 混合部署的方式,可以参考文档 https://krisztianfekete.org/adopting-thanos-at-lastpass/

Thanos Receiver 组件可以接收来自任何 Prometheus 实例的 remote write 远程写入请求,并将数据存储在其本地 TSDB 中,同样我们也可以选择将这些 TSDB 块定期上传到对象存储中,此外 Receiver 同样也暴露了 Store API 接口,这样 Thanos Querier 组件也是可以实时查询接收到的指标的,不需要再去通过 Sidecar 获取最新的数据。

多租户

Thanos Receiver 组件也支持多租户,通过 remote write 请求的 HTTP Header(通过参数--receive.tenant-header="THANOS-TENANT"配置)获取租户的 ID,为了防止数据库级别的数据泄露,每个租户都有一个单独的 TSDB 实例,数据成功提交到租户的 TSDB,就代表 remote write 请求成功了。指标会被添加标签 tenant_id(通过参数--receive.tenant-label-name="tenant_id"配置)来标识指标的所属租户。

我们可以在 Prometheus 中通过如下所示配置在 remote_write 请求中附加 Header:

remote_write:
  - url: http://tenants:19291/api/v1/receive
    headers:
      THANOS-TENANT: a

如果希望实现多个 Receiver 的负载均衡和数据复制,则需要借助配置文件(通过--receive.hashrings-file=<path>参数指定)将多个 Receiver 组成哈希环,配置文件如下所示:

 
复制代码
[
  {
    "hashring": "tenant-a",
    "endpoints": [
      "tenant-a-1.thanos.local:19291/api/v1/receive",
      "tenant-a-2.thanos.local:19291/api/v1/receive"
    ],
    "tenants": ["tenant-a"]
  },
  {
    "hashring": "tenants-b-c",
    "endpoints": [
      "tenant-b-c-1.thanos.local:19291/api/v1/receive",
      "tenant-b-c-2.thanos.local:19291/api/v1/receive"
    ],
    "tenants": ["tenant-b", "tenant-c"]
  },
  {
    "hashring": "soft-tenants",
    "endpoints": ["http://soft-tenants-1.thanos.local:19291/api/v1/receive"]
  }
]
复制代码

上面这个配置文件表示租户 ID 为 a 的写请求,将由 tenant-a-1 与 tenant-a-2 这两个 Receiver 进行处理,租户 ID 为 b 或 c 的写请求,将由 tenant-b-c-1tenant-b-c-2 这两个 Receiver 处理,没有配置 THANOS-TENANT header 或其值不为 a、b、c 之一的写请求,则由 soft-tenants-1 这个 Receiver 处理。

这里涉及到两个概念:硬租户与软租户。

  • Soft tenancy:如果哈希环配置文件中未显式指定租户,则任何租户均被视为有效匹配。所有未设置 HTTP Header 或租户 ID 与其他哈希环不匹配的租户写请求,都将被分配给该哈希环。相应的 Receiver 将会为指标添加默认租户标签和值(可通过参数–receive.default-tenant-id指定)。
  • Hard tenancy:所有的 remote write 请求必须通过 HTTP Header 显式地指定租户 ID,请求将进入匹配到的哈希环进行处理。

注意:remote write 请求可以发送给一组 Receiver 中任意一个,但是最终请求会交由匹配到的 Receiver 进行处理。所以可以在 Receiver 前面添加负载均衡器来随机分发请求实现 Receiver 的负载均衡。

安装

用于创建 Thanos Receiver 的资源清单如下所示:

复制代码
# thanos-receiver.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: thanos-receiver
  name: thanos-receiver
  namespace: kube-mon
spec:
  selector:
    matchLabels:
      app: thanos-receiver
  serviceName: thanos-receiver
  replicas: 3 #节点数量
  template:
    metadata:
      labels:
        app: thanos-receiver
        thanos-store-api: "true"
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - thanos-receiver
      containers:
        - name: thanos-receiver
          image: thanosio/thanos:v0.25.1
          args:
            - receive
            - --grpc-address=0.0.0.0:10901
            - --http-address=0.0.0.0:10902
            - --remote-write.address=0.0.0.0:19291 # 提供给 prometheus 的 remote_write 端口
            - --receive.replication-factor=3 # 副本数,详细解释参考https://thanos.io/tip/proposals-done/201812-thanos-remote-receive.md/#:~:text=--receive.replication-factor=3
            - --objstore.config-file=/etc/secret/thanos.yaml # 对象存储配置文件
            - --tsdb.path=/var/thanos/receiver # 本地tsdb路径
            - --tsdb.retention=1d # 热数据的保存时间
            - --label=receive_replica="$(NAME)" # 用于过滤重复数据的标签
            - --receive.local-endpoint=$(NAME).thanos-receiver:10901 # 节点endpoint,hashring 中记录的节点host需要与此处保持一致
            - --receive.hashrings-file=/var/lib/thanos-receive/hashring.json # hashring文件,用于记录集群节点
          ports:
            - containerPort: 10901
              name: grpc
            - containerPort: 10902
              name: http
            - containerPort: 19291
              name: remote-write
          env:
            - name: NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          livenessProbe:
            failureThreshold: 8
            httpGet:
              path: /-/healthy
              port: 10902
              scheme: HTTP
            periodSeconds: 30
          readinessProbe:
            failureThreshold: 20
            httpGet:
              path: /-/ready
              port: 10902
              scheme: HTTP
            periodSeconds: 5
          volumeMounts:
            - mountPath: /var/thanos/receiver
              name: data
              readOnly: false
            - name: hashring-config
              mountPath: /var/lib/thanos-receive
            - name: object-storage-config
              mountPath: /etc/secret
              readOnly: false
      volumes:
        - name: object-storage-config
          secret:
            secretName: thanos-objectstorage
        - name: hashring-config
          configMap:
            name: hashring-config
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: longhorn
        resources:
          requests:
            storage: 2Gi
---
apiVersion: v1
kind: Service
metadata:
  name: thanos-receiver
  namespace: kube-mon
spec:
  clusterIP: None
  ports:
    - name: grpc
      port: 10901
      targetPort: 10901
    - name: http
      port: 10902
      targetPort: 10902
    - name: remote-write
      port: 19291
      targetPort: 19291
  selector:
    app: thanos-receiver
View Code
复制代码

由于 Thanos Receiver 同样会将数据同步到对象存储,所以同样将对象存储的配置挂载到容器中,通过 --objstore.config-file 参数指定配置文件路径,为了能够直接对接到 Store 组件,同样这里为 Pod 添加一个 thanos-store-api: "true" 的标签即可(Service 会进行自动关联)被 query 组件服务发现。

Thanos Receiver 支持将接收到的时间序列复制到同一哈希环中的其他 Receiver,复制因子通过在 Receiver 上设置一个标志来控制,并指示应该存储在哈希环中的任何时间序列的最大副本数。如果 Thanos Receiver 接收到的写入请求中的任何时间序列未成功写入至少 (REPLICATION_FACTOR + 1)/2 个节点,则 Receiver 会以错误响应,例如,要尝试存储每个时间序列的 3 个副本并确保每个时间序列成功写入目标哈希环中的至少 2 个 Thanos Receiver,所以需要配置 --receive.replication-factor=3 参数。

此外 Thanos Receiver 本地同样有数据,所以也需要做持久化。

通过参数 --label=receive_replica="$(NAME)" 指定用于重复过滤的标签为 receive_replica,这样我们同样需要在 query 组件参数中添加该标签 --query.replica-label=receive_replica 用于查询的时候去重:

args:
  - query
  - --log.level=debug
  - --query.replica-label=replica
  - --query.replica-label=receive_replica # 添加该参数
  # Discover local store APIs using DNS SRV.
  - --store=dnssrv+thanos-store-gateway:10901

最后一个比较重要的参数就是 --receive.hashrings-file=/var/lib/thanos-receive/hashring.json,用来指定 hashring 文件,该文件用于记录集群节点,这里我们通过一个 ConfigMap 挂载到容器中去,资源清单如下所示:

复制代码
# thanos-receiver-hashring.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: hashring-config
  namespace: kube-mon
data:
  hashring.json: |-
    [
      {
        "endpoints": [ 
            "thanos-receiver-0.thanos-receiver:10901",
            "thanos-receiver-1.thanos-receiver:10901",
            "thanos-receiver-2.thanos-receiver:10901"
        ]
      }
    ]
复制代码

其中的 endpoints 用来记录所有 Receiver 节点的 host 地址的。然后直接应用上面的资源清单即可:

☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-receiver-hashring.yaml
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-receiver.yaml
☸ ➜ kubectl get pods -n kube-mon -l app=thanos-receiver
NAME                READY   STATUS    RESTARTS   AGE
thanos-receiver-0   1/1     Running   0          100s
thanos-receiver-1   1/1     Running   0          89s
thanos-receiver-2   1/1     Running   0          72s

此外要记得为 Querier 组件添加 - --query.replica-label=receive_replica 参数,由于我们添加了 thanos-store-api: "true" 参数,所以会自动被 Store 发现,我们可以在查询前端页面验证 Store 组件,正常就会包含上面我们新增的 3 各 Receiver:

store

配置

由于现在我们已经使用了 Receiver 模式了,只需要 Prometheus 将数据以 remote write 的形式传入 Receiver 即可,所以可以将 Sidecar 组件停掉了,现在我们的 Prometheus 变成了近乎无状态的了,只需要 Prometheus 应用本身,然后加上 remote write 地址即可,在 Prometheus 配置文件中新增远程写的接口地址:

 
复制代码
# prometheus-config1.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: kube-mon
data:
  prometheus.yaml.tmpl: | # 注意这里的名称是 prometheus.yaml.tmpl
    global:
      scrape_interval: 15s
      scrape_timeout: 15s
      external_labels:
        cluster: ydzs-test
        replica: $(POD_NAME)  # 每个 Prometheus 有一个唯一的标签

    rule_files:  # 报警规则文件配置
    - /etc/prometheus/rules/*rules.yaml

    # 指定 remote write 地址
    remote_write:
    - url: http://thanos-receiver:19291/api/v1/receive

    # ...... 省略其他配置
复制代码

在配置文件中通过 remote_write.url 来指定远程写入接口地址。正常现在也不需要 Sidecar 容器了,这里我们为了用一个 StatefulSet 来运行两个 Prometheus 副本,可以借助 Sidecar 来帮我们渲染 prometheus.yaml.tmpl 模板文件(因为 Prometheus 本身是不支持环境变量替换的),这里的 Sidecar 仅作渲染用,同样需要去掉 thanos-store-api: "true" 标签,因为不需要直接对接 Store 了,修改后的资源清单文件如下所示:

复制代码
# thanos-sidecar1.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: prometheus
  namespace: kube-mon
  labels:
    app: prometheus
spec:
  serviceName: prometheus
  replicas: 2
  selector:
    matchLabels:
      app: prometheus
      thanos-store-api: "true"
  template:
    metadata:
      labels:
        app: prometheus
        thanos-store-api: "true"
    spec:
      serviceAccountName: prometheus
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - prometheus
      volumes:
        - name: prometheus-config
          configMap:
            name: prometheus-config
        - name: prometheus-rules
          configMap:
            name: prometheus-rules
        - name: prometheus-config-shared
          emptyDir: {}
      initContainers:
        - name: fix-permissions
          image: busybox:stable
          command: [chown, -R, "nobody:nobody", /prometheus]
          volumeMounts:
            - name: data
              mountPath: /prometheus
      containers:
        - name: prometheus
          image: prom/prometheus:v2.34.0
          imagePullPolicy: IfNotPresent
          args:
            - "--config.file=/etc/prometheus-shared/prometheus.yaml"
            - "--storage.tsdb.path=/prometheus"
            - "--storage.tsdb.retention.time=6h"
            - "--storage.tsdb.no-lockfile"
            - "--storage.tsdb.min-block-duration=2h" # Thanos处理数据压缩
            - "--storage.tsdb.max-block-duration=2h"
            - "--web.enable-admin-api" # 通过一些命令去管理数据
            - "--web.enable-lifecycle" # 支持热更新  localhost:9090/-/reload 加载
          ports:
            - name: http
              containerPort: 9090
          volumeMounts:
            - name: prometheus-config-shared
              mountPath: /etc/prometheus-shared/
            - name: prometheus-rules
              mountPath: /etc/prometheus/rules
            - name: data
              mountPath: /prometheus
        - name: thanos
          image: thanosio/thanos:v0.25.1
          imagePullPolicy: IfNotPresent
          args:
            - sidecar
            - --log.level=debug
            - --reloader.config-file=/etc/prometheus/prometheus.yaml.tmpl
            - --reloader.config-envsubst-file=/etc/prometheus-shared/prometheus.yaml
            - --reloader.rule-dir=/etc/prometheus/rules/
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          volumeMounts:
            - name: prometheus-config-shared
              mountPath: /etc/prometheus-shared/
            - name: prometheus-config
              mountPath: /etc/prometheus
            - name: prometheus-rules
              mountPath: /etc/prometheus/rules
  volumeClaimTemplates: # 由于prometheus每2h生成一个TSDB数据块,所以还是需要保存本地的数据
    - metadata:
        name: data
        labels:
          app: prometheus
      spec:
        storageClassName: longhorn # 不要用nfs存储
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 2Gi
View Code
复制代码

重新更新上面两个资源清单文件:

☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/prometheus-config1.yaml
☸ ➜ kubectl apply -f https://p8s.io/docs/thanos/manifests/thanos-sidecar1.yaml

更新后 Prometheus 就会将数据远程写入到 Thanos Receiver 组件中去,然后 Receiver 也会定时将数据上传到对象存储中,而 Receiver 实现了 Store API,所以也可以直接通过 Query 组件查询获取到数据,这样最新的数据就会从 Receiver 组件中获取,而历史的数据就可以去 Store 对象存储中获取了。现在我们这 Query 中去查询数据就可以获取到最新的指标数据,由于配置了去重,所以查询出来的数据也会自动去重:

数据

 

 

 

posted @   wanghhhh  阅读(136)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示