Skip to content

Commit da8e987

Browse files
smiratalos-bot
authored andcommitted
feat: implement Reconcile - ability to change upstream list on the fly
This is going to be used in Sfyra to provide dynamic load balancer for the control plane. Signed-off-by: Andrey Smirnov <[email protected]>
1 parent 8b1dfa6 commit da8e987

File tree

6 files changed

+255
-4
lines changed

6 files changed

+255
-4
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
22
#
3-
# Generated on 2020-09-01T20:34:11Z by kres ee90a80-dirty.
3+
# Generated on 2020-09-09T15:36:30Z by kres 7e146df-dirty.
44

55
# common variables
66

@@ -34,7 +34,7 @@ COMMON_ARGS += --build-arg=USERNAME=$(USERNAME)
3434
COMMON_ARGS += --build-arg=TOOLCHAIN=$(TOOLCHAIN)
3535
COMMON_ARGS += --build-arg=GOFUMPT_VERSION=$(GOFUMPT_VERSION)
3636
COMMON_ARGS += --build-arg=TESTPKGS=$(TESTPKGS)
37-
TOOLCHAIN ?= docker.io/golang:1.14-alpine
37+
TOOLCHAIN ?= docker.io/golang:1.15-alpine
3838

3939
# help menu
4040

hack/git-chglog/config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
22
#
3-
# Generated on 2020-09-01T20:34:11Z by kres ee90a80-dirty.
3+
# Generated on 2020-09-09T15:36:30Z by kres 7e146df-dirty.
44

55
style: github
66
template: CHANGELOG.tpl.md
77
info:
88
title: CHANGELOG
9-
repository_url: https://github.com/talos-systems/talos
9+
repository_url: https://github.com/talos-systems/go-loadbalancer
1010
options:
1111
commits:
1212
# filters:

loadbalancer/loadbalancer.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package loadbalancer
77

