Docker 官方文档 翻译

Table of Contents

1. Docker 底层技术

1.1. Namespaces

Docker 使用 namespaces 技术来处理容器的隔离工作空间。当创建容器时,Docker 会创建容器的一组 namespaces

namespaces 提供了一层隔离,容器的每个部分都在一个单独的 namspaces 中运行,访问权限被限制在该 namespace。

Docker 引擎在 Linux 上使用以下 namesspaces:

  • pid namespace:进程隔离(PID: Process ID)
  • net namespace:管理网络接口(NET: Networking)
  • ipc namespace:管理访问 IPC 资源(IPC: InterProcess Communication)
  • mnt namespace:管理文件系统挂载点(MNT: Mount)
  • uts namespace:隔离内核和版本标识符(UTS: Unix Timesharing System)

1.2. 控制组(Control groups)

Linux 上的 Docker 引擎依赖的另外一项技术叫 control groupscgroups )。cgroup 将应用程序限制为一组特定的一组资源。cgroups 允许 Docker 引擎将可用的硬件资源共享给容器,并可选择强制执限制和约束。比如,你可以限制指定容器的可用内存。

1.3. 联合文件系统(Union file systems)

Union file systems,或者称为 UnionFS,是一种分层文件系统,非常轻量级而且快速。Docker 引擎使用 UnionFS 为容器创建块。Docker 引擎可以使用多个 UnionFS 的变种,包括 AUFS,btrfs,vfs 和 DeviceMapper。

1.4. 容器格式

Docker 引擎把 namespaces,cgroups 和 UnionFS 组合到一个称为容器格式的包装器中。默认的容器格式是 libcontainer 。在未来,Docker 可能会通过 BSD Jails 或者 Solaris Zones 等技术集成来支持其他容器格式。

2. Docker 开发最佳实践

2.1. 如何让构建的镜像足够小

启动容器或者服务时,小的镜像可以更快的下载、当容器或者服务启动时,更快的加载到内存中。这些原则可以让你的镜像尽可能的小:

  • 选择合适的基础镜像。比如,你需要 JDK,直接使用官网的 openjdk 镜像,而不是基于 ubuntu 镜像然后在 Dockerfile 中安装 =openjdk=。
  • 使用多阶段构建。比如,你可以使用 maven 镜像来构建你的 Java 应用,然后重新设置为 tomcat 镜像,把 Java artifacts 复制到这个正确的位置部署你的应用,这些都是在同一个 Dockerfile 中。 这么做可以让你的最终镜像不包含构建时添加的以库和依赖项,只包含 artifacts 和它的运行环境。
    • 如果你使用到的 Docker 版本不支持多阶段构建,试着减少使用 RUN 命令的数量来减少镜像中的层数。把多个单行执行的 RUN 命令,利用 shell 机制将他们合并到一起来实现此目的。考虑下面两个代码片段,第一个在镜像中创建两个层,而第二个只创建了一个层。

      // 版本一
      RUN apt-get -y update
      RUN apt-get install -y python
      // 版本二
      RUN apt-get -y update && apt-get install -y python
      
  • 如果你有多个有部分重叠的镜像,把共同的组件抽离出来作为基础镜像,然后基于基础镜像来构建。Docker 只会加载基础镜像一次,然后缓存下来。这样的话你的派生镜像就可以更有效的使用 Docker 主机上的内存更快的加载。
  • 为了生产环境下的镜像在保持精简的情况下还允许调试,可以把生产环境的镜像作为调试镜像的基本镜像。在生产镜像的基础上添加其他测试或调试工具。
  • 在构建镜像时,总是用有些有意义的标记来标记它们,这些标记可以是构建版本信息,比如 prod 或者 test ,也可以记录不同的部署环境信息。不要依赖自动创建的 latest 标签。

