mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-04 02:57:44 +01:00
* master, volume, filer: start on a read-only mounted directory and serve existing data read-only A read-only mount was treated as writable (permission bits only), so every service failed on its first write: the master panicked writing raft conf.tmp, the filer exited opening the leveldb LOCK, and the volume server skipped every volume while still offering the directory as free capacity. - util.IsReadOnlyMount: access(W_OK) == EROFS. - volume: on a read-only -dir take the existing O_RDONLY/noWriteOrDelete load, index .idx in memory when no .sdx exists, offer no free slots (same branches as isDiskSpaceLow), skip the vid stamp there. - master: on a read-only -mdir run raft from a private copy and refuse new volume ids. - filer: leveldb2/leveldb3 set their existing ReadOnly option. * volume: skip read-only mounts when picking EC shard copy targets A shard copy landing on a read-only mount fails at write time; offer only writable locations like the other placement paths do. * volume: mirror the read-only mount handling in the Rust volume server The .dat open only fell back to read-only on EACCES, so a real read-only mount (EROFS) still failed the load. The index side already degrades once no_write_or_delete is set. Add is_read_only_mount via access(W_OK) == EROFS, mark DiskLocation.read_only_mount, and mirror the Go checks: no free slots, heartbeat max = used, no vid-stamp writes, no EC shard copy targets, and a read-only index dir forces the volume read-only. * volume: write vid stamps while only the index dir is read-only * filer: skip the write-bit check when a leveldb store opens read-only * filer: serve reads of buckets with no leveldb3 dir on a read-only store * volume: derive a stable dir uuid when a read-only mount cannot store one * master: keep the read-only raft copy across restarts so votes survive * volume: index a missing .idx in memory on a read-only mount * volume: release the .idx handle after an in-memory read-only load * volume: mount EC shards read-only when their index dir is on a read-only mount * volume: mark the index dir read-only before the tail repair opens .idx * volume: write vid stamps while only the index dir is read-only (Rust) * volume: derive a stable dir uuid and report no free slots on a read-only mount (Rust) * volume: index a missing .idx in memory on a read-only mount (Rust) * volume: refuse an EC copy whose shard owner is on a read-only disk * master: key the read-only raft copy by the absolute -mdir Two masters sharing the same relative -mdir text from different working directories would have collided on one temp copy and shared raft state. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: mix device and host into the read-only dir uuid Deriving the uuid from the path alone let independent servers at the same mount point report one uuid, which RegisterUuids rejects. Hash in the device id and hostname so restarts stay stable but disks differ. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: vic6996-web <320442181+vic6996-web@users.noreply.github.com> Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
333 lines
9.4 KiB
Go
333 lines
9.4 KiB
Go
package leveldb
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
|
|
"github.com/syndtr/goleveldb/leveldb"
|
|
leveldb_errors "github.com/syndtr/goleveldb/leveldb/errors"
|
|
"github.com/syndtr/goleveldb/leveldb/filter"
|
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
|
leveldb_util "github.com/syndtr/goleveldb/leveldb/util"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
weed_util "github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func init() {
|
|
filer.Stores = append(filer.Stores, &LevelDB2Store{})
|
|
}
|
|
|
|
type LevelDB2Store struct {
|
|
dbs []*leveldb.DB
|
|
dbCount int
|
|
ReadOnly bool
|
|
|
|
blockCacheSizeMB int
|
|
writeBufferSizeMB int
|
|
blockSizeKB int
|
|
compactionTableSizeMB int
|
|
}
|
|
|
|
func (store *LevelDB2Store) GetName() string {
|
|
return "leveldb2"
|
|
}
|
|
|
|
func (store *LevelDB2Store) Initialize(configuration weed_util.Configuration, prefix string) (err error) {
|
|
dir := configuration.GetString(prefix + "dir")
|
|
store.blockCacheSizeMB = configuration.GetInt(prefix + "block_cache_size_mb")
|
|
store.writeBufferSizeMB = configuration.GetInt(prefix + "write_buffer_size_mb")
|
|
store.blockSizeKB = configuration.GetInt(prefix + "block_size_kb")
|
|
store.compactionTableSizeMB = configuration.GetInt(prefix + "compaction_table_size_mb")
|
|
if err := store.validateSizeOptions(); err != nil {
|
|
return err
|
|
}
|
|
return store.initialize(dir, 8)
|
|
}
|
|
|
|
// validateSizeOptions rejects sizes too large to convert to bytes without
|
|
// overflowing int on this platform.
|
|
func (store *LevelDB2Store) validateSizeOptions() error {
|
|
for name, kv := range map[string][2]int{
|
|
"block_cache_size_mb": {store.blockCacheSizeMB, 1 << 20},
|
|
"write_buffer_size_mb": {store.writeBufferSizeMB, 1 << 20},
|
|
"block_size_kb": {store.blockSizeKB, 1 << 10},
|
|
"compaction_table_size_mb": {store.compactionTableSizeMB, 1 << 20},
|
|
} {
|
|
if kv[0] > math.MaxInt/kv[1] {
|
|
return fmt.Errorf("leveldb2 option %s=%d overflows int", name, kv[0])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) buildOptions() *opt.Options {
|
|
opts := &opt.Options{
|
|
BlockCacheCapacity: 32 * 1024 * 1024, // default value is 8MiB
|
|
WriteBuffer: 16 * 1024 * 1024, // default value is 4MiB
|
|
Filter: filter.NewBloomFilter(8), // false positive rate 0.02
|
|
ReadOnly: store.ReadOnly,
|
|
}
|
|
if store.blockCacheSizeMB > 0 {
|
|
opts.BlockCacheCapacity = store.blockCacheSizeMB * 1024 * 1024
|
|
}
|
|
if store.writeBufferSizeMB > 0 {
|
|
opts.WriteBuffer = store.writeBufferSizeMB * 1024 * 1024
|
|
}
|
|
if store.blockSizeKB > 0 {
|
|
opts.BlockSize = store.blockSizeKB * 1024
|
|
}
|
|
if store.compactionTableSizeMB > 0 {
|
|
opts.CompactionTableSize = store.compactionTableSizeMB * 1024 * 1024
|
|
}
|
|
return opts
|
|
}
|
|
|
|
func (store *LevelDB2Store) initialize(dir string, dbCount int) (err error) {
|
|
glog.Infof("filer store leveldb2 dir: %s", dir)
|
|
os.MkdirAll(dir, 0755)
|
|
if weed_util.IsReadOnlyMount(dir) {
|
|
glog.V(0).Infof("filer store %s is on a read-only filesystem, opening it read-only", dir)
|
|
store.ReadOnly = true
|
|
}
|
|
if !store.ReadOnly {
|
|
if err := weed_util.TestFolderWritable(dir); err != nil {
|
|
return fmt.Errorf("Check Level Folder %s Writable: %s", dir, err)
|
|
}
|
|
}
|
|
|
|
opts := store.buildOptions()
|
|
|
|
for d := 0; d < dbCount; d++ {
|
|
dbFolder := fmt.Sprintf("%s/%02d", dir, d)
|
|
os.MkdirAll(dbFolder, 0755)
|
|
db, dbErr := leveldb.OpenFile(dbFolder, opts)
|
|
if leveldb_errors.IsCorrupted(dbErr) {
|
|
db, dbErr = leveldb.RecoverFile(dbFolder, opts)
|
|
}
|
|
if dbErr != nil {
|
|
glog.Errorf("filer store open dir %s: %v", dbFolder, dbErr)
|
|
return dbErr
|
|
}
|
|
store.dbs = append(store.dbs, db)
|
|
}
|
|
store.dbCount = dbCount
|
|
|
|
return
|
|
}
|
|
|
|
func (store *LevelDB2Store) BeginTransaction(ctx context.Context) (context.Context, error) {
|
|
return ctx, nil
|
|
}
|
|
func (store *LevelDB2Store) CommitTransaction(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
func (store *LevelDB2Store) RollbackTransaction(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
var syncWriteOptions = &opt.WriteOptions{Sync: true}
|
|
|
|
func writeOptions(ctx context.Context) *opt.WriteOptions {
|
|
if filer.SyncWrite(ctx) {
|
|
return syncWriteOptions
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) InsertEntry(ctx context.Context, entry *filer.Entry) (err error) {
|
|
dir, name := entry.DirAndName()
|
|
key, partitionId := genKey(dir, name, store.dbCount)
|
|
|
|
value, err := entry.EncodeAttributesAndChunks()
|
|
if err != nil {
|
|
return fmt.Errorf("encoding %s %+v: %v", entry.FullPath, entry.Attr, err)
|
|
}
|
|
|
|
if len(entry.GetChunks()) > filer.CountEntryChunksForGzip {
|
|
value = weed_util.MaybeGzipData(value)
|
|
}
|
|
|
|
err = store.dbs[partitionId].Put(key, value, writeOptions(ctx))
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("persisting %s : %v", entry.FullPath, err)
|
|
}
|
|
|
|
// println("saved", entry.FullPath, "chunks", len(entry.GetChunks()))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) UpdateEntry(ctx context.Context, entry *filer.Entry) (err error) {
|
|
|
|
return store.InsertEntry(ctx, entry)
|
|
}
|
|
|
|
func (store *LevelDB2Store) FindEntry(ctx context.Context, fullpath weed_util.FullPath) (entry *filer.Entry, err error) {
|
|
dir, name := fullpath.DirAndName()
|
|
key, partitionId := genKey(dir, name, store.dbCount)
|
|
|
|
data, err := store.dbs[partitionId].Get(key, nil)
|
|
|
|
if err == leveldb.ErrNotFound {
|
|
return nil, filer_pb.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get %s : %v", fullpath, err)
|
|
}
|
|
|
|
entry = &filer.Entry{
|
|
FullPath: fullpath,
|
|
}
|
|
err = entry.DecodeAttributesAndChunks(weed_util.MaybeDecompressData(data))
|
|
if err != nil {
|
|
return entry, fmt.Errorf("decode %s : %v", entry.FullPath, err)
|
|
}
|
|
|
|
// println("read", entry.FullPath, "chunks", len(entry.GetChunks()), "data", len(data), string(data))
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) DeleteEntry(ctx context.Context, fullpath weed_util.FullPath) (err error) {
|
|
dir, name := fullpath.DirAndName()
|
|
key, partitionId := genKey(dir, name, store.dbCount)
|
|
|
|
err = store.dbs[partitionId].Delete(key, writeOptions(ctx))
|
|
if err != nil {
|
|
return fmt.Errorf("delete %s : %v", fullpath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) DeleteFolderChildren(ctx context.Context, fullpath weed_util.FullPath) (err error) {
|
|
directoryPrefix, partitionId := genDirectoryKeyPrefix(fullpath, "", store.dbCount)
|
|
|
|
batch := new(leveldb.Batch)
|
|
|
|
iter := store.dbs[partitionId].NewIterator(&leveldb_util.Range{Start: directoryPrefix}, nil)
|
|
for iter.Next() {
|
|
key := iter.Key()
|
|
if !bytes.HasPrefix(key, directoryPrefix) {
|
|
break
|
|
}
|
|
fileName := getNameFromKey(key)
|
|
if fileName == "" {
|
|
continue
|
|
}
|
|
batch.Delete(append(directoryPrefix, []byte(fileName)...))
|
|
}
|
|
iter.Release()
|
|
|
|
err = store.dbs[partitionId].Write(batch, writeOptions(ctx))
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("delete %s : %v", fullpath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) ListDirectoryEntries(ctx context.Context, dirPath weed_util.FullPath, startFileName string, includeStartFile bool, limit int64, eachEntryFunc filer.ListEachEntryFunc) (lastFileName string, err error) {
|
|
return store.ListDirectoryPrefixedEntries(ctx, dirPath, startFileName, includeStartFile, limit, "", eachEntryFunc)
|
|
}
|
|
|
|
func (store *LevelDB2Store) ListDirectoryPrefixedEntries(ctx context.Context, dirPath weed_util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc filer.ListEachEntryFunc) (lastFileName string, err error) {
|
|
|
|
directoryPrefix, partitionId := genDirectoryKeyPrefix(dirPath, prefix, store.dbCount)
|
|
lastFileStart := directoryPrefix
|
|
if startFileName > prefix {
|
|
lastFileStart, _ = genDirectoryKeyPrefix(dirPath, startFileName, store.dbCount)
|
|
}
|
|
|
|
iter := store.dbs[partitionId].NewIterator(&leveldb_util.Range{Start: lastFileStart}, nil)
|
|
for iter.Next() {
|
|
key := iter.Key()
|
|
if !bytes.HasPrefix(key, directoryPrefix) {
|
|
break
|
|
}
|
|
fileName := getNameFromKey(key)
|
|
if fileName == "" {
|
|
continue
|
|
}
|
|
if fileName == startFileName && !includeStartFile {
|
|
continue
|
|
}
|
|
limit--
|
|
if limit < 0 {
|
|
break
|
|
}
|
|
lastFileName = fileName
|
|
entry := &filer.Entry{
|
|
FullPath: weed_util.NewFullPath(string(dirPath), fileName),
|
|
}
|
|
|
|
// println("list", entry.FullPath, "chunks", len(entry.GetChunks()))
|
|
if decodeErr := entry.DecodeAttributesAndChunks(weed_util.MaybeDecompressData(iter.Value())); decodeErr != nil {
|
|
err = decodeErr
|
|
glog.V(0).InfofCtx(ctx, "list %s : %v", entry.FullPath, err)
|
|
break
|
|
}
|
|
|
|
resEachEntryFunc, resEachEntryFuncErr := eachEntryFunc(entry)
|
|
if resEachEntryFuncErr != nil {
|
|
err = fmt.Errorf("failed to process eachEntryFunc: %w", resEachEntryFuncErr)
|
|
break
|
|
}
|
|
|
|
if !resEachEntryFunc {
|
|
break
|
|
}
|
|
}
|
|
iter.Release()
|
|
|
|
return lastFileName, err
|
|
}
|
|
|
|
func genKey(dirPath, fileName string, dbCount int) (key []byte, partitionId int) {
|
|
key, partitionId = hashToBytes(dirPath, dbCount)
|
|
key = append(key, []byte(fileName)...)
|
|
return key, partitionId
|
|
}
|
|
|
|
func genDirectoryKeyPrefix(fullpath weed_util.FullPath, startFileName string, dbCount int) (keyPrefix []byte, partitionId int) {
|
|
keyPrefix, partitionId = hashToBytes(string(fullpath), dbCount)
|
|
if len(startFileName) > 0 {
|
|
keyPrefix = append(keyPrefix, []byte(startFileName)...)
|
|
}
|
|
return keyPrefix, partitionId
|
|
}
|
|
|
|
func getNameFromKey(key []byte) string {
|
|
|
|
return string(key[md5.Size:])
|
|
|
|
}
|
|
|
|
// hash directory, and use last byte for partitioning
|
|
func hashToBytes(dir string, dbCount int) ([]byte, int) {
|
|
h := md5.New()
|
|
io.WriteString(h, dir)
|
|
|
|
b := h.Sum(nil)
|
|
|
|
x := b[len(b)-1]
|
|
|
|
return b, int(x) % dbCount
|
|
}
|
|
|
|
func (store *LevelDB2Store) Shutdown() {
|
|
for d := 0; d < store.dbCount; d++ {
|
|
store.dbs[d].Close()
|
|
}
|
|
}
|