本文是《深入剖析k8s》学习笔记的第一篇,主要帮助深入理解容器的发展历史和底层原理,顺便了解k8s区别于Docker、Mesos的的地方及优势。
一、容器的历史介绍
k8s难以理解的原因在于从过去物理机和虚拟机为主体的开发运维环境,向以容器为核心的基础设施转变的过程,并不是一次温和的改革,而是涵盖了对网络、存储、调度、操作系统和分布式原理等各个方面的容器化改造。
Docker之所以能击败其它Paas产品的杀手锏就是Docker镜像。大家虽然都是用namespace和cgroups技术创建沙盒来实现应用隔离,但是Paas是对应用程序打包,这个包可以在本地运行不代表在其它环境也能正常运行,会受限于各种各样的环境因素。Docker直接基于本地运行时的操作系统环境打包,把应用程序、所有运行依赖、环境变量都打包了,从而保证了本地和远端环境的高度一致,实现了只要本地能跑,部署到其它任何环境下肯定也能跑的终极目标。
Docker为什么会想出Docker镜像这一创新概念,是由其目标客户的定位所决定的。Paas的用户是运维人员,这也就决定了它不会考虑把系统依赖和环境变量作为考虑的焦点;而Docker的目标客户是开发,开发人员不一定懂得操作系统、网络原理等运维技术,他们只需要简单、便捷、可靠地随处运行自己的代码,为了实现这一目标,Docker镜像便出现了。
Docker Compose和Docker Swarm,前者是容器编排,后者是集群管理,是Docker公司走向Paas平台化的两大重要工具。
Docker、Mesos、k8s三者之争中,Docker将其开源项目和商业产品紧密绑定,造就了一个极端封闭的生态,违背了为其开发者用户考虑的初衷;Mesos则是因为其所属的Apache社区的封闭性,鲜有创新;最终后来的k8s依靠其开放、可靠、强大成为了最终的赢家。
二、容器的概念介绍
容器本质上是一种特殊的进程,操作系统使用了Namespace技术,为容器的运行创造一个新的进程空间,这个进程空间包含一组限定的资源,比如文件、设备、配置等,使得容器以为这个空间就是一个全新的操作系统。容器无法看到宿主机、其它进程空间的任何资源。
和虚拟机相比,Docker Engine在使用效果上确实等同Hypervisor,它们看起来都是为了创建和管理沙盒的。但实际上,Docker Engine只是旁路式的辅助和管理,并不像Hypervisor一样对沙盒的隔离性负责,真正为容器负责隔离性的是宿主机的操作系统。
虚拟机和Docker对比图
虚拟机Hypervisor创建的每一个沙盒都有独立的Guest OS,所以隔离性是能够得到保障的,但缺点就是笨重、效率低下。Docker Engine与之相比依靠的是操作系统的namespace,因此更加敏捷和高性能,但是问题就是沙盒之间隔离地不够彻底,比如它们共享宿主机的操作系统内核、无法隔离时间这样的特殊资源。
因为容器只是一种特殊的进程,因此就存在会和宿主机其它进程竞争资源的情形,Cgroups技术就是为了限制某些进程能够使用的资源上限而存在的,是容器沙盒资源隔离的重要实现手段,通过进行一系列的配置,就能限制容器使用CPU、内存、磁盘、网络带宽的用量。
容器本身的设计就是希望容器能和应用同生命周期,而容器作为一个进程,就意味着我们在启动容器时,没法同时启动两个应用,除非能事先找到这两个应用的共同父进程作为启动进程,比如systemd、supervisord等,但是问题也随之而来,如果出现容器正常、父进程正常,但是应用挂掉的情形,就无法进行健康监控了。
容器进程在创建之后,为了能和宿主机的文件系统相互隔离而又保持各个容器的文件目录一致(各环境下的高度一致性),才有了容器镜像这个概念,其作用就是在容器的根目录(var/lib/docker/aufs/mnt/***)下挂载一个标准且完整的rootfs。rootfs只是包含一个操作系统所拥有的文件、配置和目录而已,并不包含操作系统的内核。形象地说,每一个新建容器的rootfs仅是操作系统的分身躯壳,操作系统的内核灵魂仍然只有一个,就是宿主机的操作系统内核。
以上总结下来,Namespace决定了容器内的应用能看到什么资源(四周的墙),Cgroups决定了容器内的应用使用某种资源能用多少(天空),rootfs保证了各个容器使用的文件系统都是一致的(大地),从这三个维度限制了容器内应用的运行,从而模拟出了一个任意环境下都高度一致的沙盒运行环境。
在下图中,容器进程“python app.py”运行在由 Linux Namespace 和 Cgroups 构成的隔离环境里;而它运行所需要的各种文件,比如 python,app.py,以及整个操作系统文件,则由多个联合挂载在一起的 rootfs 层提供。这些 rootfs 层的最下层,是来自 Docker 镜像的只读层。在只读层之上,是 Docker 自己添加的 Init 层,用来存放被临时修改过的 /etc/hosts 等文件。而 rootfs 的最上层是一个可读写层,它以 Copy-on-Write 的方式存放任何对只读层的修改,容器声明的 Volume 的挂载点,也出现在这一层。
容器分成示意图
Copy-on-Write简称写时复制,是一种为了满足读多写少高并发场景下的延时程序设计策略。其基本策略时当多个对象共享同一块内容时,并不给所有对象都复制该内容,而是采用延时策略,多个对象都引用到该内容上,如此对象的复制和读写都很快,能承受较高的并发,只有当某个对象需要对该内容进行修改的时候,才将内容真的复制给该对象。在Docker中的应用就体现在可读写层和只读层的关系上。
容器本身没有价值,容器编排才有意义。容器编排是指定义容器组织形式和管理规范的技术。k8s不仅仅是为了拉取镜像、运行镜像和做一些集群运维工作,这些工作使用Mesos、Docker Compose&Swarm都可以实现,k8s更重要的是它能定义和解决大规模集群中各种任务之间的复杂关系。比如Pod可以将紧密的容器结合在一起;Service可以屏蔽后端Pod的细节,固定对外暴露的访问方式;还有Job、StatefulSet、DaemonSet、HPA等API对象,都是为了实现不同场景下容器编排和集群管理的需求。
k8s组件示意图
k8s中API对象