github.com/thanos-io/thanos@v0.32.5/pkg/receive/hashring_test.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package receive 5 6 import ( 7 "fmt" 8 "math" 9 "strings" 10 "testing" 11 12 "github.com/efficientgo/core/testutil" 13 "github.com/stretchr/testify/require" 14 15 "github.com/prometheus/prometheus/model/labels" 16 17 "github.com/thanos-io/thanos/pkg/store/labelpb" 18 "github.com/thanos-io/thanos/pkg/store/storepb/prompb" 19 ) 20 21 func TestHashringGet(t *testing.T) { 22 ts := &prompb.TimeSeries{ 23 Labels: []labelpb.ZLabel{ 24 { 25 Name: "foo", 26 Value: "bar", 27 }, 28 { 29 Name: "baz", 30 Value: "qux", 31 }, 32 }, 33 } 34 35 for _, tc := range []struct { 36 name string 37 cfg []HashringConfig 38 nodes map[string]struct{} 39 tenant string 40 }{ 41 { 42 name: "empty", 43 cfg: nil, 44 tenant: "tenant1", 45 }, 46 { 47 name: "simple", 48 cfg: []HashringConfig{ 49 { 50 Endpoints: []Endpoint{{Address: "node1"}}, 51 }, 52 }, 53 nodes: map[string]struct{}{"node1": {}}, 54 }, 55 { 56 name: "specific", 57 cfg: []HashringConfig{ 58 { 59 Endpoints: []Endpoint{{Address: "node2"}}, 60 Tenants: []string{"tenant2"}, 61 }, 62 { 63 Endpoints: []Endpoint{{Address: "node1"}}, 64 }, 65 }, 66 nodes: map[string]struct{}{"node2": {}}, 67 tenant: "tenant2", 68 }, 69 { 70 name: "many tenants", 71 cfg: []HashringConfig{ 72 { 73 Endpoints: []Endpoint{{Address: "node1"}}, 74 Tenants: []string{"tenant1"}, 75 }, 76 { 77 Endpoints: []Endpoint{{Address: "node2"}}, 78 Tenants: []string{"tenant2"}, 79 }, 80 { 81 Endpoints: []Endpoint{{Address: "node3"}}, 82 Tenants: []string{"tenant3"}, 83 }, 84 }, 85 nodes: map[string]struct{}{"node1": {}}, 86 tenant: "tenant1", 87 }, 88 { 89 name: "many tenants error", 90 cfg: []HashringConfig{ 91 { 92 Endpoints: []Endpoint{{Address: "node1"}}, 93 Tenants: []string{"tenant1"}, 94 }, 95 { 96 Endpoints: []Endpoint{{Address: "node2"}}, 97 Tenants: []string{"tenant2"}, 98 }, 99 { 100 Endpoints: []Endpoint{{Address: "node3"}}, 101 Tenants: []string{"tenant3"}, 102 }, 103 }, 104 tenant: "tenant4", 105 }, 106 { 107 name: "many nodes", 108 cfg: []HashringConfig{ 109 { 110 Endpoints: []Endpoint{{Address: "node1"}, {Address: "node2"}, {Address: "node3"}}, 111 Tenants: []string{"tenant1"}, 112 }, 113 { 114 Endpoints: []Endpoint{{Address: "node4"}, {Address: "node5"}, {Address: "node6"}}, 115 }, 116 }, 117 nodes: map[string]struct{}{ 118 "node1": {}, 119 "node2": {}, 120 "node3": {}, 121 }, 122 tenant: "tenant1", 123 }, 124 { 125 name: "many nodes default", 126 cfg: []HashringConfig{ 127 { 128 Endpoints: []Endpoint{{Address: "node1"}, {Address: "node2"}, {Address: "node3"}}, 129 Tenants: []string{"tenant1"}, 130 }, 131 { 132 Endpoints: []Endpoint{{Address: "node4"}, {Address: "node5"}, {Address: "node6"}}, 133 }, 134 }, 135 nodes: map[string]struct{}{ 136 "node4": {}, 137 "node5": {}, 138 "node6": {}, 139 }, 140 }, 141 } { 142 hs, err := NewMultiHashring(AlgorithmHashmod, 3, tc.cfg) 143 require.NoError(t, err) 144 145 h, err := hs.Get(tc.tenant, ts) 146 if tc.nodes != nil { 147 if err != nil { 148 t.Errorf("case %q: got unexpected error: %v", tc.name, err) 149 continue 150 } 151 if _, ok := tc.nodes[h]; !ok { 152 t.Errorf("case %q: got unexpected node %q", tc.name, h) 153 } 154 continue 155 } 156 if err == nil { 157 t.Errorf("case %q: expected error", tc.name) 158 } 159 } 160 } 161 162 func TestKetamaHashringGet(t *testing.T) { 163 baseTS := &prompb.TimeSeries{ 164 Labels: []labelpb.ZLabel{ 165 { 166 Name: "pod", 167 Value: "nginx", 168 }, 169 }, 170 } 171 tests := []struct { 172 name string 173 endpoints []Endpoint 174 expectedNode string 175 ts *prompb.TimeSeries 176 n uint64 177 }{ 178 { 179 name: "base case", 180 endpoints: []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}, 181 ts: baseTS, 182 expectedNode: "node-2", 183 }, 184 { 185 name: "base case with replication", 186 endpoints: []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}, 187 ts: baseTS, 188 n: 1, 189 expectedNode: "node-1", 190 }, 191 { 192 name: "base case with replication", 193 endpoints: []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}, 194 ts: baseTS, 195 n: 2, 196 expectedNode: "node-3", 197 }, 198 { 199 name: "base case with replication and reordered nodes", 200 endpoints: []Endpoint{{Address: "node-1"}, {Address: "node-3"}, {Address: "node-2"}}, 201 ts: baseTS, 202 n: 2, 203 expectedNode: "node-3", 204 }, 205 { 206 name: "base case with new node at beginning of ring", 207 endpoints: []Endpoint{{Address: "node-0"}, {Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}, 208 ts: baseTS, 209 expectedNode: "node-2", 210 }, 211 { 212 name: "base case with new node at end of ring", 213 endpoints: []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}, {Address: "node-4"}}, 214 ts: baseTS, 215 expectedNode: "node-2", 216 }, 217 { 218 name: "base case with different timeseries", 219 endpoints: []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}, 220 ts: &prompb.TimeSeries{ 221 Labels: []labelpb.ZLabel{ 222 { 223 Name: "pod", 224 Value: "thanos", 225 }, 226 }, 227 }, 228 expectedNode: "node-3", 229 }, 230 } 231 232 for _, test := range tests { 233 t.Run(test.name, func(t *testing.T) { 234 hashRing, err := newKetamaHashring(test.endpoints, 10, test.n+1) 235 require.NoError(t, err) 236 237 result, err := hashRing.GetN("tenant", test.ts, test.n) 238 require.NoError(t, err) 239 require.Equal(t, test.expectedNode, result) 240 }) 241 } 242 } 243 244 func TestKetamaHashringBadConfigIsRejected(t *testing.T) { 245 _, err := newKetamaHashring([]Endpoint{{Address: "node-1"}}, 1, 2) 246 require.Error(t, err) 247 } 248 249 func TestKetamaHashringConsistency(t *testing.T) { 250 series := makeSeries() 251 252 ringA := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}} 253 a1, err := assignSeries(series, ringA) 254 require.NoError(t, err) 255 256 ringB := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}} 257 a2, err := assignSeries(series, ringB) 258 require.NoError(t, err) 259 260 for node, ts := range a1 { 261 require.Len(t, a2[node], len(ts), "node %s has an inconsistent number of series", node) 262 } 263 264 for node, ts := range a2 { 265 require.Len(t, a1[node], len(ts), "node %s has an inconsistent number of series", node) 266 } 267 } 268 269 func TestKetamaHashringIncreaseAtEnd(t *testing.T) { 270 series := makeSeries() 271 272 initialRing := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}} 273 initialAssignments, err := assignSeries(series, initialRing) 274 require.NoError(t, err) 275 276 resizedRing := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}, {Address: "node-4"}, {Address: "node-5"}} 277 reassignments, err := assignSeries(series, resizedRing) 278 require.NoError(t, err) 279 280 // Assert that the initial nodes have no new keys after increasing the ring size 281 for _, node := range initialRing { 282 for _, ts := range reassignments[node.Address] { 283 foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts) 284 require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node) 285 } 286 } 287 } 288 289 func TestKetamaHashringIncreaseInMiddle(t *testing.T) { 290 series := makeSeries() 291 292 initialRing := []Endpoint{{Address: "node-1"}, {Address: "node-3"}} 293 initialAssignments, err := assignSeries(series, initialRing) 294 require.NoError(t, err) 295 296 resizedRing := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}} 297 reassignments, err := assignSeries(series, resizedRing) 298 require.NoError(t, err) 299 300 // Assert that the initial nodes have no new keys after increasing the ring size 301 for _, node := range initialRing { 302 for _, ts := range reassignments[node.Address] { 303 foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts) 304 require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node) 305 } 306 } 307 } 308 309 func TestKetamaHashringReplicationConsistency(t *testing.T) { 310 series := makeSeries() 311 312 initialRing := []Endpoint{{Address: "node-1"}, {Address: "node-4"}, {Address: "node-5"}} 313 initialAssignments, err := assignReplicatedSeries(series, initialRing, 2) 314 require.NoError(t, err) 315 316 resizedRing := []Endpoint{{Address: "node-4"}, {Address: "node-3"}, {Address: "node-1"}, {Address: "node-2"}, {Address: "node-5"}} 317 reassignments, err := assignReplicatedSeries(series, resizedRing, 2) 318 require.NoError(t, err) 319 320 // Assert that the initial nodes have no new keys after increasing the ring size 321 for _, node := range initialRing { 322 for _, ts := range reassignments[node.Address] { 323 foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts) 324 require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node) 325 } 326 } 327 } 328 329 func TestKetamaHashringReplicationConsistencyWithAZs(t *testing.T) { 330 for _, tt := range []struct { 331 initialRing []Endpoint 332 resizedRing []Endpoint 333 replicas uint64 334 }{ 335 { 336 initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}}, 337 resizedRing: []Endpoint{{Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "a", AZ: "1"}, {Address: "d", AZ: "2"}, {Address: "e", AZ: "4"}}, 338 replicas: 3, 339 }, 340 { 341 initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}}, 342 resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "d", AZ: "1"}, {Address: "e", AZ: "2"}, {Address: "f", AZ: "3"}}, 343 replicas: 3, 344 }, 345 { 346 initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}}, 347 resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "d", AZ: "4"}, {Address: "e", AZ: "5"}, {Address: "f", AZ: "6"}}, 348 replicas: 3, 349 }, 350 { 351 initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}}, 352 resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "d", AZ: "4"}, {Address: "e", AZ: "5"}, {Address: "f", AZ: "6"}}, 353 replicas: 2, 354 }, 355 { 356 initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "c", AZ: "2"}, {Address: "f", AZ: "3"}}, 357 resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "1"}, {Address: "c", AZ: "2"}, {Address: "d", AZ: "2"}, {Address: "f", AZ: "3"}}, 358 replicas: 2, 359 }, 360 } { 361 t.Run("", func(t *testing.T) { 362 series := makeSeries() 363 364 initialAssignments, err := assignReplicatedSeries(series, tt.initialRing, tt.replicas) 365 require.NoError(t, err) 366 367 reassignments, err := assignReplicatedSeries(series, tt.resizedRing, tt.replicas) 368 require.NoError(t, err) 369 370 // Assert that the initial nodes have no new keys after increasing the ring size 371 for _, node := range tt.initialRing { 372 for _, ts := range reassignments[node.Address] { 373 foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts) 374 require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node) 375 } 376 } 377 }) 378 } 379 } 380 381 func TestKetamaHashringEvenAZSpread(t *testing.T) { 382 tenant := "default-tenant" 383 ts := &prompb.TimeSeries{ 384 Labels: labelpb.ZLabelsFromPromLabels(labels.FromStrings("foo", "bar")), 385 Samples: []prompb.Sample{{Value: 1, Timestamp: 0}}, 386 } 387 388 for _, tt := range []struct { 389 nodes []Endpoint 390 replicas uint64 391 }{ 392 { 393 nodes: []Endpoint{ 394 {Address: "a", AZ: "1"}, 395 {Address: "b", AZ: "2"}, 396 {Address: "c", AZ: "1"}, 397 {Address: "d", AZ: "2"}, 398 }, 399 replicas: 1, 400 }, 401 { 402 nodes: []Endpoint{{Address: "a"}, {Address: "b"}, {Address: "c"}, {Address: "d"}}, 403 replicas: 1, 404 }, 405 { 406 nodes: []Endpoint{ 407 {Address: "a", AZ: "1"}, 408 {Address: "b", AZ: "2"}, 409 {Address: "c", AZ: "1"}, 410 {Address: "d", AZ: "2"}, 411 }, 412 replicas: 2, 413 }, 414 { 415 nodes: []Endpoint{ 416 {Address: "a", AZ: "1"}, 417 {Address: "b", AZ: "2"}, 418 {Address: "c", AZ: "3"}, 419 {Address: "d", AZ: "1"}, 420 {Address: "e", AZ: "2"}, 421 {Address: "f", AZ: "3"}, 422 }, 423 replicas: 3, 424 }, 425 { 426 nodes: []Endpoint{{Address: "a"}, {Address: "b"}, {Address: "c"}, {Address: "d"}, {Address: "e"}, {Address: "f"}, {Address: "g"}}, 427 replicas: 3, 428 }, 429 { 430 nodes: []Endpoint{ 431 {Address: "a", AZ: "1"}, 432 {Address: "b", AZ: "2"}, 433 {Address: "c", AZ: "3"}, 434 {Address: "d", AZ: "1"}, 435 {Address: "e", AZ: "2"}, 436 {Address: "f", AZ: "3"}, 437 {Address: "g", AZ: "4"}, 438 {Address: "h", AZ: "4"}, 439 {Address: "i", AZ: "4"}, 440 {Address: "j", AZ: "5"}, 441 {Address: "k", AZ: "5"}, 442 {Address: "l", AZ: "5"}, 443 }, 444 replicas: 10, 445 }, 446 } { 447 t.Run("", func(t *testing.T) { 448 hashRing, err := newKetamaHashring(tt.nodes, SectionsPerNode, tt.replicas) 449 testutil.Ok(t, err) 450 451 availableAzs := make(map[string]int64) 452 for _, endpoint := range tt.nodes { 453 availableAzs[endpoint.AZ] = 0 454 } 455 456 azSpread := make(map[string]int64) 457 for i := 0; i < int(tt.replicas); i++ { 458 r, err := hashRing.GetN(tenant, ts, uint64(i)) 459 testutil.Ok(t, err) 460 461 for _, n := range tt.nodes { 462 if !strings.HasPrefix(n.Address, r) { 463 continue 464 } 465 azSpread[n.AZ]++ 466 } 467 468 } 469 470 expectedAzSpreadLength := int(tt.replicas) 471 if int(tt.replicas) > len(availableAzs) { 472 expectedAzSpreadLength = len(availableAzs) 473 } 474 testutil.Equals(t, len(azSpread), expectedAzSpreadLength) 475 476 for _, writeToAz := range azSpread { 477 minAz := sizeOfLeastOccupiedAZ(azSpread) 478 testutil.Assert(t, math.Abs(float64(writeToAz-minAz)) <= 1.0) 479 } 480 }) 481 } 482 } 483 484 func TestKetamaHashringEvenNodeSpread(t *testing.T) { 485 tenant := "default-tenant" 486 487 for _, tt := range []struct { 488 nodes []Endpoint 489 replicas uint64 490 numSeries uint64 491 }{ 492 { 493 nodes: []Endpoint{ 494 {Address: "a", AZ: "1"}, 495 {Address: "b", AZ: "2"}, 496 {Address: "c", AZ: "1"}, 497 {Address: "d", AZ: "2"}, 498 }, 499 replicas: 2, 500 numSeries: 1000, 501 }, 502 { 503 nodes: []Endpoint{{Address: "a"}, {Address: "b"}, {Address: "c"}, {Address: "d"}}, 504 replicas: 2, 505 numSeries: 1000, 506 }, 507 { 508 nodes: []Endpoint{ 509 {Address: "a", AZ: "1"}, 510 {Address: "b", AZ: "2"}, 511 {Address: "c", AZ: "3"}, 512 {Address: "d", AZ: "2"}, 513 {Address: "e", AZ: "1"}, 514 {Address: "f", AZ: "3"}, 515 }, 516 replicas: 3, 517 numSeries: 10000, 518 }, 519 { 520 nodes: []Endpoint{ 521 {Address: "a", AZ: "1"}, 522 {Address: "b", AZ: "2"}, 523 {Address: "c", AZ: "3"}, 524 {Address: "d", AZ: "2"}, 525 {Address: "e", AZ: "1"}, 526 {Address: "f", AZ: "3"}, 527 {Address: "g", AZ: "1"}, 528 {Address: "h", AZ: "2"}, 529 {Address: "i", AZ: "3"}, 530 }, 531 replicas: 2, 532 numSeries: 10000, 533 }, 534 { 535 nodes: []Endpoint{ 536 {Address: "a", AZ: "1"}, 537 {Address: "b", AZ: "2"}, 538 {Address: "c", AZ: "3"}, 539 {Address: "d", AZ: "2"}, 540 {Address: "e", AZ: "1"}, 541 {Address: "f", AZ: "3"}, 542 {Address: "g", AZ: "1"}, 543 {Address: "h", AZ: "2"}, 544 {Address: "i", AZ: "3"}, 545 }, 546 replicas: 9, 547 numSeries: 10000, 548 }, 549 } { 550 t.Run("", func(t *testing.T) { 551 hashRing, err := newKetamaHashring(tt.nodes, SectionsPerNode, tt.replicas) 552 testutil.Ok(t, err) 553 optimalSpread := int(tt.numSeries*tt.replicas) / len(tt.nodes) 554 nodeSpread := make(map[string]int) 555 for i := 0; i < int(tt.numSeries); i++ { 556 ts := &prompb.TimeSeries{ 557 Labels: labelpb.ZLabelsFromPromLabels(labels.FromStrings("foo", fmt.Sprintf("%d", i))), 558 Samples: []prompb.Sample{{Value: 1, Timestamp: 0}}, 559 } 560 for j := 0; j < int(tt.replicas); j++ { 561 r, err := hashRing.GetN(tenant, ts, uint64(j)) 562 testutil.Ok(t, err) 563 564 nodeSpread[r]++ 565 } 566 } 567 for _, node := range nodeSpread { 568 diff := math.Abs(float64(node) - float64(optimalSpread)) 569 testutil.Assert(t, diff/float64(optimalSpread) < 0.1) 570 } 571 }) 572 } 573 } 574 575 func TestInvalidAZHashringCfg(t *testing.T) { 576 for _, tt := range []struct { 577 cfg []HashringConfig 578 replicas uint64 579 algorithm HashringAlgorithm 580 expectedError string 581 }{ 582 { 583 cfg: []HashringConfig{{Endpoints: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}}}}, 584 replicas: 2, 585 expectedError: "Hashmod algorithm does not support AZ aware hashring configuration. Either use Ketama or remove AZ configuration.", 586 }, 587 } { 588 t.Run("", func(t *testing.T) { 589 _, err := NewMultiHashring(tt.algorithm, tt.replicas, tt.cfg) 590 require.EqualError(t, err, tt.expectedError) 591 }) 592 } 593 } 594 595 func makeSeries() []prompb.TimeSeries { 596 numSeries := 10000 597 series := make([]prompb.TimeSeries, numSeries) 598 for i := 0; i < numSeries; i++ { 599 series[i] = prompb.TimeSeries{ 600 Labels: []labelpb.ZLabel{ 601 { 602 Name: "pod", 603 Value: fmt.Sprintf("nginx-%d", i), 604 }, 605 }, 606 } 607 } 608 return series 609 } 610 611 func findSeries(initialAssignments map[string][]prompb.TimeSeries, node string, newSeries prompb.TimeSeries) bool { 612 for _, oldSeries := range initialAssignments[node] { 613 l1 := labelpb.ZLabelsToPromLabels(newSeries.Labels) 614 l2 := labelpb.ZLabelsToPromLabels(oldSeries.Labels) 615 if labels.Equal(l1, l2) { 616 return true 617 } 618 } 619 620 return false 621 } 622 623 func assignSeries(series []prompb.TimeSeries, nodes []Endpoint) (map[string][]prompb.TimeSeries, error) { 624 return assignReplicatedSeries(series, nodes, 0) 625 } 626 627 func assignReplicatedSeries(series []prompb.TimeSeries, nodes []Endpoint, replicas uint64) (map[string][]prompb.TimeSeries, error) { 628 hashRing, err := newKetamaHashring(nodes, SectionsPerNode, replicas) 629 if err != nil { 630 return nil, err 631 } 632 assignments := make(map[string][]prompb.TimeSeries) 633 for i := uint64(0); i < replicas; i++ { 634 for _, ts := range series { 635 result, err := hashRing.GetN("tenant", &ts, i) 636 if err != nil { 637 return nil, err 638 } 639 assignments[result] = append(assignments[result], ts) 640 641 } 642 } 643 644 return assignments, nil 645 }