【云原生 • Docker】用故事给老板讲Docker核心原理

1,447 阅读8分钟

白话Docker核心原理

Docker是什么?

「Docker使用Google公司推出的Go语言进行开发实现,基于操作系统内核中Cgroup(资源控制)、Namespace(资源隔离)与OverlayFS(数据存储)等技术,实现了基于操作系统层面的虚拟化技术。」

理解的早就理解了这句话核心本质,不理解的给他这么一解释还是云里雾里。那我们先不急于搞懂Docker是什么,说到Docker容器,就不得不说下虚拟机(Virtual Machine),Docker容器和虚拟机又有什么区别呢?

Docker vs 虚拟机

虚拟机对于我们开发者是个再熟悉不过的概念,比如我们经常使用VMware Workstation搭建虚拟操作系统部署应用,使用JVM虚拟机运行Java应用等,如下图,「通常使用虚拟机管理器作为中间转换层,可以屏蔽底层操作系统或硬件设备差异」,比如上层虚拟机操作系统(Guest OS)执行程序或Java程序运行等,「这个中间件转换层就像翻译家一样,将上层执行的指令解释翻译成下层操作系统对应的指令进行执行」

image-20230315213250225

正如Java世界中吹嘘的"一次编译,到处运行",「虚拟机本质上通过中间件转换层屏蔽了底层差异,模拟出一个新环境,实现与平台无关,达到与外界隔离的目的,这就是虚拟机实现虚拟化的核心思想」

从虚拟机架构实现上可以看出,其存在一个很大问题:所有的指令都必须经过虚拟机管理器这个中间转换层翻译解释才能在真实操作系统上运行,这就意味着虚拟机会存在性能损耗。另外,为了模拟一个Linux环境上运行的应用,需要使用VMware运行部署一个宿主机(Guest OS),再在宿主机上运行应用,宿主机本身占用好几个G的存储空间、400-500MB+内存空间,现在微服务架构动不动就是10+、100+个应用组件需要部署,那这些组件都需要做隔离部署使用虚拟机方式无疑是致命的。

上述说的虚拟机存在性能问题和资源浪费造成了虚拟机对细粒度的环境隔离有点力不从心,而这又与当前流行的微服务架构场景下,系统被拆分成几十、上百个微服务应用组件需要独立部署存在冲突。Docker推崇的是一种轻量级容器的结构,即一个应用一个容器。所以,Docker一出来就被推向巅峰,那它又是如何搞定虚拟机隔离存在的问题的呢?

Docker容器核心技术

Docker容器中进程是直接运行在底层操作系统上,没有中间转换层,所以也就不存在性能损耗的问题。关键那它是如何做到隔离的呢?

image-20230315221413265

「这里就引出了支撑Docker容器的两大内核技术:Namespace和Cgroups(Control Groups)」。Namespace主要是用来进行**「资源隔离」,对于那些计算型资源,比如CPU、内存、磁盘IO等不能进行隔离的资源,这时就需要采用Cgroups进行「资源限制」**,防止有些资源消耗较大的容器,将整个物理机器的硬件资源(CPU, Memory、磁盘IO等) 占满,进而影响其它进程性能。

NamespaceCgroups这两个技术都是Linux内核本身支持的功能,Docker如果只使用这两大技术也不可能造就出道即巅峰的火热程度,Docker创新点恰恰是引入镜像概念,并使用联合文件系统(UnionFS)技术很好的实现了镜像分层,这样就可以将应用部署介质、依赖环境配置文件以及操作系统二进制文件进行分层叠加构建出应用运行时文件系统环境。

img

镜像包含一个基础镜像(Base Image),这个一般包含操作系统介质,比如centosdebian,但是它只包括使用的操作系统二进制文件,并没有包括内核相关,所以,它的体积远远小于部署整个操作系统占用的空间,比如一个centos基础镜像大概只有70-80MB。另外,镜像分层设计进一步减少存储占用,比如现在100+应用组件都是基于centos基础镜像部署,实际部署时只需要拉取一份centos基础镜像,就像搭积木一样,将每一层使用的文件进行组合叠加,最终构建出程序运行时完整的目录结构。

