随着公司业务的发展,底层容器环境也须要在各个区域部署,实现多云架构, 使用各个云厂商提供的CNI插件是k8s多云环境下网络架构的一种高效的解法。咱们在阿里云的方案中,便用到了阿里云提供的CNI插件terway。terway所提供的VPC互通的网络方案,方便对接已有的基础设施,同时没有overlay网络封包解包的性能损耗,简单易用,出现网络问题方便诊断。本文对该插件作简单的代码分析,理解其原理,以便后期诊断问题和维护。html
阿里云开源的terway代码有三部分组成:node
CNI plugin: 即CNI插件,实现ADD、DEL、VERSION
三个接口来供kubelet调用, 该插件将kubelet传递的参数进行简单处理后,会经过gRPC调用terwayBackendServer来实现具体的逻辑,例如申请网络设备等。同步调用terwayBackendServer将网络设备分配完毕以后,会经过ipvlanDriver.Driver
进行pod sandbox network namespace的Setup
操做,同时还会经过TC进行流控。该插件会经过daemonSet中initContainer安装到全部node上。面试
在terway的main函数中会启动gRPC server监听请求,同时会建立一个TerwayBackendServer
, TerwayBackendServer封装所有操做逻辑,在newNetworkService
函数中会依次初始化各个子模块实例,具体包括:api
ENIMultiIP
这种模式,对应的就是newENIIPResourceManager
ENIMultiIP模式会申请阿里云弹性网卡并配置多个辅助VPC的IP地址,将这些辅助IP地址映射和分配到Pod中,这些Pod的网段和宿主机网段是一致的,可以实现VPC网络互通。缓存
整个架构以下图所示:安全
首先咱们理解一下kubernetes pod管理模块,该模块用于获取kubernetes pod状态。terway为了支持一些高级的特性,例如流控等,有一些信息没法经过CNI调用传递过来, 仍是得去kubernetes中去查询这些信息。此外CNI调用在一些异常状况下可能没法准确回调CNI插件, 例如用户直接kubectl delete pod --force --graceperiod=0
,此时就须要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
这种模式来讲,具体的实现类是eniIPResourceManager
:闭包
type 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依次初始化各成员变量, 其中
eniIPFactory
。priorityQeueu
类型,即全部空闲的networkResouce经过优先级队列排列,优先级队列的比较函数会比较reverse
字段,reverse
默认是入队时间,也就是该networkResouce的释放的时间,这样作可以尽可能使一个IP释放以后不会被立马被复用。reverse
字段对于一些statueSet的resouce也会进行一些特殊处理,由于statufulSet是有状态workload, 对于IP的释放也会特殊处理,保证其尽量复用。成员变量初始化完成以后会调用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对应一个接受channel ipBacklog
,用于传递分配请求到该goroutine。 每次factory 须要建立(eniIPFactory.Create)一个eniIP时, 会一次遍历当前已经存在的ENI
设备,若是该设备还有空闲的eniIP,就会经过该ipBacklog
channel发送一个元素到该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进行流控,防止瞬间调用过大。
总之,terway的整个实现,逻辑比较清晰,而且扩展性也较高。后期,能够比较方便地在此基础上作一些定制和运维支持,从而很好地融入公司的基础架构设施。
(转载)原文地址:http://www.javashuo.com/article/p-qmvkqoux-co.html
超值推荐:
阿里云双12已开启,云产品冰点价,新用户专享1折起,1核2G云服务器仅需89元/年,229元/3年。买了对于提高技术或者在服务器上搭建自由站点,都是很不错的,若是本身有实际操做,面试+工做中确定是加分项。(老用户能够用家人或朋友的帐号购买,真心便宜&划算)
可“扫码”或者“点击购买 "
##END