中国卓越技术团队访谈录

优秀的产品背后,必定有优秀的团队做支撑。

InfoQ 编辑部

vivo AI计算平台的监控高可用方案

一、背景

2018 年底,vivo AI 研究院为了解决统一高性能训练环境、大规模分布式训练、计算资源的高效利用调度等痛点,着手建设 AI 计算平台。经过两年的持续迭代,平台建设和落地取得了很大进展,成为 vivo AI 领域的核心基础平台。平台从当初服务深度学习训练为主,到现在演进成包含 VTraining、VServing、VContainer 三大模块,对外提供模型训练、模型推理和容器化能力。VContainer是计算平台的底座,是基于Kubernetes构建的容器平台,具备资源调度、弹性伸缩、零一混部等核心能力。VContainer的容器集群有上千个节点,拥有超过100PFLOPS的GPU算力。集群里同时运行着上千个VTraining的训练任务和上百个VServing的推理服务。本文主要分享了VContainer的监控高可用方案的选型和部署实践,以及各种踩坑经验。

二、为什么选择thanos

2.1 promethues介绍

Prometheus是新一代的云原生监控系统,始于2012年,并于2016年继Kubernetes之后成为第二个正式加入CNCF基金会的项目,天生完美支持Kubernetes。Prometheus提供强大的数据采集、数据存储、数据展示、告警等功能,并且很多设计思想都来源于Google内部的监控系统Borgmon。

2.2 prometheus瓶颈

vivo ai计算平台之前的方案采用了Prometheus + AlertManager + Grafana,此方案存在以下问题:

1.服务都是单副本的,如果故障会导致相关功能不可用;

2.prometheus做的事情太多,指标采集,复杂查询,告警检测,数据压缩/存储等,出问题的概率也比较大;

3.集群规模变大,prometheus处理的数据更多,内存也消耗更多,容易OOM。

2.3 几种监控高可用方案的比较

由此可见,thanos虽然性能非最优,但架构简单,使用/维护方便,且有较完善的社区支持。所以我们选择了thanos作为集群监控的高可用方案。

三、thanos部署实践

3.1 thanos介绍

Thanos是由多个组件构成的监控高可用系统,并且有着不受限制的数据存储能力,可以无缝集成到现有的Prometheus上。Thanos使用Prometheus 2.0存储格式,把历史数据以相对高性价比的方式保存在对象存储里,同时兼有较快的查询速度。此外,它还能对你所有的Prometheus提供全局查询视图。

Thanos的目标:

1. 指标全局查询视图;

2. 指标无限期保留;

3. 组件的高可用性(包含Prometheus)。

3.2 thanos组件

sidecar

sidecar是伴随prometheus的主要组件,部署时sidecar容器和prometheus容器在同一个pod里。sidecar主要功能有:1.读取和归档对象存储中的数据;2.管理prometheus的配置和生命周期;3.将外部标签注入prometheus配置中,并区分Prometheus的副本;4.调用prometheus的promQL接口,提供给thanos query查询。

ruler

除了Prometheus的规则外,Thanos Ruler基本上执行与查询器相同的操作。唯一的区别是它可以与Thanos组件进行通信。ruler是可选组件,可根据需求评估是否使用。我们大部分的告警使用了prometheus自身的rule功能,因为告警需要最新的指标。prometheus副本数量的告警,可以使用ruler实现。

query

query是thanos的指标查询入口,使用grafana或prometheus client查询时,均可用query地址取代prometheus地址。可以部署多个副本,实现query的高可用。query的主要功能有:1.监听HTTP并将查询转换为Thanos gRPC格式;2.汇总来自不同来源的查询结果,并且可以从Sidecar和Store中读取数据;3.在高可用设置中,Thanos Query可以对查询结果进行去重。

store gateway

store gateway将对象存储的数据暴露给thanos query去查询。store gateway在对象存储桶中的历史数据之上实现store API,主要充当API网关,因此不需要大量的本地磁盘空间。在启动时加入Thanos集群,并暴露其可以访问的数据。store gateway保存了少量本地磁盘远程块与存储桶的同步信息,通常可以安全地在重新启动期间删除数据,但这样做会增加启动时间。

compact

compact使用Prometheus 2.0存储引擎,对对象存储中的数据进行压缩分块。通常不与安全语义并发,确保每个存储桶必须部署一个。compact还会进行数据下采样,40小时后5分钟下采样,10天后1小时下采样。下采样能加速大时间区间监控数据查询的速度。

3.3 thanos 部署

部署前需要对thanos进行测试,需确保以下功能正常。a.grafana/Querier数据展示正常;b.query读去重;c.query分片合并功能;d.历史数据(S3)和近期数据(2小时内)的读写;e.ruler告警;f.停掉一个/多个prometheus时,监控系统的可用性。

集群原有的监控采用了prometheus + grafana + exporter的方案。因为k8s的组件都是松耦合的,且thanos的prometheus需要和sidecar一同部署。所以部署thanos共用了之前的expoter,其余组件均另外单独部署。thanos部署的yaml和细节可参考https://www.metricfire.com/blog/ha-kubernetes-monitoring-using-prometheus-and-thanos/

四、监控踩坑记录

4.1 thanos相关

4.1.1 thanos-query查询出错

thanos-query查不到数据,报错“No store matched for this query”,目前只能重启解决。重启prometheus可能导致这个问题。

4.1.2 存储依赖

thanos的历史数据需要上传到对象存储S3中,但近两小时的数据是没有上传的。S3存储我们采用了vivo对象存储(轩辕),近期存储我们采用了glusterfs集群的高可用卷。thanos S3配置可参考,https://thanos.io/tip/thanos/storage.md/#s3

