Dienstag, 14:30. Ein Influencer mit 2 Millionen Followern postet einen Link zu deinem Produkt. Dein Gesamtthroughput sieht gut aus — 50k req/s, Cluster healthy, alle Dashboards grün. Bis jemand den Consumer Lag aufmacht. Eine Consumer Group fällt zurück. Nicht alle Partitions. Eine Partition. P2. Lag steigt von 50 auf 20.000. Dann 40.000.
Die anderen fünf Partitions? Langweilen sich.
Du guckst auf den Key und siehst es sofort: alle Requests von diesem Influencer-Account landen auf derselben Partition. Weil dein Partition Key userId ist. Und hash("user-28491") ergibt — Überraschung — immer denselben Wert.
Wie Partitioning funktioniert
Kurzer Refresher, falls nötig. Kafka verteilt Nachrichten auf Partitions. Wenn du keinen Key angibst, geht’s Round-Robin, gleichmäßig verteilt. Sobald du einen Key setzt, bestimmt hash(key) % numPartitions die Zielpartition. Das garantiert: gleicher Key, gleiche Partition. Immer.
Das ist oft genau was du willst. Ordering innerhalb eines Users. Alle Events einer Session zusammen. State-Aggregation pro Key. Alles sauber.
Bis ein Key 80% des Traffics ausmacht.
Starte den normalen Traffic und drück dann auf “Viral Event” — schau was mit P2 passiert:
Siehst du die Skew-Ratio? Eine Partition am Limit, die anderen idle. Dein Cluster hat 6x die Kapazität die es braucht, und trotzdem laggt ein Consumer hinterher, weil die Last nicht verteilt ist. Jetzt aktivier Key Salting und trigger nochmal. Anderes Bild.
Das Problem ist nicht Kafka-spezifisch. DynamoDB hat dasselbe mit Partition Keys: eine heiße Partition throttled den gesamten Table, auch wenn die anderen Partitions leer sind. Redis Cluster: ein Hot Key auf einem Shard, die anderen fünf Shards schauen zu.
Warum hash(userId) deterministisch wehtut
Das ist kein Bug. Es ist by design. Deterministic Hashing ist der Grund warum Ordering per Key funktioniert. Kafka muss denselben Key auf dieselbe Partition legen, sonst kannst du keine Consumer bauen die auf Reihenfolge angewiesen sind.
Aber Determinismus heißt auch: wenn ein Key hot wird, bleibt er hot. Es gibt keinen Mechanismus der sagt “dieser Key macht 40% des Traffics aus, lass mich den auf mehrere Partitions verteilen.” Die Hash-Funktion weiß nichts über Lastverteilung. Die rechnet, fertig.
Und das Fiese: du merkst es oft erst in Production. In deiner Testumgebung hast du gleichmäßig verteilte User-IDs. Niemand simuliert den einen Account der plötzlich viral geht.
Drei Wege raus
Key Salting
Die einfachste Variante. Statt userId als Key nimmst du userId-{random(0, N)}. Damit verteilst du den Traffic eines einzelnen Users auf N Partitions.
const NUM_SALT_BUCKETS = 6;
async function publishEvent(userId: string, event: OrderEvent) {
// Salt nur für bekannte Hot Keys — oder für alle, wenn dir
// Ordering pro User nicht kritisch ist
const salt = Math.floor(Math.random() * NUM_SALT_BUCKETS);
const partitionKey = `${userId}-${salt}`;
await producer.send({
topic: 'order-events',
messages: [{
key: partitionKey,
value: JSON.stringify(event),
}],
});
}
Trade-off ist klar: du verlierst Ordering innerhalb eines Users. Wenn du Events eines Users in Reihenfolge verarbeiten musst, ist blindes Salting keine Option. Für viele Use Cases — Analytics, Notifications, Log-Aggregation — ist Ordering pro User aber gar nicht nötig.
Du musst nicht alle Keys salten. Eine Allowlist der Top-N Keys reicht oft. Die erkennst du über Consumer Lag per Partition oder Producer-Metriken pro Key. Der Rest bleibt deterministisch.
Compound Keys
Statt Random Salt nimmst du eine zweite Dimension die inhaltlich Sinn macht. userId-orderId, userId-sessionId, userId-{timestamp % 60}. Damit behältst du Ordering innerhalb der Subdimension — alle Events einer Order landen zusammen — aber verteilst den User über Partitions.
const partitionKey = `${userId}-${orderId}`;
Das funktioniert gut wenn deine Business-Logik natürliche Subkeys hat. Eine Bestellung, eine Session, ein Checkout-Flow. Ordering innerhalb der Subentität, Verteilung über Partitions. Win-win.
Dedicated Hot-Key Handling
Der aggressivste Ansatz: du erkennst Hot Keys in Echtzeit und routest sie auf ein separates Topic oder in einen eigenen Processing-Pfad. Der Producer tracked Request-Counts pro Key und ab einem Threshold geht der Key in den Hot-Pfad.
const keyCounters = new Map<string, number>();
const HOT_THRESHOLD = 5000; // req/s
function getTopicForKey(userId: string): string {
const count = keyCounters.get(userId) ?? 0;
keyCounters.set(userId, count + 1);
if (count > HOT_THRESHOLD) {
return 'order-events-hot'; // separates Topic, mehr Partitions
}
return 'order-events';
}
Mehr Komplexität, aber du behältst volle Kontrolle. Das Hot-Topic kann mehr Partitions haben, eigene Consumer Groups, eigene Scaling-Regeln. Instagram macht das so mit ihren Activity Feeds — die Top-1000 Accounts haben eigene Processing-Pipelines.
Monitoring: bevor es knallt
Du erkennst Hot Partitions bevor sie zum Problem werden. Drei Metriken:
Consumer Lag per Partition. Nicht aggregiert über die Consumer Group, sondern pro Partition. Wenn eine Partition konstant höheren Lag hat als die anderen, hast du einen Hot Key. kafka_consumer_group_lag mit Partition-Label in Prometheus.
Throughput Skew. Vergleich Messages/s über alle Partitions eines Topics. Eine gesunde Verteilung hat weniger als 1.5x Skew zwischen der heißesten und der kältesten Partition. Ab 3x wird’s kritisch.
Producer Key Cardinality. Wenn 80% deiner Messages von 10 Keys kommen, ist es nur eine Frage der Zeit bis einer davon explodiert. Sampling reicht, du brauchst nicht jeden Key zu tracken.
In DynamoDB gibt dir CloudWatch die ThrottledRequests-Metrik pro Partition. In Redis Cluster zeigt SLOWLOG dir welcher Shard unter Last steht. Die Symptome sind überall gleich: eine Partition heiß, der Rest kalt.
Es ist eine Load-Balancing-Entscheidung
Ich denke, die meisten Teams behandeln den Partition Key als Nebensache. “Nimm die User-ID, passt schon.” Und für 95% des Traffics passt es auch. Aber der Partition Key ist eine Load-Balancing-Entscheidung. Du entscheidest damit, wie dein System unter extremer Last verteilt. Und “extreme Last” heißt nicht “gleichmäßig 10x mehr”, sondern “ein Key macht plötzlich 80% aus.”
Frag dich bei jedem Partition Key: was passiert, wenn ein einzelner Wert dieses Keys viral geht? Wenn die Antwort “eine Partition stirbt” ist, hast du ein Design-Problem das du jetzt lösen solltest. Nicht wenn der Influencer-Tweet live geht.