Docker的本地网络实现其实就是利用了Linux上的网络命名空间和虚拟网络设备(特别是veth pair)。
基本原理
直观上看,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)与外界相通,并可以收发数据包;此外,如果不同子网之间要进行通信,需要额外的路由机制。
Docker中的网络接口默认都是虚拟的接口。虚拟接口的最大优势就是转发效率极高。这是因为Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无需通过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并无区别,只是它速度要快得多。
Docker容器网络就很好地利用了Linux虚拟网络技术,在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做veth pair)。
一般情况下,Docker创建一个容器的时候,会具体执行如下操作:
1.创建一对虚拟接口,分别放到本地主机和新容器的命名空间中;
2.本地主机一端的虚拟接口连接到默认的docker0网桥或指定网桥上,并具有一个以veth开头的唯一名字,如veth1234;
3.容器一端的虚拟接口将放到新创建的容器中,并修改名字作为eth0。这个接口只在容器的命名空间可见;
4.从网桥可用地址段中获取一个空闲地址分配给容器的eth0(例如172.17.0.2/16),并配置默认路由网关为docker0网卡的内部接口docker0的IP地址(例如172.17.42.1/16)。
完成这些之后,容器就可以使用它所能看到的eth0虚拟网卡来连接其他容器和访问外部网络。用户也可以通过docker network命令来手动管理网络。
docker网络模式
安装Docker时,它会自动创建三个网络,bridge(创建容器默认连接到此网络,也就是在不使用--network参数时)、 none 、host。还有以后一种自定义模式,自定义模式有三种:bridge、overlay、macvlan。
host:容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。
Container:创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围。
None:该模式关闭了容器的网络功能。
Bridge:此模式会为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,通过docker0网桥以及Iptables nat表配置与宿主机通信。
通过docker network ls可以查看docker网络:
当容器运行分别以none、host、bridge这三种模式的时候如下:
-
docker none network
-
docker host network
-
docker bridge network
bridge用法也就是容器默认使用,也就是上面基本原理所讲到的。
-
自定义bridge模式
创建network
# docker network create -d bridge --ip-range=192.168.1.0/24 --gateway=192.168.1.1 --subnet=192.168.1.0/24 bridge2
# docker network ls
创建两个容器指定ip并指定network
# docker run -it --network=bridge2 --ip=192.168.1.3 busybox
# docker run -it --network=bridge2 --ip=192.168.1.4 busybox
在使用docker run命令启动容器的时候,可以通过--net参数来指定容器的网络配置。
有5个可选值bridge、none、container、host和用户定义的网络:
--net=bridge:默认值,在Docker网桥docker0上为容器创建新的网络栈。
--net=none:让Docker将新容器放到隔离的网络栈中,但是不进行网络配置。之后,用户可以自行进行配置。
--net=container:NAME_or_ID:让Docker将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享IP地址和端口等网络资源,两者进程可以直接通过lo环回接口通信。
--net=host:告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络。此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。容器进程可以跟主机其他root进程一样打开低范围的端口,可以访问本地网络服务,比如D-bus,还可以让容器做一些影响整个主机系统的事情,比如重启主机。因此使用这个选项的时候要非常小心。如果进一步的使用--privileged=true参数,容器甚至会被允许直接配置主机的网络栈。
--net=user_defined_network:用户自行用network相关命令创建一个网络,通过这种方式将容器连接到指定的已创建网络上去。
网络参数
有些命令选项只有在Docker服务启动的时候才能配置,而且不能马上生效:
-b BRIDGE or--bridge=BRIDGE——指定容器挂载的网桥;
--bip=CIDR——定制docker0的掩码;
-H SOCKET...or--host=SOCKET...——Docker服务端接收命令的通道;
--icc=true|false——是否支持容器之间进行通信;
--ip-forward=true|false——启用net.ipv4.ip_forward,即打开转发功能;
--iptables=true|false——禁止Docker添加iptables规则;
--mtu=BYTES——容器网络中的MTU。
下面2个命令选项既可以在启动服务时指定,也可以docker run时候指定。在Docker服务启动的时候指定则会成为默认值,后续执行docker run时可以覆盖设置的默认值。
--dns=IP_ADDRESS...——使用指定的DNS服务器;
--dns-search=DOMAIN...——指定DNS搜索域。
最后这些选项只能在docker run执行时使用,因为它是针对容器的特性内容:
-h HOSTNAME or--hostname=HOSTNAME——配置容器主机名;
--link=CONTAINER_NAME:ALIAS——添加到另一个容器的连接;
--net=bridge|none|container:NAME_or_ID|host|user_defined_network——配置容器的桥接模式;
-p SPEC or--publish=SPEC——映射容器端口到宿主主机;
-P or--publish-all=true|false——映射容器所有端口到宿主主机。
配置容器DNS和主机名
Docker支持自定义容器的主机名和DNS配置。
1.相关配置文件
容器中主机名和DNS配置信息都是通过三个系统配置文件来维护的:/etc/resolv.conf、/etc/hostname和/etc/hosts。
启动一个容器,在容器中使用mount命令可以看到这三个文件挂载信息:
/etc/resolv.conf文件在创建容器时候,默认会与宿主机/etc/resolv.conf文件内容保持一致
/etc/hosts文件中默认只记录了容器自身的一些地址和名称:
/etc/hostname文件则记录了容器的主机名。
2.容器内修改配置文件
Docker 1.2.0开始支持在运行中的容器里直接编辑/etc/hosts,/etc/hostname和/etc/resolve.conf文件。但是这些修改是临时的,只在运行的容器中保留,容器终止或重启后并不会被保存下来。也不会被docker commit提交。
3.通过参数指定
如果用户想要自定义容器的配置,可以在创建或启动容器时利用下面的参数指定:
1)指定主机名-h HOSTNAME或者--hostname=HOSTNAME。设定容器的主机名,它会被写到容器内的/etc/hostname和/etc/hosts。但这个主机名只有容器内能看到,在容器外部则看不到,既不会在docker ps中显示,也不会在其他的容器的/etc/hosts看到。
2)记录其他容器主机名--link=CONTAINER_NAME:ALIAS。选项会在创建容器的时候,添加一个所连接容器的主机名到容器内/etc/hosts文件中。这样,新创建容器可以直接使用主机名来与所连接容器通信。
3)指定DNS服务器--dns=IP_ADDRESS。添加DNS服务器到容器的/etc/resolv.conf中,容器会用指定的服务器来解析所有不在/etc/hosts中的主机名。
4)指定DNS搜索域--dns-search=DOMAIN。设定容器的搜索域,当设定搜索域为.example.com时,在搜索一个名为host的主机时,DNS不仅搜索host,还会搜索host.example.com。
容器之间的通信
容器之间可通过 IP,Docker DNS Server 和joined 容器三种方式通信。
IP 通信
两个容器要能通信,必须要有属于同一个网络的网卡。满足这个条件后,容器就可以通过 IP 交互了。具体做法是在容器创建时通过 --network 指定相应的网络,或者通过 docker network connect 将现有容器加入到指定网络。
Docker DNS Server
通过 IP 访问容器虽然满足了通信的需求,但还是不够灵活。因为我们在部署应用之前可能无法确定 IP,部署之后再指定要访问的 IP 会比较麻烦。对于这个问题,可以通过 docker 自带的 DNS 服务解决。
从 Docker 1.10 版本开始,docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过"容器名"通信。方法很简单,只要在启动时用 --name 为容器命名就可以了。
下面启动两个容器 box1 和 box2,并且在上面定义的网络模式bridge2中:
docker run -it --network=bridge2 --name box1 busybox
docker run -it --network=bridge2 --name box2 busybox
box2和 box1可以互ping通
使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的。
joined 容器
joined 容器是另一种实现容器间通信的方式。它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过 127.0.0.1 直接通信。例:
先创建一个http容器,名字为 box1
docker run -it --name box1 http
然后创建 busybox 容器并通过 --network=container:box1 指定 jointed 容器为 box1:
docker run -it --network=container:box1 busybox
box1 的网络:
busybox 和 box1 的网卡 mac 地址与 IP 完全一样,它们共享了相同的网络栈。busybox 可以直接用 127.0.0.1 访问 box1 的 http 服务。
joined 容器非常适合以下场景:
不同容器中的程序希望通过 loopback 高效快速地通信,比如 web server 与 app server。
希望监控其他容器的网络流量,比如运行在独立容器中的网络监控程序。
容器访问控制
容器的访问控制主要通过Linux上的iptables防火墙软件来进行管理和实现。iptables是Linux系统流行的防火墙软件,在大部分发行版中都自带。
1.容器访问外部网络
我们知道容器默认指定了网关为docker0网桥上的docker0内部接口。docker0内部接口同时也是宿主机的一个本地接口。因此,容器默认情况下是可以访问到宿主机本地的。更进一步,容器要想通过宿主机访问到外部网络,需要宿主机进行转发。
如果为0,则没有开启转发,则需要手动打开:
# sysctl -w net.ipv4.ip_forward=1
更简单的,在启动Docker服务的时候设定--ip-forward=true,Docker服务会自动打开宿主机系统的转发服务。
2.外部访问容器实现
容器允许外部访问,可以在docker run时候通过-p或-P参数来启用。
不管用那种办法,其实也是在本地的iptable的nat表中添加相应的规则,将访问外部IP地址的网包进行目标地址DNAT,将目标地址修改为容器的IP地址。
以一个开放80端口的Web容器为例,使用-P时,会自动映射本地49000~49900范文内的端口随机端口到容器的80端口:
可以看到,nat表中涉及两条链,PREROUTING链负责包到达网络接口时,改写其目的地址。其中规则将所有流量都扔到DOCKER链。而DOCKER链中将所有不是从docker0进来的网包(意味着不是本地主机产生),将目标端口为49153的,修改目标地址为172.17.0.2,目标端口修改为80。
使用-p 80:80时,与上面类似,只是本地端口也为80:
有两点需要注意:
这里的规则映射了0.0.0.0,意味着将接受主机来自所有网络接口上的流量。用户可以通过-p IP:host_port:container_port或-p IP::port来指定绑定的外部网络接口,以制定更严格的访问规则;
如果希望映射永久绑定到某个固定的IP地址,可以在Docker配置文件/etc/default/docker中指定DOCKER_OPTS="--ip=IP_ADDRESS",之后重启Docker服务即可生效。
配置docker网桥
1.默认网桥
Docker服务默认会创建一个名称为docker0的Linux网桥(其上有一个docker0内部接口),它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。用户使用Docker创建多个自定义网络时可能会出现多个容器网桥。
Docker默认指定了docker0接口的IP地址和子网掩码,让主机和容器之间可以通过网桥相互通信,它还给出了MTU(接口允许接收的最大传输单元),通常是1500字节,或宿主主机网络路由上支持的默认值。这些值都可以在服务启动的时候进行配置:
--bip=CIDR——IP地址加掩码格式,例如192.168.1.5/24;
--mtu=BYTES——覆盖默认的Docker mtu配置。
也可以在配置文件中配置OPTIONS,然后重启服务。
由于目前Docker网桥是Linux网桥,用户可以使用brctl show来查看网桥和端口连接信息。
brctl命令如果系统中没有自带,可以使用 yum install bridge-utils安装(centos)
每次创建一个新容器的时候,Docker从可用的地址段中选择一个空闲的IP地址分配给容器的eth0端口。并且使用本地主机上docker0接口的IP作为容器的默认网关。
目前,Docker不支持在启动容器时候指定IP地址。实际上,Linux网桥自身功能已经十分完备,也可以替换为OpenvSwitch等功能更强大的网桥实现。
2.自定义网桥
在上述docker网络模式已经讲到,使用docker network即可实现。
3.使用OpenvSwitch网桥
Docker默认使用的是Linux自带的网桥实现,实际上,OpenvSwitch项目作为一个成熟的虚拟交换机实现,具备更丰富的功能。将来会有越来越多的容器支持OpenvSwitch作为底层网桥实现。
1.安装Docker(centos 7)
安装并启动服务。默认Docker服务会创建一个名为docker0的Linux网桥,作为连接容器的本地网桥。
2.安装OpenvSwitch
# yum install -y openvswitch
测试添加一个网桥br0并查看:
# ovs-vsctl add-br br0
# ovs-vsctl show
3.配置容器连接到OpenvSwitch网桥
目前OpenvSwitch网桥还不能直接支持挂载容器,需要手动在OpenvSwitch网桥上创建虚拟网口并挂载到容器中。
(1)创建无网口容器
启动一个容器,并指定不创建网络,后面我们手动添加网络。较新版本的Docker默认不允许在容器内修改网络配置,需要在run的时候指定参数--privileged=true:
记住这里容器的id。
(2)手动为容器添加网络
下载OpenvSwitch项目提供的支持Docker容器的辅助脚本ovs-docker:
# wget https://github.com/openvswitch/ovs/raw/master/utilities/ovs-docker
# chmod a+x ovs-docker
为容器添加网卡,并挂载到br0上,命令为:
#./ovs-docker add-port br0 eth0 00b9028e14aa --ipaddress=10.0.0.2/16
添加成功后,在容器内查看网络信息,多了一个新添加的网卡eth0,对应添加的IP地址。
在容器外,配置OpenvSwitch的网桥br0内部接口地址为10.0.1.2/16(只要与所挂载容器IP在同一个子网内即可):
# ifconfig br0 10.0.1.2/16
(3)测试连通
经过上面步骤,容器已经连接到了网桥br0上了,拓扑如下所示:
容器(10.0.7.2/16)<-->br0网桥<-->br0内部端口(10.0.1.2/16)
此时,在容器内就可以测试是否连通到网桥br0上了:
在容器内也可以配置默认网关为br0接口地址:
route add default gw GWIP
删除该接口的命令为:
# ./ovs-docker del-port br0 eth0<container_id>
删除名为br0的网桥:ovs-vsctl del-br br0
实际上,Docker社区也已经讨论对OpenvSwitch进行原生支持了。在Docker原生支持OpenvSwitch之前,用户可以通过编写脚本或更高级的工具来让这一过程自动化。