4.1.3 thanos-compact崩溃,报错"syncing metas: BaseFetcher: iter bucket: Truncated response should have continuation token set"

实践中发现写入到vivo对象存储的数据足够多时(对象数量超过1000),thanos-compact会崩溃并报错。原因是对象存储服务端只支持老版本接口listObjects,不支持新接口listObjectsV2。listObjectsV2要求IsTruncated为true的时候,同时设置NextContinuationToken参数。解决方法对对象存储服务端进行升级修复。可参考issue,https://github.com/minio/mc/issues/3067

4.1.4 thanos-compact配置监控数据保留时间

thanos支持无限扩展存储容量,但考虑到存储的成本,以及较长远的历史数据没有价值。调整不同分辨率数据的保存时间,避免对象存储增长过快。

thanos-compact.yaml

- “–retention.resolution-raw=7d”

- “–retention.resolution-5m=31d”

- “–retention.resolution-1h=0d”

可参考issue,https://github.com/thanos-io/thanos/issues/813

4.2 prometheus相关

4.2.1 指标重复

更新节点label后的几分钟内,prometheus会采集到重复的metrics,可能会导致promQL查询语句失败。在社区中提了issue,https://groups.google.com/g/prometheus-users/c/WK-wROf3e3g/m/RKy40nFrAQAJ。或许跟使用的prometheus/node-exporter版本比较老有关系,可以尝试使用较新的版本。

4.2.2 内存控制

集群规模扩大,prometheus采集的数据增加,导致其使用内存增加,OOM风险变大,存储占用增加。并不是所有的容器指标都是有用的,可以根据集群监控/运维的需求进行指标筛选,筛掉大部分不需要的指标。通过prometheus configmap的relabel_config配置相应指标的保留与删除,对于kube-state-metrics可以直接在启动参数里配置开启相应的collector以及采集标签的白名单。

4.3 exporter相关

4.3.1 cadvisor cpu利用率过高

部分cadvisor cpu利用率过高,导致拉取不到容器指标。社区相关issue:https://github.com/google/cadvisor/issues/1774。原因是读取cgroup的memory.stat耗费了较多时间,内核内存延迟释放,把内核版本更新到4.19及以上就能解决。

4.3.2 节点网络指标异常

部分node-exporter采集到的网络收发指标过大,给社区提了issue,https://github.com/prometheus/node_exporter/issues/1849。社区反馈是内核的问题,node-exporter只是上报了内核的数据,没有做任何数据变换。节点的内核版本是3.10.0,建议使用更高版本的内核。

4.3.3 确保exporter不被驱逐

节点负荷高的时候,节点上的pod会被k8s驱逐。监控exporter作为基础组件,应确保不被驱逐。若将exporter的qosClass设置成guranteed,又缺乏资源使用的灵活性。因此我们将node-exporter/cadvisor/dcgm设置成critical pod。方法有两种:1.annotations里配置scheduler.alpha.kubernetes.io/critical-pod: “”;2.设置priorityClassName: system-node-critical,需要exporter部署在kube-system下。

4.3.4 prometheus target cadvisor报错“Get http://10.193.180.18:4194/metrics: context deadline exceeded”

指标获取失败,通常是由于为容器分配的cpu/内存数不足导致的,增加资源即可解决。

4.4 容器内存

容器内存的使用量我们采用了目前大家公认的container_memory_working_set_bytes指标,因为working_set是kubelet判断OOM的依据。但working_set和容器内用top观察,以及程序运行在物理机上的内存都会有差距。大部分情况差距不大,少数情况下会有较大差距,并不是容器指标不准,而是计算方式不同导致的。下面介绍几个我们遇到的case。

4.4.1 file cache引起的误差

某线上业务,容器内top命令看到使用63GiB,容器监控看内存77GiB,相差14GiB。原因是读写文件产生了二十多G的file cache,top RES不包含file cache,working_set包含近期file cache。通过查看/sys/fs/cgroup/memeory/memory.stat,可以计算得到top RES = total_rss,working_set = total_rss + total_cache - total_inactive_file。

4.4.2 共享内存重复计算

某线上业务,容器内top统计消耗约1.2GiB,容器监控查看约1GiB。原因是RES包含共享内存SHR,多个进程使用同样的共享内存,SHR被重复计算了。减去被重复计算的SHR,即可得到正确的值。

4.4.3 calico占用大量内存被OOMKilled

calico占用大量内存,容器OOM,导致业务域名不可用,业务受影响。原因是kmem未关闭,导致/sys/fs/cgroup/memory/memory.kmem.usage_in_bytes值非0且较大,而容器内存是把这个值加进去了的,所以导致OOM。calico本身并没有内存泄漏。解决方案是disable掉kmem,细节可以参考之前的文章https://www.infoq.cn/article/2LCOXVLD0WxDN4itXj35

4.4.4 容器CPU/内存利用率计算

以内存利用率为例,计算规则常采用container_memory_working_set_bytes / memory.limit_in_bytes。对于pod,k8s存在两个资源限制,request和limit,二者常不相等。

当涉及资源超卖,limit做分母是合理的。因为request会设置为一个较小的值,便于调度。request作分母会导致利用率超100%。

当涉及弹性伸缩,k8s计算pod利用率,是以request做为分母的,可参见代码实现,https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/metrics/utilization.go

当limit > request,基于POD的CPU/内存利用率做弹性伸缩时,是会出现利用率大于100%的情况。

作者介绍

汪凯,目前是 vivo AI 研究院计算平台组的工程师,关注 K8s、容器等云原生技术。