github.com/grafana/pyroscope@v1.18.0/pkg/metastore/fsm/boltdb.go (about)

     1  package fsm
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"time"
     9  
    10  	"github.com/go-kit/log"
    11  	"github.com/go-kit/log/level"
    12  	"go.etcd.io/bbolt"
    13  )
    14  
    15  // TODO(kolesnikovae): Parametrize.
    16  const (
    17  	boltDBFileName        = "metastore.boltdb"
    18  	boltDBSnapshotName    = "metastore_snapshot.boltdb"
    19  	boltDBCompactedName   = "metastore_compacted.boltdb"
    20  	boltDBInitialMmapSize = 1 << 30
    21  
    22  	boltDBCompactionMaxTxnSize = 1 << 20
    23  )
    24  
    25  type boltdb struct {
    26  	logger  log.Logger
    27  	metrics *metrics
    28  	boltdb  *bbolt.DB
    29  	config  Config
    30  	path    string
    31  }
    32  
    33  func newDB(logger log.Logger, metrics *metrics, config Config) *boltdb {
    34  	return &boltdb{
    35  		logger:  logger,
    36  		metrics: metrics,
    37  		config:  config,
    38  	}
    39  }
    40  
    41  // open creates a new or opens an existing boltdb database.
    42  //
    43  // The only case in which we open the database in read-only mode is when we
    44  // restore it from a snapshot: before closing the database in use, we open
    45  // the snapshot in read-only mode to verify its integrity.
    46  //
    47  // Read-only mode guarantees the snapshot won't be corrupted by the current
    48  // process and allows loading the database more quickly (by skipping the
    49  // free page list preload).
    50  func (db *boltdb) open(readOnly bool) (err error) {
    51  	defer func() {
    52  		if err != nil {
    53  			// If the initialization fails, initialized components
    54  			// should be de-initialized gracefully.
    55  			db.shutdown()
    56  		}
    57  	}()
    58  
    59  	if err = os.MkdirAll(db.config.DataDir, 0755); err != nil {
    60  		return fmt.Errorf("db dir: %w", err)
    61  	}
    62  
    63  	if db.path == "" {
    64  		db.path = filepath.Join(db.config.DataDir, boltDBFileName)
    65  	}
    66  
    67  	opts := *bbolt.DefaultOptions
    68  	// open is called with readOnly=true to verify the snapshot integrity.
    69  	opts.ReadOnly = readOnly
    70  	opts.PreLoadFreelist = !readOnly
    71  	if !readOnly {
    72  		// If we open the DB for restoration/compaction, we don't need
    73  		// a large mmap size as no writes are performed.
    74  		opts.InitialMmapSize = boltDBInitialMmapSize
    75  	}
    76  	// Because of the nature of the metastore, we do not need to sync
    77  	// the database: the state is always restored from the snapshot.
    78  	opts.NoSync = true
    79  	opts.NoGrowSync = true
    80  	opts.NoFreelistSync = true
    81  	opts.FreelistType = bbolt.FreelistMapType
    82  	if db.boltdb, err = bbolt.Open(db.path, 0644, &opts); err != nil {
    83  		return fmt.Errorf("failed to open db: %w", err)
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  func (db *boltdb) shutdown() {
    90  	if db.boltdb != nil {
    91  		if err := db.boltdb.Sync(); err != nil {
    92  			level.Error(db.logger).Log("msg", "failed to sync database", "err", err)
    93  		}
    94  		if err := db.boltdb.Close(); err != nil {
    95  			level.Error(db.logger).Log("msg", "failed to close database", "err", err)
    96  		}
    97  	}
    98  }
    99  
   100  func (db *boltdb) restore(snapshot io.Reader) error {
   101  	start := time.Now()
   102  	defer func() {
   103  		db.metrics.boltDBRestoreSnapshotDuration.Observe(time.Since(start).Seconds())
   104  	}()
   105  
   106  	path, err := db.copySnapshot(snapshot)
   107  	if err != nil {
   108  		_ = os.RemoveAll(path)
   109  		return fmt.Errorf("failed to copy snapshot: %w", err)
   110  	}
   111  
   112  	// Open in Read-Only mode to ensure the snapshot is not corrupted.
   113  	restored := &boltdb{
   114  		logger:  db.logger,
   115  		metrics: db.metrics,
   116  		config:  db.config,
   117  		path:    path,
   118  	}
   119  	if err = restored.open(true); err != nil {
   120  		restored.shutdown()
   121  		if removeErr := os.RemoveAll(restored.path); removeErr != nil {
   122  			level.Error(db.logger).Log("msg", "failed to remove compacted snapshot", "err", removeErr)
   123  		}
   124  		return fmt.Errorf("failed to open restored snapshot: %w", err)
   125  	}
   126  
   127  	if !db.config.SnapshotCompactOnRestore {
   128  		restored.shutdown()
   129  		return db.openPath(path)
   130  	}
   131  
   132  	// Snapshot is a full copy of the database, therefore we copy
   133  	// it on disk, compact, and use it instead of the current database.
   134  	// Compacting the snapshot is necessary to reclaim the space
   135  	// that the source database no longer has use for. This is rather
   136  	// wasteful to do it at restoration time, but it helps to reduce
   137  	// the footprint and latencies caused by the snapshot capturing.
   138  	// Ideally, this should be done in the background, outside the
   139  	// snapshot-restore path; however, this will require broad locks
   140  	// and will impact the transactions.
   141  	compacted, compactErr := restored.compact()
   142  	// Regardless of the compaction result, we need to close the restored
   143  	// db as we're going to either remove it (and use the compacted version),
   144  	// or move it and open for writes.
   145  	restored.shutdown()
   146  	if compactErr != nil {
   147  		// If compaction failed, we want to try the original snapshot.
   148  		// It's know that it is not corrupted, but it may be larger.
   149  		// For clarity: this step is not required (path == restored.path).
   150  		path = restored.path
   151  		level.Error(db.logger).Log("msg", "failed to compact boltdb; skipping compaction", "err", compactErr)
   152  		if compacted != nil {
   153  			level.Warn(db.logger).Log("msg", "trying to delete compacted snapshot", "path", compacted.path)
   154  			if removeErr := os.RemoveAll(compacted.path); removeErr != nil {
   155  				level.Error(db.logger).Log("msg", "failed to remove compacted snapshot", "err", removeErr)
   156  			}
   157  		}
   158  	} else {
   159  		// If compaction succeeded, we want to remove the restored snapshot.
   160  		path = compacted.path
   161  		if removeErr := os.RemoveAll(restored.path); removeErr != nil {
   162  			level.Error(db.logger).Log("msg", "failed to remove restored snapshot", "err", removeErr)
   163  		}
   164  	}
   165  
   166  	return db.openPath(path)
   167  }
   168  
   169  func (db *boltdb) copySnapshot(snapshot io.Reader) (path string, err error) {
   170  	path = filepath.Join(db.config.DataDir, boltDBSnapshotName)
   171  	level.Info(db.logger).Log("msg", "copying snapshot", "path", path)
   172  	snapFile, err := os.Create(path)
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	_, err = io.Copy(snapFile, snapshot)
   177  	if syncErr := syncFD(snapFile); err == nil {
   178  		err = syncErr
   179  	}
   180  	return path, err
   181  }
   182  
   183  func (db *boltdb) compact() (compacted *boltdb, err error) {
   184  	level.Info(db.logger).Log("msg", "compacting snapshot")
   185  	src := db.boltdb
   186  	compacted = &boltdb{
   187  		logger:  db.logger,
   188  		metrics: db.metrics,
   189  		config:  db.config,
   190  		path:    filepath.Join(db.config.DataDir, boltDBCompactedName),
   191  	}
   192  	if err = os.RemoveAll(compacted.path); err != nil {
   193  		return nil, fmt.Errorf("compacted db path cannot be deleted: %w", err)
   194  	}
   195  	if err = compacted.open(false); err != nil {
   196  		return nil, fmt.Errorf("failed to create db for compaction: %w", err)
   197  	}
   198  	defer compacted.shutdown()
   199  	dst := compacted.boltdb
   200  	if err = bbolt.Compact(dst, src, boltDBCompactionMaxTxnSize); err != nil {
   201  		return nil, fmt.Errorf("failed to compact db: %w", err)
   202  	}
   203  	level.Info(db.logger).Log("msg", "boltdb compaction ratio", "ratio", float64(compacted.size())/float64(db.size()))
   204  	return compacted, nil
   205  }
   206  
   207  func (db *boltdb) size() int64 {
   208  	fi, err := os.Stat(db.path)
   209  	if err != nil {
   210  		return 0
   211  	}
   212  	return fi.Size()
   213  }
   214  
   215  func (db *boltdb) openPath(path string) (err error) {
   216  	db.shutdown()
   217  	if err = os.Rename(path, db.path); err != nil {
   218  		return err
   219  	}
   220  	if err = syncPath(db.path); err != nil {
   221  		return err
   222  	}
   223  	return db.open(false)
   224  }
   225  
   226  func syncPath(path string) (err error) {
   227  	d, err := os.Open(path)
   228  	if err != nil {
   229  		return err
   230  	}
   231  	return syncFD(d)
   232  }
   233  
   234  func syncFD(f *os.File) (err error) {
   235  	err = f.Sync()
   236  	if closeErr := f.Close(); err == nil {
   237  		return closeErr
   238  	}
   239  	return err
   240  }