1. 程式人生 > >CNI, From A Developer's Perspective

CNI, From A Developer's Perspective

ortmap 需要 種類型 ndb 它的 管理系 rspec 避免 必須

1、為什麽會有CNI?

CNI是Container Network Interface的縮寫,簡答地說,其實就是一個標準的,通用的接口。已知我們現在有各種各樣的容器平臺:docker,kubernetes,mesos,我們也有各種各樣的容器網絡解決方案:flannel,calico,weave,況且還有各種新的解決方案層出不窮。如果每出現一個新的解決方案,我們都需要進行兩者的適配,那麽工作量必然是巨大的,而且也是重復和不必要的。事實上,我們只要提供一個標準的接口,更準確的說是一種協議,就能完美地解決上述問題。一旦有新的網絡方案出現時,只要它能滿足這個標準的協議,那麽它就能為同樣滿足該協議的所有容器平臺提供網絡功能,而CNI正是這樣的標準接口協議。

2、什麽是CNI?

通俗地講,CNI是一個接口協議,用於連接容器管理系統和網絡插件。前者提供一個容器所在的network namespace(從網絡的角度來看,network namespace和容器是完全等價的),後者負責將network interface插入該network namespace中(比如veth的一端)並且在宿主機做一些必要的配置(例如將veth的另一端加入bridge中),最後對namespace中的interface進行IP和路由的配置。那麽CNI的工作其實主要是從容器運行時處獲取運行時信息,包括network namespace的路徑,容器ID以及network interface name,再從容器網絡的配置文件中加載網絡配置信息,再將這些信息傳遞給插件,由插件進行具體的網絡配置,並將配置的結果再返回到容器管理系統中。

最後,需要註意的是,在之前的CNI版本中,網絡配置文件只能描述一個network,這也就表明了一個容器只能加入一個容器網絡。但是在後來的CNI版本中,我們可以在配置文件中定義一個所謂的NetworkList,事實上就是定義一個network序列,CNI會依次調用各個network的插件對容器進行相應的配置,從而允許一個容器能夠加入多個容器網絡。

3、怎麽用CNI?

在進一步探索CNI之前,我覺得首先來看看CNI是怎麽使用的,先對CNI有一個直觀的認識,是很有必要的,這對我們之後的理解也將非常有幫助。接著我們將依次執行如下步驟來完成演示,我會對每一步的操作都進行必要的說明。

(1)、編譯安裝CNI的官方插件

現在官方提供了三種類型的插件:main,meta和ipam。其中main類型的插件主要提供某種網絡功能,比如我們在示例中將使用的brdige,以及loopback,ipvlan,macvlan等等。meta類型的插件不能作為獨立的插件使用,它通常需要調用其他插件,例如flannel,或者配合其他插件使用,例如portmap。最後ipam類型的插件其實是對所有CNI插件共有的IP管理部分的抽象,從而減少插件編寫過程中的重復工作,官方提供的有dhcp和host-local兩種類型。

接著執行如下命令,完成插件的下載,編譯,安裝工作:

$ mkdir -p $GOPATH/src/github.com/containernetworking/plugins

$ git clone https://github.com/containernetworking/plugins.git  $GOPATH/src/github.com/containernetworking/plugins

$ cd $GOPATH/src/github.com/containernetworking/plugins

$ ./build.sh

  

最終所有的插件都將以可執行文件的形式存在在目錄$GOPATH/src/github.com/containernetworking/plugins/bin之下。

(2)、創建配置文件,對所創建的網絡進行描述

工作目錄"/etc/cni/net.d"是CNI默認的網絡配置文件目錄,當沒有特別指定時,CNI就會默認對該文件進行查找,從中加載配置文件進行容器網絡的創建。至於對配置文件各個字段的詳細描述,我將在後續章節進行說明。

