Skip to content

Commit 015ccb5

Browse files
authored
[fix][backend] add MaxBytes limit to ListSpansRepeat to prevent… (#568)
[fix][observability] add MaxBytes limit to ListSpansRepeat to prevent OOM in trajectory backfill When a traceID contains a large number of spans, ListSpansRepeat would fetch all pages into memory without any bound, potentially causing OOM in TCE instances during backfill tasks. This adds a configurable MaxBytes parameter to ListSpansRepeat. When the accumulated span size exceeds the limit, it returns ErrMaxBytesExceeded. GetTrajectories catches this error, logs a warning, and returns an empty trajectory map to skip oversized traces gracefully. The limit is configured per workspace via BackfillConfig.TrajectoryMaxBytes. When unset (0), the original unlimited behavior is preserved.
1 parent 3f01307 commit 015ccb5

5 files changed

Lines changed: 38 additions & 0 deletions

File tree

backend/modules/observability/domain/component/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ type BackfillConfig struct {
149149
CkQueryLimit SpaceAwareParam[int] `mapstructure:"ck_query_limit" json:"ck_query_limit"`
150150
// BatchDispatchGray 批量分发灰度配置
151151
BatchDispatchGray *BatchGrayConfig `mapstructure:"batch_dispatch_gray" json:"batch_dispatch_gray"`
152+
// TrajectoryMaxBytes 单次 GetTrajectories 中 ListSpansRepeat 的内存上限(字节),0 表示不限制
153+
TrajectoryMaxBytes SpaceAwareParam[int64] `mapstructure:"trajectory_max_bytes" json:"trajectory_max_bytes"`
152154
}
153155

154156
// GetDispatchBatchSize 获取指定 workspace 的分发批大小
@@ -166,6 +168,11 @@ func (c *BackfillConfig) GetCkQueryLimit(workspaceID int64) int {
166168
return c.CkQueryLimit.Get(workspaceID)
167169
}
168170

171+
// GetTrajectoryMaxBytes 获取指定 workspace 的 trajectory 内存上限
172+
func (c *BackfillConfig) GetTrajectoryMaxBytes(workspaceID int64) int64 {
173+
return c.TrajectoryMaxBytes.Get(workspaceID)
174+
}
175+
169176
// BatchGrayConfig 批量处理灰度开关配置
170177
type BatchGrayConfig struct {
171178
// EnableAll 全开开关,为 true 时所有 workspace 走批量逻辑

backend/modules/observability/domain/trace/repo/trace.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ package repo
55

66
import (
77
"context"
8+
"errors"
89

910
"github.com/coze-dev/coze-loop/backend/modules/observability/domain/trace/entity"
1011
"github.com/coze-dev/coze-loop/backend/modules/observability/domain/trace/entity/loop_span"
1112
)
1213

14+
var ErrMaxBytesExceeded = errors.New("ListSpansRepeat: max bytes exceeded")
15+
1316
type GetTraceParam struct {
1417
WorkSpaceID string
1518
Tenants []string
@@ -46,6 +49,7 @@ type ListSpansParam struct {
4649
NotQueryAnnotation bool
4750
OmitColumns []string // omit specific columns
4851
SelectColumns []string // select specific columns, default select all columns
52+
MaxBytes int64 // max total bytes for ListSpansRepeat, 0 means no limit
4953
}
5054

5155
type ListSpansResult struct {

backend/modules/observability/domain/trace/service/trace_service.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package service
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"math"
1011
"sort"
@@ -2489,6 +2490,11 @@ func (r *TraceServiceImpl) GetTrajectories(ctx context.Context, workspaceID int6
24892490
metaRules = metaCfg.Spaces[workspaceID]
24902491
}
24912492

2493+
var maxBytes int64
2494+
if backfillCfg := r.traceConfig.GetBackfillConfig(ctx); backfillCfg != nil {
2495+
maxBytes = backfillCfg.GetTrajectoryMaxBytes(workspaceID)
2496+
}
2497+
24922498
allSpans, err := r.traceRepo.ListSpansRepeat(ctx, &repo.ListSpansParam{
24932499
Tenants: tenant,
24942500
Filters: &loop_span.FilterFields{
@@ -2506,8 +2512,13 @@ func (r *TraceServiceImpl) GetTrajectories(ctx context.Context, workspaceID int6
25062512
Limit: 1000,
25072513
NotQueryAnnotation: true,
25082514
SelectColumns: []string{loop_span.SpanFieldTraceId, loop_span.SpanFieldSpanId, loop_span.SpanFieldParentID, loop_span.SpanFieldSpaceId, loop_span.SpanFieldSpanName},
2515+
MaxBytes: maxBytes,
25092516
})
25102517
if err != nil {
2518+
if errors.Is(err, repo.ErrMaxBytesExceeded) {
2519+
logs.CtxWarn(ctx, "GetTrajectories skipped: allSpans exceeded max bytes, traceIDs:%v, maxBytes:%d", traceIDs, maxBytes)
2520+
return map[string]*loop_span.Trajectory{}, nil
2521+
}
25112522
logs.CtxError(ctx, "Failed to list all spans, err:%+v", err)
25122523
return nil, err
25132524
}
@@ -2520,8 +2531,13 @@ func (r *TraceServiceImpl) GetTrajectories(ctx context.Context, workspaceID int6
25202531
EndAt: endTime,
25212532
Limit: 100,
25222533
NotQueryAnnotation: true,
2534+
MaxBytes: maxBytes,
25232535
})
25242536
if err != nil {
2537+
if errors.Is(err, repo.ErrMaxBytesExceeded) {
2538+
logs.CtxWarn(ctx, "GetTrajectories skipped: selectedSpans exceeded max bytes, traceIDs:%v, maxBytes:%d", traceIDs, maxBytes)
2539+
return map[string]*loop_span.Trajectory{}, nil
2540+
}
25252541
logs.CtxError(ctx, "Failed to list selected spans, err:%+v", err)
25262542
return nil, err
25272543
}

backend/modules/observability/domain/trace/service/trace_trajectory_service_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func TestTraceServiceImpl_GetTrajectories_and_ListTrajectory(t *testing.T) {
129129
repoMock.EXPECT().GetTrajectoryConfig(gomock.Any(), repo.GetTrajectoryConfigParam{WorkspaceId: 1}).Return(nil, nil).AnyTimes()
130130
traceConfigMock := configmocks.NewMockITraceConfig(ctrl)
131131
traceConfigMock.EXPECT().GetTrajectoryMetadataConfig(gomock.Any()).Return(nil).AnyTimes()
132+
traceConfigMock.EXPECT().GetBackfillConfig(gomock.Any()).Return(nil).AnyTimes()
132133

133134
svc := &TraceServiceImpl{traceRepo: repoMock, buildHelper: builder, tenantProvider: tenantProviderMock, traceConfig: traceConfigMock}
134135
// mock list all spans

backend/modules/observability/infra/repo/trace.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,22 @@ func (t *TraceRepoImpl) ListSpansRepeat(ctx context.Context, req *repo.ListSpans
296296

297297
clonedReq := *req
298298
totalSpans := loop_span.SpanList{}
299+
var totalBytes int64
299300

300301
for {
301302
resp, err := t.ListSpans(ctx, &clonedReq)
302303
if err != nil {
303304
return nil, err
304305
}
306+
307+
if req.MaxBytes > 0 {
308+
pageBytes := int64(loop_span.SizeofSpans(resp.Spans))
309+
if totalBytes+pageBytes > req.MaxBytes {
310+
return nil, repo.ErrMaxBytesExceeded
311+
}
312+
totalBytes += pageBytes
313+
}
314+
305315
totalSpans = append(totalSpans, resp.Spans...)
306316
if !resp.HasMore || resp.PageToken == "" {
307317
break

0 commit comments

Comments
 (0)