Learning Platform
Глоссарий Troubleshooting
Урок 21.03 · 22 мин
Средний
dockercapstonetestingvalidationstress-tests

Валидация и стресс-тесты

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).

Точки проверки validation flow
1. ps healthyШаг 1: docker compose ps -- все healthy. Без этого нет смысла проверять дальше.
ok?
2. kafka eventsШаг 2: Kafka содержит сообщения. Smoke test producer -> Kafka path.
3. spark runningШаг 3: Spark UI показывает streaming job. Batches каждые 30 секунд.
ok?
4. clickhouse rowsШаг 4: ClickHouse имеет агрегаты. End-to-end pipeline works.
5. grafana graphsШаг 5: Grafana показывает графики. UX-level verification.

Стресс-тест 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:

  1. Логи всех сервисов: docker compose logs --tail 50. Ищи ERROR, FATAL.

  2. Конкретный сервис: docker compose logs -f event-producer. Может крутиться restart-loop.

  3. Network connectivity: заходи в контейнер и ping другие. docker compose exec event-producer python -c "import socket; print(socket.gethostbyname('kafka'))".

  4. Port mappings: docker compose port event-producer 9000 — проверь, что mapping есть.

  5. Volumes: docker volume ls | grep capstone. Должны быть ch-data, kafka-data, grafana-data, spark-checkpoint.

  6. Disk space: docker system df. Если nearly full — prune.

  7. Resources: docker stats. Если контейнер съел 100% CPU и завис — limits.


Проверка знанийKnowledge check
Ты выполняешь стресс-тест "kill producer на 30 секунд". После рестарта в дашборде Grafana виден gap в линии events/minute -- это нормально. Но кто-то говорит "это plохо -- pipeline не recovered". Какие два аргумента поддерживают, что gap корректен?
ОтветAnswer
Gap корректен по двум причинам. (1) Streaming pipeline отражает РЕАЛЬНОСТЬ. Если в источнике (producer) не было событий за минуту, в агрегатах за эту минуту должно быть 0 (или этой минуты просто нет, если row создается только при наличии данных). Скрывать gap = lie -- дашборд будет показывать события, которых не было. (2) Recovery это про continuity ПОСЛЕ down-period, а не про back-fill. После рестарта producer'а в минуты после рестарта количество событий снова normal -- это и есть recovery. Если в minute X не было событий и producer был down, никто не может "восстановить" эти missed events -- они не существовали. Back-fill возможен, если у тебя есть source-of-truth с timestamp'ами и можно перечитать (например, replay из Kafka -- но если producer не писал, в Kafka и нет данных).

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. Что значит, что в Grafana дашборде events/minute виден gap в одну минуту после kill producer на 30 секунд, и это OK?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4