Skip to content
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

Auto-scale DynamoDB provision based on Prometheus metrics #841

Merged
merged 33 commits into from
Aug 7, 2018

Conversation

bboreham
Copy link
Contributor

@bboreham bboreham commented Jun 7, 2018

Fixes #735

To use this feature, set -dynamodb....scale.enabled on in command-line args but do not supply an AWS autoscaler -applicationautoscaling.url.
Also set -metrics.url to point at a Prometheus API-compatible server which will serve the metrics.

(at the time of writing this can't be a multi-tenant server because I didn't put in an option for the tenant)

PR also includes much refactoring of existing tests because I couldn't stand so much repetition.

Outstanding things I should do:

  • allow the parameters like queueLengthAcceptable to be changed
  • use the min/max capacity if configured
  • implement out-cooldown and in-cooldown
  • scale last week's table to zero even though this week's may be queuing
  • protect against scaling-down when the ingesters hiccup (e.g. all crash at once)
  • protect against scaling-down when Prometheus restarts and has no data

For extra credit:

  • expose some metrics - e.g. requested throughput
  • more aggressive scale-up when the queue is really big
  • look at the recent rate of usage when scaling down instead of a percentage
  • automatically increase capacity when an ingester rollout is under way (or fix Mysterious flush of underutilised chunks 1hr after ingester rollout #467)
  • support use of Cortex to supply the metrics

@bboreham bboreham force-pushed the dynamic-dynamodb branch from b7990d1 to 8497e4f Compare June 8, 2018 13:59
@bboreham
Copy link
Contributor Author

At the weekly table changeover (which happens Wednesday night for me) it initially does a scale-down of the new table, because the error rate is zero.

Having thought about it a bit, I think this is best addressed by setting the minimum write capacity to a level which is enough to handle the expected traffic at midnight. I like the idea that the core algorithm should operate without knowing anything specific about the individual tables.

@bboreham
Copy link
Contributor Author

Scaling up by a fixed fraction of current provision isn't very good.
At the low end it doesn't give you very much, while at the high end it gets expensive.
I find it hard to come up with a formula based on queue growth - it doesn't give information about which tables need most.

As a simple improvement I might floor the increment to a fraction of the max, e.g. if max is 10K then min increment is 1K.

func chunkTagsToDynamoDB(ts chunk.Tags) []*dynamodb.Tag {
if ts == nil {
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this redundant? Looks like result will be nil if len(ts) == 0 (or ts is nill)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes; it's cut/pasted from what was there before

err = d.disableAutoScaling(ctx, expected)
} else if current.WriteScale != expected.WriteScale {
err = d.enableAutoScaling(ctx, expected)
if expected.WriteScale.Enabled && d.metrics != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the check expected.WriteScale.Enabled && d.metrics != nil should be put in the constructor so we can fail early? Then we can just check expected.WriteScale.Enabled here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, in what situation is metrics nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metrics is nil when you want AWS auto-scaling

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how - must be missing something. Would you mind explaining a little further for me?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plus or minus a bug...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the bug.

@@ -319,7 +353,11 @@ func (d dynamoTableClient) UpdateTable(ctx context.Context, current, expected ch
return err
})
}); err != nil {
return err
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "LimitExceededException" {
level.Warn(util.Logger).Log("msg", "update limit exceeded", "err", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps export a counter for this too?

if err != nil {
return nil, err
}
promAPI = promV1.NewAPI(client)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read this right, this will return a non-functioning but non-nil metricsData if the addr is empty? That seems odd to me; I'd expect either an error or a fully-functioningmetricsData - perhaps this logic should be pushed up to the called?

}

func extractRates(matrix model.Matrix) (map[string]float64, error) {
ret := make(map[string]float64)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC throughout the codebase we tend to prefer map[string]float64, and only use make when we know the size.

}

// fetch write error rate per DynamoDB table
deMatrix, err := promQuery(ctx, m.promAPI, `sum(rate(cortex_dynamo_failures_total{error="ProvisionedThroughputExceededException",operation=~".*Write.*"}[1m])) by (table) > 0`, 0, time.Second)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be be able to inject extra selectors into these queries, for instance we run multiple cortex cluster monitored by the same prometheus.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made the whole promql expression configurable, on the basis that other things like rate periods might need tweaking too.

return chunk.TableDesc{
Name: name,
}, dynamodb.TableStatusActive, nil
}, true, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

f.Int64Var(&cfg.OutCooldown, argPrefix+".out-cooldown", 3000, "DynamoDB minimum time between each autoscaling event that increases provision capacity.")
f.Int64Var(&cfg.InCooldown, argPrefix+".in-cooldown", 3000, "DynamoDB minimum time between each autoscaling event that decreases provision capacity.")
f.Int64Var(&cfg.OutCooldown, argPrefix+".out-cooldown", 1800, "DynamoDB minimum seconds between each autoscale up.")
f.Int64Var(&cfg.InCooldown, argPrefix+".in-cooldown", 1800, "DynamoDB minimum seconds between each autoscale down.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All usages of this are as a time.Duration, so perhaps we should make this a DurationVar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, but not backwards-compatible. I guess we could add two new flags and deprecate the old.

})
}
return result
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

queueLengths []float64
errorRates map[string]float64
usageRates map[string]float64
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is quite a lot of coupling between metricsData and dynamoTableClient; I wonder if more work needs to be done to tease out a clean interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think much of this was simply the metrics methods were on the wrong struct, but I went ahead and created an abstract interface so the choice between old-style and new-style is in the constructor. Let me know what you think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now!

@tomwilkie
Copy link
Contributor

A few minor nits, main concern is about coupling between the metricsData and tableClient. I wonder if a better interface could be teased out?

}
}

func newMetrics(cfg DynamoDBConfig) (*metricsData, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably belongs at the top of the file now?

}, []string{"operation", "status_code"})
// Pluggable auto-scaler implementation
type autoscale interface {
CreateTable(ctx context.Context, desc chunk.TableDesc) error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really PostCreateTable, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I was wrestling with how similar it is to the higher-level interface

@bboreham
Copy link
Contributor Author

Scaling up by a fixed fraction of current provision isn't very good.
At the low end it doesn't give you very much

If the current provision is 1, it's very bad.

@tomwilkie
Copy link
Contributor

LGTM!

@bboreham bboreham merged commit 7007f01 into master Aug 7, 2018
@tomwilkie tomwilkie deleted the dynamic-dynamodb branch August 29, 2018 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants