Sharding vs Partitioning — Arsitektur untuk Tabel Masif
Engineering Team
Partitioning vs Sharding: Apa Bedanya?
Partitioning membagi tabel menjadi beberapa bagian dalam satu server database. PostgreSQL mengelolanya secara transparan.
Sharding mendistribusikan data ke beberapa server database yang berbeda. Aplikasi (atau middleware) bertanggung jawab routing query ke shard yang tepat.
| Aspek | Partitioning | Sharding |
|---|---|---|
| Server | Satu | Banyak |
| Transparansi | Otomatis | Manual/middleware |
| Skalabilitas | Vertikal | Horizontal |
| Kompleksitas | Rendah | Tinggi |
| Cross-partition query | Mudah | Sulit/lambat |
| Batas | Hardware satu server | Teoritis tak terbatas |
Kapan Partitioning Cukup
Partitioning cukup ketika:
- Data muat di satu server (< 1TB aktif)
- Beban write < 10.000 TPS
- Perlu cross-partition query yang sering
- Tim kecil tanpa expertise distributed systems
Kapan Harus Sharding
Pertimbangkan sharding ketika:
- Data melebihi kapasitas satu server
- Beban write > 10.000 TPS
- Memerlukan isolasi multi-region
- Ketersediaan 99.99%+ diperlukan
Strategi Shard Key
Pilihan shard key menentukan distribusi data dan pola query:
1. Hash-Based Sharding
fn get_shard(user_id: Uuid, num_shards: u32) -> u32 {
let hash = xxhash(&user_id.as_bytes());
hash % num_shards
}
Distribusi merata, tetapi range query harus menghubungi semua shard.
2. Range-Based Sharding
fn get_shard(created_at: DateTime) -> &str {
match created_at.year() {
2024 => "shard_2024",
2025 => "shard_2025",
2026 => "shard_2026",
_ => "shard_default",
}
}
Cocok untuk data time-series. Shard lama bisa di-archive.
3. Directory-Based Sharding
async fn get_shard(tenant_id: Uuid, directory: &PgPool) -> String {
sqlx::query_scalar("SELECT shard_name FROM shard_directory WHERE tenant_id = $1")
.bind(tenant_id)
.fetch_one(directory)
.await
.unwrap()
}
Fleksibel — bisa memindahkan tenant antar shard. Tetapi directory adalah single point of failure.
Implementasi Query Router di Rust
struct ShardRouter {
shards: HashMap<u32, PgPool>,
num_shards: u32,
}
impl ShardRouter {
fn get_pool(&self, shard_key: &Uuid) -> &PgPool {
let shard_id = xxhash(shard_key.as_bytes()) % self.num_shards;
&self.shards[&shard_id]
}
async fn query_all<T: FromRow>(
&self,
query: &str,
) -> Result<Vec<T>, DbError> {
let futures: Vec<_> = self.shards.values()
.map(|pool| {
sqlx::query_as::<_, T>(query)
.fetch_all(pool)
})
.collect();
let results = futures::future::try_join_all(futures).await?;
Ok(results.into_iter().flatten().collect())
}
}
Migrasi ke Sharding
- Mulai dengan partitioning — Lebih sederhana, selesaikan dulu
- Identifikasi shard key — Kolom yang paling sering di-filter
- Dual-write — Tulis ke database lama dan baru secara bersamaan
- Verifikasi — Bandingkan hasil query antara kedua sistem
- Cutover — Arahkan pembacaan ke sistem baru
- Cleanup — Hapus dual-write dan database lama
Tantangan Sharding
- Cross-shard join — Tidak bisa JOIN antar shard secara native
- Transaksi terdistribusi — 2-phase commit lambat dan kompleks
- Rebalancing — Menambah shard memerlukan redistribusi data
- Operasional — Monitoring, backup, dan upgrade lebih kompleks
Kesimpulan
Mulai dengan partitioning — ini menyelesaikan 90% masalah skala database. Sharding adalah langkah selanjutnya ketika satu server tidak lagi cukup. Pilih shard key berdasarkan pola query, implementasikan router query, dan rencanakan migrasi dengan hati-hati.