浏览代码

lib/db: Use different defaults for larger databases (fixes #5966) (#5967)

This introduces a better set of defaults for large databases. I've
experimentally determined that it results in much better throughput in a
couple of scenarios with large databases, but I can't give any
guarantees the values are always optimal. They're probably no worse than
the defaults though.
Jakob Borg 6 年之前
父节点
当前提交
90b70c7a16
共有 7 个文件被更改,包括 174 次插入18 次删除
  1. 2 2
      cmd/syncthing/main.go
  2. 1 0
      lib/config/optionsconfiguration.go
  3. 47 0
      lib/config/tuning.go
  4. 26 0
      lib/config/tuning_test.go
  5. 95 13
      lib/db/lowlevel.go
  6. 1 1
      lib/db/set_test.go
  7. 2 2
      lib/syncthing/utils.go

+ 2 - 2
cmd/syncthing/main.go

@@ -491,7 +491,7 @@ func checkUpgrade() upgrade.Release {
 
 func performUpgrade(release upgrade.Release) {
 	// Use leveldb database locks to protect against concurrent upgrades
-	_, err := syncthing.OpenGoleveldb(locations.Get(locations.Database))
+	_, err := syncthing.OpenGoleveldb(locations.Get(locations.Database), config.TuningAuto)
 	if err == nil {
 		err = upgrade.To(release)
 		if err != nil {
@@ -583,7 +583,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 	}
 
 	dbFile := locations.Get(locations.Database)
-	ldb, err := syncthing.OpenGoleveldb(dbFile)
+	ldb, err := syncthing.OpenGoleveldb(dbFile, cfg.Options().DatabaseTuning)
 	if err != nil {
 		l.Warnln("Error opening database:", err)
 		os.Exit(1)

+ 1 - 0
lib/config/optionsconfiguration.go

@@ -57,6 +57,7 @@ type OptionsConfiguration struct {
 	StunKeepaliveStartS     int      `xml:"stunKeepaliveStartS" json:"stunKeepaliveStartS" default:"180"` // 0 for off
 	StunKeepaliveMinS       int      `xml:"stunKeepaliveMinS" json:"stunKeepaliveMinS" default:"20"`      // 0 for off
 	StunServers             []string `xml:"stunServer" json:"stunServers" default:"default"`
+	DatabaseTuning          Tuning   `xml:"databaseTuning" json:"databaseTuning" restart:"true"`
 
 	DeprecatedUPnPEnabled        bool     `xml:"upnpEnabled,omitempty" json:"-"`
 	DeprecatedUPnPLeaseM         int      `xml:"upnpLeaseMinutes,omitempty" json:"-"`

+ 47 - 0
lib/config/tuning.go

@@ -0,0 +1,47 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package config
+
+type Tuning int
+
+const (
+	// N.b. these constants must match those in lib/db.Tuning!
+	TuningAuto Tuning = iota // default is auto
+	TuningSmall
+	TuningLarge
+)
+
+func (t Tuning) String() string {
+	switch t {
+	case TuningAuto:
+		return "auto"
+	case TuningSmall:
+		return "small"
+	case TuningLarge:
+		return "large"
+	default:
+		return "unknown"
+	}
+}
+
+func (t Tuning) MarshalText() ([]byte, error) {
+	return []byte(t.String()), nil
+}
+
+func (t *Tuning) UnmarshalText(bs []byte) error {
+	switch string(bs) {
+	case "auto":
+		*t = TuningAuto
+	case "small":
+		*t = TuningSmall
+	case "large":
+		*t = TuningLarge
+	default:
+		*t = TuningAuto
+	}
+	return nil
+}

+ 26 - 0
lib/config/tuning_test.go

@@ -0,0 +1,26 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package config_test
+
+import (
+	"testing"
+
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/db"
+)
+
+func TestTuningMatches(t *testing.T) {
+	if int(config.TuningAuto) != int(db.TuningAuto) {
+		t.Error("mismatch for TuningAuto")
+	}
+	if int(config.TuningSmall) != int(db.TuningSmall) {
+		t.Error("mismatch for TuningSmall")
+	}
+	if int(config.TuningLarge) != int(db.TuningLarge) {
+		t.Error("mismatch for TuningLarge")
+	}
+}

+ 95 - 13
lib/db/lowlevel.go

@@ -23,11 +23,26 @@ import (
 
 const (
 	dbMaxOpenFiles = 100
-	dbWriteBuffer  = 16 << 20
+	dbFlushBatch   = 4 << MiB
+
+	// A large database is > 200 MiB. It's a mostly arbitrary value, but
+	// it's also the case that each file is 2 MiB by default and when we
+	// have dbMaxOpenFiles of them we will need to start thrashing fd:s.
+	// Switching to large database settings causes larger files to be used
+	// when compacting, reducing the number.
+	dbLargeThreshold = dbMaxOpenFiles * (2 << MiB)
+
+	KiB = 10
+	MiB = 20
 )
 
-var (
-	dbFlushBatch = debugEnvValue("WriteBuffer", dbWriteBuffer) / 4 // Some leeway for any leveldb in-memory optimizations
+type Tuning int
+
+const (
+	// N.b. these constants must match those in lib/config.Tuning!
+	TuningAuto Tuning = iota
+	TuningSmall
+	TuningLarge
 )
 
 // Lowlevel is the lowest level database interface. It has a very simple
@@ -49,18 +64,58 @@ type Lowlevel struct {
 // Open attempts to open the database at the given location, and runs
 // recovery on it if opening fails. Worst case, if recovery is not possible,
 // the database is erased and created from scratch.
-func Open(location string) (*Lowlevel, error) {
+func Open(location string, tuning Tuning) (*Lowlevel, error) {
+	opts := optsFor(location, tuning)
+	return open(location, opts)
+}
+
+// optsFor returns the database options to use when opening a database with
+// the given location and tuning. Settings can be overridden by debug
+// environment variables.
+func optsFor(location string, tuning Tuning) *opt.Options {
+	large := false
+	switch tuning {
+	case TuningLarge:
+		large = true
+	case TuningAuto:
+		large = dbIsLarge(location)
+	}
+
+	var (
+		// Set defaults used for small databases.
+		defaultBlockCacheCapacity            = 0 // 0 means let leveldb use default
+		defaultBlockSize                     = 0
+		defaultCompactionTableSize           = 0
+		defaultCompactionTableSizeMultiplier = 0
+		defaultWriteBuffer                   = 16 << MiB                      // increased from leveldb default of 4 MiB
+		defaultCompactionL0Trigger           = opt.DefaultCompactionL0Trigger // explicit because we use it as base for other stuff
+	)
+
+	if large {
+		// Change the parameters for better throughput at the price of some
+		// RAM and larger files. This results in larger batches of writes
+		// and compaction at a lower frequency.
+		l.Infoln("Using large-database tuning")
+
+		defaultBlockCacheCapacity = 64 << MiB
+		defaultBlockSize = 64 << KiB
+		defaultCompactionTableSize = 16 << MiB
+		defaultCompactionTableSizeMultiplier = 20 // 2.0 after division by ten
+		defaultWriteBuffer = 64 << MiB
+		defaultCompactionL0Trigger = 8 // number of l0 files
+	}
+
 	opts := &opt.Options{
-		BlockCacheCapacity:            debugEnvValue("BlockCacheCapacity", 0),
+		BlockCacheCapacity:            debugEnvValue("BlockCacheCapacity", defaultBlockCacheCapacity),
 		BlockCacheEvictRemoved:        debugEnvValue("BlockCacheEvictRemoved", 0) != 0,
 		BlockRestartInterval:          debugEnvValue("BlockRestartInterval", 0),
-		BlockSize:                     debugEnvValue("BlockSize", 0),
+		BlockSize:                     debugEnvValue("BlockSize", defaultBlockSize),
 		CompactionExpandLimitFactor:   debugEnvValue("CompactionExpandLimitFactor", 0),
 		CompactionGPOverlapsFactor:    debugEnvValue("CompactionGPOverlapsFactor", 0),
-		CompactionL0Trigger:           debugEnvValue("CompactionL0Trigger", 0),
+		CompactionL0Trigger:           debugEnvValue("CompactionL0Trigger", defaultCompactionL0Trigger),
 		CompactionSourceLimitFactor:   debugEnvValue("CompactionSourceLimitFactor", 0),
-		CompactionTableSize:           debugEnvValue("CompactionTableSize", 0),
-		CompactionTableSizeMultiplier: float64(debugEnvValue("CompactionTableSizeMultiplier", 0)) / 10.0,
+		CompactionTableSize:           debugEnvValue("CompactionTableSize", defaultCompactionTableSize),
+		CompactionTableSizeMultiplier: float64(debugEnvValue("CompactionTableSizeMultiplier", defaultCompactionTableSizeMultiplier)) / 10.0,
 		CompactionTotalSize:           debugEnvValue("CompactionTotalSize", 0),
 		CompactionTotalSizeMultiplier: float64(debugEnvValue("CompactionTotalSizeMultiplier", 0)) / 10.0,
 		DisableBufferPool:             debugEnvValue("DisableBufferPool", 0) != 0,
@@ -70,15 +125,16 @@ func Open(location string) (*Lowlevel, error) {
 		NoSync:                        debugEnvValue("NoSync", 0) != 0,
 		NoWriteMerge:                  debugEnvValue("NoWriteMerge", 0) != 0,
 		OpenFilesCacheCapacity:        debugEnvValue("OpenFilesCacheCapacity", dbMaxOpenFiles),
-		WriteBuffer:                   debugEnvValue("WriteBuffer", dbWriteBuffer),
+		WriteBuffer:                   debugEnvValue("WriteBuffer", defaultWriteBuffer),
 		// The write slowdown and pause can be overridden, but even if they
 		// are not and the compaction trigger is overridden we need to
 		// adjust so that we don't pause writes for L0 compaction before we
 		// even *start* L0 compaction...
-		WriteL0SlowdownTrigger: debugEnvValue("WriteL0SlowdownTrigger", 2*debugEnvValue("CompactionL0Trigger", opt.DefaultCompactionL0Trigger)),
-		WriteL0PauseTrigger:    debugEnvValue("WriteL0SlowdownTrigger", 3*debugEnvValue("CompactionL0Trigger", opt.DefaultCompactionL0Trigger)),
+		WriteL0SlowdownTrigger: debugEnvValue("WriteL0SlowdownTrigger", 2*debugEnvValue("CompactionL0Trigger", defaultCompactionL0Trigger)),
+		WriteL0PauseTrigger:    debugEnvValue("WriteL0SlowdownTrigger", 3*debugEnvValue("CompactionL0Trigger", defaultCompactionL0Trigger)),
 	}
-	return open(location, opts)
+
+	return opts
 }
 
 // OpenRO attempts to open the database at the given location, read only.
@@ -114,6 +170,7 @@ func open(location string, opts *opt.Options) (*Lowlevel, error) {
 			l.Warnln("Compacting database:", err)
 		}
 	}
+
 	return NewLowlevel(db, location), nil
 }
 
@@ -207,6 +264,31 @@ func (db *Lowlevel) Close() {
 	db.DB.Close()
 }
 
+// dbIsLarge returns whether the estimated size of the database at location
+// is large enough to warrant optimization for large databases.
+func dbIsLarge(location string) bool {
+	dir, err := os.Open(location)
+	if err != nil {
+		return false
+	}
+
+	fis, err := dir.Readdir(-1)
+	if err != nil {
+		return false
+	}
+
+	var size int64
+	for _, fi := range fis {
+		if fi.Name() == "LOG" {
+			// don't count the size
+			continue
+		}
+		size += fi.Size()
+	}
+
+	return size > dbLargeThreshold
+}
+
 // NewLowlevel wraps the given *leveldb.DB into a *lowlevel
 func NewLowlevel(db *leveldb.DB, location string) *Lowlevel {
 	return &Lowlevel{

+ 1 - 1
lib/db/set_test.go

@@ -729,7 +729,7 @@ func BenchmarkUpdateOneFile(b *testing.B) {
 		protocol.FileInfo{Name: "zajksdhaskjdh/askjdhaskjdashkajshd/kasjdhaskjdhaskdjhaskdjash/dkjashdaksjdhaskdjahskdjh", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
 	}
 
-	ldb, err := db.Open("testdata/benchmarkupdate.db")
+	ldb, err := db.Open("testdata/benchmarkupdate.db", db.TuningAuto)
 	if err != nil {
 		b.Fatal(err)
 	}

+ 2 - 2
lib/syncthing/utils.go

@@ -122,6 +122,6 @@ func copyFile(src, dst string) error {
 	return nil
 }
 
-func OpenGoleveldb(path string) (*db.Lowlevel, error) {
-	return db.Open(path)
+func OpenGoleveldb(path string, tuning config.Tuning) (*db.Lowlevel, error) {
+	return db.Open(path, db.Tuning(tuning))
 }