功能划分阿里云开源的terway代码有三部分
CNI plugin 即CNI插件实现ADD、DEL、VERSION三个接口来供kubelet调用 该插件将kubelet传递的参数进行简单处理后会通过gRPC调用terwayBackendServer来实现具体的逻辑例如申请网络设备等。同步调用terwayBackendServer将网络设备分配完毕之后会通过ipvlanDriver.Driver进行pod sandbox network namespace的Setup操作同时还会通过TC进行流控。该插件会通过daemonSet中initContainer安装到所有node上。backend server terway中主要的执行逻辑 会进行IPAM管理并申请对应的网络设备 这部分是本次着重分析的对象。该程序以daemonSet的方式运行在每个节点上。networkPolicy 该部分是借助calico felix实现 完全与上面两部分解耦。我们看到terway创建的网络设备是以cali为前缀的 其实就是为了兼容calico的schema。TerwayBackendServer在terway的main函数中会启动gRPC server监听请求同时会创建一个TerwayBackendServer TerwayBackendServer封装全部操作逻辑在newNetworkService函数中会依次初始化各个子模块实例具体包括ECS client 用来操作ECS client, 所有创建删除更新操作最后都会通过该client进行处理简单封装了一层alicloud的SKDkubernetes pod 管理模块用来同步kubernetes pod信息resouceDB 用来存储状态信息便于重启等操作后恢复状态resourceManager 管理资源分配的实例terway会根据不同的配置生成不同的resourceManager此处我们使用的是ENIMultiIP这种模式对应的就是newENIIPResourceManagerENIMultiIP模式会申请阿里云弹性网卡并配置多个辅助VPC的IP地址将这些辅助IP地址映射和分配到Pod中这些Pod的网段和宿主机网段是一致的能够实现VPC网络互通。整个架构如下图所示首先我们理解一下kubernetes pod管理模块该模块用于获取kubernetes pod状态。terway为了支持一些高级的特性例如流控等有一些信息无法通过CNI调用传递过来 还是得去kubernetes中去查询这些信息。此外CNI调用在一些异常情况下可能无法准确回调CNI插件 例如用户直接kubectl delete pod --force --graceperiod0此时就需要kubernetes作为唯一的single source of truth 保证最后网络设备在pod删除时肯定能够被释放掉。 它内部主要的方法就是GetPod与GetLocalPod。GetPod方法会请求apiserver返回pod信息如果该pod已经在apiserver中删除就会从本地的storage中获取。该storage是用boltDB做为底层存储的一个本地文件每个被处理过的pod都会在该storage中保存一份信息且该pod副本并不会随着apiserver中pod的删除而删除这样后面程序如果需要该pod信息可以从该storage中获取。同时该pod副本会通过异步清理goroutine在pod删除一小时后删除。GetLocalPod是从apiserver获取该node上所有的pod信息该过程是调用kubernetes最多的地方目前两个清理goroutine会每5min调用一次调用量相对较小对apiserver的负载影响不大。该模块也会在本地DB里缓存一份数据便于在kubernetes pod删除后还可以拿到用户信息。其次是resourceDB模块该模块是用来持久化状态信息该DB中记录了当前已分配的pod及其网络设备(networkResource)信息。每次请求/释放设备都会更新该DB。程序重新启动初始化完成之后也会从resouceDB中恢复上次运行的数据。除了基本的分配删除操作会更新该DB, terway还启动异步goroutine定期清理保证异常情况下的最终一致性该goroutine会从apiserve中获取所有pod信息和当前DB中的信息进行对比如果对应的pod已经删除会先释放对应的网络设备然后从DB中删除该记录。同时延迟清理可以实现Statefulset的Pod在更新过程中IP地址保持不变最重要的是resouceManager模块该iterface封装了具体网络设备的操作如下所示:// ResourceManager Allocate/Release/Pool/Stick/GC pod resource // managed pod and resource relationship type ResourceManager interface { Allocate(context *networkContext, prefer string) (types.NetworkResource, error) Release(context *networkContext, resID string) error GarbageCollection(inUseResList map[string]interface{}, expireResList map[string]interface{}) error }从其中三个method可以很明显的看出可以执行的的动作每次CNI插件调用backendServer时 就会调用ResoueceManager进行具体的分配释放操作。对于ENIMultiIP这种模式来说具体的实现类是eniIPResourceManagertype eniIPResourceManager struct { pool pool.ObjectPool }其中只有pool一个成员函数具体的实现类型是simpleObjectPool, 该pool维护了当前所有的ENI信息。当resouceManager进行分配释放网络设备的时候其实是从该pool中进行存取即可func (m *eniIPResourceManager) Allocate(ctx *networkContext, prefer string) (types.NetworkResource, error) { return m.pool.Acquire(ctx, prefer) } func (m *eniIPResourceManager) Release(context *networkContext, resID string) error { if context ! nil context.pod ! nil { return m.pool.ReleaseWithReverse(resID, context.pod.IPStickTime) } return m.pool.Release(resID) } func (m *eniIPResourceManager) GarbageCollection(inUseSet map[string]interface{}, expireResSet map[string]interface{}) error { for expireRes : range expireResSet { if err : m.pool.Stat(expireRes); err nil { err m.Release(nil, expireRes) if err ! nil { return err } } } return nil }由上述代码可见resouceManager实际操作的都是simpleObjectPool这个对象。 我们看看这个pool到底做了那些操作。首先初始化该pool:// NewSimpleObjectPool return an object pool implement func NewSimpleObjectPool(cfg Config) (ObjectPool, error) { if cfg.MinIdle cfg.MaxIdle { return nil, ErrInvalidArguments } if cfg.MaxIdle cfg.Capacity { return nil, ErrInvalidArguments } pool : simpleObjectPool{ factory: cfg.Factory, inuse: make(map[string]types.NetworkResource), idle: newPriorityQueue(), maxIdle: cfg.MaxIdle, minIdle: cfg.MinIdle, capacity: cfg.Capacity, notifyCh: make(chan interface{}), tokenCh: make(chan struct{}, cfg.Capacity), } if cfg.Initializer ! nil { if err : cfg.Initializer(pool); err ! nil { return nil, err } } if err : pool.preload(); err ! nil { return nil, err } log.Infof(pool initial state, capacity %d, maxIdle: %d, minIdle %d, idle: %s, inuse: %s, pool.capacity, pool.maxIdle, pool.minIdle, queueKeys(pool.idle), mapKeys(pool.inuse)) go pool.startCheckIdleTicker() return pool, nil }可以看到在创建的时候会根据传入的config依次初始化各成员变量 其中factory 成员用来分配网络设备会调用ECS SDK进行分配资源分配之后将信息存储在pool之中具体的实现是eniIPFactory。inuse 存储了当前所有正在使用的networkResourceidle 存储了当前所有空闲的networkResource, 即已经通过factory分配好但是还未被某个pod实际使用。如果某个network resouce不再使用也会归还到该idle之中。 通过这种方式pool具备一定的缓充能力避免频繁调用factory进行分配释放。idle为priorityQeueu类型即所有空闲的networkResouce通过优先级队列排列优先级队列的比较函数会比较reverse字段reverse默认是入队时间也就是该networkResouce的释放的时间这样做能够尽量使一个IP释放之后不会被立马被复用。reverse字段对于一些statueSet的resouce也会进行一些特殊处理因为statufulSet是有状态workload, 对于IP的释放也会特殊处理保证其尽可能复用。maxIdle, minIdle 分别表示上述idle队列中允许的最大和最小个数 minIdle是为了提供有一定的缓冲能力但该值并不保证最大是为了防止缓存过多如果空闲的networkResouce太多没有被使用就会释放一部分IP地址不止是节点级别的资源也会占用整个vpc/vswitch/安全组的资源太多的空闲可能会导致其他节点或者云产品分配不出IP。capacity 是该pool的容量最大能分配的networkResouce的个数。该值可以自己指定 但如果超过该ECS能允许的最大个数就会被设置成允许的最大个数。tokenCh 是个buffered channel, 容量大小即为上面capacity的值被做token bucket。 pool初始化的时候会将其中放满元素后面运行过程中中只要能从该channel中读取到元素则意味着该pool还没有满。每次调用factory申请networkResouce之前会从该channel中读取一个元素 每次调用factory释放networkDevice会从该channel中放入一个元素。成员变量初始化完成之后会调用Initializer, 该函数会回调一个闭包函数定义在newENIIPResourceManager中 当程序启动时resouceManager通过读取存储在本地磁盘也就是resouceDB中的信息获取当前正在使用的networkResouce然后通过ecs获取当前所有eni设备及其ip, 依次遍历所有ip判断当前是否在使用分别来初始化inuse和idle。这样可以保证程序重启之后可以重构内存中的pool数据信息。然后会调用preload,该函数确保pool(idle)中有minIdle个空闲元素, 防止启动时大量调用factory。最后会进行go pool.startCheckIdleTicker()异步来goroutine中调用checkIdle定期查询pool(idle)中的元素是否超过maxIdle个元素 如果超过则会调用factory进行释放。同时每次调用factory也会通过notifyCh来通知该goroutine执行检查操作。pool结构初始化完成之后resouceManager中所有对于networkResource的操作都会通过该pool进行该pool在必要条件下再调用factory进行分配释放。factory的具体实现是eniIPFactory, 用来调用ecs SDK进行申请释放eniIP, 并维护对应的数据结构。不同于直接使用eni设备ENIMultiIP模式会为每个eni设备会有多个eniIP。eni设备是通过ENI结构体标识 eniIP通过ENIIP结构体标识。terway会为每个ENI创建一个goroutine, 该ENI上所有eniIP的分配释放都会在goroutine内进行factory通过channel与该groutine通信 每个goroutine对应一个接受channelipBacklog用于传递分配请求到该goroutine。 每次factory 需要创建(eniIPFactory.Create)一个eniIP时 会一次遍历当前已经存在的ENI设备如果该设备还有空闲的eniIP就会通过该ipBacklogchannel发送一个元素到该ENI设备的goroutine进行请求分配 当goroutine将eniIP分配完毕之后通过factory 的resultChan通知factory, 这样factory就成功完成一次分配。 如果所有的ENI的eniIP都分配完毕会首先创建ENI设备及其对应goroutine。因为每个ENI设备会有个主IP 所以首次分配ENI不需要发送请求到ipBacklog, 直接将该主ip返回即可。对应的释放(Dispose)就是先释放eniIP 等到只剩最后一个eniIP(主eniIP)时会释放整个ENI设备。对于所有ecs调用都会通过buffer channel进行流控防止瞬间调用过大。总结