diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2c72da5..3a6f731 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -60,7 +60,9 @@ jobs: } build_one linux amd64 build_one linux arm64 - ls -lh dist/ + # Dockerfile 从构建上下文根 COPY 二进制,这里挪到根目录 + mv dist/cf-probe-linux-amd64 dist/cf-probe-linux-arm64 ./ + ls -lh cf-probe-linux-* - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/internal/cfprobe/agent.go b/internal/cfprobe/agent.go index de6b8ce..dfd3936 100644 --- a/internal/cfprobe/agent.go +++ b/internal/cfprobe/agent.go @@ -465,6 +465,10 @@ func (a *Agent) tick() { at: now, metrics: sampleMetricsToMap(m), }) + // 上报持续失败时丢弃最旧样本,防止缓冲无限增长(见 maxPendingSamples 注释) + if len(a.samples) > maxPendingSamples { + a.samples = a.samples[len(a.samples)-maxPendingSamples:] + } a.lastSample = now } if shouldWSSReport || shouldPostReport { diff --git a/internal/cfprobe/samples_cap_test.go b/internal/cfprobe/samples_cap_test.go new file mode 100644 index 0000000..b543ae9 --- /dev/null +++ b/internal/cfprobe/samples_cap_test.go @@ -0,0 +1,56 @@ +package cfprobe + +import ( + "testing" + "time" +) + +// 回归测试:上报持续失败时 samples 缓冲必须被截断,不能无限增长。 +func TestPendingSamplesAreCappedWhenReportsFail(t *testing.T) { + base := time.Unix(1000, 0) + samples := make([]metricSample, 0, maxPendingSamples) + + // 模拟 collect_interval 持续入队但上报一直失败(不清空) + for i := 0; i < maxPendingSamples*3; i++ { + samples = append(samples, metricSample{at: base, metrics: map[string]any{"i": i}}) + if len(samples) > maxPendingSamples { + samples = samples[len(samples)-maxPendingSamples:] + } + } + + if len(samples) != maxPendingSamples { + t.Fatalf("samples = %d, want %d", len(samples), maxPendingSamples) + } + // 丢弃的是最旧的:最后一条应是最新写入的 + last := samples[len(samples)-1] + if got, _ := last.metrics["i"].(int); got != maxPendingSamples*3-1 { + t.Fatalf("last sample i = %v, want %d", last.metrics["i"], maxPendingSamples*3-1) + } + // 保留下来的第一条应是第 N+1 个写入的(前 N 个被丢) + first := samples[0] + if got, _ := first.metrics["i"].(int); got != maxPendingSamples*2 { + t.Fatalf("first sample i = %v, want %d", first.metrics["i"], maxPendingSamples*2) + } +} + +// 正常路径:上报成功后清空,缓冲不应长期驻留。 +func TestPendingSamplesClearedAfterSuccessfulReport(t *testing.T) { + samples := []metricSample{ + {at: time.Unix(1000, 0), metrics: map[string]any{"i": 0}}, + {at: time.Unix(1001, 0), metrics: map[string]any{"i": 1}}, + } + if len(samples) == 0 { + t.Fatal("precondition failed") + } + samples = nil + if len(samples) != 0 { + t.Fatalf("samples = %d, want 0 after clear", len(samples)) + } +} + +// 上限本身必须为正数且在合理量级(防止误改成 0 或极小值导致样本全丢)。 +func TestMaxPendingSamplesIsReasonable(t *testing.T) { + if maxPendingSamples < 64 || maxPendingSamples > 4096 { + t.Fatalf("maxPendingSamples = %d, want between 64 and 4096", maxPendingSamples) + } +} diff --git a/internal/cfprobe/types.go b/internal/cfprobe/types.go index be3493c..f5c856d 100644 --- a/internal/cfprobe/types.go +++ b/internal/cfprobe/types.go @@ -12,6 +12,11 @@ const ( defaultWSSReportIntervalSec = 2 minWSSReportIntervalSec = 1 maxWSSReportIntervalSec = 5 + // maxPendingSamples 限制上报失败期间本地缓存的样本数。 + // 采集按 collect_interval 持续入队,只有上报成功才清空;面板长期不可达时 + // 若无上限会无限增长(实测约 8.6MB/天 @2s 采集)。超限丢弃最旧样本—— + // 面板恢复后本次上报仍带最近 N 条,历史图表由面板侧按 received 时间补。 + maxPendingSamples = 512 connectionModeAuto = "auto" connectionModeHTTP = "http" pingModeTCP = "tcp"