深入理解Docker容器镜像

深入理解Docker容器镜像

1 容器是什么:特殊的进程

容器其实是一种沙盒技术。顾名思义,沙盒就是能够像一个集装箱一样,把你的应用“装”起来的技术。这样,应用与应用之间,就因为有了边界而不至于相互干扰;而被装进集装箱的应用,也可以被方便地搬来搬去,这也是 PaaS 最理想的状态。

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。

Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

障眼法(PID Namespace)解释:

  • 本来,每当我们在宿主机上运行了一个 /bin/sh 程序,操作系统都会给它分配一个进程编号,比如 PID=100。这个编号是进程的唯一标识,就像员工的工牌一样。所以 PID=100,可以粗略地理解为这个 /bin/sh 是我们公司里的第 100 号员工,而第 1 号员工就自然是比尔 · 盖茨这样统领全局的人物。
  • 而现在,我们要通过 Docker 把这个 /bin/sh 程序运行在一个容器当中。这时候,Docker 就会在这个第 100 号员工入职时给他施一个“障眼法”,让他永远看不到前面的其他 99 个员工,更看不到比尔 · 盖茨。这样,他就会错误地以为自己就是公司里的第 1 号员工。
  • 这种机制(Namespace),其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如 PID=1。可实际上,他们在宿主机的操作系统里,还是原来的第 100 号进程。

除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。

2 容器的隔离与限制:Namespace&Cgroups(障眼法)

在这里插入图片描述

2.1 Namespace:隔离

Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。

2.2 Cgroups:限制进程资源

Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能

  • Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

案例:限制某个进程的CPU使用

# 限制cpu使用率最多为20%
# 每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间
$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us# 配置限制的进程ID
$ echo 226 > /sys/fs/cgroup/cpu/container/tasks 

3 容器镜像是如何组成的

在这里插入图片描述

3.1 创建一个容器所需步骤

Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

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

这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用虽然功能类似,但是也有细微的区别。

  • chroot是只改变即将运行的 某进程的根目录。
  • pviot_root主要是把整个系统切换到一个新的root目录,然后去掉对之前rootfs的依赖,以便于可以umount 之前的文件系统(pivot_root需要root权限)

3.2 容器中的文件系统rootfs:只包含文件及目录

①概念

在第二部分我们已经知道了,可以通过Namespace来隔离容器与宿主机。但是Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

  • 这就导致,我们挂载之后看到的文件依然是宿主机的文件(继承宿主机文件)。我们希望每当创建一个新容器时,我希望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。
  • 解决办法:通过chroot 或者pivot_root命令。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到你指定的位置。它的用法也非常简单。
# 告诉操作系统,我们将使用$HOME/test目录作为/bin/bash进程的根目录
chroot $HOME/test /bin/bash
  • chroot是只改变即将运行的 某进程的根目录。
  • pviot_root主要是把整个系统切换到一个新的root目录,然后去掉对之前rootfs的依赖,以便于可以umount 之前的文件系统(pivot_root需要root权限)

这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

所以,一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等:

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

  • rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”
  • 实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核

这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

②组成

在这里插入图片描述
最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。

1. 只读层(read only+whiteout)

它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout白障)。

2. 可读写层:容器中写操作会以增量方式写入该层

它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。

  • 写操作:而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。
    • 容器的rootfs,比如ubuntu镜像都是以只读方式挂载,那么如何实现修改?
    • Copy on write:拷贝只读层的文件到可读写层,然后修改。后续可以docker commit提交,新增一层layer。相同文件名,上层会覆盖下层。
  • 删除操作:AuFS(用于联合挂载,比如:我把A、B目录合并起来挂载为一个C目录) 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。

所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。

3. Init 层:/etc/hosts文件等,只对当前容器有效

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。

  • 可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。
  • 所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。

4 体验整个容器过程

在这里插入图片描述

4.1 Dockerfile构建并启动容器

①编写Dockerfile
  1. app.py
from flask import Flask
import socket
import osapp = Flask(__name__)@app.route('/')
def hello():html = "<h3>Hello {name}!</h3>" \"<b>Hostname:</b> {hostname}<br/>"           return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname())if __name__ == "__main__":app.run(host='0.0.0.0', port=80)
  1. requirements.txt
