From 3edaf9ecb64445fc0e3938b376ac7da95038b28c Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 24 Dec 2021 14:31:34 +0300 Subject: [PATCH] netmap: sort buckets by HRW value If weights for some buckets are equal, result depends on the initial positions of buckets in slice. Thus we sort buckets by value using hash of the first node as a have for the whole bucket. This is ok, because buckets are already sorted by HRW. Signed-off-by: Evgenii Stratonikov --- netmap/node_info.go | 10 +++ netmap/selector.go | 7 +- netmap/selector_test.go | 177 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 2 deletions(-) diff --git a/netmap/node_info.go b/netmap/node_info.go index 08c50e43..3a3b57f7 100644 --- a/netmap/node_info.go +++ b/netmap/node_info.go @@ -96,6 +96,16 @@ func (n Node) Hash() uint64 { return n.ID } +// Hash is a function from hrw.Hasher interface. It is implemented +// to support weighted hrw sorting of buckets. Each bucket is already sorted by hrw, +// thus giving us needed "randomness". +func (n Nodes) Hash() uint64 { + if len(n) > 0 { + return n[0].Hash() + } + return 0 +} + // NodesFromInfo converts slice of NodeInfo to a generic node slice. func NodesFromInfo(infos []NodeInfo) Nodes { nodes := make(Nodes, len(infos)) diff --git a/netmap/selector.go b/netmap/selector.go index 236259b7..87192a00 100644 --- a/netmap/selector.go +++ b/netmap/selector.go @@ -58,8 +58,11 @@ func (c *Context) getSelection(p *PlacementPolicy, s *Selector) ([]Nodes, error) return nil, fmt.Errorf("%w: '%s'", ErrNotEnoughNodes, s.Name()) } + // We need deterministic output in case there is no pivot. + // If pivot is set, buckets are sorted by HRW. + // However, because initial order influences HRW order for buckets with equal weights, + // we also need to have deterministic input to HRW sorting routine. if len(c.pivot) == 0 { - // Deterministic order in case of zero seed. if s.Attribute() == "" { sort.Slice(buckets, func(i, j int) bool { return buckets[i].nodes[0].ID < buckets[j].nodes[0].ID @@ -98,7 +101,7 @@ func (c *Context) getSelection(p *PlacementPolicy, s *Selector) ([]Nodes, error) weights[i] = GetBucketWeight(nodes[i], c.aggregator(), c.weightFunc) } - hrw.SortSliceByWeightIndex(nodes, weights, c.pivotHash) + hrw.SortSliceByWeightValue(nodes, weights, c.pivotHash) } if s.Attribute() == "" { diff --git a/netmap/selector_test.go b/netmap/selector_test.go index 2aa9798c..b84cb1d2 100644 --- a/netmap/selector_test.go +++ b/netmap/selector_test.go @@ -3,13 +3,190 @@ package netmap import ( "errors" "fmt" + "math/rand" + "sort" + "strconv" "testing" + "github.com/nspcc-dev/hrw" "github.com/nspcc-dev/neofs-api-go/v2/netmap" testv2 "github.com/nspcc-dev/neofs-api-go/v2/netmap/test" "github.com/stretchr/testify/require" ) +func BenchmarkHRWSort(b *testing.B) { + const netmapSize = 1000 + + nodes := make([]Nodes, netmapSize) + weights := make([]float64, netmapSize) + for i := range nodes { + nodes[i] = Nodes{{ + ID: rand.Uint64(), + Index: i, + Capacity: 100, + Price: 1, + AttrMap: nil, + }} + weights[i] = float64(rand.Uint32()%10) / 10.0 + } + + pivot := rand.Uint64() + b.Run("sort by index, no weight", func(b *testing.B) { + realNodes := make([]Nodes, netmapSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + copy(realNodes, nodes) + b.StartTimer() + + hrw.SortSliceByIndex(realNodes, pivot) + } + }) + b.Run("sort by value, no weight", func(b *testing.B) { + realNodes := make([]Nodes, netmapSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + copy(realNodes, nodes) + b.StartTimer() + + hrw.SortSliceByValue(realNodes, pivot) + } + }) + b.Run("only sort by index", func(b *testing.B) { + realNodes := make([]Nodes, netmapSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + copy(realNodes, nodes) + b.StartTimer() + + hrw.SortSliceByWeightIndex(realNodes, weights, pivot) + } + }) + b.Run("sort by value", func(b *testing.B) { + realNodes := make([]Nodes, netmapSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + copy(realNodes, nodes) + b.StartTimer() + + hrw.SortSliceByWeightValue(realNodes, weights, pivot) + } + }) + b.Run("sort by ID, then by index (deterministic)", func(b *testing.B) { + realNodes := make([]Nodes, netmapSize) + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + copy(realNodes, nodes) + b.StartTimer() + + sort.Slice(nodes, func(i, j int) bool { + return nodes[i][0].ID < nodes[j][0].ID + }) + hrw.SortSliceByWeightIndex(realNodes, weights, pivot) + } + }) +} + +func BenchmarkPolicyHRWType(b *testing.B) { + const netmapSize = 100 + + p := newPlacementPolicy(1, + []*Replica{ + newReplica(1, "loc1"), + newReplica(1, "loc2")}, + []*Selector{ + newSelector("loc1", "Location", ClauseSame, 1, "loc1"), + newSelector("loc2", "Location", ClauseSame, 1, "loc2")}, + []*Filter{ + newFilter("loc1", "Location", "Shanghai", OpEQ), + newFilter("loc2", "Location", "Shanghai", OpNE), + }) + + nodes := make([]NodeInfo, netmapSize) + for i := range nodes { + var loc string + switch i % 20 { + case 0: + loc = "Shanghai" + default: + loc = strconv.Itoa(i % 20) + } + + // Having the same price and capacity ensures equal weights for all nodes. + // This way placement is more dependent on the initial order. + nodes[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10") + pub := make([]byte, 33) + pub[0] = byte(i) + nodes[i].SetPublicKey(pub) + } + + nm, err := NewNetmap(NodesFromInfo(nodes)) + require.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := nm.GetContainerNodes(p, []byte{1}) + if err != nil { + b.Fatal() + } + } +} + +func TestPlacementPolicy_DeterministicOrder(t *testing.T) { + const netmapSize = 100 + + p := newPlacementPolicy(1, + []*Replica{ + newReplica(1, "loc1"), + newReplica(1, "loc2")}, + []*Selector{ + newSelector("loc1", "Location", ClauseSame, 1, "loc1"), + newSelector("loc2", "Location", ClauseSame, 1, "loc2")}, + []*Filter{ + newFilter("loc1", "Location", "Shanghai", OpEQ), + newFilter("loc2", "Location", "Shanghai", OpNE), + }) + + nodes := make([]NodeInfo, netmapSize) + for i := range nodes { + var loc string + switch i % 20 { + case 0: + loc = "Shanghai" + default: + loc = strconv.Itoa(i % 20) + } + + // Having the same price and capacity ensures equal weights for all nodes. + // This way placement is more dependent on the initial order. + nodes[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10") + pub := make([]byte, 33) + pub[0] = byte(i) + nodes[i].SetPublicKey(pub) + } + + nm, err := NewNetmap(NodesFromInfo(nodes)) + require.NoError(t, err) + getIndices := func(t *testing.T) (int, int) { + v, err := nm.GetContainerNodes(p, []byte{1}) + require.NoError(t, err) + ns := v.Flatten() + require.Equal(t, 2, len(ns)) + return ns[0].Index, ns[1].Index + } + + a, b := getIndices(t) + for i := 0; i < 10; i++ { + x, y := getIndices(t) + require.Equal(t, a, x) + require.Equal(t, b, y) + } +} + func TestPlacementPolicy_UnspecifiedClause(t *testing.T) { p := newPlacementPolicy(1, []*Replica{newReplica(1, "X")},