为什么我们需要 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 要做的修改。而如果它们要做的修改有冲突的话,这些冲突字段就不会被修改。

文档更新时间: 2020-08-06 18:53   作者:子木