第7章 持续交付:与Docker原则完美契合
本章主要内容
- 开发与运维之间的Docker契约
- 掌控跨环境的构建可用性
- 通过低带宽连接在不同环境间迁移构建
- 集中配置一个环境中的所有容器
- 使用Docker实现零停机时间部署
一旦确信使用一致的持续集成(CI)过程对所有的构建都进行了质量检验,下一步自然是开始着手将每个良好的构建部署给用户。这个目标称为持续交付(continuous delivery,CD)。
本章涉及的就是CD流水线——构建从CI流水线出来后所经历的过程。这两者的分界点有时会比较模糊,不过可以这么认为,在构建过程中通过初始测试获得最终镜像的那一刻即是CD流水线开始的时刻。图7-1演示了镜像在到达生产环境(但愿如此)之前是如何通过CD流水线的。
图7-1 一个典型的CD流水线
最后一点值得再提一下,在CD全过程中,从CI产出的镜像必须是最终的、不可修改的!Docker通过不可变镜像及状态封装很容易实现这一点,因此使用Docker已经让你在CD的路上前进了一步。
7.1 在CD流水线上与其他团队互动
先回头看一下Docker是如何改变开发团队与运维团队之间的关系的。
软件开发的一些最大的挑战不是技术性的——按照角色和技能将人员划分成团队是一个普遍做法,但是这会造成沟通障碍和封闭性。一个成功的CD流水线要求在这个过程的所有阶段,从开发环境到测试环境再到生产环境,所有场景里的团队都参与进来,此时对所有团队而言,一个单一的参考点有助于通过提供结构来促进互动。
技巧62 Docker契约——减少摩擦
Docker 的目标之一是让与包含单个应用程序的容器相关联的输入与输出易于表达。在与其他人一起工作时这可以增加透明度——沟通是合作的重要环节之一,而理解Docker如何通过提供一个参考点来简化事务将有助于赢得不信任Docker的人的支持。
问题
想要合作团队的可交付成果是整洁的、明确的,从而减少交付流水线里的摩擦。
解决方案
使用Docker契约来推动团队间整洁的可交付成果。
讨论
随着公司规模扩大,经常可以看到其曾经拥有的扁平化的、精益化的组织架构——几个关键的个人“了解整个系统”,让位给了一个更加结构化的组织架构——不同的团队具有不同的职责和能力。我们在效力过的组织中都对此有过切身体会。
如果没有进行技术投入,随着团队之间相互交付的增多,摩擦也会不断升级。对日益增长的复杂度的抱怨——“把这个版本扔出去!”以及问题一堆的升级将变得稀松平常。“呃,在我们的机器上是正常的!”这样的叫喊声不绝于耳,所有各方都感到失望。图7-2展示了这个场景的一个简化了但具有代表性的情形。
图7-2中的工作流有几个大家熟知的问题。这些问题最终都归结于状态管理的困难。测试团队可能在一台不是运维团队所设置的机器上进行测试。理论上,对所有环境的修改都应仔细地记录下来,并在出现问题时进行回滚以保持一致性。但是,商业压力与人类行为的存在总是破坏这个目标,造成环境性漂移。
图7-2 之前:一个典型的软件工作流
对这一问题的现有解决方案包括虚拟机及RPM。虚拟机通过交付完整的机器表示来减少环境引发风险的可能性。其缺点在于虚拟机是相对单一的实体,对各个团队来说很难有效地进行操作。RPM提供了一种打包应用程序的标准方法,可以在交付软件时定义其依赖。这并未消除配置管理的问题,交付兄弟团队创建的RPM要远比使用互联网上久经考验的RPM问题多得多。
Docker契约
Docker所能做的是在团队之间划出清晰的分界线,Docker镜像既是分界线,又是交换的单位。我们称其为Docker契约,如图7-3所示。
使用Docker,所有团队的参考点变成更加清晰了。与处理处于不可重现状态的庞大的单体虚拟机(或真机)不同,所有团队面对的是相同的代码,而不论是在测试环境、生产环境还是开发环境。此外,代码与数据有了一个清晰的分离,更易于推断问题是由数据的变化造成的还是由代码的变化造成的。
因为Docker使用非常稳定的Linux API作为环境,交付软件的团队具有更大的自由度使用他们喜欢的风格来构建软件和服务,并确信它可在不同环境中按预期运行。这不代表可以忽略它运行所在的环境,但它确实减少了环境差异造成问题的风险。
图7-3 之后:Docker契约
这样单一参考接触点带来的是各种运维效率。问题重现变得更加简单,所有团队都能从一个已知的起点描述并重现问题。升级变成了交付变更的团队的责任。简而言之,状态由那些做变更的人管理。所有这些优点极大地降低了沟通开销,让各个团队能继续他们的工作。沟通开销的降低还有助于向微服务构架进行迁移。
这不只是理论上的好处:我们在一家拥有超过500名开发人员的公司亲身体验了这种提升,它也是各类Docker技术聚会中一个频繁讨论的话题。
7.2 推动Docker镜像的部署
在尝试实现CD时,首要的问题是将构建过程的产出移动到合适的位置。如果对CD流水线中所有场景使用同一个注册中心,看起来问题好像是解决了。但这并未涵盖CD的一个关键方面。
CD背后的关键思想之一是构建提升。构建提升是指流水线的每个场景(用户验收测试、集成测试以及性能测试)只有在前一个场景成功时才能触发下一个场景。使用多个注册中心时,只在某个构建场景通过时才将其提交到下一个注册中心中,就可确保使用的是提升后的构建。
下面讲述在注册中心间迁移镜像的几种方法,甚至有一个无须注册中心即可共享Docker对象的方法。
技巧63 手动同步注册中心镜像
最简单的镜像同步情景是有一台与两个registry高速连接的机器。使用正常的Docker功能即可完成镜像复制。
问题
想要在两个注册中心之间复制一个镜像。
解决方案
拉取镜像,重新打标签,然后推送。
讨论
假设在test-registry.company.com有一个镜像,想要移动到stage-registry.company.com,这个过程很简单:
$ IMAGE=mygroup/myimage:mytag$ OLDREG=test-registry.company.com$ NEWREG=stage-registry.company.com$ docker pull $OLDREG/$MYIMAGE[…]$ docker tag -f $OLDREG/$MYIMAGE $NEWREG/$MYIMAGE$ docker push $NEWREG/$MYIMAGE$ docker rmi $OLDREG/$MYIMAGE$ docker rmi $(docker images -q —filter dangling=true)
这个过程有以下3个重要的点需要注意。
(1)新镜像被强制打了标签。这意味着这台机器上任何具有相同名称的旧镜像(用于层缓存而留在那儿)将丢失镜像名称,因而新的镜像可以使用所需的名称打上标签。
(2)所有没有标签的镜像已经被删除了。虽然层缓存对加速部署非常有用,但是留着无用的镜像层会很快耗尽磁盘空间。一般而言,随着时间过去,旧的层很少会被使用,变得越来越过时。
(3)可能需要使用docker login登录到新的注册中心中。
现在,该镜像就在新的注册中心里可用了,可供CD流水线的后续场景使用。
技巧64 通过受限连接交付镜像
即使是使用了分层,推送和拉取Docker镜像仍然是一个耗费带宽的过程。在具有免费大带宽的世界,这不成问题,但现实有时会迫使我们去处理两个数据中心之间低宽带连接或昂贵的宽带计费的问题。在这种情况下,需要找到一种更加高效的传输差异部分的方法,否则一天运行多次流水线的CD憧憬将遥不可及。
理想的解决方案是一个可以降低镜像的平均尺寸的工具,甚至比典型的压缩方法可达到的更小。
问题
想要在两台使用低带宽连接的机器之间复制镜像。
解决方案
导出镜像,使用bup进行拆分,传输bup块,并在另一端导入重新组合的镜像。
讨论
首先需要介绍一个新的工具bup。它是作为一个备份工具创造出来的,具有极其高效的去重能力——去重能力是指识别重复使用的数据,只存储一份副本。去重在其他情形中也非常有用,例如,传输多个具有非常相似内容的镜像。我们为这个技巧创建了一个名为dbup(“docker bup”的简称)的镜像,可更容易地使用 bup 对镜像进行去重。可在https://github.com/docker-in- practice/ dbup找到其背后的代码。
作为一个示范,来看看从ubuntu:14.04.1镜像升级到ubuntu:14.04.2时可以节省多少宽带。需要牢记的一点是,在现实中这些镜像上面都有多个层,Docker会在下面的层变更时进行完全的重新传输。相比之下,本技巧将识别出高度的相似性,节省大量的带宽。
第一步是把两个镜像都拉取下来以便查看通过网络传输了多少流量:
$ docker pull ubuntu:14.04.1 && docker pull ubuntu:14.04.2[…]$ docker history ubuntu:14.04.1IMAGE CREATED CREATED BY SIZE5ba9dab47459 3 months ago /bin/sh -c #(nop) CMD [/bin/bash] 0 B51a9c7c1f8bb 3 months ago /bin/sh -c sed -i ‘s/^#\s(deb.universe)$/ 1.895 kB5f92234dcf1e 3 months ago /bin/sh -c echo ‘#!/bin/sh’ > /usr/sbin/polic 194.5 kB27d47432a69b 3 months ago /bin/sh -c #(nop) ADD file:62400a49cced0d7521 188.1 MB511136ea3c5a 23 months ago 0 B$ docker history ubuntu:14.04.2IMAGE CREATED CREATED BY SIZE07f8e8c5e660 2 weeks ago /bin/sh -c #(nop) CMD [“/bin/bash”] 0 B37bea4ee0c81 2 weeks ago /bin/sh -c sed -i ‘s/^#\s(deb.universe)$/ 1.895 kBa82efea989f9 2 weeks ago /bin/sh -c echo ‘#!/bin/sh’ > /usr/sbin/polic 194.5 kBe9e06b06e14c 2 weeks ago /bin/sh -c #(nop) ADD file:f4d7b4b3402b5c53f2 188.1 MB$ docker save ubuntu:14.04.1 | gzip | wc -c65970990$ docker save ubuntu:14.04.2 | gzip | wc -c65978132
这里演示的是Ubuntu镜像间不共享层,因此可将整个镜像尺寸作为推送新镜像时将传输的数据量。同样需要注意的是,Docker注册中心使用gzip压缩来传输层,因此在测量时也将其加入(而不是从docker history获得尺寸)。在初始部署及后续部署中都大概传输了65 MB。
在开始前,需要准备两样东西——一个bup作为内部存储器用于存储数据“池”的目录,以及dockerinpractice/dbup镜像。然后就可以将镜像添加到bup数据池中了:
$ mkdir bup_pool$ alias dbup=”docker run —rm \-v $(pwd)/bup_pool:/pool -v /var/run/docker.sock:/var/run/docker.sock \dockerinpractice/dbup”$ dbup save ubuntu:14.04.1Saving image!Done!$ du -sh bup_pool74M bup_pool$ dbup save ubuntu:14.04.2Saving image!Done!$ du -sh bup_pool90M bup_pool
将第二个镜像添加到bup数据池中只增加了大概15 MB的大小。假设在添加ubuntu:14.04.1后已经将这个目录同步到另一台机器上(可能使用的是rsync),再次同步这个目录将只传输15 MB(而不是之前的65 MB)。
在另一端需要这样加载镜像:
$ dbup load ubuntu:14.04.1Loading image!Done!
在两个注册中心之间传输的过程将类似下面这样:
(1)在主机1上docker pull;
(2)在主机1上dbup save;
(3)从主机1 rsync到主机2;
(4)在主机2上dbup load;
(5)在主机2上docker push。
本技巧将之前的很多不可能变成了可能。例如,现在可以重新安排和合并层,而无须考虑通过低带宽连接传输所有的新层需要花费多长时间。
即使是遵从最佳实践将应用程序代码作为最后一个环节进行添加,bup也能帮上忙——它将识别出大部分未修改的代码,而只将差异部分添加到数据池中。
虽然读者对这个方法可能没有迫切的需要,但请在宽带账单开始上升时记起这一点!
技巧65 以TAR文件方式共享Docker对象
TAR文件是Linux上移动文件的一种传统方法。在没有注册中心或者没必要设立注册中心的情况下,可以使用Docker手工创建TAR文件然后进行移动。下面展示这些命令的详细内容。
问题
想要在没有注册中心的情况下与其他人共享镜像和容器。
解决方案
使用docker export或者docker save创建 TAR 文件,然后经由 SSH 使用docker import或docker load来使用它们。
讨论
如果只是随意地使用这些命令,它们之间的区别将难于掌握,因此下面花点儿时间快速看一下它们都做了什么。表7-1概述了这些命令的输入与输出。
表7-1 export和import与save和load的对比
|
命令 |
创 建 了 |
目 标 类 型 |
来源 |
|---|---|---|---|
|
|
TAR文件 |
容器文件系统 |
容器 |
|
|
Docker镜像 |
平面文件系统 |
TAR包 |
|
|
TAR文件 |
Docker镜像(带历史记录) |
镜像 |
|
|
Docker镜像 |
Docker镜像(带历史记录) |
TAR包 |
前两个命令使用平面文件系统。docker export命令输出一个TAR文件,这个TAR文件包含了组成容器状态的文件。在Docker中,正在运行进程的状态不会被保存——只有文件。docker import命令从TAR包创建Docker镜像——没有历史也没有元数据。
这些命令不是对称的——无法仅使用导入和导出从现有容器创建一个容器。这种不对称非常有用,因为可以使用docker export将一个镜像导出成一个TAR文件,然后使用docker import导入从而“丢弃”所有的层历史及元数据。这就是技巧43中描述的镜像扁平化方法。
在导出或保存成TAR文件时,文件会被默认发送到标准输出(stdout)中,因此需要像下面这样确保将其保存到文件中:
docker pull debian:7:3[…]docker save debian:7.3 > debian7_3.tar
然后就可以放心地在网络上传送这个TAR文件了,而其他人可以使用它们完好地导入镜像。可以通过电子邮件或scp来传输:
$ scp debian7_3.tar example.com:/tmp/debian7_3.tar
如果拥有相应权限,还可以更进一步,直接将镜像发送给其他用户的Docker守护进程:
docker save debian:7.3 | \ (1)ssh example.com \ (2)docker import - (3)
(1)docker save命令将7.3版本的Debian提取出来,并通过管道发送给ssh命令
(2)ssh命令在远程机器example.com上运行命令
(3)docker import命令从赋予它的TAR文件创建出一个没有历史的镜像。-表示TAR文件是通过标准输入获取的
如果要保留镜像的历史,可以使用load而不是import,这样其历史将在另一边的Docker守护进程上得到保留:
docker save debian:7.3 | ssh example.com docker load
docker load与docker import不一致性 与docker import不同,docker load不需要在最后使用一个-来表示TAR文件是通过标准输入获取的。
7.3 为不同环境配置镜像
如本章介绍中提到的,CD的一个重点是“在所有地方完成同样的事情”的概念。在没有Docker的情况下,这意味着要进行一次部署工件的构建,并在所有地方使用同一个工件。在一个Docker化的世界,这意味着在所有地方使用同一个镜像。
不过环境并不完全相同,例如,外部服务的URL可能是不同的。对于“一般”的应用程序,可以使用环境变量来解决这个问题(需要说明的是它们不易于应用到大量机器上)。相同的解决方案对Docker也适用(明确地传入变量),不过对Docker而言还有更好的方法可以实现这一点,并且可以带来额外的好处。
技巧66 使用etcd通知容器
Docker镜像被设计成可以在任何地方部署,不过用户经常希望能在部署后添加额外信息以便影响运行中的应用程序的行为。此外,运行Docker的机器可能需要保持不变,因此需要一个外部的信息来源(这使得环境变量变得不太适合)。
问题
在运行容器时,需要一个外部的配置源。
解决方案
创建一个etcd集群来保存配置,并使用etcd代理来访问它。
讨论
etcd 是一个分布式键值存储系统——它保存着信息片段,可作为实现弹性的多节点集群的一部分。
保持简洁的配置 etcd保存的每个值都应该保持小巧——小于512 KB是一个较好的经验规则,超过这个点则应考虑进行基准测试,以确认etcd是否仍然按预期运行。这种限制不是etcd特有的,其他键值存储系统(如Zookeeper和Consul)也应注意这个问题。
因为etcd集群节点需要互相通信,所以第一步是识别外部IP地址。如果节点运行在不同机器上,则需要每一台机器的外部IP:
$ ip addr | grep ‘inet ‘ | grep -v ‘lo$|docker0$’inet 10.194.12.221/20 brd 10.194.15.255 scope global eth0
这里查找了所有的IPv4网卡,并将环回及Docker排除在外。这将得到所需的IP(此行的第一个)。
现在可以开始设置集群了。请谨慎修改每一行的下列参数:暴露的及公布的端口,以及集群节点与容器的名称:
$ IMG=quay.io/coreos/etcd:v2.0.10$ docker pull $IMG[…]$ HTTPIP=http://10.194.12.221 (1)$ CLUSTER=”etcd0=$HTTPIP:2380,etcd1=$HTTPIP:2480,etcd2=$HTTPIP:2580” (2)$ ARGS=$ ARGS=”$ARGS -listen-client-urls http://0.0.0.0:2379“ (3)$ ARGS=”$ARGS -listen-peer-urls http://0.0.0.0:2380“ (4)$ ARGS=”$ARGS -initial-cluster-state new”$ ARGS=”$ARGS -initial-cluster $CLUSTER”$ docker run -d -p 2379:2379 -p 2380:2380 —name etcd0 $IMG \$ARGS -name etcd0 -advertise-client-urls $HTTPIP:2379 \-initial-advertise-peer-urls $HTTPIP:2380912390c041f8e9e71cf4cc1e51fba2a02d3cd4857d9ccd90149e21d9a5d3685b$ docker run -d -p 2479:2379 -p 2480:2380 —name etcd1 $IMG \$ARGS -name etcd1 -advertise-client-urls $HTTPIP:2479 \-initial-advertise-peer-urls $HTTPIP:2480446b7584a4ec747e960fe2555a9aaa2b3e2c7870097b5babe65d65cffa175dec$ docker run -d -p 2579:2379 -p 2580:2380 —name etcd2 $IMG \$ARGS -name etcd2 -advertise-client-urls $HTTPIP:2579 \-initial-advertise-peer-urls $HTTPIP:25803089063b6b2ba0868e0f903a3d5b22e617a240cec22ad080dd1b497ddf4736be$ curl -L $HTTPIP:2579/versionetcd 2.0.10
(1)机器的外部IP地址
(2)在集群定义中使用机器的外部IP地址,让节点可以相互通信。因为所有的节点都在同一台宿主机上,集群的端口(用于连接其他节点)必须不同
(3)用于处理客户端请求的端口
(4)用于与集群其他节点通信的监听端口,与$CLUSTER中指定的端口一致
现在集群就已经启动了,并且每个节点都有响应。在上述命令中,指向对等点(peer)的内容是在控制etcd节点如何查找其他节点并与之通信,而指向客户端的内容则定义了其他应用程序如何连接到etcd。
下面用实例来说明一下etcd的分布式特性:
$ curl -L $HTTPIP:2579/v2/keys/mykey -XPUT -d value=”test key”{“action”:”set”,”node”: {“key”:”/mykey”,”value”:”test key”,➥ “modifiedIndex”:7,”createdIndex”:7}}$ sleep 5$ docker kill etcd2etcd2$ curl -L $HTTPIP:2579/v2/keys/mykeycurl: (7) couldn’t connect to host$ curl -L $HTTPIP:2379/v2/keys/mykey{“action”:”get”,”node”: {“key”:”/mykey”,”value”:”test key”,➥ “modifiedIndex”:7,”createdIndex”:7}}
上述代码添加了一个键到etcd2中,然后杀掉它。不过etcd已经将信息自动复制到其他节点上,因此还是能够提供该信息。尽管上述代码暂停了5秒,etcd通常会在1秒内进行复制(即便是在跨不同机器时)。可随时运行docker start etcd2让其再次投入使用。
可以看出,数据还是可用的,不过必须手工选择另外一个节点进行连接显然有点儿不友好。幸好etcd为此提供了一个解决方案——可以以“代理”模式启动节点,这意味着它不复制任何数据,而只是将请求转发给其他节点:
$ docker run -d -p 8080:8080 —restart always —name etcd-proxy $IMG \-proxy on -listen-client-urls http://0.0.0.0:8080 \-initial-cluster $CLUSTER037c3c3dba04826a76c1d4506c922267885edbfa690e3de6188ac6b6380717ef$ curl -L $HTTPIP:8080/v2/keys/mykey2 -XPUT -d value=”t”{“action”:”set”,”node”: {“key”:”/mykey2”,”value”:”t”,➥ “modifiedIndex”:12,”createdIndex”:12}}$ docker kill etcd1 etcd2$ curl -L $HTTPIP:8080/v2/keys/mykey2{“action”:”get”,”node”: {“key”:”/mykey2”,”value”:”t”,➥ “modifiedIndex”:12,”createdIndex”:12}}
这样就可以自由地体验当一半的节点离线时etcd是如何工作的了:
$ curl -L $HTTPIP:8080/v2/keys/mykey3 -XPUT -d value=”t”{“message”:”proxy: unable to get response from 3 endpoint(s)”}$ docker start etcd2etcd2$ curl -L $HTTPIP:8080/v2/keys/mykey3 -XPUT -d value=”t”{“action”:”set”,”node”: {“key”:”/mykey3”,”value”:”t”,➥ “modifiedIndex”:16,”createdIndex”:16}}
当一半或更多节点不可用时,etcd将允许读取,而禁止写入。
由此可见,可以在集群的每个节点上启动一个etcd代理作为“大使容器”(ambassador container)用于获取集中式配置:
$ docker run -it —rm —link etcd-proxy:etcd ubuntu:14.04.2 bashroot@8df11eaae71e:/# apt-get install -y wgetroot@8df11eaae71e:/# wget -q -O- http://etcd:8080/v2/keys/mykey3{“action”:”get”,”node”: {“key”:”/mykey3”,”value”:”t”,➥ “modifiedIndex”:16,”createdIndex”:16}}
什么是大使 大使是Docker用户中流行的一种所谓“Docker模式”。大使容器位于应用程序容器与外部服务之间,负责处理相应请求。它与代理类似,但融入了一些智能用于处理具体情况的需求——就像现实中的大使一样。
一旦在所有环境中运行了etcd,在某个环境中创建机器只需要将其链接到etcd-proxy容器并启动即可——所有该机器的 CD 构建都将使用该环境的正确配置。下一个技巧将展示如何使用etcd-provided配置来实现零停机时间升级。
7.4 升级运行中的容器
为了实现每天多次部署到生产环境的理想目标,减少部署流程最后一步——停止旧应用程序并启动新应用程序——的停机时间就非常重要了。如果每次切换都需要一小时,那么一天部署4次就没有意义!
因为容器提供了一个隔离的环境,所以很多问题已经得到缓解。例如,无须再担心一个应用程序的两个版本会使用同一工作目录并互相冲突,也无须使用新代码重启来重新读取配置文件以获取新值。
但是,这同样存在一些弊端——就地修改文件不再是一件简单的事,“软重启”(用于获取配置文件变更)因而变得更难实现。因此,一个好的做法是,不论修改的是一些配置文件还是上千行代码,永远执行相同的升级过程。
下面来看一个升级过程,它将实现面向Web的应用程序零停机时间部署的黄金标准。
技巧67 使用confd启用零停机时间切换
由于容器在一台宿主机上可以共存,删除一个容器并启动一个新容器这样的简单切换方式可以在几秒内完成(同样可以实现快速回滚)。
对大多数应用程序来说,这可能已经够快了,但那些具有较长启动时间或高可用需求的应用程序则需要另外一种方法。有时,这是一个要求应用程序自身做特殊处理的不可避免的复杂过程,不过面向Web的应用程序有一个方案可以优先考虑。
问题
想要在升级面向Web的应用程序时做到零停机时间。
解决方案
在宿主机上使用nginx和confd来执行两阶段切换。
讨论
nginx是一个非常流行的Web服务器,它具有一项重要的内置功能——在不断开客户端与服务器连接的情况下重新加载配置文件。通过将其与confd——一个可从中央数据仓库(如etcd)获取信息并对配置文件进行相应修改的工具——组合,即可使用最新的设置对etcd进行更新,然后由其完成后续处理。
Apache/HAProxy方案 Apache HTTP服务器与HAProxy二者也提供了零停机时间重载功能,对于有相应配置经验的用户也可以用其替代nginx。
第一步是启动一个应用程序作为最终将更新的旧应用程序。Ubuntu附带的Python具有一个内置的Web服务器,可以使用它作为示例:
$ ip addr | grep ‘inet ‘ | grep -v ‘lo$|docker0$’inet 10.194.12.221/20 brd 10.194.15.255 scope global eth0$ HTTPIP=http://10.194.12.221$ docker run -d —name py1 -p 80 ubuntu:14.04.2 \sh -c ‘cd / && python3 -m http.server 80’e6b769ec3efa563a959ce771164de8337140d910de67e1df54d4960fdff74544$ docker inspect -f ‘{{.NetworkSettings.Ports}}’ py1map[80/tcp:[map[HostIp:0.0.0.0 HostPort:49156]]]$ curl -s localhost:49156 | tail<li><a href=”sbin/“>sbin/</a></li><li><a href=”srv/“>srv/</a></li><li><a href=”sys/“>sys/</a></li><li><a href=”tmp/“>tmp/</a></li><li><a href=”usr/“>usr/</a></li><li><a href=”var/“>var/</a></li></ul><hr></body></html>
HTTP服务器已经成功启动,这里使用了inspect命令的过滤选项将宿主机端口与内部容器映射信息提取出来。
现在确保etcd正在运行——本技巧假定工作环境与上一技巧相同。为简单起见,这一次将使用etcdctl(“etcd controller”的简称)与etcd(而不是直接对etcd进行curl)进行交互:
$ IMG=dockerinpractice/etcdctl$ docker pull dockerinpractice/etcdctl[…]$ alias etcdctl=”docker run —rm $IMG -C \”$HTTPIP:8080\””$ etcdctl set /test valuevalue$ etcdctl ls/test
这将下载我们准备好的etcdctl Docker镜像,同时设置一个别名用于连接前面设置的etcd集群。现在启动nginx:
$ IMG=dockerinpractice/confd-nginx$ docker pull $IMG[…]$ docker run -d —name nginx -p 8000:80 $IMG $HTTPIP:80805a0b176586ef9e3514c5826f17d7f78ba8090537794cef06160ea7310728f7dc
这是一个我们提前准备好的镜像,它使用confd从etcd获取信息,并自动更新配置文件。传递的参数将告知容器它可以连接的etcd集群。不过这里尚未告诉它到哪去查找应用程序,因此日志中充满了错误!
下面将添加适当的信息到etcd中:
$ docker logs nginxUsing http://10.194.12.221:8080 as backend2015-05-18T13:09:56Z fc6082e55a77 confd[14]:➥ ERROR 100: Key not found (/app) [14]2015-05-18T13:10:06Z fc6082e55a77 confd[14]:➥ ERROR 100: Key not found (/app) [14]$ echo $HTTPIPhttp://10.194.12.221$ etcdctl set /app/upstream/py1 10.194.12.221:4915610.194.12.221:49156$ sleep 10$ docker logs nginxUsing http://10.194.12.221:8080 as backend2015-05-18T13:09:56Z fc6082e55a77 confd[14]:➥ ERROR 100: Key not found (/app) [14]2015-05-18T13:10:06Z fc6082e55a77 confd[14]:➥ ERROR 100: Key not found (/app) [14]2015-05-18T13:10:16Z fc6082e55a77 confd[14]:➥ ERROR 100: Key not found (/app) [14]2015-05-18T13:10:26Z fc6082e55a77 confd[14]:➥ INFO Target config /etc/nginx/conf.d/app.conf out of sync2015-05-18T13:10:26Z fc6082e55a77 confd[14]:➥ INFO Target config /etc/nginx/conf.d/app.conf has been updated$ curl -s localhost:8000 | tail<li><a href=”sbin/“>sbin/</a></li><li><a href=”srv/“>srv/</a></li><li><a href=”sys/“>sys/</a></li><li><a href=”tmp/“>tmp/</a></li><li><a href=”usr/“>usr/</a></li><li><a href=”var/“>var/</a></li></ul><hr></body></html>
对etcd的更新已经被confd读取并应用到nginx配置文件中,让用户可以访问这个简单的文件服务器。这里包含sleep命令是因为配置了confd每10秒检查更新。在这背后,confd-nginx容器中运行着一个confd守护进程来拉取etcd集群中的变更,并只在检测到变更时使用容器内的模板重新生成nginx配置。
假设我们决定对外提供/etc目录而不是/目录。现在启动第二个应用程序并将其添加到etcd中。因为此时有两个后端,最终将得到其各自的响应:
$ docker run -d —name py2 -p 80 ubuntu:14.04.2 \sh -c ‘cd /etc && python3 -m http.server 80’9b5355b9b188427abaf367a51a88c1afa2186e6179ab46830715a20eacc33660$ docker inspect -f ‘{{.NetworkSettings.Ports}}’ py2map[80/tcp:[map[HostIp:0.0.0.0 HostPort:49161]]]$ curl $HTTPIP:49161 | tail | head -n 5<li><a href=”udev/“>udev/</a></li><li><a href=”update-motd.d/“>update-motd.d/</a></li><li><a href=”upstart-xsessions”>upstart-xsessions</a></li><li><a href=”vim/“>vim/</a></li><li><a href=”vtrgb”>vtrgb@</a></li>$ etcdctl set /app/upstream/py2 $HTTPIP:4916110.194.12.221:49161$ etcdctl ls /app/upstream/app/upstream/py1/app/upstream/py2$ curl -s localhost:8000 | tail | head -n 5<li><a href=”sbin/“>sbin/</a></li><li><a href=”srv/“>srv/</a></li><li><a href=”sys/“>sys/</a></li><li><a href=”tmp/“>tmp/</a></li><li><a href=”usr/“>usr/</a></li>$ curl -s localhost:8000 | tail | head -n 5<li><a href=”udev/“>udev/</a></li><li><a href=”update-motd.d/“>update-motd.d/</a></li><li><a href=”upstart-xsessions”>upstart-xsessions</a></li><li><a href=”vim/“>vim/</a></li><li><a href=”vtrgb”>vtrgb@</a></li>
在上述过程中,在将新容器添加到etcd之前,会先确认它是否正确启动(见图7-4)。因此,可以通过覆盖etcd中的/app/upstream/py1键一步完成该过程。如果要求一次只能有一个后台可供访问,这个做法就很有用。
图7-4 添加py2容器到etcd中
使用两阶段切换,最后的阶段是删除旧的后端和容器:
$ etcdctl rm /app/upstream/py1$ etcdctl ls /app/upstream/app/upstream/py2$ docker rm -f py1py1
这样,新的应用程序就启动运行了!对用户而言,此应用程序没有不可访问的情况,并且不需要手工连接到Web服务器机器上去重载nginx。
confd的使用不仅限于配置Web服务器:只要某个文件包含需要根据外部值来更新的文本,confd便可派上用场。上一个技巧中提到了etcd不能用于存储长度很大的值。因此,并不一定要将confd与etcd搭配使用。多数流行的键值存储系统都有可用的集成方案,因此如果已经有可以正常运转的系统,则无须再添加另外的部分。
在生产环境中使用Docker时,如果需要为某个服务更新后端服务器,完全可以避免手工修改etcd,后面的技巧83就介绍了这样一种方法。
7.5 小结
使用Docker将引导用户走上流线型CD流水线的道路。不过,本章也探索了一些方法,使用额外的工具对Docker进行补充,解决Docker的一些局限性。
本章主要讲述:
- 在registry之间移动镜像可作为控制构建能在CD流水线中走多远的一个好办法;
- bup在压榨镜像传输方面非常在行,甚至比分层效果更佳;
- etcd可作为环境的中央配置存储;
- 零停机时间部署可通过etcd、confd以及nginx的组合来实现。
下一章将讲述在测试时通过模拟网络进一步加快CD流水线,而不需要一个完整的环境。
