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