Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
ReaderExecutor: pipeline-based read orchestration #103706
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
base: master
Are you sure you want to change the base?
Changes from 250 commits
9dfbe97
07f665d
d6a3b43
c1675ab
0efff93
f02441a
c784f93
140f65c
e547649
4e4fc7a
ea1dc62
f2e8565
16af93e
58312d9
24d1d8c
1fabec1
ac47888
4c619e6
20a9fbd
f1f67ec
05cd497
f1e8519
8ec832b
6e1fc87
eb612b2
7d9fc78
b370b9b
07bdd08
818433d
8cfb789
0154f6a
cb5451b
f470a17
67e73b6
a17faa5
66d42c0
2bd85de
a4c40f5
70155c2
20efca6
5a80cdc
8eb95e5
c04e8ab
913893d
7fd073e
c92d4a4
975edb7
b6ef568
bfdf330
f799b12
56b8d5e
89e0c3b
2691ce3
c7f4fe2
8e4eb48
e234515
ac8c3da
45f6e1e
b8bee06
8f92155
af624b2
a409146
f3c3629
7641e7a
f8755b3
f9c1d50
20148b8
177230b
aa1ad95
1eefb49
d355642
d3c1e42
c73ac18
253045d
b429b24
edad35e
0de7fb5
bbc35f0
aa90bc7
f4cc7e5
d175306
f1eca60
a2fb469
987c007
40fca57
9680332
d3ef2f4
38d300b
0351007
66aba74
e4d7f63
8269973
3801145
f3e5dd4
9fcbe11
83c4b74
bb0322a
49a9207
f74cec4
c3e3969
aa6db73
aa1c5a9
c5e7b4d
8a270db
37dda1e
e88ee69
523b9ca
d6b76f5
24b5c2f
fd81c4f
35aa486
9ae45a3
d90416b
f427770
16ee0dc
0339f04
3831f2c
19987eb
f3d08f1
39897da
c767109
b52f2e8
d26c425
3b1b4b0
06cc926
533f108
7f41490
a0ccaf2
697c0b9
7b767a5
8590a64
c2312ee
d01f2cc
5e16353
55ef505
e2ef8f9
68a70a6
2d43694
8c9a37c
b83fad1
138a808
a35b22b
9583696
c193993
640eff4
3f5cb17
e1fcc49
4702cf8
c027f3c
4f0dce8
e5029fe
6c0e16a
74dbf7b
5872c17
b2095bf
92571d4
76daea3
b8d55d5
f9c9ab6
7f3c16a
c25acc6
b86bbbd
0103244
a0c4d47
ca42a2e
19ea7a4
7662754
147ac6c
7f5f5ea
bb8e7b2
071a5fa
d1fc3b4
bb43049
5865aa3
a1adbe4
7255aea
2d59fb5
d615ed1
fb23513
4b42e0f
6474a9a
eb5a1a7
6527d22
7a3abd2
763dae6
9ca00f6
d146dfb
572618f
c401ddc
bb5dd1a
06a682c
8ac1161
33655f7
e2229b9
379a824
e3bdefc
2419d56
dce291f
54f31af
075d6a3
e9d7a48
07f966f
8e45ec0
80363da
8471787
752b7b4
4bb230a
57450de
d89b5c3
024e783
bf81bd6
a02014b
4c20002
13f72ff
0db4a1e
08d738f
65944cd
03c675f
5de0e71
bb6ba1f
04d0e39
2c71a12
75c7bf7
67d9e50
a779791
6ca25c0
ede1e59
bd5f96b
c132b0d
3dd1511
d051922
3327bd7
cee6c48
5a570fb
3203064
8d7a31b
6ef81de
3f8ce13
cab3217
47ae0d9
45003bc
6623e5d
e800c8e
330ee26
0e45921
d9a7463
37215f3
76cae15
43d9894
1500181
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 |
|---|---|---|
|
|
@@ -58,6 +58,7 @@ | |
| M(Read, "Number of read (read, pread, io_getevents, etc.) syscalls in fly") \ | ||
| M(RemoteRead, "Number of read with remote reader in fly") \ | ||
| M(ReaderExecutorActive, "Number of live ReaderExecutor instances.") \ | ||
| M(ReaderExecutorPrefetchInFlight, "Number of ReaderExecutor prefetch tasks currently queued or executing.") \ | ||
|
Contributor
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. The metric text says this counts prefetch tasks that are currently queued or executing, but the counter is tied to the Please either scope the increment to the scheduled job lifetime, or change the metric description to say it includes completed-but-not-yet-collected read-ahead machines. |
||
| M(ReaderExecutorChainedBufferBytes, "Bytes currently held in live ReaderExecutor chain buffers (OwnedChainedBuffer) -- live in-flight read memory, not a cumulative total.") \ | ||
| M(Write, "Number of write (write, pwrite, io_getevents, etc.) syscalls in fly") \ | ||
| M(NetworkReceive, "Number of threads receiving data from network. Only ClickHouse-related network interaction is included, not by 3rd party libraries.") \ | ||
|
|
@@ -536,6 +537,8 @@ | |
| M(MergeTreeSnapshotCommitThreads, "Number of threads used to commit snapshot") \ | ||
| M(MergeTreeSnapshotCommitThreadsActive, "Number of active threads used to commit snapshot") \ | ||
| M(MergeTreeSnapshotCommitThreadsScheduled, "Number of scheduled threads used to commit snapshot") \ | ||
| \ | ||
| M(LongConnections, "Number of long source connections currently held open by ReaderExecutor for sequential read optimization.") \ | ||
|
|
||
|
|
||
| #ifdef APPLY_FOR_EXTERNAL_METRICS | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| #include <Common/MemoryPressureMonitor.h> | ||
| #include <Common/CurrentThread.h> | ||
| #include <Common/Exception.h> | ||
| #include <Common/MemoryTracker.h> | ||
| #include <Common/VariableContext.h> | ||
| #include <base/defines.h> | ||
|
|
||
| #include <algorithm> | ||
| #include <chrono> | ||
|
|
||
| namespace DB | ||
| { | ||
|
|
||
| namespace ErrorCodes | ||
| { | ||
| extern const int BAD_ARGUMENTS; | ||
| } | ||
|
|
||
| uint8_t PressureLevelMachine::rawLevel(double pressure) const | ||
| { | ||
| const uint32_t packed = thresholds_packed.load(std::memory_order_relaxed); | ||
| const double t1 = ((packed >> 16) & 0xFFu) / 100.0; | ||
| const double t2 = ((packed >> 8) & 0xFFu) / 100.0; | ||
| const double t3 = (packed & 0xFFu) / 100.0; | ||
|
|
||
| if (pressure >= t3) | ||
| return 3; | ||
| if (pressure >= t2) | ||
| return 2; | ||
| if (pressure >= t1) | ||
| return 1; | ||
| return 0; | ||
| } | ||
|
|
||
| MemoryPressureLevel PressureLevelMachine::levelForPressure(double pressure) const | ||
| { | ||
| /// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) | ||
| return static_cast<MemoryPressureLevel>(rawLevel(pressure)); | ||
| } | ||
|
|
||
| MemoryPressureLevel PressureLevelMachine::sample(double pressure, uint64_t now_ns) | ||
| { | ||
| std::lock_guard lk(mutex); | ||
|
|
||
| /// Classify under the lock: a concurrent `setThresholds` swaps the ladder and | ||
| /// resets the cooldown under this same lock, so the thresholds can't change | ||
| /// between this classification and the cooldown update below. | ||
| const uint8_t raw_level = rawLevel(pressure); | ||
|
|
||
| if (raw_level >= level) | ||
| { | ||
| /// Snap up immediately; refresh the "still elevated" timestamp. | ||
| level = raw_level; | ||
| last_at_or_above_ns = now_ns; | ||
| } | ||
| else if (level > 0 && now_ns >= last_at_or_above_ns + COOLDOWN_NS) | ||
| { | ||
| /// Step down by ONE level per cooldown — a CRITICAL → NORMAL recovery | ||
| /// needs ≥ 3 × COOLDOWN_NS of sustained low pressure. | ||
| level -= 1; | ||
| last_at_or_above_ns = now_ns; | ||
| } | ||
|
|
||
| chassert(level <= 3); | ||
| /// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) | ||
| return static_cast<MemoryPressureLevel>(level); | ||
| } | ||
|
|
||
| void validateMemoryPressureThresholds(UInt64 l1_pct, UInt64 l2_pct, UInt64 l3_pct) | ||
| { | ||
| /// Validate range and ordering loudly. Silent clamping/sorting (the | ||
| /// previous behavior) hid config typos — e.g. `300` would wrap to `44` | ||
| /// through `uint8_t` at the call site and then look valid here, so the | ||
| /// server would silently apply a wrong threshold ladder. | ||
| if (l1_pct > 100 || l2_pct > 100 || l3_pct > 100) | ||
| throw Exception(ErrorCodes::BAD_ARGUMENTS, | ||
| "Memory pressure thresholds must be in [0, 100], got " | ||
| "level_1={}, level_2={}, level_3={}", | ||
| l1_pct, l2_pct, l3_pct); | ||
| if (!(l1_pct <= l2_pct && l2_pct <= l3_pct)) | ||
| throw Exception(ErrorCodes::BAD_ARGUMENTS, | ||
| "Memory pressure thresholds must satisfy level_1 <= level_2 <= level_3, got " | ||
| "level_1={}, level_2={}, level_3={}", | ||
| l1_pct, l2_pct, l3_pct); | ||
| } | ||
|
|
||
| void PressureLevelMachine::setThresholds(UInt64 l1_pct, UInt64 l2_pct, UInt64 l3_pct) | ||
| { | ||
| validateMemoryPressureThresholds(l1_pct, l2_pct, l3_pct); | ||
|
|
||
| /// Publish all three as one packed value so `rawLevel` never sees a | ||
| /// half-applied ladder. | ||
| const uint32_t packed = (static_cast<uint32_t>(l1_pct) << 16) | ||
| | (static_cast<uint32_t>(l2_pct) << 8) | ||
| | static_cast<uint32_t>(l3_pct); | ||
|
|
||
| std::lock_guard lk(mutex); | ||
| thresholds_packed.store(packed, std::memory_order_relaxed); | ||
| /// Reset the cooldown under the same lock `sample` uses: a level classified | ||
| /// under the old ladder must not stick after the ladder changes. The next | ||
| /// `sample` reclassifies (snap-up is immediate, so genuinely-high pressure | ||
| /// re-escalates at once rather than waiting out a stale cooldown). | ||
| level = 0; | ||
|
Contributor
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.
Please skip the reset when |
||
| last_at_or_above_ns = 0; | ||
| } | ||
|
|
||
| MemoryPressureThresholds PressureLevelMachine::getThresholds() const | ||
| { | ||
| const uint32_t packed = thresholds_packed.load(std::memory_order_relaxed); | ||
| return {(packed >> 16) & 0xFFu, (packed >> 8) & 0xFFu, packed & 0xFFu}; | ||
| } | ||
|
|
||
| namespace | ||
| { | ||
|
|
||
| double readTotalPressure() | ||
| { | ||
| const Int64 limit = total_memory_tracker.getHardLimit(); | ||
| if (limit <= 0) | ||
| return 0.0; | ||
| const Int64 used = total_memory_tracker.get(); | ||
| if (used <= 0) | ||
| return 0.0; | ||
| return static_cast<double>(used) / static_cast<double>(limit); | ||
| } | ||
|
|
||
| uint64_t steadyNowNs() | ||
| { | ||
| return std::chrono::duration_cast<std::chrono::nanoseconds>( | ||
| std::chrono::steady_clock::now().time_since_epoch()) | ||
| .count(); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| double localMemoryPressureFromChain(MemoryTracker * start) | ||
| { | ||
| double worst = 0.0; | ||
| for (MemoryTracker * t = start; t != nullptr; t = t->getParent()) | ||
| { | ||
| /// The server total (`Global`) is handled by the cooldown-smoothed | ||
| /// total-pressure path; here we react only to the transient per-query | ||
| /// (`Process`) and per-user (`User`) limits. | ||
| if (t->level == VariableContext::Global) | ||
| continue; | ||
| worst = std::max(worst, t->getPressure()); | ||
| } | ||
| return worst; | ||
| } | ||
|
|
||
| MemoryPressureLevel MemoryPressureMonitor::currentLevel() | ||
| { | ||
| /// Server-wide pressure keeps its 60s sticky-downward cooldown. | ||
| const MemoryPressureLevel total_level = machine.sample(readTotalPressure(), steadyNowNs()); | ||
| /// Per-query / per-user limits are transient: react immediately, with no | ||
| /// sticky state that could leak one query's pressure into another's. | ||
| const MemoryPressureLevel local_level = | ||
| machine.levelForPressure(localMemoryPressureFromChain(CurrentThread::getMemoryTracker())); | ||
| return std::max(total_level, local_level); | ||
| } | ||
|
|
||
| MemoryPressureLevel FakeMemoryPressureMonitor::currentLevel() | ||
| { | ||
| return machine.sample( | ||
| pressure.load(std::memory_order_relaxed), | ||
| now_ns.load(std::memory_order_relaxed)); | ||
| } | ||
|
|
||
| namespace | ||
| { | ||
|
|
||
| /// Atomic pointer to the active monitor. Production never writes after | ||
| /// first init; tests swap it via `ScopedMemoryPressureMonitor`. | ||
| std::atomic<IMemoryPressureMonitor *> & activeMonitorSlot() | ||
| { | ||
| static std::atomic<IMemoryPressureMonitor *> slot{nullptr}; | ||
| return slot; | ||
| } | ||
|
|
||
| MemoryPressureMonitor & productionInstance() | ||
| { | ||
| static MemoryPressureMonitor instance; | ||
| return instance; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| IMemoryPressureMonitor & memoryPressureMonitor() | ||
| { | ||
| auto & slot = activeMonitorSlot(); | ||
| auto * current = slot.load(std::memory_order_acquire); | ||
| if (current) | ||
| return *current; | ||
|
|
||
| /// First access: install the production singleton. Concurrent first | ||
| /// callers all reference the same Meyers singleton; `compare_exchange` | ||
| /// ensures only one wins the slot, the rest pick up that value. | ||
| auto * production = &productionInstance(); | ||
| IMemoryPressureMonitor * expected = nullptr; | ||
| slot.compare_exchange_strong(expected, production, std::memory_order_acq_rel); | ||
| return *slot.load(std::memory_order_acquire); | ||
| } | ||
|
|
||
| ScopedMemoryPressureMonitor::ScopedMemoryPressureMonitor(IMemoryPressureMonitor & override_monitor) | ||
| { | ||
| /// Force the production singleton to be constructed and slotted so the | ||
| /// dtor can restore it even if nothing called `memoryPressureMonitor()` | ||
| /// before this scope was entered. | ||
| (void)memoryPressureMonitor(); | ||
| prior = activeMonitorSlot().exchange(&override_monitor, std::memory_order_acq_rel); | ||
| } | ||
|
|
||
| ScopedMemoryPressureMonitor::~ScopedMemoryPressureMonitor() | ||
| { | ||
| activeMonitorSlot().store(prior, std::memory_order_release); | ||
| } | ||
|
|
||
| } | ||

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.
validateMemoryPressureThresholdsnow prevents the invalid threshold triple from being applied to the liveReaderExecutorcomponents, but it runs afterconfig().replacehas already installed the rejected config into the global application config. A failedSYSTEM RELOAD CONFIGcan therefore still leak unrelated rejected values throughsystem.server_settings, which rebuilds rows fromcontext->getConfigRef: for example, reload a config that changesreader_executor_prefetch_pool_sizeand also setsreader_executor_memory_pressure_level_1_pct=150. The reload throws here, the prefetch pool still uses the oldServerSettings, butsystem.server_settingscan report the newreader_executor_prefetch_pool_sizefrom the rejected config. The new integration test only checks the threshold rows, which are live-overridden, so it misses this split-brain state. Please validate againstloaded_configbefore replacing the live config, or roll the config back on validation failure.