第3章 网络问题
3.1 怎么固定容器 IP 地址?每次重启容器都要变化 IP 地址怎么办?
一般情况是不需要指定容器 IP 地址的。这不是虚拟主机,而是容器。其地址是供容器间通讯的,容器间则不用 IP 直接通讯,而使用容器名、服务名、网络别名。
为了保持向后兼容,docker run 在不指定 --network 时,所在的网络是 default bridge,在这个网络下,需要使用 --link 参数才可以让两个容器找到对方。
这是有局限性的,因为这个时候使用的是 /etc/hosts 静态文件来进行的解析,比如一个主机挂了后,重新启动IP可能会改变。虽然说这种改变Docker是可能更新/etc/hosts文件,但是这有诸多问题,可能会因为竞争冒险导致 /etc/hosts 文件损毁,也可能还在运行的容器在取得 /etc/hosts 的解析结果后,不再去监视该文件是否变动。种种原因都可能会导致旧的主机无法通过容器名访问到新的主机。
参考官网文档:https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
如果可能不要使用这种过时的方式,而是用下面说的自定义网络的方式。
而对于新的环境(Docker 1.10以上),应该给容器建立自定义网络,同一个自定义网络中,可以使用对方容器的容器名、服务名、网络别名来找到对方。这个时候帮助进行服务发现的是Docker 内置的DNS。所以,无论容器是否重启、更换IP,内置的DNS都能正确指定到对方的位置。
参考官网文档:https://docs.docker.com/engine/userguide/networking/work-with-networks/#linking-containers-in-user-defined-networks
建议参考一下我写的 LNMP 的例子:
https://coding.net/u/twang2218/p/docker-lnmp/git
3.2 如何修改容器的 /etc/hosts 文件?
容器内的 /etc/hosts 文件不应该被随意修改,如果必须添加主机名和 IP 地址映射关系,应该在 docker run 时使用 --add-host 参数,或者在 docker-compose.yml 中添加 extra_hosts 项。
不过在用之前,应该再考虑一下真的需要修改 /etc/hosts 么?如果只是为了容器间互相访问,应该建立自定义网络,并使用 Docker 内置的 DNS 服务。
可以参考一下我写的这个 LNMP 多容器互连的例子:https://coding.net/u/twang2218/p/docker-lnmp/git
3.3 怎么映射宿主端口?Dockerfile 中的EXPOSE和 docker run -p 有啥区别?
Docker中有两个概念,一个叫做 EXPOSE ,一个叫做 PUBLISH 。
EXPOSE 是镜像/容器声明要暴露该端口,可以供其他容器使用。这种声明,在没有设定 --icc=false的时候,实际上只是一种标注,并不强制。也就是说,没有声明 EXPOSE 的端口,其它容器也可以访问。但是当强制 --icc=false 的时候,那么只有 EXPOSE 的端口,其它容器才可以访问。
PUBLISH 则是通过映射宿主端口,将容器的端口公开于外界,也就是说宿主之外的机器,可以通过访问宿主IP及对应的该映射端口,访问到容器对应端口,从而使用容器服务。
EXPOSE 的端口可以不 PUBLISH,这样只有容器间可以访问,宿主之外无法访问。而 PUBLISH 的端口,可以不事先 EXPOSE,换句话说 PUBLISH 等于同时隐式定义了该端口要 EXPOSE。
docker run 命令中的 -p, -P 参数,以及 docker-compose.yml 中的 ports 部分,实际上均是指 PUBLISH。
小写 -p 是端口映射,格式为 [宿主IP:]<宿主端口>:<容器端口>,其中宿主端口和容器端口,既可以是一个数字,也可以是一个范围,比如:1000-2000:1000-2000。对于多宿主的机器,可以指定宿主IP,不指定宿主IP时,守护所有接口。
大写 -P 则是自动映射,将所有定义 EXPOSE 的端口,随机映射到宿主的某个端口。
3.4 我要映射好几百个端口,难道要一个个 -p 么?
-p 是可以用范围的:
-p 8001-8010:8001-8010
3.5 为什么 -p 后还是无法通过映射端口访问容器里面的服务?
首先,当然是检查这个 docker 的容器是否启动正常: docker ps、docker top <容器ID>、docker logs <容器ID>、docker exec -it <容器ID> bash等,
这是比较常用的排障的命令;如果是 docker-compose 也有其对应的这一组命令,所以排障很容易。
如果确保服务一切正常,甚至在容器里,可以访问到这些服务,docker ps 也显示出了端口映射成功,那么就需要检查防火墙了。
本机防火墙
在 Docker 运行的系统上不应该运行任何防火墙……没错,说你呢,CentOS 的 firewalld 和 Ubuntu 的 ufw 同学。由于 Docker 使用 iptables 规则来进行网络数据流的控制,而那些防火墙总以为只有自己撰写 iptables,从而经常会导致 Docker 设置了一些规则,然后转眼就被 firewalld 或 ufw 给清了,特别是起、停防火墙服务的时候。从而导致 Docker 的网络从外界无法访问。
为了避免 iptables 的规则干扰,不要在运行 Docker 的服务器上,运行任何防火墙或配置自定义的 iptables 规则,除非你非常清楚你在做什么,并且知道会产生什么后果。另外,关闭防火墙后,记得重启系统,至少是重启 Docker 服务。否则防火墙的起、停、刷新这类行为会导致清空 Docker 设置的网络规则,而导致容器内的网络无法和外部互联。
边界防火墙
如果你使用的是云服务器,那么除了本机防火墙外,云服务的服务商一般会提供边界防火墙服务,比如安全组、安全策略类的东西。有些服务器为了安全起见,默认只开通必需的 22 端口给 SSH 使用,而其它端口屏蔽。这也是可能导致远程访问服务器 -p 端口失败的原因之一。如果你发现你在服务器本地访问服务,比如 curl localhost 没有阻碍,但是远程访问该服务就连接失败的话,那么应该去检查云服务商的安全设置,是否忘记了开启所需的端口。
3.6 vethxxxx 这种虚拟网卡和容器的对应关系从哪里看?
某网友提供了一个好办法。
$ docker network ls NETWORK ID NAME DRIVER 56f04389b8f0 dockerlnmp_backend bridge 094fcb269385 dockerlnmp_frontend bridge
注意这里的 NETWORK ID,然后运行 ip a | grep veth。
$ ip a | grep veth 12: veth22996d2@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-56f04389b8f0 state UP group default 14: veth34ace9a@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-56f04389b8f0 state UP group default 16: veth0bb3771@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-56f04389b8f0 state UP group default 22: veth399b874@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-094fcb269385 state UP group default 24: vethf24a0a9@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-094fcb269385 state UP group default
注意这里的 br-56f04389b8f0 以及 br-094fcb269385,br- 后面的是上面的网络id,由此可以看出 veth 和 Docker 网络的对应关系,而容器都是连接到了某个Docker网络上的,从而就有了容器和 veth 的对应关系。
对于某个网络出现了多个 veth 的情况,可以观察 veth22996d2@if11 后面的 if11 这部分,和容器内的 ip addr 的结果,一般 奇-偶是一对。
3.7 如何让一个容器连接两个网络?
如果是使用 docker run,那很不幸,一次只可以连接一个网络,因为 docker run 的 --network 参数只可以出现一次(如果出现多次,最后的会覆盖之前的)。不过容器运行后,可以用命令 docker network connect 连接多个网络。
假设我们创建了两个网络:
$ docker network create mynet1
$ docker network create mynet2
然后,我们运行容器,并连接这两个网络。
$ docker run -d --name web --network mynet1 nginx
$ docker network connect mynet2 web
但是如果使用 docker-compose 那就没这个问题了。因为实际上,Docker Remote API 是支持一次性指定多个网络的,但是估计是命令行上不方便,所以 docker run 限定为只可以一次连一个。docker-compose 直接就可以将服务的容器连入多个网络,没有问题。
version: '2' services: web: image: nginx networks: - mynet1 - mynet2 networks: mynet1: mynet2:
3.8 Docker 多宿主网络怎么配置?
Docker 跨节点容器网络互联,最通用的是使用 overlay 网络。
一代 Swarm 已经不再使用,它要求使用 overlay 网络前先准备好分布式键值库,比如 etcd, consul 或 zookeeper。然后在每个节点的 Docker 引擎中,配置 --cluster-store 和 --cluster-advertise 参数。这样才可以互连。可以参考我写的 LNMP 容器互联例子中的 run1.sh 这个脚本,这个脚本是利用 docker-machine自动建立 Swarm 并且配置好 overlay 的脚本,可以分析其流程。
现在都在使用二代 Swarm,也就是 Docker Swarm Mode,非常简单,只要 docker swarm init 建立集群,其它节点 docker swarm join 加入集群后,集群内的服务就自动建立了 overlay 网络互联能力。
需要注意的是,如果是多网卡环境,无论是 docker swarm init 还是 docker swarm join,都不要忘记使用参数 --advertise-addr 指定宣告地址,否则自动选择的地址很可能不是你期望的,从而导致集群互联失败。
格式为 --advertise-addr <地址>:<端口>,地址可以是 IP 地址,也可以是网卡接口,比如 eth0。端口默认为 2377,如果不改动可以忽略。
此外,这是供服务使用的 overlay,因此所有 docker service create 的服务容器可以使用该网络,而 docker run 不可以使用该网络,除非明确该网络为 --attachable。
关于 overlay 网络的进一步信息,可以参考官网文档:https://docs.docker.com/engine/userguide/networking/get-started-overlay/
虽然默认使用的是 overlay 网络,但这并不是唯一的多宿主互联方案。Docker 内置了一些其它的互联方案,比如效率比较高的 macvlan。如果在局域网络环境下,对 overlay 的额外开销不满意,那么可以考虑 macvlan 以及 ipvlan,这是比较好的方案。
https://docs.docker.com/engine/userguide/networking/get-started-macvlan/
此外,还有很多第三方的网络可以用来进行跨宿主互联,可以访问官网对应文档进一步查看:https://docs.docker.com/engine/extend/legacy_plugins/#/network-plugins
3.9 docker network ls 中看到了建立的 overlay 网络,为何docker run 网络不存在?
如果在 docker network ls 中看到了如下的 overlay 网络:
NETWORK ID NAME DRIVER SCOPE
...
24pz359114y0 mynet overlay swarm
...
那么这个名为 mynet 的网络是不可以连接到 docker run 的容器。如果试图连接则会出现报错。
如果是 1.12 的系统,会看到这样报错信息:
$ docker run --rm --network mynet busybox docker: Error response from daemon: network mynet not found. See 'docker run --help'.
报错说 mynet 网络找不到。其实如果仔细观察,会看到这个名为 mynet 的网络,驱动是 overlay 没有错,但它的 Scope 是 swarm。这个意思是说这个网络是在二代 Swarm 环境中建立的 overlay 网络,因此只可以由 Swarm 环境下的服务容器才可以使用。而 docker run 所运行的只是零散的容器,并非 Service,因此自然在零散容器所能使用的网络中,不存在叫 mynet 网络。
docker run 可以使用的 overlay 网络是 Scope 为 global 的 overlay 网络,也就是使用外置键值库所建立的 overlay 网络,比如一代 Swarm 的 overlay 网络。
这点在 1.13 后稍有变化。如果是 1.13 以后的系统,会看到这样的信息:
$ docker run --rm --network mynet busybox docker: Error response from daemon: Could not attach to network mynet: rpc error: code = 7 desc = network mynet not manually attachable.
报错信息不再说网络找不到,而是说这个 mynet 网络无法连接。这是由于从 1.13 开始,允许在建立网络的时候声明这个网络是否可以被零散的容器所连接。如果 docker network create 加了 --attachable 的参数,那么在后期,这个网络是可以被普通容器所连接的。
但是这是在安全模型上开了一个口子,因此,默认不允许普通容器链接,并且不建议使用。
3.10 使用Swarm Mode时,看到有个叫 ingress的 overlay网络,它和自己创建的网络有什么区别?
在启用了二代 Swarm 后,可能会在网络列表时看到一个名为 ingress 的 overlay 网络。
$ docker network ls NETWORK ID NAME DRIVER SCOPE 6beb824623a4 bridge bridge local f3f636574c7a docker_gwbridge bridge local cfeb2513a4a3 host host local 88smbt683r5p ingress overlay swarm 24pz359114y0 mynet overlay swarm d35d69ece740 none null local
这里可以看到两个 overlay 网络,其中一个是我们创建的 mynet,另一个则是 Docker 引擎自己创建的 ingress,从驱动和 Scope 可以看出两个网络都是给 Swarm Mode 使用的 overlay 网络。
ingress 是 overlay 网络,但并不是普通的 overlay network,它是为边界进入流量特殊准备的网络。这个网络存在于集群中每一个Docker宿主上,不需要额外建立。
当我们使用 docker service create -p 80:80 这种形式创建一个服务的时候,我们要求映射集群端口 80 到服务容器的 80 端口上。其效果是访问任一节点的 80 端口,即使这个节点没有运行我们所需的容器,依旧可以连接到容器服务,并且取得结果。实现这样效果的一个原因就是因为 ingress 网络的存在。
Swarm 中的每个节点,都会有一个隐藏的沙箱容器监听宿主的服务端口,用于接收来自集群外界的访问。
我们可以通过 docker network inspect ingress 来看到这个沙箱容器:
$ docker network inspect ingress ##。。。。这个格式没法复制过来了。就这么看吧。。。 [ { "Name": "ingress", "Id": "88smbt683r5p7c0l7sd0dpniw", "Scope": "swarm", "Driver": "overlay", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "10.255.0.0/16", "Gateway": "10.255.0.1" } ] }, "Internal": false, "Containers": { "faff08692b5f916fcb15aa7ac6bc8633a0fa714a52a1fb75e57525c94581c45a": { "Name": "web.1.1jyunyva6picwsztzrj6t2cio", "EndpointID": "58240770eb25565b472384731b1b90e36141a633ce184a5163829cf96e9d1195", "MacAddress": "02:42:0a:ff:00:05", "IPv4Address": "10.255.0.5/16", "IPv6Address": "" }, "ingress-sbox": { "Name": "ingress-endpoint", "EndpointID": "fe8f89d4f99d7bacb14c5cb723682c180278d62e9edd10b523cdd81a45695c5d", "MacAddress": "02:42:0a:ff:00:03", "IPv4Address": "10.255.0.3/16", "IPv6Address": "" } }, "Options": { "com.docker.network.driver.overlay.vxlanid_list": "256" }, "Labels": {} } ]
在上面的命令返回信息中,我们可以看到一个名为 ingress-endpoint 的容器,这就是边界沙箱容器。
当我们创建服务时,使用了 -p 参数后,服务容器就会被自动的加入到 ingress 网络中,同时会在沙箱中注册映射信息,告知哪个服务要求守护哪个端口,具体对应容器是哪些。
因此当沙箱收到外部连接后,通过访问端口就可以知道具体服务在守护,然后会通过这个 ingress 网络去将连接请求转发给对应服务容器。而由于 ingress 的本质是 overlay network,因此,无论服务容器运行于哪个节点上,沙箱都可以成功的将连接转发给正确的服务容器。
所以,ingress 是特殊用途的网络,只要服务有 -p 选项,那么服务容器就会自动被加入该网络。因此把 ingress 网络当做普通的 overlay 网络使用的话,除了会干扰 Swarm 正常的边界负载均衡的能力,也会破坏服务隔离的安全机制。所以不要把这个网络当做普通的 overlay 网络来使用,需要控制服务互联和隔离时,请用自行创建的 overlay 网络。
3.11 听说--link过时不再用了?那容器互联、服务发现怎么办?
在 1-2 年前,Docker 所有容器都连接于默认的桥接网络上,也就是很多老文章鼓捣的 docker0 桥接网卡。因此实际上默认情况下所有容器都是可以互联的,没有隔离,当然这样安全性不好。而服务发现,是在这种环境下发展出来的,通过修改容器内的 /etc/hosts 文件来完成的。凡是 --link 的主机的别名就会出现于 /etc/hosts 中,其地址由 Docker 引擎维护。因此容器间才可以通过别名互访。
但是这种办法并不是好的解决方案,Docker 早在一年多以前就已经使用自定义网络了。在同一个网络中的容器,可以互联,并且,Docker 内置了 DNS,容器内的应用可以使用服务名、容器名、别名来进行服务发现,名称会经由内置的 DNS 进行解析,其结果是动态的;而不在同一网络中的容器,不可以互联。
因此,现在早就不用 --link 了,而且非常不建议使用。
首先是因为使用 --link 就很可能还在用默认桥接网络,这很不安全,所有容器都没有适度隔离,用自定义网络才比较方便互联隔离。
其次,修改 /etc/hosts 文件有很多弊病。比如,高频繁的容器启停环境时,容易产生竞争冒险,导致 /etc/hosts 文件损坏,出现访问故障;或者有些应用发现是来自于 /etc/hosts 文件后,就假定其为静态文件,而缓存结果不再查询,从而导致容器启停 IP 变更后,使用旧的条目而无法连接到正确的容器等等。
另外,在一代 Swarm 环境中,在 docker-compose.yml 中使用了 links 就意味着服务间的强依赖关系,因此调度时不会将服务运行于不同节点,而是全部运行于一个节点,使得横向扩展失败。
所以不要再使用 --link 以及 docker-compose.yml 中的 links 了。应该使用 docker network,建立网络,而 docker run --network 来连接特定网络。或者使用 version: '2' 的 docker-compose.yml 直接定义自定义网络并使用。
建议去看一下我写的 LNMP 多容器互联的例子:https://coding.net/u/twang2218/p/docker-lnmp/git
3.12 使用HBase/Hadoop的时候,反向解析总是不对,怎么办?
Hadoop/HBase 这类东西总喜欢根据设定的名称正向的解析一遍,然后在某个时候会反向的解析一遍检查是否一致。这种默认假定很多时候会出问题,特别是对于使用 /etc/hosts 的时候。正向解析会从 /etc/hosts 中取得,而反向解析则更可能走 DNS,于是出现了不一致。
对于 Docker 而言,使用自定义网络后,一个容器有很多个名字,内置 DNS 可以根据服务名、容器名、网络别名、<容器名>.<网络名> 等来进行解析。因此正向解析设置任何一个,其结果都会指向容器的 IP。
而反向解析则不会返回所有结果,而只返回<容器名>.<网络名>。
所以当有人这样运行容器的时候:
$ docker run -it --rm \ --name wombat.example.com \ --hostname wombat.example.com \ --network net1 \ m3adow/nettools
会发现反向解析结果并非自己所期望的:
/ # ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 50: eth0@if51: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff inet 172.19.0.2/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe13:2/64 scope link valid_lft forever preferred_lft forever / # dig +short wombat.example.com 172.19.0.2 / # host 172.19.0.2 2.0.19.172.in-addr.arpa domain name pointer wombat.example.com.net1.
从上面的解析结果可以看出来,由 wombat.example.com 正向解析的话,其结果是 172.19.0.2,确实是我们的 IP 地址;但是由 172.19.0.2 反向解析的话,所得到的域名确实 wombat.example.com.net1。多了一个 .net1 的尾巴。从而导致 HBase/Hadoop 这类软件出现故障。
解决办法很简单,我们现在知道反向域名解析的格式为 <容器名>.<网络名>。那么我们只需要将网络名设为域名就可以了。
$ docker network create example.com $ docker run -it --rm \ --name wombat \ --hostname wombat.example.com \ --network example.com \ m3adow/nettools / # ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 52: eth0@if53: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:15:00:02 brd ff:ff:ff:ff:ff:ff inet 172.21.0.2/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe15:2/64 scope link valid_lft forever preferred_lft forever / # dig +short wombat.example.com 172.21.0.2 / # host 172.21.0.2 2.0.21.172.in-addr.arpa domain name pointer wombat.example.com.
这里看到,正向解析没问题,反向解析也得到了 wombat.example.com 这个所期望的结果。
需要注意的是,服务名、主机名、容器名这类可用于服务发现的名称,应该尽量使用 非 FQDN,也就是不包含 . 的单一名字,否则在某些情况下会出错。
3.13 容器怎么取宿主机IP啊?
单机环境
如果是单机环境,很简单,不必琢磨怎么突破命名空间限制,直接用环境变量送进去即可。
docker run -d -e HOST_IP=<宿主的IP地址> nginx
然后容器内直接读取 HOST_IP 环境变量即可。
集群环境
集群环境相对比较复杂,docker service create 中的 -e 以及 --env-file是在服务创建时指定、读取环境变量内容,而不是运行时,因此对于每个节点都是一样的。而且目前不存在 dockerd -e 选项,所以直接使用这些选项达不到我们想要的效果。
不过有变通的办法,可以在宿主上建立一个 /etc/variables 文件(名字随意,这里用这个文件举例)。其内容为:
HOST_IP=1.2.3.4
其中 1.2.3.4 是这个节点的宿主 IP,因此每个节点的 /etc/variables 的内容不同。
而在启动服务时,指定挂载这个服务端本地文件:
docker service create --name app \ --mount type=bind,source=/etc/variables,target=/etc/variables:ro \ myapp
由于 --mount 是发生于容器运行时,因此所加载的是所运行的服务器的 /etc/variables,里面所包含的也是该服务器的 IP 地址。
在 myapp 这个镜像的入口脚本加入加载该环境变量文件的命令:
source /etc/variables
这样 app 这个服务容器就会拥有 HOST_IP 环境变量,其值为所运行的宿主 IP。