Späť na blog

Jedna partition na 99% CPU: Zastav Kafka hotspoty skôr ako dorazia do produkcie

|
| kafka, debugging, testing, ci, partition-key

Kafka partition skew ma naučil, že “rovnomerne rozdelené” je lož, pokiaľ to nedokážeš dátami. “Prečo rastie consumer lag? Všetky metriky vyzerajú normálne.” Zízali sme do Grafana dashboardov ukazujúcich ako consumer group lag stúpa, rebalance sa dejú a občas prichádzajú timeout errory. CPU na Kafka brokeroch vyzeralo v poriadku - okrem jedného brokera obsluhujúceho partition 23, ktorý bol na 99% CPU kým ostatné boli na 15%.

Incident sa odvíjal tri dni. Prvý deň: “Možno je to zvláštny traffic spike.” Druhý deň: “Rebalancing by to malo opraviť.” Tretí deň: “Ups, to sa vôbec nevyvažuje - jedna partition dostáva 60% všetkých eventov.” Root cause bol trápne jednoduchý: zmenili sme partition key z userId (milióny hodnôt) na countryCode (12 hodnôt). Nikto to nechytil v code review pretože logika generovania kľúča bola skrytá v shared library.

Čo robilo toto zvlášť frustrujúce bolo, že testovanie vyzeralo perfektne. Unit testy prešli. Integration testy prešli. Load testy ukazovali “rovnomerne distribuovaný” traffic. Ale load testy používali generované UUIDs, nie reálnu produkčnú distribúciu dát. V momente keď dorazil skutočný user traffic s reálnou distribúciou krajín (80% z jednej krajiny), partition 23 sa stal bottleneckom a všetko downstream začalo timeoutovať.

Toto je typ bugu čo ťa prinúti spochybniť celú testovú stratégiu. Kafka partition key je v skutočnosti API kontrakt - zmeníš ho a môžeš ticho rozbiť celý streaming pipeline. Ale na rozdiel od REST API zmien, neexistuje schema validácia, compile-time check ani štandardný spôsob ako zachytiť kardinalitné problémy pred produkciou.

Prostredie: Kafka 3.6+, Java produceri, 48 partitionov, multi-region user base

Pochopenie Partition Skew

Ako Kafka Partitioning funguje (a zlyhá)

Očakávané správanie (dobrý key):
Topic: user-events (48 partitionov)
Key: userId (milióny unikátnych hodnôt)

partition_id = hash(key) % 48

Distribúcia:
Partition 0:  ████░░░░░░  2.1%
Partition 1:  ███░░░░░░░  2.0%
Partition 2:  ████░░░░░░  2.2%
...
Partition 47: ███░░░░░░░  2.0%

✓ Rovnomerný CPU load
✓ Rovnomerný consumer lag
✓ Predvídateľná latencia
Čo sa skutočne deje (zlý key):
Topic: user-events (48 partitionov)
Key: countryCode (12 unikátnych hodnôt... ale nie rovnomerne distribuovaných)

Reálna produkčná distribúcia:
- US: 80% trafficu
- UK: 10%
- DE: 5%
- Ostatní: 5%

partition_id = hash("US") % 48 = 23  ← 80% ide sem!
partition_id = hash("UK") % 48 = 7   ← 10% ide sem
partition_id = hash("DE") % 48 = 41  ← 5% ide sem

Distribúcia:
Partition 23: ████████████████████████████████  80% ← ROZTOPENIE
Partition 7:  ████░░░░░░  10%
Partition 41: ██░░░░░░░░   5%
Ostatné:      ░░░░░░░░░░   5%

❌ Jeden consumer zaseknutý
❌ Lag spiky
❌ Rebalance timeouty
❌ Downstream kaskádové zlyhania

Prečo štandardné testovanie toto prehliada

// Tento test vyzerá v poriadku ale nič nedokazuje
@Test
void testPartitionDistribution() {
    for (int i = 0; i < 100000; i++) {
        String key = UUID.randomUUID().toString();  // ← FAKE DATA!
        producer.send(new ProducerRecord<>("events", key, event));
    }
    // Distribúcia je perfektná... pretože UUIDs sú náhodné
}

Test prejde pretože náhodné UUIDs majú perfektnú kardinalitu. Produkcia zlyhá pretože reálne keys nie.

Bežné Skew scenáre (videl som všetky)

1. Nízka kardinalita key

// Predtým: dobré
record.setKey(userId);  // milióny userov

