Animowany kurs · architektura i design

Cassandra od środka

Jak baza danych bez szefa przyjmuje miliony zapisów na sekundę, przeżywa awarie całych serwerowni i nigdy nie robi JOIN-ów. Pierścienie, plotki, nagrobki i drzewa — przewijaj, a wszystko zacznie się ruszać.

Twój mózg na Cassandrze 🧠
przewijaj ↓
0

Jak czytać ten kurs

…czyli dlaczego tu są obrazki, żarty i pytania

Twój mózg jest leniwy w bardzo sprytny sposób: zapamiętuje to, co dziwne, wizualne i emocjonalne, a wyrzuca nudne listy parametrów konfiguracyjnych. Dlatego zamiast suchej dokumentacji dostajesz tu diagramy, które same się animują, pogawędki komponentów bazy danych i pytania, które zmuszą Cię do myślenia. Merytorycznie nie odpuszczamy jednak niczego: po lekturze powinieneś umieć wyjaśnić, dlaczego Cassandra zachowuje się tak, jak się zachowuje, a nie tylko że tak robi.

🧠 Mózg na wysokich obrotach

Zanim zaczniesz: jak zaprojektowałbyś bazę, która ma przyjmować zapisy zawsze — nawet gdy połowa serwerów w jednej serwerowni właśnie zniknęła z sieci? Kto decyduje, gdzie leżą dane? Kto pilnuje porządku? Zapisz swoją odpowiedź (serio, choćby w głowie) i porównaj ją z Cassandrą za 13 rozdziałów.

Każdy diagram ze znaczkiem ▶ startuje sam, gdy wjedzie na ekran, ale możesz go też klikać, psuć i resetować. Najwięcej nauczysz się, psując: wyłączaj węzły, zmieniaj poziomy spójności, zapominaj o naprawach i patrz, co się stanie.

🤓 Rodowód

Cassandra powstała w Facebooku (2008, Avinash Lakshman i Prashant Malik) do wyszukiwania w skrzynce odbiorczej. To skrzyżowanie dwóch słynnych publikacji: Amazon Dynamo (2007) dał jej model dystrybucji — pierścień, consistent hashing, gossip, hinted handoff, tunable consistency, drzewa Merkle — a Google Bigtable (2006) model przechowywania: memtable, SSTable, commit log, kompakcja i model „szerokich wierszy”. Zapamiętaj ten podział: Dynamo = jak dane się rozchodzą po klastrze, Bigtable = jak leżą na jednym dysku.

1

Dlaczego bez mastera?

demokracja węzłów: każdy równy, nikt niezastąpiony

Klasyczna replikacja master–slave jest prosta: wszystkie zapisy idą do jednego serwera, który rozsyła je do kopii. Prosta aż do chwili, gdy master padnie. Wtedy zapisy stoją, dopóki ktoś (człowiek albo automat typu failover) nie wybierze nowego szefa — a jeśli wybierze dwóch naraz, masz split brain. Do tego master jest wąskim gardłem: wszystkie zapisy przechodzą przez jeden serwer, więc dokładanie maszyn nie zwiększa przepustowości zapisu.

Cassandra jest masterless / peer-to-peer: każdy węzeł może przyjąć dowolne zapytanie (staje się wtedy jego koordynatorem), każdy zna topologię klastra, każdy przechowuje fragment danych. Nie ma pojedynczego punktu awarii i nie ma specjalnych ról — nawet „seed nodes”, o których jeszcze usłyszysz, to tylko adresy kontaktowe, a nie szefowie.

▶ Awaria w dwóch światach kropki to zapisy klientów; po kilku sekundach coś pada
Master–slave: wszystkie zapisy przez M.
Cassandra: każdy węzeł może koordynować zapis.
❓ Nie istnieją głupie pytania
P:
Skoro nie ma mastera, kto decyduje, na którym serwerze leży mój wiersz?
O:
Matematyka. Każdy węzeł liczy to samo: hash klucza partycji → pozycja na pierścieniu → właściciel. Nie trzeba nikogo pytać, bo wszyscy mają tę samą mapę (dzięki gossipowi) i tę samą funkcję haszującą. Rozdział 2.
P:
A kto pilnuje, żeby kopie danych były zgodne?
O:
Nikt centralnie — i to jest świadoma decyzja. Zgodność jest „dociągana” przez trzy mechanizmy: hinted handoff (przy zapisie), read repair (przy odczycie) i anti-entropy repair (w tle, drzewa Merkle). Zobaczysz wszystkie trzy.
📌 Celne spostrzeżenia
  • Każdy węzeł jest równy: przyjmuje zapisy i odczyty, zna topologię, przechowuje część danych.
  • Węzeł, który dostał zapytanie od klienta, jest dla tego zapytania koordynatorem.
  • Przepustowość rośnie liniowo z liczbą węzłów, bo nie ma wąskiego gardła.
  • Cena: brak globalnej kolejności zapisów i spójność, którą ty konfigurujesz (rozdział 7).
