如何扩展单个Prometheus实现近万Kubernetes集群监控?
引言
TKE团队负责公有云,私有云场景下近万个集群,数百万核节点的运维管理工作。为了监控规模如此庞大的集群联邦,TKE团队在原生Prometheus的基础上进行了大量探索与改进,研发出一套可扩展,高可用且兼容原生配置的Prometheus集群系统,理论上可支持无限的series数目和存储容量,支持纳管TKE集群,EKS集群以及自建K8s集群的监控诉求。
本文从TKE的架构出发,逐步介绍了整个监控系统的演进过程,包括早期的方案和遇到的问题,社区方案的瓶颈,我们的改进原理等。
TKE架构简介
为了让读者更好理解我们的场景,我们首先简单介绍一下TKE的基础架构。
TKE团队是公有云界首家采用Kubernetes in Kubernetes进行集群联邦管理的Kubernetes运营团队,其核心思想就是用一个Meta Cluster来托管其他集群的apiserver,controller-manager,scheduler,监控套件等非业务组件,在Meta Cluster中的组件对用户而言是隐藏的,如下图所示。
上图Meta Cluster中的组件对于用户而言都是隐藏的。支撑环境服务用于直接处理来至TKE控制台的请求。
- Meta Cluster用于管理集群的控制面板组件,如apiserver等
- Meta Cluster中还与一些隐藏的功能组件,例如监控组件
- 支撑服务用于接收来至控制台的请求,并连接到用户集群进行实际操作
早期的监控方案
需求
TKE早期监控方案不支持用户添加业务相关的监控指标,只包括集群运维关注的监控,主要希望监控的目标如下:
- 每个用户集群的核心组件监控,如apiserver, scheduler, controller-manager等
- 每个用户集群的基础资源监控,如Pod状态,Deployment负载,集群总负载等
- Meta Cluster中所有组件的监控,包含Cluster-monitor自身,一些Meta Cluster自身的addon组件等
- 支撑环境组件的监控,如支持web server服务处理成功率,外部接口调用成功率等
架构
集群级别
在上一节的TKE架构图中,我们在Meta Cluster中看到每个集群有一套Cluster-monitor组件,该组件就是单集群级别的监控采集套件。Cluster-monitor包含了以Prometheus为核心的一系列组件,其基本功能就是采集每个用户集群的基础监控数据,例如Pod负载,Deployment负载,Node CPU使用率等,采集到的数据将直接写到云监控团队提供的Argus系统中存储于告警。核心组件如下图。
Barad:云监控提供的多维监控系统,是云上其他服务主要使用的监控系统,其相对成熟稳定,但是不灵活,指标和label都需要提前在系统上设置好。
Argus:云监控团队提供的多维业务监控系统,其特点是支持较为灵活的指标上报机制和强大的告警能力。这是TKE团队主要使用的监控系统。
数据流:
- Prometheus从kubelet采集container负载信息,从kube-state-metrics采集集群元数据,比如Pod状态,Node状态等。数据在Prometheus进行聚合,产生固定的聚合指标,如container级别指标,Pod级别指标。采集到的数据写往两个地方,一部分数据写往Argus系统,这部分数据用于支撑TKE控制台上的监控面板及告警,另外一部分数据会写往Barad系统,这是因为更早时期的TKE支持在Barad控制台配置容器相关的告警,这份数据是为了使旧版告警能继续使用。
- 另外一条数据流是Barad-importer组件会从Barad(云监控)处拉取节点相关的数据,比如CPU使用率,内存使用率等,并将数据导入Argus系统,从而使得Argus也能进行节点相关的数据展示和告警。这里没有选择社区主流的node-exporter来收集节点数据是因为node-exporter需要在用户集群内部署Daemonset,而我们希望整个监控数据采集系统对用户是隐藏的。
这部分数据将通过控制台输出给用户
地域级别
成功采集到了属于每个用户集群的数据,但是,对于一些地域级别的监控,包括
- Meta Cluster中的管理组件
- Cluster-monitor组件自身
- 整个地域级别的集合信息,如总集群数,集群平均节点数,平均创建时间等数据
通过单个Cluster-monitor无法采集。需要构建更上一级的地域级别监控。
Region Prometheus不仅拉取如meta cluster operator,meta cluster service controller等核心组件的数据外,还通过Prometheus联邦接口拉取Cluster-monitor中的单集群数据进行二次聚合,产生地域级别集群的数据。地域级别数据直接存在本地,不写往Argus,因为这部分数据需要对接Grafana,由团队内部使用。
全网级别
我们在单地域监控的基础上又构建了一层全网级别的监控。用于监控
- 支撑环境组件监控
- 所有地域的Region Prometheus数据再聚合得到全网级别指标
全网数据也是给内部人员查看。
架构总览
逐渐暴露出的问题
上述介绍的架构虽然解决了我们对于大规模集群联邦的基本监控诉求,但是依旧存在几点不足。
Prometheus性能不足
原生Prometheus并不支持高可用,也不能做横向扩缩容,当集群规模较大时,单一Prometheus会出现性能瓶颈,无法正常采集数据,我们将在后续章节中给出Prometheus的压测数据。
采集周期过长
目前采集周期是1m,我们希望能降低到15s。
原始数据存储时长过短
由于云监控所能提供的Argus系统的聚合能力有限,我们并没有将Cluster-monitor采集到的数据直接输出到Argus,而是将数据按预定的指标进行聚合,只发送聚合过的数据,TKE控制台在数据展示时只做时间上的聚合。而原始数据我们只保存15分钟。如果加长时间进行本地存储,我们需要为每个Cluster-monitor部署云硬盘,由于TKE存在部分空集群(节点个数为0),这会产生资源浪费。
不支持跨集群查询
由于每个集群的数据都是本地落盘,Region Prometheus由于性能有限的原因,只采集了部分聚合指标,使得无法进行跨集群原始数据的聚合查询,而这类查询对于获取单用户多集群的综合数据是很有帮助的。
运维难度大
每一级Prometheus都是单独管理的,缺乏全局管理工具。
设计理想模型
怎样的监控系统,可以同时解决上述几个问题呢?我们先构思一个理想模型,称之为Kvass。
采集【高性能】
先看采集,我们采集侧遇到的问题主要就是性能问题,即我们希望Kvass拥有以下能力
- 高性能:有无限性能的采集器。
- 原生:支持原生Prometheus主流的配置方式,包括Prometheus operator所支持的ServiceMonitor,PodMonitor等。
存储【长期存储】
存储侧,我们遇到的问题是存储时长,以及资源利用率,我们希望Kvass的存储拥有以下能力
- 时长可能达到1年
- 存储资源利用率高
展示【全局视图】
展示侧,我们遇到的问题是无法得到全局视图,所以,对于理想化的展示,我们希望Kvass的展示拥有以下能力
- 能对接Grafana
- 可以跨集群聚合查询
- 支持原生Prometheus语句
告警【原生】
告警侧,我们希望能支持原生Prometheus的告警配置。
运维【便捷】
我们希望Kvass没有过于复杂的配置项,且系统拥有一套完整的运维工具,能使用Kubernetes原生方式进行管理。
整体模型
假设我们有了这么一个模型,那么我们的监控就可以变成下面这种架构,在这种模型下,我们拥有了单个地域下所有我们要的原始数据。
- 去掉了Cluster-monitor中的Prometheus
- 去掉了Region Prometheus
高性能采集
这一节介绍我们是如何实现理想模型中的高性能采集器的
Prometheus采集原理
各模块的关系
首先我们先了解一下Prometheus的采集原理,为后面修改Prometheus实现高可用分片打下基础。下图展示了Prometheus采集时各模块的关系
- 配置管理模块:该模块负责接收配置更新动作,所有依赖配置文件的模块,在初始化的时候都会向配置管理模块注册配置更新监听函数。
- 服务发现模块:当job配置了服务发现时,target的个数是动态变化的,该模块负责做服务发现并生成target的变化信息,并通知抓取模块。
- 存储模块:该模块有两部分组成,一个是本地TSDB模块,一个是远程存储模块,该模块负责将target采集到的数据进行本地存储,同时也管理远程存储的发送过程。
- 抓取模块:该模块是抓取的核心模块,其负责根据配置文件以及服务发现模块给出的target信息,生成多个job对象,每个job对象包含多个target scaper对象,每个target scraper对象都会启动一个协程,周期性地对目标进行指标抓取,并发送到存储模块。
内存占用
我们已经从Prometheus在实际中的表现知道Prometheus对内存使用会随着采集目标的规模增长而增长,那Prometheus的内存到底用在哪了?
存储模块
- Prometheus的存储不是将每个采集到的点都直接落盘,而是会先写入wal文件,采集一段时间后,将wal压缩成块。在这期间,存储模块需要缓存所有series的label信息,并且在压缩的时候,也需要产生较大的临时内存消耗。
- 远程存储的原理是通过监听wal文件的变化,将wal文件中的点逐步发送到远端,在一个wal文件被完全发送完之前,远程存储管理器也会缓存所有发现的series的label信息,并且维护多个发送队列,这也是内存消耗比较大的地方。
抓取模块
- 对于每个target,每个series只有第一次被存储的时候才会把series的label信息传给存储模块,存储模块会返回一个id,target scraper就会将series进行hash并与id对应,后续抓取时,本series只需将id和值告诉存储模块即可。hash与id的对应表也比较占内存。
Prometheus性能压测
压测目的
分析了Prometheus的采集原理后,我们可以想确定以下几个事情
- target数目对Prometheus负载的关系
- series规模和Prometheus负载的关系
target相关性
压测方法
压测数据
压测结论
- target个数对Prometheus的整体负载影响不大
series规模压测
压测方法
压测数据
官方大规模集群各个资源产生的series
以下表格中的资源个数为Kubenetes官方给出的大规模集群应该包含的资源数 series个数通过统计cadvisor 和kube-state-metrics的指标得出
总计 5118w series。
压测结论
- 当series数目高于300w时,Prometheus内存将暴增
- 按等比例换算,单Prometheus采集300节点以上的集群时会内存会出现较大涨幅
实现可分片高可用Prometheus
有大量节点数目高于300的集群,通过前面的压测,单个Prometheus确实存在性能瓶颈。那我们根据前面的采集原理,尝试修改Prometheus让其支持横向扩缩容。
设计原则
无论怎么修改,我们希望保持以下特性
- 扩缩容时不断点
- 负载均衡
- 100%兼容原来的配置文件及采集能力
核心原理
再来回顾一下上边的采集原理图,看看我们应该在哪个地方进行修改。
从上图中,我们发现,负载产生的源泉是target scraper,如果减少target scraper个数,就能减少整体采集到的series,从而降低负载。
假设我们有多个Prometheus共享相同的配置文件,那么理论上他们产生出来的target scraper应当是一模一样的。如果多个Prometheus之间能够相互协调,根据每个target scraper抓取的目标数据量情况,分配这些target scraper,就是实现负载的均摊。如下图所示。
实现动态打散
-
为了实现上述方案,我们需要一个独立于所有Prometheus的负载协调器,协调器周期性(15s) 进行负载计算,该协调器负责收集所有target scraper的信息,以及所有Prometheus的信息,随后通过分配算法,为每个Prometheus分配一些target scraper,最后将结果同步给所有Prometheus。
-
相应的,每个Prometheus需要添加一个本地协调模块,该模块负责和独立的协调器进行对接,上报本Prometheus通过服务发现发现的所有target,以及上一次采集获知的target的数据量,另外该模块也接受协调器下发的采集任务信息,用于控制本Prometheus应该开启哪些target scraper。
targets分配算法
当协调器收集到所有target信息后,需要将target分配给所有Prometheus在分配时,我们保持以下原则
- 优先分配到正在采集该target的Prometheus
- 负载尽可能均衡
我们最终采用了如下算法来分配target
- 规定target负载 = series * 每分钟采集次数。
- 将各个Prometheus的target信息进行汇总,得到全局信息,假设为global_targets,并全部标记为未分配。
- 计算每个Prometheus理论上平均应该负责的采集负载,设为avg_load。
- 针对每个Prometheus,尝试将其正在采集的target分配给他,前提是该Prometheus负载不超过avg_load,并将成功分配的target在global_targets中标记为已分配。
- 遍历global_targets,针对步骤3剩下的target, 有以下几种情况
4.1 如果之前没有采集过,则随机分配个一个Prometheus。
4.2 如果原来采集的Prometheus负载未超过avg_load,则分配给他。
4.3 找到所有Prometheus中负载最低的实例,如果该实例目前的负载总和加上当前target的负载依旧小于avg_load,则分配他给,否则分配给原来的采集的Prometheus。
我们还可以用伪代码来表示这个算法:
func load(t target) int {
return t.series * (60 / t.scrape_interval)
}
func reBalance(){
global_targets := 所有Prometheus的targets信息汇总
avg_load = avg(global_targets)
for 每个Prometheus {
p := 当前Prometheus
for 正在采集的target{
t := 当前target
if p.Load <= avg_load {
p.addTarget(t)
global_targets[t] = 已分配
p.Load += load(t)
}
}
}
for global_targets{
t := 当前target
if t 已分配{
continue
}
p := 正在采集t的Prometheus
if p 不存在 {
p = 随机Prometheus
}else{
if p.Load > avg_load {
exp := 负载最轻的Prometheus
if exp.Load + load(t) <= avg_load{
p = exp
}
}
}
p.addTarget(t)
p.Load += load(t)
}
}
targets交接
当一个Prometheus上的target抓取任务被分配到另外一个Prometheus时,需要增加一种平滑转移机制,确保转移过程中不掉点。这里我们容忍重复点,因为我们将在后面将数据去重。
target交接的实现非常简单,由于各个Prometheus的target更新几乎是同时发生的,所以只需要让第一个Prometheus的发现抓取任务被转移后,延迟2个抓取周期结束任务即可。
扩容
协调器会在每个协调周期计算所有Prometheus的负载,确保平均负载不高于一个阈值,否则就会增加Prometheus个数,在下个协调周期采用上边介绍的targets交接方法将一部分targets分配给它。
缩容
考虑到每个Prometheus都有本地数据,缩容操作并不能直接将多余的Prometheus删除。我们采用了以下方法进行缩容
- 将多余的Prometheus标记为闲置,并记录当前时间。
- 闲置的Prometheus上的target会全部被转移,并且不再参与后续任务分配。
- 当闲置Prometheus所有数据已上报远端(后续将介绍),将实例删除。
- 特别的,如果在闲置过程中,出现了扩容操作,则将闲置最久的实例重新取消闲置,继续参与工作。
高可用
在上述介绍的方案中,当某个Prometheus的服务不可用时,协调器会第一时间把target转移到其他Prometheus上继续采集,在协调周期很短(5s)的情况下,出现断点的几率其实是非常低的。但是如果需要更高的可用性,更好的方法是进行数据冗余,即每个targets都会被分配给多个Prometheus实例,从而达到高可用的效果。
关于存储的问题
到目前为止,我们虽然将Prometheus的采集功能成功分片化,但是,各个Prometheus采集到的数据是分散的,我们需要一个统一的存储机制,将各个Prometheus采集到的数据进行整合。
统一存储
在上一节最后,我们引出,我们需要一个统一的存储来将分片化的Prometheus数据进行存储。业界在这方面有不少优秀的开源项目,我们选取了知名度最高的两个项目,从架构,接入方式,社区活跃度,性能等各方面做了调研。
Thanos vs Cortex
整体比较
Thanos简介
Thanos是社区十分流行的Prometheus高可用解决方案,其设计如图所示
从采集侧看,Thanos,利用Prometheus边上的Thanos sidecar,将Prometheus落在本地的数据盘上传至对象存储中进行远程存储,这里的Prometheus可以有多个,各自上报各自的数据。
查询时,优先从各Prometheus处查询数据,如果没查到,则从对象存储中查询历史数据,Thanos会将查询到的数据进行去重。Thanos的设计十分符合我们前面的采集方案提到的统一存储。接入后如图所示。
Cortex简介
Cortex是Weavework公司开源的Prometheus兼容的TSDB,其原生支持多租户,且官方宣传其具有非常强大的性能,能存储高达2500万级别的series,其架构如图所示
从架构图不难发现,Cortex比Thanos要复杂得多,外部依赖也多,估计整体运维难度的比较大。Cortex不再使用Prometheus自带的存储,而是让Prometheus通过remote write将数据全部写到Cortex系统进行统一的存储。Cortex通过可分片接收器来接收数据,随后将数据块存储到对象存储中,而将数据索引存储到Memcache中。
- 从架构上来看,Cortex似乎更加复杂,运维难度也高
- 从接入方式看,Thanos对原来的Prometheus配置文件没有改动,属于无侵入方式,而Cortex需要在配置文件中加入remote write,另外目前版本的Prometheus无法通过参数关闭本地存储,所以即使只使用remote write存储到Cortex, Prometheus本地还是会有数据。
社区现状
- 从社区活跃度上看,Thanos表现更加优秀
性能压测
上文从架构角度对两个项目进行了一番对比,但是实际使用中,他两表现如何呢,我们进行性能压测:
压测方式
我们保持两个系统series总量总是拥有相同的变化,从查询性能,系统负载等多方面,去评估他们之前的优劣
压测结果
-
稳定性:不同数据规模下,组件是否正常工作
从数据上看 Thanos 更加稳定一些。
-
查询性能:不同数据规模下,查询的效率
从数据上看,Thanos的查询效率更高。
-
未启用Ruler资源消耗:没有启动Ruler情况下,各组件的负载
就采集和查询而言,Thanos的资源消耗要比Cortex低很多。
在整个压测过程中,我们发现Cortex的性能远没有官方宣称的好,当然也可能是我们的调参不合理,但是这也反应出Cortex的使用难度极高,运维十分复杂(上百的参数),整体使用体验非常差。反观Thanos整体表现和官方介绍的较为相近,运维难度也比较低,系统较好把控。
选型
从前面的分析对比来看,Thanos无论是从性能还是从社区活跃度,还是从接入方式上看,较Cortex都有比较大的优势。所以我们选择采用Thanos方案来作为统一存储。
Kvass系统整体实现
到目前为止,我们通过实现可分片Prometheus加Thanos,实现了一套与原生Prometheus配置100%兼容的高性能可伸缩的Kvass监控系统。组件关系如图:
接入多个k8s集群
上图我们只画了一套采集端(即多个共享同一份配置文件的Prometheus,以及他们的协调器),实际上系统支持多个采集端,即一个系统可支持多个Kubernetes集群的监控,从而得到多集群全局数据视图。
Kvass-operator
回顾旧版本监控在运维方法的不足,我们希望我们的新监控系统有用完善的管理工具,且能用Kubernetes的方式进行管理。我们决定使用operator模式进行管理,Kvass-operator就是整个系统的管理中心,它包含如下三种自定义资源
- Thanos:定义了Thanos相关组件的配置及状态,全局唯一。
- Prometheus: 每个Prometheus定义了一个Prometheus集群的配置,例如其关联的Kubernetes集群基础信息,协调算法的一些阈值等
- Notification: 定义了告警渠道,Kvass-operator负责根据其定义去更新云上告警配置
Prometheus-operator及集群内采集配置管理
由于Prometheus配置文件管理比较复杂,CoreOS开源了一个Prometheus-operator项目,用于管理Prometheus及其配置文件,它支持通过定义ServiceMonitor,PodMonitor这两种相比于原生配置文件具有更优可读性的自定义类型,协助用户生成最终的采集配置文件。
我们希望实现一种虚拟Prometheus机制,即每个user cluster能够在自己集群内部管理其所对应的Prometheus采集配置文件,进行ServiceMonitor和PodMonitor的增删改查,也就是说,Prometheus就好像部署在自己集群里面一样。
为了达到这种效果,我们引入并修改了Prometheus-operator。新版Prometheus-operator会连接上用户集群进行ServiceMonitor和PodMonitor的监听,并将配置文件生成在采集侧。
另外我们将协调器和Prometheus-operator放在了一起。
基于Kvass的TKE监控方案
通过一步一步改进,我们最终拥有了一套支持多集群采集,并支持扩缩容的高可用监控系统,我们用其替换原来监控方案中的Cluster-monitor + Region Prometheus。实现了文章之初的诉求。
最初版本
新方案
我们上边介绍的方案,已经可以整体替换早期方案中的Region Prometheus及Cluster-monitor。现在我们再加入一套Thanos,用于将全网数据进行整合。
相比于旧版本监控的指标预定义,新版本监控系统由于Prometheus是可扩缩容的,所以是可以支持用户上报自定义数据的。
总结
项目思路
Kvass的设计不是天马行空拍脑袋决定的,而是在当前场景下一些问题的解决思路所组成的产物。
客观看待旧版本
虽然我们整篇文章就是在介绍一种用于取代旧版本监控的新系统,但是这并不意味着我们觉得旧版本监控设计得差劲,只是随着业务的发展,旧版本监控系统所面临的场景相较于设计之初有了较大变化,当时合理的一些决策,在当前场景下变得不再适用而已。与其说是替换,不如称为为演进。
先设计模型
相比于直接开始系统落地,我们更倾向于先设计系统模型,以及确定设计原则,系统模型用于理清我们到底是要解决什么问题,我们的系统应该由哪几个核心模块组件,每个模块最核心要解决的问题是什么,有了系统模型,就等于有了设计蓝图和思路。
确定设计原则
在系统设计过程中,我们尤为重视设计的原则,即无论我们采用什么形式,什么方案,哪些特性是新系统必须要有的,对于Kvass而言,原生兼容是我们首要的设计原则,我们希望无论我们怎么设计,对用户集群而言,就是个Prometheus。
落地
在整体研发过程中,我们也踩了不少坑。Cortex的架构设计相比于thaos而言,采用了索引与数据分离的方式,设计上确实更加合理,理论上对于大规模数据,读取性能会更优,并且Cortex由于原生就支持多租户,实现了大量参数用于限制用户的查询规模,这点是Thanos有待加强的地方。我们最初的方案也尝试采用Cortex来作为统一存储,但是在实际使用时,发现Cortex存在内存占用高,调参复杂等问题,而Thanos相比而言,性能较为稳定,也更加切近我们的场景,我们再结合压测报告,选择将存储切换为Thanos。
产品化
由于Kvass系统所以解决的问题具有一定普适性,TKE决定将其作为一个子产品对用户暴露,为用户提供基于Kvass的云原生系统,该产品目前已开放内测。
【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!