Flannel和Calico网络模式笔记

2024-01-08  本文已影响0人  Teddy_b

Falnnel

UDP模式

引用极客时间-张磊深入剖析k8s的图

image.png
/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0
docker@ubuntu:~$ brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.0242cbadb428       no              vethadc8dc2
                                                        vethd2a5b9e
$ route -n
172.17.0.0        0.0.0.0        255.255.0.0    U    0      0        0 flannel0
$ etcdctl get /coreos.com/network/subnets/172.17.2.0-24
{"PublicIP":"105.168.0.3"}
主机上抓包

缺点:性能差,由用户态进程进程UDP封包操作,需要经历用户(容器)->内核->用户(flanneld)->内核 多次状态转换

源码解析

创建flannel0设备

func OpenTun(name string) (*os.File, string, error) {
    // 以读写模式打开文件 /dev/net/tun
    tun, err := os.OpenFile(tunDevice, os.O_RDWR, 0)
    if err != nil {
        return nil, "", err
    }

    var ifr ifreqFlags
    copy(ifr.IfrnName[:len(ifr.IfrnName)-1], []byte(name+"\000"))
    ifr.IfruFlags = syscall.IFF_TUN | syscall.IFF_NO_PI

    // 系统调用ioctl,此时会创建一个tun设备出来
    // 相当于执行了:ip tuntap add dev flannel0 mod tun
    err = ioctl(int(tun.Fd()), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ifr)))
    if err != nil {
        return nil, "", err
    }

    ifname := fromZeroTerm(ifr.IfrnName[:ifnameSize])
    return tun, ifname, nil
}

配置flannel0设备

// 找到网卡设备
// 相当于执行了: ip link show | grep tun
iface, err := netlink.LinkByName(ifname)

// 为网卡添加IP地址
// 相当于执行了: ip addr add 10.1.1.0/32 dev flannel0
netlink.AddrAdd(iface, &netlink.Addr{IPNet: ipnLocal.ToIPNet(), Label: ""})

// 设置MTU
// 相当于执行了:  ifconfig flannel0 mtu 1472
netlink.LinkSetMTU(iface, mtu)

// 设置网卡UP
// 相当于执行了: ip link set flannel0 up
netlink.LinkSetUp(iface)

// 添加路由
// 相当于执行了: ip route add 10.1.1.0/16 dev flannel0
netlink.RouteAdd(&netlink.Route{
        LinkIndex: iface.Attrs().Index,
        Scope:     netlink.SCOPE_UNIVERSE,
        Dst:       ipn.Network().ToIPNet(),
    })

开启UDP和文件句柄/dev/net/tun监听,flannel中通过调用C函数实现中的poll函数实现同时监听多个文件句柄

void run_proxy(int tun, int sock, int ctl, in_addr_t tun_ip, size_t tun_mtu, int log_errors) {
    char *buf;
    // pollfd 定义在 poll.h 中,用来监控一组文件句柄
    struct pollfd fds[PFD_CNT] = {
        {
            .fd = tun,
            .events = POLLIN // 监听的是文件句柄可读取事件
        },
        {
            .fd = sock,
            .events = POLLIN
        },
        {
            .fd = ctl,
            .events = POLLIN
        },
    };

    exit_flag = 0;
    tun_addr = tun_ip;
    log_enabled = log_errors;

    // 分配 flannel0网卡MTU大小的内存空间
    buf = (char *) malloc(tun_mtu);
    if( !buf ) {
        log_error("Failed to allocate %d byte buffer\n", tun_mtu);
        exit(1);
    }

    // 将 /dev/net/tun 文件描述符设置为非阻塞
    // 因为下面代码是先调用 tun_to_udp(),如果是UDP先收到数据包,而tun是阻塞的,那么会一直阻塞在tun_to_udp()方法里
    fcntl(tun, F_SETFL, O_NONBLOCK);

    while( !exit_flag ) {
        // 通过 poll() 监听文件描述符是否有可读事件,指定了永不超时,poll()会一直挂起
        int nfds = poll(fds, PFD_CNT, -1), activity;
        if( nfds < 0 ) {
            if( errno == EINTR )  // 请求的事件之前产生一个信号,调用可以重新发起
                continue;

            log_error("Poll failed: %s\n", strerror(errno));
            exit(1);
        }

        // ctl2 文件句柄上有可读事件,对应的是有节点加入/删除,需要添加/移除路由信息
        if( fds[PFD_CTL].revents & POLLIN )
            process_cmd(ctl);

        // flannel0网卡或UDP文件句柄有可读事件,对应的是有网络包需要转发
        if( fds[PFD_TUN].revents & POLLIN || fds[PFD_SOCK].revents & POLLIN )
            do {
                activity = 0;
                activity += tun_to_udp(tun, sock, buf, tun_mtu);
                activity += udp_to_tun(sock, tun, buf, tun_mtu);

                /* As long as tun or udp is readable bypass poll().
                 * We'll just occasionally get EAGAIN on an unreadable fd which
                 * is cheaper than the poll() call, the rest of the time the
                 * read/recvfrom call moves data which poll() never does for us.
                 *
                 * This is at the expense of the ctl socket, a counter could be
                 * used to place an upper bound on how long we may neglect ctl.
                 */
            } while( activity );
    }

    free(buf);
}

VxLAN模式

VxLAN是Linux内核默认提供的能力,可以再内核完成UDP封包,因此较UDP方式性能更好

在学习这种模式之前,可以先了解下VxLAN是什么?https://icyfenix.cn/immutable-infrastructure/network/linux-vnet.html

引用极客时间-张磊深入剖析k8s的图

image.png
$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
...
10.1.16.0       10.1.16.0       255.255.255.0   UG    0      0        0 flannel.1
$ ip neigh show dev flannel.1
10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT
$ bridge fdb show flannel.1 | grep 5e:f8:4f:00:e3:37
5e:f8:4f:00:e3:37 dev flannel.1 dst 10.168.0.3 self permanent
源码解析

VxLAN模式会watch节点的加入和删除事件,实现方式和UDP模式基本一致

func (nw *network) Run(ctx context.Context) {
    
    go func() {
        subnet.WatchLeases(ctx, nw.subnetMgr, nw.SubnetLease, events)
        log.V(1).Info("WatchLeases exited")
        wg.Done()
    }()

    for {
        evtBatch, ok := <-events
        
        nw.handleSubnetEvents(evtBatch)
    }
}

收到节点接入的事件后

switch event.Type {
        case subnet.EventAdded:
            if event.Lease.EnableIPv4 {
                else {
                    if err := nw.dev.AddARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
                    
                    if err := nw.dev.AddFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
                    
                    if err := netlink.RouteReplace(&vxlanRoute); err != nil {
                    
            }

Host-GW模式

前面两种模式都是通过隧道方式,将容器的数据包包装成宿主机的数据包后发出,因此对宿主机的网络没具体要求,只要能够连通即可

host-gw模式则不一样,它是一种纯靠路由转发的方案

这种模式下flanneld进程只需要维护宿主机的路由信息即可

$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0

即目标容器IP在网段10.244.1.0/24在这个范围内的,直接通过宿主机的网卡发出,并且下一跳地址是10.168.0.3,即这个网段对应的目标宿主机IP

由于这个容器数据包是直接通过宿主机网卡发出去的,所以性能相对最好

同样也是因为这里设置了下一跳地址,所以对宿主机的网络要求更高了,需要宿主机之间是二层可达的,即属于同一个vlan

因为如果宿主机之间二层不可达,这个直接指定的下一跳地址将无法获取到MAC地址,进而无法发出去

(宿主机之间二层不可达时,原本可以通过指定下一跳路由器来到达目标IP的,现在改了下一跳地址了,所以现在不可达了)

源码分析

这种方式的实现相对更简单,只需要watch etcd中节点的加入和删除,然后添加将目标节点作为网关,目标容器网段作为dst即可

func (be *HostgwBackend) RegisterNetwork(ctx context.Context, wg *sync.WaitGroup, config *subnet.Config) (backend.Network, error) {
    
    if config.EnableIPv4 {
        attrs.PublicIP = ip.FromIP(be.extIface.ExtAddr)
        n.GetRoute = func(lease *subnet.Lease) *netlink.Route {
            return &netlink.Route{
                Dst:       lease.Subnet.ToIPNet(),
                Gw:        lease.Attrs.PublicIP.ToIP(),
                LinkIndex: n.LinkIndex,
            }
        }
    }

Calico

Calico和上面flannel的host-gw模式类似,不过Calico不自己维护路由规则了,而是依靠BGP协议(边界网关协议)来传递节点间的路由规则

同时Calico也不会创建网桥设备了,取而代之的是每个容器的路由规则都会写入宿主机,cali5863f3 和 容器内的 eth0 组成veth pair

10.233.2.3 dev cali5863f3 scope link

跨主机的容器IP通过网段形式也写入宿主机,这点和host-gw模式是一致的,不同的是这个路由消息是通过BGP协议交换的,BGP协议是Linux内核支持的

10.244.1.0/24 via 10.168.0.3 dev eth0

这里的路由规则也指定了下一跳,因此也需要宿主机之间是二层可达的

对于宿主机之间二层不可达的,Calico也提供了隧道模式,Calico中使用的不是VxLAN了,而是IP tunnel(IP隧道),这种隧道会在原始IP包上再包装一层IP包,外层的IP包指定为宿主机的IP到目标IP,即IPv4 in IPv4

经过包装后,这个IP包源地址变成了宿主机IP,然后到目标IP地址,这样就可以经过宿主机的路由器发送出去了,实现跨网段的传送

参考

上一篇下一篇

猜你喜欢

热点阅读