Skip to content

Conversation

@samliok
Copy link
Contributor

@samliok samliok commented Nov 7, 2025

Why this should be merged

We have noticed synchronous spikes in LevelDB compactions across our nodes. This potentially causes network blips if Disk IO or CPU resources get overconsumed by many nodes in the network at the same time.

Screenshot 2025-11-07 at 11 21 19 AM

How this works

Our nodes are configured by using the default levelDB configuration. This may be the reason for why we are seeing synchrony in compactions. We update this by deterministically randomizing two levelDB config parameters when levelDB is set up. These parameters should provide enough variance to offset the compaction synchrony.

CompactionL0Trigger -> The default for this is 4. This means compaction for level-0 tables will start when 4 L0 files are created. We set this value to be a random value(seeded by nodeID) from 3-7. By changing this value we get variance in when L0 compactions are triggered.

CompactionTableSize -> The default value for this is 2MiB. It defines the target size for files produced by compaction. By increasing this, compaction runs less often but consumes more cpu & I/O. We now randomize this value from 2MiB - 4MiB

How this was tested

I spun up a mainnet node on an EC2 instance. Initially it was created with the default compaction config for levelDB, but then I restarted the node with two different sets of configs. One with CompactionTrigger to 3 and Size to 4MiB and then another restart with a CompationL0Trigger of 7 and Size of 3MiB. The node appeared healthy after the restart.

Need to be documented in RELEASES.md?

Copilot AI review requested due to automatic review settings November 7, 2025 16:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds node-specific randomization of LevelDB compaction parameters to spread out compaction behavior across different nodes in the network. The implementation uses the node ID as a seed to generate deterministic but varied compaction settings.

  • Adds a nodeID parameter to the leveldb.New function signature
  • Introduces a randomizeCompaction function that derives compaction settings from the node ID
  • Updates all call sites to provide a node ID (using ids.EmptyNodeID for tests)

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
database/leveldb/db.go Adds randomizeCompaction function and updates New to accept nodeID parameter; updates documentation comments
database/factory/factory.go Updates New function signature to accept and pass through nodeID parameter
database/leveldb/db_test.go Updates test calls to New to pass ids.EmptyNodeID
node/node.go Updates database initialization to pass node ID
tests/reexecute/c/vm_reexecute_test.go Updates test calls to New to pass ids.EmptyNodeID
vms/platformvm/validators/manager_benchmark_test.go Updates benchmark call to New to pass ids.EmptyNodeID
Comments suppressed due to low confidence (1)

database/leveldb/db.go:233

  • The randomized compaction parameters will be overwritten by any user-provided configuration. This may result in unexpected behavior if users are unaware that their config will override the randomization. Consider either: (1) only applying randomization when configBytes is empty, or (2) documenting this behavior clearly, or (3) only randomizing unset fields after unmarshaling.
	randomizeCompaction(nodeID, &parsedConfig)

	if len(configBytes) > 0 {
		if err := json.Unmarshal(configBytes, &parsedConfig); err != nil {
			return nil, fmt.Errorf("%w: %w", ErrInvalidConfig, err)
		}
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Member

@rkuris rkuris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with suggestions / nits

MetricUpdateFrequency: DefaultMetricUpdateFrequency,
}

randomizeCompaction(nodeID, &parsedConfig)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may want to mention here that configuration will override the returns from here

bytes = paddedBytes
}
seed := binary.BigEndian.Uint64(bytes)
r := rand.New(rand.NewSource(int64(seed)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would have pulled out the creation of a random source from a NodeId into a separate, testable function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tests for this function + renamed

require.Equal(t, cfg1.CompactionL0Trigger, cfg2.CompactionL0Trigger)
require.Equal(t, cfg1.CompactionTableSize, cfg2.CompactionTableSize)
})
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe a test that 100 of them don't all generate exactly the same configuration?

// should be added to a batch size per operation.
levelDBByteOverhead = 8

// minCompactionL0Trigger is the minimum value for CompactionL0Trigger.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we observed that two of these parameters are needed to be changed at the same time?

Have we seen what happens when only one of them is changed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just get less variance if you do this, but I think we're shooting for the maximum possible variance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that, but if (for example) variable A is responsible for 99% of the variance and variable B is responsible for 1% of it then it makes sense to only tune A.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the algorithm, it shouldn't be that skewed, unless something is wrong with the implementation. I'm not sure this is worth testing to verify though, since even if it is as bad as 99/1 there's not any harm in changing both.

@joshua-kim joshua-kim moved this to In Progress 🏗️ in avalanchego Nov 7, 2025
@samliok samliok self-assigned this Nov 7, 2025

// New returns a wrapped LevelDB object.
func New(file string, configBytes []byte, log logging.Logger, reg prometheus.Registerer) (database.Database, error) {
func New(nodeID ids.NodeID, file string, configBytes []byte, log logging.Logger, reg prometheus.Registerer) (database.Database, error) {
Copy link
Contributor

@DracoLi DracoLi Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does making this take a NodeID make sense?
db.leveldb is a general kv db but this makes db initialization dependent on having a NodeID when this package is used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't have a NodeID then we don't care about being out of sync. Or at least we don't know what we're being out of sync with.

This lets us maintain the exact same configuration on a restart with the same NodeID so IMO this is the right approach.

Copy link
Contributor

@DracoLi DracoLi Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this just take something like a seed int? factory can convert the NodeID to a seed when creating a leveldb. I think its kind of weird needing to pass ids.EmptyNodeID when creating a leveldb.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ended up removing the NodeID because I did find it weird requiring that to be a part of the constructor. Plus I don't think we really care if the levelDB config is randomized on restart(as long as it different changes don't break the node, which seems like they don't).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which seems like they don't

This is a requirement for merging anyways, as we are going to be deploying this on nodes that are already running a different config.

// minCompactionL0Trigger is the minimum value for CompactionL0Trigger.
minCompactionL0Trigger = 3

// maxCompactionL0Trigger is the maximum value for CompactionL0Trigger.
Copy link
Contributor

@DracoLi DracoLi Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we tested how much compactions will be spread out if we are randomizing these two values? Its not clear to me if it will spread out enough to make a big impact.
If the total size per level is still the same, won't nodes hit a level 4 or 5 compaction at around the same time when previous level is full due to similar db size? It should not be the exact same time due to L0 trigger config but since L0 size is small, it might still be very close together.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd have to see it in production to know for sure, but this is the best we can do so it's worth a shot.

@rkuris
Copy link
Member

rkuris commented Nov 7, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress 🏗️

Development

Successfully merging this pull request may close these issues.

6 participants