白话核心技术关系

Docker容器技术火热的背后,其实是NamespaceCgroupsUnionFS三大技术创新的结合,造就出了Docker这种现象级产品」。下面用个比较形象的比喻来帮助你理解三大技术关系:

1、正常程序启动时直接运行在操作系统上,使用Docker启动程序时,也是直接运行在操作系统上,但是Docker引擎在启动程序时会给程序套一个立方体壳(见下图);

image-20230315235537887

2、这个立方体壳前后左右四个面使用Namespace资源隔离技术打造,这样就给Docker容器中进程和其它进程隔离开来,给容器中进程造成一种运行在一个独立环境中的假象(见下图);

3、这个立方体壳的上面这个面使用Cgroups资源限制技术打造,避免程序壮大生长出来抢占其它进程的资源,进而影响其它进程性能,这样就给盖盖上加上了一个紧箍咒,再牛逼的程序也会把你死死的限制住(见下图);

4、最后再来看下这个立方体壳剩下的最下面这个面,其采用UnionFS技术打造,构建出容器中进程运行时文件系统根基。将操作系统二进制指令、依赖配置文件、程序介质等通过镜像分层叠加构建出程序运行时看到的整个文件系统环境;比如宿主机是Debian系统,但是基础镜像是CentOS环境,容器中进程看到的是CentOS系统,而不是Debian系统,同时将yum install安装的依赖介质也通过镜像打包进来,容器中进程就不需要关注宿主机上到底有没有安装该依赖介质等等,这样容器中进程看到是一个拥有程序运行时完整介质,并与宿主机操作系统隔离开的独立操作系统(见下图);

5、所以,程序运行在三大核心技术创造的立方体壳壳中,被蒙蔽双眼傻乎乎的以为运行在一个独立计算机环境中,看不到外界程序运行情况,也影响不到外界程序的运行。

image-20230315234911260

如何查看Docker进程在宿主机上的PID?

Docker容器中的进程是直接运行在宿主机上,可以通过docker inspect container查看到Docker容器中进程在宿主机上对应的PID信息(见下图):

image-20230316002649476

宿主机上ps -ef查看下容器进程信息:

image-20230316002837850

因为,这里运行的是一个nginx容器,所以宿主机上看到对应的是nginx主进程,同时该进程创建了两个nginx worker子进程。

Docker容器缺陷

「高性能、轻便是容器相较于虚拟机最大的优势,容器本质上是一种特殊的进程。」

不过,有利就有弊,基于Namespace的资源隔离和Cgroups的资源限制都不是那么彻底,因为容器之间底层还是共享使用宿主机的Linux内核,尽管你可以在容器里使用不同版本的操作系统文件,比如CentOS或者Ubuntu,但这并不能改变共享宿主机内核的事实。这意味着,如果你要在Windows宿主机上运行Linux容器,或者在低版本的Linux宿主机上运行高版本的Linux容器,都是行不通的。

其次,在Linux内核中,有很多资源和对象是不能被Namespace化的,最典型的例子就是:时间。这就意味着,如果你的容器中的程序修改了时间,整个宿主机的时间都会被随之修改,这显然不符合用户的预期。

另外,跟Namespace的情况类似,Cgroups对资源的限制能力也有很多不完善的地方,这里最常见的是/proc 文件系统的问题。Linux下的/proc目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如CPU使用情况、内存占用率等,这些文件也是top指令查看系统信息的主要数据来源。但是,你如果在容器里执行top指令,就会发现,它显示的信息居然是宿主机的CPU和内存数据,而不是当前容器的数据。造成这个问题的原因就是,Docker引擎在启动进程时直接将宿主机/proc下很多文件挂载到Docker容器上。