Files
seaweedfs/weed/topology/collection_statistics.go
T
Chris Lu 567052bfb6 s3: take bucket sizes from the master's summary (#10664)
* pb: ask the master what each collection holds

Callers tracking usage were sent every volume in the cluster to add up
themselves, which is the master's largest single allocation.

* topology: summarise what each collection holds

One pass over the topology, allocating per collection rather than per volume.
Regular volumes count once each for logical totals and once per replica for
physical, taken from the lookup index, which is already keyed by volume and so
needs no set of seen ids. Ec shards are node-local so their sizes sum, while
the file and delete counts describe the volume and resolve once every holder
has been seen.

Replicas of one volume disagree while a write is landing or a heartbeat is
late. Walking a full listing took whichever replica the map iteration reached
first, so the answer moved between runs; this takes the largest, which is
stable and never reports usage below what some replica already holds.

* s3: take bucket sizes from the master's summary

The bucket size metrics pulled the whole volume list once a minute and added it
up, which cost the master 184.6MB of allocation and 17.8MB on the wire for six
numbers per collection.

  VolumeList over 550k volumes   184.6 MB allocated, 17.8 MB on the wire
  CollectionStatistics              176 bytes allocated, 47 bytes on the wire

The aggregation moves to the master with it, so the cases the removed tests
covered are now asserted against it directly.

* topology: count the replica holding the most live data

Quotas are enforced on size less deletions, and the replica with the biggest
raw size can be the one that has deleted the most. Counting it reported a
bucket smaller than it is and would leave one writable over its quota, which is
the opposite of what picking the largest was meant to guarantee.

* topology: cap a volume's deletions at what it holds

Live usage is read as a collection's size less its deletions, so a volume
reporting more deleted bytes than it has cancels live bytes belonging to other
volumes in the same bucket and reports it smaller than it is. Replica selection
already floored that volume's own live size at zero; the totals have to agree
with it.
2026-08-09 00:00:19 -07:00

149 lines
4.9 KiB
Go

package topology
import (
"github.com/seaweedfs/seaweedfs/weed/storage"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// CollectionStatistics is what a collection holds, summarised so that callers
// tracking usage do not have to be sent every volume in the cluster to add it
// up themselves.
type CollectionStatistics struct {
Collection string
FileCount uint64
DeleteCount uint64
DeletedByteCount uint64
// Size counts one copy of the data: a single replica of a regular volume,
// the data shards of an ec volume.
Size uint64
// PhysicalSize counts what is on disk: every replica, and parity shards.
PhysicalSize uint64
VolumeCount uint64
}
type ecStatsKey struct {
collection string
volumeId needle.VolumeId
}
// ecFileCounts holds the per-volume counts that can only be resolved once every
// shard holder has been seen.
type ecFileCounts struct {
collection string
fileCount uint64
deleteCount uint64
}
// CollectionStatistics summarises every collection in one pass over the
// topology, allocating per collection rather than per volume.
func (t *Topology) CollectionStatistics() []*CollectionStatistics {
byCollection := make(map[string]*CollectionStatistics)
statsFor := func(collection string) *CollectionStatistics {
stats, found := byCollection[collection]
if !found {
stats = &CollectionStatistics{Collection: collection}
byCollection[collection] = stats
}
return stats
}
// Regular volumes are counted once each for logical totals and once per
// replica for physical, which the lookup index gives without a set of seen
// ids: it is already keyed by volume.
for _, c := range t.collectionMap.Items() {
collection := c.(*Collection)
for _, vl := range collection.GetAllVolumeLayouts() {
vl.accessLock.RLock()
for vid, locations := range vl.vid2location {
stats := statsFor(collection.Name)
// Replicas of one volume can disagree while a write is landing
// or a heartbeat is late. Count the one holding the most live
// data, so the answer does not depend on which replica is
// looked at first and usage is never reported lower than some
// replica already holds. Quotas are enforced on size less
// deletions, so that is what has to be the largest -- a replica
// with the biggest raw size can be the one that has deleted the
// most, and picking it would leave an over-quota bucket
// writable.
var largest storage.VolumeInfo
var largestLive uint64
found := false
for _, dn := range locations.list {
v, err := dn.GetVolumesById(vid)
if err != nil {
continue
}
stats.PhysicalSize += v.Size
live := v.Size
if v.DeletedByteCount < live {
live -= v.DeletedByteCount
} else {
live = 0
}
if !found || live > largestLive {
largest, largestLive, found = v, live, true
}
}
if !found {
continue
}
stats.Size += largest.Size
stats.FileCount += uint64(largest.FileCount)
stats.DeleteCount += uint64(largest.DeleteCount)
// Never more deletions than the volume holds. Live usage is
// read as the collection's size less its deletions, so a volume
// reporting more deleted bytes than it has would cancel live
// bytes belonging to other volumes and report the bucket
// smaller than it is.
stats.DeletedByteCount += largest.Size - largestLive
stats.VolumeCount++
}
vl.accessLock.RUnlock()
}
}
// Ec shards are node-local rather than replicated, so their sizes sum
// across holders. The file and delete counts describe the volume rather
// than the shard, so they resolve once every holder has been seen.
perEcVolume := make(map[ecStatsKey]*ecFileCounts)
for _, dcNode := range t.Children() {
for _, rackNode := range dcNode.(*DataCenter).Children() {
for _, dnNode := range rackNode.(*Rack).Children() {
for _, ecInfo := range dnNode.(*DataNode).GetEcShards() {
message := ecInfo.ToVolumeEcShardInformationMessage()
stats := statsFor(ecInfo.Collection)
stats.PhysicalSize += uint64(erasure_coding.EcShardsTotalSize(message))
stats.Size += uint64(erasure_coding.EcShardsDataSize(message, 0))
key := ecStatsKey{collection: ecInfo.Collection, volumeId: ecInfo.VolumeId}
counts, found := perEcVolume[key]
if !found {
counts = &ecFileCounts{collection: ecInfo.Collection}
perEcVolume[key] = counts
stats.VolumeCount++
}
if message.FileCount > counts.fileCount {
counts.fileCount = message.FileCount
}
counts.deleteCount += message.DeleteCount
}
}
}
}
for _, counts := range perEcVolume {
stats := byCollection[counts.collection]
if stats == nil {
continue
}
stats.FileCount += counts.fileCount
stats.DeleteCount += counts.deleteCount
}
ret := make([]*CollectionStatistics, 0, len(byCollection))
for _, stats := range byCollection {
ret = append(ret, stats)
}
return ret
}