Skip to content

TooManyInvalidVersion GC rewrites entire stable when reclaimable data is tiny #11048

Description

@JaySon-Huang

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:

  1. shouldCompactDeltaWithStable — delta delete-range vs stable (dt_bg_gc_delta_delete_ratio_to_trigger_gc, default 0.3) → TooManyDeleteRange
  2. shouldCompactStableWithTooMuchDataOutOfSegmentRange — packs outside the segment range → TooMuchOutOfRange
  3. shouldCompactStableWithTooManyInvalidVersion — once per new gc_safe_pointTooManyInvalidVersion

The third check (code comment: "for optimization purpose, does not mean to be accurate") is:

  1. Skip if gc_hint_version > gc_safepoint (nothing older than safepoint).
  2. Trigger if num_versions > num_rows * ratio or num_versions > num_puts * ratio.
  3. 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

  1. 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.
  2. Wrong granularity. gc_hint_version is a segment-wide minimum. One GC-able key + many live extra versions ⇒ rewrite the whole DMFile.
  3. 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×.
  4. 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.
  5. 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”.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions