Apache Cassandra – big data NoSQL dla aplikacji o wysokiej dostępności

TL;DR: Apache Cassandra to rozproszona baza NoSQL stworzona dla aplikacji wymagających wysokiej dostępności i skalowalności przy dużych wolumenach danych. Dzięki architekturze peer-to-peer i replikacji danych, Cassandra zapewnia ciągłość działania nawet przy awarii części węzłów. Idealna dla aplikacji IoT, systemów analitycznych i platform social media obsługujących miliony użytkowników.

Dlaczego Apache Cassandra jest ważna w erze big data

W 2018 roku przedsiębiorstwa borykają się z eksplozywnym wzrostem danych – od IoT po social media, objętość generowanych informacji rośnie wykładniczo. Tradycyjne relacyjne bazy danych osiągają swoje limity skalowalności, szczególnie przy zapisie milionów rekordów dziennie. Apache Cassandra została zaprojektowana właśnie z myślą o tych wyzwaniach – oferuje linearną skalowalność i wysoką dostępność bez kompromisów.

Co się nauczysz:

  • Czym różni się Cassandra od tradycyjnych baz SQL i innych rozwiązań NoSQL
  • Jak działa architektura peer-to-peer i dlaczego eliminuje single point of failure
  • Kiedy wybrać Cassandrę zamiast MongoDB, Redis czy PostgreSQL
  • Jak modelować dane w Cassandrze według query-first approach
  • Praktyczna implementacja z Java i Spring Boot
  • Jak skonfigurować klaster Cassandry dla środowiska produkcyjnego
  • Monitoring i troubleshooting najpopularniejszych problemów
Wymagania wstępne: Podstawowa znajomość SQL, pojęcie NoSQL, doświadczenie z Java i Spring Boot (min. 2 lata). Pomocna znajomość koncepcji rozproszonych systemów bazodanowych.

Czym jest Apache Cassandra i dlaczego powstała

Apache Cassandra to open-source’owa rozproszona baza danych NoSQL, pierwotnie stworzona przez Facebook w 2008 roku do obsługi inbox search. Obecnie jest używana przez Netflix, Apple, Spotify i setki innych firm do zarządzania petabajtami danych.

Apache Cassandra – rozproszona, wysokoskalowalna baza NoSQL typu wide-column, zaprojektowana dla aplikacji wymagających wysokiej dostępności i obsługi dużych wolumenów danych bez single point of failure.

Kluczowe zalety Cassandry w kontekście big data:

  • Liniowa skalowalność: dodanie nowego węzła zwiększa wydajność proporcjonalnie
  • Wysoka dostępność: brak centralnego punktu awarii dzięki architekturze peer-to-peer
  • Geograficzna replikacja: dane mogą być replikowane między data centers
  • Tunable consistency: możliwość dostosowania poziomu spójności do potrzeb aplikacji
  • Optimized for writes: doskonała wydajność zapisu, kluczowa dla aplikacji big data
Analogia: Jeśli tradycyjna baza SQL to centralna biblioteka z jednym bibliotekarzem, to Cassandra to sieć bibliotek partnerskich – gdy jedna jest zamknięta, pozostałe nadal obsługują czytelników, a każda nowa biblioteka zwiększa całkowitą pojemność systemu.

Architektura Cassandry – peer-to-peer bez master węzłów

Jedną z największych innowacji Cassandry jest całkowite odejście od architektury master-slave znanej z MySQL czy MongoDB. Wszystkie węzły w klastrze Cassandry są równorzędne.

Consistent Hashing i Token Ring

Cassandra wykorzystuje consistent hashing do dystrybucji danych. Każdy węzeł odpowiada za określony zakres tokenów w pierścieniu, co zapewnia równomierne rozłożenie danych:

// Przykład: jak Cassandra determinuje lokalizację danych
public class TokenExample {
    // Partition key: user_id = "12345"
    // Hash MD5: 5994471abb01112afcc18159f6cc74b4
    // Token: 118059168096963894540361804555486654644
    // Węzeł odpowiedzialny: node-3 (token range: 113...142...)
    
    String partitionKey = "user_id:12345";
    long token = consistentHash(partitionKey);
    Node responsibleNode = tokenRing.findNode(token);
}

Replikacja i Fault Tolerance

Cassandra automatycznie replikuje dane na wiele węzłów zgodnie z konfiguracją replication factor. Domyślny RF=3 oznacza, że każdy rekord jest przechowywany na trzech różnych węzłach.

Pro tip: W środowisku produkcyjnym zawsze ustaw replication factor na nieparzystą liczbę (3, 5, 7) aby uniknąć split-brain scenarios podczas partycjonowania sieci.

Kiedy wybrać Cassandrę – przypadki użycia w 2018

Cassandra nie jest rozwiązaniem uniwersalnym. Sprawdza się idealnie w określonych scenariuszach big data:

Idealne przypadki użycia Cassandry:

  • Time-series data: logi aplikacji, metryki systemowe, dane sensorowe IoT
  • Content management: posty na social media, komentarze, notyfikacje
  • Recommendation engines: profiling użytkowników, historia aktywności
  • Real-time analytics: tracking eventów, fraud detection
  • Messaging systems: historia wiadomości, inbox management
Uwaga: Cassandra NIE jest dobrym wyborem dla aplikacji wymagających complex joins, transactions ACID, ad-hoc queries czy częstych updates/deletes. W takich przypadkach rozważ PostgreSQL lub MongoDB.

Porównanie z innymi bazami NoSQL (stan na 2018)

BazaNajlepsze zastosowanieSkalowalnośćConsistency
CassandraBig data, time-seriesLinearnaEventual
MongoDBRapid development, JSON docsShardingStrong
RedisCaching, real-time appsClusteringStrong
HBaseRandom read/write, Hadoop ecosystemRegionserverStrong

Modelowanie danych w Cassandrze – query-first approach

Największa różnica między SQL a Cassandrą to podejście do modelowania danych. W SQL normalizujemy dane i piszemy queries. W Cassandrze najpierw określamy queries, potem modelujemy tabele.

Typowy błąd: Próba przeniesienia relacyjnego modelu danych 1:1 do Cassandry. To prowadzi do nieefektywnych queries i problemów z wydajnością.

Praktyczny przykład – system blogowy

Załóżmy, że budujemy system blogowy obsługujący miliony postów dziennie. Kluczowe queries:

-- Query 1: Najnowsze posty użytkownika
SELECT * FROM posts WHERE user_id = ? ORDER BY created_at DESC LIMIT 10;

-- Query 2: Posty z określonej kategorii
SELECT * FROM posts WHERE category = ? ORDER BY created_at DESC LIMIT 20;

-- Query 3: Post details
SELECT * FROM posts WHERE post_id = ?;

Na podstawie tych queries projektujemy tabele w Cassandrze:

-- Tabela dla Query 1: posty użytkownika
CREATE TABLE posts_by_user (
    user_id UUID,
    created_at TIMESTAMP,
    post_id UUID,
    title TEXT,
    content TEXT,
    category TEXT,
    PRIMARY KEY (user_id, created_at, post_id)
) WITH CLUSTERING ORDER BY (created_at DESC);

-- Tabela dla Query 2: posty z kategorii  
CREATE TABLE posts_by_category (
    category TEXT,
    created_at TIMESTAMP,
    post_id UUID,
    user_id UUID,
    title TEXT,
    content TEXT,
    PRIMARY KEY (category, created_at, post_id)
) WITH CLUSTERING ORDER BY (created_at DESC);

-- Tabela dla Query 3: detale postu
CREATE TABLE posts_by_id (
    post_id UUID PRIMARY KEY,
    user_id UUID,
    title TEXT,
    content TEXT,
    category TEXT,
    created_at TIMESTAMP,
    tags SET
);
Partition Key – determinuje na którym węźle są przechowywane dane. Clustering Key – określa porządek sortowania danych w partycji.

Implementacja z Java i Spring Boot

W 2018 roku najstabilniejszą opcją integracji Cassandry z Spring Boot jest wykorzystanie Spring Data Cassandra w wersji 2.0.

Konfiguracja projektu



    
        org.springframework.boot
        spring-boot-starter-data-cassandra
        2.0.5.RELEASE
    
    
        com.datastax.cassandra
        cassandra-driver-core
        3.6.0
    

# application.yml
spring:
  data:
    cassandra:
      contact-points: 
        - 127.0.0.1
        - 127.0.0.2
        - 127.0.0.3
      port: 9042
      keyspace-name: blog_system
      local-datacenter: datacenter1
      username: cassandra
      password: cassandra
      consistency-level: LOCAL_QUORUM
      serial-consistency-level: LOCAL_SERIAL

Model danych i Repository

@Table("posts_by_user")
public class PostByUser {
    
    @PrimaryKeyColumn(name = "user_id", type = PrimaryKeyType.PARTITIONED)
    private UUID userId;
    
    @PrimaryKeyColumn(name = "created_at", type = PrimaryKeyType.CLUSTERED, 
                     ordering = Ordering.DESCENDING)
    private LocalDateTime createdAt;
    
    @PrimaryKeyColumn(name = "post_id", type = PrimaryKeyType.CLUSTERED)
    private UUID postId;
    
    @Column("title")
    private String title;
    
    @Column("content")
    private String content;
    
    @Column("category")
    private String category;
    
    // konstruktory, gettery, settery
}

@Repository
public interface PostByUserRepository extends CassandraRepository {
    
    @Query("SELECT * FROM posts_by_user WHERE user_id = ?0 LIMIT ?1")
    List findRecentPostsByUser(UUID userId, int limit);
    
    @Query("SELECT * FROM posts_by_user WHERE user_id = ?0 AND created_at >= ?1")
    List findPostsByUserSince(UUID userId, LocalDateTime since);
}

Service layer z obsługą błędów

@Service
@Slf4j
public class BlogService {
    
    private final PostByUserRepository postByUserRepository;
    private final PostByCategoryRepository postByCategoryRepository;
    private final PostByIdRepository postByIdRepository;
    
    @Retryable(value = {DriverException.class}, maxAttempts = 3)
    public void createPost(CreatePostRequest request) {
        UUID postId = UUID.randomUUID();
        LocalDateTime now = LocalDateTime.now();
        
        try {
            // Zapisujemy do wszystkich trzech tabel - denormalizacja
            PostByUser postByUser = new PostByUser(
                request.getUserId(), now, postId, 
                request.getTitle(), request.getContent(), request.getCategory()
            );
            postByUserRepository.save(postByUser);
            
            PostByCategory postByCategory = new PostByCategory(
                request.getCategory(), now, postId, request.getUserId(),
                request.getTitle(), request.getContent()
            );
            postByCategoryRepository.save(postByCategory);
            
            PostById postById = new PostById(
                postId, request.getUserId(), request.getTitle(),
                request.getContent(), request.getCategory(), now
            );
            postByIdRepository.save(postById);
            
            log.info("Post created successfully: {}", postId);
            
        } catch (DriverException e) {
            log.error("Failed to create post: {}", e.getMessage());
            throw new BlogServiceException("Unable to create post", e);
        }
    }
    
    public List getUserRecentPosts(UUID userId, int limit) {
        return postByUserRepository.findRecentPostsByUser(userId, limit);
    }
}
Pro tip: W Cassandrze często zapisujemy te same dane w wielu tabelach (denormalizacja). To jest normalne i pożądane – storage jest tani, queries muszą być szybkie.

Konfiguracja klastra produkcyjnego

Przejście z single-node development do klastra produkcyjnego wymaga przemyślenia kilku kluczowych aspektów.

Topology i replication strategy

-- Keyspace z replikacją multi-datacenter
CREATE KEYSPACE blog_system 
WITH REPLICATION = {
    'class': 'NetworkTopologyStrategy',
    'DC1': 3,  -- 3 repliki w głównym DC
    'DC2': 2   -- 2 repliki w backup DC
};

-- Konfiguracja dla single datacenter
CREATE KEYSPACE blog_system_dev
WITH REPLICATION = {
    'class': 'SimpleStrategy',
    'replication_factor': 3
};

Tuning parametrów JVM i Cassandry

# cassandra-env.sh - optymalizacja dla maszyn 32GB RAM
MAX_HEAP_SIZE="8G"
HEAP_NEWSIZE="2G"

# Dodatkowe parametry JVM dla produkcji
JVM_OPTS="$JVM_OPTS -XX:+UseG1GC"
JVM_OPTS="$JVM_OPTS -XX:MaxGCPauseMillis=200"
JVM_OPTS="$JVM_OPTS -XX:+UnlockExperimentalVMOptions"
JVM_OPTS="$JVM_OPTS -XX:+UseCGroupMemoryLimitForHeap"

# cassandra.yaml - kluczowe ustawienia
concurrent_reads: 32
concurrent_writes: 32  
memtable_allocation_type: heap_buffers
commitlog_sync: periodic
commitlog_sync_period_in_ms: 10000
Uwaga: Nigdy nie ustawiaj heap size powyżej 50% RAM dostępnego na maszynie. Cassandra potrzebuje dużo RAM dla OS cache’u i off-heap structures.

Monitoring i troubleshooting

W środowisku produkcyjnym monitoring Cassandry jest kluczowy dla wczesnego wykrywania problemów.

Kluczowe metryki do monitorowania

  • Read/Write latency: P95, P99 czasów odpowiedzi
  • Pending tasks: backlog w thread pools
  • Compaction: liczba pending compactions
  • GC performance: czas i częstotliwość garbage collection
  • Disk space: wykorzystanie per-node i per-keyspace
  • Network: dropped messages między węzłami
# Przydatne komendy nodetool dla diagnostyki
nodetool status                    # Status wszystkich węzłów
nodetool tpstats                   # Thread pool statistics  
nodetool compactionstats           # Status kompakcji
nodetool cfstats blog_system       # Column family statistics
nodetool netstats                  # Network statistics
nodetool proxyhistograms           # Latency histograms

# Monitoring z JMX
nodetool sjk ttop -o PID,STATE,CPU,TIME,NAME | head -20

Najczęstsze problemy i rozwiązania

Pułapka: Large partitions – partycje powyżej 100MB powodują degradację wydajności. Używaj nodetool cfstats do wykrywania dużych partycji i przeprojektuj model danych.
Pułapka: Tombstones accumulation – częste usuwanie bez proper TTL prowadzi do wzrostu ilości tombstones. Ustaw domyślne TTL dla tabel lub używaj periodic batch cleanup.

Performance i optymalizacja dla big data

Przy obsłudze rzeczywistych obciążeń big data, kilka praktyk może znacząco wpłynąć na wydajność:

Batch operations

// Efektywne batch processing
@Service
public class BatchInsertService {
    
    private final CassandraTemplate cassandraTemplate;
    private final int BATCH_SIZE = 100;
    
    public void processBulkInsert(List posts) {
        List> batches = Lists.partition(posts, BATCH_SIZE);
        
        for (List batch : batches) {
            BatchStatement batchStatement = new BatchStatement(BatchStatement.Type.UNLOGGED);
            
            for (PostByUser post : batch) {
                Statement insert = cassandraTemplate.createInsertQuery("posts_by_user", post);
                batchStatement.add(insert);
            }
            
            cassandraTemplate.execute(batchStatement);
        }
    }
}
Pro tip: Używaj UNLOGGED batches dla bulk operations na różnych partycjach. LOGGED batches tylko gdy potrzebujesz atomicity w obrębie jednej partycji.

Async queries dla wysokiej przepustowości

@Service
public class AsyncBlogService {
    
    private final CassandraTemplate cassandraTemplate;
    
    public CompletableFuture> getUserPostsAsync(UUID userId) {
        String query = "SELECT * FROM posts_by_user WHERE user_id = ? LIMIT 50";
        
        return cassandraTemplate.selectAsync(query, PostByUser.class, userId)
                .toCompletableFuture()
                .exceptionally(throwable -> {
                    log.error("Failed to fetch user posts: {}", throwable.getMessage());
                    return Collections.emptyList();
                });
    }
    
    // Paralelne zapytania do wielu tabel
    public CompletableFuture getUserDashboard(UUID userId) {
        CompletableFuture> posts = getUserPostsAsync(userId);
        CompletableFuture> comments = getUserCommentsAsync(userId);
        CompletableFuture stats = getUserStatsAsync(userId);
        
        return CompletableFuture.allOf(posts, comments, stats)
                .thenApply(v -> new UserDashboard(
                    posts.join(), comments.join(), stats.join()
                ));
    }
}

Czy Cassandra może zastąpić PostgreSQL w każdej aplikacji?

Nie. Cassandra jest świetna dla aplikacji wymagających wysokiej skalowalności i dostępności, ale brakuje jej funkcjonalności SQL jak JOINs, transactions czy ad-hoc queries. PostgreSQL nadal będzie lepszym wyborem dla aplikacji biznesowych z kompleksowymi relacjami między danymi.

Jak radzić sobie z eventual consistency w aplikacjach biznesowych?

Użyj consistency level QUORUM lub ALL dla krytycznych operacji. Dla mniej krytycznych danych eventual consistency jest akceptowalna – przykładowo liczba polubień posta nie musi być dokładna w czasie rzeczywistym. Projektuj aplikację z myślą o tym, że dane mogą być chwilowo niespójne.

Ile węzłów potrzebuję w klastrze Cassandry?

Minimum 3 węzły dla environment produkcyjnego z RF=3. Dla większych obciążeń rozważ 6-12 węzłów w jednym datacenter. Pamiętaj że dodanie węzłów zwiększa wydajność liniowo – to jedna z największych zalet Cassandry.

Czy można migrować dane z MySQL do Cassandry?

Tak, ale wymaga to przeprojektowania modelu danych. Nie da się przenieść relacyjnych struktur 1:1. Zacznij od zidentyfikowania queries które wykonuje aplikacja, potem zaprojektuj tabele Cassandry pod te queries. Używaj narzędzi jak Spark do bulk migration.

Jak często robić backup klastra Cassandry?

Dla danych krytycznych – snapshot codziennie na każdym węźle. Cassandra oferuje incremental backups i point-in-time recovery. Testuj restore procedures regularnie. Pamiętaj że przy RF=3 dane są już replikowane, ale backup chroni przed błędami aplikacji lub corruption.

Czy Cassandra nadaje się dla aplikacji real-time?

Tak, szczególnie dla write-heavy applications. Latencje poniżej 1ms są możliwe przy odpowiedniej konfiguracji. Dla aplikacji wymagających sub-millisecond response times rozważ kombinację Cassandry z Redis jako cache layer.

Jak monitorować koszty storage w Cassandrze?

Używaj nodetool tablehistograms i cfstats do analizy wykorzystania przestrzeni per-table. Cassandra automatycznie kompresuje dane – LZ4 lub Snappy dają 50-70% compression ratio. Ustaw TTL dla tymczasowych danych aby automatycznie je usuwać.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Projekt: Stwórz system logowania wydarzeń (event logging) dla aplikacji e-commerce obsługującej 10,000 transakcji dziennie.

Wymagania:

  • Zaprojektuj model danych dla queries: „logi z ostatnich 7 dni dla użytkownika”, „wszystkie błędy z dzisiaj”, „top 10 najczęstszych eventów”
  • Zaimplementuj w Spring Boot z async insertami
  • Dodaj TTL=30 dni dla automatycznego usuwania starych logów
  • Skonfiguruj lokalny klaster 3-node w Docker
  • Napisz test obciążeniowy symulujący 1000 eventów/minutę

Bonus: Dodaj monitoring z JMX i stwórz dashboard pokazujący write latency i throughput.

Czy masz doświadczenie z bazami NoSQL? Jakie największe wyzwania widzisz przy migracji z systemów relacyjnych do Cassandry? Podziel się swoimi przemyśleniami w komentarzach!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry