运行应用

这一节从本章开始,我们将通过实践深入学习Kubernetes的各种特性。作为容器编排引擎,最重要也是最基本的功能当然是运行容器化应用。

Kubernetes通过各种Controller来管理Pod的生命周期。为了满足不同业务场景,Kubernetes开发了Deployment、ReplicaSet、DaemonSet、StatefuleSet、Job等多种Controller。我们首先学习最常用的Deployment。

Deployment

运行Deployment

上一个示例中已经通过Deployment部署了nginx-app,

image-20230218234123895

image-20230218234205176

通过kubectl get deployment命令查看nginx-app的状态,输出显示两个副本正常运行。

接下来我们用kubectl describe deployment了解更详细的信息:

image-20230218234425527

大部分内容都是自解释的,我们重点看上图Type部分。这里告诉我们创建了一个ReplicaSet nginx-app-7f4fc68488,Events是Deployment的日志,记录了ReplicaSet的启动过程。通过上面的分析,也验证了Deployment通过ReplicaSet来管理Pod的事实。接着我们将注意力切换到nginx-app-7f4fc68488,执行kubectl describe replicaset,

image-20230218234852539

两个副本已经就绪,用kubectl describe replicaset查看详细信息,

image-20230218235041696

Controlled By指明此ReplicaSet是由Deployment nginx-app创建的。

接着我们来看下两个副本Pod创建的日志,接着我们来看Pod,执行kubectl get pod

image-20230218235410074

两个副本Pod都处于Running状态,然后用kubectl describe pod查看更详细的信息,

image-20230218235709862

image-20230218235831875

Controlled By指明此Pod是由ReplicaSet nginx-app-7f4fc68488创建的。Events记录了Pod的启动过程。如果操作失败(比如image不存在),也能在这里查到原因。

总结一下这个过程中,如下图所示:

  1. 用户通过kubectl创建Deployment。
  2. Deployment创建ReplicaSet。
  3. ReplicaSet创建Pod。

image-20230219000244511

从图中也可以看出,对象的命名方式是**“子对象的名字=父对象名字+随机字符串或数字”。**

命令VS配置文件

Kubernetes支持两种创建资源的方式:

  1. 用kubectl命令直接创建,比如“kubectl run nginx-deployment –image=nginx:1.7.9–replicas=2”,在命令行中通过参数指定资源的属性。

  2. 通过配置文件和kubectl apply创建。要完成前面同样的工作,可执行命令“kubectl apply -f nginx.yml”,nginx.yml的内容如下所示。

     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
    
    # API 版本号
    apiVersion: apps/v1
    # 类型,如:Pod/ReplicationController/Deployment/Service/Ingress
    kind: Deployment
    metadata:
      # Kind 的名称
      name: nginx-app
    spec:
      selector:
        matchLabels:
          # 容器标签的名字,发布 Service 时,selector 需要和这里对应
          app: nginx
      # 部署的实例数量
      replicas: 2
      template:
        metadata:
          labels:
            app: nginx
        spec:
          # 配置容器,数组类型,说明可以配置多个容器
          containers:
          # 容器名称
          - name: nginx
            # 容器镜像
            image: nginx:1.17
            # 只有镜像不存在时,才会进行镜像拉取
            imagePullPolicy: IfNotPresent
            ports:
            # Pod 端口
            - containerPort: 80
    

    资源的属性写在配置文件中,文件格式为YAML。

下面对这两种方式进行比较。

(1)基于命令的方式:简单、直观、快捷,上手快。适合临时测试实验

(2)基于配置文件的方式:配置文件描述了What,即应用最终要达到的状态。

配置文件提供了创建资源的模板,能够重复部署。可以像管理代码一样管理部署。适合正式的、跨环境的、规模化部署。这种方式要求熟悉配置文件的语法,有一定难度。

kubectl apply不但能够创建Kubernetes资源,也能对资源进行更新,非常方便。不过Kubernets还提供了几个类似的命令,例如kubectl create、kubectl replace、kubectl edit和kubectl patch。

为避免造成不必要的困扰,我们会尽量只使用kubectl apply,此命令已经能够应对百分之九十多的场景,事半功倍。

Deployment配置文件

既然要用YAML配置文件部署应用,现在就很有必要了解一下Deployment的配置格式了,其他Controller(比如DaemonSet)非常类似。

以nginx-deployment为例,配置文件如下图所示。

