深入剖析Flannel-监控

本文深入剖析Flannel网络监控机制,包括监控整个子网和自身节点的运作流程,详细解读了Flannel如何通过监控Etcd的变化来感知新容器的加入,以及如何处理事件更新路由表、ARP表和FDB表。

在上一篇末尾提到了一个问题:当新容器加入到网络中,其他flannel是如何感知的呢?这个就要取决flannel的监控这部分实现了。flannel监控实际上是监视etcd某些key的变化,当这些key有变化时etcd会发送响应给flannel,这一点需要明确。接下来看一下flannel是如何实现的

一、监控子网

flannel监控处理一共有两个分别:监控整个子网和监控自己所在网络。先介绍一下监控整个子网,在main函数中有一个处这样的代码:

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

创建一个协程,Run函数定义在vxlan_network.go中,具体实现如下:

func (nw *network) Run(ctx context.Context) {
    wg := sync.WaitGroup{}

    log.V(0).Info("watching for new subnet leases")
    events := make(chan []subnet.Event)
    wg.Add(1)
    /**
     * 对所有租约进行监控 调用watch.go 中WatchLeases函数
     */
    go func() {
        subnet.WatchLeases(ctx, nw.subnetMgr, nw.SubnetLease, events)
        log.V(1).Info("WatchLeases exited")
        wg.Done()
    }()

    defer wg.Wait()
    // 死循环 用于事件处理
    for {
        select {
        case evtBatch := <-events:
            nw.handleSubnetEvents(evtBatch) //有事件发生需要处理

        case <-ctx.Done():
            return
        }
    }
}

从上面代码可知,主要有两部分处理:启动协程进行监控和事件处理。 

1.1、监控处理

// WatchLeases performs a long term watch of the given network's subnet leases
// and communicates addition/deletion events on receiver channel. It takes care
// of handling "fall-behind" logic where the history window has advanced too far
// and it needs to diff the latest snapshot with its saved state and generate events
// 调用的地方在 vxlan_network.go中Run函数
// 发送http请求类似:/v2/keys/coreos.com/network/subnets?recursive=true&wait=true&waitIndex=96
func WatchLeases(ctx context.Context, sm Manager, ownLease *Lease, receiver chan []Event) {
    lw := &leaseWatcher{
        ownLease: ownLease,
    }
    var cursor interface{}
    // 死循环 循环监听
    for {
        res, err := sm.WatchLeases(ctx, cursor)
        if err != nil {
            if err == context.Canceled || err == context.DeadlineExceeded {
                return
            }

            log.Errorf("Watch subnets: %v", err)
            time.Sleep(time.Second)
            continue
        }

        cursor = res.Cursor

        var batch []Event
        // 表示有事件发生 update和reset函数中都会过滤掉自己所在租约事件
        if len(res.Events) > 0 {
            batch = lw.update(res.Events)
        } else {
            batch = lw.reset(res.Snapshot)
        }

        if len(batch) > 0 {
            receiver <- batch //接收端在vxlan_network.go中Run
        }
    }
}

说明:

1)上面说过监控实际是向etcd发送监控请求,如果etcd数据有变化,etcd会通知客户端,但是需要明确一点这里是同步方式,即flannel会发起http请求,直到etcd有数据变化etcd才会返回http应答,在未得到响应前flannel一直在这里等待。对于函数WatchLeases发送的http请求格式类似于:/v2/keys/coreos.com/network/subnets?recursive=true&wait=true&waitIndex=96。对url解释说明:

参数说明
recursive=true

表示递归监控。

subnets是目录,参数设置为true表示该目录下面只有有数据变化就返回数据。

wait=true&waitIndex=96

wait和waitIndex是成对出现,表示期望获取index为96的数据,这种就会有两种可能:

如果etcd当前index值小于96,则会一直等待index变为96

如果etcd当前index值大于等于96,则会立即返回数据。

注意:96只是举例说明

2)cursor游标,实际取值为etcd中数据索引,即上面waitIndex值,注意:每次操作etcd中数据index值就会变化

下面来看一下WatchLeases实现内容:

/**
 * 监视所有租约  实质是监控子网是否有变化(新增、删除、超时)
 * @param ctx 上下文
 * @param sn 子网管理对象
 * @param cursor 游标
 */
func (m *LocalManager) WatchLeases(ctx context.Context, cursor interface{}) (LeaseWatchResult, error) {
    if cursor == nil {
        return m.leasesWatchReset(ctx)
    }

    nextIndex, err := getNextIndex(cursor)
    if err != nil {
        return LeaseWatchResult{}, err
    }

    evt, index, err := m.registry.watchSubnets(ctx, nextIndex)

    switch {
    case err == nil: // 返回正确数据
        return LeaseWatchResult{
            Events: []Event{evt},
            Cursor: watchCursor{index},
        }, nil

    case isIndexTooSmall(err):
        log.Warning("Watch of subnet leases failed because etcd index outside history window")
        return m.leasesWatchReset(ctx)

    default:
        return LeaseWatchResult{}, err
    }
}

该函数处理还是比较简单,这里需要提示游标cursor保存的值为:etcd最后一次操作index+1。下面在看一下watchSubnets函数实现内容:

/**
 * 监控整个子网
 * @param ctx 上下文
 * @param since 索引值
 * @param sn 子网地址
 * 发送http请求: /v2/keys/coreos.com/network/subnets?recursive=true&wait=true&waitIndex=96
 *               其中96=since+1,监控整个子网
 */
func (esr *etcdSubnetRegistry) watchSubnets(ctx context.Context, since uint64) (Event, uint64, error) {
    key := path.Join(esr.etcdCfg.Prefix, "subnets")
    opts := &etcd.WatcherOptions{
        AfterIndex: since,
        Recursive:  true,
    }
    /**
     * 注意Next发送http请求 这里会一直阻塞 直到有http reponse响应回来才会继续执行后流程
     * 那么http response什么时候返回呢? 只有子网数据有变化后才会有响应,那么子网数据变化又指的是什么呢?
     * 有新的节点加入子网或者已有子网被删除才会返回http response
     */
    e, err := esr.client().Watcher(key, opts).Next(ctx)
    if err != nil {
        return Event{}, 0, err
    }
    // 解析响应
    evt, err := parseSubnetWatchResponse(e)
    return evt, e.Node.ModifiedIndex, err
}

这里在注释中说明的很清楚,就是获取etcd数据,如果指定index不存在则会阻塞在这里直到获取到数据。当数据返回后会经parseSubnetWatchResponse封装成事件返回给上层调用者。下面来看一下事件处理流程

1.2、事件处理

首先我们需要思考一下,事件处理流程做了哪些事情呢?应该能很容易想到,根据事件类型,对路由/转发表进行设置。对于vxlan这种模式,转发是通过设置fdb表,由内核进行转发处理而flannel中的udp模式是由flannel用户层程序进行转发,所以vxlan模式在性能上要优于udp模式。这一点需要清楚。

/**
 * 处理事件 主要操作转发表、arp、路由表项
 * @param batch 事件对象
 */
