[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Konsistentes, dateibasiertes MySQL-Backup mit ZFS in FreeBSD-Jails


Hallo zusammen,

ich habe nach einer moeglichst einifachen Moeglichkeit gesucht, um moeglichst 
automatisiert eine offline-Kopie einer MySQL-Datenbank zu erstellen. 

Mit FreeBSD-Jails und ZFS hat man generell alles zusammen, allerdings muss
man die Aufgabe hier ein wenig verteilen, damit der FreeBSD-Jailhost
selbst nichts ueber MySQL und dessen Strukturen wissen muss.

Grundannahmen: 

* Jedes Jail hat sein eigenes ZFS Volume, d.h. / im Jail zeigt auf ein Dataset.
* MySQL unter FreeBSD arbeitet standardmaessig unter /var/db/mysql/, alle
  *.pid und *.err Dateien liegen ebenfalls hier.
* Die Offline-Kopie soll unter /var/db/mysql.snap/ angelegt werden.

Das folgende Beispiel bezieht sich auf mysql.stwserv.de (Stadtwiki e.V.)

Step 1: Auf dem Jail-Server: Automatisches Erzeugen von ZFS-Snapshots 
beim Starten oder Stoppen eines Jails per jail(8) durch Konfiguration in 
jail.conf(5):

.--- /etc/jail.conf ------- 
| swg_mysql {
|         name            = "swg_mysql";
|         host.hostname   = "mysql.stwserv.de";
|         path            = "/jails/swg/mysql";
|         ip4.addr        = "164.177.171.48";
|         ip4.addr        += "em1|10.253.1.48";
|         ip6.addr        = "2a03:2500:1:6:30::";
|         mount.fstab     = "/jails/meta/swg/fstab.swg_mysql";
|         exec.consolelog = "/var/log/jail_swg_mysql_console.log";
|         exec.prestart   = "/root/bin/jail_zfs_snap.sh zroot/jails/swg/mysql prestart";
|         exec.poststop   = "/root/bin/jail_zfs_snap.sh zroot/jails/swg/mysql poststop";
| }
`----------

Relevant hier: exec.prestart und exec.poststop, der Rest sei hier nur
der Vollstaendigkeit halber aufgefuehrt.

Das Script erwartet genau 2 Parameter: Dataset und "tag": 

.--- /root/bin/jail_zfs_snap.sh ------- 
| #! /bin/sh
| #
| # zfs snapshot before jail start (precmd) / after jail stop (postcmd)
| #
| # Usage: /root/bin/jail_zfs_snap.sh DATASET TAG
| # 
| 
| test $# -eq 2 || 
|         { echo "ERROR: Usage: $0 DATASET TAG" >&2; exit 1; }
| 
| DATASET="${1}"
| TAG="${2:-"offline"}"
| 
| ZFS_MNT="$(/sbin/zfs get -H mountpoint "${DATASET}")"
| test $? -eq 0 ||
|         { echo "ERROR: Cannot get mountpoint of DATASET=${DATASET}" >&2 ; exit 1; }
| 
| 
| MOUNTPOINT="$( echo "${ZFS_MNT}" | tr '\t' '|' | cut -f 3 -d "|" )"
| test -d "${MOUNTPOINT}" ||
|         { echo "ERROR: MOUNTPOINT=${MOUNTPOINT} of DATASET=${DATASET} is not a directory" >&2 ; exit 1; }
| 
| 
| NOW="$(/bin/date +%Y%m%d%H%M%S)"
| SNAPSHOT="${DATASET}@${NOW}_${TAG}"
| SNAPSHOT_DIR="${MOUNTPOINT}/.zfs/snapshot/${NOW}_${TAG}"
| 
| /sbin/zfs snapshot "${SNAPSHOT}"
| test $? -eq 0 ||
|         { echo "ERROR: Cannot snapshot DATASET=${DATASET} to SNAPSHOT=${SNAPSHOT}" >&2; exit 1; }
| 
| echo "Snapshotted ${SNAPSHOT}, see ${SNAPSHOT_DIR}"
| 
`----------

Stoppe/Starte ich nun dieses Jail, dann passiert folgeneds:
.---------- 
| [root@stwserv2 ~]# jail -r swg_mysql 
| swg_mysql: removed
| Snapshotted zroot/jails/swg/mysql@20131221004705_poststop, see /jails/swg/mysql/.zfs/snapshot/20131221004705_poststop
| [root@stwserv2 ~]# jail -c swg_mysql 
| Snapshotted zroot/jails/swg/mysql@20131221004708_prestart, see /jails/swg/mysql/.zfs/snapshot/20131221004708_prestart
| swg_mysql: created
`----------


Das Ganze noch in /etc/crontab, damit es taeglich automatisch passiert:
.--- /etc/crontab ------- 
| 15      5       *       *       *       root    /usr/sbin/jail -rc swg_mysql
`----------

Soweit muss das also auf dem Jail-Server selbst aussehen.





Step 2: Im Jail suchen unter /.zfs/snapshot/* nach MySQL-Logdateien 
(/var/db/mysql/fqdn.err), die in der letzten Zeile die Nachricht enthalten, 
dass die MySQL-Datenbank (sauber) heruntergefahren wurde. Findet das Script 
ein solches Logfile, dann wird das Verzeichnis, in dem dieses Logfile 
gefunden wurde als Quelle fuer die (saubere) offline-Kopie verwendet. 

Metainformationen zum verwendeten Snapshot werden in /var/db/mysql.snap/SNAPSHOT festgehalten.

.--- /root/bin/mysql_get_snapshot.sh ------- 
| #! /bin/sh
| #
| # 1. find most recent zfs snapshot with clean mysql shutdown
| # 2. copy /var/db/mysql.snap/ from most current clean zfs snapshot
| 
| 
| MYSQL_HOSTNAME="mysql.stwserv.de"
| MYSQL_DATA_DIR="/var/db/mysql"
| MYSQL_SNAP_DIR="/var/db/mysql.snap"
| 
| # this message needs to be in the last line of a clean shutdown
| MYSQL_LOG_LINE="mysqld_safe mysqld from pid file ${MYSQL_DATA_DIR}/${MYSQL_HOSTNAME}.pid ended"
| 
| MYSQL_ZFS_SNAP_ERROR_FILE=""
| for FILE in $(find /.zfs/snapshot/*${MYSQL_DATA_DIR}/ -name ${MYSQL_HOSTNAME}.err | sort -r) ; do
|         if tail -n 1 "${FILE}" | grep --silent --fixed-strings "${MYSQL_LOG_LINE}" ; then
|                 MYSQL_ZFS_SNAP_ERROR_FILE="${FILE}"
|                 break
|         fi
| done
| 
| test -n "${MYSQL_ZFS_SNAP_ERROR_FILE}" ||
|         { echo "ERROR: No clean shutdown found in /.zfs/snapshot/*${MYSQL_DATA_DIR}/${MYSQL_HOSTNAME}.err" >&2; exit 1; }
| 
| test -f "${MYSQL_ZFS_SNAP_ERROR_FILE}" ||
|         { echo "ERROR: Found MYSQL_ZFS_SNAP_ERROR_FILE=${MYSQL_ZFS_SNAP_ERROR_FILE} is not a file" >&2; exit 1; }
| 
| MYSQL_ZFS_SNAP_DIR="$(dirname "${MYSQL_ZFS_SNAP_ERROR_FILE}")"
| 
| RSYNC_LOG="$(mktemp -t mysql_get_snapshot)"
| /usr/local/bin/rsync -avHWx --itemize-changes --stats --delete "${MYSQL_ZFS_SNAP_DIR}/" "${MYSQL_SNAP_DIR}/" >"${RSYNC_LOG}"  2>"${RSYNC_LOG}"
| 
| echo "NOW=$(date)"                       > "${MYSQL_SNAP_DIR}/SNAPSHOT"
| echo "SRC=${MYSQL_ZFS_SNAP_DIR}/"       >> "${MYSQL_SNAP_DIR}/SNAPSHOT"
| echo "DST=${MYSQL_SNAP_DIR}/"           >> "${MYSQL_SNAP_DIR}/SNAPSHOT"
| echo "RSYNC:"                           >> "${MYSQL_SNAP_DIR}/SNAPSHOT"
| cat "${RSYNC_LOG}"                      >> "${MYSQL_SNAP_DIR}/SNAPSHOT"
| rm "${RSYNC_LOG}"
`----------

Und damit das auch moeglichst automatisiert erfolgt, wird das script in
/etc/crontab eingetragen:

.--- /etc/crontab ------- 
| ...
| @reboot root    /root/bin/mysql_get_snapshot.sh
| ...
`----------

Zum Abschluss nochmal testen: Jail einmal neu starten:

.---------- 
| [root@stwserv2 ~]# date; jail -r swg_mysql ; jail -c swg_mysql ; date         
| Sat Dec 21 01:15:04 CET 2013
| swg_mysql: removed
| Snapshotted zroot/jails/swg/mysql@20131221011506_poststop, see /jails/swg/mysql/.zfs/snapshot/20131221011506_poststop
| Snapshotted zroot/jails/swg/mysql@20131221011506_prestart, see /jails/swg/mysql/.zfs/snapshot/20131221011506_prestart
| swg_mysql: created
| Sat Dec 21 01:15:11 CET 2013
`----------
... kurz abwarten und dann: 
.---------- 
| [root@stwserv2 ~]# head -n 3 /jails/swg/mysql/var/db/mysql.snap/SNAPSHOT
| NOW=Sat Dec 21 01:16:52 CET 2013
| SRC=/.zfs/snapshot/20131221011506_prestart/var/db/mysql/
| DST=/var/db/mysql.snap/
`----------


Was soll das alles?

* mit einer MySQL-Downtime von nur wenigen Sekunden (hier: 7 Sekunden)
  erhalten wir beim Neustart des MySQL-Jails 2 ZFS Snapshots (einer nach
  dem Beenden, einer vor dem Starten). 

* Die ZFS-Snapshots enthalten jeweils /var/db/mysql/ in einem
  offline-Zustand, also nach dem Herunterfahren von MySQL bzw. kurz
  davor. 

* MySQL kann auf einer solchen konsistenten Kopie sehr schnell ohne
  Crash Recovery starten. 

* Wir haben unter /var/db/mysql.snap/ die aktuellste Offline-Kopie und
  koennen innerhalb des Jails unmittelbar darauf zugreifen, etwa um hier
  ein (alternatives) MySQL zu starten, wenn /var/db/mysql/ defekt ist.

* Die Offline-Kopie von MySQL kann ueber ein sehr simples dateibasiertes 
  Backup gesichert werden, das Backup muss dabei nichts von MySQL wissen 
  oder auf dessen Belange Ruecksicht nehmen.

* Das Verfahren kann fuer alle Services in Jails angewendet werden, die
  viel In-Memory arbeiten und zB nur offline konsistente Daten im
  Dateisystem liegen haben (analog zu MySQL mit InnoDB).


Wenn obiges Konstrukt fehlschlaegt erkennt man das zB daran, dass
http://rhein-neckar-wiki.de/ ueber Datenbankprobleme klagt, das ist die
letzten Tage leider ein paar mal passiert ;-)

Kritik und Anregungen willkommen. 

Have fun!
Raphael
-- 
Raphael Eiselstein <rabe@xxxxxxxxx>               http://rabe.uugrn.org/
xmpp:freibyter@xxxxxx  | https://www.xing.com/profile/Raphael_Eiselstein   
PGP (alt):            E7B2 1D66 3AF2 EDC7 9828  6D7A 9CDA 3E7B 10CA 9F2D
PGP (neu):            4E63 5307 6F6A 036D 518D  3C4F 75EE EA14 F625 DB4E
.........|.........|.........|.........|.........|.........|.........|..




-- 
UUGRN e.V. http://www.uugrn.org/
http://mailman.uugrn.org/mailman/listinfo/uugrn
Wiki: https://wiki.uugrn.org/UUGRN:Mailingliste
Archiv: http://lists.uugrn.org/