Merge pull request #2845 from Garrybest/pr_division

refactor static weight strategy
This commit is contained in:
karmada-bot 2022-12-01 10:41:15 +08:00 committed by GitHub
commit 464270f7c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 421 additions and 254 deletions

View File

@ -48,14 +48,28 @@ func assignByAggregatedStrategy(state *assignState) ([]workv1alpha2.TargetCluste
return divideReplicasByResource(state.candidates, state.object, policyv1alpha1.ReplicaDivisionPreferenceAggregated) return divideReplicasByResource(state.candidates, state.object, policyv1alpha1.ReplicaDivisionPreferenceAggregated)
} }
// assignByStaticWeightStrategy assigns replicas by StaticWeightStrategy. /*
* assignByStaticWeightStrategy assigns a total number of replicas to the selected clusters by the weight list.
* For example, we want to assign replicas to two clusters named A and B.
* | Total | Weight(A:B) | Assignment(A:B) |
* | 9 | 1:2 | 3:6 |
* | 9 | 1:3 | 2:7 | Approximate assignment
* Note:
* 1. If any selected cluster which not present on the weight list will be ignored(different with '0' replica).
* 2. In case of not enough replica for specific cluster which will get '0' replica.
*/
func assignByStaticWeightStrategy(state *assignState) ([]workv1alpha2.TargetCluster, error) { func assignByStaticWeightStrategy(state *assignState) ([]workv1alpha2.TargetCluster, error) {
// If ReplicaDivisionPreference is set to "Weighted" and WeightPreference is not set, // If ReplicaDivisionPreference is set to "Weighted" and WeightPreference is not set,
// scheduler will weight all clusters averagely. // scheduler will weight all clusters averagely.
if state.strategy.WeightPreference == nil { if state.strategy.WeightPreference == nil {
state.strategy.WeightPreference = getDefaultWeightPreference(state.candidates) state.strategy.WeightPreference = getDefaultWeightPreference(state.candidates)
} }
return divideReplicasByStaticWeight(state.candidates, state.strategy.WeightPreference.StaticWeightList, state.object.Replicas) weightList := getStaticWeightInfoList(state.candidates, state.strategy.WeightPreference.StaticWeightList)
acc := newDispenser(state.object.Replicas, nil)
acc.takeByWeight(weightList)
return acc.result, nil
} }
// assignByDynamicWeightStrategy assigns replicas by assignByDynamicWeightStrategy. // assignByDynamicWeightStrategy assigns replicas by assignByDynamicWeightStrategy.

View File

@ -0,0 +1,259 @@
package core
import (
"testing"
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/test/helper"
)
func Test_assignByStaticWeightStrategy(t *testing.T) {
tests := []struct {
name string
clusters []*clusterv1alpha1.Cluster
weightPreference *policyv1alpha1.ClusterPreferences
replicas int32
want []workv1alpha2.TargetCluster
wantErr bool
}{
{
name: "replica 12, weight 3:2:1",
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
helper.NewCluster(ClusterMember3),
},
weightPreference: &policyv1alpha1.ClusterPreferences{
StaticWeightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 3,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember2},
},
Weight: 2,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember3},
},
Weight: 1,
},
},
},
replicas: 12,
want: []workv1alpha2.TargetCluster{
{
Name: ClusterMember1,
Replicas: 6,
},
{
Name: ClusterMember2,
Replicas: 4,
},
{
Name: ClusterMember3,
Replicas: 2,
},
},
wantErr: false,
},
{
name: "replica 12, default weight",
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
helper.NewCluster(ClusterMember3),
},
replicas: 12,
want: []workv1alpha2.TargetCluster{
{
Name: ClusterMember1,
Replicas: 4,
},
{
Name: ClusterMember2,
Replicas: 4,
},
{
Name: ClusterMember3,
Replicas: 4,
},
},
wantErr: false,
},
{
name: "replica 14, weight 3:2:1",
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
helper.NewCluster(ClusterMember3),
},
weightPreference: &policyv1alpha1.ClusterPreferences{
StaticWeightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 3,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember2},
},
Weight: 2,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember3},
},
Weight: 1,
},
},
},
replicas: 14,
want: []workv1alpha2.TargetCluster{
{
Name: ClusterMember1,
Replicas: 8,
},
{
Name: ClusterMember2,
Replicas: 4,
},
{
Name: ClusterMember3,
Replicas: 2,
},
},
wantErr: false,
},
{
name: "insufficient replica assignment should get 0 replica",
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
},
weightPreference: &policyv1alpha1.ClusterPreferences{
StaticWeightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 1,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember2},
},
Weight: 1,
},
},
},
replicas: 0,
want: []workv1alpha2.TargetCluster{
{
Name: ClusterMember1,
Replicas: 0,
},
{
Name: ClusterMember2,
Replicas: 0,
},
},
wantErr: false,
},
{
name: "selected cluster without weight should be ignored",
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
},
weightPreference: &policyv1alpha1.ClusterPreferences{
StaticWeightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 1,
},
},
},
replicas: 2,
want: []workv1alpha2.TargetCluster{
{
Name: ClusterMember1,
Replicas: 2,
},
},
wantErr: false,
},
{
name: "cluster with multiple weights",
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
},
weightPreference: &policyv1alpha1.ClusterPreferences{
StaticWeightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 1,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember2},
},
Weight: 1,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 2,
},
},
},
replicas: 3,
want: []workv1alpha2.TargetCluster{
{
Name: ClusterMember1,
Replicas: 2,
},
{
Name: ClusterMember2,
Replicas: 1,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := assignByStaticWeightStrategy(&assignState{
candidates: tt.clusters,
strategy: &policyv1alpha1.ReplicaSchedulingStrategy{
ReplicaSchedulingType: policyv1alpha1.ReplicaSchedulingTypeDivided,
ReplicaDivisionPreference: policyv1alpha1.ReplicaDivisionPreferenceWeighted,
WeightPreference: tt.weightPreference,
},
object: &workv1alpha2.ResourceBindingSpec{Replicas: tt.replicas},
})
if (err != nil) != tt.wantErr {
t.Errorf("divideReplicasByStaticWeight() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !helper.IsScheduleResultEqual(got, tt.want) {
t.Errorf("divideReplicasByStaticWeight() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -20,6 +20,82 @@ func (a TargetClustersList) Len() int { return len(a) }
func (a TargetClustersList) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a TargetClustersList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a TargetClustersList) Less(i, j int) bool { return a[i].Replicas > a[j].Replicas } func (a TargetClustersList) Less(i, j int) bool { return a[i].Replicas > a[j].Replicas }
type dispenser struct {
numReplicas int32
result []workv1alpha2.TargetCluster
}
func newDispenser(numReplicas int32, init []workv1alpha2.TargetCluster) *dispenser {
cp := make([]workv1alpha2.TargetCluster, len(init))
copy(cp, init)
return &dispenser{numReplicas: numReplicas, result: cp}
}
func (a *dispenser) done() bool {
return a.numReplicas == 0 && len(a.result) != 0
}
func (a *dispenser) takeByWeight(w helper.ClusterWeightInfoList) {
if a.done() {
return
}
sum := w.GetWeightSum()
if sum == 0 {
return
}
sort.Sort(w)
result := make([]workv1alpha2.TargetCluster, 0, w.Len())
remain := a.numReplicas
for _, info := range w {
replicas := int32(info.Weight * int64(a.numReplicas) / sum)
result = append(result, workv1alpha2.TargetCluster{
Name: info.ClusterName,
Replicas: replicas,
})
remain -= replicas
}
// TODO(Garrybest): take rest replicas by fraction part
for i := range result {
if remain == 0 {
break
}
result[i].Replicas++
remain--
}
a.numReplicas = remain
a.result = util.MergeTargetClusters(a.result, result)
}
func getStaticWeightInfoList(clusters []*clusterv1alpha1.Cluster, weightList []policyv1alpha1.StaticClusterWeight) helper.ClusterWeightInfoList {
list := make(helper.ClusterWeightInfoList, 0)
for _, cluster := range clusters {
var weight int64
for _, staticWeightRule := range weightList {
if util.ClusterMatches(cluster, staticWeightRule.TargetCluster) {
weight = util.MaxInt64(weight, staticWeightRule.Weight)
}
}
if weight > 0 {
list = append(list, helper.ClusterWeightInfo{
ClusterName: cluster.Name,
Weight: weight,
})
}
}
if list.GetWeightSum() == 0 {
for _, cluster := range clusters {
list = append(list, helper.ClusterWeightInfo{
ClusterName: cluster.Name,
Weight: 1,
})
}
}
return list
}
// divideReplicasByDynamicWeight assigns a total number of replicas to the selected clusters by the dynamic weight list. // divideReplicasByDynamicWeight assigns a total number of replicas to the selected clusters by the dynamic weight list.
func divideReplicasByDynamicWeight(clusters []*clusterv1alpha1.Cluster, dynamicWeight policyv1alpha1.DynamicWeightFactor, spec *workv1alpha2.ResourceBindingSpec) ([]workv1alpha2.TargetCluster, error) { func divideReplicasByDynamicWeight(clusters []*clusterv1alpha1.Cluster, dynamicWeight policyv1alpha1.DynamicWeightFactor, spec *workv1alpha2.ResourceBindingSpec) ([]workv1alpha2.TargetCluster, error) {
switch dynamicWeight { switch dynamicWeight {
@ -63,61 +139,6 @@ func divideReplicasByResource(
} }
} }
// divideReplicasByStaticWeight assigns a total number of replicas to the selected clusters by the weight list.
// For example, we want to assign replicas to two clusters named A and B.
// | Total | Weight(A:B) | Assignment(A:B) |
// | 9 | 1:2 | 3:6 |
// | 9 | 1:3 | 2:7 | Approximate assignment
// Note:
// 1. If any selected cluster which not present on the weight list will be ignored(different with '0' replica).
// 2. In case of not enough replica for specific cluster which will get '0' replica.
func divideReplicasByStaticWeight(clusters []*clusterv1alpha1.Cluster, weightList []policyv1alpha1.StaticClusterWeight,
replicas int32) ([]workv1alpha2.TargetCluster, error) {
weightSum := int64(0)
matchClusters := make(map[string]int64)
desireReplicaInfos := make(map[string]int64)
for _, cluster := range clusters {
for _, staticWeightRule := range weightList {
if util.ClusterMatches(cluster, staticWeightRule.TargetCluster) {
weightSum += staticWeightRule.Weight
matchClusters[cluster.Name] = staticWeightRule.Weight
break
}
}
}
if weightSum == 0 {
for _, cluster := range clusters {
weightSum++
matchClusters[cluster.Name] = 1
}
}
allocatedReplicas := int32(0)
for clusterName, weight := range matchClusters {
desireReplicaInfos[clusterName] = weight * int64(replicas) / weightSum
allocatedReplicas += int32(desireReplicaInfos[clusterName])
}
clusterWeights := helper.SortClusterByWeight(matchClusters)
var clusterNames []string
for _, clusterWeightInfo := range clusterWeights {
clusterNames = append(clusterNames, clusterWeightInfo.ClusterName)
}
divideRemainingReplicas(int(replicas-allocatedReplicas), desireReplicaInfos, clusterNames)
targetClusters := make([]workv1alpha2.TargetCluster, len(desireReplicaInfos))
i := 0
for key, value := range desireReplicaInfos {
targetClusters[i] = workv1alpha2.TargetCluster{Name: key, Replicas: int32(value)}
i++
}
return targetClusters, nil
}
// divideReplicasByPreference assigns a total number of replicas to the selected clusters by preference according to the resource. // divideReplicasByPreference assigns a total number of replicas to the selected clusters by preference according to the resource.
func divideReplicasByPreference( func divideReplicasByPreference(
clusterAvailableReplicas []workv1alpha2.TargetCluster, clusterAvailableReplicas []workv1alpha2.TargetCluster,

View File

@ -11,6 +11,7 @@ import (
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util"
utilhelper "github.com/karmada-io/karmada/pkg/util/helper"
"github.com/karmada-io/karmada/test/helper" "github.com/karmada-io/karmada/test/helper"
) )
@ -21,222 +22,85 @@ const (
ClusterMember4 = "member4" ClusterMember4 = "member4"
) )
func Test_divideReplicasByStaticWeight(t *testing.T) { func Test_dispenser_takeByWeight(t *testing.T) {
type args struct {
clusters []*clusterv1alpha1.Cluster
weightList []policyv1alpha1.StaticClusterWeight
replicas int32
}
tests := []struct { tests := []struct {
name string name string
args args numReplicas int32
want []workv1alpha2.TargetCluster result []workv1alpha2.TargetCluster
wantErr bool weightList utilhelper.ClusterWeightInfoList
desired []workv1alpha2.TargetCluster
done bool
}{ }{
{ {
name: "replica 12, weight 3:2:1", name: "Scale up 6 replicas",
args: args{ numReplicas: 6,
clusters: []*clusterv1alpha1.Cluster{ result: []workv1alpha2.TargetCluster{
helper.NewCluster(ClusterMember1), {Name: "A", Replicas: 1},
helper.NewCluster(ClusterMember2), {Name: "B", Replicas: 2},
helper.NewCluster(ClusterMember3), {Name: "C", Replicas: 3},
},
weightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 3,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember2},
},
Weight: 2,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember3},
},
Weight: 1,
},
},
replicas: 12,
}, },
want: []workv1alpha2.TargetCluster{ weightList: []utilhelper.ClusterWeightInfo{
{ {ClusterName: "A", Weight: 1},
Name: ClusterMember1, {ClusterName: "B", Weight: 2},
Replicas: 6, {ClusterName: "C", Weight: 3},
},
{
Name: ClusterMember2,
Replicas: 4,
},
{
Name: ClusterMember3,
Replicas: 2,
},
}, },
wantErr: false, desired: []workv1alpha2.TargetCluster{
{Name: "A", Replicas: 2},
{Name: "B", Replicas: 4},
{Name: "C", Replicas: 6},
},
done: true,
}, },
{ {
name: "replica 12, default weight", name: "Scale up 3 replicas",
args: struct { numReplicas: 3,
clusters []*clusterv1alpha1.Cluster result: []workv1alpha2.TargetCluster{
weightList []policyv1alpha1.StaticClusterWeight {Name: "A", Replicas: 1},
replicas int32 {Name: "B", Replicas: 2},
}{ {Name: "C", Replicas: 3},
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
helper.NewCluster(ClusterMember3),
},
replicas: 12,
}, },
want: []workv1alpha2.TargetCluster{ weightList: []utilhelper.ClusterWeightInfo{
{ {ClusterName: "A", Weight: 1},
Name: ClusterMember1, {ClusterName: "B", Weight: 2},
Replicas: 4, {ClusterName: "C", Weight: 3},
},
{
Name: ClusterMember2,
Replicas: 4,
},
{
Name: ClusterMember3,
Replicas: 4,
},
}, },
wantErr: false, desired: []workv1alpha2.TargetCluster{
{Name: "A", Replicas: 1},
{Name: "B", Replicas: 3},
{Name: "C", Replicas: 5},
},
done: true,
}, },
{ {
name: "replica 14, weight 3:2:1", name: "Scale up 2 replicas",
args: struct { numReplicas: 2,
clusters []*clusterv1alpha1.Cluster result: []workv1alpha2.TargetCluster{
weightList []policyv1alpha1.StaticClusterWeight {Name: "A", Replicas: 1},
replicas int32 {Name: "B", Replicas: 2},
}{ {Name: "C", Replicas: 3},
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
helper.NewCluster(ClusterMember3),
},
weightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 3,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember2},
},
Weight: 2,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember3},
},
Weight: 1,
},
},
replicas: 14,
}, },
want: []workv1alpha2.TargetCluster{ weightList: []utilhelper.ClusterWeightInfo{
{ {ClusterName: "A", Weight: 1},
Name: ClusterMember1, {ClusterName: "B", Weight: 2},
Replicas: 8, {ClusterName: "C", Weight: 3},
},
{
Name: ClusterMember2,
Replicas: 4,
},
{
Name: ClusterMember3,
Replicas: 2,
},
}, },
wantErr: false, desired: []workv1alpha2.TargetCluster{
}, {Name: "A", Replicas: 1},
{ {Name: "B", Replicas: 2},
name: "insufficient replica assignment should get 0 replica", {Name: "C", Replicas: 5},
args: struct {
clusters []*clusterv1alpha1.Cluster
weightList []policyv1alpha1.StaticClusterWeight
replicas int32
}{
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
},
weightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 1,
},
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember2},
},
Weight: 1,
},
},
replicas: 0,
}, },
want: []workv1alpha2.TargetCluster{ done: true,
{
Name: ClusterMember1,
Replicas: 0,
},
{
Name: ClusterMember2,
Replicas: 0,
},
},
wantErr: false,
},
{
name: "selected cluster without weight should be ignored",
args: struct {
clusters []*clusterv1alpha1.Cluster
weightList []policyv1alpha1.StaticClusterWeight
replicas int32
}{
clusters: []*clusterv1alpha1.Cluster{
helper.NewCluster(ClusterMember1),
helper.NewCluster(ClusterMember2),
},
weightList: []policyv1alpha1.StaticClusterWeight{
{
TargetCluster: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{ClusterMember1},
},
Weight: 1,
},
},
replicas: 2,
},
want: []workv1alpha2.TargetCluster{
{
Name: ClusterMember1,
Replicas: 2,
},
},
wantErr: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := divideReplicasByStaticWeight(tt.args.clusters, tt.args.weightList, tt.args.replicas) a := newDispenser(tt.numReplicas, tt.result)
if (err != nil) != tt.wantErr { a.takeByWeight(tt.weightList)
t.Errorf("divideReplicasByStaticWeight() error = %v, wantErr %v", err, tt.wantErr) if a.done() != tt.done {
return t.Errorf("expected after takeByWeight: %v, but got: %v", tt.done, a.done())
} }
if !helper.IsScheduleResultEqual(got, tt.want) { if !helper.IsScheduleResultEqual(a.result, tt.desired) {
t.Errorf("divideReplicasByStaticWeight() got = %v, want %v", got, tt.want) t.Errorf("expected result after takeByWeight: %v, but got: %v", tt.desired, a.result)
} }
}) })
} }

View File

@ -59,6 +59,15 @@ func SortClusterByWeight(m map[string]int64) ClusterWeightInfoList {
return p return p
} }
// GetWeightSum returns the sum of the weight info.
func (p ClusterWeightInfoList) GetWeightSum() int64 {
var res int64
for i := range p {
res += p[i].Weight
}
return res
}
// IsBindingScheduled will check if resourceBinding/clusterResourceBinding is successfully scheduled. // IsBindingScheduled will check if resourceBinding/clusterResourceBinding is successfully scheduled.
func IsBindingScheduled(status *workv1alpha2.ResourceBindingStatus) bool { func IsBindingScheduled(status *workv1alpha2.ResourceBindingStatus) bool {
return meta.IsStatusConditionTrue(status.Conditions, workv1alpha2.Scheduled) return meta.IsStatusConditionTrue(status.Conditions, workv1alpha2.Scheduled)