Docker用户指南(10) – Docker容器网络

本文对Docker提供的几个默认网络的行为做个简单介绍。描述默认创建的网络类型,以及如何创建自己的用户定义的网络。同时说明在单台主机或跨主机集群创建网络所需的资源。

默认网络

当你安装Docker后,它自动创建了三个网络。你可以使用docker network ls命令来列出这些网络。

  1. $ docker network ls
  2.  
  3. NETWORK ID          NAME                DRIVER
  4. 7fca4eb8c647        bridge              bridge
  5. 9f904ee27bf5        none                null
  6. cf03ee007fb4        host                host

历史上,这三个网络是Docker的一部分。当你运行一个容器时,你可以使用–network参数来指定你想运行容器在哪个网络上。这三个网络仍然可用。
称为docker0的bridge网络出现在所有Docker的安装中。除非你使用docker run –network=来指定其它网络,否则Docker daemon默认连接容器到这个网络。你可以在主机使用ifconfig命令看到这个bridge是主机网络堆栈的一部分。

  1. $ ifconfig
  2.  
  3. docker0   Link encap:Ethernet  HWaddr 02:42:47:bc:3a:eb
  4.           inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
  5.           inet6 addr: fe80::42:47ff:febc:3aeb/64 Scope:Link
  6.           UP BROADCAST RUNNING MULTICAST  MTU:9001  Metric:1
  7.           RX packets:17 errors:0 dropped:0 overruns:0 frame:0
  8.           TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
  9.           collisions:0 txqueuelen:0
  10.           RX bytes:1100 (1.1 KB)  TX bytes:648 (648.0 B)

none网络添加一个容器到一个容器特定网络堆栈。这样会使那个容器缺少网络接口。附加到这样一个容器,按如下来查看:

  1. $ docker attach nonenetcontainer
  2.  
  3. root@0cb243cd1293:/# cat /etc/hosts
  4. 127.0.0.1   localhost
  5. ::1 localhost ip6-localhost ip6-loopback
  6. fe00::0 ip6-localnet
  7. ff00::0 ip6-mcastprefix
  8. ff02::1 ip6-allnodes
  9. ff02::2 ip6-allrouters
  10. root@0cb243cd1293:/# ifconfig
  11. lo        Link encap:Local Loopback
  12.           inet addr:127.0.0.1  Mask:255.0.0.0
  13.           inet6 addr: ::1/128 Scope:Host
  14.           UP LOOPBACK RUNNING  MTU:65536  Metric:1
  15.           RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  16.           TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  17.           collisions:0 txqueuelen:0
  18.           RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
  19.  
  20. root@0cb243cd1293:/#

注意:你按下CTRL-p CTRL-q与容器分离。

host网络添加一个容器到主机的网络堆栈。你会发现容器内的网络配置与主机的相同。
除了bridge网络,你不需要与这些默认的网络交互。虽然你能列出和查看它们,你不能删除它们。因为Docker的安装依赖它们。不过你可以添加你自己自定义的网络并且不再需要它们时可以删除。在学习创建你自己的网络之前,值得再看下默认的bridge网络。

默认的bridge网络详细信息

默认的bridge网络存在于所有的Docker主机中。docker network inspect网络返回关于网络的信息:

  1. $ docker network inspect bridge
  2.  
  3. [
  4.    {
  5.        "Name": "bridge",
  6.        "Id": "f7ab26d71dbd6f557852c7156ae0574bbf62c42f539b50c8ebde0f728a253b6f",
  7.        "Scope": "local",
  8.        "Driver": "bridge",
  9.        "IPAM": {
  10.            "Driver": "default",
  11.            "Config": [
  12.                {
  13.                    "Subnet": "172.17.0.1/16",
  14.                    "Gateway": "172.17.0.1"
  15.                }
  16.            ]
  17.        },
  18.        "Containers": {},
  19.        "Options": {
  20.            "com.docker.network.bridge.default_bridge": "true",
  21.            "com.docker.network.bridge.enable_icc": "true",
  22.            "com.docker.network.bridge.enable_ip_masquerade": "true",
  23.            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
  24.            "com.docker.network.bridge.name": "docker0",
  25.            "com.docker.network.driver.mtu": "9001"
  26.        }
  27.    }
  28. ]

Docker Engine自动给这个网络创建一个子网和网关。docker run命令自动地增加新容器到这个网络。

  1. $ docker run -itd –name=container1 busybox
  2.  
  3. 3386a527aa08b37ea9232cbcace2d2458d49f44bb05a6b775fba7ddd40d8f92c
  4.  
  5. $ docker run -itd –name=container2 busybox
  6.  
  7. 94447ca479852d29aeddca75c28f7104df3c3196d7b6d83061879e339946805c

