to inspire confidence in somebody.

0%

Calico的IP分配策略以及存在的一些问题

之前线上运行的K8S集群出现了一个Pod IP无法访问问题,调查了一下,发现和CalicoIP地址的分配策略相关,具体表现为一个/26的IP Block192.168.100.0/26分配给了A机器之后,在另外一台B机器上又出现了该IP Block内的一个IP 192.168.100.10,同时因为A机器上有该IP Block的blackhole路由blackhole 192.168.100.0/26 proto bird,所以导致A机器上所有的Pod访问192.168.100.10时因为黑洞路由原因直接失败。

遇到这个问题之前,只是通过文档大致了解Calico的IP分配策略,没有深入源码看看实际的情况,现在出现了相关问题,还是需要阅读一下相关代码,当然在这过程中也发现了一些问题,有些问题Calico官方也没有很好的解决。

Calico IP分配策略

这里参考Calico 3.9版本代码,其中,CNI插件的流程就省去了,实际在CNI的ipam插件中会调用libcalico-go的相关代码,主要代码在lib/ipam/ipam.go,由于我们线上是默认的配置,也就是不会定义特殊的IP分配策略,因此主要逻辑集中在AutoAssign(ctx context.Context, args AutoAssignArgs)这个接口,而具体实现在autoAssign函数中:

1
func (c ipamClient) autoAssign(ctx context.Context, num int, handleID *string, attrs map[string]string, requestedPools []net.IPNet, version int, host string, maxNumBlocks int) ([]net.IPNet, error) {
2
	...
3
	// 根据当前的节点获取可用的所有IP池
4
	pools, allPools, err := c.determinePools(requestedPools, version, *v3n)
5
	if err != nil {
6
		return nil, err
7
	}
8
9
	// 没有可用的池子就直接返回了
10
	if len(pools) == 0 {
11
		return nil, fmt.Errorf("no configured Calico pools for node %v", host)
12
	}
13
	...
14
	ips := []net.IPNet{}
15
	newIPs := []net.IPNet{}
16
17
	// Record how many blocks we own so we can check against the limit later.
18
	numBlocksOwned := len(affBlocks)
19
20
	for len(ips) < num {
21
		// 所有的可用Block已经尝试完了
22
		if len(affBlocks) == 0 {
23
			logCtx.Infof("Ran out of existing affine blocks for host")
24
			break
25
        }
26
		// 选取当前Block列表第一个Block作为当前Block
27
        cidr := affBlocks[0]
28
		// 把第一个Block去除
29
		affBlocks = affBlocks[1:]
30
31
		for i := 0; i < datastoreRetries; i++ {
32
            ...
33
			// 尝试从当前的Block里分配一个可用的IP
34
			newIPs, err = c.assignFromExistingBlock(ctx, b, num, handleID, attrs, host, true)
35
			if err != nil {
36
				if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok {
37
					logCtx.WithError(err).Debug("CAS error assigning from affine block - retry")
38
					continue
39
				}
40
				logCtx.WithError(err).Warn("Couldn't assign from affine block, try next one")
41
				break
42
            }
43
			// 成功则添加到IP列表
44
			ips = append(ips, newIPs...)
45
			break
46
		}
47
		logCtx.Infof("Block '%s' provided addresses: %v", cidr.String(), newIPs)
48
	}
49
50
	// 获取 Calico IPAM 的配置
51
	// Calico IPAM 有两个全局配置项目(二者之中只能有一个为 true):
52
	// StrictAffinity: 严格的一个 host 对应一个地址块,如果地址块耗尽不再分配新的地址
53
	// AutoAllocateBlocks: 自动分配地址块,如果基于 host affine 的地址块耗尽,将分配新的地址块
54
	// 这部分配置没有对外暴露,只能通过人工配置对应 etcd key 值或者编程调用相关接口来进行配置
55
	// 相关讨论可参考 issue: https://github.com/projectcalico/calico/issues/1577
56
	// 直接设置 etcd key:/calico/ipam/v2/config/ => "{\"strict_affinity\":true}"
57
	config, err := c.GetIPAMConfig(ctx)
58
	if err != nil {
59
		return ips, err
60
    }
61
	// 如果自动分配Block选项打开并且当前IP还不够
62
	if config.AutoAllocateBlocks == true {
63
		rem := num - len(ips)
64
		retries := datastoreRetries
65
		for rem > 0 && retries > 0 {
66
			...
67
			// 先看看还有没有没有被绑定到节点的Block
68
			subnet, err := c.blockReaderWriter.findUnclaimedBlock(ctx, host, version, pools, *config)
69
			if err != nil {
70
				if _, ok := err.(noFreeBlocksError); ok {
71
					// 没有就中断了
72
					logCtx.Info("No free blocks available for allocation")
73
					break
74
				}
75
				log.WithError(err).Error("Failed to find an unclaimed block")
76
				return ips, err
77
			}
78
			logCtx := log.WithFields(log.Fields{"host": host, "subnet": subnet})
79
			logCtx.Info("Found unclaimed block")
80
81
			for i := 0; i < datastoreRetries; i++ {
82
				// 有的话,就绑定到当前节点
83
				pa, err := c.blockReaderWriter.getPendingAffinity(ctx, host, *subnet)
84
				if err != nil {
85
					if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok {
86
						logCtx.WithError(err).Debug("CAS error claiming pending affinity, retry")
87
						continue
88
					}
89
					logCtx.WithError(err).Errorf("Error claiming pending affinity")
90
					return ips, err
91
				}
92
93
				// 新绑定了Block,尝试在新Block里分配IP
94
				b, err := c.getBlockFromAffinity(ctx, pa)
95
				...
96
				newIPs, err := c.assignFromExistingBlock(ctx, b, rem, handleID, attrs, host, config.StrictAffinity)
97
				...
98
				// 分配成功
99
				ips = append(ips, newIPs...)
100
				rem = num - len(ips)
101
				break
102
			}
103
		}
104
105
		if retries == 0 {
106
			return ips, errors.New("Max retries hit - excessive concurrent IPAM requests")
107
		}
108
	}
109
110
	// 如果IP分配还不够(也就是说已绑定的Block里的IP都分完了,并且也没有没有可绑定的新IPBlock了)并且IP还没分完,
111
	// 并且StrictAffinity为false
112
	rem := num - len(ips)
113
	if config.StrictAffinity != true && rem != 0 {
114
		logCtx.Infof("Attempting to assign %d more addresses from non-affine blocks", rem)
115
116
		// Iterate over pools and assign addresses until we either run out of pools,
117
		// or the request has been satisfied.
118
		logCtx.Info("Looking for blocks with free IP addresses")
119
		for _, p := range pools {
120
			// 在所有的Pool里,随机选一个Block,在这个Block里找可用的IP地址
121
			newBlock := randomBlockGenerator(p, host)
122
			for rem > 0 {
123
				// Grab a new random block.
124
				blockCIDR := newBlock()
125
				if blockCIDR == nil {
126
					logCtx.Warningf("All addresses exhausted in pool %s", p.Spec.CIDR)
127
					break
128
				}
129
130
				for i := 0; i < datastoreRetries; i++ {
131
                    b, err := c.blockReaderWriter.queryBlock(ctx, *blockCIDR, "")
132
					...
133
                    newIPs, err := c.assignFromExistingBlock(ctx, b, rem, handleID, attrs, host, false)
134
					...
135
					// 分配成功
136
					ips = append(ips, newIPs...)
137
					rem = num - len(ips)
138
					break
139
				}
140
			}
141
		}
142
	}
143
	// 最后,如果执行到这里,意味着根据配置,无法再分配IP或者干脆全局都没有IP可用了
144
	logCtx.Infof("Auto-assigned %d out of %d IPv%ds: %v", len(ips), num, version, ips)
145
	return ips, nil
146
}

