Docker实践:Docker日常中的一些技巧

同任何相对复杂的软件项目一样,Docker有很多细节问题和缺点,如果想要让用户体验尽可能地流畅,知道这些很重要。

本章讲述的一些技巧将会向读者展示其中更为重要的一些部分,而且会介绍如何使用一些第三方构建的外部工具来解决自身问题。不妨把它看作一个Docker工具箱。

6.1 保持阵型

如果你跟我们一样(并且有心关注本书),那么你对Docker的依赖将会与日俱增,这也就意味着会在选定的宿主机上启动大量容器,然后下载更多的镜像。

随着时间的流逝,Docker将会消耗越来越多的资源,一些容器和卷的清理需要摆上日程。我们将会展示怎么做以及为什么这样做。我们也会介绍一些用来保持Docker环境干净整洁的可视化工具,以便让不喜欢敲命令行的用户可以从中解脱出来。

正在运行的容器都很好,但是用户很快会发现自己想要的不仅仅是在前台启动一个单条命令。我们会一起来看看如何在不“杀死”该容器的前提下退出一个正在运行的容器,还会看看如何在一个正在运行的容器内执行命令。

技巧41 运行Docker时不加sudo

Docker守护进程以root用户身份在机器的后台运行,这给了它莫大的权力,同时它又是对你,即终端用户开放的。需要使用sudo是一个结果,但是这样做不太方便,而且也会造成一些第三方Docker工具无法使用。

问题

想要无须sudo便可以执行docker命令。

解决方案

官方解决方案是把自己加到docker组。Docker通过一个用户组围绕着Docker Unix域套接字来管理权限。为安全起见,发行版默认不会将用户加到该用户组里,因为这样做会开放系统完整的root访问权限。

把自己加到该用户组后,用户便能以自己的身份使用docker命令:

$ sudo addgroup -a username docker

重启Docker然后完全注销并再次登录,或者更简单点,重启机器。现在执行Docker命令时不用再留意键入sudo或设置别名了。

讨论

对于本书后面部分用到的一系列工具来说,这是一项极其重要的技巧。一般来说,任何想要和Docker通信的对象(无须在容器里启动)都需要能够访问Docker套接字,这需要使用sudo或者使用本技巧里提到的设置。技巧76里引入的Docker Compose是Docker公司的官方工具,也是这类工具的一个示例。

技巧42 清理容器

Docker新手经常抱怨的一点便是,在短时间内,用户可能在系统上残留许多不同状态的容器,而且没有一个标准工具通过命令行管理这些容器。

问题

想要清理系统上的残留容器。

解决方案

设置一个别名来执行清理旧容器的命令。这里最简单的办法是删除所有容器。显然,这是一个有风险的方案,只应在确定这是预期行为的时候使用。下列命令将会删除宿主机上的所有容器。

$ docker ps -a -q | \  ⇽--- 获取所有容器ID的列表,包括正在运行的以及已停止的,然后将它们传给……
 xargs --no-run-if-empty docker rm -f  ⇽--- ……docker rm -f命令,被传入的任意容器将会被删除,即使它们还处于运行状态

简单介绍一下xargs命令,它会获取输入的每一行内容,并将它们全部作为参数传递给后续命令。为了防止报错,我们这里传入了一个额外参数--no-run-if-empty,这可以避免在前面的命令完全没有输出的情况下执行该命令。

如果有正在运行的容器想要保留,但是又想删除所有已经退出的容器,那么不妨过滤一下docker ps命令返回的条目:

docker ps -a -q --filter status=exited | \  ⇽--- --filter标志会告知docker ps命令想要返回的容器。在这种情况下限制成状态为已经退出的那些容器。也可以选择处于正在运行中或者正在重启状态的容器
 xargs --no-run-if-empty docker rm  ⇽--- 这次不用再强行删除容器,因为根据给定的过滤参数,它们本身就不应该处于运行状态

事实上,删掉所有已停止的容器是一个很常见的用例,为此Docker专门添加了一条命令:docker container prune。然而,这条命令仅限于该用例,要进行任何更复杂的操作,仍然需要回过头来参考本技巧里介绍的命令。

