Pod
Pod 是 k8s 中最基础的 API 对象,关于 Pod 我们需要重点关注以下几点:
- 为什么我们需要 Pod
- 如何定义一个 Pod
1. 为什么我们需要 Pod
1.1 成组调度
如果我们把物理机与云环境做一个对比:
- 容器的本质是进程,是未来云计算系统中的进程
- 容器镜像就是这个系统里的“.exe”安装包
- Kubernetes 则是未来云计算的操作系统
- 在操作系统里进程是以进程组的方式组织在一起,Pod 就是将“进程组”的概念映射到了容器技术中
之所以需要 Pod 是因为应用往往都存在着类似于“进程和进程组”的关系,更具体地说,就是这些应用之间有着密切的协作关系,使得它们必须部署在同一台机器上。像这样容器间的紧密协作,我们可以称为“超亲密关系”。这些具有“超亲密关系”容器的典型特征包括但不限于:
- 互相之间会发生直接的文件交换
- 使用 localhost 或者 Socket 文件进行本地通信
- 会发生非常频繁的远程调用
- 需要共享某些 Linux Namespace(比如,一个容器要加入另一个容器的 Network Namespace)等等。
所以 Pod 的存在就是为了解决成组调度(gang scheduling)的问题。Pod 是 Kubernetes 里的原子调度单位。这就意味着,Kubernetes 项目的调度器,是统一按照 Pod 而非容器的资源需求进行计算的。
注:再次强调一下:容器的“单进程模型”,并不是指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力。这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个 PID=1 进程的子进程。可是,用户编写的应用,并不能够像正常操作系统里的 init 进程或者 systemd 那样拥有进程管理的功能。
1.2 Pod 的实现原理
Pod 只是一个逻辑概念,本质上 Pod,其实是一组共享了某些资源的容器。具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。
而为了避免使用 docker run --net=B --volumes-from=B --name=A image-A ...
让 Pod 内的容器引入不对等关系,Pod 的实现使用了一个中间容器,这个容器叫作 Infra 容器。Infra 容器使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/paus,占用极少的资源。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。
在容器的启动过程中:
- Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起,这样的组织关系,可以用下面这样一个示意图来表达
- Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。因为共享了 Network Namespace,Pod 里的容器可以直接使用 localhost 进行通信,并共享相同的网络设备
共享 Network/Volume 意味着:
- 对于同一个 Pod 里面的所有用户容器来说,它们的进出流量,也可以认为都是通过 Infra 容器完成的。这一点很重要,因为将来如果你要为 Kubernetes 开发一个网络插件时,应该重点考虑的是如何配置这个 Pod 的 Network Namespace,而不是每一个用户容器如何使用你的网络配置,这是没有意义的。
- 共享 Volume 就简单多了:Kubernetes 项目只要把所有 Volume 的定义都设计在 Pod 层级 即可。一个 Volume 对应的宿主机目录对于 Pod 来说就只有一个,Pod 里的容器只要声明挂载这个 Volume,就一定可以共享这个 Volume 对应的宿主机目录。
1.3 容器设计模式
前面说了 Pod 存在的一个重要原因是成组调度,而另一个原因就是容器设计模式。容器设计模式,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。
第一个最典型的例子是:WAR 包与 Web 服务器:
|
|
在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
在上面的 Pod 中,使用的是容器设计模式里最常用的一种模式:sidecar。sidecar 指的就是我们可以在一个 Pod 中,启动一个辅助容器,来完成一些独立于主进程(主容器)之外的工作。这个例子中的 sidecar 的主要工作是使用 共享的 Volume 将 Jar 包从 InitContainer 拷贝到 Tomcat 容器中,从而解耦 Tomcat 与 Jar 包的发布。
但不要忘记,Pod 的另一个重要特性是,它的所有容器都共享同一个 Network Namespace。这就使得很多与 Pod 网络相关的配置和管理,也都可以交给 sidecar 完成,而完全无须干涉用户容器。这里最典型的例子莫过于 Istio 这个微服务治理项目了。
Kubernetes 社区曾经把“容器设计模式”这个理论,整理成了一篇小论文
2.4 pod 的本质
你现在可以这么理解 Pod 的本质:Pod,实际上是在扮演传统基础设施里“虚拟机”的角色;而容器,则是这个虚拟机里运行的用户程序。当你需要把一个运行在虚拟机里的应用迁移到 Docker 容器中时,一定要仔细分析到底有哪些进程(组件)运行在这个虚拟机里。然后,你就可以把整个虚拟机想象成为一个 Pod,把这些进程分别做成容器镜像,把有顺序关系的容器,定义为 Init Container。这才是更加合理的、松耦合的容器编排诀窍,也是从传统应用架构,到“微服务架构”最自然的过渡方式。
Pod 这个概念,提供的是一种编排思想,而不是具体的技术方案。所以,如果愿意的话,你完全可以使用虚拟机来作为 Pod 的实现,然后把用户容器都运行在这个虚拟机里。比如,Mirantis 公司的virtlet 项目。甚至,你可以去实现一个带有 Init 进程的容器项目,来模拟传统应用的运行方式。这些工作,在 Kubernetes 中都是非常轻松的,也是我们后面讲解 CRI 时会提到的内容。
2. 如何定义一个 Pod
2.1 k8s 中的对象定义
定义一个 Pod 首先我们要面对的问题就是: 到底哪些属性属于 Pod 对象,而又有哪些属性属于 Container 呢?。一个原则是:
- Pod 对标的是虚拟机所以在 Pod 的配置中,所以凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。这些属性的共同特征是,它们描述的是“机器”这个整体,而不是里面运行的“程序”
- 其他的与进程相关的配置则属于 Container。
k8s api 对象的定义存放在源码目录下的 ./staging/src/k8s.io/api 内
|
|
在 k8s.io/api/core/v1/ 内:
- generated.proto 是所有 API 对象的 pb 定义文件
- type.go 则是由 generated.proto 生成的 go struct 定义。
|
|
2.2 Pod 定义
Pod 的定义如下:
|
|
其中:
- TypeMeta: 定义了 API 对象的类型以及其发布的版本
- ObjectMeta: 定义了对象的元数据,这个是用户必须定义的
- PodSpec: 定义 Pod 包含哪些字段
- PodStatus: Pod 当前状态字段,由系统定义
TypeMeta、ObjectMeta 是通用字段,所有的 API 对象都有这两个属性。
TypeMeta
TypeMeta 只包含了,两个属性
- name: API 对象名称
- apiVersion: API 对象版本
|
|
ObjectMeta
ObjectMeta 的定义github,比较重要的是以下几个:
|
|
- Name: API 对象的实例名称
- Namespace: 实例存放的 namespace
- Labels: 实例的标签,通过 label Selector 可以实现对实例的筛选
- Annotations: 携带 key-value 格式的内部信息
PodSpec
PodSpec 是为 Pod 定义的结构体
|
|
PodSpec的字段 包括:
分类 | 字段 | 作用 |
---|---|---|
容器定义 | Container ([]Container) | |
InitContainers ([]Container) | Init Containers 的生命周期,会先于所有的 Containers,并且严格按照定义的顺序执行 | |
EphemeralContainers ([]EphemeralContainer) | ||
ImagePullSecrets ([]LocalObjectReference) | ||
EnableServiceLinks (boolean) | ||
Os (PodOS) | ||
Volumes ([]Volume) | ||
调度 | NodeSelector (map[string]string) | |
NodeName (string) | ||
Affinity (Affinity) | ||
Tolerations ([]Toleration) | ||
SchedulerName (string) | ||
RuntimeClassName (string) | ||
PriorityClassName (string) | ||
Priority (int32) | ||
PreemptionPolicy (string) | ||
TopologySpreadConstraints ([]TopologySpreadConstraint) | ||
生命周期 | restartPolicy (string) | |
TerminationGracePeriodSeconds (int64) | Pod 优雅退出的时间, | |
ActiveDeadlineSeconds (int64) | 在系统将主动尝试将此 Pod 标记为已失败并杀死相关容器之前,Pod 可能在节点上活跃的时长 | |
ReadinessGate ([]PodReadinessGate) | ||
主机名 | Hostname (string) | 指定 Pod 的主机名 |
SetHostnameAsFQDN (boolean) | ||
Subdomain (string) | 如果设置了此字段,则完全限定的 Pod 主机名将是 <hostname>.<subdomain>.<Pod 名字空间>.svc.<集群域名> 。 如果未设置此字段,则该 Pod 将没有域名。 |
|
HostAliases ([]HostAlias) | ||
DNS | DnsConfig (PodDNSConfig) | 指定 Pod 的 DNS 参数 |
DnsPolicy (string) | 为 Pod 设置 DNS 策略 | |
Namespace | HostNetwork (boolean) | 为此 Pod 请求主机层面联网支持。使用主机的 host namespace |
HostPID (boolean) | 使用主机的 PID 名字空间 | |
HostIPC (boolean) | 使用主机的 IPC 名字空间 | |
HostUsers (boolean) | 使用主机的用户名字空间 | |
ShareProcessNamespace (boolean) | Pod 里的容器要共享 PID Namespace | |
认证 | ServiceAccountName (string) | |
AutomountServiceAccountToken (boolean) | ||
安全控制 | securityContext (PodSecurityContext) | Pod 级别的安全属性和常见的容器设置 |
3. Pod 重要字段
2.1 调度相关
与调度有关的字段包括:
- NodeName:
- 一旦 Pod 的这个字段被赋值,Kubernetes 项目就会被认为这个 Pod 已经经过了调度,调度的结果就是赋值的节点名字
- 所以,这个字段一般由调度器负责设置,但用户也可以设置它来“骗过”调度器,当然这个做法一般是在测试或者调试的时候才会用到
- NodeSelector:通过 label 选择特定范围的 Node
|
|
这样的一个配置,意味着这个 Pod 永远只能运行在携带了“disktype: ssd”标签(Label)的节点上;否则,它将调度失败。
2.2 网络配置
与网络配置有关的字段包括:
- HostAliases:定义了 Pod 的 hosts 文件(比如 /etc/hosts)里的内容
|
|
上面配置的 Pod 启动后,/etc/hosts 文件的内容将如下所示:
|
|
2.3 Linux Namespace 相关
与 Namespace 配置有关的字段包括:
shareProcessNamespace: true
: Pod 里的容器要共享 PID NamespacehostNetwork: true
: 共享宿主机的 Network NamespacehostIPC: true
: 共享宿主机的 IPC NamespacehostPID: true
: 共享宿主机的 PID Namespace
|
|
2.4 Pod 恢复机制
RestartPolicy 定义了 Pod 的恢复机制,默认值是 Always,即:任何时候这个容器发生了异常,它一定会被重新创建。
需要强调的是,Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点。这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理 Pod。
restartPolicy 的可选值包括:
- Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
- OnFailure: 只在容器 异常时才自动重启容器;
- Never: 从来不重启容器。
在实际使用时,我们需要根据应用运行的特性,合理设置这三种恢复策略。如果 Pod 执行的是一个一次性的 job 任务,就不要设置成 Always 了。如果你要关心这个容器退出后的上下文环境,比如容器退出后的日志、文件和目录,就需要将 restartPolicy 设置为 Never。因为一旦容器被自动重新创建,这些内容就有可能丢失掉了(被垃圾回收了)。
restartPolicy 与容器状态的关系
Kubernetes 的官方文档,把 restartPolicy 和 Pod 里容器的状态,以及 Pod 状态的对应关系,总结了非常复杂的一大堆情况。但总体的设计原则是:
- 只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态 。
- 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态。在此之前,Pod 都是 Running 状态。此时,Pod 的 READY 字段会显示正常容器的个数
所以,假如一个 Pod 里只有一个容器,然后这个容器异常退出了。那么,只有当 restartPolicy=Never 时,这个 Pod 才会进入 Failed 状态。而其他情况下,由于 Kubernetes 都可以重启这个容器,所以 Pod 的状态保持 Running 不变。
而如果这个 Pod 有多个容器,仅有一个容器异常退出,它就始终保持 Running 状态,哪怕即使 restartPolicy=Never。只有当所有容器也异常退出之后,这个 Pod 才会进入 Failed 状态。
3. Container 定义
Container 的结构体定义如下:
|
|
Container的字段 包括:
分类 | 字段 | 作用 |
---|---|---|
Docker | name (string) | |
image (string) | ||
imagePullPolicy (string) | 镜像拉取策略 | |
command ([]string) | entrypoint array,默认使用容器内的 ENTRYPOINT | |
args ([]string) | entrypoint 的参数,默认使用容器镜像的 CMD | |
workingDir (string) | 容器的工作目录 | |
ports([]ContainerPort) | 容器暴露的端口列表,不指定端口不会阻止该端口被暴露 | |
env([]EnvVar) | 在容器中设置的环境变量列表,无法更新 | |
envFrom ([]EnvFromSource) | 容器中填充环境变量的数据源列表 | |
volumeMounts ([]VolumeMount) | 挂载到容器文件系统中的 Pod 卷 | |
volumeDevices ([]VolumeDevice) | 容器要使用的块设备列表 | |
资源限制 | resources(ResourceRequirements) | 容器所需的计算资源 |
Hook | lifecycle (Lifecycle) | |
terminationMessagePath (string) | ||
terminationMessagePolicy (string) | ||
健康检查 | livenessProbe (Probe) | 定期探针容器活跃度。如果探针失败,容器将重新启动 |
readinessProbe (Probe) | 定期探测容器服务就绪情况。如果探针失败,容器将被从服务端点中删除 | |
startupProbe (Probe) | startupProbe 表示 Pod 已成功初始化。如果设置了此字段,则此探针成功完成之前不会执行其他探针。如果这个探针失败,Pod 会重新启动,就像存活态探针失败一样 | |
安全控制 | securityContext (SecurityContext) | 定义了容器应该运行的安全选项,将覆盖Pod SecurityContext 的同名字段 |
调试 | stdin(boolean) | 此容器是否应在容器运行时为 stdin 分配缓冲区。如果未设置,从容器中的 stdin 读取将始终导致 EOF。 默认为 false |
stdinOnce(boolean) | stdin 是否在第一链接后就关闭 |
4. Container 重要字段
4.1 Docker 常规字段
Docker 常规字段包括定义容器运行的相关参数,常用的字段包括:
|
|
ImagePullPolicy
ImagePullPolicy 定义了镜像拉取的策略
- Always: 默认值,每次创建 Pod 都重新拉取一次镜像。另外,当容器的镜像是类似于 nginx 或者 nginx:latest 这样的名字时,ImagePullPolicy 也会被认为 Always
- Never 或者 IfNotPresent,则意味着 Pod 永远不会主动拉取这个镜像,或者只在宿主机上不存在这个镜像时才拉取
4.2 容器健康检查
在 Kubernetes 中,你可以为 Pod 里的容器定义各种探针(Probe) kubelet 会根据这些 Probe 的返回值决定容器的可用性。
Probe 的定义如下:
|
|
|字段|含义| |TimeoutSeconds (int32)|容器启动后启动存活态探针之前的秒数| |terminationGracePeriodSeconds (int64)|Pod 需要在探针失败时体面终止所需的时间长度,Pod 优雅终止需要的时间| |periodSeconds (int32)|探针的执行周期| |timeoutSeconds (int32)|探针超时的秒数| |failureThreshold (int32)|探针成功后的最小连续失败次数,超出此阈值则认为探针失败| |successThreshold (int32)|探针失败后最小连续成功次数,超过此阈值才会被视为探针成功| |ProbeHandler|定义探针的类型|
ProbeHandler 包含的探针有如下几种类型:
- Exec: 在容器启动后,在容器里面执行一条的命令
- HTTPGet: 在容器启动后,发送一个 get 请求
- TCPSocket: 在容器启动后,尝试一次 Tcp 连接
- GRPCAction: 在容器启动后,发送一个 Grpc 请求
|
|
livenessProbe
livenessProbe 定义一个健康检查“探针”(Probe)。kubelet 会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器镜像是否运行(来自 Docker 返回的信息)作为依据。这种机制,是生产环境中保证应用健康存活的重要手段。
|
|
readinessProbe
readinessProbe 用法与 livenessProbe 类似,但作用却大不一样。readinessProbe 检查结果的成功与否,决定的这个 Pod 是不是能被通过 Service 的方式访问到,而并不影响 Pod 的生命周期。
4.3 Hook
Lifecycle 定义的是 Container Lifecycle Hooks。顾名思义,Container Lifecycle Hooks 就是在容器状态发生变化时触发一系列“钩子”。
|
|
其中:
- postStart:
- 指的是,在容器启动后,立刻执行一个指定的操作。
- 需要明确的是,postStart 定义的操作,虽然是在 Docker 容器 ENTRYPOINT 执行之后,但它并不严格保证顺序。
- 也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。
- 如果 postStart 执行超时或者错误,Kubernetes 会在该 Pod 的 Events 中报出该容器启动失败的错误信息,导致 Pod 也处于失败的状态
- preStop:
- 发生的时机,则是容器被杀死之前
- preStop 操作的执行是同步的。所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死,这跟 postStart 不一样
LifecycleHandler 包含的 Handler 与 ProbeHandler 基本一直,只是少了 GRPCAction
|
|
5. Pod 生命周期
|
|
Pod 生命周期的变化,主要体现在 Pod API 对象的 Status 部分,其中,pod.status.phase,就是 Pod 的当前状态,它有如下几种可能的情况:
- Pending: API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功
- Running: Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中
- Succeeded: Pod 里的所有容器都正常运行完毕,并且已经退出了,运行一次性任务时最为常见
- Failed: Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出
- Unknown: 这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题
更进一步地,Pod 对象的 Status 字段,还可以再细分出一组 Conditions。这些细分状态的值包括:PodScheduled、Ready、Initialized,以及 Unschedulable。它们主要用于描述造成当前 Status 的具体原因是什么。比如,Pod 当前的 Status 是 Pending,对应的 Condition 是 Unschedulable,这就意味着它的调度出现了问题。
而其中,Ready 这个细分状态非常值得我们关注:它意味着 Pod 不仅已经正常启动(Running 状态),而且已经可以对外提供服务了。这两者之间(Running 和 Ready)是有区别的。
6.PodPreset
PodPreset 可以自动给对应的 Pod 对象填充字段。
6.1 PodPreset 定义
|
|
6.2 PodPreset
|
|
这个 PodPreset 会作用于 selector 所定义的、带有“role: frontend”标签的 Pod 对象。
PodPreset 里定义的内容,只会在 Pod API 对象被创建之前追加在这个对象本身上,而不会影响任何 Pod 的控制器的定义。比如,我们现在提交的是一个 nginx-deployment,那么这个 Deployment 对象本身是永远不会被 PodPreset 改变的,被修改的只是这个 Deployment 创建出来的所有 Pod。
如果你定义了同时作用于一个 Pod 对象的多个 PodPreset,Kubernetes 会帮你合并(Merge)这两个 PodPreset 要做的修改。而如果它们要做的修改有冲突的话,这些冲突字段就不会被修改。