在正式学习pod这个概念之前, 我想先和读者共同学习一下容器、镜像、pod这几个我们在云原生环境中经常听到的名词的概述, 以及他们三者之间究竟有者怎么样的关联关系, 使得我们在云原生中常常用到。
定义:容器镜像是一个只读的模板,包含了运行应用程序所需的所有代码、运行时库、环境变量和配置文件等。它是一个特殊的文件系统,用于提供容器运行时所需的程序、库、资源、配置等文件,并包含了一些为运行时准备的一些配置参数 作用: 在制作镜像时 , 常常用到的就是Docker技术 。制作成的镜像使得应用程序及其依赖项可以在不同的环境中进行部署和运行, 无需担心环境问题而导致的问题。 它是创建容器的起点,通过在镜像上添加一个可写层,容器可以在镜像的基础上进行变化,而不会影响到原始镜像 , 其实对于相关的配置文件在现网中不是打包到镜像中的,而是通过环境变量的方式读取的, 这就是在可写层执行的一个实例。
定义: 容器是Docker的核心概念之一,是一个独立运行的应用程序及其所有运行时依赖项的轻量级、可执行单元。它与镜像几乎一模一样,区别在于容器的最上面那一层是可读可写的。 作用: 容器利用操作系统内核的功能(命名空间和控制组cgroup技术实现的)来隔离进程,并控制进程可以访问的CPU、内存和磁盘的数量。它们小巧轻便、速度快且可移植,可以在桌面、传统IT还是云端运行。
命名空间隔离了每个容器的进程、网络、用户和挂载点,确保容器之间相互隔离。而cgroup则负责限制容器可以使用的资源,如CPU、内存和存储等。
与传统虚拟机相比,Docker容器具有更轻量级和快速启动的特点。传统虚拟机是在宿主机上运行一个完整的操作系统,而容器则共享宿主机的操作系统内核。这使得容器的资源消耗更低,启动时间更快。同时,由于容器共享操作系统,容器的隔离性相对虚拟机略低,但仍然足以满足大多数应用场景的需求。
这是他们在应用架构上的对比
为什么我要讲pod和容器、镜像拿出来共同对比呢。 随着容器数量的增加, 手动管理容器变的越来越困难。 这是就引入了编排工具kubernetes和DockerSwarm等。 但是DockerSwarm并没有改变自身, 而是在Docker的基础上做改进, 同时在支持广度上也只支持Docker镜像, 所以导致逐渐被淘汰。 但是kubernetes则不同, 他能够兼容各种镜像, 同时还能够自动化容器的部署、伸缩和管理, 使得容器集群的管理变的更加高效。 在这基础上, pod应运而生, 他将一组或者一个容器一个单独的单位。 要么全都调度成功, 要么全部调度失败。 同时对于像Java和Tomcat这种相互依赖的东西, pod的调度更加有优势。 对于相同作用的应用服务,给予其“同生共死”的权限。 但是, pod又是如何管理容器的呢 ? 如何将其作为一个整体来管理的? 这些都是我们的疑惑。 下面简要说说。 具体深入学习后面我再整理输出。
为什么要说CRI ? 他又是什么 ?
为什么从 CRI
讲起,因为 k8s 集群使用 kubelet
服务通过CRI
接口和对应的 runtime(运行时)
交互,从而控制管理容器。
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定义了一套容器规范,包括容器的镜像格式、运行时规范等。
这个基础组件使 Kubernetes 能够有效运行容器。 它负责管理 Kubernetes 环境中容器的生命周期管理,包括创建、启动、停止和删除容器等操作。 你可以允许集群为一个 Pod 选择其默认的容器运行时。如果你需要在集群中使用多个容器运行时, 你可以为一个 Pod 指定 RuntimeClass, 以确保 Kubernetes 会使用特定的容器运行时来运行这些容器。
容器运行时可以有多种实现,例如Docker、containerd、CRI-O等。这些容器运行时实现了CRI接口,使得Kubernetes可以与它们兼容。
:::info
前面这些基础知识是我们学习kubernetes基本必须要掌握的内容。只有掌握了整体的大致架构, 才能在每个知识点的学习中对应上, 这样才能构建出自己的知识树。 回归正题, 通过前面的学习我们都知道pod是容器编排的最基本单位。 所以,想要学好kubernetes, pod的各种细节是一定要烂熟于心的。 下面我们就来一点点学习
Pod 在其生命周期中只会被调度一次。 将 Pod 分配到特定节点的过程称为绑定,而选择使用哪个节点的过程称为调度。 一旦 Pod 被调度并绑定到某个节点,Kubernetes 会尝试在该节点上运行 Pod。 Pod 会在该节点上运行,直到 Pod 停止或者被终止; 如果 Kubernetes 无法在选定的节点上启动 Pod(例如,如果节点在 Pod 启动前崩溃), 那么特定的 Pod 将永远不会启动。
在上一章的学习中, 我将pod创建的大致细节介绍了下, 同时,如果读者细心的话, 还会发现这两张图。
这是通过原生的pod的形式创建的两张图 ,通过这两张图的STATUS
可以看到,刚开是创建的时候STATUS的值为ContainerCreating
然后创建成功之后 STATUS的值为Running
, 这是我们能够通过命令查看到的。 具体的细节 ,下面我来详细解释下。
:::info
以创建nginx的pod为例
kubectl apply -f nginx.yaml
向Kubernetes API Server 发起一个创建pod的请求。 :::
:::info
kubectl delete pod <pod-name>
然后通过API Server处理删除请求**Terminating**
,并在元数据Metadata中添加 ****deletionTimestamp**
**。 **Terminating
并且开始执行预定义的删除流程 。对于有状态服务(绑定了持久卷声明PersistentVolumeClaim(PVC) ), 控制器管理器会更新PVC 。**deletionTimestamp**
就会通知容器优雅的关闭。 容器有 30 秒的宽限期(grace period)来完成正在进行的工作并优雅退出。 上述的总结,我个人人为非常详细了, 通过对每一步的详细分析, 我们大致可以知道一个pod从创建到启动的全过程, 以及从运行中到删除完成。
然后具体的每个容器之间是通过**API Server进行通信的, 请求是 基于 HTTP/HTTPS 协议的 RESTful API ** 其他的比如说Scheduler和kubelet监听pod的状态也是通过http请求
kubelet监听pod的机制是通过Watch机制。 Watch 是 Kubernetes API 的一种特性,它允许客户端(如 Kubelet)持续接收对象的更改通知。 ** Kubelet 会发起一个 Watch 请求,API Server 会保持这个连接,并在有相关对象的更改时立即返回更改内容。 Watch 请求是通过 HTTP/HTTPS 协议进行的,通常使用长连接**(长时间保持连接 **API Server 返回的响应是一个持续的 JSON 流,每当有对象变化(如创建、更新或删除)时,都会在流中发送一个事件通知。 **
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
容器探针(Container Probes)是一种机制,由 kubelet 对容器执行的定期诊断,从而获取容器的状态。 分布式系统和微服务体系结构的挑战之一是自动检测不正常的应用程序(在云原生架构下就是pod),并将请求(request)重新路由到其他可用系统,恢复损坏的组件。健康检查是应对该挑战的一种可靠方法。使用 Kubernetes,可以通过探针配置运行状况检查,以确定每个 Pod 的状态。 kubernetes中的探针类型总共有三种, 分别是liveness probe、readiness probe和startup 探针。每类探针都支持三种探测方法 。 下面我们来一一学习
运行原理:
用于判断容器是否存活,即Pod是否为running状态,如果LivenessProbe探针探测到容器不健康,则kubelet将kill掉容器,并根据容器的重启策略是否重启。 如果一个容器不包含LivenessProbe探针,则Kubelet认为容器的LivenessProbe探针的返回值永远成功。 有时应用程序可能因为某些原因(后端服务故障等)导致暂时无法对外提供服务,但应用软件没有终止,导致K8S无法隔离有故障的pod,调用者可能会访问到有故障的pod,导致业务不稳定。 K8S提供livenessProbe来检测应用程序是否正常运行,并且对相应状况进行相应的补救措施。 注意,liveness探测失败并一定不会重启pod,pod是否会重启由你的restart policy 控制。
**运行原理: **
用于判断容器是否启动完成,即容器的Ready是否为True,可以接收请求,如果ReadinessProbe探测失败,则容器的Ready将为False,控制器将此Pod的Endpoint从对应的service的Endpoint列表中移除,从此不再将任何请求调度此Pod上,直到下次探测成功。 通过使用Readiness探针,Kubernetes能够等待应用程序完全启动,然后才允许服务将流量发送到新副本。
关于** Readiness 探针有一点很重要,它会在容器的整个生命周期中运行(这里实际上是错的, 因为他是在容器创建之后 ,启动探针success 之后才运行的)**。这意味着 Readiness 探针不仅会在启动时运行,而且还会在 Pod 运行期间反复运行。这是为了处理应用程序暂时不可用的情况(比如加载大量数据、等待外部连接时)。在这种情况下,我们不一定要杀死应用程序,可以等待它恢复。Readiness 探针可用于检测这种情况,并在 Pod 再次通过 Readiness 检查后,将流量发送到这些 Pod。
**exec**
在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。
**grpc**
使用 gRPC 执行一个远程过程调用。 目标应该实现 gRPC 健康检查。 如果响应的状态是 “SERVING”,则认为诊断成功。
**httpGet**
对容器的 IP 地址上指定端口和路径执行 HTTP GET
请求。如果响应的状态码大于等于 200 且小于 400,则诊断被认为是成功的。
**tcpSocket**
对容器的 IP 地址上的指定端口执行 TCP 检查。如果端口打开,则诊断被认为是成功的。 如果远程系统(容器)在打开连接后立即将其关闭,这算作是健康的。
**restartPolicy**
为 “**Always**
“ 或 “**OnFailure**
“。如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针。 当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用。 这可以帮助你避免将流量导向只能返回错误信息的 Pod。
在 Kubernetes 中,探针(启动探针、就绪探针、存活探针)是由 Kubelet 组件发起和管理的。Kubelet 是 Kubernetes 集群中每个节点上的一个代理,负责管理节点上的 Pod 和容器的生命周期。
每个 Pod 中可以包含多个容器, 应用运行在这些容器里面,同时 Pod 也可以有一个或多个先于应用容器启动的 Init 容器。 Init 容器与普通的容器非常像,除了如下两点:
如果 Pod 的 Init 容器失败,kubelet 会不断地重启该 Init 容器直到该容器成功为止。 然而,如果 Pod 对应的 restartPolicy
值为 “Never”,并且 Pod 的 Init 容器失败, 则 Kubernetes 会将整个 Pod 状态设置为失败。
Init 容器在主应用容器启动之前运行并完成其任务。 与边车容器不同, Init 容器不会持续与主容器一起运行。
这里还有很多细节
边车容器是与主应用容器在同一个 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, 整体架构图如下
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- mountPath: /var/log/nginx
name: log
- name: filebeat
image: docker.elastic.co/beats/filebeat:7.16.2
volumeMounts:
- mountPath: /var/log/nginx
name: log
volumes:
- name: log
emptyDir: {}
Pod Sidecar模式:通过在Pod里定义专门容器,来执行主业务容器需要的辅助工作(比如:日志采集容器,流量代理容器)。优势:将辅助能力同业务容器解耦,实现独立发布和能力重用。但是也有一些弊端,如下:
上面介绍的就是直接通过pod Sidecar来模式来管理日志
SidecarSet是OpenKruise中针对sidecar容器管理抽象出来的概念,负责注入和升级k8s集群中的sidecar容器,是OpenKruise的核心workload之一,详细可参考:SidecarSet文档。
我这里只能列出好处 ,但是自己目前也没有完善具体的用法和实现效果。….
临时容器:一种特殊的容器,该容器在现有 Pod 中临时运行,以便完成用户发起的操作,例如故障排查。 你会使用临时容器来检查服务,而不是用它来构建应用程序。 具体的用法:
kubectl exec
无用时, 临时容器对于交互式故障排查很有用。Kubernetes 中的 Pod Quality of Service (QoS) 类是一种用于描述 Pod 的资源分配优先级的机制
Kubernetes 提供了三种 QoS 类:
可以通过kubernetes的describe来查看pod的Qos类
BestEffort( 尽力而为 )
对于容器, 我们知道他是通过namespace和 cgroups实现隔离的。 但是在kubernetes中, 是按照pod来作为最小的单元划分的 。所以pod如何使用容器的
对于容器来说,在不与 Kubernetes 过度耦合的情况下,拥有关于自身的信息有时是很有用的。 Downward API 允许容器在不使用 Kubernetes 客户端或 API 服务器的情况下获得自己或集群的信息【允许将集群中 Pod 的元数据(如 Pod 名称、命名空间、节点名称等)暴露给 Pod 内的容器】
注意, 这些信息必须是容器启动之前就能确定下来的
在Kubernetes中,工作负载是对一组Pod的抽象模型,用于描述业务的运行载体。这些工作负载类型帮助用户定义和管理他们的应用程序,确保它们在容器化环境中高效运行。 工作负载是在Kubernetes上运行的应用程序,无论是由单个组件还是由多个一同工作的组件构成,都可以在一组Pod中运行。Kubernetes提供了多种内置的工作负载资源,如Deployment、StatefulSet、DaemonSet、Job和CronJob等,以简化应用程序的部署和管理。
接下来,以 Deployment 为例,我和你简单描述一下它对控制器模型的实现:
可以看到,一个 Kubernetes 对象的主要编排逻辑,实际上是在第三步的“对比”阶段完成的。 这个操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。 Kubernetes 项目“面向 API 对象编程”的一个直观体现。 使用一个对象控制管理另一个对象。
如上图所示,类似 Deployment 这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。
对于更多更详细的控制器内容, 我们后面再进行讨论。