Pod
Pod是 Kubernetes 项目里最核心的编排对象,是 Kubernetes 项目的最小的原子调度单位。
如果有一组容器,它们之间有亲密的耦合关系,要做统一的调度管理,这样的基础设施就是Pod。
如果把 Pod 看成传统环境里的“机器”、把容器看作是运行在这个“机器”里的“用户程序”,那么很多关于 Pod 对象的设计就非常容易理解了。
哪些属性属于 Pod 对象,哪些属性属于 Container 呢?
比如, 凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。
这些属性的共同特征是,它们描述的是“机器”这个整体,而不是里面运行的“程序”。比如,配置这个“机器”的网卡(即:Pod 的网络定义),配置这个“机器”的磁盘(即:Pod 的存储定义),配置这个“机器”的防火墙(即:Pod 的安全定义)。更不用说,这台“机器”运行在哪个服务器之上(即:Pod 的调度)。
Pod只是一个逻辑概念 ,Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器的 Namespace 和 Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。
Pod 的实现需要使用一个中间容器,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。对于 Pod 里的容器 A 和容器 B 来说:
-
它们可以直接使用 localhost 进行通信;
-
它们看到的网络设备跟 Infra 容器看到的完全一样;
-
一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
-
当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享;
-
Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
例子
debian-container 和 nginx-container 都声明挂载了 shared-data 这个 Volume。而 shared-data 是 hostPath 类型。所以,它对应在宿主机上的目录就是:/data。而这个目录,其实就被同时绑定挂载进了上述两个容器当中。
这就是为什么,nginx-container 可以从它的 /usr/share/nginx/html 目录中,读取到 debian-container 生成的 index.html 文件的原因。
sidecar
所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
geektime/sample:v2 容器执行了一句"cp /sample.war /app",把应用的 WAR 包拷贝到 /app 目录下,然后退出。而后这个 /app 目录,就挂载了一个名叫 app-volume 的 Volume。
Tomcat 容器,同样声明了挂载 app-volume 到自己的 webapps 目录下。
所以,等 Tomcat 容器启动时,它的 webapps 目录下就一定会存在 sample.war 文件:这个文件正是 WAR 包容器启动时拷贝到这个 Volume 里面的,而这个 Volume 是被这两个容器共享的。
像这样,我们就用一种“组合”方式,解决了 WAR 包与 Tomcat 容器之间耦合关系的问题,WAR 包与 Tomcat 容器就可以分开做版本管理了。 这个“组合”操作,正是容器设计模式里最常用的一种模式,它的名字叫:sidecar。
另一个场景:容器的日志收集。Pod 内的容器挂载到一个 Volume,同时运行一个做日志收集的 sidecar 容器也挂载上该 Volume,接下来 sidecar 容器就只需要做一件事儿,那就是不断地从自己挂载的目录里读取日志文件,转发到 MongoDB 或者 Elasticsearch 中存储起来。这个 sidecar 容器可以与主容器一起写到 spec.container 下,这也属于 sidecar 容器设计模式。
Projected Volume
-
Secret;
-
ConfigMap;
-
Downward API;
-
ServiceAccountToken
这些特殊 Volume 的作用,是为容器提供预先定义好的数据。所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes“投射”(Project)进入容器当中的。
Secret
Secret 把 Pod 想要访问的加密数据,存放到 Etcd 中。然后,就可以通过在 Pod 的容器里以挂载 Volume 的方式,访问到这些 Secret 里保存的信息了。
Secret 最典型的使用场景,就是存放数据库的 Credential 信息:
除了使用 kubectl create secret 指令外,也可以直接通过编写 YAML 文件的方式来创建 Secret 对象
只需要创建一个通过编写 YAML 文件创建出来的 Secret 对象。它的 data 字段,以 Key-Value 的格式保存了两份 Secret 数据,user 和 pass。需要注意的是,以这种方式创建的 Secret 对象要求这些数据必须是经过 Base64 转码的,这个转码操作也很简单,比如:
注:base64 只是一种转码方式,在生产环境中,需要在 Kubernetes 中开启 Secret 的加密插件。
创建好 user 和 pass 的 Secret 对象后,就可以在 Pod 中使用它们:
当 Pod 变成 Running 状态之后,再验证一下这些 Secret 对象是不是已经在容器里了:
从返回结果中,可以看到,保存在 Etcd 里的用户名和密码信息,已经以文件的形式出现在了容器的 Volume 目录里,而且已经经过了解码得到了原始值。
像这样通过挂载方式进入到容器里的 Secret,一旦其对应的 Etcd 里的数据被更新,这些 Volume 里的文件内容,同样也会被更新。是 kubelet 组件在定时维护这些 Volume。需要注意的是,这个更新可能会有一定的延时。所以在编写应用程序时,在发起数据库连接的代码处写好重试和超时的逻辑,绝对是个好习惯。
ConfigMap
ConfigMap 的用法几乎与 Secret 完全相同,只不过 ConfigMap 保存的是不需要加密的、应用所需的配置信息。
Downward API
它的作用是:让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。举个例子:
通过这样的声明方式,当前 Pod 的 Labels 字段的值,就会被 Kubernetes 自动挂载成为容器里的 /etc/podinfo/labels 文件。
如上这个 Pod 的容器的启动命令,是不断打印出 /etc/podinfo/labels 里的内容。所以,当创建了这个 Pod 之后,就可以通过 kubectl logs 指令,查看到这些 Labels 字段被打印出来,如下所示:
Downward API 支持的字段非常丰富,比如:
注:Downward API 能够获取到的信息,是 Pod 里的容器进程启动之前就能够确定下来的信息。而如果想要获取 Pod 容器运行后才会出现的信息,比如,容器进程的 PID,那就肯定不能使用 Downward API 了,而应该考虑在 Pod 里定义一个 sidecar 容器。
注:Secret、ConfigMap,以及 Downward API 这三种 Projected Volume 定义的信息,大多还可以通过环境变量的方式出现在容器里。但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,还是建议使用 Volume 文件的方式获取这些信息。
ServiceAccountToken
现在有了一个 Pod,我能不能在这个 Pod 里安装一个 Kubernetes 的 Client,这样就可以从容器里直接访问并且操作这个 Kubernetes 的 API 了呢?
答案是可以的。不过需要解决 API Server 的授权问题。
Service Account 对象的作用,就是 Kubernetes 系统内置的一种“服务账户”,它是 Kubernetes 进行权限分配的对象。比如,Service Account A,可以只被允许对 Kubernetes API 进行 GET 操作,而 Service Account B,则可以有 Kubernetes API 的所有操作的权限。
像这样的 Service Account 的授权信息和文件,实际上保存在它所绑定的一个特殊的 Secret 对象里的,就是 ServiceAccountToken 。
所以说,Kubernetes 项目的 Projected Volume 其实只有三种,因为第四种 ServiceAccountToken,只是一种特殊的 Secret 而已。
为了方便使用,Kubernetes 已经提供了一个的默认“服务账户”(default Service Account)。并且,任何一个运行在 Kubernetes 里的 Pod,都可以直接使用这个默认的 Service Account,而无需显示地声明挂载它。
Kubernetes 在每个 Pod 创建的时候,自动在它的 spec.volumes 部分添加上了默认 ServiceAccountToken 的定义,然后自动给每个容器加上了对应的 volumeMounts 字段。可以通过命令来查看:
可以发现,每一个 Pod,都已经自动声明了一个类型是 Secret、名为 default-token-xxxx 的 Volume,然后 自动挂载在了每个容器的一个固定目录上。
一旦 Pod 创建完成,容器里的应用就可以直接从这个默认 ServiceAccountToken 的挂载目录里访问到授权信息和文件。这个容器内的路径在 Kubernetes 里是固定的:
所以,应用程序只要直接加载这些授权文件,就可以访问并操作 Kubernetes API 了。而且,如果使用的是 Kubernetes 官方的 Client 包(
k8s.io/client-go
)的话,它还可以自动加载这个目录下的文件,不需要做任何配置或者编码操作。
容器健康检查和恢复机制
可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器进行是否运行(来自 Docker 返回的信息)作为依据。例子:
在这个 Pod 中,定义了一个有趣的容器。它在启动之后做的第一件事,就是在 /tmp 目录下创建了一个 healthy 文件,以此作为自己已经正常运行的标志。而 30 s 过后,它会把这个文件删除掉(以此测试Probe的判定机制)。
定义的 livenessProbe(健康检查),它的类型是 exec,这意味着,它会在容器启动后,在容器里面执行一句指定好的命令:cat /tmp/healthy。如果这个文件存在,这条命令的返回值就是 0,Pod 就会认为这个容器不仅已经启动,而且是健康的。这个健康检查,在容器启动 5 s 后开始执行(initialDelaySeconds: 5),每 5 s 执行一次(periodSeconds: 5)。
操作验证
可以看到,由于已经通过了健康检查,这个 Pod 就进入了 Running 状态。
而 30 s 之后,再查看一下 Pod 的 Events:
这个 Pod 在 Events 报告了一个异常,显然,这个健康检查探查到 /tmp/healthy 已经不存在了,所以它报告容器是不健康的。那么接下来会发生什么呢?再次查看一下这个 Pod 的状态:
Pod 并没有进入 Failed 状态,而是保持了 Running 状态。这是为什么呢?
注意到 RESTARTS 字段从 0 到 1 的变化,说明这个异常的容器已经被 Kubernetes 重启了。在这个过程中,Pod 保持 Running 状态不变。
注:Kubernetes 中并没有 Docker 的 Stop 语义。所以虽然是 Restart(重启),但实际却是重新创建了容器。
恢复机制
以上功能就是 Kubernetes 里的 Pod 恢复机制 ,也叫 restartPolicy。它是 Pod 的 Spec 部分的一个标准字段(pod.spec.restartPolicy),默认值是 Always,即:任何时候这个容器发生了异常,它一定会被重新创建。
注:Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点。这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。如果想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理 Pod,哪怕你只需要一个 Pod 副本(这也是 一个单 Pod 的 Deployment 与一个 Pod 最主要的区别 )。
还可以通过设置 restartPolicy,来改变 Pod 的恢复策略:
-
Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
-
OnFailure: 只在容器 异常时才自动重启容器;
-
Never: 从来不重启容器。
需要结合业务场景,合理设置这三种恢复策略:
比如,一个 Pod,它只计算 1+1=2,计算完成输出结果后退出,变成 Succeeded 状态。这时,你如果再用 restartPolicy=Always 强制重启这个 Pod 的容器,就没有任何意义了。
而如果要关心这个容器退出后的上下文环境,比如容器退出后的日志、文件和目录,就需要将 restartPolicy 设置为 Never。因为一旦容器被自动重新创建,这些内容就有可能丢失掉了(被垃圾回收了)。
健康检查
除了在容器中执行命令外,livenessProbe 也可以定义为发起 HTTP 或者 TCP 请求的方式:
Pod 暴露一个健康检查 URL(比如 /healthz),或者直接让健康检查去检测应用的监听端口。这两种配置方法,在 Web 服务类的应用中非常常用。
PodPreset(Pod 预设置)
在这个 PodPreset 的定义中,首先是一个 selector。这就意味着后面这些追加的定义,只会作用于 selector 所定义的、带有“role: frontend”标签的 Pod 对象,这就可以防止“误伤”。
还定义了一组 Pod 的 Spec 里的标准字段,以及对应的值。比如,env 里定义了 DB_PORT 这个环境变量,volumeMounts 定义了容器 Volume 的挂载目录,volumes 定义了一个 emptyDir 的 Volume。
可以清楚地看到,这个 Pod 里多了新添加的 labels、env、volumes 和 volumeMount 的定义,它们的配置跟 PodPreset 的内容一样。此外,这个 Pod 还被自动加上了一个 annotation 表示这个 Pod 对象被 PodPreset 改动过。
注:PodPreset 改的是 Pod,而不会影响任Pod 的控制器。比如,现在提交的是一个 nginx-deployment,那么这个 Deployment 对象本身是永远不会被 PodPreset 改变的,被修改的只是这个 Deployment 创建出来的所有 Pod。
如果定义了同时作用于一个 Pod 对象的多个 PodPreset,会发生什么呢?
Kubernetes 会帮你合并(Merge)这两个 PodPreset 要做的修改。而如果它们要做的修改有冲突的话,这些冲突字段就不会被修改。
资源限制
CPU 和内存资源的限额,要配置在每个 Container 的定义上。
requests、limits
定义的分别是容器最小、最大的资源消耗占用范围。
cpu: "250m" 含义是 250millicpu,1000 millicpu = 1个cpu。
memory: "128Mi" 含义是 128 1024 1024 bytes。
若设置为 memory: "128M",则单位换算为 128 1000 1000 bytes。