Container & Kubernetes
【总结自张磊-深入剖析Kubernetes】
【图片看不到,可以去
文章目录
Container
容器 = cgroup + namespace + rootfs + 容器引擎
- Cgroup: 资源控制(限制namspace隔离进程的资源,但是Cgroups 对资源的限制能力也有很多不完善的地方, /proc 文件系统的问题,/proc 文件系统不了解 Cgroups 限制的存在)
- namespace: 访问隔离(进程级别的隔离,看不见其他进程,但是资源都是共享的,因此存在资源被占用的情况,也存在把所有资源都吃掉的情况)
- rootfs:文件系统隔离。镜像的本质就是一个rootfs文件
- 容器引擎:生命周期控制
**Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。**这
么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间就是 PaaS 项目赖以生存的应用“沙盒”。,“Namespace 做隔离,Cgroups 做限制,rootfs 做文件系统”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NxjfHsEj-1623207279252)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210510230243050.png)]
-
Hypervisor 的软件是虚拟机最主要的部分。它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如 CPU、内存、
I/O 设备等等。然后,它在这些虚拟的硬件上安装了一个新的操作系统,即 Guest OS。 -
Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的Namespace 参数。而不是使用docker engine代替hypervisor,但是多个容器之间使用的就还是同一个宿主机的操作系统内核,因此linux版本和不同系统的容器并不兼容。而虚拟机则是可以做到这种情况
-
在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,比如时间;此外,共享宿主机内核的事实,容器给应用暴露出来的攻击面是相当大的,应用“越狱”的难度自然也比虚拟机低得多
-
在生产环境中,这个问题必须进行修正,否则应用程序在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险。这也是在企业中,容器化应用碰到的一个常见问题,也是容器相较于虚拟机另一个不尽如人意的地方
Docker 原理
最核心的原理实际上就是为待创建的用户进程:
- 启用 Linux Namespace 配置;
- 设置指定的 Cgroups 参数;
- 切换进程的根目录(Change Root)。
docker exec 是怎么做到进入容器里的呢?
Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。也就是通过查看宿主机的 proc 文件,看到这个 25686 进程的所有 Namespace 对应的文件
ls -l /proc/25686/ns
一个进程的每种 Linux Namespace,都在它对应的/proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上,这也就意味着:一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。而这个操作所依赖的,乃是一个名叫 setns() 的 Linux 系统调用
docker volume挂载
- 允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作
具体实现:直接挂载
- 只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即/var/lib/docker/aufs/mnt/[可读写层 ID]/test)上
- 于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。
- 使用到的挂载技术,就是 Linux 的绑定挂载(Bind Mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐
藏起来且不受影响。(实验被挂载节点还能看到修改的东西,源挂载点应该就是这个名字,也就是/test里面东西不可见了,unmount后恢复) - 容器 Volume 里的信息,并不会被 docker commit 提交掉;但这个挂载点目录/test 本身,则会出现在新的镜像当中。
容器是“单进程模型”
容器的“单进程模型”,并不是指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力,这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个PID=1 进程的子进程。所以一般一个容器里面只有一个进程。
Kubenertes
pod
Pod,是 Kubernetes 项目中最小的 API 对象, 是 Kubernetes 项目的原子调度单位。
重要字段和含义
- NodeSelector:供用户将Pod和Node进行绑定的字段
- NodeName:赋值意味该Pod已被调度
为何以pod作为调度基本单位?
因为存在容器成组问题,这种问题在调度上比较难以来做(成组调度问题),资源囤积带来了不可避免的调度效率损失和死锁的可能性;而乐观调度的复杂程度,则不是常规技术团队所能驾驭的。而pod的引入,就是将容器打包在一起,可以不管这种成组调度问题了。
pod的意义–容器设计模式
-
Pod,其实是一组共享了某些资源的容器,Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个Volume。
-
在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。对于 Pod 里的容器 A 和容器 B 来说
- 它们可以直接使用 localhost 进行通信;(容器本地通信就好了,不需要配置内网ip)
- 它们看到的网络设备跟 Infra 容器看到的完全一样;(方便构建网络插件)
- 一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
- 当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享;(比如volume)
- Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bg67dpWT-1623207279254)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210514000023852.png)]
projected Volume
v1.11之后特性,存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换,是为容器提供预先定义好的数据,有四种:
- Secret,
- 把 Pod 想要访问的加密数据,存放到 Etcd 中。然后,你就可以通过在 Pod 的容器里挂载 Volume 的方式,访问到这些 Secret里保存的信息了
- Secret 对象要求这些数据必须是经过 Base64 转码的
- ConfigMap
- 保存的是不需要加密的、应用所需的配置信息
- Downward API
- 声明了要暴露 Pod 的 一些 信息给容器,让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。
- ServiceAccountToken
- 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。
容器健康检查和恢复机制
- livenessProbe
- 除了在容器中执行命令外,livenessProbe 也可以定义为发起 HTTP 或者 TCP 请求的方式
- restartPolicy:pod.spec.restartPolicy,默认值是 Always
- Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。
- 事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点(被调度了),这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理Pod,哪怕你只需要一个 Pod 副本。
- Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
- OnFailure: 只在容器 异常时才自动重启容器;
- Never: 从来不重启容器。
- 只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态。
- 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态
控制器模型
- 状态信息来源
- kubelet 通过心跳汇报的容器状态和节点状态
- 监控系统中保存的应用监控数据
- 控制器主动收集的它自己感兴趣的信息,这些都是常见的实际状态的来源。
- 控制方法:循环控制control loop
Deployment控制器
-
Pod的“水平扩展 / 收缩”(horizontal scaling out/in)
-
ReplicaSet,由副本数目的定义和一个 Pod 模板组成的。不难发现,它的定义其实是 Deployment 的一个子集,实现pod的滚动更新。
-
对于一个 Deployment 所管理的 Pod,它的 ownerReference 是ReplicaSet
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sWbns9bF-1623207279257)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210518222717150.png)]
Deployment 控制 ReplicaSet(版本),ReplicaSet 控制 Pod(副本数)。
-
ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数,–》 deployment只允许容器
restartPolicy=Always
,因为只有容器保证自己是running
状态下,ReplicaSet调整pod的个数才有意义,如果pod里面容器都是never
,运行完了不就没了,pod就死掉了(Failed )
水平拓展
kubectl scale deployment nginx-deployment --replicas=4/1
滚动更新
kubectl create -f nginx-deployment.yaml --record
kubectl rollout undo 滚动回去上一个版本
deployment对象状态查看
kubectl rollout status deployment/nginx-deployment
只生成一个ReplicaSet
Deployment更新操作会生成多个ReplicaSet,因此可以使用
kubectl rollout pause deployment/nginx-deployment
暂停deployment,因此对于deployment所有修改不会触发滚动更新。
kubectl rollout resume deploy/nginx-deployment
Deployment 修改操作都完成之后,只需要再执行该指令,就可以把这个 Deployment“恢复”回来
当然,也可以使用spec.revisionHistoryLimit
来限制历史ReplicaSet数量,如果为0
则再也不能回滚操作。
StatefulSet
为了解决Deployment认为一个应用的pod都是完全一样的, 之间没有顺序,也没有所谓运行在哪一个主机上的问题。这个问题让Deployment随意创建或者删掉一个pod。
但是,实际上很多应用,特别时分布式应用,他们之间多个实例往往存在依赖关系:主从关系和主备关系等。还有一些数据存储类应用,被杀掉后丢失实例与数据之间的对应关系。这些实例间存在不对等关系以及实例对外部数据有依赖关系的应用,被称为有状态应用
由此,StatefulSet就是为了解决这个问题。
StatefulSef设计
- 拓扑状态。应用的多个实例之间不是完全对等的关系,应用实例,必须按照某些顺序启动,并且新建的pod必需和原来的pod网络标识一样。
- 存储状态。应用的多个实例分别绑定了不同的存储数据,应用实例,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。
通过这样的状态设计,SatefulSet将真实世界里的应用状态,抽象出来,并通过某种方式来记录这些状态。
Headless Service
Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。用户通过访问Service就可以访问到具体的Pod。
访问Service方式有:
- Service的VIP形式(Virtual IP)
- Service的DNS方式:访问“my-svc.mynamespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod
- Normal Service:解析DNS得到对应的VIP,然后访问VIP就好了
- Headless Service:解析DNS得到my-svc 代理的某一个 Pod 的 IP 地址,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。
创建Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定为以下格式的 DNS 记录:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
# 改记录k8s为 Pod 分配的唯一的“可解析身份”
**Persistent Volume Claim PVC **
解决过度暴露问题,大大降低用户声明和使用持久化Volume的门槛
- 定义一个PVC,声明想要的Volume的属性;不需要任何关于 Volume 细节的字段,只有描述性的属性和定义。
- 在应用的 Pod 中,声明使用这个 PVC;只需要声明它的类型是persistentVolumeClaim,然后指定 PVC 的名字,而完全不必关心 Volume 本身的定义
- 创建这个 PVC 对象,Kubernetes 就会自动为它绑定一个符合条件的Volume(自于由运维人员维护的 PV(Persistent Volume)对象)。
StatefulSet 拓扑状态维护
StatefulSet 就是通过Headless Service维护网络拓扑结构,编号方式维护pod的顺序。
- 首先StatefulSet 按照 Pod 的“名字 + 编号”的方式将将 Pod 的拓扑状态固定下来。
- 然后为每一个Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录 Headless Service
- 这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者重新创建而失效。
- 但是,通过Headless Service解析到的 Pod 的IP 地址,并不是固定的。因此必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址
StatefulSet 存储状态维护
StatefulSet 通过PVC、PV 来管理存储状态
- 在StatefulSet中声明一个PVC(就来自于volumeClaimTemplates 这个模板字段),这个 PVC 的名字,会被分配一个与这个Pod 完全一致的编号。
- 自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。PVC 与 PV 的绑定得以实现的前提是,运维人员已经在系统里创建好了符合条件的PV(比如,我们在前面用到的 pv-volume);或者,你的 Kubernetes 集群运行在公有云上,这样 Kubernetes 就会通过 Dynamic Provisioning 的方式,自动为你创建与 PVC 匹配的PV。
- PVC,都以
<PVC 名字 >-<StatefulSet 名字 >-< 编号 >
的方式命名,并且处于 Bound 状态。
也就是说,StatefulSet通过维护一个有状态的PVC,进而找到跟这个 PVC 绑定在一起的PV。这个PVC名字是不会变的重新创建一个pod时。
总结
- StatefulSet 的控制器直接管理的是 Pod。StatefulSet 里的不同 Pod 实例,有着不同的编号,用于维护拓扑以及唯一性。
- Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录。StatefulSet 能够保证这些 Pod 名字里的编号不变,对应的DNS记录也不会变,也就确保网络拓扑的一致性(解析到的 Pod 的IP 地址,并不是固定的,但是对于用户来说这是透明的)
- StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有一个独立的 Volume。PVC和PV在pod被删除后依旧被保留下来的。
DaemonSet
在 Kubernetes 集群里,运行一个 Daemon Pod,这个pod特点有:
- 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
- 每一个节点上只有一个这样的pod实例;
- 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉;
- 跟其他编排对象不一样,DaemonSet 开始运行的时机,很多时候比整个
Kubernetes 集群出现的时机都要早 - DaemonSet 自动地给被管理的 Pod 加上一个特殊的Toleration,即能容忍那些不被调度的节点
意义
- 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络;
- 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录
- 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。
DaemonSet 其实是一个非常简单的控制器。在它的控制循环中,只需要遍历所有节点,然后根据节点上是否有被管理 Pod 的情况,来决定是否要创建或者删除一个 Pod。
版本管理
DaemonSet也存在版本号,但是和deployment控制器不一样,通过ReplicaSet来管理容器版本,DaemonSet的对象是pod,没有ReplicaSet来管理。但是k8s通过抽象对象来实现对应的功能。Kubernetes v1.7 之后添加了一个 API 对象,名叫ControllerRevision,专门用来记录某种Controller 对象的版本
万物皆对象,将DaemonSet和ReplicaSet都看抽象为ControllerRevision对象,通过这个ControllerRevision来进行管理。
特别地,如果undo一个对象,相当于进行一次 Patch
操作,也就是将新版本更新为一个旧版本,所以对应这个 旧版本
实际上是一个 新版本
,对应版本号 加一。
PS: 对于操作粒度为Pod的控制器(如StatefulSet),也是通过ControllerRevision进行版本管理的。
Job & CronJob
Job,对于离线任务的管理
-
这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。Job 对象本身,则被自动加上了这个 Label 对应的Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。避免了不同 Job 对象所管理的 Pod 发生重合
-
离线计算的 Pod 永远都不应该被重启,否则它们会再重新计算一遍
-
restartPolicy只有两种类型:
-
Never:
运行结束后pod状态变为completed,如果离线作业失败,Job Controller就会不断地尝试创建一个新 Pod,Job 对象的 spec.backoffLimit 字段
里定义了重试次数为 4, 默认值是 6Job Controller 重新创建 Pod 的间隔是呈指数增加的,即下一次重新创建 Pod的动作会分别发生在 10 s、20 s、40 s …后
-
OnFailure:
离线作业失败后,Job Controller 就不会去尝试创建新的 Pod
会不断地尝试重启 Pod 里的容器
-
-
spec.activeDeadlineSeconds 字段可以设置最长运行时间,避免jod一直不肯结束。
-
并行控制
- spec.parallelism,它定义的是一个 Job 在任意时间最多可以启动多少个 Pod 同时运行