08 | 白话容器基础(四):重新认识Docker容器

08 | 白话容器基础(四):重新认识Docker容器

朗读人:张磊    23′01′′ | 9.23M

你好,我是张磊。今天我和你分享的主题是:白话容器基础之重新认识 Docker 容器。

在前面的三次分享中,我分别从 Linux Namespace 的隔离能力、Linux Cgroups 的限制能力,以及基于 rootfs 的文件系统三个角度,为你剖析了一个 Linux 容器的核心实现原理。

备注:之所以要强调 Linux 容器,是因为比如 Docker on Mac,以及 Windows Docker(Hyper-V 实现),实际上是基于虚拟化技术实现的,跟我们这个专栏着重介绍的 Linux 容器完全不同。

而在今天的分享中,我会通过一个实际案例,对“白话容器基础”系列的所有内容做一次深入的总结和扩展。希望通过这次的讲解,能够让你更透彻地理解 Docker 容器的本质。

在开始实践之前,你需要准备一台 Linux 机器,并安装 Docker。这个流程我就不再赘述了。

这一次,我要用 Docker 部署一个用 Python 编写的 Web 应用。这个应用的代码部分(app.py)非常简单:

from flask import Flask
import socket
import os
app = 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)
复制代码

在这段代码中,我使用 Flask 框架启动了一个 Web 服务器,而它唯一的功能是:如果当前环境中有“NAME”这个环境变量,就把它打印在“Hello”后,否则就打印“Hello world”,最后再打印出当前环境的 hostname。

这个应用的依赖,则被定义在了同目录下的 requirements.txt 文件里,内容如下所示:

$ cat requirements.txt
Flask
复制代码

而将这样一个应用容器化的第一步,是制作容器镜像。

不过,相较于我之前介绍的制作 rootfs 的过程,Docker 为你提供了一种更便捷的方式,叫作 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"]
复制代码

通过这个文件的内容,你可以看到Dockerfile 的设计思想,是使用一些标准的原语(即大写高亮的词语),描述我们所要构建的 Docker 镜像。并且这些原语,都是按顺序处理的。

比如 FROM 原语,指定了“python:2.7-slim”这个官方维护的基础镜像,从而免去了安装 Python 等语言环境的操作。否则,这一段我们就得这么写了:

FROM ubuntu:latest
RUN apt-get update -yRUN apt-get install -y python-pip python-dev build-essential
...
复制代码

其中,RUN 原语就是在容器里执行 shell 命令的意思。

而 WORKDIR,意思是在这一句之后,Dockerfile 后面的操作都以这一句指定的 /app 目录作为当前目录。

所以,到了最后的 CMD,意思是 Dockerfile 指定 python app.py 为这个容器的进程。这里,app.py 的实际路径是 /app/app.py。所以,CMD [“python”, “app.py”] 等价于 "docker run python app.py"。

另外,在使用 Dockerfile 时,你可能还会看到一个叫作 ENTRYPOINT 的原语。实际上,它和 CMD 都是 Docker 容器进程启动所必需的参数,完整执行格式是:“ENTRYPOINT CMD”。

但是,默认情况下,Docker 会为你提供一个隐含的 ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 时,比如在我们这个例子里,实际上运行在容器里的完整进程是:/bin/sh -c “python app.py”,即 CMD 的内容就是 ENTRYPOINT 的参数。

备注:基于以上原因,我们后面会统一称 Docker 容器的启动进程为 ENTRYPOINT,而不是 CMD。

需要注意的是,Dockerfile 里的原语并不都是指对容器内部的操作。就比如 ADD,它指的是把当前目录(即 Dockerfile 所在的目录)里的文件,复制到指定容器内的目录当中。

读懂这个 Dockerfile 之后,我再把上述内容,保存到当前目录里一个名叫“Dockerfile”的文件中:

$ ls
Dockerfile app.py requirements.txt
复制代码

接下来,我就可以让 Docker 制作这个镜像了,在当前目录执行:

$ docker build -t helloworld .
复制代码

其中,-t 的作用是给这个镜像加一个 Tag,即:起一个好听的名字。docker build 会自动加载当前目录下的 Dockerfile 文件,然后按照顺序,执行文件中的原语。而这个过程,实际上可以等同于 Docker 使用基础镜像启动了一个容器,然后在容器中依次执行 Dockerfile 中的原语。

需要注意的是,Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。即使原语本身并没有明显地修改文件的操作(比如,ENV 原语),它对应的层也会存在。只不过在外界看来,这个层是空的。

docker build 操作完成后,我可以通过 docker images 命令查看结果:

$ docker image ls
REPOSITORY TAG IMAGE ID
helloworld latest 653287cdf998
复制代码

通过这个镜像 ID,你就可以使用在《白话容器基础(三):深入理解容器镜像》中讲过的方法,查看这些新增的层在 AuFS 路径下对应的文件和目录了。

接下来,我使用这个镜像,通过 docker run 命令启动容器:

$ docker run -p 4000:80 helloworld
复制代码

在这一句命令中,镜像名 helloworld 后面,我什么都不用写,因为在 Dockerfile 中已经指定了 CMD。否则,我就得把进程的启动命令加在后面:

$ docker run -p 4000:80 helloworld python app.py
复制代码

容器启动之后,我可以使用 docker ps 命令看到:

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED
4ddf4638572d helloworld "python app.py" 10 seconds ago
复制代码

同时,我已经通过 -p 4000:80 告诉了 Docker,请把容器内的 80 端口映射在宿主机的 4000 端口上。

这样做的目的是,只要访问宿主机的 4000 端口,我就可以看到容器里应用返回的结果:

$ curl http://localhost:4000
<h3>Hello World!</h3><b>Hostname:</b> 4ddf4638572d<br/>
复制代码

否则,我就得先用 docker inspect 命令查看容器的 IP 地址,然后访问“http://< 容器 IP 地址 >:80”才可以看到容器内应用的返回。

至此,我已经使用容器完成了一个应用的开发与测试,如果现在想要把这个容器的镜像上传到 DockerHub 上分享给更多的人,我要怎么做呢?

为了能够上传镜像,我首先需要注册一个 Docker Hub 账号,然后使用 docker login 命令登录

接下来,我要用 docker tag 命令给容器镜像起一个完整的名字

$ docker tag helloworld geektime/helloworld:v1
复制代码

注意:你自己做实验时,请将 "geektime" 替换成你自己的 Docker Hub 账户名称,比如 zhangsan/helloworld:v1

其中,geektime 是我在 Docker Hub 上的用户名,它的“学名”叫镜像仓库(Repository);“/”后面的 helloworld 是这个镜像的名字,而“v1”则是我给这个镜像分配的版本号。

然后,我执行 docker push:

$ docker push geektime/helloworld:v1
复制代码

这样,我就可以把这个镜像上传到 Docker Hub 上了。

此外,我还可以使用 docker commit 指令,把一个正在运行的容器,直接提交为一个镜像。一般来说,需要这么操作原因是:这个容器运行起来后,我又在里面做了一些操作,并且要把操作结果保存到镜像里,比如:

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

这里,我使用了 docker exec 命令进入到了容器当中。在了解了 Linux Namespace 的隔离机制后,你应该会很自然地想到一个问题:docker exec 是怎么做到进入容器里的呢?

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

比如,通过如下指令,你可以看到当前正在运行的 Docker 容器的进程号(PID)是 25686:

$ docker inspect --format '{{ .State.Pid }}' 4ddf4638572d
25686
复制代码

这时,你可以通过查看宿主机的 proc 文件,看到这个 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 文件上。

有了这样一个可以“hold 住”所有 Linux Namespace 的文件,我们就可以对 Namespace 做一些很有意义事情了,比如:加入到一个已经存在的 Namespace 当中。

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

而这个操作所依赖的,乃是一个名叫 setns() 的 Linux 系统调用。它的调用方法,我可以用如下一段小程序为你说明:

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
int main(int argc, char *argv[]) {
int fd;
fd = open(argv[1], O_RDONLY);
if (setns(fd, 0) == -1) {
errExit("setns");
}
execvp(argv[2], &argv[2]);
errExit("execvp");
}
复制代码

这段代码功能非常简单:它一共接收两个参数,第一个参数是 argv[1],即当前进程要加入的 Namespace 文件的路径,比如 /proc/25686/ns/net;而第二个参数,则是你要在这个 Namespace 里运行的进程,比如 /bin/bash。

这段代码的的核心操作,则是通过 open() 系统调用打开了指定的 Namespace 文件,并把这个文件的描述符 fd 交给 setns() 使用。在 setns() 执行后,当前进程就加入了这个文件对应的 Linux Namespace 当中了。

现在,你可以编译执行一下这个程序,加入到容器进程(PID=25686)的 Network Namespace 中:

$ gcc -o set_ns set_ns.c
$ ./set_ns /proc/25686/ns/net /bin/bash
$ ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:12 errors:0 dropped:0 overruns:0 frame:0
TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:976 (976.0 B) TX bytes:796 (796.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
复制代码

正如上所示,当我们执行 ifconfig 命令查看网络设备时,我会发现能看到的网卡“变少”了:只有两个。而我的宿主机则至少有四个网卡。这是怎么回事呢?

实际上,在 setns() 之后我看到的这两个网卡,正是我在前面启动的 Docker 容器里的网卡。也就是说,我新创建的这个 /bin/bash 进程,由于加入了该容器进程(PID=25686)的 Network Namepace,它看到的网络设备与这个容器里是一样的,即:/bin/bash 进程的网络设备视图,也被修改了。

而一旦一个进程加入到了另一个 Namespace 当中,在宿主机的 Namespace 文件上,也会有所体现。

在宿主机上,你可以用 ps 指令找到这个 set_ns 程序执行的 /bin/bash 进程,其真实的 PID 是 28499:

# 在宿主机上
ps aux | grep /bin/bash
root 28499 0.0 0.0 19944 3612 pts/0 S 14:15 0:00 /bin/bash
复制代码

这时,如果按照前面介绍过的方法,查看一下这个 PID=28499 的进程的 Namespace,你就会发现这样一个事实:

$ ls -l /proc/28499/ns/net
lrwxrwxrwx 1 root root 0 Aug 13 14:18 /proc/28499/ns/net -> net:[4026532281]
$ ls -l /proc/25686/ns/net
lrwxrwxrwx 1 root root 0 Aug 13 14:05 /proc/25686/ns/net -> net:[4026532281]
复制代码

在 /proc/[PID]/ns/net 目录下,这个 PID=28499 进程,与我们前面的 Docker 容器进程(PID=25686)指向的 Network Namespace 文件完全一样。这说明这两个进程,共享了这个名叫 net:[4026532281] 的 Network Namespace。

此外,Docker 还专门提供了一个参数,可以让你启动一个容器并“加入”到另一个容器的 Network Namespace 里,这个参数就是 -net,比如:

$ docker run -it --net container:4ddf4638572d busybox ifconfig
复制代码

这样,我们新启动的这个容器,就会直接加入到 ID=4ddf4638572d 的容器,也就是我们前面的创建的 Python 应用容器(PID=25686)的 Network Namespace 中。所以,这里 ifconfig 返回的网卡信息,跟我前面那个小程序返回的结果一模一样,你也可以尝试一下。

而如果我指定–net=host,就意味着这个容器不会为进程启用 Network Namespace。这就意味着,这个容器拆除了 Network Namespace 的“隔离墙”,所以,它会和宿主机上的其他普通进程一样,直接共享宿主机的网络栈。这就为容器直接操作和使用宿主机网络提供了一个渠道。

转了一个大圈子,我其实是为你详细解读了 docker exec 这个操作背后,Linux Namespace 更具体的工作原理。

这种通过操作系统进程相关的知识,逐步剖析 Docker 容器的方法,是理解容器的一个关键思路,希望你一定要掌握。

现在,我们再一起回到前面提交镜像的操作 docker commit 上来吧。

docker commit,实际上就是在容器运行起来后,把最上层的“可读写层”,加上原先容器镜像的只读层,打包组成了一个新的镜像。当然,下面这些只读层在宿主机上是共享的,不会占用额外的空间。

而由于使用了联合文件系统,你在容器里对镜像 rootfs 所做的任何修改,都会被操作系统先复制到这个可读写层,然后再修改。这就是所谓的:Copy-on-Write。

而正如前所说,Init 层的存在,就是为了避免你执行 docker commit 时,把 Docker 自己对 /etc/hosts 等文件做的修改,也一起提交掉。

有了新的镜像,我们就可以把它推送到 Docker Hub 上了:

$ docker push geektime/helloworld:v2
复制代码

你可能还会有这样的问题:我在企业内部,能不能也搭建一个跟 Docker Hub 类似的镜像上传系统呢?

当然可以,这个统一存放镜像的系统,就叫作 Docker Registry。感兴趣的话,你可以查看Docker 的官方文档,以及VMware 的 Harbor 项目

最后,我再来讲解一下 Docker 项目另一个重要的内容:Volume(数据卷)。

前面我已经介绍过,容器技术使用了 rootfs 机制和 Mount Namespace,构建出了一个同宿主机完全隔离开的文件系统环境。这时候,我们就需要考虑这样两个问题:

  1. 容器里进程新建的文件,怎么才能让宿主机获取到?

  2. 宿主机上的文件和目录,怎么才能让容器里的进程访问到?

这正是 Docker Volume 要解决的问题:Volume 机制,允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作。

在 Docker 项目里,它支持两种 Volume 声明方式,可以把宿主机目录挂载进容器的 /test 目录当中:

$ docker run -v /test ...
$ docker run -v /home:/test ...
复制代码

而这两种声明方式的本质,实际上是相同的:都是把一个宿主机的目录挂载进了容器的 /test 目录。

只不过,在第一种情况下,由于你并没有显示声明宿主机目录,那么 Docker 就会默认在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。而在第二种情况下,Docker 就直接把宿主机的 /home 目录挂载到容器的 /test 目录上。

那么,Docker 又是如何做到把一个宿主机上的目录或者文件,挂载到容器里面去呢?难道又是 Mount Namespace 的黑科技吗?

实际上,并不需要这么麻烦。

在《白话容器基础(三):深入理解容器镜像》的分享中,我已经介绍过,当容器进程被创建之后,尽管开启了 Mount Namespace,但是在它执行 chroot(或者 pivot_root)之前,容器进程一直可以看到宿主机上的整个文件系统。

而宿主机上的文件系统,也自然包括了我们要使用的容器镜像。这个镜像的各个层,保存在 /var/lib/docker/aufs/diff 目录下,在容器进程启动后,它们会被联合挂载在 /var/lib/docker/aufs/mnt/ 目录中,这样容器所需的 rootfs 就准备好了。

所以,我们只需要在 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 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响。

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


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

所以,在一个正确的时机,进行一次绑定挂载,Docker 就可以成功地将一个宿主机上的目录或文件,不动声色地挂载到容器中。

这样,进程在容器里对这个 /test 目录进行的所有操作,都实际发生在宿主机的对应目录(比如,/home,或者 /var/lib/docker/volumes/[VOLUME_ID]/_data)里,而不会影响容器镜像的内容。

那么,这个 /test 目录里的内容,既然挂载在容器 rootfs 的可读写层,它会不会被 docker commit 提交掉呢?

也不会。

这个原因其实我们前面已经提到过。容器的镜像操作,比如 docker commit,都是发生在宿主机空间的。而由于 Mount Namespace 的隔离作用,宿主机并不知道这个绑定挂载的存在。所以,在宿主机看来,容器中可读写层的 /test 目录(/var/lib/docker/aufs/mnt/[可读写层 ID]/test),始终是空的。

不过,由于 Docker 一开始还是要创建 /test 这个目录作为挂载点,所以执行了 docker commit 之后,你会发现新产生的镜像里,会多出来一个空的 /test 目录。毕竟,新建目录操作,又不是挂载操作,Mount Namespace 对它可起不到“障眼法”的作用。

结合以上的讲解,我们现在来亲自验证一下:

首先,启动一个 helloworld 容器,给它声明一个 Volume,挂载在容器里的 /test 目录上:

$ docker run -d -v /test helloworld
cf53b766fa6f
复制代码

容器启动之后,我们来查看一下这个 Volume 的 ID:

$ docker volume ls
DRIVER VOLUME NAME
local cb1c2f7221fa9b0971cc35f68aa1034824755ac44a034c0c0a1dd318838d3a6d
复制代码

然后,使用这个 ID,可以找到它在 Docker 工作目录下的 volumes 路径:

$ ls /var/lib/docker/volumes/cb1c2f7221fa/_data/
复制代码

这个 _data 文件夹,就是这个容器的 Volume 在宿主机上对应的临时目录了。

接下来,我们在容器的 Volume 里,添加一个文件 text.txt:

$ docker exec -it cf53b766fa6f /bin/sh
cd test/
touch text.txt
复制代码

这时,我们再回到宿主机,就会发现 text.txt 已经出现在了宿主机上对应的临时目录里:

$ ls /var/lib/docker/volumes/cb1c2f7221fa/_data/
text.txt
复制代码

可是,如果你在宿主机上查看该容器的可读写层,虽然可以看到这个 /test 目录,但其内容是空的(关于如何找到这个 AuFS 文件系统的路径,请参考我上一次分享的内容):

$ ls /var/lib/docker/aufs/mnt/6780d0778b8a/test
复制代码

可以确认,容器 Volume 里的信息,并不会被 docker commit 提交掉;但这个挂载点目录 /test 本身,则会出现在新的镜像当中。

以上内容,就是 Docker Volume 的核心原理了。

总结

在今天的这次分享中,我用了一个非常经典的 Python 应用作为案例,讲解了 Docke 容器使用的主要场景。熟悉了这些操作,你也就基本上摸清了 Docker 容器的核心功能。

更重要的是,我着重介绍了如何使用 Linux Namespace、Cgroups,以及 rootfs 的知识,对容器进行了一次庖丁解牛似的解读。

借助这种思考问题的方法,最后的 Docker 容器,我们实际上就可以用下面这个“全景图”描述出来:

这个容器进程“python app.py”,运行在由 Linux Namespace 和 Cgroups 构成的隔离环境里;而它运行所需要的各种文件,比如 python,app.py,以及整个操作系统文件,则由多个联合挂载在一起的 rootfs 层提供。

这些 rootfs 层的最下层,是来自 Docker 镜像的只读层。

在只读层之上,是 Docker 自己添加的 Init 层,用来存放被临时修改过的 /etc/hosts 等文件。

而 rootfs 的最上层是一个可读写层,它以 Copy-on-Write 的方式存放任何对只读层的修改,容器声明的 Volume 的挂载点,也出现在这一层。

通过这样的剖析,对于曾经“神秘莫测”的容器技术,你是不是感觉清晰了很多呢?

思考题

  1. 你在查看 Docker 容器的 Namespace 时,是否注意到有一个叫 cgroup 的 Namespace?它是 Linux 4.6 之后新增加的一个 Namespace,你知道它的作用吗?

  2. 如果你执行 docker run -v /home:/test 的时候,容器镜像里的 /test 目录下本来就有内容的话,你会发现,在宿主机的 /home 目录下,也会出现这些内容。这是怎么回事?为什么它们没有被绑定挂载隐藏起来呢?(提示:Docker 的“copyData”功能)

  3. 请尝试给这个 Python 应用加上 CPU 和 Memory 限制,然后启动它。根据我们前面介绍的 Cgroups 的知识,请你查看一下这个容器的 Cgroups 文件系统的设置,是不是跟我前面的讲解一致。

感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。

版权归极客邦科技所有,未经许可不得转载

精选留言

  • 一步
    这样把原理刨根究底的讲解出来,很好,理解的很透彻
    2018-09-10
  • 黄文刚 
    收货很大,感谢张磊!请教一个问题,请问在容器内部如何获取宿主机的IP? 谢谢。
    2018-09-10
    作者回复

    单靠容器,在隔离开的情况下是拿不到的。但是有了kubernetes之后这些系统信息都可以从环境变量里拿到。这个功能叫downwardapi

    2018-09-10

  • Liam
    这节干货满满啊
    2018-09-10
  • 与路同飞
    有预感专栏会破2万
    2018-09-10
  • 假装乐
    听来清晰易懂,省去不少学习时间
    2018-09-10
  • 蔡鹏飞
    docker run 时指定-v挂载宿主机目录到容器目录,即使容器原有目录内有数据,也会被我宿主机目录数据替代的呀。难道是和我使用的storage-driver有关?我用的是overlay存储。
    2018-09-12
    作者回复

    这个行为其实是可配置的

    2018-09-14

  • 陶希阳
    想知道云服务器等技术是不是也是通过namespace + cgroup实习的?
    2018-09-10
    作者回复

    当然不是,那可是正儿八经的虚拟化技术

    2018-09-10

  • 多肉
    一切从问题出发,根据问题理解答案,总结问题如下:
    一、docker镜像如何制作的两种方式是什么?
    二、容器既然是一个封闭的进程,那么外接程序是如何进入容器这个进程的呢?
    三、docker commit对挂载点volume内容修改的影响是什么?
    四、容器与宿主机如何进行文件读写?或volume是为了解决什么题?
    五、Docker的copyData功能是什么?解决了什么问题?
    六、bind mount机制是什么?
    七、cgroup Namespace的作用是什么?
    2018-09-17
  • manatee
    非常感谢老师的讲解,咱们这个有群可以互相交流吗?
    2018-09-10
  • Casper
    对于linux大部分容器做不到在运行容器中动态添加宿主机目录,那在什么特定场合下可以做到呢?给个大致思路即可,谢谢。
    2018-09-10
    作者回复

    如果你用的是katacontainers这种基于虚拟化的容器,才可以实现。但原生其实也不提供这个功能。

    2018-09-10

  • 择动
    在等更新,沙发!
    2018-09-10
  • Leon廖
    谢谢老师!前4节高屋建瓴地介绍了Docker和K8S的演进历史,接着4节Docker基础深入浅出地介绍了重要的基础知识,受益匪浅,这4节Docker基础再次让我体会到了醍醐灌顶的感觉。
    2018-09-26
  • 多肉
    课程干货满满,一堂课下来不听上几遍根本无法掌握课中的知识点,一切从问题出发,能明白课中解决了什么问题或提出了什么问题,从这个角度出发,更正理清课中讲的知识点,总结内容如下:
    一、docker镜像如何制作的两种方式是什么?
    二、容器既然是一个封闭的进程,那么外接程序是如何进入容器这个进程的呢?
    三、docker commit对挂载点volume内容修改的影响是什么?
    四、容器与宿主机如何进行文件读写?或volume是为了解决什么题?
    五、Docker的copyData功能是什么?解决了什么问题?
    六、bind mount机制是什么?
    七、cgroup Namespace的作用是什么?
    2018-09-17
  • 兽医
    讲得非常不错,曾经翻遍了几乎所有Docker官方文档,都没教程中来得深刻,谢谢。。
    2018-09-13
  • long904
    图中不太明白为什么'CMD'属于只读层,那如果 dockfile 里面 yum install 并且 commit 的话,这些 CMD 执行的 yum 命令修改的内容还属于只读层?
    2018-09-13
    作者回复

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

    2018-09-13

  • 一叶
    你好,磊哥,谢谢你讲得这么详细,我有一点不是很清楚:
    容器中的主进程在系统调用或调用一些lib时,调用到的和容器只读层提供的lib吗?
    2018-09-13
    作者回复

    对的

    2018-09-13

  • jason_liew
    精彩精彩!有些地方自己折腾狠难理解到!多谢老师高维指点。
    2018-09-12
  • jssfy
    请问docker挂载有何限制没,是否随便一个目录都可以挂载?在容器里应该是root用户,岂不是可以对目录无节制地操作,哪怕原本主机目录中有些文件并不允许当前用户访问?是否可以相应限制
    2018-09-11
    作者回复

    无限制。至于用户权限,是有user namespace可以做一定的限制。

    2018-09-11

  • netwyh
    如果在docker容器中部署oracle,实际生产环境有没有人这么干?
    2018-09-10
    作者回复

    好像oracle自己就有个方案?

    2018-09-11

  • silver
    所以说 docker exec 每次都会创建一个和容器共享namespace的新进程?
    2018-09-10
    作者回复

    可以这么理解,i.e. spawn a new process in existing namespace

    2018-09-10

  • 蓝色天际
    Linux基础薄弱,理解起来有点吃力
    2018-09-10
    作者回复

    不太理解的地方,可以提出来

    2018-09-10

  • 微leng
    很清晰 👍
    2018-09-10
  • Hurt
    确实清晰了很多了 不过还是有不清楚的点
    2018-09-10
    作者回复

    可以提出来讨论

    2018-09-10

  • NBB
    老师您好!我想请问,我自己搭建的Harbor服务器,在项目管理页面的右上角会显示我有50GB的空间,请问这个空间是取决于我的服务器硬盘容量还是harbor的配置?我有没有办法把空间弄大一点?
    2018-10-18
    作者回复

    不了解harbor……

    2018-10-18

  • 老师好,又来问您问题了。生产中有没有什么好的工具管理本地的docker registry,比如磁盘满了想清理等等的操作。期待老师的回复,谢谢!
    2018-10-18
    作者回复

    harbor啊

    2018-10-18

  • 孙磊
    1. 4.6内核之前,查看/proc/$PID/cgroup,或者挂载cgroup时,在容器中会看到整个系统的cgroup,存在安全隐患,4.6内核之后新增cgroup namespace ,每个namespace中的进程都有自己的cgroup了;
    2. 这个问题感觉像陷阱,挂载后改变的是home目录的inode值(/test的inode),有数据就很正常了;
    2018-10-16
    作者回复

    2. docker对这个行为其实可配置

    2018-10-16

  • 挂载文件夹能理解,但是如果挂载宿主机的某一文件到容器,为什么需要容器事先存在同名的这个文件呢?
    2018-10-12
    作者回复

    不需要啊,文件和目录都不需要事先存在啊

    2018-10-12

  • 韩懿留
    很好的课程,从docker开始讲了来龙去脉。
    边听语音边看文字往下学,效率很高。
    repository读错了😏
    2018-10-11
  • 豆沙包
    你好 我用docker run起了app这个容器之后,查看aufs里层的信息,读写层为什么会和init层是同一个层呢?不是一个会被打包 一个不会被打包么?
    # cat /sys/fs/aufs/si_cc04a4d4a9a38631/br[0-9]*
    /var/lib/docker/aufs/diff/fcacef659a1dee2d16c584979fddcd584971507cd7845316e7fca768069e9295=rw
    /var/lib/docker/aufs/diff/fcacef659a1dee2d16c584979fddcd584971507cd7845316e7fca768069e9295-init=ro+wh
    /var/lib/docker/aufs/diff/d2a9b0eca68f7bcd061648edace2f0f5e7e50ba8bb556cae0ab408681cb4ea02=ro+wh
    /var/lib/docker/aufs/diff/6d5cd3d3a45e40517745c86e363756021286100ae0fb5db7a1cb6a7ec8cd4411=ro+wh
    /var/lib/docker/aufs/diff/684b4a4ca733a7b66b46e8c1e5fe841a36af14f237a3fa82724832a335279122=ro+wh
    /var/lib/docker/aufs/diff/d5e965276684d42d69c3f12bb0bfeeaa3c79f2b95cf6251e6779014a73430a86=ro+wh
    /var/lib/docker/aufs/diff/ce080aadd734de0ddcd0cce2f65a6b79e4d41c736ddde97e48dc923fd6c5d067=ro+wh
    /var/lib/docker/aufs/diff/d4672d8b1865d99adebeb807a5b161fb4558d8ff8b0164747a6dc22f2f09c428=ro+wh
    /var/lib/docker/aufs/diff/b651ff0b03fec7f0f69669473589d4ed8c3ffd84d5a509b41f71b19f687425c8=ro+wh
    2018-10-10
    作者回复

    你没分清楚mnt目录和diff目录的区别……

    2018-10-10

  • alanzx
    慢慢学
    2018-10-05
  • yyarrow
    干货很多,要反复琢磨一下了!
    2018-10-02
  • 追寻云的痕迹
    docker-ee的实现不是基于Hyper-V吧
    2018-09-27
    作者回复

    在windows 上一共就两种方案hyperv和win container ,它必然得支持

    2018-09-27

  • robin
    只会最后这个。
    docker run -p 8000:80 --cpuset-cpus="1" -m 500M helloworld 限制只能在cpu1上运行,内存限制为500M
    2018-09-21
  • 木 易
    张老师,能把搭建docker k8s的环境简要说一下吗,比如使用ubuntu还是centos、rhel,什么版本。我们可以参考教程,自己搭建模拟环境。
    2018-09-21
    作者回复

    看部署那篇

    2018-09-21

  • Geek_9ca34e
    ;老师,有个地方还是不太明白,以下是你些的原文:
    注意:这里提到的 " 容器进程 ",是 Docker 创建的一个容器初始化进程(dockerinit),而不是应用进程 (ENTRYPOINT + CMD) dockerinit 会负责完成根目录的准备、挂载设备和目录、配置 hostname 等一系列需要在容器内进行的初始化操作。 最后,它通过 execv() 系统调用,让应用进程取代自己,成为容器里的 PID=1的进程。
    我有以下疑问:
    1、dockerinit 是一个进程,完成初始化之后 ,让应用进程取代自己,那么dockerinit 进程会自动销毁么?
    2、这里的dockerinit和应用进程在宿主机中是两个不同的进程id么?
    3、执行 docker exec -it container bin/sh 命令后,bin/sh 命令创建的进程只是加入了container的namespace,但是从宿主机的角度它是一个独立的进程,只是共享了namespace信息么?
    4、一个容器内部是否只有一个进程?,或者说容器只是一个房间(由namespace和cgroup组成),而其他的进程都是走进了这个房间,让大家以为这个房间就是一个系统,里面包含了这么多进程,其实在宿主机的角度他们都是一个个进程,只是共享了namespace和cgroup 而已,这样理解对么?
    2018-09-21
    作者回复

    是。替换当然就是为了保证pid不变。注意,容器里的其他进程都是1号进程的子进程,不存在独立的说法。

    2018-09-22

  • 甄心cherishm
    为什么容器内只能看到两个网卡信息呢?
    2018-09-20
    作者回复

    因为被隔离到单独的net ns里了呀

    2018-09-21

  • Young
    2018-09-20
  • Genius
    我想请教一个问题,为啥子镜像的entrypoint会覆盖母镜像的entrypoint的呢?
    2018-09-19
    作者回复

    因为dockerfile顺序build啊

    2018-09-19

  • ShJin、Cheng
    那么老师,请问有没有办法禁止通过exec -it的方式进入容器?
    2018-09-17
    作者回复

    权限控制的办法很多,但这也啥意义呢?我手写一个20行的C语言代码就可以加入容器的namespace 啊。所以不随便给人root权限比较重要吧。

    2018-09-17

  • gao
    docker容器挂载目录在commit后再次进入image,还是能看到之前挂载目录下的文件的,有点奇怪。
    抱歉:再次进入image 还是docker run image,但没有指定volumn。
    2018-09-16
    作者回复

    因为volume除非删除掉它还是在那台主机上,你在别的机器上拉镜像试试

    2018-09-17

  • 江宇霆
    写的不错 点赞
    2018-09-16
  • gao
    docker容器挂载目录在commit后再次进入image,还是能看到之前挂载目录下的文件的,有点奇怪。
    2018-09-15
    作者回复

    再次进入image是什么操作……

    2018-09-16

  • Aaron
    请问老师,镜像只提供文件系统,系统调用使用的宿主机的内核?这个感觉很模糊,不是很理解
    2018-09-14
    作者回复

    默念三遍 内核共享

    2018-09-16

  • 侯强亮
    公司研发环境用的 oracle 官方的 12c 容器
    2018-09-14
  • jssfy
    centos7都是版本3的内核,针对k8s,你们到目前为止有遇到过因为内核bug的问题必须从3切换到版本4的内核的场景吗,比如切换到ubuntu?篇幅问题具体问题不方便在这里描述。主要是想了解下,你们是否有遇到过centos有些比较棘手的问题,而不得不试图切换到其它os的场景呢
    2018-09-13
  • vasks
    docket里面的用户和真实系统的会冲突吗 docker启动用户用root好不好
    2018-09-13
  • leo
    第三章并没有说明/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e的来源,而是直接告知ubuntu:latest就是挂载在这个目录。
    如果我想查看alpine:latest的挂载目录,应该如何查找?
    换句话说就是如何通过docker image的唯一id来查找出内部id(si_xxx)?
    感谢解答!
    2018-09-12
    作者回复

    遗憾的是,这个ID没有对应关系,最方便的做法是查看mnt的目录变化。

    2018-09-13

  • 小鱼儿
    老师分享的太清晰明了了!Linux不是很懂。有几个问题困扰很久了,想请教下。我安装了docker for window,启用了Linux容器,虚拟机是hv。
    1、如何进入docker所在的linux宿主
    2、即使进入了,安装命令都没有如何安装。如,rpm,yum,apt,make 这些命令都没有
    3、如果用curl安装,下载下来,没有make命令也安装不了

    老师,这些有什么办法解决呢?只是想在window机器上直接可以用docker

    2018-09-12
  • hepr1989
    干货满满,终于把之前的一些疑惑弄清楚了
    2018-09-12
  • jssfy
    1. 如果使用user namespace的话,容器里的root是否还是对文件有权限?
    2. centos 7貌似目前还不支持 user namespace
    3. 现在部署k8s会考虑用centos吗?特别是对gpu有需求的场景
    2018-09-12
    作者回复

    kubernetes 肯定不挑操作系统啊

    2018-09-13

  • 明珠
    容器init层的文件是容器独享的,还是和操作系统共享的? 不是很明白init层是怎么实现的?
    2018-09-12
  • 笑忘日月星辰
    庖丁解牛
    2018-09-12
  • Geek_7d117e
    一条条留言看过去,并逐一解答 赞
    2018-09-11
  • 王雅光
    老师,如果在docker运行的服务上,修改其配置文件,比如tomcat配置文件,或者webapp下面spring-mvc.xml等配置文件,是否都是以volumn绑定挂载的形式做修改!这个跟init层的区别在哪里!
    2018-09-11
    作者回复

    volume本质是宿主机上的一个独立目录!不属于rootfs的一部分!

    2018-09-11

  • 王雅光
    老师,如果在docker容器里跑的服务里面做一些配置的修改,比如tomcat里面的配置,或者webapp里面的spring-mvc.xml等类似的配置文件,都是以volumn这样的绑定挂载形式做修改吧!不知是否正确!
    2018-09-11
    作者回复

    可以的

    2018-09-11

  • Gordon_luo
    您好,rancher好用吗
    2018-09-11
    作者回复

    商业产品,不做评价

    2018-09-11

  • Tank
    你好,你提供的python 脚本似乎存在问题。没有引入 os 但是下面却有用os.getenv 这时会报 global name 'os' is not defined 当我在文件开始的地方加入 import os 运行镜像请求即可成功
    2018-09-11
    作者回复

    感谢指出,已联系编辑修正

    2018-09-11

  • Geek_zz
    你好,可读写层的修改数据保存的话,会保存到哪里
    2018-09-11
    作者回复

    所有的层都保存在diff目录下

    2018-09-11

  • 天下
    非常好,在结合容器云的书。之前不懂得清晰多了。辛苦了
    2018-09-10
  • Geek_700d17
    Cgroup namespace 推测是用来"容器"内的资源管理的,不知对不对
    2018-09-10
  • Casper
    我遇到一个问题,如何才能在运行的容器中动态挂载宿主机上的目录呢?
    2018-09-10
    作者回复

    对于大部分linux容器实现来说,都是做不到的

    2018-09-10

  • asdf100
    这样,我们新启动的这个容器,就会直接加入到 ID=4ddf4638572d 的容器,也就是我们前面的创建的 Python 应用容器(PID=25686)的 Network Namespace 中。所以,这里 ifconfig 返回的网卡信息,跟我前面那个小程序返回的结果一模一样,你也可以尝试一下。
    ---------------
    容器加入容器?不是把ifconfig 这个进程加入到ID=4ddf4638572d 的容器里吗?
    2018-09-10
    作者回复

    新创建的这个容器,不就是个进程嘛。也正因为如此,新容器里的所有进程,不只是ifconfig,看到的都是这个已经存在的network namespace。

    2018-09-10