Показаны сообщения с ярлыком postgresql. Показать все сообщения
Показаны сообщения с ярлыком postgresql. Показать все сообщения

воскресенье, 8 декабря 2019 г.

Резервное копирование файлов и баз данных через SSH

Когда-то давно на работе задумались о резервном копировании серверов. У меня к тому времени уже был наработанный подход к резервному копированию, которым пользуются многие системные администраторы Unix.

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

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

Управление архивом

Начнём с того, что нужно организовать резервные копии в хранилище таким образом, чтобы можно было легко найти нужную резервную копию и удалять устаревшие резервные копии. Файлы будем именовать по схеме YYYYMMDD_filename. При добавлении новой резервной копии filename нужно будет удостовериться, что новый файл не пуст. Если с файлом всё в порядке, то можно снабдить его префиксом YYYYMMDD_, а затем найти и удалить устаревшие резервные копии этого файла. Для выполнения этих функций в скрипте предусмотрены переменная KEEP_DAYS и функция с не самым удачным названием clear_old, которая берёт на себя описанные задачи:
KEEP_DAYS=14

clear_old()
{
# $1 - backup filename

# Если файл с указанным именем не существует
if [ ! -f "$1" ]
then
  return 0
fi

size=`du --bytes "$1" | cut -f1`

# Если размер меньше 512 байт
if [ $size -lt 512 ]
then
  # Удаляем сам файл резервной, а старые резервные копии не трогаем
  rm $1
  return 1
else
  # Переименовываем новый файл резервной копии
  mv $1 `date "+%Y%m%d"`_$1
  
  # Удаляем предыдущие файлы старше KEEP_DAYS дней
  find . -name \*$1 -mtime +$KEEP_DAYS -delete
  return 0
fi
}
Чтобы добавить файл в архив, нужно вызвать функцию clear_old и передать ей имя добавляемого в архив файла.

Резервное копирование локальных файлов

Начнём с простого. Резервные копии нужно снимать не только с других компьютеров, но и с самого компьютера, на котором будет работать скрипт. В моём случае в разное время это был сервер FreeBSD и виртуальная машина с Linux. В дальнейшем скрипт был перемещён на выделенную виртуальную машину с Linux, которая выполняла только функции резервного копирования. Острая необходимость снимать резервные копии самой виртуальной машины, где работал скрипт резервного копирования, отпала, но функции для этого уже были разработаны, поэтому на всякий случай резервные копии локальных файлов продолжают сниматься.

В случае с FreeBSD функция резервного копирования файлов выглядела следующим образом:
BACKUP_USER=rbackup
BACKUP_GROUP=rbackup

freebsd_files_local()
{
# $1 - backup filename

touch "$1"
chown $BACKUP_USER:$BACKUP_GROUP "$1"
chmod 0600 "$1"

tar -cjf - -T- --exclude '*.sql.gz' --exclude 'etc/zabbix/xbackup/*' <<END 2>/dev/null > "$1"
/etc/
/usr/local/etc/
/root/
/usr/home/
/usr/local/www/
END

clear_old "$1"
if [ $? -ne 0 ]
then
  echo "Backing up local system to file $1 failed"
fi
}
Функция принимает один аргумент - имя файла, в котором нужно сохранить резервную копию файлов. Создаваемый файл архива будет иметь формат .tbz (или .tar.bz2).

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

В архив помещаются файлы из каталогов /etc/, /usr/local/etc/, /usr/local/www/, /root/ и /usr/home/, кроме файлов с расширением .sql.gz и файлов из каталога /etc/zabbix/xbackup/. По-сути, в архив сохраняются файлы конфигурации, файлы из домашних каталогов пользователей и файлы веб-приложений, за исключением файлов с резервными копиями баз данных.

После создания архива для файла вызывается функция clear_old, которая переименовывает файл, снабжая его имя префиксом YYYYMMDD_ с текущей датой, и удаляет устаревшие экземпляры этого файла.

В переменных BACKUP_USER и BACKUP_GROUP выше указаны пользователь и группа с именем rbackup. Чтобы создать их, можно воспользоваться такими командами:
# pw add group rbackup
# pw add user rbackup -g rbackup -c "User for backup purposes" -d /usr/home/rbackup -m
В случае с Linux функция резервного копирования файлов была такой:
linux_files_local()
{
# $1 - backup filename

touch "$1"
chown $BACKUP_USER:$BACKUP_GROUP "$1"
chmod 0600 "$1"

tar -cjf - --files-from=- --exclude 'home/*/.pycharm_helpers/*' --exclude 'root/.cpan/*' --exclude 'root/.cache/*' --exclude 'home/*/.cache/*' --exclude 'usr/local/lib/*' <<END 2>/dev/null > "$1"
/etc/
/root/
/home/
/usr/local/
/usr/lib/zabbix/
/usr/share/mapnik/
/var/www/
/var/lib/dokuwiki/
END

clear_old "$1"
if [ $? -ne 0 ]
then
  echo "Backing up local system to file $1 failed"
fi
}
В целом эта функция не отличается от функции для FreeBSD. Резервному копированию подвергаются каталог с файлами конфигурации, домашние каталоги пользователей, каталог веб-приложений, а также каталог /usr/local/, и каталоги с файлами Zabbix и DokuWiki. Из резервного копирования исключаются файлы, создаваемые средой разработки PyCharm (она умеет работать по SSH), каталоги с кэшированными данными, модулями Perl.

Аналогично FreeBSD, в Linux нужно создать пользователя и группу rbackup, который будут использоваться в качестве владельца резервных копий:
# groupadd rbackup
# useradd -c "User for backup purposes" -d /home/rbackup -m -g rbackup rbackup

Резервное копирование файлов с удалённых систем

Резервное копирование удалённых файловых систем работает аналогично, с той лишь разницей, что команды резервного копирования запускаются по SSH, а их стандартный вывод сохраняется в файл в локальной файловой системе.
SSH_PRIVKEY=/root/.ssh/id_rsa

freebsd_files()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - backup filename

touch "$3"
chown $BACKUP_USER:$BACKUP_GROUP "$3"
chmod 0600 "$3"

ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "/usr/local/bin/sudo /usr/bin/tar -cjf - -T- --exclude '*.sql.gz' --exclude 'etc/zabbix/xbackup/*'" <<END 2>/dev/null > "$3"
/etc/
/usr/local/etc/
/root/
/usr/home/
/usr/local/www/
END
  
clear_old "$3"
if [ $? -ne 0 ]
then
  echo "Backing up remote system $1:$2 to file $3 failed"
fi
}
Функции передаются три аргумента:
  1. IP-адрес или доменное имя удалённой системы, резервную копию файлов с которой нужно снять,
  2. порт SSH-сервера на этой системе (было время, когда использовались системы с SSH-сервером на нестандартном порту),
  3. имя файла создаваемого архива.
