容器

Docker网络

1. 简介


容器网络实质上是由 Dokcer 为应用程序所创造的虚拟环境的一部分,它能让应用从宿主机操作系统的网络环境中独立出来,形成容器自有的网络设备、IP 协议栈、端口套接字、IP 路由表、防火墙等等与网络相关的模块。

Docker 为实现容器网络,主要采用的架构由三部分组成:CNM、Libnetwork 和驱动。

1.1 CNM


Container Network Model,它是 Docker 网络架构采用的设计规范。只要符合该模型的网络接口就能被用于容器之间通信,而通信的过程和细节可以完全由网络接口来实现。

CNM 的网络组成:

  • Sandbox: 提供容器的虚拟网络栈,即端口套接字,IP路由表、iptables配置,DNS等。用于隔离容器网络和宿主机网络
  • Network: Docker 虚拟网络,与宿主机网络隔离,只有参与者能够通信
  • Endpoint: 容器内的虚拟网络接口,负责与Docker虚拟网络连接

1.2 Libnetwork


CNM 的标准实现,由Golang开发,它实现了CNM定义的全部三个组件,还实现了本地服务发现,基于 Ingress 的容器负载均衡,及网络控制层和管理层功能

1.3 支撑技术


  • network namespace:用于隔离容器网络资源(IP、网卡、路由等)。netns可确保同一主机上的两个容器无法相互通信,甚至不能与主机本身进行通信,除非配置为通过docker网络进行通信。CNM网络驱动程序为每个容器实现单独的netns。但是,容器可以共享相同的netns,甚至可以是主机的netns的一部分。
  • veth pair:用于不同 netns 间进行通信。veth是全双工链接,在每个名称空间中都有一个接口,充当两个网络名称空间之间的连接线,负责将一个 netns 数据发往另一个 netns 的 veth。如当容器连接到docker网络时,veth的一端放置在容器内部(通常视为ethX接口),而另一端连接到Docker网络(vethxxx)。
  • iptables:包过滤,端口映射和负载均衡

1.4 Docker 网络实现


首先,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)来收发数据包;此外,如果不同子网之间要进行通信,需要路由机制。

Docker 中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。 Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中。对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,速度要快很多。

Docker 容器网络就利用了这项技术。它在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做 veth pair)。

Docker 创建一个容器的时候,会执行如下操作:

1.创建一对虚拟接口,分别放到本地主机和新容器中; 
2.本地主机一端桥接到默认的 docker0 或指定网桥上,并具有一个唯一的名字,如 veth65f9; 
3.容器一端放到新容器中,并修改名字作为 eth0,这个接口只在容器的名称空间可见; 从网桥可用地址段中获取一个空闲地址分配给容器的 eth0,并配置默认路由到桥接网卡 veth65f9。
4.完成这些之后,容器就可以使用 eth0 虚拟网卡来连接其他容器和其他网络。

可以在 docker run 的时候通过 –net 参数来指定容器的网络配置,有4个可选值:

1.--net=bridge:这个是默认值,连接到默认的网桥。
2.--net=host:告诉 Docker 不要将容器网络放到隔离的名字空间中,即不要容器化容器内的网络。此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。容器进程可以跟主机其 它 root 进程一样可以打开低范围的端口,可以访问本地网络服务比如 D-bus,还可以让容器做一些影响整个主机系统的事情,比如重启主机。因此使用这个选项的时候要非常小心。如果进一步的使用 --privileged=true,容器会被允许直接配置主机的网络堆栈。
3.--net=container:NAME_or_ID:让 Docker 将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享 IP 地址和端口等网络资源,两者进程可以直接通过 lo 环回接口通信。
4.--net=none:让 Docker 将新容器放到隔离的网络栈中,但是不进行网络配置。之后,用户可以自己进行配置。

2. 四种网络模式


Docker 安装后,会自动创建三个网络

2.1 bridge


bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看。bridge模式如下图所示:

当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器都会连接到这个虚拟网桥上。

1.创建虚拟网桥 docker0,新建的容器会自动桥接到该接口下,附加在其上的任何网卡之间都能自动转发数据包。
2.创建一对虚拟设备接口 veth pair,将其中一个接口设置为容器的 eth0 接口(容器网卡),另一个接口放置在宿主机命名空间中,以 vethxxx 这样的名字命名,宿主机上的3.所有容器都连接到这个内部网络上
4.分配一个和网桥 docker0在同网段的IP地址给容器,并设置 docker0 的 IP 地址为容器的默认网关

Docker 容器默认使用 bridge 模式的网络。其特点如下:

  • 使用一个 linux bridge,默认为 docker0。
  • 使用 veth 对,一头在容器的网络 namespace 中,一头在 docker0 上。
  • 该模式下Docker Container不具有一个公有IP,因为宿主机的IP地址与veth pair的 IP地址不在同一个网段内。
  • Docker采用 NAT 方式,将容器内部的服务监听的端口与宿主机的某一个端口port 进行“绑定”,使得宿主机以外的世界可以主动将网络报文发送至容器内部
  • 外界访问容器内的服务时,需要访问宿主机的 IP 以及宿主机的端口 port。
  • NAT 模式由于是在三层网络上的实现手段,故肯定会影响网络的传输效率。
  • 容器拥有独立、隔离的网络栈;让容器和宿主机以外的世界通过NAT建立通信

bridge模式是docker默认的。在这种模式下,docker为容器创建独立的网络栈,保证容器内的进程使用独立的网络环境,实现容器之间、容器与宿主机之间的网络栈隔离。同时,通过宿主机上的docker0网桥,容器可以与宿主机乃至外界进行网络通信。其网络模型可以参考下图:

从上面的网络模型可以看出,容器从原理上是可以与宿主机乃至外界的其他机器通信的。同一宿主机上,容器之间都是连接掉docker0这个网桥上的,它可以作为虚拟交换机使容器可以相互通信。然而,由于宿主机的IP地址与容器veth pair的 IP地址均不在同一个网段,故仅仅依靠veth pair和namespace的技术,还不足以使宿主机以外的网络主动发现容器的存在。为了使外界可以方位容器中的进程,docker采用了端口绑定的方式,也就是通过iptables的NAT,将宿主机上的端口端口流量转发到容器内的端口上。举一个简单的例子,使用下面的命令创建容器,并将宿主机的3306端口绑定到容器的3306端口:

# docker run -tid --name db -p 3306:3306 MySQL

在宿主机上,可以通过iptables -t nat -L -n,查到一条DNAT规则:

# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:3306 to:172.17.0.5:3306

上面的172.17.0.5即为bridge模式下,创建的容器IP。
很明显,bridge模式的容器与外界通信时,必定会占用宿主机上的端口,从而与宿主机竞争端口资源,对宿主机端口的管理会是一个比较大的问题。同时,由于容器与外界通信是基于三层上iptables NAT,性能和效率上的损耗是可以预见的。

2.2、Host 模式


如果启动容器时使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。

使用host模式的容器可以直接使用宿主机的IP地址与外界通信,容器内部的服务端口也可以使用宿主机的端口,host最大的优势就是网络性能比较好,但是docker host上已经使用的端口就不能再用了,网络的隔离性不好。

Host 模式并没有为容器创建一个隔离的网络环境。而之所以称之为host模式,是因为该模式下的 Docker 容器会和 host 宿主机共享同一个网络 namespace,故 Docker Container可以和宿主机一样,使用宿主机的eth0,实现和外界的通信。换言之,Docker Container的 IP 地址即为宿主机 eth0 的 IP 地址。其特点包括:

  • 这种模式下的容器没有隔离的 network namespace。
  • 容器的 IP 地址同 Docker host 的 IP 地址。
  • 需要注意容器中服务的端口号不能与 Docker host 上已经使用的端口号相冲突。
  • host 模式能够和其它模式共存。

总结:采用host模式的容器,可以直接使用宿主机的IP地址与外界进行通信,若宿主机具有公有IP,那么容器也拥有这个公有IP。同时容器内服务的端口也可以使用宿主机的端口,无需额外进行NAT转换,而且由于容器通信时,不再需要通过linuxbridge等方式转发或数据包的拆封,性能上有很大优势。当然,这种模式有优势,也就有劣势,主要包括以下几个方面:

  • 最明显的就是容器不再拥有隔离、独立的网络栈。容器会与宿主机竞争网络栈的使用,并且容器的崩溃就可能导致宿主机崩溃,在生产环境中,这种问题可能是不被允许的。
  • 容器内部将不再拥有所有的端口资源,因为一些端口已经被宿主机服务、bridge模式的容器端口绑定等其他服务占用掉了。

2.3、container 模式


其他网络模式是docker中一种较为特别的网络的模式。在这个模式下的容器,会使用其他容器的网络命名空间,其网络隔离性会处于bridge桥接模式与host模式之间。当容器共享其他容器的网络命名空间,则在这两个容器之间不存在网络隔离,而她们又与宿主机以及除此之外其他的容器存在网络隔离。其网络模型可以参考下图:

在这种模式下的容器可以通过localhost来同一网络命名空间下的其他容器,传输效率较高。而且这种模式还节约了一定数量的网络资源,但它并没有改变容器与外界通信的方式。在一些特殊的场景中非常有用,例如,kubernetes的pod,kubernetes为pod创建一个基础设施容器,同一pod下的其他容器都以其他容器模式共享这个基础设施容器的网络命名空间,相互之间以localhost访问,构成一个统一的整体。

2.4 none 模式


网络模式为 none,即不为 Docker 容器构造任何网络环境。一旦Docker 容器采用了none 网络模式,那么容器内部就只能使用loopback网络设备,不会再有其他的网络资源。Docker Container的none网络模式意味着不给该容器创建任何网络环境,容器只能使用127.0.0.1的本机网络。

在这种模式下,容器有独立的网络栈,但不包含任何网络配置,只具有lo这个loopback网卡用于进程通信。也就是说,none模式为容器做了最少的网络设置,但是俗话说得好“少即是多”,在没有网络配置的情况下,通过第三方工具或者手工的方式,开发这任意定制容器的网络,提供了最高的灵活性。

2.5. 暴露端口


同一个网络中的容器之间虽然可以互相 ping 通,但是并不意味着可以任意访问容器中的任何服务。Docker 为容器增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问。如下所示,我们可以通过

docker container ls

命令可以看到容器暴露给其他容器访问的端口是 80,那么我们只能容器的 80 端口进行访问,而不能对没有开放的 22 端口进行访问。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
5a8dece3841d        nginx               "/docker-entrypoint.…"   3 minutes ago       Up 3 minutes        80/tcp              web

$ telnet 172.18.0.2 80
Trying 172.18.0.2...
Connected to 172.18.0.2.
Escape character is '^]'.

$ telnet 172.18.0.2 20
Trying 172.18.0.2...
telnet: Unable to connect to remote host: Connection refused

我们可以在镜像创建的时候定义要暴露的端口,也可以在容器创建时定义要暴露的端口,使用 –expose。如下所示,就额外暴露了 20、22 这两个端口。

$ docker container run -d --name web --expose 22 --expose 20 nginx

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
4749dac32711        nginx               "/docker-entrypoint.…"   12 seconds ago      Up 10 seconds       20/tcp, 22/tcp, 80/tcp   web

容器的端口暴露类似于打开了容器的防火墙,具体能不能通过这个端口访问容器中的服务,还得看容器中有无应用监听并处理来自这个端口的请求。

2.6. 端口映射


上面提到的桥接网络中的容器只能与位于相同网络中的容器进行通信,假如一个容器想对外提供服务的话,需要进行端口映射。端口映射将容器的某个端口映射到 Docker 主机端口上。那么任何发送到该端口的流量,都会被转发到容器中。如图所示,容器内部开放端口为 80,该端口被映射到了 Docker 主机的 10.0.0.15 的 5000 端口上。最终访问 10.0.0.15:5000 的所有流量都会被转发到容器的 80 端口。

如下图所示,假设我们运行了一个新的 web 服务容器,并且将容器 80 端口映射到 Dokcer 主机的 5000 端口。

$ docker container run -d --name web --network localnet -p 5000:80 nginx

那么,当我们通过 web 浏览器访问 Docker 主机的 5000 端口时,会得到如图所示的结果。外部系统可以通过访问 Docker 主机的 TCP 端口 5000,来访问运行在桥接网络上的 Nginx 容器了。

端口映射之后,假如主机的 5000 端口被占用了,那么其他容器就不能再使用这个端口了。

2.7 相关命令


# 列出运行在本地 docker 主机上的全部网络
docker network ls

# 提供 Docker 网络的详细配置信息
docker network inspect <NETWORK_NAME>

# 创建新的单机桥接网络,名为 localnet,其中 -d 不指定的话,默认是 bridge 驱动。并且主机内核中也会创建一个新的网桥。
docker network create -d bridge localnet

# 删除 Docker 主机上指定的网络
docker network rm

# 删除主机上全部未使用的网络
docker network prune

# 运行一个新的容器,并且让这个容器加入 Docker 的 localnet 这个网络中
docker container run -d --name demo1 --network localnet alpine sleep 3600

# 运行一个新的容器,并且让这个容器暴露 22、20 两个端口
docker container run -d --name web --expose 22 --expose 20 nginx

# 运行一个新的容器,并且将这个容器的 80 端口映射到主机的 5000 端口
docker container run -d --name web --network localnet -p 5000:80 nginx

# 查看系统中的网桥
brctl show

3. docker通过容器访问宿主机器


通过容器里面访问宿主机ip发现是访问不通的,

当我们在容器中,想使用localhost访问宿主机,发现是访问不通的。

分析,在docker容器里面使用localhost访问的是docker容器的本地ip,而不是宿主机的。

方法1: 寻找docker0虚拟网桥的ip

因为容器默认是bridge桥接的,我们可以通过下面的命令来找docker0虚拟网桥的ip。

ip addr show docker0

docker0这个网卡上有IP信息

可以看到ip:172.17.0.1

容器上把localhost改成172.17.0.1,即可访问到宿主机上的服务

curl http://172.17.0.1:8883/test

方法2: 使用host.docker.internal 或者 gateway.docker.internal 来替换localhost

curl http://host.docker.internal:8000
curl http://gateway.docker.internal:8000

方法3:使用Host

还有一种是使用host,docker 容器运行有三种网络配置:host, bridge,none,默认是bridge, none表示容器无法使用网络,bridge 需要用-p 参数把端口映射出来。如果用host,即表示宿主机与容器共用网络,那么容器的localhost 就是 宿主机的localhost。

docker run -d --name my_docker --network host ubuntu:18

使用–network host 就不需要进行-p端口影射了。容器的地址就是宿主机的地址,端口也是一样。