作为更高级用例的示范,下列命令将会列出所有返回非零错误码的容器。如果系统上有许多容器,用户想要自动检查并删除那些异常退出的任意容器,就可能需要这样做:

comm -3 \  ⇽--- 执行comm命令来比较两个文件内容的差异。加上-3 参数将不会显示同时出现在两个文件里的行内容(这些容器的退出码都是0),然后输出其他不同的部分
 <(docker ps -a -q --filter=status=exited | sort) \  ⇽--- 找出退出的容器 ID,给它们排序,然后以文件形式传给comm
<(docker ps -a -q --filter=exited=0 | sort) | \  ⇽--- 找出退出码为0的容器,给它们排序,然后以文件形式传给comm
 xargs --no-run-if-empty docker inspect > error_containers  ⇽--- 对非0退出码(comm命令管道的输出)的容器执行docker inspect,并将输出结果保存到error_containers文件中

提示 

也许你还没看到过这种用法,bash里的<(command)语法被称为进程替换。它允许把一个命令的输出结果作为文件,传给其他命令,这在无法使用管道输出的时候非常有用。

上述示例相对比较复杂,但是它展示了将不同的工具命令组合在一起的威力。它会输出所有已停止的容器的ID,然后挑出那些非0退出码的容器(即那些以异常方式退出的容器)。如果读者还在努力理解这个用法,不妨先单独执行每条命令,然后理解它们的含义,这样有助于了解整个过程。

像这样的命令可以用来在生产环境里采集容器信息。用户可能想要对它做些调整,改为执行一个cron定时任务来清除正常退出的容器。


将单行代码包装成命令

可以给命令设置别名,以便在登录到宿主机后更容易操作。为了达成这一点,需要在~/.bashrc文件里添加如下代码:

alias dockernuke='docker ps -a -q | \
xargs --no-run-if-empty docker rm -f'

然后,在下一次登录时,从命令行执行dockernuke,将删除在系统上找到的任何Docker容器。

我们发现这样做节省的时间是相当可观的。但是要小心!这种方式同样也非常容易误删生产环境的容器,我们可以证明。即使足够小心,不去删除正在运行的容器,仍然可能会误删那些没有运行但仍然有用的纯数据容器。


讨论

本书介绍到的许多技巧的最终目的都是创建容器,尤其是在技巧76介绍到的Docker Compose以及有关编排的章节里——毕竟,编排都是关于如何管理多个容器的。用户也许会发现这里讨论到的命令用于清理机器(本地或远程)很有价值,在完成每个技巧后可以获得一个全新的环境。

技巧43 清理卷

尽管卷是Docker提供的一个强大功能,与之伴随而来的也有一些显著的运维缺陷。由于卷可以在不同的容器之间共享,因此在挂载它们的容器被删除时无法清空这些卷。试想一下图6-1中描述的场景。

Docker实践:Docker日常中的一些技巧

图6-1 当容器被删除时/var/db下会发生什么

“简单!”你可能会这样想,“在最后一个引用的容器被删除时把卷删掉不就行了!”事实上,Docker可以采取这种手段,这也是垃圾回收式编程语言从内存中删除对象时所采用的方法:当没有其他对象引用它时,它便可以被删除。

但是Docker认为这可能会让人们不小心丢失重要的数据,而且最好把是否在删除容器的时候删除卷的决定权交给用户。这样做带来的一个不幸的副作用便是,默认情况下,卷会一直保留在Docker守护进程所在的宿主机磁盘上,直到它们被手动删除。

如果这些卷填满了数据,磁盘可能会被装满,因此最好关注一下管理这些孤立卷的方法。

问题

挂载到宿主机上的孤立Docker卷用掉了大量的磁盘空间。

解决方案

在调用docker rm命令时加上-v标志,或者如果忘记了,使用docker volumes子命令来销毁它们。

在图6-1描述的场景中,如果在调用docker rm时总是加上-v标志可以确保/var/db最后被删除掉。-v标志会将那些没有被其他容器挂载的关联卷一一删除。幸好,Docker很聪明,它知道是否有其他容器挂载该卷,因此不会出现什么意外尴尬的情形。

最简单的方式莫过于养成在删除容器时加上-v标志这样的好习惯。这样可以保留对容器是否删除卷的控制权。而这种做法的问题在于用户可能不想每次都删除卷。如果用户正在写入大量数据到这些卷,极有可能不希望丢失这些数据。此外,如果养成了这样的习惯,很有可能就会变成自动的了,而用户将会在删除某些重要东西之后才反应过来,但为时已晚。

在这类情况下,用户可以使用一个经过许多人抱怨并且涌现出众多第三方解决方案之后添加到Docker的命令:docker volume prune。这条命令将会删除所有未使用的卷:

$ docker volume ls  ⇽--- 执行命令列出Docker所知的卷
 DRIVER             VOLUME NAME
 local              80a40d34a2322f505d67472f8301c16dc75f4209b231bb08faa8ae48f
➥ 36c033f  ⇽--- 
 local              b40a19d89fe89f60d30b3324a6ea423796828a1ec5b613693a740b33
➥ 77fd6a7b
 local              bceef6294fb5b62c9453fcbba4b7100fc4a0c918d11d580f362b09eb
➥ 58503014  ⇽--- 宿主机上存在的卷,无论是否在使用
$ docker volume prune  ⇽--- 执行命令删除未使用的卷
 WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] y  ⇽--- 确认删除卷
 Deleted Volumes:
 80a40d34a2322f505d67472f8301c16dc75f4209b231bb08faa8ae48f36c033f  ⇽--- 
 b40a19d89fe89f60d30b3324a6ea423796828a1ec5b613693a740b3377fd6a7b  ⇽--- 已经被删除的卷

Total reclaimed space: 230.7MB

如果想要跳过提示确认步骤,也许可以用一个自动化脚本,在执行docker volume prune时带上-f选项来跳过这一步。

提示 

如果想要恢复一个未被删除但是已经不再被任何容器引用的卷里的数据,可以使用docker volume inspect来找出卷所在的目录(像是/var/lib/docker/volumes下)。随后可以用root用户的身份浏览。

讨论

删除卷可能不是需要经常执行的操作,因为容器里的大文件通常是从宿主机挂载的,并不会存放在Docker数据目录里。但是值得大约每周清理一次,避免它们堆积,尤其是当你使用技巧37里的数据容器时。

技巧44 无须停止容器,从容器里解绑

使用Docker时,你常常会发现自己打开了一个交互式shell,但是一旦退出shell,容器便会被终止,因为它是容器的主进程。幸运的是,有办法可以做到和一个容器解绑(而且,如果愿意,还可以用dockerattach命令再连到容器里)

问题

想要退出一个容器的交互会话,同时不停掉它。

解决方案

使用Docker内置的按键组合从容器里退出。Docker很有建设性地实现了一个不太可能被其他应用使用也不太可能被意外按到的按键组合。

假设我们执行docker run -t -i -p 9005:80 ubuntu /bin/bash命令启动了一个容器,然后用apt-get安装了一个Nginx Web服务器。我们想通过一个快捷的到localhost:9005的curl命令来测试该Web服务器能否在宿主机上被访问到。

先按组合键Ctrl+P然后再按组合键Ctrl+Q。注意,不是3个键一起按!

注意 

如果运行容器时带上了–rm标志,那么在解绑后一旦容器被终止仍然会被删除,无论是命令执行完毕还是手动把它停掉。

讨论

如技巧2所述,如果我们之前已经启动了一个容器,却忘了在后台启动,本技巧会很有用。如果想检查容器的运行情况或提供一些输入,它还允许用户和容器自由地绑定和解绑。

技巧45 使用Portainer管理Docker守护进程

在演示Docker时,很难表现出容器和镜像之间的差异——从终端里的输出看不出来。此外,如果想要从多个容器里杀掉并删除一个特定的容器,Docker命令行工具对于这种场景也不太友好。创建一个即点即用的工具来管理宿主机上的镜像和容器可以解决这个问题。

