Sharding por hash¶
Cuando un dataset crece más allá de lo que cabe en una sola BD, el patrón es sharding horizontal: partir filas entre N servidores físicos por una shard key (típicamente tenant_id, user_id, org_id).
dorm.contrib.sharding (3.4+).
Cuándo usarlo¶
- Tabla principal pasó del TB; vertical scaling agotado.
- Tenants distribuidos geográficamente (US-east, EU-west).
- Carga write-heavy que satura un único primary.
Cuándo NO¶
- <100GB por tabla y <5000 QPS — vertical scaling es mucho más simple.
- Si tus queries hacen JOINs cross-shard frecuentes — el sharding rompe esos. Replanteate el modelo de datos primero.
- Sin separación clara por una clave (queries todo-vs-todo contra cualquier subset).
API¶
from dorm.contrib.sharding import (
HashShardRouter, with_shard_key, shard_for, for_each_shard,
)
# settings.py
DATABASES = {
"default": {...},
"shard_0": {...},
"shard_1": {...},
"shard_2": {...},
"shard_3": {...},
}
DATABASE_ROUTERS = [
HashShardRouter(num_shards=4, shard_models={Order, Customer}),
]
# Request handler
from dorm.contrib.sharding import with_shard_key
@app.post("/orders")
async def create_order(request, body):
with with_shard_key(request.user.tenant_id):
order = await Order.objects.acreate(...) # ruteado a shard_N
return order
Hash determinista¶
shard_for(key, num_shards) usa hashlib.blake2b con salt
configurable, NO Python's built-in hash() (que es
randomizado per-proceso desde Python 3.3 — usaría una asignación
de shards distinta en cada worker).
from dorm.contrib.sharding import shard_for
assert shard_for("user-42", 4) == shard_for("user-42", 4) # determinista
# Algunos llamadores prefieren su propio salt para seguridad:
shard_for("user-42", 4, salt=b"mi-salt-secreta")
for_each_shard — fan-out¶
Para queries globales (count total, batch jobs por shard):
from dorm.contrib.sharding import for_each_shard
results = for_each_shard(
lambda alias: Order.objects.using(alias).count(),
num_shards=4,
)
# {"shard_0": 1234, "shard_1": 1209, ...}
total = sum(results.values())
Secuencial; envuelve en asyncio.gather o threads para
paralelismo si es necesario.
Composer con multi-tenancy fila¶
HashShardRouter + TenantModel componen elegantemente — la
shard key suele ser el tenant id:
with current_tenant(tenant_id), with_shard_key(tenant_id):
Note.objects.create(title="hi")
# → tenant_id auto-fill + ruteo al shard correcto
Sin shard key activa¶
Si tu modelo es sharded y no hay with_shard_key() activo:
Por diseño. Un fallback silencioso a default repartiría rows
inconsistentemente entre shards.
Rebalancing (shard splits)¶
dorm no rebalancea automáticamente. Si pasas de 4 → 8 shards:
- Crea los nuevos shards (vacíos).
- Cambia
num_shards=8en producción — nuevos rows van con distribución nueva. - Por cada shard viejo, migra rows a su nuevo destino:
- Pausa o no del tráfico durante la migración: tu decisión ops/négocio.
Para evitar este dolor, hash consistente (consistent hashing ring) en lugar de modulo. dorm no lo implementa de fábrica; considera Citus o Vitess si lo necesitas.
Pitfalls¶
- JOINs cross-shard imposibles — cada shard es una BD independiente. Modelo data antes de shardar.
allow_relationrechaza FKs cross-shard: el router devuelveFalsecuando obj1 / obj2 viven en aliases distintos. Atrapas bugs en código antes de runtime.- Migraciones:
dorm migrateaplica solo endefaultpor defecto. Para correr en cada shard:
Más¶
- Helpers
- Multi-tenancy fila — combinación natural
- API:
dorm.contrib.sharding