Enhancement
Background GC (DeltaMergeStore::gcTrySegmentMergeDelta) decides whether to rewrite a whole segment via shouldCompactStableWithTooManyInvalidVersion. That heuristic compares total MVCC version amplification against dt_bg_gc_ratio_threhold_to_trigger_gc (default 1.2), not how much data the current GC safepoint can actually drop. On update-heavy tables this fires full-stable MergeDelta that rewrite hundreds of MB to reclaim well under 1% of versions.
The other two compact reasons in the same function (TooManyDeleteRange, TooMuchOutOfRange) are not the issue here. Production logs below are 100% compact_reason=TooManyInvalidVersion.
Current strategy
gcTrySegmentMergeDelta tries three reasons, in order:
shouldCompactDeltaWithStable — delta delete-range vs stable (dt_bg_gc_delta_delete_ratio_to_trigger_gc, default 0.3) → TooManyDeleteRange
shouldCompactStableWithTooMuchDataOutOfSegmentRange — packs outside the segment range → TooMuchOutOfRange
shouldCompactStableWithTooManyInvalidVersion — once per new gc_safe_point → TooManyInvalidVersion
The third check (code comment: "for optimization purpose, does not mean to be accurate") is:
- Skip if
gc_hint_version > gc_safepoint (nothing older than safepoint).
- Trigger if
num_versions > num_rows * ratio or num_versions > num_puts * ratio.
- On trigger,
segmentMergeDelta rewrites the entire stable DMFile.
gc_hint_version is the minimum second-oldest version across keys/packs. One key with an expired extra version makes the whole segment eligible. num_versions / num_rows (or / num_puts) counts extra versions that may still be above safepoint (recent updates). The decision never estimates reclaimable rows/bytes at the current safepoint.
After a check, the segment records lastCheckGCSafePoint and will not be re-checked until safepoint advances — whether or not MergeDelta ran. Each GC round still scans up to dt_bg_gc_max_segments_to_check_every_round (default 100) segments.
These GC-triggered MergeDeltas are also not the normal delta-flush path. Observed delta_rows / delta_bytes were far below dt_segment_delta_limit_rows (80,000) / dt_segment_delta_limit_size (41 MB), and delta_deletes was almost always 0. The rewrite happens only because of the invalid-version heuristic.
What went wrong in production
Two TiFlash log extracts from the same node (10.107.2.188:3930, 2026-08-17). Write cost = new stable_bytes. Space saved = (old stable + old delta) − (new stable + new delta). Version drop = (old stable_rows + old delta_rows) − (new stable_rows + new delta_rows).
Log tiflash_10.107.2.188_3930.log (10:18:15–10:18:46)
| Metric |
Value |
TooManyInvalidVersion / all GC MergeDelta |
100 / 100 |
Unique (table_id, segment_id) |
100 (one full GC round) |
| New stable written |
14.0 GB |
| Space reclaimed |
92 MB |
| Aggregate write amp (written / reclaimed) |
152× |
| Version rows dropped |
0.65% (median 0.58%, max 4.17%) |
| Median write amp per segment |
171× (p90 291×, worst 510×) |
| Delta vs stable |
median 0.61%; max 3.3 MB / 24k rows |
96/100 events had write amp > 50×. None of this work would have been scheduled by the regular delta-compact thresholds.
Log tiflash_10.107.2.188_3930.0943.log (09:43:00–09:56:34)
192 paired TooManyInvalidVersion MergeDeltas in two rounds (safepoint advanced in between). Aggregate numbers look acceptable (55.4 GB written, 8.94 GB reclaimed, 6.2×) only because two tables dominate the reclaim. The same heuristic is bimodal:
| Slice |
N |
Written |
Reclaimed |
Write amp |
Median version drop |
Round 1 (09:43, table_id 152xxx) |
92 |
40.5 GB |
8.83 GB |
4.6× |
18% |
Round 2 (09:56, table_id 1606xx) |
100 |
14.9 GB |
108 MB |
138× |
0.75% |
High-reclaim tables 152950 + 152515 |
84 |
35.6 GB |
8.83 GB |
4.0× |
21% |
| The other 19 tables |
108 |
19.8 GB |
111 MB |
179× |
0.77% |
Round 2 is the same failure mode as the 10:18 log (same ~30s burst, 100-segment cap, ~0.7% reclaim, ~140× write amp).
Even the “good” round is mixed:
152515 had segments that dropped 99.6% of versions (write ~0.4 MB, reclaim ~105 MB) — the heuristic is doing the right thing.
152950 internally: 52 segments with GC% ≥ 10% at 2.9× write amp, vs 25 segments with GC% < 10% at 29×.
- Worst case in the same round:
table_id=152950 segment 1131229 rewrote 650 MB and dropped 2 rows.
Raising dt_bg_gc_ratio_threhold_to_trigger_gc (e.g. 1.2 → 1.5) only helps if low-reclaim segments sit in a lower version-amplification band than high-reclaim ones. INFO logs do not print StableProperty (num_versions / num_rows / num_puts are TRACE-only), so operators cannot tell whether the two populations are separable on that axis. If both sit just above 1.2 and differ only in how much is below safepoint, turning the knob reduces useful and useless GC together.
Why this is a strategy bug, not a tuning miss
- Wrong quantity. The threshold is advertised as “ratio of invalid version”, but the implementation compares total versions to unique keys / clean puts. Extra versions that are still live count toward the trigger.
- Wrong granularity.
gc_hint_version is a segment-wide minimum. One GC-able key + many live extra versions ⇒ rewrite the whole DMFile.
- No ROI gate. There is no minimum reclaimable-bytes or reclaimable-rows check before paying the full stable rewrite. Observed low-reclaim events had write amp of 100×–500000×.
- Full-round blast radius. Default 100 segments/round × one rewrite each means a safepoint bump can immediately write ~14 GB of useless compact (as in both 1606xx rounds) and occupy the GC thread for ~30s.
- Observability. Trigger logs only
compact_reason and segment id. Without TRACE StableProperty, it is impossible to see why the 1.2 line was crossed or how much was expected to be dropped.
What a better policy should do
Keep MergeDelta when the current safepoint can drop a meaningful fraction of the stable (the 152515 / high-GC 152950 cases, write amp ~1–5×). Do not rewrite when version amplification is high but reclaimable data at this safepoint is tiny (the 1606xx pattern).
Concretely, TooManyInvalidVersion should be gated on an estimate of reclaimable rows/bytes at gc_safe_point, not only num_versions / num_rows (or / num_puts). The existing ratio can stay as a cheap prefilter; a rewrite should also require something like “reclaimable stable bytes ≥ X% of current stable” (or a minimum absolute size).
Tuning-only workarounds (dt_bg_gc_ratio_threhold_to_trigger_gc, dt_bg_gc_max_segments_to_check_every_round) can rate-limit the damage; they cannot express “skip this rewrite because it would drop 0.6% of the file”.
Enhancement
Background GC (
DeltaMergeStore::gcTrySegmentMergeDelta) decides whether to rewrite a whole segment viashouldCompactStableWithTooManyInvalidVersion. That heuristic compares total MVCC version amplification againstdt_bg_gc_ratio_threhold_to_trigger_gc(default1.2), not how much data the current GC safepoint can actually drop. On update-heavy tables this fires full-stableMergeDeltathat rewrite hundreds of MB to reclaim well under 1% of versions.The other two compact reasons in the same function (
TooManyDeleteRange,TooMuchOutOfRange) are not the issue here. Production logs below are 100%compact_reason=TooManyInvalidVersion.Current strategy
gcTrySegmentMergeDeltatries three reasons, in order:shouldCompactDeltaWithStable— delta delete-range vs stable (dt_bg_gc_delta_delete_ratio_to_trigger_gc, default0.3) →TooManyDeleteRangeshouldCompactStableWithTooMuchDataOutOfSegmentRange— packs outside the segment range →TooMuchOutOfRangeshouldCompactStableWithTooManyInvalidVersion— once per newgc_safe_point→TooManyInvalidVersionThe third check (code comment: "for optimization purpose, does not mean to be accurate") is:
gc_hint_version > gc_safepoint(nothing older than safepoint).num_versions > num_rows * ratioornum_versions > num_puts * ratio.segmentMergeDeltarewrites the entire stable DMFile.gc_hint_versionis the minimum second-oldest version across keys/packs. One key with an expired extra version makes the whole segment eligible.num_versions / num_rows(or/ num_puts) counts extra versions that may still be above safepoint (recent updates). The decision never estimates reclaimable rows/bytes at the current safepoint.After a check, the segment records
lastCheckGCSafePointand will not be re-checked until safepoint advances — whether or notMergeDeltaran. Each GC round still scans up todt_bg_gc_max_segments_to_check_every_round(default 100) segments.These GC-triggered
MergeDeltas are also not the normal delta-flush path. Observeddelta_rows/delta_byteswere far belowdt_segment_delta_limit_rows(80,000) /dt_segment_delta_limit_size(41 MB), anddelta_deleteswas almost always 0. The rewrite happens only because of the invalid-version heuristic.What went wrong in production
Two TiFlash log extracts from the same node (
10.107.2.188:3930, 2026-08-17). Write cost = newstable_bytes. Space saved =(old stable + old delta) − (new stable + new delta). Version drop =(old stable_rows + old delta_rows) − (new stable_rows + new delta_rows).Log
tiflash_10.107.2.188_3930.log(10:18:15–10:18:46)TooManyInvalidVersion/ all GC MergeDelta(table_id, segment_id)96/100 events had write amp > 50×. None of this work would have been scheduled by the regular delta-compact thresholds.
Log
tiflash_10.107.2.188_3930.0943.log(09:43:00–09:56:34)192 paired
TooManyInvalidVersionMergeDeltas in two rounds (safepoint advanced in between). Aggregate numbers look acceptable (55.4 GB written, 8.94 GB reclaimed, 6.2×) only because two tables dominate the reclaim. The same heuristic is bimodal:table_id152xxx)table_id1606xx)152950+152515Round 2 is the same failure mode as the 10:18 log (same ~30s burst, 100-segment cap, ~0.7% reclaim, ~140× write amp).
Even the “good” round is mixed:
152515had segments that dropped 99.6% of versions (write ~0.4 MB, reclaim ~105 MB) — the heuristic is doing the right thing.152950internally: 52 segments with GC% ≥ 10% at 2.9× write amp, vs 25 segments with GC% < 10% at 29×.table_id=152950segment1131229rewrote 650 MB and dropped 2 rows.Raising
dt_bg_gc_ratio_threhold_to_trigger_gc(e.g. 1.2 → 1.5) only helps if low-reclaim segments sit in a lower version-amplification band than high-reclaim ones. INFO logs do not printStableProperty(num_versions/num_rows/num_putsare TRACE-only), so operators cannot tell whether the two populations are separable on that axis. If both sit just above 1.2 and differ only in how much is below safepoint, turning the knob reduces useful and useless GC together.Why this is a strategy bug, not a tuning miss
gc_hint_versionis a segment-wide minimum. One GC-able key + many live extra versions ⇒ rewrite the whole DMFile.compact_reasonand segment id. Without TRACEStableProperty, it is impossible to see why the 1.2 line was crossed or how much was expected to be dropped.What a better policy should do
Keep
MergeDeltawhen the current safepoint can drop a meaningful fraction of the stable (the152515/ high-GC152950cases, write amp ~1–5×). Do not rewrite when version amplification is high but reclaimable data at this safepoint is tiny (the 1606xx pattern).Concretely,
TooManyInvalidVersionshould be gated on an estimate of reclaimable rows/bytes atgc_safe_point, not onlynum_versions / num_rows(or/ num_puts). The existing ratio can stay as a cheap prefilter; a rewrite should also require something like “reclaimable stable bytes ≥ X% of current stable” (or a minimum absolute size).Tuning-only workarounds (
dt_bg_gc_ratio_threhold_to_trigger_gc,dt_bg_gc_max_segments_to_check_every_round) can rate-limit the damage; they cannot express “skip this rewrite because it would drop 0.6% of the file”.