-
Notifications
You must be signed in to change notification settings - Fork 820
Enabling availability zone awareness in metric R/W with ingesters. #2317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5d34fac
43de699
04939c3
e35561d
10fbf0a
fb2f78b
a978fd4
ccd0aa3
60722a2
00fe153
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--- | ||
title: "Ingester Hand-over" | ||
linkTitle: "Ingester Hand-over" | ||
weight: 5 | ||
slug: ingester-handover | ||
--- | ||
|
||
In a default configuration, time-series written to ingesters are replicated based on the container/pod name of the ingester instances. It is completely possible that all the replicas for the given time-series are held with in the same availability zone, even if the cortex infrastructure spans multiple zones within the region. Storing multiple replicas for a given time-series poses a risk for data loss if there is an outage affecting various nodes within a zone or a total outage. | ||
|
||
## Configuration | ||
|
||
Cortex can be configured to consider an availability zone value in its replication system. Doing so mitigates risks associated with losing multiple nodes with in the same availability zone. The availability zone for an ingester can be defined on the command line of the ingester using the `ingester.availability-zone` flag or using the yaml configuration: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @khaines |
||
|
||
```yaml | ||
ingester: | ||
lifecycler: | ||
availability_zone: "zone-3" | ||
``` | ||
|
||
## Zone Replication Considerations | ||
|
||
Enabling availability zone awareness helps mitigate risks regarding data loss within a single zone, some items need consideration by an operator if they are thinking of enabling this feature. | ||
|
||
### Minimum number of Zones | ||
|
||
For cortex to function correctly, there must be at least the same number of availability zones as there is replica count. So by default, a cortex cluster should be spread over 3 zones as the default replica count is 3. It is safe to have more zones than the replica count, but it cannot be less. Having fewer availability zones than replica count causes a replica write to be missed, and in some cases, the write fails if the availability zone count is too low. | ||
|
||
### Cost | ||
|
||
Depending on the existing cortex infrastructure being used, this may cause an increase in running costs as most cloud providers charge for cross availability zone traffic. The most significant change would be for a cortex cluster currently running in a singular zone. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,6 +55,7 @@ type LifecyclerConfig struct { | |
InfNames []string `yaml:"interface_names"` | ||
FinalSleep time.Duration `yaml:"final_sleep"` | ||
TokensFilePath string `yaml:"tokens_file_path"` | ||
Zone string `yaml:"availability_zone"` | ||
|
||
// For testing, you can override the address and ID of this ingester | ||
Addr string `yaml:"address" doc:"hidden"` | ||
|
@@ -103,6 +104,7 @@ func (cfg *LifecyclerConfig) RegisterFlagsWithPrefix(prefix string, f *flag.Flag | |
f.StringVar(&cfg.Addr, prefix+"lifecycler.addr", "", "IP address to advertise in consul.") | ||
f.IntVar(&cfg.Port, prefix+"lifecycler.port", 0, "port to advertise in consul (defaults to server.grpc-listen-port).") | ||
f.StringVar(&cfg.ID, prefix+"lifecycler.ID", hostname, "ID to register into consul.") | ||
f.StringVar(&cfg.Zone, prefix+"availability-zone", "", "The availability zone of the host, this instance is running on. Default is the lifecycler ID.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @khaines There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made an edit to this with #2384 |
||
} | ||
|
||
// Lifecycler is responsible for managing the lifecycle of entries in the ring. | ||
|
@@ -120,6 +122,7 @@ type Lifecycler struct { | |
Addr string | ||
RingName string | ||
RingKey string | ||
Zone string | ||
|
||
// Whether to flush if transfer fails on shutdown. | ||
flushOnShutdown bool | ||
|
@@ -160,6 +163,11 @@ func NewLifecycler(cfg LifecyclerConfig, flushTransferer FlushTransferer, ringNa | |
return nil, err | ||
} | ||
|
||
zone := cfg.Zone | ||
if zone == "" { | ||
zone = cfg.ID | ||
} | ||
|
||
// We do allow a nil FlushTransferer, but to keep the ring logic easier we assume | ||
// it's always set, so we use a noop FlushTransferer | ||
if flushTransferer == nil { | ||
|
@@ -176,6 +184,7 @@ func NewLifecycler(cfg LifecyclerConfig, flushTransferer FlushTransferer, ringNa | |
RingName: ringName, | ||
RingKey: ringKey, | ||
flushOnShutdown: flushOnShutdown, | ||
Zone: zone, | ||
|
||
actorChan: make(chan func()), | ||
|
||
|
@@ -502,14 +511,14 @@ func (i *Lifecycler) initRing(ctx context.Context) error { | |
if len(tokensFromFile) >= i.cfg.NumTokens { | ||
i.setState(ACTIVE) | ||
} | ||
ringDesc.AddIngester(i.ID, i.Addr, tokensFromFile, i.GetState()) | ||
ringDesc.AddIngester(i.ID, i.Addr, i.Zone, tokensFromFile, i.GetState()) | ||
i.setTokens(tokensFromFile) | ||
return ringDesc, true, nil | ||
} | ||
|
||
// Either we are a new ingester, or consul must have restarted | ||
level.Info(util.Logger).Log("msg", "instance not found in ring, adding with no tokens", "ring", i.RingName) | ||
ringDesc.AddIngester(i.ID, i.Addr, []uint32{}, i.GetState()) | ||
ringDesc.AddIngester(i.ID, i.Addr, i.Zone, []uint32{}, i.GetState()) | ||
return ringDesc, true, nil | ||
} | ||
|
||
|
@@ -564,7 +573,7 @@ func (i *Lifecycler) verifyTokens(ctx context.Context) bool { | |
ringTokens = append(ringTokens, newTokens...) | ||
sort.Sort(ringTokens) | ||
|
||
ringDesc.AddIngester(i.ID, i.Addr, ringTokens, i.GetState()) | ||
ringDesc.AddIngester(i.ID, i.Addr, i.Zone, ringTokens, i.GetState()) | ||
|
||
i.setTokens(ringTokens) | ||
|
||
|
@@ -626,7 +635,7 @@ func (i *Lifecycler) autoJoin(ctx context.Context, targetState IngesterState) er | |
sort.Sort(myTokens) | ||
i.setTokens(myTokens) | ||
|
||
ringDesc.AddIngester(i.ID, i.Addr, i.getTokens(), i.GetState()) | ||
ringDesc.AddIngester(i.ID, i.Addr, i.Zone, i.getTokens(), i.GetState()) | ||
|
||
return ringDesc, true, nil | ||
}) | ||
|
@@ -655,7 +664,7 @@ func (i *Lifecycler) updateConsul(ctx context.Context) error { | |
if !ok { | ||
// consul must have restarted | ||
level.Info(util.Logger).Log("msg", "found empty ring, inserting tokens", "ring", i.RingName) | ||
ringDesc.AddIngester(i.ID, i.Addr, i.getTokens(), i.GetState()) | ||
ringDesc.AddIngester(i.ID, i.Addr, i.Zone, i.getTokens(), i.GetState()) | ||
} else { | ||
ingesterDesc.Timestamp = time.Now().Unix() | ||
ingesterDesc.State = i.GetState() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@khaines This comes from a bad copy-paste. May you submit a PR to fix it, please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes it did... looked at it a few times and this didn't catch my eye.