Skip to content

core, schedulers: fix balance-region churn on near-empty clusters - #11137

Open
bufferflies wants to merge 3 commits into
tikv:masterfrom
bufferflies:fix/balance-region-target-score-candidate-size
Open

core, schedulers: fix balance-region churn on near-empty clusters#11137
bufferflies wants to merge 3 commits into
tikv:masterfrom
bufferflies:fix/balance-region-target-score-candidate-size

Conversation

@bufferflies

@bufferflies bufferflies commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Close #11135

balance-region could churn/thrash on a cluster with many near-empty regions (e.g. freshly pre-split tables, mostly unwritten) alongside a few regions that actually hold data — observed in production as repeated, low-value peer moves between otherwise-empty stores.

Two contributing gaps:

  • targetStoreScore()'s delta never accounted for the specific region being evaluated for the move, so a target store's score didn't reflect what it was about to receive until after the move landed.
  • getTolerantResource()'s margin is derived from GetAverageRegionSize(), which averages over every region in the cluster. On a cluster full of empty regions, that average collapses toward zero, so the margin stops damping marginal score differences between otherwise-equivalent stores.

What is changed and how does it work?

Add RegionsInfo.GetNonEmptyAverageRegionSize(), which excludes empty
regions from the average region size used by getTolerantResource(),
backed by an incrementally-maintained pair of totals on regionTree
(nonEmptyTotalSize / nonEmptyRegionsCnt) alongside the existing
totalSize/length(), so it stays O(1). GetAverageRegionSize() itself is
untouched; the new method is plumbed through the RegionSetInformer
interface and rangeCluster's pass-through wrapper.

targetStoreScore() now takes max(tolerantResource,
candidateRegion.GetApproximateSize()) instead of adding the two
together. tolerantResource already represents roughly one region's
worth of margin, so summing them double-counted it and rejected
ordinary moves between similarly-sized regions. Falling back to the
candidate's real size only when it exceeds the average-based margin
matches the long-documented but never-implemented intent in
shouldBalance()'s own comment ("we use max(regionSize,
averageRegionSize)").

sourceStoreScore is intentionally left unchanged.

Add TestSingleRegionOnLargeEmptyDiskDoesNotMigrate: a single
non-empty region isolated among many empty regions and stores
correctly stays put instead of being shuffled between equally
"empty-looking" targets.

Add TestBalanceRegionOrdinaryMoveNotBlockedByCandidateSize: ordinary
balancing between several similarly-sized, non-empty regions is not
blocked by the fix above.

Check List

Tests

  • Unit test

Release note

Fix a `balance-region` scheduling issue that could cause repeated, low-value peer moves ("churn") on clusters with many near-empty regions, while preserving normal balancing between ordinary, similarly-sized regions.

@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-linked-issue dco-signoff: yes Indicates the PR's author has signed the dco. do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. labels Aug 12, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign jmpotato for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 33a4b6f1-51ea-4de9-b5ce-6ea285380ffd

📥 Commits

Reviewing files that changed from the base of the PR and between 0867c8d and 8532b76.

📒 Files selected for processing (6)
  • pkg/core/basic_cluster.go
  • pkg/core/region.go
  • pkg/core/region_tree.go
  • pkg/schedule/schedulers/balance_region_test.go
  • pkg/schedule/schedulers/range_cluster.go
  • pkg/schedule/schedulers/utils.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/schedule/schedulers/utils.go

📝 Walkthrough

Walkthrough

The balance-region scheduler now tracks non-empty region sizes separately from overall region sizes. It uses this data for tolerant resource calculations and target scoring. Tests update influence thresholds and verify that a small data-bearing region is not migrated between large mostly empty stores.

Changes

Region balance scoring

Layer / File(s) Summary
Track non-empty region statistics
pkg/core/basic_cluster.go, pkg/core/region.go, pkg/core/region_tree.go
The region tree tracks size and count for regions above EmptyRegionApproximateSize. RegionsInfo and RegionSetInformer expose the non-empty average region size.
Use non-empty sizes in scheduling
pkg/schedule/schedulers/range_cluster.go, pkg/schedule/schedulers/utils.go
The scheduler uses the non-empty average for tolerant resource calculations. Target scoring adds candidate region size without amplifying it.
Validate revised balance decisions
pkg/schedule/schedulers/balance_region_test.go
Tests update influence thresholds and verify that a small data-bearing region is not selected for migration between large mostly empty stores.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • tikv/pd#11098: Both PRs modify RegionsInfo region-size tracking in pkg/core/region.go, but they implement different logic.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue [#11135] by accounting for candidate size and non-empty region averages, with a focused regression test for the empty-region scenario.
Out of Scope Changes check ✅ Passed All production and test changes support the linked issue by improving balance-region scoring and handling non-empty region statistics.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly identifies the affected components and the main fix for balance-region churn on near-empty clusters.
Description check ✅ Passed The description covers the issue, implementation, unit tests, and release note, with sufficient detail to explain the change.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/schedule/schedulers/balance_region_test.go`:
- Around line 121-151: Update the test fixture around AddLeaderRegion so region
1 exists only on store 1 and store 2 does not already host its peer; add one
additional empty region to store 2 to preserve ten regions per store. Keep
solver target selection and the shouldBalance assertion unchanged so the test
validates a schedulable target.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b1815c2-ed3a-4df9-be53-0059ac28fab7

📥 Commits

Reviewing files that changed from the base of the PR and between 3430f76 and 0867c8d.

📒 Files selected for processing (2)
  • pkg/schedule/schedulers/balance_region_test.go
  • pkg/schedule/schedulers/utils.go

Comment thread pkg/schedule/schedulers/balance_region_test.go Outdated
targetStoreScore() only reflected already-pending operators' influence,
never the size of the region currently being evaluated for the move.
On a cluster where most regions are near-empty (e.g. freshly pre-split,
mostly unwritten), the average region size collapses, tolerantResource
shrinks with it, and a target store can look artificially light right
up until the move lands — letting balance-region pick a target that
becomes overloaded the instant the pending region is counted, which
triggers a follow-up move to shed it again.

Add the candidate region's own approximate size to the target delta
(unamplified, since it is not "other pending influence" but the exact
size about to be received) so a target's projected post-move score is
what actually decides whether it's picked.

Recalibrate TestInfluenceAmp's boundary counts: the new term shifted
the pre-existing count+size boundary this test pins down by exactly
one region-size step.

Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies
bufferflies force-pushed the fix/balance-region-target-score-candidate-size branch from 0867c8d to 61a5934 Compare August 12, 2026 07:41
@ti-chi-bot ti-chi-bot Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/needs-triage-completed and removed do-not-merge/needs-linked-issue do-not-merge/release-note-label-needed Indicates that a PR should not merge because it's missing one of the release note labels. labels Aug 12, 2026
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.43%. Comparing base (3430f76) to head (61a5934).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #11137   +/-   ##
=======================================
  Coverage   79.43%   79.43%           
=======================================
  Files         542      542           
  Lines       77117    77117           
=======================================
+ Hits        61259    61260    +1     
+ Misses      11571    11560   -11     
- Partials     4287     4297   +10     
Flag Coverage Δ
unittests 79.43% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread pkg/schedule/schedulers/utils.go Outdated
// look artificially light just because this move hasn't landed yet.
// Unlike opInfluence (other, already-pending operators), this is not
// amplified: it is the literal size the target is about to receive.
targetDelta := influence*influenceAmp + tolerantResource + p.Region.GetApproximateSize()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the added 10 MiB/6 TiB regression scenario, GetAverageRegionSize() truncates to zero; the populated source's v2 score is slightly above 10 while an empty target's projected score is exactly 10, so this comparison still schedules the peer. After it lands on any empty store, another empty target recreates the same state, allowing the peer to rotate indefinitely and leaving the reported churn unresolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8532b76. getTolerantResource() now uses a new GetNonEmptyAverageRegionSize() (excludes empty regions from the average instead of GetAverageRegionSize()), so it no longer collapses to zero in this scenario. Re-verified the exact case you described: tolerantResource computes to the candidate region's own size instead of truncating to zero, and the target's projected score now exceeds the source's, so shouldBalance() returns false — the region stays put instead of rotating. TestSingleRegionOnLargeEmptyDiskDoesNotMigrate (renamed/inverted from the original test) asserts this directly.

…t margin

getTolerantResource() derived its margin from GetAverageRegionSize(),
which averages over every region in the cluster. On a cluster with many
freshly-split, unwritten regions, that average collapses toward zero,
so the tolerant margin stops damping marginal score differences between
otherwise-equivalent stores.

Add RegionsInfo.GetNonEmptyAverageRegionSize(), backed by a second pair
of incrementally-maintained totals on regionTree (nonEmptyTotalSize /
nonEmptyRegionsCnt) alongside the existing totalSize/length(), so it
stays O(1). GetAverageRegionSize() itself is untouched; the new method
is plumbed through the RegionSetInformer interface and rangeCluster's
pass-through wrapper, and getTolerantResource() is switched to use it.

Rework TestSingleRegionOnLargeEmptyDiskCanMigrate into
TestSingleRegionOnLargeEmptyDiskDoesNotMigrate: with the tolerant
margin no longer diluted, a single non-empty region isolated among many
empty ones and stores correctly stays put — moving it would not fix a
real imbalance and would just relocate the same "which store holds the
only real data" state onto a different empty store, inviting the
churn reported in tikv#11135. Also drops the region's phantom peer on the
target store from the fixture, per review feedback on the prior
version of this test.

Signed-off-by: bufferflies <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Aug 12, 2026
Comment thread pkg/schedule/schedulers/utils.go Outdated
// look artificially light just because this move hasn't landed yet.
// Unlike opInfluence (other, already-pending operators), this is not
// amplified: it is the literal size the target is about to receive.
targetDelta := influence*influenceAmp + tolerantResource + p.Region.GetApproximateSize()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tolerantResource already contributes one average region to the target margin. Adding the candidate again rejects legitimate moves when the candidate equals the average: with three 96 MiB regions on the source and an empty target (v1, ratio=1), this head compares 192 vs 192 and schedules nothing, although moving one region leaves 192 vs 96. The base head schedules this move, so this introduces a balance-region regression for ordinary equal-sized regions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in dafffd2. You're right that tolerantResource already represents about one region's worth of margin, so adding the candidate's size on top double-counted it. Switched to max(tolerantResource, p.Region.GetApproximateSize()) instead of summing them — this falls back to the candidate's real size only when it exceeds the average-based margin, matching the (previously unimplemented) intent in shouldBalance()'s own comment about max(regionSize, averageRegionSize). Re-ran your exact reproduction (three 96MiB regions on the source, empty target, v1, ratio=1): now scores 192 vs 96 and schedules the move, matching pre-PR/base behavior. Added TestBalanceRegionOrdinaryMoveNotBlockedByCandidateSize as a permanent regression test for this case, since no existing test previously covered balancing between several ordinary, similarly-sized regions.

…ntResource

targetStoreScore added the candidate region's own approximate size on
top of tolerantResource, but tolerantResource (averageRegionSize *
ratio) already represents roughly one region's worth of margin. In any
ordinary cluster where the candidate is close to the average region
size, this summed to about two regions' worth, silently doubling the
score gap required before balance-region would act and rejecting
legitimate moves between equal-sized regions (reported by rleungx:
three 96MiB regions on a source vs an empty target, 192 vs 96 after
the move, no longer scheduled).

Take the larger of the two instead of adding them. This matches the
long-standing but never-implemented intent documented in
shouldBalance()'s own comment ("we use max(regionSize,
averageRegionSize)"): fall back to the candidate's real size only when
it exceeds the average-based margin. Verified against both the
regression case above (now matches pre-PR behavior) and the
originally-motivating churn scenario in
TestSingleRegionOnLargeEmptyDiskDoesNotMigrate (still correctly stays
put). TestInfluenceAmp's boundary counts revert to their original,
pre-PR values, since max() is behaviorally identical to the base
formula whenever the candidate doesn't exceed tolerantResource.

Add TestBalanceRegionOrdinaryMoveNotBlockedByCandidateSize as a
permanent regression test for this, since no existing test previously
covered balancing between several ordinary, similarly-sized regions.

Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies bufferflies changed the title schedulers: account for candidate region size in target balance score core, schedulers: fix balance-region churn on near-empty clusters Aug 12, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@bufferflies: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-unit-test-next-gen-2 dafffd2 link true /test pull-unit-test-next-gen-2
pull-unit-test-next-gen-3 dafffd2 link true /test pull-unit-test-next-gen-3

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

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

Labels

dco-signoff: yes Indicates the PR's author has signed the dco. do-not-merge/needs-triage-completed release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

balance-region may thrash when data is concentrated in a single small region among many near-empty regions

2 participants