Docker容器(底层实现)——入门

近期项目涉及到 Docker 底层实现的相关知识,借此机会记录下,方便自己以后翻阅。

本文参考文章链接如下:

浅谈日常使用的 Docker 底层原理-三大底座-腾讯云开发者社区-腾讯云

Docker 原理剖析(一)隔离_docker原理剖析 geffin-CSDN博客

Docker 原理剖析(二)限制_docker 限制内存原理-CSDN博客

1. Docker 基本架构图

根据上图,可以发现 Docker 采用的是 C/S 架构,包括客户端和服务端。客户端和服务端既可以运行在一个机器上,也可通过 socket 或 RESTful API 来进行通信。

Docker 服务端:有一个守护进程(Daemon)接受来自客户端的请求,并处理这些请求(拉取镜像,运行容器)。Docker 守护进程一般在宿主主机后台运行,等待接收来自客户端的消息。

Docker 客户端:用户通过客户端输入命令实现跟 Docker 守护进程交互。

docker info

2. Docker 的三大核心原理

  1. 在容器进程启动之前重新挂载它的整个根目录“/”,用来为容器提供隔离后的执行环境文件系统(rootfs)。
  2. 通过 namespace 创建隔离,决定进程能够看到和使用哪些东西。
  3. 通过 cgroups(control groups) 技术来约束进程对资源的使用。
  • namespace 和 cgroups 都是 Linux 内核中的技术,Docker 底层原理基于这两个技术。
  • 对于 Linux 系统来说,Docker 也只是运行在上面的一个进程。
  • 对于 Docker 来说,Cgroups 是制造约束的主要手段,而 Namespace 是用来修改进程视图的主要方法。

2.1 隔离(namespace)

namespace:namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。在同一个 namespace 下的进程可以感知彼此的变化,而对外界的进程一无所知,从而达到隔离的目的。

实现手段:通过约束和修改进程的动态表现,从而为其创造出一个边界。

执行下面图片中命令,可以查看容器进程的命名空间,发现宿主机上面的两个 docker 容器进程之间的命名空间的编号是不一样的,说明这两个进程不是在一个命名空间的。一个进程的每种 namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 namespace 文件上。

 实现原理:

使用 Linux 内核的 namespace 机制。如果我们创建了多个 PID namespace,每个 namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID namespace 里的具体情况。

总结:Docker 容器实际上是在创建容器进程时,指定了这个进程所需要启用的一组 namespace 参数。这样,容器就只能看到当前 namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。因此,容器只是一个特殊的进程

docker exec 是怎么进入容器里的?

namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在(见图片中的命令)。一个进程,可以选择加入到某个进程已有的 namespace 当中,从而达到进入这个进程所在容器的目的。

namespace的分类:

  • Mount Namespace 文件系统隔离。
  • UTS Namespace 隔离主机和域名信息。
  • IPC Namespace 隔离进程间通信。
  • PID Namespace 进程隔离。
  • Network Namespace 网络资源隔离。
  • User Namespace 用户和用户组隔离。

2.2 限制(Cgroups)

隔离开后面临的问题:

无论 Docker 如何进行隔离,这些容器都运行在我们的宿主机上,它依赖的硬件资源都只是当前机器。另外,其实启动的每一个容器进程,它本身其实就是当前宿主机的进程之一,那么本质上来说,它也会和宿主机中的其他进程进行资源的竞争。

因此,我们就要针对Docker运行的容器进行资源的限制,Cgroups 就是 Linux 内核中用来为进程设置资源的一个技术。

Cgroups:

限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等。 还可以对进程进行优先级设置,审计,挂起和恢复等操作。

Linxu中为了方便用户使用cgroups,已经把其实现成了文件系统,其目录在 /sys/fs/cgroup 下,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统,这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。

  • cpu:限制进程在一段时间内能够分配到的 CPU 时间
  • blkio:为块设备设定 I/O 限制,一般用于磁盘等设备
  • cpuset:为进程分配单独的 CPU 核和对应的内存节点
  • memory:为进程设定内存使用的限制

 2.3 rootfs

Linux中:rootfs 是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。

Docker中:在 Docker 架构中,当 Docker daemon 为 Docker 容器挂载 rootfs 时,沿用的 liunx 内核启动时的方法,即将 rootfs 设为只读模式。在挂载完毕之后,利用联合挂载(union mount )技术在已有的只读 rootfs 上再挂载一个读写层。这样,可读写层处于Docker容器文件系统的最顶层,其下可能联合挂载了多个只读层,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并且隐藏只读层的老版本文件。

rootfs 组成:

rootfs 由三部分组成,由上往下分别是:可读写层,Init 层,只读层。以 Ubuntu 镜像为例。

 

  • 只读层:是容器的 rootfs 的下五层,它们的挂载方式都是只读的,可见这些层都以增量的方式分别包含了 Ubuntu 操作系统的一部分。
  • 可读写层:是容器的 rootfs 的最上面一层,在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。该层专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里,而原先的只读层里的内容则不会有任何变化。

删除只读层里的一个文件是怎么完成的?

为了实现这样的删除操作,会在可读写层创建一个 whiteout 文件,把只读层里的文件遮挡起来。比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件遮挡起来,消失了。

  • Init 层:在只读层与可读写层的中间,是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。

需要Init层的原因:

这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。

综上,docker run 命令启动某个容器时,实际上在镜像的顶部添加了一个新的可写层,而这个新的可写层,被我们称为了容器。容器启动后,其内的应用所有对容器的改动,文件的增删改操作都只会发生在容器层(可读写层)中,对容器层(可读写层)下面的所有只读镜像层没有影响。


写在最后:

写到这里,对 Docker 和 Linux 的一些技术有了新的理解,该博客只是一个浅浅开端,希望能够帮助像我一样对容器技术底层实现迷惑的人初步认识容器。我对 Linux 的知识还很匮乏,有很多知识还需要进一步学习,希望能继续和大佬们交流学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值