Kubernetes的CNI插件非常智能,通过几个yaml文件就可以完成整套安装。这是kubernet的优势,但是对于想要了解原理的同学,太智能了,反而感觉无从下手。
当我开始学习CNI插件的时候也有这般感受。当时我的第一个想法是找一个最最最简单的CNI插件,快速的了解CNI的原理。后来反复对比,选择了macvlan。一是macvlan plugin由containernetworking官方 提供,较可靠;二是macvlan使用了最简单的命令操作,这些命令我都比较熟悉;选择macvlan之后,也还是遇到了小插曲,可能是由于macvlan太简单,反而网上并没有了整理完整的安装步骤。好吧,那一切只能靠自己拼凑。等macvlan顺利运行起来后,感觉此刻对kubernet cni插件的原理立刻就打通了。写这篇博客,把我的实验过程整理出来。如果你也习惯通过一个简单的例子,解开复杂的流程,那么以下内容会非常适合你。
接下来本文将介绍:
本文假定用户已经安装Kubernetes环境。
CNI(container network interface),是一个接口规范,这个规范定义了输入、输出的标准和调用的接口,只要调用CNI插件的实体遵守这个规范,就能从CNI拿到满足网络互通条件的网络参数(如IP地址、网关、路由、DNS等),这些网络参数可以配置container实例。
macvlan 插件正确运行需要两个前提:
我这边环境kubernetes版本是1.14.3,已正确安装kubernetes-cni-0.7.5-0.x86_64内部含有macvlan插件。
配置文件路径:/etc/cni/net.d/10-maclannet.conf
内容如下:
{
"cniVersion":"0.3.1",
"name": "macvlannet",
"type": "macvlan",
"master": "ens3",
"mode": "bridge",
"isGateway": true,
"ipMasq": false,
"ipam": {
"type": "host-local",
"subnet": "192.168.1.0/24",
"rangeStart": "192.168.1.100",
"rangeEnd": "192.168.1.200",
"gateway": "192.168.1.1",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
配置文件路径:/etc/cni/net.d/99-loopback.conf
内容如下:
{
"cniVersion": "0.3.1",
"name": "lo",
"type": "loopback"
}
字段说明:
这里仅仅展示Kubernetes集群master上的配置,其它node节点上的配置,可以从以下链接获取:集群macvlan配置文件
在刚安装完Kubernetes集群时,通过命令kubectl get nodes -A -o wide可以看到集群节点还是NotReady的状态。
在确认完macvlan二进制已经安装、内核支持macvlan虚拟化、配置正确的放在对应的位置后,通过上述同样的命令,我们可以看到集群节点已经变成Ready状态。
另外这里还需要特别注意,Kubernetes初始化的时候指定的pod-network-cidr需要包含这里的subnet子网
CNI插件的接口方法一般包括:
实际使用中,将一个容器添加进一个网络,调用的是ADD接口方法。将一个容器从网络中删除,则是调用DEL接口方法。
调用接口方法的参数又可以分成两类:输入参数,环境变量
我们这里罗列出常用的接口方法的输入参输,环境变量,输出参数,这些参数的获取方法详见:cni插件输入输出获取方法
{
"cniVersion":"0.3.1",
"name": "macvlannet",
"type": "macvlan",
"master": "ens3",
"mode": "bridge",
"ipam": {
"type": "host-local",
"subnet": "192.168.122.0/24",
"rangeStart": "192.168.122.21",
"rangeEnd": "192.168.122.99",
"gateway": "192.168.122.14",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
CNI_ARGS=IgnoreUnknown=1;K8S_POD_NAMESPACE=kube-system;K8S_POD_NAME=nginx-controller-dxxfg;K8S_POD_INFRA_CONTAINER_ID=b76473e8fe8dd4bba26b73fef7be002a59da050f0c96d55d293848b0f891e302
CNI_COMMAND=ADD
CNI_IFNAME=eth0
CNI_NETNS=/proc/19504/ns/net
CNI_CONTAINERID=b76473e8fe8dd4bba26b73fef7be002a59da050f0c96d55d293848b0f891e302
CNI_PATH=/opt/cni/bin
{
"cniVersion": "0.3.1",
"interfaces": [
{
"name": "eth0",
"mac": "5e:e0:14:f3:31:61",
"sandbox": "/proc/19504/ns/net"
}
],
"ips": [
{
"version": "4",
"interface": 0,
"address": "192.168.122.117/24",
"gateway": "192.168.122.14"
}
],
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"dns": {}
}
git clone https://github.com/containernetworking/plugins
./build_linux.sh
如果想增加sample的编译,则可以先执行
cp -rf plugins/sample plugins/main
编译结果:
[root@k8s-new-master ~]# ls xujx/cni/plugins/bin/
bandwidth bridge dhcp firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan
可以以上内容拷贝到Kubernetes对应路径:/opt/cni/bin/
macvlan 是在 HOST 网卡上创建多个子网卡,并分配独立的 IP 地址和 MAC 地址,把子网卡分配给容器实例来实现实例与物理网络的直通,并同时保持容器实例的隔离性。Host 收到数据包后,则根据不同的 MAC 地址把数据包从转发给不同的子接口,在外界来看就相当于多台主机。macvlan 要求物理网卡支持混杂 promisc 模式并且要求 kernel 为 v3.9-3.19 和 4.0+,因为是直接通过子接口转发数据包,所以可想而知,性能比 bridge 要高,不需要经过 NAT。
以上四种模式,子接口都不能与父接口通信
从linux man ip link可以看到还有一种source模式,这里没有列出
[root@k8s-new-master ~]# ip netns add net1
[root@k8s-new-master ~]# ip netns add net2
[root@k8s-new-master ~]# ip link add link ens3 mac1 type macvlan mode bridge
[root@k8s-new-master ~]# ip link
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 88:4f:d5:25:80:12 brd ff:ff:ff:ff:ff:ff
6: mac1@ens3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether ae:08:c6:45:4b:0c brd ff:ff:ff:ff:ff:ff
[root@k8s-new-master ~]# ip link set mac1 netns net1
[root@k8s-new-master ~]# ip netns exec net1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
6: mac1@if2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether ae:08:c6:45:4b:0c brd ff:ff:ff:ff:ff:ff link-netnsid 0
[root@k8s-new-master ~]# ip netns exec net1 ip link set mac1 name eth0
[root@k8s-new-master ~]# ip netns exec net1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
6: eth0@if2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether ae:08:c6:45:4b:0c brd ff:ff:ff:ff:ff:ff link-netnsid 0
[root@k8s-new-master ~]# ip netns exec net1 ip addr add 192.168.88.1/24 dev eth0
[root@k8s-new-master ~]# ip netns exec net1 ip link set eth0 up
[root@k8s-new-master ~]# ip link add link ens3 mac2 type macvlan mode bridge
[root@k8s-new-master ~]# ip link set mac2 netns net2
[root@k8s-new-master ~]# ip netns exec net2 ip link set mac2 name eth0
[root@k8s-new-master ~]# ip netns exec net2 ip addr add 192.168.88.2/24 dev eth0
[root@k8s-new-master ~]# ip netns exec net2 ip link set eth0 up
[root@k8s-new-master ~]# ip netns exec net2 ping 192.168.88.1
PING 192.168.88.1 (192.168.88.1) 56(84) bytes of data.
64 bytes from 192.168.88.1: icmp_seq=1 ttl=64 time=0.701 ms
64 bytes from 192.168.88.1: icmp_seq=2 ttl=64 time=0.065 ms
main函数到调用接口ADD/CHECK/DEL/DEL/VERSION的流程。
这里我们主要介绍两个常用的接口cmdAdd和cmdDel
main
|=> skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("macvlan")) //源码位置containernetworking\cni\pkg\skel\skel.go
|=> PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string)
|=> (&dispatcher{Getenv: os.Getenv,Stdin: os.Stdin,Stdout: os.Stdout,Stderr: os.Stderr,}).pluginMain(cmdAdd, cmdCheck, cmdDel, versionInfo, about)
|=> cmd == "ADD":
|=> t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
|=> cmdAdd()
|=> cmd == "CHECK"
|=> version.GreaterThanOrEqualTo(pluginVersion, configVersion)
|=> cmd == "DEL"
|=> t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
|=> cmdDel()
|=> cmd == "VERSION"
|=> versionInfo.Encode(t.Stdout)
输入参数:args.StdinData, 也就是插件的标准输入参数
输出参数:result结构体涵IPs(分配的ip信息), Routes(分配的路由信息)等信息
函数流程:
func cmdAdd(args *skel.CmdArgs) error {
n, cniVersion, err := loadConf(args.StdinData, args.Args) //加载输入参数
if err != nil {
return err
}
isLayer3 := n.IPAM.Type != "" //获取置文件ipam选项
netns, err := ns.GetNS(args.Netns) //获取容器隔离空间名称
if err != nil {
return fmt.Errorf("failed to open netns %q: %v", netns, err)
}
defer netns.Close()
macvlanInterface, err := createMacvlan(n, args.IfName, netns) //创建macvlan网络设备
if err != nil {
return err
}
// Delete link if err to avoid link leak in this ns
defer func() {
if err != nil {
netns.Do(func(_ ns.NetNS) error {
return ip.DelLinkByName(args.IfName)
})
}
}()
// Assume L2 interface only
result := ¤t.Result{CNIVersion: cniVersion, Interfaces: []*current.Interface{macvlanInterface}}
if isLayer3 {
// run the IPAM plugin and get back the config to apply
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) //从ipam 获取ip,路由信息
if err != nil {
return err
}
// Invoke ipam del if err to avoid ip leak
defer func() {
if err != nil {
ipam.ExecDel(n.IPAM.Type, args.StdinData)
}
}()
// Convert whatever the IPAM result was into the current Result type
ipamResult, err := current.NewResultFromResult(r)
if err != nil {
return err
}
if len(ipamResult.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
result.IPs = ipamResult.IPs
result.Routes = ipamResult.Routes
for _, ipc := range result.IPs {
// All addresses apply to the container macvlan interface
ipc.Interface = current.Int(0)
}
err = netns.Do(func(_ ns.NetNS) error {
if err := ipam.ConfigureIface(args.IfName, result); err != nil { //配置ip地址,路由等信息到网络接口上
return err
}
contVeth, err := net.InterfaceByName(args.IfName)
if err != nil {
return fmt.Errorf("failed to look up %q: %v", args.IfName, err)
}
for _, ipc := range result.IPs {
if ipc.Version == "4" {
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
}
}
return nil
})
if err != nil {
return err
}
} else {
// For L2 just change interface status to up
err = netns.Do(func(_ ns.NetNS) error {
macvlanInterfaceLink, err := netlink.LinkByName(args.IfName)
if err != nil {
return fmt.Errorf("failed to find interface name %q: %v", macvlanInterface.Name, err)
}
if err := netlink.LinkSetUp(macvlanInterfaceLink); err != nil {
return fmt.Errorf("failed to set %q UP: %v", args.IfName, err)
}
return nil
})
if err != nil {
return err
}
}
result.DNS = n.DNS
return types.PrintResult(result, cniVersion)
}
createMacvlan:
输入参数: 网络配置, 接口名称, 网络隔离参数
输出参数:网络接口结构体
函数流程:
func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
macvlan := ¤t.Interface{}
mode, err := modeFromString(conf.Mode) //获取配置的mode信息
if err != nil {
return nil, err
}
m, err := netlink.LinkByName(conf.Master) //通过master(parent)接口名称获取相关信息
if err != nil {
return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
}
// due to kernel bug we have to create with tmpName or it might
// collide with the name on the host and error out
tmpName, err := ip.RandomVethName()
if err != nil {
return nil, err
}
linkAttrs := netlink.LinkAttrs{ //填充netlink.Attrs属性
MTU: conf.MTU,
Name: tmpName, //创建的子网络接口名称
ParentIndex: m.Attrs().Index, //接口index参数
Namespace: netlink.NsFd(int(netns.Fd())), //隔离空间参数
}
if conf.Mac != "" { //是否输入参数含有mac地址
addr, err := net.ParseMAC(conf.Mac) //解析mac地址
if err != nil {
return nil, fmt.Errorf("invalid args %v for MAC addr: %v", conf.Mac, err)
}
linkAttrs.HardwareAddr = addr //netlink.Attrs属性添加mac信息
}
mv := &netlink.Macvlan{ //mode, linkAttrs赋值给netlink.Macvlan
LinkAttrs: linkAttrs,
Mode: mode,
}
if err := netlink.LinkAdd(mv); err != nil { //根据上述设置的netlink信息,执行创建子网络接口的动作
return nil, fmt.Errorf("failed to create macvlan: %v", err)
}
err = netns.Do(func(_ ns.NetNS) error {
// TODO: duplicate following lines for ipv6 support, when it will be added in other places
ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName)
if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil {
// remove the newly added link and ignore errors, because we already are in a failed state
_ = netlink.LinkDel(mv)
return fmt.Errorf("failed to set proxy_arp on newly added interface %q: %v", tmpName, err)
}
err := ip.RenameLink(tmpName, ifName) //重命名子网络接口名称
if err != nil {
_ = netlink.LinkDel(mv)
return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
}
macvlan.Name = ifName //输出参数macvlan设置接口名称
// Re-fetch macvlan to get all properties/attributes
contMacvlan, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to refetch macvlan %q: %v", ifName, err)
}
macvlan.Mac = contMacvlan.Attrs().HardwareAddr.String() //输出参数获取mac地址
macvlan.Sandbox = netns.Path() //输出参数设置Sandbox(隔离空间路径信息)
return nil
})
if err != nil {
return nil, err
}
return macvlan, nil //返回输出参数macvlan结构体
}
输入参数: 网络配置, 接口名称, 网络隔离参数
输出参数: 无
代码流程:
func cmdDel(args *skel.CmdArgs) error {
n, _, err := loadConf(args.StdinData, args.Args) //加载输入参数
if err != nil {
return err
}
isLayer3 := n.IPAM.Type != ""
if isLayer3 {
err = ipam.ExecDel(n.IPAM.Type, args.StdinData) //从ipam 删除ip,路由等信息
if err != nil {
return err
}
}
if args.Netns == "" {
return nil
}
// There is a netns so try to clean up. Delete can be called multiple times
// so don't return an error if the device is already removed.
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
if err := ip.DelLinkByName(args.IfName); err != nil { //删除子网络接口
if err != ip.ErrLinkNotFound {
return err
}
}
return nil
})
return err
}
ipam主要用于ip地址、网关的分配,函数调用顺序是从上面的macvlan插件开始:
cmdAdd=>…=>ipam.ExecAdd
cmd=>…=>ipam.ExecDel
|=> ipam.ExecAdd //源码位置 pkg/ipam/ipam.go
|=> invoke.DelegateAdd(context.TODO(), plugin, netconf, nil) //cni/pkg/invoke/delegate.go, 配置文件制定host-local
|=> ExecPluginWithResult(ctx, pluginPath, netconf, delegateArgs("ADD"), realExec)
|=> exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
...
|=> main //host-local插件的主函数, 这里假设我们配置文件里面配置ipam的type为host-local
|=> ipam.ExecDel
|=> invoke.DelegateDel(context.TODO(), plugin, netconf, nil)
... //同上
ipam.cmdAdd:
输入参数: stdin_data标准输入等信息
输出参数: ip、路由信息
代码流程:
func cmdAdd(args *skel.CmdArgs) error {
ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) //加载IPAM的配置和版本号
if err != nil {
return err
}
result := ¤t.Result{}
if ipamConf.ResolvConf != "" { //若ipamConf.ResolvConf不为""
dns, err := parseResolvConf(ipamConf.ResolvConf) //解析ResolvConf信息
if err != nil {
return err
}
result.DNS = *dns // 设置DNS信息
}
store, err := disk.New(ipamConf.Name, ipamConf.DataDir) // 存储路径示例 /var/lib/cni/networks/<cni name>/
if err != nil {
return err
}
defer store.Close()
// Keep the allocators we used, so we can release all IPs if an error
// occurs after we start allocating
allocs := []*allocator.IPAllocator{}
// Store all requested IPs in a map, so we can easily remove ones we use
// and error if some remain
requestedIPs := map[string]net.IP{} //net.IP cannot be a key
for _, ip := range ipamConf.IPArgs {
requestedIPs[ip.String()] = ip
}
for idx, rangeset := range ipamConf.Ranges {
allocator := allocator.NewIPAllocator(&rangeset, store, idx)
// Check to see if there are any custom IPs requested in this range.
var requestedIP net.IP
for k, ip := range requestedIPs {
if rangeset.Contains(ip) {
requestedIP = ip
delete(requestedIPs, k)
break
}
}
ipConf, err := allocator.Get(args.ContainerID, args.IfName, requestedIP) //获取一个ip
if err != nil {
// Deallocate all already allocated IPs
for _, alloc := range allocs {
_ = alloc.Release(args.ContainerID, args.IfName)
}
return fmt.Errorf("failed to allocate for range %d: %v", idx, err)
}
allocs = append(allocs, allocator)
result.IPs = append(result.IPs, ipConf)
}
// If an IP was requested that wasn't fulfilled, fail
if len(requestedIPs) != 0 {
for _, alloc := range allocs {
_ = alloc.Release(args.ContainerID, args.IfName)
}
errstr := "failed to allocate all requested IPs:"
for _, ip := range requestedIPs {
errstr = errstr + " " + ip.String()
}
return fmt.Errorf(errstr)
}
result.Routes = ipamConf.Routes //设置路由信息
return types.PrintResult(result, confVersion)
}
ipam.cmdDel
输入参数: stdin_data标准输入等信息
输出参数: nil or err
代码流程:
func cmdDel(args *skel.CmdArgs) error {
ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) //加载IPAM的配置
if err != nil {
return err
}
store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
if err != nil {
return err
}
defer store.Close()
// Loop through all ranges, releasing all IPs, even if an error occurs
var errors []string
for idx, rangeset := range ipamConf.Ranges {
ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx)
err := ipAllocator.Release(args.ContainerID, args.IfName) //删除已经分配的ip
if err != nil {
errors = append(errors, err.Error())
}
}
if errors != nil {
return fmt.Errorf(strings.Join(errors, ";"))
}
return nil
}
我们这里通过一个shell 脚本编写的bash-cni插件,实现容器ip地址分配与设置及容器网关的设置
bash-cni插件、配置详见超链接
{
"cniVersion": "0.3.1", //版本
"name": "mynet", //网络名词
"type": "bash-cni", //使用的插件
"network": "192.168.0.0/16", //集群POD 地址区间
"subnet": "192.168.1.0/24" //本节点 POD 地址区间
}
shell脚本大家应该都比较熟悉,这里就只注释最核心的几个命令
输入参数,输出参数详见上面的cmdAdd
ADD)
network=$(echo "$stdin" | jq -r ".network")
subnet=$(echo "$stdin" | jq -r ".subnet")
subnet_mask_size=$(echo $subnet | awk -F "/" '{print $2}')
all_ips=$(nmap -sL $subnet | grep "Nmap scan report" | awk '{print $NF}')
all_ips=(${all_ips[@]})
skip_ip=${all_ips[0]}
gw_ip=${all_ips[1]}
reserved_ips=$(cat $IP_STORE 2> /dev/null || printf "$skip_ip\n$gw_ip\n") # reserving 10.244.0.0 and 10.244.0.1
reserved_ips=(${reserved_ips[@]})
printf '%s\n' "${reserved_ips[@]}" > $IP_STORE
container_ip=$(allocate_ip)
mkdir -p /var/run/netns/
ln -sfT $CNI_NETNS /var/run/netns/$CNI_CONTAINERID # 创建进程对应的网络隔离空间软连接,之后ip netns list就可以查看到,
# 有了这一步,之后的命令就都很熟悉了
# 这里的别名是容器id:$CNI_CONTAINERID, 后面通过ip netns exec $CNI_CONTAINERID bash即可进入容器
# 容器id示例: 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8
rand=$(tr -dc 'A-F0-9' < /dev/urandom | head -c4)
host_if_name="veth$rand"
ip link add $CNI_IFNAME type veth peer name $host_if_name # 创建veth-pair 这样一对虚拟设备接口,
# 实际命令示例:ip link add eth0 type veth peer name veth2D2A
# 2D2A随机生成
ip link set $host_if_name up # up 接口
ip link set $host_if_name master cni0 # 将接口veth2D2A 添加到桥cni0上
ip link set $CNI_IFNAME netns $CNI_CONTAINERID # 将网络接口eth0移进隔离空间$CNI_CONTAINERID:
# 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8
ip netns exec $CNI_CONTAINERID ip link set $CNI_IFNAME up # up容器中的eth0接口
ip netns exec $CNI_CONTAINERID ip addr add $container_ip/$subnet_mask_size dev $CNI_IFNAME # 设置ip,掩码到隔离空间中的eth0
# 示例:ip netns exec 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8 ip addr add 192.168.1.6/24 dev eth0
ip netns exec $CNI_CONTAINERID ip route add default via $gw_ip dev $CNI_IFNAME # 设置隔离空间网关
# 示例: ip netns exec 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8 ip route add default via 192.168.1.1 dev eth0
mac=$(ip netns exec $CNI_CONTAINERID ip link show eth0 | awk '/ether/ {print $2}') # 获取隔离空间中eth0对应的mac地址
# 输出json格式的各个参数: mac/ip/gw/netns等等
echo "{
\"cniVersion\": \"0.3.1\",
\"interfaces\": [
{
\"name\": \"eth0\",
\"mac\": \"$mac\",
\"sandbox\": \"$CNI_NETNS\"
}
],
\"ips\": [
{
\"version\": \"4\",
\"address\": \"$container_ip/$subnet_mask_size\",
\"gateway\": \"$gw_ip\",
\"interface\": 0
}
]
}" >&3
;;
输入参数,输出参数详见上面的cmdDel
DEL)
ip=$(ip netns exec $CNI_CONTAINERID ip addr show eth0 | awk '/inet / {print $2}' | sed s%/.*%% || echo "") # 获取隔离空间ip eth0网络接口设置的ip
if [ ! -z "$ip" ]
then
sed -i "/$ip/d" $IP_STORE # 从/tmp/reserved_ips移除该ip
fi
rm -f /var/run/netns/$CNI_CONTAINERID # 删除之前创建个ip netns 命令用的软链接
;;
添加转发允许
iptables -t filter -A FORWARD -s 192.168.0.0/16 -j ACCEPT
iptables -t filter -A FORWARD -d 192.168.0.0/16 -j ACCEPT
增加不是从cni0出去的规则NAT(SNAT)规则
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 ! -o cni0 -j MASQUERADE # node1:
iptables -t nat -A POSTROUTING -s 192.168.2.0/24 ! -o cni0 -j MASQUERADE # node2:
添加跨网段路由
ip route add 192.168.2.0/24 via 192.168.122.16 dev ens3 # run on node1, 192.168.122.16是node2节点的宿主ip
ip route add 192.168.1.0/24 via 192.168.122.15 dev ens3 # run on node2, 192.168.122.15是node1节点的宿主ip
示例:
node1: 192.168.122.15 容器A网络ip:192.168.1.14, 容器网络网关:192.168.1.1(配置在node1的cni0桥上)
node1: 192.168.122.16 容器B网络ip:192.168.2.14, 容器网络网关:192.168.2.1(配置在node2的cni0桥上)
动作:在容器A上ping容器B
在使用Kubernetes的CNI进行配置网络的过程中,编写插件,配置插件配置文件外,实际使用中经常会关注容器将通信的问题,同节点容器间通信及跨节点容器间通信等。
通过命令kubectl exec -it -n kube-system -c kube-flannel kube-flannel-ds-amd64-frmtz /bin/sh 我们可以再主节点进相应的POD节点的相应的容器中,但这存在一定的极限:
比如部分image里面,实际上是没有我们常用的工具,比如ifconfig/ip/route等命令。
这里我们提供一个进入POD所在的网络隔离空间的方法,在隔离空间中,可以使用宿主机丰富的工具命令。脚本及说明详见超链接
比如:
执行以下命令进入coredns所在的隔离空间:
contain2ns.sh add coredns-fb8b8dccf-gshlr
enter_ns.sh coredns-fb8b8dccf-gshlr
脚本输入: POD_NAME
输出无:
代码流程:
CONTAIN=k8s_POD_$2 # 输入POD NAME, 补充在docker 中显示的前缀:k8s_POD
CONTAIN_ID=`docker ps |grep $CONTAIN |awk -F " " '{printf $1}'` # 获取docker 容器列表中,含有k8s_POD_<POD_NAME>的容器id,其中docker ps可以显示容器列表, grep用于过滤名称
PID=`docker top $CONTAIN_ID|awk -F " " '{if(NR==2)printf $2}'` # 通过docker <CONTAIN_ID> 获取容器对应你的PID等信息,然后awk获取PID,行号为2
ln -sf /proc/$PID/ns/net "/var/run/netns/$2" #创建进程网络隔离空间的软链接,放在/var/run/netns目录中,别名为输入的POD名称。通过ip netns list即可查看到对应的隔离空间,这里类似本篇文章中自己编写一个网络插件章节中的实现。
通过使用与分析,我们知道:
同时作为一个underlay的实现,macvlan方案在性能上接近于宿主机网络。
那当前有什么方案可以直接解决上述问题呢? 下一篇《CNI网络插件之flannel》将为你详细介绍。