MogileFS с поддержкой работы через PgBouncer

Содержание

Введение

После миграции трекера MogileFS с MySQL на PostgreSQL на сервере PostgreSQL возросло количество используемых подключений. Поскольку каждое подключение обслуживается отдельным процессом PostgreSQL, для которого выделяются буферы work_mem и temp_buffers, возрастает потребление оперативной памяти. Для экономии оперативной памяти можно воспользоваться PgBouncer, который позволяет экономить подключения к серверу PostgreSQL за счёт повторного использования одного и того же процесса для обслуживания запросов, поступающих из разных входящих подключений. Однако при попытке переключить mogilefsd на работу через PgBouncer возникли проблемы.

Заготовленные запросы

Драйвер DBD::Pg, используемый трекером MogieFS, при подключении к PostgreSQL версии 8.8 и выше автоматически использует заготовленные запросы (prepared statements). К сожалению, при использовании PgBouncer заготовка запроса и попытка использования заготовленного запроса могут попадать в разные процессы PostgreSQL, из-за чего заготовленные запросы будут завершаться ошибкой.

В драйвере DBD::Pg имеется опция pg_server_prepare, позволяющая отключить использование заготовленных запросов. К сожалению, её нельзя указать в строке DSN с настройками подключения и поэтому не получится указать в файле конфигурации трекера MogileFS. Для того, чтобы отключить использование заготовленных запросов, придётся отредактировать исходные тексты трекера MogileFS. Интересующий нас фрагмент находится в файле MogileFS::Store. Строка 382 выглядит следующим образом:

sqlite_use_immediate_transaction => 1,

Добавим сразу за ней такую строчку:

pg_server_prepare => 0,

Аналогичные исправления нужно внести в исходные тексты mogstats. Интересующий нас фрагмент находится в одноимённом файле mogstats. Строчка 312 выглядит следующим образом:

RaiseError => 1,

Добавим сразу за ней строчку:

pg_server_prepare => 0,

Рекомендательные блокировки

Если ограничиться описанным выше исправлением, то в журнале сервера PostgreSQL периодически будут появляться ошибки следующего вида:

[local] WARNING:  you don't own a lock of type ExclusiveLock

Оказалось, что в модуле MogileFS::Store::Postgres используются рекомендательные блокировки (advisory locks). Если заглянуть в исходные тексты модуля MogileFS::Store::Postgres, то можно обнаружить, что в нём используются функции PostgreSQL pg_try_advisory_lock и pg_advisory_unlock. Обе эти функции не блокируют доступ к каким-либо объектам в самой базе данных, а являются просто удобным механизмом синхронизации частей приложения, работающих на разных серверах, но использующих одну и ту же базу данных. Первая функция принимает идентификатор блокировки, который может состоять из одного или двух чисел и создаёт блокировку с указанным идентификатором. Блокировка действует до её снятия с помощью второй функции, либо до конца подключения.

Возникающая проблема хорошо описана в статье The Pitfall of Using PostgreSQL Advisory Locks with Go's DB Connection Pool. освбодить блокировку можно только из того же процесса PostgreSQL, в котором она была создана. Поскольку PgBouncer может направлять запросы из входящих подключений в разные процессы PostgreSQL, попытка снять блокировку из другого процесса приведёт к возникновению приведённой выше ошибки: блокировка принадлежит другому процессу и поэтому не снимается.

Я снова обратился к исходным текстам MogileFS, для чего сначала получил исходные тексты из репозитория:

$ git clone https://github.com/mogilefs/MogileFS-Server

Перешёл в каталог с репозиторием:

$ cd MogileFS-Server

И переключился на используемую мной версию 2.70:

$ git checkout 2.70

Я просмотрел историю изменений модуля MogileFS::Store::Postgres следующим образом:

$ git log -p lib/MogileFS/Store/Postgres.pm

Оказалось, что до того, как в трекере начали использоваться рекомендательные блокировки (advisory locks), тот же функционал был реализован с использованием таблицы lock. Для возврата нужно откатить две фиксации, одна из которых заменяет использование таблицы lock на рекомендательные блокировки, а вторая исправляет ошибку, добавленную первой фиксацией. Для начала я сформировал заплатку с изменениями:

$ git diff -R ac5534a0c3d046e660fa7581c9173857f182bd81 21a66942fde3bb4f9e5ee24dac787d3c9ebbb41f lib/MogileFS/Store/Postgres.pm > mogilefs_without_pg_advisory_locks.patch

Применить эти изменения к исходным текстам можно следующим образом:

$ patch -p1 < mogilefs_without_pg_advisory_locks.patch

Статистика дисков

В выводе команды mogadm device list могут отображаться неверные данные. Эти данные берутся из таблицы device, которая по каким-то причинам не обновляется. В журнале трекера можно найти следующие ошибки:

Mar 19 14:56:42 mogilefs-tracker-2 mogilefsd[8211]: crash log: DBD::Pg::db do failed: ERROR:  smallint out of range at /usr/share/perl5/MogileFS/Store.pm line 1886.#012 at /usr/share/perl5/MogileFS/Worker/Delete.pm line 189
Mar 19 14:56:43 mogilefs-tracker-2 mogilefsd[8199]: Child 8211 (delete) died: 256 (UNEXPECTED)
Mar 19 14:56:43 mogilefs-tracker-2 mogilefsd[8199]: Job delete has only 0, wants 1, making 1.

Исправить проблему можно изменив тип поля failcount в таблице file_to_delete2 со smallint на integer с помощью следующего запроса:

ALTER TABLE file_to_delete2 ALTER COLUMN failcount SET DATA TYPE integer;

Чтобы таблица file_to_delete2 сразу создавалась с полем failcount типа integer, поправим исходные тексты. Интересующий нас фрагмент находится в файле lib/MogileFS/Store. В строке 813 поле failcount имеет тип TINYINT:

failcount TINYINT UNSIGNED NOT NULL default '0',

Заменим тип поля на INT, так что строка примет следующий вид:

failcount INT UNSIGNED NOT NULL default '0',

Также утилита mogstats не выводит статистику по дискам, на которых нет файлов. Интересующая нас фрагмент находится в файле mogstats. Строка 410 выглядит следующим образом:

my $stats = $dbh->selectall_arrayref('SELECT devid, COUNT(devid) FROM file_on GROUP BY 1');

Заменим в ней запрос, так чтобы строчка приняла следующий вид:

my $stats = $dbh->selectall_arrayref('SELECT device.devid, COUNT(file_on.devid) FROM device LEFT JOIN file_on ON file_on.devid = device.devid WHERE device.status = \'alive\' GROUP BY 1;');

Удаление неактуальных блокировок

Описанная выше проблема с пересчётом статистики дисков иногда возникает уже по другой причине. В журнале трекера можно найти следующие ошибки:

Sep 29 19:10:43 mogilefs-tracker-4 mogilefsd[19311]: Watchdog killing worker 26388 (monitor)
Sep 29 19:10:43 mogilefs-tracker-4 mogilefsd[19311]: Child 26388 (monitor) died: 9 (expected)
Sep 29 19:10:43 mogilefs-tracker-4 mogilefsd[19311]: Job monitor has only 0, wants 1, making 1.

На этот раз дочерний процесс завершается ожидаемым образом, потому что он принудительно завершается самим трекером по сторожевому таймеру. Затем менеджер процессов запускает новый процесс взамен принудительно завершённого. Точная причина, почему дочерний процесс перестаёт отвечать на запросы сторожевого таймера, остаётся неизвестной. Но после завершения процесса остаётся запись в таблице блокировок, которая в дальнейшем не позволяет новому процессу захватить блокировку, поскольку новый процес считает, что такая блокировка уже активна:

mogilefs_34=# select * from lock;
   lockid   |      hostname      | pid   | acquiredat
------------+--------------------+-------+------------
 2065055674 | mogilefs-tracker-4 | 19311 | 1725948160
(1 row)

Для того, чтобы находить и устранять неактуальные блокировки, я доработал функцию получения блокировки get_lock в файле MogileFS-Server/lib/MogileFS/Store/Postgres.pm, добавив следующий фрагмент перед попытками получения блокировки:

debug("$$ Checking stale lock $lockname ($lockid)\n") if $Mgd::DEBUG >= 5;

my $sth = $self->dbh->prepare('SELECT pid FROM lock WHERE lockid = ? AND hostname = ? AND pid <> ?');
$sth->execute($lockid, hostname, $$);
my $aref = $sth->fetchall_arrayref();
$sth->finish();
$self->condthrow;
for my $row (@{$aref}) {
    my @columns = @{$row};
    my $pid = $columns[0];

    my $killed = kill('SIGZERO', $pid);
    unless ($killed) {
       debug("$$ Releasing stale lock $lockname ($lockid) for pid $pid\n") if $Mgd::DEBUG >= 2;
       eval {
         $self->dbh->do('DELETE FROM lock WHERE lockid = ? AND hostname = ? AND pid = ?', undef, $lockid, hostname, $pid);
       };
       $self->condthrow;
    }
}

Этот фрагмент извлекает из таблицы блокировок идентификаторы процессов на этом же трекере, которые установили блокировку того же типа, которую нужно получить. Дале каждому процессу отправляется нулевой сигнал, чтобы проверить, присутствует ли он в системе. Если процесса с таким идентификатором в системе нет, то блокировка из таблицы удаляется.

Теперь можно приступить к сборке доработанного deb-пакета.

Настройка виртуальной машины

Для сборки настроим виртуальную машину, аналогичную используемой на том сервере, где установлен трекер MogileFS. В рассматриваемом примере это система Debian 7.11.0 с кодовым именем Wheezy. Получить образ установочного диска можно по ссылке debian-7.11.0-amd64-netinst.iso.

Настройка репозиториев

Для настройки репозиториев поместим в файл /etc/apt/sources.list следующие строки:

deb http://archive.debian.org/debian/ wheezy main contrib non-free
deb http://archive.debian.org/debian/ wheezy-backports main contrib non-free

Поскольку мы установили устаревший релиз, отключим проверку актуальности репозиториев, создав файл /etc/apt/apt.conf.d/valid со следующим содержимым:

Acquire::Check-Valid-Until "false";

Отключаем установку предлагаемых зависимостей, создав файл /etc/apt/apt.conf.d/suggests со следующим содержимым:

APT::Install-Suggests "false";

Отключаем установку рекомендуемых зависимостей, создав файл /etc/apt/apt.conf.d/recommends со следующим содержимым:

APT::Install-Recommends "false";

Система apt сохраняет скачанные пакеты в каталоге /var/cache/apt/archives/, чтобы при необходимости не скачивать их снова. Файлы в этом каталоге по умолчанию не удаляются, что может привести к переполнению диска. Чтобы отключить размер файлов в этом каталоге 200 мегабайтами, создадим файл /etc/apt/apt.conf.d/cache со следующим содержимым:

APT::Cache-Limit "209715200";

Создадим файл /etc/apt/apt.conf.d/timeouts с настройками таймаутов обращения к репозиториям:

Acquire::http::Timeout "5";
Acquire::https::Timeout "5";
Acquire::ftp::Timeout "5";

При необходимости, если репозитории доступны через веб-прокси, можно создать файл /etc/apt/apt.conf.d/proxy, прописав в него прокси-серверы для протоколов HTTP, HTTPS и FTP:

Acquire::http::Proxy "http://10.0.25.3:8080";
Acquire::https::Proxy "http://10.0.25.3:8080";
Acquire::ftp::Proxy "http://10.0.25.3:8080";

Обновим список пакетов, доступных через репозиторий:

# apt-get update

Обновим систему с использованием самых свежих пакетов, доступных через репозитории:

# apt-get upgrade
# apt-get dist-upgrade

Установка пакетов

Установим пакеты, необходимые для сборки:

# apt-get install dpkg-dev devscripts libparse-debcontrol-perl quilt fakeroot build-essential:native debhelper debconf perl sysstat libstring-crc32-perl libperlbal-perl libio-aio-perl libdbd-mysql-perl libdbi-perl libnet-netmask-perl libwww-perl libdanga-socket-perl libsys-syscall-perl libfile-fcntllock-perl git

Сборка клиента

Скачиваем репозитории с исходниками клиентской библиотеки MogileFS:

$ GIT_SSL_NO_VERIFY=yes git clone https://github.com/mogilefs/perl-MogileFS-Client

Перейдём в каталог с получеными исходным текстами:

$ cd perl-MogileFS-Client

И переключимся на используемую на сервере версию 2.16:

$ git checkout 1.16

Меняем уровень совместимости для утилиты debhelper:

$ echo -n "7" > debian/compat

Исправим в файле changelog номер версии, отредактировав этот файл с помощью команды dch -i, добавив в его начало дополнительную запись:

libmogilefs-perl (1.16) UNRELEASED; urgency=low

  * Building version 1.16.

 -- Vladimir Stupin <stupin_v@ufanet.ru>  Wed, 20 Mar 2024 11:52:35 +0500

Выполняем сборку пакета с исходниками и двоичного пакета:

$ dpkg-buildpackage -us -uc -rfakeroot

В вышестоящем каталоге появятся следующие файлы с результатами сборки:

Эти файлы можно поместить в репозиторий, например, с помощью утилиты aptly или reprepro.

Доработка и сборка пакета с сервером

Скачиваем репозитории с исходниками сервера MogileFS:

$ GIT_SSL_NO_VERIFY=yes git clone https://github.com/mogilefs/MogileFS-Server

Перейдём в каталог с получеными исходным текстами:

$ cd MogileFS-Server

И переключимся на используемую на сервере версию 2.70:

$ git checkout 2.70

Подготовим описанную выше заплатку, если она ещё не подготовлена:

$ git diff -R ac5534a0c3d046e660fa7581c9173857f182bd81 21a66942fde3bb4f9e5ee24dac787d3c9ebbb41f lib/MogileFS/Store/Postgres.pm > ../mogilefs_without_pg_advisory_locks.patch

Начинаем добавление новой заплатки с названием revert_pg_advisory_revert:

$ quilt new pg_advisory_locks_revert

Добавляем отслеживание в заплатке файла lib/MogileFS/Store/Postgres.pm:

$ quilt add lib/MogileFS/Store/Postgres.pm

Вносим в файл lib/MogileFS/Store/Postgres.pm изменения, отменяющие использование Advisory Locks из PostgreSQL:

$ patch -p1 < ../mogilefs_without_pg_advisory_locks.patch

Вносим изменения в патч:

$ quilt refresh

Начинаем добавление новой заплатки с названием pg_server_prepare_disabled:

$ quilt new pg_server_prepare_disabled

Добавляем в заплатку отслеживание изменений в файле lib/MogileFS/Store.pm:

$ quilt add lib/MogileFS/Store.pm

Редактируем файл lib/MogileFS/Store.pm, за строкой 380 с текстом sqlite_use_immediate_transaction => 1, добавляем строку pg_server_prepare => 0,.

Вносим изменения в патч:

$ quilt refresh

Начинаем добавление новой заплатки с названием file_to_delete2_failcount_int:

$ quilt new file_to_delete2_failcount_int

Добавляем в заплатку отслеживание изменений в файле lib/MogileFS/Store.pm:

$ quilt add lib/MogileFS/Store.pm

Откроем файл lib/MogileFS/Store на редактирвоание, перейдём к строчке 813, которая имеет вид:

failcount TINYINT UNSIGNED NOT NULL default '0',

Заменим тип поля на INT, так что строка примет следующий вид:

failcount INT UNSIGNED NOT NULL default '0',

Вносим изменения в патч:

$ quilt refresh

Начинаем добавление новой заплатки с названием postgres_remove_stale_locks:

$ quilt new postgres_remove_stale_locks

Добавляем в заплатку отслеживание изменений в файле lib/MogileFS/Store/Postgres.pm:

$ quilt add lib/MogileFS/Store/Postgres.pm

Редактируем файл lib/MogileFS/Store/Postgres.pm, в функции get_locks перед строчкой debug("$$ Locking $lockname ($lockid)\n") if $Mgd::DEBUG >= 5; добавляем фрагмент уже описанного выше кода:

debug("$$ Checking stale lock $lockname ($lockid)\n") if $Mgd::DEBUG >= 5;

my $sth = $self->dbh->prepare('SELECT pid FROM lock WHERE lockid = ? AND hostname = ? AND pid <> ?');
$sth->execute($lockid, hostname, $$);
my $aref = $sth->fetchall_arrayref();
$sth->finish();
$self->condthrow;
for my $row (@{$aref}) {
    my @columns = @{$row};
    my $pid = $columns[0];

    my $killed = kill('SIGZERO', $pid);
    unless ($killed) {
       debug("$$ Releasing stale lock $lockname ($lockid) for pid $pid\n") if $Mgd::DEBUG >= 2;
       eval {
         $self->dbh->do('DELETE FROM lock WHERE lockid = ? AND hostname = ? AND pid = ?', undef, $lockid, hostname, $pid);
       };
       $self->condthrow;
    }
}

Вносим изменения в патч:

$ quilt refresh

Запускаем утилиту dch для обновления журнала изменений пакета:

$ dch -i

Вводим описание доработанной нами версии пакета:

mogilefs-server (2.70+ufanet3) UNRELEASED; urgency=low

  * Revert usage of PostgreSQL advisory locks to support PgBouncer,
  * Disabled option pg_server_prepare to support PgBouncer,
  * Type of field failcount in table file_to_delete2 changed from TINYINT to INT,
  * Added removing stale locks for PostgreSQL.

 -- Vladimir Stupin <stupin_v@ufanet.ru>  Mon, 07 Oct 2024 12:19:13 +0500

Меняем уровень совместимости для утилиты debhelper:

$ echo -n "7" > debian/compat

Выполняем сборку доработанного deb-пакета:

$ dpkg-buildpackage -us -uc -rfakeroot

В результате в каталоге выше должны появиться следующие файлы:

Эти файлы можно поместить в репозиторий, например, с помощью утилиты aptly или reprepro.

Доработка и сборка пакета с утилитами

Устанавливаем в систему пакет libmogilefs-perl, собранный нами ранее:

# dpkg -i libmogilefs-perl_1.16_all.deb

Скачиваем репозитории с исходниками утилит MogileFS:

$ GIT_SSL_NO_VERIFY=yes git clone https://github.com/mogilefs/MogileFS-Utils

Перейдём в каталог с получеными исходным текстами:

$ cd MogileFS-Utils

И переключимся на используемую на сервере версию 2.28:

$ git checkout 2.28

Начинаем добавление новой заплатки с названием pg_server_prepare_disabled:

$ quilt new pg_server_prepare_disabled

Добавляем в заплатку отслеживание изменений в файле mogstats:

$ quilt add mogstats

Редактируем файл mogstats, за строкой 312 с текстом RaiseError => 1, добавляем строку pg_server_prepare => 0,.

Вносим изменения в патч:

$ quilt refresh

Начинаем добавление новой заплатки с названием listing_devices_without_files:

$ quilt new listing_devices_without_files

Добавляем в заплатку отслеживание изменений в файле mogstats:

$ quilt add mogstats

Откроем файл mogstats для редактирования, перейдём к строке 411:

my $stats = $dbh->selectall_arrayref('SELECT devid, COUNT(devid) FROM file_on GROUP BY 1');

Заменим в ней запрос, так чтобы строчка приняла следующий вид:

my $stats = $dbh->selectall_arrayref('SELECT device.devid, COUNT(file_on.devid) FROM device LEFT JOIN file_on ON file_on.devid = device.devid WHERE device.status = \'alive\' GROUP BY 1');

Вносим изменения в патч:

$ quilt refresh

Запускаем утилиту dch для обновления журнала изменений пакета:

$ dch -i

Вводим описание доработанной нами версии пакета:

mogilefs-utils (2.28+ufanet2) UNRELEASED; urgency=low

  * Disabled option pg_server_prepare to support PgBouncer in mogstats.
  * Added listing devices without files in mogstats.

 -- Vladimir Stupin <stupin_v@ufanet.ru>  Wed, 20 Mar 2024 12:48:41 +0500

Меняем уровень совместимости для утилиты debhelper:

$ echo -n "7" > debian/compat

Выполняем сборку доработанного deb-пакета:

$ dpkg-buildpackage -us -uc -rfakeroot

В результате в каталоге выше должны появиться следующие файлы:

Эти файлы можно поместить в репозиторий, например, с помощью утилиты aptly или reprepro.

Дополнительные материалы