第8章 网络模拟:无痛的现实环境测试

    本章主要内容

    • Docker Compose初探
    • 运行一个DNS服务器来完成基本的容器服务发现
    • 在有问题的网络上测试应用程序
    • 创建一个基底网络实现跨Docker宿主机的无缝通信

    作为DevOps工作流的一部分,读者总会涉及某种形式的网络使用。不论是查找本地memcache容器的所在、连接到外部世界,还是将运行在不同宿主机上的Docker容器整合在一起,迟早都需要接触到广阔的网络。

    本章将展示如何使用Docker的虚拟网络工具来模拟和管理网络,还将向编排(orchestration)和服务发现(service discovery)迈进一小步。编排和服务发现是第9章将深入探讨的话题。

    技巧6中讲述过如何使用链接来连接容器,同时提到了明确的容器依赖声明所带来的优势。但是,链接具有一些不足之处。链接在启动每个容器时都要手工指定、容器必须以正确的顺序启动(以免容器链接循环)、链接无法进行替换(如果某个容器宕了,那么所有依赖于它的容器都需要重启以便重新创建链接)。

    幸好有些工具可直击这些痛点。

    Docker Compose的前身名为fig,fig是一个现已弃用的独立项目,旨在减轻使用正确的链接、卷及端口参数来启动多个容器的痛苦。Docker 公司对其情有独钟,直接将其收购、重制,并使用新的名字进行了发布。

    本技巧使用一个简单的Docker容器编排示例来介绍Docker Compose。

    问题

    想要让宿主机上链接的容器协同工作。

    解决方案

    使用Docker Compose。

    讨论

    Docker Compose是一个用于定义和运行复杂的Docker应用程序的工具。其核心思想是声明应用程序的启动配置,然后使用一条简单的命令来启动该应用程序,而无须使用复杂的shell脚本或Makefile来组装复杂的容器启动命令。

    在编写本书时,还不建议在生产环境中使用Docker Compose。

    安装 本书假定读者已安装好Docker Compose。在编写本书时其安装说明还在快速变化,因此请访问Docker的说明文档(http://docs.docker.com/compose/install)以获取最新信息。读者可能需要使用sudo来运行docker-compose。

    为保持尽可能简单,本技巧将使用一个回响(echo)服务器和客户端。客户端每5 s发送一个常见的“Hello world!”消息给服务器,然后接收返回的信息。

    源代码可用 本技巧的源代码可从https://github.com/docker-in-practice/docker-compose-echo获取。

    下面的命令将创建一个工作目录用于创建服务器镜像:

    1. $ mkdir server
    2. $ cd server

    使用代码清单8-1 所示代码创建服务器Dockerfile。

    代码清单8-1 Dockerfile——简单的回响服务器

    1. FROM debian
    2. RUN apt-get update && apt-get install -y nmap 1
    3. CMD ncat -l 2000 -k exec /bin/cat 2

    (1)安装nmap包,它提供了这里所使用的ncat程序

    (2)在启动该镜像时默认运行ncat程序

    参数-l 2000指示ncat监听端口2000,参数-k让它同时接受多个客户端连接,并在客户端关闭连接后继续运行,以便更多客户端可以接入。最后一个参数—exec /bin/cat是让ncat为所有接入的连接运行/bin/cat,把来自该连接的数据转发给该运行中的程序。

    接下来,使用以下命令构建这个Dockerfile:

    1. $ docker build -t server .

    现在可以创建客户端镜像,用于发送消息给服务器。创建一个新目录,并将 client.py 文件及Dockerfile放置于此:

    1. $ cd ..
    2. $ mkdir client
    3. $ cd client

    代码清单8-2给出的是一个简单的Python程序,作为回响服务器的客户端。

    代码清单8-2 client.py——一个简单的回响客户端

    1. import socket, time, sys 1
    2. while True:
    3.   s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 2) 
    4.   s.connect((‘talkto’,2000)) 3
    5.   s.send(‘Hello, world\n’) 4
    6.   data = s.recv(1024)(5)      
    7.   print Received:’, data 6
    8.   sys.stdout.flush() 7)       
    9.   s.close() 8)      
    10.   time.sleep(5) 9

    (1)导入所需的Python包

    (2)创建一个套接字对象

    (3)使用该套接字连接’talkto’服务器的2000 端口

    (4)发送一个带换行符的字符串到该套接字上

    (5)创建一个1024字节的缓冲区用于接收数据,并在收到消息时将数据放置到data变量中

    (6)将接收到的数据打印到标准输出中

    (7)刷新标准输出缓冲区以便在消息进入时将其显示出来

    (8)关闭该套接字对象

    (9)等待5秒然后重复上述步骤

    客户端的Dockerfile很简单。它安装Python,添加client.py文件,然后将其指定为启动时的默认运行项,如代码清单8-3所示。

    代码清单8-3 Dockerfile——一个简单的回响客户端

    1. FROM debian
    2. RUN apt-get update && apt-get install -y python
    3. ADD client.py /client.py
    4. CMD [“/usr/bin/python”,”/client.py”]

    使用以下命令构建客户端:

    1. docker build -t client .

    为了展示docker-compose的价值,首先手动地运行这些容器:

    1. docker run name echo-server -d server
    2. docker run rm name client link echo-server:talkto client

    命令执行完成后,按Ctrl+C退出客户端,并删除这些容器:

    1. docker rm -f client echo-server

    即便在这个简单的示例中,还是会有很多东西出错:先启动客户端将造成应用程序启动失败,忘记删除容器将在重启时造成问题,错误命名容器也将造成失败。当容器及其架构变得越来越复杂时,这类编排问题将随之增多。

    Compose对此提供了解决之道,它把容器的启动和配置的编排封装在一个简单的文本文件中,并为用户管理启动与关闭命令的细节。

    Compose需要一个YAML文件。请在一个新目录中创建该文件:

    1. cd ..
    2. mkdir docker-compose
    3. cd docker-compose

    YAML文件的内容如代码清单8-4所示。

    代码清单8-4 docker-compose.yml——Docker Compose回响服务器与客户端YAML文件

    1. echo-server: 1)   
    2.  image: server 2
    3.  expose: 3
    4.  - 2000
    5. client:       
    6.  image: client
    7.  links: 4)          
    8.  - echo-server:talkto

    (1)运行中的服务的引用名称是它们的标识:本示例中是echo-server和client

    (2)每个小节都必须定义所使用的镜像:本示例中是客户端与服务器镜像

    (3)将echo-server的2000端口暴露给其他服务

    (4)定义一个指向echo-server的链接。客户端内对talkto的引用将被发送给回响服务器。其映射是通过在运行的容器中动态设置/etc/hosts文件完成的

    docker-compose.yml的语法非常容易理解:最左侧是命名的服务,其配置声明在下方缩进的小节中。每个配置项名称后面都有一个冒号,这些项目的属性要么声明在同一行,要么声明在后续以相同缩进层次破折号开始的几行中。

    这里需要理解的关键配置项是客户端定义中的links。这些链接的创建方式与docker run命令创建链接的方式一致。实际上,大部分Docker命令行参数在docker-compose.yml语法中都有直接的对应关系。

    这个示例中使用image:语句来定义每个服务所使用的镜像,不过也可以在build:语句中定义Dockerfile的路径,让docker-compose动态地重新构建所需镜像。Docker Compose会自动进行构建。

    什么是YAML文件 YAML文件是使用简单语法的一个文本配置文件。更多信息可从其官网http://yaml.org获得。

    现在所有的基础设施都创建好了,运行该应用程序很简单:

    1. $ docker-compose up
    2. Creating dockercompose_server_1
    3. Creating dockercompose_client_1
    4. Attaching to dockercompose_server_1, dockercompose_client_1
    5. client_1 | Received: Hello, world
    6. client_1 |
    7. client_1 | Received: Hello, world
    8. client_1 |

    出现无法连接错误? 如果在启动docker-compose时出现类似“Couldn’t connect to Docker daemon athttp+unix://var/run/docker.sock—is it running?”这样的错误,其问题可能是需要使用sudo运行。

    确认结果无误后,按多次Ctrl+C退出应用程序。使用相同的命令即可重新运行该应用程序,而无须考虑删除容器的问题。需要注意的是,在重新运行时输出的是“Recreating”(重新创建)而不是“Creating”(创建)。

    我们已经对Docker Compose有所了解,接下来讨论一个更复杂的docker-compose现实场景:使用socat、卷及链接为运行在宿主机上的一个SQLite实例添加类似服务器的功能。

    默认情况下,SQLite没有任何TCP服务器的概念。本技巧在上一个技巧的基础上,提供了一种使用Docker Compose来实现TCP服务器功能的方法。

    具体说来,它是使用此前介绍过的工具和概念来构建的:

    • 卷;
    • 使用socat代理;
    • 容器链接;
    • Docker Compose。

    要求使用SQLite 3版本 本技巧要求在宿主机上安装SQLite 3版本。同时建议安装rlwrap,以便在与SQLite服务器交互时让行编辑变得更友好一些(虽然这不是必需的)。这些软件包在标准的包管理器中都能免费获得。

    本技巧对应的代码可在https://github.com/docker-in-practice/docker-compose-sqlite下载。

    如果在使用本技巧时出现问题,则说明Docker版本可能需要升级。1.7.0之后的版本应该都可以正常工作。

    问题

    想要使用Docker高效地开发一个复杂的引用宿主机外部数据的应用程序。

    解决方案

    使用Docker Compose。

    讨论

    图8-1给出了本技巧架构的一个概述。从高层次上看,有两个运行中的Docker容器,一个负责执行SQLite客户端,另一个用于将不同的TCP连接代理到这些客户端上。需要注意的是,执行SQLite的容器并未暴露给宿主机,代理容器则实现了这一点。将职责分离成离散单元是微服务构架的一个共同特点。

    8

    图8-1 SQLite服务器工作原理

    所有节点都将使用同一个镜像。设置代码清单8-5所示的Dockerfile。

    代码清单8-5 SQLite服务器、客户端及代理合一的Dockerfile

    1. FROM ubuntu:14.04
    2. RUN apt-get update && apt-get -y install rlwrap sqlite3 socat 1)  
    3. EXPOSE 12345 (2

    (1)安装必要的应用程序

    (2)暴露12345端口以便节点可通过Docker守护进程进行通信

    代码清单8-6展示的是docker-compose.yml的内容,它定义了容器将如何启动。

    代码清单8-6 SQLite服务器与代理的docker-compose.yml

    1. server: 1)   
    2.  command: socat TCP-L:12345,fork,reuseaddr
    3. EXEC:’sqlite3 /opt/sqlite/db’,pty 2)    
    4.  build: .(3
    5.  volumes:(4
    6.  - /tmp/sqlitedbs/test:/opt/sqlite/db
    7. proxy:
    8.  command: socat TCP-L:12346,fork,reuseaddr TCP:sqliteserver:12345 5
    9.  build: .
    10.  links: 6)         
    11.  - server:sqliteserver
    12.  ports: 7
    13.  - 12346:12346

    (1)服务器与代理容器定义在这一节中

    (2)创建一个socat代理,用于将SQLite调用的输出链接到一个TCP端口上

    (3)在启动时从当前目录的Dockerfile构建镜像

    (4)将SQLite测试数据库文件挂载到容器内的/opt/ sqlite/db上

    (5)创建一个socat代理,用于把12346端口的数据传递到服务器容器的12345端口上

    (6)定义一个代理与服务器之间的链接,将此容器内对sqliteserver的引用映射到服务器容器中

    (7)向宿主机发布12346端口

    参数TCP-L:12345,fork,reuseaddr指定服务器容器中的socat进程监听12345端口,并允许接入多个连接。后面的EXEC:部分告诉socat针对每个连接在/opt/sqlite/db文件上运行SQLite,并为此进程分配一个伪终端。客户端容器中的socat进程与服务器容器的监听行为一样(除了端口不同),不过它将建立一个与SQLite服务器的TCP连接,而不是为呼入的连接运行某个程序。

    尽管这项功能可以在一个容器内实现,但服务器/代理容器的设置可以让这个系统的架构更易于扩展,因为每个容器只对一项任务负责:服务器负责打开SQLite连接,而代理负责将服务暴露给宿主机。

    代码清单8-7(从仓库的原始版本简化而来,见https://github.com/docker-in-practice/docker- compose-sqlite)将在宿主机上创建两个最小化的SQLite数据库,即test和live。

    代码清单8-7 setup_dbs.sh

    1. #!/bin/bash
    2. echo Creating directory
    3. SQLITEDIR=/tmp/sqlitedbs
    4. rm -rf $SQLITEDIR 1)     
    5. if [ -a $SQLITEDIR ] 2
    6. then
    7.   echo Failed to remove $SQLITEDIR
    8.   exit 1
    9. fi
    10. mkdir -p $SQLITEDIR
    11. cd $SQLITEDIR
    12. echo Creating DBs
    13. echo create table t1(c1 text);’ | sqlite3 test 3
    14. echo create table t1(c1 text);’ | sqlite3 live 4
    15. echo Inserting data
    16. echo insert into t1 values (“test”);’ | sqlite3 test 5) 
    17. echo insert into t1 values (“live”);’ | sqlite3 live 6
    18. cd - > /dev/null 2>&1 7) 
    19. echo All done OK

    (1)删除上一次运行的所有目录

    (2)如果目录依然存在则抛出一个错误

    (3)创建test数据库以及一张表

    (4)创建live数据库以及一张表

    (5)插入一行”test”字符串到表中

    (6)插入一行”live”字符串到表中

    (7)返回到此前的目录

    要运行这个示例,可如代码清单8-8所示设置数据库并调用docker-compose up

    代码清单8-8 启动Docker Compose集群

    1. $ chmod +x setup_dbs.sh
    2. $ ./setup_dbs.sh
    3. $ sudo docker-compose up
    4. Creating dockercomposesqlite_server_1
    5. Creating dockercomposesqlite_proxy_1
    6. Attaching to dockercomposesqlite_server_1, dockercomposesqlite_proxy_1

    接着,在一个或多个其他终端中,可以针对一个SQLite数据库运行Telnet来创建多个会话,如代码清单8-9所示。

    代码清单8-9 连接SQLite服务器

    1. $ rlwrap telnet localhost 12346 1
    2. Trying 127.0.0.1 2)          
    3. Connected to localhost.
    4. Escape character is ‘^]’.       
    5. SQLite version 3.7.17 3)         
    6. Enter “.help for instructions
    7. sqlite> select from t1; 4)     
    8. select from t1;
    9. test
    10. sqlite>

    (1)使用Telnet连接代理,将其包装在rlwrap里,实现命令行的编辑和历史功能

    (2)Telnet连接的输出

    (3)SQLite连接在此时建立

    (4)在sqlite提示符下运行SQL命令

    现在如果要切换到live服务器,通过修改docker-compose.yml的volumes这一行来修改配置,从

    1. - /tmp/sqlitedbs/test:/opt/sqlite/db

    变为

    1. - /tmp/sqlitedbs/live:/opt/sqlite/db

    然后运行这个命令:

    1. $ sudo docker-compose up

    不适用于生产环境 尽管我们对这个SQLite客户端的多路复用做了一些基本的测试,但我们不对任何类型负载下该服务器的数据完整性及性能做保证。

    本技巧演示了 Docker Compose 如何能把相对棘手和复杂的事务变得健壮且简单。这里以SQLite为例,通过连接容器,将SQLite调用代理到宿主机的数据上,从而赋予它类似服务器的功能。使用Docker Compose的YAML配置,使容器复杂度的管理变得十分简单,它把编排容器的复杂事务从手工且易出错的过程变成了可通过源代码控制的更安全和自动化的过程。这是编排之路的开端,第9章将对此做更多介绍。

    容器启动时默认会被分配到自己的IP地址,并且只有在知道对方的IP时才能与之通信——链接(不论是手工建立还是由Docker Compose提供)是将IP分布到/etc/hosts及环境变量中的一种方法。

    不过,这种分布方式存在限制——运行中的容器的环境变量无法更新,并且试图动态更新/etc/hosts 可能会带来问题。为避免出现这两个问题,Docker不允许为运行中的容器添加链接。

    解决分布IP地址的问题有一个很好的方案就是使用,每天都会用到的DNS服务器!

    问题

    想要无须使用链接即可让容器相互发现。

    解决方案

    使用Resolvable作为DNS服务器。

    讨论

    Resolvable(https://github.com/gliderlabs/resolvable)是一个工具,它会读取宿主机上当前运行的容器的信息,并以标准方式提供名称到IP地址的映射服务——它就是一个DNS服务器。

    内置到Docker中 Docker的某些版本(1.7.1之后,1.9.0之前)提供了一项功能,无须任何外部工具,容器即可使用名称与其他容器通信。不过如果想从宿主机上查找容器,Resolvable可能还是有价值的,有如本技巧末尾所述。

    在开始之前,需要先确定一些设置——docker0网络接口的地址以及启动容器时当下使用的DNS服务器:

    1. $ ip addr | grep inet.docker0
    2.   inet 172.17.42.1/16 scope global docker0
    3. $ docker run rm ubuntu:14.04.2 cat /etc/resolv.conf | grep nameserver
    4. nameserver 8.8.8.8
    5. nameserver 8.8.4.4

    上述两项是Docker的默认值。如果调整过Docker守护进程的设置,或电脑上有些不同寻常的配置,则结果可能会略有不同。

    现在即可使用刚查找出的恰当值来启动Resolvable容器:

    1. $ DNSARGS=”—dns 8.8.8.8 dns 8.8.4.4
    2. $ PORTARGS=”-p 172.17.42.1:53:53/udp
    3. $ VOLARGS=”-v /var/run/docker.sock:/tmp/docker.sock
    4. $ docker run -d name res -h resolvable $DNSARGS $PORTARGS $VOLARGS \
    5. gliderlabs/resolvable:master
    6. 5ebbe218b297da6390b8f05c0217613e47f46fe46c04be919e415a5a1763fb11

    容器的启动过程中提供了以下3个关键信息。

    • Resolvable需要知道在请求的地址无法映射到容器时向谁查询。答案是向上游DNS服务器查询。Resolvable会从容器内的/etc/resolv.conf获取这些值,—dns参数用于填充/etc/resolv.conf。虽然此处并不是严格必需的(指定的值是默认值),但在后面就能看到这还是相当有用的。
    • 监听DNS请求的接口。使用Docker网桥的IP地址的好处在于无须将服务器暴露给外界(用0.0.0.0则会这样),且使用的是不变的IP地址(与使用容器IP地址不同)。
    • Resolvable需要Docker套接字以便能查找容器名称。

    对于每个运行的容器,Resolvable会创建两个名称,即<容器名称>.docker<容器主机名>。可使用dig命令通过查找Resolvable容器自身对此进行测试(dig命令存在于Ubuntu的dnsutils包或CentOS的bind-utils中):

    1. $ dig +short @172.17.42.1 res.docker
    2. 172.17.0.22
    3. $ dig +short @172.17.42.1 resolvable
    4. 172.17.0.22

    这很有趣,但并不是十分有用——类似的信息使用docker inspect也可获得。不过,在启动配置使用这个新的DNS服务器的容器时,它的价值就体现出来了:

    1. $ docker run -it dns 172.17.42.1 ubuntu:14.04.2 bash
    2. root@216a71584c9c:/# ping -q -c1 res.docker
    3. PING res.docker (172.17.0.22) 56(84) bytes of data.
    4. —- res.docker ping statistics —-
    5. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
    6. rtt min/avg/max/mdev = 0.065/0.065/0.065/0.000 ms
    7. root@216a71584c9c:/# ping -q -c1 www.google.com
    8. PING www.google.com (216.58.210.36) 56(84) bytes of data.
    9. —- www.google.com ping statistics —-
    10. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
    11. rtt min/avg/max/mdev = 7.991/7.991/7.991/0.000 ms

    此处证实了使用由Resolvable提供的DNS服务器可以同时访问其他容器和外界。但是,每次启动容器时都要指定—dns参数有些麻烦。好在,可以通过给Docker守护进程传递相关参数来节省时间。编辑守护进程参数(使用适合当前操作系统的方法),添加以下内容:

    1. bip=172.17.42.1/16 dns=172.17.42.1

    设置Docker守护进程参数 Docker守护进程的具体参数帮助详见附录B。

    这些值来自本技巧开始时运行的用于查找Docker网桥细节的命令。读者需要根据自己的结果进行适当修改。—dns参数对守护进程的作用相当直观——它修改了容器使用的默认DNS服务器。与此同时,—bip将Docker网桥的配置固定了下来,这样在守护进程重启时它就不会发生潜在的变化(这会让所有容器DNS失效)。

    在启动Resolvable时传递的DNS参数在这里很关键——如果未对其进行指定,Resolvable在向上游查询时将使用默认的DNS服务器,也就是指向它自身!如果这种情况发生,将产生巨量的日志,而客户端的查询将会超时。

    一旦Docker守护进程完成重启,且Resolvable启动后,就可尝试启动一个容器:

    1. $ docker run rm ubuntu:14.04.2 ping -q -c1 resolvable
    2. PING resolvable (172.17.0.1) 56(84) bytes of data.
    3. —- resolvable ping statistics —-
    4. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
    5. rtt min/avg/max/mdev = 0.095/0.095/0.095/0.000 ms

    Resolvable具有一些决定性功能,虽然这里不对其进行详述,但是为了保持完整性稍作提及。如果将/etc/resolv.conf挂载到/tmp/resolv.conf上,该DNS服务器地址将被添加到宿主机的DNS服务器中,这样就可以在容器之外通过容器名称对其进行访问。使用systemd的用户可以通过将/run/systemd挂载到/tmp/systemd,并将/var/run/dbus/system_bus_socket挂载到容器中相同路径来实现类似的整合。

    在使用多宿主机时,如何能方便地查找和连接到其他容器是一个热门话题。本书后续章节将对服务发现做更多详细论述。

    多数用户会将互联网当作一个黑盒看待,即通过某些方式从世界其他角落获取信息并显示在屏幕上。时不时会碰到网速慢或连接中断的情况,对ISP的抱怨屡见不鲜。

    在构建的镜像包含需要进行连接的应用程序时,用户可能对哪些组件需要连接到哪里以及整体的设置情况有一个清晰的了解。但有一件事是不变的:还是会遭遇网速慢和连接中断的情况。即使是拥有并运营着自有数据中心的大型公司,也能观察到不可靠的网络及其引起的应用程序问题。

    接下来介绍几种方法,对不可靠网络进行体验,以确定现实世界中可能面对的问题。

    尽管用户在进行跨多主机分发应用程序时希望网络状况尽可能好,但现实却是残酷的——分组(packet,也称数据包)丢失(也称丢包)、连接中断、网络分区比比皆是,尤其是在商用云服务供应商上。

    在技术栈遭遇现实世界的这些情况之前对其进行测试以确认其行为是非常明智的—— 一个为高可用设计的应用程序不应在外部服务开始出现显著的额外延迟时陷入停顿。

    问题

    想要为单个容器应用不同的网络状况。

    解决方案

    使用Comcast(指的是网络工具,而非ISP)。

    讨论

    Comcast(https://github.com/tylertreat/Comcast)是一个娱乐化命名的工具,用于修改Linux机器的网络接口,以便对其应用某些不同寻常的(或者,对不走运的人而言是典型的)状况。

    在Docker创建容器时,它会同时创建几个虚拟的网络接口——这也是容器具有不同IP并且可以相互通信的原因。因为这些都是标准网络接口,只要能查找出其网络接口名称,就可以在其上使用Comcast。这说起来容易做起来难。

    以下Docker镜像包含了Comcast及其前置要求,以及一些优化:

    1. $ IMG=dockerinpractice/comcast
    2. $ docker pull $IMG
    3. latest: Pulling from dockerinpractice/comcast
    4. […]
    5. Status: Downloaded newer image for dockerinpractice/comcast:latest
    6. $ alias comcast=”docker run rm pid=host privileged \
    7. -v /var/run/docker.sock:/var/run/docker.sock $IMG
    8. $ comcast -help
    9. Usage of comcast:
    10.  -cont=””: Container ID or name to get virtual interface of
    11.  -default-bw=-1: Default bandwidth limit in kbit/s (fast-lane)
    12.  -device=””: Interface (device) to use (defaults to eth0 where applicable)
    13.  -dry-run=false: Specifies whether or not to commit the rule changes
    14.  -latency=-1: Latency to add in ms
    15.  -mode=”start”: Start or stop packet controls
    16.  -packet-loss=”0”: Packet loss percentage (eg: 0.1%%)
    17.  -target-addr=””: Target addresses, \
    18. (eg: 10.0.0.1 or 10.0.0.0/24 or 10.0.0.1,192.168.0.0/24)
    19.  -target-bw=-1: Target bandwidth limit in kbit/s (slow-lane)
    20.  -target-port=””: Target port(s) (eg: 80 or 1:65535 or 22,80,443,1000:1010)
    21.  -target-proto=”tcp,udp,icmp”: \
    22. Target protocol TCP/UDP (eg: tcp or tcp,udp or icmp)

    新增的优化提供了-cont选项,可以指向一个容器而无须查找虚拟网络接口的名称。请注意,为了赋予容器更多权限,docker run命令中增加了一些特殊的标志,这样Comcast就可以自由地对网络接口进行检查并应用变更。

    为了展示Comcast可以带来的变化,先来看一下一个正常的网络连接是什么样的。打开一个新的终端,并运行以下命令来设置基准网络性能的预期:

    1. $ docker run -it name c1 ubuntu:14.04.2 bash
    2. root@0749a2e74a68:/# apt-get update && apt-get install -y wget
    3. […]
    4. root@0749a2e74a68:/# ping -q -c 5 www.docker.com
    5. PING www.docker.com (104.239.220.248) 56(84) bytes of data.
    6. —- www.docker.com ping statistics —-
    7. 5 packets transmitted, 5 received, 0% packet loss, time 4005ms 1)        
    8. rtt min/avg/max/mdev = 98.546/101.272/106.424/2.880 ms 2)            
    9. root@0749a2e74a68:/# time wget -o /dev/null https://www.docker.com
    10. real  0m0.680s 3)          
    11. user  0m0.012s
    12. sys   0m0.006s
    13. root@0749a2e74a68:/#

    (1)这台机器与www.docker.com的连接看起来是可靠的,没有分组丢失

    (2)到www.docker.com的往返时间在100 ms左右

    (3)下载www.docker.com的HTML首页总共花费时间大概是0.7 s

    完成上述步骤后,保持该容器处于运行状态,然后对其应用一些网络状况:

    1. $ comcast -cont c1 -default-bw 50 -latency 100 -packet-loss 20%
    2. 2015/07/29 02:28:13 Found interface vetha7b90a7 for container c1
    3. sudo tc qdisc show | grep netem
    4. sudo tc qdisc add dev vetha7b90a7 handle 10: root htb
    5. sudo tc class add dev vetha7b90a7 parent 10: classid 10:1 htb rate 50kbit
    6. sudo tc class add dev vetha7b90a7 parent 10:1 classid 10:10 htb rate 50kbit
    7. sudo tc qdisc add dev vetha7b90a7 parent 10:10 handle 100:
    8. netem delay 100ms loss 20.00%
    9. sudo iptables -A POSTROUTING -t mangle -j CLASSIFY set-class 10:10 -p tcp
    10. sudo iptables -A POSTROUTING -t mangle -j CLASSIFY set-class 10:10 -p udp
    11. sudo iptables -A POSTROUTING -t mangle -j CLASSIFY set-class 10:10 -p icmp
    12. 2015/07/29 02:28:13 Packet rules setup
    13. 2015/07/29 02:28:13 Run sudo tc -s qdisc to double check
    14. 2015/07/29 02:28:13 Run comcast --mode stop to reset

    上述命令应用了3种不同的状况:针对所有目标设置50 KB/s的带宽上限(唤起了对拨号的回忆),添加100 ms的延迟,以及20%的分组丢失率。

    Comcast首先确定容器正确的虚拟网络接口,然后调用一些标准的Linux命令行网络工具来应用流量规则,并在执行过程中列出其所做的动作。来看一下容器是如何对此进行回应的:

    1. root@0749a2e74a68:/# ping -q -c 5 www.docker.com
    2. PING www.docker.com (104.239.220.248) 56(84) bytes of data.
    3. —- www.docker.com ping statistics —-
    4. 5 packets transmitted, 2 received, 60% packet loss, time 4001ms
    5. rtt min/avg/max/mdev = 200.453/200.581/200.709/0.128 ms
    6. root@0749a2e74a68:/# time wget -o /dev/null https://www.docker.com
    7. real  0m9.673s
    8. user  0m0.011s
    9. sys   0m0.011s

    成功了!ping报告的延迟增加了100 ms,而对wget的计时展示了10倍左右的降速,与预期相当(带宽上限、额外的延迟及分组丢失同时产生了影响)。但是分组丢失有点儿奇怪——似乎比预期大了3倍。需要注意的很重要的一点是,ping只发送了少量的分组,而分组丢失不是精确的“五分之一”计数器——如果将ping次数提高到50,将会发现分组丢失结果与预期要接近得多。

    注意,上面应用的规则对通过该网络接口的所有网络连接都有效,包括与宿主机及其他容器的连接。

    现在告诉Comcast删除这些规则。Comcast还无法对单个状况进行添加或删除,因此修改某张网络接口上的任何东西意味着要完全删除或重新添加该网络接口上的规则。如果要恢复正常的容器网络操作,也必须删除这些规则。不过,在退出容器时无须考虑这些规则的删除——它们会在Docker删除虚拟网络接口时被自动删除:

    1. $ comcast -cont c1 -mode stop
    2. 2015/07/29 02:31:34 Found interface vetha7b90a7 for container c1
    3. […]
    4. 2015/07/29 02:31:34 Packet rules stopped
    5. 2015/07/29 02:31:34 Run sudo tc -s qdisc to double check
    6. 2015/07/29 02:31:34 Run comcast --mode start to start

    如果读者有兴趣动手实践,可以深入挖掘Linux的流量控制工具,使用Comcast来生成要使用的命令示例集合。其可能性的完整性论述已经超出本技巧的范围,不过请记住,只要能将其放到容器内并连接到网络,就能使用它来做试验。

    对很多应用程序而言,Comcast是一个优秀的工具,不过有一个重要的使用场景它无法解决——如何将网络状况应用到全体容器中?手工对几十个容器运行Comcast将是非常痛苦的,上百个更是无法想象!这个问题对容器而言尤为相关,因为它们的启动代价非常低——如果在单台机器上运行上百个虚拟机而不是容器来模拟大型网络,将会遇到更大的问题,如内存不足!

    在使用多台机器模拟网络时,有一种特殊类型的网络故障会在这种规模下变得有趣起来——网络分区。当一组网络化的机器被分成两个或更多部分时,这种情况就会出现,同一部分里的所有机器可以相互通信,但不同部分则无法通信。研究表明这种情况的发生比想象中要多得多,尤其是在消费级的云服务上!

    遵循经典的Docker微服务线路可急剧缓解此类问题,而要理解服务如何对其进行处理,拥有用于做实验的工具就至关重要了。

    问题

    想要对大量容器进行网络状况编排设置,包括创建网络分区。

    解决方案

    使用Blockade。

    讨论

    Blockade(https://github.com/dcm-oss/blockade.git)是出自戴尔公司一个部门的开源软件,为“测试网络故障及分区”而生。看起来正是这里所需要的。

    注意 在编写本书时,如果不做优化,官方的Blockade仓库无法与Docker 1.6.2之后的任何版本兼容。为了本技巧的需要,我们对其做了一些修改,希望可以贡献回去。如果读者想深入Blockade,请记住这一点。

    Blockade通过读取当前目录中的配置文件(blockade.yml)来工作,该配置文件定义了容器启动的方式以及需要对其应用的状况。完整的配置细节可从Blockade文档中获得,因此这里只对核心部分进行说明。

    首先需要创建一个blockade.yml:

    1. containers:
    2.  server:
    3.   image: ubuntu:14.04.2
    4.   command: /bin/sleep infinity
    5.   expose: [10000]
    6.  client1:
    7.   image: ubuntu:14.04.2
    8.   command: sh -c ping $SERVER_PORT_10000_TCP_ADDR
    9.   links: [“server”]
    10.  client2:
    11.   image: ubuntu:14.04.2
    12.   command: sh -c ping $SERVER_PORT_10000_TCP_ADDR
    13.   links: [“server”]
    14. network:
    15.  flaky: 50%
    16.  slow: 100ms

    上述配置中设置的容器代表由两个客户端连接的一个服务器。在实践中,可能是一个数据库服务器及其客户端应用程序,并且对要建模的组件数量没有任何限制。只要它能在一个compose.yml文件(见技巧68)中表示,就可以在Blockade中对其进行建模。

    server的配置中指定了要暴露的一个端口,但并未在其上提供服务也不会对其进行连接——这只是启用Docker的链接功能,并在客户端容器中暴露相关的环境变量以便让它们知道去ping哪个IP。如果使用了其他IP发现技巧,如本章讲述的DNS技巧(见技巧70),则这些链接就不是必需的。

    这里暂时不用考虑network小节,稍后会对其进行说明。

    与往常一样,使用Blockade的第一步是拉取镜像:

    1. $ IMG=dockerinpractice/blockade
    2. $ docker pull $IMG
    3. latest: Pulling from dockerinpractice/blockade
    4. […]
    5. Status: Downloaded newer image for dockerinpractice/blockade:latest
    6. $ alias blockade=”docker run rm pid=host privileged \
    7. -v \$PWD:/blockade -v /var/run/docker.sock:/var/run/docker.sock $IMG

    可以看到,这里传递给docker run的参数与上一个技巧中的参数一致,只有一个例外——Blockade将当前目录挂载到容器中以便访问blockade.yml,并将状态存储在一个隐藏目录中。

    网络化文件系统之痛 如果是运行在网络文件系统之上,第一次启动Blockade可能会遇到奇怪的权限问题,这可能是因为Docker正在尝试以root身份创建该隐藏的状态目录,而网络文件系统不予配合。解决方案是使用本地磁盘。

    最后到了关键时刻——运行Blockade。要确保目前位于保存blockade.yml的目录中:

    1. $ blockade up
    2. NODE    CONTAINER ID  STATUS  IP      NETWORK  PARTITION
    3. client1  8c4d956cf9cf  UP    172.17.0.53 NORMAL
    4. client2  fcd9af2b0eef  UP    172.17.0.54 NORMAL
    5. server   b8f9f179a10d  UP    172.17.0.52 NORMAL

    调试提示 在启动时,Blockade有时可能会报“/proc”中文件不存在的晦涩错误。首先检查容器是否在启动时马上退出,阻止了Blockade检查其网络状态。此外,请尽量不要使用Blockade的-c选项来指定自定义配置文件目录——容器内只有当前目录的子目录可用。

    所有配置文件中定义的容器已经启动,并显示了已启动容器的一些有用信息。现在来应用一些基本的网络状况。在一个新的终端中持续打印client1的日志(使用docker logs -f 8c4d956cf9cf),以便在做修改时可以查看所发生的情况:

    1. $ blockade flaky all 1
    2. $ sleep 5 2)       
    3. $ blockade slow client1 3)      
    4. $ blockade status 4
    5. NODE    CONTAINER ID  STATUS  IP      NETWORK  PARTITION
    6. client1  8c4d956cf9cf  UP    172.17.0.53 SLOW
    7. client2  fcd9af2b0eef  UP    172.17.0.54 FLAKY
    8. server   b8f9f179a10d  UP    172.17.0.52 FLAKY
    9. $ blockade fast all 5

    (1)让所有容器的网络变得不稳定(分组丢失)

    (2)延后下一条命令,让前一条命令有时间生效并输出一些日志

    (3)让容器client1的网络变慢(为分组增加了延迟)

    (4)检查容器所处的状态

    (5)将所有容器恢复为正常操作

    flakyslow命令使用了之前配置文件中network一节定义的值——限定值无法在命令行中指定。如果有需要,可以在容器运行时编辑blockade.yml,然后有选择性地将新的限定值应用到容器上。需要注意的是,一个容器只能处在慢速网络不稳定网络中,不能二者皆有。撇开这些限制,对成百上千个容器运行这一命令的便捷性还是相当可观的。

    如果回头查看client1的日志,可以看到不同命令生效的时间:

    1. 64 bytes from 172.17.0.52: icmp_seq=638 ttl=64 time=0.054 ms 1) 
    2. 64 bytes from 172.17.0.52: icmp_seq=639 ttl=64 time=0.098 ms
    3. 64 bytes from 172.17.0.52: icmp_seq=640 ttl=64 time=0.112 ms
    4. 64 bytes from 172.17.0.52: icmp_seq=645 ttl=64 time=0.112 ms 2
    5. 64 bytes from 172.17.0.52: icmp_seq=652 ttl=64 time=0.113 ms
    6. 64 bytes from 172.17.0.52: icmp_seq=654 ttl=64 time=0.115 ms
    7. 64 bytes from 172.17.0.52: icmp_seq=660 ttl=64 time=100 ms 3) 
    8. 64 bytes from 172.17.0.52: icmp_seq=661 ttl=64 time=100 ms
    9. 64 bytes from 172.17.0.52: icmp_seq=662 ttl=64 time=100 ms
    10. 64 bytes from 172.17.0.52: icmp_seq=663 ttl=64 time=100 ms

    (1)icmp_seq是连续的(没有分组丢失),time也比较低(延迟小)

    (2)icmp_seq出现了一个大跳跃——flaky命令生效了

    (3)time出现了一个大跳跃——slow命令生效了

    虽然这很有用,不过在Comcast之上使用一些(可能是比较费力的)脚本也能实现,那么来看看Blockade的杀手锏功能——网络分区:

    1. $ blockade partition server client1,client2
    2. $ blockade status
    3. NODE    CONTAINER ID  STATUS  IP      NETWORK  PARTITION
    4. client1  8c4d956cf9cf  UP    172.17.0.53 NORMAL   2
    5. client2  fcd9af2b0eef  UP    172.17.0.54 NORMAL   2
    6. server   b8f9f179a10d  UP    172.17.0.52 NORMAL   1

    这会将3个节点划分成2个区域——服务器在其中一个区域,而客户端在另一个区域,它们之间无法进行通信。可以看到client1的日志停止了,因为所有的ping分组都丢失了!不过,两个客户端依然可以相互通信,这一点可以通过在二者之间发送一些ping分组来验证:

    1. $ docker exec 8c4d956cf9cf ping -qc 3 172.17.0.54
    2. PING 172.17.0.54 (172.17.0.54) 56(84) bytes of data.
    3. —- 172.17.0.54 ping statistics —-
    4. 3 packets transmitted, 3 received, 0% packet loss, time 1999ms
    5. rtt min/avg/max/mdev = 0.065/0.084/0.095/0.015 ms

    没有分组丢失,延迟也很低……看起来是个不错的连接。分区及其他网络状况是独立操作的,因此可以在应用分区的同时试验分组丢失。可以定义的分区数量没有限制,因此可以尽情地对复杂场景进行试验。

    最后一个建议是,将Blockade和Comcast组合起来能获得比它们单独提供的能力更强大。Blockade擅长创建分区以及完成启动容器的繁杂事务,添加Comcast则能实现每一个容器网络连接的细粒度控制!

    Docker的核心功能都与隔离性有关。前面几章已经展示了进程和文件系统隔离的好处,而本章呈现的是网络隔离。

    可以认为网络隔离具有以下两个方面:

    • 个体沙箱——每个容器具有各自用于监听的IP及端口集合,不会与其他容器(或宿主机)发生重叠;
    • 组沙箱——这是个体沙箱的逻辑扩展,所有隔离的容器都被分组在同一个私有网络中,可以在不干扰主机网络(惹恼公司网络管理员)的情况下进行试验。

    前面的两个技巧提供了这两个方面的一些实例——Comcast操纵单独的沙箱来为每个容器应用规则,而Blockade中的分区依赖对私有容器网络的全面监管能力来将其拆分成几部分。

    这些场景的背后看起来有点儿像图8-2。

    网桥工作的具体细节并不重要。简单来说,网桥在容器之间创建了一个扁平化网络(无须中间步骤即可直接通信),并将对外界的请求转发到外部连接上。

    虚拟网络产生的灵活性极大地激发了第三方通过多种方式扩展网络系统以实现更复杂的用例。Docker 公司使用这些成果来引导当下(编写本书时)的工作,允许直接将网络扩展插入到Docker中而不是绕过它。

    8

    图8-2 宿主机上的Docker内部网络

    基底网络是构建于其他网络之上的软件级网络。从效果上看,它像是一个本地网络,但其背后还是通过其他网络进行通信。这代表着性能代价,这个网络相比本地网络稳定性要差一些,不过从可用性角度来看还是大有好处的:位置完全不同的节点可以像在同一个空间里一样进行通信。

    实现这一点对Docker容器来说尤为有趣——容器可以像通过网络连接宿主机一样跨宿主机无缝地进行连接。这么做消除了规划单个宿主机上能容纳多少容器的迫切需要。

    问题

    想要跨宿主机进行容器间无缝通信。

    解决方案

    使用基底网络。

    讨论

    接下来将使用Weave(http://weave.works)来演示基底网络的原理,Weave是为这个目的设计的工具。图8-3展示了一个典型Weave网络的梗概。

    8

    图8-3 一个典型的Weave网络

    在图8-3中,宿主机1无法访问宿主机3,不过它们可以像本地连接一样通过Weave网络相互通信。Weave网络不对公众开放——只对在Weave下启动的容器开放。这使得跨不同环境的代码开发、测试与部署变得相对简单,因为可以将所有情景中的网络拓扑都做成一样的。

    1.安装Weave

    Weave是一个二进制程序,可以在https://github.com/zettio/weave找到其安装说明。

    Weave需要安装在要作为Weave网络一部分的所有宿主机上。可执行以下指令:

    1. $ sudo wget -O /usr/local/bin/weave \
    2. https://github.com/zettio/weave/releases/download/latest_release/weave
    3. $ sudo chmod +x /usr/local/bin/weave

    Weave二进制文件冲突 如果在此处遇到问题,机器上可能已经存在一个作为其他软件包一部分的Weave二进制文件。

    2.设置Weave

    要操作本示例,需要使用两台宿主机。此处分别将其命名为host1host2。使用ping确保它们可以相互通信。接着获取两台宿主机的IP地址。

    如何获取IP地址 一种获取宿主机IP地址的快速方法是使用浏览器访问http://ip-addr.es,或者运行curl http://ip-addr.es

    网络防火墙 如果在此处遇到问题,网络中可能存在某种形式的防火墙。如果不清楚,请与网络管理员确认。特别需要注意的是,TCP与UDP的6783端口需要同时打开。

    在第一台宿主机上运行第一个Weave路由器:

    1. host1$ curl http://ip-addr.es (1)
    2. 1.2.3.4
    3. host1$ sudo weave launch 2)       
    4. host1$ C=$(sudo weave run 10.0.1.1/24 -t -i ubuntu) 3

    (1)确认host1的IP地址

    (2)以root身份在host1上启动Weave服务。这一步需要在每台宿主机上进行一次

    (3)在容器内启动Weave路由器。以CIDR标记法为它指定一个IP地址和网络。此处给定的 IP 地址和网络定义了私有Weave网络的样子

    什么是CIDR CIDR是Classless Inter-Domain Routing(无类别域间路由)的缩写。它是分配IP地址和路由IP网络分组的一种方法。CIDR标记法由一个IP地址、一个斜杠及一个数字组成。最后的数字表示路由前缀中前导位的位数。其他位则形成了路由的地址空间。最后的数字越小,地址空间就越大。例如,一个192.168.2.0/24的网络将具有一个包含256个地址的8位地址空间,而一个16.0.0.0/8的网络将具有一个包含16 777 216个地址的地址空间。

    host2上可以执行类似的步骤,不过需要将host1的位置通知给Weave,并为它分配一个不同的IP地址:

    1. host2$ curl http://ip-addr.es (1)   
    2. 1.2.3.5
    3. host2# sudo weave launch 1.2.3.4 (2)    
    4. host2# C=$(sudo weave run 10.0.1.2/24 -t -i ubuntu) (3)

    (1)确定host2的IP地址

    (2)以root身份在host2上启动Weave服务。这一次添加了第一台宿主机的公共IP地址以便它可以连接到另一台宿主机上

    (3)在容器内启动 Weave 路由器。为其指定了相同网络上与host1不同的IP地址

    除了应用程序容器的IP地址的选择之外,host2上唯一的不同是要通知Weave,让它与host1上的Weave对等(通过host2可以访问到的IP地址或主机名及可选的“:端口”进行指定)。

    3.测试连接

    现在万事俱备,可以进行测试,看看容器是否能相互通信:

    1. host1# docker attach $C (1)   
    2. root@28841bd02eff:/# ping -c 1 -q 10.0.1.2 2)    
    3. PING 10.0.1.2 (10.0.1.2): 48 data bytes
    4. —- 10.0.1.2 ping statistics —-
    5. 1 packets transmitted, 1 packets received, 0% packet loss 3)    
    6. round-trip min/avg/max/stddev = 1.048/1.048/1.048/0.000 ms

    (1)附加到本次交互会话早前返回的容器ID上

    (2)ping 另一台服务器分配到的IP地址

    (3)一个成功的ping响应

    如果ping成功了,即可证实自行分配的私有网络内部的可连接性。现在可以将10.0.1.1分配给应用程序服务器,将10.0.1.2分配给Web服务器。

    ICMP被阻塞? 如果(ping使用的)ICMP协议信息被防火墙阻塞,这一步就无法工作。如果出现这种情况,可以尝试telnet到另一台宿主机的6783端口来测试是否可以建立连接。

    Weave是一个优秀的工具,不过它依赖于Docker外部的一个工具,因此可能无法很好地与生态系统里的其他工具整合。

    由于Weave这类工具的流行,Docker公司收集了对Docker内网络解决方案有兴趣的多家公司的反馈,并形成一个方案来尝试解决这些最迫切的需求,同时又不将人们限制在一个大而全的解决方案之中。这项工作还在进行中,不过已经接近发布了!

    问题

    想要一种由Docker公司支持的用于创建虚拟网络的解决方案。

    解决方案

    使用实验性的网络与服务功能。

    讨论

    在阅读本书时,Docker 可能已经在一个稳定版本中发布了(目前是)实验性的网络功能。这一点可以通过运行以下命令来确认:

    1. $ docker network help
    2. Usage: docker network [OPTIONS] COMMAND [OPTIONS] [arg…]
    3. Commands:
    4.  create          Create a network
    5.  rm            Remove a network
    6.  ls            List all networks
    7.  info           Display information of a network
    8. Run docker network COMMAND help for more information on a command.
    9.  help=false    Print usage

    如果提示类似‘network’ is not a docker command的信息,则表示需要安装Docker的实验性版本。要安装实验性功能,可查阅GitHub上的Docker Experimental Features(Docker实验性功能)页面:https://github.com/docker/docker/tree/master/experimental。

    实验性功能发生根本变更 本书尽可能地保持本技巧的时效性,不过实验性功能往往会发生改变。在试验本技巧时,相应的指令可能需要做轻微改动。

    这项功能的高级别目标是允许在Docker中使用网络插件把虚拟网络的创建抽象掉。这些插件要么是内置的,要么由第三方提供,都将提供一个虚拟网络。在这些场景的背后,插件将完成创建网络所需的所有必要工作,让用户可以接着使用它。在理论上,类似Weave的工具应该可以变成网络插件,带来更奇特的用例。在实践中,这个功能的设计很可能是一个迭代的过程。

    运行以下命令可查看Docker拥有的网络清单:

    1. $ docker network ls
    2. NETWORK ID     NAME       TYPE
    3. 04365ecf2eaa    none       null
    4. c82bde52597d    host       host
    5. 7e8c8a0eab7d    bridge      bridge

    可以看出,这就是在启动容器时传递给docker run—net选项的可选值。下面添加一个新的bridge网络(一个让容器可以在其中自由通信的扁平化网络):

    1. $ docker network create driver=bridge mynet
    2. 3265097deff3847cb1f7b8e8bc924bae1c439d8bf6247458400e620b35447292
    3. $ docker network ls | grep mynet
    4. 3265097deff3    mynet        bridge
    5. $ ip addr | grep mynet
    6. 34: mynet: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
    7. state DOWN
    8.   inet 172.18.42.1/16 scope global mynet
    9. $ ip addr | grep docker
    10. 4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
    11. state DOWN
    12.   inet 172.17.42.1/16 scope global docker0

    这创建了一个新的网络接口,它将使用与普通Docker网桥不同的一个IP地址范围。

    接下来启动两个容器,将服务绑定到该网络中。目前,服务的概念是紧密结合在网络功能里的,容器要参与特定的网络必须具有一个服务名:

    1. $ docker run -it -d name c1 ubuntu:14.04.2 bash 1
    2. 87c67f4fb376f559976e4a975e3661148d622ae635fae4695747170c00513165
    3. $ docker service publish c1service.mynet 2
    4. ed190f2cc0887ac87e1024ebb425f653989582942ab25a341e3d3e2a980475f5
    5. $ docker service attach c1 c1service.mynet 3
    6. $ docker run -it -d name c2 \
    7. publish-service=c2service.mynet ubuntu:14.04.2 bash 4
    8. 0ee74a3e3444f27df9c2aa973a156f2827bcdd0852c6fd4ecfd5b152846dea5b
    9. $ docker service ls network mynet 5
    10. SERVICE ID   NAME     NETWORK  CONTAINER
    11. ed190f2cc088  c1service   mynet   87c67f4fb376 6
    12. 21aef543af70  c2service   mynet   0ee74a3e3444

    (1)启动一个名为c1的容器

    (2)在mynet网络内部创建一个名为c1service的未关联的服务名

    (3)将容器c1 与服务c1service关联起来

    (4)在mynet网络内部创建一个名为c2 的容器以及一个c2service服务名

    (5)展示两个容器现在已经将服务绑定到mynet里

    (6)如果CONTAINER字段为空,记住要运行“docker service attach c1 c1service.mynet”来附加该服务

    上述命令演示了注册服务的两种不同方法,即创建一个容器然后附加服务,以及一步完成创建和附加。

    这二者之间存在一个差异。第一个容器将在启动时加入默认网络(通常是 Docker 网桥,不过这可以使用Docker守护进程的参数进行定制),然后添加一个新的网络接口以便可以同时访问mynet。第二个容器将加入mynet——正常Docker网桥上的任何容器都无法访问它。

    下面做一些连接性检查:

    1. $ docker exec c1 ip addr | grep inet.eth 1) 
    2.   inet 172.17.0.6/16 scope global eth0
    3.   inet 172.18.0.5/16 scope global eth1
    4. $ docker exec c2 ip addr | grep inet.*eth’(2
    5.   inet 172.18.0.6/16 scope global eth0
    6. $ docker exec c1 ping -qc1 c2service 3)     
    7. ping: unknown host c2service
    8. $ docker exec c1 ping -qc1 172.18.0.6 4
    9. PING 172.18.0.6 (172.18.0.6) 56(84) bytes of data.
    10. —- 172.18.0.6 ping statistics —-
    11. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
    12. rtt min/avg/max/mdev = 0.069/0.069/0.069/0.000 ms
    13. $ docker exec c2 ping -qc1 c1service 5
    14. PING c1service (172.18.0.5) 56(84) bytes of data.
    15. —- c1service ping statistics —-
    16. 1 packets transmitted, 1 received, 0% packet loss, time 0ms
    17. rtt min/avg/max/mdev = 0.084/0.084/0.084/0.000 ms

    (1)列出c1的网络接口和IP地址

    (2)列出c2的网络接口和IP地址

    (3)尝试从容器1 ping容器2的服务

    (4)尝试从容器1 ping容器2的IP地址

    (5)尝试从容器2 ping容器1的服务

    这里发生了很多事情!我们发现,尽管容器的实际IP地址对彼此都是绝对可用的,容器c1无法找到c2的服务。这是由服务的工作方式造成的——在添加和删除服务时,容器内的/etc/hosts将被更新,不过c1不会发生这样的情况,因为它初始启动在默认了docker0网桥上。这个意外的行为在未来可能会改变,也可能不会。

    简单起见,建议尽可能坚持使用—publish-service,不过,由一个容器连接两个网络的场景对模拟现实世界堡垒主机的场景还是相当有用的。

    这里未说明的一件事(因为项目还在进行中)是内置的叠加(overlay)网络插件——根据用例不同,将其作为Weave可能的替换品还是值得做些研究的。

    在评估 Docker 其他方面时,它提供的网络可能性最初可能会被忽略,因此,本章通过使用网络功能对其他功能进行补充,同时展现了其自身的价值。

    本章主要讲述了:

    • 如何使用Docker Compose;
    • 使用链接的一种替代方案;
    • 在恶劣的网络环境中对容器进行测试;
    • 把不同宿主机的容器串联在一起。

    在开发流水线中使用Docker的话题到此为止,是时候看看如何在生产环境中实际使用它了,第一步是探索如何实际地管理所有容器。