为什么我们需要 Pod ?
Pod 是 Kubernetes 项目中最小的 API 对象。如果换一个更专业的说法,我们可以这样描述:Pod 是 Kubernetes 项目的原子调度单位。
容器的本质到底是什么?容器的本质是进程。容器镜像就是这个系统里的“.exe”安装包。
那么 Kubernetes 呢?Kubernetes 就是操作系统!
首先,关于 Pod 最重要的一个事实是:它只是一个逻辑概念。具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。
在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。这样的组织关系,可以用下面这样一个示意图来表达:
如上图所示,这个 Pod 里有两个用户容器 A 和 B,还有一个 Infra 容器。很容易理解,在 Kubernetes 项目里,Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。
这也就意味着,对于 Pod 里的容器 A 和容器 B 来说:
它们可以直接使用 localhost 进行通信;
它们看到的网络设备跟 Infra 容器看到的完全一样;
一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享;
Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
对于共享 Volume, Kubernetes 项目只要把所有 Volume 的定义都设计在 Pod 层级即可。
在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
所以,你现在可以这么理解 Pod 的本质:Pod,实际上是在扮演传统基础设施里“虚拟机”的角色;而容器,则是这个虚拟机里运行的用户程序。
然后,你就可以把整个虚拟机想象成为一个 Pod,把这些进程分别做成容器镜像,把有顺序关系的容器,定义为 Init Container。这才是更加合理的、松耦合的容器编排诀窍,也是从传统应用架构,到“微服务架构”最自然的过渡方式。
深入解析POD
凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的,下面介绍 Pod 中几个重要字段的含义和用法:
NodeSelector:是一个供用户将 Pod 与 Node 进行绑定的字段,用法如下所示:
apiVersion: v1
kind: Pod
...
spec:
nodeSelector: #意味着这个 Pod 永远只能运行在携带了“disktype: ssd”标签(Label)的节点上
disktype: ssd
HostAliases:定义了 Pod 的 hosts 文件(比如 /etc/hosts)里的内容,用法如下:
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...
#在 Kubernetes 项目中,一定要通过这种方法设置 hosts 文件里的内容。
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote
除了上述跟“机器”相关的配置外,你可能也会发现,凡是跟容器的 Linux Namespace 相关的属性,也一定是 Pod 级别的。这个原因也很容易理解:Pod 的设计,就是要让它里面的容器尽可能多地共享 Linux Namespace,仅保留必要的隔离和限制能力。
举个例子,在下面这个 Pod 的 YAML 文件定义了 shareProcessNamespace=true:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
shareProcessNamespace: true #意味着这个 Pod 里的容器要共享 PID Namespace
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
这个 Pod 被创建后,你就可以使用 shell 容器的 tty 跟这个容器进行交互了。我们一起实践一下:
$ kubectl create -f nginx.yaml
[root@kubeadm ~]# kubectl attach -it nginx -c shell
If you don't see a command prompt, try pressing enter.
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /pause
6 root 0:00 nginx: master process nginx -g daemon off;
33 101 0:00 nginx: worker process
40 root 0:00 sh
50 root 0:00 ps aux
注:可以看到,在这个容器里,我们不仅可以看到它本身的 ps ax 指令,还可以看到 nginx 容器的进程,以及 Infra 容器的 /pause 进程。这就意味着,整个 Pod 里的每个容器的进程,对于所有容器来说都是可见的:它们共享了同一个 PID Namespace。
类似地,凡是 Pod 中的容器要共享宿主机的 Namespace,也一定是 Pod 级别的定义,比如:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
hostNetwork: true
hostIPC: true
hostPID: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
注:在这个 Pod 中,我定义了共享宿主机的 Network、IPC 和 PID Namespace。这就意味着,这个 Pod 里的所有容器,会直接使用宿主机的网络、直接与宿主机进行 IPC 通信、看到宿主机里正在运行的所有进程。
ImagePullPolicy 字段:
它定义了镜像拉取的策略。而它之所以是一个 Container 级别的属性,是因为容器镜像本来就是 Container 定义中的一部分。magePullPolicy 的值默认是 Always,即每次创建 Pod 都重新拉取一次镜像。另外,当容器的镜像是类似于 nginx 或者 nginx:latest 这样的名字时,ImagePullPolicy 也会被认为 Always。而如果它的值被定义为 Never 或者 IfNotPresent,则意味着 Pod 永远不会主动拉取这个镜像,或者只在宿主机上不存在这个镜像时才拉取。
Lifecycle 字段:它定义的是 Container Lifecycle Hooks。顾名思义,Container Lifecycle Hooks 的作用,是在容器状态发生变化时触发一系列“钩子”。例:
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart: #在容器启动后,立刻执行一个指定的操作。
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop: #则是容器被杀死之前需要执行的操作,直到这个 Hook 定义操作完成之后,才允许容器被杀死
exec:
command: ["/usr/sbin/nginx","-s","quit"]
Kubernetes 支持的 Projected Volume
- Secret;
- ConfigMap;
- Downward API;
- ServiceAccountToken。
参考链接:Projected Volume
容器健康检查和恢复机制
在 Kubernetes 中,你可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器进行是否运行(来自 Docker 返回的信息)作为依据。这种机制,是生产环境中保证应用健康存活的重要手段。
用一个例子来说明:
[root@kubeadm ~]# vi liveness.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: test-liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe: #健康检查
exec:
command:
- cat
- /tmp/healthy #如果这个文件存在,这条命令的返回值就是 0,Pod 就会认为这个容器是健康的。
initialDelaySeconds: 5 #在容器启动 5 s 后开始执行
periodSeconds: 5 #每 5 s 执行一次
看一下这个过程
#创建并查看一下这个pod的状态
[root@kubeadm ~]# kubectl apply -f test-liveness-exec.yaml
pod/test-liveness-exec created
[root@kubeadm ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
test-liveness-exec 1/1 Running 0 11s
#30 s 之后,我们再查看一下 Pod 的 Events:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 56s (x6 over 2m21s) kubelet, kube02 Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
#再次查看一下这个 Pod 的状态
#RESTARTS 字段从 0 到 2 的变化,就明白原因了:这个异常的容器已经被 Kubernetes 重启了
[root@kubeadm ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
test-liveness-exec 1/1 Running 2 3m7s
注意:Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点。这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理 Pod,哪怕你只需要一个 Pod 副本。
而作为用户,你还可以通过设置 restartPolicy,改变 Pod 的恢复策略。除了 Always,它还有 OnFailure 和 Never 两种情况:
- Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
- OnFailure: 只在容器 异常时才自动重启容器;
- Never: 从来不重启容器
restartPolicy 和 Pod 里容器的状态,以及 Pod 状态的对应关系:
- 只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。
- 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态。
除了在容器中执行命令外,livenessProbe 也可以定义为发起 HTTP 或者 TCP 请求的方式,定义格式如下:
...
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: X-Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3
...
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
PodPreset
PodPreset用来给指定标签的Pod注入额外的信息,如环境变量、存储卷等。这样,Pod模板就不需要为每个Pod都显式设置重复的信息。
查看 K8s 集群是否已开启 PodPreset 支持,可以通过 kubectl api-versions 命令查看是否存在该类型,或者用kubectl get podpreset 命令查看,如果没开启会提示 error: the server doesn’t have a resource type “podpreset” 错误。
开启podpreset
[root@kubeadm ~]# vi /etc/kubernetes/manifests/kube-apiserver.yaml
- --enable-admission-plugins=NodeRestriction,PodPreset #在这行后添加PodPreset
- --runtime-config=settings.k8s.io/v1alpha1=true #新增这一行
使用标签选择器来指定某个或某些 Pod,来将 PodPreset 预设信息应用上去:
[root@kubeadm ~]# vi preset.yaml
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
name: allow-database
spec:
selector:
matchLabels:
role: frontend #这里选择了role为frontend的注入额外的信息
env:
- name: DB_PORT
value: "6379"
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
编写一个简洁的pod:
[root@kubeadm ~]# vi pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
spec:
containers:
- name: website
image: nginx
ports:
- containerPort: 80
接下来,我们先创建了这个 PodPreset,然后才创建 Pod:
[root@kubeadm ~]# kubectl create -f preset.yaml
[root@kubeadm ~]# kubectl create -f pod.yaml
我们进入website这个pod去看一下preset.yaml这个pod的信息有没有被注入pod.yaml所创建的pod里面
[root@kubeadm ~]# kubectl exec -it website /bin/sh
# echo $DB_PORT
6379
需要说明的是,PodPreset 里定义的内容,只会在 Pod API 对象被创建之前追加在这个对象本身上,而不会影响任何 Pod 的控制器的定义。
如果你定义了同时作用于一个 Pod 对象的多个 PodPreset,Kubernetes 项目会帮你合并(Merge)这两个 PodPreset 要做的修改。而如果它们要做的修改有冲突的话,这些冲突字段就不会被修改。