基於 Prometheus 的監控系統實踐

監控作爲底層基礎設施的一環,是保障生產環境服務穩定性不可或缺的一部分,線上問題從發現到定位再到解決,通過監控和告警手段可以有效地覆蓋了「發現」和「定位」,甚至可以通過故障自愈等手段實現解決,服務開發和運維人員能及時有效地發現服務運行的異常,從而更有效率地排查和解決問題。

一個典型的監控(如白盒監控),通常會關注於目標服務的內部狀態,例如:

白盒監控很好地描述了系統的內部狀態,但缺少從外部角度看到的現象,比如:白盒監控只能看到已經接收的請求,並不能看到由於 DNS 故障導致沒有發送成功的請求,而黑盒監控此時便可以作爲補充手段,由探針(probe)程序來探測目標服務是否成功返回,更好地反饋系統的當前狀態。

某日需要爲服務搭建一個監控系統來採集應用埋點上報的指標,經過一番對比,最終選擇了 Prometheus 來作爲我們的業務監控,因爲它具有以下優點:

Prometheus 的架構圖如下:

在上面流程中,Prometheus 通過配置文件中指定的服務發現方式來確定要拉取監控指標的目標(Target),接着從要拉取的目標(應用容器和 Pushgateway)發起 HTTP 請求到特定的端點(Metric Path),將指標持久化至本身的 TSDB 中,TSDB 最終會把內存中的時間序列壓縮落到硬盤,除此之外,Prometheus 會定期通過 PromQL 計算設置好的告警規則,決定是否生成告警到 Alertmanager,後者接收到告警後會負責把通知發送到郵件或企業內部羣聊中。

Prometheus 的指標名稱只能由 ASCII 字符、數字、下劃線以及冒號組成,而且有一套命名規範:

Prometheus 提供了以下基本的指標類型:

Prometheus 是基於時間序列存儲的,首先了解一下什麼是時間序列,時間序列的格式類似於(timestamp,value)這種格式,即一個時間點擁有一個對應值,例如生活中很常見的天氣預報,如:[(14:00, 27℃),(15:00,28℃),(16:00,26℃)],就是一個單維的時間序列,這種按照時間戳和值存放的序列也被稱之爲向量(vector)。

再來舉另一個例子,如上圖所示,假如有一個指標http_requests,它的作用是統計每個時間段對應的總請求量是多少,這時候它即爲上面提到的是一個單維矩陣,而當我們給這個指標加上一個維度:主機名,這時候這個指標的作用就變成了統計每個時間段各個主機名對應的請求量是多少,這時候這個矩陣區域就變成擁有多列向量(每一列對應一個主機名)的時間序列,當給這個時間序列再添加多個標籤(key=value)時,這個矩陣就相應會變成一個多維矩陣。

每一組唯一的標籤集合對應着一個唯一的向量(vector),也可叫做一個時間序列(Time Serie),當在某一個時間點來看它時,它是一個瞬時向量(Instant Vector),瞬時向量的時序只有一個時間點以及它對於的一個值,比如:今天 12:05:30 時服務器的 CPU 負載;而在一個時間段來看它時,它是一個範圍向量(Range Vector),範圍向量對於着一組時序數據,比如:今天 11:00 到 12:00 時服務器的 CPU 負載。

類似的,可以通過指標名和標籤集來查詢符合條件的時間序列:

http_requests{host="host1",service="web",code="200",env="test"}

查詢結果會是一個瞬時向量:

http_requests{host="host1",service="web",code="200",env="test"} 10
http_requests{host="host2",service="web",code="200",env="test"} 0
http_requests{host="host3",service="web",code="200",env="test"} 12

而如果給這個條件加上一個時間參數,查詢一段時間內的時間序列:

http_requests{host="host1",service="web",code="200",env="test"}[:5m]

結果將會是一個範圍向量:

http_requests{host="host1",service="web",code="200",env="test"} 0 4 6 8 10
http_requests{host="host2",service="web",code="200",env="test"} 0 0 0 0 0
http_requests{host="host3",service="web",code="200",env="test"} 0 2 5 9 12

擁有了範圍向量,我們是否可以針對這些時間序列進行一些聚合運算呢?沒錯,PromQL 就是這麼幹的,比如我們要算最近 5 分鐘的請求增長速率,就可以拿上面的範圍向量加上聚合函數來做運算:

