Repository navigation
storage: add experimental opt-in log severity clustering - #118
Conversation
Clustering previously concatenated each buffered row group and then took a full reordered copy, holding two decoded copies of the group at once. Compute the stable order from the (at most two) key columns only, then gather rows from the buffered slices with `interleave` in 8,192-row batches and write them to the same row group. Already-ordered groups are written as buffered. Row-group membership, order, schema and footer metadata are unchanged; a new test checks the output is byte-identical to writing each reordered group as one batch, and another checks the order against an independent stable sort. In a 1.5M-row, 101-column follow-up run, the median RSS rise during conversion fell from 810 MiB to 143 MiB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up review and qualification (2026-11-09)Decision: hold as draft. Defaults are unchanged. The memory cost is much lower now, but the benefit on this remote shape is still modest and the CPU cost is still unmeasured. Fix pushed (8c3ffb1). Clustering used to concatenate each row group and then build a full reordered copy, so it held two decoded copies of the group at once. It now orders rows using only the key columns and gathers them from the buffered slices with
Focused release run: 77 passed, 1 existing fixture case ignored (clustering, crash recovery and ordering tests). Run setup. One boat.dev large VM, now erased. All four binaries used the same source tree, rustc 1.96.0 and Cargo.lock, release profile, and the same stub UI; each was rebuilt incrementally after checkout. The server was pinned to four CPUs against MinIO. The fixture is 1.5M rows with 101 columns (about 1.1% ERROR, 0.5% missing severity) and default 262,144-row groups. Each arm ran three rotated blocks; figures are medians.
Process CPU varied by up to 40% between runs of the same binary, and the variation followed run order, so this matrix does not resolve the CPU cost. The earlier +17–19% figure was measured on the first implementation. Queries. These read the baseline-written and clustered fixtures with the base binary, through 25 ms of
Bytes fall sharply, but latency improves by only about 0–8%. At this window, roughly 26 What would justify merge: a clean CPU measurement of the current writer, a narrow time-window query without a severity filter (the earlier run read +16–24% more), and real-cloud or blog-scale evidence. Kafka, live tail, OTLP gRPC and spilling remain unqualified. Raw evidence is kept locally; none of it is in this PR. 🤖 Generated with Claude Code |
Record the per-process writer benchmark (clustered writer CPU versus the default writer at staged and coalesced batch sizes) and a 6M-row run with 1 s and 10 s time windows without a severity filter, including the one consistent regression (severity filter plus a narrow window). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up 2: conversion CPU and narrow windows (2026-11-09)Doc-only update pushed (b0da249); the writer code is unchanged since 8c3ffb1. One boat.dev large VM, now erased. Every query result was checked: 6M distinct rows per fixture, exact 24h ERROR and severity oracles, ordered window rows, and window counts consistent with per-service sums. Zero failures. Writer CPU. Same 1M-row × 101-column input, production writer settings, each variant in its own process, median of 5:
Clustering's sort and gather cost about 13% more CPU than a default writer at the same batch size. But the clustered writer hands the encoder 8,192-row batches instead of the staged 2,048-row ones, so against today's path its writer CPU is lower. Whole-server CPU varied too much between runs of the same binary to resolve this at app level. Side finding, outside this PR: coalescing staged batches before writing would cut default-writer CPU by about 50% (to 2.70 s at 65,536 rows) for about 90 MiB more memory. Queries at 6M rows, 25 ms netem, C8 QPS:
A 1 s unfiltered window reads up to 2× the bytes, but no latency regression was measured. The one consistent regression is a severity filter combined with a narrow window: 7–12% lower throughput while reading half the bytes, cause not yet identified. Remaining before merge: the cause of that regression, and real-cloud or blog-scale evidence. The PR stays a draft with clustering off by default. 🤖 Generated with Claude Code |
Validate and normalize up to 16 clustering candidates, choosing the first present supported column for each merged output schema. Warn and continue past unsupported candidates while preserving skipped writer bytes. Cover parsing, candidate precedence, fallback and byte compatibility, and document a follow-up design for durable per-stream overrides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Description
Add experimental opt-in log severity clustering during staging-to-Parquet conversion.
P_LOG_SEVERITY_CLUSTER_COLUMNis unset by default; unset or inapplicable streams use the existing writer unchanged.For applicable log files, buffer one row group's rows and stably reorder them by the configured string/integer column ascending, then the timestamp descending, with nulls last. Preserve row-group membership, schemas, values, timestamp units, row counts, file paths and manifest format. Clear footer ordering claims on clustered files. Metrics, traces, internal streams and missing/unsupported keys retain the existing path.
This draft is independent of partition budgeting and the Flight metrics fix. It includes the writer/configuration, focused writer/crash-recovery/query tests and
docs/log-severity-clustering.md; no benchmark harnesses or bulk evidence archives.Validation
Why this remains a draft
In the standalone 3.6-million-row MinIO-backed campaign, severity-grouping logical reads fell from 7.24 to 3.68 MiB. Conversion CPU increased 17–19% and peak conversion RSS increased roughly 280–295 MiB; narrow time-only queries read 16–24% more. No reliable local query-latency gain was established. These synthetic results do not justify default enablement or production rollout.
Queries without complete ordering can return rows in a different order, and floating-point aggregation can change in its last bits. Existing files are not rewritten. Kafka, live-tail and OTLP gRPC compatibility, real-cloud latency, sustained ingestion capacity and production-scale memory cost remain unqualified. The standalone campaign encountered the inherited Flight metrics panic; successful Flight was separately qualified in the combined campaign with its independent fix.
This PR has:
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.