当前位置: 首页 > 科技观察

Docker底层原理解析

时间:2023-03-20 21:11:48 科技观察

作者:vitovzhong,腾讯TEG应用开发工程师容器的本质是一个进程,它与宿主机上的其他进程共享一个内核,但不同于直接在宿主机上执行的进程,容器进程运行在它属于自己独立的命名空间。命名空间隔离了进程之间的资源,使得进程a和b可以看到S资源,但是进程c看不到。1.演进对统一开发、测试、生产环境的渴望远早于docker的出现。先来看看docker之前出现过哪些解决方案。1.1vagrantVagarant是笔者接触到的第一个解决环境配置不一致的技术方案。由Ruby编写,2010年1月由HashCorp发布。Vagrant底层是虚拟机,首选是virtualbox。一个配置好的虚拟机被称为一个盒子。用户可以在虚拟机内部自由安装依赖库和软件服务,发布盒子。通过简单的命令,您可以拉动盒子并构建环境。//拉一个ubuntu12.04的盒子$vagrantinithashicorp/precise32//运行虚拟机$vagrantup//查看本地当前有哪些盒子$vagrantboxlist如果需要运行多个服务,也可以写vagrantfile,这个会依赖彼此的服务一起运行,很像今天的docker-compose。config.vm.define("web")do|web|web.vm.box="apache"endconfig.vm.define("db")do|db|db.vm.box="mysql"end1.2LXC(LinuxContainer)2008年,Linux2.6.24将cgroups功能合并到trunk中。LinuxContainer是Canonical基于namespace、cgroups等技术开发的项目,瞄准容器世界。目标是创建一个运行在Linux系统上,隔离性好的容器环境。当然,它首先出现在Ubuntu操作系统上。2013年,Docker在PyCon大会上正式推出。当时Docker是在Ubuntu12.04上开发实现的。它只是一个基于LXC的工具,屏蔽了LXC的使用细节(类似vagrant屏蔽了底层虚拟机),让用户创建dockerrun命令行。自己的容器环境。2、技术发展容器技术是一种操作系统层面的虚拟化技术,可以概括为利用Linux内核的cgroup、namespace等技术对进程进行封装和隔离。早在Docker之前,Linux就提供了Docker今天使用的底层技术。Docker一夜之间风靡全球,但技术的积累并不是一蹴而就的。我们提取几个关键技术节点进行介绍。2.1Chroot软件主要分为系统软件和应用软件,容器中运行的程序不是系统软件。容器中的进程本质上是在宿主机上运行,??并与宿主机上的其他进程共享一个内核。并且每个应用软件都需要运行所必需的环境,包括一些lib库依赖之类的。所以,为了避免不同应用的lib库依赖冲突,我们自然会想能不能把它们隔离开,让它们看到的库不一样。基于这个简单的想法,1979年,chroot系统调用首次问世。下面举个例子感受一下。devcloud上申请的云主机,现在我在自己的home目录下准备了一个alpine系统的rootfs,如下:在这个目录下执行:chrootrootfs//bin/bash然后打印出/etc/os-release看看什么时候“AlpineLinux”,意思是新运行的bash与devcloud主机上的rootfs隔离开来。2.1Namespace简单来说,namespace是Linux内核提供的,是一种进程间资源隔离的技术,让进程a和b可以看到S资源;进程c看不到它们。它是2002年Linux2.4.19加入内核的一个特性,到2013年Linux3.8引入用户命名空间,我们现在熟悉的容器所需要的命名空间都已经实现了。Linux提供了多种命名空间来隔离不同的资源。容器的本质是一个进程,但与直接在宿主机上执行的进程不同,容器进程运行在自己独立的命名空间中。所以一个容器可以有自己的根文件系统、自己的网络配置、自己的进程空间,甚至自己的用户ID空间。下面看一个简单的例子,让我们对什么是命名空间有一个感性的认识,在哪里可以直观的看到它。在devcloud云主机上执行:ls-l/proc/self/ns查看当前系统支持的命名空间。然后我们使用unshare命令运行一个bash,使其不使用当前pid命名空间:unshare--pid--fork--mount-procbash然后运行:ps-a查看当前pid命名空间下有哪些进程:在新的bash上执行:ls-l/proc/self/ns,发现当前bash的pid命名空间和之前的不一样。由于docker是基于内核的命名空间特性实现的,我们可以简单地进行身份验证并执行命令:dockerrun–pidhost–rm-italpinesh来运行一个简单的alpine容器,并让它与宿主机共享相同的pid命名空间。然后在容器内部执行命令ps-a,会发现进程数和devcloud机器上一样;执行命令ls-l/proc/self/ns/也会看到容器内部的pidnamespace和devcloud机器上的是一样的。2.2cgroupscgroups是一种命名空间。是为实现虚拟化而采用的一种资源管理机制。它决定了分配给容器的哪些资源是我们可以管理的,以及分配给容器的资源有多少。容器中的进程运行在一个隔离的环境中,使用时,仿佛是在一个独立于宿主机的系统下运行。这个特性使得容器封装的应用程序比直接在主机上运行更安全。例如,您可以设置内存使用上限。一旦进程组(容器)使用的内存达到限制再申请内存,就会触发OOM(outofmemory),这样就不会受到某个进程消耗过多内存的影响。其他进程的运行。我们举个例子感受一下。在devcloud机器上运行一个apline容器,只限制前2个CPU和1.5个核:dockerrun--rm-it--cpus"1.5"--cpuset-cpus0,1alpine然后再启动一个新的Terminal,看看是什么我们可以控制的系统资源:cat/proc/cgroups最左边是可以设置的资源。然后我们需要找出控制资源分配的信息放在哪个目录:mount|grepcgroup然后我们找到刚才运行的alpine镜像的cgroups配置:cat/proc/`dockerinspect--format='{{.State.pid}}'$(dockerps-ql)`/cgroup这样,把两者拼接起来就可以看到这个容器的资源配置了。我们先验证下cpu使用率是1.5核:cat/sys/fs/cgroup/cpu,cpuacct/docker/c1f68e86241f9babb84a9556dfce84ec01e447bf1b8f918520de06656fa50ab4/cpu.cfs_period_us输出100000,可以认为是/fscgroup的单位,然后/fscgroup/cpu,cpuacct/docker/c1f68e86241f9babb84a9556dfce84ec01e447bf1b8f918520de06656fa50ab4/cpu.cfs_quota_usoutputs150000,andthedivisionwiththeunitisexactly1.5coresset,andthenverifywhetherthefirsttwocoresareused:cat/sys/rofsc/docker/c1f68e86241f9babb84a9556dfce84ec01e447bf1b8f918520de06656fa50ab4/cpuset.CPU输出0-1。目前容器的资源配置是按照我们的设置来分配的,但是真的可以在CPU0-CPU1上限制使用1.5核吗?我们先看一下当前的CPU使用率:dockerstats$(dockerps-ql)因为程序没有运行在alpine中,所以CPU使用率为0。现在我们回到最开始执行docker命令的alpine终端,执行一个死循环:i=0;whiletrue;doi=i+i;done然后观察当前CPUUsage:接近1,为什么不是1.5?因为刚刚运行的死循环只能在一个核上运行,所以我们再打开一个终端,进入alpine镜像,执行死循环的指令,看到CPU占用稳定在1.5,说明资源占用确实有限。现在我们对docker容器实现进程间资源隔离的黑科技有了一定的了解。单从隔离来说,vagrant已经做到了。那么为什么docker风靡全球呢?是因为它允许用户将容器环境打包成一个镜像进行分发,并且镜像是分层增量构建的,可以大大降低用户使用的门槛。3.StorageImage是Docker部署的基本单元,包括程序文件和程序所依赖的资源环境。DockerImage使用挂载点挂载在容器内。容器可以大致理解为镜像的运行时实例,默认认为是在镜像层之上增加了一个可写层。因此,一般来说,如果您在容器中进行更改,它们将包含在这个可写层中。3.1联合文件系统(UFS)联合文件系统字面意思是“联合文件系统”。它将物理位置不同的多个文件目录组合起来,挂载到某个目录下,形成一个抽象的文件系统。如上图所示,从右边的UFS来看,lowerdir和upperdir是两个不同的目录,UFS将两者进行合并,得到合并层显示给调用者。从左边的docker来看,lowerdir就是镜像,upperdir相当于容器默认的可写层。如果在运行的容器中修改了文件,可以使用dockercommit命令将其保存为新图像。3.2Docker镜像的存储管理有了UFS的分层概念,我们很容易理解这么一个简单的Dockerfile:FROMalpineCOPYfoo/fooCOPYbar/bar在构建时的含义。但是dockerpull拉取的镜像文件存放在本机的什么位置,又是如何管理的呢?让我们在实践中验证一下。确认devcloud上docker当前使用的存储驱动(默认是overlay2):dockerinfo--format'{{.Driver}}'和镜像下载后的存储路径(默认存储在/var/lib/docker):dockerinfo--format'{{.DockerRootDir}}'目前我的docker修改了默认的存放路径,配置为/data/docker-data。下面以它为例来说明。首先查看这个目录的结构:tree-L1/data/docker-data注意image和overlay2目录。前者是存放图像信息的地方,后者是存放各层文件内容的地方。下面深入了解一下image目录结构:tree-L2/data/docker-data/image/注意imagedb目录,然后以最新的alpineimage为例,看看docker是如何管理image的。执行命令:dockerpullalpine:latest然后查看其镜像ID:dockerimagelsalpine:latest记住这个IDa24bb4013296,现在可以在imagedb目录下查看变化:tree-L2/data/docker-data/image/overlay2/imagedb/content/|grepa24bb4013296有这样一个镜像ID文件,是一个json格式的文件,里面包含了镜像的参数信息:jq./data/docker-data/image/overlay2/imagedb/content/sha256/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e接下来,让我们看看运行图像后会发生什么。运行一个alpine容器并让它休眠10分钟:dockerrun--rm-dalpinesleep600然后找到它的覆盖挂载点:dockerinspect--format='{{.GraphDriver.Data}}'$(dockerps-ql)|grepMergedDircombined第一节提到的UFS文件系统可以ls:ls/data/docker-data/overlay2/74e92699164736980c9e20475388568f482671625a177cb946c4b136e4d94a64/merged可以看到合并后呈现在alpine容器中的文件系统。先进入容器:dockerexec-it$(dockerps-ql)sh然后新开一个终端,看看容器运行后和镜像有什么变化:/中添加dockerdiff$(dockerps-ql)根目录sh的历史文件。然后我们在容器中手动添加一个hello.txt文件:echo'HelloDocker'>hello.txt这个时候我们来看看镜像上默认添加的可写层的UpperDir目录的变化:ls/data/docker-data/overlay2/74e92699164736980c9e20475388568f482671625a177cb946c4b136e4d94a64/diff这验证了overlay2驱动程序合并了图像的内容和容器的可写层以用作文件系统。多个运行的容器共享一个基础镜像,但每个容器都有独立的可写层,节省了存储空间。这个时候我们也可以回答镜像的实际内容存放在哪里:cat/data/docker-data/overlay2/74e92699164736980c9e20475388568f482671625a177cb946c4b136e4d94a64/lower查看这些层:ls/data/docker-data/overlay2/l/ZIIZFSKQUQ4UFS下层的图像内容。小结本次和大家分享了Docker使用的底层技术,包括namespace、cgroups和overlay2联合文件系统,重点介绍了隔离环境是如何在宿主机上演进和实现的。通过实际手动操作来真正感受这些概念。希望下次再给大家介绍一下docker网络的实现机制。【本文为专栏作者《腾讯技术工程》原创稿件,转载请联系原作者(微信ID:Tencent_TEG)】点此查看该作者更多好文