前言

简单介绍一下docker的镜像。

正文

前面讲到了容器的工作原理了(namespace 限制了时间, cgroup限制了资源),知道docker 历史的也知道,docker 之所以能够称为容器大佬,是因为其只做了容器。

也就是做到了一次打包,到处运行的这种思想得到了实现。

那么容器的镜像涉及思路是怎么样的呢?

tee test.c <<- 'EOF'
> #define _GNU_SOURCE
> #include <sys/mount.h>
> #include <sys/types.h>
> #include <sys/wait.h>
> #include <stdio.h>
> #include <sched.h>
> #include <signal.h>
> #include <unistd.h>
> #define STACK_SIZE (1024 * 1024)
> static char container_stack[STACK_SIZE];
> char* const container_args[] = {
> "/bin/bash",
> NULL
> };
>
> int container_main(void* arg)
> {
> printf("Container - inside the container!\n");
> execv(container_args[0], container_args);
> printf("Something's wrong!\n");
> return 1;
> }
>
> int main()
> {
> printf("Parent - start a container!\n");
> int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
> waitpid(container_pid, NULL, 0);
> printf("Parent - container stopped!\n");
> return 0;
> }
> EOF

写入这一段代码。

然后编译:

gcc -o test test.h

然后运行一下:

./test

这个时候容器就启动了。

这个时候就已经进入了,容器里面了。

但是你这个时候发现一个问题,那就是你所在的容器和外面一模一样。

即使开启了 Mount Namespace,容器进程看到的文件系统也跟宿主机完全一样。

那么是不是挂载点的问题呢?

还是说代码根本就没有成功。

第一,验证代码是否开启了mount namespace:

kill 之后,然后就推出了,这时候就知道了,其实我们一直是在容器里面。

Mount Namespace 修改的,是容器进程对

文件系统“挂载点”的认知。但是,这也就意味着,只有在“挂载”这个操作发生之后,进

程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。

那么更换一下挂载点:

[root@iZ8vb42623daibnzbt0j16Z ~]# tee test.c <<- 'EOF'
> #define _GNU_SOURCE
> #include <sys/mount.h>
> #include <sys/types.h>
> #include <sys/wait.h>
> #include <stdio.h>
> #include <sched.h>
> #include <signal.h>
> #include <unistd.h>
> #define STACK_SIZE (1024 * 1024)
> static char container_stack[STACK_SIZE];
> char* const container_args[] = {
> "/bin/bash",
> NULL
> };
>
> int container_main(void* arg)
> {
> printf("Container - inside the container!\n");
> // 如果你的机器的根目录的挂载类型是 shared,那必须先重新挂载根目录
> // mount("", "/", NULL, MS_PRIVATE, "");
> mount("none", "/tmp", "tmpfs", 0, "");
> execv(container_args[0], container_args);
> printf("Something's wrong!\n");
> return 1;
> }
>
> int main()
> {
> printf("Parent - start a container!\n");
> int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
> waitpid(container_pid, NULL, 0);
> printf("Parent - container stopped!\n");
> return 0;
> }
> EOF

mount("none", "/tmp", "tmpfs", 0, ""); 这一行。

那么看下效果,编译,然后运行。

现在这个进程来说,tmp 就完全看不到了。

然后看一下tmpfs 的挂载情况:

mount -l | grep tmpfs

tmpfs 这个表示内存盘的意思,就是往这个盘上写东西,其实是写入内存中。

这个时候看下宿主机的情况。

宿主机也能看到。

也就是说容器影响了宿主机,这很危险。

改下。

更重要的是,因为我们创建的新进程启用了 Mount Namespace,所以这次重新挂载的操

作,只在容器进程的 Mount Namespace 中有效。如果在宿主机上用 mount -l 来检查一

下这个挂载,你会发现它是不存在的:

这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程

视图的改变,一定是伴随着挂载操作(mount)才能生效。

可是,作为一个普通用户,我们希望的是一个更友好的情况:每当创建一个新容器时,我希

望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎

么才能做到这一点呢?

不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount

Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。

那么如果希望看到一个独立的环境呢? 那么就得重新挂载整个根目录了。

在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工

作。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到

你指定的位置。它的用法也非常简单。

上面创建一些目录。

然后把文件拷贝进去。

cp -v /bin/{bash,ls} $HOME/testnamespace/bin

拷贝依赖:

T=$HOME/testnamespace
list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
for i in $list; do cp -v "$i" "${T}${i}"; done

如果是64位,运行这个:

T=$HOME/testnamespace
list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
for i in $list; do cp -v "$i" "${T}${i}"; done

然后运行:

chroot $HOME/testnamespace /bin/bash