現在我們只需要執行如下命令,描述一個我們想要創建的容器網絡"mynet"即可。為了簡單起見,我們的NetworkList中僅僅只有"mynet"這一個network。

$ mkdir -p /etc/cni/net.d

$ cat >/etc/cni/net.d/10-mynet.conflist <<EOF
{
        "cniVersion": "0.3.0",
        "name": "mynet",
        "plugins": [
          {
                "type": "bridge",
                "bridge": "cni0",
                "isGateway": true,
                "ipMasq": true,
                "ipam": {
                        "type": "host-local",
                        "subnet": "10.22.0.0/16",
                        "routes": [
                                { "dst": "0.0.0.0/0" }
                        ]
                }
          }
        ]
}
EOF

$ cat >/etc/cni/net.d/99-loopback.conf <<EOF
{
	"cniVersion": "0.3.0",
	"type": "loopback"
}
EOF

  

(3)、模擬CNI的執行過程,創建network namespace,加入上文中描述的容器網絡"mynet"

首先我們從github上下載、編譯CNI的源碼,最終將在bin目錄下生成一個名為"cnitool"的可執行文件。事實上,可以認為cnitool是一個模擬程序,我們先創建一個名為ns的network namespace,用來模擬一個新創建的容器,再調用cnitool對該network namespace進行網絡配置,從而模擬一個新建的容器加入一個容器網絡的過程。

從cnitool的執行結果來看,它會返回一個包含了interface,IP,路由等等各種信息的json串,事實上它正是CNI對容器進行網絡配置後生成的結果信息,對此我們將在後續章節進行詳細的描述。

最終,我們可以看到network namespace內新建的網卡eth0的IP地址為10.22.0.5/16,正好包含在容器網絡"mynet"的子網範圍10.22.0.0/16之內,因此我們可以認為容器已經成功加入了容器網絡之中。

$ git clone https://github.com/containernetworking/cni.git $GOPATH/src/github.com/containernetworking/cni

$ cd $GOPATH/src/github.com/containernetworking/cni

$ ./build.sh

$ cd $GOPATH/src/github.com/containernetworking/cni/bin

$ export CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin

$ ip netns add ns $ ./cnitool add mynet /var/run/netns/ns { "cniVersion": "0.3.0", "interfaces": [ { "name": "cni0", "mac": "0a:58:0a:16:00:01" }, { "name": "vetha418f787", "mac": "c6:e3:e9:1c:2f:20" }, { "name": "eth0", "mac": "0a:58:0a:16:00:05", "sandbox": "/var/run/netns/ns" } ], "ips": [ { "version": "4", "interface": 2, "address": "10.22.0.5/16", "gateway": "10.22.0.1" } ], "routes": [ { "dst": "0.0.0.0/0" } ], "dns": {} } $ ip netns exec ns ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.22.0.5 netmask 255.255.0.0 broadcast 0.0.0.0 inet6 fe80::646e:89ff:fea6:f9b5 prefixlen 64 scopeid 0x20<link> ether 0a:58:0a:16:00:05 txqueuelen 0 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 8 bytes 648 (648.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

  

4、怎麽配CNI?

從上文中我們可以知道,CNI只支持三種操作:ADD, DEL,VERSION,而三種操作所需要配置的參數和結果如下:

  • 將container加入network(Add):
    • Parameters:
      • Version. 調用者使用的CNI 配置的版本信息
      • Container ID. 這個字段是可選的,但是建議使用,在容器活著的時候要求該字段全局唯一的。比如,存在IPAM的環境可能會要求每個container都分配一個獨立的ID,這樣每一個IP的分配都能和一個特定的容器相關聯。例如,在appc implementations中,container ID其實就是pod ID
      • Network namespace path. 這個字段表示要加入的network namespace的路徑。例如,/proc/[pid]/ns/net或者對於該目錄的bind-mount/link。
      • Network configuration. 這是一個JSON文件用於描述container可以加入的network,具體內容在下文中描述
      • Extra arguments. 該字段提供了可選的機制,從而允許基於每個容器進行CNI插件的簡單配置
      • Name of the interface inside the container. 該字段提供了在container (network namespace)中的interface的名字;因此,它也必須符合Linux對於網絡命名的限制
    • Result:
      • Interface list. 根據插件的不同,這個字段可以包括sandbox (container or hypervisor) interface的name,以及host interface的name,每個interface的hardware address,以及interface所在的sandbox(如果存在的話)的信息。
      • IP configuration assigned to each interface. IPv4和/或者IPv6地址,gateways以及為sandbox或host interfaces中添加的路由
      • DNS inormation. 包含nameservers,domains,search domains和options的DNS information的字典
  • 將container從network中刪除(Delete):
    • Parameter:
      • Version. 調用者使用的CNI 配置的版本信息
      • ContainerID. 定義同上
      • Network namespace path. 定義同上
      • Network configuration. 定義同上
      • Extra argument. 定義同上
      • Name of the interface inside the container. 定義同上
  • 版本信息
    • Parameter: 無
    • Result: 返回插件支持的所有CNI版本  

在上文的敘述中我們省略了對Network configuration的描述。事實上,它的內容和上文演示實例中的"/etc/cni/net.d/10-mynet.conf"網絡配置文件是一致的,用於描述容器了容器需要加入的網絡,下面是對其中一些重要字段的描述:

  • cniVersion(string):cniVersion以Semantic Version 2.0的格式指定了插件使用的CNI版本
  • name (string):Network name。這應該在整個管理域中都是唯一的
  • type (string):代表了CNI插件可執行文件的文件名
  • args (dictionary):由容器運行時提供的可選的參數。比如,可以將一個由label組成的dictionary傳遞給CNI插件,通過在args下增加一個labels字段
  • ipMasqs (boolean):可選項(如果插件支持的話)。為network在宿主機創建IP masquerade。這個字段是必須的,如果需要將宿主機作為網關,從而能夠路由到容器分配的IP
  • ipam:由特定的IPAM值組成的dictionary
    • type (string):表示IPAM插件的可執行文件的文件名
  • dns:由特定的DNS值組成的dictionary
    • nameservers (list of strings):一系列對network可見的,以優先級順序排列的DNS nameserver列表。列表中的每一項都包含了一個IPv4或者一個IPv6地址
    • domain (string):用於查找short hostname的本地域
    • search (list of strings):以優先級順序排列的用於查找short domain的查找域。對於大多數resolver,它的優先級比domain更高
    • options(list of strings):一系列可以被傳輸給resolver的可選項

插件可能會定義它們自己能接收的額外的字段,但是遇到一個未知的字段可能會產生錯誤。例外的是args字段,它可以被用於傳輸一些額外的字段,但可能會被插件忽略

5、CNI具體是怎麽實現的?

到目前為止,我們對CNI的使用、配置和原理都已經有了基本的認識,所以也是時候從源碼的角度來對CNI做一個透徹的理解了。下面,我將以上文中的演示實例作為線索,以模擬程序cnitool作為切入口,來對整個CNI的執行過程進行詳盡的分析。

(1)、加載容器網絡配置信息

首先我們來看一下容器網絡配置的數據結構表示:

type NetworkConfigList struct {
	Name       string
	CNIVersion string
	Plugins    []*NetworkConfig
	Bytes      []byte
}

type NetworkConfig struct {
	Network *types.NetConf
	Bytes   []byte
}

// NetConf describes a network.
type NetConf struct {
	CNIVersion string `json:"cniVersion,omitempty"`

	Name         string          `json:"name,omitempty"`
	Type         string          `json:"type,omitempty"`
	Capabilities map[string]bool `json:"capabilities,omitempty"`
	IPAM         struct {
		Type string `json:"type,omitempty"`
	} `json:"ipam,omitempty"`
	DNS DNS `json:"dns"`
}

  

經過粗略的分析之後,我們可以發現,數據結構表示的內容和演示實例中的json配置文件基本是一致的。因此,這一步的源碼實現很簡單,基本流程如下:

  • 首先確定配置文件netdir所在的目錄,如果沒有特別指定,則默認為"/etc/cni/net.d"
  • 調用netconf, err := libcni.LoadConfList(netdir, os.Args[2]),該函數首先會查找netdir中是否有以".conflist"作為後綴的目錄,如果有,且配置信息中的"Name"和參數os.Args[2]一致,則直接用配置信息填充並返回NetConfigList即可。否則,查找是否存在以".conf"或".json"作為後綴的配置文件。同樣,如果存在"Name"一致的配置,則加載該配置文件。由於".conf"或".json"中都是單個的網絡配置,因此需要將其包裝成僅有一個NetConf的NetworkConfigList再返回。到此位置,容器網絡配置加載完成。

(2)、配置容器運行時信息

同樣,我們先來看一下容器運行時信息的數據結構:

type RuntimeConf struct {
	ContainerID string
	NetNS       string
	IfName      string
	Args        [][2]string
	// A dictionary of capability-specific data passed by the runtime
	// to plugins as top-level keys in the ‘runtimeConfig‘ dictionary
	// of the plugin‘s stdin data.  libcni will ensure that only keys
	// in this map which match the capabilities of the plugin are passed
	// to the plugin
	CapabilityArgs map[string]interface{}
}

  

其中最重要的字段無疑是"NetNS",它指定了需要加入容器網絡的network namespace路徑。而Args字段和CapabilityArgs字段都是可選的,用於傳遞額外的配置信息。具體的內容參見上文中的配置說明。從上文的演示實例中,我們並沒有對Args和CapabilityArgs進行任何的配置,為了簡單起見,我們可以直接認為它們為空。因此,cnitool對RuntimeConf的配置也就極為簡單了,只需要將參數指定的netns賦值給NetNS字段,而ContainerID和IfName字段隨意賦值即可,默認將它們分別賦值為"cni"和"eth0",具體代碼如下:

rt := &libcni.RuntimeConf{
	ContainerID:    "cni",
	NetNS:          netns,
	IfName:         "eth0",
	Args:           cniArgs,
	CapabilityArgs: capabilityArgs,
}

  

(3)、加入容器網絡

根據加載的容器網絡配置信息和容器運行時信息,執行加入容器網絡的操作,並將執行的結果打印輸出

switch os.Args[1] {
case CmdAdd:
	result, err := cninet.AddNetworkList(netconf, rt)
	if result != nil {
		_ = result.Print()
	}
	exit(err)
     ....
}

接下來我們進入AddNetworkList函數中

// AddNetworkList executes a sequence of plugins with the ADD command
func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
	var prevResult types.Result
	for _, net := range list.Plugins {
		pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
                .....
		newConf, err := buildOneConfig(list, net, prevResult, rt)
                ......
		prevResult, err = invoke.ExecPluginWithResult(pluginPath, newConf.Bytes, c.args("ADD", rt))
                ......
	}

	return prevResult, nil
}

  

