Bootstrap

Kubernetes - Configmap热更新原理

GitHub地址: https://github.com/QingyaFan/container-cloud/issues/2

Kubernetes中提供configmap,用来管理应用的配置,configmap具备热更新的能力,但只有通过目录挂载的configmap才具备热更新能力,其余通过环境变量,通过subPath挂载的文件都不能动态更新。这篇文章里我们来看看configmap热更新的原理,以及为什么只有目录形式挂载才具备热更新能力。

configmap热更新原理

我们首先创建一个configmap(configmap-test.yaml)用于说明,其内容如下。我们初始化好这个configmapkubectl apply -f configmap-test.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: marvel-configmap
data:
  marvel: |
    {
      name: "iron man",
      skill: [
        "fight", "fly"
      ]
    }

configmap资源对象会存储在etcd中,我们看下存储的是什么东东,哦,原来就是明文存储的。

[root@bogon ~]# ETCDCTL_API=3 etcdctl get /registry/configmaps/default/marvel-configmap
/registry/configmaps/default/marvel-configmap
k8s

v1	ConfigMap?
?
marvel-configmapdefault"*$02d3b66f-da26-11e9-a8c5-0800275f21132????b?
0kubectl.kubernetes.io/last-applied-configuration?{"apiVersion":"v1","data":{"marval":"{
  name: "iron man",
  skill: [
    "fight", "fly"
  ]
}
"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"marvel-configmap","namespace":"default"}}
zD
marval:{
  name: "iron man",
  skill: [
    "fight", "fly"
  ]
}
"

接下来使用一个redis的pod来挂载这个configmap:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name:  redis
  labels:
    name:  redis
spec:
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        name:  redis
    spec:
      containers:
      - image:  redis:5.0.5-alpine
        name:  redis
        resources:
          limits:
            cpu: 1
            memory: "100M"
          requests:
            cpu: "200m"
            memory: "55M"
        ports:
        - containerPort:  6379
          name:  redis
        volumeMounts:
        - mountPath: /data
          name: data
        - mountPath: /etc/marvel
          name: marvel
      volumes:
        - name: data
          emptyDir: {}
        - name: marvel
          configMap:
              name: marval-configmap
              items:
                - key: marvel
                  path: marvel
      restartPolicy: Always
      imagePullPolicy: Always

我们启动这个deploy,然后修改一下configmap,把用拳头锤别人的浩克加上,看多长时间可以得到更新。

apiVersion: v1
kind: ConfigMap
metadata:
  name: marvel-configmap
data:
  marvel: |
    {
      name: "iron man",
      skill: [
        "fight", "fly"
      ]
    },
    {
      name: "hulk",
      skill: [
        "fist"
      ]
    }

经过测试,经过了11s时间,pod中的内容得到了更新。再把黑寡妇也加上,耗时48s得到了更新。

apiVersion: v1
kind: ConfigMap
metadata:
  name: marvel-configmap
data:
  marvel: |
    {
      name: "iron man",
      skill: [
        "fight", "fly"
      ]
    },
    {
      name: "hulk",
      skill: [
        "fist"
      ]
    },
    {
      name: "Black widow",
      skill: [
        "magic"
      ]
    }

所以更新延迟不一定,为什么呢?接下来我们看下configmap热更新的原理。

是kubelet在做事

kubelet是每个节点都会安装的主要代理,负责维护节点上的所有容器,并监控容器的健康状况,同步容器需要的数据,数据可能来自配置文件,也可能来自etcd。kubelet有一个启动参数--sync-frequency,控制同步配置的时间间隔,它的默认值是1min,所以更新configmap的内容后,真正容器中的挂载内容变化可能在0~1min之后。修改一下这个值,修改为5s,然后更改configmap的数据,检查热更新延迟时间,都降低到了3s左右,但同时kubelet的资源消耗会上升,尤其运行比较多pod的node上,性能会显著下降。

怎么实现的呢

Kubelet是管理pod生命周期的主要组件,同时它也会维护pod所需的资源,其中之一就是configmap,实现定义在pkg/kubelet/configmap/中,kubelet主要是通过 configmap_manager 来管理每个pod所使用的configmap,configmap_manager有三种:

  • Simple Manager
  • TTL Based Manager
  • Watch Manager

默认使用 Watch Manager。其实Manager管理的主要是缓存中的configmap对象,而kubelet同步的是Pod和缓存中的configmap对象。如下图所示:

kubelet update

Simple Manager

Simple Manager直接封装了访问api-server的逻辑,其更新延迟(图中delay)为0。

TTL Based Manager

当pod启动或者更新时,pod 引用的缓存中的configmap都会被无效化。获取configmap时(GetObject()),先尝试从TTL缓存中获取,如果没有,过期或者无效,将会从api-server获取,获取的内容更新到缓存中,替换原来的内容。

CacheBasedManager 的定义在 pkg/kubelet/util/manager/cache_based_manager.go 中。

func NewCacheBasedManager(objectStore Store, getReferencedObjects func(*v1.Pod) sets.String) Manager {
	return &cacheBasedManager{
		objectStore:          objectStore,
		getReferencedObjects: getReferencedObjects,
		registeredPods:       make(map[objectKey]*v1.Pod),
	}
}

Watch Manager

每当pod启动或更新时,kubelet会对该 pod 新引用的所有configmap对象启动监控(watches),watch负责利用新的configmap对缓存的configmap更新或替换。

WatchBasedManager 的定义在 pkg/kubelet/util/manager/watch_based_manager.go 中。

func NewWatchBasedManager(listObject listObjectFunc, watchObject watchObjectFunc, newObject newObjectFunc, groupResource schema.GroupResource, getReferencedObjects func(*v1.Pod) sets.String) Manager {
	objectStore := NewObjectCache(listObject, watchObject, newObject, groupResource)
	return NewCacheBasedManager(objectStore, getReferencedObjects)
}

总结

只有当Pod使用目录形式挂载configmap时才会得到热更新能力,其余两种使用configmap的方式是Pod环境变量注入和subPath形式。

因为kubelet是定时(以一定的时间间隔)同步Pod和缓存中的configmap内容的,且三种Manager更新缓存中的configmap内容可能会有延迟,所以,当我们更改了configmap的内容后,真正反映到Pod中可能要经过syncFrequency + delay这么长的时间。

;