这个时候这个进程,运行的就是在$HOME/testnamepsace空间下。

看一下ls -al,看到的就是$HOME/testnamepsace的内容。

实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是

Linux 操作系统里的第一个 Namespace。

而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓

的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

现在,你应该可以理解,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户

进程:

  1. 启用 Linux Namespace 配置;
  2. 设置指定的 Cgroups 参数;
  3. 切换进程的根目录(Change Root)。

这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用

pivot_root 系统调用,如果系统不支持,才会使用 chroot。

另外,需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操

作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时

才会加载指定版本的内核镜像

所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。

实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。

这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行

直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对

于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

这也是容器相比于虚拟机的主要缺陷之一:毕竟后者不仅有模拟出来的硬件机器充当沙盒,

而且每个沙盒里还运行着一个完整的 Guest OS 给应用随便折腾。

不过,正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。

由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以

及它运行所需要的所有依赖,都被封装在了一起。

事实上,对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如

Golang 的 Godeps.json。但实际上,一个一直以来很容易被忽视的事实是,对一个应用来

说,操作系统本身才是它运行所需要的最完整的“依赖库”。

Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作

镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。

当然,这个想法不是凭空臆造出来的,而是用到了一种叫作联合文件系统(Union File

System)的能力。

最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。

做个实验

有A B C三个目录

A下有a x两个文件

B下有b x两个文件

用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上

mount -t aufs -o dirs=./A:./B none ./C

再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

形成这样一个覆盖的功能。

查看docker info:

使用的是overlay2

来查看一下这个东西吧。

我们来随便查看一个镜像。

docker image inspect busybox

可以看到这一层的id 是sha256:01fd6df81c8ec7dd24bbbd72342671f41813f992999a3471b9d9cbc44ad88374

然后也可也看到镜像的位置在:

/var/lib/docker/overlay2/62f55dcb71d6096a2f9a874ae51397fac69c689122aa7fcecb20bfa3ee087d5e

进入/var/lib/docker/overlay2/62f55dcb71d6096a2f9a874ae51397fac69c689122aa7fcecb20bfa3ee087d5e/diff

就可以看到完整的镜像目录。

当我们启动一个容器的时候,我们发现了两个东西:

这两层是什么呢?

这两层分别是可读性层 和 init 层。

第一部分,只读层。

它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看

到,它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout,至于什么是

whiteout,我下面马上会讲到)。

第二部分,可读写层。

它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,

即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你

修改产生的内容就会以增量的方式出现在这个层中。

可是,你有没有想到这样一个问题:如果我现在要做的,是删除只读层里的一个文件呢?

为了实现这样的删除操作,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文

件“遮挡”起来。

比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创

建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会

被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只

读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。

所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论

是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用

docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub

上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量

rootfs 的好处

第三部分,Init 层。

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成

的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。

需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往

需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行

修改。

可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些

信息连同可读写层一起提交掉。

所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行

docker commit 只会提交可读写层,所以是不包含这些内容的。

通过:

docker inspect 7ebe 可查看.

那么前面提及到了layer,也就是层sha256:01fd6df81c8ec7dd24bbbd72342671f41813f992999a3471b9d9cbc44ad88374,到底在哪呢?

然后查看cacheid 这个,发现:

这个就是我们前面看到的一层哈。

正是我们经常提到的容器镜像,也叫作:rootfs。它只是一个操作系统的所有文件和目录,并不包含

内核,最多也就几百兆。而相比之下,传统虚拟机的镜像大多是一个磁盘的“快照”,磁盘

有多大,镜像就至少有多大。

通过结合使用 Mount Namespace 和 rootfs,容器就能够为进程构建出一个完善的文件系

统隔离环境。当然,这个功能的实现还必须感谢 chroot 和 pivot_root 这两个系统调用切

换进程根目录的能力。

而在 rootfs 的基础上,Docker 公司创新性地提出了使用多个增量 rootfs 联合挂载一个完

整 rootfs 的方案,这就是容器镜像中“层”的概念。

下一节容器的基本知识。

k8s 深入篇———— docker 镜像是什么[二]的更多相关文章

  1. 第四章 使用Docker镜像和仓库(二)

    第四章 使用Docker镜像和仓库(二) 回顾: 开始学习之前,我先pull下来ubuntu和fedora镜像 [#9#cloudsoar@cloudsoar-virtual-machine ~]$s ...

  2. Docker镜像与仓库(二)Dockerfile

    Docker镜像文件与仓库(二) Docker镜像文件与仓库(二) Dockerfile指令 Dockerfile格式: 1.#Comment注释2.INSTRUCTION大写的指令名 argumen ...

  3. 【Docker】第二篇 Docker镜像管理

    一.搜索镜像 1.下载一个docker镜像:我们可以通过登陆docker网站搜索自己需要的镜像,可以选择自己所需要的版本,然后通过详情也可以看到:网址:https://hub.docker.com/2 ...

  4. Docker镜像服务(二)

    一.Docker镜像介绍 镜像是Docker的三大核心概念之一. Docker运行容器前需要本地存在对应的镜像,如果镜像不存在本地,Docker会尝试先从默认的镜像仓库下载(默认使用Docker Hu ...

  5. docker镜像管理(二)

    docker镜像 docker镜像含有启动容器所需要的文件系统和内容,因此,其用于创建并启动docker容器 docker镜像采用分层构建机制,最底层为bootfs,其之为rootfs bootfs: ...

  6. [第九篇]——Docker 镜像使用之Spring Cloud直播商城 b2b2c电子商务技术总结

    Docker 镜像使用 当运行容器时,使用的镜像如果在本地中不存在,docker 就会自动从 docker 镜像仓库中下载,默认是从 Docker Hub 公共镜像源下载. 下面我们来学习: 1.管理 ...

  7. [第五篇]——Docker 镜像加速之Spring Cloud直播商城 b2b2c电子商务技术总结

    Docker 镜像加速 国内从 DockerHub 拉取镜像有时会遇到困难,此时可以配置镜像加速器.Docker 官方和国内很多云服务商都提供了国内加速器服务,例如: 科大镜像: 网易: 阿里云: 你 ...

  8. docker镜像原理(二)

    一.docker镜像定义 如果我们想要定义mysql5.7镜像应该怎么做? 获取基础镜像,选择一个发行版平台(unbtu.centos) 在centos镜像中安装mysql5.7软件 导出镜像,可以命 ...

  9. Docker基本命令与使用 —— Docker镜像与仓库(二)

    一.查看和删除镜像 1.Docker Image 镜像 容器的基石 层叠的只读文件系统 联合加载(union mount) (存储位置 /var/lib/docker) docker info 2.列 ...

  10. docker+k8s基础篇二

    Docker+K8s基础篇(二) docker的资源控制 A:docker的资源限制 Kubernetes的基础篇 A:DevOps的介绍 B:Kubernetes的架构概述 C:Kubernetes ...

随机推荐

  1. python 字典列表,元组列表 列表嵌套字典 列表嵌套元组 字典嵌套列表

    列表嵌套字典 l=[] for i in alist: kk = {} names.append(i.string) a_url.append(i.get('href')) kk['章节名']=i.s ...

  2. Educational Codeforces Round 158 (Rated for Div. 2)C. Add, Divide and Floor(思维/数学)

    C. Add, Divide and Floor 这里我们选择固定最小数不变,然后每次让其他数向最小数靠近,模拟一下可以发现,只要最大值变为和最小值一样,其他都会和最小值一样. #include &l ...

  3. Ubuntu 离线安装软件包

    Ubuntu 离线安装软件包 关键词:apt-offline,Ubuntu,dpkg,.deb 本文使用的ubuntu20.04,当机器无法连接外网时,我们使用离线的方式安装软件包. 离线安装的软件包 ...

  4. GPS 方案总结

    GPS 方案 搜集网络上关于GPS的方案. redis + mysql redis 用来做设备或用户实时定位的查询. mysql存储历史轨迹.存储时分两部分,一张表做实时查询用.一张表做备份用.如果需 ...

  5. day04-2发送文件

    多用户即时通讯系统04 4.编码实现03 4.6功能实现-发送文件功能实现 4.6.1思路分析 客户端(发送者): 先把文件a.jpg读取到客户端的字节数组 把文件对应的字节数组封装到message对 ...

  6. 14_编译FFmpeg

    本文来详细讲解一下:如何在Mac.Windows环境下成功编译FFmpeg. 目标 这里先提前说明一下,最后希望达到的效果: 编译出ffmpeg.ffprobe.ffplay三个命令行工具 只产生动态 ...

  7. 【atcoder abc276 】(a* 搜索)

    import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import ...

  8. 聊一下Button事件、命令、行为的触发顺序

    1.我们新建一个xaml <StackPanel Width="200" Margin="20"> <Button Height=" ...

  9. Oracle regexp_replace 手机号脱敏

    select '18012345678',regexp_replace('18012345678','(.){4}','****',4,1) from dual;

  10. KingbaseES 复制冲突之锁类型冲突

    背景 昨天遇到客户现场的一个有关复制冲突的问题 备库报错:ERROR: canceling statement due to conflict with recovery,user was holdi ...