Docker-高性能指南-全-
Docker 高性能指南(全)
原文:
annas-archive.org/md5/dd39feb6beef105c72d56854a3ed23fc译者:飞龙
序言
Docker 是构建和部署应用程序的伟大工具。它的可移植容器格式使我们能够在任何地方运行代码,从开发者的工作站到流行的云计算提供商。围绕 Docker 的工作流程使开发、测试和部署变得更容易、更快速。然而,Docker 的内部机制以及持续改进的最佳实践对充分发挥其潜力至关重要。
本书涵盖的内容
具有基础 Docker 知识的工程师可以按顺序阅读本书,从章节到章节。对于已经深入了解 Docker 或曾在生产环境中部署过应用的技术负责人,可以先阅读第八章,上生产环境,以了解 Docker 如何与现有应用程序兼容。本书涵盖的主题如下:
第一章,准备 Docker 主机,快速回顾了如何设置和运行 Docker。本章记录了你将在本书中使用的设置。
第二章,优化 Docker 镜像,展示了调优 Docker 镜像的重要性。将展示一些调优技巧,以提高 Docker 容器的可部署性和性能。
第三章,使用 Chef 自动化 Docker 部署,介绍了如何自动化 Docker 主机的供应和设置。本章将讨论自动化投资的重要性以及它如何促进 Docker 容器的可扩展部署。
第四章,监控 Docker 主机和容器,介绍了如何使用 Graphite 和 Elasticsearch-Logstash-Kibana (ELK)堆栈搭建监控系统和日志系统。
第五章,性能基准测试,是关于如何使用 Apache JMeter 创建工作负载来基准测试 Docker 容器性能的教程。本章回顾了你在第四章,监控 Docker 主机和容器,中设置的监控系统,分析一些 Docker 应用的基准测试结果,如响应时间和吞吐量。
第六章,负载均衡,将教你如何配置和部署基于 Nginx 的负载均衡器 Docker 容器。本章还提供了如何使用你所设置的负载均衡器来扩展 Docker 应用程序的性能和可部署性的教程。
第七章,容器故障排除,展示了如何使用典型 Linux 系统中的常见调试工具来排查 Docker 容器的问题。它们描述了每个工具的工作原理以及如何读取运行中 Docker 容器的诊断信息。
第八章,进入生产环境,总结了你在上一章中所做的所有性能优化,并讲解了在生产环境中使用 Docker 运营任何 Web 应用程序的含义。
本书所需的内容
需要一台运行最新内核的 Linux 工作站作为 Docker 1.10.0 的主机。本书使用 Debian Jessie 8.2 作为其基础操作系统来安装和设置 Docker。
如何让 Docker 启动并运行的更多细节,可以在 第一章,准备 Docker 主机 中找到。
本书适合谁阅读
本书是为那些希望将其 Docker 应用程序和基础设施部署到生产环境中的开发人员和运维人员编写的。如果你已经学习了 Docker 的基础知识,但希望更进一步,那么本书适合你。
约定
在本书中,你会看到多种文本样式,用来区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。
文章中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号等都会按如下方式显示:“我们将使用 --link <source>:<alias> 来创建一个从源容器 source 到别名 webapp 的链接。”
一块代码的显示方式如下:
FROM ubuntu:14.04
MAINTAINER Docker Education Team <education@docker.com>
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get \
install -y -q python-all python-pip
ADD ./webapp/requirements.txt /tmp/requirements.txt
RUN pip install -qr /tmp/requirements.txt
ADD ./webapp /opt/webapp/
WORKDIR /opt/webapp
EXPOSE 5000
CMD ["python", "app.py"]
当我们希望引起你对某个代码块中特定部分的注意时,相关的行或项目会以粗体显示:
import os
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
provider = str(os.environ.get('PROVIDER', 'world'))
return 'Hello '+provider+'!'
if __name__ == '__main__':
# Bind to PORT if defined, otherwise default to 5000.
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)
任何命令行输入或输出会按如下方式显示:
dockerhost$ docker inspect -f "{{ .NetworkSettings.IPAddress }}" \
source
172.17.0.15
dockerhost$ docker inspect -f "{{ .NetworkSettings.IPAddress }}" \
destination
172.17.0.28
dockerhost$ iptables -L DOCKER
Chain DOCKER (1 references)
target prot opt source destination
ACCEPT tcp -- 172.17.0.28 172.17.0.15 tcp dpt:5000
ACCEPT tcp -- 172.17.0.15 172.17.0.28 tcp spt:5000
新术语和重要单词会以粗体显示。你在屏幕上看到的词语,例如菜单或对话框中的内容,会像这样出现在文本中:“最后,点击 下载入门套件。”
注意
警告或重要提示会以类似这样的框框显示。
提示
小贴士和技巧会像这样显示。
读者反馈
我们欢迎读者的反馈。请告诉我们你对本书的看法——你喜欢或不喜欢的内容。读者的反馈对我们非常重要,因为它帮助我们开发出你真正能从中受益的书籍。
要向我们发送一般反馈,只需通过电子邮件发送至<feedback@packtpub.com>,并在邮件的主题中提及书籍的标题。
如果您在某个领域拥有专业知识并且有兴趣撰写或为书籍做贡献,请查看我们的作者指南,网址为www.packtpub.com/authors。
客户支持
既然您已经成为了 Packt 书籍的骄傲拥有者,我们为您提供了许多资源,帮助您最大化地利用您的购买。
下载示例代码
您可以通过您的帐户从www.packtpub.com下载所有您购买的 Packt 出版书籍的示例代码文件。如果您是在其他地方购买了此书,可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
勘误
尽管我们已尽力确保内容的准确性,但难免会出现错误。如果您在我们的书籍中发现错误——例如文本或代码中的错误——我们将非常感激您能向我们报告。这样,您可以避免其他读者的困扰,并帮助我们改进后续版本的书籍。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并填写勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误将上传至我们的网站或添加到该书籍的勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书籍的名称。所需的信息将在勘误部分显示。
盗版
网络上版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视保护我们的版权和许可证。如果您在网络上发现我们的作品的任何非法副本,请立即向我们提供该副本的地址或网站名称,以便我们采取措施。
请通过<copyright@packtpub.com>联系我们,提供涉嫌盗版材料的链接。
我们感谢您为保护我们的作者以及我们提供有价值内容的能力所做的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。
第一章:准备 Docker 主机
Docker 使我们能够更快地将应用交付给客户。它通过使我们能够轻松创建并启动 Docker 容器,简化了将代码从开发到生产所需的工作流程。本章将简要回顾如何准备我们的环境,以便运行基于 Docker 的开发和运维工作流,具体包括:
-
准备 Docker 主机
-
使用 Docker 镜像
-
运行 Docker 容器
本章的大部分内容是我们已经熟悉的概念,并且可以在 Docker 文档网站上轻松找到。本章展示了在后续章节中将使用的与 Docker 主机的选定命令和交互。
准备 Docker 主机
假设我们已经熟悉如何设置 Docker 主机。本书的大多数章节将基于以下环境来运行示例,除非另有明确说明:
-
操作系统——Debian 8.2 Jessie
-
Docker 版本——1.10.0
以下命令显示操作系统和 Docker 版本:
$ ssh dockerhost
dockerhost$ lsb_release –a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 8.2 (jessie)
Release: 8.2
Codename: jessie
dockerhost$ docker version
Client:
Version: 1.10.0
API version: 1.21
Go version: go1.4.2
Git commit: a34a1d5
Built: Fri Nov 20 12:59:02 UTC 2015
OS/Arch: linux/amd64
Server:
Version: 1.10.0
API version: 1.21
Go version: go1.4.2
Git commit: a34a1d5
Built: Fri Nov 20 12:59:02 UTC 2015
OS/Arch: linux/amd64
如果我们还没有设置 Docker 环境,可以按照 Docker 官网的docs.docker.com/installation/debian上的说明来准备 Docker 主机。
提示
下载示例代码
你可以从你的账户中下载所有你购买的 Packt 书籍的示例代码文件,网址为www.packtpub.com。如果你是在其他地方购买的这本书,可以访问www.packtpub.com/support并注册,文件将通过电子邮件直接发送给你。
使用 Docker 镜像
Docker 镜像是包含我们的应用程序和其他支持组件的工件,帮助运行它们,比如操作系统基础镜像、运行时和开发库等。它们被部署并下载到 Docker 主机中,以便将我们的应用作为 Docker 容器运行。本节将涵盖以下与 Docker 镜像相关的命令:
-
docker build -
docker images -
docker push -
docker pull
注意
本节中的大部分内容可以在 Docker 文档网站docs.docker.com/userguide/dockerimages上轻松找到。
构建 Docker 镜像
我们将使用 Docker 教育团队提供的training/webapp的Dockerfile来构建一个 Docker 镜像。接下来的几步将展示如何构建这个 Web 应用:
-
首先,我们将克隆
webapp的 Git 仓库,可以通过以下命令从github.com/docker-training/webapp获取:dockerhost$ git clone https://github.com/docker-training/webapp.git training-webapp Cloning into 'training-webapp'... remote: Counting objects: 45, done. remote: Total 45 (delta 0), reused 0 (de..., pack-reused 45 Unpacking objects: 100% (45/45), done. Checking connectivity... done. -
然后,通过执行以下命令使用
docker build命令构建 Docker 镜像:dockerhost$ cd training-webapp dockerhost$ docker build -t hubuser/webapp . Sending build context to Docker daemon 121.3 kB Sending build context to Docker daemon Step 0 : FROM ubuntu:14.04 Repository ubuntu already being ... another client. Waiting. ---> 6d4946999d4f Step 1 : MAINTAINER Docker Education Team <education@docker.com> ---> Running in 0fd24c915568 ---> e835d0c77b04 Removing intermediate container 0fd24c915568 Step 2 : RUN apt-get update ---> Running in 45b654e66939 Ign http://archive.ubuntu.com trusty InRelease ... Removing intermediate container c08be35b1529 Step 9 : CMD python app.py ---> Running in 48632c5fa300 ---> 55850135bada Removing intermediate container 48632c5fa300 Successfully built 55850135bada注意
-t标志用于将镜像标记为hubuser/webapp。将容器标记为<username>/<imagename>是推送 Docker 镜像到后续部分的重要约定。有关docker build命令的更多细节,可以参考docs.docker.com/reference/commandline/build,或运行docker build --help。 -
最后,让我们通过
docker images命令确认镜像已经在我们的 Docker 主机中可用:dockerhost$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE hubuser/webapp latest 55850135 5 minutes ago 360 MB ubuntu 14.04 6d494699 3 weeks ago 188.3 MB
将 Docker 镜像推送到仓库
现在我们已经制作了一个 Docker 镜像,让我们将其推送到仓库中,以便在其他 Docker 主机之间共享和部署。Docker 的默认安装会将镜像推送到 Docker Hub。Docker Hub 是由 Docker, Inc. 公共托管的一个仓库,任何拥有账户的人都可以推送和分享他们的 Docker 镜像。以下步骤将展示如何操作:
-
在能够推送到 Docker Hub 之前,我们需要使用
docker login命令进行身份验证,如下所示:dockerhost$ docker login Username: hubuser Password: ******** Email: hubuser@hubemail.com WARNING: login credentials saved in /home/hubuser/.dockercfg. Login Succeeded注意
如果我们还没有 Docker Hub 账户,可以按照以下指示在
hub.docker.com/account/signup注册一个账户。 -
现在我们可以将镜像推送到 Docker Hub。正如前面部分提到的,镜像的标签标识了仓库中的
<username>/<imagename>。使用以下docker push命令来推送我们的镜像到 Docker Hub:dockerhost$ docker push hubuser/webapp The push refers to a repository [hubuser/webapp] (len: 1) Sending image list Pushing repository hubuser/webapp (1 tags) 428b411c28f0: Image already pushed, skipping ... 7d04572a66ec: Image successfully pushed 55850135bada: Image successfully pushed latest: digest: sha256:b00a3d4e703b5f9571ad6a... size: 2745
现在我们已经成功地将 Docker 镜像推送到 Docker Hub,它将会在 Docker Hub 中可用。我们还可以在 Docker Hub 页面中获取我们推送的镜像的更多信息,页面与下图类似。在这个例子中,我们的 Docker Hub URL 是 https:// hub.docker.com/r/hubuser/webapp:

注意
更多关于将 Docker 镜像推送到仓库的细节,请参考 docker push --help 和 docs.docker.com/reference/commandline/push。
Docker Hub 是一个很好的开始托管我们 Docker 镜像的地方。然而,在某些情况下,我们可能希望托管自己的镜像仓库。例如,当我们希望在将镜像拉取到 Docker 主机时节省带宽时。另一个原因可能是我们的 Docker 主机位于数据中心内部,可能已将 Internet 防火墙隔离开来。在第二章,优化 Docker 镜像中,我们将更详细地讨论如何运行我们自己的 Docker 注册表,以便拥有一个内部的 Docker 镜像仓库。
从仓库拉取 Docker 镜像
一旦我们的 Docker 镜像构建并推送到如 Docker Hub 的仓库中,我们可以将它们拉取到 Docker 主机上。当我们在开发工作站的 Docker 主机上首次构建 Docker 镜像,并希望将其部署到云中生产环境的 Docker 主机时,这一工作流程特别有用。这样就不需要在其他 Docker 主机上重新构建相同的镜像。拉取镜像也可以用来从 Docker Hub 获取现有的 Docker 镜像,在此基础上构建我们的 Docker 镜像。所以,与我们之前克隆 Git 仓库并在其他 Docker 主机上重新构建不同,我们可以选择拉取镜像。接下来的步骤将引导我们拉取我们之前推送的hubuser/webapp Docker 镜像:
-
首先,让我们清理现有的 Docker 主机,确保我们将从 Docker Hub 下载镜像。输入以下命令,确保我们从干净的环境开始:
dockerhost$ dockerhost rmi hubuser/webapp -
接下来,我们可以使用
docker pull命令下载镜像,如下所示:dockerhost$ docker pull hubuser/webapp latest: Pulling from hubuser/webapp e9e06b06e14c: Pull complete ... b37deb56df95: Pull complete 02a8815912ca: Already exists Digest: sha256:06e9c1983bd6d5db5fba376ccd63bfa529e8d02f23d5 Status: Downloaded newer image for hubuser/webapp:latest -
最后,我们通过执行以下命令再次确认是否成功下载了镜像:
dockerhost$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE ubuntu 14.04 6d494699 3 weeks ago 188.3 MB hubuser/webapp latest 2a8815ca 7 weeks ago 348.8 MB
注意
有关如何拉取 Docker 镜像的更多详细信息,请参见docker pull --help和docs.docker.com/reference/commandline/pull。
运行 Docker 容器
现在我们已经拉取或构建了 Docker 镜像,可以使用docker run命令来运行和测试它们。本节将回顾一些我们将在后续章节中使用的命令行标志,并使用以下 Docker 命令获取关于在 Docker 主机内运行的 Docker 容器的更多信息:
-
docker ps -
docker inspect
注意
更全面的命令行标志详细信息可以在docker run --help和docs.docker.com/reference/commandline/run找到。
暴露容器端口
在training/webapp示例中,其 Docker 容器作为 Web 服务器运行。为了让应用程序能够为容器环境外的 Web 流量提供服务,Docker 需要知道该应用程序绑定的端口。Docker 将这些信息称为暴露端口。本节将引导我们在运行容器时如何暴露端口信息。
回到我们之前使用的training/webapp Docker 镜像,该应用程序提供了一个监听端口5000的 Python Flask Web 应用程序,具体内容见webapp/app.py中的突出部分:
import os
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
provider = str(os.environ.get('PROVIDER', 'world'))
return 'Hello '+provider+'!'
if __name__ == '__main__':
# Bind to PORT if defined, otherwise default to 5000.
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)
相应地,Docker 镜像通过Dockerfile中的EXPOSE指令让 Docker 主机知道该应用程序在端口5000上监听,具体说明如下:
FROM ubuntu:14.04
MAINTAINER Docker Education Team <education@docker.com>
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get \
install -y -q python-all python-pip
ADD ./webapp/requirements.txt /tmp/requirements.txt
RUN pip install -qr /tmp/requirements.txt
ADD ./webapp /opt/webapp/
WORKDIR /opt/webapp
EXPOSE 5000
CMD ["python", "app.py"]
现在我们已经对 Docker 如何暴露容器的端口有了基本了解,接下来的步骤将引导我们运行hubuser/webapp容器:
-
使用
docker run命令,并带上-d标志,以守护进程方式运行容器,如下所示:dockerhost$ docker run --name ourapp -d hubuser/webapp -
最后,通过
docker ps确认 Docker 主机上容器运行并暴露了5000端口。我们可以通过以下命令来执行此操作:dockerhost:~/training-webapp$ docker ps CONTAINER ID IMAGE ... STATUS PORTS NAMES df3e6b788fd8 hubuser... Up 4 seconds 5000/tcp ourapp
除了EXPOSE指令外,还可以在运行时通过--expose=[]标志覆盖暴露的端口。例如,使用以下命令使hubuser/webapp应用暴露4000到4500端口:
dockerhost$ docker run -d --expose=4000-4500 \
--name app hubuser/webapp
dockerhost $ docker ps
CONTAINER ID IMAGE ... PORTS NAMES
ca4dc1da26d hubuser/webapp:latest ... 4000-4500/tcp,5000/tcp app
df3e6b788fd8 hubuser/webapp:l... 5000/tcp ourapp
这个临时的docker run标志在调试应用时非常有用。例如,假设我们的 Web 应用使用4000到4500端口。然而,我们通常不希望这些端口范围在生产环境中可用。我们可以使用--expose=[]暂时启用它,以启动一个可调试的容器。有关如何使用此类技术来排除 Docker 容器故障的更多细节,将在第七章,容器故障排除中讨论。
发布容器端口
暴露仅使端口在容器内可用。为了使应用能够在其 Docker 主机外部提供服务,端口需要被发布。docker run命令使用-P和-p标志来发布容器的暴露端口。本节将讲解如何使用这两个标志在 Docker 主机上发布端口。
--publish-all
-P或--publish-all标志将容器的所有暴露端口发布到 Docker 主机的随机高端口,这些端口位于/proc/sys/net/ipv4/ip_local_port_range定义的临时端口范围内。接下来的几步将回到我们之前使用的hubuser/webapp Docker镜像,探讨如何发布暴露的端口:
-
首先,输入以下命令运行一个发布所有暴露端口的容器:
dockerhost$ docker run -P –d --name exposed hubuser/webapp -
接下来,让我们确认 Docker 主机将端口
32771发布到 Docker 容器暴露的端口5000。输入如下的docker ps命令进行验证:dockerhost$ docker ps CONTAINER ID IMAGE ... PORTS NAMES 508cf1fb3e5 hubuser/webapp:latest ... 0.0.0.0:32771->5000/tcp exposed -
我们还可以验证分配的端口
32771是否位于我们 Docker 主机配置的临时端口范围内:dockerhost$ cat /proc/sys/net/ipv4/ip_local_port_range 32768 61000 -
此外,我们还可以通过以下命令确认 Docker 主机正在监听分配的端口
32771:dockerhost$ ss -lt 'sport = *:32771' State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 :::32771 :::* -
最后,我们可以通过实际发起 HTTP 请求来验证 Docker 主机的端口
32771确实映射到正在运行的 Docker 容器,并确认它是由training/webappPython 应用响应的。运行以下命令进行确认:$ curl http://dockerhost:32771 Hello world!
--publish
-p或--publish标志将容器端口发布到 Docker 主机。如果容器端口尚未暴露,则该容器也会被暴露。根据文档,-p标志可以使用以下格式来发布容器端口:
-
containerPort -
hostPort:containerPort -
ip::containerPort -
ip:hostPort:containerPort
通过指定hostPort,我们可以确定将容器端口映射到 Docker 主机的哪个端口,而不是分配随机的临时端口。通过指定ip,我们可以限制 Docker 主机接受连接的接口,以将数据包中继到映射的 Docker 容器的暴露端口。回到hubuser/webapp的例子,以下是将 Python 应用程序的暴露端口5000映射到我们 Docker 主机的端口80的回环接口的命令:
$ ssh dockerhost
dockerhost$ docker run -d -p 127.0.0.1:80:5000 training/webapp
dockerhost$ curl http://localhost
Hello world!
dockerhost$ exit
logout
Connection to dockerhost closed.
$ curl http://dockerhost
curl: (7) Failed connect to dockerhost:80; Connection refused
使用上述的docker run调用,Docker 主机只能从http://localhost中提供应用程序的 HTTP 请求。
连接容器
在前面的部分描述的发布端口还允许容器通过连接到发布的 Docker 主机端口相互通信。另一种直接连接容器的方法是建立容器链接。链接的容器允许源容器将信息发送到目标容器。它使得通信的容器可以安全地发现彼此。
注意
更多关于链接容器的详细信息可以在 Docker 文档网站上找到:docs.docker.com/userguide/dockerlinks。
在本节中,我们将使用--link标志安全地连接容器。接下来的几个步骤将为我们展示如何使用链接的容器的示例:
-
作为准备工作,请确保我们的
hubuser/webapp容器仅运行具有暴露端口的情况。我们将创建一个名为source的容器,作为我们的源容器。输入以下命令以重新创建此容器:dockerhost$ docker run --name source –d hubuser/webapp -
接下来,我们将创建一个目标容器。我们将使用
--link<source>:<alias>来创建从名为source的源容器到名为webapp的别名的链接。输入以下命令以创建到我们目标容器的链接:dockerhost$ docker run -d --link source:webapp \ --name destination busybox /bin/ping webapp -
现在通过检查新创建的目标容器
destination,来确认链接已经建立。执行以下命令:dockerhost$ docker inspect -f "{{ .HostConfig.Links }}" \ destination [/source:/destination/webapp]
在链接过程中发生的情况是 Docker 主机在两个容器之间创建了一个安全隧道。我们可以在 Docker 主机的 iptables 中确认这个隧道,如下所示:
dockerhost$ docker inspect -f "{{ .NetworkSettings.IPAddress }}" \
source
172.17.0.15
dockerhost$ docker inspect -f "{{ .NetworkSettings.IPAddress }}" \
destination
172.17.0.28
dockerhost$ iptables -L DOCKER
Chain DOCKER (1 references)
target prot opt source destination
ACCEPT tcp -- 172.17.0.28 172.17.0.15 tcp dpt:5000
ACCEPT tcp -- 172.17.0.15 172.17.0.28 tcp spt:5000
在上述 iptables 中,Docker 主机允许名为destination (172.17.0.28)的目标容器接受到来自名为source (172.17.0.15)的源容器端口5000的出站连接。第二个 iptables 条目允许容器source从容器destination接收到其端口5000的连接。
除了 Docker 主机之间建立的安全连接之外,Docker 主机还通过以下方式向目标容器公开源容器的信息:
-
环境变量
-
/etc/hosts中的条目
这两种信息源将在下一节中作为使用交互式容器的示例用例进一步探讨。
交互式容器
通过指定 -i 标志,我们可以指定一个在前台运行的容器连接到标准输入流。结合使用 -t 标志时,还会为容器分配一个伪终端。这样,我们就可以像正常的 Shell 一样使用 Docker 容器进行交互式操作。当我们想要调试并检查 Docker 容器内发生的事情时,这个功能非常有用。从前面的章节继续,我们可以通过以下步骤调试容器链接发生的情况:
-
为了准备,输入以下命令以建立与先前运行的名为
source的容器链接的交互式容器会话:dockerhost$ docker run -i -t --link source:webapp \ --name interactive_container \ busybox /bin/sh / # -
接下来,让我们首先通过以下命令探索暴露给交互式目标容器的环境变量:
/ # env | grep WEBAPP WEBAPP_NAME=/interactive_container/webapp WEBAPP_PORT_5000_TCP_ADDR=172.17.0.15 WEBAPP_PORT_5000_TCP_PORT=5000 WEBAPP_PORT_5000_TCP_PROTO=tcp WEBAPP_PORT_5000_TCP=tcp://172.17.0.15:5000 WEBAPP_PORT=tcp://172.17.0.15:5000注意
通常,在链接的容器中会设置以下环境变量:
-
<alias>_NAME=/container_name/alias_name针对每个源容器 -
<alias>_PORT_<port>_<protocol>显示每个暴露端口的 URL。它还作为一个独特的前缀,扩展到以下更多的环境变量:-
<prefix>_ADDR包含源容器的 IP 地址 -
<prefix>_PORT显示暴露端口的号码 -
<prefix>_PROTO描述了暴露端口的协议,可以是 TCP 或 UDP
-
-
<alias>_PORT显示源容器的第一个暴露端口
-
-
链接容器中的第二个容器发现功能是更新过的
/etc/hosts文件。webapp链接容器的别名被映射到source源容器的 IP 地址,源容器的名称也映射到相同的 IP 地址。以下片段是我们交互式容器会话中/etc/hosts文件的内容,包含了这个映射:172.17.0.29 d4509e3da954 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.15 webapp 85173b8686fc source -
最后,我们可以使用别名连接到源容器。在以下示例中,我们将通过向别名
webapp发起 HTTP 请求来连接运行在源容器中的 Web 应用:/ # nc webapp 5000 GET / Hello world! / #注意
交互式容器也可以用来构建容器,配合
docker commit使用。但这是一个繁琐的过程,而且这种开发过程不能扩展到多个开发者。应该改用docker build,并在版本控制中管理我们的Dockerfile。
总结
希望到这个时候,我们已经重新熟悉了本书中将要使用的大部分命令。我们准备了一个 Docker 主机来与 Docker 容器进行交互。接着,我们构建、下载并上传了各种 Docker 镜像,以便在我们的开发和生产 Docker 主机上开发和部署容器。最后,我们从构建或下载的 Docker 镜像中运行了 Docker 容器。此外,我们还通过学习 Docker 容器的运行方式,掌握了如何与正在运行的容器进行通信和交互的一些基本技能。
在下一章中,你将学习如何优化我们的 Docker 镜像。那么,让我们直接开始吧!
第二章:优化 Docker 镜像
现在我们已经构建并部署了 Docker 容器,可以开始享受使用它们的好处。我们拥有一个标准的软件包格式,允许开发人员和系统管理员合作,简化我们应用程序代码的管理。Docker 的容器格式使我们能够快速迭代应用程序的版本,并与组织中的其他成员共享。由于 Docker 容器的轻量级特性和速度,我们的开发、测试和部署时间都大大减少。Docker 容器的可移植性使我们能够将应用程序从物理服务器扩展到云中的虚拟机。
然而,我们将开始注意到,最初使用 Docker 的原因正失去其效力。开发时间增加,因为我们必须每次都下载应用程序最新版本的 Docker 镜像运行时库。部署花费大量时间,因为 Docker Hub 很慢。最糟糕的是,Docker Hub 可能会宕机,我们根本无法进行任何部署。我们的 Docker 镜像现在变得非常大,达到几个 GB,简单的一行更新也需要整整一天。
本章将涵盖以下几种情况,说明 Docker 容器如何失控,并提出前面提到的问题的解决步骤:
-
减少镜像部署时间
-
减少镜像构建时间
-
减少镜像大小
减少部署时间
随着构建 Docker 容器的时间推移,它的大小越来越大。更新我们现有 Docker 主机上的运行容器并不成问题。Docker 利用我们随着应用程序增长而逐步构建的 Docker 镜像层。然而,假设我们想要扩展应用程序。这就需要在额外的 Docker 主机上部署更多的 Docker 容器。每个新的 Docker 主机都必须下载我们随着时间推移构建的所有大镜像层。本节将展示一个 大型 Docker 应用程序如何影响新 Docker 主机上的部署时间。首先,按照以下步骤构建这个问题 Docker 应用程序:
-
编写以下
Dockerfile来创建我们的 "大型" Docker 镜像:FROM debian:jessie RUN dd if=/dev/urandom of=/largefile bs=1024 count=524288 -
接下来,使用以下命令构建
Dockerfile为hubuser/largeapp:dockerhost$ docker build -t hubuser/largeapp . -
注意记录创建的 Docker 镜像的大小。在下面的输出示例中,大小为
662 MB:dockerhost$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE hubuser/largeapp latest 450e3123 5 minutes ago 662 MB debian jessie 9a61b6b1 4 days ago 125.2 MB -
使用
time命令,记录从 Docker Hub 推送和拉取的时间,如下所示:dockerhost$ time docker push hubuser/largeapp The push refers to a repository [hubuser/largeapp] (len: 1) 450e319e42c3: Image already exists 9a61b6b1315e: Image successfully pushed 902b87aaaec9: Image successfully pushed Digest: sha256:18ef52e36996dd583f923673618483a4466aa2d1d0d6ce9f0... real 11m34.133s user 0m0.164s sys 0m0.104s dockerhost$ time docker pull hubuser/largeapp latest: Pulling from hubuser/largeapp 902b87aaaec9: Pull complete 9a61b6b1315e: Pull complete 450e319e42c3: Already exists Digest: sha256:18ef52e36996dd583f923673618483a4466aa2d1d0d6ce9f0... Status: Downloaded newer image for hubuser/largeapp:latest real 2m56.805s user 0m0.204s sys 0m0.188s
正如我们在前面突出显示的时间值所看到的,执行 docker push 将镜像上传到 Docker Hub 时需要花费大量时间。部署时,docker pull 也需要同样长的时间来将我们新创建的 Docker 镜像传播到新的生产 Docker 主机。这些上传和下载的时间值还取决于 Docker Hub 与我们的 Docker 主机之间的网络连接。最终,当 Docker Hub 出现故障时,我们将失去部署新的 Docker 容器或按需扩展到更多 Docker 主机的能力。
为了利用 Docker 快速交付应用程序、简化部署和扩展的优势,我们推送和拉取 Docker 镜像的方法必须可靠且快速。幸运的是,我们可以运行自己的 Docker 注册中心,能够托管和分发 Docker 镜像,而无需依赖公共 Docker Hub。接下来的几步将描述如何设置这一过程,以确认性能的提升:
-
让我们通过输入以下命令来运行我们自己的 Docker 注册中心。这将使我们在
tcp://dockerhost:5000上运行本地注册中心:dockerhost$ docker run -p 5000:5000 -d registry:2 -
接下来,让我们确认 Docker 镜像部署的改进。首先,为之前创建的镜像创建一个标签,以便通过以下命令将其推送到本地 Docker 注册中心:
dockerhost$ docker tag hubuser/largeapp \ dockerhost:5000/largeapp -
观察通过我们新运行的 Docker 注册中心推送相同 Docker 镜像时有多快。以下测试表明,推送 Docker 镜像的速度现在至少提高了 10 倍:
dockerhost$ time docker push dockerhost:5000/largeapp The push refers to a ...[dockerhost:5000/largeapp] (len: 1) ... real 0m52.928s user 0m0.084s sys 0m0.048s -
现在,在测试从本地 Docker 注册中心拉取镜像之前,先确认我们 Docker 镜像拉取的新性能。我们先确保删除之前构建的镜像。以下测试表明,Docker 镜像的下载速度现在提高了 30 倍:
dockerhost$ docker rmi dockerhost:5000/largeapp \ hubuser/largeapp Untagged: dockerhost:5000/largeapp:latest Untagged: hubuser/largeapp:latest Deleted: 549d099c0edaef424edb6cfca8f16f5609b066ba744638990daf3b43... dockerhost$ time docker pull dockerhost:5000/largeapp latest: Pulling from dockerhost:5000/largeapp 549d099c0eda: Already exists 902b87aaaec9: Already exists 9a61b6b1315e: Already exists Digest: sha256:323bed623625b3647a6c678ee6840be23616edc357dbe07c5a0c68b62dd52ecf Status: Downloaded newer image for dockerhost:5000/largeapp:latest real 0m10.444s user 0m0.160s sys 0m0.056s
这些改进的主要原因是我们从本地网络上传和下载了相同的镜像。我们节省了 Docker 主机的带宽,部署时间也缩短了。最重要的是,我们不再需要依赖 Docker Hub 的可用性来进行部署。
注意
为了将 Docker 镜像部署到其他 Docker 主机,我们需要为 Docker 注册中心设置安全性。本书的范围之外会涉及如何设置 Docker 注册中心的细节。然而,更多关于如何设置 Docker 注册中心的信息可以在 docs.docker.com/registry/deploying 获取。
改善镜像构建时间
Docker 镜像是开发人员一直在处理的主要成果物。Docker 文件的简单性和容器技术的速度使我们能够对正在开发的应用程序进行快速迭代。然而,一旦构建 Docker 镜像所需的时间开始失控,这些 Docker 的优势就会开始减弱。在本节中,我们将讨论一些构建 Docker 镜像需要较长时间的案例,然后我们将给出一些提升这些效果的建议。
使用镜像仓库
镜像构建时间的一个大因素是获取上游镜像所花费的时间。假设我们有一个 Dockerfile,其中有以下一行:
FROM java:8u45-jre
该镜像需要下载 java:8u45-jre 以进行构建。当我们迁移到另一个 Docker 主机,或当 java:8u45-jre 镜像在 Docker Hub 上更新时,我们的构建时间会暂时增加。配置本地注册表镜像可以减少这种镜像构建时间的情况。这在组织环境中非常有用,每个开发人员在其工作站上都有自己的 Docker 主机。组织的网络只需从 Docker Hub 下载一次镜像。现在,组织中每个工作站的 Docker 主机都可以直接从本地注册表镜像获取镜像。
设置一个注册表镜像就像在前一节中设置本地注册表一样简单。然而,除此之外,我们还需要配置 Docker 主机,使其能够识别这个注册表镜像,通过向 Docker 守护进程传递 --registry-mirror 选项来完成。以下是进行此设置的步骤:
-
在我们的 Debian Jessie Docker 主机上,通过更新并创建一个位于
/etc/systemd/system/docker.service.d/10-syslog.conf的 Systemd drop-in 文件,配置 Docker 守护进程,该文件应包含以下行:[Service] ExecStart= ExecStart=/usr/bin/docker daemon-H fd:// \ --registry-mirror=http://dockerhost:5000 -
现在,我们将重新加载 Systemd,以便加载
docker.service单元的新 drop-in 配置,如下所示:dockerhost$ systemctl daemon-reload -
接下来,重启 Docker 守护进程,以通过以下命令启动它并加载新的 Systemd 单元:
dockerhost$ systemctl restartdocker.service -
最后,运行注册表镜像 Docker 容器。运行以下命令:
dockerhost$ docker run -p 5000:5000 -d \ -e STANDALONE=false \ -e MIRROR_SOURCE=https://registry-1.docker.io \ -e MIRROR_SOURCE_INDEX=https://index.docker.io \registry
为了确认注册表镜像按预期工作,请执行以下步骤:
-
构建开头部分所描述的
Dockerfile并注意它的构建时间。请注意,构建 Docker 镜像所需的大部分时间都花在了下载上游java:8u45-jreDocker 镜像上,如下所示的命令:dockerhost$ time docker build -t hubuser/mirrorupstream . Sending build context to Docker daemon 2.048 kB Sending build context to Docker daemon Step 0 : FROM java:8u45-jre Pulling repository java 4ac125456dd3: Download complete 902b87aaaec9: Download complete 9a61b6b1315e: Download complete 1ff9f26f09fb: Download complete 6f6bffbbf095: Download complete 4b61c52d7fe4: Download complete 1a9b1e5c4dd5: Download complete 2e8cff440182: Download complete 46bc3bbea0ec: Download complete 3948efdeee11: Download complete 918f0691336e: Download complete Status: Downloaded newer image for java:8u45-jre ---> 4ac125456dd3 Successfully built 4ac125456dd3 real 1m58.095s user 0m0.036s sys 0m0.028s -
现在,删除镜像及其上游依赖,并使用以下命令重新构建镜像:
dockerhost$ docker rmi java:8u45-jre hubuser/mirrorupstream dockerhost$ time docker build -t hubuser/mirrorupstream . Sending build context to Docker daemon 2.048 kB Sending build context to Docker daemon Step 0 : FROM java:8u45-jre Pulling repository java 4ac125456dd3: Download complete 902b87aaaec9: Download complete 9a61b6b1315e: Download complete 1ff9f26f09fb: Download complete 6f6bffbbf095: Download complete 4b61c52d7fe4: Download complete 1a9b1e5c4dd5: Download complete 2e8cff440182: Download complete 46bc3bbea0ec: Download complete 3948efdeee11: Download complete 918f0691336e: Download complete Status: Downloaded newer image for java:8u45-jre ---> 4ac125456dd3 Successfully built 4ac125456dd3 real 0m59.260s user 0m0.032s sys 0m0.028s
当第二次下载 java:8u45-jre Docker 镜像时,它是从本地注册表镜像中获取的,而不是连接到 Docker Hub。设置 Docker 注册表镜像使得下载上游镜像的时间几乎减少了两倍。如果我们有其他 Docker 主机指向同一个注册表镜像,它也会做相同的事情:跳过从 Docker Hub 下载。
注意
本指南关于如何设置注册表镜像,基于 Docker 官方文档网站上的内容。更多细节可以在 docs.docker.com/articles/registry_mirror 找到。
重用镜像层
如我们所知,Docker 镜像由一系列层组成,这些层通过单个镜像的联合文件系统进行组合。当我们构建 Docker 镜像时,Docker 会检查 Dockerfile 中的前置指令,查看是否有现有的镜像可以复用,而不是为这些指令创建类似或重复的镜像。如果了解了构建缓存的工作原理,我们可以大大提高后续 Docker 镜像构建的速度。一个好的例子是,当我们开发应用程序的行为时,我们并不会一直添加依赖项。大多数时候,我们只需要更新应用程序本身的核心行为。了解这一点后,我们可以在开发工作流中围绕这一点设计 Docker 镜像的构建方式。
注意
关于 Dockerfile 指令缓存的详细规则,可以参阅 docs.docker.com/articles/dockerfile_best-practices/#build-cache。
例如,假设我们正在开发一个 Ruby 应用程序,它的源代码树看起来像下面这样:

config.ru的内容如下:
app = proc do |env|
[200, {}, %w(hello world)]
end
run app
Gemfile的内容如下:
source 'https://rubygems.org'
gem 'rack'
gem 'nokogiri'
Dockerfile的内容如下:
FROM ruby:2.2.2
ADD . /app
WORKDIR /app
RUN bundle install
EXPOSE 9292
CMD rackup -E none
接下来的步骤将向您展示如何将我们之前编写的 Ruby 应用程序构建为 Docker 镜像:
-
首先,通过以下命令构建这个 Docker 镜像。请注意,构建所用的时间大约为一分钟:
dockerhost$ time docker build -t slowdependencies . Sending build context to Docker daemon 4.096 kB Sending build context to Docker daemon Step 0 : FROM ruby:2.2.2 ---> d763add83c94 Step 1 : ADD . /app ---> 6663d8b8b5d4 Removing intermediate container 2fda8dc40966 Step 2 : WORKDIR /app ---> Running in f2bec0dea1c9 ---> 289108c6655f Removing intermediate container f2bec0dea1c9 Step 3 : RUN bundle install ---> Running in 7025de40c01d Don't run Bundler as root. Bundler can ask for sudo if ... Fetching gem metadata from https://rubygems.org/......... Fetching version metadata from https://rubygems.org/.. Resolving dependencies... Installing mini_portile 0.6.2 Installing nokogiri 1.6.6.2 with native extensions Installing rack 1.6.4 Using bundler 1.10.5 Bundle complete! 2 Gemfile dependencies, 4 gems now installed. Bundled gems are installed into /usr/local/bundle. ---> ab26818ccd85 Removing intermediate container 7025de40c01d Step 4 : EXPOSE 9292 ---> Running in e4d7647e978b ---> a602159cb786 Removing intermediate container e4d7647e978b Step 5 : CMD rackup -E none ---> Running in 407308682d13 ---> bffce44702f8 Removing intermediate container 407308682d13 Successfully built bffce44702f8 real 0m54.428s user 0m0.004s sys 0m0.008s -
接下来,更新
config.ru来改变应用程序的行为,如下所示:app = proc do |env| [200, {}, %w(hello other world)] end run app -
现在,我们重新构建 Docker 镜像,并记录构建完成所花费的时间。运行以下命令:
dockerhost$ time docker build -t slowdependencies . Sending build context to Docker daemon 4.096 kB Sending build context to Docker daemon Step 0 : FROM ruby:2.2.2 ---> d763add83c94 Step 1 : ADD . /app ---> 05234a367589 Removing intermediate container e9d33db67914 Step 2 : WORKDIR /app ---> Running in 65b3f40d6228 ---> c656079a833f Removing intermediate container 65b3f40d6228 Step 3 : RUN bundle install ---> Running in c84bd4aa70a0 Don't run Bundler as root. Bundler can ask for sudo ... Fetching gem metadata from https://rubygems.org/......... Fetching version metadata from https://rubygems.org/.. Resolving dependencies... Installing mini_portile 0.6.2 Installing nokogiri 1.6.6.2 with native extensions Installing rack 1.6.4 Using bundler 1.10.5 Bundle complete! 2 Gemfile dep..., 4 gems now installed. Bundled gems are installed into /usr/local/bundle. ---> 68f5dc363171 Removing intermediate container c84bd4aa70a0 Step 4 : EXPOSE 9292 ---> Running in 68c1462c2018 ---> c257c74eb7a8 Removing intermediate container 68c1462c2018 Step 5 : CMD rackup -E none ---> Running in 7e13fd0c26f0 ---> e31f97d2d96a Removing intermediate container 7e13fd0c26f0 Successfully built e31f97d2d96a real 0m57.468s user 0m0.008s sys 0m0.004s
我们可以注意到,即使只是对应用程序进行了单行更改,我们也需要在每次构建 Docker 镜像时都运行 bundle install。这非常低效,而且会中断我们的开发流程,因为构建和运行 Docker 应用程序需要一分钟。对于像我们这样的急性子开发者来说,这简直就像度日如年!
为了优化这一工作流,我们可以将准备应用程序依赖项的阶段与准备实际工件的阶段分开。接下来的步骤展示了如何做到这一点:
-
首先,更新我们的
Dockerfile,进行如下更改:FROM ruby:2.2.2 ADD Gemfile /app/Gemfile WORKDIR /app RUN bundle install ADD . /app EXPOSE 9292 CMD rackup -E none -
接下来,通过以下命令构建重新构建的 Docker 镜像:
dockerhost$ time docker build -t separatedependencies . Sending build context to Docker daemon 4.096 kB Sending build context to Docker daemon ... Step 3 : RUN bundle install ---> Running in b4cbc6803947 Don't run Bundler as root. Bundler can ask for sudo if it is needed, and installing your bundle as root will break this application for all non-root users on this machine. Fetching gem metadata from https://rubygems.org/......... Fetching version metadata from https://rubygems.org/.. Resolving dependencies... Installing mini_portile 0.6.2 Installing nokogiri 1.6.6.2 with native extensions Installing rack 1.6.4 Using bundler 1.10.5 Bundle complete! 2 Gemfile dependencies, 4 gems now installed. Bundled gems are installed into /usr/local/bundle. ---> 5c009ed03934 Removing intermediate container b4cbc6803947 Step 4 : ADD . /app ... Successfully built ff2d4efd233f real 0m57.908s user 0m0.008s sys 0m0.004s -
初始构建时间仍然相同,但请注意在
Step 3生成的镜像 ID。现在,再次尝试更新config.ru并重建镜像,如下所示:dockerhost$ vi config.ru # edit as we please dockerhost$ time docker build -t separatedependencies . Sending build context to Docker daemon 4.096 kB Sending build context to Docker daemon Step 0 : FROM ruby:2.2.2 ---> d763add83c94 Step 1 : ADD Gemfile /app/Gemfile ---> Using cache ---> a7f68475cf92 Step 2 : WORKDIR /app ---> Using cache ---> 203b5b800611 Step 3 : RUN bundle install ---> Using cache ---> 5c009ed03934 Step 4 : ADD . /app ---> 30b2bfc3f313 Removing intermediate container cd643f871828 Step 5 : EXPOSE 9292 ---> Running in a56bfd37f721 ---> 553ae65c061c Removing intermediate container a56bfd37f721 Step 6 : CMD rackup -E none ---> Running in 0ceaa70bee6c ---> 762b7ccf7860 Removing intermediate container 0ceaa70bee6c... Successfully built 762b7ccf7860 real 0m0.734s user 0m0.008s sys 0m0.000s
从前面的输出中我们可以看到,docker build 在 Step 3 之前复用了缓存,因为 Gemfile 没有发生变化。请注意,我们 Docker 镜像的构建时间比通常减少了 80 倍!
这种对 Docker 镜像的重构同样有助于减少部署时间。因为我们的生产环境中的 Docker 主机已经包含了 Docker 镜像在上一版本容器中Step 3的镜像层,所以在应用新版本时,Docker 主机只需拉取Step 4到Step 6的新镜像层即可更新我们的应用。
减少构建上下文大小
假设我们在 Git 版本控制中有一个类似于以下的Dockerfile:

