docker 容器网络集成在 Libnetwork 库中,Libnetwork的目标是提供一个强大的容器网络模型CNM(Container Network Model),另一种CNI(Container Network Interface),为应用程序提供一致的编程接口和所需的网络抽象。这里主要讨论CNM模型,因为kuryr-Libnetwork就是这种模型的实现,kuryr-kubernetes是CNI的实现。
首先来看 Libnetwork 的网络模型:
用户可以创建一个或多个网络(一个网络就是一个网桥或者一个VLAN ),一个容器可以加入一个或多个网络。 同一个网络中容器可以通信,不同网络中的容器隔离。在创建容器之前,我们可以先创建网络(即创建容器与创建网络是分开的),然后决定让容器加入哪个网络。
上图涉及的几个概念:
- Sandbox:对应一个容器中的网络环境(没有实体),包括相应的网卡配置、路由表、DNS配置等。CNM很形象的将它表示为网络的『沙盒』,因为这样的网络环境是随着容器的创建而创建,又随着容器销毁而不复存在的;
- Endpoint:其用于连接sandbox与network,实际上就是一个容器中的虚拟网卡,veth pair 就是Endpoint的标准实现,在容器中会显示为eth0、eth1依次类推,veth接口挂载到名为docker0的网桥上。
- Network:指的是一个能够相互通信的容器网络,加入了同一个网络的容器直接可以直接通过对方的名字相互连接。它的实体本质上是主机上的虚拟网卡或网桥。
CNM的生命周期如下图:首先,将驱动 driver 注册到网络控制器 NetworkController 中,网络控制器NetworkController 使用驱动类型来创建网络,然后在创建的网络上创建接口(即Endpoint实现的veth pair),最后把容器连接到接口上即可。而销毁过程则正好相反,先把容器从接口上卸载,然后删除接入点和网络即可。
Libnetwork 支持以下几种网络:
-
none 模式
-
host 模式
-
bridge 模式
- container 模式
-
user-defined 模式
-
remote 模式
Docker 安装时会自动在 host 上创建三个网络,我们可用 docker network ls
命令查看:
|
1. none 网络
故名思议,none 网络就是什么都没有的网络。挂在这个网络下的容器除了 lo,没有其他任何网卡。例如需要用容器运行专门在本地计算文件hash值的应用,用来产生随机密码的应用。容器创建时,可以通过 --network=none
指定使用 none 网络。
比如:
|
2. host 网络
连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样,其实容器共用主机的namespace。可以通过 --network=host
指定使用 host 网络。
|
在容器中运行:
|
即可查看到挂载容器的网卡信息和容器主机的网卡信息一致,并且连 hostname 也是 host 的。
使用场景:
- 使用 host 网络好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。
- Docker host 的另一个用途是让容器可以直接配置 host 网路。比如某些跨 host 的网络解决方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。
也有不足之处:
- 容器隔离性弱化,没有独立的网络栈。
- 容器与主机竞争网络资源,比如要考虑端口冲突问题,Docker host 上已经使用的端口及其他容器映射到主机的端口,容器就不能再用了。
3. bridge网络
Docker 安装时会创建一个命名为 docker0 的 linux bridge。如果不指定 --network,创建的容器默认都会挂到 docker0 的网络上。当我们使用默认的网络配置来创建容器的时候,会在 docker0 上挂一个虚拟的网络接口。新创建的容器会被自动分配一个特定网段的IP地址。
为了使用更加灵活,用户可以创建自己的bridge网络来提供给容器使用。
创建bridge网桥的方法如下:
|
1.当没有容器启动时,查看 docker0 网络挂载的网卡:
|
此时docker0 网桥上没有挂载网络接口。
2. 当启动一个或多个网络是 bridge 的容器后
|
可以看到 docker0 上挂载了以 veth开头的网络接口,veth21573f8和 vethbbf81f5就是新创建的容器的虚拟网卡。
3. 进入容器,查看网卡:
|
可以看到容器的网卡 eth0@if35 ,而且地址是172.17.0.2。
- 那么结合以上两点,当网络默认是bridge的容器被创建时会同时创建一对网络接口 veth pair,即eth0 类型和 veth 类型。以eth0 开头类型接入容器中,以 veth 开头类型接入docker0 上。
4. 探寻容器IP地址来源
通过 docker network inspect bridge
看一下 bridge 网络的配置信息:
|
可以看到 bridge 网络配置的 "Subnet": "172.17.0.0/16" ,网关"Gateway": "172.17.0.1"。
5. 探寻网关
|
可以看到,网关172.17.0.1 就是docker0 。
此时的容器网络拓扑如图:
4. container模式
创建容器时使用 --network=container:NAME_or_ID 这个模式在创建新的容器的时候指定容器的网络和一个已经存在的容器共享一个 Network Namespace,但是并不为docker容器进行任何网络配置,这个docker容器没有网卡、IP、路由等信息,需要手动的去为docker容器添加网卡、配置IP等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。
5. user-defined模式
用户自定义模式主要可选的有三种网络驱动:bridge、overlay、macvlan。bridge驱动用于创建类似于前面提到的 bridge 网络;overlay和macvlan驱动用于创建跨主机的网络。
此处将着重介绍 overlay 网络,macvlan有时间再补充。
docker 原生overlay 的网络通信模型
-
docker官方文档的示例中,overlay网络是在swarm集群中配置的,但实际上,overlay网络可以独立于swarm集群实现,只需要满足以下前提条件即可。
- 有 consul 或者 etcd,zookeeper 的集群 key-value 存储服务;
组成集群的所有主机的主机名不允许重复,因为docker守护进程与consul通信时,以主机名相互区分; - 所有主机都可以访问集群 key-value 的服务端口,按具体类型需要打开进行配置。例如 docker daemon 启动时增加参数 –cluster-store=etcd://<ETCD-IP>:4001 – -cluster-advertise=eth0:2376
- overlay网络依赖宿主机三层网络的组播实现,需要在所有宿主机的防火墙上打开必要端口。
- 有 consul 或者 etcd,zookeeper 的集群 key-value 存储服务;
-
满足以上条件后,就可以通过 docker network 命令来创建跨主机的 overlay 网络了,例如:
docker network create -d overlay overlaynet
可以在集群的不同主机上,使用 overlaynet 这个网络创建容器。
该模型网络拓扑如下:
从这个通信过程中来看,跨主机通信过程中的步骤如下:
- 容器的网络命名空间与 overlay 网络的网络命名空间通过一对 veth pair 连接起来,当容器对外通信时,veth pair 起到网线的作用,将流量发送到 overlay 网络的网络命名空间中。
- 容器的 veth pair对端 veth2 与 vxlan设备通过 br0 这个 Linux bridge 桥接在一起,br0 在同一宿主机上起到虚拟机交换机的作用,如果目标地址在同一宿主机上,则直接通信,如果不在,则通过设置在 vxlan1 这个vxlan设备进行跨主机通信。
- vxlan1 设备上会在创建时,由 docker daemon 为其分配 vxlan 隧道ID,起到网络隔离的作用。
- docker 主机集群通过 key/value 存储共享数据,在7946端口上,相互之间通过gossip协议学习各个宿主机上运行了哪些容器。守护进程根据这些数据来在 vxlan1 设备上生成静态MAC转发表。
- 根据静态MAC转发表的设置,通过UDP端口4789,将流量转发到对端宿主机的网卡上。
- 根据流量包中的 vxlan 隧道 ID,将流量转发到对端宿主机的 overlay 网络的网络命名空间中。
- 对端宿主机的 overlay网络的网络命名空间中br0网桥,起到虚拟交换机的作用,将流量根据MAC地址转发到对应容器内部。
那么,最后在执行
-
ln
-s
/var/run/docker/netns
/var/run/netns
并在host1和host2 分别执行ip netns 查看容器和网络的网络命名空间,会发现存在一个相同的namespace。
-
容器的网络命名空间名称可以通过
docker inspect -f ‘{{.NetworkSettings.SandboxKey}}’ <container ID>
方式查看到。主机上的网络命名空间则是通过 docker network ls 查看到网络ID。
6. remote 模式
remote是为用其他项目来管理 docker 网络实现的接口。remote 采用 rpc 的方式将参数通过网络发给外部管理程序,外部程序处理好了后通过 json 返回结果给remote。
libnetwork的 remote driver 中实现了 Networkdriver 驱动,有基本的创建网络、创建port的接口,只要对应的 REST Server 实现了这些接口,就可以提供整套 docker 的网络。这些接口有:CreateNetwork、DeleteNetwork、CreateEndpoint、DeleteEndpoint、Join(绑定)、Leave(解绑或卸载)。
remote driver的接口定义在 https://github.com/docker/libnetwork/blob/master/docs/remote.md
libnetwork使用 Ipamdriver 来管理ip。这些接口有:GetDefaultAddressSpaces (获取本地或全局地址空间),RequestPool(获取IP地址池),ReleasePool(释放IP地址池),RequestAddress(获取IP地址),ReleaseAddress(释放IP地址)。
ipam driver的接口定义在 https://github.com/docker/libnetwork/blob/master/docs/ipam.md
libnetwork的插件发现机制在 https://docs.docker.com/engine/extend/plugin_api/#plugin-discovery
关于OpenStack kuryr
这里我将介绍的是 OpenStack kuryr 项目作为 remote driver 实现 Docker 操作 neutron 网络的案例。
kuryr安装:https://github.com/openstack/kuryr。
正如以上生命周期中所述,kuryr 需要先注册到 Libnetwork 中,Kuryr 是怎么作为 remote driver 注册到 Libnetwork 中呢?
这要依赖于 Docker 的 plugin discovery 机制。
- 配置 /etc/kuryr/kuryr.conf 文件,若是通过Devstack 安装会有默认配置,包括 [neutron] 选项,以便能连接到 neutron 。
- 指定远程驱动(插件)名称:用户或者容器要使用的 Docker 的插件(kuryr)时,他/它只需要指定插件的名字(kuryr)。
- 查询文件:Docker 会在相应的目录中( /usr/lib/docker/plugins/kuryr )查找与插件(kuryr)名字相同的文件,文件中定义了如何连接该插件(kuryr)。
如果用 devstack 安装 kuryr-libnetwork ,devstack 的脚本会创建文件 /usr/lib/docker/plugins/kuryr/kuryr.spec,该文件内容也很简单,默认是:http://127.0.0.1:23750 。也就是说, kuryr-libnetwork 实际上就起了一个 http server,这个 http server 提供了 Libnetwork 所需的所有接口。 Docker 找到有这样的文件之后,就通过文件的内容与Kuryr进行通信。
- 补充:插件目录下有三种文件类型:
- ① .sock : 文件是UNIX 套接字类型;
- ② .spec : 包含URL 的文本文件。比如unix:///other.sock 或者 tcp://localhost:8080。
- ③ .json : 文件是包含插件的完整 json 规范的文本文件。
具有UNIX域套接字文件的插件必须在同一个docker主机上运行,而带有spec或json文件的插件可以在指定远程URL的情况下在不同的主机上运行。
UNIX域套接字文件必须位于/run/docker/plugins下,而spec文件可以位于/etc/docker/plugins 或/usr/lib/docker/plugins下。
且文件名(不包括扩展名)决定了插件名称。
4. 激活插件:找到插件文件后,通过 /Plugin.Active 握手连接激活
请求:请求内容为空。
响应:
|
5. 激活后,插件将从docker子系统发送事件到neutron。
此外这里有三种返回类型:authz(认证插件返回类型)、VolumeDriver(卷插件返回类型)、NetworkDriver(网络插件返回类型)。
在这期间,IPAMdriver 就会在Endpoint 和network 的接口配置 IP 地址,实现上述接口。
综上,这样 kuryr 就注册到 Libnetwork 中了,IP地址也分配了,就可以与 neutron 网络交互了。
比如:
1. 在docker中创建一个网络
|
2. 用户使用docker创建一个容器的时候,指定网络名称 mykuryr。若这里不指定 --subnet 会默认是10.0.0.0/24,不过若是该子网被占用则报错。
|
即可查看到 mykuryr 网络。
|
获取关键信息:"Subnet": "10.0.1.0/24", "Gateway": "10.0.1.1","Id":“dd72f421”。
3. kuryr 从 libnetwork接收到请求,并用很多硬编码的schemata来验证用户输入的参数对不对,然后把创建 Network/Sandbox/Endpoint 转为对应 neutron 的资源,发送给 neutron;
查询neutron网络情况:
|
查看neutron的subnet信息:
|
从上述两条命令subnet为kuryr-subnet-10.0.1.0/24的所属网络30a14ee6,即是docker所创建出来的网络。
再来看子网池:
|
获取“Name”:kuryrPool-10.0.1.0/24 。对应上述subnet为 kuryr-subnet-10.0.1.0/24所在网络地址池。
注:若在docker创建网络时报错:kuryrPool-(子网段)does not exist.则说明subnetpool中无可用的子网段,需要创建subnetpool。
4. kuryr 接收 neutron 的结果,再用 pyroute2 创建 veth pair,一个是container interface 接入容器network namspace,一个是host interface 接入neutron 。
创建容器:
|
这是挂载了kuryr网络的容器的eth0网卡,地址是10.0.1.2。由于之前说过endpoint 对应在neutron中的port上,所以查看port信息:
|
获取的ID:6b1acede-9571-4417-a3dc-a2698969d78c。
|
因为neutron底层是ovs实现的,所以通过上述命令查询到“Bridge br-int”中包含了10.0.1.2的port ID:6b1acede,即是
|
那么 tap6b1acede-95 就是kuryr网络接入neutron的host interface。
5. kuryr把 neutron 返回的结果包装成 libnetwork 的样式,一般是 json 格式,转发给 libnetwork。
|
即可查看容器的网络信息与neutron的对应情况。
写在最后:容器网络部分还要待完善,比如通过Ambassador 容器机制,macvlan 网络等实现跨主机容器访问,其他第三方插件flannel、weave 和 calico的实现,CNI(kuryr-kubernetes)模型的实现等等。