rate(http_requests{host="host1",service="web",code="200",env="test"}[:5m])

比如要求最近 5 分鐘請求的增長量,可以用以下的 PromQL:

increase(http_requests{host="host1",service="web",code="200",env="test"}[:5m])

要計算過去 10 分鐘內第 90 個百分位數:

histogram_quantile(0.9, rate(employee_age_bucket_bucket[10m]))

在 Prometheus 中,一個指標(即擁有唯一的標籤集的 metric)和一個(timestamp,value)組成了一個樣本(sample),Prometheus 將採集的樣本放到內存中,默認每隔 2 小時將數據壓縮成一個 block,持久化到硬盤中,樣本的數量越多,Prometheus 佔用的內存就越高,因此在實踐中,一般不建議用區分度(cardinality)太高的標籤,比如:用戶 IP,ID,URL 地址等等,否則結果會造成時間序列數以指數級別增長(label 數量相乘)。除了控制樣本數量和大小合理之外,還可以通過降低storage.tsdb.min-block-duration來加快數據落盤時間和增加scrape interval的值提高拉取間隔來控制 Prometheus 的佔用內存。

通過聲明配置文件中的scrape_configs來指定 Prometheus 在運行時需要拉取指標的目標,目標實例需要實現一個可以被 Prometheus 進行輪詢的端點,而要實現一個這樣的接口,可以用來給 Prometheus 提供監控樣本數據的獨立程序一般被稱作爲 Exporter,比如用來拉取操作系統指標的 Node Exporter,它會從操作系統上收集硬件指標,供 Prometheus 來拉取。

在開發環境,往往只需要部署一個 Prometheus 實例便可以滿足數十萬指標的收集。但在生產環境中,應用和服務實例數量衆多,只部署一個 Prometheus 實例通常是不夠的,比較好的做法是部署多個 Prometheus 實例,每個實例通過分區只拉取一部分指標,例如 Prometheus Relabel 配置中的 hashmod 功能,可以對拉取目標的地址進行 hashmod,再將結果匹配自身 ID 的目標保留:

relabel_configs:
- source_labels: [__address__]
  modulus:       3
  target_label:  __tmp_hash
  action:        hashmod
- source_labels: [__tmp_hash]
  regex:         $(PROM_ID)
  action:        keep

或者說,我們想讓每個 Prometheus 拉取一個集羣的指標,一樣可以用 Relabel 來完成:

relabel_configs:
- source_labels:  ["__meta_consul_dc"]
  regex: "dc1"
  action: keep

現在每個 Prometheus 都有各自的數據了,那麼怎麼把他們關聯起來,建立一個全局的視圖呢?官方提供了一個做法:聯邦集羣(federation),即把 Prometheuse Server 按照樹狀結構進行分層,根節點方向的 Prometheus 將查詢葉子節點的 Prometheus 實例,再將指標聚合返回。

聯邦集羣

不過顯然易見的時,使用聯邦集羣依然不能解決問題,首先單點問題依然存在,根節點掛了的話查詢將會變得不可用,如果配置多個父節點的話又會造成數據冗餘和抓取時機導致數據不一致等問題,而且葉子節點目標數量太多時,更加會容易使父節點壓力增大以至打滿宕機,除此之外規則配置管理也是個大麻煩。

還好社區出現了一個 Prometheus 的集羣解決方案:Thanos,它提供了全局查詢視圖,可以從多臺 Prometheus 查詢和聚合數據,因爲所有這些數據均可以從單個端點獲取。

Thanos

  1. Querier 收到一個請求時,它會向相關的 Sidecar 發送請求,並從他們的 Prometheus 服務器獲取時間序列數據。

  2. 它將這些響應的數據聚合在一起,並對它們執行 PromQL 查詢。它可以聚合不相交的數據也可以針對 Prometheus 的高可用組進行數據去重。

有了 Thanos 之後,Prometheus 的水平擴展就能變得更加簡單,不僅如此,Thanos 還提供了可靠的數據存儲方案,可以監聽和備份 prometheus 本地數據到遠程存儲。另外,由於 Thanos 提供了 Prometheus 集羣的全局視圖,那麼針對全局 Prometheus 的記錄規則也不是問題,Thanos 提供的 Ruler 組件,會基於 Thanos Querier 執行規則併發出告警。

再來說到存儲,Prometheus 查詢的高可用可以通過水平擴展 + 統一查詢視圖的方式解決,那麼存儲的高可用要怎麼解決呢?在 Prometheus 的設計中,數據是以本地存儲的方式進行持久化的,雖然本地持久化方便,當也會帶來一些麻煩,比如節點掛了或者 Prometheus 被調度到其他節點上,就會意味着原節點上的監控數據在查詢接口中丟失,本地存儲導致了 Prometheus 無法彈性擴展,爲此 Prometheus 提供了Remote ReadRemote Write功能,支持把 Prometheus 的時間序列遠程寫入到遠端存儲中,查詢時可以從遠端存儲中讀取數據。

其中一個例子中就是 M3DB,M3DB 是一個分佈式的時間序列數據庫,它提供了 Prometheus 的遠程讀寫接口,當一個時間序列寫入到 M3DB 集羣后會按照分片(Shard)和複製(Replication Factor)參數把數據複製到集羣的其他節點上,實現存儲高可用。除了 M3DB 外,Prometheus 目前還支持 InfluxDB、OpenTSDB 等作爲遠程寫的端點。

解決了 Prometheus 的高可用,再來關注一下 Prometheus 如何對監控目標進行採集,當監控節點數量較小時,可以通過Static Config將目標主機列表寫到 Prometheus 的拉取配置中,但如果目標節點一多的話這種方式管理便有很大問題了,而且在生產環境中,服務實例的 IP 通常不是固定的,這時候用靜態配置就沒辦法對目標節點進行有效管理,這時候 Prometheus 提供的服務發現功能便可以有效解決監控節點狀態變化的問題,在這種模式下,Prometheus 會到註冊中心監聽查詢節點列表,定期對節點進行指標的拉取。如果對服務發現有更靈活的需求,Prometheus 也支持基於文件的服務發現功能,這時候我們可以從多個註冊中心中獲取節點列表,再通過自己的需求進行過濾,最終寫入到文件,這時候 Prometheus 檢測到文件變化後便能動態地替換監控節點,再去拉取目標了。

前面看到 Prometheus 都是以拉模式定期對目標節點進行抓取的,那假如有一種情況是一些任務節點還沒來得及被拉取就運行完退出了,這時候監控數據就會丟失,爲了應對這種情況,Prometheus 提供了一個工具:Pushgateway,用來接收來自服務的主動上報,它適用於那些短暫存活的批量任務來將指標推送並暫存到自身上,藉着再由 Prometheus 來拉取自身,以防止指標還沒來得及被 Prometheus 拉取便退出。除此以外 Pushgateway 也適用於在 Prometheus 與應用節點運行在異構網絡或被防火牆隔絕時,無法主動拉取節點的問題,在這種情況下應用節點可以通過使用 Pushgateway 的域名將指標推送到 Pushgateway 實例上,Prometheus 就可以拉取同網絡下的 Pushgateway 節點了,另外配置拉取 Pushgateway 時要注意一個問題:Prometheus 會把每個指標賦予jobinstance標籤,當 Prometheus 拉取 Pushgateway 時,jobinstance則可能分別是 Pushgateway 和 Pushgateway 主機的 ip,當 pushgateway 上報的指標中也包含jobinstance標籤時,Prometheus 會把衝突的標籤重命名爲exported_jobexported_instance,如果需要覆蓋這兩個標籤的話,需要在 Prometheus 中配置honor_labels: true

Pushgateway 可以替代拉模型來作爲指標的收集方案,但在這種模式下會帶來許多負面影響:

最後再來聊一下 Alertmanager,簡單說 Alertmanager 是與 Prometheus 分離的告警組件,主要接收 Promethues 發送過來的告警事件,然後對告警進行去重,分組,抑制和發送,在實際中可以搭配 webhook 把告警通知發送到企業微信或釘釘上,其架構圖如下:

最後的最後再來嘗試一下用 Kubernetes 來搭建一套 Prometheus 的監控系統,關於 Kubernetes 也是摸爬滾打折騰了一週才清楚怎麼使用的,雖然 Promehteus 已經有官方的 Operator 了,但是爲了學習都用手動編寫 yaml 文件,整個完成下來發現還是挺方便的,而且只需要用幾個實例就可以完成收集監控 200 + 服務數千個實例的業務指標。

爲了部署 Prometheus 實例,需要聲明 Prometheus 的 StatefulSet,Pod 中包括了三個容器,分別是 Prometheus 以及綁定的 Thanos Sidecar,最後再加入一個 watch 容器,來監聽 prometheus 配置文件的變化,當修改 ConfigMap 時就可以自動調用 Prometheus 的 Reload API 完成配置加載,這裏按照之前提到的數據分區的方式,在 Prometheus 啓動前加入一個環境變量PROM_ID,作爲 Relabel 時 hashmod 的標識,而POD_NAME用作 Thanos Sidecar 給 Prometheus 指定的external_labels.replica來使用:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: prometheus
  labels:
    app: prometheus
spec:
  serviceName: "prometheus"
  updateStrategy:
    type: RollingUpdate
  replicas: 3
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
        thanos-store-api: "true"
    spec:
      serviceAccountName: prometheus
      volumes:
      - name: prometheus-config
        configMap:
          name: prometheus-config
      - name: prometheus-data
        hostPath:
          path: /data/prometheus
      - name: prometheus-config-shared
        emptyDir: {}
      containers:
      - name: prometheus
        image: prom/prometheus:v2.11.1
        args:
          - --config.file=/etc/prometheus-shared/prometheus.yml
          - --web.enable-lifecycle
          - --storage.tsdb.path=/data/prometheus
          - --storage.tsdb.retention=2w
          - --storage.tsdb.min-block-duration=2h
          - --storage.tsdb.max-block-duration=2h
          - --web.enable-admin-api
        ports:
          - name: http
            containerPort: 9090
        volumeMounts:
          - name: prometheus-config-shared
            mountPath: /etc/prometheus-shared
          - name: prometheus-data
            mountPath: /data/prometheus
        livenessProbe:
          httpGet:
            path: /-/healthy
            port: http
      - name: watch
        image: watch
        args: ["-v""-t""-p=/etc/prometheus-shared""curl""-X""POST""--fail""-o""-""-sS""http://localhost:9090/-/reload"]
        volumeMounts:
        - name: prometheus-config-shared
          mountPath: /etc/prometheus-shared
      - name: thanos
        image: improbable/thanos:v0.6.0
        command: ["/bin/sh""-c"]
        args:
          - PROM_ID=`echo $POD_NAME| rev | cut -d '-' -f1` /bin/thanos sidecar
            --prometheus.url=http://localhost:9090
            --reloader.config-file=/etc/prometheus/prometheus.yml.tmpl
            --reloader.config-envsubst-file=/etc/prometheus-shared/prometheus.yml
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
        ports:
          - name: http-sidecar
            containerPort: 10902
          - name: grpc
            containerPort: 10901
        volumeMounts:
          - name: prometheus-config
            mountPath: /etc/prometheus
          - name: prometheus-config-shared
            mountPath: /etc/prometheus-shared

因爲 Prometheus 默認是沒辦法訪問 Kubernetes 中的集羣資源的,因此需要爲之分配 RBAC:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: prometheus
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: prometheus
  namespace: default
  labels:
    app: prometheus
rules:
- apiGroups: [""]
  resources: ["services""pods""nodes""nodes/proxy""endpoints"]
  verbs: ["get""list""watch"]
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["create"]
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["prometheus-config"]
  verbs: ["get""update""delete"]
