diff --git a/pkg/gameservers/controller.go b/pkg/gameservers/controller.go index d64b6dde77..36010380f9 100644 --- a/pkg/gameservers/controller.go +++ b/pkg/gameservers/controller.go @@ -64,7 +64,13 @@ const ( sdkserverSidecarName = "agones-gameserver-sidecar" grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" httpPortEnvVar = "AGONES_SDK_HTTP_PORT" + goMaxProcsEnvVar = "GOMAXPROCS" + goMemLimitEnvVar = "GOMEMLIMIT" passthroughPortEnvVar = "PASSTHROUGH" + + sidecarGoMaxProcs = "1" + sidecarGoMemLimitRequestPercentage = 90 + bytesPerMebibyte = 1024 * 1024 ) // Extensions struct contains what is needed to bind webhook handlers @@ -763,6 +769,10 @@ func (c *Controller) sidecar(gs *agonesv1.GameServer) corev1.Container { Name: "REQUESTS_RATE_LIMIT", Value: c.sidecarRequestsRateLimit.String(), }, + { + Name: goMaxProcsEnvVar, + Value: sidecarGoMaxProcs, + }, }, Resources: corev1.ResourceRequirements{}, LivenessProbe: &corev1.Probe{ @@ -777,6 +787,18 @@ func (c *Controller) sidecar(gs *agonesv1.GameServer) corev1.Container { }, } + if !c.sidecarMemoryRequest.IsZero() { + goMemLimitBytes := c.sidecarMemoryRequest.Value() * sidecarGoMemLimitRequestPercentage / 100 + goMemLimitMiB := goMemLimitBytes / bytesPerMebibyte + if goMemLimitMiB < 1 { + goMemLimitMiB = 1 + } + sidecar.Env = append(sidecar.Env, corev1.EnvVar{ + Name: goMemLimitEnvVar, + Value: fmt.Sprintf("%dMiB", goMemLimitMiB), + }) + } + if gs.Spec.SdkServer.GRPCPort != 0 { sidecar.Args = append(sidecar.Args, fmt.Sprintf("--grpc-port=%d", gs.Spec.SdkServer.GRPCPort)) } diff --git a/pkg/gameservers/controller_test.go b/pkg/gameservers/controller_test.go index 661c3479e7..8e84d7abb4 100644 --- a/pkg/gameservers/controller_test.go +++ b/pkg/gameservers/controller_test.go @@ -1621,14 +1621,19 @@ func TestControllerCreateGameServerPod(t *testing.T) { assert.Equal(t, sidecarContainer.Resources.Requests.Cpu(), &c.sidecarCPURequest) assert.Equal(t, sidecarContainer.Resources.Limits.Memory(), &c.sidecarMemoryLimit) assert.Equal(t, sidecarContainer.Resources.Requests.Memory(), &c.sidecarMemoryRequest) - assert.Len(t, sidecarContainer.Env, 5, "5 env vars") + assert.Len(t, sidecarContainer.Env, 7, "7 env vars") assert.Equal(t, "GAMESERVER_NAME", sidecarContainer.Env[0].Name) assert.Equal(t, fixture.ObjectMeta.Name, sidecarContainer.Env[0].Value) assert.Equal(t, "POD_NAMESPACE", sidecarContainer.Env[1].Name) assert.Equal(t, "FEATURE_GATES", sidecarContainer.Env[2].Name) assert.Equal(t, "LOG_LEVEL", sidecarContainer.Env[3].Name) assert.Equal(t, "REQUESTS_RATE_LIMIT", sidecarContainer.Env[4].Name) + assert.Equal(t, goMaxProcsEnvVar, sidecarContainer.Env[5].Name) + assert.Equal(t, goMemLimitEnvVar, sidecarContainer.Env[6].Name) assert.Equal(t, "500ms", sidecarContainer.Env[4].Value) + assert.Equal(t, sidecarGoMaxProcs, sidecarContainer.Env[5].Value) + goMemLimitBytes := c.sidecarMemoryRequest.Value() * sidecarGoMemLimitRequestPercentage / 100 + assert.Equal(t, strconv.FormatInt(goMemLimitBytes, 10), sidecarContainer.Env[6].Value) assert.Equal(t, string(fixture.Spec.SdkServer.LogLevel), sidecarContainer.Env[3].Value) assert.Equal(t, *sidecarContainer.SecurityContext.AllowPrivilegeEscalation, false) assert.Equal(t, *sidecarContainer.SecurityContext.RunAsNonRoot, true) @@ -2527,6 +2532,77 @@ func TestControllerAddSDKServerEnvVars(t *testing.T) { }) } +func TestControllerSidecarGoRuntimeResourceHints(t *testing.T) { + t.Parallel() + + fixtures := map[string]struct { + memoryRequest resource.Quantity + expectedEnv map[string]string + }{ + "with memory request": { + memoryRequest: resource.MustParse("100Mi"), + expectedEnv: map[string]string{ + goMaxProcsEnvVar: sidecarGoMaxProcs, + goMemLimitEnvVar: "90MiB", + }, + }, + "with GiB memory request": { + memoryRequest: resource.MustParse("1Gi"), + expectedEnv: map[string]string{ + goMaxProcsEnvVar: sidecarGoMaxProcs, + goMemLimitEnvVar: "921MiB", + }, + }, + "with non-whole MiB memory request": { + memoryRequest: resource.MustParse("100500Ki"), + expectedEnv: map[string]string{ + goMaxProcsEnvVar: sidecarGoMaxProcs, + goMemLimitEnvVar: "88MiB", + }, + }, + "with small memory request": { + memoryRequest: resource.MustParse("1"), + expectedEnv: map[string]string{ + goMaxProcsEnvVar: sidecarGoMaxProcs, + goMemLimitEnvVar: "1MiB", + }, + }, + "without memory request": { + memoryRequest: resource.Quantity{}, + expectedEnv: map[string]string{ + goMaxProcsEnvVar: sidecarGoMaxProcs, + }, + }, + } + + for name, tc := range fixtures { + t.Run(name, func(t *testing.T) { + t.Parallel() + + c, _ := newFakeController() + c.sidecarMemoryRequest = tc.memoryRequest + gs := &agonesv1.GameServer{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: newSingleContainerSpec(), + } + gs.ApplyDefaults() + + sidecar := c.sidecar(gs) + env := map[string]string{} + for _, e := range sidecar.Env { + env[e.Name] = e.Value + } + + for name, value := range tc.expectedEnv { + assert.Equal(t, value, env[name]) + } + if _, ok := tc.expectedEnv[goMemLimitEnvVar]; !ok { + assert.NotContains(t, env, goMemLimitEnvVar) + } + }) + } +} + // testNoChange runs a test with a state that doesn't exist, to ensure a handler // doesn't do process anything beyond the state it is meant to handle. func testNoChange(t *testing.T, state agonesv1.GameServerState, f func(*Controller, *agonesv1.GameServer) (*agonesv1.GameServer, error)) { diff --git a/site/content/en/docs/Advanced/limiting-resources.md b/site/content/en/docs/Advanced/limiting-resources.md index c75cab84de..715e77c1b4 100644 --- a/site/content/en/docs/Advanced/limiting-resources.md +++ b/site/content/en/docs/Advanced/limiting-resources.md @@ -56,3 +56,6 @@ You can do this through the [Helm configuration]({{< ref "/docs/Installation/Ins By default, this is set to having a CPU request value of 30m, with no hard CPU limit. This ensures that the sidecar always has enough CPU to function, but it is configurable in case a lower, or higher value is required on your clusters, or if you desire hard limit. + +When a memory request is configured, Agones sets the SDK sidecar's `GOMEMLIMIT` to 90% of that request so the Go runtime +keeps its managed memory close to the amount scheduled for the sidecar. diff --git a/site/content/en/docs/Installation/Install Agones/helm.md b/site/content/en/docs/Installation/Install Agones/helm.md index 5bbfb05497..26cd800d66 100644 --- a/site/content/en/docs/Installation/Install Agones/helm.md +++ b/site/content/en/docs/Installation/Install Agones/helm.md @@ -150,7 +150,7 @@ The following tables lists the configurable parameters of the Agones chart and t | `agones.image.sdk.tag` | Image tag for the sdk | value of `agones.image.tag` | | `agones.image.sdk.cpuRequest` | The [cpu request][cpu-constraints] for sdk server container | `30m` | | `agones.image.sdk.cpuLimit` | The [cpu limit][cpu-constraints] for the sdk server container | `0` (none) | -| `agones.image.sdk.memoryRequest` | The [memory request][memory-constraints] for sdk server container | `0` (none) | +| `agones.image.sdk.memoryRequest` | The [memory request][memory-constraints] for sdk server container. When set, 90% is used as the SDK sidecar `GOMEMLIMIT` | `0` (none) | | `agones.image.sdk.memoryLimit` | The [memory limit][memory-constraints] for the sdk server container | `0` (none) | | `agones.image.sdk.alwaysPull` | Tells if the sdk image should always be pulled | `false` | | `agones.image.ping.name` | Image name for the ping service | `agones-ping` |