阿里開源富容器引擎 PouchContainer 的 network 連接機制



PouchContainer 是阿里巴巴集團開源的高效、輕量級企業級富容器引擎技術,擁有隔離性強、可移植性高、資源佔用少等特性。可以幫助企業快速實現存量業務容器化,同時提高超大規模下數據中心的物理資源利用率

PouchContainer 源自阿里巴巴內部場景,誕生初期,在如何爲互聯網應用保駕護航方面,傾盡了阿里巴巴工程師們的設計心血。PouchContainer 的強隔離、富容器等技術特性是最好的證明。在阿里巴巴的體量規模下,PouchContainer 對業務的支撐得到雙 11 史無前例的檢驗,開源之後,阿里容器成爲一項普惠技術,定位於「助力企業快速實現存量業務容器化」。

本文將給大家介紹 PouchContainer 實現 network 的機制以及將容器連接到 network 上的原理。爲了充分闡述 network 的連接機制,本文將以Connect方法爲例,敘述如何動態地將一個 container 連接到一個已存在的 network 上。

1. PouchContainer 實現 network 的機制

在目前的容器網絡虛擬化技術中,Docker 推行的 CNM (Container Network Model)模型是一種通用的解決方案,CNM 構建了一種成熟的容器虛擬化網絡模型,並定義了多種供開發者調用的標準化接口。PouchContainer 沿用了 CNM 模型,基於 libnetwork 來實現容器間通信。下面先對 Sandbox、Endpoint 和 Network 這三個 CNM 中的核心組件進行介紹。

Sandbox

Sandbox 一詞在不同的機制裏,被分別賦予了不同的定義。例如,在 CRI(container runtime interface)裏面 sandbox 就代表着 pod 的概念。而在 CNM 模型裏,sandbox 代表着一個容器的網絡棧配置,包含管理容器的網卡,路由表以及 DNS 設置。Sandbox 的具體實現可以通過 Linux 系統的 network namespace,一個 FreeBSD Jail 或者其他類似的概念。一個 sandbox 可以包含多個 endpoints。

Endpoint

一個 endpoint 將 sandbox 連接到 network 上。一個 endpoint 的實現可以通過 veth pair,Open vSwitch internal port 或者其他的方式。比較常見的方法是用 veth pair,顧名思義,veth pair一定是成對出現的,因此會存在 veth0 和 veth1 兩塊網卡。創建容器時,其中一塊會被設置到容器內部,充當容器內部的eth0,所有目的地址爲容器 IP 的數據包都要經過 eth0 網卡;另一塊(以下稱爲 veth 設備)則會被連接到宿主機的網橋上。從 veth 設備出去的數據包,會轉發到對應的 eth0 設備上,當數據包的目的地址爲 eth0 設備的 IP 時,就能被內核協議棧處理。用 veth pair 來連接兩個 network namespace,從而建立網絡連通關係。一個 Endpoint 只能屬於一個 Network,也只能屬於一個 Sandbox。

Network

一個 Network 是一組可以相互通信的 Endpoints 的集合。一個 network 的實現可以通過 Linux bridge,VLAN 或者其他方式。值得一提的是,一個 network 中可以包含很多個 endpoints。

可以看到,在如下圖所示的結構下,Container A 和 Container B 同屬於 backend network,這兩個 container通過各自紫色的 endpoint 構成 network 連接;container B和 container C 同屬於 frontend network,通過藍色的 endpoint 構成 network 連接。因此 container A 和 container B之間可以通信,container B和 container C之間也可以通信。

接下來重點看一下 container B 內部的兩個 endpoints,雖然 backend network 和 frontend network 在 container B 內都有各自對應的 endpoint,但紫色 endpoint 和藍色 endpoint 間不構成通信。因此 backend network 和 frontend network 是兩個完全隔離的 network,並不因爲連接同一個 container 而產生連通。顯而易見,container A 和 container C 間其實是無法通信的。

