监控的两要素:

  1. 查看系统当前(历史)的运行情况
  2. 系统异常时给出告警

通过两要素可使系统具备可观测性,并及时反馈系统异常。监控的目的是为了达到性能目标和维稳。

目标

监控是一种手段,目标才是目的。上监控之前要清晰的了解,系统应该达成哪些目标,稳定性应达成哪些目标。对于目标按层次分成3类:

  • 业务目标

    业务目标,是最高层次的目标,如PV, UV,在线用户数,乃至日营收等,其关乎产品是否有价值,是否符合团队预期。业务目标总是伴随着一些限定条件,比如时间点、投入人力、投入机器资源等。业务指标应该是项目、产品、开发负责人一起制定出来的。

  • 性能目标

    性能指标,服务于业务指标,如接口耗时,接口成功率,页面加载耗时等。性能指标直接关乎产品是否好用,主要应该由开发和测试制定目标,然后由开发负责。 性能指标往往会有取舍,比如延迟低但吞吐量大,入库慢但查询快,需结合场景去权衡。性能指标也要限制条件,主要是硬件资源、并发量、数据量,没有限定条件的指标是没有意义的。对于耗时,除了平均,业界往往用百分位(tp95, tp99)来观察抖动情况。

  • 稳定性目标

    稳定性同样服务于业务指标,性能很好,但服务长期不可用是没有意义的。系统整体的服务稳定性(SLA)、机器的负载,磁盘的剩余容量都是稳定性需要关注的。稳定性应该是开发和运维一起制定和负责的。

    稳定性指标需要防患未然,如磁盘占用空间,磁盘的使用寿命,在当前时刻不会影响系统性能,但将来的某个时刻有概率会出问题,这个是需要关心的。

Prometheus

Prometheus是一种工具,可以用来监控以上三种目标。

部署

在k8s中,使用helm安装,我们用的是loki-stack, 集成了prometheus、grafana、loki:

1
 helm upgrade --install loki --namespace=loki loki-stack   --set grafana.enabled=true,prometheus.enabled=true

架构

prometheus

metrics

prometheus的核心概念是metrics(指标)。一个metrics一般对应一项真实世界的指标,比如

1
  http_request_total{method="POST", code=404} 

表示http的请求数:http_request_total代表指标的名字,method和code表示指标下面有2个标签,标签可以用来做过滤。

metrics有四种类型:

  1. Gauge 瞬时值,前后数值没有关系,一般用来表达CPU占用率,内存占用率、网速等瞬时状态。
1
2
3
   # HELP go_goroutines Number of goroutines that currently exist.
   # TYPE go_goroutines gauge
   go_goroutines 73
  1. Counter 计数器,总是递增的。一般以total后缀命名。可以结合prometheus的rate, increase函数计算QPS, 区间增量。
1
2
3
   # TYPE http_requests_total_count counter
   # HELP http_requests_total_count A counter of the total number of external HTTP requests.
   http_request_total 3.7156890216e+10

注:counter逻辑上是递增的,但服务的重启会导致counter归零,这个会导致counter的绝对值下降,但rate等函数会处理好这种情况,所以一般无需关心。

  1. Summary 一般用来表示分位值。由Client SDK计算。比如计算接口耗时的TP95。
1
2
3
4
5
6
7
8
9
   # HELP go_gc_duration_seconds A summary of the GC invocation durations.
   # TYPE go_gc_duration_seconds summary
   go_gc_duration_seconds{quantile="0"} 3.291e-05
   go_gc_duration_seconds{quantile="0.25"} 4.3849e-05
   go_gc_duration_seconds{quantile="0.5"} 6.2452e-05
   go_gc_duration_seconds{quantile="0.75"} 9.8154e-05
   go_gc_duration_seconds{quantile="1"} 0.011689149
   go_gc_duration_seconds_sum 3.451780079
   go_gc_duration_seconds_count 13118
  1. Histogram 和summary类似,可以表示百分位。但由Prometheus Server端计算,CLient端只做分桶计数。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
   # HELP http_request_duration_seconds request duration histogram
   # TYPE http_request_duration_seconds histogram
   http_request_duration_seconds_bucket{le="0.5"} 0
   http_request_duration_seconds_bucket{le="1"} 1
   http_request_duration_seconds_bucket{le="2"} 2
   http_request_duration_seconds_bucket{le="3"} 3
   http_request_duration_seconds_bucket{le="5"} 3
   http_request_duration_seconds_bucket{le="+Inf"} 3
   http_request_duration_seconds_sum 6
   http_request_duration_seconds_count 3

exporter

exporter会暴露一个Http的接口,供Prometheus来采集Metrics。可以:

  1. 业务服务集成Prometheus Sdk, 直接收集和暴露Metrics接口。适合我们自己编写的服务。golang建议使用go-metrics这个sdk
  2. 编写独立的服务,专门用来收集指标和暴露Metrics接口。适合对第三方服务开发exporter。业界也有大量的exporter供我们使用:mysql-exporter,node-exporter等。

在exporter的deployment中配置pull的开关和端口, 配置完成后prometheus server会定时来拉取metrics。

1
2
3
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '9090'

除了exporter,还可以通过将metrics委托给push-gataway来收集,push-gateway自身是一个exporter,适用于启动时间端的服务(比如job)。

