Repository: karmada-io/karmada

Issue:
StaticWeightList/TakeByWeight may reduce replicas in some clusters when total replicas increase
### Description

When distributing replicas across clusters using [TakeByWeight](https://github.com/karmada-io/karmada/blob/b0b0bb482e6a963d10850b8b72723079c83588af/pkg/util/helper/binding.go#L112), the rounding and remainder handling logic can cause temporary inconsistencies in the total number of active replicas.

Specifically, after rounding down fractional allocations (`int32` cast), the remaining replicas are redistributed sequentially by incrementing each cluster’s replica count. However, this can lead to scenarios where one cluster’s replicas are reduced compared to the previous iteration, causing the overall system to run with fewer replicas than intended until all clusters catch up.

This is particularly problematic for us because we deploy **LLM servers across multiple regions**, and **a new server can take up to \~10 minutes to become ready**. During that time, the system may run noticeably below the requested replica count, affecting availability.

### Weights

The following examples use our production weights:

* `southcentralus = 170`
* `westus = 110`
* `eastus2 = 60`
  (sum = 340)

### Example

* **Total replicas = 5**

  * Ideal (weight/sum × total):

    * `southcentralus` = 5 × 170/340 = **2.5** → 2
    * `westus`        = 5 × 110/340 = **1.6176…** → 1
    * `eastus2`       = 5 × 60/340  = **0.8823…** → 0
  * After remainder pass (current logic: +1 in order): **3, 2, 0**

* **Total replicas = 6**

  * Ideal:

    * `southcentralus` = 6 × 170/340 = **3.0** → 3
    * `westus`        = 6 × 110/340 = **1.9411…** → 1
    * `eastus2`       = 6 × 60/340  = **1.0588…** → 1
  * After remainder pass: **4, 1, 1**
  * **Issue:** Compared to the 5-replica case, `westus` drops from **2 → 1**. Given \~10 minutes startup for new LLM servers, we effectively run at only **4 active replicas** for a noticeable period.

### Expected Behavior

* When the global total increases, **no cluster’s replica count should decrease**

Hints from issue comments:
@RainbowMango: > When the global total increases, no cluster’s replica count should decrease

I totally agree. During scale-up, the replica on the existing cluster should never be reduced. 

I will try to reproduce it on my side.

@akakakakakaa by the way, do you have an idea how to fix it?
@akakakakakaa: I am not very familiar with how this kind of issue is usually solved. To explore possible approaches, I asked GPT for some ideas. One of the suggestions was to switch from the current rounding logic to a divisor method (e.g., D’Hondt or Webster). After reviewing the idea, it seems like a reasonable solution.

---
## Divisor Method

A divisor method assigns replicas incrementally, always giving the “next replica” to the cluster with the highest priority score.

* **D’Hondt (Jefferson):** favors larger weights slightly.
* **Webster (Sainte-Laguë):** more neutral / proportional.
* Both guarantee that when total replicas increase, **no cluster’s allocation decreases**.
* Because this approach is implemented with a **priority queue**, replicas in each cluster grow **linearly with total replicas**, and allocations remain consistent:

  * if two clusters have the same weight, the one with fewer replicas will always be prioritized until they balance.
---
```go
package main

import (
	"container/heap"
	"fmt"
)

// ==== Data structures ====

// Cluster allocation target
type TargetCluster struct {
	Name     string
	Replicas int32
}

// Item used in the priority queue
type Item struct {
	name   string
	weight float64
	alloc  int
	method string
}

// Score calculation (priority)
func (it Item) score() float64 {
	switch it.method {
	case "webster": // Webster (Sainte-Laguë): divisors 1,3,5,...
		return it.weight / float64(2*it.alloc+1)
	default: // D'Hondt (Jefferson): divisors 1,2,3,...
		return it.weight / float64(it.alloc+1)
	}
}

// PriorityQueue implements heap.Interface
type PriorityQueue []Item

func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
	// max-heap by score
	si, sj := pq[i].score(), pq[j].score()
	if si != sj {
		return si > sj
	}
	// tie-break by weight, then name
	if pq[i].weight != pq[j].weight {
		return pq[i].weight > pq[j].weight
	}
	return pq[i].name < pq[j].name
}
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }

func (pq *PriorityQueue) Push(x interface{}) {
	*pq = append(*pq, x.(Item))
}
func (pq *PriorityQueue) Pop() interface{} {
	old := *pq
	n := len(old)
	item := old[n-1]
	*pq = old[0 : n-1]
	return item
}

// ==== Divisor allocation ====

// TakeByWeightDivisor allocates replicas using D’Hondt or Webster method.
func TakeByWeightDivisor(weights map[string]int, total int, method string) []TargetCluster {
	// Build priority queue
	h := &PriorityQueue{}
	for name, w := range weights {
		*h = append(*h, Item{name: name, weight: float64(w), alloc: 0, method: method})
	}
	heap.Init(h)

	// Assign replicas one by one
	for i := 0; i < total; i++ {
		it := heap.Pop(h).(Item)
		it.alloc++
		heap.Push(h, it)
	}

	// Collect result
	result := make([]TargetCluster, 0, len(*h))
	for h.Len() > 0 {
		it := heap.Pop(h).(Item)
		result = append(result, TargetCluster{
			Name:     it.name,
			Replicas: int32(it.alloc),
		})
	}
	return result
}
```
@RainbowMango: Interesting, I will put this in my queue and explore it further.

@akakakakakaa are you using Karmada now? Or in the phase of evaluation? I'm interested in the use case of running inferencing workloads with Karmada.@akakakakakaa: A couple of months ago we ran into GPU supply issues, which pushed us to move from a single-cluster setup to a multi-region one. Our main requirements were:

1. It should run on Kubernetes for operational ease.
2. Traffic should be easily distributed across regions.
3. Because GPU quotas differ by region, we needed to deploy with different weights per region.

After evaluating different options, we found Karmada to be the best fit and adopted it. We’ve been running it in production for about two months now without major issues. Combined with Istio multicluster, it works very well, even if pods are deployed unevenly across regions, traffic naturally shifts to match capacity.@RainbowMango: That's awesome! I'm glad to hear that. 

Since you are already running Karmada in production, we recommend joining [the Karmada Adopter Group](https://github.com/karmada-io/community/tree/main/adopter-group). This way, you'll receive important updates(like critical bugfixes) and stay connected with the community.

Joining is easy ---  just leave a comment on the https://github.com/karmada-io/karmada/issues/4540, then the community member will help you get onboard. @RainbowMango: I reproduced this on my side: 

**1: Create a Deployment with `5` replicas as follows:**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
```

**2: Create a PropagationPolicy with cluster weight `170:110:60` (member1:member2:member3):**
```yaml
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: nginx-propagation
spec:
  resourceSelectors:
    - apiVersion: apps/v1
      kind: Deployment
      name: nginx
  placement:
    clusterAffinity:
      clusterNames:
        - member1
        - member2
        - member3
    replicaScheduling:
      replicaDivisionPreference: Weighted
      replicaSchedulingType: Divided
      weightPreference:
        staticWeightList:
          - targetCluster:
              clusterNames:
                - member1
            weight: 170
          - targetCluster:
              clusterNames:
                - member2
            weight: 110
          - targetCluster:
              clusterNames:
                - member3
            weight: 60
    spreadConstraints:
      - spreadByField: cluster
        maxGroups: 3
        minGroups: 3
```

**3: Check schedule result: `member1: 3`, `member2:2`**
```
I0909 09:01:51.748566       1 generic_scheduler.go:108] Assigned Replicas: [{member1 3} {member2 2}]
```

**4: Scale deployment replicas from `5` to `6`**
```
-bash-5.0# kubectl scale deployment nginx --replicas 6
deployment.apps/nginx scaled
```

**5: Check the schedule result: `member1: 4`, `member2:1`, `member3: 1`**
```
I0909 09:07:06.500411       1 generic_scheduler.go:108] Assigned Replicas: [{member1 4} {member2 1} {member3 1}]
```

This test shows the replicas in `member2` reduced from `2` to `1` during the scale operation. This is the unexpected behavior. @RainbowMango: @akakakakakaa Now I can confirm that the root cause is exactly as you mentioned, the [TakeByWeight](https://github.com/karmada-io/karmada/blob/b0b0bb482e6a963d10850b8b72723079c83588af/pkg/util/helper/binding.go#L112) uses an unstable algorithm. 
I will continue to investigate how to fix it. (will look at the solution you provided above)@RainbowMango: @akakakakakaa The `D’Hondt` algorithm you provided above looks pretty good, but I need to explore it further, as [another scenario](https://github.com/karmada-io/karmada/issues/6424) requires each cluster to have at least one replica assigned. 

By the way, what do you think of [the adopter thing](https://github.com/karmada-io/karmada/issues/6735#issuecomment-3264943038) I mentioned above?@FYLSunghwan: Hi @RainbowMango,
I’m Sunghwan Kim, an ML Engineer working together with @akakakakakaa.

Our team is positive about joining the Adopter Group, and as you suggested, we’ve already created an [issue comment](https://github.com/karmada-io/karmada/issues/4540#issuecomment-3279110408) to apply.
We look forward to work with the Karmada community to address challenges in serving LLMs across multi-cluster environments :)@RainbowMango: Awesome! Thank you @FYLSunghwan and @akakakakakaa. We'd love to cooperate with every user. I believe this issue would be a good start.

I just created https://github.com/karmada-io/community/issues/160 to get you onboard.@RainbowMango: A quick update on this issue: I've been working on it over the past few days. I have almost finished the new algorithm, but given that this feature is widely used, I just sent #6757 to build the test base, so that we can clearly identify the effect of adopting the new algorithm.@RainbowMango: Another quick update:

I am trying to adopt [the Webster method](https://en.wikipedia.org/wiki/Sainte-Lagu%C3%AB_method) to refactor the [TakeByWeight](https://github.com/karmada-io/karmada/blob/b0b0bb482e6a963d10850b8b72723079c83588af/pkg/util/helper/binding.go#L112), it breaks quite a lot of unit tests. I've fixed most of them. But some of the tests were introduced by [another feature](https://github.com/karmada-io/karmada/issues/4220), which needs more time to evaluate the effect.

Here is the code snapshot: https://github.com/RainbowMango/karmada/commit/f87985d892b74737770b20107428a7f0cd68b5e2
And failing tests are:
```
-bash-5.0# go test ./pkg/scheduler/core/... -run=Test_EvenDistributionOfReplicas
I0918 17:19:53.939512 3249861 defaults.go:54] default Karmada Image Version: v0.0.0-master
--- FAIL: Test_EvenDistributionOfReplicas (0.00s)
    --- FAIL: Test_EvenDistributionOfReplicas/replica_7,_static_weighted_2:1:1:1,_change_replicas_from_7_to_8,_after_change (0.00s)
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 3} {member2 2} {member3 1} {member4 2}], wants 3:2:1:2 --> [4:2:1:1]
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 3} {member2 2} {member3 1} {member4 2}], wants 3:2:1:2 --> [4:1:2:1]
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 3} {member2 2} {member3 1} {member4 2}], wants 3:2:1:2 --> [4:1:1:2]
    --- FAIL: Test_EvenDistributionOfReplicas/replica_9,_static_weighted_2:1:1:1,_change_replicas_from_9_to_8,_before_change (0.00s)
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 3} {member2 2} {member3 2} {member4 2}], wants 3:2:2:2 --> [4:2:2:1 4:1:2:2 4:2:1:2]
    --- FAIL: Test_EvenDistributionOfReplicas/replica_9,_static_weighted_2:1:1:1,_change_replicas_from_9_to_8,_after_change (0.00s)
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 3} {member2 2} {member3 1} {member4 2}], wants 3:2:1:2 --> [4:1:1:2 4:1:2:1]
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 3} {member2 2} {member3 1} {member4 2}], wants 3:2:1:2 --> [4:1:1:2 4:2:1:1]
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 3} {member2 2} {member3 1} {member4 2}], wants 3:2:1:2 --> [4:2:1:1 4:1:2:1]
    --- FAIL: Test_EvenDistributionOfReplicas/replica_5,_static_weighted_1:1:1,_add_a_new_cluster_and_change_static_weight_to_1:1:1:1,_after_change (0.00s)
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 1} {member2 1} {member3 1} {member4 2}], wants 1:1:1:2 --> [1:2:1:1 2:1:1:1]
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 1} {member2 1} {member3 1} {member4 2}], wants 1:1:1:2 --> [1:1:2:1 2:1:1:1]
        generic_scheduler_test.go:527: AssignReplicas() got = [{member1 1} {member2 1} {member3 1} {member4 2}], wants 1:1:1:2 --> [1:1:2:1 1:2:1:1]
FAIL
FAIL	github.com/karmada-io/karmada/pkg/scheduler/core	0.023s
```
@RainbowMango: @akakakakakaa I've sent another PR #6772 to get the priority queue onboard. And after that, the [TakeByWeight](https://github.com/karmada-io/karmada/blob/b0b0bb482e6a963d10850b8b72723079c83588af/pkg/util/helper/binding.go#L112) will be refactored by follow-up PRs iteratively.  To ensure the changes won't break anything else, just fix the bug. All the tasks are tracked by #6739.  (By the way, if you want to join the iteration, just leave a comment and claim which task you'd like to take.)@RainbowMango: @akakakakakaa @FYLSunghwan 
This issue has been resolved by #6793. And the patch is included in [the Karmada v1.15.1 release](https://github.com/karmada-io/karmada/releases/tag/v1.15.1), which was available on Sep 30, 2025.  Please feel free to let us know if you need any help, like upgrading your Karmada deployment. 

Solution patch:
diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json
index 5598f0cc9..c786058ba 100644
--- a/api/openapi-spec/swagger.json
+++ b/api/openapi-spec/swagger.json
@@ -18894,7 +18894,8 @@
       "properties": {
         "name": {
           "description": "Name is the name of the cluster to be selected.",
-          "type": "string"
+          "type": "string",
+          "default": ""
         }
       }
     },
diff --git a/operator/pkg/apis/operator/v1alpha1/type.go b/operator/pkg/apis/operator/v1alpha1/type.go
index 52b4a1b44..f184a4281 100644
--- a/operator/pkg/apis/operator/v1alpha1/type.go
+++ b/operator/pkg/apis/operator/v1alpha1/type.go
@@ -59,7 +59,7 @@ const (
 type HTTPSource struct {
 	// URL specifies the URL of the CRD tarball resource.
 	// +required
-	URL string `json:"url,omitempty"`
+	URL string `json:"url"`
 
 	// Proxy specifies the configuration of a proxy server to use when downloading the CRD tarball.
 	// When set, the operator will use the configuration to determine how to establish a connection to the proxy to fetch the tarball from the URL specified above.
@@ -179,7 +179,7 @@ type ImageRegistry struct {
 type KarmadaComponents struct {
 	// Etcd holds configuration for etcd.
 	// +required
-	Etcd *Etcd `json:"etcd,omitempty"`
+	Etcd *Etcd `json:"etcd"`
 
 	// KarmadaAPIServer holds settings to kube-apiserver component. Currently, kube-apiserver
 	// is used as the apiserver of karmada. we had the same experience as k8s apiserver.
diff --git a/pkg/apis/networking/v1alpha1/service_types.go b/pkg/apis/networking/v1alpha1/service_types.go
index 15fc084fc..1dbdd6a98 100644
--- a/pkg/apis/networking/v1alpha1/service_types.go
+++ b/pkg/apis/networking/v1alpha1/service_types.go
@@ -112,7 +112,7 @@ type MultiClusterServiceSpec struct {
 type ClusterSelector struct {
 	// Name is the name of the cluster to be selected.
 	// +required
-	Name string `json:"name,omitempty"`
+	Name string `json:"name"`
 }
 
 // ExposureType describes how to expose the service.
diff --git a/pkg/apis/policy/v1alpha1/override_types.go b/pkg/apis/policy/v1alpha1/override_types.go
index 6c8b3d47b..84f909b5f 100644
--- a/pkg/apis/policy/v1alpha1/override_types.go
+++ b/pkg/apis/policy/v1alpha1/override_types.go
@@ -148,7 +148,7 @@ type LabelAnnotationOverrider struct {
 	// Items in Value which match in annotations/labels will be deleted when Operator is 'remove'.
 	// Items in Value which match in annotations/labels will be replaced when Operator is 'replace'.
 	// +required
-	Value map[string]string `json:"value,omitempty"`
+	Value map[string]string `json:"value"`
 }
 
 // ImageOverrider represents the rules dedicated to handling image overrides.
diff --git a/pkg/apis/search/searchregistry_types.go b/pkg/apis/search/searchregistry_types.go
index 4389cf18c..0137ca1f2 100644
--- a/pkg/apis/search/searchregistry_types.go
+++ b/pkg/apis/search/searchregistry_types.go
@@ -92,7 +92,7 @@ type OpenSearchConfig struct {
 	// - secret.data.userName
 	// - secret.data.password
 	// +required
-	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef,omitempty"`
+	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef"`
 
 	// More configurations such as transport, index should be added from here.
 }
