-
ConfigMap热更新
2020-04-19 20:36:28有一天,同事遇到了客户的奇怪...接下来,就想到了ConfigMap的热更新可以触发Deploy的滚动升级呀,这样不就能将pod重建了吗?但是,同事测试后却发现ConfigMap更新后,但是缺没看到Deployment的滚动升级呢?这是为何...有一天,同事遇到了客户的奇怪的需求:主动触发pod的重建,但是又不能影响到pod中的业务。当时我就想到,给相关deployment配置滚动更新呀。但是,紧接着问题就来了,怎么触发这个滚动更新呢?接下来,就想到了ConfigMap的热更新可以触发Deploy的滚动升级呀,这样不就能将pod重建了吗?但是,同事测试后却发现ConfigMap更新后,但是缺没看到Deployment的滚动升级呢?这是为何呢?这就涉及到ConfigMap被挂载进pod的方式了。
以环境变量的方式挂载ConfigMap
当我们编写Deployment以环境变量的方式挂载ConfigMap的时候,有时候可能看到ValueFrom,有时候可能会使用EnvFrom。这是为啥呢?
主要是k8s中一开始都是使用的ValueFrom,一次只能配置一个变量,我们可能要写多个如下结构configMapKeyRef: # The ConfigMap containing the value you want to assign to SPECIAL_LEVEL_KEY name: special-config # Specify the key associated with the value key: special.how
但是,在kubernetes1.6开始,引入了一个新字段envFrom,实现了在pod环境中将ConfigMap(或者Secret资源对象)中定义的key=value自动生成为环境变量。如下:
apiVersion: v1 kind: ConfigMap metadata: name: special-config namespace: default data: SPECIAL_LEVEL: very SPECIAL_TYPE: charm
apiVersion: v1 kind: Pod metadata: name: dapi-test-pod spec: containers: - name: test-container image: gcr.io/google_containers/busybox command: [ "/bin/sh", "-c", "env" ] envFrom: - configMapRef: name: special-config restartPolicy: Never
测试后发现通过这种方式挂载Config Map,更新Config Map并不会触发Deployment更新。
以Volume的方式挂载Config Map
apiVersion: v1 kind: ConfigMap metadata: name: special-config namespace: default data: special.level: very special.type: charm
apiVersion: v1 kind: Pod metadata: name: dapi-test-pod spec: containers: - name: test-container image: gcr.io/google_containers/busybox command: [ "/bin/sh", "-c", "ls /etc/config/" ] volumeMounts: - name: config-volume mountPath: /etc/config volumes: - name: config-volume configMap: # Provide the name of the ConfigMap containing the files you want # to add to the container name: special-config restartPolicy: Never
测试后发现通过这种方式挂载Config Map,更新Config Map会触发Deployment更新。
另外, 如果使用ConfigMap的subPath挂载为Container的Volume,Kubernetes不会做自动热更新
详情点击那么问题来了,
为什么通过Volume挂载的configMap更新后能触发Deployment的更新呢?
ENV 是在容器启动的时候注入的,启动之后 kubernetes 就不会再改变环境变量的值,且同一个 namespace 中的 pod 的环境变量是不断累加的。但是Volume不同,kubelet的源码里KubeletManager中是有Volume Manager的,这就说明Kubelet会监控管理每个Pod中的Volume资源,当发现配置的Volume更新后,就会重建Pod,以更新所用的Volume,但是会有一定延迟,大概10秒以内。
源码如下// Kubelet is the main kubelet implementation. type Kubelet struct { kubeletConfiguration kubeletconfiginternal.KubeletConfiguration // hostname is the hostname the kubelet detected or was given via flag/config hostname string // hostnameOverridden indicates the hostname was overridden via flag/config hostnameOverridden bool nodeName types.NodeName runtimeCache kubecontainer.RuntimeCache kubeClient clientset.Interface heartbeatClient clientset.Interface iptClient utilipt.Interface rootDirectory string lastObservedNodeAddressesMux sync.RWMutex lastObservedNodeAddresses []v1.NodeAddress // onRepeatedHeartbeatFailure is called when a heartbeat operation fails more than once. optional. onRepeatedHeartbeatFailure func() // podWorkers handle syncing Pods in response to events. podWorkers PodWorkers // resyncInterval is the interval between periodic full reconciliations of // pods on this node. resyncInterval time.Duration // sourcesReady records the sources seen by the kubelet, it is thread-safe. sourcesReady config.SourcesReady // podManager is a facade that abstracts away the various sources of pods // this Kubelet services. podManager kubepod.Manager // Needed to observe and respond to situations that could impact node stability evictionManager eviction.Manager // Optional, defaults to /logs/ from /var/log logServer http.Handler // Optional, defaults to simple Docker implementation runner kubecontainer.ContainerCommandRunner // cAdvisor used for container information. cadvisor cadvisor.Interface // Set to true to have the node register itself with the apiserver. registerNode bool // List of taints to add to a node object when the kubelet registers itself. registerWithTaints []api.Taint // Set to true to have the node register itself as schedulable. registerSchedulable bool // for internal book keeping; access only from within registerWithApiserver registrationCompleted bool // dnsConfigurer is used for setting up DNS resolver configuration when launching pods. dnsConfigurer *dns.Configurer // masterServiceNamespace is the namespace that the master service is exposed in. masterServiceNamespace string // serviceLister knows how to list services serviceLister serviceLister // nodeLister knows how to list nodes nodeLister corelisters.NodeLister // a list of node labels to register nodeLabels map[string]string // Last timestamp when runtime responded on ping. // Mutex is used to protect this value. runtimeState *runtimeState // Volume plugins. volumePluginMgr *volume.VolumePluginMgr // Handles container probing. probeManager prober.Manager // Manages container health check results. livenessManager proberesults.Manager startupManager proberesults.Manager // How long to keep idle streaming command execution/port forwarding // connections open before terminating them streamingConnectionIdleTimeout time.Duration // The EventRecorder to use recorder record.EventRecorder // Policy for handling garbage collection of dead containers. containerGC kubecontainer.ContainerGC // Manager for image garbage collection. imageManager images.ImageGCManager // Manager for container logs. containerLogManager logs.ContainerLogManager // Secret manager. secretManager secret.Manager // ConfigMap manager. configMapManager configmap.Manager // Cached MachineInfo returned by cadvisor. machineInfo *cadvisorapi.MachineInfo // Handles certificate rotations. serverCertificateManager certificate.Manager // Syncs pods statuses with apiserver; also used as a cache of statuses. statusManager status.Manager // VolumeManager runs a set of asynchronous loops that figure out which // volumes need to be attached/mounted/unmounted/detached based on the pods // scheduled on this node and makes it so. volumeManager volumemanager.VolumeManager // 以下省略 }
那我们怎么才能让更新Config Map一定触发Deployment滚动更新呢?
可以通过修改 pod annotations 的方式强制触发滚动更新。这样一定会触发更新,亲测有效。
kubectl patch deployment test-deploy --patch '{"spec": {"template": {"metadata": {"annotations": {"update": "2" }}}}}'
-
Kubernetes ConfigMap热更新测试 – 探究ConfigMap的创建和更新流程
2018-12-14 23:26:00ConfigMap热更新测试 ConfigMap是用来存储配置文件的kubernetes资源对象,所有的配置内容都存储在etcd中,下文主要是探究 ConfigMap 的创建和更新流程,以及对 ConfigMap 更新后容器内挂载的内容是否同步更新的测试...ConfigMap热更新测试
ConfigMap是用来存储配置文件的kubernetes资源对象,所有的配置内容都存储在etcd中,下文主要是探究 ConfigMap 的创建和更新流程,以及对 ConfigMap 更新后容器内挂载的内容是否同步更新的测试。
测试示例
假设我们在 default namespace 下有一个名为 nginx-config 的 ConfigMap,可以使用 kubectl命令来获取:
$ kubectl get configmap nginx-config NAME DATA AGE nginx-config 1 99d
获取该ConfigMap的内容。
kubectl get configmap nginx-config -o yaml
apiVersion: v1 data: nginx.conf: |- worker_processes 1; events { worker_connections 1024; } http { sendfile on; server { listen 80; # a test endpoint that returns http 200s location / { proxy_pass http://httpstat.us/200; proxy_set_header X-Real-IP $remote_addr; } } server { listen 80; server_name api.hello.world; location / { proxy_pass http://l5d.default.svc.cluster.local; proxy_set_header Host $host; proxy_set_header Connection ""; proxy_http_version 1.1; more_clear_input_headers 'l5d-ctx-*' 'l5d-dtab' 'l5d-sample'; } } server { listen 80; server_name www.hello.world; location / { # allow 'employees' to perform dtab overrides if ($cookie_special_employee_cookie != "letmein") { more_clear_input_headers 'l5d-ctx-*' 'l5d-dtab' 'l5d-sample'; } # add a dtab override to get people to our beta, world-v2 set $xheader ""; if ($cookie_special_employee_cookie ~* "dogfood") { set $xheader "/host/world => /srv/world-v2;"; } proxy_set_header 'l5d-dtab' $xheader; proxy_pass http://l5d.default.svc.cluster.local; proxy_set_header Host $host; proxy_set_header Connection ""; proxy_http_version 1.1; } } } kind: ConfigMap metadata: creationTimestamp: 2017-08-01T06:53:17Z name: nginx-config namespace: default resourceVersion: "14925806" selfLink: /api/v1/namespaces/default/configmaps/nginx-config uid: 18d70527-7686-11e7-bfbd-8af1e3a7c5bd
ConfigMap中的内容是存储到etcd中的,然后查询etcd:
ETCDCTL_API=3 etcdctl get /registry/configmaps/default/nginx-config /registry/configmaps/default/nginx-config
注意使用 v3 版本的 etcdctl API,下面是输出结果:
k8s v1 ConfigMap� T nginx-configdefault"*$18d70527-7686-11e7-bfbd-8af1e3a7c5bd28B �ʀ����xz� nginx.conf� worker_processes 1; events { worker_connections 1024; } http { sendfile on; server { listen 80; # a test endpoint that returns http 200s location / { proxy_pass http://httpstat.us/200; proxy_set_header X-Real-IP $remote_addr; } } server { listen 80; server_name api.hello.world; location / { proxy_pass http://l5d.default.svc.cluster.local; proxy_set_header Host $host; proxy_set_header Connection ""; proxy_http_version 1.1; more_clear_input_headers 'l5d-ctx-*' 'l5d-dtab' 'l5d-sample'; } } server { listen 80; server_name www.hello.world; location / { # allow 'employees' to perform dtab overrides if ($cookie_special_employee_cookie != "letmein") { more_clear_input_headers 'l5d-ctx-*' 'l5d-dtab' 'l5d-sample'; } # add a dtab override to get people to our beta, world-v2 set $xheader ""; if ($cookie_special_employee_cookie ~* "dogfood") { set $xheader "/host/world => /srv/world-v2;"; } proxy_set_header 'l5d-dtab' $xheader; proxy_pass http://l5d.default.svc.cluster.local; proxy_set_header Host $host; proxy_set_header Connection ""; proxy_http_version 1.1; } } }"
输出中在 nginx.conf 配置文件的基础中增加了文件头内容,是kubernetes增加的。
代码
ConfigMap 结构体的定义:
// ConfigMap holds configuration data for pods to consume. type ConfigMap struct { metav1.TypeMeta `json:",inline"` // Standard object's metadata. // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata // +optional metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` // Data contains the configuration data. // Each key must be a valid DNS_SUBDOMAIN with an optional leading dot. // +optional Data map[string]string `json:"data,omitempty" protobuf:"bytes,2,rep,name=data"` }
在 staging/src/k8s.io/client-go/kubernetes/typed/core/v1/configmap.go 中ConfigMap 的接口定义:
// ConfigMapInterface has methods to work with ConfigMap resources. type ConfigMapInterface interface { Create(*v1.ConfigMap) (*v1.ConfigMap, error) Update(*v1.ConfigMap) (*v1.ConfigMap, error) Delete(name string, options *meta_v1.DeleteOptions) error DeleteCollection(options *meta_v1.DeleteOptions, listOptions meta_v1.ListOptions) error Get(name string, options meta_v1.GetOptions) (*v1.ConfigMap, error) List(opts meta_v1.ListOptions) (*v1.ConfigMapList, error) Watch(opts meta_v1.ListOptions) (watch.Interface, error) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.ConfigMap, err error) ConfigMapExpansion }
在 staging/src/k8s.io/client-go/kubernetes/typed/core/v1/configmap.go 中创建 ConfigMap 的方法如下:
// Create takes the representation of a configMap and creates it. Returns the server's representation of the configMap, and an error, if there is any. func (c *configMaps) Create(configMap *v1.ConfigMap) (result *v1.ConfigMap, err error) { result = &v1.ConfigMap{} err = c.client.Post(). Namespace(c.ns). Resource("configmaps"). Body(configMap). Do(). Into(result) return }
通过 RESTful 请求在 etcd 中存储 ConfigMap 的配置,该方法中设置了资源对象的 namespace 和 HTTP 请求中的 body,执行后将请求结果保存到 result 中返回给调用者。
注意 Body 的结构
// Body makes the request use obj as the body. Optional. // If obj is a string, try to read a file of that name. // If obj is a []byte, send it directly. // If obj is an io.Reader, use it directly. // If obj is a runtime.Object, marshal it correctly, and set Content-Type header. // If obj is a runtime.Object and nil, do nothing. // Otherwise, set an error.
创建 ConfigMap RESTful 请求中的的 Body 中包含 ObjectMeta 和 namespace。
HTTP 请求中的结构体:
// Request allows for building up a request to a server in a chained fashion. // Any errors are stored until the end of your call, so you only have to // check once. type Request struct { // required client HTTPClient verb string baseURL *url.URL content ContentConfig serializers Serializers // generic components accessible via method setters pathPrefix string subpath string params url.Values headers http.Header // structural elements of the request that are part of the Kubernetes API conventions namespace string namespaceSet bool resource string resourceName string subresource string timeout time.Duration // output err error body io.Reader // This is only used for per-request timeouts, deadlines, and cancellations. ctx context.Context backoffMgr BackoffManager throttle flowcontrol.RateLimiter }
测试
分别测试使用 ConfigMap 挂载 Env 和 Volume 的情况。
更新使用ConfigMap挂载的Env
使用下面的配置创建 nginx 容器测试更新 ConfigMap 后容器内的环境变量是否也跟着更新。
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-nginx spec: replicas: 1 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: sz-pg-oam-docker-hub-001.tendcloud.com/library/nginx:1.9 ports: - containerPort: 80 envFrom: - configMapRef: name: env-config --- apiVersion: v1 kind: ConfigMap metadata: name: env-config namespace: default data: log_level: INFO
获取环境变量的值
$ kubectl exec `kubectl get pods -l run=my-nginx -o=name|cut -d "/" -f2` env|grep log_level log_level=INFO
修改 ConfigMap
$ kubectl edit configmap env-config
修改 log_level 的值为 DEBUG。
再次查看环境变量的值。
$ kubectl exec `kubectl get pods -l run=my-nginx -o=name|cut -d "/" -f2` env|grep log_level log_level=INFO
实践证明修改 ConfigMap 无法更新容器中已注入的环境变量信息。
更新使用ConfigMap挂载的Volume
使用下面的配置创建 nginx 容器测试更新 ConfigMap 后容器内挂载的文件是否也跟着更新。
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-nginx spec: replicas: 1 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: sz-pg-oam-docker-hub-001.tendcloud.com/library/nginx:1.9 ports: - containerPort: 80 volumeMounts: - name: config-volume mountPath: /etc/config volumes: - name: config-volume configMap: name: special-config --- apiVersion: v1 kind: ConfigMap metadata: name: special-config namespace: default data: log_level: INFO
$ kubectl exec `kubectl get pods -l run=my-nginx -o=name|cut -d "/" -f2` cat /tmp/log_level INFO
修改 ConfigMap
$ kubectl edit configmap special-config
修改 log_level 的值为 DEBUG。
等待大概10秒钟时间,再次查看环境变量的值。
$ kubectl exec `kubectl get pods -l run=my-nginx -o=name|cut -d "/" -f2` cat /tmp/log_level DEBUG
我们可以看到使用 ConfigMap 方式挂载的 Volume 的文件中的内容已经变成了 DEBUG。
总结
更新 ConfigMap 后:
- 使用该 ConfigMap 挂载的 Env 不会同步更新
- 使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间(实测大概10秒)才能同步更新
ENV 是在容器启动的时候注入的,启动之后 kubernetes 就不会再改变环境变量的值,且同一个 namespace 中的 pod 的环境变量是不断累加的,参考 Kubernetes中的服务发现与docker容器间的环境变量传递源码探究。为了更新容器中使用 ConfigMap 挂载的配置,可以通过滚动更新 pod 的方式来强制重新挂载 ConfigMap,也可以在更新了 ConfigMap 后,先将副本数设置为 0,然后再扩容。
本文转自kubernetes中文社区-Kubernetes ConfigMap热更新测试 – 探究ConfigMap的创建和更新流程
-
Kubernetes - Configmap热更新原理
2019-11-01 00:58:30Kubernetes中提供configmap,用来管理应用的配置,configmap具备热更新的能力,但只有通过目录挂载的configmap才具备热更新能力,其余通过环境变量,通过subPath挂载...这篇文章里我们来看看configmap热更新的原理,...GitHub地址: https://github.com/QingyaFan/container-cloud/issues/2
Kubernetes中提供configmap,用来管理应用的配置,configmap具备热更新的能力,但只有通过目录挂载的configmap才具备热更新能力,其余通过环境变量,通过subPath挂载的文件都不能动态更新。这篇文章里我们来看看configmap热更新的原理,以及为什么只有目录形式挂载才具备热更新能力。
configmap热更新原理
我们首先创建一个configmap(configmap-test.yaml)用于说明,其内容如下。我们初始化好这个configmap
kubectl 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":"{\n name: \"iron man\",\n skill: [\n \"fight\", \"fly\"\n ]\n}\n"},"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对象。如下图所示: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
这么长的时间。 -
kubernetes configmap热更新源码简析
2020-12-14 02:10:471.2 configmap热更新原理概述 configmap(secret的热更新也是一个原理)是kubernetes支持的卷的一种,底层原理是kubelet根据configmap中的内容在宿主机上写成若干文件(目录默认是/var/lib/kubelet/pods/<pod的UUI&...1 概述:
1.1 环境
版本信息如下:
a、操作系统:centos 7.6
c、kubernetes版本:v1.15.0
1.2 configmap热更新原理概述
configmap(secret的热更新也是一个原理)是kubernetes支持的卷的一种,底层原理是kubelet根据configmap中的内容在宿主机上写成若干文件(目录默认是/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名>),最终通过绑定挂载的方式映射到容器内部,在宿主机上可直接修改这些文件,容器也能实时看见,因为是同一个文件。kubelet的一个组件叫volume manager,此volume manager的reconciler协程专门执行块设备的attach/detach、目录的mount/remount/unmount。reconciler协程的mount操作,就包含了更新/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名>/这种目录下的文件的内容。热更新每隔多久执行一次,也是可以通过启动参数设定。
2 影响configmap热更新的参数:
1)–sync-frequency参数,这是kubelet同步pod状态的时间间隔,也是重新挂载卷(例如configmap热更新)的时间间隔。
fs.DurationVar(&c.SyncFrequency.Duration, "sync-frequency", c.SyncFrequency.Duration, "Max period between synchronizing running containers and config")
3 源码简析:
3.1 volume manager(kubelet的一个属性)的初始化
func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,) { /* 其他代码 */ // 为kubelet的对象设置volumeManager klet.volumeManager = volumemanager.NewVolumeManager( kubeCfg.EnableControllerAttachDetach, nodeName, klet.podManager, klet.statusManager, klet.kubeClient, klet.volumePluginMgr, klet.containerRuntime, kubeDeps.Mounter, klet.getPodsDir(), kubeDeps.Recorder, experimentalCheckNodeCapabilitiesBeforeMount, keepTerminatedPodVolumes) return klet, nil }
3.2 启动volume manager
kubelet在启动过程中会启动众多组件,其中一个重要组件就是volume manager。
func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) { /* 其他代码 */ // 启动volume manager go kl.volumeManager.Run(kl.sourcesReady, wait.NeverStop) /* 其他代码 */ // 启动kubelet的主循环 kl.syncLoop(updates, kl) }
volume manager的启动,又会启动3个协程。本文文章主要关心第二个协程:reconciler启动的协程。
func (vm *volumeManager) Run(sourcesReady config.SourcesReady, stopCh <-chan struct{}) { defer runtime.HandleCrash() // 1)启动volume manager的desiredStateOfWorldPopulator go vm.desiredStateOfWorldPopulator.Run(sourcesReady, stopCh) // 2)启动volume manager的reconciler go vm.reconciler.Run(stopCh) if vm.kubeClient != nil { // 3)启动volume manager的volumePluginMgr vm.volumePluginMgr.Run(stopCh) } <-stopCh klog.Infof("Shutting down Kubelet Volume Manager") }
3.3 启动volume manager的reconciler
reconciler协程(100毫秒的循环,因为rc.loopSleepDuration=100毫秒)主要负责卷的attach/detach和mount(remount)/unmount。
configmap的热更新也是这个协程做的。func (rc *reconciler) Run(stopCh <-chan struct{}) { wait.Until(rc.reconciliationLoopFunc(), rc.loopSleepDuration, stopCh) } func (rc *reconciler) reconciliationLoopFunc() func() { return func() { rc.reconcile() if rc.populatorHasAddedPods() && !rc.StatesHasBeenSynced() { klog.Infof("Reconciler: start to sync state") rc.sync() } } }
3.4 reconciler的reconcile() 方法
核心业务逻辑是 reconcile()方法,它会不断地遍历desiredStateOfWorld和actualStateOfWorld,然后执行一系列的attach/detach和mount(remount)/unmount等操作。
func (rc *reconciler) reconcile() { /* 第一步: 先把不需要挂载的卷执行卸载操作,代码省略 */ /* 第二步: 执行attach操作和mount(remount)操作。 */ for _, volumeToMount := range rc.desiredStateOfWorld.GetVolumesToMount() { volMounted, devicePath, err := rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName) volumeToMount.DevicePath = devicePath /* 这个err对象很重要,不同的err进入不同的if分支,而configmap热更新就是进入其中一个分支。 很多时候,这个err对象是nil,因此 reconcile()方法其实是空跑。 */ if cache.IsVolumeNotAttachedError(err) { if rc.controllerAttachDetachEnabled || !volumeToMount.PluginIsAttachable { /* kubelet等待其他程序(例如一些控制器)将卷attach到当前节点。 */ } else { /* 卷没有attach到当前节点,因此kubelet将卷attach到当前节点。 err := rc.operationExecutor.AttachVolume(volumeToAttach, rc.actualStateOfWorld) */ } } else if !volMounted || cache.IsRemountRequiredError(err) { /* 进入此语句,说明卷未挂载,或者已经挂载的卷需要重新挂载 confingmap、secret等热更新的情景就是在此处实现,isRemount = true。 */ isRemount := cache.IsRemountRequiredError(err) // 执行挂载或重新挂载的操作 err := rc.operationExecutor.MountVolume( rc.waitForAttachTimeout, volumeToMount.VolumeToMount, rc.actualStateOfWorld, isRemount) /* 打印一些日志 */ } else if cache.IsFSResizeRequiredError(err) && utilfeature.DefaultFeatureGate.Enabled(features.ExpandInUsePersistentVolumes) { /* kubelet对正在使用的卷的文件系统进行扩容 */ } } // Ensure devices that should be detached/unmounted are detached/unmounted. /* 第三步: umount和detach块设备,代码省略 */ }
func (oe *operationExecutor) MountVolume( waitForAttachTimeout time.Duration, volumeToMount VolumeToMount, actualStateOfWorld ActualStateOfWorldMounterUpdater, isRemount bool) error { fsVolume, err := util.CheckVolumeModeFilesystem(volumeToMount.VolumeSpec) if err != nil { return err } // 根据volume的类型来构建generatedOperations对象 // 挂载卷的方法就保存在generatedOperations对象。 var generatedOperations volumetypes.GeneratedOperations if fsVolume { /* 这是文件系统卷的情景,configmap等卷会进入这里 */ generatedOperations = oe.operationGenerator.GenerateMountVolumeFunc( waitForAttachTimeout, volumeToMount, actualStateOfWorld, isRemount) } else { /* 这是块设备的情景,代码省略 */ } if err != nil { return err } /* 其他代码 */ // 底层会启动一个协程来执行generatedOperations对象的Run()方法 return oe.pendingOperations.Run( volumeToMount.VolumeName, podName, generatedOperations) }
3.5 generatedOperations对象的mountVolumeFunc
挂载卷的方法(mountVolumeFunc)就保存在generatedOperations对象。mountVolumeFunc主要是从卷插件管理volumePluginMgr(它维护着plugin列表)中拿一个插件出来,再从插件中得到一个mouter,mounter执行真正的挂载操作。
type GeneratedOperations struct { OperationName string OperationFunc func() (eventErr error, detailedErr error) EventRecorderFunc func(*error) CompleteFunc func(*error) }
func (og *operationGenerator) GenerateMountVolumeFunc( waitForAttachTimeout time.Duration, volumeToMount VolumeToMount, actualStateOfWorld ActualStateOfWorldMounterUpdater, isRemount bool) volumetypes.GeneratedOperations { /* 其他代码 */ // 这就是挂载卷的方法,最终作为generatedOperations对象的一个属性 mountVolumeFunc := func() (error, error) { /* 其他代码 */ // 从volume plgin管理器中获取一个volume plgin volumePlugin, err := og.volumePluginMgr.FindPluginBySpec(volumeToMount.VolumeSpec) if err != nil || volumePlugin == nil { return volumeToMount.GenerateError("MountVolume.FindPluginBySpec failed", err) } /* 其他代码 */ // volume plgin对象中得到一个mounter // 对于configmap这种卷,插件的实现是结构体configMapVolumeMounter volumeMounter, newMounterErr := volumePlugin.NewMounter( volumeToMount.VolumeSpec, volumeToMount.Pod, volume.VolumeOptions{}) /* 其他代码 */ // mounter的SetUp(...)方法就是执行挂载操作 mountErr := volumeMounter.SetUp(volume.MounterArgs{ FsGroup: fsGroup, DesiredSize: volumeToMount.DesiredSizeLimit, PodUID: string(volumeToMount.Pod.UID), }) // 更新actualStateOfWorld,标记当前卷已经被挂载 actualStateOfWorld.MarkVolumeAsMounted(...) return nil, nil } /* 其他代码 */ return volumetypes.GeneratedOperations{ OperationName: "volume_mount", OperationFunc: mountVolumeFunc, EventRecorderFunc: eventRecorderFunc, CompleteFunc: util.OperationCompleteHook(util.GetFullQualifiedPluginNameForVolume(volumePluginName, volumeToMount.VolumeSpec), "volume_mount"), } }
volumePluginMgr支持的常见的plugin如下:
3.6 configMapPlugin和configMapVolumeMounter
从configMapPlugin对象得到一个mounter对象,其实是把插件的getConfigMap方法赋值给mounter对象的getConfigMap属性。
func (plugin *configMapPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, opts volume.VolumeOptions) (volume.Mounter, error) { return &configMapVolumeMounter{ configMapVolume: &configMapVolume{ spec.Name(), pod.UID, plugin, plugin.host.GetMounter(plugin.GetPluginName()), volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))), }, source: *spec.Volume.ConfigMap, pod: *pod, opts: &opts, getConfigMap: plugin.getConfigMap, }, nil }
configMapVolumeMounter的SetUp()方法是真正执行挂载操作(将configmap的内容写到文件)的方法。
func (b *configMapVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error { return b.SetUpAt(b.GetPath(), mounterArgs) } // 入参dir就是/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名> func (b *configMapVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error { klog.V(3).Infof("Setting up volume %v for pod %v at %v", b.volName, b.pod.UID, dir) /* 其他代码 */ // configmap对象可以来自kube-apiserver,也可以来自kubelet中的缓存,取决于configMapManager的实现 configMap, err := b.getConfigMap(b.pod.Namespace, b.source.Name) /* 其他代码 */ payload, err := MakePayload(b.source.Items, configMap, b.source.DefaultMode, optional) if err != nil { return err } /* 其他代码 */ setupSuccess := false writerContext := fmt.Sprintf("pod %v/%v volume %v", b.pod.Namespace, b.pod.Name, b.volName) writer, err := volumeutil.NewAtomicWriter(dir, writerContext) // 把configmap的键值对写到入参dir下 err = writer.Write(payload) /* 其他代码 */ setupSuccess = true return nil }
4 kubelet主循环和volume manager的协作
volume manager的reconciler协程大多数时候是空跑的,只有pod被认为需要重新挂载卷的时候,才会执行重新挂载的操作。那什么时候pod会被认为需要重新挂载已挂载的卷?这个时间间隔和kubelet的主循环有关,和主循环的时间间隔由–sync-frequency参数来设定。每次同步pod的状态,其实都给了该pod重新挂载已挂载卷的机会。当pod被认为需要重新挂载卷,volumeManager的reconciler的reconcile()方法中调用rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName)会返回一个err对象,代码会进入重新挂载卷的if语句中。
func (kl *Kubelet) syncPod(o syncPodOptions) error { /* 其他代码 */ if !kl.podIsTerminated(pod) { // WaitForAttachAndMount(...)等待卷的挂载,同时也给了该pod对已挂载的卷进行重新挂载的机会 if err := kl.volumeManager.WaitForAttachAndMount(pod); err != nil { kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedMountVolume, "Unable to mount volumes for pod %q: %v", format.Pod(pod), err) klog.Errorf("Unable to mount volumes for pod %q: %v; skipping pod", format.Pod(pod), err) return err } } /* 其他代码 */ }
func (vm *volumeManager) WaitForAttachAndMount(pod *v1.Pod) error { /* 其他代码 */ // 标记这个pod的卷要被重新挂载一次(其实是从一个map中删除数据,这样volume manager会以为pod未被处理) // volumeManager的reconciler的reconcile()方法中调用rc.actualStateOfWorld.PodExistsInVolume(volumeToMount.PodName, volumeToMount.VolumeName)会返回一个err对象,从而触发重新挂载的操作 vm.desiredStateOfWorldPopulator.ReprocessPod(uniquePodName) /* 其他代码 */ klog.V(3).Infof("All volumes are attached and mounted for pod %q", format.Pod(pod)) return nil }
5 总结
configmap热更新原理很简单,kubelet有专门的协程来根据configmap对象中的内容来修改/var/lib/kubelet/pods/<pod的UUI>/volumes/kubernetes.io~configmap/<卷名>/这种目录下的文件。至于协程多久执行一次需要热更新(在volume manager看就是mount volume),这个时间间隔由参数–sync-frequency参数来决定,此参数直接决定同步pod的时间间隔,也间接决定了pod热更新的时间间隔,因为kubelet的主循环同步pod的过程中会标记该pod需要被volume manager重新处理一次。
-
k8s ConfigMap热更新
2020-02-22 19:07:25ConfigMap是用来存储配置文件的kubernetes资源对象,所有的配置内容都存储在etcd中,下文主要是探究 ConfigMap 的创建和更新流程,以及对 ConfigMap 更新后容器内挂载的内容是否同步更新的测试。 测试示例 假设... -
K8S容器编排之ConfigMap热更新测试
2019-01-22 22:47:08##文档说明 实验环境:kubernetes Version v1.9.6 网络CNI:fannel 存储CSI: NFS Dynamic Class 获取configmap中的环境变量方式有2种: ...##更新使用ConfigMap挂载的Env nginx-cm.yaml apiVersion: ... -
kubernetes存储—Configmap配置管理介绍、创建、如何使用configmap、configmap热更新、Secret配置管理的...
2020-07-08 01:19:011.configmap配置管理 简介: <1> Configmap用于保存配置数据,以键值对形式存储。 <2> configMap 资源提供了向 Pod 注入配置数据的方法。 <3> 旨在让镜像和配置文件解耦,以便实现镜像的可移植性...