Don't Split Your Data Until You Have To
I once watched a primary PostgreSQL node hit 98% CPU utilization during a Black Friday flash sale for a major Nordic retailer. The replication lag to the slaves drifted from milliseconds to minutes. Data consistency evaporated. The checkout system started throwing 500s because the connection pool was exhausted waiting for locks. It was a disaster.
We survived by aggressively caching at the edge, but the post-mortem was clear: the monolith had reached its vertical limit. We needed to shard.
Sharding—splitting your data horizontally across multiple nodes—is complex. It breaks joins, complicates transactions, and turns simple deployments into operational nightmares. Yet, when you are pushing 50,000 writes per second, it is often the only path forward. This guide ignores the theory found in textbooks and focuses on how we actually implement sharding on bare-metal capable VPS environments in 2024.
The Latency Trap: Why Geography Matters
Before writing a single line of config, understand the physics. Sharding introduces network overhead. A simple SELECT * FROM users WHERE id = 123 might be instant on a monolith. In a sharded environment, if your routing layer doesn't know exactly where that ID lives, it might query all shards (scatter-gather). That is fatal.
If your application servers are in Oslo and your database shards are scattered across cheap instances in Frankfurt or Amsterdam, the round-trip time (RTT) will kill your throughput. For Norwegian workloads, data sovereignty (GDPR) and latency are inextricably linked. You need your shards close to the NIX (Norwegian Internet Exchange).
Pro Tip: Never shard across different availability zones (AZs) unless you have a dedicated fiber backend. For a standard setup, keep all shards in the same datacenter to minimize latency, relying on async replication to a secondary location for disaster recovery.
Strategy 1: Application-Level Sharding (The Manual Approach)
The most robust way to shard is often the simplest: let the application decide. You maintain a map of where data lives. This usually relies on Consistent Hashing.
Unlike modulo hashing (id % num_servers), which requires moving all data when you add a server, consistent hashing minimizes data movement. Here is a production-grade Python implementation using a virtual node ring to ensure even distribution:
import hashlib
class ConsistentHash:
def __init__(self, nodes=None, replicas=3):
self.replicas = replicas
self.ring = {}
self.sorted_keys = []
if nodes:
for node in nodes:
self.add_node(node)
def add_node(self, node):
for i in range(self.replicas):
key = self._hash(f"{node}:{i}")
self.ring[key] = node
self.sorted_keys.append(key)
self.sorted_keys.sort()
def remove_node(self, node):
for i in range(self.replicas):
key = self._hash(f"{node}:{i}")
del self.ring[key]
self.sorted_keys.remove(key)
def get_node(self, string_key):
if not self.ring:
return None
key = self._hash(string_key)
# Binary search for the key
import bisect
idx = bisect.bisect(self.sorted_keys, key)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
def _hash(self, key):
return int(hashlib.md5(key.encode('utf-8')).hexdigest(), 16)
# Usage
sharding = ConsistentHash(["shard01.coolvds.no", "shard02.coolvds.no", "shard03.coolvds.no"])
target_server = sharding.get_node("user_849201")
print(f"Write user_849201 to {target_server}")This code runs inside your application. It effectively load-balances data based on the key. The downside? You cannot execute complex joins across shards. You query the shard you need, get the data, and assemble it in the app layer.
Strategy 2: Middleware Routing (ProxySQL)
If modifying the application code is too risky, move the logic to the infrastructure. For MySQL workloads (common in legacy Magento or WordPress setups scaling up), ProxySQL is the industry standard. It sits between your app and the database.
Here is how you configure ProxySQL to route traffic based on ID ranges. This is a "Range Based Sharding" approach, useful when data grows predictably.
-- Define your backend shards (Hostgroups)
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (10, '10.0.0.5', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (20, '10.0.0.6', 3306);
-- Load the rules into runtime
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
-- Define sharding rules
-- IDs 0-100000 go to HG 10
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, "^SELECT.*WHERE id < 100001", 10, 1);
-- IDs 100001+ go to HG 20
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, "^SELECT.*WHERE id >= 100001", 20, 1);
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;This allows you to add shards transparently. However, middleware adds a hop. If your VPS provider has noisy internal networks, that hop adds 2-5ms. On CoolVDS, where we utilize KVM virtualization and optimized virtual switching, this internal latency is negligible, often under 0.2ms.
Native Partitioning (PostgreSQL 16+)
For those on PostgreSQL 16 or 17 (released late 2024), declarative partitioning is powerful. It is not strictly "sharding" (which implies different physical servers), but combined with Foreign Data Wrappers (postgres_fdw), it acts like it.
Here is how to set up hash partitioning, which distributes data randomly but evenly across partitions:
-- Parent table
CREATE TABLE orders (
id UUID NOT NULL,
customer_id INT NOT NULL,
amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY HASH (customer_id);
-- Create partitions (could be on different disks/tablespaces)
CREATE TABLE orders_0 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE orders_1 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE orders_2 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE orders_3 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 3);
-- Indexing
CREATE INDEX ON orders (customer_id);With postgres_fdw, these partitions can actually reside on remote CoolVDS instances. The primary node acts as the coordinator.
Hardware: The Unforgiving Factor
Software sharding fails if the underlying hardware chokes on I/O. When you split a database, you increase the aggregate IOPS (Input/Output Operations Per Second) requirement. A standard SATA SSD often bottlenecks here.
We verified this using fio benchmarks. On a standard VPS with shared storage, random write latency spikes during rebalancing operations.
fio --name=random-write --ioengine=libaio --rw=randwrite --bs=4k --numjobs=4 --size=4G --runtime=60 --time_based --group_reportingOn CoolVDS NVMe instances, we consistently see IOPS sustaining above 50k with sub-millisecond latency. This is critical when shards are resizing or handling burst traffic.
Optimization Checklist for Sharded Nodes
- Kernel Tuning: Increase the backlog for high connection rates.
sysctl -w net.core.somaxconn=4096 - File Descriptors: Databases open thousands of files.
ulimit -n 65535inside your systemd service file. - InnoDB Buffer Pool: On the shards, dedicate 70% of RAM to the pool.
innodb_buffer_pool_size = 12G(for a 16GB instance). - Disable Swapping: Swapping kills database latency.
sysctl -w vm.swappiness=1
The CoolVDS Advantage
We don't oversell our platform. For basic blogs, any host works. But for sharded architectures, you need predictability. CoolVDS provides dedicated CPU threads and NVMe namespaces. This means your shard on Node A doesn't slow down because a neighbor on Node B is mining crypto. We enforce strict resource isolation via KVM.
Furthermore, our datacenters in Norway ensure you are compliant with Datatilsynet requirements for handling citizen data, keeping latency to Oslo users effectively instant.
Final Verdict
Sharding is painful. It requires engineering effort and maintenance overhead. But when your dataset exceeds 2TB or your write throughput tops 10k/sec, it's inevitable. Start with application-level partitioning if you can. Move to middleware if you must. And always ensure your infrastructure layer—CPU, RAM, and Disk—is as isolated and performant as possible.
Ready to benchmark your sharding logic? Deploy a cluster of high-performance NVMe instances on CoolVDS in under 60 seconds.