Two-node MySQL storage with DRBD
DRBD mirrors a block device from one server to another over the network, like RAID 1 with a cable in the middle. Put a filesystem on top, put MySQL's data directory on that, and if the first server dies the second has an identical copy of the disk, up to the last committed write, ready to mount.
These notes are from a pair of Debian cloud servers running DRBD 8.3, each with a public interface and a private one. Replication runs over the private network. I'm writing them up because the one mistake in my first config is worth knowing about.
Install and check
On both nodes:
apt-get install drbd8-utils
modprobe drbd
cat /proc/drbd
version: 8.3.8 (api:88/proto:86-94)
If /proc/drbd exists, the kernel module is loaded. Note each node's private address with ip addr show and its hostname with uname -n. DRBD matches the on sections in the config against the hostname, so they have to be exactly the same.
A disk to replicate
DRBD wants a block device of its own, the same size on both nodes. On a cloud server that means attaching a separate volume, which shows up as something like /dev/xvdb. Partition it and leave the partition unformatted; DRBD puts the filesystem on later.
For a test with no spare volume, a file on a loop device will do:
dd if=/dev/zero of=/srv/drbd-r0.img bs=1M count=10240
losetup /dev/loop0 /srv/drbd-r0.img
That gives you 10 GB. Don't run production like this: DRBD on a loop device can deadlock under memory pressure, and the loop device has to be set up again before DRBD starts at every boot.
The resource file
/etc/drbd.d/r0.res, identical on both nodes:
resource r0 {
protocol C;
startup {
wfc-timeout 30;
degr-wfc-timeout 15;
}
disk {
on-io-error detach;
}
syncer {
rate 40M;
}
net {
after-sb-0pri discard-zero-changes;
after-sb-1pri discard-secondary;
after-sb-2pri disconnect;
}
on db-a {
device /dev/drbd0;
disk /dev/xvdb1;
address 10.10.10.1:7788;
meta-disk internal;
}
on db-b {
device /dev/drbd0;
disk /dev/xvdb1;
address 10.10.10.2:7788;
meta-disk internal;
}
}
protocol C is synchronous: a write isn't reported complete until both disks have it. It's the slowest of DRBD's three protocols and the only one I'd use for a database, because the whole point is that the second copy is never behind.
wfc-timeout and degr-wfc-timeout stop a node waiting forever at boot for its partner. Without them, a server whose twin is down sits at the DRBD prompt and never finishes booting.
on-io-error detach drops the local disk and carries on from the peer if the disk underneath fails. My first config said panic, which reboots the node instead; detach keeps the service up.
rate caps the background resync so a full sync doesn't starve normal traffic. Set it to around a third of the link's real throughput. I started at 100M, which is more than a shared cloud private network gives you.
meta-disk internal keeps DRBD's metadata at the end of the same device, which is simplest.
The mistake
My first version of this file also had these lines:
startup { become-primary-on both; }
net { allow-two-primaries; }
That's dual-primary mode: both nodes can write at once. It exists for cluster filesystems such as OCFS2 or GFS2, which coordinate locking between the nodes. ext4 doesn't, and nor does MySQL. Mount an ext4 DRBD device on both nodes and write to it, and you corrupt it. For a database, one node is primary and the other is a standby. Leave both lines out.
First sync
On both nodes, write the metadata and bring the resource up:
drbdadm create-md r0
drbdadm up r0
cat /proc/drbd
Both will show Secondary/Secondary and Inconsistent/Inconsistent, because neither has ever been declared the good copy. On the node that will start as primary, and only that one:
drbdadm -- --overwrite-data-of-peer primary r0
watch cat /proc/drbd
The status moves to SyncSource with a progress bar. You can carry on while it syncs. When it finishes, both sides say UpToDate/UpToDate.
Moving MySQL onto it
On the primary:
mkfs.ext4 /dev/drbd0
service mysql stop
mount /dev/drbd0 /mnt
rsync -a /var/lib/mysql/ /mnt/
umount /mnt
mount /dev/drbd0 /var/lib/mysql
service mysql start
Install the same MySQL version on the standby, but don't let it start at boot and don't mount anything there. Its own /var/lib/mysql is just a mount point.
One Debian catch: the system tables now live on the DRBD disk, including the debian-sys-maint user that Debian's init script uses. Copy /etc/mysql/debian.cnf from the primary to the standby, or the init script on the standby won't be able to shut MySQL down cleanly after a failover.
Failing over by hand
On the current primary:
service mysql stop
umount /var/lib/mysql
drbdadm secondary r0
On the other node:
drbdadm primary r0
mount /dev/drbd0 /var/lib/mysql
service mysql start
Point the application at the new node, or move a floating IP. Doing this automatically when a node dies is the job of a cluster manager (Heartbeat, later Pacemaker with Corosync); it's a separate subject, and DRBD is the easy half.
Split brain
If the link between the nodes drops and both become primary, each side has writes the other doesn't. That's split brain. The three after-sb policies decide what DRBD does when the nodes reconnect:
after-sb-0pri discard-zero-changes: neither is primary now; if only one side changed anything, take its data automatically.after-sb-1pri discard-secondary: one is primary; throw away the secondary's changes.after-sb-2pri disconnect: both are primary; don't guess, stay disconnected and wait for a person.
To recover by hand, pick the node whose data you're throwing away and run on it:
drbdadm secondary r0
drbdadm -- --discard-my-data connect r0
Then on the node you're keeping:
drbdadm connect r0
It resyncs from the survivor. Anything written to the other node during the split is gone, so look at it first.
Today
DRBD 9 moved the syncer options into the disk section (resync-rate) and uses drbdadm primary --force r0 for the first sync. For MySQL specifically I'd now reach for MySQL's own replication, Group Replication or Galera, or a managed database, before shared block storage.