Skip to content

Commit 6ea4968

Browse files
RomanZavodskikhRoman Zavodskikh
and
Roman Zavodskikh
authored
Fixed the searchRing function for consistentHash (zalando#2460)
* minor: added different colors for different endpoints in fadein graphs Signed-off-by: Roman Zavodskikh <[email protected]> * Added test for even load between not fading in endpoints Signed-off-by: Roman Zavodskikh <[email protected]> * Fixed the searchRing function for consistentHash This fix is needed to comply the requirements of input for binary search https://pkg.go.dev/sort#Search Search uses binary search to find and return the smallest index i in [0, n) at which f(i) is true, assuming that on the range [0, n), f(i) == true implies f(i+1) == true. That is, Search requires that f is false for some (possibly empty) prefix of the input range [0, n) and then true for the (possibly empty) remainder Signed-off-by: Roman Zavodskikh <[email protected]> * Added test that LBAlgorithm.Apply finishes with all endpoints fading Signed-off-by: Roman Zavodskikh <[email protected]> * minor: get rid of repeatable code Signed-off-by: Roman Zavodskikh <[email protected]> --------- Signed-off-by: Roman Zavodskikh <[email protected]> Co-authored-by: Roman Zavodskikh <[email protected]>
1 parent 4d664c9 commit 6ea4968

File tree

3 files changed

+158
-44
lines changed

3 files changed

+158
-44
lines changed

loadbalancer/algorithm.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,13 @@ func hash(s string) uint64 {
241241
// Returns index in hash ring with the closest hash to key's hash
242242
func (ch *consistentHash) searchRing(key string, skipEndpoint func(int) bool) int {
243243
h := hash(key)
244-
i := sort.Search(ch.Len(), func(i int) bool { return ch.hashRing[i].hash >= h && !skipEndpoint(ch.hashRing[i].index) })
244+
i := sort.Search(ch.Len(), func(i int) bool { return ch.hashRing[i].hash >= h })
245245
if i == ch.Len() { // rollover
246246
i = 0
247247
}
248+
for skipEndpoint(ch.hashRing[i].index) {
249+
i = (i + 1) % ch.Len()
250+
}
248251
return i
249252
}
250253

loadbalancer/fadein_test.go

+147-39
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import (
99
"testing"
1010
"time"
1111

12+
"github.com/stretchr/testify/assert"
1213
"github.com/zalando/skipper/routing"
1314
)
1415

1516
const (
16-
fadeInDuration = 500 * time.Millisecond
17-
bucketCount = 20
18-
monotonyTolerance = 0.4 // we need to use a high tolerance for CI testing
17+
fadeInDuration = 500 * time.Millisecond
18+
fadeInDurationHuge = 24 * time.Hour // we need this to be sure we're at the very beginning of fading in
19+
bucketCount = 20
20+
monotonyTolerance = 0.4 // we need to use a high tolerance for CI testing
1921
)
2022

2123
func absint(i int) int {
@@ -42,41 +44,46 @@ func checkMonotony(direction, prev, next int) bool {
4244
}
4345
}
4446

47+
func initializeEndpoints(endpointAges []time.Duration, fadeInDuration time.Duration) (*routing.LBContext, []string) {
48+
var detectionTimes []time.Time
49+
now := time.Now()
50+
for _, ea := range endpointAges {
51+
detectionTimes = append(detectionTimes, now.Add(-ea))
52+
}
53+
54+
var eps []string
55+
for i := range endpointAges {
56+
eps = append(eps, fmt.Sprintf("ep-%d-%s.test", i, endpointAges[i]))
57+
}
58+
59+
ctx := &routing.LBContext{
60+
Params: map[string]interface{}{},
61+
Route: &routing.Route{
62+
LBFadeInDuration: fadeInDuration,
63+
LBFadeInExponent: 1,
64+
},
65+
}
66+
67+
for i := range eps {
68+
ctx.Route.LBEndpoints = append(ctx.Route.LBEndpoints, routing.LBEndpoint{
69+
Host: eps[i],
70+
Detected: detectionTimes[i],
71+
})
72+
}
73+
74+
return ctx, eps
75+
}
76+
4577
func testFadeIn(
4678
t *testing.T,
4779
name string,
4880
algorithm func([]string) routing.LBAlgorithm,
4981
endpointAges ...time.Duration,
5082
) {
5183
t.Run(name, func(t *testing.T) {
52-
var detectionTimes []time.Time
53-
now := time.Now()
54-
for _, ea := range endpointAges {
55-
detectionTimes = append(detectionTimes, now.Add(-ea))
56-
}
57-
58-
var eps []string
59-
for i := range endpointAges {
60-
eps = append(eps, string('a'+rune(i)))
61-
}
84+
ctx, eps := initializeEndpoints(endpointAges, fadeInDuration)
6285

6386
a := algorithm(eps)
64-
65-
ctx := &routing.LBContext{
66-
Params: map[string]interface{}{},
67-
Route: &routing.Route{
68-
LBFadeInDuration: fadeInDuration,
69-
LBFadeInExponent: 1,
70-
},
71-
}
72-
73-
for i := range eps {
74-
ctx.Route.LBEndpoints = append(ctx.Route.LBEndpoints, routing.LBEndpoint{
75-
Host: eps[i],
76-
Detected: detectionTimes[i],
77-
})
78-
}
79-
8087
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
8188
t.Log("test start", time.Now())
8289
var stats []string
@@ -154,6 +161,11 @@ func testFadeIn(
154161
})
155162
}
156163

164+
func newConsistentHashForTest(endpoints []string) routing.LBAlgorithm {
165+
// The default parameter 100 is too small to get even distribution
166+
return newConsistentHashInternal(endpoints, 1000)
167+
}
168+
157169
func TestFadeIn(t *testing.T) {
158170
old := 2 * fadeInDuration
159171
testFadeIn(t, "round-robin, 0", newRoundRobin, old, old)
@@ -178,16 +190,112 @@ func TestFadeIn(t *testing.T) {
178190
testFadeIn(t, "random, 8", newRandom, 0, 0, 0, 0, 0, 0)
179191
testFadeIn(t, "random, 9", newRandom, fadeInDuration/2, fadeInDuration/3, fadeInDuration/4)
180192

181-
testFadeIn(t, "consistent-hash, 0", newConsistentHash, old, old)
182-
testFadeIn(t, "consistent-hash, 1", newConsistentHash, 0, old)
183-
testFadeIn(t, "consistent-hash, 2", newConsistentHash, 0, 0)
184-
testFadeIn(t, "consistent-hash, 3", newConsistentHash, old, 0)
185-
testFadeIn(t, "consistent-hash, 4", newConsistentHash, old, old, old, 0)
186-
testFadeIn(t, "consistent-hash, 5", newConsistentHash, old, old, old, 0, 0, 0)
187-
testFadeIn(t, "consistent-hash, 6", newConsistentHash, old, 0, 0, 0)
188-
testFadeIn(t, "consistent-hash, 7", newConsistentHash, old, 0, 0, 0, 0, 0, 0)
189-
testFadeIn(t, "consistent-hash, 8", newConsistentHash, 0, 0, 0, 0, 0, 0)
190-
testFadeIn(t, "consistent-hash, 9", newConsistentHash, fadeInDuration/2, fadeInDuration/3, fadeInDuration/4)
193+
testFadeIn(t, "consistent-hash, 0", newConsistentHashForTest, old, old)
194+
testFadeIn(t, "consistent-hash, 1", newConsistentHashForTest, 0, old)
195+
testFadeIn(t, "consistent-hash, 2", newConsistentHashForTest, 0, 0)
196+
testFadeIn(t, "consistent-hash, 3", newConsistentHashForTest, old, 0)
197+
testFadeIn(t, "consistent-hash, 4", newConsistentHashForTest, old, old, old, 0)
198+
testFadeIn(t, "consistent-hash, 5", newConsistentHashForTest, old, old, old, 0, 0, 0)
199+
testFadeIn(t, "consistent-hash, 6", newConsistentHashForTest, old, 0, 0, 0)
200+
testFadeIn(t, "consistent-hash, 7", newConsistentHashForTest, old, 0, 0, 0, 0, 0, 0)
201+
testFadeIn(t, "consistent-hash, 8", newConsistentHashForTest, 0, 0, 0, 0, 0, 0)
202+
testFadeIn(t, "consistent-hash, 9", newConsistentHashForTest, fadeInDuration/2, fadeInDuration/3, fadeInDuration/4)
203+
}
204+
205+
func testFadeInLoadBetweenOldEps(
206+
t *testing.T,
207+
name string,
208+
algorithm func([]string) routing.LBAlgorithm,
209+
nOld int, nNew int,
210+
) {
211+
t.Run(name, func(t *testing.T) {
212+
const (
213+
numberOfReqs = 100000
214+
acceptableErrorNearZero = 10
215+
old = fadeInDurationHuge
216+
new = time.Duration(0)
217+
)
218+
endpointAges := []time.Duration{}
219+
for i := 0; i < nOld; i++ {
220+
endpointAges = append(endpointAges, old)
221+
}
222+
for i := 0; i < nNew; i++ {
223+
endpointAges = append(endpointAges, new)
224+
}
225+
226+
ctx, eps := initializeEndpoints(endpointAges, fadeInDurationHuge)
227+
228+
a := algorithm(eps)
229+
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
230+
nReqs := map[string]int{}
231+
232+
t.Log("test start", time.Now())
233+
// Emulate the load balancer loop, sending requests to it with random hash keys
234+
// over and over again till fadeIn period is over.
235+
for i := 0; i < numberOfReqs; i++ {
236+
ctx.Params[ConsistentHashKey] = strconv.Itoa(rnd.Intn(100000))
237+
ep := a.Apply(ctx)
238+
nReqs[ep.Host]++
239+
}
240+
241+
expectedReqsPerOldEndpoint := numberOfReqs / nOld
242+
for idx, ep := range eps {
243+
if endpointAges[idx] == old {
244+
assert.InEpsilon(t, expectedReqsPerOldEndpoint, nReqs[ep], 0.2)
245+
}
246+
if endpointAges[idx] == new {
247+
assert.InDelta(t, 0, nReqs[ep], acceptableErrorNearZero)
248+
}
249+
}
250+
})
251+
}
252+
253+
func TestFadeInLoadBetweenOldEps(t *testing.T) {
254+
for nOld := 1; nOld < 6; nOld++ {
255+
for nNew := 0; nNew < 6; nNew++ {
256+
testFadeInLoadBetweenOldEps(t, fmt.Sprintf("consistent-hash, %d old, %d new", nOld, nNew), newConsistentHash, nOld, nNew)
257+
testFadeInLoadBetweenOldEps(t, fmt.Sprintf("random, %d old, %d new", nOld, nNew), newRandom, nOld, nNew)
258+
testFadeInLoadBetweenOldEps(t, fmt.Sprintf("round-robin, %d old, %d new", nOld, nNew), newRoundRobin, nOld, nNew)
259+
}
260+
}
261+
}
262+
263+
func testApplyEndsWhenAllEndpointsAreFading(
264+
t *testing.T,
265+
name string,
266+
algorithm func([]string) routing.LBAlgorithm,
267+
nEndpoints int,
268+
) {
269+
t.Run(name, func(t *testing.T) {
270+
// Initialize every endpoint with zero: every endpoint is new
271+
endpointAges := make([]time.Duration, nEndpoints)
272+
273+
ctx, eps := initializeEndpoints(endpointAges, fadeInDurationHuge)
274+
275+
a := algorithm(eps)
276+
ctx.Params[ConsistentHashKey] = "someConstantString"
277+
applied := make(chan struct{})
278+
279+
go func() {
280+
a.Apply(ctx)
281+
close(applied)
282+
}()
283+
284+
select {
285+
case <-time.After(time.Second):
286+
t.Errorf("a.Apply has not finished in a reasonable time")
287+
case <-applied:
288+
break
289+
}
290+
})
291+
}
292+
293+
func TestApplyEndsWhenAllEndpointsAreFading(t *testing.T) {
294+
for nEndpoints := 1; nEndpoints < 10; nEndpoints++ {
295+
testApplyEndsWhenAllEndpointsAreFading(t, "consistent-hash", newConsistentHash, nEndpoints)
296+
testApplyEndsWhenAllEndpointsAreFading(t, "random", newRandom, nEndpoints)
297+
testApplyEndsWhenAllEndpointsAreFading(t, "round-robin", newRoundRobin, nEndpoints)
298+
}
191299
}
192300

193301
func benchmarkFadeIn(

skptesting/analyze_fadein.r

+7-4
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,14 @@ strFactor <- paste(
5656
paste(names(dat), collapse=","),
5757
")",
5858
collapse=" ", sep="")
59-
## create the formula c(a,b,c,d,e,f) ~ c(1:length(dat$a))
60-
f <- eval(parse(text=strFactor)) ~ c(1:length(dat$a))
59+
## create the formula c(a,b,c,d,e,f) ~ c(1:nrow(dat))
60+
f <- eval(parse(text=strFactor)) ~ c(1:nrow(dat))
61+
colors <- c("blue", "green", "red", "black", "magenta", "yellow", "cyan")
6162

6263
xyplot(f,
6364
data=dat,
6465
xlab="iterations", ylab="hits", ylim=c(0, max(dat)+200), between=list(x=0,y=100),
65-
auto.key=TRUE,
66-
main=title)
66+
key=list(space="bottom",
67+
points=list(col=head(colors, n=ncol(dat)), pch=1),
68+
text=list(names(dat)), cex=0.6),
69+
main=title, col=rep(colors, each=nrow(dat)))

0 commit comments

Comments
 (0)