第5章 运行应用

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

5.1 Deployment

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

5.1.1 运行Deployment

先从例子开始,运行一个Deployment:

kubectl run nginx-deployment --image=nginx:1.7.9 --replicas=2

上面的命令将部署包含两个副本的Deployment nginx-deployment,容器的image为nginx:1.7.9。

下面详细分析Kubernetes都做了些什么工作,如图5-1所示。

图5-1

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

接下来我们用kubectl describe deployment了解更详细的信息,如图5-2和图5-3所示。

图5-2

图5-3

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

图5-4

两个副本已经就绪,用kubectl describe replicaset查看详细信息,如图5-5和图5-6所示。

图5-5

图5-6

Controlled By指明此ReplicaSet是由Deployment nginx-deployment创建的。图5-6是两个副本Pod创建的日志。接着我们来看Pod,执行kubectl get pod,如图5-7所示。

图5-7

两个副本Pod都处于Running状态,然后用kubectl describe pod查看更详细的信息,如图5-8和图5-9所示。

图5-8

图5-9

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

总结一下这个过程中,如图5-10所示。

图5-10

(1)用户通过kubectl创建Deployment。

(2)Deployment创建ReplicaSet。

(3)ReplicaSet创建Pod。

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

5.1.2 命令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的内容如图5-11所示。

图5-11

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

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

(1)基于命令的方式:

(2)基于配置文件的方式:

后面我们都将采用配置文件的方式,大家需要尽快熟悉和掌握。

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

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

5.1.3 Deployment配置文件简介

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

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

图5-12

① 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各项功能时会逐步丰富这个文件。

执行kubectl apply -f nginx.yml,如图5-13所示。

图5-13

部署成功。同样,也可以通过kubectl get查看nginx-deployment的各种资源,如图5-14所示。

图5-14

Deployment、ReplicaSet、Pod都已经就绪。如果要删除这些资源,执行kubectl delete deployment nginx-deployment或者kubectl delete -f nginx.yml,如图5-15所示。

图5-15

5.1.4 伸缩

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

Deployment nginx-deployment初始是两个副本,如图5-16所示。

图5-16

k8s-node1和k8s-node2上各跑了一个副本。现在修改nginx.yml文件,将副本改成5个,如图5-17所示。

图5-17

再次执行kubectl apply,如图5-18所示。

图5-18

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

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

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

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

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

接下来修改配置文件,将副本数减少为3个,重新执行kubectl apply,如图5-19所示。

图5-19

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

5.1.5 Failover

下面我们模拟k8s-node2故障,关闭该节点,如图5-20所示。

图5-20

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

图5-21

当k8s-node2恢复后,Unknown的Pod会被删除,不过已经运行的Pod不会重新调度回k8s-node2,如图5-22所示。

图5-22

删除nginx-deployment,如图5-23所示。

图5-23

5.1.6 用label控制Pod的位置

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

Kubernetes是通过label来实现这个功能的。

label是key-value对,各种资源都可以设置label,灵活添加各种自定义属性。比如执行如下命令标注k8s-node1是配置了SSD的节点。

kubectl label node k8s-node1 disktype=ssd

然后通过kubectl get node --show-labels查看节点的label,如图5-24所示。

图5-24

disktype=ssd已经成功添加到k8s-node1,除了disktype,Node还有几个Kubernetes自己维护的label。

有了disktype这个自定义label,接下来就可以指定将Pod部署到k8s-node1。编辑nginx.yml,如图5-25所示。

图5-25

在Pod模板的spec里通过nodeSelector指定将此Pod部署到具有label disktype=ssd的Node上。

部署Deployment并查看Pod的运行节点,如图5-26所示。

图5-26

全部6个副本都运行在k8s-node1上,符合我们的预期。要删除label disktype,执行如下命令:

kubectl label node k8s-node1 disktype-

- 即删除,如图5-27所示。

图5-27

不过此时Pod并不会重新部署,依然在k8s-node1上运行,如图5-28所示。

图5-28

除非在nginx.yml中删除nodeSelector设置,然后通过kubectl apply重新部署,如图5-29所示。

图5-29

Kubernetes会删除之前的Pod并调度和运行新的Pod。

5.2 DaemonSet

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

DaemonSet的典型应用场景有:

(1)在集群的每个节点上运行存储Daemon,比如glusterd或ceph。

(2)在每个节点上运行日志收集Daemon,比如flunentd或logstash。

(3)在每个节点上运行监控Daemon,比如Prometheus Node Exporter或collectd。

其实Kubernetes自己就在用DaemonSet运行系统组件。执行如下命令,如图5-30所示。

kubectl get daemonset --namespace=kube-system

图5-30

DaemonSet kube-flannel-ds和kube-proxy分别负责在每个节点上运行flannel和kube-proxy组件,如图5-31所示。

图5-31

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

5.2.1 kube-flannel-ds

下面我们通过分析kube-flannel-ds来学习DaemonSet。

还记得之前是如何部署flannel网络的吗?我们执行了如下命令:

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

flannel的DaemonSet就定义在kube-flannel.yml中,如图5-32所示。

图5-32

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

① DaemonSet配置文件的语法和结构与Deployment几乎完全一样,只是将kind设为DaemonSet。

② hostName指定Pod直接使用的是Node网络,相当于docker run --network=host。考虑到flannel需要为集群提供网络连接,这个要求是合理的。