Файлы, подлежащие резервному копированию, без особых проблем сможет прочитать только пользователь root. Однако, в целях безопасности, не хотелось бы разрешать удалённый доступ по SSH для пользователя root, пусть аутентификация и производится с использованием ключей. Не хочется также разрешать скрипту резервного копирования выполнять по SSH какие угодно команды. Для того, чтобы скрипт резервного копирования не смог повредить систему, случайно - из-за ошибки администратора, или специально - если доступ к скрипту получил злоумышленник, резервное копирование производится с использованием учётной записи из переменной BACKUP_USER и sudo.

Для копирования публичного SSH-ключа в домашний каталог пользователя из переменной BACKUP_USER на FreeBSD я пользовался такими командами, которые просто копировал в терминал при настройке нового сервера или виртуальной машины:
# chown rbackup:rbackup /usr/home/rbackup
# mkdir /home/rbackup/.ssh
# chown rbackup:rbackup /usr/home/rbackup/.ssh
# cat <<END > /usr/home/rbackup/.ssh/authorized_keys
ТУТ ПУБЛИЧНЫЙ SSH-КЛЮЧ
END
# chown rbackup:rbackup /usr/home/rbackup/.ssh/authorized_keys
Для того, чтобы разрешить пользователю rbackup запускать через sudo команду tar для резервного копирования файлов, я использовал запускал visudo и вставлял такие настройки:
Defaults:rbackup !requiretty
rbackup    ALL=(root:ALL) NOPASSWD:/usr/bin/tar -cjf - -T- *
Для Linux аналогичная функция выглядит следующим образом:
linux_files()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - backup filename

touch "$3"
chown $BACKUP_USER:$BACKUP_GROUP "$3"
chmod 0600 "$3"

ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "/usr/bin/sudo /bin/tar -cjf - --files-from=- --exclude 'home/*/.pycharm_helpers/*' --exclude 'root/.cpan/*' --exclude 'root/.cache/*' --exclude 'home/*/.cache/*' --exclude 'usr/local/lib/*'" 2>/dev/null <<END > "$3"
/etc/
/root/
/home/
/usr/local/
/usr/lib/zabbix/
/usr/share/mapnik/
/var/www/
/var/lib/dokuwiki/
END

clear_old "$3"
if [ $? -ne 0 ]
then
  echo "Backing up remote system $1:$2 to file $3 failed"
fi
}
Для настройки публичных SSH-ключей использовались аналогичные команды:
# mkdir /home/rbackup/.ssh
# chown rbackup:rbackup /home/rbackup/.ssh
# cat <<END > /home/rbackup/.ssh/authorized_keys
ТУТ ПУБЛИЧНЫЙ SSH-КЛЮЧ  
END
# chown rbackup:rbackup /home/rbackup/.ssh/authorized_keys
Для настройки прав в sudo использовались такие строчки:
Defaults:rbackup !requiretty
rbackup ALL=(root:ALL) NOPASSWD:/bin/tar -cjf - --files-from=- *

Резервное копирование удалённой базы данных MySQL

MySQL является сетевым сервером, поэтому резервные копии баз данных можно снимать по сети, не прибегая к помощи SSH. Однако, для того, чтобы не заниматься настройкой фильтрации пакетов, а также не гонять по сети лишний объём данных в открытом виде, резервное копирование баз данных было решено выполнять тоже через SSH.
PASSWORD=тут-пароль

generic_mysql()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - database name
# $4 - backup filename

touch "$4"
chown $BACKUP_USER:$BACKUP_GROUP "$4"
chmod 0600 "$4"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "mysqldump --single-transaction -u$BACKUP_USER -p$PASSWORD $3 | bzip2" 2>/dev/null > "$4"
  
clear_old "$4"
if [ $? -ne 0 ]
then
  echo "Backing up $3 database from system $1:$2 to file $4 failed"
fi
}
Функция принимает 4 аргумента:
  1. IP-адрес или доменное имя удалённой системы, резервную копию файлов с которой нужно снять,
  2. порт SSH-сервера на этой системе (было время, когда использовались системы с SSH-сервером на нестандартном порту),
  3. имя базы данных, резервную копию которой нужно снять,
  4. имя файла создаваемого архива.
Функция вызывает mysqldump для получения резервной копии требуемой базы данных, а для сжатия данных перед отправкой через сеть используется компрессор bzip2. Соответственно, резервная копия, которая будет получена при помощи этой функции, имеет формат .sql.bz2.

Для снятия резервной копии используется пользователь с тем же именем, который является владельцем архивов и используется для подключения к удалённым системам по SSH. Его имя настроено в переменной BACKUP_USER. А для аутентификации этого пользователя используется пароль, указанный в переменной PASSWORD. Понятно, что этот пользователь должен создан и ему должны быть предоставлены права доступа к указанным базам данных. Для этого можно воспользоваться такими запросами:
CREATE USER 'rbackup'@'localhost' IDENTIFIED BY 'тут-пароль';
FLUSH PRIVILEGES;
GRANT SHOW DATABASES, SELECT, LOCK TABLES, RELOAD ON *.* TO 'rbackup'@'localhost';
FLUSH PRIVILEGES;
Т.к. в функции не используется каких-то специфичных путей к файлам и нет необходимости указывать полный путь к команде, выполняемой через sudo, то эта функция пригодна для использования и с FreeBSD и с Linux.

Резервное копирование удалённой базы данных PostgreSQL

Функция резервного копирования базы данных PostgreSQL в целом аналогична функции для резервного копирования базы данных MySQL. Для снятия резервной копии используется утилита pg_dump:
generic_pgsql()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - database name
# $4 - backup filename

touch "$4"
chown $BACKUP_USER:$BACKUP_GROUP "$4"
chmod 0600 "$4"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "env PGPASSWORD=$PASSWORD pg_dump -U$BACKUP_USER $3 | bzip2" > "$4" 2>/dev/null

clear_old "$4"
if [ $? -ne 0 ]
then
  echo "Backing up $3 database from system $1:$2 to file $4 failed"
fi
}
Для снятия резервной копии нужно создать пользователя, указанного в переменной BACKUP_USER, и предоставить ему права доступа ко всем базам данных, резервные копии которых будет необходимо снимать. Войдя в систему под пользователем postgres, создаём пользователя для резервного копирования и вводим его пароль в процессе его создания:
$ createuser -D -R -I -S -P rbackup
Теперь нужно подключиться к каждой из баз данных при помощи команды psql -d база-данных и выполнить следующие запросы:
GRANT SELECT ON ALL TABLES IN SCHEMA public TO rbackup;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO rbackup;

Группировка действий по резервному копированию

Для снятия всех необходимых резервных копий с одного сервера можно создавать функции следующего вида:
backup_server()
{
linux_files 'server.domain.tld' 22 'server.tbz'

generic_mysql 'server.domain.tld' 22 'mysql' 'server_mysql.sql.bz2'
generic_mysql 'server.domain.tld' 22 'base' 'server_base.sql.bz2'

generic_pgsql 'server.domain.tld' 22 'database' 'server_database.sql.bz2'
}

Выполнение резервного копирования

Теперь в скрипте имеются функции для снятия резервных копий каждого сервера. Осталось только создать каталог для хранения резервных копий и выполнить резервное копирование. Для этого создадим каталог и выставим права доступа к нему:
# mkdir /backups
# chown rbackup:rbackup /backups
# chmod u=rwx,g=rx,o= /backups
Теперь впишем в скрипт команды перехода в каталог с резервными копиями, на всякий случай поменяем маску создаваемых файлов и последовательно вызовем функции резервного копирования всех требуемых серверов:
cd /backups
umask 0077

backup_server1
backup_server2

Резервное копирование резервных копий

Не стоит складывать все яйца в одну корзину. На случай, если с резервными копиями в основном месте хранения что-нибудь случится, можно выполнить резервное копирование резервных копий на другой сервер. Для этого на другом сервере создаётся аналогичный каталог, в который при помощи rsync синхронизируются изменения из каталога на основном сервере:
/usr/bin/rsync -a --delete-after -e "ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=22 -i $SSH_PRIVKEY" /backups/ $BACKUP_USER@backup-server.domain.tld.:/backups/
Один процесс rsync запускается локально, один процесс rsync запускается удалённо по SSH. Оба процесса обмениваются друг с другом информацией через SSH. Взаимодействуя друг с другом, они копируют недостающие файлы, обновляют фрагменты изменившихся файлов, удаляют файлы, ставшие не нужными.

Отладочная информация

Для того, чтобы иметь представление о времени начала и завершения тех или иных этапов резервного копирования, можно добавлять в функции или в тело скрипта отладочные сообщения с отметками времени. Например, в свой скрипт я вставил отладочные сообщения, фиксирующие моменты начала и завершения обновления основного архива и моменты начала и завершения синхронизации резервного архива:
echo "BACKUP FINISHED: "`date "+%Y-%m-%d %H:%M:%S"`
# тут резервное копирование серверов
echo "BACKUP STARTED: "`date "+%Y-%m-%d %H:%M:%S"`
  
echo "RSYNC STARTED: "`date "+%Y-%m-%d %H:%M:%S"`
# тут вызов rsync
echo "RSYNC FINISHED: "`date "+%Y-%m-%d %H:%M:%S"`

Заключение

Почти каждому системному администратору Unix когда-нибудь приходилось писать свой вариант такого скрипта. Буду рад, если вы поделитесь собственными советами и наработками. Возможно кто-то может поделиться соображениями, в каких случаях такой подход не годится, и какие решения лучше использовать взамен.

воскресенье, 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 заметен даже на общем фоне.

воскресенье, 26 июля 2015 г.

Миграция Redmine с MySQL на PostgreSQL

На этот раз рассмотрим перенос данных из MySQL в PostgreSQL, пригодный, пожалуй, для любых приложений, предоставляющих выбор из этих двух СУБД.

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

Рассмотрим эту стратегию миграции на примере Redmine.

Получение схемы базы данных

Схема базы данных - это её структура, то есть таблицы, индексы, ключи, без данных.

Установим дополнительно пакет, отвечающий за работу Redmine совместно с СУБД PostgreSQL:
# apt-get install redmine-pgsql
Для начала перейдём в каталог /etc/redmine/, в котором есть подкаталоги, соответствующие экземплярам redmine:
# cd /etc/redmine/
Скопируем каталог одного из экземпляров в другой каталог, создав таким образом новый экземпляр. В данном случае копируем экземпляр default и создаём на его основе экземпляр pgsql:
# rsync -rogp default/ pgsql/
Открываем файл /etc/redmine/pgsql/database.yml и заменяем в нём имя пользователя в поле username, имя базы данных в поле database и тип базы данных в поле adapter. У меня получился такой файл:
production:
  adapter: postgresql
  database: redmine_pgsql
  host: localhost
  port:
  username: redmine_pgsql
  password: password
  encoding: utf8
Теперь нужно создать соответствующего пользователя и базу данных:
# su - postgres
$ createuser -P redmine_pgsql
$ createdb -E UTF-8 -O redmine_pgsql redmine_pgsql
$ exit
Пустая база данных и пользователь созданы. Теперь нам нужно наполнить эту базу данных структурой и первичными данными. Для этого переходим в каталог /usr/share/redmine/, в котором находится Redmine, и запускаем команду миграции экземпляра pgsql, которая сделает всё необходимое:
# cd /usr/share/redmine
# rake db:migrate RAILS_ENV=production X_DEBIAN_SITEID=pgsql
Теперь, если всё прошло успешно, создаём резервную копию этой базы данных, но без собственно данных:
# su - postgres
$ pg_dump -s -d redmine_pgsql > redmine_pgsql.sql
Теперь можно удалить этого пользователя и его базу данных, от них нам больше ничего не нужно:
$ dropdb redmine_pgsql
$ dropuser redmine_pgsql
$ exit
Теперь можно удалить и каталог с настройками экземпляра pgsql:
# cd /etc/redmine
# rm -R pgsql
Как стало ясно в процессе дальнейших попыток воспользоваться дампом, его нужно откорректировать:
  • Удалить комментарии и пустые строки, т.к. pgloader ожидает, что в каждой строчке будет указан какой-то запрос, который что-то изменяет,
  • Удалить запрос, добавляющий описание языка PL/PgSQL,
  • Удалить двойные кавычки вокруг имён полей, совпадающих с зарезервированными словами,
  • Сменить владельца базы данных с redmine_pgsql на владельца базы данных, которую будем переносить из MySQL в PostgreSQL.
Сделать это можно вот так:
$ cat redmine_pgsql.sql | sed -e 's/^--.*$//g; s/^COMMENT .*$//; /^$/d; s/"//g; s/TO redmine_pgsql/TO redmine_default/g' > redmine_default.sql
Теперь нужно поделить файл redmine_default.sql на две части. Первый должен создать структуру базы данных без ограничений и внешних ключей, а второй - добавлять их. К счастью, стандартный дамп, созданный pg_dump, можно легко поделить на нужные нам части. Первая часть заканчивается запросами вида "ALTER TABLE ONLY ... ALTER COLUMN ... SET DEFAULT ...", а вторая часть начинается запросами вида "ALTER TABLE ONLY ... ADD CONSTRAINT ...". В результате должны получиться файлы redmine_default1.sql и redmine_default2.sql

Приведу ссылки на эти файлы: redmine_default1.sql и redmine_default2.sql Они соответствуют версии Remine, поставляющейся в репозиториях Debian Jessie (версии пакетов - 3.0~20140825-5). Если у вас именно эта версия Redmine, можно пропустить весь этот раздел и сразу воспользоваться этими файлами.

Собственно перенос данных

Если pgloader ещё не установлен, установим его:
# apt-get install pgloader
Теперь создадим файл redmine_default.sql с настройками миграции:
LOAD DATABASE
  FROM mysql://redmine_default:password@localhost/redmine_default
  INTO postgresql://redmine_default:password@localhost/redmine_default

WITH include no drop,
     truncate,
     create no tables,
     create no indexes,
     no foreign keys,
     reset sequences,
     data only

SET maintenance_work_mem TO '128MB',
    work_mem to '12MB'

