¿Cómo manejas una hot key o partición caliente que está saturando un solo shard?
Un producto viral, una cuenta de celebridad o un contador popular pueden enviar todo tu tráfico a un solo shard sin importar cuántos nodos tenga el resto de tu clúster.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo manejas una hot key o partición caliente que está saturando un solo shard?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Qué es realmente una hot key
Una hot key (o partición caliente) ocurre cuando tu esquema de sharding es, en teoría, balanceado — basado en hash, distribuido de forma uniforme, correcto según el libro de texto — pero el tráfico real no es uniforme. Una página de producto viral, el contador de seguidores de una celebridad, un SKU del Black Friday, o un feature flag compartido globalmente recibe accesos o actualizaciones órdenes de magnitud por encima de cualquier otra clave, y cada solicitud para esa clave cae exactamente en el mismo shard físico porque eso es justo lo que garantiza un hashing determinista. El resto de tu clúster puede estar al 5% de CPU mientras ese único shard está al 100% y arrojando timeouts — agregar más nodos al clúster no sirve de nada, porque la función de hash sigue enviando cada solicitud de esa clave al mismo lugar.
#Primero, detecta el problema
Antes de aplicar una solución, confirma que realmente es un problema de hot key y no un subdimensionamiento general: busca una partición/shard con CPU, latencia de solicitudes o errores de throttling dramáticamente más altos que sus pares, mientras el resto del clúster está inactivo. La mayoría de los almacenes de datos gestionados exponen métricas por partición justo por esta razón (las métricas de throttling por partición de DynamoDB, las estadísticas por slot de Redis Cluster, el lag de consumidor por partición de Kafka). Si todos los shards están calientes de manera uniforme, tienes un problema de capacidad, no de hot key, y la solución es distinta (más shards, no claves más inteligentes).
#Solución 1: salar la clave para repartir las escrituras entre shards
La técnica estándar es el salting de claves: en vez de una sola clave lógica, mantener N claves físicas, y enrutar cada escritura a una elegida al azar (o por round-robin). Las lecturas se convierten en un scatter-gather que suma entre las N. Esto cambia complejidad del lado de lectura por escalabilidad del lado de escritura — casi siempre un excelente intercambio, ya que las hot keys casi siempre están calientes del lado de escritura (contadores de vistas, conteos de likes, puntajes de tendencia).
import randomimport redisNUM_SHARDS = 10class ShardedCounter:"""Reparte un contador caliente entre NUM_SHARDS claves físicas para queninguna clave individual de Redis (y por lo tanto ningún slot de hasho partición individual) absorba todo el tráfico de escritura."""def __init__(self, client: redis.Redis, base_key: str, shards: int = NUM_SHARDS):self.client = clientself.base_key = base_keyself.shards = shardsdef _shard_key(self, shard: int) -> str:return f"{self.base_key}:shard:{shard}"def increment(self, amount: int = 1) -> None:# Elige un shard al azar en cada escritura para que la carga se# reparta de forma pareja entre las NUM_SHARDS claves, en vez# de golpear siempre a la misma.shard = random.randint(0, self.shards - 1)self.client.incrby(self._shard_key(shard), amount)def get_total(self) -> int:# Scatter-gather: lee cada shard y suma. Este es el costo que se# paga del lado de lectura a cambio de escrituras distribuidas# y baratas.pipe = self.client.pipeline()for shard in range(self.shards):pipe.get(self._shard_key(shard))values = pipe.execute()return sum(int(v) for v in values if v is not None)
#Solución 2: cachear el agregado para acotar el costo de lectura
El scatter-gather no es gratis — sumar 10 (o 100) shards en cada lectura multiplica tu carga de lectura por la cantidad de shards. Si el agregado no necesita estar perfectamente actualizado (un contador de vistas con 2 segundos de retraso está bien; el saldo de un banco no), cachea el total sumado por un TTL corto para que la mayoría de las lecturas se resuelvan con un solo acierto de caché en vez de un scatter-gather completo.
CACHE_TTL_SECONDS = 2class CachedShardedCounter(ShardedCounter):"""Acota el costo del scatter-gather de get_total() cacheando elresultado agregado durante una ventana corta. Una hot key se leecon mucha más frecuencia de la que necesita un valor perfectamenteactualizado."""def __init__(self, client: redis.Redis, base_key: str, shards: int = NUM_SHARDS):super().__init__(client, base_key, shards)self._cache_key = f"{base_key}:total_cache"def get_total(self) -> int:cached = self.client.get(self._cache_key)if cached is not None:return int(cached)total = super().get_total()self.client.set(self._cache_key, total, ex=CACHE_TTL_SECONDS)return total
#Solución 3: agrega localmente antes de siquiera tocar el shard
La escritura más barata es la que nunca envías. Especialmente para contadores, los servidores de aplicación pueden acumular incrementos en memoria local durante una ventana corta (digamos, 500ms-1s) y enviar un único incremento por lote en vez de un round trip por evento. Si una página de producto recibe 10,000 eventos de vista por segundo repartidos entre 50 servidores de aplicación, la pre-agregación local puede convertir eso en 50 solicitudes por segundo llegando al contador (una por servidor por intervalo de flush) en lugar de 10,000 — a menudo suficiente por sí solo para que el salting deje de ser necesario.
#Solución 4: incorpora el shard directamente en la clave de partición
Sistemas como DynamoDB no ofrecen un "selector de shard" separado — la clave de partición es el mecanismo de sharding. El patrón es la misma idea aplicada a nivel de esquema: agregar un sufijo aleatorio o hasheado directamente a la clave de partición, de modo que lo que antes era una clave de partición caliente se convierta en varias particiones físicas.
import randomSUFFIX_RANGE = 10def write_partition_key(product_id: str) -> str:"""Estilo DynamoDB: incorpora el sufijo de shard directamente en laclave de partición para que las escrituras del item caliente caiganen SUFFIX_RANGE particiones físicas distintas en vez de una sola."""suffix = random.randint(0, SUFFIX_RANGE - 1)return f"PRODUCT#{product_id}#SHARD#{suffix}"def read_all_partitions(product_id: str) -> list[str]:return [f"PRODUCT#{product_id}#SHARD#{i}" for i in range(SUFFIX_RANGE)]
Un BatchGetItem (o el equivalente de lectura masiva en el almacén que uses) contra read_all_partitions(product_id) reemplaza el único GetItem caliente por una lectura en fan-out entre particiones con carga pareja.
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Por qué agregar más réplicas de lectura no soluciona una partición caliente causada por un volumen alto de escrituras a una sola clave?
2. En el patrón ShardedCounter, ¿cuál es el principal costo que se intercambia por escalabilidad de escritura?
3. ¿Por qué sigue siendo riesgoso un sufijo aleatorio que en realidad no es uniforme (por ejemplo, shardear por día de la semana)?