從函數上方的註釋我們就可以了解到,該函數的作用就是按順序對NetworkList中的各個network執行ADD操作。該函數的執行過程也非常清晰,利用一個循環遍歷NetworkList中的各個network,對每個network的操作分為如下三個步驟:

  • 首先,調用FindInPath函數,根據newtork的類型,在插件的存放路徑,也就是上文中的CNI_PATH中查找是否存在對應插件的可執行文件。若存在則返回其絕對路徑pluginPath
  • 接著,調用buildOneConfig函數,從NetworkList中提取分離出當前執行ADD操作的network的NetConfig結構。這裏特別需要註意的是preResult參數,它是上一個network的操作結果,也將被編碼進NetworkConfig中。從上文中已知,當我們在執行NetworkList時,必須將前一個network的操作結果作為參數傳遞給當前進行操作的network。並且在buildOneConfig函數構建每個NetworkConfig時會默認將其中的"name"和"cniVersion"和NetworkList中的配置保持一致,從而避免沖突。
  • 最後,調用invoke.ExecPluginWithResult(pluginPath, netConf.Bytes, c.args("ADD", rt))真正執行network的ADD操作。這裏我們需要註意的是netConf.Bytes和c.args("ADD", rt)這兩個參數。其中netConf.Bytes用於存放NetworkConfig中的NetConf結構編碼以及例如上文中的prevResult進行json編碼形成的字節流。而c.args()函數用於構建一個Args類型的實例,其中主要存儲容器運行時信息,以及執行的CNI操作的信息,例如"ADD"或"DEL",和插件的存儲路徑。

