Opt-in dual-write segment files with mmap reads (storage v2 P1) - #32
Draft
SferaDev wants to merge 2 commits into
Draft
Opt-in dual-write segment files with mmap reads (storage v2 P1)#32SferaDev wants to merge 2 commits into
SferaDev wants to merge 2 commits into
Conversation
…P1b) With pg_deltax.blob_storage = 'dual' (default: 'toast', unchanged), each partition compression additionally writes its compressed segment blobs to one immutable .dxs file under $PGDATA/pg_deltax/<db_oid>/ (tmp + fsync + rename, index-at-footer, crc32c, generation-suffixed names), recorded in the new deltax.deltax_partition.blob_file column. The scan path mmaps the file and serves blob slices as zero-copy views instead of detoasting, failing open to the TOAST blobs table on any file problem (missing, renamed, truncated, bad magic/CRC) — dual mode keeps TOAST fully populated, so all data stays in regular PostgreSQL tables and replication/pg_dump/backups are unaffected. - src/segment_file.rs: file format, one-shot writer, incremental SegmentFileWriter (COPY-path P1b, appends across blob drains, footer-last), MappedSegmentFile mmap reader, unlink helpers. - src/compress.rs: SPI compress path dual-write before commit; decompress unlinks the file and clears blob_file. - src/copy.rs: direct-backfill dual-write with warn-once fail-open (file errors never fail the COPY). - src/scan/exec/segments.rs: file-backed blob load in load_segments_heap and fetch_segment_blobs; Arc<Mmap> held per segment (blob_file_backing) so views never outlive the mapping. - src/partition.rs: retention drop unlinks the file best-effort. - GUCs: pg_deltax.blob_storage (toast|dual), pg_deltax.verify_file_checksums. Measured (EC2 c6a.4xlarge, 100M ClickBench, hits_p20130702, fully cold): Q28-shape sort 894/892 ms via mmap'd .dxs vs 3966/3958 ms via TOAST — 4.4x cold-read win. Design + verdict: dev/docs/STORAGE_V2.md.
- write_partition_blob_file now goes through SegmentFileWriter, so the crash-critical tmp+fsync+rename+dir-fsync protocol exists exactly once (the one-shot encoder stays as a #[cfg(test)] oracle pinning the writer's byte-for-byte output); SPI path also gains .tmp cleanup on append error. - Drop the local PG17/PG18 tupdesc_get_attr copy in segment_file.rs; widen scan::exec::datum_utils::tupdesc_get_attr to pub(crate) instead. - Extend the BlobBytes Send/Sync SAFETY comment to cover the mmap'd segment-file backing alongside the DSA cache pin. - Add an end-to-end corrupt/truncated-file fallback test (unit tests covered detection; only the missing-file wiring was covered e2e). - STORAGE_V2.md: status line no longer claims nothing is implemented; path scheme corrected to <partition_id>_<generation>.dxs (matching the implementation and the doc's own decision log). - Remove unused pytest import in tests/test_blob_file.py.
SferaDev
marked this pull request as ready for review
June 12, 2026 12:39
This was referenced Jun 12, 2026
Member
|
I was avoiding this to keep everything into Postgres tables, so replication and crash recovery work out of the box. The opt-in dual mode idea is interesting, especially if it tells us how much we can win with it. But I'm worried about the extra maintenance, will give it a think. |
SferaDev
marked this pull request as draft
June 12, 2026 22:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Opt-in storage v2 P1/P1b: when
pg_deltax.blob_storage = 'dual', every partition compression additionally writes its compressed segment blobs into one immutable.dxssegment file per partition under$PGDATA/pg_deltax/<db_oid>/, and the scan path serves blob bytes from that file viammapinstead of detoasting from the_blobscompanion table.blob_storage = 'toast');dualis strictly opt-in..dxsfile is a redundant, read-optimized copy of the TOAST blobs — replication,pg_dump, and backups are unaffected.Why
Cold reads through TOAST pay B-tree descent + tuple reassembly + decompression-unfriendly random I/O per blob. An append-only file with an index-at-footer gives the kernel one sequential mapping to fault in, and blob "fetches" become pointer arithmetic into the mapping.
Measured (EC2 c6a.4xlarge, 100M-row ClickBench)
Partition
hits_p20130702recompressed underblob_storage=dual; Q28-shape sort query confined to that partition; fully cold per run (PG restart +echo 3 > /proc/sys/vm/drop_caches):.dxsfile4.4× cold-read win. The hidden-file runs double as a live validation of the fail-open path — results stayed correct with the file gone.
Design
Full design + measured verdict in
dev/docs/STORAGE_V2.md. Digest:src/segment_file.rs): header magic/version, blob payloads,(col_idx, segment_id, offset, len, crc32c)index at the footer, footer CRC. Index-at-footer enables incremental appends across COPY blob drains.fsync+ rename + directory fsync, all before the compressing transaction commits and records the path in the newdeltax.deltax_partition.blob_filecolumn. Generation-suffixed names make recompressions collision-free; a committedblob_filealways points at a durable, complete file.src/compress.rs, one shot) and the COPY direct-backfill path (src/copy.rs, incremental acrossBLOB_BUFFER_THRESHOLDdrains; P1b) — go through the sameSegmentFileWriter, so the durable-write protocol exists once. A test-only one-shot encoder pins the writer's output byte-for-byte. On the COPY path file-side errors warn once, abandon the writer, and never fail the COPY.src/scan/exec/segments.rs): when the catalog points at a file, blob slots getBlobBytes::Cachedraw-pointer views into anArc<Mmap>(MappedSegmentFile), with the Arc held in each segment'sblob_file_backingso views never outlive the mapping. Per-blob CRC verification is on in debug builds and behindpg_deltax.verify_file_checksumsin release.blob_file; retention drop unlinks best-effort (orphans are left for the planned P2 GC sweep).Testing
make build— clean.make clippy— zero warnings.make test(PG 17) — 577 unit tests passed, including the newsegment_fileround-trip/corruption/incremental-writer tests.make integration-test PG_VERSIONS=17— full suite green, including the newtests/test_blob_file.py(catalog/file creation, result equivalence vstoastmode, fallback after file deletion and after corruption/truncation, file removal on decompress, COPY-path dual-write).Extracted from a longer performance session (
perf/clickhouse-gap-session); this PR carries only the storage-v2 feature.