docker核心知识概括

Docker架构

Docker的核心组件包括:

  • Docker客户端:Client
  • Docker服务端:Docker Daemon
  • Docker镜像:Image
  • Registry:镜像远端
  • Docker容器:Container Docker 采用的是 Client/Server 架构。客户端向服务器发送请求,服务器负责构建、运行和分发容器。客户端和服务器可以运行在同一个 Host 上,客户端也可以通过 socket 或 REST API 与远程的服务器通信。

Docker客户端

最常用的 Docker 客户端是 docker 命令,在安装docker时一并装好。

命令 解释
docker run 运行一个容器
docker ps 列出运行中的容器
docker ps -a 列出所有容器,包括停止的容器
docker images 列出本地镜像
docker pull 从远端仓库拉取镜像
docker build 基于Dockerfile构建镜像
docker exec 在运行中的容器中执行命令
docker stop 停止一个或多个运行中的容器
docker rm 删除一个或多个容器
docker rmi 删除一个或多个本地镜像
docker network ls 列出所有网络
docker volume ls 列出所有卷
docker inspect 提供关于指定Docker对象的详细信息
docker logs 查看容器的日志
docker cp 从容器复制文件到主机
docker commit 创建一个新的镜像

除了docker命令工具之外,也可以通过REST API与Docker服务端通信,一些GUI客户端就是基于此实现的。例如Portainer,它提供了直观的用户界面,使用户能够轻松地管理容器、镜像、卷、网络等Docker组件,而无需使用命令行界面。

Docker 服务端

Docker Daemon是服务端,负责创建、运行、监控容器,构建、存储镜像。在Linux系统中有这么几个常用的路径和文件:

/var/run/docker.sock :Docker守护进程默认监听一个Unix域套接字(Unix domain socket)来接受本地通信,这个套接字通常被称为Docker套接字。

/etc/docker/daemon.json :这个文件包含Docker守护进程的配置选项,例如日志设置、存储驱动、网络配置等。如果该文件不存在,Docker将使用默认配置。

/var/lib/docker :这是Docker用于存储镜像、容器、卷等数据的默认目录。

Docker daemon 默认只能响应来自本地 Host 的客户端请求,如果需要响应网络请求需要修改docker服务配置。

Docker镜像

Docker镜像是一个轻量级、独立、可执行的软件包,可以包括代码、运行时、库、环境变量和配置文件。docker镜像是docker的基础,docker通过镜像生成容器,容器也可以打包成镜像,其具有以下特性:

  • 不可变性:镜像的内容是不可变的,任何对镜像的修改都会生成一个新的镜像。这种不可变性确保了在不同环境中一致性的部署和运行。
  • 轻量性:镜像采用分层存储的设计理念,由多个只读层组成,每个层都包含特定的文件或配置,每个只读层可以给多个镜像共享,从而节省存储空间和提高效率。

docker pull命令可以从Registry下载镜像。

docker run命令则是先下载镜像(如果本地没有),然后再启动容器。

Registry

Registry是存放Docker镜像的仓库,Registry分私有和公有两种。

