背景
CNI 这东西搞容器的运维想必都不陌生,要知道,kubernetes的设计之初是不包含网络的,网络这玩意每家公司有每家公司自己的玩法,对各个公司来说,没有那种大一统的完美方案,只有最适合自己的方案,所以,kubernetes在设计的时候,没有设计统一的网络方案,只提供了统一的容器网络接口,Container Network Interface,也就是所谓的CNI。
CNI 和 IPAM
一般说的CNI都是包含IPAM的,但其实2个功能是分开实现的。
CNI用于实现网络构建(network部分,网络接口创建,VLAN划分,端口打开关闭等等)。
IPAM用于实现IP地址,DNS,网关信息的分配。
这两者是可以*组合的,比如你可以用flannel的CNI,IPAM却用DHCP或者HOSTLOCAL,甚至如果不想用IPAM的接口,自己在CNI里也可以实现对于的代码逻辑。
CNI创建流程
创建流程可以参考官方的源码,以官方的bridge的CNI代码进行分析,在一个POD的生命周期中,CNI主要有3个方法进行调用,分别是:
- cmdadd
- cmdcheck
- cmddel
func main() { skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge")) }
看方法名字大概能猜到干了啥吧,创建容器调用cmdadd, 销毁cmddel, cmdcheck是0.4.0之后新加的,比如粗暴的del,所以在del前会做一些check,check内容可以根据需要自己定义.
先画一下基本的创建流程
graph TD; kubelet调用CNI-->获取创建基本参数; 获取创建基本参数-->解析参数; 解析参数-->获取ns,podname; 获取ns,podname-->解析net.d下配置文件; 解析net.d下配置文件-->netlink配置端口; netlink配置端口-->调用ipam; 调用ipam-->分配IP,DNS,GW; 分配IP,DNS,GW-->传值CNI; 传值CNI-->配置IP; 配置IP-->返回kubelet;
大致流程如上,每个CNI插件基本都会遵循以上流程进行网络的创建,只是不同插件会在上述流程上添加不同的动作,比如bridge会基于netlink创建网桥,vlan会分vlanID等等
代码分析(以bridge为例):
解析参数:cni创建网络一般会从2个渠道获取参数,一个是创建时master传过来的args.Args,是个很长的字符串,一般会包含namespace信息,podname信息等,一个是配置文件传入的args.StdinData,基于配置文件,结构体如下:
type NetConf struct { types.NetConf BrName string `json:"bridge"` IsGW bool `json:"isGateway"` IsDefaultGW bool `json:"isDefaultGateway"` ForceAddress bool `json:"forceAddress"` IPMasq bool `json:"ipMasq"` MTU int `json:"mtu"` HairpinMode bool `json:"hairpinMode"` PromiscMode bool `json:"promiscMode"` Vlan int `json:"vlan"` }
对比配置文件:
{ "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "isDefaultGateway": true, "forceAddress": false, "ipMasq": true, "hairpinMode": true, "ipam": { "type": "host-local", "subnet": "10.10.0.0/16" }}
再看一下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:"-"` } type IPAM struct { Type string `json:"type,omitempty"` } // DNS contains values interesting for DNS resolvers type DNS struct { Nameservers []string `json:"nameservers,omitempty"` Domain string `json:"domain,omitempty"` Search []string `json:"search,omitempty"` Options []string `json:"options,omitempty"` } // Result is an interface that provides the result of plugin execution type Result interface { // The highest CNI specification result version the result supports // without having to convert Version() string // Returns the result converted into the requested CNI specification // result version, or an error if conversion failed GetAsVersion(version string) (Result, error) // Prints the result in JSON format to stdout Print() error // Prints the result in JSON format to provided writer PrintTo(writer io.Writer) error }
其中类似IsGW,IPMasq之类的均是自定义的变量,暂时可以忽略,主要需要注意的是cniVersion,name,IPAM之类的通用变量.
创建网络:以bridge为例,基本都是调用netlink 进行创建
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) { vlanFiltering := false if n.Vlan != 0 { vlanFiltering = true } // create bridge if necessary br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode, vlanFiltering) if err != nil { return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err) } return br, ¤t.Interface{ Name: br.Attrs().Name, Mac: br.Attrs().HardwareAddr.String(), }, nil }
IP分配: ip分配基于ipam进行分配,和cni 一样,同样有cmdadd, cmddel, cmdcheck等方法,ipam之后再分析,这边只要知道ipam分配出IP即可。
// 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 { ipam.ExecDel(n.IPAM.Type, args.StdinData) } }()
IP配置: CNI的工作除了在Node节点上创建适配的网络以外,主要还要在pod所在的pasue container上进行IP的分配,DNS以及路由的配置,由于pause container 享有独立的namespace,所以需要需要在指定的ns下进行IP配置, 官方提供了netns .Do 可以帮助进行操作
pod 由于需要经常销毁,创建,对于网络来说arp刷新很频繁,一般交换机都有mac 表的老化时间,所以在配置IP后,使用arping 手动刷新arp 表。
// Send a gratuitous arp if err := netns.Do(func(_ ns.NetNS) error { contVeth, err := net.InterfaceByName(args.IfName) if err != nil { return err } for _, ipc := range result.IPs { if ipc.Version == "4" { _ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth) } } return nil }); err != nil { return err }
所以编写一个自定义的CNI并不困难,遵循基本的套路,然后再套路里加上自己的业务逻辑,就可以实现符合自己业务需求的K8S网络模型。