mirror of
https://gitee.com/milvus-io/milvus.git
synced 2025-12-28 14:35:27 +08:00
### **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>
311 lines
9.7 KiB
Go
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 })
|
|
}
|