Docker 学习笔记

虚拟化
硬件虚拟化
硬件虚拟化,指物理硬件本身就提供虚拟化的支持。
比如,A 平台的 CPU,能够将 B 平台的指令集转换为自身的指令集执行,并给程序完全运行在 B 平台上的感觉。又或者,CPU 能够自身模拟裂变,让程序或者操作系统认为存在多个 CPU,进而能够同时运行多个程序或者操作系统。这些都是硬件虚拟化的体现。
软件虚拟化
软件虚拟化指的是通过软件的方式来实现虚拟化中关键的指令转换部分。
比如,在软件虚拟化实现中,通过一层夹杂在应用程序和硬件平台上的虚拟化实现软件来进行指令的转换。也就是说,虽然应用程序向操作系统或者物理硬件发出的指令不是当前硬件平台所支持的指令,这个实现虚拟化的软件也会将之转换为当前硬件平台所能识别的。
浅析 Docker
可以把容器看作一个简易版的Linux系统环境(包括root用户权限、进程空间、用户空间和网络空间等)以及运行在其中的应用程序打包而成的沙盒。
每个容器内运行着一个应用,不同的容器相互隔离,容器之间也可以通过网络互相通信。容器的创建和停止十分快速,几乎跟创建和终止原生应用一致;另外,容器自身对系统资源的额外需求也十分有限,远远低于传统虚拟机。很多时候,甚至直接把容器当作应用本身也没有任何问题。
Docker 并没有和虚拟机一样利用一个独立的 OS 执行环境的隔离,它利用的是目前当前 Linux 内核本身支持的容器方式,实现了资源和环境的隔离。
支撑 docker 的核心技术有三个:Namespace
,Cgroup
,UnionFS
。
Namespace
提供了虚拟层面的隔离,比如文件隔离,网络隔离等等。每个命名空间中的应用看到的,都是不同的IP地址,用户空间,进程 ID 等。
Cgroup
提供了物理资源的隔离,比如 CPU,内存,磁盘等等。
UnionFS
给 docker 镜像提供了技术支撑。在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS。 AUFS
将文件的更新挂载到老的文件之上,而不去修改那些不更新的内容,这意味着即使虚拟的文件系统被反复修改,也能保证对真实文件系统的空间占用保持一个较低水平。就像在 Git 中每进行一次提交,Git
并不是将我们所有的内容打包成一个版本,而只是将修改的部分进行记录,这样即使我们提交很多次后,代码库的空间占用也不会倍数增加。 通过 AUFS,Docker 大幅减少了虚拟文件系统对物理存储空间的占用。
虚拟机和 Docker
虚拟机,通常来说就是通过一个虚拟机监视器 的设施来隔离操作系统与硬件或者应用程序和操作系统,以此达到虚拟化的目的。这个夹在其中的虚拟机监视器,常常被称为 Hypervisor。👇是虚拟机和 Docker 的对比:
传统方式是在硬件层面实现虚拟化,需要有额外的虚拟机管理应用和虚拟机操作系统层。Docker容器是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,因此更加轻量级。
虚拟机更擅长彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 Docker 通常用于隔离不同的应用,例如前端,后端以及数据库。
常用命令
CMD | 说明 |
---|---|
sudo docker create --name 容器名称 镜像名称 |
创建容器 |
docker start 名称 |
启动容器 |
sudo docker run --name 容器名称 -d 镜像名称 |
创建并启动容器且在后台运行,-d=--detach |
docker ps |
列出运行中容器 |
docker ps -a |
列出所有容器, -a=--all |
docker stop 容器名称/ID |
停止容器 |
docker rm 容器名称/ID |
删除容器 |
docker exec -it 容器名称 bash/sh |
进入容器 |
docker attach --sig-proxy=false 容器名称 |
将容器转为了前台运行,如果不加--sig-proxy=false Ctrl + C 后会停止容器 |
docker logs 容器名称 |
查看容器日志 |
docker network ls/list |
查看已经存在的网络 |
docker network create -d 网络驱动 网络名 |
创建新网络 |
docker volume ls |
列出当前已创建的数据卷 |
docker volume create 名称 |
创建数据卷 |
docker volume rm 名称 |
删除数据卷 |
docker volume prune |
删除没有被容器引用的数据卷 |
docker build |
构建镜像 |
docker inspect 容器名/ID |
查看容器详情 |
docker run --privileged |
容器获取宿主机root权限 |
docker exec
命令能帮助我们在正在运行的容器中运行指定的命令。
|
|
--rm
选项参数,可以让容器在停止后自动删除,不需要再使用容器删除命令来删除,对临时容器友好👇。
|
|
|
|
☝️-t
选项,指定新生成镜像的名称。-f
指定 Dockerfile 文件所在目录,如果不写的话会从 ./webapp
目录中去找,./webapp
可以直接写成.
理解成当前目录,也是镜像构建的上下文,比如 COPY
指令执行时就是从这个上下文中去找的。
如果需要禁止缓存可以加上--no-cache
参数。
|
|
images 子命令
更多子命令选项还可以通过man docker-images
来查看,或者查看官方文档 docker images
。
选项 | 说明 |
---|---|
-a, ‐‐all=true | false |
列出所有(包括临时文件)镜像文件,默认为否 |
‐‐digests=true | false |
列出镜像的数字摘要值,默认为否 |
-f, --filter=[] |
过滤列出的镜像,如dangling=true 只显示没有被使用的镜像;也可指定带有特定标注的镜像等 |
--format="TEMPLATE" |
控制输出格式,如.ID 代表ID信息,.Repository 代表仓库信息等 |
‐‐no-trunc=true | false |
对输出结果中太长的部分是否进行截断,如镜像的ID信息,默认为是 |
-q, ‐‐quiet=true | false |
仅输出ID信息,默认为否 |
create 子命令
create 命令支持的选项都十分复杂,选项主要包括如下几大类:与容器运行模式相关、与容器环境配置相关、与容器资源限制和安全保护相关。
- ⚠️容器运行模式相关的选项
- ⚠️容器环境和配置相关的选项
- ⚠️容器资源限制和安全保护相关的选项
build 子命令
容器网络
在 Docker 网络中,有三个比较核心的概念,就是:沙盒 、网络、端点。
- 沙盒提供了容器的虚拟网络栈,也就是端口套接字、IP 路由表、防火墙等内容。实现隔离容器网络与宿主机网络,形成了完全独立的容器网络环境。
- 网络可以理解为 Docker 内部的虚拟子网,网络内的参与者相互可见并能够进行通讯。Docker 的这种虚拟网络也是于宿主机网络存在隔离关系的,其目的主要是形成容器间的安全通讯环境。
- 端点是位于容器或网络隔离墙之上的洞,其主要目的是形成一个可以控制的突破封闭的网络环境的出入口。当容器的端点与网络的端点形成配对后,就如同在这两者之间搭建了桥梁,便能够进行数据传输了。
这三者形成了 Docker 网络的核心模型,也就是容器网络模型。
Docker 官方提供了五种 Docker 网络驱动:Bridge Driver
、Host Driver
、Overlay Driver
、MacLan Driver
、None Driver
。
Bridge
网络是 Docker 容器的默认网络驱动,通过网桥来实现网络通讯。为容器创建独立的网络命名空间,分配网卡、IP 地址等网络配置,并通过veth接口对将容器挂载到一个虚拟网桥(默认为docker0)上。
none
为容器创建独立的网络命名空间,但不进行网络配置,即容器内没有创建网卡、IP地址等。
host
不为容器创建独立的网络命名空间,容器内看到的网络配置(网卡信息、路由表、Iptables 规则等)均与主机上的保持一致。注意其他资源还是与主机隔离的。
Overlay
驱动默认采用 VXLAN 协议,在 IP 地址可以互相访问的多个主机之间搭建隧道,让容器可以互相访问,并且让这些容器感觉这个网络与其他类型的网络没有区别。
创建网络
在 Docker 中,能够创建自定义网络,形成自己定义虚拟子网的目的。创建网络的命令是 docker network create
。
|
|
通过 -d
选项我们可以为新的网络指定驱动的类型,其值可以默认的 bridge
、host
、overlay
、maclan
、none
,也可以是其他网络驱动插件所定义的类型。 当不指定网络驱动时,Docker
也会默认采用 Bridge Driver
作为网络驱动。
通过 docker network ls
或是 docker network list
可以查看 Docker 中已经存在的网络。
在创建容器时,可以通过 --network
来指定容器所加入的网络,一旦这个参数被指定,容器便不会默认加入到 bridge
这个网络中,但是仍然可以通过 --network bridge
让其加入。
|
|
可以通过 docker inspect 容器名/ID
观察此时的容器网络。
Docker 中如果两个容器处于不同的网络,之间是不能相互连接引用的。
容器互联
要让一个容器连接到另外一个容器,可以在容器通过 docker create
或 docker run
创建时通过 --link
选项进行配置。
例如,创建一个 MySQL 容器,将运行 Web 应用的容器连接到这个 MySQL 容器上,打通两个容器间的网络,实现它们之间的网络互通。
|
|
容器间的网络已经打通,如何在 Web 应用中连接到 MySQL 数据库❓
Docker 为容器间连接提供了一种非常友好的方式,只需要将容器的网络命名(容器名)填入到连接地址中,就可以访问需要连接的容器了。
|
|
在这里,连接地址中的 mysql(容器名) 好比常见的域名解析,Docker 会将其指向 MySQL 容器的 IP 地址。
Docker 在容器互通中带来的一项便利就是,不再需要真实的知道另外一个容器的 IP 地址就能进行连接。在以往的开发中,每切换一个环境(例如将程序从开发环境提交到测试环境),都需要重新配置程序中的各项连接地址等参数,而在 Docker 里,并不需要关心这个,只需要程序中配置被连接容器的名称,映射 IP 的工作就可以交给 Docker。
在 Docker 里还支持连接时使用别名来摆脱容器名的限制。
|
|
在这里,使用 --link <name>:<alias>
的形式,连接到 MySQL 容器,并设置它的别名为 database
。当我们要在 Web 应用中使用 MySQL 连接时,我们就可以使用 database
来代替连接地址了。
|
|
端口映射
容器直接通过 Docker 网络进行的互相访问,在实际使用中,我们需要在容器外通过网络访问容器中的应用。最简单的一个例子,我们提供了 Web 服务,那么我们就需要提供一种方式访问运行在容器中的 Web 应用。
通过 Docker 端口映射功能,可以把容器的端口映射到宿主操作系统的端口上,当我们从外部访问宿主操作系统的端口时,数据请求就会自动发送给与之关联的容器端口。
要映射端口,可以在创建容器时使用 -p 或者是 –publish 选项。
|
|
使用端口映射选项的格式是 -p <ip>:<host-port>:<container-port>
,其中 ip 是宿主操作系统的监听 ip,可以用来控制监听的网卡,默认为 0.0.0.0
,即是监听所有网卡。host-port
和
container-port
分别表示映射到宿主操作系统的端口和容器的端口,这两者是可以不一样的,比如,可以将容器的 80 端口映射到宿主操作系统的 8080 端口,传入 -p 8080:80
即可。
文件挂载
Bind Mount
Bind Mount
能够直接将宿主操作系统中的目录和文件挂载到容器内的文件系统中,通过指定容器外的路径和容器内的路径,就可以形成挂载映射关系,在容器内外对文件的读写,都是相互可见的。
|
|
使用 -v
或 --volume
来挂载宿主操作系统目录的形式是 -v <host-path>:<container-path>
或 --volume <host-path>:<container-path>
,其中
host-path
和 container-path
分别代表宿主操作系统中的目录和容器中的目录。这里需要注意的是,Docker 这里强制定义目录时必须使用绝对路径,不能使用相对路径。
Docker 还支持以只读的方式挂载,通过只读方式挂载的目录和文件,只能被容器中的程序读取,但不接受容器中程序修改它们的请求。在挂载选项 -v
后再接上 :ro
就可以只读挂载了。
|
|
Volume
数据卷是从宿主操作系统中挂载目录到容器内,只不过这个挂载的目录由 Docker 进行管理,只需要指定容器内的目录,不需要关心具体挂载到了宿主操作系统中的哪里。
可以使用 -v
或 --volume
选项来定义数据卷的挂载。
|
|
数据卷挂载到容器后,可以通过 docker inspect 容器名/ID
看到容器中数据卷挂载的信息👇。
|
|
Source
是 Docker 为我们分配用于挂载的宿主机目录,其位于 Docker 的资源区域,一般默认为 /var/lib/docker
。一般并不需要关心这个目录,一切对它的管理都已经在 Docker 内实现了。
为了方便识别数据卷,可以像命名容器一样为数据卷命名,这里的 Name
是数据卷的命名,在未给出数据卷命名的时候,Docker 会采用数据卷的 ID 命名数据卷。可以通过 -v <name>:<container-path>
这种形式来命名数据卷。
|
|
-v
在定义Bind Mount
时必须使用绝对路径,当不是绝对路径是就是Volume
的定义。
Tmpfs Mount
Tmpfs Mount
支持挂载系统内存中的一部分到容器的文件系统里,不过由于内存和容器的特征,它的存储并不是持久的,其中的内容会随着容器的停止而消失。
挂载临时文件目录要通过 --tmpfs
这个选项来完成。由于内存的具体位置不需要指定,在这个选项里只需要传递挂载到容器内的目录即可。
|
|
镜像版本管理
工作中,当某个镜像不能满足我们的需求时,我们能够将容器内的修改记录下来,保存为一个新的镜像。
提交修改生成新镜像
以下以官方的 nginx:1.12 镜像示例,修改后生成一个 nginx-v2 镜像。
先下载镜像👇
|
|
通过 docker exec
进入容器,并在 /root 目录下新增 hw.txt 文件,文件内容为 hello world
:
将这个改动后的容器保存为新的镜像,docker commit
提交这次修改,提交容器更新后产生的镜像并没 REPOSITORY
和 TAG
的内容,也就是说,这个新的镜像还没有名字。可以使用 docker tag
给新镜像命名。
存出载入镜像
对于某个镜像我们可以导出成一个 tar 包,也可以将一个 tar 镜像导入到系统中。
docker save -o
命令可以将一个镜像导出为 tar 包👇
可以通过docker load
导入一个 tar 包为镜像,以下删除了原有的 nginx-v2 镜像,通过 nginx-v2.tar 成功导入了 nginx-v2 镜像。
利用导入的 nginx-v2 镜像启动一个新容器 mynginxv2,在新容器中的 /root 就会有一个 hw.txt 新文件,内容为 hello world
:
Dockerfile
利用 Dockerfile 文件可以生成镜像,这对于自定义镜像非常优雅,也利于镜像分享,直接分享 Dockerfile 文件就可以了。
常用指令
- 🏆
FROM
通过 FROM
指令指定一个基础镜像,接下来所有的指令都是基于这个镜像所展开的。为了保证镜像精简,可以选用体积较小的镜像如Alpine
或Debian
作为基础镜像。
FROM
指令支持三种形式:
|
|
Dockerfile 中的第一条指令必须是 FROM
指令,因为没有了基础镜像,一切构建过程都无法开展。
- 🏆
RUN
RUN
指令用于向控制台发送命令的指令,在 RUN
指令之后,我们直接拼接上需要执行的命令,在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化。
|
|
RUN
指令是支持 \
换行的,如果单行的长度过长,建议对内容进行切割,方便阅读。
- 🏆
ENTRYPOINT
和CMD
在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为 1 的进程。而这个命令的定义,就是通过 Dockerfile 中的 ENTRYPOINT
和 CMD
实现的。
|
|
当 ENTRYPOINT
与 CMD
同时给出时,CMD
中的内容会作为 ENTRYPOINT
定义命令的参数,最终执行容器启动的还是 ENTRYPOINT
中给出的命令。
- 🏆
EXPOSE
通过 EXPOSE 指令就可以为镜像指定要暴露的端口。
|
|
- 🏆VOLUME
|
|
在 VOLUME
指令中定义的目录,在基于新镜像创建容器时,会自动建立为数据卷,不需要再单独使用 -v
选项来配置。
- 🏆
LABEL
LABEL
指令可以为生成的镜像添加元数据标签信息。这些信息可以用来辅助过滤出特定镜像。格式为:
|
|
- 🏆 COPY 和 ADD
在制作新的镜像的时候,可能需要将一些软件配置、程序代码、执行脚本等直接导入到镜像内的文件系统里,使用 COPY
或 ADD
指令能够帮助我们直接从宿主机的文件系统里拷贝内容到镜像里的文件系统中。
|
|
COPY
与 ADD
指令的定义方式完全一样,需要注意的仅是当我们的目录中存在空格时,可以使用后两种格式避免空格产生歧义。
ARG 参数
在 Dockerfile 里,可以用 ARG
指令来建立一个参数变量,可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它。
|
|
在☝️这个例子里,我们将 Tomcat 的版本号通过 ARG
指令定义为参数变量,在调用下载 Tomcat 包时,使用变量替换掉下载地址中的版本号。通过这样的定义,就可以让我们在不对 Dockerfile
进行大幅修改的前提下,轻松实现对 Tomcat 版本的切换并重新构建镜像了。
如果我们需要通过这个 Dockerfile 文件构建 Tomcat 镜像,我们可以在构建时通过 docker build 的 --build-arg
选项来设置参数变量。
Docker内置了一些镜像创建变量,用户可以直接使用而无须声明,包括(不区分大小写)HTTP_PROXY
、HTTPS_PROXY
、FTP_PROXY
、NO_PROXY
。
|
|
ENV 参数
ENV 环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值。
|
|
环境变量的值不是在构建指令中传入的,而是在 Dockerfile 中编写。由于环境变量在容器运行时依然有效,所以运行容器时我们也可以对其进行覆盖,在创建容器时使用 -e
或是 --env
选项,可以对环境变量的值进行修改或定义新的环境变量。
|
|
ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量。
FAQ
CMD 指令
CMD指令用来指定启动容器时默认执行的命令。支持三种格式:
-
CMD ["executable", "param1", "param2"]
:相当于执行executable param1 param2
,推荐方式。 -
CMD command param1 param2
:在默认的 Shell 中执行,提供给需要交互的应用。 -
CMD ["param1", "param2"]
:提供给 ENTRYPOINT 的默认参数。
每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。
ENTRYPOINT 和 CMD 的区别
这 2 个命令都是用来指定基于此镜像所创建容器里主进程的启动命令。
ENTRYPOINT
指令的优先级高于 CMD
指令。当 ENTRYPOINT
和 CMD
同时在镜像中被指定时,CMD
里的内容会作为 ENTRYPOINT
的参数,两者拼接之后,才是最终执行的命令。
NTRYPOINT | CMD | 实际执行 |
---|---|---|
ENTRYPOINT ["/bin/ep", "arge"] |
/bin/ep arge |
|
ENTRYPOINT /bin/ep arge |
/bin/sh -c /bin/ep arge |
|
CMD ["/bin/exec", "args"] |
/bin/exec args |
|
CMD /bin/exec args |
/bin/sh -c /bin/exec args |
|
ENTRYPOINT ["/bin/ep", "arge"] |
CMD ["/bin/exec", "argc"] |
/bin/ep arge /bin/exec argc |
ENTRYPOINT ["/bin/ep", "arge"] |
CMD /bin/exec args |
/bin/ep arge /bin/sh -c /bin/exec args |
ENTRYPOINT /bin/ep arge |
CMD ["/bin/exec", "argc"] |
/bin/sh -c /bin/ep arge /bin/exec argc |
ENTRYPOINT /bin/ep arge |
CMD /bin/exec args |
/bin/sh -c /bin/ep arge /bin/sh -c /bin/exec args |
ENTRYPOINT
指令主要用于对容器进行一些初始化,而 CMD
指令则用于真正定义容器中主程序的启动命令。
创建容器时可以改写容器主程序的启动命令,而这个覆盖只会覆盖 CMD
中定义的内容,不会影响 ENTRYPOINT
中的内容。
每个 Dockerfile 中只能有一个ENTRYPOINT
,当指定多个时,只有最后一个起效。在运行时,可以被--entrypoint
参数覆盖掉,如docker run --entrypoint
。
使用脚本文件来作为
ENTRYPOINT
的内容是常见的做法,因为对容器运行初始化的命令相对较多,全部直接放置在ENTRYPOINT
后会特别复杂。
COPY 和 ADD 的区别
|
|
两者的区别主要在于 ADD
能够支持使用网络端的 URL 地址作为 src 源,并且在源文件被识别为压缩包时,自动进行解压,而 COPY
没有这两个能力。
当使用本地目录为源目录时,推荐使用 COPY
。
写时复制
在编程里,写时复制 常常用于对象或数组的拷贝中,当拷贝对象或数组时,复制的过程并不是马上发生在内存中,而只是先让两个变量同时指向同一个内存空间,并进行一些标记,当要对对象或数组进行修改时,才真正进行内存的拷贝。
Docker 的写时复制与编程中的相类似,在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。 也就是说,容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间,与其他虚拟化方式对这个过程的操作进行对比,Docker 启动的速度可见一斑。
采用写时复制机制来设计的 Docker,既保证了镜像在生成为容器时,以及容器在运行过程中,不会对自身造成修改。又借助剔除常见虚拟化在初始化时需要从镜像中拷贝整个文件系统的过程,大幅提高了容器的创建和启动速度。可以说,Docker 容器能够实现秒级启动速度,写时复制机制在其中发挥了举足轻重的作用。
docker save、export 区别
docker save
和 docker load
是对镜像的操作,导入导出的是镜像文件。
docker export
和 docker import
是对容器的操作,是导出导入容器,导出一个已经创建的容器到一个文件,不管此时这个容器是否处于运行状态,可以理解为容器快照。
容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态,如标签信息会被丢弃),而镜像存储文件将保存完整记录,体积更大。从容器快照文件导入时可以重新指定标签等元数据信息。