4424697d7f
live accounting used to be a cache to store writes before they are picked up during the tally iteration, after which the cache is cleared. This created a window in which users could potentially exceed the storage limit. This PR refactors live accounting to hold current estimations of space used per project. This should also reduce DB load since we no longer need to query the satellite DB when checking space used for limiting. The mechanism by which the new live accounting system works is as follows: During the upload of any segment, the size of that segment is added to its respective project total in live accounting. At the beginning of the tally iteration we record the current values in live accounting as `initialLiveTotals`. At the end of the tally iteration we again record the current totals in live accounting as `latestLiveTotals`. The metainfo loop observer in tally allows us to get the project totals from what it observed in metainfo DB which are stored in `tallyProjectTotals`. However, for any particular segment uploaded during the metainfo loop, the observer may or may not have seen it. Thus, we take half of the difference between `latestLiveTotals` and `initialLiveTotals`, and add that to the total that was found during tally and set that as the new live accounting total. Initially, live accounting was storing the total stored amount across all nodes rather than the segment size, which is inconsistent with how we record amounts stored in the project accounting DB, so we have refactored live accounting to record segment size Change-Id: Ie48bfdef453428fcdc180b2d781a69d58fd927fb
256 lines
7.9 KiB
Go
256 lines
7.9 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package tally
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/skyrings/skyring-common/tools/uuid"
|
|
"github.com/zeebo/errs"
|
|
"go.uber.org/zap"
|
|
"gopkg.in/spacemonkeygo/monkit.v2"
|
|
|
|
"storj.io/common/pb"
|
|
"storj.io/common/storj"
|
|
"storj.io/common/sync2"
|
|
"storj.io/storj/satellite/accounting"
|
|
"storj.io/storj/satellite/metainfo"
|
|
)
|
|
|
|
// Error is a standard error class for this package.
|
|
var (
|
|
Error = errs.Class("tally error")
|
|
mon = monkit.Package()
|
|
)
|
|
|
|
// Config contains configurable values for the tally service
|
|
type Config struct {
|
|
Interval time.Duration `help:"how frequently the tally service should run" releaseDefault:"1h" devDefault:"30s"`
|
|
}
|
|
|
|
// Service is the tally service for data stored on each storage node
|
|
//
|
|
// architecture: Chore
|
|
type Service struct {
|
|
log *zap.Logger
|
|
Loop sync2.Cycle
|
|
|
|
metainfoLoop *metainfo.Loop
|
|
liveAccounting accounting.Cache
|
|
storagenodeAccountingDB accounting.StoragenodeAccounting
|
|
projectAccountingDB accounting.ProjectAccounting
|
|
}
|
|
|
|
// New creates a new tally Service
|
|
func New(log *zap.Logger, sdb accounting.StoragenodeAccounting, pdb accounting.ProjectAccounting, liveAccounting accounting.Cache, metainfoLoop *metainfo.Loop, interval time.Duration) *Service {
|
|
return &Service{
|
|
log: log,
|
|
Loop: *sync2.NewCycle(interval),
|
|
|
|
metainfoLoop: metainfoLoop,
|
|
liveAccounting: liveAccounting,
|
|
storagenodeAccountingDB: sdb,
|
|
projectAccountingDB: pdb,
|
|
}
|
|
}
|
|
|
|
// Run the tally service loop
|
|
func (service *Service) Run(ctx context.Context) (err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
return service.Loop.Run(ctx, func(ctx context.Context) error {
|
|
err := service.Tally(ctx)
|
|
if err != nil {
|
|
service.log.Error("tally failed", zap.Error(err))
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Close stops the service and releases any resources.
|
|
func (service *Service) Close() error {
|
|
service.Loop.Close()
|
|
return nil
|
|
}
|
|
|
|
// Tally calculates data-at-rest usage once
|
|
func (service *Service) Tally(ctx context.Context) (err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
initialLiveTotals, err := service.liveAccounting.GetAllProjectTotals(ctx)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
|
|
// Fetch when the last tally happened so we can roughly calculate the byte-hours.
|
|
lastTime, err := service.storagenodeAccountingDB.LastTimestamp(ctx, accounting.LastAtRestTally)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
if lastTime.IsZero() {
|
|
lastTime = time.Now()
|
|
}
|
|
|
|
// add up all nodes and buckets
|
|
observer := NewObserver(service.log.Named("observer"))
|
|
err = service.metainfoLoop.Join(ctx, observer)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
finishTime := time.Now()
|
|
|
|
// calculate byte hours, not just bytes
|
|
hours := time.Since(lastTime).Hours()
|
|
for id := range observer.Node {
|
|
observer.Node[id] *= hours
|
|
}
|
|
|
|
// save the new results
|
|
var errAtRest, errBucketInfo error
|
|
if len(observer.Node) > 0 {
|
|
err = service.storagenodeAccountingDB.SaveTallies(ctx, finishTime, observer.Node)
|
|
if err != nil {
|
|
errAtRest = errs.New("StorageNodeAccounting.SaveTallies failed: %v", err)
|
|
}
|
|
}
|
|
|
|
if len(observer.Bucket) > 0 {
|
|
// record bucket tallies to DB
|
|
err = service.projectAccountingDB.SaveTallies(ctx, finishTime, observer.Bucket)
|
|
if err != nil {
|
|
errAtRest = errs.New("ProjectAccounting.SaveTallies failed: %v", err)
|
|
}
|
|
|
|
// update live accounting totals
|
|
tallyProjectTotals := projectTotalsFromBuckets(observer.Bucket)
|
|
latestLiveTotals, err := service.liveAccounting.GetAllProjectTotals(ctx)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
for projectID, latest := range latestLiveTotals {
|
|
delta := latest - initialLiveTotals[projectID]
|
|
err = service.liveAccounting.AddProjectStorageUsage(ctx, projectID, -latest+tallyProjectTotals[projectID]+(delta/2))
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// report bucket metrics
|
|
if len(observer.Bucket) > 0 {
|
|
var total accounting.BucketTally
|
|
for _, bucket := range observer.Bucket {
|
|
monAccounting.IntVal("bucket.objects").Observe(bucket.ObjectCount)
|
|
|
|
monAccounting.IntVal("bucket.segments").Observe(bucket.Segments())
|
|
monAccounting.IntVal("bucket.inline_segments").Observe(bucket.InlineSegments)
|
|
monAccounting.IntVal("bucket.remote_segments").Observe(bucket.RemoteSegments)
|
|
|
|
monAccounting.IntVal("bucket.bytes").Observe(bucket.Bytes())
|
|
monAccounting.IntVal("bucket.inline_bytes").Observe(bucket.InlineBytes)
|
|
monAccounting.IntVal("bucket.remote_bytes").Observe(bucket.RemoteBytes)
|
|
total.Combine(bucket)
|
|
}
|
|
monAccounting.IntVal("total.objects").Observe(total.ObjectCount) //locked
|
|
|
|
monAccounting.IntVal("total.segments").Observe(total.Segments()) //locked
|
|
monAccounting.IntVal("total.inline_segments").Observe(total.InlineSegments) //locked
|
|
monAccounting.IntVal("total.remote_segments").Observe(total.RemoteSegments) //locked
|
|
|
|
monAccounting.IntVal("total.bytes").Observe(total.Bytes()) //locked
|
|
monAccounting.IntVal("total.inline_bytes").Observe(total.InlineBytes) //locked
|
|
monAccounting.IntVal("total.remote_bytes").Observe(total.RemoteBytes) //locked
|
|
}
|
|
|
|
// return errors if something went wrong.
|
|
return errs.Combine(errAtRest, errBucketInfo)
|
|
}
|
|
|
|
var _ metainfo.Observer = (*Observer)(nil)
|
|
|
|
// Observer observes metainfo and adds up tallies for nodes and buckets
|
|
type Observer struct {
|
|
Log *zap.Logger
|
|
Node map[storj.NodeID]float64
|
|
Bucket map[string]*accounting.BucketTally
|
|
}
|
|
|
|
// NewObserver returns an metainfo loop observer that adds up totals for buckets and nodes.
|
|
func NewObserver(log *zap.Logger) *Observer {
|
|
return &Observer{
|
|
Log: log,
|
|
Node: make(map[storj.NodeID]float64),
|
|
Bucket: make(map[string]*accounting.BucketTally),
|
|
}
|
|
}
|
|
|
|
// ensureBucket returns bucket corresponding to the passed in path
|
|
func (observer *Observer) ensureBucket(ctx context.Context, path metainfo.ScopedPath) *accounting.BucketTally {
|
|
bucketID := storj.JoinPaths(path.ProjectIDString, path.BucketName)
|
|
|
|
bucket, exists := observer.Bucket[bucketID]
|
|
if !exists {
|
|
bucket = &accounting.BucketTally{}
|
|
bucket.ProjectID = path.ProjectID
|
|
bucket.BucketName = []byte(path.BucketName)
|
|
observer.Bucket[bucketID] = bucket
|
|
}
|
|
|
|
return bucket
|
|
}
|
|
|
|
// Object is called for each object once.
|
|
func (observer *Observer) Object(ctx context.Context, path metainfo.ScopedPath, pointer *pb.Pointer) (err error) {
|
|
bucket := observer.ensureBucket(ctx, path)
|
|
bucket.ObjectCount++
|
|
return nil
|
|
}
|
|
|
|
// InlineSegment is called for each inline segment.
|
|
func (observer *Observer) InlineSegment(ctx context.Context, path metainfo.ScopedPath, pointer *pb.Pointer) (err error) {
|
|
bucket := observer.ensureBucket(ctx, path)
|
|
bucket.InlineSegments++
|
|
bucket.InlineBytes += int64(len(pointer.InlineSegment))
|
|
bucket.MetadataSize += int64(len(pointer.Metadata))
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoteSegment is called for each remote segment.
|
|
func (observer *Observer) RemoteSegment(ctx context.Context, path metainfo.ScopedPath, pointer *pb.Pointer) (err error) {
|
|
bucket := observer.ensureBucket(ctx, path)
|
|
bucket.RemoteSegments++
|
|
bucket.RemoteBytes += pointer.GetSegmentSize()
|
|
bucket.MetadataSize += int64(len(pointer.Metadata))
|
|
|
|
// add node info
|
|
remote := pointer.GetRemote()
|
|
redundancy := remote.GetRedundancy()
|
|
segmentSize := pointer.GetSegmentSize()
|
|
minimumRequired := redundancy.GetMinReq()
|
|
|
|
if remote == nil || redundancy == nil || minimumRequired <= 0 {
|
|
observer.Log.Error("failed sanity check", zap.String("path", path.Raw))
|
|
return nil
|
|
}
|
|
|
|
pieceSize := float64(segmentSize / int64(minimumRequired))
|
|
for _, piece := range remote.GetRemotePieces() {
|
|
observer.Node[piece.NodeId] += pieceSize
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func projectTotalsFromBuckets(buckets map[string]*accounting.BucketTally) map[uuid.UUID]int64 {
|
|
projectTallyTotals := make(map[uuid.UUID]int64)
|
|
for _, bucket := range buckets {
|
|
projectTallyTotals[bucket.ProjectID] += (bucket.InlineBytes + bucket.RemoteBytes)
|
|
}
|
|
return projectTallyTotals
|
|
}
|
|
|
|
// using custom name to avoid breaking monitoring
|
|
var monAccounting = monkit.ScopeNamed("storj.io/storj/satellite/accounting")
|