在某些时候,我们会注意到我们的.git目录太大。这可能是由于我们的源代码树中提交了越来越多的代码:
dockerhost$ du -hsc .git
1001M .git
1001M total
现在,当我们构建我们的 Docker 应用时,我们会注意到构建 Docker 应用所需的时间也非常长。请看以下输出:
dockerhost$ time docker build -t hubuser/largecontext .
Sending build context to Docker daemon 1.049 GB
Sending build context to Docker daemon
...
Successfully built 9a61b6b1315e
real 0m17.342s
user 0m0.408s
sys 0m1.360s
如果我们仔细查看前面的输出,我们会看到 Docker 客户端将整个 1GB 的.git目录上传到 Docker 守护进程,因为它是我们构建上下文的一部分。而且,由于这是一个大型构建上下文,在 Docker 守护进程能够开始构建我们的 Docker 镜像之前,它需要花时间接收这些数据。
然而,这些文件在构建我们的应用时并不是必需的。而且,当我们在生产环境中运行应用时,这些与 Git 相关的文件完全不需要。我们可以设置 Docker 忽略一组特定的文件,这些文件在构建 Docker 镜像时并不需要。按照接下来的几个步骤进行优化:
-
在与我们的
Dockerfile相同的目录中创建一个.dockerignore文件,内容如下:.git -
最后,通过执行以下命令重新构建我们的 Docker 镜像:
dockerhost$ time docker build -t hubuser/largecontext . Sending build context to Docker daemon 3.072 kB ... Successfully built 9a61b6b1315e real 0m0.030s user 0m0.004s sys 0m0.004s
请注意,现在构建时间仅通过减小构建上下文的大小就提高了 500 倍以上!
注意
有关如何使用.dockerignore文件的更多信息,请参见docs.docker.com/reference/builder/#dockerignore-file。
使用缓存代理
另一个常见的导致 Docker 镜像构建时间过长的原因是下载依赖项的指令。例如,基于 Debian 的 Docker 镜像需要从 APT 仓库获取包。根据这些包的大小,apt-get install指令的构建时间可能会很长。为了减少这些构建指令的时间,可以使用代理来缓存这些依赖包。一个流行的缓存代理是apt-cacher-ng。本节将描述如何运行和设置它,以改进我们的 Docker 镜像构建工作流。
以下是一个示例Dockerfile,它安装了大量的 Debian 包:
FROM debian:jessie
RUN echo deb http://httpredir.debian.org/debian \
jessie-backports main > \
/etc/apt/sources.list.d/jessie-backports.list
RUN apt-get update &&\
apt-get --no-install-recommends \
install -y openjdk-8-jre-headless
请注意,下面输出中的构建时间非常长,因为这个Dockerfile文件下载了很多与 Java(openjdk-8-jre-headless)相关的依赖和包。运行以下命令:
dockerhost$ time docker build -t beforecaching .
...
Successfully built 476f2ebd35f6
real 3m22.949s
user 0m0.048s
sys 0m0.020s
为了改善构建这个 Docker 镜像的工作流程,我们将使用 apt-cacher-ng 设置一个缓存代理。幸运的是,它已经作为一个即插即用的容器在 Docker Hub 上提供。请按照以下几步准备 apt-cacher-ng:
-
在我们的 Docker 主机上运行以下命令以启动
apt-cacher-ng:dockerhost$ docker run -d -p 3142:3142 sameersbn/apt-cacher-ng -
完成后,我们将使用之前运行的缓存代理,如以下
Dockerfile所示:FROM debian:jessie RUN echo Acquire::http { \ Proxy\"http://dockerhost:3142\"\; \ }\;>/etc/apt/apt.conf.d/01proxy -
通过以下命令行构建我们之前创建的
Dockerfile,将其标记为hubuser/debian:jessie的 Docker 镜像:dockerhost$ docker buid -t hubuser/debian:jessie -
最后,通过更新我们的
Dockerfile来使hubuser/debian:jessie成为新的基础 Docker 镜像,该文件安装了许多 Debian 包作为依赖,例如以下内容:FROM hubuser/debian:jessie RUN echo deb http://httpredir.debian.org/debian \ jessie-backports main > \ /etc/apt/sources.list.d/jessie-backports.list RUN apt-get update && \ apt-get --no-install-recommends \ install -y openjdk-8-jre-headless -
为了确认新的工作流程,运行初始构建以使用以下命令预热缓存:
dockerhost$ docker build -t aftercaching . -
最后,执行以下命令再次构建镜像。但请确保先删除该镜像:
dockerhost$ docker rmi aftercaching dockerhost$ time docker build -t aftercaching . ... Removing intermediate container 461637e26e05 Successfully built 2b80ca0d16fd real 0m31.049s user 0m0.044s sys 0m0.024s
注意,尽管我们没有使用 Docker 的构建缓存,但后续的构建速度更快。这种技术对于我们为团队或组织开发基础 Docker 镜像时非常有用。团队成员在尝试重新构建我们的 Docker 镜像时,将会比之前快 6.5 倍,因为他们可以从我们之前准备的组织缓存代理中下载包。我们在持续集成服务器上的构建也将更快,因为在开发过程中我们已经预热了缓存服务器。
本节简要介绍了如何使用一个非常具体的缓存服务器。以下是我们可以使用的其他几个缓存服务器及其相应的文档页面:
-
apt-cacher-ng:该工具支持缓存 Debian、RPM 以及其他特定于发行版的包,并可以在
www.unix-ag.uni-kl.de/~bloch/acng找到。 -
Sonatype Nexus:该工具支持 Maven、Ruby Gems、PyPI 和 NuGet 包,并且开箱即用。它可以在
www.sonatype.org/nexus上找到。 -
Polipo:这是一款通用缓存代理工具,适用于开发,详情请访问
www.pps.univ-paris-diderot.fr/~jch/software/polipo。 -
Squid:这是另一个流行的缓存代理,能够与其他类型的网络流量一起工作。你可以在
www.squid-cache.org查找相关信息。
减小 Docker 镜像大小
随着我们继续开发 Docker 应用程序,如果不加以注意,镜像的大小往往会变得越来越大。大多数使用 Docker 的人都会发现,团队定制的 Docker 镜像大小至少会增加到 1 GB 或更多。镜像增大意味着构建和部署 Docker 应用程序所需的时间也会增加。因此,我们获取的反馈,尤其是关于我们部署的应用程序结果的反馈,会减少。这削弱了 Docker 的优势,即使我们能够快速迭代开发和部署应用程序。
本节探讨了 Docker 镜像层如何工作以及它们如何影响最终镜像的大小。接下来,我们将学习如何通过利用 Docker 镜像的工作方式来优化这些镜像层。
链接命令
Docker 镜像变大是因为一些指令被添加,而这些指令对于构建或运行镜像并不必要。一个常见的用例是打包元数据和缓存。在安装了构建和运行我们应用程序所需的包之后,这些已下载的包就不再需要。以下是Dockerfile中常见的指令模式(例如在 Docker Hub 中),用于清理Docker 镜像中此类不必要的文件:
FROM debian:jessie
RUN echo deb http://httpredir.debian.org/debian \
jessie-backports main \
> /etc/apt/sources.list.d/jessie-backports.list
RUN apt-get update
RUN apt-get --no-install-recommends \
install -y openjdk-8-jre-headless
RUN rm -rfv /var/lib/apt/lists/*
然而,Docker 镜像的大小基本上是每个单独层的镜像大小之和;这就是联合文件系统的工作原理。因此,清理步骤并不真正删除空间。请看以下命令:
dockerhost$ docker build -t fakeclean .
dockerhost$ docker history fakeclean
IMAGE CREATED CREATED BY SIZE
33c8eedfc24a 2 minutes ago /bin/sh -c rm -rfv /var/lib... 0 B
48b87c35b369 2 minutes ago /bin/sh -c apt-get install ... 318.6 MB
dad9efad9e2d 4 minutes ago /bin/sh -c apt-get update 9.847 MB
a8f7bf731a7d 5 minutes ago /bin/sh -c echo 'deb http:/... 61 B
9a61b6b1315e 6 days ago /bin/sh -c #(nop) CMD "/bi... 0 B
902b87aaaec9 6 days ago /bin/sh -c #(nop) ADD file:... 125.2 MB
没有所谓的“负”层大小。因此,Dockerfile 中的每条指令只能保持镜像大小不变或增加它。而且,由于每个步骤还会引入一些元数据,最终的大小会不断增加。
为了减少总镜像大小,清理步骤应在同一镜像层中执行。因此,解决方案是将先前多个指令的命令链接成一个。由于 Docker 使用/bin/sh来运行每条指令,我们可以使用 Bourne shell 的&&运算符来执行链式操作,如下所示:
FROM debian:jessie
RUN echo deb http://httpredir.debian.org/debian \
jessie-backports main \
> /etc/apt/sources.list.d/jessie-backports.list
RUN apt-get update && \
apt-get --no-install-recommends \
install -y openjdk-8-jre-headless && \
rm -rfv /var/lib/apt/lists/*
请注意,现在每个单独层的大小都小得多。随着各个层的大小减少,总镜像大小也随之减小。现在,运行以下命令并查看输出:
dockerhost$ docker build -t trueclean .
dockerhost$ docker history trueclean
IMAGE CREATED CREATED BY SIZE
03d0b15bad7f About a minute ago /bin/sh -c apt-get update... 318.6 MB
a8f7bf731a7d 9 minutes ago /bin/sh -c echo deb h... 61 B
9a61b6b1315e 6 days ago /bin/sh -c #(nop) CMD... 0 B
902b87aaaec9 6 days ago /bin/sh -c #(nop) ADD... 125.2 MB
分离构建和部署镜像
Docker 镜像中的另一个不必要文件来源是构建时依赖项。源代码库,例如编译器和源头头文件,只有在 Docker 镜像内构建应用程序时才是必要的。一旦应用程序构建完成,这些文件就不再需要,因为运行应用程序只需要编译后的二进制文件和相关的共享库。
例如,构建以下应用程序,现已准备好部署到我们在云端准备的 Docker 主机。以下源代码树是一个用 Go 编写的简单 Web 应用程序:
![分离构建和部署镜像
以下是描述应用程序的hello.go文件内容:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
以下对应的Dockerfile展示了如何构建源代码并运行生成的二进制文件:
FROM golang:1.4.2
ADD hello.go hello.go
RUN go build hello.go
EXPOSE 8080
ENTRYPOINT ["./hello"]
在接下来的几个步骤中,我们将展示 Docker 应用程序镜像如何变得越来越大:
-
首先,构建 Docker 镜像并注意其大小。我们将运行以下命令:
dockerhost$ docker build -t largeapp . dockerhost$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE largeapp latest 47a64e67fb81 4 minute... 523.1 MB golang 1.4.2 124e2127157f 5 days ago 517.3 MB -
现在,将此与实际运行的应用程序的大小进行比较,如下所示:
dockerhost$ docker run --name large -d largeapp dockerhost$ docker exec -it large/bin/ls -lh total 5.6M drwxrwxrwx 2 root root 4.0K Jul 14 06:26 bin -rwxr-xr-x 1 root root 5.6M Jul 20 02:40 hello -rw-r--r-- 1 root root 231 Jul 18 05:59 hello.go drwxrwxrwx 2 root root 4.0K Jul 14 06:26 src
编写 Go 应用程序以及一般编译代码的一个优点是,我们可以生成一个易于部署的单一二进制文件。Docker 镜像的剩余大小来自基础 Docker 镜像中不必要的文件。我们可以注意到,基础 Docker 镜像带来的巨大开销将总镜像大小增加了通常的 100 倍。
我们还可以通过仅打包最终的 hello 二进制文件和一些依赖的共享库,来优化部署到生产环境的 Docker 镜像。按照接下来的步骤进行优化:
-
首先,通过以下命令将二进制文件从运行中的容器复制到我们的 Docker 主机:
dockerhost$ docker cp -L large:/go/hello ../build -
如果前面的库是静态二进制文件,我们现在就完成了,可以继续下一步。然而,Go 工具链默认构建共享二进制文件。为了让二进制文件正常运行,它需要共享库。运行以下命令列出它们:
dockerhost$ docker exec -it large /usr/bin/ldd hello linux-vdso.so.1 (0x00007ffd84747000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f32f3793000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f32f33ea000) /lib64/ld-linux-x86-64.so.2 (0x00007f32f39b0000) -
接下来,将所有必需的共享库保存到我们的 Docker 主机上。执行以下
docker cp -L命令可以完成此操作:dockerhost$ docker cp -L large:/lib/x86_64-linux-gnu/libpthread.so.0 \ ../build dockerhost$ docker cp -L large:/lib/x86_64-linux-gnu/libc.so.6 \ ../build dockerhost$ docker cp -L large:/lib64/ld-linux-x86-64.so.2 \ ../build -
创建一个新的
Dockerfile来构建这个“仅二进制”镜像。请注意,ADD指令在以下输出中如何重建hello应用程序期望的共享库路径:FROM scratch ADD hello /app/hello ADD libpthread-2.19.so \ /lib/x86_64-linux-gnu/libpthread.so.0 ADD libc-2.19.so /lib/x86_64-linux-gnu/libc.so.6 ADD ld-2.19.so /lib64/ld-linux-x86-64.so.2 EXPOSE 8080 ENTRYPOINT ["/app/hello"] -
现在我们已经拥有了运行新的“仅二进制”Docker 镜像所需的所有文件。最终,我们目录树中的文件将类似于以下截图:
![分离构建和部署镜像]()
-
现在,使用以下
build/Dockerfile构建可部署的binaryDocker 镜像。构建出来的镜像现在会更小:dockerhost$ docker build -t binary . dockerhost$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE binary latest 45c327c815 seconds ago 7.853 MB largeapp latest 47a64e67f 52 minutes ago 523.1 MB golang 1.4.2 124e21271 5 days ago 517.3 MB
同样的方法也可以用来构建其他编译应用程序,如通常使用 ./configure && make && make install 组合安装的软件。我们也可以对 Python、Ruby 或 PHP 等解释型语言做同样的事情。然而,从“构建”Ruby Docker 镜像创建“运行时”Ruby Docker 镜像需要稍微多一些工作。进行这种优化的一个好时机是,当我们的应用程序交付时间太长,因为镜像太大,不适合可持续的开发工作流。
总结
在本章中,你了解了 Docker 如何构建镜像,并将其应用于改善多个因素,如部署时间、构建时间和镜像大小。本章所述的技术并不全面;随着越来越多的人发现如何将 Docker 用于他们的应用程序,肯定会出现更多的方式来实现这些目标。随着 Docker 本身的成熟和更多功能的开发,也会有更多的技术出现。进行这些优化时,最重要的指导因素是问问自己,我们是否真正获得了使用 Docker 的好处。一些值得提问的好问题如下:
-
部署时间是否得到了改善?
-
开发团队是否能从运营团队运行我们应用程序时学到的经验中快速获得反馈?
-
我们是否能够足够快速地迭代新功能,以便纳入我们从客户使用应用程序中发现的新反馈?
牢记我们使用 Docker 的动机和目标,我们可以找到自己改进工作流的方法。
使用一些前述的优化将需要更新我们的 Docker 主机配置。为了能够在大规模上管理多个 Docker 主机,我们需要某种形式的自动化来进行它们的配置和提供。在下一章中,我们将讨论如何使用配置管理软件自动化设置 Docker 主机。
第三章:使用 Chef 自动化 Docker 部署
到目前为止,我们已经了解了 Docker 生态系统的各个方面。Docker 主机有几个配置参数。然而,手动配置 Docker 主机是一个缓慢且容易出错的过程。如果我们没有自动化策略,Docker 部署在生产环境中的扩展就会遇到问题。
在本章中,我们将学习配置管理的概念,以解决这个问题。我们将使用 Chef,一款配置管理软件,来大规模管理 Docker 主机。本章将涵盖以下主题:
-
配置管理的重要性
-
Chef 简介
-
自动配置 Docker 主机
-
部署 Docker 容器
-
替代自动化工具
配置管理简介
Docker 引擎有几个需要调整的参数,如 cgroups、内存、CPU、文件系统、网络等。确定哪些 Docker 容器运行在哪些 Docker 主机上是配置的另一个方面。Docker 容器本身也需要使用不同的 cgroups 设置、共享卷、链接容器、公用端口等进行配置。找到优化应用程序的参数组合需要一些时间。
将所有前述配置项复制到另一个 Docker 主机是手动执行非常困难的。我们可能无法记住创建主机所需的所有步骤,而且这也是一个容易出错且缓慢的过程。创建一份“文档”来记录这个过程也没有帮助,因为这样的文档往往随着时间的推移而变得过时。
如果我们不能及时、可靠地配置新的 Docker 主机,就没有空间扩展我们的 Docker 应用程序。以一致和快速的方式准备和配置 Docker 主机同样重要。否则,Docker 为我们的应用程序创建容器包的能力很快就会变得毫无意义。
配置管理是一种管理我们应用程序各方面变化的策略,它报告并审计我们系统所做的更改。这不仅仅适用于开发应用程序的过程中。在我们的案例中,它记录了所有 Docker 主机的更改以及 Docker 容器本身的运行情况。从某种意义上说,Docker 实现了我们应用程序的配置管理的以下几个方面:
-
Docker 容器可以复制我们应用程序的任何环境,从开发到预发布、测试、生产等环境。
-
构建 Docker 镜像是一种简单的方式,能够对应用程序进行更改并将其部署到所有环境。
-
Docker 使得所有团队成员都能获得有关我们应用程序的信息,并进行所需的更改,以高效地将软件交付给客户。通过查看
Dockerfile,他们可以了解应用程序的哪个部分需要更新,以及为了正常运行,应用程序需要哪些内容。 -
Docker 跟踪我们环境中任何 Docker 镜像的变化。然后,它将变化追溯到相应版本的
Dockerfile。它追踪变化的内容、是谁做的、以及何时做的。
然而,运行我们应用程序的 Docker 主机怎么办?就像 Dockerfile 允许我们在版本控制中管理我们应用的环境一样,配置管理工具也可以用代码描述我们的 Docker 主机。它简化了创建 Docker 主机的过程。在扩展我们的 Docker 应用时,我们可以轻松地从头开始重新创建一个新的 Docker 主机。当发生硬件故障时,我们可以从已知配置中在其他地方启动新的 Docker 主机。如果我们想部署一个新的 Docker 容器版本,只需更新 Docker 主机的配置代码以指向新的镜像。配置管理使我们能够大规模管理 Docker 部署。
使用 Chef
Chef 是一个配置管理工具,它提供了一种领域特定语言来建模我们基础设施的配置。我们基础设施中的每个配置项都被建模为一个资源。资源基本上是一个 Ruby 方法,它接受一个代码块中的多个参数。以下示例资源描述了安装 docker-engine 包:
package 'docker-engine' do
action :install
end
这些资源随后将一起写入 Ruby 源文件中,称为食谱(recipes)。当在服务器(在我们的案例中是 Docker 主机)上运行食谱时,所有定义的资源将被执行,以达到所需的状态配置。
一些 Chef 食谱可能依赖于其他补充项目,如配置模板和其他食谱。所有这些信息都与食谱一起收集在烹饪书中。烹饪书是将配置和策略分发到我们服务器的基本单元。
我们将编写 Chef 食谱,以表示我们 Docker 主机的所需状态配置。我们的食谱将组织在 Chef 烹饪书中,并分发到我们的基础设施中。然而,首先,让我们准备我们的 Chef 环境,以便开始用食谱描述基于 Docker 的基础设施。Chef 环境由三部分组成:
-
一个 Chef 服务器
-
一个工作站
-
一个节点
接下来的几个子章节将详细描述每个组件。然后,我们将设置它们以准备我们的 Chef 环境,以便管理我们的 Docker 主机。
注意
设置 Chef 环境的更多细节超出了本章的范围。更多信息可以在 Chef 文档网站 docs.chef.io 找到。
注册一个 Chef 服务器
Chef 服务器是烹饪书和其他治理我们整个基础设施的政策项的中央存储库。它包含关于我们所管理的基础设施的元数据。在我们的案例中,Chef 服务器包含了烹饪书、策略和关于我们的 Docker 主机的元数据。
要准备一个 Chef 服务器,我们只需要注册一个托管的 Chef 服务器账户。免费的 Chef 服务器账户允许我们在基础架构中管理最多五个节点。请按照以下步骤准备一个托管的 Chef 服务器账户:
-
访问
manage.chef.io/signup,并按照以下截图填写账户信息:![注册 Chef 服务器]()
-
创建用户账户后,托管的 Chef 服务器会提示我们创建一个组织。组织用于管理 Chef 服务器的基于角色的访问控制。通过填写表单中的详细信息并点击创建组织按钮来创建一个组织:
![注册 Chef 服务器]()
-
我们现在几乎完成了托管 Chef 服务器账户的创建。最后,点击下载启动工具包。这将下载一个包含我们启动 chef-repo 的 zip 文件。我们将在下一部分详细讨论 chef-repo。
![注册 Chef 服务器]()
设置我们的工作站
我们的 Chef 环境的第二部分是工作站。工作站用于与 Chef 服务器进行交互。这是我们进行大部分准备工作并编写代码以发送到 Chef 服务器的地方。在工作站中,我们将准备基础架构的配置项,并将它们放入 Chef 仓库中。
Chef 仓库包含了与 Chef 服务器交互和同步所需的所有信息。它包含用于认证和与 Chef 服务器交互的私钥以及其他配置文件。这些文件将位于我们 Chef 仓库中的.chef目录下。它还包含我们稍后编写并与 Chef 服务器同步的食谱(cookbooks),这些文件位于cookbooks/目录下。Chef 仓库中还有其他文件和目录,如数据包(data bags)、角色(roles)和环境(environments)等。不过,目前了解食谱和认证文件就足够了,它们可以帮助我们配置 Docker 主机。
你还记得我们在上一部分下载的启动工具包吗?解压这个文件,提取我们的 chef-repo。我们应该在目录树中看到以下文件:

工作站中的另一个重要组成部分是 Chef 开发工具包。它包含了所有必要的程序,帮助我们读取 chef-repo 中的所有配置并与 Chef 服务器进行交互。Chef 开发工具包中还提供了创建、开发和测试食谱的便捷程序。我们将在本章的其余部分中使用开发工具包中的各种程序。
现在,根据我们工作站的操作系统平台,从downloads.chef.io/chef-dk下载 Chef 开发工具包。

接下来,打开下载的安装程序。根据我们的平台提示安装 Chef 开发工具包。最后,通过以下命令确认安装成功:
$ chef -v
Chef Development Kit Version: 0.6.2
chef-client version: 12.3.0
berks version: 3.2.4
kitchen version: 1.4.0
现在我们已经设置好了工作站,接下来让我们进入chef-repo/目录,准备 Chef 环境的最后一个组件。
启动节点
我们 Chef 环境的最后一部分是节点。节点是任何由 Chef 管理的计算机。它可以是物理机器、虚拟机、云中的服务器或网络设备。在我们的案例中,我们的 Docker 主机就是一个节点。
任何节点被 Chef 管理的核心组件是 chef-client。它连接到 Chef 服务器,下载必要的文件以将我们的节点带到其配置状态。当 chef-client 在我们的节点上运行时,它执行以下步骤:
-
它将节点注册并认证到 Chef 服务器。
-
它收集我们节点的系统信息以创建节点对象。
-
然后,它同步我们节点所需的 Chef 食谱。
-
它通过加载我们节点所需的配方来编译资源。
-
接下来,它执行所有资源并执行相应的操作以配置我们的节点。
-
最后,它将 chef-client 运行的结果报告回 Chef 服务器以及其他已配置的通知端点。
现在,让我们通过从工作站启动 Docker 主机来准备它作为一个节点。启动过程将安装并配置 chef-client。运行以下命令开始启动过程:
$ knife bootstrap dockerhost
...
Connecting to dockerhost
dockerhost Installing Chef Client...
...
dockerhost trying wget...
dockerhost Comparing checksum with sha256sum...
dockerhost Installing Chef 12.3.0
dockerhost installing with dpkg...
...
dockerhost Thank you for installing Chef!
dockerhost Starting first Chef Client run...
dockerhost Starting Chef Client, version 12.3.0
dockerhost Creating a new client identity for dockerhost using the validator key.
dockerhost resolving cookbooks for run list: []
dockerhost Synchronizing Cookbooks:
dockerhost Compiling Cookbooks...
dockerhost ... WARN: Node dockerhost has an empty run list.
dockerhost Converging 0 resources
dockerhost
dockerhost Running handlers:
dockerhost Running handlers complete
dockerhost Chef Client finished, 0/0 resources updated in 12.78s
如我们在前面的命令中所见,启动过程做了两件事。首先,它在我们的 Docker 主机节点上安装并配置了 chef-client。接下来,它启动了 chef-client 以将其期望的状态与我们的 Chef 服务器进行同步。由于我们尚未为 Docker 主机分配任何设计状态,因此它没有执行任何操作。
注意
我们可以根据需要定制此启动过程。关于如何使用knife bootstrap的更多信息,请参见docs.chef.io/knife_bootstrap.html。
在某些情况下,云服务提供商已经深度集成了 Chef。因此,我们将不使用knife bootstrap,而是使用云服务提供商的 SDK。在那里,我们只需指定希望集成 Chef 即可。我们将为其提供必要的信息,如 chef-client 的client.rb配置和验证密钥的凭据。
我们的 Docker 主机现在已正确注册到 Chef 服务器,准备获取其配置。请访问manage.chef.io/organizations/dockerorg/nodes/dockerhost查看我们的 Docker 主机作为 Chef 环境中的一个节点,如下图所示:

配置 Docker 主机
现在我们已经正确设置了所有 Chef 环境组件,可以开始编写 Chef 配方,实际描述我们的 Docker 主机应该具备什么样的配置。此外,我们将通过利用 Chef 生态系统中现有的 Chef 烹饪书来提升我们的生产力。由于 Docker 是一个流行的基础设施堆栈,用于部署容器,我们可以使用一些现成的烹饪书来配置我们的 Docker 主机。社区提供的 Chef 烹饪书可以在 Chef 超市中找到。我们可以访问supermarket.chef.io来发现其他可以直接使用的烹饪书。
在本节中,您将学习如何编写 Chef 配方并将其应用于我们的节点。请按照以下步骤为我们的 Docker 主机编写配方:
-
使用 Chef 开发工具包的
chef generate cookbook命令来生成我们烹饪书的样板文件。在进入烹饪书目录后,发出以下命令:$ cd cookbooks $ chef generate cookbook dockerhost标准的烹饪书目录结构将类似于以下截图:
![配置 Docker 主机]()
-
接下来,我们将准备编辑我们的烹饪书。通过以下命令将工作目录更改为我们之前创建的烹饪书所在的目录:
$ cd dockerhost -
从 Chef 超市安装以下烹饪书作为依赖项:
apt和docker。这些烹饪书提供了可以在我们的配方中使用的额外资源定义。稍后我们将使用它们作为构建块来设置我们的 Docker 主机。要添加依赖项,请更新metadata.rb文件,如下所示:name 'dockerhost' maintainer 'The Authors' maintainer_email 'you@example.com' license 'all_rights' description 'Installs/Configures dockerhost' long_description 'Installs/Configures dockerhost' version '0.1.0' depends 'apt', '~> 2.7.0' depends 'docker', '~> 0.40.3'注意
metadata.rb文件提供有关我们的 Chef 烹饪书的元数据。元数据中的信息为 Chef 服务器提供提示,以便烹饪书可以正确部署到我们的节点。有关如何配置 Chef 烹饪书的元数据,请访问docs.chef.io/config_rb_metadata.html。 -
现在我们已经声明了依赖关系,可以通过发出以下命令来下载它们:
$ berks install Resolving cookbook dependencies... Fetching 'dockerhost' from source at . Fetching cookbook index from https://supermarket.chef.io... Installing apt (2.7.0) Installing docker (0.40.3) Using dockerhost (0.1.0) from source at . -
最后,我们将编写与
blog.docker.com/2015/07/new-apt-and-yum-repos中找到的安装说明等效的 Chef 配方。我们将使用之前添加的apt 依赖烹饪书提供的apt_repository资源。然后,将以下内容添加到recipes/default.rb文件中:apt_repository 'docker' do uri 'http://apt.dockerproject.org/repo' components %w(debian-jessie main) keyserver 'p80.pool.sks-keyservers.net' key '58118E89F3A912897C070ADBF76221572C52609D' cache_rebuild true end package 'docker-engine'
现在,我们已经完成了 dockerhost/ Chef 烹饪书的准备工作。最后一步是将其应用于我们的 Docker 主机,以便它可以选择所需的配置。请按照以下剩余步骤进行操作:
-
首先,将 Chef 烹饪书上传到我们的 Chef 服务器。请注意,在以下命令的输出中,我们依赖的
apt和docker烹饪书也会自动上传:$ berks upload Uploaded apt (2.7.0) to: 'https://api.opscode.../dockerorg' Uploaded docker (0.40.3) to: 'https://api.ops.../dockerorg' Uploaded dockerhost (0.1.0) to: 'https://api.opscode.com:443/organizations/dockerorg' -
接下来,通过设置其
run_list来将我们之前编写的dockerhost配方应用于节点(即 Docker 主机),使用以下命令:$ knife node run_list set dockerhost dockerhost dockerhost: run_list: recipe[dockerhost] -
最后,在
dockerhost上运行 chef-client。chef-client 将获取 Docker 主机的节点对象,并应用我们在前面步骤中所设置的期望状态配置,如下所示:$ ssh dockerhost dockerhost$ sudo chef-client Starting Chef Client, version 12.3.0 resolving cookbooks for run list: ["dockerhost"] Synchronizing Cookbooks: - apt - dockerhost - docker Compiling Cookbooks... Converging 2 resources Recipe: dockerhost::default * apt_repository[docker] action add * execute[install-key 58118E89F3A912897C...] action run ... * apt_package[docker-engine] action install - install version 1.7.1-0~j... of package docker-engine Running handlers: Running handlers complete Chef Cl... finished, 6/7 resources updated in 24.69 seconds
现在,我们已经通过 Chef 在 Docker 主机上安装并配置了 Docker。每当我们需要添加另一个 Docker 主机时,只需在云服务提供商中创建另一个服务器,并使用之前编写的 dockerhost Chef 配方进行初始化配置。当我们想要更新所有 Docker 主机中 Docker 守护进程的配置时,只需更新 Chef cookbook 并重新运行 chef-client。
提示
在生产环境中,安装配置管理软件的目的是让我们的 Docker 主机无需登录即可进行配置更新。手动运行 chef-client 只是自动化的一半。
我们希望将 chef-client 作为守护进程运行,这样我们就不必每次执行更新时都运行它。chef-client 守护进程将定期轮询 Chef 服务器,检查是否有任何更新需要应用到它所管理的节点。默认情况下,轮询间隔设置为 30 分钟。
有关如何将 chef-client 配置为守护进程的更多信息,请参阅 Chef 文档:docs.chef.io/chef_client.html。
部署 Docker 容器
管理大规模 Docker 的下一步是自动化将 Docker 容器部署到我们的 Docker 主机池。到目前为止,我们已经构建了一些 Docker 应用程序,并且大致了解这些容器如何相互通信并相互消费。Chef 配方可以用来在代码中表示这种架构拓扑,这对于大规模管理我们的整个应用程序和基础设施至关重要。我们可以识别出需要运行的 Docker 容器,并了解每个容器如何与其他容器连接。我们可以确定 Docker 容器的部署位置。将整个架构用代码表示,可以为我们的应用程序制定编排策略。
在本节中,我们将创建一个 Chef 配方,用于编排将 Nginx Docker 镜像部署到我们的 Docker 主机。我们将使用在上一节中添加的 docker cookbook 提供的 Chef 资源来配置我们的 Docker 主机。请按照以下步骤进行部署:
-
首先,创建我们将要使用的 Chef 配方。以下命令将在我们的
dockerhost/cookbook 中创建recipes/containers.rb配方文件:$ chef generate recipe . containers -
接下来,将官方 Nginx Docker 镜像从
registry.hub.docker.com/_/nginx拉取到我们的 Docker 主机。请在recipes/containers.rb中编写以下代码:docker_image 'nginx' do tag '1.9.3' end -
下载 Docker 镜像后,配置 Docker 主机以运行容器。从
docker菜谱的版本 0.40.3 开始,我们需要指定我们的 Debian Jessie Docker 主机部署使用systemd作为其init系统。还需要将以下内容添加到recipes/containers.rb中:node.set['docker']['container_init_type'] = 'systemd' directory '/usr/lib/systemd/system' docker_container 'nginx' do tag '1.9.3' container_name 'webserver' detach true port '80:80' end注意
docker_container和docker_image有其他一些选项,我们可以调整这些选项来指定我们想对容器执行的操作。docker菜谱还包含其他资源,可以与我们的 Docker 主机进行交互。有关选项和进一步使用的信息,可以在其项目页面github.com/bflad/chef-docker找到。 -
接下来,我们将为发布准备新版本的菜谱。为此,请在
metadata.rb中更新版本信息,具体如下:name 'dockerhost' maintainer 'The Authors' maintainer_email 'you@example.com' license 'all_rights' description 'Installs/Configures dockerhost' long_description 'Installs/Configures dockerhost' version '0.2.0' depends 'apt', '~> 2.7.0' depends 'docker', '~> 0.40.3' -
更新
Berksfile.lock文件,以固定我们将在下一步上传到 Chef 服务器的所有菜谱的版本。输入以下命令以执行更新:$ berks install Resolving cookbook dependencies... Fetching 'dockerhost' from source at . Fetching cookbook index from https://supermarket.chef.io... Using dockerhost (0.2.0) from source at . Using apt (2.7.0) Using docker (0.40.3) -
现在,我们的新菜谱的所有工件已经准备好,我们将输入以下命令将更新后的菜谱上传到我们的 Chef 服务器。请注意,
berks upload命令会自动识别出只有dockerhost菜谱需要更新,并跳过上传apt和docker菜谱:$ berks upload Skipping apt (2.7.0) (frozen) Skipping docker (0.40.3) (frozen) Uploaded dockerhost (0.2.0) to: 'https://ap.../dockerorg' -
接下来,将
recipes/containers.rb添加到 Docker 主机的运行列表中。输入以下命令以更新表示 Docker 主机的节点:$ knife node run_list add dockerhost dockerhost::containers dockerhost: run_list: recipe[dockerhost] recipe[dockerhost::containers] -
最后,重新运行 chef-client 以获取 Docker 主机的新配置。如果我们将 chef-client 配置为以守护进程方式运行,我们也可以等待 chef-client 的重新运行。执行以下命令:
$ ssh dockerhost dockerhost$ sudo chef-client Starting Chef Client, version 12.3.0 resolving cookbooks for run list: ["dockerhost", "dockerhost::containers"] Synchronizing Cookbooks: - dockerhost - apt - docker Compiling Cookbooks... Converging 5 resources Recipe: dockerhost::default ... Recipe: dockerhost::containers * docker_image[nginx] action pull * directory[/usr/lib/systemd/system] action create - create new directory /usr/lib/systemd/system * docker_container[nginx] action run * template[/usr/lib/.../webserver.socket] action create ... * service[webserver] action enable (up to date) * service[webserver] action start - start service service[webserver] * template[webserver.socket] action nothing ... * template[webserver.service] action nothing ... * service[webserver] action nothing ... Running handlers: Running handlers complete Chef Client finished, 6/10 resources updated in 42.83 seconds
我们现在已经让 Docker 主机运行了 nginx Docker 容器。我们可以通过访问http://dockerhost来确认它是否正常工作。我们应该能够看到如下图所示的页面:

其他方法
还有其他通用的配置管理工具,可以配置我们的 Docker 主机。以下是我们可以使用的其他工具的简短列表:
-
Puppet:请参阅
puppetlabs.com。 -
Ansible:可以在
ansible.com找到。 -
CFEngine:可以在
cfengine.com找到。 -
SaltStack:可以在
saltstack.com找到更多信息。 -
Docker 机器:这是一个非常特定的配置管理工具,允许我们在基础设施中配置和管理 Docker 主机。有关 Docker 机器的更多信息,请参阅 Docker 文档页面
docs.docker.com/machine。
如果我们不想管理 Docker 主机基础设施,我们可以使用 Docker 托管服务。流行的云服务提供商已经开始提供作为预配置云镜像的 Docker 主机,我们可以使用它们。其他提供商则提供更全面的解决方案,让我们能够将所有云中的 Docker 主机当作一个虚拟 Docker 主机来交互。以下是一些流行的云服务提供商及其与 Docker 生态系统集成的链接:
-
Google Container Engine (
cloud.google.com/container-engine) -
Amazon EC2 容器服务 (
aws.amazon.com/documentation/ecs) -
Azure Docker VM 扩展 (
github.com/Azure/azure-docker-extension) -
Joyent 弹性容器服务 (
www.joyent.com/public-cloud)
在部署 Docker 容器方面,有几种容器工具可以帮助我们实现这一目标。它们提供 API 来运行和部署我们的 Docker 容器。某些提供的 API 甚至与 Docker 引擎本身兼容。这让我们可以像操作一个虚拟 Docker 主机一样与 Docker 主机池进行交互。以下是一些可以帮助我们协调容器部署到 Docker 主机池的工具:
-
Docker Swarm (
www.docker.com/docker-swarm) -
Google Kubernetes (
kubernetes.io) -
CoreOS fleet (
coreos.com/fleet) -
Mesophere Marathon (
mesosphere.github.io/marathon) -
SmartDataCenter Docker 引擎 (
github.com/joyent/sdc-docker)
然而,我们仍然需要像 Chef 这样的配置管理工具来部署和配置我们的编排系统,以管理我们 Docker 主机池之上的系统。
总结
在本章中,我们学习了如何自动化配置我们的 Docker 部署。使用 Chef 使我们能够配置和提供多个 Docker 主机的规模。它还使我们能够为应用程序部署并协调 Docker 容器到我们的 Docker 主机池。从此以后,你可以编写 Chef 配方来保持本书中你将学习到的所有 Docker 优化技术。
在下一章中,我们将介绍如何对整个 Docker 基础设施和应用程序进行监控。这将帮助我们进一步了解如何优化 Docker 部署,以提高性能。
第四章:监控 Docker 主机和容器
我们现在已经知道了优化 Docker 部署的一些方法,也知道如何通过扩展来提高性能。但是我们如何知道我们的调优假设是否正确呢?能够监控我们的 Docker 基础设施和应用程序对于确定我们何时以及为什么需要进行优化非常重要。衡量系统的性能可以帮助我们识别其扩展极限,并据此进行调优。
除了监控 Docker 的低层次信息,衡量应用程序的业务相关性能同样重要。通过追踪应用程序的价值流,我们可以将业务相关的指标与系统层面的指标关联起来。这样,Docker 开发和运维团队就能够向业务同事展示 Docker 如何帮助节省组织成本并提高业务价值。
在本章中,我们将涵盖以下关于能够大规模监控 Docker 基础设施和应用程序的主题:
-
监控的重要性
-
在 Graphite 中收集监控数据
-
使用 collectd 监控 Docker
-
在 ELK 堆栈中合并日志
-
发送来自 Docker 的日志
监控的重要性
监控非常重要,因为它提供了关于我们构建的 Docker 部署的反馈来源。它可以回答关于我们的应用程序的一些问题,从低层次的操作系统性能到高层次的业务目标。通过在 Docker 主机中插入适当的监控工具,我们可以识别系统的状态。我们可以利用这个反馈源来判断我们的应用程序是否按最初计划的方式运行。
如果我们的初始假设不正确,我们可以利用反馈数据修正我们的计划,并通过调优 Docker 主机和容器或更新正在运行的 Docker 应用程序来调整系统。我们还可以使用相同的监控过程来识别系统部署到生产环境后出现的错误和 bug。
Docker 具有内置的日志记录和监控功能。默认情况下,Docker 主机会将 Docker 容器的标准输出和错误流存储为 JSON 文件,路径为 /var/lib/docker/<container_id>/<container_id>-json.log。docker logs 命令会请求 Docker 引擎守护进程读取此处文件的内容。
另一个监控工具是 docker stats 命令。该命令查询 Docker 引擎远程 API 的 /containers/<container_id>/stats 端点,报告关于运行容器的控制组的运行时统计信息,包括其 CPU、内存和网络使用情况。以下是 docker stats 命令报告上述指标的示例输出:
dockerhost$ docker run --name running –d busybox \
/bin/sh -c 'while true; do echo hello && sleep 1; done' dockerhost$ docker stats running
CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O
running 0.00% 0 B/518.5 MB 0.00% 17.06 MB/119.8 kB
内置的 docker logs 和 docker stats 命令非常适合用来监控我们用于开发和小规模部署的 Docker 应用程序。当我们进入生产级别的 Docker 部署,管理数十个、数百个甚至数千个 Docker 主机时,这种方法就不再具备扩展性。因为我们无法登录到每一个 Docker 主机并输入 docker logs 和 docker stats。
一一操作还使得我们很难对整个 Docker 部署形成更全面的了解。而且,并不是每一个关心我们 Docker 应用性能的人都能登录到我们的 Docker 主机。仅仅处理应用业务方面的同事可能希望了解我们的 Docker 部署如何帮助组织实现目标,但他们不一定希望学习如何登录并在我们的基础设施中输入 Docker 命令。
因此,能够将我们 Docker 部署中的所有事件和度量数据整合到一个集中的监控基础设施中是非常重要的。它通过提供一个单一的查询点来让我们的运维团队能够了解系统的运行情况,从而帮助我们的运维实现规模化。一个集中的仪表盘还使得开发和运维团队以外的人,比如业务同事,能够访问我们的监控系统提供的反馈信息。接下来的章节将向你展示如何整合来自 docker logs 的信息,并收集来自数据源(如 docker stats)的统计数据。
收集度量数据到 Graphite
要开始监控我们的 Docker 部署,首先我们必须设置一个端点,将我们的监控数据发送到那里。Graphite 是一个流行的堆栈,用于收集各种度量数据。它的纯文本协议因其简单性而广受欢迎,许多第三方工具都能理解这个简单的协议。稍后我们将展示,在设置完 Graphite 后,如何轻松地向其发送数据。
Graphite 的另一个特点是它可以将收集到的数据呈现为图表。然后我们可以将这些图表整合到一起,构建一个仪表盘。最终我们构建的仪表盘将展示我们需要监控 Docker 应用程序的各种信息。
在本节中,我们将设置 Graphite 的以下组件,以创建一个最小化的堆栈:
-
carbon-cache:这是接收网络上传输的度量数据的 Graphite 组件。它实现了前面描述的简单纯文本协议。它还可以监听一种基于二进制的协议,叫做 pickle 协议,这是一种更先进但更小巧且优化过的格式,用于接收度量数据。
-
whisper:这是一个基于文件的有界时间序列数据库,carbon-cache 在其中持久化它接收到的指标。它的有界或固定大小特性使其成为监控的理想解决方案。随着时间的推移,我们监控的指标将不断积累。因此,我们的数据库大小将不断增加,你需要对其进行监控!然而,在实践中,我们大多数情况下关心的是在固定时间点之前监控我们的应用程序。基于这一假设,我们可以提前规划 whisper 数据库的资源需求,随着 Docker 应用程序的运行而不必过多考虑它。
-
graphite-web:该组件读取 whisper 数据库以渲染图表和仪表板。它还经过优化,可以通过查询 carbon-cache 端点实时创建此类可视化,显示尚未在 whisper 数据库中持久化的数据。
注意
carbon 中还有其他组件,如 carbon-aggregator 和 carbon-relay。这些组件对于在你测量的指标数量增长时有效地扩展 Graphite 至关重要。有关这些组件的更多信息,请访问 github.com/graphite-project/carbon。目前,我们将专注于仅部署 carbon-cache 来创建一个简单的 Graphite 集群。
接下来的几个步骤描述了如何部署 carbon-cache 和 whisper 数据库:
-
首先,为 carbon 准备一个 Docker 镜像,以便在我们的 Docker 主机部署中使用。创建以下
Dockerfile来准备此镜像:FROM debian:jessie RUN apt-get update && \ apt-get --no-install-recommends \ install -y graphite-carbon ENV GRAPHITE_ROOT /graphite ADD carbon.conf /graphite/conf/carbon.conf RUN mkdir -p $GRAPHITE_ROOT/conf && \ mkdir -p $GRAPHITE_ROOT/storage && \ touch $GRAPHITE_ROOT/conf/storage-aggregation.conf && \ touch $GRAPHITE_ROOT/conf/storage-schemas.conf VOLUME /whisper EXPOSE 2003 2004 7002 ENTRYPOINT ["/usr/bin/twistd", "--nodaemon", \ "--reactor=epoll", "--no_save"] CMD ["carbon-cache"] -
接下来,构建我们之前创建的
Dockerfile作为hubuser/carbon镜像,如下所示:dockerhost$ docker build -t hubuser/carbon . -
在
carbon.conf配置文件中,我们将配置 carbon-cache 使用 Docker 卷/whisper作为 whisper 数据库。以下是描述此设置的内容:[cache] CARBON_METRIC_INTERVAL = 0 LOCAL_DATA_DIR = /whisper -
在构建
hubuser/carbon镜像后,我们将通过创建数据容器来准备一个 whisper 数据库。输入以下命令来完成此操作:dockerhost$ docker create --name whisper \--entrypoint='whisper database for graphite' \ hubuser/carbon -
最后,运行附加到我们之前创建的数据容器的 carbon-cache 端点。我们将使用自定义容器名称和公开端口,以便可以从中发送和读取指标,具体如下:
dockerhost$ docker run --volumes-from whisper -p 2003:2003 \--name=carboncache hubuser/carbon
我们现在有了一个可以发送我们稍后收集的所有 Docker 相关指标的地方。为了利用我们存储的指标,我们需要一种方法来读取和可视化它们。我们将部署 graphite-web 来可视化我们 Docker 容器的运行情况。以下是在我们的 Docker 主机上部署 graphite-web 的步骤:
-
构建
Dockerfile为hubuser/graphite-web,以准备一个 Docker 镜像来通过以下代码部署 graphite-web:FROM debian:jessie RUN apt-get update && \ apt-get --no-install-recommends install -y \ graphite-web \ apache2 \ libapache2-mod-wsgi ADD local_settings.py /etc/graphite/local_settings.py RUN ln -sf /usr/share/graphite-web/apache2-graphite.conf \ /etc/apache2/sites-available/100-graphite.conf && \ a2dissite 000-default && a2ensite 100-graphite && \ mkdir -p /graphite/storage && \ graphite-manage syncdb --noinput && \ chown -R _graphite:_graphite /graphite EXPOSE 80 ENTRYPOINT ["apachectl", "-DFOREGROUND"] -
上述 Docker 镜像引用了
local_settings.py来配置 graphite-web。请添加以下注释来链接 carbon-cache 容器和 whisper 卷:import os # --link-from carboncache:carbon CARBONLINK_HOSTS = ['carbon:7002'] # --volumes-from whisper WHISPER_DIR = '/whisper' GRAPHITE_ROOT = '/graphite' SECRET_KEY = os.environ.get('SECRET_KEY', 'replacekey') LOG_RENDERING_PERFORMANCE = False LOG_CACHE_PERFORMANCE = False LOG_METRIC_ACCESS = False LOG_DIR = '/var/log/graphite' -
在准备好
Dockerfile和local_settings.py配置文件后,使用以下命令构建hubuser/graphite-webDocker 镜像:dockerhost$ docker build -t hubuser/graphite-web . -
最后,运行与 carbon-cache 容器和 whisper 卷链接的
hubuser/graphite-webDocker 镜像,执行以下命令:dockerhost$ docker run --rm --env SECRET_KEY=somestring \--volumes-from whisper --link carboncache:carbon \-p 80:80 hubuser/graphite-web注意
SECRET_KEY环境变量是将多个 graphite-web 实例组合在一起的必要组件,当你决定扩展时,它是必不可少的。更多 graphite-web 设置的相关信息可以在graphite.readthedocs.org/en/latest/config-local-settings.html找到。
现在我们已经完成了 Graphite 的部署,可以进行一些初步的测试,查看其实际运行效果。我们将通过向 whisper 数据库填充随机数据来进行测试。输入以下命令,将名为 local.random 的随机指标发送到 carbon-cache 端点:
dockerhost$ seq `date +%s` -60 $((`date +%s` - 24*60*60)) \| perl -n -e \'print "local.random ". int(rand(100)) . " " . $_' \| docker run --link carboncache:carbon -i --rm \busybox nc carbon 2003
最后,通过访问我们的 hubuser/graphite-web 的 composer URL dockerhost/compose 来确认数据是否持久化。进入 Tree 选项卡,展开 Graphite/local 文件夹,获取 random 指标。以下是我们在 graphite-web 部署中看到的图表:

生产环境中的 Graphite
在生产环境中,随着我们在 Docker 部署中监控越来越多的指标,这种简单的 Graphite 配置将达到其极限。为了跟上监控指标数量的增加,我们需要对其进行扩展。为此,你需要以集群方式部署 Graphite。
为了扩展 carbon-cache 的指标处理能力,我们需要用 carbon-relay 和 carbon-aggregator 进行增强。为了让 graphite-web 更具响应性,我们需要将其与其他缓存组件(如 memcached)一起进行水平扩展。我们还需要添加另一个 graphite-web 实例,它连接到其他 graphite-web 实例,从而创建一个统一的指标视图。whisper 数据库将与 carbon-cache 和 graphite-web 一起共同部署,因此它会随着它们的扩展而自然扩展。
注意
有关如何在生产环境中扩展 Graphite 集群的更多信息,请访问 graphite.readthedocs.org。
使用 collectd 进行监控
我们已经完成了设置一个接收所有 Docker 相关数据的位置。现在,到了真正获取我们 Docker 应用程序相关数据的时候。在本节中,我们将使用 collectd,一个流行的系统统计收集守护进程。它是一个非常轻量且高性能的 C 程序,这使得它成为一种非侵入式监控软件,因为它不会消耗被监控系统的太多资源。由于其轻量性,它非常易于部署,所需的依赖很少。它有各种各样的插件,几乎可以监控我们系统的每个组件。
让我们开始监控我们的 Docker 主机。按照接下来的步骤安装collectd并将度量数据发送到我们的 Graphite 部署:
-
首先,通过输入以下命令在我们的 Docker 主机上安装
collectd:dockerhost$ apt-get install collectd-core -
接下来,创建一个最简化的
collectd配置,将数据发送到我们的 Graphite 部署。你可能还记得之前我们暴露了 carbon-cache 的默认明文协议端口(2003)。在/etc/collectd/collectd.conf中写入以下配置项来设置:LoadPlugin "write_graphite" <Plugin write_graphite> <Node "carboncache"> Host "dockerhost" </Node> </Plugin> -
现在,轮到我们从 Docker 主机上收集一些数据了。通过将以下几行添加到
/etc/collectd/collectd.conf中,加载相应的collectd插件:LoadPlugin "cpu" LoadPlugin "memory" LoadPlugin "disk" LoadPlugin "interface" -
配置完成后,使用以下命令重启
collectd:dockerhost$ systemctl restart collectd.service -
最后,让我们在 graphite-web 部署中创建一个可视化仪表盘,用来查看之前的度量数据。访问
dockerhost/dashboard,点击Dashboard,然后点击编辑仪表盘链接。系统会提示我们输入一个文本区域以放置仪表盘定义。将以下 JSON 文本粘贴到此文本区域,创建我们的初步仪表盘:[ { "areaMode": "stacked", "yMin": "0", "target": [ "aliasByMetric(dockerhost.memory.*)" ], "title": "Memory" }, { "areaMode": "stacked", "yMin": "0", "target": [ "aliasByMetric(dockerhost.cpu-0.*)" ], "title": "CPU" } ]
我们现在已经为 Docker 主机构建了一个基本的监控堆栈。上一节的最后一步将展示一个类似于以下屏幕截图的仪表盘:

收集与 Docker 相关的数据
现在,我们将测量一些基本的度量指标,这些指标决定了应用程序的性能。但是,如何深入了解在 Docker 主机中运行的容器呢?在我们的 Debian Jessie Docker 主机中,容器运行在docker-[container_id].scope控制组下。这些信息可以在 Docker 主机的 sysfs 路径/sys/fs/cgroup/cpu,cpuacct/system.slice中找到。幸运的是,collectd提供了一个cgroups插件,用于与先前暴露的 sysfs 信息接口。接下来的步骤将展示如何使用该插件来测量我们正在运行的 Docker 容器的 CPU 性能:
-
首先,将以下行插入
/etc/collectd/collectd.conf中:LoadPlugin "cgroups" <Plugin cgroups> CGroup "/^docker.*.scope/" </Plugin> -
接下来,使用以下命令重启 collectd:
dockerhost$ systemctl restart collectd.service -
最后,等待几分钟,让 Graphite 从
collectd接收到足够的度量数据,这样我们就可以初步了解如何可视化我们 Docker 容器的 CPU 度量。
现在,我们可以通过查询 Graphite 部署的渲染 API 上的dockerhost.cgroups-docker*.*度量指标,来查看 Docker 容器的 CPU 度量数据。以下是通过渲染 API URL http://dockerhost/render/?target=dockerhost.cgroups-docker-*.*生成的图像:

注意
更多关于cgroups插件的信息,可以在collectd文档页面中找到,链接为collectd.org/documentation/manpages/collectd.conf.5.shtml#plugin_cgroups。
当前,cgroups 插件仅测量我们运行的 Docker 容器的 CPU 度量。虽然有一些工作正在进行中,但在本书撰写时尚未准备好。幸运的是,有一个基于 Python 的 collectd 插件,它与 docker stats 进行接口。以下是设置该插件所需的步骤:
-
首先,下载以下依赖项以便能够运行插件:
dockerhost$ apt-get install python-pip libpython2.7 -
接下来,从其 GitHub 页面下载并安装插件:
dockerhost$ cd /opt dockerhost$ git clone https://github.com/lebauce/docker-collectd-plugin.gitdockerhost$ cd docker-collectd-plugindockerhost$ pip install -r requirements.txt -
将以下行添加到
/etc/collectd/collectd.conf以配置插件:TypesDB "/opt/docker-collectd-plugin/dockerplugin.db" LoadPlugin python <Plugin python> ModulePath "/opt/docker-collectd-plugin" Import "dockerplugin" <Module dockerplugin> BaseURL "unix://var/run/docker.sock" Timeout 3 </Module> </Plugin> -
最后,通过以下命令重启
collectd以反映前述配置更改:dockerhost$ systemctl restart collectd.service
注意
有一种情况,我们不希望仅为了查询 Docker 容器的统计信息端点而安装完整的 Python 堆栈。在这种情况下,我们可以使用 collectd 的低级插件 curl_json 来收集容器的统计数据。我们可以将其配置为向容器统计端点发起请求,并将返回的 JSON 解析为一组 collectd 度量。有关该插件如何工作的更多信息,可以参考 collectd.org/documentation/manpages/collectd.conf.5.shtml#plugin_curl_json。
在下图中,我们可以查看 cgroups 插件从我们的 Graphite 部署 http://docker/compose 中提供的度量:

在 Docker 中运行 collectd
如果我们希望像部署应用程序一样部署 collectd 配置,我们也可以将其运行在 Docker 中。以下是一个初始的 Dockerfile,我们可以用它开始将 collectd 部署为正在运行的 Docker 容器:
FROM debian:jessie
RUN apt-get update && \
apt-get --no-install-recommends install -y \
collectd-core
ADD collectd.conf /etc/collectd/collectd.conf
ENTRYPOINT ["collectd", "-f"]
注意
大多数插件查看 /proc 和 /sys 文件系统。为了让 collectd 在 Docker 容器内访问这些文件,我们需要将它们挂载为 Docker 卷,如 --volume /proc:/host/proc。然而,目前大多数插件仍然读取硬编码的 /proc 和 /sys 路径。关于使其可配置的讨论正在进行中。请参考此 GitHub 页面以跟踪进展:github.com/collectd/collectd/issues/1169。
在 ELK 堆栈中合并日志
我们的 Docker 主机和容器的所有状态并不能立即通过我们的监控解决方案(collectd 和 Graphite)查询到。有些事件和度量仅以原始文本行的形式出现在日志文件中。我们需要将这些原始且无结构的日志转化为有意义的度量。类似于原始度量,我们以后可以通过分析提出更高层次的问题,了解在我们的基于 Docker 的应用程序中发生了什么。
ELK 堆栈是 Elastic 提供的一套流行的组合工具,解决了这些问题。该缩写中的每个字母代表其组件。以下是每个组件的描述:
-
Logstash:Logstash 是用于收集和管理日志与事件的组件。它是我们用来收集来自不同日志源的所有日志的中心点,例如我们部署中的多个 Docker 主机和容器。我们还可以使用 Logstash 来转换和注释接收到的日志。这使我们能够在后续探索日志的更丰富特性时进行搜索。
-
Elasticsearch:Elasticsearch 是一个分布式搜索引擎,具有高度可扩展性。它的分片功能使我们能够随着 Docker 容器不断发送更多日志而扩展日志存储。它的数据库引擎是面向文档的,这使我们能够在继续发掘关于我们在大型 Docker 部署中管理的事件的更多洞察时,灵活地存储和注释日志。
-
Kibana:Kibana 是一个 Elasticsearch 的分析和搜索仪表盘。它的简便性使我们能够为 Docker 应用程序创建仪表盘。然而,Kibana 也非常灵活,可以进行定制,因此我们可以构建出可以为需要了解我们基于 Docker 的应用程序的人提供有价值洞察的仪表盘,无论是低级的技术细节还是更高层次的业务需求。
在本节的剩余部分,我们将设置这些组件,并将我们的 Docker 主机和容器日志发送到它们。接下来的几个步骤将描述如何构建 ELK 堆栈:
-
首先,在我们的 Docker 主机上启动官方的 Elasticsearch 镜像。我们将为其指定一个容器名称,以便后续步骤中可以轻松链接它,如下所示:
dockerhost$ docker run -d --name=elastic elasticsearch:1.7.1 -
接下来,我们将通过将其链接到前面创建的 Elasticsearch 容器来运行 Kibana 的官方 Docker 镜像。请注意,我们将公开暴露的端口
5601映射到 Docker 主机中的端口80,以便 Kibana 的 URL 更加简洁,如下所示:dockerhost$ docker run -d --link elastic:elasticsearch \-p 80:5601 kibana:4.1.1 -
现在,准备我们的 Logstash Docker 镜像和配置。准备以下的
Dockerfile来创建 Docker 镜像:FROM logstash:1.5.3 ADD logstash.conf /etc/logstash.conf EXPOSE 1514/udp -
在此 Docker 镜像中,将 Logstash 配置为 Syslog 服务器。这解释了在前面的
Dockerfile中暴露的 UDP 端口。至于logstash.conf文件,以下是使其作为 Syslog 服务器监听的基本配置。配置的后半部分表明它将日志发送到名为elasticsearch的 Elasticsearch。我们将在链接先前运行的 Elasticsearch 容器时使用此作为主机名:input { syslog { port => 1514 type => syslog } } output { elasticsearch { host => "elasticsearch" } }提示
Logstash 拥有大量插件,可以读取各种日志数据源。特别是,它有一个
collectd编解码器插件。通过这个插件,我们可以使用 ELK 堆栈来代替 Graphite 监控我们的指标。更多关于如何进行此设置的信息,请访问
www.elastic.co/guide/en/logstash/current/plugins-codecs-collectd.html。 -
现在我们已经准备好了所有需要的文件,输入以下命令将其创建为
hubuser/logstashDocker 镜像:dockerhost$ docker build -t hubuser/logstash . -
使用以下命令运行 Logstash。请注意,我们正在将端口
1514暴露给 Docker 主机,作为 Syslog 端口。我们还链接了之前创建的名为elastic的 Elasticsearch 容器。目标名称设置为elasticsearch,因为它是我们在logstash.conf中配置的 Elasticsearch 主机名,用于将日志发送到该主机:dockerhost$ docker run --link elastic:elasticsearch -d \-p 1514:1514/udp hubuser/logstash -f /etc/logstash.conf -
接下来,让我们配置 Docker 主机的 Syslog 服务,将日志转发到 Logstash 容器。作为基本配置,我们可以设置 Rsyslog 将所有日志转发。这将包括来自 Docker 引擎守护进程的日志。为此,创建一个
/etc/rsyslog.d/100-logstash.conf文件,并包含以下内容:*.* @dockerhost:1514 -
最后,通过输入以下命令重启 Syslog,以加载前一步骤中的更改:
dockerhost$ systemctl restart rsyslog.service
我们现在已经有了一个基本运行的 ELK 堆栈。现在让我们通过发送一条消息到 Logstash 并查看它是否出现在 Kibana 仪表板中来进行测试:
-
首先,输入以下命令发送一条测试消息:
dockerhost$ logger -t test 'message to elasticsearch' -
接下来,访问我们的 Kibana 仪表板,访问
http://dockerhost。Kibana 现在会要求我们设置默认索引。使用以下默认值并点击 Create 开始索引:![Consolidating logs in an ELK stack]()
-
访问
http://dockerhost/#discover并在搜索框中输入elasticsearch。以下截图显示了我们之前生成的 Syslog 消息:![Consolidating logs in an ELK stack]()
注意
在 ELK 堆栈上,我们可以做很多事情来优化日志基础设施。我们可以添加 Logstash 插件和过滤器来注释从 Docker 主机和容器接收到的日志。随着日志需求的增加,Elasticsearch 可以进行扩展和调优,以提高其容量。我们可以创建 Kibana 仪表板来共享指标。欲了解更多如何调优 ELK 堆栈的细节,请访问 Elastic 的
www.elastic.co/guide。
转发 Docker 容器日志
现在我们已经有了一个基本功能的 ELK 堆栈,我们可以开始将 Docker 日志转发到它。从 Docker 1.7 版本开始,Docker 支持自定义日志驱动程序。在这一部分,我们将配置 Docker 主机使用 syslog 驱动程序。默认情况下,Docker 的 Syslog 事件会发送到 Docker 主机的 Syslog 服务,并且由于我们已将 Syslog 配置为转发到我们的 ELK 堆栈,因此我们可以在那里看到容器日志。按照以下步骤开始在 ELK 堆栈中接收容器日志:
-
Docker 引擎服务通过 Systemd 在我们的 Debian Jessie 主机上配置。为了更新它在 Docker 主机中的运行方式,创建一个名为
/etc/systemd/system/docker.service.d/10-syslog.conf的 Systemd 单元文件,并包含以下内容:[Service] ExecStart= ExecStart=/usr/bin/docker daemon -H fd:// \ --log-driver=syslog -
通过重新加载 Systemd 配置,应用我们将如何在主机中运行 Docker 的更改。以下命令将完成此操作:
dockerhost$ systemctl daemon-reload -
最后,通过执行以下命令重启 Docker 引擎守护进程:
dockerhost$ systemctl restart docker.service -
如果我们希望对 Docker 容器的日志进行自定义注释,可选择应用任何 Logstash 过滤。
现在,任何来自我们 Docker 容器的标准输出和错误流都应该被捕获到我们的 ELK 堆栈中。我们可以做一些初步测试,以确认设置是否有效。输入以下命令从 Docker 创建一个测试消息:
dockerhost$ docker run --rm busybox echo message to elk
注意
docker run 命令还支持 --log-driver 和 --log-opt=[] 命令行选项,仅为我们要运行的容器设置日志驱动程序。我们可以使用它进一步调整我们在 Docker 主机中运行的每个 Docker 容器的日志策略。
输入前面的命令后,我们的消息现在应该存储在 Elasticsearch 中。我们可以访问 http://dockerhost 上的 Kibana 端点,并在文本框中搜索 message to elk。它应该会显示我们之前发送的消息的 Syslog 条目。以下截图是 Kibana 搜索结果应该显示的内容:

在前面的截图中,我们可以看到我们发送的消息。还有关于 Syslog 的其他信息。Docker 的 Syslog 驱动程序默认将设施和严重性注释设置为系统和信息,分别对应。除此之外,前面的程序被设置为docker/c469a2dfdc9a。
c469a2dfdc9a 字符串是我们之前运行的 busybox 镜像的容器 ID。Docker 容器的默认程序标签设置为 docker/<container-id> 格式。所有前述的默认注释可以通过向 --log-opt=[] 选项传递参数来配置。
注意
除了 Syslog 和 JSON 文件日志驱动程序,Docker 还支持将日志发送到多个其他端点。有关所有日志驱动程序及其使用指南的更多信息,可以在 docs.docker.com/reference/logging 中找到。
其他监控和日志解决方案
还有一些其他解决方案可以部署来监控和记录基础设施,以支持基于 Docker 的应用程序。其中一些已经内建支持 Docker 容器的监控。其他的则需要与我们之前展示的其他解决方案结合使用,因为它们仅专注于监控或日志的特定部分。
对于其他一些,我们可能需要做一些变通方法。然而,它们的好处显然超过了我们需要做出的妥协。虽然以下列表并不详尽,但这些是我们可以探索的一些堆栈,以创建我们的日志记录和监控解决方案:
-
cAdvisor (
github.com/google/cadvisor) -
InfluxDB (
influxdb.com) -
Sensu (
sensuapp.org) -
Fluentd (
www.fluentd.org/) -
Graylog (
www.graylog.org) -
Splunk (
www.splunk.com)
有时候,我们的运营人员和开发者在运行和开发 Docker 应用时还不够成熟,或者他们不想集中精力维护这些监控和日志基础设施。有几个托管的监控和日志平台可以供我们使用,这样我们就可以专注于实际编写和提升 Docker 应用的性能。
其中一些与现有的监控和日志代理(如 Syslog 和 collectd)兼容。对于其他一些,我们可能需要下载并部署它们的代理,才能将事件和指标转发到它们的托管平台。以下是我们可能希望考虑的一些解决方案的非详尽列表:
-
New Relic (
www.newrelic.com) -
Datadog (
www.datadoghq.com) -
Librato (
www.librato.com) -
Elastic's Found (
www.elastic.co/found) -
Treasure Data (
www.treasuredata.com) -
Splunk Cloud (
www.splunk.com)
总结
我们现在知道,以可扩展和可访问的方式监控 Docker 部署是很重要的。我们部署了 collectd 和 Graphite 来监控 Docker 容器的指标。我们推出了 ELK 堆栈,用于整合来自不同 Docker 主机和容器的日志。
除了原始的指标和事件,了解这些数据对我们的应用意味着什么也很重要。Graphite-web 和 Kibana 允许我们创建自定义的仪表板和分析,以提供关于 Docker 应用的洞察。凭借这些监控工具和技能,我们应该能够在生产环境中良好地操作和运行我们的 Docker 部署。
在下一章,我们将开始进行性能测试,并基准测试我们的 Docker 应用在高负载下的表现。我们应该能够利用我们部署的监控系统来观察和验证我们的性能测试活动。
第五章:基准测试
在优化我们的 Docker 应用程序时,验证我们调整的参数是非常重要的。基准测试是一种实验性的方式,用于确定我们在 Docker 容器中修改的元素是否按预期执行。我们的应用程序将有广泛的选项可以优化。运行这些应用程序的 Docker 主机也有自己的参数集,如内存、网络、CPU 和存储等。根据我们应用程序的性质,这些参数中的一个或多个可能会成为瓶颈。进行一系列测试以通过基准测试验证每个组件对于指导我们的优化策略非常重要。
此外,通过创建适当的性能测试,我们还可以识别当前 Docker 基础应用程序配置的极限。有了这些信息,我们可以开始探索基础设施参数,例如通过将应用程序部署到更多 Docker 主机上来进行横向扩展。我们还可以利用这些信息,通过将工作负载迁移到内存、存储或 CPU 更强大的 Docker 主机上来纵向扩展相同的应用程序。而当我们有混合云部署时,我们可以利用这些测量数据来确定哪个云服务商能为我们的应用程序提供最佳性能。
测量我们的应用程序如何响应这些基准测试对于规划我们 Docker 基础设施所需的容量至关重要。通过创建一个模拟峰值和正常状态的测试工作负载,我们可以预测应用程序一旦发布到生产环境中,它将如何表现。
在本章中,我们将涵盖以下内容,以基准测试我们在 Docker 基础设施中部署的一个简单 Web 应用程序:
-
设置 Apache JMeter 进行基准测试
-
创建和设计基准工作负载
-
分析应用性能
设置 Apache JMeter
Apache JMeter 是一种流行的应用程序,用于测试 Web 服务器的性能。除了对 Web 服务器进行负载测试外,这个开源项目还支持测试其他网络协议,如 LDAP、FTP,甚至是原始的 TCP 数据包。它具有高度的可配置性,并且足够强大,能够设计复杂的工作负载以适应不同的使用模式。这个功能可以用来模拟成千上万的用户突然访问我们的 Web 应用程序,从而引发负载激增。
任何负载测试软件中预期的另一项功能是其数据捕获和分析功能。JMeter 具有如此多样化的数据记录、绘图和分析功能,我们可以立即查看基准测试的结果。最后,它拥有广泛的插件,可能已经具备了我们计划使用的负载模式、分析或网络连接。
注意
关于 Apache JMeter 的功能和使用方法的更多信息,请访问其官方网站:jmeter.apache.org。
在本节中,我们将部署一个示例应用程序进行基准测试,并准备我们的工作站运行我们的第一个基于 JMeter 的基准测试。
部署示例应用程序
如果需要,我们也可以带上自己想要基准测试的 web 应用程序。但在本章的其余部分,我们将基准测试本节中描述的以下应用程序。该应用程序是一个简单的 Ruby web 应用程序,通过 Unicorn(一个流行的 Ruby 应用服务器)部署。它通过 Nginx 的 Unix 套接字接收流量。这个设置对于大多数现实中的 Ruby 应用程序来说非常典型。
在本节中,我们将把这个 Ruby 应用程序部署到名为 webapp 的 Docker 主机上。我们将为应用程序、基准工具和监控使用不同的 Docker 主机。这种分离非常重要,因为我们运行的基准测试和监控工具不会影响基准测试结果。
接下来的几步将展示如何构建和部署我们的简单 Ruby web 应用栈:
-
首先,通过创建以下 Rack
config.ru文件来创建 Ruby 应用程序:app = proc do |env| Math.sqrt rand [200, {}, %w(hello world)] end run app -
接下来,我们将使用以下
Dockerfile将应用程序打包为 Docker 容器:FROM ruby:2.2.3 RUN gem install unicorn WORKDIR /app COPY . /app VOLUME /var/run/unicorn CMD unicorn -l /var/run/unicorn/unicorn.sock -
现在我们将创建 Nginx 配置文件
nginx.conf。它将通过我们在前一步中创建的 Unix 套接字将请求转发到我们的 Unicorn 应用服务器。在记录请求日志时,我们将记录$remote_addr和$response_time。稍后在分析基准测试结果时,我们将特别关注这些指标:events { } http { log_format unicorn ''$remote_addr [$time_local]'' '' ""$request"" $status'' '' $body_bytes_sent $request_time''; access_log /var/log/nginx/access.log unicorn; upstream app_server { server unix:/var/run/unicorn/unicorn.sock; } server { location / { proxy_pass http://app_server; } } } -
上述的 Nginx 配置文件将被打包为一个 Docker 容器,并使用以下
Dockerfile:FROM nginx:1.9.4 COPY nginx.conf /etc/nginx/nginx.conf -
最后一个组件将是一个
docker-compose.yml文件,用于将两个 Docker 容器连接在一起进行部署:web: log_opt: syslog-tag: nginx build: ./nginx ports: - 80:80 volumes_from: - app app: build: ./unicorn
最终,我们的代码库中将包含如下截图所示的文件:

在准备好我们的 Docker 化 web 应用程序后,现在通过输入以下命令将其部署到 Docker 主机上:
webapp$ docker-compose up -d
注意
Docker Compose 是一个用于创建多容器应用程序的工具。它在 YML 文件中定义了一个模式,用于描述我们希望 Docker 容器如何运行以及如何相互连接。
Docker Compose 支持 curl | bash 类型的安装方式。为了在 Docker 主机上快速安装它,请输入以下命令:
dockerhost$ curl -L https://github.com/docker/compose/releases/download/1.5.2/docker-compose-`uname -s`-`uname -m` \> /usr/local/bin/docker-compose
本章中我们只简单提到过 Docker Compose。然而,我们可以在 Docker Compose 的文档网站上获得更多信息:docs.docker.com/compose。
最后,让我们进行初步测试,以确定我们的应用程序是否正常工作:
$ curl http://webapp.dev
hello world
现在我们已经准备好了要基准测试的应用程序。在接下来的部分,我们将通过安装 Apache JMeter 来准备我们的工作站进行基准测试。
安装 JMeter
在本章的其余部分,我们将使用 Apache JMeter 2.13 版本来进行基准测试。在本节中,我们将下载并安装它到我们的工作站上。按照接下来的几步正确设置 JMeter:
-
首先,访问 JMeter 的下载网页:
jmeter.apache.org/download_jmeter.cgi。 -
选择 apache-jmeter-2.13.tgz 的链接以开始下载二进制文件。
-
下载完成后,通过输入以下命令解压 tarball:
$ tar -xzf apache-jmeter-2.13.tgz -
接下来,我们将把
bin/目录添加到我们的$PATH中,以便可以轻松地从命令行启动 JMeter。为此,我们将输入以下命令:$ export PATH=$PATH:`pwd`/apache-jmeter-2.13/bin -
最后,通过输入以下命令启动 JMeter:
$ jmeter
我们现在将看到 JMeter 的 UI,和以下截图一样。现在我们终于准备好为我们的应用程序编写基准测试了!:

注意
请注意,Apache JMeter 是一个 Java 应用程序。根据 JMeter 网站的信息,它至少需要 Java 1.6 才能正常运行。在安装 JMeter 之前,确保你已经正确设置了 Java 运行环境(JRE)。
如果我们在 Mac OSX 环境中,可以使用 Homebrew,并输入以下命令:
$ brew install jmeter
对于其他平台,前面描述的安装说明应该足够让你入门。有关如何安装 JMeter 的更多信息,可以参考 jmeter.apache.org/usermanual/get-started.html。
构建基准工作负载
为应用程序编写基准测试是一个开放的探索领域。刚开始使用 Apache JMeter 时可能会觉得有些复杂。它有许多选项需要调整,以便编写我们的基准测试。首先,我们可以使用我们应用程序的“故事”作为起点。以下是我们可以问自己的几个问题:
-
我们的应用程序做了什么?
-
我们用户的角色是什么?
-
他们如何与我们的应用程序进行互动?
从这些问题开始,我们可以将它们转化为对我们应用程序的实际请求。
在我们前面写的示例应用程序中,我们有一个 Web 应用程序,它会向用户展示 Hello World。在 Web 应用程序中,我们通常关注的是吞吐量和响应时间。吞吐量指的是一次能有多少用户接收到 Hello World。响应时间则是指从用户请求 Hello World 到接收到 Hello World 消息之间的时间延迟。
在本节中,我们将创建一个初步的基准测试,在 Apache JMeter 中进行。接着,我们将使用 JMeter 的分析工具和我们在第四章,监控 Docker 主机和容器中部署的监控堆栈开始分析我们的初步结果。之后,我们将对我们开发的基准进行迭代并进行调整。通过这种方式,我们可以确保我们的应用程序基准测试是正确的。
在 JMeter 中创建测试计划
一系列的基准测试在 Apache JMeter 中由一个测试计划描述。测试计划描述了 JMeter 将执行的一系列步骤,例如对一个 Web 应用程序进行请求。测试计划中的每个步骤都称为一个元素。这些元素本身也可以包含一个或多个子元素。最终,我们的测试计划看起来就像一棵树——一个元素的层次结构,用来描述我们为应用程序设计的基准测试。
要将元素添加到我们的测试计划中,我们只需右键单击我们想要的父元素,然后选择添加。这会打开一个上下文菜单,列出可以添加到选定父元素的元素。在以下截图中,我们向主元素测试计划添加了一个线程组元素:

接下来的几个步骤展示了如何创建一个测试计划,进行我们想要的基准测试:
-
首先,让我们将测试计划重命名为一个更合适的名称。点击测试计划元素。这将更新右侧的主要 JMeter 窗口。在标有名称的表单字段中,将值设置为Unicorn Capacity。
-
在Unicorn Capacity测试计划下,创建一个线程组。将其命名为应用程序用户。我们将配置该线程组,初始时从单个线程发送 10,000 个请求到我们的应用程序。使用以下参数填写表单以实现此设置:
-
线程数: 1
-
Ramp-up 时间: 0 秒
-
循环次数: 120,000 次
提示
当我们开始制定测试计划时,使用较低的循环次数是有用的。与其设置 120,000 次循环,不如从 10,000 次甚至仅仅 10 次开始。我们的基准测试时间较短,但在开发过程中能立即得到反馈,比如在进行下一步时。完成整个测试计划后,我们随时可以回溯并调整它,以生成更多的请求。
-
-
接下来,在应用程序用户线程组下,我们通过添加采样器,HTTP 请求来创建实际的请求。这是配置我们如何向 Web 应用程序发出请求的设置:
-
名称: 访问
http://webapp/ -
服务器名称:
webapp
-
-
最后,我们通过在Unicorn Capacity测试计划下添加监听器来配置如何保存测试结果。为此,我们将添加一个Simple Data Writer,并将其命名为保存结果。我们将文件名字段设置为
result.jtl,以便将基准测试结果保存到该文件中。稍后在分析基准测试结果时,我们会引用此文件。
现在我们有了一个基本的基准负载,它会生成 120,000 个 HTTP 请求到http://webapp/。然后,测试计划会将每个请求的结果保存在名为result.jtl的文件中。以下是创建测试计划最后一步后 JMeter 的屏幕截图:

最后,到了运行基准测试的时候了。进入运行菜单,然后选择开始以开始执行测试计划。在基准测试运行时,开始按钮会变灰并禁用。执行完成后,按钮会重新启用。
在运行基准测试后,我们将在下一节使用 JMeter 的分析工具查看 result.jtl 文件来分析结果。
注意
在 JMeter 测试计划中,可以放置多种类型的元素。除了我们之前用来为应用程序创建基本基准的三个元素外,还有一些其他元素可以调控请求、执行其他网络请求和分析数据。
测试计划元素的全面列表及其描述可以在 JMeter 页面找到:jmeter.apache.org/usermanual/component_reference.html。
分析基准测试结果
在本节中,我们将分析基准测试结果,并识别 120,000 次请求如何影响我们的应用程序。在创建 Web 应用程序基准时,通常有两个我们关注的方面:
-
我们的应用程序一次能处理多少请求?
-
每个请求在我们的应用程序中被处理的时间有多长?
这两个低级 Web 性能指标可以很容易地转化为我们应用程序的业务影响。例如,有多少客户正在使用我们的应用程序?另一个是,他们如何从用户体验的角度感知我们应用程序的响应性?我们可以关联应用程序中的二级指标,如 CPU、内存和网络,以确定我们的系统容量。
查看 JMeter 运行结果
JMeter 的多个监听器元素具有渲染图形的功能。在运行基准测试时启用这一功能对于开发测试计划很有用。但 UI 渲染结果所花费的时间,加上实际基准请求的时间,会影响测试性能。因此,我们最好将基准测试的执行和分析组件分开。在本节中,我们将创建一个新的测试计划,并查看一些 JMeter 监听器元素,用于分析我们在 result.jtl 中获得的数据。
为了开始分析,我们首先创建一个新的测试计划,并将其命名为分析结果。我们将在这个测试计划的父元素下添加各种监听器元素。接下来,按照以下步骤添加可以用来分析基准结果的 JMeter 监听器。
计算吞吐量
对于我们的第一次分析,我们将使用汇总报告监听器。这个监听器将显示我们应用程序的吞吐量。吞吐量的测量将显示我们的应用程序每秒可以处理的事务数量。
要显示吞吐量,执行以下步骤:
加载监听器后,填写文件名字段,选择我们在运行基准测试时生成的result.jtl文件。对于我们之前执行的测试,以下截图显示了以每秒 746.7 次请求的吞吐量向http://webapp/发送了 120,000 个 HTTP 请求:

我们还可以通过图形结果监听器查看在基准测试过程中吞吐量的变化。在分析结果测试计划元素下创建这个监听器,并将其命名为吞吐量随时间变化。确保只选中吞吐量复选框(不过你也可以稍后查看其他数据点)。创建监听器后,再次加载我们的result.jtl测试结果。以下截图展示了吞吐量随时间的变化情况:

如我们在前面的截图中看到的,吞吐量在 JMeter 尝试预热其单线程请求池时起初较慢。但是当基准测试继续运行后,吞吐量水平逐渐稳定。通过在线程组中提前设置较多的循环次数,我们能够最小化早期预热期的影响。
这样,在汇总报告中显示的吞吐量更或多或少是一个一致的结果。请注意,图形结果监听器会在采样几次后将其数据点环绕。
提示
记住,在基准测试中,获取更多的样本数据,我们的观察结果会更加精确!
绘制响应时间
在基准测试应用程序时,我们感兴趣的另一个指标是响应时间。响应时间显示了 JMeter 在收到来自我们应用程序的网页响应之前必须等待的时间。就真实用户而言,我们可以将其视为用户从输入我们网页应用的 URL 到一切显示在他们浏览器中的时间(如果我们的应用程序渲染了一些较慢的 JavaScript,这可能无法完全代表真实情况,但对于我们之前制作的应用程序,这种类比应该足够了)。
为了查看我们应用程序的响应时间,我们将使用响应时间图监听器。作为初步设置,我们可以将间隔设置为 500 毫秒。这将在result.jtl中对一些响应时间进行 500 毫秒的平均处理。在下面的图片中,你可以看到我们应用程序的响应时间大部分都保持在 1 毫秒左右:

如果我们想更精细地显示响应时间,可以将间隔减少到 1 毫秒。请注意,这会需要更多时间来显示,因为 JMeter 界面需要在应用程序中绘制更多的数据点。有时,当样本过多时,JMeter 可能会崩溃,因为我们的工作站没有足够的内存来显示整个图表。在进行大规模基准测试时,我们最好通过监控系统来观察结果。我们将在下一节中查看这些数据。
在 Graphite 和 Kibana 中观察性能
可能会有一种情况,我们的工作站太旧,以至于 Java 无法在 JMeter 界面中显示 120,000 个数据点。为了解决这个问题,我们可以通过减少基准测试中生成的请求量,或像之前绘制响应时间图时那样对一些数据进行平均,来减少数据量。然而,有时我们希望看到数据的完整分辨率。这种完整视图在我们想要检查应用程序行为的细节时非常有用。幸运的是,我们已经为我们的 Docker 基础设施建立了一个监控系统,如第四章,监控 Docker 主机和容器中所述。
注意
在本节中,我们的监控和日志系统部署在名为 monitoring 的 Docker 主机上。运行应用程序容器的 Docker 主机 webapp 将会收集事件并通过 Rsyslog 发送到 Docker 主机 monitoring。
记得我们在描述基准测试时提到过 Nginx 配置吗?从 Nginx 容器标准生成的访问日志被 Docker 捕获。如果我们使用第四章,监控 Docker 主机和容器中 Docker 守护进程的相同设置,这些日志事件将由本地 Rsyslog 服务捕获。然后,这些 Syslog 条目会被转发到 Logstash Syslog 收集器,并存储到 Elasticsearch 中。接着,我们可以使用 Kibana 的可视化功能查看我们应用的吞吐量。以下分析是通过计算 Elasticsearch 每秒收到的访问日志条目数量得出的:

我们还可以在基准测试过程中,在 Kibana 中绘制应用程序的响应时间。为此,我们首先需要重新配置 Logstash 配置文件,以解析从访问日志接收到的数据,并使用过滤器将响应时间提取为一个指标。为此,更新第四章,监控 Docker 主机和容器中的 logstash.conf 文件,加入 grok {} 过滤器,如下所示:
input {
syslog {
port => 1514
type => syslog
}
}
filter {
if [program] == ""docker/nginx"" {
grok {
patterns_dir => [""/etc/logstash/patterns""]
match => {
""message"" => ""%{NGINXACCESS}""
}
}
}
}
output {
elasticsearch {
host => ""elasticsearch""
}
}
注意
Logstash 的过滤器插件用于在事件到达目标存储端点(如 Elasticsearch)之前进行中间处理。它将原始数据(如文本行)转换为 JSON 格式的更丰富数据架构,然后我们可以在后续分析中使用。关于 Logstash 过滤器插件的更多信息,请访问www.elastic.co/guide/en/logstash/current/filter-plugins.html。
在前面的代码中提到的NGINXACCESS模式是在外部定义的,称为grok {}过滤器所调用的patterns文件。将以下内容写入其中:
REQUESTPATH \""%{WORD:method} %{URIPATHPARAM} HTTP.*\""
HTTPREQUEST %{REQUESTPATH} %{NUMBER:response_code}
WEBMETRICS %{NUMBER:bytes_sent:int} %{NUMBER:response_time:float}
NGINXSOURCE %{IP:client} \[%{HTTPDATE:requested_at}\]
NGINXACCESS %{NGINXSOURCE} %{HTTPREQUEST} %{WEBMETRICS}
最后,从第四章,监控 Docker 主机和容器中重建我们的hubuser/logstash Docker 容器。别忘了按照以下方式更新Dockerfile,以将模式文件添加到我们的 Docker 上下文中:
FROM logstash:1.5.3
ADD logstash.conf /etc/logstash.conf
ADD patterns /etc/logstash/patterns/nginx
EXPOSE 1514/udp
EXPOSE 25826/udp
现在我们已经从 Nginx 访问日志中提取了响应时间,可以在 Kibana 可视化中绘制这些数据点。以下是 Kibana 的截图,显示了我们先前运行的基准测试的每秒平均响应时间:

我们可以探索的另一个结果是 Docker 主机webapp如何响应基准测试负载。首先,我们可以检查我们的 Web 应用程序如何消耗 Docker 主机的 CPU。让我们登录到监控系统的 graphite-web 仪表板,并绘制webapp.cpu-0.cpu-*的度量值,排除cpu-idle。正如我们在以下图像中看到的,当我们开始向应用程序发送大量请求时,Docker 主机的 CPU 使用率迅速达到 100%:

我们可以探索 Docker 主机的其他系统度量,看看它是如何受到 HTTP 请求负载影响的。关键点是,我们使用这些数据并进行关联,看看我们的 Web 应用程序的表现如何。
注意
Apache JMeter 版本 2.13 及更高版本包括一个后端监听器,我们可以使用它实时将 JMeter 数据度量发送到外部端点。默认情况下,它支持 Graphite wire 协议。我们可以利用这个功能将基准测试结果发送到我们在第四章,监控 Docker 主机和容器中构建的 Graphite 监控基础设施。关于如何使用此功能的更多信息,请访问jmeter.apache.org/usermanual/realtime-results.html。
调整基准测试
到目前为止,我们已经拥有了在 Apache JMeter 中创建测试计划并分析初步结果的基本工作流程。在此基础上,我们可以调整几个参数来实现基准测试目标。在本节中,我们将迭代我们的测试计划,以识别 Docker 应用程序的限制。
增加并发量
我们可能希望调整的第一个参数是增加 循环次数。推动我们的测试计划生成更多请求将帮助我们观察负载对应用程序的影响。这提高了基准实验的精度,因为网络连接缓慢或硬件故障等异常事件(除非我们专门测试这些情况!)会影响我们的测试结果。
在为我们的基准测试收集到足够的数据点后,我们可能会发现生成的负载不足以对抗我们的 Docker 应用程序。例如,第一轮分析得到的当前吞吐量可能无法模拟真实用户的行为。假设我们希望每秒处理 2000 个请求。为了提高 JMeter 生成请求的速率,我们可以增加先前创建的线程组中的线程数量。这将增加 JMeter 同时生成的并发请求数。如果我们想要模拟用户数的逐步增加,我们可以将 ramp-up 时间设定得更长。
提示
对于我们希望模拟用户突然增加的工作负载,可以保持 ramp-up 时间为 0,立即启动所有线程。在我们希望调整其他行为(例如恒定负载然后突然激增)时,可以使用 Stepping Thread Group 插件。
我们还可能希望将其限制为每秒仅 100 个请求。在这种情况下,我们可以使用 Timer 元素来控制线程生成请求的方式。为了开始限制吞吐量,我们可以使用 常量吞吐量定时器。当 JMeter 发现来自我们的 web 应用程序的吞吐量增加过快时,它会自动减慢线程速度。
这里的一些基准测试技术很难通过内置的 Apache JMeter 组件来应用。为了简化生成负载以驱动应用程序的过程,存在多种插件可供使用。它们作为插件提供。Apache JMeter 常用的社区插件列表可以在jmeter-plugins.org找到。
运行分布式测试
调整并发参数一段时间后,我们意识到结果并没有变化。我们可以设置 JMeter 一次生成 10,000 个请求,但这很可能会导致我们的 UI 崩溃!在这种情况下,我们在构建基准测试时已经达到了工作站的性能极限。从这里开始,我们可以考虑使用多个运行 JMeter 的服务器池来创建分布式测试。分布式测试非常有用,因为我们可以从云端获取几台性能更高的服务器来模拟流量峰值。它也适用于模拟来自多个源的负载。这种分布式设置对于模拟高延迟场景非常有用,尤其是当我们的用户从世界各地访问我们的 Docker 应用时。
执行以下步骤以在多个 Docker 主机上部署 Apache JMeter,进行分布式基准测试:
-
首先,创建以下
Dockerfile来创建一个名为hubuser/jmeter的 Docker 镜像:FROM java:8u66-jre # Download URL for JMeter RUN curl http://www.apache.org/dist/jmeter/binaries/apache-jmeter-2.13.tgz | tar xz WORKDIR /apache-jmeter-2.13 EXPOSE 1099 EXPOSE 1100 ENTRYPOINT ["./bin/jmeter", "-j", "/dev/stdout", "-s", \ "-Dserver_port=1099", "-Jserver.rmi.localport=1100"] -
接下来,根据我们的云或服务器提供商,配置所需的 Docker 主机数量。记下每个 Docker 主机的主机名或 IP 地址。在我们的案例中,我们创建了两个名为
dockerhost1和dockerhost2的 Docker 主机。 -
现在,我们将在 Docker 主机上运行 JMeter 服务器。登录到每台主机,并输入以下命令:
dockerhost1$ docker run -p 1099:1099 -p 1100:1100 \ hubuser/jmeter -Djava.rmi.server.hostname=dockerhost1 dockerhost2$ docker run -p 1099:1099 -p 1100:1100 \ hubuser/jmeter -Djava.rmi.server.hostname=dockerhost2 -
要完成我们的 JMeter 集群,我们将输入以下命令启动 JMeter UI 客户端,并连接到 JMeter 服务器:
$ jmeter -Jremote_hosts=dockerhost1,dockerhost2
有了 Apache JMeter 集群,我们现在可以运行分布式测试了。请注意,测试计划中的线程数指定了每个 JMeter 服务器上的线程数。在我们之前章节中制作的测试计划中,我们的 JMeter 基准测试将生成 240,000 个请求。我们应该根据预期的测试负载调整这些计数。前面章节中提到的一些指南可以用来调整我们的远程测试。
最后,要启动远程测试,从 Run 菜单中选择 Remote Start All。这将把我们在测试计划中创建的线程组分配到 dockerhost1 和 dockerhost2 上的 JMeter 服务器。当我们查看 Nginx 的访问日志时,我们现在可以看到 IP 来源来自两个不同的源。以下 IP 地址分别来自我们的两个 Docker 主机:
172.16.132.216 [14/Sep/2015:16:...] ""GET / HTTP/1.1"" 200 20 0.003
172.16.132.187 [14/Sep/2015:16:...] ""GET / HTTP/1.1"" 200 20 0.003
172.16.132.216 [14/Sep/2015:16:...] ""GET / HTTP/1.1"" 200 20 0.003
172.16.132.187 [14/Sep/2015:16:...] ""GET / HTTP/1.1"" 200 20 0.002
172.16.132.216 [14/Sep/2015:16:...] ""GET / HTTP/1.1"" 200 20 0.002
172.16.132.187 [14/Sep/2015:16:...] ""GET / HTTP/1.1"" 200 20 0.003
注意
有关分布式和远程测试的更多信息,请访问 jmeter.apache.org/usermanual/remote-test.html。
其他基准测试工具
还有一些专门用于基准测试基于 Web 的应用程序的工具。以下是这些工具的简短列表及其链接:
-
Apache Bench:
httpd.apache.org/docs/2.4/en/programs/ab.html -
HP Lab's Httperf:
www.hpl.hp.com/research/linux/httperf -
Siege:
www.joedog.org/siege-home
总结
在本章中,我们创建了衡量 Docker 应用程序性能的基准。通过使用 Apache JMeter 和我们在第四章中设置的监控系统,监控 Docker 主机和容器,我们分析了应用程序在不同条件下的表现。现在我们对应用程序的局限性有了了解,并将利用这些信息进一步优化或扩展它。
在下一章中,我们将讨论负载均衡器,如何扩展我们的应用程序以提高其容量。
第六章:负载均衡
无论我们如何调优 Docker 应用程序,都将达到应用程序的性能极限。使用我们在上一章中讨论的基准测试技术,我们应该能够识别应用程序的容量。在不久的将来,我们的 Docker 应用程序的用户将超过这个限制。仅仅因为我们的 Docker 应用程序无法再处理他们的请求,我们不能拒绝这些用户。我们需要扩展我们的应用程序,以便能够服务更多的用户。
在本章中,我们将讨论如何扩展我们的 Docker 应用程序以增加容量。我们将使用负载均衡器,这是各种 Web 大规模应用程序架构中的关键组成部分。负载均衡器将我们的应用程序用户分配到多个部署在 Docker 主机集群中的 Docker 应用程序上。本章中将介绍的以下步骤将帮助我们实现这一目标:
-
准备 Docker 主机集群
-
使用 Nginx 进行负载均衡
-
扩展我们的 Docker 应用程序
-
使用负载均衡器管理零停机时间发布
准备一个 Docker 主机集群
在对 Docker 应用程序进行负载均衡时,一个关键组成部分是有一组服务器来分发应用程序的请求。在我们的基础设施中,这涉及到准备一组 Docker 主机来部署我们的应用程序。可扩展的做法是拥有一个由配置管理软件(例如 Chef)管理的通用基础配置,正如我们在第三章中讨论的那样,使用 Chef 自动化 Docker 部署。
在准备好 Docker 主机集群后,接下来就是准备我们要运行的应用程序。在本章中,我们将扩展一个简单的 NodeJS 应用程序。接下来的部分将描述该应用程序是如何工作的。
该 Web 应用程序是一个小型的 NodeJS 应用程序,代码写在名为 app.js 的文件中。为了可视化我们应用程序的负载均衡方式,我们还会记录一些关于应用程序和它运行的 Docker 主机的信息。app.js 文件将包含以下代码:
var http = require('http');
var server = http.createServer(function (request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
var version = "1.0.0";
var log = {};
log.header = 'mywebapp';
log.name = process.env.HOSTNAME;
log.version = version;
console.log(JSON.stringify(log));
response.end(version + " Hello World "+ process.env.HOSTNAME);
});
server.listen(8000);
要部署前面的应用程序代码,我们将把它打包成一个名为 hubuser/app:1.0.0 的 Docker 镜像,使用以下的 Dockerfile:
FROM node:4.0.0
COPY app.js /app/app.js
EXPOSE 8000
CMD ["node", "/app/app.js"]
确保我们的 Docker 镜像已经构建并且可以在 Docker Hub 上使用。这样,我们就可以轻松地进行部署。使用以下命令运行:
dockerhost$ docker build -t hubuser/app:1.0.0 .
dockerhost$ docker push hubuser/app:1.0.0
作为准备工作的最后一步,我们将把 Docker 应用程序部署到三个 Docker 主机上:greenhost00、greenhost01 和 greenhost02。登录到每个主机,并输入以下命令以启动容器:
greenhost00$ docker run -d -p 8000:8000 hubuser/app:1.0.0
greenhost01$ docker run -d -p 8000:8000 hubuser/app:1.0.0
greenhost02$ docker run -d -p 8000:8000 hubuser/app:1.0.0
提示
更好的是,我们可以编写一个 Chef 食谱,来部署我们刚才编写的 Docker 应用程序。
使用 Nginx 进行负载均衡
现在我们已经有了一个 Docker 应用池来转发流量,我们可以准备我们的负载均衡器。在本节中,我们将简要介绍 Nginx,它是一款具有高并发性和高性能的流行 Web 服务器。它通常作为反向代理,将请求转发到更动态的 Web 应用程序,例如我们之前编写的 NodeJS 应用程序。通过配置 Nginx 使其具有多个反向代理目标(如我们的 Docker 应用池),它将平衡发送到它的请求负载,分配到池中。
在我们的负载均衡器部署中,我们将在名为 dockerhost 的 Docker 主机中部署 Nginx Docker 容器。部署后,Nginx 容器将开始将流量转发到名为 greenhost* 的 Docker 主机池,该池是我们在之前的章节中配置的。
以下是一个简单的 Nginx 配置,将流量转发到我们之前部署的 Docker 应用池。将此文件保存在 dockerhost Docker 主机中的 /root/nginx.conf,如以下所示:
events { }
http {
upstream app_server {
server greenhost00:8000;
server greenhost01:8000;
server greenhost02:8000;
}
server {
location / {
proxy_pass http://app_server;
}
}
}
上面的 Nginx 配置文件基本上由指令组成。每个指令都会对 Nginx 的配置产生相应的影响。为了定义我们的应用池,我们将使用upstream指令来定义一组服务器。接下来,我们将使用server指令将服务器列表添加到我们的池中。池中的服务器通常以<hostname-or-ip>:<port>格式定义。
注意
以下是提到的指令的参考文献:
-
upstream—nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream -
server—nginx.org/en/docs/http/ngx_http_upstream_module.html#server -
proxy_pass—nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
讨论指令基础知识的入门材料可以在 nginx.org/en/docs/beginners_guide.html#conf_structure 找到。
现在我们已经准备好 nginx.conf 文件,可以将其与 Nginx 容器一起部署。要进行此部署,请在我们的 dockerhost Docker 主机中运行以下命令:
dockerhost$ docker run -p 80:80 -d --name=balancer \
--volume=/root/nginx.conf:/etc/nginx/nginx.conf:ro nginx:1.9.4
我们的 Web 应用现在可以通过 http://dockerhost 访问。每个请求将被路由到我们部署到 Docker 主机池中的 hubuser/webapp:1.0.0 容器之一。
为了确认我们的部署情况,我们可以查看 Kibana 可视化,展示流量在我们三个主机之间的分布。为了展示流量分布,我们必须首先为应用程序生成负载。我们可以使用在第五章中描述的 JMeter 测试基础设施,基准测试,来实现这一点。为了快速测试,我们还可以使用类似下面的长时间运行命令生成负载:
$ while true; do curl http://dockerhost && sleep 0.1; done
1.0.0 Hello World 56547aceb063
1.0.0 Hello World af272c6968f0
1.0.0 Hello World 7791edeefb8c
1.0.0 Hello World 56547aceb063
1.0.0 Hello World af272c6968f0
1.0.0 Hello World 7791edeefb8c
1.0.0 Hello World 56547aceb063
1.0.0 Hello World af272c6968f0
1.0.0 Hello World 7791edeefb8c
记住,在我们之前准备的应用程序中,我们将$HOSTNAME作为 HTTP 响应的一部分打印出来。在之前的情况下,响应显示的是 Docker 容器的主机名。注意,Docker 容器默认将其容器 ID 的短哈希值作为主机名。正如我们从测试工作负载的初始输出中看到的,我们收到了来自三个容器的响应。
如果我们按照第四章中所做的那样设置我们的日志基础设施,我们可以在 Kibana 可视化中更好地展示响应。在以下屏幕截图中,我们可以根据日志条目来自的 Docker 主机计算每分钟的响应数量:

我们可以从前面的图中注意到,Nginx 将我们的工作负载均匀地分配到三个 Docker 主机:greenhost00、greenhost01和greenhost02。
提示
为了在 Kibana 中正确地可视化我们的部署,我们必须对 Docker 容器进行注释,并在 Logstash 中过滤这些日志条目,以便它们能够正确地注释到 Elasticsearch。我们可以通过以下步骤实现:
首先,我们需要确保在部署 Docker 容器时使用syslog-tag选项。这样可以使我们以后在 Logstash 中过滤应用程序时更容易。运行以下代码:
greenhost01$ docker run -d -p 8000:8000 \
--log-driver syslog \
--log-opt syslogtag=webapp \
hubuser/app:1.0.0
这样,Logstash 将接收到带有docker/webapp标签的 Docker 容器日志条目。然后,我们可以使用 Logstash 的filter,如下所示,将这些信息导入到 Elasticsearch 中:
filter {
if [program] == "docker/webapp" {
json {
source => "message"
}
}
}
扩展我们的 Docker 应用程序
现在,假设前一部分中的工作负载开始超载我们三个 Docker 主机中的每一个。没有像我们之前设置的 Nginx 这样的负载均衡器时,我们的应用程序性能将开始下降。这可能意味着应用程序用户的服务质量下降,或者在半夜接到电话,进行紧急的系统操作。然而,通过负载均衡器管理应用程序的连接,我们可以很简单地增加更多的容量来扩展应用程序的性能。
由于我们的应用程序已经设计为负载均衡,因此扩展过程非常简单。接下来的几步形成了一个典型的工作流,展示了如何为负载均衡的应用程序添加容量:
-
首先,提供与我们 Docker 主机池中前三个主机相同基础配置的新 Docker 主机。在本节中,我们将创建两个新 Docker 主机,分别命名为
greenhost03和greenhost04。 -
我们扩展过程中的下一步是将我们的应用程序部署到这些新 Docker 主机中。请在每个新 Docker 主机上键入以下与之前相同的命令进行部署:
greenhost03$ docker run -d -p 8000:8000 hubuser/app:1.0.0greenhost04$ docker run -d -p 8000:8000 hubuser/app:1.0.0 -
此时,我们池中的新应用程序服务器已经准备好接收连接。现在是时候将它们作为目标添加到基于 Nginx 的负载均衡器中。要将它们添加到我们的上游服务器池中,首先更新
/root/nginx.conf文件,如下所示:events { } http { upstream app_server { server greenhost00:8000; server greenhost01:8000; server greenhost02:8000; server greenhost03:8000; server greenhost04:8000; } server { location / { proxy_pass http://app_server; } } } -
最后,我们将通知正在运行的 Nginx Docker 容器重新加载其配置。在 Nginx 中,重新加载是通过向其主进程发送
HUPUnix 信号来完成的。要向 Docker 容器中的主进程发送信号,请键入以下 Docker 命令。发送重新加载信号:dockerhost$ docker kill -s HUP balancer注意
有关如何使用各种 Unix 信号控制 Nginx 的更多信息,请参阅
nginx.org/en/docs/control.html。
现在我们已经完成了 Docker 应用程序的扩展,让我们回顾一下我们的 Kibana 可视化效果,观察其影响。以下截图显示了我们当前五个 Docker 主机之间的流量分布:

我们可以在前面的截图中看到,在重新加载 Nginx 后,它开始在新的 Docker 容器之间分配负载。在此之前,每个 Docker 容器仅接收 Nginx 的三分之一流量。而现在,池中的每个 Docker 应用程序只接收五分之一的流量。
零停机部署
拥有 Docker 应用程序负载均衡的另一个优势是,我们可以使用相同的负载均衡技术来更新我们的应用程序。通常,运维工程师需要安排停机时间或维护窗口才能更新生产环境中部署的应用程序。然而,由于我们的应用程序流量首先会流向负载均衡器,再到达我们的应用程序,我们可以利用这一中间步骤来为自己谋利。在本节中,我们将采用一种名为蓝绿部署的技术,以零停机时间更新我们的正在运行的应用程序。
我们当前的hubuser/app:1.0.0 Docker 容器池被称为我们的绿色 Docker 主机池,因为它会主动接收来自 Nginx 负载均衡器的请求。我们将把 Nginx 负载均衡器服务的应用程序更新为 hubuser/app:2.0.0 Docker 容器池。以下是执行更新的步骤:
-
首先,让我们通过修改
app.js文件中的版本字符串来更新我们的应用程序,如下所示:var http = require('http'); var server = http.createServer(function (request, response) { response.writeHead(200, {"Content-Type": "text/plain"}); var version = "2.0.0"; var log = {}; log.header = 'mywebapp'; log.name = process.env.HOSTNAME; log.version = version; console.log(JSON.stringify(log)); response.end(version + " Hello World "+ process.env.HOSTNAME); }); server.listen(8000); -
在更新内容后,我们将准备一个新的 Docker 镜像版本,名为
hubuser/app:2.0.0,并通过以下命令将其发布到 Docker Hub:dockerhost$ docker build -t hubuser/app:2.0.0 . dockerhost$ docker push hubuser/app:2.0.0 -
接下来,我们将通过我们的云服务提供商或购买实际硬件来配置一组 Docker 主机,命名为
bluehost01、bluehost02和bluehost03。这将成为我们的蓝色 Docker 主机池。 -
现在我们的 Docker 主机已经准备好,我们将在每个新主机上部署新的 Docker 应用程序。请在每个 Docker 主机上输入以下命令进行部署:
bluehost00$ docker run -d -p 8000:8000 hubuser/app:2.0.0 bluehost01$ docker run -d -p 8000:8000 hubuser/app:2.0.0 bluehost02$ docker run -d -p 8000:8000 hubuser/app:2.0.0
我们的蓝色 Docker 主机池现在已经准备好。之所以称其为蓝色,是因为尽管它现在已经在线并运行,但它还没有接收到用户流量。此时,我们可以执行必要的操作,例如在将用户转向新版本的应用程序之前,进行预飞行检查和测试。
在确认我们的蓝色 Docker 主机池完全正常并投入使用后,就该开始将流量发送到它了。与扩展 Docker 主机池的过程类似,我们只需将蓝色 Docker 主机添加到 /root/nginx.conf 配置中的服务器列表中,如下所示:
events { }
http {
upstream app_server {
server greenhost00:8000;
server greenhost01:8000;
server greenhost02:8000;
server greenhost03:8000;
server greenhost04:8000;
server bluehost00:8000;
server bluehost01:8000;
server bluehost02:8000;
}
server {
location / {
proxy_pass http://app_server;
}
}
}
为完成激活,重新加载我们的 Nginx 负载均衡器,通过以下命令向其发送 HUP 信号:
dockerhost$ docker kill -s HUP balancer
此时,Nginx 将流量同时发送到旧版本(hubuser/app:1.0.0)和新版本(hubuser/app:2.0.0)的 Docker 应用程序。通过这种方式,我们可以完全验证新应用程序是否按预期工作,因为它现在正在处理来自我们应用程序用户的实时流量。如果在某些情况下它没有正常工作,我们可以通过删除池中的 bluehost* Docker 主机并向我们的 Nginx 容器重新发送 HUP 信号来安全回滚。
但是,假设我们已经对新应用程序感到满意。我们可以安全地从负载均衡器的配置中移除旧的 Docker 应用程序。在我们的 /root/nginx.conf 文件中,我们可以通过删除所有 greenhost* 行来完成此操作,如下所示:
http {
upstream app_server {
server bluehost00:8000;
server bluehost01:8000;
server bluehost02:8000;
}
server {
location / {
proxy_pass http://app_server;
}
}
}
现在,我们可以通过向 Nginx 发送另一个 HUP 信号来完成零停机时间部署。此时,我们的蓝色 Docker 主机池处理我们应用程序的所有生产流量。因此,它成为了我们的新绿色 Docker 主机池。我们也可以选择停用旧的绿色 Docker 主机池,以节省资源使用。
我们之前做的整个蓝绿部署过程可以通过以下 Kibana 可视化进行总结:

请注意,在上面的图表中,尽管我们更新了应用程序,但我们的应用仍然在处理流量。还请注意,在此之前,所有的流量都分配给了我们的五个1.0.0版本的应用程序。启用蓝色 Docker 主机池后,三分之八的流量开始转向我们应用程序的2.0.0版本。最终,我们停用了旧的绿色 Docker 主机池中的所有端点,所有应用的流量现在都由2.0.0版本的应用程序处理。
注意
关于蓝绿部署和其他零停机发布技术的更多信息,可以参考 Jez Humble 和 Dave Farley 的书《持续交付》。该书的网站可以在continuousdelivery.com找到。
其他负载均衡器
还有其他可以用于负载均衡应用程序的工具。一些工具类似于 Nginx,其中配置通过外部配置文件进行定义。然后,我们可以向正在运行的进程发送信号,以重新加载更新后的配置。有些工具将其池配置存储在外部存储中,如 Redis、etcd,甚至是常规数据库,以便负载均衡器本身动态加载该列表。即便是 Nginx,其商业版本也具有一些此类功能。还有一些开源项目通过第三方模块扩展了 Nginx。
以下是我们可以以某种形式作为 Docker 容器部署到基础设施中的负载均衡器简短列表:
-
Redx (
github.com/rstudio/redx) -
HAProxy (
www.haproxy.org) -
Apache HTTP Server (
httpd.apache.org) -
Vulcand (
vulcand.github.io/) -
CloudFoundry 的 GoRouter (
github.com/cloudfoundry/gorouter) -
dotCloud 的 Hipache (
github.com/hipache/hipache)
也有基于硬件的负载均衡器,我们可以自行采购并通过它们自己专有的格式或 API 进行配置。如果我们使用云服务提供商,它们的一些负载均衡器服务将会有自己的云 API,我们也可以使用。
总结
在本章中,你了解了使用负载均衡器的好处以及如何使用它们。我们在 Docker 容器中部署并配置了 Nginx 作为负载均衡器,以便扩展我们的 Docker 应用程序。我们还使用负载均衡器执行了零停机发布,将我们的应用程序更新为新版本。
在下一章,我们将继续通过调试我们部署的 Docker 容器来提升 Docker 优化技能。
第七章:容器故障排除
有时,像我们在第四章中设置的监控和日志系统,监控 Docker 主机和容器,可能还不足够。理想情况下,我们应该建立一种可扩展的方式来排查 Docker 部署中的问题。然而,有时我们别无选择,只能登录到 Docker 主机并查看 Docker 容器本身。
本章将涵盖以下主题:
-
使用
docker exec检查容器 -
从 Docker 外部进行调试
-
其他调试套件
检查容器
在故障排除服务器时,传统的调试方法是登录并随便查看机器。使用 Docker 时,这种典型的工作流程被分为两个步骤:第一步是使用标准的远程访问工具(如 ssh)登录到 Docker 主机,第二步是通过 docker exec 进入所需的运行容器的进程命名空间。这在作为最后手段调试应用程序内部发生了什么时非常有用。
本章的大部分内容将涉及故障排除和调试运行 HAProxy 的 Docker 容器。为准备该容器,创建一个名为 haproxy.cfg 的 HAProxy 配置文件,内容如下:
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend stats
bind 127.0.0.1:80
stats enable
listen http-in
bind *:80
server server1 www.debian.org:80
接下来,使用官方的 HAProxy Docker 镜像(haproxy:1.5.14),我们将运行该容器并使用我们之前创建的配置。请在 Docker 主机中运行以下命令来启动 HAProxy 并应用我们准备好的配置:
dockerhost$ docker run -d -p 80:80 --name haproxy \
-v `pwd`/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg \
haproxy:1.5.14
现在,我们可以开始检查容器并调试它。一个好的初步示例是确认 HAProxy 容器是否正在监听端口 80。ss 程序可以打印出大多数 Linux 发行版中可用的套接字统计信息,例如我们的 Debian Docker 主机。我们可以运行以下命令来显示 Docker 容器内监听套接字的统计信息:
dockerhost$ docker exec haproxy /bin/ss -l
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:http *:*
LISTEN 0 128 127.0.0.1:http *:*
由于 ss 默认包含在 haproxy:1.5.14 的 debian:jessie 父容器中,这种使用 docker exec 的方法才有效。我们不能使用类似的未默认安装的工具,比如 netstat。输入相应的 netstat 命令将显示以下错误:
dockerhost$ docker exec haproxy /usr/bin/netstat -an
dockerhost$ echo $?
255
让我们通过查看 Docker 引擎服务的日志来调查发生了什么。输入以下命令显示 netstat 程序在我们的容器内不存在:
dockerhost$ journalctl -u docker.service –o cat
...
time="..." level=info msg="POST /v1.20/containers/haproxy/exec"
time="..." level=info msg="POST /v1.20/exec/c64fcf22b5c4.../start"
time="..." level=warning msg="signal: killed"
time="..." level=error msg="Error running command in existing...:"
" [8] System error: exec: \"/usr/bin/netstat\":"
" stat /usr/bin/netstat: no such file or directory"
time="..." level=error msg="Handler for POST /exec/{n.../start..."
time="..." level=error msg="HTTP Error" err="Cannot run exec c..."
2015/11/18 17:58:12 http: response.WriteHeader on hijacked conn...
2015/11/18 17:58:12 http: response.Write on hijacked connect...
time="..." level=info msg="GET /v1.20/exec/c64fcf22b5c47be8278..."
...
查找 netstat 是否已安装在系统中的另一种方式是交互式进入我们的容器。docker exec 命令具有 -it 标志,我们可以使用它来启动一个交互式的 shell 会话来进行调试。输入以下命令,使用 bash shell 进入容器:
dockerhost$ docker exec -it haproxy /bin/bash
root@b397ffb9df13:/#
现在我们处于标准的 shell 环境中,可以使用容器内所有标准的 Linux 工具进行调试。我们将在下一节介绍一些这些命令。现在,让我们来看一下为什么 netstat 在容器内无法使用,具体如下:
root@b397ffb9df13:/# netstat
bash: netstat: command not found
root@b397ffb9df13:/# /usr/bin/netstat -an
bash: /usr/bin/netstat: No such file or directory
如我们所见,bash 告诉我们,经过更互动的调试过程后,我们已经确认没有安装 netstat。
我们可以通过在容器内安装它来提供一个快速的解决方案,类似于在普通的 Debian 环境中所做的那样。虽然我们仍然在容器内,我们将输入以下命令来安装 netstat:
root@b397ffb9df13:/# apt-get update
root@b397ffb9df13:/# apt-get install -y net-tools
现在,我们可以成功运行 netstat,如下所示:
root@b397ffb9df13:/# netstat -an
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:80 0.0.0.0:* LISTEN
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags Type State I-Node Path
这种临时容器调试方法不推荐使用!下一次我们在设计 Docker 基础架构时,应该使用适当的工具和监控系统。下次让我们改进在第四章,监控 Docker 主机和容器,中构建的初始版本!以下是这种临时方法的一些局限性:
-
当我们停止并重新创建容器时,安装的
netstat包将不再可用。这是因为原始的 HAProxy Docker 镜像根本就不包含它。临时安装包来运行容器违背了 Docker 的主要特性,即启用不可变基础设施。 -
如果我们想将所有调试工具打包到 Docker 镜像中,镜像的大小将相应增加。这意味着我们的部署将变得更大,变得更慢。记住,在第二章,优化 Docker 镜像,中我们优化了容器的大小以减小其体积。
-
对于仅包含所需二进制文件的最小容器,我们现在几乎是“盲目”的。连
bashshell 都不可用!我们无法进入容器,看看以下命令:dockerhost$ docker exec -it minimal_image /bin/bashdockerhost$ echo $? 255
总结来说,docker exec 是一个强大的工具,可以让我们进入容器并通过运行各种命令进行调试。结合 -it 参数,我们可以获得一个交互式 shell 来进行更深层次的调试。由于假设容器内的所有工具都可以使用,这种方法有一定的局限性。
注意
更多关于 docker exec 命令的信息可以在官方文档中找到,docs.docker.com/reference/commandline/exec。
下一节将介绍如何通过外部工具检查正在运行的容器的状态,从而绕过这个限制。我们将简要概述如何使用其中一些工具。
从外部调试
尽管 Docker 在容器内隔离了网络、内存、CPU 和存储资源,每个容器仍然需要访问 Docker 主机的操作系统来执行实际命令。我们可以利用这些调用传递到主机操作系统的特性,从外部拦截并调试我们的 Docker 容器。在本节中,我们将介绍一些选定的工具以及如何使用它们与 Docker 容器进行交互。我们可以在 Docker 主机本身或在具有提升权限的同级容器内部执行交互,以查看 Docker 主机的一些组件。
跟踪系统调用
系统调用跟踪器 是服务器操作中必不可少的工具之一。它是一种工具,能够拦截并跟踪应用程序向操作系统发出的调用。每个操作系统都有其独特的实现。即使我们在 Docker 容器内部运行各种应用程序和进程,最终它们都会作为一系列系统调用进入 Docker 主机的 Linux 操作系统。
在 Linux 系统中,strace 程序用于跟踪这些系统调用。strace 的拦截和日志记录功能可以用来从外部检查我们的 Docker 容器。容器生命周期内进行的系统调用列表可以提供关于其行为的概览。
要开始使用 strace,只需在我们的 Debian Docker 主机中输入以下命令来安装它:
dockerhost$ apt-get install strace
提示
通过向 docker run 命令添加 --pid=host 选项,我们可以将容器的 PID 命名空间设置为 Docker 主机的 PID 命名空间。这样,我们就能够在 Docker 容器内安装并使用 strace,从而检查 Docker 主机上的所有进程。如果我们使用相应的基础镜像,还可以从其他 Linux 发行版(如 CentOS 或 Ubuntu)安装 strace。
有关此选项的更多信息,请访问 docs.docker.com/engine/reference/run/#pid-settings-pid。
现在我们已经在 Docker 主机中安装了 strace,可以使用它来检查我们在上一节中创建的 HAProxy 容器内的系统调用。输入以下命令开始跟踪来自 haproxy 容器的系统调用:
dockerhost$ PID=`docker inspect -f '{{.State.Pid}}' haproxy`dockerhost$ strace -p $PID
epoll_wait(3, {}, 200, 1000) = 0
epoll_wait(3, {}, 200, 1000) = 0
epoll_wait(3, {}, 200, 1000) = 0
epoll_wait(3, {}, 200, 1000) = 0
...
如你所见,我们的 HAProxy 容器发出了 epoll_wait() 调用,等待传入的网络连接。现在,在另一个终端中输入以下命令,向正在运行的容器发出 HTTP 请求:
$ curl http://dockerhost
现在,让我们回到之前运行的 strace 程序。我们可以看到以下几行输出:
...
epoll_wait(3, {}, 200, 1000) = 0
epoll_wait(3, {{EPOLLIN, {u32=5, u64=5}}}, 200, 1000) = 1
accept4(5, {sa_family=AF_INET, sin_port=htons(56470), sin_addr...
setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0
accept4(5, 0x7ffc087a6a50, [128], SOCK_NONBLOCK) = -1 EAGAIN (...
recvfrom(6, "GET / HTTP/1.1\r\nUser-Agent: curl"..., 8192, 0, ...
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 7
fcntl(7, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
connect(7, {sa_family=AF_INET, sin_port=htons(80), sin_addr=in...
epoll_wait(3, {}, 200, 0) = 0
sendto(7, "GET / HTTP/1.1\r\nUser-Agent: curl"..., 74, MSG_DON...
recvfrom(6, 0x18c488e, 8118, 0, 0, 0) = -1 EAGAIN (Resource ...
epoll_ctl(3, EPOLL_CTL_ADD, 7, {EPOLLOUT, {u32=7, u64=7}}) = 0...
epoll_ctl(3, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLRDHUP, {u32=6, u6...
epoll_wait(3, {{EPOLLOUT, {u32=7, u64=7}}}, 200, 1000) = 1
sendto(7, "GET / HTTP/1.1\r\nUser-Agent: curl"..., 74, MSG_DON...
epoll_ctl(3, EPOLL_CTL_DEL, 7, 6bbc1c) = 0
epoll_wait(3, {}, 200, 0) = 0
recvfrom(7, 0x18c88d4, 8192, 0, 0, 0) = -1 EAGAIN (Resource ...
epoll_ctl(3, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLRDHUP, {u32=7, u6...
epoll_wait(3, {{EPOLLIN, {u32=7, u64=7}}}, 200, 1000) = 1
recvfrom(7, "HTTP/1.1 200 OK\r\nDate: Fri, 20 N"..., 8192, 0, ...
epoll_wait(3, {}, 200, 0) = 0
sendto(6, "HTTP/1.1 200 OK\r\nDate: Fri, 20 N"..., 742, MSG_DO...
epoll_wait(3, {{EPOLLIN|EPOLLRDHUP, {u32=6, u64=6}}}, 200, 100...
recvfrom(6, "", 8192, 0, NULL, NULL) = 0
shutdown(6, SHUT_WR) = 0
close(6) = 0
setsockopt(7, SOL_SOCKET, SO_LINGER, {onoff=1, linger=0}, 8) =...
close(7) = 0
epoll_wait(3, {}, 200, 1000) = 0
...
我们可以看到,HAProxy 执行了标准的 BSD 样式的套接字系统调用,如 accept4()、socket() 和 close(),用于接受、处理和终止来自 HTTP 客户端的网络连接。最后,它再次回到 epoll_wait(),等待下一个连接。同时,请注意,epoll_wait() 调用在整个跟踪过程中都有出现,即使 HAProxy 正在处理某个连接。这表明 HAProxy 可以处理并发连接。
跟踪系统调用是调试生产环境系统的一个非常有用的技巧。运维人员有时会接到页面通知,但并没有立即访问源代码的权限。或者,有时我们只会收到编译后的二进制文件(或普通的 Docker 镜像),它们在生产环境中运行,而没有源代码(也没有 Dockerfile)。我们从正在运行的应用程序中能获取到的唯一线索,就是捕捉它对 Linux 内核所做的系统调用。
注意
strace 的网页可以在 sourceforge.net/projects/strace/ 上找到。更多信息也可以通过其手册页面访问,方法是输入以下命令:
dockerhost$ man 1 strace
要获取更全面的 Linux 系统中系统调用的列表,请参考 man7.org/linux/man-pages/man2/syscalls.2.html。这对于理解 strace 输出的各种信息非常有用。
分析网络数据包
我们部署的大多数 Docker 容器都涉及提供某种形式的网络服务。在本章的 HAProxy 示例中,我们的容器基本上提供 HTTP 网络流量。无论我们运行什么类型的容器,网络数据包最终都必须通过 Docker 主机出去,以完成我们发送给它的请求。通过捕获和分析这些数据包的内容,我们可以深入了解 Docker 容器的性质。在本节中,我们将使用一个叫做 tcpdump 的数据包分析器,查看我们的 Docker 容器接收和发送的网络数据包流量。
要开始使用 tcpdump,我们可以在我们的 Debian Docker 主机中输入以下命令来安装它:
dockerhost$ apt-get install -y tcpdump
提示
我们还可以将 Docker 主机的网络接口暴露给容器。通过这种方法,我们可以在容器中安装 tcpdump,而不会污染我们的主 Docker 主机与临时调试包。可以通过在 docker run 时指定 --net=host 标志来实现。这样,我们就可以在 Docker 容器内部使用 tcpdump 访问 docker0 接口。
使用 tcpdump 的示例将特别适用于 VMware Fusion 7.0 的 Vagrant VMware Fusion 提供程序。假设我们有一个 Docker Debian 主机作为 Vagrant VMware Fusion box,运行以下命令来挂起和恢复我们 Docker 主机的虚拟机:
$ vagrant suspend
$ vagrant up
$ vagrant ssh
dockerhost$
现在我们回到 Docker 主机内部,运行以下命令并注意到,我们再也无法在交互式的 debian:jessie 容器内解析 www.google.com,如下所示:
dockerhost$ docker run -it debian:jessie /bin/bash
root@fce09c8c0e16:/# ping www.google.com
ping: unknown host
现在,让我们在一个独立的终端中运行 tcpdump。在运行前面的 ping 命令时,我们会注意到从 tcpdump 终端输出以下内容:
dockerhost$ tcpdump -i docker0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
22:03:34.512942 ARP, Request who-has 172.17.42.1 tell 172.17.0.7, length 28
22:03:35.512931 ARP, Request who-has 172.17.42.1 tell 172.17.0.7, length 28
22:03:38.520681 ARP, Request who-has 172.17.42.1 tell 172.17.0.7, length 28
22:03:39.520099 ARP, Request who-has 172.17.42.1 tell 172.17.0.7, length 28
22:03:40.520927 ARP, Request who-has 172.17.42.1 tell 172.17.0.7, length 28
22:03:43.527069 ARP, Request who-has 172.17.42.1 tell 172.17.0.7, length 28
如我们所见,交互式 /bin/bash 容器正在查找 172.17.42.1,这通常是附加在 Docker 引擎网络设备 docker0 上的 IP 地址。弄清楚这一点后,查看 docker0,请输入以下命令:
dockerhost$ ip addr show dev docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:46:66:64:b8 brd ff:ff:ff:ff:ff:ff
inet6 fe80::42:46ff:fe66:64b8/64 scope link
valid_lft forever preferred_lft forever
现在,我们可以查看问题。docker0 设备没有附加 IPv4 地址。不知何故,VMware 恢复 Docker 主机时,会移除 docker0 中映射的 IP 地址。幸运的是,解决方案是简单地重启 Docker 引擎,Docker 会自动重新初始化 docker0 网络接口。在 Docker 主机中输入以下命令以重启 Docker 引擎:
dockerhost$ systemctl restart docker.service
现在,当我们运行与之前相同的命令时,我们会看到 IP 地址已附加,如下所示:
dockerhost$ ip addr show dev docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noque...
link/ether 02:42:46:66:64:b8 brd ff:ff:ff:ff:ff:ff
inet 172.17.42.1/16 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:46ff:fe66:64b8/64 scope link
valid_lft forever preferred_lft forever
让我们回到最初显示问题的命令,我们将看到问题已经解决,如下所示:
root@fce09c8c0e16:/# ping www.google.com
PING www.google.com (74.125.21.105): 56 data bytes
64 bytes from 74.125.21.105: icmp_seq=0 ttl=127 time=65.553 ms
64 bytes from 74.125.21.105: icmp_seq=1 ttl=127 time=38.270 ms
...
注意
有关 tcpdump 数据包转储器和分析器的更多信息,请访问 www.tcpdump.org。我们还可以通过输入以下命令访问我们安装它的文档:
dockerhost$ man 8 tcpdump
观察块设备
从我们的 Docker 容器访问的数据通常存储在物理存储设备中,例如硬盘或固态硬盘。在 Docker 的写时复制文件系统下面,是一个随机访问的物理设备。这些硬盘被分组为块设备。这里存储的数据是随机访问的固定大小的数据,称为 块。
所以,如果我们的 Docker 容器出现异常的 I/O 行为和性能问题,我们可以使用名为 blktrace 的工具来追踪和排查发生在块设备中的问题。该程序会拦截内核生成的所有与块设备交互的事件,这些事件来自进程。在本节中,我们将设置 Docker 主机,以便观察支持我们容器的块设备。
要使用 blktrace,我们需要通过安装 blktrace 程序来准备我们的 Docker 主机。在 Docker 主机内输入以下命令来安装它:
dockerhost$ apt-get install -y blktrace
此外,我们需要启用文件系统的调试。我们可以通过在 Docker 主机中输入以下命令来实现:
dockerhost$ mount -t debugfs debugfs /sys/kernel/debug
准备工作完成后,我们需要弄清楚如何告诉 blktrace 该在哪里监听 I/O 事件。要追踪我们容器的 I/O 事件,我们需要知道 Docker 运行时的根目录在哪里。在我们 Docker 主机的默认配置中,运行时指向 /var/lib/docker。要找出它属于哪个分区,请输入以下命令:
dockerhost$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/dm-0 9.0G 7.6G 966M 89% /
udev 10M 0 10M 0% /dev
tmpfs 99M 13M 87M 13% /run
tmpfs 248M 52K 248M 1% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 248M 0 248M 0% /sys/fs/cgroup
/dev/sda1 236M 34M 190M 15% /boot
如前述输出所示,我们的 Docker 主机的 /var/lib/docker 目录位于 / 分区下。这就是我们将指向 blktrace 监听事件的地方。输入以下命令来开始监听此设备的 I/O 事件:
dockerhost$ blktrace -d /dev/dm-0 -o dump
提示
在 docker run 中使用 --privileged 标志,我们可以在容器内使用 blktrace。这样做将允许我们在提升的权限下挂载调试过的文件系统。
有关扩展容器权限的更多信息,请参考 docs.docker.com/engine/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration。
为了创建一个简单的工作负载,在我们的磁盘中生成 I/O 事件,我们将从容器中创建一个空文件,直到 / 分区的可用空间耗尽。输入以下命令生成该工作负载:
dockerhost$ docker run -d --name dump debian:jessie \
/bin/dd if=/dev/zero of=/root/dump bs=65000
根据我们根分区的可用空间,这个命令可能会很快完成。现在,让我们使用以下命令获取我们刚刚运行的容器的 PID:
dockerhost$ docker inspect -f '{{.State.Pid}}' dump
11099
现在我们知道了生成 I/O 事件的 Docker 容器的 PID,我们可以通过 blktrace 程序的配套工具 blkparse 来查找该 PID。blktrace 程序仅监听 Linux 内核的块 I/O 层的事件,并将结果输出到文件中。blkparse 程序是用来查看和分析这些事件的辅助工具。在我们之前生成的工作负载中,我们可以使用以下命令查找与 Docker 容器的 PID 对应的 I/O 事件:
dockerhost$ blkparse -i dump.blktrace.0 | grep --color " $PID "
...
254,0 0 730 10.6267 11099 Q R 13667072 + 24 [exe]
254,0 0 732 10.6293 11099 Q R 5042728 + 16 [exe]
254,0 0 734 10.6299 11099 Q R 13900768 + 152 [exe]
254,0 0 736 10.6313 11099 Q RM 4988776 + 8 [exe]
254,0 0 1090 10.671 11099 C W 11001856 + 1024 [0]
254,0 0 1091 10.6712 11099 C W 11002880 + 1024 [0]
254,0 0 1092 10.6712 11099 C W 11003904 + 1024 [0]
254,0 0 1093 10.6712 11099 C W 11004928 + 1024 [0]
254,0 0 1094 10.6713 11099 C W 11006976 + 1024 [0]
254,0 0 1095 10.6714 11099 C W 11005952 + 1024 [0]
254,0 0 1138 10.6930 11099 C W 11239424 + 1024 [0]
254,0 0 1139 10.6931 11099 C W 11240448 + 1024 [0]
...
在前面突出显示的输出中,我们可以看到 /dev/dm-0 块偏移量的位置为 11001856,并且完成了一次 1024 字节的数据写入(W)。为了进一步探测,我们可以查看该偏移位置所生成的事件。输入以下命令来过滤出该偏移位置:
dockerhost$ blkparse -i dump.blktrace.0 | grep 11001856
...
254,0 0 1066 10.667 8207 Q W 11001856 + 1024 [kworker/u2:2]
254,0 0 1090 10.671 11099 C W 11001856 + 1024 [0]
...
我们可以看到 kworker 进程正在将写入(W)请求排入队列(Q),这意味着写入操作已经被内核排队。40 毫秒后,注册的写入请求完成了 Docker 容器进程的操作。
我们刚刚执行的调试过程只是通过 blktrace 跟踪块 I/O 事件可以做到的一小部分。例如,我们还可以更详细地探测 Docker 容器的 I/O 行为,找出应用程序中出现的瓶颈。是否有大量的写入操作?读取操作是否如此频繁,以至于需要缓存?拥有实际的事件数据,而不仅仅是内建的 docker stats 命令提供的性能指标,在深入排查问题时非常有帮助。
注意
有关 blkparse 输出的不同值以及如何在 blktrace 中捕获 I/O 事件的更多信息,请参考位于 www.cse.unsw.edu.au/~aaronc/iosched/doc/blktrace.html 的用户指南。
一套故障排除工具
在 Docker 容器内调试应用程序需要一种不同于 Linux 上普通应用程序的方式。然而,实际使用的程序是相同的,因为容器内部的所有调用最终都会传递到 Docker 主机的内核操作系统。通过了解调用如何穿越容器外部,我们可以使用任何其他调试工具来排查问题。
除了标准的 Linux 工具外,还有一些特定于容器的工具,这些工具将前述的标准工具进行了封装,以便更适应容器的使用。以下是其中一些工具:
-
Red Hat 的
rhel-toolsDocker 镜像是一个包含我们之前讨论的各种工具的巨大容器。其文档页面access.redhat.com/documentation/en/red-hat-enterprise-linux-atomic-host/version-7/getting-started-with-containers/#using_the_atomic_tools_container_image展示了如何以正确的 Docker 权限运行该工具,以确保其正常工作。 -
CoreOS 的
toolbox程序是一个小型脚本工具,它使用 Systemd 的systemd-nspawn程序创建一个小型的 Linux 容器。通过复制流行的 Docker 镜像的根文件系统,我们可以安装任何我们需要的工具,而无需将临时调试工具污染 Docker 主机的文件系统。其使用方法已在其网页上进行文档说明,网址为[coreos.com/os/docs/latest/install-debugging-tools.html`](https://coreos.com/os/docs/latest/install-debugging-tools.html)。 -
nsenter程序是一个进入 Linux 控制组进程命名空间的工具。它是docker exec程序的前身,并被认为已经不再维护。要了解docker exec如何诞生的历史,请访问nsenter程序的项目页面,网址为github.com/jpetazzo/nsenter。
总结
请记住,登录到 Docker 主机并不是一种可扩展的方案。在应用程序层面添加监控工具,除了操作系统本身提供的工具外,能够帮助更快速、更高效地诊断未来可能遇到的问题。记住,没人愿意在凌晨两点起来运行tcpdump来调试一个着火的 Docker 容器!
在下一章中,我们将总结并再次审视将基于 Docker 的工作负载推向生产环境所需要的步骤。
第八章:进入生产环境
Docker 起源于 dotCloud 的 PaaS,满足了 IT 部门在快速、可扩展的方式中开发和部署 Web 应用程序的需求。这是为了跟上 Web 使用的日益加速的步伐。保持我们的 Docker 容器在生产环境中持续运行绝非易事。
在本章中,我们将总结你所学到的关于优化 Docker 的内容,并阐明它如何与我们在生产环境中操作 Web 应用程序相关。内容包括以下主题:
-
执行 Web 操作
-
使用 Docker 支持我们的应用程序
-
部署应用程序
-
扩展应用程序
-
进一步阅读关于 Web 操作的一般知识
执行 Web 操作
让 Web 应用程序在互联网上 24/7 全天候运行,既是软件开发上的挑战,也是系统管理上的挑战。Docker 将自己定位为连接这两个领域的纽带,通过创建可以一致构建和部署的 Docker 镜像,来实现这两者的结合。
然而,Docker 并不是 Web 的灵丹妙药。随着 Web 应用程序的日益复杂化,仍然需要了解软件开发和系统管理的基本概念。这种复杂性自然地出现,因为如今,特别是在互联网技术的推动下,Web 应用程序的数量变得更加普及,深入人们的生活。
处理保持 Web 应用程序持续运行的复杂性涉及掌握 Web 操作的方方面面,正如任何通向精通的道路一样,Theo Schlossnagle 将其归结为四个基本追求:知识、工具、经验和纪律。知识指的是像海绵一样吸收互联网上、在会议和技术交流会上获得的关于 Web 操作的信息。理解这些信息,并知道如何从噪音中筛选出有价值的信号,将帮助我们在生产环境中解决应用架构问题。随着 Docker 和 Linux 容器的日益普及,了解支撑它们的不同技术并深入学习其基础非常重要。在第七章,容器故障排除一章中,我们展示了常规的 Linux 调试工具在调试运行中的 Docker 容器时依然非常有用。通过了解容器如何与 Docker 主机的操作系统进行交互,我们能够调试 Docker 中出现的问题。
第二个方面是掌握我们的工具。本书基本围绕如何掌握 Docker 的使用展开,探讨了 Docker 的工作原理以及如何优化其使用。在第二章,优化 Docker 镜像中,我们学习了如何根据 Docker 构建镜像并使用其底层的写时复制文件系统运行容器的方式来优化 Docker 镜像。这一过程得到了我们在 Web 运维中对于为何优化 Docker 镜像在可扩展性和可部署性方面至关重要的知识支持。掌握如何有效使用 Docker 并非一蹴而就。这种掌握只能通过在生产环境中持续使用 Docker 的实践来获得。没错,我们可能会在凌晨 2 点因第一次将 Docker 部署到生产环境中而接到报警,但随着时间的推移,我们通过不断使用获得的经验将使 Docker 成为我们身体和感官的延伸,正如 Schlossnagle 所说。
通过应用知识并持续使用我们的工具,我们获得了可以在未来借鉴的经验。这帮助我们根据过去做出的错误决策做出正确的判断。这里是容器技术的理论与在生产环境中运行 Docker 的实践碰撞的地方。Schlossnagle 提到了在 Web 运维中获得经验的挑战,以及如何从错误判断中生还并从中汲取经验。他建议拥有有限的环境,以便错误决策的影响最小化。Docker 是获得这种类型经验的最佳场所。通过拥有标准化的、随时可以部署的 Docker 镜像,初级 Web 运维工程师可以拥有自己的实验环境,从错误中学习和成长。而且,由于 Docker 环境在向生产环境推进时非常相似,这些工程师将能够利用自己已经积累的经验。
掌握 Web 操作的最后一部分是自律。然而,作为一个相对年轻的学科,这些过程尚未得到很好的定义。即使有了 Docker,人们也花了几年时间才意识到容器技术的最佳使用方式。在此之前,将整个厨房用具都包含在 Docker 镜像中是非常常见的做法。然而,正如我们在第二章,优化 Docker 镜像,中看到的,减少 Docker 镜像的体积有助于管理我们必须调试的应用程序的复杂性。这使得在第七章,故障排除容器,中的调试体验更加简单,因为我们需要考虑的组件和因素更少。使用 Docker 的这些技巧并不是通过阅读 Docker 博客(好吧,有些是)就能一蹴而就的。它涉及到持续接触 Docker 社区的知识,以及在各种生产环境中实践使用 Docker。
在接下来的部分中,我们将展示使用 Docker 容器技术的理论与实践如何帮助我们运营 Web 应用。
使用 Docker 支持 Web 应用
下图展示了一个典型的 Web 应用架构。我们有一个负载均衡层,它接收来自互联网的流量,然后这些流量,通常由用户请求组成,将以负载均衡的方式转发到一组 Web 应用服务器。根据请求的性质,Web 应用可能会从持久存储层获取一些状态,类似于数据库服务器:

如前图所示,每一层都在 Docker 容器中运行,并部署在 Docker 主机上。通过这种每个组件的布局,我们可以利用 Docker 一致的方式来部署负载均衡器、应用程序和数据库,正如我们在第二章,优化 Docker 镜像,和第六章,负载均衡中所做的那样。然而,除了每个 Docker 主机中的 Docker 守护进程,我们还需要支持基础设施来以可扩展的方式管理和观察我们整个 Web 架构的堆栈。在右侧,我们可以看到每个 Docker 主机都会将诊断信息——例如,应用程序和系统事件,如日志消息和度量指标——发送到我们的集中式日志记录和监控系统。我们在第四章,监控 Docker 主机和容器中部署了这样的系统,当时我们部署了 Graphite 和 ELK 堆栈。此外,可能还有另一个系统监听日志和度量中的特定信号,并向负责我们基于 Docker 的 Web 应用堆栈操作的工程师发送警报。这些事件可能与关键事件相关,比如我们应用程序的可用性和性能,这些问题需要我们采取行动以确保我们的应用程序按照预期满足业务需求。我们使用内部管理系统(如 Nagios)或第三方系统(如 PagerDuty)来处理我们的 Docker 部署,及时向我们发出警报,例如在凌晨 2 点唤醒我们进行更深入的监控和故障排除,就像在第四章,监控 Docker 主机和容器,和第七章,故障排除容器中所提到的那样。
图的左侧包含了配置管理系统。这是每个 Docker 主机下载所需配置的地方,以确保它能够正常工作。在第三章,使用 Chef 自动化 Docker 部署中,我们使用 Chef 服务器存储 Docker 主机的配置。它包含了 Docker 主机在我们架构中的角色等信息。Chef 服务器存储了哪些 Docker 容器需要在每个层中运行,以及如何使用我们编写的 Chef 配方来运行它们。最后,配置管理系统还告诉我们的 Docker 主机 Graphite 和 Logstash 监控和日志记录的端点位置。
总的来说,除了 Docker 外,还需要各种组件来支持我们生产环境中的 web 应用程序。Docker 使我们能够轻松地设置这些基础设施,因为它在部署容器时具有速度和灵活性。然而,我们不应该忽视对这些支持基础设施的研究。在接下来的章节中,我们将通过你在前面章节中学到的技能,了解如何在 Docker 中部署 web 应用程序的支持基础设施。
部署应用程序
在调优 Docker 容器性能时,一个重要的组件是反馈,它能告诉我们是否正确地改善了我们的 web 应用程序。Graphite 和 ELK 堆栈的部署在第四章,监控 Docker 主机和容器,让我们能够看到在基于 Docker 的 web 应用程序中所做的更改所产生的效果。虽然收集反馈很重要,但更重要的是及时收集反馈。因此,我们的 Docker 容器的部署需要以快速和可扩展的方式进行。正如我们在第三章,使用 Chef 自动化 Docker 部署,中所做的那样,能够自动配置 Docker 主机是实现快速和自动化部署系统的重要组件。其余组件在以下图表中描述:

每当我们提交应用程序代码或描述其运行和构建方式的Dockerfile的更改时,我们需要支持的基础设施将这一更改传播到我们的 Docker 主机。在前面的图示中,我们可以看到,提交到版本控制系统(如 Git)的更改会触发构建新版本代码的操作。通常通过 Git 的 postreceive 钩子以 shell 脚本的形式完成此操作。这些触发器会被构建服务器接收,例如 Jenkins。传播更改的步骤类似于我们在第六章中实施的蓝绿部署过程,负载均衡。在接收到构建我们提交的新更改的触发信号后,Jenkins 会查看我们代码的新版本,并运行docker build来创建 Docker 镜像。构建完成后,Jenkins 会将新的 Docker 镜像推送到 Docker 注册表(例如 Docker Hub),这正如我们在第二章,优化 Docker 镜像中设置的那样。此外,它还会通过更新我们在第三章,使用 Chef 自动化 Docker 部署中布置的 Chef 服务器配置管理系统中的条目,间接更新目标 Docker 主机。借助 Chef 服务器和 Docker 注册表中可用的更改工件,我们的 Docker 主机现在会注意到新的配置,并在 Docker 容器中下载、部署并运行我们 Web 应用程序的新版本。
在下一节中,我们将讨论如何使用类似的过程来扩展我们的 Docker 应用。
扩展应用
当我们从监控系统收到警报(如在第四章,监控 Docker 主机和容器中所述)提示运行我们 Web 应用程序的 Docker 容器池未加载时,就是时候进行扩展了。我们通过在第六章,负载均衡中使用负载均衡器来实现这一目标。下图展示了我们在第六章,负载均衡中运行的命令的高层架构:

一旦我们决定进行扩展并添加一个额外的 Docker 主机,我们可以通过一个扩展协调器组件来自动化这个过程。这可以是一系列简单的 Shell 脚本,我们将把它们安装在构建服务器中,例如 Jenkins。该协调器将基本上请求云提供商 API 创建一个新的 Docker 主机。然后,这个请求将配置 Docker 主机并运行初始引导脚本,从我们的配置管理系统中下载配置,这部分内容在第三章,使用 Chef 自动化 Docker 部署中有详细说明。这将自动设置 Docker 主机,从 Docker 注册表下载我们的应用程序的 Docker 镜像。在整个配置过程完成后,我们的扩展协调器将更新 Chef 服务器中的负载均衡器,添加新的应用服务器列表以转发流量。因此,下次chef-client在负载均衡 Docker 主机中轮询 Chef 服务器时,它将添加新的 Docker 主机并开始将流量转发到该主机。
正如我们所注意到的,学习如何自动化设置 Docker 主机在第三章,使用 Chef 自动化 Docker 部署,对于实现我们在第六章,负载均衡中所做的可扩展负载均衡架构设置至关重要。
进一步阅读
帮助我们的 Web 应用程序使用 Docker 的支持架构仅仅是表面上的一点。 本章中的基本概念将在以下书籍中详细描述:
-
Web Operations: Keeping the Data On Time,由 J. Allspaw 和 J. Robbins 编辑。2010 年,O'Reilly Media 出版。
-
Continuous Delivery,由 J. Humble 和 D. Farley 编著。2010 年,Addison-Wesley 出版。
-
Jenkins: The Definitive Guide,J. F. Smart。2011 年,O'Reilly Media 出版。
-
The Art of Capacity Planning: Scaling Web Resources,J. Allspaw。2008 年,O'Reilly Media 出版。
-
Pro Git,S. Chacon 和 B. Straub。2014 年,Apress 出版。
总结
在本书中,你学到了很多关于 Docker 如何工作的知识。除了 Docker 的基础知识外,我们还回顾了一些关于 Web 操作的基本概念,以及它如何帮助我们实现 Docker 的全部潜力。你掌握了 Docker 和操作系统的关键概念,以便更深入地理解背后发生的事情。你现在知道了我们的应用程序是如何从代码走向 Docker 主机操作系统中的实际调用的。你学到了很多有关在生产环境中以可扩展和可管理的方式部署和排除 Docker 容器故障的工具。
然而,这不应阻止你继续开发和实践使用 Docker 在生产环境中运行我们的 web 应用程序。我们不应害怕犯错,而应该在最佳实践中积累经验,逐步掌握在生产环境中运行 Docker 的方法。随着 Docker 社区的发展,这些实践也在通过社区的集体经验不断演进。因此,我们应该继续并保持规律性地学习我们从一开始就逐步掌握的基础知识。不要犹豫,在生产环境中使用 Docker!









浙公网安备 33010602011771号