之前线上运行的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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) {
...
// 根据当前的节点获取可用的所有IP池
pools, allPools, err := c.determinePools(requestedPools, version, *v3n)
if err != nil {
return nil, err
}

// 没有可用的池子就直接返回了
if len(pools) == 0 {
return nil, fmt.Errorf("no configured Calico pools for node %v", host)
}
...
ips := []net.IPNet{}
newIPs := []net.IPNet{}

// Record how many blocks we own so we can check against the limit later.
numBlocksOwned := len(affBlocks)

for len(ips) < num {
// 所有的可用Block已经尝试完了
if len(affBlocks) == 0 {
logCtx.Infof("Ran out of existing affine blocks for host")
break
}
// 选取当前Block列表第一个Block作为当前Block
cidr := affBlocks[0]
// 把第一个Block去除
affBlocks = affBlocks[1:]

for i := 0; i < datastoreRetries; i++ {
...
// 尝试从当前的Block里分配一个可用的IP
newIPs, err = c.assignFromExistingBlock(ctx, b, num, handleID, attrs, host, true)
if err != nil {
if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok {
logCtx.WithError(err).Debug("CAS error assigning from affine block - retry")
continue
}
logCtx.WithError(err).Warn("Couldn't assign from affine block, try next one")
break
}
// 成功则添加到IP列表
ips = append(ips, newIPs...)
break
}
logCtx.Infof("Block '%s' provided addresses: %v", cidr.String(), newIPs)
}

// 获取 Calico IPAM 的配置
// Calico IPAM 有两个全局配置项目(二者之中只能有一个为 true):
// StrictAffinity: 严格的一个 host 对应一个地址块,如果地址块耗尽不再分配新的地址
// AutoAllocateBlocks: 自动分配地址块,如果基于 host affine 的地址块耗尽,将分配新的地址块
// 这部分配置没有对外暴露,只能通过人工配置对应 etcd key 值或者编程调用相关接口来进行配置
// 相关讨论可参考 issue: https://github.com/projectcalico/calico/issues/1577
// 直接设置 etcd key:/calico/ipam/v2/config/ => "{\"strict_affinity\":true}"
config, err := c.GetIPAMConfig(ctx)
if err != nil {
return ips, err
}
// 如果自动分配Block选项打开并且当前IP还不够
if config.AutoAllocateBlocks == true {
rem := num - len(ips)
retries := datastoreRetries
for rem > 0 && retries > 0 {
...
// 先看看还有没有没有被绑定到节点的Block
subnet, err := c.blockReaderWriter.findUnclaimedBlock(ctx, host, version, pools, *config)
if err != nil {
if _, ok := err.(noFreeBlocksError); ok {
// 没有就中断了
logCtx.Info("No free blocks available for allocation")
break
}
log.WithError(err).Error("Failed to find an unclaimed block")
return ips, err
}
logCtx := log.WithFields(log.Fields{"host": host, "subnet": subnet})
logCtx.Info("Found unclaimed block")

for i := 0; i < datastoreRetries; i++ {
// 有的话,就绑定到当前节点
pa, err := c.blockReaderWriter.getPendingAffinity(ctx, host, *subnet)
if err != nil {
if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok {
logCtx.WithError(err).Debug("CAS error claiming pending affinity, retry")
continue
}
logCtx.WithError(err).Errorf("Error claiming pending affinity")
return ips, err
}

// 新绑定了Block,尝试在新Block里分配IP
b, err := c.getBlockFromAffinity(ctx, pa)
...
newIPs, err := c.assignFromExistingBlock(ctx, b, rem, handleID, attrs, host, config.StrictAffinity)
...
// 分配成功
ips = append(ips, newIPs...)
rem = num - len(ips)
break
}
}

if retries == 0 {
return ips, errors.New("Max retries hit - excessive concurrent IPAM requests")
}
}

// 如果IP分配还不够(也就是说已绑定的Block里的IP都分完了,并且也没有没有可绑定的新IPBlock了)并且IP还没分完,
// 并且StrictAffinity为false
rem := num - len(ips)
if config.StrictAffinity != true && rem != 0 {
logCtx.Infof("Attempting to assign %d more addresses from non-affine blocks", rem)

// Iterate over pools and assign addresses until we either run out of pools,
// or the request has been satisfied.
logCtx.Info("Looking for blocks with free IP addresses")
for _, p := range pools {
// 在所有的Pool里,随机选一个Block,在这个Block里找可用的IP地址
newBlock := randomBlockGenerator(p, host)
for rem > 0 {
// Grab a new random block.
blockCIDR := newBlock()
if blockCIDR == nil {
logCtx.Warningf("All addresses exhausted in pool %s", p.Spec.CIDR)
break
}

for i := 0; i < datastoreRetries; i++ {
b, err := c.blockReaderWriter.queryBlock(ctx, *blockCIDR, "")
...
newIPs, err := c.assignFromExistingBlock(ctx, b, rem, handleID, attrs, host, false)
...
// 分配成功
ips = append(ips, newIPs...)
rem = num - len(ips)
break
}
}
}
}
// 最后,如果执行到这里,意味着根据配置,无法再分配IP或者干脆全局都没有IP可用了
logCtx.Infof("Auto-assigned %d out of %d IPv%ds: %v", len(ips), num, version, ips)
return ips, nil
}

从上面的代码可以看到,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