diff --git a/apis/v1alpha1/opentelemetrycollector_types.go b/apis/v1alpha1/opentelemetrycollector_types.go index 0aac34ca75..a296381b23 100644 --- a/apis/v1alpha1/opentelemetrycollector_types.go +++ b/apis/v1alpha1/opentelemetrycollector_types.go @@ -277,6 +277,9 @@ type AutoscalerSpec struct { // If average CPU exceeds this value, the HPA will scale up. Defaults to 90 percent. // +optional TargetCPUUtilization *int32 `json:"targetCPUUtilization,omitempty"` + // +optional + // TargetMemoryUtilization sets the target average memory utilization across all replicas + TargetMemoryUtilization *int32 `json:"targetMemoryUtilization,omitempty"` } func init() { diff --git a/apis/v1alpha1/opentelemetrycollector_webhook.go b/apis/v1alpha1/opentelemetrycollector_webhook.go index fd4c5bd6a9..ea41f2b103 100644 --- a/apis/v1alpha1/opentelemetrycollector_webhook.go +++ b/apis/v1alpha1/opentelemetrycollector_webhook.go @@ -67,13 +67,15 @@ func (r *OpenTelemetryCollector) Default() { r.Spec.TargetAllocator.Replicas = &one } - // Set default targetCPUUtilization for autoscaler - if r.Spec.MaxReplicas != nil && (r.Spec.Autoscaler == nil || r.Spec.Autoscaler.TargetCPUUtilization == nil) { - defaultCPUTarget := int32(90) + if r.Spec.MaxReplicas != nil { if r.Spec.Autoscaler == nil { r.Spec.Autoscaler = &AutoscalerSpec{} } - r.Spec.Autoscaler.TargetCPUUtilization = &defaultCPUTarget + + if r.Spec.Autoscaler.TargetMemoryUtilization == nil && r.Spec.Autoscaler.TargetCPUUtilization == nil { + defaultCPUTarget := int32(90) + r.Spec.Autoscaler.TargetCPUUtilization = &defaultCPUTarget + } } } @@ -176,7 +178,9 @@ func (r *OpenTelemetryCollector) validateCRDSpec() error { if r.Spec.Autoscaler != nil && r.Spec.Autoscaler.TargetCPUUtilization != nil && (*r.Spec.Autoscaler.TargetCPUUtilization < int32(1) || *r.Spec.Autoscaler.TargetCPUUtilization > int32(99)) { return fmt.Errorf("the OpenTelemetry Spec autoscale configuration is incorrect, targetCPUUtilization should be greater than 0 and less than 100") } - + if r.Spec.Autoscaler != nil && r.Spec.Autoscaler.TargetMemoryUtilization != nil && (*r.Spec.Autoscaler.TargetMemoryUtilization < int32(1) || *r.Spec.Autoscaler.TargetMemoryUtilization > int32(99)) { + return fmt.Errorf("the OpenTelemetry Spec autoscale configuration is incorrect, targetMemoryUtilization should be greater than 0 and less than 100") + } } if r.Spec.Ingress.Type == IngressTypeNginx && r.Spec.Mode == ModeSidecar { diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 65bd3901d8..01c5f1dbdc 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -39,6 +39,11 @@ func (in *AutoscalerSpec) DeepCopyInto(out *AutoscalerSpec) { *out = new(int32) **out = **in } + if in.TargetMemoryUtilization != nil { + in, out := &in.TargetMemoryUtilization, &out.TargetMemoryUtilization + *out = new(int32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalerSpec. diff --git a/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml b/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml index 41d2e014ae..38dbce2c79 100644 --- a/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml +++ b/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml @@ -1016,6 +1016,11 @@ spec: the HPA will scale up. Defaults to 90 percent. format: int32 type: integer + targetMemoryUtilization: + description: TargetMemoryUtilization sets the target average memory + utilization across all replicas + format: int32 + type: integer type: object config: description: Config is the raw JSON to be used as the collector's diff --git a/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml b/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml index 068df5d1ad..c6990780b0 100644 --- a/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml +++ b/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml @@ -1014,6 +1014,11 @@ spec: the HPA will scale up. Defaults to 90 percent. format: int32 type: integer + targetMemoryUtilization: + description: TargetMemoryUtilization sets the target average memory + utilization across all replicas + format: int32 + type: integer type: object config: description: Config is the raw JSON to be used as the collector's diff --git a/docs/api.md b/docs/api.md index 87358baaf8..9359a696c4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3228,6 +3228,15 @@ Autoscaler specifies the pod autoscaling configuration to use for the OpenTeleme Format: int32
false + + targetMemoryUtilization + integer + + TargetMemoryUtilization sets the target average memory utilization across all replicas
+
+ Format: int32
+ + false diff --git a/pkg/collector/horizontalpodautoscaler.go b/pkg/collector/horizontalpodautoscaler.go index 8fc133ab1b..c7205562d9 100644 --- a/pkg/collector/horizontalpodautoscaler.go +++ b/pkg/collector/horizontalpodautoscaler.go @@ -33,9 +33,7 @@ func HorizontalPodAutoscaler(cfg config.Config, logger logr.Logger, otelcol v1al labels := Labels(otelcol, cfg.LabelsFilter()) labels["app.kubernetes.io/name"] = naming.Collector(otelcol) - annotations := Annotations(otelcol) - var result client.Object objectMeta := metav1.ObjectMeta{ @@ -46,6 +44,22 @@ func HorizontalPodAutoscaler(cfg config.Config, logger logr.Logger, otelcol v1al } if autoscalingVersion == autodetect.AutoscalingVersionV2Beta2 { + metrics := []autoscalingv2beta2.MetricSpec{} + + if otelcol.Spec.Autoscaler.TargetMemoryUtilization != nil { + utilizationTarget := autoscalingv2beta2.MetricSpec{ + Type: autoscalingv2beta2.ResourceMetricSourceType, + Resource: &autoscalingv2beta2.ResourceMetricSource{ + Name: corev1.ResourceMemory, + Target: autoscalingv2beta2.MetricTarget{ + Type: autoscalingv2beta2.UtilizationMetricType, + AverageUtilization: otelcol.Spec.Autoscaler.TargetMemoryUtilization, + }, + }, + } + metrics = append(metrics, utilizationTarget) + } + targetCPUUtilization := autoscalingv2beta2.MetricSpec{ Type: autoscalingv2beta2.ResourceMetricSourceType, Resource: &autoscalingv2beta2.ResourceMetricSource{ @@ -56,7 +70,7 @@ func HorizontalPodAutoscaler(cfg config.Config, logger logr.Logger, otelcol v1al }, }, } - metrics := []autoscalingv2beta2.MetricSpec{targetCPUUtilization} + metrics = append(metrics, targetCPUUtilization) autoscaler := autoscalingv2beta2.HorizontalPodAutoscaler{ ObjectMeta: objectMeta, @@ -79,17 +93,35 @@ func HorizontalPodAutoscaler(cfg config.Config, logger logr.Logger, otelcol v1al result = &autoscaler } else { - targetCPUUtilization := autoscalingv2.MetricSpec{ - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: corev1.ResourceCPU, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: otelcol.Spec.Autoscaler.TargetCPUUtilization, + metrics := []autoscalingv2.MetricSpec{} + + if otelcol.Spec.Autoscaler.TargetMemoryUtilization != nil { + utilizationTarget := autoscalingv2.MetricSpec{ + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceMemory, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: otelcol.Spec.Autoscaler.TargetMemoryUtilization, + }, }, - }, + } + metrics = append(metrics, utilizationTarget) + } + + if otelcol.Spec.Autoscaler.TargetCPUUtilization != nil { + targetCPUUtilization := autoscalingv2.MetricSpec{ + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: otelcol.Spec.Autoscaler.TargetCPUUtilization, + }, + }, + } + metrics = append(metrics, targetCPUUtilization) } - metrics := []autoscalingv2.MetricSpec{targetCPUUtilization} autoscaler := autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: objectMeta, @@ -104,7 +136,7 @@ func HorizontalPodAutoscaler(cfg config.Config, logger logr.Logger, otelcol v1al Metrics: metrics, }, } - if otelcol.Spec.Autoscaler != nil && otelcol.Spec.Autoscaler.Behavior != nil { + if otelcol.Spec.Autoscaler.Behavior != nil { autoscaler.Spec.Behavior = otelcol.Spec.Autoscaler.Behavior } result = &autoscaler diff --git a/pkg/collector/horizontalpodautoscaler_test.go b/pkg/collector/horizontalpodautoscaler_test.go index 04b90b3480..aa5e1c173b 100644 --- a/pkg/collector/horizontalpodautoscaler_test.go +++ b/pkg/collector/horizontalpodautoscaler_test.go @@ -41,7 +41,8 @@ func TestHPA(t *testing.T) { var minReplicas int32 = 3 var maxReplicas int32 = 5 - var cpuUtilization int32 = 90 + var cpuUtilization int32 = 66 + var memoryUtilization int32 = 77 otelcol := v1alpha1.OpenTelemetryCollector{ ObjectMeta: metav1.ObjectMeta{ @@ -51,7 +52,8 @@ func TestHPA(t *testing.T) { Replicas: &minReplicas, MaxReplicas: &maxReplicas, Autoscaler: &v1alpha1.AutoscalerSpec{ - TargetCPUUtilization: &cpuUtilization, + TargetCPUUtilization: &cpuUtilization, + TargetMemoryUtilization: &memoryUtilization, }, }, } @@ -76,9 +78,13 @@ func TestHPA(t *testing.T) { assert.Equal(t, "my-instance-collector", hpa.Labels["app.kubernetes.io/name"]) assert.Equal(t, int32(3), *hpa.Spec.MinReplicas) assert.Equal(t, int32(5), hpa.Spec.MaxReplicas) - assert.Equal(t, 1, len(hpa.Spec.Metrics)) - assert.Equal(t, corev1.ResourceCPU, hpa.Spec.Metrics[0].Resource.Name) - assert.Equal(t, int32(90), *hpa.Spec.Metrics[0].Resource.Target.AverageUtilization) + for _, metric := range hpa.Spec.Metrics { + if metric.Resource.Name == corev1.ResourceCPU { + assert.Equal(t, cpuUtilization, *metric.Resource.Target.AverageUtilization) + } else if metric.Resource.Name == corev1.ResourceMemory { + assert.Equal(t, memoryUtilization, *metric.Resource.Target.AverageUtilization) + } + } } else { hpa := raw.(*autoscalingv2.HorizontalPodAutoscaler) @@ -87,9 +93,15 @@ func TestHPA(t *testing.T) { assert.Equal(t, "my-instance-collector", hpa.Labels["app.kubernetes.io/name"]) assert.Equal(t, int32(3), *hpa.Spec.MinReplicas) assert.Equal(t, int32(5), hpa.Spec.MaxReplicas) - assert.Equal(t, 1, len(hpa.Spec.Metrics)) - assert.Equal(t, corev1.ResourceCPU, hpa.Spec.Metrics[0].Resource.Name) - assert.Equal(t, int32(90), *hpa.Spec.Metrics[0].Resource.Target.AverageUtilization) + assert.Equal(t, 2, len(hpa.Spec.Metrics)) + + for _, metric := range hpa.Spec.Metrics { + if metric.Resource.Name == corev1.ResourceCPU { + assert.Equal(t, cpuUtilization, *metric.Resource.Target.AverageUtilization) + } else if metric.Resource.Name == corev1.ResourceMemory { + assert.Equal(t, memoryUtilization, *metric.Resource.Target.AverageUtilization) + } + } } }) } diff --git a/pkg/collector/reconcile/horizontalpodautoscaler.go b/pkg/collector/reconcile/horizontalpodautoscaler.go index 1f8435ff11..d5df719b0c 100644 --- a/pkg/collector/reconcile/horizontalpodautoscaler.go +++ b/pkg/collector/reconcile/horizontalpodautoscaler.go @@ -20,6 +20,7 @@ import ( autoscalingv2 "k8s.io/api/autoscaling/v2" autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" + corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/types" @@ -127,7 +128,15 @@ func setAutoscalerSpec(params Params, autoscalingVersion autodetect.AutoscalingV } else { updated.(*autoscalingv2.HorizontalPodAutoscaler).Spec.MinReplicas = &one } - updated.(*autoscalingv2.HorizontalPodAutoscaler).Spec.Metrics[0].Resource.Target.AverageUtilization = params.Instance.Spec.Autoscaler.TargetCPUUtilization + + // This will update memory and CPU usage for now, and can be used to update other metrics in the future + for _, metric := range updated.(*autoscalingv2.HorizontalPodAutoscaler).Spec.Metrics { + if metric.Resource.Name == corev1.ResourceCPU { + metric.Resource.Target.AverageUtilization = params.Instance.Spec.Autoscaler.TargetCPUUtilization + } else if metric.Resource.Name == corev1.ResourceMemory { + metric.Resource.Target.AverageUtilization = params.Instance.Spec.Autoscaler.TargetMemoryUtilization + } + } } } } diff --git a/tests/e2e/autoscale/00-install.yaml b/tests/e2e/autoscale/00-install.yaml index 04e52e7be9..15f4aed640 100644 --- a/tests/e2e/autoscale/00-install.yaml +++ b/tests/e2e/autoscale/00-install.yaml @@ -1,3 +1,6 @@ +# This creates two different deployments. The first one will be used to see if we scale properly. (Note that we are +# only scaling up to 2 because of limitations of KUTTL). The second is to check the targetCPUUtilization option. +# apiVersion: opentelemetry.io/v1alpha1 kind: OpenTelemetryCollector metadata: diff --git a/tests/e2e/autoscale/01-assert.yaml b/tests/e2e/autoscale/01-assert.yaml index 2c2eec0238..76c712f7af 100644 --- a/tests/e2e/autoscale/01-assert.yaml +++ b/tests/e2e/autoscale/01-assert.yaml @@ -1,3 +1,4 @@ +# Wait until tracegen has completed and the simplest deployment has scaled up to 2 apiVersion: batch/v1 kind: Job metadata: diff --git a/tests/e2e/autoscale/02-assert.yaml b/tests/e2e/autoscale/02-assert.yaml index fb4e052f2b..c610ad3f0e 100644 --- a/tests/e2e/autoscale/02-assert.yaml +++ b/tests/e2e/autoscale/02-assert.yaml @@ -1,3 +1,4 @@ +# Wait for the collector to scale back down to 1 apiVersion: opentelemetry.io/v1alpha1 kind: OpenTelemetryCollector