2.2. 在什么地方、怎样持久化应用数据

  • 避免 使用存储驱动程序把应用数据存储在可写层中。这会不断的增加容器的大小,从 I/O 角度来看,相比卷或者绑定挂载效率要低很多。
  • 取而代之,使用卷来存储数据。
  • 在开发期间适合使用绑定挂载比较合适(当你可能想要挂载你的源代码目录或者新构建的二进制文件)。对于生产环境,请使用卷,把它安装到开发期间挂载的相同位置上。
  • 生产环境下,使用 secrets 来存储服务应用程序的敏感数据,使用 configs 来存储不敏感的数据,比如配置文件。如果您当前使用独立容器,请考虑迁移以使用单一副本服务,以便您可以利用这些仅限服务的功能 (If you currently use standalone containers, consider migrating to use single-replica services, so that you can take advantage of these service-only features)。

2.3. 尽可能使用 swarm 服务

  • 如果可能的话,使用 swarm 服务设计应用程序支持横向扩展。
  • 即便你只需要运行应用程序的单个实例,swarm 相比单独的容器也有很多的优势。服务配置是声明性的,Docker 始终致力于所需的状态和实际状态是同步的。
  • 可以通过 swarm 服务连接和断开网络和卷,Docker 处理以不中断的方式部署服务容器。独立的容器需要手动停止,删除和重新创建。
  • 有几个特性,类似存储 secrets 和 configs 的能力,仅适用于服务而不是独立容器。这些特性让你保持镜像尽可能的通用,避免把敏感数据存储在 Docker 镜像或者容器本身中。
  • 使用 docker stack deploy 处理镜像的拉取工作,而不是使用 =docker pull=。这种情况下,你的部署不会尝试从已经 down 的节点中拉取。此外,当新节点添加到 swarm 中,镜像会自动获取。

2.4. 使用 CI/CD 进行测试和部署

  • 当你检查源代码管理的改动或者创建拉取请求时,使用 docker hub 或者另外一种 CI/CD 方式来自动构建,为 Docker 镜像打 Tag,然后测试。
  • 更进一步使用 Docker EE

2.5. 对比开发和生产环境

开发环境:

  1. 使用绑定挂载来为容器提供源代码访问权限
  2. 使用 Docker 或者 Windows 下的 Docker
  3. 不用担心时间漂移(time drift)

生产环境:

  1. 使用卷存储容器数据
  2. 尽可能使用 Docker EE,使用用户映射以更好的隔离 Docker 进程和主机进程
  3. 使用在 Docker 主机和内部容器进程上运行 NTP 客户端,并将他们同步在同一个 NTPS 服务器。如果使用 swarm 服务,还要确保每个 Docker 节点将其始终于容器同步到同一时间源。

3. Docker 网络概览

3.1. 网络驱动程序

Docker 的子网络是可插拔的,默认支持几种驱动程序,提供核心网络功能。

  • bridge: 默认的网络驱动。当你的应用程序需要在单独的容器中运行时,通常会使用桥接网络。
  • host: 对于独立容器,和主机之间没有网络隔离,直接使用主机的网络。 host 仅适用于 Docker 17.06 以及更高版本的 swarm 服务。
  • overlay: Overlay 将多个 Docker 守护进程连接在一起,并且可以在 swarms 服务内部相互通信。还可以使用 Overlay 网络在 swarm 服务和独立容器之间通信,或者在不同的 Docker 守护进程上两个独立容器之间通信。该策略无需在这些容器之间在操作系统级别路由。
  • macvlan: Macvlan 网络允许你为容器分配 MAC 地址,使其显示为网络上的物理设备。Docker 守护程序通过 MAC 地址将流量路由到容器。 macvlan 一般用于处理希望可以连接到屋里网络的陈旧的应用程序,而不是通过 Docker 的主机网络来路由。
  • none: 容器禁用所有网络。通常与自定义网络驱动一起使用。 none 对于 swarm 服务不可用。
  • 网络插件: 你可以使用 Docker 安装和使用第三方插件。在 Docker Hub 或者第三方的供应商可以找到可用的插件。

3.1.1. 总结

  • 当需要在同一个 Docker 主机上多个容器通信时,*使用用户定义的 bridge网络* 。
  • 当网络堆栈不应与 Docker 主机隔离时(但是你希望隔离容器的其他方面),*使用 host 网络* 。
  • 当需要在不同的 Docker 主机上多个容器互通时,*使用 Overlay 网络*,或者使用 swarm 服务多个应用一起工作时。
  • 当你从 VM 迁移业务时,需要容器看起来像网络上的物理主机时,每个都有唯一的 MAC 地址,*使用 Macvlan 网络*。
  • 第三方的网络插件 允许将 Docker 与专用网络堆栈集成。

4. 使用桥接网络

注:bridge 翻译为「桥接」,VMware 中也有桥接模式,这么翻译应该没什么问题。

就网络来言,桥接网络是链路层设备,在网络段之间转发流量。网桥可以是硬件设备或者是在主机内核中运行的软件设备。

就 Docker 而言,桥接网络使用软件桥接的方式,允许连接到同一桥接的容器通信,同时对没有连接到该桥接的容器进行网络隔离。

桥接网络适用于在同一个 Docker 守护程序的主机容器进行通信,对于跨机器或者集群通信需要使用 overlay 网络。

启动 Docker 时,会自动创建一个默认的桥接网络(也称之为 bridge ),除非另行指定,否则新启动的容器将连接到该网络。你可以创建用户自定义(user-defined)桥接网络。用户自定义的桥接网络优先级要高于默认的 bridge 网络。

4.1. 用户自定义的桥接和默认的桥接网络之间的区别

  • 用户自定义的桥接网络可以容器应用程序之间做更好的隔离和交互操作。

    连接到同一个用户自定义的桥接网络的容器会相互自动暴露所有的端口,但是不会被网络外的环境知道。

    比如说包含一个 web 前端和数据库后端的应用程序,外部环境需要访问 web 前端,但是只有后端本身需要访问数据库和端口。使用自定义网络,只需要打开 web 前端的端口,数据库应用端口不需要暴露端口给外部环境。

    如果使用默认的桥接网络,你需要开放所有的 web 端口和数据端口,为每个容器使用 -p 或者 --publish 参数暴露端口给外部环境。也就是说 Docker 宿主机需要通过其他方式来阻止外部环境直接访问数据库端口。

  • 用户自定义桥接网络在容器之间提供自动 DNS 解析。

    默认的桥接网络只能通过 IP 地址访问,除非你使用 --link 选项,这个选项已经被遗弃了。在用户自定义网络上容器可以通过名称或者别名互相解析。

    还是之前的例子,你可以使用类似 db 这种名称的方式访问数据库后端服务,

    默认桥接网络基于遗弃的 --link 选项访问,这种方式需要双向创建,所以两个以上的容器想要通信就会变的很复杂。另外你也可以手动指定容器内部的=/etc/hosts= 文件,但是调试起来太麻烦了。

  • 容器可以在运行状态下与用户自定义桥接网络连接分离(detach)。

    但是你如果使用默认的桥接网络,你需要停掉容器,然后使用不同的网络选项重新创建它。

  • 每个用户自定义的桥接网络都会创建一个可配置的网桥。

    如果你使用了默认的桥接网络,你也可以配置,但是所有的容器都会使用相同的配置,比如 MTU 和 iptables 规则。此外,因为配置默认桥接网络是在 Docker 自身以外,所以修改之后需要重启 Docker 。

    使用 docker network create 创建和配置用户自定义的桥接网络,你可以根据应用程序的不同需求创建不同的桥接网络。

  • 默认的桥接网络共享环境变量。

    本来,使用 --link 选项是唯一的共享环境变量的方法。用户自定义的网络无法实现这种类型的环境变量共享,但是,有些更好的想法来实现共享环境变量,比如:

    • 多个容器网络可以使用 Docker 卷挂载同一个文件或者目录来共享信息。
    • 使用 docker-compose 启动多个容器,然后在 compose 文件中定义共享变量。
    • 使用 swarm 服务而不是单独的容器,然后利用共享的 secretsconfigs

容器可以通过用户自定义的桥接网络来暴露端口给对方,实现容器内部互联。但是想要在非 Docker 主机环境下访问,你必须要通过 -p 或者 --publish 参数将端口暴露给外部环境。

4.2. 管理用户定义桥接网络

使用 docker network create 命令创建一个用户定义桥接网络。你可以指定子网,IP 地址范围,网关和其他选项。具体通过 help 参数查看 docker network create --help

使用 docker network rm 命令删除用户定义桥接网络。如果容器已经连接到当前网络,请先断开(diconnect)连接。

背后的原理是什么?

创建或者删除用户自定义桥接网络时,或者连接或者断开,Docker 使用特定于操作系统的工具来管理底层网络基础架构(例如在 Linux 上添加或者删除桥接设备实际上是配置 iptables 规则)。

4.3. 连接容器到用户定义桥接网络

当你创建容器的时候,你可以指定一个或者多个 --network 标识。下面的例子将 Nginx 容器连接到 my-net 网络,并且将容器内部的 80 端口映射到宿主机的 8080 端口,任何连接到 my-net 的网络都可以连接 my-nginx 的所有端口,反之亦然。

$ docker create --name my-nginx \
--network my-net \
--publish 8080:80 \
nginx:latest

正在运行的容器也可以连接到自定义的桥接网络,使用 docker network connect 命令。下面的命令连接已经运行的 my-nginx 容器到已经存在的 my-net 网络。

$ docker network connect my-net my-nginx

同理,断开连接: docker network disconnect my-net my-nginx

5. 禁用容器网络

创建容器时,将 --network 设置为 =none=。

6. 管理应用数据:Volumes

起初,=-v= 或者 --volume 标识只用作独立容器, --mount 标识用作集群(swarm)服务。然而,在 Docker 17.06 之后,你也可以在单独的容器上使用 --mount 选项, --mount 表达的更为明确。 他们最大的不同是 -v 语法将所有的选项组合在一个字段中,而 --mount 语法则是将他们分开。

新用户应该尝试比 --volume 更简单的 --mount 语法。

如果你需要指定 volume 的驱动器选项,你必须要使用 =–mount=。

  • -v 或者 --volume :由冒号 : 分隔的三个字段组成,字段必须是按照正确的顺序买咧,并且每个字段的含义不是很明显。
    • 第一个字段是 volume 的名字,匿名卷的话,第一个字段可以省略
    • 第二个字段是文件或者目录在容器中挂载的路径
    • 第三个字段是可选的,逗号分隔的选项列表,比如 ro
  • --mount=:由 =<key>=<value> 的多个键值对组成。=–mount= 语法比 -v 或者 --volume 更加的直观,而且键的顺序并不重要,标识也更容易理解。
    • 挂载的类型,=type=,可以是 bind=,=volume 或者 tmpfs 。这个主题讨论的是 volume,所以 type=volume
    • 挂载的源,=source=,volume 的名字,对于匿名 volume,这个字段为空。key 可以指定为 source 或者 src 。即 src=xxx
    • 挂载的目标,=destination=,容器打算挂载的文件或者目录的路径。key 可以指定为 destination=,=dst=,=target 。即 dst=/your/path
    • readonly 选项,如果设置了,会以只读的方式绑定到容器中
    • volume-opt 选项,可以指定多次,由选项名称以及值组成的键值对

6.1. -v--mount 的差异

与 bind mounts 不同,volumes 的所有选项对于 --mount-v 都是可用的。当和服务(services)一起使用时,只支持 --mount

6.2. 创建和管理 volumes

和 bind mount 不同,你需要在容器外部创建和管理 volumes。

创建 volume

docker volume create my-vol

列出 volumes

$ docker volume ls

local               my-vol

查看单个 volume

$ docker volume inspect my-vol
[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"
    }
]

移除一个 volume

$ docker volume rm my-vol

6.3. 启动一个有 volume 的容器

如果启动一个尚不存在的 volume 的容器,Docker 会自动创建为你创建一个。下面的例子会将 volume myvol2 关联到容器中的 /app/

下面的例子中 -v--mount 是相同的:

$ docker run -d \
    --name devtest \
    --mount source=myvol2,target=/app \
    nginx:latest

$ docker run -d \
    --name devtest \
    -v myvol2:/app \
    nginx:latest

使用 docker inspect 指令可查看容器的 volume 情况( Mounts 中的值)。

注意:volume 和容器是两个独立的东西,所有删除容并不会删除 volume。

6.4. 使用只读的 volume

对于某些应用,服务需要将一些变动会写到 Docker 宿主机上;还有些应用,容器只允许读取数据。多个容器可以绑定相同的 volume,再同一时间内,一些应用「读写」数据,另外一些应用「只读」数据。

将目录设置为只读,需要在挂载的参数后面添加 ro 选项,如果存在多个选项,请用逗号分隔。

$ docker run -d \
    --name=nginxtest \
    --mount source=nginx-vol,destination=/usr/share/nginx/html,readonly \
    nginx:latest

$ docker run -d \
    --name=nginxtest \
    -v nginx-vol:/usr/share/nginx/html:ro \
    nginx:latest

此时, docker inspect 可以看到 MountsRWfalse

7. 管理应用数据:bind mounts

bind mounts,是将容器的宿主目录/文件挂载到容器中,如果目录不存在会自动创建。如果你开发一个新的 Docker 容器,推荐使用命名 volumes 替代 bind mounts。

bind mounts 和 volumes 使用几乎是一模一样的(bind mounts 的路径等价于 volumes 的名称),略微的区别:

  1. bind mounts 没有匿名一说;
  2. bind mounts 多了 bind-propagationconsistency 选项(只有在 Mac 平台才有效)。

-v--mount 的差异

如果使用 -v 或者 --volume 挂载一个 Docker 主机不存在的文件或者目录上,*它会自动作为目录创建* 。而使用 --mount 时,Docker 不会自动创建,而是报错。

bind mounts 的 target 如果是一个非空的目录则会覆盖掉容器中的目录,所以选择 target 时要避免输错成系统目录,比如 /usr 这种。

8. 管理应用数据:使用 tmpfs mounts

tmpfs 和 volumes/bind mounts 不同的地方在于,tmpfs 是临时的,并且只存在于主机的内存中。容器停止时,tmpfs 挂载会被删除,数据也就不存在了。

tmpfs 在你不希望主机或者容器的可写层保留敏感文件时比较有用。

tmpfs 使用时没有 source 选项,只有 target 选项。tmpfs 支持两个选项, tmpfs-sizetmpfs-mode

  • tmpfs-size 选项用来限制数据存储大小,默认不做限制;
  • tmpfs-mode 选项用来设定文件 mode,用的是 8 进制,比如 700 或者 0700 。默认是 1777

对于 file mode 扩展阅读:File permissions and attributes

文件的权限由 4 部分组成,分别表示:文件类型(1 bit)、拥有者权限(3bit)、组权限(3bit)、其他用户(3bit)。3 bit 又分别拆分为:读、写、执,即:=rwx= 。 每个 bit 有 0 和 1 两个值,1 表示有,0 表示无。 通常用 rwx 或者 8 进制数值表示, rwx=7 以此类推。所以 700 表示的是拥有者(owner)有所有的权限,而组和其它人没有任何权限。

First created: 2019-03-04 18:05
Last updated: 2022-12-11 Sun 12:49
Power by Emacs 29.0.91 (Org mode 9.6.6)