3.2 监控系统的设计逻辑分析

监控系统的实现多种多样,开源监控系统有早些年的Nagios、Zabbix,还有近些年随着容器大火的Prometheus;也有以监控系统作为主要盈利方向的企业会对外输出收费的监控系统,如听云和OneAPM等;企业内部也会自建监控系统,用于满足业务规模化和定制化的监控需求。监控系统作为一个独立的业务领域,有很多业务分支和技术细节,不同监控系统虽然实现思路和业务能力千差万别,但总体会有一些共性问题需要处理,本节将针对监控系统的设计共性问题做分析阐述。

3.2.1 数据生产

监控系统的第一步是获得监控数据,不同监控系统需求的数据生成方式也有所不同。

一种方式是在用户机器上部署Agent,在监控系统动态下发可执行脚本给Agent,再由Agent在用户机器上周期调度脚本生产监控数据。这种方式具有很高的灵活性,采集脚本的逻辑可由用户自由定制,采集的数据内容也可以多种多样。但这种方式对脚本编写者提出了较高要求,脚本编写者需要用脚本语言实现一整套数据采集逻辑,并保证能在目标机器上持续稳定运行。

另一种方式是在用户应用进程里植入 Agent,用于实现应用性能监控。例如,Java语言支持JavaAgent启动参数,使Java应用在执行main方法前,预先执行premain 方法。监控系统就是在premain 方法中预先完成监控注入布局。与此同时,Java语言暴露的Instrumentation对象使Java字节码增强变得容易,监控系统能修改任意class(Java类)的字节码信息,实现数据采集和统计而不被应用维护者感知。JavaAgent能从MBean中获取Java应用执行信息,也能嵌入开源三方库以实现对中间件调用情况的监控,如SQL监控、URL监控、Tomcat监控等。

Agent在监控系统中存在的形式还有很多。例如,嵌入移动端App里的Agent,能实现移动端服务性能和异常情况的监控;嵌入前端页面的Agent,能实现用户浏览器侧的服务质量监控。拨测是另一种Agent监控行为,从位于不同地区、不同网络环境的Agent发起到同一个服务的请求,基于请求结果来判断服务质量。

区别于Agent的另一种数据生产方式是日志采集,日志采集将各端业务日志作为监控数据来源,提取关键信息来实现监控。之所以将日志采集同Agent区分开来,是因为Agent具有一定程度的数据处理能力,能实现解析、过滤、合并等简单逻辑,同时Agent上报的一般是结构化数据。而日志采集为了不丢失细节信息,通常要将符合条件的日志全量上报,日志数据深度处理交由服务端完成。日志采集真正实现了监控逻辑与业务逻辑零耦合、监控执行与业务执行零耦合,业务方不必担心代码被侵入和被修改,也不必担心数据采集逻辑执行会影响业务自身正常功能的执行[2]。但日志采集也存在上报数据冗余度高、服务端处理压力大等难点。

Agent和日志采集两种数据生产方式,以及随后的一整套监控系统之间并不是竞争关系,两种不同的数据生产方式适用于不同的监控场景。Agent数据生产方式的特点是数据信息量大,而日志采集数据生产方式的特点是数据细节丰富。例如,对于CPU负载指标的监控场景,不必使用日志采集的数据生产方式在磁盘的日志文件里流转一遭;而对于业务核心接口调用的监控场景,尤其是有Trace监控需求(调用链监控)的监控场景,使用日志采集数据生产方式会更加合适。此时数据生产和数据上报通过数据落盘的方式解耦,同时磁盘承担了本地消息队列的角色,即使业务请求量过大造成日志数据来不及投递,也不会影响业务自身逻辑执行。而打印日志对业务而言负担较轻,即使不使用日志采集数据生产方式,核心服务调用情况也有必要通过日志的方式记录下来,以便日后排查和审计。

3.2.2 数据上报

监控数据来源于离散的监控源,通常要上报到服务端统一处理,数据上报的过程可以基于以下几个方面分析。

1.上报协议

