The first time I needed a MariaDB restore for real — a dropped table on a 400 GB database, forty minutes before a customer-facing demo — I discovered two things in the same minute. The nightly job everyone trusted had been failing its prepare step for three weeks, and the backup binary on the box was Percona XtraBackup 8, which cannot read MariaDB's files at all. We survived on a replica promotion and luck. The next morning I replaced the whole pipeline with mariabackup and instituted the only backup rule that matters: a backup you have not restored is a rumor.
Why can't Percona XtraBackup back up modern MariaDB?
Because the physical formats parted ways and never rejoined. XtraBackup 8.x is built against MySQL 8.0's data dictionary, redo, and undo formats, and Percona's own support position is that XtraBackup is not supported on MariaDB 10.3 and later — full stop, not "your mileage may vary". Even the older 2.4 series only ever worked with plain MariaDB 10.1 and 10.2: no data-at-rest encryption, no InnoDB page compression, no non-default page sizes, and the undo-log incompatibility that MariaDB fixed in 10.2.2 already meant backups prepared by XtraBackup could silently lose transactions. MariaDB's answer was to fork XtraBackup 2.3.8 into mariabackup, shipping it with the server since 10.1.23 and 10.2.7, and to develop it in lockstep with their InnoDB fork. It understands the redo format MariaDB rewrote in 10.5, encrypted tablespaces, and punch-hole-compressed pages. The discipline is simple: run the mariabackup that ships with your server release, and treat anything from Percona as a different product for a different database.
What does the full backup workflow look like?
Three phases, always. The backup phase runs hot: mariabackup --backup --target-dir=/backups/full copies the InnoDB data files while a background thread tails the redo log, then takes a brief lock near the end to copy non-transactional tables and record the binlog position. On 10.5 and later that final lock uses MariaDB's BACKUP STAGE machinery rather than a long FLUSH TABLES WITH READ LOCK, which is noticeably gentler on a busy server. The prepare phase — mariabackup --prepare --target-dir=/backups/full — replays the captured redo against the copied files and rolls them forward to one consistent point. Until prepare succeeds, you do not have a backup; you have files. Restore is the reverse: stop the server, clear the datadir, mariabackup --copy-back, fix ownership, start.
-- the backup account needs exactly these grants, nothing more (10.5+):
GRANT RELOAD, PROCESS, LOCK TABLES, BINLOG MONITOR
ON *.* TO 'backup'@'localhost' IDENTIFIED BY 'use-a-vault';
-- right after a restore, before opening traffic:
SELECT COUNT(*) FROM mysql.global_priv;
CHECKSUM TABLE app_orders, app_customers;
SHOW BINARY LOGS;
Note the BINLOG MONITOR grant — SHOW BINARY LOGS requires it on 10.5 and later, and older backup scripts that scrape binlog coordinates fail quietly without it. Keep the credentials in a defaults file with 0600 permissions, never on the command line, where ps broadcasts them to every user on the host.
How do incremental backups work without corrupting the chain?
Increments are copy-level, keyed off log sequence numbers: mariabackup --backup --target-dir=/backups/inc1 --incremental-basedir=/backups/full copies only the pages changed since the LSN recorded in the basedir's metadata. Chaining daily increments onto a weekly full cuts both runtime and storage by an order of magnitude — on my 400 GB database the full ran about three hours and each increment about six minutes.
The prepare phase is where chains die. Each increment must be merged into the base in order: prepare the base with --prepare --target-dir=/backups/full, merge inc1 with --prepare --target-dir=/backups/full --incremental-dir=/backups/inc1, merge inc2 the same way. Note the option names, because old runbooks get both wrong: --incremental-basedir is for taking an incremental, --incremental-dir is for applying one, and --apply-log-only, which every intermediate merge used to demand, is neither needed nor supported by current mariabackup — whatever is still uncommitted at the end of the chain is rolled back by InnoDB when the restored server starts. Merge out of order and the result looks fine until InnoDB refuses to start on it — which you will discover at restore time, the worst possible moment. Script the chain; never run it from memory at 3am.
How do encryption, compression, and Galera SST fit in?
Data-at-rest encryption is the reason mariabackup exists, and it handles it well: encrypted tablespaces are copied and restored as-is, but the key management configuration — file_key_management or your KMS plugin — must be present at prepare time and at restore time, and losing the key file loses every backup made under it. Compression deserves honesty. There is a --compress option producing QuickLZ-compressed files, but I get better ratios and a simpler pipeline by streaming: mariabackup --backup --stream=mbstream piped through zstd. The stream doubles as your encryption vehicle, because unlike its XtraBackup ancestor, mariabackup has no native option to encrypt its own output — pipe the stream to age or gpg before it touches disk and you get encryption and compression in one step.
Galera ties the whole thing together: with wsrep_sst_method=mariabackup, the same tool is the State Snapshot Transfer engine, so a joining node receives exactly what a restore produces — a full physical copy, prepared on the fly by the donor. Two SST gotchas account for nearly every failure I have debugged recently: the donor needs working SST credentials in wsrep_sst_auth with the backup account's grants plus REPLICA MONITOR, which the SST path requires and the plain backup path does not, and the joiner needs enough free disk for a full copy plus working room. Check both before you need them.
What is the log-copy race, and how do you win it?
During the backup phase, the redo-copying thread must keep up with the server's redo generation for the entire duration of the file copy. If write volume pushes redo around the single ib_logfile0 faster than mariabackup reads it, the region it still needs gets overwritten and the backup aborts — the classic symptom is an error about the log sequence number being in the future, or the checkpoint having moved past what was captured. On a write-heavy server with a small log and slow backup storage, the race is lost before it starts.
Three levers win it. Grow the redo log: since MariaDB 10.9, innodb_log_file_size is dynamic, so you can double it for the backup window without a restart. Speed up the copy target — writing the backup to the same saturated disks the server is reading from is the most common self-inflicted wound, and a dedicated mount or a stream straight off the box removes it. And schedule against the write curve: put the full backup in the daily trough and let the cheap increments run at busier hours. Measure headroom instead of guessing:
-- redo generation rate: sample twice, sixty seconds apart
SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';
-- capacity and current position
SHOW GLOBAL VARIABLES LIKE 'innodb_log_file_size';
SHOW ENGINE INNODB STATUS\G
-- in the LOG section: "Log sequence number" minus "Last checkpoint at"
-- is your checkpoint age; it must stay well below the log capacity
How do you prove a backup actually restores?
You restore it, on a schedule, somewhere that is not production. My drill: every week, the latest full-plus-increment chain is prepared and copy-backed onto a scratch instance, the server starts, and a short script runs CHECKSUM TABLE on the ten largest tables plus a handful of application smoke queries with known answers. The whole thing is automated, takes about forty minutes, and has caught three silently broken chains in two years — each at least a week before we would have needed that backup. Record the wall-clock time of every drill, because that number is your real recovery time, and put it next to the recovery time the business thinks it bought. Keep the xtrabackup_binlog_info file — yes, mariabackup still calls it that — alongside every backup: the binlog file and position it records are the starting point for replaying logs forward to any moment after the backup, which is the difference between losing an afternoon and losing a day.
Where MonPG fits
The backup signals worth trending are boring and decisive: backup duration against its own history, redo generation rate during the window, and the timestamp of the last successful restore drill. Disclosure, as always in this series: I work on MonPG, which monitors PostgreSQL in production today and does not monitor MariaDB yet. MariaDB support is coming soon and in active development — the /mariadb-monitoring page tracks it — and backup health is exactly the class of signal it is being built to keep continuous: durations, redo headroom, and restore-drill freshness as first-class graphs rather than cron mail nobody reads. Until that ships, the queries and the drill above are the toolkit. If PostgreSQL is also in your fleet, that monitoring is live today — see the PostgreSQL overview and the blog.