DockerHub(https://hub.docker.com/)是默认的Registry,由Docker 公司维护,上面有数以万计的镜像,用户可以自由下载和使用。

出于对速度或安全的考虑,用户也可以创建自己的私有Registry。

Docker镜像

镜像的分层结构

docker采用分层结构,通常来说最底层镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件,这样的镜像称作base 镜像。base 镜像从0开始构建,其他镜像以此为基础进行拓展。一般来说base 镜像是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu、Debian、CentOS 等。

平时我们在虚拟机上安装Linux操作系统都是好几个G,为什么docker才200M左右呢?

docker使用的base镜像是经过精简的,只包括最基本的命令、工具和程序库。相比物理机安装的操作系统会小很多。另外base镜像只包括操作系统的rootfs部分,不包括bootfs和kermel,并且和Host共用kernel。

docker的每一层都代表着代码、运行时、库、环境变量和配置文件。下图为例,该新镜像在 Debian base 镜像上构建,添加了一层emacs 编辑器,再添加了一层apache2。新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

当容器启动时,还会添加一个新的可写层被加载到镜像的顶部。 这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层“。所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。 只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。

分层结构的好处
最主要的目的是共享资源,如果有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享 。

使用docker history命令可以检查镜像的分层结构,例如:

构建镜像

Docker 提供了两种构建镜像的方法: docker commit 命令与 Dockerfile 构建文件

docker commit

docker commit 命令是创建新镜像最直观的方法,每次使用一次该命令,则镜像多添加一层,其过程包含三个步骤:

  • 运行容器
  • 进入容器内部进行修改
  • 使用docker commit命令将修改完的容器保存为新的镜像

Docker 并不建议用户通过这种方式构建镜像。原因如下 :

  1. 手动创建镜像效率低下且可重复性弱;
  2. 镜像创建过程不透明,无法对镜像进行审计。

Dockerfile

Dockerfile 是一个文本文件,记录了镜像构建的所有步骤。
下面列出了 Dockerfile 中最常用的指令,完整列表和说明可参看官方文档

命令 解释
FROM 指定BASE镜像
MAINTAINER 设置镜像的作者,可以是任意字符串
COPY 将文件从buildcontext复制到镜像,COPY支持两种形式:COPY src dest 与COPY[“src”,”dest”],注意:src只能指定buildcontext 中的文件或目录。
ADD 与COPY类似,从build context 复制文件到镜像。不同的是,如果src是归档文件(tar、zip、tgz、xz 等),文件会被自动解压到dest。
ENV 设置环境变量,环境变量可被后面的指令使用。例如:ENV MY_VERSION 1.3 RUN apt-get install -y mypackage=$MY_VERSION
EXPOSE 指定容器中的进程会监听某个端口,Docker可以将该端口暴露出来
VOLUME 将文件或目录声明为volume
WORKDIR 为后面的RUN、CMD、ENTRYPOINT、ADD或COPY指令设置镜像中的当前工作目录
RUN 在容器中运行指定的命令
CMD 容器启动时运行指定的命令
ENTRYPOINT 设置容器启动时运行的命令。

Dockerfile 的注释以”#” 开头
RUN、CMD 和 ENTRYPOINT 这三个 Dockerfile 指令看上去很类似,很容易混淆,但是有些细微区别:

  • RUN:执行命令并创建新的镜像层,RUN经常用于安装软件包。
  • CMD:设置容器启动后默认执行的命令及其参数,但 CMD 能够被 docker run 后面跟的命令行参数替换。
  • ENTRYPOINT:配置容器启动时运行的命令。

CMD单独使用时可以使用[命令] [参数] 的形式,当和ENTRYPOINT结合使用时可以作为参数输入,使参数的设定更加灵活。

例如:

1
2
3
FROM ubuntu
ENTRYPOINT ["echo", "Hello"]
CMD ["world"]

在这个例子中,如果直接运行容器而不带任何命令行参数,它会输出Hello world。这是因为ENTRYPOINT指定了echo命令,而CMD提供了默认参数world

如果在运行容器时指定了额外的参数,比如docker run <image> Docker!,那么CMD中的world会被忽略,输出将会是Hello Docker!

Dockerfile支持Shell和Exec格式的命令

Shell格式:

1
<instruction> <command>

例如:

1
2
3
RUN apt-get install python3
CMD echo "Hello world"
NTRYPOINT echo "Hello world"

当指令执行时,shell 格式底层会调用/bin/sh-c [command]
Exec格式:

1
<instruction> ["executable", "paraml", "param2", ...]

例如:

1
2
3
RUN ["apt-get", "install", "python3"]
CMD ["/bin/echo", "Hello world" ]
ENTRYPOINT ["/bin/echo""Helloworld"]

注意:apt-get update 和 apt-get install 被放在一个 RUN 指令中执行,这样能够保证每次安装的是最新的包。如果 apt-get install 在单独的 RUN 中执行,则会使用 apt-get update 创建镜像层,而这一层可能是很久以前缓存的。

Docker容器

容器的启动

docker run是运行容器的命令,当启动时容器会执行指定的CMD或ENTRYPOINT指令,也可以手动指令运行的命令,但是当命令执行结束时容器就会退出。

例如运行docker run hello-world

容器的生命周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。 例如我们运行下面这一段不断循环的脚本

1
docker run ubuntu /bin/bash -c "while true ; do sleep 1; done"

可以打开另一个终端查看容器的状态,发现容器始终在运行。为了避免程序在前台运行,可以添加参数-d让程序在后台运行,例如

1
docker run -d ubuntu /bin/bash -c "while true ; do sleep 1; done"

进入容器内部

按用途容器大致可分为两类:服务类容器和工具类的容器,大多数都是服务类的容器,需要常驻在后台对外提供服务。对于这种常驻在后台运行的容器,有时我们也需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。

docker attach

通过 docker attach命令可以 attach 到容器启动命令的终端

1
docker attach [容器ID]

通过 Ctrl+p, 然后 Ctrl+q 组合键退出 attach 终端。

docker exec

另一种方式是通过 docker exec 进入相同的容器

1
docker exec -it [容器ID] bash

上述命令的含义是在容器内新开一个终端,并执行bash命令打开bash终端。需要注意的是,有些容器内没有bash,可以选择sh命令。

这两个方式有些不同,attach是接入到原来的终端当中,可以直接查看容器输出的内容。而exec是新开一个终端,可以执行Linux命令。建议如果想直接在终端中查看启动命令的输出,用 attach;其他情况一律使用 exec。

容器的启停

docker命令指定容器有两种方式,第一种是使用容器ID,ID可以使用短ID,即前几位就行,第二种是指定容器名

例如:

1
2
3
docker start aa9198422ccb # 使用完整的ID
docker start aa # 使用前几位ID,只需要能够和其他容器进行区分就足够
docker start practical_chandrasekhar # 使用容器名指定

docker stop

通过 docker stop 可以停止运行的容器,docker stop 命令本质上是向该进程发送一个SIGTERM 信号。如果想快速停止容器,可使用 docker kill 命令,其作用是向容器进程发送SIGKILL 信号

docker start

对于处于停止状态的容器,可以通过docker start重新启动,docker start会保留容器的第一次启动时的所有参数。

docker restart

docker restart 可以重启容器,其作用就是依次执行 docker stop 和 docker start。

容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。启动容器时设置–restart就可以达到这个效果,–restart=always 意味着无论容器因何种原因退出(包括正常退出),都立即重启;该参数的形式还可以是 –restart=on-fhilure:3, 意思是如果启动进程退出代码非 0, 则重启容器,最多重启 3 次。

1
docker run -d -—restart=always httpd

容器的暂停与恢复

docker pause 命令可以让容器暂停,docker unpause 可以使其恢复运行。

容器的删除

docker rm 是删除容器,docker rmi 是删除镜像,删除之前需要先docker stop停止容器。

资源限制

内存限额

一个docker host上会运行若干容器,每个容器都需要CPU、内存和IO资源。对于KVM、VMware等虚拟化技术,用户可以控制分配多少CPU、内存资源给每个虚拟机。对于容器,Docker也提供了类似的机制避免某个容器因占用太多资源而影响其他容器乃至整个host的性能。

与操作系统类似,容器可使用的内存包括两部分:物理内存和 swap。 Docker 通过下面两组参数来控制容器内存的使用量。

  1. -m 或 -memory:设置内存的使用限额,例如 100MB, 2GB
  2. -memory-swap:设置内存+swap 的使用限额。

如果在启动容器时只指定 -m 而不指定 -memory-swap, 那么 -memory-swap 默认为 -m的两倍

例如以下命令:

1
docker run -m 200M —memory-swap=300M ubuntu

分配的内存超过限额,容器会退出。

CPU限额

默认设置下,所有容器可以平等地使用hostCPU资源并且没有限制。Docker可以通过-c或–cpu-shares设置容器使用CPU的权重。如果不指定,默认值为1024。与内存限额不同,通过-设置的cpu share并不是CPU资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的CPU资源取决于它的cpushare占所有容器cpushare总和的比例。换句话说:通过cpu share可以设置容器使用CPU的优先级。

例如以下命令:

1
docker run —name "container_A" -c 1024 ubuntu

Block IO 带宽限额

Block IO 是另一种可以限制容器使用的资源。Block 10 指的是磁盘的读写,docker 可通过设置权重、限制 bps 和 iops 的方式控制容器读写磁盘的带宽,下面分别讨论。

1.block IO权重

默认情况下,所有容器能平等地读写磁盘,可以通过设置 -blkio-weight 参数来改变容器block 10 的优先级。

--blkio-weight--cpu-shares类似,设置的是相对权重值,默认为 500。在下面的例子中,containerA 读写磁盘的带宽是 containerB 的两倍。

1
2
docker run -it ―name container_A ―blkio-weight 600 ubuntu
docker run -it —name container_B ―blkio-weight 300 ubuntu

2. 限制 bps 和 iops
bps 是 byte per second, 每秒读写的数据量
iops 是 io per second, 每秒 IO 的次数。
可通过以下参数控制容器的 bps 和 iops:

--device-read-bps:限制读某个设备的 bps。

--device-write-bps:限制写某个设备的 bps。

--device-read-iops: 限制读某个设备的 iops。

--device-write-iops: 限制写某个设备的 iops。

下面这个例子限制容器写 /dev/sda 的速率为 30 MB/s:

1
docker run -it -一device-write-bps /dev/sda: 30MB ubuntu

Docker网络

None网络

none 网络就是什么都没有的网络。挂在这个网络下的容器除了 lo, 没有其他任何网卡。容器创建时,可以通过 -network=none 指定使用 none 网络

1
docker run -it --network=none busybox

Host网络

连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样。

可以通过—network=host 指定使用 host 网络

1
docker run -it --network=host busybox

直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题, Docker host 上已经使用的端口就不能再用了。

Docker host 的另一个用途是让容器可以直接配置 host 网路,比如某些跨 host 的网络解决

方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。

Bridge网络

Docker 安装时会创建一个命名为 docker0 的 Linux bridge。如果不指定-network, 创建的容器默认都会挂到 docker0 上。

默认的docker网关是172.17.0.1,容器创建时,docker 会自动从 172.17.0.0/16 中分配一个 IP, 这里 16 位的掩码保证有足够多的 IP 可以供容器使用。

User-defined网络

除了 none、host、 bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络。
Docker 提供三种 user-defined 网络驱动:bridge、overlay 和 macvlano overlay 和 macvlan用于创建跨主机的网络

bridge网络:

类似上述docker0这个bridge,可以通过bridge 驱动创建新的网络

1
docker network create --driver bridge my_net

通过brctl show命令和docker network inspect my_net命令可以查看新建网络的信息


也可以在创建bridge时使用--subnet--gateway参数来指定网段,相应的也可以在容器运行时指定容器的静态IP。

docker network connect命令可以将两个bridge连通

Docker DNS Server

通过IP访问容器虽然满足了通信的需求,但还是不够灵活。因为在部署应用之前可能无法确定IP,部署之后再指定要访问的IP会比较麻烦。对于这个问题,可以通过docker自带的DNS服务解决。

从Docker1.10版本开始,dockerdaemon实现了一个内嵌的DNS server,使容器可以直接通过“容器名”通信。方法很简单,只要在启动时用–name为容器命名就可以了。下面启动两个容器bbox1和bbox2:

1
2
docker run -it —network=my_net2 --name=bboxl busybox
docker run -it—network=my_net2 --name=bbox2 busybox

使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge网络是无法使用 DNS 的。

Docker存储

Docker 为容器提供了两种存放数据的资源

  1. 由 storage driver 管理的镜像层和容器层
  2. Data Volume

storage driver

前面讲到容器的分层结构特性:Copy-on-Write:

  1. 新数据会直接存放在最上面的容器层。
  2. 修改现有数据会先从镜像层将数据复制到容器层,修改后的数据直接保存在容器层中,镜像层保持不变。
  3. 如果多个层中有命名相同的文件,用户只能看到最上面那层中的文件。

分层结构使镜像和容器的创建、共享以及分发变得非常高效,而这些都要归功于 Docker storage driver。正是 storage driver 实现了多层数据的堆叠并为用户提供一个单一的合并之后的统一视图。

Docker 支持多种 storage driver, 有 AUFS、Device Mapper、Btrfc、OveHayFS、VFS 和ZFS。它们都能实现分层的架构,同时又有各自的特性。对于 Docker 用户来说,具体选择使用哪个 storage driver 是一个难题,因为:

  1. 没有哪个 driver 能够适应所有的场景。
  2. driver 本身在快速发展和迭代。

所以优先使用 Linux 发行版默认的 storage driver。

对于某些容器,直接将数据放在由 storage driver 维护的层中是很好的选择,比如那些无状态的应用。无状态意味着容器没有需要持久化的数据,随时可以从镜像直接创建。

但对于另一类应用这种方式就不合适了,它们有持久化数据的需求,容器启动时需要加载已有的数据,容器销毁时希望保留产生的新数据,也就是说,这类容器是有状态的。

这就要用到 Docker 的另一种存储机制:Data Volume。

Data Volume

Data Volume 本质上是 Docker Host 文件系统中的目录或文件,能够直接被 mount 到容器的文件系统中,具有以下优点:

  1. Data Volume 是目录或文件,而非没有格式化的磁盘 (块设备)
  2. 容器可以读写 volume 中的数据。
  3. volume 数据可以被永久地保存,即使使用它的容器已经销毁。

volume实际上是 docker host 文件系统的一部分,所以 volume 的容量取决于文件系统当前未使用的空间,目前还没有方法设置 volume 的容量。

在具体的使用上,docker 提供了两种类型的 volume:bind mountdocker managed

volume

bind mount

bind mount用于将主机文件系统中的目录或文件挂载到Docker容器中。这种挂载方式允许容器与主机之间共享文件和目录,使得容器中的应用程序可以访问主机上的文件系统。

在运行Docker容器时,可以通过-v--volume选项来指定bind mount。语法通常是-v <host_path>:<container_path>,其中<host_path>是主机上的路径,<container_path>**是容器内的路径。Docker会将主机路径和容器路径之间建立一个映射关系,容器内对该路径的访问实际上是对主机文件系统中相应路径的访问。

bind mount 的使用直观高效,易于理解,但它也有不足的地方:bind mount 需要指定 host文件系统的特定路径,这就限制了容器的可移植性,当需要将容器迁移到其他 host, 而该 host没有要 mount 的数据或者数据不在相同的路径时,操作会失败

Docker managed volumes

Docker managed volumes(Docker管理的卷)是一种Docker容器中用于存储数据的技术,与bind mount类似,但由Docker自动管理。相比于bind mount,Docker managed volumes提供了更多的便利和一些额外的功能,例如跨平台兼容性、易于备份和恢复、更好的性能等。

可以通过 docker inspect 查看 volume, 也可以用 docker volume 命令查看有哪些docker volume

作者

echo

发布于

2024-03-26

更新于

2024-08-10

许可协议

评论