不同监控系统使用不同的协议上报数据,有些使用的是 HTTP,如Prometheus。HTTP是成熟的网络通信协议,绝大多数高级语言,无论是客户端还是服务端都有基于 HTTP的网络通信封装,因此 HTTP 在通用性和跨语言性上有很大的优势。对Prometheus监控系统而言,数据源通常由用户定义的Exporter实现,因此使用HTTP对Exporter开发者非常友好。但HTTP自身也有缺陷,典型的问题就是连接复用和Header数据冗余,不过这些问题在HTTP 2.0上都做了优化。由于种种原因,到目前为止,虽然内网环境中的HTTP 2.0还没有完全替代HTTP 1.x,但相信HTTP 2.0在未来会成为一套成熟高效的监控数据通信协议。

还有一些监控系统使用基于TCP 的私有协议上报数据,这种方式通用性差一些,但定制化程度高,能在协议层封装监控业务属性,实现更多样化、更高效的数据上报。另外在通用性方面,如果监控数据大多由官方提供的Agent生产,那么协议对接的逻辑就会封装成Agent的一部分直接提供给用户,不会因为私有协议通用性差而对用户接入带来额外负担。

2.推、拉模型

由数据源主动上报的监控数据称为推模型,由服务端定时拉取的监控数据称为拉模型,推、拉模型在监控系统中都有使用到,部分监控系统还同时支持推、拉模型两种方式。推、拉模型的一个重要区别是数据周期采集的驱动端不同,这个区别会对之后一系列监控行为产生影响。

推模型将数据周期性采集的工作放在Agent,这会增加Agent 的开发难度。一方面Agent要能响应服务端下发的采集配置,从而改变采集行为,如调整采集频率、暂停采集动作等;另一方面Agent要具有定时调度能力,确保能在准确的时间点生产目标监控数据。除此之外,Agent 还需要处理调度过程中可能出现的异常,以保障调度服务稳定可靠。

虽然推模型增加了Agent实现复杂度,但推模型也有明显的优势。首先,不同数据源之间可以完全解耦,不会因为一个数据源异常而影响其他数据源的数据采集。其次,推模型能够以较小的代价保障数据采集的时间完全精确,不会加重服务端实现负担。最后,Agent 也可以作为一个数据缓冲池,在服务端无法接纳更多数据时将数据暂存在本地,待服务端可用后再逐步回补,确保数据最终一致。

拉模型由服务端定时调度触发数据周期性采集,调度的复杂性由服务端承担,因此Agent的实现难度会小很多。另外拉模型使得服务端承载的数据压力由服务端自身可控,对某些更注重服务端可用性的监控系统而言,拉模型是很好的选择。

监控系统应该使用推模型还是拉模型并没有标准答案,具体还要结合监控系统的功能定位和应用场景,在必要的情况下,也可以同时支持推模型和拉模型两种方式。

3.2.3 数据处理

监控数据被采集后需要经过处理才能发挥价值。监控系统作为典型的大数据处理系统,通常对数据处理组件的执行效率、可靠性、运维弹性等方面有很高的要求。数据处理是监控系统重要的组成部分,以下内容将针对监控系统数据处理的几个重要概念进行阐述和分析。

1.流处理

监控系统对数据处理有很高的时效性要求,当线上出现故障时,要在一定的时间范围内发现故障并通知用户才有意义。这类需求场景通常使用实时流处理解决方案,可以使用流处理引擎,如Flink、Storm等,也可以基于实际需求独立编写数据处理服务。使用开源的流处理引擎能够获得强大的技术支撑,在服务弹性、稳定性、容错性等方面都有很好的表现,但独立编写的数据处理服务灵活度更高,运维也相对简单。

为了使流处理更加高效,我们通常希望待处理的数据是无状态的。无状态的数据一方面更容易做负载均衡,另一方面也不会对流处理造成额外负担。但完全无状态的数据在实际场景下难以实现,由于监控系统有各种各样的业务需求,因此数据在处理过程中需要附着各种状态。例如,单机的数据要累积成集群聚合的结果,这就要求每笔监控数据都具有集群状态。另外还有区间报警的需求,把一段时间的多笔数据累积起来驱动报警,这就使得一笔数据同历史的几笔数据产生了关联,数据也就具有了时间状态。

