在上一篇末尾提到了一个问题:当新容器加入到网络中,其他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作为解决方案,性能不是特别高。
最后希望能和大家一起探讨学习,有什么不清楚的可留言。
本文深入剖析Flannel网络监控机制,包括监控整个子网和自身节点的运作流程,详细解读了Flannel如何通过监控Etcd的变化来感知新容器的加入,以及如何处理事件更新路由表、ARP表和FDB表。

539

被折叠的 条评论
为什么被折叠?



