03.深入理解pod
容器的单进程模型¶
我们经常都把容器比喻为"集装箱",把容器内部运行的内容比喻为"货物"。这种"集装箱"模型最大的好处就是,我们将自己处理好的应用封装起来,每个"集装箱"负责一款应用,每个应用与应用之间互相隔离,可以运行在同一个宿主机上。
在宿主机查看容器 PID 的时候,看到其启动的命令就是 docker run 时指定的命令(比如 nginx/java 等),也就是要让 Docker 容器要运行的功能。
容器的"单进程模型",其实并不是指容器里只能运行一个进程,而是指容器没有管理多个进程的能力。
这是因为容器里 PID=1 的进程就是容器应用本身,其他的进程都是这个 PID=1 进程的子进程。可是用户编写的业务应用,并不能够像正常Linux操作系统里的 init 进程或者 systemd 那样拥有进程管理的功能。
比如,你的应用是一个 Java Web 程序(PID=1),然后你执行 docker exec 进到容器bash后,在后台手动启动了一个 Nginx 进程(PID=3),可当这个 Nginx 进程异常退出的时候,你该怎么知道呢?这个进程退出后的垃圾收集工作,又应该由谁去做呢?
在容器中,1 号进程一般是 entrypoint 进程,针对上面这种 将孤儿进程的父进程置为 1 号进程进而避免僵尸进程 处理方式,容器是处理不了的。 进而就会导致容器中在孤儿进程这种异常场景下僵尸进程无法彻底处理的窘境。所以说,容器的单进程模型的本质其实是容器中的 1 号进程并不具有管理多进程、多线程等复杂场景下的能力。 如果一定在容器中处理这些复杂情况,那么需要开发者对 entrypoint 进程赋予这种能力。这无疑是加重了开发者的心智负担,这是任何一项大众技术或者平台框架都不愿看到的尴尬之地。
进程组和容器组¶
在一个真正的操作系统里,进程并不是“孤苦伶仃”地独自运行的,而是以进程组的方式,“有原则的”组织在一起。 在这个进程的树状图中,每一个进程后面括号里的数字,就是它的进程组 ID(Process Group ID, PGID)对于操作系统来说,这样的进程组更方便管理。 举个例子,Linux 操作系统只需要将信号,比如 SIGKILL 信号,发送给一个进程组,那么该进程组中的所有进程就都会收到这个信号而终止运行。
Kubernetes 项目所做的,其实就是将“进程组”的概念映射到了容器技术中,并使其成为了这个云计算“操作系统”里的“一等公民”。 由于不能将多个进程聚集在一个单独的容器中,我们需要另一种更高级的结构来将容器
深入理解pod 已知 rsyslogd 由三个进程组成:一个 imklog 模块,一个 imuxsock 模块,一个 rsyslogd 自己的 main 函数主进程。 这三个进程一定要运行在同一台机器上,否则,它们之间基于 Socket 的通信和文件交换,都会出现问题。对于这样三个必须要耦合在一起的进程。 Pod 是 Kubernetes 里的原子调度单位。这就意味着,Kubernetes 项目的调度器,是统一按照 Pod 而非容器的资源需求进行计算的。 所以,像 imklog、imuxsock 和 main 函数主进程这样的三个容器,正是一个典型的由三个容器组成的 Pod。 Kubernetes 项目在调度时,自然就会去选择可用内存等于 3 GB 的 node-1 节点进行绑定,而根本不会考虑 node-2。
像这样容器间的紧密协作,可以称为“超亲密关系”。这些具有“超亲密关系”容器的典型特征包括但不限于: - 互相之间会发生直接的文件交换、 - 使用 localhost 或者 Socket 文件进行本地通信、 - 会发生非常频繁的远程调用、 - 需要共享某些 Linux Namespace(比如,一个容器要加入另一个容器的 Network Namespace)等等。
这也就意味着,并不是所有有“关系”的容器都属于同一个 Pod。比如,PHP 应用容器和 MySQL虽然会发生访问关系,但并没有必要、也不应该部署在同一台机器上,它们更适合做成两个 Pod
何时在pod中使用多个容器¶
将多个容器添加到单个pod的主要原因可能是应用由一个主进程或多个辅助进程组成。 Pod 在 Kubernetes 项目里还有更重要的意义,那就是:容器设计模式。关于 Pod 最重要的一个事实是:它只是一个逻辑概念。 Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器的 Namespace 和 Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。
Pod,其实是一组共享了某些资源的容器。更具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个Volume。
那这么来看的话,一个有 A、B 两个容器的 Pod,不就是等同于一个容器(容器 A)共享另外一个容器(容器 B)的网络和 Volume 通过类似 docker run --net --volumes-from 这样的命令就能实现,如果这样,容器 B 就必须比容器 A 先启动,这样一个 Pod 里的多个容器就不是对等关系,而是有先后依赖关系。
在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器(也是pause容器)。
在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。
很容易理解,在Kubernetes 项目里,Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause
这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。
在 Infra 容器“Hold 住”Network Namespace 后,用户容器就可以加入到 Infra 容器的 Network Namespace 当中了。
所以,如果你查看这些容器在宿主机上的 Namespace 文件(这个Namespace 文件的路径,我已经在前面的内容中介绍过),它们指向的值一定是完全一样的。 这也就意味着,对于 Pod 里的容器 A 和容器 B 来说: - 它们可以直接使用 localhost 进行通信; - 它们看到的网络设备跟 Infra 容器看到的完全一样; - 一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址; - 当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享; - Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
对于同一个 Pod 里面的所有用户容器来说,它们的进出流量,也可以认为都是通过 Infra 容器完成的。
因为将来如果你要为 Kubernetes 开发一个网络插件时,应该重点考虑的是如何配置这个 Pod 的 Network Namespace,而不是每一个用户容器如何使用你的网络配置,这是没有意义的。 这就意味着,如果你的网络插件需要在容器里安装某些包或者配置才能完成的话,是不可取的: Infra 容器镜像的 rootfs 里几乎什么都没有,没有你随意发挥的空间。 当然,这同时也意味着你的网络插件完全不必关心用户容器的启动与否,而只需要关注如何配置 Pod,也就是 Infra 容器的 Network Namespace 即可。
验证一下,可以看到,7个pod,对应七个pause容器
root@qhdata-01:~#
root@qhdata-01:~# kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system hostpath-provisioner-69cd9ff5b8-9bt6g 1/1 Running 0 52m
kube-system coredns-6f5f9b5d74-2zz4z 1/1 Running 0 51m
kube-system calico-kube-controllers-6bb569694-lvff4 1/1 Running 0 65m
kube-system calico-node-hqqqp 1/1 Running 0 65m
kube-system kubernetes-dashboard-dc96f9fc-4pj7p 1/1 Running 0 50m
kube-system dashboard-metrics-scraper-7bc864c59-6j2kr 1/1 Running 0 50m
kube-system metrics-server-6f754f88d-7nbqg 1/1 Running 0 50m
root@qhdata-01:~#
root@qhdata-01:~#
root@qhdata-01:~#
root@qhdata-01:~#
root@qhdata-01:~# ctr c ls | grep pause
2c0272f634e23512c60697c9fa815dae4e592013a809b014d1e3b10931f3d06c registry.k8s.io/pause:3.7 io.containerd.runc.v2
4e6f85186ce24718274e77a41991033ddde6f274defadddefe2dd23b470f9a7b registry.k8s.io/pause:3.7 io.containerd.runc.v2
7f110e5b09cc8e0f14dd0edfd3f01f0a90daa9ce0d8660f7dbcdfb808c62eea7 registry.k8s.io/pause:3.7 io.containerd.runc.v2
8016054b064a988775c8f82e531912798b3c449ecb76c6ea1ac09e49f526756c registry.k8s.io/pause:3.7 io.containerd.runc.v2
965f37fa0ab9595e7b4ea80c4ba9fa4b362ea7c58fa3c9a609bba16ef54fedf5 registry.k8s.io/pause:3.7 io.containerd.runc.v2
b5d1bc3daf0fe3fc0b0ece3298c746a48ccdd490259547074d0a9169eabb83cb registry.k8s.io/pause:3.7 io.containerd.runc.v2
e582b6584b2d76b85f5aba940952360a352a6d78d5b8cb5fb159773993637cdb registry.k8s.io/pause:3.7 io.containerd.runc.v2
root@qhdata-01:~#
Pod 这种“超亲密关系”容器的设计思想,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。
有一个 Java Web 应用的 WAR 包,它需要被放在 Tomcat 的 webapps 目录下运行起来。两种方法: - 把 WAR 包直接放在 Tomcat 镜像的 webapps 目录下,做成一个新的镜像运行起来。如果你要更新 WAR 包的内容,或者要升级 Tomcat 镜像,就要重新制作一个新的发布镜像,非常麻烦。 - 只发布一个 Tomcat 容器。不过,这个容器的 webapps 目录,就必须声明一个 hostPath 类型的 Volume,从而把宿主机上的 WAR 包挂载进 Tomcat 容器当中运行起来。不过,这样你就必须要解决一个问题,即:如何让每一台宿主机,都预先准备好这个存储有 WAR 包的目录。
有了 Pod 之后,这样的问题就很容易解决了。我们可以把 WAR 包和 Tomcat 分别做成镜像,然后把它们作为一个 Pod 里的两个容器“组合”在一起。
apiVersion: v1
kind: Pod
metadata:
name: javaweb-2
spec:
initContainers:
- image: geektime/sample:v2
name: war
command: ["cp", "/sample.war", "/app"]
volumeMounts:
- mountPath: /app
name: app-volume
containers:
- image: geektime/tomcat:7.0
name: tomcat
command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
volumeMounts:
- mountPath: /root/apache-tomcat-7.0.42-v2/webapps
name: app-volume
ports:
- containerPort: 8080
hostPort: 8001
volumes:
- name: app-volume
emptyDir: {}
在这个 Pod 中,我们定义了两个容器: - 第一个容器使用的镜像是 geektime/sample:v2,这个镜像里只有一个 WAR 包(sample.war)放在根目录下。 - 第二个容器则使用的是一个标准的 Tomcat 镜像。
WAR 包容器的类型不再是一个普通容器,而是一个 Init Container 类型的容器。 所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
这个 Init Container 类型的 WAR 包容器启动后,执行了一句 "cp /sample.war /app",把应用的 WAR 包拷贝到 /app 目录下,然后退出。 而后这个 /app 目录,就挂载了一个名叫 app-volume 的 Volume。接下来就很关键了。Tomcat 容器,同样声明了挂载 app-volume 到自己的 webapps 目录下。 等 Tomcat 容器启动时,它的 webapps 目录下就一定会存在 sample.war 文件:这个文件 正是 WAR 包容器启动时拷贝到这个 Volume 里面的,而这个 Volume 是被这两个容器共享的。
像这样,我们就用一种“组合”方式,解决了 WAR 包与 Tomcat 容器之间耦合关系的问题。 实际上,这个所谓的“组合”操作,正是容器设计模式里最常用的一种模式,它的名字叫: sidecar(边车模式) 顾名思义,sidecar 指的就是我们可以在一个 Pod 中,启动一个辅助容器,来完成一些独立于主进 程(主容器)之外的工作。 比如,在我们的这个应用 Pod 中,Tomcat 容器是我们要使用的主容器,而 WAR 包容器的存在, 只是为了给它提供一个 WAR 包而已。所以,我们用 InitContainer 的方式优先运行 WAR 包容 器,扮演了一个 sidecar 的角色。
Pod 到底是什么?¶
Pod 是 Kubernetes 项目与其他单容器项目相比最大的不同,也是一位容器技术初学者需要面对的第一个与常规认知不一致的知识点。 事实上,直到现在,仍有很多人把容器跟虚拟机相提并论,他们把容器当做性能更好的虚拟机,喜欢讨论如何把应用从虚拟机无缝地迁移到容器中。 但实际上,无论是从具体的实现原理,还是从使用方法、特性、功能等方面,容器与虚拟机几乎没有任何相似的地方;也不存在一种普遍的方法,能够把虚拟机里的应用无缝迁移到容器中。
因为,容器的性能优势,必然伴随着相应缺陷,即:它不能像虚拟机那样,完全模拟本地物理机环境中的部署方法。 所以,这个“上云”工作的完成,最终还是要靠深入理解容器的本质,即:进程。
实际上,一个运行在虚拟机里的应用,哪怕再简单,也是被管理在 systemd 或者 supervisord 之下的一组进程,而不是一个进程。这跟本地物理机上应用的运行方式其实是一样的。 这也是为什么,从物理机到虚拟机之间的应用迁移,往往并不困难。 可是对于容器来说,一个容器永远只能管理一个进程。更确切地说,一个容器,就是一个进程。这是容器技术的“天性”,不可能被修改。
所以,将一个原本运行在虚拟机里的应用,“无缝迁移”到容器中的想法,实际上跟容器的本质是相悖的。 这也是当初 Swarm 项目无法成长起来的重要原因之一:一旦到了真正的生产环境上,Swarm 这种单容器的工作方式,就难以描述真实世界里复杂的应用架构了。
pod对象 Pod 这个概念,提供的是一种编排思想,而不是具体的技术方案。 Pod,而不是容器,才是 Kubernetes 项目中的最小编排单位。将这个设计落实到 API 对象上,容器(Container)就成了 Pod 属性里的一个普通的字段。 把 Pod 看成传统环境里的"机器"、把容器看作是运行在这个"机器"里的"用户程 序"
凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。这些属性的共同特征是,它们描述的是“机器”这个整体,而不是里面运行的“程序”。比如 - 配置这个“机器”的网卡(即:Pod 的网络定义) - 配置这个“机器”的磁盘(即:Pod 的存储定义) - 配置这个“机器”的防火墙(即:Pod 的安全定义) - 这台“机器”运行在哪个服务器之上(即:Pod 的调度)
https://github.com/kubernetes/api/blob/master/core/v1/types.go
nodeSelector:是一个供用户将 Pod 与 Node 进行绑定的字段。
这样的一个配置,意味着这个 Pod 永远只能运行在携带了"disktype: ssd"标签(Label)的节点上;否则,它将调度失败。 可以约束一个 Pod 以便限制其只能在特定的节点上运行, 或优先在特定的节点上运行。有几种方法可以实现这点, 推荐的方法都是用标签选择算符来进行选择。
在 Kubernetes 中,打标签(Labeling)是管理资源的"基本功"。你可以把标签想象成贴纸,方便你后续通过"标签选择器(Selector)"来批量管理或过滤这些资源。
kubectl label nodes node1 disktype=ssd
kubectl label nodes k8s-node1 node-role.kubernetes.io/jenkins=
nodeName:一旦 Pod 的这个字段被赋值,Kubernetes 项目就会被认为这个 Pod 已经经过了调度,调度的结果就是赋值的节点名字。 所以,这个字段一般由调度器负责设置,但用户也可以设置它来“骗过”调度器,当然这个做法一般是在测试或者调试的时候才会用到。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeName: kube-01
hostAliases:定义了 Pod 的 hosts 文件(比如 /etc/hosts)里的内容:
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
凡是跟容器的 Linux Namespace 相关的属性,也一定是 Pod 级别的。这个原因也很容易理解: Pod 的设计,就是要让它里面的容器尽可能多地共享 Linux Namespace,仅保留必要的隔离和限制能力。这样,Pod 模拟出的效果,就跟虚拟机里程序间的关系非常类似了。
当启用进程命名空间共享(shareProcessNamespace=true)时,容器中的进程对同一Pod中的所有其他容器都是可见的。 你可以使用此功能来配置协作容器,比如日志处理 sidecar 容器, 或者对那些不包含诸如 shell 等调试实用工具的镜像进行故障排查。
- 使用 Pod 中的 .spec.shareProcessNamespace 字段可以启用进程命名空间共享。¶
这就意味着,整个 Pod 里的每个容器的进程,对于所有容器来说都是可见的:它们共享了同一个 PID Namespace。
类似地,凡是Pod中的容器要共享宿主机的Namespace,也一定是Pod级别的定义
hostPort 与 hostNetwork 本质上都是暴露 pod 所在节点 IP 给终端用户,因为 pod 生命周期并不固定,随时都有可能异常重建,故 IP 的不确定最终导致用户使用上的不方便; 此外宿主机端口占用也导致不能在同一台机子上有多个程序使用同一端口。因此一般情况下,不建议使用 hostPort 方式。
ImagePullPolicy spec.containers.ImagePullPolicy 它定义了镜像拉取的策略。而它之所以是一个Container级别的属性,是因为容器镜像本来就是Container定义中的一部分。 ImagePullPolicy的值默认是Always,即每次创建Pod都重新拉取一次镜像。另外,当容器的镜像是类似于nginx或者nginx:latest这样的名字时,ImagePullPolicy也会被认为Always。 而如果它的值被定义为Never或者IfNotPresent,则意味着Pod永远不会主动拉取这个镜像,或者只在宿主机上不存在这个镜像时才拉取。
Lifecycle 定义的是 Container Lifecycle Hooks。顾名思义,Container Lifecycle Hooks 的作用,是在容器状态发生变化时触发一系列“钩子”。 postStart 指的是,在容器启动后,立刻执行一个指定的操作。需要明确的是, postStart 定义的操作,虽然是在 Docker 容器 ENTRYPOINT 执行之后,但它并不严格保证顺序。 也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。如果 postStart 执行超时或者错误,Kubernetes 会在该 Pod 的 Events 中报出该容器启动 失败的错误信息,导致 Pod 也处于失败的状态。
preStop 发生的时机,则是容器被杀死之前(比如,收到了 SIGKILL 信号)。而需要明 确的是,preStop 操作的执行,是同步的。 所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死,这跟 postStart 不一样。 在这个容器被删除之前,我们则先调用了 nginx 的退出指令 (即 preStop 定义的操作),从而实现了容器的“优雅退出”。
Downward API 它的作用是:让Pod里的容器能够直接获取到这个Pod API对象本身的信息。
Downward API能够获取到的信息,一定是Pod里的容器进程启动之前就能够确定下来的信息。 而如果你想要获取Pod容器运行后才会出现的信息,比如,容器进程的PID,那就肯定不能使用Downward API了,而应该考虑在Pod里定义一个sidecar容器。
其实,Secret、ConfigMap,以及Downward API这三种Projected Volume定义的信息,大多还可以通过环境变量的方式出现在容器里。 但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,我都建议你使用Volume文件的方式获取这些信息。
Pod生命周期 Pod 生命周期的变化,主要体现在 Pod API 对象的 Status 部分,这是它除了 Metadata 和 Spec之外的第三个重要字段。 Pod 的阶段(Phase)是 Pod 在其生命周期中所处位置的简单宏观概述。 该阶段并不是对容器或 Pod 状态的综合汇总,也不是为了成为完整的状态机。 其中,pod.status.phase,就是 Pod 的当前状态阶段,它有如下几种可能的情况: 状态 (Phase)核心含义常见场景/原因Pending挂起/待定。YAML 已提交并持久化到 etcd,但 Pod 尚未运行。正在调度中、正在拉取镜像、由于资源不足(CPU/内存)无法调度。Running运行中。Pod 已绑定到节点,容器已创建,且至少有一个容器正在运行或启动中。业务正常运行,或容器正在重启中。Succeeded成功。Pod 中所有容器都已成功执行完毕并退出(退出码为 0)一次性任务(Job)完成,如数据备份、数据迁移脚本Failed失败。Pod 中至少有一个容器是因为非 0 状态码退出,或者被系统终止。应用程序崩溃(Crash)、配置错误、内存溢出(OOMKilled)。Unknown未知。API Server 无法获取 Pod 状态,通常是与所在节点的通信丢失。节点宕机、网络分区(Network Partition)、kubelet 进程挂掉。
更进一步地,Pod 对象的 Status 字段,还可以再细分出一组 Conditions。这些细分状态的值包 括:PodScheduled、Ready、Initialized,以及 Unschedulable。 它们主要用于描述造成当前 Status 的具体原因是什么
Condition 类型含义状态为 True 的意义PodScheduled调度阶段调度器(Scheduler)已成功为 Pod 选定了运行的节点(Node)。Initialized初始化阶段Pod 的所有 Init Containers(初始化容器)都已经成功执行完毕并退出。Ready就绪阶段Pod 已经可以接收流量了。这意味着容器通过了 Readiness Probe(就绪探针)。ContainersReady容器就绪Pod 中所有的容器都已就绪(这是 Ready 状态的基础)。Unschedulable不可调度(通常出现在错误时) 表示调度器找不到满足条件的节点(如资源不足)。
status:
conditions:
- type: Ready # a built in PodCondition
status: "False"
lastProbeTime: null
lastTransitionTime: 2018-01-01T00:00:00Z
- type: "www.example.com/feature-1" # an extra PodCondition
status: "False"
lastProbeTime: null
lastTransitionTime: 2018-01-01T00:00:00Z
pod和其他kubernetes
ImagePullPolicy
ServiceAccount 我现在有了一个Pod,我能不能在这个Pod里安装一个Kubernetes的Client,这样就可以从容器里直接访问并且操作这个Kubernetes的API了呢?
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已经为你提供了一个默认“服务账户”(default Service Account)。并且,任何一个运行在Kubernetes里的Pod,都可以直接使用这个默认的Service Account,而无需显示地声明挂载它。 这是如何做到的呢?当然还是靠Projected Volume机制。 如果你查看一下任意一个运行在Kubernetes集群里的Pod,就会发现,每一个Pod,都已经自动声明一个类型是Secret、名为default-token-xxxx的Volume,然后 自动挂载在每个容器的一个固定目录上。
一个Kubernetes对象的主要编排逻辑,实际上是在第三步的“对比”阶段完成的。 这个操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。 调谐的最终结果,往往都是对被控制对象的某种写操作。 比如,增加Pod,删除已有的Pod,或者更新Pod的某个字段。这也是Kubernetes项目“面向API对象编程”的一个直观体现。 其实,像Deployment这种控制器的设计原理,就是我们前面提到过的,“用一种对象管理另一种对象”的“艺术”。 其中,这个控制器对象本身,负责定义被管理对象的期望状态。比如,Deployment里的replicas=2这个字段。 而被控制对象的定义,则来自于一个“模板”。比如,Deployment里的template字段。 可以看到,Deployment这个template字段里的内容,跟一个标准的Pod对象的API定义,丝毫不差。而所有被这个Deployment管理的Pod实例,其实都是根据这个template字段的内容创建出来的。 像Deployment定义的template字段,在Kubernetes项目中有一个专有的名字,叫作PodTemplate(Pod模板)。
编排对象 Kubernetes 系统是一套分布式容器应用编排系统,当我们用它来承载业务负载时主要使用的编排对象有 Deployment、ReplicaSet、StatefulSet、DaemonSet 等。 其实这些对象都是对 Pod 对象的扩展封装。并且这些对象作为核心工作负载 API 固化在 Kubernetes 系统中。
在应对常规业务容器的场景之下,Kubernetes 提供了 Deployment 标准编排对象,从命令上我们就可以理解它的作用就是用来部署容器应用的。 Deployment 管理的是业务容器 Pod,因为容器技术具备虚拟机的大部分特性,往往让用户误解认为容器就是新一代的虚拟机。 从普通用户的印象来看,虚拟机给用户的映象是稳定可靠。如果用户想当然地把业务容器 Pod 也归类为稳定可靠的实例,那就是完全错误的理解了。 容器组 Pod 更多的时候是被设计为短生命周期的实例,它无法像虚拟机那样持久地保存进程状态。因为容器组 Pod 实例的脆弱性,每次发布的实例数一定是多副本,默认最少是 2 个
Pod中容器分类
Pod Sandbox 熟悉 Pod 生命周期的同学应该知道,创建 Pod 时 Kubelet 先调用 CRI 接口 RuntimeService.RunPodSandbox 来创建一个沙箱环境,为 Pod 设置网络(例如:分配 IP)等基础运行环境。当 Pod 沙箱(Pod Sandbox)建立起来后,Kubelet 就可以在里面创建用户容器。当到删除 Pod 时,Kubelet 会先移除 Pod Sandbox 然后再停止里面的所有容器。
从 Kubernetes 的底层容器运行时 CRI 看,Pod 这种在统一隔离环境里资源受限的一组容器,就叫 Sandbox。典型的pause容器,主要是维持住这个容器组所需的名称空间环境。 这是一种特殊的容器,不需要在pod的yaml中显式定义,用kubectl查看pod的详细描述,也看不到它的定义。甚至在节点上用crictl都看吧遇到
initC Init 容器是一种特殊容器,在 Pod 内的应用容器启动之前运行 它充当准备步骤,允许在主容器中执行初始化任务、配置先决条件或配置应用程序所需的依赖项。Init 容器可以包括一些应用镜像中不存在的实用工具和安装脚本。几个应用场景: - 加载和配置依赖项::Init 容器可以在主应用程序容器开始运行之前加载和配置主应用程序容器所需的依赖项。 - 创建数据库架构:你可以使用 init 容器创建数据库架构。(初始化数据,执行建表DDL等语句) - 预热缓存:你可以使用 init 容器预热缓存。例如,将一些常用数据预加载到 Redis 缓存中。 - 网络配置:Init 容器可以处理建立网络配置或建立与外部服务的连接等任务。 - Git 克隆:Init 容器可以克隆 Git 存储库或将文件写入附加的 Pod 卷。 - 安全检查:Init 容器可以在启动主应用程序容器之前执行安全检查,例如漏洞扫描或 TLS 证书验证。 - 访问 Secrets:Init 容器可以访问应用程序容器无法访问的 Secret,例如从文件库中检索 Secret。 - 环境设置::Init 容器可以处理创建目录、应用权限或运行自定义脚本等任务,以为主应用程序设置环境。 - Wait for Services:: Init 容器可以等待服务启动后再启动主应用程序。
init容器工作特征: 1、kubelet按照 Init 容器在 Pod 的 spec 中出现的顺序运行它们,确保每个容器在开始下一个容器之前完成其任务。这意味着一次只运行一个 init 容器。 2、Init Containers 在主应用程序容器启动之前运行。 3、如果 Pod 重启,则其所有 init 容器将再次运行。 4、在 Pod 生命周期中,init 容器在 pending 阶段运行完成。 5、虽然 init 容器具有相同的容器规范,但它们不支持 lifecycle、livenessProbe、readinessProbe 和 startupProbe 字段。(本机 sidecar Alpha 功能除外)
因为 Init 容器具有与应用容器分离的单独镜像,其启动相关代码具有如下优势: - Init 容器可以包含一些安装过程中应用容器中不存在的实用工具或个性化代码。 例如,没有必要仅为了在安装过程中使用类似 sed、awk、python 或 dig 这样的工具而去 FROM 一个镜像来生成一个新的镜像 - 应用镜像的创建者和部署者可以各自独立工作,而没有必要联合构建一个单独的应用镜像。 - 与同一 Pod 中的多个应用容器相比,Init 容器能以不同的文件系统视图运行。因此,Init 容器可以被赋予访问应用容器不能访问的 Secret 的权限。 - 由于 Init 容器必须在应用容器启动之前运行完成,因此 Init 容器提供了一种机制来阻塞或延迟应用容器的启动,直到满足了一组先决条件。 一旦前置条件满足,Pod 内的所有的应用容器会并行启动。 - Init 容器可以安全地运行实用程序或自定义代码,而在其他方式下运行这些实用程序或自定义代码可能会降低应用容器镜像的安全性。 通过将不必要的工具分开,你可以限制应用容器镜像的被攻击范围。
mainC
Pod类型 在 Kubernetes (K8S) 中,Pods 可以根据其创建和管理的方式分为几类。下面是三种常见的 Pod 类型:静态 Pod、自主式 Pod 和动态 Pod。
静态 Pod (Static Pods) 静态 Pod 在指定的节点上由 kubelet 守护进程直接管理,不需要 Kubernetes API Server 监管。 (它绕过了Kubernetes API Server的控制平面,直接由节点上的kubelet进程管理) 与由控制面管理的 Pod(例如Deployment) 不同;kubelet 监视每个静态 Pod(在它失败之后重新启动)。静态 Pod 始终都会绑定到特定节点的 Kubelet 上。
维度静态Pod普通Pod管理主体kubelet直接管理由Controller Manager通过API Server管理调度位置仅限当前节点可调度到任意节点生命周期随kubelet启停由控制器管理可见性节点本地(默认不注册到API Server)集群全局可见更新方式修改本地manifest文件通过kubectl或控制器更新
虽然静态pod不属于控制面来直接管辖,但是控制面作为管理平台,它应该也要知道每个节点上自己跑的静态pod,至少是能观测到。 镜像 Pod:kubelet 启动静态 Pod 后,会主动向 API Server 汇报:“报告,我本地起了一个重要的 Pod,请给它建立一个档案。” API Server 于是创建了一个只读的记录。 kubelet 会尝试通过 Kubernetes API 服务器为每个静态 Pod 自动创建一个镜像 Pod。 这意味着节点上运行的静态 Pod 对 API 服务来说是可见的,但是不能通过 API 服务器来控制。 Pod 名称将把以连字符开头的节点主机名作为后缀。
- 定义:
- 静态 Pod 是一种特殊的 Pod 类型,它们不是通过 Kubernetes API 服务器创建的,而是直接在 Node 节点由kubelet监听管理静态pod
- 静态 Pod 的配置文件通常放置在Node节点的 /etc/kubernetes/manifests 目录中(或通过 --manifest-dir 参数指定的其他目录)
- kubelet 会处理静态 Pod 目录中所有不是以点开头的文件—— 不会根据文件扩展名进行过滤。
- 特点:
- 不受高可用性(HA)保护:如果节点宕机,静态 Pod 将不可用,直到节点恢复。
- 不支持滚动更新或回滚。
- 不受 Kubernetes API 服务器的管理,因此不支持高级功能,如自动伸缩、健康检查等。
- 主要用于运行需要在所有节点上运行的服务,如集群监控代理等。
kubelet 会处理静态 Pod 目录中所有不是以点开头的文件—— 不会根据文件扩展名进行过滤。 例如,如果你通过执行 cp kube-apiserver.yaml kube-apiserver.yaml.backup 来创建一个清单文件的备份,kubelet 将读取这两个文件, 并尝试分别从中创建一个静态 Pod。当两个文件定义了同名的 Pod 时, 其结果行为是未定义的,并且可能导致备份文件中过时的规范静默生效, 而不是使用当前的清单文件。如果你确实创建了备份文件, 应将其存储在静态 Pod 目录之外(例如,/etc/kubernetes/backup/ 目录)。 静态Pod的作用
节点级基础服务部署 可以用于在每个节点上运行一些关键的、与节点紧密相关的基础服务。例如,在 Kubernetes 集群中,用于监控节点资源使用情况的监控代理。 如 Prometheus Node Exporter,它需要在每个节点上运行,以收集该节点的 CPU、内存、磁盘等资源的使用数据。 通过将监控代理定义为静态 Pod,Kubelet 会在节点启动时自动创建和管理这些 Pod,确保监控服务在每个节点上都能稳定运行。 本地资源管理 有助于管理节点上的本地资源。比如,在一些需要使用节点本地存储的场景中,静态 Pod 可以被配置为挂载节点上的特定目录,用于存储和处理本地数据。 以日志收集为例,将日志收集器设置为静态 Pod,它可以直接访问节点上的日志文件目录,将日志数据进行收集和处理,然后发送到集中式的日志管理系统中。 这样可以确保每个节点上的日志都能被及时收集,而不会因为依赖 API Server 的调度而出现延迟或故障。 集群初始化和引导 在 Kubernetes 集群的初始化和引导阶段,静态 Pod 起着重要的作用。 例如,kube-apiserver、kube-controller-manager 和 kube-scheduler 等控制平面组件,在一些部署方式中可以作为静态 Pod 运行在特定的控制节点上。 这些静态 Pod 会在节点启动时首先被创建和启动,为整个集群的正常运行提供基础的控制和管理功能。 它们的稳定运行是集群后续正常工作的前提,能够确保 API Server 正常提供服务,控制器能够对集群中的资源进行有效的管理和调度。 高可用和故障转移 对于一些对可用性要求较高的服务,可以将其部署为静态 Pod。 因为静态 Pod 直接由 Kubelet 管理,不依赖于 API Server 的调度,所以在 API Server 出现故障或网络问题时,静态 Pod 仍然可以在节点上保持运行状态,提供一定程度的服务连续性。 例如,在一个分布式存储系统中,存储节点的静态 Pod 可以在节点故障恢复后自动重新启动,确保存储服务的快速恢复,减少数据丢失和服务中断的风险。 简化部署和管理 对于一些简单的、不需要复杂调度和管理的应用,使用静态 Pod 可以简化部署过程。 用户只需要将 Pod 的配置文件放置在 Kubelet 指定的目录下,Kubelet 就会自动创建和管理这些 Pod,无需通过 API Server 进行复杂的资源申请和调度流程。 这对于一些测试环境或小型集群中的简单应用来说,能够提高部署效率,降低管理成本。
卷 在 Kubernetes 中,有几种特殊的 Volume,它们存在的意义不是为了存放容器里的数据,也不是 用来进行容器和宿主机之间的数据交换。这些特殊 Volume 的作用,是为容器提供预先定义好的数据。 所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes"投 射"(Project)进入容器当中的。
root@k8s-master-01:~#
root@k8s-master-01:~# kubectl describe pod k8m-6f69854c9f-ps5nl -n k8m
Name: k8m-6f69854c9f-ps5nl
Namespace: k8m
Priority: 0
Service Account: k8m
Node: <none>
Labels: app=k8m
pod-template-hash=6f69854c9f
Annotations: <none>
Status: Pending
IP:
IPs: <none>
Controlled By: ReplicaSet/k8m-6f69854c9f
Containers:
k8m:
Image: registry.cn-hangzhou.aliyuncs.com/minik8m/k8m:v0.26.17
Port: 3618/TCP (http-k8m)
Host Port: 0/TCP (http-k8m)
Liveness: http-get http://:3618/healthz delay=30s timeout=1s period=10s #success=1 #failure=3
Environment:
POD_NAME: k8m-6f69854c9f-ps5nl (v1:metadata.name)
POD_NAMESPACE: k8m (v1:metadata.namespace)
POD_IP: (v1:status.podIP)
DEBUG: false
LOG_V: 2
PORT: 3618
KUBECONFIG: ~/.kube/config
ENABLE_AI: true
KUBECTL_SHELL_IMAGE: bitnami/kubectl:latest
NODE_SHELL_IMAGE: alpine:latest
SQLITE_PATH: /app/data/k8m.db
CONNECT_CLUSTER: false
IN_CLUSTER: true
PRINT_CONFIG: false
Mounts:
/app/data from k8m-data (rw)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-tqxfp (ro)
Conditions:
Type Status
PodScheduled False
Volumes:
k8m-data:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: k8m-pvc
ReadOnly: false
kube-api-access-tqxfp:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
Optional: false
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m40s default-scheduler 0/5 nodes are available: pod has unbound immediate PersistentVolumeClaims. not found
root@k8s-master-01:~#