📚 Topik: Sharding — Memilih Shard Key & Hot Partition Problem
Kategori: Distributed Systems · Level: advanced
🎯 Tujuan Belajar
Setelah sesi ini, keputusan ini jadi lebih baik:
- Bisa menilai shard key sebelum data masuk (karena shard key adalah one-way door di banyak sistem).
- Bisa mengenali hot partition dari pola akses, bukan dari jumlah data.
- Bisa memutuskan kapan sharding tidak perlu — dan opsinya lebih murah.
🧠 Penjelasan Inti
Sharding = partisi horizontal data across node, masing-masing node (shard) punya subset data independen. Tujuan: melewati batas single-node (throughput write, ukuran dataset, blast radius).
Alasan sebenarnya jarang "data kebanyakan". Single Postgres bisa pegang TB dengan indexing benar. Alasan sah sharding:
- Write throughput melewati kapasitas satu primary (fsync-bound, WAL, satu CPU core untuk apply).
- Blast radius — satu tenant besar tidak boleh bikin tenant lain down.
- Data residency — data EU harus di EU.
- Working set tidak muat RAM satu node, dan read fan-out jadi bottleneck.
Kalau alasanmu "tabel sudah 200 juta baris" → itu bukan alasan. Itu alasan partisi + index tuning.
Shard key adalah kontrak. Dia menentukan: routing query, distribusi data, distribusi load, dan apakah cross-shard transaction dibutuhkan. Salah pilih = 6 bulan kemudian kamu menulis migration job yang jalan 3 minggu sambil dua sistem hidup bersamaan.
⚙️ Cara Kerja Internal
Tiga level yang sering dicampur:
logical key → hash/range function → partition → physical node
user_id murmur3 % 4096 p_17 node-b
Poin kritis: partisi logis harus jauh lebih banyak dari node fisik (mis. 4096 partition untuk 16 node). Ini yang bikin rebalancing murah — yang dipindah adalah partition, bukan re-hash seluruh keyspace (lihat: consistent hashing, rendezvous hashing).
Dua keluarga fungsi:
| Hash-based | Range-based | |
|---|---|---|
| Distribusi | Rata secara statistik | Mengikuti data |
| Range scan | ❌ fan-out ke semua shard | ✅ lokal |
| Prefix/ordered access | ❌ | ✅ |
| Hotspot dari sequential key | ✅ imun | ❌ rentan (timestamp, auto-increment) |
| Rebalance granularity | partition | split point |
Range sharding dengan monotonic key (timestamp, UUIDv7, snowflake) = selalu hot di ujung kanan. Ini bukan bug implementasi, ini sifatnya. Solusi: prefix acak (2–3 char), atau hash prefix + sort key di dalam partisi.
Kriteria shard key yang benar (empat, harus semua):
- Cardinality tinggi — minimal ~10× jumlah partisi.
country(200 nilai) dengan 64 shard = distribusi kasar. - Akses merata — bukan cuma jumlah key merata, tapi request rate merata. 1 juta user tapi 1 user = 40% traffic → tetap hot.
- Ada di hampir semua query — kalau 80% query tidak menyertakan shard key, 80% query jadi scatter-gather.
- Immutable / low-churn — update pada field shard key = delete+insert lintas partisi. Mahal dan sering tidak didukung.
Composite shard key sering jawabannya: (tenant_id, user_id) atau (tenant_id, created_at). Tenant besar bisa di-split lebih lanjut; tenant kecil tetap rapat (locality bagus untuk query per tenant).
🏗️ Pseudocode: deteksi hot partition sebelum produksi
# estimasi load per shard dari histogram akses 7 hari
from collections import Counter
def shard_load(requests, num_partitions, key_fn):
buckets = Counter()
for tenant, user, weight in requests: # weight = req/detik
p = key_fn(tenant, user) % num_partitions
buckets[p] += weight
total = sum(buckets.values())
mean = total / num_partitions
peak = max(buckets.values())
# metrik utama: peak/mean, BUKAN stdev
return {"peak_over_mean": peak / mean, "hottest": buckets.most_common(3)}
# threshold praktis: >2.0 = desain ulang sekarang
assert shard_load(demo_reqs, 4096, key_fn)["peak_over_mean"] < 2.0
Kenapa peak/mean dan bukan stdev: stdev bilang "ada variasi"; peak/mean bilang "partisi terpanas menerima N× rata-rata" — itu yang langsung diterjemahkan ke throttling.
Aturan kasar: peak/mean > 2 → tolak desain. > 1.5 di trafik puncak (mis. flash sale) → tolak juga. Karena headroom yang kamu hitung di rata-rata akan habis di p99.
⚖️ Trade-off & Kapan JANGAN Dipakai
Jangan shard kalau:
- Masih bisa vertical scale (RAM/CPU), read replica (read-heavy), atau partitioning dalam satu node (Postgres declarative partitioning). Ketiganya menyelesaikan 80% kasus "database terlalu besar" dengan 5% kompleksitas.
- Traffic-mu < ~5k write/s dan dataset < ~1–2 TB. Angka ini bukan hukum, tapi rule of thumb yang menyelamatkan banyak tim dari prematur optimisasi.
- Kamu belum punya routing layer yang bisa diuji (proxy atau client library). Sharding tanpa routing layer = setiap service tahu topologi = coupling permanen ke jumlah shard.
Biaya yang sering dilupakan (bukan cuma ops):
| Biaya | Dampak |
|---|---|
| Cross-shard JOIN | harus di app layer / scatter-gather, latency = max(shard), bukan sum |
| Distributed transaction | 2PC atau saga; 2PC punya blocking failure mode |
| Global unique constraint | hilang; butuh ID generator terpusat (snowflake, ULID) atau reservation table |
| Cross-shard aggregation | COUNT/SUM jadi map-reduce; dashboard jadi mahal |
| Rebalancing | butuh backfill + dual-write + verifikasi; window nya berhari-hari |
| Debugging | satu request menyentuh 5 node; tracing jadi wajib, bukan opsional |
Yang benar-benar tidak bisa ditawar: sekali shard, selamanya kamu punya kompleksitas ini. Tidak ada "un-shard" yang murah.
Resharding reality check (terverifikasi):
- MongoDB: shard key immutable sampai 4.4 yang menambah
refineCollectionShardKey(hanya append field ke suffix shard key, tidak bisa ganti prefix).reshardCollectionbaru ada di 5.0 dan tetap butuh ruang disk + waktu; bukan operasi gratis. [MongoDB docs — Choose a Shard Key] - DynamoDB: partition throughput di-cap 3.000 RCU / 1.000 WCU per partisi, dan item > batas itu tetap throttle walau tabel punya kapasitas besar. Adaptive capacity meminjamkan kapasitas dari partisi dingin, tapi tidak menembus cap per partisi. Jadi single hot key = hard ceiling, apapun provisioning-mu. [AWS docs — burst & adaptive capacity]
- Cassandra/DynamoDB: hot partition juga dibatasi ukuran (~10 GB per partition di DynamoDB) — artinya bukan cuma throughput, tapi kapan data harus di-split.
🏭 Contoh Produksi & Failure Mode
1. Shard key = tenant_id dengan satu tenant raksasa.
Enterprise customer dengan 30% traffic jatuh di satu shard. Shard itu CPU 100%, sisanya idle 8%. Adaptive capacity tidak menolong karena cap per-partisi. Yang terjadi: throttling untuk tenant itu, dan tim lain melihat dashboard hijau — insiden tak terlihat sampai tenant besar komplain.
→ Fix struktural: (tenant_id, bucket) dengan bucket = hash(user_id) % 16. Tenant kecil bayar overhead routing kecil; tenant besar tersebar.
2. Shard key = timestamp / auto-increment.
Kafka-like ingestion, key = created_at. Semua write hari ini ke shard terakhir. Write throughput dibatasi satu node selamanya, dan tidak akan pernah membaik seiring pertumbuhan cluster. Ini arsitektur yang "scalable" di atas kertas dan tidak pernah scale di praktik.
3. Cross-shard query yang tumbuh diam-diam. Mulai dengan 3 query lintas shard. Dua tahun kemudian semua list endpoint pakai filter yang tidak ada shard key-nya → setiap page load = 64 RPC. Latency p50 terlihat OK (paralel), p99 hancur karena tail latency = max dari 64 sampel (lihat: tail-at-scale). Query count tumbuh lebih cepat dari traffic.
4. Resharding tanpa dual-write. Backfill 800 juta row sambil live traffic. Kalau tidak ada dual-write + verifikasi checksum per-partisi, kamu akan menemukan drift 3 minggu kemudian tanpa cara mengetahui baris mana yang salah.
5. Hot partition dari celebrity / viral content.
Key = post_id. Satu post viral = semua read ke satu partisi. Cap per partisi terlampaui meski cluster longgar. Solusi kanonik: key salting (post_id + random 0..9, read fan-out 10 partisi, lalu merge) atau cache di depan. Trade-off: write jadi 10× lebih mahal.
⚠️ Miskonsepsi Umum
| Miskonsepsi | Koreksi |
|---|---|
| "Hash sharding menyelesaikan hot partition" | Hash menyebar key, bukan load. Satu key dengan 50k req/s tetap di satu partisi. Hash hanya menyelesaikan hot partition yang disebabkan key skew, bukan load skew. |
| "Tambah shard = tambah throughput" | Hanya kalau load terdistribusi. Kalau 1 partisi panas, tambah node tidak mengubah apapun — cap per partisi tetap. |
| "Consistent hashing bikin rebalance otomatis dan murah" | Murah dalam jumlah key yang dipindah, tapi tetap butuh backfill data aktual + verifikasi. "Murah" ≠ "gratis". |
| "Shard key harus unik/primary key" | Tidak. Shard key justru sering non-unik (tenant_id) dan itu bagus untuk locality. Yang penting: ada di query dan load-nya rata. |
| "Kalau butuh cross-shard transaction, pakai 2PC saja" | 2PC punya failure mode blocking (coordinator mati = lock tertahan). Di sistem yang harus available, saga + idempotency + kompensasi lebih realistis — dengan konsekuensi eventual consistency yang harus didesain ke UI. |
| "Cardinality tinggi otomatis bagus" | UUIDv4 punya cardinality tinggi dan distribusi bagus, tapi locality-nya nol: semua query per-tenant jadi scatter-gather. Distribusi dan locality adalah trade-off, bukan dua hal yang bisa dimenangkan bersamaan. |
✅ Cek Pemahaman
- Sistem multi-tenant SaaS: 5.000 tenant, 1 tenant = 45% traffic, 1 tenant = 22%, sisanya kecil. Query dominan:
WHERE tenant_id = ? AND created_at > ?. Usulkan shard key dan jelaskan bagaimana kamu menangani dua tenant raksasa itu tanpa membuat tenant kecil menderita. - Kamu punya collection MongoDB dengan shard key
{created_at: 1}dan write 8k/s. Jelaskan mengapa menambah shard tidak menaikkan throughput, dan sebutkan dua jalur perbaikan beserta biayanya masing-masing. - Kapan keputusan untuk tidak shard adalah keputusan arsitektur yang lebih baik? Sebutkan tiga sinyal konkret yang membuatmu memilih vertical scale + read replica.
🔑 Poin Kunci
- Shard key = kontrak permanen. Pilih berdasarkan pola akses, bukan bentuk data.
- Distribusi rata ≠ load rata. Ukur
peak/meandari request rate, bukan dari jumlah baris. - Hot partition adalah hard cap per partisi (DynamoDB: 3k RCU / 1k WCU, ~10 GB). Adaptive capacity meminjam kapasitas, tidak menembus cap.
- Composite key
(tenant, sub-key)adalah jawaban default untuk multi-tenant: locality untuk yang kecil, split untuk yang besar. - Hash menyelesaikan key skew; salting menyelesaikan load skew; cache menyelesaikan read skew. Tiga masalah berbeda, tiga obat berbeda.
- Resharding itu mungkin (MongoDB 5.0+), tapi mahal dan berisiko. Desain benar di awal lebih murah dari semua opsi migrasi.
- Order of operations yang benar: index & query tuning → partitioning → read replica → cache → baru sharding.
➡️ Eksplorasi Lanjut
- Consistent hashing vs rendezvous hashing — kenapa yang kedua menang untuk cache routing (tidak butuh ring, distribusi lebih baik saat node sedikit).
- Tail latency amplification — kenapa p99 satu shard jadi p50 user; baca "The Tail at Scale" (Dean & Barroso, CACM 2013). Ini alasan matematis kenapa scatter-gather mahal.
- Saga pattern & idempotency key — cara hidup tanpa distributed transaction.
- Virtual buckets / fixed partition count — pola 4096 partisi (Kafka, Vitess, DynamoDB) sebagai cara membuat resharding jadi operasi pemindahan metadata.
- Celebrity problem — splitting satu key panas jadi N sub-key, dan bagaimana read path harus merge.
- Praktik: baca studi kasus sharding Notion (Postgres, 2021) dan Figma (vertical partitioning Postgres, 2024) — keduanya contoh keputusan menolak sharding aplikasi dan memilih partisi dalam satu DB.