写在前面

在云原生的时代,可观测性(Observability) 已经成为系统稳定运行的核心能力之一。随着业务规模的不断扩大,单机监控已无法满足分布式系统的需求,构建一套高效、可扩展的监控体系显得尤为重要。

本文将基于 Kubernetes(K8S) 环境,完整搭建一套 Prometheus + Grafana + Alertmanager 的监控与告警系统,实现从 数据采集、可视化展示到告警通知 的全链路监控闭环。

1. node-exporter的组件安装和配置

1.1 node-exporter 介绍

node-exporter 可以采集机器(物理机、虚拟机、云主机等)的监控指标数据,能够采集到的指标包 括 CPU, 内存,磁盘,网络,文件数等信息。

1.2 安装node-exporter组件

网络有限制的朋友可以使用我下载好的:node-exporter

--- 1. 创建名称空间
[root@k8s-master /manifests/daemonset]# kubectl create ns monitor-sa

--- 2. 使用daemonset控制器
[root@k8s-master /manifests/daemonset]# cat node_export.yaml 
apiVersion: apps/v1
## 可以保证 k8s 集群的每个节点都运行完全一样的 pod
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitor-sa
  labels:
    name: node-exporter
spec:
  selector:
    matchLabels:
     name: node-exporter
  template:
    metadata:
      labels:
        name: node-exporter
    spec:
      ## 可以看到宿主机里正在运行的所有进程
      hostPID: true
      ## 直接与宿主机进行 IPC(进程间通信)通信
      hostIPC: true
      ## 使用宿主机网络,同时会把宿主机的9100端口映射出来,这样就不需要创建service了。
      hostNetwork: true
      ## 这个 Pod 可以容忍 master 节点上的 node-role.kubernetes.io/master 这个污点,可以调度到 master 节点上运行
      tolerations:
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"
      ## 持久化存储
      volumes:
      - name: proc
        hostPath:
           path: /proc
      - name: dev
        hostPath:
           path: /dev
      - name: sys
        hostPath:
           path: /sys
      - name: rootfs
        hostPath:
           path: /
      containers:
      - name: node-exporter
        ## 这里我用的是镜像仓库,也可以直接使用本地镜像
        image: 192.168.0.77:32237/k8s/node-exporter:latest
        ports:
        - containerPort: 9100
        resources:
          requests:
             ## 这个容器运行至少需要CPU的核心数
             cpu: 0.15
        securityContext:
          ## 开启容器的特权模式
          privileged: true
        args:
        ## 配置挂载宿主机(node 节点)的路径
        - --path.procfs
        - /host/proc
        - --path.sysfs
        - /host/sys
        - --collector.filesystem.ignored-mount-points 
        - '"^/(sys|proc|dev|host|etc)($|/)"'
        ## 将主机/dev、/proc、/sys 这些目录挂在到容器中,这是因为我们采集的很多节点数据都是通过这些文件来获取系统信息的
        volumeMounts:
        - name: dev
          mountPath: /host/dev
        - name: proc
          mountPath: /host/proc  
        - name: sys
          mountPath: /host/sys
        - name: rootfs
          mountPath: /rootfs

--- 3. 通过 kubectl apply 更新 node-exporter.yaml 文件
[root@k8s-master /manifests/daemonset]# kubectl apply -f node_export.yaml
## 查看pod 是否正常 
[root@k8s-master /manifests/daemonset]# kubectl get pod -n monitor-sa

--- 4. 通过 node-exporter 采集数据
curl http://主机 ip:9100/metrics 
# node-export 默认的监听端口是 9100,可以看到当前主机获取到的所有监控数据
[root@k8s-master /manifests/daemonset]# curl 192.168.0.163:9100/metrics |grep node_cpu_seconds  
[root@k8s-master /manifests/daemonset]# curl 192.168.0.163:9100/metrics |grep node_load
node_load1 该指标反映了当前主机在最近一分钟以内的负载情况,系统的负载情况会随系统资源的使用而变化,因此 node_load1 反映的是当前状态,数据可能增加也可能减少

2. Prometheus server组件的安装和配置

2.1 创建sa账号,对sa做rbac 授权