BEFORE LOAD EXECUTE redmine_default1.sql
Теперь отключаем Redmine, чтобы данные не менялись в процессе переноса в новую СУБД:
# /etc/init.d/uwsgi stop redmine
Открываем файл с настройками подключения к базе данных мигрируемого экземпляра default. В данном случае это файл /etc/redmine/default/database.yml, который в данный момент настроен на использование MySQL. Нужно создать точно такого же пользователя и базу данных в PostgreSQL:
# su - postgres
$ createuser -P redmine_default
$ createdb -E UTF-8 -O redmine_default redmine_default
$ exit
Запускаем миграцию:
$ pgloader redmine_default.load
После миграции открываем снова файл с настройками подключения к базе данных мигрируемого экземпляра default. Это файл /etc/redmine/default/database.yml, заменяем в нём тип базы данных с mysql2 на postgresql.

Теперь можно запустить uwsgi, чтобы Redmine вновь стал доступен для пользователей. Тестируем, всё ли правильно работает. Дальше можно перенести другие экземпляры Redmine и по окончании миграции удалить пакет Redmine для работы с MySQL:
# apt-get purge redmine-mysql

воскресенье, 12 июля 2015 г.

Миграция Zabbix с MySQL на PostgreSQL

Периодически замечал в iotop на своём домашнем компьютере, что самую высокую нагрузку по вводу-выводу создаёт MySQL. В интернете встречал мнения, что PostgreSQL по сравнению с MySQL более стабильно ведёт себя в условиях дефицита производительности дисковой подсистемы. Больше всего MySQL на этом компьютере нагружался Zabbix'ом, поэтому ради эксперимента решил попробовать перевести Zabbix на использование PostgreSQL.

Довольно долго я пытался экспортировать данные из MySQL при помощи штатного инструмента mysqldump в виде, пригодном для последующего экспорта в PostgreSQL. У этой утилиты имеется опция, позволяющая экспортировать в формате, совместимом с PostgreSQL. Несколько дополнительных опций, чтобы отключить директивы, отключить создание таблиц, добавить явные имена колонок в запросы INSERT, позволили получить результат с виду пригодный для импорта в PostgreSQL.

Тут, однако, возникла проблема. Данные таблиц в дампе шли в порядке, соответствюущем алфавитному порядку имён таблиц. Это вызывало проблемы с обработкой внешних ключей. Почему-то на тот момент я не догадался заглянуть в файл scheme.sql и создавал схему базы данных сразу с ограничениями и внешними ключами. Для решения этой проблемы я написал скрипт, который брал информацию о внешних ключах из базы данных information_schema и сортировал таблицы так, чтобы ссылающиеся таблицы шли после тех, на которые они ссылаются. Экспортировал данные я именно в этом порядке.

Однако когда проблема с внешними ключами была решена не самым простым путём, всплыло несколько досадных мелочей:
  • в полученном дампе внутри одинарных кавычек двойные кавычки экранировались обратным слэшем, в то время как PostgreSQL принимал их без экранирования,
  • одинарные кавычки в тех же строках тоже экранировались обратным слэшем, но PostgreSQL ждал, что они просто будут продублированы,
  • двоичные данные сохранялись в виде закавыченной последовательности байтов, в то время как PostgreSQL ждал двоичные данные в виде шестнадцатеричных цифр с префиксом 0x.
Попытки воспользоваться sed'ом для исправления недостатков не привели к успеху и я решил попробовать pgloader.

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

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

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

Установим СУБД, если она ещё не установлена:
# apt-get install postgresql
Установим пакет pgloader, с помощью которого будем переносить содержимое базы из MySQL в PostgreSQL:
# apt-get install pgloader
Доустановим пакет для работы веб-интерфейса с СУБД PostgreSQL:
# apt-get install php5-pgsql
Чтобы новый модуль можно было использовать из PHP, перезапустим php5-fpm:
# systemctl restart php5-fpm
2. Подготовка СУБД

Правим файл конфигурации аутентификации пользователей /etc/postgresql/9.4/pg_hba.conf, заменяя первую строку на вторую:
local   all             all                                     peer
local   all             all                                     md5
По умолчанию, при подключении к UNIX-сокету, PostgreSQL определяет учётную запись, под которой работает подключившийся процесс и автоматически создаёт подключение от имени одноимённого пользователя из СУБД. Пароль при этом не запрашивается. Меняя эту строчку, мы будем требовать у подключившегося процесса явным образом указать имя пользователя СУБД и его пароль.

После этого перезапустим сервер базы данных, чтобы новые настройки вступили в силу:
# systemctl restart postgresql
Теперь из сеанса пользователя root заходим под пользователем postgres:
# su - postgres
От имени пользователя postgres создаём пользователя базы данных с именем zabbix:
$ createuser -P zabbix
Ключ -P означает, что будет запрошен пароль нового пользователя.

От имени пользователя postgres создаём саму базу данных с именем zabbix, владеть которой будет только что созданный пользователь с именем zabbix:
$ createdb -E UTF-8 -O zabbix zabbix
Опция -E UTF-8 означает, что текстовая информация в базе данных будет храниться в кодировке UTF-8, а опция -O задаёт пользователя, который будет владельцем базы данных.

3. Подготовка к переносу данных

Теперь возьмём файл database/postgresql/schema.sql, имеющийся в дистрибутиве Zabbix и поделим его на две части. В первой части будут запросы, создающие таблицы (CREATE TABLE), а во второй - создающие внешние ключи и ограничения (ALTER TABLE). Назовём эти файлы schema1.sql и schema2.sql

Создадим файл zabbix.load, содержащий настройки для переноса данных:
LOAD DATABASE
  FROM mysql://zabbix:zabbix_password@localhost/zabbix
  INTO postgresql://zabbix:zabbix_password@localhost/zabbix

WITH include no drop,
     truncate,
     create no tables,
     create no indexes,
     no foreign keys,
     reset sequences,
     data only

SET maintenance_work_mem TO '128MB',
    work_mem to '12MB'

BEFORE LOAD EXECUTE schema1.sql

AFTER LOAD EXECUTE schema2.sql;
Подготовим пакет с Zabbix-сервером, работающим с PostgreSQL или подключим репозиторий с этим пакетом. Как это сделать - решайте сами.

4. Перенос данных

Перед переносом данных остановим Zabbix-сервер:
# systemctl stop zabbix-server
Закроем доступ к веб-интерфейсу Zabbix, остановив php5-fpm (можно просто запретить доступ к веб-интерфейсу Zabbix через настройки nginx):
# systemctl stop php5-fpm
Теперь приступим к собственно переносу данных (в текущем каталоге должны быть подготовленные ранее файлы schema1.sql, schema2.sql, zabbix.load):
$ pgload zabbix.load
Пока данные переносятся, удалим старый Zabbix-сервер для MySQL:
# dpkg -r zabbix-server-mysql
Установим Zabbix-сервер для PostgreSQL :
# dpkg -i zabbix-server-pgsql_2.4.5-1+jessie_amd64.deb
Теперь, если данные уже перенеслись, можно запускать новый Zabbix-сервер:
# systemctl start zabbix-server
Отредактируем файл /etc/zabbix/web/zabbix.conf.php, заменив первую строчку на вторую:
$DB['TYPE']     = 'MYSQL';
$DB['TYPE']     = 'POSTGRESQL';
Запускаем php5-fpm (или открываем доступ к веб-интерфейсу Zabbix через настройки nginx):
# systemctl start php5-fpm
5. Проверка результата