如果exporter不属于k8s的pod, 需要配置prometheus的scrape_configs,具体可以参考这里

promql

可以在prometheus的web页面进行简单的metrics查询。

prometheus

samples

1
2
3
4
5
http_requests_total  # 查询http_requests_total的数据
http_requests_total{job="prometheus"} # 过滤job=prometheus
http_requests_total{job="prometheus"}[5m] # 查询最近5分钟,job=prometheus的数据
rate(http_requests_total{job="prometheus"}[5m]) # 查询最近5分钟,job=prometheus的QPS数据
increase(http_requests_total{job="prometheus"}[5m]) # 查询最近5分钟,job=prometheus的增量值

Grafana

grafana是最终展示prometheus metrics报表的地方。简单来说分2步:

  1. 添加prometheus数据源
  2. 配置dashboard 配置dashboard的过程即是编写promql的过程。

dashboard库中可以找到一些现成的dashboard。

dashboard的导入导出

我们grafana的dashboard一般是在公司的开发环境配置好的,但生产环境是在客户的私有网络,如何同步? 官方只支持单个dashboard都导入和导出。最后我们采用dump&import db文件(sqlite)的方案。

  1. 拷贝开发环境的grafana db文件。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
   #!/bin/bash

   export KUBECONFIG=/home/dev/k8s.conf

   pod=$(kubectl get pod -n loki | grep loki-grafana | awk '{print $1}')

   echo "grafana pod=$pod"

   rm -f ./grafana.db
   kubectl -n loki  cp "$pod:/var/lib/grafana/grafana.db" ./grafana.db
  1. 过滤开发环境的用户数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import sqlite3

conn = sqlite3.connect("./grafana.db")
file = open("./grafana.sql", "w")


blacklists = ["annotation", "alert_notification"]
blacklists = ["INSERT INTO \"{}\"".format(bl) for bl in blacklists]

print("blacklists: ", blacklists)

create_cmds = ["CREATE INDEX", "CREATE UNIQUE INDEX", "CREATE TABLE"]
insert_cmd = "INSERT INTO"

lines = []
for line in conn.iterdump():
    blacklist = False
    for bl in blacklists:
        if line.startswith(bl):
            blacklist = True
            break
        else:
            pass

    if not blacklist:
        for create_cmd in create_cmds:
            if line.startswith(create_cmd):
                line = line.replace(create_cmd, create_cmd + " IF NOT EXISTS ")
                break
        if line.startswith(insert_cmd):
            line = line.replace(insert_cmd, "INSERT OR REPLACE INTO")
        lines.append(line)

if len(lines) < 10:
    print("error sql lines")
file.writelines(lines)
  1. 导入现场

Alerts

这里其实有两个Alert, 一个是Prometheus的Alert,一个是Grafana的Alert。我们这里采用的是Prometheus的Alert,其更灵活和强大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  # 示例1
  - alert: KubeControllerManagerDown
    annotations:
      message: KubeControllerManager has disappeared from Prometheus target discovery.
    expr: |
      absent(up{job="kube-controller-manager"} == 1)
    for: 15m
    labels:
      severity: critical

  # 示例2
  - alert: KubePodNotReady
    annotations:
      message: Pod {{ $labels.namespace }}/{{ $labels.pod }} has been in a non-ready
        state for longer than an hour.
    expr: |
      sum by (namespace, pod) (kube_pod_status_phase{app_kubernetes_io_name="kube-state-metrics", phase=~"Pending|Unknown"}) > 0
    for: 1h
    labels:
      severity: critical

上面是2个alert规则的示例:

  • alert名字:KubeControllerManagerDown
  • annotations:alert自定义的数据,这里面我们添加了一个message, 表示alert的文本描述。
  • expr: 触发alert点表达式。由promql语句组成,会有一个比较条件,最终表达式返回的是bool类型。
  • for: 持续的时间,大于则触发alert
  • labels: 标签数据,和annotations的区别是labels会基于metrics自动填充,alert配置时可以覆盖值。

pormetheus执行alert rule的检测,一旦有新的alert, 则会推送给alertmanager, alertmanager可以:

  1. web页面可以查看alert, 临时屏蔽alert的推送
  2. 配置webhook, 将alert发给下游,比如钉钉
1
2
3
4
5
6
7
8
9
 receivers:
  - name: webhook_all
    webhook_configs:
    - url: http://10.1.0.200:8060/dingtalk/webhook_default/send
      send_resolved: false
  - name: mention_dev1
    webhook_configs:
    - url: http://10.0.0.200:8060/dingtalk/mention_dev1/send
      send_resolved: false
  1. 配置route, 将不同的alert推送给不同的receiver, 实现alert@到人的效果。
1
2
3
4
5
6
7
 route:
  group_wait: 30s
  group_interval: 5m
  receiver: webhook_all
  - match_re: 
      alertname: .*空间不足
      receiver: mention_dev1  # 如果是空间不足相关的报警,则通知dev1

钉钉

prometheus-webhook-dingtalk可以适配alertmanager, 将alert推送给钉钉。

参考:

  1. Prometheus的方法
  2. Promeetheus的counter是如何工作的
  3. GRAFANA SUPPORT FOR PROMETHEUS