从上面的代码可以看到,Calico分配IP的逻辑为:

  1. 如果节点有已绑定的IP Block,则从这些IP Block中分配IP
  2. 如果第1步失败(没有已绑定的IP Block,或者这些绑定的Block里IP耗尽),判断AutoAllocateBlocks为true,则寻找一个没有被绑定的IP Block,并绑定到当前节点,再执行分配逻辑
  3. 如果第2步失败(AutoAllocateBlocks为false或者没有空闲的IP Block),判断StrictAffinity为false,则从所有IP Blocks中寻找未使用的IP
  4. 经历前1-3步依然没有分配好IP,则失败

阅读了上面的代码,则可以知道在默认配置(StrictAffinity: true, AutoAllocateBlocks: false)下,当节点已有IP Block中没有空闲IP并且也没有空闲IP Block时,就会发生之前所说的情况,而恰好Calico在利用BIRD进行BGP路由广播时,针对每个已绑定的IP Block会设置blackhole路由,从而会导致Pod IP无法访问的问题。

一些问题

根据上面的情况,目前看我们当前使用Calico还是有些问题的,特别是对当前IPPool的处理上。

一方面也是Calico的实现并不是特别好,比如这个Issue里提到的,当前针对节点的IP Block绑定,只有自动绑定的功能,但是没有自动解绑定的功能,解绑只有在删除Calico Node对象的时候才会发生,这会引发一个问题,就是说如果集群中节点有变化了,比如某台机器下线,并有新的节点上线做替换。那如果不手动操作Calico删除对应Node,就会导致之前的IP Block不被释放,也就一直无法没绑定到其他节点。

另一方面也是我们的使用问题,没有及时跟进Calico的更新,线上版本相对版本旧一点,导致在分配Block的时候,只能固定以/26的BlockSize,也就是说一个IPBlock包含64个IP,而目前每台节点的Pod数量限制是默认的110,那么在使用2个Block也就是128个IP的时候就会出现比较大的浪费现象。这个问题在Allow the blockSize to be configured for Calico IPAM中已经得到改进,目标版本v3.3.0。

因此,在高于v3.3.0版本的Calico中可以自定义BlockSize,比如定义为30,也就是一个Block 4个IP,这样可以比较好的提升IP的利用率,当然这样带来的问题是对外广播的路由数量的增加,所以需要权衡,找到一个合适的BlockSize。

参考:

  1. https://zhengyinyong.com/calico-ip-allocation.html