Docker CNM 原理分析與實戰

2022-01-25 HelloWorld技術分享

有人說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

採用的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

相關焦點

  • docker實戰(三)
    我們在docker實戰(二)中聊了一些docker架構的內容,希望為大家呈現一個docker技術的全景圖或鳥瞰圖,不過考慮到一次聊太多大家容易消化不良
  • Docker小白到實戰之Dockerfile解析及實戰演示,果然順手
    實戰演示這裡還是以.NetCore項目構建鏡像為例,其他程式語言的項目同理;這次咱們一步一步的來,搞清楚每個命令的使用。以下關於項目創建和發布的具體細節在第一篇最後就分享了,小夥伴可以參考,這裡主要演示Dockerfile關鍵字。3.1 準備項目和Dockerfile文件新建一個項目,啥都不需要改,就用默認的接口演示,如下:
  • Docker學習之工作原理
    想學習好一點技術,最好的方法就是學習這項技術的工作原理。一直很好奇docker是如何實現互不影響的,很是神奇。    通過學習查閱一些資料,慢慢的了解了底層是如何實現的,特此記錄一下。    從本質上說,docker容器其實就是一個沙盒技術。就好像把一個應用隔離在一個盒子內,讓其運行。因為盒子有邊界的存在,應用於應用之間不會存在相互幹擾。實現原理    實現容器的核心,其實就是要生成限制應用運行時的邊界。編譯後的可執行代碼加上數據,叫做程序。
  • 實戰 Windows Server Docker :Docker化現有 IIS 應用的正確姿勢 (2)
    (點擊上方藍字,可快速關注我們)來源:Teddy's Knowledge Basecnblogs.com/teddyma/p/Windows-Server-Docker-2.html上一篇《老司機實戰這一篇,我們來填一些稍大一些的坑:如何docker化一個現有的iis應用。問題分析聽說Windows支持原生docker了,大家一定都很興奮。然而,大家想過沒有,Windows Server Docker最適合什麼場景呢?部署.NET Core應用?為什麼不選擇Linux下的docker?
  • Docker基礎與實戰,看這一篇就夠了
    使用uname -r指令查看伺服器版本卸載老版本的docker(若有)yum remove docker docker-common docker-selinux docker-engine執行該命令只會卸載Docker本身,而不會刪除Docker存儲的文件,例如鏡像、容器、卷以及網絡文件等。這些文件保存在/var/lib/docker 目錄中,需要手動刪除。
  • 老司機實戰Windows Server Docker:2 docker化現有iis應用的正確姿勢
    前言上一篇老司機實戰Windows Server Docker:1 初體驗之各種填坑介紹了安裝docker服務過程中的一些小坑
  • Redis Sentinel-深入淺出原理和實戰
    ❝本篇博客會簡單的介紹Redis的Sentinel相關的原理,同時也會在最後的文章給出「硬核的」實戰教程,讓你在了解原理之後,能夠實際上手的體驗整個過程。❞之前的文章聊到了Redis的主從複製,聊到了其相關的原理和缺點,具體的建議可以看看我之前寫的文章Redis的主從複製。
  • 【擁抱容器】:Docker 與 K8s 的入門課實戰系列課程總結
    不管你是想要複習,還是因故未能參加,還是中途缺席課程的,都可以戳以往的文章回顧學習哦~:👇👇👇【活動回顧】容器實戰:Docker 基礎入門講座實錄 | 擁抱容器:Docker 集群入門與實戰(上)講座實錄 | 擁抱容器:Docker 集群入門與實戰(下)【活動回顧】擁抱容器:Python web開發及其與
  • 【乾貨】解密監控寶Docker監控實現原理
    3、DockerAgent數據採集原理  下面我們聊一下DockerAgent採集數據的原理。DockerAgent首先會使用docker info命令來獲取docker系統信息,這些信息包含了非常有用的數據,如: Containers, Images, Name, CPUs, Data Space Used, Data Space Total, Total Memory。
  • Docker集群管理之Docker Compose
    前言:在上一篇《Docker集群管理之Docker Machine》中,我們通過源碼分析了解了Docker Machine的工作原理,使用者可以通過Docker Machine的一條命令在任意支持的平臺創建一個Docker主機,並能集中管理這些主機。Docker主機創建好之後,接下來就該考慮Docker容器部署的問題了。
  • Docker 監控實戰
    或許具體問題要具體分析,但是似乎大家都在使用開源的監控方案,來解決 Docker監控的問題。就拿騰訊遊戲來說吧,我們看看尹燁(騰訊互娛運營部高級工程師,  乾貨 | 騰訊遊戲是如何使用 Docker 的? )怎麼說:容器的監控問題也花了我們很多精力。
  • Java架構進階Docker實戰,看完我成功拿到了字節跳動的offer
    Java架構進階Docker實戰,看完我成功拿到了字節跳動的offer前言:Docker是有史以來增長最快的開源項目之一,在其周圍的生態系統也是以類似的速度不斷發展。由於這些原因,本書的重點完全在於Docker 的工具集。
  • docker的/var/run/docker.sock參數
    註:關於上述docker-compose.yml的作用和相關實戰,請參考《kafka的Docker鏡像使用說明(wurstmeister/kafka)》;預備知識搞清楚/var/run/docker.sock參數的前提是了解docker的client+server架構,如下是執行docker version命令的結果:
  • Docker簡介與簡單使用
    Docker Machine:Docker Machine是一個簡化Docker安裝的命令行工具,通過一個簡單的命令行即可在相應的平臺上安裝Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure終於到了實戰環節,在這一小節裡面,我們通過兩個具體案例介紹Docker的簡單使用方法,首先是Docker
  • ASP.NET Core容器化技術Docker零基礎從入門到實戰演練
    前面的《ASP.NET Core使用Docker進行容器化託管和部署》基礎課程我們學習了如何使用Docker來部署搭建ASP.NET Core + Mysql容器化應用程式環境。藉助Compose模塊,我們可以編寫一個docker-compose.yml文件,使用聲明性語法啟動一系列相互連接的容器,即可一步完成上面的任務。今天給大家分享一下如何使用Docker-Compose搭建ASP.NET Core多容器應用環境並一鍵構建部署運行!
  • Docker 入門到實戰教程(六)Docker數據卷
    file2.2 掛載數據卷兩種掛載方式:docker run --name 容器名 -it --mount source=卷名,target=容器內絕對路徑(掛載點) 鏡像名docker2.2.1 -mountdocker run -d -P --name test-web -mount source=my-vol,target=/webapp training/webapp python app.py2.2.2 -v掛載docker run -d -P --name test-web
  • docker-compose是個好東西,越用越香
    回顧前文前文演示了在單一容器中部署 Nginx和ASP.NET Core WebApp, 正在前文評論區某大牛指出的,容器化部署 nginx+ASP.NET Core 有更符合實戰的部署選擇:多容器獨立部署。這次記錄我在工作中利用 docker-compose部署企業級web應用。
  • Docker 入門到實戰教程(五)構建Docker鏡像
    認證信息上會被保存(保存於$HOME/.docker/config.json文件),以便之後使用。退出登錄可以使用docker logout命令。docker commit提交前,先退出容器:2.2 提交更改提交時要通過容器名或容器ID指定所要提交的容器,並要指定一個目標倉庫和鏡像名。docker commit提交時比較輕量,只會提交創建容器的鏡像與容器當前狀態之間有差異的部分。
  • Docker鏡像進階:了解其背後的技術原理
    什麼是 docker 鏡像  docker 鏡像是一個只讀的 docker 容器模板,含有啟動 docker 容器所需的文件系統結構及其內容,因此是啟動一個 docker 容器的基礎。
  • Docker 最初的2小時(Docker從入門到入門)
    最初的2小時,你會愛上Docker,對原理和使用流程有個最基本的理解,避免滿世界無頭蒼蠅式找資料。本人反對暴風驟雨式多管齊下狂轟濫炸的學習方式,提倡迭代學習法,就是先知道怎麼玩,有個感性認識,再深入學習高級用法,深層原理,一輪輪迭代。堅決反對一上來就搞幾百頁厚的東西把人腦子弄亂。Docker是什麼?