image-20230219001355136

①apiVersion是当前配置格式的版本。

②kind是要创建的资源类型,这里是Deployment。

③metadata是该资源的元数据,name是必需的元数据项。

④spec部分是该Deployment的规格说明。

⑤replicas指明副本数量,默认为1。

⑥template定义Pod的模板,这是配置文件的重要部分。

⑦metadata定义Pod的元数据,至少要定义一个label。label的key和value可以任意指定。

⑧spec描述Pod的规格,此部分定义Pod中每一个容器的属性,name和image是必需的。

这个nginx.yml是一个最简单的Deployment配置文件,后面我们学习Kubernetes各项功能时会逐步丰富这个文件。

伸缩

伸缩是指在线增加或减少Pod的副本数。

Deployment nginx-app初始是两个副本,而且是k8s-node1和k8s-node2上各跑了一个副本。

image-20230219001807024

现在修改yaml文件,将副本数改成5个,

image-20230219002043527

image-20230219002409217

三个新副本被创建并调度到k8s-node3和k8s-node4上。

出于安全考虑,默认配置下Kubernetes不会将Pod调度到Master节点。如果希望将k8s-master也当作Node使用,可以执行如下命令:

1
kubectl taint node k8s-master node-role.kubernetes.io/master-

如果要恢复Master Only的状态,执行如下命令:

1
kubectl taint node k8s-master node-role.kubernetes.io/master="":NoSchedule

接下来修改配置文件,将副本数减少为2个, 重新恢复为原来的样子,重新执行kubectl apply,

image-20230219003022493

可以看到3个副本被删除,最终保留了2个副本。

FailOver

为了演示这个问题, 重新构建了一主两从的节点, 来演示停机的问题;

image-20230219130138189

用户之前的yaml来生成3个副本,

image-20230219130451586

模拟k8s-node2故障,关闭该节点,

image-20230219130829251

等待一段时间,Kubernetes会检查到k8s-node2不可用,将k8s-node2上的Pod标记为Unknown状态,并在k8s-node1上新创建两个Pod,维持总副本数为3,

image-20230219133938200

当k8s-node2恢复后,Unknown的Pod会被删除,不过已经运行的Pod不会重新调度回k8s-node2,

image-20230219134124889

删除nginx-app控制器,如下图所示:

image-20230219134315610

用label控制Pod的位置

默认配置下,Scheduler会将Pod调度到所有可用的Node。不过有些情况我们希望将Pod部署到指定的Node,比如将有大量磁盘I/O的Pod部署到配置了SSD的Node;或者Pod需要GPU,需要运行在配置了GPU的节点上。

Kubernetes是通过label来实现这个功能的。label是key-value对,各种资源都可以设置label,灵活添加各种自定义属性。

比如执行如下命令标注k8s-node1是配置了SSD的节点。

1
kubectl label node k8s-node1 disktype=ssd

image-20230219134725467

disktype=ssd已经成功添加到k8s-node1,除了disktype,Node还有几个Kubernetes自己维护的label。有了disktype这个自定义label,接下来就可以指定将Pod部署到k8s-node1。

image-20230219134957163

在Pod模板的spec里通过nodeSelector指定将此Pod部署到具有label disktype=ssd的Node上。部署Deployment并查看Pod的运行节点。全部2个pod副本都运行在k8s-node1上,符合我们的预期。

要删除label disktype,执行如下命令:

kubectl label node k8s-node1 disktype-

不过此时Pod并不会重新部署,依然在k8s-node1上运行,除非在nginx.yml中删除nodeSelector设置,然后通过kubectl apply重新部署。Kubernetes会删除之前的Pod并调度和运行新的Pod。

DaemonSet

Deployment部署的副本Pod会分布在各个Node上,每个Node都可能运行好几个副本。DaemonSet的不同之处在于:每个Node上最多只能运行一个副本。

DaemonSet的典型应用场景有:

  • 在集群的每个节点上运行存储Daemon,比如glusterd或ceph。
  • 在每个节点上运行日志收集Daemon,比如flunentd或logstash。
  • 在每个节点上运行监控Daemon,比如Prometheus Node Exporter或collectd。

实际上,Kubernetes自己就是在用DaemonSet运行系统组件。

image-20230222224210734

DaemonSet tke-cni-agent和kube-proxy分别负责在每个节点上运行tke-cni-agent和kube-proxy组件。