启动这两个容器后重新查看bridge网络会看到在这个网络有两个新启动的容器。它们的id显示在docker network inspect命令输出的“Containers”区块中:

  1. $ docker network inspect bridge
  2.  
  3. {[
  4.     {
  5.         "Name": "bridge",
  6.         "Id": "f7ab26d71dbd6f557852c7156ae0574bbf62c42f539b50c8ebde0f728a253b6f",
  7.         "Scope": "local",
  8.         "Driver": "bridge",
  9.         "IPAM": {
  10.             "Driver": "default",
  11.             "Config": [
  12.                 {
  13.                     "Subnet": "172.17.0.1/16",
  14.                     "Gateway": "172.17.0.1"
  15.                 }
  16.             ]
  17.         },
  18.         "Containers": {
  19.             "3386a527aa08b37ea9232cbcace2d2458d49f44bb05a6b775fba7ddd40d8f92c": {
  20.                 "EndpointID": "647c12443e91faf0fd508b6edfe59c30b642abb60dfab890b4bdccee38750bc1",
  21.                 "MacAddress": "02:42:ac:11:00:02",
  22.                 "IPv4Address": "172.17.0.2/16",
  23.                 "IPv6Address": ""
  24.             },
  25.             "94447ca479852d29aeddca75c28f7104df3c3196d7b6d83061879e339946805c": {
  26.                 "EndpointID": "b047d090f446ac49747d3c37d63e4307be745876db7f0ceef7b311cbba615f48",
  27.                 "MacAddress": "02:42:ac:11:00:03",
  28.                 "IPv4Address": "172.17.0.3/16",
  29.                 "IPv6Address": ""
  30.             }
  31.         },
  32.         "Options": {
  33.             "com.docker.network.bridge.default_bridge": "true",
  34.             "com.docker.network.bridge.enable_icc": "true",
  35.             "com.docker.network.bridge.enable_ip_masquerade": "true",
  36.             "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
  37.             "com.docker.network.bridge.name": "docker0",
  38.             "com.docker.network.driver.mtu": "9001"
  39.         }
  40.     }
  41. ]

上面的docker network inspect命令显示所有连接的容器和它们的网络资源。在这个默认网络的容器能够使用IP地址来互相通信。在这个默认的bridge网络Docker不支持自动服务发现。如果你想在这个默认的bridge网络使用容器名称来通信,你必须通过旧的docker run –link选项来连接容器。
你可以attach一个运行中的容器来查看它的配置:

  1. $ docker attach container1
  2.  
  3. root@0cb243cd1293:/# ifconfig
  4. eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
  5.           inet addr:172.17.0.2  Bcast:0.0.0.0  Mask:255.255.0.0
  6.           inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link
  7.           UP BROADCAST RUNNING MULTICAST  MTU:9001  Metric:1
  8.           RX packets:16 errors:0 dropped:0 overruns:0 frame:0
  9.           TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
  10.           collisions:0 txqueuelen:0
  11.           RX bytes:1296 (1.2 KiB)  TX bytes:648 (648.0 B)
  12.  
  13. lo        Link encap:Local Loopback
  14.           inet addr:127.0.0.1  Mask:255.0.0.0
  15.           inet6 addr: ::1/128 Scope:Host
  16.           UP LOOPBACK RUNNING  MTU:65536  Metric:1
  17.           RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  18.           TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  19.           collisions:0 txqueuelen:0
  20.           RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

然后使用ping来发送三个ICMP请求测试在bridge网络的容器之间的连接性。

  1. root@0cb243cd1293:/# ping -w3 172.17.0.3
  2.  
  3. PING 172.17.0.3 (172.17.0.3): 56 data bytes
  4. 64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.096 ms
  5. 64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.080 ms
  6. 64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.074 ms
  7.  
  8. — 172.17.0.3 ping statistics —
  9. 3 packets transmitted, 3 packets received, 0% packet loss
  10. round-trip min/avg/max = 0.074/0.083/0.096 ms

最后,使用cat命令检查container1网络配置:

  1. root@0cb243cd1293:/# cat /etc/hosts
  2.  
  3. 172.17.0.2  3386a527aa08
  4. 127.0.0.1   localhost
  5. ::1 localhost ip6-localhost ip6-loopback
  6. fe00::0 ip6-localnet
  7. ff00::0 ip6-mcastprefix
  8. ff02::1 ip6-allnodes
  9. ff02::2 ip6-allrouters

按下CTRL-p CTRL-q离开container1,再attach到container2和重复这三个命令。

  1. $ docker attach container2
  2.  
  3. root@0cb243cd1293:/# ifconfig
  4. eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:03
  5.           inet addr:172.17.0.3  Bcast:0.0.0.0  Mask:255.255.0.0
  6.           inet6 addr: fe80::42:acff:fe11:3/64 Scope:Link
  7.           UP BROADCAST RUNNING MULTICAST  MTU:9001  Metric:1
  8.           RX packets:15 errors:0 dropped:0 overruns:0 frame:0
  9.           TX packets:13 errors:0 dropped:0 overruns:0 carrier:0
  10.           collisions:0 txqueuelen:0
  11.           RX bytes:1166 (1.1 KiB)  TX bytes:1026 (1.0 KiB)
  12.  
  13. lo        Link encap:Local Loopback
  14.           inet addr:127.0.0.1  Mask:255.0.0.0
  15.           inet6 addr: ::1/128 Scope:Host
  16.           UP LOOPBACK RUNNING  MTU:65536  Metric:1
  17.           RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  18.           TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  19.           collisions:0 txqueuelen:0
  20.           RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
  21.  
  22. root@0cb243cd1293:/# ping -w3 172.17.0.2
  23.  
  24. PING 172.17.0.2 (172.17.0.2): 56 data bytes
  25. 64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.067 ms
  26. 64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.075 ms
  27. 64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.072 ms
  28.  
  29. — 172.17.0.2 ping statistics —
  30. 3 packets transmitted, 3 packets received, 0% packet loss
  31. round-trip min/avg/max = 0.067/0.071/0.075 ms
  32. / # cat /etc/hosts
  33. 172.17.0.3  94447ca47985
  34. 127.0.0.1   localhost
  35. ::1 localhost ip6-localhost ip6-loopback
  36. fe00::0 ip6-localnet
  37. ff00::0 ip6-mcastprefix
  38. ff02::1 ip6-allnodes
  39. ff02::2 ip6-allrouters

默认的docker0 bridge网络支持端口映射的使用,docker run –link允许在docker0网络中的容器之间通信。这些技术设置起来很麻烦,容易出错。不过它们仍然可用,最后定义自己的bridge网络来避免它们。

自定义网络

你可以创建自己定义的网络来更好的隔离容器。Docker为方便创建这些网络提供了一些默认的网络驱动。你可以创建一个bridge网络,overlay网络或MACVLAN网络。你也可以创建一个写入自己规格的网络插件(network plugin)或远程网络(remote network)。
你既能创建多个网络,也能添加容器到多个网络。容器只能在它加入的网络内通信不过跨网络。一个容器附加到两个网络能与这两个网络的所有成员通信。当一个容器连接多个网络时,由第一个非内部网络提供外部连接。

bridge网络

创建最简单的自定义网络是bridge网络。这个网络类似于历史默认的docker0网络。不过增加了一些新的功能和一些旧的功能不再可用。

  1. $ docker network create –driver bridge isolated_nw
  2. 1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b
  3.  
  4. $ docker network inspect isolated_nw
  5.  
  6. [
  7.     {
  8.         "Name": "isolated_nw",
  9.         "Id": "1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b",
  10.         "Scope": "local",
  11.         "Driver": "bridge",
  12.         "IPAM": {
  13.             "Driver": "default",
  14.             "Config": [
  15.                 {
  16.                     "Subnet": "172.21.0.0/16",
  17.                     "Gateway": "172.21.0.1/16"
  18.                 }
  19.             ]
  20.         },
  21.         "Containers": {},
  22.         "Options": {}
  23.     }
  24. ]
  25.  
  26. $ docker network ls
  27.  
  28. NETWORK ID          NAME                DRIVER
  29. 9f904ee27bf5        none                null
  30. cf03ee007fb4        host                host
  31. 7fca4eb8c647        bridge              bridge
  32. c5ee82f76de3        isolated_nw         bridge

创建网络后,你可以使用docker run –network=选项来加入这个网络。

  1. $ docker run –network=isolated_nw -itd –name=container3 busybox
  2.  
  3. 8c1a0a5be480921d669a073393ade66a3fc49933f08bcc5515b37b8144f6d47c
  4.  
  5. $ docker network inspect isolated_nw
  6. [
  7.     {
  8.         "Name": "isolated_nw",
  9.         "Id": "1196a4c5af43a21ae38ef34515b6af19236a3fc48122cf585e3f3054d509679b",
  10.         "Scope": "local",
  11.         "Driver": "bridge",
  12.         "IPAM": {
  13.             "Driver": "default",
  14.             "Config": [
  15.                 {}
  16.             ]
  17.         },
  18.         "Containers": {
  19.             "8c1a0a5be480921d669a073393ade66a3fc49933f08bcc5515b37b8144f6d47c": {
  20.                 "EndpointID": "93b2db4a9b9a997beb912d28bcfc117f7b0eb924ff91d48cfa251d473e6a9b08",
  21.                 "MacAddress": "02:42:ac:15:00:02",
  22.                 "IPv4Address": "172.21.0.2/16",
  23.                 "IPv6Address": ""
  24.             }
  25.         },
  26.         "Options": {}
  27.     }
  28. ]

你要加入到这个网络的容器必须在同一台docker主机上。在这个网络的每一个容器能马上与在这个网络的其它容器通信。虽然网络本身将容器与外部网络隔离了。
虚拟化技术
在一个自定义的bridge网络中,linking是不支持的。你可以在这个网络的容器公开和发布容器端口。如果你想使bridge的一部分容器可用于一个外部的网络会非常有用。
虚拟化技术
你想在一个主机运行一个相关的小的网络这种场景下,可以使用bridge网络。

docker_gwbridge网络

在两种不同的情况下docker会自动创建一个本地bridge网络docker_gwbridge:

  • 当你初始化或加入一个swarm,docker创建docker_gwbridge网络并用它来与不同的主机上的swarm节点之间通信。
  • 当没有一个容器网络能提供外部连接时,docker连接容器到docker_gwbridge网络以及容器的其它网络,以使得容器能连接外部网络或其它swarm节点。
  • 如果你需要一个自定义的配置你可以提前创建docker_gwbridge网络,否则docker会根据需要创建它。下面的示例使用一些自定义选项来创建docker_gwbridge网络。

    1. $ docker network create –subnet 172.30.0.0/16
    2.                         –opt com.docker.network.bridge.name=docker_gwbridge
    3.             –opt com.docker.network.bridge.enable_icc=false
    4.             docker_gwbridge

    当你使用overlay网络时docker_gwbridge始终存在。

    Docker Engine swarm模式的overylay网络

    你可以在运行着swarm模式的管理节点上创建一个不需要外部键值存储的overylay网络。overlay网络只可用于服务依赖它的swarm节点上。当你创建一个服务使用overlay网络时,管理节点自动扩展overlay网络到运行服务任务的节点上。
    下面的示例显示如何创建一个网络并设置一个服务使用这个网络:

    1. # Create an overlay network `my-multi-host-network`.
    2. $ docker network create
    3.   –driver overlay
    4.   –subnet 10.0.9.0/24
    5.   my-multi-host-network
    6.  
    7. 400g6bwzd68jizzdx5pgyoe95
    8.  
    9. # Create an nginx service and extend the my-multi-host-network to nodes where
    10. # the service’s tasks run.
    11. $ docker service create –replicas 2 –network my-multi-host-network –name my-web nginx
    12.  
    13. 716thylsndqma81j6kkkb5aus

    依赖外部键值存储的overlay网络

    如果你的Docker Engine没有运行在swarm模式中,overlay网络则需要一个有效的键值存储服务。支持键值存储包括Consul, Etcd和ZooKeeper(分布式存储)。在这个Engine版本创建这个网络前,你必须安装和配置一个键值存储服务。要加入到这个网络的docker主机和服务必须能与这个键值存储服务通信。

    注意:运行在swarm模式的docker engine与使用一个外部键值存储的网络不兼容。

    虚拟化技术
    在这个网络的每一个主机必须运行一个docker engine实例。
    虚拟化技术
    在每一个主机之间需要开放如下端口。

    协议 端口 描述
    udp 4789 Data plane (VXLAN)
    tcp/udp 7946 Control plane

    要创建一个overlay网络,还需要在每个主机的docker daemon配置如下选项:

    选项 描述
    –cluster-store=PROVIDER://URL 键值服务地址
    –cluster-advertise=HOST_IP|HOST_IFACE:PORT 用于集群的IP地址,接口和端口
    –cluster-store-opt=KEY-VALUE OPTIONS TLS证书或调整发现时间等选项

    在集群中一个机器上创建一个overlay网络。

    1. $ docker network create –driver overlay my-multi-host-network

    这使得一个网络可以跨越多个主机。overlay网络为容器提供完全的隔离。
    虚拟化技术
    然后,在每个主机上运行容器时确保指定网络名称。

    1. $ docker run -itd –network=my-multi-host-network busybox

    一旦容器连接到这个网络,每个容器就有权限访问这个网络中的所有容器,不管容器是在哪个主机运行的。
    虚拟化技术

    自定义网络插件

    如果你想,你如果编写自己的网络驱动插件。网络驱动插件使用Docker的插件基础设施。在这个基础设施中,插件是在与Docker守护程序相同的Docker主机上运行的进程。网络插件遵循与其他插件相同的限制和安装规则。所有插件都使用插件API。它们具有包括安装,启动,停止和激活的生命周期。一旦创建并安装了自定义网络驱动程序,就可以像内置网络驱动一样使用它。 例如:

    1. $ docker network create –driver weave mynet

    你可以查看这个自定义网络,添加容器到这个网络等等。

    Docker内置的DNS服务器

    Docker daemon运行一个内置的DNS服务器为连接到自定义网络的容器提供自动服务发现功能。从容器发来的域名解析请求首先由内置的DNS服务器处理。如果内置的DNS服务器无法解析这个请求,那么它就将此请求转发到容器配置的外部DNS服务器。

    Docker用户指南(9) – ZFS存储驱动实践

    ZFS是支持许多高级存储技术(如卷管理,快照,检验和,压缩,去重,复制等)的下一代文件系统。由Microsystems(现在是Oracle Corporation)创建,以CDDL许多可证开源。由于CDDL和GPL的许可证不兼容,ZFS无法作为Linux内核主线的一部分提供。不过,Linux的ZFS(ZoL)提供了在内核树之外的模块并且可以单独安装用户空间工具。
    Linux的ZFS(ZoL)目前已经是稳定成熟了。不过,除非你在Linux上拥有丰富的ZFS使用经验,否则不建议目前在生产环境使用zfs Docker存储驱动。

    镜像分层和ZFS共享

    Docker zfs存储驱动大量使用三个ZFS数据集:

  • 文件系统
  • 快照
  • 克隆
  • ZFS文件系统是精简置备的(thinly provisioned)并通过按需分配操作从一个ZFS池(zpool)分配空间。快照和克隆是节省空间的某个时间点的ZFS文件系统副本。快照是只读的。克隆(clones)是可读写的。克隆只能从快照创建。它们简单的关系如下图.
    虚拟化技术
    上图中的实线部分是创建一个克隆的过程。步骤1创建文件系统的一个快照,步骤2是从快照创建克隆。虚线表示克隆和文件系统之间的关系,通过快照,所有三个ZFS数据集从相同的底层zpool中申请空间。
    在使用zfs存储驱动的Docker主机上,一个镜像的base层是一个ZFS文件系统。每一个子层是基于该层下面层的快照的克隆。一个容器是基于从其创建的镜像顶层的快照的ZFS克隆。所有的ZFS数据集从同一个zpool申请空间。下图显示zpool,三个数据集,一个基于两层镜像的容器的关系。
    虚拟化技术
    下面的过程解释了镜像是如何分层的和如何创建容器。该过程基于上图。
    1.镜像的base数据层作为ZFS文件系统存在于Docker主机上。
    此文件系统消耗用于在/var/lib/docker上创建Docker主机的本地存储区的zpool空间。
    2.其它的镜像数据层是在其下方的镜像层的数据集的克隆。
    在这个图中,”Layer 1″是通过创建base层的一个ZFS快照,然后从这个快照创建一个克隆增加。这个克隆是可写的并消耗从zpool按需分配的空间。快照是只读的,将base层保持为一个不可变的对象。
    3.当一个容器创建后,一个可读写的数据层增加在镜像的上面。在上图中,这个容器的可读写数据层是通过创建镜像顶层(Layer1)的快照,然后从这个快照创建一个克隆创建的。
    所有对容器的更改,将通过按需分配操作从zpool中分配空间。默认下,ZFS以128K大小的数据块分配空间。
    从只读快照创建子层和容器的此过程允许保持镜像为一个不可变对象。

    使用ZFS读写容器

    使用zfs存储驱动进行容器的读取是非常简单的。一个新创建的容器基于一个ZFS克隆。此克隆最初与从其创建的数据集共享其所有数据。这意味着使用zfs存储驱动的读操作是非常快的 – 即使要读取的数据还没有复制到容器中。下面显示这个数据块的分享。
    虚拟化技术
    写入新数据到容器是通过按需分配操作完成。每次需要写数据到容器的新区域,一个新的数据块从zpool分配。这意味着写入新数据到容器的新区域将消耗额外的空间。从底层zpool分配新空间给容器(ZFS克隆)。
    更新容器存在的数据通过分配新数据块给容器克隆和存储更改的数据到这些新数据块完成。原始的数据块没有更改,允许底层镜像数据集保持不变。这与写入正常的ZFS文件系统相同,并且都执行了写时拷贝操作。

    配置Docker使用ZFS存储驱动

    zfs存储驱动只支持/var/lib/docker挂载为一个ZFS文件系统的Docker主机。本节介绍如何在Ubuntu 14.04系统上安装和配置原生Linux ZFS(ZoL)。

    先决条件

    如果你已经在你docker主机上使用docker daemon并且有你想保留的镜像,在执行下面的步骤之前先push它们到docker hub或你的私有docker registry。
    停止docker daemon。然后确保在/dev/xvdb有一个空闲的块设备。块设备的标识符可能与你的不同,请替换成你自己的。

    在Ubuntu 16.04 LTS安装ZFS

    1.先停止docker deamon。
    2.安装zfs软件包。

    1. $ sudo apt-get install -y zfs
    2.  
    3.  Reading package lists… Done
    4.  Building dependency tree
    5.  <output truncated>

    3.验证zfs模块是否已经正确加载。

    1. $ lsmod | grep zfs
    2.  
    3.  zfs                  2813952  3
    4.  zunicode              331776  1 zfs
    5.  zcommon                57344  1 zfs
    6.  znvpair                90112  2 zfs,zcommon
    7.  spl                   102400  3 zfs,zcommon,znvpair
    8.  zavl                   16384  1 zfs

    在Ubuntu 14.04安装ZFS

    1.先停止docker daemon。
    2.安装add-apt-repository命令依赖的software-properties-common软件。

    1. $ sudo apt-get install -y software-properties-common
    2.  
    3.  Reading package lists… Done
    4.  Building dependency tree
    5.  <output truncated>

    3.增加zfs-native软件包ppa源。

    1. $ sudo add-apt-repository ppa:zfs-native/stable
    2.  
    3.   The native ZFS filesystem for Linux. Install the ubuntu-zfs package.
    4.  <output truncated>
    5.  gpg: key F6B0FC61: public key "Launchpad PPA for Native ZFS for Linux" imported
    6.  gpg: Total number processed: 1
    7.  gpg:               imported: 1  (RSA: 1)
    8.  OK

    4.获取最新的软件包列表。

    1. $ sudo apt-get update
    2.  
    3.  Ign http://us-west-2.ec2.archive.ubuntu.com trusty InRelease
    4.  Get:1 http://us-west-2.ec2.archive.ubuntu.com trusty-updates InRelease [64.4 kB]
    5.  <output truncated>
    6.  Fetched 10.3 MB in 4s (2,370 kB/s)
    7.  Reading package lists… Done

    5.安装ubuntu-zfs软件包

    1. $ sudo apt-get install -y ubuntu-zfs
    2.  
    3.  Reading package lists… Done
    4.  Building dependency tree
    5.  <output truncated>

    6.加载zfs模块。

    1. $ sudo modprobe zfs

    7.验证模块是否已正确加载。

    1. $ lsmod | grep zfs
    2.  
    3.  zfs                  2768247  0
    4.  zunicode              331170  1 zfs
    5.  zcommon                55411  1 zfs
    6.  znvpair                89086  2 zfs,zcommon
    7.  spl                    96378  3 zfs,zcommon,znvpair
    8.  zavl                   15236  1 zfs

    配置Docker ZFS

    一旦ZFS安装并加载完成,你可以继续配置docker ZFS。
    1.创建一个新的zpool。

    1. $ sudo zpool create -f zpool-docker /dev/xvdb

    该命令创建名为”zpool-docker”的zpool。名称可以是任意的。
    2.检查zpool是否存在。

    1. $ sudo zfs list
    2.  
    3.  NAME            USED  AVAIL    REFER  MOUNTPOINT
    4.  zpool-docker    55K   3.84G    19K    /zpool-docker

    3.创建并挂载一个新的ZFS文件系统到/var/lib/docker。

    1. $ sudo zfs create -o mountpoint=/var/lib/docker zpool-docker/docker

    4.检查是否已经挂载成功。

    1. $ sudo zfs list -t all
    2.  
    3.  NAME                 USED  AVAIL  REFER  MOUNTPOINT
    4.  zpool-docker         93.5K  3.84G    19K  /zpool-docker
    5.  zpool-docker/docker  19K    3.84G    19K  /var/lib/docker

    现在你已经把一个ZFS文件系统挂载到/var/lib/docker,docker daemon应该可以自动加载zfs存储驱动了。
    5.启动docker daemon。

    1. $ sudo service docker start
    2.  
    3.  docker start/running, process 2315

    6.验证docker daemon是否已经在使用zfs存储驱动了。

    1. $ sudo docker info
    2.  
    3.  Containers: 0
    4.  Images: 0
    5.  Storage Driver: zfs
    6.   Zpool: zpool-docker
    7.   Zpool Health: ONLINE
    8.   Parent Dataset: zpool-docker/docker
    9.   Space Used By Parent: 27648
    10.   Space Available: 4128139776
    11.   Parent Quota: no
    12.   Compression: off
    13.  Execution Driver: native-0.2
    14.  […]

    上面命令的输出显示docker daemon的存储驱动为zfs,父数据集为之前创建的zpool-docker/docker文件系统。

    ZFS与Docker性能

    有几个影响使用zfs存储驱动的docker性能的因素。

  • 内存。内存对ZFS性能有很大影响。这是因为ZFS刚开始是为有大量内存的Sun Solaris服务器设计的。使用ZFS时注意其内存使用情况。
  • ZFS功能。使用ZFS功能如去重,可能会明显增加ZFS内存使用量。出于内存消耗和性能考虑,建议关闭ZFS去重功能。不过,仍然可以使用堆栈中其他层(如SAN或NAS阵列)中的去重功能,因为这些不会影响ZFS内存使用和性能。
  • ZFS缓存。ZFS在一个称为自适应替换缓存(ARC)的内存结构中缓存硬盘数据块。ZFS的单拷贝ARC功能允许数据块的单个缓存副本由文件系统的多个克隆共享。这意味着多个正在运行的容器可以共享缓存数据块的单个副本。意味着ZFS是PaaS和其他类似用例的好选择。
  • 碎片。 碎片是像ZFS这样的写时拷贝文件系统的自然副产品。 然而,ZFS写入128K数据块中,并将slab(多个128K块)分配给CoW操作,以尝试减少碎片。 ZFS intent日志(ZIL)和合并写入(延迟写入)也有助于减少碎片。
  • 使用原生的Linux ZFS驱动。虽然Docker zfs存储驱动支持ZFS FUSE,当在需要高性能的场景下时不推荐使用。Linux的原生ZFS驱动的性能优于FUSE。
  • Docker用户指南(8) – OverlayFS存储驱动实践

    OverlayFS是一个与AUFS类似的现代联合(union)文件系统。与AUFS比较,OverlayFS存在如下优势:

  • 更简单的设计
  • 从3.18版本开始已经进入Linux内核主线
  • 可能更快
  • 因此,OverlayFS在Docker社区迅速普及,许多人认为这可以代替AUFS。虽然OverlayFS发展前景很好,但仍然不够成熟。因此在生产环境上部署前,请慎重考虑。
    Docker的overlay存储驱动利用OverlayFS几个功能来构建和管理镜像和容器在硬盘的结构。
    从1.12版本起,Docker也提供了overlay2存储驱动,它比overlay在inode的使用方面更高效。不过overlay2只能用在Linux内核 4.0和更高的版本。
    本文使用OverlayFS表示文件系统,overlay/overlay2表示存储驱动。

    镜像分层及OverlayFS(overlay)共享

    OverLayFS在一台Linux主机维护两个目录,一个在另一个上面,并提供一个统一的视图。这些目录通常称为数据层,用来叠加它们的技术叫做联合挂载(union mount)。OverlayFS底层称为“lowerdir”,顶层称为“upperdir”。通过它自己的”merged”目录提供一个统一视图。
    下图显示一个Docker镜像和Docker容器是如何分层的。镜像数据层是“lowerdir”,容器数据层是”uppperdir”。通过目录为”merged”提供一个统一视图,该目录是容器的挂载点。下图显示Docker结构是如何映射到OverlayFS结构的。
    虚拟化技术
    注意看容器数据层和镜像数据层能包含同样的文件。当出现此情况时,容器数据层”upperdir”的这个文件是显性的并隐藏了在镜像数据层“lowerdir”的相同文件的存在。容器挂载“merged”呈现一个统一视图。
    overlay驱动只在两个层工作。这意味着多层的镜像无法实现为多个OverlayFS层。而是每个镜像数据层在自己的/var/lib/docker/overlay目录下实现多层关系。然后使用硬链接引用与更低层共享的数据。从Docker 1.10起,镜像数据层ID不再与/var/lib/docker下的目录名称相关。
    要创建一个容器,overlay驱动将镜像顶层与一个新目录结合。镜像顶层是overlay的只读“lowerdir”。容器新的目录是可写的“upperdir”。

    镜像和容器硬盘上的结构(overlay)示例

    下面的docker pull命令显示Docker主机下载一个由5个层组成的Docker镜像。

    1. $ sudo docker pull ubuntu
    2.  
    3. Using default tag: latest
    4. latest: Pulling from library/ubuntu
    5.  
    6. 5ba4f30e5bea: Pull complete
    7. 9d7d19c9dc56: Pull complete
    8. ac6ad7efd0f9: Pull complete
    9. e7491a747824: Pull complete
    10. a3ed95caeb02: Pull complete
    11. Digest: sha256:46fb5d001b88ad904c5c732b086b596b92cfb4a4840a3abd0e35dbb6870585e4
    12. Status: Downloaded newer image for ubuntu:latest

    每个镜像数据层在/var/lib/docker/overlay/下都有自己的对应目录。这些目录存储着每个镜像数据层的数据。
    下面的命令输出显示5个存储着刚才下载的每个镜像数据层的数据的目录。不过就如你看到的镜像数据层ID没有匹配/var/lib/docker/overlay下目录的名称。

    1. $ ls -l /var/lib/docker/overlay/
    2.  
    3. total 20
    4. drwx—— 3 root root 4096 Jun 20 16:11 38f3ed2eac129654acef11c32670b534670c3a06e483fce313d72e3e0a15baa8
    5. drwx—— 3 root root 4096 Jun 20 16:11 55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358
    6. drwx—— 3 root root 4096 Jun 20 16:11 824c8a961a4f5e8fe4f4243dab57c5be798e7fd195f6d88ab06aea92ba931654
    7. drwx—— 3 root root 4096 Jun 20 16:11 ad0fe55125ebf599da124da175174a4b8c1878afe6907bf7c78570341f308461
    8. drwx—— 3 root root 4096 Jun 20 16:11 edab9b5e5bf73f2997524eebeac1de4cf9c8b904fa8ad3ec43b3504196aa3801

    镜像数据层目录包含该层唯一的文件和与更低层共享目录的硬链接。这样可以提升空间有效利用率。

    1. $ ls -i /var/lib/docker/overlay/38f3ed2eac129654acef11c32670b534670c3a06e483fce313d72e3e0a15baa8/root/bin/ls
    2.  
    3. 19793696 /var/lib/docker/overlay/38f3ed2eac129654acef11c32670b534670c3a06e483fce313d72e3e0a15baa8/root/bin/ls
    4.  
    5. $ ls -i /var/lib/docker/overlay/55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358/root/bin/ls
    6.  
    7. 19793696 /var/lib/docker/overlay/55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358/root/bin/ls

    容器也在位于/var/lib/docker/overlay的Docker主机文件系统硬盘上。如果你使用ls -l命令查看与运行容器相关的目录,你会看到如下文件和目录。

    1. $ ls -l /var/lib/docker/overlay/<directory-of-running-container>
    2.  
    3. total 16
    4. -rw-r–r– 1 root root   64 Jun 20 16:39 lower-id
    5. drwxr-xr-x 1 root root 4096 Jun 20 16:39 merged
    6. drwxr-xr-x 4 root root 4096 Jun 20 16:39 upper
    7. drwx—— 3 root root 4096 Jun 20 16:39 work

    这四个文件系统对象是OverlayFS的部件。lower-id文件包含容器基于的镜像的顶层ID。在OverlayFS上称为“loverdir”。

    1. $ cat /var/lib/docker/overlay/ec444863a55a9f1ca2df72223d459c5d940a721b2288ff86a3f27be28b53be6c/lower-id
    2.  
    3. 55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358

    upper目录是容器的可写数据层。所有对容器的更改都是写到这个目录。
    merged目录是容器的挂载点。这是镜像”loverdir”和容器“upperdir”的统一视图。任何对这个容器的修改将马上反映到这个目录。
    work目录是OverlayFS执行操作时所需的目录。如执行copy_up操作。
    你可以从mount命令的输出来验证这些结构。

    1. $ mount | grep overlay
    2.  
    3. overlay on /var/lib/docker/overlay/ec444863a55a…/merged
    4. type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay/55f1e14c361b…/root,
    5. upperdir=/var/lib/docker/overlay/ec444863a55a…/upper,
    6. workdir=/var/lib/docker/overlay/ec444863a55a…/work)

    镜像分层和OverlayFS共享(overlay2)

    overlay驱动仅支持单个lower OverlayFS层并且需要硬链接来实现多层的镜像,overlay2驱动原生支持多个lower OverlayFS层(最大到128层)。
    因此overlay2驱动为与层相关的docker命令(如docker build和docker commit)提供了更好的性能,并且比overlay驱动消耗更少的inodes。

    示例:镜像和容器在硬盘上的结构(overlay2)

    使用docker pull ubuntu命令下载一个5层的镜像后,你可以在/var/lib/docker/overlay2目录下看到6个目录。

    1. $ ls -l /var/lib/docker/overlay2
    2.  
    3. total 24
    4. drwx—— 5 root root 4096 Jun 20 07:36 223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7
    5. drwx—— 3 root root 4096 Jun 20 07:36 3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b
    6. drwx—— 5 root root 4096 Jun 20 07:36 4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1
    7. drwx—— 5 root root 4096 Jun 20 07:36 e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5
    8. drwx—— 5 root root 4096 Jun 20 07:36 eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed
    9. drwx—— 2 root root 4096 Jun 20 07:36 l

    l目录包含缩短的层标识符的软链接。这些缩短的标识符用于避免挂载参数的页大小限制。

    1. $ ls -l /var/lib/docker/overlay2/l
    2.  
    3. total 20
    4. lrwxrwxrwx 1 root root 72 Jun 20 07:36 6Y5IM2XC7TSNIJZZFLJCS6I4I4 -> ../3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff
    5. lrwxrwxrwx 1 root root 72 Jun 20 07:36 B3WWEFKBG3PLLV737KZFIASSW7 -> ../4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1/diff
    6. lrwxrwxrwx 1 root root 72 Jun 20 07:36 JEYMODZYFCZFYSDABYXD5MF6YO -> ../eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed/diff
    7. lrwxrwxrwx 1 root root 72 Jun 20 07:36 NFYKDW6APBCCUCTOUSYDH4DXAT -> ../223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff
    8. lrwxrwxrwx 1 root root 72 Jun 20 07:36 UL2MW33MSE3Q5VYIKBRN4ZAGQP -> ../e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5/diff

    最低层包含一个link文件,该文件包含缩短的标识符名称,diff目录包含其数据。

    1. $ ls /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/
    2.  
    3. diff  link
    4.  
    5. $ cat /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/link
    6.  
    7. 6Y5IM2XC7TSNIJZZFLJCS6I4I4
    8.  
    9. $ ls  /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff
    10.  
    11. bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

    第二层包含一个lower文件,该文件包含该层下一层的位置,diff目录包含该层数据。同时也包含了merged和work目录。

    1. $ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7
    2.  
    3. diff  link  lower  merged  work
    4.  
    5. $ cat /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/lower
    6.  
    7. l/6Y5IM2XC7TSNIJZZFLJCS6I4I4
    8.  
    9. $ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff/
    10.  
    11. etc  sbin  usr  var

    运行中的容器的目录也有类似的文件和目录。注意lower列表以冒号:分隔,并且从高层到低层排序。

    1. $ ls -l /var/lib/docker/overlay/<directory-of-running-container>
    2.  
    3. $ cat /var/lib/docker/overlay/<directory-of-running-container>/lower
    4.  
    5. l/DJA75GUWHWG7EWICFYX54FIOVT:l/B3WWEFKBG3PLLV737KZFIASSW7:l/JEYMODZYFCZFYSDABYXD5MF6YO:l/UL2MW33MSE3Q5VYIKBRN4ZAGQP:l/NFYKDW6APBCCUCTOUSYDH4DXAT:l/6Y5IM2XC7TSNIJZZFLJCS6I4I4

    mount的命令输出如下:

    1. $ mount | grep overlay
    2.  
    3. overlay on /var/lib/docker/overlay2/9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/merged
    4. type overlay (rw,relatime,
    5. lowerdir=l/DJA75GUWHWG7EWICFYX54FIOVT:l/B3WWEFKBG3PLLV737KZFIASSW7:l/JEYMODZYFCZFYSDABYXD5MF6YO:l/UL2MW33MSE3Q5VYIKBRN4ZAGQP:l/NFYKDW6APBCCUCTOUSYDH4DXAT:l/6Y5IM2XC7TSNIJZZFLJCS6I4I4,
    6. upperdir=9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/diff,
    7. workdir=9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/work)

    使用overlay进行容器的读和写

    下面是容器使用ovelay对文件进行的读取操作的三个场景。

  • 文件不在容器数据层。如果容器要打开读取一个文件且这个文件不在容器的upperdir目录,那它就从镜像的lowerdir读取。这个操作对性能影响很小。
  • 文件只存在于容器数据层。如果一个容器打开读取一个文件且这个在容器的upperdir不在镜像的lowerdir,那么直接从容器读取文件。
  • 文件既存在于容器数据层也存在于镜像数据层。如果一个容器打开一个既存在于镜像数据层也存在于容器数据层的文件,那么就读取在容器数据层的文件。这是因为在容器数据层upperdir的文件隐藏了在镜像数据层lowerdir的同一个文件。
  • 下面是容器中更改文件的几个场景。

  • 首次写入一个文件。首次在容器中写入一个存在的文件且文件不在容器upperdir。overlay/overlay2驱动执行一个copy_up操作来从镜像lowerdir复制文件到容器upperdir。然后容器把更改的数据写入到新复制过来的文件中。
    不过OverlayFS工作在文件级别而不是块级别上。意味着所有的OverlayFS copy-up操作复制的是整个文件,即使是文件很大但只修改一小部分。这会对容器的写性能造成显着的影响。不过,下面两点值得注意:
    1.copy_up操作只在对任意文件的首次写入时发生。随便对相同文件的写操作不再涉及到copy_up操作。
    2.OverlayFS只工作在两层。这意味着它的性能应该比AUFS对很多层的镜像搜索一个文件时的性能要好。
  • 删除文件和目录。当删除容器中的一个文件时,相当于在容器的upperdir创建了一个对应的空白(whiteout)文件。在镜像数据层lowerdir的文件版本不会被删除。只不过是容器中的空白(whiteout)文件隐藏了它。删除容器中的目录会在“upperdir”中创建不透明(opaque)的目录。这与一个空白(whiteout)文件的作用一样。这个不透明(opaque)目录有效地隐藏了镜像lowerdir同一个目录的存在。
  • 重命名目录。仅当源和目标路径都位于顶层(upperdir)时,才允许为目录调用rename(2)。否则返回EXDEV (“cross-device link not permitted”).
  • 配置Docker使用overlay/overlay2存储驱动

    要配置Docker使用ovelay存储驱动,你的Docker主机必须运行在加载有overlay内核模式的3.18版本的Linux内核或更高的版本上。对于overlay2驱动,内核版本必须为4.0或更高版本。OverlayFS能用在大多数支持的Linux文件系统上。不过在生产环境中推荐使用ext4。
    下面是配置Docker主机使用OverlayFS的步骤。假设你已经停止docker daemon。
    1.如果docker在运行,先停止它。
    2.验证内核版本和overlay内核模式是否加载

    1. $ uname -r
    2.  
    3.  3.19.0-21-generic
    4.  
    5.  $ lsmod | grep overlay
    6.  
    7.  overlay

    3.带overlay/overlay2存储驱动启动docker。

    1. $ dockerd –storage-driver=overlay &
    2.  
    3.  [1] 29403
    4.  root@ip-10-0-0-174:/home/ubuntu# INFO[0000] Listening for HTTP on unix (/var/run/docker.sock)
    5.  INFO[0000] Option DefaultDriver: bridge
    6.  INFO[0000] Option DefaultNetwork: bridge
    7.  <output truncated>

    另外,你可以通过编辑docker的配置文件增加–storage-driver=overlay到DOCKER_OPTS行来强制docker自动启动时启用overlay/overlay2驱动。
    4.验证docker是否已经是使用overlay/overlay2存储驱动。

    1. $ docker info
    2.  
    3.  Containers: 0
    4.  Images: 0
    5.  Storage Driver: overlay
    6.   Backing Filesystem: extfs
    7.  <output truncated>

    OverlayFS与Docker性能

    一般来说,overlay/overlay2驱动性能应该很好。大多时候会比aufs和devicemapper要好。在一些场景下可能比btrfs还高。我们在使用overlay/overlay2存储驱动时需要注意几点与性能相关的事项。

  • 页缓存。OverlayFS支持页缓存共享。意味着多个容器访问同一个文件可以共享单个页缓存条目。这使得overlay/overlay2驱动在内存利用方面高效,且是PaaS和其它类似场景的一个好选择。
  • copy_up。AUFS和OverlayFS在首次对容器的一个文件写入时都会执行copy-up操作。这会增加写操作的延迟 – 特别是如果要拷贝的文件非常大时。不过,一旦文件已经复制上去,随便的所有写操作都不需要copy-up操作了。
    OverlayFS的copy_up操作应该比AUFS同样的操作会快。因为AUFS比OverlayFS支持更多的层,在AUFS需要对非常多层的镜像搜索一个文件时就会出现这种情况。
  • Inode限制。使用overlay存储驱动可能会消耗更多的inode。特别是Docker主机上的镜像和容器数量很多时。一个Docker主机上有大量的镜像和大量的启动和停止容器时会更多消耗完inodes。overlay2没有这种问题。
  • 你只能在文件系统创建时指定inodes的数量。因此你可能希望把/var/lib/docker放置在有一个自己的文件系统的不同设备,或者在文件系统创建时手动指定inodes的数量。
    下面的两点性能优化同样适合于OverlayFS。

  • 固态设备(SSD)。 为了获得最佳性能,使用快速存储介质(如固态设备(SSD))总是个好主意。
  • 使用数据卷。 数据卷提供最佳和最可预测的性能。这是因为他们绕过存储驱动,并且不会引入任何通过精简置备和写时拷贝的潜在开销。
  • Docker用户指南(7) – Device Mapper存储驱动实践

    Device Mapper是一个基于内核的框架,支持Linux上的许多高级卷管理技术。Docker的devicemapper存储驱动利用这个框架的精简置备和快照功能来管理镜像和容器。本文简称Device Mapper存储驱动为devicemapper,内核框架为Device Mapper。

    AUFS的一个替代品

    Docker刚开始是运行在Ubuntu和Debian Linux系统,使用AUFS作为存储后端。随着Docker变得流行,许多公司想在Red Hat Enterprise Linux上使用Docker。不过因为Linux内核上游主线没有包括AUFS,RHEL也没有用AUFS。
    要解决这个问题,Red Hat开发人员着手调查让AUFS进内核主线。最终,他们决定开发一个新的存储后端。此外,它们基于现有的Device Mapper技术来开发新的存储后端。
    Red Hat与Docker公司合作开发这个新的驱动。由于这次合作,Docker公司把Engine的存储后端重新设计成可插拔的。所以devicemapper成为Docker支持的第二个存储驱动。
    从Linux内核版本2.6.9起,Device Mapper已经包含在Linux内核主线中。它是RHEL系列Linux发行版的核心部分。这意味着devicemapper存储驱动基于稳定的代码,具有大量实际生产环境部署和强大的社区支持。

    镜像分层和共享

    devicemapper驱动存储每个镜像和容器到它自己的虚拟设备上。这些设备是精简置备写时拷贝快照设备。Device Mapper技术工作在块级别而不是文件级别。意味着devicemapper存储驱动的精简置备和写时拷贝操作的是块而不是整个文件。
    使用devicemapper创建一个镜像的过程如下:

  • 1.devicemapper存储驱动创建一个精简池(thin pool)。这个池是从块设备或循环挂载的文件
  • 2.下一步是创建一个base设备。一个base设备是具有文件系统的精简设备。你可以通过运行docker info命令检查Backing filesystem来查看使用的是哪个文件系统。
  • 3.每一个新镜像(和镜像数据层)是这个base设备的一个快照。这些是精简置备写时拷贝快照。这意味着它们初始为空,只在往它们写入数据时才消耗池中的空间。
  • 使用devicemapper驱动时,容器数据层是从其创建的镜像的快照。与镜像一样,容器快照是精简置备写时拷贝快照。容器快照存储着容器的所有更改。当数据写入容器时,devicemapper从存储池按需分配空间。
    下图显示一个具有一个base设备和两个镜像的精简池。
    虚拟化技术
    如果你仔细查看图表你会发现快照一个连着一个。每一个镜像数据层是它下面数据层的一个快照。每个镜像的最底端数据层是存储池中base设备的快照。此base设备是Device Mapper的工件,而不是Docker镜像数据层。
    一个容器是从其创建的镜像的一个快照。下图显示两个容器 – 一个基于Ubuntu镜像和另一个基于Busybox镜像。
    虚拟化技术

    使用devicemapper读文件

    我们来看下使用devicemapper存储驱动如何进行读和写。下图显示在示例容器中读取一个单独的块[0x44f]的过程。
    虚拟化技术

  • 1.一个应用程序请求读取容器中0x44f数据块。由于容器是一个镜像的一个精简快照,它没有那数据。不过有存储在镜像堆栈下的镜像快照的数据的指针。
  • 2.存储驱动跟随指针找到与镜像数据层a005关联的快照数据块0xf33 …
  • 3.devicemapper从镜像快照复制数据块0xf33的内容到容器内存中。
  • 4.存储驱动返回数据给请求数据的应用程序。
  • 写示例

    使用devicemapper驱动,通过按需分配(allocate-on-demand)操作来实现写入新数据到容器。更新存在的数据使用写时拷贝(copy-on-write)操作。由于Device Mapper是基于块的技术,这些操作发生在块级别上。
    例如,当更新容器中一个大文件的一小部分,devicemapper存储驱动不会复制整个文件。它仅复制要更改的数据块。每个数据块是64KB。

    写入新数据

    要写入56KB的新数据到容器:

  • 1.一个应用程序请求写入56KB的新数据到容器。
  • 2.按需分配操作给容器快照分配一个新的64KB数据块。如果写操作大于64KB,就分配多个新数据块给容器快照。
  • 3.新的数据写入到新分配的数据块。
  • 覆盖存在的数据

    首次更改已存在的数据时:

  • 1.一个应用程序请求更新容器中的一些数据。
  • 2.写时拷贝操作定位需要更新的数据块。
  • 3.分配新的空白数据块给容器快照并复制数据到这些数据块。
  • 4.更新好的数据写入到新分配的数据块。
  • 容器中的应用程序不知道这些按需分配和写时拷贝操作。不过,这些操作可能会增加应用程序的读和写操作延迟。

    配置Docker使用devicemapper

    在一些Linux发行版本中,devicemapper是Docker的默认存储驱动。包括RHEL和它的大多数分支。目前,支持此驱动的发行版本如下:

  • RHEL/CentOS/Fedora
  • Ubuntu 12.04
  • Ubuntu 14.04
  • Debian
  • Arch Linux
  • Docker主机运行devicemapper存储驱动时,默认的配置模式为loop-lvm。此模式使用空闲的文件来构建用于镜像和容器快照的精简存储池。该模式设计为无需额外配置开箱即用(out-of-the-box)。不过生产部署不应该以loop-lvm模式运行。
    你可以使用docker info命令来检查目前使用的模式:

    1. $ sudo docker info
    2.  
    3. Containers: 0
    4. Images: 0
    5. Storage Driver: devicemapper
    6.  Pool Name: docker-202:2-25220302-pool
    7.  Pool Blocksize: 65.54 kB
    8.  Backing Filesystem: xfs
    9.  […]
    10.  Data loop file: /var/lib/docker/devicemapper/devicemapper/data
    11.  Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata
    12.  Library Version: 1.02.93-RHEL7 (2015-01-28)
    13.  […]

    上面的输出显示Docker主机运行的devicemapper存储驱动的模式为loop-lvm。因为Data loop file和Metadata loop file指向/var/lib/docker/devicemapper/devicemapper下的文件。这些文件是环回挂载(loopback mounted)文件。

    生产环境配置direct-lvm模式

    生产部署首选配置是direct-lvm。这个模式使用块设备来创建存储池。下面展示使用配置在使用devicemapper存储驱动的Docker主机上配置使用direct-lvm模式。
    下面的步骤创建一个逻辑卷,配置用作存储池的后端。我们假设你有在/dev/xvdf的充足空闲空间的块设备。也假设你的Docker daemon已停止。

  • 1.登录你要配置的Docker主机并停止Docker daemon。
  • 2.安装LVM2软件包。LVM2软件包含管理Linux上逻辑卷的用户空间工具集。
  • 3.创建一个物理卷
    $ pvcreate /dev/xvdf
  • 4.创建一个“docker”卷组
    1. $ vgcreate docker /dev/xvdf
  • 5.创建一个名为thinpool的存储池。
    在此示例中,设置池大小为“docker”卷组大小的95%。 其余的空闲空间可以用来自动扩展数据或元数据。

    1. $ lvcreate –wipesignatures y -n thinpool docker -l 95%VG
    2. $ lvcreate –wipesignatures y -n thinpoolmeta docker -l 1%VG
  • 6.转换存储池
    1. $ lvconvert -y –zero n -c 512K –thinpool docker/thinpool –poolmetadata docker/thinpoolmeta
  • 7.通过lvm profile配置存储池autoextension
    1. $ vi /etc/lvm/profile/docker-thinpool.profile
  • 8.设置thin_pool_autoextend_threshold值。这个值应该是之前设置存储池余下空间的百分比(100 = disabled)。
    1. thin_pool_autoextend_threshold = 80
  • 9.为当存储池autroextension发生时更改thin_pool_autoextend_percent值。
    该值的设置是增加存储池的空间百分比(100 =禁用)

    1. thin_pool_autoextend_percent = 20
  • 10.检查你的docker-thinpool.profile的设置。一个示例/etc/lvm/profile/docker-thinpool.profile应该类似如下:
    1. activation {
    2.     thin_pool_autoextend_threshold=80
    3.     thin_pool_autoextend_percent=20
    4. }
  • 11.应用新lvm配置
    1. $ lvchange –metadataprofile docker-thinpool docker/thinpool
  • 12.验证lv是否受监控。
    1. $ lvs -o+seg_monitor
  • 13.如果Docker daemon之前已经启动过,移动你的驱动数据目录到其它地方。
    1. $ mkdir /var/lib/docker.bk
    2. $ mv /var/lib/docker/* /var/lib/docker.bk
  • 14.配置一些特定的devicemapper选项。
    如果你是用dockerd命令行启动docker,使用如下参数:

    1. –storage-driver=devicemapper –storage-opt=dm.thinpooldev=/dev/mapper/docker-thinpool –storage-opt=dm.use_deferred_removal=true –storage-opt=dm.use_deferred_deletion=true

    你也可以在daemon.json启动配置文件设置它们,例如:

    1. {
    2.   "storage-driver": "devicemapper",
    3.    "storage-opts": [
    4.      "dm.thinpooldev=/dev/mapper/docker-thinpool",
    5.      "dm.use_deferred_removal=true",
    6.      "dm.use_deferred_deletion=true"
    7.    ]
    8. }
  • 15.如果你用的是systemd及通过unit或drop-in文件更新daemon配置文件,重载systemd来扫描配置更改。
    1. $ systemctl daemon-reload
  • 16.启动docker daemon
    1. $ systemctl start docker
  • 启动Docker daemon后,确保你监控存储池和卷组的可用空间。虽然卷组会自动扩展,它仍然会占满空间。要监控逻辑卷,使用lvs或lvs -a来查看数据和元数据大小。要监控卷组可用空间,使用vgs命令。
    当达到阈值时,可以通过日志查看存储池的自动扩展,使用如下命令:

    1. $ journalctl -fu dm-event.service

    检查主机上的devicemapper结构

    你可以使用lsblk命令来查看以上创建的设备文件和存储池。

    1. $ sudo lsblk
    2. NAME               MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
    3. xvda               202:0    0    8G  0 disk
    4. └─xvda1            202:1    0    8G  0 part /
    5. xvdf               202:80   0   10G  0 disk
    6. ├─vg–docker-data          253:0    0   90G  0 lvm
    7. │ └─docker-202:1-1032-pool 253:2    0   10G  0 dm
    8. └─vg–docker-metadata      253:1    0    4G  0 lvm
    9.   └─docker-202:1-1032-pool 253:2    0   10G  0 dm

    下图显示由lsblk命令输出的之前镜像的详细信息。
    虚拟化技术
    在这个图中,存储池名称为Docker-202:1-1032-pool,包括之前创建的data和metadata设备。devicemapper构造池名称如下:

    1. Docker-MAJ:MIN-INO-pool

    MAJ,MIN和INO指主设备号和次设备号和inode。
    因为Device Mapper在块级别操作,所以更难以看到镜像层和容器之间的差异。Docker 1.10和更高版本不再将镜像层ID与/var/lib/docker中的目录名称匹配。 但是,有两个关键目录。/var/lib/docker/devicemapper/mnt目录包含镜像层和容器层的挂载点。/var/lib/docker/devicemapper/metadata目录包含每个镜像层和容器快照对应的一个文件。这个文件包含每个快照的JSON格式元数据。

    增加正在使用设备的容器

    你可以增加使用中的存储池的容量。如果你的数据逻辑卷已满这会有所帮助。

    对于loop-lvm模式的配置

    在这个场景下,存储池配置为使用loop-lvm模式。使用docker info查看目前的配置:

    1. $ sudo docker info
    2.  
    3. Containers: 0
    4.  Running: 0
    5.  Paused: 0
    6.  Stopped: 0
    7. Images: 2
    8. Server Version: 1.11.0
    9. Storage Driver: devicemapper
    10.  Pool Name: docker-8:1-123141-pool
    11.  Pool Blocksize: 65.54 kB
    12.  Base Device Size: 10.74 GB
    13.  Backing Filesystem: ext4
    14.  Data file: /dev/loop0
    15.  Metadata file: /dev/loop1
    16.  Data Space Used: 1.202 GB
    17.  Data Space Total: 107.4 GB
    18.  Data Space Available: 4.506 GB
    19.  Metadata Space Used: 1.729 MB
    20.  Metadata Space Total: 2.147 GB
    21.  Metadata Space Available: 2.146 GB
    22.  Udev Sync Supported: true
    23.  Deferred Removal Enabled: false
    24.  Deferred Deletion Enabled: false
    25.  Deferred Deleted Device Count: 0
    26.  Data loop file: /var/lib/docker/devicemapper/devicemapper/data
    27.  WARNING: Usage of loopback devices is strongly discouraged for production use. Use `–storage-opt dm.thinpooldev` to specify a custom block storage device.
    28.  Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata
    29.  Library Version: 1.02.90 (2014-09-01)
    30. Logging Driver: json-file
    31. […]

    Data Space值显示存储池总共大小为100GB。此示例扩展存储池到200GB。
    1.列出设备的大小。

    1. $ sudo ls -lh /var/lib/docker/devicemapper/devicemapper/
    2.  
    3. total 1175492
    4. -rw——- 1 root root 100G Mar 30 05:22 data
    5. -rw——- 1 root root 2.0G Mar 31 11:17 metadata

    2.扩展data文件大小为200GB。

    1. $ sudo truncate -s 214748364800 /var/lib/docker/devicemapper/devicemapper/data

    3.验证更改的大小。

    1. $ sudo ls -lh /var/lib/docker/devicemapper/devicemapper/
    2.  
    3. total 1.2G
    4. -rw——- 1 root root 200G Apr 14 08:47 data
    5. -rw——- 1 root root 2.0G Apr 19 13:27 metadata

    4.重载数据loop设备

    1. $ sudo blockdev –getsize64 /dev/loop0
    2.  
    3. 107374182400
    4.  
    5. $ sudo losetup -c /dev/loop0
    6.  
    7. $ sudo blockdev –getsize64 /dev/loop0
    8.  
    9. 214748364800

    5.重载devicemapper存储池。
    1) 先获取存储池名称。

    1. $ sudo dmsetup status | grep pool
    2.  
    3. docker-8:1-123141-pool: 0 209715200 thin-pool 91
    4. 422/524288 18338/1638400 – rw discard_passdown queue_if_no_space –

    冒号前面部分是名称。
    2) 导出device mapper表:

    1. $ sudo dmsetup table docker-8:1-123141-pool
    2.  
    3. 0 209715200 thin-pool 7:1 7:0 128 32768 1 skip_block_zeroing

    3) 现在计算存储池的实际总扇区。
    更改表信息的第二个数字(即磁盘结束扇区),以反映磁盘中512字节扇区的新数。 例如,当新loop大小为200GB时,将第二个数字更改为419430400。
    4) 使用新扇区号重新加载存储池

    1. $ sudo dmsetup suspend docker-8:1-123141-pool
    2.     && sudo dmsetup reload docker-8:1-123141-pool –table ‘0 419430400 thin-pool 7:1 7:0 128 32768 1 skip_block_zeroing’
    3.     && sudo dmsetup resume docker-8:1-123141-pool

    device_tool

    Docker项目的contrib目录不是核心分发的一部分。 这些工具通常很有用,但也可能已过期。这个目录的device_tool.go可以调整loop-lvm精简池的大小。
    要使用该工具,首先编译它。 然后,执行以下操作调整池大小:

    1. $ ./device_tool resize 200GB

    对于direct-lvm模式的配置

    在此示例中,你将扩展使用direct-lvm模式设备的容量。此示例假设你使用的是/dev/sdh1磁盘分区。
    1.扩展卷组(VG)vg-docker。

    1. $ sudo vgextend vg-docker /dev/sdh1
    2.  
    3. Volume group "vg-docker" successfully extended

    2.扩展数据逻辑卷(LV)vg-docker/data

    1. $ sudo lvextend  -l+100%FREE -n vg-docker/data
    2.  
    3. Extending logical volume data to 200 GiB
    4. Logical volume data successfully resized

    3.重载devicemapper存储池。
    1) 获取存储池名称。

    1. $ sudo dmsetup status | grep pool
    2.  
    3. docker-253:17-1835016-pool: 0 96460800 thin-pool 51593 6270/1048576 701943/753600 – rw no_discard_passdown queue_if_no_space

    2)导出device mapper表。

    1. $ sudo dmsetup table docker-253:17-1835016-pool
    2.  
    3. 0 96460800 thin-pool 252:0 252:1 128 32768 1 skip_block_zeroing

    3)现在计算存储池的实际总扇区。 我们可以使用blockdev来获取数据lv的实际大小。
    更改表信息的第二个数(即扇区数)以反映磁盘中512个字节扇区的新数。 例如,由于新数据lv大小为264132100096字节,请将第二个数字更改为515883008。

    1. $ sudo blockdev –getsize64 /dev/vg-docker/data
    2.  
    3. 264132100096

    4)然后使用新的扇区号重新加载存储池。

    1. $ sudo dmsetup suspend docker-253:17-1835016-pool
    2.     && sudo dmsetup reload docker-253:17-1835016-pool
    3.       –table  ‘0 515883008 thin-pool 252:0 252:1 128 32768 1 skip_block_zeroing’
    4.     && sudo dmsetup resume docker-253:17-1835016-pool

    Device Mapper及Docker性能

    了解按需分配和写时拷贝操作对整体容器性能的影响很重要。

    按需分配对性能的影响

    devicemapper存储驱动通过按需分配操作给容器分配新的数据块。这意味着每次应用程序写入容器内的某处时,一个或多个空数据块从存储池中分配并映射到容器中。
    所有数据块为64KB。 写小于64KB的数据仍然分配一个64KB数据块。写入超过64KB的数据分配多个64KB数据块。这可能会影响容器性能,特别是在执行大量小写的容器中。不过一旦数据块分配给容器,后续的读和写可以直接在该数据块上操作。

    写时拷贝对性能的影响

    每当容器首次更新现有数据时,devicemapper存储驱动必须执行写时拷贝操作。这会从镜像快照复制数据到容器快照。此过程对容器性能产生显着影响。因此,更新一个1GB文件的32KB数据只复制一个64KB数据块到容器快照。这比在文件级别操作需要复制整个1GB文件到容器数据层有明显的性能优势。
    不过在实践中,使用devicemapper执行大量小块写入(<64KB)的容器比使用AUFS执行得更慢。

    device mapper其它性能注意事项

    还有其他一些影响devicemapper存储驱动性能的因素。

  • 模式。Docker使用的devicemapper存储驱动的默认模式是loop-lvm。这个模式使用空闲文件来构建存储池,性能非常低。不建议用到生产环境。推荐用在生产环境的模式是direct-lvm。
  • 高速存储。为获得最佳性能,应将数据文件和元数据文件放在高速存储(如SSD)上。
  • 内存使用。devicemapper在内存使用方面不是最有效率的。启动同一容器的n个副本会把n个副本加载到内存中。这可能影响你Docker主机的内存使用。因此,dervicemapper存储驱动可能不是PaaS和其他类似用例的最佳选择。
  • 最后一点,数据卷提供了最好的和最可预测的性能。这是因为他们绕过存储驱动,并且没有精简置备和写时拷贝引入的潜在开销。

    Docker用户指南(6) – Btrfs存储驱动实践

    Btrfs是下一代支持许多高级存储技术以使其更适合Docker的写时拷贝文件系统。Btrfs包含在Linux内核主线中以及现在稳定的硬盘格式(on-disk-format)。不过许多功能仍然在开发中,用户应该意识到此驱动目前正在快速发展中。
    Docker的btrfs存储驱动利用Btrfs的许多功能来管理镜像和容器。这些功能是精简置备(thin provisioning),写时拷贝和快照。
    本文把Docker的Btrfs存储驱动简称为btrfs,将整个Btrfs文件系统称为Btrfs。

    Btrfs的未来

    Btrfs一直被誉为是一个Linux的未来文件系统。在Linux内核主线的完全支持下,一个稳定的硬盘格式(on-disk-format),以及活跃的专注稳定性开发,离变成现实越来越近了。
    就Linux平台上的Docker而言,许多人将btrfs存储驱动视为潜在的长期替代devicemapper存储驱动。不过,到目前为止,devicemapper存储驱动仍然比btrfs更安全,更稳定和更适合用于生产环境。如果你对btrfs非常熟悉且已经有Btrfs的使用经验,这时你才应该考虑把btrfs部署到生产环境中。

    镜像分层与Btrfs共享

    Docker利用Btrfs subvolumes和快照来管理镜像和容器数据层的硬盘组件(on-disk components)。Btrfs subvolumes看起来像一个正常的Unix文件系统。因此,他们可以有自己的内部目录结构,挂钩到更广泛的Unix文件系统。
    subvolumes更新文件时涉及写时拷贝操作,写入新文件时涉及从一个底层存储池来按需分配空间的操作。它们既能嵌套也能做快照。下图显示了4个subvolumes。“subvolume 2″和”subvloume 3″是嵌套的,而“subvolume 4”显示它自己的内部目录树。
    虚拟化技术
    快照是在一个时间点对整一个subvolume做的一个副本。它们直接存在于从其创建的subvolume下面。你可以像下图显示的那样创建快照的快照。
    虚拟化技术
    Btfs从一个底层存储池给subvolumes和快照(snapshots)按需分配空间。分配单元称为块(chunk),并且块通常大小为〜1GB。
    快照看起来和操作跟常规的subvolumes类似。创建快照所需的技术由于Btrfs本地的写时拷贝设计而构建进Btrfs文件系统中。这意味着Btrfs快照空间利用率高且很少或没有性能开销。下图显示一个subvolume与它的快照共享同样的数据。
    虚拟化技术
    Docker的btrfs存储驱动把每个镜像和容器数据层存储到Btrfs subvolume或快照。镜像的基础数据层(最底层)作为一个subvolume存储,而镜像子数据层和容器作为快照存储。如下图所示。
    虚拟化技术
    使用btrfs驱动的Docker主机创建镜像和容器的过程如下:

  • 1.镜像的基础数据层存储在/var/lib/docker/btrfs/subvolumes的Btrfs subvloume中。
  • 2.后续的镜像数据层存储为subvolume或快照的父级数据层的一个Btrfs快照中。
  • 下图显示一个3个数据层的镜像。base layer是一个subvolume。layer 1是base layer的subvolume的一个快照。layer 2是layer1快照的快照。
    虚拟化技术
    从docker 1.10开始,镜像数据层ID不再与在/var/lib/docker的目录名称相关。

    镜像和容器在硬盘的结构

    镜像数据层和容器在docker主机的文件系统的/var/lib/docker/btrfs/subvolumes/目录可见。不过,如之前所说的,目录名不再与镜像数据层ID相关。那就是说,容器的目录即使容器已经停止都存在。这是因为btrfs存储驱动在/var/lib/docker/subvolumes/下挂载一个默认的,顶层的subvolume。所有的其它subvolumes和快照为作Btrfs文件系统对象存在,而不是作为单独挂载存在。
    因为Btrfs工作在文件系统级别上而不是块级别,所以可以使用常规的Unix命令来浏览器每个镜像和容器数据层。下面的示例显示使用ls -l命令浏览一个镜像数据层:

    1. $ ls -l /var/lib/docker/btrfs/subvolumes/0a17decee4139b0de68478f149cc16346f5e711c5ae3bb969895f22dd6723751/
    2.  
    3. total 0
    4. drwxr-xr-x 1 root root 1372 Oct  9 08:39 bin
    5. drwxr-xr-x 1 root root    0 Apr 10  2014 boot
    6. drwxr-xr-x 1 root root  882 Oct  9 08:38 dev
    7. drwxr-xr-x 1 root root 2040 Oct 12 17:27 etc
    8. drwxr-xr-x 1 root root    0 Apr 10  2014 home
    9. …output truncated…

    使用Btrfs进行容器读和写

    一个容器是一个镜像的空间高效利用的快照。快照的元数据指向存储池的实际数据块。这与subvolume相同。因此,对快照执行的读取与对subvolume执行的读取本质相同。所以Btrfs驱动不会产生任何性能开销。
    写入一个新文件到容器涉及到一个按需分配(allocate-on-demand)操作来为容器快照分配一个新的数据块。然后文件写入到这个新空间。按需分配操作对于使用Btrfs的所有写入都是原生的,写入新数据到subvolume一样涉及到到此操作。因此,将新文件写入到容器快照的速度与本地Btrfs写入速度一样。
    在容器中更新一个存在的文件将产生一个写时拷贝操作(技术上称为redirect-on-write)。驱动保留原始数据并为快照分配新空间。更新的数据写入到这个新空间。然后驱动更新快照文件系统元数据指向新的数据。原始数据仍然保留在subvolumes中。
    使用Btfs写入和更新大量小文件可能会导致性能降低。

    配置Docker使用Btrfs

    btrfs存储驱动只在Docker主机的/var/lib/docker挂载为Btrfs文件系统上操作。下面介绍如何在Ubuntu 14.04 LTS上配置Btrfs。

    先决条件

    如果你已经在Docker主机上使用Docker daemon,并且你想保持现有的镜像,在进行下面的配置前先把镜像push到Docker Hub或者其它地方。
    停止Docker daemon,然后确保你在/dev/xvdb有一个闲置的块设备。设备标识符在你的环境中可能不同,你应该在整个过程中替换你自己的值。
    下面的步骤也假设你的内核已经正确加载了Btrfs模块。要验证它,使用如下命令:

    1. $ cat /proc/filesystems | grep btrfs
    2.  
    3.         btrfs

    在Ubuntu 14.04 LTS配置Btrfs

    假设你系统已经满足了前面提到的条件,执行如下:
    1.安装btrfs-tools包。

    1. $ sudo apt-get install btrfs-tools
    2.  
    3.  Reading package lists… Done
    4.  Building dependency tree
    5.  <output truncated>

    2.创建Btrfs存储池。
    Btrfs存储池使用mkfs.btrfs命令创建。传递多个设备给mkfs.btrfs命令将在所有这些设备之间创建一个存储池。
    这里我们使用在/dev/xvdb的单个设备创建一个存储池。

    1. $ sudo mkfs.btrfs -f /dev/xvdb
    2.  
    3.  WARNING! – Btrfs v3.12 IS EXPERIMENTAL
    4.  WARNING! – see http://btrfs.wiki.kernel.org before using
    5.  
    6.  Turning ON incompat feature ‘extref’: increased hardlink limit per file to 65536
    7.  fs created label (null) on /dev/xvdb
    8.      nodesize 16384 leafsize 16384 sectorsize 4096 size 4.00GiB
    9.  Btrfs v3.12

    3.如果Docker主机本地存储区域不存在,先创建。

    1. $ sudo mkdir /var/lib/docker

    4.配置系统重启时自动挂载Btrfs文件系统。
    a.获取Btrfs文件系统的UUID。

    1. $ sudo blkid /dev/xvdb
    2.  
    3.  /dev/xvdb: UUID="a0ed851e-158b-4120-8416-c9b072c8cf47" UUID_SUB="c3927a64-4454-4eef-95c2-a7d44ac0cf27" TYPE="btrfs"

    b.在/etc/fstab添加一行来让系统每次启动时自动挂载。

    1. /dev/xvdb /var/lib/docker btrfs defaults 0 0
    2.  UUID="a0ed851e-158b-4120-8416-c9b072c8cf47" /var/lib/docker btrfs defaults 0 0

    5.挂载新的文件系统并验证

    1. $ sudo mount -a
    2.  
    3.  $ mount
    4.  
    5.  /dev/xvda1 on / type ext4 (rw,discard)
    6.  <output truncated>
    7.  /dev/xvdb on /var/lib/docker type btrfs (rw)

    上面输出的最后一行显示/dev/xvdb作为Btrfs挂载在/var/lib/docker。
    现在你已经有一个挂载在/var/lib/docker的Btrfs文件系统,docker daemon应该能自动加载btrfs存储驱动了。
    1.启动docker daemoon。

    1. $ sudo service docker start
    2.  docker start/running, process 2315

    启动之后Docker会自动加载Btrfs,不过你可以指定–storage-driver=btrfs或添加DOCKER_OPTS行来强制Docker使用btrfs。
    2.使用docker info命令验证

    1. $ sudo docker info
    2.  
    3.  Containers: 0
    4.  Images: 0
    5.  Storage Driver: btrfs
    6.  […]

    现在docker已经配置使用btrfs存储驱动了。

    Btrfs及Docker性能

  • 页缓存(page caching)。Btrfs不支持页缓存共享。意味着n个容器访问同一个文件需要n个副本缓存。因此,Btrfs可能不是PaaS和类似使用场景的最好选择。
  • 小文件写。容器执行大量的小文件写操作(包括Docker主机启动和停止大量容器)会导致Btrfs块利用率低的问题。这可能最终导致docker主机空间很快不足的情况,致命影响docker运行。这是目前使用当前版本的Btrfs的主要缺点。如果你使用btrfs存储驱动,需要使用btrfs filesys来密切监控空间使用情况。不要使用常规的Unix命令如df来查看空间使用情况,因为有可能会显示不正确;推荐始终使用Btrfs原生的命令。
  • 顺序写。Btrfs通过日志技术(journaling technique)将数据写入硬盘。其性能可以提升一半。
  • 碎片(Fragmentation)。碎片是写时拷贝文件系统(如Btrfs)的自然副产品。 许多小的随机写入可以导致这个问题。 它表现为使用SSD硬盘的Docker主机上的CPU峰值和使用机械硬盘的Docker主机上的抖动。 这两者都导致差的性能。
    最新版本的Btrfs允许你将autodefrag指定为mount选项。 此模式尝试检测随机写入和碎片整理。 在Docker主机上启用此选项之前,你应该先自己测试下。 一些测试显示此选项对执行大量小型写入的Docker主机(包括启动和停止许多容器的系统)具有负面性能影响。
  • 固态设备(SSD)。 Btrfs具有SSD介质的本机优化。 要启用这些选项,请使用-o ssd mount选项进行挂载。 这些优化包括通过避免诸如在SSD介质上没有用的查找优化等增强的SSD写性能。
    Btfs还支持原生的TRIM / Discard。 但是,使用-o discard mount选项进行挂载可能会导致性能问题。 因此,建议你在使用此选项之前先测试下。
  • 使用数据卷(data volumes)。 数据卷提供最佳和最可预测的性能。这是因为他们绕过存储驱动,并且不承担任何通过精简置备和写时拷贝引入的潜在开销。
  • Docker用户指南(5) – AUFS存储驱动实践

    AUFS是Docker使用的第一个存储驱动。因此,它与Docker有着悠久而密切的历史,AUFS非常稳定,部署在大量真实的生产环境中,并且拥有强大的社区支持。AUFS有几个特性使其成为Docker不错的选择。
    这些特性是:

  • 快速启动容器
  • 高效存储使用
  • 高效内存使用
  • 尽管它功能强大和与Docker的悠久历史,一些Linux发行版本已经不支持AUFS。这个通常是因为AUFS不在Linux内核主线。
    下面开始检查AUFS的一些功能以及它们与Docker的关系。

    AUFS的镜像分层与共享

    AUFS是一个统一文件系统。意思是它管理着单台Linux主机上的多个目录,把它们互相堆叠并提供一个统一视图。AUFS使用了联合挂载(union mount)。
    AUFS堆叠多个目录并通过一个联合挂载来提供一个统一的视图。堆栈中的所有目录以及联合挂载点必须都存在于同一Linux主机上。AUFS引用作为分支堆叠的每个目录。
    在Docker中,AUFS联合挂载支持镜像分层。AUFS存储驱动使用联合挂载系统来实现Docker的镜像分层。AUFS分支对应Docker镜像数据层。下图显示基于ubuntu:latest镜像的docker容器。
    虚拟化技术
    图中显示的每一个镜像数据层和容器数据层,表示为Docker主机上/var/lib/docker路径下的一个目录。联合挂载点提供了所有数据层的统一视图。从Docker 1.10开始,镜像数据层ID不再与包含它们数据的目录的名称相关。
    AUFS也支持不是所有存储驱动都支持的写时拷贝(CoW)技术。

    AUFS读和写

    Docker利用AUFS CoW技术实现镜像共享并最小化磁盘空间的使用。AUFS操作在文件级别上。意味着AUFS CoW技术将复制整个文件 – 即使是只更改一个文件的一小部分。此行为对容器性能有显着影响,特别是要复制的文件非常大,文件在层级很多的数据层下或者CoW操作必须搜索深度目录树。
    例如,运行在容器中的一个程序需要添加一个新的值到一个大的键值存储文件。如果是首次更改这个文件,它还没有在容器的顶部可写数据层。那么CoW必须从底层镜像复制出这个文件。AUFS存储驱动在每个镜像数据层搜索这个文件。搜索的顺序是从上到下。当找到后,复制整个文件到容器的顶部可写数据层。之后才能打开和修改这个文件了。
    大的文件复制需要的时间明显比小的文件要多,存在于较低数据层的文件比在较高数据层的文件所需时间多。不过,在任何一个容器中的文件只会产生一次copy-up操作。随后的这个文件的读和写都发生在容器的顶部数据层。

    AUFS删除文件

    AUFS存储驱动通过在容器数据层放置一个空白(whiteout)文件来从容器删除一个文件。空白文件有效地掩盖了下面的只读镜像层中文件的存在。下图显示了基于3个数据层的镜像的容器。
    虚拟化技术
    file3已经从容器中删除。因此AUFS存储驱动放置一个空白文件到容器数据层。这个空白文件通过隐藏镜像只读数据层中的任意原始文件的存在来有效地“删除”了容器的file3文件。

    AUFS重命名文件

    在AUFS中调用rename(2)函数来重命名目录没有被完全支持。它返回EXDEV(“cross-device link not permitted”),即使源文件和目标文件在同一个AUFS数据层,除非目录没有子级。
    所以你的程序应该设计能处理EXDEV并回退到“复制再删除”策略。

    配置Docker使用AUFS

    你可以在安装有AUFS的Linux系统上只使用AUFS存储驱动。使用以下的命令为判断你的系统是否支持AUFS。

    1. $ grep aufs /proc/filesystems
    2.  
    3. nodev   aufs

    输出表明系统支持AUFS。一旦你验证系统支持AUFS后,你必须设置Docker daemon使用它。使用dockerd命令启用AUFS的方法:

    1. $ sudo dockerd –storage-driver=aufs &

    或者你可以编辑Docker配置文件在DOCKER_OPTS行添加–storage-driver=aufs:

    1. # Use DOCKER_OPTS to modify the daemon startup options.
    2. DOCKER_OPTS="–storage-driver=aufs"

    daemon运行后,使用docker info为验证存储驱动。

    1. $ sudo docker info
    2.  
    3. Containers: 1
    4. Images: 4
    5. Storage Driver: aufs
    6.  Root Dir: /var/lib/docker/aufs
    7.  Backing Filesystem: extfs
    8.  Dirs: 6
    9.  Dirperm1 Supported: false
    10. Execution Driver: native-0.2
    11. …output truncated…

    本地存储与AUFS

    当dockerd使用AUFS驱动运行,驱动把镜像和容器存储到docker主机本地存储区域的/var/lib/docker/aufs/目录下。

    镜像

    镜像数据层及它们的数据存储在/var/lib/docker/aufs/diff/目录下。从docker 1.10起,镜像数据层ID不再与目录名称相关。
    /var/lib/docker/aufs/layers/目录包含镜像数据层如何堆叠的元数据。每一个镜像数据层或容器数据层都可以在这个目录下找到对应的一个文件(虽然文件名称不再与镜像数据层ID匹配)。每个文件包含该文件对应的数据层之下的数据层的目录名称列表。
    以下命令显示了/var/lib/docker/aufs/layers/中的元数据文件的内容,列出了在union mount中堆叠在其下面的三个目录。记住,从Docker 1.10起这些目录名没有映射到镜像层ID。

    1. $ cat /var/lib/docker/aufs/layers/91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c
    2.  
    3. d74508fb6632491cea586a1fd7d748dfc5274cd6fdfedee309ecdcbc2bf5cb82
    4. c22013c8472965aa5b62559f2b540cd440716ef149756e7b958a1b2aba421e87
    5. d3a1f33e8a5a513092f01bb7eb1c2abf4d711e5105390a3fe1ae2248cfde1391

    一个镜像最底层的数据层之下已经没有数据层,所以它的文件内容应该是空的。

    容器

    运行的容器挂载在/var/lib/docker/aufs/mnt/下。这个是AUFS联合挂载点所在的位置,它为容器和镜像数据层提供了一个统一的视图。如果容器没有运行,目录仍然存在但是空的。这是因为AUFS只挂载运行的容器。从Docker 1.10起,容器ID不再与/var/lib/docker/aufs/mnt/下的目录名相关。
    容器元数据和各种配置文件放置在/var/lib/docker/containers/目录。所有的容器的这个目录都存在系统上,包括已经停止的容器。运行的容器的日志文件也在这个目录。
    容器的可写数据层存储在/var/lib/docker/aufs/diff/目录。即使容器停止后,数据层目录仍然存在。意味着重启容器不会丢失容器数据。不过一旦删除容器,相应的数据层也会被删除。

    AUFS和Docker性能

    总结一些已经提到的性能相关方面:

  • AUFS存储驱动是对于容器性能很看重的PaaS和其他类似用例的理想选择。这个因为AUFS能有效的在不同运行容器共享镜像,快速启动容器以及占用最少的硬盘空间。
  • AUFS在镜像数据层和容器之间共享文件的基本机制,使得能高效利用系统页面缓存达到快速启动容器的目的。
  • AUFS可能会影响容器的写入性能。这是因为对容器文件的首次更改,必须先定位并复制文件到容器顶部可写数据层后才真正开始更新文件。特别是文件在许多数据层之下和文件非常大的情况下,性能影响会更大。
  • 最后一点,Data volumes能提供最佳和可预测的性能。因为文件的操作会绕过存储驱动,这样就不会由于写时拷贝引入潜在的性能开销。

    AUFS兼容性

    总结与其他文件系统不兼容的AUFS的方面:

  • AUFS不完全支持rename(2)系统调用。 你的应用程序需要检测其故障,并回退到“复制并删除文件”策略。
  • Docker用户指南(4) – 存储驱动选择

    本文介绍Docker存储驱动的特性。列出Docker支持的存储驱动以及管理它们相关的命令。最后,为你在选择存储驱动时提供数据参考。

    可插拔的存储驱动架构

    Docker的存储驱动架构设计成可插拔的,这可以让你能根据你的环境和使用场景灵活地选择使用一个存储驱动。每个Docker存储驱动基于Linux文件系统或卷管理器。此外,每个存储驱动器以其自己独特的方式自由地实现镜像数据层和容器数据层的管理。这意味着一些存储驱动在不同的情况下会比其它的驱动会有更好的表现。
    一旦你决定了哪个驱动对于你是最好的,你可以在Docker启动时设置为这个驱动。Docker daemon只能运行一个存储驱动,之后所有由docker daemon创建的容器都使用同样的存储驱动。下面的表格显示docker支持的存储驱动技术及它们驱动的名称:

    技术 存储驱动名称
    OverlayFS overlay或overlay2
    AUFS aufs
    Btrfs btrfs
    Device Mapper devicemapper
    VFS vfs
    ZFS zfs

    要找出目前docker daemon使用的是哪个存储驱动,可以使用docker info命令:

    1. $ docker info
    2.  
    3. Containers: 0
    4. Images: 0
    5. Storage Driver: overlay
    6.  Backing Filesystem: extfs
    7. Execution Driver: native-0.2
    8. Logging Driver: json-file
    9. Kernel Version: 3.19.0-15-generic
    10. Operating System: Ubuntu 15.04
    11. … output truncated …

    info子命令显示出Docker使用的存储驱动是overlay,Backing Filesystem为extfs。extfs值意思是overlay存储驱动在现有的ext文件系统之上操作。Backing Filesystem指的是在/var/lib/docker创建docker主机本地存储区域的文件系统。你能选择使用哪些存储驱动部分取决于你计划在哪个backing filesystem创建docker主机本地存储区域。一些存储驱动能在不同的backing filesystems上操作。不过有些存储驱动需要与存储驱动相同的backing filesystem。例如btrfs存储驱动需要Btrfs backing filesystem。下面的表格列出每个存储驱动以及是否必须匹配主机backing filesystem:

    存储驱动 可用的文件系统 禁用的文件系统
    overlay ext4 xfs btrfs aufs overlay overlay2 zfs eCryptfs
    overlay2 ext4 xfs btrfs aufs overlay overlay2 zfs eCryptfs
    aufs ext4 xfs btrfs aufs eCryptfs
    btrfs btrfs only N/A
    devicemapper direct-lvm N/A
    vfs debugging only N/A
    zfs zfs only N/A

    你可以在dockerd命令传递–storage-driver=参数来设置存储驱动,或者在/etc/default/docker文件中设置DOCKER_OPTS选项。
    下面的命令显示如何使用dockerd命令以及–storage-driver参数来设置存储驱动为devicemapper:

    1. $ dockerd –storage-driver=devicemapper &
    2.  
    3. $ docker info
    4.  
    5. Containers: 0
    6. Images: 0
    7. Storage Driver: devicemapper
    8.  Pool Name: docker-252:0-147544-pool
    9.  Pool Blocksize: 65.54 kB
    10.  Backing Filesystem: extfs
    11.  Data file: /dev/loop0
    12.  Metadata file: /dev/loop1
    13.  Data Space Used: 1.821 GB
    14.  Data Space Total: 107.4 GB
    15.  Data Space Available: 3.174 GB
    16.  Metadata Space Used: 1.479 MB
    17.  Metadata Space Total: 2.147 GB
    18.  Metadata Space Available: 2.146 GB
    19.  Thin Pool Minimum Free Space: 10.74 GB
    20.  Udev Sync Supported: true
    21.  Deferred Removal Enabled: false
    22.  Data loop file: /var/lib/docker/devicemapper/devicemapper/data
    23.  Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata
    24.  Library Version: 1.02.90 (2014-09-01)
    25. Execution Driver: native-0.2
    26. Logging Driver: json-file
    27. Kernel Version: 3.19.0-15-generic
    28. Operating System: Ubuntu 15.04
    29. <output truncated>

    你选择的存储驱动会影响你容器化应用的性能。所以理解不同的存储驱动可用选项并为你的应用选择一个合适的驱动很重要。最后,在本文你会找到帮助你选择一个适合的驱动的建议。

    你应该选择哪个存储驱动

    有几个因素会影响存储驱动的选择。不过这两个因素要特别注意:

  • 1.没有哪个驱动非常适合每一个使用场景
  • 2.存储驱动在不断改进和发展
  • 考虑到这些因素,以下应该能为你提供一些指导。

    稳定性

    为了最稳定和良好的docker体验,你应该考虑以下:

  • 使用默认的存储驱动。当docker安装时,它会根据你系统的配置选择一个默认的存储驱动。稳定性是影响选择一个默认的存储驱动的重要因素。更改这个默认驱动可能增加你使用docker的bug。
  • 遵循CS Engine兼容性列表中指定的配置。CS Engine是Docker Engine的商业支持版本。它的代码库与开源Engine相同,不过它有一组受限的支持配置。这些支持的配置使用最稳定和成熟的存储驱动程序。
  • 经验和专长

    选择一个你和你团队/组织有使用经验的存储驱动。例如,如果你使用的是RHEL或其下游分支之一,你可能已经具有LVM和Device Mapper的使用经验。如果是这样,你可能希望使用devicemapper驱动。
    如果你对任何的存储驱动都没有使用经验,并且你想要一个易于使用的稳定的Docker体验,你应该考虑使用docker安装时设置的默认驱动。

    面向未来

    许多用户认为docker未来存储驱动是OverlayFS。不过,它还不太成熟,对于已经成熟的驱动如aufs和devicemapper不够稳定。由于这个原因,你应该慎重使用OverlayFS驱动,并且作好相对于使用一个更成熟驱动遇到更多bug的准备。
    下面的图表列出的每一个存储驱动的优势和劣势。在选择要使用的存储驱动程序时,请考虑下表提供的指导以及上述要点。
    虚拟化技术

    Overlay vs Overlay2

    OverlayFS有两个使用相同的OverlayFS技术但是不同的实现及硬盘存储的不兼容的存储驱动。由于存储上不兼容,在两者之间切换需要重建所有的镜像。overlay驱动是原始的实现,是Docker 1.11和之前版本的唯一选项。overlay驱动具有已知的inode耗尽问题以及提交性能的限制。overlay2驱动解决了这些问题,不过仅与Linux内核4.0及更高的版本兼容。对于使用4.0之间的内核或已存在overlay graph的用户,推荐继续使用overlay。对于使用至少4.0内核版本的,或所需overlay graph数据的用户,那么可以使用overlay2。

    Docker用户指南(3) – 理解镜像,容器和存储驱动

    为了更有效地使用存储驱动,你必须理解Docker是如何构建和存储镜像的。然后,你需要对镜像是如何被容器使用作个了解。最后,你需要一段关于镜像和容器共同使用的技术的简洁的介绍。

    镜像和数据层

    每个Docker镜像引用一个或多个代表文件系统差异的只读数据层。数据层彼此堆叠来组成容器的根文件系统。下面的图表表示Ubuntu 15.04镜像由4个堆叠的数据层组成。
    虚拟化技术
    Docker存储驱动负责堆叠这些数据层和提供一个单独的统一视图。
    当你创建一个容器,同时也在底层堆栈顶部创建了一个新的,薄的,可写的数据层。这个数据层也称为”容器数据层(container layer)“。所有对运行容器的更改 – 如写新文件,更新文件和删除文件 – 都是写到这个数据层。下面的图表显示基于Ubuntu 15.04镜像的容器。
    虚拟化技术

    内容寻址存储

    Docker 1.10引入了一个新的内容寻址存储模型。这是一个全新的方法来定位硬盘上的镜像和数据层数据。之前的版本,镜像和数据层通过使用随机生成的UUID来引用。在这个新的模型使用了安全哈希(secure content hash)来代替。
    新的模式提高了安全性,提供了一个内置的方式来避免ID冲突,并且在pull,push,load,save后保证数据完整性。同时也通过允许镜像(即使它们不是由相同的Dockerfile构建)自由地共享它们的数据层来获取更好的使用体验。
    在使用新模式前需要注意的是:

  • 1.迁移现有的镜像
  • 2.镜像和数据层文件系统结构
  • 那些使用早期Docker版本创建和拉取的镜像,在与新模式一起使用前需要进行迁移。迁移操作涉及计算新的安全checksum,这个操作是在你首次启动新的Docker版本时自动完成的。迁移完成后,所有的镜像都会具有全新的安全ID。
    虽然迁移是自动和透明的,但是要使用比较多的计算资源。意味着当你有许多镜像需要计算时要花费比较长的时间。在这期间Docker daemon不会响应其它请求。
    Docker为此把迁移工具单独出来,允许你在升级Docker之前先把镜像迁移好。这样可以避免长时间的停机时间。
    迁移工具以容器方式运行,可以到这里下载https://github.com/docker/v1.10-migrator/releases。
    如果你使用的是默认的docker数据路径,手动迁移命令如下:

    1. $ sudo docker run –rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator

    如果你用的是devicemapper存储驱动,你需要添加–privileged参数来让容器可以访问存储设备。

    迁移示例

    下面是在Docker 1.9.1,AUFS存储驱动的环境下使用迁移工具的示例.Docker主机运行在配置为1 vCPU, 1GB RAM以及单独的8G SSD的t2.micro AWS EC2实例。Docker数据目录(/var/lib/docker)占用2GB空间。

    1. $ docker images
    2.  
    3. REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    4. jenkins             latest              285c9f0f9d3d        17 hours ago        708.5 MB
    5. mysql               latest              d39c3fa09ced        8 days ago          360.3 MB
    6. mongo               latest              a74137af4532        13 days ago         317.4 MB
    7. postgres            latest              9aae83d4127f        13 days ago         270.7 MB
    8. redis               latest              8bccd73928d9        2 weeks ago         151.3 MB
    9. centos              latest              c8a648134623        4 weeks ago         196.6 MB
    10. ubuntu              15.04               c8be1ac8145a        7 weeks ago         131.3 MB
    11.  
    12. $ sudo du -hs /var/lib/docker
    13.  
    14. 2.0G    /var/lib/docker
    15.  
    16. $ time docker run –rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator
    17.  
    18. Unable to find image ‘docker/v1.10-migrator:latest’ locally
    19. latest: Pulling from docker/v1.10-migrator
    20. ed1f33c5883d: Pull complete
    21. b3ca410aa2c1: Pull complete
    22. 2b9c6ed9099e: Pull complete
    23. dce7e318b173: Pull complete
    24. Digest: sha256:bd2b245d5d22dd94ec4a8417a9b81bb5e90b171031c6e216484db3fe300c2097
    25. Status: Downloaded newer image for docker/v1.10-migrator:latest
    26. time="2016-01-27T12:31:06Z" level=debug msg="Assembling tar data for 01e70da302a553ba13485ad020a0d77dbb47575a31c4f48221137bb08f45878d from /var/lib/docker/aufs/diff/01e70da302a553ba13485ad020a0d77dbb47575a31c4f48221137bb08f45878d"
    27. time="2016-01-27T12:31:06Z" level=debug msg="Assembling tar data for 07ac220aeeef9febf1ac16a9d1a4eff7ef3c8cbf5ed0be6b6f4c35952ed7920d from /var/lib/docker/aufs/diff/07ac220aeeef9febf1ac16a9d1a4eff7ef3c8cbf5ed0be6b6f4c35952ed7920d"
    28. <snip>
    29. time="2016-01-27T12:32:00Z" level=debug msg="layer dbacfa057b30b1feaf15937c28bd8ca0d6c634fc311ccc35bd8d56d017595d5b took 10.80 seconds"
    30.  
    31. real    0m59.583s
    32. user    0m0.046s
    33. sys     0m0.008s

    Unix time命令放在docker run命令前面来统计其运行时间。正如你所看到了,迁移大小为2GB的7个镜像总体时间将近1分钟。不过这个时间包括了拉取镜像/docker/v1.10-migrator镜像的时间(大约为3.5秒)。同样的操作在一个配置为40 vCPUs, 160GB RAM和一个8GB SSD的m4.10xlarge EC2 instance实例花费的时间少得多:

    1. real    0m9.871s
    2. user    0m0.094s
    3. sys     0m0.021s

    这个示例表明了迁移操作耗费的时间受机器硬件配置的影响。

    容器和数据层

    容器和镜像的主要区别是顶部的可写数据层。所有对容器进行文件添加或文件更新的操作都会存储到这个可写数据层。当容器被删除后,这个可写数据层也被删除了。而底层的镜像仍然不变。
    由于每个容器有自己的可写容器数据层,并且所有的更改都储存到这个数据层,意味着多个容器可以共享访问同一个底层镜像且有它们自己的数据状态。下面的图表显示多个容器共享一个相同的Ubuntu 15.04镜像。
    虚拟化技术
    Docker存储驱动负责激活和管理镜像数据层和可写容器数据层。不同的存储驱动处理这两个数据层的方式有所不同。Docker镜像和容器管理背后两个关键技术是可堆叠镜像数据层和写时拷贝(copy-on-write)。

    写时拷贝策略

    写时拷贝策略与共享和复制类似。需要相同数据的系统进程共享该数据,而不是各自拥有自己的副本。在某些时候,如果一个进程需要更新或写入数据,操作系统就为该进程拷贝一份数据使用。只有需要写入数据的系统有权限访问数据副本。所有其它进程继续使用原始的数据。
    Docker对镜像和容器都使用了写时拷贝技术。写时拷贝策略优化了镜像硬盘占用和容器启动时间的性能。接下来我们来看看写时拷贝技术是如何通过共享和复制影响镜像和容器的。

    共享使镜像更小

    现在我们来了解镜像数据层和写入拷贝技术。所有的镜像和容器数据层存储在由存储驱动管理的Docker主机本地存储区域内。在基于Linux的Docker主机这个目录是/var/lib/docker/。
    当使用docker pull和docker push拉取和推送镜像时,docker客户端将输出镜像数据层报告。下面的命令是从Docker Hub拉取ubuntu:15.04镜像。

    1. $ docker pull ubuntu:15.04
    2.  
    3. 15.04: Pulling from library/ubuntu
    4. 1ba8ac955b97: Pull complete
    5. f157c4e5ede7: Pull complete
    6. 0b7e98f84c4c: Pull complete
    7. a3ed95caeb02: Pull complete
    8. Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
    9. Status: Downloaded newer image for ubuntu:15.04

    从输出中我们看到命令实际上拉取了4个镜像数据层。上面的每一行列出了一个镜像数据层和它的UUID或加密散列。这4个数据层混合组成了ubuntu:15.04 Docker镜像。
    每一个数据层都存储在Docker主机本地存储区域内的它们自己的目录。
    Docker 1.10之前的版本把数据层存储在与它们ID相同名称的目录中。不过对于使用docker 1.10和之后的版本拉取镜像的情况并非如此。例如,下面的命令显示从Docker Hub拉取一个镜像,并列出Docker 1.9.1的一个目录文件列表。

    1. $  docker pull ubuntu:15.04
    2.  
    3. 15.04: Pulling from library/ubuntu
    4. 47984b517ca9: Pull complete
    5. df6e891a3ea9: Pull complete
    6. e65155041eed: Pull complete
    7. c8be1ac8145a: Pull complete
    8. Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
    9. Status: Downloaded newer image for ubuntu:15.04
    10.  
    11. $ ls /var/lib/docker/aufs/layers
    12.  
    13. 47984b517ca9ca0312aced5c9698753ffa964c2015f2a5f18e5efa9848cf30e2
    14. c8be1ac8145a6e59a55667f573883749ad66eaeef92b4df17e5ea1260e2d7356
    15. df6e891a3ea9cdce2a388a2cf1b1711629557454fd120abd5be6d32329a0e0ac
    16. e65155041eed7ec58dea78d90286048055ca75d41ea893c7246e794389ecf203

    注意看四个目录是如何与下载的镜像的数据层ID匹配的。现在比较下由docker 1.10完成同样的操作的表现。

    1. $ docker pull ubuntu:15.04
    2. 15.04: Pulling from library/ubuntu
    3. 1ba8ac955b97: Pull complete
    4. f157c4e5ede7: Pull complete
    5. 0b7e98f84c4c: Pull complete
    6. a3ed95caeb02: Pull complete
    7. Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
    8. Status: Downloaded newer image for ubuntu:15.04
    9.  
    10. $ ls /var/lib/docker/aufs/layers/
    11. 1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24
    12. 5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7
    13. bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73
    14. ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a

    我们看到四个目录与镜像数据层ID并不匹配。尽管docker 1.10之前与之后的版本镜像管理有不同之处,不过所有的docker版本仍然能在镜像之间共享数据层。例如,如果你拉取一个与已经拉取下来的镜像拥有一些共同的数据层的镜像,Docker会检查到这个并只拉取本地没有的数据层。在这之后,两个镜像共享一些相同的数据层。
    你可以自己做试验来说明。对你刚拉取下来的ubuntu:15.04镜像做一个更改,然后基于这个更改构建一个新的镜像。做这个操作的其中一个方法是使用Dockerfile和docker build命令。
    1.在一个空目录创建一个以ubuntu:15.04镜像开始的Dockerfile。

    1. FROM ubuntu:15.04

    2.添加一个内容为”hello world”在/tmp目录的”newfile”文件。Dockerfile类似如下:

    1. FROM ubuntu:15.04
    2.  
    3.  RUN echo "Hello world" > /tmp/newfile

    3.保存Dockerfile并关闭文件。
    4.在Dockerfile相同目录的终端,执行如下命令:

    1. $ docker build -t changed-ubuntu .
    2.  
    3.  Sending build context to Docker daemon 2.048 kB
    4.  Step 1 : FROM ubuntu:15.04
    5.   —> 3f7bcee56709
    6.  Step 2 : RUN echo "Hello world" > /tmp/newfile
    7.   —> Running in d14acd6fad4e
    8.   —> 94e6b7d2c720
    9.  Removing intermediate container d14acd6fad4e
    10.  Successfully built 94e6b7d2c720

    上面显示新镜像的ID为94e6b7d2c720。
    5.执行docker images来检查新的changed-ubuntu镜像是否在Docker主机本地存储区域。

    1. REPOSITORY       TAG      IMAGE ID       CREATED           SIZE
    2.  changed-ubuntu   latest   03b964f68d06   33 seconds ago    131.4 MB
    3.  ubuntu           15.04    013f3d01d247   6 weeks ago       131.3 MB

    6.执行docker history命令来查看哪些数据层用来创建这个新的changed-ubuntu镜像。
    $ docker history changed-ubuntu

    1. IMAGE               CREATED              CREATED BY                                      SIZE        COMMENT
    2.  94e6b7d2c720        2 minutes ago       /bin/sh -c echo "Hello world" > /tmp/newfile    12 B
    3.  3f7bcee56709        6 weeks ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
    4.  <missing>           6 weeks ago         /bin/sh -c sed -i ‘s/^#s*(deb.*universe)$/   1.879 kB
    5.  <missing>           6 weeks ago         /bin/sh -c echo ‘#!/bin/sh’ > /usr/sbin/polic   701 B
    6.  <missing>           6 weeks ago         /bin/sh -c #(nop) ADD file:8e4943cd86e9b2ca13   131.3 MB

    docker history命令输出显示新的94e6b7d2c720镜像数据层在顶部。你知道这个数据层是由于Dockerfile中的echo “Hello world” > /tmp/newfile命令添加的。下面的4个镜像数据层与组成ubuntu:15.04的数据层是一样的。
    注意到新的changed-ubuntu镜像没有它自己每个数据层的拷贝。从如下图表看到,新的镜像与ubuntu:15.04镜像4个底层数据层共享。
    虚拟化技术
    docker history命令也显示了每个镜像数据层的大小。如你所见,94e6b7d2c720数据层只消耗了12字节的空间。意味着我们刚才创建的changed-ubuntu镜像只占用了docker主机12字节的空间 – 94e6b7d2c720数据层以下的所有数据层都以存在docker主机上并与其它镜像共享。
    镜像数据层的共享使得docker镜像和容器如此的节省空间。

    复制使容器高效

    你早先学到了一个容器与镜像的区别是容器多了一个可写数据层。下面的图表显示了基于ubuntu:15.04的容器的数据层:
    虚拟化技术
    所有对容器的更改都会存储到这个薄的可写容器数据层。其它的数据层是不能修改的只读的镜像数据层。意味着多个容器能安全地共享一个底层镜像。下面的图表显示多个容器镜像一个ubuntu:15.04镜像。每一个容器有它自己的可写数据层。
    虚拟化技术
    当容器内的一个存在的文件被修改时,docker使用存储驱动来完成写时拷贝操作。操作的细节取决于存储驱动程序。对于AUFS和OverlayFS存储驱动,写时拷贝的操作类似如下:

  • 在镜像数据层中搜索要更新的文件。从顶部,每次一个数据层开始搜索。
  • 在找到的第一个文件副本执行复制(copy-up)操作。”copy up“复制文件到容器自己的可写数据层。
  • 在容器的可写数据层修改刚才复制上来的文件。
  • Btrfs, ZFS和其它驱动处理写时拷贝有所不同。你可以之后阅读这些驱动的详细说明。
    一个copy-up操作可能导致明显的性能开销。开销的不同取决于使用的存储驱动。不过,大文件,大量数据层和尝试目录树会影响更显着。幸运的是,操作只发生在第一次修改任何特定文件时。随后对同一个文件的修改不会引起一个copy-up操作,而是对存在于容器数据层的这个文件直接修改。
    让我们看看如果我们根据我们之前创建的更改的ubuntu映像启动5个容器会发生什么:
    1.从Docker主机上的终端,运行以下docker run命令5次。

    1. $ docker run -dit changed-ubuntu bash
    2.  
    3.  75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4
    4.  
    5.  $ docker run -dit changed-ubuntu bash
    6.  
    7.  9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47
    8.  
    9.  $ docker run -dit changed-ubuntu bash
    10.  
    11.  a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a
    12.  
    13.  $ docker run -dit changed-ubuntu bash
    14.  
    15.  8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373
    16.  
    17.  $ docker run -dit changed-ubuntu bash
    18.  
    19.  0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef

    这将根据更改的ubuntu映像启动5个容器。 随着每个容器的创建,Docker添加一个可写层,并为其分配一个随机UUID。 这是从docker run命令返回的值。
    2.运行docker ps命令以验证5个容器是否正在运行。

    1. $ docker ps
    2.  CONTAINER ID    IMAGE             COMMAND    CREATED              STATUS              PORTS    NAMES
    3.  0ad25d06bdf6    changed-ubuntu    "bash"     About a minute ago   Up About a minute            stoic_ptolemy
    4.  8eb24b3b2d24    changed-ubuntu    "bash"     About a minute ago   Up About a minute            pensive_bartik
    5.  a651680bd6c2    changed-ubuntu    "bash"     2 minutes ago        Up 2 minutes                 hopeful_turing
    6.  9280e777d109    changed-ubuntu    "bash"     2 minutes ago        Up 2 minutes                 backstabbing_mahavira
    7.  75bab0d54f3c    changed-ubuntu    "bash"     2 minutes ago        Up 2 minutes                 boring_pasteur

    上面的输出显示了5个正在运行的容器,它们都共享更改的ubuntu映像。 每个CONTAINER ID在创建每个容器时从UUID派生。
    3.列出本地存储区的内容。

    1. $ sudo ls /var/lib/docker/containers
    2.  
    3.  0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef
    4.  9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47
    5.  75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4
    6.  a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a
    7.  8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373

    Docker的写时拷贝策略不仅减少了容器所消耗的空间量,而且还减少了启动容器所需的时间。 在开始时,Docker只需为每个容器创建可写层。 下图显示了这5个容器共享更改的ubuntu映像的一个只读(RO)副本。
    虚拟化技术
    如果Docker在每次启动一个新容器时都必须创建底层映像堆栈的整个副本,那么容器启动时间和磁盘空间将大大增加。

    数据卷和存储驱动

    当容器被删除时,写入到容器中的未存储在数据卷中的任何数据与容器一起被删除。
    数据卷是Docker主机文件系统中直接挂载到容器中的目录或文件。 数据卷不受存储驱动程序控制。 对数据卷的读取和写入绕过存储驱动程序,并以本机主机速度运行。 你可以将任意数量的数据卷装载到容器中。 多个容器还可以共享一个或多个数据卷。
    下图显示了运行两个容器的单个Docker主机。 每个容器存在于Docker主机本地存储区(/var/lib/docker/ …)内的自己的地址空间内。 Docker主机上的/data还有一个共享数据卷。 它直接安装在两个容器中。
    虚拟化技术
    数据卷驻留在Docker主机上的本地存储区域之外,进一步增强了它们与存储驱动程序控制的独立性。 当容器被删除时,存储在数据卷中的任何数据都会保留在Docker主机上。

    Docker用户指南(2) – 创建基础镜像

    使用tar创建一个完整的镜像

    一般来说你需要准备一台将在上面打包基础镜像的工作机器,以及不是必要的工具,如Debian的Debootstrap,这个工具也能构建Ubuntu镜像。
    创建一个Ubuntu基础镜像很简单:

    1. $ sudo debootstrap raring raring > /dev/null
    2. $ sudo tar -C raring -c . | docker import – raring
    3.  
    4. a29c15f1bf7a
    5.  
    6. $ docker run raring cat /etc/lsb-release
    7.  
    8. DISTRIB_ID=Ubuntu
    9. DISTRIB_RELEASE=13.04
    10. DISTRIB_CODENAME=raring
    11. DISTRIB_DESCRIPTION="Ubuntu 13.04"

    在Docker GitHub Repo有更多的示例关于创建基础镜像的:

  • BusyBox
  • CentOS / Scientific Linux CERN (SLC) on Debian/Ubuntuon CentOS/RHEL/SLC/etc
  • Debian / Ubuntu
  • 使用scratch创建一个简单的基础镜像

    你可以使用Docker自带的,微小的镜像scratch作为构建容器的起点。scratch镜像与你下一个Dockerfile的指令将作为镜像的第一个数据层。
    scratch只能在Dockerfile中引用它,不能推送到docker hub或者运行它:

    1. FROM scratch
    2. ADD hello /
    3. CMD ["/hello"]

    hello文件可以到这里下载https://github.com/docker-library/hello-world/tree/master/hello-world

    Docker用户指南(1) – 编写Dockerfile的最佳实践

    Docker通过读取Dockerfile里的指令来自动构建一个镜像。Dockerfile是一个包含了所有用于构建镜像的命令的文本文件。
    Dockerfile遵循特定的格式来使用一组特定的指令。你可以在Dockerfile Reference了解其基础知识。
    本文涵盖了Docker,Inc推荐的最佳实践和方法。以及Docker社区创建易于使用的,有效的Dockerfile文件。

    一般准则和建议

    容器应该是精简的

    用来生成容器的Dockerfile文件应该尽可能的精简。意味着它可以停止和销毁并生成一个新的最小配置的容器替换旧的。

    使用.dockerignore文件

    在大多数情况下,最好把Dockerfile放到一个空的目录下。然后只添加构建Dockerfile所需的文件。为了提升构建性能,你应该通过添加一个.dockerignore文件到那个目录来排除文件和目录。这个文件的排除语法与.gitignore文件类似。

    避免安装不必要的包

    为了减少复杂性,依赖性,文件大小和构建时间,你应该避免安装额外的或不必要的包。例如,你不需要在一个数据库镜像添加一个文本编辑器。

    一个容器一个进程

    在决大多数情况中,你应该在一个容器只运行一个进程。将应用程序解耦到多个容器中使得容器更易水平扩展和重用。如果一个服务依赖另一个服务,使用容器链接功能。

    尽量减少层的数量

    你需要在Dockerfile的可读性(以及因此的长期可维护性)和最小化它使用的层数之间找到平衡。 要慎重引用新的数据层。

    排序多行参数

    尽可能的通过以字母数字排序多行参数以方便以后的更改。这会帮助你避免重复的软件包以及之后更容易地更新这个列表。通过添加反斜杠,可以使代码更易读。如下示例:

    1. RUN apt-get update && apt-get install -y
    2.   bzr
    3.   cvs
    4.   git
    5.   mercurial
    6.   subversion

    构建缓存

    在构建一个镜像期间,Docker将按顺序执行Dockerfile中的每一个指令。当执行每个指令前,Docker会在缓存中查找可以重复的镜像,而不是创建一个新的,重复的镜像。如果不想使用缓存可以在docker build命令中加入–no-cache=true参数。
    不过当你要用Docker镜像缓存时,很有必要了解Docker什么时候会和什么时候不会使用缓存。Docker将遵循的基本规则如下:

  • 现在你要重新构建已存在缓存中的镜像,docker将指令与该缓存镜像导出的数据层作对比看它们中的任意一个数据层构建使用的指令是否一样。如果不一样,则认为缓存是无效的。
  • 在大多数情况下仅仅对比Dockerfile中的与数据层的指令就足够了。不过某些指令需要更多的检查和解释。
  • 对于ADD和COPY指令,检查镜像中文件的内容,并计算每个文件的checksum。文件最后修改时间和最后访问时间不会影响到checksum结果。在查找缓存时,将对比当前文件与缓存镜像中文件的checksum。如果文件有更改,如内容和元数据,那么缓存将失效。
  • 除了ADD和COPY命令,docker不会通过对比文件的checksum来决定缓存是否匹配。如当执行apt-get -y update命令时,docker不会对比更新的文件的checksum,只会对比命令本身。
  • 一旦一个指令的缓存无效,接下来的Dockerfile命令将生成新的数据层,不会再使用缓存。

    Dockerfile指令

    FROM

    只要有可能,使用当前的官方镜像作为你的基础镜像。我们推荐使用Debian镜像,因为非常严格控制并保持最小大小(目前150mb以下),然后仍然是一个完整的发行版本。

    LABEL

    你可以向镜像添加标签,以帮助按项目组织镜像,记录许可信息,帮助自动化或出于其他原因。 对于每个标签,添加一行以LABEL开头,并使用一个或多个键值对。 以下示例显示了不同的可接受格式。

    1. # Set one or more individual labels
    2. LABEL com.example.version="0.0.1-beta"
    3. LABEL vendor="ACME Incorporated"
    4. LABEL com.example.release-date="2015-02-12"
    5. LABEL com.example.version.is-production=""
    6.  
    7. # Set multiple labels on one line
    8. LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
    9.  
    10. # Set multiple labels at once, using line-continuation characters to break long lines
    11. LABEL vendor=ACME Incorporated
    12.       com.example.is-beta=
    13.       com.example.is-production=""
    14.       com.example.version="0.0.1-beta"
    15.       com.example.release-date="2015-02-12"

    RUN

    一如既往,为了使你的Dockerfile更易读,可理解和可维护,使用反斜杠分隔复杂的RUN语句。
    使用RUN最常用的场景可能是使用apt-get安装软件。RUN apt-get是安装软件的命令,有几个问题需要注意。
    你应该避免使用RUN apt-get upgrade或dist-upgrade,因为许多来自基础镜像的“必需”包不会在非特权容器内升级。如果一个基础镜像的软件包过期了,你应该联系它的维护者。如果你知道的foo软件包需要升级,使用apt-get install -y foo来自动更新它。
    始终将RUN apt-get update与apt-get install组合在同一RUN语句中,例如:

    1. RUN apt-get update && apt-get install -y
    2.         package-bar
    3.         package-baz
    4.         package-foo

    在一个RUN语句中单独使用apt-get update可能会引起缓存问题和随后的apt-get install指令失败。例如,你有这样一个Dockerfile:

    1. FROM ubuntu:14.04
    2.     RUN apt-get update
    3.     RUN apt-get install -y curl

    构建镜像后,所有的数据层在docker缓存中。假设你要安装额外的包:

    1. FROM ubuntu:14.04
    2.     RUN apt-get update
    3.     RUN apt-get install -y curl nginx

    docker看到第一个指令RUN apt-get update没有更改就开始使用上一次的缓存。因为docker使用了缓存,所以导致apt-get update没有执行。因为apt-get update没有被执行,那么有可能curl和nginx的包是过期的版本。
    使用RUN apt-get update && apt-get install -y确保你的Dockerfile安装最新的软件包版本,无需进一步的编码或手动干预。
    这种技术被称为“cache busting”。 你还可以通过指定软件包版本来实现缓存无效化。 例如:

    1. RUN apt-get update && apt-get install -y
    2.         package-bar
    3.         package-baz
    4.         package-foo=1.3.*

    指定版本强制构建镜像时安装特定版本的软件而不管缓存里的是什么版本。
    以下是apt-get安装软件时推荐的格式:

    1. RUN apt-get update && apt-get install -y
    2.     aufs-tools
    3.     automake
    4.     build-essential
    5.     curl
    6.     dpkg-sig
    7.     libcap-dev
    8.     libsqlite3-dev
    9.     mercurial
    10.     reprepro
    11.     ruby1.9.1
    12.     ruby1.9.1-dev
    13.     s3cmd=1.1.*
    14.  && rm -rf /var/lib/apt/lists/*

    s3cmd指令指定一个1.1.0*版本。如果之前的镜像使用的是一个旧版本,指定一个新版本会使缓存失效而开始执行apt-get update命令,从而确保安装了新的版本。
    另外,清除apt缓存和删除/var/lib/apt/lists能有效减小镜像大小。

    注意:官方的Debian和Ubuntu镜像会自动执行apt-get clean,所以不需要我们显示调用。

    CMD

    CMD指令用来运行镜像里的软件,命令后面可以添加参数。CMD指令的格式为CMD [“executable”, “param1”, “param2”…]。因此如果镜像是用于运行服务,如Apache和Rails,指令应该为CMD [“apache2″,”-DFOREGROUND”]。实际上这种格式适用于所有运行服务的镜像。
    在大多数其它情况下,CMD应该使用一个交互式的shell,如bash,python的perl。例如,CMD [“perl”, “-de0”], CMD [“python”], 或CMD [“php”, “-a”]. 使用这种形式意味着当你执行如docker run -it python,你会进入到一个可用的shell。CMD应该很少以CMD [“param”,“param”]的形式与ENTRYPOINT连接使用,除非你对ENTRYPOINT很熟悉。

    EXPOSE

    EXPOSE指令表示容器中的哪个端口用来监听连接。因此你应该使用常见的惯例的端口。例如,Apache web server应该EXPOSE 80端口,而MongoDB容器应该EXPOSE 27017等。
    对于容器需要外部访问的时候,用户可以执行docker run跟随一个参数来映射指定的端口,此时EXPOSE对这种情况无作用。对于container linking,Docker为链接容器提供了访问被链接容器的路径环境变量,如PHP容器连接到MySQL容器的环境变量MYSQL_PORT_3306_TCP。

    ENV

    为了使新软件更容易运行,你可以使用ENV来更新PATH环境变量。例如ENV PATH /usr/local/nginx/bin:$PATH会确保CMD [“nginx”]正常运行。
    ENV指令也可以为你想容器化的软件指定所需的环境变量,如Postgres的PGDATA环境变量。
    最后,ENV也可以用来指定一个版本号,为之后的安装配置软件使用,以便更好的进行维护。

    ADD或COPY

    虽然ADD和COPY功能类似,一般来讲,首选COPY。因为它比ADD更透明。COPY只是比本地文件复制到容器中,而ADD有一些其它的功能(如会解压tar文件和下载远程文件)不会很明显。ADD最佳用途是将本地的tar文件自动解压到镜像中,如ADD rootfs.tar.xz /。
    如果在Dockerfile中有多处需要不同的文件,每个文件单独使用一个COPY,而不是使用一个COPY指令一次复制完。这保证了当其中的某个文件更新时,只是这个文件的缓存失效,其它的还是能够正常使用缓存。
    例如:

    1. COPY requirements.txt /tmp/
    2. RUN pip install –requirement /tmp/requirements.txt
    3. COPY . /tmp/

    这个示例当除requirements.txt其它文件更新时,前两步还是能够使用缓存的,如果只用一条COPY指令,那么/tmp/目录里的文件一旦更新,缓存将全部失效。
    由于关系到镜像的大小,不推荐使用ADD来获取远程文件;你应该使用curl或wget替代。用这种方式你可以当文件解压后删除原来的压缩文件,且没有新加一层数据层。例如,应该避免如下用法:

    1. ADD http://example.com/big.tar.xz /usr/src/things/
    2. RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
    3. RUN make -C /usr/src/things all

    使用如下方法:

    1. RUN mkdir -p /usr/src/things
    2.     && curl -SL http://example.com/big.tar.xz
    3.     | tar -xJC /usr/src/things
    4.     && make -C /usr/src/things all

    对于其它不需要自动解压文件的情况,你应该始终使用COPY。

    ENTRYPOINT

    ENTRYPOINT最佳用途是设置镜像的主命令,允许镜像作为命令一样运行(然后使用CMD设置默认参数)
    如下示例:

    1. ENTRYPOINT ["s3cmd"]
    2. CMD ["–help"]

    运行如下命令会显示命令帮助:

    1. $ docker run s3cmd

    或者设置参数:

    1. $ docker run s3cmd ls s3://mybucket

    ENTRYPOINT指令还可以与helper脚本结合使用,允许它以类似于上述命令的方式工作,即使启动软件可能需要多个步骤。
    例如,Postgres Official Image使用以下脚本作为其ENTRYPOINT:

    1. #!/bin/bash
    2. set -e
    3.  
    4. if [ "$1" = ‘postgres’ ]; then
    5.     chown -R postgres "$PGDATA"
    6.  
    7.     if [ -z "$(ls -A "$PGDATA")" ]; then
    8.         gosu postgres initdb
    9.     fi
    10.  
    11.     exec gosu postgres "$@"
    12. fi
    13.  
    14. exec "$@"

    注意:脚本里使用bash命令exec,使得程序以容器的PID 1运行。这样程序就能接收到发送到容器的Unix信号。

    帮助程序脚本复制到容器中,并通过容器启动时的ENTRYPOINT运行:

    1. COPY ./docker-entrypoint.sh /
    2. ENTRYPOINT ["/docker-entrypoint.sh"]
    3. CMD ["postgres"]

    该脚本允许用户以几种方式运行Postgres.
    最简单启动Postgres的方法:

    1. $ docker run postgres

    或者运行Postgres并传递参数过去:

    1. $ docker run postgres postgres –help

    最后,也可以启动一个完全不同的工具,如bash:

    1. $ docker run –rm -it postgres bash

    VOLUME

    VOLUME指令应用于公开由docker容器创建的任何数据库存储区域,配置存储或文件/文件夹。
    强烈建议你对镜像的任何更改和/或用户可维护的部分使用VOLUME。

    USER

    如果服务可以在没有权限的情况下运行,请使用USER更改为非root用户。 首先在Dockerfile中创建一个类似于RUN groupadd -r postgres && useradd -r -g postgres postgres的用户和组。

    注意:镜像中的用户和组获得非确定性的UID / GID,因为无论镜像如何重建,都会分配“下一个”UID / GID。 所以,如果它是关键的,你应该分配一个显式的UID / GID。

    你应该避免安装或使用sudo,因为它具有不可预测的TTY和信号转发行为,可能导致更多的问题。 如果你绝对需要类似于sudo的功能(例如,以root身份初始化守护程序,但以非root身份运行它),则可以使用“gosu”。
    最后,为了减少数据层和复杂性,避免频繁地来回切换USER。

    WORKDIR

    为了清晰和可靠,你应该始终为WORKDIR使用绝对路径。 此外,你应该使用WORKDIR,而不是如RUN cd …&do-something,这很难维护。