在本文中,我们将探讨可扩展的指标监控和告警系统的设计。理解基础设施的状况对维持其可用性和可靠性至关重要。
图-1展示了一些市面上最流行的商用和开源的指标监控和告警服务。
图-1
1.场景边界界定
为了便于展开设计以及考虑通用性,监控和告警需求如下:(1)流量大。
•1亿日活用户。
•1000个服务器池,每个池有100台机器,每台机器上有100个指标,总共有1000万个指标。
•数据保留12个月。
•假设每天触发50000个告警。
(2)可以监控一系列指标,包括但不限于:
•CPU使用率。
•请求数量。
•内存使用率。
•异常数量。
•消息队列里的消息数量。
(3)系统应该是可扩展的,以便容纳更多的指标、告警等。
(4)系统应该高度可靠,以避免错失关键告警。
(5)值班开发者应该可以快速接收到告警,从而及时开展调查。
2.高层级设计
在本节中,我们会讨论系统的基本原理、数据模型和高层级设计。2.1 基本原理指标
监控和告警系统通常包含5个组成部分,如图-2所示。•数据收集:从不同来源收集指标数据。
•数据传输:把数据从源头传输到指标监控系统。
•数据存储:组织和存储传入的数据。
•告警:引起工程师的注意,使其调查问题或者触发自动修复。告警系统必须能将通知发到不同的通信渠道。
•可视化:通过图、表等来展示数据。一图胜千言。当数据以可视化方式展示时,我们更容易发现模式、趋势或者问题。
图-2
2.2 数据模型指标
数据通常以时间序列(Time-series)的格式记录,它展示了数据随着时间的变化。这里有两个例子。例1:生产环境的服务器实例i631在20:00的CPU负载是怎样的(见图-3)
图-3
例2:所有Web服务器在最近的10分钟内的平均CPU负载是多少?在存储中,我们可能有如下的数据。
每个时间序列包括如表-1所列的数据指标描述:
数据访问模式
在图-4中,y轴代表时间序列名字和标签的组合,x轴表示这个时间序列被记录时的时间点。如你所见,有许多时间序列在同一时间点被记录,1.1节提到,每天大概有1000万个运行指标被写入,所以毋庸置疑这个流量是重写入(Write-heavy)的。同时,查询通常查的是某一个时间区间的多个时间序列,例如图-4中的高亮部分。如果你想查询一个服务池(Service Pool)中所有机器的报错数据,你就需要聚合多个时间序列的样本。因为可视化和告警服务都会将查询请求发送给查询服务,所以这个系统也是重读取(Read-heavy)的。如图-4所示,数据看起来像是一个巨大的矩阵,在横纵两个方向都可以无限增长。图-4
数据存储系统
有了对数据访问模式的清晰理解,我们就可以选择数据存储系统了。我们可以把数据存储在关系型数据库中,但是会遇到各种挑战,比如写性能和可扩展性方面的问题,主要是因为它们并没有针对典型的时间序列工作负载(随时间变化的数据)做优化。另一方面,有几个非关系型数据库可以很高效地处理时间序列数据。
OpenTSDB是一个分布式时间序列数据库,但它是基于Hadoop和HBase的,运行Hadoop/HBase集群会增加复杂性。Bigtable也可以用于处理时间序列数据。Twitter使用MetricsDB,而亚马逊提供了Timestream作为时间序列数据库。
根据DB-Engines的排名,两个最流行的时间序列数据库是InfluxDB和Prometheus,它们都被设计来存储大量的时间序列数据并快速对数据进行实时分析。这两个数据库主要依赖内存缓存和硬盘存储,并且在数据的持久性和性能方面表现出色。我们来看一个例子。如图-5所示,一个有8核和32GB内存的InfluxDB每秒可以处理超过250,000次的写操作!
图-5
2.3 高层级设计
图-6展示了高层级的设计示意图。图-6
•指标来源:可以是应用服务器、SQL数据库、消息队列等。
•指标收集器:收集指标数据,并把数据写入时间序列数据库。
•时间序列数据库:把指标数据存储为时间序列。
•查询服务:简化了从时间序列数据库查询和获取数据的过程。
•告警系统:将告警通知发送到各个目的地。
•可视化系统:用各种图表来展示指标。
让我们接着深入探讨一些组件。
3. 设计继续深入
在系统设计面试中,候选人要能深入解释一些关键组件或者流程。在本节,我们会详细讨论如下话题。•指标数据的收集。
•扩展系统。
•查询服务。
•存储层。
•告警系统。
•可视化系统。
3.1 指标数据的收集
首先,我们看一下指标数据收集流程。图-7高亮显示了系统中的指标数据收集部分。图-7
拉模型vs.推模型
有两种收集指标数据的方法:拉或推。选择正确的数据收集方法,是在设计的早期就要做的重要决定。(1)拉模型。图-8展示了采用拉模型通过HTTP请求收集数据。我们有专门的指标收集器,可以定期从运行的应用中拉取指标值。
在这个方法中,指标收集器需要知道服务端点的完整列表,以便爬取。我们可以在“指标收集器”服务器上用一个文件来存储DNS/IP地址信息。但是在大型系统中,服务器可能会被频繁添加或移除,因此很难维护这样的列表文件,并且我们希望确保指标收集器不会丢掉任何来自新服务器的指标数据。好消息是,我们通过服务发现功能可以得到一个可靠、可扩展和可维护的解决方案。服务发现组件是Kubernetes、ZooKeeper等提供的。在服务发现组件中注册服务的可用性,然后指标收集器就可以查询服务发现组件来获取可用服务列表以便爬取。
图-8
服务发现组件中包含了关于何时和从何处收集指标的配置规则,如图-9所示。
图-9
图-10详细解释了拉模型。
图-10
•指标收集器从服务发现组件获取Web服务器的配置元数据(比如爬取时间间隔、IP地址、超时时间等)。
•指标收集器通过HTTP/metrics端点来拉取指标数据。为了暴露/metrics端点,通常需要在Web服务器上安装客户端库(比如SDK)。
(2)推模型。如图-11所示,多个指标来源(如Web服务器、数据库集群等)直接将数据发给指标收集器。
图-11
这个简单的推模型有一个缺点,它可能会给指标收集器带来巨大的流量。这可能成为一个瓶颈。解决方案之一是在应用服务器(如Web服务器、数据库服务器等)上安装代理(agent)。代理是运行在应用服务器上的软件。它从应用服务器上收集各种指标数据并进行处理,然后把它们推送给指标收集器。如果推送的流量大,代理就会进行调整以避免指标收集器被压垮。如图-12所示,指标收集代理安装在应用服务器上。Palo Alto Networks采用了类似的方法。
图-12
采用哪一种模型更好呢?就像生活中的许多事一样,这没有一个明确的答案。在现实中,这两种模型都被广泛应用。
•采用拉模型的例子:Prometheus。
•采用推模型的例子:Amazon CloudWatch和Graphite。在面试中,更重要的是了解每个方法的优缺点,而不是选出一个优胜者。表-2比较了拉模型和推模型的优缺点。
表-2
3.2 扩展系统
我们来仔细看看指标收集器和时间序列数据库。无论你使用什么模型(推或者拉),指标收集器都会收集巨量的数据。原先设计(见图-13)中的指标收集器可扩展性不太好,这是因为:•指标收集器也负责数据处理,它很容易被压垮。指标收集器应该尽可能简单并保持低延时,以便最大限度地降低数据丢失的风险。
•如果时间序列数据库不可用,则要么有丢失数据的风险,要么指标收集器需要将数据存储在临时数据存储中,并在稍后重新发送该数据。
图-13
了减轻这两个问题的影响,我们引入了队列组件,如图-14所示。
图-14
在这个设计中,指标收集器将指标数据发送给队列系统,比如Kafka,然后消费者或者流处理服务(如Apache Storm、Flink和Spark)处理数据并将其推送给时间序列数据库。这个方法有如下优点。
•Kafka可以用作高可靠和可扩展的分布式消息平台。
•它解耦了数据收集和数据处理服务。
•当数据库不可用时,将数据保留在Kafka中,能容易地避免数据丢失。
•不会给指标收集器施压。
通过Kafka扩展
有多种方法可以利用Kafka内置的分区机制来扩展我们的系统。•基于吞吐量的需求配置分区数量。
•可以按指标名字分区,以便消费者按名字来做聚合(见图-15)。
•可以进一步按标签来分区。
•可以对指标分类和排定优先级,以便先处理重要的指标。
图-15
3.3 查询服务
查询服务由查询服务器集群提供。它们访问时间序列数据库,并处理来自可视化系统或者告警系统的请求。使用一组专有的查询服务器把时间序列数据库与客户端(可视化和告警系统)解耦,使得我们无论何时需要都可以更换时间序列数据库或者可视化和告警系统。为了减少时间序列数据库的负载并使查询服务更高效,这里添加了缓存服务器来存储查询结果,如图-16所示。图-16
时间序列数据库查询语言
大部分流行的指标监控系统(比如Prometheus和InfluxDB)不使用SQL,而是创建了自有的查询语言。这么做有几个原因。最重要的一个原因是使用SQL语句来查询时间序列数据很困难。例如,influxdata博客上的文章“Why We’re Building Flux,a New Data Scripting and Query Language?”说,使用SQL计算指数滑动平均值可能需要写这些代码:但是,如果在InfluxDB中使用Flux语言,只需要写下面这几行代码即可,因为Flux是专门针对时间序列分析做过优化的语言:
3.4 存储层
缓存最近的数据
根据Facebook的论文“Gorilla:A Fast,Scalable,In-Memory Time Series Database”,在所有对运营数据存储的查询中,至少有85%针对的是过去26小时以内收集的数据。如果我们缓存最近的时间序列数据,就可以显著提升插入和查询速度。如图-17所示,我们在时间序列数据库之上添加了一个缓存层。时间序列数据库的存储引擎通常包括下面4个组成部分:预写日志(Write Ahead Log,WAL)、缓存、时间结构合并树(Time-Structured Merge Tree,TSM树)和时间序列索引(Time Series Index,TSI)。存储引擎的设计需要专门的领域知识,不在本文的讨论范围内。感兴趣的读者可以阅读Influx DB存储引擎的设计文档“InfluxDB Storage Engine”。图-17
聚合
为了确保查询服务提供低延时的查询且减少要存储的数据量,我们允许工程师或者数据科学家按特定的粒度(1秒、10秒、1分钟、1小时等)来灵活聚合和存储时间序列数据。Uber也使用了类似的方法。图-18
空间优化
如1.1节中所述,要存储的指标数据量是巨大的。以下是一些可以缓解这个问题的策略。(1)数据编码和压缩。数据编码和压缩可以显著减小数据的大小。我们来看一个简单的例子。如图-19所示,1610087371和1610087381之间只差10秒,差值“10”只需要用4比特表示而无须保存32位的完整时间戳。所以,与其存储绝对值,不如存储值的增量以及一个基础值,比如1610087371、10、10、9、11。
图-19
(2)向下采样(Downsampling)。向下采样是把高分辨率数据转换为低分辨率数据的过程。它用于减少整体的硬盘使用量。因为我们的数据保存期是1年,所以可以向下采样老数据。例如,我们允许工程师和数据科学家为不同的指标定义规则。下面是一个例子。
•保存期:1天,不采样。
•保存期:30天,向下采样到1分钟的分辨率。
•保存期:1年,向下采样到1小时的分辨率。
3.5 告警系统
现在,我们看一下告警系统,如图-20所示。告警的流程如下:
1.将规则配置文件加载到缓存服务器。规则以配置文件的形式保存在硬盘上。YAML是用来定义规则的一种常用格式。下面是一个告警规则的例子:
2.告警管理器从缓存服务器获取告警规则。
3.基于所配置的告警规则,告警管理器按照预定的间隔请求查询服务。如果监控的指标超过了阈值,就会触发告警事件。告警管理器也负责如下事项:
•过滤、合并和删除重复告警。图-21展示了对同一个实例(实例1)触发的告警进行合并的例子。
图-21
•访问控制。为了避免人为错误并保证系统安全,只允许获得授权的人访问特定的告警管理操作。
•重试。告警管理器检查告警的状态并确保至少发送了一次通知。
4.告警存储是一个键值数据库(比如Cassandra),它保存所有告警的状态机。它确保至少发送了一次通知。
5.合格的告警被插入Kafka。
6.告警消费者从Kafka中拉取告警事件。
7.告警消费者处理Kafka中的告警事件,并将通知发送到不同的接收端,如邮件、短信、PagerDuty或者HTTP端点。
3.6 可视化系统
可视化系统建立在数据层之上,可以基于不同的时间范围将指标展示在指标仪表板上,而将告警展示在告警仪表板上。图-22展示了一个仪表板,上面列出了如当前服务器请求、内存/CPU使用率、页面加载时间、网络流量和登录信息等指标。图-22
4. 总结
在本文中,我们介绍了指标监控和告警系统的设计,讨论了数据收集、时间序列数据库、告警和可视化系统的高层级设计。我们还深入探讨了其中几个最重要的技术/组成部分:•收集指标数据的拉模型和推模型。
•使用Kafka来扩展系统。
•在时间序列数据库之上添加缓存层。
•使用编码或者压缩算法来减小数据大小。
•过滤和合并告警,使得值班开发人员不会被收到的告警数量压垮。
系统经过了几轮迭代和优化,最终的设计如图-23所示。
图-23