Repository: kubernetes
Issue: Excessive memory usage in `k8s.io/apimachinery/pkg/runtime/serializer/versioning.identifiersMap`
Labels: kind/bug,sig/api-machinery,triage/accepted

### What happened?

The `k8s.io/apimachinery/pkg/runtime/serializer/versioning.identifiersMap` https://github.com/kubernetes/kubernetes/blob/ad18954259eae3db51bac2274ed4ca7304b923c4/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning.go#L94 map grows almost every time when `k8s.io/apimachinery/pkg/runtime/serializer/versioning.identifier` is invoked with the list of `GroupVersions` of the same `runtime.scheme`. 

We noticed this issue from one of our controllers, which is creating new `runtime.Codec` on each reconciliation via https://github.com/kubernetes/kubernetes/blob/ad18954259eae3db51bac2274ed4ca7304b923c4/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go#L295

A sample code snippet looks like this:
```golang 
...
var groupVersions schema.GroupVersions
for k := range scheme.AllKnownTypes() {
	groupVersions = append(groupVersions, k.GroupVersion())
}
codec.CodecForVersions(serializer, serializer, groupVersions, groupVersions)
...
```

So, the issue is that `scheme.AllKnownTypes()` is not stable and each time returns the `GroupVersion`s in different order, also a lot of the entries in the list exist multiple times. This way, the list `groupVersions` is different almost every time  when a new Codec is created, and this list is used as key in the `identifiersMap`, hence the map grows every time when a new codec is created. 

As the `identifiersMap` is global variable, it is also not garbage collected and results in excessive memory usage. 




### What did you expect to happen?

The `k8s.io/apimachinery/pkg/runtime/serializer/versioning.identifiersMap` to not grow indefinitely.

On our side, we have applied a mitigation to sort and remove the duplicates from the list of `GroupVersion`s and since then the memory usage stopped increasing with each reconciliation. 

It would be nice this or another mitigation to be implemented in `k8s.io/apimachinery` so that other users to benefit. 
I would propose https://github.com/kubernetes/kubernetes/blob/ad18954259eae3db51bac2274ed4ca7304b923c4/staging/src/k8s.io/apimachinery/pkg/runtime/schema/group_version.go#L245-L252

to remove the duplicates from `gvs` and sort it, before to generate the identifier out of it, but of course it could be done via other means. 


### How can we reproduce it (as minimally and precisely as possible)?

I've implemented a unit test in my fork that shows how the `identifiersMap` grows almost every time 

Here is a git patch based on ad18954259eae3db51bac2274ed4ca7304b923c4

<details>
  <summary>click to see the patch </summary>

```patch
diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning_test.go
index ee12a69adf2..2e9bbaec9d7 100644
--- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning_test.go
+++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/versioning/versioning_test.go
@@ -21,12 +21,15 @@ import (
 	"io"
 	"io/ioutil"
 	"reflect"
+	"sort"
 	"testing"
 
+	testapigroupinstall "k8s.io/apimachinery/pkg/apis/testapigroup/install"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
 	"k8s.io/apimachinery/pkg/util/diff"
+	"k8s.io/apimachinery/pkg/util/sets"
 )
 
 type testDecodable struct {
@@ -403,3 +406,75 @@ func BenchmarkIdentifier(b *testing.B) {
 		}
 	}
 }
+
+func sortGroupVersions(gv schema.GroupVersions) schema.GroupVersions {
+	returnGV := sets.New(gv...).UnsortedList()
+
+	sort.Slice(returnGV, func(i, j int) bool {
+		if returnGV[i].Group == returnGV[j].Group {
+			return returnGV[i].Version < returnGV[j].Version
+		}
+		return returnGV[i].Group < returnGV[j].Group
+	})
+
+	return returnGV
+}
+
+func identifiersMapLen() int {
+	var len = 0
+	identifiersMap.Range(func(key, value any) bool {
+		len++
+		return true
+	})
+	return len
+}
+
+func getGroupVersionsFromScheme(scheme *runtime.Scheme) schema.GroupVersions {
+	var groupVersions schema.GroupVersions
+	for k := range scheme.AllKnownTypes() {
+		groupVersions = append(groupVersions, k.GroupVersion())
+	}
+
+	return groupVersions
+}
+
+func TestExcessiveMemoryUsageIdentifierMap(t *testing.T) {
+	scheme := runtime.NewScheme()
+	testapigroupinstall.Install(scheme)
+
+	initialLen := identifiersMapLen()
+	groupVersions := getGroupVersionsFromScheme(scheme)
+	identifier(groupVersions, nil)
+	firstInsertionLen := identifiersMapLen()
+
+	if firstInsertionLen-initialLen != 1 {
+		t.Error("Expected `identifiersMap` map size to grow by 1")
+	}
+
+	iterations := 100
+	for i := 0; i < iterations; i++ {
+		gv := getGroupVersionsFromScheme(scheme)
+		identifier(gv, nil)
+	}
+	identifierLenAfterAllInsertions := identifiersMapLen()
+
+	if firstInsertionLen == identifierLenAfterAllInsertions {
+		t.Error("Expected the `identifiersMap` to keep growing when groupVersions is not a sorted list of unique elements",
+			"length after first insertion", firstInsertionLen,
+			"length after multiple insertion", identifierLenAfterAllInsertions,
+		)
+	}
+
+	for i := 0; i < iterations; i++ {
+		gv := sortGroupVersions(getGroupVersionsFromScheme(scheme))
+		identifier(gv, nil)
+	}
+	identifierLenAfterUniqueGVInsertions := identifiersMapLen()
+
+	if identifierLenAfterUniqueGVInsertions-identifierLenAfterAllInsertions > 1 {
+		t.Error("Expected the `identifiersMap` size to increase with at most 1 when groupVersions is list of sorted unique elements",
+			"identifierLenAfterUniqueGVInsertions", identifierLenAfterUniqueGVInsertions,
+			"identifierLenAfterAllInsertions", identifierLenAfterAllInsertions,
+		)
+	}
+}

```


</details>

The change is also available in my fork: https://github.com/vpnachev/kubernetes/commit/a07f2afc9eff35e387a33002a5e5b00b4b8fdef9

It can be also reproduced with the codec creation code snippet from above, however with a stand alone program there is no way the size of `k8s.io/apimachinery/pkg/runtime/serializer/versioning.identifiersMap` to be measured from outside.  

### Anything else we need to know?

_No response_

### Kubernetes version

master, ad18954259eae3db51bac2274ed4ca7304b923c4

### Cloud provider

<details>

</details>


### OS version

<details>

```console
# On Linux:
$ cat /etc/os-release
# paste output here
$ uname -a
# paste output here

# On Windows:
C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
# paste output here
```

</details>


### Install tools

<details>

</details>


### Container runtime (CRI) and version (if applicable)

<details>

</details>


### Related plugins (CNI, CSI, ...) and versions (if applicable)

<details>

</details>