针对数据有状态的场景,要评估使用流处理数据状态的代价,必要时可以考虑用离线的方式来满足状态处理要求。

2.数据热点

监控系统往往要对接各种各样的数据源,不同数据源上报的数据内容千差万别,在一些极端场景下,单个数据源上报的数据包大小是平均数据包大小的上千倍。与此同时,由于监控数据存在状态,不得已要同时处理多个大数据包,因此就带来了数据热点问题。数据热点问题对监控系统而言非常棘手,出现热点后,系统难以通过横向扩容的方式化解热点。在极端情况下,热点数据会压垮消费节点,消费节点转移后会继续压垮下一个消费节点,最终产生“雪崩效应”,拖垮整个消费集群。

解决数据热点问题的第一步是分析热点成因,具体原因通常是数据包过大或数据有状态。对于数据包过大的情况,可以考虑在监控系统允许的范围内做数据裁剪或数据分片。对于数据有状态的情况,可以考虑在不影响数据状态的前提下分包处理,或者改变有状态热点数据的处理方式,如将实时处理改为离线处理。

对于可能存在数据热点的监控系统,有必要做好热点数据自监控,在系统出现热点时能快速定位问题。也可以在数据处理的关键节点接入配置下发服务,以便在线实时处理数据热点。

3.2.4 数据存储

监控系统在数据处理后,通常会将数据落库,用于日后分析查询。监控系统处理的数据绝大多数都带有时间属性,因此使用 TSDB(时间序列数据库)存储较为合适,这类数据存储服务会针对时序无尽数据输入场景做针对性设计和优化,以提供极高的写入吞吐和高效的数据检索能力。

1.HBase

一种可用的数据存储方案是HBase。HBase是Apache开源项目,是基于Google BigTable实现的一套分布式、大数据存储服务。虽然HBase并不是典型的TSDB,但在RowKey设计时可以将时间戳包含进去,以达到基于时间快速检索的目的。使用HBase的关键点是RowKey的设计,合理的RowKey设计既能使数据流在HBase集群多个服务节点之间均匀写入,又能在查询时实现并发、高效的数据读取。

RowKey 的具体设计要基于实际业务数据写入情况和查询需求,没有统一标准,此处提供一种RowKey的设计思路。首先,收集需要存储的数据源,梳理数据源的各类维度信息,其次,基于可预期的数据读取方式对维度信息的优先级做排序,最后,将维度信息组装成RowKey来标记唯一一个数据源。以上方式仅作参考,RowKey的具体设计请结合实际监控需求。

HBase数据写入LSM树(Log-Structured Merge Tree)和WAL(Write-Ahead Logging),具有非常不错的写入吞吐。虽然在不同场景下的写入情况有所区别,但通常单个HBase服务节点的写入TPS能达到数万甚至更高。

LSM树的重要思想是将数据写入操作和有状态持久化操作分离(有状态持久化操作的目的是利于数据检索)。一次数据写入只要在内存里被记录下来并写入 WAL 日志就算成功,因此写入操作的开销并不大,在一些特殊场景下甚至会关闭 WAL 日志写入来进一步提升写入效率。随着内存中积累的数据逐渐增多,可以一次性将内存中的数据刷新到磁盘中形成HFile文件。随着数据的持续涌入,HFile文件不断增加,会有另一个任务对多个HFile文件做合并,称为Compact(合并HFile文件能优化数据读操作)。

HBase 作为监控系统的数据存储也存在一些问题。例如,HBase对索引的支持度不高,勉强能被当作索引的是RowKey。对HBase的数据搜索要基于一个固定的RowKey设计展开,任何超出RowKey设计的搜索需求,均难以保证高效索引甚至不能支持索引。为了增加HBase索引的灵活性,通常会在HBase上再搭建一套二级索引服务来弥补HBase索引的不足,但这样会加重系统复杂度和运维负担。

