我们分别从Linux Namespace的隔离,Linux的Cgroups的限制,基于rootfs的文件系统三个角度,剖析了一个Linux容器的核心实现原理

我们拿一个实际的部署Docker的案例,来进行一次深入的总结和扩展,来看透Docker容器的本质,我们拿Docker部署一个python试下

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,

如果没有环境变量 NAME,就输出 Hello world

然后我们针对这个py文件,制作对应的Docker,相对来说,Docker制作现在来说很便捷了,直接使用Dockerfile即可

#使用官方提供Python开发镜像为基础

FROM python:2.7-slim

#切换工作目录

WORKDIR /app

#将目前目录下所有内容复制到 /app下

ADD . /app

#利用pip命令来在容器启动前安装应用依赖

RUN pip install –trusted-host pypi.python.org -r requirements.txt

#端口映射

EXPOSE 80

#设置所需的环境变量

ENV NAME HEAVEN

#设置容器进程

CMD [‘python”,”app.py”]

Dockerfile通过一些标准的原语,构建了需要的Docker环境

比如FROM原语,就是制定了 python这个官方的基础镜像,免去了安装python环境的操作

RUN在补充的安装了一些第三方类库

而且RUN 等于 执行制定shell命令的意思

WORKDIR,等于是制定了Dockerfile的操作目录

最后的CMD,就是Dockerfile指定执行的进程

而且Dockerfile,还有一个隐含的参数

ENTRYPOINT, /bin/sh -c 不指定ENTRYPOINT 时候,会在容器中实际运行 /bin/sh -c “python app.py”

我们统一称Docker的容器的启动进程为ENTRYPOINT,而不是CMD

然后,我们将上面的内容保存在一个Dockerfile的文件中

然后Docker就可以制作这个镜像了,在Dockerfile目录下执行

docker build -t helloword

-t就是给镜像加了一个Tag,就是一个镜像名字

Docker会找到这个目录下的dockerfile,然后进行执行

在执行Dockerfile的时候,每一个原语都会有一个对应的镜像层

build之后,就可以通过docker images来查看结果

docker image ls

图片

然后通过docker run启动容器

docker run -p 4000:80 helloword

在镜像名 helloworld 之后因为已经在file中指定了CMD,不需要加任何启动命令了

容器启动后,可以利用docker ps看到对应的容器

图片

这样,我们利用-p进行了端口映射,只要访问宿主机的4000端口,就可以看到容器返回的结果

这样,我们已经完成了一个应用的部署,如果想要上传,就直接利用DockerHub即可

直接Docekr push,就可以配合自己的docker hub账号

我们还可以利用docker commit的指令,讲一个正在运行的容器进行保存提交

我们可以先进入这个容器进行一些新的操作

docker exec -it container id /bin/sh

然后做些操作

然后进行提交保存

那么一个问题,docker如何进入的容器内部的呢?

首先说,Docker利用Namespace隔离的空间,那么只要我们进入Namespace就可以了

我们可以首先看一个Docker容器的进程号

docker inspect –format ‘{{.State.Pid}}’ 23738ead2182

获得了对应的PID

然后,我们查看宿主机的proc的文件,

图片

每一个Namespace都会在这个对应的ns文件夹下有一个对应的虚拟文件

这样,我们就可以加入到一个已经存在的Namespace当中的了

这就是Docker exec的原理

Docker操作真正依赖的是一个名为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”);

}

我们首先传入一个Namespace的文件路径,然后通过open()函数打开了Namespace的文件,交给了setnus使用,然后进程就相当于加入了指定的Linux Namespace

但当我们加入到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)

但是在这里面看网络设备的时候,可以看到的是,网卡数量变少了,说明我们加入了对应的Network Namespace中

并且,一旦一个新的进程加入了另一个Namespace中,宿主机的Namespace文件也会修改

我们看一下这个set ns 的程序的进程PID

并且看一下这个PID的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]

两者指向的Network Namespace文件一样,说明两个进程,共享了这个network Namespace

其实docker容器,也提供了类似的参数.可以启动容器并加入另一个容器的Network Namespace中中,使用参数 -net

使用代码如下

$ docker run -it –net container:4ddf4638572d busybox ifconfig

我们新启动的容器,就直接加入到另一个容器的Namesapce了,于是两者的容器信息一致

-net 如果指定为host,就意味着这个容器不会为进程启动Network Namespace,这就意味着,这个容器将会使用宿主机的网络栈

返回docker打包

如果使用了docker commit ,那么就是将容器的可读写层和只读层进行了一个打包,成为了一个新的镜像,只读层只在宿主机上共享,不会占用额外的空间

这样,就可以利用docker push的命令,将镜像打包传到docker hub的镜像上传系统了

如果想要自己的内部镜像上传系统,可以参考Docker Registry和VMware的Harbor项目

最后一个就是Docker的数据共享问题

就是容器内的新建文件,如何让宿主机获取到

宿主机上的文件和目录,如何让容器内的进程访问到

Docker Volume就是解决这个问题的,Volume机制,可以让宿主机上的指定目录,挂载到容器内进行修改和读取

Docker可以在启动的时候

将宿主机的目录挂载到容器内

docker run -v /test

docker run -v /hone:/test

这样,就是讲宿主机的目录挂载到了容器的 /test目录

第一种不指定宿主机目录,会在宿主机上创建一个临时目录 /var/lib/docker/volumnes/[VOLUME_ID]/_data

然后挂载到容器 /test目录下,第二种方式,则是直接的/home 挂载到/test目录下

Docker又是如何把宿主机上的目录挂载到容器内的呢?

我们需要注意,虽然有了Mount Namespace,但是在执行chroot之前,是可以看到整个文件系统的

目录

而宿主机上的文件系统,自然包含了我们的容器镜像,在启动后,会被联合挂载起来

我们只需要在rootfs准备好之前,将Volume制定的宿主机的目录,挂载到指定的容器目录在宿主机上对应的目录上,这个Volume的挂载工作就算完成了

我们这里的挂载,就是Linux的绑定挂载的机制 bind mount

可以将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上,这时候,这个挂载点上的任何操作,都只会发生在被挂载的目录或者文件上,原挂载点的内容会被隐藏起来’

绑定挂载是一个inode替换的流程,在Linux系统中,inode是存放文件内容的对象,目录是这个对象的外部指针

图片

整体来说,就是在一个合适的实际,Docker进行了一次挂载绑定,将宿主机的目录或者文件进行挂载

那么,这个/test目录内的内容,会不会被在docker commit的时候一起提交了呢?

这个问题大可放心,因为在Mount Namespace的作用下,宿主机不知道这个绑定挂载的存在,在宿主机看来,容器中读写层的/test目录

整体的验证流程如下

就是先声明一个Volume,挂载到容器的/test目录上

docker run -d -v /test helloworld

然后查看这个容器的Volume的 ID

$ docker volume ls

DRIVER              VOLUME NAME

local               cb1c2f7221fa9b0971cc35f68aa1034824755ac44a034c0c0a1dd318838d3a6d

然后根据这个ID,找到对应的Docker目录下的volumns路径

ls /var/lib/docker/volumns/cb1c2f7221fa9b0971cc35f68aa1034824755ac44a034c0c0a1dd318838d3a6d/_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

最后说下整体的Docker容器全景图

图片

整体结构如上,rootfs的最下层,是来自Docker的只读层,只读层之上,是Docker自己添加的init层,存放修改的一些配置文件

而rootfs的最上层是一个可读写层,以Copy-on-Write的方式存放任何只读层的修改

1.现在Docker容器有一个新的cgroup的Namespace,作用是什么呢?

2.执行docker run -v/home :/test的时候,如果容器的/test目录下本来就有内容,宿主机的/home目录下,也会出现这些内容,为啥

dockerfile使用sh启动Java,而不是exec启动的时候,若是通过docker stop,是不能停掉Java进程的,可以选择trap sig term kill

 

发表评论

邮箱地址不会被公开。 必填项已用*标注