Patroni、Haproxy、Keepalivedに基づいた高信頼性のPostgreSQLクラスター

こんにちは、Habr! 私は最近、PostgreSQLバージョン9.6サーバーの最も信頼できるクラスターを構成するという課題に直面しました。

計画どおり、任意のサーバーまたは複数のサーバーの損失を経験し、事故後にサーバーを自動的にコミッションできるクラスターを取得したかったのです。

クラスターを計画するとき、PostgreSQLのメインドキュメントとHabrを含むさまざまなハウツーの両方から多くの記事を研究し、RepMgrを使用して標準クラスターを構成し、pgpoolを試しました。

一般的にはうまくいきましたが、時々切り替えに問題が発生したり、事故から回復するために手動の介入が必要になりました。 一般的に、私はより多くのオプションを探すことにしました。

その結果、どこか(正確にはどこにいるかは覚えていません)美しいZalando Patroniプロジェクトへのリンクを見つけて、すべてをまとめました...

はじめに


Patroniは、さまざまなタイプのレプリケーションでPostgreSQLクラスターを自動的に提供し、ロールを自動的に切り替えることができるPythonデーモンです。

私の意見では、その特別な美しさは、クラスターとウィザードの選択の関連性を維持するために、分散DCSリポジトリーが使用されることです(Zookeeper、etcd、Consulによってサポートされます)。

したがって、クラスターはほとんどすべてのシステムに簡単に統合でき、DCSの要求によって、またはhttpを介してPatroniに直接、現在誰がマスターであり、すべてのサーバーのステータスを常に確認できます。

まあ、それはただ美しいです:)

Patroniの作業をテストし、ウィザードと他のサーバーをドロップし、さまざまなデータベースを注ぎ込もうとしました(数分で25 GBのベースが自動的に0から10 GBのネットワークに上昇しました)。 以下に説明するスキームを完全に実装した後、単一のアドレスでデータベースにアクセスする単純なベンチャーでテストし、クラスターのすべての要素(サーバーマスター、haproxy、keepalived)の落下を経験しました。

役割を新しいマスターに転送する際の遅延は数秒でした。 以前のマスターをクラスターに戻すか、新しいサーバーを追加すると、ロールの変更は発生しません。

クラスターの展開を自動化し、新しいサーバーを追加するために、使い慣れたAnsibleを使用することにしました(この記事の最後に、作成されたロールへのリンクを示します)。 DCSとして、すでに使用されているConsulが使用されます。

この記事には2つの主な目標があります:PostgreSQLユーザーにPatroniのような素晴らしいものがあることを示すこと(実際にはRunet全般と特にHabrに言及することはほとんどありません)と同時に、Ansibleを使用して少しの経験を共有します。

Ansibleのロールとプレイブックを解析する例により、すべてのアクションを一度に説明しようとします。 Ansibleを使用しない人は、すべてのアクションを自動サーバー管理用のお気に入りのツールに転送するか、手動で実行できます。

yamlスクリプトのほとんどは長いので、スポイラーでラップします。
ストーリーは、サーバーの準備とクラスター自体の展開の2つの部分に分かれます。

Ansibleに慣れている人にとっては、最初の部分はおもしろくないので、2番目の部分に直接切り替えることをお勧めします。

パートI


この例では、Centos 7ベースの仮想マシンを使用しています。仮想マシンは、定期的に更新されるテンプレート(カーネル、システムパッケージ)からデプロイされますが、このトピックはこの記事の範囲外です。

仮想マシンにアプリケーションまたはサーバーソフトウェアが事前にインストールされていないことに注意してください。 たとえば、AWS、DO、vScaleなどのクラウドリソースも非常に適しています。 それらには、動的インベントリとAnsibleとの統合用のスクリプトがあります。または、Terraformをねじ込むことができるため、サーバーを最初から作成および削除するプロセス全体を自動化できます。

まず、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パッケージのパッケージは、あらゆるサーバーで使い慣れた作業環境を作成するために使用されます。
開発ツールパッケージグループ、いくつかの-develおよびpythonライブラリは、pipがPostgreSQL用のPythonモジュールを構築するために必要です。

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 


最初に、サーバーに個人用タイムゾーンが設定されているかどうかが確認され、設定されていない場合はモスクワが設定されます(そのようなサーバーの大部分があります)。

ESXi仮想マシンでの時間変動に問題があるため、ntpdを使用しません。その後、ntpdは時間の同期を拒否します。 (そして、いじくり回すパニック0は役に立ちません)。 したがって、クラウンでntpクライアントを5分に1回起動するだけです。

Zabbixエージェント:

/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.x.98にアクセスし、アクセスできないサーバーは同じサーバーの実際のアドレスに移動します。

sshキーの転送とsshの設定は、たとえばansible-galaxyにある別のロールに移動されました。

多くのオプションがあり、変更の本質は非常に簡単であるため、ここですべての内容を引用する意味はありません。

