有人說Docker都過時了,有人說Docker CNM也都幹不過CNI等等。可是對於自己來說,熟悉Docker網絡無論對於自己的工作、興趣以及自己的執念都有一定的意義。如果對Docker network感興趣可以看下歷史文章。本文是一個Docker CNM原理的簡單的分析以及實現一個類似於bridge的自定義插件。這並不是結束,後面我還會實現基於全局的OVS的網絡插件。實例的代碼見文末。
CNM基本要素眾所周知,Docker主導的Container Network Model 簡稱CNM,是容器網絡的第一個標準。而libnetwork實現了容器網絡模型(CNM)。CNM 定義了 3 個基本要素:沙盒(Sandbox)、終端(Endpoint)和網絡(Network)。
Sandbox
Sandbox包含了一個容器網絡棧的配置。這包括對容器接口、路由表和 DNS 設置的管理,對應的技術有網絡命名空間, FreeBSD Jail 或其他類似的概念 。一個Sandbox可以包含多個處於不同Network的Endpoint。在docker中,Sandbox就是network namespce,在網絡命名空間中可以包括乙太網接口,路由表配置以及DNS等相關網絡的配置。
Endpoint
Endpoint作為Sandbox連接Network的媒介,Endpoint的實現技術有veth/peer設備,Open vSwitch的埠等類似的技術。一個Endpoint只能屬於一個Network。通俗的講,有了上面的Sandbox,不能沒有連接Network的介質,否則Sandbox就是一個孤島,沒法互聯互通,而Endpoint就是連接Network和Sandbox的介質,一個Endpoint只能屬於一個Sandbox。
Network
網絡是一組能夠相關通信的端點,網絡的實現可以是Linux Bridge、VLAN、Open vSwitch Bridge等。
插件發現機制
每當用戶或容器嘗試按名稱使用插件時,Docker 都會通過在插件目錄中查找插件來發現插件。插件目錄中可以放置三種類型的文件。
帶有 UNIX 域套接字文件的插件必須在同一個 docker 主機上運行,而帶有 spec 或 json 文件的插件如果指定了遠程 URL,則可以在不同的主機上運行。
UNIX域套接字文件必須位於下/run/docker/plugins,而規範的文本文件可以在位於/etc/docker/plugins或/usr/lib/docker/plugins。
接口實現Docker提供了實現網絡插件的庫, go-plugins-helpers 我們只需要引用這個庫,實現下面的接口
1// Driver represent the interface a driver must fulfill.
2type Driver interface {
3 GetCapabilities() (*CapabilitiesResponse, error)
4 CreateNetwork(*CreateNetworkRequest) error
5 AllocateNetwork(*AllocateNetworkRequest) (*AllocateNetworkResponse, error)
6 DeleteNetwork(*DeleteNetworkRequest) error
7 FreeNetwork(*FreeNetworkRequest) error
8 CreateEndpoint(*CreateEndpointRequest) (*CreateEndpointResponse, error)
9 DeleteEndpoint(*DeleteEndpointRequest) error
10 EndpointInfo(*InfoRequest) (*InfoResponse, error)
11 Join(*JoinRequest) (*JoinResponse, error)
12 Leave(*LeaveRequest) error
13 DiscoverNew(*DiscoveryNotification) error
14 DiscoverDelete(*DiscoveryNotification) error
15 ProgramExternalConnectivity(*ProgramExternalConnectivityRequest) error
16 RevokeExternalConnectivity(*RevokeExternalConnectivityRequest) error
17}
首先看著這十幾個方法挺嚇人的,但是並不是所有的方法都需要去具體的實現,最主要用的是下面的這幾個方法。
初始化配置,配置是全局的,還是本地的。全局的是集群的網絡同步,本地的就是本地節點的網絡。
1GetCapabilities() (*CapabilitiesResponse, error)
創建網絡
1CreateNetwork(*CreateNetworkRequest) error
創建容器的接口
1CreateEndpoint(*CreateEndpointRequest) (*CreateEndpointResponse, error)
2Join(*JoinRequest) (*JoinResponse, error)
3ProgramExternalConnectivity(*ProgramExternalConnectivityRequest) error
4EndpointInfo(*InfoRequest) (*InfoResponse, error)
刪除容器的接口
1RevokeExternalConnectivity(*RevokeExternalConnectivityRequest) error
2Leave(*LeaveRequest) error
3DeleteEndpoint(*DeleteEndpointRequest) error
刪除網絡
1DeleteNetwork(*DeleteNetworkRequest) error
只說不練假把式,我基於以上理論實現了一個類似於bridge的插件,可以用來練練手,了解下如何去開發自己的網絡插件。
定義三個類,一個是bridgeDriver,是我們的設計的橋的驅動類,bridgeConfiguration這個類是橋的配置,主要有網絡以及橋的名字。還有endPoint這個類實現的是存儲的veth-peer信息,以及ip信息。
1type bridgeDriver struct {
2 name string
3 networks map[string]*bridgeConfiguration
4 endPoints map[string]*endPoint
5}
6
7type bridgeConfiguration struct {
8 name string
9 mtu int
10 address *net.IPNet
11 gateway string
12}
13
14type endPoint struct {
15 id string
16 nid string
17 srcName string
18 address *net.IPNet
19}
根據上面的Network的定義,這次的網絡使用的是linux bridge,先通過解析negwork pool和網關,通過庫函數實現創建網橋,以及啟動網絡,設置網橋的IP信息。
1// create network method
2func (driver *bridgeDriver) CreateNetwork(createNetworkRequest *network.CreateNetworkRequest) error {
3 fmt.Println("create network... ")
4
5 // get ip pool and gateway info
6 for _, ip := range createNetworkRequest.IPv4Data {
7 fmt.Printf("request IPAM Data Address Space %s Pool %s Gateway %s\n", ip.AddressSpace, ip.Pool, ip.Gateway)
8 }
9
10 // get network id
11 networkId := createNetworkRequest.NetworkID
12
13 // init bridgeConfiguration
14 brigeConf := bridgeConfiguration{
15 name: "gbr-" + networkId[:10],
16 mtu: 1500,
17 }
18 if err:= brigeConf.getGatewayIP(createNetworkRequest); err != nil {
19 return err
20 }
21 // Set value for variable driver.networks
22 driver.networks[networkId] = &brigeConf
23 // create linux brige
24 if err := driver.createBridge(networkId); err != nil {
25 fmt.Println(err)
26 return err
27 }
28 return nil
29}
具體實現網橋的方法
1// create and set up linux bridge
2func (driver *bridgeDriver) createBridge(networkId string) error {
3 fmt.Println(networkId)
4 endPoint := &netlink.Bridge{
5 LinkAttrs: netlink.LinkAttrs{
6 Name: driver.networks[networkId].name,
7 },
8 }
9 if err := netlink.LinkAdd(endPoint); err != nil {
10 fmt.Println(err)
11 return fmt.Errorf("[setup] failed to create bridge %s", driver.networks[networkId].name)
12 }
13 if err := netlink.LinkSetUp(endPoint); err != nil {
14 return fmt.Errorf("[setup] failed to set link up for %s", driver.networks[networkId].name)
15 }
16 fmt.Println(driver.networks[networkId].gateway)
17 i, n, _ := net.ParseCIDR(driver.networks[networkId].gateway)
18 addr := &net.IPNet{IP: i, Mask: n.Mask}
19 nlAddr := &netlink.Addr{IPNet: addr, Label: ""}
20 if err := netlink.AddrAdd(endPoint, nlAddr); err != nil {
21 return fmt.Errorf("[setup] failed to set link up for %s", driver.networks[networkId].name)
22 }
23 return nil
24}
採用的endpoint是veth peer,veth peer是一對虛擬設備對,類似的網線的兩端水晶頭,一頭依附在上面創建的網絡內,一頭添加在網絡中,這樣就能實現容器的通信。具體實現就是創建了虛擬設備對,然後需要啟動虛擬設備對,以及依附到bridge上。
1func (driver *bridgeDriver) CreateEndpoint(createEndpointRequest *network.CreateEndpointRequest) (*network.CreateEndpointResponse, error) {
2 fmt.Println("CreateEndpoint")
3 response := &network.CreateEndpointResponse{}
4 endPointId := createEndpointRequest.EndpointID
5 ipaddress := createEndpointRequest.Interface.Address
6 fmt.Println("network=%s",ipaddress)
7 veth := "veth" + endPointId[:5]
8 peer := "peer" + endPointId[:5]
9
10 // init endPoint
11 end := endPoint{
12 id: endPointId,
13 nid: createEndpointRequest.NetworkID,
14 srcName: veth,
15 }
16 driver.endPoints[endPointId] = &end
17 vethPeer := &netlink.Veth{
18 LinkAttrs: netlink.LinkAttrs{Name: peer, TxQLen: 0},
19 PeerName: veth,
20 }
21 if err := netlink.LinkAdd(vethPeer); err != nil {
22 return response, fmt.Errorf("[CreateEndpoint] failed to add the veth device")
23 }
24
25 hostside, err := netlink.LinkByName(peer)
26 if err != nil {
27 return response, fmt.Errorf("[CreateEndpoint] failed to get host side interface")
28 }
29 defer func() {
30 if err != nil {
31 netlink.LinkDel(hostside)
32 }
33 }()
34 containerside, err := netlink.LinkByName(veth)
35 if err != nil {
36 return response, fmt.Errorf("[CreateEndpoint] failed to get container side interface")
37 }
38 defer func() {
39 if err != nil {
40 netlink.LinkDel(containerside)
41 }
42 }()
43
44 if err := netlink.LinkSetUp(containerside); err != nil {
45 return response, fmt.Errorf("[setup] failed to set link up for %s", containerside)
46 }
47
48 if err := netlink.LinkSetUp(hostside); err != nil {
49 return response, fmt.Errorf("[setup] failed to set link up for %s", hostside)
50 }
51
52 master, err := netlink.LinkByName(driver.networks[createEndpointRequest.NetworkID].name)
53 if err != nil {
54 return response, fmt.Errorf("[Get] failed to get master Bridge.")
55 }
56
57 if err = netlink.LinkSetMaster(hostside, master); err != nil {
58 return response, fmt.Errorf("[CreateEndpoint] failed to add hostside to Bridge")
59 }
60
61 return response, nil
Join方法實現了需要插入到Sandbox的joinRequest,包括veth peer名字,以及在容器內部的接口的標識信息,以及網關信息,實例化這個joinRequest,然後返回,Docker Daemon會自動幫我們加入到制定的容器的Sandbox中。
1func (driver *bridgeDriver) Join(joinRequest *network.JoinRequest) (*network.JoinResponse, error) {
2 fmt.Println("joing....")
3 eid := joinRequest.EndpointID
4
5 resp := &network.JoinResponse{}
6 resp.InterfaceName.SrcName = driver.endPoints[eid].srcName
7 resp.InterfaceName.DstPrefix = "eth"
8 parts := strings.Split(driver.networks[joinRequest.NetworkID].gateway, "/")
9 if parts[0] == "" || parts[1] == "" {
10 return nil, fmt.Errorf("Cannot split gateway IP address")
11 }
12 resp.Gateway = parts[0]
13 return resp, nil
14}
啟動編寫好的控制器代碼
可以看到生成了socket文件
通過docker network create創建自定義的網絡
查看網絡的詳細信息
創建容器並且添加到網絡中,查看具體的IP信息
完整的代碼見:https://gitee.com/yunqianqian/docker_network_plugin_example.git