Pull to refresh

Как вынудить процесс использовать новый адрес DNS-сервера из обновлённого resolv.conf без перезапуска самого процесса

System Programming *
Я работаю системным администратором Unix. Однажды к нам в отдел эксплуатации сервисов упал тикет от программиста с выдержой из лога application-сервера в заголовке: "pgbouncer cannot connect to server". Посмотрев логи pgbouncer'ов, я увидел, что периодически возникают lookup fail'ы при обращении к нашим DNS. Было установленно, что это связано не с работой наших DNS-серверов, а с ненадёжностью самого протокола UDP: иногда возникают потери пакетов по разным причинам.
image
В результате, было решено установить на каждом сервере с 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 начал использоваться немедленно, нам нужно проделать следующие шаги:

  1. Поменять в forwarders BIND'а серверы на новые (другие) рабочие DNS'серверы, которые до этого не использовались в resolv.conf и не будут использоваться, а затем сделать rndc reload
  2. Забанить локальным фаерволом обращения к старым DNS-серверам (кроме 127.0.0.1)
  3. Инициировать обращение pgbouncer'a к несуществующему серверу БД:
    su -m pgbouncer -c 'export PGPASSWORD="123" && /usr/local/bin/psql -Utest test -h127.0.0.1 -p6000';
    

  4. Убедиться с помощью tcpdump, что pgbouncer обращается к 127.0.0.1 по 53-му порту:
    tcpdump -n -i lo0 port 53 | grep xaxa
    
    "> 127.0.0.1.53" -
    

    Где xaxa — часть имени сервера из pgbouncer.conf
  5. Разбанить старые DNS в фаерволе
  6. Вернуть настройки forwarders BIND'а в первоначальное состояние

И последнее

Если вы захотите повторить мой опыт, настоятельно рекомендую тренироваться на тестовом стенде.
Если вы захотите «пулять» команду в gdb в batch mode, имейте в виду, что gdb нужно сначала дать время на чтение символов, а потом уже следует вызывать функции: я как-то из-за этого здорово напортачил, убив один из 8-ми работающих pgbouncer'ов.
batch mode для gdb у меня выполняется теперь так:
printf 'shell sleep 3\ncall res_init()\ndetach\nquit\n' > /tmp/pb.gdb && gdb -batch -x /tmp/pb.gdb /usr/local/bin/pgbouncer `cat /var/run/pgbouncer/test.pid`


Надеюсь, мой опыт кому-то поможет чуть лучше понять как работают процессы в операционных системах.
Tags: pgbouncerpostgresqlсиcgdbktraceчёрная магия
Hubs: System Programming
Total votes 38: ↑36 and ↓2 +34
Comments 18
Comments Comments 18

Popular right now