Привет, Хабр! Встала передо мной недавно задача: настроить максимально надежный кластер серверов PostgreSQL версии 9.6.

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

Планируя кластер я проштудировал много статей, как из основной документации к PostgreSQL, так и различных howto, в том числе с Хабра, и пробовал настроить стандартный кластер с RepMgr, эксперементировал с pgpool.

В целом оно заработало, но у меня периодически всплывали проблемы с переключениями, требовалось ручное вмешательство для восстановления после аварий, и т.д. В общем я решил поискать еще варианты.

В итоге где-то (уже не вспомню точно где) нашел ссылку на прекрасный проект Zalando Patroni, и все заверте…

Введение


Patroni — это демон на python, позволяющий автоматически обслуживать кластеры PostgreSQL с различными типами репликации, и автоматическим переключением ролей.

Его особенная красота, на мой взгляд в том, что для поддержания актуальности кластера и выборов мастера используются распределенные DCS хранилища (поддерживаются Zookeeper, etcd, Consul).

Таким образом кластер легко интегрируется практически в любую систему, всегда можно выяснить кто в данный момент мастер, и статус всех серверов запросами в DCS, или напрямую к Patroni через http.

Ну и просто это красиво :)

Я потестировал работу Patroni, пробовал ронять мастера и другие сервера, пробовал наливать разные базы (~25 Гб база автоматически поднимается с нуля на 10Гб сети за несколько минут), и в целом мне проект Patroni очень понравился. После полной реализации описанной ниже схемы я проводил тестирование простым бенчером, который ходил в базу по единому адресу, и переживал падения всех элементов кластера (мастер сервера, haproxy, keepalived).

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

Для автоматизации разворачивания кластера и добавления новых серверов, решено было использовать привычный Ansible (я дам ссылки на получившиеся роли в конце статьи). В качестве DCS выступает уже применяемый у нас Consul.

У статьи две основные цели: показать пользователям PostgreSQL что есть такая прекрасная штука как Patroni (упоминаний в рунете вообще и на Хабре в частности, практически нет), и заодно немного поделиться опытом использования Ansible на простом примере, тем кто только начинает с ним работать.

Я постараюсь разъяснить все действо сразу на примере разбора Ansible ролей и плейбуков. Те, кто не использует Ansible, смогут перенести все действия в любимое средство автоматизированного управления серверами, либо выполнить их же вручную.

Поскольку большая часть yaml скриптов будет длинной, я буду заворачивать их в спойлер.
Рассказ будет разделен на две части — подготовка серверов и разворачивание непосредственно кластера.

Тем кто хорошо знаком с Ansible первая часть интересна не будет, поэтому рекомендую перейти сразу ко второй.

Часть I


Для этого примера я использую виртуальные машины на базе Centos 7. Виртуалки разворачиваются из шаблона который периодически обновляется (ядро, системные пакеты), но эта тема выходит за рамки данной статьи.

Отмечу только, что никакого прикладного или серверного софта на виртуалках заранее не установлено. Также вполне подойдут любые облачные ресурсы, например с AWS, DO, vScale, и т.п. Для них есть скрипты динамического инвентаря и интеграции с Ansible, либо можно прикрутить Terraform, так что весь процесс создания и удаления серверов c нуля может быть автоматизирован.

Для начала нужно создать инвентарь используемых ресурсов для Ansible. Ansible у меня (и по умолчанию) расположен в /etc/ansible. Создаем инвентарь в файле /etc/ansible/hosts:

[pgsql]
cluster-pgsql-01.local
cluster-pgsql-02.local
cluster-pgsql-03.local

У нас используется внутренняя доменная зона .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


Набор пакетов Essential служит для создания на любом сервере привычного рабочего окружения.
Группа пакетов Development tools, некоторые библиотеки -devel и python нужны pip-у для сборки Python модулей к PostgreSQL.

Мы используем виртуальные машины на базе 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



При установке 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



Запускаем обработку всех серверов:

Скрытый текст
~# ansible-playbook cluster-pgsql.yml --skip-tags 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

Помимо PostgreSQL наш кластер будет состоять из следующих компонентов:

1) haproxy для отслеживания состояния серверов и перенаправления запросов на мастер сервер.
2) keepalived для обеспечения наличия единой точки входа в кластер — виртуального IP.

Все плейбуки выполняемые данной ролью перечисляем в файле, запускаемом ansible по умолчанию:

/etc/ansible/roles/ansible-role-patroni/tasks/main.yml
- include: postgres.yml
- include: haproxy.yml
- include: keepalived.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:

/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-playbook cluster-pgsql.yml

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

Если же не хочется дольше ждать, или вы уверены что сервера полностью готовы, можно запустить ansible-playbook с ключом -t patroni.

Тогда будут выполнены только шаги из роли Patroni.

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

При добавлении новых серверов Patroni увидит через DCS что мастер кластера уже есть, автоматически скопирует с текущего мастера базу, и подключит к нему слейв.

В случае запуска слейва отставшего на какое-то время от мастера, Patroni автоматически вольет изменения при помощи pg_rewind.

Убеждаемся что все сервера запустились и выбрали себе роли:

~# journalctl -f -u patroni

Сообщения со слейва (сервер 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


По логам ясно видно что каждый сервер постоянно мониторит свой статус и статус мастера.
Попробуем остановить мастер:

~# systemctl stop patroni

спойлер
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 обратно в кластер:

~# systemctl start patroni

Заголовок спойлера
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 a
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 00:50:56:a9:b8:7b brd ff:ff:ff:ff:ff:ff
inet 192.xx.xx.121/24 brd 192.168.142.255 scope global ens160
valid_lft forever preferred_lft forever
inet 192.xx.xx.125/32 scope global ens160 <---- виртуальный адрес кластера
valid_lft forever preferred_lft forever
inet6 fe80::xxx::4895:6d90/64 scope link
valid_lft forever preferred_lft forever


Остановим haproxy:

~# systemctl stop haproxy ; journalctl -fl

Feb 18 00:18:54 cluster-pgsql-02.local Keepalived_vrrp[25018]: VRRP_Script(chk_haproxy) failed
Feb 18 00:18:56 cluster-pgsql-02.local Keepalived_vrrp[25018]: VRRP_Instance(cluster_pgsql_02) Received higher prio advert
Feb 18 00:18:56 cluster-pgsql-02.local Keepalived_vrrp[25018]: VRRP_Instance(cluster_pgsql_02) Entering BACKUP STATE
Feb 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/sh
Feb 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 election
Feb 18 00:18:56 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) forcing a new MASTER election
Feb 18 00:18:56 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) forcing a new MASTER election
Feb 18 00:18:56 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) forcing a new MASTER election
Feb 18 00:18:57 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Transition to MASTER STATE
Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Entering MASTER STATE
Feb 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.125
Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: Opening script file /usr/bin/sh
Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Received lower prio advert, forcing new election
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.125
Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_healthcheckers[41189]: Netlink reflector reports IP 192.xx.xx.125 added
Feb 18 00:18:58 cluster-pgsql-01.local Keepalived_vrrp[41190]: VRRP_Instance(cluster_pgsql_01) Received lower prio advert, forcing new election
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.125
Feb 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 a
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 00:50:56:a9:f0:90 brd ff:ff:ff:ff:ff:ff
inet 192.xx.xx.120/24 brd 192.xx.xx.255 scope global ens160
valid_lft forever preferred_lft forever
inet 192.xx.xx.125/32 scope global ens160 <---- виртуальный адрес кластера присутствует!
valid_lft forever preferred_lft forever
inet6 fe80::1d75:40f6:a14e:5e27/64 scope link
valid_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 — к сожалению разработка давно заморожена.
Поделиться с друзьями
-->

Комментарии (40)


  1. alexkuzko
    21.02.2017 16:58

    Отличная статья. Спасибо.
    Попробую параллельно поднять ваш конфиг и сравнить.


    1. citius
      21.02.2017 17:05

      Благодарю! Сравнивать будете с более традиционным кластером?
      Поделитесь потом впечатлением, если не сложно. :)


  1. arzonus
    21.02.2017 17:18

    Спасибо за статью!
    Хотел сам написать, да как то руки не доходили :)
    Мы использовали patroni, засунув его с postgresql в Docker контейнер.


    1. citius
      21.02.2017 17:19

      А мы как-то пока базы в контейнеры не решаемся.
      Кстати, какие проблемы в эксплуатации выявили? Интересует именно аспект контейнеризации.


      1. arzonus
        21.02.2017 17:26

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


        1. citius
          21.02.2017 17:31

          Страшного то нет, меня скорее беспокоит то, что с контейнерами меньше возможностей управлять ресурсами, например гарантировать процессорное или дисковое время.
          Мне кажется что для кустов микросервисов контейнеры подходят больше чем для таких тяжелых вещей.
          Хотя в целом у меня с контейнерами пока и опыта богатого нет. :)


          1. arzonus
            21.02.2017 17:52

            Тут зависит от задач. Так как если у вас небольшая база и вы не хотите управлять процессорным и дисковым временем, то вопрос где запускается БД особо не стоит. Однако если вам уже важны эти времена (я думаю на больших БД), то я думаю, что и Patroni тут уже не будет использоваться, а что то посерьезнее :)


          1. nulled
            21.02.2017 18:59

            На данный момент юзаем в контейнере postgresql, как уже сказали проблем нет. Но управление ресурсами это уже не совсем про контейнеры. Хотя положительные подвижки в этом аспекте есть.


        1. PutPixel
          21.02.2017 19:36

          Много где написано, что если монтировать docker volume на хост начинаются проблемы. У нас это приводило к порче всех файлов docker и сам демон не мог даже стартовать. Какие то особые настойки для docker?


          1. arzonus
            21.02.2017 20:56

            Никогда не сталкивался с такой проблемой. Обычная команда docker run -v /path:/path repo:tag.
            Я только пробовал подключать docker volume как Azure File System. Однако из-за отсутствия поддержки симлинков в Azure File System, постгрес не хотел работать :)


          1. nulled
            21.02.2017 21:54

            Подобные проблемы возникали на старых версиях docker, просто умирал dm в котором был rootfs контейнера. Fedora 25, последнее доступное ядро + данные, персистентность которых нужно обеспечить, пробрасываются через -v. Ну и сам докер держим последний.


  1. FireWolf2007
    21.02.2017 22:03

    Что будет, если не программно тушить сервисы, а выдергивать сетевой кабель?


    1. citius
      21.02.2017 22:04
      +1

      keepalived потеряют соседа и перевыберут мастера между собой.
      Patroni тоже выберут нового мастера, т.к. перестанет обновляться тикет в DCS.
      Текущий мастер будет изолирован, и начнет отставать по базе.
      После восстановления коннективити нужно будет просто перезапустить Patroni на старом мастере, и он должен штатно догнаться до слейва.


  1. greberj
    22.02.2017 01:53

    Интересная статья. Хочу попробовать реализовать и потестить.
    Замечание: ansible фейлит на zabbix на задаче скопировать скрипты. Их Вы не выложили в репозиторий. Если можно — выложите. Если нет, обойдемся.
    Спасибо за труд!


    1. citius
      22.02.2017 01:53

      Да, стормозил я с этими скриптами. Надо было роль от лишнего почистить. :)
      Впрочем мне не жалко, и ничего секретного там нет. Выложил в репозиторий.
      Как пример пойдет.


      1. greberj
        22.02.2017 11:09

        Спасибо за оперативность!
        Теперь ругается на /etc/ansible/files/mysql/.my.cnf, так как его тоже нет. Подкиньте еще пожалуйста этот файлик, чтоб не ругался больше.
        Спасибо!


        1. citius
          22.02.2017 13:01

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


  1. SonicGD
    22.02.2017 07:38

    Спасибо за статью. А на Stolon не смотрели?


    1. citius
      22.02.2017 10:13

      Он мне попадался, но питон мне лично гораздо проще чем Go, поэтому Patroni больше заинтересовала.
      Судя по описанию тоже стоящая штука.


  1. CyberDem0n
    22.02.2017 10:08
    +4

    «rm -rf /var/lib/pgsql/9.6/data», и перезапустить Patroni. Она сольет базу с мастера целиком.

    Хотите повторить опыт Gitlab? :) Пожалуйста, НИКОГДА так не делайте. Специально для таких случаев мы придумали patronictl reinit <cluster> <node>
    Эта команда абсолютна безопасна, текущий мастер просто откажется её выполнять.
    Реплика-же сделает всё как нужно: Patroni вначале остановит postgres, удалит data директорию, заберёт новый pg_basebackup с мастера и снова запустит postgres.


    Огромное Вам спасибо за статью от Zalando!


    1. citius
      22.02.2017 10:11

      Ох, крутяк какой. Каким-то образом я это пропустил, хотя точно помню что patronictl я ковырял.
      Добавлю в статью, спасибо! :)


  1. past
    22.02.2017 11:32

    service: name=ntpd state=stopped enabled=no
    Зачем Вы так жестоко ломаете ntp?


    1. citius
      22.02.2017 12:48

      А я писал в статье про проблемы с синхронизацией времени.
      Полное описание есть в KB VmWare тут.
      Нам пока на данный момент проще убить вообще ntpd, от vSphere мы планируем отказаться.


  1. NoOne
    22.02.2017 11:52

    Т.е. в вашей конфигурации получается, что виртуальный IP кластера может попасть на ноду слейва postgresql? И тогда при большой загрузке канала будет падать скорость, т.к. трафик удваивается (клиент<->слейв<->мастер).

    Текущий мастер будет изолирован, и начнет отставать по базе.
    После восстановления коннективити нужно будет просто перезапустить Patroni на старом мастере, и он должен штатно догнаться до слейва.


    Что если в мастер попали данные, на слейв улететь не успели и мастер потерял сеть? Один из слейвов все равно поднимется в мастер, а бывший мастер при возврате в сеть затрет уникальные данные и станет слейвом?


    1. citius
      22.02.2017 12:52

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

      2) Это проблема асинхронной репликации: транзакции которые не успеют считать слейвы будут потеряны.
      Именно поэтому у меня репликация синхронная, у нас такие потери недопустимы.
      Синхронная репликация обеспечивает консистентность на уровне транзакций.

      Недавно тут бы прекрасный был пост про CAP теорему, там эта проблема расписана в деталях.


      1. NoOne
        22.02.2017 13:47

        Да, обе проблемы ясные и понятно в какую сторону их решать. Просто всегда необходимо выбирать компромис между вариантами :)


      1. VolCh
        22.02.2017 17:26

        Синхронная репликация обеспечивает консистентность на уровне транзакций.

        А если ляжет слэйв, мастер продолжит выполнять транзакции?


        1. citius
          22.02.2017 17:31
          +1

          Асинхронный слейв будет переключен в синхронный режим.
          Если совсем не будет слейвов, patroni отключит синхронную репликацию.

          Вот цитата из документации:

          On each HA loop iteration Patroni re-evaluates synchronous standby choice. If the current synchronous standby is connected and has not requested its synchronous status to be removed it remains picked. Otherwise the cluster member avaiable for sync that is furthest ahead in replication is picked.


          1. VolCh
            22.02.2017 18:27

            Если совсем не будет слейвов, patroni отключит синхронную репликацию.

            Вот это интересовало. Спасибо.


          1. CyberDem0n
            22.02.2017 18:32
            +1

            Всё верно, но скоро ещё добавим synchronous_mode_strict.
            В этом случае мастер не будет выполнять транзакции если нет synchronous standby


            Но не забывайте, это поведение по умолчанию, и клиент всегда может решить что ему не нужна синхронная репликация и отключить её: SET local synchronous_commit = 'local';


            1. VolCh
              22.02.2017 18:34

              В этом случае мастер не будет выполнять транзакции если нет synchronous standby

              гибко регулировать можно будет? Типа из пяти слейвов в кластере минимум два должны быть с синхронной репликацией, чтобы мастер принимал транзакции?


              1. CyberDem0n
                23.02.2017 14:37
                +1

                Начиная с 9.6 такое возможно, но Patroni пока-что так не умеет.
                Если будет свободное время — сделаю, но с другое стороны мы всегда рады пулл-реквестам :)


  1. unnforgiven
    22.02.2017 12:03

    Хорошая статья, спасибо автору. Я писал тоже про кластер postgres только с repmgr. Не рассматривали repmgr?
    https://habrahabr.ru/company/etagi/blog/314000/


    1. citius
      22.02.2017 12:54

      Видел, я же даже в «использованные статьи» вас добавил. :)
      С Patroni подобная же схема, на мой взгляд гораздо проще и прозрачнее.


  1. SXN
    22.02.2017 14:13

    Отличная статья. Спасибо. надо попробовать.


  1. neb0t
    22.02.2017 16:40

    Статья обалденная, но скажите пожалуйста, что вы делаете если ansible trigger перезагружает мастера с которого «шарится» IP? Существует бородатый баг, когда нетворк перезагруажается — keepalive вылетает со скоростью света.
    Делали здесь


    1. citius
      22.02.2017 16:40

      Ссылка не вставилась, повторите плз.
      У нас таких проблем не возникало.


      1. neb0t
        22.02.2017 18:43

        Вот ссылка…
        Я попытался сделать реализацию с 2 лбл. Если на мастере перезапустить нетворк — тогда шаред ИР станет недоступным.


        1. citius
          22.02.2017 18:47

          Я только что попробовал перезапустить сеть на главном keepalived, ничего не случилось.
          Пинги не пропадали, сеть осталась рабочей.

          Это Centos 7.2 с ядром kernel-ml 4.9.0, перезапускал через systemctl restart network.


    1. neb0t
      22.02.2017 18:42

      https://blog.a2o.si/2013/10/08/restarting-network-with-keepalived-on-redhat-centos/