背景
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网络模型。