milvus/internal/datacoord/compaction_view_forcemerge.go
cai.zhang b2fa3dd0ae
fix: Disable VCS to allow pkg tests to run (#46501)
### **Description**
- Add `-buildvcs=false` flag to Go test commands in coverage script

- Increase default session TTL from 10s to 15s

- Update SessionTTL parameter default value from 30 to 15


Signed-off-by: Cai Zhang <cai.zhang@zilliz.com>
Co-authored-by: bigsheeper <yihao.dai@zilliz.com>
Co-authored-by: chyezh <chyezh@outlook.com>
Co-authored-by: czs007 <zhenshan.cao@zilliz.com>
2025-12-25 14:31:19 +08:00

311 lines
9.7 KiB
Go

// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datacoord
import (
"fmt"
"math"
"time"
"github.com/samber/lo"
"go.uber.org/zap"
"github.com/milvus-io/milvus/pkg/v2/log"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
)
const (
defaultToleranceMB = 0.05
)
// static segment view, only algothrims here, no IO
type ForceMergeSegmentView struct {
label *CompactionGroupLabel
segments []*SegmentView
triggerID int64
collectionTTL time.Duration
configMaxSize float64
topology *CollectionTopology
targetSize float64
targetCount int64
}
func (v *ForceMergeSegmentView) GetGroupLabel() *CompactionGroupLabel {
return v.label
}
func (v *ForceMergeSegmentView) GetSegmentsView() []*SegmentView {
return v.segments
}
func (v *ForceMergeSegmentView) Append(segments ...*SegmentView) {
v.segments = append(v.segments, segments...)
}
func (v *ForceMergeSegmentView) String() string {
return fmt.Sprintf("ForceMerge: %s, segments=%d, triggerID=%d",
v.label, len(v.segments), v.triggerID)
}
func (v *ForceMergeSegmentView) Trigger() (CompactionView, string) {
panic("implement me")
}
func (v *ForceMergeSegmentView) ForceTrigger() (CompactionView, string) {
panic("implement me")
}
func (v *ForceMergeSegmentView) GetTriggerID() int64 {
return v.triggerID
}
func (v *ForceMergeSegmentView) calculateTargetSizeCount() (maxSafeSize float64, targetCount int64) {
log := log.With(zap.Int64("triggerID", v.triggerID), zap.String("label", v.label.String()))
maxSafeSize = v.calculateMaxSafeSize()
if maxSafeSize < v.configMaxSize {
log.Info("maxSafeSize is less than configMaxSize, set to configMaxSize",
zap.Float64("targetSize", maxSafeSize),
zap.Float64("configMaxSize", v.configMaxSize))
maxSafeSize = v.configMaxSize
}
targetCount = max(1, int64(sumSegmentSize(v.segments)/maxSafeSize))
log.Info("topology-aware force merge calculation",
zap.Int64("targetSegmentCount", targetCount),
zap.Float64("maxSafeSize", maxSafeSize))
return maxSafeSize, targetCount
}
func (v *ForceMergeSegmentView) ForceTriggerAll() ([]CompactionView, string) {
targetSizePerSegment, targetCount := v.calculateTargetSizeCount()
groups := adaptiveGroupSegments(v.segments, float64(targetSizePerSegment))
results := make([]CompactionView, 0, len(groups))
for _, group := range groups {
results = append(results, &ForceMergeSegmentView{
label: v.label,
segments: group,
triggerID: v.triggerID,
collectionTTL: v.collectionTTL,
configMaxSize: v.configMaxSize,
targetSize: targetSizePerSegment,
targetCount: targetCount,
topology: v.topology,
})
}
return results, "force merge trigger"
}
// adaptiveGroupSegments automatically selects the best grouping algorithm based on segment count
// For small segment counts (≤ threshold), use maxFull for optimal full segment count
// For large segment counts, use larger for better performance
func adaptiveGroupSegments(segments []*SegmentView, targetSize float64) [][]*SegmentView {
if len(segments) == 0 {
return nil
}
n := len(segments)
// Get threshold from config, fallback to default if not available
threshold := paramtable.Get().DataCoordCfg.CompactionMaxFullSegmentThreshold.GetAsInt()
// Use maxFull for small segment counts to maximize full segments
// Use larger for large segment counts for O(n) performance
if n <= threshold {
return maxFullSegmentsGrouping(segments, targetSize)
}
return largerGroupingSegments(segments, targetSize)
}
// largerGroupingSegments groups segments to minimize number of tasks
// Strategy: Create larger groups that produce multiple full target-sized segments
// This approach favors fewer compaction tasks with larger batches
func largerGroupingSegments(segments []*SegmentView, targetSize float64) [][]*SegmentView {
if len(segments) == 0 {
return nil
}
n := len(segments)
// Pre-allocate with estimated capacity to reduce allocations
estimatedGroups := max(1, n/10)
groups := make([][]*SegmentView, 0, estimatedGroups)
i := 0
for i < n {
groupStart := i
groupSize := 0.0
// Accumulate segments to form multiple target-sized outputs
for i < n {
groupSize += segments[i].Size
i++
// Check if we should stop
if i < n {
nextSize := groupSize + segments[i].Size
currentFull := int(groupSize / targetSize)
nextFull := int(nextSize / targetSize)
// Stop if we have full segments and next addition won't give another full segment
if currentFull > 0 && nextFull == currentFull {
currentRemainder := math.Mod(groupSize, targetSize)
if currentRemainder < targetSize*defaultToleranceMB {
break
}
}
}
}
groups = append(groups, segments[groupStart:i])
}
return groups
}
// maxFullSegmentsGrouping groups segments to maximize number of full target-sized outputs
// Strategy: Use dynamic programming to find partitioning that produces most full segments
// This approach minimizes tail segments and achieves best space utilization
func maxFullSegmentsGrouping(segments []*SegmentView, targetSize float64) [][]*SegmentView {
if len(segments) == 0 {
return nil
}
n := len(segments)
// Pre-compute prefix sums to avoid repeated summation
prefixSum := make([]float64, n+1)
for i := 0; i < n; i++ {
prefixSum[i+1] = prefixSum[i] + segments[i].Size
}
// dp[i] = best result for segments[0:i]
type dpState struct {
fullSegments int
tailSegments int
numGroups int
groupIndices []int
}
dp := make([]dpState, n+1)
dp[0] = dpState{fullSegments: 0, tailSegments: 0, numGroups: 0, groupIndices: make([]int, 0, n/10)}
for i := 1; i <= n; i++ {
dp[i] = dpState{fullSegments: -1, tailSegments: math.MaxInt32, numGroups: 0, groupIndices: nil}
// Try different starting positions for the last group
for j := 0; j < i; j++ {
// Calculate group size from j to i-1 using prefix sums (O(1) instead of O(n))
groupSize := prefixSum[i] - prefixSum[j]
numFull := int(groupSize / targetSize)
remainder := math.Mod(groupSize, targetSize)
hasTail := 0
if remainder > 0.01 {
hasTail = 1
}
newFull := dp[j].fullSegments + numFull
newTails := dp[j].tailSegments + hasTail
newGroups := dp[j].numGroups + 1
// Prioritize: more full segments, then fewer tails, then more groups (for parallelism)
isBetter := false
if newFull > dp[i].fullSegments {
isBetter = true
} else if newFull == dp[i].fullSegments && newTails < dp[i].tailSegments {
isBetter = true
} else if newFull == dp[i].fullSegments && newTails == dp[i].tailSegments && newGroups > dp[i].numGroups {
isBetter = true
}
if isBetter {
// Pre-allocate with exact capacity to avoid reallocation
newIndices := make([]int, 0, len(dp[j].groupIndices)+1)
newIndices = append(newIndices, dp[j].groupIndices...)
newIndices = append(newIndices, j)
dp[i] = dpState{
fullSegments: newFull,
tailSegments: newTails,
numGroups: newGroups,
groupIndices: newIndices,
}
}
}
}
// Reconstruct groups from indices
if dp[n].groupIndices == nil {
return [][]*SegmentView{segments}
}
groupStarts := append(dp[n].groupIndices, n)
groups := make([][]*SegmentView, 0, len(groupStarts))
for i := 0; i < len(groupStarts); i++ {
start := groupStarts[i]
end := n
if i+1 < len(groupStarts) {
end = groupStarts[i+1]
}
if start < end {
groups = append(groups, segments[start:end])
}
}
return groups
}
func (v *ForceMergeSegmentView) calculateMaxSafeSize() float64 {
log := log.With(zap.Int64("triggerID", v.triggerID), zap.String("label", v.label.String()))
if len(v.topology.QueryNodeMemory) == 0 || len(v.topology.DataNodeMemory) == 0 {
log.Warn("No querynodes or datanodes in topology, using config size")
return v.configMaxSize
}
// QueryNode constraint: use global minimum memory
querynodeMemoryFactor := paramtable.Get().DataCoordCfg.CompactionForceMergeQueryNodeMemoryFactor.GetAsFloat()
qnMaxSafeSize := float64(lo.Min(lo.Values(v.topology.QueryNodeMemory))) / querynodeMemoryFactor
// DataNode constraint: segments must fit in smallest DataNode
datanodeMemoryFactor := paramtable.Get().DataCoordCfg.CompactionForceMergeDataNodeMemoryFactor.GetAsFloat()
dnMaxSafeSize := float64(lo.Min(lo.Values(v.topology.DataNodeMemory))) / datanodeMemoryFactor
maxSafeSize := min(qnMaxSafeSize, dnMaxSafeSize)
if v.topology.IsStandaloneMode && !v.topology.IsPooling {
log.Info("force merge on standalone not pooling mode, half the max size",
zap.Float64("qnMaxSafeSize", qnMaxSafeSize),
zap.Float64("dnMaxSafeSize", dnMaxSafeSize),
zap.Float64("maxSafeSize/2", maxSafeSize/2),
zap.Float64("configMaxSize", v.configMaxSize))
// dn and qn are co-located, half the min
return maxSafeSize * 0.5
}
log.Info("force merge on cluster/pooling mode",
zap.Float64("qnMaxSafeSize", qnMaxSafeSize),
zap.Float64("dnMaxSafeSize", dnMaxSafeSize),
zap.Float64("maxSafeSize", maxSafeSize),
zap.Float64("configMaxSize", v.configMaxSize))
return maxSafeSize
}
func sumSegmentSize(views []*SegmentView) float64 {
return lo.SumBy(views, func(v *SegmentView) float64 { return v.Size })
}