Docker 容器网络的发展历史
在 Dokcer 发布之初,Docker 是将网络、管理、安全等集成在一起的,其中网络模块可以为容器提供桥接网络、主机网络等简单的网络功能。
从 1.7 版本开始,Docker正是把网络和存储这两部分的功能都以插件化形式剥离出来,允许用户通过指令来选择不同的后端实现。剥离出来的独立容器网络项目叫 libnetwork。
在 1.9 版本时,Docker 又引入了一整套 network 子命令和跨主机网络支持,这允许用户可以根据他们应用的拓扑结构创建虚拟网络并将容器接入其所对应的网络。
什么是 Docker Libnetwork
为了标准化网络的驱动开发步骤和支持多种网络驱动,Docker 将网络部分代码被抽离成为了单独的网络库(Libnetwork),Libnetwork 提供了可以用于开发多种网络驱动的标准化接口和组件。
Docker daemon 通过调用 Libnetwork 对外提供的 API 完成网络的创建和管理等功能,Libnetwork 内置了5种驱动来提供不同类型的网络: bridge driver, host driver, null driver, overlay driver, remote driver
bridge driver
此驱动为Docker的默认设置驱动,使用这个驱动的时候,libnetwork将创建出来的Docker容器连接到Docker网桥上。作为最常规的模式,bridge模式已经可以满足Docker容器最基本的使用需求了。然而其与外界通信使用NAT,增加了通信的复杂性,在复杂场景下使用会有诸多限制。
host driver
使用这种驱动的时候,libnetwork将不为Docker容器创建网络协议栈,即不会创建独立的network namespace。Docker容器中的进程处于宿主机的网络环境中,相当于Docker容器和宿主机共同用一个network namespace,使用宿主机的网卡、IP和端口等信息。
但是,容器其他方面,如文件系统、进程列表等还是和宿主机隔离的。host模式很好地解决了容器与外界通信的地址转换问题,可以直接使用宿主机的IP进行通信,不存在虚拟化网络带来的额外性能负担。但是host驱动也降低了容器与容器之间、容器与宿主机之间网络层面的隔离性,引起网络资源的竞争与冲突。
因此可以认为host驱动适用于对于容器集群规模不大的场景。
null driver
使用这种驱动的时候,Docker容器拥有自己的network namespace,但是并不为Docker容器进行任何网络配置。也就是说,这个Docker容器除了network namespace自带的loopback网卡名,没有其他任何网卡、IP、路由等信息,需要用户为Docker容器添加网卡、配置IP等。
这种模式如果不进行特定的配置是无法正常使用的,但是优点也非常明显,它给了用户最大的自由度来自定义容器的网络环境。
overlay driver
此驱动采用IETE标准的VXLAN方式,并且是VXLAN中被普遍认为最适合大规模的云计算虚拟化环境的SDN controller模式。在使用过程中,需要一个额外的配置存储服务,例如Consul、etcd和zookeeper。还需要在启动Docker daemon的时候额外添加参数来指定所使用的配置存储服务地址。
remote Driver
这个驱动实际上并未做真正的网络服务实现,而是调用了用户自行实现的网络驱动插件,使libnetwork实现了驱动的可插件化,更好地满足了用户的多种需求。用户只需要根据libnetwork提供的协议标准,实现其所要求的各个接口并向Docker daemon进行注册。
什么是 Docker CNM
Docker Libnetwork 中使用了 CNM 的容器网络模式概念,CNM定义了构建容器虚拟化网络的模型,此后容器网络模式也被抽象变成了统一接口的驱动。
CNM 中主要有 sandbox、endpoint 和 network 3 种核心组件CNM 中核心组件的使用模型如下图:
沙盒 (sandbox):一个沙盒包含了一个容器网络栈的信息。沙盒可以对容器的接口(interface)、路由和 DNS 设置等进行管理,沙盒的实现可以是Linux netns、FreeBSD Jail 或者类似的机制,一个沙盒可以有多个端点和多个网络。
端点 (endpoint):一个端点可以加入一个沙盒和一个网络。端点的实现可以是 veth pair、ovs 内部端口或者相似的设备,一个端点只属于一个网络并且只属于一个沙盒。
网络 (network):一个网络是一组可以直接互相联通的端点。网络的实现可以是 Linux bridge、VLAN等,一个网络可以包含多个端点。
Libnetwork Remote driver
kuryr-libnetwork 是 Libnetwork 框架下的一种 remote driver 实现,现在已经成为Docker 官网推荐的一个 remote driver,kuryr-libnetwork 需要做的就是实现 Libnetwork remote driver 需要实现的接口.
常见的 remote driver 要实现的接口如下,格式:HTTP POST + JSON Body
/Plugin.Activate no payload -- Handshake
/NetworkDriver.GetCapabilities -- Set capability
/NetworkDriver.DiscoverNew -- DiscoverNew Notification
/NetworkDriver.DiscoverDelete -- DiscoverDelete Notification
/NetworkDriver.AllocateNetwork -- Allocate network specific resources, only called in docker swarm mode
/NetworkDriver.FreeNetwork -- Free network specific resources, only called in docker swarm mode
/NetworkDriver.CreateNetwork -- Create network
/NetworkDriver.DeleteNetwork -- Delete network
/NetworkDriver.CreateEndpoint -- Create endpoint
/NetworkDriver.EndpointOperInfo -- Endpoint operational info
/NetworkDriver.DeleteEndpoint -- Delete endpoint
/NetworkDriver.Join -- Join an endpoint to a sandbox
/NetworkDriver.Leave -- Remove an endpoint from a sandbox
IPAM Driver
在 Libnetwork 中,CNM 模块通过 IPAM Driver 管理 IP 地址的分配,Libnetwork 内含有一个默认的IPAM驱动,同时它也允许动态地增加第三方IPAM驱动。
在用户创建网络时可以指定 Libnetwork 使用的 IPAM 驱动, Kuryr 项目通过实现了 IPAM 的驱动接口,成为了Docker 的第三方 libnetwork IPAM driver。
常见的 IPAM driver 要实现的接口如下,格式:HTTP POST + JSON Body
/IpamDriver.GetCapabilities -- provides the IPAM driver capabilities. it's called during the registration of the IPAM driver.
/IpamDriver.GetDefaultAddressSpaces -- returns the default local and global address space names for this IPAM. it's called after the registration of the IPAM driver
/IpamDriver.RequestPool -- registering an address pool with the IPAM driver. multiple identical calls must return the same result.
/IpamDriver.RequestAddress -- allocates the IP address
/IpamDriver.ReleaseAddress -- deallocates the IP address
/IpamDriver.ReleasePool -- releasing a previously registered address pool
Docker 网络的生命周期
Docker 用户可以通过与 CNM 的 Object 以及 API 的交互来管理对应容器的网络,下面是一个典型的容器网络生命周期:
1、Driver要向NetworkController注册。内置的Driver在Libnetwork内注册,远程的Driver则通过Plugin mechanism注册。每一个Driver处理特定的networkType。
2、libnetwork.New():NetworkController通过libnetwork.New()创建,用于Network的创建以及通过一些特定的Options配置Driver。
3、controller.NewNetwork():Network通过给这个API提供name和networkType来创建,networkType参数用来选择特定的Driver并且将创建的Network和该Driver相关联。从此以后,对于Network的任何操作都由Driver处理。controller.NewNetwork() 还有一个可选的options参数,用于提供特定Driver的options和Labels。
4、network.CreateEndpoint():可以用于在给定的Network中创建一个新的Endpoint。同时该API还有一个可选的options参数供Driver使用。这个"options"既可以携带已知的labels,也可以携带和特定Driver相关的labels。之后调用相应的Driver的driver.CreateEndpoint,它可以为在一个Endpoint在Network中被创建时,为它们保留IP地址。Driver会通过driverapi中定义的InterfaceInfo进行这些地址的赋值。IP地址将和endpoint暴露的端口用来完善Endpoint作为Service的定义。事实上,Service endpoint不是其他什么东西,仅仅只是一个网络地址以及该应用的容器监听的端口号。
5、endpoint.Join():用于将Endpoint与一个容器相连接。Join操作会先创建一个Sandbox如果对应的容器中还没有的话。Driver可以使用Sandbox Key来识别连接到同一个容器的多个Endpoint。这个API同样接受可选的options参数供Driver使用。
- 虽然这并不是Libnetwork直接的设计要求,但是我们鼓励像Docker这样的用户在执行容器的Start()操作时,即在容器可以操作之前,调用endpoint.Join()。
- 另一个关于endpoint.join()这个API经常被提到的问题是,为什么我们需要一个API创建Endpoint和另一个API来join endpoint。事实上Endpoint代表的是一个Service,它可能有,也可能并没有容器支持。当一个Endpoint被创建的时候,会预留它所需的资源,因此任何容器都能连接该Endpoint并且获得一个一致的网络行为。
6、endpoint.Leave():会在容器停止的时候被调用。Driver可以清除它在调用Join()时获取的状态。Libnetwork会在最后一个Endpoint离开的时候删除Sandbox。但是只要该Endpoint依旧存在,Libnetwork会依然保有IP地址并且在有新的容器加入的时候进行重用。这保证了容器的资源在停止并重启的过程中能够重用。
7、endpoint.Delete():用于从一个Network中删除Endpoint。这将导致Endpoint的删除以及清空缓存的sandbox.Info。
8、network.Delete():用于删除Network。如果还有Endpoint连接到该网络,Libnetwork是不允许对它进行删除的。
Docker 网络命名空间
docker 常常使用 linux netns 实现网络资源隔离,但使用 ip netns 命令却无法查看,这是因为 docker 默认把创建的网络命名空间链接文件隐藏起来了,导致 ip netns 命令无法读取,可以通过下面的方法复现 docker 的 ip netns 命名空间。
# 创建一个带有桥接网络的 docker 容器
$ docker run -it -d --rm --name mytest --network bridge cirros /bin/sh
c093857c756028b4d4f37b16262d017239236bde22a3545f8769fd17366f183a
$ docker ps | grep mytest
c093857c7560 cirros "/bin/sh" 6 seconds ago Up 2 seconds mytest
# 可以通过 inspect 命令查看该容器的 ip 地址和进程号
$ docker inspect mytest |egrep '"IPAddress"|"Pid"'
"Pid": 14908,
"IPAddress": "172.17.0.2",
# 通过进程号参考容器进程
$ ps -fp 14908
UID PID PPID C STIME CMD
root 14889 1676 0 11:42 containerd-shim -namespace moby \
-workdir
/var/lib/containerd/io.containerd.runtime.v1.linux/moby/c093857c756028b4d4f37b16262d017239236bde22a3545f8769fd17366f183a \
-address /run/containerd/containerd.sock \
-containerd-binary /usr/bin/containerd \
-runtime-root /var/run/docker/runtime-runc
# 通过 nsenter 进入容器网络空间
$ nsenter --target 14908 --net ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
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
54: eth0@if55: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
# 通过软连接容器命名空间实现在 ip netns 下显示
$ ls /proc/14908/ns/net
lrwxrwxrwx 1 root root 0 Jul 25 11:42 /proc/14908/ns/net -> net:[4026532445]
$ ln -s /proc/14908/ns/net /var/run/netns/mytest
# 最后检查一下
$ ip netns
mytest (id: 1)
$ ip netns exec mytest ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
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
54: eth0@if55: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
Docker 主机名与DNS
一个镜像可以启动多个容器,但是它们的主机名和网络信息并不一样,也即是说主机名和网络信息并非是被写入镜像中的。实际上容器中/etc/目录下有三个文件是容器启动后被虚拟文件覆盖的,分别是/etc/hostname、/etc/hosts、/etc/resolv.conf。对这三个文件的修改不会被docker commit保存,也就是不会保存在镜像中,重启容器也会导致修改失效。
$ docker exec -it mytest mount | grep etc
/dev/mapper/centos-root on /etc/resolv.conf type xfs (rw,relatime,attr2,inode64,noquota)
/dev/mapper/centos-root on /etc/hostname type xfs (rw,relatime,attr2,inode64,noquota)
/dev/mapper/centos-root on /etc/hosts type xfs (rw,relatime,attr2,inode64,noquota)
- 这样能解决主机名的问题,同时也能让DNS及时更新(改变resolv.conf)。
- 由于这些文件的维护方法随着Docker版本演进而不断变化,因此尽量不修改这些文件,而是通过Docker提供的参数进行相关设置。
参考资料
https://github.com/docker/libnetwork/blob/master/docs/design.md
http://dockone.io/article/1306
https://www.oreilly.com/library/view/learning-docker-networking/9781785280955/
https://feisky.gitbooks.io/sdn/container/cnm/
https://www.nuagenetworks.net/blog/container-networking-standards/