Implementing Sharding with Citus
Introduction
- In the era of big data, when storage capacity reaches the
Terabytethreshold or the number of requests exceeds the physical limit of a single server (Vertical Scaling/Scale-up),Shardingis the optimal solution forHorizontal Scaling / Scale-out - It is achieved by dividing a massive data table (billions of rows) into multiple small, independent and self-managed parts called
Shards - Each
Shardis a separate physical database located on a different physicalServer. - The key point is that the data in the
Shardsdoes not overlap, but when combined, it forms a complete dataset.
Horizontal Partitioning vs Sharding
Horizontal Partitioning: Splitting a large table into smaller tables (such as by month) but all of these child tables still reside on the same physical server.Sharding: Distributing those child tables after partitioning across multiple different physical servers. Therefore,Shardingis the architecture of horizontal data partitioning in a distributed environment.
Sharding Key
To know which Shard a row will be stored in, the system relies on a specific data column called Sharding Key (or Distribution Key), which is used as a routing landmark.
Common routing algorithms include:
Range-based Sharding: Partitioning based on a range of key values- Example: IDs from 1 - 10,000 in Shard 1, from 10,001 - 20,000 in Shard 2
- Disadvantage: Easily leads to load imbalance if new data is concentrated within a certain range.
Directory-based Sharding: Using a separate configuration storage service to map which Key value goes to which Shard.Hash-based Sharding: This is the most commonly used method- The system applies a
Hash Functionto theSharding Keyaccording to a formula to determine the location:Shard ID = Hash(Sharding Key) mod N(where N is the number of Shards) - Advantage: Data is distributed extremely evenly among servers.
Hash-based Sharding
Query Routing
- When there is a
SQLstatement, it connects to an intermediate node (RouterorCoordinator). ThisNodeanalyzes the statement, relies on theSharding Keyto calculate theShardlocation and then sends the statement directly to the correctShardstoring the data without needing to scan others - If the statement needs to aggregate from multiple
Shards, theCoordinatorwill run the subqueries in parallel and merge the results before returning them to theClient.
Components
Citus
Citus is a powerful open-source extension that transforms Postgres from a monolithic Database into a distributed Database. The architecture consists of 2 main components:
Node Worker- It is the physical
Server, which is actually a regularPostgresdatabase containing multiple small physical tables (the logicalShards). - For example, in a system with 2
Workersand32 Shards:Worker 1will contain16actual Postgres tables representingShards 1-16andWorker 2will contain16tables representingShards 17-32. - The
Workersdirectly perform calculations, filtering and sorting of data according to commands from theCoordinator - They have independent processing performance, allowing linear query speedups when adding
Workers.
- It is the physical
Node Coordinator- This is the sole communication gateway with the application, it does not contain actual data but only stores the
metadataof theNode Workersincluding IP, host, port and shard information. - When you query data, the
Coordinatorplans the execution of the distributedSQLstatement and coordinates by opening directSocket Connectionsto theWorkersto request data processing, then returns the result to it
- This is the sole communication gateway with the application, it does not contain actual data but only stores the
Prerequisites
- If your goal is to improve query performance on large datasets, you should first consider
Query Tuning, using appropriateIndextypes likePartial IndexorTable Partitioningas primary choices - If none of the above solutions help because the system data size is too large, only then should you think about
Sharding
Detail
First, create a docker-compose.yml file with the following content
services:
citus_master:
image: citusdata/citus:alpine
container_name: citus_coordinator
ports:
- "5432:5432"
environment:
&citus-env
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
citus_worker_1:
image: citusdata/citus:alpine
container_name: citus_worker_1
environment: *citus-env
depends_on:
- citus_master
citus_worker_2:
image: citusdata/citus:alpine
container_name: citus_worker_2
environment: *citus-env
depends_on:
- citus_master
pgadmin:
image: dpage/pgadmin4
container_name: citus_pgadmin
ports:
- "8080:80"
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
depends_on:
- citus_master
- You can see that I have created
citus_masterand 2 workerscitus_worker_1/citus_worker_2, along withpgadmin - You can change the Postgres value information accordingly in the
.envfile
Then connect with pgAdmin to citus_master as follows
Next, add the workers to the Coordinator with 5432 as the port defined in docker-compose
SELECT citus_add_node('citus_worker_1', 5432);
SELECT citus_add_node('citus_worker_2', 5432);
You can check the successfully added worker information and the metadata of the Coordinator as follows
SELECT * FROM citus_get_active_worker_nodes();
SELECT * FROM pg_dist_node;
After that, create the tables and enable the Sharding feature as follows
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(150) NOT NULL,
price NUMERIC(12, 2) NOT NULL CHECK (price >= 0),
stock_quantity INT NOT NULL DEFAULT 0,
status VARCHAR(20) DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS customers (
id SERIAL PRIMARY KEY,
fullname VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(15),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
SELECT create_distributed_table('products', 'product_id');
SELECT create_distributed_table('customers', 'id', shard_count := 16);
SHOW citus.shard_count;
SET citus.shard_count = 8;
- You can see that I use the
create_distributed_tablefunction to create the distributed tableproductswith the Sharding key asproduct_id - By default, it will be divided into
32 Shards, which you can check withSHOW citus.shard_count - If you want to adjust it, use
SET citus.shard_countwith the appropriate value or passshard_countdirectly into thecreate_distributed_tablefunction - The number of
Shardswill be divided equally amongWorkers. For example, if we have2 Workers, each will have16 Shards. If you adjustshard_count := 16, each Worker will have8 Shards
After seeding data, you can check directly on the Coordinator and it will retrieve data from Workers to aggregate and return
SELECT * FROM products;
SELECT * FROM customers;
To view specific Shard information
-- Query 1
SELECT
table_name,
shardid,
nodename,
nodeport,
shard_size
FROM citus_shards
ORDER BY table_name, shardid;
-- Query 2
SELECT
shardid AS shard_name,
result::bigint AS row_count
FROM run_command_on_shards('products', 'SELECT count(*) FROM %s');
-- Query 3
SELECT
shardid,
result AS row_data
FROM run_command_on_shards(
'products',
'SELECT json_agg(t) FROM (SELECT * FROM %s LIMIT 5) t'
)
WHERE shardid = 102008;
- Query 1: Shard information within each Worker
- Query 2: Execute query
SELECT count(*) FROM %son each Shard to count data - Query 3: Query data on a specific Shard, namely
102008
And if you want to query data from a specific Shard, you must connect to that Worker first
Then perform the query as usual with the table name now being {table name}+{shardid}
SELECT * FROM products_102008;
Happy coding!
Comments
Post a Comment