Заглядываем в журналы Zabbix-сервера /var/log/zabbix/zabbix_server.log и заходим в веб-интерфейс Zabbix, проверяя, что всё работает нормально.

Кстати, наблюдение за нагрузкой на дисковую подсистему по такому не совсем чёткому параметру как iowait, показало, что нагрузка действительно упала:

воскресенье, 19 августа 2012 г.

Тайловый сервер - что с ним делать?

В прошлых заметках Подготовка карт для генератора тайлов Mapnik, Настройка базы данных для генератора тайлов Mapnik, Установка генератора тайлов Mapnik и Установка renderd и mod_tile - системы отрисовки тайлов по запросу мы настроили обычный тайловый сервер. Однако, от одного лишь просмотра карт толку мало - ведь то же самое без лишних усилий можно увидеть и на официальном сайте OpenStreetMap.

1. Добавление локальной информации

Для того, чтобы на карту можно было вносить локальную информацию, можно настроить локальный веб-сайт OpenStreetMap и подключаясь к нему с помощью редактора JOSM, редактировать имеющуюся информацию. Я пробовал устанавливать Ruby и Rails и мне даже удалось запустить локальный веб-сайт, однако работал он очень медленно, а ускорить его работу мне не удалось - не хватило знаний Ruby on Rails и времени, чтобы в нём разобраться.

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

Поэтому я выбрал другой вариант - локальные данные будут храниться в OSM-файле, который можно редактировать с помощью уже знакомого нам редактора JOSM. Локальной информацией может быть, например, информация с расположением торговых точек в случае магазина, с расположением банкоматов в случае банка, платёжных терминалов, телефонов-автоматов, wifi-точек, зон ответственности развозчиков пиццы и т.п.

Этот OSM-файл сразу после редактирования можно импортировать в отдельную базу данных. А для того, чтобы Mapnik отображал информацию из локальной базы данных, нужно добавить в файл стилей /etc/mapnik-osm-data/osm.xml настройки для подключения к новой базе данных и написать стиль отрисовки объектов из неё.

Документацию по написанию файлов стилей можно найти здесь: Mapnik configuration XML.

Вот пример фрагмента файла osm.xml, в котором задаётся стиль отображения некоего зонального деления территорий, берущегося из базы данных zones:
<Style name="zones">
  <Rule>
    &maxscale_zoom0;
    &minscale_zoom10;
  </Rule>

  <Rule>
    &maxscale_zoom9;
    &minscale_zoom11;
    <Filter>not ([name] = '') and [area] = 'yes'</Filter>
    <LineSymbolizer stroke="#000000" stroke-opacity="1" stroke-width="1" stroke-linecap="round"/>
  </Rule>

  <Rule>
    &maxscale_zoom12;
    &minscale_zoom13;
    <Filter>not ([name] = '') and [area] = 'yes'</Filter>
    <LineSymbolizer stroke="#000000" stroke-opacity="1" stroke-width="2" stroke-linecap="round"/>
    <TextSymbolizer size="10" allow-overlap="yes" fill="#000000" fontset-name="book-fonts" opacity="1" placement="interior">[name]</TextSymbolizer>
  </Rule>

  <Rule>
    &maxscale_zoom14;
    &minscale_zoom19;
    <Filter>not ([name] = '') and [area] = 'yes'</Filter>
    <LineSymbolizer stroke="#000000" stroke-opacity="1" stroke-width="4" stroke-linecap="round"/>
    <TextSymbolizer size="20" allow-overlap="yes" fill="#000000" fontset-name="book-fonts" opacity="1" placement="interior">[name]</TextSymbolizer>
  </Rule>
</Style>

<Layer name="zones" status="on" srs="&srs900913;">
  <StyleName>zones</StyleName>
  <Datasource>
    <Parameter name="table">(select * from planet_osm_polygon) as zones</Parameter>
    <Parameter name="type">postgis</Parameter>
    <Parameter name="password">password</Parameter>
    <!-- <Parameter name="host">localhost</Parameter> -->
    <Parameter name="user">osm</Parameter>
    <Parameter name="dbname">zones</Parameter>
    <Parameter name="estimate_extent">false</Parameter>
    <Parameter name="extent">-20037508,-19929239,20037508,19929239</Parameter>
  </Datasource>
</Layer>
Кстати, этот фрагмент стиля является не самым оптимальным, но он первым пришёл мне в голову, а кроме того, он хорошо иллюстрирует возможности файла стилей.

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

Второй момент - запрос написан не оптимально, т.к. извлекает из таблицы все строки, а Mapnik будет рисовать только те объекты, которые удовлетворяют настройкам фильтра. Вместо этого можно дополнить запрос условием WHERE name IS NOT NULL AND name <> '' AND area = 'yes', а из описания стиля удалить все фильтры.

Третий момент - это настройка extent, в которой указаны границы всего мира, хотя, наверняка, локальные данные находятся в каких-то предсказуемых границах. Например, локальные данные в моём случае ограничиваются только Республикой Башкортостан, Республикой Татарстан и Оренбургской областью. Можно однажды выполнить следующий запрос:
SELECT ST_Extent(way)
FROM (SELECT ST_ConvexHull(ST_Extent(way)) AS way
FROM planet_osm_polygon
UNION SELECT ST_ConvexHull(ST_Extent(way)) AS way
FROM planet_osm_point
UNION SELECT ST_ConvexHull(ST_Extent(way)) AS way
FROM planet_osm_line
UNION SELECT ST_ConvexHull(ST_Extent(way)) AS way
FROM planet_osm_roads) AS ways;
и прописать в настройки extent возвращённые значения. Это позволит Mapnik'у не выполнять запросы к базе данных, если заведомо известно, что запрос не вернёт результатов для интересующей его области.

Более подробно о рекомендациях по оптимизации файла стилей Mapnik можно прочитать в статье Optimize Rendering with PostGIS.

2. Использование информации из базы данных

Кроме заливки информации в локальную базу данных, я также пользуюсь и основной базой данных Mapnik, непосредственно залезая в её недра с помощью SQL-запросов. Поэтому мне в её таблицах бывают нужны некоторые атрибуты объектов, которые по умолчанию не импортируются в базу данных утилитой osm2pgsql.

Чтобы указать дополнительные поля, нужно отредактировать файл стиля базы данных /usr/share/osm2pgsql/default.style

Например, я добавил в файл стиля базы данных колонки addr:city и addr:street, которые берутся из одноимённых атрибутов объектов из файла OSM:
node,way         addr:city           text
linearnode,way   addr:street         text  linear
node означает, что этот атрибут может быть назначен точке и должен быть импортирован в таблицу planet_osm_point.

