
























Apache Cassandra is a distributed database designed for high availability and horizontal scalability. This write-up explores the complete write path in Cassandra, from the moment a client sends a write request to how data gets replicated across nodes in the cluster.
When a client writes data to Cassandra, the data flows through multiple stages before being safely stored. The write path involves several key components working together to ensure durability, consistency, and performance. Let’s break this down…
Every write request in Cassandra starts with a client connecting to any node in the cluster. The node that receives the client request becomes the coordinator for that operation. This is an important concept: there is no “master” node in Cassandra—any node can coordinate any request.
The coordinator’s role is to:
The coordinator doesn’t necessarily store the data itself, though it might be one of the replica nodes depending on the partition key and replication strategy.
Before the coordinator can forward writes, it needs to determine which nodes should store the data. This decision is based on three key factors.
Every piece of data in Cassandra has a partition key. This key is hashed using a partitioner (typically Murmur3Partitioner) to produce a token—a 64-bit integer that determines where the data lives in the cluster.
Partition Key → Hash Function → Token → Node(s)
For example, if you have a table storing user data:
CREATE TABLE users (
user_id uuid PRIMARY KEY,
name text,
email text
);
When you insert a user with user_id = '550e8400-e29b-41d4-a716-446655440000', Cassandra hashes this UUID to produce a token. This token falls within a range owned by specific nodes in the cluster.
Cassandra organizes nodes in a logical token ring. The entire token space (from -2^63 to 2^63-1 for Murmur3) is divided among the nodes in the cluster. Each node is assigned a token range and is responsible for storing data whose tokens fall within that range.
When you add or remove nodes, the token ranges are redistributed, but only a fraction of the data needs to move—this is the beauty of consistent hashing.
The replication strategy determines how many copies of the data exist and where they’re placed. There are two main strategies:
SimpleStrategy: Used for single data center deployments. It places replicas on consecutive nodes in the ring. For example, with a replication factor of 3, the data is stored on the first node determined by the token, plus the next two nodes clockwise in the ring.
NetworkTopologyStrategy: Used for multi-data center deployments. It allows you to specify how many replicas should exist in each data center. For example:
CREATE KEYSPACE my_keyspace WITH replication = {
'class': 'NetworkTopologyStrategy',
'datacenter1': 3,
'datacenter2': 2
};
This creates 3 replicas in datacenter1 and 2 in datacenter2. Within each data center, replicas are placed on different racks when possible to maximize availability.
Once the coordinator determines which nodes should receive the write, it forwards the mutation to those nodes. Now let’s see what happens inside each replica node when it receives a write request.
The very first thing that happens when a node receives a write is that it appends the mutation to the CommitLog. This is a critical step for durability.
The CommitLog is an append-only log file on disk. It’s structured as a sequential write, which is extremely fast—modern SSDs can handle hundreds of thousands of sequential writes per second. The CommitLog entry contains:
By writing to the CommitLog first, Cassandra ensures that even if the node crashes immediately after, the write can be recovered when the node restarts. This is the Write-Ahead Log (WAL) pattern.
CommitLog configuration considerations:
# In cassandra.yaml
commitlog_sync: batch
commitlog_sync_batch_window_in_ms: 2
commitlog_segment_size_in_mb: 32
commitlog_compression:
- class_name: LZ4Compressor
The commitlog_sync parameter has two modes:
Most production systems use batch mode with a 2ms window, providing a good balance between durability and performance.
Immediately after writing to the CommitLog, the mutation is written to an in-memory structure called a MemTable. Each table in Cassandra has its own MemTable.
The MemTable is essentially a sorted map structure (similar to a Red-Black tree or Skip List) that keeps data sorted by partition key and clustering columns. This sorting is crucial for efficient reads and for the later flush to disk.
Multiple writes to the same partition key will update the MemTable in place. However, Cassandra doesn’t actually update data—it writes new timestamped versions. When you “update” a column, you’re really adding a new entry with a newer timestamp. The reconciliation happens at read time.
Example of MemTable organization:
Partition Key: user_123
├─ Clustering: timestamp=2025-01-15T10:00:00, value="xxxx"
├─ Clustering: timestamp=2025-01-15T10:05:00, value="xxxxxxxx"
└─ Clustering: timestamp=2025-01-15T10:10:00, value="xxxxxx"
Partition Key: user_456
├─ Clustering: timestamp=2025-01-15T10:02:00, value="xxxxxxxxxx"
└─ Clustering: timestamp=2025-01-15T10:12:00, value="xxxxxxxxx"
Once the write is in the CommitLog and MemTable, the node sends an acknowledgment back to the coordinator. This happens very quickly, typically in microseconds, because it only involves an append to the CommitLog and an in-memory update.
At this point, the write is considered successful from the replica node’s perspective, but the coordinator hasn’t responded to the client yet. That depends on the consistency level.
The MemTable is memory-bound, so it can’t grow indefinitely. When a MemTable reaches a certain size threshold or after a certain time period, it’s flushed to disk as an SSTable (Sorted String Table).
The flush process:
SSTables are immutable once written. This immutability provides several benefits:
However, immutability means we accumulate multiple SSTables over time, which is why compaction is necessary.
The consistency level determines how many replica nodes must acknowledge a write before the coordinator responds to the client. This is configurable per query and represents a fundamental trade-off between consistency, availability, and latency.
The coordinator waits for acknowledgment from just one replica node. This provides the lowest latency and highest availability - the write succeeds as long as any single replica node is reachable. However, you might read stale data if you subsequently read from a node that hasn’t received the write yet.
The coordinator waits for acknowledgments from a majority of replica nodes. For a replication factor of 3, QUORUM means 2 nodes must acknowledge. This is the most commonly used level in production because it provides a good balance:
Similar to QUORUM, but only counts replicas in the local data center. This is crucial for multi-datacenter deployments because you don’t want to wait for cross-datacenter network latency on every write.
The coordinator waits for all replica nodes to acknowledge. This provides the strongest consistency but has the worst availability; if even one replica is down, writes fail. Generally not recommended for production.
This is an interesting case. The write is considered successful if it can be written to at least one node, or if it can be stored as a hint for a temporarily unavailable node. More on hints below.
Consider a cluster with a replication factor of 3, and you issue a write with consistency level QUORUM:
# Using the Python driver
from cassandra.cluster import Cluster
from cassandra import ConsistencyLevel
cluster = Cluster(['10.0.0.1'])
session = cluster.connect('my_keyspace')
# Prepare a statement with QUORUM consistency
statement = session.prepare("""
INSERT INTO users (user_id, name, email)
VALUES (?, ?, ?)
""")
statement.consistency_level = ConsistencyLevel.QUORUM
# Execute the write
session.execute(statement, (user_id, "Alice", "alice@example.com"))
Timeline:
The entire operation typically completes in 2-5ms, depending on network latency and load.
Now let’s dive deeper into how data actually gets replicated across nodes. The replication happens synchronously as part of the write operation; there’s no separate background replication process for normal writes.
When the coordinator receives a write, it immediately forwards the mutation to all replica nodes (as determined by the replication strategy and replication factor). These writes happen in parallel. The coordinator just waits as per the consistency level, but it still writes to all the replicas synchronously.
Here’s what actually gets sent over the network:
Mutation Message:
- Keyspace: "my_keyspace"
- Table: "users"
- Partition Key: user_id = UUID('...')
- Timestamp: 1705320000000000 (microseconds since epoch)
- Columns:
- name: "Alice"
- email: "alice@example.com"
- TTL: null
Each replica node processes this mutation independently through its own CommitLog → MemTable path.
Cassandra uses last-write-wins (LWW) conflict resolution based on timestamps. Every mutation includes a timestamp (in microseconds), and when multiple versions of the same data exist, the one with the highest timestamp wins.
Timestamps can come from two sources:
INSERT INTO users (user_id, name) VALUES (?, ?)
USING TIMESTAMP 1705320000000000;
If your cluster’s clocks are not synchronized (using NTP), you can get unexpected results. A write with timestamp 1000 can be overwritten by a later write with timestamp 999 if it arrives from a node with a clock that’s behind.
Hence, always use NTP to keep clocks synchronized across your cluster, with clock drift kept under 500ms.
In a distributed system, network partitions are inevitable. Cassandra handles these gracefully through its consistency model and hinted handoff mechanism.
Suppose we have RF=3 (replication factor), CL=QUORUM, and Node C goes down:
A hint is a special write that says, “when Node C comes back online, replay this mutation to it.” Hints are stored on the coordinator (or other available nodes) in a system table.
Hint structure:
- Target node: C
- Mutation: INSERT INTO users (user_id, name) VALUES (...)
- Expiration: 3 hours (configurable via max_hint_window_in_ms)
When Node C comes back online, other nodes detect it and start replaying hints to it. This brings Node C up to date with the writes it missed while down.
Important limitations of hints:
max_hint_window_in_ms (default 3 hours), hints expireEven with a hinted handoff, replicas can get out of sync. Cassandra uses read repair to detect and fix inconsistencies during read operations.
When you read with a consistency level higher than ONE, the coordinator sends read requests to multiple replicas and compares their responses. If it finds differences, it:
This happens transparently and helps maintain eventual consistency. We can also enable read_repair_chance for additional background repair:
ALTER TABLE users WITH read_repair_chance = 0.1;
This tells Cassandra to perform read repair on 10% of reads, even if the consistency level is ONE. However, this feature is often disabled in modern Cassandra (3.0+) in favor of explicit repair operations.
Cassandra is write-optimized. A single node can handle 10,000-50,000 writes per second, depending on:
The write path is designed for speed:
Typical write latencies (p99):
These are remarkably consistent because writes don’t involve disk reads—only sequential appends to the CommitLog.
Higher consistency levels increase latency because the coordinator must wait for more acknowledgments.
Cassandra supports batches, but they’re not always a performance win:
BEGIN BATCH
INSERT INTO users (user_id, name) VALUES (uuid1, 'Alice');
INSERT INTO users (user_id, name) VALUES (uuid2, 'Bob');
INSERT INTO users (user_id, name) VALUES (uuid3, 'Charlie');
APPLY BATCH;
Logged batches (default) use a batch log for atomicity, which adds overhead. They’re useful for ensuring multiple writes succeed or fail together, but they’re slower than individual writes.
Unlogged batches skip the batch log and are faster, but they don’t guarantee atomicity. They’re only useful for performance when writing to the same partition:
BEGIN UNLOGGED BATCH
INSERT INTO user_events (user_id, timestamp, event) VALUES (uuid1, t1, 'login');
INSERT INTO user_events (user_id, timestamp, event) VALUES (uuid1, t2, 'purchase');
INSERT INTO user_events (user_id, timestamp, event) VALUES (uuid1, t3, 'logout');
APPLY BATCH;
Writing to the same partition repeatedly can create “hot spots.” Cassandra performs best when writes are distributed across many partitions. If you have a counter table that updates the same partition millions of times, you’ll experience performance degradation.
As SSTables accumulate, read performance degrades (you have to check more files) and disk space increases (duplicate and deleted data). Compaction solves this by merging SSTables and removing deleted or overwritten data.
Compaction runs in the background but competes for I/O resources. Heavy compaction can temporarily impact write performance. You can tune compaction with:
# In cassandra.yaml
compaction_throughput_mb_per_sec: 16 # Limit compaction I/O
concurrent_compactors: 2 # Number of parallel compactions
Cassandra deployments often span multiple datacenters for disaster recovery and geographical distribution. This is how replication works across datacenters.
CREATE KEYSPACE global_app WITH replication = {
'class': 'NetworkTopologyStrategy',
'us-east': 3,
'us-west': 3,
'eu-west': 2
};
This creates:
When a write arrives at a coordinator in us-east with consistency level LOCAL_QUORUM:
Cross-datacenter replication is still synchronous (the write is sent immediately), but LOCAL_QUORUM doesn’t wait for remote datacenters. This provides consistency within a datacenter and eventual consistency across datacenters.
LOCAL_QUORUM: Most common for production
EACH_QUORUM: Stronger consistency
Cluster with nodes A, B, C, D. Node C fails. RF=3, so data has replicas on A, B, and C.
Write behavior:
No impact on write availability. Writes continues successfully.
Same cluster, nodes B and C both fail.
Write behavior:
Write availability is lost for data replicated to B and C. However, data replicated to other sets of nodes (e.g., A, D, E) continues working.
Cluster split into two groups: {A, B} and {C, D}. RF=3, CL=QUORUM.
Write behavior:
When the network heals, read repair and explicit repair (nodetool repair) restore consistency.
Setup: Three datacenters: US (RF=3), EU (RF=3), ASIA (RF=2). US datacenter goes offline.
Write behavior with LOCAL_QUORUM in EU:
No impact on EU writes. US writes fail. ASIA applications can continue if they write to EU or ASIA with LOCAL_QUORUM.
Problem: Sacrifices availability for consistency. Any single node failure causes writes to fail.
Solution: Use QUORUM or LOCAL_QUORUM instead. They provide strong consistency while tolerating failures.
Problem: Batching unrelated writes hurts performance and can overwhelm coordinators.
-- BAD: Batching writes to different partitions
BEGIN BATCH
INSERT INTO users (user_id, name) VALUES (uuid1, 'Alice');
INSERT INTO products (product_id, name) VALUES (uuid2, 'Widget');
INSERT INTO orders (order_id, total) VALUES (uuid3, 100);
APPLY BATCH;
Solution: Only batch writes to the same partition, or use unlogged batches when atomicity isn’t needed.
Problem: Mixing client-side and server-side timestamps leads to unpredictable conflict resolution.
Solution: Either always use USING TIMESTAMP or never use it. Be consistent across your application.
Problem: Clock drift causes last-write-wins to produce unexpected results.
Solution: Keep NTP running on all nodes with drift under 500ms. Monitor clock sync regularly.
Problem: Repeatedly updating the same partition creates a bottleneck.
-- BAD: Global counter
UPDATE stats SET page_views = page_views + 1 WHERE stat_name = 'homepage';
Solution: Distribute load across partitions:
-- GOOD: Sharded counter
UPDATE stats
SET page_views = page_views + 1
WHERE stat_name = 'homepage' AND shard = 0; -- Pick shard 0-99 randomly
Ensure your partition keys distribute data evenly:
-- BAD: All events for a user in one partition (can grow huge)
CREATE TABLE user_events (
user_id uuid,
timestamp timestamp,
event_type text,
PRIMARY KEY (user_id, timestamp)
);
-- GOOD: Partition by user and date bucket
CREATE TABLE user_events (
user_id uuid,
date_bucket text, -- e.g., "2025-01-15"
timestamp timestamp,
event_type text,
PRIMARY KEY ((user_id, date_bucket), timestamp)
);
Instead of manually deleting old data, use TTL:
INSERT INTO sensor_data (sensor_id, timestamp, value)
VALUES (?, ?, ?)
USING TTL 86400; -- Expire after 24 hours
This is much more efficient than DELETE statements.
Run regular repairs to ensure replicas stay synchronized:
# Full repair (expensive)
nodetool repair
# Incremental repair (recommended)
nodetool repair --incremental
Schedule repairs weekly or monthly, depending on your workload.
Cassandra uses a Log-Structured Merge (LSM) tree to hold the data, where writes are first persisted to an append-only CommitLog before being written to an in-memory MemTable.
MemTables are periodically flushed to disk as immutable SSTables, and background compaction merges them to optimize read performance. This classic Write-Ahead Logging (WAL) mechanism ensures durability and high write throughput via sequential disk I/O.
The coordinator node also takes care of writing to all the replicas. In case write to one of the replicas fails or times out, then hinted handoff is leveraged to repair and maintain eventual consistency. Cassandra leverages quorum with tunable consistency levels, allowing trade-offs between consistency and availability for both reads and writes.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。