问题

想要不通过命令行管理宿主机上的容器和镜像。

解决方案

试试Portainer,这是一款由Docker核心贡献者之一开发的工具。Portainer的前身是DockerUI。由于没有先决条件,可以直接跳到执行步骤:

$ docker run -d -p 9000:9000 \
 -v /var/run/docker.sock:/var/run/docker.sock \
 portainer/portainer -H unix:///var/run/docker.sock

执行上述命令将会在后台启动一个portainer容器。如果现在访问 http://localhost:9000 ,可以在看板上看到机器上运行的Docker的简要信息。

容器管理功能可能是这里面最有用的部分之一 ——转到“Containers”页面,我们会看到正在运行的容器列表(包括portainer容器本身),还提供选项可以展示所有容器。在这里,你可以对容器执行批量操作(如杀掉它们),或者点击一个容器的名字,深入了解该容器的详细信息,而且可以执行该容器相关的一些单个操作。例如,可以看到删除一个正在运行的容器的选项。

“Images”页面看起来和“Containers”页面非常相似,并且还允许选择多个镜像然后执行一些批量操作。点击镜像的ID会提供一些有趣的选项,比如基于该镜像创建一个容器以及给镜像打标签等。

记住,Portainer可能会落后于Docker官方提供的功能——如果想要使用最新最强大的功能,那么可能不得不选择命令行。

讨论

Portainer是Docker众多的图形工具里的其中一款,也是这里面最受欢迎的,拥有众多功能并且持续迭代的工具之一。举个例子,你可以使用它来管理远程机器,也许会是技巧32里在这些机器上启动容器之后用到。

技巧46 生成Docker镜像的依赖图

Docker的文件分层系统是一个非常强大的理念,它可以节省空间,而且可以让软件的构建变得更快。但是一旦启用了大量的镜像,便很难搞清楚镜像之间是如何关联的。docker images -a命令会返回系统上所有镜像层的列表,但是对于理解它们之间的关联关系而言,这不是一个友好的方式——使用Graphviz可以更方便地通过创建一个镜像树并做成镜像的形式来可视化镜像之间的关系。

这也展示了Docker在把复杂的任务变得简单方面的强大实力。在宿主机上安装所有的组件来生产镜像时,老的方式可能会包含一长串容易出错的步骤,但是对Docker来说,这就变成了一条相对失败较少的可移植命令。

问题

想要以树的形式将存放在宿主机上的镜像可视化。

解决方案

使用一个我们之前创建的镜像(基于CenturyLink Labs的一个镜像)配合这项功能输出一个PNG图片或者获取一个Web视图。此镜像包含了一些使用Graphviz生成PNG图片文件的脚本。

本技巧使用的Docker镜像放在dockerinpractice/docker-image-graph。时间长了该镜像可能会过期然后停止工作,可以通过执行代码清单6-1中的命令确保生成最新的镜像。

代码清单6-1 构建一个最新的docker-image-graph镜像(可选)

$ git clone https://github.com/docker-in-practice/docker-image-graph
$ cd docker-image-graph
$ docker build -t dockerinpractice/docker-image-graph

在run命令里需要做的就是挂载Docker服务器套接字,然后一切便准备就绪,如代码清单6-2所示。

代码清单6-2 生成一个镜像的层树

$ docker run --rm \  ⇽--- 在生成镜像之后删除容器
 -v /var/run/docker.sock:/var/run/docker.sock \  ⇽--- 挂载 Docker 服务器的Unix 域套接字,以便可以在容器里访问Docker服务器。如果已经更改了Docker守护进程的默认配置,这将不会奏效
 dockerinpractice/docker-image-graph > docker_images.png  ⇽--- 指定一个镜像然后生成一个PNG图片作为制品

图6-2以PNG图片形式展示了一台机器的镜像树。从这张图片可以看出,node和golang:1.3镜像拥有一个共同的根节点,然后golang:runtime只和golang:1.3共享全局的根节点。类似地,mesosphere镜像和ubuntu-upstart镜像也是基于同一个根节点构建的。

