背景

生产要把网络插件换成calico,对于IP的分配方法有点存疑,花了2天看下了calico-ipam的源码,学习下calico的IP分配逻辑,对比生产环境,看看有没有明显的坑。

calico比较突出的一点是可以指定单个pod的ip,也可以指定一个RS的网段,但实际用的比较少,首先不可能单个pod部署,一个rs指定一个ippool的意义并不是很大,所以主要看下自动分配的代码逻辑,代码其实不是很多,但几个概念挺绕的,根据目前的了解,简单画了一个流程图:

IPAM的入口还是老三样,分IP 从cmdAdd开始看:

func cmdAdd(args *skel.CmdArgs) error {conf := types.NetConf{}if err := json.Unmarshal(args.StdinData, &conf); err != nil {return fmt.Errorf("failed to load netconf: %v", err)}nodename := utils.DetermineNodename(conf)utils.ConfigureLogging(conf)calicoClient, err := utils.CreateClient(conf)if err != nil {return err}epIDs, err := utils.GetIdentifiers(args, nodename)if err != nil {return err}epIDs.WEPName, err = epIDs.CalculateWorkloadEndpointName(false)if err != nil {return fmt.Errorf("error constructing WorkloadEndpoint name: %s", err)}handleID := utils.GetHandleID(conf.Name, args.ContainerID, epIDs.WEPName)
  • 上来和其他IPAM 一样,解析conf文件,让conf配置变成一个对象
  • 基于配置获取当前node节点的名字(DetermineNodename()), 如果配置文件没有指定nodename,则会直接获取当前node的hostname,所以node的hostname 请不要修改,修改后可能会导致后面的一系列失败。
  • 获取handleID, 该ID 后续会和分配的IP地址做绑定,存到etcd中,handleID的组成规则是ns+containerid, 在容器释放IP的时候,会根据该key找到对应IP,然后释放IP。
func GetHandleID(netName, containerID, workload string) string {handleID := fmt.Sprintf("%s.%s", netName, containerID)

calico默认只使用ipv4的地址,除非配置里指定了ipv6的分配,不然不会启用,calico 可以提供指定网段/IP的功能,kubernetes通过使用cni.projectcalico.org/ipAddrscni.projectcalico.org/ipv4pools 这2个配置指定IP,所以即使使用自动分配,同样会通过ResolvePools函数,确认下是不是需要使用配置里的指定网段,如果没有指定,返回会为空,如果指定了,会和ippool的数组进行匹配,确认可以匹配后则返回。

     // Default to assigning an IPv4 addressnum4 := 1if conf.IPAM.AssignIpv4 != nil && *conf.IPAM.AssignIpv4 == "false" {num4 = 0}// Default to NOT assigning an IPv6 addressnum6 := 0if conf.IPAM.AssignIpv6 != nil && *conf.IPAM.AssignIpv6 == "true" {num6 = 1}logger.Infof("Calico CNI IPAM request count IPv4=%d IPv6=%d", num4, num6)v4pools, err := utils.ResolvePools(ctx, calicoClient, conf.IPAM.IPv4Pools, true)if err != nil {return err}v6pools, err := utils.ResolvePools(ctx, calicoClient, conf.IPAM.IPv6Pools, false)if err != nil {return err}

如果rs的yaml里没有指定网段, 那么下面的返回其实就是个空数组

func ResolvePools(ctx context.Context, c client.Interface, pools []string, isv4 bool) ([]cnet.IPNet, error) {// First, query all IP pools. We need these so we can resolve names to CIDRs.pl, err := c.IPPools().List(ctx, options.ListOptions{})if err != nil {return nil, err}// Iterate through the provided pools. If it parses as a CIDR, just use that.// If it does not parse as a CIDR, then attempt to lookup an IP pool with a matching name.result := []cnet.IPNet{}for _, p := range pools {_, cidr, err := net.ParseCIDR(p)if err != nil {......}ip := cidr.IPif isv4 && ip.To4() == nil {return nil, fmt.Errorf("%q isn't a IPv4 address", ip)}if !isv4 && ip.To4() != nil {return nil, fmt.Errorf("%q isn't a IPv6 address", ip)}result = append(result, cnet.IPNet{IPNet: *cidr})}return result, nil
}

之后开始正式分配IP地址,先初始化掉自动分配IP的参数结构体:

     assignArgs := ipam.AutoAssignArgs{Num4:             num4, // 分配的IPV4数量Num6:             num6, // 分配的IPV6数量HandleID:         &handleID, // handleIDHostname:         nodename, // node名字IPv4Pools:        v4pools, // 初定选择的ipv4的poolIPv6Pools:        v6pools, // 初定选择的ipv6的poolMaxBlocksPerHost: maxBlocks, // 每台机器最大的block数量,初始为0Attrs:            attrs, // 容器的基本信息,ns,containerID等}

开始正式执行IP分配:

func (c ipamClient) AutoAssign(ctx context.Context, args AutoAssignArgs) ([]net.IPNet, []net.IPNet, error) {
......var v4list, v6list []net.IPNetif args.Num4 != 0 {// Assign IPv4 addresses.log.Debugf("Assigning IPv4 addresses")for _, pool := range args.IPv4Pools {if pool.IP.To4() == nil {return nil, nil, fmt.Errorf("provided IPv4 IPPools list contains one or more IPv6 IPPools")}}v4list, err = c.autoAssign(ctx, args.Num4, args.HandleID, args.Attrs, args.IPv4Pools, 4, hostname, args.MaxBlocksPerHost, args.HostReservedAttrIPv4s)if err != nil {log.Errorf("Error assigning IPV4 addresses: %v", err)return v4list, nil, err}}......return v4list, v6list, nil
}

实际分配IP的逻辑在这里autoAssign,所幸作者代码习惯好,注释很给力:

// First, get the existing host-affine blocks.
......pools, affBlocks, err := c.prepareAffinityBlocksForHost(ctx, requestedPools, version, host, rsvdAttr)if err != nil {return nil, err}

首先会进行block的亲和性检查, prepareAffinityBlocksForHost, 比较核心的代码如下:

func (c ipamClient) prepareAffinityBlocksForHost(ctx context.Context,requestedPools []net.IPNet,version int,host string,rsvdAttr *HostReservedAttr) ([]v3.IPPool, []net.IPNet, error) {
......// 判断下掩码的长度maxPrefixLen, err := getMaxPrefixLen(version, rsvdAttr)if err != nil {return nil, nil, err}
......// Determine the correct set of IP pools to use for this request.pools, allPools, err := c.determinePools(ctx, requestedPools, version, *v3n, maxPrefixLen)if err != nil {return nil, nil, err}
......affBlocks, affBlocksToRelease, err := c.blockReaderWriter.getAffineBlocks(ctx, host, version, pools)......for _, block := range affBlocksToRelease {// Determine the pool for each block.pool, err := c.blockReaderWriter.getPoolForIP(net.IP{block.IP}, allPools)
......// Determine if the pool selects the current node, refusing to release this particular block affinity if so.blockSelectsNode, err := pool.SelectsNode(*v3n)if err != nil {logCtx.WithError(err).WithField("pool", pool).Error("Failed to determine if node matches pool, skipping")continue}if blockSelectsNode {logCtx.WithFields(log.Fields{"pool": pool, "block": block}).Debug("Block's pool still selects node, refusing to remove affinity")continue}// Release the block affinity, requiring it to be empty.for i := 0; i < datastoreRetries; i++ {if err = c.blockReaderWriter.releaseBlockAffinity(ctx, host, block, true); err != nil {......}}return pools, affBlocks, nil
}

这里提到了block,block说白了就是将calico的ippool的网段进行了拆分,主要目的是为了减少路由条目,默认是/26位的,所以参数里的blocksize就是26, 但有了block之后,有个比较大的限制就是blockAffinity, 即多个block 默认会和一个node绑定(亲和性), 即这个node上的pod默认都是这几个block网段的,为啥要这样?试想一下,calico没有自动的路由汇总,所以只能通过/26的block网段减少路由条目,一旦一台node上跑了所有网段的服务器,会造成什么问题?最明显的就是主机上的明细路由会非常非常多,维护起来不方便。

再看下亲和性检查会执行大致以下几个步骤:

  • getMaxPrefixLen() 判断block是不是大于32(ipv4情况下),所以block分片的最小值,就是一条明细路由
  • c.determinePools() 决定了哪些pool 可以被分配,代码不贴了,大致逻辑是先拉取所有状态是enable的ippool, 然后遍历和上面为空的IPv4Pools()match,如果match 上了,返回matchpool,因为默认不指定网段,所以这块逻辑是不用的,然后会判断enable的ippool是不是在指定node上的(nodeselect),所以最终只会返回nodeSelect匹配,且状态都是enable的ippool
  • c.blockReaderWriter.getAffineBlocks(), 该方法遍历了blockAffinity对象,并开始进行以下判断:

    a. 如果c.determinePools() 获得的ippool数量为0,那么将所有遍历出来的blockAffinity对象放入blocksInPool内,等待后续分配

    b. 如果c.determinePools() 获得的ippool数量不为0,则遍历返回的上述方法返回的pool列表,如果有pool 包含了blockAffinity对象,则把该blockAffinity对象放入blocksInPool内,等待后续分配,如果不包含,则把该blockAffinity对象放入blocksNotInPool内,等待后续释放

  • 之后将blocksNotInPool内的blockAffinity对象进行释放,先进行遍历,先获取该对象内IP所在的网段(ippool),根据返回值进行判断:

    a. 如果该则不释放blockAffinity没有匹配到IP池,则跳过。

    b. 则如果该IP池选择了当前节点,则不释放blockAffinity。

    c. 都不满足则调用releaseBlockAffinity函数进行释放, 出现其他错误时,会进行100次以内的重试。

  • 最后返回所有可用的ippool,符合亲和性规则的blockAffinity对象数组

然后再回到autoAssign方法

func (c ipamClient) autoAssign(ctx context.Context, num int, handleID *string, attrs map[string]string, requestedPools []net.IPNet, version int, host string, maxNumBlocks int, rsvdAttr *HostReservedAttr) ([]net.IPNet, error) {......pools, affBlocks, err := c.prepareAffinityBlocksForHost(ctx, requestedPools, version, host, rsvdAttr)if err != nil {return nil, err}......s := &blockAssignState{client:                c,version:               version,host:                  host,pools:                 pools,remainingAffineBlocks: affBlocks,hostReservedAttr:      rsvdAttr,allowNewClaim:         true,}// Allocate the IPs.for len(ips) < num {
......b, newlyClaimed, err := s.findOrClaimBlock(ctx, 1)
......// We have got a block b.for i := 0; i < datastoreRetries; i++ {newIPs, err := c.assignFromExistingBlock(ctx, b, rem, handleID, attrs, host, config.StrictAffinity)......ips = append(ips, newIPs...)rem = num - len(ips)break}}rem := num - len(ips)if config.StrictAffinity != true && rem != 0 {......newBlockCIDR := randomBlockGenerator(p, host)......}}}logCtx.Infof("Auto-assigned %d out of %d IPv%ds: %v", len(ips), num, version, ips)return ips, nil
}

上一步做的事情其实是找亲和性符合的block,现在才是真正的IP分配:

  • 第一次分IP用的是findOrClaimBlock方法,分配逻辑是先把所以符合亲和性的block取出,取出第一个block,然后根据block进行遍历,查询出是否空闲的IP够,如果够,则直接返回这个block。
  • 如果发现所有block的ip都不够了,则检查if config.AutoAllocateBlocks 是否开启,如果开启了,则新建一个全新的block进行分配。
  • 如果上述完成了,ipam会获取到一个IP足够的block, 然后通过*assignFromExistingBlock**分配IP,在该方法里会先检查StrictAffinity配置,如果是强制亲和的策略,则直接返回。
  • 如果是分强制亲和策略,则继续遍历pool,获取其他的非亲和的block,从其他的block里借IP,完成最终的IP 分配。

总结

calico分IP的流程基本都是围绕着block展开的,在规划的时候,根据上面的代码逻辑会需要注意以下几个问题:

  • handleId是存在etcd里,是pod 和 其对应IP的信息的主要Key,而handleID的组成是ns+container,所以namespaces一定要提前规划好,避免后续踩坑。
  • node的主机名配置不会去写,所以大多数都是获取node的当前主机名,所以尽量不要修改主机名字
  • 由于block的存在,需要规划好blockSize的大小,计算和pod数量的关系,避免浪费或者不够。
  • node下线后,请一定要把从集群中删除node,不然因为亲和性强匹配的规则,可能会导致下线node的IP永远无法再次被使用
  • 默认的blockSize是26位,也就64个地址,所以不建议ippool设置的太大,避免产生的block太多,每次创建新block都要取etcd里一个个get,遍历一遍虽然快,但高性能场景下还是不太好。
  • 如果没有block了,且亲和性检查关着,会需要取其他node的block里借IP,借用IP的操作本质上就是每个block重新分IP的逻辑,block数量多的话也一定会带来性能损耗。
个人公众号, 分享一些日常开发,运维工作中的日常以及一些学习感悟,欢迎大家互相学习,交流

calico源码分析-ipam(1)相关推荐

  1. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  2. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  3. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  4. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  5. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  6. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  7. View的Touch事件分发(二.源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...

  8. MyBatis原理分析之四:一次SQL查询的源码分析

    上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的. 准备工作 Mybatis完成一次SQL查询需要使用的代码如下: Java代码   String res ...

  9. [转]slf4j + log4j原理实现及源码分析

    slf4j + log4j原理实现及源码分析 转载于:https://www.cnblogs.com/jasonzeng888/p/6051080.html

  10. Spark源码分析之七:Task运行(一)

    在Task调度相关的两篇文章<Spark源码分析之五:Task调度(一)>与<Spark源码分析之六:Task调度(二)>中,我们大致了解了Task调度相关的主要逻辑,并且在T ...

最新文章

  1. SOC,System on-a-Chip技术初步
  2. 技术图文:02 创建型设计模式(上)
  3. blfs(systemv版本)学习笔记-使用apache创建简单的网页服务器
  4. mysql gtid binlog_MySQL之-四步实现BinLog Replication升级为GTIDs Replication的代码实例
  5. Pytorch 为什么每一轮batch需要设置optimizer.zero_grad
  6. python字典遍历方法
  7. openssh漏洞_技术干货 | OpenSSH命令注入漏洞复现(CVE202015778)
  8. Python获取本机所有网卡的MAC地址
  9. “水仙花数”你了解多少??
  10. fckeditor php 不显示,PHP Fckeditor上传文件(或图片)中文显示为乱码的解决方法
  11. 40个良好用户界面设计Tips
  12. 让IIS7.0.0.0支持 .iso .7z .torrent .apk等文件下载的设置方法
  13. 《JavaScript 高级程序设计(第四版)》—— 06 集合引用类型
  14. win10系统安装eplan2.7加密狗驱动蓝屏问题解决
  15. 单目标跟踪SiamMask:特定目标车辆追踪 part1
  16. jar 坐标系转换工具_谷歌百度经纬度转换
  17. MATLAB中绘制椭圆
  18. JavaScript用法------判断二维数组
  19. 用PHPnow运行PHP项目以及PHPnow相关问题的解决
  20. Gentoo Linux+KDE Plasma桌面安装教程

热门文章

  1. 高通 linux usb 休眠,系统休眠(System Suspend)和设备中断处理
  2. Java 第三阶段增强分析需求,代码实现能力【满汉楼】
  3. 四位共阳极数码管显示函数_DS1302,四位共阳极数码管显示时钟,可调时间
  4. arm mali 天梯图_手机最新CPU天梯图 2018年12月手机最新处理器排名表
  5. TensorFlow案例---概率学中的逆概率
  6. UUID是什么 ?
  7. Android N 的新特性
  8. Windows网络共享方式
  9. 前端JavaScript学习网站(重磅推荐)
  10. 获取小程序码所携带的参数