Flask
  1. Dockerfile
# 使用官方提供的Python开发镜像作为基础镜像
FROM python:2.7-slim# 将工作目录切换为/app
WORKDIR /app# 将当前目录下的所有内容复制到/app下
ADD . /app# 使用pip命令安装这个应用所需要的依赖
RUN pip install --trusted-host pypi.python.org -r requirements.txt# 允许外界访问容器的80端口
EXPOSE 80# 设置环境变量
ENV NAME World# 设置容器进程为:python app.py,即:这个Python应用的启动命令
CMD ["python", "app.py"]

该容器启动之后(run),会直接执行python app.py命令

②docker build 构建镜像

所需文件结构:
在这里插入图片描述

# -t 的作用是给这个镜像加一个 Tag,即:起一个好听的名字
docker build -t helloworld .

在这里插入图片描述

# 查看build出来的镜像
docker images | grep helloworld

在这里插入图片描述

③docker run运行容器
# 运行容器
docker run -p 4000:80 helloworld

在这里插入图片描述
浏览器访问容器:
在这里插入图片描述

④ docker commit & docker push

有时,我们想将在原来镜像的基础上增添加自己的修改,那么则需要使用docker commit命令

$ docker exec -it 26df5f908a6f /bin/sh
# 在容器内部新建了一个文件
root@26df5f908a6f:/app# touch test.txt
root@26df5f908a6f:/app# exit#将这个新建的文件提交到镜像中保存
$ docker commit 26df5f908a6f ziyi/helloworld:v2

在这里插入图片描述

下一次使用,只需要拉取v2版本的镜像即可。

问题解析:

1. docker exec 是怎么做到进入容器里的呢(启进程共享namespace)

Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。

# containerID
$ docker inspect --format '{{ .State.Pid }}'  4ddf4638572d
25686# 查看该进程对应NameSpace的文件
ls -l /proc/25686/ns
# 命令结果:
total 0
lrwxrwxrwx 1 root root 0 Aug 13 14:05 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 ipc -> ipc:[4026532278]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 mnt -> mnt:[4026532276]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 net -> net:[4026532281]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid_for_children -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 uts -> uts:[4026532277]

一个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。

这也就意味着一个进程,可以选择加入到某个进程已有的 Namespace 当中(可以看到该容器下的所有文件),从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。

  • setns() 的 Linux 系统调用,实现进入namespace
2. 容器与宿主机的文件如何相互访问(Volume)

Volume 机制,允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作。

# 运行时,通过-v参数指定挂载目录
docker run -v /home:/test ...

我们只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。
更重要的是,由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。

  • 注意:这里提到的"容器进程",是 Docker 创建的一个容器初始化进程 (dockerinit),而不是应用进程 (ENTRYPOINT + CMD)。dockerinit 会负责完成根目录的准备、挂载设备和目录、配置 hostname 等一系列需要在容器内进行的初始化操作。最后,它通过 execv() 系统调用,让应用进程取代自己,成为容器里的 PID=1 的进程。

绑定挂载原理:

如果大家有了解Linux 内核的话,就会明白,绑定挂载实际上是一个 inode 替换的过程。在 Linux 操作系统中,inode 可以理解为存放文件内容的“对象”,而 dentry,也叫目录项,就是访问这个 inode 所使用的“指针”。

在这里插入图片描述

正如上图所示,mount --bind /home /test,会将 /home 挂载到 /test 上。其实相当于将 /test 的 dentry,重定向到了 /home 的 inode。这样当我们修改 /test 目录时,实际修改的是 /home 目录的 inode。这也就是为何,一旦执行 umount 命令,/test 目录原先的内容就会恢复:因为修改真正发生在的,是 /home 目录里。

3. rootfs是以只读方式挂载,那么我们如何修改镜像

rootfs:

  • 读写层:运行容器后进行的增删改,都会记录到该层
  • init层:存放/etc/hosts这种只对当前容器有效的文件
  • 只读层:镜像原本的文件

