Приветствую.
Продолжаем серию постов о работе PostgreSQL на уровне кода. Сегодня, познакомимся с главным циклом сервера. Цикл расположен в src/backend/postmaster/postmaster.c
Инициализация
В начале цикла инициализируем переменные времени для проверок локфайлов текущим временем:
last_lockfile_recheck_time
- время последней проверки postmaster.pid;last_touch_time
- время последней проверки сокет-файлов.
Далее инициализируем прослушиваемые порты.
Для прослушивания используется мультиплексирование, используя select()
. Инициализируем множество прослушиваемых портов и одновременно находим значение числа наибольшего дескриптора.
select()
Функция select() позволяет нам прослушивать сразу несколько дескрипторов на предмет изменения: чтение, запись, исключительные ситуации.
Сигнатура функции:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *utimeout);
nfds - значение наибольшего дескриптора + 1 (так надо)
utimeout - максимальный таймаут ожидания.
Также есть 3 параметра типа fd_set - набор дескрипторов. При изменении в любом из них функция возвращается:
readfds - появились данные для чтения
writefds - появилось место для записи
exceptfds - произошла исключительная ситуация
Для работы с fd_set используются макросы:
FD_ZERO() - инициализация;
FD_CLR() - убрать дескриптор из набора;
FD_SET() - добавить дескриптор в набор;
FD_ISSET() - есть ли дескриптор в наборе.
После работы переданные наборы изменяются. Поэтому при работе в цикле, мы постоянно производим копирование.
Например, вот как инициализируем маску дескрипторов для чтения
// nSockets - число наибольшего дескриптора + 1
nSockets = initMasks(&readmask);
for (;;)
{
// Копируем перед использованием
memcpy((char *) &rmask, (char *) &readmask, sizeof(fd_set));
// Рабочий код
}
Теперь можем войти в бесконечный цикл для обслуживания клиентов.
* Цикл создан через for(;;)
, а не while (1)
.
Бесконечный цикл
Каждую итерацию цикла можно разбить на несколько логических секций.
Начальная часть определяется текущим состоянием:
PM_WAIT_DEAD_END - Спим
Остальное - Обрабатываем входящие подключения
Спим
Если Postmaster в состоянии PM_WAIT_DEAD_END , то ждем пока dead_end бэкэнды завершатся. Для этого просто засыпаем на 100 мс.
Почему на 100 мс, объяснений нет:
pg_usleep(100000L); /* 100 msec seems reasonable */
Обработка входящих подключений
Первым делом копируем переменную дескриптора портов в локальную, чтобы не затирать порты между обработкой клиентов в select()
.
После вычисляется таймаут ожидания подключения. Он нужен, так как помимо клиентов необходимо следить за воркерами и окружением.
Как расчитывается таймаут:
Нормальное состояние: 60 секунд;
Принят SIGKILL: время ожидания = 5 - (ТЕКУЩЕЕ ВРЕМЯ - ВРЕМЯ ПОЛУЧЕНИЯ SIGKILL);
-
Запрошен старт воркера: ждем 0 секунд;
Тогда
select()
сделает возврат немедленно, а значит процесс старта воркера запустится как можно скорее; Воркер упал: таймаут - наибольшее время необходимое для восстановление воркера, но не более 1 минуты.
Теперь начинаем принимать подключения:
selres = select(nSockets, &rmask, NULL, NULL, &timeout);
Как только функция вернулась проверяем результат.
Если select()
вернул ошибку (-1), то падаем.
Если select()
вернул не 0, то на какой-то порт пришел запрос. Проходимся по всем портам, и для каждого, на который постучались:
Создаем соединение
Соединение создается через сокеты:
Принимается сокетное соединение на порту -
accept()
Сохраняется адрес -
getsockname()
-
Если TCP - настраиваем
TCP_NODELAY
TCP_KEEPALIVE
Оптимизация буфера для Windows
TCP_NODELAY
TCP_NODELAY - флаг, устанавливающий немедленную отправку пакетов. Грубо говоря, буфер сразу сбрасывается (делается сетевой запрос), независимо от его размера. Если его не указывать будет использоваться алгоритм Нейгла.
Алгоритм Нейгла - алгоритм, позволяющий уменьшить количество пакетов, которые должны быть отправлены по сети. Идея состоит в том, что несколько небольших пакетов объединяются, а затем отправляются все сразу.
TCP_KEEPALIVE
Когда мы выходим в интернет, то можем сидеть через NAT. Для оптимизации работы, “мертвые” соединения могут удалятся. Так как соединение создается с базой данной, перерывы в отправке пакетов могут быть частые и долгие. Например, сделать анализ выполненного SELECT. За это время NAT сервер может соединение прервать и подключаться придется заново. Чтобы такого не происходило, была добавлена опция TCP_KEEPALIVE.
Идея проста: будем посылать пакеты, чтобы соединение не удалялось. Настраивать работу можем через несколько параметров:
tcp_keepalive_time - сколько ждать, прежде чем посылать KEEPALIVE пакет
tcp_keepalive_probes - количество попыток послать KEEPALIVE пакет
tcp_keepalive_intvl - интервал отправки между попытками
Алгоритм такой:
Ждем tcp_keepalive_time секунд
Посылаем KEEPALIVE пакет
Ждем tcp_keepalive_intvl секунд
-
Если ответ не пришел:
Если количество попыток меньше tcp_keepalive_probes - повторить заново
Соединение мертво - закрываем соединение
Ответ пришел - соединение живо
Повторяем заново - ждем tcp_keepalive_time секунд
Оптимизация буфера под Windows
Размер буфера отправки Windows по умолчанию 8 Кб. Если его не увеличить, то с установленным TCP_NODELAY будет отправляться много мелких TCP пакетов, что скажется на производительности. Чтобы исправить это, размер буфера отправки делается изначально достаточно большим.
В версиях позднее Windows Server 2012 размер буфера стал 64 Кб, что убирает потребность в увеличении буфера. Также в Windows 7 появилась функция “dynamic send buffering” (размер буфера приема подстраивается), но если вручную выставить размер буфера, эта функция выключится. Поэтому, перед тем как выставлять новый размер, нужно проверить, что это целесообразно
int optlen;
int newopt;
optlen = sizeof(oldopt);
if (getsockopt(port->sock, SOL_SOCKET, SO_SNDBUF, (char *) &oldopt,
&optlen) < 0)
{
ereport(LOG,
(errmsg("%s(%s) failed: %m", "getsockopt", "SO_SNDBUF")));
return STATUS_ERROR;
}
newopt = PQ_SEND_BUFFER_SIZE * 4;
if (oldopt < newopt)
{
if (setsockopt(port->sock, SOL_SOCKET, SO_SNDBUF, (char *) &newopt,
sizeof(newopt)) < 0)
{
ereport(LOG,
(errmsg("%s(%s) failed: %m", "setsockopt", "SO_SNDBUF")));
return STATUS_ERROR;
}
}
Старт бэкэнда
Создаем структуру Бэкэнда:
typedef struct bkend
{
pid_t pid; /* process id of backend */
int32 cancel_key; /* cancel key for cancels for this backend */
int child_slot; /* PMChildSlot for this backend, if any */
int bkend_type; /* child process flavor, see above */
bool dead_end; /* is it going to send an error and quit? */
bool bgworker_notify; /* gets bgworker start/stop notifications */
dlist_node elem; /* list link in BackendList */
} Backend;
И инициализируем его:
Выделяем память
Создаем CancelKey
Определяем может ли бэкэнд принимать соединения (он может стартовать сразу в DEAD_END)
Запускаем его - форкаемся. Бэкэнд уходит в свой процесс и работает там до конца, не возвращается.
Дальше проверяем, что запуск произошел успешно и добавляем его в список работающих. Дальше будем проверять его состояние в перерывах. Для этого, в частности, установили лимит на таймаут ожидания подключения.
Когда обрабатываем сигналы?
Главный механизм обмена сообщениями в Postgres - сигналы. Но если мы запустим обработчик сигнала в неподходящее время, то можем нарушить свою целостность. Чтобы избежать этого, сигналы следует принимать только в моменты, когда это безопасно: ничего не делаем. В частности, сигналы обрабатываются когда:
Ждем подключения - вызов
select()
Засыпаем -
pg_usleep()
Как блокировать сигналы?
В Linux для этого существует getprocsigmask()
- функция, позволяющая игнорировать определенные сигналы, не запускать обработчики. (Обработчики не запускаются в вызвавшем ее потоке - для многопоточного приложения поведение не определено)
В Windows такого нет, поэтому эмулируется самостоятельно.
Реализации склеиваются через макросы:
#ifndef WIN32
#define PG_SETMASK(mask) sigprocmask(SIG_SETMASK, mask, NULL)
#else
/* Emulate POSIX sigset_t APIs on Windows */
typedef int sigset_t;
extern int pqsigsetmask(int mask);
#define PG_SETMASK(mask) pqsigsetmask(*(mask))
#define sigemptyset(set) (*(set) = 0)
#define sigfillset(set) (*(set) = ~0)
#define sigaddset(set, signum) (*(set) |= (sigmask(signum)))
#define sigdelset(set, signum) (*(set) &= ~(sigmask(signum)))
#endif /* WIN32 */
Например, при приеме соединений, сигналы обрабатываем так:
PG_SETMASK(&UnBlockSig);
selres = select(nSockets, &rmask, NULL, NULL, &timeout);
PG_SETMASK(&BlockSig);
Заголовочный файл лежит в src/include/libpq/pqsignal.h
Проверка вспомогательных процессов
Postgres - многопроцессное приложение. Если какой-то процесс упадет, то система продолжит работать.
Как узнаем, что дочерний процесс упал? Делать регулярные проверки. Postmaster хранит PID'ы основных процессов:
/* PIDs of special child processes; 0 when not running */
static pid_t StartupPID = 0,
BgWriterPID = 0,
CheckpointerPID = 0,
WalWriterPID = 0,
WalReceiverPID = 0,
AutoVacPID = 0,
PgArchPID = 0,
PgStatPID = 0,
SysLoggerPID = 0;
После обработки пришедшего сигнала, смотрим упал ли какой-нибудь воркер, и поднимаем при необходимости. Для многих процессов есть свои условия для запуска. Например:
/*
* If no background writer process is running, and we are not in a
* state that prevents it, start one. It doesn't matter if this
* fails, we'll just try again later. Likewise for the checkpointer.
*/
if (pmState == PM_RUN || pmState == PM_RECOVERY ||
pmState == PM_HOT_STANDBY)
{
if (CheckpointerPID == 0)
CheckpointerPID = StartCheckpointer();
if (BgWriterPID == 0)
BgWriterPID = StartBackgroundWriter();
}
Проверке подвергаются также и бэкэнды: проходимся по всем и проверяем их статус.
Проверка локфайлов
При запуске, был создан локфайл postmaster.pid. В нем хранится рабочая информация.
В частности, первая строка хранит PID запущенного Postmaster. Может ли получиться так, что 2 процесса запущено одновременно? Да, например, на различных портах. Лучше, чтобы такого не происходило, иначе могут одновременно измениться несколько файлов. Ничего хорошего это не принесет.
Раз в минуту проверяем файл. Если его нет или хранящийся в нем PID не равен нашему, то значит новый процесс БД запустился. В таком случае, экстренно закрываемся - отправляем SIGQUIT самому себе.
now = time(NULL);
if (now - last_lockfile_recheck_time >= 1 * SECS_PER_MINUTE)
{
if (!RecheckDataDirLockFile())
{
ereport(LOG,
(errmsg("performing immediate shutdown because data directory lock file is invalid")));
kill(MyProcPid, SIGQUIT);
}
last_lockfile_recheck_time = now;
}
Не забываем про UNIX сокеты.
Они сохраняются в директории, указанной в настройке unix_socket_directories
в postgresql.conf
. По умолчанию, равен /tmp
. В нем хранятся временные файлы. Чтобы их не скопилось слишком много, часто запущены процессы очищающие директорию. Чтобы они случайно не удалили сокет-файл, будем обновлять время последнего изменения файла - вызывать touch()
.
if (now - last_touch_time >= 58 * SECS_PER_MINUTE)
{
TouchSocketFiles();
TouchSocketLockFiles();
last_touch_time = now;
}
Кто удаляет сокет файлы?
По умолчанию, сокетные файлы создаются в /tmp.
Так как мы запускаем БД, то скорее всего исполняться будем на сервере. Сервер работает долго, и, если не очищать /tmp, она может заполнить все свободное пространство. Чтобы такого не произошло существуют процессы очистки.
Существует несколько программ для автоматического очищения:
tmpwatch
systemd-tmpfiles
tmpreaper
Свой скрипт
Для запуска с периодичностью можно использовать cron
Конец итерации
На этом итерация заканчивается и заходим на новый круг.
Stillgray
Спасибо за статью.
Было бы здорово упомянывать названия исходных файлов. Или это всё происходит в src/backend/main/main.c?
AshBlade Автор
Добавил ссылку на файл с циклом. Все действия происходят в нем