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,这很难维护。

    管理swarm(20) – 管理和维护swarm

    当你运行Docker Engines swarm,管理节点是管理swarm和存储swarm状态的关键组件。为了正确地部署和维护swarm,理解管理节点一些关键的功能很重要。

    swarm的管理节点

    swarm管理节点使用Raft一致性算法来管理swarm的状态。你只需要理解Raft的一些常见概念以便来更好的管理swarm。
    对管理节点的数量没有限制。关于要实现多少个管理器节点的决定是性能和容错之间的折衷。增加更多管理节点到swarm使得swarm有更好的容灾能力。不过额外的管理节点会降低写入性能,因为更多的节点必须确认更新swarm状态的提议。意味着产生更多的流量,花更多的时间。
    Raft要求大多数管理者(也称为法定人数)同意对swarm的建议更新。 法定人数的管理人员还必须同意节点添加和删除。 成员资格操作受到与状态复制相同的约束。

    使用一个advertise静态IP地址

    当启动一个swarm时,你必须指定–advertise-addr一个IP地址来通告swarm中的其它管理节点。因为管理节点是一个相对稳定的组件,你应该使用一个固定的IP地址来避免机器重启时IP变更其它管理节点无法连接。
    如果整个swarm重启,随便每个管理节点都获取到一个新的IP地址,那么任何的节点都没有办法连接到一个已存在的管理节点。worker节点就可以使用动态IP地址。

    添加管理节点来容错

    你应该维护奇数个swarm管理节点以支持管理节点故障而不影响swarm运行。奇数的管理节点数量确保了在网络故障分裂为两部分时,有很大可能管理节点的数量仍然为法定数量(即数量超过一半)来选举出新的leader节点。如果遇到网络分裂超过三部分,那就不能保证有法定数量的管理节点了。
    例如,在一个有5个管理节点的swarm中,如果你失去了3个节点,余下的2个节点没有超过一半,达不到法定数量,无法选举出新的leader节点。因此在你恢复不可用的管理节点或使用灾难恢复命令恢复swarm前,你不能添加或删除节点。
    虽然可以将一个swarm缩放为单个管理器节点,但不可能降级最后一个管理器节点。这可以确保你能访问swarm,并且swarm仍然能处理请求。缩放为单个管理节点是一个不安全的操作且不建议。如果你最后一个节点在降级操作期间突然变得不可用,在你重启节点或使用–force-new-cluster重启前,swarm将变得不可用。

    分布管理节点到多个区域

    除了维护奇数个管理节点,在部署管理节点时注意数据中心拓扑。为了实现最佳容错性,将管理节点分布在至少3个可用性区域中,以支持整个机房或常见维护情况的故障。如果任意一个区域发生故障,swarm管理节点的数量仍然会超过一半来选举出leader节点。
    下面的图表说明在三个可用区域中部署管理节点的数量。
    虚拟化技术

    运行仅负责管理的节点

    默认下管理节点也作为worker节点。意味着调度器可以分配任务给管理节点。对于小的和非关键的swarm,只要你使用cpu和内存的资源约束来调度服务,则向管理节点分配任务的风险相对较低。
    然而因为管理节点使用Raft一致性算法以一致的方式复制数据,它们对资源匮乏较敏感。你应该设置管理节点不接受任务以避免影响swarm心跳或leader选举。
    为了避免影响管理节点的相关管理操作,你可以设置管理节点为drain状态不再继续作为worker节点。

    1. docker node update –availability drain <NODE>

    当你drain一个节点,调度器会重新分配这个节点上运行的任务到其它可用节点。也会阻止调度器分配新的任务到这个节点。

    备份swarm状态

    Docker管理节点存储swarm状态和日志在以下目录:

    1. /var/lib/docker/swarm/raft

    经常的备份raft数据目录以便你能灾难恢复swarm。

    监控swarm状态

    你可以通过/nodes HTTP endpoint以JSON格式查询docker nodes API来监视管理节点的运行状况。
    也可以在命令行执行docker node inspect 来查询节点。
    例如,查询管理节点的reachability:

    1. docker node inspect manager1 –format "{{ .ManagerStatus.Reachability }}"
    2. reachable

    查询worker节点的接受任务的状态:

    1. docker node inspect manager1 –format "{{ .Status.State }}"
    2. ready

    从这些命令的输出来看,我们可以知道manager1作为管理节点时状态为reachable,作为worker节点时为ready。
    unreachable状态意味着从其它管理节点无法访问这个管理节点。这种情况你需要马上恢复这个不可用的管理节点:

  • 重启docker进程来看管理节点是否会恢复为reachable
  • 重启机器
  • 如果以上都没有用,你应该添加一个管理节点或升级一个worker节点为管理节点。你也需要使用docker node demote 和docker node rm 来清除失效的管理节点。
  • 或者你可以在管理节点上执行docker node ls来查看所有节点的运行情况:

    1. docker node ls
    2. ID                           HOSTNAME  MEMBERSHIP  STATUS  AVAILABILITY  MANAGER STATUS
    3. 1mhtdwhvsgr3c26xxbnzdc3yp    node05    Accepted    Ready   Active
    4. 516pacagkqp2xc3fk9t1dhjor    node02    Accepted    Ready   Active        Reachable
    5. 9ifojw8of78kkusuc4a6c23fx *  node01    Accepted    Ready   Active        Leader
    6. ax11wdpwrrb6db3mfjydscgk7    node04    Accepted    Ready   Active
    7. bb1nrq2cswhtbg4mrsqnlx1ck    node03    Accepted    Ready   Active        Reachable
    8. di9wxgz8dtuh9d2hn089ecqkf    node06    Accepted    Ready   Active

    强制删除一个节点

    在大多数情况下在你要使用docker node rm命令来删除一个节点时应该先关闭这个节点。如果某个节点变得不可达,无响应或受损,你可以通过传递–force标志强制删除该节点,而不关闭它。例如如果node9受损:

    1. $ docker node rm node9
    2.  
    3. Error response from daemon: rpc error: code = 9 desc = node node9 is not down and can’t be removed
    4.  
    5. $ docker node rm –force node9
    6.  
    7. Node node9 removed from swarm

    在你强制删除一个管理节点之前,你必须先降级它为worker节点。如果你降级或删除一个管理节点,必须始终确保管理节点数量为奇数。

    灾难恢复

    Swarm对故障适应能力强,并且可以从任何数量的临时节点故障(机器重新启动或与崩溃时重新启动)恢复。
    在N个管理节点的swarm中,为了使swarm处理请求并保持可用,必须有大于管理节点总数的50%(或(N / 2)+1)的管理节点的法定数量。 这意味着swarm可以容忍多达(N-1)/ 2个永久故障,超过这些永久故障,无法处理涉及swarm管理的请求。 这些类型的故障包括数据损坏或硬件故障。
    即使你遵循此处的指南,也可能会丢失一个仲裁节点。 如果无法通过常规方法(如重新启动故障节点)恢复仲裁,则可以通过在管理节点上运行docker swarm init –force-new-cluster来恢复该swarm。

    1. # From the node to recover
    2. docker swarm init –force-new-cluster –advertise-addr node01:2377

    –force-new-cluster参数设置为单管理节点的swarm。 它丢弃失去法定数量的管理节点之前存在的成员信息,但它保留Swarm所需的数据,例如服务,任务和worker节点的列表。

    强制重新平衡swarm

    一般情况下,你不需要强制平衡swarm的任务。当你添加一个新节点到swarm或者节点不可用一段时间后重新连接,swarm不会自动重新将运行在其它节点的任务分配到这个空闲节点。swarm是这样设计的。如果为了平衡,swarm周期性的迁移任务到不同的节点,使用这些任务的用户会被中断。如果启动一个新任务或者一个运行的节点变为不可用,它们的任务会分配到不太繁忙的节点。目标是保证对用户影响最小来达到重新平衡。
    如果你不在意中断运行的服务来强制平衡swarm任务,你可以临时的增大服务的规模。
    使用docker service inspect –pretty 来查看服务的规模。使用docker service scale时,调度器会把任务优先分配到最低任务数的节点。 你的swarm中可能有多个负载不足的节点。 你可能需要多次增大服务的规模,以实现所有节点上所需的平衡。
    如果平衡满足你的要求,你可以恢复服务到原来的规模数。可以使用docker service ps命令来评估跨节点的服务的当前平衡。

    管理swarm(19) – 将服务附加到覆盖网络

    Docker Engine的swarm模式原生支持覆盖网络(overlay networks),所以你可以启用容器到容器的网络。swarm模式的覆盖网络包括以下功能:

  • 你可以附加多个服务到同一个网络
  • 默认情况下,service discovery为每个swarm服务分配一个虚拟IP地址(vip)和DNS名称,使得在同一个网络中容器之间可以使用服务名称为互相连接。
  • 你可以配置使用DNS轮循而不使用VIP
  • 为了可以使用swarm的覆盖网络,在启用swarm模式之间你需要在swarm节点之间开放以下端口:

  • TCP/UDP端口7946 – 用于容器网络发现
  • UDP端口4789 – 用于容器覆盖网络
  • 在swarm中创建一个覆盖网络

    当你运行Docker Engine的swarm模式时,你可以在管理节点执行docker network create命令来创建一个覆盖网络。例如,创建一个名为my-network的网络:

    1. $ docker network create
    2.   –driver overlay
    3.   –subnet 10.0.9.0/24
    4.   –opt encrypted
    5.   my-network
    6.  
    7. 273d53261bcdfda5f198587974dae3827e947ccd7e74a41bf1f482ad17fa0d33

    默认情况下swarm中的节点通信是加密的。在不同节点的容器之间,可选的–opt encrypted参数能在它们的vxlan流量启用附加的加密层。
    –subnet参数指定覆盖网络的子网。当你不指定一个子网时,swarm管理器自动选择一个子网并分配给网络。在一些旧的内核,包括kernel 3.10,自动分配的地址可能会与其它子网重叠。这样的重叠可能引起连接问题。
    执行docker network ls来查看网络:

    1. $ docker network ls
    2.  
    3. NETWORK ID          NAME        DRIVER   SCOPE
    4. f9145f09b38b        bridge      bridge   local
    5. ..snip..
    6. 273d53261bcd        my-network  overlay  swarm

    swarm scope表示部署到swarm的服务可以使用这个网络。当你创建一个服务并附加到一个网络后,swarm仅仅扩展该网络到服务运行的节点上。在一个没有运行有附加到网络的服务worker节点上,network ls命令不会显示有任何网络。

    附加服务到覆盖网络

    要附加一个服务到一个覆盖网络,在创建服务的时候传递–network参数。例如创建一个nginx服务并附加到一个名为my-network的网络:

    1. $ docker service create
    2.   –replicas 3
    3.   –name my-web
    4.   –network my-network
    5.   nginx

    注意:在附加服务到网络前,必须先创建这个网络。

    在同一个覆盖网络的容器之间能互相连接。在管理节点执行docker service ps 来查看哪些节点运行着这个服务:

    1. $ docker service ps my-web
    2.  
    3. ID                         NAME      IMAGE  NODE   DESIRED STATE  CURRENT STATE               ERROR
    4. 63s86gf6a0ms34mvboniev7bs  my-web.1  nginx  node1  Running        Running 58 seconds ago
    5. 6b3q2qbjveo4zauc6xig7au10  my-web.2  nginx  node2  Running        Running 58 seconds ago
    6. 66u2hcrz0miqpc8h0y0f3v7aw  my-web.3  nginx  node3  Running        Running about a minute ago

    pic1
    你可以在运行着附加有网络的服务的节点上查看这个网络的详情:

    1. $ docker network inspect <NETWORK>

    这个网络信息包括了该节点上附加到该网络的容器的列表。例如:

    1. $ docker network inspect my-network
    2. [
    3.     {
    4.         "Name": "my-network",
    5.         "Id": "273d53261bcdfda5f198587974dae3827e947ccd7e74a41bf1f482ad17fa0d33",
    6.         "Scope": "swarm",
    7.         "Driver": "overlay",
    8.         "EnableIPv6": false,
    9.         "IPAM": {
    10.             "Driver": "default",
    11.             "Options": null,
    12.             "Config": [
    13.                 {
    14.                     "Subnet": "10.0.9.0/24",
    15.                     "Gateway": "10.0.9.1"
    16.                 }
    17.             ]
    18.         },
    19.         "Internal": false,
    20.         "Containers": {
    21.             "404d1dec939a021678132a35259c3604b9657649437e59060621a17edae7a819": {
    22.                 "Name": "my-web.1.63s86gf6a0ms34mvboniev7bs",
    23.                 "EndpointID": "3c9588d04db9bc2bf8749cb079689a3072c44c68e544944cbea8e4bc20eb7de7",
    24.                 "MacAddress": "02:42:0a:00:09:03",
    25.                 "IPv4Address": "10.0.9.3/24",
    26.                 "IPv6Address": ""
    27.             }
    28.         },
    29.         "Options": {
    30.             "com.docker.network.driver.overlay.vxlanid_list": "257"
    31.         },
    32.         "Labels": {}
    33.     }
    34. ]

    在上面的示例中,my-web服务的容器my-web.1.63s86gf6a0ms34mvboniev7bs附加到该节点的my-network网络。

    使用swarm模式的service discovery

    默认情况下,当你创建一个服务并附加到一个网络时,swarm就给服务分配一个VIP。VIP根据服务名称映射到DNS别名。在该网络的容器之间通过gossip来共享DNS映射信息,所以在该网络的容器能通过服务名称来访问彼此。
    你不需要公开特定于服务的端口,以使服务可用于同一覆盖网络上的其他服务。 swarm的内部负载均衡会自动将请求分发到服务VIP。
    你可以使用如下命令来查看服务的VIP:

    1. $ docker service inspect
    2.   –format='{{json .Endpoint.VirtualIPs}}’
    3.   my-web
    4.  
    5. [{"NetworkID":"7m2rjx0a97n88wzr4nu8772r3" "Addr":"10.0.0.2/24"}]

    下面的示例展示如何添加一个busybox服务到与nginx服务相同的网络,以及busybox服务使用DNS名称my-web访问nginx服务:
    1.在管理节点,部署busybox服务到与my-web同一个网络:

    1. $ docker service create
    2.   –name my-busybox
    3.   –network my-network
    4.   busybox
    5.   sleep 3000

    2.查看哪个节点运行着my-busybox服务:

    1. $ docker service ps my-busybox
    2.  
    3. ID                         NAME          IMAGE    NODE   DESIRED STATE  CURRENT STATE          ERROR
    4. 1dok2cmx2mln5hbqve8ilnair  my-busybox.1  busybox  node1  Running        Running 5 seconds ago

    3.登录上一步查询到的node1节点,在busybox容器中打开一个可交互的shell:

    1. $ docker exec -it my-busybox.1.1dok2cmx2mln5hbqve8ilnair /bin/sh

    你可以将容器名称推断为 + 。 或者,你可以在运行任务的节点上运行docker ps。
    4.在busybox容器内部,查询my-web服务的VIP:

    1. $ nslookup my-web
    2.  
    3. Server:    127.0.0.11
    4. Address 1: 127.0.0.11
    5.  
    6. Name:      my-web
    7. Address 1: 10.0.9.2 ip-10-0-9-2.us-west-2.compute.internal

    5.在busybox容器内部,查询的DNS记录来找出my-web服务的所有容器IP地址:

    1. $ nslookup tasks.my-web
    2.  
    3. Server:    127.0.0.11
    4. Address 1: 127.0.0.11
    5.  
    6. Name:      tasks.my-web
    7. Address 1: 10.0.9.4 my-web.2.6b3q2qbjveo4zauc6xig7au10.my-network
    8. Address 2: 10.0.9.3 my-web.1.63s86gf6a0ms34mvboniev7bs.my-network
    9. Address 3: 10.0.9.5 my-web.3.66u2hcrz0miqpc8h0y0f3v7aw.my-network

    6.在busybox容器内,执行wget来访问my-web服务的nginx web server:

    1. $ wget -O- my-web
    2.  
    3. Connecting to my-web (10.0.9.2:80)
    4. <!DOCTYPE html>
    5. <html>
    6. <head>
    7. <title>Welcome to nginx!</title>
    8. …snip…

    通过访问服务的VIP,swarm负载均衡自动将HTTP请求路由可用的容器中。 它使用轮循的方式来平均地分发请求。

    使用DNS轮循请求

    你可以配置服务直接使用DNS轮循而不用VIP,在创建服务的时候设置–endpoint-mode dnsrr。在你要使用你自己的负载均衡器时可能会用此方法。
    下面的示例展示一个服务使用dnsrr endpoint模式:

    1. $ docker service create
    2.   –replicas 3
    3.   –name my-dnsrr-service
    4.   –network my-network
    5.   –endpoint-mode dnsrr
    6.   nginx

    当你查询服务名称的DNS记录时,DNS服务会返回所有该服务容器的IP地址:

    1. $ nslookup my-dnsrr-service
    2. Server:    127.0.0.11
    3. Address 1: 127.0.0.11
    4.  
    5. Name:      my-dnsrr
    6. Address 1: 10.0.9.8 my-dnsrr-service.1.bd3a67p61by5dfdkyk7kog7pr.my-network
    7. Address 2: 10.0.9.10 my-dnsrr-service.3.0sb1jxr99bywbvzac8xyw73b1.my-network
    8. Address 3: 10.0.9.9 my-dnsrr-service.2.am6fx47p3bropyy2dy4f8hofb.my-network

    管理swarm(18) – 部署服务

    创建服务

    简单地在swarm中创建一个服务,你仅需要提供一个镜像名称:

    1. $ docker service create <IMAGE>

    这时协调器开始调度任务给一个可用节点。任务基于你给的镜像来运行一个容器。例如,你可以执行如下命令来创建一个nginx web服务器的实例服务:

    1. $ docker service create –name my_web nginx
    2.  
    3. anixjtol6wdfn6yylbkrbj2nx

    在这个示例中–name参数命名服务为my_web。
    在管理节点上执行docker service ls来列出服务:

    1. $ docker service ls
    2.  
    3. ID            NAME    REPLICAS  IMAGE  COMMAND
    4. anixjtol6wdf  my_web  1/1       nginx

    要让web服务器在swarm外部可用,你需要发布一个swarm监听web请求的端口。
    你可以在指定镜像后添加一个命令来运行在容器内部:

    1. $ docker service create <IMAGE> <COMMAND>

    例如启动一个alpine镜像来执行ping docker.com:

    1. $ docker service create –name helloworld alpine ping docker.com
    2.  
    3. 9uk4639qpg7npwf3fn2aasksr

    配置运行环境

    你可以使用如下选项来配置容器的运行环境:

  • 使用–env参数配置环境变量
  • 使用–workdir参数配置容器工作目录
  • 使用–user参数配置程序运行使用的用户或ID
  • 例如:

    1. $ docker service create –name helloworld
    2.   –env MYVAR=myvalue
    3.   –workdir /tmp
    4.   –user my_user
    5.   alpine ping docker.com
    6.  
    7. 9uk4639qpg7npwf3fn2aasksr

    控制服务规模和布局

    swarm模式有两种类型的服务,副本(replicated)和全局(global)。对于副本服务,你可以指定任务副本的数量。对于全局服务,调度器将在每个可用节点都执行一个相同的任务。
    你可以使用–mode参数来设置服务类型。如果不指定,默认服务类型为副本。对于副本服务,你可以使用–replicas参数为设置副本数量。例如,启动一个副本数为3的nginx服务:

    1. $ docker service create –name my_web –replicas 3 nginx

    要启动一个全局服务,在创建服务执行docker service create时传递–mode global参数。当任何时候一个新的节点可用,调度器会调度全局服务的任务到该新节点。例如在每个节点上执行alpine服务:

    1. $ docker service create –name myservice –mode global alpine top

    控制服务规模和布局

    swarm模式中有两种可以访问服务的方法:

  • 使用ingress网络来发布端口到swarm外部
  • 在swarm的覆盖网络内部连接服务和任务
  • 发布端口到swarm外部网络

    你可以使用–publish:参数来发布服务端口到swarm外部网络。发布服务端口时,无论该节点上是否有任务运行,swarm都会使服务在每个节点的目标端口上可访问。
    例如,假设你想部署一个3个副本的nginx服务到一个10个节点的swarm:

    1. docker service create –name my_web –replicas 3 –publish 8080:80 nginx

    调度器会把nginx任务分配到三个可用节点。不过swarm会设置10个节点都监听8080端口,访问此端口时会把请求转发到nginx容器的80端口。你可以使用curl在任意的节点上测试服务:

    1. $ curl localhost:8080
    2.  
    3. <!DOCTYPE html>
    4. <html>
    5. <head>
    6. <title>Welcome to nginx!</title>
    7. <style>
    8.     body {
    9.         width: 35em;
    10.         margin: 0 auto;
    11.         font-family: Tahoma, Verdana, Arial, sans-serif;
    12.     }
    13. </style>
    14. </head>
    15. <body>
    16. <h1>Welcome to nginx!</h1>
    17. <p>If you see this page, the nginx web server is successfully installed and
    18. working. Further configuration is required.</p>
    19.  
    20. <p>For online documentation and support please refer to
    21. <a href="http://nginx.org/">nginx.org</a>.<br/>
    22. Commercial support is available at
    23. <a href="http://nginx.com/">nginx.com</a>.</p>
    24.  
    25. <p><em>Thank you for using nginx.</em></p>
    26. </body>
    27. </html>

    添加一个覆盖网络

    使用覆盖网络可以在swarm内连接一个或多个服务。
    首先在管理节点执行docker network create来创建一个覆盖网络:

    1. $ docker network create –driver overlay my-network
    2.  
    3. etjpu59cykrptrgw0z0hk5snf

    当你在swarm模式创建一个覆盖网络后,所有的管理节点都可以访问这个网络。
    可以在创建服务时传递–network参数过去指定服务加入的覆盖网络:

    1. $ docker service create
    2.   –replicas 3
    3.   –network my-network
    4.   –name my-web
    5.   nginx
    6.  
    7. 716thylsndqma81j6kkkb5aus

    配置更新策略

    你可以在创建服务或更新服务时指定swarm应用服务更新的策略。
    –update-delay标志配置更新到服务任务或任务集之间的时间延迟。 你可以将时间T描述为秒数Ts,分钟Tm或小时Th的组合。 所以10m30s表示10分30秒的延迟。
    默认情况下,调度器一次更新1个任务。 你可以传递–update-parallelism标志来配置调度器同时更新的最大服务任务数。
    当对单个任务的更新返回RUNNING状态时,调度器通过继续执行另一个任务,直到所有任务都更新为止,继续更新。 如果在更新期间的任何时间任务返回FAILED,则调度器暂停更新。 你可以使用–update-failure-action标志控制docker服务创建或docker服务更新的行为。
    在下面的示例服务中,调度器一次最多应用更新2个副本。 当更新的任务返回RUNNING或FAILED时,调度程序在停止下一个要更新的任务之前等待10秒钟:

    1. $ docker service create
    2.   –replicas 10
    3.   –name my_web
    4.   –update-delay 10s
    5.   –update-parallelism 2
    6.   –update-failure-action continue
    7.   alpine
    8.  
    9. 0u6a4s31ybk7yw2wyvtikmu50

    配置挂载

    你可以创建两种类型的swarm服务挂载,volume挂载或bind挂载。在创建服务时传递–mount参数指定挂载类型。默认挂载类型是volume。

  • Volumes存储在容器任务被删除时仍然会保留。
    挂载volumes首先方法是利用已存在的volumes:

    1. $ docker service create
    2.   –mount src=<VOLUME-NAME>,dst=<CONTAINER-PATH>
    3.   –name myservice
    4.   <IMAGE>

    下面的方法是在部署服务时创建一个volume:

    1. $ docker service create
    2.   –mount type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=<DRIVER>,volume-opt=<KEY0>=<VALUE0>,volume-opt=<KEY1>=<VALUE1>
    3.   –name myservice
    4.   <IMAGE>
  • Bind挂载是把容器中的目录挂载到运行容器所在的主机上的目录。在swarm初始化任务容器时,主机文件系统路径必须存在。
    下面的示例显示bind挂载的语法:

    1. # Mount a read-write bind
    2. $ docker service create
    3.   –mount type=bind,src=<HOST-PATH>,dst=<CONTAINER-PATH>
    4.   –name myservice
    5.   <IMAGE>
    6.  
    7. # Mount a read-only bind
    8. $ docker service create
    9.   –mount type=bind,src=<HOST-PATH>,dst=<CONTAINER-PATH>,readonly
    10.   –name myservice
    11.   <IMAGE>