Что такое Cassandra
Apache Cassandra -- распределённая NoSQL база данных, спроектированная для работы с огромными объёмами данных на множестве серверов без единой точки отказа.
Ключевые свойства
| Свойство | Описание |
|---|---|
| Masterless | Нет мастера, все узлы равны |
| Линейная масштабируемость | 2x узлов = 2x пропускной способности |
| Высокая доступность | Работает при потере нескольких узлов |
| Write-optimized | Очень быстрая запись (LSM-tree) |
| Tunable consistency | Настраиваемая консистентность |
| Multi-datacenter | Встроенная поддержка нескольких ЦОД |
Архитектура
Ring Topology
Node A
(Token: 0-24)
╱ ╲
Node F Node B
(Token: 75-99) (Token: 25-49)
╲ ╱
Node E
(Token: 50-74)
Data for key "user_123":
hash("user_123") = 37 → Node B (primary)
Replicas: Node E, Node A (RF=3)
Каждый узел отвечает за диапазон токенов. Данные распределяются по кольцу на основе хеша partition key.
Gossip Protocol
Узлы обмениваются информацией о состоянии кластера через gossip:
- Каждую секунду узел отправляет gossip сообщение случайному узлу
- Сообщение содержит информацию обо всех известных узлах
- Через несколько раундов все узлы знают обо всех
- Обнаружение отказов через отсутствие heartbeat
Преимущество: нет центрального координатора. Кластер самоорганизуется и восстанавливается автоматически.
Модель данных
Partition Key и Clustering Key
PRIMARY KEY ((partition_key), clustering_key1, clustering_key2)
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Определяет узел Определяет порядок внутри партиции
<?php
declare(strict_types=1);
/**
* Cassandra data model design patterns
*/
final class CassandraSchemaDesign
{
/**
* Table design follows query patterns, NOT entity relationships.
*
* Rule: One table per query pattern
* Anti-pattern: Designing like relational DB with JOINs
*/
/**
* Query 1: Get all orders for a user, sorted by date
*/
public function ordersTableDesign(): string
{
return <<<CQL
CREATE TABLE orders_by_user (
user_id UUID,
order_date TIMESTAMP,
order_id UUID,
total_amount DECIMAL,
status TEXT,
items LIST<FROZEN<order_item>>,
PRIMARY KEY ((user_id), order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC, order_id ASC);
CREATE TYPE order_item (
product_id UUID,
name TEXT,
price DECIMAL,
quantity INT
);
CQL;
}
/**
* Query 2: Get order by ID
* Different table for different query!
*/
public function orderByIdDesign(): string
{
return <<<CQL
CREATE TABLE orders_by_id (
order_id UUID PRIMARY KEY,
user_id UUID,
order_date TIMESTAMP,
total_amount DECIMAL,
status TEXT,
items LIST<FROZEN<order_item>>
);
CQL;
}
/**
* Query 3: Get orders by status within a date range
*/
public function ordersByStatusDesign(): string
{
return <<<CQL
CREATE TABLE orders_by_status (
status TEXT,
order_date TIMESTAMP,
order_id UUID,
user_id UUID,
total_amount DECIMAL,
PRIMARY KEY ((status), order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC);
CQL;
}
}
Правила моделирования
| Правило | Описание |
|---|---|
| Denormalize | Дублировать данные для каждого паттерна запросов |
| No JOINs | Все данные для запроса -- в одной таблице |
| Partition size | Держать партиции < 100 MB, < 100K строк |
| Know your queries | Проектировать таблицы от запросов, не от данных |
| Avoid ALLOW FILTERING | Медленно, сканирует весь кластер |
Consistency Levels
Cassandra позволяет настраивать консистентность для каждого запроса:
| Level | Write | Read | Описание |
|---|---|---|---|
| ONE | 1 узел | 1 узел | Минимальная задержка |
| QUORUM | N/2+1 | N/2+1 | Баланс |
| LOCAL_QUORUM | N/2+1 в локальном DC | N/2+1 в локальном DC | Для multi-DC |
| ALL | Все реплики | Все реплики | Максимальная консистентность |
| EACH_QUORUM | Quorum в каждом DC | -- | Для multi-DC writes |
Strong Consistency формула
R + W > N → Strong Consistency
R = read consistency level
W = write consistency level
N = replication factor
QUORUM + QUORUM = (3/2+1) + (3/2+1) = 4 > 3 ✓ (strong)
ONE + ONE = 1 + 1 = 2 < 3 ✗ (eventual)
PHP + Cassandra
<?php
declare(strict_types=1);
/**
* Cassandra operations from PHP using DataStax driver
*/
final class CassandraOrderRepository
{
private \Cassandra\Session $session;
public function __construct(string $contactPoints, string $keyspace)
{
$cluster = \Cassandra::cluster()
->withContactPoints($contactPoints)
->withDefaultConsistency(\Cassandra::CONSISTENCY_LOCAL_QUORUM)
->withDefaultPageSize(100)
->build();
$this->session = $cluster->connect($keyspace);
}
/**
* Insert order into multiple tables (denormalized)
*/
public function createOrder(array $orderData): void
{
$batch = new \Cassandra\BatchStatement(\Cassandra::BATCH_LOGGED);
// Table 1: orders_by_user
$stmt1 = $this->session->prepare(<<<CQL
INSERT INTO orders_by_user (user_id, order_date, order_id, total_amount, status)
VALUES (?, ?, ?, ?, ?)
CQL);
$batch->add($stmt1, [
'user_id' => new \Cassandra\Uuid($orderData['user_id']),
'order_date' => new \Cassandra\Timestamp(time()),
'order_id' => new \Cassandra\Uuid($orderData['order_id']),
'total_amount' => new \Cassandra\Decimal((string) $orderData['total']),
'status' => 'pending',
]);
// Table 2: orders_by_id
$stmt2 = $this->session->prepare(<<<CQL
INSERT INTO orders_by_id (order_id, user_id, order_date, total_amount, status)
VALUES (?, ?, ?, ?, ?)
CQL);
$batch->add($stmt2, [
'order_id' => new \Cassandra\Uuid($orderData['order_id']),
'user_id' => new \Cassandra\Uuid($orderData['user_id']),
'order_date' => new \Cassandra\Timestamp(time()),
'total_amount' => new \Cassandra\Decimal((string) $orderData['total']),
'status' => 'pending',
]);
$this->session->execute($batch);
}
/**
* Pagination using token-based approach
*/
public function getUserOrders(string $userId, ?string $pagingState = null): array
{
$stmt = $this->session->prepare(<<<CQL
SELECT order_date, order_id, total_amount, status
FROM orders_by_user
WHERE user_id = ?
ORDER BY order_date DESC
CQL);
$options = ['page_size' => 20];
if ($pagingState !== null) {
$options['paging_state_token'] = $pagingState;
}
$result = $this->session->execute($stmt, [
'arguments' => [new \Cassandra\Uuid($userId)],
...$options,
]);
return [
'orders' => iterator_to_array($result),
'next_page' => $result->pagingStateToken(),
'has_more' => $result->isLastPage() === false,
];
}
/**
* Lightweight transaction (compare-and-set)
*/
public function updateStatusIfPending(string $orderId, string $newStatus): bool
{
$stmt = $this->session->prepare(<<<CQL
UPDATE orders_by_id
SET status = ?
WHERE order_id = ?
IF status = 'pending'
CQL);
$result = $this->session->execute($stmt, [
'arguments' => [$newStatus, new \Cassandra\Uuid($orderId)],
'consistency' => \Cassandra::CONSISTENCY_SERIAL,
]);
$row = $result->first();
return $row['[applied]'] ?? false;
}
}
Когда использовать Cassandra
Подходит
- Запись > 100K ops/sec (IoT, логи, events)
- Multi-datacenter с автоматической репликацией
- Данные с известными паттернами запросов
- Time-series данные (с правильным моделированием)
- Высокая доступность важнее консистентности
Не подходит
- Ad-hoc запросы и аналитика (нет JOIN, нет агрегаций)
- Транзакции между несколькими партициями
- Малый объём данных (< 100 GB) -- overkill
- Часто меняющиеся паттерны запросов
- Команда без опыта распределённых систем
Итоги
- Cassandra -- masterless, линейно масштабируемая, write-optimized БД
- Gossip protocol обеспечивает самоорганизацию кластера без координатора
- Модель данных проектируется от запросов, а не от данных (денормализация)
- Consistency levels позволяют балансировать между C и A по запросу
- Идеальна для write-heavy workloads и multi-DC deployments
- Требует опытной команды для правильного моделирования и эксплуатации