容器与虚拟化技术实现原理对比
写在前面:
在互联网技术日益革新、产品快速迭代的今天,如何在提高资源使用率、提升效率的同时又兼顾互不干涉、安全隔离的原则,促使了虚拟化技术和容器技术的相继诞生。
虚拟化技术有KVM、VMWare等并驾齐驱,容器技术为docker独领风骚。
传统虚拟化技术与容器虚拟化技术的简要技术架构对比如下:
Part.1 虚拟化技术
虚拟化技术是指在同一台计算机上通过hypervisor虚拟出多个包括完整虚拟机系统镜像,每个虚拟机拥有独立的操作系统和硬件资源。
以KVM为例,下面初探一下虚拟化技术的实现原理。
1.1 KVM虚拟化技术
KVM虚拟化的实现,主要是通过嵌入linux内核的kvm模块与QEMU相互配合实现全虚拟化,而两者的通信主要通过一系列针对特殊设备文件/dev/kvm的IOCTL调用实现。
Kvm模块的主要功能为cpu虚拟化+内存虚拟化,在提供虚拟化功能时,它会暴露一个/dev/kvm的接口,这个接口主要是用来创建和运行vCPU(虚拟CPU)以及分配虚拟内存空间、vCPU寄存器的读写。
而QEMU则是以动态二进制转换来创建和管理各种设备,通过IOCTL调用kvm的接口将部分CPU指令交给vCPU执行,kvm也依赖qemu模拟IO设备(磁盘,网卡,显卡等),从而实现完整意义上的全虚拟化。
从下图QEMU简化的内核代码可以管中窥豹领略两者的联系。
Qemu启动代码:
可以看到,kvm提供了一个设备/dev/kvm,对kvm的控制要通过这个设备提供的io_ctl接口实现。
Kvm内核在主机上虚拟出vCPU,虚拟内存,再通过QEMU创建和管理各个虚拟I/O设备包括虚拟网卡,磁盘等,安装上OS(操作系统),实现虚拟机创建的每个虚拟机相当于独立出来的计算机。
1.2 举个例子
从技术上无法理解的童鞋可以不看上面的原理,我用通俗的语言说明一下,好比我们有一个冷藏室,我有很多种类的食物需要冷藏,但是为了空间利用,快速分类和互不串味,虚拟化技术的实现就是将冷藏室隔成各个小库房,并且把冷冻机拆开组装成多个小制冷机每个房间放一个,这样小房间温度也可控,而且味道也不会串。
Part.2 容器技术
容器技术是后于虚拟化技术出现的,如果说虚拟化技术的出现主要是为了解决资源调配和隔离的问题,那么容器技术解决的是应用开发、测试和部署等提升效率的问题。而Docker在众多容器解决方案中脱颖而出,俨然成为了容器技术的代表,现在就以Docker为例,介绍一下容器技术的实现原理。
2.1 Docker
Docker的理念为“Build,Ship and Run Any App,Anywhere”,非常美好的愿景。为了实现这个目标,Docker通过Namespace分离进程,隔离网络接口、挂载点和进程间通信,使用Croups将CPU和内存等物理资源隔离开,这样就将一个完全对宿主机“一无所知”而且拥有“独立”资源的容器构造出来了,相比虚拟化技术,实际上容器还是容器之间共享同一个系统内核。
2.1.1 Namespace
Namespace的目的为通过抽象方法使得namespace 中的进程看起来拥有它们自己的隔离的全局系统资源实例,linux内核实现了六种namespace:Mount namespaces,UTS namespaces,IPC namespaces,PID namespaces,Network namespaces,User namespaces,分别的功能为:隔离文件系统、定义hostname和domainame、特定的进程间通信资源、独立进程ID结构、独立网络设备、用户和组ID空间。
Docker在创建一个容器的时候,会创建以上六种Namespace实例,然后将隔离的系统资源放入到相应的Namespace中,使得每个容器只能看到自己独立的系统资源。
以PID namespaces为例,Docker是怎么使容器拥有独立的PID空间的:
Linux内核中通过pid_namespace隔离PID,首先来看下pid_namespace的简要数据结构:
struct pid_namespace {
struct kref kref;
//引用计数
struct pidmap pidmap[PIDMAP_ENTRIES];
//pid分配的bitmap,如果位为1,表示这个pid已经分配了
int last_pid;
//记录上次分配的pid,理论上,当前分配的pid=last_pid+1
struct task_struct *child_reaper;
//表示进程结束后,需要这个child_reaper进程对这个进程进行托管
struct kmem_cache *pid_cachep;
unsigned int level;
//记录这个pid namespace的深度
struct pid_namespace *parent;
//记录父pid namespace
struct fs_pin *bacct;
#endif
};
其中数组pidmap记录了PID的分配情况,每一位代表了对应偏移量的PID是否分配,保证了PID不重复。
每一个进程都会生成一个task_struct,task_struct的简单数据结构如下:
struct task_struct
{
.............
pid_t pid;
struct pid_link pids[PIDTYPE_MAX];
.............
}
其中pid的简单数据结构如下:
struct pid
{
unsigned int level;
//这个pid所在的层级
struct hlist_head tasks[PIDTYPE_MAX];
//一个hash表,又三个表头,分别是pid表头,进程组id表头,会话id表头,用于和task_struct进行关联
struct upid numbers[1];
//这个pid对应的命名空间,一个pid不仅要包含当前的pid,还有包含父命名空间,默认大小为1,所以就处于根命名空间中
};
可以看出来,PID namespace主要通过以上三种数据结构的关联,将容器内部也就是独立的namespace中的uPID与宿主机上的PID建立起查找关系。
具体的做法为,task_struct结构体中的pid_link成员的node字段就被邻接到pid中的upid。upid通过pid_hash和pid数值关联了起来,这样就可以通过pid数值快速的找到所有命名空间的upid结构,numbers是一个struct pid的最后一个成员,利用可变数组来表示这个pid结构当前有多少个命名空间.这样Docker就实现了容器进程间PID的隔离。
其它系统资源的实现方式虽然与PID隔离有所差异,但是总体来说大同小异,都是通过linux内核的namespace实现资源隔离。
2.1.2Cgroups
前面介绍了Docker如何将系统资源进行隔离,下面简单介绍一下Docker如何利用Croups控制各个容器使用系系统资源。
Croups也是linux内核中提供一种机制,它的功能主要是限制、记录、隔离进程所使用的物理资源,比如:CPU、mermory、IO、network等,下面我们就看看它是如何做到的吧。
简单来说,Cgroups在接收到调用时,会给指定的进程挂上钩子,这个钩子会在资源被使用的时候触发,触发时会根据资源的类别(CPU,mermory,io等)使用对应的方法进行限制。
Croups中有一个术语叫做subsystem(子系统),也就是一个资源调度控制器,CPU subsystem负责CPU的时间分配,mermory subsystem负责mermory的使用量等。Cgroups的资源控制单位为组称之为cgroup,每个cgroup都包含一个或者多个subsystem。当一个任务加入了某个cgroup,cgroup对应的subsystem就开始工作,像上文提到的钩子就会触发subsystem进行资源的限制。
Docker 启动一个容器后,会在/sys/fs/cgroup目录下生成带有此容器ID的文件夹,里面就是调用Croups的配置文件,从而实现通过cgroups限制容器的资源使用率。
2.2 举个例子
结合最开始虚拟化的例子,我们有一个冷藏室,容器化技术就好比将冷藏室隔成各个小房间(namespace),然后用导管和阀门(cgroups)将冷气输送到各个房间。相比与之前提到的“虚拟化冷藏”,这种方式占用更少的资源和扩展启动速度更快的优点。
Part.3 总结
综上,虚拟化技术为用户提供了一个完整的虚拟机:包括内核在内的一个完整的系统镜像。容器化技术为应用程序提供了隔离的运行空间:每个容器内都包含一个独享的完整用户环境空间,容器之间共享同一个系统内核。
两种技术都有各自的优点,比如虚拟化有更佳的隔离性和安全性,容器化快速扩展、灵活性和易用性。也有各自的缺点,比如虚拟化技术实施难度高、更新和升级困难、相比容器过于笨重。容器化技术也存在较差的隔离性、安全性不高(宿主机被感染,所有容器受到影响)等缺点。
虽然两者的出现希望解决相同的问题,但是目前看来,并无孰优孰劣的定论。反而将两种技术结合起来,一个容器中运行一个虚拟机或者一个虚拟机中运行多个容器。这样,既保证了强隔离性和安全性的同时,也有了快速扩展、灵活性和易用性。所以说,除了世界上最好的语言PHP,技术都是不完美的,但是不能阻挡我们追求完美的步伐,就酱。