精华内容
下载资源
问答
  • 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" }}}}}'
    
    展开全文
  • ConfigMap热更新测试 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:30
    Kubernetes中提供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)用于说明,其内容如下。我们初始化好这个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":"{\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对象。如下图所示:

    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这么长的时间。

    展开全文
  • 1.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:25
    ConfigMap是用来存储配置文件的kubernetes资源对象,所有的配置内容都存储在etcd中,下文主要是探究 ConfigMap 的创建和更新流程,以及对 ConfigMap 更新后容器内挂载的内容是否同步更新的测试。 测试示例 假设...
  • ##文档说明 实验环境:kubernetes Version v1.9.6 网络CNI:fannel 存储CSI: NFS Dynamic Class 获取configmap中的环境变量方式有2种: ...##更新使用ConfigMap挂载的Env nginx-cm.yaml apiVersion: ...
  • 1.configmap配置管理 简介: <1> Configmap用于保存配置数据,以键值对形式存储。 <2> configMap 资源提供了向 Pod 注入配置数据的方法。 <3> 旨在让镜像和配置文件解耦,以便实现镜像的可移植性...

空空如也

空空如也

1 2 3 4 5 ... 7
收藏数 136
精华内容 54
关键字:

configmap热更新