读者可能会好奇这棵树上的全局根节点是什么。它是一个叫作scratch的伪镜像,实际上大小为0字节。

讨论

在构建更多的Docker镜像时,也许作为第9章里持续交付的一部分,跟踪一个镜像的历史以及它所基于的内容可能会很麻烦。如果试图通过共享更多层精简镜像大小的方式来加快交付速度,这一点尤为重要。定期拉取所有镜像并生成图谱是一个追踪的好办法。

Docker实践:Docker日常中的一些技巧

图6-2 一棵镜像树

技巧47 直接行动:在容器上执行命令

在Docker早期,许多用户会在他们的镜像里添加SSH服务,这样一来便可以从外部通过shell来访问它们。Docker不主张这样做,它认为这相当于把容器当成一台虚拟机(而我们知道,容器并不是虚拟机),并且这给本不应该需要它的系统带来了额外的进程开销。很多人对此持反对意见的原因在于,一旦容器启动了,没有一个简便的办法进到容器里面。结果便是,Docker引入了exec命令,它是一个更优雅地解决干涉和检索启动后的容器内部问题的解决方案。我们这里也将讨论此命令。

问题

想要在一个正在运行的容器里执行一些命令。

解决方案

使用dockerexec命令。

下列命令会在后台(带上-d标志)启动一个容器,然后告诉它一直休眠(不做任何事情)。我们把这条命令命名为sleeper。

docker run -d --name sleeper debian sleep infinity

现在已经启动了一个容器,可以用Docker的exec命令对它执行一些操作。该命令可以看成有3种基本模式,如表6-1所示。

表6-1 Docker exec 模式

Docker实践:Docker日常中的一些技巧

我们先介绍一下基本模式。下列命令在sleeper容器内部执行了一个echo命令。

$ docker exec sleeper echo "hello host from container"
hello host from container

注意,该命令的结构和dockerrun命令非常相似,但是把镜像ID替换成一个正在运行的容器的ID。echo命令指代的是容器里面的echo二进制文件,而非容器外部的。

守护进程模式会在后台执行命令,用户无法在终端看到输出结果。这可能适用于一些常规的清理任务,在这些任务中,你希望敲完即走,如清理日志文件。

$ docker exec -d sleeper \  ⇽--- 执行命令时加上-d标志即可在后台以守护进程的形式运行,类似dcoker run
 find / -ctime 7 -name '*log' -exec rm {} \;  ⇽--- 删除所有在最近7天没有做过更改并且以log结尾的文件
 $  ⇽--- 无论需要多长时间完成这一操作,该命令都会立即返回

最后,我们来试试交互模式。这种模式允许用户在容器里执行任何想要执行的命令。要启用这一功能,通常需要指定用来在运行时交互的shell,在如下代码里便是bash:

$ docker exec -i -t sleeper /bin/bash
root@d46dc042480f:/#

-i和-t参数同我们所熟悉的dockerrun做着相同的事情——它们会让命令成为可交互的,然后设置一个TTY设备,以便shell可以正常工作。在执行该命令后,用户便拿到了一个在容器里运行的命令提示符。

讨论

当出现问题或者想要弄清楚容器在做什么时,跳到容器里是必不可少的调试步骤。往往不太可能使用技巧44里提到的绑定和解绑方法,因为容器内的进程通常运行在前台,无法访问shell提示符。由于exec允许用户指定想要运行的二进制文件,这便不再是问题……只要容器文件系统上实际存在那份想要运行的二进制文件即可。

特别的是,如果你使用技巧58创建一个带有单个二进制文件的容器,那么将无法启动shell。在这种情况下,可能想坚持采用技巧57作为允许exec执行的低开销办法。

技巧48 你在容器里吗

在创建容器时,通常会把运行逻辑放到一个shell脚本里,很少会尝试直接在Dockerfile里编写脚本。又或者,你可能在容器运行时用到了各种脚本。无论哪种方式,这些执行的任务通常都需要经过仔细定制,以便能够运行在容器里,并且运行在一台“常规”机器上可能会搞破坏。在这种情况下,设置一些安全防护,防止在容器外部意外执行是很有用的。