way означает, что этот атрибут может быть назначен контуру (линии, дороге или многоугольнику) и должен быть импортирован в таблицу planet_osm_line, planet_osm_roads или planet_osm_polygon.

Теперь информацию из базы данных можно извлекать с помощью SQL-запросов, в чём особенно помогают различные функции PostGIS.

Вот лишь краткий список функций, которые оказались полезными для моих задач:

1. ST_AsText - возвращает геометрический объект в формате WKT (Well-known Text), описанный в стандартах OpenGIS.

2. ST_Transform - переводит координаты опорных точек геометрического объекта из одной проекции в указанную.

3. ST_GeomFromText - возвращает геометрический объект по его описанию в формате WKT и (опционально) заданной проекции.

4. ST_IsValid - проверяет правильность объекта - замкнутость многоугольника, отсутствие самопересечений и т.п.

5. ST_PointOnSurface - возвращает точку, находящуюся строго на поверхности объекта (многоугольника или мультиполигона, многоугольника с дырами - геометрического объекта, имеющего один внешний контур и произвольное количество внутренних контуров).

6. ST_ContainsProperly - функция, возвращающая "истину", если второй объект находится строго внутри первого. Достаточно, чтобы хотя-бы одна вершина второго объекта не попала внутрь первого, или попала в дыру первого объекта, чтобы функция вернула "ложь".

7. ST_Extent - агрегатная функция (работает подобно агрегатным функциям COUNT, MIN, MAX, SUM или AVG), возвращает геометрический объект BOX - прямоугольник, охватывающий выбранные геометрические объекты.

Для чего можно использовать эти функции? Приведу несколько примеров, иллюстрирующих, как их использую я.

Например, для того, чтобы удалить из таблицы planet_osm_polygon многоугольники с самопересечениями и просто многоугольники, имеющие какие-то ошибки, можно воспользоваться таким запросом:
DELETE FROM planet_osm_polygon
WHERE NOT ST_IsValid(way);
Или можно вернуть координаты точки на поверхности каждого дома из таблицы planet_osm_polygon в формате WKT в проекции WGS 84:
SELECT ST_AsText(ST_Transform(ST_PointOnSurface(way), 4326))
FROM planet_osm_polygon
WHERE building IS NOT NULL;
Или, например, найти, контур здания по точке внутри него:
SELECT way
FROM planet_osm_polygon
WHERE building IS NOT NULL
  AND ST_ContainsProperly(way, ST_Transform(ST_GeomFromText('POINT(48.2445263783448 55.8405766215408)', 4326), 900913));
Где 48.2445263783448 - долгота, 55.8405766215408 - широта.

Или вычислить прямоугольник, содержащий весь населённый пункт с указанным именем:
SELECT ST_Extent(ST_Transform(way, 4326))
FROM planet_osm_polygon
WHERE place IN ('city', 'town', 'village', 'hamlet')
  AND name = 'Салават';
Естественно, чтобы извлекать значения полей addr:city, addr:street, нужно их сначала добавить в файл стиля базы данных для утилиты osm2pgsql, а затем импортировать данные, что мы уже проделали в предыдущем пункте этой заметки. Правда, не всегда и везде проставляются значения этих полей, потому что для отрисовки карты Mapnik их никак не использует - поверх дома выводится только его номер.

