概述
不久前,我们在文章《如何扩展单个Prometheus实现近万Kubernetes集群监控?》中详细介绍了TKE团队大规模Kubernetes联邦监控系统Kvass的演进过程,其中介绍了针对规模较大的集群,我们是如何通过修改Prometheus代码来实现横向扩缩容的。经过方案上的改进,Kvass目前已经支持以Sidecar的方式实现Prometheus集群化,而不是修改Prometheus代码。由于方案对社区有一定价值,团队决定将项目开源出来,分享给社区。项目地址
本文首先将给出Prometheus的单机性能瓶颈,以及现有的社区集群化方案,随后将详细介绍开源版Kvass的设计思想,使用案例及压测结果。
另外,腾讯云容器团队在Kvass的设计思想上进一步优化,构建了支持多集群的高性能云原生监控服务,产品目前已正式公测,欢迎读者试用。传送门: https://console.cloud.tencent.com/tke2/prometheus
后续章节我们也将直接使用该产品来展示Kvass对大规模集群的监控能力。
Prometheus
Prometheus依靠其强劲的单机性能,灵活的PromSQL,活跃的社区生态,逐渐成为云原生时代最核心的监控组件,被全球各大产商用于监控他们的核心业务。
然而,面对大规模监控目标(数千万series)时,由于原生Prometheus只有单机版本,不提供集群化功能,开发人员不得不通过不断增加机器的配置来满足Prometheus不断上涨的内存。
单机性能瓶颈
我们对单机Prometheus进行的压测,用以探测单个Prometheus分片的合理负载,压测的目标有两个。
- 确定target数目对Prometheus负载的关系
- 确定series数目和Prometheus负载的关系
target相关性
我们保持总series为100万不变, 通过改变target个数,观察Prometheus负载变动。
压测结果
target数量 | CPU (core) | mem (GB) |
---|---|---|
100 | 0.17 | 4.6 |
500 | 0.19 | 4.2 |
1000 | 0.16 | 3.9 |
5000 | 0.3 | 4.6 |
- 从表中我们发现target数目的改动对Prometheus负载的影响并不是强相关的。在target数目增长50倍的情况下,CPU消耗有小量增长,但是内存几乎不变。
series相关性
我们保持target数目不变,通过改变总series数,观察Prometheus的负载变动。
压测结果
series数量 (万) | CPU (core) | mem (GB) | 查询1000 series 15m数据(s) |
---|---|---|---|
100 | 0.191 | 3.15 | 0.2 |
300 | 0.939 | 20.14 | 1.6 |
500 | 2.026 | 30.57 | 1.5 |
- 从表中,Prometheus的负载受到series的影响较大,series越多,资源消耗越大。
- 当series数据超过300万时,Prometheus内存增长较为明显,需要使用较大内存的机器来运行。
压测过程中,我们使用了工具去生成预期数目的series,工具生成的series每个label的长度及值的长度都较小,固定为10个字符左右。我们的目的是观察相对负载变化,实际生产中由于label长度不同,服务发现机制的消耗不同,相同的series数目所消耗的负载会比压测中高不少。
现有集群化方案
针对单机Prometheus在大规模数据监控时的性能瓶颈问题,社区目前已经存在一些分片化方案,主要包括以下几种。
hash_mod
Prometheus官方支持通过Relabel机制,在配置文件中,对采集上来的数据进行hash,通过在不同Prometheus实例的配置文件中指定不同的moduleID来进行分片化,然后通过联邦,Thanos等方式将数据进行统一汇总,如下图所示,读者也可以直接参考【官方文档】。
配置文件分割
还有一种方法是根据业务进行job层面的分割,不同Prometheus使用完全独立的采集配置,其中包含了不同的job,。
上述方案存在的问题
无论是hash_mod的方式,还是配置文件分割的方式,其本质都是将数据切分到多个采集配置中,由不同Prometheus进行采集。两者都存在以下几个缺点。
- 对预监控数据要有所了解:使用上述方法的前提是使用者必须对监控对象会上报的数据有所了解,例如必须知道监控对象会上报某个用于hash_mod的label,或者必须知道不同job的整体规模,才能对job进行划分。
- 实例负载不均衡:虽然上述方案预期都是希望将数据打散到不同Prometheus实例上,但实际上通过某些label的值进行hash_mod的,或者干脆按job进行划分的方式并不能保证每个实例最终所采集的series数是均衡的,实例依旧存在内存占用过高的风险。
- 配置文件有侵入:使用者必须对原配置文件进行改造,加入Relabel相关配置,或者将一份配置文件划分成多份,由于配置文件不再单一,新增,修改配置难度大大增加。
- 无法动态扩缩容:上述方案中的由于配置是根据实际监控目标的数据规模来特殊制定的,并没有一种统一的扩缩容方案,可以在数据规模增长时增加Prometheus个数。当然,用户如果针对自己业务实际情况编写扩缩容的工具确实是可以的,但是这种方式并不能在不同业务间复用。
- 部分API不再正常:上述方案将数据打散到了不同实例中,然后通过联邦或者Thanos进行汇总,得到全局监控数据,但是在不额外处理的情况下会导致部分Prometheus 原生API无法得到正确的值,最典型的是/api/v1/targets ,上述方案下无法得到全局targets值。
Kvass的原理
设计目标
针对上述问题,我们希望设计一种无侵入的集群化方案,它对使用者表现出来的,是一个与原生Prometheus配置文件一致,API兼容,可扩缩容的虚拟Prometheus。具体而言,我们有以下设计目标。
- 无侵入,单配置文件:我们希望使用者看到的,修改的都是一份原生的配置文件,不用加任何特殊的配置。
- 无需感知监控对象:我们希望使用者不再需要预先了解采集对象,不参与集群化的过程。
- 实例负载尽可能均衡:我们希望能根据监控目标的实际负载来划分采集任务,让实例尽可能均衡。
- 动态扩缩容:我们希望系统能够根据采集对象规模的变化进行动态扩缩容,过程中数据不断点,不缺失。
- 兼容核心PrometheusAPI:我们希望一些较为核心的API,如上边提到的/api/v1/target接口是正常的。
架构
Kvass由多个组件构成,下图给出了Kvass的架构图,我们在架构图中使用了Thanos,实际上Kvass并不强依赖于Thanos,可以换成其他TSDB。
- Kvass sidecar: 用于接收Coordinator下发的采集任务,生成新的配置文件给Prometheus,也服务维护target负载情况。
- Kvass coordinator: 该组件是集群的中心控制器,负责服务发现,负载探测,targets下发等。
- Thanos 组件: 图中只使用了Thanos sidecar与Thanos query,用于对分片的数据进行汇总,得到统一的数据视图。
Coordinator
Kvass coordinaor 首先会代替Prometheus对采集目标做服务发现,实时获得需要采集的target列表。
针对这些target,Kvass coordinaor会负责对其做负载探测,评估每个target的series数,一旦target负载被探测成功,Kvass coordinaor 就会在下个计算周期将target分配给某个负载在阈值以下的分片。
Kvass coordinaor 还负责对分片集群做扩缩容。
服务发现
Kvass coordinaor引用了原生Prometheus的服务发现代码,用于实现与Prometheus 100%兼容的服务发现能力,针对服务发现得到的待抓取targets,Coordinaor会对其应用配置文件中的relabel_configs进行处理,得到处理之后的targets及其label集合。服务发现后得到的target被送往负载探测模块进行负载探测。
负载探测
负载探测模块从服务发现模块获得处理之后的targets,结合配置文件中的抓取配置(如proxy,证书等)对目标进行抓取,随后解析计算抓取结果,获得target的series规模。
负载探测模块并不存储任何抓取到的指标数据,只记录target的负载,负载探测只对target探测一次,不维护后续target的负载变化,长期运行的target的负载信息由Sidecar维护,我们将在后面章节介绍。
target分配与扩容
在Prometheus单机性能瓶颈那一节,我们介绍过Prometheus的内存和series相关,确切来说,Prometheus的内存和其head series直接相关。Prometheus 会将最近(默认为2小时)采集到的数据的series信息缓存在内存中,我们如果能控制好每个分片内存中head series的数目,就能有效控制每个分片的内存使用量,而控制head series实际就是控制分片当前采集的target列表。
基于上边的思路,Kvass coordinaor会周期性的对每个分片当前采集的target列表进行管理:分配新target,删除无效target。
在每个周期,Coordinaor会首先从所有分片获得当前运行状态,其中包括分片当前内存中的series数目及当前正在抓取的target列表。随后针对从服务发现模块得到的全局target信息进行以下处理
- 如果该target已经被某个分片抓取,则继续分配给他,分片的series数不变。
- 如果该target没有任何分片抓取,则从负载探测模块获得其series(如果还未探测完则跳过,下个周期继续),从分片中挑一个目前内存中series加上该target的series后依然比阈值低的,分配给他。
- 如果当前所有分片没法容纳所有待分配的targets,则进行扩容,扩容数量与全局series总量成正比。
target迁移和缩容
在系统运行过程中,target有可能会被删除,如果某个分片的target被删除且超过2小时,则该分片中的head series就会降低,也就是出现了部分空闲,因为target分配到了不同分片,如果有大量target被删除,则会出现很多分片的内存占用都很低的情况,这种情况下,系统的资源利用率很低,我们需要对系统进行缩容。
当出现这种情时,Coordinaor会对target进行迁移,即将序号更大的分片(分片会从0进行编号)中的target转移到序号更低的分片中,最终让序号低的分片负载变高,让序号高的分片完全空闲出来。如果存储使用了thanos,并会将数据存储到cos中,则空闲分片在经过2小时候会删除(确保数据已被传到cos中)。
多副本
Kvass的分片当前只支持以StatefulSet方式部署。
Coordinator将通过label selector来获得所有分片StatefulSet,每个StatefulSet被认为是一个副本,StatefulSet中编号相同的Pod会被认为是同一个分片组,相同分片组的Pod将被分配相同的target并预期有相同的负载。
/api/v1/targets接口
上文提到Coordinator根据配置文件做了服务发现,得到了target列表,所以Coordinator实际上可以得到/api/v1/targets接口所需要的返回结果集合,但是由于Coordinator只做了服务发现,并不进行实际采集,所以target的采集状态(例如健康状态,上一次采集时间等)都无法直接得知。
当Coordinator接收到/api/v1/targets请求时,他会基于服务发现得到的target集合,结合向Sidecar(如果target已分配)或向探测模块(target还未分配)询问target采集状态,综合后将正确的/api/v1/targets结果返回。
Sidecar
上一节介绍了Kvass coordinaor的基本功能,要想系统正常运行,还需要Kvass sidecar的配合,其核心思想是将配置文件中所有服务发现模式全部改成static_configs并直接将已经relabel过的target信息写入配置中,来达到消除分片服务发现和relabel行为,只采集部分target的效果。
每个分片都会有一个Kvass sidecar,其核心功能包括从Kvass coordinator接受本分片负责的target列表,生成新的配置文件给该分片的Prometheus使用。另外,Kvass sidecar还会劫持抓取请求,维护target最新负载。Kvass sidecar还作为PrometheusAPI的网关,修正部分请求结果。
配置文件生成
Coordinaor经过服务发现,relabel及负载探测后,会将target分配给某个分片,并将target信息下发给Sidecar,包括
- target的地址,
- target预估的series值
- target的hash值
- 处理完relabel之后的label集合。
Sidecar根据从Coordinator得到的target信息,结合原始配置文件,生成一个新的配置文件给Prometheus使用,这个新的配置文件做了如下改动。
- 将所有服务发现机制改为static_configs模式,并直接写入target列表,每个target包含经过relabel之后的label值
- 由于现在target已经relabel过了,所以删除job配置中的relabel_configs项,但是依旧保留metrics_rebale_configs
- 将target的label中的scheme字段全部替换成http,并将原schme以请求参数的形式加入到label集合中
- 将target的job_name以请求参数的形式加入到label集合中* 注入proxy_url将所有抓取请求代理到Sidecar
我们来看一个例子,假如原来的配置是一个kubelet的采集配置
global:
evaluation_interval: 30s
scrape_interval: 15s
scrape_configs:
- job_name: kubelet
honor_timestamps: true
metrics_path: /metrics
scheme: https
kubernetes_sd_configs:
- role: node
bearer_token: xxx
tls_config:
insecure_skip_verify: true
relabel_configs:
- separator: ;
regex: __meta_kubernetes_node_label_(.+)
replacement: $1
action: labelmap
通过注入将生成一个新的配置文件
global:
evaluation_interval: 30s
scrape_interval: 15s
scrape_configs:
- job_name: kubelet
honor_timestamps: true
metrics_path: /metrics
scheme: https
proxy_url: http://127.0.0.1:8008 # 所有抓取请求代理到Sidecar
static_configs:
- targets:
- 111.111.111.111:10250
labels:
__address__: 111.111.111.111:10250
__metrics_path__: /metrics
__param__hash: "15696628886240206341"
__param__jobName: kubelet
__param__scheme: https # 保存原始的scheme
__scheme__: http # 设置新的scheme,这将使得代理到Sidecar的抓取请求都是http请求
# 以下是经过relabel_configs处理之后得到的label集合
beta_kubernetes_io_arch: amd64
beta_kubernetes_io_instance_type: QCLOUD
beta_kubernetes_io_os: linux
cloud_tencent_com_auto_scaling_group_id: asg-b4pwdxq5
cloud_tencent_com_node_instance_id: ins-q0toknxf
failure_domain_beta_kubernetes_io_region: sh
failure_domain_beta_kubernetes_io_zone: "200003"
instance: 172.18.1.106
job: kubelet
kubernetes_io_arch: amd64
kubernetes_io_hostname: 172.18.1.106
kubernetes_io_os: linux
上边新生成的配置文件是Prometheus真正使用的配置文件,Sidecar通过Coordinator下发的target列表来生成配置,就可以让Prometheus有选择性得进行采集。
抓取劫持
在上边的配置生成中,我们会将proxy注入到job的配置中,并且target的label中,scheme会被设置成http,所以Prometheus所有的抓取请求都会被代理到Sidecar,之所以要这么做,是因为Sidecar需要维护每个target新的series规模,用于Coordinator查阅后作为target迁移的参考。
从上边配置生成我们可以看到,有以下几个额外的请求参数会被一并发送到Sidecar
- hash:target的hash值,用于Sidecar识别是哪个target的抓取请求,hash值由Coordinator根据target的label集合进行计算获得并传递给Sidecar。
- jobName:是哪个job下的抓取请求,用于Sidecar根据原配置文件中job的请求配置(如原proxy_url,证书等)对抓取目标发起真正的请求。
- scheme:这里的scheme是target通过relabel操作之后最终得到的协议值,虽然在job配置文件中已经有scheme字段,但Prometheus配置文件依旧支持通过relabel指定某个target的请求协议。在上述生成新配置过程中,我们将真实的scheme保存到这个参数里,然后将scheme全部设置成http。
有了上述几个参数,Sidecar就可以对抓取目标发起正确的请求,并得到监控数据,在统计的target这次抓取的series规模后,Sidecar会将监控数据拷贝一份给Prometheus。
API代理
由于Sidecar的存在,部分发往Prometheus的API请求需要被特殊处理,包括
- /-/reload:由于Prometheus真正使用的配置文件由Sidecar生成,针对该接口,需要由Sidecar去处理并在处理成功后调用Prometheus的/-/reload接口。
- /api/v1/status/config:该接口需要由Sidecar处理并把原配置文件返回。
- 其他接口直接发往Prometheus。
全局数据视图
由于我们将采集目标分散到了不同分片中,导致每个分片的数据都只是全局数据的一部分,所以我们需要使用额外的组件来将所有数据进行汇总并去重(多副本的情况下),得到全局数据视图。
以thanos为例
thanos是一个非常好的方案,通过加入thanos组件,可以很方便得得到kvass集群的全局数据视图。当然我们也可以通过加入remote writer配置来使用其他TSDB方案,例如influxdb,M3等等。
使用例子
这一节我们通过一个部署例子,来直观感受一下Kvass的效果,相关yaml文件可以在这里找到https://github.com/tkestack/kvass/tree/master/examples
读者可以将项目clone到本地,并进入examples。
git clone https://github.com/tkestack/kvass.git
cd kvass/examples
部署数据生成器
我们提供了一个metrics数据生成器,可以指定生成一定数量的series,在本例子中,我们将部署6个metrics生成器副本,每个会生成10045 series (其中45 series为golang的metrics)。
kubectl create -f metrics.yaml
部署kvass
现在我们部署基于Kvass的Prometheus集群,用以采集这6个metrics生成器的指标。
首先我们部署rbac相关配置
kubectl create -f kvass-rbac.yaml
接着部署一个Prometheus config文件,这个文件就是我们的原始配置,我们在这个配置文件中,使用kubernetes_sd来做服务发现
kubectl create -f config.yaml
配置如下
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: custom
scrape_configs:
- job_name: 'metrics-test'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
regex: metrics
action: keep
- source_labels: [__meta_kubernetes_pod_ip]
action: replace
regex: (.*)
replacement: ${1}:9091
target_label: __address__
- source_labels:
- __meta_kubernetes_pod_name
target_label: pod
现在我们来部署Kvass coordinator
kubectl create -f coordinator.yaml
我们在Coordinator的启动参数中设置每个分片的最大head series数目不超过30000
--shard.max-series=30000
我们现在就可以部署带有Kvass sidecar的Prometheus了,这里我们只部署单个副本
kubectl create -f prometheus-rep-0.yaml
部署thanos-query
为了得到全局数据,我们需要部署一个thanos-query
kubectl create -f thanos-query.yaml
查看结果
根据上述计算,监控目标总计6个target, 60270 series,根据我们设置每个分片不能超过30000 series,则预期需要3个分片。
我们发现,Coordinator成功将StatefulSet的副本数改成了3。
我们看下单个分片内存中的series数目,发现只有2个target的量
我们再通过thanos-query来查看全局数据,发现数据是完整的(其中metrics0为指标生成器生成的指标名)
云原生监控
腾讯云容器团队在Kvass的设计思想上进一步优化,构建了高性能支持多集群云原生监控服务,产品目前已正式公测。
大集群监控
这一节我们就直接使用云原生监控服务来监控一个规模较大的真实集群,测试一下Kvass监控大集群的能力。
集群规模
我们关联的集群规模大致如下
- 1060个节点
- 64000+ Pod
- 96000+ container
采集配置
我们直接使用云原生监控服务在关联集群默认添加的采集配置,目前已包含了社区主流的监控指标:
- kube-state-metrics
- node-exporer
- kubelet
- cadvisor
- kube-apiserver
- kube-scheduler
- kube-controler-manager
测试结果
- 总计3400+target, 2700+万series
- 总计扩容了17个分片
- 每个分片series稳定在200w以下
- 每个分片消耗内存在6-10G左右
云原生监控所提供的默认Grafana面板也能正常拉取
targets列表也能正常拉取
多集群监控
值得一提的是,云原生监控服务不仅支持监控单个大规模集群,还可以用同个实例监控多个集群,并支持采集和告警模板功能,可一键将采集告警模板下发至各地域各个集群,彻底告别了每个集群重复添加配置的问题。
总结
本文从问题分析,设计目标,原理剖析,使用案例等方面详细介绍了一种开源Prometheus集群化技术,可在不修改Prometheus代码的前提下使其支持横向扩缩容,从而监控单机Prometheus无法监控的大规模集群。
【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,关注同名公众号,及时获取更多干货!!
有疑问加站长微信联系(非本文作者)