## 创建sa账号
[root@k8s-master /manifests/daemonset]# kubectl create serviceaccount monitor -n monitor-sa
[root@k8s-master /manifests/daemonset]# kubectl create clusterrolebinding monitor-clusterrolebinding   --clusterrole=cluster-admin   --serviceaccount=monitor-sa:monitor 
## 参数解释
kubectl create clusterrolebinding  ## 创建一个集群级别的角色绑定(ClusterRoleBinding)
monitor-clusterrolebinding         ## 这是 ClusterRoleBinding 资源的名字(随便取,用来标识这个绑定)
clusterrole=cluster-admin          ## 指定要绑定的 ClusterRole,这里是 cluster-admin(内置的超级管理员角色)
serviceaccount=monitor-sa:monitor  ## 指定绑定的 ServiceAccount,格式 名称空间:服务名,表示这个 SA 将获得上面 ClusterRole 的权限。

2.2 创建pv和pvc做数据持久化

--- 1. 创建目录
[root@k8s-master /data/nfs]# mkdir -p /data/nfs/prometheus
## 权限最好是65534 不然可能会启动不了
[root@k8s-master /data/nfs]# chown -R 65534:65534 prometheus/
## 创建pv,这里我使用K8S自带的nfs 存储,如果有变化的要自行修改哦

--- 2. 创建pv
## 怎么创建nfs存储可以在我其他文章里有提过
[root@k8s-master /manifests/pv]# cat prometheus-pv.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: prometheus-pv
  namespace: monitor-sa
spec:
  accessModes:
  - ReadWriteMany
  nfs:
   server: 192.168.0.160
   path: /data/nfs/prometheus
  persistentVolumeReclaimPolicy: Retain
  capacity: 
    storage: 2Gi

--- 3. 创建pvc
[root@k8s-master /manifests/pv]# cat prometheus-pvc.yaml 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: prometheus-pvc
  namespace: monitor-sa
spec:
  ## 声明资源的访问模式
  accessModes:
  - ReadWriteMany
  ## 声明资源的使用量
  resources:
    requests:
     storage: 2Gi
  ## 绑定上面的pv
  volumeName: prometheus-pv

2.3 创建一个 configmap 存储卷,用来存放 prometheus 配置信息

[root@k8s-master /manifests/configmap]# cat prometheus-cfg.yaml 
kind: ConfigMap
apiVersion: v1
metadata:
  labels:
    app: prometheus
  name: prometheus-config
  namespace: monitor-sa
data:
  prometheus.yml: |
    global:
      ## 采集目标主机监控据的时间间隔
      scrape_interval: 15s
      ## 数据采集超时时间,默认 10s
      scrape_timeout: 10s
      ## 触发告警检测的时间,默认是 1m
      evaluation_interval: 1m
    scrape_configs:
    - job_name: 'kubernetes-node'
      ## 使用的是 k8s 的服务发现 
      kubernetes_sd_configs:
      ## 使用 node 角色,它使用默认的 kubelet 提供的 http 端口来发现集群中每个 node 节点
      - role: node
      ## 重新标记
      relabel_configs:
      ## 匹配原始标签,匹配地址
      - source_labels: [__address__]
        ## 匹配带有10250端口的url
        regex: '(.*):10250'
        ## 把匹配到的IP:10250的ip保留下来
        replacement: '${1}:9100'
        ## 新生成的 url 是${1}获取到的 ip:9100
        target_label: __address__
        action: replace
      - action: labelmap
        ## 匹配到下面正则表达式的标签会被保留,如果不做 regex 正则的话,默认只是会显示 instance 标签
        regex: __meta_kubernetes_node_label_(.+)

    - job_name: 'kubernetes-node-cadvisor'
      ## 抓取 cAdvisor 数据,是获取 kubelet 上/metrics/cadvisor 接口数据来获取容器的资源使用情况
      kubernetes_sd_configs:
      - role:  node
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      relabel_configs:
        ## 把匹配到的标签保留
      - action: labelmap
        ## 保留匹配到的具有__meta_kubernetes_node_label 的标签 
        regex: __meta_kubernetes_node_label_(.+)
        ## 获取到的地址:__address__="192.168.0.160:10250" 
      - target_label: __address__
        ## 把获取到的地址替换成新的地址 kubernetes.default.svc:443 
        replacement: kubernetes.default.svc:443
        ## 把原始标签中__meta_kubernetes_node_name 值匹配到
      - source_labels: [__meta_kubernetes_node_name]
        regex: (.+)
        target_label: __metrics_path__
        replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor

    - job_name: 'kubernetes-apiserver'
      kubernetes_sd_configs:
        ## 使用 k8s 中的 endpoint 服务发现,采集 apiserver 6443 端口获取到的数据 
      - role: endpoints
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      relabel_configs:
        ## endpoint 这个对象的名称空间,服务名称,端口名称
      - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
        ## 采集满足条件的实例,其他的不采集
        action: keep
        regex: default;kubernetes;https

    - job_name: 'kubernetes-service-endpoints'
      kubernetes_sd_configs:
      - role: endpoints
      relabel_configs:
        ## 重新打标仅抓取到的具有 "prometheus.io/scrape: true" 的 annotation 的端点,
        #意思是说如果某个 service 具有 prometheus.io/scrape = true annotation 声明则抓取,
        #annotation 本身也是键值结构,所以这里的源标签设置为键,而 regex 设置值 true,当值匹配到 regex 设定的内容时则执行 keep 动作也就是保留,其余则丢弃。
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
        action: keep
        regex: true
        ## 重新设置 scheme,匹配源标签__meta_kubernetes_service_annotation_prometheus_io_scheme 
        #也就是 prometheus.io/scheme annotation,如果源标签的值匹配到 regex,则把值替换为__scheme__对应的值。
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
        action: replace
        target_label: __scheme__
        regex: (https?)
        ## 应用中自定义暴露的指标,也许你暴露的 API 接口不是/metrics 这个路径,
        #那么你可以在这个POD 对应的 service 中做一个"prometheus.io/path = /mymetrics" 声明,
        #上面的意思就是把你声明的这个路径赋值给__metrics_path__,其实就是让 prometheus 来获取自定义应用暴露的 metrices 的具体路径,
        #不过这里写的要和 service 中做好约定,如果 service 中这样写 prometheus.io/app-metrics-path:'/metrics' 那么你这里就要 __meta_kubernetes_service_annotation_prometheus_io_app_metrics_path 这样写
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)
        ## 暴露自定义的应用的端口,就是把地址和你在 service 中定义的 "prometheus.io/port = <port>" 声明做一个拼接,
        #然后赋值给__address__,这样 prometheus 就能获取自定义应用的端口,
        #然后通过这个端口再结合__metrics_path__来获取指标,如果__metrics_path__值不是默认的/metrics 那么就要使用上面的标签替换来获取真正暴露的具体路径。
      - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
        action: replace
        target_label: __address__
        regex: ([^:]+)(?::\d+)?;(\d+)
        replacement: $1:$2
      - action: labelmap
        regex: __meta_kubernetes_service_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_service_name]
        action: replace
        target_label: kubernetes_name 

2.4 通过 deployment 部署 prometheus

如果网络有限制的朋友可以使用我下载好的镜像:prometheus-server

[root@k8s-master /manifests/deployment]# cat prometheus-server.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-server
  namespace: monitor-sa
  labels:
    app: prometheus
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus
      component: server
  template:
    metadata:
      labels:
        app: prometheus
        component: server
      annotations:
        prometheus.io/scrape: 'false'
    spec:
      ## 指定pod调度的节点
      ## 在k8s集群的哪个节点创建数据目录,就让 pod 调度到哪个节点
      nodeName: k8s-master
      serviceAccountName: monitor
      securityContext:
        runAsUser: 65534
        runAsGroup: 65534
        fsGroup: 65534
      containers:
      - name: prometheus
        ## 这里的镜像在我的私有仓库哦
        image: 192.168.0.77:32237/prom/prometheus:v2.53.5
        command:
          - prometheus
          - --config.file=/etc/prometheus/prometheus.yml
          ## 旧数据的存储目录
          - --storage.tsdb.path=/prometheus
          ## 何时删除旧数据,默认15天
          - --storage.tsdb.retention=720h
          ## 开启热加载
          - --web.enable-lifecycle
        ports:
        - containerPort: 9090
          protocol: TCP
        volumeMounts:
        - mountPath: /etc/prometheus/prometheus.yml
          name: prometheus-config
          subPath: prometheus.yml
        - mountPath: /prometheus/
          name: prometheus-storage-volume
      volumes:
        - name: prometheus-config
          configMap:
            name: prometheus-config
            items:
              - key: prometheus.yml
                path: prometheus.yml
                mode: 0644
        - name: prometheus-storage-volume
          hostPath:
           path: /data/nfs/prometheus
           type: Directory

2.5 创建service

[root@k8s-master /manifests/service]# cat prometheus-svc.yaml 
apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: monitor-sa
  labels:
    app: prometheus
spec:
  type: NodePort
  ports:
    - port: 9090
      targetPort: 9090
      protocol: TCP
  selector:
    app: prometheus
    component: server

## 更新service
[root@k8s-master /manifests/service]# kubectl apply -f prometheus-svc.yaml 
## 查看 service 在物理机映射的端口
[root@k8s-master /manifests/service]# kubectl get svc -n monitor-sa
NAME         TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
prometheus   NodePort   10.200.162.114   <none>        9090:31326/TCP   80m

