Prometheus迷雾 - 无法获取Pod的label

问题背景

Prometheus在抓取container的CPU/Mem等metric的时候,发现metric上没有带Pod的label,这导致一个问题,无法通过自定义的label查看其下的所有metric资源。
例如下面的一个a-custom-project, 带了一个project: a-custom-project label

apiVersion: v1
kind: Pod
metadata:
  labels:
    project: a-custom-project 
  name: a-custom-project 
spec:
  containers:
    image: nginx
    imagePullPolicy: Always

希望project: a-custom-project label过滤出其下的所有metic指标,下面是一个container_cpu_usage_seconds_total的metric

正常情况下是不会出现红色框中的label,因此在grafana上也无法通过下面的方式显示

问题分析

导致此问题的出现主要的原因是kubelet在调用kubeGenericRuntimeManager创建container的时候,过滤掉所有的自定义的label,只添加了固定的几个指标。

kubernetes/pkg/kubelet/kuberuntime/labels.go

// newContainerLabels creates container labels from v1.Container and v1.Pod.
func newContainerLabels(container *v1.Container, pod *v1.Pod) map[string]string {
  labels := map[string]string{}
  labels[types.KubernetesPodNameLabel] = pod.Name
  labels[types.KubernetesPodNamespaceLabel] = pod.Namespace
  labels[types.KubernetesPodUIDLabel] = string(pod.UID)
  labels[types.KubernetesContainerNameLabel] = container.Name

  return labels
}

并且kubelet会过滤容器上的label标签,只保留规定的KubernetesPodNameLabel / KubernetesPodNameSpaceLabel / KubernetesContainerNameLabel 等label

kubernetes/pkg/kubelet/server/server.go

func containerPrometheusLabelsFunc(s stats.Provider) metrics.ContainerLabelsFunc {
  // containerPrometheusLabels maps cAdvisor labels to prometheus labels.
  return func(c *cadvisorapi.ContainerInfo) map[string]string {
    // Prometheus requires that all metrics in the same family have the same labels,
    // so we arrange to supply blank strings for missing labels
    var name, image, podName, namespace, containerName string
    if len(c.Aliases) > 0 {
      name = c.Aliases[0]
    }
    image = c.Spec.Image
    if v, ok := c.Spec.Labels[kubelettypes.KubernetesPodNameLabel]; ok {
      podName = v
    }
    if v, ok := c.Spec.Labels[kubelettypes.KubernetesPodNamespaceLabel]; ok {
      namespace = v
    }
    if v, ok := c.Spec.Labels[kubelettypes.KubernetesContainerNameLabel]; ok {
      containerName = v
    }

    // Associate pod cgroup with pod so we have an accurate accounting of sandbox
    if podName == "" && namespace == "" {
      if pod, found := s.GetPodByCgroupfs(c.Name); found {
        podName = pod.Name
        namespace = pod.Namespace
      }
    }
    set := map[string]string{
      metrics.LabelID:    c.Name,
      metrics.LabelName:  name,
      metrics.LabelImage: image,
      "pod":              podName,
      "namespace":        namespace,
      "container":        containerName,
    }
    return set
  }
}

此问题从2015年,k8s 1.1版本起就在社区开始有反馈,期间也不断有人issues,但是一直没有解决,社区主要考虑的有2点:
1)abel是一个很易变的数据,变化后会导致图标不连续(详见:https://www.robustperception.io/how-to-have-labels-for-machine-roles

2)过多的label会导致prometheus存储压力的增加,每个label都会创建一个timeseries,(详见:https://github.com/kubernetes/kubernetes/issues/79702)

image

references:https://github.com/kubernetes/kubernetes/issues/32326

影响范围

目前只影响cadvisor上指标,所有的container_*指标都存在此问题,例如fs / cpu / mem / net 等等容器指标。

解决方案一:联表聚合(社区方案)

社区的思想是减少容器的label,这块大大减轻prometheus的存储压力,并且认为label应该是恒定,在整个业务的生命周期中应该固定的,并且认为label是个易变,很容易导致业务的图标出现中断:

references:

基于上面这个思想,社区提供一个联表聚合的方案,这个方案也是社区提供的唯一方案(详见:https://www.weave.works/blog/aggregating-pod-resource-cpu-memory-usage-arbitrary-labels-prometheus/

大概的解决思路是:kube_state_metrics exporter提供一个kube_pod_labels的metric,这个metric里面有pod自定义的pod labels,然后和没有自定义label的metric进行联表聚合查询,从而实现了通过自定义label来过滤没有自定义label的metric的能力,由于是联表聚合查询,因此此能力是运行时的。

sum(irate(container_cpu_usage_seconds_total{image!=""}[1m])) by (namespace,pod)
    * on (namespace, pod) group_left(label_yfd_service)
kube_pod_labels{label_yfd_service="tutor-live-subjective-question"}

缓解方案

1)通过sharding方式,但是此方式无法按时间段存储在不同的prometheus,因此这种方式对单个merric来说是无效的。在metric固定的情况下,增加prometheus是可以起到一些缓解作用
2)通过recording rule方式,此方式通过将聚合查询的结果记录为一张新表,可以实现和原生label相同的查询速度,但是此方式对计算和存储的资源消耗很大,但是也可以通过增加prometheus来缓解。

解决方案二:修改kubelet源码方案

