воскресенье, 30 августа 2015 г.

php5-fpm и chroot

В php5-fpm имеется множество разных средств обеспечения безопасности веб-приложений. Во-первых, каждое приложение можно запустить в отдельном пуле, от имени отдельного системного пользователя. Во-вторых, можно зафиксировать некоторые настройки php при помощи директив php_admin_value и php_admin_flag. В-третьих, можно запретить веб-приложению использовать определённые функции php при помощи директивы php_admin_value[disable_functions]. В-четвёртых, при помощи директивы php_admin_value[open_basedir] можно ограничить список каталогов, в пределах которых будет работать веб-приложение.

Для параноиков же предусмотрен ещё один способ защиты - запуск веб-приложения внутри chroot-среды. Этот режим ценен тем, что позволяет защитить систему от взломщиков, пытающихся воспользоваться локальными уязвимостями, то есть уязвимостями установленного в системе программного обеспечения. В идеале, в chroot-среде должен иметься только необходимый для работы веб-приложения минимум файлов, программ и библиотек. Уязвимость в программе внутри chroot-окружения не окажет никакого влияния на файлы за его пределами. Также хорошо бы было защититься от сетевой активности, не характерной для штатной работы веб-приложения. Например, от возможности отправлять куда-то спам. Но это уже можно попробовать реализовать при помощи модуля owner для iptables. А вот если уязвимости есть в ядре системы, то воспользоваться ими, по идее, можно даже из chroot-среды.

Для поиска проблем при работе веб-приложений в chroot-среде неоценимую помощь мне оказали две программы: strace и ltrace. Первая позволяет следить за системными вызовами, выполняемыми отслеживаемым процессом, а вторая - за вызовами библиотечных функций. Мне оказалось достаточным знать о всего двух опциях этих программ:
  • -p - позволяет указать идентификатор процесса, за которым нужно следить. Эту опцию можно указать несколько раз, тогда будут отслеживаться системные вызовы (или вызовы библиотечных функций) из всех указанных процессов.
  • -f - позволяет отслеживать системные вызовы (или вызовы библиотечных функций) порождённых процессов.
Насколько я понял, для работы веб-приложений на php в chroot-среде достаточно добавить в неё разделяемые библиотеки libnss и минимальные настройки для них. Кроме того, нужно создать каталоги для хранения сеансов php и временных файлов. Ещё нужны файлы временных зон. Наконец, нужно устройство /dev/urandom. Возможно каким-то другим приложениям понадобится что-то ещё. Выяснить это можно при помощи упомянутых выше утилит strace и ltrace.

Для работы веб-приложений с базами данных MySQL и PostgreSQL нужно в настройках веб-приложения поменять адрес сервера localhost на 127.0.0.1, поскольку при указании localhost будет предпринята попытка подключения к Unix-сокету, которого в chroot-среде нет. При замене адреса сервера на 127.0.0.1 подключение будет осуществляться на TCP-порт.

Многие веб-приложения на PHP используют функцию mail для отправки писем с помощью sendmail или утилит, заменяющих sendmail. Чтобы отправка почты работала в chroot-окружении, нужно установить в неё альтернативу sendmail. Я попробовал воспользоваться для этих целей утилитами esmtp и msmtp. Обе утилиты мне удалось настроить для отправки почты с аутентификацией, но вот настраивать шифрование по протоколу TLS мне уже было лень, т.к. для его работы в chroot-среду нужно положить дополнительные файлы - список доверенных сертификатов и сами сертификаты.

Поскольку описание настройки chroot-среды в блоге получилось бы очень объёмным, я скомпоновал инструкции по настройке среды в скрипт, который можно взять здесь: http://stupin.su/files/php_fpm_chroot.sh

Для удобства пересоздания chroot-среды предполагается, что файлы веб-приложения будут находиться в каталоге /site. Перед созданием chroot-среды из неё удаляются каталоги bin, etc, dev, lib, lib64, tmp, usr, var. Перед запуском скрипта нужно поменять его настройки под ваши нужды и обязательно убедиться в том, что переменная CHROOT указывает не на корень вашей системы. Чем это грозит, думаю говорить не приходится.

Внутри скрипта есть следующие настройки:
  • USER - пользователь, которому будут принадлежать файлы веб-приложения, лежащего в каталоге site,
  • GROUP - группа, которой будут принадлежать файлы веб-приложения, лежащего в каталоге site,
  • UID - идентификатор пользователя, указанного в USER,
  • GID - идентификатор группы, указанной в GROUP,
  • ZONEINFO - локальная временная зона веб-приложения, например - Asia/Yekaterinburg,
  • MAILER - программа-аналог sendmail для отправки писем. Доступны варианты - esmtp, msmtp. Любое другое значение отключит настройку программы (я в таких случаях указываю значение no),
  • MAIL_SERVER - адрес SMTP-сервера,
  • MAIL_PORT - порт SMTP-сервера (чаще всего это порт 25 или 587),
  • MAIL_USER - почтовый ящик с доменом. Предполагается, что аутентификация на SMTP-сервере происходит по имени этого почтового ящика, вместе с доменом,
  • MAIL_PASSWORD - пароль для аутентификации на SMTP-сервере.
На этом всё. Готов выслушать замечания и предложения.

воскресенье, 23 августа 2015 г.

Чистка системы от устаревших и ненужных пакетов

Установим пакет, специально предназначенный для удаления ненужных пакетов:
# apt-get install debfoster
Удаляем пакеты, которые по нашему мнению не нужны. Предлагаются к выбору только те пакеты, от которых не зависят другие:
# debfoster
Создадим скрипт с именем no_repo.sh, который выведет список установленных пакетов, отсутствующих в текущих репозиториях:
#!/bin/sh

dpkg -l | awk '/^ii/ { print $2; }' \
  | while read pkg;
    do
      apt-cache policy $pkg \
        | awk -v pkg=$pkg 'BEGIN { local = 0;
                                   repo = 0; }

                           /^[ ]+[0-9]+ .*status$/ { local = 1; }

                           /^[ ]+[0-9]+ / && !/status$/ { repo = 1; }

                           END { if ((local == 1) && (repo == 0))
                                   print pkg; }'
    done
Дадим скрипту право быть запущенным:
$ chmod +x not_repo.sh

Составляем список пакетов, отсутствующих в текущих репозиториях:
$ ./not_repo.sh > not_repo

Редактируем полученный список, удаляя из него пакеты, которые нам нужны или которые были установлены вручную. Затем удаляем оставшиеся:
# apt-get purge `cat not_repo`

Составляем список удалённых пакетов, от которых остались какие-либо файлы:
$ dpkg -l | awk '$1 !~ /^ii/ { print $2; }' > removed

Редактируем полученный список, удаляя из него пакеты, файлы конфигурации и файлы данных которых нужно оставить нетронутыми. Далее удаляем оставшиеся:
# apt-get purge `cat removed`

Теперь осталось удалить пакеты, установленные автоматически и больше не нужные:
# apt-get autoremove

Результат - удалось высвободить почти гигабайт места:

воскресенье, 16 августа 2015 г.

systemd и uwsgi в режиме Emperor

Изо всех щелей рассказывают о том, какой systemd хороший. На эти рассказы даже купились в Debian, устраивали голосования одно за другим вплоть до достижения "нужного" исхода. Решение пропихнули и в Jessie теперь systemd идёт по умолчанию. Только вот в Jessie service-файлов кот наплакал и большей частью всё происходит по старинке - через те же скрипты /etc/init.d/, только теперь запускает их процесс с другим именем. Непонятно, то ли происходит саботаж этого решения, то ли на самом деле не так уж важны эти фишки systemd. А вы думали только в России бывает такое головотяпство?

