Coalesce consecutive page cache misses into single S3 requests#104230
Conversation
`CachedInMemoryReadBufferFromFile::populateBlockRange` previously issued one `in->readBigAt` per missing 1 MiB block. On object storage, each call is a separate HTTP request, so a cold scan of a 14 GB Parquet file through the userspace page cache made ~15k requests, each paying the TCP/TLS round-trip — measurably slower than the filesystem cache, which fetches in larger segments. Coalescing was previously implemented in commit 682b070 and reverted in c178d2a to avoid transient memory spikes from huge temporary buffers under parallel cold reads. Re-introduce coalescing with a hard cap on the temporary buffer (`max_coalesced_bytes` = 16 MiB). Long miss runs are split into multiple fetches, bounding peak transient memory per call. Single-block misses still read directly into the cache cell, avoiding the buffer and the extra `memcpy`. Measured locally on c8g.24xlarge against the ClickBench `clickhouse-datalake` queries (43 queries, single 14.7 GB Parquet on S3, totals over all queries): cold runs: filesystem cache 62.28s -> page cache (default) 56.58s hot runs: filesystem cache 18.57s -> page cache (default) 13.59s The page cache is now strictly faster than the filesystem cache on both cold and hot, with no benchmark-script tuning required. Context: ClickHouse/ClickBench#818 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Workflow [PR], commit [26f5279] Summary: ✅ AI ReviewSummaryThis PR re-introduces coalesced page-cache miss reads for object storage, now bounded by a new setting ClickHouse Rules
Final Verdict
|
| /// The coalesced read uses a temporary buffer, capped at `max_coalesced_bytes` to bound | ||
| /// transient memory under parallel cold reads. A run longer than the cap is split. | ||
| /// Single-block misses bypass the buffer and read directly into the cache cell. | ||
| constexpr size_t max_coalesced_bytes = 16 * 1024 * 1024; |
There was a problem hiding this comment.
Can we make it a setting?
| /// The coalesced read uses a temporary buffer, capped at `max_coalesced_bytes` to bound | ||
| /// transient memory under parallel cold reads. A run longer than the cap is split. | ||
| /// Single-block misses bypass the buffer and read directly into the cache cell. | ||
| constexpr size_t max_coalesced_bytes = 16 * 1024 * 1024; |
There was a problem hiding this comment.
max_coalesced_bytes = 16 MiB is an important behavior threshold (memory vs request coalescing), but it's hardcoded. This makes tuning impossible for clusters with very different object-store RTT or memory pressure, and it violates the usual ClickHouse pattern of exposing such trade-offs as a setting.
Please make this cap configurable (e.g. a read/page-cache setting with a conservative default), then use that value here.
Address review feedback on PR ClickHouse#104230: the 16 MiB cap on coalesced page-cache reads was hardcoded, which prevents tuning for clusters with different object-store RTT or memory pressure. Expose it as the session setting `page_cache_max_coalesced_bytes`, with the same 16 MiB default. The setting flows through `ReadSettings` to `CachedInMemoryReadBufferFromFile::populateBlockRange`, which uses it to compute the per-fetch block cap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LLVM Coverage Report
Changed lines: 30.77% (20/65) · Uncovered code |
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Cold reads of object storage through the userspace page cache (
use_page_cache_for_object_storage = 1) are now significantly faster, because consecutive cache misses are coalesced into a single HTTP request instead of one request perpage_cache_block_sizeblock.Description
CachedInMemoryReadBufferFromFile::populateBlockRangepreviously issued onein->readBigAtper missing 1 MiB block. On object storage, each call is a separate HTTP request, so a cold scan of a 14 GB Parquet file through the userspace page cache made ~15k requests, each paying the TCP/TLS round-trip — measurably slower than the filesystem cache, which fetches in larger segments.Coalescing was previously implemented in commit 682b070 and reverted in c178d2a to avoid transient memory spikes from huge temporary buffers under parallel cold reads.
This change re-introduces coalescing with a hard cap on the temporary buffer (
max_coalesced_bytes= 16 MiB). Long miss runs are split into multiple fetches, bounding peak transient memory per call. Single-block misses still read directly into the cache cell, avoiding the buffer and the extramemcpy.Test results
Measured on c8g.24xlarge against the ClickBench
clickhouse-datalakequeries (43 queries, single 14.7 GB Parquet on S3, totals over all queries):Per-query, the worst regressed queries are back to baseline:
SELECT *): 15.23s -> 34.61s broken -> 15.27s fixedContext: ClickHouse/ClickBench#818
Documentation entry for user-facing changes