如何扩展单个Prometheus实现近万Kubernetes集群监控?

引言

TKE团队负责公有云,私有云场景下近万个集群,数百万核节点的运维管理工做。为了监控规模如此庞大的集群联邦,TKE团队在原生Prometheus的基础上进行了大量探索与改进,研发出一套可扩展,高可用且兼容原生配置的Prometheus集群系统,理论上可支持无限的series数目和存储容量,支持纳管TKE集群,EKS集群以及自建K8s集群的监控诉求。node

本文从TKE的架构出发,逐步介绍了整个监控系统的演进过程,包括早期的方案和遇到的问题,社区方案的瓶颈,咱们的改进原理等。web

TKE架构简介

为了让读者更好理解咱们的场景,咱们首先简单介绍一下TKE的基础架构。算法

TKE团队是公有云界首家采用Kubernetes in Kubernetes进行集群联邦管理的Kubernetes运营团队,其核心思想就是用一个Meta Cluster来托管其余集群的apiserver,controller-manager,scheduler,监控套件等非业务组件,在Meta Cluster中的组件对用户而言是隐藏的,以下图所示。api

img上图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系统中存储于告警。核心组件以下图。并发

img

Barad:云监控提供的多维监控系统,是云上其余服务主要使用的监控系统,其相对成熟稳定,可是不灵活,指标和label都须要提早在系统上设置好。负载均衡

Argus:云监控团队提供的多维业务监控系统,其特色是支持较为灵活的指标上报机制和强大的告警能力。这是TKE团队主要使用的监控系统。运维

数据流:ide

  • 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,而咱们但愿整个监控数据采集系统对用户是隐藏的。

这部分数据将经过控制台输出给用户

img

地域级别

成功采集到了属于每一个用户集群的数据,可是,对于一些地域级别的监控,包括

  • Meta Cluster中的管理组件
  • Cluster-monitor组件自身
  • 整个地域级别的集合信息,如总集群数,集群平均节点数,平均建立时间等数据

经过单个Cluster-monitor没法采集。须要构建更上一级的地域级别监控。

imgRegion Prometheus不只拉取如meta cluster operator,meta cluster service controller等核心组件的数据外,还经过Prometheus联邦接口拉取Cluster-monitor中的单集群数据进行二次聚合,产生地域级别集群的数据。地域级别数据直接存在本地,不写往Argus,由于这部分数据须要对接Grafana,由团队内部使用。img

全网级别

咱们在单地域监控的基础上又构建了一层全网级别的监控。用于监控

  • 支撑环境组件监控
  • 全部地域的Region Prometheus数据再聚合获得全网级别指标

img

全网数据也是给内部人员查看。img

架构总览

img

逐渐暴露出的问题

上述介绍的架构虽然解决了咱们对于大规模集群联邦的基本监控诉求,可是依旧存在几点不足。

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原生方式进行管理。

总体模型

假设咱们有了这么一个模型,那么咱们的监控就能够变成下面这种架构,在这种模型下,咱们拥有了单个地域下全部咱们要的原始数据。

img

  • 去掉了Cluster-monitor中的Prometheus
  • 去掉了Region Prometheus

高性能采集

这一节介绍咱们是如何实现理想模型中的高性能采集器的

Prometheus采集原理

各模块的关系

首先咱们先了解一下Prometheus的采集原理,为后面修改Prometheus实现高可用分片打下基础。下图展现了Prometheus采集时各模块的关系

img

  • 配置管理模块:该模块负责接收配置更新动做,全部依赖配置文件的模块,在初始化的时候都会向配置管理模块注册配置更新监听函数。
  • 服务发现模块:当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相关性

压测方法

img

压测数据

压测结论
  • target个数对Prometheus的总体负载影响不大

series规模压测

压测方法

img

压测数据

官方大规模集群各个资源产生的series

如下表格中的资源个数为Kubenetes官方给出的大规模集群应该包含的资源数 series个数经过统计cadvisor 和kube-state-metrics的指标得出

总计 5118w series。

压测结论
  • 当series数目高于300w时,Prometheus内存将暴增
  • 按等比例换算,单Prometheus采集300节点以上的集群时会内存会出现较大涨幅

实现可分片高可用Prometheus

有大量节点数目高于300的集群,经过前面的压测,单个Prometheus确实存在性能瓶颈。那咱们根据前面的采集原理,尝试修改Prometheus让其支持横向扩缩容。

设计原则

不管怎么修改,咱们但愿保持如下特性

  • 扩缩容时不断点
  • 负载均衡
  • 100%兼容原来的配置文件及采集能力

核心原理

再来回顾一下上边的采集原理图,看看咱们应该在哪一个地方进行修改。

img

从上图中,咱们发现,负载产生的源泉是target scraper,若是减小target scraper个数,就能减小总体采集到的series,从而下降负载。

假设咱们有多个Prometheus共享相同的配置文件,那么理论上他们产生出来的target scraper应当是如出一辙的。若是多个Prometheus之间可以相互协调,根据每一个target scraper抓取的目标数据量状况,分配这些target scraper,就是实现负载的均摊。以下图所示。

img

