2019-01-24 20:15:10 +00:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
2018-11-08 16:18:28 +00:00
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
2018-11-14 01:22:18 +00:00
|
|
|
package tally
|
2018-11-08 16:18:28 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"time"
|
|
|
|
|
2019-02-01 18:50:12 +00:00
|
|
|
"github.com/zeebo/errs"
|
2018-11-08 16:18:28 +00:00
|
|
|
"go.uber.org/zap"
|
2019-10-15 18:00:14 +01:00
|
|
|
"gopkg.in/spacemonkeygo/monkit.v2"
|
2018-12-07 09:59:31 +00:00
|
|
|
|
2018-11-08 16:18:28 +00:00
|
|
|
"storj.io/storj/pkg/pb"
|
2018-11-29 18:39:27 +00:00
|
|
|
"storj.io/storj/pkg/storj"
|
2019-11-14 19:46:15 +00:00
|
|
|
"storj.io/storj/private/sync2"
|
2019-07-28 06:55:36 +01:00
|
|
|
"storj.io/storj/satellite/accounting"
|
2019-04-25 09:46:32 +01:00
|
|
|
"storj.io/storj/satellite/metainfo"
|
2018-11-08 16:18:28 +00:00
|
|
|
)
|
|
|
|
|
2019-10-04 20:09:52 +01:00
|
|
|
// Error is a standard error class for this package.
|
|
|
|
var (
|
|
|
|
Error = errs.Class("tally error")
|
|
|
|
mon = monkit.Package()
|
|
|
|
)
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// Config contains configurable values for the tally service
|
2019-01-23 19:58:44 +00:00
|
|
|
type Config struct {
|
2019-04-19 19:17:30 +01:00
|
|
|
Interval time.Duration `help:"how frequently the tally service should run" releaseDefault:"1h" devDefault:"30s"`
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// Service is the tally service for data stored on each storage node
|
2019-09-10 14:24:16 +01:00
|
|
|
//
|
|
|
|
// architecture: Chore
|
2019-04-04 16:20:59 +01:00
|
|
|
type Service struct {
|
2019-10-07 21:55:20 +01:00
|
|
|
log *zap.Logger
|
|
|
|
Loop sync2.Cycle
|
|
|
|
|
|
|
|
metainfoLoop *metainfo.Loop
|
2019-10-16 17:50:29 +01:00
|
|
|
liveAccounting accounting.Cache
|
2019-05-10 20:05:42 +01:00
|
|
|
storagenodeAccountingDB accounting.StoragenodeAccounting
|
|
|
|
projectAccountingDB accounting.ProjectAccounting
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// New creates a new tally Service
|
2019-10-16 17:50:29 +01:00
|
|
|
func New(log *zap.Logger, sdb accounting.StoragenodeAccounting, pdb accounting.ProjectAccounting, liveAccounting accounting.Cache, metainfoLoop *metainfo.Loop, interval time.Duration) *Service {
|
2019-04-04 16:20:59 +01:00
|
|
|
return &Service{
|
2019-10-07 21:55:20 +01:00
|
|
|
log: log,
|
|
|
|
Loop: *sync2.NewCycle(interval),
|
|
|
|
|
|
|
|
metainfoLoop: metainfoLoop,
|
|
|
|
liveAccounting: liveAccounting,
|
2019-05-10 20:05:42 +01:00
|
|
|
storagenodeAccountingDB: sdb,
|
|
|
|
projectAccountingDB: pdb,
|
2018-12-05 14:03:23 +00:00
|
|
|
}
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// Run the tally service loop
|
2019-10-07 21:55:20 +01:00
|
|
|
func (service *Service) Run(ctx context.Context) (err error) {
|
2018-11-08 16:18:28 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-10-07 21:55:20 +01:00
|
|
|
service.log.Info("Tally service starting up")
|
2019-03-18 10:55:06 +00:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
return service.Loop.Run(ctx, func(ctx context.Context) error {
|
|
|
|
err := service.Tally(ctx)
|
2019-09-09 17:48:24 +01:00
|
|
|
if err != nil {
|
2019-10-07 21:55:20 +01:00
|
|
|
service.log.Error("tally failed", zap.Error(err))
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
2019-09-09 17:48:24 +01:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close stops the service and releases any resources.
|
2019-10-07 21:55:20 +01:00
|
|
|
func (service *Service) Close() error {
|
|
|
|
service.Loop.Close()
|
2019-09-09 17:48:24 +01:00
|
|
|
return nil
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
|
2019-04-09 14:48:35 +01:00
|
|
|
// Tally calculates data-at-rest usage once
|
2019-10-07 21:55:20 +01:00
|
|
|
func (service *Service) Tally(ctx context.Context) (err error) {
|
2019-06-04 12:36:27 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-10-07 21:55:20 +01:00
|
|
|
|
2019-05-10 02:39:21 +01:00
|
|
|
// The live accounting store will only keep a delta to space used relative
|
|
|
|
// to the latest tally. Since a new tally is beginning, we will zero it out
|
|
|
|
// now. There is a window between this call and the point where the tally DB
|
|
|
|
// transaction starts, during which some changes in space usage may be
|
|
|
|
// double-counted (counted in the tally and also counted as a delta to
|
|
|
|
// the tally). If that happens, it will be fixed at the time of the next
|
|
|
|
// tally run.
|
2019-10-16 17:50:29 +01:00
|
|
|
err = service.liveAccounting.ResetTotals(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
2019-05-10 02:39:21 +01:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
// Fetch when the last tally happened so we can roughly calculate the byte-hours.
|
|
|
|
lastTime, err := service.storagenodeAccountingDB.LastTimestamp(ctx, accounting.LastAtRestTally)
|
2019-02-01 18:50:12 +00:00
|
|
|
if err != nil {
|
2019-10-07 21:55:20 +01:00
|
|
|
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 {
|
|
|
|
err = service.projectAccountingDB.SaveTallies(ctx, finishTime, observer.Bucket)
|
|
|
|
if err != nil {
|
|
|
|
errAtRest = errs.New("ProjectAccounting.SaveTallies failed: %v", err)
|
2019-04-01 14:42:17 +01:00
|
|
|
}
|
2019-10-07 21:55:20 +01:00
|
|
|
}
|
2019-06-13 17:58:40 +01:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
// report bucket metrics
|
|
|
|
if len(observer.Bucket) > 0 {
|
|
|
|
var total accounting.BucketTally
|
|
|
|
for _, bucket := range observer.Bucket {
|
2019-10-15 18:00:14 +01:00
|
|
|
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)
|
2019-10-07 21:55:20 +01:00
|
|
|
total.Combine(bucket)
|
2019-02-01 18:50:12 +00:00
|
|
|
}
|
2019-10-15 18:00:14 +01:00
|
|
|
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
|
2019-02-01 18:50:12 +00:00
|
|
|
}
|
2019-09-09 17:48:24 +01:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
// return errors if something went wrong.
|
2019-04-04 16:20:59 +01:00
|
|
|
return errs.Combine(errAtRest, errBucketInfo)
|
2019-02-01 18:50:12 +00:00
|
|
|
}
|
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
var _ metainfo.Observer = (*Observer)(nil)
|
2019-01-16 19:30:33 +00:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
// 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
|
|
|
|
}
|
2019-02-26 15:17:51 +00:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
// 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),
|
2018-12-18 17:18:42 +00:00
|
|
|
}
|
2019-10-07 21:55:20 +01:00
|
|
|
}
|
2019-02-26 15:17:51 +00:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
// 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)
|
2019-04-29 18:46:38 +01:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
bucket, exists := observer.Bucket[bucketID]
|
|
|
|
if !exists {
|
|
|
|
bucket = &accounting.BucketTally{}
|
|
|
|
bucket.ProjectID = path.ProjectID
|
|
|
|
bucket.BucketName = []byte(path.BucketName)
|
|
|
|
observer.Bucket[bucketID] = bucket
|
2018-12-18 17:18:42 +00:00
|
|
|
}
|
2019-10-07 21:55:20 +01:00
|
|
|
|
|
|
|
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
|
2019-02-01 18:50:12 +00:00
|
|
|
}
|
2019-10-04 20:09:52 +01:00
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
// 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
|
2019-10-04 20:09:52 +01:00
|
|
|
}
|
|
|
|
|
2019-10-07 21:55:20 +01:00
|
|
|
pieceSize := float64(segmentSize / int64(minimumRequired))
|
|
|
|
for _, piece := range remote.GetRemotePieces() {
|
|
|
|
observer.Node[piece.NodeId] += pieceSize
|
2019-10-04 20:09:52 +01:00
|
|
|
}
|
2019-10-07 21:55:20 +01:00
|
|
|
return nil
|
2019-10-04 20:09:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// using custom name to avoid breaking monitoring
|
|
|
|
var monAccounting = monkit.ScopeNamed("storj.io/storj/satellite/accounting")
|