Libnetwork作为Docker网络部分的依赖库,在Docker1.9中正式脱离实验阶段,进入主分支正式投入生产使用阶段。有了新的Networking我们可以创建虚拟网络,然后将container加入到虚拟网络中,以获得最适合所部署应用的网络拓扑结构。
1 组件的标准化
为了标准化网络驱动的开发步骤和支持多种网络驱动,Docker公司在Libnetwork中实现了CNM(Container Network Model)。CNM主要建立在如下三个组件上。
- 沙盒(Sandbox):一个沙盒包含了一个容器网络栈的信息。沙盒可以对容器的接口,路由和DNS设置等进行管理。沙盒的实现可以是Linux Network Namespace, FreeBSD Jail或者类似的机制。一个沙盒可以有多个端点(Endpoint)和多个网络(Network)。
- 端点(Endpoint):一个端点可以加入一个沙盒和一个网络。端点的实现可以是veth pair, Open vSwitch内部端口或者相似的设备。一个端点只可以属于一个网络并且只属于一个沙盒。
- 网络(Network):一个网络是一组可以直接互相联通的端点。网络的实现可以是Linux bridge,VLAN等等。一个网络可以包含多个端点。
2 CNM对象详细介绍
介绍了CNM中三个主要的组件之后,下面对CNM中的各个对象做一个详细的介绍。
- NetworkController:NetworkController通过暴露给用户创建和管理网络的API,以便用户对libnetwork进行调用。Libnetwork支持多种驱动,其中包括内置的bridge,host,container和overlay,也对远程驱动进行了支持(即支持用户使用自定义的网络驱动)。
- Driver:Driver不直接暴露给用户接口,但是driver是真正实现了网络功能的对象。NetworkController可以对特定driver提供特定的配置选项,其选项对libnetwork透明。为了满足用户使用需求和开发需求,driver既可以是内置的类型,也可以是用户指定的远程插件。每一种驱动可以创建自己独特类型的network,并对其进行管理。在未来可以会对不同插件的管理功能进行整合,以求做到方便管理不同类型的网络。
- Network:Network对象是CNM的一种实现。NetworkController通过提供API对Network对象进行创建和管理。NetworkController当需要创建或者更新Network的时候,它所对应的Driver会得到通知。Libnetwork通过抽象层面对同属于一个网络的各个endpoint提供通信支持,同时将其与其他endpoint进行隔离。这种通信支持可以是同一主机的,也可以是跨主机的。
- Endpoint:Endpoint代表了服务端点。它可以提供不同网络中容器的互联。Endpoint由创建对应Sandbox的驱动进行创建。
- Sandbox:Sandbox代表了一个容器中诸如ip地址,mac地址,routes,DNS等信息的配置。Sandbox在用户要求在一个Network创建Endpoint的时候进行创建并分配对应的网络资源。Libnetwork会用特定的操作系统机制(例如:linux的netns)去填充sandbox对应的容器中的网络配置。一个sandbox中可以有多个连接到不同网络的endpoint。
3 libnetwork工作执行流简介
下面对libnetwork的工作流做一个梗概。
- 1.指定network的驱动和各项相关参数之后调用 libnetwork.New()创建一个NetWorkController实例。这个实例提供了各种接口,Docker可以通过它创建新的NetWork和Sandbox等。
- 2.通过controller.NewNetwork(networkType, “network1”)来创建指定类型和名称的Network。
- 3.通过network.CreateEndpoint(”Endpoint1”)来创建一个Endpoint。在这个函数中Docker为这个Endpoint分配了ip和接口,而对应的network实例中的各项配置信息则会被使用到Endpoint中,其中包括iptables的配置规则和端口信息等。
- 4.通过调用controller.NewSandbox()来创建Sandbox。这个函数主要调用了namespace和cgroup等来创建一个相对独立的沙盒空间。
- 5.调用ep.Join(sbx)将Endpoint加入指定的Sandbox中,则这个Sandbox也会加入创建Endpoint对应的Network中。
总的来说,Endpoint是由Network创建的,隶属于这个创建它的Network,当Endpoint加入Sandbox的时候,就相当于这个Sandbox加入到了这个Network中来。三者简要的关系如图1所示。
图1 libnetwork中概念简要关系图
4 Docker使用libnetwork内置驱动的执行流详解
在介绍介绍Docker使用libnetwork内置驱动的执行流之前,我们先来看一下Dorker的默认网络模式(bridge模式)中,各个设备和容器之间的关系。其简要关系如图2所示。
图2 bridge模式中各个设备与容器关系
上图中container1和container2是两个Docker容器,veth0和veth1,veth2和veth3是两对veth pair(veth pair可以用于不同netns之间的通信),docker0是网桥(docker0可以连接不同的网络),eth0是宿主机的物理网卡。
图中的container1和container2都拥有不同的netns。以container1为例,veth pair中的veth0被加入到container1的netns中,并且被配置ip地址等基本信息,则veth0就成为了container1的一块网卡,与veth0所对应的的veth1被连接到docker0网桥上。Docker0网桥上还连接了宿主机的物理网卡eth0。这样container1就可以利用veth0与Internet进行通信了。类似的container2也连接到了docker0网桥上,显然container1与container2之间也是可以进行通信的。
结合上面提到的CNM中的概念来,我们可以知道docker0网桥就是CNM中的network组件,它可以代表一个网络。Container所拥有的netns就是CNM中的一个sandbox,它是一个独立的网络栈。Veth pair则是CNM中的endpoint,它的一个端点可以通过加入一个网桥来加入一个network,另一个端点可以加入一个sandbox,这样就将这个sandbox所对应的容器加入到了这个network中。
在了解了各个设备和容器之间的关系和各种设备和CNM各种组件的对应关系后,我们来看一下Docker是如何与libnetwork和libcontainer进行协作交互,并且为各个容器创建和配置网络的。Docker中使用默认模式(bridge模式)为容器创建和配置网络的过程如图3所示。
图3 网络创建与配置过程
结合图3,Docker源码和libcontainer源码,我们来详细看一下Docker在bridge模式下网络创建与配置的过程。
- 1.在Docker daemon启动的时候,daemon会创建一个bridge驱动所对应的netcontroller,这个controller可以在后面的流程里面对network和sandbox等进行创建。
- 2.接下来daemon通过调用controller.NewNetwork()来创建指定类型(bridge类型)和指定名称(即docker0)的network。Libnetwork在接收到创建命令后会使用系统linux的系统调用,创建一个名为docker0的网桥。我们可以在Docker daemon启动后,使用ifconfig命令看到这个名为docker0的网桥。
- 3.在Docker daemon成功启动后,我们就可以使用Docker Client进行容器创建了。容器的网络栈会在容器真正启动之前完成创建。在为容器创建网络栈的时候,首先会取得daemon中的netController,其值就是libnetwork中的向外提供的一组接口,即NetworkController。
接下来,Docker会调用BuildCreateEndpointOptions()来创建此容器中endpoint的配置信息。然后再调用CreateEndpoint()使用上面配置好的信息创建对应的endpoint。在bridge模式中,libnetwork创建的设备是veth pair。Libnetwork中调用netlink.LinkAdd(veth)进行了veth pair的创建,得到的一个veth设备是为了host所准备的,另一个是为了sandbox所准备的。将host端的veth加入到网桥(docker0)中。然后调用netlink.LinkSetUp(host),启动主机端的veth。最后对endpoint中的端口映射进行配置。
从本质上来讲,这一部分所做的工作就是调用linux系统调用,进行veth pair的创建。然后将veth pair的一端,作为docker0网桥的一个接口加入到这个网桥中。
- 4.创建SandboxOptions,然后调用controller.NewSandbox()来创建属于此container的新的sandbox。在接收到Docker创建sandbox的请求后,libnetwork会使用系统调用为容器创建一个新的netns,并将这个netns的路径返回给Docker。
- 5.调用ep.Join(sb)将endpoint加入到容器对应的sandbox中。先将endpoint加入到容器对应的sandbox中,然后对endpoint的ip信息和gateway等信息进行配置。
- 6.Docker在调用libcontainer来启动容器之后,libcontainer会在容器中的init进程初始化容器坏境的时候,将容器中的所有进程都加入到4中得到的netns中。这样容器就拥有了属于自己独立的网络栈,进而完成了网络部分的创建和配置工作。
5 libnetwork对插件的支持
正如前面所说,libnetwork不仅内置了bridge,host,container和overlay四种驱动,也对第三方插件提供了支持。第三方插件为了对libnetwork提供网络支持,需要实现并提供如下的API。
- driver.Config
- driver.CreateNetwork
- driver.DeleteNetwork
- driver.CreateEndpoint
- driver.DeleteEndpoint
- driver.Join
- driver.Leave
这些驱动不使用设备的名称作为其API的参数,而是使用唯一的id作为其参数,如networkid,endpointid等。
当用户指定自定义的网络驱动时,Docker会将libnetwork中的驱动设置为remote。Remote实际是上并没有进行具体的驱动实现,而是通过调用第三方插件来完成网络的创建,配置和管理工作。libnetwork与第三方插件的交互方式如图4所示。
图4 libnetwork与第三方插件交互过程
首先,我们需要手动启动libnetwork的第三方插件,这样libnetwork才能与第三方插件进行交互工作。
然后,我们在使用Docker创建容器的时候,需要指定第三方插件作为Docker的网络驱动。当libnetwork初始化其中的remote启动的时候,就会将第三方插件在libnetwork中进行注册,并且写入NetworkController当中。Libnetwork也会通过socket来和第三方插件进行连接和通信,并且进行libnetwork中所规定的握手协议等,来进行信息交换。
这样Docker可以像使用内置驱动一样来调用第三方插件的各种API。当libnetwork接收到Docker的调用请求之后,libnetwork会调用remote驱动中所对应的函数进行处理。这些函数都是通过对请求参数的封装,然后编码成JSON串的形式,通过之前建立的socket连接发送调用请求。
第三方插件在接收到请求之后,会解析请求中的JSON串,完成JSON串中所请求的操作,然后将执行结果打包成JSON串,并且通过socket返回给libnetwork。libnetwork解析第三方插件发送过来的返回信息,并将执行结果返回给Docker。