事實上ExecPluginWithResult僅僅是一個包裝函數,它僅僅只是調用了函數defaultPluginExec.WithResult(pluginPath, netconf, args)之後,就直接返回了。

func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
	stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
        .....
	// Plugin must return result in same version as specified in netconf
	versionDecoder := &version.ConfigDecoder{}
	confVersion, err := versionDecoder.Decode(netconf)
        ....
	return version.NewResult(confVersion, stdoutBytes)
}

  

可以看得出WithResult函數的執行流同樣也是非常清晰的,同樣也可以分為以下三步執行:

  • 首先調用e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())函數執行具體的CNI操作,對於它的具體內容,我們將在下文進行分析。此處需要註意的是它的第三個參數args.AsEnv(),該函數做的工作其實就是以獲取已有的環境變量,並且將args內的信息,例如CNI操作命令,同樣以環境變量的形式保存起來,例如"CNI_COMMAND=ADD"的形式傳輸給插件。由此我們可以知道,容器運行時信息、CNI操作命令以及插件存儲路徑都是以環境變量的形式傳遞給插件的
  • 接著調用versionDecoder.Decode(netconf)從network配置中解析出CNI版本信息
  • 最後,調用version.NewResult(confVersion, stdoutBytes),根據CNI版本,構建相應的返回結果

最後,我們來看看e.RawExecPlugin函數是如何操作的,代碼如下所示:

func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
	stdout := &bytes.Buffer{}

	c := exec.Cmd{
		Env:    environ,
		Path:   pluginPath,
		Args:   []string{pluginPath},
		Stdin:  bytes.NewBuffer(stdinData),
		Stdout: stdout,
		Stderr: e.Stderr,
	}
	if err := c.Run(); err != nil {
		return nil, pluginErr(err, stdout.Bytes())
	}

	return stdout.Bytes(), nil
}

  

不過看過代碼之後,我們可能要失望了。因為這個理論上最為核心的函數出乎意料的簡單,它所做的工作僅僅只是exec了插件的可執行文件。話雖如此,我們仍然有以下幾點需要註意:

  • 容器運行時信息以及CNI操作命令等都是以環境變量的形式傳遞給插件的,這點在上文中已經有所提及
  • 容器網絡的配置信息是通過標準輸入的形式傳遞給插件的
  • 插件的運行結果是以標準輸出的形式返回給CNI的

到此為止,整個CNI的執行流已經非常清楚了。簡單地說,一個CNI插件就是一個可執行文件,我們從配置文件中獲取network配置信息,從容器管理系統處獲取運行時信息,再將前者以標準輸入的形式,後者以環境變量的形式傳遞傳遞給插件,最終以配置文件中定義的順序依次調用各個插件,並且將前一個插件的執行結果包含在network配置信息中傳遞給下一個執行的插件,整個過程就是這樣。鑒於篇幅所限,僅僅只分析了CNI的ADD操作,不過相信有了上文的基礎之後,再理解另外的DEL操作也不會太難。

  

CNI, From A Developer's Perspective