docker与k8s深层理解(1)
namespace技术,是的docker创建的100号进程里面,认为自己这个100号进程为pid=1左图为虚拟机,右图为docker名为 Hypervisor 的软件是虚拟机最主要的部 分。它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如 CPU、内存、 I/O 设备等等。然后,它在这些虚拟的硬件上安装了一个新的操作系统,即 Guest OS。而这幅图的右边,则用...
namespace技术,是的docker创建的100号进程里面,认为自己这个100号进程为pid=1
左图为虚拟机,右图为docker
名为 Hypervisor 的软件是虚拟机最主要的部 分。它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如 CPU、内存、 I/O 设备等等。然后,它在这些虚拟的硬件上安装了一个新的操作系统,即 Guest OS。
而这幅图的右边,则用一个名为 Docker Engine 的软件替换了 Hypervisor。这也是为什么,很 多人会把 Docker 项目称为“轻量级”虚拟化技术的原因,实际上就是把虚拟机的概念套在了容 器上。
首先,既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个 宿主机的操作系统内核。
其次,在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是: 时间。
在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织 在操作系统的 /sys/fs/cgroup 路径下。
,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫 子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资 源种类下,你就可以看到该类资源具体可以被限制的方法。
这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 container 目录下,自 动生成该子系统对应的资源限制文件。
比如,向 container 组里的 cfs_quota 文件写入 20 ms(20000 us):
结合前面的介绍,你应该能明白这个操作的含义,它意味着在每 100 ms 的时间里,被该控制组 限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。 接下来,我们把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该 进程生效了:
除 CPU 子系统外,Cgroups 的每一项子系统都有其独有的资源限制能力,
比如: blkio,为 块 设 备 设 定 I/O 限 制,一般用于磁盘等设备;
cpuset,为进程分配单独的 CPU 核和对应的内存节点;
memory,为进程设定内存使用的限制。
即使开启了 Mount Namespace,容器进程看到的文件系统也跟宿主机完全一 样。
Mount Namespace 修改的,是容器进程对文件 系统“挂载点”的认知。但是,这也就意味着,只有在“挂载”这个操作发生之后,进程的视图 才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。
实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容 器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。
所以,一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等
对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进 程:
1. 启用 Linux Namespace 配置;
2. 设置指定的 Cgroups 参数;
3. 切换进程的根目录(Change Root)。
需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系 统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载 指定版本的内核镜像。
这个容器的 rootfs 由如下图所示的三部分组成
第一部分,只读层。
它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它 们的挂载方式都是只读的
第二部分,可读写层
它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生 的内容就会以增量的方式出现在这个层中。
第三部分,Init 层。
它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一 个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。
由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比 原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的 总空间,也比每个镜像的总和要小。这样就使得基于容器镜像的团队协作,要比基于动则几个 GB 的虚拟机磁盘镜像的协作要敏捷得多。
Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。
docker exec 是怎么做到进入容器里的呢?
Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信 息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。
这时,你可以通过查看宿主机的 proc 文件,看到这个 25686 进程的所有 Namespace 对应的 文件:
一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进 入”这个进程所在容器的目的,这正是 docker exec 的实现原理
Docker 还专门提供了一个参数,可以让你启动一个容器并“加入”到另一个容器的 Network Namespace 里,这个参数就是 -net
而如果我指定–net=host,就意味着这个容器不会为进程启用 Network Namespace。这就意 味着,这个容器拆除了 Network Namespace 的“隔离墙”,所以,它会和宿主机上的其他普通进程一样,直接共享宿主机的网络栈。这就为容器直接操作和使用宿主机网络提供了一个渠 道。
docker commit,实际上就是在容器运行起来后,把最上层的“可读写层”,加上原先容器镜 像的只读层,打包组成了一个新的镜像。当然,下面这些只读层在宿主机上是共享的,不会占用 额外的空间。
由于 Docker 一开始还是要创建 /test 这个目录作为挂载点,所以执行了 docker commit 之后,你会发现新产生的镜像里,会多出来一个空的 /test 目录。毕竟,新建目录操 作,又不是挂载操作,Mount Namespace 对它可起不到“障眼法”的作用。
一个“容器”,实际上是一个由 Linux Namespace、Linux Cgroups 和 rootfs 三种技术构建出来的进程的隔离环
一个正在运行的 Linux 容器,其实可以被“一分为二”地看
1. 一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分我们称为“容器镜 像”(Container Image),是容器的静态视图;
2. 一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行 时”(Container Runtime),是容器的动态视图
,Kubernetes 项目的架构
控制节点,即 Master 节点,由三个紧密协作的独立组件组合而成,它们分别是负责 API 服 务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controllermanager。整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Ectd 中。
在 Kubernetes 项目中,kubelet 主要负责同容器运行时(比如 Docker 项目)打交道。
kubelet 还通过 gRPC 协议同一个叫作 Device Plugin 的插件进行交互。用来管理 GPU 等宿主机物理设备的主要组件,也是基于 Kubernetes 项目进行机 器学习训练、高性能作业支持等工作必须关注的功能。
kubelet的另一个重要功能,则是调用网络插件和存储插件为容器配置网络和持久化存储。这两 个插件与 kubelet 进行交互的接口,分别是 CNI(Container Networking Interface)和 CSI(Container Storage Interface)。
从一开始,Kubernetes 项目就没有像同时期的各种“容器云”项目那 样,把 Docker 作为整个架构的核心,而仅仅把它作为最底层的一个容器运行时实现。
运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系。这些关系的处理,才是作 业编排和管理系统最困难的地方。
容器技术出现以后,你就不难发现,在“功能单位”的划分上,容器有着独一无二的“细粒 度”优势:毕竟容器的本质,只是一个进程而已。
只要你愿意,那些原先拥挤在同一个虚拟机里的各个应用、组件、守护进程,都可以被 分别做成镜像,然后运行在一个个专属的容器中。它们之间互不干涉,拥有各自的资源配额,可以 被调度在整个集群里的任何一台机器上。而这,正是一个 PaaS 系统最理想的工作状态,也是所 谓“微服务”思想得以落地的先决条件。
Kubernetes 项目最主要的设计思想是,从更宏观的角度,以统一的方式来定义任务之间的各 种关系,并且为将来支持更多种类的关系留有余地。
在 Kubernetes 项目中,这些容器则会被划分为一个“Pod”,Pod 里的容器共 享同一个 Network Namespace、同一组数据卷,从而达到高效率交换信息的目的。
Pod 是 Kubernetes 项目中最基础的一个对象
Web 应用又怎么找到数据库容器的 Pod 呢
Kubernetes 项目的做法是给 Pod 绑定一个 Service 服务,而 Service 服务声明的 IP 地址等 信息是“终生不变”的。这个Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从 而代替 Pod 对外暴露一个固定的网络地址。
从容器这个最基础的概念出发,首先遇到了容器间“紧密协作”关系的难 题,于是就扩展到了 Pod;有了 Pod 之后,我们希望能一次启动多个应用的实例,这样就需要 Deployment 这个 Pod 的多实例管理器;而有了这样一组相同的 Pod 后,我们又需要通过一个固 定的 IP 地址和端口以负载均衡的方式访问它,于是就有了 Service。
除了应用与应用之间的关系外,应用运行的形态是影响“如何容器化这个应用”的第二个重要因 素。
,在 Kubernetes 项目中,我们所推崇的使用方法是
首先,通过一个“编排对象”,比如 Pod、Job、CronJob 等,来描述你试图管理的应用; 然后,再为它定义一些“服务对象”,比如 Service、Secret、Horizontal Pod Autoscaler(自 动水平扩展器)等。这些对象,会负责具体的平台级功能。
Kubernetes 项目如何启动一个容器化任务呢
比如,我现在已经制作好了一个 Nginx 容器镜像,希望让平台帮我启动这个镜像。并且,我要求平 台帮我运行两个完全相同的 Nginx 副本,以负载均衡的方式共同对外提供服务。
如果是自己 DIY 的话,可能需要启动两台虚拟机,分别安装两个 Nginx,然后使用 keepalived 为这两个虚拟机做一个虚拟 IP。 而如果使用 Kubernetes 项目呢?你需要做的则是编写如下这样一个 YAML 文件
在上面这个 YAML 文件中,我们定义了一个 Deployment 对象,它的主体部分(spec.template 部分)是一个使用 Nginx 镜像的 Pod,而这个 Pod 的副本数是 2(replicas=2)。
了容器其实可以分为两个部分:容器运行时和容器 镜像。
kubelet 容器可以通过不开启 Network Namespace(即 Docker 的 host network 模式)的方式,直接共享宿主机的网络栈。可是,要让 kubelet 隔着容器的 Mount Namespace 和文件系统,操作宿主机的文件系统,就有点儿困难了。
,kubeadm 选择了一种妥协方案
把 kubelet 直接运行在宿主机上,然后使用容器部署其他的 Kubernetes 组件。
使用 kubeadm 的第一步,是在机器上手动安装 kubeadm、kubelet 和 kubectl 这三个 二进制文件。
Kubernetes 对外提供服务时,除非专门开启“不安全模式”,否则都要通过 HTTPS 才能访问 kube-apiserver。这就需要为 Kubernetes 集群配置好证书文件。
kubelet 在 Kubernetes 项目中的地位非常高,在设计上它就是一个完全独 立的组件,而其他 Master 组件,则更像是辅助性的系统容器。
在 kubeadm 中,Master 组件的 YAML 文件会被生成在 /etc/kubernetes/manifests 路径下。比 如,kube-apiserver.yaml:
推荐你在使用 kubeadm init 部署 Master 节点时,使用下面这条指令
kubeadm 能够用于生产环境吗?
到目前为止(19.5.26)不能
因为 kubeadm 目前最欠缺的是,一键部署一个高可用的 Kubernetes 集群,即:Etcd、Master 组 件都应该是多节点集群,而不是现在这样的单点。这,当然也正是 kubeadm 接下来发展的主要方 向。 另一方面,Lucas 也正在积极地把 kubeadm phases 开放给用户,即:用户可以更加自由地定制 kubeadm 的每一个部署步骤。
推荐使用kops或者 SaltStack 这样更复杂的部署工 具。
就是想要一个单节点的 Kubernetes,删除这个 Taint 才是正确的选择
很多时候我们需要用数据卷(Volume)把外面宿主机上的目 录或者文件挂载进容器的 Mount Namespace 中,从而达到容器和宿主机共享这些目录或者文件的 目的。容器里的应用,也就可以在这些数据卷中新建和写入文件。
如果你在某一台机器上启动的一个容器,显然无法看到其他机器上的容器在它们的数据卷里 写入的文件。这是容器最典型的特征之一:无状态。
而容器的持久化存储,就是用来保存容器存储状态的重要手段:存储插件会在容器里挂载一个基于 网络或者其他机制的远程数据卷,使得在容器里创建的文件,实际上是保存在远程存储服务器上, 或者以分布式的方式保存在多个节点上,而与当前宿主机没有任何绑定关系。这样,无论你在其他 哪个宿主机上启动新的容器,都可以请求挂载指定的持久化存储卷,从而访问到数据卷里保存的内 容。这就是“持久化”的含义。
Pod是一组共享了某些资源的一种容器的最小编排,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。容器(Container)就成了 Pod 属性里的一个普通的字段
把 Pod 看成传统环境里的“机器”、把容器看作是运行在这个“机器”里的“用户程 序,凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。
对于 Pod 里的容器 A 和容器 B 来说
它们可以直接使用 localhost 进行通信;
它们看到的网络设备跟 Infra 容器看到的完全一样;
一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享;
Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。
NodeName:一旦 Pod 的这个字段被赋值,Kubernetes 项目就会被认为这个 Pod 已经经过了调 度,调度的结果就是赋值的节点名字。所以,这个字段一般由调度器负责设置,但用户也可以设置 它来“骗过”调度器,当然这个做法一般是在测试或者调试的时候才会用到。
HostAliases:定义了 Pod 的 hosts 文件(比如 /etc/hosts)里的内容
凡是 Pod 中的容器要共享宿主机的 Namespace,也一定是 Pod 级别的定义
IPC(进程间通信)
Pod 生命周期的变化,主要体现在 Pod API 对象的Status 部分,这是它除了 Metadata 和 Spec 之外的第三个重要字段。其中,pod.status.phase,就是 Pod 的当前状态,它有如下几种可能的情 况:
1. Pending。这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被 创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比 如,调度不成功。
2. Running。这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创 建成功,并且至少有一个正在运行中。
3. Succeeded。这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情 况在运行一次性任务时最为常见。
4. Failed。这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状 态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。
5. Unknown。这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kubeapiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。
到目前为止,Kubernetes 支持的 Projected Volume 一共有四种:
1. Secret; 把 Pod 想要访问的加密数据, 存放到 Etcd 中。
2. ConfigMap;
3. Downward API;
4. ServiceAccountToken。
StatefulSet 对存储状态的管理机制
这个机制,主要 使用的是一个叫作 Persistent Volume Claim (PVC)的功能。
StatefulSet 的工作原理
StatefulSet 的控制器直接管理的是 Pod。这是因为,StatefulSet 里的不同 Pod 实例, 不再像 ReplicaSet 中那样都是完全一样的,而是有了细微区别的。比如,每个 Pod 的 hostname、名字等都是不同的、携带了编号的。而 StatefulSet 区分这些实例的方式,就是通 过在 Pod 的名字里加上事先约定好的编号。
其次,Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有 同样编号的 DNS 记录。只要 StatefulSet 能够保证这些 Pod 名字里的编号不变,那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变,而这条记录解 析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新。这当然是 Service 机制本身的能力,不需要 StatefulSet 操心。
最后,StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。这样,Kubernetes 就可 以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有 一个独立的 Volume。
DaemonSet 的主要作用,是让你在 Kubernetes 集群里,运行一个 Daemon Pod。 所以,这个 Pod 有如下三个特征:
1. 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
2. 每个节点上只有一个这样的 Pod 实例;
3. 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧 节点被删除后,它上面的 Pod 也相应地会被回收掉。
Daemon Pod 的意义确实是非常重要的
1. 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网 络;
2. 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存 储目录,操作容器的 Volume 目录; 3. 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志 搜集。
一切皆对象
DaemonSet 只管理 Pod 对象,然后通过 nodeAffinity 和 Toleration 这两个调度器的小功能,保证了每个节点上有且只有一个 Pod。
更多推荐
所有评论(0)