A couple of weeks ago, I attended a PGDay event in Blumenau, a city not far away from where I live in Brazil. Opening the day were former Percona colleagues Marcelo Altmann and Wagner Bianchi, showcasing ReadySet’s support for PostgreSQL. Readyset is a source-available database cache service that differs from other solutions by not relying on a timer-based approach to expire/invalidate a cached result: it uses PostgreSQL (or MySQL) own replication stream for that. In other words, Readyset acts as a replication client and keeps its cached data in sync by updating it based on row changes in the replication flow.
My understanding is that the risk of accessing dirty data should, thus, be limited to how fast Readyset can sync its cached data. In a way, that’s not very different from a replica keeping data in sync while avoiding replication lag. The expectation is for Readyset to perform this operation in a more efficient manner (that’s where the caching should shine) while returning data for customers faster and, ideally, supporting more requests at the same time (higher concurrency).
As for ProxySQL, it wasn’t on my initial plan to give it a try. This tool was a game changer in the MySQL ecosystem in so many ways, and I have been looking for an opportunity to test it with PostgreSQL since they started supporting it (although it is not GA yet), so I couldn’t resist. But I will only be using ProxySQL’s query rules feature for splitting traffic, and not its own caching feature. Finally, why not test with HAproxy too, since it was already, right?
This post became too long, so I divided it into two parts: in this initial part, I outline my test environment and methodology and explain how to install ReadySet and configure it to work with PostgreSQL. I do the same for ProxySQL and HAproxy, although with fewer details. In part two, I present the test results and discuss them.
A simple test suite
What I had in mind was a simple experiment: I wanted to take advantage of a test environment I had deployed for another project and give ReadySet a try. It ended up evolving to something a bit bigger, but it helped me better understand how the different technologies compare, and I trust you will too.
Environment
The main elements that are relevant for us here in this particular test environment are the main Patroni cluster with two nodes (the primary and a standby server), an application server used to generate load, and an additional server that was added to host ReadySet, all running on small cloud instances (GCP):
- The Patroni nodes and the ReadySet server with 4 vCPUs (2 cores), 4 GB of memory, and dedicated data disks:
- Primary: 10.128.0.84
- Standby: 10.128.0.67
- ReadySet: 10.128.0.113
- The application server with 2 vCPUs (1 core) and 2 GB of memory:
- App: 10.128.0.112
The essence of the PostgreSQL 17.5.1 configuration is shown below, in Patroni’s yaml format:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
postgresql: parameters: archive_mode: on archive_timeout: 600s checkpoint_completion_target: 0.9 checkpoint_timeout: 1800 effective_cache_size: 3GB max_connections: 1000 max_wal_size: 40GB min_wal_size: 10GB random_page_cost: 1.1 shared_buffers: 1GB wal_level: logical work_mem: 64MB |
Methodology
On the application server, I compiled Sysbench from source with support for PostgreSQL:
1 |
git clone https://github.com/akopytov/sysbench.git && cd sysbench && ./autogen.sh && ./configure --without-mysql --with-pgsql && make -j && sudo make install |
and created an OLTP test database with eight 10k-row tables:
1 |
sysbench /home/fernando/sysbench/src/lua/oltp_read_write.lua --db-driver=pgsql --db-ps-mode=disable --skip_trx=on --pgsql-user=sysbench --pgsql-password='secret' --pgsql-db=sysbench --pgsql-host=10.128.0.84 --tables=8 --pgsql-port=5432 --table_size=10000000 --threads=16 prepare |
resulting in a dataset of approximately 20 GB:
1 2 3 4 5 |
sysbench=# select datname, pg_size_pretty(pg_database_size(datname)) as "DB_Size" from pg_stat_database where datname = 'sysbench'; datname | DB_Size ----------+--------- sysbench | 20 GB (1 row) |
I ran VACUUM ANALYZE on the sysbench database a single time after populating it:
1 |
sysbench=# vacuum analyze; |
The actual test consisted of running the Sysbench OLTP Read-Write and OLTP Read-Only workloads for 20 minutes for the different scenarios I envisioned, which will be described in the follow-up post (because it’s easier to make sense of them once we understand how ReadySet works and how ProxySQL and HAproxy are configured):
1 |
sysbench /home/fernando/sysbench/src/lua/oltp_read_write.lua --db-driver=pgsql --db-ps-mode=disable --skip_trx=on --pgsql-user=sysbench --pgsql-password='secret' --pgsql-db=sysbench --pgsql-host=<target_host> --tables=8 --pgsql-port=<target_port> --table_size=10000000 --report-interval=1 --threads=64 --time=1200 run> sysbench.out 2>&1 |
- For the Read-Only test, I used the oltp_read_only.lua script instead.
- For each test scenario, I adjusted the –pgsql-host and –pgsql-port to the target server/connection point each time.
The tests were run with an interval of five minutes between them.
Installing and configuring ReadySet
I installed ReadySet using the Ubuntu package:
1 2 |
wget https://github.com/readysettech/readyset/releases/download/stable-250522/readyset_1.15.0-1_amd64.deb sudo dpkg -i readyset_1.15.0-1_amd64.deb |
I created a separate directory to store the service logs:
1 2 |
mkdir /var/log/readyset chown readyset:readyset -R /var/log/readyset |
And configured the service as follows:
1 2 3 4 5 6 7 8 9 10 11 |
$ sudo cat /etc/readyset/readyset.conf |grep -v "^#|^$" LISTEN_ADDRESS=0.0.0.0:5435 STORAGE_DIR=/var/lib/readyset LOG_PATH=/var/log/readyset LOG_ROTATION=daily NO_COLOR=true LOG_FORMAT=full LOG_LEVEL=info READYSET_MEMORY_LIMIT=3221225472 |
Note that I’ve used two database accounts above for two different connection strings:
- UPSTREAM_DB_URL: Used to route write and non-cacheable statements to the Primary server; the database user needs to have read and write access to the target database.
- CDC_DB_URL: Used to replicate data; my understanding is that we need to connect with a SUPER user to the primary, as ReadySet needs to create a replication slot, copy the initial data (snapshot), and start a replication process. Note that I’m also connecting to my target database (sysbench) here, too, and not to the main postgres one.
In my initial experiments, I used a smaller cloud instance to host ReadySet, the same type and size as the one hosting the application server, described above. Limiting the amount of memory ReadySet can use (READYSET_MEMORY_LIMIT) was essential in such a small test server; without that, ReadySet may not leave much left for the OS, and performance will suffer (watch out for messages of the type “readyset_util::time_scope: operation took X seconds” in the log file). For reasons I will explain later, I have since upgraded the server to a bigger cloud instance, but I continued leaving 1 GB of memory unallocated, indirectly reserving it for the OS.
You can then start the service:
1 |
sudo systemctl start readyset |
Once ReadySet starts running, if you point database connections to it, at this stage, it will simply route all requests to the primary (UPSTREAM_DB_URL). And it will continue to do so until the following requirements are met:
- The initial data snapshot is completed.
- You define at least one query to be cached by ReadySet.
Initial snapshot and data replication
I thought ReadySet would only keep records of the data it is configured to cache, but I was wrong. Once the service is started, it will initiate a snapshot process of the target data (using PostgreSQL logical replication, hence the need for setting wal_level to replica), which you can follow along in its log file:
1 2 3 4 5 6 7 |
2025-06-10T20:32:18.322399Z INFO readyset: Listening for new connections listen_address=0.0.0.0:5435 (...) 2025-06-10T20:32:18.862986Z INFO replicators::postgres_connector::connector: Creating replication slot slot="readyset" temporary=false 2025-06-10T20:32:18.903024Z INFO replicators::noria_adapter: Connected to PostgreSQL 2025-06-10T20:32:19.109998Z INFO dataflow_state::persistent_state: Base creating primary index base=readyset.db-public-sbtest8-0.db index=[0] is_unique=true (...) 2025-06-10T20:32:20.244408Z INFO replicators::postgres_connector::snapshot: Snapshotting tables tables=8 max_parallel_snapshot_tables=1 |
Alternatively, you can query its current state with the command SHOW READYSET STATUS (the output below is from a previous snapshot run):
1 2 3 4 5 6 7 8 9 10 |
name | value ----------------------------+---------------------------- Database Connection | Connected Connection Count | 1 Status | Snapshot In Progress Minimum Replication Offset | (F6/2D392240, F6/2D392240) Last started Controller | 2025-06-10 17:40:33 UTC Last completed snapshot | NULL Last started replication | NULL (7 rows) |
Once the initial snapshot process is completed, ReadySet starts consuming replication data:
1 2 3 4 5 |
2025-06-10T20:52:03.369664Z INFO Snapshotting table{table="public"."sbtest6"}: replicators: Snapshotting progress rows_replicated=9999994 rows_total_est=9999994 progress=99.90% estimate=00:00:00 2025-06-10T20:52:03.369686Z INFO Snapshotting table{table="public"."sbtest6"}: replicators::postgres_connector::snapshot: Snapshotting finished rows_replicated=9999994 2025-06-10T20:52:03.499831Z INFO replicators::noria_adapter: Snapshot finished 2025-06-10T20:52:03.552013Z INFO replicators::postgres_connector::connector: confirmed_flush_lsn="F9/74011A88" wal_status="reserved" 2025-06-10T20:52:03.571911Z INFO replicators::noria_adapter: Streaming replication started position=(F9/74011A88, F9/74011A88) |
If you query the pg_stat_replication table on the primary server, you will see its connection state changed from startup to streaming:
1 2 3 4 5 6 |
postgres=# select pid, usename, application_name, client_addr, state, sent_lsn, write_lsn from pg_stat_replication; pid | usename | application_name | client_addr | state | sent_lsn | write_lsn ------+------------+------------------+--------------+-----------+-------------+------------ 3046 | replicator | pg2 | 10.128.0.67 | streaming | F9/BA000000 | F9/BA000000 3289 | postgres | | 10.128.0.113 | streaming | F9/BA000000 | F9/BA000000 (3 rows) |
and ReadySet status changes to Online:
1 2 3 4 5 6 7 8 9 10 11 |
name | value ----------------------------+---------------------------- Database Connection | Connected Connection Count | 1 Status | Online Maximum Replication Offset | (F9/74011B10, F9/74011B40) Minimum Replication Offset | (F9/74011B10, F9/74011B40) Last started Controller | 2025-06-10 20:32:18 UTC Last completed snapshot | 2025-06-10 20:52:03 UTC Last started replication | 2025-06-10 20:52:03 UTC (8 rows) |
From now on, ReadySet will start serving cached data – that is, if you have any defined.
Caching data
If we start running traffic through ReadySet, we can see all query requests with the command SHOW PROXIED QUERIES. ReadySet’s documentation explains its current limitations for query support. We can limit the output of that command to display only queries that are SUPPORTED by ReadySet, limiting it to those that can be cached:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
sysbench=> SHOW PROXIED SUPPORTED QUERIES; query id | proxied query | readyset supported | count --------------------+------------------------------------+--------------------+------- q_615d9af2323da276 | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest1" +| | | WHERE +| | | ("id" = $1) | | q_1f1ebd226f791884 | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest2" +| | | WHERE +| | | ("id" = $1) | | q_5be174ec989bf960 | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest3" +| | | WHERE +| | | ("id" = $1) | | q_7bcda81e14f5a48 | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest4" +| | | WHERE +| | | ("id" = $1) | | q_46b8c4d2f1dd48d | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest5" +| | | WHERE +| | | ("id" = $1) | | q_89098158ec3e99dd | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest6" +| | | WHERE +| | | ("id" = $1) | | q_3e2ac69a8cc95fd4 | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest7" +| | | WHERE +| | | ("id" = $1) | | q_da4df9eb266fc12 | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest8" +| | | WHERE +| | | ("id" = $1) | | |
Note that you may find in that list queries that, while supported by ReadySet, employ exact values (constants) for certain parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
q_773e362dbdaba65e | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest7" +| | | WHERE +| | | "id" BETWEEN 7880653 AND 7880752 | | q_cc7bbe6281b86ced | SELECT +| yes | 0 | sum("k") +| | | FROM +| | | "sbtest8" +| | | WHERE +| | | "id" BETWEEN 239139 AND 239238 | | q_c37fa2ce5e012006 | SELECT +| yes | 0 | "c" +| | | FROM +| | | "sbtest7" +| | | WHERE +| | | "id" BETWEEN 5373861 AND 5373960 | | |
Unless your workload tends to employ the same values repeatedly, these might not be good candidates for caching. After running the Sysbench OLTP read-write workload against the primary, I found that the majority of it is composed of the very simple queries at the top of the list above:
Thus, I decided to limit my test on ReadySet to those eight queries, each targeting a different test table, and created cache definitions for them using their query IDs:
1 2 3 4 5 6 7 8 |
CREATE CACHE FROM q_615d9af2323da276; CREATE CACHE FROM q_1f1ebd226f791884; CREATE CACHE FROM q_5be174ec989bf960; CREATE CACHE FROM q_7bcda81e14f5a48; CREATE CACHE FROM q_46b8c4d2f1dd48d; CREATE CACHE FROM q_89098158ec3e99dd; CREATE CACHE FROM q_3e2ac69a8cc95fd4; CREATE CACHE FROM q_da4df9eb266fc12; |
Providing the full query instead also works:
1 2 3 4 5 6 7 8 |
CREATE CACHE FROM SELECT "c" FROM "sbtest1" WHERE ("id" = $1); CREATE CACHE FROM SELECT "c" FROM "sbtest2" WHERE ("id" = $1); CREATE CACHE FROM SELECT "c" FROM "sbtest3" WHERE ("id" = $1); CREATE CACHE FROM SELECT "c" FROM "sbtest4" WHERE ("id" = $1); CREATE CACHE FROM SELECT "c" FROM "sbtest5" WHERE ("id" = $1); CREATE CACHE FROM SELECT "c" FROM "sbtest6" WHERE ("id" = $1); CREATE CACHE FROM SELECT "c" FROM "sbtest7" WHERE ("id" = $1); CREATE CACHE FROM SELECT "c" FROM "sbtest8" WHERE ("id" = $1); |
We can confirm the queries have been added to the “cacheable” list with the SHOW CACHES command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
sysbench=> SHOW CACHES; query id | cache name | query text | fallback behavior | count --------------------+--------------------+----------------------------------+-------------------+------- q_615d9af2323da276 | q_615d9af2323da276 | SELECT +| fallback allowed | 0 | | "public"."sbtest1"."c" +| | | | FROM +| | | | "public"."sbtest1" +| | | | WHERE +| | | | ("public"."sbtest1"."id" = $1) | | q_1f1ebd226f791884 | q_1f1ebd226f791884 | SELECT +| fallback allowed | 0 | | "public"."sbtest2"."c" +| | | | FROM +| | | | "public"."sbtest2" +| | | | WHERE +| | | | ("public"."sbtest2"."id" = $1) | | q_5be174ec989bf960 | q_5be174ec989bf960 | SELECT +| fallback allowed | 0 | | "public"."sbtest3"."c" +| | | | FROM +| | | | "public"."sbtest3" +| | | | WHERE +| | | | ("public"."sbtest3"."id" = $1) | | q_7bcda81e14f5a48 | q_7bcda81e14f5a48 | SELECT +| fallback allowed | 0 | | "public"."sbtest4"."c" +| | | | FROM +| | | | "public"."sbtest4" +| | | | WHERE +| | | | ("public"."sbtest4"."id" = $1) | | q_46b8c4d2f1dd48d | q_46b8c4d2f1dd48d | SELECT +| fallback allowed | 0 | | "public"."sbtest5"."c" +| | | | FROM +| | | | "public"."sbtest5" +| | | | WHERE +| | | | ("public"."sbtest5"."id" = $1) | | q_89098158ec3e99dd | q_89098158ec3e99dd | SELECT +| fallback allowed | 0 | | "public"."sbtest6"."c" +| | | | FROM +| | | | "public"."sbtest6" +| | | | WHERE +| | | | ("public"."sbtest6"."id" = $1) | | q_3e2ac69a8cc95fd4 | q_3e2ac69a8cc95fd4 | SELECT +| fallback allowed | 0 | | "public"."sbtest7"."c" +| | | | FROM +| | | | "public"."sbtest7" +| | | | WHERE +| | | | ("public"."sbtest7"."id" = $1) | | q_da4df9eb266fc12 | q_da4df9eb266fc12 | SELECT +| fallback allowed | 0 | | "public"."sbtest8"."c" +| | | | FROM +| | | | "public"."sbtest8" +| | | | WHERE +| | | | ("public"."sbtest8"."id" = $1) | | (8 rows) |
They will also show in the list of “materializations”, alongside our target tables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
sysbench=> EXPLAIN MATERIALIZATIONS; domain_index | node_index | node_name | node_description | keys | size_bytes | partial | indexes --------------+------------+--------------------+------------------+----------+------------+---------+------------ 2 | 3 | public.sbtest7 | B | ~9999994 | 2152509264 | f | HashMap[0] 7 | 8 | public.sbtest6 | B | ~9999994 | 2152508973 | f | HashMap[0] 4 | 5 | public.sbtest2 | B | ~9999995 | 2152508944 | f | HashMap[0] 3 | 4 | public.sbtest5 | B | ~9999994 | 2152508549 | f | HashMap[0] 1 | 2 | public.sbtest1 | B | ~9999992 | 2152508496 | f | HashMap[0] 6 | 7 | public.sbtest3 | B | ~9999992 | 2152508373 | f | HashMap[0] 0 | 1 | public.sbtest8 | B | ~9999994 | 2152508032 | f | HashMap[0] 5 | 6 | public.sbtest4 | B | ~9999987 | 2152507419 | f | HashMap[0] 8 | 10 | q_615d9af2323da276 | R | 0 | 0 | t | HashMap[1] 9 | 14 | q_1f1ebd226f791884 | R | 0 | 0 | t | HashMap[1] 10 | 18 | q_5be174ec989bf960 | R | 0 | 0 | t | HashMap[1] 11 | 22 | q_7bcda81e14f5a48 | R | 0 | 0 | t | HashMap[1] 12 | 26 | q_46b8c4d2f1dd48d | R | 0 | 0 | t | HashMap[1] 13 | 30 | q_89098158ec3e99dd | R | 0 | 0 | t | HashMap[1] 14 | 34 | q_3e2ac69a8cc95fd4 | R | 0 | 0 | t | HashMap[1] 15 | 38 | q_da4df9eb266fc12 | R | 0 | 0 | t | HashMap[1] (16 rows) |
ProxySQL
ProxySQL is another open source proxy software, but different from HAProxy, it “speaks” the MySQL language (or, in their words, it is “database protocol aware”) and, since version 3.0, PostgreSQL too. This opens the space to explore many different features, including query caching (using a more traditional TTL-based implementation). Ironically, I had a different feature in mind for this project: query rules.
With ReadySet, we are effectively splitting traffic between two servers: read-only queries of the type “SELECT c FROM sbtestX WHERE id=?” were being processed by the ReadySet server, whereas everything else (other SELECT queries and all writes) was being routed to the PostgreSQL Primary. The query rules in ProxySQL allow us to use a similar approach, sending the target SELECT queries to a PostgreSQL standby server. It is not the same thing because there is a chance that the standby server might have stale data due to replication lag, whereas ReadySet should protect against this. Still, I was curious to see which performance would result in splitting the traffic between two servers in this other way.
Installing ProxySQL
I just followed their documentation to configure their Ubuntu repository and install ProxySQL 3.0.1 on the application server itself:
1 2 3 4 5 |
wget -nv -O /etc/apt/trusted.gpg.d/proxysql-3.0.x-keyring.gpg 'https://repo.proxysql.com/ProxySQL/proxysql-3.0.x/repo_pub_key.gpg' echo "deb https://repo.proxysql.com/ProxySQL/proxysql-3.0.x/$(lsb_release -sc)/ ./" | tee /etc/apt/sources.list.d/proxysql.list apt-get update apt-get install proxysql=3.0.1 systemctl start proxysql |
Configuring ProxySQL
We configure ProxySQL by accessing the administration “door” (port 6132):
1 |
psql -h 127.0.0.1 -p6132 -U admin -d admin |
I don’t intend to provide a detailed explanation of how ProxySQL works in this post, so I will restrict myself to basic instructions:
- Provide the credentials to access the database servers, used to check their health status (I’m using the SUPER user account below, but you don’t have to):
1 2 3 4 5 |
UPDATE global_variables SET variable_value = 'postgres' WHERE variable_name = 'pgsql-monitor_username'; UPDATE global_variables SET variable_value = 'secret' WHERE variable_name = 'pgsql-monitor_password'; UPDATE global_variables SET variable_value = '2000' WHERE variable_name IN ('pgsql-monitor_connect_interval', 'pgsql-monitor_ping_interval'); LOAD PGSQL VARIABLES TO RUNTIME; SAVE PGSQL VARIABLES TO DISK; |
- We configure two hostgroups: one to send writes (id 1), to which we add the primary server, and another one to distribute reads (id 2) according to the rules we will create later, to which we add the standby server:
1 2 3 4 5 |
INSERT INTO pgsql_replication_hostgroups (writer_hostgroup,reader_hostgroup,comment) VALUES (1,2,'cluster_1'); INSERT INTO pgsql_servers (hostgroup_id, hostname, port) VALUES (1, '10.128.0.84', 5432); INSERT INTO pgsql_servers (hostgroup_id, hostname, port) VALUES (2, '10.128.0.67', 5432); LOAD PGSQL SERVERS TO RUNTIME; SAVE PGSQL SERVERS TO DISK; |
Here’s a clearer view of the hostgroups and their members:
1 2 3 4 5 6 |
admin=# select hostgroup_id, hostname, port, status, weight, max_connections from pgsql_servers; hostgroup_id | hostname | port | status | weight | max_connections --------------+-------------+------+--------+--------+----------------- 1 | 10.128.0.84 | 5432 | ONLINE | 1 | 1000 2 | 10.128.0.67 | 5432 | ONLINE | 1 | 1000 (3 rows) |
- Clients need to authenticate to the ProxySQL server, so we add the database user used by our application:
1 2 3 |
INSERT INTO pgsql_users (username, password, default_hostgroup) VALUES ('sysbench', 'secret', 1); LOAD PGSQL USERS TO RUNTIME; SAVE PGSQL USERS TO DISK; |
- Finally, we create the query rules in a similar way to how we did for ReadSet’s query cache, and indicate that those queries should be sent to the hostgroup 2:
1 2 3 4 5 6 7 8 9 10 |
INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (10,1,'sysbench','^SELECT c FROM sbtest1 WHERE id=?',2,1); INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (11,1,'sysbench','^SELECT c FROM sbtest2 WHERE id=?',2,1); INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (12,1,'sysbench','^SELECT c FROM sbtest3 WHERE id=?',2,1); INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (13,1,'sysbench','^SELECT c FROM sbtest4 WHERE id=?',2,1); INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (14,1,'sysbench','^SELECT c FROM sbtest5 WHERE id=?',2,1); INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (15,1,'sysbench','^SELECT c FROM sbtest6 WHERE id=?',2,1); INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (16,1,'sysbench','^SELECT c FROM sbtest7 WHERE id=?',2,1); INSERT INTO pgsql_query_rules (rule_id,active,username,match_digest,destination_hostgroup,apply) VALUES (17,1,'sysbench','^SELECT c FROM sbtest8 WHERE id=?',2,1); LOAD PGSQL QUERY RULES TO RUNTIME; SAVE PGSQL QUERY RULES TO DISK; |
Clients should connect to ProxySQL through a different port, 6133:
1 |
psql -h 127.0.0.1 -p6133 -U sysbench -d sysbench |
HAproxy
For “completeness”, why not run the read-only test equally balancing loads between the primary and the standby server? That’s easy to do with HAproxy. It was already installed on my setup alongside Patroni, but I opted to also install it on the application server, alongside ProxySQL, and run the tests from there too.
Installing HAproxy
Just:
1 |
sudo apt-get install haproxy |
Configuring HAproxy
I could have created a single pool, but followed the standard that uses the Patroni REST API to create a pool with the current primary (http://<dbnode>:8008/primary), listening on port 5433, and another one where I had both the primary and standby servers (http://<dbnode>:8008/read-only) listening on port 5434:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
cat <<EOF > /etc/haproxy/haproxy.cfg global maxconn 100 defaults log global mode tcp retries 2 timeout client 30m timeout connect 4s timeout server 30m timeout check 5s listen stats mode http bind *:8080 stats enable stats uri / listen primary bind *:5433 option httpchk OPTIONS /primary http-check expect status 200 default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions server pg1 10.128.0.84:5432 maxconn 100 check port 8008 server pg2 10.128.0.67:5432 maxconn 100 check port 8008 listen reads bind *:5434 option httpchk OPTIONS /read-only http-check expect status 200 default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions server pg1 10.128.0.84:5432 maxconn 100 check port 8008 server pg2 10.128.0.67:5432 maxconn 100 check port 8008 EOF |
I only used the reads pool in my test. The API endpoint named read-only is misleading: it includes both primary and standby servers in the pool. The default HAproxy algorithm is round robin, so it balances connection requests between both servers.
Remember to restart the service:
1 |
sudo systemctl restart haproxy |
This concludes the initial post in this series, in which I covered the test environment and methodology and explained how to install ReadySet, ProxySQL, and HAproxy and configure them to work with PostgreSQL. Here is the second and final part presenting the test results.