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 }