Скрипт /etc/init.d/uwsgi кроме команды типа start/stop/restart опционально может принимать ещё имя экземпляра, так что с помощью этого скрипта можно было легко перезапускать отдельные экземпляры uwsgi, обслуживающие разные приложения. Теперь при попытке воспользоваться этим скриптом происходит перехват управления, в результате чего этот скрипт выполняется через systemctl. systemctl о дополнительном аргументе ничего не знает, поэтому указать нужный экземпляр uwsgi не представляется возможным. Перехват происходит через файл-хук /lib/lsb/init-functions.d/40-systemd, который используется в файле /lib/lsb/init-functions, который в свою очередь используется во всех скриптах /etc/init.d/ в Debian.

Само собой напрашивалось решение с многоэкземплярным service-файлом, который вызывался бы примерно так:
# systemctl start uwsgi@redmine.service
Но тут я вспомнил о том, что в Jessie поставляется новый uwsgi, в котором поддерживается режим Emperor. Emperor - это отдельный процесс uwsgi, который умеет заглядывать в указанный каталог и искать там файлы конфигурации экземпляров uwsgi. При появлении нового файла конфигурации он умеет самостоятельно порождать новые процессы master, которые в свою очередь порождают процессы worker. Соответственно, при пропадании файла конфигурации Emperor корректно завершает master-процесс, соответствующий пропавшему файлу конфигурации. Плюс к тому, он умеет следить за master-процессами, порождая их, если они вдруг неожиданно завершились.

Поиски документации привели меня сюда: uwsgi - systemd. В ходе экспериментов получилось рабочее решение, которым и спешу поделиться.

Для начала создадим service-файл /etc/systemd/system/uwsgi.service:
[Unit]
Description=uWSGI Emperor
After=syslog.target

[Service]
ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/emperor.ini
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

[Install]
WantedBy=multi-user.target
Теперь создаём файл /etc/uwsgi/emperor.ini, который будет содержать конфигурацию "императора":
[uwsgi]

emperor = /etc/uwsgi/apps-enabled/
vassals-inherit = /etc/uwsgi/vassal-default.ini
Теперь создаём файл с настройками по умолчанию для "вассалов". За основу возьмём файл /usr/share/uwsgi/conf/default.ini, который упоминался в файле /etc/default/uwsgi. Пути к файлам придётся изменить так, чтобы для создания любого из файлов не нужно было создавать соответствующий каталог - файл взятый за основу этим грешит, но в нём это не является проблемой, т.к. файл инициализации может создавать необходимые каталоги сам. Доведя файл конфигурации вассала до состояния, пригодного для использования, получим файл /etc/uwsgi/vassal-default.ini:
[uwsgi]

autoload = true

master = true
workers = 2
no-orphans = true

pidfile = /run/uwsgi/%N.pid
socket = /run/uwsgi/%N.socket
chmod-socket = 660

logto = /var/log/uwsgi/%N.log
log-date = true

uid = www-data
gid = www-data
Есть ещё один странный момент. Чтобы экземпляры заработали, у имён плагинов нужно убрать префикс uwsgi_. Например, имя плагина плагина uwsgi_rack_ruby21 нужно сократить до rack_ruby21.

Чтобы при перезагрузке компьютера создавался каталог /run/uwsgi, создадим файл /etc/tmpfiles.d/uwsgi.conf со следующей строчкой:
d /run/uwsgi 0750 www-data www-data -
На всякий случай приведу команду, при помощи которой можно создать все каталоги, создаваемые в процессе загрузки системы:
# systemd-tmpfiles --create
Поскольку пути к сокетам поменяются, нужно подготовить пути к новым сокетам в конфигурации nginx.

