作为一个对容器没有过多了解的初学者,我直接学习kubernetes有些难以理解一些组件的原理和设计缘由,因此有必要先学习容器技术基础。
1 容器发展历史
在2013年,云计算已经发展一段时间了,那时候,用户租一批虚拟机,就可以像管理物理服务器一样用脚本或手工部署应用。显然,这当中会面对一个棘手的问题就是云端虚拟机和本地环境不一致,所以当时云计算厂商都在比拼谁能更好地消除两者的环境差异,提高“上云”体验。
在当时,以Cloud Foundry为代表的开源PaaS项目被寄予厚望,PaaS项目提供了一种“应用托管”的能力,核心组件是一套应用的打包和分发机制。用户执行一条命令即可将应用的可执行文件和启动脚本打进一个压缩包,并上传到云上Cloud Foundry的存储中。然后Cloud Foundry的调度器将这个任务分发到具体的虚拟机中运行。
这时候关键来了,由于需要在一个虚拟机上启动很多个来自不同用户的应用,Cloud Foundry 会调用操作系统的 Cgroups 和 Namespace 机制为每一个应用单独创建一个称作“沙盒”的隔离环境,然后在“沙盒”中启动这些应用进程。这样,就实现了把多个用户的应用互不干涉地在虚拟机里批量地、自动地运行起来的目的。这正是 PaaS 项目最核心的能力。 而这些 Cloud Foundry 用来运行应用的隔离环境,或者说“沙盒”,就是所谓的“容器”。
当时,另一个与Cloud Foundry相似的项目也发布了,它就是后来席卷容器天下的Docker。事实上,Docker和Cloud Foundry并没有太大差别,也是使用Cgroups 和 Namespace 实现“沙盒”,只有一小部分不同,可使得Docker能完胜PaaS项目的,正是这一点点不同 —— Docker镜像。
在PaaS项目中,用户虽然可以一键打包并上传压缩包,部署应用,但这个打包过程及其麻烦。很多时候本地正常运行,却需要做很多修改和配置工作才能在 PaaS 里运行起来,这个过程没有章法可循,只能不断试错,这个一键部署前的工作让用户苦不堪言。而Docker镜像,解决了这个打包困难的问题,Docker镜像中不仅包含可执行文件和脚本,它其实是由一个完整的操作系统的所有的文件和目录构成的,这个压缩包包含了应用运行所需要的所有依赖,有了这个压缩包,无论将其在哪个虚拟机中解压,都能得到和本地完全一样的开发环境,保证了本地环境和云端环境的高度一致。
Docker项目虽然解决了打包问题,但是没有解决大规模应用部署、集群管理的问题。随后,Docker公司发布了Swarm 项目,以一个完整的整体来对外提供集群管理功能。相对于当时市场上已存在的许多Docker容器集群管理的开源项目,Swarm的亮点是它完全使用 Docker 项目原本的容器管理 API 来完成集群管理。
Google也灵敏地发现了容器市场的机会,2014年,Google基于其内部使用的Borg系统,开源了一个容器编排平台 —— kubernetes,直接与Swarm竞争。kubernetes是 Google 公司在容器化基础设施领域多年来实践经验的沉淀与升华,有很多超前的设计,大规模容器编排领域有足够大的竞争优势。并且,Kubernetes 项目都为开发者暴露出了可以扩展的插件机制,鼓励用户通过代码的方式介入 Kubernetes 项目的每一个阶段。很快在整个容器社区中催生出了大量的、基于 Kubernetes API 和扩展接口的二次创新工作。不久后,kubernetes在GitHub上的热度远远超过Swarm。逐渐使得Docker公司不得不放弃容器编排市场,到2017年,Docker公司宣布在Docker 企业版中内置 Kubernetes 项目,宣布这场了这场竞争的结果。当然,Docker已成为容器标准,Docker公司并没有消失,只是放弃容器编排以另一种方式存在。
2 容器对进程做了什么
容器是一种沙盒技术,能够像一个集装箱一样,把应用“装”起来,应用之间就有了边界,不至于相互干扰,而且集装箱方便搬来搬去。容器的重点就是如何实现这个应用间的边界,主要依靠两大技术——Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。
对 Docker 项目来说,核心原理是为待创建的用户进程:
- 启用 Linux Namespace 配置;
- 设置指定的 Cgroups 参数;
- 切换进程的根目录(Change Root)。
2.1 隔离—Namespace
在宿主机上运行一个进程时,操作系统会给它分配一个PID,而当使用Docker将一个进程运行在容器中时,Docker会让它看不到其他进程,从而误认为自己是1号进程,实际上在宿主机操作系统里,这个进程并不是1。这就是直接使用Linux的Namespace机制实现的。Linux中,当用 clone() 系统调用创建一个新进程时,可以在参数中指定 CLONE_NEWPID 参数,就可以使得创建的进程在一个新的PID Namespace里:
int pid = clone(function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
当然,不止PID Namespace,还有如Mount Namespace、Network Namespace、UTS Namespace、IPC Namespace、和 User Namespace等。容器在创建进程时指定这样一组Namespace参数,进程就只能看见当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
2.2 限制—Cgroups
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下,是一个子系统目录加上一组资源限制文件的组合。比如,可以通过设置控制组中的的cfs_period 和 cfs_quota来控制CPU使用率。
对于Docker等容器来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的tasks文件中就可以了。
2.3 容器与虚拟机的区别
与虚拟机的硬件栈虚拟化不同,容器在操作系统级别进行虚拟化,且可以直接在操作系统内核上运行多个容器。区别如下图:
虚拟机通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如 CPU、内存、I/O 设备等等。然后,它在这些虚拟的硬件上安装了一个新的操作系统,即 Guest OS,用户进程只能看到所在OS内部的文件和目录,从而起到将不同的应用进程相互隔离的作用。
而容器并没有单独的OS,容器只是通过隔离与限制,为进程虚拟出一个单独的环境,然而进程其实还是宿主机OS上的进程。容器更轻巧,不同的容器共享操作系统内核,启动速度更快,且与虚拟机启动整个操作系统相比其占用的内存非常小。
3 容器镜像
传统虚拟机的镜像大多是一个磁盘的“快照”,磁盘有多大,镜像就至少有多大,而容器镜像是一个操作系统的所有文件和目录,并不包含内核,因而小很多。
容器镜像是基于Linux 容器文件系统的方式实现的,是在容器的根目录下挂载的一个完整操作系统的文件系统,为容器进程提供隔离后执行环境,容器镜像也叫rootfs(根文件系统)。rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。同一台机器上的所有容器,都共享宿主机操作系统的内核。
Docker镜像的分层设计
如果镜像每次修改都需要重新制造一个rootfs,将带来极度的碎片化,因此Docker使用的是一种增量的修改方式,在镜像的设计中,引入了层(layer)的概念,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。
增量rootfs是基于联合文件系统(Union File System)实现的,比如下面例子:
$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
$
$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C
$
$ tree ./C
./C
├── a
├── b
└── x
目录A和B中分别有a、x和b、x文件,将两个目录联合挂载到目录C上时,目录C中只含有一份x文件。并且对合并后的C目录中的a、b、x文件进行的修改,在A、B中也会生效。Docker正是使用多个层的联合挂载实现增量rootfs的。
在Docker中,容器的 rootfs 由三部分组成:只读层、可读写层、Init层。只读层包含镜像最初的部分形态,不可被修改。而在容器里做了增、删、改操作时,修改产生的内容就会以增量的方式出现在可读写层中。Init层里保存的是只对当前容器有效的一些数据的修改,并不会随着可读写层提交。对容器修改之后,使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比原本多个完整的操作系统的文件系统的大小要小得多。这就是增量 rootfs 的优势。
4 常用Docker命令
# 列出镜像
$ docker images
# 搜索镜像
docker search <name>
# 拉取镜像
docker pull <IP+端口>
# 构造镜像
docker build
# 删除镜像
docker rmi <name>
# 创建并启动容器
# -d 表示容器启动时在后台运行
# -p 5000:80 表示将容器内暴露的80端口映射到宿主机指定的5000端口
docker run -d -p 5000:80 --name <container name> <image name>
# 列出容器
docker ps
# 执行容器
docker exec <ID或名字>
# 停止容器
docker stop <ID>
# 启动容器
docker start
# 暂停容器
docker pause
# 恢复容器
docker unpause
# 删除容器
docker rm