image-20230222224604848

因为tke-cni-agent和kube-proxy属于系统组件,,需要在命令行中通过–namespace=kube-system指定namespace kube-system。若不指定,则只返回默认namespace default中的资源。

tke-cni-agent

 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
apiVersion: apps/v1
kind: DaemonSet
metadata:
  annotations:
    deprecated.daemonset.template.generation: "1"
  creationTimestamp: "2022-10-20T02:38:51Z"
  generation: 1
  labels:
    k8s-app: tke-cni-agent
  name: tke-cni-agent
  namespace: kube-system
  resourceVersion: "29185904014"
  selfLink: /apis/apps/v1/namespaces/kube-system/daemonsets/tke-cni-agent
  uid: 9079ae3d-e069-47d3-982b-63635936f8e4
spec:
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: tke-cni-agent
  template:
    metadata:
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ""
      creationTimestamp: null
 labels:
        k8s-app: tke-cni-agent
    spec:
      containers:
      - env:
        - name: K8S_NODE_NAME
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: spec.nodeName
        image: ccr.ccs.tencentyun.com/tkeimages/tke-cni-agent:v0.1.2
        imagePullPolicy: Always
        name: tke-cni-agent
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /host/opt/cni/bin
          name: cni-bin-dir
        - mountPath: /host/etc/cni/net.d
          name: cni-net-dir
        - mountPath: /host/etc/kubernetes/
          name: kube-conf-dir
        - mountPath: /etc/tke-cni-agent-conf
          name: tke-cni-agent-conf
      dnsPolicy: ClusterFirst
      hostNetwork: true
      priorityClassName: system-node-critical
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      serviceAccount: tke-cni
      serviceAccountName: tke-cni
      terminationGracePeriodSeconds: 0
      tolerations:
      - operator: Exists
      volumes:
      - hostPath:
      path: /opt/cni/bin
      type: ""
      name: cni-bin-dir
      - hostPath:
      path: /etc/cni/net.d
      type: ""
      name: cni-net-dir
      - hostPath:
      path: /etc/kubernetes
      type: ""
      name: kube-conf-dir
      - configMap:
      defaultMode: 420
      name: tke-cni-agent-conf
      name: tke-cni-agent-conf
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 1
    type: RollingUpdate
status:
  currentNumberScheduled: 5
  desiredNumberScheduled: 5
  numberAvailable: 5
  numberMisscheduled: 0
  numberReady: 5
  observedGeneration: 1
  updatedNumberScheduled: 5

注意:配置文件的完整内容要更复杂一些,为了更好地学习DaemonSet,这里只保留了最重要的内容。

  • DaemonSet配置文件的语法和结构与Deployment几乎完全一样,只是将kind设为DaemonSet。
  • hostName指定Pod直接使用的是Node网络,相当于docker run –network=host。考虑到cni需要为集群提供网络连接,这个要求是合理的。
  • containers定义了运行cni服务的容器。

kube-proxy

由于无法拿到kube-proxy的YAML文件,只能运行如下命令查看配置:

image-20230222230152693

同样这里为了方便理解, 也是只是摘取最重要的部分信息。

①kind: DaemonSet指定这是一个DaemonSet类型的资源。

②containers定义了kube-proxy的容器。

③status是当前DaemonSet的运行时状态,这个部分是kubectl edit特有的。

其实Kubernetes集群中每个当前运行的资源都可以通过kubectl edit查看其配置和运行状态,比如kubectl edit deployment nginx-deployment。

运行自己的DaemonSet

这里以Prometheus Node Exporter为例演示用户如何运行自己的DaemonSet。

Prometheus是流行的系统监控方案,Node Exporter是Prometheus的agent,以Daemon的形式运行在每个被监控节点上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
docker run -d \

-v "/proc:/host/proc" \

-v "/sys:/host/sys" \

-v "/:/rootfs" \

--net=host \  prom/node-exporter \

--path.procfs /host/proc \

--path.sysfs /host/sys \

--collector.filesystem.ignored-mount-points "^/(sys|proc|dev|host|etc)($|/)"

将其转换为DaemonSet的YAML配置文件node_exporter.yml,

 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
43
44
45
apiVersion: apps/v1                             #Api接口版本
kind: DaemonSet                                 #定义控制器
metadata:                                       #信息
  name: node-exporter-daemonset                 #deployment名称