作成された構成をサーバーにロールします。 一般に、私はすべてのコンポーネントとクラスター自体のインストールをワンステップで実行しますが、すでに完全な設定をしていますが、このチュートリアルの目的のために、それぞれの章で2つのステップに分割する方が良いと思われます

サーバーのグループ用のプレイブックを作成します。

/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

githubリポジトリから私の例を完全にダウンロードした場合、Patroniの役割も持つことになりますが、これはまだ解決する必要はありません。

--skip-tags引数により、Ansibleはこのタグでマークされたステップをスキップするため、ansible-role-patroniロールは現在実行されません。

ディスク上にない場合、ひどいことは何も起こりません。アニスブルはこのキーを無視します。

Ansibleはすぐにルートとしてサーバーにログインします。権限のないユーザーとしてansibleを実行する必要がある場合は、ルート権限を必要とするステップに特別なフラグ「become:true」を追加する必要があります。

準備は終わりました。

パート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ユニットは、ロール内のテンプレートディレクトリに存在する必要があります。

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ファイル)の形式であり、テンプレートステップでは、このテンプレートを変数の置換で強制的に変換できます。

インベントリ内でクラスタ全体に共通の変数を直接示します。これは次の形式になります。

/ etc / ansible /ホスト
 [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-Patroni httpインターフェイスのパスワード(クラスターを変更するコマンドを送信するために必要)
patroni_postgres_password:postgresユーザーのパスワード。 新しいベースがpatroniによって作成された場合にインストールされます。
patroni_replicator_password-ユーザーレプリケーターのパスワード。 彼に代わって、スレーブへの複製が実行されます。

また、このファイルには、他のプレイブックまたはロールで使用される他の変数がリストされています。特に、ssh設定(キー、ユーザー)、サーバーのタイムゾーン、keepalivedクラスターのサーバー優先順位などがあります。

他のサーバーの構成も同様で、サーバー名と優先順位はそれに応じて変更されます(たとえば、3つのサーバーの場合は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インターフェースが使用されます。

サーバーにアクセスする場合:8008 URL(8008がデフォルトポート)Patroniはjsonのクラスターの状態に関するレポートを返し、サーバーがマスターであるかどうかの応答コードhttpも反映します。 そうである場合、コード200の応答があります。そうでない場合、コード503の応答があります。

Patroniのドキュメントを参照することを強くお勧めします。httpインターフェイスは非常に興味深いものであり、ロールを強制的に切り替えたり、クラスターを管理したりすることもできます。
同様に、これはPatroniのpatronyctl.pyコンソールユーティリティを使用して実行できます。

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プロトコルを使用し、デーモンの1つをメインのデーモンとして選択した結果(優先順位はconfigで指定され、各サーバーのhost_varsのkeepalived_priority変数にテンプレート化されます)、仮想IPアドレスを上げます。
残りの悪魔は辛抱強く待ちます。 現在のメインkeepalivedサーバーが何らかの理由で死んだり、近隣に事故を知らせたりすると、再選挙が行われ、次に優先順位の高いサーバーが仮想IPアドレスを取得します。

haproxyのクラッシュから保護するために、keepalivedデーモンはkillall -0 haproxyコマンドを1秒に1回実行してチェックを実行します。 haproxyプロセスがある場合はコード0を返し、ない場合は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を指定するだけです。

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構成には、ステータスの変更に関するメッセージを電報チャネルに送信するためのスクリプトも含まれています。

完全なクラスター構成をサーバーにロールします。

 ~# ansible-playbook cluster-pgsql.yml 

Ansibleはべき等であるため、つまり 以前に完了していない場合にのみステップを実行し、追加のパラメーターなしでプレイブックを開始できます。

長く待てない場合、またはサーバーの準備が完全に整っていることが確実な場合は、-t patroniスイッチを使用してansible-playbookを実行できます。

その後、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は、既存のマスターを使用してクラスターに接続し、データベースを現在の状態に更新していることを発見し、スレーブの役割を正しく引き継ぎました。

メインkeepalivedサーバーでhaproxyを停止して、クラスターの別のレイヤーでエラーを作成してみましょう。

優先順位により、2番目のサーバーはこの役割を受け入れます。

[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は問題をキャッチし、仮想アドレスを自身から削除し、それについて近隣に信号を送りました。

2番目のサーバーで何が起こったのかを見てみましょう。
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

再選出が2回行われたため(クラスターの3番目のサーバーが最初の選出前にアナウンスを送信できたため)、サーバー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がシステムに返されると、再選出が再び行われ(最高の優先順位でキープアライブが動作するようになります)、仮想IPがその場所に戻ります。

まれに、スレーブがマスターに追い付かないことがあります(たとえば、かなり前に落ちて、walログが部分的にリタイアすることがありました)。 この場合、スレーブのベースを使用してディレクトリを完全にクリアできます。

「Rm -rf /var/lib/pgsql/9.6/data」、Patroniを再起動します。 彼女はベースとマスターを完全にマージします。
(「不要な」データベースをクリーンアップする際の注意、コマンドを実行しているサーバーに注意してください!!!)

この場合、patronictlユーティリティを使用する必要があります。 reinitコマンドを使用すると、特定のクラスターノードを安全にクリーニングできますが、ウィザードでは実行されません。
Cyber​​Demonを追加していただきありがとうございます。

patronictlユーティリティ自体を使用すると、DCSにアクセスせずに、コマンドラインからクラスターの現在の状況を確認し、クラスターを管理できます。

クラスターステータスレポートの例:
/opt/patroni/patronictl.py -c /etc/patroni/postgres.yml list cluster-pgsql:
 + --------------- + ------------------ + -------------- --- + -------------- + ------------------ + ----------- +
 | クラスター| メンバー| ホスト| 役割| 州|  MBのラグ|
 + --------------- + ------------------ + -------------- --- + -------------- + ------------------ + ----------- +
 |  cluster-pgsql |  cluster_pgsql_01 |  192.xxx.xxx.120 | リーダー| ランニング|  0.0 |
 |  cluster-pgsql |  cluster_pgsql_02 |  192.xxx.xxx.121 | 同期スタンバイ| ランニング|  0.0 |
 |  cluster-pgsql |  cluster_pgsql_03 |  192.xxx.xxx.122 |  | レプリカの作成|  33712.0 |
 + --------------- + ------------------ + -------------- --- + -------------- + ------------------ + ----------- + 



この場合、3番目のノードが注がれ、マスターからの遅延は33 GBです。
このプロセスを完了すると、ゼロラグで実行状態になります。
Stateフィールドが空であることにも注意してください。これは、私の場合のクラスターが同期モードで動作するためです。同期レプリケーションの遅延を減らすために、一方のスレーブは同期モードで動作し、もう一方は通常の非同期で動作します。マスターが消えると、ロールがシフトし、2番目のスレーブがマスターになった最初のスレーブと同期モードに入ります。

あとがき


私の意見では、このクラスターが幸福に十分ではない唯一のことは、接続プーリングとすべてのスレーブへの読み取り要求のプロキシーで読み取りパフォーマンスを向上させ、ウィザードのみに要求を挿入および更新することです。

非同期レプリケーションを使用する構成では、読み込み負荷をアンロードすると予期しない応答が発生する可能性があります。スレーブがマスターより遅れている場合、これを考慮する必要があります。

ストリーミング(非同期)レプリケーションは、常にクラスターの一貫性を提供しません。これには同期レプリケーションが必要です。

このモードでは、マスターサーバーはトランザクションのコピーとスレーブへの適用の確認を待機し、データベースの速度が低下します。ただし、トランザクションの損失が許容できない場合(一部の金融アプリケーションなど)、同期レプリケーションが選択されます。

Patroniはすべてのオプションをサポートしており、同期レプリケーションがより適している場合は、Patroni configsのいくつかのフィールドの値を変更するだけです。
さまざまな複製方法に関する質問は、Patroniのドキュメントに詳しく記載されています。

実際、このシステムのすべての機能をカバーするpgpoolを使用することをお勧めします。データベース、プロキシ要求を監視し、仮想IPを設定し、クライアント接続のプーリングも実装できます。

はい、彼はこれをすべて行うことができます。しかし、私の意見では、パトロニとのスキームははるかに透明です(もちろんこれは私の意見です)、pgpoolでの実験中に、ウォッチドッグと仮想アドレスで奇妙な動作を見つけました。

もちろん、問題が私の手にあるだけであり、後でpgpoolのテストに戻る予定です。

ただし、いずれにしても、pgpoolはクラスターを完全に自動的に管理し、新しいサーバーを入力して(特に)失敗したサーバーを返し、DCSを操作することはできません。私の意見では、これはPatroniの最も興味深い機能です。

ご清聴ありがとうございました。このソリューションのさらなる改善のための提案をご覧いただき、コメントで質問にお答えします。

Zalando Patroniのためにどうもありがとう、そして元のプロジェクトの作者知事 Patroniの基礎を務め、そしてアレックスChistyakov Ansibleのためのテンプレートの役割のために。

記事で説明されているプレイブックとAnsibleテンプレートの完全なコードはこちらにあります。 AnsibleとPostgreSQLの達人からの改善に感謝します。 :)

:主な記事や情報源を使用

いくつかのオプションクラスターPgSQLは:

https://habrahabr.ru/post/301370/
https://habrahabr.ru/post/213409/
https://habrahabr.ru/company/etagi/を/ 314000 /ブログ

Zalandoでpatroni上のポストはブログ
プロジェクトpatroni
Ansible-役割patroniアレックスChistyakov
知事長期凍結の、残念ながら開発を- 。
Ansble for Devopsは、多数のAnsibleの例を含む素晴らしいチュートリアルです。

Source: https://habr.com/ru/post/J322036/


All Articles