ae5947327b
We want to eliminate usages of LoopSegmentEntry.Pieces, because it is costing a lot of cpu time to look up node IDs with every piece of every segment we read. In this change, we are eliminating use of LoopSegmentEntry.Pieces in the node tally observer (both the ranged loop and segments loop variants). It is not necessary to have a fully resolved nodeID until it is time to store totals in the database. We can use NodeAliases as the map key instead, and resolve NodeIDs just before storing totals. Refs: https://github.com/storj/storj/issues/5622 Change-Id: Iec12aa393072436d7c22cc5a4ae1b63966cbcc18
190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
// Copyright (C) 2021 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package nodetally
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
|
"github.com/zeebo/errs"
|
|
"go.uber.org/zap"
|
|
|
|
"storj.io/common/storj"
|
|
"storj.io/common/sync2"
|
|
"storj.io/storj/satellite/accounting"
|
|
"storj.io/storj/satellite/metabase"
|
|
"storj.io/storj/satellite/metabase/segmentloop"
|
|
)
|
|
|
|
// Error is a standard error class for this package.
|
|
var (
|
|
Error = errs.Class("node tally")
|
|
mon = monkit.Package()
|
|
)
|
|
|
|
// Service is the tally service for data stored on each storage node.
|
|
//
|
|
// architecture: Chore
|
|
type Service struct {
|
|
log *zap.Logger
|
|
Loop *sync2.Cycle
|
|
|
|
segmentLoop *segmentloop.Service
|
|
storagenodeAccountingDB accounting.StoragenodeAccounting
|
|
metabaseDB *metabase.DB
|
|
nowFn func() time.Time
|
|
}
|
|
|
|
// New creates a new node tally Service.
|
|
func New(log *zap.Logger, sdb accounting.StoragenodeAccounting, mdb *metabase.DB, loop *segmentloop.Service, interval time.Duration) *Service {
|
|
return &Service{
|
|
log: log,
|
|
Loop: sync2.NewCycle(interval),
|
|
|
|
segmentLoop: loop,
|
|
storagenodeAccountingDB: sdb,
|
|
metabaseDB: mdb,
|
|
nowFn: time.Now,
|
|
}
|
|
}
|
|
|
|
// Run the node 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("node 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
|
|
}
|
|
|
|
// SetNow allows tests to have the Service act as if the current time is whatever
|
|
// they want. This avoids races and sleeping, making tests more reliable and efficient.
|
|
func (service *Service) SetNow(now func() time.Time) {
|
|
service.nowFn = now
|
|
}
|
|
|
|
// for backwards compatibility.
|
|
var monTally = monkit.ScopeNamed("storj.io/storj/satellite/accounting/tally")
|
|
|
|
// Tally calculates data-at-rest usage once.
|
|
func (service *Service) Tally(ctx context.Context) (err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
// Fetch when the last node 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 = service.nowFn()
|
|
}
|
|
|
|
// add up all nodes
|
|
observer := NewObserver(service.log.Named("observer"), service.nowFn())
|
|
err = service.segmentLoop.Join(ctx, observer)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
finishTime := service.nowFn()
|
|
|
|
// calculate byte hours, not just bytes
|
|
hours := time.Since(lastTime).Hours()
|
|
var totalSum float64
|
|
for id, pieceSize := range observer.Node {
|
|
totalSum += pieceSize
|
|
observer.Node[id] = pieceSize * hours
|
|
}
|
|
monTally.IntVal("nodetallies.totalsum").Observe(int64(totalSum)) //mon:locked
|
|
|
|
if len(observer.Node) > 0 {
|
|
nodeIDs := make([]storj.NodeID, 0, len(observer.Node))
|
|
nodeTotals := make([]float64, 0, len(observer.Node))
|
|
nodeAliasMap, err := service.metabaseDB.LatestNodesAliasMap(ctx)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
for nodeAlias, total := range observer.Node {
|
|
nodeID, ok := nodeAliasMap.Node(nodeAlias)
|
|
if !ok {
|
|
observer.log.Error("unrecognized node alias in tally", zap.Int32("node-alias", int32(nodeAlias)))
|
|
continue
|
|
}
|
|
nodeIDs = append(nodeIDs, nodeID)
|
|
nodeTotals = append(nodeTotals, total)
|
|
}
|
|
err = service.storagenodeAccountingDB.SaveTallies(ctx, finishTime, nodeIDs, nodeTotals)
|
|
if err != nil {
|
|
return Error.New("StorageNodeAccounting.SaveTallies failed: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var _ segmentloop.Observer = (*Observer)(nil)
|
|
|
|
// Observer observes metainfo and adds up tallies for nodes and buckets.
|
|
type Observer struct {
|
|
log *zap.Logger
|
|
now time.Time
|
|
|
|
Node map[metabase.NodeAlias]float64
|
|
}
|
|
|
|
// NewObserver returns an segment loop observer that adds up totals for nodes.
|
|
func NewObserver(log *zap.Logger, now time.Time) *Observer {
|
|
return &Observer{
|
|
log: log,
|
|
now: now,
|
|
|
|
Node: make(map[metabase.NodeAlias]float64),
|
|
}
|
|
}
|
|
|
|
// LoopStarted is called at each start of a loop.
|
|
func (observer *Observer) LoopStarted(context.Context, segmentloop.LoopInfo) (err error) {
|
|
return nil
|
|
}
|
|
|
|
// RemoteSegment is called for each remote segment.
|
|
func (observer *Observer) RemoteSegment(ctx context.Context, segment *segmentloop.Segment) error {
|
|
// we are expliticy not adding monitoring here as we are tracking loop observers separately
|
|
|
|
if segment.Expired(observer.now) {
|
|
return nil
|
|
}
|
|
|
|
// add node info
|
|
minimumRequired := segment.Redundancy.RequiredShares
|
|
|
|
if minimumRequired <= 0 {
|
|
observer.log.Error("failed sanity check", zap.String("StreamID", segment.StreamID.String()), zap.Uint64("Position", segment.Position.Encode()))
|
|
return nil
|
|
}
|
|
|
|
pieceSize := float64(segment.EncryptedSize / int32(minimumRequired)) // TODO: Add this as a method to RedundancyScheme
|
|
|
|
for _, piece := range segment.AliasPieces {
|
|
observer.Node[piece.Alias] += pieceSize
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InlineSegment is called for each inline segment.
|
|
func (observer *Observer) InlineSegment(ctx context.Context, segment *segmentloop.Segment) (err error) {
|
|
return nil
|
|
}
|