上面的读写层通常也称为容器层,下面的只读层称为镜像层,所有的增删查改操作都只会作用在容器层,相同的文件上层会覆盖掉下层。知道这一点,就不难理解镜像文件的修改,比如修改一个文件的时候,首先会从上到下查找有没有这个文件,找到,就复制到容器层中,修改,修改的结果就会作用到下层的文件,这种方式也被称为copy-on-write。

任何镜像里的内容,都属于只读层。commit之后的东西当然也属于只读层。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/6518.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker高频使用命令

一、Docker常用命令总结 1.镜像命令管理 指令描述ls列出镜像build构建镜像来自Dockerfilehoistory查看历史镜像inspect显示一个或多个镜像的详细信息pull从镜像仓库拉取镜像push推送一个镜像仓库rm移除一个或多个镜像prune一处未使用的镜像&#xff0c;没有被标记或被任何容器…

linux之ssh

SSH远程连接协议 SSH远程管理 定义 SSH&#xff08;Secure Shell &#xff09;是一种安全通道协议&#xff0c;主要用来实现字符界面的远程的登录、远程复制等功能。 SSH协议对通信双方的数据传输进行了加密处理&#xff0c;其中包括用户登录时输入的用户口令。因此SSH协议具…

WPF之绑定验证(错误模板使用)

1&#xff0c;前言&#xff1a; 默认情况下&#xff0c;WPF XAML 中使用的绑定并未开启绑定验证&#xff0c;这样导致用户在UI上对绑定的属性进行赋值时即使因不符合规范内部已抛出异常&#xff08;此情况仅限WPF中的数据绑定操作&#xff09;&#xff0c;也被程序默认忽略&…

《苍穹外卖》前端课程知识点记录

一、VUE基础知识 基于脚手架创建前端工程 1. 环境要求 安装node.js&#xff1a;Node.js安装与配置&#xff08;详细步骤&#xff09;_nodejs安装及环境配置-CSDN博客查看node和npm的版本号 安装Vue CLI&#xff1a;Vue.js安装与创建默认项目&#xff08;详细步骤&#xff09;…

分享一篇关于AGI的短文:苦涩的教训

学习强化学习之父、加拿大计算机科学家理查德萨顿&#xff08; Richard S. Sutton &#xff09;2019年的经典文章《The Bitter Lesson&#xff08;苦涩的教训&#xff09;》。 文章指出&#xff0c;过去70年来AI研究走过的最大弯路&#xff0c;就是过于重视人类既有经验和知识&…

探究Android的多分辨率支持以及各种类型图标尺寸大小

术语和概念 屏幕尺寸 屏幕的物理尺寸&#xff0c;以屏幕的对角线长度作为依据&#xff08;比如 2.8寸&#xff0c; 3.5寸&#xff09;。 简而言之&#xff0c; Android把所有的屏幕尺寸简化为三大类&#xff1a;大&#xff0c;正常&#xff0c;和小。 程序可以针对这三种尺寸…

Docker部署nginx并且实现https访问

实验环境&#xff1a; 在已有的docker环境和nginx镜像的基础上进行操作 1、生成私钥 &#xff08;1&#xff09;openssl genrsa -out key.pem 2048 生成证书签名请求 (CSR) 并自签证书: &#xff08;2&#xff09;openssl req -new -x509 -key key.pem -out cert.pem -day…

DDD:根据maven的脚手架archetype生成ddd多模块项目目录结构

随着领域驱动的兴起&#xff0c;很多人都想学习如何进行ddd的项目开发&#xff0c;那ddd的项目结构是怎么样的&#xff1f;又是如何结合SpringBoot呢&#xff1f;那么针对这个问题&#xff0c;笔者使用maven的archetype封装一个相对通用的ddd的项目目录&#xff0c;方便一键生成…

karpathy Let‘s build GPT

1 introduction 按照karpathy的教程&#xff0c;一步步的完成transformer的构建&#xff0c;并在这个过程中&#xff0c;加深对transformer设计的理解。 karpathy推荐在进行网络设计的过程中&#xff0c;同时利用jupyter notebook进行快速测试和python进行主要的网络的构建。 …

STM32标准库SPI通信协议与W25Q64

