[860] Reuse the Delta snapshot across an incremental backlog - #861
[860] Reuse the Delta snapshot across an incremental backlog#861AadhiKat wants to merge 1 commit into
Conversation
a504af2 to
446797c
Compare
getTableChangeForCommit reconstructed the table snapshot (getSnapshotAt) for every commit, re-reading the Delta checkpoint each time. That is cheap on a small checkpoint but dominates incremental catch-up on tables with a large checkpoint. Cache the snapshot (and the table/file format derived from it) and reload it only when a commit carries a Metadata or Protocol action. Otherwise the snapshot is used only for schema, partitioning, file format and base path, which do not change between commits. latestCommitTime feeds the sync watermark, so it can't come from the reused snapshot or the watermark stops advancing. Take it from the commit's own CommitInfo (already in the incremental actions), falling back to the snapshot timestamp only if a commit has no CommitInfo. Signed-off-by: Aadhithya Hari <aadhikat@gmail.com>
446797c to
388bb0f
Compare
|
Before I approve, I tried reproducing this on S3 first and couldn't a ~10 TB delta table showed no per-commit penalty, which I take to mean checkpoint part count drives it rather than bytes. Could you share a bit about the table behind the 454-part / ~6.5 GB checkpoint (schema width, how long the history runs between checkpoints, Delta and Spark versions)? Mainly I'd like to know whether the 2-3 min per commit went into getLogSegmentForVersion's _delta_log listing or the checkpoint Parquet read after it -- reading 2.4.0, the only expensive call in here looks like snapshot.metadata()? The direction of the optimization looks right either way; but want to see validate it thoroughly once before approving because it's a change in the xtable core path used by users in production. |
|
@vinishjail97, I did profiling and it ended up correcting the previous numbers. Repro you can run yourself is at the bottom. Where the time goes On the table from the issue (16 vCPU / 64 GB,
For listing vs read, Delta's own log timestamps the boundary on each reload: Same shape on all 11 reconstructions across the runs below: discovery 4-5 s, segment build 3-5 s, read 11-12 s. So roughly 9 s listing + 12 s checkpoint read per commit. I'd originally assumed the listing was negligible since a flat (The issue said 454 parts / ~6.5 GB; it's 459 / 6.90 GB now. Table is live, it grew.) A/B on the real table Same jar, same machine, same source instance, same 6-commit backlog. Only difference: clearing the cached snapshot before each commit (= main) or not (= this patch). RELOAD run before and after REUSE to rule out page cache: The two RELOAD runs agree within 0.7%. Non-snapshot per-commit work is ~8 ms, so the reconstruction is ~99.96% of the per-commit cost. (The last commit is ~3 ms even under RELOAD because Repro Builds a synthetic Delta table, checkpoints it, appends a backlog, then walks it through RELOAD grows linearly with backlog length, REUSE stays flat: the checkpoint gets read once instead of N times. Which also means there's no single speedup number, and I shouldn't have implied there was — the longer the backlog, the bigger the win. To push it further, raise the backlog length, or raise
|
| schema width | 256 columns |
| partitioning | year, month, day, hour |
| checkpoint cadence | every 10 commits |
| checkpoint | 459 parts, 6.90 GB, 45,638,865 add-files |
_delta_log |
72,238 objects, 594.74 GB |
| writer | Apache-Spark/3.5.4, Delta-Lake/3.2.0 |
| reader | Spark 3.4.2, Delta 2.4.0 |
| commit shape | blind append, ~130-1000 files per commit |
Closes #860.
getTableChangeForCommit reconstructs the table snapshot (deltaLog.getSnapshotAt) for every commit,
which re-reads the Delta checkpoint each time. It's fine when the checkpoint is small, but on a table
with a large checkpoint it becomes the bottleneck for incremental catch-up (see #860).
This caches the snapshot, along with the table and file format derived from it, and reloads it only
when a commit carries a Metadata or Protocol action. Apart from those, the snapshot is only used for
schema, partitioning, file format, and the table base path, which don't change between commits.
getActionsForVersion returns the unfiltered action list, so a schema/protocol change is visible to
the check and forces a reload for that version onward.
The one thing that varies per commit and comes from the snapshot is InternalTable.latestCommitTime
(snapshot.timestamp()). With reuse, intermediate commits report the timestamp of the last reload
rather than their own. If keeping that exact matters, it can come from the commit's CommitInfo
instead — happy to change it if you'd prefer.
Testing:
it adds a column mid-backlog, so if the cache weren't invalidated on the metadata change the later
commits would carry the old schema and it would fail.
append backlog through a source built with a spied DeltaLog, checks the table changes still
validate, and asserts getSnapshotAt is called once rather than once per commit.
On the ~45M-file table from the issue, incremental catch-up went from ~2-3 min/commit to ~3 s/commit
(around 210 commits in ~10 min), with driver memory flat.