Интеграция и оптимизация BIND+PostgreSQL

Мотивация

Возможно у Вас уже возникало когда-либо желание внести все данные 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.

$ psql
Welcome to psql 8.0.15, the PostgreSQL interactive terminal.

Type:  \copyright for distribution terms
       \h for help with SQL commands
       \? for help with psql commands
       \g or terminate with semicolon to execute query
       \q to quit
postgres=# CREATE USER named PASSWORD '123456';
postgres=# CREATE DATABASE named WITH OWNER = named;
CREATE DATABASE

Создаём таблицы и индексы:

postgres=# \c named
You are now connected to database "named".
named=# CREATE TABLE dns_records (
    zone text,
    host text,
    ttl int8,
    type text,
    mx_priority text,
    data text,
    resp_person text,
    serial int8,
    refresh int8,
    retry int8,
    expire int8,
    minimum int8
)WITH OIDS;
CREATE TABLE
named=# CREATE INDEX host_index ON dns_records USING btree (host);
CREATE INDEX
named=# CREATE INDEX type_index ON dns_records USING btree ("type");
CREATE INDEX
named=# CREATE TABLE xfr_table
(
    zone text,
    client text
)
WITH OIDS;
CREATE TABLE

Внимание! Две следующие команды были пропущены в оригинальной статье, следствием этого являлось падение BIND при попытке использовать dlz-драйвер.

named=# ALTER TABLE dns_records OWNER TO named;
ALTER TABLE
named=# ALTER TABLE xfr_table OWNER TO named;
ALTER TABLE

Проблема возникала т.к. все манипуляции с БД named мы производили от имени пользователя postgres, поэтому при попытке обратиться к БД от любого другого пользователя (от того же named) происходила ошибка - "отказано в доступе" потому что владельцем таблиц в базе named был пользователь postgres. К сожалению в dlz-драйвере на момент написания статьи присутствовал баг, в следствии которого происходило двойное освобождение памяти со всеми вытекающими последствиями. Исправление этой проблемы описано ниже.
Добавим основные записи прямой и обратной зоны для тестового домена test-zone.info:

named=# INSERT INTO dns_records(zone, host, ttl,type, data, 
                            resp_person, serial, refresh, retry,expire, minimum)
            VALUES ('test-zone.info', '@',38400, 'SOA', 'ns1.test-zone.info.',
            'admin.test-zone.info.', 1161450241,10800,3600,604800,38400);
named=# INSERT INTO dns_records(zone, host, type, data) 
            VALUES ('test-zone.info', '@','NS','ns1.test-zone.info.');
named=# INSERT INTO dns_records(zone, host, type, data) 
            VALUES ('test-zone.info', '@','NS','ns2.test-zone.info.');
named=# INSERT INTO dns_records(zone, host, type, data) 
            VALUES ('test-zone.info', 'www','CNAME','test-zone.info.');
named=# INSERT INTO dns_records(zone, host, type, mx_priority, data) 
            VALUES ('test-zone.info', 'test-zone.info','MX',50,'mx.test-zone.info.');
named=# INSERT INTO dns_records(zone, host, ttl,type, data, resp_person, 
		serial, refresh, retry,expire, minimum)
            VALUES ('168.192.in-addr.arpa', '@',38400, 'SOA', 'ns1.test-zone.info.',
		'admin.test-zone.info.', 1161450241,10800,3600,604800,38400);
named=# INSERT INTO dns_records(zone, host, ttl,type, data,	resp_person, 
		serial, refresh, retry,expire, minimum)
            VALUES ('10.in-addr.arpa', '@',38400, 'SOA', 'ns1.test-zone.info.',
            'admin.test-zone.info.', 1161450241,10800,3600,604800,38400);
named=# INSERT INTO xfr_table VALUES ('test-zone.info', '127.0.0.1');
named=# INSERT INTO xfr_table VALUES ('10.in-addr.arpa', '127.0.0.1');
named=# INSERT INTO xfr_table VALUES ('168.192.in-addr.arpa', '127.0.0.1');

