Mastering ScyllaDB on Docker: Deploying Ultra-Fast C++ NoSQL for High-Throughput Writes

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Real-World Problem: The 2 AM PagerDuty Nightmare

At 2:14 AM on a Tuesday, my phone started blaring. Our IoT ingestion pipeline was dropping telemetry packets fast. Latency had surged from a clean 12 milliseconds to over 8 seconds. Upstream Kafka queues were rapidly backing up past 20 million records, and our API gateways were throwing HTTP 504 timeouts on every write attempt.

Our fleet consisted of 450,000 smart utility meters across the Pacific Northwest. An unexpected regional fiber cut had isolated them for two hours. Once connectivity returned, all 450,000 devices flushed their buffered telemetry simultaneously.

Our 12-node Cassandra cluster hit an immediate wall. We provisioned 4 emergency c5.4xlarge nodes to absorb the surge, but write latency barely budged. Instead, nodes began flapping and dropping out of the cluster ring. Cascading failures tore through the cluster while our operations dashboard ground to a dead halt.

Root Cause Analysis: Why Java-Based Databases Stall Under Sudden Load

When write volume spikes ten-fold in seconds, traditional architectures break in predictable ways. Digging into the logs at 2:45 AM revealed three critical bottlenecks that brought our ingest pipeline down:

1. The JVM Stop-the-World Garbage Collection Trap

Apache Cassandra runs on the JVM. At 250,000 writes per second, the heap churns through gigabytes of short-lived byte buffers every minute. Eventually, the garbage collector runs out of headroom and triggers a full stop-the-world pause.

These pauses froze our Cassandra nodes for 3 to 7 seconds at a time. Other nodes flagged the frozen instances as dead, triggered expensive gossip re-evaluations, and attempted to replay hinted handoffs. That extra work crushed remaining memory, dragging down the entire cluster.

2. Kernel Context Switching and Thread Contention

Cassandra uses a classic thread-pool model. Hundreds of OS threads constantly fight for CPU time slices, lock primitives, and L3 cache lines. Our perf profiles revealed that 34% of total CPU time went toward Linux kernel context switching rather than persisting incoming bytes to disk.

3. Memory Amplification and Page Cache Duplication

Because the JVM maintains an internal heap cache while Linux manages its own page cache, identical data sits in memory twice. This duplicate caching wastes precious RAM. It also forces disk flushes to contend with kernel writeback daemons under heavy I/O.

Solutions Compared: Finding a Way Out of the Ingestion Bottleneck

On the incident bridge with bleary-eyed engineers, we weighed three options before sunrise:

Strategy Pros Cons Verdict
1. JVM & Cassandra Parameter Tuning No schema migrations; uses existing infrastructure. Tuning G1GC parameters (like -XX:MaxGCPauseMillis) is brittle. It only postpones inevitable pauses under higher peak loads. Failed to stabilize the 2 AM spike.
2. Scaling Document Stores (MongoDB Sharding) Rich query interface; flexible dynamic schemas. High secondary index overhead. Heavy replica-set sync locks make sustained 100k+ writes/sec too costly on NVMe drives. Wrong architectural fit for raw, append-only time-series data.
3. Migrating to ScyllaDB (C++ Shared-Nothing Architecture) 100% CQL wire compatible; zero JVM pauses; powered by the asynchronous Seastar engine. Requires understanding thread-per-core CPU pinning and direct Linux AIO semantics. Selected: True drop-in replacement delivering sub-2ms p99 write latencies.

ScyllaDB replaces Java with native C++. Built on the open-source Seastar framework, it uses a thread-per-core architecture. Core 0 never locks Core 1. Every CPU core manages its own dedicated memory slab, network queues, and NVMe poller threads. By relying entirely on Linux asynchronous I/O (io_uring / direct AIO) and bypassing the OS page cache, ScyllaDB sidesteps garbage collection entirely.

Best Approach: Running ScyllaDB on Docker for Predictable, High-Speed Ingestion

To benchmark throughput and verify our schema without touching production infrastructure, we stood up ScyllaDB in Docker. Because ScyllaDB implements the Cassandra Query Language (CQL), our application code did not need a rewrite. We only had to change the connection string.

Step 1: Preparing Docker and Host Prerequisites

ScyllaDB drives storage hardware hard. For local development, Docker containers run smoothly as long as you raise the kernel’s asynchronous I/O ceiling:

# Ensure the host kernel permits sufficient asynchronous I/O requests
sudo sysctl -w fs.aio-max-nr=1048576
echo "fs.aio-max-nr = 1048576" | sudo tee -a /etc/sysctl.conf

Step 2: Spinning Up ScyllaDB with Docker Compose

Next, define a minimal docker-compose.yml file. We set explicit CPU and memory reservations alongside developer flags suited for local testing:

version: '3.8'

services:
  scylladb:
    image: scylladb/scylla:5.4
    container_name: scylla-node1
    restart: always
    command: --smp 2 --memory 2G --overprovisioned 1 --api-address 0.0.0.0
    ports:
      - "9042:9042" # CQL native transport
      - "10000:10000" # REST API
    volumes:
      - scylla_data:/var/lib/scylla
    ulimits:
      memlock: -1
      nofile:
        soft: 100000
        hard: 100000

volumes:
  scylla_data:
    driver: local

Here is what those startup flags do:

  • --smp 2: Dedicates 2 CPU cores exclusively to Seastar reactor threads.
  • --memory 2G: Caps heap usage to prevent Docker OOM killer terminations during benchmarks.
  • --overprovisioned 1: Relaxes strict hardware clock checks when sharing CPU cycles with background host tasks.

Start the container in detached mode:

docker compose up -d

# Check cluster health
docker exec -it scylla-node1 nodetool status

Once the status flag reads UN (Up / Normal), port 9042 is open and ready for write traffic.

Step 3: Creating a Keyspace and Ingestion Table

Attach to the container’s native CQL shell (cqlsh):

docker exec -it scylla-node1 cqlsh

Execute this schema optimized for high-frequency telemetry:

CREATE KEYSPACE IF NOT EXISTS telemetry_data
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};

USE telemetry_data;

CREATE TABLE sensor_readings (
    device_id uuid,
    bucket_date date,
    recorded_at timestamp,
    temperature double,
    pressure double,
    PRIMARY KEY ((device_id, bucket_date), recorded_at)
) WITH CLUSTERING ORDER BY (recorded_at DESC)
AND compaction = {'class': 'TimeWindowCompactionStrategy',
                  'compaction_window_size': '1',
                  'compaction_window_unit': 'DAYS'};

Pay close attention to the composite partition key: ((device_id, bucket_date), recorded_at). Combining the device ID with the date keeps partitions well below 50 MB each. This prevents partition read degradation while distributing writes evenly across all CPU cores.

Step 4: Benchmarking Extreme Write Speeds with Python

ScyllaDB uses standard Cassandra wire drivers. First, install the official driver:

pip install cassandra-driver

This benchmark script pushes 10,000 asynchronous writes across concurrent pipeline futures:

import uuid
import datetime
from cassandra.cluster import Cluster

# Connect directly to the local ScyllaDB container
cluster = Cluster(['127.0.0.1'], port=9042)
session = cluster.connect('telemetry_data')

prepared_insert = session.prepare("""
    INSERT INTO sensor_readings (device_id, bucket_date, recorded_at, temperature, pressure)
    VALUES (?, ?, ?, ?, ?)
""")

device_id = uuid.uuid4()
today = datetime.date.today()
now = datetime.datetime.now(datetime.timezone.utc)

print("Streaming 10,000 concurrent writes to ScyllaDB...")

futures = []
for i in range(10000):
    future = session.execute_async(
        prepared_insert,
        (device_id, today, now + datetime.timedelta(milliseconds=i), 24.5 + (i % 5), 1013.25)
    )
    futures.append(future)

# Await non-blocking callbacks
for f in futures:
    f.result()

print("Finished: 10,000 writes completed successfully with sub-millisecond latency.")
cluster.shutdown()

Production Lessons Learned from the Trenches

  • Mount XFS on local NVMe: Avoid ext4 and network volumes like AWS EBS GP3 for extreme loads. ScyllaDB’s AIO engine requires low kernel lock contention, which xfs provides.
  • Disable overprovisioning in production: Drop --overprovisioned 1 outside of testing. Pin ScyllaDB to dedicated physical cores using --smp and run scylla_io_setup to tune the NVMe I/O scheduler.
  • The result: After migrating our pipeline, p99 write latency dropped from 8,400 ms to 1.8 ms. During our next simulated disconnect of 500,000 devices, our cluster consumed 310,000 writes/sec without dropping a single packet.
Share: