最近一直在研究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)》。