Best practices for Cassandra drivers
If your applications use Cassandra drivers, use these guidelines to help improve performance and minimize resource utilization.
Session and cluster handling
All drivers use root objects to connect to Hyper-Converged Database (HCD) clusters. These root objects are expensive to create because they initialize and maintain connection pools to every node in a cluster. However, a single root object can handle thousands of queries concurrently, so you can use the same driver session instance to execute all the queries for an application.
Follow these best practices to ensure that you instantiate and reuse these objects effectively:
-
Create only one long-lived root object for each physical cluster.
-
Create one session instance for each application, and then reuse that session for the entire lifetime of the application. For example, don’t open and close the session object for each individual request or batch of requests.
Most Cassandra drivers provide separate cluster and session objects.
The exceptions are the Java and Node.js drivers that combine the cluster and session concepts into one object.
For more information about session and cluster configuration, see your driver’s documentation:
- C/C++ driver
-
Create one
CassClusterfor each physical cluster.Create and reuse one
CassSessionfor each application.For an example, see Get started with the C++ driver.
- C# driver
-
Create one
Cassandra.Clusterfor each physical cluster.Create and reuse one
Cassandra.Sessionfor each application.For an example, see Get started with the C# driver.
- Go driver
-
Create one
NewClusterfor each physical cluster.Create and reuse one
Sessionfor each application.For an example, see Get started with the Go driver.
- Java driver
-
The Java driver defines clusters and sessions together in
CqlSession. Create oneCqlSessioninstance for each target physical cluster, and then reuse those instances throughout your application. For more information and examples, see the documentation for your version of the Java driver:If you want the driver to retry the connection if it fails to establish a CQL session, set
advanced.reconnect-on-initto true. For more information, see Cassandra driver reconnection policies. - Node.js driver
-
The Node.js driver combines the concepts of session and cluster into a single
Clientinterface. Create oneClientinstance for a given cluster, and then use that instance across your application. For more information and examples, see the documentation for your version of the Node.js driver: - Python driver
-
Create one
Clusterfor each physical cluster.Create and reuse one
Sessionfor each application.For more information and examples, see the documentation for your version of the Python driver:
For more information and best practices to help optimize driver connections, see Performance tuning for Cassandra drivers.
Use asynchronous queries for bulk data access
DataStax recommends executing queries asynchronously when processing large amounts of data, including large numbers of queries or long-running queries. For more information and recommended usage patterns, see Asynchronous query execution with Cassandra drivers.
In large partitions, fetch rows in batches
When dealing with large partitions, don’t attempt to read the complete partition at once. Doing so can exceed memory limits granted to your operating system process.
// Avoid patterns that read all rows into memory at once:
List<Row> rows =
session.execute("SELECT * FROM employees WHERE company = 'DS'").all();
// Prefer patterns that fetch rows in batches:
Iterator<Row> iterator =
session.execute("SELECT * FROM employees WHERE company = 'DS'").iterator();
while (iterator.hasNext()) {
Row row = iterator.next();
// process...
}
from cassandra.query import SimpleStatement
query = "SELECT * FROM users" # Assume users contains 100 rows
statement = SimpleStatement(query, fetch_size=10) # Fetch rows in batches
for user_row in session.execute(statement):
process_user(user_row)
By default, drivers pre-fetch 5000 rows, and this setting is configurable. For more information about paging and iteration, see Result paging with Cassandra drivers, Use object mappers in Cassandra drivers, and your driver’s documentation.
Use prepared statements for frequently run queries
Prepared statements are queries that you can run multiple times with different parameters. They can make your applications more efficient by reducing resource requirements and eliminating redundancies in code. For more information, see Prepared statements with Cassandra drivers.
Use lightweight transactions (LWTs) judiciously
Lightweight transaction (LWT) statements aren’t intended as general purpose optimistic locking mechanisms.
LWTs require three round trips between nodes to propose, agree, and publish the new row state, and the row must be read from disk.
Compared to traditional CQL statements, LWTs usually have higher resource requirements, higher response latency, and lower throughput.
For additional considerations for LWTs, see the following:
Avoid allow filtering
Don’t use the ALLOW FILTERING CQL clause unless you can guarantee that the given table is small and performing a full scan will have acceptable response latency.
Even if the clause is limited to a single partition, ALLOW FILTERING cannot guarantee the same performance as searching on clustering columns.
When searching on clustering columns, Cassandra-based databases can perform a binary search. When searching on non-clustering columns, they must read and compare all rows.
Use idempotent statements
A statement is idempotent if executing it multiple times leaves the database in the same state as executing it only once. This also means that the request is safe to retry, and your driver can automatically retry the request in the event of a failure. For more information, see Query idempotence in Cassandra drivers and Retry policies for Cassandra drivers.
Manage tombstones
A tombstone is a marker that indicates deleted table data.
HCD databases store updates to tables in immutable SSTable files to maintain throughput and avoid reading stale data. Deleted data, time-to-live (TTL) data, and null values create tombstones that allow the database to reconcile the logically deleted data with new queries across the cluster.
While tombstones are a necessary byproduct of a distributed database, they can cause warnings and log errors. Additionally, heavy deletes and nulls use extra disk space and decrease performance on reads.
Example: Tombstone creation from writing nulls
The following example demonstrates how tombstones are created from writing nulls.
Assume a table has the following schema:
CREATE TABLE test_ks.my_table_compound_key (
primary_key text,
clustering_key text,
regular_col text,
PRIMARY KEY (primary_key, clustering_key)
)
The following query doesn’t create any tombstones:
INSERT INTO my_table_compound_key (primary_key, clustering_key)
VALUES ('pk1', 'ck1');
However, this query does create a tombstone because it explicitly writes null to regular_col:
INSERT INTO my_table_compound_key (primary_key, clustering_key, regular_col)
VALUES ('pk1', 'ck1', null);
Cull and prevent tombstones
Culling tombstones and avoiding tombstone creation increases database and application performance:
-
Design your data model to avoid unnecessary deletes.
For example, minimize explicit data removal and leverage TTL. With TTL, make sure garbage collection occurs promptly, and be aware that each
mapkey has a separate TTL value. -
When writing, construct queries properly to avoid writing nulls.
-
When reading, use efficient filters in your queries to avoid fetching unnecessary rows, which can contain tombstones.
For example, when reading all rows from a partition, tombstones are scanned. You can use ordering on clustering columns to avoid reading lots of tombstones.
-
Optimize garbage collection and compaction strategies to remove tombstones efficiently.
-
Use tracing to monitor how tombstones impact query performance.
For more information, see Tombstones.
Performance tuning for Cassandra drivers
For information about performance tuning, see metrics, connection pools, load balancing, and your driver’s documentation.
- C/C++ driver
- C# driver
- Go driver
- Java driver
-
For all performance tuning options and general information about performance tuning for the Java driver, see the documentation for your version of the Java driver:
For applications that send or store large payloads with the Java driver, DataStax recommends the following configurations in addition to other performance tuning practices:
-
Adjust thread pooling when storing large payloads: If your application stores large payloads, you can potentially improve overall throughput and latency by adjusting the Netty IO thread count. By default, IO thread pool size is twice the value of
Runtime.getRuntime().availableProcessors():p = Runtime.getRuntime().availableProcessors() Default Netty IO thread count = p * 2Test your application’s performance before and after the change to verify any performance improvements. You might need to adjust the value multiple times to find an ideal configuration. For more information, see the documentation for your version of the Java driver:
-
Enable compression when sending large payloads: If your application sends large text values (that could have high compression ratios), and your driver connects to HCD clusters running in remote locations, consider enabling compression of protocol frames. This configuration saves network bandwidth but requires slightly more CPU resources.
datastax-java-driver { advanced.protocol { compression = lz4 } }For more information, see the documentation for your version of the Java driver:
-
- Node.js driver
-
See the documentation for your version of the Node.js driver:
- Python driver
-
See the documentation for your version of the Python driver: