Я работаю системным администратором Unix. Однажды к нам в отдел эксплуатации сервисов упал тикет от программиста с выдержой из лога application-сервера в заголовке: "pgbouncer cannot connect to server". Посмотрев логи pgbouncer'ов, я увидел, что периодически возникают lookup fail'ы при обращении к нашим DNS. Было установленно, что это связано не с работой наших DNS-серверов, а с ненадёжностью самого протокола UDP: иногда возникают потери пакетов по разным причинам.
В результате, было решено установить на каждом сервере с pgbouncer'ами по кэширующему BIND. И тут возникла интересная проблема: pgbouncer не перечитывал по сигналу HUP файл /etc/resolv.conf и продолжал обращаться к старым DNS-серверам. А перезагружать баунсеры категорически нельзя: есть проблемные проекты, которые очень болезненно относятся к разрывом сессий с базой.
В данной статье я расскажу как можно pgbouncer или любую другую программу, использующую библиотечный вызов getaddrinfo(), заставить перечитать resolv.conf и начать использовать новый DNS-сервер совершенно безболезненно для клиентов (без даунтайма).
Приступим
Сразу оговорюсь, что в моём случае pgbouncer'ы были версий 1.5.2 и собраны с libevent-1.4 под FreeBSD.
Если посмотреть в исходник pgbouncer'а, то можно увидеть в файле dnslookup.c следующий комментарий:
/*
* Available backends:
*
* udns - libudns
* getaddrinfo_a - glibc only
* libevent1 - returns TTL, ignores hosts file.
* libevent2 - does not return TTL, uses hosts file.
*/
Это означает, что в случае когда pgbouncer собран с libevent1, для асинхронного резолва адресов используется функция getaddrinfo_a() из стандартной библиотеки libc.
Опытным путём было установлено, что асинхронная getaddrinfo_a() использует обычную функцию getaddrinfo() из libc. На последнюю функцию мы и будем ставить точку останова. Этот факт избавит нас от необходимости собирать pgbouncer с отладочными символами, так как gdb знает функцию getaddrinfo, не смотря на то, что libc собрана без отладочных символов.
Добавим в конфиг pgbouncer'а несуществующую базу, ссылающуюся на несуществующий домен (пригодится для тестов):
test = host=test.xaxa.blabla12313212.su user=pgsql dbname=template1 pool_size=10
В отдельном окне запустим pgbouncer:
su -m pgbouncer -c '/usr/local/bin/pgbouncer /usr/local/etc/pgbouncer.ini'
В другом окне подключимся к процессу с помощью отладчика gdb:
gdb /usr/local/bin/pgbouncer `cat /var/run/pgbouncer/pgbouncer.pid`
Поставим точку останова и позволим процессу выполняться дальше:
(gdb) b getaddrinfo
Breakpoint 1 at 0x800f862a4
(gdb) c
Continuing.
В другом окне попробуем подключиться к нашей базе с несуществующим доменом, чтобы инициировать попытку резолва:
su -m pgbouncer -c 'export PGPASSWORD="123" && /usr/local/bin/psql -Utest test -h10.9.9.16 -p6000';
В gdb мы видим, что мы попали в яблочко:
Breakpoint 1, 0x0000000800f862a4 in getaddrinfo () from /lib/libc.so.7
(gdb)
Как же работает getaddrinfo()?
С помощью мануалов и поисковика было выяснено, что эта функция при первом вызове читает файл resolv.conf, инициализирует в памяти структуру с кучей данных, среди которых можно найти и список DNS-серверов. Далее, функция пытается сделать резолв адреса при помощи первого адреса из списка. Если DNS-сервер не отвечает, функция делает активным следующий DNS-сервер из списка. И так по кругу. Функция читает resolv.conf только единожды.
Сначала я хотел пропатчить виртуальную память pgbouncer'а, найдя 4 байта адреса DNS-сервера в network order или host order формате. Для этого даже была написана программа «дампер памяти» на Си, которая позволяла дампить память процесса и искать определённый порядок байт. Но, как оказалось, в таком виде эти адреса в памяти найти невозможно. Понять же исходник getaddinfo() оказалось выше моих сил: очень много текста и всяческие goto чуть не сломали моё сознание. К тому же, я не являюсь программистом, а Си начал изучать всего месяц назад.
Кстати, моя программа, использующая ptrace и procfs подошла бы для pgbouncer'а собранного с libevent2: там ip-адреса DNS-серверов хранятся как раз в виде четырёх байт. Но описание данного опыта выходит за рамки статьи.
Что же делать?
К счастью, при помощи поисковика я нашёл в стандартной библиотеке спасительную функцию res_init():
The res_init() routine reads the configuration file (if any; see
resolver(5)) to get the default domain name, search list and the Internet
address of the local name server(s)
Именно эта функция вызывается при первом вызове getaddrinfo() и инициализирует нужную нам структуру!
Повторный же вызов функции переинициализирует структуру и перечитает resolv.conf.
Проверим на практике
Подключимся трассировщиком к нашему «замороженному» pgbouncer'у и начнём grep'ать файл дампа трассировки:
ktrace -f out.ktrace -p `cat /var/run/pgbouncer/pgbouncer.pid`
kdump -l -f out.ktrace | grep resolv
В окне с gdb осуществим вызов функции res_init():
(gdb) call res_init()
Breakpoint 1, 0x0000000800f862a4 in getaddrinfo () from /lib/libc.so.7
В окне с выводом результата трассировки мы видим:
37933 pgbouncer NAMI "/etc/resolv.conf"
Цель достигнута
Нам удалось заставить процесс перечитать resolv.conf, при этом не уронив сервер и не разорвав активные state'ы tcp. В момент заморозки запросы также не теряются.
Если мы захотим чтобы локальный кэширующий DNS начал использоваться немедленно, нам нужно проделать следующие шаги:
- Поменять в forwarders BIND'а серверы на новые (другие) рабочие DNS'серверы, которые до этого не использовались в resolv.conf и не будут использоваться, а затем сделать rndc reload
- Забанить локальным фаерволом обращения к старым DNS-серверам (кроме 127.0.0.1)
- Инициировать обращение pgbouncer'a к несуществующему серверу БД:
su -m pgbouncer -c 'export PGPASSWORD="123" && /usr/local/bin/psql -Utest test -h127.0.0.1 -p6000';
- Убедиться с помощью tcpdump, что pgbouncer обращается к 127.0.0.1 по 53-му порту:
tcpdump -n -i lo0 port 53 | grep xaxa "> 127.0.0.1.53" -
Где xaxa — часть имени сервера из pgbouncer.conf
- Разбанить старые DNS в фаерволе
- Вернуть настройки forwarders BIND'а в первоначальное состояние
И последнее
Если вы захотите повторить мой опыт, настоятельно рекомендую тренироваться на тестовом стенде.
Если вы захотите «пулять» команду в gdb в batch mode, имейте в виду, что gdb нужно сначала дать время на чтение символов, а потом уже следует вызывать функции: я как-то из-за этого здорово напортачил, убив один из 8-ми работающих pgbouncer'ов.
batch mode для gdb у меня выполняется теперь так:
printf 'shell sleep 3ncall res_init()ndetachnquitn' > /tmp/pb.gdb && gdb -batch -x /tmp/pb.gdb /usr/local/bin/pgbouncer `cat /var/run/pgbouncer/test.pid`
Надеюсь, мой опыт кому-то поможет чуть лучше понять как работают процессы в операционных системах.
Автор: Nastradamus