基於 Prometheus 的監控系統實踐
監控作爲底層基礎設施的一環,是保障生產環境服務穩定性不可或缺的一部分,線上問題從發現到定位再到解決,通過監控和告警手段可以有效地覆蓋了「發現」和「定位」,甚至可以通過故障自愈等手段實現解決,服務開發和運維人員能及時有效地發現服務運行的異常,從而更有效率地排查和解決問題。
一個典型的監控(如白盒監控),通常會關注於目標服務的內部狀態,例如:
-
單位時間接收到的請求數量
-
單位時間內請求的成功率 / 失敗率
-
請求的平均處理耗時
白盒監控很好地描述了系統的內部狀態,但缺少從外部角度看到的現象,比如:白盒監控只能看到已經接收的請求,並不能看到由於 DNS 故障導致沒有發送成功的請求,而黑盒監控此時便可以作爲補充手段,由探針(probe)程序來探測目標服務是否成功返回,更好地反饋系統的當前狀態。
某日需要爲服務搭建一個監控系統來採集應用埋點上報的指標,經過一番對比,最終選擇了 Prometheus 來作爲我們的業務監控,因爲它具有以下優點:
-
支持 PromQL(一種查詢語言),可以靈活地聚合指標數據
-
部署簡單,只需要一個二進制文件就能跑起來,不需要依賴分佈式存儲
-
Go 語言編寫,組件更方便集成在同樣是 Go 編寫項目代碼中
-
原生自帶 WebUI,通過 PromQL 渲染時間序列到面板上
-
生態組件衆多,Alertmanager,Pushgateway,Exporter...
Prometheus 的架構圖如下:
在上面流程中,Prometheus 通過配置文件中指定的服務發現方式來確定要拉取監控指標的目標(Target),接着從要拉取的目標(應用容器和 Pushgateway)發起 HTTP 請求到特定的端點(Metric Path),將指標持久化至本身的 TSDB 中,TSDB 最終會把內存中的時間序列壓縮落到硬盤,除此之外,Prometheus 會定期通過 PromQL 計算設置好的告警規則,決定是否生成告警到 Alertmanager,後者接收到告警後會負責把通知發送到郵件或企業內部羣聊中。
Prometheus 的指標名稱只能由 ASCII 字符、數字、下劃線以及冒號組成,而且有一套命名規範:
-
使用基礎 Unit(如 seconds 而非 milliseconds)
-
指標名以 application namespace 作爲前綴,如:
-
process_cpu_seconds_total
-
http_request_duration_seconds
-
用後綴來描述 Unit,如:
-
http_request_duration_**seconds**
-
node_memory_usage_**bytes**
-
http_requests_**total**
-
process_cpu_**seconds_total**
-
foobar_build**_info**
Prometheus 提供了以下基本的指標類型:
-
Counter:代表一種樣本數據單調遞增的指標,即只增不減,通常用來統計如服務的請求數,錯誤數等。
-
Gauge:代表一種樣本數據可以任意變化的指標,即可增可減,通常用來統計如服務的 CPU 使用值,內存佔用值等。
-
Histogram 和 Summary:用於表示一段時間內的數據採樣和點分位圖統計結果,通常用來統計請求耗時或響應大小等。
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
-
Querier 收到一個請求時,它會向相關的 Sidecar 發送請求,並從他們的 Prometheus 服務器獲取時間序列數據。
-
它將這些響應的數據聚合在一起,並對它們執行 PromQL 查詢。它可以聚合不相交的數據也可以針對 Prometheus 的高可用組進行數據去重。
有了 Thanos 之後,Prometheus 的水平擴展就能變得更加簡單,不僅如此,Thanos 還提供了可靠的數據存儲方案,可以監聽和備份 prometheus 本地數據到遠程存儲。另外,由於 Thanos 提供了 Prometheus 集羣的全局視圖,那麼針對全局 Prometheus 的記錄規則也不是問題,Thanos 提供的 Ruler 組件,會基於 Thanos Querier 執行規則併發出告警。
再來說到存儲,Prometheus 查詢的高可用可以通過水平擴展 + 統一查詢視圖的方式解決,那麼存儲的高可用要怎麼解決呢?在 Prometheus 的設計中,數據是以本地存儲的方式進行持久化的,雖然本地持久化方便,當也會帶來一些麻煩,比如節點掛了或者 Prometheus 被調度到其他節點上,就會意味着原節點上的監控數據在查詢接口中丟失,本地存儲導致了 Prometheus 無法彈性擴展,爲此 Prometheus 提供了Remote Read
和Remote 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 會把每個指標賦予job
和instance
標籤,當 Prometheus 拉取 Pushgateway 時,job
和instance
則可能分別是 Pushgateway 和 Pushgateway 主機的 ip,當 pushgateway 上報的指標中也包含job
和instance
標籤時,Prometheus 會把衝突的標籤重命名爲exported_job
和exported_instance
,如果需要覆蓋這兩個標籤的話,需要在 Prometheus 中配置honor_labels: true
。
Pushgateway 可以替代拉模型來作爲指標的收集方案,但在這種模式下會帶來許多負面影響:
-
Pushgateway 被設計爲一個監控指標的緩存,這意味着它不會主動過期服務上報的指標,這種情況在服務一直運行的時候不會有問題,但當服務被重新調度或銷燬時,Pushgateway 依然會保留着之前節點上報的指標。而且,假如多個 Pushgateway 運行在 LB 下,會造成一個監控指標有可能出現在多個 Pushgateway 的實例上,造成數據重複多份,需要在代理層加入一致性哈希路由來解決
-
在拉模式下,Prometheus 可以更容易的查看監控目標實例的健康狀態,並且可以快速定位故障,但在推模式下,由於不會對客戶端進行主動探測,因此對目標實例的健康狀態也變得一無所知
最後再來聊一下 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