Skip to content

Commit 84a4ed8

Browse files
authored
Merge pull request #604 from fengcone/feature/public-scaleLimiter
feat(pool): add scaleStrategy to control scaling behavior
2 parents 138063b + 58a6368 commit 84a4ed8

7 files changed

Lines changed: 327 additions & 5 deletions

File tree

‎kubernetes/README-ZH.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,14 @@ spec:
412412
kubectl apply -f pool-example.yaml
413413
```
414414

415+
**可选:配置扩容速率控制** - 添加 `scaleStrategy` 限制扩容节奏:
416+
```yaml
417+
scaleStrategy:
418+
maxUnavailable: "20%" # 或绝对数量如 5
419+
```
420+
421+
该配置控制扩容过程中允许不可用的 Pod 数量。例如,当 `poolMax=50` 且 `maxUnavailable=20%` 时,每次最多扩容 10 个 Pod。
422+
415423
使用资源池创建一批沙箱:
416424

417425
```yaml

‎kubernetes/README.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,14 @@ Apply the pool configuration:
411411
kubectl apply -f pool-example.yaml
412412
```
413413

414+
**Optional: Configure scale rate control** - Add `scaleStrategy` to limit the pace of scaling:
415+
```yaml
416+
scaleStrategy:
417+
maxUnavailable: "20%" # or absolute number like 5
418+
```
419+
420+
This controls how many pods can be unavailable during scaling. For example, with `poolMax=50` and `maxUnavailable=20%`, at most 10 pods will be scaled at once.
421+
414422
Create a batch of sandboxes using the pool:
415423

416424
```yaml
@@ -428,6 +436,53 @@ Apply the batch sandbox configuration:
428436
kubectl apply -f pooled-batch-sandbox.yaml
429437
```
430438

439+
##### Pooled Sandbox with Scale Rate Control
440+
441+
The Pool supports configurable scale rate control through `scaleStrategy`, which limits the pace of scaling operations to prevent resource contention:
442+
443+
```yaml
444+
apiVersion: sandbox.opensandbox.io/v1alpha1
445+
kind: Pool
446+
metadata:
447+
name: scale-controlled-pool
448+
spec:
449+
template:
450+
spec:
451+
containers:
452+
- name: sandbox-container
453+
image: nginx:latest
454+
ports:
455+
- containerPort: 80
456+
capacitySpec:
457+
bufferMax: 20
458+
bufferMin: 5
459+
poolMax: 50
460+
poolMin: 10
461+
scaleStrategy:
462+
# MaxUnavailable controls the maximum number of pods that can be unavailable during scaling.
463+
# Can be an absolute number (ex: 5) or a percentage of desired pods (ex: "10%").
464+
# Defaults to 25% if not specified.
465+
maxUnavailable: "20%"
466+
```
467+
468+
**ScaleStrategy parameters:**
469+
470+
- **maxUnavailable**: Specifies the maximum number of pods that can be unavailable during scaling operations. This can be:
471+
- An absolute number (e.g., `5` means at most 5 pods can be unavailable at once)
472+
- A percentage string (e.g., `"10%"` means at most 10% of desired pods can be unavailable)
473+
- Defaults to `25%` if not specified
474+
475+
**Use cases:**
476+
477+
- **Prevent resource contention**: Limit scaling pace to avoid overwhelming the cluster with simultaneous pod creation/deletion
478+
- **Gradual scaling**: Ensure smooth scaling transitions by capping the rate of change
479+
- **Production stability**: Protect production workloads from aggressive scaling that might impact service quality
480+
481+
Apply the pool configuration:
482+
```sh
483+
kubectl apply -f pool-with-scale-strategy.yaml
484+
```
485+
431486
##### Pooled Sandbox With Heterogeneous Tasks
432487
Create a batch of sandboxes with process-based heterogeneous tasks. For task execution to work properly, the task-executor must be deployed as a sidecar container in the pool template and share the process namespace with the sandbox container:
433488

‎kubernetes/apis/sandbox/v1alpha1/pool_types.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package v1alpha1
1717
import (
1818
corev1 "k8s.io/api/core/v1"
1919
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"k8s.io/apimachinery/pkg/util/intstr"
2021
)
2122

2223
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
@@ -32,6 +33,9 @@ type PoolSpec struct {
3233
// CapacitySpec controls the size of the resource pool.
3334
// +kubebuilder:validation:Required
3435
CapacitySpec CapacitySpec `json:"capacitySpec"`
36+
// ScaleStrategy controls the scaling behavior.
37+
// +optional
38+
ScaleStrategy *ScaleStrategy `json:"scaleStrategy,omitempty"`
3539
}
3640

3741
type CapacitySpec struct {
@@ -53,6 +57,15 @@ type CapacitySpec struct {
5357
PoolMin int32 `json:"poolMin"`
5458
}
5559

60+
// ScaleStrategy controls the pace of scaling operations.
61+
type ScaleStrategy struct {
62+
// MaxUnavailable is the maximum number of pods that can be unavailable during scaling.
63+
// Can be an absolute number (ex: 5) or a percentage of desired pods (ex: "20%").
64+
// Defaults to 25%.
65+
// +optional
66+
MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"`
67+
}
68+
5669
// PoolStatus defines the observed state of Pool.
5770
type PoolStatus struct {
5871
// ObservedGeneration is the most recent generation observed for this BatchSandbox. It corresponds to the

‎kubernetes/apis/sandbox/v1alpha1/zz_generated.deepcopy.go‎

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎kubernetes/config/crd/bases/sandbox.opensandbox.io_pools.yaml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ spec:
8484
- poolMax
8585
- poolMin
8686
type: object
87+
scaleStrategy:
88+
description: ScaleStrategy controls the scaling behavior.
89+
properties:
90+
maxUnavailable:
91+
anyOf:
92+
- type: integer
93+
- type: string
94+
description: |-
95+
MaxUnavailable is the maximum number of pods that can be unavailable during scaling.
96+
Can be an absolute number (ex: 5) or a percentage of desired pods (ex: "20%").
97+
Defaults to 25%.
98+
x-kubernetes-int-or-string: true
99+
type: object
87100
template:
88101
description: Pod Template used to create pre-warmed nodes in the pool.
89102
x-kubernetes-preserve-unknown-fields: true

‎kubernetes/internal/controller/pool_controller.go‎

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"encoding/hex"
2121
gerrors "errors"
2222
"fmt"
23+
"math"
2324
"sort"
2425
"time"
2526

@@ -30,6 +31,7 @@ import (
3031
"k8s.io/apimachinery/pkg/fields"
3132
"k8s.io/apimachinery/pkg/runtime"
3233
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/intstr"
3335
"k8s.io/apimachinery/pkg/util/json"
3436
"k8s.io/client-go/tools/record"
3537
"k8s.io/client-go/util/retry"
@@ -447,11 +449,19 @@ func (r *PoolReconciler) scalePool(ctx context.Context, args *scaleArgs) error {
447449
if createCnt > maxNewPods {
448450
createCnt = maxNewPods
449451
}
450-
log.Info("Scaling up pool", "pool", pool.Name, "createCnt", createCnt)
451-
for range createCnt {
452-
if err := r.createPoolPod(ctx, pool, args.latestRevision); err != nil {
453-
log.Error(err, "Failed to create pool pod")
454-
errs = append(errs, err)
452+
maxUnavailable := r.getMaxUnavailable(pool, desiredSchedulableCnt)
453+
notReadyCnt := r.countNotReadyPods(pods)
454+
limitedCreatCnt := maxUnavailable - notReadyCnt
455+
createCnt = int32(math.Max(0, math.Min(float64(createCnt), float64(limitedCreatCnt))))
456+
if createCnt > 0 {
457+
log.Info("Scaling up pool with constraint", "pool", pool.Name,
458+
"createCnt", createCnt, "maxUnavailable", maxUnavailable,
459+
"notReadyCnt", notReadyCnt, "desiredSchedulableCnt", desiredSchedulableCnt, "limitedCreatCnt", limitedCreatCnt)
460+
for range createCnt {
461+
if err := r.createPoolPod(ctx, pool, args.latestRevision); err != nil {
462+
log.Error(err, "Failed to create pool pod")
463+
errs = append(errs, err)
464+
}
455465
}
456466
}
457467
}
@@ -541,6 +551,37 @@ func (r *PoolReconciler) pickPodsToDelete(pods []*corev1.Pod, idlePodNames []str
541551
return podsToDelete
542552
}
543553

554+
// getMaxUnavailable returns the resolved maxUnavailable value.
555+
// If not specified, defaults to 25% of desiredTotal.
556+
// Minimum return value is 1 to ensure scaling progress.
557+
func (r *PoolReconciler) getMaxUnavailable(pool *sandboxv1alpha1.Pool, desiredTotal int32) int32 {
558+
defaultPercentage := intstr.FromString("25%")
559+
560+
maxUnavailable := &defaultPercentage
561+
if pool.Spec.ScaleStrategy != nil && pool.Spec.ScaleStrategy.MaxUnavailable != nil {
562+
maxUnavailable = pool.Spec.ScaleStrategy.MaxUnavailable
563+
}
564+
565+
result, err := intstr.GetScaledValueFromIntOrPercent(maxUnavailable, int(desiredTotal), true)
566+
if err != nil || result < 1 {
567+
result = 1
568+
}
569+
return int32(result)
570+
}
571+
572+
// countNotReadyPods returns the count of pods that are not ready.
573+
// A pod is considered not ready if it doesn't have a Ready condition
574+
// with status True.
575+
func (r *PoolReconciler) countNotReadyPods(pods []*corev1.Pod) int32 {
576+
var count int32
577+
for _, pod := range pods {
578+
if !isPodReady(pod) {
579+
count++
580+
}
581+
}
582+
return count
583+
}
584+
544585
func (r *PoolReconciler) createPoolPod(ctx context.Context, pool *sandboxv1alpha1.Pool, latestRevision string) error {
545586
log := logf.FromContext(ctx)
546587
pod, err := utils.GetPodFromTemplate(pool.Spec.Template, pool, metav1.NewControllerRef(pool, sandboxv1alpha1.SchemeBuilder.GroupVersion.WithKind("Pool")))

0 commit comments

Comments
 (0)