Но некоторые поля можно проставить довольно просто. Например, чтобы проставить поле "addr:city" у всех домов, попадающих в административную границу какого-либо населённого пункта, я пользуюсь скриптом на Perl, часть которого приведена ниже:
# Перебираем населённые пункты, прописываем домам населённый пункт в поле addr:city
sub osm_fill_city()
{
  my $total = 0;
  my $sth_polygon = $dbh_o->prepare("UPDATE planet_osm_polygon
                                     SET \"addr:city\" = ?
                                     WHERE building IS NOT NULL
                                       AND (\"addr:city\" IS NULL OR \"addr:city\" = '')
                                       AND ST_ContainsProperly(ST_GeomFromText(?, 900913), way)");
  my $sth_point = $dbh_o->prepare("UPDATE planet_osm_point
                                   SET \"addr:city\" = ?
                                   WHERE building IS NOT NULL
                                     AND (\"addr:city\" IS NULL OR \"addr:city\" = '')
                                     AND ST_ContainsProperly(ST_GeomFromText(?, 900913), way)");
  my $sth_city = $dbh_o->prepare("SELECT name,
                                         ST_AsText(way)
                                  FROM planet_osm_polygon
                                  WHERE place IN ('city', 'town', 'village', 'hamlet')
                                     AND name IS NOT NULL
                                     AND name <> ''");
  $sth_city->execute();
  while (my ($name, $wkt) = $sth_city->fetchrow_array())
  {
    $sth_polygon->execute($name, $wkt);
    $sth_point->execute($name, $wkt);
    $total++;
    print "Простановка населённых пунктов на зданиях, всего обработано населённых пунктов: $total\r";
  }
  $sth_city->finish();
  $sth_point->finish();
  $sth_polygon->finish();
  print "Простановка населённых пунктов на зданиях, всего обработано населённых пунктов: $total\n";
}
Можно, конечно, не заниматься этим, а взять координаты или контур интересующего нас объекта и с помощью функции ST_ContainsProperly узнать, в административные границы какого населённого пункта этот объект попадает.

3. Геокодинг - поиск географических объектов

Эту информацию без дополнительной обработки можно использовать для обратного геокодинга, то есть для получения адреса здания по географическим координатам точки, попавшей в контур здания:
SELECT "addr:city", "addr:street", "addr:housenumber"
FROM planet_osm_polygon
WHERE building IS NOT NULL
  AND building <> ''
  AND "addr:city" IS NOT NULL
  AND "addr:city" <> ''
  AND "addr:street" IS NOT NULL
  AND "addr:street" <> ''
  AND "addr:housenumber" IS NOT NULL
  AND "addr:housenumber" <> ''
  AND ST_ContainsProperly(way, ST_Transform(ST_GeomFromText('POINT(55.98886 54.74241)', 4326), 900913));
Прямой же геокодинг - нахождение координат дома по адресу - не является столь же тривиальной задачей, как обратный геокодинг. Это так, потому что людей довольно трудно заставить писать адрес всегда одним и тем же образом. Люди используют сокращения слов, переставляют слова местами, пропускают слова, кажущиеся им незначимыми, а подобные знания в "голову" компьютера не заложишь.

Для себя я нашёл подходящее решение, которое в целом меня устраивает, но не является универсальным, т.к. опирается на некоторые допущения, которые верны для интересующих меня населённых пунктов.

Для поиска адреса создаётся индекс адресов, в который помещаются "нормализованные" строки, содержащие название населённого пункта, улицы и дома. Перед поиском адреса по индексу, искомый адрес тоже переводится в нормализованную форму, а дальнейший поиск выполняется простым SQL-запросом.

Процедура нормализации у меня делится на три части, из которых самой сложной является нормализация названия улицы.

Нормализация названия населённого пункта:
  1. Буквы переводятся в нижний регистр,
  2. Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
  3. Буква "ё" заменяется на "е",
  4. Удаляются сокращения "г.", "п.", "с.", "д.", слова "город", "поселок", "село", "деревня".
Полученная строка используется для сравнения.

Из неучтённых особенностей тут могут быть одноимённые населённые пункты разного класса. Например, посёлок Октябрьский и город Октябрьский. Или одноимённые населённые пункты из разных районов - посёлок Фёдоровка рядом с Уфой и посёлок Фёдоровка в Фёдоровском районе. Но поскольку мне нужен поиск адресов только в 9 городах, то эти особенности я учитывать не стал.

Нормализация номера дома:
  1. Буквы переводятся в нижний регистр,
  2. Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
  3. Буква "ё" заменяется на "е".
Из неучтённых особенностей тут могут быть попытки вставить в поле номера дома слово "дом" или сокращение "д.", могут быть присутствовать слова "корпус", "корп.", "строение", "стр.", попытки вместо знака дроби написать слово "дробь" и т.п.

Нормализация названия улицы:
  1. Буквы переводятся в нижний регистр,
  2. Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
  3. Буква "ё" заменяется на "е",
  4. Получившаяся строка разбивается на последовательность слов, а границами слов считаются пробелы и точки. Это сделано для того, чтобы различные сокращения и инициалы отделились от слов, с которыми они написаны слитно,
  5. Удаляются одиночные буквы,

  6. Раскрываются сокращения "ул" -> "улица", "пер" -> "переулок", "пр" -> "проспект", "пер" -> "переулок", "бул" -> "бульвар", "пл" -> "площадь", "шос" -> "шоссе", "наб" -> "набережная", "им" -> "имени",

  7. От чисел отрезаются окончания, так что строки типа "60-летия", "2-й", "1-я" превращаются просто в числа,

  8. Удаляются незначащие слова типа "лет", "летия", "реки", "имени". Названия многих улиц приурочены к юбилеям каких-либо памятных событий ("50-летия Октября" или "60 лет СССР"). Набережные, естественно, часто имеют в своём названии названия рек, вдоль которых они расположены, поэтому между названием типа "набережная реки Уфы" или "набережная Уфы" нет никакой разницы. И, наконец, улицы часто называются в честь каких-то людей, поэтому нет разницы между названиями типа "проспект имени Ленина" или "проспект Ленина",

  9. Удаляются слова-классификаторы адреса типа "улица", "проспект", "площадь", "тракт", из которых запоминается только первое.

  10. Из оставшихся слов собирается нормализованный адрес, перед которым ставится слово-классификатор адреса.

  11. В получившейся строке ищутся идущие подряд пары слов типа "имя фамилия" или "титул фамилия", из которых остаётся только фамилия. Тут я делаю предположение, что в городе не бывает улиц одного класса, названных именами однофамильцев. То есть, в городе не может быть улицы Льва Толстого и улицы Алексея Толстого, но может быть улица Льва Толстого и проспект Алексея Толстого - в этом случае однофамильцы будут различаться классом улицы. И сюда же относятся различия в титулах - алгоритм нормализации не учитывает, что могут быть улицы академика Морозова и Павлика Морозова. Это преобразование помогает находить названия улиц, в случае если имя или титул человека, в честь которого названа искомая улица, не были указаны. Тут мне пришлось приложить усилия и составить список людей, именами которых названы улицы. У меня это единый список, но вообще, хорошо бы иметь отдельный список для каждого населённого пункта - так и точность и скорость нормализации будут выше. В России для этого можно использовать адресный справочник КЛАДР или пришедший ему на смену ФИАС - читайте, например КЛАДР умер, да здравствует ФИАС?

Также при поиске дома по адресу следует учитывать, что существуют угловые дома, которым часто назначаются сразу два адреса. В проекте OpenStreetMap нет единого соглашения по тому, каким образом в базе данных указывать такие адреса. Есть несколько разных подходов, которые описаны на этой странице: Key:addr. Угловые дома Для отображения информации на карте я использую JavaScript-библиотеку LeafLet, написанную киевским программистом Владимиром Агафонкиным. Эта библиотека отстаёт по возможностям от библиотеки OpenLayers, которая используется самим проектом OpenStreetMap, но мне она понравилась компактностью и простотой использования.

воскресенье, 5 августа 2012 г.

Установка генератора тайлов Mapnik

Продолжение серии заметок, посвящённых настройке тайлового сервера. Смотрите также предыдущие заметки Подготовка карт для генератора тайлов Mapnik и Настройка базы данных для генератора тайлов Mapnik.

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

Ставим Mapnik, готовим границы мира и береговые линии

Ставим генератор тайлов mapnik2:
# apt-get install python-mapnik2
Скачиваем береговые линии, границы мира и т.п. с помощью скрипта:
$ wget http://svn.openstreetmap.org/applications/rendering/mapnik/get-coastlines.sh
$ chmod +x get-coastlines.sh
$ ./get-coastlines.sh
Всё скачанное помещаем в каталог /usr/share/mapnik/world_boundaries:
# mkdir /usr/share/mapnik
# mv world_boundaries /usr/share/mapnik
Настраиваем стиль отрисовки карты

Скачиваем скрипты и файлы стилей Mapnik (может понадобиться установить subversion):
$ svn co http://svn.openstreetmap.org/applications/rendering/mapnik/
И удаляем лишнее:
$ find . -name .svn -exec rm -Rf \{\} \;
Перемещаем файлы стилей Mapnik в каталог /etc/mapnik-osm-data:
# mkdir /etc/mapnik-osm-data
# mv mapnik/inc mapnik/symbols mapnik/osm.xml /etc/mapnik-osm-data/
Переименовываем файлы шаблонов настроек:
# cd /etc/mapnik-osm-data/inc/
# mv fontset-settings.xml.inc.template fontset-settings.xml.inc
# mv datasource-settings.xml.inc.template datasource-settings.xml.inc
# mv settings.xml.inc.template settings.xml.inc
Исправляем настройки в соответствии с описанием Manually building a tile server.

В файле settings.xml.inc заменим:
1. <!ENTITY symbols "%(symbols)s">
на <ENTITY symbols "symbols">
2. <!ENTITY osm2pgsql_projection "&srs%(epsg)s;">
на <!ENTITY osm2pgsql_projection "&srs900913;">
3. <!ENTITY dwithin_node_way "&dwithin_%(epsg)s;">
на <!ENTITY dwithin_node_way "&dwithin_900913;">
4. <!ENTITY world_boundaries "%(world_boundaries)s">
на <!ENTITY world_boundaries "/usr/share/mapnik/world_boundaries">
5. <!ENTITY prefix "%(prefix)s">
на <!ENTITY prefix "planet_osm">

В файле datasource-settings.xml.inc прописываем настройки подключения к базе данных, заменив:
1. <Parameter name="password">%(password)</Parameter>
на <Parameter name="password">password</Parameter>
2. <Parameter name="host">%(host)s</Parameter>
закомментировав как
<!-- <Parameter name="host">%(host)s</Parameter> -->
3. <Parameter name="port">%(port)s</Parameter>
закомментировав как
<!-- <Parameter name="port">%(port)s</Parameter> -->
4. <Parameter name="user">%(user)s</Parameter>
на <Parameter name="user">osm</Parameter>
5. <Parameter name="dbname">%(dbname)s</Parameter>
на <Parameter name="dbname">osm</Parameter>
6. <Parameter name="estimate_extent">%(estimate_extent)s</Parameter>
на <Parameter name="estimate_extent">false</Parameter>
7. <Parameter name="extent">%(extent)s</Parameter>
на <Parameter name="extent">-20037508,-19929239,20037508,19929239</Parameter>

Здесь указываются настройки подключения к базе данных, а также указывается прямоугольник, в пределах которого умещаются все данные карты. По прямоугольнику Mapnik определяет, стоит ли вообще обращаться к базе данных за этими данными - быть может запрос заведомо ничего не вернёт и будет выполнен вхолостую.

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

Будьте крайне внимательны с настройками extent - если вы их укажете неправильно, Mapnik может не обращаться к базе данных, несмотря на то, что они в ней есть. Как следствие, Mapnik будет рисовать только береговые линии и границы мира.

Настройки адреса и порта сервера комментируем, поскольку сервер базы данных установлен на том же компьютере, что и Mapnik - подключение к серверу базы данных будет осуществляться через Unix-сокет.

Файл fontset-settings.xml.inc не меняем.

На этом настройка самого генератора тайлов Mapnik закончена. В следующей заметке я расскажу, как настроить верхушку всей инфраструктуры - тайловый сервер.

воскресенье, 29 июля 2012 г.

Настройка базы данных для генератора тайлов Mapnik

Продолжение серии заметок, посвящённых настройке тайлового сервера. Смотрите также предыдущую заметку Подготовка карт для генератора тайлов Mapnik.

Введение

Эта заметка посвящена настройке базы данных для генератора тайлов Mapnik. Генератор тайлов - это программа, которая на основе векторного описания карты и файла описания стилей формирует квадратные растровые картинки с изображением карты.

Генератор тайлов можно использовать в сочетании с различными JavaScript-библиотеками, позволяющими прямо в веб-браузере просматривать карту. Проект OpenStreetMap, в котором используется генератор тайлов Mapnik, пользуется JavaScript-бибилиотекой OpenLayers. Забегая вперёд хочу сказать, что мой выбор остановился на JavaScript-библиотеке Киевского программиста Владимира Агафонкина LeafLet, которая хотя и уступает библиотеке OpenLayers по возможностям, зато проще в использовании и имеет более качественную документацию.

Вообще, о начинке проекта OpenStreetMap могу сказать следующее. Видно что проект динамично развивается и не имеет какой-либо генеральной линии развития. Основные компоненты проекта написаны на самых разных языках: C++ и Python (Mapnik), Java (JOSM и Osmosis), JavaScript (OpenLayers, но тут уж без альтернатив), Ruby On Rails (веб-сайт OpenStreetMap). Одно время сайт OpenStreetMap хранил свои данные в MySQL, в то время как вся географическая информация хранилась в PostgreSQL. Однако, разработчики сайта всё-таки перешли полностью на PostgreSQL, отказавшись от MySQL.

Из-за такого буйства технологий и энергичности развития бывает довольно трудно подобрать согласованные версии программ, которые бы хорошо работали друг с другом, а документация часто оказывается либо не полной, либо просто устаревшей. Есть много побочных и альтернативных проектов, которые тоже частично устарели, а частично потеряли совместимость с современными версиями программ.

Для облегчения настройки я советую взять Debian 7.0 Wheezy, хотя и на Debian 6.0 Squeeze настройка тоже возможна, но она сопряжена с бОльшими усилиями (подключение сторонних репозиториев, пересборка пакетов и т.п.). Всё дальнейшее описание делается применительно именно к Debian 7.0 Wheezy.

Установка СУБД

Установим пакеты, необходимые нам на этом этапе:
# apt-get install postgresql postgresql-contrib postgresql-9.1-postgis osm2pgsql
Настраиваем авторизацию по паролю в файле /etc/postgresql/9.1/main/pg_hba.conf, заменив первую строчку на вторую:
local all all peer
local all all password
Теперь выполним тюнинг ядра Linux для увеличения скорости работы PostgreSQL:
# sysctl kernel.shmmax=268435456
И пропишем такую же настройку в файле /etc/sysctl.conf:
kernel.shmmax=268435456
После тюнинга ядра можно произвести тюнинг самого сервера PostgreSQL. Для этого в файле /etc/postgresql/9.1/main/postgresql.conf изменим следующие строчки:
shared_buffers = 128MB
checkpoint_segments = 20
maintenance_work_mem = 256MB
autovacuum = off
Перезапустим сервер PostgreSQL, чтобы новые настройки вступили в силу:
# /etc/init.d/postgresql restart
Создание базы данных для Mapnik.

Теперь создадим пользователя, базу данных, установим в неё расширения, необходимые для работы Mapnik. Войдём под администратором PostgreSQL:
# su - postgres
Создадим пользователя:
$ createuser -PRDS osm
Создадим базу данных, принадлежащую только что созданному пользователю:
$ createdb -E UTF8 -O osm osm
Установим в базу данных расширение для индексирования геометрических объектов, находящееся в пакете postgresql-contrib:
$ psql -d osm -c "CREATE EXTENSION btree_gist;"
Установим в базу данных расширение PostGIS, необходимое для осуществления различных операций над геометрическими объектами. Расширение находится в пакете postgresql-9.1-postgis. Подробнее почитать о функциях, добавляемых PostGIS в PostgreSQL, можно на официальном сайте проекта: PostGIS 1.5.4 Manual.
$ psql -d osm -f /usr/share/postgresql/9.1/contrib/postgis-1.5/postgis.sql
Делаем пользователя osm владельцем таблиц geometry_columns и spatial_ref_sys, чтобы osm2pgsql мог импортировать карты от имени пользователя osm:
$ psql -d osm -c "ALTER TABLE geometry_columns OWNER TO osm; ALTER TABLE spatial_ref_sys OWNER TO osm;"
Теперь нужно установить описания проекций. Самому Mapnik'у нужна лишь одна проекция - 900913, проекция Меркатора. Установить её можно с помощью следующей команды:
$ psql -d osm -f /usr/share/doc/osm2pgsql/examples/900913.sql
Я же установил все доступные проекции, так как в дальнейшем я с помощью SQL-запросов извлекал геометрическую информацию в проекции 4326 WGS84 (обычные широта и долгота), преобразовывая проекцию объектов с помощью функции PostGIS ST_Transform.
$ psql -d osm -f /usr/share/postgresql/9.1/contrib/postgis-1.5/spatial_ref_sys.sql
На этом настройка базы данных завершается, можно выйти из сеанса пользователя postgres, открытого командой su.

Заливка данных в базу данных Mapnik

Осталось залить карты в базу данных. Для этого можно воспользоваться утилитой из одноимённого пакета osm2pgsql:
$ osm2pgsql -U osm -d osm -W /home/stupin/Downloads/RU-BA_TA_ORE.pbf