Що таке ключ повідомлення і як він впливає на партиціонування
Уявіть аеропорт:
🟢 Junior Level
Що таке ключ повідомлення?
Ключ повідомлення (Message Key) — це значення, яке визначає, в яку партицію потрапить повідомлення. Ключ — це «адреса» для маршрутизації повідомлення.
Навіщо: спосіб сказати Kafka: «усі події, пов’язані з цим користувачем/замовленням, мають оброблятися в одному місці». Без ключа події одного користувача розлетяться по різних партиціях і можуть прийти в неправильному порядку.
Аналогія
Уявіть аеропорт:
- Ключ = код призначення рейсу (наприклад, «KYIV-BA101»)
- Партиція = конкретний гейт
- Усі пасажири з одним кодом йдуть на один гейт
- Пасажири з різними кодами — на різні гейти
key="KYIV-BA101" → гейт 5 (партиція 1)
key="NYC-AA202" → гейт 3 (партиція 0)
key="PARIS-AF303" → гейт 7 (партиція 2)
Краща аналогія: ключ — це номер справи в суді. Усі документи по одній справі потрапляють в одну папку і розглядаються у строгому порядку. Документи різних справ — у різних папках, порядок між ними не важливий.
Навіщо потрібен ключ?
- Гарантія порядку — повідомлення з одним ключем йдуть в одну партицію → зберігають порядок
- Групування — усі події одного користувача/об’єкта обробляються разом
Приклад
// З ключем — порядок гарантовано
ProducerRecord<String, String> record =
new ProducerRecord<>("orders", "user-123", "Order created");
// topic key value
// user-123 завжди потрапить в одну й ту саму партицію
З ключем:
user-123 → партиція 1 → порядок: login → browse → purchase
Без ключа:
msg-1 → партиція 2
msg-2 → партиція 0 // порядок не гарантовано
msg-3 → партиція 1
Коли НЕ використовувати ключ повідомлення
- Порядок повідомлень не важливий і потрібна максимальна throughput — key=null
- Ключі мають сильний skew (нерівномірний розподіл) — hot partition
- Розмір ключа занадто великий (>1KB) — CPU overhead на хешування
🟡 Middle Level
Гарантії порядку
Партиція 0: user-456 події (строго по порядку)
Партиція 1: user-123 події (строго по порядку)
Партиція 2: user-789 події (строго по порядку)
Важливо: Порядок гарантовано тільки всередині однієї партиції! Між партиціями — немає гарантій.
Алгоритм хешування
partition = Math.abs(Utils.murmur2(keyBytes)) % numPartitions
Kafka використовує MurmurHash 2 для рівномірного розподілу ключів по партиціях.
Вибір хорошого ключа
| Ключ | Розподіл | Коли використовувати |
|---|---|---|
userId |
✅ Рівномірне (багато користувачів) | User events |
orderId |
✅ Рівномірне | Order events |
"all" |
❌ Hot partition | Не використовуйте для event-driven систем. Допустимо для broadcast (глобальна конфігурація), але усвідомлюйте hot partition risk. |
null |
✅ Sticky partitioning | Коли порядок не важливий |
aggregateId |
✅ Рівномірне | Event sourcing |
Null Key
// key = null → sticky partitioning
// Порядок НЕ гарантується
ProducerRecord<String, String> record =
new ProducerRecord<>("orders", "Order created");
Порівняння стратегій ключів
| Стратегія | Порядок | Throughput | Розподіл |
|---|---|---|---|
| Business Key (userId) | ✅ Всередині партиції | Середний | Залежить від даних |
| Composite Key (userId_orderId) | ✅ | Середний | Хороший |
| Salted Key (userId + random) | ⚠️ Частковий | Хороший | Відмінний |
| Null Key | ❌ | Максимальний | Рівномірний |
Типові помилки
| Помилка | Наслідок | Рішення |
|---|---|---|
| Null key коли потрібен порядок | Події користувача перемішані | Використовуйте userId як ключ |
| Один ключ для всіх | Hot partition | Різні ключі для різних сутностей |
| Великий ключ (JSON) | CPU overhead на хешування | Компактні ключі (UUID, Long) |
| Зміна кількості партицій | Розподіл ключів змінюється | Створюйте новий топик |
🔴 Senior Level
Internal Implementation — MurmurHash 2
// org.apache.kafka.common.utils.Utils
public static int murmur2(byte[] data) {
int length = data.length;
int seed = 0x9747b28c; // Kafka seed
// ... MurmurHash2 algorithm ...
return fmix(hash(length, seed, data));
}
// Partition calculation
int hash = Utils.murmur2(keyBytes);
int partition = Math.abs(hash) % numPartitions;
Чому MurmurHash 2:
- Non-cryptographic, швидкий (бітові операції)
- Avalanche effect — зміна 1 біта → ~50% бітів результату змінюються
- Uniform distribution — мінімум колізій для реальних даних
Integer.MIN_VALUE edge case:
// Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE (від'ємне!)
// Kafka використовує бітову маску:
int partition = (hash & 0x7fffffff) % numPartitions;
Key Ordering Guarantees — формальна модель
Всередині партиції: строго FIFO (total order)
Між партиціями: partial order (causal ordering тільки при явному дизайні)
Формально:
∀ messages m1, m2:
key(m1) == key(m2) → partition(m1) == partition(m2) → order(m1, m2) preserved
key(m1) != key(m2) → order(m1, m2) NOT guaranteed
Advanced Key Strategies
1. Composite Key для боротьби з hot partitions:
// Складений ключ: entity + sub-entity
String key = userId + "_" + orderId;
// Забезпечує більш рівномірний розподіл
// Але: порядок тільки всередині userId_orderId, не всередині userId
2. Key Salting (randomized partitioning всередині entity):
// N-ary salting: розподіляємо події одного користувача по N партиціях
int salt = ThreadLocalRandom.current().nextInt(10);
String key = userId + "_" + salt;
// Порядок втрачається, але throughput росте в N разів
// Використовуйте коли порядок НЕ критичний
3. Time-based Key Rotation:
// Ключ включає часовий bucket
String key = userId + "_" + (System.currentTimeMillis() / 3600000);
// Кожну годину ключ змінюється → повідомлення потрапляють в нову партицію
// Корисно для time-windowed aggregations
4. Consistent Hashing для динамічних партицій:
Ring-based approach: мінімізує переміщення ключів при зміні N
Бібліотеки: ketama, jmpmv/consistent-hashing
Застосування: multi-tenant системи, sharded databases
Key Evolution Problem — формальний аналіз
До: partition = hash(key) % 3
key="user-1" → hash("user-1") % 3 = 1 → Partition 1
Після: partition = hash(key) % 6
key="user-1" → hash("user-1") % 6 = 4 → Partition 4
Той самий ключ → інша партиція → порядок порушено!
Математично:
P(new_partition != old_partition) = 1 - (old_N / new_N)
Для 3 → 6: P = 1 - 3/6 = 50% ключів перемістяться
Для 10 → 100: P = 1 - 10/100 = 90% ключів перемістяться
Рішення:
- Плануйте кількість партицій із запасом (×10 від поточної потреби)
- Використовуйте consistent hashing (кастомний партиціонер)
- Створюйте новий топик з потрібною кількістю партицій і мігруйте через dual-write
Key Size Impact on Performance
| Розмір ключа | Memory overhead | CPU (hash) | Network | Рекомендація |
|---|---|---|---|---|
| Long (8 bytes) | Мінімальний | ~5 ns | Мінімальний | ✅ Оптимально |
| UUID (36 bytes) | Середній | ~20 ns | Середній | ✅ Прийнятно |
| String (100 bytes) | Помітний | ~50 ns | Помітний | ⚠️ Допустимо |
| JSON (10 KB) | Значний | ~500 µs | Значний | ❌ Уникайте |
Memory footprint:
Ключ зберігається в:
- Producer RecordBatch → MemoryRecord (heap)
- Broker Log → MessageSet (page cache + heap)
- Consumer ConsumerRecord (heap)
Підсумок: 3 × розмір_ключа × throughput × retention
Для 10KB ключів при 10K msg/s = ~3 GB/s memory traffic
Edge Cases (3+)
-
Key Null + Idempotent Producer: При
enable.idempotence=trueіkey=null, Kafka генерує sequence number на рівні партиції. Оскільки sticky partitioning перемикає партиції, sequence numbers не конфліктують. Але якщоpartitioner.classкастомний і недетермінований — можливіOutOfOrderSequenceException. -
Key Serialization Schema Mismatch: Продюсер серіалізує ключ як String (
StringSerializer), консьюмер десеріалізує як ByteArray (ByteArrayDeserializer). Ключ «працює» для партиціонування, але бізнес-логіка консьюмера отримуєbyte[]замість очікуваногоString. Тестуйте серіалізацію на обох кінцях. -
Unicode Keys і MurmurHash:
MurmurHashпрацює на байтах. Ключ"café"в UTF-8 =[99, 97, 102, 195, 169], в Latin-1 =[99, 97, 102, 233]. Різні кодування → різні байти → різні партиції. Переконайтеся, що всі продюсери використовують однакове кодування (UTF-8 за замовчуванням в Kafka). -
Key Mutation в Processor: В Kafka Streams або ksqlDB, якщо processor змінює ключ повідомлення (
map((key, value) -> new KeyValue<>(newKey, value))), повідомлення перерозподіляється в нову партицію. Це викликає shuffle — дорогу операцію. Використовуйтеthrough()для явного контролю repartitioning. -
Empty Key (не null, але порожній рядок):
key=""→MurmurHash("")= константа → всі повідомлення з порожнім ключем в одну партицію. Це як hot partition, але складніше виявити (ключ «є», але він марний).
Performance Numbers
| Метрика | Значення | Умови |
|---|---|---|
| MurmurHash latency | ~5-50 ns | Залежить від розміру ключа |
| Partition compute overhead | < 100 ns | Включаючи серіалізацію |
| Key serialization (String, 36 bytes) | ~100 ns | UTF-8 encoding |
| Impact of hot key on p99 | 3-10x latency increase | 1 ключ = 50% трафіку |
Production War Story
Ситуація: Ride-sharing компанія використовувала
driverIdяк ключ для топикаrides. При 10 000 активних водіїв розподіл був рівномірним (20 партицій). Але в годину пік топ-50 водіїв (найактивніші) генерували 40% усіх подій.Проблема: P99 latency для замовлень виросла з 15ms до 500ms. Lag на «гарячих» партиціях досягав 30K повідомлень. Клієнти скаржилися на «завислі» замовлення.
Аналіз:
Top-10 keys by volume: driver-42: 15K msg/min → Partition 7 driver-17: 12K msg/min → Partition 3 driver-88: 11K msg/min → Partition 14 Average: 50 msg/min per driverРішення:
- Ввели composite key:
driverId + "_" + (timestamp / 60000)— ключ змінюється кожну хвилину- Збільшили партиції з 20 до 60
- Для агрегацій (count rides per driver) використали Kafka Streams з
.groupByKey()— автоматичний repartition- Налаштували alert на per-partition throughput imbalance > 3x
Урок: Рівномірний розподіл ключів ≠ рівномірне навантаження. Аналізуйте frequency distribution ключів, а не тільки cardinality.
Моніторинг (JMX + Burrow)
JMX метрики:
kafka.producer:type=ProducerMetrics,client-id=producer-1,key=record-send-rate
kafka.producer:type=ProducerMetrics,client-id=producer-1,key=batch-size-avg
kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec
Custom monitoring:
// Перехоплювач для аналізу розподілу ключів
public class KeyDistributionInterceptor implements ProducerInterceptor {
private final Map<String, Long> keyCounts = new ConcurrentHashMap<>();
public ProducerRecord onSend(ProducerRecord record) {
keyCounts.merge(String.valueOf(record.key()), 1L, Long::sum);
return record;
}
}
Highload Best Practices
- Ключ = компактний business entity ID (Long > UUID > String)
- Аналізуйте frequency distribution ключів перед production
- Composite/salted keys для боротьби з hot partitions (trade-off: порядок)
- Уникайте зміни кількості партицій — створюйте новий топик
- Тестуйте на production-like даних — реальні ключі мають skew
- Моніторте per-partition throughput — alert при imbalance > 3x
- Empty string != null — порожній ключ це hot partition
- Unicode consistency — усі продюсери повинні використовувати UTF-8
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Ключ визначає партицію:
MurmurHash2(key) % numPartitions - Повідомлення з однаковим ключем завжди потрапляють в одну партицію → порядок гарантовано
- Без ключа (null) — Sticky Partitioning, порядку немає
- Композитний ключ (
userId + "_" + orderId) — для рівномірного розподілу - Великий ключ (>1KB) — CPU overhead на хешування кожного повідомлення
- При зміні кількості партицій той самий ключ → інша партиція
- Empty string ≠ null — порожній ключ = hot partition (все в одну партицію)
- Ключ = compact business entity ID (Long > UUID > String)
Часті уточнюючі запитання:
- Що буде якщо key=null? — Sticky Partitioning, порядок не гарантовано.
- Чому не використовувати JSON як ключ? — 10KB ключ × 10K msg/s = ~3 GB/s memory traffic.
- Як забезпечити порядок при hot key? — Composite/salted key, trade-off: часткова втрата порядку.
- Що таке Key Evolution Problem? — При зміні N в
hash(key) % N50-90% ключів переміщуються.
Червоні прапорці (НЕ говорити):
- «Ключ гарантує глобальний порядок» — тільки всередині однієї партиції
- «Empty string те саме що null» — empty = hot partition, null = sticky
- «Можна змінювати кількість партицій без наслідків» — ключі переміщуються
- «Ключ зберігається тільки у продюсера» — зберігається в RecordBatch, Log, ConsumerRecord (3×)
Пов’язані теми:
- [[3. Як дані розподіляються по партиціях]]
- [[2. Що таке партиція (partition) і навіщо вона потрібна]]
- [[21. Що таке batch в Kafka producer]]
- [[1. Що таке топiк (topic) в Kafka]]