// Potom: katastrofa
record.setKey(tenantId);  // 50 tenantov, 1 má 80% userov

2. Normalizácia zabije distribúciu

// Vyzerá nevinne
String normalizedEmail = email.trim().toLowerCase();
record.setKey(normalizedEmail);

// Ale ak tvoje emaily sú ako:
// "[email protected]  " (s trailing spacmi)
// "[email protected]"
// Oba sa hashujú na tú istú partition po normalizácii

// Horšie: null handling
String key = userId != null ? userId : "";  // Všetky nully → jedna partition!

3. Composite Key Prefix dominancia

// Zámer: distribuovať podľa tenant aj user
String key = String.format("%s:%s", tenantId, userId);

// Realita: hash of "TENANT_A:user123" je dominovaný prefixom
// Ak TENANT_A má 80% userov → skew

Skew Contract prístup

Namiesto nádeje že tvoj key je dobrý, definuj explicitný kontrakt:

# skew_contract.yml
version: 1

topics:
  user-events:
    partitions: 48
    max_partition_share: 0.08   # Žiadna partition by nemala dostať >8% trafficu
    max_gini: 0.15              # Gini koeficient meria nerovnosť
    min_unique_keys: 20000      # Zistí vysokú kardinalitu
    sample_size: 100000         # Testuj s reálnou sample veľkosťou

Tento kontrakt hovorí:

  • max_partition_share: V ideálnom svete každá z 48 partitions dostane 2.08%. Povoľ až 8% (nejaká variancia je OK).
  • max_gini: Gini koeficient 0 = perfektná rovnosť, 1 = kompletná nerovnosť. 0.15 povolí prirodzenú varianciu.
  • min_unique_keys: Tvoj sample musí mať aspoň 20k unikátnych keys (chytí nízku kardinalitu skoro).

Implementácia: Testovanie keys bez Kafka

Trik: nepotrebuješ Kafka cluster. Potrebuješ len testovať tvoju logiku generovania key s realistickými dátami.

Krok 1: Key Sampler Test (Java)

// src/test/java/com/yourapp/kafka/KeySamplerTest.java
import org.junit.jupiter.api.Test;
import java.nio.file.*;
import java.util.*;

class KeySamplerTest {

    @Test
    void generateRepresentativeKeys() throws Exception {
        // Načítaj realistické test data (anonymizovaný produkčný sample, fixtures, atď.)
        List<UserEvent> events = loadRealisticEvents();  // Tvoja implementácia

        List<String> keys = new ArrayList<>(100_000);

        for (UserEvent event : events) {
            // TOTO JE KRITICKÁ ČASŤ: použi tvoju REÁLNU key logiku
            String key = KeyFactory.keyFor(event);
            keys.add(key);
        }

        // Zápis do súboru pre validáciu
        Path output = Paths.get("build/kafka_keys_user_events.txt");
        Files.createDirectories(output.getParent());
        Files.write(output, keys, UTF_8);

        System.out.printf("Vygenerovaných %d keys s %d unikátnymi hodnotami\n",
            keys.size(), new HashSet<>(keys).size());
    }

    private List<UserEvent> loadRealisticEvents() {
        // DÔLEŽITÉ: Toto by malo reflektovať produkčnú distribúciu
        // - 80% US userov, 10% UK, atď. ak je to tvoja realita
        // - Mix veľkosti tenantov ak si B2B
        // - Zahrňaj edge casy: null handling, špeciálne znaky

        // Príklad (nahraď tvojimi reálnymi dátami):
        return IntStream.range(0, 100_000)
            .mapToObj(i -> {
                String country = selectCountryRealistic();  // Vážená distribúcia
                String userId = "user-" + (i % 50000);  // Realistická kardinalita
                return new UserEvent(country, userId, "action");
            })
            .collect(Collectors.toList());
    }
}

Krok 2: Skew Contract Validator (Go CLI)

Tento nástroj používa ten istý murmur2 hash čo Kafka Java klienti:

go run ./cmd/skewcontract \
  -topic user-events \
  -keys build/kafka_keys_user_events.txt \
  -partitions 48 \
  -max-share 0.08 \
  -max-gini 0.15 \
  -min-unique 20000

Validator:

  1. Číta tvoje vygenerované keys
  2. Aplikuje murmur2(key) % partitions (Kafka default)
  3. Vypočíta distribučné metriky
  4. Zlyhá ak skew prekročí prahy

CI Integrácia (GitHub Actions)

name: kafka-skew-contract

on: [pull_request]

jobs:
  skew:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'

      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Vygeneruj partition keys z realistického sample
        run: ./gradlew test --tests KeySamplerTest

      - name: Over skew kontrakt
        run: |
          go run ./cmd/skewcontract \
            -topic user-events \
            -keys build/kafka_keys_user_events.txt \
            -partitions 48 \
            -max-share 0.08 \
            -max-gini 0.15 \
            -min-unique 20000 \
            -report skew_report.json

      - name: Uploadni report (pre debugovanie zlyhaní)
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: skew-report
          path: skew_report.json

Toto ti dá rýchly, deterministický gate ktorý beží ~10 sekúnd a chytí skew pred merge.

Runtime Monitoring (pretože CI nestačí)

Aj s CI gates, produkcia ťa môže prekvapiť:

  • Nové traffic patterny
  • Data migrácia zmení distribúciu
  • A/B testy posunú user segmenty

Monitoruj tieto metriky:

# Partition lag skew
max by (partition) (kafka_consumer_lag{topic="user-events"})
/ avg by (partition) (kafka_consumer_lag{topic="user-events"})
> 3  # Jedna partition má 3x priemerný lag

# Partition size skew
max by (partition) (kafka_log_size{topic="user-events"})
/ avg by (partition) (kafka_log_size{topic="user-events"})
> 2

Alertuj keď tieto ratios vystrielia - znamená to že sa formuje hotspot.

Keď sa Skew stane v produkcii

Ak detekované skew po deploymente:

Možnosť 1: Oprav key (preferované)

// Zlé: nízka kardinalita
String key = countryCode;

// Dobré: pridaj high-cardinality komponent
String key = countryCode + ":" + userId;
// alebo lepšie: len userId ak je globálne unikátny
String key = userId;

Potom re-publishni eventy s novým key (alebo akceptuj dočasný skew počas prechodu).

Možnosť 2: Zvýš partitiony (rizikové)

kafka-topics --alter --topic user-events --partitions 96

Pozor: Toto pomáha len ak tvoj key má dosť kardinalitu. Ak máš 12 country codes a 96 partitionov, stále používaš len 12 partitionov.

Checklist

## Partition Key Checklist

### Pred zmenou keys
- [ ] Spusti key sampler test s realistickou produkčnou distribúciou dát
- [ ] Over kardinalitu: `SELECT COUNT(DISTINCT key_column) FROM events`
- [ ] Skontroluj null handling: `WHERE key_column IS NULL`
- [ ] Prezri normalizačnú logiku (trim, lowercase, substring)

### CI Kontrakt
- [ ] `max_partition_share` nastavený podľa `1/partitions * acceptable_variance`
- [ ] `min_unique_keys``partitions * 100` (pravidlo palca)
- [ ] Sample data reflektuje produkčnú distribúciu (nie random UUIDs!)

### Produkčné Monitorovanie
- [ ] Alert na partition lag skew ratio > 3x
- [ ] Alert na partition size skew ratio > 2x
- [ ] Dashboard ukazujúci per-partition metriky

### Keď je Skew detekovaný
- [ ] Identifikuj key kardinalitu: príliš nízka? normalizačný bug? null handling?
- [ ] Oprav key generation logiku (preferuj high-cardinality stabilné IDčka)
- [ ] Ak neopraviteľné, zvaž topic split alebo akceptuj varianciu
- [ ] Zvyšuj partitiony len ak key má dostatočnú kardinalitu

Záver

Kafka partition key je API kontrakt ktorý väčšina tímov netreatuje ako jeden. Zmeníš ho nedbalo a dostaneš tiché hotspoty, consumer lag spiky a záhadné timeout errory - všetko kým tvoje dashboardy ukazujú “všetko je v poriadku.”

Skew Contracts spravia z “vyber dobrý key” testovateľnú, vynukovateľnú policy. Implementácia je prekvapivo jednoduchá: vygeneruj realistické keys v teste, hashuj ich rovnako ako Kafka, zmer distribúciu, zlyhá ak je skewed. Žiaden Kafka cluster potrebný, žiadne flaky testy, len deterministická matematika.

Kľúčový insight: tvoj partition key nie je implementačný detail, je to API. Treatuj ho tak.


Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Jedna partition na 99% CPU: Zastav Kafka hotspoty skôr ako dorazia do produkcie". https://www.michal-drozd.com/sk/blog/kafka-partition-skew-contracts/ (Publikované 15. novembra 2025).