前言:为什么选择容器
从物理机到云平台虚拟机PaaS,再到云容器平台CaaS,容器技术是如何一步一步变得流行的?容器技术带来了哪些便利?为什么 Docker 一炮而红,长盛不衰?
在 docker 之前,开发者们是如何解决应用打包和分发的问题的?
Compose + Swarm 容器编排和集群管理给 Docker 带来了什么
Google 的 K8S 又给容器技术带来了什么?
从 Brog 和 Omega 系统中积累的弹性伸缩和资源调度经验,是 K8s 崛起的先见之明
Borg 是 Google 内部的一个大规模集群管理系统,用于管理数以万计的应用程序实例。它负责调度、部署和监控这些实例,并提供了高可用性、弹性伸缩和资源管理等功能。Kubernetes 在设计上受到了 Borg 的启发,借鉴了其架构和一些核心概念。同样,Omega 是 Google 内部的另一个集群管理系统,它向 Borg 提供了不同的编程模型,可以更灵活地部署和管理应用程序。Kubernetes 也从 Omega 中获得了一些设计思路和特性。
容器结构有哪几层?k8s 对开放可拓展插件接入带来了哪些新的技术?
容器基础——Docker
容器本身没有价值,有价值的是容器编排
容器进程的边界:Cgroup 和 Namespace
Cgroups(Control Groups)技术是用于为Linux容器提供资源限制和约束的主要手段。它可以对CPU、内存、磁盘、网络等资源进行控制和限制,确保容器在运行时不会占用过多的系统资源。
而Namespace技术则是用来创建隔离的运行环境,即修改进程视图的主要方法。它能够为容器中的进程提供独立的命名空间,这使得每个容器都有自己独立的进程树、网络栈、文件系统视图等,从而实现了容器之间的隔离。
综合使用Cgroups和Namespace技术,Linux容器可以在共享同一个宿主机操作系统内核的情况下,实现资源约束和进程隔离,提供了一种轻量级的虚拟化方式,使得应用程序可以更高效地运行,并且更方便部署和管理。容器只是一种更特殊的进程,隔离化,虚拟化的进程。
这种由Cgroups和Namespace 技术构造起来的隔离环境,被称为容器运行时(Container Runtime)。
简而言之容器和虚拟机的最大区别在于,虚拟机本身是一个进程,但是容器不是。更直观的理解,虚拟机为进程构造了一个楚门的世界,而容器只是对进程发动了无限月读,但这两种方案对于身处隔离时空间的进程的感知是一样的。
敏捷和高性能是容器相比虚拟机最大的优势,但是相比之下,也会隔离得不够彻底,带来越狱问题。
所以容器是一个单线程模型,容器和应用共享生命周期
容器静态视图的边界:容器镜像rootfs
容器镜像:根文件系统(rootfs),为容器继承提供的隔离的文件系统。
docker 为用户创建进程的过程:启用 Namespace 配置,设置指定的 Cgroup 参数,切换进程的根目录(两种系统调用,chroot 和 privot_root,优先使用 privot_root)
注意一点:容器的内核态操作是直接对宿主主机操作,相当于全局变量,会直接影响到整个宿主机器上的所有容器的内核操作状态。容器镜像:根文件系统(rootfs),为容器继承提供的隔离的文件系统。
为什么容器镜像带来了容器的高度一致性?
在 Paas 年代,将应用部署在云端服务部署管理平台,往往面临整个云端环境和本机环境不一致的复杂情况,应用运行所需要的依赖都需要在云端重新部署配置一遍,这让很多开发者都很痛苦。而容器镜像就是为了解决这个问题,对于一个应用来说,真正的运行依赖环境就只有一个,那就是整个操作系统,容器直接把这整个操作系统镜像过去,就可以有效保证本地和云端的高度一致,这个镜像也就是容器的根文件系统。
docker 镜像的高复用:Layer 设计
镜像整个操作系统的代价太高,对于不同应用之间的操作细微差异导致镜像的复用性很差,所以,Docker 选择了一种更加高效的方式:Layer 设计(通过联合文件系统UFS实现)
UFS:联合挂载示例
.
|-A
| |-a
| \-Duplicate file
\-B
|-b
\-Duplicate file
//联合挂载AB
.
|-a
|-b
\-Duplicate file
Docker 的 Layer 分为三大部分,读写层(rw),init层(ro+wh),只读层(ro+wh)共同构建起一个完整的锦镜像。
-
读写层(容器层):对应每个容器镜像的个性化配置,容器景象对一些通用操作系统配置的修改都会写在这个层里面,当我们自己配置了镜像打算 commit 和 push 到 dockerHub 的时候,实际提交的就是这个层。
-
init 层:对应每个实际启动的应用实例的个性化配置,比如常见的 hostname。
-
只读层:通用可复用镜像,也就是操作系统的基础环境,大部分镜像可以直接复用这些镜像来构建基础的操作系统环境。
Q:镜像个性化配置的对操作系统环境的增删改操作是如何实现的呢?
A:增:增量添加在 rw 层;删:在 rw 层设置 whiteout 白障文件屏蔽对应的文件;改:Copy-on-Write,先复制到容器层,再进行操作。
容器如何和宿主机器的文件系统交互——Volume(绑定挂载)
在容器初始化的阶段,容器进程已经被创建,Mount Namespace 已经开启,在执行 chroot 之前,容器进程仍然可以看到宿主机上的所有文件,但是自己的挂载事件却会被 Namespace 隔离在自己的进程中。此时,执行绑定挂载的系统调用,将宿主机上的文件夹绑定挂载到容器层(读写层)的指定文件夹(可以理解成把容器层的文件夹的指针指向宿主机的文件夹,这个涉及到 LInux 内核的 dentry 和 inode 相关的访问指针和替换过程),此时容器内对指定文件夹的读写操作都会变成对宿主机的文件夹的操作。
显而易见的是,由于真实的操作都没发生在容器层,而是发生在宿主机上,所以 commit 的时候是不会吧 Volume 内的数据上传的,上传的只是一个用来当 dentry 的空文件夹。
从容器到容器云————Kubernetes(k8s)
上文提到:有价值的不是容器,而是容器编排。何为容器编排?这是一种第一容器组的组织和管理的技术方案,对于开发者来说,容器运行时并非自己的需要关心的东西,后端的开发者们只是希望自己做好的程序,能够很合适且方便地部署到正确的应用集群上,并且忽略其中的部署细节。
在复杂的应用集群项目的设计中,实际存在各种各样的关系,而作业编排和管理系统最复杂困难的问题,正是这些关系。
在过去的容器集群管理项目中,比如 Swarm、Mesos,最擅长的,是把一个容器,按照某种约定好的规则,放置在某个最佳节点上运行,也就是“调度”。
而 k8s 擅长的,是按照用户的意愿和整个系统的规则,自动化地处理各个容器之间的关系,这一部分过程是不需要我们人工参与的,也就是“编排”。
可以理解成,k8s 比过去的集群管理项目多走了一步,自己承担了容器编排的任务,而不是交给开发者。
K8S 的集群管理框架
Kubernetes 的项目架构是从其原型架构 Borg 长期以来的实践总结经验的设计,所以很多地方十分相似。
Kubernetes 整体分为两种节点,负责编排、管理、调度的控制节点 Master 节点,和负责运行容器的计算节点 Node 节点。
Master 节点由三个独立组件组装而成:负责API服务的 kube-apiserver、负责调度的 kube-schedule、负责整个集群持久化数据的 kube-apiserver。
Node 节点上面最核心的部分,是叫做 kubelet 的组件,主要负责通过 CRI(Container Runtime Interface) 和容器运行时(比如Docker)交互,这种设计下,k8s 可以完全不关心具体容器采用了那种容器运行时,只要求这个容器可以运行标准的容器镜像,就可以直接把这个容器通过 CRI 接入到 k8s 当中。
除此之外, kubelet 还承担一系列负责容器运行时的各种环境的功能:
- 通过 gRPC 协议和 Device Plugin 交互,用来管理 GPU 等宿主机物理设备的主要组件。
- 通过 CNI 调用网络插件为容器配置网络。
- 通过 CSI 调用存储插件为容器配置持久化存储。
从顶层设计看 K8S 如何对待容器云集群
Brog 带来的项目经验使得 K8S 和其他同期“容器云”项目的最大区别在于:并没有把 Docker 当作自己项目的核心,而是把 Docker 作为一个程序打包的底层实现,加入到自己的顶层系统设计中。
在容器还未诞生的很长一段时间里,任务集群对待不同服务之间的关系是非常粗糙的,毫不相干的几个服务因为偶尔之间会发生 http 通信就塞入同一台虚拟机,不同虚拟机内部的日志搜集,容灾恢复,数据备份,都需要手动维护不同的 Daemon 来实现。
容器技术的出现后,在任务集群的编排这件苦天下久矣的事情上,容器天然带有细粒度的优势,因为容器本质是一个高度隔离的单进程服务!这是一种天生自带的功能划分。
容器的出现,也正是微服务思想落地先决条件。
Pod——Kubernetes容器编排的基本单位
回到 K8S,和过去容器之间简单的 link 链接然后交给平台自动处理容器间关系的设计不同,K8S 选择使用一种更加宏观和统一的方式来定义任务之间的各种关系和将来的拓展。
K8S 设计容器之间需要频繁访问甚至需要通过本地文件交互的关系作为基础的编排对象,称为Pod,这些 Pod 在过去往往部署在同一个机器上,通过localhost进行通信。如今在 K8S 中,这些 Pod 共享同一个 Network Namespace,同一组数据卷,高效地进行数据交换。
Pod 是 K8S 中最基础的编排对象
但是 Pod 是动态变化的,一些服务和服务之间处于安全容灾的考虑是不建议部署到同一个 Pod 上的(比如 web 服务、后端服务、数据库这些肯定要分开保证容灾),Pod 和 Pod 之间的通信实现呢————答案是 K8S 用一个 Service 服务反向代理了那些提供这种服务 Pod Replicas。
Service 反向代理,也就是我们现在十分熟悉 Niginx 的做法
如果 Pod 与 Pod 之间还需要授权关系,比如数据库的Credential(数据库的用户名和密码)信息,K8S 提供了一种 Secret 对象,储存在 Etcd 中,在用户指定需要这个 Secert 的 Pod 启动时候,这个 Secret 对象会自动以 Volum 的方式挂载到容器里。
Etcd 是 Kubernetes 中使用的一种分布式键值存储系统。它被用作 Kubernetes 集群的数据存储后端,用于存储集群的各种配置数据、元数据和状态信息。
管理应用的不同运行形态
前置了解——声明式API:
声明式API是一种用于描述所需状态和配置的编程模型,特征是只要指定系统或应用的期望状态,而不需要显式编写详细的操作步骤。
通过声明式API,用户只需指定系统或应用的最终目标状态,而不需要告诉系统如何达到这个状态。在声明式API中,用户只需要定义所需的配置和规范,并将其提交给系统进行处理。系统会自动根据声明的配置,采取必要的操作来将系统实际状态与所需状态保持一致。这种方式能够简化开发和管理,因为只需关注目标状态,而不需要关注具体的操作细节。
一个典型的例子是 Kubernetes。在 Kubernetes 中,你可以使用 YAML 或 JSON 格式的配置文件来定义应用的规范,如 Pod、Service、Deployment 等。你只需描述所需的状态,如要启动多少个副本、使用哪个镜像以及暴露哪些端口等,然后将配置文件提交给 Kubernetes 控制平面。Kubernetes 控制平面负责解析这些配置文件,并根据描述的目标状态来创建、更新或删除相应资源对象,使实际状态与目标状态匹配。
–以上来自GPT
除了应用与应用的关系,另一个重要因素也是影响如何容器话这个应用的关键:
这个应用的运行形态
是 Deamon?定时任务?一次性任务?是配置中心?
所以 Kubernetes 提供了各种声明式API对象来描述这些常用的运行形态,这样就可以方便地管理自己应用在 K8S 框架中的运行形态,比如:
- Pod(Pod):是最小的可部署单元,用于封装一个或多个容器、存储资源、网络设置和容器运行时选项。
- Deployment(部署):管理 Pod 和 ReplicaSet 对象的声明式对象,用于定义应用的部署策略,如副本数、更新策略和回滚策略。
- ReplicaSet(副本集):确保指定数量的副本副本都在运行,并根据需求进行扩展或缩减,以达到所需的 Pod 副本数。
- StatefulSet(有状态副本集):与 ReplicaSet 类似,但为有状态应用程序提供稳定的网络标识和唯一的持久化存储,以维护每个副本的稳定身份。
- Service(服务):提供了一种方式来公开一组 Pod,并为其提供稳定的访问地址和负载均衡功能。
- Ingress(入口):管理从集群外部到集群内部的 HTTP 和 HTTPS 路由规则,并提供负载均衡和 SSL/TLS 终止功能。
- ConfigMap(配置映射):用于存储非敏感的配置数据,如环境变量、命令行参数等,供容器应用程序使用。
- Secret(密钥):用于存储敏感的密钥和凭证,如密码、证书等。Secret 对象会进行加密,并确保只有特定的工作负载能够访问。
- PersistentVolume(持久卷)和 PersistentVolumeClaim(持久卷请求):用于提供持久化存储,使应用程序能够在 Pod 之间共享和持久化数据。
- Namespace(命名空间):用于将集群划分为多个虚拟集群,以支持多租户和资源隔离。
写在最后
到这里,PaaS 到 Docker 再到 K8S 的技术进化历程就暂时告一段落了,写完这篇笔记,我也对容器的发展有了自己的认知,了解整个技术行业的发展历程中,让我心里最有感触的一句话仍然是:
一些技术与开发之间的天然的亲密关系,才是他们在历史舞台粉墨登场的真正原因
在互联网发展的长河里,大多数技术不需要我们实际工作去开发改进,但是了解技术背后代表的实践经验和理论的沉淀,却能给我们带来更多的启发和收获。具体去了解学习技术一个技术是怎么做的,可以先去了解这个技术的发展过程,一个方案对应一个问题,问题之间的相似性也能让我们从过去方案中窥得前人得灵感。
期待下一个新大陆的相遇,生活愉快