## 通过上面可以看到 service 在宿主机上映射的端口是 31326,这样我们访问 k8s 集群的 master 节点的 ip:31326,就可以访问到 prometheus 的 web ui 界面了

2.6 Prometheus 热加载

## 为了每次修改配置文件可以热加载 prometheus,也就是不停止 prometheus,就可以使配置生效,想要使配置生效可用如下热加载命令: 
[root@k8s-master /manifests/service]# kubectl get pod -owide -n monitor-sa -l app=prometheus
NAME                                 READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
prometheus-server-844b4b7c9b-pqqnd   1/1     Running   0          88m   10.100.235.206   k8s-master   <none>           <none>
## 我们可以看出pod的ip是10.100.235.206
## 想要使配置生效可用如下命令热加载
[root@k8s-master /manifests/service]# curl -X POST http://10.100.235.206:9090/-/reload
## 热加载速度比较慢,可以暴力重启 prometheus 删除yaml文件的方式,但是线上最好热加载,暴力删除可能造成监控数据的丢失! 

3. 安装Grafana

3.1 通过deployment 部署grafana

[root@k8s-master /manifests/deployment]# cat grafana.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: monitoring-grafana
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      task: monitoring
      k8s-app: grafana
  template:
    metadata:
      labels:
        task: monitoring
        k8s-app: grafana
    spec:
      initContainers:
      - name: init-grafana-data
        image: busybox:1.28
        command:
        - sh
        - -c
        - |
          mkdir -p /var/lib/grafana/plugins
          chown -R 472:472 /var/lib/grafana
        volumeMounts:
        - name: grafana-storage
          mountPath: /var/lib/grafana

      containers:
      - name: grafana
        image: 192.168.0.77:32237/k8s/grafana:latest
        ports:
        - containerPort: 3000
          protocol: TCP
        env:
        - name: INFLUXDB_HOST
          value: monitoring-influxdb
        - name: GF_SERVER_HTTP_PORT
          value: "3000"
        - name: GF_AUTH_BASIC_ENABLED
          value: "false"
        - name: GF_AUTH_ANONYMOUS_ENABLED
          value: "true"
        - name: GF_AUTH_ANONYMOUS_ORG_ROLE
          value: Admin
        - name: GF_SERVER_ROOT_URL
          value: /
        volumeMounts:
        - mountPath: /var/lib/grafana
          name: grafana-storage
        - mountPath: /etc/ssl/certs
          name: ca-certificates
          readOnly: true

      volumes:
      - name: grafana-storage
        emptyDir: {}
      - name: ca-certificates
        hostPath:
          path: /etc/ssl/certs

3.2 创建service

[root@k8s-master /manifests/service]# cat grafana-svc.yaml 
apiVersion: v1
kind: Service
metadata:
  labels:
    kubernetes.io/cluster-service: 'true'
    kubernetes.io/name: monitoring-grafana
  name: monitoring-grafana
  namespace: kube-system
spec:
  ports:
  - port: 80
    targetPort: 3000
  selector:
    k8s-app: grafana
  type: NodePort

3.3 Grafana 界面接入 Prometheus 数据源

--- 1. 查看 grafana 前端的 service
[root@k8s-master /manifests/service]# kubectl  get svc -n kube-system |grep grafana
monitoring-grafana   NodePort    10.200.164.156   <none>        80:31593/TCP             87s

--- 2. 浏览器登录 192.168.0.160:31593
## 默认账号密码登录: 账号 admin  密码: admin
## 添加数据源,搜索prometheus
## 连接的地址:
http://prometheus.monitor-sa.svc:9090
## prometheus → Kubernetes 中你 Prometheus 服务的名字(Service 名)
## monitor-sa → Prometheus 所在的 Namespace
## svc → Kubernetes 内部 DNS 的固定后缀,表示这是一个 Service。
## :9090 → Prometheus 服务的端口号(默认 9090)

我这里两份模板觉得还不错,分别是监控docker和K8S的模板,可以导入体验一下

docker: docker.json

K8S: node_exporter.json

4. 配置 alertmanager-发送报警到个人邮箱

报警:指 prometheus 将监测到的异常事件发送给 alertmanager

通知:alertmanager 将报警信息发送到邮件、微信、钉钉等

4.1 创建 alertmanager 配置文件

[root@k8s-master /manifests/configmap]# cat altermanager.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  name: alertmanager
  namespace: monitor-sa
data:
  alertmanager.yml: |-
    global:
      resolve_timeout: 5m
      ## 这是指定从哪个邮箱发送报警 
      smtp_from: 'xing775288271@163.com'
      ## 163 邮箱的 SMTP 服务器地址+端口 
      smtp_smarthost: 'smtp.163.com:465'  
      smtp_hello: '163.com'
      ## 发送邮箱的认证用户,不是邮箱名
      smtp_auth_username: 'xing775288271@163.com'
      ## 授权码
      smtp_auth_password: 'DHTqgQ6pF9kb8FGg'
      smtp_require_tls: false
    ## 用于配置告警分发策略
    route:
      ## 采用哪个标签来作为分组依据 
      group_by: [alertname]  
      ## 组告警等待时间。也就是告警产生后等待 10s,如果有同组告警一起发出
      group_wait: 10s
      ## 上下两组发送告警的间隔时间        
      group_interval: 10s 
      ## 重复发送告警的时间,减少相同邮件的发送频率,默认是 1h   
      repeat_interval: 10m   
      ## 定义谁来收告警 
      receiver: default-receiver  
    receivers:
    - name: 'default-receiver'  
      email_configs:
        ## 后面指定发送到哪个邮箱,我发送到我的企业邮箱 
      - to: 'xingzhibang@pexetech.com'
        send_resolved: true

4.2 创建altermanager的告警规则配置文件

我这里有写好的配置文件模板,可以用一下:prometheus-alertmanager-cfg.yaml

## 文件需要修改内容如下:
...
- job_name: 'kubernetes-schedule'
      scrape_interval: 5s
      static_configs:
      ## scheduler 组件所在节点的 ip
      - targets: ['192.168.0.160:10251']
- job_name: 'kubernetes-controller-manager'
      scrape_interval: 5s
      static_configs:
      ## controller-manager组件所在节点的 ip
      - targets: ['192.168.0.160:10252']
- job_name: 'kubernetes-kube-proxy'
      scrape_interval: 5s
      static_configs:
      ## kube-proxy 组件所在节点的 ip 
      - targets: ['192.168.0.160:10249','192.168.0.162:10249','192.168.0.163:10249']
- job_name: 'kubernetes-etcd'
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/k8s-certs/etcd/ca.crt
        cert_file: /var/run/secrets/kubernetes.io/k8s-certs/etcd/server.crt
        key_file: /var/run/secrets/kubernetes.io/k8s-certs/etcd/server.key
      scrape_interval: 5s
      static_configs:
      ## kube-proxy 组件所在节点的 ip 
      - targets: ['192.168.0.160:2379']

4.3 通过 deployment 部署 altermanager

网络有限制的朋友可以使用我下载好的altermanager镜像:altermanager.tar.gz

## 配置文件指定了 nodeName 要写你自己环境的 k8s 的 node 节点名字哦
[root@k8s-master /manifests/deployment]# cat prometheus-alertmanager.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-alertmanager
  namespace: monitor-sa
  labels:
    app: prometheus
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus
      component: server
  template:
    metadata:
      labels:
        app: prometheus
        component: server
      annotations:
        prometheus.io/scrape: 'false'
    spec:
      nodeName: k8s-master
      serviceAccountName: monitor
      containers:
      - name: alertmanager
        ## 这里使用的是我的镜像仓库
        image: 192.168.0.77:32237/k8s/alertmanager:latest
        args:
        - "--config.file=/etc/alertmanager/alertmanager.yml"
        - "--log.level=debug"
        ports:
        - containerPort: 9093
          protocol: TCP
          name: alertmanager
        volumeMounts:
        - name: alertmanager-config
          mountPath: /etc/alertmanager
        - name: alertmanager-storage
          mountPath: /alertmanager
        - name: localtime
          mountPath: /etc/localtime
      volumes:
        - name: alertmanager-config
          configMap:
            name: alertmanager
        - name: alertmanager-storage
          hostPath:
           path: /data/alertmanager
           type: DirectoryOrCreate
        - name: localtime
          hostPath:
           path: /usr/share/zoneinfo/Asia/Shanghai 

4.4 创建service

[root@k8s-master /manifests/service]# cat alertmanager-svc.yaml 
apiVersion: v1
kind: Service
metadata:
  labels:
    name: prometheus
    kubernetes.io/cluster-service: 'true'
  name: alertmanager
  namespace: monitor-sa
spec:
  ports:
  - name: alertmanager
    nodePort: 30066
    port: 9093
    protocol: TCP
    targetPort: 9093
  selector:
    app: prometheus
  sessionAffinity: None
  type: NodePort

## service 在物理机映射的端口是 30066 

重启Prometheus的pod后发现有个问题,这三个节点显示连接不上对应的端 口