diff --git a/apis/v1alpha1/gameserverset_types.go b/apis/v1alpha1/gameserverset_types.go index 639d3c16..3122d3e7 100644 --- a/apis/v1alpha1/gameserverset_types.go +++ b/apis/v1alpha1/gameserverset_types.go @@ -40,6 +40,20 @@ const ( InplaceUpdateNotReadyBlocker = "game.kruise.io/inplace-update-not-ready-blocker" ) +type TopologyDeletionPriorityConfig struct { + // +optional + // +kubebuilder:default=100 + BasePriority int `json:"basePriority,omitempty"` + + // +optional + // +kubebuilder:default=10 + PodCountWeight int `json:"podCountWeight,omitempty"` + + // +optional + // +kubebuilder:default=5 + OwnerCountWeight int `json:"ownerCountWeight,omitempty"` +} + // GameServerSetSpec defines the desired state of GameServerSet type GameServerSetSpec struct { // replicas is the desired number of replicas of the given Template. @@ -54,6 +68,9 @@ type GameServerSetSpec struct { ServiceName string `json:"serviceName,omitempty"` ReserveGameServerIds []intstr.IntOrString `json:"reserveGameServerIds,omitempty"` ServiceQualities []ServiceQuality `json:"serviceQualities,omitempty"` + // +optional + TopologyDeletionPriority *TopologyDeletionPriorityConfig `json:"topologyDeletionPriority,omitempty"` + UpdateStrategy UpdateStrategy `json:"updateStrategy,omitempty"` ScaleStrategy ScaleStrategy `json:"scaleStrategy,omitempty"` Network *Network `json:"network,omitempty"` diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 54c06c39..00bae054 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -200,6 +200,11 @@ func (in *GameServerSetSpec) DeepCopyInto(out *GameServerSetSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.TopologyDeletionPriority != nil { + in, out := &in.TopologyDeletionPriority, &out.TopologyDeletionPriority + *out = new(TopologyDeletionPriorityConfig) + **out = **in + } in.UpdateStrategy.DeepCopyInto(&out.UpdateStrategy) in.ScaleStrategy.DeepCopyInto(&out.ScaleStrategy) if in.Network != nil { @@ -626,6 +631,21 @@ func (in *ServiceQualityCondition) DeepCopy() *ServiceQualityCondition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TopologyDeletionPriorityConfig) DeepCopyInto(out *TopologyDeletionPriorityConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TopologyDeletionPriorityConfig. +func (in *TopologyDeletionPriorityConfig) DeepCopy() *TopologyDeletionPriorityConfig { + if in == nil { + return nil + } + out := new(TopologyDeletionPriorityConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) { *out = *in diff --git a/config/crd/bases/game.kruise.io_gameserversets.yaml b/config/crd/bases/game.kruise.io_gameserversets.yaml index 05afc9d8..26a7eef7 100644 --- a/config/crd/bases/game.kruise.io_gameserversets.yaml +++ b/config/crd/bases/game.kruise.io_gameserversets.yaml @@ -21,7 +21,7 @@ spec: jsonPath: .spec.replicas name: DESIRED type: integer - - description: The number of currently all GameServers. + - description: The total number of current GameServers. jsonPath: .status.currentReplicas name: CURRENT type: integer @@ -794,7 +794,7 @@ spec: image: description: |- Image indicates the image of the container to update. - When Image updated, pod.spec.containers[*].image will be updated immediately. + When Image is updated, pod.spec.containers[*].image will be updated immediately. type: string name: description: Name indicates the name of the container @@ -803,8 +803,8 @@ spec: resources: description: |- Resources indicates the resources of the container to update. - When Resources updated, pod.spec.containers[*].Resources will be not updated immediately, - which will be updated when pod recreate. + When Resources are updated, pod.spec.containers[*].Resources will not be updated immediately, + which will be updated when the pod is recreated. properties: claims: description: |- @@ -882,8 +882,8 @@ spec: type: string result: description: |- - Result indicate the probe message returned by the script. - When Result is defined, it would exec action only when the according Result is actually returns. + Result indicates the probe message returned by the script. + When Result is defined, it would exec action only when the corresponding Result is actually returned. type: string state: type: boolean @@ -947,6 +947,18 @@ spec: - permanent type: object type: array + topologyDeletionPriority: + properties: + basePriority: + default: 100 + type: integer + ownerCountWeight: + default: 5 + type: integer + podCountWeight: + default: 10 + type: integer + type: object updateStrategy: properties: rollingUpdate: @@ -957,7 +969,7 @@ spec: description: |- UnorderedUpdate contains strategies for non-ordered update. If it is not nil, pods will be updated with non-ordered sequence. - Noted that UnorderedUpdate can only be allowed to work with Parallel podManagementPolicy + Note that UnorderedUpdate can only be allowed to work with Parallel podManagementPolicy UnorderedUpdate *kruiseV1beta1.UnorderedUpdateStrategy `json:"unorderedUpdate,omitempty"` InPlaceUpdateStrategy contains strategies for in-place update. properties: diff --git a/examples/topology-deletion-priority-automated/gameserverset.yaml b/examples/topology-deletion-priority-automated/gameserverset.yaml new file mode 100644 index 00000000..10034719 --- /dev/null +++ b/examples/topology-deletion-priority-automated/gameserverset.yaml @@ -0,0 +1,21 @@ +apiVersion: game.kruise.io/v1alpha1 +kind: GameServerSet +metadata: + name: gs-topology-automated + namespace: default +spec: + replicas: 10 + topologyDeletionPriority: + basePriority: 100 + podCountWeight: 10 + ownerCountWeight: 5 + + gameServerTemplate: + spec: + containers: + - name: minecraft + image: minecraft:latest + resources: + requests: + cpu: 100m + memory: 128Mi diff --git a/pkg/controllers/gameserver/gameserver_manager.go b/pkg/controllers/gameserver/gameserver_manager.go index 1ca24c32..f9755da7 100644 --- a/pkg/controllers/gameserver/gameserver_manager.go +++ b/pkg/controllers/gameserver/gameserver_manager.go @@ -73,6 +73,7 @@ type GameServerManager struct { client client.Client eventRecorder record.EventRecorder logger logr.Logger + topologyRater *TopologyRater } func isNeedToSyncMetadata(gss *gameKruiseV1alpha1.GameServerSet, gs *gameKruiseV1alpha1.GameServer) bool { @@ -383,13 +384,30 @@ func (manager GameServerManager) SyncPodToGs(ctx context.Context, gss *gameKruis return err } - // patch gs status + if gss.Spec.TopologyDeletionPriority != nil && gs.Spec.DeletionPriority == nil { + if manager.topologyRater != nil { + topologyPriority, err := manager.topologyRater.CalculateDeletionPriority(ctx, gs, pod, gss.Spec.TopologyDeletionPriority) + if err != nil { + manager.logger.Error(err, "failed to calculate topology-based deletion priority", + telemetryfields.FieldGameServerNamespace, gs.GetNamespace(), + telemetryfields.FieldGameServerName, gs.GetName()) + } else if topologyPriority != nil { + podDeletePriority = *topologyPriority + manager.logger.V(1).Info("calculated topology-based deletion priority", + telemetryfields.FieldGameServerNamespace, gs.GetNamespace(), + telemetryfields.FieldGameServerName, gs.GetName(), + "priority", topologyPriority.String()) + } + } + } + newStatus := gameKruiseV1alpha1.GameServerStatus{ PodStatus: pod.Status, CurrentState: podGsState, DesiredState: gameKruiseV1alpha1.Ready, UpdatePriority: &podUpdatePriority, DeletionPriority: &podDeletePriority, + ServiceQualitiesCondition: sqConditions, NetworkStatus: manager.syncNetworkStatus(), LastTransitionTime: oldGsStatus.LastTransitionTime, @@ -619,12 +637,13 @@ func (manager GameServerManager) syncPodContainers(gsContainers []gameKruiseV1al return newContainers } -func NewGameServerManager(gs *gameKruiseV1alpha1.GameServer, pod *corev1.Pod, c client.Client, recorder record.EventRecorder, logger logr.Logger) Control { +func NewGameServerManager(gameServer *gameKruiseV1alpha1.GameServer, pod *corev1.Pod, c client.Client, eventRecorder record.EventRecorder, logger logr.Logger) Control { return &GameServerManager{ - gameServer: gs, + gameServer: gameServer, pod: pod, client: c, - eventRecorder: recorder, + eventRecorder: eventRecorder, logger: logger, + topologyRater: NewTopologyRater(c), } } diff --git a/pkg/controllers/gameserver/gameserver_manager_test.go b/pkg/controllers/gameserver/gameserver_manager_test.go index d7bcaa0f..27f1c220 100644 --- a/pkg/controllers/gameserver/gameserver_manager_test.go +++ b/pkg/controllers/gameserver/gameserver_manager_test.go @@ -1208,3 +1208,4 @@ func TestSyncPodToGs(t *testing.T) { } } } + diff --git a/pkg/controllers/gameserver/topology_rater.go b/pkg/controllers/gameserver/topology_rater.go new file mode 100644 index 00000000..fb8a70eb --- /dev/null +++ b/pkg/controllers/gameserver/topology_rater.go @@ -0,0 +1,88 @@ +package gameserver + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" +) + +const ( + defaultBasePriority = 100 + defaultPodCountWeight = 10 + defaultOwnerCountWeight = 5 +) + +type TopologyRater struct { + client client.Client +} + +func NewTopologyRater(c client.Client) *TopologyRater { + return &TopologyRater{client: c} +} + +func (r *TopologyRater) CalculateDeletionPriority( + ctx context.Context, + gs *gameKruiseV1alpha1.GameServer, + pod *corev1.Pod, + config *gameKruiseV1alpha1.TopologyDeletionPriorityConfig, +) (*intstr.IntOrString, error) { + if config == nil { + return nil, nil + } + + nodeName := pod.Spec.NodeName + if nodeName == "" { + return nil, nil + } + + basePriority := defaultBasePriority + podCountWeight := defaultPodCountWeight + ownerCountWeight := defaultOwnerCountWeight + + if config.BasePriority != 0 { + basePriority = config.BasePriority + } + if config.PodCountWeight != 0 { + podCountWeight = config.PodCountWeight + } + if config.OwnerCountWeight != 0 { + ownerCountWeight = config.OwnerCountWeight + } + + podList := &corev1.PodList{} + listOpts := &client.ListOptions{ + Namespace: pod.Namespace, + FieldSelector: fields.SelectorFromSet(fields.Set{"spec.nodeName": nodeName}), + } + + if err := r.client.List(ctx, podList, listOpts); err != nil { + return nil, fmt.Errorf("failed to list pods on node %s: %w", nodeName, err) + } + + podCount := len(podList.Items) + ownerSet := make(map[types.UID]struct{}) + + for _, p := range podList.Items { + ownerRefs := p.GetOwnerReferences() + for _, owner := range ownerRefs { + ownerSet[owner.UID] = struct{}{} + } + } + ownerCount := len(ownerSet) + + priority := basePriority - (podCount * podCountWeight) - (ownerCount * ownerCountWeight) + + if priority < 0 { + priority = 0 + } + + result := intstr.FromInt(priority) + return &result, nil +} diff --git a/pkg/controllers/gameserver/topology_rater_test.go b/pkg/controllers/gameserver/topology_rater_test.go new file mode 100644 index 00000000..9760eba1 --- /dev/null +++ b/pkg/controllers/gameserver/topology_rater_test.go @@ -0,0 +1,62 @@ +package gameserver + +import ( + "context" + "testing" + + gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +// TestTopologyRater_Basic covers the main happy-path calculation using defaults. +func TestTopologyRater_Basic(t *testing.T) { + ctx := context.TODO() + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = gameKruiseV1alpha1.AddToScheme(scheme) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "pod-1", + OwnerReferences: []metav1.OwnerReference{ + {UID: types.UID("owner-1")}, + }, + }, + Spec: corev1.PodSpec{ + NodeName: "node-a", + }, + } + + cl := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod). + WithIndex(&corev1.Pod{}, "spec.nodeName", func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{pod.Spec.NodeName} + }). + Build() + rater := NewTopologyRater(cl) + + cfg := &gameKruiseV1alpha1.TopologyDeletionPriorityConfig{} + + got, err := rater.CalculateDeletionPriority(ctx, &gameKruiseV1alpha1.GameServer{}, pod, cfg) + if err != nil { + t.Fatalf("CalculateDeletionPriority returned error: %v", err) + } + if got == nil { + t.Fatalf("expected non-nil priority") + } + + // With one pod and one owner on the node and default weights: + // priority = 100 - 1*10 - 1*5 = 85. + if got.IntVal != 85 { + t.Fatalf("unexpected priority: got=%d want=%d", got.IntVal, 85) + } +}