2

Pierścień i consistent hashing

gdzie leży mój wiersz? zapytaj funkcję haszującą

Każdy wiersz w Cassandrze ma klucz partycji (partition key). Domyślny partitioner, Murmur3Partitioner, zamienia go na 64-bitową liczbę — token — z zakresu od −263 do 263−1. Wyobraź sobie ten zakres zwinięty w okrąg: to jest pierścień (ring). Każdy węzeł ma na nim swój token i jest właścicielem zakresu od tokena poprzednika (wyłącznie) do własnego (włącznie). Żeby znaleźć właściciela klucza, idziesz od jego tokena zgodnie z ruchem wskazówek zegara do pierwszego napotkanego węzła.

▶ Pierścień tokenów wpisz dowolny klucz, np. swoje imię
klucztokenwęzeł
Dodaj węzeł G i zobacz, ile kluczy musi się przeprowadzić.
Hash w tej animacji to uproszczony 32-bitowy mieszacz (Cassandra używa Murmur3 x64_128 i bierze pierwsze 64 bity), ale geometria jest identyczna.

Dlaczego nie zwykłe hash(klucz) mod N? Bo zmiana N przetasowuje prawie wszystko: przy przejściu z 6 na 7 serwerów ~86% kluczy zmienia właściciela, a każdy przeprowadzany klucz to dane przesyłane przez sieć. W consistent hashingu nowy węzeł rozcina jeden zakres — przeprowadzają się tylko klucze z kawałka między nim a jego poprzednikiem. Kliknij „Dodaj węzeł G” i porównaj liczby.

✏️ Zaostrz ołówek

Klaster ma 4 węzły z tokenami (w uproszczeniu na skali 0–100): A=10, B=40, C=70, D=90. Który węzeł jest właścicielem kluczy o tokenach 5, 40, 41 i 95?

Odpowiedź

5 → A (pierwszy token ≥ 5 to 10). 40 → B (zakres jest domknięty z prawej, więc token równy tokenowi węzła należy do niego). 41 → C. 95 → A — nie ma tokena ≥ 95, więc „zawijamy” przez koniec pierścienia do najmniejszego tokena.

🤓 Dla dociekliwych: partitioner

Partitioner ustawia się raz, dla całego klastra, i nie da się go zmienić bez przeładowania wszystkich danych. Historyczny RandomPartitioner używał MD5 (tokeny 0…2127), a ByteOrderedPartitioner układał klucze leksykograficznie — co pozwalało na zapytania zakresowe po kluczu partycji, ale tworzyło gorące punkty (wszystkie klucze „2026-09-…” lądowały na jednym węźle). Dlatego Cassandra celowo niszczy kolejność kluczy partycji: równomierny rozkład jest ważniejszy niż skanowanie zakresu. Kolejność zachowuje się dopiero wewnątrz partycji (kolumny klastrujące — rozdział 13).

3

Vnodes — wirtualne węzły

jeden serwer, wiele kawałków tortu

Jeden token na serwer ma dwie wady. Po pierwsze, zakresy wychodzą nierówne, więc jeden węzeł dźwiga dwa razy więcej danych niż sąsiad. Po drugie, gdy węzeł padnie, cały jego zakres przejmuje jeden sąsiad (i tylko on streamuje dane do zastępcy). Rozwiązanie z Cassandry 1.2: każdy fizyczny węzeł dostaje wiele tokenów — vnodes. Liczbę ustawia num_tokens (kiedyś domyślnie 256, od Cassandry 4.0 domyślnie 16 w połączeniu z inteligentnym algorytmem przydziału tokenów allocate_tokens_for_local_replication_factor).

▶ Jeden token kontra vnodes kolor łuku = fizyczny węzeł, który jest właścicielem zakresu
❓ Nie istnieją głupie pytania
P:
To czemu nie 1000 vnodes na węzeł? Im więcej, tym równiej.
O:
Bo każdy zakres kosztuje: więcej metadanych w gossipie, wolniejsze repair (naprawia się zakres po zakresie) i — co ważniejsze — przy dużej liczbie tokenów każdy węzeł współdzieli jakiś zakres z prawie każdym innym. Wtedy utrata dowolnych dwóch węzłów przy RF=3 i QUORUM prawie na pewno uniemożliwia dostęp do jakiegoś fragmentu danych. Stąd zejście z 256 na 16 w wersji 4.0.
4

Replikacja, snitch i centra danych

