Labs¶
Friday, 4:55 PM. You clone labs/kafka, run docker compose up -d, produce a few hundred events, watch the consumer keep up, and close the laptop. Nothing broke.
Predict before you read on: if you skip the README's hypothesis section and go straight to docker compose up, what do you actually lose — the ten minutes it would have taken, or the lesson?
It's the lesson. Hands-on work here lives in labs/ at the repository root, not under docs/, and each subdirectory is a small Compose (or a laptop PySpark) environment built around a README that follows predict → run → break, not run → shrug.
New: enter from the concept
The practice map pairs each core idea with a short simulation, this real-system lab, and an incident. If you are learning rather than looking up commands, start there. Each lab README now repeats its exact read → simulate → run → diagnose sequence.
You do not need a cloud account. You need Docker for Kafka, ClickHouse, and the optional Flink UI. Kafka/Spark scripts support Python 3.9+; the pinned PyFlink 1.18 wheel should use a supported Python 3.9–3.11 environment. About 8 GB RAM is enough; 16 GB if you run Kafka + ClickHouse at once.
Module pages still have extra exercises (Spark labs, Kafka labs, Flink labs). Start from root labs/ so the Compose files stay in one place.
How to run a lab¶
- Read the README hypothesis section. Write what you expect to see.
cd labs/<name> && docker compose up -d(Spark: no cluster required).- Run the commands in order. Do not skip the "break" step — that is the incident.
- Run that lab's
check_*.pyscript. It makes the same claim you were asked to predict and either printsPASSwith the measured numbers, or raises anAssertionErrornaming exactly what did not hold — you do not have to trust your own eyeballing of a chart or a log line. - Explain any gap between prediction and output. That gap is the lesson.
docker compose down -vso the next lab is clean.
Predict first
If you run the happy path and only then read the questions, you are executing a recipe. The academy's study loop is the opposite.
The check script is the exit criterion
Every lab now has at least one check_*.py (check_hot_partition.py, check_skew.py, check_order_by.py, check_event_time.py) that turns the lab's central prediction into a real assertion against the running system. A green PASS is evidence you reproduced the failure mode; a page full of terminal output with no assertion is not the same thing.
Lab map¶
| Lab | Directory | What you stand up | What you should feel |
|---|---|---|---|
| Kafka | labs/kafka/ | Single-broker KRaft Kafka | Produce/consume, lag, hot partition |
| Spark | labs/spark/ | Local PySpark on the laptop | Shuffle, skew, join strategy, partition count |
| Flink | labs/flink/ | Optional Compose JobManager+TaskManager; PyFlink conceptual path | Event time vs processing time, idle watermark, keyed state |
| ClickHouse | labs/clickhouse/ | Official ClickHouse server | ORDER BY skip vs full scan, too many parts |
| Cassandra | labs/cassandra/ | Official Cassandra server, single node | Partition key is the query plan, unbounded wide partition |
| Time series | labs/time-series/ | Synthetic exporter + official Prometheus | Series count is a product of label domains, cardinality explosion |
Open the runnable lab directories on GitHub: Kafka, Spark, Flink, ClickHouse, Cassandra, and Time series.
If you are browsing the documentation site, open the GitHub tree or your local clone — Compose files are not inside docs/.
Kafka (labs/kafka)¶
Compose: Apache Kafka 3.7, KRaft combined broker+controller, port 9092.
You will:
- Create
user-eventswith 6 partitions. - Produce the System A JSON events (SaaS analytics).
- Consume in a group; watch offsets.
- Predict lag with a slow consumer; measure with
kafka-consumer-groups. - Break: produce with a hot key (
customer_id=cust_0042at 80%). Watch one partition's lag climb. Extra consumers do not split that partition.
Check: python check_hot_partition.py asserts one partition holds a large majority of the traffic instead of asking you to eyeball it.
Pairs with partitions, incident 1, partition simulator.
Spark (labs/spark)¶
No Compose. pip install pyspark and run the scripts in the README (or the longer set in docs/spark/labs.md). Spark UI at http://localhost:4040.
You will:
groupByand find shuffle read/write in the UI.- Build an 80% skewed
customer_idand watch one task dominate. - Compare sort-merge vs broadcast join times.
- Sweep
spark.sql.shuffle.partitions.
Break: disable AQE and broadcast; join the skewed frame; predict which task dies or crawls.
Check: python check_skew.py reads the per-key row counts back from the aggregation output and asserts uniform mode is actually uniform and skew mode is actually skewed.
Pairs with shuffle, incident 2, shuffle simulator.
Flink (labs/flink)¶
Two tracks:
- Conceptual / PyFlink on the laptop — enough to see watermarks and keyed state without a cluster.
- Compose (JobManager
8081+ TaskManager) if you want the Flink UI. Optional Kafka fromlabs/kafkaon the same Docker network is extra credit, not required.
You will:
- Run the same 20 events through processing-time vs event-time windows (timestamps 5 minutes in the past).
- Predict which window they land in.
- Break: one idle source split stalls the downstream minimum watermark so no window output — incident 3. A stale record on an active split is a different failure and does not move a max-based watermark backwards.
- Keyed failed-login counter with
ValueState.
Check: python stalled_watermark.py self-asserts the idleness-exclusion claim; python check_event_time.py collects both jobs' actual window boundaries and asserts event-time buckets land ~5 minutes in the past while processing-time buckets land ~now.
Pairs with time, windows, state.
ClickHouse (labs/clickhouse)¶
Compose: clickhouse/clickhouse-server, HTTP 8123, native 9000.
You will:
- Load the same SaaS events into two tables:
ORDER BY (customer_id, timestamp)vsORDER BY (timestamp, customer_id). - Predict
marks/ rows read for a tenant dashboard query vs a global time query. - Compare with
EXPLAINand query log. - Break: insert one row per INSERT in a loop; watch
system.partsexplode and the same SELECT slow down — incident 4.
Check: python check_order_by.py reads ClickHouse's own read_rows for the tenant query on both tables and asserts the tenant-first design reads meaningfully fewer rows.
Pairs with ClickHouse, ORDER BY explorer.
Cassandra (labs/cassandra)¶
Compose: official cassandra:4.1, native protocol 9042. Slower to boot than the other labs — 30-60s before the healthcheck is green.
You will:
- Load the same SaaS events into two tables:
events_by_service(partition keyservice, 4 values) vsevents_by_customer_day(partition key(customer_id, day_bucket)). - Predict which design produces the larger single partition once 80% of traffic hits one service and one customer.
- Inspect partition sizes with
nodetool tablehistograms; both queries are fast single-partition reads regardless of which design is a bad idea. - Break (optional): push
--hot-ratioand row count higher and watchnodetool tablehistogramsshow a dramatically larger max partition size — real, but slow on a laptop; the check script proves the same point instantly.
Check: python check_wide_partition.py queries Cassandra's own row counts and asserts the low-cardinality partition key absorbs a large majority of all rows, while the compound key bounds the same hot customer to one partition.
Pairs with Cassandra & ScyllaDB, consistent hashing visualizer.
Time series (labs/time-series)¶
Compose: a dependency-free synthetic Prometheus exporter + official prom/prometheus.
You will:
- Scrape a metric with 4 bounded labels (service, region, status_code) — 60 series.
- Predict the series count
prometheus_tsdb_head_seriesshould report (including Prometheus's own several hundred self-monitoring series). - Break: add a
user_idlabel with 10,000 values and watchprometheus_tsdb_head_seriesjump by roughly that multiplier — the same arithmetic as the cardinality calculator.
Check: python3 check_cardinality.py --expect-min <N> --expect-max <M> reads Prometheus's own head-series count via its HTTP API for whichever scenario you just ran.
Pairs with Cardinality, cardinality calculator.
Shared dataset (all labs)¶
SaaS analytics event (System A):
{
"timestamp": "2024-01-15T10:30:00Z",
"customer_id": "cust_0042",
"user_id": "user_98712",
"service": "api-gateway",
"endpoint": "/v2/events",
"region": "eu-west-1",
"latency_ms": 45,
"status_code": 200,
"bytes": 1024
}
Use this shape so Kafka, Spark, Flink, ClickHouse, and Cassandra are comparable. When you "hot key" or "skew join," it is cust_0042 — the same whale as analytics architecture. The time-series lab uses a synthetic metric instead, since Prometheus labels are not this event shape.
Prerequisites¶
| Tool | Why |
|---|---|
| Docker + Compose v2 | Kafka, ClickHouse, Cassandra, Prometheus, Flink UI |
| Python 3.9+ | kafka-python or confluent-kafka, PySpark, optional PyFlink, cassandra-driver |
| 8 GB RAM | 16 GB if stacking Compose files; Cassandra alone wants ~2 GB for its JVM |
| Browser | Spark UI 4040, Flink 8081, CH HTTP, Prometheus 9090 |
Windows: use WSL2. Apple Silicon: the images used in labs/*/docker-compose.yml publish arm64 or emulate; Kafka/CH/Cassandra/Prometheus official images are fine on M-series. cassandra-driver's source build can fail on very new Python versions — force the prebuilt wheel with pip install --only-binary=:all: cassandra-driver if that happens.
What is not a Compose lab here¶
Iceberg time travel, Trino federation, Timescale continuous aggregates, and Neo4j rings are taught in their modules with SQL/Cypher you can run if you already have those systems. They are not stubbed as "coming soon" on this page — they are not in labs/ yet. Use simulations and incidents (Iceberg snapshots, Trino OOM) as tabletop drills until you add Compose.
Do not wait for those to run Kafka/Spark/Flink/CH/Cassandra/Prometheus. Those six are the data plane (plus one metrics plane).
After a lab¶
- Write one sentence: the metric that would have paged me.
- Open the matching incident and do the hypothesis pause without looking at your lab notes first.
- Connect the observation to one architecture page; that step turns a useful experiment into a reusable design insight.
Root map next to Compose: labs/README.md.
Suggested order (one weekend)¶
| When | Lab | Stop when you can say |
|---|---|---|
| Sat morning | Kafka | "More consumers will not save partition 7" |
| Sat midday | Cassandra | "The partition key is the query plan" |
| Sat afternoon | ClickHouse | "I can predict marks read from ORDER BY" |
| Sun morning | Spark | "I know which UI column is skew" |
| Sun midday | Time series | "Cost is #series, not #samples" |
| Sun afternoon | Flink | "Windows close on watermarks, not on hope" |
Then sit the matching incidents without the lab notes open.
Troubleshooting¶
| Symptom | Likely |
|---|---|
Kafka client Connection refused | Compose not healthy; advertised listener is localhost:9092 — do not use the container hostname from the host |
PySpark Java gateway | Install JDK 11/17; JAVA_HOME |
ClickHouse too many parts immediately | You are on the break step — that is success |
| Flink no output on event-time lab | Watermark never passed window end; read the README dummy-event note |
Cassandra cqlsh/driver connection refused | Its healthcheck takes 30-60s — wait for docker compose ps to show healthy, not just Up |
prometheus_tsdb_head_series query returns no data | Prometheus must scrape itself, not just the exporter — check prometheus.yml has a prometheus job targeting localhost:9090 |
| Port 9092 / 8123 / 8081 / 9042 / 9090 busy | Another lab still up; docker compose down |
Do not change image tags casually. The Compose files pin versions that match the READMEs.
Pairing with simulations¶
Do the HTML before Compose if you are new to the concept; after Compose if you want to check the mental model. Same whale key cust_0042 everywhere so the stories compose.