HBase 不是完全意义上的按列存储,HBase 把数据记录在 HFile 文件的Data Block里,每个Data Block都有一组KV按字典序排列组成。HBase KV存储结构如图3-2所示,展示了一个KV数据在HFile文件里的存储内存。

图3-2 HBase KV存储结构

如图3-2所示,Key=Row+Column Family+Column Qualifier+Time Stamp+Key Type,其中Column Family(CF)是列族,不同列族的数据在物理上隔离,但HBase官方出于性能考虑不建议使用多个Column Family,通常只使用一个。Column Qualifier(CQ)是列名,Time Stamp是时间戳,用来区分版本。监控系统通常将数据的时间属性附着在Row上,在Key使用字典序的情况下,同时间点的多列数据连续存储,而一列数据在不同时间点的数据实际是离散的。HBase KV分布如图3-3所示。

图3-3 HBase KV分布

如图3-3所示,如果一次请求需要获取CQ1在连续时间段的数据,就不得不扫描整个V1到V7的数据集,但实际需要的只是V1、V4和V6。这样的存储结构,需要对监控系统展示一个指标、一段时间数据走势的请求还有额外的性能开销。

总体而言,HBase在监控系统数据存储领域使用广泛。

2.InfluxDB

另一种可用的数据存储方案是InfluxDB。InfluxDB数据写入基于TSM、LSM 树的变种,同样有很强的数据写入吞吐。InfluxDB 是典型意义上的TSDB,InfluxDB 写入的数据默认具有时间属性,且时间属性会贯穿在数据操作的各个阶段。InfluxDB使用倒排索引的方式搭建索引结构,可以基于多种主键组合灵活的索引数据。此外,InfluxDB 还直接对外提供 SQL 的查询API,很多查询的业务运算逻辑都能通过SQL实现。InfluxDB的运维和部署也相对简单,不依赖Hadoop或HDFS等其他服务。只有单机版的InfluxDB是开源免费的,集群版的InfluxDB要付费才能使用。

当然数据存储也不一定局限于 TSDB,在监控数据量不大的情况下,出于技术通用性和运维复杂性等方面的考虑,也可以使用MySQL等存储引擎来实现数据存储。

3.2.5 数据使用

从各类数据源和各种网络环境中收集监控数据,耗费了大量资源做计算和存储,最终要有效使用才能体现其价值。针对监控数据的使用方式有很多探索,以下几个方向可供参考。

1.监控数据展示

监控数据展示是监控数据使用的一个方向,用户通过查询历史监控结果来定位问题,调优系统。

不同类型的监控数据可以有不同的展示方式,监控数据展示常用的视图类型有趋势图、表格、柱状图、地图、散点图等。关注变化的监控数据通常由趋势图来展示,其中x 轴是时间,y 轴是数值,通过趋势图能快速分辨出时间区间内重要指标的变化情况。当趋势图中存在多条曲线时,还能形成明显的对比效果。例如,将同个指标不同时间区间的两条曲线放在一起,能实现监控数据的同环比对比;将同个指标不同机器的两条曲线放在一起,能实现同服务下不同机器间的负载对比;将一个数据源的不同指标放在一起,能实现多指标相关性对比。

统计类的监控数据通常用表格来展示,如机器的磁盘使用情况的监控数据用表格展示,表头是盘符、使用率等,表项是不同盘符的具体使用情况。表格可以细分为区间统计类表格和最近一笔监控数据表格。区间统计类表格会将一段时间的监控数据汇聚在一起,统一展示在一个表格中,如应用的方法调用情况,通常关注的是一段时间内的请求次数、错误次数、耗时等。最近一笔监控数据表格展示的是最近一笔监控数据,这种监控源对当前所处的状态尤为敏感,历史监控数据通常只用于故障排查。例如,服务的版本信息,通常关注的是最近一次获取到的监控数据版本。磁盘利用率统计如图3-4所示。

图3-4 磁盘利用率统计

地域性较强的监控数据,用地图类视图展示效果很好,虽然信息量比表格类视图少,但会更加直观。能使用地图类视图展示的监控数据,要求具有与地域相关的标,可以是省市信息,也可以是各端IP地址(能转化为地域信息)。地图类视图在移动端监控场景下使用频繁,有助于感知区域性服务质量优劣、发现区域性异常和故障。

针对这类问题的监控系统,通常需要多张视图从多个维度来展示。例如,对CPU负载的监控系统,可以搭配一张Load趋势图、一张单核利用率表格、一张上下文切换次数趋势图。通用型监控系统要有视图自由搭配和组织的能力,能够让用户表达不同业务的视图编排需求。但对于典型场景,可以考虑提供通用视图搭配,从而减少用户接入监控系统的成本,同时提供专业性更强的监控体验。

监控数据展示的另一个方向是智能视图。对于复杂业务的线上监控场景,运维工程师往往要从大量监控视图中寻找问题的蛛丝马迹,智能视图的其中一个目的就是辅助用户从视图中快速获取关键信息。一种简单的方式是将触发报警的数据点在监控视图上高亮,以提示用户相关数据存在报警,需要重点关注;另一种方式是智能识别视图中存在的异常现象,如陡升陡降、同环比异常等,高亮这部分数据以提示用户;还可以做关联视图分析,基于数据或业务相关性,自动输出一组关联视图供用户查询,典型的可以形成业务异常和硬件负载的关联、虚拟主机和宿主机的关联、应用和中间件的关联、RPC服务调用链之间的关联等。

2.监控数据报警

监控数据报警是监控系统的重要功能,是大多用户接入监控服务的根本诉求。做一个简单的报警服务很容易,只要对每笔监控数据做报警判断,触发报警后发送报警消息给用户即可。但简单的报警服务在生产环境中的表现往往不尽人意,这是因为在报警服务设计上,通常会面临以下几点问题。

(1)多指标和单指标。

监控数据处理有多指标和单指标两种思路,多指标的最小单位是一个具体的监控源问题域,如针对 Java 方法监控,不仅要监控方法的正确调用次数,还要监控方法的错误调用次数、调用耗时等。正确调用次数和错误调用次数是Java方法执行状态问题域下的两个不同指标。多指标支持使用正确和错误的两个指标共同驱动报警;而单指标则认为每个指标都是离散的,监控系统不需要感知指标的具体含义,将每个指标统一作为离散数据流处理。

多指标的报警服务相比单指标的监控服务而言,通常会提供更丰富的能力。例如,用户需要对方法调用的错误率情况做监控,在一个时间窗口内,应用中的多个方法分别有自己的正确调用次数和错误调用次数。一种报警配置方式是针对每个方法配置不同的报警阈值,但这种报警配置方式复杂、运维成本高,在生产环境中很少会使用这种报警配置方式(部分应用会存在上千个方法需要监控)。有一种折中的办法是区分核心方法和非核心方法,核心方法是精细化管理、独立配置的,非核心方法是统一管理、统一配置的。统一配置的多个方法之间,存在方法调用量不均匀的情况,部分方法调用次数很少,因此错误率抖动很大,如方法调用了3次,只要有一次错误,错误率就是33%。在很多情况下,调用错误次数很少的方法,业务方通常不需要关心。因此,为了避免小调用量方法频繁触发错误率报警,通常需要结合调用次数再做出判断。只有当错误率和调用次数同时超过一定阈值时,才会触发报警,这种报警配置方式能减少复杂数据源适配通用报警规则时的误报情况,使用多指标实现这种报警配置方式的代价很小,但使用单指标要麻烦得多。

多指标也有自己的困扰,一方面数据处理的灵活性降低,可能存在潜在的热点问题;另一方面业务需要主动对问题域下的多指标分类,从而驱动监控系统有计划地管理多指标数据。另外多指标在部分问题场景下对数据采集和数据上报都提出了更高的要求。

(2)实时报警和离线报警。

实时报警和离线报警是比较常见的两种报警思路。所谓实时报警,是指在数据生产、收集和处理的链路中完成报警判断。监控数据在内存中流转时就能判断是否触发报警,这种实现方式代价小、性能高,适用于简单的报警场景。

