不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

Kubernetes编排原理——Pod

本文将开始系统学习Kubernetes的编排原理,因此先从最重要最基本的Pod开始。

为什么需要Pod

首先需要记住:

  • PodKubernetes项目的院子调度单位;
  • Namespace做隔离,Cgroups做限制,rootfs做文件系统
  • 容器的本质是进程,Kubernetes的本质是操作系统。

Pod的实现原理

首先需要明白:Pod只是一个逻辑概念,也就是说,Kubernetes真正处理的还是宿主机操作系统上Linux容器的NamespaceCgroups,并不存在所谓的Pod的边界或者隔离环境。

那么Pod又是怎么被“创建”出来的呢?——Pod其实是一组共享了某些资源的容器。Pod里的所有容器都共享一个Network Namespace,并且可以声明共享同一个Volume

问题1

这么说来,一个有A、B两个容器的Pod,不就等同于一个容器(容器A)共享另外一个容器(容器B)的网络和Volume的做法吗?
这好像通过docker run --net --volumes-from这样的命令就可以实现,比如:

1
docker run --net=B --volumes-from=B --name=A image-A ...

但是,如果容器B就必须比容器A先启动,这样一个Pod里的多个容器就不是对等关系,而是拓扑关系。

问题1解答

所以,在Kubernetes项目里,Pod的实现需要使用一个中间容器,这个容器叫作Infra容器。在这个Pod中,Infra容器永远是第一个被创建的容器,用户定义的其他容器则通过Join Network Namespace的方式与Infra容器关联在一起。

如上图所示,这个Pod里有两个用户容器A和容器B,还有一个Infra容器。
Kubernetes项目里,Infra容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作k8s.gcr.io/pause。这个镜像是用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有100~200KB。

Infra容器“hold” Network Namespace后,用户容器就可以加入Infra容器的Network Namespace中了。这也意味着,对于Pod里的容器A和容器B来说:

  • 它们可以直接使用localhost进行通信;
  • 它们“看到”的网络设备跟Infra容器“看到”的完全一样;
  • 一个Pod只有一个IP地址,也就是这个PodNetwork Namespace对应的IP地址;
  • 当然,其他所有网络资源都是一个Pod一份,并且被该Pod中所有容器共享;
  • Pod的生命周期只跟Infra容器一致,而与容器A和容器B无关。

而对于同一个Pod里的所有用户容器来说,它们的进出流量也可以都是通过Infra容器完成的。

有了这个设计之后,Kubernetes项目只要把所有Volume的定义都设计在Pod层级,就可以实现共享Volume。一个Volume对应的宿主机目录对于Pod来说只有一个,Pod里的容器只要声明挂载这个Volume,就一定可以共享这个Volume对应的宿主机目录。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
name: two-containers
spec:
restartPolicy: Never
volumes:
- name: shared-data
hostPath:
path: /data
containers:
- name: nginx-container
image: nginx
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
- name: debian-container
image: debian
volumeMounts:
- name: shared-data
mountPath: /pod-data
command: ["/bin/sh"]
args: ["-c","echo hello from the debian container > /pod-data/index.html"]

在这个例子中,debian-containernginx-container都声明挂载了shared-data这个Volume。而shared-datahostPath类型,所以它在宿主机上对应的目录就是/data。而这个目录其实被同时绑定挂载进了debian-containernginx-container两个容器。

这就是nginx-container可以从它的/usr/share/nginx/html目录读取到debian-container生成的index.html文件的原因。

Pod的本质

Pod实际上是在扮演传统基础设施里“虚拟机”的角色,容器则是这个虚拟机里运行的用户程序。

所以,当你需要把一个在虚拟机里运行的应用迁移到Docker容器中时,一定要分析到底哪些进程或组件在这个虚拟机里运行,然后就可以把整台虚拟机想象成Pod,把这些进程分别做成容器镜像,把有顺序关系的容器定义为Init Container。这是从传统应用架构到微服务架构最自然的过渡方式。

深入解析Pod对象

首先需要我们得出的结论:Pod扮演的是传统部署环境中“虚拟机”的角色。这样的设计是为了让用户从传统环境(虚拟机环境)向Kubernetes(容器环境)的迁移更加平滑。

Pod中重要字段的含义和用法

NodeSelector

一个供用户将PodNode进行绑定的字段。

1
2
3
4
5
apiVersion: v1
kind: Pod
spec:
nodeSelector:
disktype: ssd

此配置意味着Pod永远只能在携带了disktype: ssd标签的节点上运行,否则它将调度失败。

NodeName

一旦Pod的这个字段被赋值,Kubernetes项目就会认为这个Pod已调度,调度的结果就是赋值的节点名称。

注:该字段一般由调度器负责设置,但用户也可以设置它来骗过调度器。

HostAliases

定义了Podhosts文件(比如/etc/hosts)里的内容。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...

设置了一组IP和hostname的数据,当这个Pod启动后,/etc/hosts文件的内容将如下所示:

1
2
3
4
5
6
cat /etc/hosts
# Kubernetes 管理的hosts文件
127.0.0.1 localhost
...
10.1.2.3 foo.remote
10.1.2.3 bar.remote

注意:在Kubernetes项目中,如果要设置hosts文件里的内容,一定要通过这种方法;而如果直接修改了hosts文件,在Pod被删除重建之后,kubelet会自动覆盖被修改的内容。

Containers的ImagePullPolicy

定义了镜像拉取的策略。

ImagePullPolicy的默认值是Always,即每次创建Pod都重新拉取一次镜像,另外,当容器的镜像类似于nginx或者nginx:latest这样的名字时,ImagePullPolicy也会被认为Always

如果ImagePullPolicy的值被定义为Never或者IfNotPresent,则意味着Pod永远不会主动拉取这个镜像,或者只在宿主机上不存在这个镜像时才拉取。

Containers的Lifecycle

定义的是Container Lifecycle Hooks
它的作用是在容器状态发生变化时出发一系列“钩子”,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart: # 容器启动后立刻执行一个指定操作
exec:
command: ["/bin/bash", "-c","echo Hello from the postStart handler > /usr/share/message"]
postStop: # 容器被结束之前(比如收到SIGKILL信号)立刻指定一个指定操作
exec:
command: ["/bin/sbin/nginx", "-s","quit"]

在容器成功启动之后,我们在/usr/share/message里写入一句“欢迎信息”,而在这个容器被删除之前,我们先调用了Nginx的退出指令,从而实现了容器的“优雅退出”。

Pod对象在Kubernetes中的生命周期

Pod生命周期的变化主要体现在Pod API对象的Status部分,这是它除MetadataSpec外第三个重要字段。其中pod.status.phase就是Pod的当前状态,它有如下几种可能的情况:

  • Pending。这个状态意味着Pod的YAML文件已经提交给了KubernetesAPI对象已经被创建并保存到etcd中。但是这个Pod里有些容器因为某种原因不能被顺利创建。比如,调度不成功。
  • Running。这个状态下,Pod已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行。
  • Successed。这个状态意味着Pod里所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
  • Failed。这个状态下,Pod里至少有一个容器以不正常的状态(非0的返回码)退出。出现这个状态意味着需要想办法调试这个容器的应用,比如查看PodEvents日志。
  • Unknown。这是一个异常状态,意味着Pod的状态不能持续地被kubelet汇报给kube-apiserver,这很有可能是主从节点(Masterkubelet)间的通信出了问题。

Pod对象使用进阶

本节将从一种特殊的Volume(Projected Volume)开始,带你更加深入地理解Pod对象各个重要字段的含义。

目前为止,Kubernetes支持的常用Projected Volume共有以下4种

Secret

帮你把Pod想要访问的加密数据存放到etcd中,你就可以通过在Pod的容器里挂载Volume的方式访问这些Secret里保存的信息了。

典型场景:存放数据库的Credential信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
name: test-projected-volume
spec:
containers:
- name: test-projected-volume
image: busybox
args:
- sleep
- "86400"
volumeMounts:
- name: mysql-cred
mountPath: "/projected-volume"
readOnly: true
volumes:
- name: mysql-cred
projected: # 类型
sources:
- secret:
name: user # mysql用户
- secret:
name: pass # mysql密码

Secret对象的方式交给Kubernetes保存,完成这个操作的指令如下:

1
2
3
4
5
6
7
8
cat ./username.txt
admin

cat ./password.txt
admin123*

kubectl create secret generic user --form-file=./username.txt
kubectl create secret generic pass --form-file=./password.txt

username.txtpassword.txt文件里存放的就是用户名和密码,user和pass则是我为Secret对象指定的名字。而想要查看这些Secret对象的话,则只要执行一条kubectl get命令即可:

除了kubectl create secret指令,也可以直接通过编写YAML文件来创建这个Secret对象,比如:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
user: YMRtaW4= # admin -> base64转码,
pass: MWYyZDF1MmU2N2Rm # 1f2d1e2e67df -> base64转码