- nonResourceURLs: ["/metrics"]
  verbs: ["get"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: prometheus
  namespace: default
  labels:
    app: prometheus
subjects:
- kind: ServiceAccount
  name: prometheus
  namespace: default
roleRef:
  kind: ClusterRole
  name: prometheus
  apiGroup: ""

接着 Thanos Querier 的部署比較簡單,需要在啓動時指定store的參數爲dnssrv+thanos-store-gateway.default.svc來發現 Sidecar:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: thanos-query
  name: thanos-query
spec:
  replicas: 2
  selector:
    matchLabels:
      app: thanos-query
  minReadySeconds: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app: thanos-query
    spec:
      containers:
      - args:
        - query
        - --log.level=debug
        - --query.timeout=2m
        - --query.max-concurrent=20
        - --query.replica-label=replica
        - --query.auto-downsampling
        - --store=dnssrv+thanos-store-gateway.default.svc
        - --store.sd-dns-interval=30s
        image: improbable/thanos:v0.6.0
        name: thanos-query
        ports:
        - containerPort: 10902
          name: http
        - containerPort: 10901
          name: grpc
        livenessProbe:
          httpGet:
            path: /-/healthy
            port: http
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: thanos-query
  name: thanos-query
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 10901
    targetPort: http
  selector:
    app: thanos-query
---
apiVersion: v1
kind: Service
metadata:
  labels:
    thanos-store-api: "true"
  name: thanos-store-gateway
spec:
  type: ClusterIP
  clusterIP: None
  ports:
  - name: grpc
    port: 10901
    targetPort: grpc
  selector:
    thanos-store-api: "true"

部署 Thanos Ruler:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: thanos-rule
  name: thanos-rule
spec:
  replicas: 1
  selector:
    matchLabels:
      app: thanos-rule
  template:
    metadata:
      labels:
      labels:
        app: thanos-rule
    spec:
      containers:
      - name: thanos-rule
        image: improbable/thanos:v0.6.0
        args:
        - rule
        - --web.route-prefix=/rule
        - --web.external-prefix=/rule
        - --log.level=debug
        - --eval-interval=15s
        - --rule-file=/etc/rules/thanos-rule.yml
        - --query=dnssrv+thanos-query.default.svc
        - --alertmanagers.url=dns+http://alertmanager.default
        ports:
        - containerPort: 10902
          name: http
        volumeMounts:
          - name: thanos-rule-config
            mountPath: /etc/rules
      volumes:
      - name: thanos-rule-config
        configMap:
          name: thanos-rule-config

部署 Pushgateway:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: pushgateway
  name: pushgateway
spec:
  replicas: 15
  selector:
    matchLabels:
      app: pushgateway
  template:
    metadata:
      labels:
        app: pushgateway
    spec:
      containers:
      - image: prom/pushgateway:v1.0.0
        name: pushgateway
        ports:
        - containerPort: 9091
          name: http
        resources:
          limits:
            memory: 1Gi
          requests:
            memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: pushgateway
  name: pushgateway
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 9091
    targetPort: http
  selector:
    app: pushgateway

部署 Alertmanager:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: alertmanager
spec:
  replicas: 3
  selector:
    matchLabels:
      app: alertmanager
  template:
    metadata:
      name: alertmanager
      labels:
        app: alertmanager
    spec:
      containers:
      - name: alertmanager
        image: prom/alertmanager:latest
        args:
          - --web.route-prefix=/alertmanager
          - --config.file=/etc/alertmanager/config.yml
          - --storage.path=/alertmanager
          - --cluster.listen-address=0.0.0.0:8001
          - --cluster.peer=alertmanager-peers.default:8001
        ports:
        - name: alertmanager
          containerPort: 9093
        volumeMounts:
        - name: alertmanager-config
          mountPath: /etc/alertmanager
        - name: alertmanager
          mountPath: /alertmanager
      volumes:
      - name: alertmanager-config
        configMap:
          name: alertmanager-config
      - name: alertmanager
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  labels:
    name: alertmanager-peers
  name: alertmanager-peers
spec:
  type: ClusterIP
  clusterIP: None
  selector:
    app: alertmanager
  ports:
  - name: alertmanager
    protocol: TCP
    port: 9093
    targetPort: 9093

最後部署一下 ingress,大功告成:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: pushgateway-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
  - host: $(DOMAIN)
    http:
      paths:
      - backend:
          serviceName: pushgateway
          servicePort: 9091
        path: /metrics
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: prometheus-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: $(DOMAIN)
    http:
      paths:
      - backend:
          serviceName: thanos-query
          servicePort: 10901
        path: /
      - backend:
          serviceName: alertmanager
          servicePort: 9093
        path: /alertmanager
      - backend:
          serviceName: thanos-rule
          servicePort: 10092
        path: /rule
      - backend:
          serviceName: grafana
          servicePort: 3000
        path: /grafana

訪問 Prometheus 地址,監控節點狀態正常:

轉自:知乎 - tonnie

鏈接:https://zhuanlan.zhihu.com/p/101184971

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DuggRX8DOlt_5k1eilAMMA