③ containers定义了运行flannel服务的两个容器。

下面我们再来分析另一个DaemonSet:kube-proxy。

5.2.2 kube-proxy

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

kubectl edit daemonset kube-proxy --namespace=kube-system

结果如图5-33所示。

图5-33

同样为了便于理解,这里只保留了最重要的信息。

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

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

③ status是当前DaemonSet的运行时状态,这个部分是kubectl edit特有的。其实Kubernetes集群中每个当前运行的资源都可以通过kubectl edit查看其配置和运行状态,比如kubectl edit deployment nginx-deployment。

5.2.3 运行自己的DaemonSet

本小节以Prometheus Node Exporter为例演示用户如何运行自己的DaemonSet。

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

如果是直接在Docker中运行Node Exporter容器,命令为:

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,如图5-34所示。

图5-34

① 直接使用Host的网络。

② 设置容器启动命令。

③ 通过Volume将Host路径/proc、/sys和/映射到容器中。我们将在后面详细讨论Volume。

执行kubectl apply -f node_exporter.yml,如图5-35所示。

图5-35

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

5.3 Job

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

服务类容器通常持续提供服务,需要一直运行,比如HTTP Server、Daemon等。工作类容器则是一次性任务,比如批处理程序,完成后容器就退出。

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

先看一个简单的Job配置文件myjob.yml,如图5-36所示。

图5-36

① batch/v1是当前Job的apiVersion。

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

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

通过kubectl apply -f myjob.yml启动Job,如图5-37所示。

图5-37

通过kubectl get job查看Job的状态,如图5-38所示。

图5-38

DESIRED和SUCCESSFUL都为1,表示按照预期启动了一个Pod,并且已经成功执行。通过kubectl get pod查看Pod的状态,如图5-39所示。

图5-39

因为Pod执行完毕后容器已经退出,需要用--show-all才能查看Completed状态的Pod。

通过kubectl logs可以查看Pod的标准输出,如图5-40所示。

图5-40

5.3.1 Pod失败的情况

以上是Pod成功执行的情况,如果Pod失败了会怎么样呢?

我们做个试验,修改myjob.yml,故意引入一个错误,如图5-41所示。

图5-41

先删除之前的Job,如图5-42所示。

图5-42

运行新的Job并查看状态,如图5-43所示。

图5-43

当前SUCCESSFUL的Pod数量为0,查看Pod的状态,如图5-44所示。

图5-44

可以看到有多个Pod,状态均不正常。通过kubectl describe pod查看某个Pod的启动日志,如图5-45所示。

图5-45

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

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

原因是:当第一个Pod启动时,容器失败退出,根据restartPolicy: Never,此失败容器不会被重启,但Job DESIRED的Pod是1,目前SUCCESSFUL为0,不满足,所以Job controller会启动新的Pod,直到SUCCESSFUL为1。对于我们这个例子,SUCCESSFUL永远也到不了1,所以Job controller会一直创建新的Pod。为了终止这个行为,只能删除Job,如图5-46所示。

图5-46

如果将restartPolicy设置为OnFailure会怎么样?下面我们实践一下,修改myjob.yml后重新启动,如图5-47所示。

图5-47

Job的SUCCESSFUL Pod数量还是0,再看看Pod的情况,如图5-48所示。

图5-48

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

5.3.2 Job的并行性

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

图5-49

这里我们将并行的Pod数量设置为2,实践一下,如图5-50所示。Job一共启动了两个Pod,而且AGE相同,可见是并行运行的。

图5-50

我们还可以通过completions设置Job成功完成Pod的总数,如图5-51所示。

图5-51

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

图5-52

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

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

5.3.3 定时Job

Linux中有cron程序定时执行任务,Kubernetes的CronJob提供了类似的功能,可以定时执行Job。CronJob配置文件示例如图5-53所示。

图5-53

① batch/v2alpha1是当前CronJob的apiVersion。

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

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

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

接下来通过kubectl apply创建CronJob,如图5-54所示。

图5-54

失败了。这是因为Kubernetes默认没有enable CronJob功能,需要在kube-apiserver中加入这个功能。方法很简单,修改kube-apiserver的配置文件/etc/kubernetes/manifests/kubeapiserver.yaml,如图5-55所示。

图5-55

kube-apiserver本身也是一个Pod,在启动参数中加上--runtime-config=batch/v2alpha1=true即可。然后重启kubelet服务:

systemctl restart kubelet.service

kubelet会重启kube-apiserver Pod。通过kubectl api-versions确认kube-apiserver现在已经支持batch/v2alpha1,如图5-56所示。

图5-56

再次创建CronJob,如图5-57所示。

图5-57

这次成功了。通过kubectl get cronjob查看CronJob的状态,如图5-58所示。

图5-58

等待几分钟,然后通过kubectl get jobs查看Job的执行情况,如图5-59所示。

图5-59

可以看到每隔一分钟就会启动一个Job。执行kubectl logs可查看某个Job的运行日志,如图5-60所示。

图5-60

5.4 小结

运行容器化应用是Kubernetes最重要的核心功能。为满足不同的业务需要,Kubernetes提供了多种Controller,包括Deployment、DaemonSet、Job、CronJob等。本章我们通过实践详细学习了这些Controller,并讨论了它们的特性和应用场景。