mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-24 00:44:18 +00:00
telemetry: keep only clusters that store at least 10 GiB (#11138)
* telemetry: tidy the server module after the protobuf bump Claude-Session: https://claude.ai/code/session_01VGiphDxpsKMwu9XpUFhybQ * telemetry: keep only clusters that store at least 10 GiB Fresh weed server runs, CI jobs and throwaway containers each mint their own cluster id. They came in at tens of thousands a day, were most of the counted clusters and held almost none of the bytes, and the state file and the metrics page grew with every one of them. Reports under the floor are counted and dropped, and a state file written before the floor sheds them on the first restart. Claude-Session: https://claude.ai/code/session_01VGiphDxpsKMwu9XpUFhybQ * master: report telemetry only once the cluster stores 10 GiB A throwaway cluster no longer registers itself with its first report a minute after start; a real one begins reporting at the first daily tick after it crosses the floor. Claude-Session: https://claude.ai/code/session_01VGiphDxpsKMwu9XpUFhybQ
This commit is contained in:
@@ -21,7 +21,7 @@ func validReport() *proto.TelemetryData {
|
||||
Version: "4.40",
|
||||
Os: "linux/amd64",
|
||||
VolumeServerCount: 5,
|
||||
TotalDiskBytes: 123456789,
|
||||
TotalDiskBytes: 123 << 30,
|
||||
TotalVolumeCount: 42,
|
||||
FilerCount: 2,
|
||||
BrokerCount: 1,
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.26
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/seaweedfs/seaweedfs v0.0.0-00010101000000-000000000000
|
||||
google.golang.org/protobuf v1.36.11
|
||||
google.golang.org/protobuf v1.36.12
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -30,7 +30,7 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -14,7 +14,7 @@ func report(id, version string) *proto.TelemetryData {
|
||||
Version: version,
|
||||
Os: "linux/amd64",
|
||||
VolumeServerCount: 1,
|
||||
TotalDiskBytes: 100,
|
||||
TotalDiskBytes: proto.MinDiskBytes,
|
||||
TotalVolumeCount: 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/seaweedfs/seaweedfs/telemetry/proto"
|
||||
)
|
||||
|
||||
func counterValue(t *testing.T, c prometheus.Counter) float64 {
|
||||
t.Helper()
|
||||
var m dto.Metric
|
||||
if err := c.Write(&m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m.GetCounter().GetValue()
|
||||
}
|
||||
|
||||
func TestClustersUnderTheFloorAreNotKept(t *testing.T) {
|
||||
s := newPrometheusStorage(prometheus.NewRegistry())
|
||||
|
||||
// report() sits exactly on the floor, which is enough.
|
||||
if err := s.StoreTelemetry(report("aaaaaaaa-0000-0000-0000-000000000001", "4.40")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
small := report("bbbbbbbb-0000-0000-0000-000000000002", "4.40")
|
||||
small.TotalDiskBytes = proto.MinDiskBytes - 1
|
||||
if err := s.StoreTelemetry(small); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, ok := s.instances[small.TopologyId]; ok {
|
||||
t.Fatal("cluster under the floor was kept")
|
||||
}
|
||||
if _, ok := s.GetHistory(small.TopologyId, 90); ok {
|
||||
t.Fatal("history kept for a cluster under the floor")
|
||||
}
|
||||
if stats := statsOf(t, s); stats["active_instances"] != 1 {
|
||||
t.Errorf("active = %v, want the one cluster on the floor", stats["active_instances"])
|
||||
}
|
||||
if got := counterValue(t, s.telemetryReceived); got != 2 {
|
||||
t.Errorf("received = %v, want 2", got)
|
||||
}
|
||||
if got := counterValue(t, s.reportsSkipped); got != 1 {
|
||||
t.Errorf("skipped = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// State written before the floor existed carries clusters under it; loading
|
||||
// drops them and marks the state dirty so the next save sheds them from disk.
|
||||
func TestLoadStateDropsClustersUnderTheFloor(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "telemetry-state.json")
|
||||
|
||||
s := newPrometheusStorage(prometheus.NewRegistry())
|
||||
kept := report("aaaaaaaa-0000-0000-0000-000000000001", "4.40")
|
||||
if err := s.StoreTelemetry(kept); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
small := report("bbbbbbbb-0000-0000-0000-000000000002", "4.40")
|
||||
small.TotalDiskBytes = proto.MinDiskBytes - 1
|
||||
s.instances[small.TopologyId] = &telemetryData{TelemetryData: small, ReceivedAt: time.Now()}
|
||||
s.histories[small.TopologyId] = []HistorySample{{Ts: time.Now().Unix(), TotalDiskBytes: small.TotalDiskBytes}}
|
||||
if err := s.SaveStateIfDirty(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s = newPrometheusStorage(prometheus.NewRegistry())
|
||||
n, err := s.LoadState(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("loaded %d instances, want 1", n)
|
||||
}
|
||||
if _, ok := s.instances[small.TopologyId]; ok {
|
||||
t.Error("cluster under the floor survived the load")
|
||||
}
|
||||
if _, ok := s.histories[small.TopologyId]; ok {
|
||||
t.Error("history of a cluster under the floor survived the load")
|
||||
}
|
||||
if _, ok := s.instances[kept.TopologyId]; !ok {
|
||||
t.Error("cluster on the floor was dropped")
|
||||
}
|
||||
if !s.dirty {
|
||||
t.Error("dropping a cluster left the state clean, so it would stay on disk")
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func TestClusterHistory(t *testing.T) {
|
||||
Version: "4.40",
|
||||
Os: "linux/amd64",
|
||||
VolumeServerCount: 3,
|
||||
TotalDiskBytes: 1000,
|
||||
TotalDiskBytes: proto.MinDiskBytes + 1000,
|
||||
TotalVolumeCount: 10,
|
||||
}
|
||||
if err := s.StoreTelemetry(report); err != nil {
|
||||
@@ -25,7 +25,7 @@ func TestClusterHistory(t *testing.T) {
|
||||
}
|
||||
|
||||
// A second report on the same UTC day replaces the day's sample.
|
||||
report.TotalDiskBytes = 2000
|
||||
report.TotalDiskBytes = proto.MinDiskBytes + 2000
|
||||
if err := s.StoreTelemetry(report); err != nil {
|
||||
t.Fatalf("store: %v", err)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func TestClusterHistory(t *testing.T) {
|
||||
if len(samples) != 1 {
|
||||
t.Fatalf("got %d samples, want 1 (same-day replace)", len(samples))
|
||||
}
|
||||
if samples[0].TotalDiskBytes != 2000 {
|
||||
if samples[0].TotalDiskBytes != proto.MinDiskBytes+2000 {
|
||||
t.Errorf("same-day sample not replaced: got %d", samples[0].TotalDiskBytes)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestClusterHistory(t *testing.T) {
|
||||
if !ok || len(samples) != 2 {
|
||||
t.Fatalf("after round trip: ok=%v samples=%d, want 2", ok, len(samples))
|
||||
}
|
||||
if samples[0].TotalDiskBytes != 500 || samples[1].TotalDiskBytes != 2000 {
|
||||
if samples[0].TotalDiskBytes != 500 || samples[1].TotalDiskBytes != proto.MinDiskBytes+2000 {
|
||||
t.Errorf("samples corrupted after round trip: %+v", samples)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/telemetry/proto"
|
||||
)
|
||||
|
||||
// persistedState is the on-disk snapshot of the in-memory instance map.
|
||||
@@ -16,7 +18,8 @@ type persistedState struct {
|
||||
// LoadState restores the instance map and Prometheus gauges from a state file
|
||||
// written by SaveStateIfDirty. A missing file is not an error. Original
|
||||
// ReceivedAt timestamps are preserved so cleanup and the active-cluster
|
||||
// windows stay correct across restarts.
|
||||
// windows stay correct across restarts. Clusters under proto.MinDiskBytes are
|
||||
// dropped, so state written before the floor sheds them on the first restart.
|
||||
func (s *PrometheusStorage) LoadState(path string) (int, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -39,6 +42,10 @@ func (s *PrometheusStorage) LoadState(path string) (int, error) {
|
||||
if instance == nil || instance.TelemetryData == nil || instance.TelemetryData.TopologyId == "" {
|
||||
continue
|
||||
}
|
||||
if instance.TelemetryData.TotalDiskBytes < proto.MinDiskBytes {
|
||||
s.dirty = true // so the next save sheds it
|
||||
continue
|
||||
}
|
||||
s.instances[id] = instance
|
||||
s.setClusterMetrics(instance.TelemetryData)
|
||||
loaded++
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestStateRoundTrip(t *testing.T) {
|
||||
Version: "4.40",
|
||||
Os: "linux/amd64",
|
||||
VolumeServerCount: 5,
|
||||
TotalDiskBytes: 123456789,
|
||||
TotalDiskBytes: 123 << 30,
|
||||
TotalVolumeCount: 42,
|
||||
FilerCount: 2,
|
||||
BrokerCount: 1,
|
||||
@@ -79,7 +79,7 @@ func TestLoadStateFillsNewestSampleVersion(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "telemetry-state.json")
|
||||
|
||||
s := newPrometheusStorage(prometheus.NewRegistry())
|
||||
report := &proto.TelemetryData{TopologyId: "test-cluster-1", Version: "4.40", Os: "linux/amd64"}
|
||||
report := &proto.TelemetryData{TopologyId: "test-cluster-1", Version: "4.40", Os: "linux/amd64", TotalDiskBytes: proto.MinDiskBytes}
|
||||
if err := s.StoreTelemetry(report); err != nil {
|
||||
t.Fatalf("store: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -21,6 +22,7 @@ type PrometheusStorage struct {
|
||||
brokerCount *prometheus.GaugeVec
|
||||
clusterInfo *prometheus.GaugeVec
|
||||
telemetryReceived prometheus.Counter
|
||||
reportsSkipped prometheus.Counter
|
||||
|
||||
// In-memory storage for API endpoints (if needed)
|
||||
mu sync.RWMutex
|
||||
@@ -83,6 +85,10 @@ func newPrometheusStorage(reg prometheus.Registerer) *PrometheusStorage {
|
||||
Name: "seaweedfs_telemetry_reports_received_total",
|
||||
Help: "Total number of telemetry reports received",
|
||||
}),
|
||||
reportsSkipped: promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "seaweedfs_telemetry_reports_skipped_total",
|
||||
Help: fmt.Sprintf("Reports not kept because the cluster stores less than %d GiB", proto.MinDiskBytes>>30),
|
||||
}),
|
||||
instances: make(map[string]*telemetryData),
|
||||
histories: make(map[string][]HistorySample),
|
||||
stats: make(map[string]interface{}),
|
||||
@@ -93,6 +99,12 @@ func (s *PrometheusStorage) StoreTelemetry(data *proto.TelemetryData) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.telemetryReceived.Inc()
|
||||
if data.TotalDiskBytes < proto.MinDiskBytes {
|
||||
s.reportsSkipped.Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Drop the cluster_info series recorded under the previous label set when
|
||||
// a cluster reports back with a different version or OS, so it is not
|
||||
// counted under two versions at once.
|
||||
@@ -102,8 +114,6 @@ func (s *PrometheusStorage) StoreTelemetry(data *proto.TelemetryData) error {
|
||||
}
|
||||
s.setClusterMetrics(data)
|
||||
|
||||
s.telemetryReceived.Inc()
|
||||
|
||||
// Store in memory for API endpoints
|
||||
receivedAt := time.Now().UTC()
|
||||
s.instances[data.TopologyId] = &telemetryData{
|
||||
|
||||
@@ -112,13 +112,13 @@ func TestClusterSizeSeriesUsesLatestDailySample(t *testing.T) {
|
||||
TopologyId: "aaaaaaaa-0000-0000-0000-000000000001",
|
||||
Version: "4.40",
|
||||
Os: "linux/amd64",
|
||||
TotalDiskBytes: 100,
|
||||
TotalDiskBytes: proto.MinDiskBytes + 100,
|
||||
VolumeServerCount: 4,
|
||||
}
|
||||
if err := s.StoreTelemetry(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data.TotalDiskBytes = 700
|
||||
data.TotalDiskBytes = proto.MinDiskBytes + 700
|
||||
data.VolumeServerCount = 6
|
||||
if err := s.StoreTelemetry(data); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -133,14 +133,14 @@ func TestClusterSizeSeriesUsesLatestDailySample(t *testing.T) {
|
||||
if today := time.Now().UTC().Format("2006-01-02"); len(series.Dates) != 1 || series.Dates[0] != today {
|
||||
t.Errorf("dates = %v, want %s only", series.Dates, today)
|
||||
}
|
||||
if got := series.Clusters[0].Disk; !equal(got, []uint64{700}) {
|
||||
if got := series.Clusters[0].Disk; !equal(got, []uint64{proto.MinDiskBytes + 700}) {
|
||||
t.Errorf("disk = %v, want today's latest sample only", got)
|
||||
}
|
||||
if got := series.Clusters[0].Servers; !equal(got, []uint64{6}) {
|
||||
t.Errorf("servers = %v, want today's latest sample only", got)
|
||||
}
|
||||
if series.TotalDisk != 700 || series.TotalServers != 6 {
|
||||
t.Errorf("totals = %d disk / %d servers, want 700/6", series.TotalDisk, series.TotalServers)
|
||||
if series.TotalDisk != proto.MinDiskBytes+700 || series.TotalServers != 6 {
|
||||
t.Errorf("totals = %d disk / %d servers, want %d/6", series.TotalDisk, series.TotalServers, proto.MinDiskBytes+700)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user