Skip to content

Commit fb5d82f

Browse files
authored
More load balancer docs (#47)
1 parent f542675 commit fb5d82f

File tree

5 files changed

+261
-131
lines changed

5 files changed

+261
-131
lines changed

docs/features/load-balancer/healthchecks.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
icon: material/lan-check
33
---
4+
45
# Health checks
56

67
All databases load balanced by PgDog are regularly checked with health checks. A health check is a small query that ensures the database is reachable and able to handle requests.
@@ -90,7 +91,7 @@ The **default** value is **5 minutes** (`300_000` milliseconds).
9091
!!! note
9192
A database will not be placed back into the load balancer until it passes a health check again.
9293

93-
Make sure that `idle_healthcheck_timeout` is set to a lower setting than `ban_timeout`, so health checks have time to run before you expect the database to resume serving traffic.
94+
Make sure that `idle_healthcheck_interval` is set to a lower value than `ban_timeout`, so health checks have time to run before you expect the database to resume serving traffic.
9495

9596
### False positives
9697

docs/features/load-balancer/index.md

Lines changed: 17 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
---
22
next_steps:
3-
- ["Health checks", "/features/healthchecks/", "Ensure replica databases are up and running. Block offline databases from serving queries."]
3+
- ["Health checks", "/features/load-balancer/healthchecks", "Ensure replica databases are up and running. Block offline databases from serving queries."]
4+
- ["Replication & failover", "/features/load-balancer/replication-failover", "Replica lag detection and automatic traffic failover on replica promotion."]
5+
- ["Transactions", "/features/load-balancer/transactions", "Handling of manually-started transactions."]
6+
- ["Manual routing", "/features/load-balancer/manual-routing", "Overriding the load balancer using connection parameters or query comments."]
47
icon: material/lan
58
---
69

@@ -21,23 +24,23 @@ When a query is received by PgDog, it will inspect it using the native Postgres
2124
Applications don't have to manually route queries between databases or maintain several connection pools internally.
2225

2326
!!! note "SQL compatibility"
24-
PgDog's query parser is powered by the `pg_query` library, which extracts the Postgres native SQL parser directly from its source code. This makes it **100% compatible** with the PostgreSQL query language and allows PgDog to understand all valid Postgres queries.
27+
PgDog's query parser is powered by the `pg_query` library, which extracts the Postgres native SQL parser directly from its source code. This makes it **100% compatible** with the PostgreSQL query language and allows PgDog to understand all valid PostgreSQL queries.
2528

2629
## Load distribution
2730

2831
The load balancer is configurable and can distribute read queries between replicas using one of the following strategies:
2932

30-
* Round robin (default)
31-
* Random
32-
* Least active connections
33+
* [Round robin](#round-robin) (default)
34+
* [Random](#random)
35+
* [Least active connections](#least-active-connections)
3336

34-
Choosing the best strategy depends on your query workload and the size of the databases. Each one has its pros and cons. If you're not sure, using the **round robin** strategy usually works well for most deployments.
37+
Choosing the best strategy depends on your query workload and the size of the databases. Each strategy has its pros and cons. If you're not sure, using the **round robin** strategy usually works well for most deployments.
3538

3639
### Round robin
3740

38-
Round robin is often used in HTTP load balancers (e.g., nginx) to evenly distribute requests to hosts, in the same order as they appear in the configuration. Each database receives exactly one query before the next one is used.
41+
Round robin is often used in HTTP load balancers (e.g., nginx) to evenly distribute requests between hosts, in the same order as they appear in the configuration. Each database receives exactly one transaction before the next one is used.
3942

40-
This algorithm makes no assumptions about the capacity of each database host or the cost of each query. It works best when all queries have similar runtime cost and replica databases have identical hardware.
43+
This algorithm makes no assumptions about the capacity of each database or the cost of each query. It works best when all queries have similar runtime cost and replica databases have identical hardware.
4144

4245
##### Configuration
4346

@@ -50,7 +53,7 @@ load_balancing_strategy = "round_robin"
5053

5154
### Random
5255

53-
The random strategy sends queries to a database based on the output of a random number generator modulus the number of replicas in the configuration. This strategy assumes no knowledge about the runtime cost of queries or the size of database hardware.
56+
The random strategy sends queries to a database based on the output of a random number generator modulus the number of replicas in the configuration. This strategy assumes no knowledge about the runtime cost of queries or the capacity of database hardware.
5457

5558
This algorithm is often effective when queries have unpredictable runtime. By randomly distributing them between databases, it reduces hot spots in the replica cluster.
5659

@@ -87,6 +90,10 @@ The most common edge case is `SELECT FOR UPDATE` which locks rows for exclusive
8790

8891
The load balancer detects this and will send this query to the primary database instead of a replica.
8992

93+
!!! note "Transaction required"
94+
95+
`SELECT FOR UPDATE` is used inside manual [transactions](transactions.md) (i.e., started with `BEGIN`), which are routed to the primary database by default.
96+
9097
### Write CTEs
9198

9299
Some `SELECT` queries can trigger a write to the database from a CTE, for example:
@@ -98,115 +105,7 @@ WITH t AS (
98105
SELECT * FROM users INNER JOIN t ON t.id = users.id
99106
```
100107

101-
The load balancer recursively checks all of them and, if any CTE contains a query that could trigger a write, it will send the whole statement to the primary database.
102-
103-
### Transactions
104-
105-
All manual transactions are sent to the primary database by default. Transactions are started by sending the `BEGIN` command, for example:
106-
107-
```postgresql
108-
BEGIN;
109-
INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING *;
110-
COMMIT;
111-
```
112-
113-
PgDog processes queries immediately upon receiving them, and since transactions can contain multiple statements, it isn't possible to determine whether the whole transaction writes to the database. Therefore, it is more reliable to send it to the primary database.
114-
115-
!!! note "Replica lag"
116-
While transactions are used to atomically change multiple tables, they can also be used to manually route `SELECT` queries to the primary database. For example:
117-
118-
```postgresql
119-
BEGIN;
120-
SELECT * FROM users WHERE id = $1;
121-
COMMIT;
122-
```
123-
124-
125-
This is useful when the data in the table(s) has been recently updated and you want to avoid errors caused by replication lag. This often manifests as "record not-found"-style errors, for example:
126-
127-
```
128-
ActiveRecord::RecordNotFound (Couldn't find User with 'id'=9999):
129-
```
130-
131-
While sending read queries to the primary adds load, it is often necessary in real-time systems that are not equipped to handle replication delays.
132-
133-
134-
#### Read-only transactions
135-
136-
The PostgreSQL query language allows you to declare a transaction as read-only. This prevents it from writing data to the database. PgDog takes advantage of this property and will send such transactions to a replica database.
137-
138-
Read-only transactions can be started with the `BEGIN READ ONLY` command, for example:
139-
140-
```postgresql
141-
BEGIN READ ONLY;
142-
SELECT * FROM users WHERE id = $1;
143-
COMMIT;
144-
```
145-
146-
Read-only transactions are useful when queries depend on each other's results and need a consistent view of the database. Some Postgres database drivers allow this option to be set in the code, for example:
147-
148-
=== "pgx (go)"
149-
```go
150-
tx, err := conn.BeginTx(ctx, pgx.TxOptions{
151-
AccessMode: pgx.ReadOnly,
152-
})
153-
```
154-
=== "Sequelize (node)"
155-
```javascript
156-
const tx = await sequelize.transaction({
157-
readOnly: true,
158-
});
159-
```
160-
=== "SQLAlchemy (python)"
161-
Add `postgresql_readonly=True` to [execution options](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Engine.execution_options), like so:
162-
```python
163-
engine = create_engine("postgresql://user:pw@pgdog:6432/prod")
164-
.execution_options(postgresql_readonly=True)
165-
```
166-
167-
#### Primary-only connections
168-
169-
If you need to override the load balancer routing decision and send a query (or all queries) to the primary, it's possible to do so by configuring the `pgdog.role` connection parameter.
170-
171-
Configuring this connection parameter can be done at connection creation:
172-
173-
=== "Connection URL"
174-
```bash
175-
postgres://pgdog:pgdog@10.0.0.0:6432/database?options=-c%20pgdog.role%3Dprimary
176-
```
177-
=== "asyncpg (Python)"
178-
```python
179-
conn = await asyncpg.connect(
180-
user="pgdog",
181-
password="pgdog",
182-
database="pgdog",
183-
host="10.0.0.0",
184-
port=6432,
185-
server_settings={
186-
"pgdog.role": "primary",
187-
}
188-
)
189-
```
190-
=== "SQLAlchemy (Python)"
191-
```python
192-
engine = create_async_engine(
193-
"postgresql+asyncpg://pgdog:pgdog@10.0.0.0:6432/pgdog",
194-
pool_size=20,
195-
max_overflow=30,
196-
pool_timeout=30,
197-
pool_recycle=3600,
198-
pool_pre_ping=True,
199-
connect_args={"server_settings": {"pgdog.role": "primary"}},
200-
)
201-
```
202-
203-
The following values are supported:
204-
205-
| Value | Routing decision |
206-
|-|-|
207-
| `primary` | Queries are sent to the primary database only. |
208-
| `replica` | Queries are load balanced between primary and replicas, depending on the value of the [`read_write_split`](../../configuration/pgdog.toml/general.md#read_write_split) setting. |
209-
108+
The load balancer recursively checks CTEs and, if any of them contains a query that could trigger a write, it will send the whole statement to the primary database.
210109

211110
## Using the load balancer
212111

@@ -244,17 +143,6 @@ In case one of your replicas fails, you can configure the primary to serve read
244143
read_write_split = "include_primary_if_replica_banned"
245144
```
246145

247-
### Manual routing
248-
249-
!!! note "New feature"
250-
This feature was added in commit version [`c49339f`](https://github.com/pgdogdev/pgdog/commit/c49339f70db8be63b76ebb3aa0f31433c4266f21). If using this feature, make sure to run the latest version of PgDog.
251-
252-
If your query is replica-lag sensitive (e.g., you are reading data that you just wrote), you can route it to the primary manually. The query router supports doing this with a query comment:
253-
254-
```postgresql
255-
/* pgdog_role: primary */ SELECT * FROM users WHERE id = $1
256-
```
257-
258146
## Learn more
259147

260148
{{ next_steps_links(next_steps) }}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
icon: material/routes
3+
---
4+
5+
# Manual routing
6+
7+
PgDog's load balancer uses the PostgreSQL parser to understand and route queries between the primary and replicas. If you want more control, you can provide the load balancer with hints, influencing its routing decisions.
8+
9+
This can be done on a per-query basis by using a comment, or on the entire client connection, with a session parameter.
10+
11+
## Query comments
12+
13+
If your query is replica-lag sensitive (e.g., you are reading data that you just wrote), you can route it to the primary manually. The load balancer supports doing this with a query comment:
14+
15+
```postgresql
16+
/* pgdog_role: primary */ SELECT * FROM users WHERE id = $1
17+
```
18+
19+
Query comments are supported in all types of queries, including prepared statements. If you're using the latter, the comments are parsed only once per client connection, removing any performance overhead of extracting them from the query.
20+
21+
22+
## Parameters
23+
24+
Parameters are connection-specific settings that can be set on connection creation to configure database behavior. For example, this is how ORMs and web frameworks control settings like `application_name`, `statement_timeout` and many others.
25+
26+
The Postgres protocol doesn't have any restrictions on parameter names or values, and PgDog has access to them at connection creation.
27+
28+
The following two parameters allow you to control which database is used for all queries on a client connection:
29+
30+
| Parameter | Description |
31+
|-|-|
32+
| **`pgdog.role`** | Determines whether queries are sent to the primary database or the replica(s). |
33+
| **`pgdog.shard`** | Determines which shard the queries are sent to. |
34+
35+
The `pgdog.role` parameter accepts the following values:
36+
37+
| Parameter value | Behavior |
38+
|-|-|
39+
| `primary` | All queries are sent to the primary database. |
40+
| `replica` | All queries are load balanced between replica databases, and possibly the primary if [`read_write_split`](../../configuration/pgdog.toml/general.md#read_write_split) is set to `include_primary` (default). |
41+
42+
The `pgdog.shard` parameter accepts a shard number for any database specified in [`pgdog.toml`](../../configuration/pgdog.toml/databases.md).
43+
44+
### Setting the parameters
45+
46+
Configuring parameters at connection creation is PostgreSQL driver-specific. Some of the common drivers and frameworks are shown below.
47+
48+
#### Database URL
49+
50+
Most PostgreSQL client libraries support the database URL format and can accept connection parameters as part of the URL. For example, when using `psql`, you can set the `pgdog.role` parameter like so:
51+
52+
```
53+
psql postgres://user:password@host:6432/db?options=-c%20pgdog.role%3Dreplica
54+
```
55+
56+
Depending on the environment, the parameters may need to be URL-encoded, e.g., `%20` is a space and `%3D` is the equals (`=`) sign.
57+
58+
=== "asyncpg"
59+
60+
[asyncpg](https://pypi.org/project/asyncpg/) is a popular PostgreSQL driver for asynchronous Python applications. It allows you to set connection parameters on connection setup:
61+
62+
```python
63+
conn = await asyncpg.connect(
64+
user="pgdog",
65+
password="pgdog",
66+
database="pgdog",
67+
host="10.0.0.0",
68+
port=6432,
69+
server_settings={
70+
"pgdog.role": "primary",
71+
}
72+
)
73+
```
74+
75+
=== "SQLAlchemy"
76+
77+
[SQLAlchemy](https://www.sqlalchemy.org/) is a Python ORM, which supports any number of PostgreSQL connection drivers. For example, if you're using `asyncpg`, you can set connection parameters as follows:
78+
79+
```python
80+
engine = create_async_engine(
81+
"postgresql+asyncpg://pgdog:[email protected]:6432/pgdog",
82+
pool_size=20,
83+
# [...]
84+
connect_args={"server_settings": {"pgdog.role": "primary"}},
85+
)
86+
```
87+
88+
=== "Rails / ActiveRecord"
89+
90+
[Rails](https://rubyonrails.org/) and ActiveRecord support passing connection parameters in the `database.yml` configuration file:
91+
92+
```yaml
93+
# config/database.yml
94+
production:
95+
adapter: postgresql
96+
database: pgdog
97+
username: user
98+
password: password
99+
host: 10.0.0.0
100+
options: "-c pgdog.role=replica -c pgdog.shard=0"
101+
```
102+
103+
These options are passed to the [`pg`](https://github.com/ged/ruby-pg) driver. If you're using it directly, you can create connections like so:
104+
105+
```ruby
106+
require "pg"
107+
108+
conn = PG.connect(
109+
host: "10.0.0.0",
110+
# [...]
111+
options: "-c pgdog.role=primary -c pgdog.shard=1"
112+
)
113+
```
114+
115+
### Using `SET`
116+
117+
The PostgreSQL protocol supports configuring connection parameters using the `SET` statement. This also works for configuring both `pgdog.role` and `pgdog.shard`.
118+
119+
For example, to make sure all subsequent queries to be sent to the primary, you can execute the following statement:
120+
121+
```postgresql
122+
SET pgdog.role TO "primary";
123+
```
124+
125+
The parameter is persisted on the connection until it's closed or the parameter is changed with another `SET` statement.
126+
127+
#### Inside transactions
128+
129+
If you want to provide a transaction routing hint without affecting the rest of the connection, you can use `SET LOCAL` instead:
130+
131+
```postgresql
132+
BEGIN;
133+
SET LOCAL pgdog.role TO "primary";
134+
```
135+
136+
In this example, all transaction statements (including the `BEGIN` statement) will be sent to the primary database. Whether the transaction is committed or reverted, the value of `pgdog.role` will be reset to its previous value.
137+
138+
!!! note "Statement ordering"
139+
To make sure PgDog intercepts the routing hint early enough in the transaction flow, make sure to send all hints _before_ executing actual queries.
140+
141+
The following flow, for example, _will not_ work:
142+
143+
```postgresql
144+
BEGIN;
145+
SELECT * FROM users WHERE id = $1;
146+
SET LOCAL pgdog.role TO "primary"; -- The client is already connected to a server.
147+
INSERT INTO users (id) VALUES ($1); -- If connected to a replica, this will fail.
148+
```
149+
150+
151+
152+
## Disabling the parser
153+
154+
In certain situations, the overhead of parsing queries may be too high, e.g., when your application can't use prepared statements.
155+
156+
If you've configured the desired database role (and/or shard) for each of your application connections, you can disable the query parser in [pgdog.toml](../../configuration/pgdog.toml/general.md#query_parser):
157+
158+
```toml
159+
[general]
160+
query_parser = "off"
161+
```
162+
163+
Once it's disabled, PgDog will rely solely on the `pgdog.role` and `pgdog.shard` parameters to make its routing decisions.
164+
165+
### Session state & `SET`
166+
167+
The query parser is used to intercept and interpret `SET` commands. If the parser is disabled and your application uses `SET` commands to configure the connection, PgDog will not be able to guarantee that all connections have the correct session settings in [transaction mode](../transaction-mode.md).

docs/features/load-balancer/replication-failover.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ By default, PgDog will not query databases for their replication status. To enab
4949
lsn_check_delay = 0
5050

5151
# Run LSN check every second.
52-
lsn_check_interval = 1_000
52+
lsn_check_interval = 1_000
5353
```
5454

5555
| Setting | Description |

0 commit comments

Comments
 (0)