|3分钟带你吃透Docker工作原理与核心概念


一、Docker 简介
Docker 是一个构建 , 发布和运行应用程序的开放平台 。 Docker 以容器为资源分隔和调度的基本单位 , 容器封装了整个项目运行时所需要的所有环境 , 通过 Docker 你可以将应用程序与基础架构分离 , 像管理应用程序一样管理基础架构 , 以便快速完成项目的部署与交付 。
Docker 使用 Go 语言进行开发 , 基于 Linux 内核的 cgroup , namespace , 以及 AUFS 类的 Union FS 等技术 , 对进程进行封装隔离 , 属于操作系统层面的虚拟化技术 。 最初实现是基于 LXC , 从 0.7 版本以后开始去除 LXC , 转而使用自行开发的 libcontainer , 从 1.11 开始 , 则进一步演进为使用 runC 和 containerd 。

  • runc :是一个 Linux 命令行工具 , 用于根据 OCI容器运行时规范 创建和运行容器 。
  • containerd :是一个守护程序 , 它管理容器生命周期 , 提供了在一个节点上执行容器和管理镜像的最小功能集 。
下图体现了 Docker 和传统虚拟化方式的差异:传统虚拟机技术是虚拟出一套硬件后 , 再在其上运行一个完整操作系统 , 之后可以在该系统上运行所需的应用进程;而 Docker 容器内的应用进程则是直接运行于宿主的内核 , 容器没有自己的内核 , 也没有进行硬件虚拟 , 因此要比传统虚拟机更为轻便 。
|3分钟带你吃透Docker工作原理与核心概念
本文插图

二、Docker 架构与核心概念
Docker 使用 client-server 架构 ,Docker 客户端将命令发送给 Docker 守护进程 , 后者负责构建 , 运行和分发 Docker 容器 。Docker 客户端和守护程序使用 REST API , 通过 UNIX 套接字或网络接口进行通信 。
核心概念如下:
|3分钟带你吃透Docker工作原理与核心概念
本文插图

2.1 镜像
Docker 镜像(Image)是一个特殊的文件系统 , 包含了程序运行时候所需要的资源和环境 。 镜像不包含任何动态数据 , 其内容在构建之后也不会被改变 。
因为镜像包含操作系统完整的 root 文件系统 , 其体积往往是庞大的 , 因此在 Docker 设计时 , 充分利用 Union FS (联合文件系统)的技术 , 将其设计为分层存储的架构 , 所以一个镜像实际上是由多层文件系统联合组成 。 镜像构建时 , 会一层层构建 , 前一层是后一层的基础;每一层构建完就不会再发生改变 , 后一层上的任何改变只发生在自己这一层 。 比如 , 删除前一层文件的操作 , 实际不是真的删除前一层的文件 , 而是仅在当前层标记为该文件已删除 。 在最终容器运行的时候 , 虽然不会看到这个文件 , 但是实际上该文件会一直跟随镜像 。 因此 , 在构建镜像的时候 , 需要额外小心 , 每一层尽量只包含该层需要添加的东西 , 任何额外的东西应该在该层构建结束前清理掉 。
分层存储的特征使得镜像的复用、定制变的更为容易 。 甚至可以用之前构建好的镜像作为基础层 , 然后进一步添加新的层 , 以定制自己所需的内容 , 构建新的镜像 。
2.2 容器
镜像(Image)和容器(Container)的关系 , 就像是面向对象程序设计中的 类 和 实例 一样 , 镜像是静态的定义 , 容器是镜像运行时的实体 , 容器可以被创建、启动、停止、删除、暂停等 。
容器的实质是进程 , 但与直接在宿主执行的进程不同 , 容器进程运行在属于自己的、独立的命名空间中 。 因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间 , 甚至自己的用户 ID 空间 。 容器内的进程运行在一个隔离的环境里 , 使用起来 , 就好像是在一个独立于宿主的系统下操作一样 , 这种特性使得容器封装的应用比直接在宿主运行更加安全 。
前面讲过镜像使用的是分层存储 , 容器也是如此 。 每一个容器运行时 , 是以镜像为基础层 , 在其上创建一个当前容器的存储层 , 我们可以称这个为容器运行时读写而准备的存储层称为容器存储层 。 容器存储层的生存周期和容器一样 , 容器消亡时 , 容器存储层也随之消亡 。 因此 , 任何保存于容器存储层的信息都会随容器删除而丢失 。
按照 Docker 最佳实践的要求 , 容器不应该向其存储层内写入任何数据 , 容器存储层要保持无状态化 。 所有的文件写入操作 , 都应该使用数据卷(Volume)、或者绑定宿主目录 , 在这些位置的读写会跳过容器存储层 , 直接对宿主(或网络存储)发生读写 , 其性能和稳定性更高 。 数据卷的生存周期独立于容器 , 容器消亡 , 数据卷不会消亡 , 因此 , 使用数据卷后 , 容器删除或者重新运行之后 , 数据都不会丢失 。
2.3 仓库
镜像构建完成后 , 可以很容易的在当前宿主机上运行 , 但如果需要在其它服务器上使用这个镜像 , 就需要一个集中的存储、分发镜像的服务 , 这就是镜像仓库(Registry) 。 Docker Hub 是 Docker 官方提供的镜像公有仓库 , 提供了大量常用软件的镜像 , 当然出于安全和保密的需要 , 你也可以构建自己的私有仓库 。
2.4 Docker daemon
Docker daemon(dockerd)负责监听 Docker API 请求并管理 Docker 对象 , 如镜像 , 容器 , 网络和卷 , 守护程序彼此之间也可以进行通讯 。
2.5 Docker Client
Docker 客户端(docker)是用户与 Docker 交互的主要方式 。 当你使用 docker run 等命令时 , 客户端会将这些命令发送到 dockerd , dockerd 负责将其执行 。 一个 Docker客户端可以与多个 dockerd 进行通讯 。
三、Docker 常用命令
Docker 提供了大量命令用于管理镜像、容器和服务 , 命令的统一使用格式为:docker [OPTIONS] COMMAND, 其中 OPTIONS 代表可选参数 。 需要注意的是 Docker 命令的执行一般都需要获取 root 权限 , 这是因为 Docker 的命令行工具 docker 与 docker daemon 是同一个二进制文件 , docker daemon 负责接收并执行来自 docker 的命令 , 它的运行需要 root 权限 。
所有常用命令及其使用场景如下:
|3分钟带你吃透Docker工作原理与核心概念
本文插图

3.1 基础命令
  • docker version:用于查看 docker 的版本信息
  • docker info:用于查看 docker 的配置信息
  • docker help:用于查看帮助信息
3.2 镜像相关命令
1. docker search 镜像名
从官方镜像仓库 Docker Hub 查找指定名称的镜像 。 常用参数为 --no-trunc , 代表显示完整的镜像信息 。
2. docker images
列出所有顶层镜像的相关信息 。 常用参数如下:
  • -a :显示所有镜像 , 包括中间隐藏的镜像
  • -q :只显示镜像 ID
  • --digests :显示摘要信息
  • --no-trunc :显示完整镜像信息
3. docker pull 镜像名 [:TAG]
从官方仓库下载镜像 , :TAG 为镜像版本 , 不加则默认下载最新版本 。
4. docker rmi 镜像名或ID [:TAG]
删除指定版本的镜像 , 不加 :TAG 则默认删除镜像的最新版本 。 如果有基于该镜像的容器存在 , 则该镜像无法直接删除 , 此时可以使用参数 -f , 代表强制删除 。 rmi 命令支持批量删除 , 多个镜像名之间使用空格分隔 。 如果想要删除所有镜像 , 则可以使用命令 docker rmi -f $(docker images -qa)。
3.3 容器相关命令
1. docker run [OPTIONS] IMAGE [COMMAND][ARG...]
run 是 docker 中最为核心的一个命令 , 用于新建并启动容器 , 其拥有众多可用参数 , 可以使用 docker run --help 查看所有可用参数 。 常用参数如下:
  • -i :表示使用交互模式 , 始终保持输入流开放;
  • -t :表示分配一个伪终端 , 通常和 -i 结合使用 , 表示使用伪终端与容器进行交互;
  • -d :以后台方式运行容器;
  • --name :指定启动容器的名字 , 如果不指定 , 则由 docker 随机分配;
  • -c :用于给运行在容器中的所有进程分配 CPU 的 shares 值 , 这是一个相对权重 , 实际的处理速度与宿主机的 CPU 相关;
  • -m :用于限制为容器中所有进程分配的内存总量 , 以 B、K、M、G 为单位;
  • -v :挂载数据卷 volume , 可以用多个 -v 参数同时挂载多个 volume 。 volume 的格式为:[host-dir]:[container-dir]:[rw:ro], [rw:ro] 用于指定数据卷的模式 , rw 代表读写模式 , ro 代表只读模式 。
  • -p :用于将容器的端口暴露给宿主机的端口 , 格式为:hostPort:containerPort, 通过端口的暴露 , 可以让外部主机能够访问容器内的应用 。
2. docker ps [OPTIONS]
列出当前所有正在运行的容器 。 常用参数如下:
  • -a :列出所有容器 , 包括运行的和已经停止的所有容器
  • -n :显示最近创建的 n 个容器
  • -q :只显示容器编号
  • --no-trunc :不要截断输出信息
3. 启动重启停止强制停止容器
与容器启动、停止相关的命令为:docker start|restart|stop|kill 容器名或ID, start 命令用于启动已有的容器 , restart 用于重启正在运行的容器 , stop 用于停止正在运行的容器 , kill 用于强制停止容器 。
4. 进入正在运行的容器
想要进入正在运行的容器 , 与容器主进程交互 , 有以下两种常用方法:
  • docker attach 容器名或ID
  • docker exec -it 容器名或ID /bin/bash
5. 退出容器
想要退出正在运行的容器 , 有以下两种常用方法:
  • exit :退出并停止容器;
  • ctrl+P+Q :退出 。
6. docker rm 容器名或ID
删除已停止的容器 , 常用参数为-f , 表示强制删除容器 , 即便容器还在运行 。 想要删除所有容器 , 可以使用 docker rm -f $(docker ps -aq) 命令 。
7. 查看容器信息
可以使用 docker inspect [OPTIONS] NAME|ID [NAME|ID...] 查看容器或者镜像的详细信息 , 想要查看指定的信息 , 可以使用 -- format 参数来指定输出的模板格式 , 示例如下:
docker inspect --format='{{.NetworkSettings}}'32cb3ace3279【|3分钟带你吃透Docker工作原理与核心概念】8. 查看容器运行日志
可以使用 docker logs [OPTIONS] CONTAINER 查看容器中进程的运行日志 , 常用参数如下:
  • --details :显示日志详情
  • -f :跟随日志输出显示
  • --tail :从末尾开始显示指定行的数据
  • -t :显示时间戳
  • --since :开始时间
  • --until : 结束时间
四、DockerFile
dockerfile 是 Docker 用来构建镜像的文本文件 , 包含自定义的指令和格式 , 可以通过 build 命令从 dockerfile 中构建镜像 , 命令格式为:docker build [OPTIONS] PATH | URL | -。
dockerfile 描述了组装镜像的步骤 , 其中每条指令都是单独执行的 。 除了 FROM 指令 , 其他的每一条指令都会在上一条指令所生成镜像的基础上执行 , 执行完后会生成一个新的镜像层 , 新镜像层覆盖在原来的镜像之上从而形成新的镜像 。 为了提高镜像构建的速度 ,Docker Daemon 会缓存构建过程中的中间镜像 。 在构建镜像时 , 它会将 dockerfile 中下一条指令和基础镜像的所有子镜像做比较 , 如果有一个子镜像是由相同的指令生成的 , 则命中缓存 , 直接使用该镜像 , 而不用再生成一个新的镜像 。 常用指令如下:
1. FROM
FROM 指令用于指定基础镜像 , 因此所有的 dockerfile 都必须使用 FROM 指令开头 。 FROM 指令可以出现多次 , 这样会构建多个镜像 , 每个镜像创建完成后 , Docker 命令行界面会输出该镜像的 ID 。 常用指令格式为:FROM <image>[:<tag>] [AS <name>] 。
2. MAINTAINER
MAINTAINER 指令可以用来设置作者名称和邮箱 , 目前 MAINTAINER 指令被标识为废弃 , 官方推荐使用 LABEL 代替 。
3. LABEL
LABEL 指令可以用于指定镜像相关的元数据信息 。 格式为:LABEL <key>=<value> <key>=<value> <key>=<value> ...。
4. ENV
ENV 指令用于声明环境变量 , 声明好的环境变量可以在后面的指令中引用 , 引用格式为 $variable_name 或 ${variable_name}。 常用格式有以下两种:
  • ENV <key> <value> :用于设置单个环境变量;
  • ENV <key>=<value> ... :用于一次设置多个环境变量 。
5. EXPOSE
EXPOSE 用于指明容器对外暴露的端口号 , 格式为:EXPOSE <port> [<port>/<protocol>...], 您可以指定端口是侦听 TCP 还是 UDP , 如果未指定协议 , 则默认为 TCP 。
6. WORKDIR
WORKDIR 用于指明工作目录 , 它可以多次使用 。 如果指明的是相对路径 , 则它将相对于上一个WORKDIR指令的路径 。 示例如下:
WORKDIR /a WORKDIR b WORKDIR c RUN pwd # 此时pwd为:/a/b/c7. COPY
COPY 指令的常用格式为:COPY <src>... <dest> , 用于将指定路径中的文件添加到新的镜像中 , 拷贝的目标路径可以不存在 , 程序会自动创建 。
8. ADD
ADD 指令的常用格式为:COPY <src>... <dest> , 作用与 COPY 指令类似 , 但功能更为强大 , 例如 Src 支持文件的网络地址 , 且如果 Src 指向的是压缩文件 , ADD 在复制完成后还会自动进行解压 。
9. RUN
RUN 指令会在前一条命令创建出的镜像基础上再创建一个容器 , 并在容器中运行命令 , 在命令结束后提交该容器为新的镜像 。 它支持以下两种格式:
  • RUN <command> (shell 格式)
  • RUN [''executable'', ''param1'', ''param2''] (exec 格式)
使用 shell 格式时候 , 命令通过 /bin/sh -c 运行 , 而当使用 exec 格式时 , 命令是直接运行的 , 容器不调用 shell 程序 , 这意味着不会发生正常的 shell 处理 。 例如 , RUN [''echo'',''$HOME''] 不会对 $HOME 执行变量替换 , 此时正确的格式应为:RUN [''sh'',''-c'',''echo $HOME''] 。 下面的 CMD 指令也存在同样的问题 。
10. CMD
  • CMD [''executable'',''param1'',''param2''] (exec 格式, 首选)
  • CMD [''param1'',''param2''] (作为ENTRYPOINT 的默认参数)
  • CMD command param1 param2 (shell 格式)
CMD 指令提供容器运行时的默认值 , 这些默认值可以是一条指令 , 也可以是一些参数 。 一个 dockerfile 中可以有多条 CMD 指令 , 但只有最后一条 CMD 指令有效 。 CMD 指令与 RUN 指令的命令格式相同 , 但作用不同:RUN 指令是在镜像的构建阶段用于产生新的镜像;而 CMD 指令则是在容器的启动阶段默认将 CMD 指令作为第一条执行的命令 , 如果用户在 docker run 时指定了新的命令参数 , 则会覆盖 CMD 指令中的命令 。
11. ENTRYPOINT
ENTRYPOINT 指令 支持以下两种格式:
  • ENTRYPOINT [''executable'', ''param1'', ''param2''] (exec 格式 , 首先)
  • ENTRYPOINT command param1 param2 (shell 格式)
ENTRYPOINT 指令 和 CMD 指令类似 , 都可以让容器在每次启动时执行相同的命令 。 但不同的是 CMD 后面可以是参数也可以是命令 , 而 ENTRYPOINT 只能是命令;另外 docker run 命令提供的运行参数可以覆盖 CMD , 但不能覆盖 ENTRYPOINT, 这意味着 ENTRYPOINT 指令上的命令一定会被执行 。 如下 dockerfile 片段:
ENTRYPOINT [''/bin/echo'', ''Hello''] CMD [''world'']当执行 docker run -it image 后 , 此时输出为 hello world , 而当你执行 docker run -it image spring, 此时 CMD 中的参数会被覆盖 , 此时输出为hello spring 。
五、案例
5.1 基于 Centos 镜像部署 Spring Boot 项目
生产环境中的大多数项目通常都部署在 Linux 服务器上 , 这里我们从基础的 Linux 镜像开始 , 并将我们的项目(这里以 Spring Boot 项目为例)一起打包构建成为一个完整的可执行的镜像 。 首先需要创建Dockerfile文件 , 其内容如下:
# 以官方仓库的centos镜像为基础开始创建 FROM centos # 作者信息 MAINTAINERheibaiying@heibaiying.com # 把JDK安装包拷贝到容器中并自动进行解压 ADD jdk-8u211-linux-x64.tar.gz /usr/java/ # 拷贝项目Jar包到容器中 COPY spring-boot-base.jar/usr/app/ # 配置Java环境变量 ENV JAVA_HOME /usr/java/jdk1.8.0_211 ENV JRE_HOME ${JAVA_HOME}/jre ENV CLASSPATH .:${JAVA_HOME}/lib:${JRE_HOME}/lib ENV PATH ${JAVA_HOME}/bin:$PATH # 项目启动命令 ENTRYPOINT [''java'', ''-jar'', ''/usr/app/spring-boot-base.jar'']将 JDK 安装包 , Spring Boot 项目的 Jar 包以及 Dockerfile 文件放在同一个目录 , 然后执行下面镜像构建命令:
docker build -t spring-boot-base-java:latest .镜像构建完成后 , 可以使用以下命令进行启动:
docker run -it-p 8080:8080 spring-boot-base-java这里为了观察到启动效果 , 所以使用交互的方式启动 , 实际部署时可以使用-d参数来后台启动 , 输出如下:
|3分钟带你吃透Docker工作原理与核心概念
本文插图

5.2 基于 JDK 镜像部署 Spring Boot 项目
上面的项目我们是基于最基础的 Centos 镜像开始构建 , 但由于 Docker Hub 上已经提供了 JDK 的镜像 , 我们也可以选择从 JDK 镜像开始构建 , 此时构建过程更加简单 。 构建步骤和上面的完全一致 , 只是 Dockerfile 的内容有所不同 , 如下:
# 由于只需要运行环境 , 这里我们直接以官方仓库的jre镜像为基础开始创建 FROM openjdk:8u212-jre # 作者信息 MAINTAINERheibaiying@heibaiying.com # 拷贝项目Jar包到容器中 COPY spring-boot-base.jar/usr/app/ # 项目启动命令 ENTRYPOINT [''java'', ''-jar'', ''/usr/app/spring-boot-base.jar'']作者:zed链接:https://juejin.im/post/6861129902889992199


    推荐阅读