Optimize Database, improve SNO API performance

The bandwidth.db file contains two tables of information. One is the bandwidth_usage_rollups table that stores historical rollups of bandwidth usage with a granularity of one hour (per satellite and per data transfer type, so you should expect around 20-30 rows per hour of node’s operation, not much). The other one is bandwidth_usage , which stores bandwidth usage for each single data transfer (potentially thousands per hour). You can look at this file using any SQLite client (though, preferably do this on a copy of the file made while the node was turned off, just to be sure; this paste is reformatted and trimmed for clarity):

% sqlite3 bandwidth.db
sqlite> .schema
CREATE TABLE bandwidth_usage (
	satellite_id  BLOB    NOT NULL,
	action        INTEGER NOT NULL,
	amount        BIGINT  NOT NULL,
	created_at    TIMESTAMP NOT NULL
);
CREATE TABLE bandwidth_usage_rollups (
	interval_start	TIMESTAMP NOT NULL,
	satellite_id  	BLOB    NOT NULL,
	action        	INTEGER NOT NULL,
	amount        	BIGINT  NOT NULL,
	PRIMARY KEY ( interval_start, satellite_id, action )
);

The latter is only supposed to be stored for a brief period of time until a chore will aggregate per-transfer data. This chore is run once an hour, so technically you should not be seeing rows older than ~2 hours in the bandwidth_usage table. You can look at the number of rows in both tables (this is for a node operating for close to three years, full and hence and not accepting more uploads):

sqlite> SELECT COUNT(1) FROM bandwidth_usage;
19
sqlite> SELECT COUNT(1) FROM bandwidth_usage_rollups;
280927

The bandwidth.db on this node is currently 36MB.

I see at least two reasons why the bandwidth.db file might grow to gigabytes.

Sometimes the rollup procedure is not performed correctly. SQLite is a transactional database. Transactions can fail if they are not done quickly enough. If a database is stored on a slow storage (e.g. a HDD with parity-based block storage with a slow copy-on-write fragmented file system on a memory-starved setup), rollups may start to fail, accumulating more and more data in the bandwidth_usage table, making future rollups even bigger. You can verify this hypothesis by looking at the row count of the bandwidth_usage table.

A possible one-time workaround is to reduce the I/O on the storage the database is placed for about two hours and make sure a rollup is performed correctly. If the node itself is the source of most I/O on that storage, you can do that e.g. by restarting the node with a disk allocation below the current stored amount (the node will stop accepting uploads then), or disabling network access (the node will become offline, so no I/O). You can check whether the rollup was performed by looking at the number of rows in the database, or at storage node logs (a Performing bandwidth usage rollups entry at the INFO level signifies the start of a rollup procedure—it would be nice to have the end logged as well…).

Another possible reason comes from the fact that SQLite does not free up disk space after deleting rows to optimize for the case where that disk space would be necessary for new rows. This is usually the case for storage nodes, but after big peaks of traffic (or after a large consecutive number of missing rollup executions!) you may actually end up with large amount of disk space allocated, but not used by SQLite. You can verify this hypothesis by looking at database statistics:

sqlite> SELECT SUM(pgsize) FROM dbstat;
36712448

(which matches the file size of 36MB here, so no problem).

To fix this problem, the best way is to vacuum the database file.

5 Likes