Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #126, ИЮНЬ 2009 г.

Пять звезд и отменный сервис. Продолжаем настройку сервиса по сдаче в аренду виртуальных FreeBSD серверов

Евгений Зобнин (j1m@synack.ru)




Рассмотрев создание сервиса в прошлом номере, мы остановились на вполне работоспособном каркасе. Он хорошо подходит для частного использования, но еще далек от продакшн, то есть от применения в среде производственной эксплуатации. Сегодня мы прикрутим к нашему творению ограничение ресурсов для каждого виртуального сервера и полноценный мониторинг хост-системы.

Работа напильником

Чтобы повысить эффективность сервиса, я решил несколько видоизменить его дизайн. Логика работы и формат конфигурационных файлов остались прежними, однако структура директорий претерпела изменения. Вместо каталогов для виртуальных серверов теперь используются виртуальные диски, подключаемые с помощью команд mdconfig и mount. Это дает большие преимущества:

  1. Виртуальный диск копируется заметно быстрее, так что на создание нового окружения уйдет не так много времени.
  2. Виртуальные диски позволят ограничивать дисковое пространство для каждого виртуального сервера.
  3. Упрощается миграция виртуальных серверов с одной машины на другую.

Чтобы преобразовать базовый каталог /usr/jailbase/FreeBSD-версия из прошлой статьи в базовый виртуальный диск размером в 2 Гб, выполни следующую последовательность команд:

# dd if=/dev/zero of=/usr/jailbase/FreeBSD-`uname -r`.2g.image bs=1m count=2k
# bsdlabel -w -f /usr/jailbase/FreeBSD-`uname -r`.2g.image auto
# mdconfig -a -t vnode -f /usr/jailbase/FreeBSD-`uname -r`.2g.image -u 0
# newfs md0c
# mount /dev/md0c /mnt
# cp -a /usr/jailbase/FreeBSD-`uname -r` /mnt
# umount /mnt
# mdconfig -d -u 0
# rm -Rf /usr/jailbase/FreeBSD-`uname -r`

Ограничения

В прошлой статье я упомянул о каталоге /usr/jailbase/conf, который хранит конфигурационные файлы для разных типов аккаунтов. Каждый такой файл содержит настройки ограничений, которые будут наложены командами addvserver и startvserver на виртуальный сервер во время его создания или запуска. Вот примерное содержимое одного из них:

# vi /usr/jailbase/conf/base

# Размер образа (ограничение дискового пространства)
SIZE=2g
# Ограничение пропускной способности
BANDWITH=1Mbit/s

Создай и заполни конфигурационные файлы для всех типов аккаунтов, которые будет поддерживать сервис:

# mkdir /usr/jailbase/conf
# touch /usr/jailbase/conf/{trial,base,extra,vip}

Каких-то особенных рекомендаций относительно значений опций дать не могу. Все зависит от ресурсов сервера, ширины канала и целевой аудитории сервиса.
Чтобы реализовать возможность накладывания ограничений, я модифицировал несколько скриптов. Первый из них - addvserver, который теперь копирует виртуальный диск вместо базового каталога:

# vi /usr/local/bin/addvserver

# Копируем и подключаем образ диска
mkdir $JAILDIR/$IP
cp $JAILBASE/FreeBSD-${OSVER}.${SIZE}.image $JAILDIR/${IP}.image
mdconfig -a -t vnode -f $JAILDIR/${IP}.image -u 99
mount /dev/md99c $JAILDIR/$IP
# Создаем файл инициализации для нового сервера
…
# Копируем публичный ключ в каталог /root/.ssh
…
# Отключаем образ
umount $JAILDIR/$IP
mdconfig -d -u 99

Тебе придется подготовить целый набор базовых виртуальных дисков, по одному на каждый тип аккаунта. Скрипт startvserver подключает виртуальный диск и создает набор правил для ограничения пропускной способности. Все правила помещаются в отдельный набор (set), номер которого равен номеру виртуального диска + 1. Так мы перекладываем работу по уникализации номеров наборов правил на плечи команды mdconfig.

# vi /usr/local/bin/startvserver

# Подключаем виртуальный диск сервера
MDNUM=`mdconfig -n -a -t vnode -f $JAILDIR/$IP.image`
mount /dev/md${MD}c $JAILDIR/$IP
echo $MDNUM > $JAILDIR/$IP.run
# Привязываем виртуальный сервер к сетевому интерфейсу…
ifconfig $IF inet alias $IP
# …и накладываем ограничения на пропускную способность
FWSETNUM=$(($MDNUM+1))
ipfw set disable $FWSETNUM
ipfw add set $FWSETNUM pipe ${FWSETNUM}0 ip from any to $IP
ipfw add set $FWSETNUM pipe ${FWSETNUM}1 ip from $IP to any
ipfw pipe ${FWSETNUM}0 config bw $BANDWITH
ipfw pipe ${FWSETNUM}1 config bw $BANDWITH
ipfw set enable $FWSETNUM
# Запускаем сервер

…

Скрипт stopvserver удаляет закрепленный за виртуальным сервером набор правил ipfw и отключает виртуальный диск.

# vi /usr/local/bin/stopvserver

# Отключаем виртуальный диск
MDNUM=`cat $JAILDIR/$IP.run`
umount $JAILDIR/$IP
mdconfig -d -u $MDNUM
# Удаляем правила ipfw и IP-адрес виртуального сервера
FWSETNUM=$((MDNUM+1))
ipfw delete set $FWSETNUM
ifconfig $IF inet -alias $IP

Мониторинг

Перед тем, как приступить к настройке системы мониторинга, определимся с тем, что же мы все-таки собираемся мониторить. Одним из требований к нашему сервису была полная свобода клиента в отношении виртуального сервера. Поэтому мы не можем выполнять мониторинг сервисов клиентов, и им придется самим позаботиться об этом. C другой стороны, мы должны обеспечить бесперебойную работу всех виртуальных серверов, что потребует постоянного слежения за их работоспособностью.

Создать базовую систему мониторинга виртуальных серверов не так уж и сложно. Надо всего лишь взять вывод утилиты jls и сравнить его со списком готовых к выполнению серверов (статус 'ok' в /usr/jailbase/db). Если какой-то сервер готов, но не запущен - попытаться его запустить и снова сравнить списки. Если сервера вновь не окажется в списке - отключить его (установить статус 'disabled') и начать бить тревогу (отправить письмо админу и создать специальный файл $JAILDIR/$IP.trouble).

У нас уже есть скрипт для cron, который останавливает и отключает виртуальные серверы, срок аренды которых истек. Добавим нужную функциональность прямо в него. Ниже приведен сокращенный вариант этого скрипта; он содержит базовый набор действий, выполняемых над каждым «активным» сервером:

# vi /usr/local/bin/watchvservers

# Проверяем, запущен ли сервер
check_running()
{
# Покидаем функцию, если сервер работает исправно
jls | grep $IP && return
# Иначе пробуем его запустить
/usr/local/bin/startvserver $IP > /tmp/startvserver.out 2>&1
sleep 15
# Запустился? Тогда покидаем функцию
jls | grep $IP && return
# Нет? Тогда отключаем его, генерируем файл trouble и отправляем письмо админу
/usr/local/bin/disablevserver $IP
cp /tmp/startvserver.out $JAILDIR/${IP}.trouble
cat $JAILDIR/${IP}.trouble | mail -s "watchvservers: проблемы с сервером $IP" root
exit
}
# Проверяем на истечение срока аренды
check_est_time()
{
# Останавливаем и отключаем сервер, если его срок аренды истек
if [ $CURTIME -ge $ESTTIME ]; then
/usr/local/bin/stopvserver $IP
/usr/local/bin/disablevserver $IP
# Отправляем письмо админу и владельцу
cat $JAILDIR/${IP}.expire | mail -s "watchvservers: истек срок аренды сервера $IP" root
cat /usr/jailbase/message_expire | mail -s "www.host.com: срок аренды вашего сервера истек" $ACMAIL
fi
}

