|
7 | 7 | "testing" |
8 | 8 |
|
9 | 9 | "github.com/stretchr/testify/assert" |
| 10 | + "github.com/stretchr/testify/require" |
10 | 11 | "github.com/zalando/skipper/eskip" |
11 | 12 | "github.com/zalando/skipper/net" |
12 | 13 | "github.com/zalando/skipper/routing" |
@@ -282,6 +283,11 @@ func TestApply(t *testing.T) { |
282 | 283 | expected: N, |
283 | 284 | algorithm: newPowerOfRandomNChoices(eps), |
284 | 285 | algorithmName: "powerOfRandomNChoices", |
| 286 | + }, { |
| 287 | + name: "weightedRoundRobin algorithm", |
| 288 | + expected: N, |
| 289 | + algorithm: newWeightedRoundRobin(eps), |
| 290 | + algorithmName: "weightedRoundRobin", |
285 | 291 | }} { |
286 | 292 | t.Run(tt.name, func(t *testing.T) { |
287 | 293 | req, _ := http.NewRequest("GET", "http://127.0.0.1:1234/foo", nil) |
@@ -573,3 +579,125 @@ func BenchmarkRandomAlgorithm(b *testing.B) { |
573 | 579 | alg.Apply(lbc) |
574 | 580 | } |
575 | 581 | } |
| 582 | + |
| 583 | +type fixedWeightMetrics struct { |
| 584 | + routing.Metrics |
| 585 | + weight float64 |
| 586 | +} |
| 587 | + |
| 588 | +func (m fixedWeightMetrics) Weight() float64 { return m.weight } |
| 589 | + |
| 590 | +func setupWeightedRoundRobinRoute(t *testing.T, registry *routing.EndpointRegistry, endpointAddresses []string) *routing.Route { |
| 591 | + t.Helper() |
| 592 | + provider := NewAlgorithmProvider() |
| 593 | + route := &routing.Route{ |
| 594 | + Route: eskip.Route{ |
| 595 | + BackendType: eskip.LBBackend, |
| 596 | + LBAlgorithm: "weightedRoundRobin", |
| 597 | + LBEndpoints: eskip.NewLBEndpoints(endpointAddresses), |
| 598 | + }, |
| 599 | + } |
| 600 | + processedRoutes := provider.Do([]*routing.Route{route}) |
| 601 | + registry.Do([]*routing.Route{route}) |
| 602 | + require.Len(t, processedRoutes, 1) |
| 603 | + return processedRoutes[0] |
| 604 | +} |
| 605 | + |
| 606 | +func applyAndCountSelections(t *testing.T, route *routing.Route, rounds int) map[string]int { |
| 607 | + t.Helper() |
| 608 | + request, err := http.NewRequest("GET", "http://127.0.0.1:1234/foo", nil) |
| 609 | + require.NoError(t, err) |
| 610 | + lbContext := &routing.LBContext{ |
| 611 | + Request: request, |
| 612 | + Route: route, |
| 613 | + LBEndpoints: route.LBEndpoints, |
| 614 | + } |
| 615 | + |
| 616 | + selectionCounts := make(map[string]int) |
| 617 | + for i := 0; i < rounds; i++ { |
| 618 | + selectionCounts[route.LBAlgorithm.Apply(lbContext).Host]++ |
| 619 | + } |
| 620 | + return selectionCounts |
| 621 | +} |
| 622 | + |
| 623 | +func TestWeightedRoundRobinDistribution(t *testing.T) { |
| 624 | + endpointAddresses := []string{"http://127.0.0.1:1231/foo", "http://127.0.0.1:1232/foo", "http://127.0.0.1:1233/foo"} |
| 625 | + endpointWeights := []float64{0.2, 0.8, 1.0} |
| 626 | + |
| 627 | + registry := routing.NewEndpointRegistry(routing.RegistryOptions{}) |
| 628 | + defer registry.Close() |
| 629 | + route := setupWeightedRoundRobinRoute(t, registry, endpointAddresses) |
| 630 | + for i := range route.LBEndpoints { |
| 631 | + route.LBEndpoints[i].Metrics = fixedWeightMetrics{Metrics: route.LBEndpoints[i].Metrics, weight: endpointWeights[i]} |
| 632 | + } |
| 633 | + |
| 634 | + const rounds = 1000 |
| 635 | + selectionCounts := applyAndCountSelections(t, route, rounds) |
| 636 | + |
| 637 | + // smooth weighted roundrobin distributes proportionally to the weights |
| 638 | + totalWeight := 0.0 |
| 639 | + for _, weight := range endpointWeights { |
| 640 | + totalWeight += weight |
| 641 | + } |
| 642 | + for i, weight := range endpointWeights { |
| 643 | + expectedSelections := rounds * weight / totalWeight |
| 644 | + assert.InDelta(t, expectedSelections, selectionCounts[route.LBEndpoints[i].Host], 1.0, "endpoint %d", i) |
| 645 | + } |
| 646 | +} |
| 647 | + |
| 648 | +func TestWeightedRoundRobinEqualWeights(t *testing.T) { |
| 649 | + const numberOfEndpoints = 5 |
| 650 | + endpointAddresses := make([]string, 0, numberOfEndpoints) |
| 651 | + for i := 0; i < numberOfEndpoints; i++ { |
| 652 | + endpointAddresses = append(endpointAddresses, fmt.Sprintf("http://127.0.0.1:123%d/foo", i)) |
| 653 | + } |
| 654 | + |
| 655 | + registry := routing.NewEndpointRegistry(routing.RegistryOptions{}) |
| 656 | + defer registry.Close() |
| 657 | + route := setupWeightedRoundRobinRoute(t, registry, endpointAddresses) |
| 658 | + |
| 659 | + // with the default weight of 1.0 every endpoint receives an equal share |
| 660 | + const rounds = 1000 |
| 661 | + selectionCounts := applyAndCountSelections(t, route, rounds) |
| 662 | + |
| 663 | + assert.Len(t, selectionCounts, numberOfEndpoints) |
| 664 | + for host, count := range selectionCounts { |
| 665 | + assert.Equal(t, rounds/numberOfEndpoints, count, "host %s", host) |
| 666 | + } |
| 667 | +} |
| 668 | + |
| 669 | +func BenchmarkWeightedRoundRobinAlgorithm(b *testing.B) { |
| 670 | + for _, numberOfEndpoints := range []int{10, 100, 1000, 10000} { |
| 671 | + b.Run(fmt.Sprintf("%d_endpoints", numberOfEndpoints), func(b *testing.B) { |
| 672 | + endpointAddresses := make([]string, numberOfEndpoints) |
| 673 | + for i := range numberOfEndpoints { |
| 674 | + endpointAddresses[i] = fmt.Sprintf("10.0.%d.%d:8080", i/256, i%256) |
| 675 | + } |
| 676 | + |
| 677 | + registry := routing.NewEndpointRegistry(routing.RegistryOptions{}) |
| 678 | + defer registry.Close() |
| 679 | + |
| 680 | + algorithm := newWeightedRoundRobin(endpointAddresses) |
| 681 | + |
| 682 | + endpoints := make([]routing.LBEndpoint, len(endpointAddresses)) |
| 683 | + for i := range len(endpointAddresses) { |
| 684 | + endpoints[i] = routing.LBEndpoint{ |
| 685 | + Scheme: "http", |
| 686 | + Host: endpointAddresses[i], |
| 687 | + Metrics: registry.GetMetrics(endpointAddresses[i]), |
| 688 | + } |
| 689 | + } |
| 690 | + |
| 691 | + lbContext := &routing.LBContext{ |
| 692 | + Route: &routing.Route{}, |
| 693 | + LBEndpoints: endpoints, |
| 694 | + } |
| 695 | + |
| 696 | + b.ResetTimer() |
| 697 | + |
| 698 | + for i := 0; i < b.N; i++ { |
| 699 | + algorithm.Apply(lbContext) |
| 700 | + } |
| 701 | + }) |
| 702 | + } |
| 703 | +} |
0 commit comments