88
import (
99
"context"
10+
"fmt"
1011
"log"
1112
"net"
1213

@@ -25,6 +26,8 @@ import (
2526
// Usage: call Run() to start lb and wait for shutdown, call Close() to shutdown lb.
2627
type TCP struct {
2728
tcpproxy.Proxy
29+
30+
routes map[string]*upstream.List
2831
}
2932

3033
type lbUpstream string
@@ -76,6 +79,10 @@ func (target *lbTarget) HandleConn(conn net.Conn) {
7679
// TCP automatically does background health checks for the upstreams and picks only healthy
7780
// ones. Healthcheck is simple Dial attempt.
7881
func (t *TCP) AddRoute(ipPort string, upstreamAddrs []string, options ...upstream.ListOption) error {
82+
if t.routes == nil {
83+
t.routes = make(map[string]*upstream.List)
84+
}
85+
7986
upstreams := make([]upstream.Backend, len(upstreamAddrs))
8087
for i := range upstreams {
8188
upstreams[i] = lbUpstream(upstreamAddrs[i])
@@ -86,7 +93,30 @@ func (t *TCP) AddRoute(ipPort string, upstreamAddrs []string, options ...upstrea
8693
return err
8794
}
8895

96+
t.routes[ipPort] = list
97+
8998
t.Proxy.AddRoute(ipPort, &lbTarget{list: list})
9099

91100
return nil
92101
}
102+
103+
// ReconcileRoute updates the list of upstreamAddrs for the specified route (ipPort).
104+
func (t *TCP) ReconcileRoute(ipPort string, upstreamAddrs []string) error {
105+
if t.routes == nil {
106+
t.routes = make(map[string]*upstream.List)
107+
}
108+
109+
list := t.routes[ipPort]
110+
if list == nil {
111+
return fmt.Errorf("handler not registered for %q", ipPort)
112+
}
113+
114+
upstreams := make([]upstream.Backend, len(upstreamAddrs))
115+
for i := range upstreams {
116+
upstreams[i] = lbUpstream(upstreamAddrs[i])
117+
}
118+
119+
list.Reconcile(upstreams)
120+
121+
return nil
122+
}

loadbalancer/loadbalancer_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,102 @@ type TCPSuite struct {
7272
suite.Suite
7373
}
7474

75+
func (suite *TCPSuite) TestReconcile() {
76+
const (
77+
upstreamCount = 5
78+
pivot = 2
79+
)
80+
81+
upstreams := make([]mockUpstream, upstreamCount)
82+
for i := range upstreams {
83+
upstreams[i].identity = strconv.Itoa(i)
84+
suite.Require().NoError(upstreams[i].Start())
85+
}
86+
87+
upstreamAddrs := make([]string, len(upstreams))
88+
for i := range upstreamAddrs {
89+
upstreamAddrs[i] = upstreams[i].addr
90+
}
91+
92+
listenAddr, err := findListenAddress()
93+
suite.Require().NoError(err)
94+
95+
lb := &loadbalancer.TCP{}
96+
suite.Require().NoError(lb.AddRoute(
97+
listenAddr,
98+
upstreamAddrs[:pivot],
99+
upstream.WithLowHighScores(-3, 3),
100+
upstream.WithInitialScore(1),
101+
upstream.WithScoreDeltas(-1, 1),
102+
upstream.WithHealthcheckInterval(time.Second),
103+
upstream.WithHealthcheckTimeout(100*time.Millisecond),
104+
))
105+
106+
suite.Require().NoError(lb.Start())
107+
108+
var wg sync.WaitGroup
109+
110+
wg.Add(1)
111+
112+
go func() {
113+
defer wg.Done()
114+
115+
lb.Wait() //nolint: errcheck
116+
}()
117+
118+
for i := 0; i < 5*pivot; i++ {
119+
c, err := net.Dial("tcp", listenAddr)
120+
suite.Require().NoError(err)
121+
122+
id, err := ioutil.ReadAll(c)
123+
suite.Require().NoError(err)
124+
125+
// load balancer should go round-robin across all the upstreams [0:pivot]
126+
suite.Assert().Equal([]byte(strconv.Itoa(i%pivot)), id)
127+
128+
suite.Require().NoError(c.Close())
129+
}
130+
131+
// reconcile the list
132+
suite.Require().NoError(lb.ReconcileRoute(listenAddr, upstreamAddrs[pivot:]))
133+
134+
// bring down pre-pivot upstreams
135+
for i := 0; i < pivot; i++ {
136+
upstreams[i].Close()
137+
}
138+
139+
upstreamsUsed := map[int64]int{}
140+
141+
for i := 0; i < 10*(upstreamCount-pivot); i++ {
142+
c, err := net.Dial("tcp", listenAddr)
143+
suite.Require().NoError(err)
144+
145+
id, err := ioutil.ReadAll(c)
146+
suite.Require().NoError(err)
147+
148+
// load balancer should go round-robin across all the upstreams [pivot:]
149+
no, err := strconv.ParseInt(string(id), 10, 32)
150+
suite.Require().NoError(err)
151+
152+
suite.Assert().Less(no, int64(upstreamCount))
153+
suite.Assert().GreaterOrEqual(no, int64(pivot))
154+
upstreamsUsed[no]++
155+
156+
suite.Require().NoError(c.Close())
157+
}
158+
159+
for _, count := range upstreamsUsed {
160+
suite.Assert().Equal(10, count)
161+
}
162+
163+
suite.Require().NoError(lb.Close())
164+
wg.Wait()
165+
166+
for i := range upstreams {
167+
upstreams[i].Close()
168+
}
169+
}
170+
75171
func (suite *TCPSuite) TestBalancer() {
76172
const (
77173
upstreamCount = 5

upstream/upstream.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,39 @@ func NewList(upstreams []Backend, options ...ListOption) (*List, error) {
159159
return list, nil
160160
}
161161

162+
// Reconcile the list of backends with passed list.
163+
//
164+
// Any new backends are added with initial score, score is untouched
165+
// for backends which haven't changed their score.
166+
func (list *List) Reconcile(upstreams []Backend) {
167+
newUpstreams := make(map[Backend]struct{}, len(upstreams))
168+
169+
for _, upstream := range upstreams {
170+
newUpstreams[upstream] = struct{}{}
171+
}
172+
173+
list.mu.Lock()
174+
defer list.mu.Unlock()
175+
176+
for i := 0; i < len(list.nodes); i++ {
177+
if _, exists := newUpstreams[list.nodes[i].backend]; exists {
178+
delete(newUpstreams, list.nodes[i].backend)
179+
180+
continue
181+
}
182+
183+
list.nodes = append(list.nodes[:i], list.nodes[i+1:]...)
184+
i--
185+
}
186+
187+
for upstream := range newUpstreams {
188+
list.nodes = append(list.nodes, node{
189+
backend: upstream,
190+
score: list.initialScore,
191+
})
192+
}
193+
}
194+
162195
// Shutdown stops healthchecks.
163196
func (list *List) Shutdown() {
164197
list.healthCtxCancel()

upstream/upstream_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,98 @@ func (suite *ListSuite) TestHealthcheck() {
187187
}))
188188
}
189189

190+
func (suite *ListSuite) TestReconcile() {
191+
l, err := upstream.NewList(
192+
[]upstream.Backend{
193+
mockBackend("one"),
194+
mockBackend("two"),
195+
mockBackend("three"),
196+
},
197+
upstream.WithLowHighScores(-3, 3),
198+
upstream.WithInitialScore(1),
199+
upstream.WithScoreDeltas(-1, 1),
200+
upstream.WithHealthcheckInterval(time.Hour),
201+
)
202+
suite.Require().NoError(err)
203+
204+
defer l.Shutdown()
205+
206+
backend, err := l.Pick()
207+
suite.Assert().Equal(mockBackend("one"), backend)
208+
suite.Assert().NoError(err)
209+
210+
l.Reconcile([]upstream.Backend{
211+
mockBackend("one"),
212+
mockBackend("two"),
213+
mockBackend("three"),
214+
})
215+
216+
backend, err = l.Pick()
217+
suite.Assert().Equal(mockBackend("two"), backend)
218+
suite.Assert().NoError(err)
219+
220+
l.Reconcile([]upstream.Backend{
221+
mockBackend("one"),
222+
mockBackend("two"),
223+
mockBackend("four"),
224+
})
225+
226+
backend, err = l.Pick()
227+
suite.Assert().Equal(mockBackend("four"), backend)
228+
suite.Assert().NoError(err)
229+
230+
l.Reconcile([]upstream.Backend{
231+
mockBackend("five"),
232+
mockBackend("six"),
233+
mockBackend("four"),
234+
})
235+
236+
backend, err = l.Pick()
237+
suite.Assert().Equal(mockBackend("four"), backend)
238+
suite.Assert().NoError(err)
239+
240+
backend, err = l.Pick()
241+
suite.Assert().Equal(mockBackend("five"), backend)
242+
suite.Assert().NoError(err)
243+
244+
backend, err = l.Pick()
245+
suite.Assert().Equal(mockBackend("six"), backend)
246+
suite.Assert().NoError(err)
247+
248+
l.Down(mockBackend("four")) // score == 2
249+
l.Down(mockBackend("four")) // score == 1
250+
l.Down(mockBackend("four")) // score == 0
251+
l.Down(mockBackend("four")) // score == -1
252+
253+
backend, err = l.Pick()
254+
suite.Assert().Equal(mockBackend("five"), backend)
255+
suite.Assert().NoError(err)
256+
257+
backend, err = l.Pick()
258+
suite.Assert().Equal(mockBackend("six"), backend)
259+
suite.Assert().NoError(err)
260+
261+
l.Reconcile([]upstream.Backend{
262+
mockBackend("five"),
263+
mockBackend("six"),
264+
mockBackend("four"),
265+
})
266+
267+
backend, err = l.Pick()
268+
suite.Assert().Equal(mockBackend("five"), backend)
269+
suite.Assert().NoError(err)
270+
271+
backend, err = l.Pick()
272+
suite.Assert().Equal(mockBackend("six"), backend)
273+
suite.Assert().NoError(err)
274+
275+
l.Reconcile(nil)
276+
277+
backend, err = l.Pick()
278+
suite.Assert().Nil(backend)
279+
suite.Assert().EqualError(err, "no upstreams available")
280+
}
281+
190282
func TestListSuite(t *testing.T) {
191283
suite.Run(t, new(ListSuite))
192284
}

0 commit comments

Comments
 (0)