diff --git a/START_HERE.md b/START_HERE.md index 7bffd46..f4ad300 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -204,6 +204,6 @@ ls -lh data/nyc_taxi/ - пройти `notebooks/01_environment_and_smoke_test.ipynb`; - пройти `notebooks/02_lakehouse_mental_model.ipynb`; -- подготовить data bundle по инструкции выше и пройти [модуль 3](notebooks/03_raw_ingest_and_first_read.ipynb): сначала настроить Spark-сессию и проверить подключение, затем загрузить и прочитать raw-данные; +- подготовить data bundle по инструкции выше и пройти `notebooks/03_raw_ingest_and_first_read.ipynb`; - свериться с программой курса в `docs/course_program.md`; - после прохождения первых модулей переходить к следующим учебным материалам. diff --git a/docs/course_prd.md b/docs/course_prd.md index 7aa53e8..716c2e0 100644 --- a/docs/course_prd.md +++ b/docs/course_prd.md @@ -50,12 +50,11 @@ 1. Понимает различие между `storage`, `catalog` и `compute`, и может объяснить роль `MinIO`, `PostgreSQL`, `Spark` и `Trino` в этом стенде. 2. Умеет поднять локальный стенд, проверить его базовую работоспособность и использовать логи/веб-интерфейсы для первичной диагностики проблем. -3. Умеет загрузить raw-данные, прочитать их в `Spark`, проверить схему и подготовить данные к дальнейшей обработке. +3. Умеет настроить Spark-сессию для кластера, объяснить происхождение настроек и повторить подключение в новом ядре; загрузить raw-данные, прочитать их и проверить схему. 4. Умеет создать и заполнить `Iceberg`-таблицу как основной табличный формат курса. 5. Умеет реализовать простой поток `raw -> bronze -> silver` с базовыми трансформациями и сохранением воспроизводимости шагов. 6. Умеет читать одну и ту же таблицу из `Spark` и `Trino` и понимает, почему это возможно без копирования данных. 7. Умеет выполнить базовые безопасные операции с таблицей: изменение схемы, просмотр snapshot-ов, чтение предыдущего состояния, базовый `compaction` и `vacuum`. -8. Умеет создать `SparkSession` для кластера с подготовленным каталогом, проверить фактические настройки и объяснить, какие из них можно изменить в работающей сессии, а для каких нужен новый запуск. Трассировка этих outcomes на модули и checkpoints фиксируется в [course_program.md](./course_program.md). @@ -91,8 +90,7 @@ * один основной табличный формат: `Iceberg`; * работа с raw-данными; * построение слоев `bronze` и `silver`; -* чтение и запись через `Spark`; -* обязательная практика базовой настройки `SparkSession` в начале модуля 3: источники конфигурации, подключение к кластеру, проверка Iceberg/MinIO и жизненный цикл сессии; +* базовая настройка сессии, чтение и запись через `Spark`; * чтение через `Trino`; * базовое schema evolution; * snapshot/time travel на прикладном уровне; @@ -149,7 +147,6 @@ Это целевые ориентиры курса, но не blocking acceptance criteria для материалов. 1. Студент проходит курс за `~12-15 часов` вдумчивой работы. - Практика настройки сессии в модуле 3 рассчитана на 20–30 минут внутри этого диапазона. Это предварительная оценка: машинный прогон не измеряет время самостоятельной работы студента. 2. Студент после курса способен безопасно выполнить базовые операции в Lakehouse-стенде без постоянной внешней помощи. 3. Студент может внятно объяснить ключевые архитектурные принципы Lakehouse на уровне junior/middle interview readiness. @@ -158,8 +155,7 @@ 1. В составе курса есть стартовая документация до первого ноутбука, включая prerequisites, запуск стенда, вход в `Jupyter` и получение учебного data bundle. 2. Студент проходит обязательные `checkpoints` по урокам. 3. Студент самостоятельно выполняет набор практик на одном датасете, включая: - * создание Spark-сессии по шаблону и проверку подключения на отдельной маленькой Iceberg-таблице; - * загрузку raw-данных; + * подключение к Spark и загрузку raw-данных; * построение `bronze`; * построение `silver`; * создание/чтение `Iceberg`-таблицы; diff --git a/docs/course_program.md b/docs/course_program.md index 7c5fae0..9ce667f 100644 --- a/docs/course_program.md +++ b/docs/course_program.md @@ -10,8 +10,6 @@ **Ориентир на модуль:** в среднем `~1.5 часа`, но финальная практика может занять дольше. -В модуле 3 дополнительно предусмотрены 20–30 минут на настройку Spark-сессии; ориентир на весь модуль — около 2 часов. Общие `~12-15 часов` пока остаются предварительной оценкой: её нужно уточнить после самостоятельного прохождения студентом. - **Сквозной учебный кейс:** `NYC TLC Yellow Taxi Trip Records` + `Taxi Zone Lookup`. **Методика каждого урока:** `объяснение -> демонстрация -> самостоятельное повторение -> checkpoint`. @@ -71,12 +69,11 @@ | --- | --- | --- | | `LO1`. Понимание различий между `storage`, `catalog`, `compute` | 1, 2, 6 | checkpoints модулей 2 и 6 | | `LO2`. Умение поднять и диагностировать стенд | 1 | checkpoint модуля 1 | -| `LO3`. Умение загрузить raw-данные и проверить схему | 3 | checkpoint модуля 3 | +| `LO3`. Умение подключиться к Spark, загрузить raw-данные и проверить схему | 3 | checkpoint модуля 3 | | `LO4`. Умение создать и заполнить `Iceberg`-таблицу | 4 | checkpoint модуля 4 | | `LO5`. Умение собрать поток `raw -> bronze -> silver` | 4, 5, 8 | checkpoints модулей 5 и 8 | | `LO6`. Умение читать одну таблицу из `Spark` и `Trino` | 6, 8 | checkpoint модуля 6 и финальная практика | | `LO7`. Умение выполнять безопасные операции с таблицей | 7, 8 | checkpoints модулей 7 и 8 | -| `LO8`. Умение настроить Spark-сессию и проверить подключение | 3 | checkpoint практики настройки в начале модуля 3 | ## 5. Верхнеуровневая структура практической части @@ -127,35 +124,27 @@ **Рабочее название ноутбука:** `03_raw_ingest_and_first_read.ipynb` -**Зачем нужен:** научить создавать Spark-сессию для нашего кластера, затем загружать и читать raw-данные. +**Зачем нужен:** пройти путь от подключения к Spark до первого чтения и проверки исходных файлов Taxi. + +**Ориентир по времени:** около 2 часов, из них 20–30 минут на подготовку Spark-сессии. **Содержание:** -* обязательная практика «Как настроен Spark в нашем стенде» после ментальной модели модуля 2, до raw-ingest (20–30 минут); -* роли `SparkSession`, `SparkContext`, driver и executors; `master`, `appName`, `getOrCreate`, отличие `local` от кластера; -* настройки из `spark-defaults.conf`, параметров запуска и кода; подготовленный каталог Iceberg и доступ к MinIO; -* проверка фактического master и выбранных несекретных настроек; повторная ячейка, остановка сессии и перезапуск ядра; -* изменение часового пояса с восстановлением исходного значения и смена имени приложения при новом запуске; без подбора памяти, executors, shuffle или GC; -* работа с заранее подготовленным локальным data bundle из onboarding-документации; -* явная загрузка исходных `PARQUET`-файлов в raw-зону; -* чтение raw-данных через Spark; -* проверка схемы, типов и базового качества данных; -* обсуждение того, почему raw лучше не "чинить на месте" и почему датасет не должен скачиваться "из интернета из ноутбука". +* подключение к Spark-кластеру: сессия, контекст, driver и executors; настройки из файла, параметров запуска и кода; +* повторное использование сессии, остановка и перезапуск ядра; изменение настроек во время работы; +* подготовленный data bundle и загрузка исходных файлов в raw-зону MinIO; +* чтение через Spark, проверка схемы и типов; +* базовые метрики, аномалии и причины сохранять raw без изменений. **Практика студента:** -* выполнить демонстрацию настройки сессии, затем заполнить шаблон с адресом кластера и именем приложения, используя подготовленный каталог; -* проверить подключение на собственной маленькой Iceberg-таблице, прочитать две строки и удалить только эту таблицу; данные модуля 2 не нужны; -* объяснить происхождение настроек и повторить создание сессии после перезапуска ядра; -* проверить состав локально полученного набора `NYC Taxi`; -* загрузить его в raw-зону `MinIO`; -* прочитать несколько месяцев `NYC Taxi`; -* проверить схему и посчитать базовые метрики; -* найти простые аномалии или проблемные значения. +* заполнить шаблон сессии с подготовленным каталогом Iceberg; проверить подключение на собственной временной таблице и удалить ее; +* изменить и восстановить часовой пояс, сравнить повторный вызов builder-а с новым запуском приложения; +* проверить состав data bundle и загрузить несколько месяцев Taxi в MinIO; +* прочитать поездки и справочник зон, проверить схему и посчитать метрики; +* найти проблемные значения и объяснить, почему их сохраняют в raw. -**Checkpoint настройки:** master равен `spark://spark-master:7077`, имя приложения совпадает с заданным, из собственной Iceberg-таблицы прочитаны строки `0` и `1`, временная таблица удалена. Часовой пояс восстановлен. Студент объясняет, что задал в коде, что взял из стенда, где выполняются вычисления и какие ячейки повторит после перезапуска ядра. - -**Checkpoint raw-ingest:** студент умеет загрузить учебный набор в raw-зону, прочитать raw-данные, проверить схему и объяснить, почему raw-слой нужен как воспроизводимая точка входа. +**Checkpoint:** студент создает сессию для кластера, объясняет источники настроек и место выполнения заданий, умеет повторить подключение в новом ядре. Загружает учебный набор в raw-зону, читает его через Spark, проверяет схему и объясняет, зачем сохранять исходные данные. ### Модуль 4. Первая рабочая Iceberg-таблица и слой bronze @@ -269,12 +258,11 @@ 1. Поднимает локальный стенд и диагностирует типовые проблемы на старте. 2. Понимает различие между `storage`, `catalog` и `compute`. -3. Умеет загрузить raw-данные и прочитать их в Spark. +3. Подключается к Spark-кластеру, загружает raw-данные и проверяет их после чтения. 4. Умеет создать и заполнить Iceberg-таблицу. 5. Умеет построить простой поток `raw -> bronze -> silver`. 6. Умеет читать одну и ту же таблицу из `Spark` и `Trino`. 7. Умеет выполнять базовые безопасные операции: schema evolution, snapshots/time travel, compaction, vacuum. -8. Создаёт Spark-сессию для кластера, проверяет настройки и понимает, когда нужен новый запуск. ## 7. Что не входит в первую версию курса diff --git a/docs/glossary.md b/docs/glossary.md index 1af1833..d89dd22 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -58,7 +58,7 @@ Driver управляет приложением: выполняет код ноутбука, строит план и отправляет задания. Executors выполняют задания и читают данные. В нашем стенде driver работает в `jupyter`, executors запускаются на Spark worker-ах. Master выделяет приложению ресурсы worker-ов. -**Где в курсе:** [практика настройки в модуле 3](../notebooks/03_raw_ingest_and_first_read.ipynb). +**Где в курсе:** Модуль 3. ## Структура Iceberg-таблицы diff --git a/docs/stack_reference.md b/docs/stack_reference.md index 2723f10..8c21178 100644 --- a/docs/stack_reference.md +++ b/docs/stack_reference.md @@ -64,26 +64,10 @@ docker compose down -v или настройки в файле, перезапусти ядро ноутбука и снова выполни ячейки с импортами и созданием сессии. -Практика с объяснением, шаблоном и проверкой подключения находится в начале -[модуля 3](../notebooks/03_raw_ingest_and_first_read.ipynb). -В ней `spark.sparkContext.master` и `.appName` показывают фактический кластер -и имя приложения. Для SQL-настроек используется `spark.conf.get(...)`; -вывод ограничен конкретными несекретными ключами. - -В Spark 3.5.1 `spark.sql.session.timeZone` можно менять через -`spark.conf.set(...)` во время работы, сохранив и восстановив исходное значение. -Повторный builder с другим `appName` использует прежний контекст и не меняет -его фактическое имя. После `spark.stop()` новый контекст получает новое имя; -при смене master или общего файла конфигурации перезапусти ядро. -`stop()` оставляет переменные Python, перезапуск ядра удаляет их. -Оба действия сохраняют таблицы в каталоге и файлы MinIO. - -Параметры обычного нового приложения имеют приоритет: код → параметры -`spark-submit` → `spark-defaults.conf`. Настройки запуска driver-а нужно -передавать до его старта. Вне стенда также потребуются зависимости и конфигурация -Iceberg/S3A/JDBC из [`spark/Dockerfile`](../spark/Dockerfile) и общего файла. -Подробности: [конфигурация Spark 3.5.1](https://github.com/apache/spark/blob/v3.5.1/docs/configuration.md) -и [поведение SparkSession в этой версии](https://github.com/apache/spark/blob/v3.5.1/python/pyspark/sql/session.py). +Фактический master и имя приложения доступны через `spark.sparkContext.master` +и `.appName`. Текущие SQL-настройки можно прочитать через `spark.conf.get(...)`. +В Spark 3.5.1 `spark.sql.session.timeZone` меняется через `spark.conf.set(...)` +без остановки сессии. Для нового имени приложения нужен новый SparkContext. Пример запуска smoke-скрипта: diff --git a/notebooks/03_raw_ingest_and_first_read.ipynb b/notebooks/03_raw_ingest_and_first_read.ipynb index 0640657..8e0041f 100644 --- a/notebooks/03_raw_ingest_and_first_read.ipynb +++ b/notebooks/03_raw_ingest_and_first_read.ipynb @@ -7,16 +7,9 @@ "source": [ "# Модуль 3. Raw-данные и первое чтение в Spark\n", "\n", - "В этом модуле мы разбираем полный первый цикл работы с учебным датасетом в Lakehouse:\n", + "В прошлом модуле мы проследили, как Spark работает с каталогом и файлами в MinIO. Теперь загрузим настоящие данные: поездки NYC Taxi и справочник зон.\n", "\n", - "- создаём Spark-сессию для кластера и проверяем её настройки;\n", - "- проверяем заранее скачанный локальный data bundle;\n", - "- загружаем исходные файлы в raw-зону `MinIO`;\n", - "- читаем raw-данные через `Spark` по `s3a://` пути;\n", - "- смотрим схему, типы, базовые метрики и простые аномалии;\n", - "- фиксируем, почему raw-слой нужен как воспроизводимая неизменяемая точка входа.\n", - "\n", - "После этого ноутбука ты должен уметь объяснить, почему raw-данные не надо «чинить на месте» и почему их не стоит скачивать прямо из ноутбука." + "Сначала подготовим Spark-сессию и проверим подключение. Затем перенесем заранее скачанные файлы в raw-зону MinIO, прочитаем их через Spark и посмотрим на схему, пропуски и аномалии. Эти файлы останутся источником для таблиц, которые мы построим в следующих модулях.\n" ] }, { @@ -26,21 +19,9 @@ "source": [ "## 0. Перед стартом\n", "\n", - "Перед выполнением ноутбука:\n", - "\n", - "- подними стенд по `START_HERE.md`;\n", - "- пройди `notebooks/01_environment_and_smoke_test.ipynb`;\n", - "- пройди `notebooks/02_lakehouse_mental_model.ipynb`;\n", - "- начни с нового ядра, чтобы настройки не зависели от предыдущих опытов;\n", - "- заранее скачай data bundle в `./data/nyc_taxi` по инструкции из `START_HERE.md`.\n", - "\n", - "В классическом `PostgreSQL / Greenplum` данные часто уже лежат внутри системы. В Lakehouse путь более явный: сначала исходные файлы должны попасть в object storage, и только потом мы начинаем поверх них читать, профилировать и строить таблицы.\n", - "\n", - "| | Классический DWH | Lakehouse |\n", - "| --- | --- | --- |\n", - "| Где стартует работа | Данные уже внутри СУБД | Исходные файлы сначала попадают в storage |\n", - "| Первая практическая задача | `SELECT` из готовой таблицы | Загрузить raw и прочитать raw |\n", - "| Что важно не сломать | Таблицу / схему БД | Неизменяемый raw-слой |" + "- Подними стенд по `START_HERE.md` и пройди модули 1 и 2.\n", + "- Скачай data bundle в `./data/nyc_taxi` по инструкции из `START_HERE.md`.\n", + "- Открой этот ноутбук в новом ядре, чтобы начать без сессии от предыдущих опытов.\n" ] }, { @@ -48,27 +29,21 @@ "id": "session-intro", "metadata": {}, "source": [ - "## 0.1. Как настроен Spark в нашем стенде\n", + "## 1. Как настроен Spark в нашем стенде\n", "\n", - "Отведи на эту практику 20–30 минут. Сначала разберём готовый пример, затем ты заполнишь шаблон и проверишь подключение. Файлы Taxi понадобятся после практики.\n", + "До сих пор мы пользовались готовым объектом `spark`. Разберем, как его создать и куда он отправляет задания: это понадобится при чтении файлов Taxi.\n", "\n", - "`SparkSession` — объект `spark`, через который мы читаем DataFrame и выполняем SQL. Внутри него `SparkContext` связывает приложение с вычислительными ресурсами. В обычном ядре нашего ноутбука используется один работающий контекст.\n", + "`SparkSession` дает доступ к DataFrame и SQL. Его `SparkContext` связывает приложение с вычислительными ресурсами. В обычном ядре нашего ноутбука используется один работающий контекст.\n", "\n", - "| Участник | Что делает в этом стенде |\n", + "| Участник | Что делает в стенде |\n", "| --- | --- |\n", - "| Driver | Работает в контейнере `jupyter`: выполняет Python-код ноутбука, строит план и раздаёт задания |\n", + "| Driver | Работает в `jupyter`: выполняет Python-код, строит план и раздает задания |\n", "| Master | Принимает подключение по `spark://spark-master:7077` и выделяет ресурсы worker-ов |\n", - "| Executors | Запускаются на `spark-worker-1` и `spark-worker-2`, читают данные и выполняют задания |\n", + "| Executors | Работают на `spark-worker-1` и `spark-worker-2`, читают данные и выполняют задания |\n", "\n", - "В режиме `local[*]` Spark выполняет задания локально, используя потоки на машине driver-а. В нашем курсе нужен кластер: успешный `count()` сам по себе ещё не доказывает, что подключение правильное.\n", + "В режиме `local[*]` задания выполняются в потоках на машине driver-а. Нам нужен кластер, поэтому проверим фактический master вместе с результатом вычисления.\n", "\n", - "### Откуда берутся настройки\n", - "\n", - "Для новой сессии стенд читает `spark/spark-defaults.conf`, смонтированный в Jupyter как `/opt/spark/conf/spark-defaults.conf`. В нём уже заданы master, каталог и доступ к MinIO.\n", - "\n", - "При запуске приложения через `spark-submit` можно передать `--master`, `--name` и `--conf`. В коде похожие настройки задают методы builder-а: `.master(...)`, `.appName(...)`, `.config(...)`. Обычно приоритет такой: код → параметры запуска → файл defaults. Но уже запущенный контекст не перестраивается от нового вызова builder-а, а параметры запуска driver-а надо передавать до его старта.\n", - "\n", - "В нашем Jupyter дополнительные `--master`/`--conf` при запуске не передаются. Готовый пример ниже берёт master из файла, а имя задаёт в коде. Вне стенда понадобятся также библиотеки Iceberg/S3A/JDBC и конфигурация каталога: одного адреса master недостаточно. Пути и состав образа описаны в [справочнике стенда](../docs/stack_reference.md#spark)." + "Создадим сессию. `.appName(...)` задает имя приложения в Spark UI, `.getOrCreate()` создает сессию или возвращает уже работающую. Адрес кластера пока возьмем из общего файла настроек.\n" ] }, { @@ -99,19 +74,23 @@ "id": "session-config-explanation", "metadata": {}, "source": [ - "`appName` — имя приложения, видимое в Spark UI. `getOrCreate()` создаёт сессию, если её ещё нет, или возвращает существующую. `.master(...)` выбирает вычислитель; в демонстрации он не указан, поэтому используется общий файл.\n", + "### Откуда взялось подключение\n", "\n", - "### Что уже подготовлено для Iceberg и MinIO\n", + "Master мы не указали в коде: Spark прочитал его из `spark/spark-defaults.conf`. Docker подключает этот файл к Jupyter как `/opt/spark/conf/spark-defaults.conf`. Там же подготовлены каталог Iceberg и доступ к MinIO.\n", "\n", - "| Настройка в `spark-defaults.conf` | Назначение |\n", + "| Настройка в файле | Для чего нужна |\n", "| --- | --- |\n", "| `spark.sql.extensions` | Добавляет команды Iceberg к Spark SQL |\n", "| `spark.sql.catalog.lakehouse` | Подключает Spark-каталог с именем `lakehouse` |\n", "| `spark.sql.catalog.lakehouse.catalog-impl` и `.uri` | Выбирает JDBC-каталог в PostgreSQL, где зарегистрированы таблицы |\n", - "| `spark.sql.catalog.lakehouse.warehouse` | Задаёт место для файлов таблиц в MinIO: `s3a://lakehouse/warehouse` |\n", + "| `spark.sql.catalog.lakehouse.warehouse` | Задает место для файлов таблиц: `s3a://lakehouse/warehouse` |\n", "| `spark.hadoop.fs.s3a.endpoint` и `.path.style.access` | Направляют S3A-запросы в MinIO внутри Docker-сети |\n", "\n", - "В том же файле находятся настройки доступа к PostgreSQL и MinIO. Выведем только выбранные несекретные значения. SQL-настройки читаем через `spark.conf`, master и имя — через `spark.sparkContext`. При передаче настроек в Hadoop префикс `spark.hadoop.` снимается." + "При запуске через `spark-submit` настройки можно передать параметрами `--master`, `--name` и `--conf`. В коде для этого есть `.master(...)`, `.appName(...)` и `.config(...)`. Для нового приложения обычный приоритет: код -> параметры запуска -> файл defaults. Параметры запуска driver-а надо задать до его старта; уже работающий контекст новый builder не перестраивает.\n", + "\n", + "В нашем Jupyter дополнительные `--master` и `--conf` при запуске не передаются. Вне стенда потребуются также библиотеки Iceberg, S3A и JDBC: их состав и конфигурация описаны в [справочнике](../docs/stack_reference.md#spark).\n", + "\n", + "Посмотрим, какие значения получила сессия. В файле есть и учетные данные, поэтому выводим только выбранные несекретные настройки. Для SQL используем `spark.conf`; в Hadoop-настройках префикс `spark.hadoop.` уже снят.\n" ] }, { @@ -140,11 +119,11 @@ "id": "session-live-explanation", "metadata": {}, "source": [ - "### Что можно изменить во время работы\n", + "### Изменение настройки в работающей сессии\n", "\n", - "`spark.sql.session.timeZone` задаёт часовой пояс SQL-сессии. У одной и той же секунды Unix-времени будет разное отображение в UTC и Москве. Это настройка интерпретации времени, без изменения файлов данных.\n", + "Некоторые настройки можно менять, пока приложение работает. Например, `spark.sql.session.timeZone` определяет часовой пояс SQL-сессии. Посмотрим, как одна и та же секунда Unix-времени отображается в UTC и Москве.\n", "\n", - "Сохраним исходное значение и восстановим его в `finally`: оно вернётся даже при ошибке внутри опыта. Для Spark 3.5.1 эта настройка изменяется в работающей сессии." + "В Spark 3.5.1 эта настройка изменяется через `spark.conf.set(...)`. Сохраним исходное значение и вернем его в `finally`, в том числе при ошибке. Файлы данных этот опыт не меняет.\n" ] }, { @@ -174,11 +153,9 @@ "id": "session-reuse-explanation", "metadata": {}, "source": [ - "### Для чего нужен новый запуск\n", + "### Повторный вызов builder-а\n", "\n", - "Проверим `appName`: это имя приложения, которое запущено через `SparkContext`. Попробуем задать другое имя через builder и посмотрим на **фактическое** `spark.sparkContext.appName`.\n", - "\n", - "Повторное выполнение ячейки не останавливает приложение. `getOrCreate()` использует ту же сессию и тот же контекст; это не способ переключиться на другой master или переименовать уже запущенное приложение." + "Теперь попробуем сменить имя приложения на `module-03-raw-ingest-and-first-read`, с которым будем читать Taxi. Проверим имя у работающего контекста: `spark.sparkContext.appName`.\n" ] }, { @@ -206,13 +183,11 @@ "id": "session-stop-explanation", "metadata": {}, "source": [ - "Теперь остановим демонстрационную сессию. В следующем задании новый контекст получит новое имя.\n", + "Имя осталось прежним: `getOrCreate()` вернул ту же сессию с тем же контекстом. Повтор ячейки сохраняет и переменные Python, и работающее приложение.\n", "\n", - "- **Повторный запуск ячейки** сохраняет переменные Python и может использовать уже работающий контекст.\n", - "- **`spark.stop()`** останавливает сессию и её контекст. Переменные Python остаются, но старые Spark DataFrame больше нельзя использовать. Файлы MinIO и записи каталога сохраняются.\n", - "- **Перезапуск ядра** заново запускает Python и связанный с ним Spark driver. Переменные исчезают; нужно повторить импорты, создание сессии и чтение DataFrame. Данные в MinIO и каталоге от этого не удаляются.\n", + "Чтобы создать приложение с новым именем, вызовем `spark.stop()`. Этот вызов останавливает сессию и ее контекст. Переменные Python останутся, но старые Spark DataFrame использовать уже нельзя. Файлы MinIO и таблицы каталога сохранятся.\n", "\n", - "Для смены master или конфигурации файла начинай с нового ядра. Остановка сессии без перезапуска ядра достаточна для нашего опыта с именем, но не заменяет новый процесс для всех стартовых настроек." + "Для смены master или настроек в общем файле перезапускай ядро: одной остановки сессии недостаточно для всех параметров запуска. Перезапуск удалит переменные Python, и тогда придется повторить импорты, создание сессии и чтение DataFrame.\n" ] }, { @@ -230,17 +205,13 @@ "id": "session-exercise-explanation", "metadata": {}, "source": [ - "### Самостоятельно: создай сессию и проверь подключение\n", + "## 2. Подключись к Spark самостоятельно\n", "\n", - "Заполни три пропуска в следующей ячейке:\n", + "Создай сессию, с которой продолжишь работу с Taxi. Заполни в следующей ячейке адрес кластера, имя `module-03-raw-ingest-and-first-read` и цепочку `.master(...)`, `.appName(...)`, `.getOrCreate()`.\n", "\n", - "1. Адрес кластера внутри Docker-сети.\n", - "2. Имя приложения `module-03-raw-ingest-and-first-read`.\n", - "3. Вызов builder-а с `.master(...)`, `.appName(...)` и `.getOrCreate()`.\n", + "Настройки каталога возьми из общего файла. Адрес кластера в этот раз задай явно через `.master(...)`; остальные ноутбуки берут его из того же файла.\n", "\n", - "Используй подготовленный каталог из `spark-defaults.conf`: его параметры не нужно копировать в код. Явный `.master(...)` здесь нужен, чтобы ты самостоятельно указал подключение; остальные ноутбуки берут тот же адрес из файла.\n", - "\n", - "Шаблон остановится на `assert`, пока ты его не заполнишь. После заполнения выполни шаблон и ячейку проверки подключения дважды: имя и `applicationId` должны остаться теми же." + "Пока пропуски не заполнены, шаблон останавливается на `assert`. Выполни заполненный шаблон и проверку подключения дважды: имя и `applicationId` должны остаться теми же.\n" ] }, { @@ -269,7 +240,7 @@ "metadata": {}, "source": [ "
\n", - "Контрольное решение — открой после своей попытки\n", + "Контрольное решение (после своей попытки)\n", "\n", "```python\n", "from pyspark.sql import SparkSession\n", @@ -307,13 +278,13 @@ "id": "session-table-explanation", "metadata": {}, "source": [ - "### Проверь весь путь: Spark → каталог → MinIO\n", + "### Запись и чтение через каталог\n", "\n", - "Одного правильного master недостаточно: проверим запись и чтение через каталог. Ячейка сама создаёт маленькую Iceberg-таблицу с числами `0` и `1`, читает её и удаляет в `finally`. Таблицы модуля 2 не требуются.\n", + "Проверим, что сессия может работать с каталогом и MinIO. Следующая ячейка создаст собственную маленькую Iceberg-таблицу, прочитает из нее числа `0` и `1` и удалит ее в `finally`. Она работает и после удаления таблиц из модуля 2.\n", "\n", - "У таблицы уникальное имя. `DROP TABLE … PURGE` удаляет именно её запись в каталоге, файлы данных и метаданных. В MinIO могут остаться пустые маркеры каталогов `data/` и `metadata/`; это не данные таблицы. Пока ячейка работает, не перезапускай ядро: при принудительном завершении процесса `finally` не выполнится. Если это произошло, сохрани напечатанное имя для удаления этой таблицы после нового подключения.\n", + "У таблицы уникальное имя. `DROP TABLE ... PURGE` удаляет ее запись в каталоге, данные и метаданные; в MinIO могут остаться пустые маркеры `data/` и `metadata/`.\n", "\n", - "Это только проверка подключения; создание рабочей таблицы поездок изучим в модуле 4." + "Дождись завершения ячейки перед перезапуском ядра: при принудительной остановке процесса `finally` не выполнится. Если прервал ее, сохрани напечатанное имя, чтобы удалить эту таблицу после нового подключения.\n" ] }, { @@ -344,20 +315,13 @@ "id": "session-checkpoint", "metadata": {}, "source": [ - "### Checkpoint настройки\n", + "### Проверь запуск в новом ядре\n", "\n", - "Проверки выше должны подтвердить master `spark://spark-master:7077`, заданное имя приложения, строки `[0, 1]` и удаление временной таблицы. В опыте с часовым поясом исходное значение должно восстановиться.\n", + "В выводе должны быть master `spark://spark-master:7077`, заданное имя приложения, строки `[0, 1]` и сообщение об удалении таблицы.\n", "\n", - "Ответь своими словами:\n", + "Перезапусти ядро и повтори заполненный шаблон, проверку подключения и ячейку с таблицей. `applicationId` будет новым, остальные результаты должны совпасть. Импорт `SparkSession` уже есть в шаблоне, поэтому демонстрацию повторять не нужно.\n", "\n", - "1. Какие настройки ты задал в коде, а какие предоставил стенд? Где их найти?\n", - "2. Чем SparkSession отличается от SparkContext? Где работают driver и executors?\n", - "3. Почему повторный `getOrCreate()` не сменил имя демонстрационного приложения, а после `stop()` оно изменилось?\n", - "4. Что останется после перезапуска ядра и что потребуется выполнить заново?\n", - "\n", - "Теперь, когда временная таблица удалена, **перезапусти ядро**. Выполни заполненный шаблон, проверку подключения и ячейку с временной таблицей. Они должны работать без демонстрации и без состояния прошлого ядра; `applicationId` будет новым. Импорт `SparkSession` уже есть в шаблоне.\n", - "\n", - "После этой проверки продолжи ниже: подготовь переменные и MinIO-клиент, затем переходи к raw-данным. Сессию пока не останавливай." + "С этим подключением перейдем к файлам Taxi. Оставь сессию работать.\n" ] }, { @@ -365,9 +329,9 @@ "id": "raw-preparation", "metadata": {}, "source": [ - "## 0.2. Подготовка к raw-ingest\n", + "## 3. Проверка data bundle\n", "\n", - "Сессия из самостоятельного задания продолжает работать. Создадим MinIO-клиент и переменные для исходного сценария модуля." + "Подготовим MinIO-клиент для загрузки файлов и пути к данным. Docker подключает каталог `data/` с хоста к Jupyter как `/opt/data/`.\n" ] }, { @@ -460,15 +424,7 @@ "id": "d4d8f302", "metadata": {}, "source": [ - "## 1. Проверка data bundle\n", - "\n", - "Сначала убеждаемся, что нужные файлы уже лежат внутри Jupyter-контейнера по пути `/opt/data/nyc_taxi`.\n", - "\n", - "Почему не скачиваем данные прямо здесь:\n", - "\n", - "- так модуль остаётся воспроизводимым и не зависит от сетевого доступа внутри ноутбука;\n", - "- onboarding и подготовка окружения остаются в `START_HERE.md`, а практика в ноутбуке занимается только Lakehouse-операциями;\n", - "- студент видит границу ответственности: сначала подготовить bundle, потом работать с raw-слоем.\n" + "Файлы должны быть скачаны заранее по `START_HERE.md`. Здесь проверим состав набора, прежде чем отправлять его в MinIO. Так во время урока нам не понадобится внешняя сеть, а при повторном прохождении мы будем читать те же исходные файлы.\n" ] }, { @@ -523,7 +479,7 @@ "id": "dabd90f2", "metadata": {}, "source": [ - "## 2. Загрузка в raw-зону MinIO\n", + "## 4. Загрузка в raw-зону MinIO\n", "\n", "Raw-слой в Lakehouse похож на воспроизводимый входной `stg`-контур: мы сохраняем исходные файлы как есть, под оригинальными именами, не переписывая их содержимое.\n", "\n", @@ -621,7 +577,7 @@ "id": "696d6adc", "metadata": {}, "source": [ - "## 3. Первое чтение raw PARQUET через Spark\n", + "## 5. Первое чтение raw PARQUET через Spark\n", "\n", "Теперь читаем raw-файлы напрямую из `MinIO` по `s3a://` пути. Это и есть первая важная практика Lakehouse: вычислитель (`Spark`) работает поверх файлов в object storage, а не только поверх локальной файловой системы или таблиц внутри одной СУБД.\n", "\n", @@ -630,7 +586,7 @@ "- `spark.read.parquet(\"s3a://...\")` читает raw-файлы напрямую по физическому пути;\n", "- `spark.table(\"lakehouse....\")` читает уже зарегистрированную управляемую таблицу через каталог.\n", "\n", - "Проверочную `Iceberg`-таблицу из практики настройки мы уже удалили. Поездки пока лежат отдельными raw-файлами, поэтому читаем их через `spark.read.parquet(...)`. Рабочую таблицу поездок создадим в Модуле 4." + "Для проверки подключения мы читали маленькую таблицу по имени через `spark.table(...)`. Файлы Taxi пока не зарегистрированы в каталоге: прочитаем их по пути через `spark.read.parquet(...)`. Таблицу поездок создадим в модуле 4." ] }, { @@ -697,7 +653,7 @@ "id": "9701728a", "metadata": {}, "source": [ - "## 4. Схема, типы и null-значения\n", + "## 6. Схема, типы и null-значения\n", "\n", "Следующий шаг после первого чтения: понять, какие колонки пришли, какие у них типы и где уже на raw-слое встречаются `null`.\n", "\n", @@ -760,7 +716,7 @@ "id": "69e6b77e", "metadata": {}, "source": [ - "## 5. Базовые метрики и аномалии\n", + "## 7. Базовые метрики и аномалии\n", "\n", "Теперь быстро профилируем данные и ищем несколько очевидных проблемных паттернов:\n", "\n", @@ -879,7 +835,7 @@ "id": "095c9ac6", "metadata": {}, "source": [ - "## 6. Самостоятельное задание\n", + "## 8. Самостоятельное задание\n", "\n", "Используй raw-данные, которые уже лежат в `MinIO`:\n", "\n", @@ -937,7 +893,7 @@ "id": "e23dfe47", "metadata": {}, "source": [ - "## 7. Почему raw-слой важен\n", + "## 9. Почему raw-слой важен\n", "\n", "Raw-слой полезен не потому, что в нём удобно жить постоянно, а потому что он даёт устойчивую исходную точку. Если завтра поменяются правила очистки, дедупликации или нормализации, мы не обязаны заново скачивать источник и гадать, что именно уже успели «подправить». Мы просто перечитываем raw и перестраиваем downstream-слои.\n", "\n", @@ -953,16 +909,17 @@ "id": "8849c73f", "metadata": {}, "source": [ - "## 8. Checkpoint\n", + "## 10. Checkpoint\n", "\n", "Проверь себя:\n", "\n", - "1. Можешь ли ты показать файлы raw-зоны в `MinIO` через код или `MinIO Console`?\n", - "2. Сколько колонок в схеме `Yellow Taxi` и какие типы там встречаются?\n", - "3. Какие колонки содержат `null` и почему это не обязательно баг загрузки?\n", - "4. Зачем нужен raw-слой, если дальше всё равно будут трансформации?\n", - "5. Почему мы не скачиваем датасет прямо из ноутбука?\n", - "6. Что произойдёт, если источник завтра поменяет формат файлов?\n" + "1. К какому master подключена сессия? Какие настройки ты задал в коде, а какие она получила из общего файла?\n", + "2. Чем SparkSession отличается от SparkContext? Где работают driver и executors?\n", + "3. Почему часовой пояс изменился сразу, а для нового имени приложения понадобилось остановить сессию? Что придется повторить после перезапуска ядра?\n", + "4. Можешь ли ты показать загруженные raw-файлы в MinIO и прочитать их через Spark?\n", + "5. Сколько колонок в Yellow Taxi, какие у них типы и где встречаются `null`? Почему пропуски не обязательно означают ошибку загрузки?\n", + "6. Зачем сохранять raw-слой без изменений, если дальше будут трансформации?\n", + "7. Почему мы скачали датасет до запуска ноутбука? Что потребуется проверить, если источник изменит формат файлов?\n" ] }, { @@ -970,7 +927,7 @@ "id": "371c4c9d", "metadata": {}, "source": [ - "## 9. Завершение\n", + "## 11. Завершение\n", "\n", "Мы намеренно не удаляем raw-данные из `MinIO`. В Модуле 4 они понадобятся для построения первого управляемого `bronze`-слоя на `Iceberg`.\n" ] diff --git a/plans/course-roadmap.md b/plans/course-roadmap.md index 8313ddf..ee01ac1 100644 --- a/plans/course-roadmap.md +++ b/plans/course-roadmap.md @@ -36,28 +36,13 @@ Iceberg-таблицы. Результаты проверок и их огран ### 2. Добавить практику «Как настроен Spark в нашем стенде» -**Статус:** готово к ревью по #2; зависимость #1 закрыта, её итог прочитан. Практика добавлена в начало модуля 3, PRD, программа и справочные материалы обновлены. - -**Результат:** проверены демонстрация, заполненный шаблон и контрольное решение, повторные ячейки, настоящий перезапуск ядра, временная Iceberg-таблица и весь raw-ingest. Машинный прогон — 4 минуты 10 секунд; время самостоятельного прохождения студентом ещё не измерено. Подробности и ограничения — в [отчёте проверки](issue-2-spark-session-validation.md). +**Статус:** на ревью, [MR #4](https://git.dementev.space/ddmitry/mini-lakehouse-lab/pulls/4). **Задача:** [#2 — практика базовой настройки SparkSession](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2). -Практику разместить в начале модуля 3. Ориентир — 20–30 минут; фактическую длительность проверить при прохождении. +В модуле 3 студент готовит Spark-сессию, проверяет подключение и затем переходит к загрузке и чтению Taxi. Содержание и checkpoint описаны в [программе](../docs/course_program.md#модуль-3-raw-данные-и-первое-чтение-в-spark). -Студент должен научиться: - -- объяснять роли `SparkSession`, `SparkContext`, driver и executors на примере стенда; -- читать код с `master`, `appName` и `getOrCreate`; -- находить источник настройки: конфигурационный файл, параметры запуска или код; -- проверять фактический master и выбранные настройки каталога Iceberg и MinIO; -- различать настройки, меняемые во время работы, и настройки, требующие нового запуска; -- понимать, что повторное выполнение ячейки может использовать уже существующую сессию. - -**Практика и checkpoint:** студент заполняет небольшой шаблон создания сессии, задаёт подключение к кластеру и имя приложения, использует подготовленные настройки каталога, проверяет фактическое подключение и читает небольшую учебную Iceberg-таблицу. Таблица должна создаваться самой практикой: демонстрационные таблицы модуля 2 могут быть удалены при его завершении. Студент объясняет, какие настройки он задал сам, какие предоставил стенд и что потребуется повторить в новом ядре. - -При реализации обновить learning outcomes и scope в PRD, содержание и checkpoint модуля 3 в программе, ноутбук и связанные справочные материалы. Сохранить структуру из восьми модулей. - -**Граница темы:** базовая настройка подключения и жизненный цикл сессии. Подробный подбор памяти, числа executors, shuffle и GC остаётся за рамками этой доработки. +Код проверен на стенде, включая перезапуск ядра и полный raw-ingest. Замеры и ограничения проверки сохранены в [отчете](issue-2-spark-session-validation.md). ### 3. Собрать замечания по оставшейся части курса diff --git a/plans/issue-2-spark-session-validation.md b/plans/issue-2-spark-session-validation.md index 00e257e..20e9113 100644 --- a/plans/issue-2-spark-session-validation.md +++ b/plans/issue-2-spark-session-validation.md @@ -89,3 +89,7 @@ Hadoop S3A оставил шесть нулевых маркеров `data/` и - проверка ноутбука через `nbformat.validate` и синтаксиса Python-ячеек через AST; - относительные ссылки изменённых документов и ноутбука; - сохранение восьми модулей и маршрута README → START_HERE → программа → ноутбук. + +После редакторской правки по ревью повторно проверены ссылки, нумерация разделов +и синтаксис Python. Все исполняемые ячейки, их порядок и контрольное решение +совпадают с проверенной версией. Стенд повторно не запускался. diff --git a/plans/module-03-raw-ingest-and-first-read.md b/plans/module-03-raw-ingest-and-first-read.md index 64d21bb..618cc7d 100644 --- a/plans/module-03-raw-ingest-and-first-read.md +++ b/plans/module-03-raw-ingest-and-first-read.md @@ -1,23 +1,22 @@ # Модуль 3. Raw-данные и первое чтение в Spark -**Статус:** `Ready for review` — код проверен на стенде, время самостоятельного прохождения студентом пока не измерено +**Статус:** `Ready for review` **Последнее обновление:** `2026-09-15` -Дополнение: [#2 — практика настройки SparkSession](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2), после закрытой [#1](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/1). Новая практика входит в модуль 3; структура курса остаётся из восьми модулей. +**Задача:** [#2](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2). Результаты прогона и ограничения: [проверка #2](issue-2-spark-session-validation.md). ## Цель -Научить студента полному циклу работы с raw-данными в Lakehouse: доставка заранее скачанного PARQUET-датасета в raw-зону MinIO, чтение raw-данных через Spark, проверка схемы, типов и базового качества. Студент должен понять, зачем нужен raw-слой и почему raw-данные не следует «чинить на месте». +Научить студента подключаться к Spark-кластеру и работать с raw-данными: загружать заранее скачанный PARQUET-датасет в MinIO, читать его через Spark и проверять схему, типы и базовое качество. Студент должен понять, зачем нужен raw-слой и почему raw-данные не следует «чинить на месте». ## Результат для студента После прохождения модуля студент: -- создаёт Spark-сессию для кластера, различает настройки из кода, параметров запуска и общего файла; -- проверяет фактический master, имя приложения, настройки каталога и чтение собственной временной Iceberg-таблицы; -- объясняет повторное использование контекста, остановку сессии и перезапуск ядра; меняет часовой пояс с восстановлением исходного значения; +- создает сессию для кластера с подготовленным каталогом, проверяет настройки и понимает их источники; +- различает повторный вызов builder-а, остановку сессии и перезапуск ядра; - понимает, как локальный data bundle попадает в raw-зону MinIO; - умеет загружать PARQUET-файлы в MinIO программно через `boto3`; - умеет читать raw-данные из MinIO через Spark (`spark.read.parquet` по `s3a://` пути); @@ -88,63 +87,58 @@ volumes: ## Структура ноутбука -### Секция 0: Введение -- **[md]** Заголовок, цели модуля, prerequisite (data bundle из `START_HERE.md`). Таблица-сравнение: «Классический DWH: данные уже внутри СУБД» vs «Lakehouse: данные нужно явно доставить в storage». +### Секция 0: Перед стартом +- **[md]** Связь с модулем 2, цели работы с Taxi и условия старта: работающий стенд, скачанный data bundle, новое ядро. -### Секция 0.1: Как настроен Spark в нашем стенде +### Секция 1: Как настроен Spark в нашем стенде +- **[md]** SparkSession, SparkContext, driver и executors, local и кластер; источники настроек и подготовленный каталог. +- **[code]** Создание сессии, фактический master и несекретные настройки. Изменение часового пояса с восстановлением. Повторный builder сохраняет контекст; `stop()` позволяет создать приложение с новым именем. -- **Объяснение:** SparkSession, SparkContext, driver в Jupyter, executors на worker-ах; master, appName, getOrCreate; local и кластер. Источники настроек и подготовленный доступ к Iceberg/MinIO. -- **Демонстрация:** сессия с master из общего файла, вычисление суммы 0…9, вывод выбранных несекретных настроек. Изменение `spark.sql.session.timeZone` и восстановление в `finally`. -- **Новый запуск:** повторный builder с другим appName сохраняет фактическое имя контекста; после `stop()` новый контекст получает новое имя. -- **Самостоятельное повторение:** заполнить адрес кластера, имя приложения и builder; контрольное решение в раскрывающемся блоке. Повторить ячейку и проверить неизменный applicationId. -- **Проверка каталога:** создать собственную Iceberg-таблицу с уникальным именем, прочитать `[0, 1]`, удалить только её через `DROP TABLE … PURGE` в `finally`. Учебные таблицы других модулей не используются. -- **Checkpoint:** проверить master и appName, объяснить источники настроек и место вычислений. После удаления временной таблицы перезапустить ядро, повторить заполненный шаблон и проверку Iceberg, затем продолжить raw-ingest. -- **Длительность:** ориентир 20–30 минут; на модуль целиком около 2 часов. Замер машинного выполнения и ограничения проверки фиксируются отдельно от оценки времени студента. +### Секция 2: Подключись к Spark самостоятельно +- **[code]** Шаблон подключения и контрольное решение. Проверка master и appName, запись и чтение `[0, 1]` из собственной временной таблицы, удаление в `finally`. +- **[md]** После удаления таблицы перезапустить ядро, повторить заполненный шаблон и проверки. Оставить сессию для чтения Taxi. -### Секция 0.2: Подготовка к raw-ingest - -- Импорты, MinIO-клиент, пути и вспомогательные функции исходного урока. Сессия продолжает работать после checkpoint настройки. - -### Секция 1: Проверка data bundle +### Секция 3: Проверка data bundle +- **[code]** Импорты, MinIO-клиент, пути и вспомогательные функции для работы с файлами. - **[md]** Зачем проверяем; почему не скачиваем из интернета в ноутбуке (воспроизводимость, сетевые ограничения, разделение ответственности onboarding vs практика). -- **[code]** `os.listdir("/opt/data/nyc_taxi")`, вывод файлов с размерами, assert на наличие PARQUET-файлов и `taxi_zone_lookup.csv` — оба нужны для полного прохождения модуля (включая самостоятельное задание в Секции 6). Понятное сообщение с отсылкой к `START_HERE.md` при отсутствии любого из компонентов. +- **[code]** `os.listdir("/opt/data/nyc_taxi")`, вывод файлов с размерами, assert на наличие PARQUET-файлов и `taxi_zone_lookup.csv` — оба нужны для полного прохождения модуля (включая самостоятельное задание в Секции 8). Понятное сообщение с отсылкой к `START_HERE.md` при отсутствии любого из компонентов. -### Секция 2: Загрузка в raw-зону MinIO +### Секция 4: Загрузка в raw-зону MinIO - **[md]** Что такое raw-зона, параллель с stg-слоем в Greenplum, конвенция путей, почему raw не чинится «на месте». - **[code]** Демонстрация: перед загрузкой проверяем через `s3.head_object()`, существует ли уже объект с таким ключом. Три ветки: (1) объект не существует — загружаем через `boto3.upload_file()`; (2) объект существует и размер совпадает с локальным файлом — пропускаем с пометкой «already exists, skipping»; (3) объект существует, но размер отличается — **fail fast** с понятной ошибкой (raise), без перезаписи. Это обеспечивает идемпотентность и подкрепляет принцип «raw не перезаписывается». В markdown-пояснении перед ячейкой явно объяснить: повторный запуск безопасен, уже загруженные файлы не перетираются, а конфликт размеров сигнализирует о проблеме с данными. - **[code]** Верификация: `s3.list_objects_v2()` — листинг raw-зоны. - **[md]** Данные теперь в storage. Spark может читать их напрямую по `s3a://` пути; для Trino потребуется зарегистрированная таблица (это будет в Модулях 4 и 6). -### Секция 3: Первое чтение в Spark -- **[md]** Разница `spark.read.parquet("s3a://...")` (raw-файлы) vs `spark.table(...)` (управляемая Iceberg-таблица). Мы пока на этапе raw, Iceberg-таблица появится в Модуле 4. -- **[code]** Чтение через сессию, созданную в секции 0.1; повторная инициализация не нужна. +### Секция 5: Первое чтение в Spark +- **[md]** Разница `spark.read.parquet("s3a://...")` (raw-файлы) vs `spark.table(...)` (управляемая Iceberg-таблица). Проверочная таблица удалена; таблицу поездок создаем в Модуле 4. +- **[code]** Чтение через сессию, созданную в секции 2; повторная инициализация не нужна. - **[code]** Чтение одного файла: `spark.read.parquet("s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_2024-01.parquet")`. - **[code]** Чтение всех файлов wildcard: `yellow_tripdata_*.parquet` (без привязки к конкретному году в wildcard, чтобы код работал с любыми скачанными файлами). -### Секция 4: Проверка схемы +### Секция 6: Проверка схемы - **[md]** Зачем проверять схему: в классическом DWH схема задана DDL, в raw — вычитывается из файлов. Что может пойти не так: разные типы в разных файлах, неожиданные nullable-поля. - **[code]** `.printSchema()`. - **[code]** Колонки и типы в удобном формате (`.dtypes`). - **[code]** Null-анализ ключевых колонок (`VendorID`, `tpep_pickup_datetime`, `passenger_count`, `trip_distance`, `total_amount`) — количество и доля null в каждой. - **[md]** Обсуждение: null в raw — норма, не исправляем на месте. Очистка — задача bronze/silver слоев (Модули 4-5). -### Секция 5: Базовые метрики и аномалии +### Секция 7: Базовые метрики и аномалии - **[md]** Цель: быстро понять диапазоны значений, выявить очевидные проблемы. - **[code]** `.describe()` на числовых колонках. - **[code]** Поиск аномалий: отрицательные `fare_amount`, нулевая дистанция с ненулевой суммой, `total_amount > $1000`, pickup за пределами ожидаемого диапазона дат (определяется динамически из имён файлов, а не хардкодится). - **[md]** Аномалии в реальных данных — норма, а не баг загрузки. Задача raw — сохранить как есть. -### Секция 6: Самостоятельное задание +### Секция 8: Самостоятельное задание - **[md]** Инструкция: (1) прочитать `taxi_zone_lookup.csv` из raw-зоны через Spark, (2) вывести схему + 10 строк, (3) посчитать количество зон по borough, (4) найти топ-5 pickup locations и join с зонами, (5) найти ещё одну аномалию или интересный паттерн в данных. - **[code]** 4 пустых ячейки с `# Ваш код: ...`. -### Секция 7: Почему raw-слой важен +### Секция 9: Почему raw-слой важен - **[md]** Мини-лекция (3-5 абзацев): raw = неизменяемый архив; параллель с stg/ods в Greenplum; почему не скачиваем в ноутбуке; почему не чиним на месте (можно перестроить bronze/silver заново); в Lakehouse raw-файлы лежат в storage отдельно от compute — Spark читает их напрямую, а для Trino потребуется оформить данные в таблицу. -### Секция 8: Checkpoint -- **[md]** Вопросы для самопроверки: (1) показать файлы в raw-зоне MinIO, (2) какая схема у Yellow Taxi — сколько колонок, (3) какие колонки содержат null — это баг или норма, (4) зачем raw-слой, если данные потом трансформируются, (5) почему не скачиваем из ноутбука, (6) что произойдёт, если NYC TLC изменит формат файла. +### Секция 10: Checkpoint +- **[md]** Вопросы об источниках настроек, месте выполнения заданий и новом ядре; затем самопроверка по данным: (1) показать файлы в raw-зоне MinIO, (2) какая схема у Yellow Taxi — сколько колонок, (3) какие колонки содержат null — это баг или норма, (4) зачем raw-слой, если данные потом трансформируются, (5) почему не скачиваем из ноутбука, (6) что произойдёт, если NYC TLC изменит формат файла. -### Секция 9: Завершение +### Секция 11: Завершение - **[md]** «Мы намеренно не удаляем raw-данные из MinIO. В Модуле 4 мы будем использовать их для создания bronze-слоя.» - **[code]** `spark.stop()`. @@ -184,7 +178,7 @@ volumes: ## Риски - **Размер данных.** 3 месяца Yellow Taxi — примерно 120-150 MB (~40-50 MB на файл). Комфортно для стенда с 10 GB RAM. Extended (12 месяцев, ~600 MB) может замедлить Spark, но не должен вызывать падения. -- **Студент забыл скачать данные.** Assert в Секции 1 с понятным сообщением и ссылкой на `START_HERE.md`. +- **Студент забыл скачать данные.** Assert в Секции 3 с понятным сообщением и ссылкой на `START_HERE.md`. - **NYC TLC изменит формат/URL.** Ноутбук использует wildcard `yellow_tripdata_*.parquet` без привязки к году. Инструкция в `START_HERE.md` фиксирует конкретные файлы 2024-01..03, но если URL станут недоступны — потребуется обновление инструкции. - **Директория `data/` не существует.** Docker volume mount создаст пустую директорию, assert в ноутбуке поймает отсутствие файлов. - **Пересоздание контейнера.** Добавление volume в `docker-compose.yml` требует пересоздания контейнера `jupyter` (`docker compose up -d` достаточно — Docker пересоздаст только изменённый сервис). После этого bind mount уже работает: новые файлы в `./data` видны сразу без дополнительных перезапусков. Указать явно в инструкции.