深入剖析Flannel-启动流程

最近一直在研究flannel的源代码,发现flannel自身代码不是很多,依赖的第三方代码却很多,例如创建隧道是通过第三方库vishvananda.后续分析源码过程中,不会深入介绍第三方库,只把我们关心的逻辑(流程)介绍清楚。

一、流程图

flannel的源码逻辑并不是很复杂,main函数处理逻辑也比较简单,所以就从main函数开始介绍。main函数流程图如下:

二、源码

在上一篇提到过,flannel提供三种方式来处理跨主机容器间通信,其中vxlan属于推荐方式(工程环境),所以在后续源码分析皆以此为基础进行介绍。按照上述流程图,我们开始介绍源码具体内容:

2.1、确定网卡

func main() {
    if opts.version { //输出版本信息
        fmt.Fprintln(os.Stderr, version.Version)
        os.Exit(0)
    }

    flagutil.SetFlagsFromEnv(flannelFlags, "FLANNELD")

    // Validate flags
    // 子网续约时间不能大于1天 单位是分钟
    if opts.subnetLeaseRenewMargin >= 24*60 || opts.subnetLeaseRenewMargin <= 0 {
        log.Error("Invalid subnet-lease-renew-margin option, out of acceptable range")
        os.Exit(1)
    }

    // Work out which interface to use
    var extIface *backend.ExternalInterface
    var err error
    // Check the default interface only if no interfaces are specified
    if len(opts.iface) == 0 && len(opts.ifaceRegex) == 0 { //没有指定网卡 则自己查找
        extIface, err = LookupExtIface("", "")
        if err != nil {
            log.Error("Failed to find any valid interface to use: ", err)
            os.Exit(1)
        }
    } else {
        // Check explicitly specified interfaces
        // 用户指定 严格物理网卡名称
        for _, iface := range opts.iface {
            extIface, err = LookupExtIface(iface, "")
            if err != nil {
                log.Infof("Could not find valid interface matching %s: %s", iface, err)
            }

            if extIface != nil {
                break
            }
        }

        // Check interfaces that match any specified regexes
        // 用户通过正则表达式 指定物理网卡名称
        if extIface == nil {
            for _, ifaceRegex := range opts.ifaceRegex {
                extIface, err = LookupExtIface("", ifaceRegex)
                if err != nil {
                    log.Infof("Could not find valid interface matching %s: %s", ifaceRegex, err)
                }

                if extIface != nil {
                    break
                }
            }
        }
        // 没有找到合适的网卡 直接退出
        if extIface == nil {
            // Exit if any of the specified interfaces do not match
            log.Error("Failed to find interface to use that matches the interfaces and/or regexes provided")
            os.Exit(1)
        }
    }

 既然是跨主机容器间进行通信最终还是要体现在物理网卡层面,所以flannel上来的第一件事情就是确定要使用哪个网卡?我们可以在启动flannel时,通过命令行参数进行指定(严格指定、模糊指定),如果没有指定则flannel自行确定。通常情况下为了方便我们不指定网卡,由flannel自行确定。那么flannel依据什么确定网卡呢?默认路由

2.2、创建子网管理对象

flannel需要存储一些数据(子网段范围等),但flannel自身没有持久化存储功能,所以flannel内置两种存储管理方式--kubernetes api-server和etcd,在这里以etcd作为存储管理。这里的子网管理对象实际上就是对etcd或者kube api进行的封装,并没有什么特别高深的内容(哈哈)。

    sm, err := newSubnetManager() //创建子网管理对象
    if err != nil {
        log.Error("Failed to create SubnetManager: ", err)
        os.Exit(1)
    }
    log.Infof("Created subnet manager: %s", sm.Name())

    // Register for SIGINT and SIGTERM
    // 只接受SIGINT、SIGTERM两种信号
    log.Info("Installing signal handlers")
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)

    // This is the main context that everything should run in.
    // All spawned goroutines should exit when cancel is called on this context.
    // Go routines spawned from main.go coordinate using a WaitGroup.
    // This provides a mechanism(机制) to allow the shutdownHandler goroutine
    // to block until all the goroutines return . If those goroutines spawn other goroutines then they are responsible for
    // blocking and returning only when cancel() is called.
    // 返回值:
    // ctx 全局上下文  cancel 函数指针
    // Background() 返回一个空白context,不能被cancel
    // WitchCancel(parent) 继承parent后返回一个新的context,该context可以被cancel
    ctx, cancel := context.WithCancel(context.Background())
    wg := sync.WaitGroup{} //go routines 同步手段

    wg.Add(1) //此处1 表示等待一个go routines
    go func() {
        shutdownHandler(ctx, sigs, cancel)
        wg.Done() //通知Wait()返回
    }()

    if opts.healthzPort > 0 {
        // It's not super easy to shutdown the HTTP server so don't attempt to stop it cleanly
        //关闭HTTP服务并不是很容易,因此不要试图干净的停止它
        go mustRunHealthz()
    }

这里除了创建一个子网管理对象外,还进行信号注册,用于接收SIGINT、SIGTERM,对于这两种信号处理一般是退出程序。最后判断是否需要开启健康检查功能。

2.3、创建网卡并且激活

    // Fetch the network config (i.e. what backend to use etc..).
    // 从etcd中获取配置
    config, err := getConfig(ctx, sm)
    if err == errCanceled {
        wg.Wait()
        os.Exit(0)
    }

    // Create a backend manager then use it to create the backend and register the network with it.
    bm := backend.NewManager(ctx, sm, extIface) //创建manager对象
    be, err := bm.GetBackend(config.BackendType)
    if err != nil {
        log.Errorf("Error fetching backend: %s", err)
        cancel()  // 促使context.done()返回
        wg.Wait() // 等待同步
        os.Exit(1)
    }

    /**
     * 如果backend指向的是vxlan则RegisterNetwork函数指向vxlan.go文件中RegisterNetwork
     * 相当于创建网卡。
     * vxlan模式 flannel.VNI VNI为vlan id
     * udp模式   flannel.数字 从0开始
     * 这个是非常重要的函数
     */
    bn, err := be.RegisterNetwork(ctx, config)
    if err != nil {
        log.Errorf("Error registering network: %s", err)
        cancel()  // 促使context.done()返回
        wg.Wait() // 等待同步
        os.Exit(1)
    }

以上代码主要如下几件事:

首先,通过getConfig函数从etcd中获取flannel相关配置,所以必须提前将配置写到etcd中,例如通过,

etcdctl --endpoint="http://192.63.63.1:2379" set /coreos.com/network/config \
 '{"Network":"172.17.0.0/16","SubnetMin":"172.17.0.0","SubnetMax":"172.17.10.0","Backend":{"Type":"vxlan","VNI":3}}'

该配置主要设置了子网地址范围以及采用backend类型。 

最后、根据配置创建backend对象,后续流程中会根据backend创建网卡。上面代码中已经添加详细注释。

    // Set up ipMasq if needed
    if opts.ipMasq { // 开启防火墙 ip-masquerade
        go network.SetupAndEnsureIPTables(network.MasqRules(config.Network, bn.Lease()))
    }

    // Always enables forwarding rules. This is needed for Docker versions >1.13
    // (https://docs.docker.com/engine/userguide/networking/default_network/container-communication/#container-communication-between-hosts)
    // In Docker 1.12 and earlier, the default FORWARD chain policy was ACCEPT.
    // In Docker 1.13 and later, Docker sets the default policy of the FORWARD chain to DROP.
    // 设置防火墙策略
    go network.SetupAndEnsureIPTables(network.ForwardRules(config.Network.String())) // iptables.go
    // 保存到配置文件中
    if err := WriteSubnetFile(opts.subnetFile, config.Network, opts.ipMasq, bn); err != nil {
        // Continue, even though it failed.
        log.Warningf("Failed to write subnet file: %s", err)
    } else {
        log.Infof("Wrote subnet file to %s", opts.subnetFile)
    }

    // Start "Running" the backend network. This will block until the context is done so run in another goroutine.
    log.Info("Running backend.")
    wg.Add(1)
    go func() {
        bn.Run(ctx) //如果是vxlan网络 执行的是vxlan_network.go中Run
        wg.Done()
    }()

    daemon.SdNotify(false, "READY=1")

 在这部分代码可知,启动了一个协程,该协程主要工作是监控租约以及事件处理,后面章节会详细介绍。

2.4、启动监控

    // Kube subnet mgr doesn't lease the subnet for this node - it just uses the podCidr that's already assigned.
    // kubernets管理的网络不需要使用该节点
    if !opts.kubeSubnetMgr {
        // 通过etcd管理网络 会进入此函数 此函数是一个死循环
        err = MonitorLease(ctx, sm, bn, &wg) //监控该节点 主要用于节点租约过期后 能够快速获取新的租约
        if err == errInterrupted {
            // The lease was "revoked" - shut everything down
            cancel()
        }
    }

    log.Info("Waiting for all goroutines to exit")
    // Block waiting for all the goroutines to finish.
    wg.Wait()
    log.Info("Exiting cleanly...")
    os.Exit(0)
}

启动监控,用于监视租约,当一个租约过期后能够快速获取新的租约,这里的租约类似于dhcp获取ip有一个有效期

三、查找网卡

在上面介绍了,flannel上来的第一件事情就是确定可用网卡,如果命令行没有指定网卡名称则flannel自行决定使用哪个一个,然而flannel依据是默认路由所在网卡,接下来看一下LookupExtIface函数实现。