实现动态打散

  • 为了实现上述方案,咱们须要一个独立于全部Prometheus的负载协调器,协调器周期性(15s) 进行负载计算,该协调器负责收集全部target scraper的信息,以及全部Prometheus的信息,随后经过分配算法,为每一个Prometheus分配一些target scraper,最后将结果同步给全部Prometheus。
  • 相应的,每一个Prometheus须要添加一个本地协调模块,该模块负责和独立的协调器进行对接,上报本Prometheus经过服务发现发现的全部target,以及上一次采集获知的target的数据量,另外该模块也接受协调器下发的采集任务信息,用于控制本Prometheus应该开启哪些target scraper。

    img

targets分配算法

当协调器收集到全部target信息后,须要将target分配给全部Prometheus在分配时,咱们保持如下原则

  • 优先分配到正在采集该target的Prometheus
  • 负载尽量均衡

咱们最终采用了以下算法来分配target

  1. 规定target负载 = series * 每分钟采集次数。
  2. 将各个Prometheus的target信息进行汇总,获得全局信息,假设为global_targets,并所有标记为未分配。
  3. 计算每一个Prometheus理论上平均应该负责的采集负载,设为avg_load。
  4. 针对每一个Prometheus,尝试将其正在采集的target分配给他,前提是该Prometheus负载不超过avg_load,并将成功分配的target在global_targets中标记为已分配。
  5. 遍历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删除。咱们采用了如下方法进行缩容

img

  • 将多余的Prometheus标记为闲置,并记录当前时间。
  • 闲置的Prometheus上的target会所有被转移,而且再也不参与后续任务分配。
  • 当闲置Prometheus全部数据已上报远端(后续将介绍),将实例删除。
  • 特别的,若是在闲置过程当中,出现了扩容操做,则将闲置最久的实例从新取消闲置,继续参与工做。

高可用

在上述介绍的方案中,当某个Prometheus的服务不可用时,协调器会第一时间把target转移到其余Prometheus上继续采集,在协调周期很短(5s)的状况下,出现断点的概率实际上是很是低的。可是若是须要更高的可用性,更好的方法是进行数据冗余,即每一个targets都会被分配给多个Prometheus实例,从而达到高可用的效果。

关于存储的问题

到目前为止,咱们虽然将Prometheus的采集功能成功分片化,可是,各个Prometheus采集到的数据是分散的,咱们须要一个统一的存储机制,将各个Prometheus采集到的数据进行整合。

img

统一存储

在上一节最后,咱们引出,咱们须要一个统一的存储来将分片化的Prometheus数据进行存储。业界在这方面有很多优秀的开源项目,咱们选取了知名度最高的两个项目,从架构,接入方式,社区活跃度,性能等各方面作了调研。

Thanos vs Cortex

总体比较

Thanos简介

Thanos是社区十分流行的Prometheus高可用解决方案,其设计如图所示

img

从采集侧看,Thanos,利用Prometheus边上的Thanos sidecar,将Prometheus落在本地的数据盘上传至对象存储中进行远程存储,这里的Prometheus能够有多个,各自上报各自的数据。

查询时,优先从各Prometheus处查询数据,若是没查到,则从对象存储中查询历史数据,Thanos会将查询到的数据进行去重。Thanos的设计十分符合咱们前面的采集方案提到的统一存储。接入后如图所示。

img

Cortex简介

Cortex是Weavework公司开源的Prometheus兼容的TSDB,其原生支持多租户,且官方宣传其具备很是强大的性能,能存储高达2500万级别的series,其架构如图所示

img

从架构图不难发现,Cortex比Thanos要复杂得多,外部依赖也多,估计总体运维难度的比较大。Cortex再也不使用Prometheus自带的存储,而是让Prometheus经过remote write将数据所有写到Cortex系统进行统一的存储。Cortex经过可分片接收器来接收数据,随后将数据块存储到对象存储中,而将数据索引存储到Memcache中。

img

  • 从架构上来看,Cortex彷佛更加复杂,运维难度也高
  • 从接入方式看,Thanos对原来的Prometheus配置文件没有改动,属于无侵入方式,而Cortex须要在配置文件中加入remote write,另外目前版本的Prometheus没法经过参数关闭本地存储,因此即便只使用remote write存储到Cortex, Prometheus本地仍是会有数据。

社区现状

  • 从社区活跃度上看,Thanos表现更加优秀

性能压测

上文从架构角度对两个项目进行了一番对比,可是实际使用中,他两表现如何呢,咱们进行性能压测:

压测方式

img

img

咱们保持两个系统series总量老是拥有相同的变化,从查询性能,系统负载等多方面,去评估他们以前的优劣

压测结果
  • 稳定性:不一样数据规模下,组件是否正常工做

    img从数据上看 Thanos 更加稳定一些。

  • 查询性能:不一样数据规模下,查询的效率

    img从数据上看,Thanos的查询效率更高。

  • 未启用Ruler资源消耗:没有启动Ruler状况下,各组件的负载

    img就采集和查询而言,Thanos的资源消耗要比Cortex低不少。

在整个压测过程当中,咱们发现Cortex的性能远没有官方宣称的好,固然也多是咱们的调参不合理,可是这也反应出Cortex的使用难度极高,运维十分复杂(上百的参数),总体使用体验很是差。反观Thanos总体表现和官方介绍的较为相近,运维难度也比较低,系统较好把控。

选型

从前面的分析对比来看,Thanos不管是从性能仍是从社区活跃度,仍是从接入方式上看,较Cortex都有比较大的优点。因此咱们选择采用Thanos方案来做为统一存储。

Kvass系统总体实现

到目前为止,咱们经过实现可分片Prometheus加Thanos,实现了一套与原生Prometheus配置100%兼容的高性能可伸缩的Kvass监控系统。组件关系如图:

img

接入多个k8s集群

上图咱们只画了一套采集端(即多个共享同一份配置文件的Prometheus,以及他们的协调器),实际上系统支持多个采集端,即一个系统可支持多个Kubernetes集群的监控,从而获得多集群全局数据视图。

img

Kvass-operator

回顾旧版本监控在运维方法的不足,咱们但愿咱们的新监控系统有用完善的管理工具,且能用Kubernetes的方式进行管理。咱们决定使用operator模式进行管理,Kvass-operator就是整个系统的管理中心,它包含以下三种自定义资源

  • Thanos:定义了Thanos相关组件的配置及状态,全局惟一。
  • Prometheus: 每一个Prometheus定义了一个Prometheus集群的配置,例如其关联的Kubernetes集群基础信息,协调算法的一些阈值等
  • Notification: 定义了告警渠道,Kvass-operator负责根据其定义去更新云上告警配置

img

Prometheus-operator及集群内采集配置管理

因为Prometheus配置文件管理比较复杂,CoreOS开源了一个Prometheus-operator项目,用于管理Prometheus及其配置文件,它支持经过定义ServiceMonitor,PodMonitor这两种相比于原生配置文件具备更优可读性的自定义类型,协助用户生成最终的采集配置文件。

咱们但愿实现一种虚拟Prometheus机制,即每一个user cluster可以在本身集群内部管理其所对应的Prometheus采集配置文件,进行ServiceMonitor和PodMonitor的增删改查,也就是说,Prometheus就好像部署在本身集群里面同样。

为了达到这种效果,咱们引入并修改了Prometheus-operator。新版Prometheus-operator会链接上用户集群进行ServiceMonitor和PodMonitor的监听,并将配置文件生成在采集侧。

另外咱们将协调器和Prometheus-operator放在了一块儿。

img

基于Kvass的TKE监控方案

经过一步一步改进,咱们最终拥有了一套支持多集群采集,并支持扩缩容的高可用监控系统,咱们用其替换原来监控方案中的Cluster-monitor + Region Prometheus。实现了文章之初的诉求。

最第一版本

img

新方案

咱们上边介绍的方案,已经能够总体替换早期方案中的Region Prometheus及Cluster-monitor。如今咱们再加入一套Thanos,用于将全网数据进行整合。

img

相比于旧版本监控的指标预约义,新版本监控系统因为Prometheus是可扩缩容的,因此是能够支持用户上报自定义数据的。

总结

项目思路

Kvass的设计不是天马行空拍脑壳决定的,而是在当前场景下一些问题的解决思路所组成的产物。

客观看待旧版本

虽然咱们整篇文章就是在介绍一种用于取代旧版本监控的新系统,可是这并不意味着咱们以为旧版本监控设计得差劲,只是随着业务的发展,旧版本监控系统所面临的场景相较于设计之初有了较大变化,当时合理的一些决策,在当前场景下变得再也不适用而已。与其说是替换,不如称为为演进。

先设计模型

相比于直接开始系统落地,咱们更倾向于先设计系统模型,以及肯定设计原则,系统模型用于理清咱们究竟是要解决什么问题,咱们的系统应该由哪几个核心模块组件,每一个模块最核心要解决的问题是什么,有了系统模型,就等于有了设计蓝图和思路。

肯定设计原则

在系统设计过程当中,咱们尤其重视设计的原则,即不管咱们采用什么形式,什么方案,哪些特性是新系统必需要有的,对于Kvass而言,原生兼容是咱们首要的设计原则,咱们但愿不管咱们怎么设计,对用户集群而言,就是个Prometheus。

落地

在总体研发过程当中,咱们也踩了很多坑。Cortex的架构设计相比于thaos而言,采用了索引与数据分离的方式,设计上确实更加合理,理论上对于大规模数据,读取性能会更优,而且Cortex因为原生就支持多租户,实现了大量参数用于限制用户的查询规模,这点是Thanos有待增强的地方。咱们最初的方案也尝试采用Cortex来做为统一存储,可是在实际使用时,发现Cortex存在内存占用高,调参复杂等问题,而Thanos相比而言,性能较为稳定,也更加切近咱们的场景,咱们再结合压测报告,选择将存储切换为Thanos。

产品化

因为Kvass系统因此解决的问题具备必定普适性,TKE决定将其做为一个子产品对用户暴露,为用户提供基于Kvass的云原生系统,该产品目前已开放内测。

img

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!