写点什么

当 Kubernetes 遇见 Macvlan——实现 CNI 路由插件

作者:陆云
  • 2022-11-22
    上海
  • 本文字数:4603 字

    阅读完需:约 15 分钟

当Kubernetes遇见Macvlan——实现CNI路由插件

实验背景

上次做了一个基于Macvlan的Kubernetes网络方案,Macvlan 插件执行以后,Pod 和 Host 网络还没有互通。于是我们创建了一个 Bridge 模式的 Macvlan 子设备,手写从 Bridge 到 Pod 的路由规则,使得 Pod 和 Host 网络互通。现在我们实现一个 CNI 插件,通过链式执行自动完成上面的事情。

创建项目

CNI 插件使用 golang 开发,我们随便创建一个文件夹,在其中go mod init <module>一下就可以了。下面是我的项目结构。


.├── go.mod├── go.sum ├── main.go├── Makefile└── net.d    └── 00-default.conflist
复制代码


go.mod来看,我们的项目只依赖两个外部模块,一个是 CNI 插件框架,一个用来操作 Linux 网络设备。


...require (    // CNI插件框架    github.com/containernetworking/cni v1.0.1    // 操作Linux网络设备    github.com/vishvananda/netlink v1.1.0)...
复制代码


00-default.conflist是我们的 CNI 插件配置,最后会放到/etc/cni/net.d下面。


{    "cniVersion": "0.4.0",    "name": "default",    "plugins": [        {            "type": "macvlan",            "master": "eno1",            "ipam": {                "type": "host-local",                "ranges": [                    [                        {                            "subnet": "192.168.0.0/16",                            "rangeStart": "192.168.1.2",                            "rangeEnd": "192.168.1.254",                            "gateway": "192.168.0.1"                        }                    ]                ],                "routes": [                    {"dst": "0.0.0.0/0"}                ]            }        },        {            "type": "route",            "master": "eno1",            "bridge": "eno1.host"        }    ]}
复制代码

代码实现

这里是我们用到的的所有 go module。


import (    "encoding/json"    "net"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/040" "github.com/containernetworking/cni/pkg/version" "github.com/vishvananda/netlink")
复制代码


首先我们需要声明一个结构体,用来解析我们自定义插件的配置,以及接收 macvlan 插件的执行结果。


type PluginConfig struct {    // 组合代替继承    types.NetConf
Master string `json:"master"` Bridge string `json:"bridge"` MasterIndex int `json:"-"` BridgeIndex int `json:"-"`}
复制代码


每次调用 CNI 插件的 ADD 和 DEL 方法时,会通过 stdin 传入插件配置和上一步的执行结果,我们实现一个 parseConfig 方法解析传入的内容。


func parseConfig(stdin []byte) (*PluginConfig, error) {    var config PluginConfig
// 解析当前插件的配置 if err := json.Unmarshal(stdin, &config); err != nil { return nil, err }
// 解析上一步执行结果 if err := version.ParsePrevResult(&config.NetConf); err != nil { return nil, err }
return &config, nil}
复制代码


然后实现一个 initBridge 方法,在 Host 网络空间创建 Bridge 模式的 Macvlan 子设备。


func initBridge(config *PluginConfig) error {    // 找到macvlan主设备的索引    masterLink, err := netlink.LinkByName(config.Master)    if err != nil {        return err    }    config.MasterIndex = masterLink.Attrs().Index
// 检查当前是否存在bridge bridgeLink, err := netlink.LinkByName(config.Bridge) if err == nil { config.BridgeIndex = bridgeLink.Attrs().Index return nil } if _, ok := err.(netlink.LinkNotFoundError); !ok { return err } // 创建bridge模式的macvlan子设备 if err := netlink.LinkAdd(&netlink.Macvlan{ LinkAttrs: netlink.LinkAttrs{ Name: config.Bridge, ParentIndex: config.MasterIndex, }, Mode: netlink.MACVLAN_MODE_BRIDGE, }); err != nil { return err }
// 找到创建的bridge并启动它 bridgeLink, err = netlink.LinkByName(config.Bridge) if err != nil { return err } if err := netlink.LinkSetUp(bridgeLink); err != nil { return err } config.BridgeIndex = bridgeLink.Attrs().Index return nil}
复制代码


实现 CNI 插件的 ADD 方法,根据 Pod IP 在 Host 上创建路由。


func cmdAdd(args *skel.CmdArgs) error {    // 解析插件配置和上一步的结果    config, err := parseConfig(args.StdinData)    if err != nil {        return err    }
// 获取上一步(macvlan插件)的执行结果 prevResult, err := types040.GetResult(config.PrevResult) if err != nil { return err }
// 检查或初始化macvlan bridge if err := initBridge(config); err != nil { return err }
// 获得Pod IP podIP := prevResult.IPs[0].Address podIP.Mask = net.CIDRMask(32, 32)
// 创建路由 route := &netlink.Route{ Dst: &podIP, Scope: netlink.SCOPE_LINK, LinkIndex: config.BridgeIndex, } if err := netlink.RouteAdd(route); err != nil { return err }
// 透传macvlan插件的执行结果 // 这里必须主动打印一下,否则执行会报错 return types.PrintResult(prevResult, config.CNIVersion)}
复制代码


实现 CNI 插件的 DEL 方法,Pod 被回收时删除路由。


func cmdDel(args *skel.CmdArgs) error {    // 解析插件配置和上一步的结果    config, err := parseConfig(args.StdinData)    if err != nil {        return err    }
// 获取ADD操作的的执行结果 prevResult, err := types040.GetResult(config.PrevResult) if err != nil { return err }
// 获得Pod IP podIP := prevResult.IPs[0].Address podIP.Mask = net.CIDRMask(32, 32)
// 删除路由 route := &netlink.Route{ Dst: &podIP, Scope: netlink.SCOPE_LINK, LinkIndex: config.BridgeIndex, } return netlink.RouteDel(route)}
复制代码


CNI 插件的 CHECK 方法不用实现,声明一下就可以。


func cmdCheck(args *skel.CmdArgs) error {    return nil}
复制代码


下面是 CNI 插件的执行入口,在 CNI 框架的支持下,一行代码足矣。


func main() {    skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "CNI route plugin 0.0.1")}
复制代码

功能测试

没有将插件应用到 Kubernetes 时,我们用cnitool这个工具对插件进行测试,可以现场装一个。


go install github.com/containernetworking/cni/cnitool
复制代码


构建 route 插件,放到/opt/cni/bin这个目录。


go build -o /opt/cni/bin/route main.go
复制代码


使用实验配置测试 ADD 功能。


$ NETCONFPATH=$PWD/net.d CNI_PATH=/opt/cni/bin cnitool add default /var/run/netns/testing
复制代码


CNI 插件链被成功执行,产生了一个 JSON 输出。这是 ADD 操作的执行结果,会被缓存到 CNI 工作目录(一般是/var/lib/cni)。


{    "cniVersion": "0.4.0",    "interfaces": [        {            "name": "eth0",            "mac": "ea:a8:70:cd:18:62",            "sandbox": "/var/run/netns/testing"        }    ],    "ips": [        {            "version": "4",            "interface": 0,            "address": "192.168.1.2/16",            "gateway": "192.168.0.1"        }    ],    "routes": [        {            "dst": "0.0.0.0/0"        }    ],    "dns": {}}
复制代码


我们在/var/lib/cni/networks/default下面可以看到 IPAM 分配的 Pod IP。


$ ls /var/lib/cni/networks/default192.168.1.2  last_reserved_ip.0  lock
# 上一个被分配的IP$ cat /var/lib/cni/networks/default/last_reserved_ip.0 192.168.1.2
复制代码


/var/lib/cni/results下可以看到 CNI ADD 的结果缓存,执行 DEL 操作时缓存的结果会被传入。


$ ls /var/lib/cni/resultsdefault-cnitool-77383ca0a0715733ca6f-eth0
$ cat /var/lib/cni/results/default-cnitool-77383ca0a0715733ca6f-eth0 | jq{ "kind": "cniCacheV1", "containerId": "cnitool-77383ca0a0715733ca6f", "config": ...(base64后的原始配置) "ifName": "eth0", "networkName": "default", "result": { ...(刚才那一大坨JSON执行结果) }}
复制代码


现在我们测试 DEL 功能。


$ NETCONFPATH=$PWD/net.d CNI_PATH=/opt/cni/bin cnitool del default /var/run/netns/testing
复制代码


虽然什么输出都没有,但是执行是成功的。这时你再去/var/lib/cni下面查看 ADD 的执行结果,很多文件已经被删除了。

集成测试

上一步我们已经将 route 插件放到了/opt/cni/bin目录,这里只要覆盖一下 Kubernetes 的 CNI 配置。


$ cp $PWD/net.d/00-default.conflist /etc/cni/net.d
复制代码


删除从前创建的 Macvlan Bridge。


ip link del eno1.host
复制代码


然后我们创建一个 Nginx Deployment,观察 Pod IP。


$ kubectl create deployment nginx --image nginx:stable-alpine deployment.apps/nginx created
$ kubectl get pods -o wideNAME READY STATUS RESTARTS AGE IP NODE nginx-7bd849c599-vkppk 1/1 Running 0 28s 192.168.1.3 lyr620
复制代码


测试 Pod 到 Host 的网络连通性。


$ ping -c 3 192.168.1.3PING 192.168.1.3 (192.168.1.3) 56(84) bytes of data.64 bytes from 192.168.1.3: icmp_seq=1 ttl=64 time=0.172 ms64 bytes from 192.168.1.3: icmp_seq=2 ttl=64 time=0.057 ms64 bytes from 192.168.1.3: icmp_seq=3 ttl=64 time=0.089 ms
--- 192.168.1.3 ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 2036msrtt min/avg/max/mdev = 0.057/0.106/0.172/0.048 ms

$ kubectl exec nginx-7bd849c599-vkppk -it -- ping -c 3 192.168.0.5PING 192.168.0.5 (192.168.0.5): 56 data bytes64 bytes from 192.168.0.5: seq=0 ttl=64 time=0.149 ms64 bytes from 192.168.0.5: seq=1 ttl=64 time=0.118 ms64 bytes from 192.168.0.5: seq=2 ttl=64 time=0.132 ms
--- 192.168.0.5 ping statistics ---3 packets transmitted, 3 packets received, 0% packet lossround-trip min/avg/max = 0.118/0.133/0.149 m
复制代码


查看 Host 上创建的 Macvlan 子设备。


$ ip link list...47: eno1.host@eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default     link/ether be:c3:ad:8b:1f:53 brd ff:ff:ff:ff:ff:ff
复制代码


查看 Host 上为 Pod 创建的路由。


$ ip route list...192.168.1.3 dev eno1.host scope link
复制代码

参考资料


发布于: 刚刚阅读数: 3
用户头像

陆云

关注

还未添加个人签名 2019-11-08 加入

SRE / DeoOps

评论

发布
暂无评论
当Kubernetes遇见Macvlan——实现CNI路由插件_陆云_InfoQ写作社区