/**
 * 查找外部通信网卡信息
 * @param ifname 网卡名称 完全匹配
 * @param ifregex 正则表达式 方式查找网卡
 * 如果两个参数都没有指定则查找默认路由所在网卡
 */
func LookupExtIface(ifname string, ifregex string) (*backend.ExternalInterface, error) {
    var iface *net.Interface
    var ifaceAddr net.IP
    var err error

    if len(ifname) > 0 {// 严格指定网卡名称
        if ifaceAddr = net.ParseIP(ifname); ifaceAddr != nil {
            log.Infof("Searching for interface using %s", ifaceAddr)
            iface, err = ip.GetInterfaceByIP(ifaceAddr)
            if err != nil {
                return nil, fmt.Errorf("error looking up interface %s: %s", ifname, err)
            }
        } else {
            iface, err = net.InterfaceByName(ifname)
            if err != nil {
                return nil, fmt.Errorf("error looking up interface %s: %s", ifname, err)
            }
        }
    } else if len(ifregex) > 0 {// 正则表达式 指定网卡(模糊指定)
        // Use the regex if specified and the iface option for matching a specific ip or name is not used
        ifaces, err := net.Interfaces()
        if err != nil {
            return nil, fmt.Errorf("error listing all interfaces: %s", err)
        }

        // Check IP
        for _, ifaceToMatch := range ifaces {
            ifaceIP, err := ip.GetIfaceIP4Addr(&ifaceToMatch)
            if err != nil {
                // Skip if there is no IPv4 address
                continue
            }

            matched, err := regexp.MatchString(ifregex, ifaceIP.String())
            if err != nil {
                return nil, fmt.Errorf("regex error matching pattern %s to %s", ifregex, ifaceIP.String())
            }

            if matched {
                ifaceAddr = ifaceIP
                iface = &ifaceToMatch
                break
            }
        }

        // Check Name
        if iface == nil && ifaceAddr == nil {
            for _, ifaceToMatch := range ifaces {
                matched, err := regexp.MatchString(ifregex, ifaceToMatch.Name)
                if err != nil {
                    return nil, fmt.Errorf("regex error matching pattern %s to %s", ifregex, ifaceToMatch.Name)
                }

                if matched {
                    iface = &ifaceToMatch
                    break
                }
            }
        }

        // Check that nothing was matched
        if iface == nil {
            return nil, fmt.Errorf("Could not match pattern %s to any of the available network interfaces", ifregex)
        }
    } else {// 没有指定 由flannel自行决定
        log.Info("Determining IP address of default interface")
        // linux   通过route信息获取gateway所在网卡
        // windows 通过netsh命令行获取信息netsh interface ipv4 show addresses
        if iface, err = ip.GetDefaultGatewayIface(); err != nil {
            return nil, fmt.Errorf("failed to get default interface: %s", err)
        }
    }

上面逻辑很简单,就是根据函数形参进行判断,一般情况下回进入else分支即没有指定任何网卡信息,flannel就会根据路由信息决定使用哪个一个网卡,这里调用GetDefaultGatewayIfacce接口。实际就是采用默认路由所在网卡。 

    // 进入到这里表示 网卡获取成功
    if ifaceAddr == nil {
        ifaceAddr, err = ip.GetIfaceIP4Addr(iface)
        if err != nil {
            return nil, fmt.Errorf("failed to find IPv4 address for interface %s", iface.Name)
        }
    }

    log.Infof("Using interface with name %s and address %s", iface.Name, ifaceAddr)

    if iface.MTU == 0 {
        return nil, fmt.Errorf("failed to determine MTU for %s interface", ifaceAddr)
    }

    var extAddr net.IP

    if len(opts.publicIP) > 0 {
        extAddr = net.ParseIP(opts.publicIP) //根据ip字符串 生成IP对象
        if extAddr == nil {
            return nil, fmt.Errorf("invalid public IP address: %s", opts.publicIP)
        }
        log.Infof("Using %s as external address", extAddr)
    }

    if extAddr == nil {
        log.Infof("Defaulting external address to interface address (%s)", ifaceAddr)
        extAddr = ifaceAddr
    }

    return &backend.ExternalInterface{
        Iface:     iface,
        IfaceAddr: ifaceAddr,
        ExtAddr:   extAddr,
    }, nil
}

这里就是创建ExternalInterface对象,其中publicIP就是主机外网ip,也就是默认路由所在网卡配置的ip地址。publicIP用于跨主机容器通信时使用out ip。

四、总结

至此flannel的main函数介绍完毕,整体看下来main函数还是比较简单。后续会深入探讨flannel具体工作流程。请看下一篇《深入剖析Flannel-创建网卡(1)》

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值