目录 一、SPI通信 1.SPI通信简介 2.硬件电路 3.移位示意图 4.SPI基本时序图 &#xff08;1&#xff09;起始和终止 &#xff08;2&#xff09;交换一个字节 模式0&#xff1a; 模式1&#xff1a;​编辑 模式2&#xff1a;​编辑 模式3&#xff1a;​编辑 5.SPI时序 …

初识C语言——第九天

ASCII定义 在 C 语言中&#xff0c;每个字符都对应一个 ASCII 码。ASCII 码是一个字符集&#xff0c;它定义了许多常用的字符对应的数字编码。这些编码可以表示为整数&#xff0c;也可以表示为字符类型。在 C 语言中&#xff0c;字符类型被定义为一个整数类型&#xff0c;它占…

数据仓库实验三:分类规则挖掘实验

目录 一、实验目的二、实验内容和要求三、实验步骤1、创建数据库和表2、决策树分类规则挖掘&#xff08;1&#xff09;新建一个 Analysis Services 项目 jueceshu&#xff08;2&#xff09;建立数据源视图&#xff08;3&#xff09;建立挖掘结构 DST.dmm&#xff08;4&#xff…

43 单例模式

目录 1.什么是单例模式 2.什么是设计模式 3.特点 4.饿汉和懒汉 5.峨汉实现单例 6.懒汉实现单例 7.懒汉实现单例&#xff08;线程安全&#xff09; 8.STL容器是否线程安全 9.智能指针是否线程安全 10.其他常见的锁 11.读者写者问题 1. 什么是单例模式 单例模式是一种经典的&a…

线性数据结构-手写队列-哈希(散列)Hash

什么是hash散列&#xff1f; 哈希表的存在是为了解决能通过O(1)时间复杂度直接索引到指定元素。这是什么意思呢&#xff1f;通过我们使用数组存放元素&#xff0c;都是按照顺序存放的&#xff0c;当需要获取某个元素的时候&#xff0c;则需要对数组进行遍历&#xff0c;获取到指…

【skill】onedrive的烦人问题

Onedrive的迷惑行为 安装Onedrive&#xff0c;如果勾选了同步&#xff0c;会默认把当前用户的数个文件夹&#xff08;桌面、文档、图片、下载 等等&#xff09;移动到安装时提示的那个文件夹 查看其中的一个文件的路径&#xff1a; 这样一整&#xff0c;原来的文件收到严重影…

吴恩达2022机器学习专项课程C2(高级学习算法)W1(神经网络):2.1神经元与大脑

目录 神经网络1.初始动机*2.发展历史3.深度学习*4.应用历程 生物神经元1.基本功能2.神经元的互动方式3.信号传递与思维形成4.神经网络的形成 生物神经元简化1.生物神经元的结构2.信号传递过程3.生物学术语与人工神经网络 人工神经元*1.模型简化2.人工神经网络的构建3.计算和输入…

Java与Go: 生产者消费者模型

什么是生产者消费者模型 生产者-消费者模型&#xff08;也称为生产者-消费者问题&#xff09;是一种常见的并发编程模型&#xff0c;用于处理多线程或多进程之间的协同工作。该模型涉及两个主要角色&#xff1a;生产者和消费者&#xff0c;一个次要角色&#xff1a;缓冲区。 生…

18 内核开发-内核重点数据结构学习

课程简介&#xff1a; Linux内核开发入门是一门旨在帮助学习者从最基本的知识开始学习Linux内核开发的入门课程。该课程旨在为对Linux内核开发感兴趣的初学者提供一个扎实的基础&#xff0c;让他们能够理解和参与到Linux内核的开发过程中。 课程特点&#xff1a; 1. 入门级别&…

办公数据分析利器:Excel与Power Query透视功能

数据分析利器&#xff1a;Excel与Power Query透视功能 Excel透视表和Power Query透视功能是强大的数据分析工具&#xff0c;它们使用户能够从大量数据中提取有意义的信息和趋势&#xff0c;可用于汇总、分析和可视化大量数据。 本文通过示例演示Power Query透视功能的一个小技…

Linux专栏08:Linux基本指令之压缩解压缩指令

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Linux专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Linux基本指令之压缩解压缩指令 编号&#xff1a;08 文章目录 Linu…