问题

用户代码需要知道是否是在一个Docker容器里操作。

解决方案

检查/.dockerenv文件是否存在。如果存在,那么很可能在一个Docker容器里。

注意,这并不是100%确定的——如果任何人或任何事物把/.dockerenv文件删掉,这个检查就会给出误导性的结果。这些情况不太可能发生,但是最坏的情况便是用户得到错误的诊断结果而没有不良影响。用户会认为自己不在Docker容器里,并且在最坏的情况下不会运行潜在的破坏性代码。

一个更现实的情况是,在较新的Docker版本里(或者使用的是实现这一行为之前的版本)已经更改或删除了这种未记录的Docker行为。

这些代码可能是启动bash脚本的一部分,如代码清单6-3所示,其后是剩余的启动脚本代码。

代码清单6-3 如果在容器外运行,如下shell脚本会运行失败

#!/bin/bash
if ! [ -f /.dockerenv ]
then
    echo 'Not in a Docker container, exiting.'
    exit 1
fi

当然,如有需要,可以使用相反的逻辑来确认自己是不是运行在容器外面,如代码清单6-4所示。

代码清单6-4 如果在容器里运行,如下shell脚本会运行失败

#!/bin/bash
if [ -f /.dockerenv ]
then
    echo 'In a Docker container, exiting.'
    exit 1
fi

上述示例使用bash命令来确认文件是否存在,但是绝大多数编程语言有自己的办法来确认容器(或宿主机)文件系统里是否存在某些文件。

讨论

用户可能想知道这种情况多久出现一次。作为一个时常讨论的话题,它经常出现在Docker论坛里,关于这是否是一个有效的用例,又或者是应用程序设计方面存在其他问题,这块仍然存在争议。

撇开这些争议不提,用户很容易陷入需要根据自己是否在Docker容器里来切换代码路径的情况。我们经历过的一个这样的例子便是使用Makefile来构建一个容器。

6.2 小结

  • 用户可以配置自己的机器,让自己可以不带sudo运行Docker。
  • 使用内置的Docker命令来清理未使用的容器和卷。
  • 以一种全新的方式使用外部工具来公开容器的相关信息。
  • docker exec命令是进入一个正在运行的容器内部的正确途径——抵制安装SSH。

本文摘自《Docker实践(第2版)》

Docker实践:Docker日常中的一些技巧

  • 深入浅出Docker源码分析,畅销容器与容器云实践教程升级版
  • 基于Docker1.13,114个实战技巧
  • 解决Docker的应用问题,并提供源代码

1.畅销Docker容器实践教程升级版,编写时参考的Docker版本是Docker 1.13;
2.114个实战技巧为读者提供解决方案以及一些细节和技巧方面的实践经验;
3.提供配套源代码下载。
本书详细介绍了一些坚实可靠的、经过检验的Docker技术,如替换虚拟机(VM)、启用微服务架构、高效网络建模、离线生产和建立容器驱动的持续交付过程等,让开发人员能够按照手册风格的“问题-解决方案-讨论”模式探索真实案例,并学习如何将这些经验应用到自己的开发项目中。

本书由浅入深地讲解了Docker的相关内容,涵盖从开发环境到DevOps流水线,再一路到生产环境的整个落地过程以及相关的实用技巧。书中介绍Docker的核心概念和架构,以及将Docker和开发环境有机、高效地结合起来的方法,包括背Docker用作轻量级虚拟机、构建容器、宿主机编排、配置管理、精简镜像等。不仅如此,本书还通过“问题-解决方案-讨论”的形式,将Docker如何融入DevOps流水线、如何在生产环境落地等一系列难题拆解成114个相关的实用技巧,为读者提供解决方案以及一些细节和技巧方面的实践经验。阅读本书,读者学到的不只是Docker,还包括持续集成、持续交付、构建和镜像管理、容器编排等相关领域的一线生产经验。本书编写时一些案例参考的Docker版本是Docker 1.13。

猜你喜欢

转载自blog.csdn.net/epubit17/article/details/113788092