第9章 容器编排:管理多个Docker容器
本章主要内容
- 在单台宿主机上管理Docker容器
- 将容器部署到多台宿主机
- 获取容器部署的位置信息
Docker依赖的技术实际上已经以不同形式存在一段时间了,但Docker是那个成功抓住技术行业兴趣点的解决方案。这把Docker推到了一个令人羡慕不已的位置——社区先驱们完成了这一系列工具的开创工作,这些工具又吸引使用者加入社区并不断地回馈社区,形成了一个自行运转的生态系统。
这片繁荣的景象在编排领域尤为明显。在这一领域提供服务的公司可以列出一大堆,看过这些公司名字的清单之后读者会发现,关于如何实现编排它们都有自己的看法,也都开发了自己的工具。
虽然该生态系统是Docker的一个巨大优势(这也是我们在本书中用如此大的篇幅介绍的原因),然而编排工具数量众多,无论对新手还是老手都有点儿难以抉择。本章将带读者浏览一些最受瞩目的工具,感受一下这些高端工具,以便在需要选用适合自己的框架时能更加了解情况。
有多种不同的方式来组合编排工具的家族树。图9-1展示了我们熟悉的一些工具。
树的根节点是docker run命令,这是启动容器最常用的方式。Docker家族的几乎所有工具都衍生于这一命令。树的左侧分支上的工具将一组容器视为单个实体,中间分支的工具借助systemd或服务文件管理容器,右侧分支上的工具将单个容器视为单个实体。沿着这些分支往下,这些工具做的事情越来越多,例如,它可以跨多台宿主机工作,或者让用户远离手动部署容器的繁琐操作。
读者可能会注意到图 9-1 中看似孤立的两个区域——Mesos和Consul/etcd/Zookeeper组。Mesos是一个有趣的东西,它在Docker之前就已经存在,并且它对Docker的支持是一个附加功能,而不是核心功能。虽然它做得不错,但是也需要仔细评估,如果仅仅是从功能特性上来看,用户可能在其他工具中也想要有这些。相比之下,Consul、etcd和Zookeeper根本不是编排工具。相反,它们为编排提供了重要的补充功能——服务发现。
图9-1 Docker生态系统中的编排工具
阅读本章时,时不时地回顾每个编排工具,并尝试和提出一个适用场景,可能会有助于确定哪款特定的工具适合用户的需求。我们还会提供一些示例,供读者开始学习。
接下来先从单台宿主机开始。
9.1 简单的单台宿主机
在本地机器上管理容器可能是一种痛苦的体验。Docker为长期运行的容器提供的管理功能比较原始,而启动带有链接和共享卷的容器更是一个令人沮丧的手动过程。
在第8章里我们讲过如何使用Docker Compose来更方便地管理链接,因此现在我们把重心转向另一个痛点,来看看如何使单机下长期运行容器的管理变得更加健壮。
技巧75 使用systemd管理宿主机上的容器
在这一技巧里,我们将使用systemd配置一个简单的Docker服务。如果读者熟悉systemd,跟进本章内容将会相对容易些,但我们假设读者之前对此工具并不了解。
对于一个拥有运维团队的成熟公司来说,使用systemd控制Docker是很有用的,因为他们更喜欢沿用自己已经了解并且已经工具化的经过生产验证的技术。
问题
想要管理宿主机上Docker容器服务的运行。
解决方案
使用systemd管理容器服务。
讨论
systemd是一个系统管理的守护进程,它在前段时间取代了Fedora的SysV init脚本。它可以通过独立单元的形式管理系统上的所有服务——从挂载点到进程,甚至到一次性脚本。它在被推广到其他发行版和操作系统后变得愈加受欢迎,虽然在一些系统上安装和启用它可能还有问题(编写本书时Gentoo就是一个例子)。设置systemd时,别人使用systemd过程中遇到类似问题的处理经验值得借鉴。
在本技巧里,我们将通过运行第1章中的to-do应用程序来演示如何使用systemd管理容器的启动。
安装systemd
如果用户的宿主机系统上还没有安装systemd(可以运行systemctl status命令来检查,查看是否能得到正确的响应),可以使用标准包管理工具将其直接安装到宿主机的操作系统上。如果不太习惯以这种方式与宿主机系统交互,推荐使用Vagrant来部署一个已经安装好systemd的虚拟机,如代码清单9-1所示。这里我们只做简要介绍,有关安装Vagrant的更多建议参见附录C。
代码清单9-1 设置Vagrant
$ mkdir centos7docker (1)$ cd centos7_docker$ vagrant init jdiprizio/centos-docker-io (2)$ vagrant (3)$ vagrant ssh (4)
(1)创建并进入一个新的目录
(2)将目录初始化成一个Vagrant环境,指定Vagrant镜像
(3)启动虚拟机
(4)采用SSH的方式登入虚拟机
jdiprizio/centos-docker-io镜像库不再可用? 在编写本书时,jdiprizio/centos-docker-io是一个合适并可用的虚拟机镜像。如果读者阅读本书时发现它已经失效,可以使用另一个镜像名称来替换代码清单9-1中的这一字符串。读者可以在Atlas的“Discover Vagrant Boxes”页面上搜索一个镜像:https://atlas.hashicorp.com/boxes/search(box是一个Vagrant用来指代虚拟机镜像的术语)。要查找该镜像,我们可以搜索“docker centos”。在启动新的虚拟机之前,读者可能需要查看
vargant box add命令行的帮助文档,了解如何下载该虚拟机。
用systemd设置一个简单的Docker应用程序
现在机器上安装好了systemd和Docker,接下来使用该机器运行第1章中讲到的to-do应用程序。
systemd通过读取INI格式的配置文件来工作。
INI文件 INI文件是一种简单的文本文件,其基本结构由节、属性和值组成。
首先以root身份创建一个服务文件/etc/systemd/system/todo.service,如代码清单9-2所示。在这个文件里告诉systemd在宿主机的8000端口上运行一个名为todo的Docker容器。
代码清单9-2 /etc/systemd/system/todo.service
[Unit] (1)Description=Simple ToDo ApplicationAfter=docker.service (2)Requires=docker.service (3)[Service] (4)Restart=always (5)ExecStartPre=/bin/bash \-c ‘/usr/bin/docker rm -f todo || /bin/true’ (6)ExecStartPre=/usr/bin/docker pull dockerinpractice/todo (7)ExecStart=/usr/bin/docker run —name todo \-p 8000:8000 dockerinpractice/todo (8)ExecStop=/usr/bin/docker rm -f todo (9)[Install] (10)WantedBy=multi-user.target (11)
(1)Unit部分定义了systemd对象的通用信息
(2)Docker服务启动之后立即启动这个单元
(3)该单元成功运行的前提是运行Docker服务
(4)Service 部分定义了与systemd 服务单元类型相关的配置信息
(5)如果服务终止了,总是重启它
(6)ExecStartPre 定义了一个命令。该命令会在该单元启动前运行。要确保启动该单元前容器已经删掉,可以在这里删除它
(7)确保运行容器之前已经下载了该镜像
(8)ExecStart 定义了服务启动时要运行的命令
(9)ExecStop 定义了服务停止时要运行的命令
(10)Install 部分包含了启用该单元时systemd 所需的信息
(11)告知systemd当进入多用户目标环境的时候希望启动该服务单元
从该配置文件可以非常清楚地看出,systemd为进程的管理提供了一种简单的声明式模式,将依赖管理的细节交给systemd服务去处理。但这并不意味着用户可以忽视这些细节,只是它确实为用户提供了很多方便的工具来管理Docker(和其他)进程。
Docker重启策略和进程管理器 默认情况下Docker不会设置任何重启策略,但值得注意的是,一旦有所设置,它都会和大多数的进程管理器冲突。因此,如果使用了进程管理器就不要设置重启策略。
启动一个新的服务单元即是调用systemctl enable命令。如果希望系统启动的时候该服务单元能够自动启动,也可以在systemd的multi-user.target.wants目录下创建一个符号链接。一旦完成,就可以使用systemctl start来启动该单元了:
$ systemctl enable /etc/systemd/system/todo.service$ ln -s ‘/etc/systemd/system/todo.service’ \‘/etc/systemd/system/multi-user.target.wants/todo.service’$ systemctl start todo.service
然后只要等它启动。如果出现问题会有相应的提示。
可以使用systemctl status命令来检查是否一切正常。它会打印一些关于该服务单元的通用信息,如进程运行的时间以及相应的进程 ID,紧随其后的是该进程的日志信息。通过以下例子可以中看出Swarm服务端在8000端口下正常启动:
[root@centos system]# systemctl status todo.servicetodo.service - Simple ToDo ApplicationLoaded: loaded (/etc/systemd/system/todo.service; enabled)Active: active (running) since Wed 2015-03-04 19:57:19 UTC; 2min 13s agoProcess: 21266 ExecStartPre=/usr/bin/docker pull dockerinpractice/todo➥ (code=exited, status=0/SUCCESS)Process: 21255 ExecStartPre=/bin/bash -c /usr/bin/docker rm -f todo ||➥ /bin/true (code=exited, status=0/SUCCESS)Process: 21246 ExecStartPre=/bin/bash -c /usr/bin/docker kill todo ||➥ /bin/true (code=exited, status=0/SUCCESS)Main PID: 21275 (docker)CGroup: /system.slice/todo.service??21275 /usr/bin/docker run —name todo➥ -p 8000:8000 dockerinpractice/todoMar 04 19:57:24 centos docker[21275]: TodoApp.js:117:➥ // TODO scroll into viewMar 04 19:57:24 centos docker[21275]: TodoApp.js:176:➥ if (i>=list.length()) { i=list.length()-1;} // TODO .lengthMar 04 19:57:24 centos docker[21275]: local.html:30:➥ <!— TODO 2-split, 3-split —>Mar 04 19:57:24 centos docker[21275]: model/TodoList.js:29:➥ // TODO one op - repeated spec? long spec?Mar 04 19:57:24 centos docker[21275]: view/Footer.jsx:61:➥ // TODO: show the entry’s metadataMar 04 19:57:24 centos docker[21275]: view/Footer.jsx:80:➥ todoList.addObject(new TodoItem()); // TODO create defaultMar 04 19:57:24 centos docker[21275]: view/Header.jsx:25:➥ // TODO list some meaningful header (apart from the id)Mar 04 19:57:24 centos docker[21275]: > todomvc-swarm@0.0.1 start /todoMar 04 19:57:24 centos docker[21275]: > node TodoAppServer.jsMar 04 19:57:25 centos docker[21275]: Swarm server started port 8000
本技巧中介绍的一些原理不只适用于systemd,大部分进程管理器,包括其他的init系统,都可以采用类似的方式来配置。
在下一个技巧里,我们会更进一步,使用systemd来实现在技巧69中创建的SQLite服务器。
技巧76 使用systemd编排宿主机上的容器
不同于docker-compose(编写本书时),systemd已经是一个用于生产的成熟技术。在本技巧中,我们将展示如何使用systemd来实现和docker-compose类似的本地编排功能。
如果读者在学习本技巧的过程中遇到问题,可能需要升级一下Docker版本,1.7.0及以上版本应该会工作正常。
问题
想要在生产环境的宿主机上管理更复杂的容器编排。
解决方案
使用systemd和相应的依赖服务来管理容器。
讨论
为了展示systemd在更复杂的场景中的应用,我们会在systemd中重新实现一遍技巧69中提到的SQLite TCP服务器。
图9-2展示了我们计划实现的systemd服务单元配置中的依赖。
图9-2 systemd单元依赖图
这里的模式和技巧69中Docker Compose例子中的模式是类似的。其中有一个关键的差别,SQLite服务在这里不再被当成一个单体对象,这里的每个容器都是一个离散的实体。在这个场景下,SQLite代理可以独立于SQLite服务器,被单独停止。
代码清单9-3展示了sqliteserver服务的代码。像以前一样,它依赖Docker服务,但和前面介绍的to-do实例有一些不同之处。
代码清单9-3 /etc/systemd/system/sqliteserver.service
[Unit] (1)Description=SQLite Docker ServerAfter=docker.service (2)Requires=docker.service (3)[Service]Restart=alwaysExecStartPre=-/bin/touch /tmp/sqlitedbs/test (4)ExecStartPre=-/bin/touch /tmp/sqlitedbs/liveExecStartPre=/bin/bash \-c ‘/usr/bin/docker kill sqliteserver || bin/true’ExecStartPre=/bin/bash \ (5)-c ‘/usr/bin/docker rm -f sqliteserver || /bin/true’ExecStartPre=/usr/bin/docker \pull dockerinpractice/docker-compose-sqlite (6)ExecStart=/usr/bin/docker run —name sqliteserver \ (7)-v /tmp/sqlitedbs/test:/opt/sqlite/db \dockerinpractice/docker-compose-sqlite /bin/bash -c \‘socat TCP-L:12345,fork,reuseaddr \EXEC:”sqlite3 /opt/sqlite/db”,pty’ExecStop=/usr/bin/docker rm -f sqliteserver (8)[Install]WantedBy=multi-user.target
(1)Unit小节定义了该systemd对象的通用信息
(2)Docker服务启动之后启动该单元
(3)为了让该服务正常运行,Docker服务必须处于正常运行状态
(4)这几行代码确保服务启动之前SQLite的数据库文件是存在的,touch命令行之前的-告诉systemd:如果该命令返回错误代码则表明启动失败
(5)ExecStartPre定义了服务单元被启动之前运行的命令。为了确保容器在用户启动之前已被删除,这里使用了一个前置命令将其删除
(6)确保启动容器之前镜像已下载完成了
(7)ExecStart 定义了服务被启动之后运行的命令。这里值得注意的是,我们在另一个/bin/bash–c调用中包含了socat 命令,因为在ExecStart 这一行定义的命令是由systemd来运行的
(8)ExecStop定义了服务停止之后运行的命令
要求绝对路径 systemd中使用的路径必须是绝对路径。
代码清单9-4列出的是sqliteproxy服务。这里最大的区别在于,代理服务依赖于刚刚定义的服务器进程,而服务端进程又依赖于Docker服务。
代码清单9-4 /etc/systemd/system/sqliteproxy.service
[Unit]Description=SQLite Docker ProxyAfter=sqliteserver.service (1)Requires=sqliteserver.service (2)[Service]Restart=alwaysExecStartPre=/bin/bash -c ‘/usr/bin/docker kill sqliteproxy || /bin/true’ExecStartPre=/bin/bash -c ‘/usr/bin/docker rm -f sqliteproxy || /bin/true’ExecStartPre=/usr/bin/docker pull dockerinpractice/docker-compose-sqliteExecStart=/usr/bin/docker run —name sqliteproxy \-p 12346:12346 —link sqliteserver:sqliteserver \dockerinpractice/docker-compose-sqlite /bin/bash \-c ‘socat TCP-L:12346,fork,reuseaddr TCP:sqliteserver:12345’ (3)ExecStop=/usr/bin/docker rm -f sqliteproxy[Install]WantedBy=multi-user.target
(1)该代理单元必须在前面定义的sqliteserver服务之后运行
(2)启动该代理之前要求服务器实例在运行
(3)该命令用于运行容器
通过这两个配置文件,我们为在systemd控制下安装和运行SQLite服务奠定了基础。现在我们可以启用这些服务了:
$ sudo systemctl enable /etc/systemd/system/sqliteserver.serviceln -s ‘/etc/systemd/system/sqliteserver.service’ \‘/etc/systemd/system/multi-user.target.wants/sqliteserver.service’$ sudo systemctl enable /etc/systemd/system/sqliteproxy.serviceln -s ‘/etc/systemd/system/sqliteproxy.service’ \‘/etc/systemd/system/multi-user.target.wants/sqliteproxy.service’
然后启动它们:
$ sudo systemctl start sqlit\eproxy$ telnet localhost 12346[vagrant@centos ~]$ telnet localhost 12346Trying ::1…Connected to localhost.Escape character is ‘^]’.SQLite version 3.8.2 2013-12-06 14:53:30Enter “.help” for instructionsEnter SQL statements terminated with a “;”sqlite> select from t1;select from t1;test
值得注意的是,sqliteproxy服务依赖于sqliteserver服务的运行。只需要启动sqliteproxy服务即可,其他依赖的服务会自动启动。
9.2 多宿主机Docker
目前读者应该对单台机器上相对复杂的Docker部署和编排已经有了一定的信心,现在该考虑一些更复杂的事情了——让我们进入多宿主机的世界,这样我们便可以更大规模地使用Docker。
将Docker容器迁移到目标机器并启动它们的最佳流程在Docker世界里一直颇具争议。一些知名公司创造了自己的方式并发布给全世界使用。如果用户可以自行决定使用什么工具的话,他会从中获益不少。
这是一个快速变动的话题——我们已经看到了Docker的多个编排工具的诞生和消亡,而在考虑是否迁移到全新的工具时建议保持谨慎。因此,我们应该试图去选用那些显著稳定或是势头强劲(或两者兼有)的工具。
技巧77 使用Helios手动管理多宿主机Docker
将一组机器的初始化和部署工作都交给一个应用程序确实容易让人恐慌,如果能有一种手动的方式来操作可以缓解这种恐慌。
对主要采用静态基础设施并且希望将 Docker 用于关键业务时这一过程能够有人力监督(可以理解)的公司来说,Helios是理想的选择。
问题
想要初始化多台Docker宿主机环境来运行容器,同时又保留对整个过程的手动控制。
解决方案
使用Spotify公司的Helios工具。
讨论
Helios是Spotify公司目前在生产环境中用来管理其服务器的工具,它具有易于上手和稳定的友好特性(如你所望)。Helios允许用户管理Docker容器在多台宿主机上的部署。它提供了一个简单的命令行接口,用户可以用它来指定运行的内容以及运行的位置,也可以查看当前运行的状态。
因为这里只是介绍Helios,简单起见,我们将在Docker内的单个节点上运行所有内容——不用担心,与多宿主机运行场景有关的一切都会被清楚地着重标示出来。Helios的整体架构如图9-3所示。
如图9-3所示,运行Helios时只需要运行一个额外的服务Zookeeper即可。Helios使用Zookeeper跟踪所有宿主机的状态,同时它也作为主机和代理节点之间的通信通道。
什么是Zookeeper Zookeeper是一款轻量级的分布式数据库,经过优化用于存储Java编写的配置信息。它是Apache开源软件产品套件之一,功能类似于etcd(第7章介绍过该工具,本章也会再次出现)。
在本技巧中读者只需要知道Zookeeper是用来存放数据的,以便通过运行多个Zookeeper实例的方式将数据分布在多个节点上,从而实现可扩展性和可靠性。这可能听起来和第 7 章中对etcd的描述有些类似——这两个工具在功能上有很大的重叠。
执行如下命令来运行我们将在本技巧中使用的单个Zookeeper实例:
$ docker run —name zookeeper -d jplock/zookeeper:3.4.6cd0964d2ba18baac58b29081b227f15e05f11644adfa785c6e9fc5dd15b85910$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ zookeeper172.17.0.9
图9-3 Helios安装的鸟瞰图
宿主机和其他节点上的端口 在自身节点启动Zookeeper实例时,用户需要暴露一些服务端口供其他宿主机访问,并且使用卷来持久化数据。查看Docker Hub上的Dockerfile(https://hub.docker.com/ r/jplock/zookeeper/~/dockerfile/)来了解应该使用哪些端口和文件夹等细节。读者也可能想要在多个节点上运行Zookeeper,但是配置一个Zookeeper集群超出了本技巧的范畴。
可以使用zkCli.sh工具检查Zookeeper存储的数据,既可以通过交互的方式也可以通过管道输入。该工具的启动过程提示信息非常丰富,但它会立马进入一个交互式命令行,用户可以针对存储Zookeeper数据的文件树状结构执行一些命令:
$ docker exec -it zookeeper bin/zkCli.shConnecting to localhost:21812015-03-07 02:56:05,076 [myid:] - INFO [main:Environment@100] - Client➥ environment:zookeeper.version=3.4.6-1569965, built on 02/20/2014 09:09 GMT2015-03-07 02:56:05,079 [myid:] - INFO [main:Environment@100] - Client➥ environment:host.name=917d0f8ac0772015-03-07 02:56:05,079 [myid:] - INFO [main:Environment@100] - Client➥ environment:java.version=1.7.0_652015-03-07 02:56:05,081 [myid:] - INFO [main:Environment@100] - Client➥ environment:java.vendor=Oracle Corporation[…]2015-03-07 03:00:59,043 [myid:] - INFO [main-SendThread(localhost:2181):➥ ClientCnxn$SendThread@1235] - Session establishment complete on server➥ localhost/0:0:0:0:0:0:0:1:2181, sessionid = 0x14bf223e159000d, negotiated➥ timeout = 30000WATCHER::WatchedEvent state:SyncConnected type:None path:null[zk: localhost:2181(CONNECTED) 0] ls /[zookeeper]
目前没有对Zookeeper进行任何操作,因此目前仅存储了一些Zookeepr内部信息。先保留这个命令行提示符开放,稍后我们会继续用到它。
Helios本身由以下3部分组成:
- 主机(master)——它通常是用作对Zookeeper中数据进行修改的接口;
- 代理(agent)——运行在每台Docker宿主机上,启动和停止基于Zookeeper的容器,然后报告状态;
- 命令行工具——用于向主机发起请求。
图9-4展示了完成对系统发起的操作时最终系统是如何处理的(箭头表示数据流)。
现在Zookeeper已经运行起来了,是时候去启动Helios了。启动主机时需要指定前面启动的Zookeeper节点的IP地址:
$ IMG=dockerinpractice/docker-helios$ docker run -d —name hmaster $IMG helios-master —zk 172.17.0.9896bc963d899154436938e260b1d4e6fdb0a81e4a082df50043290569e5921ff$ docker logs —tail=3 hmaster03:20:14.460 helios[1]: INFO [MasterService STARTING] ContextHandler:➥ Started i.d.j.MutableServletContextHandler@7b48d370{/,null,AVAILABLE}03:20:14.465 helios[1]: INFO [MasterService STARTING] ServerConnector:➥ Started application@2192bcac{HTTP/1.1}{0.0.0.0:5801}03:20:14.466 helios[1]: INFO [MasterService STARTING] ServerConnector:➥ Started admin@28a0d16c{HTTP/1.1}{0.0.0.0:5802}$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ hmaster172.17.0.11
图9-4 在安装了Helios的单台宿主机上启动容器
现在看看Zookeeper里面新增了些什么:
[zk: localhost:2181(CONNECTED) 1] ls /[history, config, status, zookeeper][zk: localhost:2181(CONNECTED) 2] ls /status/masters[896bc963d899][zk: localhost:2181(CONNECTED) 3] ls /status/hosts[]
看起来Helios主机已经创建了一堆新的配置,包括将自身注册为主机。遗憾的是,现在还没有任何宿主机。让我们通过启动一个代理来解决这个问题。该代理将使用当前宿主机的Docker套接字来启动容器:
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -d —name hagent \dockerinpractice/docker-helios helios-agent —zk 172.17.0.95a4abcb271070d0171ca809ff2beafac5798e86131b72aeb201fe27df64b2698$ docker logs —tail=3 hagent03:30:53.344 helios[1]: INFO [AgentService STARTING] ContextHandler:➥ Started i.d.j.MutableServletContextHandler@774c71b1{/,null,AVAILABLE}03:30:53.375 helios[1]: INFO [AgentService STARTING] ServerConnector:➥ Started application@7d9e6c27{HTTP/1.1}{0.0.0.0:5803}03:30:53.376 helios[1]: INFO [AgentService STARTING] ServerConnector:➥ Started admin@2bceb4df{HTTP/1.1}{0.0.0.0:5804}$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ hagent172.17.0.12
再次检查Zookeeper:
[zk: localhost:2181(CONNECTED) 4] ls /status/hosts[5a4abcb27107][zk: localhost:2181(CONNECTED) 5] ls /status/hosts/5a4abcb27107[agentinfo, jobs, environment, hostinfo, up][zk: localhost:2181(CONNECTED) 6] get /status/hosts/5a4abcb27107/agentinfo{“inputArguments”:[“-Dcom.sun.management.jmxremote.port=9203”, […][…]
可以看到/status/hosts现在有了一条数据。深入该宿主机的Zookeeper目录可以看到这里面有Helios针对该宿主机存储的内部信息。
多宿主机设置时需要提供宿主机名 如果运行在多宿主机环境下,将需要把
—name \$(hostname -f)作为Helios主机和代理使用的参数传递进去。用户还需要为主机暴露5801和5802端口,为代理暴露5803和5804端口。
让我们一起来简化和Helios的交互:
$ alias helios=”docker run -i —rm dockerinpractice/docker-helios \helios -z http://172.17.0.11:5801“
上面的别名意味着调用helios的话将会启动一个一次性容器来执行所需的操作,并且在开头指向正确的helios集群。注意,cli需要指向Helios主机而不是Zookeeper。
现在一切准备就绪。我们可以轻松地和Helios集群进行交互了,不妨尝试下面这个示例:
$ helios create -p nc=8080:8080 netcat:v1 ubuntu:14.04.2 — \sh -c ‘echo hello | nc -l 8080’Creating job: {“command”:[“sh”,”-c”,”echo hello | nc -l 8080”],➥ “creatingUser”:null,”env”:{},”expires”:null,”gracePeriod”:null,➥ “healthCheck”:null,”id”:➥ “netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac”,➥ “image”:”ubuntu:14.04.2”,”ports”:{“nc”:{“externalPort”:8080,➥ “internalPort”:8080,”protocol”:”tcp”}},”registration”:{},➥ “registrationDomain”:””,”resources”:null,”token”:””,”volumes”:{}}Done.netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac$ helios jobsJOB ID NAME VERSION HOSTS COMMAND ENVIRONMENTnetcat:v1:2067d43 netcat v1 0 sh -c “echo hello | nc -l 8080”
Helios是围绕作业(job)的概念建立的——所有被执行的对象都必须被表示为一个作业,然后才能被发送到要执行的主机。至少,用户需要一个镜像,带上Helios需要知道的如何启动容器的基本信息:一个要执行的命令以及任意端口、卷或者环境选项。用户可能还需要一些其他高级选项,包括健康检查、过期时间和服务注册等。
上面的第一条命令创建了一个监听8080端口的作业,访问该端口时会先输出hello,然后终止运行。
可以使用helios hosts列出可用于作业部署的主机,然后使用helios deploy命令完成部署。然后helios status命令将显示作业已成功启动:
$ helios hostsHOST STATUS DEPLOYED RUNNING CPUS MEM LOAD AVG MEM USAGE➥ OS HELIOS DOCKER5a4abcb27107.Up 19 minutes 0 0 4 7 gb 0.61 0.84➥ Linux 3.13.0-46-generic 0.8.213 1.3.1 (1.15)$ helios deploy netcat:v1 5a4abcb27107Deploying Deployment{jobId=netcat:v1:➥ 2067d43fc2c6f004ea27d7bb7412aff502e3cdac, goal=START, deployerUser=null}➥ on [5a4abcb27107]5a4abcb27107: done$ helios statusJOB ID HOST GOAL STATE CONTAINER ID PORTSnetcat:v1:2067d43 5a4abcb27107.START RUNNING b1225bc nc=8080:8080
当然,我们现在要验证服务是否正常工作:
$ curl localhost:8080hello$ helios statusJOB ID HOST GOAL STATE CONTAINER ID PORTSnetcat:v1:2067d43 5a4abcb27107.START PULLING_IMAGE b1225bc nc=8080:8080
curl命令的结果清楚地表明服务正在正常工作,但是helios status当前展示了一些有趣的信息。在定义作业时我们注意到,在输出完hello之后作业会终止,但上面的输出结果中显示的是PULLING_IMAGE状态。这涉及Helios是如何管理作业的——一旦将其部署到一台宿主机上,Helios会尽可能地确保作业处于运行状态。这个状态说明Helios正在进行完整的作业启动过程,包括确保镜像被成功拉取。
最后,我们需要自己手动做一些清理:
$ helios undeploy -a —yes netcat:v1Undeploying netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac from➥ [5a4abcb27107]5a4abcb27107: done$ helios remove —yes netcat:v1Removing job netcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdacnetcat:v1:2067d43fc2c6f004ea27d7bb7412aff502e3cdac: done
我们要求从所有节点中删除该作业(如有必要就终止它,并且停止所有自动重启的设置),然后删除作业本身,这意味着它不能再被部署到任何节点。
Helios是一个将容器部署到多台宿主机的简单可靠的方案。和后续将讲到的一些技巧不同的是,它的背后没有“魔法”来确定合适的部署位置——它会严格地部署到给定的位置,不会出现意外。
技巧78 基于Swarm的无缝Docker集群
能够完全控制集群当然很好,但是有时候集群管理太细是没有必要的。实际上,如果用户的这些应用程序没有很复杂的需求,完全可以充分利用Docker可以在任何地方运行的承诺——实在没有任何理由不把容器丢到集群里,让集群决定在哪里运行它们。
Swarm在研究型的实验室环境中很有用处,如果实验室能够将计算密集型的问题分解为一系列的小块,将使它们可以在机器集群里非常轻松地运行它们的问题。
问题
有一组安装了Docker的宿主机,想要启动容器并且不需要很细地管理它们的运行位置。
解决方案
使用Docker Swarm将宿主机集群当成一个Docker守护程序,并像平常一样运行Docker命令。
讨论
Docker Swarm由3个部分组成:代理(多个)、发现服务和主机。图9-5展示了这3个部分是如何在具备3个节点的Docker Swarm上交互的——每个节点上都安装了代理。
代理是一个运行在作为集群一部分的宿主机上的应用程序,它们将连接信息汇报到发现服务,并将宿主机转换为Docker Swarm中的一个节点。具有代理的宿主机都需要让Docker守护程序对外暴露一个端口——我们在技巧1中讲述了如何通过给Docker守护程序带上-H选项来实现这一点,并且我们假设使用默认的2375端口。
只有一个主机 默认情况下Swarm集群中只有一个主机。如果想要主机在出错情况下有容错机制,可以查看Docker官方提供的关于高可用的文档:https://docs.docker.com/swarm/multi-manager-setup/。
主节点启动时会联系发现服务,寻找集群中的节点。之后就可以直接连接到主机运行命令,主机会自动将请求转发给代理。
Docker 客户端最小要求 集群中的代理和客户端使用的所有Docker版本都必须至少是1.4.0。应该尽量让所有版本都保持一致,但是如果客户端中的版本不比代理中的版本新,集群应该也能正常工作。
设置 Swarm 集群的第一步是选择一个想用的发现服务。这里有多个选择,可以在一个文件中列出一组IP地址,也可以使用Zookeeper(还记得Helios吗?)。在本技巧中,我们将使用Docker Hub内置的带有令牌机制的发现服务。
发现服务后端 本技巧中的所有内容都可以在用户部署的服务中完成——借助Docker Hub提供的发现服务来注册用户的节点可以方便用户快速实现发现服务的功能。但如果用户不想把自己节点的IP地址放到潜在的公共区域(有些人可能会猜出用户的集群ID),可以阅读相关的可选后端系统的文档:http://docs.docker.com/swarm/discovery/。
图9-5 包含3个节点的Docker Swarm集群
Docker Hub发现服务要求用户取得一个令牌(token)来识别用户的集群。因为它是Docker公司提供的服务,所以Swarm拥有内置的功能来简化这个过程。Swarm二进制文件(自然)可以作为Docker镜像使用,因此可以使用如下命令进行操作:
h1 $ docker pull swarmh1 $ docker run swarm create126400c309dbd1405cd7218ed3f1a25eh1 $ CLUSTER_ID=126400c309dbd1405cd7218ed3f1a25e
swarm create命令后面的一长串字符串是用来标识用户集群的令牌。这个令牌非常重要——记下它!本技巧的后续内容中我们将使用CLUSTER_ID变量来引用它。
现在可以检查新创建的Swarm:
h1 $ docker run swarm list token://$CLUSTER_IDh1 $ curl https://discovery-stage.hub.docker.com/v1/clusters/$CLUSTER_ID[]
正如所见,当前还没什么东西。swarm list命令返回了空内容,同时(深入一点)直接查询Docker Hub发现服务里面集群的主机返回一个空列表。
启用了TLS的Docker守护程序 某些云服务提供了对启用了TLS的Docker守护程序的访问,读者也可以自己启用TLS。读者应该查阅Swarm文档,了解相关的生成证书并将其用于Swarm连接的最新信息:https://docs.docker.com/v1.5/swarm/#tls。
可以通过如下命令在当前机器上启动第一个代理:
h1 $ ip addr show eth0 | grep ‘inet ‘inet 10.194.12.221/20 brd 10.194.15.255 scope global eth0h1 $ docker run -d swarm join —addr=10.194.12.221:2375 token://$CLUSTER_ID9bf2db849bac7b33201d6d258187bd14132b74909c72912e5f135b3a4a7f4e51h1 $ docker run swarm list token://$CLUSTER_ID10.194.12.221:2375h1 $ curl https://discovery-stage.hub.docker.com/v1/clusters/$CLUSTER_ID[“10.194.12.221:2375”]
第一步是找出主机用于连接代理的IP地址,这种连接可以通过任何用户最熟悉的方式。启动代理时会使用该IP地址,并会将其信息发送给发现服务,这意味着swarm list命令输出会被更新,打印出新代理的信息。
现在还无法对节点执行任何操作——我们需要运行一个主机。因为我们要在同一台机器上作为代理运行主机,而且标准的Docker端口也被暴露出来,所以需要为主机使用其他任意一个不同的端口:
h1 $ docker run -d -p 4000:2375 swarm manage token://$CLUSTER_ID04227ba0c472000bafac8499e2b67b5f0629a80615bb8c2691c6ceda242a1dd0h1 $ docker -H tcp://localhost:4000 infoContainers: 10Strategy: spreadFilters: affinity, health, constraint, port, dependencyNodes: 1h1: 10.194.12.221:2375? Containers: 2? Reserved CPUs: 0 / 4? Reserved Memory: 0 B / 7.907 GiB
现在已经启动了主机,并运行docker info命令获取了该集群的一些详细信息。列出运行的两个容器分别是主机和代理。
现在,让我们在一个完全不同的节点上启动一个代理:
h2 $ docker run -d swarm join —addr=10.194.8.7:2375 token://$CLUSTER_IDh2 $ docker -H tcp://10.194.12.221:4000 infoContainers: 3Strategy: spreadFilters: affinity, health, constraint, port, dependencyNodes: 2h2: 10.194.8.7:2375? Containers: 1? Reserved CPUs: 0 / 4? Reserved Memory: 0 B / 3.93 GiBh1: 10.194.12.221:2375? Containers: 2? Reserved CPUs: 0 / 4? Reserved Memory: 0 B / 7.907 GiB
另一个节点已经添加到了集群里。注意,这里利用了从另一台机器访问主机的能力。
Swarm策略和过滤器 读者可能已经注意到
docker info的输出结果中以Strategy和Filters开头的几行。这里暗示了几个Swarm提供的可用的更高级的功能,但本书不会涉及。过滤器允许用户定义一系列条件,只有满足这些条件的节点才能运行容器。策略的选择则定义了Swarm如何从一些可选的节点中选择节点来启动容器。可以阅读Docker Swarm官方文档了解关于策略和过滤器的更多内容:https://docs.docker.com/swarm/scheduler/。
最后,让我们启动一个容器:
h2 $ docker -H tcp://10.194.12.221:4000 run -d ubuntu:14.04.2 sleep 600747c14774c70bad00bd7e2bcbf583d756ffe6d61459ca920887894b33734d3ah2 $ docker -H tcp://localhost:4000 psCONTAINER ID IMAGE COMMAND CREATED STATUS➥ PORTS NAMES0747c14774c7 ubuntu:14.04 sleep 60 19 seconds ago Up Less than a second➥ h1/serene_poitrash2 $ docker -H tcp://10.194.12.221:4000 info | grep ContainersContainers: 4? Containers: 1? Containers: 3
这里有几点值得注意。最重要的是,Swarm会自动选择一台机器来启动容器。用户可以从容器名(如这里的h1)看出它是在哪个节点上启动的,而其容器数量也会相应的增加。正如所见,Swarm会自动隐藏任何与Swarm相关的容器,不过用户也可以使用带-a参数的ps命令把它们列出来。
最后一步可做可不做,用户可能需要从发现服务中删除自己的集群:
h1 $ curl -X DELETE https://discovery.hub.docker.com/v1/clusters/$CLUSTER_IDOK
技巧79 使用Kubernetes集群
现在读者已经了解了容器编排的两种极端方式——比较保守的Helios方式以及更自由的Docker Swarm方式。但有些用户或者公司可能期望他们使用的工具更复杂一些。这种可定制编排的需求有很多工具可以满足,不过有些工具使用率和活跃度比其他工具要高一些。从某种角度讲,这部分原因无疑在于其背后的品牌,而用户寄希望于Google知道如何构建编排软件。
Kubernetes适合那种希望对编排应用程序和应用程序之间的状态的关系具有清晰的指引和最佳实践的公司。它允许用户使用一些专门设计的工具来管理基于特定结构的动态基础设施。
问题
想要跨宿主机管理Docker服务。
解决方案
使用Kubernetes。
讨论
在正式介绍Kubernetes的细节之前,让我们快速浏览一下图9-6所示的Kubernetes宏观架构图。
图9-6 Kubernetes高层视图
Kubernetes有一个主从架构(master-minion architecture)。主节点的职责是接收需要在集群上执行的命令,以及编排自身的资源。每个从节点上都安装了Docker,以及一个kubelet服务,kubelet用于管理每个节点上的pod(一组容器)。集群的信息交由etcd维护,etcd是一个分布式的键值数据存储(见技巧66),它是集群中信息的真实来源。
什么是pod 我们将在本技巧的稍后部分再探讨这一块,所以现在不必有过多疑问,只需要知道pod是一组相关的容器即可。这个概念是为了便于Docker容器的管理和维护。
Kubernetes的最终目标是让系统以简单可扩展的方式来运行用户容器,用户只需要声明他需要什么,让Kubernetes确保集群满足用户的需求。在本技巧中,读者将可以看到如何通过执行一条命令将一个简单的服务配置到指定的规模。
Kubernetes的起源 Kubernetes起初由Google公司开发,作为一种大规模管理容器的手段。Google公司大规模运行容器已达 10年之久,在 Docker开始流行时即决定开发这个容器编排系统。Kubernetes建立在Googlel大量的使用经验之上。Kubernetes也被简称为k8s。
关于Kubernetes安装、设置以及功能的详细介绍是一个很大且快速变化的话题,已超出本书的讨论范围(毫无疑问,不久之后将会有一本专门的书来介绍它)。这里我们将专注于Kubernetes的核心概念,并设置一个简单的服务以便读者能够对它有一个简单认知。
1.安装Kubernetes
可以直接在宿主机上安装Kubernetes,从而得到一个单从节点的集群,也可以使用Vagrant来安装一个由虚拟机管理的多从节点集群。
要在主机上安装一个单从节点集群,可以运行以下命令:
export KUBERNETES_PROVIDER=vagrantcurl -sS https://get.k8s.io | bash
获得最新的安装指南 在本书印刷时这种安装方式是正确的。访问GitHub上的文档链接可以获取Kubernetes的最新安装方式:http://mng.bz/62ZH。
如果想安装多从节点集群,还有另一个选择。可以按照Kubernetes GitHub仓库(如前面注释所述)上对 Vagrant 的说明,或者尝试我们维护的一个自动化脚本来设置两个从节点的集群(https://github.com/docker-in-practice/shutit-kubernetes-vagrant)。
如果已经安装了Kubernetes,那么不妨跟着继续。接下来的内容将是基于一个多节点的集群。我们将创建一个容器并使用Kubernets对它进行扩展。
2.扩展单个容器
用于管理Kubernetes的命令行工具叫kubectl。在下面例子中会使用它的run-container子命令,用一个给定的镜像作为容器运行在一个pod中:
$ kubectl run-container todo —image=dockerinpractice/todo (1)$ kubectl get pods | egrep “(POD|todo)” (2)POD IP CONTAINER(S) IMAGE(S) HOST➥ LABELS STATUS CREATED MESSAGEtodo-hmj8e 10.245.1.3/ (3)➥ run-container=todo Pending About a minute (4)
(1)todo是产出的pod的名称,可以通过—image标志来指定想要的镜像;这里我们用的todo镜像就是第1章里的那个
(2)kuberctl的get pods子命令列出所有的pod。我们只对todo pod感兴趣,所以使用grep过滤出这个pod
(3)todo-hmj8e是pod名
(4)LABELS是与pod有关的键值对,如这里显示的run-container标签。pod的状态是Pending(挂起),这说明Kubernetes正在准备运行这个pod,很可能是正在从Docker Hub下载镜像
Kubernetes生成pod名称是根据run-container命令中的名称(上面的例子是todo)加上破折号,然后再加上一个随机的字符串。这确保了不会和其他的pod重名。
等待几分钟下载todo镜像后,最终会看到状态变为Running(运行中):
$ kubectl get pods | egrep “(POD|todo)”POD IP CONTAINER(S) IMAGE(S)➥ HOST LABELS STATUS CREATED MESSAGEtodo-hmj8e 10.246.1.3➥ 10.245.1.3/10.245.1.3 run-container=todo Running 4 minutestodo dockerinpractice/todoRunning About a minute
这次IP、CONTAINER(S)和IMAGE(S)列都有值。IP列是pod的地址(这个例子中是10.246.1.3),CONTAINER(S)列中每一行包含了pod中的一个容器(这个例子中只有一个容器,即todo)。可以直接访问该IP地址和端口来测试todo容器确实已经在运行并且提供服务处理请求:
$ wget -qO- 10.246.1.3:8000<html manifest=”/todo.appcache”>[…]
现在我们还没看到这与直接运行一个Docker容器有什么区别。为了首次尝试Kubernetes,可以运行Kubernetes的resize命令来扩展该服务:
$ kubectl resize —replicas=3 replicationController todoresized
这一命令告诉Kubernetes我们想要todo的复制控制器(replication controller),以确保有3个todo应用程序实例运行在集群中。
什么是复制控制器 复制控制器是一个Kubernetes服务,用来确保存在正确数量的pod节点运行在集群中。
可以使用kubectl get pods命令来检查todo应用程序的已经启动的额外实例:
$ kubectl get pods | egrep “(POD|todo)”POD IP CONTAINER(S) IMAGE(S)➥ HOST LABELS STATUS CREATED MESSAGEtodo-2ip3n 10.246.2.2➥ 10.245.1.4/10.245.1.4 run-container=todo Running 10 minutestodo dockerinpractice/todo➥ Running 8 minutestodo-4os5b 10.246.1.3➥ 10.245.1.3/10.245.1.3 run-container=todo Running 2 minutestodo dockerinpractice/todo➥ Running 48 secondstodo-cuggp 10.246.2.3➥ 10.245.1.4/10.245.1.4 run-container=todo Running 2 minutestodo dockerinpractice/todo➥ Running 2 minutes
Kubernetes已经获得了resize指令和todo复制控制器,并确保启动了正确数目的pod。注意,有两个pod在同一个宿主机上(10.245.1.4),有一个pod在另一个宿主机上(10.245.1.3)。这是因为Kubernetes的默认调度程序有一个算法会默认跨节点散布pod。
什么是调度程序 调度程序是一个软件,它决定一些工作负载应该在哪里以及什么时候运行。例如,Linux内核便有一个调度程序,它会决定下一步应该运行什么任务。调度程序可以有非常简单的,也有超级复杂的。
读者已经看到了 Kubernetes使跨宿主机管理容器更加容易的方法。接下来我们将深入了解Kubernetes的pod概念。
3.使用pod
pod是一组容器,它们被设计成以某种方式在一起工作并共享资源。
每个pod拥有自己的IP地址并共享相同的卷和网络端口段。因为一个pod的所有容器共享一台本地主机,所以只要它们被部署了,依赖的不同服务都是可用和相互可见的。
图9-7用两个容器共享一个卷来演示了这一点。在该图中,容器1是一个Web服务器,从共享卷中读取数据,而容器2则会更新数据。因此两个容器都是无状态的。状态存储在在共享卷中。
图9-7 拥有两个容器的pod
这种责任分离的设计通过单独管理服务的每个部分实现了微服务的方式。升级一个镜像不必担心会影响其他镜像。
代码清单9-5中的pod规范定义了一个复杂的pod,其拥有两个容器,一个容器会每5秒在文件中写入随机数据(simple-writer),另一个容器会从同一个文件中读取数据。文件通过卷(pod-disk)共享。
代码清单9-5 complexpod.json
{“id”: “complexpod”, (1)“kind”: “Pod”, (2)“apiVersion”: “v1beta1”, (3)“desiredState”: {“manifest”: { (4)“version”: “v1beta1”,“id”: “complexpod”,“containers”: [{ (5)“name”: “simplereader”,“image”: “dockerinpractice/simplereader”, (6)“volumeMounts”: [{ (7)“mountPath”: “/data”, (8)“name”: “pod-disk” (9)}]},{“name”: “simplewriter”,“image”: “dockerinpractice/simplewriter”,“volumeMounts”: [{“mountPath”: “/data”,“name”: “pod-disk”}]}],“volumes”: [{ (10)“name”: “pod-disk”, (11)“emptydir”: {} (12)}]}}}
(1)id属性给该实体一个名称
(2)kind属性指定了这是什么类型的对象
(3)apiVersion属性指定了Kubernetes使用的JSON目标版本
(4)pod 规范的内容位于desired-State 和manifest属性中
(5)pod 中容器的细节存储在JSON 数组中
(6)每个容器都有一个名称用于引用,Docker镜像被定义在image属性中
(7)为每个容器都指定了卷的挂载点
(8)mountPath 是卷挂载到容器文件系统的路径。每个容器可以使用不同的位置
(9)卷挂载名称引用了pod manifest 的卷定义中的名称
(10)volumes属性定义了为这个pod创建的卷名称
(11)卷名称在前面volumeMounts项中被引用
(12)一个临时目录,用来共享pod的生命周期
创建一个包含上述配置的文件,运行如下命令来加载pod规范:
$ kubectl create -f complexpod.jsonpods/complexpod
等待一会儿下载完镜像后,通过运行kubectl log命令并指定第一个pod和感兴趣的容器来查看容器的日志输出:
$ kubectl log complexpod simplereader2015-08-04T21:03:36.535014550Z ‘? U[2015-08-04T21:03:41.537370907Z] h(^3eSk4y[2015-08-04T21:03:41.537370907Z] CM(@[2015-08-04T21:03:46.542871125Z] qm>5[2015-08-04T21:03:46.542871125Z] {Vv[2015-08-04T21:03:51.552111956Z] KH+74 f[2015-08-04T21:03:56.556372427Z] j?p+!\
4.接下来做什么
这里我们只是接触了Kubernetes的冰山一角,但这已经让读者清楚它能干什么以及它是怎样让编排容器变得更加简单的。在后面的OpenShift部分读者会再次看到Kubernetes,OpenShift是一个使用Kubernetes作为其编排引擎的应用程序平台即服务(见技巧87)。
技巧80 在Mesos上构建框架
当讨论众多的编排可能性时,读者可能会发现一个被特别提及作为Kubernetes的替代品的工具——Mesos。人们通常会这样描述Mesos,如“Mesos是框架的框架”,以及“Kubernetes可以运行在Mesos之上”!
我们遇到的最恰当的类比是将Mesos看作数据中心的内核。如果单独使用它办不成什么有价值的事情,将它和一个init系统还有应用程序组合在一起时就有价值了!
一个通俗的解释是,想象有一只猴子坐在面板前面控制所有的机器,并有权随意启动和停止应用程序。当然,你需要给猴子一个非常清楚的指示,在特定情况下要做什么,何时启动应用程序等。你可以自己做,但是很花时间,而猴子的劳动力比较廉价。
Mesos就是这只猴子!
Mesos是具有高度动态化和复杂基础设施的企业的理想选择,这些企业拥有自己的生产环境编排方案的经验。如果不满足这些条件的话,比起花时间量身定制Mesos,现成的解决方案可能会服务得更好一些。
问题
有很多控制应用程序和作业启动的规则,想要无须手动就能在远程机器上启动它们并跟踪其状态。
解决方案
使用Apache Mesos和一个量身定制的框架。
讨论
Mesos是一款成熟的软件,用于在多台机器上提供资源管理的抽象。一些有所耳闻的公司已经将它们部署到了生产环境并且历经考验,结果证明,它是稳定和可靠的。
要求Docker 1.6.2+ 本技巧要求Docker 1.6.2或更高版本,这样Mesos才能使用正确的Docker API版本。
图9-8展示了一个通用的生产环境Mesos配置。
图9-8 通用的生产环境Mesos配置
参考图9-8可以看到Mesos启动一个任务的基本生命周期是怎样的。
(1)一个从节点运行在节点上,追踪资源利用率并持续接收主节点的通知。
(2)主节点从一个或多个从节点上收集可用的资源,并向调度程序提供资源。
(3)调度程序接收主节点提供的资源,决定在哪里运行任务,并将消息通知回主节点。
(4)主节点将任务信息传递给适当的从节点。
(5)每个从节点将任务信息传递给节点上现有的执行器,或启动一个新的执行器。
(6)执行器读取任务信息并在节点上启动任务。
(7)任务运行。
Mesos项目提供了主节点(master)和从节点(slave),还有内置的shell执行器。你的工作是提供一个框架(或应用程序),它是由一个调度程序(猴子例子中的指令列表)和可选的自定义执行器组成。
许多第三方项目提供了使用户可以运行在Mesos上的框架(我们将在下一个技巧中详细介绍),但为了更好地了解如何充分利用Mesos和Docker的力量,我们将构建自己的框架,该框架只包含一个调度程序。如果启动应用程序有非常复杂的逻辑,这可能也会是最终的途径。
与Mesos结合使用Docker的本质 与Mesos结合使用Docker并不是必须的,但由于这是本书的内容,我们会这样做。因为Mesos非常灵活,所以我们不会深入细节。我们也会在单台机器上运行Mesos,但我们会尝试尽可能地保持其真实性,并指出需要怎么做才能上线。
我们还没有解释 Docker 是如何适配 Mesos 的生命周期的——该谜题的最后一部分便是Mesos提供了对容器化支持,允许用户隔离执行器或任务(或两者同时隔离)。在这里,Docker并不是唯一可用的工具,但是它非常受欢迎,所以Mesos有一些特定于Docker的功能支持,可以让用户快速上手。
我们的示例只会容器化我们运行的任务,因为我们使用的是默认执行器。如果用户有自定义的执行器只能运行在一个特定的语言环境,每个任务需要动态加载和执行一些代码,那么可能需要考虑容器化执行器。作为一个示例用例,用户可能会把JVM用作执行器来实时加载和执行代码,从而避免当执行潜在的非常小的任务时JVM的启动开销。
图9-9展示了在创建新的Docker化任务时,我们当前示例里它的后台会发生什么。
图9-9 单宿主机Mesos设置启动一个容器
话不多说,让我们开始吧!接下来首先要做的是通过代码清单9-6启动一个主节点。
代码清单9-6 启动主节点
$ docker run -d —name mesmaster redjack/mesos:0.21.0 mesos-master \—work_dir=/opt24e277601260dcc6df35dc20a32a81f0336ae49531c46c2c8db84fe99ac1da35$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ mesmaster172.17.0.2$ docker logs -f mesmasterI0312 01:43:59.182916 1 main.cpp:167] Build: 2014-11-22 05:29:57 by rootI0312 01:43:59.183073 1 main.cpp:169] Version: 0.21.0I0312 01:43:59.183084 1 main.cpp:172] Git tag: 0.21.0[…]
主节点的启动有点儿冗长,但是用户应该能发现它很快就停止了日志记录。保持该终端开启,以便在启动其他容器时可以看到主节点发生了什么。
多主节点的Mesos设置 通常Mesos将会配置成具有多个Mesos主节点(一个归档和多个备份),以及一个Zookeeper集群。Mesos站点的“Mesos High-Availability Mode”(Mesos高可用模式)页面(http://mesos.apache.org/documentation/latest/high-availability)记录了如何配置多主节点的Mesos。用户还需要暴露端口5050用于外部通信,并且使用work_dir文件夹作为卷来保存持久化信息。
我们也需要从节点。但是这需要一些技巧。Mesos的定义特征之一就是对执行任务有资源限制的能力,这要求从节点拥有自由检查和管理进程的能力。因此,运行从节点的命令需要将一些外部系统的细节暴露到容器内部,如代码清单9-7所示。
代码清单9-7 启动从节点
$ docker run -d —name messlave —pid=host \-v /var/run/docker.sock:/var/run/docker.sock -v /sys:/sys \redjack/mesos:0.21.0 mesos-slave \—master=172.17.0.2:5050 —executor_registration_timeout=5mins \—isolation=cgroups/cpu,cgroups/mem —containerizers=docker,mesos \—resources=”ports():[8000-8100]”1b88c414527f63e24241691a96e3e3251fbb24996f3bfba3ebba91d7a541a9f5$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ messlave172.17.0.3$ docker logs -f messlaveI0312 01:46:43.341621 32398 main.cpp:142] Build: 2014-11-22 05:29:57 by rootI0312 01:46:43.341789 32398 main.cpp:144] Version: 0.21.0I0312 01:46:43.341795 32398 main.cpp:147] Git tag: 0.21.0[…]I0312 01:46:43.554498 32429 slave.cpp:627] No credentials provided.➥ Attempting to register without authenticationI0312 01:46:43.554633 32429 slave.cpp:638] Detecting new masterI0312 01:46:44.419646 32424 slave.cpp:756] Registered with master➥ master@172.17.0.2:5050; given slave ID 20150312-014359-33558956-5050-1-S0[…]
此刻,用户应该可以看到Mesos主节点终端的一些活动,开始几行如下所示:
I0312 01:46:44.332494 9 master.cpp:3068] Registering slave at➥ slave(1)@172.17.0.3:5051 (8c6c63023050) with id➥ 20150312-014359-33558956-5050-1-S0I0312 01:46:44.333772 8 registrar.cpp:445] Applied 1 operations in➥ 134310ns; attempting to update the ‘registry’
这两行日志展示了用户已经启动了从节点并连接到了主节点。如果没有看到日志的话,不妨停下来再次检查配置的主节点的IP地址。当没有可连接的从节点时,尝试和调试为什么框架没有启动任何任务是非常令人沮丧的。
不管怎样,代码清单9-7中的命令做了很多事情。run之后和redjack/mesos:0.21.0之前的部分传入的参数都是Docker的参数,它们主要包含了给从节点容器传入的很多关于外部世界的信息。mesos-slave之后的参数更有意思。master告诉从节点在哪里能找到主节点(或者Zookeeper集群)。接下来的3个参数executor_registration_timeout、isolation和containerizers都是对Mesos设置的调整,使用Docker时要始终设置这3个参数。最后,也是相当重要的一点是,需要让Mesos从节点知道哪些端口可以作为资源交出来。默认情况下,Mesos提供31000~32000,但我们想使用更小的更容易记忆的端口。
现在简单的步骤都已经完成,我们进行到了设置Mesos的最后阶段——创建调度程序。
幸好我们已经有了一个示例框架可以使用。我们来试试它能做什么,然后探索它的工作原理。用户不妨在主节点容器和从节点容器中保持docker logs –f命令窗口是打开的,以便可以看到通信。
代码清单9-8中给出的命令将从GitHub获取示例框架的源代码库并启动它。
代码清单9-8 下载和启动示例框架
$ git clone https://github.com/docker-in-practice/mesos-nc.git$ docker run -it —rm -v $(pwd)/mesos-nc:/opt redjack/mesos:0.21.0 bash# apt-get update && apt-get install -y python# cd /opt# export PYTHONUSERBASE=/usr/local# python myframework.py 172.17.0.2:5050I0312 02:11:07.642227 182 sched.cpp:137] Version: 0.21.0I0312 02:11:07.645598 176 sched.cpp:234] New master detected at➥ master@172.17.0.2:5050I0312 02:11:07.645800 176 sched.cpp:242] No credentials provided.➥ Attempting to register without authenticationI0312 02:11:07.648449 176 sched.cpp:408] Framework registered with➥ 20150312-014359-33558956-5050-1-0000Registered with framework ID 20150312-014359-33558956-5050-1-0000Received offer 20150312-014359-33558956-5050-1-O0. cpus: 4.0, mem: 6686.0,➥ ports: 8000-8100Creating task 0Task 0 is in state TASK_RUNNING[…]Received offer 20150312-014359-33558956-5050-1-O5. cpus: 3.5, mem: 6586.0,➥ ports: 8005-8100Creating task 5Task 5 is in state TASK_RUNNINGReceived offer 20150312-014359-33558956-5050-1-O6. cpus: 3.4, mem: 6566.0,➥ ports: 8006-8100Declining offer
读者可能会注意到,我们已经把Git仓库挂载到了Mesos镜像中。这是因为它包含了我们需要的所有Mesos库。不然,安装它们会是一个很痛苦的过程。
mesos-nc框架被设计用来在所有可用的宿主机上的8000和8005之间的所有可用端口之间运行echo ‘hello <task id>’ | nc -l <port>命令。由于netcat的工作原理,这些“服务器”在用户访问它们时就会终止,无论是通过curl、Telnet、nc还是浏览器来访问。可以在新的终端中运行curl localhost:8003来进行验证。它将会返回预期的响应,并且Mesos的日志会显示新产生的任务替代了被终止的那个。用户还可以使用docker ps来跟踪哪些任务正在运行。
值得一提的是,这里有Mesos保持跟踪已分配的资源并在任务终止时将它标记为可用的证据。特别是,当访问localhost:8003(不妨再次尝试下)时,请仔细查看收到的日志行,它展示了两个端口范围(因为它们没有被连接),包括刚刚释放的端口范围:
Received offer 20150312-014359-33558956-5050-1-O45. cpus: 3.5, mem: 6586.0,➥ ports: 8006-8100,8003-8003
Mesos从节点命名冲突 Mesos从节点给所有容器的命名均是以mesos-开头的,并且它假定这样任意的名称可以安全地被从节点管理。留心容器的命名,否则Mesos从节点可能会杀掉它。
框架代码(myframework.py)加上了很好的注释,以便用户能够读懂。我们将介绍一些宏观的设计:
class TestScheduler(mesos.interface.Scheduler):[…]def registered(self, driver, frameworkId, masterInfo):[…]def statusUpdate(self, driver, update):[…]def resourceOffers(self, driver, offers):[…]
所有的Mesos调度程序都是Mesos调度程序类的子类,并且实现了一些方法,Mesos将会在适当的时间点通知用户运行的框架,使其对相应的事件做出响应。尽管上面的代码段中已经实现了3个方法,但其中的两个是可选的,它们的实现是用来添加额外的日志来进行演示的。唯一一个用户必须实现的方法是resourceOffers——框架在启动任务时没有太多不清楚的点。用户可以为了自己的目的添加任意额外的方法,如init和_makeTask,只要它们不和Mesos期望使用的任何方法冲突就好,因此,请读者先确保阅读过相关文档(http://mesos.apache.org/ documentation/latest/app-framework-development-guide/)。
构建自己的框架? 如果用户最终选择编写自己的框架,那么需要查看一些方法和结构的文档。但是在编写本书时,官方唯一产出的文档是针对Java方法的。想要找到深入结构的起点的读者,可以从查看Mesos源代码中的include/mesos/mesos.proto文件开始。祝你好运!
我们来看一下main方法中一个有意思的部分——resourceOffers的更多细节。它会决定启动任务还是拒绝任务。图9-10展示了Mesos调用了框架的resourceOffers方法后的执行流程(通常是因为一些资源已经可供框架使用)。
图9-10 调用resourceOffers时框架的执行流程
resourceOffers会接收一个offer列表,每个offer对应单个Mesos从节点。该offer包含了在从节点上运行的任务的可供使用的资源的细节,而该方法的一个典型实现将会使用该信息来识别最适合的位置来启动想要运行的任务。启动任务会给Mesos主节点发送消息,主节点会继续图9-9中列出的生命周期。
重要的是注意这里的灵活性——任务的启动取决于选定的任意标准,从外部服务的健康检查乃至于月相都行!这种灵活性可能是一种负担,所以业内现有的已发布的框架会隐藏这一底层的细节并简化对Mesos的使用。接下来的技巧会讲述其中一个框架。
读者可以阅读Roger Ignazio的《Mesos in Action》来了解Mesos的功能的更多细节——这里只进行了一些简单的介绍,而我们已经看到了它和Docker的配合是多么轻松。
技巧81 使用Marathon细粒度管理Mesos
现在读者应该已经意识到了,使用Mesos需要考虑很多细节,即便是对一个极其简单的框架也是如此。能够信赖被正确部署的应用程序这一点非常重要——框架中的bug造成的影响可能会导致部署新应用程序失败,也可能会导致整个服务中断。
随着集群规模的扩展,风险也在上升,除非团队擅长于编写可靠的动态部署代码,否则可能需要考虑更多经过验证的方法——Mesos自身是很稳定的,但内部定制的框架可能不是人们想象的那么可靠。
Marathon适用于那些没有内部部署工具开发经验但需要一个良好支持而且易于使用的方案,以便在有些动态的环境中部署容器的公司。
问题
需要一种可靠的方式来利用Mesos的力量,而不会陷入编写自己的框架的困扰。
解决方案
使用Marathon。
讨论
Marathon是一款Apache Mesos框架,它是由Mesosphere构建的、用于管理长期运行的应用程序。市场资料将其描述为数据中心(Mesos是其核心)的init或upstart守护进程。这个比喻并非没有道理。
Marathon可以让用户启动一个包含了Mesos主节点、Mesos从节点和Marathon自身的容器来作为简单的快速上手。这对于演示很有用,但不适用于生产环境下的Marathon部署。要配置一个真实环境的Marathon,用户需要一个Mesos主节点和从节点(来自之前的技巧)以及一个Zookeeper实例(出自技巧77)。确保这些都在运行之后,我们将开始运行Marathon容器:
$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ mesmaster172.17.0.2$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ messlave172.17.0.3$ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ zookeeper172.17.0.4$ docker pull mesosphere/marathon:v0.8.2[…]$ docker run -d -h $(hostname) —name marathon -p 8080:8080 \mesosphere/marathon:v0.8.2 —master 172.17.0.2:5050 —local_port_min 8000 \—local_port_max 8100 —zk zk://172.17.0.4:2181/marathonaccd6de46cfab65572539ccffa5c2303009be7ec7dbfb49e3ab8f447453f2b93$ docker logs -f marathonMESOS_NATIVE_JAVA_LIBRARY is not set. Searching in /usr/lib /usr/local/lib.MESOS_NATIVE_LIBRARY, MESOS_NATIVE_JAVA_LIBRARY set to➥ ‘/usr/lib/libmesos.so’[2015-06-23 19:42:14,836] INFO Starting Marathon 0.8.2➥ (mesosphere.marathon.Main$:87)[2015-06-23 19:42:16,270] INFO Connecting to Zookeeper…➥ (mesosphere.marathon.Main$:37)[…][2015-06-30 18:20:07,971] INFO started processing 1 offers,➥ launching at most 1 tasks per offer and 1000 tasks in total➥ (mesosphere.marathon.tasks.IterativeOfferMatcher$:124)[2015-06-30 18:20:07,972] INFO Launched 0 tasks on 0 offers,➥ declining 1 (mesosphere.marathon.tasks.IterativeOfferMatcher$:216)
就像Mesos一样,Marathon非常啰嗦,不过(也像Mesos)它也会很快停下来。此刻,我们将从编写自己框架那部分内容进入一个很熟悉的环节——考虑资源供应并决定如何用这些资源做些什么。因为还没有启动任何东西,所以自从前面的日志的declining 1后我们便看不到任何活动。
Marathon有一个漂亮的Web界面,这也是要在宿主机上暴露8080端口——在浏览器中访问http://localhost:8080端口来打开页面的原因。
我们直接切换到Marathon的具体操作部分,先创建一个新的应用程序。这里有一些术语要澄清一下——在Marathon的世界里“应用程序”(app)是拥有完全相同定义的一个或多个任务的集合。
点击右上角的App New(新建应用程序)按钮,会弹出一个对话框,可以用它来定义要启动的应用程序。我们将继续沿用自己创建的框架,设置ID为marathon-nc,设置CPU、内存和磁盘空间为默认值(以符合mesos-nc框架的资源限制),并且设置启动命令为echo “hello $MESOS_TASK_ID” | nc -l $PORT0(使用该任务可用的环境变量,注意就是数字0)。将端口字段值设置为8000,指定我们想要监听的位置。随即,跳过其他的字段设置。点击Create(创建)。
用户新定义的应用程序现在将显示在Web界面上。状态会先简要显示为Deploying,然后变为Running。应用程序现在已经启动了!
如果点击应用程序列表中的/marathon-nc条目,将会看到该应用程序的唯一ID。通过REST API可以得到完整的配置,如下面的代码所示,也可以通过对Mesos从节点容器对应的端口运行curl命令来验证它在运行。用户需要确保保存了REST API返回的完整的配置,因为稍后会派上用场——它被保存在下面例子中的app.json中:
$ curl http://localhost:8080/v2/apps/marathon-nc/versions{“versions”:[“2015-06-30T19:52:44.649Z”]}$ curl -s \http://localhost:8080/v2/apps/marathon-nc/versions/2015-06-30T19:52:44.649Z \> app.json$ cat app.json{“id”:”/marathon-nc”,➥ “cmd”:”echo \”hello $MESOS_TASK_ID\” | nc -l $PORT0”,[…]$ curl http://172.17.0.3:8000hello marathon-nc.f56f140e-19e9-11e5-a44d-0242ac110012
留意一下对应用程序执行curl命令的输出结果中hello后面的文本——它应该和界面中的唯一ID是匹配的。检查要快速,因为运行curl命令会终止该应用程序,Marathon会重新启动它,界面中的唯一ID会改变。一旦验证了这些,继续点击Destroy App(销毁应用程序)按钮来删除marathon-nc。
一切工作正常,但是读者可能已经注意到,我们没有达成使用Marathon的目的——编排Docker容器。尽管应用程序在容器中,但它在Mesos从节点容器中启动,而不是在自己的容器中启动。阅读Marathon文档说明,在Docker容器中创建任务还需要做更多的配置(就像编写自己的框架时一样)。
幸好,之前启动的Mesos从节点都有所需的设置,所以只需修改一些Marathon选项——特别是应用程序方面的选项。通过获取之前Marathon API的响应信息(存放在app.json中),我们可以专注于添加Marathon的设置信息从而启用Docker。我们将使用jq工具执行操作,尽管通过文本编辑器来做也同样简单:
$ JQ=https://github.com/stedolan/jq/releases/download/jq-1.3/jq-linux-x86_64$ curl -Os $JQ && mv jq-linux-x86_64 jq && chmod +x jq$ cat >container.json <<EOF{“container”: {“type”: “DOCKER”,“docker”: {“image”: “ubuntu:14.04.2”,“network”: “BRIDGE”,“portMappings”: [{“hostPort”: 8000, “containerPort”: 8000}]} }}$ # merge the app and container details$ cat app.json container.json | ./jq -s add > newapp.json
现在,我们可以将新的应用程序定义发送给API然后见证Marathon启动它:
$ curl -X POST -H ‘Content-Type: application/json; charset=utf-8’ \—data-binary @newapp.json http://localhost:8080/v2/apps{“id”:”/marathon-nc”,➥ “cmd”:”echo \”hello $MESOS_TASK_ID\” | nc -l $PORT0”,[…]$ sleep 10$ docker ps —since=marathonCONTAINER ID IMAGE COMMAND CREATED➥ STATUS PORTS NAMES284ced88246c ubuntu:14.04 “\”/bin/sh -c ‘echo About a minute ago➥ Up About a minute 0.0.0.0:8000->8000/tcp mesos-➥ 1da85151-59c0-4469-9c50-2bfc34f1a987$ curl localhost:8000hello mesos-nc.675b2dc9-1f88-11e5-bc4d-0242ac11000e$ docker ps —since=marathonCONTAINER ID IMAGE COMMAND CREATED➥ STATUS PORTS NAMES851279a9292f ubuntu:14.04 “\”/bin/sh -c ‘echo 44 seconds ago➥ Up 43 seconds 0.0.0.0:8000->8000/tcp mesos-➥ 37d84e5e-3908-405b-aa04-9524b59ba4f6284ced88246c ubuntu:14.04 “\”/bin/sh -c ‘echo 24 minutes ago➥ Exited (0) 45 seconds ago➥ mesos-1da85151-59c0-4469-9c50-2bfc34f1a987
和我们的自定义框架一样,Mesos 已经启动了一个Docker容器,应用程序就运行在里面。运行curl命令会终止应用程序和容器,然后自动启动一个新的。
这些框架之间有一些显著的差异。例如,在一个自定义框架里,我们可以针对资源供给的接受进行非常细粒度的控制,我们可以选择和征用单个要监听的端口。为了在Marathon中也能做类似的事情,则需要给每一个从节点强加一些额外的设置。
相比之下,Marathon拥有很多内置的功能,包括健康检查、事件通知系统和REST API。这并不是微不足道的实现细节,使用 Marathon 可以确保的一点是在操作它时你并不是第一个吃螃蟹的人。如果没别的需求,获取Marathon的支持比定制框架要容易得多。我们发现Marathon的文档比Mesos的更加通俗易懂。
我们已经介绍了设置和使用 Marathon 的一些基础知识,但是这里面还有更多的事情要做。我们看到的更有趣的一个建议便是使用Marathon启动其他的 Mesos 框架,可能包括你自己的定制框架!我们鼓励读者去积极探索——Mesos 是一个专注于编排领域的高品质工具,而Marathon在其上提供了一个可用的应用层。
9.3 服务发现:我们有什么
本节将介绍的服务发现即是编排的另一面。能够将应用程序部署到数百台不同的机器当然很棒,但如果不知道哪些应用程序位于哪里,就无法实际使用它们。
虽然服务发现领域不如编排领域那么饱和,但是这一领域也有一些竞争对手。不过,他们的功能集只是略有不同。
服务发现通常需要两个功能:一个通用的键值存储和通过一些方便的接口(就像 DNS)检索服务终端的方法。etcd和Zookeeper是前者的例子,而SkyDNS(这一工具我们将不会有所涉及)就是后者的例子。事实上,SkyDNS正是使用etcd来存储所需信息的。
技巧82 使用Consul来发现服务
etcd是一款非常流行的工具,但它有一个特别的竞争者,谈论它时总会被提及,那就是Consul。这有点儿奇怪,因为业内有其他更像etcd的工具(Zookeeper具有与etcd相似的功能,但是用不同的语言实现的),而Consul通过一些有意思的附加功能与其区别开来,如服务发现和健康检查。
事实上如果仔细看,Consul有点儿像是etcd、SkyDNS、Nagios的整个打包。
问题
想要能分发消息给一组容器、在一组容器中发现服务以及监控一组容器。
解决方案
在每台Docker宿主机上启动带有Consul的容器,从而提供服务目录和配置通信系统。
讨论
当用户需要协同一些独立的服务时,Consul试图成为用于完成一些重要任务的通用工具。其他工具当然也可以完成这些任务,但 Consul 提供了统一的配置界面,这对用户而言非常方便。Consul从宏观来说提供了以下功能:
- 服务配置——用于存储和共享小值的键值存储,类似于etcd和Zookeeper;
- 服务发现——用于注册服务的API和用于发现服务的DNS端点,就像SkyDNS;
- 服务监控——用于注册健康检查的API,就像Nagios。
用户可以使用这里面的全部或者部分功能,因为它们之间没有任何联系。如果用户已经有了监控应用服务的基础设施,则无须替换成Consul。
本技巧会覆盖Consul的服务发现和服务监控部分,但是不包括键值存储。在熟读了Consul的文档之后,etcd和Consul之间的强相似性也使第7章的最后两个技巧是可以相互转换的。
图9-11展示了典型的Consul设置。
图9-11 一个典型的Consul设置
存储在 Consul 中的数据由服务器代理负责。它们负责对存储的信息达成共识——这个概念存在于大多数分布式数据存储系统中。简而言之,如果丢失了少于一半的服务器代理,该分布式系统将仍然能够确保数据是可恢复的(见技巧66中与etcd相关的示例)。因为这些服务器代理很重要,而且有更高的资源要求,所以将其部署到专用的机器上是典型的选择。
保留数据 虽然本技巧中的命令会将Consul数据目录(/data)保存在容器中,但通常至少针对服务器而言,最好将此目录指定为卷,从而可以保留备份。
这里建议用户控制的所有可能要与Consul交互的机器都应该运行客户端代理。这些代理会将请求转发给服务器并运行健康检查。
让Consul运行的第一步是启动服务器代理:
c1 $ IMG=dockerinpractice/consul-serverc1 $ docker pull $IMG[…]c1 $ ip addr | grep ‘inet ‘ | grep -v ‘lo$|docker0$|vbox.$’inet 192.168.1.87/24 brd 192.168.1.255 scope global wlan0c1 $ EXTIP1=192.168.1.87c1 $ echo ‘{“ports”: {“dns”: 53}}’ > dns.jsonc1 $ docker run -d —name consul —net host \-v $(pwd)/dns.json:/config/dns.json $IMG -bind $EXTIP1 -client $EXTIP1 \-recursor 8.8.8.8 -recursor 8.8.4.4 -bootstrap-expect 188d5cb48b8b1ef9ada754f97f024a9ba691279e1a863fa95fa196539555310c1c1 $ docker logs consul[…]Client Addr: 192.168.1.87 (HTTP: 8500, HTTPS: -1, DNS: 53, RPC: 8400)Cluster Addr: 192.168.1.87 (LAN: 8301, WAN: 8302)[…]==> Log data will now stream in as it occurs:2015/08/14 12:35:41 [INFO] serf: EventMemberJoin: mylaptop 192.168.1.87[…]2015/08/14 12:35:43 [INFO] consul: member ‘mylaptop’ joined, marking➥ health alive2015/08/14 12:35:43 [INFO] agent: Synced service ‘consul’
由于我们想要把 Consul 当作一台 DNS 服务器使用,因此,我们将一个文件放到了 Consul读取配置的文件夹里,使Consul监听53端口(DNS协议的注册端口)。然后,我们使用在之前的技巧中学到的一个命令序列来尝试查找机器的面向外部的 IP 地址,以便与其他代理进行通信并监听客户端请求。
DNS端口冲突 IP地址
0.0.0.0通常用于指示应用程序应该监听机器上的所有可用接口。我们故意没有这样做,因为一些Linux发行版有一个DNS缓存守护进程,监听127.0.0.1,它不允许监听0.0.0.0:53。
在前面的docker run命令里有以下3个注意事项。
- 使用了
—net host。虽然这可以被视为 Docker 世界的一个人造天地,但是另外一种选择是在命令行上暴露8个端口——这是个人偏好的问题,但是我们认为这是合理的。它还有助于绕过UDP通信的潜在问题。如果是手动设置的路由,则不需要设置DNS端口——可以将默认的Consul DNS端口(8600)作为端口53暴露在宿主机。 - 两个
recursor参数告诉Consul,如果consul本身不知道请求的地址,可以通过哪些DNS服务器查看。 -bootstrap-expect 1参数表明Consul集群启动运转之初只需要运行一个代理即可,这不是很健壮。一个典型的设置是将数量设置为3(或更多),以确保直到所需数量的服务器已加入后,集群才会启动。要启动其他服务器代理,可以添加一个-join参数,我们将在启动客户端时讨论。
现在让我们配置第二台机器,启动客户端代理,并将其添加到集群中。
这里有坑 由于Consul与其他代理通信时期望能够监听到一组特别的端口,这样一来在单个机器上配置多个代理用来演示现实世界中 Consul 的工作方式,就会有点儿困难。现在,我们将使用不同的宿主机——如果决定使用IP别名,一定要确保带上了
-node newAgent参数,因为Consul默认会使用主机名,而这会引起冲突。
c2 $ IMG=dockerinpractice/consul-agentc2 $ docker pull $IMG[…]c2 $ EXTIP1=192.168.1.87c2 $ ip addr | grep docker0 | grep inetinet 172.17.42.1/16 scope global docker0c2 $ BRIDGEIP=172.17.42.1c2 $ ip addr | grep ‘inet ‘ | grep -v ‘lo$|docker0$’inet 192.168.1.80/24 brd 192.168.1.255 scope global wlan0c2 $ EXTIP2=192.168.1.80c2 $ echo ‘{“ports”: {“dns”: 53}}’ > dns.jsonc2 $ docker run -d —name consul-client —net host \-v $(pwd)/dns.json:/config/dns.json $IMG -client $BRIDGEIP -bind $EXTIP2 \-join $EXTIP1 -recursor 8.8.8.8 -recursor 8.8.4.45454029b139cd28e8500922d1167286f7e4fb4b7220985ac932f8fd5b1cdef25c2 $ docker logs consul-client[…]2015/08/14 19:40:20 [INFO] serf: EventMemberJoin: mylaptop2 192.168.1.80[…]2015/08/14 13:24:37 [INFO] consul: adding server mylaptop➥ (Addr: 192.168.1.87:8300) (DC: dc1)
反馈信息 我们使用的镜像是基于gliderlabs/consul-server:0.5和gliderlabs/consul-agent:0.5的,并且它们附带一个较新版本的Consul,以避免UDP通信的可能的问题,这可以通过不断记录的日志行(如“Refuting a suspect message.”)获得提示。在镜像的0.6版本发布后,用户可以切换回gliderlabs中的镜像。
所有的客户端服务(HTTP、DNS等)都已配置为监听Docker网桥IP地址。这为容器提供了一个周知的位置来获取Consul的信息,而它只能在机器的内部公开Consul,这迫使其他机器直接访问服务器代理,而不是由客户端代理再到服务器代理这样一条更慢的路径。为了确保所有宿主机的网桥IP地址一致,可以查看Docker守护进程的—bip参数——在技巧70中配置Resolvable时可能会熟悉该指令。
跟之前一样,我们已经找到了外部的IP地址并且将集群的通信绑定到了上面。-join参数告诉Consul以哪里为起点寻找集群。用户无须担心集群信息的管理细节——当两个代理最初相遇时,它们会协同交流(gossip),传递在集群中发现的其他代理信息。最后的-recursor参数告诉Consul,在处理本意并非是查找已注册服务的DNS请求时应该使用上游的哪些DNS服务器。
我们来验证代理是否已经通过客户端机器上的HTTP API连接到服务器。我们将用到的API调用会返回客户端代理程序当前认为在集群中的成员列表(在很大的、快速变化的集群中,这可能并不总是与集群的成员相匹配——针对这一点,这里还有一个更慢的API调用可供使用):
c2 $ curl -sSL $BRIDGEIP:8500/v1/agent/members | tr ‘,’ ‘\n’ | grep Name[{“Name”:”mylaptop2”{“Name”:”mylaptop”
现在 Consul 的基础设施已经建立起来了,是时候查看如何注册和发现服务。典型的注册过程是让应用程序初始化后对本地客户端代理进行API的调用,提醒客户端代理将信息分发给服务器代理。出于演示的目的,我们手动执行这一注册步骤:
c2 $ docker run -d —name files -p 8000:80 ubuntu:14.04.2 \python3 -m http.server 8096ee81148154a75bc5c8a83e3b3d11b73d738417974eed4e019b26027787e9d1c2 $ docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ files172.17.0.16c2 $ /bin/echo -e ‘GET / HTTP/1.0\r\n\r\n’ | nc -i1 172.17.0.16 80 \| head -n 1HTTP/1.0 200 OKc2 $ curl -X PUT —data-binary ‘{“Name”: “files”, “Port”: 8000}’ \$BRIDGEIP:8500/v1/agent/service/registerc2 $ docker logs consul-client | tail -n 12015/08/15 03:44:30 [INFO] agent: Synced service ‘files’
这里我们在容器中设置了一个简单的HTTP服务器,将其暴露在宿主机的8000端口,并检查其是否工作。然后使用curl和Consul的HTTP API来注册服务定义。这里唯一一件必不可少的事情便是给服务配一个名字——端口和Consul文档中列出的其他字段都是可选的。ID字段值得一提,它默认便是服务的名称,但在所有服务中必须是唯一的。如果想要同一个服务的多个实例,就需要指定它。
Consul的日志行告诉我们服务已经同步,所以我们应该可以通过服务的DNS接口来检索服务的信息。该信息来自服务器代理,所以它可以作为验证该服务已经被Consul目录接受的依据。可以使用dig命令查询服务DNS信息并检查它是否出现:
c2 $ EXTIP1=192.168.1.87c2 $ dig @$EXTIP1 files.service.consul +short (1)192.168.1.80c2 $ BRIDGEIP=172.17.42.1c2 $ dig @$BRIDGEIP files.service.consul +short (2)192.168.1.80c2 $ dig @$BRIDGEIP files.service.consul srv +short (3)1 1 8000 mylaptop2.node.dc1.consul.c2 $ docker run -it —dns $BRIDGEIP ubuntu:14.04.2 bash (4)root@934e9c26bc7e:/# ping -c1 -q www.google.com (5)PING www.google.com (216.58.210.4) 56(84) bytes of data.—- www.google.com ping statistics —-1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min/avg/max/mdev = 25.358/25.358/25.358/0.000 msroot@934e9c26bc7e:/# ping -c1 -q files.service.consul (6)PING files.service.consul (192.168.1.80) 56(84) bytes of data.—- files.service.consul ping statistics —-1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min/avg/max/mdev = 0.062/0.062/0.062/0.000 ms
(1)通过服务端代理DNS查找files服务的IP 地址。该 DNS 服务对于任何不在Consul 集群的机器都是可用的,允许它们享用服务发现的好处
(2)从客户端代理DNS查找files服务的 IP 地址。如果使用\$BRIDGEIP失败,可能希望尝试使用\$EXTIP1
(3)从客户端代理DNS请求files服务的SRV记录。SRV记录是通过DNS交流服务信息的方式,包括协议,端口和其他条目。值得注意的两条是,用户可以在响应中看到端口号,并且用户已经被授予提供服务的机器的规范主机名而非IP地址
(4)启动一个使用本地客户端代理作为唯一DNS服务器的容器。如果用户自己对之前提到的关于Resolvable的技巧(技巧70)还熟悉的话,不妨重新试下,将其设置为所有容器的默认值。别忘了要覆盖Consul代理的默认值,否则可能会导致意外的行为
(5)验证外部地址的查找是否依然有效
(6)验证服务查找是否在容器内仍然自动工作
Resolvable和Consul DNS服务的类似之处实在令人震惊。它们之间的关键区别在于Consul可以跨多个节点找到容器。然而,正如本技巧开始时提到的那样,Consul还有一个我们将会了解到的有趣特性,即健康检查。
健康检查是一个大的课题,如果想要了解细节请查看Consul的综合文档,这里我们关注监控的其中一个选项,即脚本检查。配置该选项将会运行一个命令,并根据返回值设置健康状态,0代表成功,1代表警告,其他值代表致命。可以在初始化服务时就注册健康检查,也可以通过单独的API调用中进行注册,就像这里做的一样:
c2 $ cat >check <<’EOF’ (1)#!/bin/shset -o errexitset -o pipefailSVC_ID=”$1”SVC_PORT=\“$(wget -qO - 172.17.42.1:8500/v1/agent/services | jq “.$SVC_ID.Port”)”wget -qsO - “localhost:$SVC_PORT”echo “Success!”EOFc2 $ cat check | docker exec -i consul-client sh -c \‘cat > /check && chmod +x /check’ (2)c2 $ cat >health.json <<’EOF’ (3){“Name”: “filescheck”,“ServiceID”: “files”,“Script”: “/check files”,“Interval”: “10s”}EOFc2 $ curl -X PUT —data-binary @health.json \172.17.42.1:8500/v1/agent/check/register (4)c2 $ sleep 300 (5)c2 $ curl -sSL 172.17.42.1:8500/v1/health/service/files | \python -m json.tool | head -n 13 (6)[{“Checks”: [{“CheckID”: “filescheck”,“Name”: “filescheck”,“Node”: “mylaptop2”,“Notes”: “”,“Output”: “/check: line 6: jq: not \found\nConnecting to 172.17.42.1:8500 (172.17.42.1:8500)\n”,“ServiceID”: “files”,“ServiceName”: “files”,“Status”: “critical”} ,c2 $ dig @$BRIDGEIP files.service.consul srv +short (7)c2 $
(1)创建检查脚本,验证服务中的HTTP状态代码是否为200 OK。将服务ID作为参数传递给脚本用来查找服务端口
(2)复制检查脚本到Consul代理容器
(3)创建一个健康检查的定义,发送给Consul HTTP API。在ServiceID字段和脚本命令行中指定服务ID
(4)提交健康检查JSON给Consul代理
(5)等待与服务器代理通信后的检查输出
(6)从注册的检查中获取健康检查信息
(7)试图查找files服务,结果为空
避免检查状态的分流 因为健康检查的输出在每次执行时会改变(例如,它包括时间戳),Consul只会在服务器状态更改或每5分钟(尽管这个时间间隔是可配置的)同步检查输出。由于状态开始时是
critical,因此在这种情况下没有初始状态的更改,也就需要等待一个时间间隔才能获得输出。
我们为files服务添加了每10秒运行一次的健康检查,但是检查显示服务为critical状态。因此,Consul 自动将失败的端点从DNS返回的条目中删除,我们也就没有服务器可用。这对于在生产环境中自动从多后端服务中删除服务器特别有用。
我们曾经遇到的一个错误的根本原因,也就是在容器内运行 Consul 时需要注意的一个很重要的点。所有检查也都在容器中运行,因此必须将检查脚本复制到容器中,还需要确保需要的任何命令都已安装在容器中。在当前特定场景下,我们缺少jq命令(用于从JSON中提取信息的实用程序)。我们可以手动安装,但是在生产环境里正确的方法是在镜像中添加层:
c2 $ docker exec consul-client sh -c ‘apk update && apk add jq’fetch http://dl-4.alpinelinux.org/alpine/v3.2/main/x86_64/APKINDEX.tar.gzv3.2.3 [http://dl-4.alpinelinux.org/alpine/v3.2/main]OK: 5289 distinct packages available(1/1) Installing jq (1.4-r0)Executing busybox-1.23.2-r0.triggerOK: 14 MiB in 28 packagesc2 $ docker exec consul-client sh -c \‘wget -qO - 172.17.42.1:8500/v1/agent/services | jq “.files.Port”‘8000c2 $ sleep 15c2 $ curl -sSL 172.17.42.1:8500/v1/health/service/files | \python -m json.tool | head -n 13[{“Checks”: [{“CheckID”: “filescheck”,“Name”: “filescheck”,“Node”: “mylaptop2”,“Notes”: “”,“Output”: “Success!\n”,“ServiceID”: “files”,“ServiceName”: “files”,“Status”: “passing”} ,
我们现在已经借助Alpine Linux软件包管理器(见技巧51)将jq安装到了镜像,可以通过手动执行以前在脚本中失败的行来验证这一点,然后等待检查重新运行。现在成功了!
借助在前面的例子中用到的脚本来实施健康检查,现在用户拥有了一个重要的构建块来组建应用程序的监控——如果能够将健康检查表示为在终端中运行的一组命令,那么便可以让Consul自动运行它。如果你发现自己想要检查的状态代码可以由一个HTTP端口返回,那么你是幸运的——这是一个普通任务的常见做法,Consul的3种类型的健康检查的其中一种就是专为它设计的。最后一种健康检查——“存活时间”,需要和应用程序进行更深入的集成。状态必须定期设置为健康的,否则检查将自动设置为失败。通过结合这3种类型的健康检查,用户可以在自己的系统之上构建一套全面的应用层健康状况的监控。
在本技巧最后,我们将看看服务器代理镜像附带的可选的Consul Web界面,它会帮助用户了解集群的当前状态。可以访问服务器代理外部IP地址的8500端口来打开界面。这里用户可以访问$EXTIP1:8500。请记住,即使用户在服务器代理的宿主机上,localhost或127.0.0.1也将是无法正常工作的。
我们在本技巧中已经介绍了很多内容——Consul 是一个很大的话题!幸运的是,正如使用etcd 一样,使用键值存储的知识可以转换到其他的键值存储(如Consul),服务发现的相关知识也可以类比到其他提供DNS接口的工具(SkyDNS是可能遇到的一款)。我们所涉及的使用宿主机网络栈和使用外部 IP 地址的一些细节之处也是能够以此类推的。大多数容器化的分布式工具需要跨多个节点进行服务发现时都面临类似的问题。
技巧83 使用Registrator进行自动化服务注册
到目前为止,Consul(以及任何服务发现工具)最明显的缺点是必须管理服务条目的创建和删除。如果将它集成到应用程序中,将会存在多种实现方案以及多个可能出错的地方。
对于没有完全控制权的应用程序,集成也不起作用,因此在启动数据库之类的应用时最终还得编写包装脚本。
问题
不想在Consul手动管理服务条目和健康检查。
解决方案
使用Registrator。
讨论
本技巧是构建在之前的技巧之上的,并假设有一个两部分的 Consul 集群可用,如前文所描述的那样。我们还假设集群中没有服务,所以可能需要从头开始重新创建容器。
我们所做的一切都将在客户端代理机器上。如前所述,除服务器代理之外,我们不应该在其他机器上运行任何容器。
需要根据以下命令启动Registrator:
$ IMG=gliderlabs/registrator:v6$ docker pull $IMG[…]$ ip addr | grep ‘inet ‘ | grep -v ‘lo$|docker0$’inet 192.168.1.80/24 brd 192.168.1.255 scope global wlan0$ EXTIP=192.168.1.80$ ip addr | grep docker0 | grep inetinet 172.17.42.1/16 scope global docker0$ BRIDGEIP=172.17.42.1$ docker run -d —name registrator -h $(hostname)-reg \-v /var/run/docker.sock:/tmp/docker.sock $IMG -ip $EXTIP -resync \60 consul://$BRIDGEIP:8500 # if this fails, $EXTIP is an alternativeb3c8a04b9dfaf588e46a255ddf4e35f14a9d51199fc6f39d47340df31b019b90$ docker logs registrator2015/08/14 20:05:57 Starting registrator v6 …2015/08/14 20:05:57 Forcing host IP to 192.168.1.802015/08/14 20:05:58 consul: current leader 192.168.1.87:83002015/08/14 20:05:58 Using consul adapter: consul://172.17.42.1:85002015/08/14 20:05:58 Listening for Docker events …2015/08/14 20:05:58 Syncing services on 2 containers2015/08/14 20:05:58 ignored: b3c8a04b9dfa no published ports2015/08/14 20:05:58 ignored: a633e58c66b3 no published ports
这里的第一个命令(用于拉动镜像和查找外部IP地址)应该看上去很熟悉。该IP地址会传给Registrator,这样一来它便知道使用哪个IP地址来广播服务。Docker套接字已挂载,以便容器起动和停止时随时通知Registrator。我们也告诉了Registrator该如何连接到 Consul 代理,我们希望所有的容器每 60 秒刷新一次。容器的变化会自动通知Registrator,因此最终的配置有助于减轻Registrator可能错过变更时带来的影响。
现在Registrator正在运行,注册第一个服务非常简单:
$ curl -sSL 172.17.42.1:8500/v1/catalog/services | python -m json.tool{“consul”: []}$ docker run -d -e “SERVICE_NAME=files” -p 8000:80 ubuntu:14.04.2 python3 \-m http.server 803126a8668d7a058333d613f7995954f1919b314705589a9cd8b4e367d4092c9b$ docker inspect 3126a8668d7a | grep ‘Name.*/‘“Name”: “/evil_hopper”,$ curl -sSL 172.17.42.1:8500/v1/catalog/services | python -m json.tool{“consul”: [],“files”: [] }$ curl -sSL 172.17.42.1:8500/v1/catalog/service/files | python -m json.tool[{“Address”: “192.168.1.80”,“Node”: “mylaptop2”,“ServiceAddress”: “192.168.1.80”,“ServiceID”: “mylaptop2-reg:evil_hopper:80”,“ServiceName”: “files”,“ServicePort”: 8000,“ServiceTags”: null}]
在注册服务时,要做的唯一一件事情便是传递一个环境变量给Registrator,告诉它要使用的服务的名称。在默认情况下,Registrator使用的名称基于斜杠之后和标签之前的容器名称组件,mycorp.com/myteam/myimage:0.5 的名称为 myimage。用户可以使用该命名约定,也可以根据自己的命名约定手动指定名称。
其余的值也正如预期的那样。Registrator已经发现正在侦听的端口,将其添加到Consul,并为之配置了一个服务ID,用于指示哪里可以找到容器(这就是为什么主机名配置在了Registrator容器里)。
如果环境里有其他的详细信息,Registrator也会获取,包括标签、每个端口的服务名称(如果有多个)以及所使用的健康检查(如果使用Consul作为数据存储)。可以在JSON中指定环境中检查的细节来启用所有3种类型的Consul健康检查。读者可以在“Registrator Backends”这一节文档的Consul部分阅读更多信息,网址为http://gliderlabs.com/registrator/latest/user/backends/#consul。
如果在不断变化的环境中拥有一大波容器,那么Registrator会表现得很优秀,它可以确保用户不必再担心创建服务时是否建好健康检查的问题。
9.4 小结
本章可能是本书中最开放的部分。我们试图让读者了解 Docker 编排的世界,以便可以自行做出决定——在这里绝对没有一刀切的解决方案。
即使我们在这里对这些可用的工具做了这一调查,实际要具体选择某一款时可能还是会令人很纠结的。我们的建议是保持尽可能简单,如果在本章开头考虑了图9-1中的分支,请权衡利弊。例如,如果流量增长表明两台服务器在下一年将足够,那么现在可能不需要动态置备以及服务发现。
本章包括的主题有:
- 使用Docker世界以外的成熟解决方案来控制单个机器上的容器执行;
- 简单的多宿主机编排解决方案;
- Docker编排领域的两个重量级工具;
- 将应用程序自动插入所选的服务发现后端。
下一章将转向更严肃的话题,即保证Docker安全性。