Теперь останавливаем uwsgi, запущенный скриптом /etc/init.d/uwsgi, включаем использование service-файла и запускаем uwsgi уже с его помощью:
# systemctl stop uwsgi.service
# systemctl enable uwsgi.service
# systemctl start uwsgi.service
Чтобы nginx обращался к новым сокетам, размещающимся прямо в каталоге /run/uwsgi, перезапустим и его:
# systemctl restart nginx.service
И как же теперь перезагрузить/перезапустить экземпляр uwsgi? К сожалению, это не так наглядно, как со скриптом /etc/init.d/uwsgi. Для перезагрузки конфигурации используется команда с PID-файлом экземпляра:
# uwsgi --reload /run/uwsgi/redmine.pid
Перезапуск делается через остановку экземпляра, а systemd автоматически запустит его снова:
# uwsgi --stop /run/uwsgi/redmine.pid
Чтобы остановить экземпляр, нужно удалить ссылку из каталога /etc/uwsgi/apps-enabled/:
# rm /etc/uwsgi/apps-enabled/redmine.ini
Чтобы снова запустить экземпляр, нужно снова создать ссылку в каталоге /etc/uwsgi/apps-enabled/:
# ln -s /etc/uwsgi/apps-available/redmine.ini /etc/uwsgi/apps-enabled/redmine.ini
Посмотреть список запущенных процессов можно при помощи systemctl:
$ systemctl status uwsgi.service
На этом всё. Если у вас есть идеи, как сделать перезапуск экземпляров более наглядным, например при помощи многоэкземплярного service-файла, прошу делиться предложениями.

воскресенье, 9 августа 2015 г.

Перенос сайта Wordpress на другой домен

Не так давно возникла задача перенести блог на Wordpress на другой домен. Имелись только резервная копия базы данных и файлов. Задача тривиальная, за исключением нескольких моментов: 1. в базе данных имеется много ссылок на старый домен, 2. перенос осуществлялся с Apache на nginx + php-fpm, поэтому нужно было правильно настроить веб-сервер, 3. на старом хостинге отправлять письма можно было без аутентификации, на новом - только с аутентификацией.

На всякий случай, пройдусь полностью по всей процедуре восстановления.

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

Установим пакеты, необходимые для работы Wordpress:
# apt-get install nginx mysql-server php5-fpm php5-gd php5-mysql libphp-phpmailer
Возможно понадобится что-то ещё, но мне хватило этого.

2. Восстановление базы данных

Подключимся к только что установленному серверу MySQL:
# mysql -uroot -p mysql
Создадим базу данных blog, в которую будем восстанавливать резервную копию:
mysql> CREATE DATABASE blog CHARSET utf8;
Восстановим резервную копию:
mysql> use blog
mysql> source backup.sql
Теперь создадим пользователя, от имени которого Wordpress будет работать с базой данных:
mysql> use mysql
mysql> INSERT INTO user(user, password, host) VALUES('blog', PASSWORD('blog_password'), 'localhost');
mysql> FLUSH PRIVILEGES;
mysql> GRANT ALL ON blog.* TO blog@localhost;
mysql> FLUSH PRIVILEGES;
Теперь исправим содержимое базы данных так, чтобы все ссылки указывали на новый домен (domain.old - старый домен, domain.new - новый домен):
mysql> UPDATE wp_options SET option_value = REPLACE(option_value, 'domain.old', 'domain.new') WHERE option_name IN ('home', 'siteurl');
mysql> UPDATE wp_posts SET post_content = REPLACE(post_content, 'http://domain.old', 'http://domain.new');
mysql> UPDATE wp_posts SET guid = REPLACE(guid, 'http://domain.old', 'http://domain.new');
mysql> UPDATE wp_posts SET pinged = REPLACE(pinged, 'domain.old', 'domain.new');
mysql> UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, 'domain.old', 'domain.new');
mysql> UPDATE wp_comments SET comment_author_url = REPLACE(comment_author_url, 'domain.old', 'domain.new');
Выйдем из клиента mysql:
mysql> \q
3. Настройка php5-fpm

Создадим новый или отредактируем имеющийся файл /etc/php5/fpm/pool.d/default.conf:
[default]

# Блог будет работать с правами пользователя www-data и группы www-data
user = www-data
group = www-data

# Этот пул php5-fpm будет ждать соединений на указанном UNIX-сокете
listen = /var/run/default.sock

# Права доступа к UNIX-сокету
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

# Динамическое количество процессов в пуле
pm = dynamic
# Максимальное количество процессов в пуле - 6
pm.max_children = 6
# При запуске php5-fpm создаётся 2 процесса в пуле
pm.start_servers = 2
# Если все процессы заняты, значит ожидается увеличение нагрузки, нужно создать ещё один процесс
pm.min_spare_servers = 1
# Если свободно больше 4 процессов, завершаем лишние
pm.max_spare_servers = 4
 
# Журнал запросов, обработанных php5-fpm
access.log = /var/log/php5-fpm.access.log

# Журнал для отладки, сюда попадают сообщения об ошибках
php_value[log_errors] = On
php_value[error_log] = /var/log/php5-fpm.error.log

# Отключаем возможность использовать URL'ы вида file.php/section/page/1
php_value[cgi.fix_pathinfo] = 0

# Страница может генерироваться максимум 30 секунд
php_value[max_execution_time] = 30
# Максимальный объём POST-запроса - 16 мегабайт
php_value[post_max_size] = 16M
# Максимальный размер вложений в POST-запросе - 8 мегабайт
php_value[upload_max_filesize] = 8M
# Выставляем местный часовой пояс
php_value[date.timezone] = Asia/Yekaterinburg
Теперь запустим php5-fpm:
# systemctl start php5-fpm.service
4. Подготовка файлов сайта

После распаковки файлов из резервной копии нужно положить их туда, где с ними будут работать nginx и php5-fpm (в нашем случае это путь /home/www01/domain.new) и выставить правильные права доступа:
# cd /home/www01/domain.new/
# chown www-data:www-data -R *
# find . -type d -exec chmod u=rwx,g=rx,o= \{\} \;
# find . -type f -exec chmod u=rw,g=r,o= \{\} \;
5. Настройка nginx

Пропишем в файл /etc/nginx/sites-available/default такие настройки:
server {
  listen 80;

  server_name domain.new;

  root /home/www01/domain.new/;
  index index.php;

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ \.php$ {
    include fastcgi.conf;
    fastcgi_pass unix:/var/run/default.sock;
  }
}
Включим использование этого файла веб-сервером:
# cd /etc/nginx/sites-enabled/
# ln /etc/nginx/sites-available/default .
И запустим сам веб-сервер:
# systemctl nginx start
6. Правка шаблонов Wordpress

Если шаблоны для сайта были разработаны или доработаны специально, нужно поискать и заменить в шаблонах возможно имеющиеся в них ссылки на старый домен. Поиск можно осуществить, например, так:
# cd /home/www/domain.new/
# grep -R domain.old *
Будут выведены имена всех файлов, в которых встречается старый домен и строчки, в которых этот домен встретился. Далее нужно последовательно открыть каждый файл и исправить в нём домен на новый.

По идее, уже сейчас сайт должен работать правильно. Однако, стоит открыть сайт в веб-браузере с активированной функцией отладки и посмотреть, не происходит ли обращений по старому адресу. Если такие обращения происходят, то скорее всего нужно исправить настройки используемой темы или какого-либо плагина Wordpress.

7. Перенаправление со старого домена

Может случиться так, что старый домен проплачен и будет работать ещё некоторое время. Чтобы не терять посетителей, пришедших на старый домен, можно перенастроить его DNS-серверы так, чтобы они указывали на новый хостинг. В таком случае в конфигурацию nginx можно внести небольшое дополнение, которое будет перенаправлять посетителей со старого адреса на новый:
server {
  listen 80;

  server_name domain.old;

  return 301 http://domain.new$request_uri;
}
Можно ещё немного подкорректировать этот фрагмент конфигурации. У многих интернет-пользователей старой закалки сложился стереотип, что имя любого сайта обязано начинаться с www. Чтобы не терять и таких посетителей, поменяем предыдущий фрагмент файла конфигурации следующим образом:
server {
  listen 80;

  server_name domain.old www.domain.old www.domain.new;

  return 301 http://domain.new$request_uri;
}
Чтобы новые настройки вступили в силу, перезагрузим веб-сервер:
# systemctl nginx reload
8. Настройка аутентификации при отправке почты

