后浪笔记一零二四

kube-controller-manager

所有的控制器都由kube-controller-manager这个组件管理,在kubernetes项目的pkg/controller目录下可以查看所有的控制器:

BUILD                          deployment                     podautoscaler
OWNERS                         disruption                     podgc
apis                           doc.go                         replicaset
bootstrap                      endpoint                       replication
certificates                   endpointslice                  resourcequota
client_builder_dynamic.go      endpointslicemirroring         serviceaccount
clusterroleaggregation         garbagecollector               statefulset
controller_ref_manager.go      history                        storageversiongc
controller_ref_manager_test.go job                            testutil
controller_utils.go            lookup_cache.go                ttl
controller_utils_test.go       namespace                      ttlafterfinished
cronjob                        nodeipam                       util
daemon                         nodelifecycle                  volume

这些控制器之所以被统一放在pkg/controller目录下,就是因为它们被遵循Kubernetes项目中的一个通用编排模式,即:控制循环(control loop)。

for {
  实际状态 := 获取集群中对象x的实际状态(Actual State)
  期望状态 := 获取集群中对象x的期望状态(Desired State)
  if 实际状态 == 期望状态 { // 第三步,执行对比,也被称之为Reconcile Loop(调谐循环)
    什么都不做
  } else {
    执行编排动作,将实际状态调整为期望状态
  }
}

1. Deployment/ReplicaSet

接下来,以 Deployment 为例,我和你简单描述一下它对控制器模型的实现:

  1. Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
  2. Deployment 对象的 Replicas 字段的值就是期望状态;
  3. Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
-------------------------
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

类似 Deployment 这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。 这就是为什么,在所有 API 对象的 Metadata 里,都有一个字段叫作 ownerReference,用于保存当前这个 API 对象的拥有者(Owner)的信息。 对于Deployment管理的Pod来说,它的ownerReference其实是ReplicaSet。 Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象。

Pod的"水平扩展/收缩"(horizontal scaling out/in) 举个例子,如果你更新了 Deployment 的 Pod 模板(比如,修改了容器的镜像),那么 Deployment 就需要遵循一种叫作“滚动更新”(rolling update)的方式,来升级现有的容器。 而这个能力的实现,依赖的是 Kubernetes 项目中的一个非常重要的概念(API 对象):ReplicaSet。 ReplicaSet 的结构非常简单,我们可以通过这个 YAML 文件查看一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-set
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9

其实,Deployment、ReplicaSet和Pod之间,是层层控制的关系。其中,ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数(比如,3 个)。 这也正是 Deployment 只允许容器的 restartPolicy=Always 的主要原因:只有在容器能保证自己始终是 Running 状态的前提下,ReplicaSet 调整 Pod 的个数才有意义。 而Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。

  1. Deployment通过修改它所控制的ReplicaSet的Pod副本个数来实现"水平扩展/收缩" 可以通过直接修改Deployment的yaml,或者使用下面的命令实现:
1
$ kubectl scale deployment nginx-deployment --replicas=4
  1. 滚动更新就相对要复杂些 接下来,我们以下面这个yaml来演示"滚动更新"的过程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

创建nginx-deployment,并使用–record参数来记录每次操作所执行的命令(会在metadata.annotations的kubernetes.io/change-cause中记录下每次的操作):

1
2
3
4
$ kubectl create -f nginx-deployment.yaml --record
$ kubectl get deployments
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           60s

在返回结果中,我们可以看到三个状态字段,它们的含义如下所示。

  • READY: 通过健康检查的Pod的个数,可能包含就版本的Pod。
  • UP-TO-DATE: 当前处于最新版本的 Pod 的个数,所谓最新版本指的是 Pod 的 Spec 部分与 Deployment 里 Pod 模板里定义的完全一致;
  • AVAILABLE:当前已经可用的 Pod 的个数,即:既是 Running 状态,又是最新版本,并且已经处于 Ready(健康检查正确)状态的 Pod 的个数。
  • CURRENT: 当前处于 Running 状态的 Pod 的个数;

可以使用kubectl rollout statu命令来实时查看Deployment对象的状态变化: $ kubectl rollout status deployment/nginx-deployment

查看该Deployment所控制的ReplicaSet:

1
2
3
$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5bf87f5f59   3         3         3       6m6s

可以看到,在用户提交了一个Deployment对象后,Deployment Controller就会立即创建一个ReplicaSet,这个ReplicaSet的名字,有Deployment的名字和一个随机字符串组成。 将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。Deployment每执行一次滚动更新,都会新建一个新的ReplicaSet,新的ReplicaSet就绪后才会删除旧的ReplicaSet。 可以通过spec.strategy字段来控制滚动更新的策略,例如:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
...
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1