diff --git a/pkg/apis/search/v1alpha1/searchregistry_types.go b/pkg/apis/search/v1alpha1/searchregistry_types.go
index edbc25fcf..88159dcc2 100644
--- a/pkg/apis/search/v1alpha1/searchregistry_types.go
+++ b/pkg/apis/search/v1alpha1/searchregistry_types.go
@@ -104,7 +104,7 @@ type OpenSearchConfig struct {
 	// - secret.data.userName
 	// - secret.data.password
 	// +required
-	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef,omitempty"`
+	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef"`
 
 	// More configurations such as transport, index should be added from here.
 }
diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go
index 5be909c56..1676c92dd 100644
--- a/pkg/generated/openapi/zz_generated.openapi.go
+++ b/pkg/generated/openapi/zz_generated.openapi.go
@@ -3083,6 +3083,7 @@ func schema_pkg_apis_networking_v1alpha1_ClusterSelector(ref common.ReferenceCal
 					"name": {
 						SchemaProps: spec.SchemaProps{
 							Description: "Name is the name of the cluster to be selected.",
+							Default:     "",
 							Type:        []string{"string"},
 							Format:      "",
 						},
diff --git a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go
index 36b085dca..8115f0744 100644
--- a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go
+++ b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go
@@ -161,7 +161,7 @@ func traverseToFindEmptyField(root gjson.Result, fieldPath []string) (sets.Set[s
 		// the first element is key=0, value={"ruleName":[]}
 		// however, we expected to record the `ruleName` field path as `rules.ruleName`, rather than `rules.0.ruleName`.
 		if rootIsNotArray {
-			curFieldPath = append(fieldPath, key.String())
+			curFieldPath = append(fieldPath, escapeJSONPathDotAndColon(key.String()))
 		}
 		curFieldStr := strings.Join(curFieldPath, ".")
 
@@ -181,6 +181,16 @@ func traverseToFindEmptyField(root gjson.Result, fieldPath []string) (sets.Set[s
 	return fieldOfEmptySlice, fieldOfEmptyStruct
 }
 
+// escapeJSONPathDotAndColon escape the dot and colon characters in json path key.
+// sjson package use '.' and '.:' to split the json path, so if a json key contains these characters,
+// we need to escape them by '\' first.
+// More details can refer to https://pkg.go.dev/github.com/tidwall/sjson#readme-path-syntax.
+func escapeJSONPathDotAndColon(field string) string {
+	field = strings.ReplaceAll(field, ".", `\.`)
+	field = strings.ReplaceAll(field, ":", `\:`)
+	return field
+}
+
 // traverseToFindEmptyFieldNeededModify find the field with empty values which needed to be modified by traverse a gjson.Result
 //
 // e.g: root={"spec":{"aa":[],"bb":[],"cc":["x"],"dd":{"ee":[],"ff":[]}}}
@@ -208,9 +218,9 @@ func traverseToFindEmptyFieldNeededModify(root gjson.Result, fieldPath, fieldPat
 		// the first element is key=0, value={"ruleName":[]}
 		// we record `rules.ruleName` into curFieldPath, and record `rules.0.ruleName` into curFieldPathWithArrayIndex.
 		if rootIsNotArray {
-			curFieldPath = append(fieldPath, key.String())
+			curFieldPath = append(fieldPath, escapeJSONPathDotAndColon(key.String()))
 		}
-		curFieldPathWithArrayIndex := append(fieldPathWithArrayIndex, key.String())
+		curFieldPathWithArrayIndex := append(fieldPathWithArrayIndex, escapeJSONPathDotAndColon(key.String()))
 
 		if value.IsArray() && len(value.Array()) == 0 {
 			curFieldPathStr := strings.Join(curFieldPath, ".")



Test patch:
diff --git a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go
index f6461bbc6..597566b6e 100644
--- a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go
+++ b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go
@@ -325,6 +325,25 @@ func Test_traverseToFindEmptyField(t *testing.T) {
 				fieldOfEmptyStruct: sets.New[string]("spec.aa", "spec.dd.ee"),
 			},
 		},
+		{
+			name: "traverse to find empty field with special characters in keys",
+			args: args{
+				root: gjson.Parse(`{
+					"spec": {
+						"replicas": 3,
+						":colons": {},
+						".dots": {},
+						":.colonAndDot": {},
+						".:dotAndColon": {}
+					}
+				}`),
+				fieldPath: nil,
+			},
+			wants: wants{
+				fieldOfEmptySlice:  sets.New[string](),
+				fieldOfEmptyStruct: sets.New[string](`spec.\:colons`, `spec.\.dots`, `spec.\:\.colonAndDot`, `spec.\.\:dotAndColon`),
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -374,6 +393,32 @@ func Test_traverseToFindEmptyFieldNeededModify(t *testing.T) {
 				fieldOfEmptySliceToDelete: sets.New[string]("spec.0.dd.ff"),
 			},
 		},
+		{
+			name: "traverse to find empty field needed modify with special characters in keys",
+			args: args{
+				root: gjson.Parse(`{
+					"spec": {
+						"replicas": 3,
+						":colons": [],
+						".dots": [],
+						":.colonAndDot": [],
+						".:dotAndColon": []
+					}
+				}`),
+				fieldPath:               nil,
+				fieldPathWithArrayIndex: nil,
+				fieldOfEmptySlice:       sets.New[string](`spec.\.dots`),
+				fieldOfEmptyStruct:      sets.New[string](`spec.\:colons`, `spec.\:\.colonAndDot`, `spec.\.\:dotAndColon`),
+			},
+			wants: wants{
+				fieldOfEmptySliceToStruct: sets.New[string](
+					`spec.\:colons`,
+					`spec.\:\.colonAndDot`,
+					`spec.\.\:dotAndColon`,
+				),
+				fieldOfEmptySliceToDelete: sets.New[string](),
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -435,6 +480,34 @@ func Test_convertEmptyObjectBackToEmptySlice(t *testing.T) {
 			wantErr: false,
 			want:    []byte(`[]`),
 		},
+		{
+			name: "convert empty object back to empty slice with special characters in keys",
+			args: args{
+				objBytes: []byte(`{"spec": {"replicas": 3, ":colons": [], ".dots": [], ":.colonAndDot": [], ".:dotAndColon": []}}`),
+				isStruct: true,
+				references: func() []*unstructured.Unstructured {
+					desired := &unstructured.Unstructured{}
+					observed := &unstructured.Unstructured{}
+					_ = json.Unmarshal([]byte(`{
+						"spec": {
+							"replicas": 3,
+							":colons": {},
+							":.colonAndDot": {},
+							".:dotAndColon": {}
+						}
+					}`), desired)
+					_ = json.Unmarshal([]byte(`{
+						"spec": {
+							"replicas": 3,
+							".dots": []
+						}
+					}`), observed)
+					return []*unstructured.Unstructured{desired, observed}
+				}(),
+			},
+			wantErr: false,
+			want:    []byte(`{"spec": {"replicas": 3, ":colons": {}, ".dots": [], ":.colonAndDot": {}, ".:dotAndColon": {}}}`),
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/scheduler/core/division_algorithm_test.go b/pkg/scheduler/core/division_algorithm_test.go
index 313534501..b1071c835 100644
--- a/pkg/scheduler/core/division_algorithm_test.go
+++ b/pkg/scheduler/core/division_algorithm_test.go
@@ -20,7 +20,6 @@ import (
 	"testing"
 
 	workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
-	utilhelper "github.com/karmada-io/karmada/pkg/util/helper"
 	"github.com/karmada-io/karmada/test/helper"
 )
 
@@ -31,90 +30,6 @@ const (
 	ClusterMember4 = "member4"
 )
 
-func Test_dispenser_takeByWeight(t *testing.T) {
-	tests := []struct {
-		name        string
-		numReplicas int32
-		result      []workv1alpha2.TargetCluster
-		weightList  utilhelper.ClusterWeightInfoList
-		desired     []workv1alpha2.TargetCluster
-		done        bool
-	}{
-		{
-			name:        "Scale up 6 replicas",
-			numReplicas: 6,
-			result: []workv1alpha2.TargetCluster{
-				{Name: "A", Replicas: 1},
-				{Name: "B", Replicas: 2},
-				{Name: "C", Replicas: 3},
-			},
-			weightList: []utilhelper.ClusterWeightInfo{
-				{ClusterName: "A", Weight: 1},
-				{ClusterName: "B", Weight: 2},
-				{ClusterName: "C", Weight: 3},
-			},
-			desired: []workv1alpha2.TargetCluster{
-				{Name: "A", Replicas: 2},
-				{Name: "B", Replicas: 4},
-				{Name: "C", Replicas: 6},
-			},
-			done: true,
-		},
-		{
-			name:        "Scale up 3 replicas",
-			numReplicas: 3,
-			result: []workv1alpha2.TargetCluster{
-				{Name: "A", Replicas: 1},
-				{Name: "B", Replicas: 2},
-				{Name: "C", Replicas: 3},
-			},
-			weightList: []utilhelper.ClusterWeightInfo{
-				{ClusterName: "A", Weight: 1},
-				{ClusterName: "B", Weight: 2},
-				{ClusterName: "C", Weight: 3},
-			},
-			desired: []workv1alpha2.TargetCluster{
-				{Name: "A", Replicas: 1},
-				{Name: "B", Replicas: 3},
-				{Name: "C", Replicas: 5},
-			},
-			done: true,
-		},
-		{
-			name:        "Scale up 2 replicas",
-			numReplicas: 2,
-			result: []workv1alpha2.TargetCluster{
-				{Name: "A", Replicas: 1},
-				{Name: "B", Replicas: 2},
-				{Name: "C", Replicas: 3},
-			},
-			weightList: []utilhelper.ClusterWeightInfo{
-				{ClusterName: "A", Weight: 1},
-				{ClusterName: "B", Weight: 2},
-				{ClusterName: "C", Weight: 3},
-			},
-			desired: []workv1alpha2.TargetCluster{
-				{Name: "A", Replicas: 1},
-				{Name: "B", Replicas: 2},
-				{Name: "C", Replicas: 5},
-			},
-			done: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			a := utilhelper.NewDispenser(tt.numReplicas, tt.result)
-			a.TakeByWeight(tt.weightList)
-			if a.Done() != tt.done {
-				t.Errorf("expected after takeByWeight: %v, but got: %v", tt.done, a.Done())
-			}
-			if !helper.IsScheduleResultEqual(a.Result, tt.desired) {
-				t.Errorf("expected result after takeByWeight: %v, but got: %v", tt.desired, a.Result)
-			}
-		})
-	}
-}
-
 func Test_dynamicDivideReplicas(t *testing.T) {
 	tests := []struct {
 		name    string
diff --git a/pkg/util/helper/binding_test.go b/pkg/util/helper/binding_test.go
index aa12a7b33..6363030d9 100644
--- a/pkg/util/helper/binding_test.go
+++ b/pkg/util/helper/binding_test.go
@@ -54,6 +54,155 @@ const (
 	ClusterMember3 = "member3"
 )
 
+func Test_dispenser_takeByWeight(t *testing.T) {
+	tests := []struct {
+		name              string
+		newReplicas       int32
+		initialAssignment []workv1alpha2.TargetCluster
+		weightList        ClusterWeightInfoList
+		desired           []workv1alpha2.TargetCluster
+	}{
+		{
+			name:        "Scale up 6 replicas",
+			newReplicas: 6,
+			initialAssignment: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 1},
+				{Name: "B", Replicas: 2},
+				{Name: "C", Replicas: 3},
+			},
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 1},
+				{ClusterName: "B", Weight: 2},
+				{ClusterName: "C", Weight: 3},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 2},
+				{Name: "B", Replicas: 4},
+				{Name: "C", Replicas: 6},
+			},
+		},
+		{
+			name:        "Scale up 3 replicas",
+			newReplicas: 3,
+			initialAssignment: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 1},
+				{Name: "B", Replicas: 2},
+				{Name: "C", Replicas: 3},
+			},
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 1},
+				{ClusterName: "B", Weight: 2},
+				{ClusterName: "C", Weight: 3},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 1},
+				{Name: "B", Replicas: 3},
+				{Name: "C", Replicas: 5},
+			},
+		},
+		{
+			name:        "Scale up 2 replicas",
+			newReplicas: 2,
+			initialAssignment: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 1},
+				{Name: "B", Replicas: 2},
+				{Name: "C", Replicas: 3},
+			},
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 1},
+				{ClusterName: "B", Weight: 2},
+				{ClusterName: "C", Weight: 3},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 1},
+				{Name: "B", Replicas: 2},
+				{Name: "C", Replicas: 5},
+			},
+		},
+		{
+			name:              "HA scenario with weight 2:1: assign 1 replica",
+			newReplicas:       1,
+			initialAssignment: nil,
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 2},
+				{ClusterName: "B", Weight: 1},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 1},
+				{Name: "B", Replicas: 0},
+			},
+		},
+		{
+			name:              "HA scenario with weight 2:1: assign 2 replicas",
+			newReplicas:       2,
+			initialAssignment: nil,
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 2},
+				{ClusterName: "B", Weight: 1},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 2},
+				{Name: "B", Replicas: 0},
+			},
+		},
+		{
+			name:              "HA scenario with weight 2:1: assign 3 replicas",
+			newReplicas:       3,
+			initialAssignment: nil,
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 2},
+				{ClusterName: "B", Weight: 1},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 2},
+				{Name: "B", Replicas: 1},
+			},
+		},
+		{
+			name:              "brand new assign 5 replicas",
+			newReplicas:       5,
+			initialAssignment: nil,
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 170},
+				{ClusterName: "B", Weight: 110},
+				{ClusterName: "C", Weight: 60},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 3},
+				{Name: "B", Replicas: 2},
+				{Name: "C", Replicas: 0},
+			},
+		},
+		{
+			name:              "brand new assign 6 replicas",
+			newReplicas:       6,
+			initialAssignment: nil,
+			weightList: []ClusterWeightInfo{
+				{ClusterName: "A", Weight: 170},
+				{ClusterName: "B", Weight: 110},
+				{ClusterName: "C", Weight: 60},
+			},
+			desired: []workv1alpha2.TargetCluster{
+				{Name: "A", Replicas: 4},
+				{Name: "B", Replicas: 1}, // TODO(@RainbowMango): comparing to the previous test where assign 5 replicas, this tests kind of scale up 1 replica, but the replica cluster B got reduced from 2 to 1, this is not expected and tracked by https://github.com/karmada-io/karmada/issues/6735.
+				{Name: "C", Replicas: 1},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			a := NewDispenser(tt.newReplicas, tt.initialAssignment)
+			a.TakeByWeight(tt.weightList)
+			if !a.Done() {
+				t.Errorf("dispensing unexpectedly not finished")
+			}
+			if !testhelper.IsScheduleResultEqual(a.Result, tt.desired) {
+				t.Errorf("expected result after takeByWeight: %v, but got: %v", tt.desired, a.Result)
+			}
+		})
+	}
+}
+
 func TestDispenseReplicasByTargetClusters(t *testing.T) {
 	type args struct {
 		clusters []workv1alpha2.TargetCluster