Fix wrong `OSIOWaitMicroseconds` for leaking stats by Felixoid · Pull Request #105246 · ClickHouse/ClickHouse · GitHub
Skip to content

Fix wrong OSIOWaitMicroseconds for leaking stats#105246

Merged
alexey-milovidov merged 3 commits into
masterfrom
fix-stats-reset
May 19, 2026
Merged

Fix wrong OSIOWaitMicroseconds for leaking stats#105246
alexey-milovidov merged 3 commits into
masterfrom
fix-stats-reset

Conversation

@Felixoid

@Felixoid Felixoid commented May 18, 2026

Copy link
Copy Markdown
Member

Linked issues

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fix OSIOWaitMicroseconds reporting thread lifetime I/O wait instead of query I/O wait

Description

Long-lived idle threads (e.g. MergeTreeFetchThreadPool workers, global ThreadPool workers) legitimately accumulate large delayacct_blkio_ticks from the kernel while sleeping in io_schedule_timeout on a futex. When such a thread is first enlisted into a query via ThreadGroupSwitcher, initPerformanceCounters creates a fresh TasksStatsCounters and calls reset() to establish the baseline prev. If reset() throws (e.g. transient read failure on /proc/thread-self/stat), the exception propagates through ThreadGroupSwitcher's constructor where it is silently swallowed — leaving prev.blkio_delay_total = 0 (from the uninitialized ::taskstats stats member). The very next updateCounters call then computes safeDiff(0, ~33_days_of_nanoseconds) and dumps it into the query's OSIOWaitMicroseconds.

Two fixes:

  1. Value-initialize ::taskstats stats{} in TasksStatsCounters so the member is zero rather than indeterminate if reset() fails before populating it.
  2. Wrap both (*taskstats).reset() call sites (initPerformanceCounters and resetPerformanceCountersLastUsage) in try/catch: on failure, log the exception and null out taskstats for that thread, so it stops collecting rather than reporting garbage on the next counter update.

Version info

  • Merged into: 26.5.1.799
  • Backported to: 26.4.5.21, 26.3.14.17

@clickhouse-gh

clickhouse-gh Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label May 18, 2026
Comment thread src/Interpreters/ThreadStatusExt.cpp
Comment thread tests/queries/0_stateless/04249_taskstats_reset_throwpoint.sh
Comment thread tests/queries/0_stateless/04249_taskstats_reset_throwpoint.sh
@Felixoid Felixoid force-pushed the fix-stats-reset branch 2 times, most recently from e896d74 to a75dccf Compare May 18, 2026 17:34
Comment thread tests/queries/0_stateless/04249_taskstats_reset_throwpoint.sh
@Felixoid Felixoid force-pushed the fix-stats-reset branch 3 times, most recently from d376e41 to a958602 Compare May 18, 2026 18:28
Comment thread src/Common/ThreadProfileEvents.cpp Outdated
@clickhouse-gh

clickhouse-gh Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 84.20% 84.20% +0.00%
Functions 91.40% 91.40% +0.00%
Branches 76.60% 76.60% +0.00%

Changed lines: 73.08% (19/26) · Uncovered code

Full report · Diff report

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, LGTM.
The only thing remained unclear - what was the reason they were throwing?

@alexey-milovidov alexey-milovidov self-assigned this May 19, 2026
@alexey-milovidov alexey-milovidov added this pull request to the merge queue May 19, 2026
Merged via the queue into master with commit d7ed79f May 19, 2026
166 checks passed
@alexey-milovidov alexey-milovidov deleted the fix-stats-reset branch May 19, 2026 04:55
@robot-clickhouse robot-clickhouse added the pr-synced-to-cloud The PR is synced to the cloud repo label May 19, 2026
@Felixoid

Copy link
Copy Markdown
Member Author

I tried to pin it down, but query_thread_log is empty for affected pods.

Although excluding other possible reasons, leaving only unreset ::taskstats stats as the remaining suspect:


Kernel side: delayacct_blkio_ticks on long-idle workers. On the affected pod, cat /proc/<tid>/stat on long-lived workers (e.g. tid 1103 Fetch, tid 719 ThreadPool) shows delayacct_blkio_ticks ≈ 287,000,000 ticks ≈ 33.2 days. These threads do no actual block I/O for those 33 days - they sit in pthread_cond_*wait or similar futex sleeps. This is a well-known Linux behavior: tasks asleep with in_iowait set (or when the run-queue is empty and the scheduler tags the sleep as iowait) accumulate delayacct_blkio_ticks at HZ regardless of whether disk activity is happening. So curr.blkio_delay_total ≈ wall_time_since_thread_start for these workers - and they form a large population that rotates over weeks.

ClickHouse side: prev is zero when updateCounters runs. Given a HZ-growing curr, the only way safeDiff produces a value equal to pod uptime is prev == 0. Pre-fix, TasksStatsCounters::stats was declared ::taskstats stats; at src/Common/ThreadProfileEvents.h:210 - POD-typed, not value-initialized in the default constructor. After new TasksStatsCounters(...) the struct is indeterminate; reset() (src/Common/ThreadProfileEvents.cpp:143-147) is expected to overwrite it via stats = stats_getter();. If that assignment doesn't take effect (exception path before write, or a fresh TasksStatsCounters whose reset() hasn't run yet on the path that calls updateCounters), stats stays at whatever the constructor left there - and on platforms where uninitialized stack/heap happens to be zeroed, that's 0. Diff = curr − 0 = full kernel-side accumulation.

Hypotheses we explicitly ruled out:

  • Parser misalignment in /proc/thread-self/stat. Manually walked the stat line of a thread on the affected pod and reproduced the parser's 41-token skip - it correctly lands on field 42 (delayacct_blkio_ticks). The big numbers in that line are arg_start/vsize/rsslim at much higher field positions; none has the right magnitude to be misread as the leak value.
  • Procfs read failures silently zeroing prev. Queried system.text_log for CANNOT_READ_FROM_FILE_DESCRIPTOR, CANNOT_OPEN_FILE, FILE_DOESNT_EXIST, ProcfsMetricsProvider, ThreadGroupSwitcher log messages, and any message mentioning /proc/thread-self/* over the affected window on the affected pod - zero rows. The parser is not throwing.
  • OSCPUWaitMicroseconds co-affected. It is not. Same struct, same safeDiff, same incrementProfileEvents, same reset() - and cpu_delay_total (from schedstat, not from stat) reads correctly. The asymmetry confirms the bug is at the field/init level, not at a higher layer.

What we couldn't fully pin down, and why it doesn't change the fix: which specific call-site reaches updateCounters with an uninitialized stats. system.query_thread_log was empty for the leaky queries - but that's because log_query_threads defaults to false and isn't enabled on these pods (src/Core/Settings.cpp:3537, src/Interpreters/ThreadStatusExt.cpp:578), not because the threads bypassed the counter. So we can't see per-thread attribution to localize the exact path. The value-init of ::taskstats stats{}; closes every path that could otherwise leak indeterminate-memory-as-prev, regardless of which one was firing in practice.

Why this PR is the right fix. It eliminates the only remaining route by which prev.blkio_delay_total can be effectively zero at the first updateCounters call: ensuring the struct is value-initialized at construction. Sibling fields (cpu_delay_total, cpu_run_virtual_total) inherit the same protection, hardening any future analogous condition. Validation should be a canary deploy + the existing times-enriched.log-style query - if no queries exceed the 3-day filter on the patched pod over the next few days, the fix is doing its job; if any do, we'll have a much narrower question to ask next.

@cho11y

cho11y Bot commented Jun 10, 2026

Copy link
Copy Markdown

@robot-clickhouse-ci-1 robot-clickhouse-ci-1 added the pr-must-backport-synced The `*-must-backport` labels are synced into the cloud Sync PR label Jun 10, 2026
robot-clickhouse-ci-1 added a commit that referenced this pull request Jun 10, 2026
Cherry pick #105246 to 26.3: Fix wrong `OSIOWaitMicroseconds` for leaking stats
robot-clickhouse added a commit that referenced this pull request Jun 10, 2026
robot-clickhouse-ci-1 added a commit that referenced this pull request Jun 10, 2026
Cherry pick #105246 to 26.4: Fix wrong `OSIOWaitMicroseconds` for leaking stats
robot-clickhouse added a commit that referenced this pull request Jun 10, 2026
@robot-ch-test-poll4 robot-ch-test-poll4 added the pr-backports-created Backport PRs are successfully created, it won't be processed by CI script anymore label Jun 10, 2026
alexey-milovidov added a commit that referenced this pull request Jun 11, 2026
Backport #105246 to 26.4: Fix wrong `OSIOWaitMicroseconds` for leaking stats
alexey-milovidov added a commit that referenced this pull request Jun 11, 2026
Backport #105246 to 26.3: Fix wrong `OSIOWaitMicroseconds` for leaking stats
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-backports-created Backport PRs are successfully created, it won't be processed by CI script anymore pr-bugfix Pull request with bugfix, not backported by default pr-must-backport-synced The `*-must-backport` labels are synced into the cloud Sync PR pr-synced-to-cloud The PR is synced to the cloud repo v25.12-must-backport v26.4-must-backport

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants