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
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.
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
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.
Kiedy wybrać Cassandrę – przypadki użycia w 2018
Cassandra nie jest rozwiązaniem uniwersalnym. Sprawdza się idealnie w określonych scenariuszach big data:
- 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
Porównanie z innymi bazami NoSQL (stan na 2018)
Baza | Najlepsze zastosowanie | Skalowalność | Consistency |
---|---|---|---|
Cassandra | Big data, time-series | Linearna | Eventual |
MongoDB | Rapid development, JSON docs | Sharding | Strong |
Redis | Caching, real-time apps | Clustering | Strong |
HBase | Random read/write, Hadoop ecosystem | Regionserver | Strong |
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.
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);
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 ListgetUserRecentPosts(UUID userId, int limit) { return postByUserRepository.findRecentPostsByUser(userId, limit); } }
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
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
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(Listposts) { 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); } } }
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() )); } }
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.
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.
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.
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.
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.
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.
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:
- Oficjalna dokumentacja Apache Cassandra
- DataStax Java Driver 3.6 Documentation
- Spring Data Cassandra Reference
- Nodetool Command Reference
- The Last Pickle – Cassandra Best Practices
🚀 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!