这一讲我们来剖析一下常见的采集数据的技术手段,了解了这些,我们就可以写自己的采集器,或者扩展很多采集器的能力了,对很多监控数据也会拥有原理层面的理解。
目录/proc 是一个位于内存中的伪文件系统,该目录下保存的不是真正的文件和目录,而是一些“运行时”信息,Linux 操作系统层面的很多监控数据,比如内存数据、网卡流量、机器负载等,都是从 /proc 中获取的信息。
内存总量、剩余量、可用量、Buffer、Cached 等数据都可以轻易拿到。当然,/proc/meminfo 没有使用率、可用率这样的百分比指标,这类指标需要二次计算,可以在客户端采集器中完成,也可以在服务端查询时现算。内存相关的指标都是 Gauge 类型的,下面我们再来看一下网卡流量相关的指标,网卡相关的数据都是 Counter 类型的数据。
这种方式非常简单,就是调用一下系统命令,解析输出就可以了。比如我们想获取 9090 端口的监听状态,可以使用 ss 命令 ss -tln|grep 9090,想要拿到各个分区的使用率可以通过 df 命令 df -k。但是这个方式不太通用,性能也不好。
先说通用性问题,就拿 ss 命令来说吧,不是所有的机器都安装了这个命令行工具,而且不同的发行版或不同 ss 版本,命令输出内容格式可能不同。
性能问题也容易理解,调用命令行工具是需要 fork 一个进程的,相比于进程内的逻辑,效率大打折扣,不过监控采集频率一般都是 10 秒起步,不频繁,所以这个性能问题倒不是什么大事,关键还是通用性问题。读取本地 /proc 目录或执行命令行工具,都是在目标监控机器上进行的操作。有的时候,我们无法在目标机器上部署客户端程序,这时候就需要黑盒探测手段了。
典型的探测手段有三类,ICMP、TCP 和 HTTP。有一个软件叫 Blackbox Exporter,就是专门用来探测的,Categraf、Datadog-Agent 等采集器也都可以做这种探测。
ICMP 协议,我们可以通过 Ping 工具做测试。
监控采集器和手工 Ping 测试的原理是一样的,也是发几个包做统计。不过有些机器是禁 Ping 的,这时候我们就可以通过 TCP 或 HTTP 来探测。对于 Linux 机器,一般是会开放 sshd 的 22 端口,那我们就可以用类似 telnet 的方式探测机器的 22 端口,如果成功就认为机器存活。
对于 HTTP 协议的探测,除了基本的连通性测试,还可以检查协议内容,比如要求返回的 status code 必须是 200,返回的 response body 必须包含 success 字符串,如果任何一个条件没有满足,从监控的角度就认为是异常的。有黑盒监控,自然就有白盒监控。
黑盒监控是把监控对象当成一个黑盒子,不去了解其内部运行机理,只是通过几种协议做简单探测。白盒监控与之相反,它要收集能够反映监控对象内部运行健康度的指标。但是监控对象的内部指标,从外部其实是无法拿到的,所以白盒监控的指标,需要监控对象自身想办法暴露出来。最典型的暴露方式,就是提供一个 HTTP 接口,在 response body 中返回监控指标的数据。
有很多组件都通过 HTTP 接口的方式,暴露了自身的监控指标。这里我给你举几个例子,让你有个感性的认识,比如 Elasticsearch 的 /_cluster/health 接口。
我们可以看到,返回的内容大多是指标数值,转换成监控服务端要求的数据格式,传上去即可。
除了 /_cluster/health ,还可以测试一下 /_cluster/stats,也能帮你了解这种获取指标的方式。除了 Elasticsearch,还有很多其他组件也是用这种方式来暴露指标的,比如 RabbitMQ,访问 /api/overview 可以拿到 Message 数量、Connection 数量等概要信息。再比如 Kubelet,访问 /stats/summary 可以拿到 Node 和 Pod 等很多概要信息。
不同的接口返回的内容虽然都是指标数据,但是要推给监控服务端,还是要做一次格式转换,比如统一转换为 Prometheus 的文本格式。要是这些组件都直接暴露 Prometheus 的协议数据就好了,使用统一的解析器,就能大大简化监控采集逻辑。所幸这个趋势正在发生,上一讲我们也提到了,像 etcd、CoreDNS、新版 ZooKeeper、新版 RabbitMQ、nginx-vts 等,都内置暴露了 Prometheus 协议数据,可谓行业幸事。这种拉取监控数据的方式虽然需要做一些数据格式的转换,但并不复杂。
因为目标对象会把需要监控的数据直接通过接口暴露出来,监控采集器把数据拉到本地做格式转换即可。更复杂的方式是需要我们连接到目标对象上执行指令,MySQL、Redis、MongoDB 等都是这种方式,下面我们就来一起看一下这种采集方式的工作原理。
目前最常用的数据库就是 MySQL 和 Redis 了,我们就拿这两个组件来举例。先说 MySQL,我们经常需要获取一些连接相关的指标数据,比如当前有多少连接,总共拒绝了多少连接,总共接收过多少连接,登录 MySQL 命令行,使用下面的命令可以获取。
mysql> show global status like '%onn%';
Threads_connected 表示当前有多少连接,Max_used_connections 表示曾经最多有多少连接,Connections 表示总计接收过多少连接。当然,除了连接数相关的指标,通过 show global status 还可以获取很多其他的指标,这些指标用于表示 MySQL 的运行状态,随着实例运行,这些数据会动态变化。
还有另一个命令 show global variables 可以获取一些全局变量信息,比如使用下面的命令可以获取 MySQL 最大连接数。
mysql> show global variables like '%onn%';
其中 max_connections 就是最大连接数,这个数值默认是 151。在很多生产环境下,都应该调大,所以我们要把这个指标作为一个告警规则监控起来,如果发现这个数值太小要及时告警。当然,除了刚才介绍的两个命令,我们还可以执行其他命令获取其他数据库指标,比如 show slave status 可以获取 Slave 节点的信息。
总的来看,MySQL 监控的原理就是,连上 MySQL 后执行各种 SQL 语句,解析结果,转换为监控时序数据。上面例子中的 SQL 语句都是用来获取 MySQL 实例运行状态的。实际上,既然可以执行 SQL 语句,我们就可以自定义一些 SQL 来查询业务数据,获取业务指标。上一讲我们也提到过,业务指标数据是整个监控体系中价值最大的,我们要好好利用这个能力。
有些业务数据可能是存在 Redis 里的,所以监控 Redis 不只是获取 Redis 自身的指标,还应该支持自定义命令,获取一些业务数据,比如下面的命令,用于获取订单总量。
get /orders/total
接下来就是应用监控和业务监控,应用监控和业务监控有两种典型的采集手段,一个是埋点,一个是日志解析。
所谓的代码埋点方式,是指应用程序内嵌一些监控相关的 SDK,在请求的关键链路上调用 SDK 的方法,告诉 SDK 当前是个什么请求、耗时多少、是否成功之类的,SDK 汇总这些数据并二次计算,最终推给监控服务端。
比如一个用 Go 写的 Web 程序,提供了 10 个 HTTP 接口,我们想获取这 10 个接口的成功率和延迟数据,那就要写程序实现这些逻辑,包括数据采集、统计、转发给服务端等。这些监控相关的逻辑是典型的横向需求,这个 Web 程序有需求,其他的程序也有这个需求,所以就把这部分代码抽象成一个统一的 Lib,即上面提到的这个 SDK,每个需要监控逻辑的程序都可以复用这个 SDK,提升效率。
但是每个项目都要调用这个 SDK 的方法仍然显得很冗余,是否有更简单的办法呢?有!每个公司可以建立统一的框架开发团队,开发统一的 HTTP 框架,在框架里使用 AOP 的编程方式,内置采集这些监控数据。
这样一来,只要某个团队用了这个统一的 HTTP 框架,就自动具备了监控埋点能力。同理,我们也可以构建统一的 RPC 调用框架,统一的 MySQL 调用框架,这样就逐步构建了统一且完备的应用监控数据收集体系。这种方式需要内嵌 SDK,对代码有侵入性,是否有更简单的办法呢?有!对于 Java 这种字节码语言,我们可以使用 JavaAgent 技术,动态修改字节码,自动获取监控数据,到 Google 上通过关键词“javaagent monitoring”可以找到很多资料。
当然,现在也开始流行 eBPF 技术,有些厂商在尝试,你也可以关注下。不过在生产实践中,我观察大部分厂商还是在用埋点的方式采集监控数据。对于自研的程序,代码埋点是没问题的,但是很多程序可能是外采的,我们没法修改它的源代码,这时候就要使用日志解析的方式了。一般程序都会打印日志,我们可以写日志解析程序,从日志中提取一些关键信息,比如从业务日志中很容易拿到 Exception 关键字出现的次数,从接入层日志中很容易就能拿到某个接口的访问次数。