Skip to content

Commit 72bd925

Browse files
authored
[Autoscaler] Refactor metrics client (#83)
1 parent 4e44dbc commit 72bd925

File tree

5 files changed

+199
-66
lines changed

5 files changed

+199
-66
lines changed

cmd/autoscaler/app/autoscaler.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,34 @@ import (
2525
"time"
2626

2727
"github.com/v3io/scaler/pkg/autoscaler"
28+
"github.com/v3io/scaler/pkg/autoscaler/metricsclient"
2829
"github.com/v3io/scaler/pkg/common"
2930
"github.com/v3io/scaler/pkg/pluginloader"
3031
"github.com/v3io/scaler/pkg/scalertypes"
3132

3233
"github.com/nuclio/errors"
3334
"github.com/nuclio/zap"
3435
"k8s.io/apimachinery/pkg/runtime/schema"
35-
"k8s.io/client-go/discovery"
36-
"k8s.io/client-go/discovery/cached/memory"
3736
"k8s.io/client-go/rest"
38-
"k8s.io/client-go/restmapper"
39-
"k8s.io/metrics/pkg/client/custom_metrics"
4037
)
4138

4239
func Run(kubeconfigPath string,
4340
namespace string,
4441
scaleInterval time.Duration,
4542
metricsResourceKind string,
4643
metricsResourceGroup string) error {
44+
// define default auto scaler options
4745
autoScalerOptions := scalertypes.AutoScalerOptions{
4846
Namespace: namespace,
4947
ScaleInterval: scalertypes.Duration{Duration: scaleInterval},
5048
GroupKind: schema.GroupKind{
5149
Kind: metricsResourceKind,
5250
Group: metricsResourceGroup,
5351
},
52+
MetricsClientOptions: scalertypes.MetricsClientOptions{
53+
// default to k8s metrics client for the sake of backwards compatibility
54+
MetricsClientKind: scalertypes.KindK8sMetricsClient,
55+
},
5456
}
5557

5658
pluginLoader, err := pluginloader.New()
@@ -103,16 +105,14 @@ func createAutoScaler(restConfig *rest.Config,
103105
return nil, errors.Wrap(err, "Failed to initialize root logger")
104106
}
105107

106-
discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig)
108+
// create metrics client using factory
109+
metricsClient, err := metricsclient.NewMetricsClient(rootLogger, restConfig, options)
107110
if err != nil {
108-
return nil, errors.Wrap(err, "Failed to create discovery client")
111+
return nil, errors.Wrap(err, "Failed to create metrics client")
109112
}
110-
availableAPIsGetter := custom_metrics.NewAvailableAPIsGetter(discoveryClient)
111-
restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient))
112-
customMetricsClient := custom_metrics.NewForConfig(restConfig, restMapper, availableAPIsGetter)
113113

114114
// create auto scaler
115-
newScaler, err := autoscaler.NewAutoScaler(rootLogger, resourceScaler, customMetricsClient, options)
115+
newScaler, err := autoscaler.NewAutoScaler(rootLogger, resourceScaler, metricsClient, options)
116116
if err != nil {
117117
return nil, errors.Wrap(err, "Failed to create auto scaler")
118118
}

pkg/autoscaler/autoscaler.go

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ import (
2828

2929
"github.com/nuclio/errors"
3030
"github.com/nuclio/logger"
31-
k8serrors "k8s.io/apimachinery/pkg/api/errors"
32-
"k8s.io/apimachinery/pkg/labels"
3331
"k8s.io/apimachinery/pkg/runtime/schema"
34-
"k8s.io/metrics/pkg/client/custom_metrics"
3532
)
3633

3734
type Autoscaler struct {
@@ -41,13 +38,13 @@ type Autoscaler struct {
4138
scaleInterval scalertypes.Duration
4239
inScaleToZeroProcessMap map[string]bool
4340
groupKind schema.GroupKind
44-
customMetricsClientSet custom_metrics.CustomMetricsClient
41+
metricsClient scalertypes.MetricsClient
4542
ticker *time.Ticker
4643
}
4744

4845
func NewAutoScaler(parentLogger logger.Logger,
4946
resourceScaler scalertypes.ResourceScaler,
50-
customMetricsClientSet custom_metrics.CustomMetricsClient,
47+
metricsClient scalertypes.MetricsClient,
5148
options scalertypes.AutoScalerOptions) (*Autoscaler, error) {
5249
childLogger := parentLogger.GetChild("autoscaler")
5350
childLogger.InfoWith("Creating Autoscaler",
@@ -59,7 +56,7 @@ func NewAutoScaler(parentLogger logger.Logger,
5956
resourceScaler: resourceScaler,
6057
scaleInterval: options.ScaleInterval,
6158
groupKind: options.GroupKind,
62-
customMetricsClientSet: customMetricsClientSet,
59+
metricsClient: metricsClient,
6360
inScaleToZeroProcessMap: make(map[string]bool),
6461
}, nil
6562
}
@@ -99,52 +96,6 @@ func (as *Autoscaler) getMetricNames(resources []scalertypes.Resource) []string
9996
return metricNames
10097
}
10198

102-
func (as *Autoscaler) getResourceMetrics(metricNames []string) (map[string]map[string]int, error) {
103-
resourcesMetricsMap := make(map[string]map[string]int)
104-
resourceLabels := labels.Everything()
105-
metricSelectorLabels := labels.Everything()
106-
metricsClient := as.customMetricsClientSet.NamespacedMetrics(as.namespace)
107-
108-
for _, metricName := range metricNames {
109-
110-
// getting the metric values for all object of schema group kind (e.g. deployment)
111-
metrics, err := metricsClient.GetForObjects(as.groupKind, resourceLabels, metricName, metricSelectorLabels)
112-
if err != nil {
113-
114-
// if no data points submitted yet it's ok, continue to the next metric
115-
if k8serrors.IsNotFound(err) {
116-
continue
117-
}
118-
return nil, errors.Wrap(err, "Failed to get custom metrics")
119-
}
120-
121-
// fill the resourcesMetricsMap with the metrics data we got
122-
for _, item := range metrics.Items {
123-
124-
resourceName := item.DescribedObject.Name
125-
value := int(item.Value.MilliValue())
126-
127-
as.logger.DebugWith("Got metric entry",
128-
"resourceName", resourceName,
129-
"metricName", metricName,
130-
"value", value)
131-
132-
if _, found := resourcesMetricsMap[resourceName]; !found {
133-
resourcesMetricsMap[resourceName] = make(map[string]int)
134-
}
135-
136-
// sanity
137-
if _, found := resourcesMetricsMap[resourceName][metricName]; found {
138-
return nil, errors.New("Can not have more than one metric value per resource")
139-
}
140-
141-
resourcesMetricsMap[resourceName][metricName] = value
142-
}
143-
}
144-
145-
return resourcesMetricsMap, nil
146-
}
147-
14899
func (as *Autoscaler) checkResourceToScale(resource scalertypes.Resource, resourcesMetricsMap map[string]map[string]int) bool {
149100
if _, found := resourcesMetricsMap[resource.Name]; !found {
150101
as.logger.DebugWith("Resource does not have metrics data yet, keeping up", "resourceName", resource.Name)
@@ -198,7 +149,7 @@ func (as *Autoscaler) checkResourcesToScale() error {
198149
}
199150
metricNames := as.getMetricNames(activeResources)
200151
as.logger.DebugWith("Got metric names", "metricNames", metricNames)
201-
resourceMetricsMap, err := as.getResourceMetrics(metricNames)
152+
resourceMetricsMap, err := as.metricsClient.GetResourceMetrics(metricNames)
202153
if err != nil {
203154
return errors.Wrap(err, "Failed to get resources metrics")
204155
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Copyright 2026 Iguazio Systems Ltd.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License") with
5+
an addition restriction as set forth herein. You may not use this
6+
file except in compliance with the License. You may obtain a copy of
7+
the License at http://www.apache.org/licenses/LICENSE-2.0.
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
implied. See the License for the specific language governing
13+
permissions and limitations under the License.
14+
15+
In addition, you may not use the software for any purposes that are
16+
illegal under applicable law, and the grant of the foregoing license
17+
under the Apache 2.0 license is conditioned upon your compliance with
18+
such restriction.
19+
*/
20+
21+
package metricsclient
22+
23+
import (
24+
"github.com/v3io/scaler/pkg/scalertypes"
25+
26+
"github.com/nuclio/errors"
27+
"github.com/nuclio/logger"
28+
"k8s.io/client-go/rest"
29+
)
30+
31+
func NewMetricsClient(logger logger.Logger,
32+
restConfig *rest.Config,
33+
autoScalerConf scalertypes.AutoScalerOptions) (scalertypes.MetricsClient, error) {
34+
switch autoScalerConf.MetricsClientOptions.MetricsClientKind {
35+
case scalertypes.KindK8sMetricsClient:
36+
return NewCustomMetricsClient(
37+
logger,
38+
restConfig,
39+
autoScalerConf.Namespace,
40+
autoScalerConf.GroupKind)
41+
default:
42+
return nil, errors.Errorf("unsupported metrics client kind: %s", autoScalerConf.MetricsClientOptions.MetricsClientKind)
43+
}
44+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2026 Iguazio Systems Ltd.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License") with
5+
an addition restriction as set forth herein. You may not use this
6+
file except in compliance with the License. You may obtain a copy of
7+
the License at http://www.apache.org/licenses/LICENSE-2.0.
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
implied. See the License for the specific language governing
13+
permissions and limitations under the License.
14+
15+
In addition, you may not use the software for any purposes that are
16+
illegal under applicable law, and the grant of the foregoing license
17+
under the Apache 2.0 license is conditioned upon your compliance with
18+
such restriction.
19+
*/
20+
21+
package metricsclient
22+
23+
import (
24+
"github.com/nuclio/errors"
25+
"github.com/nuclio/logger"
26+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
27+
"k8s.io/apimachinery/pkg/labels"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"k8s.io/client-go/discovery"
30+
"k8s.io/client-go/discovery/cached/memory"
31+
"k8s.io/client-go/rest"
32+
"k8s.io/client-go/restmapper"
33+
k8scustommetrics "k8s.io/metrics/pkg/client/custom_metrics"
34+
)
35+
36+
type K8sCustomMetricsClient struct {
37+
k8scustommetrics.CustomMetricsClient
38+
namespace string
39+
groupKind schema.GroupKind
40+
logger logger.Logger
41+
}
42+
43+
func NewCustomMetricsClient(
44+
parentLogger logger.Logger,
45+
restConfig *rest.Config,
46+
namespace string,
47+
groupKind schema.GroupKind) (*K8sCustomMetricsClient, error) {
48+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig)
49+
if err != nil {
50+
return nil, errors.Wrap(err, "Failed to create k8s metrics client")
51+
}
52+
availableAPIsGetter := k8scustommetrics.NewAvailableAPIsGetter(discoveryClient)
53+
restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient))
54+
customMetricsClient := k8scustommetrics.NewForConfig(restConfig, restMapper, availableAPIsGetter)
55+
return &K8sCustomMetricsClient{
56+
logger: parentLogger.GetChild("custom-metrics"),
57+
CustomMetricsClient: customMetricsClient,
58+
namespace: namespace,
59+
groupKind: groupKind,
60+
}, nil
61+
}
62+
63+
func (cmw *K8sCustomMetricsClient) GetResourceMetrics(metricNames []string) (map[string]map[string]int, error) {
64+
resourcesMetricsMap := make(map[string]map[string]int)
65+
resourceLabels := labels.Everything()
66+
metricSelectorLabels := labels.Everything()
67+
metricsClient := cmw.NamespacedMetrics(cmw.namespace)
68+
69+
for _, metricName := range metricNames {
70+
// getting the metric values for all object of schema group kind (e.g. deployment)
71+
metrics, err := metricsClient.GetForObjects(cmw.groupKind, resourceLabels, metricName, metricSelectorLabels)
72+
if err != nil {
73+
// No data points have been submitted yet; this is expected—proceed to the next metric.
74+
if k8serrors.IsNotFound(err) {
75+
continue
76+
}
77+
return nil, errors.Wrap(err, "Failed to get custom metrics")
78+
}
79+
80+
// fill the resourcesMetricsMap with the metrics data we got
81+
for _, item := range metrics.Items {
82+
resourceName := item.DescribedObject.Name
83+
value := int(item.Value.MilliValue())
84+
85+
cmw.logger.DebugWith("Got metric entry",
86+
"resourceName", resourceName,
87+
"metricName", metricName,
88+
"value", value)
89+
90+
if _, found := resourcesMetricsMap[resourceName]; !found {
91+
resourcesMetricsMap[resourceName] = make(map[string]int)
92+
}
93+
94+
// sanity
95+
if _, found := resourcesMetricsMap[resourceName][metricName]; found {
96+
return nil, errors.New("Can not have more than one metric value per resource")
97+
}
98+
99+
resourcesMetricsMap[resourceName][metricName] = value
100+
}
101+
}
102+
103+
return resourcesMetricsMap, nil
104+
}

pkg/scalertypes/types.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,23 @@ import (
3333
"k8s.io/client-go/kubernetes"
3434
)
3535

36+
type MetricsClientKind string
37+
38+
const (
39+
KindK8sMetricsClient = "k8sMetricsClient"
40+
)
41+
42+
type MetricsClientOptions struct {
43+
MetricsClientKind MetricsClientKind
44+
URL string
45+
Template string
46+
}
47+
3648
type AutoScalerOptions struct {
37-
Namespace string
38-
ScaleInterval Duration
39-
GroupKind schema.GroupKind
49+
Namespace string
50+
ScaleInterval Duration
51+
GroupKind schema.GroupKind
52+
MetricsClientOptions MetricsClientOptions
4053
}
4154

4255
type ResourceScalerConfig struct {
@@ -199,3 +212,24 @@ func shortDurationString(d Duration) string {
199212
}
200213
return s
201214
}
215+
216+
// MetricsClient defines an interface for retrieving resource metrics used by the autoscaler.
217+
type MetricsClient interface {
218+
// GetResourceMetrics retrieves metrics for multiple resources and metric names.
219+
//
220+
// Parameters:
221+
// - metricNames: A slice of metric names to retrieve (e.g., "requests_per_minute", "cpu_usage_per_hour")
222+
//
223+
// Returns:
224+
// - map[string]map[string]int: A nested map structure where:
225+
// * The outer map key is the resource name (e.g., deployment name)
226+
// * The inner map key is the metric name
227+
// * The inner map value is the metric value as an integer
228+
// Example: map["my-deployment"]["requests_per_minute"] = 42
229+
// - error: An error if metric retrieval fails
230+
//
231+
// The dual map structure allows efficient lookup of metric values by resource name
232+
// and then by metric name, enabling the autoscaler to check multiple metrics
233+
// per resource when making scaling decisions.
234+
GetResourceMetrics(metricNames []string) (map[string]map[string]int, error)
235+
}

0 commit comments

Comments
 (0)