Skip to content

Commit 81640d3

Browse files
authored
Merge pull request #69 from jawnsy/add-initial-capacity
Add initial capacity setting
2 parents 6c496bd + 86779e9 commit 81640d3

6 files changed

Lines changed: 217 additions & 852 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ service specified in the `autoneg` configuration annotation.
4747
Only the NEGs created by the GKE NEG controller will be added or removed from your backend service. This mechanism should be safe to
4848
use across multiple clusters.
4949

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

5456
## Autoneg Configuration
@@ -67,6 +69,7 @@ Specify options to configure the backends representing the NEGs that will be ass
6769
* `region`: optional. Used to specify that this is a regional backend service.
6870
* `max_rate_per_endpoint`: required/optional. Integer representing the maximum rate a pod can handle. Pick either rate or connection.
6971
* `max_connections_per_endpoint`: required/optional. Integer representing the maximum amount of connections a pod can handle. Pick either rate or connection.
72+
* `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).
7073

7174
### Controller parameters
7275

controllers/autoneg.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2019-2021 Google LLC.
2+
Copyright 2019-2023 Google LLC.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -59,19 +59,33 @@ func (e *errNotFound) Error() string {
5959
// Backend returns a compute.Backend struct specified with a backend group
6060
// and the embedded AutonegConfig
6161
func (s AutonegStatus) Backend(name string, port string, group string) compute.Backend {
62-
if s.AutonegConfig.BackendServices[port][name].Rate > 0 {
62+
cfg := s.AutonegConfig.BackendServices[port][name]
63+
64+
// Extract initial_capacity setting, if set
65+
var capacityScaler float64 = 1
66+
if capacity := cfg.InitialCapacity; capacity != nil {
67+
// This case should not be possible since validateNewConfig checks
68+
// it, but leave the default setting of 100% if capacity is less
69+
// than 0 or greater than 100
70+
if *capacity >= int32(0) && *capacity <= int32(100) {
71+
capacityScaler = float64(*capacity) / 100
72+
}
73+
}
74+
75+
// Prefer the rate balancing mode if set
76+
if cfg.Rate > 0 {
6377
return compute.Backend{
6478
Group: group,
6579
BalancingMode: "RATE",
66-
MaxRatePerEndpoint: s.AutonegConfig.BackendServices[port][name].Rate,
67-
CapacityScaler: 1,
80+
MaxRatePerEndpoint: cfg.Rate,
81+
CapacityScaler: capacityScaler,
6882
}
6983
} else {
7084
return compute.Backend{
7185
Group: group,
7286
BalancingMode: "CONNECTION",
73-
MaxConnectionsPerEndpoint: int64(s.AutonegConfig.BackendServices[port][name].Connections),
74-
CapacityScaler: 1,
87+
MaxConnectionsPerEndpoint: int64(cfg.Connections),
88+
CapacityScaler: capacityScaler,
7589
}
7690
}
7791
}
@@ -381,8 +395,16 @@ func validateOldConfig(cfg OldAutonegConfig) error {
381395
return nil
382396
}
383397

384-
func validateNewConfig(cfg AutonegConfig) error {
385-
// do additional validation
398+
func validateNewConfig(config AutonegConfig) error {
399+
for _, cfgs := range config.BackendServices {
400+
for _, cfg := range cfgs {
401+
if cfg.InitialCapacity != nil {
402+
if *cfg.InitialCapacity < 0 || *cfg.InitialCapacity > 100 {
403+
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)
404+
}
405+
}
406+
}
407+
}
386408
return nil
387409
}
388410

controllers/autoneg_test.go

Lines changed: 167 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2019-2021 Google LLC.
2+
Copyright 2019-2023 Google LLC.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -17,18 +17,21 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"math"
2021
"reflect"
2122
"testing"
2223

2324
"google.golang.org/api/compute/v1"
25+
"k8s.io/utils/pointer"
2426
)
2527

2628
var (
27-
malformedJSON = `{`
28-
validConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":100}],"443":[{"name":"https-be","max_connections_per_endpoint":1000}]}}`
29-
brokenConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":"100"}],"443":[{"name":"https-be","max_connections_per_endpoint":1000}}}`
30-
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}]}}`
31-
validConfigWoName = `{"backend_services":{"80":[{"max_rate_per_endpoint":100}],"443":[{"max_connections_per_endpoint":1000}]}}`
29+
malformedJSON = `{`
30+
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}]}}`
31+
brokenConfig = `{"backend_services":{"80":[{"name":"http-be","max_rate_per_endpoint":"100"}],"443":[{"name":"https-be","max_connections_per_endpoint":1000}}}`
32+
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}]}}`
33+
validConfigWoName = `{"backend_services":{"80":[{"max_rate_per_endpoint":100}],"443":[{"max_connections_per_endpoint":1000}]}}`
34+
invalidCapacityConfig = `{"backend_services":{"443":[{"max_connections_per_endpoint":1000,"initial_capacity":500}]}}`
3235

3336
validStatus = `{}`
3437
validAutonegConfig = `{}`
@@ -79,6 +82,14 @@ var statusTests = []struct {
7982
true,
8083
false,
8184
},
85+
{
86+
"valid multi autoneg",
87+
map[string]string{
88+
autonegAnnotation: validMultiConfig,
89+
},
90+
true,
91+
false,
92+
},
8293
{
8394
"valid autoneg with invalid status",
8495
map[string]string{
@@ -115,6 +126,24 @@ var statusTests = []struct {
115126
true,
116127
false,
117128
},
129+
{
130+
"invalid capacity config with valid neg status",
131+
map[string]string{
132+
autonegAnnotation: invalidCapacityConfig,
133+
negStatusAnnotation: validStatus,
134+
},
135+
true,
136+
true,
137+
},
138+
{
139+
"valid autoneg config with valid neg status",
140+
map[string]string{
141+
autonegAnnotation: validAutonegConfig,
142+
negStatusAnnotation: validStatus,
143+
},
144+
true,
145+
false,
146+
},
118147
}
119148

120149
var oldStatusTests = []struct {
@@ -273,20 +302,20 @@ func TestGetStatusesServiceNameAllowed(t *testing.T) {
273302
}
274303
}
275304

276-
var configTests = []struct {
277-
name string
278-
config OldAutonegConfig
279-
err bool
280-
}{
281-
{
282-
"default config",
283-
OldAutonegConfig{},
284-
false,
285-
},
286-
}
305+
func TestValidateOldConfig(t *testing.T) {
306+
tests := []struct {
307+
name string
308+
config OldAutonegConfig
309+
err bool
310+
}{
311+
{
312+
"default config",
313+
OldAutonegConfig{},
314+
false,
315+
},
316+
}
287317

288-
func TestValidateConfig(t *testing.T) {
289-
for _, ct := range configTests {
318+
for _, ct := range tests {
290319
err := validateOldConfig(ct.config)
291320
if err == nil && ct.err {
292321
t.Errorf("Set %q: expected error, got none", ct.name)
@@ -297,6 +326,125 @@ func TestValidateConfig(t *testing.T) {
297326
}
298327
}
299328

329+
func TestValidateNewConfig(t *testing.T) {
330+
tests := []struct {
331+
name string
332+
config AutonegConfig
333+
err bool
334+
expectedCapacityScaler float64
335+
}{
336+
{
337+
name: "default config",
338+
config: AutonegConfig{},
339+
err: false,
340+
expectedCapacityScaler: 1,
341+
},
342+
{
343+
name: "negative initial_capacity",
344+
config: AutonegConfig{
345+
BackendServices: map[string]map[string]AutonegNEGConfig{
346+
"80": {
347+
"http-be": {
348+
Name: "http-be",
349+
Connections: 100,
350+
InitialCapacity: pointer.Int32Ptr(int32(-10)),
351+
},
352+
},
353+
},
354+
},
355+
err: true,
356+
expectedCapacityScaler: 1,
357+
},
358+
{
359+
name: "large initial capacity",
360+
config: AutonegConfig{
361+
BackendServices: map[string]map[string]AutonegNEGConfig{
362+
"80": {
363+
"http-be": {
364+
Name: "http-be",
365+
Connections: 100,
366+
InitialCapacity: pointer.Int32Ptr(int32(5000)),
367+
},
368+
},
369+
},
370+
},
371+
err: true,
372+
expectedCapacityScaler: 1,
373+
},
374+
{
375+
name: "zero initial capacity",
376+
config: AutonegConfig{
377+
BackendServices: map[string]map[string]AutonegNEGConfig{
378+
"80": {
379+
"http-be": {
380+
Name: "http-be",
381+
Connections: 100,
382+
InitialCapacity: pointer.Int32Ptr(int32(0)),
383+
},
384+
},
385+
},
386+
},
387+
err: false,
388+
expectedCapacityScaler: 0,
389+
},
390+
{
391+
name: "half initial capacity",
392+
config: AutonegConfig{
393+
BackendServices: map[string]map[string]AutonegNEGConfig{
394+
"80": {
395+
"http-be": {
396+
Name: "http-be",
397+
Connections: 100,
398+
InitialCapacity: pointer.Int32Ptr(int32(50)),
399+
},
400+
},
401+
},
402+
},
403+
err: false,
404+
expectedCapacityScaler: 0.5,
405+
},
406+
{
407+
name: "max initial capacity",
408+
config: AutonegConfig{
409+
BackendServices: map[string]map[string]AutonegNEGConfig{
410+
"80": {
411+
"http-be": {
412+
Name: "http-be",
413+
Rate: 100,
414+
InitialCapacity: pointer.Int32Ptr(int32(100)),
415+
},
416+
},
417+
},
418+
},
419+
err: false,
420+
expectedCapacityScaler: 1,
421+
},
422+
}
423+
424+
for _, ct := range tests {
425+
err := validateNewConfig(ct.config)
426+
if err == nil && ct.err {
427+
t.Errorf("Set %q: expected error, got none", ct.name)
428+
}
429+
if err != nil && !ct.err {
430+
t.Errorf("Set %q: expected no error, got one: %v", ct.name, err)
431+
}
432+
433+
// The compute.Backend object should have a float64 value in
434+
// the range [0.0, 1.0]
435+
status := AutonegStatus{AutonegConfig: ct.config}
436+
beConfig := status.Backend("http-be", "80", "group")
437+
if beConfig.CapacityScaler < 0 || beConfig.CapacityScaler > 1 {
438+
t.Errorf("Set %q: expected capacityScaler in [0.0, 1.0], got %f", ct.name, beConfig.CapacityScaler)
439+
}
440+
441+
// Actual value should be within 1e-9 of expected
442+
if diff := math.Abs(beConfig.CapacityScaler - ct.expectedCapacityScaler); diff > 1e-9 {
443+
t.Errorf("Set %q: expected CapacityScaler of %f, got %f (diff %f)", ct.name, ct.expectedCapacityScaler, beConfig.CapacityScaler, diff)
444+
}
445+
}
446+
}
447+
300448
func relevantCopy(a compute.Backend) compute.Backend {
301449
b := compute.Backend{}
302450
b.Group = a.Group

controllers/types.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2019-2021 Google LLC.
2+
Copyright 2019-2023 Google LLC.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -45,10 +45,11 @@ type AutonegConfigTemp struct {
4545
// AutonegConfig specifies the intended configuration of autoneg
4646
// stored in the controller.autoneg.dev/neg annotation
4747
type AutonegNEGConfig struct {
48-
Name string `json:"name,omitempty"`
49-
Region string `json:"region,omitempty"`
50-
Rate float64 `json:"max_rate_per_endpoint,omitempty"`
51-
Connections float64 `json:"max_connections_per_endpoint,omitempty"`
48+
Name string `json:"name,omitempty"`
49+
Region string `json:"region,omitempty"`
50+
Rate float64 `json:"max_rate_per_endpoint,omitempty"`
51+
Connections float64 `json:"max_connections_per_endpoint,omitempty"`
52+
InitialCapacity *int32 `json:"initial_capacity,omitempty"`
5253
}
5354

5455
// AutonegStatus specifies the reconciled status of autoneg

0 commit comments

Comments
 (0)