繼上期“智匯華云”邀請到華云數據容器組資深OpenStack開發(fā)工程師郭棟為您帶來“ Kubernetes網絡詳解(一) Flannel基本原理”之后, 本期“智匯華云”為大家?guī)鞬ubernetes網絡詳解的系列內容——Kubernetes網絡詳解(二) CNI原理與實現。
1、CNI概述
本文中 Kubernetes版本是v1.16.3。
CNI的全稱是Container Network Interface,屬于CNCF的一個子項目,它是一個完整的規(guī)范,詳見 https://github.com/containernetworking/cni/blob/master/SPEC.md
本文 會在一個實際的環(huán)境中講解其基本原理,并給出一些直觀的例子。
Kubernetes自身在創(chuàng)建和刪除pod的時候不涉及網絡相關的操作,它會把這些交給CNI插件來完成。具體來講就是當Kubernetes分別在創(chuàng)建一個pod的時候和刪除一個pod的時候,對應的cni插件要做哪些操作。
2、CNI的入口
Kubernetes中創(chuàng)建pod的實際操作是 由node節(jié)點上的kubelet進行來完成的,它有兩個重要的cni相關的命令行參數
- `cni-conf-dir`
這個參數指定了 cni配置文件的目錄默認是 `/etc/cni/net.d`
- `cni-bin-dir`
這個參數指定了 cni插件的可執(zhí)行文件所在的目錄 `/opt/cni/bin`
CNI插件會以二進制可執(zhí)行文件的形式提供,存放于 `cni-bin-dir`這個目錄下,kubelet會根據cni配置文件來決定具體調用哪些插件。
```
[root@node1 ~]# ls -l /opt/cni/bin/
total 36132
```
在本文中我們會詳細分析 幾個基礎的cni plugin。
3、flannel cni plugin
在上一篇文章中,我們 以flannel為例,講解了其基本原理,現在接著分析cni的部分。
flannel的DaemonSet的啟動前會 將一個ConfigMap中的內容copy成一個cni配置文件
```
[root@node1 ~]# cat /etc/cni/net.d/10-flannel.conflist
{
"cniVersion": "0.2.0",
"name": "cbr0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
```
配置文件中的plugins參數指定了需要調用的plugin列表和對應的配置,本文中我們只關心基礎的plugin,因此這里會暫時忽略portmap這個plugin。
plugin中的type參數指定具體的plugin二進制文件名稱,因此kubelet會調用 `/opt/cni/bin/flannel`,代碼位于 `https://github.com/containernetworking/plugins/tree/master/plugins/meta/flannel`
```
func main {
skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO")
}
```
這個main就是flannel cni plugin的入口, 當創(chuàng)建和刪除pod時會分別調用其中的 `cmdAdd`和 `cmdDel`函數來執(zhí)行對應的操作 。
```
func cmdAdd(args *skel.CmdArgs) error {
n, err := loadFlannelNetConf(args.StdinData)
if err != nil {
return err
}
fenv, err := loadFlannelSubnetEnv(n.SubnetFile)
if err != nil {
return err
}
if n.Delegate == nil {
n.Delegate = make(map[string]interface{})
} else {
if hasKey(n.Delegate, "type") && !isString(n.Delegate["type"]) {
return fmt.Errorf("'delegate' dictionary, if present, must have (string) 'type' field")
}
if hasKey(n.Delegate, "name") {
return fmt.Errorf("'delegate' dictionary must not have 'name' field, it'll be set by flannel")
}
if hasKey(n.Delegate, "ipam") {
return fmt.Errorf("'delegate' dictionary must not have 'ipam' field, it'll be set by flannel")
}
}
return doCmdAdd(args, n, fenv)
}
```
`cmdAdd`中主要有 三個步驟
1. 從stdin讀取配置,生成一個 `type NetConf struct`對象
2. 從第1步返回的 `NetConf.SubnetFile`指定的路徑(默認是 `/run/flannel/subnet.env`)載入一個配置文件, 這個文件由Flanneld DaemonSet生成,在上一篇文章中有說明
3. 調用 `doCmdAdd`函數執(zhí)行真正的操作
3.1、讀取NetConf配置
```
type NetConf struct {
types.NetConf
SubnetFile string `json:"subnetFile"`
DataDir string `json:"dataDir"`
Delegate map[string]interface{} `json:"delegate"`
}
# types.NetConf結構體
// 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 IPAM `json:"ipam,omitempty"`
DNS DNS `json:"dns"`
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
PrevResult Result `json:"-"`
}
```
現在回頭看 cni的配置文件(忽略了portmap plugin)
```
{
"cniVersion": "0.2.0",
"name": "cbr0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
}
]
}
```
對應于flannel plugin中的 `NetConf`結構體 ,可以看出配置文件中的
```
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
```
對應于 `NetConf`中的 `Delegate map[string]interface{}`, 除了這個和name, type, version之外而其它值都為空或者默認。
3.2、讀取flanneld生成的配置文件
在node1上 `/run/flannel/subnet.env`這個文件的內容如下
```
[root@node1 ~]# cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.1.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
[root@node1 ~]#
```
3.3、執(zhí)行doCmdAdd
這是 flannel cni plugin的核心
```
func doCmdAdd(args *skel.CmdArgs, n *NetConf, fenv *subnetEnv) error {
n.Delegate["name"] = n.Name
if !hasKey(n.Delegate, "type") {
n.Delegate["type"] = "bridge"
}
if !hasKey(n.Delegate, "ipMasq") {
// if flannel is not doing ipmasq, we should
ipmasq := !*fenv.ipmasq
n.Delegate["ipMasq"] = ipmasq
}
if !hasKey(n.Delegate, "mtu") {
mtu := fenv.mtu
n.Delegate["mtu"] = mtu
}
if n.Delegate["type"].(string) == "bridge" {
if !hasKey(n.Delegate, "isGateway") {
n.Delegate["isGateway"] = true
}
}
if n.CNIVersion != "" {
n.Delegate["cniVersion"] = n.CNIVersion
}
n.Delegate["ipam"] = map[string]interface{}{
"type": "host-local",
"subnet": fenv.sn.String,
"routes": []types.Route{
{
Dst: *fenv.nw,
},
},
}
return delegateAdd(args.ContainerID, n.DataDir, n.Delegate)
}
```
主要完成的工作如下
- 將下一級調用的的 cni plugin設置為 `bridge`
- 設置ipMasq和mtu, 注意這里cni plugin最終的ipMasq和flanneld自身的是相反的,也就是說如果flanneld設置了,cni plugin就不再設置了
- 如果是 `bridge`,且沒有設置isGateway的話 ,將其默認設置為true
- 設置ipam cni plugin為host-local,并且將subnet參數設置為上文 `/run/flannel/subnet.env`中的 `FLANNEL_SUBNET`,并設置路由
從這里可以 看出flannel cni plugin的核心邏輯就根據當前配置生成bridge和host-local這兩個cni plugin的配置參數,隨后通過調用它們來實現主要的功能。
host-local cni plugin的功能是在當前節(jié)點從一個subnet中給pod分配ip地址,詳細邏輯可閱讀其代碼來理解。 這個cni plugin會被bridge cni plugin調用。
下面簡述一下 bridge的實現。
4、bridge cni plugin
bridge插件負責 從pod到veth到cni0的整個流程。
核心功能同樣 在cmdAdd和cmdDel這兩個函數中,下面看cmdAdd中的邏輯。
總體上分為二層和三層兩部分
4.1、二層處理
第一步會先 處理網橋設備
```
br, brInterface, err := setupBridge(n)
if err != nil {
return err
}
```
`setupBridge`負責創(chuàng)建cni0這個bridge,并配置它的mtu、混雜模式等等,最后將這個網橋設備up起來。詳細的代碼如下
```
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
// create bridge if necessary
br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode)
if err != nil {
return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
}
...
}
func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) {
br := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: brName,
MTU: mtu,
TxQLen: -1,
},
}
err := netlink.LinkAdd(br)
...
if promiscMode {
...
if err := netlink.LinkSetUp(br); err != nil {
...
}
```
第二步會 處理veth pair接口
```
netns, err := ns.GetNS(args.Netns)
if err != nil {
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
}
defer netns.Close
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
if err != nil {
return err
}
```
`setupVeth`函數會在pod所在的network namespace中創(chuàng)建一對veth接口,并將其中的一端移到host中,然后設置它的mac地址,并將其掛載到cni0這個網橋上,如果需要的話還會設置這個接口的hairpin模式。代碼如下
```
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
contIface := ¤t.Interface{}
hostIface := ¤t.Interface{}
err := netns.Do(func(hostNS ns.NetNS) error {
// create the veth pair in the container and move host end into host netns
hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
if err != nil {
return err
}
contIface.Name = containerVeth.Name
contIface.Mac = containerVeth.HardwareAddr.String
contIface.Sandbox = netns.Path
hostIface.Name = hostVeth.Name
return nil
})
if err != nil {
return nil, nil, err
}
// need to lookup hostVeth again as its index has changed during ns move
hostVeth, err := netlink.LinkByName(hostIface.Name)
if err != nil {
return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
}
hostIface.Mac = hostVeth.Attrs.HardwareAddr.String
// connect host veth end to the bridge
if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs.Name, br.Attrs.Name, err)
}
// set hairpin mode
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs.Name, err)
}
return hostIface, contIface, nil
}
```
4.2、三層處理
```
isLayer3 := n.IPAM.Type != ""
...
if isLayer3 {
..
}
```
三層網絡是否需要處理取決于ipam cni plugin是否已經配置,在我們的環(huán)境中, 這個字段已經由flannel cni plugin配置成host-local了,因此這里需要處理三層的邏輯。處理的詳細邏輯如下
```
// run the IPAM plugin and get back the config to apply
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
if err != nil {
return err
}
// release IP in case of failure
defer func {
if !success {
os.Setenv("CNI_COMMAND", "DEL")
ipam.ExecDel(n.IPAM.Type, args.StdinData)
os.Setenv("CNI_COMMAND", "ADD")
}
}
// Convert whatever the IPAM result was into the current Result type
ipamResult, err := current.NewResultFromResult(r)
if err != nil {
return err
}
result.IPs = ipamResult.IPs
result.Routes = ipamResult.Routes
if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
```
這里bridge plugin 會調用host-local這個ipmi plugin來為當前處理的pod分配一個ip地址。
```
// Gather gateway information for each IP family
gwsV4, gwsV6, err := calcGateways(result, n)
if err != nil {
return err
}
```
接著會 根據這個地址計算出bridge設備cni0的ip地址。
```
// Configure the container hardware address and IP address(es)
if err := netns.Do(func(_ ns.NetNS) error {
contVeth, err := net.InterfaceByName(args.IfName)
...
// Add the IP to the interface
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
return err
}
// Send a gratuitous arp
for _, ipc := range result.IPs {
if ipc.Version == "4" {
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
}
}
return nil
}); err != nil {
return err
}
```
這段代碼的含義是 在pod所在的network namespace中配置之前添加進來的那個veth接口,包括將設備設置為up狀態(tài),配置ip地址和路由信息,最后發(fā)送gratuitous arp廣播。
```
if n.IsGW {
var firstV4Addr net.IP
// Set the IP address(es) on the bridge and enable forwarding
for _, gws := range []*gwInfo{gwsV4, gwsV6} {
for _, gw := range gws.gws {
if gw.IP.To4 != nil && firstV4Addr == nil {
firstV4Addr = gw.IP
}
err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
if err != nil {
return fmt.Errorf("failed to set bridge addr: %v", err)
}
}
if gws.gws != nil {
if err = enableIPForward(gws.family); err != nil {
return fmt.Errorf("failed to enable forwarding: %v", err)
}
}
}
}
```
接下來會給cni0這個bridge配置ip地址,并執(zhí)行類似于 `echo 1 > /proc/sys/net/ipv4/ip_forward`的操作來啟用數據包轉發(fā)功能
```
if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
return err
}
}
}
```
最后如果cni plugin設置ipmasq則需要進行相關ipmasq相關的設置, 在當前環(huán)境中ipmasq是由Flanneld Daemon完成的,因此這里的cni plugin不會進行設置。
至此, 詳細分析了bridge cni plugin的cmAdd主要流程,其核心功能總結如下
- 新建并配置網橋設備cni0
- 在pod所在的namespace中創(chuàng)建一對veth接口,并 將其中的一端移到host中并將其掛載到網橋上
- 調用ipmi cni plugin給pod中的 一端veth接口分配ip地址并將其配置到接口上
- 給cni0配置ip地址并開啟數據包轉發(fā)功能
參考文獻
-https://github.com/containernetworking/cni/blob/master/SPEC.md
-https://github.com/containernetworking/cni
-https://github.com/containernetworking/plugins
責任編輯: