Валидация и стресс-тесты
Capstone-стенд работает — это половина задачи. Вторая половина — доказать, что он работает надёжно. В этом уроке: систематические проверки руками, что должно работать в каждом сценарии, и стресс-тесты edge cases.
Чек-лист 1: базовая работоспособность
После make up && sleep 30 && make submit-spark && sleep 90, проверь:
1. Все сервисы healthy
docker compose ps
Ожидаемый вывод:
NAME IMAGE STATUS
clickhouse clickhouse/clickhouse-server:24.10 Up (healthy)
event-producer de-capstone-event-producer Up
grafana grafana/grafana:11.3.1 Up
kafka bitnami/kafka:3.8 Up (healthy)
spark-master bitnami/spark:3.5 Up
spark-worker bitnami/spark:3.5 Up
spark-worker bitnami/spark:3.5 Up
Все либо Up (healthy), либо Up. Никаких Restarting или Exited.
2. Kafka принимает события
docker compose exec kafka kafka-topics.sh \
--bootstrap-server localhost:9092 \
--describe --topic events
Должно показать Topic: events PartitionCount: 3 ReplicationFactor: 1.
docker compose exec kafka kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic events --max-messages 3
Ожидаешь 3 JSON-сообщения с полями user_id, event_type, event_time, session_id.
3. Consumer lag
Consumer groups и lag: как Kafka отслеживает прогрессdocker compose exec kafka kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--list
# spark-kafka-source-* -- spark consumer group
docker compose exec kafka kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--group spark-kafka-source-... --describe
LAG колонка должна быть < 1000 (т.е. Spark успевает обрабатывать). Если LAG растёт постоянно — Spark не успевает, увеличь workers / cores.
4. ClickHouse получает агрегаты
docker compose exec clickhouse clickhouse-client \
--query "SELECT count() FROM analytics.events_minute"
Растёт с каждой минутой (через 5 минут после старта будет ~20 строк — 4 event_type × 5 minutes).
docker compose exec clickhouse clickhouse-client \
--query "
SELECT minute, event_type, cnt, unique_users
FROM analytics.events_minute
ORDER BY minute DESC, event_type
LIMIT 8
FORMAT PrettyCompact"
Должны видеть последние 2 минуты × 4 типа = 8 строк. cnt должен быть ~150 (10 EPS × 60 sec / 4 типа), unique_users — меньше cnt.
5. Spark UI
open http://localhost:8080
- Workers: 2 ALIVE
- Running Applications: 1 (UserEventsStreaming)
- Status of application — зелёный
Кликни на application -> Streaming tab. Видишь batches каждые 30 секунд. “Processed records / sec” — ~10 (соответствует producer).
6. Grafana дашборд
open http://localhost:3000
Login admin/admin. Открой Dashboards -> User Events. Видишь два графика, оба растут вправо. Лёгкая колебательная natura (rate +/- 20%, потому что producer randomized).
Стресс-тест 1: kill producer на 30 секунд
Сценарий: моделируем сбой источника данных.
# 1. Запомни последнюю минуту, для которой есть данные
docker compose exec clickhouse clickhouse-client \
--query "SELECT max(minute) FROM analytics.events_minute"
# 2026-05-15 14:35:00
# 2. Останови producer на 30 секунд
docker compose stop event-producer
sleep 30
# 3. Запусти обратно
docker compose start event-producer
# 4. Подожди 90 секунд (1 minute window + processing)
sleep 90
# 5. Проверь
docker compose exec clickhouse clickhouse-client --query "
SELECT minute, event_type, cnt
FROM analytics.events_minute
WHERE minute > now() - INTERVAL 5 MINUTE
ORDER BY minute DESC, event_type
FORMAT PrettyCompact"
Ожидаемое поведение:
- Минута, когда producer был down — либо отсутствует в
events_minute, либо имеет очень низкийcnt. Это корректно — streaming aggregator показывает реальность. - После рестарта producer’а агрегаты возобновляются.
- Spark в Spark UI — continued running, без рестарта job’а.
Что НЕ должно произойти:
- Spark job НЕ должен упасть.
- ClickHouse НЕ должен потерять старые агрегаты.
- Grafana дашборд показывает gap в графике (это правильно — провал реальных данных).
Стресс-тест 2: рестарт Spark worker
Сценарий: один из worker-нод k8s падает.
# 1. List worker'ов
docker compose ps | grep spark-worker
# 2. Останови один
docker compose restart de-capstone-spark-worker-1 # или как named контейнер
# 3. Подожди 60 секунд
sleep 60
# 4. Проверь Spark UI
open http://localhost:8080
# Worker появился обратно (после ~10-15 секунд)
# 5. Streaming job
# Видишь, что batches продолжают идти, "Total Delay" может скакнуть но восстановиться
Spark standalone устойчив к падению одного worker’а: master перераспределяет executor’ы. Если у тебя только один worker — запасной нет, streaming-job будет ждать его восстановления.
Что НЕ должно произойти:
- Streaming job полностью упал и не recovered.
- В ClickHouse появились duplicate-агрегаты (одна и та же минута дважды).
- Checkpoint corrupted.
Стресс-тест 3: рестарт всего compose с persistence
Сценарий: ноутбук reboot, рестарт docker daemon.
# 1. Запиши текущее состояние
docker compose exec clickhouse clickhouse-client \
--query "SELECT count() FROM analytics.events_minute" \
> /tmp/before.txt
cat /tmp/before.txt
# 24
# 2. Полностью stop (БЕЗ -v)
docker compose down
# 3. Подожди 5 секунд
sleep 5
# 4. Запусти обратно
docker compose up -d
sleep 30
# 5. Сабмит Spark снова (checkpoint volume сохранил state)
make submit-spark
sleep 60
# 6. Проверь -- старые данные на месте?
docker compose exec clickhouse clickhouse-client \
--query "SELECT count() FROM analytics.events_minute" \
> /tmp/after.txt
cat /tmp/after.txt
# 24 (или больше, если Spark добавил новые минуты)
Ожидаемое поведение:
cntв “after.txt” >= “before.txt”. Старые агрегаты не потерялись.- Grafana dashboard показывает историю с того времени, до рестарта.
- Spark continues с last checkpoint — не переобрабатывает старые offset’ы.
Что НЕ должно произойти:
- ClickHouse пустая (значит, volume ch-data не настроен).
- Spark переобрабатывает Kafka с самого начала (значит, checkpoint volume не сохранен).
- Producer не запускается (значит, restart: unless-stopped не указан).
Стресс-тест 4: высокая нагрузка
Сценарий: внезапный спайк трафика.
# 1. Увеличь rate producer'а
echo "EVENTS_PER_SECOND=100" >> .env
docker compose up -d --force-recreate event-producer
# 2. Проверь, что producer выдаёт ~100/sec
docker compose logs --tail 20 event-producer
# Produced 100 events -- интервал ~1 sec
# Produced 200 events -- интервал ~1 sec
# 3. Подожди 3 минуты
sleep 180
# 4. Проверь lag
docker compose exec kafka kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--group spark-kafka-source-... --describe
# LAG должен быть стабильным (Spark catches up), не растёт неограниченно
# 5. Проверь cnt в ClickHouse
docker compose exec clickhouse clickhouse-client --query "
SELECT minute, event_type, cnt
FROM analytics.events_minute
WHERE minute > now() - INTERVAL 3 MINUTE
ORDER BY minute DESC, event_type"
# cnt ~ 1500 на event_type (100 EPS * 60 sec / 4 типа)
# 6. CPU usage
docker stats --no-stream
# spark-worker должен использовать значительную часть CPU
Что показывает test:
- Spark успевает обрабатывать поток в 100 EPS.
- Lag не растёт неограниченно.
- ClickHouse записывает агрегаты соответственно.
Если lag растёт — увеличь Spark resources (executor-cores, workers).
Стресс-тест 5: persistence -v
Сценарий: проверь, что -v действительно удаляет данные.
# 1. Запомни state
docker compose exec clickhouse clickhouse-client \
--query "SELECT count() FROM analytics.events_minute"
# 24
# 2. Полный stop с volumes
docker compose down -v
# 3. Запусти обратно
docker compose up -d
sleep 30
make submit-spark
sleep 60
# 4. Проверь -- БД пустая
docker compose exec clickhouse clickhouse-client \
--query "SELECT count() FROM analytics.events_minute"
# 1 или 2 (новые агрегаты с момента старта)
-v удаляет volumes. Состояние с нуля. Это destructive action, использовать только когда уверен.
Edge cases для проверки
Edge 1: Kafka topic auto-created
Если producer пишет в topic, которого нет, Kafka auto-create по default (включено KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: 'true'). Но без partitions config будет 1 partition — плохо для параллельности.
Решение: создавай topic явно в init-script:
# init/create-topics.sh
docker compose exec kafka kafka-topics.sh \
--bootstrap-server localhost:9092 \
--create --topic events --partitions 3 --replication-factor 1 \
--if-not-exists
Или через Spark option("subscribe", "events") — автокреэйт даст 1 partition, что в нашем case OK.
Edge 2: Spark и ClickHouse schema mismatch
Если Spark пишет колонку, которой нет в ClickHouse, INSERT падает.
# Pre-flight check:
docker compose exec clickhouse clickhouse-client \
--query "DESCRIBE TABLE analytics.events_minute"
# minute DateTime
# event_type LowCardinality(String)
# cnt UInt64
# unique_users UInt64
Spark DF должен иметь только эти колонки. Если есть лишние — DROP в .select() перед .write.
Edge 3: timezone проблемы
ClickHouse по умолчанию UTC. Producer пишет event_time как Unix timestamp (без TZ). Spark обрабатывает в UTC. Grafana показывает в browser timezone.
Если на дашборде минуты сдвинуты на 3 часа — это TZ-mismatch. Решение: всё в UTC (стандарт DE).
Готовый verification скрипт
#!/bin/bash
# verify.sh
set -e
echo "=== 1. docker compose ps ==="
docker compose ps
echo "=== 2. Kafka topic ==="
docker compose exec -T kafka kafka-topics.sh \
--bootstrap-server localhost:9092 --describe --topic events
echo "=== 3. Spark UI accessible ==="
curl -fs http://localhost:8080/ > /dev/null && echo "Spark UI OK"
echo "=== 4. ClickHouse health ==="
curl -fs http://localhost:8123/ping
echo "=== 5. Grafana health ==="
curl -fs http://localhost:3000/api/health > /dev/null && echo "Grafana OK"
echo "=== 6. Aggregates count ==="
COUNT=$(docker compose exec -T clickhouse clickhouse-client \
--query "SELECT count() FROM analytics.events_minute")
echo "events_minute rows: $COUNT"
[ "$COUNT" -gt 0 ] && echo "ClickHouse has data" || (echo "FAIL: no aggregates"; exit 1)
echo "=== 7. Recent activity ==="
docker compose exec -T clickhouse clickhouse-client --query "
SELECT minute, event_type, cnt
FROM analytics.events_minute
WHERE minute > now() - INTERVAL 5 MINUTE
ORDER BY minute DESC, event_type
LIMIT 8
FORMAT PrettyCompact"
echo "=== ALL CHECKS PASSED ==="
Запускай:
chmod +x verify.sh
./verify.sh
Если что-то не работает
Сценарий: ты прошёл этапы 1-5 (урок 02), но что-то не работает. Алгоритм debug:
-
Логи всех сервисов:
docker compose logs --tail 50. Ищи ERROR, FATAL. -
Конкретный сервис:
docker compose logs -f event-producer. Может крутиться restart-loop. -
Network connectivity: заходи в контейнер и
pingдругие.docker compose exec event-producer python -c "import socket; print(socket.gethostbyname('kafka'))". -
Port mappings:
docker compose port event-producer 9000— проверь, что mapping есть. -
Volumes:
docker volume ls | grep capstone. Должны бытьch-data,kafka-data,grafana-data,spark-checkpoint. -
Disk space:
docker system df. Если nearly full — prune. -
Resources:
docker stats. Если контейнер съел 100% CPU и завис — limits.