但实时报警的一个典型问题是报警能力不足。由于监控系统往往要处理海量监控数据,因此通常以流处理的方式来应对实时报警。为了增加流处理的并发度和处理开销,实时报警通常希望待处理的监控数据是无状态的,但复杂场景需要当前的监控数据同其他维度的监控数据产生关联,共同驱动报警。

例如,有这样一种报警需求,当前监控数据超出最近5笔历史监控数据均值的30%则触发报警。这是一种通用性的无阈值陡升报警规则,该需求要实现实时报警难度很高,一方面要缓存最近5笔历史监控数据用于当前监控数据的报警判断,另一方面还要求同一个监控源的监控数据发往同一个处理节点,这样才能使用该处理节点本地缓存的历史监控数据。历史监控数据缓存在Redis等共享内存中能解决该问题,但这无形之间为实时监控数据处理增加了历史监控数据网络流转的开销,拖慢了监控数据处理速度,并且会让中间件承担更大的压力。对于历史监控数据缓存在本地,且当前监控数据具有节点状态的情况,应用运维难度会变大。任何可能造成监控数据路由规则变化的行为,如发布等,都会造成报警误报;为了弥补这一点,还要引入更复杂的设计和更高的性能开销。

所谓离线报警,是指将监控数据处理和报警功能通过存储服务解耦。监控数据处理只负责处理监控数据并录入存储引擎,而报警服务则周期性地从存储引擎中获取监控数据来判断是否满足报警条件。由于监控数据来源于存储引擎,因此对监控数据时效性要求较低,上文提到的无阈值陡升报警需求,在离线报警场景下就能很轻松实现。除此之外,存储引擎或开源适配服务还会提供SQL数据扫描能力。借助SQL语法,用户表达复杂报警需求的配置方式变得简单,且由于SQL语法的通用性,上手难度也不会太大。

但离线报警的劣势和优势一样明显。一方面离线报警会加重存储引擎负担,随着报警规则逐渐增多,存储引擎的搜索负载也呈线性增长。尤其是部分极端规则扫描数据区间大,扫描时间范围长[3],会进一步加重存储引擎负担。另一方面离线报警使得报警服务的依赖链被延长,报警服务的强可用性依赖处理服务和存储引擎的可用性,一旦存储出现故障,报警服务也会被影响。相较而言,实时报警就没有这种担心,即使数据不存储,报警服务依然可以正常工作。

实时报警和离线报警并非互斥,两者在功能上互补,在应用场景上相对独立,理想的监控系统可以同时具有实时报警和离线报警两种能力。实时报警主要应对大量系统级和部分业务级的监控数据报警,这类监控数据基数大,报警复杂度低,重要性也相对偏弱。离线报警主要用于核心业务监控,这类监控重要性强,与业务关系紧密,存在周期性和规律性,报警复杂度高。

3.对外输出

监控数据作为运维基础,积累的监控结果不仅能给监控系统使用,还能对外输出支撑其他运维产品。监控数据暴露给DBA,能构建慢SQL发现和治理系统;监控数据暴露给发布系统,能提供应用自动扩缩容功能;监控数据暴露给运维管理平台,能实现业务容量周期巡检。

监控数据对外输出的形式可以是 HTTP 调用,也可以是 RPC 调用。HTTP调用有很强的通用能力和跨语言能力,而RPC调用效率高、编码方式更友好。无论是HTTP调用还是RPC调用,都需要考虑访问权限和限流的问题。在监控系统中,部分业务数据敏感度高,不应开放给未授权的用户。限流是对监控系统的保护,要避免极端用户频繁刷新数据拉取接口,造成监控系统压力增加,甚至影响监控系统的正常运行。

监控数据的使用方需要充分考虑监控系统的可靠性,需要做好在极端情况下的功能容错。监控系统要直面数据处理压力,在服务出现异常时可能存在数据丢失、存储降级等情况,若此时使用监控数据的平台,尤其是利用监控数据执行重要自动化操作的平台时,一定要兼顾各类极端情况,确保在监控系统异常时,不会因为平台自身不健壮而对平台造成二次伤害。