MongoDB Notes

Condensed operational notes from a MySQL/MariaDB PoV.

Backup & Filesystem Snapshots

db.fsyncLock() -> zfs snapshot tank/mongodb@bkp_{{DATETIME}} -> db.fsyncUnlock()

Take a dump:

mongodump --db=local --collection=oplog.rs --query='{ "ts": { "$gte": Timestamp(UNIX_TIMESTAMP, 1), "$lt": Timestamp(UNIX_TIMESTAMP, 1) } }' --out=/tmp/oplog_dump

Then, on the failed instance:

systemctl stop mongod
zfs rollback tank/mongodb@bkp_{{DATETIME}}

or just nuke everything off of the directory manually and apply snapshot:

cd /tank
mkdir mongodb
zfs recv tank/mongodb < /tmp/mongo-backup.zfs
cd ~
zfs set mountpoint=legacy tank/mongodb
zfs set mountpoint=/var/lib/mongodb tank/mongodb
chown -R mongodb:mongodb /var/lib/mongodb
/sbin/restorecon -rv /var/lib/mongodb

Important: If the timeframe between snapshot and problem < oplog expiry, might not need the oplog and can wait for catch up, otherwise proceed

DISABLE REPLICATION. (start standalone)

mongod --port 27018 --dbpath /var/lib/mongodb

Replay the Oplog Using mongorestore Use the mongorestore utility with the –oplogReplay flag and rename the exported BSON file to oplog.bson so the utility recognizes it as a system replay log

Rename the dump file to the format expected by mongorestore:

mkdir /tmp/replay
mv /tmp/oplog_dump/local/oplog.rs.bson /tmp/replay/oplog.bson

replay the transactions into the restored standalone instance

mongorestore --port 27018 --oplogReplay /tmp/replay/

Once the restoration tool finished applying the operations, stop the standalone process and restart MongoDB normally with standard replica set configurations. (change in /etc)

Sharded Cluster

MongoDB Sharded Cluster is not as bad as MySQL NDB.

NDB table rows are spread across ALL data nodes, and for a consistent backup, all writes across all nodes must be frozen. Can not just snapshot one node.

For MongoDB, the shards are independent, can snapshot shards at different times, as long as balancer is stopped, and no chunks are moving, the operation is consistent.

Backup procedure (with ZFS):

#!/bin/bash
# mongodb-sharded-backup.sh

# Stop balancer (30 seconds)
mongosh --eval 'sh.stopBalancer()'
sleep 30

# Timestamp for snapshot
TIMESTAMP=$(date +%Y%m%d-%H%M%S)

# Snapshot all nodes in parallel
for host in shard1-sec shard2-sec shard3-sec config1 config2 config3; do
  ssh $host "zfs snapshot tank/mongodb@backup-$TIMESTAMP" &
done
wait

# Resume balancer
mongosh --eval 'sh.startBalancer()'

echo "Backup complete: backup-$TIMESTAMP"

Sharding Keys

They need good cardinality. If the cardinality is low, then the maximum number of shards can be limited by it. Good alignment with query patterns. If the query patterns do not align with the shard key, then queries will need to hit many shards to retrieve relatively little information.

Failover

In MariaDB, the database nodes themselves know very little about the overall topology, and they don’t natively keep track of who the master is without external help. So failover and routing depend on maxscale.

In MongoDB, the nodes themselves are smart. MongoDB uses a consensus algorithm built directly into the database binary. The nodes constantly ping each other via heartbeats every 2 seconds.

If the Primary goes down, the remaining Secondary nodes immediately hold an internal election and promote a new Primary within seconds, completely on their own, without needing an external coordinator.

Routing Traffic (No MaxScale Needed)

In MariaDB, the application connects to a maxscale listener, and maxscale figures out where to send reads and writes.

In MongoDB, the intelligence is moved to the Application Driver. When an app starts up, the connection string lists the entire cluster: mongodb://node1,node2,node3/?replicaSet=my-cluster.

The MongoDB driver connects to the cluster, discovers who the Primary is, and routes all writes there automatically. So this works a bit like the readwrite split in maxscale.

If a failover occurs, the driver automatically learns about the new Primary from the remaining nodes.

No need to manage or maintain a middleman proxy server like MaxScale.

In /etc/mongod.conf of all the mongodb servers, ensure they all share the exact same replication cluster name:

replication:
  replSetName: "my-cluster"

Then using mongosh in one of the servers:

rs.initiate({
  _id: "my-cluster",
  members: [
    { _id: 0, host: "10.0.0.1:27017" },
    { _id: 1, host: "10.0.0.2:27017" },
    { _id: 2, host: "10.0.0.3:27017" }
  ]
})

Basic CRUD Operations

MariaDB:

INSERT INTO users (name, email, age) VALUES ('Bob', 'bob@example.com', 30);

SELECT * FROM users WHERE age > 25;

UPDATE users SET age = 31 WHERE name = 'Bob';

DELETE FROM users WHERE name = 'Bob';

MongoDB:

db.users.insertOne({
  name: "Bob",
  email: "bob@example.com",
  age: 30
})

db.users.find({ age: { $gt: 25 } })

db.users.updateOne(
  { name: "Bob" },
  { $set: { age: 31 } }
)

db.users.deleteOne({ name: "Bob" })

Basic Index Management

MariaDB:

SHOW INDEX FROM users;

CREATE INDEX idx_email ON users(email);

CREATE INDEX idx_user_status ON users(status, created_at);

DROP INDEX idx_email ON users;

MongoDB:

db.users.getIndexes()

db.users.createIndex({ email: 1 })

db.users.createIndex({ status: 1, created_at: 1 })

db.users.dropIndex("email_1")  // or { email: 1 }

Works similarly:

Basic Monitoring & Troubleshooting

MariaDB:

-- Check current connections
SHOW PROCESSLIST;

-- Check long-running queries
SELECT * FROM information_schema.PROCESSLIST WHERE TIME > 30;

-- Kill problematic query
KILL QUERY 12345;

MongoDB equivalent:

// Check slow queries
db.system.profile.find({ millis: { $gt: 100 } }).sort({ ts: -1 }).limit(10)

// Check current operations
db.currentOp()

// Check long-running operations
db.currentOp({ "secs_running": { $gt: 30 } })

// Kill operation
db.killOp(12345)

Different commands, same workflow:

SETTINGS

RAM

By default, MongoDB will greedily swoop up available RAM, so set it manually if the default doesn’t make sense.

Open /etc/mongod.conf and configure the wiredTiger.engineConfig.cacheSizeGB setting:

storage:
  dbPath: /var/lib/mongodb
  engine: wiredTiger
  wiredTiger:
    engineConfig:
      cacheSizeGB: 16

oplog size

oplogSizeMB = (daily_write_GB * retention_days * 1024) + x% buffer

File Per Table

By default, WiredTiger engine automatically creates a separate file for every single collection and every single index (equivalent to innodb_file_per_table=1).

Tell MongoDB to separate the collection data files from the index files into different directories. This allows them to be mounted on separate ZFS datasets with different record sizes:

In /etc/mongod.conf, enable directory management:

storage:
  dbPath: /var/lib/mongodb
  wiredTiger:
    collectionConfig:
      blockCompressor: snappy
    engineConfig:
      directoryForIndexes: true  # <-- Splits data and indexes into /collection and /index subdirs

In ZFS, optimize block layers:

tank/mongo/collection: Set recordsize=16k (matches WiredTiger’s default internal page write size).
tank/mongo/index: Set recordsize=8k or 16k depending on index access behavior

Other useful ZFS settings:

zfs set atime=off tank/mongodb
zfs set sync=standard tank/mongodb
zfs set compression=lz4 tank/mongodb

But compression off for journal (adds little value)