nie wkładaj wszystkich jajek do jednej szafy serwerowej

Replication factor (RF) mówi, ile kopii każdego wiersza trzyma klaster. RF ustawia się na poziomie keyspace’u razem ze strategią replikacji. Pierwsza replika trafia do właściciela tokena, a kolejne — do następnych węzłów idąc zgodnie z zegarem. Tu zaczyna się różnica między strategiami:

CREATE KEYSPACE sklep WITH replication = {
  'class': 'NetworkTopologyStrategy', 'dc1': 3, 'dc2': 2
};
▶ Gdzie trafiają repliki? zmień strategię i RF, potem kliknij „Umieść”

    Skąd Cassandra wie, który węzeł jest w którym DC i racku? Od snitcha. W produkcji używa się GossipingPropertyFileSnitch: każdy węzeł czyta swoje DC i rack z pliku cassandra-rackdc.properties i ogłasza je reszcie przez gossip. Nad nim zawsze działa dynamic snitch, który mierzy opóźnienia replik i przy odczytach omija wolniejsze („ten węzeł ma właśnie GC pause — zapytajmy inną replikę”).

    ⚠️ Uwaga, pułapka

    RF nie może być większe niż liczba węzłów w danym DC, a zmiana RF nie przenosi danych sama: po ALTER KEYSPACE nowe repliki są puste, dopóki nie uruchomisz nodetool repair (albo rebuild dla nowego DC). Do tego czasu odczyty z niskim CL mogą trafiać w pustą replikę i „nie widzieć” danych.

    5

    Gossip i wykrywanie awarii

    plotka obiega klaster szybciej niż myślisz

    Skoro nie ma centralnego rejestru, skąd węzły wiedzą, kto żyje, jakie ma tokeny, w którym jest DC i jaką wersję schematu? Z plotek. Co sekundę każdy węzeł wybiera losowo jednego żywego rozmówcę (czasem dodatkowo węzeł uznany za martwy i seed) i wymienia się z nim stanem w trzech wiadomościach: GossipDigestSyn → GossipDigestAck → GossipDigestAck2. Każda informacja ma wersję (para generation — czas startu węzła — i rosnący version), więc zawsze wiadomo, która jest świeższa.

    Magia epidemii: w każdej rundzie liczba „wtajemniczonych” mniej więcej się podwaja, więc informacja dociera do N węzłów po około log₂N rundach. 1000 węzłów? Kilkanaście sekund. Patrz sam:

    ▶ Epidemia plotki w 32-węzłowym klastrze żółty = zna nową wersję stanu; linie = rozmowy w tej rundzie
    Wtajemniczeni / runda
    …
    Detektor awarii φ
    Zabij węzeł, żeby zobaczyć, jak rośnie podejrzenie.

    Wykrywanie awarii to nie „nie odpowiedział 3 razy = martwy”. Cassandra używa Phi Accrual Failure Detector: zamiast binarnej decyzji liczy poziom podejrzenia φ na podstawie historii odstępów między heartbeatami danego węzła. φ rośnie z czasem ciszy (przy odstępach ~1 s: φ ≈ cisza / ln 10); gdy przekroczy phi_convict_threshold (domyślnie 8), węzeł zostaje uznany za DOWN — lokalnie, przez każdego obserwatora osobno. W chmurach z niestabilną siecią podnosi się to do 10–12.

    🔥 Pogawędka przy kominku

    Dzisiejsi goście: Seed Node i Zwykły Węzeł.

    Zwykły Węzeł
    Słyszałem, że jesteś kimś ważnym. „Seed”. Brzmi jak szef.
    Seed
    Nie, nie. Jestem po prostu adresem, który nowi znają na pamięć. Gdy startujesz i nikogo nie znasz, pukasz do mnie, a ja opowiadam ci o reszcie klastra.
    Zwykły Węzeł
    I tyle? Nie zatwierdzasz zapisów, nie trzymasz mapy klastra?
    Seed
    Mapę ma każdy z nas. Jedyna różnica: węzły trochę częściej plotkują ze mną, co pomaga szybko zszyć klaster po podziale sieci. Jak padnę, nic się nie zawali — nowe węzły po prostu zapukają do innego seeda z listy. Dlatego w każdym DC jest 2–3 seedy, a nie wszystkie węzły.
    Zwykły Węzeł
    Czyli jesteś… recepcją.
    Seed
    Recepcją z dobrą pamięcią. Tylko nie próbuj bootstrapować mnie jako nowego węzła — seed nie bootstrapuje się sam z siebie i nie pobierze danych.
    6

    Ścieżka zapisu

    dlaczego zapisy w Cassandrze są tak absurdalnie szybkie

    Tajemnica szybkich zapisów: Cassandra nigdy nie nadpisuje danych na dysku w miejscu i nie czyta przed zapisem. Każdy zapis na replice to dwa tanie kroki: dopisanie na koniec commit logu (sekwencyjny zapis — trwałość na wypadek awarii) i wstawienie do memtable (posortowana struktura w pamięci). Potwierdzenie leci do koordynatora. Koniec. Kiedy memtable urośnie, zostaje w całości zrzucona (flush) do nowego, niezmiennego pliku SSTable (Sorted String Table). To właśnie struktura LSM-tree (Log-Structured Merge tree) odziedziczona po Bigtable.

    ▶ Zapis krok po kroku INSERT i DELETE to w Cassandrze ten sam rodzaj operacji: dopisanie
    👩‍💻 Klient
    sterownik (driver)
    🧭 Koordynator
    węzeł, który dostał zapytanie
    PAMIĘĆ (RAM)
    Memtable posortowana po kluczu · flush przy 5 kluczach
      DYSK
      Commit log append-only, tylko do odtwarzania po awarii
      SSTables niezmienne pliki, nigdy nie edytowane
      Kliknij INSERT (albo po prostu poczekaj).
      🔥 Pogawędka przy kominku

      Dzisiejsi goście: Commit Log i Memtable.

      Memtable
      Nie rozumiem, po co ty w ogóle jesteś. Wszystkie dane mam ja, ładnie posortowane, do odczytu gotowe.
      Commit Log
      Masz je w RAM-ie. Zabierz prąd i co ci zostanie?
      Memtable
      …nic. Ale ty jesteś nieczytelny! Pomieszane klucze, jeden za drugim, bez ładu.
      Commit Log
      I dlatego jestem szybki: tylko dopisuję na koniec, zero szukania po dysku. Nikt mnie nigdy nie czyta — poza restartem po awarii, kiedy odtwarzam ciebie.
      Memtable
      A kiedy mnie zrzucą do SSTable?
      Commit Log
      Wtedy moje segmenty z twoimi danymi idą do recyklingu. Jesteśmy zespołem: ja daję trwałość, ty szybkie sortowanie. Aha, i fsync robię domyślnie co 10 sekund (commitlog_sync: periodic) — replikacja na inne węzły chroni przed tym, że akurat te 10 sekund zginie z jednego serwera.

      Kiedy replika leży: hinted handoff

      Koordynator wysyła zapis do wszystkich replik danego klucza. Jeśli któraś jest DOWN (wie to z gossipu), zapisuje dla niej hint — „przypominajkę” z mutacją — na swoim dysku. Gdy gossip ogłosi, że replika wróciła, koordynator odtwarza jej hinty. Hinty zbiera się tylko przez max_hint_window (domyślnie 3 godziny); węzeł leżący dłużej trzeba naprawić przez nodetool repair. Hint to optymalizacja, nie gwarancja — i (poza CL=ANY) nie liczy się do potwierdzeń zapisu.

      📌 Celne spostrzeżenia
      • Zapis = commit log (sekwencyjnie) + memtable (RAM). Żadnego odczytu przed zapisem, żadnych blokad wierszy.
      • INSERT i UPDATE to to samo: upsert. Wygrywa wartość z nowszym timestampem (per komórka, nie per wiersz).
      • SSTable są niezmienne. „Zmiana” i „usunięcie” to nowe wpisy, które przesłaniają stare — sprzątanie robi kompakcja.
      • Hinted handoff łata krótkie awarie (≤ 3 h); dłuższe łata repair.
      7

      Tunable consistency

      spójność to suwak, nie przełącznik

      Każde zapytanie niesie consistency level (CL): ile replik musi odpowiedzieć, zanim koordynator uzna operację za udaną. Najważniejsze poziomy: ONE, TWO, THREE, QUORUM (⌊RF/2⌋+1 replik), ALL, a dla wielu DC: LOCAL_ONE, LOCAL_QUORUM (kworum tylko w lokalnym DC — najpopularniejszy wybór produkcyjny) i EACH_QUORUM. Dla zapisów jest jeszcze ANY — wystarczy, że ktokolwiek, choćby koordynator jako hint, przyjął zapis.

      Magiczna nierówność: jeśli R + W > RF, zbiór replik, które potwierdziły zapis, i zbiór replik czytanych muszą się przeciąć — więc odczyt zobaczy najnowszą potwierdzoną wartość.

      ▶ Laboratorium spójności kliknij replikę, żeby ją wyłączyć / włączyć
      Spróbuj: RF=3, zapis ONE, odczyt ONE, wyłącz hinted handoff, wyłącz R1, zapisz, włącz R1 i czytaj kilka razy.
      Repliki do odczytu wybierane są tu losowo — w prawdziwym klastrze wybiera je dynamic snitch (najszybsze). Pierwsza dostaje żądanie pełnych danych, pozostałe tylko o digest (hash odpowiedzi).
      ❓ Nie istnieją głupie pytania
      P:
      Przy zapisie z CL=ONE dane trafiają tylko do jednej repliki?
      O:
      Nie! To najczęstszy mit. Koordynator zawsze wysyła zapis do wszystkich replik (RF). CL mówi tylko, na ile potwierdzeń czeka, zanim odpowie klientowi. Pozostałe repliki dostaną dane chwilę później.
      P:
      A jeśli zapis z QUORUM się nie uda (timeout), to dane zostały zapisane czy nie?
      O:
      Może tak, może nie, może częściowo. WriteTimeoutException znaczy „nie dostałem dość potwierdzeń na czas” — nie ma rollbacku. Dlatego zapisy w Cassandrze projektuje się jako idempotentne, żeby można je było bezpiecznie ponowić. Za to UnavailableException znaczy „z góry wiedziałem, że za mało replik żyje, nic nie wysłałem”.
      ✏️ Zaostrz ołówek

      RF=3 w każdym z dwóch DC. Aplikacja w dc1 pisze i czyta z LOCAL_QUORUM. (1) Czy odczyt w dc1 zawsze widzi zapis z dc1? (2) Czy aplikacja w dc2 czytająca LOCAL_QUORUM zawsze widzi zapis z dc1? (3) Ile węzłów w dc1 może leżeć?

      Odpowiedź

      (1) Tak: 2 + 2 > 3 w obrębie dc1. (2) Nie — replikacja do dc2 jest asynchroniczna względem klienta; spójność między DC jest ewentualna (chyba że zapis idzie z EACH_QUORUM albo QUORUM liczonym po wszystkich 6 replikach). (3) Jedna replika danego zakresu; przy vnodes w praktyce „jeden węzeł w DC”, bo dwa leżące węzły prawie na pewno dzielą jakiś zakres.

      8

      Ścieżka odczytu

      poszukiwanie wiersza rozrzuconego po wielu plikach

      Zapisy były tanie, bo niczego nie porządkowaliśmy. Rachunek przychodzi przy odczycie: kawałki jednego wiersza mogą leżeć w memtable i w kilku SSTables (każdy UPDATE innej kolumny to osobny fragment). Replika musi je wszystkie znaleźć i scalić, wybierając dla każdej komórki wartość z najnowszym timestampem. Żeby nie czytać każdego pliku z dysku, każda SSTable ma zestaw pomocników:

      1. Filtr Blooma (Filter.db, w RAM) — probabilistyczne „na pewno nie ma” albo „może jest”.
      2. Key cache — pamięta pozycje ostatnio czytanych partycji w pliku.
      3. Partition summary (próbka indeksu w RAM) → partition index (Index.db) — gdzie w pliku zaczyna się partycja. (Cassandra 5.0 ma też nowy format BTI oparty na drzewach trie: Partitions.db/Rows.db.)
      4. Compression offsets (CompressionInfo.db) — który skompresowany blok (domyślnie 16 KiB) trzeba rozpakować.
      5. Wreszcie dane (Data.db).
      ▶ Filtr Blooma na żywo 32 bity, 3 funkcje haszujące
      Dodaj klucze, potem sprawdzaj inne.

      Filtr Blooma nigdy nie kłamie w stronę „nie ma” — jeśli choć jeden z bitów klucza jest zerem, klucza na pewno nie dodano. Może natomiast fałszywie powiedzieć „może jest”. Prawdopodobieństwo ustawia bloom_filter_fp_chance (typowo 0.01; im mniej, tym więcej RAM-u). Tu kompletny odczyt klucza id=42 na jednej replice:

      ▶ Odczyt SELECT * FROM users WHERE id = 42 na replice
      źródłofiltr Bloomaindeks / pozycjaznalezione komórki

      Poziom wyżej: koordynator i read repair

      Przy CL wyższym niż ONE koordynator prosi jedną (najszybszą według dynamic snitcha) replikę o pełne dane, a pozostałe tylko o digest — hash wyniku. Jeśli hashe się zgadzają, odsyła wynik. Jeśli nie — pobiera pełne dane od wszystkich pytanych, scala je (najnowszy timestamp wygrywa) i przed odpowiedzią dopisuje brakujące wersje do przestarzałych replik. To jest blocking read repair (w Cassandrze 4.0 usunięto dawne losowe „read_repair_chance”; zostało naprawianie tego, co wyszło przy okazji odczytu). Dodatkowo speculative retry (domyślnie 99p) wysyła zapytanie do kolejnej repliki, jeśli pierwsza odpowiada wolniej niż 99. percentyl. Możesz to zobaczyć w laboratorium spójności z rozdziału 7.

      😌 Rozluźnij się

      Nie musisz pamiętać nazw wszystkich plików SSTable. Zapamiętaj zasadę: każda SSTable więcej to potencjalnie jeden odczyt dysku więcej. Stąd cała reszta — filtry Blooma, cache i przede wszystkim kompakcja, która trzyma liczbę SSTables w ryzach.

      9

      LSM-tree i kompakcja

      sprzątanie po imprezie niezmiennych plików

      Każdy flush dokłada nową SSTable. Bez sprzątania po tygodniu miałbyś ich tysiące, a każdy odczyt dotykałby dziesiątek plików. Kompakcja bierze kilka SSTables, scala je sortowaniem przez scalanie (merge sort — tanie, bo pliki już są posortowane), wybiera najnowsze wersje komórek, wyrzuca nadpisane dane i przedawnione tombstony, i zapisuje nowy plik. Stare są usuwane. Każda strategia to kompromis między trzema „wzmocnieniami”:

      ▶ Symulator kompakcji co takt memtable robi flush nowej SSTable (~10 MB)
      🤓 Która strategia kiedy?

      SizeTieredCompactionStrategy (STCS) — domyślna przez lata. Scala ≥ min_threshold (4) plików o podobnym rozmiarze. Świetna dla obciążeń z przewagą zapisów; słabsza dla odczytów aktualizowanych wierszy i wymaga dużo wolnego miejsca.

      LeveledCompactionStrategy (LCS) — pliki po ~160 MB w poziomach L1, L2… każdy 10× większy od poprzedniego, w obrębie poziomu zakresy tokenów się nie nakładają. ~90% odczytów obsługuje jedna SSTable. Płacisz dużo większym write amplification — dobra do obciążeń z przewagą odczytów i częstymi aktualizacjami.

      TimeWindowCompactionStrategy (TWCS) — dla szeregów czasowych z TTL. Dane grupowane w okna czasowe; po zamknięciu okna jego SSTables scalają się w jedną i już nigdy nie są dotykane. Gdy wszystko w pliku wygaśnie, cały plik jest po prostu usuwany — zero kompakcji tombstonów. Warunek: nie aktualizuj starych danych i nie mieszaj TTL-i.

      UnifiedCompactionStrategy (UCS) — nowość z Cassandry 5.0: jedna strategia sparametryzowana tak, by płynnie przechodzić między zachowaniem „tiered” a „leveled”, z równoległą kompakcją shardów.

      10

      Tombstones — nagrobki

      jak usunąć coś z niezmiennych plików? postawić nagrobek

      Skoro SSTables są niezmienne, DELETE nie może niczego wymazać. Zamiast tego zapisuje tombstone — znacznik „ta komórka / wiersz / zakres / partycja jest usunięta od timestampu T”. Przy odczycie tombstone przesłania starsze dane, a przy kompakcji dane i nagrobek w końcu znikają. Tombstony powstają też, gdy:

      Ale uwaga: kompakcja może wyrzucić tombstone dopiero po gc_grace_seconds (domyślnie 864000 s = 10 dni). Dlaczego tak długo? Bo nagrobek musi zdążyć dotrzeć do wszystkich replik. Jeśli któraś go nie dostała, a pozostałe już go usunęły… zobacz sam, co się dzieje:

      ▶ Atak zombie RF=3, usuwamy email użytkownika
      gc_grace_seconds
      ⚠️ Uwaga: anty-wzorzec kolejki

      Tabela używana jak kolejka (wstaw zadanie → przeczytaj najstarsze → usuń) to klasyczna katastrofa. Każde „pobranie z kolejki” musi przeskoczyć tysiące tombstonów poprzednich zadań, bo leżą w tej samej partycji przez 10 dni. Po przekroczeniu tombstone_warn_threshold (1000 tombstonów w jednym zapytaniu) dostajesz ostrzeżenie w logach, a po tombstone_failure_threshold (100 000) zapytanie jest przerywane (TombstoneOverwhelmingException).

      📌 Celne spostrzeżenia
      • Delete to zapis. Tombstone przesłania dane z mniejszym timestampem (przy remisie timestampów tombstone wygrywa).
      • Tombstone żyje co najmniej gc_grace_seconds; wyrzuca go dopiero kompakcja, która widzi też przesłonięte dane.
      • Pełny repair każdej tabeli musi przejść częściej niż raz na gc_grace_seconds — inaczej grożą zombie.
      • Nie wstawiaj nulli, nie używaj tabel jako kolejek, preferuj TTL + TWCS dla danych z datą ważności.
      11

      Anti-entropy repair i drzewa Merkle

      jak porównać terabajty, przesyłając kilobajty

      Hinted handoff i read repair naprawiają tylko to, co „przy okazji” zauważą. Dane, których nikt nie czyta, mogą się rozjeżdżać latami. Dlatego jest nodetool repair: każda replika buduje dla swojego zakresu tokenów drzewo Merkle (tzw. validation compaction — czyta dane i je haszuje). Liście to hashe małych podzakresów tokenów, a każdy węzeł wewnętrzny to hash dzieci. Repliki wymieniają się drzewami, porównują je od korzenia w dół i streamują tylko te podzakresy, których liście się różnią.

      ▶ Porównanie drzew Merkle dwóch replik 8 liści = 8 podzakresów tokenów
      Identyczne hashe korzeni = identyczne dane w całym zakresie. Różne = schodzimy niżej.
      🤓 Repair w praktyce

      Prawdziwe drzewa są głębokie (do 220 liści na sesję), ale i tak każdy liść obejmuje sporo partycji — jedna różna komórka powoduje przesłanie całego podzakresu („overstreaming”). Rodzaje: full repair (wszystko), incremental repair (domyślny w nodetool repair od lat; naprawia tylko SSTables jeszcze nienaprawione i oznacza je jako „repaired”), -pr (tylko zakres główny danego węzła — uruchamiasz na każdym węźle po kolei). W praktyce repair orkiestruje się narzędziem (Cassandra Reaper lub wbudowany w nowsze wersje harmonogram), bo ręczne pilnowanie „raz na gc_grace” to proszenie się o zombie.

      12

      CAP, AP i lightweight transactions

      gdy sieć się rozpada, wybierz: odpowiadać czy mieć rację

      Twierdzenie CAP mówi, że gdy nastąpi podział sieci (P), system musi wybrać między spójnością (C) a dostępnością (A). Cassandra jest projektowana jako AP: domyślnie woli przyjąć zapis i pogodzić wersje później (last-write-wins po timestampie). Ale dzięki tunable consistency pojedyncze zapytanie może zażądać zachowania „CP-podobnego” (np. QUORUM) — kosztem tego, że po mniejszościowej stronie podziału dostanie błąd.

      ▶ Podział sieci RF=3: repliki A i B po lewej, C po prawej
      Wybierz CL i przetnij sieć.
      🤓 PACELC — ważniejsze na co dzień

      Podziały sieci są rzadkie; opóźnienia są zawsze. Rozszerzenie PACELC mówi: gdy P — wybierz A lub C; w przeciwnym razie (Else) — wybierz Latency lub Consistency. Cassandra to PA/EL: nawet bez awarii domyślnie wybiera niskie opóźnienia (CL=ONE/LOCAL_ONE), a ty podnosisz CL tam, gdzie spójność jest warta dodatkowych milisekund.

      Lightweight transactions (LWT)

      Czasem naprawdę potrzebujesz „zapisz tylko, jeśli…”, np. unikalna nazwa użytkownika. Do tego służą LWT: INSERT … IF NOT EXISTS, UPDATE … IF balance = 100. Pod spodem działa Paxos między replikami danej partycji (CL SERIAL / LOCAL_SERIAL): prepare/promise, odczyt aktualnej wartości, propose/accept, commit — klasycznie 4 rundy sieciowe zamiast jednej (Paxos v2 z Cassandry 4.1 ścina to do ~2). To linearizowalność dla jednej partycji — nie ma transakcji obejmujących wiele partycji (na to czeka projekt Accord z CEP-15). Używaj LWT oszczędnie i nie mieszaj ich ze zwykłymi zapisami tych samych danych.

      INSERT INTO users_by_login (login, user_id) VALUES ('ala', 42) IF NOT EXISTS;
      --  [applied] | login | user_id
      --  False     | ala   | 17        ← ktoś był szybszy
      13

      Modelowanie danych: najpierw zapytania

      zapomnij o 3NF — projektujesz pod odczyt

      W bazie relacyjnej modelujesz byty i relacje, a zapytania składasz JOIN-ami. W Cassandrze nie ma JOIN-ów, a zapytanie powinno trafić w jedną partycję (jeden zestaw replik, kilka sekwencyjnych odczytów). Zatem odwracasz proces: zaczynasz od listy zapytań aplikacji, a dla każdego projektujesz tabelę (query-first modeling). Te same dane lądują w kilku tabelach — to denormalizacja i jest zamierzona: dysk jest tani, a zapisy szybkie. Płacisz tym, że aplikacja (lub batch LOGGED) musi pisać do kilku tabel.

      Od zapytań do tabel
      Q1: Pokaż ostatnie wiadomości na kanale
      →
      messages_by_channel
      ((channel_id, bucket), sent_at DESC)
      Q2: Pokaż kanały użytkownika
      →
      channels_by_user
      ((user_id), channel_name)
      Q3: Znajdź użytkownika po loginie
      →
      users_by_login
      ((login))

      Anatomia klucza głównego

      CREATE TABLE messages_by_channel (
        channel_id  text,
        bucket      text,        -- np. tydzień: '2026-W39'
        sent_at     timeuuid,
        author      text,
        body        text,
        PRIMARY KEY ((channel_id, bucket), sent_at)
      ) WITH CLUSTERING ORDER BY (sent_at DESC);
      ▶ Szerokie partycje i gorące punkty każdy kwadracik to wiersz (wiadomość); najnowsze po lewej (DESC)
      Wiersze na węzłach A–F (RF=1, żeby było widać nierówność):
      Wyślij kilka wiadomości, a potem zalej jeden kanał.
      ✏️ Zaostrz ołówek: kalkulator partycji

      Reguła kciuka: trzymaj partycję poniżej ~100 MB i ~100 000 wierszy. Liczba wartości (komórek) w partycji: Nv = Nr × (Nc − Npk − Ns) + Ns (wiersze × kolumny nie-kluczowe i nie-statyczne + kolumny statyczne).

      ⚠️ Anty-wzorce modelowania
      • ALLOW FILTERING na dużej tabeli — skanuje cały klaster. Jeśli go potrzebujesz, brakuje ci tabeli.
      • Nieograniczone partycje (kanał/czujnik/użytkownik bez bucketu czasowego) — rosną w nieskończoność i robią gorące punkty.
      • Klucz partycji o niskiej kardynalności (country, status) — kilka gigantycznych partycji zamiast milionów małych.
      • Read-before-write („przeczytaj, zmień, zapisz”) — wyścigi; użyj upsertu, liczników albo LWT.
      • Klasyczne indeksy wtórne na kolumnach o wysokiej kardynalności — zapytanie pyta każdy węzeł. (Nowe SAI z Cassandry 5.0 radzą sobie znacznie lepiej, ale nadal nie zastąpią dobrze dobranego klucza partycji.)
      • Wielkie batche na wiele partycji „dla wydajności” — batch w Cassandrze służy atomowości, nie szybkości; obciąża koordynatora.
      📌 Celne spostrzeżenia
      • Jedno zapytanie → jedna tabela → (najlepiej) jedna partycja.
      • Klucz partycji = rozkład danych i równomierność obciążenia; kolumny klastrujące = kolejność i zakresy wewnątrz partycji.
      • Denormalizacja jest normalna; spójność między tabelami utrzymuje aplikacja (logged batch dla atomowości).
      • Ogranicz wzrost partycji bucketem (czas, hash) — patrz rozmiar i liczba wierszy.
      14

      Egzamin końcowy i ściąga

      sprawdź, czy twój mózg wszystko zapamiętał
      Wynik: 0 / 0

      Ściąga: parametry, które warto znać

      parametrdomyślnieco robi
      partitionerMurmur3Partitionerklucz partycji → token (−2⁶³ … 2⁶³−1)
      num_tokens16 (4.0+), dawniej 256liczba vnodes na węzeł
      phi_convict_threshold8próg φ, po którym węzeł jest uznany za DOWN
      max_hint_window3 hjak długo zbierać hinty dla leżącej repliki
      commitlog_syncperiodic, 10 sjak często fsync commit logu
      gc_grace_seconds864000 (10 dni)minimalny czas życia tombstone; repair musi być częstszy
      tombstone_warn / failure_threshold1 000 / 100 000ostrzeżenie / przerwanie zapytania skanującego tombstony
      bloom_filter_fp_chance0.01odsetek fałszywych „może jest” w filtrze Blooma
      speculative_retry99pkiedy zapytać dodatkową replikę przy odczycie
      compactionSTCS (UCS w 5.0 jako alternatywa)strategia scalania SSTables, per tabela
      QUORUM⌊RF/2⌋ + 1R + W > RF ⇒ odczyt widzi ostatni potwierdzony zapis
      🎓 Gratulacje

      Przeszedłeś całą drogę: od hasha klucza, przez pierścień, plotki, commit log, memtable i SSTables, po drzewa Merkle i model zapytań. Wróć do pytania z rozdziału 0 — twoja baza „zawsze przyjmująca zapisy” prawdopodobnie wygląda teraz podejrzanie podobnie do Cassandry. Następny krok: postaw 3 węzły w Dockerze, włącz nodetool status, nodetool tablestats i TRACING ON w cqlsh — i obejrzyj te same mechanizmy na prawdziwych danych.