在社区有人提出通过白名单的给cadvisor添加label,但是社区没有下文(详见:https://github.com/google/cadvisor/issues/2380。在社区搜了一下有很多人都在提这个问题,最近还有一个人提了PR,但是被拒绝了(详见:https://github.com/kubernetes/kubernetes/pull/95210

Demo实现:直接通过硬编码 project 关键字 label,检查此 label 并将其传递到container 的 label 上。kubelet 暴露给 prometheus 的接口处也添加了一个检查,会将此 label 加入到返回的 set 集中,从而实现 promethues 能获取到pod 的 label。

更优方案:通过设置一个白名单列表,通过命令参数传入,这样既可以避免过多的 label 影响prometheus的存储,也可以实现灵活的 label 配置。

kubernetes/pkg/kubelet/kuberuntime/labels.go

// newContainerLabels creates container labels from v1.Container and v1.Pod.
func newContainerLabels(container *v1.Container, pod *v1.Pod) map[string]string {
  labels := map[string]string{}
  labels[types.KubernetesPodNameLabel] = pod.Name
  labels[types.KubernetesPodNamespaceLabel] = pod.Namespace
  labels[types.KubernetesPodUIDLabel] = string(pod.UID)
  labels[types.KubernetesContainerNameLabel] = container.Name
  
  // patch by zhaixigui

  for labelName ,_ := range pod.ObjectMeta.GetLabels() {
    if labelName == "project" {
      labels[labelName] = pod.ObjectMeta.Labels[labelName]
    }
  }

  return labels

kubernetes/pkg/kubelet/server/server.go

func containerPrometheusLabelsFunc(s stats.Provider) metrics.ContainerLabelsFunc {
  // containerPrometheusLabels maps cAdvisor labels to prometheus labels.
  return func(c *cadvisorapi.ContainerInfo) map[string]string {
    // Prometheus requires that all metrics in the same family have the same labels,
    // so we arrange to supply blank strings for missing labels
    var name, image, podName, namespace, containerName string
    if len(c.Aliases) > 0 {
      name = c.Aliases[0]
    }
    image = c.Spec.Image
    if v, ok := c.Spec.Labels[kubelettypes.KubernetesPodNameLabel]; ok {
      podName = v
    }
    if v, ok := c.Spec.Labels[kubelettypes.KubernetesPodNamespaceLabel]; ok {
      namespace = v
    }
    if v, ok := c.Spec.Labels[kubelettypes.KubernetesContainerNameLabel]; ok {
      containerName = v
    }

    // Associate pod cgroup with pod so we have an accurate accounting of sandbox
    if podName == "" && namespace == "" {
      if pod, found := s.GetPodByCgroupfs(c.Name); found {
        podName = pod.Name
        namespace = pod.Namespace
      }
    }
    set := map[string]string{
      metrics.LabelID:    c.Name,
      metrics.LabelName:  name,
      metrics.LabelImage: image,
      "pod":              podName,
      "namespace":        namespace,
      "container":        containerName,
    }

    // patch by xigui

    for labelName,labelValue := range c.Spec.Labels {
      if labelName == "project" {
        set[labelName] = labelValue
      }
    }

    return set
  }
}

解决方案三:阿里云 - 大数据

阿里云将 Promsql 查询时间小于2秒的用社区的 kube_pod_label 联表聚合的方案,而大于2秒的查询用大数据平台处理(类似Hbase),有大数据平台保证查询的响应时间。

解决方案四:独立安装cAdvisor

社区有 member 想将 cAdvisor 独立出来,为了解决 kubelet 的性能问题,但是最后也不了了之。(详解:https://github.com/kubernetes/kubernetes/issues/18770

另外有一个人将 cAdvisor 的部分统计信息放到了CRI里,试图将 kubelet 和 cadvisor 分离,但是此PR并不是解决 Pod 的 label 传递到 container 上的问题(详见:https://github.com/kubernetes/kubernetes/pull/45614),此方案没有最终的实现。

性能测试

prometheus查询container_cpu_usage_seconds_total metric,实例数5个,最大查询时间2天

1)联表查询(单位:毫秒)

sum(irate(container_cpu_usage_seconds_total{image!=""}[1m])) by (namespace,pod)
    * on (namespace, pod) group_left(label_yfd_service)
kube_pod_labels{label_yfd_service="tutor-live-subjective-question"}

2d: 1273, 1312, 1277, 1258, 1320
1h: 311, 396, 367, 365, 2882)原生查询(单位:毫秒)

2)原生查询

sum(irate(container_cpu_usage_seconds_total{image!="", yfd_service="tutor-live-subjective-question"}[1m])) by (namespace,pod)

2d: 71, 73, 78, 76, 85
1h: 39, 43, 45, 50, 40

对比:通过直接过滤label和kube_pod_labels连表查询的返回响应

2d: 平均相差1212毫秒,相差16倍
1h:平均相差302毫秒, 相差7倍

方案对比

修改源码 联表聚合 大数据
优点 简单,性能高 简单,符合社区方向 在不改源码的情况,解决了「联表聚合」性能问题
缺点 修改了kubelet源码 响应时间很慢,数据量大了很难处理 需要一个大数据平台,对维护和成本来说都是巨大的挑战
响应时间 很慢
架构复杂性 简单 简单 非常复杂
开发成本
资源要求
符合社区方向 不符合 符合 -
稳定性

其他公司的实践

公司 规模 选用方案 说明
快手 超大 放弃prometheus 经过几次修改prometheus源码后均不能解决其大数据量的问题
瓜子 联表聚合 规模较小,提升性能用rule
马蜂窝 联表聚合 规模小,对基于自定义的label查询需求不大

对kubernetes云原生方面的故障排查、解决方案、优化调优感兴趣的朋友可以关注喜贵的云原生

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341