Skip to content

Commit 238d419

Browse files
authored
Merge pull request #944 from l1b0k/feat/ipam
feat: add warm up
2 parents 095bc73 + df1053d commit 238d419

File tree

16 files changed

+1428
-101
lines changed

16 files changed

+1428
-101
lines changed

docs/warm-up.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# IP Warm-Up Configuration
2+
3+
The IP warm-up feature allows Terway to pre-allocate a specified number of IP addresses when a node starts up. This helps reduce pod startup latency by having IPs ready in the pool before pods are scheduled.
4+
5+
## Configuration
6+
7+
The warm-up size can be configured through the `eni-config` ConfigMap:
8+
9+
| Parameter | Description | Default Value | Example |
10+
|-----------|-------------|---------------|---------|
11+
| `ip_warm_up_size` | Number of IPs to pre-allocate during node warm-up | `0` (disabled) | `10`, `20` |
12+
13+
## How It Works
14+
15+
1. **Initialization**: When a node starts and `ip_warm_up_size` is configured (> 0), the controller initializes the warm-up state:
16+
- `WarmUpTarget`: Set to the configured `ip_warm_up_size`
17+
- `WarmUpAllocatedCount`: Tracks the number of IPs allocated via OpenAPI during warm-up
18+
- `WarmUpCompleted`: Set to `false` initially
19+
20+
2. **Allocation**: During reconciliation, if warm-up is not completed, the controller calculates additional IP demand to reach the warm-up target and allocates IPs accordingly.
21+
22+
3. **Completion**: Warm-up is marked as completed when `WarmUpAllocatedCount >= WarmUpTarget`. Once completed, the warm-up process will not run again for that node.
23+
24+
## Relationship with Pool Size
25+
26+
**Important**: The `ip_warm_up_size` is independent of `min_pool_size` and `max_pool_size`. It can be set to a value larger or smaller than the pool size limits.
27+
28+
However, setting `ip_warm_up_size` larger than `max_pool_size` is **not recommended** because:
29+
30+
- The warm-up process will allocate IPs up to `ip_warm_up_size`
31+
- After warm-up completes, the pool management and idle IP reclaim policy will release excess IPs to maintain the pool within `min_pool_size` and `max_pool_size` boundaries
32+
- This results in unnecessary IP allocation and deallocation, wasting resources and API calls
33+
34+
### Recommended Configuration
35+
36+
```json
37+
{
38+
"version": "1",
39+
"max_pool_size": 20,
40+
"min_pool_size": 5,
41+
"ip_warm_up_size": 15
42+
}
43+
```
44+
45+
In this example:
46+
47+
- On node startup, 15 IPs will be pre-allocated (warm-up)
48+
- The pool will maintain between 5-20 IPs during normal operation
49+
- Since `ip_warm_up_size` (15) is within the pool size range (5-20), all pre-allocated IPs will be retained
50+
51+
### Not Recommended Configuration
52+
53+
```json
54+
{
55+
"version": "1",
56+
"max_pool_size": 10,
57+
"min_pool_size": 2,
58+
"ip_warm_up_size": 20
59+
}
60+
```
61+
62+
In this example:
63+
64+
- On node startup, 20 IPs will be pre-allocated (warm-up)
65+
- After warm-up completes, 10 IPs will be released because `max_pool_size` is only 10
66+
- This causes unnecessary IP churn and API calls
67+
68+
## Status Fields
69+
70+
The warm-up progress can be monitored through the Node CR status:
71+
72+
| Field | Description |
73+
|-------|-------------|
74+
| `warmUpTarget` | The target number of IPs to allocate during warm-up |
75+
| `warmUpAllocatedCount` | Current count of IPs allocated via OpenAPI during warm-up |
76+
| `warmUpCompleted` | Whether warm-up has been completed |
77+
78+
Example status:
79+
80+
```yaml
81+
status:
82+
warmUpTarget: 10
83+
warmUpAllocatedCount: 10
84+
warmUpCompleted: true
85+
```
86+
87+
## Use Cases
88+
89+
1. **Batch Job Scheduling**: When scheduling many pods simultaneously on a new node, pre-allocated IPs reduce waiting time.
90+
91+
2. **Auto-scaling**: New nodes in auto-scaling groups can have IPs ready before workloads are scheduled.
92+
93+
3. **Low-latency Requirements**: Applications requiring fast pod startup benefit from having IPs pre-allocated.
94+
95+
## Notes
96+
97+
- Warm-up only runs once per node lifecycle (when the node first joins the cluster)
98+
- If a node already has warm-up status initialized, changing `ip_warm_up_size` will not affect the ongoing warm-up
99+
- Warm-up progress is tracked independently of actual IP usage, ensuring consistent behavior even if IPs are allocated/deallocated during warm-up

