Mediante HAST (Highly Available Storage) seremos capaces de montar un sistema de almacenamiento transparente entre dos equipos remotos conectados mediante una red TCP/IP, se podrÃa decir que HAST es un RAID1 (mirror) por red.
El artÃculo se compone de varias secciones:
Introducción:
Para mostar el funcionamiento de HAST dispondremos de dos servidores FreeBSD13.1 cada uno con su ip independiente y una VIP-CARP :
VIP: 192.168.69.40
PeanutBrain01: 192.168.69.41
PeanutBrain02: 192.168.69.42
Cuando trabajemos con HAST debemos tener en cuenta varios aspectos, HAST proporciona replicación sÃncrona a nivel de bloque, haciendo que sea transparente para los sistemas de ficheros y las aplicaciones. No existe diferencia entre utilizar dispositivos HAST o discos en crudo, particiones, etc, todos ellos no son mas que proveedores regulares GEOM . HAST trabaja en modo primary-secondary, solo unos de los nodos puede estar activo en un momento dado. El nodo primary atenderá las peticiones I/O, el nodo secundary es automáticamente sincronizado desde el primary. Las operaciones de escritura/borrado/flusheo son enviadas al primary y luego replicadas al secondary. Las operaciones de lectura son servidas desde el primary a no ser que exista algún error de I/O, en tal caso la lectura se envÃa al secondary.
HAST implementa varios modos de sincronización:
- memsync: Este modo reporta una operación de escritura como completada cuando el primary ha escrito los datos en disco y el secondary envÃa el ACK para confirmar el inicio de recepción de los datos, solo el inicio de recepción, los datos no han sido escritos en el secondary. Este modo reduce la latencia incluso proveyendo una fiabilidad razonable, es el modo por defecto.
- fullsync: Este modo reporta una operación de escritura como completada cuando ambos nodos han escrito los datos en disco, es el modo mas seguro pero también el mas lento.
- async: Este modo reporta una operación de escritura como completada cuando el primary ha escrito los datos en disco, es el modo mas rápido pero también el mas peligroso, solo debe ser utilizado cuando la latencia hasta el secondary es demasiado alta para utilizar alguno de los otros dos modos.
Si estamos utilizando un custom kernel tendremos que habilitar la siguiente opción en la compilación del mismo:
options GEOM_GATE
Configuración HAST:
En ambos servidores tenemos dos discos además del que utiliza el sistema de este modo podremos probar el funcionamiento con UFS/ZFS.
- UFS: /dev/ada1
- ZFS: /dev/ada2
root@PeanutBrain01:~ # camcontrol devlist
<VBOX HARDDISK 1.0> at scbus0 target 0 lun 0 (pass0,ada0)
<VBOX HARDDISK 1.0> at scbus0 target 1 lun 0 (pass1,ada1)
<VBOX HARDDISK 1.0> at scbus1 target 0 lun 0 (pass2,ada2)
root@PeanutBrain02:~ # camcontrol devlist
<VBOX HARDDISK 1.0> at scbus0 target 0 lun 0 (pass0,ada0)
<VBOX HARDDISK 1.0> at scbus0 target 1 lun 0 (pass1,ada1)
<VBOX HARDDISK 1.0> at scbus1 target 0 lun 0 (pass2,ada2)
Los discos deben estar impolutos:
It is not possible to use GEOM providers with an existing file system or to convert an existing storage to a HAST-managed pool
La configuración HAST serÃa la siguiente en ambos nodos:
resource MySQLData {
on PeanutBrain01 {
local /dev/ada1
remote 192.168.69.42
}
on PeanutBrain02 {
local /dev/ada1
remote 192.168.69.41
}
}
resource FilesData {
on PeanutBrain01 {
local /dev/ada2
remote 192.168.69.42
}
on PeanutBrain02 {
local /dev/ada2
remote 192.168.69.41
}
}
Creamos los pools HAST en ambos nodos:
hastctl create FilesData
Habilitamos y arrancamos el servicio:
service hastd start
Pasamos uno de los nodos a primary(PeanutBrain01):
hastctl role primary FilesData
El otro nodo a secondary(PeanutBrain02):
hastctl role secondary FilesData
Comprobamos el estado en ambos:
root@PeanutBrain01:~ # hastctl status MySQLData
Name Status Role Components
MySQLData complete primary /dev/ada1 192.168.69.42
root@PeanutBrain01:~ # hastctl status FilesData
Name Status Role Components
FilesData complete primary /dev/ada2 192.168.69.42
root@PeanutBrain02:~ # hastctl status MySQLData
Name Status Role Components
MySQLData complete secondary /dev/ada1 192.168.69.41
root@PeanutBrain02:~ # hastctl status FilesData
Name Status Role Components
FilesData complete secondary /dev/ada2 192.168.69.41
Podemos ver como el dispositivo HAST solo ha sido generado en el primary:
root@PeanutBrain01:~ # ls -la /dev/hast/
total 1
dr-xr-xr-x 2 root wheel 512 Oct 30 18:11 .
dr-xr-xr-x 10 root wheel 512 Oct 30 18:04 ..
crw-r----- 1 root operator 0x61 Oct 30 18:11 FilesData
crw-r----- 1 root operator 0x5f Oct 30 18:11 MySQLData
root@PeanutBrain02:~ # ls -la /dev/hast/
ls: /dev/hast/: No such file or directory
Ahora debemos decidir que sistema de ficheros queremos utilizar en el dispositivo HAST, en mi caso voy a utilizar tanto UFS como ZFS, los siguientes comandos SOLO los ejecutamos en el primary:
newfs -U /dev/hast/MySQLData
mkdir /var/db/mysql
mount /dev/hast/MySQLData /var/db/mysql
zpool create FilesData /dev/hast/FilesData
Comprobamos que esté montado en el nodo primary:
df -Th /var/db/mysql
Filesystem Type Size Used Avail Capacity Mounted on
/dev/hast/MySQLData ufs 15G 8.0K 14G 0% /var/db/mysql
zpool status FilesData
pool: FilesData
state: ONLINE
config:
NAME STATE READ WRITE CKSUM
FilesData ONLINE 0 0 0
hast/FilesData ONLINE 0 0 0
errors: No known data errors
df -Th /FilesData
Filesystem Type Size Used Avail Capacity Mounted on
FilesData zfs 15G 96K 15G 0% /FilesData
La idea es montar dos servicios MySQL y NFS, ambos serán ofrecidos por el nodo primary que será un nodo u otro dependiendo de donde esté configurada la VIP-CARP en cada momento.
Instalamos el paquete mysql-server en ambos nodos:
Habilitamos el servicio en ambos nodos:
Bindeamos el servicio a todas las ips del servidor:
[mysqld]
bind-address = 0.0.0.0
Arrancamos el servicio en ambos nodos:
Ahora habilitamos NFS para el directorio /FilesData en el primary:
Comrpobamos que esté compartido:
NAME PROPERTY VALUE SOURCE
FilesData sharenfs maproot=root:wheel local
Habilitamos los servicios NFS en ambos nodos de este modo cuando la VIP-CARP migre estará todo listo:
sysrc nfs_server_enable=YES
sysrc mountd_enable=YES
Arrancamos el servicio NFS en ambos nodos:
Creamos un fichero de prueba en el nodo primary:
Desde nuestro pc nos aseguramos de que podamos acceder al contenido por NFS:
Garrus # ~> showmount -e 192.168.69.40
Exports list on 192.168.69.40:
/FilesData Everyone
Garrus # ~> mount 192.168.69.40:/FilesData /mnt/nfs/
Garrus # ~> df -Th /mnt/nfs/
Filesystem Type Size Used Avail Capacity Mounted on
192.168.69.40:/FilesData nfs 15G 100K 15G 0% /mnt/nfs
Garrus # ~> cat /mnt/nfs/AA
kr0m
El cambio de role será controlado mediante eventos devd, cuando la VIP-CARP migre a un nodo este se configurará como primary, cuando se pierda la VIP-CARP se configurará como secondary.
Los parámetros necesarios para la configuración de las reglas devd son
los siguientes
:
System Subsystem Type Description
CARP Events related to the carp(4) protocol.
CARP vhid@inet The "subsystem" contains the actual
CARP vhid and the name of the network
interface on which the event took
place.
CARP vhid@inet MASTER Node become the master for a virtual
host.
CARP vhid@inet BACKUP Node become the backup for a virtual
host.
Añadimos la siguiente configuración a devd para que ejecute un script de reconfiguración de HAST cuando detecte una migración de la VIP-CARP:
notify 30 {
match "system" "CARP";
match "subsystem" "1@em0";
match "type" "MASTER";
action "/usr/local/sbin/carp-hast-switch primary";
};
notify 30 {
match "system" "CARP";
match "subsystem" "1@em0";
match "type" "BACKUP";
action "/usr/local/sbin/carp-hast-switch secondary";
};
Reiniciamos devd:
Según el cambio detectado el script de failover actuará de un modo determinado:
- Primary: Espera a que los procesos HAST-secondary mueran, cambia el role, monta/importa el sistema de ficheros y arranca los servicios.
- Secondary: Para los servicios, desmonta/exports el sistema de ficheros y cambia el role.
Todos los recursos HAST están vinculados a un directorio donde se montarán, el tipo de sistema de ficheros y a un servicio:
Recurso | Directorio | Sistema de ficheros | Servicio |
---|---|---|---|
MySQLData | /var/db/mysql | UFS | mysql |
FilesData | /FilesData | ZFS | nfs |
El script en cuestión serÃa el siguiente:
#!/bin/sh
# The names of the HAST resources, as listed in /etc/hast.conf
resources="MySQLData FilesData"
# Resource mountpoints
resource_mountpoints="/var/db/mysql /FilesData"
# Supported file system types: UFS, ZFS
resource_filesystems="UFS ZFS"
# Service types: mysql nfs
resource_services="mysql nfs"
# Delay in mounting HAST resource after becoming primary
delay=3
# logging
log="local0.debug"
name="carp-hast"
# end of user configurable stuff
case "$1" in
primary)
logger -p $log -t $name "Switching to primary provider for - ${resources} -."
sleep ${delay}
# -- SERVICE MANAGEMENT --
logger -p $log -t $name ">> Stopping services."
resource_counter=1
for resource in ${resources}; do
resource_service=`echo $resource_services | cut -d\ -f$resource_counter`
case "${resource_service}" in
mysql)
logger -p $log -t $name "Service MySQL detected for resource - ${resource} -."
logger -p $log -t $name "Stoping MySQL service for resource - ${resource} -."
service mysql-server stop
logger -p $log -t $name "Done for resource ${resource}"
;;
nfs)
logger -p $log -t $name "Service NFS detected for resource - ${resource} -."
logger -p $log -t $name "Disabling NFS-ZFS share for resource - ${resource} -."
zfs set sharenfs="off" FilesData
logger -p $log -t $name "Done for resource ${resource}"
logger -p $log -t $name "Stopping NFS service for resource - ${resource} -."
service nfsd stop
logger -p $log -t $name "Done for resource ${resource}"
;;
*)
logger -p local0.error -t $name "ERROR: Unknown service: ${resource_filesystem}, exiting."
exit 1
;;
esac
let resource_counter=$resource_counter+1
done
# -- HAST ROLE MANAGEMENT --
logger -p $log -t $name ">> Managing disks."
for resource in ${resources}; do
# When primary HAST node is inaccesible secondary node stops hastd secondary processes automatically
# Wait 30s for any "hastd secondary" processes to stop
num=0
logger -p $log -t $name "Waitting for secondary process of resource - ${resource} - to die."
while $( pgrep -lf "hastd: ${resource} \(secondary\)" > /dev/null 2>&1 ); do
let num=$num+1
sleep 1
if [ $num -gt 29 ]; then
logger -p $log -t $name "ERROR: Secondary process for resource - ${resource} - is still running after 30 seconds, exiting."
exit
fi
done
logger -p $log -t $name "Secondary process for resource - ${resource} - dead successfully."
# Switch role for resource
logger -p $log -t $name "Switching resource - ${resource} - to primary."
hastctl role primary ${resource}
if [ $? -ne 0 ]; then
logger -p $log -t $name "ERROR: Unable to change role to primary for resource - ${resource} -."
exit 1
fi
logger -p $log -t $name "Role for HAST resource - ${resource} - switched to primary."
done
# -- WAIT FOR HAST DEVICE CREATION --
logger -p $log -t $name ">> Waitting for hast devices."
for resource in ${resources}; do
num=0
logger -p $log -t $name "Waitting for hast device of resource - ${resource} -."
while [ ! -c "/dev/hast/${resource}" ]; do
let num=$num+1
sleep 1
if [ $num -gt 29 ]; then
logger -p $log -t $name "ERROR: GEOM provider /dev/hast/${resource} did not appear, exiting."
exit
fi
done
logger -p $log -t $name "Device /dev/hast/${resource} appeared for resource - ${resource} -."
done
# -- FILESYSTEM MANAGEMENT --
logger -p $log -t $name ">> Managing filesystems."
resource_counter=1
for resource in ${resources}; do
resource_mountpoint=`echo $resource_mountpoints | cut -d\ -f$resource_counter`
resource_filesystem=`echo $resource_filesystems | cut -d\ -f$resource_counter`
case "${resource_filesystem}" in
UFS)
logger -p $log -t $name "UFS filesystem detected in resource - ${resource} -."
mkdir -p ${resource_mountpoint} 2>/dev/null
logger -p $log -t $name "Checking /dev/hast/${resource} of resource - ${resource} -."
fsck -p -y -t ufs /dev/hast/${resource}
logger -p $log -t $name "Mounting /dev/hast/${resource} in ${resource_mountpoint}."
out=`mount /dev/hast/${resource} ${resource_mountpoint} 2>&1`
if [ $? -ne 0 ]; then
logger -p local0.error -t hast "ERROR: UFS mount - ${resource} - failed: ${out}."
exit 1
fi
logger -p local0.debug -t $name "UFS mount - ${resource} - mounted successfully."
;;
ZFS)
logger -p $log -t $name "ZFS filesystem detected in resource - ${resource} -."
logger -p $log -t $name "Importing ZFS pool of resource - ${resource} -."
out=`zpool import -f "${resource}" 2>&1`
if [ $? -ne 0 ]; then
logger -p local0.error -t $name "ERROR: ZFS pool import for resource - ${resource} - failed: ${out}."
exit 1
fi
logger -p local0.debug -t $name "ZFS pool for resource - ${resource} - imported successfully."
;;
*)
logger -p local0.error -t $name "ERROR: Unknown filesystem: ${resource_filesystem}, exiting."
exit 1
;;
esac
let resource_counter=$resource_counter+1
done
# -- SERVICE MANAGEMENT --
logger -p $log -t $name ">> Starting services."
resource_counter=1
for resource in ${resources}; do
logger -p $log -t $name "Starting service for resource - ${resource} -."
resource_service=`echo $resource_services | cut -d\ -f$resource_counter`
case "${resource_service}" in
mysql)
logger -p $log -t $name "Service MySQL detected for resource - ${resource} -."
logger -p $log -t $name "Starting MySQL service for resource - ${resource} -."
service mysql-server start
logger -p $log -t $name "Done for resource . ${resource} -."
;;
nfs)
logger -p $log -t $name "Service NFS detected for resource - ${resource} -."
logger -p $log -t $name "Starting NFS service for resource - ${resource} -."
service nfsd start
logger -p $log -t $name "Done for resource - ${resource} -."
logger -p $log -t $name "Enabling NFS-ZFS share for resource - ${resource} -."
zfs set sharenfs="maproot=root:wheel" FilesData
logger -p $log -t $name "Done for resource - ${resource} -."
;;
*)
logger -p local0.error -t $name "ERROR: Unknown service: ${resource_filesystem}, exiting."
exit 1
;;
esac
let resource_counter=$resource_counter+1
done
;;
secondary)
logger -p $log -t $name "Switching to secondary provider for - ${resources} -."
# -- SERVICE MANAGEMENT --
logger -p $log -t $name ">> Stopping services."
resource_counter=1
for resource in ${resources}; do
resource_service=`echo $resource_services | cut -d\ -f$resource_counter`
logger -p $log -t $name "Stopping services for resource - ${resource} -."
case "${resource_service}" in
mysql)
logger -p $log -t $name "Service MySQL detected for resource - ${resource} -."
logger -p $log -t $name "Stopping MySQL service for resource - ${resource} -."
service mysql-server stop
logger -p $log -t $name "Done for resource - ${resource} -."
;;
nfs)
logger -p $log -t $name "Service NFS detected for resource - ${resource} -."
logger -p $log -t $name "Disabling NFS-ZFS share for resource - ${resource} -."
zfs set sharenfs="off" FilesData
logger -p $log -t $name "Done for resource - ${resource} -."
logger -p $log -t $name "Restarting NFS service for resource - ${resource} -."
service nfsd restart
logger -p $log -t $name "Done for resource - ${resource} -."
;;
*)
logger -p local0.error -t $name "ERROR: Unknown service: ${resource_service}, exiting."
exit 1
;;
esac
let resource_counter=$resource_counter+1
done
# -- FILESYSTEM MANAGEMENT --
logger -p $log -t $name ">> Managing filesystems."
resource_counter=1
for resource in ${resources}; do
resource_mountpoint=`echo $resource_mountpoints | cut -d\ -f$resource_counter`
resource_filesystem=`echo $resource_filesystems | cut -d\ -f$resource_counter`
case "${resource_filesystem}" in
UFS)
logger -p $log -t $name "UFS filesystem detected in resource - ${resource} -."
if ! mount | grep -q "^/dev/hast/${resource} on "
then
else
logger -p $log -t $name "Umounting - ${resource} -."
umount -f ${resource_mountpoint}
logger -p $log -t $name "Done."
fi
sleep $delay
;;
ZFS)
logger -p $log -t $name "ZFS filesystem detected in resources - ${resource} -."
if ! mount | grep -q "^${resource} on ${resource_mountpoint}"
then
else
logger -p $log -t $name "Umounting - ${resource} -."
zfs umount ${resource}
logger -p $log -t $name "Done."
logger -p $log -t $name "Exporting ZFS pool of resources - ${resource} -."
out=`zpool export -f "${resource}" 2>&1`
if [ $? -ne 0 ]; then
logger -p local0.error -t $name "ERROR: ZFS pool export for resource - ${resource} - failed: ${out}."
exit 1
fi
logger -p local0.error -t $name "ZFS pool for resource - ${resource} - exported successfully."
fi
sleep $delay
;;
*)
logger -p local0.error -t $name "ERROR: Unknown filesystem: ${resource_filesystem}, exiting."
exit 1
;;
esac
let resource_counter=$resource_counter+1
done
# -- HAST ROLE MANAGEMENT --
logger -p $log -t $name ">> Managing resources."
resource_counter=1
for resource in ${resources}; do
logger -p $log -t $name "Switching resource - ${resource} - to secondary."
hastctl role secondary ${resource} 2>&1
if [ $? -ne 0 ]; then
logger -p $log -t $name "ERROR: Unable to switch resource - ${resource} - to secondary role."
exit 1
fi
logger -p $log -t $name "Role for resource - ${resource} - switched to secondary successfully."
let resource_counter=$resource_counter+1
done
;;
esac
Asignamos los permisos necesarios:
Pruebas:
MySQL:
Para generar tráfico MySQL vamos a utilizar
sysbench
para ello creamos en el primary la base de datos sobre la que realizar el test:
root@localhost [(none)]> create database kr0m;
Query OK, 1 row affected (0.00 sec)
root@localhost [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| kr0m |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
Creamos el usuario de acceso con los grants para la base de datos:
root@localhost [(none)]> create user sbtest_user identified by 'password';
Query OK, 0 rows affected (0.01 sec)
root@localhost [(none)]> grant all on kr0m.* to `sbtest_user`@`%`;
Query OK, 0 rows affected (0.01 sec)
root@localhost [(none)]> show grants for sbtest_user;
+-------------------------------------------------------+
| Grants for sbtest_user@% |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO `sbtest_user`@`%` |
| GRANT ALL PRIVILEGES ON `kr0m`.* TO `sbtest_user`@`%` |
+-------------------------------------------------------+
2 rows in set (0.01 sec)
En nuestro pc instalamos sysbench:
Creamos algunas tablas e insertamos datos en estas:
Ahora que ya tenemos datos en la base de datos, dejemos sysbench realizando un test de lectura_escritura:
NFS:
Para probar el acceso NFS utilizaremos
bonnie++
, lo instalamos en nuestro pc donde tenemos el NFS montado:
Dejamos bonnie trabajando mientras realizamos las pruebas de failover.
Failover:
Para migrar el servicio entre nodos disponemos de tres posibilidades:
-
Migrar la VIP, ejecutando en el nodo primary:
ifconfig em0 vhid 1 state backup -
Apagar ordenadamente el nodo primary:
service mysql-server stop
umount -f /var/db/mysqlservice nfsd stop
zpool export -f FilesDataservice hastd stop
shutdown -p now
-
Apagar abruptamente el nodo primary:
Tirar del cable de alimentación eléctrica.
En caso de migrar la VIP el nodo que ha perdido la ip flotante se reconfigurará automáticamente como secondary en los otros dos casos cuando el nodo vuelva a la vida los recursos HAST estarán en estado init:
Name Status Role Components
MySQLData - init /dev/ada1 192.168.69.42
Name Status Role Components
FilesData - init /dev/ada2 192.168.69.42
Debemos pasarlo a secondary:
hastctl role secondary FilesData
service mysql-server stop
service nfsd stop
Quedando del siguiente modo:
Name Status Role Components
MySQLData complete secondary /dev/ada1 192.168.69.42
Name Status Role Components
FilesData complete secondary /dev/ada2 192.168.69.42
Troubleshooting:
Un punto muy importante es que los nodos tengan la misma hora, utilizar NTP es una buena idea.
En caso de Split-brain si se han escrito datos en los dos nodos el administrador debe decidir que datos son mas importantes y configurar el otro nodo como secondary descartando los datos:
hastctl create test
hastctl role secondary test
Hay ocasiones en las que el script de migración falla, siempre podemos ejecutarlo a mano para comprobar que el script no tenga ningún problema:
Podemos ver los pasos realizados por el script de failover en los logs:
También podemos comprobar el estado de HAST:
hastctl status FilesData
Comprobar si el pool ZFS está importado:
zfs get sharenfs FilesData
Comprobar que la base de datos de prueba exista:
Comprobar que se tenga acceso NFS desde el cliente:
Debemos tener muy claro que HAST no es un backup, si se borran datos, se replica por red en el secondary perdiéndolos.
Además en ciertos escenarios como en su uso como backend para bases de datos puede ocasionar problemas, un apagado incorrecto de MySQL podrÃa dejar los datos corrompidos, se replicarÃan a nivel de bloque en el secondary dejándonos con una base de datos corrupta en tal caso solo podemos restarurar un backup en el primary actual.