.. include:: ../../sumaccess.rst =============== Docker 网络管理 =============== Docker网络基础 ================== 在深入Docker内部的网络实现原理之前,先来直观感受一下Docker的网络架构与基本操作。 Docker网络基础 ---------------- Docker在1.9版本中引入了一整套的docker network子命令和跨主机网络支持。这允许用户可以根据他们应用的拓扑结构创建虚拟网络并将容器接入其所对应的网络。 其实,早在Docker1.7版本中,网络部分代码就已经被抽离并单独成为了Docker的网络库,即libnetwork。在此之后,容器的网络模式也被抽象变成了统一接口的驱动。 为了标准化网络驱动的开发步骤和支持多种网络驱动,Docker公司在libnetwork中使用了CNM(Container NetworkModel)。CNM定义了构建容器虚拟化网络的模型,同时还提供了可以用于开发多种网络驱动的标准化接口和组件。 libnetwork和Docker daemon及各个网络驱动的关系可以通过下图进行形象的表示: .. figure:: ../image/docker网络模型.svg :width: 480px :alt: Docker网络虚拟化架构 :align: center Docker网络虚拟化架构 Docker daemon通过调用libnetwork对外提供的API完成网络的创建和管理等功能。libnetwork中则使用了CNM来完成网络功能的提供。 而CNM中主要有沙盒(sandbox)、端点(endpoint)和网络(network)这3种组件。 libnetwork中内置的5种驱动则为libnetwork提供了不同类型的网络服务。下面分别对CNM中的3个核心组件和libnetwork中的5种内置驱动进行介绍。 CNM中的3个核心组件如下: **沙盒** 一个沙盒包含了一个容器网络栈的信息。沙盒可以对容器的接口、路由和DNS设置等进行管理。沙盒的实现可以是Linux network namespace、FreeBSD Jail或者类似的机制。一个沙盒可以有多个端点和多个网络。 **端点** 一个端点可以加入一个沙盒和一个网络。端点的实现可以是veth pair、Open vSwitch内部端口或者相似的设备。一个端点只可以属于一个网络并且只属于一个沙盒。 **网络** 一个网络是一组可以直接互相联通的端点。网络的实现可以是Linux bridge、VLAN等。一个网络可以包含多个端点。 libnetwork中的5种内置驱动如下: **brige驱动** 此驱动为Docker的默认设置,使用这个驱动的时候,libnetwork将创建出来的Docker容器连接到Docker网桥上(Docker网桥稍后会做介绍)。 作为最常规的模式,bridge模式已经可以满足Docker容器最基本的使用需求了。然而其与外界通信使用NAT,增加了通信的复杂性, 在复杂场景下使用会有诸多限制。 **host驱动** 使用这种驱动的时候,libnetwork将不为Docker容器创建网络协议栈,即不会创建独立的networknamespace。 Docker容器中的进程处于宿主机的网络环境中,相当于Docker容器和宿主机共用同一个networknamespace,使用宿主机的网卡、IP和端口等信息。 但是,容器其他方面,如文件系统、进程列表等还是和宿主机隔离的。host模式很好地解决了容器与外界通信的地址转换问题, 可以直接使用宿主机的IP进行通信,不存在虚拟化网络带来的额外性能负担。但是host驱动也降低了容器与容器之间、 容器与宿主机之间网络层面的隔离性,引起网络资源的竞争与冲突。因此可以认为host驱动适用于对于容器集群规模不大的场景。 **overlay驱动** 此驱动采用IETF标准的VXLAN方式,并且是VXLAN中被普遍认为最适合大规模的云计算虚拟化环境的SDN controller模式。 在使用的过程中,需要一个额外的配置存储服务,例如Consul、etcd或ZooKeeper。还需要在启动Docker daemon的的时候额外添加参数来指定所 使用的配置存储服务地址。 **remote驱动** 这个驱动实际上并未做真正的网络服务实现,而是调用了用户自行实现的网络驱动插件,使libnetwork实现了驱动的可插件化, 更好地满足了用户的多种需求。用户只要根据libnetwork提供的协议标准,实现其所要求的各个接口并向Docker daemon进行注册。 **null驱动** 使用这种驱动的时候,Docker容器拥有自己的network namespace,但是并不为Docker容器进行任何网络配置。也就是说, 这个Docker容器除了networknamespace自带的loopback网卡外,没有其他任何网卡、IP、路由等信息,需要用户为Docker容器添加网卡、 配置IP等。这种模式如果不进行特定的配置是无法正常使用的,但是优点也非常明显,它给了用户最大的自由度来自定义容器的网络环境。 libnetwork案例实验 +++++++++++++++++++ 在初步了解了libnetwork中各个组件和驱动后,为了更加深入地理解libnetwork中的CNM模型和熟悉dockernetwork子命令的使用, 这里介绍一个libnetwork官方GitHub上示例的搭建过程,并在搭建成功后对其中容器之间的连通性进行验证,如下图所示。 .. figure:: ../image/dockerCNM.svg :width: 480px :alt: Docker CNM示意图 :align: center 在这个例子中,使用Docker默认的bridge驱动进行演示。在此例中,会在Docker上组成一个网络拓扑的应用。 - 它有两个网络,其中backend network为后端网络,frontend network则为前端网络,两个网络互不联通。 - 其中container1和container3各拥有一个端点,并且分别加入到后端网络和前端网络中。而container2则有两个端点,它们两个分别加入到后端网络和前端网络中。 在开始实验之前,先来查看有什么默认的网络驱动类型: .. code-block:: bash :linenos: :emphasize-lines: 1 $ docker network ls NETWORK ID NAME DRIVER SCOPE 12ea7ab05bda bridge bridge local 930b2daaaf16 host host local 1587f8f9fe33 none null local .. NOTE:: 这3个网络是Docker daemon默认创建的,分别使用了3种不同的驱动,而这3种驱动则对应了Docker原来的3种网络模式,这个在后面做详细讲解。 需要注意的是,3种内置的默认网络是无法使用docker network rm进行删除的。 接下来通过 `docker network create` 命令创建前后端两个网络 .. code-block:: bash :linenos: :emphasize-lines: 1,3 :caption: 创建Docker网络 $ docker network create frontend 16e2b38d6bb184332580032c852b350d63e9348f51b1c663675ec1329522b359 $ docker network create backend ebaba5717f786b3d1b8ac5d337dbe42756372d7c7f47dd006354cab69992c494 $ docker network ls NETWORK ID NAME DRIVER SCOPE ebaba5717f78 backend bridge local 12ea7ab05bda bridge bridge local 16e2b38d6bb1 frontend bridge local 930b2daaaf16 host host local 1587f8f9fe33 none null local 在创建了所需要的两个网络之后,接下来创建3个容器,并使用如下命令将名为container1和container2的容器加入到backend网络中,将名为container3的容器加入到frontend网络中。 .. code-block:: bash :linenos: :emphasize-lines: 1,3 $ docker run --name container1 --rm -itd --network backend busybox sleep 600 ...... $ docker run --name container2 --rm -itd --network backend busybox sleep 600 ...... $ docker run --name container3 --rm -itd --network frontend busybox sleep 600 ...... 分别在container1和container3中使用ping命令测试其与container2的连通性 .. code-block:: bash :linenos: :emphasize-lines: 1, 10 $ docker exec -it container1 ping container2 PING container2 (172.19.0.3): 56 data bytes 64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.101 ms 64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.110 ms ^C --- container2 ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.101/0.105/0.110 ms $ docker exec -it container3 ping container2 ping: bad address 'container2' - 因为container1与container2都在backend网络中,所以两者可以连通。 - 但是,因为container3和container2不在一个网络中,所以两个之间并不能连通。 在container2中使用命令ifconfig来查看此容器中的网卡及其配置情况: .. code-block:: bash :linenos: :emphasize-lines: 3 $ docker exec -it container2 ifconfig eth0 Link encap:Ethernet HWaddr 02:42:AC:13:00:03 inet addr:172.19.0.3 Bcast:172.19.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:13 errors:0 dropped:0 overruns:0 frame:0 TX packets:4 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:978 (978.0 B) TX bytes:280 (280.0 B) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) 可以看到,除了回环网卡lo之外,此容器中只有一块以太网卡,其名称为eth0, 再来通过 `docker network inspect` 来查看backend网络的细节 .. code-block:: bash :linenos: :emphasize-lines: 15 :caption: 查看docker网络 :name: 查看docker网络 $ docker network inspect backend [ { "Name": "backend", "Id": "ebaba5717f786b3d1b8ac5d337dbe42756372d7c7f47dd006354cab69992c494", "Created": "2021-05-22T08:57:55.535020461+08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.19.0.0/16", "Gateway": "172.19.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "3176d88add17577c0c5c8e49cbc45eb3d6235568756ca363b06f4852eaf4556a": { "Name": "container1", "EndpointID": "8b96843a0998294bb54b84d52fea1e7f030c67fc82d43d9bd0e122230e63eb08", "MacAddress": "02:42:ac:13:00:02", "IPv4Address": "172.19.0.2/16", "IPv6Address": "" }, "cce37be32e8160f6ec387b5bac0311ed163ee2873df29a763af34f2de4ef4157": { "Name": "container2", "EndpointID": "ea3820c431454681ffbab3486df9d5fe7bc61b40b4cd91e22ee0eb20cd6a501a", "MacAddress": "02:42:ac:13:00:03", "IPv4Address": "172.19.0.3/16", "IPv6Address": "" } }, "Options": {}, "Labels": {} } ] 可以发现Container2容器配置了和网桥backend同在一个IP段的IP地址,这个网卡eth0就是CNM模型中的 **端点** 。 最后,使用如下命令将container2加入到frontend网络中。 .. code:: bash $ docker network connect frontend container2 再次,在container2中使用命令ifconfig来查看此容器中的网卡及其配置情况。 .. code-block:: bash :linenos: :emphasize-lines: 30 $ docker exec -it container2 ifconfig eth0 Link encap:Ethernet HWaddr 02:42:AC:13:00:03 ...... eth1 Link encap:Ethernet HWaddr 02:42:AC:12:00:03 inet addr:172.18.0.3 Bcast:172.18.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:8 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:656 (656.0 B) TX bytes:0 (0.0 B) lo Link encap:Local Loopback ...... $ docker network inspect frontend [ { "Name": "frontend", "Id": "16e2b38d6bb184332580032c852b350d63e9348f51b1c663675ec1329522b359", "Created": "2021-05-22T08:57:47.412957638+08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "2212ba15bba07a925a9a20860369fcf2c1cd186c62697089c9215db59f6b59e9": { "Name": "container3", "EndpointID": "53105b0ebc849f9bb6ce593dd0744f016bac8e89f4d32a5a422e0c06ce806bf1", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": "" }, "e604c96796d6bb01c4c0f4fea9574d9222af5fb6ff46f7fcb1e6fd37826925b3": { "Name": "container2", "EndpointID": "c630c73de7cc8a6bc5ff7b52abe6d98fab6e07dddc453f3ec4577eaa07f80126", "MacAddress": "02:42:ac:12:00:03", "IPv4Address": "172.18.0.3/16", "IPv6Address": "" } }, "Options": {}, "Labels": {} } ] 发现多了一块名为eth1的以太网卡,并且其IP和网桥frontend同在一个IP段。 测试container2与container3的连通性 .. code:: bash $ docker exec -it container3 ping container2 PING container2 (172.18.0.3): 56 data bytes 64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.106 ms 64 bytes from 172.18.0.3: seq=1 ttl=64 time=0.101 ms ^C --- container2 ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.101/0.103/0.106 ms $ docker exec -it container2 ping container3 PING container3 (172.18.0.2): 56 data bytes 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.085 ms 64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.113 ms ^C --- container3 ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.085/0.099/0.113 ms 可以看出, ``docker network connect`` 命令会在所连接的容器中创建新的网卡,以完成其与所指定网络的连接。 bridge驱动实现机制分析 ---------------------- 前面我们演示了bridge驱动下的CNM使用方式,接下来将会分析bridge驱动的实现机制。 docker0网桥 ++++++++++++++ 当在一台未经特殊网络配置的RHEL机器上安装完Docker之后,在宿主机上通过使用ifconfig命令可以看到多了一块名为docker0的网卡,IP为172.17.0.1/16。 .. code:: bash $ ifconfig ...... docker0: flags=4099 mtu 1500 inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255 inet6 fe80::42:45ff:fe4f:7917 prefixlen 64 scopeid 0x20 ether 02:42:45:4f:79:17 txqueuelen 0 (Ethernet) RX packets 25996766891 bytes 3338045520307 (3.0 TiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 25996762485 bytes 3701990436932 (3.3 TiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ...... 有了这样一块网卡,宿主机也会在内核路由表上添加一条到达相应网络的静态路由,可通过route -n命令查看。 .. code:: bash $ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface ...... 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 ...... 此条路由表示所有目的IP地址为172.17.0.0/16的数据包从docker0网卡发出。 然后使用 ``docker run`` 命令创建一个Docker容器: .. code:: bash $ docker run -itd --rm --name con1 busybox sleep 600 查看该容器网卡: .. code:: bash $ docker exec -it con1 ifconfig eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02 inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:8 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:656 (656.0 B) TX bytes:0 (0.0 B) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) 在con1容器中可以看到它有两块网卡lo和eth0。lo设备不必多说,是容器的回环网卡;eth0即为容器与外界通信的网卡,eth0的IP为 ``172.17.0.2/16`` , 和宿主机上的网桥docker0在同一个网段。 查看con1的路由表: .. code:: bash $ docker exec -it con1 route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 可以发现con1的默认网关正是宿主机的docker0网卡,通过测试,con1可以顺利访问外网和宿主机网络, 因此表明con1的eth0网卡与宿主机的docker0网卡是相互连通的。 这时查看宿主机的网络设备,会发现有一块以“veth”开头的网卡, .. code:: bash $ ifconfig ...... vetha311e81: flags=4163 mtu 1500 inet6 fe80::90a9:5bff:feb0:eb9c prefixlen 64 scopeid 0x20 ether 92:a9:5b:b0:eb:9c txqueuelen 0 (Ethernet) RX packets 8 bytes 608 (608.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 15 bytes 1271 (1.2 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 ``vetha311e81`` ,我们可以大胆猜测这块网卡肯定是veth设备了, 而veth pair总是成对出现的。前面介绍过,veth pair通常用来连接两个 ``network namespace`` ,那么另一个应该是Docker容器con1中的eth0了。 之前已经判断con1容器的eth0和宿主机的docker0是相连的,那么 ``vethe043f86`` 也应该是与docker0相连的,不难想到,docker0就不只是一个简单的网卡设备了,而是一个网桥。 真实情况正是如此,下图即为Docker默认网络模式(bridge模式)下的网络环境拓扑图,创建了docker0网桥,并以veth pair连接各容器的网络, 容器中的数据通过docker0网桥转发到eth0网卡上。 .. figure:: ../image/dockerbrige模型.svg :width: 480px :alt: Docker网络brige模型 :align: center Docker网络brige模型示意图 这里网桥的概念等同于交换机,为连在其上的设备转发数据帧。网桥上的veth网卡设备相当于交换机上的端口,可以将多个容器或虚拟机连接在其上, 这些端口工作在二层,所以是不需要配置IP信息的。图中docker0网桥就为连在其上的容器转发数据帧, 使得同一台宿主机上的Docker容器之间可以相互通信。那么docker0既然是二层设备,其上怎么也配置了IP呢? docker0是普通的Linux网桥,它是可以在上面配置IP的,可以认为其内部有一个可以用于配置IP信息的网卡接口 (如同每一个Open vSwitch网桥都有一个同名的内部接口一样)。在Docker的桥接网络模式中, docker0的IP地址作为连于之上的容器的默认网关地址存在。在Linux中,可以使用brctl命令查看和管理网桥(需要安装 ``bridge-utils`` 软件包) 如查看本机上的Linux网桥以及其上的端口: .. code:: bash $ brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242454f7917 no veth1baf753 docker0网桥是在Docker daemon启动时自动创建的,其IP默认为172.17.0.1/16,之后创建的Docker容器都会在docker0子网的范围内选取一个未占用 的IP使用,并连接到docker0网桥上。Docker提供了如下参数可以帮助用户自定义docker0的设置。 - ``--bip=CIDR`` : 设置docker0的IP地址和子网范围,使用CIDR格式,如192.168.100.1/24。注意这个参数仅仅是配置docker0的,对其他自定义的网桥无效。并且在指定这个参数的时候,宿主机是不存在docker0的或者docker0已存在且docker0的IP和参数指定的IP一致才行。 - ``--fixed-cidr=CIDR`` : 限制Docker容器获取IP的范围。Docker容器默认获取的IP范围为Docker网桥(docker0网桥或者 ``--bridge`` 指定的网桥)的整个子网范围,此参数可将其缩小到某个子网范围内,所以这个参数必须在Docker网桥的子网范围内。如docker0的IP为 ``172.17.0.1/16`` ,可将 ``--fxed-cidr`` 设为 ``172.17.1.1/24`` ,那么Docker容器的IP范围将为 ``172.17.1.1~172.17.1.254`` 。 - ``--mtu=BYTES`` : 指定docker0的最大传输单元( ``MTU`` )。 除了使用docker0网桥外,还可以使用自己创建的网桥,使用--bridge=BRIDGE参数指定。使用如下命令添加一个名为br0的网桥,并且为其配置IP。 .. code:: bash $ brctl addbr br0 $ ifconfig br0 188.18.0.1 然后在启动Docker daemon的时候使用--bridge=br0指定使用br0网桥即可。 .. CAUTION:: --bridge参数若和--bip参数同时使用会产生冲突。 iptables规则 ++++++++++++++++ Docker安装完成后,将默认在宿主机系统上增加一些iptables规则,以用于Docker容器和容器之间以及和外界的通信,可以使用 ``iptables-save`` 命令查看 。其中nat表上的POSTROUTING链有这么一条规则: .. code:: bash -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE 这条规则关系着Docker容器和外界的通信,含义是将源地址为 ``172.17.0.0/16`` 的数据包(即Docker容器发出的数据), 当不是从docker0网卡发出时做SNAT(源地址转换,将IP包的源地址替换为相应网卡的地址)。这样一来,从Docker容器访问外网的流量, 在外部看来就是从宿主机上发出的,外部感觉不到Docker容器的存在。那么,外界想要访问Docker容器的服务时该怎么办? 我们启动一个简单的Web服务容器,观察iptables规则有何变化。 首先启动一个Web容器,将其80端口映射到宿主机的80端口上。 .. code:: shell $ docker run --name mynginx --rm -d -p 80:80 nginx:latest 然后查看iptables规则,省略部分无用信息。 .. code:: bash *nat ...... $ iptables-save -A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80 ...... *filter ...... -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT ...... 可以看到,在nat和filter的DOCKER链中分别增加了一条规则,这两条规则将访问宿主机80端口的流量转发到 ``172.17.0.2`` 的 ``80`` 端口上 (真正提供服务的Docker容器IP端口),所以外界访问Docker容器是通过iptables做DNAT(目的地址转换)实现的。 此外,Docker的forward规则默认允许所有的外部IP访问容器,可以通过在filter的DOCKER链上添加规则来对外部的IP访问做出限制, 如只允许源IP为 ``8.8.8.8`` 的数据包访问容器,需要添加如下规则: .. code:: bash iptables -I DOCKER -i docker0 ! -s 8.8.8. -j DROP 不仅仅是与外界间通信,Docker容器之间互相通信也受到iptables规则限制。 通过前面的学习,我们了解到同一台宿主机上的Docker容器默认都连在docker0网桥上,它们属于一个子网,这是满足相互通信的第一步。 同时,Docker daemon会在filter的FORWARD链中增加一条ACCEPT的规则( ``--icc=true`` ): .. code:: bash -A FORWARD -i docker0 -o docker0 -j ACCEPT 这是满足相互通信的第二步。当Docker daemon启动参数 ``--icc`` (icc参数表示是否允许容器间相互通信)设置为false时,以上规则会被设置为DROP, Docker容器间的相互通信就被禁止,这种情况下,想让两个容器通信就需要在docker run时使用 ``--link`` 选项。 在Docker容器和外界通信的过程中,还涉及了数据包在多个网卡间的转发(如从docker0网卡到宿主机eth0的转发), 这需要内核将ip-forward功能打开,即将ip_forward系统参数设为 ``1`` 。Docker daemon启动的时候默认会将其设为1( ``--ip-forward=true`` ), 也可以通过以下命令手动设置: .. code:: bash $ echo 1 > /proc/sys/net/ipv4/ip_forward $ cat /proc/sys/net/ipv4/ip_forward 1 以上过程中所涉及的Docker daemon启动参数如下: - ``--iptables`` : 是否允许Docker daemon设置宿主机的iptables规则,默认为true。当设为false时,Dockerdaemon将不会改变你宿主机上的iptables规则。 - ``--icc`` : 是否允许Docker容器间相互通信,默认为true。true或false改变的是FORWARD链中相应iptables规则的策略(ACCEPT、DROP)。由于操作的是iptables规则,所以需要——iptables=true才能生效。 - ``--ip-forward`` : 是否将ip_forward参数设为1,默认为true,用于打开Linux内核的ip数据包转发功能。 .. NOTE:: 这些参数也是在Docker daemon启动时进行设置的,所以可以设置在DOCKER_OPTS变量中。 Docker容器的DNS和主机名 +++++++++++++++++++++++ 一个Docker镜像可以启动很多个Docker容器,通过查看,它们的主机名并不一样,也即是说主机名并非是被写入镜像中的。 前面已经提及,实际上容器中/etc目录下有3个文件是容器启动后被虚拟文件覆盖掉的, 分别是 ``/etc/hostname`` 、 ``/etc/hosts`` 、 ``/etc/resolv.conf`` ,通过在容器中运行 ``mount`` 命令可以查看。 .. code-block:: bash :linenos: :emphasize-lines: 3-5 $ docker exec -it mynginx mount ...... /dev/sdb on /etc/resolv.conf type xfs (rw,relatime,attr2,inode64,noquota) /dev/sdb on /etc/hostname type xfs (rw,relatime,attr2,inode64,noquota) /dev/sdb on /etc/hosts type xfs (rw,relatime,attr2,inode64,noquota) ...... 这样能解决主机名的问题,同时也能让DNS及时更新(改变resolv.conf)。由于这些文件的维护方法随着Docker版本演进而不断变化, 因此尽量不修改这些文件,而是通过Docker提供的参数进行相关设置,参数配置方式如下: - ``-h`` HOSTNAME或者--hostname=HOSTNAME: 设置容器的主机名,此名称会写在/etc/hostname和/etc/hosts文件中,也会在容器的bash提示符中看到。但是在外部,容器的主机名是无法查看的,不会出现在其他容器的hosts文件中,即使使用docker ps命令也查看不到。此参数是docker run命令的参数,而非Dockerdaemon的启动参数。 - ``--dns=IP_ADDRESS...`` : 为容器配置DNS,写在 ``/etc/resolv.conf`` 中。该参数既可以在Docker daemon启动的时候设置也可以在 ``docker run`` 时设置,默认为 ``8.8.8.8`` 和 ``8.8.4.4`` 。 注意对以上3个文件的修改不会被 ``docker commit`` 保存,也就是不会保存在镜像中,重启容器也会导致修改失效。另外,在不稳定的网络环境下使用需要特别注意DNS的设置。 传统的link原理解析 =================== 在使用Docker容器部署服务的时候,经常会遇到需要容器间交互的情况,如Web应用与数据库服务。在前面的介绍中, 了解到容器间的通信由Docker daemon的启动参数--icc控制。很多情况下,为了保证容器以及主机的安全,--icc通常设置为false。 这种情况下该如何解决容器间的通信呢?通过容器向外界进行端口映射的方式可以实现通信,但这种方式不够安全, 因为提供服务的容器仅希望个别容器可以访问。除此之外,这种方式需要经过NAT,效率也不高。这时候, 就需要使用Docker的连接(linking)系统了。Docker的连接系统可以在两个容器之间建立一个安全的通道, 使得接收容器(如Web应用)可以通过通道得到源容器(如数据库服务)指定的相关信息。 在Docker 1.9版本后,网络操作独立成为一个命令组(``docker network``), link系统也与原来不同了,Docker为了保持对向上兼容,若容器使用默认的bridge模式网络,则会默认使用传统的link系统;而使用用户自定义的网络(user-definednetwork),则会使用新的link系统。 使用link通信 ---------------- link是在容器创建的过程中通过--link参数创建的。还是以Web应用与数据库为例来演示link的使用。 首先,新建一个含有web服务的nginx容器。 然后,新建一个curl容器,并在容器内连接web容器。 .. code:: bash $ docker run --rm -d --name web nginx:latest $ docker run -it --name client --rm --link web:webserver curlimages/curl:latest curl http://webserver:80 Welcome to nginx!

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

``--link`` 参数的格式是这样的 ``--link :alias`` 。其中name是容器通过 ``--name`` 参数指定或自动生成的名字,如“web”, “client”等,而不是容器的主机名。alias为容器的别名,如本例中的webserver 这样一个link就创建完成了,client容器可以从web容器中获取数据。client容器叫作接收容器或父容器,web容器叫作源容器或子容器。一个接收容器可以设置多个源容器,一个源容器也可以有多个接收容器。那么,link究竟做了什么呢? Docker将连接信息以下面两种方式保存在接收容器中。 - 设置接收容器的环境变量 - 更新接收容器的 ``/etc/hosts`` 文件 设置接收容器的环境变量 -------------------------- 当两个容器通过--link建立了连接后,会在接收容器中额外设置一些环境变量,以保存源容器的一些信息。这些环境变量包含以下几个方面。 - 每有一个源容器,接收容器就会设置一个名为 ``_NAME`` 环境变量,alias为源容器的别名,如上面例子的web容器中会有一个 ``WEBSERVER_NAME=/client/webserver`` 的环境变量。 - 接收容器同样会为源容器中暴露的端口设置环境变量。如web容器的IP为 ``172.17.0.2`` ,且暴露了80的tcp端口,则在client容器中会看到如下环境变量。其中,前4个环境变量会为每一个暴露的端口设置,而最后一个则是所有暴露端口中最小的一个端口的URL(若最小的端口在TCP和UDP上都使用了,则TCP优先)。 .. code:: bash WEBSERVER_PORT_80_TCP_ADDR=172.17.0.2 WEBSERVER_PORT_80_TCP_PORT=80 WEBSERVER_PORT_80_TCP_PROTO=tcp WEBSERVER_PORT_80_TCP=tcp://172.17.0.2:80 WEBSERVER_PORT=tcp://172.17.0.2:80 从上面的示例中,看到 ``--link`` 是 ``docker run`` 命令的参数,也就是说link是在启动容器的过程中创建的。因此,回到容器的启动过程中, 去看看link是如何完成以上环境变量的设置的。我们发现在容器启动过程中(``daemon/start.go`` 中的containerStart函数) 需要调用 ``setupLinkedCon-tainers`` 函数,发现这个函数最终返回的是env变量,这个变量中包含了由于link操作, 所需要额外为启动容器创建的所有环境变量,其执行过程如下。 - 找到要启动容器的所有子容器,即所有连接到的源容器。 - 遍历所有源容器,将link信息记录起来。 - 将link相关的环境变量(包括当前容器和源容器的IP、源容器的名称和别称、源容器中设置的环境变量以及源容器暴露的端口信息)放入到env中,最后将env变量返回。 - 若以上过程中出现错误,则取消做过的修改。 .. WARNING:: 在传统的link方式中,要求当前容器和所有的源容器都必须在默认网络中。 更新接收容器的/etc/hosts文件 ----------------------------- Docker容器的IP地址是不固定的,容器重启后IP地址可能就和之前不同了。在有link关系的两个容器中,虽然接收方容器中包含有源容器IP的环境变量,但是如果源容器重启,接收方容器中的环境变量不会自动更新。这些环境变量主要是为容器中的第一个进程所设置的,如sshd等守护进程。因此,link操作除了在将link信息保存在接收容器中之外,还在/etc/hosts中添加了一项---源容器的IP和别名(——link参数中指定的别名),以用来解析源容器的IP地址。并且当源容器重启后,会自动更新接收容器的/etc/hosts文件。需要注意的是这里仍然用的是别名,而不是源容器的主机名(实际上,主机名对外界是不可见的)。因此,可以用这个别名来配置应用程序,而不需要担心IP的变化。 配置hosts文件分为两步,一是当前sandbox(对应当前容器)的hosts文件,先找到接收容器(将要启动的容器)的所有源容器, 然后将源容器的别名和IP地址添加到接收容器的 ``/etc/hosts`` 文件中;二是更新所有父sandbox(也就是接收容器对应的sandbox)的hosts文 件,将源容器的别名和IP地址添加到接收容器的 ``/etc/hosts`` 文件中。 这样,当一个容器重启以后,自身的hosts文件和以自己为源容器的接收容器的hosts文件都会更新,保证了link系统的正常工作。 建立iptables规则进行通信 -------------------------- 在接收容器上设置了环境变量和更改了 ``/etc/hosts`` 文件之后,接收容器仅仅是得到了源容器的相关信息(环境变量、IP地址),并不代表源容器和接收容器在网络上可以互相通信。当用户为了安全起见,将Docker daemon的 ``--icc`` 参数设置为false时,容器间的通信就被禁止了。那么,Docker daemon如何保证两个容器间的通信呢? 答案是为连接的容器添加特定的iptables规则。 Link是一种比端口映射更亲密的Docker容器间通信方式,提供了更安全、高效的服务,通过环境变量和 ``/etc/hosts`` 文件的设置提供了从别名到具体通信地址的发现,适合于一些需要各组件间通信的应用。 新的link介绍 ================ 相比于传统的link系统提供的名字和别名的解析、容器间网络隔离( ``--icc=false`` )以及环境变量的注入, Docker v1.9后为用户自定义网络提供了DNS自动名字解析、同一个网络中容器间的隔离、可以动态加入或者退出多个网络、 支持 ``--link`` 为源容器设定别名等服务。在使用上,可以说除了环境变量的注入,新的网络模型给用户提供了更便捷和更自然的使用方式而 不影响原有的使用习惯。 在新的网络模型中,link系统只是在当前网络给源容器起了一个别名,并且这个别名只对接收容器有效。 新旧link系统的另一个重要的区别是新的link系统在创建一个link时并不要求源容器已经创建或者启动。 比如我们使用bridge驱动创建一个自定义网络isolated_nw,再运行一个容器container1加入该网络并链接另一个容器container2, 虽然container2还并不存在,如下代码所示。 .. code-block:: bash :linenos: :caption: new link demo :name: new link demo :emphasize-lines: 10 $ docker network create isolated_nw 721faded37d4cdc60ff518b3c1509d233c794e298f2b4f284a00e60dd6022437 $ docker run --net=isolated_nw -itd --rm --name=container1 --link container2:c2 busybox cb983f6f3ff2759a46dc45b4fbe8fe75ace446039b33cd8142f66cf2e4151b90 $ docker run --net isolated_nw --name container2 -itd --rm busybox 86ed914ccedddb69bea813013e7c05413706e79394a4eec05435a775eac9d42d $ docker exec -it container1 ping c2 PING c2 (172.20.0.3): 56 data bytes 64 bytes from 172.20.0.3: seq=0 ttl=64 time=0.106 ms 64 bytes from 172.20.0.3: seq=1 ttl=64 time=0.110 ms ^C --- c2 ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.106/0.108/0.110 ms 可以看到,创建并启动完container2后,就可以在container1里面pingcontainer2的别名c2了(同样也可以ping名字和ID) .. code:: bash $ docker exec -it container1 cat /etc/hosts 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.20.0.2 cb983f6f3ff2 在查看 ``/etc/hosts`` 文件后,发现里面并没有container2的相关信息,这表示新的link系统的实现与原来的配置hosts文件的方式并不相同。实际上,Docker是通过DNS解析的方式提供名字和别名的解析,这很好地解决了在传统link系统中由于容器重启造成注入的环境变量更新不及时的问题。在新的link系统下,用户甚至可以实现一对容器之间相互link。 .. include:: ../../comment.rst