You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
| Transaction pooling |:heavy_check_mark:| Identical to PgBouncer. |
16
+
| Session pooling |:heavy_check_mark:| Identical to PgBouncer. |
17
+
|`COPY` support |:heavy_check_mark:| Both `COPY TO` and `COPY FROM` are supported. |
18
+
| Query cancellation |:heavy_check_mark:| Supported both in transaction and session pooling modes. |
19
+
| Load balancing of read queries |:heavy_check_mark:| Using round-robin between replicas. Primary is included when `primary_reads_enabled` is enabled (default). |
20
+
| Sharding |:heavy_check_mark:| Transactions are sharded using `SET SHARD TO` and `SET SHARDING KEY TO` syntax extensions; see examples below. |
21
+
| Failover |:heavy_check_mark:| Replicas are tested with a health check. If a health check fails, remaining replicas are attempted; see below for algorithm description and examples. |
22
+
| Statistics reporting |:heavy_check_mark:| Statistics similar to PgBouncers are reported via StatsD. |
23
+
| Live configuration reloading |:x::wrench:| On the roadmap; currently config changes require restart. |
24
+
| Client authentication |:x::wrench:| On the roadmap; currently all clients are allowed to connect and one user is used to connect to Postgres. |
25
+
26
+
## Deployment
27
+
28
+
See `Dockerfile` for example deployment using Docker. The pooler is configured to spawn 4 workers so 4 CPUs are recommended for optimal performance.
29
+
That setting can be adjusted to spawn as many (or as little) workers as needed.
30
+
11
31
## Local development
12
32
13
33
1. Install Rust (latest stable will work great).
@@ -18,7 +38,7 @@ Meow. PgBouncer rewritten in Rust, with sharding, load balancing and failover su
18
38
19
39
### Tests
20
40
21
-
You can just PgBench to test your changes:
41
+
Quickest way to test your changes is to use pgbench:
| Transaction pooling |:heavy_check_mark:|:heavy_check_mark:| Used by default for all tests. |
54
+
| Session pooling |:x:|:heavy_check_mark:| Easiest way to test is to enable it and run pgbench - results will be better than transaction pooling as expected. |
55
+
|`COPY`|:heavy_check_mark:|:heavy_check_mark:|`pgbench -i` uses `COPY`. `COPY FROM` is tested as well. |
| Load balancing |:x:|:heavy_check_mark:| We could test this by emitting statistics for each replica and compare them. |
58
+
| Failover |:x:|:heavy_check_mark:| Misconfigure a replica in `pgcat.toml` and watch it forward queries to spares. CI testing could include using Toxiproxy. |
59
+
| Sharding |:heavy_check_mark:|:heavy_check_mark:| See `tests/sharding` and `tests/ruby` for an Rails/ActiveRecord example. |
60
+
| Statistics reporting |:x:|:heavy_check_mark:| Run `nc -l -u 8125` and watch the stats come in every 15 seconds. |
61
+
32
62
33
-
1. Session mode.
34
-
2. Transaction mode.
35
-
3.`COPY` protocol support.
36
-
4. Query cancellation.
37
-
5. Round-robin load balancing of replicas.
38
-
6. Banlist & failover.
39
-
7. Sharding!
40
-
8. Explicit query routing to primary or replicas.
63
+
## Usage
41
64
42
65
### Session mode
43
-
Each client owns its own server for the duration of the session. Commands like `SET` are allowed.
44
-
This is identical to PgBouncer session mode.
66
+
In session mode, a client talks to one server for the duration of the connection. Prepared statements, `SET`, and advisory locks are supported. In terms of supported features, there is very little if any difference between session mode and talking directly to the server.
67
+
68
+
To use session mode, change `pool_mode = "session"`.
45
69
46
70
### Transaction mode
47
-
The connection is attached to the server for the duration of the transaction. `SET` will pollute the connection,
48
-
but `SET LOCAL` works great. Identical to PgBouncer transaction mode.
71
+
In transaction mode, a client talks to one server for the duration of a single transaction; once it's over, the server is returned to the pool. Prepared statements, `SET`, and advisory locks are not supported; alternatives are to use `SET LOCAL` and `pg_advisory_xact_lock` which are scoped to the transaction.
49
72
50
-
### COPY protocol
51
-
That one isn't particularly special, but good to mention that you can `COPY` data in and from the server
52
-
using this pooler.
73
+
This mode is enabled by default.
53
74
54
-
### Query cancellation
55
-
Okay, this is just basic stuff, but we support cancelling queries. If you know the Postgres protocol,
56
-
this might be relevant given than this is a transactional pooler but if you're new to Pg, don't worry about it, it works.
75
+
### Load balancing of read queries
76
+
All queries are load balanced against the configured servers using the round-robin algorithm. The most straight forward configuration example would be to put this pooler in front of several replicas and let it load balance all queries.
57
77
58
-
### Round-robin load balancing
59
-
This is the novel part. PgBouncer doesn't support it and suggests we use DNS or a TCP proxy instead.
60
-
We prefer to have everything as part of one package; arguably, it's easier to understand and optimize.
61
-
This pooler will round-robin between multiple replicas keeping load reasonably even. If the primary is in
62
-
the pool as well, it'll be treated as a replica for read-only queries.
78
+
If the configuration includes a primary and replicas, the queries can be separated with the built-in query parser. The query parser will interpret the query and route all `SELECT` queries to a replica, while all other queries including explicit transactions will be routed to the primary.
63
79
64
-
### Banlist & failover
65
-
This is where it gets even more interesting. If we fail to connect to one of the replicas or it fails a health check,
66
-
we add it to a ban list. No more new transactions will be served by that replica for, in our case, 60 seconds. This
67
-
gives it the opportunity to recover while clients are happily served by the remaining replicas.
80
+
The query parser is disabled by default.
68
81
69
-
This decreases error rates substantially! Worth noting here that on busy systems, if the replicas are running too hot,
70
-
failing over could bring even more load and tip over the remaining healthy-ish replicas. In this case, a decision should be made:
71
-
either lose 1/x of your traffic or risk losing it all eventually. Ideally you overprovision your system, so you don't necessarily need
72
-
to make this choice :-).
82
+
#### Query parser
83
+
The query parser will do its best to determine where the query should go, but sometimes that's not possible. In that case, the client can select which server it wants using this custom SQL syntax:
73
84
74
-
### Sharding
75
-
We're implemeting Postgres' `PARTITION BY HASH` sharding function for `BIGINT` fields. This works well for tables that use `BIGSERIAL` primary key which I think is common enough these days. We can also add many more functions here, but this is a good start. See `src/sharding.rs` and `tests/sharding/partition_hash_test_setup.sql` for more details on the implementation.
85
+
```sql
86
+
-- To talk to the primary for the duration of the next transaction:
87
+
SET SERVER ROLE TO 'primary';
88
+
89
+
-- To talk to the replica for the duration of the next transaction:
90
+
SET SERVER ROLE TO 'replica';
91
+
92
+
-- Let the query parser decide
93
+
SET SERVER ROLE TO 'auto';
94
+
95
+
-- Pick any server at random
96
+
SET SERVER ROLE TO 'any';
97
+
98
+
-- Reset to default configured settings
99
+
SET SERVER ROLE TO 'default';
100
+
```
76
101
77
-
The biggest advantage of using this sharding function is that anyone can shard the dataset using Postgres partitions
78
-
while also access it for both reads and writes using this pooler. No custom obscure sharding function is needed and database sharding can be done entirely in Postgres.
102
+
The setting will persist until it's changed again or the client disconnects.
79
103
80
-
To select the shard we want to talk to, we introduced special syntax:
104
+
By default, all queries are routed to all servers; `default_role` setting controls this behavior.
105
+
106
+
### Failover
107
+
All servers are checked with a `SELECT 1` query before being given to a client. If the server is not reachable, it will be banned and cannot serve any more transactions for the duration of the ban. The queries are routed to the remaining servers. If all servers become banned, the ban list is cleared: this is a safety precaution against false positives. The primary can never be banned.
108
+
109
+
The ban time can be changed with `ban_time`. The default is 60 seconds.
110
+
111
+
### Sharding
112
+
We use the `PARTITION BY HASH` hashing function, the same as used by Postgres for declarative partitioning. This allows to shard the database using Postgres partitions and place the partitions on different servers (shards). Both read and write queries can be routed to the shards using this pooler.
113
+
114
+
To route queries to a particular shard, we use this custom SQL syntax:
81
115
82
116
```sql
117
+
-- To talk to a shard explicitely
118
+
SET SHARD TO '1';
119
+
120
+
-- To let the pooler choose based on a value
83
121
SET SHARDING KEY TO '1234';
84
122
```
85
123
86
-
This sharding key will be hashed and the pooler will select a shard to use for the next transaction. If the pooler is in session mode, this sharding key has to be set as the first query on startup & cannot be changed until the client re-connects.
124
+
The active shard will last until it's changed again or the client disconnects. By default, the queries are routed to shard 0.
87
125
88
-
### Explicit read/write query routing
126
+
For hash function implementation, see `src/sharding.rs` and `tests/sharding/partition_hash_test_setup.sql`.
89
127
90
-
If you want to have the primary and replicas in the same pooler, you'd probably want to
91
-
route queries explicitely to the primary or replicas, depending if they are reads or writes (e.g `SELECT`s or `INSERT`/`UPDATE`, etc). To help with this, we introduce some more custom syntax:
128
+
#### ActiveRecord/Rails
92
129
93
-
```sql
94
-
SET SERVER ROLE TO 'primary';
95
-
SET SERVER ROLE TO 'replica';
130
+
```ruby
131
+
classUser < ActiveRecord::Base
132
+
end
133
+
134
+
# Metadata will be fetched from shard 0
135
+
ActiveRecord::Base.establish_connection
136
+
137
+
# Grab a bunch of users from shard 1
138
+
User.connection.execute "SET SHARD TO '1'"
139
+
User.take(10)
140
+
141
+
# Using id as the sharding key
142
+
User.connection.execute "SET SHARDING KEY TO '1234'"
143
+
User.find_by_id(1234)
144
+
145
+
# Using geographical sharding
146
+
User.connection.execute "SET SERVER ROLE TO 'primary'"
147
+
User.connection.execute "SET SHARDING KEY TO '85'"
# Let the query parser figure out where the query should go.
151
+
# We are still on shard = hash(85) % shards.
152
+
User.connection.execute "SET SERVER ROLE TO 'auto'"
153
+
User.find_by_email("test@example.com")
96
154
```
97
155
98
-
After executing this, the next transaction will be routed to the primary or replica respectively. By default, all queries will be load-balanced between all servers, so if the client wants to write or talk to the primary, they have to explicitely select it using the syntax above.
0 commit comments