前言

简单介绍一下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. C++ STL 容器 list类型

    C++ STL 容器 list类型 list对于异常支持很好,要么成功,要么不会发生什么事情 以下是 std::list 在异常处理方面表现良好的几个原因: 动态内存管理:std::list 使用动态 ...

  2. Java 手动抛异常

    1 package com.bytezero.throwable; 2 3 import java.io.File; 4 import java.io.FileInputStream; 5 impor ...

  3. javascript web development es6 pdf js - Cheat Sheet 表格 [vuejs / webcomponents-cheatsheet-2021] 共3套

    ES6 预览 VUE2 预览 webcomponents 预览 ES6 2019 pdf下载: https://files.cnblogs.com/files/pengchenggang/javasc ...

  4. linux文件管理(补充)

    linux文件管理 vim编辑器 vi概述 vi 编辑器 他是linux和unix系统上最基本的文本编辑器,类似于windows系统下的记事本编辑器 vim编辑器 vim是vi的加强版,比vi更容易使 ...

  5. Vue 长文本组件(有展开更多按钮)实现 附源码及使用

    原文地址:Vue 长文本组件(有展开更多按钮) | Stars-One的杂货小窝 最近项目需要优化长文本的显示,如果长文本过长,固定显示几行并显示一个展开更多的按钮,点击按钮即可把隐藏的文本显示出来 ...

  6. 深度学习论文翻译解析(二十一):High-Performance Large-Scale Image Recognition Without Normalization

    论文标题:High-Performance Large-Scale Image Recognition Without Normalization 论文作者:Andrew Brock Soham De ...

  7. SSR解决了什么问题?有做过SSR吗?你是怎么做的?

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.是什么 Server-Side Rendering 我们称其为SSR,意为服务端渲染 指由服务侧完成页面的 HTML 结构拼接的页面处 ...

  8. 使用 NocoDB 一键将各种数据库转换为智能表格

    NocoDB 是一款开源的无代码数据库平台,可以进行数据管理和应用开发.它的灵感来自 Airtable,支持与 Airtable 类似的电子表格式交互.关系型数据库 Schema 设计.API 自动生 ...

  9. Win10 如何在桌面显示我的电脑

    Win10桌面右键鼠标,然后在弹出来的选项中选择个性化. 选择了个性化后会弹出设置界面,在设置中选择[主题] 找到[桌面图标设置] 点击[桌面图标设置],会弹出一个对话框,该对话框有可以设置显示的图标 ...

  10. Python数据类型---列表、元祖、字典【详解】

    一.列表(List) 1.列表可以用来存储不同的数据类型,使用 [ ] e.g. 1 service = ['http','ssh','ftp'] 2.列表是有索引的,也就是可以通过下标来访问数据 3 ...