Skip to content

Add initial capacity setting #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ service specified in the `autoneg` configuration annotation.
Only the NEGs created by the GKE NEG controller will be added or removed from your backend service. This mechanism should be safe to
use across multiple clusters.

Note: `autoneg` will initialize the `capacityScaler` variable to 1 on new registrations. On any changes, `autoneg` will leave
whatever is set in that value. The `capacityScaler` mechanism can be used orthogonally by interactive tooling to manage
By default, `autoneg` will initialize the `capacityScaler` to 1, which means that the new backend will receive a proportional volume
of traffic according to the maximum rate or connections per endpoint configuration. You can customize this default by supplying
the `initial_capacity` variable, which may be useful to steer traffic in blue/green deployment scenarios. On any changes, `autoneg`
will leave whatever is set in that value. The `capacityScaler` mechanism can be used orthogonally by interactive tooling to manage
traffic shifting in such uses cases as deployment or failover.

## Autoneg Configuration
Expand All @@ -67,6 +69,7 @@ Specify options to configure the backends representing the NEGs that will be ass
* `region`: optional. Used to specify that this is a regional backend service.
* `max_rate_per_endpoint`: required/optional. Integer representing the maximum rate a pod can handle. Pick either rate or connection.
* `max_connections_per_endpoint`: required/optional. Integer representing the maximum amount of connections a pod can handle. Pick either rate or connection.
* `initial_capacity`: optional. Integer configuring the initial capacityScaler, expressed as a percentage between 0 and 100. If set to 0, the backend service will not receive any traffic until an operator or other service adjusts the [capacity scaler setting](https://cloud.google.com/load-balancing/docs/backend-service#capacity_scaler).

### Controller parameters

Expand Down
38 changes: 30 additions & 8 deletions controllers/autoneg.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019-2021 Google LLC.
Copyright 2019-2023 Google LLC.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -59,19 +59,33 @@ func (e *errNotFound) Error() string {
// Backend returns a compute.Backend struct specified with a backend group
// and the embedded AutonegConfig
func (s AutonegStatus) Backend(name string, port string, group string) compute.Backend {
if s.AutonegConfig.BackendServices[port][name].Rate > 0 {
cfg := s.AutonegConfig.BackendServices[port][name]

// Extract initial_capacity setting, if set
var capacityScaler float64 = 1
if capacity := cfg.InitialCapacity; capacity != nil {
// This case should not be possible since validateNewConfig checks
// it, but leave the default setting of 100% if capacity is less
// than 0 or greater than 100
if *capacity >= int32(0) && *capacity <= int32(100) {
capacityScaler = float64(*capacity) / 100
}
}

// Prefer the rate balancing mode if set
if cfg.Rate > 0 {
return compute.Backend{
Group: group,
BalancingMode: "RATE",
MaxRatePerEndpoint: s.AutonegConfig.BackendServices[port][name].Rate,
CapacityScaler: 1,
MaxRatePerEndpoint: cfg.Rate,
CapacityScaler: capacityScaler,
}
} else {
return compute.Backend{
Group: group,
BalancingMode: "CONNECTION",
MaxConnectionsPerEndpoint: int64(s.AutonegConfig.BackendServices[port][name].Connections),
CapacityScaler: 1,
MaxConnectionsPerEndpoint: int64(cfg.Connections),
CapacityScaler: capacityScaler,
}
}
}
Expand Down Expand Up @@ -381,8 +395,16 @@ func validateOldConfig(cfg OldAutonegConfig) error {
return nil
}

func validateNewConfig(cfg AutonegConfig) error {
// do additional validation
func validateNewConfig(config AutonegConfig) error {
for _, cfgs := range config.BackendServices {
for _, cfg := range cfgs {
if cfg.InitialCapacity != nil {
if *cfg.InitialCapacity < 0 || *cfg.InitialCapacity > 100 {
return fmt.Errorf("initial_capacity for backend %q must be between 0 and 100 inclusive, but was %q; see https://cloud.google.com/load-balancing/docs/backend-service#capacity_scaler for details", cfg.Name, *cfg.InitialCapacity)
}
}
}
}
return nil
}

Expand Down
186 changes: 167 additions & 19 deletions controllers/autoneg_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019-2021 Google LLC.
Copyright 2019-2023 Google LLC.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -17,18 +17,21 @@ limitations under the License.
package controllers

import (
"math"
"reflect"
"testing"

"google.golang.org/api/compute/v1"
"k8s.io/utils/pointer"
)

var (
malformedJSON = `{`
validConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":100}],"443":[{"name":"https-be","max_connections_per_endpoint":1000}]}}`
brokenConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":"100"}],"443":[{"name":"https-be","max_connections_per_endpoint":1000}}}`
validMultiConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":100},{"name":"http-ilb-be","max_rate_per_endpoint":100}],"443":[{"name":"https-be","max_connections_per_endpoint":1000},{"name":"https-ilb-be","max_connections_per_endpoint":1000}]}}`
validConfigWoName = `{"backend_services":{"80":[{"max_rate_per_endpoint":100}],"443":[{"max_connections_per_endpoint":1000}]}}`
malformedJSON = `{`
validConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":100,"initial_capacity":100}],"443":[{"name":"https-be","max_connections_per_endpoint":1000,"initial_capacity":0}]}}`
brokenConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":"100"}],"443":[{"name":"https-be","max_connections_per_endpoint":1000}}}`
validMultiConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":100},{"name":"http-ilb-be","max_rate_per_endpoint":100}],"443":[{"name":"https-be","max_connections_per_endpoint":1000},{"name":"https-ilb-be","max_connections_per_endpoint":1000}]}}`
validConfigWoName = `{"backend_services":{"80":[{"max_rate_per_endpoint":100}],"443":[{"max_connections_per_endpoint":1000}]}}`
invalidCapacityConfig = `{"backend_services":{"443":[{"max_connections_per_endpoint":1000,"initial_capacity":500}]}}`

validStatus = `{}`
validAutonegConfig = `{}`
Expand Down Expand Up @@ -79,6 +82,14 @@ var statusTests = []struct {
true,
false,
},
{
"valid multi autoneg",
map[string]string{
autonegAnnotation: validMultiConfig,
},
true,
false,
},
{
"valid autoneg with invalid status",
map[string]string{
Expand Down Expand Up @@ -115,6 +126,24 @@ var statusTests = []struct {
true,
false,
},
{
"invalid capacity config with valid neg status",
map[string]string{
autonegAnnotation: invalidCapacityConfig,
negStatusAnnotation: validStatus,
},
true,
true,
},
{
"valid autoneg config with valid neg status",
map[string]string{
autonegAnnotation: validAutonegConfig,
negStatusAnnotation: validStatus,
},
true,
false,
},
}

var oldStatusTests = []struct {
Expand Down Expand Up @@ -273,20 +302,20 @@ func TestGetStatusesServiceNameAllowed(t *testing.T) {
}
}

var configTests = []struct {
name string
config OldAutonegConfig
err bool
}{
{
"default config",
OldAutonegConfig{},
false,
},
}
func TestValidateOldConfig(t *testing.T) {
tests := []struct {
name string
config OldAutonegConfig
err bool
}{
{
"default config",
OldAutonegConfig{},
false,
},
}

func TestValidateConfig(t *testing.T) {
for _, ct := range configTests {
for _, ct := range tests {
err := validateOldConfig(ct.config)
if err == nil && ct.err {
t.Errorf("Set %q: expected error, got none", ct.name)
Expand All @@ -297,6 +326,125 @@ func TestValidateConfig(t *testing.T) {
}
}

func TestValidateNewConfig(t *testing.T) {
tests := []struct {
name string
config AutonegConfig
err bool
expectedCapacityScaler float64
}{
{
name: "default config",
config: AutonegConfig{},
err: false,
expectedCapacityScaler: 1,
},
{
name: "negative initial_capacity",
config: AutonegConfig{
BackendServices: map[string]map[string]AutonegNEGConfig{
"80": {
"http-be": {
Name: "http-be",
Connections: 100,
InitialCapacity: pointer.Int32Ptr(int32(-10)),
},
},
},
},
err: true,
expectedCapacityScaler: 1,
},
{
name: "large initial capacity",
config: AutonegConfig{
BackendServices: map[string]map[string]AutonegNEGConfig{
"80": {
"http-be": {
Name: "http-be",
Connections: 100,
InitialCapacity: pointer.Int32Ptr(int32(5000)),
},
},
},
},
err: true,
expectedCapacityScaler: 1,
},
{
name: "zero initial capacity",
config: AutonegConfig{
BackendServices: map[string]map[string]AutonegNEGConfig{
"80": {
"http-be": {
Name: "http-be",
Connections: 100,
InitialCapacity: pointer.Int32Ptr(int32(0)),
},
},
},
},
err: false,
expectedCapacityScaler: 0,
},
{
name: "half initial capacity",
config: AutonegConfig{
BackendServices: map[string]map[string]AutonegNEGConfig{
"80": {
"http-be": {
Name: "http-be",
Connections: 100,
InitialCapacity: pointer.Int32Ptr(int32(50)),
},
},
},
},
err: false,
expectedCapacityScaler: 0.5,
},
{
name: "max initial capacity",
config: AutonegConfig{
BackendServices: map[string]map[string]AutonegNEGConfig{
"80": {
"http-be": {
Name: "http-be",
Rate: 100,
InitialCapacity: pointer.Int32Ptr(int32(100)),
},
},
},
},
err: false,
expectedCapacityScaler: 1,
},
}

for _, ct := range tests {
err := validateNewConfig(ct.config)
if err == nil && ct.err {
t.Errorf("Set %q: expected error, got none", ct.name)
}
if err != nil && !ct.err {
t.Errorf("Set %q: expected no error, got one: %v", ct.name, err)
}

// The compute.Backend object should have a float64 value in
// the range [0.0, 1.0]
status := AutonegStatus{AutonegConfig: ct.config}
beConfig := status.Backend("http-be", "80", "group")
if beConfig.CapacityScaler < 0 || beConfig.CapacityScaler > 1 {
t.Errorf("Set %q: expected capacityScaler in [0.0, 1.0], got %f", ct.name, beConfig.CapacityScaler)
}

// Actual value should be within 1e-9 of expected
if diff := math.Abs(beConfig.CapacityScaler - ct.expectedCapacityScaler); diff > 1e-9 {
t.Errorf("Set %q: expected CapacityScaler of %f, got %f (diff %f)", ct.name, ct.expectedCapacityScaler, beConfig.CapacityScaler, diff)
}
}
}

func relevantCopy(a compute.Backend) compute.Backend {
b := compute.Backend{}
b.Group = a.Group
Expand Down
11 changes: 6 additions & 5 deletions controllers/types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019-2021 Google LLC.
Copyright 2019-2023 Google LLC.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,10 +45,11 @@ type AutonegConfigTemp struct {
// AutonegConfig specifies the intended configuration of autoneg
// stored in the controller.autoneg.dev/neg annotation
type AutonegNEGConfig struct {
Name string `json:"name,omitempty"`
Region string `json:"region,omitempty"`
Rate float64 `json:"max_rate_per_endpoint,omitempty"`
Connections float64 `json:"max_connections_per_endpoint,omitempty"`
Name string `json:"name,omitempty"`
Region string `json:"region,omitempty"`
Rate float64 `json:"max_rate_per_endpoint,omitempty"`
Connections float64 `json:"max_connections_per_endpoint,omitempty"`
InitialCapacity *int32 `json:"initial_capacity,omitempty"`
}

// AutonegStatus specifies the reconciled status of autoneg
Expand Down
Loading