From 9b34f000a840971979837b329a6d45e8c5eb814b Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:29:35 +0200 Subject: [PATCH 01/16] Create SETTINGS.rst --- SETTINGS.rst | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 SETTINGS.rst diff --git a/SETTINGS.rst b/SETTINGS.rst new file mode 100644 index 00000000..49666035 --- /dev/null +++ b/SETTINGS.rst @@ -0,0 +1,74 @@ +YAML Configuration Settings + +- *loop\_wait*: the number of seconds the loop will sleep. +- *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + +Consul +--------------- +- *host*: the host:port for the Consul endpoint. +- *scope*: the relative path used on Consul's HTTP API for this deployment; makes it possible to run multiple HA deployments from a single Consul cluster. +- *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + +etcd +--------------- +- *host*: the host:port for the etcd endpoint. +- *scope*: the relative path used on etcd's HTTP API for this deployment. Makes it possible to run multiple HA deployments from a single etcd cluster. +- *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + + +PostgreSQL +--------------- +- *admin*: + - *password*: admin password; user is created during initialization. + - *username*: admin username; user is created during initialization. It will have CREATEDB and CREATEROLE privileges. +- *callbacks*: callback scripts to run on certain actions. Patroni will pass the action, role and cluster name. (See scripts/aws.py as an example of how to write them.) + - *on\_reload*: run this script when configuration reload is triggered. + - *on\_restart*: run this script when the cluster restarts. + - *on\_role\_change*: run this script when the cluster is being promoted or demoted. + - *on\_start*: run this script when the cluster starts. + - *on\_stop*: run this script when the cluster stops. +- *connect\_address*: IP address + port through which Postgres is accessible from other nodes and applications. +- *create\_replica\_methods*: an ordered list of the create methods for turning a Patroni node into a new replica. "basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured as its own config item. +- *data\_dir*: file path to initialize and store Postgres data files. +- *initdb*: List options to be passed on to initdb. +- *data-checksums*: Must be enabled when pg_rewind is needed on 9.3. +- *encoding*: default encoding for new databases. +- *locale*: default locale for new databases. +- *listen*: IP address + port that Postgres listens to; must be accessible from other nodes in the cluster, if you're using streaming replication. Multiple comma-separated addresses are permitted, as long as the port component is appended after to the last one with a colon, i.e. ``listen: 127.0.0.1,127.0.0.2:5432``. Patroni will use the first address from this list to establish local connections to the PostgreSQL node. +- *maximum\_lag\_on\_failover*: the maximum bytes a follower may lag. +- *name*: the name of the Postgres host. Must be unique for the cluster. +- *pg\_hba*: list of lines that you should add to pg\_hba.conf. + - *- host all all 0.0.0.0/0 md5*. + - *- host replication replicator 127.0.0.1/32 md5* # A line like this is required for replication. +- *recovery\_conf*: additional configuration settings written to recovery.conf when configuring follower. + - *parameters*: list of configuration settings for Postgres. Many of these are required for replication to work. +- *replica\_method* for each create_replica_method other than basebackup, you would add a configuration section of the same name. At a minimum, this should include "command" with a full path to the actual script to be executed. Other configuration parameters will be passed along to the script in the form "parameter=value". +- *replication*: + - *username*: replication username; user will be created during initialization. + - *password*: replication password; user will be created during initialization. +- *use\_slots*: whether or not to use replication_slots. Must be False for PostgreSQL 9.3. You should comment out max_replication_slots before it becomes ineligible for leader status. +- *superuser*: + - *password*: password for the Postgres user, set during initialization. + +REST API +--------------- +- *connect\_address*: IP address and port through which restapi is accessible. +- *listen*: IP address and port that Patroni will listen to, to provide health-check information for HAProxy. +-* Optional*: +- *auth*: 'username:password' to protect dangerous REST API endpoints. + - *certfile*: Specifies a file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. + - *keyfile*: Specifies a file with the secret key in the PEM format. + + + ZooKeeper +--------------- +- *hosts*: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...']. +- *reconnect\_timeout*: how long you should try to reconnect to ZooKeeper after a connection loss. After this timeout, assume that you no longer have a lock and restart in read-only mode. +- *scope*: the relative path used on ZooKeeper for this deployment. Makes it possible to run multiple HA deployments from a single ZooKeeper cluster. +- *session\_timeout*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + +(subsection)ZooKeeper Exhibitor +If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you: +- *hosts*: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes. +- *poll\_interval*: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor +- *port*: Exhibitor port. From 6fddabf48cf45baf81c38f055365e0961e18726c Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:31:31 +0200 Subject: [PATCH 02/16] Update SETTINGS.rst --- SETTINGS.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index 49666035..ce97a7f6 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -1,5 +1,3 @@ -YAML Configuration Settings - - *loop\_wait*: the number of seconds the loop will sleep. - *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. From e84b633abde4aa10c3d633086140f54f400df2c9 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:33:50 +0200 Subject: [PATCH 03/16] Update SETTINGS.rst --- SETTINGS.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SETTINGS.rst b/SETTINGS.rst index ce97a7f6..cbe71717 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -1,3 +1,7 @@ +=========================== +YAML Configuration Settings +=========================== + - *loop\_wait*: the number of seconds the loop will sleep. - *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. From bb00ef40b6689a82da7ec06d8d7168b52b6d4992 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:35:15 +0200 Subject: [PATCH 04/16] Update SETTINGS.rst --- SETTINGS.rst | 69 ---------------------------------------------------- 1 file changed, 69 deletions(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index cbe71717..bd602104 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -5,72 +5,3 @@ YAML Configuration Settings - *loop\_wait*: the number of seconds the loop will sleep. - *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. -Consul ---------------- -- *host*: the host:port for the Consul endpoint. -- *scope*: the relative path used on Consul's HTTP API for this deployment; makes it possible to run multiple HA deployments from a single Consul cluster. -- *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - -etcd ---------------- -- *host*: the host:port for the etcd endpoint. -- *scope*: the relative path used on etcd's HTTP API for this deployment. Makes it possible to run multiple HA deployments from a single etcd cluster. -- *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - - -PostgreSQL ---------------- -- *admin*: - - *password*: admin password; user is created during initialization. - - *username*: admin username; user is created during initialization. It will have CREATEDB and CREATEROLE privileges. -- *callbacks*: callback scripts to run on certain actions. Patroni will pass the action, role and cluster name. (See scripts/aws.py as an example of how to write them.) - - *on\_reload*: run this script when configuration reload is triggered. - - *on\_restart*: run this script when the cluster restarts. - - *on\_role\_change*: run this script when the cluster is being promoted or demoted. - - *on\_start*: run this script when the cluster starts. - - *on\_stop*: run this script when the cluster stops. -- *connect\_address*: IP address + port through which Postgres is accessible from other nodes and applications. -- *create\_replica\_methods*: an ordered list of the create methods for turning a Patroni node into a new replica. "basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured as its own config item. -- *data\_dir*: file path to initialize and store Postgres data files. -- *initdb*: List options to be passed on to initdb. -- *data-checksums*: Must be enabled when pg_rewind is needed on 9.3. -- *encoding*: default encoding for new databases. -- *locale*: default locale for new databases. -- *listen*: IP address + port that Postgres listens to; must be accessible from other nodes in the cluster, if you're using streaming replication. Multiple comma-separated addresses are permitted, as long as the port component is appended after to the last one with a colon, i.e. ``listen: 127.0.0.1,127.0.0.2:5432``. Patroni will use the first address from this list to establish local connections to the PostgreSQL node. -- *maximum\_lag\_on\_failover*: the maximum bytes a follower may lag. -- *name*: the name of the Postgres host. Must be unique for the cluster. -- *pg\_hba*: list of lines that you should add to pg\_hba.conf. - - *- host all all 0.0.0.0/0 md5*. - - *- host replication replicator 127.0.0.1/32 md5* # A line like this is required for replication. -- *recovery\_conf*: additional configuration settings written to recovery.conf when configuring follower. - - *parameters*: list of configuration settings for Postgres. Many of these are required for replication to work. -- *replica\_method* for each create_replica_method other than basebackup, you would add a configuration section of the same name. At a minimum, this should include "command" with a full path to the actual script to be executed. Other configuration parameters will be passed along to the script in the form "parameter=value". -- *replication*: - - *username*: replication username; user will be created during initialization. - - *password*: replication password; user will be created during initialization. -- *use\_slots*: whether or not to use replication_slots. Must be False for PostgreSQL 9.3. You should comment out max_replication_slots before it becomes ineligible for leader status. -- *superuser*: - - *password*: password for the Postgres user, set during initialization. - -REST API ---------------- -- *connect\_address*: IP address and port through which restapi is accessible. -- *listen*: IP address and port that Patroni will listen to, to provide health-check information for HAProxy. --* Optional*: -- *auth*: 'username:password' to protect dangerous REST API endpoints. - - *certfile*: Specifies a file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. - - *keyfile*: Specifies a file with the secret key in the PEM format. - - - ZooKeeper ---------------- -- *hosts*: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...']. -- *reconnect\_timeout*: how long you should try to reconnect to ZooKeeper after a connection loss. After this timeout, assume that you no longer have a lock and restart in read-only mode. -- *scope*: the relative path used on ZooKeeper for this deployment. Makes it possible to run multiple HA deployments from a single ZooKeeper cluster. -- *session\_timeout*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - -(subsection)ZooKeeper Exhibitor -If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you: -- *hosts*: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes. -- *poll\_interval*: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor -- *port*: Exhibitor port. From e30755ddeb2123b8870cc3aa54866f24f1a662a3 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:41:56 +0200 Subject: [PATCH 05/16] Update SETTINGS.rst --- SETTINGS.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index bd602104..add2433e 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -2,6 +2,54 @@ YAML Configuration Settings =========================== -- *loop\_wait*: the number of seconds the loop will sleep. -- *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. +Global/Universal +---------------- +- **loop\_wait**: the number of seconds the loop will sleep. +- **(ttl**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + +Consul +------ +- **host**: the host:port for the Consul endpoint. +- **scope**: the relative path used on Consul's HTTP API for this deployment; makes it possible to run multiple HA deployments from a single Consul cluster. +- **ttl**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + +etcd +---- +- **host**: the host:port for the etcd endpoint. +- **scope**: the relative path used on etcd's HTTP API for this deployment. Makes it possible to run multiple HA deployments from a single etcd cluster. +- **ttl**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + +PostgreSQL +--------------- +- **admin**: + - **password**: admin password; user is created during initialization. + - **username**: admin username; user is created during initialization. It will have CREATEDB and CREATEROLE privileges. +- **callbacks**: callback scripts to run on certain actions. Patroni will pass the action, role and cluster name. (See scripts/aws.py as an example of how to write them.) + - **on\_reload**: run this script when configuration reload is triggered. + - **on\_restart**: run this script when the cluster restarts. + - **on\_role\_change**: run this script when the cluster is being promoted or demoted. + - **on\_start**: run this script when the cluster starts. + - **on\_stop**: run this script when the cluster stops. +- **connect\_address**: IP address + port through which Postgres is accessible from other nodes and applications. +- **create\_replica\_methods**: an ordered list of the create methods for turning a Patroni node into a new replica. "basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured as its own config item. +- **data\_dir**: file path to initialize and store Postgres data files. +- **initdb**: List options to be passed on to initdb. +- **data-checksums**: Must be enabled when pg_rewind is needed on 9.3. +- **encoding**: default encoding for new databases. +- **locale**: default locale for new databases. +- **listen**: IP address + port that Postgres listens to; must be accessible from other nodes in the cluster, if you're using streaming replication. Multiple comma-separated addresses are permitted, as long as the port component is appended after to the last one with a colon, i.e. ``listen: 127.0.0.1,127.0.0.2:5432``. Patroni will use the first address from this list to establish local connections to the PostgreSQL node. +- **maximum\_lag\_on\_failover**: the maximum bytes a follower may lag. +- **name**: the name of the Postgres host. Must be unique for the cluster. +- **pg\_hba**: list of lines that you should add to pg\_hba.conf. + - **- host all all 0.0.0.0/0 md5**. + - **- host replication replicator 127.0.0.1/32 md5**: A line like this is required for replication. +- **recovery\_conf**: additional configuration settings written to recovery.conf when configuring follower. + - **parameters**: list of configuration settings for Postgres. Many of these are required for replication to work. +- **replica\_method** for each create_replica_method other than basebackup, you would add a configuration section of the same name. At a minimum, this should include "command" with a full path to the actual script to be executed. Other configuration parameters will be passed along to the script in the form "parameter=value". +- **replication**: + - **username**: replication username; user will be created during initialization. + - **password**: replication password; user will be created during initialization. +- **use\_slots**: whether or not to use replication_slots. Must be False for PostgreSQL 9.3. You should comment out max_replication_slots before it becomes ineligible for leader status. +- **superuser**: + - **password**: password for the Postgres user, set during initialization. From 9a12f6371c3a0193f6b04902b87998d96bd55a6d Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:44:16 +0200 Subject: [PATCH 06/16] Update SETTINGS.rst --- SETTINGS.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/SETTINGS.rst b/SETTINGS.rst index add2433e..8b7a254a 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -53,3 +53,26 @@ PostgreSQL - **superuser**: - **password**: password for the Postgres user, set during initialization. +REST API +-------- +- **connect\_address**: IP address and port through which restapi is accessible. +- **listen**: IP address and port that Patroni will listen to, to provide health-check information for HAProxy. +-**Optional**: +- **auth**: 'username:password' to protect dangerous REST API endpoints. + - **certfile**: Specifies a file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. + - **keyfile**: Specifies a file with the secret key in the PEM format. + + ZooKeeper +---------- +- **hosts**: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...']. +- **reconnect\_timeout**: how long you should try to reconnect to ZooKeeper after a connection loss. After this timeout, assume that you no longer have a lock and restart in read-only mode. +- **scope**: the relative path used on ZooKeeper for this deployment. Makes it possible to run multiple HA deployments from a single ZooKeeper cluster. +- **session\_timeout**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. + + ZooKeeper Exhibitor +-------------------- +If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you: + +- **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes. +- **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor +- **port**: Exhibitor port. From 0f31a9918adb306ecae20cc9c7dc41cdfcecf414 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:45:47 +0200 Subject: [PATCH 07/16] Update SETTINGS.rst --- SETTINGS.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index 8b7a254a..3b078b35 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -62,14 +62,14 @@ REST API - **certfile**: Specifies a file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. - **keyfile**: Specifies a file with the secret key in the PEM format. - ZooKeeper +ZooKeeper ---------- - **hosts**: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...']. - **reconnect\_timeout**: how long you should try to reconnect to ZooKeeper after a connection loss. After this timeout, assume that you no longer have a lock and restart in read-only mode. - **scope**: the relative path used on ZooKeeper for this deployment. Makes it possible to run multiple HA deployments from a single ZooKeeper cluster. - **session\_timeout**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - ZooKeeper Exhibitor +ZooKeeper Exhibitor -------------------- If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you: From 00cadde425ce2a0c788fdbc8e8a6bcf721d7c818 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:46:53 +0200 Subject: [PATCH 08/16] Update SETTINGS.rst --- SETTINGS.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index 3b078b35..597ded22 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -57,10 +57,10 @@ REST API -------- - **connect\_address**: IP address and port through which restapi is accessible. - **listen**: IP address and port that Patroni will listen to, to provide health-check information for HAProxy. --**Optional**: -- **auth**: 'username:password' to protect dangerous REST API endpoints. - - **certfile**: Specifies a file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. - - **keyfile**: Specifies a file with the secret key in the PEM format. +- **Optional**: + - **auth**: 'username:password' to protect dangerous REST API endpoints. + - **certfile**: Specifies a file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. + - **keyfile**: Specifies a file with the secret key in the PEM format. ZooKeeper ---------- From 260cb17794ab769ca2a3264057208a4dd9f55162 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 14:51:29 +0200 Subject: [PATCH 09/16] Update SETTINGS.rst --- SETTINGS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index 597ded22..67c3986e 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -5,7 +5,7 @@ YAML Configuration Settings Global/Universal ---------------- - **loop\_wait**: the number of seconds the loop will sleep. -- **(ttl**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. +- **ttl**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. Consul ------ From c346e31d502102e9daa82bf69acef7c8197c55a2 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 15:25:58 +0200 Subject: [PATCH 10/16] Edited README + added new SETTINGS.rst file Check it out and let me know what you think. Here is the Settings file with the YAML Config Settings: https://github.com/zalando/patroni/blob/master/SETTINGS.rst We still need to do a bit more work on the intro paragraph to make Patroni's advantages/benefits a bit more clear. I also might do a bit more magic with the headers/subheads, to stack things a bit more clearly. --- README.rst | 223 ++++++++++++++++------------------------------------- 1 file changed, 67 insertions(+), 156 deletions(-) diff --git a/README.rst b/README.rst index 80861070..28e50698 100644 --- a/README.rst +++ b/README.rst @@ -1,34 +1,64 @@ -|Build Status| |Coverage Status| - Patroni: A Template for PostgreSQL HA with ZooKeeper, etcd or Consul ------------------------------------------------------------ +There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation `__. -Patroni originates from Compose Governor and includes plenty of new features. +Patroni is a template for you to create your own customized, high-availability solution using Python and — for maximum accessibility — a distributed configuration store like `ZooKeeper `__, `etcd `__ or `Consul `__. -*There are many ways to run high availability with PostgreSQL. Here, we -present a template for you to create your own customized, high-availability -solution using Python and — for maximum accessibility — a distributed -configuration store like ZooKeeper, etcd or Consul.* +We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely. -Getting Started ---------------- +.. contents:: + :local: + :depth: 1 + :backlinks: none + +============== +How Patroni Works +============== + +Patroni originated as a fork of `Governor `__, the project from Compose. It includes plenty of new features. + +For a diagram of the high availability decision loop, review `this pdf `__. + +For additional background info, see: + +* `PostgreSQL HA with Kubernetes and Patroni `__, talk by Josh Berkus at KubeCon 2016 (video) +* `Feb. 2016 Zalando Tech blog post `__ + +================ +Development Status +================ + +Patroni is in active development and accepts contributions. See our `Contributing `__ section below for more details. + +=========================== +Technical Requirements/Installation +=========================== + +**For Mac** + +To install requirements on a Mac, run the following: + +:: + + brew install postgresql etcd haproxy libyaml python + pip install psycopg2 pyyaml + +=================== +Running and Configuring +=================== To get started, do the following from different terminals: - :: > etcd --data-dir=data/etcd > ./patroni.py postgres0.yml > ./patroni.py postgres1.yml -From there, you will see a high-availability cluster start up. Test -different settings in the YAML files to see how its behavior changes. Kill -some of the components to see how the system behaves. +You will then see a high-availability cluster start up. Test different settings in the YAML files to see how the cluster’s behavior changes. Kill some of the components to see how the system behaves. Add more ``postgres*.yml`` files to create an even larger cluster. -We provide a haproxy configuration, which will give your application a -single endpoint for connecting to the cluster's leader. To configure, +Patroni provides an `HAProxy `__ configuration, which will give your application a single endpoint for connecting to the cluster's leader. To configure, run: :: @@ -39,167 +69,48 @@ run: > psql --host 127.0.0.1 --port 5000 postgres -How Patroni Works ------------------ - -For a diagram of the high availability decision loop, review this PDF: -`postgres-ha.pdf `__ - +=============== YAML Configuration ------------------- +=============== -For an example file, see ``postgres0.yml``. Regarding settings: - -- *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. -- *loop\_wait*: the number of seconds the loop will sleep - -- *restapi*: - - *listen*: IP address + port that Patroni will listen to, to provide health-check information for haproxy. - - *connect\_address*: IP address + port through which restapi is accessible. - - *auth*: (optional) 'username:password' to protect dangerous REST API endpoints. - - *certfile*: (optional) Specifies a file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. - - *keyfile*: (optional) Specifies a file with the secret key in the PEM format. - -- *etcd*: - - *scope*: the relative path used on etcd's HTTP API for this deployment; makes it possible to run multiple HA deployments from a single etcd cluster. - - *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - - *host*: the host:port for the etcd endpoint. - -- *consul*: - - *scope*: the relative path used on Consul's HTTP API for this deployment; makes it possible to run multiple HA deployments from a single Consul cluster. - - *ttl*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - - *host*: the host:port for the Consul endpoint. - -- *zookeeper*: - - *scope*: the relative path used on ZooKeeper for this deployment; makes it possible to run multiple HA deployments from a single ZooKeeper cluster. - - *session\_timeout*: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - - *reconnect\_timeout*: how long we should try to reconnect to ZooKeeper after a connection loss. After this timeout, assume that you no longer have a lock and restart in read-only mode. - - *hosts*: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...'] - - *exhibitor*: if you are running a ZooKeeper cluster under the Exhibitor supervisory, the following section might interest you: - - *poll\_interval*: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor - - *port*: Exhibitor port. - - *hosts*: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes. - -- *postgresql*: - - *name*: the name of the Postgres host. Must be unique for the cluster. - - *listen*: IP address + port that Postgres listens to; must be accessible from other nodes in the cluster, if you're using streaming replication. Multiple comma-separated addresses are permitted, as long as the port component is appended after to the last one with a colon, i.e. ``listen: 127.0.0.1,127.0.0.2:5432``. The first address from this list will be used by Patroni to establish local connections to the PostgreSQL node. - - - *connect\_address*: IP address + port through which Postgres is accessible from other nodes and applications. - - *data\_dir*: file path to initialize and store Postgres data files. - - *maximum\_lag\_on\_failover*: the maximum bytes a follower may lag. - - *use\_slots*: whether or not to use replication_slots. Must be False for PostgreSQL 9.3. You should comment out max_replication_slots before it becomes ineligible for leader status. - - - *initdb*: List options to be passed on to initdb - - *encoding*: default encoding for new databases - - *locale*: default locale for new databases - - *data-checksums* # When pg_rewind is needed on 9.3, this needs to be enabled - - - *pg\_hba*: list of lines which should be added to pg\_hba.conf. - - *- host all all 0.0.0.0/0 md5*. - - *- host replication replicator 127.0.0.1/32 md5* # A line like this is required for replication - - - *replication*: - - *username*: replication username; user will be created during initialization. - - *password*: replication password; user will be created during initialization. - - - *callbacks* callback scripts to run on certain actions. Patroni will pass the action, role and cluster name. See scripts/aws.py as an example on how to write them. - - *on\_start*: a script to run when the cluster starts. - - *on\_stop*: a script to run when the cluster stops. - - *on\_restart*: a script to run when the cluster restarts. - - *on\_reload*: a script to run when configuration reload is triggered. - - *on\_role\_change*: a script to run when the cluster is being promoted or demoted. - - - *superuser*: - - *password*: password for the Postgres user, set during initialization. - - - *admin*: - - *username*: admin username; user is created during initialization. It will have CREATEDB and CREATEROLE privileges. - - *password*: admin password; user is created during initialization. - - - *recovery\_conf*: additional configuration settings written to recovery.conf when configuring follower. - - *parameters*: list of configuration settings for Postgres. Many of these are required for replication to work. - - - *create\_replica\_methods*: an ordered list of the create methods for turning a patroni node into a new replica. - "basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured - as its own config item. - - - *replica\_method* for each create_replica_method other than basebackup, you would add a configuration section - of the same name. At a minimum, this should include "command" with a full path to the actual script to be - executed. Other configuration parameters will be passed along to the script in the form "parameter=value". +Go `here `__ for comprehensive information about settings for etcd, consul, and ZooKeeper. And for an example, see `postgres0.yml `__. +=============== Replication Choices -------------------- +=============== -Patroni uses Postgres' streaming replication. By default, this -replication is asynchronous. For more information, see the `Postgres -documentation on streaming -replication `__. +Patroni uses Postgres' streaming replication, which is asynchronous by default. For more information, see the `Postgres documentation on streaming replication `__. -Patroni's asynchronous replication configuration allows for -``maximum_lag_on_failover`` settings. This setting ensures failover will -not occur if a follower is more than a certain number of bytes behind -the follower. This setting should be increased or decreased based on -business requirements. +Patroni's asynchronous replication configuration allows for ``maximum_lag_on_failover`` settings. This setting ensures failover will not occur if a follower is more than a certain number of bytes behind the follower. This setting should be increased or decreased based on business requirements. -When asynchronous replication is not optimal for your use case, investigate -how Postgres's `synchronous -replication `__ -works. Synchronous replication ensures consistency across a cluster by -confirming that writes are written to a secondary before returning to -the connecting client with a success. The cost of synchronous -replication: reduced throughput on writes. This throughput will -be entirely based on network performance. In hosted datacenter -environments (like AWS, Rackspace, or any network you do not control), -synchrous replication significantly increases the variability of write -performance. If followers become inaccessible from the leader, the -leader effectively becomes readonly. +When asynchronous replication is not optimal for your use case, investigate Postgres's `synchronous replication `__. Synchronous replication ensures consistency across a cluster by confirming that writes are written to a secondary before returning to the connecting client with a success. The cost of synchronous replication: reduced throughput on writes. This throughput will be entirely based on network performance. -To enable a simple synchronous replication test, add the follow lines to -the ``parameters`` section of your YAML configuration files: +In hosted datacenter environments (like AWS, Rackspace, or any network you do not control), synchronous replication significantly increases the variability of write performance. If followers become inaccessible from the leader, the leader effectively becomes read-only. + +To enable a simple synchronous replication test, add the follow lines to the ``parameters`` section of your YAML configuration files: .. code:: YAML synchronous_commit: "on" synchronous_standby_names: "*" -When using synchronous replication, use at least three Postgres data nodes -to ensure write availability if one host fails. +When using synchronous replication, use at least three Postgres data nodes to ensure write availability if one host fails. -Choosing your replication schema is dependent on your business -considerations. Investigate both async and sync replication, as well as other -HA solutions, to determine which solution is best for you. +Choosing your replication schema is dependent on your business considerations. Investigate both async and sync replication, as well as other HA solutions, to determine which solution is best for you. +=============================== Applications Should Not Use Superusers --------------------------------------- +=============================== -When connecting from an application, always use a non-superuser. Patroni -requires access to the database to function properly. By using a -superuser from an application, you can potentially use the entire -connection pool, including the connections reserved for superusers with -the ``superuser_reserved_connections`` setting. If Patroni cannot access -the Primary because the connection pool is full, behavior will be -undesireable. +When connecting from an application, always use a non-superuser. Patroni requires access to the database to function properly. By using a superuser from an application, you can potentially use the entire connection pool, including the connections reserved for superusers, with the ``superuser_reserved_connections`` setting. If Patroni cannot access the Primary because the connection pool is full, behavior will be undesirable. -Requirements on a Mac ---------------------- +================ +Contributing +================ +Patroni accepts contributions from the open-source community; see the `Issues Tracker `__ for current needs. -Run the following on a Mac to install requirements: - -:: - - brew install postgresql etcd haproxy libyaml python - pip install psycopg2 pyyaml - -Notice ------- - -There are many different ways to do HA with PostgreSQL: See `the -PostgreSQL -documentation `__ -for a complete list. - -We call Patroni a "template" because it is far from being a one-size-fits-all -or plug-and-play replication system. It will have its own caveats. Use wisely. +Before making a contribution, please let us know by posting a comment to the relevant issue. +If you would like to propose a new feature, please first file a new issue explaining the feature you’d like to create. .. |Build Status| image:: https://travis-ci.org/zalando/patroni.svg?branch=master :target: https://travis-ci.org/zalando/patroni From 3508b33f8d64e21446441743a85cfa8da8bbda76 Mon Sep 17 00:00:00 2001 From: Oleksii Kliukin Date: Mon, 30 May 2016 15:55:55 +0200 Subject: [PATCH 11/16] Remove the done items --- TODO.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 91b77499..eb40b18b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,15 +1,11 @@ Failover ======== - When determining who should become master, include the minor version of PostgreSQL in the decision. -- Create a way to disable governance of a cluster, something like the existence of a "nogover" or "admin" file in PGDATA will stop patroni from changing the cluster state. Configuration ============== -- Provide a way to change postgresql.conf and pg_hba.conf of a running cluster on the Patroni level, without changing individual nodes. +- Provide a way to change pg_hba.conf of a running cluster on the Patroni level, without changing individual nodes. - Provide hooks to store and retrieve cluster-wide passwords without exposing them in a plain-text form to unauthorized users. -- Implement patronictl command to create initial configuration of the cluster with leader and member keys fixed to the user-supplied values in order to simplify migrations. -- Implement support for consul in addtion to etcd and zookeeper -- Complete zookeeper support in patronictl Documentation ============== From 79206b1fc4a4d5d7e1aae6e3016a9c8711ab2651 Mon Sep 17 00:00:00 2001 From: Lauri at Zalando Date: Mon, 30 May 2016 15:58:34 +0200 Subject: [PATCH 12/16] Update README.rst --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 28e50698..9892e787 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Patroni: A Template for PostgreSQL HA with ZooKeeper, etcd or Consul ------------------------------------------------------------ There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation `__. -Patroni is a template for you to create your own customized, high-availability solution using Python and — for maximum accessibility — a distributed configuration store like `ZooKeeper `__, `etcd `__ or `Consul `__. +Patroni is a template for you to create your own customized, high-availability solution using Python and — for maximum accessibility — a distributed configuration store like `ZooKeeper `__, `etcd `__ or `Consul `__. Database engineers, DBAs, DevOps engineers, and SREs who are looking to quickly deploy HA PostgreSQL in the datacenter—or anywhere else—will hopefully find it useful. We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely. @@ -19,6 +19,8 @@ Patroni originated as a fork of `Governor ` For a diagram of the high availability decision loop, review `this pdf `__. +For an example of a Docker-based deployment with Patroni, see `Spilo `__, currently in use at Zalando. + For additional background info, see: * `PostgreSQL HA with Kubernetes and Patroni `__, talk by Josh Berkus at KubeCon 2016 (video) From 455b7951b3aee3bf124c6714d2d18f3c00734041 Mon Sep 17 00:00:00 2001 From: Oleksii Kliukin Date: Mon, 30 May 2016 16:17:17 +0200 Subject: [PATCH 13/16] Fix the description of the zookeeper and initdb. --- SETTINGS.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index 67c3986e..9627edf2 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -34,9 +34,9 @@ PostgreSQL - **create\_replica\_methods**: an ordered list of the create methods for turning a Patroni node into a new replica. "basebackup" is the default method; other methods are assumed to refer to scripts, each of which is configured as its own config item. - **data\_dir**: file path to initialize and store Postgres data files. - **initdb**: List options to be passed on to initdb. -- **data-checksums**: Must be enabled when pg_rewind is needed on 9.3. -- **encoding**: default encoding for new databases. -- **locale**: default locale for new databases. + - **data-checksums**: Must be enabled when pg_rewind is needed on 9.3. + - **encoding**: default encoding for new databases. + - **locale**: default locale for new databases. - **listen**: IP address + port that Postgres listens to; must be accessible from other nodes in the cluster, if you're using streaming replication. Multiple comma-separated addresses are permitted, as long as the port component is appended after to the last one with a colon, i.e. ``listen: 127.0.0.1,127.0.0.2:5432``. Patroni will use the first address from this list to establish local connections to the PostgreSQL node. - **maximum\_lag\_on\_failover**: the maximum bytes a follower may lag. - **name**: the name of the Postgres host. Must be unique for the cluster. @@ -69,10 +69,8 @@ ZooKeeper - **scope**: the relative path used on ZooKeeper for this deployment. Makes it possible to run multiple HA deployments from a single ZooKeeper cluster. - **session\_timeout**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. -ZooKeeper Exhibitor --------------------- -If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you: +- **exhibitor**: If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you: -- **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes. -- **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor -- **port**: Exhibitor port. + - **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes. + - **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor + - **port**: Exhibitor port. From 1c6113eb97f45cb76a7a395ddaab4952057c0905 Mon Sep 17 00:00:00 2001 From: Oleksii Kliukin Date: Mon, 30 May 2016 16:18:00 +0200 Subject: [PATCH 14/16] Remove the quote --- SETTINGS.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/SETTINGS.rst b/SETTINGS.rst index 9627edf2..a5ba11b5 100644 --- a/SETTINGS.rst +++ b/SETTINGS.rst @@ -70,7 +70,6 @@ ZooKeeper - **session\_timeout**: the TTL to acquire the leader lock. Think of it as the length of time before initiation of the automatic failover process. - **exhibitor**: If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you: - - **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes. - **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor - **port**: Exhibitor port. From 677bd0536976ddaa0a1234e9d5a61f611f206445 Mon Sep 17 00:00:00 2001 From: Oleksii Kliukin Date: Mon, 30 May 2016 16:20:58 +0200 Subject: [PATCH 15/16] Add badges back, remove the reference to the outdated flow diagram. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9892e787..922f1e25 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,5 @@ +|Build Status| |Coverage Status| + Patroni: A Template for PostgreSQL HA with ZooKeeper, etcd or Consul ------------------------------------------------------------ There are many ways to run high availability with PostgreSQL; for a list, see the `PostgreSQL Documentation `__. @@ -17,8 +19,6 @@ How Patroni Works Patroni originated as a fork of `Governor `__, the project from Compose. It includes plenty of new features. -For a diagram of the high availability decision loop, review `this pdf `__. - For an example of a Docker-based deployment with Patroni, see `Spilo `__, currently in use at Zalando. For additional background info, see: From fb29b8426a14ffc43da763e0495d689da2f99ec5 Mon Sep 17 00:00:00 2001 From: Oleksii Kliukin Date: Mon, 30 May 2016 16:22:00 +0200 Subject: [PATCH 16/16] Remove the obsolete diagram. --- postgres-ha.pdf | Bin 20035 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 postgres-ha.pdf diff --git a/postgres-ha.pdf b/postgres-ha.pdf deleted file mode 100644 index e2eba17b2b74bc7a428b78f5399957efba61c610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20035 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

sIYZb6P*R;Id}!aH(jA>-<|!Xyp|5YlhUju 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.Y(6>VJ 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