Нам необходимо качественно протестировать получившуюся конфугурацию - желательно добавить в таблицу всё содержимое реальной зоны (в примере реальный домен 2го уровня заменён на test-zone.info). Доменная зона содержит свыше 22000 хостов - добавлять её в таблицу вручную по меньшей мере неразумно. Для решения этой задачи я использовал простой perl-скрипт. Общий вид запроса для добавления хостов в прямую зону:

INSERT INTO dns_records (zone, host, type, data) 
    VALUES ('test-zone.info', 'hostname', 'A', 'IP.адрес.добавляемого.хоста');
Общий вид запросов для добавления хоста в обратную зону:
INSERT INTO dns_records (zone, host, type, data) 
    VALUES ('10.in-addr.arpa', 'три.октета.адреса', 'PTR', 'hostname.test-zone.info.');
INSERT INTO dns_records (zone, host, type, data) 
    VALUES ('168.192.in-addr.arpa', 'два-октета.адреса', 'PTR', 'hostname.test-zone.info.');

Скрипт - для заполнения обратной зоны.

Настройка BIND

Конфигурация BIND не претерпивает каких-либо серьёзных изменений - если у Вас уже имеется конфигурационный файл (named.conf), то достаточно добавить в него настройки для dlz драйвера, а всё остальное оставить как есть.

dlz "postgres zone" {
    database "postgres 2
    {host=localhost port=5432 dbname=named user=named}
    {select zone from dns_records where zone = '%zone%'}
    {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')}
    {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')}
    {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%'}"
}

Краткое пояснение к данной конфигурации 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 она имеется в портах.
Файл для тестирования прямой зоны сгенерирован командой:

$ grep -v '^;' zone.file|grep '\WA\W'| awk '{print $1".test-zone.info A"}' > query-test.txt

Файл для тестирования несуществующих в зоне доменов создан при помощи команды:

grep -v '^;' zone.file|grep '\WA\W' |awk '{print $1".nxd.test-zone.info. A" }'

Файл для тестирования обратной зоны сгенерирован perl-скриптом.
Конфигурация компьютера на котором производились испытания: Intel(R) Celeron(R) CPU 2.40GHz, 256 Mb RAM. Среднее значение загрузки (выводимое командой uptime) до начала тестирования меньше 0.05. Результаты тестирования прямой зоны:

$ dnsperf -s localhost -d query-test.txt
DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Status] Testing complete

Statistics:

  Parse input file:     once
  Ended due to:         reaching end of file

  Queries sent:         22460 queries
  Queries completed:    22460 queries
  Queries lost:         0 queries

  Avg request size:     39 bytes
  Avg response size:    91 bytes

  Percentage completed: 100.00%
  Percentage lost:        0.00%

  Started at:           Tue Apr 22 17:20:01 2008
  Finished at:          Tue Apr 22 18:05:45 2008
  Ran for:              2743.750182 seconds

  Queries per second:   8.185876 qps

Результаты тестирования обратной зоны:

$ dnsperf -d rev-query.txt -s localhost -t 10 -l 60

DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Timeout] Query timed out: msg id 820
Warning: Received a response with an unexpected (maybe timed out) id: 820
[Timeout] Query timed out: msg id 3089
Warning: Received a response with an unexpected (maybe timed out) id: 3089
[Timeout] Query timed out: msg id 3564
Warning: Received a response with an unexpected (maybe timed out) id: 3564
[Timeout] Query timed out: msg id 3565
Warning: Received a response with an unexpected (maybe timed out) id: 3565
[Timeout] Query timed out: msg id 3566
Warning: Received a response with an unexpected (maybe timed out) id: 3566
[Timeout] Query timed out: msg id 3567
Warning: Received a response with an unexpected (maybe timed out) id: 3567
[Timeout] Query timed out: msg id 3568
Warning: Received a response with an unexpected (maybe timed out) id: 3568
/* несколько десятков строчек с сообщениями об ошибках прпущено */
[Status] Testing complete

Statistics:

  Parse input file:     once
  Ended due to:         reaching end of file

  Queries sent:         22443 queries
  Queries completed:    22383 queries
  Queries lost:         60 queries

  Avg request size:     41 bytes
  Avg response size:    77 bytes

  Percentage completed:  99.73%
  Percentage lost:        0.27%

  Started at:           Tue Apr 22 18:05:45 2008
  Finished at:          Tue Apr 22 19:13:47 2008
  Ran for:              4082.220269 seconds

  Queries per second:   5.483046 qps

Несуществующие в нашей зоне домены (NXDomain's):

$ dnsperf -s localhost -d query-nxd-test.txt
DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Timeout] Query timed out: msg id 21319
[Timeout] Query timed out: msg id 21320
[Timeout] Query timed out: msg id 21321
Warning: Received a response with an unexpected (maybe timed out) id: 21319
[Timeout] Query timed out: msg id 21322
Warning: Received a response with an unexpected (maybe timed out) id: 21320
[Timeout] Query timed out: msg id 21323
Warning: Received a response with an unexpected (maybe timed out) id: 21321
Warning: Received a response with an unexpected (maybe timed out) id: 21322
Warning: Received a response with an unexpected (maybe timed out) id: 21323
[Timeout] Query timed out: msg id 21324
Warning: Received a response with an unexpected (maybe timed out) id: 21324
[Timeout] Query timed out: msg id 21325
Warning: Received a response with an unexpected (maybe timed out) id: 21325
[Timeout] Query timed out: msg id 21326
Warning: Received a response with an unexpected (maybe timed out) id: 21326
[Timeout] Query timed out: msg id 21327
Warning: Received a response with an unexpected (maybe timed out) id: 21327
[Timeout] Query timed out: msg id 21328
Warning: Received a response with an unexpected (maybe timed out) id: 21328
[Status] Testing complete
  Statistics:

  Parse input file:     once
  Ended due to:         reaching end of file

  Queries sent:         22526 queries
  Queries completed:    22516 queries
  Queries lost:         10 queries

  Avg request size:     43 bytes
  Avg response size:    89 bytes

  Percentage completed:  99.96%
  Percentage lost:        0.04%

  Started at:           Tue Apr 22 16:18:48 2008
  Finished at:          Tue Apr 22 17:20:01 2008
  Ran for:              3673.234087 seconds

  Queries per second:   6.129748 qps

Честно говоря, когда я увидел эти результаты, то был мягко говоря расстроен - подобная производительность совершенно неприменима на практике. Число запросов к зоне локальной сети в моём случае доходит примерно до 30-40 в секунду в пиковые часы. Конечно реальный DNS сервер значительно мощнее тестовой машины, но подобное время отклика не сможет компенсироваться даже высокими процессорными мощностями.


								

Авторская оптимизация

Для начала, попробуем воспользоваться оптимизацией предложенной авторами DLZ проекта: http://bind-dlz.sourceforge.net/postgresql_perf.html. В нашем случае конфигурационная секция dlz будет выглядеть так:

dlz "postgres zone" {
       database "postgres 2
      {host=/tmp dbname=named user=named}
      {select zone from dns_records where zone = '%zone%' limit 1}
      {select ttl, type, mx_priority, data, resp_person, serial, refresh, retry,
 expire, minimum from dns_records where zone ='%zone%' and host = '%record%'}";
};

Некоторые пояснения к данной конфигурации - сделан отказ от TCP/IP сокетов в пользу каналов (pipes) для связи BIND с PostgreSQL; ограничение вывода информации запросом - при помощи limit 1; общее упрощение запроса на получение ресурса посредством отказа от конструкции case в запросе, и отказ от сравнения типа ресурса. Так же авторами предложено поэксперементировать с параметром "shared_buffers" в конфигурационном файле postgresql.conf, но честно говоря - данные эксперементы лично мне не принесли ни каких ощутимых результатов. Подробнее назначение SQL-запросов рассмотрено ниже.

Тестирование. Часть 2.

Проведём тестирование полученной конфигурации.

Прямая зона:

DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Status] Testing complete

Statistics:

  Parse input file:     multiple times
  Run time limit:       60 seconds
  Ran through file:     0 times

  Queries sent:         1447 queries
  Queries completed:    1447 queries
  Queries lost:         0 queries

  Avg request size:     39 bytes
  Avg response size:    91 bytes

  Percentage completed: 100.00%
  Percentage lost:        0.00%

  Started at:           Tue Apr 22 01:54:30 2008
  Finished at:          Tue Apr 22 01:55:31 2008
  Ran for:              60.803976 seconds

  Queries per second:   23.797786 qps							
							

Обратная зона:

$ dnsperf -s localhost -d rev-query.txt
DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Status] Testing complete

Statistics:

  Parse input file:     multiple times
  Run time limit:       60 seconds
  Ran through file:     0 times

  Queries sent:         522 queries
  Queries completed:    522 queries
  Queries lost:         0 queries

  Avg request size:     40 bytes
  Avg response size:    75 bytes

  Percentage completed: 100.00%
  Percentage lost:        0.00%

  Started at:           Tue Apr 22 01:55:31 2008
  Finished at:          Tue Apr 22 01:56:33 2008
  Ran for:              62.269844 seconds

  Queries per second:   8.382870 qps

Не существующие в нашей зоне домены (NXDomain's):

$ dnsperf -s localhost -d query-nxd-test.txt
DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Status] Testing complete

Statistics:

  Parse input file:     multiple times
  Run time limit:       60 seconds
  Ran through file:     0 times

  Queries sent:         674 queries
  Queries completed:    674 queries
  Queries lost:         0 queries

  Avg request size:     43 bytes
  Avg response size:    89 bytes

  Percentage completed: 100.00%
  Percentage lost:        0.00%

  Started at:           Tue Apr 22 01:53:28 2008
  Finished at:          Tue Apr 22 01:54:30 2008
  Ran for:              61.570339 seconds

  Queries per second:   10.946829 qps

Данные результаты ощутимо лучше результатов полученных при конфигурации по умолчанию, но данная настройка имеет недостаток в виде остуствия возможности переноса зоны.
Стоит отдельно оговориться по поводу данных о производительности, предоставленных на сайте разработчиков. Согласно им, авторам удалось добиться производительности 759 запросов в секунду при "database with nearly 2.7 million records". Я допускаю что совершил какие-то ошибки, причиной которых стало столь жуткое снижение производительности, которое скорее всего будет не реально компенсировать аппаратными средствами (например использовав конфигурацию компьютера как в примере на сайте разработчиков). Если это действительно так, и Вы найдёт эти ошибки, то буду рад узнать о них. На данный момент у меня сложилось впечатление что авторы тестировали работу сервера на зоне вида host1, host2,.... host27000000, либо на большом числе зон с малым числом хостов в них, что возможно могло улучшить результаты опроса.
В моём случае используется примерно 22000 доменных имён из реальной зоны, и соответственно примерно столько же для обратной. Но производительность и в данном случае оказалась по прежнему не применима в реальных условиях.

Дополнительная оптимизация

Рассмотрим по пунктам, что можно предпринять для исправления ситуации, и какие упущения были допущены разработчиками.

Оптимизация работы с PostgreSQL

Для наглядности ещё раз приведём конфигурацию запросов 1 :

dlz "postgres zone" {
    database "postgres 2
    {host=localhost port=5432 dbname=named user=named}
    {select zone from dns_records where zone = '%zone%'}
    {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')}
    {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')}
    {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%'}"
}

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
    Если БД обслуживает данную зону, то выводится только одна запись что, существенно сокращает время обработки запроса. В моём случае это дало почти трёхкратный прирост производительности.
  2. 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-запросе.
    Большей оптимизации выполнения этого запроса без изменения структуры БД, пожалуй добиться нельзя. Во всяком случае у меня это не получилось.
  3. 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-сервером.
  4. select ttl, type, host, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum from dns_records where zone = '%zone%'
    Назначение: выполняется для получения всех записей зоны - необходимо для переноса зоны.
  5. select zone from xfr_table where zone = '%zone%' and client = '%client%'
    Назначение: выполняется перед переносом зоны, для аутентификации клиента. IP адрес клиента находится в переменной %client%. Если запрос возвращает соответствие запрашиваемой зоны IP адресу клиента, то клиенту разрешается осуществить перенос зоны.

Кроме этого, было произведено ещё несколько не описанных выше экспериментов и опытов над конфигурацией named и БД (перестройка индексов). Затем я отчётливо осознал что без коренного изменения структуры БД лучшей производительности добиться практически не реально. И так, приступим!

  1. Создание новых таблиц и индексов.
    Основная задержка в работе 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 с относительно небольшим количеством зон и большим числом записей (свыше нескольких тысяч) в каждой зоне. Думаю что в случае большого числа зон содержащих мало записей подобная конфигурация себя не оправдает. Ниже представлен пример команд для создания новых таблиц:
    named=# CREATE TABLE xfr_table
    (
      "zone" text,
      client text
    )WITH OIDS;
    CREATE TABLE 
    named=# ALTER TABLE xfr_table OWNER TO named;
    ALTER TABLE 
    -- Создаём таблицу содержащую записи о обслуживаемых зонах
    named=# CREATE TABLE soa_table
    (
      "zone" text,
      host text,
      ttl bigint,
      "type" text,
      mx_priority text,
      data text,
      resp_person text,
      serial bigint,
      refresh bigint,
      retry bigint,
      expire bigint,
      minimum bigint
    )WITH OIDS;
    CREATE TABLE
    named=# ALTER TABLE soa_table OWNER TO named;
    ALTER TABLE
    -- И необходимые для поиска индексы
    named=# CREATE INDEX soa_zone_index ON soa_table USING btree ("zone");
    CREATE INDEX
    named=# CREATE INDEX soa_zone_type_host ON soa_table USING btree ("zone", "type", host);
    CREATE INDEX
    named=# CREATE INDEX soa_zone_type_index ON soa_table USING btree ("zone", "type");
    CREATE INDEX
    -- Создаём отдельную таблицу для прямой зоны
    named=# CREATE TABLE "test-zone.info"
    (
      host text,
      "type" text,
      mx_priority text,
      data text
    )WITH OIDS;
    CREATE TABLE
    named=# ALTER TABLE "test-zone.info" OWNER TO named;
    ALTER TABLE
    -- Создаём индекс для поиска по имени хоста
    named=# CREATE INDEX host_test_zone_index ON "test-zone.info" USING btree (host);
    CREATE INDEX
    -- Создаём таблицу для обратной зоны подсети 192.168.0.0/16
    named=# CREATE TABLE "168.192.in-addr.arpa"
    (
      host text,
      "type" text,
      mx_priority text,
      data text
    )WITH OIDS;
    CREATE TABLE
    named=# ALTER TABLE "168.192.in-addr.arpa" OWNER TO named;
    ALTER TABLE
    named=# CREATE INDEX host_192_168_index ON "168.192.in-addr.arpa" USING btree (host);
    CREATE INDEX
    named=# ALTER TABLE "168.192.in-addr.arpa" CLUSTER ON host_192_168_index;
    ALTER TABLE
    -- Создаём таблицу для обратной зоны подсети 10.0.0.0/8
    named=# CREATE TABLE "10.in-addr.arpa"
    (
      host text,
      "type" text,
      mx_priority text,
      data text
    )WITH OIDS;
    CREATE TABLE
    named=# ALTER TABLE "10.in-addr.arpa" OWNER TO named;
    ALTER TABLE
    named=# CREATE INDEX host_10_index ON "10.in-addr.arpa" USING btree (host);
    named=# ALTER TABLE "10.in-addr.arpa" CLUSTER ON host_10_index;
    ALTER TABLE
    								
  2. Заполнение БД данными.
    Для начала - внесём в новую таблицу soa_table данные об обслуживаемых зонах:
    named=# INSERT INTO soa_table(zone, host, ttl,type, data,
    		resp_person, serial, refresh, retry,expire, minimum)
    		VALUES ('test-zone.info', '@',38400, 'SOA', 'ns1.test-zone.info.',
    		'admin.test-zone.info.', 1161450241,10800,3600,604800,38400);
    INSERT 591552 1
    named=# INSERT INTO soa_table(zone, host, type, data)
    		VALUES ('test-zone.info', '@','NS','ns1.test-zone.info.');
    INSERT 591553 1
    named=# INSERT INTO soa_table(zone, host, type, data)
    		VALUES ('test-zone.info', '@','NS','ns2.test-zone.info.');
    INSERT 591554 1
    named=# INSERT INTO soa_table(zone, host, ttl,type, data,
    		resp_person, serial, refresh, retry,expire, minimum)
    		VALUES ('168.192.in-addr.arpa', '@',38400, 'SOA', 'ns1.test-zone.info.',
    		'admin.test-zone.info.', 1161450241,10800,3600,604800,38400);
    INSERT 591556 1
    named=# INSERT INTO soa_table(zone, host, ttl,type, data,
    		resp_person, serial, refresh, retry,expire, minimum)
    		VALUES ('10.in-addr.arpa', '@',38400, 'SOA', 'ns1.test-zone.info.',
    		'admin.test-zone.info.', 1161450241,10800,3600,604800,38400);
    INSERT 591557 1
    named=# INSERT INTO "test-zone.info" (host,type,mx_priority,data) values('@', 'MX', 10, 'mx');
    INSERT 591558 1
    named=# INSERT INTO "test-zone.info" (host,type,mx_priority,data) values('@', 'MX', 20, 'mx2');
    INSERT 591559 1
    Затем, при помощи perl-скриптов заполним таблицы прямой и обратной зоны:
    Скрипт для прямой зоны.
    Скрипт для обратной зоны.
    Можно сказать, что все вышеописанные операции по созданию и заполнению данными новой зоны привидены с целью наглядности. При помощи этого скрипта Вы можете выполнить их с минимальными трудозатратами.
  3. Новая конфигурационная секция dlz в файле named.conf
dlz "postgres zone" {
           database "postgres 2
       {host=localhost port=5432 dbname=named user=named}
       {select zone from soa_table where zone = '%zone%' limit 1}
       {select NULL, type, mx_priority, data from \"%zone%\" where host = '%record%';}
       {select ttl, type, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum
               from soa_table where zone = '%zone%'}
       {select ttl, type, host, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum
           from soa_table where zone = '%zone%'
       union
           select NULL, type, host, mx_priority, data, NULL,        NULL,   NULL,    NULL,  NULL,   NULL
           from \"%zone%\"}
       {select zone from xfr_table where zone = '%zone%' and client = '%client%'}";
    };

Теперь при обработке запроса выборка данных производится не из общей таблицы (dns_records), а из двух таблиц - таблицы soa_table и таблицы имя которой равно имени зоны %zone%. Рассмотрим изменения произошедшие в конфигруационном файле:
  1. Проверка наличия запрашиваемой зоны в БД производится выборкой из таблицы soa_table.
  2. Запрос получения ресурса упрощён до одного условия - сравнения по имени хоста. Выбор зоны производится автоматически благодаря получению имени таблицы из переменной %zone%. Псевдозначение NULL в начале секции цель необходимо для соблюдения формата итогового набора данных - несуществующее в таблице поле ttl должно идти первым, теперь оно заменяется на псевдозначение NULL.
  3. В запросе на получение SOA изменено только имя таблицы. Теперь SOA данные хранятся в soa_table.
  4. Запрос на перенос зоны несколько усложнён. Полная информация о зоне теперь разнесена в две таблицы, поэтому и выборка производится из 2х таблиц. Это осуществленно при помощи SQL оператора UNION объединяющего результат выполенения двух SELECT. Псевдозначение NULL в данном запросе играет такую же роль, как и в запросе на получение ресурса.
  5. Запрос выполняющий аутентификацию клиента для переноса зоны остаётся прежним.

Оптимизация BIND

Как было сказано в начале статьи - полученная связка BIND + PostgreSQL с настройками по умолчанию не отличается ни быстродействием, ни надёжностью. DLZ драйвер для BIND содержит в своём коде два недочёта сильно ухудшающих эти характеристики.

  1. Двойное освобождение памяти - приводит к падению процесса 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.
  2. Авторы забыли отключить логгирование введённое на этапе разработки - в следствии этого 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

Для тестирования производительности воспользуемся файлами, которые были использованы при первичном тестировании. Тестирование прямой зоны:

$ dnsperf -s localhost -d query-test.txt
DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Status] Testing complete

Statistics:

  Parse input file:     once
  Ended due to:         reaching end of file

  Queries sent:         22460 queries
  Queries completed:    22460 queries
  Queries lost:         0 queries

  Avg request size:     39 bytes
  Avg response size:    91 bytes

  Percentage completed: 100.00%
  Percentage lost:        0.00%

  Started at:           Wed Apr 23 19:49:33 2008
  Finished at:          Wed Apr 23 19:52:18 2008
  Ran for:              165.496636 seconds

  Queries per second:   135.712728 qps

Тестирование обратной зоны:

$ dnsperf -s localhost -d rev-query.txt
DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Status] Testing complete