pkg/apis/crds/network.alibabacloud.com_nodes.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ spec:
181181
jitterFactor:
182182
type: string
183183
type: object
184+
warmUpSize:
185+
minimum: 0
186+
type: integer
184187
type: object
185188
type: object
186189
status:
@@ -308,6 +311,12 @@ spec:
308311
nextSyncOpenAPITime:
309312
format: date-time
310313
type: string
314+
warmUpAllocatedCount:
315+
type: integer
316+
warmUpCompleted:
317+
type: boolean
318+
warmUpTarget:
319+
type: integer
311320
type: object
312321
type: object
313322
served: true

pkg/apis/crds/register.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func getCRD(name string) apiextensionsv1.CustomResourceDefinition {
5858
version = "v0.1.0"
5959
case CRDNode:
6060
crdBytes = crdsNode
61-
version = "v0.6.1"
61+
version = "v0.7.0"
6262
case CRDNodeRuntime:
6363
crdBytes = crdsNodeRuntime
6464
version = "v0.1.0"

pkg/apis/network.alibabacloud.com/v1beta1/node_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ type PoolSpec struct {
104104
PoolSyncPeriod string `json:"poolSyncPeriod,omitempty"`
105105

106106
Reclaim *IPReclaimPolicy `json:"reclaim,omitempty"`
107+
108+
// +kubebuilder:validation:Minimum=0
109+
WarmUpSize int `json:"warmUpSize,omitempty"`
107110
}
108111

109112
type IPReclaimPolicy struct {
@@ -199,6 +202,10 @@ type NodeStatus struct {
199202
LastModifiedTime metav1.Time `json:"lastModifiedTime,omitempty"`
200203
NextIdleIPReclaimTime metav1.Time `json:"nextIdleIPReclaimTime,omitempty"`
201204
NetworkInterfaces map[string]*Nic `json:"networkInterfaces,omitempty"`
205+
206+
WarmUpTarget int `json:"warmUpTarget,omitempty"`
207+
WarmUpAllocatedCount int `json:"warmUpAllocatedCount,omitempty"`
208+
WarmUpCompleted bool `json:"warmUpCompleted,omitempty"`
202209
}
203210

204211
// +genclient

pkg/controller/multi-ip/node/pool.go

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ func (n *ReconcileNode) Reconcile(ctx context.Context, request reconcile.Request
324324

325325
var errorList []error
326326

327+
// initialize warm-up if needed (for new nodes or existing nodes without warm-up status)
328+
n.initializeWarmUp(node)
329+
327330
// do not block ipam
328331
err = n.syncWithAPI(ctx, node)
329332
if err != nil {
@@ -590,10 +593,13 @@ func (n *ReconcileNode) syncPods(ctx context.Context, podsMapper map[string]*Pod
590593
errList = append(errList, err)
591594
}
592595

596+
// 6. check and mark warm-up completion
597+
n.checkWarmUpCompletion(node)
598+
593599
if utilerrors.NewAggregate(errList) != nil {
594600
return utilerrors.NewAggregate(errList)
595601
}
596-
// 6. pool management sort eni and find the victim
602+
// 7. pool management sort eni and find the victim
597603

598604
return n.adjustPool(ctx, node)
599605
}
@@ -826,8 +832,17 @@ func (n *ReconcileNode) addIP(ctx context.Context, unSucceedPods map[string]*Pod
826832
// before create eni , we need to check the quota
827833
options := getEniOptions(node)
828834

835+
// Calculate total demand including warm-up
836+
totalDemand := len(normalPods) + node.Spec.Pool.MinPoolSize
837+
if n.shouldPerformWarmUp(node) {
838+
warmUpDemand := n.calculateWarmUpDemand(node)
839+
totalDemand = max(totalDemand, warmUpDemand)
840+
841+
logr.FromContextOrDiscard(ctx).Info("warm up", "warmUpDemand", warmUpDemand, "totalDemand", totalDemand)
842+
}
843+
829844
// handle trunk/secondary eni
830-
assignEniWithOptions(ctx, node, len(normalPods)+node.Spec.Pool.MinPoolSize, options, func(option *eniOptions) bool {
845+
assignEniWithOptions(ctx, node, totalDemand, options, func(option *eniOptions) bool {
831846
return n.validateENI(ctx, option, []eniTypeKey{secondaryKey, trunkKey})
832847
})
833848
assignEniWithOptions(ctx, node, len(rdmaPods), options, func(option *eniOptions) bool {
@@ -1439,6 +1454,11 @@ func (n *ReconcileNode) createENI(ctx context.Context, node *networkv1beta1.Node
14391454
MetaCtx(ctx).Mutex.Lock()
14401455
node.Status.NetworkInterfaces[eni.NetworkInterfaceID] = networkInterface
14411456
// if changed , but we update failed , that case ,need to sync openAPI...
1457+
1458+
// Track OpenAPI allocations for warm-up
1459+
if !node.Status.WarmUpCompleted && node.Status.WarmUpTarget > 0 {
1460+
node.Status.WarmUpAllocatedCount += max(len(networkInterface.IPv4), len(networkInterface.IPv6))
1461+
}
14421462
MetaCtx(ctx).Mutex.Unlock()
14431463

14441464
MetaCtx(ctx).StatusChanged.Store(true)
@@ -1491,6 +1511,7 @@ func (n *ReconcileNode) assignIP(ctx context.Context, node *networkv1beta1.Node,
14911511
})
14921512
}
14931513
})
1514+
14941515
MetaCtx(ctx).Mutex.Unlock()
14951516

14961517
return err
@@ -1505,6 +1526,12 @@ func (n *ReconcileNode) assignIP(ctx context.Context, node *networkv1beta1.Node,
15051526
Status: networkv1beta1.IPStatusValid,
15061527
})
15071528
}
1529+
1530+
// Track OpenAPI allocations for warm-up
1531+
if !node.Status.WarmUpCompleted && node.Status.WarmUpTarget > 0 {
1532+
node.Status.WarmUpAllocatedCount += len(result)
1533+
}
1534+
15081535
MetaCtx(ctx).Mutex.Unlock()
15091536
}
15101537
}
@@ -1538,6 +1565,7 @@ func (n *ReconcileNode) assignIP(ctx context.Context, node *networkv1beta1.Node,
15381565
})
15391566
}
15401567
})
1568+
15411569
MetaCtx(ctx).Mutex.Unlock()
15421570

15431571
return err
@@ -1553,6 +1581,11 @@ func (n *ReconcileNode) assignIP(ctx context.Context, node *networkv1beta1.Node,
15531581
Status: networkv1beta1.IPStatusValid,
15541582
})
15551583
}
1584+
1585+
// Track OpenAPI allocations for warm-up
1586+
if !node.Spec.ENISpec.EnableIPv4 && !node.Status.WarmUpCompleted && node.Status.WarmUpTarget > 0 {
1587+
node.Status.WarmUpAllocatedCount += len(result)
1588+
}
15561589
MetaCtx(ctx).Mutex.Unlock()
15571590
}
15581591
}
@@ -1906,3 +1939,86 @@ func (n *ReconcileNode) waitIPGone(ctx context.Context, eni *networkv1beta1.Nic,
19061939
return !unfinished, nil
19071940
})
19081941
}
1942+
1943+
// initializeWarmUp initializes warm-up status for nodes
1944+
// For new nodes with warm-up configured: set up tracking
1945+
// For existing nodes without warm-up config or already initialized: mark as completed
1946+
func (n *ReconcileNode) initializeWarmUp(node *networkv1beta1.Node) {
1947+
if node.Spec.Pool == nil {
1948+
return
1949+
}
1950+
1951+
warmUpSize := node.Spec.Pool.WarmUpSize
1952+
1953+
// For existing nodes that already have warm-up status initialized, do nothing
1954+
if node.Status.WarmUpTarget > 0 || node.Status.WarmUpCompleted {
1955+
return
1956+
}
1957+
1958+
// If no warm-up configured, mark as completed immediately
1959+
if warmUpSize <= 0 {
1960+
node.Status.WarmUpCompleted = true
1961+
return
1962+
}
1963+
1964+
// New node with warm-up configured
1965+
node.Status.WarmUpTarget = warmUpSize
1966+
node.Status.WarmUpAllocatedCount = 0
1967+
node.Status.WarmUpCompleted = false
1968+
}
1969+
1970+
// shouldPerformWarmUp checks if warm-up should be performed
1971+
func (n *ReconcileNode) shouldPerformWarmUp(node *networkv1beta1.Node) bool {
1972+
if node.Status.WarmUpCompleted {
1973+
return false
1974+
}
1975+
if node.Status.WarmUpTarget <= 0 {
1976+
return false
1977+
}
1978+
return true
1979+
}
1980+
1981+
// calculateWarmUpDemand calculates the total IP demand for warm-up
1982+
// Warmup progress is tracked via WarmUpAllocatedCount, not by counting current IPs
1983+
// Returns total demand (currentIPs + remaining) for assignEniWithOptions
1984+
func (n *ReconcileNode) calculateWarmUpDemand(node *networkv1beta1.Node) int {
1985+
if !n.shouldPerformWarmUp(node) {
1986+
return 0
1987+
}
1988+
1989+
// Calculate remaining IPs to allocate based on WarmUpAllocatedCount
1990+
remaining := node.Status.WarmUpTarget - node.Status.WarmUpAllocatedCount
1991+
if remaining <= 0 {
1992+
return 0
1993+
}
1994+
1995+
// Count current IPs to calculate total demand for assignEniWithOptions
1996+
// (assignEniWithOptions expects total and subtracts existing IPs internally)
1997+
currentTotal := 0
1998+
for _, eni := range node.Status.NetworkInterfaces {
1999+
if eni.Status != aliyunClient.ENIStatusInUse {
2000+
continue
2001+
}
2002+
if node.Spec.ENISpec.EnableIPv4 {
2003+
currentTotal += len(getAllocatable(eni.IPv4))
2004+
} else if node.Spec.ENISpec.EnableIPv6 {
2005+
currentTotal += len(getAllocatable(eni.IPv6))
2006+
}
2007+
}
2008+
2009+
return currentTotal + remaining
2010+
}
2011+
2012+
// checkWarmUpCompletion checks if warm-up has been completed and marks it
2013+
func (n *ReconcileNode) checkWarmUpCompletion(node *networkv1beta1.Node) {
2014+
if node.Status.WarmUpCompleted {
2015+
return
2016+
}
2017+
if node.Status.WarmUpTarget <= 0 {
2018+
return
2019+
}
2020+
2021+
if node.Status.WarmUpAllocatedCount >= node.Status.WarmUpTarget {
2022+
node.Status.WarmUpCompleted = true
2023+
}
2024+
}

0 commit comments

Comments
 (0)