// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "sync" "sync/atomic" "time" "github.com/rcrowley/go-metrics" ) //go:generate msgp -file $GOFILE const ( // beta is the weight used to calculate exponential moving average beta = 0.1 // Number of averages considered = 1/(1-beta) ) // rateMeasurement captures the transfer details for one bucket/target //msgp:ignore rateMeasurement type rateMeasurement struct { lock sync.Mutex bytesSinceLastWindow uint64 // Total bytes since last window was processed startTime time.Time // Start time for window expMovingAvg float64 // Previously calculated exponential moving average } // newRateMeasurement creates a new instance of the measurement with the initial start time. func newRateMeasurement(initTime time.Time) *rateMeasurement { return &rateMeasurement{ startTime: initTime, } } // incrementBytes add bytes reported for a bucket/target. func (m *rateMeasurement) incrementBytes(bytes uint64) { atomic.AddUint64(&m.bytesSinceLastWindow, bytes) } // updateExponentialMovingAverage processes the measurements captured so far. func (m *rateMeasurement) updateExponentialMovingAverage(endTime time.Time) { // Calculate aggregate avg bandwidth and exp window avg m.lock.Lock() defer func() { m.startTime = endTime m.lock.Unlock() }() if m.startTime.IsZero() { return } if endTime.Before(m.startTime) { return } duration := endTime.Sub(m.startTime) bytesSinceLastWindow := atomic.SwapUint64(&m.bytesSinceLastWindow, 0) if m.expMovingAvg == 0 { // Should address initial calculation and should be fine for resuming from 0 m.expMovingAvg = float64(bytesSinceLastWindow) / duration.Seconds() return } increment := float64(bytesSinceLastWindow) / duration.Seconds() m.expMovingAvg = exponentialMovingAverage(beta, m.expMovingAvg, increment) } // exponentialMovingAverage calculates the exponential moving average func exponentialMovingAverage(beta, previousAvg, incrementAvg float64) float64 { return (1-beta)*incrementAvg + beta*previousAvg } // getExpMovingAvgBytesPerSecond returns the exponential moving average for the bucket/target in bytes func (m *rateMeasurement) getExpMovingAvgBytesPerSecond() float64 { m.lock.Lock() defer m.lock.Unlock() return m.expMovingAvg } // ActiveWorkerStat is stat for active replication workers type ActiveWorkerStat struct { Curr int `json:"curr"` Avg float32 `json:"avg"` Max int `json:"max"` hist metrics.Histogram } func newActiveWorkerStat(r metrics.Registry) *ActiveWorkerStat { h := metrics.NewHistogram(metrics.NewUniformSample(100)) r.Register("replication.active_workers", h) return &ActiveWorkerStat{ hist: h, } } // update curr and max workers; func (a *ActiveWorkerStat) update() { if a == nil { return } a.Curr = globalReplicationPool.Get().ActiveWorkers() a.hist.Update(int64(a.Curr)) a.Avg = float32(a.hist.Mean()) a.Max = int(a.hist.Max()) } func (a *ActiveWorkerStat) get() ActiveWorkerStat { w := ActiveWorkerStat{ Curr: a.Curr, Avg: a.Avg, Max: a.Max, } return w } // QStat holds queue stats for replication type QStat struct { Count float64 `json:"count"` Bytes float64 `json:"bytes"` } func (q *QStat) add(o QStat) QStat { return QStat{Bytes: q.Bytes + o.Bytes, Count: q.Count + o.Count} } // InQueueMetric holds queue stats for replication type InQueueMetric struct { Curr QStat `json:"curr" msg:"cq"` Avg QStat `json:"avg" msg:"aq"` Max QStat `json:"max" msg:"pq"` } func (qm InQueueMetric) merge(o InQueueMetric) InQueueMetric { return InQueueMetric{ Curr: qm.Curr.add(o.Curr), Avg: qm.Avg.add(o.Avg), Max: qm.Max.add(o.Max), } } type queueCache struct { srQueueStats InQueueStats bucketStats map[string]InQueueStats sync.RWMutex // mutex for queue stats } func newQueueCache(r metrics.Registry) queueCache { return queueCache{ bucketStats: make(map[string]InQueueStats), srQueueStats: newInQueueStats(r, "site"), } } func (q *queueCache) update() { q.Lock() defer q.Unlock() q.srQueueStats.update() for _, s := range q.bucketStats { s.update() } } func (q *queueCache) getBucketStats(bucket string) InQueueMetric { q.RLock() defer q.RUnlock() v, ok := q.bucketStats[bucket] if !ok { return InQueueMetric{} } return InQueueMetric{ Curr: QStat{Bytes: float64(v.nowBytes), Count: float64(v.nowCount)}, Max: QStat{Bytes: float64(v.histBytes.Max()), Count: float64(v.histCounts.Max())}, Avg: QStat{Bytes: v.histBytes.Mean(), Count: v.histCounts.Mean()}, } } func (q *queueCache) getSiteStats() InQueueMetric { q.RLock() defer q.RUnlock() v := q.srQueueStats return InQueueMetric{ Curr: QStat{Bytes: float64(v.nowBytes), Count: float64(v.nowCount)}, Max: QStat{Bytes: float64(v.histBytes.Max()), Count: float64(v.histCounts.Max())}, Avg: QStat{Bytes: v.histBytes.Mean(), Count: v.histCounts.Mean()}, } } // InQueueStats holds queue stats for replication type InQueueStats struct { nowBytes int64 `json:"-"` nowCount int64 `json:"-"` histCounts metrics.Histogram histBytes metrics.Histogram } func newInQueueStats(r metrics.Registry, lbl string) InQueueStats { histCounts := metrics.NewHistogram(metrics.NewUniformSample(100)) histBytes := metrics.NewHistogram(metrics.NewUniformSample(100)) r.Register("replication.queue.counts."+lbl, histCounts) r.Register("replication.queue.bytes."+lbl, histBytes) return InQueueStats{ histCounts: histCounts, histBytes: histBytes, } } func (q *InQueueStats) update() { q.histBytes.Update(atomic.LoadInt64(&q.nowBytes)) q.histCounts.Update(atomic.LoadInt64(&q.nowCount)) } // XferStats has transfer stats for replication type XferStats struct { Curr float64 `json:"currRate" msg:"cr"` Avg float64 `json:"avgRate" msg:"av"` Peak float64 `json:"peakRate" msg:"p"` N int64 `json:"n" msg:"n"` measure *rateMeasurement `json:"-"` sma *SMA `json:"-"` } // Clone returns a copy of XferStats func (rx *XferStats) Clone() *XferStats { curr := rx.curr() peak := rx.Peak if curr > peak { peak = curr } return &XferStats{ Curr: curr, Avg: rx.Avg, Peak: peak, N: rx.N, measure: rx.measure, } } func newXferStats() *XferStats { return &XferStats{ measure: newRateMeasurement(time.Now()), sma: newSMA(50), } } func (rx *XferStats) String() string { return fmt.Sprintf("curr=%f avg=%f, peak=%f", rx.curr(), rx.Avg, rx.Peak) } func (rx *XferStats) curr() float64 { if rx.measure == nil { return 0.0 } return rx.measure.getExpMovingAvgBytesPerSecond() } func (rx *XferStats) merge(o XferStats) XferStats { curr := calcAvg(rx.curr(), o.curr(), rx.N, o.N) peak := rx.Peak if o.Peak > peak { peak = o.Peak } if curr > peak { peak = curr } return XferStats{ Avg: calcAvg(rx.Avg, o.Avg, rx.N, o.N), Peak: peak, Curr: curr, measure: rx.measure, N: rx.N + o.N, } } func calcAvg(x, y float64, n1, n2 int64) float64 { if n1+n2 == 0 { return 0 } avg := (x*float64(n1) + y*float64(n2)) / float64(n1+n2) return avg } // Add a new transfer func (rx *XferStats) addSize(sz int64, t time.Duration) { if rx.measure == nil { rx.measure = newRateMeasurement(time.Now()) } rx.measure.incrementBytes(uint64(sz)) rx.Curr = rx.measure.getExpMovingAvgBytesPerSecond() rx.sma.addSample(rx.Curr) rx.Avg = rx.sma.simpleMovingAvg() if rx.Curr > rx.Peak { rx.Peak = rx.Curr } rx.N++ } // ReplicationMRFStats holds stats of MRF backlog saved to disk in the last 5 minutes // and number of entries that failed replication after 3 retries type ReplicationMRFStats struct { LastFailedCount uint64 `json:"failedCount_last5min"` // Count of unreplicated entries that were dropped after MRF retry limit reached since cluster start. TotalDroppedCount uint64 `json:"droppedCount_since_uptime"` // Bytes of unreplicated entries that were dropped after MRF retry limit reached since cluster start. TotalDroppedBytes uint64 `json:"droppedBytes_since_uptime"` } // SMA struct for calculating simple moving average type SMA struct { buf []float64 window int // len of buf idx int // current index in buf CAvg float64 // cumulative average prevSMA float64 filledBuf bool } func newSMA(ln int) *SMA { if ln <= 0 { ln = defaultWindowSize } return &SMA{ buf: make([]float64, ln), window: ln, idx: 0, } } func (s *SMA) addSample(next float64) { prev := s.buf[s.idx] s.buf[s.idx] = next if s.filledBuf { s.prevSMA += (next - prev) / float64(s.window) s.CAvg += (next - s.CAvg) / float64(s.window) } else { s.CAvg = s.simpleMovingAvg() s.prevSMA = s.CAvg } if s.idx == s.window-1 { s.filledBuf = true } s.idx = (s.idx + 1) % s.window } func (s *SMA) simpleMovingAvg() float64 { if s.filledBuf { return s.prevSMA } var tot float64 for _, r := range s.buf { tot += r } return tot / float64(s.idx+1) } const ( defaultWindowSize = 10 ) type proxyStatsCache struct { srProxyStats ProxyMetric bucketStats map[string]ProxyMetric sync.RWMutex // mutex for proxy stats } func newProxyStatsCache() proxyStatsCache { return proxyStatsCache{ bucketStats: make(map[string]ProxyMetric), } } func (p *proxyStatsCache) inc(bucket string, api replProxyAPI, isErr bool) { p.Lock() defer p.Unlock() v, ok := p.bucketStats[bucket] if !ok { v = ProxyMetric{} } switch api { case putObjectTaggingAPI: if !isErr { atomic.AddUint64(&v.PutTagTotal, 1) atomic.AddUint64(&p.srProxyStats.PutTagTotal, 1) } else { atomic.AddUint64(&v.PutTagFailedTotal, 1) atomic.AddUint64(&p.srProxyStats.PutTagFailedTotal, 1) } case getObjectTaggingAPI: if !isErr { atomic.AddUint64(&v.GetTagTotal, 1) atomic.AddUint64(&p.srProxyStats.GetTagTotal, 1) } else { atomic.AddUint64(&v.GetTagFailedTotal, 1) atomic.AddUint64(&p.srProxyStats.GetTagFailedTotal, 1) } case removeObjectTaggingAPI: if !isErr { atomic.AddUint64(&v.RmvTagTotal, 1) atomic.AddUint64(&p.srProxyStats.RmvTagTotal, 1) } else { atomic.AddUint64(&v.RmvTagFailedTotal, 1) atomic.AddUint64(&p.srProxyStats.RmvTagFailedTotal, 1) } case headObjectAPI: if !isErr { atomic.AddUint64(&v.HeadTotal, 1) atomic.AddUint64(&p.srProxyStats.HeadTotal, 1) } else { atomic.AddUint64(&v.HeadFailedTotal, 1) atomic.AddUint64(&p.srProxyStats.HeadFailedTotal, 1) } case getObjectAPI: if !isErr { atomic.AddUint64(&v.GetTotal, 1) atomic.AddUint64(&p.srProxyStats.GetTotal, 1) } else { atomic.AddUint64(&v.GetFailedTotal, 1) atomic.AddUint64(&p.srProxyStats.GetFailedTotal, 1) } default: return } p.bucketStats[bucket] = v } func (p *proxyStatsCache) getBucketStats(bucket string) ProxyMetric { p.RLock() defer p.RUnlock() v, ok := p.bucketStats[bucket] if !ok { return ProxyMetric{} } return ProxyMetric{ PutTagTotal: atomic.LoadUint64(&v.PutTagTotal), GetTagTotal: atomic.LoadUint64(&v.GetTagTotal), RmvTagTotal: atomic.LoadUint64(&v.RmvTagTotal), HeadTotal: atomic.LoadUint64(&v.HeadTotal), GetTotal: atomic.LoadUint64(&v.GetTotal), PutTagFailedTotal: atomic.LoadUint64(&v.PutTagFailedTotal), GetTagFailedTotal: atomic.LoadUint64(&v.GetTagFailedTotal), RmvTagFailedTotal: atomic.LoadUint64(&v.RmvTagFailedTotal), HeadFailedTotal: atomic.LoadUint64(&v.HeadFailedTotal), GetFailedTotal: atomic.LoadUint64(&v.GetFailedTotal), } } func (p *proxyStatsCache) getSiteStats() ProxyMetric { v := p.srProxyStats return ProxyMetric{ PutTagTotal: atomic.LoadUint64(&v.PutTagTotal), GetTagTotal: atomic.LoadUint64(&v.GetTagTotal), RmvTagTotal: atomic.LoadUint64(&v.RmvTagTotal), HeadTotal: atomic.LoadUint64(&v.HeadTotal), GetTotal: atomic.LoadUint64(&v.GetTotal), PutTagFailedTotal: atomic.LoadUint64(&v.PutTagFailedTotal), GetTagFailedTotal: atomic.LoadUint64(&v.GetTagFailedTotal), RmvTagFailedTotal: atomic.LoadUint64(&v.RmvTagFailedTotal), HeadFailedTotal: atomic.LoadUint64(&v.HeadFailedTotal), GetFailedTotal: atomic.LoadUint64(&v.GetFailedTotal), } } type replProxyAPI string const ( putObjectTaggingAPI replProxyAPI = "PutObjectTagging" getObjectTaggingAPI replProxyAPI = "GetObjectTagging" removeObjectTaggingAPI replProxyAPI = "RemoveObjectTagging" headObjectAPI replProxyAPI = "HeadObject" getObjectAPI replProxyAPI = "GetObject" ) // ProxyMetric holds stats for replication proxying type ProxyMetric struct { PutTagTotal uint64 `json:"putTaggingProxyTotal" msg:"ptc"` GetTagTotal uint64 `json:"getTaggingProxyTotal" msg:"gtc"` RmvTagTotal uint64 `json:"removeTaggingProxyTotal" msg:"rtc"` GetTotal uint64 `json:"getProxyTotal" msg:"gc"` HeadTotal uint64 `json:"headProxyTotal" msg:"hc"` PutTagFailedTotal uint64 `json:"putTaggingProxyFailed" msg:"ptf"` GetTagFailedTotal uint64 `json:"getTaggingProxyFailed" msg:"gtf"` RmvTagFailedTotal uint64 `json:"removeTaggingProxyFailed" msg:"rtf"` GetFailedTotal uint64 `json:"getProxyFailed" msg:"gf"` HeadFailedTotal uint64 `json:"headProxyFailed" msg:"hf"` } func (p *ProxyMetric) add(p2 ProxyMetric) { atomic.AddUint64(&p.GetTotal, p2.GetTotal) atomic.AddUint64(&p.HeadTotal, p2.HeadTotal) atomic.AddUint64(&p.GetTagTotal, p2.GetTagTotal) atomic.AddUint64(&p.PutTagTotal, p2.PutTagTotal) atomic.AddUint64(&p.RmvTagTotal, p2.RmvTagTotal) atomic.AddUint64(&p.GetFailedTotal, p2.GetFailedTotal) atomic.AddUint64(&p.HeadFailedTotal, p2.HeadFailedTotal) atomic.AddUint64(&p.GetTagFailedTotal, p2.GetTagFailedTotal) atomic.AddUint64(&p.PutTagFailedTotal, p2.PutTagFailedTotal) atomic.AddUint64(&p.RmvTagFailedTotal, p2.RmvTagFailedTotal) }