1
0
mirror of https://github.com/facebook/rocksdb.git synced 2026-10-23 08:09:02 +03:00
Files
rocksdb/monitoring/iostats_context.cc
Josh Kang 7247edbf0a Add opt-in stats for async RocksDB reads (#15017)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15017

## Summary

RocksDB async `GetAsync` and `MultiGetAsync` callbacks now default to a completion-only path. Callbacks can override `EnableStats()` when they need request-specific `PerfContext` or `IOStatsContext`; otherwise RocksDB avoids creating a coroutine stats context and returns null stats pointers. Callbacks must not initiate another async read from `OnComplete()`.

RocksDB stats normally use thread-local storage, which does not compose with interleaved coroutine reads. A stats-enabled request captures only the caller's configuration: perf level, per-level perf enablement, and whether io stats are disabled. It then starts with empty request contexts. `onSet()` loads that request's contexts into TLS, while `onUnset()` saves the accumulated contexts back into request data. On resume, the same contexts are loaded again. This isolates request counters without preserving or restoring pre-existing executor TLS state. The synchronous fallback similarly resets TLS before executing a stats-enabled read.

`PerfContext` and `IOStatsContext` gain `Merge()` support for caller-side aggregation. `PerfContext` move construction and assignment now transfer ownership of per-level state rather than deep-copying it, avoiding allocations during coroutine context handoffs. A fbcode-wide audit found no caller that reads a moved-from `PerfContext`.

`db_bench` opts in only for coroutine jobs that print per-job perf context and merges every completed request's callback stats into the job aggregate.

## Example: Two Async Gets on One Thread

Both callbacks enable stats. Each request starts with empty counters and carries its contexts across suspensions.

```text
Shared executor thread
|
|  onSet(A): load A's empty contexts into TLS
|  A records 3 reads
|  onUnset(A): save TLS contexts back into A
|
|  onSet(B): load B's empty contexts into TLS
|  B records 4 reads
|  onUnset(B): save TLS contexts back into B
|
|  onSet(A): load A's contexts containing 3 reads
|  A records 2 more reads
|  callback A receives block_read_count = 5
|
|  onSet(B): load B's contexts containing 4 reads
|  callback B receives block_read_count = 4
```

Isolation comes from replacing TLS with the active request's saved contexts on every `onSet()`.

### Timers

Existing wall-clock timers remain in coroutine frames, so timers spanning `co_await` include suspended time and measure end-to-end latency.

Request-level `get_cpu_nanos` is suspension-aware: it starts in `onSet()` and stops in `onUnset()` or `Finalize()`, charging only time while the request is active.

Nested CPU timers remain disabled because normal RAII CPU timers would also charge executor work performed while their coroutine is suspended. `MultiGetAsync` fanout additionally requires per-child timer state to avoid double counting.

## Benchmarks

Benchmarked to confirm no performance regressions when perf is disabled, and also to see the overhead of enabling perf.

```bash
db_bench --benchmarks=<readrandomcoroutine|multireadrandomcoroutine> --db=<db_path> --coro_jobs_per_thread=10 --use_existing_db=true --num=2000000 --value_size=1024 --compression_type=none --threads=8 --batch_size=16 --multiread_batched=true --use_direct_reads=true --cache_size=1428076626 --reads=320000 --perf_level=<1|3> --seed=<seed>
```

| Workload | Before change | Perf level 1 | Before -> 1 | Perf level 3 | 1 -> 3 |
| -------- | ------------- | ------------ | ----------- | ------------ | ------ |
| Async Get | 475.6K ops/s | 476.7K ops/s | +0.2% | 470.5K ops/s | -1.3% |
| Async MultiGet | 567.3K ops/s | 567.4K ops/s | +0.0% | 552.2K ops/s | -2.7% |

Reviewed By: xingbowang

Differential Revision: D112634874

fbshipit-source-id: 94e240aeb1496fa12dac995b5aa5a9c3543cc5fc
2026-07-28 09:32:26 -07:00

122 lines
4.3 KiB
C++

// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <sstream>
#include "monitoring/iostats_context_imp.h"
#include "rocksdb/env.h"
namespace ROCKSDB_NAMESPACE {
#ifdef NIOSTATS_CONTEXT
// Should not be used because the counters are not thread-safe.
// Put here just to make get_iostats_context() simple without ifdef.
static IOStatsContext iostats_context;
#else
thread_local IOStatsContext iostats_context;
#endif
IOStatsContext* get_iostats_context() { return &iostats_context; }
void FileIOByTemperature::Merge(const FileIOByTemperature& other) {
#ifndef NIOSTATS_CONTEXT
hot_file_bytes_read += other.hot_file_bytes_read;
warm_file_bytes_read += other.warm_file_bytes_read;
cool_file_bytes_read += other.cool_file_bytes_read;
cold_file_bytes_read += other.cold_file_bytes_read;
ice_file_bytes_read += other.ice_file_bytes_read;
unknown_non_last_level_bytes_read += other.unknown_non_last_level_bytes_read;
unknown_last_level_bytes_read += other.unknown_last_level_bytes_read;
hot_file_read_count += other.hot_file_read_count;
warm_file_read_count += other.warm_file_read_count;
cool_file_read_count += other.cool_file_read_count;
cold_file_read_count += other.cold_file_read_count;
ice_file_read_count += other.ice_file_read_count;
unknown_non_last_level_read_count += other.unknown_non_last_level_read_count;
unknown_last_level_read_count += other.unknown_last_level_read_count;
#else
(void)other;
#endif
}
void IOStatsContext::Reset() {
#ifndef NIOSTATS_CONTEXT
thread_pool_id = Env::Priority::TOTAL;
bytes_read = 0;
bytes_written = 0;
open_nanos = 0;
allocate_nanos = 0;
write_nanos = 0;
read_nanos = 0;
range_sync_nanos = 0;
prepare_write_nanos = 0;
fsync_nanos = 0;
logger_nanos = 0;
cpu_write_nanos = 0;
cpu_read_nanos = 0;
file_io_stats_by_temperature.Reset();
#endif //! NIOSTATS_CONTEXT
}
void IOStatsContext::Merge(const IOStatsContext& other) {
#ifndef NIOSTATS_CONTEXT
bytes_read += other.bytes_read;
bytes_written += other.bytes_written;
open_nanos += other.open_nanos;
allocate_nanos += other.allocate_nanos;
write_nanos += other.write_nanos;
read_nanos += other.read_nanos;
range_sync_nanos += other.range_sync_nanos;
prepare_write_nanos += other.prepare_write_nanos;
fsync_nanos += other.fsync_nanos;
logger_nanos += other.logger_nanos;
cpu_write_nanos += other.cpu_write_nanos;
cpu_read_nanos += other.cpu_read_nanos;
file_io_stats_by_temperature.Merge(other.file_io_stats_by_temperature);
#else
(void)other;
#endif
}
#define IOSTATS_CONTEXT_OUTPUT(counter) \
if (!exclude_zero_counters || counter > 0) { \
ss << #counter << " = " << counter << ", "; \
}
std::string IOStatsContext::ToString(bool exclude_zero_counters) const {
#ifdef NIOSTATS_CONTEXT
(void)exclude_zero_counters;
return "";
#else
std::ostringstream ss;
IOSTATS_CONTEXT_OUTPUT(thread_pool_id);
IOSTATS_CONTEXT_OUTPUT(bytes_read);
IOSTATS_CONTEXT_OUTPUT(bytes_written);
IOSTATS_CONTEXT_OUTPUT(open_nanos);
IOSTATS_CONTEXT_OUTPUT(allocate_nanos);
IOSTATS_CONTEXT_OUTPUT(write_nanos);
IOSTATS_CONTEXT_OUTPUT(read_nanos);
IOSTATS_CONTEXT_OUTPUT(range_sync_nanos);
IOSTATS_CONTEXT_OUTPUT(fsync_nanos);
IOSTATS_CONTEXT_OUTPUT(prepare_write_nanos);
IOSTATS_CONTEXT_OUTPUT(logger_nanos);
IOSTATS_CONTEXT_OUTPUT(cpu_write_nanos);
IOSTATS_CONTEXT_OUTPUT(cpu_read_nanos);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.hot_file_bytes_read);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.warm_file_bytes_read);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.cool_file_bytes_read);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.cold_file_bytes_read);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.hot_file_read_count);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.warm_file_read_count);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.cool_file_read_count);
IOSTATS_CONTEXT_OUTPUT(file_io_stats_by_temperature.cold_file_read_count);
std::string str = ss.str();
str.erase(str.find_last_not_of(", ") + 1);
return str;
#endif //! NIOSTATS_CONTEXT
}
} // namespace ROCKSDB_NAMESPACE