Jak czytać ten kurs
…czyli dlaczego tu są obrazki, żarty i pytaniaTwó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.
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.
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.
Dlaczego bez mastera?
demokracja węzłów: każdy równy, nikt niezastąpionyKlasyczna 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.
- 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).
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.
| klucz | token | węzeł |
|---|
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.
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.
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).
Vnodes — wirtualne węzły
jeden serwer, wiele kawałków tortuJeden 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).
Replikacja, snitch i centra danych
nie wkładaj wszystkich jajek do jednej szafy serwerowejReplication 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:
SimpleStrategy— bierze po prostu kolejne RF węzłów. Dobre tylko na laptopa, bo nie wie nic o szafach i serwerowniach.NetworkTopologyStrategy— RF osobno dla każdego centrum danych (np.{'dc1': 3, 'dc2': 2}); idąc po pierścieniu, pomija węzły z DC, które ma już komplet, i stara się rozkładać repliki po różnych rackach (szafach). To jest wybór produkcyjny.
CREATE KEYSPACE sklep WITH replication = {
'class': 'NetworkTopologyStrategy', 'dc1': 3, 'dc2': 2
};
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ę”).
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.
Gossip i wykrywanie awarii
plotka obiega klaster szybciej niż myśliszSkoro 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:
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.
Dzisiejsi goście: Seed Node i Zwykły Węzeł.
Ścieżka zapisu
dlaczego zapisy w Cassandrze są tak absurdalnie szybkieTajemnica 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.
Dzisiejsi goście: Commit Log i Memtable.
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.
- 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.
Tunable consistency
spójność to suwak, nie przełącznikKaż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ść.
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”.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.
Ścieżka odczytu
poszukiwanie wiersza rozrzuconego po wielu plikachZapisy 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:
- Filtr Blooma (
Filter.db, w RAM) — probabilistyczne „na pewno nie ma” albo „może jest”. - Key cache — pamięta pozycje ostatnio czytanych partycji w pliku.
- 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.) - Compression offsets (
CompressionInfo.db) — który skompresowany blok (domyślnie 16 KiB) trzeba rozpakować. - Wreszcie dane (
Data.db).
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:
SELECT * FROM users WHERE id = 42 na replice| źródło | filtr Blooma | indeks / pozycja | znalezione 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.
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.
LSM-tree i kompakcja
sprzątanie po imprezie niezmiennych plikówKaż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”:
- read amplification — ile SSTables trzeba przejrzeć przy odczycie,
- write amplification — ile razy te same bajty są przepisywane na dysk,
- space amplification — ile dodatkowego miejsca zajmują duplikaty i sama kompakcja (STCS potrafi potrzebować chwilowo 2× miejsca!).
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.
Tombstones — nagrobki
jak usunąć coś z niezmiennych plików? postawić nagrobekSkoro 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:
- wygasa TTL (komórka zamienia się w tombstone),
- wstawiasz
nulldo kolumny (tak,INSERT … VALUES (…, null)tworzy tombstone!), - nadpisujesz całą kolekcję (
SET tags = {…}najpierw zapisuje range tombstone).
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:
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).
- 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.
Anti-entropy repair i drzewa Merkle
jak porównać terabajty, przesyłając kilobajtyHinted 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ą.
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.
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ł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
Modelowanie danych: najpierw zapytania
zapomnij o 3NF — projektujesz pod odczytW 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.
((channel_id, bucket), sent_at DESC)
((user_id), channel_name)
((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);
- Klucz partycji
(channel_id, bucket)— to, co idzie do hasha i wybiera węzły. Zapytanie musi podać go w całości (równość). - Kolumny klastrujące
sent_at— sortują wiersze wewnątrz partycji na dysku. Pozwalają na zakresy (sent_at > …) iORDER BY— ale tylko w kolejności ich deklaracji. - Partycja z wieloma wierszami to wide partition („wide row” z czasów Thrift): jeden klucz, tysiące posortowanych wierszy, czytanych sekwencyjnie jednym „plasterkiem”.
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).
ALLOW FILTERINGna 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.
- 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.
Egzamin końcowy i ściąga
sprawdź, czy twój mózg wszystko zapamiętałŚciąga: parametry, które warto znać
| parametr | domyślnie | co robi |
|---|---|---|
| partitioner | Murmur3Partitioner | klucz partycji → token (−2⁶³ … 2⁶³−1) |
| num_tokens | 16 (4.0+), dawniej 256 | liczba vnodes na węzeł |
| phi_convict_threshold | 8 | próg φ, po którym węzeł jest uznany za DOWN |
| max_hint_window | 3 h | jak długo zbierać hinty dla leżącej repliki |
| commitlog_sync | periodic, 10 s | jak często fsync commit logu |
| gc_grace_seconds | 864000 (10 dni) | minimalny czas życia tombstone; repair musi być częstszy |
| tombstone_warn / failure_threshold | 1 000 / 100 000 | ostrzeżenie / przerwanie zapytania skanującego tombstony |
| bloom_filter_fp_chance | 0.01 | odsetek fałszywych „może jest” w filtrze Blooma |
| speculative_retry | 99p | kiedy zapytać dodatkową replikę przy odczycie |
| compaction | STCS (UCS w 5.0 jako alternatywa) | strategia scalania SSTables, per tabela |
| QUORUM | ⌊RF/2⌋ + 1 | R + W > RF ⇒ odczyt widzi ostatni potwierdzony zapis |
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.