Skip to content

Commit

Permalink
Merge pull request #69 from jawnsy/add-initial-capacity
Browse files Browse the repository at this point in the history
Add initial capacity setting
  • Loading branch information
rosmo authored Sep 26, 2023
2 parents 6c496bd + 86779e9 commit 81640d3
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 852 deletions.
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

0 comments on commit 81640d3

Please sign in to comment.