ConfigMap

ConfigMap保存的是无须加密的、应用所需的配置信息。除此之外,ConfigMap的用法几乎与Secret完全相同。
你可以使用kubectl create configmap从文件或者目录创建ConfigMap,也可以直接编写ConfigMap对象的YAML文件。

Download API

Download API的作用是让Pod里的容器能够直接获取这个Pod API对象本身的信息。

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
apiVersion: v1
kind: Pod
metadata:
name: test-downwardapi-volume
labels:
zone: us-est-coast
cluster: test-cluster1
rack: rack-22
spec:
containers:
- name: client-container # 定义一个容器
image: k8s.gcr.io/busybox
command: ["sh","-c"]
args:
- while true;do
if [[ -e /etc/podinfo/labels ]]; then
echo -en '\n\n'; cat /etc/podinfo/labels; fi;
sleep 5;
done;
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
readOnly: false
volumes:
- name: podinfo
projected: # 类型
sources:
- downloadAPI: # 名为podInfo的volume的数据源
items:
- path: "labels"
fieldRef:
fieldPath: metadate.labels # 暴露Pod的metadata.labels给容器

支持的字段

使用fieldRef可以声明使用:

  • metadata.name——Pod的名词
  • metadata.namespace——Pod的Namespace
  • metadata.uid——Pod的UID
  • metadata.labels[‘‘]——指定上的Label
  • metadata.annotations——Pod的所有Annotation

使用resourceFieldRef可以声明使用:

  • 容器的CPU limit;
  • 容器的CPU request;
  • 容器的memory limit;
  • 容器的memory request;
  • 容器的ephemeral-strorage limit;
  • 容器的ephemeral-strorage request。

通过环境变量声明使用:

  • status.podIP——Pod的IP
  • spec.serviceAccountName——Pod的ServiceAccount名词
  • spec.nodeName——Node的名字
  • status.hostIP——Node的IP

ServiceAccountToken

ServiceAccount对象的作用是Kubernetes系统内置的一种“服务账户”,它是Kubernetes进行权限分配的对象。比如Service Account A可以只被允许对Kubernetes API进行GET操作,而Service Account B可以有Kubernetes API的所有操作的权限。

像这样的Service Account的授权信息和文件,实际上保存在它所绑定的一个特殊的Secret对象里。这个特殊的Secret对象叫作ServiceAccountToken。任何在Kubernetes集群上运行的应用,都必须使用ServiceAccountToken里保存的授权信息(也就是Token),才可以合法地访问API Server

因此,ServiceAccountToken也可以理解为一种特殊的Secret

容器健康检查和恢复机制

Kubernetes中,可以为Pod里的容器定义一个健康检查“探针”(Probe)。这样的,kubelet就会根据Probe的返回值决定这个容器的状态,而不是直接以容器是否运行(来自Docker返回的信息)作为依据。这种机制是生产环境中保证应用健康的重要手段。

例子:

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
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: test-liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
# 容器启动之后在/tmp目录下创建一个healthy文件,以此作为自己已经正常运行的标志
# 30秒后,把这个文件删除
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe: # 定义一个健康检查
exec: # 类型是 exec
command:
# 检查healthy文件如果存在,这条命令的返回值是0
# Pod就会认为这个容器不仅已经启动,而且是健康的
- cat
- /tmp/healthy
initialDelaySeconds: 5 # 在容器启动5秒后检查
periodSeconds: 5 # 每5秒执行一次

执行命令,创建Pod

1
kubectl create -f liveness.yaml

查看这个Pod的状态:

30秒之后再查看一下PodEvents

发现已经报告了异常,tmp/healthy文件不存在了,然而Pod的状态仍然是Running。为什么?

因为Kubernetes中没有DockerStop语义。所以虽说是Restart,实际上却是重新创建了容器。

这个功能就是Kubernetes里的Pod恢复机制,也叫restartPolicy。它是Pod的Sepc部分的一个标准字段(pod.spec.restartPolicy),默认值是Always,即无论这个容器何时发生异常,它一定会被重启。

总结

本文重点介绍了Pod相关的内容,有Pod实现原理,本质,Pod重要字段的含义和用法,以及声明周期,还有实践“容器健康检查”机制,最后又引出了KubernetesPod恢复机制

Pod是学习Kubernetes很重要的一环,后期还需要多实践,体会用法。

------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!