maxSurge 指定的是除了 DESIRED 数量之外,在一次“滚动”中,Deployment 控制器还可以创建多少个新 Pod。值还可以是百分比。 maxUnavailable 指的是,在一次“滚动”中,Deployment 控制器可以删除多少个旧 Pod。值还可以是百分比。

可以通过直接修改Deployment的yaml,或者使用下面的命令实现滚动更新: $ kubectl set image deployment/nginx-deployment nginx=nginx:1.91

可以使用下面的命令把整个Deployment回滚到上一个版本: $ kubectl rollout undo deployment/nginx-deployment

进一步地,如果我想回滚到更早之前的版本,要怎么办呢? 首先,我需要使用 kubectl rollout history 命令,查看该Deployment 变更的所有历史版本。 如果在创建Deployment的时候指定了–record选项,就不需要再指定–revision=<版本号>来查看具体的变更细节了。 $ kubectl rollout history deployment/nginx-deployment 然后,就可以使用–to-revision=<版本号>来执行回滚了。 $ kubectl rollout history deployment/nginx-deployment –to-revision=2

如何实现对Deployment的多次更新操作,最后只生成一个ReplicaSet呢? 首先,使用如下的命令,暂停该Deployment: $ kubectl rollout pause deployment/nginx-deployment 在对Deployment的所有修改都完成之后,再恢复该Deployment: $ kubectl rollout resume deploy/nginx-deployment

可以通过spec.revisionHistoryLimit字段来设置Deployment能保留的最大ReplicaSet的个数。

2. StatefulSet

如果实例之间存在依赖关系,比如:主从关系、主备关系,这个时候就无法再使用Deployment了。

k8s是第一个解决有状态应用(比如数据库)这个编排难题的,它使用了StatefulSet这个API对象,它把真实世界里的应用状态,抽象为两种情况:

  1. 拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。 拓扑状态是通过Headless Service来维护的。
  2. 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。 存储状态是通过PVC(Persistent Volume Claim)来维护的。

所以,StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。

StatefulSet会使用到HeadlessService,而HeadlessService会给它所代理的Pod的IP地址绑定如下格式的DNS记录:

<pod-name>.<svc-name>.<namespace>.svc.cluster.local 

StatefulSet 又是如何使用这个 DNS 记录来维持 Pod 的拓扑状态的呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web

从上面的StatefulSet的yaml中可以看出,它和Deployment的区别是,多了一个serviceName=nginx的字段。 这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用名为 nginx的 这个 Headless Service 来保证 Pod 的“可解析身份”。所以在创建StatefulSet之前,需要先创建Headless Service

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx

执行如下操作:

1
2
3

$ kubectl create -f svc.yaml
$ kubectl create -f statefulset.yaml

这时候,如果你手比较快的话,还可以通过 kubectl 的 -w 参数,即:Watch 功能,实时查看 StatefulSet 创建两个有状态实例的过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         19s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         20s

通过上面这个 Pod 的创建过程,我们不难看到,StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是:-。 而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。

更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。

当这两个 Pod 都进入了 Running 状态之后,你就可以使用kubectl exec命令来查看到它们各自唯一的“网络身份”了。

1
2
3
4
$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'
web-1

接下来,我们再试着以 DNS 的方式,访问一下这个 Headless Service:

$ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh
$ nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.7

$ nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.7

从 nslookup 命令的输出结果中,我们可以看到,在访问 web-0.nginx 的时候,最后解析到的,正是 web-0 这个 Pod 的 IP 地址;而当访问 web-1.nginx 的时候,解析到的则是 web-1 的 IP 地址。

这时候,如果你在另外一个 Terminal 里把这两个“有状态应用”的 Pod 删掉: 然后,再在当前 Terminal 里 Watch 一下这两个 Pod 的状态变化,就会发现一个有趣的现象:

1
2
3
4
5
6
7
8
$ kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         32s

可以看到,当我们把这两个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出了两个新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”:web-0.nginx 和 web-1.nginx。

通过这种严格的对应规则(而不是使用hash算法生成一个字符串来标识),StatefulSet 就保证了 Pod 网络标识的稳定性。

所以,如果我们再用 nslookup 命令,查看一下这个新 Pod 对应的 Headless Service 的话,你就会发现原来的域名还能用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh 
$ nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.8

$ nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.8

不过,相信你也已经注意到了,尽管 web-0.nginx 这条记录本身不会变,但它解析到的 Pod 的 IP 地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。

为了维护存储状态,需要用到PVC,所以将上面的StatefulSet的yaml改为下面这个样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi

注意,不能单独的创建PVC之后,在挂载在StatefulSet中。而是使用StatefulSet的volumeClaimTemplates字段来维护PVC。 这样,凡是被这个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段,而且使用与这个 Pod 完全一致的编号(<PVC名字>-<StatefulSet名字>-<编号>)。 所以在部署StatefulSet之前,需要先让运维人员创建好PV。除非你的 Kubernetes 集群运行在公有云上,这样 Kubernetes 就会通过 Dynamic Provisioning 的方式,自动为你创建与 PVC 匹配的 PV。

只更新序号大于或者等于2的Pod:

1
$ kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'

3. DaemonSet

DaemonSet 的主要作用,是让你在 Kubernetes 集群里,运行一个 Daemon Pod。 所以,这个 Pod 有如下三个特征:

  1. 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
  2. 每个节点上只有一个这样的 Pod 实例;
  3. 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。

这个机制听起来很简单,但 Daemon Pod 的意义确实是非常重要的。我随便给你列举几个例子:

  1. 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络;
  2. 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录;
  3. 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。

更重要的是,跟其他编排对象不一样,DaemonSet 开始运行的时机,很多时候比整个 Kubernetes 集群出现的时机都要早。

这个乍一听起来可能有点儿奇怪。但其实你来想一下:如果这个 DaemonSet 正是一个网络插件的 Agent 组件呢?这个时候,整个 Kubernetes 集群里还没有可用的容器网络,所有 Worker 节点的状态都是 NotReady(NetworkReady=false)。这种情况下,普通的 Pod 肯定不能运行在这个集群上。

而DaemonSet 的“过人之处”,其实就是依靠 Toleration 实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
template:
    metadata:
      labels:
        name: network-plugin-agent
    spec:
      tolerations:
      - key: node.kubernetes.io/network-unavailable
        operator: Exists
        effect: NoSchedule

这样,哪怕这个Worker节点的状态是NotReady,也可以部署普通的Pod。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd-elasticsearch
        image: k8s.gcr.io/fluentd-elasticsearch:1.20
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

这个 DaemonSet,管理的是一个 fluentd-elasticsearch 镜像的 Pod。这个镜像的功能非常实用:通过 fluentd 将 Docker 容器里的日志转发到 ElasticSearch 中。

Docker 容器里应用的日志(这里只限stdout和stderr),默认会保存在宿主机的 /var/lib/docker/containers/{{. 容器 ID}}/{{. 容器 ID}}-json.log 文件里,所以这个目录正是 fluentd 的搜集目标。

如果Docker容器将日志放在某个文件中,上面的方案就不可用了, 这个时候就需要使用sidecar把日志文件的数据重新打印到stdout和stderr上才行。

那么,DaemonSet 又是如何保证每个 Node 上有且只有一个被管理的 Pod 呢? DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。这时,它就可以很容易地去检查,当前这个 Node 上是不是有一个携带了 name=fluentd-elasticsearch 标签的 Pod 在运行。而检查的结果,可能有这么三种情况:

  1. 没有这种 Pod,那么就意味着要在这个 Node 上创建这样一个 Pod;
  2. 有这种 Pod,但是数量大于 1,那就说明要把多余的 Pod 从这个 Node 上删除掉;
  3. 正好只有一个这种 Pod,那说明这个节点是正常的。

其中,删除节点(Node)上多余的 Pod 非常简单,直接调用 Kubernetes API 就可以了。但是,如何在指定的 Node 上创建新 Pod 呢? 可以使用nodeSelector,但是建议使用nodeAffinity,因为nodeSelector将来会被nodeAffinity所取代。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: metadata.name
            operator: In
            values:
            - node-geektime

其中:

  1. requiredDuringSchedulingIgnoredDuringExecution:它的意思是说,这个 nodeAffinity 必须在每次调度的时候予以考虑。同时,这也意味着你可以设置在某些情况下不考虑这个 nodeAffinity;
  2. 这个 Pod,将来只允许运行在“metadata.name”是“node-geektime”的节点上。

Kubernetes v1.7 之后添加了一个 API 对象,名叫 ControllerRevision,专门用来记录某种 Controller 对象的版本。比如,你可以通过如下命令查看 fluentd-elasticsearch 对应的 ControllerRevision:

1
2
3
$ kubectl get controllerrevision -n kube-system -l name=fluentd-elasticsearch
NAME                               CONTROLLER                             REVISION   AGE
fluentd-elasticsearch-64dc6799c9   daemonset.apps/fluentd-elasticsearch   2          1h

4. job和cronjob


本文发表于 0001-01-01,最后修改于 0001-01-01。

本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。


上一篇 « 下一篇 »

赞赏支持

请我吃鸡腿 =^_^=

i ysf

云闪付

i wechat

微信

推荐阅读

Big Image