云原生在业界的发展非常迅猛且有广泛的应用,Prometheus 作为云原生可观测中关键的技术之一,在百度集团内部,以及金融等客户中也逐步规模化应用。我本次将介绍百度云原生团队在大客户量级监控场景下所遇到的问题和解决方案。
百度监控发展史
百度的监控发展历史大致分为四代。
统一监控平台时代与开放监控平台时代
百度最初的监控平台始于 2007 年百度首次成立的专门运维平台研发团队,各个业务的运维监控统一由监控平台提供能力。在统一监控平台研发完成上线后,原先由各个业务自建脚本化监控采集逐步归拢到统一建设的平台中。
在统一监控平台一段时间的发展过后,我们发现由单一的运维研发小组构建监控平台并不容易。我们需要适配各类业务的不同监控需求、接口、数据源。因此,在 2012 年,我们构建了一个新的监控平台以支持自定义与标准化能力。
-
一是用户可通过监控平台所支持各类型可配置、可自定义方式,以标准化接口的形式将数据统一接入;
-
二是提供不同语言、框架的监控指标标准库,由各个业务自行在程序中集成,从而直接对接到监控平台之中。
智能监控时代
随着监控系统的不断完善,各业务接入的指标逐渐增加完善。但在这个过程中,我们定位故障的效率却没有提升。我们发现所收集的指标并不是全部都有用的,用处最大的是业务类型黄金指标,如请求量、交易量、错误率、响应时间,这些指标可以从业务的层面真正反映出程序是否正常。与之相反,资源类的指标很难反映业务真实状态,某台机器的 CPU 异常业务可能已经自行容灾,或者业务很早就异常了但资源类指标没有任何变化。
于是在 2014 年,我们提出了基于业务指标特征构建的监控系统,即智能监控系统。围绕业务监控,我们核心工作围绕两个方面开展:
-
智能异常监测。业务指标与传统指标不同,不能限定一个固定的告警阈值,业务在不同时间段都会有波峰波谷及各种特殊情况,因此,我们基于机器学习、同环比分析,对历史数据进行学习,预测指标趋势与异常阈值,帮助发现业务指标是否真正异常。
-
智能故障诊断。借助业务的多维度特征,如接口信息、交易量、错误码、用户信息等维度,逐层分析和推断故障根因。
云原生可观测时代
业界内云原生技术在 2019 年已进入了较为广泛的使用,百度内部也有大量业务开始了云原生架构升级。在这一阶段,我们的监控平台面临了极大的挑战。我们要对原有监控系统进行几乎是颠覆性的改造,才能支持云原生的各类型服务发现、新数据模型、数据协议等。
但同时云原生所提出的两点也与我们之前的发展思路不谋而合:
-
第一是监控标准化。Prometheus 及 OpenTelemetry 定义了一系列的监控标准方案,其中包括指标、Trace、日志等等,实现了业界数据的广泛互通。这点非常重要,我们通过暴露 Prometheus 的监控接口,用 OpenTelemetry 的客户端输出 Trace、日志信息,节约了大量用于业务兼容适配的人力资源。
-
第二则是根因故障定位的进一步探索,加深指标、Trace 等标准规范的关联分析能力。
Prometheus 业务监控遇到的挑战
业务监控在 Prometheus 中的落地场景
业务监控的重要性远超资源监控,是监控指标中的 Top 1。业务指标可落地的场景总结如下:
-
故障管理场景。其准确率要远高于资源指标,其多维度属性应能够支撑故障分析及根因定位需求。
-
容量管理场景。基于业务指标如请求量及响应时间等数据,评估模块容量是否应扩缩容;联动扩缩容平台,实现服务的自动扩缩容。
-
性能分析场景。依据业务指标中区分接口、阶段等响应时间及请求量数据对应用的性能分析优化。
-
运营分析场景。可对流量成分进行分析,可用于 AB 对比实验等场景。
在已有完善的监控系统前提下,我们仍选择使用 Prometheus 是因为其指标类型天然适合业务监控场景:
-
Counter 以及基于 Counter 的复合指标类型、Histogram、Summary 等,很适合表达请求量、响应时间、交易量等指标,其多维度指标模型也可适用于业务指标中众多的数据维度表达。
-
通过 PromQL 强大的数据分析能力,结合 Grafana 等数据可视化组件,上层业务得以快速地对数据进行分析,实时动态调整分析视图。
-
将业务指标与下层各类资源、容器监控,以及移动端上指标统一汇总分析。
大规模业务监控在 Prometheus 架构下的挑战
业务监控与普通资源监控不同,这类场景主要提出了几点诉求:
-
高性能,其中又区分以下几点:
-
指标承载能力。业务的大量维度致使指标规模巨大化,客户的极端场景中甚至可能出现每秒亿级的指标量。
-
查询分析能力。以极强的数据分析及性能才能支撑全局动态分析请求量在多个维度内的对比关系。
-
数据存储能力。业务监控中常常需要进行历史数据对比,尤其是地图类型业务,有时是数月,有时是几年,甚至也有业务需要对五年前五一假期期间数据进行对比。
-
高可用。集群需尽可能自恢复单节点故障。在面对集群级别的整体故障后,应能进行人工止损及快速切换。
-
准确性。在大部分监控场景下虽然不具备多少关注度,但在金融类型或其他关注交易量的用户中,他们对准确性有极其严苛的要求。
Prometheus 的标准开源方案
Prometheus 作为单机引擎,拥有集采集、存储、查询、计算、报警于一体的设计,非常适合于部署和运维。
受限于单机的采集能力,Prometheus 无法采集过多端口,单机的性能也限制了本地存储量仅又单机本身的磁盘可用。此外,在高可用方面,如果单机故障,则采集失效;如果磁盘故障,则数据丢失无法恢复。
由官方提供的联邦集群方案,让上层的中央 Prometheus 在采集端进行数据汇总,采用双采双存方案,使用负载均衡器进行可用性切换,从而实现更高的可用性。该方案中的中央 Prometheus Server 仍是单机方案,因此同样会存在存储、写入、性能的瓶颈及存储量方面的挑战。高可用方案中的单个集群故障期间的数据仍是丢失且无法恢复的。
大规模 Prometheus 业务监控解决方案
基于开源的方案无法有效解决我们所面对的问题,下面从三个角度入手,更详细地解释我们的考量方向。
高性能 Prometheus 实践
我们要如何应对指标的采集量级?方案一是通过集群扩展,但集群资源有限度;方案二则是提升集群性能,但这种方案所带来的提升也是有上限的。
指标降维
我们首先对用户对这些指标的使用进行了分析。业务指标巨大的量级有两部分组成,一是实例,大业务场景中模块内可能部署了上千实例,百度集团内部也有上万实例的极端情况;二是业务指标的维度,交易码、错误码、请求量、用户来源省份、运营商等各类用于分析定位的维度,其结果也有十万级别的指标量。
但在实际应用中,我们发现用户并不需要如此大量的指标。物理维度的故障意味着某台机器的指标异常、某个数据中心整体故障、某个机房存在问题等等,纯业务的故障则是接口异常、交易类型异常等场景,故障很少会在物理维度与业务维度交叉的某一个维度。
对此,我们的第一个解决思路是去掉叉乘维度,通过增加预计算,将原始指标转换为实例级或业务级聚合指标。通过这种方式,将原先的乘法指标量级变为加法指标量级,在应用中发送至后端的指标量减少了 90%以上。
需要注意的是,针对 Counter 类型指标的聚合处理。由于 Counter 指标有重置的情况,故直接加和是没有意义的,故需要使用 rate()算子将 Counter 转换为 Gauge 指标,再使用 sum 进行加和。
采集层聚合计算瓶颈优化
采集层预聚合的方法,在实际的应用中,会发现 CPU、内存使用率暴涨,导致整体采集性能下降。
问题在于预聚合的实现方式。 Prometheus 实现预聚合的方式是周期性加载全量数据进行运算,会在计算周期到到达的一瞬间会致使 CPU 和内存量的激增。
对此,我们通过一个 Adapter 服务以 sidecar 形式伴随 Prometheus 采集端进行预聚合处理。在 Prometheus 正常采集完成写入 wal 文件后,我们通过对该文件的读取获得最新数据。通过实现累加计数器,将周期性计算转变为流式计算,有效降低降低 CPU、内存占用。
至于我们为什么选用 wal 而非 remote-write?这是因为 remote-write 需要对数据序列化和反序列化,而通过 wal 文件则可以将这部分开销导致的性能损失也抹消掉。
采集端任务负载均衡
目前,我们已经可以让后端接收到真正有用的数据了。但就如之前所讲,单一的采集端无法承载所有指标的采集,我们需要借助集群,用基于分片的方式采集所有指标。
这一阶段难点在于分片。分片一般以探测目标数为基础,但在实际的应用中,不同的业务模块产出指标量级不同,带来采集端的负载严重不均。针对这个问题,我们利用主动的探测服务探测应用所产出的指标量,并基于该指标量动态分配每个 Prometheus 所采集的目标,实现按照指标量进行负载均衡。
流式计算
业务指标中存在很多需要数据分析的场景,我们可能仍需要面对十万、甚至百万级别数据量的聚合计算问题。一般场景下这类性能问题可通过预计算解决,将一次性查询计算时的资源消耗拆分为多个周期进行。
但 Prometheus 周期性的查询存储的预计算实现方式,在指标量级到达一定程度时仍有可能造成存储服务的宕机。
针对这种情况,我们的解决方案思路与先前类似:将预先计算环节从 Prometheus 中移出。为此,我们通过 Flink 实现了流式计算引擎,读取 Prometheus 配置的所有 Recording Rule 并将其生效在 Flink 计算引擎内,真正为存储引擎减负。
那么要如何在 Flink 计算引擎中实现 Prometheus 的算子?如果我们仅仅还原 Prometheus 的原有实现,就没有带来任何性能方面的提升。因此,我们需要基于流式计算以及 Flink 特点对 Prometheus 的算子进行改造。
所做的改造有三种:
-
基于 groupby 对聚合计算拆分。根据 groupby 结果维度进行并行化哈希计算,将计算分散到 Flink 集群中。
-
若 groupby 之后无法进一步拆分的数据仍然很多,则在所有 Flink 集群中首先执行本地数据聚合计算,再将计算结果汇总,避免造成局部热点。
-
通过实现 sum()、sum(rate()) 等累加算子,每次仅加算对应数据到计数器中,而不是缓存所有数据,将内存、计算开销分摊。
时序数据降采样
业务数据对比往往要用到历史数据。直接执行大跨度查询必定导致存储系统宕机;此外,大量数据存储会导致不必要的高磁盘成本。
我们为此所设计的降采样方案中包含五分钟或一小时这两个降采样级别,根据用户查询的时间范围动态选择需要查询的数据。我们也可以基于不同采样级别或特定指标配置存储有效期,从而降低磁盘成本。
在这套方案中我们需要让 Prometheus 的算子与降采样方案进行适配。常用 over_time 类型算子(sum_over_time、count_over_time 等)一般会用于 Gauge 指标,而降采样中实际存储的是周期内对应算子值(sum、count、avg、max、min 等),在对应算子输入时降采样会查询对应值以确保最终统计数据的精确性。至于 Counter 类型算子(Increase、rate 函数等),因为直接将 Counter 值存储毫无意义,因此我们在此需要存储 Counter 的增量值,从而在算子输入时通过增量值返回准确的数据。
高可用 Prometheus 实践
采集层高可用容灾
高可用一方面在于集群内组件的高可用,能够在故障后自动恢复,从而减少人为干预。我们的 Flink、Kafka,还是实际存储、远程存储,均是分布式解决方案,单实例故障后可自动容灾,因此不需要过多地关注。
至于采集端,我们通过双采,以单发模式(主备模式)向后端发送数据,借用 Kubernetes 的 Lease 组件进行选主,每个 Prometheus 上的 sidecar 对实时数据的接入情况进行汇报,若无实时数据则判定该实例故障,从而进行切主。
计算与存储高可用保障
虽然 Kafka、Flink、TSDB 都是高可用,但却并不能保证数据不会遗失。因此,在 Flink 或 TSDB 宕机恢复后,我们还会基于已经引入的 Kafka 实现数据重传。因为 Flink 进行计算时会周期产出数据,如果通过 Kafka 重传的数据不满一周期,那么我们将向前推移多个周期并丢弃第一周期数据,从而保证最终计算数据在多次执行后均能保持一致性。
两地三中心高可用
为保证整体集群的无故障,我们与金融行业客户一同构建了“两地三中心”方案。在同城构建两套中心,数据同时发往两套中心且其中数据完全一致,实现同城双活。单中心故障的情况下,通过切换最终仪表盘查询入口以获取最新正确数据。异地保障则配合客户业务,搭建一套实时存活灾备集群,在业务流量切换至异地灾备中心时,通过灾备中心监控服务对其监控。
Prometheus 的数据准确性保障
通常监控的应用场景中对准确性没有过多的要求,更多还是要靠其发现故障。但在金融之类场景中,则往往对准确性有极其严苛的要求。那么 Prometheus 能做到这点吗?根据官方文档, Prometheus 不适用于需求百分百精确性的场景。
Prometheus 的误差主要来自两个方面。
-
客户端程序生成相关计数器在进程推出重启后,我们无法即使拉取到内存中未采集到的数据。对此,我们只能提高采集周期,用更频繁的指标采集来减少数据损失。
-
其次,Prometheus 本身的算子实现也可能导致误差出现。这种算子带来的误差,是我们重点要解决的问题。
Prometheus 的 increase/rate 首点忽略会导致数据不准。举例来说,上图进程中的两条曲线中,我们首先在进程启动时收到五次失败请求,在第 20 秒时收到 10 次成功请求,在第 20 秒时收到五次失败请求,可以看出 Exporter 所吐出的数据随时间变化情况。
我们期望能获得 10 次失败 10 次成功,共计 20 次总请求数,但 Prometheus 实际计算会得出 5,这是因为每条曲线的第一个点都被忽略了。成功率的计算同理,我们期望的成功率为 50%,但最终计算却是 0%,因为成功曲线首次收到的 10 次成功请求没有被统计到。
在某些场景下,这一问题会被扩大化。正常服务的运行中很少会发生错误,而错误一旦发生则必定是第一次出现,那么这次数据 Prometheus 虽然能采集到,但我们却看不到,导致故障的漏报或延迟。
Prometheus 的首点忽略主要为解决在应用启动后进行监控采集,采集到 Counter 的中间值问题,此时采集到的是业务自启动开始到当前的累加值,并不只有当前周期的数据,因此 Prometheus 会对其忽略。对此,我们的解决方案针对 Prometheus 的这个策略,我们将应用 Target 采集状态记录下来,Target 第一次采集之后,所有点都可判断为是正常新增点。基于正确新增点,我们将首点减 0 即可得到当前点的增量。
除此之外, Prometheus 中还存在许多近似计算。以 increase/rate 为例,其计算过程包含的拟合计算可能导致统计数据中实际的正整数被以小数形式输出,甚至可能损失精度。对此,我们可以将 Counter 类型数据转换为 Gauge 类型,再使用 over_time 类型算子计算以避免误差。
此外,sum_over_time 实际是双向闭区间统计,对于定期从 Prometheus 采集数据并自行进行汇总计算的报表系统来说,中间叠加的部分会很多。解决方法是可以新增单向闭区间算子,业务上使用新的算子来进行计算。
总结
在 Prometheus 的解决方案中主要介绍了三部分:
-
在高性能部分重点提出了数据降维、动态分片采集、流式计算、存储降采样方案
-
在高可用方面介绍了集群的高可用,以及两地三中心的跨集群高可用方案
-
在数据准确性方面,我们探讨了 Prometheus 算子带来的误差及可能解决方案
这些方案不仅可以有机整合为整体,也能单独应用。希望能抛砖引玉,给大家带来一些 Prometheus 应用过程中解决问题的思路。