Application: ISC-DHCPd, InCron и RSync.
Начальные условия: располагаем корпоративной и провайдерской сетью с десятком-другим тысяч пользователей, от которых исходит густой поток DHCP-запросов на выдачу сетевых настроек.
Задача: учитывая то, что прерывать обслуживание приносящих деньги клиентов нельзя, а традиционно используемое в Linux приложение ISC-DHCPd не поддерживает применение произвольной новой конфигурации без полного перезапуска, необходимо наладить предоставление сервиса с двух идентично настроенных серверов, одновременно и по отдельности могущих обработать весь поток запросов, по возможности автоматизировав распространение настроек с условного "первичного" сервера на "вторичный".
Устанавливаем набор программного обеспечения, с которым будем работать:
# apt-get install isc-dhcp-server incron rsync tree
Настройка кластеризации ISC-DHCPd.
В "этом вашем Linux" ещё с прошлого века прижился DHCP-сервер разработки американской "Internet Systems Consortium", решения которой не блещут по нынешним временам изящностью, но работают везде. С ростом популярности и повышением требований к непрерывности сервиса для распределения нагрузки в него вкрутили куцый протокол обмена записями о клиентских состояниях и отчасти балансировки.
Работает это примерно так:
Каждый из DHCP-серверов сразу после запуска приступает к автономному обслуживанию клиентских запросов, сохраняя состояния таковых в файле "dhcpd.leases" (по умолчанию располагающемся в директории "/var/lib/dhcp/").
При запуске и периодически в процессе работы DHCP-сервер пытается подключится к указанному в настройках "failover" серверу-соседу и обменяться с ним данными о сохраняемых в файле "dhcpd.leases" клиентских состояниях, по возможности синхронизируя таковые.
После успешной синхронизации сведений о клиентских состояниях оба DHCP-сервера переходят в согласованный режим обслуживания клиентов, с обменом данными об активных "lease"-ах, по возможности балансируя нагрузку (если это явно настроено).
Развал "кластера" или рассинхронизация сведений о состояниях практически не влечёт за собой глобальных сбоев - как правило конфликты "lease"-ов решаются автоматическим отзывом проблемных адресов с последующей их выдачей по вынужденному запросу клиента.
При запуске и периодически в процессе работы DHCP-сервер пытается подключится к указанному в настройках "failover" серверу-соседу и обменяться с ним данными о сохраняемых в файле "dhcpd.leases" клиентских состояниях, по возможности синхронизируя таковые.
После успешной синхронизации сведений о клиентских состояниях оба DHCP-сервера переходят в согласованный режим обслуживания клиентов, с обменом данными об активных "lease"-ах, по возможности балансируя нагрузку (если это явно настроено).
Развал "кластера" или рассинхронизация сведений о состояниях практически не влечёт за собой глобальных сбоев - как правило конфликты "lease"-ов решаются автоматическим отзывом проблемных адресов с последующей их выдачей по вынужденному запросу клиента.
Настройка взаимодействия между серверами-соседями (их может быть только два: "primary" и "secondary" - дальнейшее масштабирование распределённой схемы обслуживания невозможно) весьма проста и укладывается в один блок параметров (в данном примере без распределения ролей балансировки нагрузки):
# vi /etc/dhcp/dhcpd.conf
....
# Блок описания параметров взаимодействия DHCP-серверов
failover peer "failover-partner" {
# Роль в спарке (primary|secondary)
primary;
# FQDN/IP-адрес и TCP-порт этого сервера для обслуживания входящих запросов серверных взаимодействий
address dhcp.example.net;
port 519;
# FQDN/IP-адрес и TCP-порт сервера-соседа
peer address dhcp2.example.net;
peer port 519;
# Задержки ожидания ответа сервера-соседа
# (в отсутствии связи сервер перейдёт в автономный режим обслуживания всех поступающих запросов)
max-response-delay 60;
max-unacked-updates 10;
}
# Блок описания параметров взаимодействия DHCP-серверов
failover peer "failover-partner" {
# Роль в спарке (primary|secondary)
primary;
# FQDN/IP-адрес и TCP-порт этого сервера для обслуживания входящих запросов серверных взаимодействий
address dhcp.example.net;
port 519;
# FQDN/IP-адрес и TCP-порт сервера-соседа
peer address dhcp2.example.net;
peer port 519;
# Задержки ожидания ответа сервера-соседа
# (в отсутствии связи сервер перейдёт в автономный режим обслуживания всех поступающих запросов)
max-response-delay 60;
max-unacked-updates 10;
}
Естественно, параметры взаимодействия нужно применить к обеим DHCP-серверам, определив каждому из них свои роли и настройки сетевого подключения, взаимно противоположные.
Сервер ISC-DHCPd принимает параметры конфигурации только путём полного перезапуска (даже в будущем разработчиками не планируется поддержка перезагрузки по чему-то вроде сигнала SIGHUP), так что важно предварительно убедится в корректности внесённых изменений:
# dhcpd -t -cf /etc/dhcp/dhcpd.conf
# /etc/init.d/isc-dhcp-server restart
# /etc/init.d/isc-dhcp-server restart
Обязательно просматриваем свежие записи журнала событий (настраивается отдельно):
# tail -1000 /var/log/dhcpd.log
....
dhcpd: For info, please visit https://www.isc.org/software/dhcp/
dhcpd: Internet Systems Consortium DHCP Server 4.1.1-P1
....
dhcpd: Wrote 0 deleted host decls to leases file.
dhcpd: Wrote 0 new dynamic host decls to leases file.
dhcpd: Wrote 32736 leases to leases file.
dhcpd: failover peer failover-partner: I move from normal to startup
dhcpd: failover peer failover-partner: peer moves from normal to communications-interrupted
dhcpd: failover peer failover-partner: I move from startup to normal
....
dhcpd: balancing pool 1316a70 10.20.30.0/24 total 245 free 97 backup 148 lts -25 max-own (+/-)25
dhcpd: balanced pool 1316a70 10.20.30.0/24 total 245 free 97 backup 148 lts -25 max-misbal 37
dhcpd: Sending updates to failover-partner.
dhcpd: For info, please visit https://www.isc.org/software/dhcp/
dhcpd: Internet Systems Consortium DHCP Server 4.1.1-P1
....
dhcpd: Wrote 0 deleted host decls to leases file.
dhcpd: Wrote 0 new dynamic host decls to leases file.
dhcpd: Wrote 32736 leases to leases file.
dhcpd: failover peer failover-partner: I move from normal to startup
dhcpd: failover peer failover-partner: peer moves from normal to communications-interrupted
dhcpd: failover peer failover-partner: I move from startup to normal
....
dhcpd: balancing pool 1316a70 10.20.30.0/24 total 245 free 97 backup 148 lts -25 max-own (+/-)25
dhcpd: balanced pool 1316a70 10.20.30.0/24 total 245 free 97 backup 148 lts -25 max-misbal 37
dhcpd: Sending updates to failover-partner.
Справедливости ради надо отметить, что в свежих версиях ISC-DHCPd реализована поддержка API изменения конфигурации "на лету", которым можно пользоваться посредством утилиты "omshell", но пока функционал очень урезан и вообще кривовато работает, так что проще держать два сервера, каждый из которых по отдельности можно перезагружать безболезненно для клиентов.
Упорядочивание файлов конфигурации ISC-DHCPd.
Одна из важных составляющих поставленной задачи - организация простого автоматического переноса изменений конфигурации от "первичного" DHCP-сервера ко "вторичному". Отличия между членами "кластера" лишь в параметрах "failover", а значит можно легко разделить настройки на два набора: свойственный конкретному серверу и разделяемый обеими.
Практическая реализация простая:
Один (корневой "/etc/dhcp/dhcpd.conf") конфигурационный файл описывает параметры сопряжения "failover peer";
Все остальные параметры, одинаковые для "главного" и "вторичного" DHCP-серверов описания клиентских подсетей, выносятся в директорию "/etc/dhcp/dhcpd.subnets/", которая будет синхронизироваться между таковыми.
Все остальные параметры, одинаковые для "главного" и "вторичного" DHCP-серверов описания клиентских подсетей, выносятся в директорию "/etc/dhcp/dhcpd.subnets/", которая будет синхронизироваться между таковыми.
Выглядит это примерно так:
# tree /etc/dhcp/
/etc/dhcp/
├── dhcpd.conf
├── dhcpd.subnets
│ ├── main.conf
│ ├── subnet_one.conf
│ ├── subnet_two.conf
│ ├── ....
│ └── subnets.conf
└── README
├── dhcpd.conf
├── dhcpd.subnets
│ ├── main.conf
│ ├── subnet_one.conf
│ ├── subnet_two.conf
│ ├── ....
│ └── subnets.conf
└── README
Чтобы хоть как-то сориентировать желающего внести дополнения в конфигурацию клиентских подсетей я оставляю в корне директории настроек сервера файл с описанием его статуса "/etc/dhcp/README", что в сочетании с говорящими именами вроде "dhcp.example.net" и "dhcp2.example.net" позволяет надеятся на то, что новенький специалист не станет изменять настройки "вторичного" DHCP-сервера вместо "первичного":
Этот сервер "dhcp.example.net" считается главным (первичным) и настройки клиентских подсетей нужно изменять на нём. Все изменения автоматически распространяются на вторичный сервер "dhcp2.example.net" посредством "InCron+RSync".
Отслеживание факта изменения конфигурационных файлов.
Несмотря на видимое в первом приближении удобство взаимной безусловной синхронизации работающих в сущности равноценно DHCP-серверов, практически одинаково обслуживающих клиентов (условное разделение в функционале "failover" на "первичный" и "вторичный" серверы обозначает лишь главенство в разрешении конфликтов среди выданных клиентам адресов, но конфигурация подсетей при этом у них полностью идентична) на практике надёжнее автоматически распространять конфигурацию только в одну сторону, от условно "первичного" DHCP-сервера к "вторичному".
Для этого настраиваем две цепочки процессов:
На "первичном" мониторим изменения файлов и по мере накопления событий высылаем обновления "вторичному" посредством RSync.
На "вторичном" мониторим поступившие изменения и, по мере накопления событий, перезапускаем DHCP-сервер для принятия изменений.
На "вторичном" мониторим поступившие изменения и, по мере накопления событий, перезапускаем DHCP-сервер для принятия изменений.
Процедуры синхронизации и перезапуска сервера будем производить только в том случае, если проверка корректности синтаксиса конфигурационных файлов прошла успешно.
Отслеживать события модификации конфигурационных файлов будем с помощью приложения "InCron", опирающегося на функционал компонента ядра Linux "inotify". "InCron" напоминает привычный "Cron", но выполняет действия не по наступлению даты и времени, а по событиям в файловой системе, поступающим от ядра Linux посредством уведомлений "inotify". Это просто, немедленно и минимально по затратам системных ресурсов.
Обработка событий изменения конфигурации "первичного" DHCP-сервера.
Заранее заготовим Bash-скрипт, который будет реализовывать логику обработки событий.
Важный нюанс в том, что во время создания, редактирования и удаления объектов файловой системы происходит не одно, а несколько событий "inotify", причём число их разниться в зависимости от инструментария воздействия - потому мы не можем точно определить момент, когда нужно приступать к синхронизации объектов и вынуждены делать это по накоплению некоторого количества событий (на практике достаточно и не избыточно делать это на каждом пятом).
# mkdir -p /etc/incron.scripts
# vi /etc/incron.scripts/rsync-dhcpd.sh
# chmod ug+x /etc/incron.scripts/rsync-dhcpd.sh
# vi /etc/incron.scripts/rsync-dhcpd.sh
# chmod ug+x /etc/incron.scripts/rsync-dhcpd.sh
#!/bin/bash
SOURCE="/etc/dhcp/dhcpd.subnets/"
TARGET="rsync://dhcp2.example.net:873/etc-dhcpd-subnets/"
COUNTER="/var/run/incron/vars/RSYNC_DHCPD_COUNT"
# Накручиваем счётчик количества вызовов
COUNT=$(($(cat "${COUNTER}")+1))
# Запускаем процедуру только после некоторого количества вызовов скрипта
if [ "${COUNT}" -gt "5" ]
then
# Запускаем синхронизацию только в том случае, если файл конфигурации DHCP-сервера корректен
/usr/sbin/dhcpd -f -t -q 2>&1 >/dev/null && /usr/bin/rsync --checksum --recursive --delete "${SOURCE}" "${TARGET}"
# Сбрасываем счётчик количества вызовов скрипта
COUNT=0
fi
# Сохраняем переменную счётчика
[ -f "${COUNTER}" ] || mkdir -p "$(dirname "${COUNTER}")"
echo "${COUNT}" > "${COUNTER}"
exit 0
SOURCE="/etc/dhcp/dhcpd.subnets/"
TARGET="rsync://dhcp2.example.net:873/etc-dhcpd-subnets/"
COUNTER="/var/run/incron/vars/RSYNC_DHCPD_COUNT"
# Накручиваем счётчик количества вызовов
COUNT=$(($(cat "${COUNTER}")+1))
# Запускаем процедуру только после некоторого количества вызовов скрипта
if [ "${COUNT}" -gt "5" ]
then
# Запускаем синхронизацию только в том случае, если файл конфигурации DHCP-сервера корректен
/usr/sbin/dhcpd -f -t -q 2>&1 >/dev/null && /usr/bin/rsync --checksum --recursive --delete "${SOURCE}" "${TARGET}"
# Сбрасываем счётчик количества вызовов скрипта
COUNT=0
fi
# Сохраняем переменную счётчика
[ -f "${COUNTER}" ] || mkdir -p "$(dirname "${COUNTER}")"
echo "${COUNT}" > "${COUNTER}"
exit 0
Составляем правило реакции на интересующие нас события изменения конфигурационных файлов:
# vi /etc/incron.d/rsync-dhcpd
#InCron_table_does_not_support_comments_with_several_(more_than_one)_words!
#<directory>_<file_change_mask>_<command_or_action>_<options>
/etc/dhcp/dhcpd.subnets/ IN_CLOSE_WRITE,IN_CREATE,IN_DELETE /etc/incron.scripts/rsync-dhcpd.sh
#<directory>_<file_change_mask>_<command_or_action>_<options>
/etc/dhcp/dhcpd.subnets/ IN_CLOSE_WRITE,IN_CREATE,IN_DELETE /etc/incron.scripts/rsync-dhcpd.sh
Сервис "incrond" мониторит состояние своих конфигурационных файлов и сразу применяет изменения:
# tail -100 /var/log/syslog
....
incrond[11568]: system table rsync-dhcpd changed, reloading
incrond[11568]: system table rsync-dhcpd changed, reloading
Для пущей уверенности можно дать указание перечитать конфигурацию принудительно:
# /etc/init.d/incron reload
Для того, чтобы скомпенсировать возможные сбои в работе реагирующей на события подсистемы "incron" дополнительно запланируем вызов процедуры синхронизации каждые пять часов:
# vi /etc/crontab
....
# Run the synchronization preparation script
1 */5 * * * root /etc/incron.scripts/rsync-dhcpd.sh &
# Run the synchronization preparation script
1 */5 * * * root /etc/incron.scripts/rsync-dhcpd.sh &
Очевидно, что я предпочитаю в автоматическом режиме лишь отправлять изменения конфигурации "первичного" DHCP-сервера на "вторичный", в то время как применение таковой в работу на "первичном" сервере остаётся ручным. Полагаю, что администратору сервиса виднее, когда следует активировать набор изменений. Правда, это конфликтует с идеей полностью автоматического применения изменений, отправляемых на "вторичный" DHCP-сервер, но в данном случае это вынужденный компромис, обусловленный примитивностью используемого "ISC DHCPd".
Обработка событий изменения конфигурации "вторичного" DHCP-сервера.
Следующим шагом, считая, что по накоплению некого порога количества модификаций конфигурации таковая выгружается с "первичного" DHCP-сервера на "вторичный", обеспечим приём изменённых файлов и перезапуск сервиса DHCPd по готовности.
Разрешим стартовому скрипту "/etc/init.d/rsync" запускать сервис "rsyncd" и зададим ряд его параметров в файле "/etc/default/rsync":
....
RSYNC_ENABLE=true
....
RSYNC_CONFIG_FILE=/etc/rsyncd.conf
....
RSYNC_OPTS=' --ipv4 --port=873'
RSYNC_ENABLE=true
....
RSYNC_CONFIG_FILE=/etc/rsyncd.conf
....
RSYNC_OPTS=' --ipv4 --port=873'
В конфигурационном файле выше мы привязали сервис "rsyncd" к определённому протоколу (только IPv4) и прослушиваемому порту.
Приведём конфигурационный файл сервиса "RSync" к следующей базовой конфигурации:
# vi /etc/rsyncd.conf
log file = /var/log/rsyncd.log
pid file=/var/run/rsyncd.pid
# hosts allow = dhcp.example.net
hosts allow = 1.2.3.4
max connections = 1
dont compress = *
uid = root
gid = root
use chroot = yes
[etc-dhcpd-subnets]
path = /etc/dhcp/dhcpd.subnets
write only = yes
read only = no
list = no
pid file=/var/run/rsyncd.pid
# hosts allow = dhcp.example.net
hosts allow = 1.2.3.4
max connections = 1
dont compress = *
uid = root
gid = root
use chroot = yes
[etc-dhcpd-subnets]
path = /etc/dhcp/dhcpd.subnets
write only = yes
read only = no
list = no
# /etc/init.d/rsync restart
Аналогично решению на исходном зеркалируемом сервисе здесь заранее заготовим Bash-скрипт, который будет реализовывать логику обработки событий приёма данных и перезапускать DHCP-сервер не по каждому событию "inotify", а после накопления некоторого их количества (на практике достаточно и не избыточно делать это после каждой второй синхронизации).
# mkdir -p /etc/incron.scripts
# vi /etc/incron.scripts/dhcpd-reload.sh
# chmod ug+x /etc/incron.scripts/dhcpd-reload.sh
# vi /etc/incron.scripts/dhcpd-reload.sh
# chmod ug+x /etc/incron.scripts/dhcpd-reload.sh
#!/bin/bash
COUNTER="/var/run/incron/vars/DHCPD_RELOAD_COUNT"
# Накручиваем счётчик количества вызовов
COUNT=$(($(cat "${COUNTER}")+1))
# Запускаем процедуру только после некоторого количества вызовов скрипта
if [ "${COUNT}" -gt "2" ]
then
# Перезапускам сервис только в том случае, если файл конфигурации DHCP-сервера корректен
/usr/sbin/dhcpd -f -t -q 2>&1 >/dev/null && /etc/init.d/isc-dhcp-server restart
# Сбрасываем счётчик количества вызовов скрипта
COUNT=0
fi
# Сохраняем переменную счётчика
[ -f "${COUNTER}" ] || mkdir -p "$(dirname "${COUNTER}")"
echo "${COUNT}" > "${COUNTER}"
exit 0
COUNTER="/var/run/incron/vars/DHCPD_RELOAD_COUNT"
# Накручиваем счётчик количества вызовов
COUNT=$(($(cat "${COUNTER}")+1))
# Запускаем процедуру только после некоторого количества вызовов скрипта
if [ "${COUNT}" -gt "2" ]
then
# Перезапускам сервис только в том случае, если файл конфигурации DHCP-сервера корректен
/usr/sbin/dhcpd -f -t -q 2>&1 >/dev/null && /etc/init.d/isc-dhcp-server restart
# Сбрасываем счётчик количества вызовов скрипта
COUNT=0
fi
# Сохраняем переменную счётчика
[ -f "${COUNTER}" ] || mkdir -p "$(dirname "${COUNTER}")"
echo "${COUNT}" > "${COUNTER}"
exit 0
Составляем правило реакции на интересующие нас события изменения (по факту загрузки посредством RSync) конфигурационных файлов:
# vi /etc/incron.d/dhcpd-reload
#InCron_table_does_not_support_comments_with_several_(more_than_one)_words!
#<directory>_<file_change_mask>_<command_or_action>_<options>
/etc/dhcp/dhcpd.subnets/ IN_MODIFY,IN_CREATE,IN_DELETE /etc/incron.scripts/dhcpd-reload.sh
#<directory>_<file_change_mask>_<command_or_action>_<options>
/etc/dhcp/dhcpd.subnets/ IN_MODIFY,IN_CREATE,IN_DELETE /etc/incron.scripts/dhcpd-reload.sh
Для того, чтобы скомпенсировать возможные сбои в работе реагирующей на события подсистемы "incron" дополнительно запланируем вызов процедуры синхронизации каждые пять часов:
# vi /etc/crontab
....
# Run the reload preparation script
1 */5 * * * root /etc/incron.scripts/dhcpd-reload.sh &
# Run the reload preparation script
1 */5 * * * root /etc/incron.scripts/dhcpd-reload.sh &
Очевидно, что по мере поступления изменений в конфигурации сервис DHCP будет автоматически перезагружаться (после успешной проверки синтаксиса, разумеется).
Режим использования.
Итак, мы запустили "вторичный" DHCP-сервер, настроили отслеживание модификации конфигурации "первичного" сервера, выгрузку таковой на "вторичный" и последующий перезапуск сервиса "вторичного" сервера для применения изменений. В общем схема проста и надёжна (у меня она с год как работает без намёка на сбой), а эксплуатация её сводится к правке файлов описания клиентских подсетей в директории "/etc/dhcp/dhcpd.subnets/" на "первичном" сервере, синтаксической проверке и перезапуске сервиса только на "первичном" сервере (на "вторичном" изменения с некоторой задержкой распространятся полностью автоматически):
# vi /etc/dhcp/dhcpd.subnets/subnets.conf
# dhcpd -t
# /etc/init.d/isc-dhcp-server restart
# dhcpd -t
# /etc/init.d/isc-dhcp-server restart