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

src/histogram: Make Histogram::observe atomic across collects #314

Merged
merged 14 commits into from Jul 14, 2020

Conversation

mxinden
Copy link
Contributor

@mxinden mxinden commented Apr 5, 2020

Motivation

A histogram supports two main execution paths:

  1. observe which increases the overall observation counter, updates the observation sum and increases a single bucket counter.

  2. collect which snapshots the state of the histogram and exposes it as a Protobuf struct.

If an observe and a collect operation interleave, the latter could be exposing a snapshot of the histogram that does not uphold all histogram invariants. For example for the invariant that the overall observation counter should equal the sum of all bucket counters: Say that an observe increases the overall counter but before updating a specific bucket counter a collect operation snapshots the histogram.

The above race condition has been solved in the Golang Prometheus client with prometheus/client_golang#457 by introducing the notion of shards, one hot shard for observe operations to record their observation and one cold shard for collect operations to collect a consistent snapshot of the histogram.

observe operations hit the hot shard and record their observation. Collect operations switch hot and cold, wait for all observe calls to finish on the previously hot now cold shard and then expose the consistent snapshot.

This pull request ports prometheus/client_golang#457 to the Rust Prometheus client.

Content of the pull request

The pull request contains three commits:

  • src/histogram: Add test ensuring Histogram::observe is atomic

    Showcasing the above race condition in the current imlementation.

  • src/{histogram,atomic64}: Make Histogram::observe atomic across collects

    Porting Lock-free atomic observations in Histograms! prometheus/client_golang#457 to fix the race
    condition.

  • benches/histogram: Add benchmark for concurrent observe and collect

    Adding a benchmark to show the impact of the patch. While the benchmark does not show a performance impact through the patch on my laptop, I am happy to test this more thoroughly on a larger machine (128 cores) in case there is general interest to accept this patch.

Greater picture

Fixing this race condition is especially attractive now that with Prometheus v2.17.0 the isolation level has been increased (See changelog entry below and prometheus/prometheus#6841).

This release implements isolation in TSDB. API queries and recording rules are
guaranteed to only see full scrapes and full recording rules.

Trade-off

While this pull request fixes the above described race condition it does increase complexity:

  • Introduction of the notion of shards.

  • The observe code path is mostly untouched other than one additional atomic operation and increased Ordering levels and thus stays lock-free.

  • collect operations need to happen sequentially (enforced through a single Mutex). A single collect and multiple observe operations can still operate concurrently. Given that the collect operation should happen rarely (> 1s) this should not introduce a performance impact.

  • In order to coordinate hot and cold shards the 64 bit histogram counter is split into a 1 bit shard index and 63 bit counter. Thus the amount of observations a histogram can record is divided by two. While this might sound like an issue, one could still record one observation per milisecond for 292_277_266 years.


I hope the above description makes sense. Let me know if this is something you are willing to accept into master.

Thanks a bunch for maintaining this library!

@mxinden mxinden changed the title src/{histogram,atomic64}: Make Histogram::observe atomic across collects src/histogram: Make Histogram::observe atomic across collects Apr 5, 2020
If an observe and a collect operation interleave, the latter should not
expose a snapshot of the histogram that does not uphold all histogram
invariants. For example for the invariant that the overall observation
counter should equal the sum of all bucket counters: Say that an
`observe` increases the overall counter but before updating a specific
bucket counter a collect operation snapshots the histogram.

This commits adds a basic unit test to test that the above is not
happening.

Signed-off-by: Max Inden <mail@max-inden.de>
A histogram supports two main execution paths:

1. `observe` which increases the overall observation counter, updates
the observation sum and increases a single bucket counter.

2. `proto` (aka. collecting the metric, from now on referred to as the
collect operation) which snapshots the state of the histogram and
exposes it as a Protobuf struct.

If an observe and a collect operation interleave, the latter could be
exposing a snapshot of the histogram that does not uphold all histogram
invariants. For example for the invariant that the overall observation
counter should equal the sum of all bucket counters: Say that an
`observe` increases the overall counter but before updating a specific
bucket counter a collect operation snapshots the histogram.

This commits adjusts the `HistogramCore` implementation to make such
race conditions impossible. It introduces the notion of shards, one hot
shard for `observe` operations to record their observation and one cold
shard for collect operations to collect a consistent snapshot of the
histogram.

`observe` operations hit the hot shard and record their observation.
Collect operations switch hot and cold, wait for all `observe` calls to
finish on the previously hot now cold shard and then expose the
consistent snapshot.

Signed-off-by: Max Inden <mail@max-inden.de>
Add a basic benchmark test which spawns 4 threads in the background
continuously calling `observe` 1_000 times and then `collect`. At the
same time call `observe` within the `Bencher::iter` closure to measure
impact of background threads on `observe` call.

Signed-off-by: Max Inden <mail@max-inden.de>
Signed-off-by: Max Inden <mail@max-inden.de>
Signed-off-by: Max Inden <mail@max-inden.de>
@lucab
Copy link
Member

lucab commented Apr 22, 2020

@BusyJay @breeswish can you have a look at this PR?

I was chatting with @mxinden trying to figure out a better approach instead of Mutex<()>, but we ran out of smarter alternatives. Do you have any feedback on this? Or perhaps is this fine?

@breezewish
Copy link
Member

Sorry I missed this PR .. 🧐 @BusyJay Do you have suggestions about the Mutex in this PR?

@BusyJay
Copy link
Member

BusyJay commented May 11, 2020

No, the mutex seems simple and reasonable.

Rusts drop semantics can be confusing sometimes. E.g. `let _ = l.lock()`
would drop the lock guard immediately whereas `let _guard = l.lock()`
would drop the guard in LIFO order at the end of the current scope.

Instead of relying on the above guarantee with `let _guard`, drop the
mutex guard explicitely hopefully making this less error prone in the
future.

Signed-off-by: Max Inden <mail@max-inden.de>
src/histogram.rs Outdated Show resolved Hide resolved
Signed-off-by: Max Inden <mail@max-inden.de>
@mxinden
Copy link
Contributor Author

mxinden commented Jun 22, 2020

@lucab @breeswish @BusyJay any objections in regards to this pull request? If not, would one of you mind approving it?

src/atomic64.rs Outdated Show resolved Hide resolved
src/atomic64.rs Outdated Show resolved Hide resolved
src/histogram.rs Outdated Show resolved Hide resolved
src/histogram.rs Outdated Show resolved Hide resolved
@mxinden
Copy link
Contributor Author

mxinden commented Jul 14, 2020

Thanks for the review @lucab. Would you mind taking another look?

@lucab lucab merged commit 4fdff69 into tikv:master Jul 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants