FreeBSD. HAST + UCARP = файловый кластер. Или нет?

Идея настроить репликацию файлов между серверами давно закралась мне в голову. Хочется такой себе файловый кластер на FreeBSD. Очень уж хотелось засунуть туда один web-ресурс. С базой данных все понятно, сам mysql прекрасно справляется с репликацией своих баз (я писал уже о репликации в mysql в одной из предыдущих заметок). Но что делать с файлами? Rsync и прочее не нравится, точнее нравится и даже трудится в данный момент, но есть одна проблема — решение содержит постоянную временную задержку синхронизации, а значит данные на slave-нодах всегда будут отставать от master-а. И вот както я нашел HAST, он лишен проблемы с задержкой синхронизации, так как синхронизирует данные в реальном времени. Жаль только что в данный момент HAST ограничен работой с двумя хостами в режиме master-slave или в терминологии самого HAST «Primary — Secondary». По сути можно построить клестер «с холодным резервом». И еще одна ложка дегтя — больше двух хостов в связку добавить нельзя, один мастер, один слейв …. эх 🙁 Также есть ограничения на общий ресурс — это может быть либо отдельный диск либо раздел, директорию или файл сделать общим ресурсом нельзя. Ну все равно интересно — пробуем!
Вводные данные:
FQDN server1 — node1.loc, IP — 10.10.10.10 — условный master
FQDN server2 — node2.loc, IP — 10.10.10.20 — условный slave
«Общий» для обоих нод IP, по сути, адрес кластера — 10.10.10.100
«Общий» ресурс — www
Диск который будем делать общим — /dev/da0
Точка монтирования «общего» ресурса — /usr/local/www/hast
Поехали!

Server1:
В /etc/hosts добавим запись:

10.10.10.20 node2.loc

Создаем конфиг для HAST /etc/hast.conf, он довольно простой:

resource www {
        on node1.loc {
                local /dev/da0
                remote 10.10.10.20
        }
        on node2.loc {
                local /dev/da0
                remote 10.10.10.10
        }
}

Имя общего ресурса в HAST — www, создадим его командой:

[root@node1 ~]# hastctl create www

Добавим hast в автозагрузку, для этого нужно в /etc/rc.conf добавить строчку:

hastd_enable="YES"

И запустим сервис:

[root@node1 ~]# service hastd start

На втором сервере действия аналогичные
Server2:
В /etc/hosts добавим запись:

10.10.10.10 node1.loc

Создаем конфиг для HAST /etc/hast.conf, он ничем не отличается от того что на мастере:

resource www {
        on node1.loc {
                local /dev/da0
                remote 10.10.10.20
        }
        on node2.loc {
                local /dev/da0
                remote 10.10.10.10
        }
}

Создадим ресурс www

[root@node2 ~]# hastctl create www

Добавим hast в автозагрузку, для этого нужно в /etc/rc.conf добавить строчку:

hastd_enable="YES"

И запустим сервис:

[root@node2 ~]# service hastd start

Теперь назначим роли, как я говорил вначале, HAST умеет работать только в режиме Primary — Secondary, поэтому в процессе настройки обязательно нужно сказать кто есть кто.
Server1:
Думаю команда в комментариях не нуждается:

[root@node1 ~]# hastctl role primary www

Server2:
Команда та же, роль другая. no comment:

[root@node2 ~]# hastctl role secondary www

Проверка статуса ноды, если все нормально, status будет «complete»
Server1:

[root@node1 ~]# hastctl status www      
www:
  role: primary
  provname: www
  localpath: /dev/da0
  extentsize: 2097152 (2.0MB)
  keepdirty: 64
  remoteaddr: 10.10.10.20
  replication: fullsync
  status: complete
  dirty: 0 (0B)
  statistics:
    reads: 23
    writes: 0
    deletes: 0
    flushes: 0
    activemap updates: 0
[root@node1 ~]#

И на втором сервере:
Server2:

[root@node2 ~]# hastctl status www
www:
  role: secondary
  provname: www
  localpath: /dev/da0
  extentsize: 2097152 (2.0MB)
  keepdirty: 0
  remoteaddr: 10.10.10.10
  replication: fullsync
  status: complete
  dirty: 0 (0B)
  statistics:
    reads: 0
    writes: 0
    deletes: 0
    flushes: 0
    activemap updates: 0
[root@node2 ~]#

Как видите, мне всё удалось с первого раза — статус «complete», отлично! Идем дальше!

Теперь на мастер-ноде появилось новое устройство /dev/hast/www, на нем можно создать FS, а затем примонтировать как обычный раздел.
Server1:
Создаем директорию куда будем монтировать наш ресурс www:

[root@node1 ~]# mkdir /usr/local/www/hast

Создаем FS ZFS, можно и UFS, тут на любителя. Лично мне больше понравился ZFS потому что поддерживает дополнительные плюшки и не требует fsck при «миграции» на slave — ноду, а UFS — требует.

[root@node1 ~]# zpool create -m /usr/local/www/hast www /dev/hast/www

Смотрим что вышло:

[root@node1 ~]# zpool status
  pool: www
 state: ONLINE
  scan: none requested
config:

        NAME        STATE     READ WRITE CKSUM
        www         ONLINE       0     0     0
          hast/www  ONLINE       0     0     0

errors: No known data errors
[root@node1 ~]#

Добавим в /etc/rc.conf опцию для ZFS

zfs_enable="YES"

Еще раз смотрим статус на primary и secondary нодах, если у обоих «complete» — двигаемся дальше, если нет — значит hast у вас не заработал и нужно начинать сначала, вдумчиво проанализируйте предыдущие действия.
Итак, сам HAST уже полностью настроен и готов к бою, но к сожалению, он не умеет определять доступность Primary-ноды, ну и темболее автоматически переключать Secondary в Primary в случае падения Primary.
Мы сами должны обеспечить авто переключение Secondary-> Primary. В официальной доке предлагают сделать это с помощью ucarp. Выглядит довольно просто в настройке, вперед!
Сначала установим ucarp на обоих серверах, как всегда из портов:
Server1:

[root@node1 ~]# cd /usr/ports/net/ucarp && make install clean

Server2:

[root@node2 ~]# cd /usr/ports/net/ucarp && make install clean

Основная фича ucarp-а — выполнение определенных действий (в нашем случае скриптов), если заданный хост становится недоступным. Это то что нам нужно, если «primary» нода завалилась — «secondary» должен стать главным, для этого ему нужно сделать 2 вещи:
— поменять статус в hast на Primary
— присвоить себе ip адрес мастера — 10.10.10.100
Поскольку стандартные скрипты ucarp нужно модифицировать, рекомендую создать для них отдельную директорию и скопировать в нее для дальнейших правок. На обоих серверах действия одинаковые. Я сделал так:
Server1:

[root@node1 ~]# mkdir -p /root/scripts/hast
[root@node1 ~]# cp /usr/share/examples/hast/* /root/scripts/hast

Server2:

[root@node2 ~]# mkdir -p /root/scripts/hast
[root@node2 ~]# cp /usr/share/examples/hast/* /root/scripts/hast

Добавим в /etc/rc.conf автозапуск ucarp, а также пропишем некоторые опции для его работы:
Server1:

ucarp_enable="YES"
ucarp_addr="10.10.10.100"
ucarp_if="em0"
ucarp_src="10.10.10.10"
ucarp_vhid="3"
ucarp_pass="carp_pass"
ucarp_upscript="/root/scripts/hast/vip-up.sh"
ucarp_downscript="/root/scripts/hast/vip-down.sh"

Повторим процедуру на втором сервере:
Server2:
В /etc/rc.conf добавим:

ucarp_enable="YES"
ucarp_addr="10.10.10.100"
ucarp_if="em0"
ucarp_src="10.10.10.20"
ucarp_vhid="3"
ucarp_pass="carp_pass"
ucarp_upscript="/root/scripts/hast/vip-up.sh"
ucarp_downscript="/root/scripts/hast/vip-down.sh"

Теперь поправим скрипты под наши нужды, я приведу листинги, объяснять особо тут нечего, читайте сами. Обратите внимание что в скриптах ucarp_down.sh и ucarp_up.sh некоторые переменные нужно исправить согласно своей конфигурации:

resource="www"
fstype="ZFS"
pool="www"

Скрипты одинаковые для обоих хостов, я их расположил в /root/scripts/hast.
/root/scripts/hast/ucarp_down.sh

#!/bin/sh
#
# Copyright (c) 2010 The FreeBSD Foundation
# All rights reserved.
#
# This software was developed by Pawel Jakub Dawidek under sponsorship from
# the FreeBSD Foundation.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# $FreeBSD: src/share/examples/hast/ucarp_down.sh,v 1.1.4.1.4.2 2012/11/17 08:47:24 svnexp Exp $

# Resource name as defined in /etc/hast.conf.
resource="www"
# Supported file system types: UFS, ZFS
fstype="ZFS"
# ZFS pool name. Required only when fstype == ZFS.
pool="www"
# File system mount point. Required only when fstype == UFS.
mountpoint="/usr/local/www/hast"
# Name of HAST provider as defined in /etc/hast.conf.
# Required only when fstype == UFS.
device="/dev/hast/${resource}"

export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

# KIll UP script if it still runs in the background.
sig="TERM"
for i in `jot 30`; do
        pgid=`pgrep -f ucarp_up.sh | head -1`
        [ -n "${pgid}" ] || break
        kill -${sig} -- -${pgid}
        sig="KILL"
        sleep 1
done
if [ -n "${pgid}" ]; then
        logger -p local0.error -t hast "UCARP UP process for resource ${resource} is still running after 30 seconds."
        exit 1
fi
logger -p local0.debug -t hast "UCARP UP is not running."

case "${fstype}" in
UFS)
        mount | egrep -q "^${device} on "
        if [ $? -eq 0 ]; then
                # Forcibly unmount file system.
                out=`umount -f "${mountpoint}" 2>&1`
                if [ $? -ne 0 ]; then
                        logger -p local0.error -t hast "Unable to unmount file system for resource ${resource}: ${out}."
                        exit 1
                fi
                logger -p local0.debug -t hast "File system for resource ${resource} unmounted."
        fi
        ;;
ZFS)
        zpool list | egrep -q "^${pool} "
        if [ $? -eq 0 ]; then
                # Forcibly export file pool.
                out=`zpool export -f "${pool}" 2>&1`
                if [ $? -ne 0 ]; then
                        logger -p local0.error -t hast "Unable to export pool for resource ${resource}: ${out}."
                        exit 1
                fi
                logger -p local0.debug -t hast "ZFS pool for resource ${resource} exported."
        fi
        ;;
esac

# Change role to secondary for our resource.
out=`hastctl role secondary "${resource}" 2>&1`
if [ $? -ne 0 ]; then
        logger -p local0.error -t hast "Unable to change to role to secondary for resource ${resource}: ${out}."
        exit 1
fi
logger -p local0.debug -t hast "Role for resource ${resource} changed to secondary."

logger -p local0.info -t hast "Successfully switched to secondary for resource ${resource}."

exit 0

/root/scripts/hast/ucarp_up.sh

#!/bin/sh
#
# Copyright (c) 2010 The FreeBSD Foundation
# All rights reserved.
#
# This software was developed by Pawel Jakub Dawidek under sponsorship from
# the FreeBSD Foundation.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# $FreeBSD: src/share/examples/hast/ucarp_up.sh,v 1.1.4.1.4.2 2012/11/17 08:47:24 svnexp Exp $

# Resource name as defined in /etc/hast.conf.
resource="www"
# Supported file system types: UFS, ZFS
fstype="ZFS"
# ZFS pool name. Required only when fstype == ZFS.
pool="www"
# File system mount point. Required only when fstype == UFS.
mountpoint="/usr/local/www/hast"
# Name of HAST provider as defined in /etc/hast.conf.
device="/dev/hast/${resource}"

export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

# If there is secondary worker process, it means that remote primary process is
# still running. We have to wait for it to terminate.
for i in `jot 30`; do
        pgrep -f "hastd: ${resource} \(secondary\)" >/dev/null 2>&1 || break
        sleep 1
done
if pgrep -f "hastd: ${resource} \(secondary\)" >/dev/null 2>&1; then
        logger -p local0.error -t hast "Secondary process for resource ${resource} is still running after 30 seconds."
        exit 1
fi
logger -p local0.debug -t hast "Secondary process in not running."

# Change role to primary for our resource.
out=`hastctl role primary "${resource}" 2>&1`
if [ $? -ne 0 ]; then
        logger -p local0.error -t hast "Unable to change to role to primary for resource ${resource}: ${out}."
        exit 1
fi
# Wait few seconds for provider to appear.
for i in `jot 50`; do
        [ -c "${device}" ] && break
        sleep 0.1
done
if [ ! -c "${device}" ]; then
        logger -p local0.error -t hast "Device ${device} didn't appear."
        exit 1
fi
logger -p local0.debug -t hast "Role for resource ${resource} changed to primary."

case "${fstype}" in
UFS)
        # Check the file system.
        fsck -y -t ufs "${device}" >/dev/null 2>&1
        if [ $? -ne 0 ]; then
                logger -p local0.error -t hast "File system check for resource ${resource} failed."
                exit 1
        fi
        logger -p local0.debug -t hast "File system check for resource ${resource} finished."
        # Mount the file system.
        out=`mount -t ufs "${device}" "${mountpoint}" 2>&1`
        if [ $? -ne 0 ]; then
                logger -p local0.error -t hast "File system mount for resource ${resource} failed: ${out}."
                exit 1
        fi
        logger -p local0.debug -t hast "File system for resource ${resource} mounted."
        ;;
ZFS)
        # Import ZFS pool. Do it forcibly as it remembers hostid of
        # the other cluster node.
        out=`zpool import -f "${pool}" 2>&1`
        if [ $? -ne 0 ]; then
                logger -p local0.error -t hast "ZFS pool import for resource ${resource} failed: ${out}."
                exit 1
        fi
        logger -p local0.debug -t hast "ZFS pool for resource ${resource} imported."
        ;;
esac

logger -p local0.info -t hast "Successfully switched to primary for resource ${resource}."

exit 0

/root/scripts/hast/vip-down.sh

#!/bin/sh
# $FreeBSD: src/share/examples/hast/vip-down.sh,v 1.1.4.1.4.2 2012/11/17 08:47:24 svnexp Exp $

/sbin/ifconfig em0 -alias 10.10.10.100
/root/scripts/hast/ucarp_down.sh
exit 0

/root/scripts/hast/vip-up.sh

#!/bin/sh
# $FreeBSD: src/share/examples/hast/vip-up.sh,v 1.1.4.1.4.2 2012/11/17 08:47:24 svnexp Exp $

set -m
/sbin/ifconfig em0 alias 10.10.10.100/24
/root/scripts/hast/ucarp_up.sh &
set +m
exit 0

При создании/копировании скриптов не забудьте про права на выполнение!
После того как скрипты будут помещены в заданную директорию, можете попробовать запустить ucarp на обоих серверах.

service ucarp start

На Primary ноде у вас должен появиться alias ip адреса, который вы задали в скриптах vip-down.sh vip-up.sh
Теперь можно посмотреть что говорят логи ucarp (/var/log/messages), например на Primary-ноде у меня так:

Nov  4 11:47:40 node1 ucarp[15653]: [WARNING] Switching to state: MASTER
Nov  4 11:47:40 node1 ucarp[15653]: [WARNING] Spawning [/root/scripts/hast/vip-up.sh em0 10.10.10.100]

Рекомендую перегрузить Server2 — проверить логи, после перезагрузки она станет BACKUP. После этого перегрузить Server1 — Server2 станет MASTER в ucarp и Primary в HAST + примонтирует раздел /dev/hast/www в заданную директорию.
Я также проверил как работает apache2 и nginx с директорией hast — все отлично. Apache2 на Secondary, правда, ругается Warning-ом при запуске на то что директория /usr/local/www/hast не найдена. Но успегно отдает контент при смене роли на Primary, даже без перезапуска, Nginx вообще молодчага — отсутствие директории его ни разу не смущает, он также отлично отрабатывает и без передергивания отдает контент с hast-овой директории. Как и говорил ранее, mysql настроен на master-slave репликацию, с ним проблем нет.
Что еще могу сказать по этой теме. Ucarp, который предлагают в оф. доке мне лично не особо нравится. Хоть со своей задачей и справляется, оставляет впечатление какойто недоделанности. Здается что carp для этой связки больше подходит.
Также проблемой считается случай когда по какимто причинам случается сбой синхронизации и переназначение роли мастера «недосинхронизированному» слейву. Такое запроста может случиться когда например данные активно пишутся на мастер, не успевая синхронизировать их на слейв рубится свет. После включения тот что был Primary по какимто причинам загрузился вторым (fsck например) и Secondary стал Primary, а данные то более актуальные на том что тупонул и загрузился вторым — тоесть на слейве. Такие проблемы оф. дока оставляет на совесть администраторам.
P.S. К сожалению в реальных проектах данное решение применить не удалось, там где нужен кластер нужно было 2 слейва и один мастер — HAST тут не помощник. Попробовал, поигрался, потратил пару дней на доки и на этом все. В перспективе перевести сервис которому нужен кластер на Linux и там настроить, благо у пингвином больший выбор подобных решений.
Enjoy!

  1. Комментов пока нет

  1. Трэкбэков пока нет.

Why ask?