func (nw *network) handleSubnetEvents(batch []subnet.Event) {
    for _, event := range batch {
        sn := event.Lease.Subnet
        attrs := event.Lease.Attrs
        if attrs.BackendType != "vxlan" {
            log.Warningf("ignoring non-vxlan subnet(%s): type=%v", sn, attrs.BackendType)
            continue
        }
        // 解析json格式化
        var vxlanAttrs vxlanLeaseAttrs
        if err := json.Unmarshal(attrs.BackendData, &vxlanAttrs); err != nil {
            log.Error("error decoding subnet lease JSON: ", err)
            continue
        }

        // This route is used when traffic should be vxlan encapsulated
        vxlanRoute := netlink.Route{
            LinkIndex: nw.dev.link.Attrs().Index,
            Scope:     netlink.SCOPE_UNIVERSE,
            Dst:       sn.ToIPNet(),
            Gw:        sn.IP.ToIP(),
        }
        vxlanRoute.SetFlag(syscall.RTNH_F_ONLINK)

        // directRouting is where the remote host is on the same subnet so vxlan isn't required.
        directRoute := netlink.Route{
            Dst: sn.ToIPNet(),
            Gw:  attrs.PublicIP.ToIP(),
        }
        var directRoutingOK = false
        if nw.dev.directRouting {
            if dr, err := ip.DirectRouting(attrs.PublicIP.ToIP()); err != nil {
                log.Error(err)
            } else {
                directRoutingOK = dr
            }
        }

        switch event.Type {
        case subnet.EventAdded: //添加事件
            if directRoutingOK { // 直接路由方式
                log.V(2).Infof("Adding direct route to subnet: %s PublicIP: %s", sn, attrs.PublicIP)

                if err := netlink.RouteReplace(&directRoute); err != nil {
                    log.Errorf("Error adding route to %v via %v: %v", sn, attrs.PublicIP, err)
                    continue
                }
            } else {
                log.V(2).Infof("adding subnet: %s PublicIP: %s VtepMAC: %s", sn, attrs.PublicIP, net.HardwareAddr(vxlanAttrs.VtepMAC))
                // 添加arp表项
                if err := nw.dev.AddARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
                    log.Error("AddARP failed: ", err)
                    continue
                }
                // 添加fdb表项
                if err := nw.dev.AddFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
                    log.Error("AddFDB failed: ", err)

                    // Try to clean up the ARP entry then continue
                    if err := nw.dev.DelARP(neighbor{IP: event.Lease.Subnet.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
                        log.Error("DelARP failed: ", err)
                    }

                    continue
                }

                // Set the route - the kernel would ARP for the Gw IP address if it hadn't already been set above so make sure
                // this is done last.
                // 更新路由表项
                if err := netlink.RouteReplace(&vxlanRoute); err != nil {
                    log.Errorf("failed to add vxlanRoute (%s -> %s): %v", vxlanRoute.Dst, vxlanRoute.Gw, err)

                    // Try to clean up both the ARP and FDB entries then continue
                    if err := nw.dev.DelARP(neighbor{IP: event.Lease.Subnet.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
                        log.Error("DelARP failed: ", err)
                    }

                    if err := nw.dev.DelFDB(neighbor{IP: event.Lease.Attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
                        log.Error("DelFDB failed: ", err)
                    }

                    continue
                }
            }
        case subnet.EventRemoved: //删除事件
            ...
        default:
            log.Error("internal error: unknown event type: ", int(event.Type))
        }
    }
}

这部分代码逻辑非常简单明了,就是操作:arp表项、fdb表项、路由表项。操作这些表项使用的netlink第三方库,这里不在深入展开,有兴趣的可自行阅读相关代码(最底层使用的是系统调用)。

二、监控自己

上面介绍了监控整个网络,那么监控自己是在什么地方呢?在main函数中:

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

监控自己的流程与监控整个子网罗成大同小异,最终调用函数为watchSubnet。此处就不在深入说明了。有一点不同的是监控自己处理的事件实际上不是转发表项而是ip租约处理,事件处理流程如下:

/**
 * 监控租约
 * @param ctx 上下文
 * @param sm  子网管理对象
 * @param bn  backend管理对象
 * @param wg  waitgroup对象
 */
func MonitorLease(ctx context.Context, sm subnet.Manager, bn backend.Network, wg *sync.WaitGroup) error {
    // Use the subnet manager to start watching leases.
    evts := make(chan subnet.Event)

    wg.Add(1)
    go func() {
        subnet.WatchLease(ctx, sm, bn.Lease().Subnet, evts)
        wg.Done()
    }()
    // 计算超时时间
    renewMargin := time.Duration(opts.subnetLeaseRenewMargin) * time.Minute
    dur := bn.Lease().Expiration.Sub(time.Now()) - renewMargin

    //死循环 事件处理 始终监控 当该函数退出表示 flanneld将要退出
    for {
        select {
        case <-time.After(dur):
            err := sm.RenewLease(ctx, bn.Lease()) //发生超时需要重新获取租约
            if err != nil {
                log.Error("Error renewing lease (trying again in 1 min): ", err)
                dur = time.Minute
                continue
            }

            log.Info("Lease renewed, new expiration: ", bn.Lease().Expiration)
            dur = bn.Lease().Expiration.Sub(time.Now()) - renewMargin

        case e := <-evts:
            switch e.Type {
            case subnet.EventAdded:
                bn.Lease().Expiration = e.Lease.Expiration
                dur = bn.Lease().Expiration.Sub(time.Now()) - renewMargin
                log.Infof("Waiting for %s to renew lease", dur)

            case subnet.EventRemoved:
                log.Error("Lease has been revoked. Shutting down daemon.")
                return errInterrupted
            }

        case <-ctx.Done():
            log.Infof("Stopped monitoring lease")
            return errCanceled
        }
    }
}

三、总结

至此flannel监控流程就介绍完毕了而且整个flannel源码介绍到这里也就结束了。这里总结一下我对flannel阅读感想:

1、flannel这套代码,代码量不是很大,逻辑也很清晰不是特别复杂。

2、对于如何解决跨主机容器建通信,起到了指明灯作用。

3、由于我是兴趣爱好,不能评价其性能如何,我也是通过相关博客,使用flannel作为解决方案,性能不是特别高。

最后希望能和大家一起探讨学习,有什么不清楚的可留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值