Возможно у Вас уже возникало когда-либо желание внести все данные bind'а в
SQL-базу во имя удобства и простоты обновления зон. Во всяком случае у меня оно
однажды возникло, т.к. число записей во внутренней зоне локальной сети стало
измеряться уже не одним десятком тысяч. После некоторых изысканий решения
данной проблемы в инете, была найдена статья "Интеграция BIND + PostgreSQL" из
журнала "Системный администратор" за декабрь 2006 г. Описаная в статье методика
в общем-то решает проблему как таковую, если не учитывать столь немаловажные
факторы как надёжность и производительность, которые оставляют желать лучшего.
Проблема с надёжностью проявляется если быть немного невнимательным в процессе
конфигурирования (об этом ниже). Проблема с производительностью - если Ваш файл
зоны содержит достаточно большое число записей.
Именно эти проблемы и сподвигли меня на некоторую доработку данной системы. Забегая вперёд: в результате оптимизации производительность
системы удалось повысить в десятки раз.
Скрипт автоматической настройки БД
Если Вам некогда (или просто лень) читать подробное описание настройки БД, то
можете воспользоваться этим
perl-скриптом автоматического создания и конфигурирования БД
необходимой для работы с DLZ. Кроме выполнения скрипта необходимо
пропатчить BIND, а так же - внести изменения в его конфигурационный
файл. Описание изменений находятся ниже.
В случае необходимости добавить ещё одну зону в уже существующую БД, Вы можете
использовать этот скрипт.
Отмечу, что на данный момент скрипты по созданию БД и добавлению зоны, при
внесении записей в таблицы обратных зон обрабатывают только "серые" адреса из
подсетей 10.0.0.0/8 и 192.168.0.0/24.
Настройка по статье "Интеграция BIND + PostgreSQL"
И так, начнём с настройки связки BIND+PostgreSQL руководствуясь статьёй Сергея
Алаева - "Интеграция BIND + PostgreSQL". Стоит сразу оговориться, что в
исходной статье описаны 2 метода соединения BIND и PostgreSQL. Когда я стал с
ними эксперементировать, то первый метод (с использованием SDB) у меня наотрез
отказался работать. Потому решил использовать второй, ибо "DLZ-драйверы ...
более гибкие в настройке и поддерживают широкий набор внешних хранилищ"
(цитата из оригинальной статьи). Предполагается что PostgreSQL у Вас уже
установлен, а BIND собран с поддержкой DLZ. Если это не так, то рекомендую
обратиться к руководству по инсталляции софта в Вашем дистрибутиве. В моём
случае (Linux Gentoo) при сборке BIND'а было достаточно указать в переменной
USE флаги "dlz" и "postgresql".
Настройка PostgreSQL
Разумеется что в первую очередь нам необходимо создать БД в которой будет
находиться информация для BIND, и пользователя от имени которого BIND будет
работать с БД. Для простоты назовём и то и другое именем 'named'. Настройка БД
производится с помощью стандартной утилиты psql входящей в дистрибутив
PostgreSQL.
Создаём таблицы и индексы:
Внимание! Две следующие команды были пропущены
в оригинальной статье, следствием этого являлось падение BIND при попытке
использовать dlz-драйвер.
Проблема возникала т.к. все манипуляции с БД named мы производили от имени
пользователя postgres, поэтому при попытке обратиться к БД от любого другого
пользователя (от того же named) происходила ошибка - "отказано в доступе"
потому что владельцем таблиц в базе named был пользователь postgres. К
сожалению в dlz-драйвере на момент написания статьи присутствовал баг, в
следствии которого происходило двойное освобождение памяти со всеми вытекающими
последствиями. Исправление этой проблемы описано ниже.
Добавим основные записи прямой и обратной зоны для тестового домена
test-zone.info:
Нам необходимо качественно протестировать получившуюся конфугурацию -
желательно добавить в таблицу всё содержимое реальной зоны (в примере реальный
домен 2го уровня заменён на test-zone.info). Доменная зона содержит свыше 22000
хостов - добавлять её в таблицу вручную по меньшей мере неразумно. Для решения
этой задачи я использовал простой perl-скрипт.
Общий вид запроса для добавления хостов в прямую зону:
Общий вид запросов для добавления хоста в обратную зону:
Конфигурация BIND не претерпивает каких-либо серьёзных изменений - если у Вас
уже имеется конфигурационный файл (named.conf), то достаточно добавить в него
настройки для dlz драйвера, а всё остальное оставить как есть.
Краткое пояснение к данной конфигурации DLZ (полное
описание (eng)):
Первая строка указывает named о необходимости использовать DLZ
драйвер PostgreSQL. "dlz" - это новое ключевое слово для bind
добавленное вместе с DLZ патчем.
Вторая строка database "postgres 2" начинается с ключевого слова
database. Оно являеся единственным параметром секции dlz,
и естественно - обязательным. Словом "postgres" мы указываем
BIND'у что желаем использовать драйвер Postgres. Цифра 2 указывает
число открываемых соединений с БД. В случае если BIND работает в однопоточном
режиме - этот параметр игнорируется. В случае использования потоков - BIND
открывает указанное число соединений к БД и всегда держит их открытыми,
даже в случае полного бездействия.
Третья строка {host=localhost port=5432 dbname=named user=named} сообщает
named'у параметры сервера PostgreSQL, назначение параметров очевидно из их
названий.
Остальная часть конфигурации представляет собой SQL запросы. Их назначение
рассмотрено ниже.
На этом настройка закончена и можно перейти к тестированию получившейся
системы.
Тестирование. Часть 1 - без доработки и оптимизации
Для тестирования была использована утилита dnsperf. В портежах Gentoo я
её не нашёл и потому скачал с сайта разработчика (см. ссылки),
в FreeBSD она имеется в портах.
Файл для тестирования прямой зоны сгенерирован командой:
Файл для тестирования несуществующих в зоне доменов создан при помощи команды:
Файл для тестирования обратной зоны сгенерирован
perl-скриптом.
Конфигурация компьютера на котором производились испытания: Intel(R) Celeron(R)
CPU 2.40GHz, 256 Mb RAM. Среднее значение загрузки (выводимое командой uptime)
до начала тестирования меньше 0.05. Результаты тестирования прямой зоны:
Результаты тестирования обратной зоны:
Несуществующие в нашей зоне домены (NXDomain's):
Честно говоря, когда я увидел эти результаты, то был мягко говоря расстроен -
подобная производительность совершенно неприменима на практике. Число запросов
к зоне локальной сети в моём случае доходит примерно до 30-40 в секунду в
пиковые часы. Конечно реальный DNS сервер значительно мощнее тестовой машины,
но подобное время отклика не сможет компенсироваться даже высокими
процессорными мощностями.
Некоторые пояснения к данной конфигурации - сделан отказ от TCP/IP сокетов в
пользу каналов (pipes) для связи BIND с PostgreSQL; ограничение
вывода информации запросом - при помощи limit 1; общее упрощение
запроса на получение ресурса посредством отказа от конструкции case
в запросе, и отказ от сравнения типа ресурса. Так же авторами предложено
поэксперементировать с параметром "shared_buffers" в
конфигурационном файле postgresql.conf, но честно говоря - данные
эксперементы лично мне не принесли ни каких ощутимых результатов. Подробнее
назначение SQL-запросов рассмотрено ниже.
Тестирование. Часть 2.
Проведём тестирование полученной конфигурации.
Прямая зона:
Обратная зона:
Не существующие в нашей зоне домены (NXDomain's):
Данные результаты ощутимо лучше результатов полученных при конфигурации по
умолчанию, но данная настройка имеет недостаток в виде остуствия возможности
переноса зоны.
Стоит отдельно оговориться по поводу данных о производительности,
предоставленных на сайте разработчиков. Согласно им, авторам удалось добиться
производительности 759 запросов в секунду при "database with nearly 2.7 million
records". Я допускаю что совершил какие-то ошибки, причиной которых стало столь
жуткое снижение производительности, которое скорее всего будет не реально
компенсировать аппаратными средствами (например использовав
конфигурацию компьютера как в примере на сайте разработчиков). Если это
действительно так, и Вы найдёт эти ошибки, то буду рад узнать о них. На данный
момент у меня сложилось впечатление что авторы тестировали работу сервера на
зоне вида host1, host2,.... host27000000, либо на большом числе зон с малым
числом хостов в них, что возможно могло улучшить результаты опроса.
В моём случае используется примерно 22000 доменных имён из реальной зоны, и
соответственно примерно столько же для обратной. Но производительность и в
данном случае оказалась по прежнему не применима в реальных условиях.
Дополнительная оптимизация
Рассмотрим по пунктам, что можно предпринять для исправления ситуации, и какие
упущения были допущены разработчиками.
Оптимизация работы с PostgreSQL
Для наглядности ещё раз приведём конфигурацию запросов 1
:
1 - если Вы не хотите использовать
какую-либо секцию опроса, то просто оставьте пустые скобки. Внимание:
в таком случае между скобками не должно быть ни каких символов, даже пробелов,
иначе это вызовет ошибку.
Проанализируем последовательно запросы содержащиеся в конфигурации драйвера и
при необходимости внесём в них изменения:
select zone from dns_records where zone = '%zone%'
Назначение: используется для проверки поддержки БД доменной зоны указанной в
переменной %zone%. Возвращает пустой набор данных если БД не
поддерживает данную зону и хотя бы одну запись если зона поддерживается.
Выполняется перед любым запросом к БД для разрешения DNS запроса.
Если не использовать оптимизированную версию запроса (с 'limit 1'),
то моём случае, когда зона содержит более 22000 записей, при выполнении этого
запроса выводится фактически всё содержимое зоны, что занимает не мало времени
и по всей видимости - ресурсов. Потому будем использовать вариант предолженный
авторами: select zone from dns_records where zone = '%zone%' limit 1
Если БД обслуживает данную зону, то выводится только одна запись что,
существенно сокращает время обработки запроса. В моём случае это дало почти
трёхкратный прирост производительности.
select ttl, type, mx_priority, case when lower(type)='txt' then '\"' || data
|| '\"' else data end from dns_records where zone = '%zone%' and host =
'%record%' and not (type = 'SOA' or type = 'NS')
Назначение: используется при выполнении функции lookup() в dlz
драйвере, фактически - в подавляющем большинстве случаев обращения к зоне
находящейся в БД, выполняется именно этот запрос.
С точки зрения производительности этот запрос имеет следующие проблемы:
Оператор case - представляет собой логическое ветвление и как
следствие, добавляет небольшую задержку выполнения.
"lower(type)='txt'" - приведение к нижнему регистру типа ресурса.
Так же является достаточно дорогостоящей операцией.
"where zone = '%zone%' and host = '%record%' and not (type = 'SOA' or type =
'NS')" - содержит целых 4 операции сравнения.
Первое что можно из него смело удалить - это оператор case. Он
необходим в случае использования TXT записей в зоне, т.к. они
могут содержать в себе пробельные символы. Если не ограничить поле data
кавычками то в результирующем выводе dns-клиента каждое слово будет ограничено
кавычками. Для решения проблемы с case воспользуемся вариантом
предложенным авторами, т.е. будем вносить TXT записии в базу уже
ограниченные кавычками. Однако стоит признать что данное изменение на практике
слабо ощутимо, разница выражается буквально десятыми долями выполненных
запросов в секунду.
После удаления оператора case автоматически исчезает проблема с
функцией lower. Для решеня проблемы с большим числом условий в секции where
можно воспользоваться вариантом авторов: select ttl, type, mx_priority, data, resp_person, serial, refresh, retry,
expire, minimum from dns_records where zone ='%zone%' and host = '%record%'
В данном случае отброшена проверка на тип возвращаемой БД записи, т.к. быстрее
будет его проверять в коде драйвера чем в SQL-запросе.
Большей оптимизации выполнения этого запроса без изменения структуры БД,
пожалуй добиться нельзя. Во всяком случае у меня это не получилось.
select ttl, type, mx_priority, data, resp_person, serial, refresh, retry,
expire, minimum from dns_records where zone = '%zone%' and (type = 'SOA' or
type='NS')
Назначение: выполняется при получении SOA для доменной зоны. Данную и
последующие секции можно опустить во имя оптимизации. Только стоит учесть, что
в этом случае будет невозможен перенос зоны slave-сервером.
select ttl, type, host, mx_priority, data, resp_person, serial, refresh,
retry, expire, minimum from dns_records where zone = '%zone%'
Назначение: выполняется для получения всех записей зоны - необходимо для
переноса зоны.
select zone from xfr_table where zone = '%zone%' and client = '%client%'
Назначение: выполняется перед переносом зоны, для аутентификации клиента. IP
адрес клиента находится в переменной %client%. Если запрос
возвращает соответствие запрашиваемой зоны IP адресу клиента, то клиенту
разрешается осуществить перенос зоны.
Кроме этого, было произведено ещё несколько не описанных выше экспериментов и
опытов над конфигурацией named и БД (перестройка индексов). Затем я отчётливо
осознал что без коренного изменения структуры БД лучшей производительности
добиться практически не реально. И так, приступим!
Создание новых таблиц и индексов.
Основная задержка в работе named образуется за счёт большого числа условий в
запросе:
select ttl, type, mx_priority, data from dns_records
where zone = '%zone%' and host = '%record%' and not (type = 'SOA' or type =
'NS')
Самым логичным действием для ускорения работы будет отказ от большинства
условий. Это сделано при помощи разнесения данных из одной большой таблицы dns_records
в несколько таблиц. Новая структура БД содержит в себе 2 + N таблиц,
где N - число обслуживаемых зон (включая обратные зоны), а 2
образовано из таблицы xfr_table (без изменений) и новой таблицы soa_table
содержащей в себе SOA, NS, TXT записи.
Стоит сразу оговориться что подобная трансформация структуры нацелена на
конфгурации DNS с относительно небольшим количеством зон и большим числом
записей (свыше нескольких тысяч) в каждой зоне. Думаю что в случае большого
числа зон содержащих мало записей подобная конфигурация себя не оправдает. Ниже
представлен пример команд для создания новых таблиц:
Заполнение БД данными.
Для начала - внесём в новую таблицу soa_table данные об
обслуживаемых зонах: Затем, при помощи perl-скриптов заполним таблицы прямой и обратной зоны: Скрипт для прямой зоны. Скрипт для обратной зоны.
Можно сказать, что все вышеописанные операции по созданию и заполнению данными
новой зоны привидены с целью наглядности. При помощи
этого скрипта Вы можете выполнить их с минимальными трудозатратами.
Новая конфигурационная секция dlz в файле named.conf
Теперь при обработке запроса выборка данных производится не из общей таблицы (dns_records),
а из двух таблиц - таблицы soa_table и таблицы имя которой равно
имени зоны %zone%. Рассмотрим изменения произошедшие в
конфигруационном файле:
Проверка наличия запрашиваемой зоны в БД производится выборкой из таблицы soa_table.
Запрос получения ресурса упрощён до одного условия - сравнения по имени хоста.
Выбор зоны производится автоматически благодаря получению имени таблицы из
переменной %zone%. Псевдозначение NULL в начале
секции цель необходимо для соблюдения формата итогового набора
данных - несуществующее в таблице поле ttl должно идти первым,
теперь оно заменяется на псевдозначение NULL.
В запросе на получение SOA изменено только имя таблицы. Теперь SOA данные
хранятся в soa_table.
Запрос на перенос зоны несколько усложнён. Полная информация о зоне теперь
разнесена в две таблицы, поэтому и выборка производится из 2х таблиц. Это
осуществленно при помощи SQL оператора UNION объединяющего
результат выполенения двух SELECT. Псевдозначение NULL
в данном запросе играет такую же роль, как и в запросе на получение ресурса.
Запрос выполняющий аутентификацию клиента для переноса зоны остаётся прежним.
Оптимизация BIND
Как было сказано в начале статьи - полученная связка BIND + PostgreSQL с
настройками по умолчанию не отличается ни быстродействием, ни надёжностью. DLZ
драйвер для BIND содержит в своём коде два недочёта сильно ухудшающих эти
характеристики.
Двойное освобождение памяти - приводит к падению
процесса named.
Ошибка находится в файле contrib/dlz/drivers/dlz_postgres_driver.c
- функция postgres_get_resultset строка 552: PQclear(*rs); /* get rid of it */
В функции PQclear(*rs) выполняется освободжение памяти по адресу
*rs в случае неудачного выполнения запроса к PostgreSQL, после этого
значение указателя не сбрасывается в NULL. rs в данном случае
является указателем на указатель типа PGresult. При последующих
обращениях к указателю (именно указателю а не указателю на указатель)
производится проверка условия if (rs != NULL) которое является
истинным даже если память уже была особождена, это приводит к повтороному
особождению памяти по адресу rs и как следствие падению всего
процесса named.
Авторы забыли отключить логгирование введённое на этапе разработки - в
следствии этого dlz драйвер пишет практически каждое своё действие в системный
лог, что ощутимо замедляет обработку DNS запросов.
На этом можно остановиться в отношении оптимизации кода DLZ драйвера BIND, но
лично меня получившийся результат не устроил. В частном случае - при запросах
PTR ресурсов выполняется много "лишних" запросов в БД - таков алгоритм работы
BIND. В упрощённом варианте (на самом деле всё несколько сложнее) он выглядит
так:
Каждый запрос клиента к DNS преобразуется в запрос named к БД содержащий в себе
параметр host, в котором указывается предполагаемое имя хоста
(изначально оно равно пустой строке) и параметр zone -
предполагаемое имя зоны (изначально равно всей строке доменного имени в DNS
запросе). Затем, осуществляется поиск хоста и зоны в БД. Если поиск успешен, то
клиенту возвращается результат. В противном случае "вырезается" часть строки из
значения zone между нулевым символом и самой левой точкой
(включительно) и добавляется к значению host (исключая замыкающую
точку), после чего повторяется опрос БД. Так продолжается либо пока опрос БД не
вернёт положительный результат, либо пока не будет достигнут конец строки
содержащей строку DNS имени. Например: если мы запрашиваем имя для адреса
192.168.1.1 то самый первый запрос который выполнит named в БД будет содержать
параметры host = '' и zone = '1.1.168.192.in-addr.arpa'.
Подобный запрос в нашем случае вернёт пустой резульатат. Следующий запрос будет
содержать host = '1' и zone = '1.168.192.in-addr.arpa'.
Разумеется что ни первый, ни второй запросы не вернут положительного результата
потому что наша БД не содержит этих зон - успешно выполнится лишь 3й. В случае
с зоной '10.in-addr.arpa' всё выглядит на треть печальней - перед
тем как named найдёт нужную зону в БД он уже выполнит 3 запроса.
Мною создан патч для оптимизации опроса обратных зон для "серых" IP адресов из
подсетей 10.0.0.0/8 и 192.168.0.0/16. Конечно имеется подсеть 172.16.0.0/12,
оптимизация запросов к ней несколько осложняется в силу того, что длина маски
содержит не целое число байт. Т.к. в нашей сети данная подсеть не используется
- я решил опустить написание кода для неё.
Принцип работы патча основан на сравнении конца строки DNS запроса с
подстроками "10.in-addr.arpa" и "168.192.in-addr.arpa".
Если одно из сравнений успешно, то мы "обманываем" bind и сразу заменяем
значение host на соответсвующее значение адреса хоста, а значению
zone присваиваем соответсвующую подстроку ("10.in-addr.arpa"
или "168.192.in-addr.arpa").
Конечно кому-то может не понравиться такая методика, но меня она устроила по
причине более чем двухкратного прироста производительности при опросе обратных
зон для "серых" адресов. Вот
здесь представлен код который вставляется в функцию postgres_get_resultset(...)
из файла contrib/dlz/drivers/dlz_postgres_driver.c в самом начале её
выполнения. Патчи для оптимизации BIND: dlz_inc_private_optimize.patch
- инкрементный патч, уже учитывающий патч для исправления double free
и отключения сообщений в лог.
dlz_full_patch_double-free_optimize_cut-dbg-msgs.patch - полный патч
содеражщий все исправления.
На этом дополнительная оптимизация связки BIND+PostgreSQL завершена.
Тестирование. Часть 3
Для тестирования производительности воспользуемся файлами, которые были
использованы при первичном тестировании. Тестирование прямой зоны:
Тестирование обратной зоны:
Тестирование на NXDomain's:
Как можно удостовериться - производительность увеличилась более чем в 10 раз по
сравнению с вариантом предложенным по умолчанию. Но если окажется что Вам этого
мало, то предлагаю выполнить действия предложенные в следующей части статьи.
Максимальная оптимизация
Для достижения наивысших результатов в области обработки зарпосов от клиентов,
воспользуемся методикой предложенной авторами проекта DLZ. Т.е. откажемся от
возможности переноса зоны с нашего сервера на slave-сервер, в связи с чем
значительно упростится конфигурация, и соответственно - уменьшится число
выполняемых запросов и время обработки отдельного запроса:
Кроме этого в таблицу зоны необходимо добавить SOA запись, иначе
сервер не сможет отвечать на запрос SOA: INSERT INTO "test-zone.info" (host, type, data) values
('@', 'SOA', 'ns.test-zone.info. admin.test-zone.info. 1161450241 10800 3600
604800 38400')
Обратите внимание, что все поля SOA записи находятся в одном поле
таблицы зоны и разделены пробелами.
Резюме
Ниже представлена таблица сравнения производительности связки BIND+PostgreSQL в
различных вариантах конфигурации.
Сравнение производительности различных конфигураций
Вариант конфигурации
Прямая зона
Обратная
NXDomains
Без оптимизации
8.185876
5.483046
6.129748
Авторская оптимизация
23.797786
8.382870
10.946829
Дополнительная оптимизация
135.712728
124.814569
82.782502
Максимальная оптимизация
182.477509
223.516061
94.867920
Я думаю что выводы о производительности различных конфигураций BIND'а и
PostgreSQL сделать достаточно просто.
Если у Вас возникли какие-либо замечания/предложения/пожелания по поводу данной
статьи, то - предлагаю их сообщить по , либо высказать в гостевой.