Как я уже говорил, на старом хостинге почта отправлялась без аутентификации, а на новом требуется аутентификация. Как вариант, на новом хостинге вообще может быть не настроен почтовый сервер. В таком случае всё-же можно отправлять почту, но для этого придётся аутентифицироваться на каком-либо стороннем сервере (gmail.com, yandex.ru, mail.ru и т.п.).

Для решения этой проблемы можно воспользоваться плагином для Wordpress, который называется WP SMTP. Ставим плагин и настраиваем почтовый ящик, который будет использоваться для отправки уведомлений:



На этом поставленные цели достигнуты, задача решена.

воскресенье, 2 августа 2015 г.

Проксирование запросов к PostgreSQL через PgBouncer

Введение

Большинство серверов со сложной логикой имеет многопроцессную или многопотоковую архитектуру. При этом каждый процесс или поток занимается обслуживанием либо одного клиента, либо одного запроса. Не исключение тут и PostgreSQL, который имеет многопроцессную архитектуру, в которой процессы взаимодействуют между собой через разделяемую область памяти. Недостатки подобной архитектуры давно известны.

Во-первых, при установлении подключения создаётся новый процесс или поток. Это не бесплатная операция и она потребляет ресурсы. Создание нового потока требует меньше ресурсов, чем создание нового процесса, но так или иначе ресурсы всё равно потребляются. Если часто устанавливать соединения и закрывать их, процессы будут создаваться и закрываться. Чтобы как-то смягчить последствия частых подключений и отключений, в серверах часто прибегают к повторному использованию однажды порождённого процесса или потока, для чего ненужные процессы или потоки не уничтожаются сразу, а на некоторое время остаются "на подхвате".

Подобные проблемы в отношении баз данных особенно часто возникают при использовании CGI-приложений. Схема работы CGI приложений такова, что для обслуживания одного клиентского запроса программа должна установить необходимые подключения к базам данных и различным сервисам, считать необходимые файлы, затем обработать один запрос клиента и завершиться. При обслуживании каждого запроса вся процедура повторяется с начала, включая повторное установление подключений к базам данных.

Во-вторых, каждый процесс или поток, созданный для обслуживания одного пользователя или одного запроса, держит ресурсы, даже если не осуществляет никакой активной работы. Веб-приложения, работающие по протоколу FastCGI или ему подобному (SCGI, uWSGI), не устанавливают новые подключения к базе данных каждый раз при получении нового запроса. Вместо этого подобные приложения запускаются единожды, открывают необходимые подключения и загружают необходимые ресурсы, а затем последовательно обрабатывают поступающие запросы. Такая схема работы избавляет от недостатков CGI, но порождает другой недостаток - однажды запущенное приложение FastCGI держит открытыми подключения к базе данных, даже если не обслуживает никаких запросов.

Веб-серверы Lighttpd и nginx стали популярны во многом благодаря тому, что позволили нивелировать недостаток веб-сервера Apache как сервера приложений. Apache построен по гибридной многопроцессной и многопоточной архитектуре, но поддерживает повторное использование процессов и потоков. Поэтому для Apache характерен второй недостаток. Когда есть много медленных клиентов, выходящих в интернет через модем или GPRS, Apache быстро обрабатывает запрос клиента, а потом медленно-медленно отдаёт ему результат. Пока результат отдаётся клиенту процесс или поток практически ничем не заняты, но продолжают удерживать ресурсы, которые могли бы пригодиться для обработки запросов от других пользователей.

И тут на сцене появились Lighttpd и nginx, которые сочетают в себе две функции - отдачу файлов с диска и проксирование запросов. Кстати, эти веб-серверы были не первыми, до них были boa, thttpd, mathopd, которые были построены по той же архитектуре - мультиплексирование запросов в пределах одного однопоточного процесса. Позже, для того чтобы равномерно нагружать все имеющиеся процессорные ядра, в некоторых из этих серверов (nginx, например) была добавлена возможность обработки соединений несколькими процессами. Так вот, эти веб-серверы, по сути, выполняют ровно две задачи. Первая - отдавать клиентам неизменяющиеся файлы в обход сервера приложений, вторая - быстро забрать у сервера приложения ответ, освободив тем самым ресурсы для обслуживания другого запроса, и затем отдавать этот ответ клиенту с той скоростью, с которой он готов его принимать.

Остаётся только догадываться о том, достигли бы эти серверы хоть какой-либо, не то что популярности, а вообще - известности, если бы разработчики Apache вовремя догадались встроить в него прокси. Разработчики веб-сервера Microsoft IIS до подобного решения догадались и встроили проксирование запросов в специальный драйвер http.sys.

Прошу прощения за столь долгое вступление, но оно очень полезно для того, чтобы описать, чем занимается pgbouncer и в чём его польза. Так вот, pgbouncer - это такой прокси для протокола PostgreSQL, каковым является nginx для протокола HTTP или его аналогов (FastCGI, SCGI, uWSGI). К сожалению, в случае с СУБД, мы имеем дело не с запросами без учёта состояния, а с полноценными сеансами, поэтому проксирование в данном случае является скорее трюком, нежели законным способом избавиться от описанных проблем. pgbouncer позволяет во-первых, уменьшить частоту порождения и уничтожения процессов PostgreSQL, занимающихся обслуживанием запросов пользователей, а во-вторых - уменьшить простой однажды порождённого процесса, заставляя один процесс обрабатывать запросы, которые раньше обрабатывались бы несколькими процессами.

Установка и настройка pgbouncer

Итак, установим pgbouncer:
# apt-get install pgbouncer
Теперь займёмся его настройкой. Откроем файл /etc/pgbouncer/pgbouncer.ini и впишем в секцию databases следующие настройки:
zabbix = host=localhost dbname=zabbix user=zabbix
redmine_default = host=localhost dbname=redmine_default user=redmine_default
В секции pgbouncer пропишем такие настройки:
auth_type = md5
pool_mode = transaction
Первая настройка указывает, что при аутентификации pgbouncer'а на сервере будет использоваться учётная запись с хэшированным по алгоритму md5 паролем. За неимением лучших вариантов приходится пользоваться md5. Вторая настройка говорит, что в пределах одного и того же подключения к серверу должна выполняться транзакция. Вариант session не позволит нам сэкономить ресурсы на простаивающих соединениях, а лишь решит проблему с частым порождением и уничтожением процессов, происходящих при обслуживании запросов веб-интерфейсом Zabbix. Вариант statement предписывает выполнять в пределах одного и того же процесса только один запрос, но это может привести к несогласованным изменениям данных. Транзакция может включать в себя несколько запросов, которые изменяют содержимое базы данных согласованным образом. Для достижения согласованности изменений нужно чтобы при ошибке в одном из запросов ни один из запросов не изменил содержимое базы данных, а содержимое было бы изменено только в том случае, если все запросы внутри транзакции успешно выполнились.

Далее, при желании, можно настроить максимальное количество разрешённых подключений от клиентов, количество подключений в пуле, таймауты подключений в пуле и т.п. Если pgbouncer будет принимать подключения от нелокальных клиентов, нужно задать прослушиваемый IP-адрес в директиве listen_addr.