2. PouchContainer 內置的 network 模式

2.1 bridge 模式

bridge 模式是 PouchContainer 默認的網絡模式,在創建容器不指定 network 模式,即不寫--net參數,該容器就會以 bridge 模式創建。pouchd啓動的時候,會自動在主機上創建一個虛擬網橋 p0。後續以 bridge 模式創建容器時,pouchd從 p0 網橋所在的 IP 網段中選取一個未使用的 IP 分配給容器的 eth0 網卡,p0 的 IP 是這些容器的默認網關。

 

2.2 host 模式

在啓動容器的時候,選擇 host 模式,那麼容器將不會獲得獨立的 network namespace,而是和主機共享 network namespace。因此,這個容器也就沒有自己的網卡和 IP 配置,會使用主機的 IP 和端口,但 fs 和 pid 等與主機還是隔離的。

2.3 container 模式

以 container 模式創建的容器,會和已經存在的容器共享一個 network namespace,直接沿用其 veth 設備對。

2.4 none 模式

使用 none 模式創建的容器,擁有獨立的 network namespace,但是不會對容器進行任何的網絡配置。因此,可以認爲 none 模式下的容器,是不和其它容器通信的。不過,在容器創建後,可以再給它添加網卡、配置 IP,這樣就可以與同一個 network 下的容器通信了。

2.5 CNM 與 network 模式的概念交叉

一個 network 是一個唯一的、可識別的 endpoint 組,組內的 endpoint 可以相互通訊。對比 CNM 來看,endpoint 可以簡單理解成 veth 設備對,容器的 sandbox 裏可以有多個 endpoints,每個 endpoint 代表和一個特定 network 的連接關係。

3. network connect 的流程分析

// daemon/mgr/container.go

// Connect is used to connect a container to a network.
func (mgr *ContainerManager) Connect(ctx context.Context, name string, networkIDOrName string, epConfig *types.EndpointSettings) error {
    ……
    if err := mgr.updateNetworkConfig(c, n.Name, epConfig); err != nil {
        return err
    } else 
    if err := mgr.connectToNetwork(ctx, c, networkIDOrName, epConfig); err != nil {
        return err
    }
    return c.Write(mgr.Store)
}

可以看到在Connect函數裏,首先根據傳入的參數獲取到具體的 container 和 network。而epConfig參數裏面,存放的是在 CLI 端通過 flag 傳入的參數,如 container 在特定 network 中的別名、指定的 IP 範圍等。

查看c.State.Status來判斷 container 此時的狀態,dead 狀態的 container 是無法執行 connect 操作的。對於非 running 但是還 live的container,只是簡單地調用updateNetworkConfig()來更新 container 的網絡配置,將傳入的epConfig加入到容器的 network 配置中。在這種情況下,不會爲 container 分配網卡,因此 container 並沒有成功連通到 network 中。對於 running 狀態的 container,調用connectToNetwork()來進行後續的操作,connectToNetwork()會根據給定的 network 和 container 進行網卡的配置,再在主機上分配一個網卡,最後將網卡加入到 container 的 sandbox 裏面。這樣,container 就成功地連接到 network 上了!具體的流程會在後續進行解析。

c.Write(mgr.Store)的作用,是將 container 連接到 network 上的一系列配置寫入 container 的 metadata 裏面,這樣就保證了數據的持久化。否則,建立的 network 連接只是一次性的,所有的數據和相關配置在pouchd重啓後都會丟失。

// daemon/mgr/container.go

func (mgr *ContainerManager) connectToNetwork(ctx context.Context, container *Container, networkIDOrName string, epConfig *types.EndpointSettings) (err error) {
    ……
    endpoint := mgr.buildContainerEndpoint(container)
    ……
    if _, err := mgr.NetworkMgr.EndpointCreate(ctx, endpoint); err != nil {
        ……
    }
    return mgr.updateNetworkConfig(container, networkIDOrName, endpoint.EndpointConfig)
}

