美文网首页
Flannel和Calico网络模式笔记

Flannel和Calico网络模式笔记

作者: Teddy_b | 来源:发表于2024-01-08 14:47 被阅读0次

    Falnnel

    UDP模式

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

    image.png
    • 按照容器内的路由表,同一主机上的容器之间发送数据包时,如到172.17.0.4会发往网关0.0.0.0,即不需要网关,通过二层直接路由,数据包直接发往容器的ech0网卡
    / # 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
    
    • 由于veth设置连接host命名空间和容器命名空间,容器1上的数据包会立马出现在veth设备的另一端

    • 在主机上的brctl命令可以看到veth设置docker0网桥的从设备,因此veth设备不具备处理数据包的资格,都需要交给网桥处理,此时网桥扮演二层交换机的角色,通过ARP拿到目标容器IP的MAC地址,然后两个容器即可正常通信了

    docker@ubuntu:~$ brctl show
    bridge name     bridge id               STP enabled     interfaces
    docker0         8000.0242cbadb428       no              vethadc8dc2
                                                            vethd2a5b9e
    
    • 如果发往另一主机的数据包,如到172.17.2.4,按照容器内的路由表,由于不在172.16.0.0网段下了,因此会发往网关172.17.0.1,有容器的ech0网卡发出

    • 此时的网关172.17.0.1对应的就是docker0的IP地址,容器1先通过ARP协议询问网关172.17.0.1的MAC地址,如果数据包的目的 MAC 地址为网桥本身,并且网桥有设置了 IP 地址的话,那该数据包即被认为是收到发往创建网桥那台主机的数据包,此数据包将不会转发到任何设备,而是直接交给上层(三层)协议栈去处理。

    • 数据包达到三层协议栈后, 将会根据宿主机的路由表来转发包,而flanneld进程会再宿主机上维护一条路由信息,即所有172.17.0.0/16这个网段的数据包都发到flannel0设备

    $ route -n
    172.17.0.0        0.0.0.0        255.255.0.0    U    0      0        0 flannel0
    
    • 而flannel0设备是一个tun设备,设备上的内核数据包都会经文件句柄/dev/net/tun发送到用户态进程,而flanneld进程监听了这个文件句柄,因此可以完全收到这个设备上的数据包

    • flanneld进程收到这个数据包后,会将数据包包装成一个UDP包,UDP端口默认是8285,UDP是应用层协议,还需要再套上IP头,IP就是flanneld所在的宿主机IP,而目标IP是通过etcd保存着的,etcd中记录了每个容器网段再哪个目标宿主机上,这样就能拿到目标IP和端口

    $ etcdctl get /coreos.com/network/subnets/172.17.2.0-24
    {"PublicIP":"105.168.0.3"}
    
    • UDP包装完成后,再通过文件句柄/dev/net/tun将数据包发送到flannel0设备,重新进入到了网络协议栈

    • 由于再容器的IP包外又包了一层UDP包,所以flannel0设备的MTU会比宿主机的小28字节,对应的是udp header 8 byte + ip header 20 byte

    • 此时目标IP已经变成了宿主机可识别的IP了,可以直接通过宿主机网络发送出去了

    • 目标IP收到UDP包后,将UDP包丢给目标主机的flanneld进程,然后目标主机解包,去掉UDP头部后,剩下的就是原始两个容器之间的IP数据包

    • 原始的容器IP数据包再通过flannel0设备进入目标IP的网络协议栈,然后由docker0设备转发给目标容器

    主机上抓包

    缺点:性能差,由用户态进程进程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);
    }
    
    • 这里一方面是监听udp和tun设备文件句柄,产生了事件,就意味着又数据包需要转发,因此会执行包的转发逻辑

    • 另一方面还监听了etcd,通过watch etcd中/coreos.com/network/subnets这个key的变化,再集群节点加入或者删除时,再节点上创建或者删除路由规则,即ip route add 10.1.2.0/16 next hop 192.168.89.130 next hop port 8285,相当于配置flannel0设备的路由

    VxLAN模式

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

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

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

    image.png
    • 容器的数据包也是先路由到flannel.1设备,这种模式下创建的设备名称叫flannel.1(不是flannel0了)

    • flannel.1是VTEP设备,同时flanneld进程会维护宿主机上的路由规则,即发往目标容器网段为10.1.16.0/24的,都经过flannel.1设备发送到网关10.1.16.0,这个网关对应的就是目标宿主机上的flannel.1设备的IP地址

    $ 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
    
    • 这样就找到了目标VTEP设备,但是宿主机上是没有到10.1.16.0网关的路由信息的,因此这个包还是不能从宿主机上发出

    • VxLAN模式下,是工作在三层之上的二层数据帧,集群内所有的VTEP设备组成一个二层vLAN,二层设备之间通过MAC地址通信,所以VTEP设备会给容器IP包包装一层目标VTEP设备的MAC,这个MAC地址从哪里来呢,也是flanneld进程维护的,在节点加入时静态写入的ARP信息

    $ ip neigh show dev flannel.1
    10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT
    
    • 包装了目标VTEP设备的MAC地址后,再包装VxLAN头部和UDP头部,其中VxLAN头部中的VID指定为1,这个flannel.1设备中的1是一致的

    • 然后再经宿主机发出这个UDP包,而目标宿主机的IP地址也是flanneld进程维护的,会记录目标VTEP设备的MAC地址和目标宿主机IP的映射关系

    $ 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
    
    • 目标IP的MAC地址通过ARP学习即可完成,至此, 容器IP数据包才能从宿主机发出
    源码解析

    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)
        }
    }
    

    收到节点接入的事件后

    • 首先会添加静态ARP记录,对应的就是上面VTEP网关地址对应的MAC信息

    • 然后再添加FDB记录,对应的就是上面VTEP设备MAC地址到目标IP的记录

    • 最后是到目标VTEP设备网关的路由信息

    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地址,这样就可以经过宿主机的路由器发送出去了,实现跨网段的传送

    参考

    相关文章

      网友评论

          本文标题:Flannel和Calico网络模式笔记

          本文链接:https://www.haomeiwen.com/subject/whlbndtx.html