第12章 Docker生产环境实践——应对各项挑战

    本章主要内容

    • 绕过Docker的命名空间功能直接使用宿主机的资源
    • 通过调整存储来获取更多空间
    • 使用宿主机工具直接调试容器的网络
    • 跟踪系统调用来确定宿主机上的容器无法正常工作的原因

    在本章中,我们将讨论当Docker的抽象不工作时我们能做些什么。这些主题会涉及Docker 的底层机制,以及理解为何要采用这样的解决方案。在此过程中我们旨在让读者更深入地了解使用Docker时的一些陷阱以及应对方法。

    虽然Docker试图将应用程序从其运行的宿主机中抽象出来,但是我们不能完全忽略宿主机。为了提供这样的抽象,Docker必须添加几个中间层。这些层可能会影响正在运行的系统。而且有时我们需要理解这些层的作用,以便解决或者变相绕过一些运维方面的难题。

    本节我们将讨论如何绕过这些抽象层,最终得到一个Docker很少侵入的Docker容器。我们也将展示,尽管Docker似乎高度抽象了所用存储的细节,但是有时候这样做反而会遇到麻烦。

    我们在技巧19中讨论了卷,它是最常用的绕过Docker抽象的手段。通过使用卷,用户可以方便地与宿主机共享文件,可以在镜像层外保存大文件。而且应用程序访问这些卷的速度比访问容器文件系统要快得多,这是因为一些存储后端给某些工作负载带来了巨大的开销——这一点并不是对所有应用程序都那么重要,但是在某些情况下会很重要。

    Docker 为了给每个容器提供独立的网络而设置的网络接口则会带来另一个性能问题。就像文件系统性能一样,网络性能肯定不是每个人都会遇到的瓶颈,但是读者可能要按需进行基准测试(尽管网络调优的细节已经超出了本书的范围)。或者,读者可能有其他原因需要直接绕过Docker网络,例如,一台开放了随机端口监听的服务器可能无法很好地与Docker指定映射端口范围的方式相契合,特别是如果要为此直接映射整段的端口的话,宿主机的这段端口就会一直被占用,不论服务是否真的在使用它们。

    无论什么样的原因,有时候Docker这样的抽象会是一个阻碍,因此,Docker也为那些有需求的用户提供了绕开其限制的能力。

    问题

    想要从容器访问宿主机的资源。

    解决方案

    在运行docker run命令时带上宿主机选项和卷标志。

    讨论

    Docker提供了几种方法来绕过Docker使用的内核命名空间功能。

    什么是命名空间 内核命名空间是内核提供给程序的一个服务,允许它们以某种方式获取全局资源的视图,使这些资源看起来像是提供给自己的单独实例。例如,一个程序可以请求一个网络命名空间,看上去就像是一个完整的网络栈。Docker使用和管理这些命名空间来创建其容器。

    表12-1总结了Docker如何使用命名空间,以及如何有效地关闭它们。

    表12-1 命名空间和Docker

    内核命名空间

    描  述

    是否在Docker中使用

    “关闭”选项

    Network

    网络子系统

    —net=host

    IPC

    进程间通信:共享内存、信号量等

    —ipc=host

    UTS

    主机名和NIS域

    —uts=host

    PID

    进程ID

    —pid=host

    Mount

    挂载点

    —volume,—device

    User

    用户和用户组ID

    N/A

    标志不可用? 如果这些标志不可用,很可能是因为所使用的Docker版本过时了。

    如果应用程序需要大量使用共享内存,例如,想让容器和主机共享内存空间,可以使用—ipc=host标志来实现这一点。这种用法比较高级,因此我们将主要关注其他一些更常见的用法。

    Docker目前还未使用Linux内核的用户命名空间功能,不过Docker正在努力实现[1]

    1.网络和主机名

    要使用宿主机的网络,可以在运行容器时将—net标志设置为host,如下所示:

    1. user@yourhostname:/$ docker run -ti —net=host ubuntu /bin/bash
    2. root@yourhostname:/#

    不难发现,这与使用了网络命名空间的容器的不同之处在于,容器中的主机名与宿主机的主机名是相同的。从实践角度讲,这可能会导致混乱,因为它不能清楚地告诉用户,是在一个容器里。

    在一个网络隔离的容器中,可以使用netstat来快速验证启动时没有网络连接:

    1. host$ docker run -ti ubuntu
    2. root@b1c4877a00cd:/# netstat
    3. Active Internet connections (w/o servers)
    4. Proto Recv-Q Send-Q Local Address      Foreign Address   State
    5. Active UNIX domain sockets (w/o servers)
    6. Proto RefCnt Flags    Type    State     I-Node Path
    7. root@b1c4877a00cd:/#

    运行相同的命令但使用host网络指令会显示繁忙的宿主机网络:

    1. $ docker run -ti net=host ubuntu
    2. root@host:/# netstat -nap | head
    3. Active Internet connections (servers and established)
    4. Proto Recv-Q Send-Q Local Address Foreign Address State    PID/
    5.    Program name
    6. tcp    0   0 127.0.0.1:47116 0.0.0.0:    LISTEN    -
    7. tcp    0   0 127.0.1.1:53  0.0.0.0:    LISTEN    -
    8. tcp    0   0 127.0.0.1:631  0.0.0.0:    LISTEN    -
    9. tcp    0   0 0.0.0.0:3000  0.0.0.0:    LISTEN    -
    10. tcp    0   0 127.0.0.1:54366 0.0.0.0:    LISTEN    -
    11. tcp    0   0 127.0.0.1:32888 127.0.0.1:47116 ESTABLISHED -
    12. tcp    0   0 127.0.0.1:32889 127.0.0.1:47116 ESTABLISHED -
    13. tcp    0   0 127.0.0.1:47116 127.0.0.1:32888 ESTABLISHED
    14. root@host:/#

    netstat是什么 netstat是一个命令,允许用户查看本地网络栈上的相关网络信息。它常用于确定网络套接字的状态。

    通常使用net=host标志有几个原因。首先,它可以让外界服务更容易连接到容器。不过这样也会失去为容器创建端口映射的好处。例如,如果有两个容器都监听80端口,使用这种方式的话将无法在同一个宿主机上同时运行它们。其次,使用这个标志时网络性能相比Docker网络会有显著提升。

    图12-1展示了Docker中的网络分组(也称数据包)和本地网络中的分组必须经过的层数。尽管本地网络只需通过宿主机的TCP/IP栈到网卡(network interface card,NIC),但Docker必须额外维护一个虚拟以太网对(也称为veth对,它是对使用以太网电缆的物理连接的虚拟表示),veth对和宿主机网络之间有一个网桥,并且有一个网络地址转换(network address translation,NAT)层。在正常的使用场景里,这样的开销可以导致Docker网络的速度仅有本地宿主机网络的一半。

    12

    图12-1 Docker网络与本地网络对比

    2.PID

    PID命名空间标志与其他命名空间很相似:

    1. imiell@host:/$ docker run ubuntu ps -p 1 1) 
    2.  PID TTY     TIME CMD
    3.   1 ?      00:00:00 ps 2
    4. imiell@host:/$ docker run pid=host ubuntu ps -p 1 3
    5.  PID TTY     TIME CMD
    6.   1 ?     00:00:27 systemd 4

    (1)在容器化环境中运行ps命令,显示只有一个PID为1的进程

    (2)运行的ps 命令是该容器中唯一的进程,而且PID为1

    (3)运行同样的ps命令但不使用PID命名空间,展示了主机的进程的视图

    (4)这次PID为1的进程是systemd命令,它是宿主机的操作系统的启动进程。读者看到的展示可能有所不同,这取决于所使用的发行版

    上面的示例演示了在具有宿主机PID视图的容器中,systemd 进程ID为1,而在没有该视图的情况下,能看到的唯一一个进程是ps命令本身。

    3.挂载

    如果要访问宿主机的设备,可以使用—device标志来使用特定设备,或者使用—volume标志来挂载宿主机的整个文件系统:

    1. docker run -ti volume /:/host ubuntu /bin/bash

    此命令将宿主机的/目录挂载到容器的/host目录。读者可能想知道为什么不能把宿主机的/目录挂载到容器的/目录,原因是这是docker命令明确禁止的行为。

    读者可能还想知道是否可以使用这些标志创建一个与宿主机几乎无法区分的容器。这个问题我们将在下一节讨论。

    4.类宿主机容器

    可以使用这些标志位来创建一个几乎拥有宿主机透明视图的容器:

    1. host:/$ docker run -ti net=host pid=host ipc=host \ 1) 
    2. volume /:/host \ 2
    3. busybox chroot /host 3

    (1)使用3个host参数(net,pid,ipc)运行容器

    (2)将宿主机的根文件系统挂载到容器的/host目录。Docker不允许将卷挂载到/目录,所以用户必须指定/host子目录卷

    (3)启动BusyBox容器。用户只需要chroot命令,而这是一个包含该命令的小镜像。执行chroot可以使被挂载的文件系统就像root一样展现给用户

    具有讽刺意味的是,Docker被称为“吃了类固醇的chroot”,而这里我们使用某些特性作为框架,以一种破坏chroot主要设计目标之一(即保护宿主机系统)的方式运行chroot。在这一点上我们尽量不要想得太复杂。

    在任何情况下,很难想象有人会在现实世界中使用这个命令(有指导性的)。如果读者想到的话,请联系我们。

    也就是说,用户可能更想将它作为一个更有用的命令的基础,像这样:

    1. $ docker run -ti workdir /host \
    2.   —volume /:/host:ro ubuntu /bin/bash

    —workdir /host将容器启动时的工作目录设置为宿主机的文件系统的根目录,使用—volume参数进行挂载。卷规格说明的:ro部分意思是宿主机文件系统以只读模式挂载。

    执行该命令可以给用户一个文件系统的只读视图,从而拥有一个环境安装工具(使用标准的Ubuntu包管理工具)并进行检查。例如,可以使用一个运行了nifty工具的镜像来向宿主机的文件系统报告安全问题,而不用将该工具安装到宿主机上。

    不安全! 正如前面的讨论中暗示的那样,带上这些标志的话会引入更多的安全风险。在安全性方面,使用它们应该被视为等同于运行时使用—privileged标志。

    在本技巧中,读者学到了该如何绕过容器中Docker的一些抽象。我们将在接下来的技巧里一起看看如何绕过Docker底层磁盘存储的限制。

    Docker附带了一些默认支持的存储驱动(storage driver)。它们在处理层上提供了不同的方法,每种方法都有自己的优点和缺点。可以在https://docs.docker.com/engine/userguide/storagedriver/找到更多相关的Docker文档。

    CentOS和Red Hat上默认的存储驱动是devicemapper,Red Hat为其加入了更多的支持,用以替代AUFS(Ubuntu使用的默认存储驱动),因为当时它具备bug更少、使用上更灵活的特点。

    Device Mapper术语 Device Mapper是一项Linux技术,它通过提供以某些用户定义的方式将物理设备映射到虚拟设备,从而抽象对物理设备的访问。本技巧中我们谈论的devicemapper指的是在Device Mapper之上构建的Docker存储驱动的名称。

    devicemapper驱动的默认行为是分配一个文件,将其视为可读取和写入的“设备”。但是,这个文件有一个固定的最大容量,当它空间不足时不会自动增加。

    问题

    使用Device Mapper存储驱动时,Docker容器上的可用空间已耗尽。

    解决方案

    更改Docker容器的最大容量。

    讨论

    为了说明这个问题,不妨尝试运行下面的Dockerfile:

    1. FROM ubuntu:14.04
    2. RUN truncate size 11G /root/file

    如果没有更改Docker守护程序上关于存储驱动的任何默认配置,应该会看到下面这样的输出:

    1. $ docker build .
    2. Sending build context to Docker daemon 24.58 kB
    3. Sending build context to Docker daemon
    4. Step 0 : FROM ubuntu:14.04
    5. Pulling repository ubuntu
    6. d2a0ecffe6fa: Download complete
    7. 83e4dde6b9cf: Download complete
    8. b670fb0c7ecd: Download complete
    9. 29460ac93442: Download complete
    10. Status: Downloaded newer image for ubuntu:14.04
    11. —-> d2a0ecffe6fa
    12. Step 1 : RUN truncate size 11G /root/file
    13. —-> Running in 77134fcbd040
    14. INFO[0200] ApplyLayer exit status 1 stdout: stderr: write /root/file:
    15. no space left on device

    这次构建最终会失败,因为尝试创建11 GB文件时失败,并且抛出“no space left”的错误消息。注意,在执行这一命令时,我们的机器上仍然有超过200 GB的磁盘空间,因此该容器并没有达到机器范围的限制。

    如何知道是否在使用devicemapper 如果运行docker info,它会在输出中告诉用户使用了哪个存储驱动。如果输出中有devicemapper,那么说明用户正在使用devicemapper,也就是说本技巧可能与用户相关。

    默认情况下,devicemapper容器的空间限制为 10 GB。要改变这个设定,需要清理Docker文件夹,重新配置Docker守护进程,然后重新启动。参见附录B了解有关如何在发行版上重新配置Docker守护程序的详细信息。

    空间限制已被更改 大约在本书出版时,devicemapper限制已升至100 GB,因此,读者的容器的空间限制可能会比这里提到的要高。

    要更改该配置,需要在Docker选项中添加或替换dm.basesize项,使其大于试图创建的11 GB文件:

    1. storage-opt dm.basesize=20G

    一个典型的文件可能如下所示:

    1. DOCKER_OPTIONS=”-s devicemapper storage-opt dm.basesize=20G

    在重新启动Docker守护进程后,可以重新执行之前运行过的docker build命令:

    1. # ocker build —no-cache -t big .
    2. Sending build context to Docker daemon 24.58 kB
    3. Sending build context to Docker daemon
    4. Step 0 : FROM ubuntu:14.04
    5. —-> d2a0ecffe6fa
    6. Step 1 : RUN truncate size 11G /root/file
    7. —-> Running in f947affe7900
    8. —-> 39766546a1a5
    9. Removing intermediate container f947affe7900
    10. Successfully built 39766546a1a5

    可以看到11 GB的文件被成功创建。

    这是devicemapper存储驱动的运行时约束,无论用户是使用Dockerfile构建镜像还是运行容器,都是如此。

    在本节中,我们将介绍几个技巧,帮助读者了解和解决在Docker容器中运行的应用程序遇到的一些问题。我们将介绍到在使用宿主机的工具来调试问题时如何“跳入”容器的网络,然后通过直接监控网络接口来了解一个更具有“实践性”的解决方案。

    最后,我们将演示Docker抽象是如何被破坏的,从而导致容器在一个宿主机上工作,而在其他宿主机上不工作,以及如何在一个生产系统上对其进行调试。

    在理想世界中,用户可以使用socat(见技巧4)在大使容器(ambassador container)中诊断问题。用户可以启动一个额外的容器,并确保连接转到这个作为代理的新容器。该代理允许诊断和监控连接,然后将其转发到正确的地方。但是,现实世界里,往往不那么方便(或可能)设置这样一个只用于调试的容器。

    大使容器模式 见技巧66对大使模式的描述。

    读者已经在技巧14中了解了docker exec命令。本技巧讨论nsenter,这个工具和docker exec命令看起来很相似,但允许在容器中使用自己机器上的工具,而不是局限于容器已经安装的东西。

    问题

    想要调试容器中的网络问题,但使用的工具却不在容器中。

    解决方案

    使用nsenter来跳到容器的网络,但是工具仍然在宿主机上。

    讨论

    如果Docker宿主机上还未安装nsenter,可以通过以下命令来安装:

    1. $ docker run -v /usr/local/bin:/target jpetazzo/nsenter

    这将在/usr/local/bin中安装nsenter,然后便即刻可以使用。nsenter也可能包含在所使用的系统发行版中(在util-linux包)。

    读者可能已经注意到,一般可用的BusyBox镜像默认不附带bash。作为一个初步的演示,我们将展示如何使用宿主机的bash程序进入容器:

    1. $ docker run -ti busybox /bin/bash
    2. FATA[0000] Error response from daemon: Cannot start container
    3. a81e7e6b2c030c29565ef7adb94de20ad516a6697deeeb617604e652e979fda6:
    4. exec: “/bin/bash”: stat /bin/bash: no such file or directory
    5. $ CID=$(docker run -d busybox sleep 9999) 1
    6. $ PID=$(docker inspect format {{.State.Pid}} $CID) 2
    7. $ sudo nsenter target $PID \ 3
    8. uts ipc net /bin/bash 4)       
    9. root@781c1fed2b18:~#

    (1)启动BusyBox容器并保存容器ID(CID)

    (2)检查容器,提取进程ID(PID)(见技巧27)

    (3)运行nsenter,指定—target标志位来进入容器。可能无须带上sudo

    (4)通过余下的参数指定进入容器时的命名空间(见技巧97,了解更多关于命名空间的知识)。这里的关键点是不使用—mount标志,因为它会使用容器的文件系统,而该文件系统没有安装 bash。指定/bin/bash 作为可执行命令来启动容器

    需要指出的是虽然不能直接访问容器的文件系统,但是用户可以使用宿主机拥有的所有工具。

    我们之前的某个需求是想有一种办法找出宿主机上哪个veth接口设备对应于哪个容器。例如,有时候我们需要快速将容器从网络上卸载,而不必使用第8章中的任何工具来模拟网络中断。但是,一个没有特权的容器无法关闭网络接口,所以我们需要找出 veth 接口的名称然后在宿主机上完成这项任务:

    1. $ docker run -d name offlinetest ubuntu:14.04.2 sleep infinity
    2. fad037a77a2fc337b7b12bc484babb2145774fde7718d1b5b53fb7e9dc0ad7b3
    3. $ docker exec offlinetest ping -q -c1 8.8.8.8 1
    4. PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
    5. —- 8.8.8.8 ping statistics —-
    6. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
    7. rtt min/avg/max/mdev = 2.966/2.966/2.966/0.000 ms
    8. $ docker exec offlinetest ifconfig eth0 down 2
    9. SIOCSIFFLAGS: Operation not permitted
    10. $ PID=$(docker inspect format {{.State.Pid}} offlinetest)
    11. $ nsenter target $PID net ethtool -S eth0 3
    12. NIC statistics:
    13.    peer_ifindex: 53
    14. $ ip addr | grep ‘^53 4)       
    15. 53: veth2e7d114: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
    16. master docker0 state UP
    17. $ sudo ifconfig veth2e7d114 down 5
    18. $ docker exec offlinetest ping -q -c1 8.8.8.8 6
    19. PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. 7
    20. —- 8.8.8.8 ping statistics —-
    21. 1 packets transmitted, 0 received, 100% packet loss, time 0ms

    (1)尝试从新容器内部执行ping命令验证连接成功与否

    (2)我们不能关闭容器中的接口。注意,用户的接口可能不是eth0, 所以如果该命令不生效,那么不妨试试通过ifconfig找出主接口的名称

    (3)进入该容器的网络空间,使用ethtool命令从宿主机查找对等接口的索引,即虚拟接口的另一端

    (4)查找宿主机上的接口列表,从而找出容器的对应veth接口

    (5)查找宿主机上的接口列表,从而找出容器的对应veth接口

    (6)关闭虚拟接口

    (7)从容器内部使用ping命令验证连接是失败的

    作为最后一个例子,读者可能想要在容器里使用的程序应该是tcpdump,这是一种在网络接口上记录所有TCP分组的工具。要使用它,需要使用—net命令运行nsenter,这样可以在宿主机上“查看”容器的网络,这样一来便可以使用tcpdump监控分组。

    例如,下面代码中的tcpdump命令会将所有分组记录到/tmp/google.tcpdump文件中(我们假设用户仍然在前面启动的nsenter会话中)。然后,我们可以通过访问网页触发一些网络流量:

    1. root@781c1fed2b18:/# tcpdump -XXs 0 -w /tmp/google.tcpdump &
    2. root@781c1fed2b18:/# wget google.com
    3. 2015-08-07 15:12:04 http://google.com/
    4. Resolving google.com (google.com)… 216.58.208.46, 2a00:1450:4009:80d::200e
    5. Connecting to google.com (google.com)|216.58.208.46|:80 connected.
    6. HTTP request sent, awaiting response 302 Found
    7. Location: http://www.google.co.uk/?gfe_rd=cr&ei=tLzEVcCXN7Lj8wepgarQAQ
    8. [following]
    9. 2015-08-07 15:12:04
    10. http://www.google.co.uk/?gfe_rd=cr&ei=tLzEVcCXN7Lj8wepgarQAQ
    11. Resolving www.google.co.uk (www.google.co.uk)… 216.58.208.67,
    12. 2a00:1450:4009:80a::2003
    13. Connecting to www.google.co.uk (www.google.co.uk)|216.58.208.67|:80
    14. connected.
    15. HTTP request sent, awaiting response 200 OK
    16. Length: unspecified [text/html]
    17. Saving to: index.html
    18. index.html      [ <=>        ] 18.28K —.-KB/s in 0.008s
    19. 2015-08-07 15:12:05 (2.18 MB/s) - index.html saved [18720]
    20. root@781c1fed2b18:# 15:12:04.839152 IP 172.17.0.26.52092 >
    21. google-public-dns-a.google.com.domain: 7950+ A? google.com. (28)
    22. 15:12:04.844754 IP 172.17.0.26.52092 >
    23. google-public-dns-a.google.com.domain: 18121+ AAAA? google.com. (28)
    24. 15:12:04.860430 IP google-public-dns-a.google.com.domain >
    25. 172.17.0.26.52092: 7950 1/0/0 A 216.58.208.46 (44)
    26. 15:12:04.869571 IP google-public-dns-a.google.com.domain >
    27. 172.17.0.26.52092: 18121 1/0/0 AAAA 2a00:1450:4009:80d::200e (56)
    28. 15:12:04.870246 IP 172.17.0.26.47834 > lhr08s07-in-f14.1e100.net.http:
    29. Flags [S], seq 2242275586, win 29200, options [mss 1460,sackOK,TS val
    30. 49337583 ecr 0,nop,wscale 7], length 0

    “Temporary failure in name resolution”错误 这取决于读者的网络配置,读者可能需要临时修改resolv.conf文件,使DNS查找可以正常工作。如果收到“Temporary failure in name resolution”错误,请尝试将 nameserver 8.8.8.8 这行添加到/etc/resolv.conf文件的顶部。别忘了在完成实验后还原它。

    这也展示了Docker另外一个备受瞩目的使用场景——在Docker提供的隔离网络环境中调试网络问题会更加容易。试着记住tcpdump的一些正确的参数,从而妥善地过滤掉一些不相关的分组,这在半夜里维护系统时会是一个容易出错的环节。使用前面的方法,大可忘掉这一点,使用tcpdump捕获容器内的所有内容,而无须在镜像里安装(或不必安装)它。

    tcpdump是网络诊断的事实标准,如果需要深入调试网络问题,它可能是大多数人首选的工具。

    但 tcpdump 通常用于显示分组摘要以及检查分组首部和协议信息——它对于两个程序之间应用层数据流的展示并不是很完善。但是这些信息在调查两个应用程序间的通信问题时可能非常重要。

    问题

    需要监控容器化应用程序的通信数据。

    解决方案

    使用tcpflow捕获通过接口的流量。

    讨论

    tcpflow类似于tcpdump(接受相同的模式匹配表达式),但是它的目的是更好地了解应用程序的数据流。可以通过系统包管理工具安装tcpflow,但是,如果系统包中没有,我们也为此准备了一个可用的Docker镜像,它在功能上与通过包管理工具安装的效果一样:

    1. $ IMG=dockerinpractice/tcpflow
    2. $ docker pull $IMG
    3. $ alias tcpflow=”docker run rm net host $IMG

    这里有两种方式通过Docker使用tcpflow,一是将其指向docker0接口,并使用分组过滤表达式只检索想要的分组,或者使用上一个技巧的方法来找到感兴趣的容器的veth接口,并捕获该接口。

    表达式过滤更加强大,因为它可以让用户深入了解感兴趣的流量,所以我们将展示一个简单的示例以便读者可以快速上手:

    1. $ docker run -d name tcpflowtest alpine:3.2 sleep 30d
    2. fa95f9763ab56e24b3a8f0d9f86204704b770ffb0fd55d4fd37c59dc1601ed11
    3. $ docker inspect -f ‘{{ .NetworkSettings.IPAddress }}’ tcpflowtest
    4. 172.17.0.1
    5. $ tcpflow -c -J -i docker0 host 172.17.0.1 and port 80
    6. tcpflow: listening on docker0

    在上面的示例中,我们要求tcpflow以彩色的方式打印容器中通过80端口(通常用于HTTP流量)的流入或流出流量。现在,读者可以通过在容器的新终端中访问网页来体验上述命令的效果:

    1. $ docker exec tcpflowtest wget -O /dev/null http://www.example.com/
    2. Connecting to www.example.com (93.184.216.34:80)
    3. null      100% |**| 1270  0:00:00 ETA

    读者将可以看到在tcpflow终端中的彩色输出。到目前为止,命令的累积输出看上去会是这样:

    1. $ tcpflow -J -c -i docker0 host 172.17.0.1 and (src or dst port 80)’
    2. tcpflow: listening on docker0
    3. 172.017.000.001.36042-093.184.216.034.00080:
    4. GET / HTTP/1.1 1)               
    5. Host: www.example.com
    6. User-Agent: Wget
    7. Connection: close
    8. 093.184.216.034.00080-172.017.000.001.36042:
    9. HTTP/1.0 200 OK 2)               
    10. Accept-Ranges: bytes
    11. Cache-Control: max-age=604800
    12. Content-Type: text/html
    13. Date: Mon, 17 Aug 2015 12:22:21 GMT
    14. […]
    15. <!doctype html>
    16. <html>
    17. <head>
    18.   <title>Example Domain</title>
    19. […]

    (1)蓝色开始

    (2)红色开始

    tcpflow 是工具箱的一个很好补充,尽管它并不引人注目。用户可以对长时间运行的容器执行tcpflow,以便了解其现在正在传送的内容,或者与tcpdump一起使用,来获得更详细的应用程序发送的请求以及传输的信息。

    前两个技巧已经展示了该如何对由容器和其他位置(无论是其他容器还是互联网上的第三方)之间的交互造成的问题开始调查。

    如果用户已经将问题定位到某台宿主机,并且确定不是外部交互的原因,那么下一步应该是尝试减少变动部分(删除卷和端口)的数量,并检查宿主机自身的详细信息(可用磁盘空间、打开的文件描述符的数量等)。每个宿主机是否使用了最新版本的Docker可能也值得检查一下。

    在某些情况下,上面的方法都行不通。例如,有一个镜像,运行时没有带任何参数(如docker run imagename),而它在不同的宿主机上运行的行为不同,这完全有可能发生。

    问题

    想要确认为什么在特定宿主机上的某个容器的特定行为无法正常工作。

    解决方案

    追踪整个过程来查看它的系统调用,同时将其与一个正常工作的系统作对比。

    讨论

    虽然Docker的目标是允许应用程序“在任何地方运行任何应用程序”,但它试图实现这一点的手段并不总是万无一失。

    Docker将Linux内核API视为其宿主机(即它可以运行的环境)。当第一次了解Docker如何工作时,许多人会问Docker如何处理Linux API的变更。据我们所知,目前它没有做任何处理。幸运的是,Linux API是向后兼容的,但是不难想象将来的某个场景,即创建一个新的Linux API并被Docker化的应用程序使用,然后将该应用程序部署到一个足够运行Docker但版本足够老的内核时,它可能并不支持该特定的API调用。

    这是否会发生 读者可能认为Linux内核API的变更只是一个理论上存在的问题而已,但是实际上,我们在编写这本书时的确遇到了这种情况。我们工作的项目使用memfdcreate Linux系统调用,它只存在于3.17及以上版本的内核。因为我们工作的一些宿主机有较老的内核,我们的容器只能在部分系统上正常工作,其他的系统则不行。

    并不是只有这种情况会导致Docker抽象不工作。容器还有可能无法工作在特定的内核上,因为应用程序可能对宿主机上的文件做出一些假设。虽然这种情况发生的概率很小,但是它确实存在,重要的是要警惕这种风险。

    1.SELinux

    可以让Docker抽象无法工作的一个例子是与SELinux交互的任何东西。如第10章所讨论的,SELinux是在内核中实现的安全层,它游离于正常的用户权限之外。

    Docker通过这层来增强容器的安全性,即管理可以在容器里执行什么操作。例如,如果你是容器的root用户,那么你也是宿主机上的root用户。虽然很难,但是一旦容器被攻破,也就同时获得了宿主机的root权限。这并非不可能。之前已经有漏洞被曝光,而且可能还存在一些社区还未发觉的漏洞。SELiux等于提供了另一层保护,即使root用户从容器侵入宿主机,这里对其在宿主机上可以执行的操作也做了一些限制。

    到目前为止还算不错,但是对Docker来说,问题在于SELinux是在宿主机上实现的,而不是容器内部。这就意味着在容器里运行的应用程序查询SELinux并发现它是开启的状态时,可能会促使它们对运行的环境做出某些假设。如果不满足这些预期,则会以意想不到的方式失败。

    在下面的示例中,我们运行一个安装了Docker的CentOS 7 Vagrant机器,并且在里面安装了一个Ubuntu 12.04容器。如果我们执行一条相当简单的命令来添加一个用户的话,退出码会是12,这代表有错误,并且提示用户没有被成功创建:

    1. [root@centos vagrant]# docker run -ti ubuntu:12.04
    2. Unable to find image ubuntu:12.04 locally
    3. Pulling repository ubuntu
    4. 78cef618c77e: Download complete
    5. b5da78899d3a: Download complete
    6. 87183ecb6716: Download complete
    7. 82ed8e312318: Download complete
    8. root@afade8b94d32:/# useradd -m -d /home/dockerinpractice dockerinpractice
    9. root@afade8b94d32:/# echo $?
    10. 12

    同样的命令在ubuntu:14.04容器上却可以正常工作。如果想尝试重现这个结果,需要一台CentOS 7(或类似的)机器。但是出于学习的目的,这对于接下来技巧中使用的任何命令和容器已经足够了。

    $?是做什么的 在bash中,$?给出最后一条执行命令的退出码。退出码的含义因命令而异,但通常退出码为0意味着调用成功,非零码指示某种错误或异常条件。

    2.调试Linux API调用

    我们知道容器之间可能的不同是由于在宿主机上运行的内核API之间存在差异性,strace可以帮助确定调用内核API之间的差异。

    什么是strace strace是一个工具,它允许嗅探一个进程对Linux API所做的调用(也称为系统调用)。这是一个非常有用的调试和教学工具。

    首先,需要使用相应的包管理工具在容器上安装strace,然后使用strace命令运行不同的命令。下面是失败的useradd调用的一些输出示例:

    1. # strace -f \ (1)          
    2. useradd -m -d /home/dockerinpractice dockerinpractice 2) 
    3. execve(“/usr/sbin/useradd”, [“useradd”, “-m”, “-d”, 3)        
    4. “/home/dockerinpractice”, dockerinpractice”], [/ 9 vars /]) = 0
    5. […]
    6. open(“/proc/self/task/39/attr/current”, 4
    7. O_RDONLY) = 9
    8. read(9, system_u:system_r:svirt_lxc_net“…, 5
    9. 4095) = 46
    10. close(9)                =0 6)   
    11. […]
    12. open(“/etc/selinux/config”, O_RDONLY) =
    13. -1 ENOENT (No such file or directory)
    14. open(“/etc/selinux/targeted/contexts/files/(7
    15. file_contexts.subs_dist”, O_RDONLY) = -1 ENOENT (No such file or directory)
    16. open(“/etc/selinux/targeted/contexts/files/
    17. file_contexts.subs”, O_RDONLY) = -1 ENOENT (No such file or directory)
    18. open(“/etc/selinux/targeted/contexts/files/
    19. file_contexts”, O_RDONLY) = -1 ENOENT (No such file or directory)      
    20. […]
    21. exit_group(12)             =? (8

    (1)带上-f标志执行strace命令,它可以确保命令的进程及其子进程都能被strace追踪

    (2)在strace调用中追加想要调试的命令

    (3)strace输出的每一行都从Linux API调用开始。execve调用在这里执行传给strace的命令。结束处的0是调用的返回值(表示成功)

    (4)open系统调用打开一个文件进行读取。返回值(9)是用于对文件进行后续调用的文件句柄号。在这种情况下,/proc文件系统会检索SELinux信息,该文件系统保存正在运行的进程的有关信息

    (5)read系统调用工作于之前打开的文件(文件句柄号为9),返回了读取的字节数(46)

    (6)close系统调用根据文件句柄关闭了引用的文件

    (7)应用程序试图打开它期望位置的 SELinux 文件,但是都失败了。strace可以告诉用户返回值的含义:没有该文件或目录

    (8)进程退出码为12,对于uesradd命令来说意味着不能创建目录

    上面的输出可能看起来很混乱,但重复阅读几次之后就不难理解了。每一行代表一个对Linux内核的调用,以便在所谓的内核空间(而不是用户空间,意味着应用程序不会将执行操作的责任交给内核)执行一些操作。

    使用man 2查找有关系统调用的更多信息 如果读者想了解有关特定系统调用的更多信息,可以运行man 2 <callname>来了解更多信息。读者可能需要使用apt-get install manpages-dev或类似的命令安装帮助手册。或者,在Google搜索man 2 <callname>也可能会得到需要的信息。

    这是一个Docker抽象无法正常工作的例子。在这种情况下,操作失败是因为程序期望SELinux文件存在,因为SELinux似乎在容器上是启用的,但是限制的细节保留在宿主机上。

    尽管这种情况很少见,但是使用strace来调试和了解应用程序如何进行交互的能力是一种非常宝贵的技术,它不仅针对Docker,还可用在更通用的开发过程。

    阅读帮助手册 如果你真的想成为一名开发人员,阅读所有系统调用的帮助手册是非常有用的。起初,这里面看似充斥着晦涩的术语,但是当阅读完各个主题之后,你会学到很多基本的Linux概念。在某些时候,你会开始看到大多数语言是如何起源于此,而且它们的一些轶事也相当有趣。耐心一点吧,毕竟你没办法一下子就全部理解。

    在本章中,我们介绍了当Docker不能像预期那样正常工作时我们可以做的一些事情。虽然在使用Docker过程中很少会发现抽象层的缺陷,但是这种可能性仍然是存在的,做好准备,了解哪些地方可能会出错是至关重要的。

    本章展示了以下几点内容:

    • 通过docker的标志直接使用宿主机的资源,以提高效率;
    • 调整DeviceMapper磁盘大小,最大限度地使用磁盘空间;
    • 使用nsenter跳转到容器的网络;
    • 运行strace以确定为什么Docker容器在特定宿主机上不工作。

    本书到此完结!我们希望读者已经了解了Docker的一些用途,并初步有了一些将Docker集成到自己的公司或个人项目中的想法。如果想联系我们或给我们一些反馈,请在Manning网站的本书论坛(https://forums.manning.com/forums/docker-in-practice)中创建一个主题,或者向“docker-in-practice”GitHub的任何一个仓库发起issue。


    [1]  Docker在1.10版本中已经引入了对用户命名空间的支持。——译者注