Кластер PostgreSQL высокой надежности на базе Patroni, Haproxy, Keepalived
Источник: habr.com
По задумке, хотелось получить кластер, который переживает выпадение любого сервера, или даже нескольких серверов, и умеет автоматически вводить в строй сервера после аварий.
Планируя кластер я проштудировал много статей, как из основной документации к PostgreSQL, так и различных howto, в том числе с Хабра, и пробовал настроить стандартный кластер с RepMgr, эксперементировал с pgpool.
В целом оно заработало, но у меня периодически всплывали проблемы с переключениями, требовалось ручное вмешательство для восстановления после аварий, и т.д.
Введение
Patroni — это демон на python, позволяющий автоматически обслуживать кластеры PostgreSQL с различными типами репликации, и автоматическим переключением ролей.
Для поддержания актуальности кластера и выборов мастера используются распределенные хранилища (поддерживаются Zookeeper, etcd, Consul).
Таким образом кластер легко интегрируется практически в любую систему, всегда можно выяснить кто в данный момент мастер, и статус всех серверов запросами в DCS, или напрямую к Patroni через http.
После полной реализации описанной ниже схемы я проводил тестирование, входя в базу по единому адресу, и переживал падения всех элементов кластера (мастер сервера, haproxy, keepalived).
Задержка при передаче роли новому мастеру составляла пару секунд. При возвращении бывшего мастера в кластер, или добавлении нового сервера, смены ролей не происходит.
Для автоматизации разворачивания кластера и добавления новых серверов, решено было использовать привычный Ansible (я дам ссылки на получившиеся роли в конце статьи). В качестве DCS выступает уже применяемый у нас Consul.
Рассказ будет разделен на две части — подготовка серверов и разворачивание непосредственно кластера.
Часть I
Для этого примера я использую виртуальные машины на базе Centos 7. Виртуалки разворачиваются из шаблона который периодически обновляется (ядро, системные пакеты), но эта тема выходит за рамки данной статьи.
Отмечу только, что никакого прикладного или серверного софта на виртуалках заранее не установлено. Также вполне подойдут любые облачные ресурсы, например с AWS, DO, vScale, и т.п. Для них есть скрипты динамического инвентаря и интеграции с Ansible, либо можно прикрутить Terraform, так что весь процесс создания и удаления серверов c нуля может быть автоматизирован.
Для начала нужно создать инвентарь используемых ресурсов для Ansible. Ansible у меня (и по умолчанию) расположен в /etc/ansible. Создаем инвентарь в файле /etc/ansible/hosts:
У нас используется внутренняя доменная зона .local, поэтому у серверов такие имена.
Далее нужно подготовить каждый сервер к установке всех необходимых компонентов, и рабочих инструментов.
Для этой цели создаем плейбук в /etc/ansible/tasks:
/etc/ansible/tasks/essentialsoftware.yml
---
- name: Install essential software
yum: name={{ item }} state=latest
tags: software
with_items:
- ntpdate
- bzip2
- zip
- unzip
- openssl-devel
- mc
- vim
- atop
- wget
- mytop
- screen
- net-tools
- rsync
- psmisc
- gdb
- subversion
- htop
- bind-utils
- sysstat
- nano
- iptraf
- nethogs
- ngrep
- tcpdump
- lm_sensors
- mtr
- s3cmd
- psmisc
- gcc
- git
- python2-pip
- python-devel
- name: install the 'Development tools' package group
yum:
name: "@Development tools"
state: present
Мы используем виртуальные машины на базе VmWare ESXi, и для удобства администрирования в них нужно запускать агент vmware.
Для этого мы запустим открытый агент vmtoolsd, и опишем его установку в отдельном плейбуке (поскольку не все сервера у нас виртуальные, и возможно для каких-то из них этот таск не понадобится):
/etc/ansible/tasks/open-vm-tools.yml
---
- name: Install open VM tools for VMWARE
yum: name={{ item }} state=latest
tags: open-vm-tools
with_items:
- open-vm-tools
- name: VmWare service start and enabling
service: name=vmtoolsd.service state=started enabled=yes
tags: open-vm-tools
Для того чтобы завершить подготовку сервера к установке основной части софта, в нашем случае, понадобятся следующие шаги:
1) настроить синхронизацию времени с помощью ntp 2) установить и запустить zabbix агент для мониторинга 3) накатить требуемые ssh ключи и authorized_keys.
Чтобы не слишком раздувать статью деталям не относящимися к собственно кластеру, я кратко процитирую ansible плейбуки, выполняющие эти задачи:
NTP: /etc/ansible/tasks/ntpd.yml
---
- name: setting default timezone
set_fact:
timezone: name=Europe/Moscow
when: timezone is not defined
- name: setting TZ
timezone: name={{ timezone }}
when: timezone is defined
tags:
- tz
- tweaks
- ntp
- ntpd
- name: Configurating cron for ntpdate
cron: name="ntpdate" minute="*/5" job="/usr/sbin/ntpdate pool.ntp.org"
tags:
- tz
- tweaks
- ntp
- ntpd
- name: ntpd stop and disable
service: name=ntpd state=stopped enabled=no
tags:
- tz
- tweaks
- ntp
- ntpd
ignore_errors: yes
- name: crond restart and enabled
service: name=crond state=restarted enabled=yes
tags:
- tz
- tweaks
- ntp
- ntpd
Вначале проверяется, не выставлена ли для сервера персональная таймзона, и если нет, то выставляется Московская (таких серверов у нас большинство).
Мы не используем ntpd из-за проблем с уплыванием времени на виртуалках ESXi, после которого ntpd отказывается синхронизировать время. (И tinker panic 0 не помогает). Поэтому просто запускаем кроном ntp клиент раз 5 минут.
Zabbix-agent:
/etc/ansible/tasks/zabbix.yml
---
- name: set zabbix ip external
set_fact:
zabbix_ip: 132.xx.xx.98
tags: zabbix
- name: set zabbix ip internal
set_fact:
zabbix_ip: 192.168.xx.98
when: ansible_all_ipv4_addresses | ipaddr('192.168.0.0/16')
tags: zabbix
- name: Import Zabbix3 repo
yum: name=http://repo.zabbix.com/zabbix/3.0/rhel/7/x86_64/zabbix-release-3.0-1.el7.noarch.rpm state=present
tags: zabbix
- name: Remove old zabbix
yum: name=zabbix2* state=absent
tags: zabbix
- name: Install zabbix-agent software
yum: name={{ item }} state=latest
tags: zabbix
with_items:
- zabbix-agent
- zabbix-release
- name: Creates directories
file: path={{ item }} state=directory
tags:
- zabbix
- zabbix-mysql
with_items:
- /etc/zabbix/externalscripts
- /etc/zabbix/zabbix_agentd.d
- /var/lib/zabbix
- name: Copy scripts
copy: src=/etc/ansible/templates/zabbix/{{ item }} dest=/etc/zabbix/externalscripts/{{ item }} owner=zabbix group=zabbix mode=0755
tags: zabbix
with_items:
- netstat.sh
- iostat.sh
- iostat2.sh
- iostat_collect.sh
- iostat_parse.sh
- php_workers_discovery.sh
- name: Copy .my.cnf
copy: src=/etc/ansible/files/mysql/.my.cnf dest=/var/lib/zabbix/.my.cnf owner=zabbix group=zabbix mode=0700
tags:
- zabbix
- zabbix-mysql
- name: remove default configs
file: path={{ item }} state=absent
tags: zabbix
with_items:
- /etc/zabbix_agentd.conf
- /etc/zabbix/zabbix_agentd.conf
- name: put zabbix-agentd.conf to default place
template: src=/etc/ansible/templates/zabbix/zabbix_agentd.tpl dest=/etc/zabbix_agentd.conf owner=zabbix group=zabbix force=yes
tags: zabbix
- name: link zabbix-agentd.conf to /etc/zabbix
file: src=/etc/zabbix_agentd.conf dest=/etc/zabbix/zabbix_agentd.conf state=link
tags: zabbix
- name: zabbix-agent start and enable
service: name=zabbix-agent state=restarted enabled=yes
tags: zabbix
Сервера расположенные в пределах нашей сети ходят на 192.168.х.98, а сервера не имеющие в нее доступа, на реальный адрес этого же сервера.
Перенос ssh ключей и настройка ssh вынесена в отдельную роль, которую можно найти, например, на ansible-galaxy.
Вариантов там много, а суть изменений достаточно тривиальна, поэтому цитировать весь ее контент здесь я смысла не вижу.
Настала пора накатить на сервера созданную конфигурацию.
Создаем плейбук для группы серверов:
/etc/ansible/cluster-pgsql.yml
---
- hosts: pgsql
pre_tasks:
- name: Setting system hostname
hostname: name="{{ ansible_host }}"
- include: tasks/essentialsoftware.yml
- include: tasks/open-vm-tools.yml
- include: tasks/ntpd.yml
post_tasks:
- include: tasks/zabbix.yml
roles:
- ssh.role
- ansible-role-patroni
Запускаем обработку всех серверов:
Если вы полностью скачали мой пример из гитхаб репозитория, то у вас будет также в наличии и роль Patroni, которую нам пока отрабатывать не нужно.
Аргумент –skip-tags заставляет Ansible пропустить шаги помеченные этим тегом, поэтому роль ansible-role-patroni выполняться сейчас не будет.
Если же ее на диске нет, то ничего страшного и не произойдет, Anisble просто проигнорирует этот ключ.
Ansible у меня заходит на сервера сразу пользователем root, а если вам потребуется пускать ansible под непревилегированного пользователя, «become: true», который побудит ansible использовать вызовы sudo для этих шагов.
Подготовка закончена.
Часть II
Приступаем к разворачиванию непосредственно кластера.
Поскольку для настройки кластера требуется много работы (установить PostgreSQL и все компоненты, залить для них индивидуальные конфиги), я выделил весь этот процесс в отдельную роль.
Роли в Ansible позволяют сгруппировать наборы смежных тасков, и тем упрощают написание скриптов и поддержку их в рабочем состоянии.
Шаблон роли для установки Patroni я взял тут: https://github.com/gitinsky/ansible-role-patroni, за что спасибо его автору. Для своих целей я переработал имеющийся и добавил свои плейбуки haproxy и keepalived.
Роли у меня лежат в каталоге /etc/ansible/roles. Создаем каталог для новой роли, и подкаталоги для ее компонентов:
~# mkdir /etc/ansible/roles/ansible-role-patroni/tasks
~# mkdir /etc/ansible/roles/ansible-role-patroni/templates
1) haproxy для отслеживания состояния серверов и перенаправления запросов на мастер сервер. 2) keepalived для обеспечения наличия единой точки входа в кластер — виртуального IP.
Все плейбуки выполняемые данной ролью перечисляем в файле, запускаемом ansible по умолчанию:
/etc/ansible/roles/ansible-role-patroni/tasks/main.yml
Далее начинаем описывать отдельные задачи.
Первый плейбук устанавливает PostgreSQL 9.6 из родного репозитория, и дополнительные пакеты требуемые Patroni, а затем скачивает с GitHub саму Patroni:
/etc/ansible/roles/ansible-role-patroni/tasks/postgres.yml
---
- name: Import Postgresql96 repo
yum: name=https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm state=present
tags: patroni
when: install is defined
- name: Install PGsql96
yum: name={{ item }} state=latest
tags: patroni
with_items:
- postgresql96
- postgresql96-contrib
- postgresql96-server
- python-psycopg2
- repmgr96
when: install is defined
- name: checkout patroni
git: repo=https://github.com/zalando/patroni.git dest=/opt/patroni
tags: patroni
when: install is defined
- name: create /etc/patroni
file: state=directory dest=/etc/patroni
tags: patroni
when: install is defined
- name: put postgres.yml
template: src=postgres0.yml dest=/etc/patroni/postgres.yml backup=yes
tags: patroni
when: install is defined
- name: install python packages
pip: name={{ item }}
tags: patroni
with_items:
- python-etcd
- python-consul
- dnspython
- boto
- mock
- requests
- six
- kazoo
- click
- tzlocal
- prettytable
- PyYAML
when: install is defined
- name: put patroni.service systemd unit
template: src=patroni.service dest=/etc/systemd/system/patroni.service backup=yes
tags: patroni
when: install is defined
- name: Reload daemon definitions
command: /usr/bin/systemctl daemon-reload
tags: patroni
- name: restart
service: name=patroni state=restarted enabled=yes
tags: patroni
Кроме установки ПО данный плейбук также заливает конфигурацию для текущего сервера Patroni, и systemd юнит для запуска демона в системе, после чего запускает демон Patroni. Шаблоны конфигов и systemd юнит должны лежать в каталоге templates внутри роли.
Шаблон конфига Patroni:
/etc/ansible/roles/ansible-role-patroni/templates/postgres.yml.j2
name: {{ patroni_node_name }}
scope: &scope {{ patroni_scope }}
consul:
host: consul.services.local:8500
restapi:
listen: 0.0.0.0:8008
connect_address: {{ ansible_default_ipv4.address }}:8008
auth: 'username:{{ patroni_rest_password }}'
bootstrap:
dcs:
ttl: &ttl 30
loop_wait: &loop_wait 10
maximum_lag_on_failover: 1048576 # 1 megabyte in bytes
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
archive_mode: "on"
wal_level: hot_standby
archive_command: mkdir -p ../wal_archive && cp %p ../wal_archive/%f
max_wal_senders: 10
wal_keep_segments: 8
archive_timeout: 1800s
max_replication_slots: 5
hot_standby: "on"
wal_log_hints: "on"
pg_hba: # Add following lines to pg_hba.conf after running 'initdb'
- host replication replicator 192.168.0.0/16 md5
- host all all 0.0.0.0/0 md5
postgresql:
listen: 0.0.0.0:5432
connect_address: {{ ansible_default_ipv4.address }}:5432
data_dir: /var/lib/pgsql/9.6/data
pg_rewind:
username: superuser
password: {{ patroni_postgres_password }}
pg_hba:
- host all all 0.0.0.0/0 md5
- hostssl all all 0.0.0.0/0 md5
replication:
username: replicator
password: {{ patroni_replicator_password }}
network: 192.168.0.0/16
superuser:
username: superuser
password: {{ patroni_postgres_password }}
admin:
username: admin
password: {{ patroni_postgres_password }}
restore: /opt/patroni/patroni/scripts/restore.py
Поскольку для каждого сервера кластера требуется индивидуальная конфигурация Patroni, его конфиг лежит в виде шаблона jinja2 (файл postgres0.yml.j2), и шаг template заставляет ansible транслировать этот шаблон с заменой переменных, значения из которых берутся из отдельного описания для каждого сервера.
Переменные, общие для всего кластера укажем в прямо в инвентаре, который примет теперь следующий вид:
/etc/ansible/hosts
[pgsql]
cluster-pgsql-01.local
cluster-pgsql-02.local
cluster-pgsql-03.local
[pgsql:vars]
patroni_scope: "cluster-pgsql"
patroni_rest_password: flsdjkfasdjhfsd
patroni_postgres_password: flsdjkfasdjhfsd
patroni_replicator_password: flsdjkfasdjhfsd
cluster_virtual_ip: 192.xx.xx.125
</spoiler>
А отдельную для каждого сервера - в каталоге host_vars/имя_сервера:
<spoiler title="/etc/ansible/host_vars/pgsql-cluster-01.local/main.yml">
<source lang="yaml">
patroni_node_name: cluster_pgsql_01
keepalived_priority: 99
Расшифрую для чего нужны некоторые переменные:
patroni_scope — название кластера при регистрации в Consul patroni_node_name — название сервера при регистрации в Consul patroni_rest_password — пароль для http интерфейса Patroni (требуется для отправки команд на изменение кластера) patroni_postgres_password: пароль для юзера postgres. Он устанавливается в случае создания patroni новой базы. patroni_replicator_password — пароль для юзера replicator. От его имени осуществляется репликация на слейвы.
Также в этом файле перечислены некоторые другие переменные, используемые в других плейбуках или ролях, в частности тот может быть настройка ssh (ключи, пользователи), таймзона для сервера, приоритет сервера в кластере keepalived, и.т.п.
Конфигурация для остальных серверов аналогична, соответственно меняется имя сервер и приоритет (например 99-100-101 для трех серверов).
Haproxy
Установка и настройка haproxy:
/etc/ansible/roles/ansible-role-patroni/tasks/haproxy.yml
---
- name: Install haproxy
yum: name={{ item }} state=latest
tags:
- patroni
- haproxy
with_items:
- haproxy
when: install is defined
- name: put config
template: src=haproxy.cfg.j2 dest=/etc/haproxy/haproxy.cfg backup=yes
tags:
- patroni
- haproxy
- name: restart and enable
service: name=haproxy state=restarted enabled=yes
tags:
- patroni
- haproxy
Haproxy устаналивается на каждом хосте, и содержит в своем конфиге ссылки на все сервера PostgreSQL, проверяет какой сервер сейчас является мастером, и отправляет запросы на него. Для этой проверки используется прекрасная фича Patroni — REST интерфейс.
При обращении на урл server:8008 (8008 это порт по умолчанию) Patroni возвращает отчет по состоянию кластера в json, а также отражает кодом ответа http является ли данный сервер мастером. Если является — будет ответ с кодом 200. Если же нет, ответ с кодом 503.
Очень советую обратится в документацию на Patroni, http интерфейс там достаточно интересный, позволяется также принудительно переключать роли, и управлять кластером. Аналогично, это можно делать при помощи консольной утилиты patronyctl.py, из поставки Patroni.
Конфигурация haproxy достаточно простая:
/etc/ansible/roles/ansible-role-patroni/templates/haproxy.cfg
global
maxconn 800
defaults
log global
mode tcp
retries 2
timeout client 30m
timeout connect 4s
timeout server 30m
timeout check 5s
frontend ft_postgresql
bind *:5000
default_backend postgres-patroni
backend postgres-patroni
option httpchk
http-check expect status 200
default-server inter 3s fall 3 rise 2
server {{ patroni_node_name }} {{ patroni_node_name }}.local:5432 maxconn 300 check port 8008
server {{ patroni_node_name }} {{ patroni_node_name }}.local:5432 maxconn 300 check port 8008
server {{ patroni_node_name }} {{ patroni_node_name }}.local:5432 maxconn 300 check port 8008
В соответствии с этой конфигурацией haproxy слушает порт 5000, и отправляет трафик с него на мастер сервер.
Проверка статуса происходит с интервалом в 1 секунду, для перевода сервера в даун требуется 3 неудачных ответа (код 500), для переключения сервера назад — 2 удачных ответа (с кодом 200). В любой момент времени можно обратиться непосредственно на любой haproxy, и он корректно запроксирует трафик на мастер сервер.
Также в комплекте с Patroni есть шаблон для настройки демона confd, и пример его интеграции с etcd, что позволяет динамически менять конфиг haproxy при удалении или добавлении новых серверов.
Я же пока делаю достаточно статичный кластер, лишняя автоматизация в данной ситуации, имхо, может привести к непредвиденным проблемам.
Нам хотелось, чтобы на клиентах особые изменения логики, отслеживание серверов на живости и т.д. не требовались, поэтому мы делаем единую точку входа в кластер с помощью keepalived.
Демон keepalived работает по протоколу vrrp со своими соседями, и в результате выборов одного из демонов как главного (приоритет указан в конфиге, и шаблонизирован в переменную keepalived_priority в host_vars для каждого сервера), он поднимает у себя виртуальный ip адрес. Остальные демоны терпеливо ждут. Если текущий основной сервер keepalived по какой-то причине умрет либо просигналит соседям аварию, произойдут перевыборы, и следуюший по приоритету сервер заберет себе виртуальный ip адрес.
Для защиты от падения haproxy демоны keepalived выполняют проверку, запуская раз в секунду команду «killall -0 haproxy». Она возвращает код 0 если процесс haproxy есть, и 1 если его нет. Если haproxy исчезнет, демон keepalived просигналит аварию по vrrp, и снимет виртуальный ip. Виртуальный IP сразу же подхватит следующий по приоритету сервер, с живым haproxy.
Установка и настройка keepalived:
/etc/ansible/roles/ansible-role-patroni/tasks/keepalived.yml
---
- name: Install keepalived
yum: name={{ item }} state=latest
tags:
- patroni
- keepalived
with_items:
- keepalived
when: install is defined
- name: put alert script
template: src=alert.sh.j2 dest=/usr/local/sbin/alert.sh backup=yes mode=755
tags:
- patroni
- keepalived
when: install is defined
- name: put config
template: src=keepalived.conf.j2 dest=/etc/keepalived/keepalived.conf backup=yes
tags:
- patroni
- keepalived
- name: restart and enable
service: name=keepalived state=restarted enabled=yes
tags:
- patroni
- keepalived
Кроме установки keepalived, этот плейбук также копирует простой скрипт для отправки алертов через телеграм. Скрипт принимает сообщение в виде переменной, и просто дергает curl-ом API телеграма.
В этом скрипте только нужно указать свои токен и ID группы telegram для отсылки оповещений.
Конфигурация keepalived описана в виде jinja2 шаблона:
/etc/ansible/roles/ansible-role-patroni/templates/keepalived.conf.j2
global_defs {
router_id {{ patroni_node_name }}
}
vrrp_script chk_haproxy {
script "killall -0 haproxy"
interval 1
weight -20
debug
fall 2
rise 2
}
vrrp_instance {{ patroni_node_name }} {
interface ens160
state BACKUP
virtual_router_id 150
priority {{ keepalived_priority }}
authentication {
auth_type PASS
auth_pass secret_for_vrrp_auth
}
track_script {
chk_haproxy weight 20
}
virtual_ipaddress {
{{ cluster_virtual_ip }}/32 dev ens160
}
notify_master "/usr/bin/sh /usr/local/sbin/alert.sh '{{ patroni_node_name }} became MASTER'"
notify_backup "/usr/bin/sh /usr/local/sbin/alert.sh '{{ patroni_node_name }} became BACKUP'"
notify_fault "/usr/bin/sh /usr/local/sbin/alert.sh '{{ patroni_node_name }} became FAULT'"
}
В переменные patroni_node_name, cluster_virtual_ip и keepalived_priority транслируются соответствующие данные из host_vars.
Также в конфиге keepalived указан скрипт для отправки сообщений о смене статуса в telegram канал.
Накатываем полную конфигурацию кластера на сервера:
Поскольку Ansible идемпотентен, т.е. выполняет шаги только если они не были выполнены ранее, можно запустить плейбук без дополнительных параметров.
Если же не хочется дольше ждать, или вы уверены что сервера полностью готовы, можно запустить ansible-playbook с ключом -t patroni.
Тогда будут выполнены только шаги из роли Patroni.
Отмечу что я не указываю отдельно роли серверов — мастер или слейв. Данная конфигурация создаст пустую базу, и мастером просто станет первый сконфигурированный сервер.
При добавлении новых серверов Patroni увидит через DCS что мастер кластера уже есть, автоматически скопирует с текущего мастера базу, и подключит к нему слейв.
В случае запуска слейва отставшего на какое-то время от мастера, Patroni автоматически вольет изменения при помощи pg_rewind.
Убеждаемся что все сервера запустились и выбрали себе роли:
Сообщения со слейва (сервер cluster-pgsql-01):
Feb 17 23:50:32 cluster-pgsql-01.local patroni.py[100626]: 2017-02-17 23:50:32,254 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_01
Feb 17 23:50:32 cluster-pgsql-01.local patroni.py[100626]: 2017-02-17 23:50:32,255 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_01
Feb 17 23:50:32 cluster-pgsql-01.local patroni.py[100626]: 2017-02-17 23:50:32,255 INFO: does not have lock
Feb 17 23:50:32 cluster-pgsql-01.local patroni.py[100626]: 2017-02-17 23:50:32,255 INFO: no action. i am a secondary and i am following a leader
Сообщения с мастера (в данном случае это сервер cluster-pgsql-02):
Feb 17 23:52:23 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:52:23,457 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_02
Feb 17 23:52:23 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:52:23,874 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_02
Feb 17 23:52:24 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:52:24,082 INFO: no action. i am the leader with the lock
Feb 17 23:52:33 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:52:33,458 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_02
Feb 17 23:52:33 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:52:33,884 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_02
Feb 17 23:52:34 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:52:34,094 INFO: no action. i am the leader with the lock
По логам ясно видно что каждый сервер постоянно мониторит свой статус и статус мастера. Попробуем остановить мастер:
Feb 17 23:54:03 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:54:03,457 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_02
Feb 17 23:54:03 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:54:03,880 INFO: Lock owner: cluster_pgsql_02; I am cluster_pgsql_02
Feb 17 23:54:04 cluster-pgsql-02.local patroni.py[4913]: 2017-02-17 23:54:04,092 INFO: no action. i am the leader with the lock
Feb 17 23:54:11 cluster-pgsql-02.local systemd[1]: Stopping Runners to orchestrate a high-availability PostgreSQL...
Feb 17 23:54:13 cluster-pgsql-02.local patroni.py[4913]: waiting for server to shut down.... done
Feb 17 23:54:13 cluster-pgsql-02.local patroni.py[4913]: server stopped
А вот что в этот момент произошло на слейве:
Feb 17 19:54:12 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:12,353 INFO: does not have lock
Feb 17 19:54:12 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:12,776 INFO: no action. i am a secondary and i am following a leader
Feb 17 19:54:13 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:13,440 WARNING: request failed: GET http://192.xx.xx.121:8008/patroni (HTTPConnectionPool(host='192.xx.xx.121', port=8008
): Max retries exceeded with url: /patroni (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x1f12750>: Failed to establish a new connection: [Er
rno 111] Connection refused',)))
Feb 17 19:54:13 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:13,444 INFO: Got response from cluster_pgsql_03 http://192.xx.xx.122:8008/patroni: {"database_system_identifier": "63847
30077944883705", "postmaster_start_time": "2017-02-17 05:36:52.388 MSK", "xlog": {"received_location": 34997272728, "replayed_timestamp": null, "paused": false, "replayed_location": 34997272
728}, "patroni": {"scope": "clusters-pgsql", "version": "1.2.3"}, "state": "running", "role": "replica", "server_version": 90601}
Feb 17 19:54:13 cluster-pgsql-01 patroni.py: server promoting
Feb 17 19:54:13 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:13,961 INFO: cleared rewind flag after becoming the leader
Feb 17 19:54:14 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:14,179 INFO: promoted self to leader by acquiring session lock
Feb 17 19:54:23 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:23,436 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_01
Feb 17 19:54:23 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:23,857 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_01
Feb 17 19:54:24 cluster-pgsql-01 patroni.py: 2017-02-17 23:54:24,485 INFO: no action. i am the leader with the lock
Этот сервер перехватил роль мастера на себя.
А теперь вернем сервер 2 обратно в кластер:
Feb 18 00:02:11 cluster-pgsql-02.local systemd[1]: Started Runners to orchestrate a high-availability PostgreSQL.
Feb 18 00:02:11 cluster-pgsql-02.local systemd[1]: Starting Runners to orchestrate a high-availability PostgreSQL...
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,186 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_02
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,190 WARNING: Postgresql is not running.
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,190 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_02
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,398 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_02
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,400 INFO: starting as a secondary
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,412 INFO: rewind flag is set
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,609 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_02
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,609 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_02
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,609 INFO: changing primary_conninfo and restarting in progress
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:13,631 INFO: running pg_rewind from user=superuser host=192.xx.xx.120 port=5432 dbname=postgres sslmode=prefer sslcompression=1
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: servers diverged at WAL position 8/26000098 on timeline 25
Feb 18 00:02:13 cluster-pgsql-02.local patroni.py[56855]: rewinding from last common checkpoint at 8/26000028 on timeline 25
Feb 18 00:02:14 cluster-pgsql-02.local patroni.py[56855]: Done!
Feb 18 00:02:14 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:14,535 INFO: postmaster pid=56893
Feb 18 00:02:14 cluster-pgsql-02.local patroni.py[56855]: < 2017-02-18 00:02:14.554 MSK > LOG: redirecting log output to logging collector process
Feb 18 00:02:14 cluster-pgsql-02.local patroni.py[56855]: < 2017-02-18 00:02:14.554 MSK > HINT: Future log output will appear in directory "pg_log".
Feb 18 00:02:15 cluster-pgsql-02.local patroni.py[56855]: localhost:5432 - accepting connections
Feb 18 00:02:15 cluster-pgsql-02.local patroni.py[56855]: localhost:5432 - accepting connections
Feb 18 00:02:15 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:15,790 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_02
Feb 18 00:02:15 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:15,791 INFO: Lock owner: cluster_pgsql_01; I am cluster_pgsql_02
Feb 18 00:02:15 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:15,791 INFO: does not have lock
Feb 18 00:02:15 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:15,791 INFO: establishing a new patroni connection to the postgres cluster
Feb 18 00:02:16 cluster-pgsql-02.local patroni.py[56855]: 2017-02-18 00:02:16,014 INFO: no action. i am a secondary and i am following a leader
Patroni обнаружила что подключается к кластеру с имеющимся мастером, и обновив базу до текущего состояния, корректно приняла на себя роль слейва.
Попробуем создать ошибку на другом слое кластера, остановив haproxy на основном сервере keepalived.
По приоритету, эту роль у меня принимает второй сервер:
[root@cluster-pgsql-02 ~]# ip a2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000link/ether 00:50:56:a9:b8:7b brd ff:ff:ff:ff:ff:ffinet 192.xx.xx.121/24 brd 192.168.142.255 scope global ens160valid_lft forever preferred_lft forever**inet 192.xx.xx.125/32 scope global ens160 <---- виртуальный адрес кластера**valid_lft forever preferred_lft foreverinet6 fe80::xxx::4895:6d90/64 scope linkvalid_lft forever preferred_lft forever
Остановим haproxy:
Feb 18 00:18:54 cluster-pgsql-02.local Keepalived_vrrp[25018]: VRRP_Script(chk_haproxy) failedFeb 18 00:18:56 cluster-pgsql-02.local Keepalived_vrrp[25018]: VRRP_Instance(cluster_pgsql_02) Received higher prio advertFeb 18 00:18:56 cluster-pgsql-02.local Keepalived_vrrp[25018]: VRRP_Instance(cluster_pgsql_02) Entering BACKUP STATEFeb 18 00:18:56 cluster-pgsql-02.local Keepalived_vrrp[25018]: VRRP_Instance(cluster_pgsql_02) removing protocol VIPs.Feb 18 00:18:56 cluster-pgsql-02.local Keepalived_vrrp[25018]: Opening script file /usr/bin/shFeb 18 00:18:56 cluster-pgsql-02.local Keepalived_healthcheckers[25017]: Netlink reflector reports IP 192.xx.xx.125 removed
Keepalived отловил проблему, и убрал с себя виртуальный адрес, а также просигналил об этом соседям.
Смотрим что произошло на втором сервере:
Feb 18 00:18:56 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) forcing a new MASTER electionFeb 18 00:18:56 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) forcing a new MASTER electionFeb 18 00:18:56 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) forcing a new MASTER electionFeb 18 00:18:56 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) forcing a new MASTER electionFeb 18 00:18:57 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Transition to MASTER STATEFeb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Entering MASTER STATEFeb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) setting protocol VIPs.Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Sending gratuitous ARPs on ens160 for 192.xx.xx.125Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: Opening script file /usr/bin/shFeb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Received lower prio advert, forcing new electionFeb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Sending gratuitous ARPs on ens160 for 192.xx.xx.125Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_healthcheckers[41189]: Netlink reflector reports IP 192.xx.xx.125 addedFeb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Received lower prio advert, forcing new electionFeb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Sending gratuitous ARPs on ens160 for 192.xx.xx.125Feb 18 00:19:03 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Sending gratuitous ARPs on ens160 for 192.xx.xx.125
Дважды произошли перевыборы (потому что третий сервер кластера успел отправить свой анонс до первых выборов), сервер 1 принял на себя роль ведущего, и выставил виртуальный IP.
Убеждаемся в этом:
[root@cluster-pgsql-01 log]# ip a2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000link/ether 00:50:56:a9:f0:90 brd ff:ff:ff:ff:ff:ffinet 192.xx.xx.120/24 brd 192.xx.xx.255 scope global ens160valid_lft forever preferred_lft forever**inet 192.xx.xx.125/32 scope global ens160 <---- виртуальный адрес кластера присутствует!**valid_lft forever preferred_lft foreverinet6 fe80::1d75:40f6:a14e:5e27/64 scope linkvalid_lft forever preferred_lft forever
Теперь виртуальный IP присутствует на сервере, не являющимся мастером репликации. Однако это не имеет значения, поскольку в базу мы обращаемся через haproxy, а она мониторит состояние кластера независимо, и отправляет запросы всегда на мастер.
При возврате в строй haproxy на втором сервере снова происходят перевыборы (keepalived с бОльшим приоритетом встает в строй), и виртуальный IP возвращается на свое место.
В редких случаях бывает что слейв не может догнаться до мастера (например он упал очень давно и wal журнал успел частично удалиться). В таком случае можно полностью очистить директорию с базой на слейве:
«rm -rf /var/lib/pgsql/9.6/data», и перезапустить Patroni. Она сольет базу с мастера целиком. (Осторожно с очисткой «ненужных» баз, внимательно смотрите на каком сервере вы выполняете команду!!!)
В таком случае нужно воспользоваться утилитой patronictl. Команда reinit позволяет безопасно очистить конкретный узел кластера, на мастере она выполняться не будет. Спасибо за дополнение CyberDemon.
Сама утилита patronictl позволяет увидеть текущую ситуацию с кластером через командную строку, без обращений в DCS, и управлять кластером.
Пример отчета о состоянии кластера: /opt/patroni/patronictl.py -c /etc/patroni/postgres.yml list cluster-pgsql:
+---------------+------------------+-----------------+--------------+------------------+-----------+ | Cluster | Member | Host | Role | State | Lag in MB | +---------------+------------------+-----------------+--------------+------------------+-----------+ | cluster-pgsql | cluster_pgsql_01 | 192.xxx.xxx.120 | Leader | running | 0.0 | | cluster-pgsql | cluster_pgsql_02 | 192.xxx.xxx.121 | Sync standby | running | 0.0 | | cluster-pgsql | cluster_pgsql_03 | 192.xxx.xxx.122 | | creating replica | 33712.0 | +---------------+------------------+-----------------+--------------+------------------+-----------+
В данном случае наливается третья нода, ее отставание от мастера составляет 33 Гб. После завершения этого процесса она также переходит в состояние Running с нулевым лагом. Также можно обратить внимание что поле State у нее пустое. Это потому, что кластер в моем случае работает в синхронном режиме. Для уменьшения лага синхронной репликации, один слейв работает в синхронном режиме, а другой в обычном асинхронном. В случае пропадания мастера роли сместятся, и второй слейв перейдет в синхронный режим к ставшему мастером, первому слейву.
Послесловие
Единственное чего этому кластеру, на мой взгляд, не хватает для счастья — это пулинг коннектов и проксирование запросов на чтение на все слейвы для повышения производительности чтения, а запросов на вставки и обновления только на мастер.
В конфигурации с асинхронной репликацией, раскладывание нагрузки на чтение может привести к непредвиденным ответам, если слейв отстанет от мастера, это нужно учитывать.
Стриминговая (асинхронная) репликация не обеспечивает консистентности кластера в любой момент времени, и для этого нужна синхронная репликация.
В этом режиме мастер сервер будет ждать получения подтверждений о копировании и применении транзакций на слейвы, что замедлит работу базы. Однако если потери транзакций недопустимы (например какие-то финансовые приложения), синхронная репликация это ваш выбор.
Patroni поддерживает все варианты, и если синхронная репликация вам подойдет больше, вам всего лишь понадобится изменить значение нескольких полей в конфигах Patroni. Вопросы разных методов репликации прекрасно разобраны в документации к Patroni.
Кто-то наверное предложит использовать pgpool который сам, по сути, покрывает весь функционал этой системы. Он может и мониторить базы, и проксировать запросы, и выставлять виртуальный IP, а также осуществляет пулинг коннектов клиентов.
Да, он все это может. Но на мой взгляд схема с Patroni гораздо прозрачнее (конечно это только мое мнение), и во время экспериментов с pgpool я ловил странное поведение с его вочдогом и виртуальными адресами, которое не стал пока слишком глубоко дебажить, решив поискать другое решение.
Конечно возможно, что проблема тут только моих в руках, и позже я к тестированию pgpool планирую вернуться.
Однако, в любом случае, pgpool не сможет полностью автоматически управлять кластером, вводом новых и (особенно) возвратом сбойных серверов, работать с DCS. На мой взгляд это самый интересный функционал Patroni.
Спасибо за внимание, буду рад увидеть предложения по дальнейшему улучшению этого решения, и ответить на вопросы в комментариях.
Огромное спасибо Zalando за Patroni, и авторам исходного проекта Governor, который послужил основой для Patroni, а также Алексу Чистякову за шаблон роли для Ansible.
Полный код плейбуков и шаблонов Ansible, описанных в статье лежит тут. Буду благодарен за доработки от гуру Ansible и PostgreSQL. :)
Основные использованные статьи и источники:
Несколько вариантов кластеров PgSQL:
→ https://habrahabr.ru/post/301370/ → https://habrahabr.ru/post/213409/ → https://habrahabr.ru/company/etagi/blog/314000/
→ Пост о Patroni в блоге Zalando → Проект Patroni → ansible-role-patroni Алекса Чистякова → Governor — к сожалению разработка давно заморожена. → Книга Ansble for Devops — прекрасный учебник с кучей примеров применения Ansible.