Files
wehub-resource-sync 498b235461
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:17 +08:00

1147 lines
39 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 compactor
import (
"context"
"fmt"
sio "io"
"math"
"path"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/samber/lo"
"go.opentelemetry.io/otel"
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/allocator"
"github.com/milvus-io/milvus/internal/compaction"
"github.com/milvus-io/milvus/internal/flushcommon/io"
"github.com/milvus-io/milvus/internal/metastore/kv/binlog"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/metrics"
"github.com/milvus-io/milvus/pkg/v3/mlog"
"github.com/milvus-io/milvus/pkg/v3/proto/clusteringpb"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
"github.com/milvus-io/milvus/pkg/v3/util/conc"
"github.com/milvus-io/milvus/pkg/v3/util/funcutil"
"github.com/milvus-io/milvus/pkg/v3/util/hardware"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/metautil"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/timerecord"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
const (
expectedBinlogSize = 16 * 1024 * 1024
)
var _ Compactor = (*clusteringCompactionTask)(nil)
type clusteringCompactionTask struct {
binlogIO io.BinlogIO
logIDAlloc allocator.Interface
segIDAlloc allocator.Interface
ctx context.Context
cancel context.CancelFunc
done chan struct{}
tr *timerecord.TimeRecorder
mappingPool *conc.Pool[any]
flushPool *conc.Pool[any]
plan *datapb.CompactionPlan
// flush
flushCount *atomic.Int64
// metrics, don't use
writtenRowNum *atomic.Int64
// inner field
collectionID int64
partitionID int64
currentTime time.Time // for TTL
isVectorClusteringKey bool
clusteringKeyField *schemapb.FieldSchema
primaryKeyField *schemapb.FieldSchema
ttlFieldID int64
memoryLimit int64
bufferSize int64
clusterBuffers []*ClusterBuffer
// scalar
keyToBufferFunc func(interface{}) *ClusterBuffer
// vector
segmentIDOffsetMapping map[int64]string
offsetToBufferFunc func(int64, []uint32) *ClusterBuffer
// bm25
bm25FieldIds []int64
compactionParams compaction.Params
// lobContext holds LOB compaction strategy decisions for TEXT columns
lobContext *compaction.LOBCompactionContext
}
type ClusterBuffer struct {
id int
writer *MultiSegmentWriter
clusteringKeyFieldStats *storage.FieldStats
lock sync.RWMutex
}
func (b *ClusterBuffer) Write(v *storage.Value) error {
b.lock.Lock()
defer b.lock.Unlock()
return b.writer.WriteValue(v)
}
func (b *ClusterBuffer) Flush() error {
b.lock.Lock()
defer b.lock.Unlock()
return b.writer.Flush()
}
func (b *ClusterBuffer) FlushChunk() error {
b.lock.Lock()
defer b.lock.Unlock()
return b.writer.FlushChunk()
}
func (b *ClusterBuffer) Close() error {
b.lock.Lock()
defer b.lock.Unlock()
return b.writer.Close()
}
func (b *ClusterBuffer) GetCompactionSegments() []*datapb.CompactionSegment {
b.lock.RLock()
defer b.lock.RUnlock()
return b.writer.GetCompactionSegments()
}
func (b *ClusterBuffer) GetBufferSize() uint64 {
b.lock.RLock()
defer b.lock.RUnlock()
return b.writer.GetBufferUncompressed()
}
func newClusterBuffer(id int, writer *MultiSegmentWriter, clusteringKeyFieldStats *storage.FieldStats) *ClusterBuffer {
return &ClusterBuffer{
id: id,
writer: writer,
clusteringKeyFieldStats: clusteringKeyFieldStats,
lock: sync.RWMutex{},
}
}
func NewClusteringCompactionTask(
ctx context.Context,
binlogIO io.BinlogIO,
plan *datapb.CompactionPlan,
compactionParams compaction.Params,
) *clusteringCompactionTask {
ctx, cancel := context.WithCancel(ctx)
return &clusteringCompactionTask{
ctx: ctx,
cancel: cancel,
binlogIO: binlogIO,
plan: plan,
tr: timerecord.NewTimeRecorder("clustering_compaction"),
done: make(chan struct{}, 1),
clusterBuffers: make([]*ClusterBuffer, 0),
flushCount: atomic.NewInt64(0),
writtenRowNum: atomic.NewInt64(0),
compactionParams: compactionParams,
}
}
func (t *clusteringCompactionTask) Complete() {
t.done <- struct{}{}
}
func (t *clusteringCompactionTask) Stop() {
t.cancel()
<-t.done
}
func (t *clusteringCompactionTask) GetPlanID() typeutil.UniqueID {
return t.plan.GetPlanID()
}
func (t *clusteringCompactionTask) GetChannelName() string {
return t.plan.GetChannel()
}
func (t *clusteringCompactionTask) GetCompactionType() datapb.CompactionType {
return t.plan.GetType()
}
func (t *clusteringCompactionTask) GetCollection() int64 {
return t.plan.GetSegmentBinlogs()[0].GetCollectionID()
}
func (t *clusteringCompactionTask) init() error {
if t.plan.GetType() != datapb.CompactionType_ClusteringCompaction {
return merr.WrapErrIllegalCompactionPlan("illegal compaction type")
}
if len(t.plan.GetSegmentBinlogs()) == 0 {
return merr.WrapErrIllegalCompactionPlan("empty segment binlogs")
}
t.collectionID = t.GetCollection()
t.partitionID = t.plan.GetSegmentBinlogs()[0].GetPartitionID()
logIDAlloc := allocator.NewLocalAllocator(t.plan.GetPreAllocatedLogIDs().GetBegin(), t.plan.GetPreAllocatedLogIDs().GetEnd())
segIDAlloc := allocator.NewLocalAllocator(t.plan.GetPreAllocatedSegmentIDs().GetBegin(), t.plan.GetPreAllocatedSegmentIDs().GetEnd())
mlog.Info(context.TODO(), "segment ID range", mlog.Int64("begin", t.plan.GetPreAllocatedSegmentIDs().GetBegin()), mlog.Int64("end", t.plan.GetPreAllocatedSegmentIDs().GetEnd()))
t.logIDAlloc = logIDAlloc
t.segIDAlloc = segIDAlloc
var pkField *schemapb.FieldSchema
if t.plan.Schema == nil {
return merr.WrapErrIllegalCompactionPlan("empty schema in compactionPlan")
}
for _, field := range t.plan.Schema.Fields {
if field.GetIsPrimaryKey() && field.GetFieldID() >= 100 && typeutil.IsPrimaryFieldType(field.GetDataType()) {
pkField = field
}
if field.GetFieldID() == t.plan.GetClusteringKeyField() {
t.clusteringKeyField = field
}
}
for _, function := range t.plan.Schema.Functions {
if function.GetType() == schemapb.FunctionType_BM25 {
t.bm25FieldIds = append(t.bm25FieldIds, function.GetOutputFieldIds()[0])
}
}
t.primaryKeyField = pkField
t.ttlFieldID = getTTLFieldID(t.plan.GetSchema())
t.isVectorClusteringKey = typeutil.IsVectorType(t.clusteringKeyField.DataType)
t.currentTime = time.Now()
t.memoryLimit = t.getMemoryLimit()
t.bufferSize = int64(t.compactionParams.BinLogMaxSize) // Use binlog max size as read and write buffer size
workerPoolSize := t.getWorkerPoolSize()
t.mappingPool = conc.NewPool[any](workerPoolSize)
t.flushPool = conc.NewPool[any](workerPoolSize)
mlog.Info(context.TODO(), "clustering compaction task initialed", mlog.Int64("memory_buffer_size", t.memoryLimit), mlog.Int("worker_pool_size", workerPoolSize))
return nil
}
func (t *clusteringCompactionTask) Compact() (*datapb.CompactionPlanResult, error) {
ctx, span := otel.Tracer(typeutil.DataNodeRole).Start(t.ctx, fmt.Sprintf("clusteringCompaction-%d", t.GetPlanID()))
defer span.End()
// 0, verify and init
err := t.init()
if err != nil {
mlog.Error(context.TODO(), "compaction task init failed", mlog.Err(err))
return nil, err
}
// 0.5, init LOB compaction context for TEXT columns (if any)
if err := t.initLOBCompactionContext(ctx); err != nil {
mlog.Error(context.TODO(), "failed to init LOB compaction context", mlog.Err(err))
return nil, err
}
if !funcutil.CheckCtxValid(ctx) {
mlog.Warn(context.TODO(), "compact wrong, task context done or timeout")
return nil, ctx.Err()
}
defer t.cleanUp(ctx)
// 1, decompose binlogs as preparation for later mapping
if err := binlog.DecompressCompactionBinlogsWithRootPath(t.compactionParams.StorageConfig.GetRootPath(), t.plan.SegmentBinlogs); err != nil {
mlog.Warn(context.TODO(), "compact wrong, fail to decompress compaction binlogs", mlog.Err(err))
return nil, err
}
// 2, get analyze result
if t.isVectorClusteringKey {
if err := t.getVectorAnalyzeResult(ctx); err != nil {
mlog.Error(context.TODO(), "failed in analyze vector", mlog.Err(err))
return nil, err
}
} else {
if err := t.getScalarAnalyzeResult(ctx); err != nil {
mlog.Error(context.TODO(), "failed in analyze scalar", mlog.Err(err))
return nil, err
}
}
// 3, mapping
mlog.Info(context.TODO(), "Clustering compaction start mapping", mlog.Int("bufferNum", len(t.clusterBuffers)))
uploadSegments, partitionStats, err := t.mapping(ctx)
if err != nil {
mlog.Error(context.TODO(), "failed in mapping", mlog.Err(err))
return nil, err
}
// 4, collect partition stats
err = t.uploadPartitionStats(ctx, t.collectionID, t.partitionID, partitionStats)
if err != nil {
return nil, err
}
// 5, assemble CompactionPlanResult
planResult := &datapb.CompactionPlanResult{
State: datapb.CompactionTaskState_completed,
PlanID: t.GetPlanID(),
Segments: uploadSegments,
Type: t.plan.GetType(),
Channel: t.plan.GetChannel(),
}
metrics.DataNodeCompactionLatency.
WithLabelValues(paramtable.GetStringNodeID(), t.plan.GetType().String()).
Observe(float64(t.tr.ElapseSpan().Milliseconds()))
mlog.Info(context.TODO(), "Clustering compaction finished", mlog.Duration("elapse", t.tr.ElapseSpan()), mlog.Int64("flushTimes", t.flushCount.Load()))
// clear the buffer cache
t.keyToBufferFunc = nil
return planResult, nil
}
func (t *clusteringCompactionTask) getScalarAnalyzeResult(ctx context.Context) error {
ctx, span := otel.Tracer(typeutil.DataNodeRole).Start(ctx, fmt.Sprintf("getScalarAnalyzeResult-%d", t.GetPlanID()))
defer span.End()
analyzeDict, err := t.scalarAnalyze(ctx)
if err != nil {
return err
}
buckets, containsNull := t.splitClusterByScalarValue(analyzeDict)
scalarToClusterBufferMap := make(map[interface{}]*ClusterBuffer, 0)
for id, bucket := range buckets {
fieldStats, err := storage.NewFieldStats(t.clusteringKeyField.FieldID, t.clusteringKeyField.DataType, 0)
if err != nil {
return err
}
for _, key := range bucket {
fieldStats.UpdateMinMax(storage.NewScalarFieldValue(t.clusteringKeyField.DataType, key))
}
alloc := NewCompactionAllocator(t.segIDAlloc, t.logIDAlloc)
writer, err := NewMultiSegmentWriter(ctx, t.binlogIO, alloc,
t.plan.GetMaxSize(), t.plan.GetSchema(), t.compactionParams, t.plan.MaxSegmentRows,
t.partitionID, t.collectionID, t.plan.Channel, 100,
t.getWriterOpts()...,
)
if err != nil {
return err
}
buffer := newClusterBuffer(id, writer, fieldStats)
t.clusterBuffers = append(t.clusterBuffers, buffer)
for _, key := range bucket {
scalarToClusterBufferMap[key] = buffer
}
}
var nullBuffer *ClusterBuffer
if containsNull {
fieldStats, err := storage.NewFieldStats(t.clusteringKeyField.FieldID, t.clusteringKeyField.DataType, 0)
if err != nil {
return err
}
alloc := NewCompactionAllocator(t.segIDAlloc, t.logIDAlloc)
writer, err := NewMultiSegmentWriter(ctx, t.binlogIO, alloc,
t.plan.GetMaxSize(), t.plan.GetSchema(), t.compactionParams, t.plan.MaxSegmentRows,
t.partitionID, t.collectionID, t.plan.Channel, 100,
t.getWriterOpts()...,
)
if err != nil {
return err
}
nullBuffer = newClusterBuffer(len(buckets), writer, fieldStats)
t.clusterBuffers = append(t.clusterBuffers, nullBuffer)
}
t.keyToBufferFunc = func(key interface{}) *ClusterBuffer {
if key == nil {
return nullBuffer
}
// todo: if keys are too many, the map will be quite large, we should mark the range of each buffer and select buffer by range
return scalarToClusterBufferMap[key]
}
return nil
}
func splitCentroids(centroids []int, num int) ([][]int, map[int]int) {
if num <= 0 {
return nil, nil
}
result := make([][]int, num)
resultIndex := make(map[int]int, len(centroids))
listLen := len(centroids)
for i := 0; i < listLen; i++ {
group := i % num
result[group] = append(result[group], centroids[i])
resultIndex[i] = group
}
return result, resultIndex
}
func (t *clusteringCompactionTask) generatedVectorPlan(ctx context.Context, bufferNum int, centroids []*schemapb.VectorField) error {
centroidsOffset := make([]int, len(centroids))
for i := 0; i < len(centroids); i++ {
centroidsOffset[i] = i
}
centroidGroups, groupIndex := splitCentroids(centroidsOffset, bufferNum)
for id, group := range centroidGroups {
fieldStats, err := storage.NewFieldStats(t.clusteringKeyField.FieldID, t.clusteringKeyField.DataType, 0)
if err != nil {
return err
}
centroidValues := make([]storage.VectorFieldValue, len(group))
for i, offset := range group {
centroidValues[i] = storage.NewVectorFieldValue(t.clusteringKeyField.DataType, centroids[offset])
}
fieldStats.SetVectorCentroids(centroidValues...)
alloc := NewCompactionAllocator(t.segIDAlloc, t.logIDAlloc)
writer, err := NewMultiSegmentWriter(ctx, t.binlogIO, alloc,
t.plan.GetMaxSize(), t.plan.GetSchema(), t.compactionParams, t.plan.MaxSegmentRows,
t.partitionID, t.collectionID, t.plan.Channel, 100,
t.getWriterOpts()...,
)
if err != nil {
return err
}
buffer := newClusterBuffer(id, writer, fieldStats)
t.clusterBuffers = append(t.clusterBuffers, buffer)
}
t.offsetToBufferFunc = func(offset int64, idMapping []uint32) *ClusterBuffer {
centroidGroupOffset := groupIndex[int(idMapping[offset])]
return t.clusterBuffers[centroidGroupOffset]
}
return nil
}
func (t *clusteringCompactionTask) switchPolicyForVectorPlan(ctx context.Context, centroids *clusteringpb.ClusteringCentroidsStats) error {
bufferNum := len(centroids.GetCentroids())
bufferNumByMemory := int(t.memoryLimit / expectedBinlogSize)
if bufferNumByMemory < bufferNum {
bufferNum = bufferNumByMemory
}
return t.generatedVectorPlan(ctx, bufferNum, centroids.GetCentroids())
}
func (t *clusteringCompactionTask) getVectorAnalyzeResult(ctx context.Context) error {
ctx, span := otel.Tracer(typeutil.DataNodeRole).Start(ctx, fmt.Sprintf("getVectorAnalyzeResult-%d", t.GetPlanID()))
defer span.End()
analyzeResultPath := t.plan.AnalyzeResultPath
centroidFilePath := path.Join(analyzeResultPath, metautil.JoinIDPath(t.collectionID, t.partitionID, t.clusteringKeyField.FieldID), common.Centroids)
offsetMappingFiles := make(map[int64]string, 0)
for _, segmentID := range t.plan.AnalyzeSegmentIds {
path := path.Join(analyzeResultPath, metautil.JoinIDPath(t.collectionID, t.partitionID, t.clusteringKeyField.FieldID, segmentID), common.OffsetMapping)
offsetMappingFiles[segmentID] = path
mlog.Debug(context.TODO(), "read segment offset mapping file", mlog.Int64("segmentID", segmentID), mlog.String("path", path))
}
t.segmentIDOffsetMapping = offsetMappingFiles
centroidBytes, err := t.binlogIO.Download(ctx, []string{centroidFilePath})
if err != nil {
return err
}
centroids := &clusteringpb.ClusteringCentroidsStats{}
err = proto.Unmarshal(centroidBytes[0], centroids)
if err != nil {
return err
}
mlog.Debug(context.TODO(), "read clustering centroids stats", mlog.String("path", centroidFilePath),
mlog.Int("centroidNum", len(centroids.GetCentroids())),
mlog.Any("offsetMappingFiles", t.segmentIDOffsetMapping))
return t.switchPolicyForVectorPlan(ctx, centroids)
}
// mapping read and split input segments into buffers
func (t *clusteringCompactionTask) mapping(ctx context.Context,
) ([]*datapb.CompactionSegment, *storage.PartitionStatsSnapshot, error) {
ctx, span := otel.Tracer(typeutil.DataNodeRole).Start(ctx, fmt.Sprintf("mapping-%d", t.GetPlanID()))
defer span.End()
inputSegments := t.plan.GetSegmentBinlogs()
mapStart := time.Now()
futures := make([]*conc.Future[any], 0, len(inputSegments))
for _, segment := range inputSegments {
segmentClone := &datapb.CompactionSegmentBinlogs{
SegmentID: segment.SegmentID,
// only FieldBinlogs and deltalogs needed
Deltalogs: segment.Deltalogs,
FieldBinlogs: segment.FieldBinlogs,
StorageVersion: segment.StorageVersion,
Manifest: segment.GetManifest(),
}
future := t.mappingPool.Submit(func() (any, error) {
err := t.mappingSegment(ctx, segmentClone)
return struct{}{}, err
})
futures = append(futures, future)
}
if err := conc.AwaitAll(futures...); err != nil {
return nil, nil, err
}
// force flush all buffers
err := t.flushAll()
if err != nil {
return nil, nil, err
}
resultSegments := make([]*datapb.CompactionSegment, 0)
resultPartitionStats := &storage.PartitionStatsSnapshot{
SegmentStats: make(map[typeutil.UniqueID]storage.SegmentStats),
}
for _, buffer := range t.clusterBuffers {
segments := buffer.GetCompactionSegments()
mlog.Debug(context.TODO(), "compaction segments", mlog.Any("segments", segments))
resultSegments = append(resultSegments, segments...)
for _, segment := range segments {
segmentStats := storage.SegmentStats{
FieldStats: []storage.FieldStats{buffer.clusteringKeyFieldStats.Clone()},
NumRows: int(segment.NumOfRows),
}
resultPartitionStats.SegmentStats[segment.SegmentID] = segmentStats
mlog.Debug(context.TODO(), "compaction segment partitioning stats", mlog.Int64("segmentID", segment.SegmentID), mlog.Any("stats", segmentStats))
}
}
mlog.Info(context.TODO(), "mapping end",
mlog.Int64("collectionID", t.GetCollection()),
mlog.Int64("partitionID", t.partitionID),
mlog.Int("segmentFrom", len(inputSegments)),
mlog.Int("segmentTo", len(resultSegments)),
mlog.Duration("elapse", time.Since(mapStart)))
return resultSegments, resultPartitionStats, nil
}
func (t *clusteringCompactionTask) getBufferTotalUsedMemorySize() int64 {
var totalBufferSize int64 = 0
for _, buffer := range t.clusterBuffers {
totalBufferSize = totalBufferSize + int64(buffer.GetBufferSize())
}
return totalBufferSize
}
// read insert log of one segment, mappingSegment into buckets according to clusteringKey. flush data to file when necessary
func (t *clusteringCompactionTask) mappingSegment(
ctx context.Context,
segment *datapb.CompactionSegmentBinlogs,
) error {
ctx, span := otel.Tracer(typeutil.DataNodeRole).Start(ctx, fmt.Sprintf("mappingSegment-%d-%d", t.GetPlanID(), segment.GetSegmentID()))
defer span.End()
mlog.Info(context.TODO(), "mapping segment start")
processStart := time.Now()
var remained int64 = 0
options := []storage.RwOption{
storage.WithDownloader(t.binlogIO.Download),
storage.WithStorageConfig(t.compactionParams.StorageConfig),
}
delta, err := compaction.ComposeDeleteFromDeltalogs(ctx, t.primaryKeyField.DataType, segment, options...)
if err != nil {
return err
}
entityFilter := compaction.NewEntityFilter(delta, t.plan.GetCollectionTtl(), t.currentTime, segment.GetCommitTimestamp())
mappingStats := &clusteringpb.ClusteringCentroidIdMappingStats{}
if t.isVectorClusteringKey {
offSetPath := t.segmentIDOffsetMapping[segment.SegmentID]
offsetBytes, err := t.binlogIO.Download(ctx, []string{offSetPath})
if err != nil {
return err
}
err = proto.Unmarshal(offsetBytes[0], mappingStats)
if err != nil {
return err
}
}
// Get the number of field binlog files from non-empty segment
var binlogNum int
for _, b := range segment.GetFieldBinlogs() {
if b != nil {
binlogNum = len(b.GetBinlogs())
break
}
}
// Unable to deal with all empty segments cases, so return error
if binlogNum == 0 {
mlog.Warn(context.TODO(), "compact wrong, all segments' binlogs are empty")
return merr.WrapErrIllegalCompactionPlan()
}
rr, existingFields, err := newCompactionSegmentRecordReader(ctx, segment, t.plan.Schema, t.compactionParams.StorageConfig,
storage.WithDownloader(func(ctx context.Context, paths []string) ([][]byte, error) {
return t.binlogIO.Download(ctx, paths)
}),
storage.WithCollectionID(t.GetCollection()),
storage.WithVersion(segment.StorageVersion),
storage.WithBufferSize(t.bufferSize),
storage.WithStorageConfig(t.compactionParams.StorageConfig),
)
if err != nil {
mlog.Warn(context.TODO(), "new binlog record reader wrong", mlog.Err(err))
return err
}
materializer, err := NewRecordMaterializer(t.plan.Schema, t.plan.Schema.GetFunctions(), existingFields)
if err != nil {
rr.Close()
mlog.Warn(ctx, "new record materializer wrong", mlog.Err(err))
return err
}
rr = newMaterializedRecordReader(rr, materializer)
defer rr.Close()
hasTTLField := t.ttlFieldID >= common.StartOfUserFieldID
offset := int64(-1)
for {
r, err := rr.Next()
if err != nil {
if err == sio.EOF {
break
}
mlog.Warn(context.TODO(), "compact wrong, failed to iter through data", mlog.Err(err))
return err
}
vs := make([]*storage.Value, r.Len())
if err = storage.ValueDeserializerWithSchema(r, vs, t.plan.Schema, true); err != nil {
mlog.Warn(context.TODO(), "compact wrong, failed to deserialize data", mlog.Err(err))
return err
}
for _, v := range vs {
offset++
row, ok := v.Value.(map[typeutil.UniqueID]interface{})
if !ok {
mlog.Warn(context.TODO(), "convert interface to map wrong")
return merr.WrapErrServiceInternalMsg("unexpected error")
}
expireTs := int64(-1)
if hasTTLField {
if val, exists := row[t.ttlFieldID]; exists {
if v, ok := val.(int64); ok {
expireTs = v
}
}
}
if entityFilter.Filtered(v.PK.GetValue(), uint64(v.Timestamp), expireTs) {
continue
}
// Normalize import segment timestamps: overwrite to commit_ts
// so the output segment becomes a normal segment (commit_ts = 0).
if commitTs := segment.GetCommitTimestamp(); commitTs != 0 {
v.Timestamp = int64(commitTs)
}
clusteringKey := row[t.clusteringKeyField.FieldID]
var clusterBuffer *ClusterBuffer
if t.isVectorClusteringKey {
clusterBuffer = t.offsetToBufferFunc(offset, mappingStats.GetCentroidIdMapping())
} else {
clusterBuffer = t.keyToBufferFunc(clusteringKey)
}
if err := clusterBuffer.Write(v); err != nil {
return err
}
t.writtenRowNum.Inc()
remained++
if (remained+1)%100 == 0 {
currentBufferTotalMemorySize := t.getBufferTotalUsedMemorySize()
if currentBufferTotalMemorySize > t.getMemoryBufferHighWatermark() {
// reach flushBinlog trigger threshold
mlog.Debug(context.TODO(), "largest buffer need to flush",
mlog.Int64("currentBufferTotalMemorySize", currentBufferTotalMemorySize))
if err := t.flushLargestBuffers(ctx); err != nil {
return err
}
}
}
}
// all cluster buffers are flushed for a certain record, since the values read from the same record are references instead of copies
for _, buffer := range t.clusterBuffers {
buffer.Flush()
}
}
missing := entityFilter.GetMissingDeleteCount()
mlog.Info(context.TODO(), "mapping segment end",
mlog.Int64("remained_entities", remained),
mlog.Int("deleted_entities", entityFilter.GetDeletedCount()),
mlog.Int("expired_entities", entityFilter.GetExpiredCount()),
mlog.Int("deltalog deletes", entityFilter.GetDeltalogDeleteCount()),
mlog.Int("missing deletes", missing),
mlog.Int64("written_row_num", t.writtenRowNum.Load()),
mlog.Duration("elapse", time.Since(processStart)))
metrics.DataNodeCompactionDeleteCount.WithLabelValues(fmt.Sprint(t.collectionID)).Add(float64(entityFilter.GetDeltalogDeleteCount()))
metrics.DataNodeCompactionMissingDeleteCount.WithLabelValues(fmt.Sprint(t.collectionID)).Add(float64(missing))
return nil
}
func (t *clusteringCompactionTask) getWorkerPoolSize() int {
return int(math.Max(float64(paramtable.Get().DataNodeCfg.ClusteringCompactionWorkerPoolSize.GetAsInt()), 1.0))
}
// getMemoryLimit returns the maximum memory that a clustering compaction task is allowed to use
func (t *clusteringCompactionTask) getMemoryLimit() int64 {
return int64(float64(hardware.GetMemoryCount()) * paramtable.Get().DataNodeCfg.ClusteringCompactionMemoryBufferRatio.GetAsFloat())
}
func (t *clusteringCompactionTask) getMemoryBufferLowWatermark() int64 {
return int64(float64(t.memoryLimit) * 0.3)
}
func (t *clusteringCompactionTask) getMemoryBufferHighWatermark() int64 {
return int64(float64(t.memoryLimit) * 0.7)
}
func (t *clusteringCompactionTask) flushLargestBuffers(ctx context.Context) error {
currentMemorySize := t.getBufferTotalUsedMemorySize()
if currentMemorySize <= t.getMemoryBufferLowWatermark() {
mlog.Info(context.TODO(), "memory low water mark", mlog.Int64("memoryBufferSize", currentMemorySize))
return nil
}
_, span := otel.Tracer(typeutil.DataNodeRole).Start(ctx, "flushLargestBuffers")
defer span.End()
bufferIDs := make([]int, 0)
bufferSizes := make([]int64, 0)
for _, buffer := range t.clusterBuffers {
bufferIDs = append(bufferIDs, buffer.id)
bufferSizes = append(bufferSizes, int64(buffer.GetBufferSize()))
}
sort.Slice(bufferIDs, func(i, j int) bool {
return bufferSizes[bufferIDs[i]] > bufferSizes[bufferIDs[j]]
})
mlog.Info(context.TODO(), "start flushLargestBuffers", mlog.Ints("bufferIDs", bufferIDs), mlog.Int64("currentMemorySize", currentMemorySize))
futures := make([]*conc.Future[any], 0)
for _, bufferId := range bufferIDs {
buffer := t.clusterBuffers[bufferId]
size := buffer.GetBufferSize()
currentMemorySize -= int64(size)
mlog.Info(context.TODO(), "currentMemorySize after flush buffer binlog",
mlog.Int64("currentMemorySize", currentMemorySize),
mlog.Int("bufferID", bufferId),
mlog.Uint64("WrittenUncompressed", size))
future := t.flushPool.Submit(func() (any, error) {
err := buffer.FlushChunk()
if err != nil {
return nil, err
}
return struct{}{}, nil
})
futures = append(futures, future)
if currentMemorySize <= t.getMemoryBufferLowWatermark() {
mlog.Info(context.TODO(), "reach memory low water mark", mlog.Int64("memoryBufferSize", t.getBufferTotalUsedMemorySize()))
break
}
}
if err := conc.AwaitAll(futures...); err != nil {
return err
}
mlog.Info(context.TODO(), "flushLargestBuffers end", mlog.Int64("currentMemorySize", currentMemorySize))
return nil
}
func (t *clusteringCompactionTask) flushAll() error {
futures := make([]*conc.Future[any], 0)
for _, buffer := range t.clusterBuffers {
b := buffer // avoid closure mis-capture
future := t.flushPool.Submit(func() (any, error) {
err := b.Close()
if err != nil {
return nil, err
}
return struct{}{}, nil
})
futures = append(futures, future)
}
if err := conc.AwaitAll(futures...); err != nil {
return err
}
return nil
}
func (t *clusteringCompactionTask) uploadPartitionStats(ctx context.Context, collectionID, partitionID typeutil.UniqueID, partitionStats *storage.PartitionStatsSnapshot) error {
// use planID as partitionStats version
version := t.plan.PlanID
partitionStats.Version = version
partitionStatsBytes, err := storage.SerializePartitionStatsSnapshot(partitionStats)
if err != nil {
return err
}
rootPath := strings.Split(t.plan.AnalyzeResultPath, common.AnalyzeStatsPath)[0]
newStatsPath := path.Join(rootPath, common.PartitionStatsPath, metautil.JoinIDPath(collectionID, partitionID), t.plan.GetChannel(), strconv.FormatInt(version, 10))
kv := map[string][]byte{
newStatsPath: partitionStatsBytes,
}
err = t.binlogIO.Upload(ctx, kv)
if err != nil {
return err
}
mlog.Info(context.TODO(), "Finish upload PartitionStats file", mlog.String("key", newStatsPath), mlog.Int("length", len(partitionStatsBytes)))
return nil
}
// cleanUp try best to clean all temp datas
func (t *clusteringCompactionTask) cleanUp(ctx context.Context) {
if t.mappingPool != nil {
t.mappingPool.Release()
}
if t.flushPool != nil {
t.flushPool.Release()
}
}
func (t *clusteringCompactionTask) scalarAnalyze(ctx context.Context) (map[interface{}]int64, error) {
ctx, span := otel.Tracer(typeutil.DataNodeRole).Start(ctx, fmt.Sprintf("scalarAnalyze-%d", t.GetPlanID()))
defer span.End()
inputSegments := t.plan.GetSegmentBinlogs()
futures := make([]*conc.Future[any], 0, len(inputSegments))
analyzeStart := time.Now()
var mutex sync.Mutex
analyzeDict := make(map[interface{}]int64, 0)
for _, segment := range inputSegments {
segmentClone := proto.Clone(segment).(*datapb.CompactionSegmentBinlogs)
future := t.mappingPool.Submit(func() (any, error) {
analyzeResult, err := t.scalarAnalyzeSegment(ctx, segmentClone)
mutex.Lock()
defer mutex.Unlock()
for key, v := range analyzeResult {
if _, exist := analyzeDict[key]; exist {
analyzeDict[key] = analyzeDict[key] + v
} else {
analyzeDict[key] = v
}
}
return struct{}{}, err
})
futures = append(futures, future)
}
if err := conc.AwaitAll(futures...); err != nil {
return nil, err
}
mlog.Info(context.TODO(), "analyze end",
mlog.Int64("collectionID", t.GetCollection()),
mlog.Int64("partitionID", t.partitionID),
mlog.Int("segments", len(inputSegments)),
mlog.Int("clustering num", len(analyzeDict)),
mlog.Duration("elapse", time.Since(analyzeStart)))
return analyzeDict, nil
}
func (t *clusteringCompactionTask) scalarAnalyzeSegment(
ctx context.Context,
segment *datapb.CompactionSegmentBinlogs,
) (map[interface{}]int64, error) {
ctx, span := otel.Tracer(typeutil.DataNodeRole).Start(ctx, fmt.Sprintf("scalarAnalyzeSegment-%d-%d", t.GetPlanID(), segment.GetSegmentID()))
defer span.End()
processStart := time.Now()
// Get the number of field binlog files from non-empty segment
var binlogNum int
for _, b := range segment.GetFieldBinlogs() {
if b != nil {
binlogNum = len(b.GetBinlogs())
break
}
}
// Unable to deal with all empty segments cases, so return error
if binlogNum == 0 {
mlog.Warn(context.TODO(), "compact wrong, all segments' binlogs are empty")
return nil, merr.WrapErrIllegalCompactionPlan("all segments' binlogs are empty")
}
mlog.Debug(context.TODO(), "binlogNum", mlog.Int("binlogNum", binlogNum))
expiredFilter := compaction.NewEntityFilter(nil, t.plan.GetCollectionTtl(), t.currentTime, segment.GetCommitTimestamp())
requiredFields := typeutil.NewSet[int64]()
requiredFields.Insert(0, 1, t.primaryKeyField.GetFieldID(), t.clusteringKeyField.GetFieldID())
if t.ttlFieldID >= common.StartOfUserFieldID {
requiredFields.Insert(t.ttlFieldID)
}
selectedFields := lo.Filter(t.plan.GetSchema().GetFields(), func(field *schemapb.FieldSchema, _ int) bool {
return requiredFields.Contain(field.GetFieldID())
})
readSchema := proto.Clone(t.plan.GetSchema()).(*schemapb.CollectionSchema)
readSchema.Fields = selectedFields
readSchema.StructArrayFields = nil
rr, _, err := newCompactionSegmentRecordReader(ctx, segment, readSchema, t.compactionParams.StorageConfig,
storage.WithDownloader(func(ctx context.Context, paths []string) ([][]byte, error) {
return t.binlogIO.Download(ctx, paths)
}),
storage.WithVersion(segment.StorageVersion),
storage.WithBufferSize(t.bufferSize),
storage.WithStorageConfig(t.compactionParams.StorageConfig),
storage.WithNeededFields(requiredFields),
storage.WithCollectionID(t.GetCollection()),
)
if err != nil {
mlog.Warn(context.TODO(), "new binlog record reader wrong", mlog.Err(err))
return make(map[interface{}]int64), err
}
pkIter := storage.NewDeserializeReader(rr, func(r storage.Record, v []*storage.Value) error {
return storage.ValueDeserializerWithSelectedFields(r, v, selectedFields, true)
})
defer pkIter.Close()
analyzeResult, remained, err := t.iterAndGetScalarAnalyzeResult(pkIter, expiredFilter)
if err != nil {
return nil, err
}
mlog.Info(context.TODO(), "analyze segment end",
mlog.Int64("remained entities", remained),
mlog.Int("expired entities", expiredFilter.GetExpiredCount()),
mlog.Duration("map elapse", time.Since(processStart)))
return analyzeResult, nil
}
func (t *clusteringCompactionTask) iterAndGetScalarAnalyzeResult(pkIter *storage.DeserializeReaderImpl[*storage.Value], expiredFilter compaction.EntityFilter) (map[interface{}]int64, int64, error) {
// initial timestampFrom, timestampTo = -1, -1 is an illegal value, only to mark initial state
var (
remained int64 = 0
analyzeResult = make(map[interface{}]int64, 0)
)
hasTTLField := t.ttlFieldID >= common.StartOfUserFieldID
for {
v, err := pkIter.NextValue()
if err != nil {
if err == sio.EOF {
pkIter.Close()
break
} else {
mlog.Warn(context.TODO(), "compact wrong, failed to iter through data", mlog.Err(err))
return nil, 0, err
}
}
// rowValue := vIter.GetData().(*iterators.InsertRow).GetValue()
row, ok := (*v).Value.(map[typeutil.UniqueID]interface{})
if !ok {
return nil, 0, merr.WrapErrServiceInternalMsg("unexpected error")
}
expireTs := int64(-1)
if hasTTLField {
if val, exists := row[t.ttlFieldID]; exists {
if v, ok := val.(int64); ok {
expireTs = v
}
}
}
// Filtering expired entity
if expiredFilter.Filtered((*v).PK.GetValue(), uint64((*v).Timestamp), expireTs) {
continue
}
key := row[t.clusteringKeyField.GetFieldID()]
if _, exist := analyzeResult[key]; exist {
analyzeResult[key] = analyzeResult[key] + 1
} else {
analyzeResult[key] = 1
}
remained++
}
return analyzeResult, remained, nil
}
func (t *clusteringCompactionTask) generatedScalarPlan(maxRows, preferRows int64, keys []interface{}, dict map[interface{}]int64) [][]interface{} {
buckets := make([][]interface{}, 0)
currentBucket := make([]interface{}, 0)
var currentBucketSize int64 = 0
for _, key := range keys {
// todo can optimize
if dict[key] > preferRows {
if len(currentBucket) != 0 {
buckets = append(buckets, currentBucket)
currentBucket = make([]interface{}, 0)
currentBucketSize = 0
}
buckets = append(buckets, []interface{}{key})
} else if currentBucketSize+dict[key] > maxRows {
buckets = append(buckets, currentBucket)
currentBucket = []interface{}{key}
currentBucketSize = dict[key]
} else if currentBucketSize+dict[key] > preferRows {
currentBucket = append(currentBucket, key)
buckets = append(buckets, currentBucket)
currentBucket = make([]interface{}, 0)
currentBucketSize = 0
} else {
currentBucket = append(currentBucket, key)
currentBucketSize += dict[key]
}
}
buckets = append(buckets, currentBucket)
return buckets
}
func (t *clusteringCompactionTask) switchPolicyForScalarPlan(totalRows int64, keys []interface{}, dict map[interface{}]int64) [][]interface{} {
bufferNumBySegmentMaxRows := totalRows / t.plan.MaxSegmentRows
bufferNumByMemory := t.memoryLimit / expectedBinlogSize
mlog.Info(context.TODO(), "switchPolicyForScalarPlan", mlog.Int64("totalRows", totalRows),
mlog.Int64("bufferNumBySegmentMaxRows", bufferNumBySegmentMaxRows),
mlog.Int64("bufferNumByMemory", bufferNumByMemory))
if bufferNumByMemory > bufferNumBySegmentMaxRows {
return t.generatedScalarPlan(t.plan.GetMaxSegmentRows(), t.plan.GetPreferSegmentRows(), keys, dict)
}
maxRows := totalRows / bufferNumByMemory
return t.generatedScalarPlan(maxRows, int64(float64(maxRows)*t.compactionParams.PreferSegmentSizeRatio), keys, dict)
}
func (t *clusteringCompactionTask) splitClusterByScalarValue(dict map[interface{}]int64) ([][]interface{}, bool) {
totalRows := int64(0)
keys := lo.MapToSlice(dict, func(k interface{}, v int64) interface{} {
totalRows += v
return k
})
notNullKeys := lo.Filter(keys, func(i interface{}, j int) bool {
return i != nil
})
sort.Slice(notNullKeys, func(i, j int) bool {
return storage.NewScalarFieldValue(t.clusteringKeyField.DataType, notNullKeys[i]).LE(storage.NewScalarFieldValue(t.clusteringKeyField.DataType, notNullKeys[j]))
})
return t.switchPolicyForScalarPlan(totalRows, notNullKeys, dict), len(keys) > len(notNullKeys)
}
func (t *clusteringCompactionTask) GetSlotUsage() int64 {
return t.plan.GetSlotUsage()
}
func (t *clusteringCompactionTask) GetStorageConfig() *indexpb.StorageConfig {
return t.compactionParams.StorageConfig
}
// getWriterOpts returns common writer options for all cluster buffer writers.
// Includes TEXT column configs when lobContext requires REWRITE_ALL.
func (t *clusteringCompactionTask) getWriterOpts() []storage.RwOption {
opts := []storage.RwOption{
storage.WithBufferSize(t.bufferSize),
storage.WithStorageConfig(t.compactionParams.StorageConfig),
storage.WithUseLoonFFI(t.compactionParams.UseLoonFFI),
storage.WithWriterFormat(t.compactionParams.GetStorageFormat()),
}
if t.lobContext != nil && t.lobContext.ShouldRewriteAnyField() {
// LOB base path at partition level: {root}/insert_log/{coll}/{part}
lobBasePath := path.Join(t.compactionParams.StorageConfig.GetRootPath(),
common.SegmentInsertLogPath, metautil.JoinIDPath(t.collectionID, t.partitionID))
textColumnConfigs := t.lobContext.GetTextColumnConfigs(
lobBasePath,
t.compactionParams.TextInlineThreshold,
t.compactionParams.TextMaxLobFileBytes,
t.compactionParams.TextFlushThresholdBytes,
)
if len(textColumnConfigs) > 0 {
opts = append(opts, storage.WithTextColumnConfigs(textColumnConfigs))
mlog.Info(context.TODO(), "clustering compaction: TEXT column REWRITE_ALL mode enabled",
mlog.Int("rewriteFieldCount", len(textColumnConfigs)),
)
}
}
return opts
}
// initLOBCompactionContext initializes the LOB compaction context for TEXT columns.
// For clustering compaction, data is repartitioned by clustering key, so TEXT columns
// always require REWRITE_ALL strategy (LOB references become invalid after repartition).
func (t *clusteringCompactionTask) initLOBCompactionContext(ctx context.Context) error {
// check if there are TEXT fields in schema
textFieldIDs := compaction.GetTEXTFieldIDsFromSchema(t.plan.GetSchema())
if len(textFieldIDs) == 0 {
return nil // no TEXT fields, nothing to do
}
// only apply for manifest-based storage (storage v2/v3)
hasManifest := false
for _, seg := range t.plan.GetSegmentBinlogs() {
if seg.GetManifest() != "" {
hasManifest = true
break
}
}
if !hasManifest {
return nil // no manifest-based segments, nothing to do
}
mlog.Info(context.TODO(), "initializing LOB compaction context for TEXT columns (clustering compaction)")
// create LOB compaction context
// for clustering compaction, we always use REWRITE_ALL (forced strategy)
// no need to collect LOB files or calculate hole ratio
t.lobContext = compaction.NewLOBCompactionContext()
// set compaction type - clustering compaction forces REWRITE_ALL
sourceSegmentCount := len(t.plan.GetSegmentBinlogs())
// target segment count is unknown for clustering compaction (determined by clustering)
t.lobContext.SetCompactionType(datapb.CompactionType_ClusteringCompaction, sourceSegmentCount, 0)
// compute strategies (will use forced REWRITE_ALL for all TEXT fields)
t.lobContext.ComputeStrategies(textFieldIDs, t.compactionParams.LOBHoleRatioThreshold)
// log strategy decisions
for fieldID, decision := range t.lobContext.Decisions {
mlog.Info(context.TODO(), "LOB compaction strategy decided",
mlog.Int64("fieldID", fieldID),
mlog.String("strategy", "REWRITE_ALL"),
mlog.Bool("isForced", t.lobContext.IsForced),
mlog.Int("sourceSegmentCount", sourceSegmentCount),
mlog.Float64("holeRatio", decision.OverallHoleRatio),
)
}
return nil
}