-
Notifications
You must be signed in to change notification settings - Fork 69
/
ring.go
886 lines (736 loc) · 30 KB
/
ring.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
package ring
// Based on https://raw.githubusercontent.com/stathat/consistent/master/consistent.go
import (
"context"
"flag"
"fmt"
"math"
"math/rand"
"sync"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/grafana/dskit/kv"
shardUtil "github.com/grafana/dskit/ring/shard"
"github.com/grafana/dskit/ring/util"
"github.com/grafana/dskit/services"
"github.com/grafana/dskit/flagext"
dsmath "github.com/grafana/dskit/math"
)
const (
unhealthy = "Unhealthy"
// IngesterRingKey is the key under which we store the ingesters ring in the KVStore.
IngesterRingKey = "ring"
// RulerRingKey is the key under which we store the rulers ring in the KVStore.
RulerRingKey = "ring"
// DistributorRingKey is the key under which we store the distributors ring in the KVStore.
DistributorRingKey = "distributor"
// CompactorRingKey is the key under which we store the compactors ring in the KVStore.
CompactorRingKey = "compactor"
// GetBufferSize is the suggested size of buffers passed to Ring.Get(). It's based on
// a typical replication factor 3, plus extra room for a JOINING + LEAVING instance.
GetBufferSize = 5
)
// ReadRing represents the read interface to the ring.
type ReadRing interface {
// Get returns n (or more) instances which form the replicas for the given key.
// bufDescs, bufHosts and bufZones are slices to be overwritten for the return value
// to avoid memory allocation; can be nil, or created with ring.MakeBuffersForGet().
Get(key uint32, op Operation, bufDescs []InstanceDesc, bufHosts, bufZones []string) (ReplicationSet, error)
// GetAllHealthy returns all healthy instances in the ring, for the given operation.
// This function doesn't check if the quorum is honored, so doesn't fail if the number
// of unhealthy instances is greater than the tolerated max unavailable.
GetAllHealthy(op Operation) (ReplicationSet, error)
// GetReplicationSetForOperation returns all instances where the input operation should be executed.
// The resulting ReplicationSet doesn't necessarily contains all healthy instances
// in the ring, but could contain the minimum set of instances required to execute
// the input operation.
GetReplicationSetForOperation(op Operation) (ReplicationSet, error)
ReplicationFactor() int
// InstancesCount returns the number of instances in the ring.
InstancesCount() int
// ShuffleShard returns a subring for the provided identifier (eg. a tenant ID)
// and size (number of instances).
ShuffleShard(identifier string, size int) ReadRing
// GetInstanceState returns the current state of an instance or an error if the
// instance does not exist in the ring.
GetInstanceState(instanceID string) (InstanceState, error)
// ShuffleShardWithLookback is like ShuffleShard() but the returned subring includes
// all instances that have been part of the identifier's shard since "now - lookbackPeriod".
ShuffleShardWithLookback(identifier string, size int, lookbackPeriod time.Duration, now time.Time) ReadRing
// HasInstance returns whether the ring contains an instance matching the provided instanceID.
HasInstance(instanceID string) bool
// CleanupShuffleShardCache should delete cached shuffle-shard subrings for given identifier.
CleanupShuffleShardCache(identifier string)
}
var (
// Write operation that also extends replica set, if instance state is not ACTIVE.
Write = NewOp([]InstanceState{ACTIVE}, func(s InstanceState) bool {
// We do not want to Write to instances that are not ACTIVE, but we do want
// to write the extra replica somewhere. So we increase the size of the set
// of replicas for the key.
// NB unhealthy instances will be filtered later by defaultReplicationStrategy.Filter().
return s != ACTIVE
})
// WriteNoExtend is like Write, but with no replicaset extension.
WriteNoExtend = NewOp([]InstanceState{ACTIVE}, nil)
// Read operation that extends the replica set if an instance is not ACTIVE or LEAVING
Read = NewOp([]InstanceState{ACTIVE, PENDING, LEAVING}, func(s InstanceState) bool {
// To match Write with extended replica set we have to also increase the
// size of the replica set for Read, but we can read from LEAVING ingesters.
return s != ACTIVE && s != LEAVING
})
// Reporting is a special value for inquiring about health.
Reporting = allStatesRingOperation
)
var (
// ErrEmptyRing is the error returned when trying to get an element when nothing has been added to hash.
ErrEmptyRing = errors.New("empty ring")
// ErrInstanceNotFound is the error returned when trying to get information for an instance
// not registered within the ring.
ErrInstanceNotFound = errors.New("instance not found in the ring")
// ErrTooManyUnhealthyInstances is the error returned when there are too many failed instances for a
// specific operation.
ErrTooManyUnhealthyInstances = errors.New("too many unhealthy instances in the ring")
// ErrInconsistentTokensInfo is the error returned if, due to an internal bug, the mapping between
// a token and its own instance is missing or unknown.
ErrInconsistentTokensInfo = errors.New("inconsistent ring tokens information")
)
// Config for a Ring
type Config struct {
KVStore kv.Config `yaml:"kvstore"`
HeartbeatTimeout time.Duration `yaml:"heartbeat_timeout"`
ReplicationFactor int `yaml:"replication_factor"`
ZoneAwarenessEnabled bool `yaml:"zone_awareness_enabled"`
ExcludedZones flagext.StringSliceCSV `yaml:"excluded_zones"`
// Whether the shuffle-sharding subring cache is disabled. This option is set
// internally and never exposed to the user.
SubringCacheDisabled bool `yaml:"-"`
}
// RegisterFlags adds the flags required to config this to the given FlagSet with a specified prefix
func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
cfg.RegisterFlagsWithPrefix("", f)
}
// RegisterFlagsWithPrefix adds the flags required to config this to the given FlagSet with a specified prefix
func (cfg *Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
cfg.KVStore.RegisterFlagsWithPrefix(prefix, "collectors/", f)
f.DurationVar(&cfg.HeartbeatTimeout, prefix+"ring.heartbeat-timeout", time.Minute, "The heartbeat timeout after which ingesters are skipped for reads/writes. 0 = never (timeout disabled).")
f.IntVar(&cfg.ReplicationFactor, prefix+"distributor.replication-factor", 3, "The number of ingesters to write to and read from.")
f.BoolVar(&cfg.ZoneAwarenessEnabled, prefix+"distributor.zone-awareness-enabled", false, "True to enable the zone-awareness and replicate ingested samples across different availability zones.")
f.Var(&cfg.ExcludedZones, prefix+"distributor.excluded-zones", "Comma-separated list of zones to exclude from the ring. Instances in excluded zones will be filtered out from the ring.")
}
type instanceInfo struct {
InstanceID string
Zone string
}
// Ring holds the information about the members of the consistent hash ring.
type Ring struct {
services.Service
key string
cfg Config
KVClient kv.Client
strategy ReplicationStrategy
mtx sync.RWMutex
ringDesc *Desc
ringTokens []uint32
ringTokensByZone map[string][]uint32
// Maps a token with the information of the instance holding it. This map is immutable and
// cannot be chanced in place because it's shared "as is" between subrings (the only way to
// change it is to create a new one and replace it).
ringInstanceByToken map[uint32]instanceInfo
// When did a set of instances change the last time (instance changing state or heartbeat is ignored for this timestamp).
lastTopologyChange time.Time
// List of zones for which there's at least 1 instance in the ring. This list is guaranteed
// to be sorted alphabetically.
ringZones []string
// Cache of shuffle-sharded subrings per identifier. Invalidated when topology changes.
// If set to nil, no caching is done (used by tests, and subrings).
shuffledSubringCache map[subringCacheKey]*Ring
memberOwnershipGaugeVec *prometheus.GaugeVec
numMembersGaugeVec *prometheus.GaugeVec
totalTokensGauge prometheus.Gauge
numTokensGaugeVec *prometheus.GaugeVec
oldestTimestampGaugeVec *prometheus.GaugeVec
logger log.Logger
}
type subringCacheKey struct {
identifier string
shardSize int
}
// New creates a new Ring. Being a service, Ring needs to be started to do anything.
func New(cfg Config, name, key string, logger log.Logger, reg prometheus.Registerer) (*Ring, error) {
codec := GetCodec()
// Suffix all client names with "-ring" to denote this kv client is used by the ring
store, err := kv.NewClient(
cfg.KVStore,
codec,
kv.RegistererWithKVName(reg, name+"-ring"),
logger,
)
if err != nil {
return nil, err
}
return NewWithStoreClientAndStrategy(cfg, name, key, store, NewDefaultReplicationStrategy(), reg, logger)
}
func NewWithStoreClientAndStrategy(cfg Config, name, key string, store kv.Client, strategy ReplicationStrategy, reg prometheus.Registerer, logger log.Logger) (*Ring, error) {
if cfg.ReplicationFactor <= 0 {
return nil, fmt.Errorf("ReplicationFactor must be greater than zero: %d", cfg.ReplicationFactor)
}
r := &Ring{
key: key,
cfg: cfg,
KVClient: store,
strategy: strategy,
ringDesc: &Desc{},
shuffledSubringCache: map[subringCacheKey]*Ring{},
memberOwnershipGaugeVec: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
Name: "ring_member_ownership_percent",
Help: "The percent ownership of the ring by member",
ConstLabels: map[string]string{"name": name}},
[]string{"member"}),
numMembersGaugeVec: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
Name: "ring_members",
Help: "Number of members in the ring",
ConstLabels: map[string]string{"name": name}},
[]string{"state"}),
totalTokensGauge: promauto.With(reg).NewGauge(prometheus.GaugeOpts{
Name: "ring_tokens_total",
Help: "Number of tokens in the ring",
ConstLabels: map[string]string{"name": name}}),
numTokensGaugeVec: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
Name: "ring_tokens_owned",
Help: "The number of tokens in the ring owned by the member",
ConstLabels: map[string]string{"name": name}},
[]string{"member"}),
oldestTimestampGaugeVec: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
Name: "ring_oldest_member_timestamp",
Help: "Timestamp of the oldest member in the ring.",
ConstLabels: map[string]string{"name": name}},
[]string{"state"}),
logger: logger,
}
r.Service = services.NewBasicService(r.starting, r.loop, nil).WithName(fmt.Sprintf("%s ring client", name))
return r, nil
}
func (r *Ring) starting(ctx context.Context) error {
// Get the initial ring state so that, as soon as the service will be running, the in-memory
// ring would be already populated and there's no race condition between when the service is
// running and the WatchKey() callback is called for the first time.
value, err := r.KVClient.Get(ctx, r.key)
if err != nil {
return errors.Wrap(err, "unable to initialise ring state")
}
if value != nil {
r.updateRingState(value.(*Desc))
} else {
level.Info(r.logger).Log("msg", "ring doesn't exist in KV store yet")
}
return nil
}
func (r *Ring) loop(ctx context.Context) error {
// Update the ring metrics at start of the main loop.
r.updateRingMetrics()
go func() {
// Start metrics update ticker to update the ring metrics.
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
r.updateRingMetrics()
case <-ctx.Done():
return
}
}
}()
r.KVClient.WatchKey(ctx, r.key, func(value interface{}) bool {
if value == nil {
level.Info(r.logger).Log("msg", "ring doesn't exist in KV store yet")
return true
}
r.updateRingState(value.(*Desc))
return true
})
return nil
}
func (r *Ring) updateRingState(ringDesc *Desc) {
r.mtx.RLock()
prevRing := r.ringDesc
r.mtx.RUnlock()
// Filter out all instances belonging to excluded zones.
if len(r.cfg.ExcludedZones) > 0 {
for instanceID, instance := range ringDesc.Ingesters {
if util.StringsContain(r.cfg.ExcludedZones, instance.Zone) {
delete(ringDesc.Ingesters, instanceID)
}
}
}
rc := prevRing.RingCompare(ringDesc)
if rc == Equal || rc == EqualButStatesAndTimestamps {
// No need to update tokens or zones. Only states and timestamps
// have changed. (If Equal, nothing has changed, but that doesn't happen
// when watching the ring for updates).
r.mtx.Lock()
r.ringDesc = ringDesc
r.mtx.Unlock()
return
}
now := time.Now()
ringTokens := ringDesc.GetTokens()
ringTokensByZone := ringDesc.getTokensByZone()
ringInstanceByToken := ringDesc.getTokensInfo()
ringZones := getZones(ringTokensByZone)
r.mtx.Lock()
defer r.mtx.Unlock()
r.ringDesc = ringDesc
r.ringTokens = ringTokens
r.ringTokensByZone = ringTokensByZone
r.ringInstanceByToken = ringInstanceByToken
r.ringZones = ringZones
r.lastTopologyChange = now
if r.shuffledSubringCache != nil {
// Invalidate all cached subrings.
r.shuffledSubringCache = make(map[subringCacheKey]*Ring)
}
}
// Get returns n (or more) instances which form the replicas for the given key.
func (r *Ring) Get(key uint32, op Operation, bufDescs []InstanceDesc, bufHosts, bufZones []string) (ReplicationSet, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
if r.ringDesc == nil || len(r.ringTokens) == 0 {
return ReplicationSet{}, ErrEmptyRing
}
var (
n = r.cfg.ReplicationFactor
instances = bufDescs[:0]
start = searchToken(r.ringTokens, key)
iterations = 0
// We use a slice instead of a map because it's faster to search within a
// slice than lookup a map for a very low number of items.
distinctHosts = bufHosts[:0]
distinctZones = bufZones[:0]
)
for i := start; len(distinctHosts) < n && iterations < len(r.ringTokens); i++ {
iterations++
// Wrap i around in the ring.
i %= len(r.ringTokens)
token := r.ringTokens[i]
info, ok := r.ringInstanceByToken[token]
if !ok {
// This should never happen unless a bug in the ring code.
return ReplicationSet{}, ErrInconsistentTokensInfo
}
// We want n *distinct* instances && distinct zones.
if util.StringsContain(distinctHosts, info.InstanceID) {
continue
}
// Ignore if the instances don't have a zone set.
if r.cfg.ZoneAwarenessEnabled && info.Zone != "" {
if util.StringsContain(distinctZones, info.Zone) {
continue
}
}
distinctHosts = append(distinctHosts, info.InstanceID)
instance := r.ringDesc.Ingesters[info.InstanceID]
// Check whether the replica set should be extended given we're including
// this instance.
if op.ShouldExtendReplicaSetOnState(instance.State) {
n++
} else if r.cfg.ZoneAwarenessEnabled && info.Zone != "" {
// We should only add the zone if we are not going to extend,
// as we want to extend the instance in the same AZ.
distinctZones = append(distinctZones, info.Zone)
}
instances = append(instances, instance)
}
healthyInstances, maxFailure, err := r.strategy.Filter(instances, op, r.cfg.ReplicationFactor, r.cfg.HeartbeatTimeout, r.cfg.ZoneAwarenessEnabled)
if err != nil {
return ReplicationSet{}, err
}
return ReplicationSet{
Instances: healthyInstances,
MaxErrors: maxFailure,
}, nil
}
// GetAllHealthy implements ReadRing.
func (r *Ring) GetAllHealthy(op Operation) (ReplicationSet, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
if r.ringDesc == nil || len(r.ringDesc.Ingesters) == 0 {
return ReplicationSet{}, ErrEmptyRing
}
now := time.Now()
instances := make([]InstanceDesc, 0, len(r.ringDesc.Ingesters))
for _, instance := range r.ringDesc.Ingesters {
if r.IsHealthy(&instance, op, now) {
instances = append(instances, instance)
}
}
return ReplicationSet{
Instances: instances,
MaxErrors: 0,
}, nil
}
// GetReplicationSetForOperation implements ReadRing.
func (r *Ring) GetReplicationSetForOperation(op Operation) (ReplicationSet, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
if r.ringDesc == nil || len(r.ringTokens) == 0 {
return ReplicationSet{}, ErrEmptyRing
}
// Build the initial replication set, excluding unhealthy instances.
healthyInstances := make([]InstanceDesc, 0, len(r.ringDesc.Ingesters))
zoneFailures := make(map[string]struct{})
now := time.Now()
for _, instance := range r.ringDesc.Ingesters {
if r.IsHealthy(&instance, op, now) {
healthyInstances = append(healthyInstances, instance)
} else {
zoneFailures[instance.Zone] = struct{}{}
}
}
// Max errors and max unavailable zones are mutually exclusive. We initialise both
// to 0 and then we update them whether zone-awareness is enabled or not.
maxErrors := 0
maxUnavailableZones := 0
if r.cfg.ZoneAwarenessEnabled {
// Given data is replicated to RF different zones, we can tolerate a number of
// RF/2 failing zones. However, we need to protect from the case the ring currently
// contains instances in a number of zones < RF.
numReplicatedZones := dsmath.Min(len(r.ringZones), r.cfg.ReplicationFactor)
minSuccessZones := (numReplicatedZones / 2) + 1
maxUnavailableZones = minSuccessZones - 1
if len(zoneFailures) > maxUnavailableZones {
return ReplicationSet{}, ErrTooManyUnhealthyInstances
}
if len(zoneFailures) > 0 {
// We remove all instances (even healthy ones) from zones with at least
// 1 failing instance. Due to how replication works when zone-awareness is
// enabled (data is replicated to RF different zones), there's no benefit in
// querying healthy instances from "failing zones". A zone is considered
// failed if there is single error.
filteredInstances := make([]InstanceDesc, 0, len(r.ringDesc.Ingesters))
for _, instance := range healthyInstances {
if _, ok := zoneFailures[instance.Zone]; !ok {
filteredInstances = append(filteredInstances, instance)
}
}
healthyInstances = filteredInstances
}
// Since we removed all instances from zones containing at least 1 failing
// instance, we have to decrease the max unavailable zones accordingly.
maxUnavailableZones -= len(zoneFailures)
} else {
// Calculate the number of required instances;
// ensure we always require at least RF-1 when RF=3.
numRequired := len(r.ringDesc.Ingesters)
if numRequired < r.cfg.ReplicationFactor {
numRequired = r.cfg.ReplicationFactor
}
// We can tolerate this many failures
numRequired -= r.cfg.ReplicationFactor / 2
if len(healthyInstances) < numRequired {
return ReplicationSet{}, ErrTooManyUnhealthyInstances
}
maxErrors = len(healthyInstances) - numRequired
}
return ReplicationSet{
Instances: healthyInstances,
MaxErrors: maxErrors,
MaxUnavailableZones: maxUnavailableZones,
}, nil
}
// countTokens returns the number of tokens and tokens within the range for each instance.
// The ring read lock must be already taken when calling this function.
func (r *Ring) countTokens() (map[string]uint32, map[string]uint32) {
owned := map[string]uint32{}
numTokens := map[string]uint32{}
for i, token := range r.ringTokens {
var diff uint32
// Compute how many tokens are within the range.
if i+1 == len(r.ringTokens) {
diff = (math.MaxUint32 - token) + r.ringTokens[0]
} else {
diff = r.ringTokens[i+1] - token
}
info := r.ringInstanceByToken[token]
numTokens[info.InstanceID] = numTokens[info.InstanceID] + 1
owned[info.InstanceID] = owned[info.InstanceID] + diff
}
// Set to 0 the number of owned tokens by instances which don't have tokens yet.
for id := range r.ringDesc.Ingesters {
if _, ok := owned[id]; !ok {
owned[id] = 0
numTokens[id] = 0
}
}
return numTokens, owned
}
// updateRingMetrics updates ring metrics.
func (r *Ring) updateRingMetrics() {
r.mtx.RLock()
defer r.mtx.RUnlock()
numTokens, ownedRange := r.countTokens()
for id, totalOwned := range ownedRange {
r.memberOwnershipGaugeVec.WithLabelValues(id).Set(float64(totalOwned) / float64(math.MaxUint32))
r.numTokensGaugeVec.WithLabelValues(id).Set(float64(numTokens[id]))
}
numByState := map[string]int{}
oldestTimestampByState := map[string]int64{}
// Initialised to zero so we emit zero-metrics (instead of not emitting anything)
for _, s := range []string{unhealthy, ACTIVE.String(), LEAVING.String(), PENDING.String(), JOINING.String()} {
numByState[s] = 0
oldestTimestampByState[s] = 0
}
for _, instance := range r.ringDesc.Ingesters {
s := instance.State.String()
if !r.IsHealthy(&instance, Reporting, time.Now()) {
s = unhealthy
}
numByState[s]++
if oldestTimestampByState[s] == 0 || instance.Timestamp < oldestTimestampByState[s] {
oldestTimestampByState[s] = instance.Timestamp
}
}
for state, count := range numByState {
r.numMembersGaugeVec.WithLabelValues(state).Set(float64(count))
}
for state, timestamp := range oldestTimestampByState {
r.oldestTimestampGaugeVec.WithLabelValues(state).Set(float64(timestamp))
}
r.totalTokensGauge.Set(float64(len(r.ringTokens)))
}
// ShuffleShard returns a subring for the provided identifier (eg. a tenant ID)
// and size (number of instances). The size is expected to be a multiple of the
// number of zones and the returned subring will contain the same number of
// instances per zone as far as there are enough registered instances in the ring.
//
// The algorithm used to build the subring is a shuffle sharder based on probabilistic
// hashing. We treat each zone as a separate ring and pick N unique replicas from each
// zone, walking the ring starting from random but predictable numbers. The random
// generator is initialised with a seed based on the provided identifier.
//
// This implementation guarantees:
//
// - Stability: given the same ring, two invocations returns the same result.
//
// - Consistency: adding/removing 1 instance from the ring generates a resulting
// subring with no more then 1 difference.
//
// - Shuffling: probabilistically, for a large enough cluster each identifier gets a different
// set of instances, with a reduced number of overlapping instances between two identifiers.
func (r *Ring) ShuffleShard(identifier string, size int) ReadRing {
// Nothing to do if the shard size is not smaller then the actual ring.
if size <= 0 || r.InstancesCount() <= size {
return r
}
if cached := r.getCachedShuffledSubring(identifier, size); cached != nil {
return cached
}
result := r.shuffleShard(identifier, size, 0, time.Now())
r.setCachedShuffledSubring(identifier, size, result)
return result
}
// ShuffleShardWithLookback is like ShuffleShard() but the returned subring includes all instances
// that have been part of the identifier's shard since "now - lookbackPeriod".
//
// The returned subring may be unbalanced with regard to zones and should never be used for write
// operations (read only).
//
// This function doesn't support caching.
func (r *Ring) ShuffleShardWithLookback(identifier string, size int, lookbackPeriod time.Duration, now time.Time) ReadRing {
// Nothing to do if the shard size is not smaller then the actual ring.
if size <= 0 || r.InstancesCount() <= size {
return r
}
return r.shuffleShard(identifier, size, lookbackPeriod, now)
}
func (r *Ring) shuffleShard(identifier string, size int, lookbackPeriod time.Duration, now time.Time) *Ring {
lookbackUntil := now.Add(-lookbackPeriod).Unix()
r.mtx.RLock()
defer r.mtx.RUnlock()
var numInstancesPerZone int
var actualZones []string
if r.cfg.ZoneAwarenessEnabled {
numInstancesPerZone = shardUtil.ShuffleShardExpectedInstancesPerZone(size, len(r.ringZones))
actualZones = r.ringZones
} else {
numInstancesPerZone = size
actualZones = []string{""}
}
shard := make(map[string]InstanceDesc, size)
// We need to iterate zones always in the same order to guarantee stability.
for _, zone := range actualZones {
var tokens []uint32
if r.cfg.ZoneAwarenessEnabled {
tokens = r.ringTokensByZone[zone]
} else {
// When zone-awareness is disabled, we just iterate over 1 single fake zone
// and use all tokens in the ring.
tokens = r.ringTokens
}
// Initialise the random generator used to select instances in the ring.
// Since we consider each zone like an independent ring, we have to use dedicated
// pseudo-random generator for each zone, in order to guarantee the "consistency"
// property when the shard size changes or a new zone is added.
random := rand.New(rand.NewSource(shardUtil.ShuffleShardSeed(identifier, zone)))
// To select one more instance while guaranteeing the "consistency" property,
// we do pick a random value from the generator and resolve uniqueness collisions
// (if any) continuing walking the ring.
for i := 0; i < numInstancesPerZone; i++ {
start := searchToken(tokens, random.Uint32())
iterations := 0
found := false
for p := start; iterations < len(tokens); p++ {
iterations++
// Wrap p around in the ring.
p %= len(tokens)
info, ok := r.ringInstanceByToken[tokens[p]]
if !ok {
// This should never happen unless a bug in the ring code.
panic(ErrInconsistentTokensInfo)
}
// Ensure we select an unique instance.
if _, ok := shard[info.InstanceID]; ok {
continue
}
instanceID := info.InstanceID
instance := r.ringDesc.Ingesters[instanceID]
shard[instanceID] = instance
// If the lookback is enabled and this instance has been registered within the lookback period
// then we should include it in the subring but continuing selecting instances.
if lookbackPeriod > 0 && instance.RegisteredTimestamp >= lookbackUntil {
continue
}
found = true
break
}
// If one more instance has not been found, we can stop looking for
// more instances in this zone, because it means the zone has no more
// instances which haven't been already selected.
if !found {
break
}
}
}
// Build a read-only ring for the shard.
shardDesc := &Desc{Ingesters: shard}
shardTokensByZone := shardDesc.getTokensByZone()
return &Ring{
cfg: r.cfg,
strategy: r.strategy,
ringDesc: shardDesc,
ringTokens: shardDesc.GetTokens(),
ringTokensByZone: shardTokensByZone,
ringZones: getZones(shardTokensByZone),
// We reference the original map as is in order to avoid copying. It's safe to do
// because this map is immutable by design and it's a superset of the actual instances
// with the subring.
ringInstanceByToken: r.ringInstanceByToken,
// For caching to work, remember these values.
lastTopologyChange: r.lastTopologyChange,
}
}
// GetInstanceState returns the current state of an instance or an error if the
// instance does not exist in the ring.
func (r *Ring) GetInstanceState(instanceID string) (InstanceState, error) {
r.mtx.RLock()
defer r.mtx.RUnlock()
instances := r.ringDesc.GetIngesters()
instance, ok := instances[instanceID]
if !ok {
return PENDING, ErrInstanceNotFound
}
return instance.GetState(), nil
}
// HasInstance returns whether the ring contains an instance matching the provided instanceID.
func (r *Ring) HasInstance(instanceID string) bool {
r.mtx.RLock()
defer r.mtx.RUnlock()
instances := r.ringDesc.GetIngesters()
_, ok := instances[instanceID]
return ok
}
func (r *Ring) getCachedShuffledSubring(identifier string, size int) *Ring {
if r.cfg.SubringCacheDisabled {
return nil
}
r.mtx.RLock()
defer r.mtx.RUnlock()
// if shuffledSubringCache map is nil, reading it returns default value (nil pointer).
cached := r.shuffledSubringCache[subringCacheKey{identifier: identifier, shardSize: size}]
if cached == nil {
return nil
}
cached.mtx.Lock()
defer cached.mtx.Unlock()
// Update instance states and timestamps. We know that the topology is the same,
// so zones and tokens are equal.
for name, cachedIng := range cached.ringDesc.Ingesters {
ing := r.ringDesc.Ingesters[name]
cachedIng.State = ing.State
cachedIng.Timestamp = ing.Timestamp
cached.ringDesc.Ingesters[name] = cachedIng
}
return cached
}
func (r *Ring) setCachedShuffledSubring(identifier string, size int, subring *Ring) {
if subring == nil || r.cfg.SubringCacheDisabled {
return
}
r.mtx.Lock()
defer r.mtx.Unlock()
// Only cache if *this* ring hasn't changed since computing result
// (which can happen between releasing the read lock and getting read-write lock).
// Note that shuffledSubringCache can be only nil when set by test.
if r.shuffledSubringCache != nil && r.lastTopologyChange.Equal(subring.lastTopologyChange) {
r.shuffledSubringCache[subringCacheKey{identifier: identifier, shardSize: size}] = subring
}
}
func (r *Ring) CleanupShuffleShardCache(identifier string) {
if r.cfg.SubringCacheDisabled {
return
}
r.mtx.Lock()
defer r.mtx.Unlock()
for k := range r.shuffledSubringCache {
if k.identifier == identifier {
delete(r.shuffledSubringCache, k)
}
}
}
// Operation describes which instances can be included in the replica set, based on their state.
//
// Implemented as bitmap, with upper 16-bits used for encoding extendReplicaSet, and lower 16-bits used for encoding healthy states.
type Operation uint32
// NewOp constructs new Operation with given "healthy" states for operation, and optional function to extend replica set.
// Result of calling shouldExtendReplicaSet is cached.
func NewOp(healthyStates []InstanceState, shouldExtendReplicaSet func(s InstanceState) bool) Operation {
op := Operation(0)
for _, s := range healthyStates {
op |= (1 << s)
}
if shouldExtendReplicaSet != nil {
for _, s := range []InstanceState{ACTIVE, LEAVING, PENDING, JOINING, LEAVING, LEFT} {
if shouldExtendReplicaSet(s) {
op |= (0x10000 << s)
}
}
}
return op
}
// IsInstanceInStateHealthy is used during "filtering" phase to remove undesired instances based on their state.
func (op Operation) IsInstanceInStateHealthy(s InstanceState) bool {
return op&(1<<s) > 0
}
// ShouldExtendReplicaSetOnState returns true if given a state of instance that's going to be
// added to the replica set, the replica set size should be extended by 1
// more instance for the given operation.
func (op Operation) ShouldExtendReplicaSetOnState(s InstanceState) bool {
return op&(0x10000<<s) > 0
}
// All states are healthy, no states extend replica set.
var allStatesRingOperation = Operation(0x0000ffff)