容器技术概念入门篇2
- 继Namespace构建了四周了围墙(进程隔离),Cgroups构建了受控的天空优先使用阳光雨露(资源限制),Mount namespace与rootfs构建了脚下的大地,这片土地是你熟悉和喜欢的,不管你走到哪里,都可以带着它,就好像你从未离开过家乡,没有丝毫的陌生感(容器的一致性)~
- 既然容器的 rootfs(比如,Ubuntu 镜像),是以只读方式挂载的,那么又如何在容器里修改 Ubuntu 镜像的内容呢?(提示:Copy-on-Write)
- 上面的读写层通常也称为容器层,下面的只读层称为镜像层,所有的增删查改操作都只会作用在容器层,相同的文件上层会覆盖掉下层。知道这一点,就不难理解镜像文件的修改,比如修改一个文件的时候,首先会从上到下查找有没有这个文件,找到,就复制到容器层中,修改,修改的结果就会作用到下层的文件,这种方式也被称为copy-on-write。
- Dockerfile 的设计思想,是使用一些标准的原语(即大写高亮的词语),描述我们所要构建的 Docker 镜像。并且这些原语,都是按顺序处理的。
- 默认情况下,Docker 会为你提供一个隐含的 ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 时,比如在我们这个例子里,实际上运行在容器里的完整进程是:/bin/sh -c “python app.py”,即 CMD 的内容就是 ENTRYPOINT 的参数。
- 我们后面会统一称 Docker 容器的启动进程为 ENTRYPOINT,而不是 CMD。
- Dockerfile中每个原语执行后,都会生成一个对应的镜像层。
- 一个进程的每种Linux Namespace,都在它对应的/proc/[进程号]/ns下有有一个对应的虚拟文件,并且链接到一个真实的Namespace文件上。
- docker inspect --format '{{ .State.Pid }}' 容器名称【或容器ID】
- ls -l /proc/78294/ns
- 有了这样一个可以“hold住”所有Linux Namespace的文件,我们就可以对Namespace做一些有意义事情了,比如:加入到一个已经存在的Namespace当中。
- 这也就意味着:一个进程,可以选择加入到某个进程已有的Namespace当中,从而达到“进入”这个进程所在容器的目的,这正是docker exec的实现原理。
- 这个操作所依赖的,乃是一个名叫 setns() 的 Linux 系统调用。
- docker commit.实际上就是在容器运行起来后,把最上层的“可读写层”,加上原先容器镜像的只读层,打包组成了一个新的镜像。
- Copy-on-Write:由于使用了联合文件系统,你在容器里对镜像rootfs所做的任何修改,都会被操作系统先复制到这个可读写层,然后再修改。
- 统一存放镜像的系统,就叫作Docker Registry。
- Docker Volume要解决的问题:Volume 机制,允许你将宿主机上指定的目录或者文件,挂载到容器进行读取和修改操作。
- docker run -v /test ...
- docker run -v /home:/test ...
- 在第一种情况下,由于你并没有显示声明宿主机目录,那么 Docker 就会默认在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。而在第二种情况下,Docker 就直接把宿主机的 /home 目录挂载到容器的 /test 目录上。
- “容器进程”已经创建,意味着Mount Namespace已经开启,这个挂载只在这个容器里可见。你在宿主机上是看不见容器内部这个挂载点的。这就保证了容器的隔离性不会被Volume打破。
- “容器进程”,是Docker创建的第一个容器初始化进程(dockerinit),而不是应用进程(ENTERPOINT+CMD)。dockerinit会负责完成根目录的准备、挂在设备和目录、配置hostname等一系列需要再容器内进行初始化操作。最后,它通过execv()系统调用,让应用进程取代自己,成为容器里PID=1的进程。
- 绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。
- 绑定挂载实际上是inode替换的过程。再Linux操作系统中,inode可以理解为存放文件内容的“对象”,而dentry,也叫目录项,就是访问这个inode所使用的“指针”。
- 再一个正确的时机,进行一次绑定挂载,Docker就可以成功地将一个宿主机上地目录或文件,不动声色地挂载到容器中。
- Docker容器地“全景图”:
1、用 Docker 部署一个用 Python 编写的 Web 应用,代码如下:
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)
2、将python依赖的包放入txt文件中:
echo 'Flask' > requirements.txt
3、制作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~ctc # 设置容器进程为:python app.py,即:这个 Python 应用的启动命令 CMD ["python","app.py"]
4、制作镜像(其中,-t 的作用是给这个镜像加一个 Tag,即:起一个好听的名字。):
docker build -t helloworld-ctc .
5、具体操作截图:
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/78294/ns/;而第二个参数,则是你要在这个 Namespace 里运行的进程,比如 /bin/bash。
这段代码的的核心操作,则是通过 open() 系统调用打开了指定的 Namespace 文件,并把这个文件的描述符 fd 交给 setns() 使用。在 setns() 执行后,当前进程就加入了这个文件对应的 Linux Namespace 当中了。
将容器打包成镜像并上传docker hub
$ docker exec -it 4ddf4638572d /bin/sh
# 在容器内部新建了一个文件
root@4ddf4638572d:/app# touch test.txt
root@4ddf4638572d:/app# exit
# 将这个新建的文件提交到镜像中保存
$ docker commit 4ddf4638572d geektime/helloworld:v2