Statistics:

  Parse input file:     once
  Ended due to:         reaching end of file

  Queries sent:         22443 queries
  Queries completed:    22443 queries
  Queries lost:         0 queries

  Avg request size:     41 bytes
  Avg response size:    77 bytes

  Percentage completed: 100.00%
  Percentage lost:        0.00%

  Started at:           Wed Apr 23 19:52:18 2008
  Finished at:          Wed Apr 23 19:55:18 2008
  Ran for:              179.810740 seconds

  Queries per second:   124.814569 qps

Тестирование на NXDomain's:

DNS Performance Testing Tool

Nominum Version 1.0.0.1

[Status] Processing input data
[Status] Sending queries (to 127.0.0.1)
[Status] Testing complete

Statistics:

  Parse input file:     once
  Ended due to:         reaching end of file

  Queries sent:         22526 queries
  Queries completed:    22526 queries
  Queries lost:         0 queries

  Avg request size:     43 bytes
  Avg response size:    89 bytes

  Percentage completed: 100.00%
  Percentage lost:        0.00%

  Started at:           Wed Apr 23 19:55:18 2008
  Finished at:          Wed Apr 23 19:59:50 2008
  Ran for:              272.110646 seconds

  Queries per second:   82.782502 qps

Как можно удостовериться - производительность увеличилась более чем в 10 раз по сравнению с вариантом предложенным по умолчанию. Но если окажется что Вам этого мало, то предлагаю выполнить действия предложенные в следующей части статьи.

Максимальная оптимизация

Для достижения наивысших результатов в области обработки зарпосов от клиентов, воспользуемся методикой предложенной авторами проекта DLZ. Т.е. откажемся от возможности переноса зоны с нашего сервера на slave-сервер, в связи с чем значительно упростится конфигурация, и соответственно - уменьшится число выполняемых запросов и время обработки отдельного запроса:

dlz "postgres zone" {
	database "postgres 2
	{host=/tmp port=5432 dbname=named user=named}
	{select zone from soa_table where zone = '%zone%' limit 1}
	{select NULL, type, mx_priority, data from \"%zone%\" where host = '%record%'}";
};

Кроме этого в таблицу зоны необходимо добавить 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 сделать достаточно просто.

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




Вернуться в "Проекты"