Пропишем скрипт в задания cron:

# crontab -e

MAILTO=root
*/20 * * * * /usr/local/bin/watchvservers

Скрипт будет следить за работоспособностью виртуальных серверов и докладывать об истечении срока аренды или проблемах администратору. В первом случае админ получит письмо с темой «watchvservers: проблемы с сервером <IP>» и выводом неудавшейся команды startvserver, который также будет записан в файл $JAILDIR/$IP.trouble. Задача админа в этом случае - подключиться к корневой машине, исправить проблему и вновь запустить виртуальный сервер. Во втором случае администратор получит сообщение «watchvservers: истек срок аренды сервера <IP>», после чего он может либо сразу удалить сервер командой delvserver, либо дождаться продления аренды владельцем (на его адрес тоже будет выслано специальное письмо, содержимое которого берется из файла /usr/jailbase/message_expire). В обоих случаях сервер будет переведен в состояние disabled и остановлен.
Команда mail подключается к локальному почтовому серверу для отправки письма, поэтому придется поднять Sendmail/Postfix или простой релей на основе ssmtp:

# cd /usr/ports/mail/ssmtp
# make install replace clean

Конфигурируем:

# vi /usr/local/etc/ssmtp/ssmtp.conf

# Кому пересылать почту, направленную пользователю root
root=admin@host.com
# Адрес почтового сервера
mailhub=mail.host.com
rewritedomain=host.com
hostname=_HOSTNAME_

Добавляем в файл обратных адресов запись, указывающую на адрес, с которого будут приходить письма от любого процесса, запущенного с правами root:

# echo root:system@`hostname` > /usr/local/etc/ssmtp/revaliases

Корневая система также нуждается в постоянном наблюдении. Вот несколько параметров, которые требуют внимания:

  • Работоспособность корневых сервисов;
  • Нагрузка на сетевые интерфейсы;
  • Нагрузка на CPU/RAM;
  • Нагрузка на диск;
  • Свободное дисковое пространство;
  • Целостность.

Мы разделим систему наблюдения за хост-системой на две независимые части, первая из которых будет производить мониторинг ресурсов сервера и отдавать данные другой машине. Вторая будет следить за сбоями и аномалиями и оповещать в случае необходимости системного администратора.

Монитор munin, основанный на клиент-серверной архитектуре, прост и удобен в установке и настройке. Мы воспользуемся им для решения первой задачи. Для начала установим ноду munin, которая будет следить за параметрами системы и отдавать накопленные данные серверу:

# cd /usr/port/sysutils/munin-node
# make install clean

Создаем новый конфигурационный файл:

# cd /usr/local/etc/munin
# cp munin-node.conf.sample munin-node.conf

Открываем файл munin-node.conf и добавляем в него две строки:

# vi /usr/local/etc/munin/munin-node.conf

# Имя хоста
host_name jail.host.com
# Адрес сервера, который будет собирать и отображать статистику
allow ^172\.168\.0\.1$"

Нода munin использует плагины для наблюдения за параметрами системы. Плагины лежат в каталоге /usr/local/share/munin/plugins, но реально используются только те из них, ссылки на которые есть в каталоге /usr/local/etc/munin/plugins. Поэтому переходим в этот каталог и создаем ссылки на нужные плагины:

# cd plugins
# for i in cpu df df_inode load memory netstat open_files \
swap vmstat; do ln -s /usr/local/share/munin/plugins/$i \
$PWD/$i; done

Также добавим ссылки на плагины if_ и if_errcoll_, которые предназначены для наблюдения за сетевыми интерфейсами (выполни эти команды для всех сетевых интерфейсов):

# ln -s /usr/local/share/munin/plugins/if_ $PWD/if_ed0
# ln -s /usr/local/share/munin/plugins/if_errcol_ $PWD/if_errcol_ed0

Запускаем munin-node и прописываем его в /etc/rc.conf для автоматической загрузки:

# /usr/local/etc/munin-node.sh start
# echo "munin_node_enable=\"YES\"" >> /etc/rc.conf

На клиентской стороне все. Осталось настроить службу, которая будет опрашивать ноды и генерировать графики. Поэтому идем на сервер (который может быть той же машиной) и устанавливаем на нем munin-main:

# cd /usr/ports/sysutils/munin-main
# make install clean

Открываем файл munin.conf:

# cd /usr/local/etc/munin
# cp munin.conf.sample munin.conf

И добавляем в него следующие строки:

# vi /usr/local/etc/munin/munin.conf

# Имя клиента c установленным munin-node
[jail.host.com]
# И его IP
address 172.30.5.129
use_node_name yes

Больше ничего трогать не надо. Команда munin-cron, которую система портов заботливо вписала в задания cron, будет запускаться каждые пять минут, собирать статистику с подчиненных нод, генерировать графики и помещать их в каталог /usr/local/www/munin. Чтобы просматривать графики, ты можешь либо поднять Web-сервер на этой машине, либо просто набрать «file:///usr/local/www/munin/index.html» в адресной строке браузера.

Возвращаемся на хост виртуальных серверов и приступаем к настройке monit, простого и удобного демона, следящего за порядком в нашей системе. Демон monit будет нашим сторожевым псом, которому мы прикажем сигнализировать обо всех неприятных ситуациях, таких как чрезмерная загруженность системы, заканчивающееся дисковое пространство или падение сервисов.

Устанавливаем monit:

# cd /usr/ports/sysutils/monit
# make install clean

И добавляем в его конфиг:

# vi /usr/local/etc/monitrc

# Проверка общих параметров системы: loadavg, занятая память, нагрузка на процессор
check system myhost.mydomain.tld
if loadavg (1min) > 4 then alert
if loadavg (5min) > 2 then alert
if memory usage > 85% then alert
if cpu usage (user) > 90% then alert
if cpu usage (system) > 30% then alert
if cpu usage (wait) > 20% then alert
# Проверка «замусоренности» файловой системы
# ad0s2 - это диск, смонтированный к /usr или к $JAILDIR
check device usrfs with path /dev/ad0s2
if space usage > 80% then alert
# Следим за работоспособностью демона sshd
check process sshd with pidfile /var/run/sshd.pid
start program = "/etc/rc.d/sshd start"
stop program = "/etc/rc.d/sshd stop"
if failed port 22 protocol ssh then restart
if 5 restarts within 5 cycles then timeout

Прописываем monit в /etc/rc.conf и запускаем:

# echo "monit_enable=\"YES\"" >> /etc/rc.conf
# /usr/local/etc/rc.d/monit start

Гетерогенность

Пятое поле базы виртуальных серверов зарезервировано для хранения версии FreeBSD-окружения. С его помощью мы убиваем сразу двух зайцев. Во-первых, возможность выбора версии FreeBSD-сервера делает наш сервис более привлекательным для клиентов, – некоторым из них в силу определенных причин может понадобиться конкретная ревизия ОС. Во-вторых, метод прямого указания версии позволяет сохранить добрую тысячу нервных клеток во время обновления корневой ОС до новой версии. Разработчики FreeBSD следуют давней традиции сохранения совместимости с прошлыми ядрами, так что после обновления ОС существующие окружения останутся полностью работоспособными.
В созданной нами системе существует только один вариант окружения, версия которого соответствует версии корневой ОС. В то же время FreeBSD 7.1 способна исполнять код, скомпилированный для предыдущих версий системы, вплоть до 4.11. Поэтому, если возникнет такая необходимость, просто скачай один из предыдущих релизов, установи его на соседний раздел и скопируй в новый базовый виртуальный диск. Срез нужной версии дерева портов можно получить с помощью cvsup:

# vi ~/ports-supfile

*default host=cvsup2.ru.FreeBSD.org
*default base=/var/db
*default prefix=/usr/jailbase
*default release=cvs tag=RELENG_6_4
*default delete use-rel-suffix
*default compress
ports-all

# cvsup ~/ports-supfile
# mv /usr/jailbase/{ports,ports-6.4-RELEASE}

Что дальше?

Наша система управления и поддержки виртуальных серверов полностью готова к использованию, но как применить ее возможности для создания полноценного сервиса? Есть три варианта:

1. Все на одной машине. В этом случае BIND, Web-сервер и созданная нами система находятся на одной машине. На Web-сервере крутится сайт сервиса, который предоставляет клиенту возможность создать виртуальный сервер. Для этого он должен заполнить несколько форм, где указать доменное имя, свой почтовый адрес, срок аренды, тип аккаунта и т.д. Скрипт (может быть написан на PHP, Python, Perl) обрабатывает поля формы, формирует из них запрос к скрипту addvserver и вызывает его. Затем запускает скрипт startvserver, а клиенту возвращает страничку с адресом его сервера и сгенерированным ключом для доступа по SSH. Когда срок аренды истечет, watchvservers автоматически остановит сервер и отправит клиенту уведомление.

2. Масштабируемая система. Одна машина едва ли выдержит более 15 виртуальных серверов, поэтому мы должны иметь возможность в любой момент добавить новую машину в инфраструктуру. Масштабируемая система предполагает наличие центрального сервера, обслуживающего Web-сервер, BIND, Sendmail, сервер munin и отправляющего запросы на создание виртуальных серверов на другие машины. Для ее реализации необходимо лишь предустановить на каждую машину нашу систему управления виртуальными серверами, munin-node, minit и ssmtp, а на центральный сервер добавить небольшую обертку для addvserver. Она будет сверяться со специальным списком машин, обслуживающих виртуальные сервера, выбирать из них ту, на которой крутится наименьшее количество серверов, подключаться к ней по SSH и выполнять команду addvserver. После чего – снова открывать список и увеличивать число виртуальных серверов для этой машины на единицу (плюс обертка для delvserver).

3. Правильная масштабируемая система. Второй вариант неплох, но предполагает хранение базы серверов /usr/jailbase/db на каждой машине. Это усложняет обслуживание и затрудняет миграцию виртуальных серверов между хостами. Правильная масштабируемая система должна использовать единую базу данных с интерфейсом SQL вместо файла db. В этом случае база данных хранится на центральном сервере, а скрипты управления виртуальными серверами подключаются к ней вместо обращения к локальному файлу db. Реализация этой идеи потребует значительной модификации скриптов управления и добавления дополнительной колонки «IP машины, на которой крутится виртуальный сервер» к базе данных, чтобы скрипт мог найти виртуальные сервера, принадлежащие только его машине.

Ограничения CPU/RAM

В нынешнем состоянии подсистема jail-окружений FreeBSD не поддерживает ограничения на объем виртуальной памяти или время использования процессора. Это серьезный недостаток, над устранением которого работают не первый год. В 2006 году Крис Джонс попал с проектом реализации идеи ограничений на Google Summer of Code и даже представил работоспособные патчи для шестой ветки, однако разработчики FreeBSD не приняли их. Позднее Кристофер Тюнс портировал на FreeBSD 7.0 часть этих патчей, отвечающую за ограничение объемов памяти, но не осилил порт остального кода. Наработки обоих энтузиастов доступны на страничке wiki.freebsd.org/Jails. Впрочем, не стоит рассчитывать на стабильность или высокую производительность ядра после наложения патчей.

DVD

Полные версии конфигов и скриптов (с комментариями) ты найдешь на диске.

Содержание