如上所示:左边是容器结构,右边是虚拟机结构。
1. 只读层:
只读层是容器 rootfs 的最下面的若干层,对应的是拉取的镜像(FROM)的结构。每层的目录结构为 /var/lib/docker/aufs/diff/***
2. 可读写层:
rootfs最上边的一层。在写入文件之前,这个目录是空的。一旦在容器里进行了写操作,修改产生的内容就会以增量的方式出现在该层中。如果现在要删除只读层里的一个文件,AuFS会在可读写层创建一个 whiteout 文件,把只读层里的文件遮挡起来。一般把 “whiteout” 形象地翻译为 “白障”。所以,最上边的可读写层就是专门用来存放你修改 rootfs 后产生的增量的,无论是增、删、改,都发生在这里。当我们使用完了这个修改过的容器之后,可以使用 docker commit 和 push 指令保存这个修改过的可读写层,并上传到 Docker Hub,与此同时,原先的只读层中的内容不会有任何变化。
3. Init 层:
Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。需要这样一层的原因是,这些文件本来属于只读层镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值(比如 hostname),所以需要在可读层修改它们。可是,这些修改往往只对当前容器有效,不希望在执行 docker commit 时把这些信息连同可读写层一起提交。
一个正在运行的 linux 容器可以被一分为二地看待:
(1)一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分被称为容器镜像,是容器的静态视图。
(2)一个由 namespace+cgroups 构成的隔离环境,这一部分被称为容器运行时,是容器的动态视图。
由 master 和 node 两种节点组成,这两种角色分别对应控制节点和计算节点。其中,master 节点由三个紧密协作的独立组件组合而成,分别是负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controller-manager。整个集群的持久化数据,则由 kube-apiserver 处理后保存在 etcd 中。
node 节点上最核心的部分,是一个名为 kubelet 的组件。在 kubernetes 项目中,kubelet 主要负责同容器运行时(比如 docker 项目)交互。而这种交互所依赖的是一个称作 CRI(container runtime interface)的远程调用端口,该接口定义了容器运行时的各项核心操作,比如启动一个容器的所有参数。只要你的容器运行时能够运行标准的容器镜像,它就可以通过实现 CRI 接入 kubernetes 项目。而具体的容器运行时,比如 docker 项目,则一般通过 OCI 这个容器运行时规范同底层的 linux 操作系统进行交互,即把 CRI 请求翻译成对 linux 操作系统的调用(操作 linux namespace 和 cgroups 等)。
kubelet 还通过 gRPC 协议同一个叫做 Device Plugin 的插件进行交互。这个插件是 kubernetes 项目用来管理 GPU 等宿主机物理设备的主要组件,也是基于 kubernetes 项目进行机器学习训练、高性能作业支持等工作必须关注的功能。kubelet 的另一个重要功能,是调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与 kubelet 进行交互的接口,分别是 CNI(container networking interface)和 CSI(container storage interface)。
从容器这个最基础的概念出发,首先遇到了容器间紧密协作关系的难题,于是扩展到了 Pod。有了 Pod 之后,我们希望能一次启动多个应用的实例,这样就需要 Deployment 这个 Pod 的多实例管理器。而有了这样一组相同的 Pod 后,我们又需要通过固定的 IP 地址和端口以负载均衡的方式访问它,于是就有了 Service。
Kubernetes 项目提供了一种叫作 Secret 的对象,它其实是保存在 etcd 里的键值对数据,这样,你把 Credential 信息以 Secret 的方式存在 etcd 里,Kubernetes 就会在你指定的 Pod (比如 Web 应用的 Pod)启动时,自动把 Secret 里的数据以 Volume 的方式挂载到容器里。这样这个 Web 应用就能访问数据库了。
应用运行的形态是影响“如何容器化这个应用”的第二个重要因素。Kubernetes 定义了新的、基于 Pod 改进后的对象。比如 Job,用来描述一次性运行的 Pod(比如大数据任务)。再比如 DaemonSet,用来描述每个宿主机上必须且只能运行一个副本的守护进程服务。又比如 CronJob,用来描述定时任务等。
在 Kubernetes 中,首先通过一个任务编排对象,比如 Pod、Job、CronJob 等,描述你试图管理的应用。然后为它定义一些运维能力对象,比如 Service、Ingress、Horizontal Pod Autoscaler(自动水平扩展器)等,这些对象会负责具体的运维能力侧功能。这种使用方法就是所谓的“声明式 API”。这种 API 对应的编排对象和服务对象都是 Kubernetes 项目中的 API 对象。
Pod 实际上是在扮演传统基础设施里“虚拟机”的角色,容器则是这个虚拟机里运行的用户程序。Kubernetes 项目中的最小编排单位是 Pod,落实到 API 对象上,Container 就成了 Pod 属性里的一个普通字段。凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。这些属性描述的都是“机器”这个整体,而不是里边运行的“程序”。比如,配置这台“机器”的网卡(Pod 的网络定义),配置这台“机器”的磁盘(Pod 的存储定义),配置这台“机器”的防火墙(Pod 的安全定义),更不用说这台“机器”在哪个服务器之上运行(Pod 的调度)。
下面介绍 Pod 中几个重要字段的含义和用法:
nodeSelector:一个供用户将 Pod 与 Node 进行绑定的字段,用法如下所示:
apiVersion: v1
kind: Pod
...
spec:
nodeSelector:
disktype: ssd
这样的配置意味着这个 Pod 永远只能在携带了 disktype: ssd 标签的节点上运行,否则它将调度失败。
nodeName:一旦 Pod 的这个字段被赋值,Kubernetes 项目就会认为这个 Pod 已调度,调度的结果就是赋值的节点名称。所以,这个字段一般由调度器负责设置,但用户也可以设置它来“骗过”调度器。当然,这种做法一般在测试或者调试是才会用到。
hostAliases:定义了 Pod 的 hosts 文件(比如 /etc/hosts)里的内容,用法如下:
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...
在这个 Pod 的 YAML 文件中,设置了一组 IP 和 hostname 的数据。这样,当这个 Pod 启动后,/etc/hosts 文件的内容将如下所示:
127.0.0.1 localhost
...
10.1.2.3 foo.remote
10.1.2.3 bar.remote
在 kubernetes 项目中,如果要设置 hosts 文件里的内容,一定要通过这种方法。如果直接修改了 hosts 文件,在 Pod 被删除重建之后,kubelet 会自动覆盖被修改的内容。
如果在 Pod 的 YAML 文件中,定义 shareProcessNamespace=true,意味着这个 Pod 里的容器要共享 PID Namespace。在 Pod 的 YAML 文件里声明开启 tty 和 stdin 等同于设置了 docker run 里的 -it(-i 即 stdin,-t 即 tty)参数。
对 container 的定义,有几个属性值的额外关注:
首先是 imagePullPolicy 字段,它定义了镜像拉取的策略,它之所以是 Container 级别的属性,是因为容器镜像本来就是 Container 定义中的一部分。imagePullPolicy 的默认值是 Always,即每次创建 Pod 都重新拉取一次镜像。另外,当容器镜像是类似于 nginx 或者 nginx:latest 这样的名字时,也会认为是 Always。如果被定义成 Never 或者 IfNotPresent,则意味着 Pod 永远不会主动拉取这个镜像,或者只在宿主机上不存在这个镜像时才拉取。
其次是 lifecycle 字段。它定义的是 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:
exec:
command: ["/usr/sbin/nginx", "-s", "quit"]
这是来自 Kubernetes 官方的一个 Pod YAML 文件,定义了一个 Nginx 镜像的容器。其中定义了两个参数:
postStart 指的是在容器启动后立刻执行一个指定操作。postStart 定义的操作虽然是在 Docker 容器 ENTRYPOINT 执行之后,但它并不严格保证顺序。也就是说在 postStart 启动时,ENTRYPOINT 有可能尚未结束。如果 postStart 执行超时或出错,Kubernetes 会在该 Pod 的 Events 中报出该容器启动失败的错误信息,导致 Pod 也处于失败状态。
preStop 发生的时机是容器被结束之前(比如收到了 SIGKILL 信号)。它会阻塞当前容器进程,直到这个 Hook 定义操作完成之后,才允许容器被结束。
Pod 生命周期的变化主要体现在 Pod API 对象的 Status 部分,这是除了 metadata 和 spec 外的第三个重要字段。其中,pod.status.phase 就是 Pod 的当前状态,它有如下几种可能的情况:
(1)pending,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存到 etcd 当中。但是,这个 Pod 里有些容器因为某种原因不能被顺利创建。比如,调度不成功。
(2)running,Pod 已经调度成功,跟一个具体节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行。
(3)succeeded,Pod 里所有的容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最常见。
(4)failed,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。出现这个状态意味着需要想办法调试这个容器的应用,比如查看 Pod 的 Events 和日志。
(5)unknown,Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 kubelet)间的通信出现了问题。
Pod 对象的 status 字段还可以细分出一组 conditions。这些细分状态的值包括:podScheduled、ready、initialized 以及 unschedulable。它们主要用于描述造成当前 status 的具体原因是什么。
在 Kubernetes 中有几种特殊的 Volume,它们存在的意义不是为了存放容器里的数据,也不是用于容器和宿主机之间的数据交换,而是为容器提供预先定义好的数据。
(1)Secret
Secret 的作用是帮你把 Pod 想要访问的加密数据存放到 etcd 中,你就可以通过在 Pod 的容器里挂载 Volume 的方式访问这些 Secret 里保存的信息了。
Secret 最典型的使用场景莫过于存放数据库的 Credential 信息了,示例如下:
apiVersion: v1
kind: Pod
metadata:
name: test-projected-volume
spec:
containers:
- name: test-secret-volume
image: busybox
args:
- sleep
- "86400"
volumeMounts:
- name: mysql-cred
mountPath: "/projected-volume"
readOnly: true
volumes:
- name: mysql-cred
projected:
sources:
- secret:
name: user
- secret:
name: pass
在这个 Pod 中定义了一个简单的容器,它声明挂载的 Volume 不是常见的 emptyDir 或者 hostPath 类型,而是 projected 类型。这个 Volume 的数据来源(sources)则是名为 user 和 pass 的 Secret 对象,分别对应数据库的用户名和密码。创建一个 Secret 对象的方式可以是使用 kuberctl create secret 指令,也可以编写一个 Secret 的 YAML 文件:
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
user: cm9vdA==
pass: MTIzNDU2Nzg=
需要注意的是,Secret 对象要求这些数据必须是经过 Base64 转码的,以免出现明文密码的安全隐患,操作命令为 echo -n 'username' | base64。
(2)ConfigMap
ConfigMap 与 Secret 类似,区别在于 ConfigMap 保存的是无需加密的、应用所需的配置信息。可以使用 kubectl create configmap 从文件或目录创建 ConfigMap,也可以直接编写 YAML 文件。
(3)Downward API
Downward API 的作用是让 Pod 里的容器能够直接获取这个 Pod API 对象本身的信息。
举个例子:
apiVersion: v1
kind: Pod
metadata:
name: test-downwardapi-volume
labels:
zone: us-east-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:
- downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
在这个 YAML 文件中,定义了一个简单的容器,声明了一个 projected 类型的 Volume。这个 Downward API Volume 声明了要暴露 Pod 的 metadata.labels 信息给容器。通过这样的声明方式,当前 Pod 的 labels 字段的值就会被 Kubernetes 自动挂载成为容器里的 /etc/podinfo/labels 文件。Downward API 支持的字段如下:
①使用 fieldRef 可以声明使用:
metadata.name Pod 的名字
metadata.namespace Pod 的 Namespace
metadata.uid Pod 的 UID
metadata.labels['
metadata.annotations['
metadata.labels Pod 的所有 Label
metadata.annotations Pod 的所有 Annotation
②使用 resourceFieldRef 可以声明使用:
容器的 CPU limit
容器的 CPU request
容器的 memory limit
容器的 memory request
容器的 ephemeral-storage limit
容器的 ephemeral-storage request
③通过环境变量声明使用:
status.podIP Pod 的 IP
spec.serviceAccountName Pod 的 ServiceAccount 名字
spec.nodeName Node 的名字
status.hostIP Node 的 IP
需要注意的是,Downward API 能够获取到的信息一定是 Pod 里的容器进程启动之前就能确定下来的信息。如果想获取 Pod 容器运行后才会出现的信息,比如容器进程的 PID,就不能使用 Downward API 了,应该考虑在 Pod 里定义一个 sidecar 容器。
(4)ServiceAccountToken
Service Account 对象的作用就是 Kubernetes 系统内置的一种“服务账户”,它是 Kubernetes 进行权限分配的对象。比如,Service Account A 可以只被允许对 Kubernetes API 进行 GET 操作,而 Service Account B 可以有 Kubernetes API 的所有操作的权限。像这样的 Service Account 的授权信息和文件,实际上保存在它所绑定的一个特殊的 Secret 对象里。这个特殊的 Secret 对象叫作 ServiceAccountToken。任何在 Kubernetes 集群上运行的应用,都必须使用 ServiceAccountToken 里保存的授权信息(也就是 Token),才可以合法地访问 API Server。
所以,Kubernetes 项目的 Projected Volume 其实只有 3 种,因为 ServiceAccountToken 只是一种特殊的 Secret。
为了方便使用,Kubernetes 已经提供了一个默认的“服务账户”。任何在 Kubernetes 里运行的 Pod 都可以直接使用它,而无须显式声明挂载它。如果查看任意一个在 Kubernetes 集群里运行的 Pod,就会发现每一个 Pod 都已经自动声明了一个类型是 Secret、名为 default-token-xxxx 的 Volume,然后自动挂载在每个容器的一个固定的目录上。比如:
$ kubectl describe pod nginx-deployment-***
Containers:
...
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-** (ro)
Volumes:
default-token-**:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-**
Optional: false
这个 Secret 类型的 Volume,正是默认 Service Account 对应的 ServiceAccountToken。所以,在每个 Pod 创建的时候,Kubernetes 其实自动在它的 spec.volumes 部分添加了默认 ServiceAccountToken 的定义,然后自动给每个容器加上了对应的 volumeMounts 字段。这样,一旦 Pod 创建完成,容器里的应用就可以直接从默认 ServiceAccountToken 的挂载目录里访问授权信息和文件。这个容器内的路径在 Kubernetes 里是固定的:/var/run/secrets/kubernetes.io/serviceaccount。所以,你的应用程序只要直接加载这些授权文件,就可以访问并操作 Kubernetes API了。如果你使用的是 Kubernetes 官方的 Client 包(k8s.io/client-go)的话,它还可以自动加载这个目录下的文件。考虑到自动挂载默认 ServiceAccountToken 的潜在风险,Kubernetes 允许设置默认不为 Pod 里的容器自动挂载 Volume。
在 Kubernetes 中,可以为 Pod 里的容器定义一个健康检查的“探针”(Probe)。这样,kubelet 会根据 Probe 的返回值决定这个容器的状态,而不是直接以容器是否运行(来自 Docker 返回的信息)作为依据。
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
initialDelaySeconds: 5
periodSeconds: 5
在这个 Pod 中的容器在启动之后做的第一件事是在 /tmp 目录下创建了一个 healthy 文件,以此作为自己正常运行的标志。而在 30 秒之后,它会把这个文件删除。与此同时,定义了一个 livenessProbe(健康检查)。它的类型是 exec,这意味着当容器启动后它会在容器中执行一句指定的命令,比如 cat /tmp/healthy。这时,如果这个文件存在,这条命令的返回值就是0,Pod 就会认为这个容器不仅已经启动,而且是健康的。这个健康检查在容器启动 5 秒后开始执行(initialDelaySeconds: 5),每 5 秒执行一次(periodSeconds: 5)。
Pod 的恢复机制,也叫 restartPolicy。它是 Pod 的 spec 部分的一个标准字段,默认值是 Always,即无论这个容器何时发生异常,它一定会被重新创建。Pod 的恢复过程永远发生在当前节点上,而不会跑到别的节点上。事实上,一旦一个 Pod 与一个节点绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远不会离开这个节点。如果这个节点宕机了,这个Pod 也不会主动迁移到其他节点上。如果想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理 Pod,哪怕你只需要一个 Pod 副本。restartPolicy 一共有三种情况:
always: 在任何情况下,只要容器不在运行状态,就自动重启容器。
onFailure: 只在容器异常时才自动重启容器。
never: 从不重启容器。
记住以下两个基本的设计原理即可:
(1)只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如 always),那么这个 Pod 就会保持 Running 状态并重启容器,否则 Pod 会进入 Failed 状态。
(2)对于包含多个容器的 Pod, 只有其中所有容器都进入异常状态后,Pod 才会进入 Failed 状态。在此之前,Pod 都是 Running 状态。此时,Pod 的 READY 字段会显示正常容器的个数。
所以,假如一个 Pod 里只有一个容器,且这个容器异常退出了,那么只有当 restartPolicy=never 时,这个 Pod 才会进入 Failed 状态。而在其他情况下,因为 Kubernetes 可以重启这个容器,所以 Pod 的状态保持 Running 不变。如果这个 Pod 有多个容器,仅有一个容器异常退出,它就会始终保持 Running 状态,即 restartPolicy=never。只有当所有容器都异常退出之后,这个 Pod 才会进入 Failed 状态。其他情况以此类推。
一个 YAML 文件,配置了 Pod 配置里追加的字段。举个例子:
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
spec:
containers:
- name: website
- image: nginx
ports:
- containerPort: 80
这个 Pod 的 YAML 文件无法在生产环境使用。下面定义一个 PodPreset 对象:
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
name: allow-database
spec:
selector:
matchLabels:
role: frontend
env:
- name: DB_PORT
value: "6379"
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
在这个 PodPreset 的定义中,首先是一个 selector。这就意味着后面这些追加的定义只会作用于 selector 所定义的、带有 role: frontend 标签的 Pod 对象。先创建 PodPreset,再创建 Pod,在 Pod 运行起来之后,查看 Pod 的 API 对象:
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
annotations:
podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
containers:
- name: website
image: nginx
volumeMounts:
- mountPath: /cache
name: cache-volume
ports:
- containerPort: 80
env:
- name: DB_PORT
value: "6379"
volumes:
- name: cache-volume
emptyDir: {}