From 70ef54f4d09b0c5f6863cba2f590580cc862aba7 Mon Sep 17 00:00:00 2001 From: Denis Volkov <3149929+Denchick@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:41:46 +0500 Subject: [PATCH] Update README.md (#338) * move assets, update README * fix * fix typo * add target-session-attrs * read-only mode * better * typo * statistics * Update README.md Co-authored-by: Yury Frolov <57130330+EinKrebs@users.noreply.github.com> --------- Co-authored-by: Yury Frolov <57130330+EinKrebs@users.noreply.github.com> --- README.md | 37 ++++++++++++++------- docs/Benchmarks.md | 21 ++++++++++++ {benchmarks => docs}/resources/main.tf | 0 {benchmarks => docs}/resources/provider.tf | 0 docs/resources/tpcc.png | Bin 0 -> 23012 bytes 5 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 docs/Benchmarks.md rename {benchmarks => docs}/resources/main.tf (100%) rename {benchmarks => docs}/resources/provider.tf (100%) create mode 100644 docs/resources/tpcc.png diff --git a/README.md b/README.md index ea3e9cf45..a36cd6efb 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,33 @@ [![Go](https://github.com/pg-sharding/spqr/actions/workflows/tests.yaml/badge.svg)](https://github.com/pg-sharding/spqr/actions/workflows/tests.yaml) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/pg-sharding/spqr) ![Go Report](https://goreportcard.com/badge/github.com/pg-sharding/spqr) +[![Telegram Chat](https://img.shields.io/badge/telegram-SPQR_dev-blue)](https://t.me/+jMGhyjwicpI3ZWQy) # Stateless Postgres Query Router -SPQR is a system for horizontal scaling of PostgreSQL via sharding. We appreciate any kind of feedback and contribution to the project. +PostgreSQL is awesome, but it's hard to manage a single database with some terabytes of data and 105+ queries per second. Existing sharding solutions focus on analytical and hybrid workloads (OLAP, HTAP). Moreover, most of those solutions do not provide a simple, painless path for the monolith<->sharded transitions. That's why the Data Platform team of Yandex.Cloud designed SPQR. -For more about SPQR, please see [docs/](docs/) and [benchmarks/](benchmarks/). +SPQR is a production-ready system for horizontal scaling of PostgreSQL via sharding. We appreciate any kind of feedback and contribution to the project. + +For more about SPQR, please see [docs/](docs/). ## Main features -- Transaction and session pooling -- Multiple routers for fault tolerance -- Sharding -- Liquid data migrations -- Limited multi-shard queries -- Works over PostgreSQL protocol -- Falling unrouted queries to the world shard -- [Minor overhead](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues/30) for query execution -- and, of course, TLS support +SPQR works well when you do not have queries that can be loaded strictly on one shard. + +- **Sharding**. If possible, the router tries to determine on the first transaction statement to which shard this transaction should be sent. But you can explicitly specify a shard or a [sharding key](https://github.com/pg-sharding/spqr/blob/master/test/regress/tests/router/expected/routing_hint.out#L30) in a comment request. +- **Transaction and session pooling**. Just right in your favorite connection poller (Odyssey or PgBouncer). +- **Multiple routers for fault tolerance**. The router stores the sharding rules only for cache purposes. Information about the entire installation store inside the QDB service, so the number of routers running simultaneously is unlimited. +- **Liquid data migrations**. Data migration between shards aims to balance the workload across shards proportionally. The main idea is to minimize any locking impact during these migrations, which is accomplished by reducing the size of the data ranges being transferred. +- **Limited cross-shard queries**. SPQR router supports limited cross-shard queries. This is made from best-effort logic in a non-disruptive and non-consistent way and is used mainly for testing purposes. Please do not use this in your production. +- **Multiple servers and failover**. In the router configuration, it is possible to specify multiple servers for one shard. Then the router will distribute read-only queries among the replicas. However, in addition to the automatic routing, you also have the option to explicitly define the destination for a specific query by using the [target-session-attr](https://github.com/pg-sharding/spqr/blob/master/test/regress/tests/router/expected/target_session_attrs.out#L32) parameter within the query. +- **Works over PostgreSQL protocol**. It means you can connect to the router and the coordinator via psql. +- **Dedicated read-only mode**. Once enabled, the router will respond to a SHOW transaction_read_only command with "true" and handle only read-only queries, similar to a standard PostgreSQL replica. +- **Minor overhead for query execution**. See benchmarks [here](docs/Benchmarks.md) and [here](https://gitlab.com/postgres-ai/postgresql-consulting/tests-and-benchmarks/-/issues/30). +- **Varias authentication types**. From basic OK and plain text to MD5 and SCRUM, see [Authentication.md](docs/Authentication.md). +- **Live configuration reloading**. You can send a SIGHUP signal to the router's process. This will trigger the router to reload its configuration file and apply any changes without interrupting its operation. +- **Statistics**. You can get access to statitics in router's administrative console via [SHOW command](https://github.com/pg-sharding/spqr/blob/master/yacc/console/gram.y#L319). +- *Falling unrouted queries to the world shard*. SPQR is optimized for single-shard OLTP queries. But we have long-term plans to support routing queries for 2 or more shards. ## Development @@ -38,8 +47,12 @@ spqr-router run --config path-to-router-config.yaml ## Tests -SPQR has regression tests. These tests require Docker Compose, and can be run using `make regress`. Also, there are stress tests, but it's a work in progress. For more information on testing, please see `test` and `stress` sections in [Makefile](./Makefile). +SPQR has all types of tests: unit, regress, and end-to-end. These tests require Docker Compose, and can be run using `make`. For more information on testing, please see `unittest`, `regress`, and `feature` sections in [Makefile](./Makefile). ## License The SPQR source code is distributed under the PostgreSQL Global Development Group License. + +## Chat + +We have a Telegram chat to discuss SPQR usage and development, to join use [invite link](https://t.me/+jMGhyjwicpI3ZWQy). diff --git a/docs/Benchmarks.md b/docs/Benchmarks.md new file mode 100644 index 000000000..53603a9af --- /dev/null +++ b/docs/Benchmarks.md @@ -0,0 +1,21 @@ +# SPQR benchmarks + +TPC-C (Transaction Processing Performance Council - C) benchmark is a standardized performance test used to measure the performance of database systems under conditions of high load and a large number of transactions. It simulates the operation of an online store with a large number of simultaneous users, each of whom performs various operations with goods, such as viewing, adding to the cart, buying, etc. + +There are a lot of implementations of TPC-C test, in our experiments we use [Percona TPC-C Variant](https://github.com/Percona-Lab/sysbench-tpcc). + +We ran PostgreSQL on s3.medium (8 vCPU, 100% vCPU rate, 32 GB RAM) instances and 300 GB of memory with default Managed PostgreSQL Cluster settings. In each test we increasing shard count only. + +### Results + +| Warehouses | Shards | CPU | TPS | TpmC | TpmC per CPU | +| ---------- | --------- | --- | ---- | ----- | ------------ | +| 1000 | no router | 8 | 433 | 26010 | 3251.25 | +| 1000 | 2 | 16 | 664 | 39840 | 2490 | +| 1000 | 4 | 32 | 875 | 52500 | 1640.625 | +| 1000 | 8 | 64 | 1303 | 78180 | 1221.5625 | +| 1000 | 16 | 128 | 1543 | 92580 | 723.28125 | + +![TPC-C test results](resources/tpcc.png) + +You can compare this results with [Vitess and Aurora](https://www.amazon.science/publications/amazon-aurora-on-avoiding-distributed-consensus-for-i-os-commits-and-membership-changes), Perfomance Results. \ No newline at end of file diff --git a/benchmarks/resources/main.tf b/docs/resources/main.tf similarity index 100% rename from benchmarks/resources/main.tf rename to docs/resources/main.tf diff --git a/benchmarks/resources/provider.tf b/docs/resources/provider.tf similarity index 100% rename from benchmarks/resources/provider.tf rename to docs/resources/provider.tf diff --git a/docs/resources/tpcc.png b/docs/resources/tpcc.png new file mode 100644 index 0000000000000000000000000000000000000000..3f9f7a1576a362d415462c353cc12b01317c2cf1 GIT binary patch literal 23012 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