Теперь откроем файл /etc/pgbouncer/userlist.txt и впишем в него имена пользователей и их пароли, чтобы pgbouncer мог аутентифицироваться при установке подключений к СУБД и мог сам аутентифицировать подключающихся клиентов (на мой взгляд - хранить пароли ещё в одном месте весьма сомнительная затея, но разработчики, видимо, не предусмотрели возможность брать необходимые данные от клиентов и использовать их при при подключении к серверу):
"zabbix" "zabbix_password"
"redmine_default" "redmine_default_password"
Осталось разрешить запуск pgbouncer и запустить его. Для этого откроем файл /etc/default/pgbouncer, пропишем в опцию START значение 1 и запустим его:
# systemctl start pgbouncer
По умолчанию pgbouncer ожидает подключений на порту 6432, то есть номер порта на тысячу больше, чем стандартный порт PostgreSQL. В случае локальных подключений клиент умеет автоматически использовать Unix-сокет, находящийся в каталоге /var/run/postgresql/ и на этот случай pgbouncer создаёт в этом каталоге свой Unix-сокет, который содержит в своём имени номер прослушиваемого TCP-порта.

Теперь осталось перенастроить приложения на использование pgbouncer.

Перенастройка сервера Zabbix

Начнём с Zabbix-сервера. Откроем файл /etc/zabbix/zabbix_server.conf и впишем номер порта pgbouncer'а:
DBPort=6432
Перезапустим Zabbix-сервер:
# systemctl restart zabbix-server
Перенастройка веб-интерфейса Zabbix

Теперь перенастроим веб-интерфейс Zabbix. Откроем файл /etc/zabbix/web/zabbix.conf.php и пропишем номер порта:
$DB['PORT']     = '6432';
Перезапустим php5-fpm, чтобы настройки веб-приложения вступили в силу (не уверен что это необходимо):
# systemctl restart php5-fpm
Перенастройка Redmine

Теперь настала очередь Redmine. Тут всё оказалось не совсем просто. Дело в том, что в Rails-приложениях активно используются заготовленные запросы. Что это такое? Это такой шаблон запроса, в котором вместо конкретных значений указаны символы подстановки. СУБД может заранее разобрать такой запрос, пропустить через оптимизатор и составить план выполнения запроса. Затем подготовленный запрос можно использовать многократно, указывая конкретные значения, которые заменят символы подстановки из подготовленного запроса. При этом СУБД экономит время, не повторяя этапы синтаксического разбора, оптимизации и составления плана выполнения запроса, выполняя эту обработку единожды при подготовке запроса, а затем многократно используя её результаты при каждом выполнении запроса.

Оказалось, что использование подготовленных запросов можно отключить через файл /etc/redmine/default/database.yml, содержащий настройки подключения Redmine к базе данных. Соответствующую опцию я нашёл по ссылке Configuring Rails Applications, 3.14.3 Configuring a PostgreSQL Database. Итак, редактируем файл /etc/redmine/default/database.yml, прописав в нём номер порта и опцию prepared_statements:
production:
  adapter: postgresql
  database: redmine_default
  host: localhost
  port: 6432
  username: redmine_default
  password: redmine_default_password
  encoding: utf8
  prepared_statements: false
Осталось перезапустить Redmine:
# systemctl restart uwsgi

Стоит отметить, что отключение подготовленных запросов может отрицательно сказаться на производительности Redmine. Но в моём случае Redmine используется очень редко, поэтому мне важнее не улучшить его производительность, а снизить потребление ресурсов системы. Это будет достигнуто за счёт того, что pgbouncer будет устанавливать подключение к PostgreSQL только в те моменты, когда кто-то будет пользоваться Redmine. Если же им никто не пользуется, то спустя некоторое время соединение к СУБД будет закрыто и ресурсы, удерживавшиеся процессом PostgreSQL, будут возвращены системе.
Итоги

Результат произведённых действий можно лицезреть на графике, отображающем количество процессов в системе до и после внедрения pgbouncer:

Кроме PostgreSQL на этом компьютере работает ещё масса всяких программ, но результат уменьшения количества процессов PostgreSQL заметен даже на общем фоне.