Kubernetes的pod解析
容器、镜像、Pod三者的关系
在正式学习pod这个概念之前, 我想先和读者共同学习一下容器、镜像、pod这几个我们在云原生环境中经常听到的名词的概述, 以及他们三者之间究竟有者怎么样的关联关系, 使得我们在云原生中常常用到。
镜像——部署项目的基石
定义:容器镜像是一个只读的模板,包含了运行应用程序所需的所有代码、运行时库、环境变量和配置文件等。它是一个特殊的文件系统,用于提供容器运行时所需的程序、库、资源、配置等文件,并包含了一些为运行时准备的一些配置参数
作用:
在制作镜像时 , 常常用到的就是Docker技术 。制作成的镜像使得应用程序及其依赖项可以在不同的环境中进行部署和运行, 无需担心环境问题而导致的问题。
它是创建容器的起点,通过在镜像上添加一个可写层,容器可以在镜像的基础上进行变化,而不会影响到原始镜像 , 其实对于相关的配置文件在现网中不是打包到镜像中的,而是通过环境变量的方式读取的, 这就是在可写层执行的一个实例。
容器——应用运行的实例
定义:
容器是Docker的核心概念之一,是一个独立运行的应用程序及其所有运行时依赖项的轻量级、可执行单元。它与镜像几乎一模一样,区别在于容器的最上面那一层是可读可写的。
作用:
容器利用操作系统内核的功能(命名空间和控制组cgroup技术实现的)来隔离进程,并控制进程可以访问的CPU、内存和磁盘的数量。它们小巧轻便、速度快且可移植,可以在桌面、传统IT还是云端运行。
命名空间隔离了每个容器的进程、网络、用户和挂载点,确保容器之间相互隔离。而cgroup则负责限制容器可以使用的资源,如CPU、内存和存储等。
容器与虚拟机的对比
与传统虚拟机相比,Docker容器具有更轻量级和快速启动的特点。传统虚拟机是在宿主机上运行一个完整的操作系统,而容器则共享宿主机的操作系统内核。这使得容器的资源消耗更低,启动时间更快。同时,由于容器共享操作系统,容器的隔离性相对虚拟机略低,但仍然足以满足大多数应用场景的需求。
这是他们在应用架构上的对比
pod——资源调度的基本单位
为什么我要讲pod和容器、镜像拿出来共同对比呢。
随着容器数量的增加, 手动管理容器变的越来越困难。 这是就引入了编排工具kubernetes和DockerSwarm等。 但是DockerSwarm并没有改变自身, 而是在Docker的基础上做改进, 同时在支持广度上也只支持Docker镜像, 所以导致逐渐被淘汰。
但是kubernetes则不同, 他能够兼容各种镜像, 同时还能够自动化容器的部署、伸缩和管理, 使得容器集群的管理变的更加高效。 在这基础上, pod应运而生, 他将一组或者一个容器一个单独的单位。 要么全都调度成功, 要么全部调度失败。 同时对于像Java和Tomcat这种相互依赖的东西, pod的调度更加有优势。 对于相同作用的应用服务,给予其“同生共死”的权限。
但是, pod又是如何管理容器的呢 ? 如何将其作为一个整体来管理的? 这些都是我们的疑惑。 下面简要说说。 具体深入学习后面我再整理输出。
CRI/OCI/RunTime(容器运行时)/Pod 之间的关系
为什么要说CRI ? 他又是什么 ?
为什么从 CRI
讲起,因为 k8s 集群使用 kubelet
服务通过CRI
接口和对应的 runtime(运行时)
交互,从而控制管理容器。
CRI(Container Runtime Interface)
CRI是个接口是Kubernetes中用于定义容器运行时与Kubelet之间通信的规范。它使得Kubernetes能够使用各种容器运行时,而不仅限于其最初默认的Docker。
借用山河无恙大哥的一张图
在 K8s
生态中通过 CRI
接口来对 容器运行时进行管理,从而实现对容器镜像的管理,具体一点,通过 kubelet
调用容器运行时的 grpc
接口。
面向接口编程,类比在刚学编程时, Java
中,操作数据库,使用 JDBC API
来连接不同的数据库实现 CRUD
,这里具体的数据操作通过不同数据库的驱动包
来实现。CRI
可以单纯理解为JDBC
,CRI 实现类比不同的数据库驱动包
CRI接口使用Protocol Buffer,基于gRPC,定义了容器和镜像的服务接口。它分为两个主要服务:ImageService和RuntimeService。ImageService负责从仓库拉取镜像、查看和移除镜像的功能。RuntimeService则负责Pod和容器的生命周期管理,以及与容器的交互(如exec/attach/port-forward).
OCI(Open Container Initiative)—— 开放标准组织
OCI定义了一套容器规范,包括容器的镜像格式、运行时规范等。
<容器运行时>Container Runtime 组件
这个基础组件使 Kubernetes 能够有效运行容器。 它负责管理 Kubernetes 环境中容器的生命周期管理,包括创建、启动、停止和删除容器等操作。
你可以允许集群为一个 Pod 选择其默认的容器运行时。如果你需要在集群中使用多个容器运行时, 你可以为一个 Pod 指定 RuntimeClass, 以确保 Kubernetes 会使用特定的容器运行时来运行这些容器。
容器运行时可以有多种实现,例如Docker、containerd、CRI-O等。这些容器运行时实现了CRI接口,使得Kubernetes可以与它们兼容。
总结关系概述
:::info
- Kubernetes与CRI:Kubernetes通过CRI接口与容器运行时通信,管理Pod中容器的生命周期。
- CRI与Runtime:容器运行时实现CRI接口,使得Kubernetes可以与不同的容器运行时兼容。
- OCI与Runtime:容器运行时通常遵循OCI规范,确保不同容器技术之间的互操作性。
- Pod与Runtime:Pod中的容器由容器运行时管理,它们共享存储、网络和运行选项
:::
Pod
前面这些基础知识是我们学习kubernetes基本必须要掌握的内容。只有掌握了整体的大致架构, 才能在每个知识点的学习中对应上, 这样才能构建出自己的知识树。
回归正题, 通过前面的学习我们都知道pod是容器编排的最基本单位。 所以,想要学好kubernetes, pod的各种细节是一定要烂熟于心的。 下面我们就来一点点学习
Pod 的生命周期
Pod 在其生命周期中只会被调度一次。 将 Pod 分配到特定节点的过程称为绑定,而选择使用哪个节点的过程称为调度。 一旦 Pod 被调度并绑定到某个节点,Kubernetes 会尝试在该节点上运行 Pod。 Pod 会在该节点上运行,直到 Pod 停止或者被终止; 如果 Kubernetes 无法在选定的节点上启动 Pod(例如,如果节点在 Pod 启动前崩溃), 那么特定的 Pod 将永远不会启动。
pod的创建
在上一章的学习中, 我将pod创建的大致细节介绍了下, 同时,如果读者细心的话, 还会发现这两张图。
这是通过原生的pod的形式创建的两张图 ,通过这两张图的STATUS
可以看到,刚开是创建的时候STATUS的值为ContainerCreating
然后创建成功之后 STATUS的值为Running
, 这是我们能够通过命令查看到的。 具体的细节 ,下面我来详细解释下。
:::info
pod 创建详细流程
以创建nginx的pod为例
- 用户在编写完pod的yaml文件之后在命令行中输入
kubectl apply -f nginx.yaml
向Kubernetes API Server 发起一个创建pod的请求。 - Kubernetes API Server 接收到创建 Pod 的请求后,会对请求进行验证(验证文件的合法性、格式、内容类型等等)和授权检查。验证成功之后。API Server 不会直接创建 Pod,而是将这个请求转化为一个内部表示,例如一个含有 Pod 创建信息的 etcd 记录。
- 调度器(Scheduler)监视API Server并分配至合适的节点。 调度要考虑的事情有很多, 比如 资源需求(如 CPU 和内存)、节点的可用资源、节点标签、亲和性和反亲和性规则等。
- kubelet接收并创建Pod。在调度器选定好节点之后 ,该节点上的kubelet组件,会从API Server获取新的Pod配置。 然后按照OCI标准 , 通过CRI接口调用容器运行时。来创建并启动容器
- 如果Pod创建失败, kubelet可以启动容器,或者根据重启策略重新创建pod。 Kubelet 监控容器的运行状态,并将状态更新反馈给 API Server。
- 服务发现和负载均衡。 如果pod暴露了服务,kubernetes的Service就会实现服务发现和负载均衡。 kubernetes会通过kube-proxy组件在每个节点上配置网络规则, 以实现Pod之间和Pod与外部之间的通信。
:::
Pod销毁
:::info
Pod销毁的流程
- 用户发起删除请求
kubectl delete pod <pod-name>
然后通过API Server处理删除请求 - API Server 接收到删除 Pod 的请求,并将该请求写入 etcd。 **API Server 将 Pod 的状态更新为
**Terminating**
,并在元数据Metadata中添加 ****deletionTimestamp**
**。 ** - 控制器管理器(Controller Manager) 监听到Pod状态为
Terminating
并且开始执行预定义的删除流程 。对于有状态服务(绑定了持久卷声明PersistentVolumeClaim(PVC) ), 控制器管理器会更新PVC 。 - 发送sigterm信号给容器,清理资源。 kubelet监听到元数据Metadata中标记的
**deletionTimestamp**
就会通知容器优雅的关闭。 容器有 30 秒的宽限期(grace period)来完成正在进行的工作并优雅退出。 - 对于那些不想优雅退出的容器, kubelet会在宽限期结束时发送sigkill信号强制终止容器。
- 容器运行时(如 Docker 或 containerd)收到 SIGTERM 或 SIGKILL 信号后,停止并移除容器。
- 清理资源, EmptyDir卷, CNI网络插件等等
- 最后从APIServer中删除pod, 也就是kubelet清理完成之后发送信号, API Server 最终删除etcd中关于pod的全部信息
:::
上述的总结,我个人人为非常详细了, 通过对每一步的详细分析, 我们大致可以知道一个pod从创建到启动的全过程, 以及从运行中到删除完成。
调度的基础
然后具体的每个容器之间是通过**API Server进行通信的, 请求是 基于 HTTP/HTTPS 协议的 RESTful API **
其他的比如说Scheduler和kubelet监听pod的状态也是通过http请求
- kubelet的监听机制
kubelet监听pod的机制是通过Watch机制。 Watch 是 Kubernetes API 的一种特性,它允许客户端(如 Kubelet)持续接收对象的更改通知。 **
Kubelet 会发起一个 Watch 请求,API Server 会保持这个连接,并在有相关对象的更改时立即返回更改内容。
Watch 请求是通过 HTTP/HTTPS 协议进行的,通常使用长连接**(长时间保持连接
**API Server 返回的响应是一个持续的 JSON 流,每当有对象变化(如创建、更新或删除)时,都会在流中发送一个事件通知。 **
- Scheduler的监听机制
Scheduler 监听 Pod 的机制是一种
Scheduler 也使用 List and Watch 模式从 API Server 获取未调度的 Pod 列表,并监听新创建的 Pod。 **
当有新的未调度的 Pod 被创建时,API Server 会将事件发送给 Scheduler。
Scheduler 收到事件后,会根据调度策略为 Pod 选择一个合适的节点,并更新 Pod 的**spec.nodeName**
字段,完成调度。
为了确保数据传输的安全性,Kubernetes 使用 TLS/SSL 加密 HTTP 通信,默认情况下,API Server 使用 HTTPS。
API Server 的证书和密钥可以通过配置文件指定,通常保存在 /etc/kubernetes/pki
目录下。
:::info
调度的关键点
- API Server 作为中心:所有组件的通信都通过 API Server 进行,API Server 是 Kubernetes 的中心枢纽。
- HTTP/HTTPS 协议:所有通信都使用 HTTP/HTTPS 协议,确保安全和标准化的通信。
- Watch 机制:Kubelet 和 Scheduler 通过 Watch 机制监听 Pod 的变化,实现实时更新
:::
容器探针
容器探针(Container Probes)是一种机制,由 kubelet 对容器执行的定期诊断,从而获取容器的状态。
分布式系统和微服务体系结构的挑战之一是自动检测不正常的应用程序(在云原生架构下就是pod),并将请求(request)重新路由到其他可用系统,恢复损坏的组件。健康检查是应对该挑战的一种可靠方法。使用 Kubernetes,可以通过探针配置运行状况检查,以确定每个 Pod 的状态。
kubernetes中的探针类型总共有三种, 分别是liveness probe、readiness probe和startup 探针。每类探针都支持三种探测方法 。 下面我们来一一学习
三种探针
- liveness Probe(存活探针): 作用的是单个容器, 如果检查失败, 那么kubelet将杀死容器, 然后根据pod的restartPolicy来操作。
运行原理:
用于判断容器是否存活,即Pod是否为running状态,如果LivenessProbe探针探测到容器不健康,则kubelet将kill掉容器,并根据容器的重启策略是否重启。
如果一个容器不包含LivenessProbe探针,则Kubelet认为容器的LivenessProbe探针的返回值永远成功。
有时应用程序可能因为某些原因(后端服务故障等)导致暂时无法对外提供服务,但应用软件没有终止,导致K8S无法隔离有故障的pod,调用者可能会访问到有故障的pod,导致业务不稳定。
K8S提供livenessProbe来检测应用程序是否正常运行,并且对相应状况进行相应的补救措施。
注意,liveness探测失败并一定不会重启pod,pod是否会重启由你的restart policy 控制。
- Readiness Probe(就绪探针):用于检查容器是否以及准备好接收流量。 如果探针检测到应用程序不可用, kubernetes就会将流量路由到其他容器, 并且将不可用的容器从负载均衡池中删除
**运行原理: **
用于判断容器是否启动完成,即容器的Ready是否为True,可以接收请求,如果ReadinessProbe探测失败,则容器的Ready将为False,控制器将此Pod的Endpoint从对应的service的Endpoint列表中移除,从此不再将任何请求调度此Pod上,直到下次探测成功。
通过使用Readiness探针,Kubernetes能够等待应用程序完全启动,然后才允许服务将流量发送到新副本。
关于** Readiness 探针有一点很重要,它会在容器的整个生命周期中运行(这里实际上是错的, 因为他是在容器创建之后 ,启动探针success 之后才运行的)**。这意味着 Readiness 探针不仅会在启动时运行,而且还会在 Pod 运行期间反复运行。这是为了处理应用程序暂时不可用的情况(比如加载大量数据、等待外部连接时)。在这种情况下,我们不一定要杀死应用程序,可以等待它恢复。Readiness 探针可用于检测这种情况,并在 Pod 再次通过 Readiness 检查后,将流量发送到这些 Pod。
- Startup probe(启动探针):指示容器中的应用是否已经启动。如果提供了启动探针(startup probe),在启动探针Success之前会禁用所有其他探针,直到它成功为止。如果启动探针失败,kubelet 将杀死容器,容器服从其重启策略进行重启。如果容器没有提供启动探针,则默认状态为成功Success。
探针检查的四种检查机制
**exec**
在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。**grpc**
使用 gRPC 执行一个远程过程调用。 目标应该实现 gRPC 健康检查。 如果响应的状态是 “SERVING”,则认为诊断成功。**httpGet**
对容器的 IP 地址上指定端口和路径执行 HTTP GET
请求。如果响应的状态码大于等于 200 且小于 400,则诊断被认为是成功的。**tcpSocket**
对容器的 IP 地址上的指定端口执行 TCP 检查。如果端口打开,则诊断被认为是成功的。 如果远程系统(容器)在打开连接后立即将其关闭,这算作是健康的。
什么时候适合使用这三种探针
- 对于存活探针, 如果容器中的进程能够在遇到问题、不健康的情况下自行崩溃, 则不一定需要存活探针, 因为kubelet会自行的 根据配置的restartPolicy自动执行修复操作。如果你希望容器在探测失败时被杀死并重新启动,那么请指定一个存活态探针, 并指定
**restartPolicy**
为 “**Always**
“ 或 “**OnFailure**
“。 - **对于就绪探针, **如果想要仅在探测成功之后才开始向pod发送流量请求, 那么请指定就绪探针。 因为就绪态探针的存在意味着 Pod 将在启动阶段不接收任何数据,并且只有在探针探测成功后才开始接收数据。
如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针。 当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用。 这可以帮助你避免将流量导向只能返回错误信息的 Pod。
- 对于启动探针, 如果所包含的容器需要较长时间才能启动就绪的 Pod 而言,启动探针是有用的。 你不再需要配置一个较长的存活态探测时间间隔,只需要设置另一个独立的配置选定, 对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。
这几种探针是由谁发起的?
在 Kubernetes 中,探针(启动探针、就绪探针、存活探针)是由 Kubelet 组件发起和管理的。Kubelet 是 Kubernetes 集群中每个节点上的一个代理,负责管理节点上的 Pod 和容器的生命周期。
init 容器
每个 Pod 中可以包含多个容器, 应用运行在这些容器里面,同时 Pod 也可以有一个或多个先于应用容器启动的 Init 容器。
Init 容器与普通的容器非常像,除了如下两点:
- 它们总是运行到完成。
- 每个都必须在下一个启动之前成功完成。
如果 Pod 的 Init 容器失败,kubelet 会不断地重启该 Init 容器直到该容器成功为止。 然而,如果 Pod 对应的 restartPolicy
值为 “Never”,并且 Pod 的 Init 容器失败, 则 Kubernetes 会将整个 Pod 状态设置为失败。
Init 容器在主应用容器启动之前运行并完成其任务。 与边车容器不同, Init 容器不会持续与主容器一起运行。
边车(sidecar)容器
这里还有很多细节
边车容器是与主应用容器在同一个 Pod 中运行的辅助容器。** 这些容器通过提供额外的服务或功能(如日志记录、监控、安全性或数据同步)来增强或扩展主应用容器的功能, 而无需直接修改主应用代码。
通常,一个 Pod 中只有一个应用程序容器。 例如,如果你有一个需要本地 Web 服务器的 Web 应用程序, 则本地 Web 服务器以边车容器形式运行,而 Web 应用本身以应用容器形式运行。 当然这只是其中的一种案例 。下面让我们来大致的了解一下sidecar容器吧
Sidecar容器, 用的最多的日志记录和监控
在kubernetes集群时代 ,监控告警成了预警失败的重要举措, 但是该如何使用监控呢, 每个pod作为集群的基本单元, 按照之前的架构图可以知道, sidecar 容器与同一 pod 中的主应用程序容器一起运行,允许它们共享相同的生命周期并有效地通信。
一般一个pod里运行一个容器,那一个pod里运行两个容器的意义何在?一个容器是主容器,一个是副容器sidecar,比如nginx容器用来提供服务,另外一个容器使用工具来进行日志分析,两个容器挂载同一个数 据卷,日志分析容器读取数据卷即可分析日志。**
日志作为任一系统不可或缺的部分,在K8S 官方文档 中也介绍了多种的日志采集形式,总结起来主要有下述3种:原生方式、DaemonSet方式和Sidecar方式。 三种方式都有利有弊,没有哪种方式能够完美的解决100%问题的,所以要根据场景来贴合。其中Sidecar方式为每个POD单独部署日志agent,相对资源占用较多,但灵活性以及多租户隔离性较强,建议大型的K8S集群或作为PAAS平台为多个业务方服务的集群使用该方式。
这里提供一个案例, 使用的openKruise 社区的FileBeat, 整体架构图如下
1 | apiVersion: v1 |
推荐使用SidecarSet管理
Pod Sidecar模式:通过在Pod里定义专门容器,来执行主业务容器需要的辅助工作(比如:日志采集容器,流量代理容器)。优势:将辅助能力同业务容器解耦,实现独立发布和能力重用。但是也有一些弊端,如下:
- 业务Pod耦合(运维、代理)多种sidecar容器,增加配置的复杂性以及业务开发人员的学习成本
- Sidecar容器升级将导致业务Pod重建,由于Sidecar容器一般是独立的中间件团队负责,如果升级会存在极大的业务方阻力
上面介绍的就是直接通过pod Sidecar来模式来管理日志
SidecarSet管理sidecar容器的利器
SidecarSet是OpenKruise中针对sidecar容器管理抽象出来的概念,负责注入和升级k8s集群中的sidecar容器,是OpenKruise的核心workload之一,详细可参考:SidecarSet文档。
- 自动注入Sidecar:将sidecar容器配置与业务Workload(Deployment、CloneSet等)配置解耦,简化用户使用成本
- 独立升级Sidecar容器:不重建Pod,单独升级Sidecar容器,对业务无感
我这里只能列出好处 ,但是自己目前也没有完善具体的用法和实现效果。….
临时容器
临时容器:一种特殊的容器,该容器在现有 Pod 中临时运行,以便完成用户发起的操作,例如故障排查。 你会使用临时容器来检查服务,而不是用它来构建应用程序。
具体的用法:
- 当由于容器崩溃或容器镜像不包含调试工具而导致
kubectl exec
无用时, 临时容器对于交互式故障排查很有用。
Pod QoS 类
Kubernetes 中的 Pod Quality of Service (QoS) 类是一种用于描述 Pod 的资源分配优先级的机制
Kubernetes 提供了三种 QoS 类:
- BestEffort(尽力而为):这是最低的 QoS 类。BestEffort 类的 Pod 不保证任何资源分配。当集群资源紧张时,这些 Pod 将首先被驱逐以满足更高 QoS 类的 Pod 的需求。BestEffort 类的 Pod 仅在资源充足时才能运行。
- Burstable(突发):这是中间的 QoS 类。Burstable 类的 Pod 在资源充足时可以运行,但在资源紧张时可能会受到限制。这些 Pod 保证了一定的最小资源分配(如 CPU 请求),但在必要时可以超过这个限制。当集群资源紧张时,Kubernetes 会根据驱逐策略来决定哪些 Burstable 类的 Pod 应该被驱逐。
- Guaranteed(保证):这是最的 QoS 类。Guaranteed 类的 Pod 保证有充足的资源分配。这些 Pod 必须具有明确的 CPU 和内存请求和限制。当集群资源紧张时,Kubernetes 会优先驱逐低 QoS 类的 Pod,以确保 Guaranteed 类的 Pod 能够正常运行
可以通过kubernetes的describe来查看pod的Qos类
BestEffort( 尽力而为 )
用户命名空间
对于容器, 我们知道他是通过namespace和 cgroups实现隔离的。 但是在kubernetes中, 是按照pod来作为最小的单元划分的 。所以pod如何使用容器的
DownwardAPI
对于容器来说,在不与 Kubernetes 过度耦合的情况下,拥有关于自身的信息有时是很有用的。 Downward API 允许容器在不使用 Kubernetes 客户端或 API 服务器的情况下获得自己或集群的信息【允许将集群中 Pod 的元数据(如 Pod 名称、命名空间、节点名称等)暴露给 Pod 内的容器】
注意, 这些信息必须是容器启动之前就能确定下来的
工作负载——管理pod的抽象概念
在Kubernetes中,工作负载是对一组Pod的抽象模型,用于描述业务的运行载体。这些工作负载类型帮助用户定义和管理他们的应用程序,确保它们在容器化环境中高效运行。
工作负载是在Kubernetes上运行的应用程序,无论是由单个组件还是由多个一同工作的组件构成,都可以在一组Pod中运行。Kubernetes提供了多种内置的工作负载资源,如Deployment、StatefulSet、DaemonSet、Job和CronJob等,以简化应用程序的部署和管理。
接下来,以 Deployment 为例,我和你简单描述一下它对控制器模型的实现:
- Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
- Deployment 对象的 Replicas 字段的值就是期望状态;
- Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod(具体如何操作 Pod 对象,我会在下一篇文章详细介绍)。
可以看到,一个 Kubernetes 对象的主要编排逻辑,实际上是在第三步的“对比”阶段完成的。
这个操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。
Kubernetes 项目“面向 API 对象编程”的一个直观体现。 使用一个对象控制管理另一个对象。
如上图所示,类似 Deployment 这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。
对于更多更详细的控制器内容, 我们后面再进行讨论。