spec:                                           #详细信息
  #replicas: 3                                   #副本数
  selector:                                     #选择标签
    matchLabels:                                 #标签匹配
     app: prometheus                            #匹配prometheus
 
  template:                                     #pod容器
    metadata:                                   #具体信息
      labels:                                   #定义标签
        app: prometheus                         #标签prometheus
    spec:
      hostNetwork: true   // 1
      containers:
      - name: node-exporter
        image: prom/node-exporter
        imagePullPolicy: IfNotPresent
        command:		// 2
        - /bin/node_exporter
        - --path.procfs
        -  /host/proc
        - --path.sysfs
        -  /host/sys
        - --collector.filesystem.ignored-mount-points
        - ^/(sys|proc|dev|host|etc)($|/)
        volumeMounts:	// 3
        - name: proc
          mountPath: /host/proc
        - name: sys
          mountPath: /host/sys
        - name: root
          mountPath: /rootfs
      volumes:
      - name: proc
        hostPath:
          path: /proc
      - name: sys
        hostPath:
          path: /sys
      - name: root
        hostPath:
          path: /

image-20230222234855148

①直接使用Host的网络。

②设置容器启动命令。

③通过Volume将Host路径/proc、/sys和/映射到容器中。

从上图可知, DaemonSet node-exporter-daemonset部署成功,k8s-node1和k8s-node2上分别运行了一个node exporter Pod。

Job

容器按照持续运行的实践可分为两类容器:服务类容器和工作类容器。

服务类容器通常持续提供服务,需要一直运行,比如HTTP Server、Daemon等。

工作类容器则是一次性任务,比如批处理程序,完成后容器就退出。

Kubernetes的Deployment、ReplicaSet和DaemonSet都用于管理服务类容器;对于工作类容器,我们使用Job。

先来做一个简单的Job配置文件myjob.yml,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: batch/v1   // 1
kind: Job  // 2
metadata:
  name: myjob
spec:
  template:
    metadata:
      name: myjob
    spec:
      containers:
      - name: hello
        image: busybox
        command: ["echo","hello k8s i love you"]
      restartPolicy: Never  // 3

①batch/v1是当前Job的apiVersion。

②指明当前资源的类型为Job。

③restartPolicy指定什么情况下需要重启容器。对于Job,只能设置为Never或者OnFailure。对于其他controller(比如Deployment),可以设置为Always。

image-20230226000032955

通过kubectl get job查看Job的状态,

image-20230226000138033

COMPLETIONS为1/1表示按照预期启动了一个Pod,并且已经成功执行。通过kubectl get pod查看Pod的状态

image-20230226000510762

可以看到Pod执行完毕后容器已经退出。

通过kubectl logs可以查看Pod的标准输出。

image-20230226000723644

Pod失败的情况

上述都是pod成功执行的情况,但如果Pod失败了是什么样子呢?

我们实验一下,修改myjob.yml,故意引入一个错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: batch/v1   // 1
kind: Job  // 2
metadata:
  name: myjob
spec:
  template:
    metadata:
      name: myjob
    spec:
      containers:
      - name: hello
        image: busybox
        command: ["invalid_command","hello k8s i love you"]
      restartPolicy: Never  // 3

先删除之前的job,

image-20230226001222122

运行新的Job并查看状态:

image-20230226001609678

可以看到有多个Pod, 状态都不正常;

image-20230226002032299

可以通过kubectl describe pod查看某个Pod的启动日志,

image-20230226002232370

日志显示没有可执行程序,这符合我们的预期;

下面解释一个现象:为什么kubectl get pod会看到这么多个失败的Pod?

**原因是:**当第一个Pod启动时,容器失败退出,根据restartPolicy: Never,此失败容器不会被重启,但Job DESIRED的Pod是1,目前SUCCESSFUL为0,不满足,所以Job controller会启动新的Pod,直到SUCCESSFUL为1。对于我们这个例子,SUCCESSFUL永远也到不了1,所以Job controller会一直创建新的Pod(新的k8s看起来会设置一个limit, 重试过多会达到Job has reached the specified backoff limit)。为了终止这个行为,只能删除Job,

image-20230226002749980

如果将restartPolicy设置为OnFailure会怎么样?下面我们实践一下,修改myjob.yml后重新启动:

image-20230226003031819

Job的SUCCESSFUL Pod数量还是0,再看看Pod的情况,

image-20230226003214711

这里只有一个Pod,不过RESTARTS为3,而且不断增加,说明OnFailure生效,容器失败后会自动重启。

Job的并行性

有时我们希望能同时运行多个Pod,提高Job的执行效率。这个可以通过parallelism设置,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: batch/v1
kind: Job
metadata:
  name: myjob
spec:
  parallelism: 2
  template:
    metadata:
      name: myjob
    spec:
      containers:
      - name: hello
        image: busybox
        command: ["echo","hello k8s i love you"]
      restartPolicy: OnFailure

这里我们将并行的Pod数量设置为2,实践一下:

image-20230226003950849

Job一共启动了两个Pod,而且AGE相同,可见是并行运行的。

我们还可以通过completions设置Job成功完成Pod的总数,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: batch/v1
kind: Job
metadata:
  name: myjob
spec:
  completions: 6
  parallelism: 2
  template:
    metadata:
      name: myjob
    spec:
      containers:
      - name: hello
        image: busybox
        command: ["echo","hello k8s i love you"]
      restartPolicy: OnFailure

上面配置的含义是:每次运行两个Pod,直到总共有6个Pod成功完成。实践一下,

image-20230226005035536

DESIRED和SUCCESSFUL均为6,符合预期。如果不指定completions和parallelism,默认值均为1。

上面的例子只是为了演示Job的并行特性,实际用途不大。不过现实中确实存在很多需要并行处理的场景。比如批处理程序,每个副本(Pod)都会从任务池中读取任务并执行,副本越多,执行时间就越短,效率就越高。这种类似的场景都可以用Job来实现。

定时Job

Linux中有cron程序定时执行任务,Kubernetes的CronJob提供了类似的功能,可以定时执行Job。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#      ┌────────────────── 时区 (可选)
#      |      ┌───────────── 分钟 (0 - 59)
#      |      │ ┌───────────── 小时 (0 - 23)
#      |      │ │ ┌───────────── 月的某天 (1 - 31)
#      |      │ │ │ ┌───────────── month (1 - 12)
#      |      │ │ │ │ ┌───────────── 周的某天 (0 - 6)(周日到周一;在某些系统上,7 也是星期日)
#      |      │ │ │ │ │                                
#      |      │ │ │ │ │
#      |      │ │ │ │ │
# CRON_TZ=UTC * * * * *

CronJob配置文件如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            command: ["echo","hello k8s job!"]
            command: ["/bin/sh","-c","date"]
          restartPolicy: OnFailure

①batch/v2alpha1是当前CronJob的apiVersion。

②指明当前资源的类型为CronJob。

③schedule指定什么时候运行Job,其格式与Linux cron一致。这里*/1 * * * *的含义是每一分钟启动一次。

④jobTemplate定义Job的模板,格式与前面的Job一致。

接下来,通过kubectl apply创建CronJob:

通过kubectl get cronjob查看CronJob的状态,

image-20230226010721287

等待几分钟,然后通过kubectl get jobs查看Job的执行情况,

image-20230226011029175

可以看到每隔一分钟就会启动一个Job。执行kubectl logs可查看某个Job的运行日志,

image-20230226011230795

cronjob可以自动清理任务,默认保留3次成功的任务,我们可以通过添加.spec.successfulJobsHistoryLimit改变保留的历史任务信息即Pod。

Job期限与清理

除了Job执行结束与重启失败认定的Job 终止外还可以通过配置活跃期限(activeDeadlineSeconds)来自动停止Job任务。

我们可以为 Job 的 .spec.activeDeadlineSeconds 设置一个秒数值。 该值适用于 Job 的整个生命期,无论 Job 创建了多少个 Pod。 一旦 Job 运行时间达到 activeDeadlineSeconds 秒,其所有运行中的 Pod 都会被终止,并且 Job 的状态更新为 type: Failedreason: DeadlineExceeded

注意 Job 的 .spec.activeDeadlineSeconds 优先级高于其 .spec.backoffLimit 设置。 因此,如果一个 Job 正在重试一个或多个失效的 Pod,该 Job 一旦到达 activeDeadlineSeconds 所设的时限即不再部署额外的 Pod,即使其重试次数还未 达到 backoffLimit 所设的限制。

清理job和终止相似,我们可以通过添加spec.ttlSecondsAfterFinished使Job在任务完成后一段时间内被清理。