Container(容器)是一种轻量级的虚拟化技术,它不需要模拟硬件创建虚拟机。在Linux系统里面,使用到Linux kernel的cgroups,namespace(ipc,network, user,pid,mount),capability等用于隔离运行环境和资源限制的技术,我们称之为容器。 容器技术早就出现。 非Linux容器技术 Linux容器技术 Solaris Zones Linux-Vserver BSD jails OpenVZ 虽然这些技术都已经成熟,但是这些解决方案还没有将它们的容器支持集成到主流 Linux 内核。总的来说,容器不等同于Docker,容器更不是虚拟机。 LXC项目由一个 Linux 内核补丁和一些 userspace 工具组成,它提供一套简化的工具来维护容器,用于虚拟环境的环境隔离、资源限制以及权限控制。LXC有点类似chroot,但是它比chroot提供了更多的隔离性。 Docker最初目标是做一个特殊的LXC的开源系统,最后慢慢演变为它自己的一套容器运行时环境。Docker基于Linux kernel的CGroups,Namespace,UnionFileSystem等技术封装成一种自定义的容器格式,用于提供一整套虚拟运行环境。毫无疑问,近些年来Docker已经成为了容器技术的代名词。 Docker Engine Docker提供了一个打包和运行应用的隔离环境,称之为容器,Docker的隔离和安全特性允许你在一个主机同时运行多个容器,而且它并不像虚拟机那样重量级,容器都是基于宿主机的内核运行的,它是轻量的,不管你运行的是ubuntu, debian还是其他Linux系统,用的内核都是宿主机内核。Docker提供了工具和平台来管理容器,而Docker Engine则是一个提供了大部分功能组件的CS架构的应用,如架构图所示,Docker Engine负责管理镜像,容器,网络以及数据卷等。 Docker架构 Docker更详细的架构如图所示,采用CS架构,client通过RESTFUL API发送docker命令到docker daemon进程,docker daemon进程执行镜像编译,容器启停以及分发,数据卷管理等,一个client可以与多个docker daemon通信。 Docker Daemon:Docker后台进程,用于管理镜像,容器以及数据卷。 Docker Client:用于与Docker Daemon交互。 Docker Registry:用于存储Docker镜像,类似github,公共的Registry有Docker Hub和Docker Cloud。 Images:镜像是用于创建容器的一种只读模板。镜像通常基于一个基础镜像,在此基础上安装额外的软件。比如你的nginx镜像可能基于debian然后安装nginx并添加配置,你可以从Docker Hub上拉取已有的镜像或者自己通过Dockerfile来编译一个镜像。 Containers:容器是镜像的一个可运行示例,我们可通过Docker client或者API来创建,启停或者删除容器。默认情况下,容器与宿主机以及其他容器已经隔离,当然你可以控制隔离容器的网络或者存储的方式。 Services:服务是docker swarm引入的概念,可以用于在多宿主机之间伸缩容器数目,支持负载均衡以及服务路由功能。 Docker底层技术概览 通过下面命令运行一个debian容器,attach到一个本机的命令行并运行/bin/bash。 docker run -i -t debian /bin/bash 这个命令背后都做了什么? 如果本机没有debian镜像,则会从你配置的Registry里面拉取一个debian的latest版本的镜像,跟你运行了docker pull debian效果一样。 创建容器。跟运行docker create一样。 给容器分配一个读写文件系统作为该容器的final layer,容器可以在它的文件系统创建和修改文件。 Docker为容器创建了一套网络接口,给容器分配一个ip。默认情况下,容器可以通过默认网络连通到外部网络。 Docker启动容器并执行 /bin/bash。因为启动时指定了-i -t参数,容器是以交互模式运行且attach到本地终端,我们可以在终端上输入命令并看到输出。 运行exit可以退出容器,但是此时容器并没有被删除,我们可以再次运行它或者删除它。 可以发现,容器的内核版本是跟宿主机一样的,不同的是容器的主机名是独立的,它默认用容器ID做主机名。 我们运行ps -ef可以发现容器进程是隔离的,容器里面看不到宿主机的进程,而且它自己有PID为1的进程。 此外,网络也是隔离的,它有独立于宿主机的IP。 文件系统也是隔离的,容器有自己的系统和软件目录,修改容器内的文件并不影响宿主机对应目录的文件。 root@stretch:/home/vagrant# uname -r4.9.0-6-amd64 root@stretch:/home/vagrant# docker run -it --name demo alpine /bin/ash/ # uname -r ## 容器内4.9.0-6-amd64/ # ps -efPID USER TIME COMMAND 1 root 0:00 /bin/ash 7 root 0:00 ps -ef / # ip a1 : lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever 这些隔离机制并不是Docker新开发的技术,而是依托Linux kernel以及一些已有的技术实现的,主要包括: Linux Namespaces(Linux2.6.24后引入):命名空间用于进程(PID)、网络(NET)、挂载点(MNT)、UTS、IPC等隔离。 Linux Control Groups(CGroups):用于限制容器使用的资源,包括内存,CPU等。 Union File Systems:UnionFS把多个目录结合成一个目录,对外使用,最上层目录为读写层(通常只有1个),下面可以有一个或多个只读层,见容器和镜像分层图。Docker支持OverlayFS,AUFS、DeviceMapper、btrfs等联合文件系统。 Container Format: Docker Engine组合Namespaces,CGroups以及UnionFS包装为一个容器格式,默认格式为libcontainer,后续可能会加入BSD Jails 或 Solaris Zones容器格式的支持。 Namespace Namespaces用于环境隔离,Linux kernel支持的Namespace包括: namespace|系统调用参数|隔离内容 ---|---|--- UTS|CLONE_NEWUTS|主机名与域名 IPC|CLONE_NEWIPC|信号量,消息队列和共享内存 PID|CLONE_NEWPID|进程编号 Network|CLONE_NEWNET|网络设备,网络栈,端口等 Mount|CLONE_NEWNS|挂载点(文件系统) User|CLONE_NEWUSER|用户和用户组 CGROUP等 Linux内核实现namespace的主要目的之一是实现轻量级虚拟化(容器)服务。在同一个namespace下的进程可以感知彼此的变化,而对外界进程一无所知。这样就可以让容器中的进程产生错觉,仿佛自己置身于一个独立的系统环境中,以达到独立和隔离的目的。 查看一个进程的各Namespace命令如下: [root@tdh-18 ~]# ls -ls /proc/self/ns/ total 0 0 lrwxrwxrwx. 1 root root 0 Jul 6 13:50 ipc -> ipc:[4026531839] 0 lrwxrwxrwx. 1 root root 0 Jul 6 13:50 mnt -> mnt:[4026531840] 0 lrwxrwxrwx. 1 root root 0 Jul 6 13:50 net -> net:[4026531956] 0 lrwxrwxrwx. 1 root root 0 Jul 6 13:50 pid -> pid:[4026531836] 0 lrwxrwxrwx. 1 root root 0 Jul 6 13:50 user -> user:[4026531837] 0 lrwxrwxrwx. 1 root root 0 Jul 6 13:50 uts -> uts:[4026531838] PID Namespace 在容器中,有自己的Pid namespace,因此我们看到的只有PID为1的初始进程以及它的子进程,而宿主机的其他进程容器内是看不到的。 通常来说,Linux启动后它会先启动一个PID为1的进程,这是系统进程树的根进程,根进程会接着创建子进程来初始化系统服务。 PID namespace允许在新的namespace创建一棵新的进程树,它可以有自己的PID为1的进程。在PID namespace的隔离下,子进程名字空间无法知道父进程名字空间的进程,如在Docker容器中无法看到宿主机的进程,而父进程名字空间可以看到子进程名字空间的所有进程。如图所示: Linux内核加入PID Namespace后,对pid结构进行了修改,新增的upid结构用于跟踪namespace和pid。 // 加入PID Namespace之前的pid结构 struct pid { atomic_t count; /* reference counter */ int nr; /* the pid value */ struct hlist_node pid_chain; /* hash chain */ ... }; // 加入PID Namespace之后的pid结构 struct upid { int nr; /* moved from struct pid */ struct pid_namespace *ns; struct hlist_node pid_chain; /* moved from struct pid */ }; struct pid { ... int level; /* the number of upids */ struct upid numbers[0]; }; 可以通过unshare测试下PID namespace,可以看到新的bash进程它的pid namespace与父进程的不同了,而且它的pid是1。 NS Namespace NS Namespace用于隔离挂载点,不同NS Namespace的挂载点互不影响。创建一个新的Mount Namespace效果有点类似chroot,不过它隔离的比chroot更加完全。 这是历史上的第一个Linux Namespace,由此得到了 NS 这个名字而不是用的 Mount。 在最初的NS Namespace版本中,挂载点是完全隔离的。初始状态下,子进程看到的挂载点与父进程是一样的。 在新的Namespace中,子进程可以随意mount/umount任何目录,而不会影响到父Namespace。 使用NS Namespace完全隔离挂载点初衷很好,但是也带来了某些情况下不方便,比如我们新加了一块磁盘,如果完全隔离则需要在所有的Namespace中都挂载一遍。为此,Linux在2.6.15版本中加入了一个shared subtree特性,通过指定Propagation来确定挂载事件如何传播。 通过指定MS_SHARED来允许在一个peer group(子namespace 和父namespace就属于同一个组)共享挂载点,mount/umount事件会传播到peer group成员中。 使用MS_PRIVATE不共享挂载点和传播挂载事件。 其他还有MS_SLAVE和NS_UNBINDABLE等选项。 可以通过查看cat /proc/self/mountinfo来看挂载点信息,若没有传播参数则为MS_PRIVATE的选项。 例如,在初始namespace有两个挂载点: 通过mount --make-shared /dev/sda1 /mntS设置/mntS为shared类型, mount --make-private /dev/sda1 /mntP设置/mntP为private类型。 当使用unshare -m bash新建一个namespace并在它们下面挂载子目录时,可以发现: /mntS下面的子目录mount/umount事件会传播到父namespace 而/mntP则不会 在前面例子Pid namespace隔离后,我们在新的名字空间执行 ps -ef可以看到宿主机进程,++这是因为ps命令是从 /proc 文件系统读取的数据++,而文件系统我们还没有隔离,为此,我们需要在新的 NS Namespace重新挂载 proc 文件系统来模拟类似Docker容器的功能。 root@stretch:/home/vagrant# unshare --pid --fork --mount-proc bash root@stretch:/home/vagrant# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 15:36 pts/1 00:00:00 bash root 2 1 0 15:36 pts/1 00:00:00 ps -ef 可以看到,隔离了NS namespace并重新挂载了proc后,ps命令只能看到2个进程了,跟我们在Docker容器中看到的一致。 NET Namespace Docker容器中另一个重要特性是网络独立(++之所以不用隔离一词是因为容器的网络还是要借助宿主机的网络来通信的++),使用到Linux 的 NET Namespace以及veth。 veth主要的目的是为了跨NET namespace之间提供一种类似于Linux进程间通信的技术,所以veth总是成对出现,如下面的veth0和veth1。它们位于不同的NET namespace中,在veth设备任意一端接收到的数据,都会从另一端发送出去。 veth实现了不同namespace的网络数据传输。 在Docker中,宿主机的veth端会桥接到网桥中,接收到容器中的veth端发过来的数据后会经由网桥docker0再转发到宿主机网卡eth0,最终通过eth0发送数据。 在发送数据前,需要经过iptables MASQUERADE规则将源地址改成宿主机ip,这样才能接收到响应数据包。 宿主机网卡接收到的数据会通过iptables DNAT根据端口号修改目的地址和端口为容器的ip和端口,然后根据路由规则发送到网桥docker0中,并最终由网桥docker0发送到对应的容器中。 Docker里面网络模式分为bridge,host,overlay等几种模式, 默认是采用bridge模式网络如图所示。 如果使用host模式,则不隔离直接使用宿主机网络。 overlay网络则是更加高级的模式,可以实现跨主机的容器通信。 USER Namespace user namespace用于隔离用户和组信息,在不同的namespace中用户可以有相同的 UID 和 GID,它们之间互相不影响。 父子namespace之间可以进行用户映射,如父namespace(宿主机)的普通用户映射到子namespace(容器)的root用户,以减少子namespace的root用户操作父namespace的风险。 创建新的user namespace之后第一步就是设置好user和group的映射关系。这个映射通过设置/proc/PID/uid_map(gid_map)实现,格式如下 ID-inside-ns ID-outside-ns length ID-inside-ns是容器内的uid/gid ID-outside-ns则是容器外映射的真实uid/gid 比如0 1000 1,表示将真实的uid=1000映射为容器内的uid=0,length为映射的范围。 不是所有的进程都能随便修改映射文件的,必须同时具备如下条件: 修改映射文件的进程必须有PID进程所在user namespace的CAP_SETUID/CAP_SETGID权限。 修改映射文件的进程必须是跟PID在同一个user namespace或者PID的父namespace。 映射文件uid_map和gid_map只能写入一次,再次写入会报错。 docker1.10之后的版本可以通过在docker daemon启动时加上--userns-remap=[USERNAME]来实现USER Namespace的隔离。 我们指定了username=ssj启动dockerd,查看subuid文件可以发现ssj映射的uid范围是165536到165536+65536= 231072,而且在docker目录下面对应ssj有一个独立的目录165536.165536存在。 root@stretch:/home/vagrant# cat /etc/subuid vagrant:100000:65536 ssj:165536:65536 root@stretch:/home/vagrant# ls /var/lib/docker/165536.165536/ builder/ containerd/ containers/ image/ network/ ... 运行docker images -a等命令可以发现在启用user namespace之前的镜像都看不到了。此时只能看到在新的user namespace里面创建的docker镜像和容器。而此时我们创建一个测试容器,可以在容器外看到容器进程的uid_map已经设置为ssj,这样容器中的root用户映射到宿主机就是ssj这个用户了,此时如果要删除我们挂载的/bin目录中的文件,会提示没有权限,增强了安全性。 dockerd 启动时加了 --userns-remap=ssj root@stretch:/home/vagrant# docker run -it -v /bin:/host/bin --name demo alpine /bin/ash # rm /host/bin/which rm: remove '/host/bin/which'? y rm: can't remove '/host/bin/which': Permission denied 宿主机查看容器进程uid_map文件 root@stretch:/home/vagrant# CPID=`ps -ef|grep '/bin/ash'|awk '{printf $2}'` root@stretch:/home/vagrant# cat /proc/$CPID/uid_map 0 165536 65536 UTS namespace UTS namespace用于隔离主机名等。可以看到在新的uts namespace修改主机名并不影响原namespace的主机名。 root@stretch:/home/vagrant# unshare --uts --fork bash root@stretch:/home/vagrant# hostname stretch root@stretch:/home/vagrant# hostname modified root@stretch:/home/vagrant# hostname modified root@stretch:/home/vagrant# exit root@stretch:/home/vagrant# hostname stretch IPC Namespace IPC Namespace用于隔离IPC消息队列等。可以看到,新老ipc namespace的消息队列互不影响。 root@stretch:/home/vagrant# ipcmk -Q Message queue id: 0 root@stretch:/home/vagrant# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 0x26c3371c 0 root 644 0 0 root@stretch:/home/vagrant# unshare --ipc --fork bash root@stretch:/home/vagrant# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages CGROUP Namespace CGROUP Namespace是Linux4.6以后才支持的新namespace。 容器技术使用namespace和cgroup实现环境隔离和资源限制,但是对于cgroup本身并没有隔离。 没有cgroup namespace前,容器中一旦挂载cgroup文件系统,便可以修改整全局的cgroup配置。 有了cgroup namespace后,每个namespace中的进程都有自己的cgroup文件系统视图,增强了安全性,同时也让容器迁移更加方便。 CGroups Linux CGroups用于资源限制,包括限制CPU、内存、blkio以及网络等。 通过工具cgroup-bin (sudo apt-get install cgroup-bin)可以创建CGroup并进入该CGroup执行命令。 root@stretch:/home/vagrant# cgcreate -a vagrant -g cpu:cg1 root@stretch:/home/vagrant# ls /sys/fs/cgroup/cpu/cg1/ cgroup.clone_children cpu.cfs_period_us cpu.shares cpuacct.stat cpuacct.usage_all cpuacct.usage_percpu_sys cpuacct.usage_sys notify_on_release cgroup.procs cpu.cfs_quota_us cpu.stat cpuacct.usage cpuacct.usage_percpu cpuacct.usage_percpu_user cpuacct.usage_user tasks cpu.cfs_period_us 和 cpu.cfs_quota_us,它们分别用来限制该组中的所有进程在单位时间里可以使用的 cpu 时间,这里的 cfs(Completely Fair Scheduler) 是完全公平调度器的意思。 cpu.cfs_period_us是时间周期,默认为100000,即100毫秒。 而cpu.cfs_quota_us是在时间周期内可以使用的时间,默认为-1即无限制。 cpu.shares用于限制cpu使用的,它用于控制各个组之间的配额。 比如组cg1的cpu.shares为1024,组cg2的cpu.shares也是1024,如果都有进程在运行则它们都可以使用最多50%的限额。如果cg2组内进程比较空闲,那么cg1组可以将使用几乎整个cpu,tasks存储的是该组里面的进程ID。++(注:debian8默认没有cfs和memory cgroup支持,需要重新编译内核及修改启动参数,debian9默认已经支持)++ 我们先在默认的分组里面运行一个死循环程序loop.py,因为默认分组/sys/fs/cgroup/cpu/cpu.cfs_period_us和cfs_quota_us是默认值,所以是没有限制 cpu 使用的。可以发现1个cpu us立马接近100%了。 # loop.py while True: pass 设置cg1组的cfs_quota_us位50000,即表示该组内进程最多使用50%的cpu时间,运行cgexec命令进入cg1的cpu组,然后运行loop.py,可以发现cpu us在50%以内了,此时也可以在tasks文件中看到我们刚刚cgexec创建的进程ID。 root@stretch:/home/vagrant# echo 50000 > /sys/fs/cgroup/cpu/cg1/cpu.cfs_quota_us root@stretch:/home/vagrant# cgexec -g cpu:cg1 /bin/bash Docker里面要限制内存和CPU使用,可以在启动时指定相关参数即可。 限制cpu使用率,加cpu-period和cpu-quota参数,限制执行的cpu核,加--cpuset-cpus参数。 限制内存使用,加--memory参数。 可以看到在 /sys/fs/cgroup/cpu/docker/目录下有个以containerid为名的分组,该分组下面的 cpu.cfs_period_us和cpu.cfs_quota_us的值就是我们在启动容器时指定的值。 root@stretch:/home/vagrant# docker run -i -t --cpu-period=100000 --cpu-quota=50000 --memory=512000000 alpine /bin/ash Capabilities 我们在启动容器时会时常看到这样的参数--cap-add=NET_ADMIN,这是用到了Linux的capability特性。capability是为了实现更精细化的权限控制而加入的。 我们以前熟知通过设置文件的SUID位,这样非root用户的可执行文件运行后的euid会成为文件的拥有者ID,比如passwd命令运行起来后有root权限,有SUID权限的可执行文件如果存在漏洞会有安全风险。(查看文件的capability的命令为 filecap -a,而查看进程capability的命令为 pscap -a,pscap和filecap工具需要安装 libcap-ng-utils这个包)。 对于capability,可以看一个简单的例子便于理解。如Debian系统中自带的ping工具,它是有设置SUID位的。这里拷贝ping重命名为anotherping,anotherping的SUID位没有设置,运行会提示权限错误。这里,我们只要将其加上 cap_net_raw权限即可,不需要设置SUID位那么大的权限。 vagrant@stretch:~$ ls -ls /bin/ping 60 -rwsr-xr-x 1 root root 61240 Nov 10 2016 /bin/ping vagrant@stretch:~$ cp /bin/ping anotherping vagrant@stretch:~$ ls -ls anotherping 60 -rwxr-xr-x 1 vagrant vagrant 61240 May 19 10:18 anotherping vagrant@stretch:~$ ./anotherping -c1 yue.uu.163.com ping: socket: Operation not permitted vagrant@stretch:~$ sudo setcap cap_net_raw+ep ./anotherping vagrant@stretch:~$ ./anotherping -c1 yue.uu.163.com PING yue.uu.163.com (59.111.137.252) 56(84) bytes of data. 64 bytes from 59.111.137.252 (59.111.137.252): icmp_seq=1 ttl=63 time=53.9 ms --- yue.uu.163.com ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 53.919/53.919/53.919/0.000 ms Union File System UnionFS(联合文件系统)简单来说就是支持将不同的目录挂载到同一个目录中的技术。 Docker支持的UnionFS包括: OverlayFS AUFS devicemapper vfs btrfs等 查看UnionFS版本可以用docker info查看对应输出中的Storage项即可,早期的Docker版本用AUFS和devicemapper居多,新版本Docker在Linux3.18之后版本基本默认用OverlayFS。 OverlayFS与早期用过的AUFS类似,不过它比AUFS更简单,读写性能更好,在docker-ce18.03版本中默认用的存储驱动是overlay2,老版本overlay官方已经不推荐使用。 它将两个目录upperdir和lowdir联合挂载到一个merged目录,提供统一视图。 其中upperdir是可读写层,对容器修改写入在该目录中,它也会隐藏lowerdir中相同的文件。 而lowdir是只读层,Docker镜像在这层。 image 在看Docker镜像和容器存储结构前,可以先简单操作下OverlayFS看下基本概念。创建了lowerdir和upperdir两个目录,然后用overlayfs挂载到merged目录,这样在merged目录可以看到两个目录的所有文件 both.txt 和only.txt。 其中upperdir是可读写的,而lowerdir只读。通过merged目录来操作文件可以发现: 读取文件时,如果upperdir不存在该文件,则会从lowerdir直接读取。 修改文件时并不影响lowerdir中的文件,因为它是只读的。 如果修改的文件在upperdir不存在,则会从lowerdir拷贝到upperdir,然后在upperdir里面修改该文件,并不影响lowerdir目录的文件。 删除文件则是将upperdir中将对应文件设置成了c类型,即字符设备类型来隐藏已经删除的文件(与AUFS创建一个whiteout文件略有不同)。 root@stretch:/home/vagrant/overlaytest# tree -a . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged |-- upperdir | `-- both.txt `-- workdir `-- work 5 directories, 3 files root@stretch:/home/vagrant/overlaytest# mount -t overlay overlay -olowerdir=./lowerdir,upperdir=./upperdir,workdir=./workdir ./merged root@stretch:/home/vagrant/overlaytest# tree . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | |-- both.txt | `-- only.txt |-- upperdir | `-- both.txt `-- workdir `-- work 5 directories, 5 files root@stretch:/home/vagrant/overlaytest# tree -a . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | |-- both.txt | `-- only.txt |-- upperdir | `-- both.txt `-- workdir `-- work 5 directories, 5 files root@stretch:/home/vagrant/overlaytest# echo "modified both" > merged/both.txt root@stretch:/home/vagrant/overlaytest# cat upperdir/both.txt modified both root@stretch:/home/vagrant/overlaytest# cat lowerdir/both.txt lower both.txt root@stretch:/home/vagrant/overlaytest# echo "modified only" > merged/only.txt root@stretch:/home/vagrant/overlaytest# tree . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | |-- both.txt | `-- only.txt |-- upperdir | |-- both.txt | `-- only.txt `-- workdir `-- work 5 directories, 6 files root@stretch:/home/vagrant/overlaytest# cat upperdir/only.txt modified only root@stretch:/home/vagrant/overlaytest# cat lowerdir/only.txt lower only.txt root@stretch:/home/vagrant/overlaytest# tree -a . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | |-- both.txt | `-- only.txt |-- upperdir | |-- both.txt | `-- only.txt `-- workdir `-- work 5 directories, 6 files root@stretch:/home/vagrant/overlaytest# rm merged/both.txt root@stretch:/home/vagrant/overlaytest# tree -a . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | `-- only.txt |-- upperdir | |-- both.txt | `-- only.txt `-- workdir `-- work root@stretch:/home/vagrant/overlaytest# ls -ls upperdir/both.txt 0 c--------- 1 root root 0, 0 May 19 02:31 upperdir/both.txt 回到Docker里面,拉取一个nginx镜像,有三层镜像,可以看到在overlay2对应每一层都有个目录(注意,这个目录名跟镜像层名从docker1.10版本后名字已经不对应了),另外的l目录是指向镜像层的软链接。 最底层存储的是基础镜像debian/alpine, 上一层是安装了nginx增加的可执行文件和配置文件, 最上层是链接/dev/stdout到nginx日志文件。 每个子目录下面: diff目录用于存储镜像内容 work目录是OverlayFS内部使用的 link文件存储的是该镜像层对应的短名称 lower文件存储的是下一层的短名称 root@stretch:/home/vagrant# docker pull nginx Using default tag: latest latest: Pulling from library/nginx f2aa67a397c4: Pull complete 3c091c23e29d: Pull complete 4a99993b8636: Pull complete Digest: sha256:0fb320e2a1b1620b4905facb3447e3d84ad36da0b2c8aa8fe3a5a81d1187b884 Status: Downloaded newer image for nginx:latest root@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/ total 16 4 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0 4 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554 4 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe 4 drwx------ 2 root root 4096 May 19 04:17 l root@stretch:/var/lib/docker/overlay2# ls 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0/ diff link lower work 三层中 f311是最顶层,下面分别是0949和8af9这两层。 root@stretch:/var/lib/docker/overlay2# cat f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe/lower l/7B2WM6DC226TCJU6QHJ4ABKRI6:l/4FHO2G5SWWRIX44IFDHU62Z7X2 root@stretch:/var/lib/docker/overlay2# cat 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0/lower l/4FHO2G5SWWRIX44IFDHU62Z7X2 root@stretch:/var/lib/docker/overlay2# cat 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554/link 4FHO2G5SWWRIX44IFDHU62Z7X2 此时启动一个nginx容器,可以看到overlay2目录多了两个目录,多出来的就是容器层的目录和只读的容器init层。 容器目录下面的merged就是我们前面提到的联合挂载目录了,而lowdir则是它下层目录。 而容器init层用来存储与这个容器内环境相关的内容,如 /etc/hosts和/etc/resolv.conf文件,它居于其他镜像层之上,容器层之下。 root@stretch:/var/lib/docker/overlay2# docker run -idt --name nginx nginx 01a873eeba41f00a5a3deb083adf5ed892c55b4680fbc2f1880e282195d3087b root@stretch:/var/lib/docker/overlay2# ls -ls 4 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0 4 drwx------ 5 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1 4 drwx------ 4 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1-init 4 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554 4 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe 4 drwx------ 2 root root 4096 May 19 09:11 l root@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/ 4 drwxr-xr-x 4 root root 4096 May 19 09:11 diff 4 -rw-r--r-- 1 root root 26 May 19 09:11 link 4 -rw-r--r-- 1 root root 115 May 19 09:11 lower 4 drwxr-xr-x 1 root root 4096 May 19 09:11 merged 4 drwx------ 3 root root 4096 May 19 09:11 work root@stretch:/var/lib/docker/overlay2# ls 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/merged/ bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var root@stretch:/var/lib/docker/overlay2# ls 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/diff/ run var 如果我们在容器中修改文件,则会反映到容器层的merged目录相关文件,容器层的diff目录相当于upperdir,其他层是lowerdir。 如果之前容器层diff目录不存在该文件,则会拷贝该文件到diff目录并修改。 读取文件时,如果upperdir目录找不到,则会直接从下层的镜像层中读取。
容器的数据管理操作: 对数据进行持久化 多个容器之间进行数据共享 容器中数据管理的两种方式: 数据卷(Data Volume):容器内数据直接映射到本地主机环境 数据卷容器(Data Volume Containers):使用特定容器维护数据卷 数据卷 可供容器使用的特殊目录,将主机操作系统目录直接映射进容器,类似于Linux的mount操作 数据卷提供的特性: 数据卷可以在容器之间重用和共享,容器见传递数据变的高效方便 对数据卷内的数据进行修改会立马见效,无论是在容器内操作还是在本地操作 对数据卷的更新不会影响到镜像,解耦了应用和数据 数据卷会一直存在直到没有容器使用,可以安全的卸载 在容器内创建数据卷示例: docker run -d -P --name web -v /webapp training/webapp python app.py //创建了一个数据卷挂载到/webapp目录 挂载一个主机目录作为数据卷 docker run -d -P --name web -v /src/webapp:/webapp:ro training/webapp python app.py //加载主机的/src/webapp目录到容器的/webapp目录 //主机目录必须是绝对路径,如果目录不存在Docker会自动创建 //挂载的目录的权限默认为rw,追加ro修改目录权限 挂载一个主机文件作为数据卷 docker run -d -P --name web -v ~/.bash_history:/.bash_history ubuntu /bin/bash //可以记录在容器中输入过的历史命令 数据卷容器 如果需要在多个容器之间共享一些持续更新的数据,可以使用数据卷容器。 数据卷容器是容器的一种,专门用来提供数据卷供其他容器挂载。 创建数据卷容器 docker run -it -v /dbdata --name dbdata ububtu //在容器中创建了一个数据卷挂载在/dbdata目录 在其他容器中使用--volumes-from来挂载dbadatra容器中的数据卷 docker run -it --volumes-from dbdata --name d1 ububtu docker run -it --volumes-from dbdata --name d2 ububtu //此时容器d1和d2都挂载同一个数据卷到相同的/dbdata目录 //三个容器中任何一个在该目录下的写入,其他容器也都可以看到 注意: 多次使用--volumes-from参数来从多个容器挂载多个数据卷 可以从其他已经挂载了数据卷的容器来挂在数据卷 使用--volumes-from参数所挂载数据卷的容器自身并不需要保持在运行状态 删除挂载的容器,数据卷不会被删除,需要删除数据卷的时候,需要在最后一个挂载数据卷的容器中显式的删除使用 docker rm -v 指定。 使用数据卷容器可以让用户在容器之间自由地升级和移动数据 利用数据卷容器迁移数据 备份数据卷容器中的数据 docker run --volume-from dbdata -v ${pwd}:/backup --name worker ubuntu tar cvf /backup/backup.tar /dbdata1. 利用Ubuntu镜像创建容器worker 2. worker容器挂载dbdata数据卷容器中的数据卷/dbdata 3. 挂载本地目录到容器中的/backup目录 4. 容器启动后执行tar cvf /backup/backup.tar /dbdata,将/dbdata目录中的数据保存到/backup,即宿主机的当前目录 恢复数据到容器中 docker run --volumr-from dbdata -v ${pwd}:/backup busybox tar xvf /backup/backup.tar1. 创建新的容器挂载数据卷容器dbdata中的/dbdata数据卷 2. 挂载本机当前路径到容器的/backup目录 3. 容器启动后执行解压缩命令将之前备份的数据恢复出来
一句话概括容器:容器就是将软件打包成标准化单元,以用于开发、交付和部署。 容器镜像是轻量的、可执行的独立软件包 ,包含软件运行所需的所有内容:代码、运行时环境、系统工具、系统库和设置。 容器化软件适用于基于Linux和Windows的应用,在任何环境中都能够始终如一地运行。 容器赋予了软件独立性 ,使其免受外在环境差异(例如,开发和预演环境的差异)的影响,从而有助于减少团队间在相同基础设施上运行不同软件时的冲突。 再来看看容器较为通俗的解释 如果需要通俗的描述容器的话,我觉得容器就是一个存放东西的地方,就像书包可以装各种文具、衣柜可以放各种衣服、鞋架可以放各种鞋子一样。我们现在所说的容器存放的东西可能更偏向于应用比如网站、程序甚至是系统环境。 什么是Docker 下面我通过四点向你说明Docker到底是个什么东西。 Docker 是世界领先的软件容器平台。 Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核 的cgroup,namespace,以及AUFS类的UnionFS等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。 由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。Docke最初实现是基于 LXC. Docker 能够自动执行重复性任务,例如搭建和配置开发环境,从而解放了开发人员以便他们专注在真正重要的事情上:构建杰出的软件。 用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。 Docker思想 集装箱 标准化: ①运输方式 ② 存储方式 ③ API接口 隔离 Docker容器的特点 轻量 在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。 标准 Docker 容器基于开放式标准,能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。 安全 Docker 赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认提供最强的隔离,因此应用出现问题,也只是单个容器的问题,而不会波及到整台机器。 为什么要用Docker Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题;——一致的运行环境 可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。——更快速的启动时间 避免公用的服务器,资源会容易受到其他用户的影响。——隔离性 善于处理集中爆发的服务器使用压力;——弹性伸缩,快速扩展 可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。——迁移方便 使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。——持续交付和部署 容器 VS 虚拟机 简单来说: 容器和虚拟机具有相似的资源隔离和分配优势,但功能有所不同,因为容器虚拟化的是操作系统,而不是硬件,因此容器更容易移植,效率也更高。 两者对比图 传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便. 容器与虚拟机 (VM) 总结 容器是一个应用层抽象,用于将代码和依赖资源打包在一起。 多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行 。与虚拟机相比, 容器占用的空间较少(容器镜像大小通常只有几十兆),瞬间就能完成启动 。 虚拟机 (VM) 是一个物理硬件层抽象,用于将一台服务器变成多台服务器。 管理程序允许多个 VM 在一台机器上运行。每个VM都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此 占用大量空间 。而且 VM 启动也十分缓慢 。 通过Docker官网,我们知道了这么多Docker的优势,但是大家也没有必要完全否定虚拟机技术,因为两者有不同的使用场景。虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 Docker通常用于隔离不同的应用 ,例如前端,后端以及数据库。 容器与虚拟机 (VM)两者是可以共存的 就我而言,对于两者无所谓谁会取代谁,而是两者可以和谐共存。 Docker基本概念 Docker 包括三个基本概念: 镜像(Image) 容器(Container) 仓库(Repository) 理解了这三个概念,就理解了 Docker 的整个生命周期 镜像(Image)——一个特殊的文件系统 操作系统分为: 内核 用户空间。 对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而Docker 镜像(Image),就相当于是一个 root 文件系统。 Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。 镜像不包含任何动态数据,其内容在构建之后也不会被改变。 Docker 设计时,就充分利用 Union FS的技术,将其设计为 分层存储的架构 。 镜像实际是由多层文件系统联合组成。 镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。 比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。 分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。 容器(Container)——镜像运行时的实体 镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等 。 容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。前面讲过镜像使用的是分层存储,容器也是如此。 容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。 按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据 ,++容器存储层要保持无状态化++。所有的文件写入操作,都应该使用数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。 数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此, 使用数据卷后,容器可以随意删除、重新 run ,数据却不会丢失。 仓库(Repository)——集中存放镜像文件的地方 镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry就是这样的服务。 一个 Docker Registry中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。所以说:镜像仓库是Docker用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。 通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。我们可以通过 <仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签.。 这里补充一下Docker Registry 公开服务和私有 Docker Registry的概念 Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。 最常使用的 Registry 公开服务是官方的 Docker Hub ,这也是默认的 Registry,并拥有大量的高质量的官方镜像,网址为:https://hub.docker.com/ 。在国内访问Docker Hub 可能会比较慢国内也有一些云服务商提供类似于 Docker Hub 的公开服务。 如 时速云镜像库 网易云镜像服务 DaoCloud 镜像市场 阿里云镜像库等 除了使用公开服务外,用户还可以在 本地搭建私有 Docker Registry 。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 docker 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。 Build, Ship, and Run “Docker - Build, Ship, and Run Any App, Anywhere” Build(构建镜像) : 镜像就像是集装箱包括文件以及运行环境等等资源。 Ship(运输镜像) :主机和仓库间运输,这里的仓库就像是超级码头一样。 Run (运行镜像) :运行的镜像就是一个容器,容器就是运行程序的地方。 Docker 运行过程也就是去仓库把镜像拉到本地,然后用一条命令把镜像运行起来变成容器。所以,我们也常常将Docker称为码头工人或码头装卸工,这和Docker的中文翻译搬运工人如出一辙。
Docker统一文件系统(the union file system)** 容器(container)和镜像(image)之间的区别 容器和运行中的容器之间的区别 Image(镜像) Definition 镜像(Image)就是一堆只读层(read-only layer)的统一视角,下面的这张图能够帮助理解镜像的定义 从左边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。 这些层是Docker内部的实现细节,并且能够在主机(运行Docker的机器)的文件系统上访问到。 统一文件系统(union file system)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。我们可以在图片的右边看到这个视角的形式。 可以在主机文件系统上找到有关这些层的文件。需要注意的是,在一个运行中的容器内部,这些层是不可见的。它们存在于/var/lib/docker/aufs目录下。 sudo tree -L 1 /var/lib/docker/ /var/lib/docker/ ├── aufs ├── containers ├── graph ├── init ├── linkgraph.db ├── repositories-aufs ├── tmp ├── trust └── volumes 7 directories, 2 files Container(容器) Definition 容器(container)的定义和镜像(image)几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。 容器的定义并没有提及容器是否在运行。 要点:==容器 = 镜像 + 读写层==。并且容器的定义并没有提及是否要运行容器。 Running Container Definition 一个运行态容器(running container)被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程。 ++正是文件系统隔离技术使得Docker成为了一个前途无量的技术++。 一个容器中的进程可能会对文件进行修改、删除、创建,这些改变都将作用于可读写层(read-write layer)。下面这张图展示了这个行为。 可以通过运行以下命令来验证上面所说的: docker run ubuntu touch happiness.txt find / -name happiness.txt /var/lib/docker/aufs/diff/860a7b...889/happiness.txt 即便是这个ubuntu容器不再运行,依旧能够在主机的文件系统上找到这个新文件。 Image Layer Definition 为了将零星的数据整合起来,提出了镜像层(image layer)这个概念。下面的这张图描述了一个镜像层,通过图片能够发现一个层并不仅仅包含文件系统的改变,它还能包含了其他重要信息。元数据(metadata)就是关于这个层的额外信息,++它不仅能够让Docker获取运行和构建时的信息,还包括父层的层次信息++。需要注意,==只读层和读写层都包含元数据==。 除此之外,每一层都包括了一个指向父层的指针。如果一个层没有这个指针,说明它处于最底层。 Metadata Location 在主机上,镜像层(image layer)的元数据被保存在名为”json”的文件中,比如说: /var/lib/docker/graph/e809f156dc985.../json //e809f156dc985... 就是这层的id 一个容器的元数据好像是被分成了很多文件,但或多或少能够在/var/lib/docker/containers/目录下找到,就是一个可读层的id。这个++目录下的文件大多是运行时的数据++,比如说网络,日志等等。 全局理解(Tying It All Together) 结合上面提到的实现细节来理解Docker的命令 docker create 命令为指定的镜像(image)添加了一个可读写层,构成了一个新的容器。注意,这个容器并没有运行。 docker create <image-id> Docker start命令为容器文件系统创建了一个进程隔离空间。注意,每一个容器只能够有一个进程隔离空间。 docker start <container-id> docker run docker run <image-id> docker start 和 docker run命令的区别:docker run 命令先是利用镜像创建了一个容器,然后运行这个容器。这个命令非常的方便,并且隐藏了两个命令的细节,但从另一方面来看,这容易让用户产生误解。 docker ps 命令会列出所有运行中的容器。这隐藏了非运行态容器的存在,如果想要找出这些容器,我们需要使用下面这个命令。 docker ps –a命令会列出所有的容器,不管是运行的,还是停止的 docker images命令会列出了所有顶层(top-level)镜像。实际上,在这里我们没有办法区分一个镜像和一个只读层,所以我们提出了top-level镜像。只有创建容器时使用的镜像或者是直接pull下来的镜像能被称为顶层(top-level)镜像,并且每一个顶层镜像下面都隐藏了多个镜像层。 docker images –a命令列出了所有的镜像,也可以说是列出了所有的可读层。如果你想要查看某一个image-id下的所有层,可以使用docker history来查看 docker stop命令会向运行中的容器发送一个SIGTERM的信号,然后停止所有的进程 docker stop <container-id> docker kill 命令向所有运行在容器中的进程发送了一个不友好的SIGKILL信号。 docker kill <container-id> docker stop和docker kill命令会发送UNIX的信号给运行中的进程。 docker pause命令则不一样,它利用了cgroups的特性将运行中的进程空间暂停。但是这种方式的不足之处在于发送一个SIGTSTP信号对于进程来说不够简单易懂,以至于不能够让所有进程暂停。 docker pause <container-id> docker rm命令会移除构成容器的可读写层。注意,这个命令只能对非运行态容器执行。 docker rm <container-id> docker rmi 命令会移除构成镜像的一个只读层。你只能够使用docker rmi来移除最顶层(top level layer)(也可以说是镜像),你也可以使用-f参数来强制删除中间的只读层。 docker rmi <image-id> docker commit命令将容器的可读写层转换为一个只读层,这样就把一个容器转换成了不可变的镜像。 docker commit <container-id> docker build命令非常有趣,它会反复的执行多个命令。 从上图可以看到: build命令根据Dockerfile文件中的FROM指令获取到镜像 然后重复地 run(create 和 start) 修改 commit 在循环中的每一步都会生成一个新的层,因此许多新的层会被创建。 docker exec 命令会在运行中的容器执行一个新进程 docker exec <running-container-id> docker inspect命令会提取出容器或者镜像最顶层的元数据。 docker inspect <container-id> or <image-id> docker save命令会创建一个镜像的压缩文件,这个文件能够在另外一个主机的Docker上使用。和export命令不同,这个命令为每一个层都保存了它们的元数据。这个命令只能对镜像生效。 docker save <image-id> docker export命令创建一个tar文件,并且++移除了元数据和不必要的层++,将多个层整合成了一个层,只保存了当前统一视角看到的内容 注意 export后的容器再import到Docker中,通过docker images –tree命令只能看到一个镜像 save后的镜像则不同,它能够看到这个镜像的历史镜像 docker export <container-id> docker history命令递归地输出指定镜像的历史镜像。 docker history <image-id>
多个服务组件容器共同协作提供服务,需要多个容器之间有能够相互访问到对方的服务。 除了通过网络访问外,Docker提供了两个功能来满足服务访问的需求: 映射容器内应用的服务端口到本地宿主机 互联机制实现多个容器见通过容器名快速访问 进程端口映射实现容器访问 从外部访问容器应用 在启动容器的时候,如果不指定对应的参数,在容器外部是无法通过网络来访问容器内的网络应用和服务的。 使用-P/-p参数来指定端口映射: 大写: Docker会随机映射主机的49000~49900的端口到容器的进程端口 小写:自己手动指定需要映射的端口地址,宿主机的一个端口只能绑定一个容器的端口 小写的时候,支持如下三种格式: ++IP:HostPort:ContainerPort++:映射到指定接口的指定端口 ++IP::ContainerPort++:映射到指定接口的任意端口,此时宿主机会自动分配一个端口 ++HostPort:ContainerPort++: 默认会绑定宿主机所有接口(网卡)上的所有地址,多次使用-p可以绑定多个端口 docker port //查看当前映射的端口配置 docker inspect + 容器ID //查看容器自己的内部网络和IP地址 互联机制实现便捷互访 容器的互联是一种让多个容器中应用进行快速交互的方式,它会在源和接收容器之间创建连接关系,接收容器可以通过容器名快速访问到源容器,而不需要指定具体的IP地址 自定义容器名 连接系统依据容器的名称来执行,因此需要定义一个好记忆的容器名字,自定义名字的好处: 好记 当要连接其他容器时,即便重启,也可以使用容器名而不用改变 容器的名字是唯一的 容器互联 使用--link参数可以让容器之间安全地进行交互 --link参数的格式:--link name:alias 其中name是要源容器的名字,alias是这个link的别名 docker run -d --name db training/postgres docker run -d -P --name web --link db:db training/webapp python app.py //web容器连接了db容器,两者之间的这个link的别名是db //允许web容器访问db容器 这相当于在两个互联的容器之间创建了虚拟通道,而且不用映射他们的端口到宿主机上,在启动容器的时候并没有使用-p参数标记,这样也不会暴露数据库服务器的端口到外部网络。 Docker通过两种方式为容器公开连接信息: 更新环境变量 更新/etc/hosts文件 使用env命令来查看web容器的环境变量: docker run --rm --name web2 --link db:db training/webapp env ... DB_NAME=/web2/db DB_PORT=tcp://172.17.0.5:5432 ... 其中DB_开头的环境病历是供web容器连接db容器使用的(前缀采用别名的大写) 除了环境变量,docker还提供host信息到源容器的/etc/hosts文件 docker run -it --rm --link db:db training/webapp /bin/bash root@aed84ee21bde:/opt/webapp# cat /etc/hosts 172.17.0.7 aed84ee21bde //web容器自己 172.17.0.2 db //db容器 可以同ping命令测试容器的连通性,可以连接多个子容器到源容器。