-
Notifications
You must be signed in to change notification settings - Fork 783
schedulers: fix balance-region churn on near-empty clusters #11137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
61a5934
8532b76
dafffd2
ec0fec6
8203c39
b4ed1af
4b53744
f4b0be4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,7 +62,14 @@ const ( | |
| type regionTree struct { | ||
| tree *btree.BTreeG[*regionItem] | ||
| // Statistics | ||
| totalSize int64 | ||
| totalSize int64 | ||
| // nonEmptyTotalSize and nonEmptyRegionsCnt mirror totalSize/length but | ||
| // exclude empty regions (approximateSize <= EmptyRegionApproximateSize), | ||
| // so GetAverageRegionSize can reflect only regions that actually hold | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment names
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 8203c39. Corrected the comment to name |
||
| // data instead of being diluted by a large number of freshly-split, | ||
| // unwritten regions. | ||
| nonEmptyTotalSize int64 | ||
| nonEmptyRegionsCnt int | ||
| totalWriteBytesRate float64 | ||
| totalWriteKeysRate float64 | ||
| // count the number of regions that not loaded from storage. | ||
|
|
@@ -75,6 +82,8 @@ func newRegionTree() *regionTree { | |
| return ®ionTree{ | ||
| tree: btree.NewG[*regionItem](defaultBTreeDegree), | ||
| totalSize: 0, | ||
| nonEmptyTotalSize: 0, | ||
| nonEmptyRegionsCnt: 0, | ||
| totalWriteBytesRate: 0, | ||
| totalWriteKeysRate: 0, | ||
| notFromStorageRegionsCnt: 0, | ||
|
|
@@ -85,6 +94,8 @@ func newRegionTreeWithCountRef() *regionTree { | |
| return ®ionTree{ | ||
| tree: btree.NewG[*regionItem](defaultBTreeDegree), | ||
| totalSize: 0, | ||
| nonEmptyTotalSize: 0, | ||
| nonEmptyRegionsCnt: 0, | ||
| totalWriteBytesRate: 0, | ||
| totalWriteKeysRate: 0, | ||
| notFromStorageRegionsCnt: 0, | ||
|
|
@@ -174,6 +185,10 @@ func (t *regionTree) updateRef(origin, region *RegionInfo) { | |
| func (t *regionTree) update(item *regionItem, withOverlaps bool, overlaps ...*RegionInfo) []*RegionInfo { | ||
| region := item.RegionInfo | ||
| t.totalSize += region.approximateSize | ||
| if region.approximateSize > EmptyRegionApproximateSize { | ||
| t.nonEmptyTotalSize += region.approximateSize | ||
| t.nonEmptyRegionsCnt++ | ||
| } | ||
| regionWriteBytesRate, regionWriteKeysRate := region.GetWriteRate() | ||
| t.totalWriteBytesRate += regionWriteBytesRate | ||
| t.totalWriteKeysRate += regionWriteKeysRate | ||
|
|
@@ -201,6 +216,10 @@ func (t *regionTree) update(item *regionItem, withOverlaps bool, overlaps ...*Re | |
| logutil.ZapRedactStringer("delete-region", RegionToHexMeta(old.GetMeta())), | ||
| logutil.ZapRedactStringer("update-region", RegionToHexMeta(region.GetMeta()))) | ||
| t.totalSize -= old.approximateSize | ||
| if old.approximateSize > EmptyRegionApproximateSize { | ||
| t.nonEmptyTotalSize -= old.approximateSize | ||
| t.nonEmptyRegionsCnt-- | ||
| } | ||
| regionWriteBytesRate, regionWriteKeysRate = old.GetWriteRate() | ||
| t.totalWriteBytesRate -= regionWriteBytesRate | ||
| t.totalWriteKeysRate -= regionWriteKeysRate | ||
|
|
@@ -218,11 +237,19 @@ func (t *regionTree) update(item *regionItem, withOverlaps bool, overlaps ...*Re | |
| // updateStat is used to update statistics when RegionInfo is directly replaced. | ||
| func (t *regionTree) updateStat(origin *RegionInfo, region *RegionInfo) { | ||
| t.totalSize += region.approximateSize | ||
| if region.approximateSize > EmptyRegionApproximateSize { | ||
| t.nonEmptyTotalSize += region.approximateSize | ||
| t.nonEmptyRegionsCnt++ | ||
| } | ||
| regionWriteBytesRate, regionWriteKeysRate := region.GetWriteRate() | ||
| t.totalWriteBytesRate += regionWriteBytesRate | ||
| t.totalWriteKeysRate += regionWriteKeysRate | ||
|
|
||
| t.totalSize -= origin.approximateSize | ||
| if origin.approximateSize > EmptyRegionApproximateSize { | ||
| t.nonEmptyTotalSize -= origin.approximateSize | ||
| t.nonEmptyRegionsCnt-- | ||
| } | ||
| regionWriteBytesRate, regionWriteKeysRate = origin.GetWriteRate() | ||
| t.totalWriteBytesRate -= regionWriteBytesRate | ||
| t.totalWriteKeysRate -= regionWriteKeysRate | ||
|
|
@@ -252,6 +279,10 @@ func (t *regionTree) remove(region *RegionInfo) { | |
| } | ||
|
|
||
| t.totalSize -= result.GetApproximateSize() | ||
| if result.GetApproximateSize() > EmptyRegionApproximateSize { | ||
| t.nonEmptyTotalSize -= result.GetApproximateSize() | ||
| t.nonEmptyRegionsCnt-- | ||
| } | ||
| regionWriteBytesRate, regionWriteKeysRate := result.GetWriteRate() | ||
| t.totalWriteBytesRate -= regionWriteBytesRate | ||
| t.totalWriteKeysRate -= regionWriteKeysRate | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,7 +139,11 @@ func (p *solver) targetStoreScore(scheduleName string) float64 { | |
| targetDelta := influence + tolerantResource | ||
| score = p.Target.LeaderScore(p.kind.Policy, targetDelta) | ||
| case constant.RegionKind: | ||
| targetDelta := influence*influenceAmp + tolerantResource | ||
| // account for the candidate region's own size, so a target doesn't | ||
| // 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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the added 10 MiB/6 TiB regression scenario,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 8532b76.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed and fixed in dafffd2. You're right that |
||
| score = p.Target.RegionScore(p.GetSchedulerConfig().GetRegionScoreFormulaVersion(), p.GetSchedulerConfig().GetHighSpaceRatio(), p.GetSchedulerConfig().GetLowSpaceRatio(), targetDelta) | ||
| case constant.WitnessKind: | ||
| targetDelta := influence + tolerantResource | ||
|
|
@@ -178,7 +182,10 @@ func (p *solver) getTolerantResource() int64 { | |
| if (p.kind.Resource == constant.LeaderKind || p.kind.Resource == constant.WitnessKind) && p.kind.Policy == constant.ByCount { | ||
| p.tolerantSource = int64(p.tolerantSizeRatio) | ||
| } else { | ||
| regionSize := p.GetAverageRegionSize() | ||
| // Use the non-empty average so a cluster full of freshly-split, | ||
| // unwritten regions doesn't collapse the tolerant margin toward | ||
| // noise levels. | ||
| regionSize := p.GetNonEmptyAverageRegionSize() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branch is shared by all non-count schedule kinds, not just balance-region. In particular, balance-leader with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in ec0fec6. Scoped |
||
| p.tolerantSource = int64(float64(regionSize) * p.tolerantSizeRatio) | ||
|
bufferflies marked this conversation as resolved.
Outdated
|
||
| } | ||
| return p.tolerantSource | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning 0 when the cluster or selected range contains no non-empty region changes the existing tolerance to zero. This is observable in scatter-range, which intentionally allows 1 MiB empty regions and sets the tolerant ratio to 2:
rangeCluster.GetNonEmptyAverageRegionSize()now returns 0, so source gets no tolerance and target only gets the candidate's 1 MiB. The configured ratio is therefore no longer honored and empty-region balancing becomes more aggressive. Please define a fallback, such as retainingGetAverageRegionSize()when the non-empty count is zero, and add an all-empty range regression test.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ec0fec6.
GetNonEmptyAverageRegionSize()now falls back to the plain all-regions average (matchingGetAverageRegionSize()'s value) when there are no non-empty regions at all, instead of returning 0. Verified: in an all-empty scenario the two methods now return identical values, soscatter-range's configuredtolerant-size-ratiois no longer silently zeroed out.