feat(module-1): доработаны материалы модуля 1 и удалён legacy-ноутбук

- Зачем:
  - нужен воспроизводимый вход в курс и единое место хранения планов по модулям.
- Что:
  - добавлены `plans/README.md`, living plan Модуля 1, `START_HERE.md` и канонический ноутбук `01_environment_and_smoke_test.ipynb`, а `cluster_smoke.py` расширен до reusable helper и CLI smoke test.
  - уточнены onboarding-материалы и окружение: добавлены Spark UI в `README.md`, ресурсы хоста и креды MinIO в `START_HERE.md`, использован `NB_GID` в `jupyter/Dockerfile`, в ноутбуке усилены самостоятельные задания и добавлены `cell id`, а пояснения в `cluster_smoke.py` переведены на русский для студентов.
  - обновлены `README.md`, `AGENTS.md` и archive howto, удалены устаревшие `spark-basic-test.ipynb` и `03_partitioning_and_schema_evolution.ipynb`.
- Проверка:
  - `python3 -m py_compile src/spark/cluster_smoke.py src/spark/__init__.py`.
  - `docker compose build spark-master` и `docker compose build jupyter`.
  - `docker compose up -d`, `docker compose exec jupyter python3 -c "from spark.cluster_smoke import create_spark_session, run_cluster_smoke; spark=create_spark_session(app_name='module-01-validation'); print(run_cluster_smoke(spark)); spark.stop()"` и `docker compose exec jupyter jupyter nbconvert --to notebook --execute /opt/work/01_environment_and_smoke_test.ipynb --output-dir /tmp --output module1-validation-2.ipynb`.
This commit is contained in:
2026-03-07 00:24:40 +03:00
parent 9d84e15e01
commit 688c46c68b
12 changed files with 554 additions and 269 deletions
@@ -0,0 +1,213 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "module1-intro",
"metadata": {},
"source": [
"# Модуль 1. Вход в стенд и базовая диагностика\n",
"\n",
"Этот ноутбук является канонической практикой Модуля 1.\n",
"\n",
"На выходе ты должен:\n",
"\n",
"- понимать, что стенд уже поднят на хосте;\n",
"- подключиться к Spark-кластеру из Jupyter;\n",
"- выполнить базовый smoke test;\n",
"- сопоставить сервисы стенда с их ролями;\n",
"- знать первый маршрут диагностики через `docker compose ps`, логи и UI.\n"
]
},
{
"cell_type": "markdown",
"id": "module1-before-start",
"metadata": {},
"source": [
"## Перед стартом\n",
"\n",
"Перед запуском этого ноутбука пройди шаги из `START_HERE.md`.\n",
"\n",
"Важно: `docker compose build`, `docker compose up -d`, `docker compose ps` и просмотр логов выполняются на хосте, а не внутри Jupyter.\n",
"\n",
"Ожидаемое состояние перед началом:\n",
"\n",
"- контейнеры `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter` уже подняты;\n",
"- открываются `Spark UI`, `Trino UI`, `MinIO Console` и `JupyterLab`;\n",
"- ты работаешь в Jupyter внутри контейнера `jupyter`.\n"
]
},
{
"cell_type": "markdown",
"id": "module1-service-map",
"metadata": {},
"source": [
"## Карта сервисов\n",
"\n",
"| Сервис | Роль |\n",
"| --- | --- |\n",
"| `MinIO` | `storage`: объектное хранилище для данных и файлов Iceberg |\n",
"| `PostgreSQL` | `catalog`: хранит метаданные JDBC-каталога Iceberg |\n",
"| `Spark` | `compute`: выполняет PySpark и Spark SQL |\n",
"| `Trino` | `compute`: читает те же таблицы через общий каталог |\n",
"| `Jupyter` | Точка входа в учебные ноутбуки |\n",
"\n",
"В этом модуле мы не строим пайплайн и не создаём Iceberg-таблицы. Задача только одна: убедиться, что среда рабочая и понятная.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "module1-create-session",
"metadata": {},
"outputs": [],
"source": [
"from spark.cluster_smoke import (\n",
" create_spark_session,\n",
" format_smoke_report,\n",
" run_cluster_smoke,\n",
")\n",
"\n",
"spark = create_spark_session(app_name=\"module-01-environment-and-smoke-test\")\n",
"spark\n"
]
},
{
"cell_type": "markdown",
"id": "module1-cluster-demo",
"metadata": {},
"source": [
"## Демонстрация: подключение к кластеру\n",
"\n",
"Сначала посмотрим на базовые признаки того, что ноутбук говорит именно с кластером, а не с локальным `local[*]` режимом.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "module1-connection-details",
"metadata": {},
"outputs": [],
"source": [
"connection_details = {\n",
" \"app_name\": spark.sparkContext.appName,\n",
" \"master_url\": spark.sparkContext.master,\n",
" \"default_parallelism\": spark.sparkContext.defaultParallelism,\n",
" \"spark_version\": spark.version,\n",
"}\n",
"connection_details\n"
]
},
{
"cell_type": "markdown",
"id": "module1-visual-check",
"metadata": {},
"source": [
"Проверь глазами:\n",
"\n",
"- `master_url` должен быть `spark://spark-master:7077`, а не `local[*]`;\n",
"- `default_parallelism` должен быть больше `1`;\n",
"- в `Spark UI` должны быть видны master и worker-ы.\n"
]
},
{
"cell_type": "markdown",
"id": "module1-smoke-explanation",
"metadata": {},
"source": [
"## Демонстрация: Spark smoke test\n",
"\n",
"Smoke test запускает распределённое вычисление на `range(0, 1_000_000)` и сверяет детерминированный результат.\n",
"\n",
"Если тест проходит, это означает минимум следующее:\n",
"\n",
"- Jupyter может создать SparkSession;\n",
"- Spark подключён к кластерному master;\n",
"- задание действительно выполняется, а не падает на первом действии.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "module1-run-smoke",
"metadata": {},
"outputs": [],
"source": [
"smoke_report = run_cluster_smoke(spark)\n",
"print(format_smoke_report(smoke_report))\n",
"smoke_report\n"
]
},
{
"cell_type": "markdown",
"id": "module1-self-check",
"metadata": {},
"source": [
"## Самостоятельное повторение\n",
"\n",
"Сделай руками и проверь себя:\n",
"\n",
"1. Открой `Spark UI`, `Trino UI`, `MinIO Console` и `JupyterLab`.\n",
"2. Сопоставь каждый сервис с одной из ролей: `storage`, `catalog`, `compute`, `entrypoint`.\n",
"3. Объясни, почему этот smoke test не должен работать в `local[*]` режиме.\n",
"4. На хосте выполни `docker compose ps` и посмотри, какие контейнеры должны быть в состоянии `Up`.\n",
"5. На хосте выполни `docker compose logs -f trino` или `docker compose logs -f spark-master`, чтобы посмотреть, где начинается первичная диагностика.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "module1-service-roles",
"metadata": {},
"outputs": [],
"source": [
"service_roles = {\n",
" # Заполни значения самостоятельно: storage, catalog, compute или entrypoint.\n",
" \"MinIO\": \"TODO\",\n",
" \"PostgreSQL\": \"TODO\",\n",
" \"Spark\": \"TODO\",\n",
" \"Trino\": \"TODO\",\n",
" \"Jupyter\": \"TODO\",\n",
"}\n",
"service_roles\n"
]
},
{
"cell_type": "markdown",
"id": "module1-checkpoint",
"metadata": {},
"source": [
"## Checkpoint\n",
"\n",
"К концу модуля ты должен уметь подтвердить:\n",
"\n",
"- стенд поднят и основные контейнеры живы;\n",
"- `Spark UI`, `MinIO Console`, `Trino UI` и `JupyterLab` открываются;\n",
"- smoke test завершился успешно и вернул ожидаемый результат;\n",
"- ты понимаешь назначение `MinIO`, `PostgreSQL`, `Spark`, `Trino`, `Jupyter`;\n",
"- ты знаешь, что первый маршрут диагностики начинается с `docker compose ps`, `docker compose logs -f <service>` и проверки UI.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "module1-stop-session",
"metadata": {},
"outputs": [],
"source": [
"spark.stop()\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,163 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Лаба 3: партиционирование и эволюция схемы",
"",
"В этом ноутбуке мы посмотрим, как Iceberg работает с партиционированными таблицами и эволюцией схемы поверх общего каталога `lakehouse`.",
"",
"Перед началом убедись, что стенд запущен (`docker compose up -d`) и Spark-кластер доступен."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pyspark.sql import SparkSession",
"",
"spark = (",
" SparkSession.builder",
" .appName(\"lab3-partitioning-schema-evolution\")",
" .master(\"spark://spark-master:7077\")",
" .getOrCreate()",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Часть 1. Партиционированная таблица",
"",
"Для начала создадим партиционированную Iceberg-таблицу `lakehouse.default.partition_demo` с помощью готового SQL-скрипта из `src/spark/partitioned_table_demo.sql`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path",
"",
"sql_path = Path(\"/opt/src/spark/partitioned_table_demo.sql\")",
"sql_text = sql_path.read_text(encoding=\"utf-8\")",
"",
"for statement in sql_text.split(\";\"):",
" stmt = statement.strip()",
" if stmt:",
" spark.sql(stmt)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Посмотрим, какие данные записаны по датам, и обсудим партиционирование по `event_date`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"spark.sql(\"\"\"",
"SELECT",
" event_date,",
" COUNT(*) AS cnt,",
" SUM(amount) AS total_amount",
"FROM lakehouse.default.partition_demo",
"GROUP BY event_date",
"ORDER BY event_date",
"\"\"\").show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"spark.sql(\"\"\"",
"SELECT *",
"FROM lakehouse.default.partition_demo",
"WHERE event_date = DATE '2024-01-01'",
"ORDER BY user_id",
"\"\"\").show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Часть 2. Эволюция схемы",
"",
"Теперь посмотрим на эволюцию схемы: создадим таблицу, добавим колонку и вставим новые строки с дополнительными данными.",
"",
"Для подготовки таблицы используем скрипт `src/spark/schema_evolution_demo.sql`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sql_path = Path(\"/opt/src/spark/schema_evolution_demo.sql\")",
"sql_text = sql_path.read_text(encoding=\"utf-8\")",
"",
"for statement in sql_text.split(\";\"):",
" stmt = statement.strip()",
" if stmt:",
" spark.sql(stmt)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"spark.sql(\"DESCRIBE TABLE lakehouse.default.schema_evolution_demo\").show(truncate=False)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"spark.sql(\"\"\"",
"SELECT *",
"FROM lakehouse.default.schema_evolution_demo",
"ORDER BY id",
"\"\"\").show(truncate=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Обрати внимание, что старые строки имеют `NULL` в колонке `metadata`, а новые — заполненное значение.",
"Iceberg хранит историю снапшотов и позволяет эволюцию схемы без сложных миграций."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-86
View File
@@ -1,86 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "ca5e5822-18cb-405e-90f3-a0f2f8ad2260",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Setting default log level to \"WARN\".\n",
"To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).\n",
"25/12/03 09:08:48 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n",
"25/12/03 09:08:52 WARN MetricsConfig: Cannot locate configuration: tried hadoop-metrics2-s3a-file-system.properties,hadoop-metrics2.properties\n",
" \r"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"+---+---+\n",
"| id|txt|\n",
"+---+---+\n",
"| 1| ok|\n",
"+---+---+\n",
"\n"
]
}
],
"source": [
"from pyspark.sql import SparkSession\n",
"\n",
"spark = (\n",
" SparkSession.builder\n",
" .appName(\"check\")\n",
" .master(\"spark://spark-master:7077\")\n",
" .getOrCreate()\n",
")\n",
"\n",
"spark.sql(\"\"\"\n",
" CREATE TABLE IF NOT EXISTS lakehouse.default.demo_fix (\n",
" id BIGINT,\n",
" txt STRING\n",
" )\n",
" USING iceberg\n",
"\"\"\")\n",
"\n",
"spark.sql(\"INSERT INTO lakehouse.default.demo_fix VALUES (1, 'ok')\")\n",
"\n",
"spark.sql(\"SELECT * FROM lakehouse.default.demo_fix\").show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1e125075-feab-4974-baa2-f787e41b46fd",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}