endpoint 裏面包含三部分的信息,一部分的信息來自於 container,一部分的信息來自 network,最後一部分信息是 connect 命令裏 flag 中的配置。buildContainerEndpoint()的邏輯比較簡單,就是獲取到 endpoint 需要的 container 相關信息。隨後調用了NetworkMgrEndpointCreate()來進行具體的構建。

// daemon/mgr/network.go

// EndpointCreate is used to create network endpoint.
func (nm *NetworkManager) EndpointCreate(ctx context.Context, endpoint *types.Endpoint) (string, error) {
    ……
    // create endpoint
    epOptions, err := endpointOptions(n, endpoint)
    ……
    endpointName := containerID[:8]
    ep, err := n.CreateEndpoint(endpointName, epOptions...)
    ……

    // create sandbox
    sb := nm.getNetworkSandbox(containerID)
    if sb == nil {
        sandboxOptions, err := buildSandboxOptions(nm.config, endpoint)
        ……
        sb, err = nm.controller.NewSandbox(containerID, sandboxOptions...)
        ……
    }

    // endpoint joins into sandbox
    joinOptions, err := joinOptions(endpoint)
    ……
    if err := ep.Join(sb, joinOptions...); err != nil {
        return "", fmt.Errorf("failed to join sandbox(%v)", err)
    }

    // update endpoint settings
    epInfo := ep.Info()
    if epInfo.Gateway() != nil {
        endpointConfig.Gateway = epInfo.Gateway().String()
    }
    if epInfo.GatewayIPv6().To16() != nil {
        endpointConfig.IPV6Gateway = epInfo.GatewayIPv6().String()
    }
    endpoint.ID = ep.ID()
    endpointConfig.EndpointID = ep.ID()
    endpointConfig.NetworkID = n.ID()
    iface := epInfo.Iface()
    ……
    return endpointName, nil

}

創建 endpoint 的整個過程,都是調用 libnetwork 來實現的。首先調用endpointOptions()來構建接口要求的EndpointOption參數,這個 setter 函數類型的參數能將不同的 option 傳遞給 network 和 endpoint 的接口。隨後調用 libnetwork 的
CreateEndpoint()接口來進行具體的構建。CreateEndpoint()執行的實際工作包括爲這個 endpoint 分配 IP 和接口(Iface),對應的配置會被應用到 Endpoint 中,其中包括 iptables 的配置規則和端口信息等。

Sandbox 所代表的就是 container 獨有的 network namespace,其創建也是基於 libnetwork。sandbox 裏面包含 container 建立網絡通信的標誌性信息,如 IP 地址、Mac 地址、路由和 DNS 等配置。會對已存在的 sandbox 進行遍歷,判斷是否存在相應的 sandbox,存在的話就直接返回對應的 sandbox。在 none 模式下,container 沿用主機的 namespace,返回的 sandbox 爲空,這時候會創建一個新的 sandbox。sandbox 的創建過程,就是調用 namespace 和 cgroup 來創建一個獨立 sandbox 空間。

將 endpoint 加入到 sandbox 的操作,實際上就是將網卡分配給 container 的過程,將 endpoint 分配到的網絡資源注入到 sandbox 中。網卡是建立連接的核心,container 通過虛擬網卡連接到 network,從而與其它 container 進行通信。

最後一步,將變化同步更新到 endpoint 的配置裏面。

4. 總結

回顧建立 network 連接的整個流程,可以簡單的分成幾步。container 在通信時需要唯一的 network namespace 來標誌自己,那麼就要有 sandbox 的創建;通信的實現需要網卡作爲基礎,那麼就要有 endpoint 的創建;最後將endpoint  加入 sandbox,建立容器間通信的基礎,連接的建立就成功完成了。

如果想更多瞭解 PouchContainer,請訪問 https://pouchcontainer.io




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章