基于prometheus搭建Kubernetes集群监控服务
本篇文章,主要介绍使用promethesu监控kubernetes集群信息以及kubernetes集群中部署的springboot业务指标。通过此篇文章,你能快速搭建自己的监控系统,这对于没有统一监控系统的公司来说,非常省事。对于自己搭建的话,后期可能需要考虑更多的事情,比如采集的监控数据如何存放,磁盘内存毕竟有限,可以考虑写到es、db等,当然这又牵涉到搭建es了,在此就不扩展这些外围组件了。
prometheus具有自动发现服务的功能,它通过kubernetes的api发现Node、Pod、Service等功能,当然它还可以基于file_sd_config定时读取配置文件发现静态目标,这时你可以通过某种机制比如自写shell等脚本更新file_sd_config文件或者基于现有的Consul、DNS等中间件实现。prometheus的数据采集之后,然后通过图形界面进行展示,自带的图形工具效果一般,所以我们选择Grafana进行展示。我们除了监控kubernetes本身的一些基本信息,我们还可以通过prometheus监控部署在集群容器中的业务应用指标,根据指标数据进行告警提示。
下面在kubernetes中单独新建一个namespace(kube-prometheus),在新建的命名空间下来部署prometheus监控服务。
1.创建namespace
新建prometheus-ns.yaml
apiVersion: v1 kind: Namespace metadata: name: kube-prometheus
执行kubectl create -f prometheus-ns.yaml
2.创建prometheus权限资源
新建prometheus-rbac.yaml文件,创建prometheus访问kubernetes容器所需的ClusterRole、ServiceAccount以及ClusterRoleBinding。
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: prometheus rules: - apiGroups: [""] resources: - nodes - nodes/proxy - services - endpoints - pods verbs: ["get", "list", "watch"] - apiGroups: - extensions resources: - ingresses verbs: ["get", "list", "watch"] - nonResourceURLs: ["/metrics"] verbs: ["get"] --- apiVersion: v1 kind: ServiceAccount metadata: name: prometheus namespace: kube-prometheus --- 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-prometheus
执行kubectl create -f prometheus-rbac.yaml
3.编写prometheus的启动配置
在kubernetes中部署prometheus服务,其配置文件通过configman形式进行映射,新建prometheus-cm.yaml
apiVersion: v1 kind: ConfigMap metadata: name: prometheus-config namespace: kube-prometheus data: prometheus.yml: | global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'kubernetes-apiservers' kubernetes_sd_configs: - 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: - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] action: keep regex: default;kubernetes;https - job_name: 'kubernetes-nodes' 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 regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics - job_name: 'kubernetes-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 regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor - job_name: 'kubernetes-service-business-endpoints' kubernetes_sd_configs: - role: endpoints metrics_path: /prometheus relabel_configs: - source_labels: [__meta_kubernetes_endpoints_name] action: keep regex: (container-console-service) - job_name: 'kubernetes-service-endpoints' kubernetes_sd_configs: - role: endpoints relabel_configs: - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] action: keep regex: true - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] action: replace target_label: __scheme__ regex: (https?) - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] action: replace target_label: __metrics_path__ regex: (.+) - 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 - job_name: 'kubernetes-services' kubernetes_sd_configs: - role: service metrics_path: /probe params: module: [http_2xx] relabel_configs: - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe] action: keep regex: true - source_labels: [__address__] target_label: __param_target - target_label: __address__ replacement: blackbox-exporter.example.com:9115 - source_labels: [__param_target] target_label: instance - action: labelmap regex: __meta_kubernetes_service_label_(.+) - source_labels: [__meta_kubernetes_namespace] target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_service_name] target_label: kubernetes_name - job_name: 'kubernetes-ingresses' kubernetes_sd_configs: - role: ingress relabel_configs: - source_labels: [__meta_kubernetes_ingress_annotation_prometheus_io_probe] action: keep regex: true - source_labels: [__meta_kubernetes_ingress_scheme,__address__,__meta_kubernetes_ingress_path] regex: (.+);(.+);(.+) replacement: ${1}://${2}${3} target_label: __param_target - target_label: __address__ replacement: blackbox-exporter.example.com:9115 - source_labels: [__param_target] target_label: instance - action: labelmap regex: __meta_kubernetes_ingress_label_(.+) - source_labels: [__meta_kubernetes_namespace] target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_ingress_name] target_label: kubernetes_name - job_name: 'kubernetes-pods' kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: true - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] action: replace target_label: __metrics_path__ regex: (.+) - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] action: replace regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:$2 target_label: __address__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - source_labels: [__meta_kubernetes_namespace] action: replace target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_pod_name] action: replace target_label: kubernetes_pod_name
执行 kubectl create -f prometheus-cm.yaml
4.部署prometheus服务
新建prometheus-deploy.yaml
apiVersion: apps/v1beta2 kind: Deployment metadata: labels: name: prometheus-deployment name: prometheus namespace: kube-prometheus spec: replicas: 1 selector: matchLabels: app: prometheus template: metadata: labels: app: prometheus spec: containers: - image: prom/prometheus:v2.3.2 name: prometheus command: - "/bin/prometheus" args: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--storage.tsdb.retention=24h" ports: - containerPort: 9090 protocol: TCP volumeMounts: - mountPath: "/prometheus" name: data - mountPath: "/etc/prometheus" name: config-volume resources: requests: cpu: 100m memory: 100Mi limits: cpu: 500m memory: 2500Mi serviceAccountName: prometheus volumes: - name: data emptyDir: {} - name: config-volume configMap: name: prometheus-config
prometheus的启动配置通过configMap方式映射到容器中,执行kubectl create -f prometheus-deploy.yaml创建部署。
新建prometheus-svc.yaml
kind: Service apiVersion: v1 metadata: labels: app: prometheus name: prometheus namespace: kube-prometheus spec: type: NodePort ports: - port: 9090 targetPort: 9090 nodePort: 30003 selector: app: prometheus
执行kubectl create -f prometheus-svc.yaml创建服务,通过NodePort方式将容器的9090端口映射到宿主机的30003端口,通过浏览器访问http://ip:30003/,应该可以看到prometheus的web页面了。如果配置文件格式错误,可以通过kubectl logs podId 查看具体的解析错误。
5.部署grafana服务
新建grafana-deploy.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: grafana-core namespace: kube-prometheus labels: app: grafana component: core spec: replicas: 1 template: metadata: labels: app: grafana component: core spec: containers: - image: grafana/grafana:5.0.0 name: grafana-core imagePullPolicy: IfNotPresent resources: limits: cpu: 100m memory: 100Mi requests: cpu: 100m memory: 100Mi env: - name: GF_AUTH_BASIC_ENABLED value: "true" - name: GF_AUTH_ANONYMOUS_ENABLED value: "false" readinessProbe: httpGet: path: /login port: 3000 volumeMounts: - name: grafana-persistent-storage mountPath: /var volumes: - name: grafana-persistent-storage emptyDir: {}
这里说一下,我们最好创建存储卷,把相关的数据文件以及插件持久化到本地目录中,grafana的5.1+版本以上有些变化,功能有所提升,如果添加/var/lib/grafana数据卷,请手动赋予文件附属权给472,chown -R 472:472,否则很可能创建失败。
执行kubectl create -f grafana-deploy.yaml创建grafana部署,它的一些相关配置可以通过环境变量的方式注入。接下来新建grafana-svc.yaml创建对应的服务,并将容器的3000端口映射到宿主机的30002端口。
apiVersion: v1 kind: Service metadata: name: grafana namespace: kube-prometheus labels: app: grafana component: core annotations: prometheus.io/scrape: 'true' spec: type: NodePort ports: - port: 3000 nodePort: 30002 selector: app: grafana component: core
执行kubectl create -f grafana-svc.yaml创建服务成功之后,则可以通过http://ip:30002/访问grafana的web服务了。上面配置中的annotations是用来控制prometheus采集的,prometheus会采集到__meta_kubernetes_service_annotation_prometheus_io_scrape=true标签,通过该标签就可以控制哪些目标需要采集,避免无谓的去采集目标,可以起到一个过滤filter的作用。
6.部署node-exporter
这个东西不是必须的,node-exporter这个是官方自带的一个数据收集器,当前很多组件都有对应的第三方收集器。node-exporter就是启动了一个http_server,持续不断去采集Linux服务的cpu、内存、磁盘、网络IO等各种数据,它提供的指标数据远远大于你想象的。它提供了一个符合prometheus格式的数据接口,prometheus定期过来请求该接口即可获取机器的各项指标输数据。它既然是监控主机信息的,在kubernetes中则最好以DaemonSet方式运行在每个node节点上。新建node-exporter-ds.yaml
apiVersion: extensions/v1beta1 kind: DaemonSet metadata: name: node-exporter namespace: kube-prometheus labels: k8s-app: node-exporter spec: template: metadata: labels: k8s-app: node-exporter spec: containers: - image: prom/node-exporter:latest name: node-exporter ports: - containerPort: 9100 protocol: TCP name: http
执行kubectl create -f node-exporter-ds.yaml创建资源,然后新建node-exporter-svc.yaml创建服务。
apiVersion: v1 kind: Service metadata: labels: k8s-app: node-exporter name: node-exporter namespace: kube-prometheus spec: ports: - name: http port: 9100 nodePort: 31672 protocol: TCP type: NodePort selector: k8s-app: node-exporter
上面已经将容器端口映射到宿主机的31672,服务启动成功之后,在宿主机上执行curl http://ip:31672/metrics就可以看到node-exporter采集到的目标数据。至此,监控的所有服务部署完毕。
下面稍微解释下prometheus的启动配置,以上面prometheus-cm.yaml中内容为例,在该文件中,其中有一段如下:
- job_name: 'kubernetes-service-business-endpoints' kubernetes_sd_configs: - role: endpoints metrics_path: /prometheus relabel_configs: - source_labels: [__meta_kubernetes_endpoints_name] action: keep regex: (container-console-service)
配置文件中,剪除上面这一小段配置之外,可以作为一个通用的监控k8s的启动配置。上面这段配置是我针对下面的springboot应用进行监控添加的。springboot应用是部署在k8s中,自然我们也会为这个应用创建部署deployment、service,有service自然则会产生endpoints,即podIp:containerPort。通过kubernetes_sd_config相关配置,可以让prometheus自动去发现应用对应的pod实例,pod数量在系统运行中经常自动变化,有了自动发现功能,则可以监控到所有的pod实例。mertrics_path不填写默认是/metrics,而springboot自带的mertrics数据格式不符合prometheus的规则,这里我们设置为/prometheus。
relabel_configs是个非常有用的东西,它可以将source_labels的东西重写,__meta_kubernetes_endpoints_name是prometheus提供的一个label,这里的springboot应用服务名称即为container-console-service,这样我们通过regex匹配规则,进而执行action,keep表示匹配了regex就去采集。
上面的prometheus服务已经具备监控kubernetes集群的基本信息以及node节点的信息,有时候,我们还需通过监控系统监控业务系统的指标,比如调用接口一共请求了多少次,失败或者异常了多少次,平均的请求时间,尤其是某些关键业务,如订单处理失败等,需要能监控到并且告警通知相应的负责人,下面以一个基础的springboot1.5.10版本的项目为例。
A. 引入prometheus包
io.prometheus.simpleclient_spring_boot:0.1.0
B. 编写工具类
import io.prometheus.client.Counter; import io.prometheus.client.Summary; import org.apache.commons.lang.StringUtils; /** * some sample data : * container_console_request_total{request_name=GetCluster,status=success} * container_console_request_total{request_name=GetCluster,status=exception} * container_console_request_total{request_name=GetCluster,status=fail} * author:SUNJINFU * date:2018/8/14 */ public class Monitor { private static final String STATUS_SUCCESS = "success"; private static final String STATUS_FAIL = "fail"; private static final String STATUS_EXCEPTION = "exception"; private static final Summary RequestTimeSummary = Summary.build("container_console_request_milliseconds", "request cost time").labelNames("request_name", "status").register(); private static final Counter RequestTotalCounter = Counter.build("container_console_request_total", "request total").labelNames("request_name", "status").register(); public static void recordSuccess(String labelName, long milliseconds) { recordTimeCost(labelName, STATUS_SUCCESS, milliseconds); } public static void recordSuccess(String labelName) { recordTotal(labelName, STATUS_SUCCESS); } public static void recordException(String labelName, long milliseconds) { recordTimeCost(labelName, STATUS_EXCEPTION, milliseconds); } public static void recordException(String labelName) { recordTotal(labelName, STATUS_EXCEPTION); } public static void recordFail(String labelName, long milliseconds) { recordTimeCost(labelName, STATUS_FAIL, milliseconds); } public static void recordFail(String labelName) { recordTotal(labelName, STATUS_FAIL); } private static void recordTimeCost(String labelName, String status, long milliseconds) { if (StringUtils.isNotEmpty(labelName)) { RequestTimeSummary.labels(labelName.trim(), status).observe(milliseconds); } } private static void recordTotal(String labelName, String status) { if (StringUtils.isNotEmpty(labelName)) { RequestTotalCounter.labels(labelName.trim(), status).inc(); } } }
上面的Summary与Counter区别具体查看prometheus文档,Counter就是用来统计次数的,这个用来统计接口调用次数最合适,Summary除了统计次数,还会统计调用时间。在你的业务代码需要监控的地方调用Monitor中的静态方法来进行数据收集。
try { //业务逻辑 Monitor.recordSuccess("serviceName_" + action, System.currentTimeMillis() - t); } catch (Exception e) { Monitor.recordException("serviceName_" + action, System.currentTimeMillis() - t); }
利用这种静态方法则可以添加到任何想要添加的代码块中,当然如果还需监控系统的http请求与响应,可以通过编写一个interceptor来进行。
@Component public class RequestTimingInterceptor extends HandlerInterceptorAdapter { private static final String REQ_PARAM_TIMING = "http_req_start_time"; private static final Summary responseTimeInMs = Summary.build() .name("container_console_http_response_time_milliseconds") .labelNames("method", "handler", "status") .help("http request completed time in milliseconds") .register(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { request.setAttribute(REQ_PARAM_TIMING, System.currentTimeMillis()); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Long timingAttr = (Long) request.getAttribute(REQ_PARAM_TIMING); long completedTime = System.currentTimeMillis() - timingAttr; String handlerLabel = handler.toString(); if (handler instanceof HandlerMethod) { Method method = ((HandlerMethod) handler).getMethod(); handlerLabel = method.getDeclaringClass().getSimpleName() + "_" + method.getName(); } responseTimeInMs.labels(request.getMethod(), handlerLabel, Integer.toString(response.getStatus())).observe(completedTime); } }
然后把这个拦截器注册到springboot中即可。
C、开启prometheus
在Springboot的启动类上添加注册@EnablePrometheusEndpoint,这样springboot应用就暴露了/prometheus接口,查看源码发现这个接口的实现与spring默认的那些endpoint返回的数据结构不一致,默认的都是返回的json格式,不支持prometheus的采集。同时在springboot的启动配置中添加management.security.enabled=false,不添加这个配置,访问/prometheus可能返回401未授权等相关错误信息。启动应用,触发数据采集的相关埋点,访问应用的/prometheus即可看到采集到的指标。
D、查看采集数据
首先利用prometheus自带的图形观察采集的数据,首先查看springboot埋点的数据。
再看Monitor静态方法埋点的数据。
E、报警设置
prometheus的图形化功能不如grafana出色,现在利用grafana来观察监控数据,同时设置报警规则。实际生产中,应该根据业务的稳定程度等相关指标进行告警设置。这里纯粹演示一个示例。这里以请求Kce_GetClusters为例,查看如下指标container_console_request_milliseconds_count{request_name=’Kce_GetClusters’
从上面可以看出异常与成功的请求次数,这时我们可以针对异常的调用设置告警。切换到Alert面饭,即可进行设置,不能直接通过指标的大小去设置告警,因为它是Counter类型,永远是递增的,一直是一个累计的数字,我们应该以它增长的速率或者增长量作为告警规则。
可以按照上面的规则,2分钟内发生一次即告警,意思就是表达式的值大于0即告警。时间维度,可以自由设置,看业务场景的重要性。